chainlesschain 0.37.8 → 0.37.9

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.
@@ -0,0 +1,302 @@
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
+
10
+ /**
11
+ * Ensure the notes table exists in the database
12
+ */
13
+ function ensureNotesTable(db) {
14
+ db.exec(`
15
+ CREATE TABLE IF NOT EXISTS notes (
16
+ id TEXT PRIMARY KEY,
17
+ title TEXT NOT NULL,
18
+ content TEXT DEFAULT '',
19
+ tags TEXT DEFAULT '[]',
20
+ category TEXT DEFAULT 'general',
21
+ created_at TEXT DEFAULT (datetime('now')),
22
+ updated_at TEXT DEFAULT (datetime('now')),
23
+ deleted_at TEXT DEFAULT NULL
24
+ )
25
+ `);
26
+ }
27
+
28
+ export function registerNoteCommand(program) {
29
+ const note = program
30
+ .command("note")
31
+ .description("Note and knowledge base management");
32
+
33
+ // note add
34
+ note
35
+ .command("add")
36
+ .description("Add a new note")
37
+ .argument("<title>", "Note title")
38
+ .option("-c, --content <content>", "Note content")
39
+ .option("-t, --tags <tags>", "Comma-separated tags")
40
+ .option("--category <category>", "Note category", "general")
41
+ .option("--json", "Output as JSON")
42
+ .action(async (title, options) => {
43
+ try {
44
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
45
+ if (!ctx.db) {
46
+ logger.error("Database not available");
47
+ process.exit(1);
48
+ }
49
+
50
+ const rawDb = ctx.db.getDatabase();
51
+ ensureNotesTable(rawDb);
52
+
53
+ const { randomUUID } = await import("crypto");
54
+ const id = randomUUID();
55
+ const tags = options.tags
56
+ ? JSON.stringify(options.tags.split(",").map((t) => t.trim()))
57
+ : "[]";
58
+
59
+ rawDb
60
+ .prepare(
61
+ "INSERT INTO notes (id, title, content, tags, category) VALUES (?, ?, ?, ?, ?)",
62
+ )
63
+ .run(id, title, options.content || "", tags, options.category);
64
+
65
+ if (options.json) {
66
+ console.log(
67
+ JSON.stringify({
68
+ id,
69
+ title,
70
+ tags: JSON.parse(tags),
71
+ category: options.category,
72
+ }),
73
+ );
74
+ } else {
75
+ logger.success(
76
+ `Note created: ${chalk.cyan(title)} (${chalk.gray(id.slice(0, 8))})`,
77
+ );
78
+ }
79
+
80
+ await shutdown();
81
+ } catch (err) {
82
+ logger.error(`Failed to add note: ${err.message}`);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ // note list
88
+ note
89
+ .command("list")
90
+ .description("List notes")
91
+ .option("-n, --limit <n>", "Max notes to show", "20")
92
+ .option("--category <category>", "Filter by category")
93
+ .option("--tag <tag>", "Filter by tag")
94
+ .option("--json", "Output as JSON")
95
+ .action(async (options) => {
96
+ try {
97
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
98
+ if (!ctx.db) {
99
+ logger.error("Database not available");
100
+ process.exit(1);
101
+ }
102
+
103
+ const rawDb = ctx.db.getDatabase();
104
+ ensureNotesTable(rawDb);
105
+
106
+ let sql =
107
+ "SELECT id, title, tags, category, created_at FROM notes WHERE deleted_at IS NULL";
108
+ const params = [];
109
+
110
+ if (options.category) {
111
+ sql += " AND category = ?";
112
+ params.push(options.category);
113
+ }
114
+
115
+ sql += " ORDER BY created_at DESC LIMIT ?";
116
+ params.push(parseInt(options.limit));
117
+
118
+ let notes = rawDb.prepare(sql).all(...params);
119
+
120
+ // Filter by tag in-memory (tags stored as JSON array)
121
+ if (options.tag) {
122
+ notes = notes.filter((n) => {
123
+ try {
124
+ const tags = JSON.parse(n.tags || "[]");
125
+ return tags.includes(options.tag);
126
+ } catch {
127
+ return false;
128
+ }
129
+ });
130
+ }
131
+
132
+ if (options.json) {
133
+ console.log(JSON.stringify(notes, null, 2));
134
+ } else if (notes.length === 0) {
135
+ logger.info("No notes found");
136
+ } else {
137
+ logger.log(chalk.bold(`Notes (${notes.length}):\n`));
138
+ for (const n of notes) {
139
+ const tags = JSON.parse(n.tags || "[]");
140
+ const tagStr =
141
+ tags.length > 0 ? chalk.gray(` [${tags.join(", ")}]`) : "";
142
+ logger.log(
143
+ ` ${chalk.gray(n.id.slice(0, 8))} ${chalk.white(n.title)}${tagStr} ${chalk.gray(n.created_at)}`,
144
+ );
145
+ }
146
+ }
147
+
148
+ await shutdown();
149
+ } catch (err) {
150
+ logger.error(`Failed to list notes: ${err.message}`);
151
+ process.exit(1);
152
+ }
153
+ });
154
+
155
+ // note show
156
+ note
157
+ .command("show")
158
+ .description("Show note content")
159
+ .argument("<id>", "Note ID (or prefix)")
160
+ .option("--json", "Output as JSON")
161
+ .action(async (id, options) => {
162
+ try {
163
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
164
+ if (!ctx.db) {
165
+ logger.error("Database not available");
166
+ process.exit(1);
167
+ }
168
+
169
+ const rawDb = ctx.db.getDatabase();
170
+ ensureNotesTable(rawDb);
171
+
172
+ const note = rawDb
173
+ .prepare("SELECT * FROM notes WHERE id LIKE ? AND deleted_at IS NULL")
174
+ .get(`${id}%`);
175
+
176
+ if (!note) {
177
+ logger.error(`Note not found: ${id}`);
178
+ process.exit(1);
179
+ }
180
+
181
+ if (options.json) {
182
+ console.log(JSON.stringify(note, null, 2));
183
+ } else {
184
+ logger.log(chalk.bold(note.title));
185
+ logger.log(chalk.gray(`ID: ${note.id}`));
186
+ logger.log(
187
+ chalk.gray(
188
+ `Category: ${note.category} Created: ${note.created_at}`,
189
+ ),
190
+ );
191
+ const tags = JSON.parse(note.tags || "[]");
192
+ if (tags.length > 0) {
193
+ logger.log(chalk.gray(`Tags: ${tags.join(", ")}`));
194
+ }
195
+ logger.log("");
196
+ logger.log(note.content || chalk.gray("(empty)"));
197
+ }
198
+
199
+ await shutdown();
200
+ } catch (err) {
201
+ logger.error(`Failed to show note: ${err.message}`);
202
+ process.exit(1);
203
+ }
204
+ });
205
+
206
+ // note search
207
+ note
208
+ .command("search")
209
+ .description("Search notes")
210
+ .argument("<query>", "Search query")
211
+ .option("--json", "Output as JSON")
212
+ .action(async (query, options) => {
213
+ try {
214
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
215
+ if (!ctx.db) {
216
+ logger.error("Database not available");
217
+ process.exit(1);
218
+ }
219
+
220
+ const rawDb = ctx.db.getDatabase();
221
+ ensureNotesTable(rawDb);
222
+
223
+ const pattern = `%${query}%`;
224
+ const notes = rawDb
225
+ .prepare(
226
+ "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",
227
+ )
228
+ .all(pattern, pattern);
229
+
230
+ if (options.json) {
231
+ console.log(JSON.stringify(notes, null, 2));
232
+ } else if (notes.length === 0) {
233
+ logger.info(`No notes matching "${query}"`);
234
+ } else {
235
+ logger.log(
236
+ chalk.bold(`Search results for "${query}" (${notes.length}):\n`),
237
+ );
238
+ for (const n of notes) {
239
+ logger.log(
240
+ ` ${chalk.gray(n.id.slice(0, 8))} ${chalk.white(n.title)} ${chalk.gray(n.created_at)}`,
241
+ );
242
+ }
243
+ }
244
+
245
+ await shutdown();
246
+ } catch (err) {
247
+ logger.error(`Search failed: ${err.message}`);
248
+ process.exit(1);
249
+ }
250
+ });
251
+
252
+ // note delete
253
+ note
254
+ .command("delete")
255
+ .description("Delete a note (soft delete)")
256
+ .argument("<id>", "Note ID (or prefix)")
257
+ .option("--force", "Skip confirmation")
258
+ .action(async (id, options) => {
259
+ try {
260
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
261
+ if (!ctx.db) {
262
+ logger.error("Database not available");
263
+ process.exit(1);
264
+ }
265
+
266
+ const rawDb = ctx.db.getDatabase();
267
+ ensureNotesTable(rawDb);
268
+
269
+ const note = rawDb
270
+ .prepare(
271
+ "SELECT id, title FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
272
+ )
273
+ .get(`${id}%`);
274
+
275
+ if (!note) {
276
+ logger.error(`Note not found: ${id}`);
277
+ process.exit(1);
278
+ }
279
+
280
+ if (!options.force) {
281
+ const { confirm } = await import("@inquirer/prompts");
282
+ const ok = await confirm({
283
+ message: `Delete note "${note.title}"?`,
284
+ });
285
+ if (!ok) {
286
+ logger.info("Cancelled");
287
+ return;
288
+ }
289
+ }
290
+
291
+ rawDb
292
+ .prepare("UPDATE notes SET deleted_at = datetime('now') WHERE id = ?")
293
+ .run(note.id);
294
+
295
+ logger.success(`Note deleted: ${chalk.cyan(note.title)}`);
296
+ await shutdown();
297
+ } catch (err) {
298
+ logger.error(`Failed to delete note: ${err.message}`);
299
+ process.exit(1);
300
+ }
301
+ });
302
+ }