chainlesschain 0.37.9 → 0.37.11

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.
Files changed (84) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/a2a.js +374 -0
  5. package/src/commands/audit.js +286 -0
  6. package/src/commands/auth.js +387 -0
  7. package/src/commands/bi.js +240 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/cowork.js +317 -0
  10. package/src/commands/did.js +376 -0
  11. package/src/commands/economy.js +375 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/evolution.js +398 -0
  14. package/src/commands/export.js +125 -0
  15. package/src/commands/git.js +215 -0
  16. package/src/commands/hmemory.js +273 -0
  17. package/src/commands/hook.js +260 -0
  18. package/src/commands/import.js +259 -0
  19. package/src/commands/init.js +184 -0
  20. package/src/commands/instinct.js +202 -0
  21. package/src/commands/llm.js +155 -4
  22. package/src/commands/lowcode.js +320 -0
  23. package/src/commands/mcp.js +302 -0
  24. package/src/commands/memory.js +282 -0
  25. package/src/commands/note.js +187 -0
  26. package/src/commands/org.js +505 -0
  27. package/src/commands/p2p.js +274 -0
  28. package/src/commands/plugin.js +451 -0
  29. package/src/commands/sandbox.js +366 -0
  30. package/src/commands/search.js +237 -0
  31. package/src/commands/session.js +238 -0
  32. package/src/commands/skill.js +254 -201
  33. package/src/commands/sync.js +249 -0
  34. package/src/commands/tokens.js +214 -0
  35. package/src/commands/wallet.js +416 -0
  36. package/src/commands/workflow.js +359 -0
  37. package/src/commands/zkp.js +277 -0
  38. package/src/index.js +93 -1
  39. package/src/lib/a2a-protocol.js +371 -0
  40. package/src/lib/agent-coordinator.js +273 -0
  41. package/src/lib/agent-economy.js +369 -0
  42. package/src/lib/app-builder.js +377 -0
  43. package/src/lib/audit-logger.js +364 -0
  44. package/src/lib/bi-engine.js +299 -0
  45. package/src/lib/bm25-search.js +322 -0
  46. package/src/lib/browser-automation.js +216 -0
  47. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  48. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  49. package/src/lib/cowork/debate-review-cli.js +144 -0
  50. package/src/lib/cowork/decision-kb-cli.js +153 -0
  51. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  52. package/src/lib/cowork-adapter.js +106 -0
  53. package/src/lib/crypto-manager.js +246 -0
  54. package/src/lib/did-manager.js +270 -0
  55. package/src/lib/ensure-utf8.js +59 -0
  56. package/src/lib/evolution-system.js +508 -0
  57. package/src/lib/git-integration.js +220 -0
  58. package/src/lib/hierarchical-memory.js +471 -0
  59. package/src/lib/hook-manager.js +387 -0
  60. package/src/lib/instinct-manager.js +190 -0
  61. package/src/lib/knowledge-exporter.js +302 -0
  62. package/src/lib/knowledge-importer.js +293 -0
  63. package/src/lib/llm-providers.js +325 -0
  64. package/src/lib/mcp-client.js +413 -0
  65. package/src/lib/memory-manager.js +211 -0
  66. package/src/lib/note-versioning.js +244 -0
  67. package/src/lib/org-manager.js +424 -0
  68. package/src/lib/p2p-manager.js +317 -0
  69. package/src/lib/pdf-parser.js +96 -0
  70. package/src/lib/permission-engine.js +374 -0
  71. package/src/lib/plan-mode.js +333 -0
  72. package/src/lib/plugin-manager.js +430 -0
  73. package/src/lib/project-detector.js +53 -0
  74. package/src/lib/response-cache.js +156 -0
  75. package/src/lib/sandbox-v2.js +503 -0
  76. package/src/lib/service-container.js +183 -0
  77. package/src/lib/session-manager.js +189 -0
  78. package/src/lib/skill-loader.js +274 -0
  79. package/src/lib/sync-manager.js +347 -0
  80. package/src/lib/token-tracker.js +200 -0
  81. package/src/lib/wallet-manager.js +348 -0
  82. package/src/lib/workflow-engine.js +503 -0
  83. package/src/lib/zkp-engine.js +241 -0
  84. package/src/repl/agent-repl.js +259 -124
@@ -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, "&amp;")
279
+ .replace(/</g, "&lt;")
280
+ .replace(/>/g, "&gt;")
281
+ .replace(/"/g, "&quot;");
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
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Knowledge importer — parse Markdown, Evernote ENEX, and Notion exports
3
+ * into the notes table.
4
+ */
5
+
6
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
7
+ import { join, basename, extname, relative } from "path";
8
+
9
+ /**
10
+ * Ensure the notes table exists
11
+ */
12
+ export function ensureNotesTable(db) {
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS notes (
15
+ id TEXT PRIMARY KEY,
16
+ title TEXT NOT NULL,
17
+ content TEXT DEFAULT '',
18
+ tags TEXT DEFAULT '[]',
19
+ category TEXT DEFAULT 'general',
20
+ created_at TEXT DEFAULT (datetime('now')),
21
+ updated_at TEXT DEFAULT (datetime('now')),
22
+ deleted_at TEXT DEFAULT NULL
23
+ )
24
+ `);
25
+ }
26
+
27
+ /**
28
+ * Generate a simple UUID-like ID
29
+ */
30
+ function generateId() {
31
+ const hex = () =>
32
+ Math.floor(Math.random() * 0x10000)
33
+ .toString(16)
34
+ .padStart(4, "0");
35
+ return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
36
+ }
37
+
38
+ /**
39
+ * Insert a note into the database
40
+ */
41
+ export function insertNote(db, { title, content, tags, category, createdAt }) {
42
+ const id = generateId();
43
+ const tagsJson = JSON.stringify(tags || []);
44
+ const cat = category || "general";
45
+ const created =
46
+ createdAt || new Date().toISOString().replace("T", " ").slice(0, 19);
47
+
48
+ db.prepare(
49
+ "INSERT INTO notes (id, title, content, tags, category, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
50
+ ).run(id, title, content || "", tagsJson, cat, created, created);
51
+
52
+ return { id, title, tags: tags || [], category: cat, created_at: created };
53
+ }
54
+
55
+ // ─── Markdown Import ────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Parse a single markdown file into a note object.
59
+ * Extracts YAML frontmatter if present.
60
+ */
61
+ export function parseMarkdownFile(filePath) {
62
+ const raw = readFileSync(filePath, "utf-8");
63
+ const name = basename(filePath, extname(filePath));
64
+
65
+ let title = name;
66
+ let content = raw;
67
+ let tags = [];
68
+ let category = "markdown";
69
+ let createdAt = null;
70
+
71
+ // Try to extract YAML frontmatter
72
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
73
+ if (fmMatch) {
74
+ const frontmatter = fmMatch[1];
75
+ content = fmMatch[2].trim();
76
+
77
+ // Parse simple YAML fields
78
+ const titleMatch = frontmatter.match(/^title:\s*["']?(.+?)["']?\s*$/m);
79
+ if (titleMatch) title = titleMatch[1];
80
+
81
+ const tagsMatch = frontmatter.match(/^tags:\s*\[([^\]]*)\]/m);
82
+ if (tagsMatch) {
83
+ tags = tagsMatch[1]
84
+ .split(",")
85
+ .map((t) => t.trim().replace(/["']/g, ""))
86
+ .filter(Boolean);
87
+ }
88
+
89
+ const catMatch = frontmatter.match(/^category:\s*["']?(.+?)["']?\s*$/m);
90
+ if (catMatch) category = catMatch[1];
91
+
92
+ const dateMatch = frontmatter.match(/^date:\s*["']?(.+?)["']?\s*$/m);
93
+ if (dateMatch) createdAt = dateMatch[1];
94
+ }
95
+
96
+ // Use first H1 as title if no frontmatter title
97
+ if (title === name && !fmMatch) {
98
+ const h1Match = content.match(/^#\s+(.+)$/m);
99
+ if (h1Match) title = h1Match[1];
100
+ }
101
+
102
+ return { title, content, tags, category, createdAt };
103
+ }
104
+
105
+ /**
106
+ * Recursively collect all .md files from a directory.
107
+ */
108
+ export function collectMarkdownFiles(dir) {
109
+ const results = [];
110
+
111
+ function walk(currentDir) {
112
+ const entries = readdirSync(currentDir);
113
+ for (const entry of entries) {
114
+ const fullPath = join(currentDir, entry);
115
+ const stat = statSync(fullPath);
116
+ if (stat.isDirectory()) {
117
+ walk(fullPath);
118
+ } else if (extname(entry).toLowerCase() === ".md") {
119
+ results.push(fullPath);
120
+ }
121
+ }
122
+ }
123
+
124
+ walk(dir);
125
+ return results;
126
+ }
127
+
128
+ /**
129
+ * Import all markdown files from a directory into the database.
130
+ */
131
+ export function importMarkdownDir(db, dir) {
132
+ ensureNotesTable(db);
133
+ const files = collectMarkdownFiles(dir);
134
+ const imported = [];
135
+
136
+ for (const file of files) {
137
+ const parsed = parseMarkdownFile(file);
138
+ const note = insertNote(db, parsed);
139
+ note.source = relative(dir, file);
140
+ imported.push(note);
141
+ }
142
+
143
+ return imported;
144
+ }
145
+
146
+ // ─── Evernote ENEX Import ───────────────────────────────────────────
147
+
148
+ /**
149
+ * Parse an ENEX (Evernote Export) XML string into note objects.
150
+ * ENEX format: <en-export><note><title>...</title><content>...</content><tag>...</tag></note>...</en-export>
151
+ */
152
+ export function parseEnex(xmlString) {
153
+ const notes = [];
154
+ const noteRegex = /<note>([\s\S]*?)<\/note>/gi;
155
+ let noteMatch;
156
+
157
+ while ((noteMatch = noteRegex.exec(xmlString)) !== null) {
158
+ const noteXml = noteMatch[1];
159
+
160
+ const titleMatch = noteXml.match(/<title>([\s\S]*?)<\/title>/i);
161
+ const title = titleMatch ? titleMatch[1].trim() : "Untitled";
162
+
163
+ // Content is wrapped in CDATA inside <content>
164
+ const contentMatch = noteXml.match(/<content>([\s\S]*?)<\/content>/i);
165
+ let content = "";
166
+ if (contentMatch) {
167
+ let raw = contentMatch[1];
168
+ // Strip CDATA wrapper
169
+ const cdataMatch = raw.match(/<!\[CDATA\[([\s\S]*?)\]\]>/);
170
+ if (cdataMatch) raw = cdataMatch[1];
171
+ // Strip ENML/HTML tags for plain text
172
+ content = stripHtml(raw);
173
+ }
174
+
175
+ // Tags
176
+ const tags = [];
177
+ const tagRegex = /<tag>([\s\S]*?)<\/tag>/gi;
178
+ let tagMatch;
179
+ while ((tagMatch = tagRegex.exec(noteXml)) !== null) {
180
+ tags.push(tagMatch[1].trim());
181
+ }
182
+
183
+ // Created date
184
+ const createdMatch = noteXml.match(/<created>([\s\S]*?)<\/created>/i);
185
+ let createdAt = null;
186
+ if (createdMatch) {
187
+ // ENEX dates: 20210315T120000Z → 2021-03-15 12:00:00
188
+ const d = createdMatch[1].trim();
189
+ const parsed = d.match(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/);
190
+ if (parsed) {
191
+ createdAt = `${parsed[1]}-${parsed[2]}-${parsed[3]} ${parsed[4]}:${parsed[5]}:${parsed[6]}`;
192
+ }
193
+ }
194
+
195
+ notes.push({ title, content, tags, category: "evernote", createdAt });
196
+ }
197
+
198
+ return notes;
199
+ }
200
+
201
+ /**
202
+ * Strip HTML/ENML tags to plain text.
203
+ */
204
+ export function stripHtml(html) {
205
+ return html
206
+ .replace(/<br\s*\/?>/gi, "\n")
207
+ .replace(/<\/p>/gi, "\n")
208
+ .replace(/<\/div>/gi, "\n")
209
+ .replace(/<\/li>/gi, "\n")
210
+ .replace(/<[^>]+>/g, "")
211
+ .replace(/&nbsp;/g, " ")
212
+ .replace(/&amp;/g, "&")
213
+ .replace(/&lt;/g, "<")
214
+ .replace(/&gt;/g, ">")
215
+ .replace(/&quot;/g, '"')
216
+ .replace(/&#39;/g, "'")
217
+ .replace(/\n{3,}/g, "\n\n")
218
+ .trim();
219
+ }
220
+
221
+ /**
222
+ * Import an Evernote ENEX file into the database.
223
+ */
224
+ export function importEnexFile(db, filePath) {
225
+ ensureNotesTable(db);
226
+ const xmlString = readFileSync(filePath, "utf-8");
227
+ const parsed = parseEnex(xmlString);
228
+ const imported = [];
229
+
230
+ for (const note of parsed) {
231
+ const result = insertNote(db, note);
232
+ imported.push(result);
233
+ }
234
+
235
+ return imported;
236
+ }
237
+
238
+ // ─── Notion Export Import ───────────────────────────────────────────
239
+
240
+ /**
241
+ * Parse a Notion export directory.
242
+ * Notion exports contain markdown files and may have metadata in filenames
243
+ * or accompanying JSON/CSV files.
244
+ */
245
+ export function parseNotionExport(dir) {
246
+ const notes = [];
247
+ const files = collectMarkdownFiles(dir);
248
+
249
+ for (const file of files) {
250
+ const raw = readFileSync(file, "utf-8");
251
+ const fileName = basename(file, ".md");
252
+
253
+ // Notion filenames often have a UUID suffix: "My Page abc123def456"
254
+ // Remove the hex suffix to get the clean title
255
+ const title = fileName.replace(/\s+[a-f0-9]{32}$/i, "") || fileName;
256
+
257
+ // Notion uses # for the first heading, which is usually the page title
258
+ let content = raw;
259
+ const h1Match = raw.match(/^#\s+(.+)\n([\s\S]*)$/);
260
+ if (h1Match) {
261
+ content = h1Match[2].trim();
262
+ }
263
+
264
+ // Try to detect tags from Notion properties (sometimes at the top)
265
+ const tags = [];
266
+ const relPath = relative(dir, file);
267
+ const parts = relPath.split(/[/\\]/);
268
+ if (parts.length > 1) {
269
+ // Use parent folder as a tag
270
+ tags.push(parts[0]);
271
+ }
272
+
273
+ notes.push({ title, content, tags, category: "notion", createdAt: null });
274
+ }
275
+
276
+ return notes;
277
+ }
278
+
279
+ /**
280
+ * Import a Notion export directory into the database.
281
+ */
282
+ export function importNotionDir(db, dir) {
283
+ ensureNotesTable(db);
284
+ const parsed = parseNotionExport(dir);
285
+ const imported = [];
286
+
287
+ for (const note of parsed) {
288
+ const result = insertNote(db, note);
289
+ imported.push(result);
290
+ }
291
+
292
+ return imported;
293
+ }