engrm 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +684 -12
- package/dist/hooks/elicitation-result.js +85 -5
- package/dist/hooks/post-tool-use.js +340 -5
- package/dist/hooks/pre-compact.js +106 -10
- package/dist/hooks/sentinel.js +90 -4
- package/dist/hooks/session-start.js +342 -11
- package/dist/hooks/stop.js +903 -4
- package/dist/server.js +155 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18,16 +18,16 @@ var __export = (target, all) => {
|
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
22
|
-
import { hostname as hostname2, homedir as homedir3 } from "os";
|
|
21
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, statSync } from "fs";
|
|
22
|
+
import { hostname as hostname2, homedir as homedir3, networkInterfaces as networkInterfaces2 } from "os";
|
|
23
23
|
import { join as join6 } from "path";
|
|
24
|
-
import {
|
|
24
|
+
import { createHash as createHash2 } from "crypto";
|
|
25
25
|
|
|
26
26
|
// src/config.ts
|
|
27
27
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
-
import { homedir, hostname } from "node:os";
|
|
28
|
+
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import {
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
31
|
var CONFIG_DIR = join(homedir(), ".engrm");
|
|
32
32
|
var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
|
|
33
33
|
var DB_PATH = join(CONFIG_DIR, "engrm.db");
|
|
@@ -42,7 +42,22 @@ function getDbPath() {
|
|
|
42
42
|
}
|
|
43
43
|
function generateDeviceId() {
|
|
44
44
|
const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
45
|
-
|
|
45
|
+
let mac = "";
|
|
46
|
+
const ifaces = networkInterfaces();
|
|
47
|
+
for (const entries of Object.values(ifaces)) {
|
|
48
|
+
if (!entries)
|
|
49
|
+
continue;
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
52
|
+
mac = entry.mac;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (mac)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
60
|
+
const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
|
|
46
61
|
return `${host}-${suffix}`;
|
|
47
62
|
}
|
|
48
63
|
function createDefaultConfig() {
|
|
@@ -84,7 +99,10 @@ function createDefaultConfig() {
|
|
|
84
99
|
observer: {
|
|
85
100
|
enabled: true,
|
|
86
101
|
mode: "per_event",
|
|
87
|
-
model: "
|
|
102
|
+
model: "sonnet"
|
|
103
|
+
},
|
|
104
|
+
transcript_analysis: {
|
|
105
|
+
enabled: false
|
|
88
106
|
}
|
|
89
107
|
};
|
|
90
108
|
}
|
|
@@ -143,6 +161,9 @@ function loadConfig() {
|
|
|
143
161
|
enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
|
|
144
162
|
mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
|
|
145
163
|
model: asString(config["observer"]?.["model"], defaults.observer.model)
|
|
164
|
+
},
|
|
165
|
+
transcript_analysis: {
|
|
166
|
+
enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
146
167
|
}
|
|
147
168
|
};
|
|
148
169
|
}
|
|
@@ -548,6 +569,56 @@ function runMigrations(db) {
|
|
|
548
569
|
}
|
|
549
570
|
}
|
|
550
571
|
}
|
|
572
|
+
function ensureObservationTypes(db) {
|
|
573
|
+
try {
|
|
574
|
+
db.exec("INSERT INTO observations (session_id, project_id, type, title, user_id, device_id, agent, created_at, created_at_epoch) " + "VALUES ('_typecheck', 1, 'message', '_test', '_test', '_test', '_test', '2000-01-01', 0)");
|
|
575
|
+
db.exec("DELETE FROM observations WHERE session_id = '_typecheck'");
|
|
576
|
+
} catch {
|
|
577
|
+
db.exec("BEGIN TRANSACTION");
|
|
578
|
+
try {
|
|
579
|
+
db.exec(`
|
|
580
|
+
CREATE TABLE observations_repair (
|
|
581
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT,
|
|
582
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
583
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
584
|
+
'bugfix','discovery','decision','pattern','change','feature',
|
|
585
|
+
'refactor','digest','standard','message')),
|
|
586
|
+
title TEXT NOT NULL, narrative TEXT, facts TEXT, concepts TEXT,
|
|
587
|
+
files_read TEXT, files_modified TEXT,
|
|
588
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
589
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN ('active','aging','archived','purged','pinned')),
|
|
590
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN ('shared','personal','secret')),
|
|
591
|
+
user_id TEXT NOT NULL, device_id TEXT NOT NULL, agent TEXT DEFAULT 'claude-code',
|
|
592
|
+
created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
|
|
593
|
+
archived_at_epoch INTEGER,
|
|
594
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
595
|
+
superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
596
|
+
remote_source_id TEXT
|
|
597
|
+
);
|
|
598
|
+
INSERT INTO observations_repair SELECT * FROM observations;
|
|
599
|
+
DROP TABLE observations;
|
|
600
|
+
ALTER TABLE observations_repair RENAME TO observations;
|
|
601
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
602
|
+
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
603
|
+
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
604
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
605
|
+
CREATE INDEX IF NOT EXISTS idx_observations_lifecycle ON observations(lifecycle);
|
|
606
|
+
CREATE INDEX IF NOT EXISTS idx_observations_quality ON observations(quality);
|
|
607
|
+
CREATE INDEX IF NOT EXISTS idx_observations_user ON observations(user_id);
|
|
608
|
+
CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by);
|
|
609
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
610
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
611
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
612
|
+
title, narrative, facts, concepts, content=observations, content_rowid=id
|
|
613
|
+
);
|
|
614
|
+
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
615
|
+
`);
|
|
616
|
+
db.exec("COMMIT");
|
|
617
|
+
} catch (err) {
|
|
618
|
+
db.exec("ROLLBACK");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
551
622
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
552
623
|
|
|
553
624
|
// src/storage/sqlite.ts
|
|
@@ -614,6 +685,7 @@ class MemDatabase {
|
|
|
614
685
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
615
686
|
this.vecAvailable = this.loadVecExtension();
|
|
616
687
|
runMigrations(this.db);
|
|
688
|
+
ensureObservationTypes(this.db);
|
|
617
689
|
}
|
|
618
690
|
loadVecExtension() {
|
|
619
691
|
try {
|
|
@@ -988,6 +1060,335 @@ function getOutboxStats(db) {
|
|
|
988
1060
|
return stats;
|
|
989
1061
|
}
|
|
990
1062
|
|
|
1063
|
+
// src/storage/migrations.ts
|
|
1064
|
+
var MIGRATIONS2 = [
|
|
1065
|
+
{
|
|
1066
|
+
version: 1,
|
|
1067
|
+
description: "Initial schema: projects, observations, sessions, sync, FTS5",
|
|
1068
|
+
sql: `
|
|
1069
|
+
-- Projects (canonical identity across machines)
|
|
1070
|
+
CREATE TABLE projects (
|
|
1071
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1072
|
+
canonical_id TEXT UNIQUE NOT NULL,
|
|
1073
|
+
name TEXT NOT NULL,
|
|
1074
|
+
local_path TEXT,
|
|
1075
|
+
remote_url TEXT,
|
|
1076
|
+
first_seen_epoch INTEGER NOT NULL,
|
|
1077
|
+
last_active_epoch INTEGER NOT NULL
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
-- Core observations table
|
|
1081
|
+
CREATE TABLE observations (
|
|
1082
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1083
|
+
session_id TEXT,
|
|
1084
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1085
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
1086
|
+
'bugfix', 'discovery', 'decision', 'pattern',
|
|
1087
|
+
'change', 'feature', 'refactor', 'digest'
|
|
1088
|
+
)),
|
|
1089
|
+
title TEXT NOT NULL,
|
|
1090
|
+
narrative TEXT,
|
|
1091
|
+
facts TEXT,
|
|
1092
|
+
concepts TEXT,
|
|
1093
|
+
files_read TEXT,
|
|
1094
|
+
files_modified TEXT,
|
|
1095
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
1096
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
|
|
1097
|
+
'active', 'aging', 'archived', 'purged', 'pinned'
|
|
1098
|
+
)),
|
|
1099
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
|
|
1100
|
+
'shared', 'personal', 'secret'
|
|
1101
|
+
)),
|
|
1102
|
+
user_id TEXT NOT NULL,
|
|
1103
|
+
device_id TEXT NOT NULL,
|
|
1104
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1105
|
+
created_at TEXT NOT NULL,
|
|
1106
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1107
|
+
archived_at_epoch INTEGER,
|
|
1108
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
-- Session tracking
|
|
1112
|
+
CREATE TABLE sessions (
|
|
1113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1114
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
1115
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1116
|
+
user_id TEXT NOT NULL,
|
|
1117
|
+
device_id TEXT NOT NULL,
|
|
1118
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1119
|
+
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed')),
|
|
1120
|
+
observation_count INTEGER DEFAULT 0,
|
|
1121
|
+
started_at_epoch INTEGER,
|
|
1122
|
+
completed_at_epoch INTEGER
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
-- Session summaries (generated on Stop hook)
|
|
1126
|
+
CREATE TABLE session_summaries (
|
|
1127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1128
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
1129
|
+
project_id INTEGER REFERENCES projects(id),
|
|
1130
|
+
user_id TEXT NOT NULL,
|
|
1131
|
+
request TEXT,
|
|
1132
|
+
investigated TEXT,
|
|
1133
|
+
learned TEXT,
|
|
1134
|
+
completed TEXT,
|
|
1135
|
+
next_steps TEXT,
|
|
1136
|
+
created_at_epoch INTEGER
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
-- Sync outbox (offline-first queue)
|
|
1140
|
+
CREATE TABLE sync_outbox (
|
|
1141
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1142
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
1143
|
+
record_id INTEGER NOT NULL,
|
|
1144
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1145
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
1146
|
+
)),
|
|
1147
|
+
retry_count INTEGER DEFAULT 0,
|
|
1148
|
+
max_retries INTEGER DEFAULT 10,
|
|
1149
|
+
last_error TEXT,
|
|
1150
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1151
|
+
synced_at_epoch INTEGER,
|
|
1152
|
+
next_retry_epoch INTEGER
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
-- Sync high-water mark and lifecycle job tracking
|
|
1156
|
+
CREATE TABLE sync_state (
|
|
1157
|
+
key TEXT PRIMARY KEY,
|
|
1158
|
+
value TEXT NOT NULL
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
-- FTS5 for local offline search (external content mode)
|
|
1162
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
1163
|
+
title, narrative, facts, concepts,
|
|
1164
|
+
content=observations,
|
|
1165
|
+
content_rowid=id
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
-- Indexes: observations
|
|
1169
|
+
CREATE INDEX idx_observations_project ON observations(project_id);
|
|
1170
|
+
CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1171
|
+
CREATE INDEX idx_observations_type ON observations(type);
|
|
1172
|
+
CREATE INDEX idx_observations_created ON observations(created_at_epoch);
|
|
1173
|
+
CREATE INDEX idx_observations_session ON observations(session_id);
|
|
1174
|
+
CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
|
|
1175
|
+
CREATE INDEX idx_observations_quality ON observations(quality);
|
|
1176
|
+
CREATE INDEX idx_observations_user ON observations(user_id);
|
|
1177
|
+
|
|
1178
|
+
-- Indexes: sessions
|
|
1179
|
+
CREATE INDEX idx_sessions_project ON sessions(project_id);
|
|
1180
|
+
|
|
1181
|
+
-- Indexes: sync outbox
|
|
1182
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1183
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1184
|
+
`
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
version: 2,
|
|
1188
|
+
description: "Add superseded_by for knowledge supersession",
|
|
1189
|
+
sql: `
|
|
1190
|
+
ALTER TABLE observations ADD COLUMN superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL;
|
|
1191
|
+
CREATE INDEX idx_observations_superseded ON observations(superseded_by);
|
|
1192
|
+
`
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
version: 3,
|
|
1196
|
+
description: "Add remote_source_id for pull deduplication",
|
|
1197
|
+
sql: `
|
|
1198
|
+
ALTER TABLE observations ADD COLUMN remote_source_id TEXT;
|
|
1199
|
+
CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
1200
|
+
`
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
version: 4,
|
|
1204
|
+
description: "Add sqlite-vec for local semantic search",
|
|
1205
|
+
sql: `
|
|
1206
|
+
CREATE VIRTUAL TABLE vec_observations USING vec0(
|
|
1207
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1208
|
+
embedding float[384]
|
|
1209
|
+
);
|
|
1210
|
+
`,
|
|
1211
|
+
condition: (db) => isVecExtensionLoaded2(db)
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
version: 5,
|
|
1215
|
+
description: "Session metrics and security findings",
|
|
1216
|
+
sql: `
|
|
1217
|
+
ALTER TABLE sessions ADD COLUMN files_touched_count INTEGER DEFAULT 0;
|
|
1218
|
+
ALTER TABLE sessions ADD COLUMN searches_performed INTEGER DEFAULT 0;
|
|
1219
|
+
ALTER TABLE sessions ADD COLUMN tool_calls_count INTEGER DEFAULT 0;
|
|
1220
|
+
|
|
1221
|
+
CREATE TABLE security_findings (
|
|
1222
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1223
|
+
session_id TEXT,
|
|
1224
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1225
|
+
finding_type TEXT NOT NULL,
|
|
1226
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK (severity IN ('critical', 'high', 'medium', 'low')),
|
|
1227
|
+
pattern_name TEXT NOT NULL,
|
|
1228
|
+
file_path TEXT,
|
|
1229
|
+
snippet TEXT,
|
|
1230
|
+
tool_name TEXT,
|
|
1231
|
+
user_id TEXT NOT NULL,
|
|
1232
|
+
device_id TEXT NOT NULL,
|
|
1233
|
+
created_at_epoch INTEGER NOT NULL
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
CREATE INDEX idx_security_findings_session ON security_findings(session_id);
|
|
1237
|
+
CREATE INDEX idx_security_findings_project ON security_findings(project_id, created_at_epoch);
|
|
1238
|
+
CREATE INDEX idx_security_findings_severity ON security_findings(severity);
|
|
1239
|
+
`
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
version: 6,
|
|
1243
|
+
description: "Add risk_score, expand observation types to include standard",
|
|
1244
|
+
sql: `
|
|
1245
|
+
ALTER TABLE sessions ADD COLUMN risk_score INTEGER;
|
|
1246
|
+
|
|
1247
|
+
-- Recreate observations table with expanded type CHECK to include 'standard'
|
|
1248
|
+
-- SQLite doesn't support ALTER CHECK, so we recreate the table
|
|
1249
|
+
CREATE TABLE observations_new (
|
|
1250
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1251
|
+
session_id TEXT,
|
|
1252
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1253
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
1254
|
+
'bugfix', 'discovery', 'decision', 'pattern',
|
|
1255
|
+
'change', 'feature', 'refactor', 'digest', 'standard'
|
|
1256
|
+
)),
|
|
1257
|
+
title TEXT NOT NULL,
|
|
1258
|
+
narrative TEXT,
|
|
1259
|
+
facts TEXT,
|
|
1260
|
+
concepts TEXT,
|
|
1261
|
+
files_read TEXT,
|
|
1262
|
+
files_modified TEXT,
|
|
1263
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
1264
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
|
|
1265
|
+
'active', 'aging', 'archived', 'purged', 'pinned'
|
|
1266
|
+
)),
|
|
1267
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
|
|
1268
|
+
'shared', 'personal', 'secret'
|
|
1269
|
+
)),
|
|
1270
|
+
user_id TEXT NOT NULL,
|
|
1271
|
+
device_id TEXT NOT NULL,
|
|
1272
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1273
|
+
created_at TEXT NOT NULL,
|
|
1274
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1275
|
+
archived_at_epoch INTEGER,
|
|
1276
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
1277
|
+
superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
1278
|
+
remote_source_id TEXT
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
INSERT INTO observations_new SELECT * FROM observations;
|
|
1282
|
+
|
|
1283
|
+
DROP TABLE observations;
|
|
1284
|
+
ALTER TABLE observations_new RENAME TO observations;
|
|
1285
|
+
|
|
1286
|
+
-- Recreate indexes
|
|
1287
|
+
CREATE INDEX idx_observations_project ON observations(project_id);
|
|
1288
|
+
CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1289
|
+
CREATE INDEX idx_observations_type ON observations(type);
|
|
1290
|
+
CREATE INDEX idx_observations_created ON observations(created_at_epoch);
|
|
1291
|
+
CREATE INDEX idx_observations_session ON observations(session_id);
|
|
1292
|
+
CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
|
|
1293
|
+
CREATE INDEX idx_observations_quality ON observations(quality);
|
|
1294
|
+
CREATE INDEX idx_observations_user ON observations(user_id);
|
|
1295
|
+
CREATE INDEX idx_observations_superseded ON observations(superseded_by);
|
|
1296
|
+
CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
1297
|
+
|
|
1298
|
+
-- Recreate FTS5 (external content mode — must rebuild after table recreation)
|
|
1299
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
1300
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
1301
|
+
title, narrative, facts, concepts,
|
|
1302
|
+
content=observations,
|
|
1303
|
+
content_rowid=id
|
|
1304
|
+
);
|
|
1305
|
+
-- Rebuild FTS index
|
|
1306
|
+
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
1307
|
+
`
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
version: 7,
|
|
1311
|
+
description: "Add packs_installed table for help pack tracking",
|
|
1312
|
+
sql: `
|
|
1313
|
+
CREATE TABLE IF NOT EXISTS packs_installed (
|
|
1314
|
+
name TEXT PRIMARY KEY,
|
|
1315
|
+
installed_at INTEGER NOT NULL,
|
|
1316
|
+
observation_count INTEGER DEFAULT 0
|
|
1317
|
+
);
|
|
1318
|
+
`
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
version: 8,
|
|
1322
|
+
description: "Add message type to observations CHECK constraint",
|
|
1323
|
+
sql: `
|
|
1324
|
+
CREATE TABLE IF NOT EXISTS observations_v8 (
|
|
1325
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1326
|
+
session_id TEXT,
|
|
1327
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1328
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
1329
|
+
'bugfix', 'discovery', 'decision', 'pattern',
|
|
1330
|
+
'change', 'feature', 'refactor', 'digest', 'standard', 'message'
|
|
1331
|
+
)),
|
|
1332
|
+
title TEXT NOT NULL,
|
|
1333
|
+
narrative TEXT,
|
|
1334
|
+
facts TEXT,
|
|
1335
|
+
concepts TEXT,
|
|
1336
|
+
files_read TEXT,
|
|
1337
|
+
files_modified TEXT,
|
|
1338
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
1339
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
|
|
1340
|
+
'active', 'aging', 'archived', 'purged', 'pinned'
|
|
1341
|
+
)),
|
|
1342
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
|
|
1343
|
+
'shared', 'personal', 'secret'
|
|
1344
|
+
)),
|
|
1345
|
+
user_id TEXT NOT NULL,
|
|
1346
|
+
device_id TEXT NOT NULL,
|
|
1347
|
+
agent TEXT DEFAULT 'claude-code',
|
|
1348
|
+
created_at TEXT NOT NULL,
|
|
1349
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1350
|
+
archived_at_epoch INTEGER,
|
|
1351
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
1352
|
+
superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
1353
|
+
remote_source_id TEXT
|
|
1354
|
+
);
|
|
1355
|
+
INSERT INTO observations_v8 SELECT * FROM observations;
|
|
1356
|
+
DROP TABLE observations;
|
|
1357
|
+
ALTER TABLE observations_v8 RENAME TO observations;
|
|
1358
|
+
CREATE INDEX idx_observations_project ON observations(project_id);
|
|
1359
|
+
CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1360
|
+
CREATE INDEX idx_observations_type ON observations(type);
|
|
1361
|
+
CREATE INDEX idx_observations_created ON observations(created_at_epoch);
|
|
1362
|
+
CREATE INDEX idx_observations_session ON observations(session_id);
|
|
1363
|
+
CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
|
|
1364
|
+
CREATE INDEX idx_observations_quality ON observations(quality);
|
|
1365
|
+
CREATE INDEX idx_observations_user ON observations(user_id);
|
|
1366
|
+
CREATE INDEX idx_observations_superseded ON observations(superseded_by);
|
|
1367
|
+
CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
1368
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
1369
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
1370
|
+
title, narrative, facts, concepts,
|
|
1371
|
+
content=observations,
|
|
1372
|
+
content_rowid=id
|
|
1373
|
+
);
|
|
1374
|
+
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
1375
|
+
`
|
|
1376
|
+
}
|
|
1377
|
+
];
|
|
1378
|
+
function isVecExtensionLoaded2(db) {
|
|
1379
|
+
try {
|
|
1380
|
+
db.exec("SELECT vec_version()");
|
|
1381
|
+
return true;
|
|
1382
|
+
} catch {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function getSchemaVersion(db) {
|
|
1387
|
+
const result = db.query("PRAGMA user_version").get();
|
|
1388
|
+
return result.user_version;
|
|
1389
|
+
}
|
|
1390
|
+
var LATEST_SCHEMA_VERSION2 = MIGRATIONS2.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
1391
|
+
|
|
991
1392
|
// src/provisioning/provision.ts
|
|
992
1393
|
var DEFAULT_CANDENGO_URL = "https://www.candengo.com";
|
|
993
1394
|
|
|
@@ -1035,12 +1436,12 @@ async function provision(baseUrl, request) {
|
|
|
1035
1436
|
}
|
|
1036
1437
|
|
|
1037
1438
|
// src/provisioning/browser-auth.ts
|
|
1038
|
-
import { randomBytes
|
|
1439
|
+
import { randomBytes } from "node:crypto";
|
|
1039
1440
|
import { createServer } from "node:http";
|
|
1040
1441
|
import { execFile } from "node:child_process";
|
|
1041
1442
|
var CALLBACK_TIMEOUT_MS = 120000;
|
|
1042
1443
|
async function runBrowserAuth(candengoUrl) {
|
|
1043
|
-
const state =
|
|
1444
|
+
const state = randomBytes(16).toString("hex");
|
|
1044
1445
|
const { port, waitForCallback, stop } = await startCallbackServer(state);
|
|
1045
1446
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
1046
1447
|
const authUrl = new URL("/connect/mem", candengoUrl);
|
|
@@ -1266,7 +1667,7 @@ function registerHooks() {
|
|
|
1266
1667
|
hooks: [{ type: "command", command: preToolUseCmd }]
|
|
1267
1668
|
}, "sentinel");
|
|
1268
1669
|
hooks["PostToolUse"] = replaceEngrmHook(hooks["PostToolUse"], {
|
|
1269
|
-
matcher: "Edit|Write|Bash|Read|mcp__.*",
|
|
1670
|
+
matcher: "Edit|Write|Bash|Read|Grep|Glob|WebSearch|WebFetch|mcp__.*",
|
|
1270
1671
|
hooks: [{ type: "command", command: postToolUseCmd }]
|
|
1271
1672
|
}, "post-tool-use");
|
|
1272
1673
|
hooks["ElicitationResult"] = replaceEngrmHook(hooks["ElicitationResult"], {
|
|
@@ -1905,7 +2306,8 @@ var VALID_TYPES = [
|
|
|
1905
2306
|
"feature",
|
|
1906
2307
|
"refactor",
|
|
1907
2308
|
"digest",
|
|
1908
|
-
"standard"
|
|
2309
|
+
"standard",
|
|
2310
|
+
"message"
|
|
1909
2311
|
];
|
|
1910
2312
|
async function saveObservation(db, config, input) {
|
|
1911
2313
|
if (!VALID_TYPES.includes(input.type)) {
|
|
@@ -2162,6 +2564,9 @@ switch (command) {
|
|
|
2162
2564
|
case "sentinel":
|
|
2163
2565
|
await handleSentinel(args.slice(1));
|
|
2164
2566
|
break;
|
|
2567
|
+
case "doctor":
|
|
2568
|
+
await handleDoctor();
|
|
2569
|
+
break;
|
|
2165
2570
|
default:
|
|
2166
2571
|
printUsage();
|
|
2167
2572
|
break;
|
|
@@ -2261,6 +2666,7 @@ async function initWithToken(baseUrl, token) {
|
|
|
2261
2666
|
console.log(`
|
|
2262
2667
|
Connected as ${result.user_email}`);
|
|
2263
2668
|
printPostInit();
|
|
2669
|
+
await checkDeviceLimits(baseUrl, result.api_key);
|
|
2264
2670
|
} catch (error) {
|
|
2265
2671
|
if (error instanceof ProvisionError) {
|
|
2266
2672
|
console.error(`
|
|
@@ -2286,6 +2692,7 @@ async function initWithBrowser(baseUrl) {
|
|
|
2286
2692
|
console.log(`
|
|
2287
2693
|
Connected as ${result.user_email}`);
|
|
2288
2694
|
printPostInit();
|
|
2695
|
+
await checkDeviceLimits(baseUrl, result.api_key);
|
|
2289
2696
|
} catch (error) {
|
|
2290
2697
|
if (error instanceof ProvisionError) {
|
|
2291
2698
|
console.error(`
|
|
@@ -2343,6 +2750,14 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
2343
2750
|
skip_patterns: [],
|
|
2344
2751
|
daily_limit: 100,
|
|
2345
2752
|
tier: "free"
|
|
2753
|
+
},
|
|
2754
|
+
observer: {
|
|
2755
|
+
enabled: true,
|
|
2756
|
+
mode: "per_event",
|
|
2757
|
+
model: "haiku"
|
|
2758
|
+
},
|
|
2759
|
+
transcript_analysis: {
|
|
2760
|
+
enabled: false
|
|
2346
2761
|
}
|
|
2347
2762
|
};
|
|
2348
2763
|
saveConfig(config);
|
|
@@ -2417,6 +2832,14 @@ function initFromFile(configPath) {
|
|
|
2417
2832
|
skip_patterns: [],
|
|
2418
2833
|
daily_limit: 100,
|
|
2419
2834
|
tier: "free"
|
|
2835
|
+
},
|
|
2836
|
+
observer: {
|
|
2837
|
+
enabled: true,
|
|
2838
|
+
mode: "per_event",
|
|
2839
|
+
model: "haiku"
|
|
2840
|
+
},
|
|
2841
|
+
transcript_analysis: {
|
|
2842
|
+
enabled: false
|
|
2420
2843
|
}
|
|
2421
2844
|
};
|
|
2422
2845
|
saveConfig(config);
|
|
@@ -2482,6 +2905,14 @@ async function initManual() {
|
|
|
2482
2905
|
skip_patterns: [],
|
|
2483
2906
|
daily_limit: 100,
|
|
2484
2907
|
tier: "free"
|
|
2908
|
+
},
|
|
2909
|
+
observer: {
|
|
2910
|
+
enabled: true,
|
|
2911
|
+
mode: "per_event",
|
|
2912
|
+
model: "haiku"
|
|
2913
|
+
},
|
|
2914
|
+
transcript_analysis: {
|
|
2915
|
+
enabled: false
|
|
2485
2916
|
}
|
|
2486
2917
|
};
|
|
2487
2918
|
saveConfig(config);
|
|
@@ -2667,7 +3098,22 @@ function ensureConfigDir() {
|
|
|
2667
3098
|
}
|
|
2668
3099
|
function generateDeviceId2() {
|
|
2669
3100
|
const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
2670
|
-
|
|
3101
|
+
let mac = "";
|
|
3102
|
+
const ifaces = networkInterfaces2();
|
|
3103
|
+
for (const entries of Object.values(ifaces)) {
|
|
3104
|
+
if (!entries)
|
|
3105
|
+
continue;
|
|
3106
|
+
for (const entry of entries) {
|
|
3107
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
3108
|
+
mac = entry.mac;
|
|
3109
|
+
break;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
if (mac)
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3115
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
3116
|
+
const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
|
|
2671
3117
|
return `${host}-${suffix}`;
|
|
2672
3118
|
}
|
|
2673
3119
|
async function handleInstallPack(flags) {
|
|
@@ -2767,6 +3213,231 @@ Restart Claude Code to use the new version.`);
|
|
|
2767
3213
|
console.error("Update failed. Try manually: npm install -g engrm@latest");
|
|
2768
3214
|
}
|
|
2769
3215
|
}
|
|
3216
|
+
async function handleDoctor() {
|
|
3217
|
+
const results = [];
|
|
3218
|
+
const pass = (msg) => results.push({ symbol: "\u2713", message: msg, kind: "pass" });
|
|
3219
|
+
const fail = (msg) => results.push({ symbol: "\u2717", message: msg, kind: "fail" });
|
|
3220
|
+
const warn = (msg) => results.push({ symbol: "\u26A0", message: msg, kind: "warn" });
|
|
3221
|
+
const info = (msg) => results.push({ symbol: "\u2139", message: msg, kind: "info" });
|
|
3222
|
+
if (configExists()) {
|
|
3223
|
+
pass("Configuration file exists");
|
|
3224
|
+
} else {
|
|
3225
|
+
fail("Configuration file not found \u2014 run: engrm init");
|
|
3226
|
+
printDoctorReport(results);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
let config = null;
|
|
3230
|
+
try {
|
|
3231
|
+
config = loadConfig();
|
|
3232
|
+
pass("Configuration is valid");
|
|
3233
|
+
} catch (err) {
|
|
3234
|
+
fail(`Configuration is invalid: ${err instanceof Error ? err.message : String(err)}`);
|
|
3235
|
+
printDoctorReport(results);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
let db = null;
|
|
3239
|
+
try {
|
|
3240
|
+
db = new MemDatabase(getDbPath());
|
|
3241
|
+
pass("Database opens successfully");
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
fail(`Database failed to open: ${err instanceof Error ? err.message : String(err)}`);
|
|
3244
|
+
printDoctorReport(results);
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
try {
|
|
3248
|
+
const currentVersion = getSchemaVersion(db.db);
|
|
3249
|
+
if (currentVersion >= LATEST_SCHEMA_VERSION2) {
|
|
3250
|
+
pass(`Database schema is current (v${currentVersion})`);
|
|
3251
|
+
} else {
|
|
3252
|
+
warn(`Database schema is outdated (v${currentVersion}, latest is v${LATEST_SCHEMA_VERSION2})`);
|
|
3253
|
+
}
|
|
3254
|
+
} catch {
|
|
3255
|
+
warn("Could not check database schema version");
|
|
3256
|
+
}
|
|
3257
|
+
const claudeJson = join6(homedir3(), ".claude.json");
|
|
3258
|
+
try {
|
|
3259
|
+
if (existsSync6(claudeJson)) {
|
|
3260
|
+
const content = readFileSync6(claudeJson, "utf-8");
|
|
3261
|
+
if (content.includes('"engrm"')) {
|
|
3262
|
+
pass("MCP server registered in Claude Code");
|
|
3263
|
+
} else {
|
|
3264
|
+
warn("MCP server not registered in Claude Code \u2014 run: engrm init");
|
|
3265
|
+
}
|
|
3266
|
+
} else {
|
|
3267
|
+
warn("Claude Code config not found (~/.claude.json)");
|
|
3268
|
+
}
|
|
3269
|
+
} catch {
|
|
3270
|
+
warn("Could not check MCP server registration");
|
|
3271
|
+
}
|
|
3272
|
+
const claudeSettings = join6(homedir3(), ".claude", "settings.json");
|
|
3273
|
+
try {
|
|
3274
|
+
if (existsSync6(claudeSettings)) {
|
|
3275
|
+
const content = readFileSync6(claudeSettings, "utf-8");
|
|
3276
|
+
let hookCount = 0;
|
|
3277
|
+
try {
|
|
3278
|
+
const settings = JSON.parse(content);
|
|
3279
|
+
const hooks = settings?.hooks ?? {};
|
|
3280
|
+
for (const entries of Object.values(hooks)) {
|
|
3281
|
+
if (Array.isArray(entries)) {
|
|
3282
|
+
for (const entry of entries) {
|
|
3283
|
+
const e = entry;
|
|
3284
|
+
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"))) {
|
|
3285
|
+
hookCount++;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
} catch {}
|
|
3291
|
+
if (hookCount > 0) {
|
|
3292
|
+
pass(`Hooks registered (${hookCount} hook${hookCount === 1 ? "" : "s"})`);
|
|
3293
|
+
} else {
|
|
3294
|
+
warn("No Engrm hooks found in Claude Code settings");
|
|
3295
|
+
}
|
|
3296
|
+
} else {
|
|
3297
|
+
warn("Claude Code settings not found (~/.claude/settings.json)");
|
|
3298
|
+
}
|
|
3299
|
+
} catch {
|
|
3300
|
+
warn("Could not check hooks registration");
|
|
3301
|
+
}
|
|
3302
|
+
if (config.candengo_url) {
|
|
3303
|
+
try {
|
|
3304
|
+
const controller = new AbortController;
|
|
3305
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3306
|
+
const start = Date.now();
|
|
3307
|
+
const res = await fetch(`${config.candengo_url}/health`, { signal: controller.signal });
|
|
3308
|
+
clearTimeout(timeout);
|
|
3309
|
+
const elapsed = Date.now() - start;
|
|
3310
|
+
if (res.ok) {
|
|
3311
|
+
const host = new URL(config.candengo_url).hostname;
|
|
3312
|
+
pass(`Server connectivity (${host}, ${elapsed}ms)`);
|
|
3313
|
+
} else {
|
|
3314
|
+
fail(`Server returned HTTP ${res.status}`);
|
|
3315
|
+
}
|
|
3316
|
+
} catch (err) {
|
|
3317
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3318
|
+
fail(`Server unreachable: ${msg.includes("abort") ? "timeout (5s)" : msg}`);
|
|
3319
|
+
}
|
|
3320
|
+
} else {
|
|
3321
|
+
fail("Server URL not configured");
|
|
3322
|
+
}
|
|
3323
|
+
if (config.candengo_url && config.candengo_api_key) {
|
|
3324
|
+
try {
|
|
3325
|
+
const controller = new AbortController;
|
|
3326
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3327
|
+
const res = await fetch(`${config.candengo_url}/v1/account/me`, {
|
|
3328
|
+
headers: { Authorization: `Bearer ${config.candengo_api_key}` },
|
|
3329
|
+
signal: controller.signal
|
|
3330
|
+
});
|
|
3331
|
+
clearTimeout(timeout);
|
|
3332
|
+
if (res.ok) {
|
|
3333
|
+
const data = await res.json();
|
|
3334
|
+
const email = data.email ?? config.user_email ?? "unknown";
|
|
3335
|
+
pass(`Authentication valid (${email})`);
|
|
3336
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
3337
|
+
fail("Authentication failed \u2014 API key may be expired");
|
|
3338
|
+
} else {
|
|
3339
|
+
fail(`Authentication check returned HTTP ${res.status}`);
|
|
3340
|
+
}
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3343
|
+
fail(`Authentication check failed: ${msg.includes("abort") ? "timeout (5s)" : msg}`);
|
|
3344
|
+
}
|
|
3345
|
+
} else {
|
|
3346
|
+
fail("Authentication not configured (missing URL or API key)");
|
|
3347
|
+
}
|
|
3348
|
+
try {
|
|
3349
|
+
const outbox = getOutboxStats(db);
|
|
3350
|
+
const failedCount = outbox["failed"] ?? 0;
|
|
3351
|
+
if (failedCount > 10) {
|
|
3352
|
+
warn(`Sync has stuck items (${failedCount} failed in outbox)`);
|
|
3353
|
+
} else {
|
|
3354
|
+
const pending = outbox["pending"] ?? 0;
|
|
3355
|
+
pass(`Sync outbox healthy (${pending} pending, ${failedCount} failed)`);
|
|
3356
|
+
}
|
|
3357
|
+
} catch {
|
|
3358
|
+
warn("Could not check sync outbox");
|
|
3359
|
+
}
|
|
3360
|
+
if (db.vecAvailable) {
|
|
3361
|
+
pass("Embedding model available (sqlite-vec loaded)");
|
|
3362
|
+
} else {
|
|
3363
|
+
warn("Embedding model not available (FTS5 fallback active)");
|
|
3364
|
+
}
|
|
3365
|
+
try {
|
|
3366
|
+
const totalActive = db.getActiveObservationCount();
|
|
3367
|
+
if (totalActive > 0) {
|
|
3368
|
+
let breakdownParts = [];
|
|
3369
|
+
try {
|
|
3370
|
+
const byLifecycle = db.db.query(`SELECT lifecycle, COUNT(*) as count FROM observations
|
|
3371
|
+
WHERE superseded_by IS NULL AND lifecycle IN ('active', 'aging', 'pinned')
|
|
3372
|
+
GROUP BY lifecycle`).all();
|
|
3373
|
+
breakdownParts = byLifecycle.map((r) => `${r.lifecycle}: ${r.count.toLocaleString()}`);
|
|
3374
|
+
} catch {}
|
|
3375
|
+
const detail = breakdownParts.length > 0 ? ` (${breakdownParts.join(", ")})` : "";
|
|
3376
|
+
pass(`${totalActive.toLocaleString()} observations${detail}`);
|
|
3377
|
+
} else {
|
|
3378
|
+
warn("No observations yet \u2014 start a Claude Code session to capture context");
|
|
3379
|
+
}
|
|
3380
|
+
} catch {
|
|
3381
|
+
warn("Could not count observations");
|
|
3382
|
+
}
|
|
3383
|
+
try {
|
|
3384
|
+
const dbPath = getDbPath();
|
|
3385
|
+
if (existsSync6(dbPath)) {
|
|
3386
|
+
const stats = statSync(dbPath);
|
|
3387
|
+
const sizeMB = stats.size / (1024 * 1024);
|
|
3388
|
+
const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
|
|
3389
|
+
info(`Database size: ${sizeStr}`);
|
|
3390
|
+
}
|
|
3391
|
+
} catch {}
|
|
3392
|
+
db.close();
|
|
3393
|
+
printDoctorReport(results);
|
|
3394
|
+
}
|
|
3395
|
+
function printDoctorReport(results) {
|
|
3396
|
+
console.log(`
|
|
3397
|
+
Engrm Doctor \u2014 Diagnostic Report
|
|
3398
|
+
`);
|
|
3399
|
+
for (const r of results) {
|
|
3400
|
+
console.log(` ${r.symbol} ${r.message}`);
|
|
3401
|
+
}
|
|
3402
|
+
const passes = results.filter((r) => r.kind === "pass").length;
|
|
3403
|
+
const fails = results.filter((r) => r.kind === "fail").length;
|
|
3404
|
+
const warns = results.filter((r) => r.kind === "warn").length;
|
|
3405
|
+
const checks = results.filter((r) => r.kind !== "info").length;
|
|
3406
|
+
const parts = [];
|
|
3407
|
+
if (warns > 0)
|
|
3408
|
+
parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
|
|
3409
|
+
if (fails > 0)
|
|
3410
|
+
parts.push(`${fails} failure${fails === 1 ? "" : "s"}`);
|
|
3411
|
+
const summary = `${passes}/${checks} checks passed` + (parts.length > 0 ? `, ${parts.join(", ")}` : "");
|
|
3412
|
+
console.log(`
|
|
3413
|
+
${summary}`);
|
|
3414
|
+
}
|
|
3415
|
+
async function checkDeviceLimits(baseUrl, apiKey) {
|
|
3416
|
+
try {
|
|
3417
|
+
const controller = new AbortController;
|
|
3418
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
3419
|
+
const resp = await fetch(`${baseUrl}/v1/mem/billing`, {
|
|
3420
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
3421
|
+
signal: controller.signal
|
|
3422
|
+
});
|
|
3423
|
+
clearTimeout(timeout);
|
|
3424
|
+
if (!resp.ok)
|
|
3425
|
+
return;
|
|
3426
|
+
const billing = await resp.json();
|
|
3427
|
+
const limits = billing.limits || {};
|
|
3428
|
+
const usage = billing.usage || {};
|
|
3429
|
+
if (limits.max_devices && usage.devices && usage.devices >= limits.max_devices) {
|
|
3430
|
+
const upgradeUrl = billing.upgrade_url || "https://engrm.dev/billing";
|
|
3431
|
+
console.warn(`
|
|
3432
|
+
\u26A0\uFE0F Device limit reached (${usage.devices}/${limits.max_devices}).`);
|
|
3433
|
+
console.warn(` This device may not sync. Upgrade at: ${upgradeUrl}`);
|
|
3434
|
+
}
|
|
3435
|
+
if (limits.max_observations && usage.observations && usage.observations >= limits.max_observations) {
|
|
3436
|
+
console.warn(`\u26A0\uFE0F Observation limit reached (${usage.observations.toLocaleString()}/${limits.max_observations.toLocaleString()}).`);
|
|
3437
|
+
console.warn(` New observations won't sync until you upgrade or delete old data.`);
|
|
3438
|
+
}
|
|
3439
|
+
} catch {}
|
|
3440
|
+
}
|
|
2770
3441
|
function printPostInit() {
|
|
2771
3442
|
console.log(`
|
|
2772
3443
|
Registering with Claude Code...`);
|
|
@@ -2808,6 +3479,7 @@ function printUsage() {
|
|
|
2808
3479
|
console.log(" engrm update Update to latest version");
|
|
2809
3480
|
console.log(" engrm packs List available starter packs");
|
|
2810
3481
|
console.log(" engrm install-pack <name> Install a starter pack");
|
|
3482
|
+
console.log(" engrm doctor Run diagnostic checks");
|
|
2811
3483
|
console.log(" engrm sentinel Sentinel code audit commands");
|
|
2812
3484
|
console.log(" engrm sentinel init-rules Install Sentinel rule packs");
|
|
2813
3485
|
}
|