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
|
@@ -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.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,
|
|
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,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);
|