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.
- package/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -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/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -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/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -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/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- 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, "&")
|
|
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
|
+
}
|
|
@@ -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(/ /g, " ")
|
|
212
|
+
.replace(/&/g, "&")
|
|
213
|
+
.replace(/</g, "<")
|
|
214
|
+
.replace(/>/g, ">")
|
|
215
|
+
.replace(/"/g, '"')
|
|
216
|
+
.replace(/'/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
|
+
}
|