engrm 0.4.11 → 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.
@@ -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) {
@@ -946,6 +1040,7 @@ function openNodeDatabase(dbPath) {
946
1040
  const BetterSqlite3 = __require("better-sqlite3");
947
1041
  const raw = new BetterSqlite3(dbPath);
948
1042
  return {
1043
+ __raw: raw,
949
1044
  query(sql) {
950
1045
  const stmt = raw.prepare(sql);
951
1046
  return {
@@ -983,7 +1078,7 @@ class MemDatabase {
983
1078
  loadVecExtension() {
984
1079
  try {
985
1080
  const sqliteVec = __require("sqlite-vec");
986
- sqliteVec.load(this.db);
1081
+ sqliteVec.load(this.db.__raw ?? this.db);
987
1082
  return true;
988
1083
  } catch {
989
1084
  return false;
@@ -1024,8 +1119,9 @@ class MemDatabase {
1024
1119
  const result = this.db.query(`INSERT INTO observations (
1025
1120
  session_id, project_id, type, title, narrative, facts, concepts,
1026
1121
  files_read, files_modified, quality, lifecycle, sensitivity,
1027
- user_id, device_id, agent, created_at, created_at_epoch
1028
- ) 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", createdAt, now);
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);
1029
1125
  const id = Number(result.lastInsertRowid);
1030
1126
  const row = this.getObservationById(id);
1031
1127
  this.ftsInsert(row);
@@ -1266,6 +1362,13 @@ class MemDatabase {
1266
1362
  ORDER BY prompt_number ASC
1267
1363
  LIMIT ?`).all(sessionId, limit);
1268
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
+ }
1269
1372
  insertToolEvent(input) {
1270
1373
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1271
1374
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1375,8 +1478,15 @@ class MemDatabase {
1375
1478
  }
1376
1479
  insertSessionSummary(summary) {
1377
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
+ };
1378
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)
1379
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1380
1490
  const id = Number(result.lastInsertRowid);
1381
1491
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1382
1492
  }
@@ -2319,6 +2429,7 @@ async function saveObservation(db, config, input) {
2319
2429
  reason: `Merged into existing observation #${duplicate.id}`
2320
2430
  };
2321
2431
  }
2432
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
2322
2433
  const obs = db.insertObservation({
2323
2434
  session_id: input.session_id ?? null,
2324
2435
  project_id: project.id,
@@ -2334,7 +2445,9 @@ async function saveObservation(db, config, input) {
2334
2445
  sensitivity,
2335
2446
  user_id: config.user_id,
2336
2447
  device_id: config.device_id,
2337
- agent: input.agent ?? "claude-code"
2448
+ agent: input.agent ?? "claude-code",
2449
+ source_tool: input.source_tool ?? null,
2450
+ source_prompt_number: sourcePromptNumber
2338
2451
  });
2339
2452
  db.addToOutbox("observation", obs.id);
2340
2453
  if (db.vecAvailable) {
@@ -3147,7 +3260,8 @@ async function main() {
3147
3260
  files_read: extracted.files_read,
3148
3261
  files_modified: extracted.files_modified,
3149
3262
  session_id: event.session_id,
3150
- cwd: event.cwd
3263
+ cwd: event.cwd,
3264
+ source_tool: event.tool_name
3151
3265
  });
3152
3266
  incrementObserverSaveCount(event.session_id);
3153
3267
  }
@@ -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) {
@@ -740,6 +834,7 @@ function openNodeDatabase(dbPath) {
740
834
  const BetterSqlite3 = __require("better-sqlite3");
741
835
  const raw = new BetterSqlite3(dbPath);
742
836
  return {
837
+ __raw: raw,
743
838
  query(sql) {
744
839
  const stmt = raw.prepare(sql);
745
840
  return {
@@ -777,7 +872,7 @@ class MemDatabase {
777
872
  loadVecExtension() {
778
873
  try {
779
874
  const sqliteVec = __require("sqlite-vec");
780
- sqliteVec.load(this.db);
875
+ sqliteVec.load(this.db.__raw ?? this.db);
781
876
  return true;
782
877
  } catch {
783
878
  return false;
@@ -818,8 +913,9 @@ class MemDatabase {
818
913
  const result = this.db.query(`INSERT INTO observations (
819
914
  session_id, project_id, type, title, narrative, facts, concepts,
820
915
  files_read, files_modified, quality, lifecycle, sensitivity,
821
- user_id, device_id, agent, created_at, created_at_epoch
822
- ) 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", createdAt, now);
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);
823
919
  const id = Number(result.lastInsertRowid);
824
920
  const row = this.getObservationById(id);
825
921
  this.ftsInsert(row);
@@ -1060,6 +1156,13 @@ class MemDatabase {
1060
1156
  ORDER BY prompt_number ASC
1061
1157
  LIMIT ?`).all(sessionId, limit);
1062
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
+ }
1063
1166
  insertToolEvent(input) {
1064
1167
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1065
1168
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1169,8 +1272,15 @@ class MemDatabase {
1169
1272
  }
1170
1273
  insertSessionSummary(summary) {
1171
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
+ };
1172
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)
1173
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1174
1284
  const id = Number(result.lastInsertRowid);
1175
1285
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1176
1286
  }
@@ -2042,23 +2152,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
2042
2152
  return request ?? completed ?? "(no summary)";
2043
2153
  }
2044
2154
  function formatSummarySection(value, maxLen) {
2045
- if (!value)
2046
- return null;
2047
- const cleaned = value.split(`
2048
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
2049
- `);
2050
- if (!cleaned)
2051
- return null;
2052
- return truncateMultilineText(cleaned, maxLen);
2053
- }
2054
- function truncateMultilineText(text, maxLen) {
2055
- if (text.length <= maxLen)
2056
- return text;
2057
- const truncated = text.slice(0, maxLen).trimEnd();
2058
- const lastBreak = Math.max(truncated.lastIndexOf(`
2059
- `), truncated.lastIndexOf(" "));
2060
- const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
2061
- return `${safe.trimEnd()}…`;
2155
+ return formatSummaryItems(value, maxLen);
2062
2156
  }
2063
2157
  function truncateText(text, maxLen) {
2064
2158
  if (text.length <= maxLen)
@@ -2082,8 +2176,7 @@ function stripInlineSectionLabel(value) {
2082
2176
  function extractMeaningfulLines(value, limit) {
2083
2177
  if (!value)
2084
2178
  return [];
2085
- return value.split(`
2086
- `).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);
2087
2180
  }
2088
2181
  function formatObservationDetailFromContext(obs) {
2089
2182
  if (obs.facts) {
@@ -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) {
@@ -816,6 +910,7 @@ function openNodeDatabase(dbPath) {
816
910
  const BetterSqlite3 = __require("better-sqlite3");
817
911
  const raw = new BetterSqlite3(dbPath);
818
912
  return {
913
+ __raw: raw,
819
914
  query(sql) {
820
915
  const stmt = raw.prepare(sql);
821
916
  return {
@@ -853,7 +948,7 @@ class MemDatabase {
853
948
  loadVecExtension() {
854
949
  try {
855
950
  const sqliteVec = __require("sqlite-vec");
856
- sqliteVec.load(this.db);
951
+ sqliteVec.load(this.db.__raw ?? this.db);
857
952
  return true;
858
953
  } catch {
859
954
  return false;
@@ -894,8 +989,9 @@ class MemDatabase {
894
989
  const result = this.db.query(`INSERT INTO observations (
895
990
  session_id, project_id, type, title, narrative, facts, concepts,
896
991
  files_read, files_modified, quality, lifecycle, sensitivity,
897
- user_id, device_id, agent, created_at, created_at_epoch
898
- ) 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", createdAt, now);
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);
899
995
  const id = Number(result.lastInsertRowid);
900
996
  const row = this.getObservationById(id);
901
997
  this.ftsInsert(row);
@@ -1136,6 +1232,13 @@ class MemDatabase {
1136
1232
  ORDER BY prompt_number ASC
1137
1233
  LIMIT ?`).all(sessionId, limit);
1138
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
+ }
1139
1242
  insertToolEvent(input) {
1140
1243
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1141
1244
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1245,8 +1348,15 @@ class MemDatabase {
1245
1348
  }
1246
1349
  insertSessionSummary(summary) {
1247
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
+ };
1248
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)
1249
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1359
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1250
1360
  const id = Number(result.lastInsertRowid);
1251
1361
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1252
1362
  }