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,462 @@
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
+ import { readFileSync, writeFileSync } from "node:fs";
15
+ import { resolve as resolvePath } from "node:path";
16
+ import { projectRoot } from "./commands/context.js";
17
+ import { ingestFile, ingestNote, ingestUrl, summarizeIngest } from "./memory-ingest.js";
18
+ import { getMemorySource, listMemorySources } from "./memory-source-store.js";
19
+ import { createMemory, decayStaleMemories, enforceMemoryCap, getActiveMemories, getActiveMemoriesRanked, supersedeMemory, } from "./memory-store.js";
20
+ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
21
+ import { createMemoryRelation, listRelationsFor } from "./memory-relations.js";
22
+ function parseArgs(raw) {
23
+ const tokens = splitArgs(raw);
24
+ const sub = (tokens.shift() ?? "list").toLowerCase();
25
+ const positional = [];
26
+ const tags = [];
27
+ let scope;
28
+ let extract = false;
29
+ for (let i = 0; i < tokens.length; i++) {
30
+ const tok = tokens[i];
31
+ if (tok === "--tag" && i + 1 < tokens.length) {
32
+ tags.push(...tokens[++i].split(",").map((t) => t.trim()).filter(Boolean));
33
+ continue;
34
+ }
35
+ if (tok.startsWith("--tag=")) {
36
+ tags.push(...tok.slice("--tag=".length).split(",").map((t) => t.trim()).filter(Boolean));
37
+ continue;
38
+ }
39
+ if (tok === "--scope" && i + 1 < tokens.length) {
40
+ scope = tokens[++i];
41
+ continue;
42
+ }
43
+ if (tok.startsWith("--scope=")) {
44
+ scope = tok.slice("--scope=".length);
45
+ continue;
46
+ }
47
+ if (tok === "--extract") {
48
+ extract = true;
49
+ continue;
50
+ }
51
+ if (tok === "--no-extract") {
52
+ extract = false;
53
+ continue;
54
+ }
55
+ positional.push(tok);
56
+ }
57
+ return { sub, positional, tags, scope, extract };
58
+ }
59
+ function splitArgs(raw) {
60
+ const tokens = [];
61
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
62
+ let match;
63
+ while ((match = re.exec(raw)) !== null) {
64
+ tokens.push(match[1] ?? match[2] ?? match[3]);
65
+ }
66
+ return tokens;
67
+ }
68
+ function truncate(text, max) {
69
+ if (text.length <= max)
70
+ return text;
71
+ return `${text.slice(0, max - 1)}…`;
72
+ }
73
+ // ─── Handler ────────────────────────────────────────────────────────────────
74
+ export async function handleMemory(args, ctx, pi) {
75
+ const parsed = parseArgs(args);
76
+ // `/gsd memory` or `/gsd memory help`
77
+ if (parsed.sub === "" || parsed.sub === "help") {
78
+ ctx.ui.notify(usage(), "info");
79
+ return;
80
+ }
81
+ // Most subcommands need the DB.
82
+ await ensureDb();
83
+ switch (parsed.sub) {
84
+ case "list":
85
+ handleList(ctx);
86
+ return;
87
+ case "show":
88
+ handleShow(ctx, parsed.positional[0]);
89
+ return;
90
+ case "forget":
91
+ handleForget(ctx, parsed.positional[0]);
92
+ return;
93
+ case "stats":
94
+ handleStats(ctx);
95
+ return;
96
+ case "sources":
97
+ handleSources(ctx);
98
+ return;
99
+ case "note":
100
+ await handleNote(ctx, parsed);
101
+ return;
102
+ case "ingest":
103
+ await handleIngest(ctx, parsed);
104
+ return;
105
+ case "extract":
106
+ handleExtractSource(ctx, pi, parsed.positional[0]);
107
+ return;
108
+ case "export":
109
+ handleExport(ctx, parsed.positional[0]);
110
+ return;
111
+ case "import":
112
+ handleImport(ctx, parsed.positional[0]);
113
+ return;
114
+ case "decay":
115
+ handleDecay(ctx);
116
+ return;
117
+ case "cap":
118
+ handleCap(ctx, parsed.positional[0]);
119
+ return;
120
+ default:
121
+ ctx.ui.notify(`Unknown subcommand "${parsed.sub}". ${usage()}`, "warning");
122
+ return;
123
+ }
124
+ }
125
+ function usage() {
126
+ return [
127
+ "Usage: /gsd memory <subcommand>",
128
+ " list list recent active memories",
129
+ " show <MEM###> print one memory",
130
+ " forget <MEM###> supersede a memory",
131
+ " stats counts by category / scope / sources / edges",
132
+ " sources list recent memory_sources",
133
+ ' note "<text>" ingest an inline note as a source',
134
+ " ingest <path|url> ingest a local file path or URL",
135
+ " extract <SRC-xxx> dispatch an LLM turn to extract memories from a source",
136
+ " export <path.json> dump memories + relations + sources to JSON",
137
+ " import <path.json> load a previous export (idempotent)",
138
+ " decay run the stale-memory decay pass immediately",
139
+ " cap [N] enforce the memory cap (default 50)",
140
+ "",
141
+ "Options: --tag a,b --scope project|global|<custom> --extract",
142
+ ].join("\n");
143
+ }
144
+ async function ensureDb() {
145
+ if (isDbAvailable())
146
+ return;
147
+ const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
148
+ await ensureDbOpen();
149
+ }
150
+ function handleList(ctx) {
151
+ if (!isDbAvailable()) {
152
+ ctx.ui.notify("No GSD database available.", "warning");
153
+ return;
154
+ }
155
+ const memories = getActiveMemoriesRanked(50);
156
+ if (memories.length === 0) {
157
+ ctx.ui.notify("No active memories.", "info");
158
+ return;
159
+ }
160
+ const lines = memories.map((m) => `- [${m.id}] (${m.category}, conf ${m.confidence.toFixed(2)}, hits ${m.hit_count}${m.scope && m.scope !== "project" ? `, ${m.scope}` : ""}) ${truncate(m.content, 100)}`);
161
+ ctx.ui.notify(lines.join("\n"), "info");
162
+ }
163
+ function handleShow(ctx, id) {
164
+ if (!id) {
165
+ ctx.ui.notify("Usage: /gsd memory show <MEM###>", "warning");
166
+ return;
167
+ }
168
+ const adapter = _getAdapter();
169
+ if (!adapter) {
170
+ ctx.ui.notify("No GSD database available.", "warning");
171
+ return;
172
+ }
173
+ const row = adapter.prepare("SELECT * FROM memories WHERE id = :id").get({ ":id": id });
174
+ if (!row) {
175
+ ctx.ui.notify(`Memory not found: ${id}`, "warning");
176
+ return;
177
+ }
178
+ const tags = row["tags"] ? safeJsonArray(row["tags"]) : [];
179
+ const lines = [
180
+ `ID: ${row["id"]}`,
181
+ `Category: ${row["category"]}`,
182
+ `Scope: ${row["scope"] ?? "project"}`,
183
+ `Confidence: ${Number(row["confidence"]).toFixed(2)}`,
184
+ `Hits: ${row["hit_count"]}`,
185
+ `Created: ${row["created_at"]}`,
186
+ `Updated: ${row["updated_at"]}`,
187
+ tags.length > 0 ? `Tags: ${tags.join(", ")}` : null,
188
+ row["superseded_by"] ? `Superseded by: ${row["superseded_by"]}` : null,
189
+ row["source_unit_type"] ? `Source: ${row["source_unit_type"]}/${row["source_unit_id"]}` : null,
190
+ "",
191
+ String(row["content"]),
192
+ ]
193
+ .filter((line) => line !== null)
194
+ .join("\n");
195
+ ctx.ui.notify(lines, "info");
196
+ }
197
+ function handleForget(ctx, id) {
198
+ if (!id) {
199
+ ctx.ui.notify("Usage: /gsd memory forget <MEM###>", "warning");
200
+ return;
201
+ }
202
+ const ok = supersedeMemory(id, "CAP_EXCEEDED");
203
+ if (!ok) {
204
+ ctx.ui.notify(`Failed to forget ${id}.`, "warning");
205
+ return;
206
+ }
207
+ ctx.ui.notify(`Forgot ${id}.`, "info");
208
+ }
209
+ function handleStats(ctx) {
210
+ const adapter = _getAdapter();
211
+ if (!adapter) {
212
+ ctx.ui.notify("No GSD database available.", "warning");
213
+ return;
214
+ }
215
+ try {
216
+ const activeRow = adapter
217
+ .prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL")
218
+ .get();
219
+ const supersededRow = adapter
220
+ .prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NOT NULL")
221
+ .get();
222
+ const byCategory = adapter
223
+ .prepare("SELECT category, count(*) as cnt FROM memories WHERE superseded_by IS NULL GROUP BY category ORDER BY cnt DESC")
224
+ .all();
225
+ const byScope = adapter
226
+ .prepare("SELECT scope, count(*) as cnt FROM memories WHERE superseded_by IS NULL GROUP BY scope ORDER BY cnt DESC")
227
+ .all();
228
+ const sourcesRow = adapter.prepare("SELECT count(*) as cnt FROM memory_sources").get();
229
+ const sourcesByKind = adapter
230
+ .prepare("SELECT kind, count(*) as cnt FROM memory_sources GROUP BY kind ORDER BY cnt DESC")
231
+ .all();
232
+ const relationsRow = adapter.prepare("SELECT count(*) as cnt FROM memory_relations").get();
233
+ const relationsByRel = adapter
234
+ .prepare("SELECT rel, count(*) as cnt FROM memory_relations GROUP BY rel ORDER BY cnt DESC")
235
+ .all();
236
+ const embeddingsRow = adapter.prepare("SELECT count(*) as cnt FROM memory_embeddings").get();
237
+ const embeddedActiveRow = adapter
238
+ .prepare(`SELECT count(*) as cnt FROM memory_embeddings e
239
+ JOIN memories m ON m.id = e.memory_id
240
+ WHERE m.superseded_by IS NULL`)
241
+ .get();
242
+ const activeCount = activeRow?.["cnt"] ?? 0;
243
+ const embeddedActive = embeddedActiveRow?.["cnt"] ?? 0;
244
+ const coverage = activeCount > 0 ? `${Math.round((embeddedActive / activeCount) * 100)}%` : "n/a";
245
+ const out = [
246
+ `Active memories: ${activeCount}`,
247
+ `Superseded: ${supersededRow?.["cnt"] ?? 0}`,
248
+ "",
249
+ "By category:",
250
+ ...byCategory.map((row) => ` ${row["category"]}: ${row["cnt"]}`),
251
+ "",
252
+ "By scope:",
253
+ ...byScope.map((row) => ` ${row["scope"]}: ${row["cnt"]}`),
254
+ "",
255
+ `Memory sources: ${sourcesRow?.["cnt"] ?? 0}`,
256
+ ...sourcesByKind.map((row) => ` ${row["kind"]}: ${row["cnt"]}`),
257
+ "",
258
+ `Relations: ${relationsRow?.["cnt"] ?? 0}`,
259
+ ...relationsByRel.map((row) => ` ${row["rel"]}: ${row["cnt"]}`),
260
+ "",
261
+ `Embeddings: ${embeddingsRow?.["cnt"] ?? 0} total, ${embeddedActive} active (coverage ${coverage})`,
262
+ ].join("\n");
263
+ ctx.ui.notify(out, "info");
264
+ }
265
+ catch (err) {
266
+ ctx.ui.notify(`Stats failed: ${err.message}`, "warning");
267
+ }
268
+ }
269
+ function handleExport(ctx, target) {
270
+ if (!target) {
271
+ ctx.ui.notify("Usage: /gsd memory export <path.json>", "warning");
272
+ return;
273
+ }
274
+ try {
275
+ const active = getActiveMemories();
276
+ const relations = active.flatMap((m) => listRelationsFor(m.id).filter((r) => r.from === m.id));
277
+ const sources = listMemorySources(500);
278
+ const payload = {
279
+ version: 1,
280
+ exported_at: new Date().toISOString(),
281
+ memories: active.map((m) => ({
282
+ id: m.id,
283
+ category: m.category,
284
+ content: m.content,
285
+ confidence: m.confidence,
286
+ hit_count: m.hit_count,
287
+ scope: m.scope,
288
+ tags: m.tags,
289
+ source_unit_type: m.source_unit_type,
290
+ source_unit_id: m.source_unit_id,
291
+ created_at: m.created_at,
292
+ updated_at: m.updated_at,
293
+ })),
294
+ relations: relations.map((r) => ({
295
+ from: r.from,
296
+ to: r.to,
297
+ rel: r.rel,
298
+ confidence: r.confidence,
299
+ })),
300
+ sources,
301
+ };
302
+ const abs = resolvePath(process.cwd(), target);
303
+ writeFileSync(abs, JSON.stringify(payload, null, 2), "utf-8");
304
+ ctx.ui.notify(`Exported ${payload.memories.length} memories, ${payload.relations.length} relations, ${payload.sources.length} sources → ${abs}`, "info");
305
+ }
306
+ catch (err) {
307
+ ctx.ui.notify(`Export failed: ${err.message}`, "error");
308
+ }
309
+ }
310
+ function handleImport(ctx, target) {
311
+ if (!target) {
312
+ ctx.ui.notify("Usage: /gsd memory import <path.json>", "warning");
313
+ return;
314
+ }
315
+ try {
316
+ const abs = resolvePath(process.cwd(), target);
317
+ const raw = readFileSync(abs, "utf-8");
318
+ const parsed = JSON.parse(raw);
319
+ let memoryCount = 0;
320
+ let relationCount = 0;
321
+ for (const mem of parsed.memories ?? []) {
322
+ if (!mem.category || !mem.content)
323
+ continue;
324
+ // createMemory allocates a fresh seq → new MEM### id; imports replay
325
+ // content rather than preserving the old ID. Relations from the export
326
+ // file still reference the old IDs, so only lossless round-trips into
327
+ // an empty DB preserve the graph.
328
+ const id = createMemory({
329
+ category: mem.category,
330
+ content: mem.content,
331
+ confidence: mem.confidence,
332
+ scope: mem.scope,
333
+ tags: mem.tags,
334
+ });
335
+ if (id)
336
+ memoryCount++;
337
+ }
338
+ for (const rel of parsed.relations ?? []) {
339
+ if (!rel.from || !rel.to || !rel.rel)
340
+ continue;
341
+ if (createMemoryRelation(rel.from, rel.to, rel.rel, rel.confidence)) {
342
+ relationCount++;
343
+ }
344
+ }
345
+ ctx.ui.notify(`Imported ${memoryCount} memories and ${relationCount} relations.`, "info");
346
+ }
347
+ catch (err) {
348
+ ctx.ui.notify(`Import failed: ${err.message}`, "error");
349
+ }
350
+ }
351
+ function handleDecay(ctx) {
352
+ const decayed = decayStaleMemories(20);
353
+ if (decayed.length === 0) {
354
+ ctx.ui.notify("Decay pass: no stale memories found.", "info");
355
+ return;
356
+ }
357
+ ctx.ui.notify(`Decayed ${decayed.length} stale memor${decayed.length === 1 ? "y" : "ies"}: ${decayed.join(", ")}`, "info");
358
+ }
359
+ function handleCap(ctx, arg) {
360
+ const max = arg ? Number.parseInt(arg, 10) : 50;
361
+ if (!Number.isFinite(max) || max < 1) {
362
+ ctx.ui.notify("Usage: /gsd memory cap <max> (default 50)", "warning");
363
+ return;
364
+ }
365
+ enforceMemoryCap(max);
366
+ ctx.ui.notify(`Enforced memory cap of ${max}.`, "info");
367
+ }
368
+ function handleSources(ctx) {
369
+ const sources = listMemorySources(30);
370
+ if (sources.length === 0) {
371
+ ctx.ui.notify("No memory sources yet. Use `/gsd memory ingest <path|url>` to add one.", "info");
372
+ return;
373
+ }
374
+ const lines = sources.map((s) => `- ${s.id} [${s.kind}${s.scope !== "project" ? `/${s.scope}` : ""}] ${truncate(s.title ?? s.uri ?? s.content, 100)}`);
375
+ ctx.ui.notify(lines.join("\n"), "info");
376
+ }
377
+ async function handleNote(ctx, args) {
378
+ const text = args.positional.join(" ").trim();
379
+ if (!text) {
380
+ ctx.ui.notify('Usage: /gsd memory note "your note"', "warning");
381
+ return;
382
+ }
383
+ try {
384
+ const result = await ingestNote(text, null, {
385
+ scope: args.scope,
386
+ tags: args.tags,
387
+ extract: false,
388
+ });
389
+ ctx.ui.notify(summarizeIngest(result), "info");
390
+ }
391
+ catch (err) {
392
+ ctx.ui.notify(`Note ingest failed: ${err.message}`, "error");
393
+ }
394
+ }
395
+ async function handleIngest(ctx, args) {
396
+ const target = args.positional[0];
397
+ if (!target) {
398
+ ctx.ui.notify("Usage: /gsd memory ingest <path|url> [--tag a,b] [--scope project|global]", "warning");
399
+ return;
400
+ }
401
+ try {
402
+ const isUrl = /^https?:\/\//i.test(target);
403
+ const result = isUrl
404
+ ? await ingestUrl(target, null, { scope: args.scope, tags: args.tags, extract: false })
405
+ : await ingestFile(target, null, { scope: args.scope, tags: args.tags, extract: false });
406
+ ctx.ui.notify(summarizeIngest(result), "info");
407
+ if (args.extract && result.sourceId) {
408
+ // TODO (P3): dispatch agent turn to extract memories once source is stored.
409
+ ctx.ui.notify(`(Dispatching extraction turn — use \`/gsd memory extract ${result.sourceId}\` to trigger manually.)`, "info");
410
+ }
411
+ }
412
+ catch (err) {
413
+ ctx.ui.notify(`Ingest failed: ${err.message}`, "error");
414
+ }
415
+ }
416
+ function handleExtractSource(ctx, pi, id) {
417
+ if (!id) {
418
+ ctx.ui.notify("Usage: /gsd memory extract <SRC-xxx>", "warning");
419
+ return;
420
+ }
421
+ const source = getMemorySource(id);
422
+ if (!source) {
423
+ ctx.ui.notify(`Source not found: ${id}`, "warning");
424
+ return;
425
+ }
426
+ const prompt = buildExtractPrompt(source);
427
+ ctx.ui.notify(`Dispatching extraction turn for ${id}...`, "info");
428
+ pi.sendMessage({ customType: "gsd-memory-extract", content: prompt, display: false }, { triggerTurn: true });
429
+ }
430
+ function buildExtractPrompt(source) {
431
+ const header = [
432
+ `## Memory extraction request`,
433
+ ``,
434
+ `Source: ${source.id} (${source.kind})`,
435
+ source.title ? `Title: ${source.title}` : null,
436
+ source.uri ? `URI: ${source.uri}` : null,
437
+ ]
438
+ .filter(Boolean)
439
+ .join("\n");
440
+ return [
441
+ header,
442
+ "",
443
+ "Read the content below and call the `capture_thought` tool once per durable insight",
444
+ "(architecture, convention, gotcha, preference, environment, pattern). Skip one-off details,",
445
+ "temporary state, and anything secret. Keep each memory to 1–3 sentences.",
446
+ "",
447
+ "---",
448
+ "",
449
+ source.content,
450
+ ].join("\n");
451
+ }
452
+ function safeJsonArray(raw) {
453
+ try {
454
+ const parsed = JSON.parse(raw);
455
+ return Array.isArray(parsed) ? parsed.filter((t) => typeof t === "string") : [];
456
+ }
457
+ catch {
458
+ return [];
459
+ }
460
+ }
461
+ // projectRoot is imported so tests can mock it via the same path as other commands.
462
+ export const _internals = { projectRoot };