engrm 0.4.12 → 0.4.14
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/README.md +53 -2
- package/dist/cli.js +136 -6
- package/dist/hooks/elicitation-result.js +116 -4
- package/dist/hooks/post-tool-use.js +118 -5
- package/dist/hooks/pre-compact.js +114 -22
- package/dist/hooks/sentinel.js +112 -3
- package/dist/hooks/session-start.js +233 -24
- package/dist/hooks/stop.js +209 -14
- package/dist/hooks/user-prompt-submit.js +112 -3
- package/dist/server.js +2440 -1228
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,6 +215,8 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
215
215
|
| `memory_console` | Show a high-signal local memory console for the current project |
|
|
216
216
|
| `project_memory_index` | Show typed local memory by project, including hot files and recent sessions |
|
|
217
217
|
| `workspace_memory_index` | Show cross-project local memory coverage across the whole workspace |
|
|
218
|
+
| `tool_memory_index` | Show which source tools and plugins are creating durable memory |
|
|
219
|
+
| `session_tool_memory` | Show which tools in one session produced reusable memory and which produced none |
|
|
218
220
|
| `recent_requests` | Inspect captured raw user prompt chronology |
|
|
219
221
|
| `recent_tools` | Inspect captured raw tool chronology |
|
|
220
222
|
| `recent_sessions` | List recent local sessions to inspect further |
|
|
@@ -222,6 +224,32 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
222
224
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
223
225
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
224
226
|
| `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
|
|
227
|
+
| `capture_git_worktree` | Read the current git worktree diff and save reduced memory directly |
|
|
228
|
+
| `capture_repo_scan` | Run a lightweight repo scan and save reduced findings as memory |
|
|
229
|
+
| `capture_openclaw_content` | Save OpenClaw content, research, and follow-up work as plugin memory |
|
|
230
|
+
|
|
231
|
+
### Thin Tools, Thick Memory
|
|
232
|
+
|
|
233
|
+
Engrm now has a real thin-tool layer, not just a plugin spec.
|
|
234
|
+
|
|
235
|
+
Current first-party thin tools:
|
|
236
|
+
|
|
237
|
+
- `capture_git_worktree`
|
|
238
|
+
- reads the current repo diff directly
|
|
239
|
+
- reduces it through `engrm.git-diff`
|
|
240
|
+
- `capture_repo_scan`
|
|
241
|
+
- runs a lightweight repo scan
|
|
242
|
+
- reduces it through `engrm.repo-scan`
|
|
243
|
+
- `capture_openclaw_content`
|
|
244
|
+
- captures posted/researched/outcome-style OpenClaw work
|
|
245
|
+
- reduces it through `engrm.openclaw-content`
|
|
246
|
+
|
|
247
|
+
These tools are intentionally small:
|
|
248
|
+
|
|
249
|
+
- tiny input surface
|
|
250
|
+
- local-first execution
|
|
251
|
+
- reduced durable memory output
|
|
252
|
+
- visible in Engrm's local inspection tools so we can judge tool value honestly
|
|
225
253
|
|
|
226
254
|
### Local Memory Inspection
|
|
227
255
|
|
|
@@ -236,8 +264,10 @@ Recommended flow:
|
|
|
236
264
|
3. activity_feed
|
|
237
265
|
4. recent_sessions
|
|
238
266
|
5. session_story
|
|
239
|
-
6.
|
|
240
|
-
7.
|
|
267
|
+
6. tool_memory_index
|
|
268
|
+
7. session_tool_memory
|
|
269
|
+
8. project_memory_index
|
|
270
|
+
9. workspace_memory_index
|
|
241
271
|
```
|
|
242
272
|
|
|
243
273
|
What each tool is good for:
|
|
@@ -247,9 +277,30 @@ What each tool is good for:
|
|
|
247
277
|
- `activity_feed` shows the merged chronology across prompts, tools, observations, and summaries
|
|
248
278
|
- `recent_sessions` helps you pick a session worth opening
|
|
249
279
|
- `session_story` reconstructs one session in detail
|
|
280
|
+
- `tool_memory_index` shows which tools and plugins are actually producing durable memory
|
|
281
|
+
- `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
|
|
250
282
|
- `project_memory_index` shows typed memory by repo
|
|
251
283
|
- `workspace_memory_index` shows coverage across all repos on the machine
|
|
252
284
|
|
|
285
|
+
### Thin Tool Workflow
|
|
286
|
+
|
|
287
|
+
The current practical flow for thin tools is:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
1. memory_console / project_memory_index
|
|
291
|
+
2. tool_memory_index
|
|
292
|
+
3. capture_git_worktree or capture_repo_scan
|
|
293
|
+
4. session_tool_memory
|
|
294
|
+
5. session_story
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
That lets you:
|
|
298
|
+
|
|
299
|
+
- see what Engrm already knows
|
|
300
|
+
- see which tools/plugins are producing value
|
|
301
|
+
- capture the current repo state with a thin tool
|
|
302
|
+
- verify whether that tool produced reusable memory
|
|
303
|
+
|
|
253
304
|
### Observation Types
|
|
254
305
|
|
|
255
306
|
| Type | What it captures |
|
package/dist/cli.js
CHANGED
|
@@ -597,6 +597,18 @@ var MIGRATIONS = [
|
|
|
597
597
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
598
598
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
599
599
|
`
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
version: 11,
|
|
603
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
604
|
+
sql: `
|
|
605
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
606
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
607
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
608
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
609
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
610
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
611
|
+
`
|
|
600
612
|
}
|
|
601
613
|
];
|
|
602
614
|
function isVecExtensionLoaded(db) {
|
|
@@ -650,6 +662,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
650
662
|
version = Math.max(version, 9);
|
|
651
663
|
if (tableExists(db, "tool_events"))
|
|
652
664
|
version = Math.max(version, 10);
|
|
665
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
666
|
+
version = Math.max(version, 11);
|
|
653
667
|
return version;
|
|
654
668
|
}
|
|
655
669
|
function runMigrations(db) {
|
|
@@ -736,6 +750,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
736
750
|
|
|
737
751
|
// src/storage/sqlite.ts
|
|
738
752
|
import { createHash as createHash2 } from "node:crypto";
|
|
753
|
+
|
|
754
|
+
// src/intelligence/summary-sections.ts
|
|
755
|
+
function extractSummaryItems(section, limit) {
|
|
756
|
+
if (!section || !section.trim())
|
|
757
|
+
return [];
|
|
758
|
+
const rawLines = section.split(`
|
|
759
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
760
|
+
const items = [];
|
|
761
|
+
const seen = new Set;
|
|
762
|
+
let heading = null;
|
|
763
|
+
for (const rawLine of rawLines) {
|
|
764
|
+
const line = stripSectionPrefix(rawLine);
|
|
765
|
+
if (!line)
|
|
766
|
+
continue;
|
|
767
|
+
const headingOnly = parseHeading(line);
|
|
768
|
+
if (headingOnly) {
|
|
769
|
+
heading = headingOnly;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
773
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
774
|
+
if (!stripped)
|
|
775
|
+
continue;
|
|
776
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
777
|
+
const normalized = normalizeItem(item);
|
|
778
|
+
if (!normalized || seen.has(normalized))
|
|
779
|
+
continue;
|
|
780
|
+
seen.add(normalized);
|
|
781
|
+
items.push(item);
|
|
782
|
+
if (limit && items.length >= limit)
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
return items;
|
|
786
|
+
}
|
|
787
|
+
function formatSummaryItems(section, maxLen) {
|
|
788
|
+
const items = extractSummaryItems(section);
|
|
789
|
+
if (items.length === 0)
|
|
790
|
+
return null;
|
|
791
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
792
|
+
`);
|
|
793
|
+
if (cleaned.length <= maxLen)
|
|
794
|
+
return cleaned;
|
|
795
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
796
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
797
|
+
`), truncated.lastIndexOf(" "));
|
|
798
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
799
|
+
return `${safe.trimEnd()}…`;
|
|
800
|
+
}
|
|
801
|
+
function normalizeSummarySection(section) {
|
|
802
|
+
const items = extractSummaryItems(section);
|
|
803
|
+
if (items.length === 0) {
|
|
804
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
805
|
+
return cleaned || null;
|
|
806
|
+
}
|
|
807
|
+
return items.map((item) => `- ${item}`).join(`
|
|
808
|
+
`);
|
|
809
|
+
}
|
|
810
|
+
function normalizeSummaryRequest(value) {
|
|
811
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
812
|
+
return cleaned || null;
|
|
813
|
+
}
|
|
814
|
+
function stripSectionPrefix(value) {
|
|
815
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
816
|
+
}
|
|
817
|
+
function parseHeading(value) {
|
|
818
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
819
|
+
if (boldMatch?.[1]) {
|
|
820
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
821
|
+
}
|
|
822
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
823
|
+
if (plainMatch?.[1]) {
|
|
824
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
function normalizeItem(value) {
|
|
829
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/storage/sqlite.ts
|
|
739
833
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
740
834
|
function openDatabase(dbPath) {
|
|
741
835
|
if (IS_BUN) {
|
|
@@ -851,8 +945,9 @@ class MemDatabase {
|
|
|
851
945
|
const result = this.db.query(`INSERT INTO observations (
|
|
852
946
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
853
947
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
854
|
-
user_id, device_id, agent,
|
|
855
|
-
|
|
948
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
949
|
+
created_at, created_at_epoch
|
|
950
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
|
|
856
951
|
const id = Number(result.lastInsertRowid);
|
|
857
952
|
const row = this.getObservationById(id);
|
|
858
953
|
this.ftsInsert(row);
|
|
@@ -1093,6 +1188,13 @@ class MemDatabase {
|
|
|
1093
1188
|
ORDER BY prompt_number ASC
|
|
1094
1189
|
LIMIT ?`).all(sessionId, limit);
|
|
1095
1190
|
}
|
|
1191
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1192
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1193
|
+
WHERE session_id = ?
|
|
1194
|
+
ORDER BY prompt_number DESC
|
|
1195
|
+
LIMIT 1`).get(sessionId);
|
|
1196
|
+
return row?.prompt_number ?? null;
|
|
1197
|
+
}
|
|
1096
1198
|
insertToolEvent(input) {
|
|
1097
1199
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1098
1200
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1202,8 +1304,15 @@ class MemDatabase {
|
|
|
1202
1304
|
}
|
|
1203
1305
|
insertSessionSummary(summary) {
|
|
1204
1306
|
const now = Math.floor(Date.now() / 1000);
|
|
1307
|
+
const normalized = {
|
|
1308
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1309
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1310
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1311
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1312
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1313
|
+
};
|
|
1205
1314
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1206
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1315
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1207
1316
|
const id = Number(result.lastInsertRowid);
|
|
1208
1317
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1209
1318
|
}
|
|
@@ -1708,6 +1817,18 @@ var MIGRATIONS2 = [
|
|
|
1708
1817
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
1709
1818
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
1710
1819
|
`
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
version: 11,
|
|
1823
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
1824
|
+
sql: `
|
|
1825
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
1826
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
1827
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
1828
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
1829
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
1830
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
1831
|
+
`
|
|
1711
1832
|
}
|
|
1712
1833
|
];
|
|
1713
1834
|
function isVecExtensionLoaded2(db) {
|
|
@@ -2987,6 +3108,7 @@ async function saveObservation(db, config, input) {
|
|
|
2987
3108
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
2988
3109
|
};
|
|
2989
3110
|
}
|
|
3111
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
2990
3112
|
const obs = db.insertObservation({
|
|
2991
3113
|
session_id: input.session_id ?? null,
|
|
2992
3114
|
project_id: project.id,
|
|
@@ -3002,7 +3124,9 @@ async function saveObservation(db, config, input) {
|
|
|
3002
3124
|
sensitivity,
|
|
3003
3125
|
user_id: config.user_id,
|
|
3004
3126
|
device_id: config.device_id,
|
|
3005
|
-
agent: input.agent ?? "claude-code"
|
|
3127
|
+
agent: input.agent ?? "claude-code",
|
|
3128
|
+
source_tool: input.source_tool ?? null,
|
|
3129
|
+
source_prompt_number: sourcePromptNumber
|
|
3006
3130
|
});
|
|
3007
3131
|
db.addToOutbox("observation", obs.id);
|
|
3008
3132
|
if (db.vecAvailable) {
|
|
@@ -4001,7 +4125,12 @@ function handleUpdate() {
|
|
|
4001
4125
|
console.log(`Updating Engrm to latest version...
|
|
4002
4126
|
`);
|
|
4003
4127
|
try {
|
|
4004
|
-
execSync2("npm
|
|
4128
|
+
const latest = execSync2("npm view engrm version", { encoding: "utf-8" }).trim();
|
|
4129
|
+
if (!latest)
|
|
4130
|
+
throw new Error("Could not resolve latest engrm version from npm");
|
|
4131
|
+
console.log(`Installing engrm@${latest}...
|
|
4132
|
+
`);
|
|
4133
|
+
execSync2(`npm install -g engrm@${latest}`, { stdio: "inherit" });
|
|
4005
4134
|
console.log(`
|
|
4006
4135
|
Update complete. Re-registering integrations...`);
|
|
4007
4136
|
const result = registerAll();
|
|
@@ -4012,7 +4141,8 @@ Update complete. Re-registering integrations...`);
|
|
|
4012
4141
|
console.log(`
|
|
4013
4142
|
Restart Claude Code or Codex to use the new version.`);
|
|
4014
4143
|
} catch (error) {
|
|
4015
|
-
console.error(
|
|
4144
|
+
console.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4145
|
+
console.error("Try manually: npm install -g engrm@<version>");
|
|
4016
4146
|
}
|
|
4017
4147
|
}
|
|
4018
4148
|
async function handleDoctor() {
|
|
@@ -797,6 +797,7 @@ async function saveObservation(db, config, input) {
|
|
|
797
797
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
798
798
|
};
|
|
799
799
|
}
|
|
800
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
800
801
|
const obs = db.insertObservation({
|
|
801
802
|
session_id: input.session_id ?? null,
|
|
802
803
|
project_id: project.id,
|
|
@@ -812,7 +813,9 @@ async function saveObservation(db, config, input) {
|
|
|
812
813
|
sensitivity,
|
|
813
814
|
user_id: config.user_id,
|
|
814
815
|
device_id: config.device_id,
|
|
815
|
-
agent: input.agent ?? "claude-code"
|
|
816
|
+
agent: input.agent ?? "claude-code",
|
|
817
|
+
source_tool: input.source_tool ?? null,
|
|
818
|
+
source_prompt_number: sourcePromptNumber
|
|
816
819
|
});
|
|
817
820
|
db.addToOutbox("observation", obs.id);
|
|
818
821
|
if (db.vecAvailable) {
|
|
@@ -1427,6 +1430,18 @@ var MIGRATIONS = [
|
|
|
1427
1430
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
1428
1431
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
1429
1432
|
`
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
version: 11,
|
|
1436
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
1437
|
+
sql: `
|
|
1438
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
1439
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
1440
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
1441
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
1442
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
1443
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
1444
|
+
`
|
|
1430
1445
|
}
|
|
1431
1446
|
];
|
|
1432
1447
|
function isVecExtensionLoaded(db) {
|
|
@@ -1480,6 +1495,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
1480
1495
|
version = Math.max(version, 9);
|
|
1481
1496
|
if (tableExists(db, "tool_events"))
|
|
1482
1497
|
version = Math.max(version, 10);
|
|
1498
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
1499
|
+
version = Math.max(version, 11);
|
|
1483
1500
|
return version;
|
|
1484
1501
|
}
|
|
1485
1502
|
function runMigrations(db) {
|
|
@@ -1562,6 +1579,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
1562
1579
|
|
|
1563
1580
|
// src/storage/sqlite.ts
|
|
1564
1581
|
import { createHash as createHash2 } from "node:crypto";
|
|
1582
|
+
|
|
1583
|
+
// src/intelligence/summary-sections.ts
|
|
1584
|
+
function extractSummaryItems(section, limit) {
|
|
1585
|
+
if (!section || !section.trim())
|
|
1586
|
+
return [];
|
|
1587
|
+
const rawLines = section.split(`
|
|
1588
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
1589
|
+
const items = [];
|
|
1590
|
+
const seen = new Set;
|
|
1591
|
+
let heading = null;
|
|
1592
|
+
for (const rawLine of rawLines) {
|
|
1593
|
+
const line = stripSectionPrefix(rawLine);
|
|
1594
|
+
if (!line)
|
|
1595
|
+
continue;
|
|
1596
|
+
const headingOnly = parseHeading(line);
|
|
1597
|
+
if (headingOnly) {
|
|
1598
|
+
heading = headingOnly;
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
1602
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
1603
|
+
if (!stripped)
|
|
1604
|
+
continue;
|
|
1605
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
1606
|
+
const normalized = normalizeItem(item);
|
|
1607
|
+
if (!normalized || seen.has(normalized))
|
|
1608
|
+
continue;
|
|
1609
|
+
seen.add(normalized);
|
|
1610
|
+
items.push(item);
|
|
1611
|
+
if (limit && items.length >= limit)
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
return items;
|
|
1615
|
+
}
|
|
1616
|
+
function formatSummaryItems(section, maxLen) {
|
|
1617
|
+
const items = extractSummaryItems(section);
|
|
1618
|
+
if (items.length === 0)
|
|
1619
|
+
return null;
|
|
1620
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
1621
|
+
`);
|
|
1622
|
+
if (cleaned.length <= maxLen)
|
|
1623
|
+
return cleaned;
|
|
1624
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
1625
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
1626
|
+
`), truncated.lastIndexOf(" "));
|
|
1627
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
1628
|
+
return `${safe.trimEnd()}…`;
|
|
1629
|
+
}
|
|
1630
|
+
function normalizeSummarySection(section) {
|
|
1631
|
+
const items = extractSummaryItems(section);
|
|
1632
|
+
if (items.length === 0) {
|
|
1633
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
1634
|
+
return cleaned || null;
|
|
1635
|
+
}
|
|
1636
|
+
return items.map((item) => `- ${item}`).join(`
|
|
1637
|
+
`);
|
|
1638
|
+
}
|
|
1639
|
+
function normalizeSummaryRequest(value) {
|
|
1640
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
1641
|
+
return cleaned || null;
|
|
1642
|
+
}
|
|
1643
|
+
function stripSectionPrefix(value) {
|
|
1644
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
1645
|
+
}
|
|
1646
|
+
function parseHeading(value) {
|
|
1647
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
1648
|
+
if (boldMatch?.[1]) {
|
|
1649
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
1650
|
+
}
|
|
1651
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
1652
|
+
if (plainMatch?.[1]) {
|
|
1653
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
function normalizeItem(value) {
|
|
1658
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/storage/sqlite.ts
|
|
1565
1662
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
1566
1663
|
function openDatabase(dbPath) {
|
|
1567
1664
|
if (IS_BUN) {
|
|
@@ -1677,8 +1774,9 @@ class MemDatabase {
|
|
|
1677
1774
|
const result = this.db.query(`INSERT INTO observations (
|
|
1678
1775
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
1679
1776
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
1680
|
-
user_id, device_id, agent,
|
|
1681
|
-
|
|
1777
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
1778
|
+
created_at, created_at_epoch
|
|
1779
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
|
|
1682
1780
|
const id = Number(result.lastInsertRowid);
|
|
1683
1781
|
const row = this.getObservationById(id);
|
|
1684
1782
|
this.ftsInsert(row);
|
|
@@ -1919,6 +2017,13 @@ class MemDatabase {
|
|
|
1919
2017
|
ORDER BY prompt_number ASC
|
|
1920
2018
|
LIMIT ?`).all(sessionId, limit);
|
|
1921
2019
|
}
|
|
2020
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
2021
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
2022
|
+
WHERE session_id = ?
|
|
2023
|
+
ORDER BY prompt_number DESC
|
|
2024
|
+
LIMIT 1`).get(sessionId);
|
|
2025
|
+
return row?.prompt_number ?? null;
|
|
2026
|
+
}
|
|
1922
2027
|
insertToolEvent(input) {
|
|
1923
2028
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1924
2029
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -2028,8 +2133,15 @@ class MemDatabase {
|
|
|
2028
2133
|
}
|
|
2029
2134
|
insertSessionSummary(summary) {
|
|
2030
2135
|
const now = Math.floor(Date.now() / 1000);
|
|
2136
|
+
const normalized = {
|
|
2137
|
+
request: normalizeSummaryRequest(summary.request),
|
|
2138
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
2139
|
+
learned: normalizeSummarySection(summary.learned),
|
|
2140
|
+
completed: normalizeSummarySection(summary.completed),
|
|
2141
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
2142
|
+
};
|
|
2031
2143
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
2032
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
2144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
2033
2145
|
const id = Number(result.lastInsertRowid);
|
|
2034
2146
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
2035
2147
|
}
|
|
@@ -775,6 +775,18 @@ var MIGRATIONS = [
|
|
|
775
775
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
776
776
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
777
777
|
`
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
version: 11,
|
|
781
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
782
|
+
sql: `
|
|
783
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
784
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
786
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
787
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
788
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
789
|
+
`
|
|
778
790
|
}
|
|
779
791
|
];
|
|
780
792
|
function isVecExtensionLoaded(db) {
|
|
@@ -828,6 +840,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
828
840
|
version = Math.max(version, 9);
|
|
829
841
|
if (tableExists(db, "tool_events"))
|
|
830
842
|
version = Math.max(version, 10);
|
|
843
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
844
|
+
version = Math.max(version, 11);
|
|
831
845
|
return version;
|
|
832
846
|
}
|
|
833
847
|
function runMigrations(db) {
|
|
@@ -910,6 +924,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
910
924
|
|
|
911
925
|
// src/storage/sqlite.ts
|
|
912
926
|
import { createHash as createHash2 } from "node:crypto";
|
|
927
|
+
|
|
928
|
+
// src/intelligence/summary-sections.ts
|
|
929
|
+
function extractSummaryItems(section, limit) {
|
|
930
|
+
if (!section || !section.trim())
|
|
931
|
+
return [];
|
|
932
|
+
const rawLines = section.split(`
|
|
933
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
934
|
+
const items = [];
|
|
935
|
+
const seen = new Set;
|
|
936
|
+
let heading = null;
|
|
937
|
+
for (const rawLine of rawLines) {
|
|
938
|
+
const line = stripSectionPrefix(rawLine);
|
|
939
|
+
if (!line)
|
|
940
|
+
continue;
|
|
941
|
+
const headingOnly = parseHeading(line);
|
|
942
|
+
if (headingOnly) {
|
|
943
|
+
heading = headingOnly;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
947
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
948
|
+
if (!stripped)
|
|
949
|
+
continue;
|
|
950
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
951
|
+
const normalized = normalizeItem(item);
|
|
952
|
+
if (!normalized || seen.has(normalized))
|
|
953
|
+
continue;
|
|
954
|
+
seen.add(normalized);
|
|
955
|
+
items.push(item);
|
|
956
|
+
if (limit && items.length >= limit)
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
return items;
|
|
960
|
+
}
|
|
961
|
+
function formatSummaryItems(section, maxLen) {
|
|
962
|
+
const items = extractSummaryItems(section);
|
|
963
|
+
if (items.length === 0)
|
|
964
|
+
return null;
|
|
965
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
966
|
+
`);
|
|
967
|
+
if (cleaned.length <= maxLen)
|
|
968
|
+
return cleaned;
|
|
969
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
970
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
971
|
+
`), truncated.lastIndexOf(" "));
|
|
972
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
973
|
+
return `${safe.trimEnd()}…`;
|
|
974
|
+
}
|
|
975
|
+
function normalizeSummarySection(section) {
|
|
976
|
+
const items = extractSummaryItems(section);
|
|
977
|
+
if (items.length === 0) {
|
|
978
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
979
|
+
return cleaned || null;
|
|
980
|
+
}
|
|
981
|
+
return items.map((item) => `- ${item}`).join(`
|
|
982
|
+
`);
|
|
983
|
+
}
|
|
984
|
+
function normalizeSummaryRequest(value) {
|
|
985
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
986
|
+
return cleaned || null;
|
|
987
|
+
}
|
|
988
|
+
function stripSectionPrefix(value) {
|
|
989
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
990
|
+
}
|
|
991
|
+
function parseHeading(value) {
|
|
992
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
993
|
+
if (boldMatch?.[1]) {
|
|
994
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
995
|
+
}
|
|
996
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
997
|
+
if (plainMatch?.[1]) {
|
|
998
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeItem(value) {
|
|
1003
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/storage/sqlite.ts
|
|
913
1007
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
914
1008
|
function openDatabase(dbPath) {
|
|
915
1009
|
if (IS_BUN) {
|
|
@@ -1025,8 +1119,9 @@ class MemDatabase {
|
|
|
1025
1119
|
const result = this.db.query(`INSERT INTO observations (
|
|
1026
1120
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
1027
1121
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
1028
|
-
user_id, device_id, agent,
|
|
1029
|
-
|
|
1122
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
1123
|
+
created_at, created_at_epoch
|
|
1124
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
|
|
1030
1125
|
const id = Number(result.lastInsertRowid);
|
|
1031
1126
|
const row = this.getObservationById(id);
|
|
1032
1127
|
this.ftsInsert(row);
|
|
@@ -1267,6 +1362,13 @@ class MemDatabase {
|
|
|
1267
1362
|
ORDER BY prompt_number ASC
|
|
1268
1363
|
LIMIT ?`).all(sessionId, limit);
|
|
1269
1364
|
}
|
|
1365
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1366
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1367
|
+
WHERE session_id = ?
|
|
1368
|
+
ORDER BY prompt_number DESC
|
|
1369
|
+
LIMIT 1`).get(sessionId);
|
|
1370
|
+
return row?.prompt_number ?? null;
|
|
1371
|
+
}
|
|
1270
1372
|
insertToolEvent(input) {
|
|
1271
1373
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1272
1374
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1376,8 +1478,15 @@ class MemDatabase {
|
|
|
1376
1478
|
}
|
|
1377
1479
|
insertSessionSummary(summary) {
|
|
1378
1480
|
const now = Math.floor(Date.now() / 1000);
|
|
1481
|
+
const normalized = {
|
|
1482
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1483
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1484
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1485
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1486
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1487
|
+
};
|
|
1379
1488
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1380
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1489
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1381
1490
|
const id = Number(result.lastInsertRowid);
|
|
1382
1491
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1383
1492
|
}
|
|
@@ -2320,6 +2429,7 @@ async function saveObservation(db, config, input) {
|
|
|
2320
2429
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
2321
2430
|
};
|
|
2322
2431
|
}
|
|
2432
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
2323
2433
|
const obs = db.insertObservation({
|
|
2324
2434
|
session_id: input.session_id ?? null,
|
|
2325
2435
|
project_id: project.id,
|
|
@@ -2335,7 +2445,9 @@ async function saveObservation(db, config, input) {
|
|
|
2335
2445
|
sensitivity,
|
|
2336
2446
|
user_id: config.user_id,
|
|
2337
2447
|
device_id: config.device_id,
|
|
2338
|
-
agent: input.agent ?? "claude-code"
|
|
2448
|
+
agent: input.agent ?? "claude-code",
|
|
2449
|
+
source_tool: input.source_tool ?? null,
|
|
2450
|
+
source_prompt_number: sourcePromptNumber
|
|
2339
2451
|
});
|
|
2340
2452
|
db.addToOutbox("observation", obs.id);
|
|
2341
2453
|
if (db.vecAvailable) {
|
|
@@ -3148,7 +3260,8 @@ async function main() {
|
|
|
3148
3260
|
files_read: extracted.files_read,
|
|
3149
3261
|
files_modified: extracted.files_modified,
|
|
3150
3262
|
session_id: event.session_id,
|
|
3151
|
-
cwd: event.cwd
|
|
3263
|
+
cwd: event.cwd,
|
|
3264
|
+
source_tool: event.tool_name
|
|
3152
3265
|
});
|
|
3153
3266
|
incrementObserverSaveCount(event.session_id);
|
|
3154
3267
|
}
|