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
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync } from "fs";
|
|
22
22
|
import { hostname as hostname2, homedir as homedir4, networkInterfaces as networkInterfaces2 } from "os";
|
|
23
|
-
import { dirname as
|
|
23
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
24
24
|
import { createHash as createHash3 } from "crypto";
|
|
25
25
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
26
26
|
|
|
@@ -689,6 +689,11 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
689
689
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
690
690
|
function openDatabase(dbPath) {
|
|
691
691
|
if (IS_BUN) {
|
|
692
|
+
if (process.platform === "darwin") {
|
|
693
|
+
try {
|
|
694
|
+
return openNodeDatabase(dbPath);
|
|
695
|
+
} catch {}
|
|
696
|
+
}
|
|
692
697
|
return openBunDatabase(dbPath);
|
|
693
698
|
}
|
|
694
699
|
return openNodeDatabase(dbPath);
|
|
@@ -805,6 +810,15 @@ class MemDatabase {
|
|
|
805
810
|
}
|
|
806
811
|
return row;
|
|
807
812
|
}
|
|
813
|
+
reassignObservationProject(observationId, projectId) {
|
|
814
|
+
const existing = this.getObservationById(observationId);
|
|
815
|
+
if (!existing)
|
|
816
|
+
return false;
|
|
817
|
+
if (existing.project_id === projectId)
|
|
818
|
+
return true;
|
|
819
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
808
822
|
getObservationById(id) {
|
|
809
823
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
810
824
|
}
|
|
@@ -938,8 +952,13 @@ class MemDatabase {
|
|
|
938
952
|
}
|
|
939
953
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
940
954
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
941
|
-
if (existing)
|
|
955
|
+
if (existing) {
|
|
956
|
+
if (existing.project_id === null && projectId !== null) {
|
|
957
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
958
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
959
|
+
}
|
|
942
960
|
return existing;
|
|
961
|
+
}
|
|
943
962
|
const now = Math.floor(Date.now() / 1000);
|
|
944
963
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
945
964
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|
|
@@ -2108,7 +2127,7 @@ function registerAll() {
|
|
|
2108
2127
|
|
|
2109
2128
|
// src/packs/loader.ts
|
|
2110
2129
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
2111
|
-
import { join as join4, dirname as
|
|
2130
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
2112
2131
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2113
2132
|
|
|
2114
2133
|
// src/tools/save.ts
|
|
@@ -2436,7 +2455,7 @@ function looksMeaningful(value) {
|
|
|
2436
2455
|
// src/storage/projects.ts
|
|
2437
2456
|
import { execSync } from "node:child_process";
|
|
2438
2457
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2439
|
-
import { basename, join as join3 } from "node:path";
|
|
2458
|
+
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
|
2440
2459
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
2441
2460
|
let url = remoteUrl.trim();
|
|
2442
2461
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -2490,6 +2509,19 @@ function getGitRemoteUrl(directory) {
|
|
|
2490
2509
|
}
|
|
2491
2510
|
}
|
|
2492
2511
|
}
|
|
2512
|
+
function getGitTopLevel(directory) {
|
|
2513
|
+
try {
|
|
2514
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
2515
|
+
cwd: directory,
|
|
2516
|
+
encoding: "utf-8",
|
|
2517
|
+
timeout: 5000,
|
|
2518
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2519
|
+
}).trim();
|
|
2520
|
+
return root || null;
|
|
2521
|
+
} catch {
|
|
2522
|
+
return null;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2493
2525
|
function readProjectConfigFile(directory) {
|
|
2494
2526
|
const configPath = join3(directory, ".engrm.json");
|
|
2495
2527
|
if (!existsSync3(configPath))
|
|
@@ -2512,11 +2544,12 @@ function detectProject(directory) {
|
|
|
2512
2544
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
2513
2545
|
if (remoteUrl) {
|
|
2514
2546
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2547
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
2515
2548
|
return {
|
|
2516
2549
|
canonical_id: canonicalId,
|
|
2517
2550
|
name: projectNameFromCanonicalId(canonicalId),
|
|
2518
2551
|
remote_url: remoteUrl,
|
|
2519
|
-
local_path:
|
|
2552
|
+
local_path: repoRoot
|
|
2520
2553
|
};
|
|
2521
2554
|
}
|
|
2522
2555
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -2536,6 +2569,32 @@ function detectProject(directory) {
|
|
|
2536
2569
|
local_path: directory
|
|
2537
2570
|
};
|
|
2538
2571
|
}
|
|
2572
|
+
function detectProjectForPath(filePath, fallbackCwd) {
|
|
2573
|
+
const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
|
|
2574
|
+
const candidateDir = existsSync3(absolutePath) && !absolutePath.endsWith("/") ? dirname2(absolutePath) : dirname2(absolutePath);
|
|
2575
|
+
const detected = detectProject(candidateDir);
|
|
2576
|
+
if (detected.canonical_id.startsWith("local/"))
|
|
2577
|
+
return null;
|
|
2578
|
+
return detected;
|
|
2579
|
+
}
|
|
2580
|
+
function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
2581
|
+
const counts = new Map;
|
|
2582
|
+
for (const rawPath of paths) {
|
|
2583
|
+
if (!rawPath || !rawPath.trim())
|
|
2584
|
+
continue;
|
|
2585
|
+
const detected = detectProjectForPath(rawPath, fallbackCwd);
|
|
2586
|
+
if (!detected)
|
|
2587
|
+
continue;
|
|
2588
|
+
const existing = counts.get(detected.canonical_id);
|
|
2589
|
+
if (existing) {
|
|
2590
|
+
existing.count += 1;
|
|
2591
|
+
} else {
|
|
2592
|
+
counts.set(detected.canonical_id, { project: detected, count: 1 });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
|
|
2596
|
+
return ranked[0]?.project ?? detectProject(fallbackCwd);
|
|
2597
|
+
}
|
|
2539
2598
|
|
|
2540
2599
|
// src/embeddings/embedder.ts
|
|
2541
2600
|
var _available = null;
|
|
@@ -2814,7 +2873,8 @@ async function saveObservation(db, config, input) {
|
|
|
2814
2873
|
return { success: false, reason: "Title is required" };
|
|
2815
2874
|
}
|
|
2816
2875
|
const cwd = input.cwd ?? process.cwd();
|
|
2817
|
-
const
|
|
2876
|
+
const touchedPaths = [...input.files_read ?? [], ...input.files_modified ?? []];
|
|
2877
|
+
const detected = touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, cwd) : detectProject(cwd);
|
|
2818
2878
|
const project = db.upsertProject({
|
|
2819
2879
|
canonical_id: detected.canonical_id,
|
|
2820
2880
|
name: detected.name,
|
|
@@ -2941,7 +3001,7 @@ function toRelativePath(filePath, projectRoot) {
|
|
|
2941
3001
|
|
|
2942
3002
|
// src/packs/loader.ts
|
|
2943
3003
|
function getPacksDir() {
|
|
2944
|
-
const thisDir =
|
|
3004
|
+
const thisDir = dirname3(fileURLToPath2(import.meta.url));
|
|
2945
3005
|
return join4(thisDir, "..", "..", "packs");
|
|
2946
3006
|
}
|
|
2947
3007
|
function listPacks() {
|
|
@@ -2991,10 +3051,10 @@ async function installPack(db, config, packName, cwd) {
|
|
|
2991
3051
|
|
|
2992
3052
|
// src/sentinel/rules.ts
|
|
2993
3053
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
2994
|
-
import { join as join5, dirname as
|
|
3054
|
+
import { join as join5, dirname as dirname4 } from "node:path";
|
|
2995
3055
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2996
3056
|
function getRulePacksDir() {
|
|
2997
|
-
const thisDir =
|
|
3057
|
+
const thisDir = dirname4(fileURLToPath3(import.meta.url));
|
|
2998
3058
|
return join5(thisDir, "rule-packs");
|
|
2999
3059
|
}
|
|
3000
3060
|
function listRulePacks() {
|
|
@@ -3065,10 +3125,18 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3065
3125
|
const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
|
|
3066
3126
|
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3067
3127
|
let claudeHookCount = 0;
|
|
3128
|
+
let claudeSessionStartHook = false;
|
|
3129
|
+
let claudeUserPromptHook = false;
|
|
3130
|
+
let claudePostToolHook = false;
|
|
3131
|
+
let claudeStopHook = false;
|
|
3068
3132
|
if (claudeHooksRegistered) {
|
|
3069
3133
|
try {
|
|
3070
3134
|
const settings = JSON.parse(claudeSettingsContent);
|
|
3071
3135
|
const hooks = settings?.hooks ?? {};
|
|
3136
|
+
claudeSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
3137
|
+
claudeUserPromptHook = Array.isArray(hooks["UserPromptSubmit"]);
|
|
3138
|
+
claudePostToolHook = Array.isArray(hooks["PostToolUse"]);
|
|
3139
|
+
claudeStopHook = Array.isArray(hooks["Stop"]);
|
|
3072
3140
|
for (const entries of Object.values(hooks)) {
|
|
3073
3141
|
if (!Array.isArray(entries))
|
|
3074
3142
|
continue;
|
|
@@ -3081,6 +3149,13 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3081
3149
|
}
|
|
3082
3150
|
} catch {}
|
|
3083
3151
|
}
|
|
3152
|
+
let codexSessionStartHook = false;
|
|
3153
|
+
let codexStopHook = false;
|
|
3154
|
+
try {
|
|
3155
|
+
const hooks = codexHooksContent ? JSON.parse(codexHooksContent)?.hooks ?? {} : {};
|
|
3156
|
+
codexSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
3157
|
+
codexStopHook = Array.isArray(hooks["Stop"]);
|
|
3158
|
+
} catch {}
|
|
3084
3159
|
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
3085
3160
|
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
3086
3161
|
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
@@ -3095,6 +3170,17 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3095
3170
|
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3096
3171
|
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
3097
3172
|
)`).get(...params)?.count ?? 0;
|
|
3173
|
+
const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
|
|
3174
|
+
FROM sessions s
|
|
3175
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
3176
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
3177
|
+
AND (
|
|
3178
|
+
(s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
|
|
3179
|
+
OR (
|
|
3180
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3181
|
+
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
3182
|
+
)
|
|
3183
|
+
)`).get(...params)?.count ?? 0;
|
|
3098
3184
|
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
3099
3185
|
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3100
3186
|
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
@@ -3103,6 +3189,9 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3103
3189
|
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3104
3190
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3105
3191
|
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
3192
|
+
const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
|
|
3193
|
+
const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
|
|
3194
|
+
const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
|
|
3106
3195
|
const schemaVersion = getSchemaVersion(db.db);
|
|
3107
3196
|
return {
|
|
3108
3197
|
schema_version: schemaVersion,
|
|
@@ -3110,16 +3199,33 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3110
3199
|
claude_mcp_registered: claudeMcpRegistered,
|
|
3111
3200
|
claude_hooks_registered: claudeHooksRegistered,
|
|
3112
3201
|
claude_hook_count: claudeHookCount,
|
|
3202
|
+
claude_session_start_hook: claudeSessionStartHook,
|
|
3203
|
+
claude_user_prompt_hook: claudeUserPromptHook,
|
|
3204
|
+
claude_post_tool_hook: claudePostToolHook,
|
|
3205
|
+
claude_stop_hook: claudeStopHook,
|
|
3113
3206
|
codex_mcp_registered: codexMcpRegistered,
|
|
3114
3207
|
codex_hooks_registered: codexHooksRegistered,
|
|
3208
|
+
codex_session_start_hook: codexSessionStartHook,
|
|
3209
|
+
codex_stop_hook: codexStopHook,
|
|
3210
|
+
codex_raw_chronology_supported: false,
|
|
3115
3211
|
recent_user_prompts: recentUserPrompts,
|
|
3116
3212
|
recent_tool_events: recentToolEvents,
|
|
3117
3213
|
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
3214
|
+
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
3118
3215
|
latest_prompt_epoch: latestPromptEpoch,
|
|
3119
3216
|
latest_tool_event_epoch: latestToolEventEpoch,
|
|
3217
|
+
latest_post_tool_hook_epoch: latestPostToolHookEpoch,
|
|
3218
|
+
latest_post_tool_parse_status: latestPostToolParseStatus,
|
|
3219
|
+
latest_post_tool_name: latestPostToolName,
|
|
3120
3220
|
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
3121
3221
|
};
|
|
3122
3222
|
}
|
|
3223
|
+
function parseNullableInt(value) {
|
|
3224
|
+
if (!value)
|
|
3225
|
+
return null;
|
|
3226
|
+
const parsed = Number.parseInt(value, 10);
|
|
3227
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3228
|
+
}
|
|
3123
3229
|
|
|
3124
3230
|
// src/sync/auth.ts
|
|
3125
3231
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
@@ -3142,7 +3248,7 @@ function normalizeBaseUrl(url) {
|
|
|
3142
3248
|
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
3143
3249
|
var args = process.argv.slice(2);
|
|
3144
3250
|
var command = args[0];
|
|
3145
|
-
var THIS_DIR =
|
|
3251
|
+
var THIS_DIR = dirname5(fileURLToPath4(import.meta.url));
|
|
3146
3252
|
var IS_BUILT_DIST = THIS_DIR.endsWith("/dist") || THIS_DIR.endsWith("\\dist");
|
|
3147
3253
|
switch (command) {
|
|
3148
3254
|
case "init":
|
|
@@ -3629,7 +3735,16 @@ function handleStatus() {
|
|
|
3629
3735
|
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
3630
3736
|
console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
|
|
3631
3737
|
console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
|
|
3632
|
-
|
|
3738
|
+
if (capture.recent_sessions_with_partial_capture > 0) {
|
|
3739
|
+
console.log(` Partial raw: ${capture.recent_sessions_with_partial_capture} recent session${capture.recent_sessions_with_partial_capture === 1 ? "" : "s"} missing some chronology`);
|
|
3740
|
+
}
|
|
3741
|
+
console.log(` Hook state: Claude ${capture.claude_user_prompt_hook && capture.claude_post_tool_hook ? "raw-ready" : "partial"}, Codex ${capture.codex_raw_chronology_supported ? "raw-ready" : "start/stop only"}`);
|
|
3742
|
+
if (capture.latest_post_tool_hook_epoch) {
|
|
3743
|
+
const lastSeen = new Date(capture.latest_post_tool_hook_epoch * 1000).toISOString();
|
|
3744
|
+
const parseStatus = capture.latest_post_tool_parse_status ?? "unknown";
|
|
3745
|
+
const toolName = capture.latest_post_tool_name ?? "unknown";
|
|
3746
|
+
console.log(` PostToolUse: ${parseStatus} (${toolName}, ${lastSeen})`);
|
|
3747
|
+
}
|
|
3633
3748
|
try {
|
|
3634
3749
|
const activeObservations = db.db.query(`SELECT * FROM observations
|
|
3635
3750
|
WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
|
|
@@ -3910,9 +4025,17 @@ async function handleDoctor() {
|
|
|
3910
4025
|
if (existsSync7(claudeSettings)) {
|
|
3911
4026
|
const content = readFileSync7(claudeSettings, "utf-8");
|
|
3912
4027
|
let hookCount = 0;
|
|
4028
|
+
let hasSessionStart = false;
|
|
4029
|
+
let hasUserPrompt = false;
|
|
4030
|
+
let hasPostToolUse = false;
|
|
4031
|
+
let hasStop = false;
|
|
3913
4032
|
try {
|
|
3914
4033
|
const settings = JSON.parse(content);
|
|
3915
4034
|
const hooks = settings?.hooks ?? {};
|
|
4035
|
+
hasSessionStart = Array.isArray(hooks["SessionStart"]);
|
|
4036
|
+
hasUserPrompt = Array.isArray(hooks["UserPromptSubmit"]);
|
|
4037
|
+
hasPostToolUse = Array.isArray(hooks["PostToolUse"]);
|
|
4038
|
+
hasStop = Array.isArray(hooks["Stop"]);
|
|
3916
4039
|
for (const entries of Object.values(hooks)) {
|
|
3917
4040
|
if (Array.isArray(entries)) {
|
|
3918
4041
|
for (const entry of entries) {
|
|
@@ -3924,8 +4047,19 @@ async function handleDoctor() {
|
|
|
3924
4047
|
}
|
|
3925
4048
|
}
|
|
3926
4049
|
} catch {}
|
|
3927
|
-
|
|
4050
|
+
const missingCritical = [];
|
|
4051
|
+
if (!hasSessionStart)
|
|
4052
|
+
missingCritical.push("SessionStart");
|
|
4053
|
+
if (!hasUserPrompt)
|
|
4054
|
+
missingCritical.push("UserPromptSubmit");
|
|
4055
|
+
if (!hasPostToolUse)
|
|
4056
|
+
missingCritical.push("PostToolUse");
|
|
4057
|
+
if (!hasStop)
|
|
4058
|
+
missingCritical.push("Stop");
|
|
4059
|
+
if (hookCount > 0 && missingCritical.length === 0) {
|
|
3928
4060
|
pass(`Hooks registered (${hookCount} hook${hookCount === 1 ? "" : "s"})`);
|
|
4061
|
+
} else if (hookCount > 0) {
|
|
4062
|
+
warn(`Hooks registered but incomplete \u2014 missing ${missingCritical.join(", ")}`);
|
|
3929
4063
|
} else {
|
|
3930
4064
|
warn("No Engrm hooks found in Claude Code settings");
|
|
3931
4065
|
}
|
|
@@ -4057,10 +4191,16 @@ async function handleDoctor() {
|
|
|
4057
4191
|
}
|
|
4058
4192
|
try {
|
|
4059
4193
|
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
4060
|
-
if (capture.raw_capture_active) {
|
|
4194
|
+
if (capture.raw_capture_active && capture.recent_tool_events > 0 && capture.recent_sessions_with_partial_capture === 0) {
|
|
4061
4195
|
pass(`Raw chronology active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h)`);
|
|
4196
|
+
} else if (capture.raw_capture_active && capture.recent_sessions_with_partial_capture > 0) {
|
|
4197
|
+
warn(`Raw chronology is only partially active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h; ${capture.recent_sessions_with_partial_capture} recent session${capture.recent_sessions_with_partial_capture === 1 ? "" : "s"} missing some chronology).`);
|
|
4198
|
+
if (capture.latest_post_tool_hook_epoch) {
|
|
4199
|
+
info(`Last PostToolUse hook: ${new Date(capture.latest_post_tool_hook_epoch * 1000).toISOString()} (${capture.latest_post_tool_parse_status ?? "unknown"}${capture.latest_post_tool_name ? `, ${capture.latest_post_tool_name}` : ""})`);
|
|
4200
|
+
}
|
|
4062
4201
|
} else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
|
|
4063
|
-
|
|
4202
|
+
const guidance = capture.claude_user_prompt_hook && capture.claude_post_tool_hook ? "Claude is raw-ready; open a fresh Claude Code session and perform a few actions to verify capture." : "Claude raw chronology hooks are incomplete, and Codex currently supports start/stop capture only.";
|
|
4203
|
+
warn(`Hooks are registered, but no raw prompt/tool chronology has been captured in the last 24h. ${guidance}`);
|
|
4064
4204
|
} else {
|
|
4065
4205
|
warn("Raw chronology inactive \u2014 hook registration is incomplete");
|
|
4066
4206
|
}
|
|
@@ -327,7 +327,7 @@ function looksMeaningful(value) {
|
|
|
327
327
|
// src/storage/projects.ts
|
|
328
328
|
import { execSync } from "node:child_process";
|
|
329
329
|
import { existsSync, readFileSync } from "node:fs";
|
|
330
|
-
import { basename, join } from "node:path";
|
|
330
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
331
331
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
332
332
|
let url = remoteUrl.trim();
|
|
333
333
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -381,6 +381,19 @@ function getGitRemoteUrl(directory) {
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
function getGitTopLevel(directory) {
|
|
385
|
+
try {
|
|
386
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
387
|
+
cwd: directory,
|
|
388
|
+
encoding: "utf-8",
|
|
389
|
+
timeout: 5000,
|
|
390
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
391
|
+
}).trim();
|
|
392
|
+
return root || null;
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
384
397
|
function readProjectConfigFile(directory) {
|
|
385
398
|
const configPath = join(directory, ".engrm.json");
|
|
386
399
|
if (!existsSync(configPath))
|
|
@@ -403,11 +416,12 @@ function detectProject(directory) {
|
|
|
403
416
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
404
417
|
if (remoteUrl) {
|
|
405
418
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
419
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
406
420
|
return {
|
|
407
421
|
canonical_id: canonicalId,
|
|
408
422
|
name: projectNameFromCanonicalId(canonicalId),
|
|
409
423
|
remote_url: remoteUrl,
|
|
410
|
-
local_path:
|
|
424
|
+
local_path: repoRoot
|
|
411
425
|
};
|
|
412
426
|
}
|
|
413
427
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -427,6 +441,32 @@ function detectProject(directory) {
|
|
|
427
441
|
local_path: directory
|
|
428
442
|
};
|
|
429
443
|
}
|
|
444
|
+
function detectProjectForPath(filePath, fallbackCwd) {
|
|
445
|
+
const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
|
|
446
|
+
const candidateDir = existsSync(absolutePath) && !absolutePath.endsWith("/") ? dirname(absolutePath) : dirname(absolutePath);
|
|
447
|
+
const detected = detectProject(candidateDir);
|
|
448
|
+
if (detected.canonical_id.startsWith("local/"))
|
|
449
|
+
return null;
|
|
450
|
+
return detected;
|
|
451
|
+
}
|
|
452
|
+
function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
453
|
+
const counts = new Map;
|
|
454
|
+
for (const rawPath of paths) {
|
|
455
|
+
if (!rawPath || !rawPath.trim())
|
|
456
|
+
continue;
|
|
457
|
+
const detected = detectProjectForPath(rawPath, fallbackCwd);
|
|
458
|
+
if (!detected)
|
|
459
|
+
continue;
|
|
460
|
+
const existing = counts.get(detected.canonical_id);
|
|
461
|
+
if (existing) {
|
|
462
|
+
existing.count += 1;
|
|
463
|
+
} else {
|
|
464
|
+
counts.set(detected.canonical_id, { project: detected, count: 1 });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
|
|
468
|
+
return ranked[0]?.project ?? detectProject(fallbackCwd);
|
|
469
|
+
}
|
|
430
470
|
|
|
431
471
|
// src/embeddings/embedder.ts
|
|
432
472
|
var _available = null;
|
|
@@ -694,7 +734,8 @@ async function saveObservation(db, config, input) {
|
|
|
694
734
|
return { success: false, reason: "Title is required" };
|
|
695
735
|
}
|
|
696
736
|
const cwd = input.cwd ?? process.cwd();
|
|
697
|
-
const
|
|
737
|
+
const touchedPaths = [...input.files_read ?? [], ...input.files_modified ?? []];
|
|
738
|
+
const detected = touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, cwd) : detectProject(cwd);
|
|
698
739
|
const project = db.upsertProject({
|
|
699
740
|
canonical_id: detected.canonical_id,
|
|
700
741
|
name: detected.name,
|
|
@@ -1474,6 +1515,11 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
1474
1515
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
1475
1516
|
function openDatabase(dbPath) {
|
|
1476
1517
|
if (IS_BUN) {
|
|
1518
|
+
if (process.platform === "darwin") {
|
|
1519
|
+
try {
|
|
1520
|
+
return openNodeDatabase(dbPath);
|
|
1521
|
+
} catch {}
|
|
1522
|
+
}
|
|
1477
1523
|
return openBunDatabase(dbPath);
|
|
1478
1524
|
}
|
|
1479
1525
|
return openNodeDatabase(dbPath);
|
|
@@ -1590,6 +1636,15 @@ class MemDatabase {
|
|
|
1590
1636
|
}
|
|
1591
1637
|
return row;
|
|
1592
1638
|
}
|
|
1639
|
+
reassignObservationProject(observationId, projectId) {
|
|
1640
|
+
const existing = this.getObservationById(observationId);
|
|
1641
|
+
if (!existing)
|
|
1642
|
+
return false;
|
|
1643
|
+
if (existing.project_id === projectId)
|
|
1644
|
+
return true;
|
|
1645
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1593
1648
|
getObservationById(id) {
|
|
1594
1649
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1595
1650
|
}
|
|
@@ -1723,8 +1778,13 @@ class MemDatabase {
|
|
|
1723
1778
|
}
|
|
1724
1779
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
1725
1780
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
1726
|
-
if (existing)
|
|
1781
|
+
if (existing) {
|
|
1782
|
+
if (existing.project_id === null && projectId !== null) {
|
|
1783
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
1784
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
1785
|
+
}
|
|
1727
1786
|
return existing;
|
|
1787
|
+
}
|
|
1728
1788
|
const now = Math.floor(Date.now() / 1000);
|
|
1729
1789
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
1730
1790
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|