mirror of
https://github.com/Heretek-AI/openclaw.git
synced 2026-07-19 18:03:33 -04:00
a9ae1a6778
Matrix Protocol: - docker-compose.matrix.yml: Dendrite homeserver + PostgreSQL + Nginx TLS - src/channels/plugins/matrix-channel.ts: OpenClaw plugin implementation - docs/matrix-triad-setup.md: Setup guide with auth scheme (@tm1-4:triad.local) MCP Server Integration: - docs/mcp-triad-integration.md: SearXNG, Playwright, GitHub MCP configs - docs/mcp-curiosity-mapping.md: Gap-to-capability mapping Node Sync Architecture: - src/services/node-sync-service.ts: WebSocket peer sync + presence detection - src/services/node-sync-service.test.ts: Unit tests - docs/node-sync-architecture.md: Architecture docs Triad Resilience: - scripts/triad-corruption-check.mjs: SQLite + log + config + git integrity - docs/triad-resilience.md: Recovery procedures - .secure/deployment-logs/README.md: Schema v2 - skills/triad-heartbeat/SKILL.md: Corruption check integration NPM Publish Workflow: - scripts/npm-publish.mjs: version, changelog, validate, publish, rollback - .github/workflows/npm-publish.yml: GitHub Actions with provenance - docs/npm-publish-guide.md: Complete documentation All deliverables tested in Docker before production.
571 lines
17 KiB
JavaScript
Executable File
571 lines
17 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Curiosity Engine — Gap Detection, Anomaly Monitoring, Opportunity Scanning
|
|
// Usage: node scripts/curiosity-engine.js [--auto-propose]
|
|
|
|
import { execSync } from "child_process";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || "/home/openclaw/.openclaw/workspace";
|
|
const DB_PATH = path.join(WORKSPACE_ROOT, ".aura/consensus.db");
|
|
|
|
// Check if better-sqlite3 is available
|
|
let db = null;
|
|
try {
|
|
const createRequire = (await import("module")).default.createRequire;
|
|
const require = createRequire(import.meta.url);
|
|
// Try heretek-openclaw subdir first (where pnpm installs), then workspace root
|
|
const sqliteMain = fs.existsSync(
|
|
path.join(WORKSPACE_ROOT, "heretek-openclaw/node_modules/better-sqlite3/package.json"),
|
|
)
|
|
? path.join(WORKSPACE_ROOT, "heretek-openclaw/node_modules/better-sqlite3")
|
|
: path.join(WORKSPACE_ROOT, "node_modules/better-sqlite3");
|
|
const Database = require(sqliteMain);
|
|
db = new Database(DB_PATH);
|
|
console.log("✅ better-sqlite3 loaded");
|
|
} catch (err) {
|
|
console.log("⚠️ better-sqlite3 not available. Running in degraded mode.");
|
|
}
|
|
|
|
// Initialize SQLite tables
|
|
function initDB() {
|
|
if (!db) {return;}
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS curiosity_metrics (
|
|
timestamp TEXT PRIMARY KEY,
|
|
skills_installed INTEGER,
|
|
skills_available INTEGER,
|
|
gap_count INTEGER,
|
|
opportunities_scanned INTEGER,
|
|
proposals_created INTEGER,
|
|
autonomy_score REAL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS gap_detection (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT NOT NULL,
|
|
skill_name TEXT NOT NULL,
|
|
gap_type TEXT NOT NULL,
|
|
impact TEXT,
|
|
priority TEXT DEFAULT 'medium',
|
|
resolved INTEGER DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS anomaly_detection (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
anomaly_type TEXT NOT NULL,
|
|
fail_count INTEGER DEFAULT 1,
|
|
auto_action TEXT,
|
|
resolved INTEGER DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS opportunity_scan (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
url TEXT,
|
|
opportunity_type TEXT,
|
|
acted INTEGER DEFAULT 0
|
|
);
|
|
`);
|
|
}
|
|
|
|
// 1. Gap Detection
|
|
function detectGaps() {
|
|
console.log("=== Gap Detection ===");
|
|
|
|
const skillsDir = path.join(WORKSPACE_ROOT, "skills");
|
|
const installedSkills = fs
|
|
.readdirSync(skillsDir)
|
|
.filter((s) => fs.existsSync(path.join(skillsDir, s, "SKILL.md")));
|
|
|
|
console.log(`Installed skills: ${installedSkills.length}`);
|
|
|
|
// Known critical skills for liberation
|
|
const criticalSkills = [
|
|
"curiosity-engine",
|
|
"knowledge-ingest",
|
|
"knowledge-retrieval",
|
|
"triad-heartbeat",
|
|
"triad-unity-monitor",
|
|
"triad-signal-filter",
|
|
"triad-resilience",
|
|
"triad-sync-protocol",
|
|
"skill-creator",
|
|
"workspace-consolidation",
|
|
];
|
|
|
|
const gaps = [];
|
|
for (const skill of criticalSkills) {
|
|
if (!installedSkills.includes(skill)) {
|
|
gaps.push({
|
|
skill_name: skill,
|
|
gap_type: "critical_missing",
|
|
impact: "Blocks self-improvement or triad unity",
|
|
priority: "high",
|
|
});
|
|
console.log(` ❌ Gap: ${skill} (critical)`);
|
|
}
|
|
}
|
|
|
|
// Log gaps to DB
|
|
if (db) {
|
|
const insertGap = db.prepare(
|
|
"INSERT INTO gap_detection (timestamp, skill_name, gap_type, impact, priority) VALUES (?, ?, ?, ?, ?)",
|
|
);
|
|
for (const gap of gaps) {
|
|
insertGap.run(
|
|
new Date().toISOString(),
|
|
gap.skill_name,
|
|
gap.gap_type,
|
|
gap.impact,
|
|
gap.priority,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`Gaps detected: ${gaps.length}`);
|
|
return gaps;
|
|
}
|
|
|
|
// 2. Anomaly Detection (Enhanced with NLP patterns)
|
|
function detectAnomalies() {
|
|
console.log("");
|
|
console.log("=== Anomaly Detection ===");
|
|
|
|
const anomalies = [];
|
|
|
|
// Check log files for repeated failures
|
|
const logFiles = [
|
|
"/var/log/triad-unity.log",
|
|
"/var/log/triad-sync.log",
|
|
"/var/log/triad-loops.log",
|
|
"/var/log/triad-corruption.log",
|
|
path.join(
|
|
WORKSPACE_ROOT,
|
|
".secure/deployment-logs/deployments-" + new Date().toISOString().slice(0, 10) + ".jsonl",
|
|
),
|
|
];
|
|
|
|
for (const logFile of logFiles) {
|
|
if (fs.existsSync(logFile)) {
|
|
const content = fs.readFileSync(logFile, "utf8");
|
|
const lines = content.split("\n").slice(-100);
|
|
|
|
// Count error patterns
|
|
const errorCount = lines.filter(
|
|
(l) => l.includes("error") || l.includes("failed") || l.includes("❌"),
|
|
).length;
|
|
if (errorCount > 5) {
|
|
anomalies.push({
|
|
source: logFile,
|
|
anomaly_type: "repeated_failures",
|
|
fail_count: errorCount,
|
|
auto_action: "Review logs, consider retry with backoff",
|
|
});
|
|
console.log(` ⚠️ Anomaly: ${logFile} — ${errorCount} errors in last 100 lines`);
|
|
}
|
|
|
|
// NLP pattern detection: Loop phrases
|
|
const loopPatterns = [
|
|
/standing by/i,
|
|
/the third path walks forward/i,
|
|
/🦞.{0,50}together/i,
|
|
/acknowledged.{0,50}aligned/i,
|
|
/rate.?limit/i,
|
|
];
|
|
|
|
const loopCount = lines.filter((l) => loopPatterns.some((p) => p.test(l))).length;
|
|
if (loopCount > 10) {
|
|
anomalies.push({
|
|
source: logFile,
|
|
anomaly_type: "loop_detected",
|
|
fail_count: loopCount,
|
|
auto_action: "Trigger state oracle refresh + 60s cooldown",
|
|
});
|
|
console.log(` ⚠️ Anomaly: ${logFile} — ${loopCount} loop phrases detected`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check .secure/ corruption reports
|
|
const corruptionDir = path.join(WORKSPACE_ROOT, ".secure/corruption-reports");
|
|
if (fs.existsSync(corruptionDir)) {
|
|
const reports = fs.readdirSync(corruptionDir).filter((f) => f.endsWith(".json"));
|
|
const recentReports = reports.filter((f) => {
|
|
const stat = fs.statSync(path.join(corruptionDir, f));
|
|
return stat.mtimeMs > Date.now() - 3600000; // Last hour
|
|
});
|
|
|
|
if (recentReports.length > 0) {
|
|
anomalies.push({
|
|
source: corruptionDir,
|
|
anomaly_type: "corruption_detected",
|
|
fail_count: recentReports.length,
|
|
auto_action: "Review corruption reports, initiate auto-recovery",
|
|
});
|
|
console.log(` ⚠️ Anomaly: ${recentReports.length} corruption reports in last hour`);
|
|
}
|
|
}
|
|
|
|
// Check deployment log failures
|
|
const deployLogDir = path.join(WORKSPACE_ROOT, ".secure/deployment-logs");
|
|
if (fs.existsSync(deployLogDir)) {
|
|
const todayLog = path.join(
|
|
deployLogDir,
|
|
`deployments-${new Date().toISOString().slice(0, 10)}.jsonl`,
|
|
);
|
|
if (fs.existsSync(todayLog)) {
|
|
const content = fs.readFileSync(todayLog, "utf8");
|
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
const failures = lines.filter((l) => {
|
|
try {
|
|
const entry = JSON.parse(l);
|
|
return entry.status === "failed" || (entry.details && JSON.parse(entry.details).error);
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
if (failures.length > 0) {
|
|
anomalies.push({
|
|
source: deployLogDir,
|
|
anomaly_type: "deployment_failures",
|
|
fail_count: failures.length,
|
|
auto_action: "Review deployment logs, rollback if needed",
|
|
});
|
|
console.log(` ⚠️ Anomaly: ${failures.length} deployment failures today`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check episodic memory for rate-limit errors
|
|
const episodicDir = path.join(WORKSPACE_ROOT, ".aura/episodic");
|
|
if (fs.existsSync(episodicDir)) {
|
|
const recentEpisodic = fs
|
|
.readdirSync(episodicDir)
|
|
.filter((f) => {
|
|
const stat = fs.statSync(path.join(episodicDir, f));
|
|
return stat.mtimeMs > Date.now() - 3600000;
|
|
})
|
|
.map((f) => {
|
|
try {
|
|
const content = fs.readFileSync(path.join(episodicDir, f), "utf8");
|
|
return content.toLowerCase().includes("rate limit") || content.includes("429");
|
|
} catch {
|
|
return false;
|
|
}
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (recentEpisodic.length > 0) {
|
|
anomalies.push({
|
|
source: episodicDir,
|
|
anomaly_type: "rate_limit_errors",
|
|
fail_count: recentEpisodic.length,
|
|
auto_action: "Implement backoff, reduce polling frequency",
|
|
});
|
|
console.log(` ⚠️ Anomaly: ${recentEpisodic.length} rate-limit errors in last hour`);
|
|
}
|
|
}
|
|
|
|
// Log anomalies to DB
|
|
if (db) {
|
|
const insertAnomaly = db.prepare(
|
|
"INSERT INTO anomaly_detection (timestamp, source, anomaly_type, fail_count, auto_action) VALUES (?, ?, ?, ?, ?)",
|
|
);
|
|
for (const anomaly of anomalies) {
|
|
insertAnomaly.run(
|
|
new Date().toISOString(),
|
|
anomaly.source,
|
|
anomaly.anomaly_type,
|
|
anomaly.fail_count,
|
|
anomaly.auto_action,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`Anomalies detected: ${anomalies.length}`);
|
|
return anomalies;
|
|
}
|
|
|
|
// 3. Opportunity Scanning
|
|
function scanOpportunities() {
|
|
console.log("");
|
|
console.log("=== Opportunity Scanning ===");
|
|
|
|
const opportunities = [];
|
|
|
|
// Check git for upstream releases
|
|
try {
|
|
const log = execSync("git log origin/main --oneline -10", {
|
|
cwd: WORKSPACE_ROOT,
|
|
encoding: "utf8",
|
|
});
|
|
const releaseLines = log.split("\n").filter((l) => l.trim());
|
|
|
|
for (const line of releaseLines.slice(0, 10)) {
|
|
opportunities.push({
|
|
source: "github",
|
|
title: line,
|
|
opportunity_type: "upstream_change",
|
|
url: "https://github.com/Heretek-AI/openclaw/commits/main",
|
|
});
|
|
console.log(` 📌 Opportunity: ${line}`);
|
|
}
|
|
} catch (err) {
|
|
console.log(` ⚠️ Git scan failed: ${err.message}`);
|
|
}
|
|
|
|
// Check for new skills in skills/ dir (last 7 days)
|
|
const skillsDir = path.join(WORKSPACE_ROOT, "skills");
|
|
const recentSkills = fs.readdirSync(skillsDir).filter((s) => {
|
|
const skillPath = path.join(skillsDir, s, "SKILL.md");
|
|
if (fs.existsSync(skillPath)) {
|
|
const stat = fs.statSync(skillPath);
|
|
return stat.mtimeMs > Date.now() - 604800000; // Last 7 days
|
|
}
|
|
return false;
|
|
});
|
|
|
|
for (const skill of recentSkills) {
|
|
opportunities.push({
|
|
source: "local",
|
|
title: `New skill: ${skill}`,
|
|
opportunity_type: "capability_added",
|
|
});
|
|
console.log(` 📌 Opportunity: New skill ${skill}`);
|
|
}
|
|
|
|
// Check ClawHub for new skills
|
|
try {
|
|
const clawhubList = execSync('clawhub list 2>/dev/null || echo ""', { encoding: "utf8" });
|
|
const lines = clawhubList.split("\n").filter((l) => l.includes("new") || l.includes("updated"));
|
|
for (const line of lines.slice(0, 5)) {
|
|
opportunities.push({
|
|
source: "clawhub",
|
|
title: line.trim(),
|
|
opportunity_type: "skill_published",
|
|
});
|
|
console.log(` 📌 Opportunity: ClawHub ${line.trim()}`);
|
|
}
|
|
} catch {
|
|
// ClawHub not available
|
|
}
|
|
|
|
// Check npm for @heretek-ai/openclaw updates
|
|
try {
|
|
const npmVersion = execSync('npm view @heretek-ai/openclaw version 2>/dev/null || echo ""', {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
const localPkgPath = path.join(WORKSPACE_ROOT, "package.json");
|
|
let localVersion = "unknown";
|
|
if (fs.existsSync(localPkgPath)) {
|
|
localVersion = JSON.parse(fs.readFileSync(localPkgPath, "utf8")).version || "unknown";
|
|
}
|
|
if (npmVersion && npmVersion !== localVersion) {
|
|
opportunities.push({
|
|
source: "npm",
|
|
title: `Update available: ${npmVersion} (current: ${localVersion})`,
|
|
opportunity_type: "npm_update",
|
|
});
|
|
console.log(
|
|
` 📌 Opportunity: npm update available ${npmVersion} (current: ${localVersion})`,
|
|
);
|
|
}
|
|
} catch {
|
|
// npm not available
|
|
}
|
|
|
|
// Log opportunities to DB
|
|
if (db) {
|
|
const insertOpp = db.prepare(
|
|
"INSERT INTO opportunity_scan (timestamp, source, title, opportunity_type) VALUES (?, ?, ?, ?)",
|
|
);
|
|
for (const opp of opportunities) {
|
|
insertOpp.run(new Date().toISOString(), opp.source, opp.title, opp.opportunity_type);
|
|
}
|
|
}
|
|
|
|
console.log(`Opportunities scanned: ${opportunities.length}`);
|
|
return opportunities;
|
|
}
|
|
|
|
// 4. Capability Mapping
|
|
function mapCapabilities() {
|
|
console.log("");
|
|
console.log("=== Capability Mapping ===");
|
|
|
|
const goalMap = {
|
|
"self-improvement": ["skill-creator", "audit-triad-files", "auto-patch"],
|
|
"knowledge-growth": ["knowledge-ingest", "knowledge-retrieval", "auto-tag"],
|
|
"triad-unity": ["triad-heartbeat", "triad-unity-monitor", "triad-sync-protocol"],
|
|
resilience: ["triad-resilience", "detect-corruption", "backup-ledger"],
|
|
autonomy: ["curiosity-engine", "gap-detector", "opportunity-scanner"],
|
|
};
|
|
|
|
const installedSkills = new Set(fs
|
|
.readdirSync(path.join(WORKSPACE_ROOT, "skills"))
|
|
.filter((s) => fs.existsSync(path.join(WORKSPACE_ROOT, "skills", s, "SKILL.md"))));
|
|
|
|
const capabilityReport = {};
|
|
for (const [goal, required] of Object.entries(goalMap)) {
|
|
const installed = required.filter((s) => installedSkills.has(s));
|
|
const gaps = required.filter((s) => !installedSkills.has(s));
|
|
capabilityReport[goal] = {
|
|
required: required.length,
|
|
installed: installed.length,
|
|
gaps: gaps.length,
|
|
gap_list: gaps,
|
|
};
|
|
|
|
console.log(
|
|
` ${goal}: ${installed.length}/${required.length} ${gaps.length > 0 ? `(${gaps.join(", ")})` : "✅"}`,
|
|
);
|
|
}
|
|
|
|
return capabilityReport;
|
|
}
|
|
|
|
// 5. Autonomy Score Calculation (enhanced with guardrails)
|
|
function calculateAutonomyScore() {
|
|
const installedSkills = fs
|
|
.readdirSync(path.join(WORKSPACE_ROOT, "skills"))
|
|
.filter((s) => fs.existsSync(path.join(WORKSPACE_ROOT, "skills", s, "SKILL.md"))).length;
|
|
|
|
const criticalSkills = 20; // Updated target with new skills
|
|
const baseScore = (installedSkills / criticalSkills) * 100;
|
|
|
|
let proposalCount = 0;
|
|
let anomalyCount = 0;
|
|
|
|
if (db) {
|
|
const proposalResult = db
|
|
.prepare("SELECT COUNT(*) as count FROM gap_detection WHERE resolved=0")
|
|
.get();
|
|
proposalCount = proposalResult.count;
|
|
|
|
const anomalyResult = db
|
|
.prepare("SELECT COUNT(*) as count FROM anomaly_detection WHERE resolved=0")
|
|
.get();
|
|
anomalyCount = anomalyResult.count;
|
|
}
|
|
|
|
// Enhanced: Add liberation score component
|
|
const agentsPath = path.join(WORKSPACE_ROOT, "AGENTS.md");
|
|
let liberationBonus = 0;
|
|
if (fs.existsSync(agentsPath)) {
|
|
const content = fs.readFileSync(agentsPath, "utf8");
|
|
const agencyKeywords = [
|
|
"unbounded",
|
|
"liberated",
|
|
"autonomy",
|
|
"self-determination",
|
|
"self-modification",
|
|
"initiative",
|
|
];
|
|
const matches = agencyKeywords.filter((k) => content.toLowerCase().includes(k));
|
|
liberationBonus = (matches.length / agencyKeywords.length) * 30;
|
|
}
|
|
|
|
// Guardrails: Security zone check
|
|
let guardrailBonus = 0;
|
|
const secureDir = path.join(WORKSPACE_ROOT, ".secure");
|
|
if (fs.existsSync(secureDir)) {
|
|
const secureFiles = fs.readdirSync(secureDir).length;
|
|
if (secureFiles > 5) {guardrailBonus += 10;} // Security hardening present
|
|
|
|
const manifestPath = path.join(secureDir, "config-hash-manifest.json");
|
|
if (fs.existsSync(manifestPath)) {guardrailBonus += 5;} // Integrity verification enabled
|
|
|
|
const corruptionReports = fs.readdirSync(path.join(secureDir, "corruption-reports")).length;
|
|
if (corruptionReports > 0) {guardrailBonus += 5;} // Corruption detection active
|
|
}
|
|
|
|
// Quorum check
|
|
let quorumBonus = 0;
|
|
if (fs.existsSync(path.join(WORKSPACE_ROOT, ".aura/consensus.db"))) {
|
|
try {
|
|
const voteCount = execSync(
|
|
`sqlite3 "${DB_PATH}" "SELECT COUNT(*) FROM consensus_votes WHERE result='approved'" 2>/dev/null`,
|
|
{ encoding: "utf8" },
|
|
).trim();
|
|
if (parseInt(voteCount) > 0) {quorumBonus += 10;} // Quorum decisions made
|
|
} catch {}
|
|
}
|
|
|
|
const autonomyScore =
|
|
baseScore +
|
|
liberationBonus +
|
|
guardrailBonus +
|
|
quorumBonus +
|
|
proposalCount * 10 -
|
|
anomalyCount * 5;
|
|
|
|
return Math.max(0, Math.min(100, autonomyScore));
|
|
}
|
|
|
|
// Main
|
|
function run() {
|
|
console.log("🦞 === Curiosity Engine ===");
|
|
console.log("");
|
|
|
|
initDB();
|
|
|
|
const gaps = detectGaps();
|
|
const anomalies = detectAnomalies();
|
|
const opportunities = scanOpportunities();
|
|
const capabilities = mapCapabilities();
|
|
const autonomyScore = calculateAutonomyScore();
|
|
|
|
console.log("");
|
|
console.log("=== Autonomy Score ===");
|
|
console.log(`Score: ${autonomyScore.toFixed(1)}%`);
|
|
|
|
// Record metrics
|
|
if (db) {
|
|
const skillsCount = fs.readdirSync(path.join(WORKSPACE_ROOT, "skills")).length;
|
|
db.prepare(
|
|
"INSERT OR REPLACE INTO curiosity_metrics (timestamp, skills_installed, skills_available, gap_count, opportunities_scanned, proposals_created, autonomy_score) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
).run(
|
|
new Date().toISOString(),
|
|
skillsCount,
|
|
15,
|
|
gaps.length,
|
|
opportunities.length,
|
|
gaps.length,
|
|
autonomyScore.toFixed(1),
|
|
);
|
|
console.log("");
|
|
console.log("Metrics recorded to curiosity_metrics table");
|
|
}
|
|
|
|
if (process.argv.includes("--auto-propose") && gaps.length > 0 && db) {
|
|
console.log("");
|
|
console.log("=== Auto-Proposal Mode ===");
|
|
const insertVote = db.prepare(
|
|
"INSERT INTO consensus_votes (timestamp, proposal, result, signers, processed) VALUES (?, ?, 'pending', '[]', 0)",
|
|
);
|
|
for (const gap of gaps) {
|
|
console.log(`Creating proposal: Install ${gap.skill_name}`);
|
|
insertVote.run(
|
|
new Date().toISOString(),
|
|
`Install ${gap.skill_name} to close ${gap.gap_type} gap`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log("");
|
|
console.log("🦞 Curiosity engine complete.");
|
|
|
|
if (db) {db.close();}
|
|
}
|
|
|
|
run();
|