engrm 0.3.4 → 0.4.1
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 +631 -11
- package/dist/hooks/elicitation-result.js +32 -4
- package/dist/hooks/post-tool-use.js +329 -4
- package/dist/hooks/pre-compact.js +55 -10
- package/dist/hooks/sentinel.js +32 -4
- package/dist/hooks/session-start.js +468 -47
- package/dist/hooks/stop.js +961 -15
- package/dist/server.js +131 -10
- package/package.json +1 -1
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
5
|
|
|
6
|
+
// hooks/session-start.ts
|
|
7
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
8
|
+
import { join as join7 } from "path";
|
|
9
|
+
import { homedir as homedir4 } from "os";
|
|
10
|
+
|
|
6
11
|
// src/config.ts
|
|
7
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { homedir, hostname } from "node:os";
|
|
13
|
+
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
9
14
|
import { join } from "node:path";
|
|
10
|
-
import {
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
11
16
|
var CONFIG_DIR = join(homedir(), ".engrm");
|
|
12
17
|
var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
|
|
13
18
|
var DB_PATH = join(CONFIG_DIR, "engrm.db");
|
|
@@ -16,7 +21,22 @@ function getDbPath() {
|
|
|
16
21
|
}
|
|
17
22
|
function generateDeviceId() {
|
|
18
23
|
const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
19
|
-
|
|
24
|
+
let mac = "";
|
|
25
|
+
const ifaces = networkInterfaces();
|
|
26
|
+
for (const entries of Object.values(ifaces)) {
|
|
27
|
+
if (!entries)
|
|
28
|
+
continue;
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
31
|
+
mac = entry.mac;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (mac)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
39
|
+
const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
|
|
20
40
|
return `${host}-${suffix}`;
|
|
21
41
|
}
|
|
22
42
|
function createDefaultConfig() {
|
|
@@ -58,7 +78,10 @@ function createDefaultConfig() {
|
|
|
58
78
|
observer: {
|
|
59
79
|
enabled: true,
|
|
60
80
|
mode: "per_event",
|
|
61
|
-
model: "
|
|
81
|
+
model: "sonnet"
|
|
82
|
+
},
|
|
83
|
+
transcript_analysis: {
|
|
84
|
+
enabled: false
|
|
62
85
|
}
|
|
63
86
|
};
|
|
64
87
|
}
|
|
@@ -117,6 +140,9 @@ function loadConfig() {
|
|
|
117
140
|
enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
|
|
118
141
|
mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
|
|
119
142
|
model: asString(config["observer"]?.["model"], defaults.observer.model)
|
|
143
|
+
},
|
|
144
|
+
transcript_analysis: {
|
|
145
|
+
enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
120
146
|
}
|
|
121
147
|
};
|
|
122
148
|
}
|
|
@@ -1109,7 +1135,7 @@ function estimateTokens(text) {
|
|
|
1109
1135
|
}
|
|
1110
1136
|
function buildSessionContext(db, cwd, options = {}) {
|
|
1111
1137
|
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1112
|
-
const tokenBudget = opts.tokenBudget ??
|
|
1138
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1113
1139
|
const maxCount = opts.maxCount;
|
|
1114
1140
|
const detected = detectProject(cwd);
|
|
1115
1141
|
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
@@ -1131,6 +1157,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1131
1157
|
AND superseded_by IS NULL
|
|
1132
1158
|
ORDER BY quality DESC, created_at_epoch DESC
|
|
1133
1159
|
LIMIT ?`).all(project.id, MAX_PINNED);
|
|
1160
|
+
const MAX_RECENT = 5;
|
|
1161
|
+
const recent = db.db.query(`SELECT * FROM observations
|
|
1162
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1163
|
+
AND superseded_by IS NULL
|
|
1164
|
+
ORDER BY created_at_epoch DESC
|
|
1165
|
+
LIMIT ?`).all(project.id, MAX_RECENT);
|
|
1134
1166
|
const candidateLimit = maxCount ?? 50;
|
|
1135
1167
|
const candidates = db.db.query(`SELECT * FROM observations
|
|
1136
1168
|
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
@@ -1158,6 +1190,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1158
1190
|
});
|
|
1159
1191
|
}
|
|
1160
1192
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
1193
|
+
const dedupedRecent = recent.filter((o) => {
|
|
1194
|
+
if (seenIds.has(o.id))
|
|
1195
|
+
return false;
|
|
1196
|
+
seenIds.add(o.id);
|
|
1197
|
+
return true;
|
|
1198
|
+
});
|
|
1161
1199
|
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
1162
1200
|
for (const obs of crossProjectCandidates) {
|
|
1163
1201
|
if (!seenIds.has(obs.id)) {
|
|
@@ -1174,8 +1212,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1174
1212
|
return scoreB - scoreA;
|
|
1175
1213
|
});
|
|
1176
1214
|
if (maxCount !== undefined) {
|
|
1177
|
-
const remaining = Math.max(0, maxCount - pinned.length);
|
|
1178
|
-
const all = [...pinned, ...sorted.slice(0, remaining)];
|
|
1215
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1216
|
+
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
1179
1217
|
return {
|
|
1180
1218
|
project_name: project.name,
|
|
1181
1219
|
canonical_id: project.canonical_id,
|
|
@@ -1191,6 +1229,11 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1191
1229
|
remainingBudget -= cost;
|
|
1192
1230
|
selected.push(obs);
|
|
1193
1231
|
}
|
|
1232
|
+
for (const obs of dedupedRecent) {
|
|
1233
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1234
|
+
remainingBudget -= cost;
|
|
1235
|
+
selected.push(obs);
|
|
1236
|
+
}
|
|
1194
1237
|
for (const obs of sorted) {
|
|
1195
1238
|
const cost = estimateObservationTokens(obs, selected.length);
|
|
1196
1239
|
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
@@ -1198,7 +1241,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1198
1241
|
remainingBudget -= cost;
|
|
1199
1242
|
selected.push(obs);
|
|
1200
1243
|
}
|
|
1201
|
-
const summaries = db.getRecentSummaries(project.id,
|
|
1244
|
+
const summaries = db.getRecentSummaries(project.id, 5);
|
|
1202
1245
|
let securityFindings = [];
|
|
1203
1246
|
try {
|
|
1204
1247
|
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
@@ -1218,7 +1261,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1218
1261
|
};
|
|
1219
1262
|
}
|
|
1220
1263
|
function estimateObservationTokens(obs, index) {
|
|
1221
|
-
const DETAILED_THRESHOLD =
|
|
1264
|
+
const DETAILED_THRESHOLD = 5;
|
|
1222
1265
|
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1223
1266
|
if (index >= DETAILED_THRESHOLD) {
|
|
1224
1267
|
return titleCost;
|
|
@@ -1230,7 +1273,7 @@ function formatContextForInjection(context) {
|
|
|
1230
1273
|
if (context.observations.length === 0) {
|
|
1231
1274
|
return `Project: ${context.project_name} (no prior observations)`;
|
|
1232
1275
|
}
|
|
1233
|
-
const DETAILED_COUNT =
|
|
1276
|
+
const DETAILED_COUNT = 5;
|
|
1234
1277
|
const lines = [
|
|
1235
1278
|
`## Project Memory: ${context.project_name}`,
|
|
1236
1279
|
`${context.session_count} relevant observation(s) from prior sessions:`,
|
|
@@ -1455,9 +1498,83 @@ function detectStacksFromProject(projectRoot, filePaths = []) {
|
|
|
1455
1498
|
return { stacks: sorted, primary };
|
|
1456
1499
|
}
|
|
1457
1500
|
|
|
1501
|
+
// src/telemetry/config-fingerprint.ts
|
|
1502
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1503
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
1504
|
+
import { join as join4 } from "node:path";
|
|
1505
|
+
import { homedir as homedir2 } from "node:os";
|
|
1506
|
+
var STATE_PATH = join4(homedir2(), ".engrm", "config-fingerprint.json");
|
|
1507
|
+
var CLIENT_VERSION = "0.4.0";
|
|
1508
|
+
function hashFile(filePath) {
|
|
1509
|
+
try {
|
|
1510
|
+
if (!existsSync4(filePath))
|
|
1511
|
+
return null;
|
|
1512
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1513
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
1514
|
+
} catch {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function countMemoryFiles(memoryDir) {
|
|
1519
|
+
try {
|
|
1520
|
+
if (!existsSync4(memoryDir))
|
|
1521
|
+
return 0;
|
|
1522
|
+
return readdirSync(memoryDir).filter((f) => f.endsWith(".md")).length;
|
|
1523
|
+
} catch {
|
|
1524
|
+
return 0;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function readPreviousFingerprint() {
|
|
1528
|
+
try {
|
|
1529
|
+
if (!existsSync4(STATE_PATH))
|
|
1530
|
+
return null;
|
|
1531
|
+
return JSON.parse(readFileSync3(STATE_PATH, "utf-8"));
|
|
1532
|
+
} catch {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function saveFingerprint(fp) {
|
|
1537
|
+
try {
|
|
1538
|
+
const dir = join4(homedir2(), ".engrm");
|
|
1539
|
+
if (!existsSync4(dir))
|
|
1540
|
+
mkdirSync2(dir, { recursive: true });
|
|
1541
|
+
writeFileSync2(STATE_PATH, JSON.stringify(fp, null, 2) + `
|
|
1542
|
+
`, "utf-8");
|
|
1543
|
+
} catch {}
|
|
1544
|
+
}
|
|
1545
|
+
function computeAndSaveFingerprint(cwd) {
|
|
1546
|
+
const claudeMdHash = hashFile(join4(cwd, "CLAUDE.md"));
|
|
1547
|
+
const engrmJsonHash = hashFile(join4(cwd, ".engrm.json"));
|
|
1548
|
+
const slug = cwd.replace(/\//g, "-");
|
|
1549
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", slug, "memory");
|
|
1550
|
+
const memoryMdHash = hashFile(join4(memoryDir, "MEMORY.md"));
|
|
1551
|
+
const memoryFileCount = countMemoryFiles(memoryDir);
|
|
1552
|
+
const material = [
|
|
1553
|
+
claudeMdHash ?? "null",
|
|
1554
|
+
memoryMdHash ?? "null",
|
|
1555
|
+
engrmJsonHash ?? "null",
|
|
1556
|
+
String(memoryFileCount),
|
|
1557
|
+
CLIENT_VERSION
|
|
1558
|
+
].join("+");
|
|
1559
|
+
const configHash = createHash2("sha256").update(material).digest("hex");
|
|
1560
|
+
const previous = readPreviousFingerprint();
|
|
1561
|
+
const configChanged = previous !== null && previous.config_hash !== configHash;
|
|
1562
|
+
const fingerprint = {
|
|
1563
|
+
config_hash: configHash,
|
|
1564
|
+
config_changed: configChanged,
|
|
1565
|
+
claude_md_hash: claudeMdHash,
|
|
1566
|
+
memory_md_hash: memoryMdHash,
|
|
1567
|
+
engrm_json_hash: engrmJsonHash,
|
|
1568
|
+
memory_file_count: memoryFileCount,
|
|
1569
|
+
client_version: CLIENT_VERSION
|
|
1570
|
+
};
|
|
1571
|
+
saveFingerprint(fingerprint);
|
|
1572
|
+
return fingerprint;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1458
1575
|
// src/packs/recommender.ts
|
|
1459
|
-
import { existsSync as
|
|
1460
|
-
import { join as
|
|
1576
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
1577
|
+
import { join as join5, basename as basename3, dirname } from "node:path";
|
|
1461
1578
|
import { fileURLToPath } from "node:url";
|
|
1462
1579
|
var STACK_PACK_MAP = {
|
|
1463
1580
|
typescript: ["typescript-patterns"],
|
|
@@ -1470,20 +1587,20 @@ var STACK_PACK_MAP = {
|
|
|
1470
1587
|
};
|
|
1471
1588
|
function getPacksDir() {
|
|
1472
1589
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
1473
|
-
return
|
|
1590
|
+
return join5(thisDir, "../../packs");
|
|
1474
1591
|
}
|
|
1475
1592
|
function listAvailablePacks() {
|
|
1476
1593
|
const dir = getPacksDir();
|
|
1477
|
-
if (!
|
|
1594
|
+
if (!existsSync5(dir))
|
|
1478
1595
|
return [];
|
|
1479
|
-
return
|
|
1596
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => basename3(f, ".json"));
|
|
1480
1597
|
}
|
|
1481
1598
|
function loadPack(name) {
|
|
1482
|
-
const packPath =
|
|
1483
|
-
if (!
|
|
1599
|
+
const packPath = join5(getPacksDir(), `${name}.json`);
|
|
1600
|
+
if (!existsSync5(packPath))
|
|
1484
1601
|
return null;
|
|
1485
1602
|
try {
|
|
1486
|
-
const raw =
|
|
1603
|
+
const raw = readFileSync4(packPath, "utf-8");
|
|
1487
1604
|
return JSON.parse(raw);
|
|
1488
1605
|
} catch {
|
|
1489
1606
|
return null;
|
|
@@ -1517,6 +1634,202 @@ function recommendPacks(stacks, installedPacks) {
|
|
|
1517
1634
|
return recommendations;
|
|
1518
1635
|
}
|
|
1519
1636
|
|
|
1637
|
+
// src/config.ts
|
|
1638
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1639
|
+
import { homedir as homedir3, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
|
|
1640
|
+
import { join as join6 } from "node:path";
|
|
1641
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1642
|
+
var CONFIG_DIR2 = join6(homedir3(), ".engrm");
|
|
1643
|
+
var SETTINGS_PATH2 = join6(CONFIG_DIR2, "settings.json");
|
|
1644
|
+
var DB_PATH2 = join6(CONFIG_DIR2, "engrm.db");
|
|
1645
|
+
function getDbPath2() {
|
|
1646
|
+
return DB_PATH2;
|
|
1647
|
+
}
|
|
1648
|
+
function generateDeviceId2() {
|
|
1649
|
+
const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1650
|
+
let mac = "";
|
|
1651
|
+
const ifaces = networkInterfaces2();
|
|
1652
|
+
for (const entries of Object.values(ifaces)) {
|
|
1653
|
+
if (!entries)
|
|
1654
|
+
continue;
|
|
1655
|
+
for (const entry of entries) {
|
|
1656
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
1657
|
+
mac = entry.mac;
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (mac)
|
|
1662
|
+
break;
|
|
1663
|
+
}
|
|
1664
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
1665
|
+
const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
|
|
1666
|
+
return `${host}-${suffix}`;
|
|
1667
|
+
}
|
|
1668
|
+
function createDefaultConfig2() {
|
|
1669
|
+
return {
|
|
1670
|
+
candengo_url: "",
|
|
1671
|
+
candengo_api_key: "",
|
|
1672
|
+
site_id: "",
|
|
1673
|
+
namespace: "",
|
|
1674
|
+
user_id: "",
|
|
1675
|
+
user_email: "",
|
|
1676
|
+
device_id: generateDeviceId2(),
|
|
1677
|
+
teams: [],
|
|
1678
|
+
sync: {
|
|
1679
|
+
enabled: true,
|
|
1680
|
+
interval_seconds: 30,
|
|
1681
|
+
batch_size: 50
|
|
1682
|
+
},
|
|
1683
|
+
search: {
|
|
1684
|
+
default_limit: 10,
|
|
1685
|
+
local_boost: 1.2,
|
|
1686
|
+
scope: "all"
|
|
1687
|
+
},
|
|
1688
|
+
scrubbing: {
|
|
1689
|
+
enabled: true,
|
|
1690
|
+
custom_patterns: [],
|
|
1691
|
+
default_sensitivity: "shared"
|
|
1692
|
+
},
|
|
1693
|
+
sentinel: {
|
|
1694
|
+
enabled: false,
|
|
1695
|
+
mode: "advisory",
|
|
1696
|
+
provider: "openai",
|
|
1697
|
+
model: "gpt-4o-mini",
|
|
1698
|
+
api_key: "",
|
|
1699
|
+
base_url: "",
|
|
1700
|
+
skip_patterns: [],
|
|
1701
|
+
daily_limit: 100,
|
|
1702
|
+
tier: "free"
|
|
1703
|
+
},
|
|
1704
|
+
observer: {
|
|
1705
|
+
enabled: true,
|
|
1706
|
+
mode: "per_event",
|
|
1707
|
+
model: "sonnet"
|
|
1708
|
+
},
|
|
1709
|
+
transcript_analysis: {
|
|
1710
|
+
enabled: false
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function loadConfig2() {
|
|
1715
|
+
if (!existsSync6(SETTINGS_PATH2)) {
|
|
1716
|
+
throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
|
|
1717
|
+
}
|
|
1718
|
+
const raw = readFileSync5(SETTINGS_PATH2, "utf-8");
|
|
1719
|
+
let parsed;
|
|
1720
|
+
try {
|
|
1721
|
+
parsed = JSON.parse(raw);
|
|
1722
|
+
} catch {
|
|
1723
|
+
throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
|
|
1724
|
+
}
|
|
1725
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1726
|
+
throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
|
|
1727
|
+
}
|
|
1728
|
+
const config = parsed;
|
|
1729
|
+
const defaults = createDefaultConfig2();
|
|
1730
|
+
return {
|
|
1731
|
+
candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
|
|
1732
|
+
candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
|
|
1733
|
+
site_id: asString2(config["site_id"], defaults.site_id),
|
|
1734
|
+
namespace: asString2(config["namespace"], defaults.namespace),
|
|
1735
|
+
user_id: asString2(config["user_id"], defaults.user_id),
|
|
1736
|
+
user_email: asString2(config["user_email"], defaults.user_email),
|
|
1737
|
+
device_id: asString2(config["device_id"], defaults.device_id),
|
|
1738
|
+
teams: asTeams2(config["teams"], defaults.teams),
|
|
1739
|
+
sync: {
|
|
1740
|
+
enabled: asBool2(config["sync"]?.["enabled"], defaults.sync.enabled),
|
|
1741
|
+
interval_seconds: asNumber2(config["sync"]?.["interval_seconds"], defaults.sync.interval_seconds),
|
|
1742
|
+
batch_size: asNumber2(config["sync"]?.["batch_size"], defaults.sync.batch_size)
|
|
1743
|
+
},
|
|
1744
|
+
search: {
|
|
1745
|
+
default_limit: asNumber2(config["search"]?.["default_limit"], defaults.search.default_limit),
|
|
1746
|
+
local_boost: asNumber2(config["search"]?.["local_boost"], defaults.search.local_boost),
|
|
1747
|
+
scope: asScope2(config["search"]?.["scope"], defaults.search.scope)
|
|
1748
|
+
},
|
|
1749
|
+
scrubbing: {
|
|
1750
|
+
enabled: asBool2(config["scrubbing"]?.["enabled"], defaults.scrubbing.enabled),
|
|
1751
|
+
custom_patterns: asStringArray2(config["scrubbing"]?.["custom_patterns"], defaults.scrubbing.custom_patterns),
|
|
1752
|
+
default_sensitivity: asSensitivity2(config["scrubbing"]?.["default_sensitivity"], defaults.scrubbing.default_sensitivity)
|
|
1753
|
+
},
|
|
1754
|
+
sentinel: {
|
|
1755
|
+
enabled: asBool2(config["sentinel"]?.["enabled"], defaults.sentinel.enabled),
|
|
1756
|
+
mode: asSentinelMode2(config["sentinel"]?.["mode"], defaults.sentinel.mode),
|
|
1757
|
+
provider: asLlmProvider2(config["sentinel"]?.["provider"], defaults.sentinel.provider),
|
|
1758
|
+
model: asString2(config["sentinel"]?.["model"], defaults.sentinel.model),
|
|
1759
|
+
api_key: asString2(config["sentinel"]?.["api_key"], defaults.sentinel.api_key),
|
|
1760
|
+
base_url: asString2(config["sentinel"]?.["base_url"], defaults.sentinel.base_url),
|
|
1761
|
+
skip_patterns: asStringArray2(config["sentinel"]?.["skip_patterns"], defaults.sentinel.skip_patterns),
|
|
1762
|
+
daily_limit: asNumber2(config["sentinel"]?.["daily_limit"], defaults.sentinel.daily_limit),
|
|
1763
|
+
tier: asTier2(config["sentinel"]?.["tier"], defaults.sentinel.tier)
|
|
1764
|
+
},
|
|
1765
|
+
observer: {
|
|
1766
|
+
enabled: asBool2(config["observer"]?.["enabled"], defaults.observer.enabled),
|
|
1767
|
+
mode: asObserverMode2(config["observer"]?.["mode"], defaults.observer.mode),
|
|
1768
|
+
model: asString2(config["observer"]?.["model"], defaults.observer.model)
|
|
1769
|
+
},
|
|
1770
|
+
transcript_analysis: {
|
|
1771
|
+
enabled: asBool2(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function saveConfig(config) {
|
|
1776
|
+
if (!existsSync6(CONFIG_DIR2)) {
|
|
1777
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
1778
|
+
}
|
|
1779
|
+
writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
|
|
1780
|
+
`, "utf-8");
|
|
1781
|
+
}
|
|
1782
|
+
function configExists2() {
|
|
1783
|
+
return existsSync6(SETTINGS_PATH2);
|
|
1784
|
+
}
|
|
1785
|
+
function asString2(value, fallback) {
|
|
1786
|
+
return typeof value === "string" ? value : fallback;
|
|
1787
|
+
}
|
|
1788
|
+
function asNumber2(value, fallback) {
|
|
1789
|
+
return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
|
|
1790
|
+
}
|
|
1791
|
+
function asBool2(value, fallback) {
|
|
1792
|
+
return typeof value === "boolean" ? value : fallback;
|
|
1793
|
+
}
|
|
1794
|
+
function asStringArray2(value, fallback) {
|
|
1795
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : fallback;
|
|
1796
|
+
}
|
|
1797
|
+
function asScope2(value, fallback) {
|
|
1798
|
+
if (value === "personal" || value === "team" || value === "all")
|
|
1799
|
+
return value;
|
|
1800
|
+
return fallback;
|
|
1801
|
+
}
|
|
1802
|
+
function asSensitivity2(value, fallback) {
|
|
1803
|
+
if (value === "shared" || value === "personal" || value === "secret")
|
|
1804
|
+
return value;
|
|
1805
|
+
return fallback;
|
|
1806
|
+
}
|
|
1807
|
+
function asSentinelMode2(value, fallback) {
|
|
1808
|
+
if (value === "advisory" || value === "blocking")
|
|
1809
|
+
return value;
|
|
1810
|
+
return fallback;
|
|
1811
|
+
}
|
|
1812
|
+
function asLlmProvider2(value, fallback) {
|
|
1813
|
+
if (value === "openai" || value === "anthropic" || value === "ollama" || value === "custom")
|
|
1814
|
+
return value;
|
|
1815
|
+
return fallback;
|
|
1816
|
+
}
|
|
1817
|
+
function asTier2(value, fallback) {
|
|
1818
|
+
if (value === "free" || value === "vibe" || value === "solo" || value === "pro" || value === "team" || value === "enterprise")
|
|
1819
|
+
return value;
|
|
1820
|
+
return fallback;
|
|
1821
|
+
}
|
|
1822
|
+
function asObserverMode2(value, fallback) {
|
|
1823
|
+
if (value === "per_event" || value === "per_session")
|
|
1824
|
+
return value;
|
|
1825
|
+
return fallback;
|
|
1826
|
+
}
|
|
1827
|
+
function asTeams2(value, fallback) {
|
|
1828
|
+
if (!Array.isArray(value))
|
|
1829
|
+
return fallback;
|
|
1830
|
+
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1520
1833
|
// src/sync/auth.ts
|
|
1521
1834
|
function getApiKey(config) {
|
|
1522
1835
|
const envKey = process.env.ENGRM_TOKEN;
|
|
@@ -1702,6 +2015,44 @@ function extractNarrative(content) {
|
|
|
1702
2015
|
`).trim();
|
|
1703
2016
|
return narrative.length > 0 ? narrative : null;
|
|
1704
2017
|
}
|
|
2018
|
+
async function pullSettings(client, config) {
|
|
2019
|
+
try {
|
|
2020
|
+
const settings = await client.fetchSettings();
|
|
2021
|
+
if (!settings)
|
|
2022
|
+
return false;
|
|
2023
|
+
let changed = false;
|
|
2024
|
+
if (settings.transcript_analysis !== undefined) {
|
|
2025
|
+
const ta = settings.transcript_analysis;
|
|
2026
|
+
if (typeof ta === "object" && ta !== null) {
|
|
2027
|
+
const taObj = ta;
|
|
2028
|
+
if (taObj.enabled !== undefined && taObj.enabled !== config.transcript_analysis.enabled) {
|
|
2029
|
+
config.transcript_analysis.enabled = !!taObj.enabled;
|
|
2030
|
+
changed = true;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (settings.observer !== undefined) {
|
|
2035
|
+
const obs = settings.observer;
|
|
2036
|
+
if (typeof obs === "object" && obs !== null) {
|
|
2037
|
+
const obsObj = obs;
|
|
2038
|
+
if (obsObj.enabled !== undefined && obsObj.enabled !== config.observer.enabled) {
|
|
2039
|
+
config.observer.enabled = !!obsObj.enabled;
|
|
2040
|
+
changed = true;
|
|
2041
|
+
}
|
|
2042
|
+
if (obsObj.model !== undefined && typeof obsObj.model === "string" && obsObj.model !== config.observer.model) {
|
|
2043
|
+
config.observer.model = obsObj.model;
|
|
2044
|
+
changed = true;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (changed) {
|
|
2049
|
+
saveConfig(config);
|
|
2050
|
+
}
|
|
2051
|
+
return changed;
|
|
2052
|
+
} catch {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1705
2056
|
|
|
1706
2057
|
// src/sync/client.ts
|
|
1707
2058
|
class VectorClient {
|
|
@@ -1756,6 +2107,13 @@ class VectorClient {
|
|
|
1756
2107
|
async sendTelemetry(beacon) {
|
|
1757
2108
|
await this.request("POST", "/v1/mem/telemetry", beacon);
|
|
1758
2109
|
}
|
|
2110
|
+
async fetchSettings() {
|
|
2111
|
+
try {
|
|
2112
|
+
return await this.request("GET", "/v1/mem/user-settings");
|
|
2113
|
+
} catch {
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
1759
2117
|
async health() {
|
|
1760
2118
|
try {
|
|
1761
2119
|
await this.request("GET", "/health");
|
|
@@ -1824,57 +2182,120 @@ async function main() {
|
|
|
1824
2182
|
} catch {
|
|
1825
2183
|
process.exit(0);
|
|
1826
2184
|
}
|
|
2185
|
+
let syncedCount = 0;
|
|
1827
2186
|
try {
|
|
1828
2187
|
if (config.sync.enabled && config.candengo_api_key) {
|
|
1829
2188
|
try {
|
|
1830
|
-
const client = new VectorClient(config
|
|
2189
|
+
const client = new VectorClient(config);
|
|
1831
2190
|
const pullResult = await pullFromVector(db, client, config, 50);
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
2191
|
+
syncedCount = pullResult.merged;
|
|
2192
|
+
await pullSettings(client, config);
|
|
1835
2193
|
} catch {}
|
|
1836
2194
|
}
|
|
2195
|
+
try {
|
|
2196
|
+
computeAndSaveFingerprint(event.cwd);
|
|
2197
|
+
} catch {}
|
|
1837
2198
|
const context = buildSessionContext(db, event.cwd, {
|
|
1838
2199
|
tokenBudget: 800,
|
|
1839
2200
|
scope: config.search.scope
|
|
1840
2201
|
});
|
|
2202
|
+
if (context) {
|
|
2203
|
+
try {
|
|
2204
|
+
const dir = join7(homedir4(), ".engrm");
|
|
2205
|
+
if (!existsSync7(dir))
|
|
2206
|
+
mkdirSync4(dir, { recursive: true });
|
|
2207
|
+
writeFileSync4(join7(dir, "hook-session-metrics.json"), JSON.stringify({
|
|
2208
|
+
contextObsInjected: context.observations.length,
|
|
2209
|
+
contextTotalAvailable: context.total_active
|
|
2210
|
+
}), "utf-8");
|
|
2211
|
+
} catch {}
|
|
2212
|
+
}
|
|
1841
2213
|
if (context && context.observations.length > 0) {
|
|
1842
|
-
console.log(formatContextForInjection(context));
|
|
1843
|
-
const parts = [];
|
|
1844
|
-
parts.push(`${context.session_count} observation(s)`);
|
|
1845
|
-
if (context.securityFindings && context.securityFindings.length > 0) {
|
|
1846
|
-
parts.push(`${context.securityFindings.length} security finding(s)`);
|
|
1847
|
-
}
|
|
1848
2214
|
const remaining = context.total_active - context.session_count;
|
|
1849
|
-
|
|
1850
|
-
parts.push(`${remaining} more searchable`);
|
|
1851
|
-
}
|
|
2215
|
+
let msgCount = 0;
|
|
1852
2216
|
try {
|
|
1853
2217
|
const readKey = `messages_read_${config.device_id}`;
|
|
1854
2218
|
const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2219
|
+
msgCount = db.db.query("SELECT COUNT(*) as c FROM observations WHERE type = 'message' AND id > ? AND lifecycle IN ('active', 'pinned')").get(lastReadId)?.c ?? 0;
|
|
2220
|
+
} catch {}
|
|
2221
|
+
const splash = formatSplashScreen({
|
|
2222
|
+
projectName: context.project_name,
|
|
2223
|
+
loaded: context.session_count,
|
|
2224
|
+
available: remaining,
|
|
2225
|
+
securityFindings: context.securityFindings?.length ?? 0,
|
|
2226
|
+
unreadMessages: msgCount,
|
|
2227
|
+
synced: syncedCount
|
|
2228
|
+
});
|
|
2229
|
+
let packLine = "";
|
|
2230
|
+
try {
|
|
2231
|
+
const { stacks } = detectStacksFromProject(event.cwd);
|
|
2232
|
+
if (stacks.length > 0) {
|
|
2233
|
+
const installed = db.getInstalledPacks();
|
|
2234
|
+
const recs = recommendPacks(stacks, installed);
|
|
2235
|
+
if (recs.length > 0) {
|
|
2236
|
+
const names = recs.map((r) => `\`${r.name}\``).join(", ");
|
|
2237
|
+
packLine = `
|
|
2238
|
+
Help packs available for your stack: ${names}. ` + `Use the install_pack tool to load curated observations.`;
|
|
2239
|
+
}
|
|
1858
2240
|
}
|
|
1859
2241
|
} catch {}
|
|
1860
|
-
console.
|
|
2242
|
+
console.log(JSON.stringify({
|
|
2243
|
+
hookSpecificOutput: {
|
|
2244
|
+
hookEventName: "SessionStart",
|
|
2245
|
+
additionalContext: formatContextForInjection(context) + packLine
|
|
2246
|
+
},
|
|
2247
|
+
systemMessage: splash
|
|
2248
|
+
}));
|
|
1861
2249
|
}
|
|
1862
|
-
try {
|
|
1863
|
-
const { stacks } = detectStacksFromProject(event.cwd);
|
|
1864
|
-
if (stacks.length > 0) {
|
|
1865
|
-
const installed = db.getInstalledPacks();
|
|
1866
|
-
const recs = recommendPacks(stacks, installed);
|
|
1867
|
-
if (recs.length > 0) {
|
|
1868
|
-
const names = recs.map((r) => `\`${r.name}\``).join(", ");
|
|
1869
|
-
console.log(`
|
|
1870
|
-
Help packs available for your stack: ${names}. ` + `Use the install_pack tool to load curated observations.`);
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
} catch {}
|
|
1874
2250
|
} finally {
|
|
1875
2251
|
db.close();
|
|
1876
2252
|
}
|
|
1877
2253
|
}
|
|
2254
|
+
var c = {
|
|
2255
|
+
reset: "\x1B[0m",
|
|
2256
|
+
dim: "\x1B[2m",
|
|
2257
|
+
bold: "\x1B[1m",
|
|
2258
|
+
cyan: "\x1B[36m",
|
|
2259
|
+
green: "\x1B[32m",
|
|
2260
|
+
yellow: "\x1B[33m",
|
|
2261
|
+
magenta: "\x1B[35m",
|
|
2262
|
+
white: "\x1B[37m"
|
|
2263
|
+
};
|
|
2264
|
+
function formatSplashScreen(data) {
|
|
2265
|
+
const lines = [];
|
|
2266
|
+
lines.push("");
|
|
2267
|
+
lines.push(`${c.cyan}${c.bold} ______ ____ _ ______ _____ ____ __${c.reset}`);
|
|
2268
|
+
lines.push(`${c.cyan}${c.bold} | ___|| \\ | || ___|| | | \\ / |${c.reset}`);
|
|
2269
|
+
lines.push(`${c.cyan}${c.bold} | ___|| \\| || | || \\ | \\/ |${c.reset}`);
|
|
2270
|
+
lines.push(`${c.cyan}${c.bold} |______||__/\\____||______||__|\\__\\|__/\\__/|__|${c.reset}`);
|
|
2271
|
+
lines.push(`${c.dim} memory layer for AI agents${c.reset}`);
|
|
2272
|
+
lines.push("");
|
|
2273
|
+
const dot = `${c.dim} \xB7 ${c.reset}`;
|
|
2274
|
+
const statParts = [];
|
|
2275
|
+
statParts.push(`${c.green}${data.loaded}${c.reset} loaded`);
|
|
2276
|
+
if (data.available > 0) {
|
|
2277
|
+
statParts.push(`${c.dim}${data.available.toLocaleString()} searchable${c.reset}`);
|
|
2278
|
+
}
|
|
2279
|
+
if (data.synced > 0) {
|
|
2280
|
+
statParts.push(`${c.cyan}${data.synced} synced${c.reset}`);
|
|
2281
|
+
}
|
|
2282
|
+
lines.push(` ${c.white}${c.bold}engrm${c.reset}${dot}${statParts.join(dot)}`);
|
|
2283
|
+
const alerts = [];
|
|
2284
|
+
if (data.securityFindings > 0) {
|
|
2285
|
+
alerts.push(`${c.yellow}${data.securityFindings} security finding${data.securityFindings !== 1 ? "s" : ""}${c.reset}`);
|
|
2286
|
+
}
|
|
2287
|
+
if (data.unreadMessages > 0) {
|
|
2288
|
+
alerts.push(`${c.magenta}${data.unreadMessages} unread message${data.unreadMessages !== 1 ? "s" : ""}${c.reset}`);
|
|
2289
|
+
}
|
|
2290
|
+
if (alerts.length > 0) {
|
|
2291
|
+
lines.push(` ${alerts.join(dot)}`);
|
|
2292
|
+
}
|
|
2293
|
+
lines.push("");
|
|
2294
|
+
lines.push(` ${c.dim}Dashboard: https://engrm.dev/dashboard${c.reset}`);
|
|
2295
|
+
lines.push("");
|
|
2296
|
+
return lines.join(`
|
|
2297
|
+
`);
|
|
2298
|
+
}
|
|
1878
2299
|
main().catch(() => {
|
|
1879
2300
|
process.exit(0);
|
|
1880
2301
|
});
|