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
|
@@ -5,9 +5,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { homedir, hostname } from "node:os";
|
|
8
|
+
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
11
|
var CONFIG_DIR = join(homedir(), ".engrm");
|
|
12
12
|
var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
|
|
13
13
|
var DB_PATH = join(CONFIG_DIR, "engrm.db");
|
|
@@ -16,7 +16,22 @@ function getDbPath() {
|
|
|
16
16
|
}
|
|
17
17
|
function generateDeviceId() {
|
|
18
18
|
const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
19
|
-
|
|
19
|
+
let mac = "";
|
|
20
|
+
const ifaces = networkInterfaces();
|
|
21
|
+
for (const entries of Object.values(ifaces)) {
|
|
22
|
+
if (!entries)
|
|
23
|
+
continue;
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
26
|
+
mac = entry.mac;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (mac)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
34
|
+
const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
|
|
20
35
|
return `${host}-${suffix}`;
|
|
21
36
|
}
|
|
22
37
|
function createDefaultConfig() {
|
|
@@ -58,7 +73,10 @@ function createDefaultConfig() {
|
|
|
58
73
|
observer: {
|
|
59
74
|
enabled: true,
|
|
60
75
|
mode: "per_event",
|
|
61
|
-
model: "
|
|
76
|
+
model: "sonnet"
|
|
77
|
+
},
|
|
78
|
+
transcript_analysis: {
|
|
79
|
+
enabled: false
|
|
62
80
|
}
|
|
63
81
|
};
|
|
64
82
|
}
|
|
@@ -117,6 +135,9 @@ function loadConfig() {
|
|
|
117
135
|
enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
|
|
118
136
|
mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
|
|
119
137
|
model: asString(config["observer"]?.["model"], defaults.observer.model)
|
|
138
|
+
},
|
|
139
|
+
transcript_analysis: {
|
|
140
|
+
enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
120
141
|
}
|
|
121
142
|
};
|
|
122
143
|
}
|
|
@@ -515,6 +536,56 @@ function runMigrations(db) {
|
|
|
515
536
|
}
|
|
516
537
|
}
|
|
517
538
|
}
|
|
539
|
+
function ensureObservationTypes(db) {
|
|
540
|
+
try {
|
|
541
|
+
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)");
|
|
542
|
+
db.exec("DELETE FROM observations WHERE session_id = '_typecheck'");
|
|
543
|
+
} catch {
|
|
544
|
+
db.exec("BEGIN TRANSACTION");
|
|
545
|
+
try {
|
|
546
|
+
db.exec(`
|
|
547
|
+
CREATE TABLE observations_repair (
|
|
548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT,
|
|
549
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
550
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
551
|
+
'bugfix','discovery','decision','pattern','change','feature',
|
|
552
|
+
'refactor','digest','standard','message')),
|
|
553
|
+
title TEXT NOT NULL, narrative TEXT, facts TEXT, concepts TEXT,
|
|
554
|
+
files_read TEXT, files_modified TEXT,
|
|
555
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
556
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN ('active','aging','archived','purged','pinned')),
|
|
557
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN ('shared','personal','secret')),
|
|
558
|
+
user_id TEXT NOT NULL, device_id TEXT NOT NULL, agent TEXT DEFAULT 'claude-code',
|
|
559
|
+
created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
|
|
560
|
+
archived_at_epoch INTEGER,
|
|
561
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
562
|
+
superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
563
|
+
remote_source_id TEXT
|
|
564
|
+
);
|
|
565
|
+
INSERT INTO observations_repair SELECT * FROM observations;
|
|
566
|
+
DROP TABLE observations;
|
|
567
|
+
ALTER TABLE observations_repair RENAME TO observations;
|
|
568
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
569
|
+
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
570
|
+
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_observations_lifecycle ON observations(lifecycle);
|
|
573
|
+
CREATE INDEX IF NOT EXISTS idx_observations_quality ON observations(quality);
|
|
574
|
+
CREATE INDEX IF NOT EXISTS idx_observations_user ON observations(user_id);
|
|
575
|
+
CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by);
|
|
576
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
577
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
578
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
579
|
+
title, narrative, facts, concepts, content=observations, content_rowid=id
|
|
580
|
+
);
|
|
581
|
+
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
582
|
+
`);
|
|
583
|
+
db.exec("COMMIT");
|
|
584
|
+
} catch (err) {
|
|
585
|
+
db.exec("ROLLBACK");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
518
589
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
519
590
|
|
|
520
591
|
// src/storage/sqlite.ts
|
|
@@ -581,6 +652,7 @@ class MemDatabase {
|
|
|
581
652
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
582
653
|
this.vecAvailable = this.loadVecExtension();
|
|
583
654
|
runMigrations(this.db);
|
|
655
|
+
ensureObservationTypes(this.db);
|
|
584
656
|
}
|
|
585
657
|
loadVecExtension() {
|
|
586
658
|
try {
|
|
@@ -1058,7 +1130,7 @@ function estimateTokens(text) {
|
|
|
1058
1130
|
}
|
|
1059
1131
|
function buildSessionContext(db, cwd, options = {}) {
|
|
1060
1132
|
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1061
|
-
const tokenBudget = opts.tokenBudget ??
|
|
1133
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1062
1134
|
const maxCount = opts.maxCount;
|
|
1063
1135
|
const detected = detectProject(cwd);
|
|
1064
1136
|
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
@@ -1080,6 +1152,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1080
1152
|
AND superseded_by IS NULL
|
|
1081
1153
|
ORDER BY quality DESC, created_at_epoch DESC
|
|
1082
1154
|
LIMIT ?`).all(project.id, MAX_PINNED);
|
|
1155
|
+
const MAX_RECENT = 5;
|
|
1156
|
+
const recent = db.db.query(`SELECT * FROM observations
|
|
1157
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1158
|
+
AND superseded_by IS NULL
|
|
1159
|
+
ORDER BY created_at_epoch DESC
|
|
1160
|
+
LIMIT ?`).all(project.id, MAX_RECENT);
|
|
1083
1161
|
const candidateLimit = maxCount ?? 50;
|
|
1084
1162
|
const candidates = db.db.query(`SELECT * FROM observations
|
|
1085
1163
|
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
@@ -1107,6 +1185,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1107
1185
|
});
|
|
1108
1186
|
}
|
|
1109
1187
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
1188
|
+
const dedupedRecent = recent.filter((o) => {
|
|
1189
|
+
if (seenIds.has(o.id))
|
|
1190
|
+
return false;
|
|
1191
|
+
seenIds.add(o.id);
|
|
1192
|
+
return true;
|
|
1193
|
+
});
|
|
1110
1194
|
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
1111
1195
|
for (const obs of crossProjectCandidates) {
|
|
1112
1196
|
if (!seenIds.has(obs.id)) {
|
|
@@ -1123,8 +1207,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1123
1207
|
return scoreB - scoreA;
|
|
1124
1208
|
});
|
|
1125
1209
|
if (maxCount !== undefined) {
|
|
1126
|
-
const remaining = Math.max(0, maxCount - pinned.length);
|
|
1127
|
-
const all = [...pinned, ...sorted.slice(0, remaining)];
|
|
1210
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1211
|
+
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
1128
1212
|
return {
|
|
1129
1213
|
project_name: project.name,
|
|
1130
1214
|
canonical_id: project.canonical_id,
|
|
@@ -1140,6 +1224,11 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1140
1224
|
remainingBudget -= cost;
|
|
1141
1225
|
selected.push(obs);
|
|
1142
1226
|
}
|
|
1227
|
+
for (const obs of dedupedRecent) {
|
|
1228
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1229
|
+
remainingBudget -= cost;
|
|
1230
|
+
selected.push(obs);
|
|
1231
|
+
}
|
|
1143
1232
|
for (const obs of sorted) {
|
|
1144
1233
|
const cost = estimateObservationTokens(obs, selected.length);
|
|
1145
1234
|
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
@@ -1147,7 +1236,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1147
1236
|
remainingBudget -= cost;
|
|
1148
1237
|
selected.push(obs);
|
|
1149
1238
|
}
|
|
1150
|
-
const summaries = db.getRecentSummaries(project.id,
|
|
1239
|
+
const summaries = db.getRecentSummaries(project.id, 5);
|
|
1151
1240
|
let securityFindings = [];
|
|
1152
1241
|
try {
|
|
1153
1242
|
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
@@ -1167,7 +1256,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1167
1256
|
};
|
|
1168
1257
|
}
|
|
1169
1258
|
function estimateObservationTokens(obs, index) {
|
|
1170
|
-
const DETAILED_THRESHOLD =
|
|
1259
|
+
const DETAILED_THRESHOLD = 5;
|
|
1171
1260
|
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1172
1261
|
if (index >= DETAILED_THRESHOLD) {
|
|
1173
1262
|
return titleCost;
|
|
@@ -1179,7 +1268,7 @@ function formatContextForInjection(context) {
|
|
|
1179
1268
|
if (context.observations.length === 0) {
|
|
1180
1269
|
return `Project: ${context.project_name} (no prior observations)`;
|
|
1181
1270
|
}
|
|
1182
|
-
const DETAILED_COUNT =
|
|
1271
|
+
const DETAILED_COUNT = 5;
|
|
1183
1272
|
const lines = [
|
|
1184
1273
|
`## Project Memory: ${context.project_name}`,
|
|
1185
1274
|
`${context.session_count} relevant observation(s) from prior sessions:`,
|
|
@@ -1466,6 +1555,202 @@ function recommendPacks(stacks, installedPacks) {
|
|
|
1466
1555
|
return recommendations;
|
|
1467
1556
|
}
|
|
1468
1557
|
|
|
1558
|
+
// src/config.ts
|
|
1559
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1560
|
+
import { homedir as homedir2, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
|
|
1561
|
+
import { join as join5 } from "node:path";
|
|
1562
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1563
|
+
var CONFIG_DIR2 = join5(homedir2(), ".engrm");
|
|
1564
|
+
var SETTINGS_PATH2 = join5(CONFIG_DIR2, "settings.json");
|
|
1565
|
+
var DB_PATH2 = join5(CONFIG_DIR2, "engrm.db");
|
|
1566
|
+
function getDbPath2() {
|
|
1567
|
+
return DB_PATH2;
|
|
1568
|
+
}
|
|
1569
|
+
function generateDeviceId2() {
|
|
1570
|
+
const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1571
|
+
let mac = "";
|
|
1572
|
+
const ifaces = networkInterfaces2();
|
|
1573
|
+
for (const entries of Object.values(ifaces)) {
|
|
1574
|
+
if (!entries)
|
|
1575
|
+
continue;
|
|
1576
|
+
for (const entry of entries) {
|
|
1577
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
1578
|
+
mac = entry.mac;
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (mac)
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
const material = `${host}:${mac || "no-mac"}`;
|
|
1586
|
+
const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
|
|
1587
|
+
return `${host}-${suffix}`;
|
|
1588
|
+
}
|
|
1589
|
+
function createDefaultConfig2() {
|
|
1590
|
+
return {
|
|
1591
|
+
candengo_url: "",
|
|
1592
|
+
candengo_api_key: "",
|
|
1593
|
+
site_id: "",
|
|
1594
|
+
namespace: "",
|
|
1595
|
+
user_id: "",
|
|
1596
|
+
user_email: "",
|
|
1597
|
+
device_id: generateDeviceId2(),
|
|
1598
|
+
teams: [],
|
|
1599
|
+
sync: {
|
|
1600
|
+
enabled: true,
|
|
1601
|
+
interval_seconds: 30,
|
|
1602
|
+
batch_size: 50
|
|
1603
|
+
},
|
|
1604
|
+
search: {
|
|
1605
|
+
default_limit: 10,
|
|
1606
|
+
local_boost: 1.2,
|
|
1607
|
+
scope: "all"
|
|
1608
|
+
},
|
|
1609
|
+
scrubbing: {
|
|
1610
|
+
enabled: true,
|
|
1611
|
+
custom_patterns: [],
|
|
1612
|
+
default_sensitivity: "shared"
|
|
1613
|
+
},
|
|
1614
|
+
sentinel: {
|
|
1615
|
+
enabled: false,
|
|
1616
|
+
mode: "advisory",
|
|
1617
|
+
provider: "openai",
|
|
1618
|
+
model: "gpt-4o-mini",
|
|
1619
|
+
api_key: "",
|
|
1620
|
+
base_url: "",
|
|
1621
|
+
skip_patterns: [],
|
|
1622
|
+
daily_limit: 100,
|
|
1623
|
+
tier: "free"
|
|
1624
|
+
},
|
|
1625
|
+
observer: {
|
|
1626
|
+
enabled: true,
|
|
1627
|
+
mode: "per_event",
|
|
1628
|
+
model: "sonnet"
|
|
1629
|
+
},
|
|
1630
|
+
transcript_analysis: {
|
|
1631
|
+
enabled: false
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function loadConfig2() {
|
|
1636
|
+
if (!existsSync5(SETTINGS_PATH2)) {
|
|
1637
|
+
throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
|
|
1638
|
+
}
|
|
1639
|
+
const raw = readFileSync4(SETTINGS_PATH2, "utf-8");
|
|
1640
|
+
let parsed;
|
|
1641
|
+
try {
|
|
1642
|
+
parsed = JSON.parse(raw);
|
|
1643
|
+
} catch {
|
|
1644
|
+
throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
|
|
1645
|
+
}
|
|
1646
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1647
|
+
throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
|
|
1648
|
+
}
|
|
1649
|
+
const config = parsed;
|
|
1650
|
+
const defaults = createDefaultConfig2();
|
|
1651
|
+
return {
|
|
1652
|
+
candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
|
|
1653
|
+
candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
|
|
1654
|
+
site_id: asString2(config["site_id"], defaults.site_id),
|
|
1655
|
+
namespace: asString2(config["namespace"], defaults.namespace),
|
|
1656
|
+
user_id: asString2(config["user_id"], defaults.user_id),
|
|
1657
|
+
user_email: asString2(config["user_email"], defaults.user_email),
|
|
1658
|
+
device_id: asString2(config["device_id"], defaults.device_id),
|
|
1659
|
+
teams: asTeams2(config["teams"], defaults.teams),
|
|
1660
|
+
sync: {
|
|
1661
|
+
enabled: asBool2(config["sync"]?.["enabled"], defaults.sync.enabled),
|
|
1662
|
+
interval_seconds: asNumber2(config["sync"]?.["interval_seconds"], defaults.sync.interval_seconds),
|
|
1663
|
+
batch_size: asNumber2(config["sync"]?.["batch_size"], defaults.sync.batch_size)
|
|
1664
|
+
},
|
|
1665
|
+
search: {
|
|
1666
|
+
default_limit: asNumber2(config["search"]?.["default_limit"], defaults.search.default_limit),
|
|
1667
|
+
local_boost: asNumber2(config["search"]?.["local_boost"], defaults.search.local_boost),
|
|
1668
|
+
scope: asScope2(config["search"]?.["scope"], defaults.search.scope)
|
|
1669
|
+
},
|
|
1670
|
+
scrubbing: {
|
|
1671
|
+
enabled: asBool2(config["scrubbing"]?.["enabled"], defaults.scrubbing.enabled),
|
|
1672
|
+
custom_patterns: asStringArray2(config["scrubbing"]?.["custom_patterns"], defaults.scrubbing.custom_patterns),
|
|
1673
|
+
default_sensitivity: asSensitivity2(config["scrubbing"]?.["default_sensitivity"], defaults.scrubbing.default_sensitivity)
|
|
1674
|
+
},
|
|
1675
|
+
sentinel: {
|
|
1676
|
+
enabled: asBool2(config["sentinel"]?.["enabled"], defaults.sentinel.enabled),
|
|
1677
|
+
mode: asSentinelMode2(config["sentinel"]?.["mode"], defaults.sentinel.mode),
|
|
1678
|
+
provider: asLlmProvider2(config["sentinel"]?.["provider"], defaults.sentinel.provider),
|
|
1679
|
+
model: asString2(config["sentinel"]?.["model"], defaults.sentinel.model),
|
|
1680
|
+
api_key: asString2(config["sentinel"]?.["api_key"], defaults.sentinel.api_key),
|
|
1681
|
+
base_url: asString2(config["sentinel"]?.["base_url"], defaults.sentinel.base_url),
|
|
1682
|
+
skip_patterns: asStringArray2(config["sentinel"]?.["skip_patterns"], defaults.sentinel.skip_patterns),
|
|
1683
|
+
daily_limit: asNumber2(config["sentinel"]?.["daily_limit"], defaults.sentinel.daily_limit),
|
|
1684
|
+
tier: asTier2(config["sentinel"]?.["tier"], defaults.sentinel.tier)
|
|
1685
|
+
},
|
|
1686
|
+
observer: {
|
|
1687
|
+
enabled: asBool2(config["observer"]?.["enabled"], defaults.observer.enabled),
|
|
1688
|
+
mode: asObserverMode2(config["observer"]?.["mode"], defaults.observer.mode),
|
|
1689
|
+
model: asString2(config["observer"]?.["model"], defaults.observer.model)
|
|
1690
|
+
},
|
|
1691
|
+
transcript_analysis: {
|
|
1692
|
+
enabled: asBool2(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function saveConfig(config) {
|
|
1697
|
+
if (!existsSync5(CONFIG_DIR2)) {
|
|
1698
|
+
mkdirSync2(CONFIG_DIR2, { recursive: true });
|
|
1699
|
+
}
|
|
1700
|
+
writeFileSync2(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
|
|
1701
|
+
`, "utf-8");
|
|
1702
|
+
}
|
|
1703
|
+
function configExists2() {
|
|
1704
|
+
return existsSync5(SETTINGS_PATH2);
|
|
1705
|
+
}
|
|
1706
|
+
function asString2(value, fallback) {
|
|
1707
|
+
return typeof value === "string" ? value : fallback;
|
|
1708
|
+
}
|
|
1709
|
+
function asNumber2(value, fallback) {
|
|
1710
|
+
return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
|
|
1711
|
+
}
|
|
1712
|
+
function asBool2(value, fallback) {
|
|
1713
|
+
return typeof value === "boolean" ? value : fallback;
|
|
1714
|
+
}
|
|
1715
|
+
function asStringArray2(value, fallback) {
|
|
1716
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : fallback;
|
|
1717
|
+
}
|
|
1718
|
+
function asScope2(value, fallback) {
|
|
1719
|
+
if (value === "personal" || value === "team" || value === "all")
|
|
1720
|
+
return value;
|
|
1721
|
+
return fallback;
|
|
1722
|
+
}
|
|
1723
|
+
function asSensitivity2(value, fallback) {
|
|
1724
|
+
if (value === "shared" || value === "personal" || value === "secret")
|
|
1725
|
+
return value;
|
|
1726
|
+
return fallback;
|
|
1727
|
+
}
|
|
1728
|
+
function asSentinelMode2(value, fallback) {
|
|
1729
|
+
if (value === "advisory" || value === "blocking")
|
|
1730
|
+
return value;
|
|
1731
|
+
return fallback;
|
|
1732
|
+
}
|
|
1733
|
+
function asLlmProvider2(value, fallback) {
|
|
1734
|
+
if (value === "openai" || value === "anthropic" || value === "ollama" || value === "custom")
|
|
1735
|
+
return value;
|
|
1736
|
+
return fallback;
|
|
1737
|
+
}
|
|
1738
|
+
function asTier2(value, fallback) {
|
|
1739
|
+
if (value === "free" || value === "vibe" || value === "solo" || value === "pro" || value === "team" || value === "enterprise")
|
|
1740
|
+
return value;
|
|
1741
|
+
return fallback;
|
|
1742
|
+
}
|
|
1743
|
+
function asObserverMode2(value, fallback) {
|
|
1744
|
+
if (value === "per_event" || value === "per_session")
|
|
1745
|
+
return value;
|
|
1746
|
+
return fallback;
|
|
1747
|
+
}
|
|
1748
|
+
function asTeams2(value, fallback) {
|
|
1749
|
+
if (!Array.isArray(value))
|
|
1750
|
+
return fallback;
|
|
1751
|
+
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1469
1754
|
// src/sync/auth.ts
|
|
1470
1755
|
function getApiKey(config) {
|
|
1471
1756
|
const envKey = process.env.ENGRM_TOKEN;
|
|
@@ -1651,6 +1936,44 @@ function extractNarrative(content) {
|
|
|
1651
1936
|
`).trim();
|
|
1652
1937
|
return narrative.length > 0 ? narrative : null;
|
|
1653
1938
|
}
|
|
1939
|
+
async function pullSettings(client, config) {
|
|
1940
|
+
try {
|
|
1941
|
+
const settings = await client.fetchSettings();
|
|
1942
|
+
if (!settings)
|
|
1943
|
+
return false;
|
|
1944
|
+
let changed = false;
|
|
1945
|
+
if (settings.transcript_analysis !== undefined) {
|
|
1946
|
+
const ta = settings.transcript_analysis;
|
|
1947
|
+
if (typeof ta === "object" && ta !== null) {
|
|
1948
|
+
const taObj = ta;
|
|
1949
|
+
if (taObj.enabled !== undefined && taObj.enabled !== config.transcript_analysis.enabled) {
|
|
1950
|
+
config.transcript_analysis.enabled = !!taObj.enabled;
|
|
1951
|
+
changed = true;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (settings.observer !== undefined) {
|
|
1956
|
+
const obs = settings.observer;
|
|
1957
|
+
if (typeof obs === "object" && obs !== null) {
|
|
1958
|
+
const obsObj = obs;
|
|
1959
|
+
if (obsObj.enabled !== undefined && obsObj.enabled !== config.observer.enabled) {
|
|
1960
|
+
config.observer.enabled = !!obsObj.enabled;
|
|
1961
|
+
changed = true;
|
|
1962
|
+
}
|
|
1963
|
+
if (obsObj.model !== undefined && typeof obsObj.model === "string" && obsObj.model !== config.observer.model) {
|
|
1964
|
+
config.observer.model = obsObj.model;
|
|
1965
|
+
changed = true;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (changed) {
|
|
1970
|
+
saveConfig(config);
|
|
1971
|
+
}
|
|
1972
|
+
return changed;
|
|
1973
|
+
} catch {
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1654
1977
|
|
|
1655
1978
|
// src/sync/client.ts
|
|
1656
1979
|
class VectorClient {
|
|
@@ -1705,6 +2028,13 @@ class VectorClient {
|
|
|
1705
2028
|
async sendTelemetry(beacon) {
|
|
1706
2029
|
await this.request("POST", "/v1/mem/telemetry", beacon);
|
|
1707
2030
|
}
|
|
2031
|
+
async fetchSettings() {
|
|
2032
|
+
try {
|
|
2033
|
+
return await this.request("GET", "/v1/mem/user-settings");
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1708
2038
|
async health() {
|
|
1709
2039
|
try {
|
|
1710
2040
|
await this.request("GET", "/health");
|
|
@@ -1776,11 +2106,12 @@ async function main() {
|
|
|
1776
2106
|
try {
|
|
1777
2107
|
if (config.sync.enabled && config.candengo_api_key) {
|
|
1778
2108
|
try {
|
|
1779
|
-
const client = new VectorClient(config
|
|
2109
|
+
const client = new VectorClient(config);
|
|
1780
2110
|
const pullResult = await pullFromVector(db, client, config, 50);
|
|
1781
2111
|
if (pullResult.merged > 0) {
|
|
1782
2112
|
console.error(`Engrm: synced ${pullResult.merged} observation(s) from server`);
|
|
1783
2113
|
}
|
|
2114
|
+
await pullSettings(client, config);
|
|
1784
2115
|
} catch {}
|
|
1785
2116
|
}
|
|
1786
2117
|
const context = buildSessionContext(db, event.cwd, {
|