@memories.sh/cli 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,2899 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command21 } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { Command } from "commander";
8
+ import chalk from "chalk";
9
+
10
+ // src/lib/db.ts
11
+ import { createClient } from "@libsql/client";
12
+ import { mkdir, readFile, writeFile } from "fs/promises";
13
+ import { existsSync } from "fs";
14
+ import { join } from "path";
15
+ import { homedir } from "os";
16
+ function resolveConfigDir() {
17
+ return process.env.MEMORIES_DATA_DIR ?? join(homedir(), ".config", "memories");
18
+ }
19
+ function getConfigDir() {
20
+ return resolveConfigDir();
21
+ }
22
+ function getDbPath() {
23
+ return join(resolveConfigDir(), "local.db");
24
+ }
25
+ function getSyncConfigPath() {
26
+ return join(resolveConfigDir(), "sync.json");
27
+ }
28
+ var client;
29
+ async function getDb() {
30
+ if (client) return client;
31
+ const configDir = resolveConfigDir();
32
+ const dbPath = getDbPath();
33
+ await mkdir(configDir, { recursive: true });
34
+ const sync = await readSyncConfig();
35
+ if (sync) {
36
+ client = createClient({
37
+ url: `file:${dbPath}`,
38
+ syncUrl: sync.syncUrl,
39
+ authToken: sync.syncToken
40
+ });
41
+ await runMigrations(client);
42
+ await client.sync();
43
+ } else {
44
+ client = createClient({ url: `file:${dbPath}` });
45
+ await runMigrations(client);
46
+ }
47
+ return client;
48
+ }
49
+ function resetDb() {
50
+ client?.close();
51
+ client = void 0;
52
+ }
53
+ async function syncDb() {
54
+ const db = await getDb();
55
+ await db.sync();
56
+ }
57
+ async function saveSyncConfig(config) {
58
+ const configDir = resolveConfigDir();
59
+ await mkdir(configDir, { recursive: true });
60
+ await writeFile(getSyncConfigPath(), JSON.stringify(config, null, 2), "utf-8");
61
+ }
62
+ async function readSyncConfig() {
63
+ const syncPath = getSyncConfigPath();
64
+ if (!existsSync(syncPath)) return null;
65
+ const raw = await readFile(syncPath, "utf-8");
66
+ return JSON.parse(raw);
67
+ }
68
+ async function runMigrations(db) {
69
+ await db.execute(
70
+ `CREATE TABLE IF NOT EXISTS memories (
71
+ id TEXT PRIMARY KEY,
72
+ content TEXT NOT NULL,
73
+ tags TEXT,
74
+ scope TEXT NOT NULL DEFAULT 'global',
75
+ project_id TEXT,
76
+ type TEXT NOT NULL DEFAULT 'note',
77
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
78
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
79
+ deleted_at TEXT
80
+ )`
81
+ );
82
+ try {
83
+ await db.execute(`ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'global'`);
84
+ } catch {
85
+ }
86
+ try {
87
+ await db.execute(`ALTER TABLE memories ADD COLUMN project_id TEXT`);
88
+ } catch {
89
+ }
90
+ try {
91
+ await db.execute(`ALTER TABLE memories ADD COLUMN type TEXT NOT NULL DEFAULT 'note'`);
92
+ } catch {
93
+ }
94
+ await db.execute(
95
+ `CREATE TABLE IF NOT EXISTS configs (
96
+ key TEXT PRIMARY KEY,
97
+ value TEXT NOT NULL
98
+ )`
99
+ );
100
+ await db.execute(
101
+ `CREATE TABLE IF NOT EXISTS projects (
102
+ id TEXT PRIMARY KEY,
103
+ name TEXT NOT NULL,
104
+ path TEXT,
105
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
106
+ )`
107
+ );
108
+ await db.execute(
109
+ `CREATE TABLE IF NOT EXISTS sync_state (
110
+ id TEXT PRIMARY KEY,
111
+ last_synced_at TEXT,
112
+ remote_url TEXT
113
+ )`
114
+ );
115
+ await db.execute(
116
+ `CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
117
+ content,
118
+ tags,
119
+ content='memories',
120
+ content_rowid='rowid'
121
+ )`
122
+ );
123
+ await db.execute(`DROP TRIGGER IF EXISTS memories_ai`);
124
+ await db.execute(`DROP TRIGGER IF EXISTS memories_ad`);
125
+ await db.execute(`DROP TRIGGER IF EXISTS memories_au`);
126
+ await db.execute(`
127
+ CREATE TRIGGER memories_ai AFTER INSERT ON memories
128
+ WHEN NEW.deleted_at IS NULL
129
+ BEGIN
130
+ INSERT INTO memories_fts(rowid, content, tags) VALUES (NEW.rowid, NEW.content, NEW.tags);
131
+ END
132
+ `);
133
+ await db.execute(`
134
+ CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
135
+ INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES('delete', OLD.rowid, OLD.content, OLD.tags);
136
+ END
137
+ `);
138
+ await db.execute(`
139
+ CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
140
+ INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES('delete', OLD.rowid, OLD.content, OLD.tags);
141
+ INSERT INTO memories_fts(rowid, content, tags)
142
+ SELECT NEW.rowid, NEW.content, NEW.tags WHERE NEW.deleted_at IS NULL;
143
+ END
144
+ `);
145
+ try {
146
+ await db.execute(`CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)`);
147
+ } catch {
148
+ }
149
+ try {
150
+ await db.execute(`CREATE INDEX IF NOT EXISTS idx_memories_scope_project ON memories(scope, project_id)`);
151
+ } catch {
152
+ }
153
+ }
154
+
155
+ // src/lib/git.ts
156
+ import { execSync } from "child_process";
157
+ function getGitRemoteUrl(cwd) {
158
+ try {
159
+ const remote = execSync("git remote get-url origin", {
160
+ cwd,
161
+ encoding: "utf-8",
162
+ stdio: ["pipe", "pipe", "pipe"]
163
+ }).trim();
164
+ return remote || null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ function getGitRoot(cwd) {
170
+ try {
171
+ const root = execSync("git rev-parse --show-toplevel", {
172
+ cwd,
173
+ encoding: "utf-8",
174
+ stdio: ["pipe", "pipe", "pipe"]
175
+ }).trim();
176
+ return root || null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function normalizeGitUrl(url) {
182
+ let normalized = url.trim();
183
+ if (normalized.endsWith(".git")) {
184
+ normalized = normalized.slice(0, -4);
185
+ }
186
+ const sshMatch = normalized.match(/^git@([^:]+):(.+)$/);
187
+ if (sshMatch) {
188
+ return `${sshMatch[1]}/${sshMatch[2]}`;
189
+ }
190
+ const httpsMatch = normalized.match(/^https?:\/\/([^/]+)\/(.+)$/);
191
+ if (httpsMatch) {
192
+ return `${httpsMatch[1]}/${httpsMatch[2]}`;
193
+ }
194
+ return normalized;
195
+ }
196
+ function getProjectId(cwd) {
197
+ const remoteUrl = getGitRemoteUrl(cwd);
198
+ if (!remoteUrl) return null;
199
+ return normalizeGitUrl(remoteUrl);
200
+ }
201
+
202
+ // src/lib/memory.ts
203
+ import { nanoid } from "nanoid";
204
+ async function addMemory(content, opts) {
205
+ const db = await getDb();
206
+ const id = nanoid(12);
207
+ const tags = opts?.tags?.length ? opts.tags.join(",") : null;
208
+ const type = opts?.type ?? "note";
209
+ let scope = "global";
210
+ let projectId = null;
211
+ if (!opts?.global) {
212
+ projectId = opts?.projectId ?? getProjectId();
213
+ if (projectId) {
214
+ scope = "project";
215
+ }
216
+ }
217
+ await db.execute({
218
+ sql: `INSERT INTO memories (id, content, tags, scope, project_id, type) VALUES (?, ?, ?, ?, ?, ?)`,
219
+ args: [id, content, tags, scope, projectId, type]
220
+ });
221
+ const result = await db.execute({
222
+ sql: `SELECT * FROM memories WHERE id = ?`,
223
+ args: [id]
224
+ });
225
+ return result.rows[0];
226
+ }
227
+ async function searchMemories(query, opts) {
228
+ const db = await getDb();
229
+ const limit = opts?.limit ?? 20;
230
+ const includeGlobal = opts?.includeGlobal ?? true;
231
+ const projectId = opts?.globalOnly ? void 0 : opts?.projectId ?? getProjectId();
232
+ const scopeConditions = [];
233
+ const args = [];
234
+ if (includeGlobal) {
235
+ scopeConditions.push("m.scope = 'global'");
236
+ }
237
+ if (projectId) {
238
+ scopeConditions.push("(m.scope = 'project' AND m.project_id = ?)");
239
+ args.push(projectId);
240
+ }
241
+ if (scopeConditions.length === 0) {
242
+ return [];
243
+ }
244
+ let typeFilter = "";
245
+ if (opts?.types?.length) {
246
+ const placeholders = opts.types.map(() => "?").join(", ");
247
+ typeFilter = `AND m.type IN (${placeholders})`;
248
+ args.push(...opts.types);
249
+ }
250
+ const ftsQuery = query.split(/\s+/).filter(Boolean).map((term) => `"${term}"*`).join(" OR ");
251
+ args.push(limit);
252
+ try {
253
+ const result = await db.execute({
254
+ sql: `
255
+ SELECT m.*, bm25(memories_fts) as rank
256
+ FROM memories m
257
+ JOIN memories_fts fts ON m.rowid = fts.rowid
258
+ WHERE memories_fts MATCH ?
259
+ AND m.deleted_at IS NULL
260
+ AND (${scopeConditions.join(" OR ")})
261
+ ${typeFilter}
262
+ ORDER BY rank ASC, m.created_at DESC
263
+ LIMIT ?
264
+ `,
265
+ args: [ftsQuery, ...args]
266
+ });
267
+ return result.rows;
268
+ } catch (error) {
269
+ console.error("FTS search failed, falling back to LIKE:", error);
270
+ return searchMemoriesLike(query, opts);
271
+ }
272
+ }
273
+ async function searchMemoriesLike(query, opts) {
274
+ const db = await getDb();
275
+ const limit = opts?.limit ?? 20;
276
+ const includeGlobal = opts?.includeGlobal ?? true;
277
+ const projectId = opts?.globalOnly ? void 0 : opts?.projectId ?? getProjectId();
278
+ const conditions = ["deleted_at IS NULL", "content LIKE ?"];
279
+ const args = [`%${query}%`];
280
+ const scopeConditions = [];
281
+ if (includeGlobal) {
282
+ scopeConditions.push("scope = 'global'");
283
+ }
284
+ if (projectId) {
285
+ scopeConditions.push("(scope = 'project' AND project_id = ?)");
286
+ args.push(projectId);
287
+ }
288
+ if (scopeConditions.length === 0) {
289
+ return [];
290
+ }
291
+ conditions.push(`(${scopeConditions.join(" OR ")})`);
292
+ if (opts?.types?.length) {
293
+ const placeholders = opts.types.map(() => "?").join(", ");
294
+ conditions.push(`type IN (${placeholders})`);
295
+ args.push(...opts.types);
296
+ }
297
+ args.push(limit);
298
+ const result = await db.execute({
299
+ sql: `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC LIMIT ?`,
300
+ args
301
+ });
302
+ return result.rows;
303
+ }
304
+ async function listMemories(opts) {
305
+ const db = await getDb();
306
+ const limit = opts?.limit ?? 50;
307
+ const includeGlobal = opts?.includeGlobal ?? true;
308
+ const projectId = opts?.globalOnly ? void 0 : opts?.projectId ?? getProjectId();
309
+ const conditions = ["deleted_at IS NULL"];
310
+ const args = [];
311
+ const scopeConditions = [];
312
+ if (includeGlobal) {
313
+ scopeConditions.push("scope = 'global'");
314
+ }
315
+ if (projectId) {
316
+ scopeConditions.push("(scope = 'project' AND project_id = ?)");
317
+ args.push(projectId);
318
+ }
319
+ if (scopeConditions.length === 0) {
320
+ return [];
321
+ }
322
+ conditions.push(`(${scopeConditions.join(" OR ")})`);
323
+ if (opts?.tags?.length) {
324
+ const tagClauses = opts.tags.map(() => `tags LIKE ?`).join(" OR ");
325
+ conditions.push(`(${tagClauses})`);
326
+ args.push(...opts.tags.map((t) => `%${t}%`));
327
+ }
328
+ if (opts?.types?.length) {
329
+ const placeholders = opts.types.map(() => "?").join(", ");
330
+ conditions.push(`type IN (${placeholders})`);
331
+ args.push(...opts.types);
332
+ }
333
+ args.push(limit);
334
+ const result = await db.execute({
335
+ sql: `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY type ASC, scope ASC, created_at DESC LIMIT ?`,
336
+ args
337
+ });
338
+ return result.rows;
339
+ }
340
+ async function getRules(opts) {
341
+ const db = await getDb();
342
+ const projectId = opts?.projectId ?? getProjectId();
343
+ const conditions = ["deleted_at IS NULL", "type = 'rule'"];
344
+ const args = [];
345
+ const scopeConditions = ["scope = 'global'"];
346
+ if (projectId) {
347
+ scopeConditions.push("(scope = 'project' AND project_id = ?)");
348
+ args.push(projectId);
349
+ }
350
+ conditions.push(`(${scopeConditions.join(" OR ")})`);
351
+ const result = await db.execute({
352
+ sql: `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY scope ASC, created_at ASC`,
353
+ args
354
+ });
355
+ return result.rows;
356
+ }
357
+ async function getContext(query, opts) {
358
+ const projectId = opts?.projectId ?? getProjectId();
359
+ const limit = opts?.limit ?? 10;
360
+ const rules = await getRules({ projectId: projectId ?? void 0 });
361
+ let memories = [];
362
+ if (query) {
363
+ memories = await searchMemories(query, {
364
+ projectId: projectId ?? void 0,
365
+ limit,
366
+ types: ["decision", "fact", "note"]
367
+ // Exclude rules, they're already included
368
+ });
369
+ }
370
+ return { rules, memories };
371
+ }
372
+ async function updateMemory(id, updates) {
373
+ const db = await getDb();
374
+ const existing = await db.execute({
375
+ sql: `SELECT * FROM memories WHERE id = ? AND deleted_at IS NULL`,
376
+ args: [id]
377
+ });
378
+ if (existing.rows.length === 0) return null;
379
+ const setClauses = ["updated_at = datetime('now')"];
380
+ const args = [];
381
+ if (updates.content !== void 0) {
382
+ setClauses.push("content = ?");
383
+ args.push(updates.content);
384
+ }
385
+ if (updates.tags !== void 0) {
386
+ setClauses.push("tags = ?");
387
+ args.push(updates.tags.length ? updates.tags.join(",") : null);
388
+ }
389
+ if (updates.type !== void 0) {
390
+ setClauses.push("type = ?");
391
+ args.push(updates.type);
392
+ }
393
+ args.push(id);
394
+ await db.execute({
395
+ sql: `UPDATE memories SET ${setClauses.join(", ")} WHERE id = ?`,
396
+ args
397
+ });
398
+ const result = await db.execute({
399
+ sql: `SELECT * FROM memories WHERE id = ?`,
400
+ args: [id]
401
+ });
402
+ return result.rows[0];
403
+ }
404
+ async function forgetMemory(id) {
405
+ const db = await getDb();
406
+ const existing = await db.execute({
407
+ sql: `SELECT id FROM memories WHERE id = ? AND deleted_at IS NULL`,
408
+ args: [id]
409
+ });
410
+ if (existing.rows.length === 0) return false;
411
+ await db.execute({
412
+ sql: `UPDATE memories SET deleted_at = datetime('now') WHERE id = ?`,
413
+ args: [id]
414
+ });
415
+ return true;
416
+ }
417
+ async function findMemoriesToForget(filter) {
418
+ const db = await getDb();
419
+ const conditions = ["deleted_at IS NULL"];
420
+ const args = [];
421
+ if (filter.types?.length) {
422
+ const placeholders = filter.types.map(() => "?").join(", ");
423
+ conditions.push(`type IN (${placeholders})`);
424
+ args.push(...filter.types);
425
+ }
426
+ if (filter.tags?.length) {
427
+ const tagClauses = filter.tags.map(() => `tags LIKE ?`).join(" OR ");
428
+ conditions.push(`(${tagClauses})`);
429
+ args.push(...filter.tags.map((t) => `%${t}%`));
430
+ }
431
+ if (filter.olderThanDays !== void 0) {
432
+ conditions.push(`created_at < datetime('now', ?)`);
433
+ args.push(`-${filter.olderThanDays} days`);
434
+ }
435
+ if (filter.pattern) {
436
+ const likePattern = filter.pattern.replace(/%/g, "\\%").replace(/_/g, "\\_").replace(/\*/g, "%").replace(/\?/g, "_");
437
+ conditions.push(`content LIKE ? ESCAPE '\\'`);
438
+ args.push(`%${likePattern}%`);
439
+ }
440
+ if (filter.projectId) {
441
+ conditions.push("(scope = 'project' AND project_id = ?)");
442
+ args.push(filter.projectId);
443
+ }
444
+ const result = await db.execute({
445
+ sql: `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`,
446
+ args
447
+ });
448
+ return result.rows;
449
+ }
450
+ async function bulkForgetByIds(ids) {
451
+ if (ids.length === 0) return 0;
452
+ const db = await getDb();
453
+ const batchSize = 500;
454
+ for (let i = 0; i < ids.length; i += batchSize) {
455
+ const batch = ids.slice(i, i + batchSize);
456
+ const placeholders = batch.map(() => "?").join(", ");
457
+ await db.execute({
458
+ sql: `UPDATE memories SET deleted_at = datetime('now') WHERE id IN (${placeholders})`,
459
+ args: batch
460
+ });
461
+ }
462
+ return ids.length;
463
+ }
464
+
465
+ // src/commands/init.ts
466
+ var initCommand = new Command("init").description("Initialize memories for the current project or globally").option("-g, --global", "Initialize global memories (user-wide)").option("-r, --rule <rule>", "Add an initial rule", (val, acc) => [...acc, val], []).action(async (opts) => {
467
+ try {
468
+ await getDb();
469
+ const configDir = getConfigDir();
470
+ if (opts.global) {
471
+ console.log(chalk.green("\u2713") + " Initialized global memories");
472
+ console.log(chalk.dim(` Database: ${configDir}/local.db`));
473
+ } else {
474
+ const projectId = getProjectId();
475
+ const gitRoot = getGitRoot();
476
+ if (!projectId) {
477
+ console.log(chalk.yellow("\u26A0") + " Not in a git repository.");
478
+ console.log(chalk.dim(" Run from a git repo for project-scoped memories, or use --global"));
479
+ return;
480
+ }
481
+ console.log(chalk.green("\u2713") + " Initialized project memories");
482
+ console.log(chalk.dim(` Project: ${projectId}`));
483
+ console.log(chalk.dim(` Root: ${gitRoot}`));
484
+ console.log(chalk.dim(` Database: ${configDir}/local.db`));
485
+ }
486
+ if (opts.rule?.length) {
487
+ console.log("");
488
+ console.log(chalk.blue("Adding rules:"));
489
+ for (const rule of opts.rule) {
490
+ const memory = await addMemory(rule, {
491
+ type: "rule",
492
+ global: opts.global
493
+ });
494
+ console.log(chalk.dim(` ${memory.id}: ${rule}`));
495
+ }
496
+ }
497
+ console.log("");
498
+ console.log(chalk.dim("Quick start:"));
499
+ console.log(chalk.dim(` memories add "Your first memory"`));
500
+ console.log(chalk.dim(` memories add --rule "Always use TypeScript strict mode"`));
501
+ console.log(chalk.dim(` memories recall "what are my coding preferences"`));
502
+ } catch (error) {
503
+ console.error(chalk.red("\u2717") + " Failed to initialize:", error instanceof Error ? error.message : "Unknown error");
504
+ process.exit(1);
505
+ }
506
+ });
507
+
508
+ // src/commands/add.ts
509
+ import { Command as Command2 } from "commander";
510
+ import chalk2 from "chalk";
511
+ var VALID_TYPES = ["rule", "decision", "fact", "note"];
512
+ var addCommand = new Command2("add").description("Add a new memory").argument("<content>", "Memory content").option("-t, --tags <tags>", "Comma-separated tags").option("-g, --global", "Store as global memory (default: project-scoped if in git repo)").option("--type <type>", "Memory type: rule, decision, fact, note (default: note)").option("-r, --rule", "Shorthand for --type rule").option("-d, --decision", "Shorthand for --type decision").option("-f, --fact", "Shorthand for --type fact").action(async (content, opts) => {
513
+ try {
514
+ const tags = opts.tags?.split(",").map((t) => t.trim());
515
+ let type = "note";
516
+ if (opts.rule) type = "rule";
517
+ else if (opts.decision) type = "decision";
518
+ else if (opts.fact) type = "fact";
519
+ else if (opts.type) {
520
+ if (!VALID_TYPES.includes(opts.type)) {
521
+ console.error(chalk2.red("\u2717") + ` Invalid type "${opts.type}". Valid types: ${VALID_TYPES.join(", ")}`);
522
+ process.exit(1);
523
+ }
524
+ type = opts.type;
525
+ }
526
+ const memory = await addMemory(content, { tags, global: opts.global, type });
527
+ const typeIcon = type === "rule" ? "\u{1F4CC}" : type === "decision" ? "\u{1F4A1}" : type === "fact" ? "\u{1F4CB}" : "\u{1F4DD}";
528
+ const scopeInfo = memory.scope === "global" ? chalk2.dim("(global)") : chalk2.dim(`(project)`);
529
+ console.log(chalk2.green("\u2713") + ` ${typeIcon} Stored ${type} ${chalk2.dim(memory.id)} ${scopeInfo}`);
530
+ } catch (error) {
531
+ console.error(chalk2.red("\u2717") + " Failed to add memory:", error instanceof Error ? error.message : "Unknown error");
532
+ process.exit(1);
533
+ }
534
+ });
535
+
536
+ // src/commands/recall.ts
537
+ import { Command as Command3 } from "commander";
538
+ import chalk3 from "chalk";
539
+ var TYPE_ICONS = {
540
+ rule: "\u{1F4CC}",
541
+ decision: "\u{1F4A1}",
542
+ fact: "\u{1F4CB}",
543
+ note: "\u{1F4DD}"
544
+ };
545
+ var TYPE_COLORS = {
546
+ rule: chalk3.yellow,
547
+ decision: chalk3.cyan,
548
+ fact: chalk3.green,
549
+ note: chalk3.white
550
+ };
551
+ function formatMemory(m, verbose) {
552
+ const icon = TYPE_ICONS[m.type] || "\u{1F4DD}";
553
+ const colorFn = TYPE_COLORS[m.type] || chalk3.white;
554
+ const scope = m.scope === "global" ? chalk3.dim("G") : chalk3.dim("P");
555
+ const tags = m.tags ? chalk3.dim(` [${m.tags}]`) : "";
556
+ if (verbose) {
557
+ return `${icon} ${scope} ${chalk3.dim(m.id)} ${colorFn(m.content)}${tags}`;
558
+ }
559
+ return `${icon} ${colorFn(m.content)}`;
560
+ }
561
+ var recallCommand = new Command3("recall").description("Recall context - get rules and relevant memories for AI agents").argument("[query]", "Optional search query to find relevant memories").option("-l, --limit <n>", "Max memories to return (excludes rules)", "10").option("-r, --rules-only", "Only return rules").option("-v, --verbose", "Show memory IDs and metadata").option("--json", "Output as JSON (for programmatic use)").action(async (query, opts) => {
562
+ try {
563
+ const projectId = getProjectId();
564
+ if (opts.rulesOnly) {
565
+ const rules2 = await getRules({ projectId: projectId ?? void 0 });
566
+ if (opts.json) {
567
+ console.log(JSON.stringify({ rules: rules2 }, null, 2));
568
+ return;
569
+ }
570
+ if (rules2.length === 0) {
571
+ console.log(chalk3.dim("No rules defined."));
572
+ console.log(chalk3.dim('Add one with: memories add --rule "Your rule here"'));
573
+ return;
574
+ }
575
+ console.log(chalk3.bold("Rules:"));
576
+ for (const rule of rules2) {
577
+ console.log(formatMemory(rule, opts.verbose ?? false));
578
+ }
579
+ return;
580
+ }
581
+ const { rules, memories } = await getContext(query, {
582
+ projectId: projectId ?? void 0,
583
+ limit: parseInt(opts.limit, 10)
584
+ });
585
+ if (opts.json) {
586
+ console.log(JSON.stringify({ rules, memories }, null, 2));
587
+ return;
588
+ }
589
+ if (rules.length > 0) {
590
+ console.log(chalk3.bold("Rules:"));
591
+ for (const rule of rules) {
592
+ console.log(formatMemory(rule, opts.verbose ?? false));
593
+ }
594
+ console.log("");
595
+ }
596
+ if (memories.length > 0) {
597
+ console.log(chalk3.bold(query ? `Relevant to "${query}":` : "Recent memories:"));
598
+ for (const m of memories) {
599
+ console.log(formatMemory(m, opts.verbose ?? false));
600
+ }
601
+ } else if (query) {
602
+ console.log(chalk3.dim(`No memories found matching "${query}"`));
603
+ }
604
+ if (rules.length === 0 && memories.length === 0) {
605
+ console.log(chalk3.dim("No memories found."));
606
+ console.log(chalk3.dim('Add some with: memories add "Your memory here"'));
607
+ }
608
+ } catch (error) {
609
+ console.error(chalk3.red("\u2717") + " Failed to recall:", error instanceof Error ? error.message : "Unknown error");
610
+ process.exit(1);
611
+ }
612
+ });
613
+
614
+ // src/commands/prompt.ts
615
+ import { Command as Command4 } from "commander";
616
+ import { execFileSync } from "child_process";
617
+ import chalk4 from "chalk";
618
+ var VALID_TYPES2 = ["rule", "decision", "fact", "note"];
619
+ function formatMarkdown(sections) {
620
+ return sections.map(({ title, memories }) => {
621
+ const items = memories.map((m) => `- ${m.content}`).join("\n");
622
+ return `## ${title}
623
+
624
+ ${items}`;
625
+ }).join("\n\n");
626
+ }
627
+ function formatXml(sections) {
628
+ return sections.map(({ title, memories }) => {
629
+ const tag = title.toLowerCase().replace(/\s+/g, "-");
630
+ const items = memories.map((m) => ` <item>${m.content}</item>`).join("\n");
631
+ return `<${tag}>
632
+ ${items}
633
+ </${tag}>`;
634
+ }).join("\n");
635
+ }
636
+ function formatPlain(sections) {
637
+ return sections.flatMap(({ memories }) => memories.map((m) => m.content)).join("\n");
638
+ }
639
+ function copyToClipboard(text) {
640
+ try {
641
+ const platform = process.platform;
642
+ if (platform === "darwin") {
643
+ execFileSync("pbcopy", [], { input: text });
644
+ } else if (platform === "linux") {
645
+ execFileSync("xclip", ["-selection", "clipboard"], { input: text });
646
+ } else if (platform === "win32") {
647
+ execFileSync("clip", [], { input: text });
648
+ } else {
649
+ return false;
650
+ }
651
+ return true;
652
+ } catch {
653
+ return false;
654
+ }
655
+ }
656
+ var promptCommand = new Command4("prompt").description("Output memories formatted for AI system prompts").option("-f, --format <format>", "Output format: markdown, xml, plain (default: markdown)").option("-i, --include <types>", "Include additional types: decisions,facts,notes (comma-separated)").option("-a, --all", "Include all memory types").option("-c, --copy", "Copy output to clipboard").option("-q, --quiet", "No stderr status messages (just the prompt)").action(async (opts) => {
657
+ try {
658
+ const format = opts.format ?? "markdown";
659
+ if (!["markdown", "xml", "plain"].includes(format)) {
660
+ console.error(chalk4.red("\u2717") + ` Invalid format "${opts.format}". Use: markdown, xml, plain`);
661
+ process.exit(1);
662
+ }
663
+ const projectId = getProjectId() ?? void 0;
664
+ const rules = await getRules({ projectId });
665
+ const extraTypes = [];
666
+ if (opts.all) {
667
+ extraTypes.push("decision", "fact", "note");
668
+ } else if (opts.include) {
669
+ for (const t of opts.include.split(",").map((s) => s.trim())) {
670
+ const normalized = t.replace(/s$/, "");
671
+ if (!VALID_TYPES2.includes(normalized)) {
672
+ console.error(chalk4.red("\u2717") + ` Invalid type "${t}". Valid: decisions, facts, notes`);
673
+ process.exit(1);
674
+ }
675
+ if (normalized !== "rule") extraTypes.push(normalized);
676
+ }
677
+ }
678
+ const sections = [];
679
+ if (rules.length > 0) {
680
+ const globalRules = rules.filter((r) => r.scope === "global");
681
+ const projectRules = rules.filter((r) => r.scope === "project");
682
+ if (globalRules.length > 0 && projectRules.length > 0) {
683
+ sections.push({ title: "Global Rules", memories: globalRules });
684
+ sections.push({ title: "Project Rules", memories: projectRules });
685
+ } else {
686
+ sections.push({ title: "Rules", memories: rules });
687
+ }
688
+ }
689
+ for (const type of extraTypes) {
690
+ const memories = await listMemories({
691
+ types: [type],
692
+ projectId
693
+ });
694
+ if (memories.length > 0) {
695
+ const title = type === "decision" ? "Key Decisions" : type === "fact" ? "Project Facts" : "Notes";
696
+ sections.push({ title, memories });
697
+ }
698
+ }
699
+ if (sections.length === 0) {
700
+ if (!opts.quiet) {
701
+ console.error(chalk4.dim('No memories found. Add rules with: memories add --rule "Your rule"'));
702
+ }
703
+ return;
704
+ }
705
+ let output;
706
+ switch (format) {
707
+ case "xml":
708
+ output = formatXml(sections);
709
+ break;
710
+ case "plain":
711
+ output = formatPlain(sections);
712
+ break;
713
+ default:
714
+ output = formatMarkdown(sections);
715
+ }
716
+ if (opts.copy) {
717
+ const copied = copyToClipboard(output);
718
+ if (copied) {
719
+ console.log(output);
720
+ if (!opts.quiet) {
721
+ console.error(chalk4.green("\u2713") + " Copied to clipboard");
722
+ }
723
+ } else {
724
+ console.log(output);
725
+ if (!opts.quiet) {
726
+ console.error(chalk4.yellow("\u26A0") + " Could not copy to clipboard (unsupported platform)");
727
+ }
728
+ }
729
+ } else {
730
+ console.log(output);
731
+ }
732
+ } catch (error) {
733
+ console.error(chalk4.red("\u2717") + " Failed to generate prompt:", error instanceof Error ? error.message : "Unknown error");
734
+ process.exit(1);
735
+ }
736
+ });
737
+
738
+ // src/commands/search.ts
739
+ import { Command as Command5 } from "commander";
740
+ import chalk5 from "chalk";
741
+ var TYPE_ICONS2 = {
742
+ rule: "\u{1F4CC}",
743
+ decision: "\u{1F4A1}",
744
+ fact: "\u{1F4CB}",
745
+ note: "\u{1F4DD}"
746
+ };
747
+ var VALID_TYPES3 = ["rule", "decision", "fact", "note"];
748
+ function formatMemory2(m) {
749
+ const icon = TYPE_ICONS2[m.type] || "\u{1F4DD}";
750
+ const scope = m.scope === "global" ? chalk5.dim("G") : chalk5.dim("P");
751
+ const tags = m.tags ? chalk5.dim(` [${m.tags}]`) : "";
752
+ return `${icon} ${scope} ${chalk5.dim(m.id)} ${m.content}${tags}`;
753
+ }
754
+ var searchCommand = new Command5("search").description("Search memories using full-text search").argument("<query>", "Search query").option("-l, --limit <n>", "Max results", "20").option("--type <type>", "Filter by type: rule, decision, fact, note").option("-g, --global", "Search only global memories").option("--project-only", "Search only project memories (exclude global)").option("--json", "Output as JSON").action(async (query, opts) => {
755
+ try {
756
+ let types;
757
+ if (opts.type) {
758
+ if (!VALID_TYPES3.includes(opts.type)) {
759
+ console.error(chalk5.red("\u2717") + ` Invalid type "${opts.type}". Valid types: ${VALID_TYPES3.join(", ")}`);
760
+ process.exit(1);
761
+ }
762
+ types = [opts.type];
763
+ }
764
+ let globalOnly = false;
765
+ let includeGlobal = true;
766
+ let projectId;
767
+ if (opts.global) {
768
+ globalOnly = true;
769
+ } else if (opts.projectOnly) {
770
+ includeGlobal = false;
771
+ projectId = getProjectId() ?? void 0;
772
+ if (!projectId) {
773
+ console.log(chalk5.yellow("\u26A0") + " Not in a git repository. No project memories to search.");
774
+ return;
775
+ }
776
+ }
777
+ const memories = await searchMemories(query, {
778
+ limit: parseInt(opts.limit, 10),
779
+ types,
780
+ projectId,
781
+ includeGlobal,
782
+ globalOnly
783
+ });
784
+ if (opts.json) {
785
+ console.log(JSON.stringify(memories, null, 2));
786
+ return;
787
+ }
788
+ if (memories.length === 0) {
789
+ console.log(chalk5.dim(`No memories found matching "${query}"`));
790
+ return;
791
+ }
792
+ console.log(chalk5.bold(`Results for "${query}":`));
793
+ console.log("");
794
+ for (const m of memories) {
795
+ console.log(formatMemory2(m));
796
+ }
797
+ console.log(chalk5.dim(`
798
+ ${memories.length} results`));
799
+ } catch (error) {
800
+ console.error(chalk5.red("\u2717") + " Failed to search:", error instanceof Error ? error.message : "Unknown error");
801
+ process.exit(1);
802
+ }
803
+ });
804
+
805
+ // src/commands/list.ts
806
+ import { Command as Command6 } from "commander";
807
+ import chalk6 from "chalk";
808
+ var TYPE_COLORS2 = {
809
+ rule: chalk6.blue,
810
+ decision: chalk6.yellow,
811
+ fact: chalk6.green,
812
+ note: chalk6.dim
813
+ };
814
+ var TYPE_LABELS = {
815
+ rule: "rule",
816
+ decision: "decision",
817
+ fact: "fact",
818
+ note: "note"
819
+ };
820
+ var VALID_TYPES4 = ["rule", "decision", "fact", "note"];
821
+ var MAX_CONTENT_WIDTH = 80;
822
+ function truncate(str, max) {
823
+ if (str.length <= max) return str;
824
+ return str.slice(0, max - 1) + "\u2026";
825
+ }
826
+ function formatMemory3(m) {
827
+ const typeColor = TYPE_COLORS2[m.type] ?? chalk6.dim;
828
+ const typeLabel = typeColor(TYPE_LABELS[m.type].padEnd(9));
829
+ const scope = m.scope === "global" ? chalk6.magenta("G") : chalk6.cyan("P");
830
+ const id = chalk6.dim(m.id);
831
+ const content = truncate(m.content, MAX_CONTENT_WIDTH);
832
+ const tags = m.tags ? chalk6.dim(` [${m.tags}]`) : "";
833
+ return ` ${scope} ${typeLabel} ${id} ${content}${tags}`;
834
+ }
835
+ var listCommand = new Command6("list").description("List memories").option("-l, --limit <n>", "Max results", "50").option("-t, --tags <tags>", "Filter by comma-separated tags").option("--type <type>", "Filter by type: rule, decision, fact, note").option("-g, --global", "Show only global memories").option("--project-only", "Show only project memories (exclude global)").option("--json", "Output as JSON").action(async (opts) => {
836
+ try {
837
+ const tags = opts.tags?.split(",").map((t) => t.trim());
838
+ let types;
839
+ if (opts.type) {
840
+ if (!VALID_TYPES4.includes(opts.type)) {
841
+ console.error(chalk6.red("\u2717") + ` Invalid type "${opts.type}". Valid types: ${VALID_TYPES4.join(", ")}`);
842
+ process.exit(1);
843
+ }
844
+ types = [opts.type];
845
+ }
846
+ let globalOnly = false;
847
+ let includeGlobal = true;
848
+ let projectId;
849
+ if (opts.global) {
850
+ globalOnly = true;
851
+ } else if (opts.projectOnly) {
852
+ includeGlobal = false;
853
+ projectId = getProjectId() ?? void 0;
854
+ if (!projectId) {
855
+ console.log(chalk6.yellow("\u26A0") + " Not in a git repository. No project memories to show.");
856
+ return;
857
+ }
858
+ }
859
+ const memories = await listMemories({
860
+ limit: parseInt(opts.limit, 10),
861
+ tags,
862
+ types,
863
+ projectId,
864
+ includeGlobal,
865
+ globalOnly
866
+ });
867
+ if (opts.json) {
868
+ console.log(JSON.stringify(memories, null, 2));
869
+ return;
870
+ }
871
+ if (memories.length === 0) {
872
+ console.log(chalk6.dim("No memories found."));
873
+ return;
874
+ }
875
+ const currentProject = getProjectId();
876
+ if (currentProject && !opts.global) {
877
+ console.log(chalk6.dim(` Project: ${currentProject}
878
+ `));
879
+ }
880
+ for (const m of memories) {
881
+ console.log(formatMemory3(m));
882
+ }
883
+ console.log(chalk6.dim(`
884
+ ${memories.length} memories`));
885
+ } catch (error) {
886
+ console.error(chalk6.red("\u2717") + " Failed to list memories:", error instanceof Error ? error.message : "Unknown error");
887
+ process.exit(1);
888
+ }
889
+ });
890
+
891
+ // src/commands/forget.ts
892
+ import { Command as Command7 } from "commander";
893
+ import { createInterface } from "readline";
894
+ import chalk7 from "chalk";
895
+ var VALID_TYPES5 = ["rule", "decision", "fact", "note"];
896
+ var TYPE_ICONS3 = {
897
+ rule: "\u{1F4CC}",
898
+ decision: "\u{1F4A1}",
899
+ fact: "\u{1F4CB}",
900
+ note: "\u{1F4DD}"
901
+ };
902
+ async function confirm(message) {
903
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
904
+ return new Promise((resolve3) => {
905
+ rl.question(message, (answer) => {
906
+ rl.close();
907
+ resolve3(answer.toLowerCase().startsWith("y"));
908
+ });
909
+ });
910
+ }
911
+ var forgetCommand = new Command7("forget").description("Soft-delete memories by ID or bulk filter").argument("[id]", "Memory ID to forget (omit for bulk operations)").option("--type <type>", "Forget all memories of this type: rule, decision, fact, note").option("--tag <tag>", "Forget all memories with this tag").option("--older-than <days>", "Forget memories older than N days").option("--pattern <pattern>", "Forget memories matching pattern (use * as wildcard)").option("--all", "Forget ALL memories (requires --force or confirmation)").option("--project-only", "Only forget project-scoped memories").option("--dry-run", "Preview what would be deleted without deleting").option("--force", "Skip confirmation prompt").action(async (id, opts) => {
912
+ try {
913
+ if (id) {
914
+ const deleted = await forgetMemory(id);
915
+ if (deleted) {
916
+ console.log(chalk7.green("\u2713") + ` Forgot memory ${chalk7.dim(id)}`);
917
+ } else {
918
+ console.error(chalk7.red("\u2717") + ` Memory ${id} not found or already forgotten.`);
919
+ process.exit(1);
920
+ }
921
+ return;
922
+ }
923
+ const hasBulkFilter = opts.type || opts.tag || opts.olderThan || opts.pattern || opts.all;
924
+ if (!hasBulkFilter) {
925
+ console.error(chalk7.red("\u2717") + " Provide a memory ID or a bulk filter (--type, --tag, --older-than, --pattern, --all)");
926
+ process.exit(1);
927
+ }
928
+ if (opts.all && (opts.type || opts.tag || opts.olderThan || opts.pattern)) {
929
+ console.error(chalk7.red("\u2717") + " --all cannot be combined with other filters. Use --all alone to delete everything.");
930
+ process.exit(1);
931
+ }
932
+ if (opts.type && !VALID_TYPES5.includes(opts.type)) {
933
+ console.error(chalk7.red("\u2717") + ` Invalid type "${opts.type}". Valid: ${VALID_TYPES5.join(", ")}`);
934
+ process.exit(1);
935
+ }
936
+ if (opts.olderThan && (isNaN(parseInt(opts.olderThan, 10)) || parseInt(opts.olderThan, 10) <= 0)) {
937
+ console.error(chalk7.red("\u2717") + " --older-than must be a positive number of days");
938
+ process.exit(1);
939
+ }
940
+ const filter = {
941
+ types: opts.type ? [opts.type] : void 0,
942
+ tags: opts.tag ? [opts.tag] : void 0,
943
+ olderThanDays: opts.olderThan ? parseInt(opts.olderThan, 10) : void 0,
944
+ pattern: opts.pattern,
945
+ all: opts.all,
946
+ projectId: opts.projectOnly ? getProjectId() ?? void 0 : void 0
947
+ };
948
+ const matches = await findMemoriesToForget(filter);
949
+ if (matches.length === 0) {
950
+ console.log(chalk7.dim("No memories match the filter."));
951
+ return;
952
+ }
953
+ console.log(chalk7.bold(`${matches.length} memories will be forgotten:
954
+ `));
955
+ for (const m of matches.slice(0, 30)) {
956
+ const icon = TYPE_ICONS3[m.type] || "\u{1F4DD}";
957
+ const scope = m.scope === "global" ? chalk7.dim("G") : chalk7.dim("P");
958
+ console.log(` ${icon} ${scope} ${chalk7.dim(m.id)} ${m.content}`);
959
+ }
960
+ if (matches.length > 30) {
961
+ console.log(chalk7.dim(` ... and ${matches.length - 30} more`));
962
+ }
963
+ console.log("");
964
+ if (opts.dryRun) {
965
+ console.log(chalk7.yellow("Dry run") + " \u2014 no memories were deleted.");
966
+ return;
967
+ }
968
+ if (!opts.force) {
969
+ const proceed = await confirm(
970
+ chalk7.yellow(`Forget ${matches.length} memories? This is a soft-delete. [y/N] `)
971
+ );
972
+ if (!proceed) {
973
+ console.log("Cancelled.");
974
+ return;
975
+ }
976
+ }
977
+ const ids = matches.map((m) => m.id);
978
+ const count = await bulkForgetByIds(ids);
979
+ console.log(chalk7.green("\u2713") + ` Forgot ${count} memories.`);
980
+ } catch (error) {
981
+ console.error(chalk7.red("\u2717") + " Failed to forget:", error instanceof Error ? error.message : "Unknown error");
982
+ process.exit(1);
983
+ }
984
+ });
985
+
986
+ // src/commands/export.ts
987
+ import { Command as Command8 } from "commander";
988
+ import chalk8 from "chalk";
989
+ import { writeFile as writeFile2 } from "fs/promises";
990
+ var exportCommand = new Command8("export").description("Export memories to JSON or YAML file").option("-o, --output <file>", "Output file path (default: stdout)").option("-f, --format <format>", "Output format: json, yaml (default: json)", "json").option("-g, --global", "Export only global memories").option("--project-only", "Export only project memories").option("--type <type>", "Filter by type: rule, decision, fact, note").action(async (opts) => {
991
+ try {
992
+ const projectId = getProjectId();
993
+ let globalOnly = false;
994
+ let includeGlobal = true;
995
+ let queryProjectId;
996
+ if (opts.global) {
997
+ globalOnly = true;
998
+ } else if (opts.projectOnly) {
999
+ includeGlobal = false;
1000
+ queryProjectId = projectId ?? void 0;
1001
+ if (!queryProjectId) {
1002
+ console.error(chalk8.red("\u2717") + " Not in a git repository. No project memories to export.");
1003
+ process.exit(1);
1004
+ }
1005
+ }
1006
+ const types = opts.type ? [opts.type] : void 0;
1007
+ const memories = await listMemories({
1008
+ limit: 1e4,
1009
+ // Export all
1010
+ types,
1011
+ projectId: queryProjectId,
1012
+ includeGlobal,
1013
+ globalOnly
1014
+ });
1015
+ const exportData = {
1016
+ version: "1.0",
1017
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
1018
+ project_id: projectId,
1019
+ memories: memories.map((m) => ({
1020
+ id: m.id,
1021
+ content: m.content,
1022
+ type: m.type,
1023
+ tags: m.tags ? m.tags.split(",") : [],
1024
+ scope: m.scope,
1025
+ created_at: m.created_at
1026
+ }))
1027
+ };
1028
+ let output;
1029
+ if (opts.format === "yaml") {
1030
+ const yaml = await import("yaml");
1031
+ output = yaml.stringify(exportData);
1032
+ } else {
1033
+ output = JSON.stringify(exportData, null, 2);
1034
+ }
1035
+ if (opts.output) {
1036
+ await writeFile2(opts.output, output, "utf-8");
1037
+ console.log(chalk8.green("\u2713") + ` Exported ${memories.length} memories to ${chalk8.dim(opts.output)}`);
1038
+ } else {
1039
+ console.log(output);
1040
+ }
1041
+ } catch (error) {
1042
+ console.error(chalk8.red("\u2717") + " Failed to export:", error instanceof Error ? error.message : "Unknown error");
1043
+ process.exit(1);
1044
+ }
1045
+ });
1046
+
1047
+ // src/commands/import.ts
1048
+ import { Command as Command9 } from "commander";
1049
+ import chalk9 from "chalk";
1050
+ import { readFile as readFile2 } from "fs/promises";
1051
+ var importCommand = new Command9("import").description("Import memories from JSON or YAML file").argument("<file>", "Input file path").option("-f, --format <format>", "Input format: json, yaml (auto-detected from extension)").option("-g, --global", "Import all as global memories (override file scope)").option("--dry-run", "Show what would be imported without actually importing").action(async (file, opts) => {
1052
+ try {
1053
+ const content = await readFile2(file, "utf-8");
1054
+ let format = opts.format;
1055
+ if (!format) {
1056
+ if (file.endsWith(".yaml") || file.endsWith(".yml")) {
1057
+ format = "yaml";
1058
+ } else {
1059
+ format = "json";
1060
+ }
1061
+ }
1062
+ let data;
1063
+ if (format === "yaml") {
1064
+ const yaml = await import("yaml");
1065
+ data = yaml.parse(content);
1066
+ } else {
1067
+ data = JSON.parse(content);
1068
+ }
1069
+ if (!data || typeof data !== "object" || !("memories" in data) || !Array.isArray(data.memories)) {
1070
+ console.error(chalk9.red("\u2717") + " Invalid import file: missing 'memories' array");
1071
+ process.exit(1);
1072
+ }
1073
+ const VALID_TYPES11 = ["rule", "decision", "fact", "note"];
1074
+ const importData = data;
1075
+ let skipped = 0;
1076
+ const validMemories = [];
1077
+ for (const m of importData.memories) {
1078
+ if (!m.content || typeof m.content !== "string" || m.content.trim().length === 0) {
1079
+ skipped++;
1080
+ continue;
1081
+ }
1082
+ if (m.type && !VALID_TYPES11.includes(m.type)) {
1083
+ console.error(chalk9.yellow("\u26A0") + ` Skipping memory with invalid type "${m.type}": ${m.content.slice(0, 50)}`);
1084
+ skipped++;
1085
+ continue;
1086
+ }
1087
+ validMemories.push(m);
1088
+ }
1089
+ if (opts.dryRun) {
1090
+ console.log(chalk9.blue("Dry run - would import:"));
1091
+ for (const m of validMemories) {
1092
+ const type = m.type || "note";
1093
+ const scope = opts.global ? "global" : m.scope || "project";
1094
+ const tags = m.tags?.length ? ` [${m.tags.join(", ")}]` : "";
1095
+ console.log(` ${type} (${scope}): ${m.content}${tags}`);
1096
+ }
1097
+ console.log(chalk9.dim(`
1098
+ ${validMemories.length} memories would be imported`));
1099
+ if (skipped > 0) console.log(chalk9.dim(`${skipped} entries skipped (invalid)`));
1100
+ return;
1101
+ }
1102
+ let imported = 0;
1103
+ let failed = 0;
1104
+ for (const m of validMemories) {
1105
+ try {
1106
+ await addMemory(m.content, {
1107
+ type: m.type || "note",
1108
+ tags: m.tags,
1109
+ global: opts.global || m.scope === "global"
1110
+ });
1111
+ imported++;
1112
+ } catch (error) {
1113
+ console.error(chalk9.yellow("\u26A0") + ` Failed to import: ${m.content.slice(0, 50)}...`);
1114
+ failed++;
1115
+ }
1116
+ }
1117
+ console.log(chalk9.green("\u2713") + ` Imported ${imported} memories`);
1118
+ if (failed > 0) {
1119
+ console.log(chalk9.yellow("\u26A0") + ` ${failed} memories failed to import`);
1120
+ }
1121
+ if (skipped > 0) {
1122
+ console.log(chalk9.dim(`${skipped} entries skipped (invalid content or type)`));
1123
+ }
1124
+ } catch (error) {
1125
+ console.error(chalk9.red("\u2717") + " Failed to import:", error instanceof Error ? error.message : "Unknown error");
1126
+ process.exit(1);
1127
+ }
1128
+ });
1129
+
1130
+ // src/commands/config.ts
1131
+ import { Command as Command10 } from "commander";
1132
+
1133
+ // src/lib/config.ts
1134
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
1135
+ import { join as join2 } from "path";
1136
+ import { existsSync as existsSync2 } from "fs";
1137
+ import { parse, stringify } from "yaml";
1138
+ var AGENTS_DIR = ".agents";
1139
+ var CONFIG_FILE = "config.yaml";
1140
+ async function initConfig(dir) {
1141
+ const agentsDir = join2(dir, AGENTS_DIR);
1142
+ await mkdir2(agentsDir, { recursive: true });
1143
+ const configPath = join2(agentsDir, CONFIG_FILE);
1144
+ if (!existsSync2(configPath)) {
1145
+ const defaultConfig = {
1146
+ name: "my-project",
1147
+ description: "Agent memory configuration",
1148
+ version: "0.1.0",
1149
+ memory: {
1150
+ provider: "local",
1151
+ store: "~/.config/memories/local.db"
1152
+ }
1153
+ };
1154
+ await writeFile3(configPath, stringify(defaultConfig), "utf-8");
1155
+ }
1156
+ return configPath;
1157
+ }
1158
+ async function readConfig(dir) {
1159
+ const configPath = join2(dir, AGENTS_DIR, CONFIG_FILE);
1160
+ if (!existsSync2(configPath)) return null;
1161
+ const raw = await readFile3(configPath, "utf-8");
1162
+ return parse(raw);
1163
+ }
1164
+
1165
+ // src/commands/config.ts
1166
+ var configCommand = new Command10("config").description("Manage agent configuration");
1167
+ configCommand.command("init").description("Initialize .agents/config.yaml in current directory").action(async () => {
1168
+ const path = await initConfig(process.cwd());
1169
+ console.log(`Created config at ${path}`);
1170
+ });
1171
+ configCommand.command("show").description("Show current agent configuration").action(async () => {
1172
+ const config = await readConfig(process.cwd());
1173
+ if (!config) {
1174
+ console.log("No .agents/config.yaml found. Run `memories config init` first.");
1175
+ return;
1176
+ }
1177
+ console.log(JSON.stringify(config, null, 2));
1178
+ });
1179
+
1180
+ // src/commands/serve.ts
1181
+ import { Command as Command11 } from "commander";
1182
+
1183
+ // src/mcp/index.ts
1184
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1185
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1186
+ import { z } from "zod";
1187
+ var TYPE_LABELS2 = {
1188
+ rule: "\u{1F4CC} RULE",
1189
+ decision: "\u{1F4A1} DECISION",
1190
+ fact: "\u{1F4CB} FACT",
1191
+ note: "\u{1F4DD} NOTE"
1192
+ };
1193
+ function formatMemory4(m) {
1194
+ const tags = m.tags ? ` [${m.tags}]` : "";
1195
+ const scope = m.scope === "global" ? "G" : "P";
1196
+ const typeLabel = TYPE_LABELS2[m.type] || "\u{1F4DD} NOTE";
1197
+ return `${typeLabel} (${scope}) ${m.id}: ${m.content}${tags}`;
1198
+ }
1199
+ function formatRulesSection(rules) {
1200
+ if (rules.length === 0) return "";
1201
+ return `## Active Rules
1202
+ ${rules.map((r) => `- ${r.content}`).join("\n")}`;
1203
+ }
1204
+ function formatMemoriesSection(memories, title) {
1205
+ if (memories.length === 0) return "";
1206
+ return `## ${title}
1207
+ ${memories.map(formatMemory4).join("\n")}`;
1208
+ }
1209
+ async function startMcpServer() {
1210
+ const projectId = getProjectId();
1211
+ const server = new McpServer({
1212
+ name: "memories",
1213
+ version: "0.1.0"
1214
+ });
1215
+ server.resource(
1216
+ "rules",
1217
+ "memories://rules",
1218
+ { description: "All active rules (coding standards, preferences, constraints)", mimeType: "text/markdown" },
1219
+ async () => {
1220
+ try {
1221
+ const rules = await getRules({ projectId: projectId ?? void 0 });
1222
+ if (rules.length === 0) {
1223
+ return {
1224
+ contents: [{ uri: "memories://rules", mimeType: "text/markdown", text: "No rules defined." }]
1225
+ };
1226
+ }
1227
+ const globalRules = rules.filter((r) => r.scope === "global");
1228
+ const projectRules = rules.filter((r) => r.scope === "project");
1229
+ const parts = [];
1230
+ if (globalRules.length > 0) {
1231
+ parts.push(`## Global Rules
1232
+
1233
+ ${globalRules.map((r) => `- ${r.content}`).join("\n")}`);
1234
+ }
1235
+ if (projectRules.length > 0) {
1236
+ parts.push(`## Project Rules
1237
+
1238
+ ${projectRules.map((r) => `- ${r.content}`).join("\n")}`);
1239
+ }
1240
+ return {
1241
+ contents: [{ uri: "memories://rules", mimeType: "text/markdown", text: parts.join("\n\n") }]
1242
+ };
1243
+ } catch (error) {
1244
+ return {
1245
+ contents: [{ uri: "memories://rules", mimeType: "text/markdown", text: `Error loading rules: ${error instanceof Error ? error.message : "Unknown error"}` }]
1246
+ };
1247
+ }
1248
+ }
1249
+ );
1250
+ server.resource(
1251
+ "recent",
1252
+ "memories://recent",
1253
+ { description: "20 most recent memories across all types", mimeType: "text/markdown" },
1254
+ async () => {
1255
+ try {
1256
+ const memories = await listMemories({
1257
+ limit: 20,
1258
+ projectId: projectId ?? void 0
1259
+ });
1260
+ if (memories.length === 0) {
1261
+ return {
1262
+ contents: [{ uri: "memories://recent", mimeType: "text/markdown", text: "No memories found." }]
1263
+ };
1264
+ }
1265
+ const text = memories.map(formatMemory4).join("\n");
1266
+ return {
1267
+ contents: [{ uri: "memories://recent", mimeType: "text/markdown", text: `## Recent Memories
1268
+
1269
+ ${text}` }]
1270
+ };
1271
+ } catch (error) {
1272
+ return {
1273
+ contents: [{ uri: "memories://recent", mimeType: "text/markdown", text: `Error loading memories: ${error instanceof Error ? error.message : "Unknown error"}` }]
1274
+ };
1275
+ }
1276
+ }
1277
+ );
1278
+ server.resource(
1279
+ "project-memories",
1280
+ new ResourceTemplate("memories://project/{projectId}", { list: void 0 }),
1281
+ { description: "All memories for a specific project", mimeType: "text/markdown" },
1282
+ async (uri, variables) => {
1283
+ try {
1284
+ const pid = String(variables.projectId);
1285
+ const memories = await listMemories({
1286
+ projectId: pid,
1287
+ includeGlobal: false
1288
+ });
1289
+ if (memories.length === 0) {
1290
+ return {
1291
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: `No memories found for project ${pid}.` }]
1292
+ };
1293
+ }
1294
+ const text = memories.map(formatMemory4).join("\n");
1295
+ return {
1296
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: `## Memories for ${pid}
1297
+
1298
+ ${text}` }]
1299
+ };
1300
+ } catch (error) {
1301
+ return {
1302
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: `Error loading project memories: ${error instanceof Error ? error.message : "Unknown error"}` }]
1303
+ };
1304
+ }
1305
+ }
1306
+ );
1307
+ server.tool(
1308
+ "get_context",
1309
+ `Get relevant context for the current task. This is the PRIMARY tool for AI agents.
1310
+ Returns:
1311
+ 1. All active RULES (coding standards, preferences) - always included
1312
+ 2. Relevant memories matching your query (decisions, facts, notes)
1313
+
1314
+ Use this at the start of tasks to understand project conventions and recall past decisions.`,
1315
+ {
1316
+ query: z.string().optional().describe("What you're working on - used to find relevant memories. Leave empty to get just rules."),
1317
+ limit: z.number().optional().describe("Max memories to return (default: 10, rules always included)")
1318
+ },
1319
+ async ({ query, limit }) => {
1320
+ try {
1321
+ const { rules, memories } = await getContext(query, {
1322
+ projectId: projectId ?? void 0,
1323
+ limit
1324
+ });
1325
+ const parts = [];
1326
+ if (rules.length > 0) {
1327
+ parts.push(formatRulesSection(rules));
1328
+ }
1329
+ if (memories.length > 0) {
1330
+ parts.push(formatMemoriesSection(memories, query ? `Relevant to: "${query}"` : "Recent Memories"));
1331
+ }
1332
+ if (parts.length === 0) {
1333
+ return {
1334
+ content: [{ type: "text", text: "No context found. Use add_memory to store rules and knowledge." }]
1335
+ };
1336
+ }
1337
+ return {
1338
+ content: [{ type: "text", text: parts.join("\n\n") }]
1339
+ };
1340
+ } catch (error) {
1341
+ return {
1342
+ content: [{ type: "text", text: `Failed to get context: ${error instanceof Error ? error.message : "Unknown error"}` }],
1343
+ isError: true
1344
+ };
1345
+ }
1346
+ }
1347
+ );
1348
+ server.tool(
1349
+ "add_memory",
1350
+ `Store a new memory. Choose the appropriate type:
1351
+ - rule: Coding standards, preferences, constraints (e.g., "Always use TypeScript strict mode")
1352
+ - decision: Why we chose something (e.g., "Chose PostgreSQL for JSONB support")
1353
+ - fact: Project-specific knowledge (e.g., "API rate limit is 100 req/min")
1354
+ - note: General notes (default)
1355
+
1356
+ By default, memories are project-scoped when in a git repo. Use global: true for user-wide preferences.`,
1357
+ {
1358
+ content: z.string().describe("The memory content to store"),
1359
+ type: z.enum(["rule", "decision", "fact", "note"]).optional().describe("Memory type (default: note)"),
1360
+ tags: z.array(z.string()).optional().describe("Tags to categorize the memory"),
1361
+ global: z.boolean().optional().describe("Store as global memory instead of project-scoped")
1362
+ },
1363
+ async ({ content, type, tags, global: isGlobal }) => {
1364
+ try {
1365
+ const memory = await addMemory(content, {
1366
+ tags,
1367
+ global: isGlobal,
1368
+ type
1369
+ });
1370
+ const typeLabel = TYPE_LABELS2[memory.type];
1371
+ return {
1372
+ content: [
1373
+ {
1374
+ type: "text",
1375
+ text: `Stored ${typeLabel} ${memory.id} (${memory.scope}${memory.project_id ? `: ${memory.project_id}` : ""})`
1376
+ }
1377
+ ]
1378
+ };
1379
+ } catch (error) {
1380
+ return {
1381
+ content: [{ type: "text", text: `Failed to add memory: ${error instanceof Error ? error.message : "Unknown error"}` }],
1382
+ isError: true
1383
+ };
1384
+ }
1385
+ }
1386
+ );
1387
+ server.tool(
1388
+ "search_memories",
1389
+ "Search memories by content using full-text search. Returns both global and project-scoped memories ranked by relevance.",
1390
+ {
1391
+ query: z.string().describe("Search query - words are matched with prefix matching"),
1392
+ limit: z.number().optional().describe("Maximum number of results (default: 20)"),
1393
+ types: z.array(z.enum(["rule", "decision", "fact", "note"])).optional().describe("Filter by memory types")
1394
+ },
1395
+ async ({ query, limit, types }) => {
1396
+ try {
1397
+ const memories = await searchMemories(query, {
1398
+ limit,
1399
+ projectId: projectId ?? void 0,
1400
+ types
1401
+ });
1402
+ if (memories.length === 0) {
1403
+ return {
1404
+ content: [{ type: "text", text: "No memories found matching your query." }]
1405
+ };
1406
+ }
1407
+ const formatted = memories.map(formatMemory4).join("\n");
1408
+ return {
1409
+ content: [
1410
+ {
1411
+ type: "text",
1412
+ text: `Found ${memories.length} memories:
1413
+
1414
+ ${formatted}`
1415
+ }
1416
+ ]
1417
+ };
1418
+ } catch (error) {
1419
+ return {
1420
+ content: [{ type: "text", text: `Failed to search memories: ${error instanceof Error ? error.message : "Unknown error"}` }],
1421
+ isError: true
1422
+ };
1423
+ }
1424
+ }
1425
+ );
1426
+ server.tool(
1427
+ "get_rules",
1428
+ "Get all active rules for the current project. Rules are coding standards, preferences, and constraints that should always be followed.",
1429
+ {},
1430
+ async () => {
1431
+ try {
1432
+ const rules = await getRules({ projectId: projectId ?? void 0 });
1433
+ if (rules.length === 0) {
1434
+ return {
1435
+ content: [{ type: "text", text: "No rules defined. Add rules with: add_memory with type='rule'" }]
1436
+ };
1437
+ }
1438
+ const globalRules = rules.filter((r) => r.scope === "global");
1439
+ const projectRules = rules.filter((r) => r.scope === "project");
1440
+ const parts = [];
1441
+ if (globalRules.length > 0) {
1442
+ parts.push(`## Global Rules
1443
+ ${globalRules.map((r) => `- ${r.content}`).join("\n")}`);
1444
+ }
1445
+ if (projectRules.length > 0) {
1446
+ parts.push(`## Project Rules
1447
+ ${projectRules.map((r) => `- ${r.content}`).join("\n")}`);
1448
+ }
1449
+ return {
1450
+ content: [{ type: "text", text: parts.join("\n\n") }]
1451
+ };
1452
+ } catch (error) {
1453
+ return {
1454
+ content: [{ type: "text", text: `Failed to get rules: ${error instanceof Error ? error.message : "Unknown error"}` }],
1455
+ isError: true
1456
+ };
1457
+ }
1458
+ }
1459
+ );
1460
+ server.tool(
1461
+ "list_memories",
1462
+ "List recent memories. Returns both global and project-scoped memories.",
1463
+ {
1464
+ limit: z.number().optional().describe("Maximum number of results (default: 50)"),
1465
+ tags: z.array(z.string()).optional().describe("Filter by tags"),
1466
+ types: z.array(z.enum(["rule", "decision", "fact", "note"])).optional().describe("Filter by memory types")
1467
+ },
1468
+ async ({ limit, tags, types }) => {
1469
+ try {
1470
+ const memories = await listMemories({
1471
+ limit,
1472
+ tags,
1473
+ projectId: projectId ?? void 0,
1474
+ types
1475
+ });
1476
+ if (memories.length === 0) {
1477
+ return {
1478
+ content: [{ type: "text", text: "No memories found." }]
1479
+ };
1480
+ }
1481
+ const formatted = memories.map(formatMemory4).join("\n");
1482
+ return {
1483
+ content: [
1484
+ {
1485
+ type: "text",
1486
+ text: `${memories.length} memories:
1487
+
1488
+ ${formatted}`
1489
+ }
1490
+ ]
1491
+ };
1492
+ } catch (error) {
1493
+ return {
1494
+ content: [{ type: "text", text: `Failed to list memories: ${error instanceof Error ? error.message : "Unknown error"}` }],
1495
+ isError: true
1496
+ };
1497
+ }
1498
+ }
1499
+ );
1500
+ server.tool(
1501
+ "edit_memory",
1502
+ `Update an existing memory's content, type, or tags. Use this to refine or correct memories.
1503
+ Find the memory ID first with search_memories or list_memories.`,
1504
+ {
1505
+ id: z.string().describe("The memory ID to edit"),
1506
+ content: z.string().optional().describe("New content for the memory"),
1507
+ type: z.enum(["rule", "decision", "fact", "note"]).optional().describe("New type for the memory"),
1508
+ tags: z.array(z.string()).optional().describe("New tags (replaces existing tags)")
1509
+ },
1510
+ async ({ id, content, type, tags }) => {
1511
+ try {
1512
+ if (!content && !type && !tags) {
1513
+ return {
1514
+ content: [{ type: "text", text: "Nothing to update. Provide at least one of: content, type, tags." }],
1515
+ isError: true
1516
+ };
1517
+ }
1518
+ const updated = await updateMemory(id, {
1519
+ content,
1520
+ type,
1521
+ tags
1522
+ });
1523
+ if (updated) {
1524
+ const typeLabel = TYPE_LABELS2[updated.type];
1525
+ return {
1526
+ content: [{ type: "text", text: `Updated ${typeLabel} ${updated.id}: ${updated.content}` }]
1527
+ };
1528
+ }
1529
+ return {
1530
+ content: [{ type: "text", text: `Memory ${id} not found or already deleted.` }],
1531
+ isError: true
1532
+ };
1533
+ } catch (error) {
1534
+ return {
1535
+ content: [{ type: "text", text: `Failed to edit memory: ${error instanceof Error ? error.message : "Unknown error"}` }],
1536
+ isError: true
1537
+ };
1538
+ }
1539
+ }
1540
+ );
1541
+ server.tool(
1542
+ "forget_memory",
1543
+ "Soft-delete a memory by ID. The memory can be recovered if needed.",
1544
+ {
1545
+ id: z.string().describe("The memory ID to forget")
1546
+ },
1547
+ async ({ id }) => {
1548
+ try {
1549
+ const deleted = await forgetMemory(id);
1550
+ if (deleted) {
1551
+ return {
1552
+ content: [{ type: "text", text: `Forgot memory ${id}` }]
1553
+ };
1554
+ }
1555
+ return {
1556
+ content: [{ type: "text", text: `Memory ${id} not found or already forgotten.` }],
1557
+ isError: true
1558
+ };
1559
+ } catch (error) {
1560
+ return {
1561
+ content: [{ type: "text", text: `Failed to forget memory: ${error instanceof Error ? error.message : "Unknown error"}` }],
1562
+ isError: true
1563
+ };
1564
+ }
1565
+ }
1566
+ );
1567
+ const transport = new StdioServerTransport();
1568
+ await server.connect(transport);
1569
+ }
1570
+
1571
+ // src/commands/serve.ts
1572
+ var serveCommand = new Command11("serve").description("Start the MCP server (stdio transport)").action(async () => {
1573
+ const projectId = getProjectId();
1574
+ if (projectId) {
1575
+ console.error(`[memories] MCP server starting (project: ${projectId})`);
1576
+ } else {
1577
+ console.error("[memories] MCP server starting (global only - not in a git repo)");
1578
+ }
1579
+ await startMcpServer();
1580
+ });
1581
+
1582
+ // src/commands/sync.ts
1583
+ import { Command as Command12 } from "commander";
1584
+
1585
+ // src/lib/turso.ts
1586
+ import { nanoid as nanoid2 } from "nanoid";
1587
+ var API_BASE = "https://api.turso.tech/v1";
1588
+ function getApiToken() {
1589
+ const token = process.env.TURSO_PLATFORM_API_TOKEN;
1590
+ if (!token) {
1591
+ throw new Error(
1592
+ "TURSO_PLATFORM_API_TOKEN not set. Get one at https://turso.tech/app/settings/api-tokens"
1593
+ );
1594
+ }
1595
+ return token;
1596
+ }
1597
+ async function api(path, opts) {
1598
+ const res = await fetch(`${API_BASE}${path}`, {
1599
+ method: opts?.method ?? "GET",
1600
+ headers: {
1601
+ Authorization: `Bearer ${getApiToken()}`,
1602
+ "Content-Type": "application/json"
1603
+ },
1604
+ body: opts?.body ? JSON.stringify(opts.body) : void 0
1605
+ });
1606
+ if (!res.ok) {
1607
+ const text = await res.text();
1608
+ throw new Error(`Turso API error (${res.status}): ${text}`);
1609
+ }
1610
+ return res.json();
1611
+ }
1612
+ async function createDatabase(org) {
1613
+ const name = `memories-${nanoid2(8).toLowerCase()}`;
1614
+ const { database } = await api(
1615
+ `/organizations/${org}/databases`,
1616
+ {
1617
+ method: "POST",
1618
+ body: { name, group: "default" }
1619
+ }
1620
+ );
1621
+ return {
1622
+ name: database.Name,
1623
+ hostname: database.Hostname,
1624
+ dbId: database.DbId
1625
+ };
1626
+ }
1627
+ async function createDatabaseToken(org, dbName) {
1628
+ const { jwt } = await api(
1629
+ `/organizations/${org}/databases/${dbName}/auth/tokens`,
1630
+ { method: "POST" }
1631
+ );
1632
+ return jwt;
1633
+ }
1634
+
1635
+ // src/commands/sync.ts
1636
+ import { unlinkSync, existsSync as existsSync3 } from "fs";
1637
+ import { join as join3 } from "path";
1638
+ import { homedir as homedir2 } from "os";
1639
+ var DB_PATH = join3(homedir2(), ".config", "memories", "local.db");
1640
+ var syncCommand = new Command12("sync").description(
1641
+ "Manage remote sync"
1642
+ );
1643
+ syncCommand.command("enable").description("Provision a Turso database and enable sync").option("-o, --org <org>", "Turso organization", "webrenew").action(async (opts) => {
1644
+ const existing = await readSyncConfig();
1645
+ if (existing) {
1646
+ console.log(`Sync already enabled: ${existing.syncUrl}`);
1647
+ console.log('Run "memories sync push" to sync now.');
1648
+ return;
1649
+ }
1650
+ console.log(`Creating database in ${opts.org}...`);
1651
+ const db = await createDatabase(opts.org);
1652
+ console.log(`Created database: ${db.name} (${db.hostname})`);
1653
+ console.log("Generating auth token...");
1654
+ const token = await createDatabaseToken(opts.org, db.name);
1655
+ const syncUrl = `libsql://${db.hostname}`;
1656
+ if (existsSync3(DB_PATH)) {
1657
+ resetDb();
1658
+ unlinkSync(DB_PATH);
1659
+ }
1660
+ await saveSyncConfig({
1661
+ syncUrl,
1662
+ syncToken: token,
1663
+ org: opts.org,
1664
+ dbName: db.name
1665
+ });
1666
+ console.log("Waiting for database to be ready...");
1667
+ await new Promise((r) => setTimeout(r, 3e3));
1668
+ resetDb();
1669
+ await syncDb();
1670
+ console.log(`Sync enabled: ${syncUrl}`);
1671
+ });
1672
+ syncCommand.command("push").description("Push local changes to remote").action(async () => {
1673
+ const config = await readSyncConfig();
1674
+ if (!config) {
1675
+ console.error('Sync not enabled. Run "memories sync enable" first.');
1676
+ process.exitCode = 1;
1677
+ return;
1678
+ }
1679
+ await syncDb();
1680
+ console.log("Synced to remote.");
1681
+ });
1682
+ syncCommand.command("status").description("Show sync configuration").action(async () => {
1683
+ const config = await readSyncConfig();
1684
+ if (!config) {
1685
+ console.log("Sync not enabled.");
1686
+ return;
1687
+ }
1688
+ console.log(`Remote: ${config.syncUrl}`);
1689
+ console.log(`Org: ${config.org}`);
1690
+ console.log(`Database: ${config.dbName}`);
1691
+ });
1692
+
1693
+ // src/commands/generate.ts
1694
+ import { Command as Command13 } from "commander";
1695
+ import chalk10 from "chalk";
1696
+ import { writeFile as writeFile4, readFile as readFile4, mkdir as mkdir3 } from "fs/promises";
1697
+ import { existsSync as existsSync4, watch as fsWatch } from "fs";
1698
+ import { dirname, resolve, join as join4 } from "path";
1699
+ import { homedir as homedir3 } from "os";
1700
+ import { checkbox } from "@inquirer/prompts";
1701
+ var MARKER = "Generated by memories.sh";
1702
+ var VALID_TYPES6 = ["rule", "decision", "fact", "note"];
1703
+ function groupByType(memories) {
1704
+ const groups = {};
1705
+ for (const m of memories) {
1706
+ const title = m.type === "rule" ? "Rules" : m.type === "decision" ? "Key Decisions" : m.type === "fact" ? "Project Facts" : "Notes";
1707
+ (groups[title] ??= []).push(m);
1708
+ }
1709
+ const order = ["Rules", "Key Decisions", "Project Facts", "Notes"];
1710
+ return order.filter((t) => groups[t]?.length).map((title) => ({ title, memories: groups[title] }));
1711
+ }
1712
+ function formatMemoriesAsMarkdown(memories) {
1713
+ const sections = groupByType(memories);
1714
+ if (sections.length === 0) return "";
1715
+ return sections.map(({ title, memories: mems }) => {
1716
+ const items = mems.map((m) => `- ${m.content}`).join("\n");
1717
+ return `## ${title}
1718
+
1719
+ ${items}`;
1720
+ }).join("\n\n");
1721
+ }
1722
+ function formatCursorMdc(memories) {
1723
+ const body = formatMemoriesAsMarkdown(memories);
1724
+ const frontmatter = [
1725
+ "---",
1726
+ "description: Project memories and rules from memories.sh",
1727
+ "globs:",
1728
+ "alwaysApply: true",
1729
+ "---"
1730
+ ].join("\n");
1731
+ return `${frontmatter}
1732
+
1733
+ # Project Memories
1734
+
1735
+ ${body}`;
1736
+ }
1737
+ function formatWindsurf(memories) {
1738
+ const full = formatMemoriesAsMarkdown(memories);
1739
+ const LIMIT = 6e3;
1740
+ if (full.length <= LIMIT) return full;
1741
+ const truncated = full.slice(0, LIMIT);
1742
+ const lastNewline = truncated.lastIndexOf("\n");
1743
+ return lastNewline > 0 ? truncated.slice(0, lastNewline) + "\n\n> _Truncated to fit Windsurf 6000 char limit._" : truncated;
1744
+ }
1745
+ var TARGETS = [
1746
+ {
1747
+ name: "cursor",
1748
+ defaultPath: ".cursor/rules/memories.mdc",
1749
+ description: "Cursor rules file (.cursor/rules/memories.mdc)",
1750
+ format: formatCursorMdc
1751
+ },
1752
+ {
1753
+ name: "claude",
1754
+ defaultPath: "CLAUDE.md",
1755
+ description: "Claude Code instructions (CLAUDE.md)",
1756
+ format: (m) => `# Project Memories
1757
+
1758
+ ${formatMemoriesAsMarkdown(m)}`
1759
+ },
1760
+ {
1761
+ name: "agents",
1762
+ defaultPath: "AGENTS.md",
1763
+ description: "AGENTS.md for Amp, Codex, Goose, Kilo, Kiro, OpenCode",
1764
+ format: (m) => `# Project Memories
1765
+
1766
+ ${formatMemoriesAsMarkdown(m)}`
1767
+ },
1768
+ {
1769
+ name: "copilot",
1770
+ defaultPath: ".github/copilot-instructions.md",
1771
+ description: "GitHub Copilot instructions (.github/copilot-instructions.md)",
1772
+ format: (m) => `# Project Memories
1773
+
1774
+ ${formatMemoriesAsMarkdown(m)}`
1775
+ },
1776
+ {
1777
+ name: "windsurf",
1778
+ defaultPath: ".windsurf/rules/memories.md",
1779
+ description: "Windsurf rules (.windsurf/rules/memories.md)",
1780
+ format: formatWindsurf
1781
+ },
1782
+ {
1783
+ name: "cline",
1784
+ defaultPath: ".clinerules/memories.md",
1785
+ description: "Cline rules (.clinerules/memories.md)",
1786
+ format: (m) => `# Project Memories
1787
+
1788
+ ${formatMemoriesAsMarkdown(m)}`
1789
+ },
1790
+ {
1791
+ name: "roo",
1792
+ defaultPath: ".roo/rules/memories.md",
1793
+ description: "Roo rules (.roo/rules/memories.md)",
1794
+ format: (m) => `# Project Memories
1795
+
1796
+ ${formatMemoriesAsMarkdown(m)}`
1797
+ },
1798
+ {
1799
+ name: "gemini",
1800
+ defaultPath: "GEMINI.md",
1801
+ description: "Gemini instructions (GEMINI.md)",
1802
+ format: (m) => `# Project Memories
1803
+
1804
+ ${formatMemoriesAsMarkdown(m)}`
1805
+ }
1806
+ ];
1807
+ function makeFooter() {
1808
+ return `
1809
+
1810
+ <!-- ${MARKER} at ${(/* @__PURE__ */ new Date()).toISOString()} -->`;
1811
+ }
1812
+ async function hasOurMarker(filePath) {
1813
+ try {
1814
+ const content = await readFile4(filePath, "utf-8");
1815
+ return content.includes(MARKER);
1816
+ } catch {
1817
+ return false;
1818
+ }
1819
+ }
1820
+ var TRACK_BY_DEFAULT = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".github/copilot-instructions.md"]);
1821
+ async function checkGitignore(filePath) {
1822
+ if (TRACK_BY_DEFAULT.has(filePath)) return;
1823
+ const gitignorePath = resolve(".gitignore");
1824
+ try {
1825
+ const content = existsSync4(gitignorePath) ? await readFile4(gitignorePath, "utf-8") : "";
1826
+ const lines = content.split("\n");
1827
+ const parentDir = filePath.split("/")[0];
1828
+ if (lines.some((l) => l.trim() === filePath || l.trim() === parentDir || l.trim() === `${parentDir}/`)) {
1829
+ return;
1830
+ }
1831
+ console.log(chalk10.dim(` hint: add "${filePath}" to .gitignore if you don't want it tracked`));
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ async function writeTarget(target, memories, opts) {
1836
+ const outPath = resolve(opts.output ?? target.defaultPath);
1837
+ const content = target.format(memories) + makeFooter();
1838
+ if (opts.dryRun) {
1839
+ console.log(chalk10.dim(`\u2500\u2500 ${target.name} \u2192 ${outPath} \u2500\u2500`));
1840
+ console.log(content);
1841
+ console.log();
1842
+ return;
1843
+ }
1844
+ if (existsSync4(outPath)) {
1845
+ const ours = await hasOurMarker(outPath);
1846
+ if (!ours && !opts.force) {
1847
+ console.error(
1848
+ chalk10.yellow("\u26A0") + ` ${outPath} exists and was not generated by memories.sh. Use ${chalk10.bold("--force")} to overwrite.`
1849
+ );
1850
+ return;
1851
+ }
1852
+ }
1853
+ await mkdir3(dirname(outPath), { recursive: true });
1854
+ await writeFile4(outPath, content, "utf-8");
1855
+ console.log(chalk10.green("\u2713") + ` Wrote ${target.name} \u2192 ${chalk10.dim(outPath)}`);
1856
+ await checkGitignore(opts.output ?? target.defaultPath);
1857
+ }
1858
+ async function fetchMemories(types) {
1859
+ const projectId = getProjectId() ?? void 0;
1860
+ return listMemories({
1861
+ limit: 1e4,
1862
+ types,
1863
+ projectId
1864
+ });
1865
+ }
1866
+ function parseTypes(raw) {
1867
+ if (!raw) return ["rule", "decision", "fact"];
1868
+ const types = raw.split(",").map((s) => s.trim());
1869
+ for (const t of types) {
1870
+ if (!VALID_TYPES6.includes(t)) {
1871
+ console.error(chalk10.red("\u2717") + ` Invalid type "${t}". Valid: ${VALID_TYPES6.join(", ")}`);
1872
+ process.exit(1);
1873
+ }
1874
+ }
1875
+ return types;
1876
+ }
1877
+ function getDbPath2() {
1878
+ const dataDir = process.env.MEMORIES_DATA_DIR ?? join4(homedir3(), ".config", "memories");
1879
+ return join4(dataDir, "local.db");
1880
+ }
1881
+ async function runWatch(targets, memories, opts) {
1882
+ const dbPath = getDbPath2();
1883
+ if (!existsSync4(dbPath)) {
1884
+ console.error(chalk10.red("\u2717") + " Database not found. Run: memories init");
1885
+ process.exit(1);
1886
+ }
1887
+ console.log(chalk10.dim(`Watching ${dbPath} for changes... (Ctrl+C to stop)
1888
+ `));
1889
+ const mems = await memories();
1890
+ if (mems.length > 0) {
1891
+ for (const target of targets) {
1892
+ await writeTarget(target, mems, { force: opts.force ?? true });
1893
+ }
1894
+ }
1895
+ let timeout = null;
1896
+ fsWatch(dbPath, () => {
1897
+ if (timeout) clearTimeout(timeout);
1898
+ timeout = setTimeout(async () => {
1899
+ try {
1900
+ const mems2 = await memories();
1901
+ if (mems2.length > 0) {
1902
+ for (const target of targets) {
1903
+ await writeTarget(target, mems2, { force: true });
1904
+ }
1905
+ }
1906
+ } catch (err) {
1907
+ console.error(chalk10.red("\u2717") + " Watch error:", err.message);
1908
+ }
1909
+ }, 500);
1910
+ });
1911
+ await new Promise(() => {
1912
+ });
1913
+ }
1914
+ var generateCommand = new Command13("generate").description("Generate IDE rule/instruction files from memories").option("--types <types>", "Comma-separated types to include (default: rule,decision,fact)").option("--dry-run", "Preview without writing").option("--force", "Overwrite files not generated by memories.sh").option("-w, --watch", "Watch for memory changes and auto-regenerate").action(async (opts) => {
1915
+ try {
1916
+ if (!process.stdin.isTTY) {
1917
+ generateCommand.outputHelp();
1918
+ return;
1919
+ }
1920
+ const selected = await checkbox({
1921
+ message: "Select targets to generate",
1922
+ choices: TARGETS.map((t) => ({
1923
+ name: `${t.name} ${chalk10.dim(`\u2192 ${t.defaultPath}`)}`,
1924
+ value: t.name,
1925
+ checked: true
1926
+ }))
1927
+ });
1928
+ if (selected.length === 0) {
1929
+ console.log(chalk10.dim("No targets selected."));
1930
+ return;
1931
+ }
1932
+ const types = parseTypes(opts.types);
1933
+ const memories = await fetchMemories(types);
1934
+ if (memories.length === 0) {
1935
+ console.error(chalk10.dim('No memories found. Add some with: memories add --rule "Your rule"'));
1936
+ return;
1937
+ }
1938
+ const selectedSet = new Set(selected);
1939
+ for (const target of TARGETS.filter((t) => selectedSet.has(t.name))) {
1940
+ await writeTarget(target, memories, opts);
1941
+ }
1942
+ if (opts.watch) {
1943
+ await runWatch(TARGETS, () => fetchMemories(types), opts);
1944
+ }
1945
+ } catch (error) {
1946
+ if (error.name === "ExitPromptError") return;
1947
+ console.error(chalk10.red("\u2717") + " Failed to generate:", error instanceof Error ? error.message : "Unknown error");
1948
+ process.exit(1);
1949
+ }
1950
+ });
1951
+ for (const target of TARGETS) {
1952
+ generateCommand.addCommand(
1953
+ new Command13(target.name).description(target.description).option("-o, --output <path>", "Override output path").option("--types <types>", "Comma-separated types to include (default: rule,decision,fact)").option("--dry-run", "Preview without writing").option("--force", "Overwrite files not generated by memories.sh").action(async (opts) => {
1954
+ try {
1955
+ const types = parseTypes(opts.types);
1956
+ const memories = await fetchMemories(types);
1957
+ if (memories.length === 0) {
1958
+ console.error(chalk10.dim('No memories found. Add some with: memories add --rule "Your rule"'));
1959
+ return;
1960
+ }
1961
+ await writeTarget(target, memories, opts);
1962
+ } catch (error) {
1963
+ console.error(chalk10.red("\u2717") + ` Failed to generate ${target.name}:`, error instanceof Error ? error.message : "Unknown error");
1964
+ process.exit(1);
1965
+ }
1966
+ })
1967
+ );
1968
+ }
1969
+ generateCommand.addCommand(
1970
+ new Command13("all").description("Generate rule files for all supported targets").option("--types <types>", "Comma-separated types to include (default: rule,decision,fact)").option("--dry-run", "Preview without writing").option("--force", "Overwrite files not generated by memories.sh").option("-w, --watch", "Watch for memory changes and auto-regenerate").action(async (opts) => {
1971
+ try {
1972
+ const types = parseTypes(opts.types);
1973
+ if (opts.watch) {
1974
+ await runWatch(TARGETS, () => fetchMemories(types), opts);
1975
+ return;
1976
+ }
1977
+ const memories = await fetchMemories(types);
1978
+ if (memories.length === 0) {
1979
+ console.error(chalk10.dim('No memories found. Add some with: memories add --rule "Your rule"'));
1980
+ return;
1981
+ }
1982
+ for (const target of TARGETS) {
1983
+ await writeTarget(target, memories, opts);
1984
+ }
1985
+ } catch (error) {
1986
+ console.error(chalk10.red("\u2717") + " Failed to generate:", error instanceof Error ? error.message : "Unknown error");
1987
+ process.exit(1);
1988
+ }
1989
+ })
1990
+ );
1991
+
1992
+ // src/commands/edit.ts
1993
+ import { Command as Command14 } from "commander";
1994
+ import chalk11 from "chalk";
1995
+ import { execFileSync as execFileSync2 } from "child_process";
1996
+ import { writeFileSync, readFileSync, unlinkSync as unlinkSync2 } from "fs";
1997
+ import { tmpdir } from "os";
1998
+ import { join as join5 } from "path";
1999
+ import { nanoid as nanoid3 } from "nanoid";
2000
+ import { select } from "@inquirer/prompts";
2001
+ var VALID_TYPES7 = ["rule", "decision", "fact", "note"];
2002
+ function truncate2(str, max) {
2003
+ if (str.length <= max) return str;
2004
+ return str.slice(0, max - 1) + "\u2026";
2005
+ }
2006
+ async function pickMemory() {
2007
+ const projectId = getProjectId() ?? void 0;
2008
+ const memories = await listMemories({ limit: 100, projectId });
2009
+ if (memories.length === 0) {
2010
+ console.error(chalk11.dim("No memories found."));
2011
+ process.exit(0);
2012
+ }
2013
+ const id = await select({
2014
+ message: "Select a memory to edit",
2015
+ choices: memories.map((m) => ({
2016
+ name: `${chalk11.dim(m.type.padEnd(9))} ${truncate2(m.content, 60)} ${chalk11.dim(m.id)}`,
2017
+ value: m.id
2018
+ }))
2019
+ });
2020
+ return id;
2021
+ }
2022
+ var editCommand = new Command14("edit").description("Edit an existing memory").argument("[id]", "Memory ID to edit (interactive picker if omitted)").option("-c, --content <content>", "New content (skips editor)").option("-t, --tags <tags>", "New comma-separated tags").option("--type <type>", "New memory type: rule, decision, fact, note").action(async (id, opts) => {
2023
+ try {
2024
+ if (!id) {
2025
+ if (!process.stdin.isTTY) {
2026
+ console.error(chalk11.red("\u2717") + " Memory ID required in non-interactive mode");
2027
+ process.exit(1);
2028
+ }
2029
+ id = await pickMemory();
2030
+ }
2031
+ const db = await getDb();
2032
+ const result = await db.execute({
2033
+ sql: `SELECT * FROM memories WHERE id = ? AND deleted_at IS NULL`,
2034
+ args: [id]
2035
+ });
2036
+ if (result.rows.length === 0) {
2037
+ console.error(chalk11.red("\u2717") + ` Memory ${chalk11.dim(id)} not found`);
2038
+ process.exit(1);
2039
+ }
2040
+ const memory = result.rows[0];
2041
+ if (opts.type && !VALID_TYPES7.includes(opts.type)) {
2042
+ console.error(chalk11.red("\u2717") + ` Invalid type "${opts.type}". Valid: ${VALID_TYPES7.join(", ")}`);
2043
+ process.exit(1);
2044
+ }
2045
+ let newContent = opts.content;
2046
+ if (newContent === void 0 && opts.tags === void 0 && opts.type === void 0) {
2047
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
2048
+ const tmpFile = join5(tmpdir(), `memories-edit-${nanoid3(6)}.md`);
2049
+ writeFileSync(tmpFile, memory.content, "utf-8");
2050
+ try {
2051
+ execFileSync2(editor, [tmpFile], { stdio: "inherit" });
2052
+ newContent = readFileSync(tmpFile, "utf-8").trimEnd();
2053
+ } finally {
2054
+ try {
2055
+ unlinkSync2(tmpFile);
2056
+ } catch {
2057
+ }
2058
+ }
2059
+ if (newContent === memory.content) {
2060
+ console.log(chalk11.dim("No changes made."));
2061
+ return;
2062
+ }
2063
+ }
2064
+ const updates = {};
2065
+ if (newContent !== void 0) updates.content = newContent;
2066
+ if (opts.tags !== void 0) updates.tags = opts.tags.split(",").map((s) => s.trim()).filter(Boolean);
2067
+ if (opts.type !== void 0) updates.type = opts.type;
2068
+ const updated = await updateMemory(id, updates);
2069
+ if (!updated) {
2070
+ console.error(chalk11.red("\u2717") + ` Failed to update memory ${chalk11.dim(id)}`);
2071
+ process.exit(1);
2072
+ }
2073
+ const changes = [];
2074
+ if (updates.content !== void 0) changes.push("content");
2075
+ if (updates.tags !== void 0) changes.push("tags");
2076
+ if (updates.type !== void 0) changes.push(`type\u2192${updates.type}`);
2077
+ console.log(chalk11.green("\u2713") + ` Updated ${chalk11.dim(id)} (${changes.join(", ")})`);
2078
+ } catch (error) {
2079
+ if (error.name === "ExitPromptError") return;
2080
+ console.error(chalk11.red("\u2717") + " Failed to edit memory:", error instanceof Error ? error.message : "Unknown error");
2081
+ process.exit(1);
2082
+ }
2083
+ });
2084
+
2085
+ // src/commands/stats.ts
2086
+ import { Command as Command15 } from "commander";
2087
+ import chalk12 from "chalk";
2088
+ var statsCommand = new Command15("stats").description("Show memory statistics").option("--json", "Output as JSON").action(async (opts) => {
2089
+ try {
2090
+ const db = await getDb();
2091
+ const projectId = getProjectId();
2092
+ const result = await db.execute({
2093
+ sql: `SELECT type, scope, COUNT(*) as count FROM memories WHERE deleted_at IS NULL GROUP BY type, scope ORDER BY type, scope`,
2094
+ args: []
2095
+ });
2096
+ const totalResult = await db.execute({
2097
+ sql: `SELECT COUNT(*) as total FROM memories WHERE deleted_at IS NULL`,
2098
+ args: []
2099
+ });
2100
+ const total = Number(totalResult.rows[0]?.total ?? 0);
2101
+ const deletedResult = await db.execute({
2102
+ sql: `SELECT COUNT(*) as deleted FROM memories WHERE deleted_at IS NOT NULL`,
2103
+ args: []
2104
+ });
2105
+ const deleted = Number(deletedResult.rows[0]?.deleted ?? 0);
2106
+ const projectCount = projectId ? Number(
2107
+ (await db.execute({
2108
+ sql: `SELECT COUNT(*) as count FROM memories WHERE deleted_at IS NULL AND scope = 'project' AND project_id = ?`,
2109
+ args: [projectId]
2110
+ })).rows[0]?.count ?? 0
2111
+ ) : null;
2112
+ const rows = result.rows;
2113
+ if (opts.json) {
2114
+ const data = {
2115
+ total,
2116
+ deleted,
2117
+ project_id: projectId,
2118
+ project_count: projectCount,
2119
+ breakdown: rows.map((r) => ({ type: r.type, scope: r.scope, count: Number(r.count) }))
2120
+ };
2121
+ console.log(JSON.stringify(data, null, 2));
2122
+ return;
2123
+ }
2124
+ console.log(chalk12.bold("Memory Statistics\n"));
2125
+ if (projectId) {
2126
+ console.log(` Project: ${chalk12.dim(projectId)}`);
2127
+ }
2128
+ console.log(` Total: ${chalk12.bold(String(total))} active, ${chalk12.dim(String(deleted))} deleted
2129
+ `);
2130
+ if (rows.length === 0) {
2131
+ console.log(chalk12.dim(' No memories yet. Add one with: memories add "Your memory"'));
2132
+ return;
2133
+ }
2134
+ const typeWidths = { rule: 8, decision: 8, fact: 8, note: 8 };
2135
+ console.log(
2136
+ ` ${chalk12.dim("Type".padEnd(12))}${chalk12.dim("Scope".padEnd(10))}${chalk12.dim("Count")}`
2137
+ );
2138
+ console.log(chalk12.dim(" " + "\u2500".repeat(30)));
2139
+ for (const row of rows) {
2140
+ const type = row.type.padEnd(12);
2141
+ const scope = row.scope.padEnd(10);
2142
+ console.log(` ${type}${scope}${Number(row.count)}`);
2143
+ }
2144
+ } catch (error) {
2145
+ console.error(chalk12.red("\u2717") + " Failed to get stats:", error instanceof Error ? error.message : "Unknown error");
2146
+ process.exit(1);
2147
+ }
2148
+ });
2149
+
2150
+ // src/commands/doctor.ts
2151
+ import { Command as Command16 } from "commander";
2152
+ import chalk13 from "chalk";
2153
+ import { existsSync as existsSync5 } from "fs";
2154
+ import { join as join6 } from "path";
2155
+ var doctorCommand = new Command16("doctor").description("Check memories health and diagnose issues").option("--fix", "Attempt to fix issues found").action(async (opts) => {
2156
+ try {
2157
+ console.log(chalk13.bold("memories doctor\n"));
2158
+ const checks = [
2159
+ {
2160
+ name: "Database file",
2161
+ run: async () => {
2162
+ const dbPath = join6(getConfigDir(), "local.db");
2163
+ if (existsSync5(dbPath)) {
2164
+ return { ok: true, message: `Found at ${dbPath}` };
2165
+ }
2166
+ return { ok: false, message: `Not found at ${dbPath}. Run: memories init` };
2167
+ }
2168
+ },
2169
+ {
2170
+ name: "Database connection",
2171
+ run: async () => {
2172
+ try {
2173
+ const db = await getDb();
2174
+ await db.execute("SELECT 1");
2175
+ return { ok: true, message: "Connected successfully" };
2176
+ } catch (e) {
2177
+ return { ok: false, message: `Connection failed: ${e.message}` };
2178
+ }
2179
+ }
2180
+ },
2181
+ {
2182
+ name: "Schema integrity",
2183
+ run: async () => {
2184
+ const db = await getDb();
2185
+ const result = await db.execute("PRAGMA integrity_check");
2186
+ const status = String(result.rows[0]?.integrity_check ?? "unknown");
2187
+ return status === "ok" ? { ok: true, message: "PRAGMA integrity_check: ok" } : { ok: false, message: `Integrity check failed: ${status}` };
2188
+ }
2189
+ },
2190
+ {
2191
+ name: "FTS index",
2192
+ run: async () => {
2193
+ const db = await getDb();
2194
+ try {
2195
+ await db.execute(
2196
+ "SELECT rowid FROM memories_fts LIMIT 1"
2197
+ );
2198
+ const leaked = await db.execute(`
2199
+ SELECT COUNT(*) as count FROM memories m
2200
+ JOIN memories_fts fts ON m.rowid = fts.rowid
2201
+ WHERE memories_fts MATCH '"*"' AND m.deleted_at IS NOT NULL
2202
+ `);
2203
+ const leakedCount = Number(leaked.rows[0]?.count ?? 0);
2204
+ if (leakedCount > 0) {
2205
+ return {
2206
+ ok: false,
2207
+ message: `${leakedCount} soft-deleted records still searchable via FTS`
2208
+ };
2209
+ }
2210
+ const active = await db.execute(
2211
+ "SELECT COUNT(*) as count FROM memories WHERE deleted_at IS NULL"
2212
+ );
2213
+ return { ok: true, message: `FTS operational, ${Number(active.rows[0]?.count ?? 0)} active memories indexed` };
2214
+ } catch {
2215
+ return { ok: false, message: "FTS table missing or corrupted" };
2216
+ }
2217
+ }
2218
+ },
2219
+ {
2220
+ name: "Git project detection",
2221
+ run: async () => {
2222
+ const projectId = getProjectId();
2223
+ if (projectId) {
2224
+ return { ok: true, message: `Detected: ${projectId}` };
2225
+ }
2226
+ return { ok: true, message: "Not in a git repo (global-only mode)" };
2227
+ }
2228
+ },
2229
+ {
2230
+ name: "Orphaned project memories",
2231
+ run: async () => {
2232
+ const db = await getDb();
2233
+ const result = await db.execute(
2234
+ "SELECT DISTINCT project_id FROM memories WHERE scope = 'project' AND deleted_at IS NULL AND project_id IS NOT NULL"
2235
+ );
2236
+ const projectIds = result.rows.map((r) => String(r.project_id));
2237
+ if (projectIds.length === 0) {
2238
+ return { ok: true, message: "No project memories found" };
2239
+ }
2240
+ return { ok: true, message: `${projectIds.length} project(s) with memories: ${projectIds.join(", ")}` };
2241
+ }
2242
+ },
2243
+ {
2244
+ name: "Soft-deleted records",
2245
+ run: async () => {
2246
+ const db = await getDb();
2247
+ const result = await db.execute(
2248
+ "SELECT COUNT(*) as count FROM memories WHERE deleted_at IS NOT NULL"
2249
+ );
2250
+ const count = Number(result.rows[0]?.count ?? 0);
2251
+ if (count === 0) {
2252
+ return { ok: true, message: "No soft-deleted records" };
2253
+ }
2254
+ return { ok: true, message: `${count} soft-deleted records (run 'memories doctor --fix' to purge)` };
2255
+ }
2256
+ }
2257
+ ];
2258
+ let hasIssues = false;
2259
+ for (const check of checks) {
2260
+ const { ok, message } = await check.run();
2261
+ const icon = ok ? chalk13.green("\u2713") : chalk13.red("\u2717");
2262
+ console.log(` ${icon} ${chalk13.bold(check.name)}: ${message}`);
2263
+ if (!ok) hasIssues = true;
2264
+ }
2265
+ if (opts.fix) {
2266
+ console.log(chalk13.bold("\nRunning fixes...\n"));
2267
+ const db = await getDb();
2268
+ const purged = await db.execute(
2269
+ "DELETE FROM memories WHERE deleted_at IS NOT NULL"
2270
+ );
2271
+ console.log(` ${chalk13.green("\u2713")} Purged ${purged.rowsAffected} soft-deleted records`);
2272
+ try {
2273
+ await db.execute("INSERT INTO memories_fts(memories_fts) VALUES('rebuild')");
2274
+ console.log(` ${chalk13.green("\u2713")} Rebuilt FTS index`);
2275
+ } catch {
2276
+ console.log(` ${chalk13.yellow("\u26A0")} Could not rebuild FTS index`);
2277
+ }
2278
+ }
2279
+ console.log();
2280
+ if (hasIssues) {
2281
+ console.log(chalk13.yellow("Some issues detected.") + (opts.fix ? "" : " Run with --fix to attempt repairs."));
2282
+ } else {
2283
+ console.log(chalk13.green("All checks passed."));
2284
+ }
2285
+ } catch (error) {
2286
+ console.error(chalk13.red("\u2717") + " Doctor failed:", error instanceof Error ? error.message : "Unknown error");
2287
+ process.exit(1);
2288
+ }
2289
+ });
2290
+
2291
+ // src/commands/hook.ts
2292
+ import { Command as Command17 } from "commander";
2293
+ import chalk14 from "chalk";
2294
+ import { readFile as readFile5, writeFile as writeFile5, chmod } from "fs/promises";
2295
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
2296
+ import { execFileSync as execFileSync3 } from "child_process";
2297
+ import { join as join7 } from "path";
2298
+ var HOOK_MARKER_START = "# >>> memories.sh hook >>>";
2299
+ var HOOK_MARKER_END = "# <<< memories.sh hook <<<";
2300
+ var HOOK_SNIPPET = `
2301
+ ${HOOK_MARKER_START}
2302
+ # Auto-generate IDE rule files from memories
2303
+ if command -v memories &> /dev/null; then
2304
+ memories generate all --force 2>/dev/null || true
2305
+ git add -A -- .cursor/rules/memories.mdc CLAUDE.md AGENTS.md .github/copilot-instructions.md .windsurf/rules/memories.md .clinerules/memories.md .roo/rules/memories.md GEMINI.md 2>/dev/null || true
2306
+ fi
2307
+ ${HOOK_MARKER_END}`;
2308
+ function getGitDir() {
2309
+ try {
2310
+ return execFileSync3("git", ["rev-parse", "--git-dir"], { encoding: "utf-8" }).trim();
2311
+ } catch {
2312
+ return null;
2313
+ }
2314
+ }
2315
+ function getHookLocation(hookName) {
2316
+ const gitDir = getGitDir();
2317
+ if (!gitDir) return null;
2318
+ const huskyPath = join7(".husky", hookName);
2319
+ if (existsSync6(".husky") && !existsSync6(join7(".husky", "_"))) {
2320
+ return { path: huskyPath, type: "husky" };
2321
+ }
2322
+ const huskyLegacyPath = join7(".husky", "_", hookName);
2323
+ if (existsSync6(join7(".husky", "_"))) {
2324
+ return { path: huskyLegacyPath, type: "husky" };
2325
+ }
2326
+ if (existsSync6(huskyPath)) {
2327
+ return { path: huskyPath, type: "husky" };
2328
+ }
2329
+ return { path: join7(gitDir, "hooks", hookName), type: "git" };
2330
+ }
2331
+ function detectLintStaged() {
2332
+ try {
2333
+ if (!existsSync6("package.json")) return false;
2334
+ const pkg = JSON.parse(readFileSync2("package.json", "utf-8"));
2335
+ return !!(pkg["lint-staged"] || pkg.devDependencies?.["lint-staged"] || pkg.dependencies?.["lint-staged"]);
2336
+ } catch {
2337
+ return false;
2338
+ }
2339
+ }
2340
+ var hookCommand = new Command17("hook").description("Manage git hooks for auto-generating rule files");
2341
+ hookCommand.addCommand(
2342
+ new Command17("install").description("Install pre-commit hook to auto-generate rule files").option("--hook <name>", "Hook name (default: pre-commit)", "pre-commit").action(async (opts) => {
2343
+ try {
2344
+ const location = getHookLocation(opts.hook);
2345
+ if (!location) {
2346
+ console.error(chalk14.red("\u2717") + " Not in a git repository");
2347
+ process.exit(1);
2348
+ }
2349
+ const hookPath = location.path;
2350
+ if (existsSync6(hookPath)) {
2351
+ const content = await readFile5(hookPath, "utf-8");
2352
+ if (content.includes(HOOK_MARKER_START)) {
2353
+ console.log(chalk14.dim("Hook already installed. Use 'memories hook uninstall' first to reinstall."));
2354
+ return;
2355
+ }
2356
+ await writeFile5(hookPath, content.trimEnd() + "\n" + HOOK_SNIPPET + "\n", "utf-8");
2357
+ } else {
2358
+ await writeFile5(hookPath, "#!/bin/sh\n" + HOOK_SNIPPET + "\n", "utf-8");
2359
+ }
2360
+ await chmod(hookPath, 493);
2361
+ const locationLabel = location.type === "husky" ? "Husky" : ".git/hooks";
2362
+ console.log(chalk14.green("\u2713") + ` Installed memories hook in ${chalk14.dim(opts.hook)} (${locationLabel})`);
2363
+ console.log(chalk14.dim(" Rule files will auto-generate on each commit."));
2364
+ if (detectLintStaged()) {
2365
+ console.log(
2366
+ chalk14.dim("\n lint-staged detected. You can also add to your lint-staged config:") + chalk14.dim('\n "*.md": "memories generate all --force"')
2367
+ );
2368
+ }
2369
+ } catch (error) {
2370
+ console.error(chalk14.red("\u2717") + " Failed to install hook:", error instanceof Error ? error.message : "Unknown error");
2371
+ process.exit(1);
2372
+ }
2373
+ })
2374
+ );
2375
+ hookCommand.addCommand(
2376
+ new Command17("uninstall").description("Remove the memories pre-commit hook").option("--hook <name>", "Hook name (default: pre-commit)", "pre-commit").action(async (opts) => {
2377
+ try {
2378
+ const location = getHookLocation(opts.hook);
2379
+ if (!location) {
2380
+ console.error(chalk14.red("\u2717") + " Not in a git repository");
2381
+ process.exit(1);
2382
+ }
2383
+ const hookPath = location.path;
2384
+ if (!existsSync6(hookPath)) {
2385
+ console.log(chalk14.dim("No hook file found."));
2386
+ return;
2387
+ }
2388
+ const content = await readFile5(hookPath, "utf-8");
2389
+ if (!content.includes(HOOK_MARKER_START)) {
2390
+ console.log(chalk14.dim("No memories hook found in " + opts.hook));
2391
+ return;
2392
+ }
2393
+ const regex = new RegExp(
2394
+ `\\n?${escapeRegex(HOOK_MARKER_START)}[\\s\\S]*?${escapeRegex(HOOK_MARKER_END)}\\n?`
2395
+ );
2396
+ const cleaned = content.replace(regex, "\n");
2397
+ if (cleaned.trim() === "#!/bin/sh" || cleaned.trim() === "") {
2398
+ const { unlink } = await import("fs/promises");
2399
+ await unlink(hookPath);
2400
+ console.log(chalk14.green("\u2713") + ` Removed ${chalk14.dim(opts.hook)} hook (was memories-only)`);
2401
+ } else {
2402
+ await writeFile5(hookPath, cleaned, "utf-8");
2403
+ console.log(chalk14.green("\u2713") + ` Removed memories section from ${chalk14.dim(opts.hook)}`);
2404
+ }
2405
+ } catch (error) {
2406
+ console.error(chalk14.red("\u2717") + " Failed to uninstall hook:", error instanceof Error ? error.message : "Unknown error");
2407
+ process.exit(1);
2408
+ }
2409
+ })
2410
+ );
2411
+ hookCommand.addCommand(
2412
+ new Command17("status").description("Check if the memories hook is installed").option("--hook <name>", "Hook name (default: pre-commit)", "pre-commit").action(async (opts) => {
2413
+ try {
2414
+ const hookPath = getHookLocation(opts.hook)?.path;
2415
+ if (!hookPath) {
2416
+ console.error(chalk14.red("\u2717") + " Not in a git repository");
2417
+ process.exit(1);
2418
+ }
2419
+ if (!existsSync6(hookPath)) {
2420
+ console.log(chalk14.dim("Not installed") + ` \u2014 no ${opts.hook} hook found`);
2421
+ return;
2422
+ }
2423
+ const content = await readFile5(hookPath, "utf-8");
2424
+ if (content.includes(HOOK_MARKER_START)) {
2425
+ console.log(chalk14.green("\u2713") + ` Installed in ${chalk14.dim(hookPath)}`);
2426
+ } else {
2427
+ console.log(chalk14.dim("Not installed") + ` \u2014 ${opts.hook} exists but has no memories section`);
2428
+ }
2429
+ } catch (error) {
2430
+ console.error(chalk14.red("\u2717") + " Failed to check hook:", error instanceof Error ? error.message : "Unknown error");
2431
+ process.exit(1);
2432
+ }
2433
+ })
2434
+ );
2435
+ function escapeRegex(str) {
2436
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2437
+ }
2438
+
2439
+ // src/commands/ingest.ts
2440
+ import { Command as Command18 } from "commander";
2441
+ import chalk15 from "chalk";
2442
+ import { readFile as readFile6 } from "fs/promises";
2443
+ import { existsSync as existsSync7 } from "fs";
2444
+ var SOURCES = [
2445
+ { name: "cursor", paths: [".cursor/rules/memories.mdc", ".cursorrules"], description: "Cursor rules" },
2446
+ { name: "claude", paths: ["CLAUDE.md"], description: "Claude Code instructions" },
2447
+ { name: "agents", paths: ["AGENTS.md"], description: "AGENTS.md" },
2448
+ { name: "copilot", paths: [".github/copilot-instructions.md"], description: "GitHub Copilot instructions" },
2449
+ { name: "windsurf", paths: [".windsurf/rules/memories.md", ".windsurfrules"], description: "Windsurf rules" },
2450
+ { name: "cline", paths: [".clinerules/memories.md", ".clinerules"], description: "Cline rules" },
2451
+ { name: "roo", paths: [".roo/rules/memories.md"], description: "Roo rules" },
2452
+ { name: "gemini", paths: ["GEMINI.md"], description: "Gemini instructions" }
2453
+ ];
2454
+ var MARKER2 = "Generated by memories.sh";
2455
+ var VALID_TYPES8 = ["rule", "decision", "fact", "note"];
2456
+ function inferType(line) {
2457
+ const lower = line.toLowerCase();
2458
+ if (lower.includes("always") || lower.includes("never") || lower.includes("must") || lower.includes("prefer")) {
2459
+ return "rule";
2460
+ }
2461
+ if (lower.includes("chose") || lower.includes("decided") || lower.includes("because") || lower.includes("instead of")) {
2462
+ return "decision";
2463
+ }
2464
+ if (lower.match(/\b(is|are|has|uses|runs on|version|limit)\b/)) {
2465
+ return "fact";
2466
+ }
2467
+ return "rule";
2468
+ }
2469
+ function extractMemories(content) {
2470
+ const memories = [];
2471
+ const stripped = content.replace(/^---[\s\S]*?---\n*/m, "");
2472
+ const clean = stripped.replace(/<!--.*?-->/g, "").trim();
2473
+ for (const line of clean.split("\n")) {
2474
+ const trimmed = line.trim();
2475
+ const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
2476
+ if (bulletMatch) {
2477
+ const text = bulletMatch[1].trim();
2478
+ if (text.length > 10 && text.length < 500) {
2479
+ memories.push({ content: text, type: inferType(text) });
2480
+ }
2481
+ continue;
2482
+ }
2483
+ const numberedMatch = trimmed.match(/^\d+[.)]\s+(.+)$/);
2484
+ if (numberedMatch) {
2485
+ const text = numberedMatch[1].trim();
2486
+ if (text.length > 10 && text.length < 500) {
2487
+ memories.push({ content: text, type: inferType(text) });
2488
+ }
2489
+ continue;
2490
+ }
2491
+ if (trimmed.length > 20 && trimmed.length < 500 && !trimmed.startsWith("#") && !trimmed.startsWith(">")) {
2492
+ memories.push({ content: trimmed, type: inferType(trimmed) });
2493
+ }
2494
+ }
2495
+ return memories;
2496
+ }
2497
+ function normalize(s) {
2498
+ return s.toLowerCase().replace(/\s+/g, " ").replace(/[.,;:!?]+$/, "").trim();
2499
+ }
2500
+ var ingestCommand = new Command18("ingest").description("Import memories from existing IDE rule files").argument("[source]", "Source to import from (cursor, claude, agents, copilot, windsurf, cline, roo, gemini, or file path)").option("--type <type>", "Override type for all imported memories").option("--dry-run", "Preview without importing").option("--all", "Scan all known IDE rule file locations").option("--no-dedup", "Skip duplicate detection").action(async (source, opts) => {
2501
+ try {
2502
+ if (opts.type && !VALID_TYPES8.includes(opts.type)) {
2503
+ console.error(chalk15.red("\u2717") + ` Invalid type "${opts.type}". Valid types: ${VALID_TYPES8.join(", ")}`);
2504
+ process.exit(1);
2505
+ }
2506
+ const filesToProcess = [];
2507
+ if (opts.all) {
2508
+ for (const src of SOURCES) {
2509
+ for (const p of src.paths) {
2510
+ if (existsSync7(p)) {
2511
+ filesToProcess.push({ name: src.name, path: p });
2512
+ }
2513
+ }
2514
+ }
2515
+ } else if (source) {
2516
+ const known = SOURCES.find((s) => s.name === source);
2517
+ if (known) {
2518
+ for (const p of known.paths) {
2519
+ if (existsSync7(p)) {
2520
+ filesToProcess.push({ name: known.name, path: p });
2521
+ break;
2522
+ }
2523
+ }
2524
+ if (filesToProcess.length === 0) {
2525
+ console.error(chalk15.red("\u2717") + ` No ${known.description} file found at: ${known.paths.join(", ")}`);
2526
+ process.exit(1);
2527
+ }
2528
+ } else if (existsSync7(source)) {
2529
+ filesToProcess.push({ name: "file", path: source });
2530
+ } else {
2531
+ console.error(chalk15.red("\u2717") + ` Unknown source "${source}". Valid: ${SOURCES.map((s) => s.name).join(", ")}, or a file path`);
2532
+ process.exit(1);
2533
+ }
2534
+ } else {
2535
+ console.error(chalk15.red("\u2717") + " Specify a source or use --all");
2536
+ process.exit(1);
2537
+ }
2538
+ if (filesToProcess.length === 0) {
2539
+ console.log(chalk15.dim("No IDE rule files found."));
2540
+ return;
2541
+ }
2542
+ const existingSet = /* @__PURE__ */ new Set();
2543
+ if (opts.dedup !== false) {
2544
+ const db = await getDb();
2545
+ const result = await db.execute("SELECT content FROM memories WHERE deleted_at IS NULL");
2546
+ for (const row of result.rows) {
2547
+ existingSet.add(normalize(String(row.content)));
2548
+ }
2549
+ }
2550
+ let totalImported = 0;
2551
+ let totalSkipped = 0;
2552
+ for (const file of filesToProcess) {
2553
+ const content = await readFile6(file.path, "utf-8");
2554
+ if (content.includes(MARKER2)) {
2555
+ console.log(chalk15.dim(` Skipping ${file.path} (generated by memories.sh)`));
2556
+ continue;
2557
+ }
2558
+ const memories = extractMemories(content);
2559
+ if (memories.length === 0) {
2560
+ console.log(chalk15.dim(` No importable memories found in ${file.path}`));
2561
+ continue;
2562
+ }
2563
+ console.log(chalk15.bold(`
2564
+ ${file.name}`) + chalk15.dim(` (${file.path}) \u2014 ${memories.length} items`));
2565
+ for (const mem of memories) {
2566
+ const type = opts.type ?? mem.type;
2567
+ if (opts.dedup !== false && existingSet.has(normalize(mem.content))) {
2568
+ if (opts.dryRun) {
2569
+ console.log(` ${chalk15.dim("skip")} ${chalk15.dim(mem.content)}`);
2570
+ }
2571
+ totalSkipped++;
2572
+ continue;
2573
+ }
2574
+ if (opts.dryRun) {
2575
+ const typeColor = type === "rule" ? chalk15.blue : type === "decision" ? chalk15.yellow : type === "fact" ? chalk15.green : chalk15.dim;
2576
+ console.log(` ${typeColor(type.padEnd(9))} ${mem.content}`);
2577
+ } else {
2578
+ await addMemory(mem.content, { type });
2579
+ existingSet.add(normalize(mem.content));
2580
+ totalImported++;
2581
+ }
2582
+ }
2583
+ }
2584
+ if (opts.dryRun) {
2585
+ const skipMsg = totalSkipped > 0 ? ` (${totalSkipped} duplicates skipped)` : "";
2586
+ console.log(chalk15.dim(`
2587
+ Dry run \u2014 no memories imported.${skipMsg} Remove --dry-run to import.`));
2588
+ } else {
2589
+ const skipMsg = totalSkipped > 0 ? chalk15.dim(` (${totalSkipped} duplicates skipped)`) : "";
2590
+ console.log(chalk15.green("\n\u2713") + ` Imported ${totalImported} memories` + skipMsg);
2591
+ }
2592
+ } catch (error) {
2593
+ console.error(chalk15.red("\u2717") + " Failed to ingest:", error instanceof Error ? error.message : "Unknown error");
2594
+ process.exit(1);
2595
+ }
2596
+ });
2597
+
2598
+ // src/commands/diff.ts
2599
+ import { Command as Command19 } from "commander";
2600
+ import chalk16 from "chalk";
2601
+ import { readFile as readFile7 } from "fs/promises";
2602
+ import { existsSync as existsSync8 } from "fs";
2603
+ import { resolve as resolve2 } from "path";
2604
+ var MARKER3 = "Generated by memories.sh";
2605
+ var VALID_TYPES9 = ["rule", "decision", "fact", "note"];
2606
+ var TARGETS2 = [
2607
+ { name: "cursor", defaultPath: ".cursor/rules/memories.mdc" },
2608
+ { name: "claude", defaultPath: "CLAUDE.md" },
2609
+ { name: "agents", defaultPath: "AGENTS.md" },
2610
+ { name: "copilot", defaultPath: ".github/copilot-instructions.md" },
2611
+ { name: "windsurf", defaultPath: ".windsurf/rules/memories.md" },
2612
+ { name: "cline", defaultPath: ".clinerules/memories.md" },
2613
+ { name: "roo", defaultPath: ".roo/rules/memories.md" },
2614
+ { name: "gemini", defaultPath: "GEMINI.md" }
2615
+ ];
2616
+ function extractFileMemories(content) {
2617
+ const memories = /* @__PURE__ */ new Set();
2618
+ for (const line of content.split("\n")) {
2619
+ const trimmed = line.trim();
2620
+ if (trimmed.startsWith("- ") && trimmed.length > 2) {
2621
+ memories.add(trimmed.slice(2));
2622
+ }
2623
+ }
2624
+ return memories;
2625
+ }
2626
+ function extractTimestamp(content) {
2627
+ const match = content.match(/Generated by memories\.sh at (.+?)\s*-->/);
2628
+ return match ? match[1] : null;
2629
+ }
2630
+ function parseTypes2(raw) {
2631
+ if (!raw) return ["rule", "decision", "fact"];
2632
+ const types = raw.split(",").map((s) => s.trim());
2633
+ for (const t of types) {
2634
+ if (!VALID_TYPES9.includes(t)) {
2635
+ console.error(chalk16.red("\u2717") + ` Invalid type "${t}". Valid: ${VALID_TYPES9.join(", ")}`);
2636
+ process.exit(1);
2637
+ }
2638
+ }
2639
+ return types;
2640
+ }
2641
+ async function fetchMemories2(types) {
2642
+ const projectId = getProjectId() ?? void 0;
2643
+ return listMemories({ limit: 1e4, types, projectId });
2644
+ }
2645
+ async function diffTarget(target, currentMemories, outputPath) {
2646
+ const filePath = resolve2(outputPath ?? target.defaultPath);
2647
+ if (!existsSync8(filePath)) {
2648
+ return {
2649
+ added: currentMemories.map((m) => m.content),
2650
+ removed: [],
2651
+ unchanged: 0,
2652
+ filePath,
2653
+ exists: false,
2654
+ isOurs: false,
2655
+ generatedAt: null
2656
+ };
2657
+ }
2658
+ const content = await readFile7(filePath, "utf-8");
2659
+ const isOurs = content.includes(MARKER3);
2660
+ const generatedAt = extractTimestamp(content);
2661
+ const fileMemories = extractFileMemories(content);
2662
+ const currentSet = new Set(currentMemories.map((m) => m.content));
2663
+ const added = [...currentSet].filter((c) => !fileMemories.has(c));
2664
+ const removed = [...fileMemories].filter((c) => !currentSet.has(c));
2665
+ const unchanged = [...currentSet].filter((c) => fileMemories.has(c)).length;
2666
+ return { added, removed, unchanged, filePath, exists: true, isOurs, generatedAt };
2667
+ }
2668
+ var diffCommand = new Command19("diff").description("Show what changed since last generate (are IDE rule files stale?)").argument("[target]", `Target to check (${TARGETS2.map((t) => t.name).join(", ")}, or all)`).option("--types <types>", "Comma-separated types to include (default: rule,decision,fact)").action(async (target, opts) => {
2669
+ try {
2670
+ const types = parseTypes2(opts.types);
2671
+ const memories = await fetchMemories2(types);
2672
+ const targetsToCheck = target && target !== "all" ? TARGETS2.filter((t) => t.name === target) : TARGETS2;
2673
+ if (target && target !== "all" && targetsToCheck.length === 0) {
2674
+ console.error(chalk16.red("\u2717") + ` Unknown target "${target}". Valid: ${TARGETS2.map((t) => t.name).join(", ")}`);
2675
+ process.exit(1);
2676
+ }
2677
+ let anyStale = false;
2678
+ for (const t of targetsToCheck) {
2679
+ const result = await diffTarget(t, memories);
2680
+ if (!result.exists && !target) continue;
2681
+ const hasChanges = result.added.length > 0 || result.removed.length > 0;
2682
+ if (!result.exists) {
2683
+ console.log(chalk16.bold(`
2684
+ ${t.name}`) + chalk16.dim(` \u2192 ${result.filePath}`));
2685
+ console.log(chalk16.yellow(" Not generated yet.") + chalk16.dim(` Run: memories generate ${t.name}`));
2686
+ anyStale = true;
2687
+ continue;
2688
+ }
2689
+ if (!result.isOurs) {
2690
+ console.log(chalk16.bold(`
2691
+ ${t.name}`) + chalk16.dim(` \u2192 ${result.filePath}`));
2692
+ console.log(chalk16.dim(" Not managed by memories.sh (no marker found)"));
2693
+ continue;
2694
+ }
2695
+ if (!hasChanges) {
2696
+ if (target) {
2697
+ console.log(chalk16.bold(`
2698
+ ${t.name}`) + chalk16.dim(` \u2192 ${result.filePath}`));
2699
+ const since = result.generatedAt ? chalk16.dim(` (generated ${formatRelative(result.generatedAt)})`) : "";
2700
+ console.log(chalk16.green(" Up to date.") + since);
2701
+ }
2702
+ continue;
2703
+ }
2704
+ anyStale = true;
2705
+ console.log(chalk16.bold(`
2706
+ ${t.name}`) + chalk16.dim(` \u2192 ${result.filePath}`));
2707
+ if (result.generatedAt) {
2708
+ console.log(chalk16.dim(` Generated ${formatRelative(result.generatedAt)}`));
2709
+ }
2710
+ for (const a of result.added) {
2711
+ console.log(chalk16.green(` + ${a}`));
2712
+ }
2713
+ for (const r of result.removed) {
2714
+ console.log(chalk16.red(` - ${r}`));
2715
+ }
2716
+ if (result.unchanged > 0) {
2717
+ console.log(chalk16.dim(` ${result.unchanged} unchanged`));
2718
+ }
2719
+ }
2720
+ if (!anyStale) {
2721
+ if (target) {
2722
+ } else {
2723
+ console.log(chalk16.green("\n All generated files are up to date."));
2724
+ }
2725
+ } else {
2726
+ console.log(chalk16.dim(`
2727
+ Run ${chalk16.bold("memories generate")} to update stale files.`));
2728
+ }
2729
+ } catch (error) {
2730
+ console.error(chalk16.red("\u2717") + " Diff failed:", error instanceof Error ? error.message : "Unknown error");
2731
+ process.exit(1);
2732
+ }
2733
+ });
2734
+ function formatRelative(isoDate) {
2735
+ const then = new Date(isoDate);
2736
+ const now = /* @__PURE__ */ new Date();
2737
+ const diffMs = now.getTime() - then.getTime();
2738
+ if (diffMs < 0) return "just now";
2739
+ const seconds = Math.floor(diffMs / 1e3);
2740
+ if (seconds < 60) return "just now";
2741
+ const minutes = Math.floor(seconds / 60);
2742
+ if (minutes < 60) return `${minutes}m ago`;
2743
+ const hours = Math.floor(minutes / 60);
2744
+ if (hours < 24) return `${hours}h ago`;
2745
+ const days = Math.floor(hours / 24);
2746
+ return `${days}d ago`;
2747
+ }
2748
+
2749
+ // src/commands/tag.ts
2750
+ import { Command as Command20 } from "commander";
2751
+ import chalk17 from "chalk";
2752
+ var VALID_TYPES10 = ["rule", "decision", "fact", "note"];
2753
+ function buildWhere(filters) {
2754
+ const conditions = ["deleted_at IS NULL"];
2755
+ const args = [];
2756
+ const projectId = getProjectId();
2757
+ if (filters.type) {
2758
+ if (!VALID_TYPES10.includes(filters.type)) {
2759
+ throw new Error(`Invalid type "${filters.type}". Valid: ${VALID_TYPES10.join(", ")}`);
2760
+ }
2761
+ conditions.push("type = ?");
2762
+ args.push(filters.type);
2763
+ }
2764
+ if (filters.scope === "global") {
2765
+ conditions.push("scope = 'global'");
2766
+ } else if (filters.scope === "project") {
2767
+ if (!projectId) throw new Error("Not in a git repo \u2014 cannot filter by project scope.");
2768
+ conditions.push("scope = 'project' AND project_id = ?");
2769
+ args.push(projectId);
2770
+ }
2771
+ return { where: conditions.join(" AND "), args };
2772
+ }
2773
+ function parseTags(raw) {
2774
+ if (!raw) return /* @__PURE__ */ new Set();
2775
+ return new Set(raw.split(",").map((t) => t.trim()).filter(Boolean));
2776
+ }
2777
+ var tagCommand = new Command20("tag").description("Bulk tag/untag operations on memories");
2778
+ tagCommand.addCommand(
2779
+ new Command20("add").description("Add a tag to matching memories").argument("<tag>", "Tag to add").option("--type <type>", "Filter by memory type (rule, decision, fact, note)").option("--scope <scope>", "Filter by scope (global, project)").option("--dry-run", "Preview without modifying").action(async (tag, opts) => {
2780
+ try {
2781
+ const db = await getDb();
2782
+ const { where, args } = buildWhere(opts);
2783
+ const result = await db.execute({ sql: `SELECT id, tags FROM memories WHERE ${where}`, args });
2784
+ let updated = 0;
2785
+ let skipped = 0;
2786
+ for (const row of result.rows) {
2787
+ const existing = parseTags(row.tags);
2788
+ if (existing.has(tag)) {
2789
+ skipped++;
2790
+ continue;
2791
+ }
2792
+ existing.add(tag);
2793
+ const newTags = [...existing].join(",");
2794
+ if (!opts.dryRun) {
2795
+ await db.execute({
2796
+ sql: "UPDATE memories SET tags = ?, updated_at = datetime('now') WHERE id = ?",
2797
+ args: [newTags, row.id]
2798
+ });
2799
+ }
2800
+ updated++;
2801
+ }
2802
+ if (opts.dryRun) {
2803
+ console.log(chalk17.dim(`Dry run \u2014 would tag ${updated} memories with "${tag}" (${skipped} already tagged)`));
2804
+ } else {
2805
+ console.log(chalk17.green("\u2713") + ` Tagged ${updated} memories with "${tag}"` + (skipped > 0 ? chalk17.dim(` (${skipped} already tagged)`) : ""));
2806
+ }
2807
+ } catch (error) {
2808
+ console.error(chalk17.red("\u2717") + ` Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
2809
+ process.exit(1);
2810
+ }
2811
+ })
2812
+ );
2813
+ tagCommand.addCommand(
2814
+ new Command20("remove").description("Remove a tag from matching memories").argument("<tag>", "Tag to remove").option("--type <type>", "Filter by memory type (rule, decision, fact, note)").option("--scope <scope>", "Filter by scope (global, project)").option("--dry-run", "Preview without modifying").action(async (tag, opts) => {
2815
+ try {
2816
+ const db = await getDb();
2817
+ const { where, args } = buildWhere(opts);
2818
+ const result = await db.execute({
2819
+ sql: `SELECT id, tags FROM memories WHERE ${where} AND tags LIKE ?`,
2820
+ args: [...args, `%${tag}%`]
2821
+ });
2822
+ let updated = 0;
2823
+ for (const row of result.rows) {
2824
+ const existing = parseTags(row.tags);
2825
+ if (!existing.has(tag)) continue;
2826
+ existing.delete(tag);
2827
+ const newTags = existing.size > 0 ? [...existing].join(",") : null;
2828
+ if (!opts.dryRun) {
2829
+ await db.execute({
2830
+ sql: "UPDATE memories SET tags = ?, updated_at = datetime('now') WHERE id = ?",
2831
+ args: [newTags, row.id]
2832
+ });
2833
+ }
2834
+ updated++;
2835
+ }
2836
+ if (opts.dryRun) {
2837
+ console.log(chalk17.dim(`Dry run \u2014 would remove "${tag}" from ${updated} memories`));
2838
+ } else {
2839
+ console.log(chalk17.green("\u2713") + ` Removed "${tag}" from ${updated} memories`);
2840
+ }
2841
+ } catch (error) {
2842
+ console.error(chalk17.red("\u2717") + ` Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
2843
+ process.exit(1);
2844
+ }
2845
+ })
2846
+ );
2847
+ tagCommand.addCommand(
2848
+ new Command20("list").description("List all tags in use with counts").option("--type <type>", "Filter by memory type").option("--scope <scope>", "Filter by scope (global, project)").action(async (opts) => {
2849
+ try {
2850
+ const db = await getDb();
2851
+ const { where, args } = buildWhere(opts);
2852
+ const result = await db.execute({
2853
+ sql: `SELECT tags FROM memories WHERE ${where} AND tags IS NOT NULL AND tags != ''`,
2854
+ args
2855
+ });
2856
+ const counts = /* @__PURE__ */ new Map();
2857
+ for (const row of result.rows) {
2858
+ for (const tag of parseTags(row.tags)) {
2859
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
2860
+ }
2861
+ }
2862
+ if (counts.size === 0) {
2863
+ console.log(chalk17.dim("No tags found."));
2864
+ return;
2865
+ }
2866
+ const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
2867
+ for (const [tag, count] of sorted) {
2868
+ console.log(` ${chalk17.bold(tag)} ${chalk17.dim(`(${count})`)}`);
2869
+ }
2870
+ } catch (error) {
2871
+ console.error(chalk17.red("\u2717") + ` Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
2872
+ process.exit(1);
2873
+ }
2874
+ })
2875
+ );
2876
+
2877
+ // src/index.ts
2878
+ var program = new Command21().name("memories").description("A local-first memory layer for AI agents").version("0.1.0");
2879
+ program.addCommand(initCommand);
2880
+ program.addCommand(addCommand);
2881
+ program.addCommand(recallCommand);
2882
+ program.addCommand(promptCommand);
2883
+ program.addCommand(searchCommand);
2884
+ program.addCommand(listCommand);
2885
+ program.addCommand(forgetCommand);
2886
+ program.addCommand(exportCommand);
2887
+ program.addCommand(importCommand);
2888
+ program.addCommand(configCommand);
2889
+ program.addCommand(serveCommand);
2890
+ program.addCommand(syncCommand);
2891
+ program.addCommand(generateCommand);
2892
+ program.addCommand(editCommand);
2893
+ program.addCommand(statsCommand);
2894
+ program.addCommand(doctorCommand);
2895
+ program.addCommand(hookCommand);
2896
+ program.addCommand(ingestCommand);
2897
+ program.addCommand(diffCommand);
2898
+ program.addCommand(tagCommand);
2899
+ program.parse();