gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/onboarding.d.ts +5 -1
  2. package/dist/onboarding.js +5 -3
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
  5. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  8. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
  9. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  10. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  11. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  12. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  13. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  14. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  15. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  16. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  17. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  18. package/dist/resources/extensions/gsd/model-router.js +9 -5
  19. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  20. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  21. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  58. package/packages/mcp-server/dist/server.js +12 -10
  59. package/packages/mcp-server/dist/server.js.map +1 -1
  60. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/session-manager.js +8 -1
  62. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  63. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  64. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.js +113 -14
  66. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  67. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  68. package/packages/mcp-server/src/server.ts +12 -10
  69. package/packages/mcp-server/src/session-manager.ts +10 -3
  70. package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
  71. package/packages/mcp-server/src/workflow-tools.ts +128 -18
  72. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  74. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  75. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  76. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  77. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  78. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
  79. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  80. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  81. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  82. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  83. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  84. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  85. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  86. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  87. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  88. package/src/resources/extensions/gsd/model-router.ts +10 -5
  89. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  90. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  91. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  92. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  94. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  97. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  98. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  99. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  100. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  102. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  103. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  104. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  105. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  106. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  107. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  108. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * GSD Command — `/gsd memory`
3
+ *
4
+ * Subcommands:
5
+ * list — show recent active memories
6
+ * show <id> — print one memory
7
+ * ingest <uri> — persist a source row (file path, URL, or "-" for stdin-piped note)
8
+ * note "<text>" — persist an inline note as a source
9
+ * forget <id> — supersede a memory (CAP_EXCEEDED sentinel)
10
+ * stats — category / scope counts + source count
11
+ * sources — list recent memory_sources rows
12
+ * extract <src> — dispatch an agent turn that distils a source into memories
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from "node:fs";
16
+ import { resolve as resolvePath } from "node:path";
17
+
18
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
19
+
20
+ import { projectRoot } from "./commands/context.js";
21
+ import { ingestFile, ingestNote, ingestUrl, summarizeIngest } from "./memory-ingest.js";
22
+ import { getMemorySource, listMemorySources } from "./memory-source-store.js";
23
+ import {
24
+ createMemory,
25
+ decayStaleMemories,
26
+ enforceMemoryCap,
27
+ getActiveMemories,
28
+ getActiveMemoriesRanked,
29
+ supersedeMemory,
30
+ } from "./memory-store.js";
31
+ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
32
+ import { createMemoryRelation, listRelationsFor } from "./memory-relations.js";
33
+
34
+ // ─── Arg parsing ────────────────────────────────────────────────────────────
35
+
36
+ interface MemoryCmdArgs {
37
+ sub: string;
38
+ positional: string[];
39
+ tags: string[];
40
+ scope?: string;
41
+ extract: boolean;
42
+ }
43
+
44
+ function parseArgs(raw: string): MemoryCmdArgs {
45
+ const tokens = splitArgs(raw);
46
+ const sub = (tokens.shift() ?? "list").toLowerCase();
47
+ const positional: string[] = [];
48
+ const tags: string[] = [];
49
+ let scope: string | undefined;
50
+ let extract = false;
51
+
52
+ for (let i = 0; i < tokens.length; i++) {
53
+ const tok = tokens[i];
54
+ if (tok === "--tag" && i + 1 < tokens.length) {
55
+ tags.push(...tokens[++i].split(",").map((t) => t.trim()).filter(Boolean));
56
+ continue;
57
+ }
58
+ if (tok.startsWith("--tag=")) {
59
+ tags.push(...tok.slice("--tag=".length).split(",").map((t) => t.trim()).filter(Boolean));
60
+ continue;
61
+ }
62
+ if (tok === "--scope" && i + 1 < tokens.length) {
63
+ scope = tokens[++i];
64
+ continue;
65
+ }
66
+ if (tok.startsWith("--scope=")) {
67
+ scope = tok.slice("--scope=".length);
68
+ continue;
69
+ }
70
+ if (tok === "--extract") {
71
+ extract = true;
72
+ continue;
73
+ }
74
+ if (tok === "--no-extract") {
75
+ extract = false;
76
+ continue;
77
+ }
78
+ positional.push(tok);
79
+ }
80
+ return { sub, positional, tags, scope, extract };
81
+ }
82
+
83
+ function splitArgs(raw: string): string[] {
84
+ const tokens: string[] = [];
85
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
86
+ let match: RegExpExecArray | null;
87
+ while ((match = re.exec(raw)) !== null) {
88
+ tokens.push(match[1] ?? match[2] ?? match[3]);
89
+ }
90
+ return tokens;
91
+ }
92
+
93
+ function truncate(text: string, max: number): string {
94
+ if (text.length <= max) return text;
95
+ return `${text.slice(0, max - 1)}…`;
96
+ }
97
+
98
+ // ─── Handler ────────────────────────────────────────────────────────────────
99
+
100
+ export async function handleMemory(
101
+ args: string,
102
+ ctx: ExtensionCommandContext,
103
+ pi: ExtensionAPI,
104
+ ): Promise<void> {
105
+ const parsed = parseArgs(args);
106
+
107
+ // `/gsd memory` or `/gsd memory help`
108
+ if (parsed.sub === "" || parsed.sub === "help") {
109
+ ctx.ui.notify(usage(), "info");
110
+ return;
111
+ }
112
+
113
+ // Most subcommands need the DB.
114
+ await ensureDb();
115
+
116
+ switch (parsed.sub) {
117
+ case "list":
118
+ handleList(ctx);
119
+ return;
120
+ case "show":
121
+ handleShow(ctx, parsed.positional[0]);
122
+ return;
123
+ case "forget":
124
+ handleForget(ctx, parsed.positional[0]);
125
+ return;
126
+ case "stats":
127
+ handleStats(ctx);
128
+ return;
129
+ case "sources":
130
+ handleSources(ctx);
131
+ return;
132
+ case "note":
133
+ await handleNote(ctx, parsed);
134
+ return;
135
+ case "ingest":
136
+ await handleIngest(ctx, parsed);
137
+ return;
138
+ case "extract":
139
+ handleExtractSource(ctx, pi, parsed.positional[0]);
140
+ return;
141
+ case "export":
142
+ handleExport(ctx, parsed.positional[0]);
143
+ return;
144
+ case "import":
145
+ handleImport(ctx, parsed.positional[0]);
146
+ return;
147
+ case "decay":
148
+ handleDecay(ctx);
149
+ return;
150
+ case "cap":
151
+ handleCap(ctx, parsed.positional[0]);
152
+ return;
153
+ default:
154
+ ctx.ui.notify(`Unknown subcommand "${parsed.sub}". ${usage()}`, "warning");
155
+ return;
156
+ }
157
+ }
158
+
159
+ function usage(): string {
160
+ return [
161
+ "Usage: /gsd memory <subcommand>",
162
+ " list list recent active memories",
163
+ " show <MEM###> print one memory",
164
+ " forget <MEM###> supersede a memory",
165
+ " stats counts by category / scope / sources / edges",
166
+ " sources list recent memory_sources",
167
+ ' note "<text>" ingest an inline note as a source',
168
+ " ingest <path|url> ingest a local file path or URL",
169
+ " extract <SRC-xxx> dispatch an LLM turn to extract memories from a source",
170
+ " export <path.json> dump memories + relations + sources to JSON",
171
+ " import <path.json> load a previous export (idempotent)",
172
+ " decay run the stale-memory decay pass immediately",
173
+ " cap [N] enforce the memory cap (default 50)",
174
+ "",
175
+ "Options: --tag a,b --scope project|global|<custom> --extract",
176
+ ].join("\n");
177
+ }
178
+
179
+ async function ensureDb(): Promise<void> {
180
+ if (isDbAvailable()) return;
181
+ const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
182
+ await ensureDbOpen();
183
+ }
184
+
185
+ function handleList(ctx: ExtensionCommandContext): void {
186
+ if (!isDbAvailable()) {
187
+ ctx.ui.notify("No GSD database available.", "warning");
188
+ return;
189
+ }
190
+ const memories = getActiveMemoriesRanked(50);
191
+ if (memories.length === 0) {
192
+ ctx.ui.notify("No active memories.", "info");
193
+ return;
194
+ }
195
+ const lines = memories.map(
196
+ (m) =>
197
+ `- [${m.id}] (${m.category}, conf ${m.confidence.toFixed(2)}, hits ${m.hit_count}${m.scope && m.scope !== "project" ? `, ${m.scope}` : ""}) ${truncate(m.content, 100)}`,
198
+ );
199
+ ctx.ui.notify(lines.join("\n"), "info");
200
+ }
201
+
202
+ function handleShow(ctx: ExtensionCommandContext, id: string | undefined): void {
203
+ if (!id) {
204
+ ctx.ui.notify("Usage: /gsd memory show <MEM###>", "warning");
205
+ return;
206
+ }
207
+ const adapter = _getAdapter();
208
+ if (!adapter) {
209
+ ctx.ui.notify("No GSD database available.", "warning");
210
+ return;
211
+ }
212
+ const row = adapter.prepare("SELECT * FROM memories WHERE id = :id").get({ ":id": id });
213
+ if (!row) {
214
+ ctx.ui.notify(`Memory not found: ${id}`, "warning");
215
+ return;
216
+ }
217
+ const tags = row["tags"] ? safeJsonArray(row["tags"] as string) : [];
218
+ const lines = [
219
+ `ID: ${row["id"]}`,
220
+ `Category: ${row["category"]}`,
221
+ `Scope: ${row["scope"] ?? "project"}`,
222
+ `Confidence: ${Number(row["confidence"]).toFixed(2)}`,
223
+ `Hits: ${row["hit_count"]}`,
224
+ `Created: ${row["created_at"]}`,
225
+ `Updated: ${row["updated_at"]}`,
226
+ tags.length > 0 ? `Tags: ${tags.join(", ")}` : null,
227
+ row["superseded_by"] ? `Superseded by: ${row["superseded_by"]}` : null,
228
+ row["source_unit_type"] ? `Source: ${row["source_unit_type"]}/${row["source_unit_id"]}` : null,
229
+ "",
230
+ String(row["content"]),
231
+ ]
232
+ .filter((line): line is string => line !== null)
233
+ .join("\n");
234
+ ctx.ui.notify(lines, "info");
235
+ }
236
+
237
+ function handleForget(ctx: ExtensionCommandContext, id: string | undefined): void {
238
+ if (!id) {
239
+ ctx.ui.notify("Usage: /gsd memory forget <MEM###>", "warning");
240
+ return;
241
+ }
242
+ const ok = supersedeMemory(id, "CAP_EXCEEDED");
243
+ if (!ok) {
244
+ ctx.ui.notify(`Failed to forget ${id}.`, "warning");
245
+ return;
246
+ }
247
+ ctx.ui.notify(`Forgot ${id}.`, "info");
248
+ }
249
+
250
+ function handleStats(ctx: ExtensionCommandContext): void {
251
+ const adapter = _getAdapter();
252
+ if (!adapter) {
253
+ ctx.ui.notify("No GSD database available.", "warning");
254
+ return;
255
+ }
256
+ try {
257
+ const activeRow = adapter
258
+ .prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL")
259
+ .get();
260
+ const supersededRow = adapter
261
+ .prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NOT NULL")
262
+ .get();
263
+ const byCategory = adapter
264
+ .prepare(
265
+ "SELECT category, count(*) as cnt FROM memories WHERE superseded_by IS NULL GROUP BY category ORDER BY cnt DESC",
266
+ )
267
+ .all();
268
+ const byScope = adapter
269
+ .prepare(
270
+ "SELECT scope, count(*) as cnt FROM memories WHERE superseded_by IS NULL GROUP BY scope ORDER BY cnt DESC",
271
+ )
272
+ .all();
273
+ const sourcesRow = adapter.prepare("SELECT count(*) as cnt FROM memory_sources").get();
274
+ const sourcesByKind = adapter
275
+ .prepare("SELECT kind, count(*) as cnt FROM memory_sources GROUP BY kind ORDER BY cnt DESC")
276
+ .all();
277
+ const relationsRow = adapter.prepare("SELECT count(*) as cnt FROM memory_relations").get();
278
+ const relationsByRel = adapter
279
+ .prepare("SELECT rel, count(*) as cnt FROM memory_relations GROUP BY rel ORDER BY cnt DESC")
280
+ .all();
281
+ const embeddingsRow = adapter.prepare("SELECT count(*) as cnt FROM memory_embeddings").get();
282
+ const embeddedActiveRow = adapter
283
+ .prepare(
284
+ `SELECT count(*) as cnt FROM memory_embeddings e
285
+ JOIN memories m ON m.id = e.memory_id
286
+ WHERE m.superseded_by IS NULL`,
287
+ )
288
+ .get();
289
+ const activeCount = (activeRow?.["cnt"] as number) ?? 0;
290
+ const embeddedActive = (embeddedActiveRow?.["cnt"] as number) ?? 0;
291
+ const coverage = activeCount > 0 ? `${Math.round((embeddedActive / activeCount) * 100)}%` : "n/a";
292
+
293
+ const out = [
294
+ `Active memories: ${activeCount}`,
295
+ `Superseded: ${supersededRow?.["cnt"] ?? 0}`,
296
+ "",
297
+ "By category:",
298
+ ...byCategory.map((row) => ` ${row["category"]}: ${row["cnt"]}`),
299
+ "",
300
+ "By scope:",
301
+ ...byScope.map((row) => ` ${row["scope"]}: ${row["cnt"]}`),
302
+ "",
303
+ `Memory sources: ${sourcesRow?.["cnt"] ?? 0}`,
304
+ ...sourcesByKind.map((row) => ` ${row["kind"]}: ${row["cnt"]}`),
305
+ "",
306
+ `Relations: ${relationsRow?.["cnt"] ?? 0}`,
307
+ ...relationsByRel.map((row) => ` ${row["rel"]}: ${row["cnt"]}`),
308
+ "",
309
+ `Embeddings: ${embeddingsRow?.["cnt"] ?? 0} total, ${embeddedActive} active (coverage ${coverage})`,
310
+ ].join("\n");
311
+ ctx.ui.notify(out, "info");
312
+ } catch (err) {
313
+ ctx.ui.notify(`Stats failed: ${(err as Error).message}`, "warning");
314
+ }
315
+ }
316
+
317
+ function handleExport(ctx: ExtensionCommandContext, target: string | undefined): void {
318
+ if (!target) {
319
+ ctx.ui.notify("Usage: /gsd memory export <path.json>", "warning");
320
+ return;
321
+ }
322
+ try {
323
+ const active = getActiveMemories();
324
+ const relations = active.flatMap((m) =>
325
+ listRelationsFor(m.id).filter((r) => r.from === m.id),
326
+ );
327
+ const sources = listMemorySources(500);
328
+ const payload = {
329
+ version: 1,
330
+ exported_at: new Date().toISOString(),
331
+ memories: active.map((m) => ({
332
+ id: m.id,
333
+ category: m.category,
334
+ content: m.content,
335
+ confidence: m.confidence,
336
+ hit_count: m.hit_count,
337
+ scope: m.scope,
338
+ tags: m.tags,
339
+ source_unit_type: m.source_unit_type,
340
+ source_unit_id: m.source_unit_id,
341
+ created_at: m.created_at,
342
+ updated_at: m.updated_at,
343
+ })),
344
+ relations: relations.map((r) => ({
345
+ from: r.from,
346
+ to: r.to,
347
+ rel: r.rel,
348
+ confidence: r.confidence,
349
+ })),
350
+ sources,
351
+ };
352
+ const abs = resolvePath(process.cwd(), target);
353
+ writeFileSync(abs, JSON.stringify(payload, null, 2), "utf-8");
354
+ ctx.ui.notify(
355
+ `Exported ${payload.memories.length} memories, ${payload.relations.length} relations, ${payload.sources.length} sources → ${abs}`,
356
+ "info",
357
+ );
358
+ } catch (err) {
359
+ ctx.ui.notify(`Export failed: ${(err as Error).message}`, "error");
360
+ }
361
+ }
362
+
363
+ interface ExportedMemory {
364
+ id?: string;
365
+ category: string;
366
+ content: string;
367
+ confidence?: number;
368
+ scope?: string;
369
+ tags?: string[];
370
+ }
371
+
372
+ interface ExportedRelation {
373
+ from: string;
374
+ to: string;
375
+ rel: string;
376
+ confidence?: number;
377
+ }
378
+
379
+ function handleImport(ctx: ExtensionCommandContext, target: string | undefined): void {
380
+ if (!target) {
381
+ ctx.ui.notify("Usage: /gsd memory import <path.json>", "warning");
382
+ return;
383
+ }
384
+ try {
385
+ const abs = resolvePath(process.cwd(), target);
386
+ const raw = readFileSync(abs, "utf-8");
387
+ const parsed = JSON.parse(raw) as { memories?: ExportedMemory[]; relations?: ExportedRelation[] };
388
+
389
+ let memoryCount = 0;
390
+ let relationCount = 0;
391
+
392
+ for (const mem of parsed.memories ?? []) {
393
+ if (!mem.category || !mem.content) continue;
394
+ // createMemory allocates a fresh seq → new MEM### id; imports replay
395
+ // content rather than preserving the old ID. Relations from the export
396
+ // file still reference the old IDs, so only lossless round-trips into
397
+ // an empty DB preserve the graph.
398
+ const id = createMemory({
399
+ category: mem.category,
400
+ content: mem.content,
401
+ confidence: mem.confidence,
402
+ scope: mem.scope,
403
+ tags: mem.tags,
404
+ });
405
+ if (id) memoryCount++;
406
+ }
407
+
408
+ for (const rel of parsed.relations ?? []) {
409
+ if (!rel.from || !rel.to || !rel.rel) continue;
410
+ if (createMemoryRelation(rel.from, rel.to, rel.rel as never, rel.confidence)) {
411
+ relationCount++;
412
+ }
413
+ }
414
+
415
+ ctx.ui.notify(`Imported ${memoryCount} memories and ${relationCount} relations.`, "info");
416
+ } catch (err) {
417
+ ctx.ui.notify(`Import failed: ${(err as Error).message}`, "error");
418
+ }
419
+ }
420
+
421
+ function handleDecay(ctx: ExtensionCommandContext): void {
422
+ const decayed = decayStaleMemories(20);
423
+ if (decayed.length === 0) {
424
+ ctx.ui.notify("Decay pass: no stale memories found.", "info");
425
+ return;
426
+ }
427
+ ctx.ui.notify(`Decayed ${decayed.length} stale memor${decayed.length === 1 ? "y" : "ies"}: ${decayed.join(", ")}`, "info");
428
+ }
429
+
430
+ function handleCap(ctx: ExtensionCommandContext, arg: string | undefined): void {
431
+ const max = arg ? Number.parseInt(arg, 10) : 50;
432
+ if (!Number.isFinite(max) || max < 1) {
433
+ ctx.ui.notify("Usage: /gsd memory cap <max> (default 50)", "warning");
434
+ return;
435
+ }
436
+ enforceMemoryCap(max);
437
+ ctx.ui.notify(`Enforced memory cap of ${max}.`, "info");
438
+ }
439
+
440
+ function handleSources(ctx: ExtensionCommandContext): void {
441
+ const sources = listMemorySources(30);
442
+ if (sources.length === 0) {
443
+ ctx.ui.notify("No memory sources yet. Use `/gsd memory ingest <path|url>` to add one.", "info");
444
+ return;
445
+ }
446
+ const lines = sources.map(
447
+ (s) =>
448
+ `- ${s.id} [${s.kind}${s.scope !== "project" ? `/${s.scope}` : ""}] ${truncate(s.title ?? s.uri ?? s.content, 100)}`,
449
+ );
450
+ ctx.ui.notify(lines.join("\n"), "info");
451
+ }
452
+
453
+ async function handleNote(ctx: ExtensionCommandContext, args: MemoryCmdArgs): Promise<void> {
454
+ const text = args.positional.join(" ").trim();
455
+ if (!text) {
456
+ ctx.ui.notify('Usage: /gsd memory note "your note"', "warning");
457
+ return;
458
+ }
459
+ try {
460
+ const result = await ingestNote(text, null, {
461
+ scope: args.scope,
462
+ tags: args.tags,
463
+ extract: false,
464
+ });
465
+ ctx.ui.notify(summarizeIngest(result), "info");
466
+ } catch (err) {
467
+ ctx.ui.notify(`Note ingest failed: ${(err as Error).message}`, "error");
468
+ }
469
+ }
470
+
471
+ async function handleIngest(ctx: ExtensionCommandContext, args: MemoryCmdArgs): Promise<void> {
472
+ const target = args.positional[0];
473
+ if (!target) {
474
+ ctx.ui.notify("Usage: /gsd memory ingest <path|url> [--tag a,b] [--scope project|global]", "warning");
475
+ return;
476
+ }
477
+ try {
478
+ const isUrl = /^https?:\/\//i.test(target);
479
+ const result = isUrl
480
+ ? await ingestUrl(target, null, { scope: args.scope, tags: args.tags, extract: false })
481
+ : await ingestFile(target, null, { scope: args.scope, tags: args.tags, extract: false });
482
+ ctx.ui.notify(summarizeIngest(result), "info");
483
+ if (args.extract && result.sourceId) {
484
+ // TODO (P3): dispatch agent turn to extract memories once source is stored.
485
+ ctx.ui.notify(
486
+ `(Dispatching extraction turn — use \`/gsd memory extract ${result.sourceId}\` to trigger manually.)`,
487
+ "info",
488
+ );
489
+ }
490
+ } catch (err) {
491
+ ctx.ui.notify(`Ingest failed: ${(err as Error).message}`, "error");
492
+ }
493
+ }
494
+
495
+ function handleExtractSource(
496
+ ctx: ExtensionCommandContext,
497
+ pi: ExtensionAPI,
498
+ id: string | undefined,
499
+ ): void {
500
+ if (!id) {
501
+ ctx.ui.notify("Usage: /gsd memory extract <SRC-xxx>", "warning");
502
+ return;
503
+ }
504
+ const source = getMemorySource(id);
505
+ if (!source) {
506
+ ctx.ui.notify(`Source not found: ${id}`, "warning");
507
+ return;
508
+ }
509
+
510
+ const prompt = buildExtractPrompt(source);
511
+ ctx.ui.notify(`Dispatching extraction turn for ${id}...`, "info");
512
+ pi.sendMessage(
513
+ { customType: "gsd-memory-extract", content: prompt, display: false },
514
+ { triggerTurn: true },
515
+ );
516
+ }
517
+
518
+ function buildExtractPrompt(source: { id: string; kind: string; title: string | null; uri: string | null; content: string }): string {
519
+ const header = [
520
+ `## Memory extraction request`,
521
+ ``,
522
+ `Source: ${source.id} (${source.kind})`,
523
+ source.title ? `Title: ${source.title}` : null,
524
+ source.uri ? `URI: ${source.uri}` : null,
525
+ ]
526
+ .filter(Boolean)
527
+ .join("\n");
528
+ return [
529
+ header,
530
+ "",
531
+ "Read the content below and call the `capture_thought` tool once per durable insight",
532
+ "(architecture, convention, gotcha, preference, environment, pattern). Skip one-off details,",
533
+ "temporary state, and anything secret. Keep each memory to 1–3 sentences.",
534
+ "",
535
+ "---",
536
+ "",
537
+ source.content,
538
+ ].join("\n");
539
+ }
540
+
541
+ function safeJsonArray(raw: string): string[] {
542
+ try {
543
+ const parsed = JSON.parse(raw);
544
+ return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === "string") : [];
545
+ } catch {
546
+ return [];
547
+ }
548
+ }
549
+
550
+ // projectRoot is imported so tests can mock it via the same path as other commands.
551
+ export const _internals = { projectRoot };