engrm 0.4.8 → 0.4.10
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/dist/cli.js +154 -14
- package/dist/hooks/elicitation-result.js +64 -4
- package/dist/hooks/post-tool-use.js +159 -33
- package/dist/hooks/pre-compact.js +193 -14
- package/dist/hooks/sentinel.js +62 -3
- package/dist/hooks/session-start.js +362 -61
- package/dist/hooks/stop.js +64 -4
- package/dist/hooks/user-prompt-submit.js +62 -3
- package/dist/server.js +883 -311
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { homedir as homedir3 } from "os";
|
|
|
11
11
|
// src/storage/projects.ts
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { basename, join } from "node:path";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
16
16
|
let url = remoteUrl.trim();
|
|
17
17
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -65,6 +65,19 @@ function getGitRemoteUrl(directory) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
function getGitTopLevel(directory) {
|
|
69
|
+
try {
|
|
70
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
71
|
+
cwd: directory,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
75
|
+
}).trim();
|
|
76
|
+
return root || null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
68
81
|
function readProjectConfigFile(directory) {
|
|
69
82
|
const configPath = join(directory, ".engrm.json");
|
|
70
83
|
if (!existsSync(configPath))
|
|
@@ -87,11 +100,12 @@ function detectProject(directory) {
|
|
|
87
100
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
88
101
|
if (remoteUrl) {
|
|
89
102
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
103
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
90
104
|
return {
|
|
91
105
|
canonical_id: canonicalId,
|
|
92
106
|
name: projectNameFromCanonicalId(canonicalId),
|
|
93
107
|
remote_url: remoteUrl,
|
|
94
|
-
local_path:
|
|
108
|
+
local_path: repoRoot
|
|
95
109
|
};
|
|
96
110
|
}
|
|
97
111
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -111,6 +125,32 @@ function detectProject(directory) {
|
|
|
111
125
|
local_path: directory
|
|
112
126
|
};
|
|
113
127
|
}
|
|
128
|
+
function detectProjectForPath(filePath, fallbackCwd) {
|
|
129
|
+
const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
|
|
130
|
+
const candidateDir = existsSync(absolutePath) && !absolutePath.endsWith("/") ? dirname(absolutePath) : dirname(absolutePath);
|
|
131
|
+
const detected = detectProject(candidateDir);
|
|
132
|
+
if (detected.canonical_id.startsWith("local/"))
|
|
133
|
+
return null;
|
|
134
|
+
return detected;
|
|
135
|
+
}
|
|
136
|
+
function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
137
|
+
const counts = new Map;
|
|
138
|
+
for (const rawPath of paths) {
|
|
139
|
+
if (!rawPath || !rawPath.trim())
|
|
140
|
+
continue;
|
|
141
|
+
const detected = detectProjectForPath(rawPath, fallbackCwd);
|
|
142
|
+
if (!detected)
|
|
143
|
+
continue;
|
|
144
|
+
const existing = counts.get(detected.canonical_id);
|
|
145
|
+
if (existing) {
|
|
146
|
+
existing.count += 1;
|
|
147
|
+
} else {
|
|
148
|
+
counts.set(detected.canonical_id, { project: detected, count: 1 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
|
|
152
|
+
return ranked[0]?.project ?? detectProject(fallbackCwd);
|
|
153
|
+
}
|
|
114
154
|
|
|
115
155
|
// src/capture/dedup.ts
|
|
116
156
|
function tokenise(text) {
|
|
@@ -356,6 +396,32 @@ function computeObservationPriority(obs, nowEpoch) {
|
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
// src/context/inject.ts
|
|
399
|
+
function tokenizeProjectHint(text) {
|
|
400
|
+
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
401
|
+
}
|
|
402
|
+
function isObservationRelatedToProject(obs, detected) {
|
|
403
|
+
const hints = new Set([
|
|
404
|
+
...tokenizeProjectHint(detected.name),
|
|
405
|
+
...tokenizeProjectHint(detected.canonical_id)
|
|
406
|
+
]);
|
|
407
|
+
if (hints.size === 0)
|
|
408
|
+
return false;
|
|
409
|
+
const haystack = [
|
|
410
|
+
obs.title,
|
|
411
|
+
obs.narrative ?? "",
|
|
412
|
+
obs.facts ?? "",
|
|
413
|
+
obs.concepts ?? "",
|
|
414
|
+
obs.files_read ?? "",
|
|
415
|
+
obs.files_modified ?? "",
|
|
416
|
+
obs._source_project ?? ""
|
|
417
|
+
].join(`
|
|
418
|
+
`).toLowerCase();
|
|
419
|
+
for (const hint of hints) {
|
|
420
|
+
if (haystack.includes(hint))
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
359
425
|
function estimateTokens(text) {
|
|
360
426
|
if (!text)
|
|
361
427
|
return 0;
|
|
@@ -431,6 +497,9 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
431
497
|
}
|
|
432
498
|
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
433
499
|
});
|
|
500
|
+
if (isNewProject) {
|
|
501
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
502
|
+
}
|
|
434
503
|
}
|
|
435
504
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
436
505
|
const dedupedRecent = recent.filter((o) => {
|
|
@@ -461,6 +530,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
461
530
|
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
462
531
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
463
532
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
533
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
464
534
|
return {
|
|
465
535
|
project_name: projectName,
|
|
466
536
|
canonical_id: canonicalId,
|
|
@@ -470,7 +540,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
470
540
|
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
471
541
|
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
472
542
|
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
473
|
-
projectTypeCounts: projectTypeCounts2
|
|
543
|
+
projectTypeCounts: projectTypeCounts2,
|
|
544
|
+
recentOutcomes: recentOutcomes2
|
|
474
545
|
};
|
|
475
546
|
}
|
|
476
547
|
let remainingBudget = tokenBudget - 30;
|
|
@@ -497,6 +568,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
497
568
|
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
498
569
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
499
570
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
571
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
500
572
|
let securityFindings = [];
|
|
501
573
|
if (!isNewProject) {
|
|
502
574
|
try {
|
|
@@ -554,7 +626,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
554
626
|
recentPrompts: recentPrompts.length > 0 ? recentPrompts : undefined,
|
|
555
627
|
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
556
628
|
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
557
|
-
projectTypeCounts
|
|
629
|
+
projectTypeCounts,
|
|
630
|
+
recentOutcomes
|
|
558
631
|
};
|
|
559
632
|
}
|
|
560
633
|
function estimateObservationTokens(obs, index) {
|
|
@@ -591,12 +664,15 @@ function formatContextForInjection(context) {
|
|
|
591
664
|
lines.push("");
|
|
592
665
|
}
|
|
593
666
|
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
667
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
668
|
+
if (promptLines.length > 0) {
|
|
669
|
+
lines.push("## Recent Requests");
|
|
670
|
+
for (const prompt of promptLines) {
|
|
671
|
+
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
672
|
+
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
673
|
+
}
|
|
674
|
+
lines.push("");
|
|
598
675
|
}
|
|
599
|
-
lines.push("");
|
|
600
676
|
}
|
|
601
677
|
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
602
678
|
lines.push("## Recent Tools");
|
|
@@ -606,10 +682,22 @@ function formatContextForInjection(context) {
|
|
|
606
682
|
lines.push("");
|
|
607
683
|
}
|
|
608
684
|
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
685
|
+
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
686
|
+
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
687
|
+
if (summary === "(no summary)")
|
|
688
|
+
return null;
|
|
689
|
+
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
690
|
+
}).filter((line) => Boolean(line));
|
|
691
|
+
if (recentSessionLines.length > 0) {
|
|
692
|
+
lines.push("## Recent Sessions");
|
|
693
|
+
lines.push(...recentSessionLines);
|
|
694
|
+
lines.push("");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
698
|
+
lines.push("## Recent Outcomes");
|
|
699
|
+
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
700
|
+
lines.push(`- ${truncateText(outcome, 160)}`);
|
|
613
701
|
}
|
|
614
702
|
lines.push("");
|
|
615
703
|
}
|
|
@@ -689,6 +777,14 @@ function formatSessionBrief(summary) {
|
|
|
689
777
|
}
|
|
690
778
|
return lines;
|
|
691
779
|
}
|
|
780
|
+
function chooseMeaningfulSessionHeadline(request, completed) {
|
|
781
|
+
if (request && !looksLikeFileOperationTitle(request))
|
|
782
|
+
return request;
|
|
783
|
+
const completedItems = extractMeaningfulLines(completed, 1);
|
|
784
|
+
if (completedItems.length > 0)
|
|
785
|
+
return completedItems[0];
|
|
786
|
+
return request ?? completed ?? "(no summary)";
|
|
787
|
+
}
|
|
692
788
|
function formatSummarySection(value, maxLen) {
|
|
693
789
|
if (!value)
|
|
694
790
|
return null;
|
|
@@ -713,6 +809,26 @@ function truncateText(text, maxLen) {
|
|
|
713
809
|
return text;
|
|
714
810
|
return text.slice(0, maxLen - 3) + "...";
|
|
715
811
|
}
|
|
812
|
+
function isMeaningfulPrompt(value) {
|
|
813
|
+
if (!value)
|
|
814
|
+
return false;
|
|
815
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
816
|
+
if (compact.length < 8)
|
|
817
|
+
return false;
|
|
818
|
+
return /[a-z]{3,}/i.test(compact);
|
|
819
|
+
}
|
|
820
|
+
function looksLikeFileOperationTitle(value) {
|
|
821
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
822
|
+
}
|
|
823
|
+
function stripInlineSectionLabel(value) {
|
|
824
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
825
|
+
}
|
|
826
|
+
function extractMeaningfulLines(value, limit) {
|
|
827
|
+
if (!value)
|
|
828
|
+
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);
|
|
831
|
+
}
|
|
716
832
|
function formatObservationDetailFromContext(obs) {
|
|
717
833
|
if (obs.facts) {
|
|
718
834
|
const bullets = parseFacts(obs.facts);
|
|
@@ -812,6 +928,50 @@ function getProjectTypeCounts(db, projectId, userId) {
|
|
|
812
928
|
}
|
|
813
929
|
return counts;
|
|
814
930
|
}
|
|
931
|
+
function getRecentOutcomes(db, projectId, userId) {
|
|
932
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
933
|
+
const visibilityParams = userId ? [userId] : [];
|
|
934
|
+
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
935
|
+
WHERE project_id = ?
|
|
936
|
+
ORDER BY created_at_epoch DESC
|
|
937
|
+
LIMIT 6`).all(projectId);
|
|
938
|
+
const picked = [];
|
|
939
|
+
const seen = new Set;
|
|
940
|
+
for (const summary of summaries) {
|
|
941
|
+
for (const line of [
|
|
942
|
+
...extractMeaningfulLines(summary.completed, 2),
|
|
943
|
+
...extractMeaningfulLines(summary.learned, 1)
|
|
944
|
+
]) {
|
|
945
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
946
|
+
if (!normalized || seen.has(normalized))
|
|
947
|
+
continue;
|
|
948
|
+
seen.add(normalized);
|
|
949
|
+
picked.push(line);
|
|
950
|
+
if (picked.length >= 5)
|
|
951
|
+
return picked;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const rows = db.db.query(`SELECT * FROM observations
|
|
955
|
+
WHERE project_id = ?
|
|
956
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
957
|
+
AND superseded_by IS NULL
|
|
958
|
+
${visibilityClause}
|
|
959
|
+
ORDER BY created_at_epoch DESC
|
|
960
|
+
LIMIT 20`).all(projectId, ...visibilityParams);
|
|
961
|
+
for (const obs of rows) {
|
|
962
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
963
|
+
continue;
|
|
964
|
+
const title = stripInlineSectionLabel(obs.title);
|
|
965
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
966
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle(title))
|
|
967
|
+
continue;
|
|
968
|
+
seen.add(normalized);
|
|
969
|
+
picked.push(title);
|
|
970
|
+
if (picked.length >= 5)
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
return picked;
|
|
974
|
+
}
|
|
815
975
|
|
|
816
976
|
// src/telemetry/stack-detect.ts
|
|
817
977
|
import { existsSync as existsSync2 } from "node:fs";
|
|
@@ -1003,7 +1163,7 @@ function computeAndSaveFingerprint(cwd) {
|
|
|
1003
1163
|
|
|
1004
1164
|
// src/packs/recommender.ts
|
|
1005
1165
|
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1006
|
-
import { join as join4, basename as basename3, dirname } from "node:path";
|
|
1166
|
+
import { join as join4, basename as basename3, dirname as dirname2 } from "node:path";
|
|
1007
1167
|
import { fileURLToPath } from "node:url";
|
|
1008
1168
|
var STACK_PACK_MAP = {
|
|
1009
1169
|
typescript: ["typescript-patterns"],
|
|
@@ -1015,7 +1175,7 @@ var STACK_PACK_MAP = {
|
|
|
1015
1175
|
bun: ["node-security"]
|
|
1016
1176
|
};
|
|
1017
1177
|
function getPacksDir() {
|
|
1018
|
-
const thisDir =
|
|
1178
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
1019
1179
|
return join4(thisDir, "../../packs");
|
|
1020
1180
|
}
|
|
1021
1181
|
function listAvailablePacks() {
|
|
@@ -1420,10 +1580,15 @@ function mergeChanges(db, config, changes) {
|
|
|
1420
1580
|
name: change.metadata?.project_name ?? projectCanonical.split("/").pop() ?? "unknown"
|
|
1421
1581
|
});
|
|
1422
1582
|
}
|
|
1583
|
+
const normalizedType = normalizeRemoteObservationType(change.metadata?.type, change.source_id);
|
|
1584
|
+
if (!normalizedType) {
|
|
1585
|
+
skipped++;
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1423
1588
|
const obs = db.insertObservation({
|
|
1424
1589
|
session_id: change.metadata?.session_id ?? null,
|
|
1425
1590
|
project_id: project.id,
|
|
1426
|
-
type:
|
|
1591
|
+
type: normalizedType,
|
|
1427
1592
|
title: change.metadata?.title ?? change.content.split(`
|
|
1428
1593
|
`)[0] ?? "Untitled",
|
|
1429
1594
|
narrative: extractNarrative(change.content),
|
|
@@ -1446,6 +1611,23 @@ function mergeChanges(db, config, changes) {
|
|
|
1446
1611
|
}
|
|
1447
1612
|
return { merged, skipped };
|
|
1448
1613
|
}
|
|
1614
|
+
function normalizeRemoteObservationType(rawType, sourceId) {
|
|
1615
|
+
const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
|
|
1616
|
+
if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
|
|
1617
|
+
return type;
|
|
1618
|
+
}
|
|
1619
|
+
if (type === "summary") {
|
|
1620
|
+
return "digest";
|
|
1621
|
+
}
|
|
1622
|
+
if (!type) {
|
|
1623
|
+
if (sourceId.includes("-summary-"))
|
|
1624
|
+
return "digest";
|
|
1625
|
+
if (sourceId.includes("-message-"))
|
|
1626
|
+
return "message";
|
|
1627
|
+
return "standard";
|
|
1628
|
+
}
|
|
1629
|
+
return "standard";
|
|
1630
|
+
}
|
|
1449
1631
|
async function embedAndInsert(db, obs) {
|
|
1450
1632
|
const text = composeEmbeddingText(obs);
|
|
1451
1633
|
const embedding = await embedText(text);
|
|
@@ -2062,6 +2244,11 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
2062
2244
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
2063
2245
|
function openDatabase(dbPath) {
|
|
2064
2246
|
if (IS_BUN) {
|
|
2247
|
+
if (process.platform === "darwin") {
|
|
2248
|
+
try {
|
|
2249
|
+
return openNodeDatabase(dbPath);
|
|
2250
|
+
} catch {}
|
|
2251
|
+
}
|
|
2065
2252
|
return openBunDatabase(dbPath);
|
|
2066
2253
|
}
|
|
2067
2254
|
return openNodeDatabase(dbPath);
|
|
@@ -2178,6 +2365,15 @@ class MemDatabase {
|
|
|
2178
2365
|
}
|
|
2179
2366
|
return row;
|
|
2180
2367
|
}
|
|
2368
|
+
reassignObservationProject(observationId, projectId) {
|
|
2369
|
+
const existing = this.getObservationById(observationId);
|
|
2370
|
+
if (!existing)
|
|
2371
|
+
return false;
|
|
2372
|
+
if (existing.project_id === projectId)
|
|
2373
|
+
return true;
|
|
2374
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2181
2377
|
getObservationById(id) {
|
|
2182
2378
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
2183
2379
|
}
|
|
@@ -2311,8 +2507,13 @@ class MemDatabase {
|
|
|
2311
2507
|
}
|
|
2312
2508
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
2313
2509
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
2314
|
-
if (existing)
|
|
2510
|
+
if (existing) {
|
|
2511
|
+
if (existing.project_id === null && projectId !== null) {
|
|
2512
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
2513
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
2514
|
+
}
|
|
2315
2515
|
return existing;
|
|
2516
|
+
}
|
|
2316
2517
|
const now = Math.floor(Date.now() / 1000);
|
|
2317
2518
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
2318
2519
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|
|
@@ -2792,7 +2993,7 @@ function formatSplashScreen(data) {
|
|
|
2792
2993
|
const brief = formatVisibleStartupBrief(data.context);
|
|
2793
2994
|
if (brief.length > 0) {
|
|
2794
2995
|
lines.push("");
|
|
2795
|
-
lines.push(` ${c2.bold}Startup
|
|
2996
|
+
lines.push(` ${c2.bold}Startup context${c2.reset}`);
|
|
2796
2997
|
for (const line of brief) {
|
|
2797
2998
|
lines.push(` ${line}`);
|
|
2798
2999
|
}
|
|
@@ -2803,15 +3004,25 @@ function formatSplashScreen(data) {
|
|
|
2803
3004
|
}
|
|
2804
3005
|
function formatVisibleStartupBrief(context) {
|
|
2805
3006
|
const lines = [];
|
|
2806
|
-
const latest =
|
|
3007
|
+
const latest = pickPrimarySummary(context);
|
|
2807
3008
|
const observationFallbacks = buildObservationFallbacks(context);
|
|
2808
3009
|
const promptFallback = buildPromptFallback(context);
|
|
3010
|
+
const promptLines = buildPromptLines(context);
|
|
3011
|
+
const latestPromptLine = promptLines[0] ?? null;
|
|
3012
|
+
const currentRequest = latest ? chooseRequest(latest.request, promptFallback ?? sessionFallbacksFromContext(context)[0] ?? observationFallbacks.request) : promptFallback;
|
|
2809
3013
|
const toolFallbacks = buildToolFallbacks(context);
|
|
2810
|
-
const sessionFallbacks =
|
|
3014
|
+
const sessionFallbacks = sessionFallbacksFromContext(context);
|
|
3015
|
+
const recentOutcomeLines = buildRecentOutcomeLines(context, latest);
|
|
2811
3016
|
const projectSignals = buildProjectSignalLine(context);
|
|
3017
|
+
if (promptLines.length > 0) {
|
|
3018
|
+
lines.push(`${c2.cyan}Recent Requests:${c2.reset}`);
|
|
3019
|
+
for (const item of promptLines) {
|
|
3020
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2812
3023
|
if (latest) {
|
|
2813
3024
|
const sections = [
|
|
2814
|
-
["Request",
|
|
3025
|
+
["Request", currentRequest, 1],
|
|
2815
3026
|
["Investigated", chooseSection(latest.investigated, observationFallbacks.investigated, "Investigated"), 2],
|
|
2816
3027
|
["Learned", latest.learned, 2],
|
|
2817
3028
|
["Completed", chooseSection(latest.completed, observationFallbacks.completed, "Completed"), 2],
|
|
@@ -2826,58 +3037,131 @@ function formatVisibleStartupBrief(context) {
|
|
|
2826
3037
|
}
|
|
2827
3038
|
}
|
|
2828
3039
|
}
|
|
2829
|
-
} else if (
|
|
2830
|
-
lines.push(`${c2.cyan}Request:${c2.reset}`);
|
|
2831
|
-
lines.push(` - ${truncateInline(
|
|
3040
|
+
} else if (currentRequest && !duplicatesPromptLine(currentRequest, latestPromptLine)) {
|
|
3041
|
+
lines.push(`${c2.cyan}Current Request:${c2.reset}`);
|
|
3042
|
+
lines.push(` - ${truncateInline(currentRequest, 160)}`);
|
|
2832
3043
|
if (toolFallbacks.length > 0) {
|
|
2833
3044
|
lines.push(`${c2.cyan}Recent Tools:${c2.reset}`);
|
|
2834
3045
|
for (const item of toolFallbacks) {
|
|
2835
|
-
lines.push(` - ${truncateInline(item,
|
|
3046
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
2836
3047
|
}
|
|
2837
3048
|
}
|
|
2838
3049
|
}
|
|
3050
|
+
if (latest && currentRequest && !hasRequestSection(lines) && !duplicatesPromptLine(currentRequest, latestPromptLine)) {
|
|
3051
|
+
lines.push(`${c2.cyan}Current Request:${c2.reset}`);
|
|
3052
|
+
lines.push(` - ${truncateInline(currentRequest, 160)}`);
|
|
3053
|
+
}
|
|
3054
|
+
if (recentOutcomeLines.length > 0) {
|
|
3055
|
+
lines.push(`${c2.cyan}Recent Work:${c2.reset}`);
|
|
3056
|
+
for (const item of recentOutcomeLines) {
|
|
3057
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
if (toolFallbacks.length > 0 && latest) {
|
|
3061
|
+
lines.push(`${c2.cyan}Recent Tools:${c2.reset}`);
|
|
3062
|
+
for (const item of toolFallbacks) {
|
|
3063
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
2839
3066
|
if (sessionFallbacks.length > 0) {
|
|
2840
3067
|
lines.push(`${c2.cyan}Recent Sessions:${c2.reset}`);
|
|
2841
3068
|
for (const item of sessionFallbacks) {
|
|
2842
|
-
lines.push(` - ${truncateInline(item,
|
|
3069
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
2843
3070
|
}
|
|
2844
3071
|
}
|
|
2845
3072
|
if (projectSignals) {
|
|
2846
3073
|
lines.push(`${c2.cyan}Project Signals:${c2.reset}`);
|
|
2847
|
-
lines.push(` - ${truncateInline(projectSignals,
|
|
3074
|
+
lines.push(` - ${truncateInline(projectSignals, 160)}`);
|
|
2848
3075
|
}
|
|
2849
3076
|
const stale = pickRelevantStaleDecision(context, latest);
|
|
2850
3077
|
if (stale) {
|
|
2851
3078
|
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
2852
3079
|
}
|
|
2853
3080
|
if (lines.length === 0 && context.observations.length > 0) {
|
|
2854
|
-
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => !
|
|
3081
|
+
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !looksLikeFileOperationTitle2(obs.title)).slice(0, 3);
|
|
2855
3082
|
for (const obs of top) {
|
|
2856
3083
|
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
2857
3084
|
}
|
|
2858
3085
|
}
|
|
2859
|
-
return lines.slice(0,
|
|
3086
|
+
return lines.slice(0, 14);
|
|
2860
3087
|
}
|
|
2861
3088
|
function buildPromptFallback(context) {
|
|
2862
|
-
const latest = context.recentPrompts
|
|
3089
|
+
const latest = (context.recentPrompts ?? []).find((prompt) => isMeaningfulPrompt2(prompt.prompt));
|
|
2863
3090
|
if (!latest?.prompt)
|
|
2864
3091
|
return null;
|
|
2865
3092
|
return latest.prompt.replace(/\s+/g, " ").trim();
|
|
2866
3093
|
}
|
|
3094
|
+
function buildPromptLines(context) {
|
|
3095
|
+
return (context.recentPrompts ?? []).filter((prompt) => isMeaningfulPrompt2(prompt.prompt)).slice(0, 2).map((prompt) => {
|
|
3096
|
+
const prefix = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : "request";
|
|
3097
|
+
return `${prefix}: ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
|
|
3098
|
+
}).filter((item) => item.length > 0);
|
|
3099
|
+
}
|
|
3100
|
+
function duplicatesPromptLine(request, promptLine) {
|
|
3101
|
+
if (!request || !promptLine)
|
|
3102
|
+
return false;
|
|
3103
|
+
const promptBody = promptLine.replace(/^#?\d+:\s*/, "").trim();
|
|
3104
|
+
return normalizeStartupItem(request) === normalizeStartupItem(promptBody);
|
|
3105
|
+
}
|
|
2867
3106
|
function buildToolFallbacks(context) {
|
|
2868
3107
|
return (context.recentToolEvents ?? []).slice(0, 3).map((tool) => {
|
|
2869
3108
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
2870
|
-
return `${tool.tool_name}
|
|
3109
|
+
return `${tool.tool_name}${detail ? `: ${detail}` : ""}`.trim();
|
|
2871
3110
|
}).filter((item) => item.length > 0);
|
|
2872
3111
|
}
|
|
2873
|
-
function
|
|
3112
|
+
function sessionFallbacksFromContext(context) {
|
|
2874
3113
|
return (context.recentSessions ?? []).slice(0, 2).map((session) => {
|
|
2875
|
-
const summary = session.request
|
|
3114
|
+
const summary = chooseMeaningfulSessionSummary(session.request, session.completed);
|
|
2876
3115
|
if (!summary)
|
|
2877
3116
|
return "";
|
|
2878
|
-
return `${session.session_id}: ${summary}`;
|
|
3117
|
+
return `${session.session_id}: ${summary} (prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
2879
3118
|
}).filter((item) => item.length > 0);
|
|
2880
3119
|
}
|
|
3120
|
+
function buildRecentOutcomeLines(context, summary) {
|
|
3121
|
+
const picked = [];
|
|
3122
|
+
const seen = new Set;
|
|
3123
|
+
const push = (value) => {
|
|
3124
|
+
for (const line of toSplashLines(value ?? null, 2)) {
|
|
3125
|
+
const normalized = normalizeStartupItem(line);
|
|
3126
|
+
if (!normalized || seen.has(normalized))
|
|
3127
|
+
continue;
|
|
3128
|
+
seen.add(normalized);
|
|
3129
|
+
picked.push(line.replace(/^-\s*/, ""));
|
|
3130
|
+
if (picked.length >= 2)
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
push(summary?.completed);
|
|
3135
|
+
push(summary?.learned);
|
|
3136
|
+
if (picked.length < 2) {
|
|
3137
|
+
for (const obs of context.observations) {
|
|
3138
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
3139
|
+
continue;
|
|
3140
|
+
const title = stripInlineSectionLabel2(obs.title);
|
|
3141
|
+
if (!title || looksLikeFileOperationTitle2(title))
|
|
3142
|
+
continue;
|
|
3143
|
+
const normalized = normalizeStartupItem(title);
|
|
3144
|
+
if (!normalized || seen.has(normalized))
|
|
3145
|
+
continue;
|
|
3146
|
+
seen.add(normalized);
|
|
3147
|
+
picked.push(title);
|
|
3148
|
+
if (picked.length >= 2)
|
|
3149
|
+
break;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return picked;
|
|
3153
|
+
}
|
|
3154
|
+
function chooseMeaningfulSessionSummary(request, completed) {
|
|
3155
|
+
if (request && !looksLikeFileOperationTitle2(request))
|
|
3156
|
+
return request;
|
|
3157
|
+
if (completed) {
|
|
3158
|
+
const lines = completed.split(`
|
|
3159
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) => stripInlineSectionLabel2(line)).filter((line) => !looksLikeFileOperationTitle2(line));
|
|
3160
|
+
if (lines.length > 0)
|
|
3161
|
+
return lines[0] ?? null;
|
|
3162
|
+
}
|
|
3163
|
+
return request ?? completed ?? null;
|
|
3164
|
+
}
|
|
2881
3165
|
function buildProjectSignalLine(context) {
|
|
2882
3166
|
if (!context.projectTypeCounts)
|
|
2883
3167
|
return null;
|
|
@@ -2888,29 +3172,26 @@ function toSplashLines(value, maxItems) {
|
|
|
2888
3172
|
if (!value)
|
|
2889
3173
|
return [];
|
|
2890
3174
|
const lines = value.split(`
|
|
2891
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) =>
|
|
3175
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel2(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
|
|
2892
3176
|
return dedupeFragmentsInLines(lines);
|
|
2893
3177
|
}
|
|
2894
|
-
function
|
|
3178
|
+
function pickPrimarySummary(context) {
|
|
2895
3179
|
const summaries = context.summaries || [];
|
|
2896
3180
|
if (!summaries.length)
|
|
2897
3181
|
return null;
|
|
2898
|
-
|
|
3182
|
+
const meaningfulRecent = summaries.find((summary) => {
|
|
3183
|
+
const request = summary.request?.trim();
|
|
3184
|
+
const learned = summary.learned?.trim();
|
|
3185
|
+
const completed = summary.completed?.trim();
|
|
3186
|
+
return Boolean(request && !looksLikeFileOperationTitle2(request) || learned || hasMeaningfulCompleted(completed));
|
|
3187
|
+
});
|
|
3188
|
+
return meaningfulRecent ?? summaries[0] ?? null;
|
|
2899
3189
|
}
|
|
2900
|
-
function
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
score += 4;
|
|
2906
|
-
if (summary.learned)
|
|
2907
|
-
score += 5;
|
|
2908
|
-
if (summary.completed)
|
|
2909
|
-
score += 5;
|
|
2910
|
-
if (summary.next_steps)
|
|
2911
|
-
score += 4;
|
|
2912
|
-
score += Math.min(4, sectionItemCount(summary.completed) + sectionItemCount(summary.learned));
|
|
2913
|
-
return score;
|
|
3190
|
+
function hasMeaningfulCompleted(value) {
|
|
3191
|
+
if (!value)
|
|
3192
|
+
return false;
|
|
3193
|
+
return value.split(`
|
|
3194
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !looksLikeFileOperationTitle2(stripInlineSectionLabel2(line)));
|
|
2914
3195
|
}
|
|
2915
3196
|
function sectionItemCount(value) {
|
|
2916
3197
|
if (!value)
|
|
@@ -2935,7 +3216,7 @@ function dedupeFragmentsInLines(lines) {
|
|
|
2935
3216
|
const seen = new Set;
|
|
2936
3217
|
const deduped = [];
|
|
2937
3218
|
for (const line of lines) {
|
|
2938
|
-
const normalized =
|
|
3219
|
+
const normalized = stripInlineSectionLabel2(line).toLowerCase().replace(/\s+/g, " ").trim();
|
|
2939
3220
|
if (!normalized || seen.has(normalized))
|
|
2940
3221
|
continue;
|
|
2941
3222
|
seen.add(normalized);
|
|
@@ -2943,8 +3224,22 @@ function dedupeFragmentsInLines(lines) {
|
|
|
2943
3224
|
}
|
|
2944
3225
|
return deduped;
|
|
2945
3226
|
}
|
|
3227
|
+
function hasRequestSection(lines) {
|
|
3228
|
+
return lines.some((line) => line.includes("Request:"));
|
|
3229
|
+
}
|
|
3230
|
+
function normalizeStartupItem(value) {
|
|
3231
|
+
return stripInlineSectionLabel2(value).replace(/^#?\d+:\s*/, "").replace(/^-\s*/, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
3232
|
+
}
|
|
3233
|
+
function isMeaningfulPrompt2(value) {
|
|
3234
|
+
if (!value)
|
|
3235
|
+
return false;
|
|
3236
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
3237
|
+
if (compact.length < 8)
|
|
3238
|
+
return false;
|
|
3239
|
+
return /[a-z]{3,}/i.test(compact);
|
|
3240
|
+
}
|
|
2946
3241
|
function chooseRequest(primary, fallback) {
|
|
2947
|
-
if (primary && !
|
|
3242
|
+
if (primary && !looksLikeFileOperationTitle2(primary))
|
|
2948
3243
|
return primary;
|
|
2949
3244
|
return fallback;
|
|
2950
3245
|
}
|
|
@@ -2962,15 +3257,15 @@ function isWeakCompletedSection(value) {
|
|
|
2962
3257
|
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean);
|
|
2963
3258
|
if (!items.length)
|
|
2964
3259
|
return true;
|
|
2965
|
-
const weakCount = items.filter((item) =>
|
|
3260
|
+
const weakCount = items.filter((item) => looksLikeFileOperationTitle2(item)).length;
|
|
2966
3261
|
return weakCount === items.length;
|
|
2967
3262
|
}
|
|
2968
|
-
function
|
|
3263
|
+
function looksLikeFileOperationTitle2(value) {
|
|
2969
3264
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2970
3265
|
}
|
|
2971
3266
|
function scoreSplashLine(value) {
|
|
2972
3267
|
let score = 0;
|
|
2973
|
-
if (!
|
|
3268
|
+
if (!looksLikeFileOperationTitle2(value))
|
|
2974
3269
|
score += 2;
|
|
2975
3270
|
if (/[:;]/.test(value))
|
|
2976
3271
|
score += 1;
|
|
@@ -2979,9 +3274,9 @@ function scoreSplashLine(value) {
|
|
|
2979
3274
|
return score;
|
|
2980
3275
|
}
|
|
2981
3276
|
function buildObservationFallbacks(context) {
|
|
2982
|
-
const request = context.observations.find((obs) => !
|
|
3277
|
+
const request = context.observations.find((obs) => obs.type !== "decision" && !looksLikeFileOperationTitle2(obs.title))?.title ?? null;
|
|
2983
3278
|
const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
|
|
2984
|
-
const completed = collectObservationTitles(context, (obs) => ["feature", "refactor", "change"].includes(obs.type) && !
|
|
3279
|
+
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !looksLikeFileOperationTitle2(obs.title), 2);
|
|
2985
3280
|
return {
|
|
2986
3281
|
request,
|
|
2987
3282
|
investigated,
|
|
@@ -2994,18 +3289,18 @@ function collectObservationTitles(context, predicate, limit) {
|
|
|
2994
3289
|
for (const obs of context.observations) {
|
|
2995
3290
|
if (!predicate(obs))
|
|
2996
3291
|
continue;
|
|
2997
|
-
const normalized =
|
|
3292
|
+
const normalized = stripInlineSectionLabel2(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
|
|
2998
3293
|
if (!normalized || seen.has(normalized))
|
|
2999
3294
|
continue;
|
|
3000
3295
|
seen.add(normalized);
|
|
3001
|
-
picked.push(`- ${
|
|
3296
|
+
picked.push(`- ${stripInlineSectionLabel2(obs.title)}`);
|
|
3002
3297
|
if (picked.length >= limit)
|
|
3003
3298
|
break;
|
|
3004
3299
|
}
|
|
3005
3300
|
return picked.length ? picked.join(`
|
|
3006
3301
|
`) : null;
|
|
3007
3302
|
}
|
|
3008
|
-
function
|
|
3303
|
+
function stripInlineSectionLabel2(value) {
|
|
3009
3304
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
3010
3305
|
}
|
|
3011
3306
|
function pickRelevantStaleDecision(context, summary) {
|
|
@@ -3094,4 +3389,10 @@ function capitalize(value) {
|
|
|
3094
3389
|
return value;
|
|
3095
3390
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3096
3391
|
}
|
|
3392
|
+
var __testables = {
|
|
3393
|
+
formatVisibleStartupBrief
|
|
3394
|
+
};
|
|
3097
3395
|
runHook("session-start", main);
|
|
3396
|
+
export {
|
|
3397
|
+
__testables
|
|
3398
|
+
};
|