chainlesschain 0.37.8 → 0.37.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +403 -8
- package/bin/chainlesschain.js +4 -0
- package/package.json +7 -2
- package/src/commands/agent.js +30 -0
- package/src/commands/ask.js +114 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/chat.js +35 -0
- package/src/commands/db.js +152 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +288 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +489 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +479 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +65 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/platform.js +15 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/repl/agent-repl.js +912 -0
- package/src/repl/chat-repl.js +262 -0
- package/src/runtime/bootstrap.js +159 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instinct Manager — learns user preferences from agent interactions.
|
|
3
|
+
* Tracks patterns like preferred tools, coding style, response format, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensure instincts table exists.
|
|
8
|
+
*/
|
|
9
|
+
export function ensureInstinctsTable(db) {
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS instincts (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
category TEXT NOT NULL,
|
|
14
|
+
pattern TEXT NOT NULL,
|
|
15
|
+
confidence REAL DEFAULT 0.5,
|
|
16
|
+
occurrences INTEGER DEFAULT 1,
|
|
17
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
18
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateId() {
|
|
24
|
+
const hex = () =>
|
|
25
|
+
Math.floor(Math.random() * 0x10000)
|
|
26
|
+
.toString(16)
|
|
27
|
+
.padStart(4, "0");
|
|
28
|
+
return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Instinct categories.
|
|
33
|
+
*/
|
|
34
|
+
export const INSTINCT_CATEGORIES = {
|
|
35
|
+
TOOL_PREFERENCE: "tool_preference",
|
|
36
|
+
CODING_STYLE: "coding_style",
|
|
37
|
+
RESPONSE_FORMAT: "response_format",
|
|
38
|
+
LANGUAGE: "language",
|
|
39
|
+
WORKFLOW: "workflow",
|
|
40
|
+
BEHAVIOR: "behavior",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Record an instinct observation.
|
|
45
|
+
* If an instinct with the same category+pattern exists, increment its confidence and occurrences.
|
|
46
|
+
*/
|
|
47
|
+
export function recordInstinct(db, category, pattern) {
|
|
48
|
+
ensureInstinctsTable(db);
|
|
49
|
+
|
|
50
|
+
// Check if exists
|
|
51
|
+
const existing = db
|
|
52
|
+
.prepare("SELECT * FROM instincts WHERE category = ? AND pattern = ?")
|
|
53
|
+
.get(category, pattern);
|
|
54
|
+
|
|
55
|
+
if (existing) {
|
|
56
|
+
// Boost confidence (asymptotic approach to 1.0)
|
|
57
|
+
const newConfidence = Math.min(
|
|
58
|
+
0.99,
|
|
59
|
+
existing.confidence + (1 - existing.confidence) * 0.1,
|
|
60
|
+
);
|
|
61
|
+
db.prepare(
|
|
62
|
+
"UPDATE instincts SET confidence = ?, occurrences = occurrences + 1, last_seen = datetime('now') WHERE id = ?",
|
|
63
|
+
).run(newConfidence, existing.id);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
id: existing.id,
|
|
67
|
+
category,
|
|
68
|
+
pattern,
|
|
69
|
+
confidence: newConfidence,
|
|
70
|
+
occurrences: (existing.occurrences || 1) + 1,
|
|
71
|
+
isNew: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create new instinct
|
|
76
|
+
const id = generateId();
|
|
77
|
+
db.prepare(
|
|
78
|
+
"INSERT INTO instincts (id, category, pattern, confidence, occurrences) VALUES (?, ?, ?, ?, ?)",
|
|
79
|
+
).run(id, category, pattern, 0.5, 1);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
category,
|
|
84
|
+
pattern,
|
|
85
|
+
confidence: 0.5,
|
|
86
|
+
occurrences: 1,
|
|
87
|
+
isNew: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all instincts, optionally filtered by category.
|
|
93
|
+
*/
|
|
94
|
+
export function getInstincts(db, options = {}) {
|
|
95
|
+
ensureInstinctsTable(db);
|
|
96
|
+
|
|
97
|
+
let sql = "SELECT * FROM instincts";
|
|
98
|
+
const params = [];
|
|
99
|
+
|
|
100
|
+
if (options.category) {
|
|
101
|
+
sql += " WHERE category = ?";
|
|
102
|
+
params.push(options.category);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sql += " ORDER BY confidence DESC";
|
|
106
|
+
|
|
107
|
+
if (options.limit) {
|
|
108
|
+
sql += " LIMIT ?";
|
|
109
|
+
params.push(options.limit);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return db.prepare(sql).all(...params);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get top instincts (confidence >= threshold).
|
|
117
|
+
*/
|
|
118
|
+
export function getStrongInstincts(db, threshold = 0.7) {
|
|
119
|
+
ensureInstinctsTable(db);
|
|
120
|
+
return db
|
|
121
|
+
.prepare(
|
|
122
|
+
"SELECT * FROM instincts WHERE confidence >= ? ORDER BY confidence DESC",
|
|
123
|
+
)
|
|
124
|
+
.all(threshold);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Delete an instinct by ID or prefix.
|
|
129
|
+
*/
|
|
130
|
+
export function deleteInstinct(db, id) {
|
|
131
|
+
ensureInstinctsTable(db);
|
|
132
|
+
const result = db
|
|
133
|
+
.prepare("DELETE FROM instincts WHERE id LIKE ?")
|
|
134
|
+
.run(`${id}%`);
|
|
135
|
+
return result.changes > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reset all instincts (clear the table).
|
|
140
|
+
*/
|
|
141
|
+
export function resetInstincts(db) {
|
|
142
|
+
ensureInstinctsTable(db);
|
|
143
|
+
const result = db.prepare("DELETE FROM instincts WHERE 1=1").run();
|
|
144
|
+
return result.changes;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Decay instincts that haven't been seen recently.
|
|
149
|
+
* Reduces confidence of old instincts over time.
|
|
150
|
+
*/
|
|
151
|
+
export function decayInstincts(db, daysThreshold = 30) {
|
|
152
|
+
ensureInstinctsTable(db);
|
|
153
|
+
// Simple decay: multiply confidence by 0.9 for old instincts
|
|
154
|
+
const rows = db.prepare("SELECT * FROM instincts").all();
|
|
155
|
+
let decayed = 0;
|
|
156
|
+
|
|
157
|
+
const cutoff = new Date();
|
|
158
|
+
cutoff.setDate(cutoff.getDate() - daysThreshold);
|
|
159
|
+
const cutoffStr = cutoff.toISOString().replace("T", " ").slice(0, 19);
|
|
160
|
+
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
if (row.last_seen && row.last_seen < cutoffStr) {
|
|
163
|
+
const newConfidence = Math.max(0.1, (row.confidence || 0.5) * 0.9);
|
|
164
|
+
db.prepare("UPDATE instincts SET confidence = ? WHERE id = ?").run(
|
|
165
|
+
newConfidence,
|
|
166
|
+
row.id,
|
|
167
|
+
);
|
|
168
|
+
decayed++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return decayed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate a system prompt fragment from strong instincts.
|
|
177
|
+
*/
|
|
178
|
+
export function generateInstinctPrompt(db) {
|
|
179
|
+
const strong = getStrongInstincts(db, 0.6);
|
|
180
|
+
if (strong.length === 0) return "";
|
|
181
|
+
|
|
182
|
+
const lines = ["Based on learned preferences:"];
|
|
183
|
+
for (const inst of strong) {
|
|
184
|
+
lines.push(
|
|
185
|
+
`- [${inst.category}] ${inst.pattern} (confidence: ${(inst.confidence * 100).toFixed(0)}%)`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge exporter — export notes to Markdown files or static HTML site.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the notes table exists
|
|
10
|
+
*/
|
|
11
|
+
function ensureNotesTable(db) {
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
title TEXT NOT NULL,
|
|
16
|
+
content TEXT DEFAULT '',
|
|
17
|
+
tags TEXT DEFAULT '[]',
|
|
18
|
+
category TEXT DEFAULT 'general',
|
|
19
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
20
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
21
|
+
deleted_at TEXT DEFAULT NULL
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch all active notes from the database.
|
|
28
|
+
*/
|
|
29
|
+
export function fetchNotes(db, { category, tag, limit } = {}) {
|
|
30
|
+
ensureNotesTable(db);
|
|
31
|
+
|
|
32
|
+
let sql = "SELECT * FROM notes WHERE deleted_at IS NULL";
|
|
33
|
+
const params = [];
|
|
34
|
+
|
|
35
|
+
if (category) {
|
|
36
|
+
sql += " AND category = ?";
|
|
37
|
+
params.push(category);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
sql += " ORDER BY created_at DESC";
|
|
41
|
+
|
|
42
|
+
if (limit) {
|
|
43
|
+
sql += " LIMIT ?";
|
|
44
|
+
params.push(limit);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let notes = db.prepare(sql).all(...params);
|
|
48
|
+
|
|
49
|
+
// Filter by tag in-memory
|
|
50
|
+
if (tag) {
|
|
51
|
+
notes = notes.filter((n) => {
|
|
52
|
+
try {
|
|
53
|
+
const tags = JSON.parse(n.tags || "[]");
|
|
54
|
+
return tags.includes(tag);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return notes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sanitize a filename (remove invalid characters).
|
|
66
|
+
*/
|
|
67
|
+
export function sanitizeFilename(name) {
|
|
68
|
+
return name
|
|
69
|
+
.replace(/[<>:"/\\|?*]/g, "")
|
|
70
|
+
.replace(/\s+/g, "-")
|
|
71
|
+
.substring(0, 200);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Markdown Export ────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a note to markdown with YAML frontmatter.
|
|
78
|
+
*/
|
|
79
|
+
export function noteToMarkdown(note) {
|
|
80
|
+
const tags = JSON.parse(note.tags || "[]");
|
|
81
|
+
const lines = [
|
|
82
|
+
"---",
|
|
83
|
+
`title: "${note.title.replace(/"/g, '\\"')}"`,
|
|
84
|
+
`category: ${note.category || "general"}`,
|
|
85
|
+
`tags: [${tags.map((t) => `"${t}"`).join(", ")}]`,
|
|
86
|
+
`date: ${note.created_at || ""}`,
|
|
87
|
+
`id: ${note.id}`,
|
|
88
|
+
"---",
|
|
89
|
+
"",
|
|
90
|
+
`# ${note.title}`,
|
|
91
|
+
"",
|
|
92
|
+
note.content || "",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Export notes to a directory as individual markdown files.
|
|
100
|
+
* Groups notes by category into subdirectories.
|
|
101
|
+
*/
|
|
102
|
+
export function exportToMarkdown(db, outputDir, options = {}) {
|
|
103
|
+
const notes = fetchNotes(db, options);
|
|
104
|
+
if (!existsSync(outputDir)) {
|
|
105
|
+
mkdirSync(outputDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const exported = [];
|
|
109
|
+
|
|
110
|
+
for (const note of notes) {
|
|
111
|
+
const category = note.category || "general";
|
|
112
|
+
const catDir = join(outputDir, sanitizeFilename(category));
|
|
113
|
+
if (!existsSync(catDir)) {
|
|
114
|
+
mkdirSync(catDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filename = `${sanitizeFilename(note.title)}.md`;
|
|
118
|
+
const filePath = join(catDir, filename);
|
|
119
|
+
const markdown = noteToMarkdown(note);
|
|
120
|
+
|
|
121
|
+
writeFileSync(filePath, markdown, "utf-8");
|
|
122
|
+
exported.push({
|
|
123
|
+
id: note.id,
|
|
124
|
+
title: note.title,
|
|
125
|
+
path: `${category}/${filename}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return exported;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Static HTML Site Export ────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate a minimal HTML page for a note.
|
|
136
|
+
*/
|
|
137
|
+
export function noteToHtml(note) {
|
|
138
|
+
const tags = JSON.parse(note.tags || "[]");
|
|
139
|
+
const tagsHtml = tags
|
|
140
|
+
.map((t) => `<span class="tag">${escapeHtml(t)}</span>`)
|
|
141
|
+
.join(" ");
|
|
142
|
+
const contentHtml = markdownToSimpleHtml(note.content || "");
|
|
143
|
+
|
|
144
|
+
return `<!DOCTYPE html>
|
|
145
|
+
<html lang="en">
|
|
146
|
+
<head>
|
|
147
|
+
<meta charset="UTF-8">
|
|
148
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
149
|
+
<title>${escapeHtml(note.title)}</title>
|
|
150
|
+
<link rel="stylesheet" href="../style.css">
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<nav><a href="../index.html">Home</a></nav>
|
|
154
|
+
<article>
|
|
155
|
+
<h1>${escapeHtml(note.title)}</h1>
|
|
156
|
+
<div class="meta">
|
|
157
|
+
<time>${note.created_at || ""}</time>
|
|
158
|
+
<span class="category">${escapeHtml(note.category || "general")}</span>
|
|
159
|
+
${tagsHtml}
|
|
160
|
+
</div>
|
|
161
|
+
<div class="content">${contentHtml}</div>
|
|
162
|
+
</article>
|
|
163
|
+
</body>
|
|
164
|
+
</html>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate the index page listing all notes.
|
|
169
|
+
*/
|
|
170
|
+
export function generateIndexHtml(
|
|
171
|
+
notes,
|
|
172
|
+
siteTitle = "ChainlessChain Knowledge Base",
|
|
173
|
+
) {
|
|
174
|
+
const noteLinks = notes
|
|
175
|
+
.map((n) => {
|
|
176
|
+
const tags = JSON.parse(n.tags || "[]");
|
|
177
|
+
const tagsHtml = tags
|
|
178
|
+
.map((t) => `<span class="tag">${escapeHtml(t)}</span>`)
|
|
179
|
+
.join(" ");
|
|
180
|
+
const cat = sanitizeFilename(n.category || "general");
|
|
181
|
+
const file = sanitizeFilename(n.title) + ".html";
|
|
182
|
+
return `<li>
|
|
183
|
+
<a href="${cat}/${file}">${escapeHtml(n.title)}</a>
|
|
184
|
+
<span class="category">${escapeHtml(n.category || "general")}</span>
|
|
185
|
+
${tagsHtml}
|
|
186
|
+
<time>${n.created_at || ""}</time>
|
|
187
|
+
</li>`;
|
|
188
|
+
})
|
|
189
|
+
.join("\n");
|
|
190
|
+
|
|
191
|
+
return `<!DOCTYPE html>
|
|
192
|
+
<html lang="en">
|
|
193
|
+
<head>
|
|
194
|
+
<meta charset="UTF-8">
|
|
195
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
196
|
+
<title>${escapeHtml(siteTitle)}</title>
|
|
197
|
+
<link rel="stylesheet" href="style.css">
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<header><h1>${escapeHtml(siteTitle)}</h1></header>
|
|
201
|
+
<main>
|
|
202
|
+
<p>${notes.length} notes</p>
|
|
203
|
+
<ul class="note-list">${noteLinks}</ul>
|
|
204
|
+
</main>
|
|
205
|
+
</body>
|
|
206
|
+
</html>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate a minimal CSS stylesheet.
|
|
211
|
+
*/
|
|
212
|
+
export function generateStyleCss() {
|
|
213
|
+
return `* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
214
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; color: #333; line-height: 1.6; }
|
|
215
|
+
nav { margin-bottom: 2rem; }
|
|
216
|
+
nav a { color: #0066cc; text-decoration: none; }
|
|
217
|
+
h1 { margin-bottom: 1rem; }
|
|
218
|
+
.meta { color: #666; margin-bottom: 1.5rem; font-size: 0.9rem; }
|
|
219
|
+
.tag { background: #e8f0fe; color: #1a73e8; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-right: 4px; }
|
|
220
|
+
.category { color: #5f6368; margin-right: 8px; }
|
|
221
|
+
.content { line-height: 1.8; }
|
|
222
|
+
.content p { margin-bottom: 1rem; }
|
|
223
|
+
.content pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto; margin-bottom: 1rem; }
|
|
224
|
+
.content code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; font-size: 0.9em; }
|
|
225
|
+
.note-list { list-style: none; }
|
|
226
|
+
.note-list li { padding: 0.75rem 0; border-bottom: 1px solid #eee; }
|
|
227
|
+
.note-list a { color: #1a73e8; text-decoration: none; font-weight: 500; margin-right: 8px; }
|
|
228
|
+
time { color: #999; font-size: 0.85rem; }
|
|
229
|
+
header { border-bottom: 2px solid #1a73e8; padding-bottom: 1rem; margin-bottom: 2rem; }`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Export notes as a static HTML site.
|
|
234
|
+
*/
|
|
235
|
+
export function exportToSite(db, outputDir, options = {}) {
|
|
236
|
+
const notes = fetchNotes(db, options);
|
|
237
|
+
if (!existsSync(outputDir)) {
|
|
238
|
+
mkdirSync(outputDir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Write CSS
|
|
242
|
+
writeFileSync(join(outputDir, "style.css"), generateStyleCss(), "utf-8");
|
|
243
|
+
|
|
244
|
+
// Write index
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(outputDir, "index.html"),
|
|
247
|
+
generateIndexHtml(notes, options.title),
|
|
248
|
+
"utf-8",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Write individual pages
|
|
252
|
+
const exported = [];
|
|
253
|
+
for (const note of notes) {
|
|
254
|
+
const category = note.category || "general";
|
|
255
|
+
const catDir = join(outputDir, sanitizeFilename(category));
|
|
256
|
+
if (!existsSync(catDir)) {
|
|
257
|
+
mkdirSync(catDir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const filename = `${sanitizeFilename(note.title)}.html`;
|
|
261
|
+
const filePath = join(catDir, filename);
|
|
262
|
+
writeFileSync(filePath, noteToHtml(note), "utf-8");
|
|
263
|
+
|
|
264
|
+
exported.push({
|
|
265
|
+
id: note.id,
|
|
266
|
+
title: note.title,
|
|
267
|
+
path: `${sanitizeFilename(category)}/${filename}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return exported;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function escapeHtml(str) {
|
|
277
|
+
return (str || "")
|
|
278
|
+
.replace(/&/g, "&")
|
|
279
|
+
.replace(/</g, "<")
|
|
280
|
+
.replace(/>/g, ">")
|
|
281
|
+
.replace(/"/g, """);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Very simple markdown→HTML for note content.
|
|
286
|
+
* Handles headings, paragraphs, code blocks, bold, italic, links.
|
|
287
|
+
*/
|
|
288
|
+
function markdownToSimpleHtml(md) {
|
|
289
|
+
return md
|
|
290
|
+
.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>")
|
|
291
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
292
|
+
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
|
|
293
|
+
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
|
|
294
|
+
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
|
|
295
|
+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
296
|
+
.replace(/\*(.+?)\*/g, "<em>$1</em>")
|
|
297
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
298
|
+
.replace(/^- (.+)$/gm, "<li>$1</li>")
|
|
299
|
+
.replace(/\n\n/g, "</p><p>")
|
|
300
|
+
.replace(/^/, "<p>")
|
|
301
|
+
.replace(/$/, "</p>");
|
|
302
|
+
}
|