engrm 0.4.7 → 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/README.md +42 -1
- package/dist/cli.js +549 -64
- package/dist/hooks/elicitation-result.js +225 -4
- package/dist/hooks/post-tool-use.js +346 -21
- package/dist/hooks/pre-compact.js +439 -7
- package/dist/hooks/sentinel.js +223 -3
- package/dist/hooks/session-start.js +670 -43
- package/dist/hooks/stop.js +259 -7
- package/dist/hooks/user-prompt-submit.js +1441 -0
- package/dist/server.js +2378 -306
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18,10 +18,10 @@ var __export = (target, all) => {
|
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
|
-
import { existsSync as
|
|
22
|
-
import { hostname as hostname2, homedir as
|
|
23
|
-
import { dirname as
|
|
24
|
-
import { createHash as
|
|
21
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync } from "fs";
|
|
22
|
+
import { hostname as hostname2, homedir as homedir4, networkInterfaces as networkInterfaces2 } from "os";
|
|
23
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
24
|
+
import { createHash as createHash3 } from "crypto";
|
|
25
25
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
26
26
|
|
|
27
27
|
// src/config.ts
|
|
@@ -539,6 +539,64 @@ var MIGRATIONS = [
|
|
|
539
539
|
);
|
|
540
540
|
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
541
541
|
`
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
version: 9,
|
|
545
|
+
description: "Add first-class user prompt capture",
|
|
546
|
+
sql: `
|
|
547
|
+
CREATE TABLE IF NOT EXISTS user_prompts (
|
|
548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
549
|
+
session_id TEXT NOT NULL,
|
|
550
|
+
project_id INTEGER REFERENCES projects(id),
|
|
551
|
+
prompt_number INTEGER NOT NULL,
|
|
552
|
+
prompt TEXT NOT NULL,
|
|
553
|
+
prompt_hash TEXT NOT NULL,
|
|
554
|
+
cwd TEXT,
|
|
555
|
+
user_id TEXT NOT NULL,
|
|
556
|
+
device_id TEXT NOT NULL,
|
|
557
|
+
agent TEXT DEFAULT 'claude-code',
|
|
558
|
+
created_at_epoch INTEGER NOT NULL,
|
|
559
|
+
UNIQUE(session_id, prompt_number)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_session
|
|
563
|
+
ON user_prompts(session_id, prompt_number DESC);
|
|
564
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_project
|
|
565
|
+
ON user_prompts(project_id, created_at_epoch DESC);
|
|
566
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_created
|
|
567
|
+
ON user_prompts(created_at_epoch DESC);
|
|
568
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
|
|
569
|
+
ON user_prompts(prompt_hash);
|
|
570
|
+
`
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
version: 10,
|
|
574
|
+
description: "Add first-class tool event chronology",
|
|
575
|
+
sql: `
|
|
576
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
577
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
578
|
+
session_id TEXT NOT NULL,
|
|
579
|
+
project_id INTEGER REFERENCES projects(id),
|
|
580
|
+
tool_name TEXT NOT NULL,
|
|
581
|
+
tool_input_json TEXT,
|
|
582
|
+
tool_response_preview TEXT,
|
|
583
|
+
file_path TEXT,
|
|
584
|
+
command TEXT,
|
|
585
|
+
user_id TEXT NOT NULL,
|
|
586
|
+
device_id TEXT NOT NULL,
|
|
587
|
+
agent TEXT DEFAULT 'claude-code',
|
|
588
|
+
created_at_epoch INTEGER NOT NULL
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
592
|
+
ON tool_events(session_id, created_at_epoch DESC, id DESC);
|
|
593
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_project
|
|
594
|
+
ON tool_events(project_id, created_at_epoch DESC, id DESC);
|
|
595
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
|
|
596
|
+
ON tool_events(tool_name, created_at_epoch DESC);
|
|
597
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
598
|
+
ON tool_events(created_at_epoch DESC, id DESC);
|
|
599
|
+
`
|
|
542
600
|
}
|
|
543
601
|
];
|
|
544
602
|
function isVecExtensionLoaded(db) {
|
|
@@ -620,9 +678,14 @@ function ensureObservationTypes(db) {
|
|
|
620
678
|
}
|
|
621
679
|
}
|
|
622
680
|
}
|
|
681
|
+
function getSchemaVersion(db) {
|
|
682
|
+
const result = db.query("PRAGMA user_version").get();
|
|
683
|
+
return result.user_version;
|
|
684
|
+
}
|
|
623
685
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
624
686
|
|
|
625
687
|
// src/storage/sqlite.ts
|
|
688
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
626
689
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
627
690
|
function openDatabase(dbPath) {
|
|
628
691
|
if (IS_BUN) {
|
|
@@ -742,6 +805,15 @@ class MemDatabase {
|
|
|
742
805
|
}
|
|
743
806
|
return row;
|
|
744
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
|
+
}
|
|
745
817
|
getObservationById(id) {
|
|
746
818
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
747
819
|
}
|
|
@@ -875,8 +947,13 @@ class MemDatabase {
|
|
|
875
947
|
}
|
|
876
948
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
877
949
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
878
|
-
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
|
+
}
|
|
879
955
|
return existing;
|
|
956
|
+
}
|
|
880
957
|
const now = Math.floor(Date.now() / 1000);
|
|
881
958
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
882
959
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|
|
@@ -886,6 +963,110 @@ class MemDatabase {
|
|
|
886
963
|
const now = Math.floor(Date.now() / 1000);
|
|
887
964
|
this.db.query("UPDATE sessions SET status = 'completed', completed_at_epoch = ? WHERE session_id = ?").run(now, sessionId);
|
|
888
965
|
}
|
|
966
|
+
getSessionById(sessionId) {
|
|
967
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId) ?? null;
|
|
968
|
+
}
|
|
969
|
+
getRecentSessions(projectId, limit = 10, userId) {
|
|
970
|
+
const visibilityClause = userId ? " AND s.user_id = ?" : "";
|
|
971
|
+
if (projectId !== null) {
|
|
972
|
+
return this.db.query(`SELECT
|
|
973
|
+
s.*,
|
|
974
|
+
p.name AS project_name,
|
|
975
|
+
ss.request AS request,
|
|
976
|
+
ss.completed AS completed,
|
|
977
|
+
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
978
|
+
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
979
|
+
FROM sessions s
|
|
980
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
981
|
+
LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
|
|
982
|
+
WHERE s.project_id = ?${visibilityClause}
|
|
983
|
+
ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
|
|
984
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
985
|
+
}
|
|
986
|
+
return this.db.query(`SELECT
|
|
987
|
+
s.*,
|
|
988
|
+
p.name AS project_name,
|
|
989
|
+
ss.request AS request,
|
|
990
|
+
ss.completed AS completed,
|
|
991
|
+
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
992
|
+
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
993
|
+
FROM sessions s
|
|
994
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
995
|
+
LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
|
|
996
|
+
WHERE 1 = 1${visibilityClause}
|
|
997
|
+
ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
|
|
998
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
999
|
+
}
|
|
1000
|
+
insertUserPrompt(input) {
|
|
1001
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1002
|
+
const normalizedPrompt = input.prompt.trim();
|
|
1003
|
+
const promptHash = hashPrompt(normalizedPrompt);
|
|
1004
|
+
const latest = this.db.query(`SELECT * FROM user_prompts
|
|
1005
|
+
WHERE session_id = ?
|
|
1006
|
+
ORDER BY prompt_number DESC
|
|
1007
|
+
LIMIT 1`).get(input.session_id);
|
|
1008
|
+
if (latest && latest.prompt_hash === promptHash) {
|
|
1009
|
+
return latest;
|
|
1010
|
+
}
|
|
1011
|
+
const promptNumber = (latest?.prompt_number ?? 0) + 1;
|
|
1012
|
+
const result = this.db.query(`INSERT INTO user_prompts (
|
|
1013
|
+
session_id, project_id, prompt_number, prompt, prompt_hash, cwd,
|
|
1014
|
+
user_id, device_id, agent, created_at_epoch
|
|
1015
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, promptNumber, normalizedPrompt, promptHash, input.cwd ?? null, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt);
|
|
1016
|
+
return this.getUserPromptById(Number(result.lastInsertRowid));
|
|
1017
|
+
}
|
|
1018
|
+
getUserPromptById(id) {
|
|
1019
|
+
return this.db.query("SELECT * FROM user_prompts WHERE id = ?").get(id) ?? null;
|
|
1020
|
+
}
|
|
1021
|
+
getRecentUserPrompts(projectId, limit = 10, userId) {
|
|
1022
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1023
|
+
if (projectId !== null) {
|
|
1024
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1025
|
+
WHERE project_id = ?${visibilityClause}
|
|
1026
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
1027
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1028
|
+
}
|
|
1029
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1030
|
+
WHERE 1 = 1${visibilityClause}
|
|
1031
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
1032
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1033
|
+
}
|
|
1034
|
+
getSessionUserPrompts(sessionId, limit = 20) {
|
|
1035
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1036
|
+
WHERE session_id = ?
|
|
1037
|
+
ORDER BY prompt_number ASC
|
|
1038
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1039
|
+
}
|
|
1040
|
+
insertToolEvent(input) {
|
|
1041
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1042
|
+
const result = this.db.query(`INSERT INTO tool_events (
|
|
1043
|
+
session_id, project_id, tool_name, tool_input_json, tool_response_preview,
|
|
1044
|
+
file_path, command, user_id, device_id, agent, created_at_epoch
|
|
1045
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.tool_name, input.tool_input_json ?? null, input.tool_response_preview ?? null, input.file_path ?? null, input.command ?? null, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt);
|
|
1046
|
+
return this.getToolEventById(Number(result.lastInsertRowid));
|
|
1047
|
+
}
|
|
1048
|
+
getToolEventById(id) {
|
|
1049
|
+
return this.db.query("SELECT * FROM tool_events WHERE id = ?").get(id) ?? null;
|
|
1050
|
+
}
|
|
1051
|
+
getSessionToolEvents(sessionId, limit = 20) {
|
|
1052
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1053
|
+
WHERE session_id = ?
|
|
1054
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1055
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1056
|
+
}
|
|
1057
|
+
getRecentToolEvents(projectId, limit = 20, userId) {
|
|
1058
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1059
|
+
if (projectId !== null) {
|
|
1060
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1061
|
+
WHERE project_id = ?${visibilityClause}
|
|
1062
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1063
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1064
|
+
}
|
|
1065
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1066
|
+
WHERE 1 = 1${visibilityClause}
|
|
1067
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1068
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1069
|
+
}
|
|
889
1070
|
addToOutbox(recordType, recordId) {
|
|
890
1071
|
const now = Math.floor(Date.now() / 1000);
|
|
891
1072
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1056,6 +1237,9 @@ class MemDatabase {
|
|
|
1056
1237
|
this.db.query("INSERT OR REPLACE INTO packs_installed (name, installed_at, observation_count) VALUES (?, ?, ?)").run(name, now, observationCount);
|
|
1057
1238
|
}
|
|
1058
1239
|
}
|
|
1240
|
+
function hashPrompt(prompt) {
|
|
1241
|
+
return createHash2("sha256").update(prompt).digest("hex");
|
|
1242
|
+
}
|
|
1059
1243
|
|
|
1060
1244
|
// src/storage/outbox.ts
|
|
1061
1245
|
function getOutboxStats(db) {
|
|
@@ -1410,6 +1594,64 @@ var MIGRATIONS2 = [
|
|
|
1410
1594
|
);
|
|
1411
1595
|
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
1412
1596
|
`
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
version: 9,
|
|
1600
|
+
description: "Add first-class user prompt capture",
|
|
1601
|
+
sql: `
|
|
1602
|
+
CREATE TABLE IF NOT EXISTS user_prompts (
|
|
1603
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1604
|
+
session_id TEXT NOT NULL,
|
|
1605
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1606
|
+
prompt_number INTEGER NOT NULL,
|
|
1607
|
+
prompt TEXT NOT NULL,
|
|
1608
|
+
prompt_hash TEXT NOT NULL,
|
|
1609
|
+
cwd TEXT,
|
|
1610
|
+
user_id TEXT NOT NULL,
|
|
1611
|
+
device_id TEXT NOT NULL,
|
|
1612
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1613
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1614
|
+
UNIQUE(session_id, prompt_number)
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_session
|
|
1618
|
+
ON user_prompts(session_id, prompt_number DESC);
|
|
1619
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_project
|
|
1620
|
+
ON user_prompts(project_id, created_at_epoch DESC);
|
|
1621
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_created
|
|
1622
|
+
ON user_prompts(created_at_epoch DESC);
|
|
1623
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
|
|
1624
|
+
ON user_prompts(prompt_hash);
|
|
1625
|
+
`
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
version: 10,
|
|
1629
|
+
description: "Add first-class tool event chronology",
|
|
1630
|
+
sql: `
|
|
1631
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
1632
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1633
|
+
session_id TEXT NOT NULL,
|
|
1634
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1635
|
+
tool_name TEXT NOT NULL,
|
|
1636
|
+
tool_input_json TEXT,
|
|
1637
|
+
tool_response_preview TEXT,
|
|
1638
|
+
file_path TEXT,
|
|
1639
|
+
command TEXT,
|
|
1640
|
+
user_id TEXT NOT NULL,
|
|
1641
|
+
device_id TEXT NOT NULL,
|
|
1642
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1643
|
+
created_at_epoch INTEGER NOT NULL
|
|
1644
|
+
);
|
|
1645
|
+
|
|
1646
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
1647
|
+
ON tool_events(session_id, created_at_epoch DESC, id DESC);
|
|
1648
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_project
|
|
1649
|
+
ON tool_events(project_id, created_at_epoch DESC, id DESC);
|
|
1650
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
|
|
1651
|
+
ON tool_events(tool_name, created_at_epoch DESC);
|
|
1652
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
1653
|
+
ON tool_events(created_at_epoch DESC, id DESC);
|
|
1654
|
+
`
|
|
1413
1655
|
}
|
|
1414
1656
|
];
|
|
1415
1657
|
function isVecExtensionLoaded2(db) {
|
|
@@ -1420,14 +1662,14 @@ function isVecExtensionLoaded2(db) {
|
|
|
1420
1662
|
return false;
|
|
1421
1663
|
}
|
|
1422
1664
|
}
|
|
1423
|
-
function
|
|
1665
|
+
function getSchemaVersion2(db) {
|
|
1424
1666
|
const result = db.query("PRAGMA user_version").get();
|
|
1425
1667
|
return result.user_version;
|
|
1426
1668
|
}
|
|
1427
1669
|
var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
1428
1670
|
|
|
1429
1671
|
// src/provisioning/provision.ts
|
|
1430
|
-
var DEFAULT_CANDENGO_URL = "https://
|
|
1672
|
+
var DEFAULT_CANDENGO_URL = "https://engrm.dev";
|
|
1431
1673
|
|
|
1432
1674
|
class ProvisionError extends Error {
|
|
1433
1675
|
status;
|
|
@@ -1819,6 +2061,7 @@ function registerHooks() {
|
|
|
1819
2061
|
return [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
|
|
1820
2062
|
}
|
|
1821
2063
|
const sessionStartCmd = hookCmd("session-start");
|
|
2064
|
+
const userPromptSubmitCmd = hookCmd("user-prompt-submit");
|
|
1822
2065
|
const preCompactCmd = hookCmd("pre-compact");
|
|
1823
2066
|
const preToolUseCmd = hookCmd("sentinel");
|
|
1824
2067
|
const postToolUseCmd = hookCmd("post-tool-use");
|
|
@@ -1827,6 +2070,7 @@ function registerHooks() {
|
|
|
1827
2070
|
const settings = readJsonFile(CLAUDE_SETTINGS);
|
|
1828
2071
|
const hooks = settings["hooks"] ?? {};
|
|
1829
2072
|
hooks["SessionStart"] = replaceEngrmHook(hooks["SessionStart"], { hooks: [{ type: "command", command: sessionStartCmd }] }, "session-start");
|
|
2073
|
+
hooks["UserPromptSubmit"] = replaceEngrmHook(hooks["UserPromptSubmit"], { hooks: [{ type: "command", command: userPromptSubmitCmd }] }, "user-prompt-submit");
|
|
1830
2074
|
hooks["PreCompact"] = replaceEngrmHook(hooks["PreCompact"], { hooks: [{ type: "command", command: preCompactCmd }] }, "pre-compact");
|
|
1831
2075
|
hooks["PreToolUse"] = replaceEngrmHook(hooks["PreToolUse"], {
|
|
1832
2076
|
matcher: "Edit|Write",
|
|
@@ -1878,7 +2122,7 @@ function registerAll() {
|
|
|
1878
2122
|
|
|
1879
2123
|
// src/packs/loader.ts
|
|
1880
2124
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
1881
|
-
import { join as join4, dirname as
|
|
2125
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
1882
2126
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1883
2127
|
|
|
1884
2128
|
// src/tools/save.ts
|
|
@@ -2206,7 +2450,7 @@ function looksMeaningful(value) {
|
|
|
2206
2450
|
// src/storage/projects.ts
|
|
2207
2451
|
import { execSync } from "node:child_process";
|
|
2208
2452
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2209
|
-
import { basename, join as join3 } from "node:path";
|
|
2453
|
+
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
|
2210
2454
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
2211
2455
|
let url = remoteUrl.trim();
|
|
2212
2456
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -2260,6 +2504,19 @@ function getGitRemoteUrl(directory) {
|
|
|
2260
2504
|
}
|
|
2261
2505
|
}
|
|
2262
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
|
+
}
|
|
2263
2520
|
function readProjectConfigFile(directory) {
|
|
2264
2521
|
const configPath = join3(directory, ".engrm.json");
|
|
2265
2522
|
if (!existsSync3(configPath))
|
|
@@ -2282,11 +2539,12 @@ function detectProject(directory) {
|
|
|
2282
2539
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
2283
2540
|
if (remoteUrl) {
|
|
2284
2541
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2542
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
2285
2543
|
return {
|
|
2286
2544
|
canonical_id: canonicalId,
|
|
2287
2545
|
name: projectNameFromCanonicalId(canonicalId),
|
|
2288
2546
|
remote_url: remoteUrl,
|
|
2289
|
-
local_path:
|
|
2547
|
+
local_path: repoRoot
|
|
2290
2548
|
};
|
|
2291
2549
|
}
|
|
2292
2550
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -2306,6 +2564,32 @@ function detectProject(directory) {
|
|
|
2306
2564
|
local_path: directory
|
|
2307
2565
|
};
|
|
2308
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
|
+
}
|
|
2309
2593
|
|
|
2310
2594
|
// src/embeddings/embedder.ts
|
|
2311
2595
|
var _available = null;
|
|
@@ -2584,7 +2868,8 @@ async function saveObservation(db, config, input) {
|
|
|
2584
2868
|
return { success: false, reason: "Title is required" };
|
|
2585
2869
|
}
|
|
2586
2870
|
const cwd = input.cwd ?? process.cwd();
|
|
2587
|
-
const
|
|
2871
|
+
const touchedPaths = [...input.files_read ?? [], ...input.files_modified ?? []];
|
|
2872
|
+
const detected = touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, cwd) : detectProject(cwd);
|
|
2588
2873
|
const project = db.upsertProject({
|
|
2589
2874
|
canonical_id: detected.canonical_id,
|
|
2590
2875
|
name: detected.name,
|
|
@@ -2711,7 +2996,7 @@ function toRelativePath(filePath, projectRoot) {
|
|
|
2711
2996
|
|
|
2712
2997
|
// src/packs/loader.ts
|
|
2713
2998
|
function getPacksDir() {
|
|
2714
|
-
const thisDir =
|
|
2999
|
+
const thisDir = dirname3(fileURLToPath2(import.meta.url));
|
|
2715
3000
|
return join4(thisDir, "..", "..", "packs");
|
|
2716
3001
|
}
|
|
2717
3002
|
function listPacks() {
|
|
@@ -2761,10 +3046,10 @@ async function installPack(db, config, packName, cwd) {
|
|
|
2761
3046
|
|
|
2762
3047
|
// src/sentinel/rules.ts
|
|
2763
3048
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
2764
|
-
import { join as join5, dirname as
|
|
3049
|
+
import { join as join5, dirname as dirname4 } from "node:path";
|
|
2765
3050
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2766
3051
|
function getRulePacksDir() {
|
|
2767
|
-
const thisDir =
|
|
3052
|
+
const thisDir = dirname4(fileURLToPath3(import.meta.url));
|
|
2768
3053
|
return join5(thisDir, "rule-packs");
|
|
2769
3054
|
}
|
|
2770
3055
|
function listRulePacks() {
|
|
@@ -2813,11 +3098,152 @@ async function installRulePacks(db, config, packNames) {
|
|
|
2813
3098
|
return { installed, skipped };
|
|
2814
3099
|
}
|
|
2815
3100
|
|
|
2816
|
-
// src/
|
|
3101
|
+
// src/tools/capture-status.ts
|
|
3102
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
3103
|
+
import { homedir as homedir3 } from "node:os";
|
|
3104
|
+
import { join as join6 } from "node:path";
|
|
2817
3105
|
var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
|
|
3106
|
+
function getCaptureStatus(db, input = {}) {
|
|
3107
|
+
const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
|
|
3108
|
+
const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
|
|
3109
|
+
const home = input.home_dir ?? homedir3();
|
|
3110
|
+
const claudeJson = join6(home, ".claude.json");
|
|
3111
|
+
const claudeSettings = join6(home, ".claude", "settings.json");
|
|
3112
|
+
const codexConfig = join6(home, ".codex", "config.toml");
|
|
3113
|
+
const codexHooks = join6(home, ".codex", "hooks.json");
|
|
3114
|
+
const claudeJsonContent = existsSync6(claudeJson) ? readFileSync6(claudeJson, "utf-8") : "";
|
|
3115
|
+
const claudeSettingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
|
|
3116
|
+
const codexConfigContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
|
|
3117
|
+
const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
|
|
3118
|
+
const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
|
|
3119
|
+
const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
|
|
3120
|
+
const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
|
|
3121
|
+
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3122
|
+
let claudeHookCount = 0;
|
|
3123
|
+
let claudeSessionStartHook = false;
|
|
3124
|
+
let claudeUserPromptHook = false;
|
|
3125
|
+
let claudePostToolHook = false;
|
|
3126
|
+
let claudeStopHook = false;
|
|
3127
|
+
if (claudeHooksRegistered) {
|
|
3128
|
+
try {
|
|
3129
|
+
const settings = JSON.parse(claudeSettingsContent);
|
|
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"]);
|
|
3135
|
+
for (const entries of Object.values(hooks)) {
|
|
3136
|
+
if (!Array.isArray(entries))
|
|
3137
|
+
continue;
|
|
3138
|
+
for (const entry of entries) {
|
|
3139
|
+
const e = entry;
|
|
3140
|
+
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
3141
|
+
claudeHookCount++;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
} catch {}
|
|
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 {}
|
|
3154
|
+
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
3155
|
+
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
3156
|
+
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
3157
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
3158
|
+
const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
|
|
3159
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
3160
|
+
const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
|
|
3161
|
+
FROM sessions s
|
|
3162
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
3163
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
3164
|
+
AND (
|
|
3165
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3166
|
+
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
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;
|
|
3179
|
+
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
3180
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3181
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
3182
|
+
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
3183
|
+
const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
|
|
3184
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3185
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
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");
|
|
3190
|
+
const schemaVersion = getSchemaVersion(db.db);
|
|
3191
|
+
return {
|
|
3192
|
+
schema_version: schemaVersion,
|
|
3193
|
+
schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
|
|
3194
|
+
claude_mcp_registered: claudeMcpRegistered,
|
|
3195
|
+
claude_hooks_registered: claudeHooksRegistered,
|
|
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,
|
|
3201
|
+
codex_mcp_registered: codexMcpRegistered,
|
|
3202
|
+
codex_hooks_registered: codexHooksRegistered,
|
|
3203
|
+
codex_session_start_hook: codexSessionStartHook,
|
|
3204
|
+
codex_stop_hook: codexStopHook,
|
|
3205
|
+
codex_raw_chronology_supported: false,
|
|
3206
|
+
recent_user_prompts: recentUserPrompts,
|
|
3207
|
+
recent_tool_events: recentToolEvents,
|
|
3208
|
+
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
3209
|
+
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
3210
|
+
latest_prompt_epoch: latestPromptEpoch,
|
|
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,
|
|
3215
|
+
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
3216
|
+
};
|
|
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
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// src/sync/auth.ts
|
|
3226
|
+
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
3227
|
+
function normalizeBaseUrl(url) {
|
|
3228
|
+
const trimmed = url.trim();
|
|
3229
|
+
if (!trimmed)
|
|
3230
|
+
return trimmed;
|
|
3231
|
+
try {
|
|
3232
|
+
const parsed = new URL(trimmed);
|
|
3233
|
+
if (LEGACY_PUBLIC_HOSTS.has(parsed.hostname)) {
|
|
3234
|
+
parsed.hostname = "engrm.dev";
|
|
3235
|
+
}
|
|
3236
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3237
|
+
} catch {
|
|
3238
|
+
return trimmed.replace(/\/$/, "");
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/cli.ts
|
|
3243
|
+
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
2818
3244
|
var args = process.argv.slice(2);
|
|
2819
3245
|
var command = args[0];
|
|
2820
|
-
var THIS_DIR =
|
|
3246
|
+
var THIS_DIR = dirname5(fileURLToPath4(import.meta.url));
|
|
2821
3247
|
var IS_BUILT_DIST = THIS_DIR.endsWith("/dist") || THIS_DIR.endsWith("\\dist");
|
|
2822
3248
|
switch (command) {
|
|
2823
3249
|
case "init":
|
|
@@ -3041,13 +3467,13 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
3041
3467
|
console.log(`Database initialised at ${getDbPath()}`);
|
|
3042
3468
|
}
|
|
3043
3469
|
function initFromFile(configPath) {
|
|
3044
|
-
if (!
|
|
3470
|
+
if (!existsSync7(configPath)) {
|
|
3045
3471
|
console.error(`Config file not found: ${configPath}`);
|
|
3046
3472
|
process.exit(1);
|
|
3047
3473
|
}
|
|
3048
3474
|
let parsed;
|
|
3049
3475
|
try {
|
|
3050
|
-
const raw =
|
|
3476
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
3051
3477
|
parsed = JSON.parse(raw);
|
|
3052
3478
|
} catch {
|
|
3053
3479
|
console.error(`Invalid JSON in ${configPath}`);
|
|
@@ -3134,7 +3560,7 @@ async function initManual() {
|
|
|
3134
3560
|
return;
|
|
3135
3561
|
}
|
|
3136
3562
|
}
|
|
3137
|
-
const candengoUrl = await prompt("
|
|
3563
|
+
const candengoUrl = await prompt("Engrm server URL (e.g. https://engrm.dev): ");
|
|
3138
3564
|
const apiKey = await prompt("API key (cvk_...): ");
|
|
3139
3565
|
const siteId = await prompt("Site ID: ");
|
|
3140
3566
|
const namespace = await prompt("Namespace: ");
|
|
@@ -3227,18 +3653,18 @@ function handleStatus() {
|
|
|
3227
3653
|
console.log(` Plan: ${tierLabels[tier] ?? tier}`);
|
|
3228
3654
|
console.log(`
|
|
3229
3655
|
Integration`);
|
|
3230
|
-
console.log(`
|
|
3656
|
+
console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
|
|
3231
3657
|
console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
|
|
3232
|
-
const claudeJson =
|
|
3233
|
-
const claudeSettings =
|
|
3234
|
-
const codexConfig =
|
|
3235
|
-
const codexHooks =
|
|
3236
|
-
const mcpRegistered =
|
|
3237
|
-
const settingsContent =
|
|
3238
|
-
const codexContent =
|
|
3239
|
-
const codexHooksContent =
|
|
3240
|
-
const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start");
|
|
3241
|
-
const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${
|
|
3658
|
+
const claudeJson = join7(homedir4(), ".claude.json");
|
|
3659
|
+
const claudeSettings = join7(homedir4(), ".claude", "settings.json");
|
|
3660
|
+
const codexConfig = join7(homedir4(), ".codex", "config.toml");
|
|
3661
|
+
const codexHooks = join7(homedir4(), ".codex", "hooks.json");
|
|
3662
|
+
const mcpRegistered = existsSync7(claudeJson) && readFileSync7(claudeJson, "utf-8").includes('"engrm"');
|
|
3663
|
+
const settingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
|
|
3664
|
+
const codexContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
|
|
3665
|
+
const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
|
|
3666
|
+
const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start") || settingsContent.includes("user-prompt-submit");
|
|
3667
|
+
const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`);
|
|
3242
3668
|
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3243
3669
|
let hookCount = 0;
|
|
3244
3670
|
if (hooksRegistered) {
|
|
@@ -3249,7 +3675,7 @@ function handleStatus() {
|
|
|
3249
3675
|
if (Array.isArray(entries)) {
|
|
3250
3676
|
for (const entry of entries) {
|
|
3251
3677
|
const e = entry;
|
|
3252
|
-
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
3678
|
+
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
3253
3679
|
hookCount++;
|
|
3254
3680
|
}
|
|
3255
3681
|
}
|
|
@@ -3269,7 +3695,7 @@ function handleStatus() {
|
|
|
3269
3695
|
if (config.sentinel.provider) {
|
|
3270
3696
|
console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
|
|
3271
3697
|
}
|
|
3272
|
-
if (
|
|
3698
|
+
if (existsSync7(getDbPath())) {
|
|
3273
3699
|
try {
|
|
3274
3700
|
const db = new MemDatabase(getDbPath());
|
|
3275
3701
|
const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
|
|
@@ -3282,7 +3708,7 @@ function handleStatus() {
|
|
|
3282
3708
|
console.log(`
|
|
3283
3709
|
Sentinel: disabled`);
|
|
3284
3710
|
}
|
|
3285
|
-
if (
|
|
3711
|
+
if (existsSync7(getDbPath())) {
|
|
3286
3712
|
try {
|
|
3287
3713
|
const db = new MemDatabase(getDbPath());
|
|
3288
3714
|
const obsCount = db.getActiveObservationCount();
|
|
@@ -3301,6 +3727,19 @@ function handleStatus() {
|
|
|
3301
3727
|
} catch {}
|
|
3302
3728
|
const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
|
|
3303
3729
|
console.log(` Sessions: ${summaryCount} summarised`);
|
|
3730
|
+
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
3731
|
+
console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
|
|
3732
|
+
console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
|
|
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
|
+
}
|
|
3304
3743
|
try {
|
|
3305
3744
|
const activeObservations = db.db.query(`SELECT * FROM observations
|
|
3306
3745
|
WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
|
|
@@ -3374,8 +3813,8 @@ function handleStatus() {
|
|
|
3374
3813
|
Files`);
|
|
3375
3814
|
console.log(` Config: ${getSettingsPath()}`);
|
|
3376
3815
|
console.log(` Database: ${getDbPath()}`);
|
|
3377
|
-
console.log(` Codex config: ${
|
|
3378
|
-
console.log(` Codex hooks: ${
|
|
3816
|
+
console.log(` Codex config: ${join7(homedir4(), ".codex", "config.toml")}`);
|
|
3817
|
+
console.log(` Codex hooks: ${join7(homedir4(), ".codex", "hooks.json")}`);
|
|
3379
3818
|
}
|
|
3380
3819
|
function formatTimeAgo(epoch) {
|
|
3381
3820
|
const ago = Math.floor(Date.now() / 1000) - epoch;
|
|
@@ -3397,7 +3836,7 @@ function formatSyncTime(epochStr) {
|
|
|
3397
3836
|
}
|
|
3398
3837
|
function ensureConfigDir() {
|
|
3399
3838
|
const dir = getConfigDir();
|
|
3400
|
-
if (!
|
|
3839
|
+
if (!existsSync7(dir)) {
|
|
3401
3840
|
mkdirSync3(dir, { recursive: true });
|
|
3402
3841
|
}
|
|
3403
3842
|
}
|
|
@@ -3418,7 +3857,7 @@ function generateDeviceId2() {
|
|
|
3418
3857
|
break;
|
|
3419
3858
|
}
|
|
3420
3859
|
const material = `${host}:${mac || "no-mac"}`;
|
|
3421
|
-
const suffix =
|
|
3860
|
+
const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
|
|
3422
3861
|
return `${host}-${suffix}`;
|
|
3423
3862
|
}
|
|
3424
3863
|
async function handleInstallPack(flags) {
|
|
@@ -3552,7 +3991,7 @@ async function handleDoctor() {
|
|
|
3552
3991
|
return;
|
|
3553
3992
|
}
|
|
3554
3993
|
try {
|
|
3555
|
-
const currentVersion =
|
|
3994
|
+
const currentVersion = getSchemaVersion2(db.db);
|
|
3556
3995
|
if (currentVersion >= LATEST_SCHEMA_VERSION2) {
|
|
3557
3996
|
pass(`Database schema is current (v${currentVersion})`);
|
|
3558
3997
|
} else {
|
|
@@ -3561,10 +4000,10 @@ async function handleDoctor() {
|
|
|
3561
4000
|
} catch {
|
|
3562
4001
|
warn("Could not check database schema version");
|
|
3563
4002
|
}
|
|
3564
|
-
const claudeJson =
|
|
4003
|
+
const claudeJson = join7(homedir4(), ".claude.json");
|
|
3565
4004
|
try {
|
|
3566
|
-
if (
|
|
3567
|
-
const content =
|
|
4005
|
+
if (existsSync7(claudeJson)) {
|
|
4006
|
+
const content = readFileSync7(claudeJson, "utf-8");
|
|
3568
4007
|
if (content.includes('"engrm"')) {
|
|
3569
4008
|
pass("MCP server registered in Claude Code");
|
|
3570
4009
|
} else {
|
|
@@ -3576,27 +4015,46 @@ async function handleDoctor() {
|
|
|
3576
4015
|
} catch {
|
|
3577
4016
|
warn("Could not check MCP server registration");
|
|
3578
4017
|
}
|
|
3579
|
-
const claudeSettings =
|
|
4018
|
+
const claudeSettings = join7(homedir4(), ".claude", "settings.json");
|
|
3580
4019
|
try {
|
|
3581
|
-
if (
|
|
3582
|
-
const content =
|
|
4020
|
+
if (existsSync7(claudeSettings)) {
|
|
4021
|
+
const content = readFileSync7(claudeSettings, "utf-8");
|
|
3583
4022
|
let hookCount = 0;
|
|
4023
|
+
let hasSessionStart = false;
|
|
4024
|
+
let hasUserPrompt = false;
|
|
4025
|
+
let hasPostToolUse = false;
|
|
4026
|
+
let hasStop = false;
|
|
3584
4027
|
try {
|
|
3585
4028
|
const settings = JSON.parse(content);
|
|
3586
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"]);
|
|
3587
4034
|
for (const entries of Object.values(hooks)) {
|
|
3588
4035
|
if (Array.isArray(entries)) {
|
|
3589
4036
|
for (const entry of entries) {
|
|
3590
4037
|
const e = entry;
|
|
3591
|
-
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
4038
|
+
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
3592
4039
|
hookCount++;
|
|
3593
4040
|
}
|
|
3594
4041
|
}
|
|
3595
4042
|
}
|
|
3596
4043
|
}
|
|
3597
4044
|
} catch {}
|
|
3598
|
-
|
|
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) {
|
|
3599
4055
|
pass(`Hooks registered (${hookCount} hook${hookCount === 1 ? "" : "s"})`);
|
|
4056
|
+
} else if (hookCount > 0) {
|
|
4057
|
+
warn(`Hooks registered but incomplete \u2014 missing ${missingCritical.join(", ")}`);
|
|
3600
4058
|
} else {
|
|
3601
4059
|
warn("No Engrm hooks found in Claude Code settings");
|
|
3602
4060
|
}
|
|
@@ -3606,11 +4064,11 @@ async function handleDoctor() {
|
|
|
3606
4064
|
} catch {
|
|
3607
4065
|
warn("Could not check hooks registration");
|
|
3608
4066
|
}
|
|
3609
|
-
const codexConfig =
|
|
4067
|
+
const codexConfig = join7(homedir4(), ".codex", "config.toml");
|
|
3610
4068
|
try {
|
|
3611
|
-
if (
|
|
3612
|
-
const content =
|
|
3613
|
-
if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${
|
|
4069
|
+
if (existsSync7(codexConfig)) {
|
|
4070
|
+
const content = readFileSync7(codexConfig, "utf-8");
|
|
4071
|
+
if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`)) {
|
|
3614
4072
|
pass("MCP server registered in Codex");
|
|
3615
4073
|
} else {
|
|
3616
4074
|
warn("MCP server not registered in Codex");
|
|
@@ -3621,10 +4079,10 @@ async function handleDoctor() {
|
|
|
3621
4079
|
} catch {
|
|
3622
4080
|
warn("Could not check Codex MCP registration");
|
|
3623
4081
|
}
|
|
3624
|
-
const codexHooks =
|
|
4082
|
+
const codexHooks = join7(homedir4(), ".codex", "hooks.json");
|
|
3625
4083
|
try {
|
|
3626
|
-
if (
|
|
3627
|
-
const content =
|
|
4084
|
+
if (existsSync7(codexHooks)) {
|
|
4085
|
+
const content = readFileSync7(codexHooks, "utf-8");
|
|
3628
4086
|
if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
|
|
3629
4087
|
pass("Hooks registered in Codex");
|
|
3630
4088
|
} else {
|
|
@@ -3638,14 +4096,23 @@ async function handleDoctor() {
|
|
|
3638
4096
|
}
|
|
3639
4097
|
if (config.candengo_url) {
|
|
3640
4098
|
try {
|
|
4099
|
+
const baseUrl = normalizeBaseUrl(config.candengo_url);
|
|
3641
4100
|
const controller = new AbortController;
|
|
3642
4101
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3643
4102
|
const start = Date.now();
|
|
3644
|
-
|
|
4103
|
+
let res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
|
|
4104
|
+
if (res.status === 404) {
|
|
4105
|
+
res = await fetch(`${baseUrl}/v1/mem/provision`, {
|
|
4106
|
+
method: "POST",
|
|
4107
|
+
headers: { "Content-Type": "application/json" },
|
|
4108
|
+
body: "{}",
|
|
4109
|
+
signal: controller.signal
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
3645
4112
|
clearTimeout(timeout);
|
|
3646
4113
|
const elapsed = Date.now() - start;
|
|
3647
|
-
if (res.ok) {
|
|
3648
|
-
const host = new URL(
|
|
4114
|
+
if (res.ok || res.status === 400) {
|
|
4115
|
+
const host = new URL(baseUrl).hostname;
|
|
3649
4116
|
pass(`Server connectivity (${host}, ${elapsed}ms)`);
|
|
3650
4117
|
} else {
|
|
3651
4118
|
fail(`Server returned HTTP ${res.status}`);
|
|
@@ -3659,16 +4126,16 @@ async function handleDoctor() {
|
|
|
3659
4126
|
}
|
|
3660
4127
|
if (config.candengo_url && config.candengo_api_key) {
|
|
3661
4128
|
try {
|
|
4129
|
+
const baseUrl = normalizeBaseUrl(config.candengo_url);
|
|
3662
4130
|
const controller = new AbortController;
|
|
3663
4131
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3664
|
-
const res = await fetch(`${
|
|
4132
|
+
const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
|
|
3665
4133
|
headers: { Authorization: `Bearer ${config.candengo_api_key}` },
|
|
3666
4134
|
signal: controller.signal
|
|
3667
4135
|
});
|
|
3668
4136
|
clearTimeout(timeout);
|
|
3669
4137
|
if (res.ok) {
|
|
3670
|
-
const
|
|
3671
|
-
const email = data.email ?? config.user_email ?? "unknown";
|
|
4138
|
+
const email = config.user_email ?? "configured";
|
|
3672
4139
|
pass(`Authentication valid (${email})`);
|
|
3673
4140
|
} else if (res.status === 401 || res.status === 403) {
|
|
3674
4141
|
fail("Authentication failed \u2014 API key may be expired");
|
|
@@ -3717,9 +4184,27 @@ async function handleDoctor() {
|
|
|
3717
4184
|
} catch {
|
|
3718
4185
|
warn("Could not count observations");
|
|
3719
4186
|
}
|
|
4187
|
+
try {
|
|
4188
|
+
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
4189
|
+
if (capture.raw_capture_active && capture.recent_tool_events > 0 && capture.recent_sessions_with_partial_capture === 0) {
|
|
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
|
+
}
|
|
4196
|
+
} else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
|
|
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}`);
|
|
4199
|
+
} else {
|
|
4200
|
+
warn("Raw chronology inactive \u2014 hook registration is incomplete");
|
|
4201
|
+
}
|
|
4202
|
+
} catch {
|
|
4203
|
+
warn("Could not check raw chronology capture");
|
|
4204
|
+
}
|
|
3720
4205
|
try {
|
|
3721
4206
|
const dbPath = getDbPath();
|
|
3722
|
-
if (
|
|
4207
|
+
if (existsSync7(dbPath)) {
|
|
3723
4208
|
const stats = statSync(dbPath);
|
|
3724
4209
|
const sizeMB = stats.size / (1024 * 1024);
|
|
3725
4210
|
const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
|
|
@@ -3787,11 +4272,11 @@ Registering with Claude Code and Codex...`);
|
|
|
3787
4272
|
console.log(`
|
|
3788
4273
|
Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
|
|
3789
4274
|
} catch (error) {
|
|
3790
|
-
const packageRoot =
|
|
4275
|
+
const packageRoot = join7(THIS_DIR, "..");
|
|
3791
4276
|
const runtime = IS_BUILT_DIST ? process.execPath : "bun";
|
|
3792
|
-
const serverArgs = IS_BUILT_DIST ? [
|
|
3793
|
-
const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${
|
|
3794
|
-
const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${
|
|
4277
|
+
const serverArgs = IS_BUILT_DIST ? [join7(packageRoot, "dist", "server.js")] : ["run", join7(packageRoot, "src", "server.ts")];
|
|
4278
|
+
const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join7(packageRoot, "hooks", "session-start.ts")}`;
|
|
4279
|
+
const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join7(packageRoot, "hooks", "codex-stop.ts")}`;
|
|
3795
4280
|
console.log(`
|
|
3796
4281
|
Could not auto-register with Claude Code and Codex.`);
|
|
3797
4282
|
console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|