engrm 0.4.8 → 0.4.9
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 +149 -14
- package/dist/hooks/elicitation-result.js +59 -4
- package/dist/hooks/post-tool-use.js +154 -33
- package/dist/hooks/pre-compact.js +188 -14
- package/dist/hooks/sentinel.js +57 -3
- package/dist/hooks/session-start.js +357 -61
- package/dist/hooks/stop.js +59 -4
- package/dist/hooks/user-prompt-submit.js +57 -3
- package/dist/server.js +878 -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
|
|
|
@@ -805,6 +805,15 @@ class MemDatabase {
|
|
|
805
805
|
}
|
|
806
806
|
return row;
|
|
807
807
|
}
|
|
808
|
+
reassignObservationProject(observationId, projectId) {
|
|
809
|
+
const existing = this.getObservationById(observationId);
|
|
810
|
+
if (!existing)
|
|
811
|
+
return false;
|
|
812
|
+
if (existing.project_id === projectId)
|
|
813
|
+
return true;
|
|
814
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
808
817
|
getObservationById(id) {
|
|
809
818
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
810
819
|
}
|
|
@@ -938,8 +947,13 @@ class MemDatabase {
|
|
|
938
947
|
}
|
|
939
948
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
940
949
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
941
|
-
if (existing)
|
|
950
|
+
if (existing) {
|
|
951
|
+
if (existing.project_id === null && projectId !== null) {
|
|
952
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
953
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
954
|
+
}
|
|
942
955
|
return existing;
|
|
956
|
+
}
|
|
943
957
|
const now = Math.floor(Date.now() / 1000);
|
|
944
958
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
945
959
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|
|
@@ -2108,7 +2122,7 @@ function registerAll() {
|
|
|
2108
2122
|
|
|
2109
2123
|
// src/packs/loader.ts
|
|
2110
2124
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
2111
|
-
import { join as join4, dirname as
|
|
2125
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
2112
2126
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2113
2127
|
|
|
2114
2128
|
// src/tools/save.ts
|
|
@@ -2436,7 +2450,7 @@ function looksMeaningful(value) {
|
|
|
2436
2450
|
// src/storage/projects.ts
|
|
2437
2451
|
import { execSync } from "node:child_process";
|
|
2438
2452
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2439
|
-
import { basename, join as join3 } from "node:path";
|
|
2453
|
+
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
|
2440
2454
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
2441
2455
|
let url = remoteUrl.trim();
|
|
2442
2456
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -2490,6 +2504,19 @@ function getGitRemoteUrl(directory) {
|
|
|
2490
2504
|
}
|
|
2491
2505
|
}
|
|
2492
2506
|
}
|
|
2507
|
+
function getGitTopLevel(directory) {
|
|
2508
|
+
try {
|
|
2509
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
2510
|
+
cwd: directory,
|
|
2511
|
+
encoding: "utf-8",
|
|
2512
|
+
timeout: 5000,
|
|
2513
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2514
|
+
}).trim();
|
|
2515
|
+
return root || null;
|
|
2516
|
+
} catch {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2493
2520
|
function readProjectConfigFile(directory) {
|
|
2494
2521
|
const configPath = join3(directory, ".engrm.json");
|
|
2495
2522
|
if (!existsSync3(configPath))
|
|
@@ -2512,11 +2539,12 @@ function detectProject(directory) {
|
|
|
2512
2539
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
2513
2540
|
if (remoteUrl) {
|
|
2514
2541
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2542
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
2515
2543
|
return {
|
|
2516
2544
|
canonical_id: canonicalId,
|
|
2517
2545
|
name: projectNameFromCanonicalId(canonicalId),
|
|
2518
2546
|
remote_url: remoteUrl,
|
|
2519
|
-
local_path:
|
|
2547
|
+
local_path: repoRoot
|
|
2520
2548
|
};
|
|
2521
2549
|
}
|
|
2522
2550
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -2536,6 +2564,32 @@ function detectProject(directory) {
|
|
|
2536
2564
|
local_path: directory
|
|
2537
2565
|
};
|
|
2538
2566
|
}
|
|
2567
|
+
function detectProjectForPath(filePath, fallbackCwd) {
|
|
2568
|
+
const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
|
|
2569
|
+
const candidateDir = existsSync3(absolutePath) && !absolutePath.endsWith("/") ? dirname2(absolutePath) : dirname2(absolutePath);
|
|
2570
|
+
const detected = detectProject(candidateDir);
|
|
2571
|
+
if (detected.canonical_id.startsWith("local/"))
|
|
2572
|
+
return null;
|
|
2573
|
+
return detected;
|
|
2574
|
+
}
|
|
2575
|
+
function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
2576
|
+
const counts = new Map;
|
|
2577
|
+
for (const rawPath of paths) {
|
|
2578
|
+
if (!rawPath || !rawPath.trim())
|
|
2579
|
+
continue;
|
|
2580
|
+
const detected = detectProjectForPath(rawPath, fallbackCwd);
|
|
2581
|
+
if (!detected)
|
|
2582
|
+
continue;
|
|
2583
|
+
const existing = counts.get(detected.canonical_id);
|
|
2584
|
+
if (existing) {
|
|
2585
|
+
existing.count += 1;
|
|
2586
|
+
} else {
|
|
2587
|
+
counts.set(detected.canonical_id, { project: detected, count: 1 });
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
|
|
2591
|
+
return ranked[0]?.project ?? detectProject(fallbackCwd);
|
|
2592
|
+
}
|
|
2539
2593
|
|
|
2540
2594
|
// src/embeddings/embedder.ts
|
|
2541
2595
|
var _available = null;
|
|
@@ -2814,7 +2868,8 @@ async function saveObservation(db, config, input) {
|
|
|
2814
2868
|
return { success: false, reason: "Title is required" };
|
|
2815
2869
|
}
|
|
2816
2870
|
const cwd = input.cwd ?? process.cwd();
|
|
2817
|
-
const
|
|
2871
|
+
const touchedPaths = [...input.files_read ?? [], ...input.files_modified ?? []];
|
|
2872
|
+
const detected = touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, cwd) : detectProject(cwd);
|
|
2818
2873
|
const project = db.upsertProject({
|
|
2819
2874
|
canonical_id: detected.canonical_id,
|
|
2820
2875
|
name: detected.name,
|
|
@@ -2941,7 +2996,7 @@ function toRelativePath(filePath, projectRoot) {
|
|
|
2941
2996
|
|
|
2942
2997
|
// src/packs/loader.ts
|
|
2943
2998
|
function getPacksDir() {
|
|
2944
|
-
const thisDir =
|
|
2999
|
+
const thisDir = dirname3(fileURLToPath2(import.meta.url));
|
|
2945
3000
|
return join4(thisDir, "..", "..", "packs");
|
|
2946
3001
|
}
|
|
2947
3002
|
function listPacks() {
|
|
@@ -2991,10 +3046,10 @@ async function installPack(db, config, packName, cwd) {
|
|
|
2991
3046
|
|
|
2992
3047
|
// src/sentinel/rules.ts
|
|
2993
3048
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
2994
|
-
import { join as join5, dirname as
|
|
3049
|
+
import { join as join5, dirname as dirname4 } from "node:path";
|
|
2995
3050
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2996
3051
|
function getRulePacksDir() {
|
|
2997
|
-
const thisDir =
|
|
3052
|
+
const thisDir = dirname4(fileURLToPath3(import.meta.url));
|
|
2998
3053
|
return join5(thisDir, "rule-packs");
|
|
2999
3054
|
}
|
|
3000
3055
|
function listRulePacks() {
|
|
@@ -3065,10 +3120,18 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3065
3120
|
const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
|
|
3066
3121
|
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3067
3122
|
let claudeHookCount = 0;
|
|
3123
|
+
let claudeSessionStartHook = false;
|
|
3124
|
+
let claudeUserPromptHook = false;
|
|
3125
|
+
let claudePostToolHook = false;
|
|
3126
|
+
let claudeStopHook = false;
|
|
3068
3127
|
if (claudeHooksRegistered) {
|
|
3069
3128
|
try {
|
|
3070
3129
|
const settings = JSON.parse(claudeSettingsContent);
|
|
3071
3130
|
const hooks = settings?.hooks ?? {};
|
|
3131
|
+
claudeSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
3132
|
+
claudeUserPromptHook = Array.isArray(hooks["UserPromptSubmit"]);
|
|
3133
|
+
claudePostToolHook = Array.isArray(hooks["PostToolUse"]);
|
|
3134
|
+
claudeStopHook = Array.isArray(hooks["Stop"]);
|
|
3072
3135
|
for (const entries of Object.values(hooks)) {
|
|
3073
3136
|
if (!Array.isArray(entries))
|
|
3074
3137
|
continue;
|
|
@@ -3081,6 +3144,13 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3081
3144
|
}
|
|
3082
3145
|
} catch {}
|
|
3083
3146
|
}
|
|
3147
|
+
let codexSessionStartHook = false;
|
|
3148
|
+
let codexStopHook = false;
|
|
3149
|
+
try {
|
|
3150
|
+
const hooks = codexHooksContent ? JSON.parse(codexHooksContent)?.hooks ?? {} : {};
|
|
3151
|
+
codexSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
3152
|
+
codexStopHook = Array.isArray(hooks["Stop"]);
|
|
3153
|
+
} catch {}
|
|
3084
3154
|
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
3085
3155
|
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
3086
3156
|
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
@@ -3095,6 +3165,17 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3095
3165
|
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3096
3166
|
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
3097
3167
|
)`).get(...params)?.count ?? 0;
|
|
3168
|
+
const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
|
|
3169
|
+
FROM sessions s
|
|
3170
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
3171
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
3172
|
+
AND (
|
|
3173
|
+
(s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
|
|
3174
|
+
OR (
|
|
3175
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3176
|
+
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
3177
|
+
)
|
|
3178
|
+
)`).get(...params)?.count ?? 0;
|
|
3098
3179
|
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
3099
3180
|
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3100
3181
|
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
@@ -3103,6 +3184,9 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3103
3184
|
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3104
3185
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3105
3186
|
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
3187
|
+
const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
|
|
3188
|
+
const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
|
|
3189
|
+
const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
|
|
3106
3190
|
const schemaVersion = getSchemaVersion(db.db);
|
|
3107
3191
|
return {
|
|
3108
3192
|
schema_version: schemaVersion,
|
|
@@ -3110,16 +3194,33 @@ function getCaptureStatus(db, input = {}) {
|
|
|
3110
3194
|
claude_mcp_registered: claudeMcpRegistered,
|
|
3111
3195
|
claude_hooks_registered: claudeHooksRegistered,
|
|
3112
3196
|
claude_hook_count: claudeHookCount,
|
|
3197
|
+
claude_session_start_hook: claudeSessionStartHook,
|
|
3198
|
+
claude_user_prompt_hook: claudeUserPromptHook,
|
|
3199
|
+
claude_post_tool_hook: claudePostToolHook,
|
|
3200
|
+
claude_stop_hook: claudeStopHook,
|
|
3113
3201
|
codex_mcp_registered: codexMcpRegistered,
|
|
3114
3202
|
codex_hooks_registered: codexHooksRegistered,
|
|
3203
|
+
codex_session_start_hook: codexSessionStartHook,
|
|
3204
|
+
codex_stop_hook: codexStopHook,
|
|
3205
|
+
codex_raw_chronology_supported: false,
|
|
3115
3206
|
recent_user_prompts: recentUserPrompts,
|
|
3116
3207
|
recent_tool_events: recentToolEvents,
|
|
3117
3208
|
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
3209
|
+
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
3118
3210
|
latest_prompt_epoch: latestPromptEpoch,
|
|
3119
3211
|
latest_tool_event_epoch: latestToolEventEpoch,
|
|
3212
|
+
latest_post_tool_hook_epoch: latestPostToolHookEpoch,
|
|
3213
|
+
latest_post_tool_parse_status: latestPostToolParseStatus,
|
|
3214
|
+
latest_post_tool_name: latestPostToolName,
|
|
3120
3215
|
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
3121
3216
|
};
|
|
3122
3217
|
}
|
|
3218
|
+
function parseNullableInt(value) {
|
|
3219
|
+
if (!value)
|
|
3220
|
+
return null;
|
|
3221
|
+
const parsed = Number.parseInt(value, 10);
|
|
3222
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3223
|
+
}
|
|
3123
3224
|
|
|
3124
3225
|
// src/sync/auth.ts
|
|
3125
3226
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
@@ -3142,7 +3243,7 @@ function normalizeBaseUrl(url) {
|
|
|
3142
3243
|
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
3143
3244
|
var args = process.argv.slice(2);
|
|
3144
3245
|
var command = args[0];
|
|
3145
|
-
var THIS_DIR =
|
|
3246
|
+
var THIS_DIR = dirname5(fileURLToPath4(import.meta.url));
|
|
3146
3247
|
var IS_BUILT_DIST = THIS_DIR.endsWith("/dist") || THIS_DIR.endsWith("\\dist");
|
|
3147
3248
|
switch (command) {
|
|
3148
3249
|
case "init":
|
|
@@ -3629,7 +3730,16 @@ function handleStatus() {
|
|
|
3629
3730
|
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
3630
3731
|
console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
|
|
3631
3732
|
console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
|
|
3632
|
-
|
|
3733
|
+
if (capture.recent_sessions_with_partial_capture > 0) {
|
|
3734
|
+
console.log(` Partial raw: ${capture.recent_sessions_with_partial_capture} recent session${capture.recent_sessions_with_partial_capture === 1 ? "" : "s"} missing some chronology`);
|
|
3735
|
+
}
|
|
3736
|
+
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"}`);
|
|
3737
|
+
if (capture.latest_post_tool_hook_epoch) {
|
|
3738
|
+
const lastSeen = new Date(capture.latest_post_tool_hook_epoch * 1000).toISOString();
|
|
3739
|
+
const parseStatus = capture.latest_post_tool_parse_status ?? "unknown";
|
|
3740
|
+
const toolName = capture.latest_post_tool_name ?? "unknown";
|
|
3741
|
+
console.log(` PostToolUse: ${parseStatus} (${toolName}, ${lastSeen})`);
|
|
3742
|
+
}
|
|
3633
3743
|
try {
|
|
3634
3744
|
const activeObservations = db.db.query(`SELECT * FROM observations
|
|
3635
3745
|
WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
|
|
@@ -3910,9 +4020,17 @@ async function handleDoctor() {
|
|
|
3910
4020
|
if (existsSync7(claudeSettings)) {
|
|
3911
4021
|
const content = readFileSync7(claudeSettings, "utf-8");
|
|
3912
4022
|
let hookCount = 0;
|
|
4023
|
+
let hasSessionStart = false;
|
|
4024
|
+
let hasUserPrompt = false;
|
|
4025
|
+
let hasPostToolUse = false;
|
|
4026
|
+
let hasStop = false;
|
|
3913
4027
|
try {
|
|
3914
4028
|
const settings = JSON.parse(content);
|
|
3915
4029
|
const hooks = settings?.hooks ?? {};
|
|
4030
|
+
hasSessionStart = Array.isArray(hooks["SessionStart"]);
|
|
4031
|
+
hasUserPrompt = Array.isArray(hooks["UserPromptSubmit"]);
|
|
4032
|
+
hasPostToolUse = Array.isArray(hooks["PostToolUse"]);
|
|
4033
|
+
hasStop = Array.isArray(hooks["Stop"]);
|
|
3916
4034
|
for (const entries of Object.values(hooks)) {
|
|
3917
4035
|
if (Array.isArray(entries)) {
|
|
3918
4036
|
for (const entry of entries) {
|
|
@@ -3924,8 +4042,19 @@ async function handleDoctor() {
|
|
|
3924
4042
|
}
|
|
3925
4043
|
}
|
|
3926
4044
|
} catch {}
|
|
3927
|
-
|
|
4045
|
+
const missingCritical = [];
|
|
4046
|
+
if (!hasSessionStart)
|
|
4047
|
+
missingCritical.push("SessionStart");
|
|
4048
|
+
if (!hasUserPrompt)
|
|
4049
|
+
missingCritical.push("UserPromptSubmit");
|
|
4050
|
+
if (!hasPostToolUse)
|
|
4051
|
+
missingCritical.push("PostToolUse");
|
|
4052
|
+
if (!hasStop)
|
|
4053
|
+
missingCritical.push("Stop");
|
|
4054
|
+
if (hookCount > 0 && missingCritical.length === 0) {
|
|
3928
4055
|
pass(`Hooks registered (${hookCount} hook${hookCount === 1 ? "" : "s"})`);
|
|
4056
|
+
} else if (hookCount > 0) {
|
|
4057
|
+
warn(`Hooks registered but incomplete \u2014 missing ${missingCritical.join(", ")}`);
|
|
3929
4058
|
} else {
|
|
3930
4059
|
warn("No Engrm hooks found in Claude Code settings");
|
|
3931
4060
|
}
|
|
@@ -4057,10 +4186,16 @@ async function handleDoctor() {
|
|
|
4057
4186
|
}
|
|
4058
4187
|
try {
|
|
4059
4188
|
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
4060
|
-
if (capture.raw_capture_active) {
|
|
4189
|
+
if (capture.raw_capture_active && capture.recent_tool_events > 0 && capture.recent_sessions_with_partial_capture === 0) {
|
|
4061
4190
|
pass(`Raw chronology active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h)`);
|
|
4191
|
+
} else if (capture.raw_capture_active && capture.recent_sessions_with_partial_capture > 0) {
|
|
4192
|
+
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).`);
|
|
4193
|
+
if (capture.latest_post_tool_hook_epoch) {
|
|
4194
|
+
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}` : ""})`);
|
|
4195
|
+
}
|
|
4062
4196
|
} else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
|
|
4063
|
-
|
|
4197
|
+
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.";
|
|
4198
|
+
warn(`Hooks are registered, but no raw prompt/tool chronology has been captured in the last 24h. ${guidance}`);
|
|
4064
4199
|
} else {
|
|
4065
4200
|
warn("Raw chronology inactive \u2014 hook registration is incomplete");
|
|
4066
4201
|
}
|
|
@@ -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,
|
|
@@ -1590,6 +1631,15 @@ class MemDatabase {
|
|
|
1590
1631
|
}
|
|
1591
1632
|
return row;
|
|
1592
1633
|
}
|
|
1634
|
+
reassignObservationProject(observationId, projectId) {
|
|
1635
|
+
const existing = this.getObservationById(observationId);
|
|
1636
|
+
if (!existing)
|
|
1637
|
+
return false;
|
|
1638
|
+
if (existing.project_id === projectId)
|
|
1639
|
+
return true;
|
|
1640
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1593
1643
|
getObservationById(id) {
|
|
1594
1644
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1595
1645
|
}
|
|
@@ -1723,8 +1773,13 @@ class MemDatabase {
|
|
|
1723
1773
|
}
|
|
1724
1774
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
1725
1775
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
1726
|
-
if (existing)
|
|
1776
|
+
if (existing) {
|
|
1777
|
+
if (existing.project_id === null && projectId !== null) {
|
|
1778
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
1779
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
1780
|
+
}
|
|
1727
1781
|
return existing;
|
|
1782
|
+
}
|
|
1728
1783
|
const now = Math.floor(Date.now() / 1000);
|
|
1729
1784
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
1730
1785
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|