engrm 0.4.6 → 0.4.8
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 +532 -55
- package/dist/hooks/elicitation-result.js +248 -1
- package/dist/hooks/post-tool-use.js +286 -1
- package/dist/hooks/pre-compact.js +292 -9
- package/dist/hooks/sentinel.js +166 -0
- package/dist/hooks/session-start.js +376 -17
- package/dist/hooks/stop.js +489 -15
- package/dist/hooks/user-prompt-submit.js +1387 -0
- package/dist/server.js +1895 -48
- 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 dirname4, join 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 dirname4, 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) {
|
|
@@ -886,6 +949,110 @@ class MemDatabase {
|
|
|
886
949
|
const now = Math.floor(Date.now() / 1000);
|
|
887
950
|
this.db.query("UPDATE sessions SET status = 'completed', completed_at_epoch = ? WHERE session_id = ?").run(now, sessionId);
|
|
888
951
|
}
|
|
952
|
+
getSessionById(sessionId) {
|
|
953
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId) ?? null;
|
|
954
|
+
}
|
|
955
|
+
getRecentSessions(projectId, limit = 10, userId) {
|
|
956
|
+
const visibilityClause = userId ? " AND s.user_id = ?" : "";
|
|
957
|
+
if (projectId !== null) {
|
|
958
|
+
return this.db.query(`SELECT
|
|
959
|
+
s.*,
|
|
960
|
+
p.name AS project_name,
|
|
961
|
+
ss.request AS request,
|
|
962
|
+
ss.completed AS completed,
|
|
963
|
+
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
964
|
+
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
965
|
+
FROM sessions s
|
|
966
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
967
|
+
LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
|
|
968
|
+
WHERE s.project_id = ?${visibilityClause}
|
|
969
|
+
ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
|
|
970
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
971
|
+
}
|
|
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 1 = 1${visibilityClause}
|
|
983
|
+
ORDER BY COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) DESC, s.id DESC
|
|
984
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
985
|
+
}
|
|
986
|
+
insertUserPrompt(input) {
|
|
987
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
988
|
+
const normalizedPrompt = input.prompt.trim();
|
|
989
|
+
const promptHash = hashPrompt(normalizedPrompt);
|
|
990
|
+
const latest = this.db.query(`SELECT * FROM user_prompts
|
|
991
|
+
WHERE session_id = ?
|
|
992
|
+
ORDER BY prompt_number DESC
|
|
993
|
+
LIMIT 1`).get(input.session_id);
|
|
994
|
+
if (latest && latest.prompt_hash === promptHash) {
|
|
995
|
+
return latest;
|
|
996
|
+
}
|
|
997
|
+
const promptNumber = (latest?.prompt_number ?? 0) + 1;
|
|
998
|
+
const result = this.db.query(`INSERT INTO user_prompts (
|
|
999
|
+
session_id, project_id, prompt_number, prompt, prompt_hash, cwd,
|
|
1000
|
+
user_id, device_id, agent, created_at_epoch
|
|
1001
|
+
) 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);
|
|
1002
|
+
return this.getUserPromptById(Number(result.lastInsertRowid));
|
|
1003
|
+
}
|
|
1004
|
+
getUserPromptById(id) {
|
|
1005
|
+
return this.db.query("SELECT * FROM user_prompts WHERE id = ?").get(id) ?? null;
|
|
1006
|
+
}
|
|
1007
|
+
getRecentUserPrompts(projectId, limit = 10, userId) {
|
|
1008
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1009
|
+
if (projectId !== null) {
|
|
1010
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1011
|
+
WHERE project_id = ?${visibilityClause}
|
|
1012
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
1013
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1014
|
+
}
|
|
1015
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1016
|
+
WHERE 1 = 1${visibilityClause}
|
|
1017
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
1018
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1019
|
+
}
|
|
1020
|
+
getSessionUserPrompts(sessionId, limit = 20) {
|
|
1021
|
+
return this.db.query(`SELECT * FROM user_prompts
|
|
1022
|
+
WHERE session_id = ?
|
|
1023
|
+
ORDER BY prompt_number ASC
|
|
1024
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1025
|
+
}
|
|
1026
|
+
insertToolEvent(input) {
|
|
1027
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1028
|
+
const result = this.db.query(`INSERT INTO tool_events (
|
|
1029
|
+
session_id, project_id, tool_name, tool_input_json, tool_response_preview,
|
|
1030
|
+
file_path, command, user_id, device_id, agent, created_at_epoch
|
|
1031
|
+
) 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);
|
|
1032
|
+
return this.getToolEventById(Number(result.lastInsertRowid));
|
|
1033
|
+
}
|
|
1034
|
+
getToolEventById(id) {
|
|
1035
|
+
return this.db.query("SELECT * FROM tool_events WHERE id = ?").get(id) ?? null;
|
|
1036
|
+
}
|
|
1037
|
+
getSessionToolEvents(sessionId, limit = 20) {
|
|
1038
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1039
|
+
WHERE session_id = ?
|
|
1040
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1041
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1042
|
+
}
|
|
1043
|
+
getRecentToolEvents(projectId, limit = 20, userId) {
|
|
1044
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1045
|
+
if (projectId !== null) {
|
|
1046
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1047
|
+
WHERE project_id = ?${visibilityClause}
|
|
1048
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1049
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1050
|
+
}
|
|
1051
|
+
return this.db.query(`SELECT * FROM tool_events
|
|
1052
|
+
WHERE 1 = 1${visibilityClause}
|
|
1053
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1054
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1055
|
+
}
|
|
889
1056
|
addToOutbox(recordType, recordId) {
|
|
890
1057
|
const now = Math.floor(Date.now() / 1000);
|
|
891
1058
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1056,6 +1223,9 @@ class MemDatabase {
|
|
|
1056
1223
|
this.db.query("INSERT OR REPLACE INTO packs_installed (name, installed_at, observation_count) VALUES (?, ?, ?)").run(name, now, observationCount);
|
|
1057
1224
|
}
|
|
1058
1225
|
}
|
|
1226
|
+
function hashPrompt(prompt) {
|
|
1227
|
+
return createHash2("sha256").update(prompt).digest("hex");
|
|
1228
|
+
}
|
|
1059
1229
|
|
|
1060
1230
|
// src/storage/outbox.ts
|
|
1061
1231
|
function getOutboxStats(db) {
|
|
@@ -1072,6 +1242,31 @@ function getOutboxStats(db) {
|
|
|
1072
1242
|
return stats;
|
|
1073
1243
|
}
|
|
1074
1244
|
|
|
1245
|
+
// src/intelligence/value-signals.ts
|
|
1246
|
+
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
1247
|
+
function computeSessionValueSignals(observations, securityFindings = []) {
|
|
1248
|
+
const decisionsCount = observations.filter((o) => o.type === "decision").length;
|
|
1249
|
+
const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
|
|
1250
|
+
const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
|
|
1251
|
+
const featuresCount = observations.filter((o) => o.type === "feature").length;
|
|
1252
|
+
const refactorsCount = observations.filter((o) => o.type === "refactor").length;
|
|
1253
|
+
const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
|
|
1254
|
+
const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
|
|
1255
|
+
const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
|
|
1256
|
+
return {
|
|
1257
|
+
decisions_count: decisionsCount,
|
|
1258
|
+
lessons_count: lessonsCount,
|
|
1259
|
+
discoveries_count: discoveriesCount,
|
|
1260
|
+
features_count: featuresCount,
|
|
1261
|
+
refactors_count: refactorsCount,
|
|
1262
|
+
repeated_patterns_count: repeatedPatternsCount,
|
|
1263
|
+
security_findings_count: securityFindings.length,
|
|
1264
|
+
critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
|
|
1265
|
+
delivery_review_ready: hasRequestSignal && hasCompletionSignal,
|
|
1266
|
+
vibe_guardian_active: securityFindings.length > 0
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1075
1270
|
// src/storage/migrations.ts
|
|
1076
1271
|
var MIGRATIONS2 = [
|
|
1077
1272
|
{
|
|
@@ -1385,6 +1580,64 @@ var MIGRATIONS2 = [
|
|
|
1385
1580
|
);
|
|
1386
1581
|
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
1387
1582
|
`
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
version: 9,
|
|
1586
|
+
description: "Add first-class user prompt capture",
|
|
1587
|
+
sql: `
|
|
1588
|
+
CREATE TABLE IF NOT EXISTS user_prompts (
|
|
1589
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1590
|
+
session_id TEXT NOT NULL,
|
|
1591
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1592
|
+
prompt_number INTEGER NOT NULL,
|
|
1593
|
+
prompt TEXT NOT NULL,
|
|
1594
|
+
prompt_hash TEXT NOT NULL,
|
|
1595
|
+
cwd TEXT,
|
|
1596
|
+
user_id TEXT NOT NULL,
|
|
1597
|
+
device_id TEXT NOT NULL,
|
|
1598
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1599
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1600
|
+
UNIQUE(session_id, prompt_number)
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_session
|
|
1604
|
+
ON user_prompts(session_id, prompt_number DESC);
|
|
1605
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_project
|
|
1606
|
+
ON user_prompts(project_id, created_at_epoch DESC);
|
|
1607
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_created
|
|
1608
|
+
ON user_prompts(created_at_epoch DESC);
|
|
1609
|
+
CREATE INDEX IF NOT EXISTS idx_user_prompts_hash
|
|
1610
|
+
ON user_prompts(prompt_hash);
|
|
1611
|
+
`
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
version: 10,
|
|
1615
|
+
description: "Add first-class tool event chronology",
|
|
1616
|
+
sql: `
|
|
1617
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
1618
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1619
|
+
session_id TEXT NOT NULL,
|
|
1620
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1621
|
+
tool_name TEXT NOT NULL,
|
|
1622
|
+
tool_input_json TEXT,
|
|
1623
|
+
tool_response_preview TEXT,
|
|
1624
|
+
file_path TEXT,
|
|
1625
|
+
command TEXT,
|
|
1626
|
+
user_id TEXT NOT NULL,
|
|
1627
|
+
device_id TEXT NOT NULL,
|
|
1628
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1629
|
+
created_at_epoch INTEGER NOT NULL
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
1633
|
+
ON tool_events(session_id, created_at_epoch DESC, id DESC);
|
|
1634
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_project
|
|
1635
|
+
ON tool_events(project_id, created_at_epoch DESC, id DESC);
|
|
1636
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name
|
|
1637
|
+
ON tool_events(tool_name, created_at_epoch DESC);
|
|
1638
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
1639
|
+
ON tool_events(created_at_epoch DESC, id DESC);
|
|
1640
|
+
`
|
|
1388
1641
|
}
|
|
1389
1642
|
];
|
|
1390
1643
|
function isVecExtensionLoaded2(db) {
|
|
@@ -1395,14 +1648,14 @@ function isVecExtensionLoaded2(db) {
|
|
|
1395
1648
|
return false;
|
|
1396
1649
|
}
|
|
1397
1650
|
}
|
|
1398
|
-
function
|
|
1651
|
+
function getSchemaVersion2(db) {
|
|
1399
1652
|
const result = db.query("PRAGMA user_version").get();
|
|
1400
1653
|
return result.user_version;
|
|
1401
1654
|
}
|
|
1402
1655
|
var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
1403
1656
|
|
|
1404
1657
|
// src/provisioning/provision.ts
|
|
1405
|
-
var DEFAULT_CANDENGO_URL = "https://
|
|
1658
|
+
var DEFAULT_CANDENGO_URL = "https://engrm.dev";
|
|
1406
1659
|
|
|
1407
1660
|
class ProvisionError extends Error {
|
|
1408
1661
|
status;
|
|
@@ -1794,6 +2047,7 @@ function registerHooks() {
|
|
|
1794
2047
|
return [runtime, ...runArg, join2(hooksDir, `${name}${ext}`)].join(" ");
|
|
1795
2048
|
}
|
|
1796
2049
|
const sessionStartCmd = hookCmd("session-start");
|
|
2050
|
+
const userPromptSubmitCmd = hookCmd("user-prompt-submit");
|
|
1797
2051
|
const preCompactCmd = hookCmd("pre-compact");
|
|
1798
2052
|
const preToolUseCmd = hookCmd("sentinel");
|
|
1799
2053
|
const postToolUseCmd = hookCmd("post-tool-use");
|
|
@@ -1802,6 +2056,7 @@ function registerHooks() {
|
|
|
1802
2056
|
const settings = readJsonFile(CLAUDE_SETTINGS);
|
|
1803
2057
|
const hooks = settings["hooks"] ?? {};
|
|
1804
2058
|
hooks["SessionStart"] = replaceEngrmHook(hooks["SessionStart"], { hooks: [{ type: "command", command: sessionStartCmd }] }, "session-start");
|
|
2059
|
+
hooks["UserPromptSubmit"] = replaceEngrmHook(hooks["UserPromptSubmit"], { hooks: [{ type: "command", command: userPromptSubmitCmd }] }, "user-prompt-submit");
|
|
1805
2060
|
hooks["PreCompact"] = replaceEngrmHook(hooks["PreCompact"], { hooks: [{ type: "command", command: preCompactCmd }] }, "pre-compact");
|
|
1806
2061
|
hooks["PreToolUse"] = replaceEngrmHook(hooks["PreToolUse"], {
|
|
1807
2062
|
matcher: "Edit|Write",
|
|
@@ -2104,6 +2359,80 @@ function findDuplicate(newTitle, candidates) {
|
|
|
2104
2359
|
return bestMatch;
|
|
2105
2360
|
}
|
|
2106
2361
|
|
|
2362
|
+
// src/capture/facts.ts
|
|
2363
|
+
var FACT_ELIGIBLE_TYPES = new Set([
|
|
2364
|
+
"bugfix",
|
|
2365
|
+
"decision",
|
|
2366
|
+
"discovery",
|
|
2367
|
+
"pattern",
|
|
2368
|
+
"feature",
|
|
2369
|
+
"refactor",
|
|
2370
|
+
"change"
|
|
2371
|
+
]);
|
|
2372
|
+
function buildStructuredFacts(input) {
|
|
2373
|
+
const seedFacts = dedupeFacts(input.facts ?? []);
|
|
2374
|
+
if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
|
|
2375
|
+
return seedFacts;
|
|
2376
|
+
}
|
|
2377
|
+
const derived = [...seedFacts];
|
|
2378
|
+
if (seedFacts.length === 0 && looksMeaningful(input.title)) {
|
|
2379
|
+
derived.push(input.title.trim());
|
|
2380
|
+
}
|
|
2381
|
+
for (const sentence of extractNarrativeFacts(input.narrative)) {
|
|
2382
|
+
derived.push(sentence);
|
|
2383
|
+
}
|
|
2384
|
+
const fileFact = buildFilesFact(input.filesModified);
|
|
2385
|
+
if (fileFact) {
|
|
2386
|
+
derived.push(fileFact);
|
|
2387
|
+
}
|
|
2388
|
+
return dedupeFacts(derived).slice(0, 4);
|
|
2389
|
+
}
|
|
2390
|
+
function extractNarrativeFacts(narrative) {
|
|
2391
|
+
if (!narrative)
|
|
2392
|
+
return [];
|
|
2393
|
+
const cleaned = narrative.replace(/\s+/g, " ").trim();
|
|
2394
|
+
if (cleaned.length < 24)
|
|
2395
|
+
return [];
|
|
2396
|
+
const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
|
|
2397
|
+
return parts.slice(0, 2);
|
|
2398
|
+
}
|
|
2399
|
+
function buildFilesFact(filesModified) {
|
|
2400
|
+
if (!filesModified || filesModified.length === 0)
|
|
2401
|
+
return null;
|
|
2402
|
+
const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
|
|
2403
|
+
if (cleaned.length === 0)
|
|
2404
|
+
return null;
|
|
2405
|
+
if (cleaned.length === 1) {
|
|
2406
|
+
return `Touched ${cleaned[0]}`;
|
|
2407
|
+
}
|
|
2408
|
+
return `Touched ${cleaned.join(", ")}`;
|
|
2409
|
+
}
|
|
2410
|
+
function dedupeFacts(facts) {
|
|
2411
|
+
const seen = new Set;
|
|
2412
|
+
const result = [];
|
|
2413
|
+
for (const fact of facts) {
|
|
2414
|
+
const cleaned = fact.trim().replace(/\s+/g, " ");
|
|
2415
|
+
if (!looksMeaningful(cleaned))
|
|
2416
|
+
continue;
|
|
2417
|
+
const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
2418
|
+
if (!key || seen.has(key))
|
|
2419
|
+
continue;
|
|
2420
|
+
seen.add(key);
|
|
2421
|
+
result.push(cleaned);
|
|
2422
|
+
}
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
function looksMeaningful(value) {
|
|
2426
|
+
const cleaned = value.trim();
|
|
2427
|
+
if (cleaned.length < 12)
|
|
2428
|
+
return false;
|
|
2429
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
2430
|
+
return false;
|
|
2431
|
+
if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
|
|
2432
|
+
return false;
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2107
2436
|
// src/storage/projects.ts
|
|
2108
2437
|
import { execSync } from "node:child_process";
|
|
2109
2438
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -2495,10 +2824,17 @@ async function saveObservation(db, config, input) {
|
|
|
2495
2824
|
const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
|
|
2496
2825
|
const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
|
|
2497
2826
|
const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
|
|
2498
|
-
const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
|
|
2499
2827
|
const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
|
|
2500
2828
|
const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
|
|
2501
2829
|
const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
|
|
2830
|
+
const structuredFacts = buildStructuredFacts({
|
|
2831
|
+
type: input.type,
|
|
2832
|
+
title: input.title,
|
|
2833
|
+
narrative: input.narrative,
|
|
2834
|
+
facts: input.facts,
|
|
2835
|
+
filesModified
|
|
2836
|
+
});
|
|
2837
|
+
const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
|
|
2502
2838
|
const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
|
|
2503
2839
|
const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
|
|
2504
2840
|
let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
|
|
@@ -2707,8 +3043,103 @@ async function installRulePacks(db, config, packNames) {
|
|
|
2707
3043
|
return { installed, skipped };
|
|
2708
3044
|
}
|
|
2709
3045
|
|
|
2710
|
-
// src/
|
|
3046
|
+
// src/tools/capture-status.ts
|
|
3047
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
3048
|
+
import { homedir as homedir3 } from "node:os";
|
|
3049
|
+
import { join as join6 } from "node:path";
|
|
2711
3050
|
var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
|
|
3051
|
+
function getCaptureStatus(db, input = {}) {
|
|
3052
|
+
const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
|
|
3053
|
+
const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
|
|
3054
|
+
const home = input.home_dir ?? homedir3();
|
|
3055
|
+
const claudeJson = join6(home, ".claude.json");
|
|
3056
|
+
const claudeSettings = join6(home, ".claude", "settings.json");
|
|
3057
|
+
const codexConfig = join6(home, ".codex", "config.toml");
|
|
3058
|
+
const codexHooks = join6(home, ".codex", "hooks.json");
|
|
3059
|
+
const claudeJsonContent = existsSync6(claudeJson) ? readFileSync6(claudeJson, "utf-8") : "";
|
|
3060
|
+
const claudeSettingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
|
|
3061
|
+
const codexConfigContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
|
|
3062
|
+
const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
|
|
3063
|
+
const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
|
|
3064
|
+
const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
|
|
3065
|
+
const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
|
|
3066
|
+
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3067
|
+
let claudeHookCount = 0;
|
|
3068
|
+
if (claudeHooksRegistered) {
|
|
3069
|
+
try {
|
|
3070
|
+
const settings = JSON.parse(claudeSettingsContent);
|
|
3071
|
+
const hooks = settings?.hooks ?? {};
|
|
3072
|
+
for (const entries of Object.values(hooks)) {
|
|
3073
|
+
if (!Array.isArray(entries))
|
|
3074
|
+
continue;
|
|
3075
|
+
for (const entry of entries) {
|
|
3076
|
+
const e = entry;
|
|
3077
|
+
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"))) {
|
|
3078
|
+
claudeHookCount++;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
} catch {}
|
|
3083
|
+
}
|
|
3084
|
+
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
3085
|
+
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
3086
|
+
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
3087
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
3088
|
+
const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
|
|
3089
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
3090
|
+
const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
|
|
3091
|
+
FROM sessions s
|
|
3092
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
3093
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
3094
|
+
AND (
|
|
3095
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
3096
|
+
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
3097
|
+
)`).get(...params)?.count ?? 0;
|
|
3098
|
+
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
3099
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3100
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
3101
|
+
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
3102
|
+
const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
|
|
3103
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
3104
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
3105
|
+
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
3106
|
+
const schemaVersion = getSchemaVersion(db.db);
|
|
3107
|
+
return {
|
|
3108
|
+
schema_version: schemaVersion,
|
|
3109
|
+
schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
|
|
3110
|
+
claude_mcp_registered: claudeMcpRegistered,
|
|
3111
|
+
claude_hooks_registered: claudeHooksRegistered,
|
|
3112
|
+
claude_hook_count: claudeHookCount,
|
|
3113
|
+
codex_mcp_registered: codexMcpRegistered,
|
|
3114
|
+
codex_hooks_registered: codexHooksRegistered,
|
|
3115
|
+
recent_user_prompts: recentUserPrompts,
|
|
3116
|
+
recent_tool_events: recentToolEvents,
|
|
3117
|
+
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
3118
|
+
latest_prompt_epoch: latestPromptEpoch,
|
|
3119
|
+
latest_tool_event_epoch: latestToolEventEpoch,
|
|
3120
|
+
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/sync/auth.ts
|
|
3125
|
+
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
3126
|
+
function normalizeBaseUrl(url) {
|
|
3127
|
+
const trimmed = url.trim();
|
|
3128
|
+
if (!trimmed)
|
|
3129
|
+
return trimmed;
|
|
3130
|
+
try {
|
|
3131
|
+
const parsed = new URL(trimmed);
|
|
3132
|
+
if (LEGACY_PUBLIC_HOSTS.has(parsed.hostname)) {
|
|
3133
|
+
parsed.hostname = "engrm.dev";
|
|
3134
|
+
}
|
|
3135
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3136
|
+
} catch {
|
|
3137
|
+
return trimmed.replace(/\/$/, "");
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/cli.ts
|
|
3142
|
+
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
2712
3143
|
var args = process.argv.slice(2);
|
|
2713
3144
|
var command = args[0];
|
|
2714
3145
|
var THIS_DIR = dirname4(fileURLToPath4(import.meta.url));
|
|
@@ -2935,13 +3366,13 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
2935
3366
|
console.log(`Database initialised at ${getDbPath()}`);
|
|
2936
3367
|
}
|
|
2937
3368
|
function initFromFile(configPath) {
|
|
2938
|
-
if (!
|
|
3369
|
+
if (!existsSync7(configPath)) {
|
|
2939
3370
|
console.error(`Config file not found: ${configPath}`);
|
|
2940
3371
|
process.exit(1);
|
|
2941
3372
|
}
|
|
2942
3373
|
let parsed;
|
|
2943
3374
|
try {
|
|
2944
|
-
const raw =
|
|
3375
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
2945
3376
|
parsed = JSON.parse(raw);
|
|
2946
3377
|
} catch {
|
|
2947
3378
|
console.error(`Invalid JSON in ${configPath}`);
|
|
@@ -3028,7 +3459,7 @@ async function initManual() {
|
|
|
3028
3459
|
return;
|
|
3029
3460
|
}
|
|
3030
3461
|
}
|
|
3031
|
-
const candengoUrl = await prompt("
|
|
3462
|
+
const candengoUrl = await prompt("Engrm server URL (e.g. https://engrm.dev): ");
|
|
3032
3463
|
const apiKey = await prompt("API key (cvk_...): ");
|
|
3033
3464
|
const siteId = await prompt("Site ID: ");
|
|
3034
3465
|
const namespace = await prompt("Namespace: ");
|
|
@@ -3121,18 +3552,18 @@ function handleStatus() {
|
|
|
3121
3552
|
console.log(` Plan: ${tierLabels[tier] ?? tier}`);
|
|
3122
3553
|
console.log(`
|
|
3123
3554
|
Integration`);
|
|
3124
|
-
console.log(`
|
|
3555
|
+
console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
|
|
3125
3556
|
console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
|
|
3126
|
-
const claudeJson =
|
|
3127
|
-
const claudeSettings =
|
|
3128
|
-
const codexConfig =
|
|
3129
|
-
const codexHooks =
|
|
3130
|
-
const mcpRegistered =
|
|
3131
|
-
const settingsContent =
|
|
3132
|
-
const codexContent =
|
|
3133
|
-
const codexHooksContent =
|
|
3134
|
-
const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start");
|
|
3135
|
-
const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${
|
|
3557
|
+
const claudeJson = join7(homedir4(), ".claude.json");
|
|
3558
|
+
const claudeSettings = join7(homedir4(), ".claude", "settings.json");
|
|
3559
|
+
const codexConfig = join7(homedir4(), ".codex", "config.toml");
|
|
3560
|
+
const codexHooks = join7(homedir4(), ".codex", "hooks.json");
|
|
3561
|
+
const mcpRegistered = existsSync7(claudeJson) && readFileSync7(claudeJson, "utf-8").includes('"engrm"');
|
|
3562
|
+
const settingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
|
|
3563
|
+
const codexContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
|
|
3564
|
+
const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
|
|
3565
|
+
const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start") || settingsContent.includes("user-prompt-submit");
|
|
3566
|
+
const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`);
|
|
3136
3567
|
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
3137
3568
|
let hookCount = 0;
|
|
3138
3569
|
if (hooksRegistered) {
|
|
@@ -3143,7 +3574,7 @@ function handleStatus() {
|
|
|
3143
3574
|
if (Array.isArray(entries)) {
|
|
3144
3575
|
for (const entry of entries) {
|
|
3145
3576
|
const e = entry;
|
|
3146
|
-
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"))) {
|
|
3577
|
+
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"))) {
|
|
3147
3578
|
hookCount++;
|
|
3148
3579
|
}
|
|
3149
3580
|
}
|
|
@@ -3163,7 +3594,7 @@ function handleStatus() {
|
|
|
3163
3594
|
if (config.sentinel.provider) {
|
|
3164
3595
|
console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
|
|
3165
3596
|
}
|
|
3166
|
-
if (
|
|
3597
|
+
if (existsSync7(getDbPath())) {
|
|
3167
3598
|
try {
|
|
3168
3599
|
const db = new MemDatabase(getDbPath());
|
|
3169
3600
|
const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
|
|
@@ -3176,7 +3607,7 @@ function handleStatus() {
|
|
|
3176
3607
|
console.log(`
|
|
3177
3608
|
Sentinel: disabled`);
|
|
3178
3609
|
}
|
|
3179
|
-
if (
|
|
3610
|
+
if (existsSync7(getDbPath())) {
|
|
3180
3611
|
try {
|
|
3181
3612
|
const db = new MemDatabase(getDbPath());
|
|
3182
3613
|
const obsCount = db.getActiveObservationCount();
|
|
@@ -3195,6 +3626,31 @@ function handleStatus() {
|
|
|
3195
3626
|
} catch {}
|
|
3196
3627
|
const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
|
|
3197
3628
|
console.log(` Sessions: ${summaryCount} summarised`);
|
|
3629
|
+
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
3630
|
+
console.log(` Raw capture: ${capture.raw_capture_active ? "active" : "observations-only so far"}`);
|
|
3631
|
+
console.log(` Prompts/tools: ${capture.recent_user_prompts}/${capture.recent_tool_events} in last 24h`);
|
|
3632
|
+
console.log(` Hook state: Claude ${capture.claude_hooks_registered ? "ok" : "missing"}, Codex ${capture.codex_hooks_registered ? "ok" : "missing"}`);
|
|
3633
|
+
try {
|
|
3634
|
+
const activeObservations = db.db.query(`SELECT * FROM observations
|
|
3635
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
|
|
3636
|
+
const securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
3637
|
+
ORDER BY created_at_epoch DESC
|
|
3638
|
+
LIMIT 500`).all();
|
|
3639
|
+
const signals = computeSessionValueSignals(activeObservations, securityFindings);
|
|
3640
|
+
const signalParts = [
|
|
3641
|
+
`lessons: ${signals.lessons_count}`,
|
|
3642
|
+
`decisions: ${signals.decisions_count}`,
|
|
3643
|
+
`discoveries: ${signals.discoveries_count}`,
|
|
3644
|
+
`features: ${signals.features_count}`
|
|
3645
|
+
];
|
|
3646
|
+
if (signals.repeated_patterns_count > 0) {
|
|
3647
|
+
signalParts.push(`patterns: ${signals.repeated_patterns_count}`);
|
|
3648
|
+
}
|
|
3649
|
+
console.log(` Value: ${signalParts.join(", ")}`);
|
|
3650
|
+
if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
|
|
3651
|
+
console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
|
|
3652
|
+
}
|
|
3653
|
+
} catch {}
|
|
3198
3654
|
try {
|
|
3199
3655
|
const lastSummary = db.db.query(`SELECT request, created_at_epoch FROM session_summaries
|
|
3200
3656
|
ORDER BY created_at_epoch DESC LIMIT 1`).get();
|
|
@@ -3247,8 +3703,8 @@ function handleStatus() {
|
|
|
3247
3703
|
Files`);
|
|
3248
3704
|
console.log(` Config: ${getSettingsPath()}`);
|
|
3249
3705
|
console.log(` Database: ${getDbPath()}`);
|
|
3250
|
-
console.log(` Codex config: ${
|
|
3251
|
-
console.log(` Codex hooks: ${
|
|
3706
|
+
console.log(` Codex config: ${join7(homedir4(), ".codex", "config.toml")}`);
|
|
3707
|
+
console.log(` Codex hooks: ${join7(homedir4(), ".codex", "hooks.json")}`);
|
|
3252
3708
|
}
|
|
3253
3709
|
function formatTimeAgo(epoch) {
|
|
3254
3710
|
const ago = Math.floor(Date.now() / 1000) - epoch;
|
|
@@ -3270,7 +3726,7 @@ function formatSyncTime(epochStr) {
|
|
|
3270
3726
|
}
|
|
3271
3727
|
function ensureConfigDir() {
|
|
3272
3728
|
const dir = getConfigDir();
|
|
3273
|
-
if (!
|
|
3729
|
+
if (!existsSync7(dir)) {
|
|
3274
3730
|
mkdirSync3(dir, { recursive: true });
|
|
3275
3731
|
}
|
|
3276
3732
|
}
|
|
@@ -3291,7 +3747,7 @@ function generateDeviceId2() {
|
|
|
3291
3747
|
break;
|
|
3292
3748
|
}
|
|
3293
3749
|
const material = `${host}:${mac || "no-mac"}`;
|
|
3294
|
-
const suffix =
|
|
3750
|
+
const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
|
|
3295
3751
|
return `${host}-${suffix}`;
|
|
3296
3752
|
}
|
|
3297
3753
|
async function handleInstallPack(flags) {
|
|
@@ -3425,7 +3881,7 @@ async function handleDoctor() {
|
|
|
3425
3881
|
return;
|
|
3426
3882
|
}
|
|
3427
3883
|
try {
|
|
3428
|
-
const currentVersion =
|
|
3884
|
+
const currentVersion = getSchemaVersion2(db.db);
|
|
3429
3885
|
if (currentVersion >= LATEST_SCHEMA_VERSION2) {
|
|
3430
3886
|
pass(`Database schema is current (v${currentVersion})`);
|
|
3431
3887
|
} else {
|
|
@@ -3434,10 +3890,10 @@ async function handleDoctor() {
|
|
|
3434
3890
|
} catch {
|
|
3435
3891
|
warn("Could not check database schema version");
|
|
3436
3892
|
}
|
|
3437
|
-
const claudeJson =
|
|
3893
|
+
const claudeJson = join7(homedir4(), ".claude.json");
|
|
3438
3894
|
try {
|
|
3439
|
-
if (
|
|
3440
|
-
const content =
|
|
3895
|
+
if (existsSync7(claudeJson)) {
|
|
3896
|
+
const content = readFileSync7(claudeJson, "utf-8");
|
|
3441
3897
|
if (content.includes('"engrm"')) {
|
|
3442
3898
|
pass("MCP server registered in Claude Code");
|
|
3443
3899
|
} else {
|
|
@@ -3449,10 +3905,10 @@ async function handleDoctor() {
|
|
|
3449
3905
|
} catch {
|
|
3450
3906
|
warn("Could not check MCP server registration");
|
|
3451
3907
|
}
|
|
3452
|
-
const claudeSettings =
|
|
3908
|
+
const claudeSettings = join7(homedir4(), ".claude", "settings.json");
|
|
3453
3909
|
try {
|
|
3454
|
-
if (
|
|
3455
|
-
const content =
|
|
3910
|
+
if (existsSync7(claudeSettings)) {
|
|
3911
|
+
const content = readFileSync7(claudeSettings, "utf-8");
|
|
3456
3912
|
let hookCount = 0;
|
|
3457
3913
|
try {
|
|
3458
3914
|
const settings = JSON.parse(content);
|
|
@@ -3461,7 +3917,7 @@ async function handleDoctor() {
|
|
|
3461
3917
|
if (Array.isArray(entries)) {
|
|
3462
3918
|
for (const entry of entries) {
|
|
3463
3919
|
const e = entry;
|
|
3464
|
-
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"))) {
|
|
3920
|
+
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"))) {
|
|
3465
3921
|
hookCount++;
|
|
3466
3922
|
}
|
|
3467
3923
|
}
|
|
@@ -3479,11 +3935,11 @@ async function handleDoctor() {
|
|
|
3479
3935
|
} catch {
|
|
3480
3936
|
warn("Could not check hooks registration");
|
|
3481
3937
|
}
|
|
3482
|
-
const codexConfig =
|
|
3938
|
+
const codexConfig = join7(homedir4(), ".codex", "config.toml");
|
|
3483
3939
|
try {
|
|
3484
|
-
if (
|
|
3485
|
-
const content =
|
|
3486
|
-
if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${
|
|
3940
|
+
if (existsSync7(codexConfig)) {
|
|
3941
|
+
const content = readFileSync7(codexConfig, "utf-8");
|
|
3942
|
+
if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`)) {
|
|
3487
3943
|
pass("MCP server registered in Codex");
|
|
3488
3944
|
} else {
|
|
3489
3945
|
warn("MCP server not registered in Codex");
|
|
@@ -3494,10 +3950,10 @@ async function handleDoctor() {
|
|
|
3494
3950
|
} catch {
|
|
3495
3951
|
warn("Could not check Codex MCP registration");
|
|
3496
3952
|
}
|
|
3497
|
-
const codexHooks =
|
|
3953
|
+
const codexHooks = join7(homedir4(), ".codex", "hooks.json");
|
|
3498
3954
|
try {
|
|
3499
|
-
if (
|
|
3500
|
-
const content =
|
|
3955
|
+
if (existsSync7(codexHooks)) {
|
|
3956
|
+
const content = readFileSync7(codexHooks, "utf-8");
|
|
3501
3957
|
if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
|
|
3502
3958
|
pass("Hooks registered in Codex");
|
|
3503
3959
|
} else {
|
|
@@ -3511,14 +3967,23 @@ async function handleDoctor() {
|
|
|
3511
3967
|
}
|
|
3512
3968
|
if (config.candengo_url) {
|
|
3513
3969
|
try {
|
|
3970
|
+
const baseUrl = normalizeBaseUrl(config.candengo_url);
|
|
3514
3971
|
const controller = new AbortController;
|
|
3515
3972
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3516
3973
|
const start = Date.now();
|
|
3517
|
-
|
|
3974
|
+
let res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
|
|
3975
|
+
if (res.status === 404) {
|
|
3976
|
+
res = await fetch(`${baseUrl}/v1/mem/provision`, {
|
|
3977
|
+
method: "POST",
|
|
3978
|
+
headers: { "Content-Type": "application/json" },
|
|
3979
|
+
body: "{}",
|
|
3980
|
+
signal: controller.signal
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3518
3983
|
clearTimeout(timeout);
|
|
3519
3984
|
const elapsed = Date.now() - start;
|
|
3520
|
-
if (res.ok) {
|
|
3521
|
-
const host = new URL(
|
|
3985
|
+
if (res.ok || res.status === 400) {
|
|
3986
|
+
const host = new URL(baseUrl).hostname;
|
|
3522
3987
|
pass(`Server connectivity (${host}, ${elapsed}ms)`);
|
|
3523
3988
|
} else {
|
|
3524
3989
|
fail(`Server returned HTTP ${res.status}`);
|
|
@@ -3532,16 +3997,16 @@ async function handleDoctor() {
|
|
|
3532
3997
|
}
|
|
3533
3998
|
if (config.candengo_url && config.candengo_api_key) {
|
|
3534
3999
|
try {
|
|
4000
|
+
const baseUrl = normalizeBaseUrl(config.candengo_url);
|
|
3535
4001
|
const controller = new AbortController;
|
|
3536
4002
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3537
|
-
const res = await fetch(`${
|
|
4003
|
+
const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
|
|
3538
4004
|
headers: { Authorization: `Bearer ${config.candengo_api_key}` },
|
|
3539
4005
|
signal: controller.signal
|
|
3540
4006
|
});
|
|
3541
4007
|
clearTimeout(timeout);
|
|
3542
4008
|
if (res.ok) {
|
|
3543
|
-
const
|
|
3544
|
-
const email = data.email ?? config.user_email ?? "unknown";
|
|
4009
|
+
const email = config.user_email ?? "configured";
|
|
3545
4010
|
pass(`Authentication valid (${email})`);
|
|
3546
4011
|
} else if (res.status === 401 || res.status === 403) {
|
|
3547
4012
|
fail("Authentication failed \u2014 API key may be expired");
|
|
@@ -3590,9 +4055,21 @@ async function handleDoctor() {
|
|
|
3590
4055
|
} catch {
|
|
3591
4056
|
warn("Could not count observations");
|
|
3592
4057
|
}
|
|
4058
|
+
try {
|
|
4059
|
+
const capture = getCaptureStatus(db, { user_id: config.user_id });
|
|
4060
|
+
if (capture.raw_capture_active) {
|
|
4061
|
+
pass(`Raw chronology active (${capture.recent_user_prompts} prompts, ${capture.recent_tool_events} tools in last 24h)`);
|
|
4062
|
+
} else if (capture.claude_hooks_registered || capture.codex_hooks_registered) {
|
|
4063
|
+
warn("Hooks are registered, but no raw prompt/tool chronology has been captured in the last 24h");
|
|
4064
|
+
} else {
|
|
4065
|
+
warn("Raw chronology inactive \u2014 hook registration is incomplete");
|
|
4066
|
+
}
|
|
4067
|
+
} catch {
|
|
4068
|
+
warn("Could not check raw chronology capture");
|
|
4069
|
+
}
|
|
3593
4070
|
try {
|
|
3594
4071
|
const dbPath = getDbPath();
|
|
3595
|
-
if (
|
|
4072
|
+
if (existsSync7(dbPath)) {
|
|
3596
4073
|
const stats = statSync(dbPath);
|
|
3597
4074
|
const sizeMB = stats.size / (1024 * 1024);
|
|
3598
4075
|
const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
|
|
@@ -3660,11 +4137,11 @@ Registering with Claude Code and Codex...`);
|
|
|
3660
4137
|
console.log(`
|
|
3661
4138
|
Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
|
|
3662
4139
|
} catch (error) {
|
|
3663
|
-
const packageRoot =
|
|
4140
|
+
const packageRoot = join7(THIS_DIR, "..");
|
|
3664
4141
|
const runtime = IS_BUILT_DIST ? process.execPath : "bun";
|
|
3665
|
-
const serverArgs = IS_BUILT_DIST ? [
|
|
3666
|
-
const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${
|
|
3667
|
-
const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${
|
|
4142
|
+
const serverArgs = IS_BUILT_DIST ? [join7(packageRoot, "dist", "server.js")] : ["run", join7(packageRoot, "src", "server.ts")];
|
|
4143
|
+
const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join7(packageRoot, "hooks", "session-start.ts")}`;
|
|
4144
|
+
const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join7(packageRoot, "hooks", "codex-stop.ts")}`;
|
|
3668
4145
|
console.log(`
|
|
3669
4146
|
Could not auto-register with Claude Code and Codex.`);
|
|
3670
4147
|
console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|