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.
@@ -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
- if (!value)
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.split(`
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.0";
1157
+ var CLIENT_VERSION = "0.4.14";
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, created_at, created_at_epoch
2410
- ) 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);
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, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
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,34 @@ 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 legend = formatLegend();
3151
+ if (legend.length > 0) {
3152
+ lines.push("");
3153
+ for (const line of legend) {
3154
+ lines.push(` ${line}`);
3155
+ }
3156
+ }
3157
+ const contextIndex = formatContextIndex(data.context);
3158
+ if (contextIndex.length > 0) {
3159
+ lines.push("");
3160
+ for (const line of contextIndex) {
3161
+ lines.push(` ${line}`);
3162
+ }
3163
+ }
3164
+ const inspectHints = formatInspectHints(data.context);
3165
+ if (inspectHints.length > 0) {
3166
+ lines.push("");
3167
+ for (const line of inspectHints) {
3168
+ lines.push(` ${line}`);
3169
+ }
3170
+ }
3052
3171
  lines.push("");
3053
3172
  return lines.join(`
3054
3173
  `);
@@ -3157,6 +3276,62 @@ function formatVisibleStartupBrief(context) {
3157
3276
  }
3158
3277
  return lines.slice(0, 14);
3159
3278
  }
3279
+ function formatContextEconomics(data) {
3280
+ const totalMemories = Math.max(0, data.loaded + data.available);
3281
+ const parts = [];
3282
+ if (totalMemories > 0) {
3283
+ parts.push(`${totalMemories.toLocaleString()} total memories`);
3284
+ }
3285
+ if (data.estimatedReadTokens > 0) {
3286
+ parts.push(`read now ~${data.estimatedReadTokens.toLocaleString()}t`);
3287
+ }
3288
+ if (data.context.observations.length > 0) {
3289
+ parts.push(`${data.context.observations.length} observations loaded`);
3290
+ }
3291
+ if (parts.length === 0)
3292
+ return [];
3293
+ return [`${c2.dim}Context economics:${c2.reset} ${parts.join(" \xB7 ")}`];
3294
+ }
3295
+ function formatLegend() {
3296
+ return [
3297
+ `${c2.dim}Legend:${c2.reset} #id | \uD83D\uDD34 bugfix | \uD83D\uDFE3 feature | \uD83D\uDD04 refactor | \u2705 change | \uD83D\uDD35 discovery | \u2696\uFE0F decision`
3298
+ ];
3299
+ }
3300
+ function formatContextIndex(context) {
3301
+ const rows = context.observations.filter((obs) => obs.type !== "digest").slice(0, 6).map((obs) => {
3302
+ const icon = observationIcon(obs.type);
3303
+ const fileHint = extractPrimaryFileHint(obs);
3304
+ return `${icon} #${obs.id} ${truncateInline(obs.title, 110)}${fileHint ? ` ${c2.dim}(${fileHint})${c2.reset}` : ""}`;
3305
+ });
3306
+ if (rows.length === 0)
3307
+ return [];
3308
+ return [
3309
+ `${c2.dim}Context index:${c2.reset} use IDs to fetch deeper detail when needed`,
3310
+ ...rows
3311
+ ];
3312
+ }
3313
+ function formatInspectHints(context) {
3314
+ const hints = [];
3315
+ if ((context.recentSessions?.length ?? 0) > 0) {
3316
+ hints.push("recent_sessions");
3317
+ hints.push("session_story");
3318
+ }
3319
+ if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
3320
+ hints.push("activity_feed");
3321
+ }
3322
+ if (context.observations.length > 0) {
3323
+ hints.push("memory_console");
3324
+ }
3325
+ const unique = Array.from(new Set(hints)).slice(0, 4);
3326
+ if (unique.length === 0)
3327
+ return [];
3328
+ const ids = context.observations.slice(0, 5).map((obs) => obs.id);
3329
+ const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
3330
+ return [
3331
+ `${c2.dim}Inspect:${c2.reset} ${unique.join(" \xB7 ")}`,
3332
+ ...fetchHint ? [`${c2.dim}Fetch by ID:${c2.reset} ${fetchHint}`] : []
3333
+ ];
3334
+ }
3160
3335
  function rememberShownItem(shown, value) {
3161
3336
  if (!value)
3162
3337
  return;
@@ -3280,6 +3455,39 @@ function buildProjectSignalLine(context) {
3280
3455
  const top = Object.entries(context.projectTypeCounts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4).map(([type, count]) => `${type} ${count}`).join("; ");
3281
3456
  return top || null;
3282
3457
  }
3458
+ function observationIcon(type) {
3459
+ switch (type) {
3460
+ case "bugfix":
3461
+ return "\uD83D\uDD34";
3462
+ case "feature":
3463
+ return "\uD83D\uDFE3";
3464
+ case "refactor":
3465
+ return "\uD83D\uDD04";
3466
+ case "change":
3467
+ return "\u2705";
3468
+ case "discovery":
3469
+ return "\uD83D\uDD35";
3470
+ case "decision":
3471
+ return "\u2696\uFE0F";
3472
+ default:
3473
+ return "\u2022";
3474
+ }
3475
+ }
3476
+ function extractPrimaryFileHint(obs) {
3477
+ const firstRead = parseJsonArraySafe(obs.files_read)[0];
3478
+ const firstModified = parseJsonArraySafe(obs.files_modified)[0];
3479
+ return firstModified ?? firstRead ?? null;
3480
+ }
3481
+ function parseJsonArraySafe(value) {
3482
+ if (!value)
3483
+ return [];
3484
+ try {
3485
+ const parsed = JSON.parse(value);
3486
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3487
+ } catch {
3488
+ return [];
3489
+ }
3490
+ }
3283
3491
  function toSplashLines(value, maxItems) {
3284
3492
  if (!value)
3285
3493
  return [];
@@ -3502,6 +3710,7 @@ function capitalize(value) {
3502
3710
  return value.charAt(0).toUpperCase() + value.slice(1);
3503
3711
  }
3504
3712
  var __testables = {
3713
+ formatSplashScreen,
3505
3714
  formatVisibleStartupBrief
3506
3715
  };
3507
3716
  runHook("session-start", main);