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.
- package/dist/onboarding.d.ts +5 -1
- package/dist/onboarding.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-memory.js +462 -0
- package/dist/resources/extensions/gsd/gsd-db.js +237 -4
- package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
- package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
- package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
- package/dist/resources/extensions/gsd/memory-relations.js +189 -0
- package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
- package/dist/resources/extensions/gsd/memory-store.js +299 -6
- package/dist/resources/extensions/gsd/model-router.js +9 -5
- package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
- package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
- package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +12 -10
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +8 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +113 -14
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +40 -4
- package/packages/mcp-server/src/server.ts +12 -10
- package/packages/mcp-server/src/session-manager.ts +10 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
- package/packages/mcp-server/src/workflow-tools.ts +128 -18
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-memory.ts +551 -0
- package/src/resources/extensions/gsd/gsd-db.ts +273 -4
- package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
- package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
- package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
- package/src/resources/extensions/gsd/memory-relations.ts +240 -0
- package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
- package/src/resources/extensions/gsd/memory-store.ts +351 -7
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
- package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
- package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
- /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 };
|