engrm 0.4.12 → 0.4.13
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 +160 -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
|
@@ -569,6 +569,18 @@ var MIGRATIONS = [
|
|
|
569
569
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
570
570
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
571
571
|
`
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
version: 11,
|
|
575
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
576
|
+
sql: `
|
|
577
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
578
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
579
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
580
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
581
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
582
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
583
|
+
`
|
|
572
584
|
}
|
|
573
585
|
];
|
|
574
586
|
function isVecExtensionLoaded(db) {
|
|
@@ -622,6 +634,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
622
634
|
version = Math.max(version, 9);
|
|
623
635
|
if (tableExists(db, "tool_events"))
|
|
624
636
|
version = Math.max(version, 10);
|
|
637
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
638
|
+
version = Math.max(version, 11);
|
|
625
639
|
return version;
|
|
626
640
|
}
|
|
627
641
|
function runMigrations(db) {
|
|
@@ -704,6 +718,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
704
718
|
|
|
705
719
|
// src/storage/sqlite.ts
|
|
706
720
|
import { createHash as createHash2 } from "node:crypto";
|
|
721
|
+
|
|
722
|
+
// src/intelligence/summary-sections.ts
|
|
723
|
+
function extractSummaryItems(section, limit) {
|
|
724
|
+
if (!section || !section.trim())
|
|
725
|
+
return [];
|
|
726
|
+
const rawLines = section.split(`
|
|
727
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
728
|
+
const items = [];
|
|
729
|
+
const seen = new Set;
|
|
730
|
+
let heading = null;
|
|
731
|
+
for (const rawLine of rawLines) {
|
|
732
|
+
const line = stripSectionPrefix(rawLine);
|
|
733
|
+
if (!line)
|
|
734
|
+
continue;
|
|
735
|
+
const headingOnly = parseHeading(line);
|
|
736
|
+
if (headingOnly) {
|
|
737
|
+
heading = headingOnly;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
741
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
742
|
+
if (!stripped)
|
|
743
|
+
continue;
|
|
744
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
745
|
+
const normalized = normalizeItem(item);
|
|
746
|
+
if (!normalized || seen.has(normalized))
|
|
747
|
+
continue;
|
|
748
|
+
seen.add(normalized);
|
|
749
|
+
items.push(item);
|
|
750
|
+
if (limit && items.length >= limit)
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
return items;
|
|
754
|
+
}
|
|
755
|
+
function formatSummaryItems(section, maxLen) {
|
|
756
|
+
const items = extractSummaryItems(section);
|
|
757
|
+
if (items.length === 0)
|
|
758
|
+
return null;
|
|
759
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
760
|
+
`);
|
|
761
|
+
if (cleaned.length <= maxLen)
|
|
762
|
+
return cleaned;
|
|
763
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
764
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
765
|
+
`), truncated.lastIndexOf(" "));
|
|
766
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
767
|
+
return `${safe.trimEnd()}…`;
|
|
768
|
+
}
|
|
769
|
+
function normalizeSummarySection(section) {
|
|
770
|
+
const items = extractSummaryItems(section);
|
|
771
|
+
if (items.length === 0) {
|
|
772
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
773
|
+
return cleaned || null;
|
|
774
|
+
}
|
|
775
|
+
return items.map((item) => `- ${item}`).join(`
|
|
776
|
+
`);
|
|
777
|
+
}
|
|
778
|
+
function normalizeSummaryRequest(value) {
|
|
779
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
780
|
+
return cleaned || null;
|
|
781
|
+
}
|
|
782
|
+
function stripSectionPrefix(value) {
|
|
783
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
784
|
+
}
|
|
785
|
+
function parseHeading(value) {
|
|
786
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
787
|
+
if (boldMatch?.[1]) {
|
|
788
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
789
|
+
}
|
|
790
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
791
|
+
if (plainMatch?.[1]) {
|
|
792
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
function normalizeItem(value) {
|
|
797
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/storage/sqlite.ts
|
|
707
801
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
708
802
|
function openDatabase(dbPath) {
|
|
709
803
|
if (IS_BUN) {
|
|
@@ -819,8 +913,9 @@ class MemDatabase {
|
|
|
819
913
|
const result = this.db.query(`INSERT INTO observations (
|
|
820
914
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
821
915
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
822
|
-
user_id, device_id, agent,
|
|
823
|
-
|
|
916
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
917
|
+
created_at, created_at_epoch
|
|
918
|
+
) 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);
|
|
824
919
|
const id = Number(result.lastInsertRowid);
|
|
825
920
|
const row = this.getObservationById(id);
|
|
826
921
|
this.ftsInsert(row);
|
|
@@ -1061,6 +1156,13 @@ class MemDatabase {
|
|
|
1061
1156
|
ORDER BY prompt_number ASC
|
|
1062
1157
|
LIMIT ?`).all(sessionId, limit);
|
|
1063
1158
|
}
|
|
1159
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1160
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1161
|
+
WHERE session_id = ?
|
|
1162
|
+
ORDER BY prompt_number DESC
|
|
1163
|
+
LIMIT 1`).get(sessionId);
|
|
1164
|
+
return row?.prompt_number ?? null;
|
|
1165
|
+
}
|
|
1064
1166
|
insertToolEvent(input) {
|
|
1065
1167
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1066
1168
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1170,8 +1272,15 @@ class MemDatabase {
|
|
|
1170
1272
|
}
|
|
1171
1273
|
insertSessionSummary(summary) {
|
|
1172
1274
|
const now = Math.floor(Date.now() / 1000);
|
|
1275
|
+
const normalized = {
|
|
1276
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1277
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1278
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1279
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1280
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1281
|
+
};
|
|
1173
1282
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1174
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1175
1284
|
const id = Number(result.lastInsertRowid);
|
|
1176
1285
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1177
1286
|
}
|
|
@@ -2043,23 +2152,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
|
|
|
2043
2152
|
return request ?? completed ?? "(no summary)";
|
|
2044
2153
|
}
|
|
2045
2154
|
function formatSummarySection(value, maxLen) {
|
|
2046
|
-
|
|
2047
|
-
return null;
|
|
2048
|
-
const cleaned = value.split(`
|
|
2049
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
|
|
2050
|
-
`);
|
|
2051
|
-
if (!cleaned)
|
|
2052
|
-
return null;
|
|
2053
|
-
return truncateMultilineText(cleaned, maxLen);
|
|
2054
|
-
}
|
|
2055
|
-
function truncateMultilineText(text, maxLen) {
|
|
2056
|
-
if (text.length <= maxLen)
|
|
2057
|
-
return text;
|
|
2058
|
-
const truncated = text.slice(0, maxLen).trimEnd();
|
|
2059
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
2060
|
-
`), truncated.lastIndexOf(" "));
|
|
2061
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
2062
|
-
return `${safe.trimEnd()}…`;
|
|
2155
|
+
return formatSummaryItems(value, maxLen);
|
|
2063
2156
|
}
|
|
2064
2157
|
function truncateText(text, maxLen) {
|
|
2065
2158
|
if (text.length <= maxLen)
|
|
@@ -2083,8 +2176,7 @@ function stripInlineSectionLabel(value) {
|
|
|
2083
2176
|
function extractMeaningfulLines(value, limit) {
|
|
2084
2177
|
if (!value)
|
|
2085
2178
|
return [];
|
|
2086
|
-
return value.
|
|
2087
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
2179
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
2088
2180
|
}
|
|
2089
2181
|
function formatObservationDetailFromContext(obs) {
|
|
2090
2182
|
if (obs.facts) {
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -645,6 +645,18 @@ var MIGRATIONS = [
|
|
|
645
645
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
646
646
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
647
647
|
`
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
version: 11,
|
|
651
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
652
|
+
sql: `
|
|
653
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
654
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
655
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
656
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
657
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
658
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
659
|
+
`
|
|
648
660
|
}
|
|
649
661
|
];
|
|
650
662
|
function isVecExtensionLoaded(db) {
|
|
@@ -698,6 +710,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
698
710
|
version = Math.max(version, 9);
|
|
699
711
|
if (tableExists(db, "tool_events"))
|
|
700
712
|
version = Math.max(version, 10);
|
|
713
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
714
|
+
version = Math.max(version, 11);
|
|
701
715
|
return version;
|
|
702
716
|
}
|
|
703
717
|
function runMigrations(db) {
|
|
@@ -780,6 +794,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
780
794
|
|
|
781
795
|
// src/storage/sqlite.ts
|
|
782
796
|
import { createHash as createHash2 } from "node:crypto";
|
|
797
|
+
|
|
798
|
+
// src/intelligence/summary-sections.ts
|
|
799
|
+
function extractSummaryItems(section, limit) {
|
|
800
|
+
if (!section || !section.trim())
|
|
801
|
+
return [];
|
|
802
|
+
const rawLines = section.split(`
|
|
803
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
804
|
+
const items = [];
|
|
805
|
+
const seen = new Set;
|
|
806
|
+
let heading = null;
|
|
807
|
+
for (const rawLine of rawLines) {
|
|
808
|
+
const line = stripSectionPrefix(rawLine);
|
|
809
|
+
if (!line)
|
|
810
|
+
continue;
|
|
811
|
+
const headingOnly = parseHeading(line);
|
|
812
|
+
if (headingOnly) {
|
|
813
|
+
heading = headingOnly;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
817
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
818
|
+
if (!stripped)
|
|
819
|
+
continue;
|
|
820
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
821
|
+
const normalized = normalizeItem(item);
|
|
822
|
+
if (!normalized || seen.has(normalized))
|
|
823
|
+
continue;
|
|
824
|
+
seen.add(normalized);
|
|
825
|
+
items.push(item);
|
|
826
|
+
if (limit && items.length >= limit)
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
return items;
|
|
830
|
+
}
|
|
831
|
+
function formatSummaryItems(section, maxLen) {
|
|
832
|
+
const items = extractSummaryItems(section);
|
|
833
|
+
if (items.length === 0)
|
|
834
|
+
return null;
|
|
835
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
836
|
+
`);
|
|
837
|
+
if (cleaned.length <= maxLen)
|
|
838
|
+
return cleaned;
|
|
839
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
840
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
841
|
+
`), truncated.lastIndexOf(" "));
|
|
842
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
843
|
+
return `${safe.trimEnd()}…`;
|
|
844
|
+
}
|
|
845
|
+
function normalizeSummarySection(section) {
|
|
846
|
+
const items = extractSummaryItems(section);
|
|
847
|
+
if (items.length === 0) {
|
|
848
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
849
|
+
return cleaned || null;
|
|
850
|
+
}
|
|
851
|
+
return items.map((item) => `- ${item}`).join(`
|
|
852
|
+
`);
|
|
853
|
+
}
|
|
854
|
+
function normalizeSummaryRequest(value) {
|
|
855
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
856
|
+
return cleaned || null;
|
|
857
|
+
}
|
|
858
|
+
function stripSectionPrefix(value) {
|
|
859
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
860
|
+
}
|
|
861
|
+
function parseHeading(value) {
|
|
862
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
863
|
+
if (boldMatch?.[1]) {
|
|
864
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
865
|
+
}
|
|
866
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
867
|
+
if (plainMatch?.[1]) {
|
|
868
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
function normalizeItem(value) {
|
|
873
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/storage/sqlite.ts
|
|
783
877
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
784
878
|
function openDatabase(dbPath) {
|
|
785
879
|
if (IS_BUN) {
|
|
@@ -895,8 +989,9 @@ class MemDatabase {
|
|
|
895
989
|
const result = this.db.query(`INSERT INTO observations (
|
|
896
990
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
897
991
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
898
|
-
user_id, device_id, agent,
|
|
899
|
-
|
|
992
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
993
|
+
created_at, created_at_epoch
|
|
994
|
+
) 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);
|
|
900
995
|
const id = Number(result.lastInsertRowid);
|
|
901
996
|
const row = this.getObservationById(id);
|
|
902
997
|
this.ftsInsert(row);
|
|
@@ -1137,6 +1232,13 @@ class MemDatabase {
|
|
|
1137
1232
|
ORDER BY prompt_number ASC
|
|
1138
1233
|
LIMIT ?`).all(sessionId, limit);
|
|
1139
1234
|
}
|
|
1235
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1236
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1237
|
+
WHERE session_id = ?
|
|
1238
|
+
ORDER BY prompt_number DESC
|
|
1239
|
+
LIMIT 1`).get(sessionId);
|
|
1240
|
+
return row?.prompt_number ?? null;
|
|
1241
|
+
}
|
|
1140
1242
|
insertToolEvent(input) {
|
|
1141
1243
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1142
1244
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1246,8 +1348,15 @@ class MemDatabase {
|
|
|
1246
1348
|
}
|
|
1247
1349
|
insertSessionSummary(summary) {
|
|
1248
1350
|
const now = Math.floor(Date.now() / 1000);
|
|
1351
|
+
const normalized = {
|
|
1352
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1353
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1354
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1355
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1356
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1357
|
+
};
|
|
1249
1358
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1250
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1359
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1251
1360
|
const id = Number(result.lastInsertRowid);
|
|
1252
1361
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1253
1362
|
}
|
|
@@ -395,6 +395,84 @@ function computeObservationPriority(obs, nowEpoch) {
|
|
|
395
395
|
return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// src/intelligence/summary-sections.ts
|
|
399
|
+
function extractSummaryItems(section, limit) {
|
|
400
|
+
if (!section || !section.trim())
|
|
401
|
+
return [];
|
|
402
|
+
const rawLines = section.split(`
|
|
403
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
404
|
+
const items = [];
|
|
405
|
+
const seen = new Set;
|
|
406
|
+
let heading = null;
|
|
407
|
+
for (const rawLine of rawLines) {
|
|
408
|
+
const line = stripSectionPrefix(rawLine);
|
|
409
|
+
if (!line)
|
|
410
|
+
continue;
|
|
411
|
+
const headingOnly = parseHeading(line);
|
|
412
|
+
if (headingOnly) {
|
|
413
|
+
heading = headingOnly;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
417
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
418
|
+
if (!stripped)
|
|
419
|
+
continue;
|
|
420
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
421
|
+
const normalized = normalizeItem(item);
|
|
422
|
+
if (!normalized || seen.has(normalized))
|
|
423
|
+
continue;
|
|
424
|
+
seen.add(normalized);
|
|
425
|
+
items.push(item);
|
|
426
|
+
if (limit && items.length >= limit)
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
return items;
|
|
430
|
+
}
|
|
431
|
+
function formatSummaryItems(section, maxLen) {
|
|
432
|
+
const items = extractSummaryItems(section);
|
|
433
|
+
if (items.length === 0)
|
|
434
|
+
return null;
|
|
435
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
436
|
+
`);
|
|
437
|
+
if (cleaned.length <= maxLen)
|
|
438
|
+
return cleaned;
|
|
439
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
440
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
441
|
+
`), truncated.lastIndexOf(" "));
|
|
442
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
443
|
+
return `${safe.trimEnd()}…`;
|
|
444
|
+
}
|
|
445
|
+
function normalizeSummarySection(section) {
|
|
446
|
+
const items = extractSummaryItems(section);
|
|
447
|
+
if (items.length === 0) {
|
|
448
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
449
|
+
return cleaned || null;
|
|
450
|
+
}
|
|
451
|
+
return items.map((item) => `- ${item}`).join(`
|
|
452
|
+
`);
|
|
453
|
+
}
|
|
454
|
+
function normalizeSummaryRequest(value) {
|
|
455
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
456
|
+
return cleaned || null;
|
|
457
|
+
}
|
|
458
|
+
function stripSectionPrefix(value) {
|
|
459
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
460
|
+
}
|
|
461
|
+
function parseHeading(value) {
|
|
462
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
463
|
+
if (boldMatch?.[1]) {
|
|
464
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
465
|
+
}
|
|
466
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
467
|
+
if (plainMatch?.[1]) {
|
|
468
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
function normalizeItem(value) {
|
|
473
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
474
|
+
}
|
|
475
|
+
|
|
398
476
|
// src/context/inject.ts
|
|
399
477
|
function tokenizeProjectHint(text) {
|
|
400
478
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
@@ -786,23 +864,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
|
|
|
786
864
|
return request ?? completed ?? "(no summary)";
|
|
787
865
|
}
|
|
788
866
|
function formatSummarySection(value, maxLen) {
|
|
789
|
-
|
|
790
|
-
return null;
|
|
791
|
-
const cleaned = value.split(`
|
|
792
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
|
|
793
|
-
`);
|
|
794
|
-
if (!cleaned)
|
|
795
|
-
return null;
|
|
796
|
-
return truncateMultilineText(cleaned, maxLen);
|
|
797
|
-
}
|
|
798
|
-
function truncateMultilineText(text, maxLen) {
|
|
799
|
-
if (text.length <= maxLen)
|
|
800
|
-
return text;
|
|
801
|
-
const truncated = text.slice(0, maxLen).trimEnd();
|
|
802
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
803
|
-
`), truncated.lastIndexOf(" "));
|
|
804
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
805
|
-
return `${safe.trimEnd()}…`;
|
|
867
|
+
return formatSummaryItems(value, maxLen);
|
|
806
868
|
}
|
|
807
869
|
function truncateText(text, maxLen) {
|
|
808
870
|
if (text.length <= maxLen)
|
|
@@ -826,8 +888,7 @@ function stripInlineSectionLabel(value) {
|
|
|
826
888
|
function extractMeaningfulLines(value, limit) {
|
|
827
889
|
if (!value)
|
|
828
890
|
return [];
|
|
829
|
-
return value.
|
|
830
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
891
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
831
892
|
}
|
|
832
893
|
function formatObservationDetailFromContext(obs) {
|
|
833
894
|
if (obs.facts) {
|
|
@@ -1093,7 +1154,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
1093
1154
|
import { join as join3 } from "node:path";
|
|
1094
1155
|
import { homedir } from "node:os";
|
|
1095
1156
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
1096
|
-
var CLIENT_VERSION = "0.4.
|
|
1157
|
+
var CLIENT_VERSION = "0.4.13";
|
|
1097
1158
|
function hashFile(filePath) {
|
|
1098
1159
|
try {
|
|
1099
1160
|
if (!existsSync3(filePath))
|
|
@@ -2156,6 +2217,18 @@ var MIGRATIONS = [
|
|
|
2156
2217
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
2157
2218
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
2158
2219
|
`
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
version: 11,
|
|
2223
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
2224
|
+
sql: `
|
|
2225
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
2226
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
2227
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
2228
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
2229
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
2230
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
2231
|
+
`
|
|
2159
2232
|
}
|
|
2160
2233
|
];
|
|
2161
2234
|
function isVecExtensionLoaded(db) {
|
|
@@ -2209,6 +2282,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
2209
2282
|
version = Math.max(version, 9);
|
|
2210
2283
|
if (tableExists(db, "tool_events"))
|
|
2211
2284
|
version = Math.max(version, 10);
|
|
2285
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
2286
|
+
version = Math.max(version, 11);
|
|
2212
2287
|
return version;
|
|
2213
2288
|
}
|
|
2214
2289
|
function runMigrations(db) {
|
|
@@ -2406,8 +2481,9 @@ class MemDatabase {
|
|
|
2406
2481
|
const result = this.db.query(`INSERT INTO observations (
|
|
2407
2482
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
2408
2483
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
2409
|
-
user_id, device_id, agent,
|
|
2410
|
-
|
|
2484
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
2485
|
+
created_at, created_at_epoch
|
|
2486
|
+
) 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);
|
|
2411
2487
|
const id = Number(result.lastInsertRowid);
|
|
2412
2488
|
const row = this.getObservationById(id);
|
|
2413
2489
|
this.ftsInsert(row);
|
|
@@ -2648,6 +2724,13 @@ class MemDatabase {
|
|
|
2648
2724
|
ORDER BY prompt_number ASC
|
|
2649
2725
|
LIMIT ?`).all(sessionId, limit);
|
|
2650
2726
|
}
|
|
2727
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
2728
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
2729
|
+
WHERE session_id = ?
|
|
2730
|
+
ORDER BY prompt_number DESC
|
|
2731
|
+
LIMIT 1`).get(sessionId);
|
|
2732
|
+
return row?.prompt_number ?? null;
|
|
2733
|
+
}
|
|
2651
2734
|
insertToolEvent(input) {
|
|
2652
2735
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
2653
2736
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -2757,8 +2840,15 @@ class MemDatabase {
|
|
|
2757
2840
|
}
|
|
2758
2841
|
insertSessionSummary(summary) {
|
|
2759
2842
|
const now = Math.floor(Date.now() / 1000);
|
|
2843
|
+
const normalized = {
|
|
2844
|
+
request: normalizeSummaryRequest(summary.request),
|
|
2845
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
2846
|
+
learned: normalizeSummarySection(summary.learned),
|
|
2847
|
+
completed: normalizeSummarySection(summary.completed),
|
|
2848
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
2849
|
+
};
|
|
2760
2850
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
2761
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
2851
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
2762
2852
|
const id = Number(result.lastInsertRowid);
|
|
2763
2853
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
2764
2854
|
}
|
|
@@ -2964,7 +3054,8 @@ async function main() {
|
|
|
2964
3054
|
securityFindings: context.securityFindings?.length ?? 0,
|
|
2965
3055
|
unreadMessages: msgCount,
|
|
2966
3056
|
synced: syncedCount,
|
|
2967
|
-
context
|
|
3057
|
+
context,
|
|
3058
|
+
estimatedReadTokens: estimateTokens(formatContextForInjection(context))
|
|
2968
3059
|
});
|
|
2969
3060
|
let packLine = "";
|
|
2970
3061
|
try {
|
|
@@ -3049,6 +3140,20 @@ function formatSplashScreen(data) {
|
|
|
3049
3140
|
lines.push(` ${line}`);
|
|
3050
3141
|
}
|
|
3051
3142
|
}
|
|
3143
|
+
const economics = formatContextEconomics(data);
|
|
3144
|
+
if (economics.length > 0) {
|
|
3145
|
+
lines.push("");
|
|
3146
|
+
for (const line of economics) {
|
|
3147
|
+
lines.push(` ${line}`);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
const inspectHints = formatInspectHints(data.context);
|
|
3151
|
+
if (inspectHints.length > 0) {
|
|
3152
|
+
lines.push("");
|
|
3153
|
+
for (const line of inspectHints) {
|
|
3154
|
+
lines.push(` ${line}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3052
3157
|
lines.push("");
|
|
3053
3158
|
return lines.join(`
|
|
3054
3159
|
`);
|
|
@@ -3157,6 +3262,36 @@ function formatVisibleStartupBrief(context) {
|
|
|
3157
3262
|
}
|
|
3158
3263
|
return lines.slice(0, 14);
|
|
3159
3264
|
}
|
|
3265
|
+
function formatContextEconomics(data) {
|
|
3266
|
+
const totalMemories = Math.max(0, data.loaded + data.available);
|
|
3267
|
+
const parts = [];
|
|
3268
|
+
if (totalMemories > 0) {
|
|
3269
|
+
parts.push(`${totalMemories.toLocaleString()} total memories`);
|
|
3270
|
+
}
|
|
3271
|
+
if (data.estimatedReadTokens > 0) {
|
|
3272
|
+
parts.push(`read now ~${data.estimatedReadTokens.toLocaleString()}t`);
|
|
3273
|
+
}
|
|
3274
|
+
if (parts.length === 0)
|
|
3275
|
+
return [];
|
|
3276
|
+
return [`${c2.dim}Context economics:${c2.reset} ${parts.join(" \xB7 ")}`];
|
|
3277
|
+
}
|
|
3278
|
+
function formatInspectHints(context) {
|
|
3279
|
+
const hints = [];
|
|
3280
|
+
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
3281
|
+
hints.push("recent_sessions");
|
|
3282
|
+
hints.push("session_story");
|
|
3283
|
+
}
|
|
3284
|
+
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
|
|
3285
|
+
hints.push("activity_feed");
|
|
3286
|
+
}
|
|
3287
|
+
if (context.observations.length > 0) {
|
|
3288
|
+
hints.push("memory_console");
|
|
3289
|
+
}
|
|
3290
|
+
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
3291
|
+
if (unique.length === 0)
|
|
3292
|
+
return [];
|
|
3293
|
+
return [`${c2.dim}Inspect:${c2.reset} ${unique.join(" \xB7 ")}`];
|
|
3294
|
+
}
|
|
3160
3295
|
function rememberShownItem(shown, value) {
|
|
3161
3296
|
if (!value)
|
|
3162
3297
|
return;
|
|
@@ -3502,6 +3637,7 @@ function capitalize(value) {
|
|
|
3502
3637
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3503
3638
|
}
|
|
3504
3639
|
var __testables = {
|
|
3640
|
+
formatSplashScreen,
|
|
3505
3641
|
formatVisibleStartupBrief
|
|
3506
3642
|
};
|
|
3507
3643
|
runHook("session-start", main);
|