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,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note/knowledge base management commands
|
|
3
|
+
* chainlesschain note add|list|show|search|delete|export
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
ensureVersionsTable,
|
|
11
|
+
saveVersion,
|
|
12
|
+
getHistory,
|
|
13
|
+
getVersion,
|
|
14
|
+
simpleDiff,
|
|
15
|
+
formatDiff,
|
|
16
|
+
revertToVersion,
|
|
17
|
+
} from "../lib/note-versioning.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure the notes table exists in the database
|
|
21
|
+
*/
|
|
22
|
+
function ensureNotesTable(db) {
|
|
23
|
+
db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
title TEXT NOT NULL,
|
|
27
|
+
content TEXT DEFAULT '',
|
|
28
|
+
tags TEXT DEFAULT '[]',
|
|
29
|
+
category TEXT DEFAULT 'general',
|
|
30
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
32
|
+
deleted_at TEXT DEFAULT NULL
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registerNoteCommand(program) {
|
|
38
|
+
const note = program
|
|
39
|
+
.command("note")
|
|
40
|
+
.description("Note and knowledge base management");
|
|
41
|
+
|
|
42
|
+
// note add
|
|
43
|
+
note
|
|
44
|
+
.command("add")
|
|
45
|
+
.description("Add a new note")
|
|
46
|
+
.argument("<title>", "Note title")
|
|
47
|
+
.option("-c, --content <content>", "Note content")
|
|
48
|
+
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
49
|
+
.option("--category <category>", "Note category", "general")
|
|
50
|
+
.option("--json", "Output as JSON")
|
|
51
|
+
.action(async (title, options) => {
|
|
52
|
+
try {
|
|
53
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
54
|
+
if (!ctx.db) {
|
|
55
|
+
logger.error("Database not available");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rawDb = ctx.db.getDatabase();
|
|
60
|
+
ensureNotesTable(rawDb);
|
|
61
|
+
|
|
62
|
+
const { randomUUID } = await import("crypto");
|
|
63
|
+
const id = randomUUID();
|
|
64
|
+
const tags = options.tags
|
|
65
|
+
? JSON.stringify(options.tags.split(",").map((t) => t.trim()))
|
|
66
|
+
: "[]";
|
|
67
|
+
|
|
68
|
+
rawDb
|
|
69
|
+
.prepare(
|
|
70
|
+
"INSERT INTO notes (id, title, content, tags, category) VALUES (?, ?, ?, ?, ?)",
|
|
71
|
+
)
|
|
72
|
+
.run(id, title, options.content || "", tags, options.category);
|
|
73
|
+
|
|
74
|
+
if (options.json) {
|
|
75
|
+
console.log(
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
id,
|
|
78
|
+
title,
|
|
79
|
+
tags: JSON.parse(tags),
|
|
80
|
+
category: options.category,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
logger.success(
|
|
85
|
+
`Note created: ${chalk.cyan(title)} (${chalk.gray(id.slice(0, 8))})`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await shutdown();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.error(`Failed to add note: ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// note list
|
|
97
|
+
note
|
|
98
|
+
.command("list")
|
|
99
|
+
.description("List notes")
|
|
100
|
+
.option("-n, --limit <n>", "Max notes to show", "20")
|
|
101
|
+
.option("--category <category>", "Filter by category")
|
|
102
|
+
.option("--tag <tag>", "Filter by tag")
|
|
103
|
+
.option("--json", "Output as JSON")
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
try {
|
|
106
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
107
|
+
if (!ctx.db) {
|
|
108
|
+
logger.error("Database not available");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rawDb = ctx.db.getDatabase();
|
|
113
|
+
ensureNotesTable(rawDb);
|
|
114
|
+
|
|
115
|
+
let sql =
|
|
116
|
+
"SELECT id, title, tags, category, created_at FROM notes WHERE deleted_at IS NULL";
|
|
117
|
+
const params = [];
|
|
118
|
+
|
|
119
|
+
if (options.category) {
|
|
120
|
+
sql += " AND category = ?";
|
|
121
|
+
params.push(options.category);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
125
|
+
params.push(parseInt(options.limit));
|
|
126
|
+
|
|
127
|
+
let notes = rawDb.prepare(sql).all(...params);
|
|
128
|
+
|
|
129
|
+
// Filter by tag in-memory (tags stored as JSON array)
|
|
130
|
+
if (options.tag) {
|
|
131
|
+
notes = notes.filter((n) => {
|
|
132
|
+
try {
|
|
133
|
+
const tags = JSON.parse(n.tags || "[]");
|
|
134
|
+
return tags.includes(options.tag);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(notes, null, 2));
|
|
143
|
+
} else if (notes.length === 0) {
|
|
144
|
+
logger.info("No notes found");
|
|
145
|
+
} else {
|
|
146
|
+
logger.log(chalk.bold(`Notes (${notes.length}):\n`));
|
|
147
|
+
for (const n of notes) {
|
|
148
|
+
const tags = JSON.parse(n.tags || "[]");
|
|
149
|
+
const tagStr =
|
|
150
|
+
tags.length > 0 ? chalk.gray(` [${tags.join(", ")}]`) : "";
|
|
151
|
+
logger.log(
|
|
152
|
+
` ${chalk.gray(n.id.slice(0, 8))} ${chalk.white(n.title)}${tagStr} ${chalk.gray(n.created_at)}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await shutdown();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.error(`Failed to list notes: ${err.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// note show
|
|
165
|
+
note
|
|
166
|
+
.command("show")
|
|
167
|
+
.description("Show note content")
|
|
168
|
+
.argument("<id>", "Note ID (or prefix)")
|
|
169
|
+
.option("--json", "Output as JSON")
|
|
170
|
+
.action(async (id, options) => {
|
|
171
|
+
try {
|
|
172
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
173
|
+
if (!ctx.db) {
|
|
174
|
+
logger.error("Database not available");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const rawDb = ctx.db.getDatabase();
|
|
179
|
+
ensureNotesTable(rawDb);
|
|
180
|
+
|
|
181
|
+
const note = rawDb
|
|
182
|
+
.prepare("SELECT * FROM notes WHERE id LIKE ? AND deleted_at IS NULL")
|
|
183
|
+
.get(`${id}%`);
|
|
184
|
+
|
|
185
|
+
if (!note) {
|
|
186
|
+
logger.error(`Note not found: ${id}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (options.json) {
|
|
191
|
+
console.log(JSON.stringify(note, null, 2));
|
|
192
|
+
} else {
|
|
193
|
+
logger.log(chalk.bold(note.title));
|
|
194
|
+
logger.log(chalk.gray(`ID: ${note.id}`));
|
|
195
|
+
logger.log(
|
|
196
|
+
chalk.gray(
|
|
197
|
+
`Category: ${note.category} Created: ${note.created_at}`,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
const tags = JSON.parse(note.tags || "[]");
|
|
201
|
+
if (tags.length > 0) {
|
|
202
|
+
logger.log(chalk.gray(`Tags: ${tags.join(", ")}`));
|
|
203
|
+
}
|
|
204
|
+
logger.log("");
|
|
205
|
+
logger.log(note.content || chalk.gray("(empty)"));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await shutdown();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
logger.error(`Failed to show note: ${err.message}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// note search
|
|
216
|
+
note
|
|
217
|
+
.command("search")
|
|
218
|
+
.description("Search notes")
|
|
219
|
+
.argument("<query>", "Search query")
|
|
220
|
+
.option("--json", "Output as JSON")
|
|
221
|
+
.action(async (query, options) => {
|
|
222
|
+
try {
|
|
223
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
224
|
+
if (!ctx.db) {
|
|
225
|
+
logger.error("Database not available");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const rawDb = ctx.db.getDatabase();
|
|
230
|
+
ensureNotesTable(rawDb);
|
|
231
|
+
|
|
232
|
+
const pattern = `%${query}%`;
|
|
233
|
+
const notes = rawDb
|
|
234
|
+
.prepare(
|
|
235
|
+
"SELECT id, title, category, created_at FROM notes WHERE deleted_at IS NULL AND (title LIKE ? OR content LIKE ?) ORDER BY created_at DESC LIMIT 50",
|
|
236
|
+
)
|
|
237
|
+
.all(pattern, pattern);
|
|
238
|
+
|
|
239
|
+
if (options.json) {
|
|
240
|
+
console.log(JSON.stringify(notes, null, 2));
|
|
241
|
+
} else if (notes.length === 0) {
|
|
242
|
+
logger.info(`No notes matching "${query}"`);
|
|
243
|
+
} else {
|
|
244
|
+
logger.log(
|
|
245
|
+
chalk.bold(`Search results for "${query}" (${notes.length}):\n`),
|
|
246
|
+
);
|
|
247
|
+
for (const n of notes) {
|
|
248
|
+
logger.log(
|
|
249
|
+
` ${chalk.gray(n.id.slice(0, 8))} ${chalk.white(n.title)} ${chalk.gray(n.created_at)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await shutdown();
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger.error(`Search failed: ${err.message}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// note delete
|
|
262
|
+
note
|
|
263
|
+
.command("delete")
|
|
264
|
+
.description("Delete a note (soft delete)")
|
|
265
|
+
.argument("<id>", "Note ID (or prefix)")
|
|
266
|
+
.option("--force", "Skip confirmation")
|
|
267
|
+
.action(async (id, options) => {
|
|
268
|
+
try {
|
|
269
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
270
|
+
if (!ctx.db) {
|
|
271
|
+
logger.error("Database not available");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const rawDb = ctx.db.getDatabase();
|
|
276
|
+
ensureNotesTable(rawDb);
|
|
277
|
+
|
|
278
|
+
const note = rawDb
|
|
279
|
+
.prepare(
|
|
280
|
+
"SELECT id, title FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
|
|
281
|
+
)
|
|
282
|
+
.get(`${id}%`);
|
|
283
|
+
|
|
284
|
+
if (!note) {
|
|
285
|
+
logger.error(`Note not found: ${id}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!options.force) {
|
|
290
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
291
|
+
const ok = await confirm({
|
|
292
|
+
message: `Delete note "${note.title}"?`,
|
|
293
|
+
});
|
|
294
|
+
if (!ok) {
|
|
295
|
+
logger.info("Cancelled");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
rawDb
|
|
301
|
+
.prepare("UPDATE notes SET deleted_at = datetime('now') WHERE id = ?")
|
|
302
|
+
.run(note.id);
|
|
303
|
+
|
|
304
|
+
logger.success(`Note deleted: ${chalk.cyan(note.title)}`);
|
|
305
|
+
await shutdown();
|
|
306
|
+
} catch (err) {
|
|
307
|
+
logger.error(`Failed to delete note: ${err.message}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// note history
|
|
313
|
+
note
|
|
314
|
+
.command("history")
|
|
315
|
+
.description("Show version history for a note")
|
|
316
|
+
.argument("<id>", "Note ID (or prefix)")
|
|
317
|
+
.option("--json", "Output as JSON")
|
|
318
|
+
.action(async (id, options) => {
|
|
319
|
+
try {
|
|
320
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
321
|
+
if (!ctx.db) {
|
|
322
|
+
logger.error("Database not available");
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const rawDb = ctx.db.getDatabase();
|
|
327
|
+
ensureNotesTable(rawDb);
|
|
328
|
+
ensureVersionsTable(rawDb);
|
|
329
|
+
|
|
330
|
+
// Find the note
|
|
331
|
+
const noteRow = rawDb
|
|
332
|
+
.prepare(
|
|
333
|
+
"SELECT id FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
|
|
334
|
+
)
|
|
335
|
+
.get(`${id}%`);
|
|
336
|
+
|
|
337
|
+
if (!noteRow) {
|
|
338
|
+
logger.error(`Note not found: ${id}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const history = getHistory(rawDb, noteRow.id);
|
|
343
|
+
|
|
344
|
+
if (options.json) {
|
|
345
|
+
console.log(JSON.stringify(history, null, 2));
|
|
346
|
+
} else if (history.length === 0) {
|
|
347
|
+
logger.info("No version history found");
|
|
348
|
+
} else {
|
|
349
|
+
logger.log(
|
|
350
|
+
chalk.bold(`Version history (${history.length} versions):\n`),
|
|
351
|
+
);
|
|
352
|
+
for (const v of history) {
|
|
353
|
+
logger.log(
|
|
354
|
+
` ${chalk.cyan(`v${v.version}`)} ${chalk.white(v.title)} ${chalk.gray(v.change_type)} ${chalk.gray(v.created_at || "")}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await shutdown();
|
|
360
|
+
} catch (err) {
|
|
361
|
+
logger.error(`Failed to get history: ${err.message}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// note diff
|
|
367
|
+
note
|
|
368
|
+
.command("diff")
|
|
369
|
+
.description("Show diff between two versions of a note")
|
|
370
|
+
.argument("<id>", "Note ID (or prefix)")
|
|
371
|
+
.argument("<v1>", "First version number")
|
|
372
|
+
.argument("<v2>", "Second version number")
|
|
373
|
+
.option("--json", "Output as JSON")
|
|
374
|
+
.action(async (id, v1, v2, options) => {
|
|
375
|
+
try {
|
|
376
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
377
|
+
if (!ctx.db) {
|
|
378
|
+
logger.error("Database not available");
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const rawDb = ctx.db.getDatabase();
|
|
383
|
+
ensureNotesTable(rawDb);
|
|
384
|
+
ensureVersionsTable(rawDb);
|
|
385
|
+
|
|
386
|
+
const noteRow = rawDb
|
|
387
|
+
.prepare(
|
|
388
|
+
"SELECT id FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
|
|
389
|
+
)
|
|
390
|
+
.get(`${id}%`);
|
|
391
|
+
|
|
392
|
+
if (!noteRow) {
|
|
393
|
+
logger.error(`Note not found: ${id}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const ver1 = getVersion(rawDb, noteRow.id, parseInt(v1));
|
|
398
|
+
const ver2 = getVersion(rawDb, noteRow.id, parseInt(v2));
|
|
399
|
+
|
|
400
|
+
if (!ver1) {
|
|
401
|
+
logger.error(`Version ${v1} not found`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
if (!ver2) {
|
|
405
|
+
logger.error(`Version ${v2} not found`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const diff = simpleDiff(ver1.content, ver2.content);
|
|
410
|
+
|
|
411
|
+
if (options.json) {
|
|
412
|
+
console.log(
|
|
413
|
+
JSON.stringify(
|
|
414
|
+
{ v1: parseInt(v1), v2: parseInt(v2), diff },
|
|
415
|
+
null,
|
|
416
|
+
2,
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
} else {
|
|
420
|
+
logger.log(chalk.bold(`Diff: v${v1} → v${v2}\n`));
|
|
421
|
+
logger.log(formatDiff(diff));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await shutdown();
|
|
425
|
+
} catch (err) {
|
|
426
|
+
logger.error(`Failed to compute diff: ${err.message}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// note revert
|
|
432
|
+
note
|
|
433
|
+
.command("revert")
|
|
434
|
+
.description("Revert a note to a specific version")
|
|
435
|
+
.argument("<id>", "Note ID (or prefix)")
|
|
436
|
+
.argument("<version>", "Version number to revert to")
|
|
437
|
+
.option("--force", "Skip confirmation")
|
|
438
|
+
.action(async (id, version, options) => {
|
|
439
|
+
try {
|
|
440
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
441
|
+
if (!ctx.db) {
|
|
442
|
+
logger.error("Database not available");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const rawDb = ctx.db.getDatabase();
|
|
447
|
+
ensureNotesTable(rawDb);
|
|
448
|
+
ensureVersionsTable(rawDb);
|
|
449
|
+
|
|
450
|
+
const noteRow = rawDb
|
|
451
|
+
.prepare(
|
|
452
|
+
"SELECT id, title FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
|
|
453
|
+
)
|
|
454
|
+
.get(`${id}%`);
|
|
455
|
+
|
|
456
|
+
if (!noteRow) {
|
|
457
|
+
logger.error(`Note not found: ${id}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!options.force) {
|
|
462
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
463
|
+
const ok = await confirm({
|
|
464
|
+
message: `Revert note "${noteRow.title}" to version ${version}?`,
|
|
465
|
+
});
|
|
466
|
+
if (!ok) {
|
|
467
|
+
logger.info("Cancelled");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const result = revertToVersion(rawDb, noteRow.id, parseInt(version));
|
|
473
|
+
|
|
474
|
+
if (!result) {
|
|
475
|
+
logger.error(`Version ${version} not found or note unavailable`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
logger.success(
|
|
480
|
+
`Note reverted to v${result.reverted_to} → new version v${result.new_version}`,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
await shutdown();
|
|
484
|
+
} catch (err) {
|
|
485
|
+
logger.error(`Failed to revert: ${err.message}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|