milens 0.6.3 → 0.6.4

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 (137) hide show
  1. package/.agents/skills/adapters/SKILL.md +31 -0
  2. package/.agents/skills/analyzer/SKILL.md +55 -0
  3. package/.agents/skills/apps/SKILL.md +42 -0
  4. package/.agents/skills/docs/SKILL.md +46 -0
  5. package/.agents/skills/milens/SKILL.md +168 -0
  6. package/.agents/skills/milens-code-review/SKILL.md +186 -0
  7. package/.agents/skills/milens-eval/SKILL.md +221 -0
  8. package/.agents/skills/milens-plan/SKILL.md +227 -0
  9. package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
  10. package/.agents/skills/milens-security-review/SKILL.md +224 -0
  11. package/.agents/skills/milens-tdd/SKILL.md +156 -0
  12. package/.agents/skills/parser/SKILL.md +60 -0
  13. package/.agents/skills/root/SKILL.md +64 -0
  14. package/.agents/skills/scripts/SKILL.md +27 -0
  15. package/.agents/skills/security/SKILL.md +44 -0
  16. package/.agents/skills/server/SKILL.md +46 -0
  17. package/.agents/skills/store/SKILL.md +53 -0
  18. package/.agents/skills/test/SKILL.md +73 -0
  19. package/LICENSE +75 -75
  20. package/README.md +508 -432
  21. package/adapters/README.md +107 -0
  22. package/adapters/claude-code/.claude/mcp.json +9 -0
  23. package/adapters/claude-code/CLAUDE.md +58 -0
  24. package/adapters/codex/.codex/codex.md +52 -0
  25. package/adapters/copilot/.github/copilot-instructions.md +62 -0
  26. package/adapters/cursor/.cursorrules +9 -0
  27. package/adapters/gemini/.gemini/context.md +58 -0
  28. package/adapters/opencode/.opencode/config.json +9 -0
  29. package/adapters/opencode/AGENTS.md +58 -0
  30. package/adapters/zed/.zed/settings.json +8 -0
  31. package/dist/agents-md.d.ts +3 -0
  32. package/dist/agents-md.d.ts.map +1 -0
  33. package/dist/agents-md.js +112 -0
  34. package/dist/agents-md.js.map +1 -0
  35. package/dist/analyzer/engine.js +1 -1
  36. package/dist/analyzer/engine.js.map +1 -1
  37. package/dist/cli.js +1190 -401
  38. package/dist/cli.js.map +1 -1
  39. package/dist/metrics.d.ts +51 -0
  40. package/dist/metrics.d.ts.map +1 -0
  41. package/dist/metrics.js +64 -0
  42. package/dist/metrics.js.map +1 -0
  43. package/dist/parser/lang-go.js +47 -47
  44. package/dist/parser/lang-java.js +29 -29
  45. package/dist/parser/lang-js.js +105 -105
  46. package/dist/parser/lang-php.js +38 -38
  47. package/dist/parser/lang-py.js +34 -34
  48. package/dist/parser/lang-ruby.js +14 -14
  49. package/dist/parser/lang-rust.js +30 -30
  50. package/dist/parser/lang-ts.js +191 -191
  51. package/dist/security/deps.d.ts +38 -0
  52. package/dist/security/deps.d.ts.map +1 -0
  53. package/dist/security/deps.js +685 -0
  54. package/dist/security/deps.js.map +1 -0
  55. package/dist/security/rules.d.ts +42 -0
  56. package/dist/security/rules.d.ts.map +1 -0
  57. package/dist/security/rules.js +940 -0
  58. package/dist/security/rules.js.map +1 -0
  59. package/dist/server/hooks.d.ts +26 -0
  60. package/dist/server/hooks.d.ts.map +1 -0
  61. package/dist/server/hooks.js +253 -0
  62. package/dist/server/hooks.js.map +1 -0
  63. package/dist/server/mcp-prompts.d.ts +277 -0
  64. package/dist/server/mcp-prompts.d.ts.map +1 -0
  65. package/dist/server/mcp-prompts.js +627 -0
  66. package/dist/server/mcp-prompts.js.map +1 -0
  67. package/dist/server/mcp.d.ts.map +1 -1
  68. package/dist/server/mcp.js +618 -643
  69. package/dist/server/mcp.js.map +1 -1
  70. package/dist/server/test-plan.d.ts +20 -0
  71. package/dist/server/test-plan.d.ts.map +1 -0
  72. package/dist/server/test-plan.js +100 -0
  73. package/dist/server/test-plan.js.map +1 -0
  74. package/dist/skills.js +152 -152
  75. package/dist/store/annotations.d.ts +41 -0
  76. package/dist/store/annotations.d.ts.map +1 -0
  77. package/dist/store/annotations.js +192 -0
  78. package/dist/store/annotations.js.map +1 -0
  79. package/dist/store/confidence.d.ts +18 -0
  80. package/dist/store/confidence.d.ts.map +1 -0
  81. package/dist/store/confidence.js +82 -0
  82. package/dist/store/confidence.js.map +1 -0
  83. package/dist/store/db.d.ts +37 -14
  84. package/dist/store/db.d.ts.map +1 -1
  85. package/dist/store/db.js +332 -239
  86. package/dist/store/db.js.map +1 -1
  87. package/dist/store/schema.sql +128 -116
  88. package/dist/store/vectors.js +2 -2
  89. package/dist/types.d.ts +101 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/docs/README.md +24 -0
  92. package/package.json +80 -66
  93. package/dist/gateway/analyzer.d.ts +0 -6
  94. package/dist/gateway/analyzer.d.ts.map +0 -1
  95. package/dist/gateway/analyzer.js +0 -218
  96. package/dist/gateway/analyzer.js.map +0 -1
  97. package/dist/gateway/cache.d.ts +0 -35
  98. package/dist/gateway/cache.d.ts.map +0 -1
  99. package/dist/gateway/cache.js +0 -175
  100. package/dist/gateway/cache.js.map +0 -1
  101. package/dist/gateway/config.d.ts +0 -10
  102. package/dist/gateway/config.d.ts.map +0 -1
  103. package/dist/gateway/config.js +0 -167
  104. package/dist/gateway/config.js.map +0 -1
  105. package/dist/gateway/context-memory.d.ts +0 -68
  106. package/dist/gateway/context-memory.d.ts.map +0 -1
  107. package/dist/gateway/context-memory.js +0 -157
  108. package/dist/gateway/context-memory.js.map +0 -1
  109. package/dist/gateway/observability.d.ts +0 -83
  110. package/dist/gateway/observability.d.ts.map +0 -1
  111. package/dist/gateway/observability.js +0 -152
  112. package/dist/gateway/observability.js.map +0 -1
  113. package/dist/gateway/privacy.d.ts +0 -27
  114. package/dist/gateway/privacy.d.ts.map +0 -1
  115. package/dist/gateway/privacy.js +0 -139
  116. package/dist/gateway/privacy.js.map +0 -1
  117. package/dist/gateway/providers.d.ts +0 -66
  118. package/dist/gateway/providers.d.ts.map +0 -1
  119. package/dist/gateway/providers.js +0 -377
  120. package/dist/gateway/providers.js.map +0 -1
  121. package/dist/gateway/router.d.ts +0 -18
  122. package/dist/gateway/router.d.ts.map +0 -1
  123. package/dist/gateway/router.js +0 -102
  124. package/dist/gateway/router.js.map +0 -1
  125. package/dist/gateway/server.d.ts +0 -20
  126. package/dist/gateway/server.d.ts.map +0 -1
  127. package/dist/gateway/server.js +0 -387
  128. package/dist/gateway/server.js.map +0 -1
  129. package/dist/gateway/translator.d.ts +0 -19
  130. package/dist/gateway/translator.d.ts.map +0 -1
  131. package/dist/gateway/translator.js +0 -340
  132. package/dist/gateway/translator.js.map +0 -1
  133. package/dist/gateway/types.d.ts +0 -215
  134. package/dist/gateway/types.d.ts.map +0 -1
  135. package/dist/gateway/types.js +0 -3
  136. package/dist/gateway/types.js.map +0 -1
  137. package/dist/store/gateway-schema.sql +0 -53
package/dist/store/db.js CHANGED
@@ -7,6 +7,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  export class Database {
8
8
  db;
9
9
  stmts;
10
+ /** Access the raw better-sqlite3 connection (for AnnotationStore etc.) */
11
+ get connection() {
12
+ return this.db;
13
+ }
10
14
  constructor(dbPath) {
11
15
  this.db = new BetterSqlite3(dbPath);
12
16
  this.db.pragma('journal_mode = WAL');
@@ -21,48 +25,62 @@ export class Database {
21
25
  prepareStatements() {
22
26
  return {
23
27
  checkHash: this.db.prepare('SELECT hash FROM file_hashes WHERE path = ?'),
24
- upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
28
+ upsertHash: this.db.prepare(`INSERT INTO file_hashes (path, hash) VALUES (?, ?)
25
29
  ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, analyzed_at = datetime('now')`),
26
- insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
30
+ insertSym: this.db.prepare(`INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, parent_id, signature, role, heat)
27
31
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
28
32
  updateMeta: this.db.prepare(`UPDATE symbols SET role = ?, heat = ? WHERE id = ?`),
29
33
  upsertZone: this.db.prepare(`UPDATE file_hashes SET zone = ? WHERE path = ?`),
30
- insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
34
+ insertLink: this.db.prepare(`INSERT OR REPLACE INTO links (id, from_id, to_id, type, confidence, line_number)
31
35
  VALUES (?, ?, ?, ?, ?, ?)`),
32
- searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
33
- JOIN symbols s ON s.rowid = f.rowid
34
- WHERE symbol_fts MATCH ?
36
+ searchFts: this.db.prepare(`SELECT s.* FROM symbol_fts f
37
+ JOIN symbols s ON s.rowid = f.rowid
38
+ WHERE symbol_fts MATCH ?
35
39
  ORDER BY rank LIMIT ?`),
36
40
  byName: this.db.prepare('SELECT * FROM symbols WHERE name = ?'),
37
41
  byId: this.db.prepare('SELECT * FROM symbols WHERE id = ?'),
38
42
  byFile: this.db.prepare('SELECT * FROM symbols WHERE file_path = ?'),
39
43
  linksIn: this.db.prepare('SELECT * FROM links WHERE to_id = ?'),
40
44
  linksOut: this.db.prepare('SELECT * FROM links WHERE from_id = ?'),
41
- upstream: this.db.prepare(`
42
- WITH RECURSIVE upstream(id, depth, via) AS (
43
- SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
44
- UNION
45
- SELECT l.from_id, u.depth + 1, l.type
46
- FROM links l JOIN upstream u ON l.to_id = u.id
47
- WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
48
- )
49
- SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
45
+ upstream: this.db.prepare(`
46
+ WITH RECURSIVE upstream(id, depth, via) AS (
47
+ SELECT from_id, 1, type FROM links WHERE to_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
48
+ UNION
49
+ SELECT l.from_id, u.depth + 1, l.type
50
+ FROM links l JOIN upstream u ON l.to_id = u.id
51
+ WHERE u.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
52
+ )
53
+ SELECT DISTINCT s.*, u.depth, u.via FROM upstream u JOIN symbols s ON s.id = u.id ORDER BY u.depth
50
54
  `),
51
- downstream: this.db.prepare(`
52
- WITH RECURSIVE downstream(id, depth, via) AS (
53
- SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
54
- UNION
55
- SELECT l.to_id, d.depth + 1, l.type
56
- FROM links l JOIN downstream d ON l.from_id = d.id
57
- WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
58
- )
59
- SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
55
+ downstream: this.db.prepare(`
56
+ WITH RECURSIVE downstream(id, depth, via) AS (
57
+ SELECT to_id, 1, type FROM links WHERE from_id = ? AND type IN ('calls', 'imports', 'extends', 'implements')
58
+ UNION
59
+ SELECT l.to_id, d.depth + 1, l.type
60
+ FROM links l JOIN downstream d ON l.from_id = d.id
61
+ WHERE d.depth < ? AND l.type IN ('calls', 'imports', 'extends', 'implements')
62
+ )
63
+ SELECT DISTINCT s.*, d.depth, d.via FROM downstream d JOIN symbols s ON s.id = d.id ORDER BY d.depth
60
64
  `),
61
65
  countSymbols: this.db.prepare('SELECT COUNT(*) as c FROM symbols'),
62
66
  countLinks: this.db.prepare('SELECT COUNT(*) as c FROM links'),
63
67
  countFiles: this.db.prepare('SELECT COUNT(*) as c FROM file_hashes'),
64
- deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?) OR to_id IN (SELECT id FROM symbols WHERE file_path = ?)'),
68
+ deleteFileLinks: this.db.prepare('DELETE FROM links WHERE from_id IN (SELECT id FROM symbols WHERE file_path = ?)'),
65
69
  deleteFileSymbols: this.db.prepare('DELETE FROM symbols WHERE file_path = ?'),
70
+ topHubs: this.db.prepare('SELECT * FROM symbols WHERE exported = 1 ORDER BY heat DESC LIMIT ?'),
71
+ testCoverageGaps: this.db.prepare(`
72
+ SELECT s.* FROM symbols s
73
+ WHERE s.exported = 1 AND s.heat > 0
74
+ AND s.id NOT IN (
75
+ SELECT DISTINCT l.to_id FROM links l
76
+ JOIN symbols src ON src.id = l.from_id
77
+ WHERE (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
78
+ OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
79
+ )
80
+ ORDER BY s.heat DESC
81
+ LIMIT ?
82
+ `),
83
+ annotationCount: this.db.prepare('SELECT COUNT(*) as c FROM annotations'),
66
84
  };
67
85
  }
68
86
  applySchema() {
@@ -153,35 +171,6 @@ export class Database {
153
171
  const rows = this.stmts.linksOut.all(symbolId);
154
172
  return rows.map(rowToLink);
155
173
  }
156
- /** Batch link counts for multiple symbol IDs (avoids N+1 queries) */
157
- getLinkCountsForSymbols(symbolIds) {
158
- if (symbolIds.length === 0)
159
- return new Map();
160
- const placeholders = symbolIds.map(() => '?').join(',');
161
- const inRows = this.db.prepare(`SELECT to_id as id, COUNT(*) as c FROM links WHERE to_id IN (${placeholders}) AND type != 'contains' GROUP BY to_id`).all(...symbolIds);
162
- const outRows = this.db.prepare(`SELECT from_id as id, COUNT(*) as c FROM links WHERE from_id IN (${placeholders}) AND type != 'contains' GROUP BY from_id`).all(...symbolIds);
163
- const result = new Map();
164
- for (const id of symbolIds)
165
- result.set(id, { incoming: 0, outgoing: 0 });
166
- for (const r of inRows)
167
- result.get(r.id).incoming = r.c;
168
- for (const r of outRows)
169
- result.get(r.id).outgoing = r.c;
170
- return result;
171
- }
172
- /** Batch: get symbol IDs that have at least one incoming link from a test file. */
173
- getTestedSymbolIds(symbolIds, isTestFile) {
174
- if (symbolIds.length === 0)
175
- return new Set();
176
- const placeholders = symbolIds.map(() => '?').join(',');
177
- const rows = this.db.prepare(`SELECT DISTINCT l.to_id, s.file_path FROM links l JOIN symbols s ON s.id = l.from_id WHERE l.to_id IN (${placeholders})`).all(...symbolIds);
178
- const tested = new Set();
179
- for (const r of rows) {
180
- if (isTestFile(r.file_path))
181
- tested.add(r.to_id);
182
- }
183
- return tested;
184
- }
185
174
  findUpstream(symbolId, maxDepth = 3) {
186
175
  const rows = this.stmts.upstream.all(symbolId, maxDepth);
187
176
  return rows.map(r => ({ symbol: rowToSymbol(r), depth: r.depth, via: r.via }));
@@ -200,47 +189,25 @@ export class Database {
200
189
  const rows = this.db.prepare('SELECT * FROM symbols').all();
201
190
  return rows.map(rowToSymbol);
202
191
  }
203
- getTopSymbolsByHeat(limit = 10) {
204
- const rows = this.db.prepare(`SELECT * FROM symbols WHERE heat > 0 AND kind != 'section' ORDER BY heat DESC LIMIT ?`).all(limit);
205
- return rows.map(rowToSymbol);
206
- }
207
192
  getAllLinks() {
208
193
  const rows = this.db.prepare('SELECT * FROM links').all();
209
194
  return rows.map(rowToLink);
210
195
  }
211
196
  findDeadCode(kind, limit = 50) {
212
- // Exclude: section symbols (markdown headings), test fixtures (not real production code),
213
- // framework entry-point files (consumed by runtime, not imported by project code),
214
- // and config files (consumed by CLI tooling)
215
- const excludeClause = `AND s.kind != 'section'
216
- AND s.file_path NOT LIKE 'test/fixtures/%'
217
- AND s.file_path NOT LIKE '%/page.ts' AND s.file_path NOT LIKE '%/page.tsx'
218
- AND s.file_path NOT LIKE '%/page.js' AND s.file_path NOT LIKE '%/page.jsx'
219
- AND s.file_path NOT LIKE '%/layout.ts' AND s.file_path NOT LIKE '%/layout.tsx'
220
- AND s.file_path NOT LIKE '%/layout.js' AND s.file_path NOT LIKE '%/layout.jsx'
221
- AND s.file_path NOT LIKE '%/loading.ts' AND s.file_path NOT LIKE '%/loading.tsx'
222
- AND s.file_path NOT LIKE '%/error.ts' AND s.file_path NOT LIKE '%/error.tsx'
223
- AND s.file_path NOT LIKE '%/not-found.ts' AND s.file_path NOT LIKE '%/not-found.tsx'
224
- AND s.file_path NOT LIKE '%/template.ts' AND s.file_path NOT LIKE '%/template.tsx'
225
- AND s.file_path NOT LIKE '%/route.ts' AND s.file_path NOT LIKE '%/route.tsx'
226
- AND s.file_path NOT LIKE '%/route.js' AND s.file_path NOT LIKE '%/route.jsx'
227
- AND s.file_path NOT LIKE '%/default.ts' AND s.file_path NOT LIKE '%/default.tsx'
228
- AND s.file_path NOT LIKE '%/global-error.ts' AND s.file_path NOT LIKE '%/global-error.tsx'
229
- AND s.file_path NOT LIKE '%/middleware.ts' AND s.file_path NOT LIKE '%/middleware.js'
230
- AND s.file_path NOT LIKE '%.config.ts' AND s.file_path NOT LIKE '%.config.js'
231
- AND s.file_path NOT LIKE '%.config.mjs' AND s.file_path NOT LIKE '%.config.cjs'
232
- AND s.file_path NOT LIKE '%/+page.svelte' AND s.file_path NOT LIKE '%/+page.ts'
233
- AND s.file_path NOT LIKE '%/+page.server.ts' AND s.file_path NOT LIKE '%/+layout.svelte'
234
- AND s.file_path NOT LIKE '%/+layout.ts' AND s.file_path NOT LIKE '%/+layout.server.ts'
235
- AND s.file_path NOT LIKE '%/+server.ts'`;
197
+ const frameworkExclude = `AND s.file_path NOT LIKE 'app/%/page.%' AND s.file_path NOT LIKE 'app/%/layout.%'
198
+ AND s.file_path NOT LIKE 'app/page.%' AND s.file_path NOT LIKE 'app/layout.%'
199
+ AND s.file_path NOT LIKE 'app/api/%/route.%' AND s.file_path NOT LIKE 'jest.config.%'
200
+ AND s.file_path NOT LIKE 'src/routes/+page.%' AND s.file_path NOT LIKE 'src/routes/+layout.%'`;
236
201
  const sql = kind
237
- ? `SELECT s.* FROM symbols s
238
- LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
239
- WHERE s.exported = 1 AND s.kind = ? ${excludeClause} AND l.id IS NULL
202
+ ? `SELECT s.* FROM symbols s
203
+ LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
204
+ WHERE s.exported = 1 AND s.kind = ? AND l.id IS NULL
205
+ ${frameworkExclude}
240
206
  LIMIT ?`
241
- : `SELECT s.* FROM symbols s
242
- LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
243
- WHERE s.exported = 1 ${excludeClause} AND l.id IS NULL
207
+ : `SELECT s.* FROM symbols s
208
+ LEFT JOIN links l ON l.to_id = s.id AND l.type != 'contains'
209
+ WHERE s.exported = 1 AND l.id IS NULL
210
+ ${frameworkExclude}
244
211
  LIMIT ?`;
245
212
  const rows = kind
246
213
  ? this.db.prepare(sql).all(kind, limit)
@@ -248,25 +215,25 @@ export class Database {
248
215
  return rows.map(rowToSymbol);
249
216
  }
250
217
  getTypeHierarchy(symbolId) {
251
- const ancestors = this.db.prepare(`
252
- WITH RECURSIVE up(id, depth) AS (
253
- SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
254
- UNION
255
- SELECT l.to_id, u.depth + 1
256
- FROM links l JOIN up u ON l.from_id = u.id
257
- WHERE l.type IN ('extends', 'implements') AND u.depth < 10
258
- )
259
- SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
218
+ const ancestors = this.db.prepare(`
219
+ WITH RECURSIVE up(id, depth) AS (
220
+ SELECT to_id, 1 FROM links WHERE from_id = ? AND type IN ('extends', 'implements')
221
+ UNION
222
+ SELECT l.to_id, u.depth + 1
223
+ FROM links l JOIN up u ON l.from_id = u.id
224
+ WHERE l.type IN ('extends', 'implements') AND u.depth < 10
225
+ )
226
+ SELECT DISTINCT s.*, u.depth FROM up u JOIN symbols s ON s.id = u.id ORDER BY u.depth
260
227
  `).all(symbolId);
261
- const descendants = this.db.prepare(`
262
- WITH RECURSIVE down(id, depth) AS (
263
- SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
264
- UNION
265
- SELECT l.from_id, d.depth + 1
266
- FROM links l JOIN down d ON l.to_id = d.id
267
- WHERE l.type IN ('extends', 'implements') AND d.depth < 10
268
- )
269
- SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
228
+ const descendants = this.db.prepare(`
229
+ WITH RECURSIVE down(id, depth) AS (
230
+ SELECT from_id, 1 FROM links WHERE to_id = ? AND type IN ('extends', 'implements')
231
+ UNION
232
+ SELECT l.from_id, d.depth + 1
233
+ FROM links l JOIN down d ON l.to_id = d.id
234
+ WHERE l.type IN ('extends', 'implements') AND d.depth < 10
235
+ )
236
+ SELECT DISTINCT s.*, d.depth FROM down d JOIN symbols s ON s.id = d.id ORDER BY d.depth
270
237
  `).all(symbolId);
271
238
  return {
272
239
  ancestors: ancestors.map(r => ({ symbol: rowToSymbol(r), depth: r.depth })),
@@ -280,40 +247,27 @@ export class Database {
280
247
  return null;
281
248
  const fromId = fromSyms[0].id;
282
249
  const toIds = new Set(toSyms.map(s => s.id));
283
- // BFS with parent tracking to reconstruct shortest path
284
- const rows = this.db.prepare(`
285
- WITH RECURSIVE path(id, depth, via, parent_id) AS (
286
- SELECT to_id, 1, type, from_id FROM links WHERE from_id = ? AND type != 'contains'
287
- UNION
288
- SELECT l.to_id, p.depth + 1, l.type, l.from_id
289
- FROM links l JOIN path p ON l.from_id = p.id
290
- WHERE l.type != 'contains' AND p.depth < ?
291
- )
292
- SELECT s.*, p.depth, p.via, p.parent_id FROM path p JOIN symbols s ON s.id = p.id ORDER BY p.depth
250
+ // BFS outgoing from source
251
+ const rows = this.db.prepare(`
252
+ WITH RECURSIVE path(id, depth, via) AS (
253
+ SELECT to_id, 1, type FROM links WHERE from_id = ? AND type != 'contains'
254
+ UNION
255
+ SELECT l.to_id, p.depth + 1, l.type
256
+ FROM links l JOIN path p ON l.from_id = p.id
257
+ WHERE l.type != 'contains' AND p.depth < ?
258
+ )
259
+ SELECT DISTINCT s.*, p.depth, p.via FROM path p JOIN symbols s ON s.id = p.id ORDER BY p.depth
293
260
  `).all(fromId, maxDepth);
294
- // Find first row matching target
295
- const targetRow = rows.find(r => toIds.has(r.id));
296
- if (!targetRow)
297
- return null;
298
- // Build lookup: id → { row, parent_id }
299
- const byId = new Map();
261
+ const result = [];
300
262
  for (const r of rows) {
301
- if (!byId.has(r.id)) {
302
- byId.set(r.id, { row: r, parentId: r.parent_id });
303
- }
263
+ result.push({ symbol: rowToSymbol(r), depth: r.depth, via: r.via });
264
+ if (toIds.has(r.id))
265
+ break;
304
266
  }
305
- // Walk backwards from target to source to reconstruct the actual path
306
- const chain = [];
307
- const visited = new Set();
308
- let cur = targetRow;
309
- while (cur && cur.id !== fromId && !visited.has(cur.id)) {
310
- visited.add(cur.id);
311
- chain.push({ symbol: rowToSymbol(cur), depth: cur.depth, via: cur.via });
312
- const parent = byId.get(cur.parent_id);
313
- cur = parent ? parent.row : null;
314
- }
315
- chain.reverse();
316
- return chain;
267
+ const found = result.find(r => toIds.has(r.symbol.id));
268
+ if (!found)
269
+ return null;
270
+ return result.filter(r => r.depth <= found.depth);
317
271
  }
318
272
  getChangedFiles() {
319
273
  const rows = this.db.prepare('SELECT DISTINCT file_path FROM symbols').all();
@@ -321,7 +275,7 @@ export class Database {
321
275
  }
322
276
  // ── Maintenance ──
323
277
  deleteFileData(filePath) {
324
- this.stmts.deleteFileLinks.run(filePath, filePath);
278
+ this.stmts.deleteFileLinks.run(filePath);
325
279
  this.stmts.deleteFileSymbols.run(filePath);
326
280
  }
327
281
  rebuildSearch() {
@@ -355,14 +309,14 @@ export class Database {
355
309
  };
356
310
  }
357
311
  getConfidenceDistribution() {
358
- const rows = this.db.prepare(`
359
- SELECT
360
- SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
361
- SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
362
- SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
363
- COUNT(*) as total
364
- FROM links
365
- WHERE type != 'contains'
312
+ const rows = this.db.prepare(`
313
+ SELECT
314
+ SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
315
+ SUM(CASE WHEN confidence >= 0.7 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
316
+ SUM(CASE WHEN confidence < 0.7 THEN 1 ELSE 0 END) as low,
317
+ COUNT(*) as total
318
+ FROM links
319
+ WHERE type != 'contains'
366
320
  `).get();
367
321
  return {
368
322
  high: rows?.high ?? 0,
@@ -376,14 +330,6 @@ export class Database {
376
330
  // Walk upstream following only 'calls' links to find paths from entrypoints
377
331
  const paths = [];
378
332
  const visited = new Set();
379
- const symCache = new Map();
380
- const cachedFindSymbol = (id) => {
381
- if (symCache.has(id))
382
- return symCache.get(id);
383
- const sym = this.findSymbolById(id);
384
- symCache.set(id, sym);
385
- return sym;
386
- };
387
333
  const dfs = (currentId, currentPath, depth) => {
388
334
  if (depth > maxDepth)
389
335
  return;
@@ -391,7 +337,7 @@ export class Database {
391
337
  return;
392
338
  visited.add(currentId);
393
339
  const incoming = this.getIncomingLinks(currentId).filter(l => l.type === 'calls' || l.type === 'imports');
394
- const sym = cachedFindSymbol(currentId);
340
+ const sym = this.findSymbolById(currentId);
395
341
  if (incoming.length === 0 && sym?.exported) {
396
342
  // Reached an entrypoint — save this path
397
343
  paths.push({ path: [...currentPath] });
@@ -399,10 +345,17 @@ export class Database {
399
345
  return;
400
346
  }
401
347
  for (const link of incoming) {
402
- const fromSym = cachedFindSymbol(link.fromId);
348
+ const fromSym = this.findSymbolById(link.fromId);
403
349
  if (!fromSym)
404
350
  continue;
405
- dfs(link.fromId, [{ symbol: fromSym, via: link.type }, ...currentPath], depth + 1);
351
+ // Skip module-level _top imports go to their real callers
352
+ if (fromSym.name === '_top' && fromSym.kind === 'module') {
353
+ // Recurse from the _top module's incoming callers
354
+ dfs(link.fromId, [{ symbol: fromSym, via: link.type }, ...currentPath], depth + 1);
355
+ }
356
+ else {
357
+ dfs(link.fromId, [{ symbol: fromSym, via: link.type }, ...currentPath], depth + 1);
358
+ }
406
359
  }
407
360
  visited.delete(currentId);
408
361
  };
@@ -416,34 +369,34 @@ export class Database {
416
369
  // ── Route/endpoint detection via link patterns ──
417
370
  getEntrypoints() {
418
371
  // Symbols with role='entrypoint' OR exported + 0 incoming non-contains links
419
- const rows = this.db.prepare(`
420
- SELECT s.* FROM symbols s
421
- WHERE s.exported = 1
422
- AND s.role = 'entrypoint'
423
- ORDER BY s.heat DESC
424
- LIMIT 50
372
+ const rows = this.db.prepare(`
373
+ SELECT s.* FROM symbols s
374
+ WHERE s.exported = 1
375
+ AND s.role = 'entrypoint'
376
+ ORDER BY s.heat DESC
377
+ LIMIT 50
425
378
  `).all();
426
379
  return rows.map(rowToSymbol);
427
380
  }
428
381
  // ── Domain clustering stats ──
429
382
  getDomainStats() {
430
- const rows = this.db.prepare(`
431
- SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
432
- COUNT(s.id) AS symbol_count
433
- FROM file_hashes fh
434
- LEFT JOIN symbols s ON s.file_path = fh.path
435
- WHERE fh.zone IS NOT NULL
436
- GROUP BY fh.zone
437
- ORDER BY symbol_count DESC
383
+ const rows = this.db.prepare(`
384
+ SELECT fh.zone AS domain, COUNT(DISTINCT fh.path) AS file_count,
385
+ COUNT(s.id) AS symbol_count
386
+ FROM file_hashes fh
387
+ LEFT JOIN symbols s ON s.file_path = fh.path
388
+ WHERE fh.zone IS NOT NULL
389
+ GROUP BY fh.zone
390
+ ORDER BY symbol_count DESC
438
391
  `).all();
439
392
  return rows.map((r) => ({ domain: r.domain, files: r.file_count, symbols: r.symbol_count }));
440
393
  }
441
394
  // ── Staleness detection ──
442
395
  getStaleFiles(hoursOld = 24) {
443
- const rows = this.db.prepare(`
444
- SELECT path FROM file_hashes
445
- WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
446
- ORDER BY analyzed_at ASC
396
+ const rows = this.db.prepare(`
397
+ SELECT path FROM file_hashes
398
+ WHERE analyzed_at < datetime('now', '-' || ? || ' hours')
399
+ ORDER BY analyzed_at ASC
447
400
  `).all(hoursOld);
448
401
  return rows.map((r) => r.path);
449
402
  }
@@ -466,40 +419,46 @@ export class Database {
466
419
  }
467
420
  // ── Tool usage tracking ──
468
421
  logToolUsage(tool, durationMs, tokensOut, tokensSaved, repo) {
469
- this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
422
+ this.db.prepare(`INSERT INTO tool_usage (tool, duration_ms, tokens_out, tokens_saved, repo)
470
423
  VALUES (?, ?, ?, ?, ?)`).run(tool, durationMs, tokensOut, tokensSaved, repo ?? null);
471
424
  }
472
- getToolUsageStats() {
473
- const totals = this.db.prepare(`
474
- SELECT COUNT(*) as total_calls,
475
- COALESCE(SUM(tokens_saved), 0) as total_saved,
476
- COALESCE(SUM(tokens_out), 0) as total_out,
477
- COALESCE(SUM(duration_ms), 0) as total_ms
478
- FROM tool_usage
479
- `).get();
480
- const byTool = this.db.prepare(`
481
- SELECT tool, COUNT(*) as calls,
482
- COALESCE(SUM(tokens_saved), 0) as tokens_saved,
483
- COALESCE(SUM(tokens_out), 0) as tokens_out,
484
- CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
485
- FROM tool_usage
486
- GROUP BY tool
487
- ORDER BY calls DESC
488
- `).all();
489
- const byDay = this.db.prepare(`
490
- SELECT date(called_at) as date, COUNT(*) as calls,
491
- COALESCE(SUM(tokens_saved), 0) as tokens_saved
492
- FROM tool_usage
493
- GROUP BY date(called_at)
494
- ORDER BY date DESC
495
- LIMIT 30
496
- `).all();
497
- const recentCalls = this.db.prepare(`
498
- SELECT tool, called_at, duration_ms, tokens_saved
499
- FROM tool_usage
500
- ORDER BY id DESC
501
- LIMIT 50
502
- `).all();
425
+ getToolUsageStats(repo) {
426
+ const repoFilter = repo ? `WHERE repo = ?` : '';
427
+ const repoParam = repo ? [repo] : [];
428
+ const totals = this.db.prepare(`
429
+ SELECT COUNT(*) as total_calls,
430
+ COALESCE(SUM(tokens_saved), 0) as total_saved,
431
+ COALESCE(SUM(tokens_out), 0) as total_out,
432
+ COALESCE(SUM(duration_ms), 0) as total_ms
433
+ FROM tool_usage
434
+ ${repoFilter}
435
+ `).get(...repoParam);
436
+ const byTool = this.db.prepare(`
437
+ SELECT tool, COUNT(*) as calls,
438
+ COALESCE(SUM(tokens_saved), 0) as tokens_saved,
439
+ COALESCE(SUM(tokens_out), 0) as tokens_out,
440
+ CAST(COALESCE(AVG(duration_ms), 0) AS INTEGER) as avg_ms
441
+ FROM tool_usage
442
+ ${repoFilter}
443
+ GROUP BY tool
444
+ ORDER BY calls DESC
445
+ `).all(...repoParam);
446
+ const byDay = this.db.prepare(`
447
+ SELECT date(called_at) as date, COUNT(*) as calls,
448
+ COALESCE(SUM(tokens_saved), 0) as tokens_saved
449
+ FROM tool_usage
450
+ ${repoFilter}
451
+ GROUP BY date(called_at)
452
+ ORDER BY date DESC
453
+ LIMIT 30
454
+ `).all(...repoParam);
455
+ const recentCalls = this.db.prepare(`
456
+ SELECT tool, called_at, duration_ms, tokens_saved
457
+ FROM tool_usage
458
+ ${repoFilter}
459
+ ORDER BY id DESC
460
+ LIMIT 50
461
+ `).all(...repoParam);
503
462
  return {
504
463
  totalCalls: totals.total_calls,
505
464
  totalTokensSaved: totals.total_saved,
@@ -515,16 +474,168 @@ export class Database {
515
474
  })),
516
475
  };
517
476
  }
518
- // ── Annotations (agent code memory) ──
477
+ // ── Test file detection ──
478
+ isTestFile(filePath) {
479
+ return /[/\\]test[/\\]/.test(filePath) || /\.(test|spec)\./.test(filePath);
480
+ }
481
+ // ── Heat / hubs ──
482
+ getTopHubs(limit) {
483
+ const rows = this.stmts.topHubs.all(limit);
484
+ return rows.map(rowToSymbol);
485
+ }
486
+ // ── Test coverage ──
487
+ getSymbolTestCoverage(symbolId) {
488
+ const incomingLinks = this.getIncomingLinks(symbolId);
489
+ for (const link of incomingLinks) {
490
+ const sourceSymbol = this.findSymbolById(link.fromId);
491
+ if (sourceSymbol && this.isTestFile(sourceSymbol.filePath)) {
492
+ return true;
493
+ }
494
+ }
495
+ return false;
496
+ }
497
+ getTestedSymbolIds(candidateIds, isTestFileFn) {
498
+ if (candidateIds.length === 0)
499
+ return new Set();
500
+ const placeholders = candidateIds.map(() => '?').join(',');
501
+ const rows = this.db.prepare(`
502
+ SELECT DISTINCT l.to_id
503
+ FROM links l JOIN symbols src ON src.id = l.from_id
504
+ WHERE l.to_id IN (${placeholders}) AND l.type != 'contains'
505
+ `).all(...candidateIds);
506
+ const testedIds = new Set();
507
+ for (const row of rows) {
508
+ const src = this.findSymbolById(row.to_id);
509
+ if (src && isTestFileFn(src.filePath))
510
+ continue;
511
+ // Check each from_id for test files
512
+ const links = this.getIncomingLinks(row.to_id);
513
+ for (const l of links) {
514
+ if (l.type === 'contains')
515
+ continue;
516
+ const from = this.findSymbolById(l.fromId);
517
+ if (from && isTestFileFn(from.filePath)) {
518
+ testedIds.add(row.to_id);
519
+ break;
520
+ }
521
+ }
522
+ }
523
+ // Also include ids not in the result set (no incoming links)
524
+ const inResult = new Set(rows.map(r => r.to_id));
525
+ for (const id of candidateIds) {
526
+ if (!inResult.has(id))
527
+ continue; // doesn't exist
528
+ }
529
+ return testedIds;
530
+ }
531
+ getTestCoverageGaps(limit) {
532
+ const rows = this.stmts.testCoverageGaps.all(limit);
533
+ return rows.map(rowToSymbol);
534
+ }
535
+ getTestImpact(changedSymbolIds) {
536
+ if (changedSymbolIds.length === 0)
537
+ return { testFiles: [], changedSymbols: [] };
538
+ const placeholders = changedSymbolIds.map(() => '?').join(',');
539
+ const testFilesDirect = this.db.prepare(`
540
+ SELECT DISTINCT src.file_path
541
+ FROM links l
542
+ JOIN symbols src ON src.id = l.from_id
543
+ WHERE l.to_id IN (${placeholders})
544
+ AND (src.file_path LIKE '%/test/%' OR src.file_path LIKE '%\\test\\%'
545
+ OR src.file_path LIKE '%.test.%' OR src.file_path LIKE '%.spec.%')
546
+ `).all(...changedSymbolIds);
547
+ const testFilesUpstream = this.db.prepare(`
548
+ WITH RECURSIVE upstream(id, depth) AS (
549
+ SELECT from_id, 1 FROM links WHERE to_id IN (${placeholders})
550
+ AND type IN ('calls', 'imports', 'extends', 'implements')
551
+ UNION
552
+ SELECT l.from_id, u.depth + 1
553
+ FROM links l JOIN upstream u ON l.to_id = u.id
554
+ WHERE l.type IN ('calls', 'imports', 'extends', 'implements') AND u.depth < 5
555
+ )
556
+ SELECT DISTINCT s.file_path FROM upstream u
557
+ JOIN symbols s ON s.id = u.id
558
+ WHERE (s.file_path LIKE '%/test/%' OR s.file_path LIKE '%\\test\\%'
559
+ OR s.file_path LIKE '%.test.%' OR s.file_path LIKE '%.spec.%')
560
+ `).all(...changedSymbolIds);
561
+ const allTestFiles = [...new Set([
562
+ ...testFilesDirect.map((r) => r.file_path),
563
+ ...testFilesUpstream.map((r) => r.file_path),
564
+ ])];
565
+ const changedSymbols = changedSymbolIds.filter(id => this.getSymbolTestCoverage(id));
566
+ return { testFiles: allTestFiles, changedSymbols };
567
+ }
568
+ // ── Codebase summary ──
569
+ getCodebaseSummary() {
570
+ const stats = this.getStats();
571
+ const coverage = this.getTestCoverage();
572
+ const domains = this.getDomainStats();
573
+ const topHubs = this.getTopHubs(10);
574
+ const coveragePct = coverage.exportedProductionSymbols > 0
575
+ ? Math.round((coverage.testedSymbols / coverage.exportedProductionSymbols) * 100)
576
+ : 0;
577
+ return {
578
+ symbols: stats.symbols,
579
+ links: stats.links,
580
+ files: stats.files,
581
+ coveragePct,
582
+ testedSymbols: coverage.testedSymbols,
583
+ exportedSymbols: coverage.exportedProductionSymbols,
584
+ domains,
585
+ topHubs,
586
+ };
587
+ }
588
+ // ── Topological similarity ──
589
+ findTopologicallySimilar(symbolId, limit = 10) {
590
+ const target = this.findSymbolById(symbolId);
591
+ if (!target)
592
+ return [];
593
+ const incoming = this.getIncomingLinks(symbolId).filter(l => l.type !== 'contains');
594
+ const outgoing = this.getOutgoingLinks(symbolId).filter(l => l.type !== 'contains');
595
+ const targetLinks = new Set();
596
+ for (const link of incoming)
597
+ targetLinks.add(link.fromId);
598
+ for (const link of outgoing)
599
+ targetLinks.add(link.toId);
600
+ const siblings = this.getSymbolsByFile(target.filePath).filter(s => s.id !== symbolId);
601
+ const results = [];
602
+ for (const sibling of siblings) {
603
+ const sibIncoming = this.getIncomingLinks(sibling.id).filter(l => l.type !== 'contains');
604
+ const sibOutgoing = this.getOutgoingLinks(sibling.id).filter(l => l.type !== 'contains');
605
+ const siblingLinks = new Set();
606
+ for (const link of sibIncoming)
607
+ siblingLinks.add(link.fromId);
608
+ for (const link of sibOutgoing)
609
+ siblingLinks.add(link.toId);
610
+ let intersection = 0;
611
+ for (const id of targetLinks) {
612
+ if (siblingLinks.has(id))
613
+ intersection++;
614
+ }
615
+ const union = new Set([...targetLinks, ...siblingLinks]).size;
616
+ const similarity = union > 0 ? intersection / union : 0;
617
+ if (similarity >= 0.3) {
618
+ results.push({ symbol: sibling, similarity: Math.round(similarity * 100) / 100 });
619
+ }
620
+ }
621
+ return results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
622
+ }
623
+ // ── Annotations ──
624
+ getAnnotationCount() {
625
+ const row = this.stmts.annotationCount.get();
626
+ return row.c;
627
+ }
628
+ // ── Annotation/session API ──
629
+ getRawDb() { return this.db; }
519
630
  addAnnotation(symbolId, key, value, agent, sessionId, ttlHours) {
520
631
  const expiresAt = ttlHours
521
632
  ? new Date(Date.now() + ttlHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19)
522
633
  : null;
523
- const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
634
+ const result = this.db.prepare(`INSERT INTO annotations (symbol_id, key, value, agent, session_id, expires_at)
524
635
  VALUES (?, ?, ?, ?, ?, ?)`).run(symbolId, key, value, agent ?? null, sessionId ?? null, expiresAt);
525
636
  return result.lastInsertRowid;
526
637
  }
527
- getAnnotations(filters) {
638
+ getAnnotations(filters = {}) {
528
639
  const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
529
640
  const params = [];
530
641
  if (filters.symbolId) {
@@ -543,47 +654,29 @@ export class Database {
543
654
  clauses.push('session_id = ?');
544
655
  params.push(filters.sessionId);
545
656
  }
546
- const where = `WHERE ${clauses.join(' AND ')}`;
547
- const limit = filters.limit ?? 50;
548
- const rows = this.db.prepare(`SELECT id, symbol_id, key, value, agent, session_id, created_at
549
- FROM annotations ${where}
550
- ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
551
- return rows.map(r => ({
552
- id: r.id,
553
- symbolId: r.symbol_id,
554
- key: r.key,
555
- value: r.value,
556
- agent: r.agent,
557
- sessionId: r.session_id,
558
- createdAt: r.created_at,
559
- }));
657
+ const sql = `SELECT *, symbol_id as symbolId, created_at as createdAt FROM annotations WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC` + (filters.limit ? ` LIMIT ${filters.limit}` : '');
658
+ const rows = this.db.prepare(sql).all(...params);
659
+ return rows.map((r) => ({ id: r.id, symbolId: r.symbol_id, key: r.key, value: r.value, agent: r.agent, sessionId: r.session_id, createdAt: r.created_at }));
560
660
  }
561
661
  getAnnotationsForSymbol(symbolId) {
562
- const rows = this.db.prepare(`SELECT key, value, agent, created_at FROM annotations
563
- WHERE symbol_id = ? AND (expires_at IS NULL OR expires_at > datetime('now'))
564
- ORDER BY created_at DESC`).all(symbolId);
565
- return rows.map(r => ({ key: r.key, value: r.value, agent: r.agent, createdAt: r.created_at }));
662
+ return this.getAnnotations({ symbolId });
566
663
  }
567
664
  cleanupExpiredAnnotations() {
568
- const result = this.db.prepare(`DELETE FROM annotations WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')`).run();
665
+ const result = this.db.prepare("DELETE FROM annotations WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')").run();
569
666
  return result.changes;
570
667
  }
571
- // ── Agent sessions ──
572
668
  startSession(id, agent, context) {
573
- this.db.prepare(`INSERT INTO agent_sessions (id, agent, context_json, status) VALUES (?, ?, ?, 'active')`).run(id, agent, context ?? null);
669
+ this.db.prepare('INSERT OR REPLACE INTO agent_sessions (id, agent, status, started_at, context_json) VALUES (?, ?, ?, datetime(\'now\'), ?)')
670
+ .run(id, agent, 'active', context ?? null);
574
671
  }
575
672
  getSession(id) {
576
- const row = this.db.prepare('SELECT * FROM agent_sessions WHERE id = ?').get(id);
673
+ const row = this.db.prepare('SELECT *, started_at as startedAt, ended_at as endedAt, context_json as context FROM agent_sessions WHERE id = ?').get(id);
577
674
  if (!row)
578
675
  return null;
579
- return { id: row.id, agent: row.agent, startedAt: row.started_at, endedAt: row.ended_at, context: row.context_json, status: row.status };
676
+ return { id: row.id, agent: row.agent, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, context: row.context };
580
677
  }
581
- endSession(id, status = 'completed') {
582
- this.db.prepare(`UPDATE agent_sessions SET ended_at = datetime('now'), status = ? WHERE id = ?`).run(status, id);
583
- }
584
- /** Expose raw handle for subsystems (e.g. EmbeddingStore) that need direct access. */
585
- getRawDb() {
586
- return this.db;
678
+ endSession(id, status) {
679
+ this.db.prepare("UPDATE agent_sessions SET status = ?, ended_at = datetime('now') WHERE id = ?").run(status, id);
587
680
  }
588
681
  transaction(fn) {
589
682
  return this.db.transaction(fn)();