engrm 0.4.45 → 0.4.47
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 +411 -118
- package/dist/hooks/elicitation-result.js +96 -15
- package/dist/hooks/post-tool-use.js +265 -23
- package/dist/hooks/pre-compact.js +264 -23
- package/dist/hooks/sentinel.js +96 -15
- package/dist/hooks/session-start.js +120 -17
- package/dist/hooks/stop.js +326 -27
- package/dist/hooks/user-prompt-submit.js +96 -15
- package/dist/server.js +208 -34
- package/package.json +1 -1
|
@@ -153,11 +153,20 @@ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, wr
|
|
|
153
153
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
154
154
|
import { join as join2 } from "node:path";
|
|
155
155
|
import { createHash } from "node:crypto";
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
function resolveConfigDir() {
|
|
157
|
+
return process.env["ENGRM_CONFIG_DIR"]?.trim() || join2(homedir(), ".engrm");
|
|
158
|
+
}
|
|
159
|
+
function resolveSettingsPath() {
|
|
160
|
+
return join2(resolveConfigDir(), "settings.json");
|
|
161
|
+
}
|
|
162
|
+
function resolveDbPath() {
|
|
163
|
+
return join2(resolveConfigDir(), "engrm.db");
|
|
164
|
+
}
|
|
165
|
+
function resolveAuthBackupPath() {
|
|
166
|
+
return join2(resolveConfigDir(), "auth-backup.json");
|
|
167
|
+
}
|
|
159
168
|
function getDbPath() {
|
|
160
|
-
return
|
|
169
|
+
return resolveDbPath();
|
|
161
170
|
}
|
|
162
171
|
function generateDeviceId() {
|
|
163
172
|
const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
@@ -180,7 +189,7 @@ function generateDeviceId() {
|
|
|
180
189
|
return `${host}-${suffix}`;
|
|
181
190
|
}
|
|
182
191
|
function createDefaultConfig() {
|
|
183
|
-
|
|
192
|
+
const merged = {
|
|
184
193
|
candengo_url: "",
|
|
185
194
|
candengo_api_key: "",
|
|
186
195
|
site_id: "",
|
|
@@ -235,24 +244,26 @@ function createDefaultConfig() {
|
|
|
235
244
|
},
|
|
236
245
|
tool_profile: "full"
|
|
237
246
|
};
|
|
247
|
+
return merged;
|
|
238
248
|
}
|
|
239
249
|
function loadConfig() {
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
const settingsPath = resolveSettingsPath();
|
|
251
|
+
if (!existsSync2(settingsPath)) {
|
|
252
|
+
throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
|
|
242
253
|
}
|
|
243
|
-
const raw = readFileSync2(
|
|
254
|
+
const raw = readFileSync2(settingsPath, "utf-8");
|
|
244
255
|
let parsed;
|
|
245
256
|
try {
|
|
246
257
|
parsed = JSON.parse(raw);
|
|
247
258
|
} catch {
|
|
248
|
-
throw new Error(`Invalid JSON in ${
|
|
259
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
249
260
|
}
|
|
250
261
|
if (typeof parsed !== "object" || parsed === null) {
|
|
251
|
-
throw new Error(`Config at ${
|
|
262
|
+
throw new Error(`Config at ${settingsPath} is not a JSON object`);
|
|
252
263
|
}
|
|
253
264
|
const config = parsed;
|
|
254
265
|
const defaults = createDefaultConfig();
|
|
255
|
-
|
|
266
|
+
const merged = {
|
|
256
267
|
candengo_url: asString(config["candengo_url"], defaults.candengo_url),
|
|
257
268
|
candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
|
|
258
269
|
site_id: asString(config["site_id"], defaults.site_id),
|
|
@@ -307,16 +318,27 @@ function loadConfig() {
|
|
|
307
318
|
},
|
|
308
319
|
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
309
320
|
};
|
|
321
|
+
if (looksLikePlaceholderAuth(merged)) {
|
|
322
|
+
return restoreAuthBackup(merged) ?? merged;
|
|
323
|
+
}
|
|
324
|
+
return merged;
|
|
310
325
|
}
|
|
311
326
|
function saveConfig(config) {
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
const configDir = resolveConfigDir();
|
|
328
|
+
const settingsPath = resolveSettingsPath();
|
|
329
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
330
|
+
if (!existsSync2(configDir)) {
|
|
331
|
+
mkdirSync(configDir, { recursive: true });
|
|
314
332
|
}
|
|
315
|
-
writeFileSync(
|
|
333
|
+
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
|
|
334
|
+
`, "utf-8");
|
|
335
|
+
if (!looksLikePlaceholderAuth(config)) {
|
|
336
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
|
|
316
337
|
`, "utf-8");
|
|
338
|
+
}
|
|
317
339
|
}
|
|
318
340
|
function configExists() {
|
|
319
|
-
return existsSync2(
|
|
341
|
+
return existsSync2(resolveSettingsPath());
|
|
320
342
|
}
|
|
321
343
|
function asString(value, fallback) {
|
|
322
344
|
return typeof value === "string" ? value : fallback;
|
|
@@ -370,6 +392,50 @@ function asTeams(value, fallback) {
|
|
|
370
392
|
return fallback;
|
|
371
393
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
372
394
|
}
|
|
395
|
+
function looksLikePlaceholderAuth(config) {
|
|
396
|
+
const apiKey = config.candengo_api_key.trim();
|
|
397
|
+
const siteId = config.site_id.trim();
|
|
398
|
+
const namespace = config.namespace.trim();
|
|
399
|
+
const email = config.user_email.trim().toLowerCase();
|
|
400
|
+
if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
|
|
401
|
+
return true;
|
|
402
|
+
if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
|
|
403
|
+
return true;
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
function extractAuthBackup(config) {
|
|
407
|
+
return {
|
|
408
|
+
candengo_url: config.candengo_url,
|
|
409
|
+
candengo_api_key: config.candengo_api_key,
|
|
410
|
+
site_id: config.site_id,
|
|
411
|
+
namespace: config.namespace,
|
|
412
|
+
user_id: config.user_id,
|
|
413
|
+
user_email: config.user_email,
|
|
414
|
+
teams: config.teams
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function restoreAuthBackup(config) {
|
|
418
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
419
|
+
if (!existsSync2(authBackupPath))
|
|
420
|
+
return null;
|
|
421
|
+
try {
|
|
422
|
+
const raw = readFileSync2(authBackupPath, "utf-8");
|
|
423
|
+
const parsed = JSON.parse(raw);
|
|
424
|
+
const restored = {
|
|
425
|
+
...config,
|
|
426
|
+
candengo_url: asString(parsed["candengo_url"], config.candengo_url),
|
|
427
|
+
candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
|
|
428
|
+
site_id: asString(parsed["site_id"], config.site_id),
|
|
429
|
+
namespace: asString(parsed["namespace"], config.namespace),
|
|
430
|
+
user_id: asString(parsed["user_id"], config.user_id),
|
|
431
|
+
user_email: asString(parsed["user_email"], config.user_email),
|
|
432
|
+
teams: asTeams(parsed["teams"], config.teams)
|
|
433
|
+
};
|
|
434
|
+
return looksLikePlaceholderAuth(restored) ? null : restored;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
373
439
|
|
|
374
440
|
// src/storage/migrations.ts
|
|
375
441
|
var MIGRATIONS = [
|
|
@@ -1057,6 +1123,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1057
1123
|
db.exec("PRAGMA user_version = 17");
|
|
1058
1124
|
}
|
|
1059
1125
|
}
|
|
1126
|
+
function ensureObservationVectorTable(db) {
|
|
1127
|
+
if (!isVecExtensionLoaded(db))
|
|
1128
|
+
return;
|
|
1129
|
+
db.exec(`
|
|
1130
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1131
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1132
|
+
embedding FLOAT[384]
|
|
1133
|
+
);
|
|
1134
|
+
`);
|
|
1135
|
+
const current = getSchemaVersion(db);
|
|
1136
|
+
if (current < 4) {
|
|
1137
|
+
db.exec("PRAGMA user_version = 4");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1060
1140
|
function ensureChatVectorTable(db) {
|
|
1061
1141
|
if (!isVecExtensionLoaded(db))
|
|
1062
1142
|
return;
|
|
@@ -1285,6 +1365,7 @@ class MemDatabase {
|
|
|
1285
1365
|
ensureObservationTypes(this.db);
|
|
1286
1366
|
ensureSessionSummaryColumns(this.db);
|
|
1287
1367
|
ensureChatMessageColumns(this.db);
|
|
1368
|
+
ensureObservationVectorTable(this.db);
|
|
1288
1369
|
ensureChatVectorTable(this.db);
|
|
1289
1370
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1290
1371
|
}
|
package/dist/server.js
CHANGED
|
@@ -13564,11 +13564,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
13564
13564
|
import { homedir, hostname as hostname3, networkInterfaces } from "node:os";
|
|
13565
13565
|
import { join } from "node:path";
|
|
13566
13566
|
import { createHash } from "node:crypto";
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13567
|
+
function resolveConfigDir() {
|
|
13568
|
+
return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
|
|
13569
|
+
}
|
|
13570
|
+
function resolveSettingsPath() {
|
|
13571
|
+
return join(resolveConfigDir(), "settings.json");
|
|
13572
|
+
}
|
|
13573
|
+
function resolveDbPath() {
|
|
13574
|
+
return join(resolveConfigDir(), "engrm.db");
|
|
13575
|
+
}
|
|
13576
|
+
function resolveAuthBackupPath() {
|
|
13577
|
+
return join(resolveConfigDir(), "auth-backup.json");
|
|
13578
|
+
}
|
|
13570
13579
|
function getDbPath() {
|
|
13571
|
-
return
|
|
13580
|
+
return resolveDbPath();
|
|
13572
13581
|
}
|
|
13573
13582
|
function generateDeviceId() {
|
|
13574
13583
|
const host = hostname3().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
@@ -13591,7 +13600,7 @@ function generateDeviceId() {
|
|
|
13591
13600
|
return `${host}-${suffix}`;
|
|
13592
13601
|
}
|
|
13593
13602
|
function createDefaultConfig() {
|
|
13594
|
-
|
|
13603
|
+
const merged = {
|
|
13595
13604
|
candengo_url: "",
|
|
13596
13605
|
candengo_api_key: "",
|
|
13597
13606
|
site_id: "",
|
|
@@ -13646,24 +13655,26 @@ function createDefaultConfig() {
|
|
|
13646
13655
|
},
|
|
13647
13656
|
tool_profile: "full"
|
|
13648
13657
|
};
|
|
13658
|
+
return merged;
|
|
13649
13659
|
}
|
|
13650
13660
|
function loadConfig() {
|
|
13651
|
-
|
|
13652
|
-
|
|
13661
|
+
const settingsPath = resolveSettingsPath();
|
|
13662
|
+
if (!existsSync(settingsPath)) {
|
|
13663
|
+
throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
|
|
13653
13664
|
}
|
|
13654
|
-
const raw = readFileSync(
|
|
13665
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
13655
13666
|
let parsed;
|
|
13656
13667
|
try {
|
|
13657
13668
|
parsed = JSON.parse(raw);
|
|
13658
13669
|
} catch {
|
|
13659
|
-
throw new Error(`Invalid JSON in ${
|
|
13670
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
13660
13671
|
}
|
|
13661
13672
|
if (typeof parsed !== "object" || parsed === null) {
|
|
13662
|
-
throw new Error(`Config at ${
|
|
13673
|
+
throw new Error(`Config at ${settingsPath} is not a JSON object`);
|
|
13663
13674
|
}
|
|
13664
13675
|
const config2 = parsed;
|
|
13665
13676
|
const defaults = createDefaultConfig();
|
|
13666
|
-
|
|
13677
|
+
const merged = {
|
|
13667
13678
|
candengo_url: asString(config2["candengo_url"], defaults.candengo_url),
|
|
13668
13679
|
candengo_api_key: asString(config2["candengo_api_key"], defaults.candengo_api_key),
|
|
13669
13680
|
site_id: asString(config2["site_id"], defaults.site_id),
|
|
@@ -13718,16 +13729,27 @@ function loadConfig() {
|
|
|
13718
13729
|
},
|
|
13719
13730
|
tool_profile: asToolProfile(config2["tool_profile"], defaults.tool_profile)
|
|
13720
13731
|
};
|
|
13732
|
+
if (looksLikePlaceholderAuth(merged)) {
|
|
13733
|
+
return restoreAuthBackup(merged) ?? merged;
|
|
13734
|
+
}
|
|
13735
|
+
return merged;
|
|
13721
13736
|
}
|
|
13722
13737
|
function saveConfig(config2) {
|
|
13723
|
-
|
|
13724
|
-
|
|
13738
|
+
const configDir = resolveConfigDir();
|
|
13739
|
+
const settingsPath = resolveSettingsPath();
|
|
13740
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
13741
|
+
if (!existsSync(configDir)) {
|
|
13742
|
+
mkdirSync(configDir, { recursive: true });
|
|
13725
13743
|
}
|
|
13726
|
-
writeFileSync(
|
|
13744
|
+
writeFileSync(settingsPath, JSON.stringify(config2, null, 2) + `
|
|
13727
13745
|
`, "utf-8");
|
|
13746
|
+
if (!looksLikePlaceholderAuth(config2)) {
|
|
13747
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config2), null, 2) + `
|
|
13748
|
+
`, "utf-8");
|
|
13749
|
+
}
|
|
13728
13750
|
}
|
|
13729
13751
|
function configExists() {
|
|
13730
|
-
return existsSync(
|
|
13752
|
+
return existsSync(resolveSettingsPath());
|
|
13731
13753
|
}
|
|
13732
13754
|
function asString(value, fallback) {
|
|
13733
13755
|
return typeof value === "string" ? value : fallback;
|
|
@@ -13781,6 +13803,50 @@ function asTeams(value, fallback) {
|
|
|
13781
13803
|
return fallback;
|
|
13782
13804
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
13783
13805
|
}
|
|
13806
|
+
function looksLikePlaceholderAuth(config2) {
|
|
13807
|
+
const apiKey = config2.candengo_api_key.trim();
|
|
13808
|
+
const siteId = config2.site_id.trim();
|
|
13809
|
+
const namespace = config2.namespace.trim();
|
|
13810
|
+
const email3 = config2.user_email.trim().toLowerCase();
|
|
13811
|
+
if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
|
|
13812
|
+
return true;
|
|
13813
|
+
if (siteId === "site-1" && namespace === "org-ns" && email3.endsWith("@example.com"))
|
|
13814
|
+
return true;
|
|
13815
|
+
return false;
|
|
13816
|
+
}
|
|
13817
|
+
function extractAuthBackup(config2) {
|
|
13818
|
+
return {
|
|
13819
|
+
candengo_url: config2.candengo_url,
|
|
13820
|
+
candengo_api_key: config2.candengo_api_key,
|
|
13821
|
+
site_id: config2.site_id,
|
|
13822
|
+
namespace: config2.namespace,
|
|
13823
|
+
user_id: config2.user_id,
|
|
13824
|
+
user_email: config2.user_email,
|
|
13825
|
+
teams: config2.teams
|
|
13826
|
+
};
|
|
13827
|
+
}
|
|
13828
|
+
function restoreAuthBackup(config2) {
|
|
13829
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
13830
|
+
if (!existsSync(authBackupPath))
|
|
13831
|
+
return null;
|
|
13832
|
+
try {
|
|
13833
|
+
const raw = readFileSync(authBackupPath, "utf-8");
|
|
13834
|
+
const parsed = JSON.parse(raw);
|
|
13835
|
+
const restored = {
|
|
13836
|
+
...config2,
|
|
13837
|
+
candengo_url: asString(parsed["candengo_url"], config2.candengo_url),
|
|
13838
|
+
candengo_api_key: asString(parsed["candengo_api_key"], config2.candengo_api_key),
|
|
13839
|
+
site_id: asString(parsed["site_id"], config2.site_id),
|
|
13840
|
+
namespace: asString(parsed["namespace"], config2.namespace),
|
|
13841
|
+
user_id: asString(parsed["user_id"], config2.user_id),
|
|
13842
|
+
user_email: asString(parsed["user_email"], config2.user_email),
|
|
13843
|
+
teams: asTeams(parsed["teams"], config2.teams)
|
|
13844
|
+
};
|
|
13845
|
+
return looksLikePlaceholderAuth(restored) ? null : restored;
|
|
13846
|
+
} catch {
|
|
13847
|
+
return null;
|
|
13848
|
+
}
|
|
13849
|
+
}
|
|
13784
13850
|
|
|
13785
13851
|
// src/storage/migrations.ts
|
|
13786
13852
|
var MIGRATIONS = [
|
|
@@ -14468,6 +14534,20 @@ function ensureChatMessageColumns(db) {
|
|
|
14468
14534
|
db.exec("PRAGMA user_version = 17");
|
|
14469
14535
|
}
|
|
14470
14536
|
}
|
|
14537
|
+
function ensureObservationVectorTable(db) {
|
|
14538
|
+
if (!isVecExtensionLoaded(db))
|
|
14539
|
+
return;
|
|
14540
|
+
db.exec(`
|
|
14541
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
14542
|
+
observation_id INTEGER PRIMARY KEY,
|
|
14543
|
+
embedding FLOAT[384]
|
|
14544
|
+
);
|
|
14545
|
+
`);
|
|
14546
|
+
const current = getSchemaVersion(db);
|
|
14547
|
+
if (current < 4) {
|
|
14548
|
+
db.exec("PRAGMA user_version = 4");
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14471
14551
|
function ensureChatVectorTable(db) {
|
|
14472
14552
|
if (!isVecExtensionLoaded(db))
|
|
14473
14553
|
return;
|
|
@@ -14696,6 +14776,7 @@ class MemDatabase {
|
|
|
14696
14776
|
ensureObservationTypes(this.db);
|
|
14697
14777
|
ensureSessionSummaryColumns(this.db);
|
|
14698
14778
|
ensureChatMessageColumns(this.db);
|
|
14779
|
+
ensureObservationVectorTable(this.db);
|
|
14699
14780
|
ensureChatVectorTable(this.db);
|
|
14700
14781
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
14701
14782
|
}
|
|
@@ -19600,13 +19681,8 @@ function getCaptureStatus(db, input = {}) {
|
|
|
19600
19681
|
FROM sessions s
|
|
19601
19682
|
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
19602
19683
|
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
19603
|
-
AND
|
|
19604
|
-
|
|
19605
|
-
OR (
|
|
19606
|
-
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
19607
|
-
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
19608
|
-
)
|
|
19609
|
-
)`).get(...params)?.count ?? 0;
|
|
19684
|
+
AND s.tool_calls_count > 0
|
|
19685
|
+
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)`).get(...params)?.count ?? 0;
|
|
19610
19686
|
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
19611
19687
|
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
19612
19688
|
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
@@ -20648,14 +20724,18 @@ function captureRepoScan(input = {}) {
|
|
|
20648
20724
|
|
|
20649
20725
|
// src/capture/transcript.ts
|
|
20650
20726
|
import { createHash as createHash3 } from "node:crypto";
|
|
20651
|
-
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "node:fs";
|
|
20727
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6, statSync, readdirSync } from "node:fs";
|
|
20652
20728
|
import { join as join4 } from "node:path";
|
|
20653
20729
|
import { homedir as homedir3 } from "node:os";
|
|
20654
20730
|
function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
|
|
20655
20731
|
if (transcriptPath)
|
|
20656
20732
|
return transcriptPath;
|
|
20657
20733
|
const encodedCwd = cwd.replace(/\//g, "-");
|
|
20658
|
-
|
|
20734
|
+
const directPath = join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
20735
|
+
if (existsSync6(directPath))
|
|
20736
|
+
return directPath;
|
|
20737
|
+
const discovered = findTranscriptPathBySessionId(sessionId);
|
|
20738
|
+
return discovered ?? directPath;
|
|
20659
20739
|
}
|
|
20660
20740
|
function readTranscript(sessionId, cwd, transcriptPath) {
|
|
20661
20741
|
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
@@ -20678,10 +20758,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
20678
20758
|
} catch {
|
|
20679
20759
|
continue;
|
|
20680
20760
|
}
|
|
20681
|
-
const role = entry
|
|
20761
|
+
const role = getTranscriptRole(entry);
|
|
20682
20762
|
if (role !== "user" && role !== "assistant")
|
|
20683
20763
|
continue;
|
|
20684
|
-
const content = entry
|
|
20764
|
+
const content = getTranscriptContent(entry);
|
|
20685
20765
|
if (typeof content === "string") {
|
|
20686
20766
|
messages.push({ role, text: content });
|
|
20687
20767
|
continue;
|
|
@@ -20766,9 +20846,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
|
|
|
20766
20846
|
createdAtEpoch: entry.timestamp
|
|
20767
20847
|
})));
|
|
20768
20848
|
}
|
|
20769
|
-
async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
20849
|
+
async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath, options = {}) {
|
|
20850
|
+
const embed = options.embed ?? true;
|
|
20770
20851
|
const session = db.getSessionById(sessionId);
|
|
20771
|
-
const
|
|
20852
|
+
const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
20853
|
+
const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
|
|
20854
|
+
if (existsSync6(resolvedTranscriptPath)) {
|
|
20855
|
+
try {
|
|
20856
|
+
const stat = statSync(resolvedTranscriptPath);
|
|
20857
|
+
const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
20858
|
+
if (db.getSyncState(syncCursorKey) === cursor) {
|
|
20859
|
+
return { imported: 0, total: 0 };
|
|
20860
|
+
}
|
|
20861
|
+
db.setSyncState(syncCursorKey, cursor);
|
|
20862
|
+
} catch {}
|
|
20863
|
+
}
|
|
20864
|
+
const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
|
|
20772
20865
|
...message,
|
|
20773
20866
|
text: message.text.trim()
|
|
20774
20867
|
})).filter((message) => message.text.length > 0);
|
|
@@ -20830,7 +20923,7 @@ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
|
20830
20923
|
created_at_epoch: createdAtEpoch
|
|
20831
20924
|
});
|
|
20832
20925
|
}
|
|
20833
|
-
if (db.vecAvailable) {
|
|
20926
|
+
if (embed && db.vecAvailable) {
|
|
20834
20927
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
20835
20928
|
if (embedding) {
|
|
20836
20929
|
db.vecChatInsert(row.id, embedding);
|
|
@@ -20857,6 +20950,29 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
|
20857
20950
|
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
20858
20951
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
20859
20952
|
}
|
|
20953
|
+
function getTranscriptRole(entry) {
|
|
20954
|
+
return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
|
|
20955
|
+
}
|
|
20956
|
+
function getTranscriptContent(entry) {
|
|
20957
|
+
return entry.content ?? entry.message?.content;
|
|
20958
|
+
}
|
|
20959
|
+
function findTranscriptPathBySessionId(sessionId) {
|
|
20960
|
+
const projectsDir = join4(homedir3(), ".claude", "projects");
|
|
20961
|
+
if (!existsSync6(projectsDir))
|
|
20962
|
+
return null;
|
|
20963
|
+
try {
|
|
20964
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
20965
|
+
if (!entry.isDirectory())
|
|
20966
|
+
continue;
|
|
20967
|
+
const candidate = join4(projectsDir, entry.name, `${sessionId}.jsonl`);
|
|
20968
|
+
if (existsSync6(candidate))
|
|
20969
|
+
return candidate;
|
|
20970
|
+
}
|
|
20971
|
+
} catch {
|
|
20972
|
+
return null;
|
|
20973
|
+
}
|
|
20974
|
+
return null;
|
|
20975
|
+
}
|
|
20860
20976
|
|
|
20861
20977
|
// src/tools/repair-recall.ts
|
|
20862
20978
|
async function repairRecall(db, config2, input = {}) {
|
|
@@ -21664,6 +21780,10 @@ function isDue(db, key, interval, now) {
|
|
|
21664
21780
|
// src/sync/auth.ts
|
|
21665
21781
|
import { createHash as createHash4 } from "node:crypto";
|
|
21666
21782
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
21783
|
+
var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
|
|
21784
|
+
var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
|
|
21785
|
+
var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
|
|
21786
|
+
var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
|
|
21667
21787
|
function normalizeBaseUrl(url2) {
|
|
21668
21788
|
const trimmed = url2.trim();
|
|
21669
21789
|
if (!trimmed)
|
|
@@ -21682,7 +21802,7 @@ function getApiKey(config2) {
|
|
|
21682
21802
|
const envKey = process.env.ENGRM_TOKEN;
|
|
21683
21803
|
if (envKey && envKey.startsWith("cvk_"))
|
|
21684
21804
|
return envKey;
|
|
21685
|
-
if (config2.candengo_api_key && config2.candengo_api_key.length > 0) {
|
|
21805
|
+
if (config2.candengo_api_key && config2.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config2)) {
|
|
21686
21806
|
return config2.candengo_api_key;
|
|
21687
21807
|
}
|
|
21688
21808
|
return null;
|
|
@@ -21703,6 +21823,23 @@ ${apiKey}
|
|
|
21703
21823
|
${config2.namespace}
|
|
21704
21824
|
${config2.site_id}`).digest("hex");
|
|
21705
21825
|
}
|
|
21826
|
+
function looksLikePlaceholderConfig(config2) {
|
|
21827
|
+
const apiKey = config2.candengo_api_key?.trim() ?? "";
|
|
21828
|
+
const siteId = config2.site_id?.trim() ?? "";
|
|
21829
|
+
const namespace = config2.namespace?.trim() ?? "";
|
|
21830
|
+
const email3 = config2.user_email?.trim().toLowerCase() ?? "";
|
|
21831
|
+
if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
|
|
21832
|
+
return true;
|
|
21833
|
+
}
|
|
21834
|
+
if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email3.endsWith(suffix))) {
|
|
21835
|
+
return true;
|
|
21836
|
+
}
|
|
21837
|
+
return false;
|
|
21838
|
+
}
|
|
21839
|
+
function clearSyncPushBlock(db) {
|
|
21840
|
+
db.setSyncState("sync_push_blocked_until", "0");
|
|
21841
|
+
db.setSyncState("sync_push_block_reason", "");
|
|
21842
|
+
}
|
|
21706
21843
|
function recoverOutboxAfterAuthChange(db, config2) {
|
|
21707
21844
|
const fingerprint = getAuthFingerprint(config2);
|
|
21708
21845
|
if (!fingerprint) {
|
|
@@ -21721,6 +21858,7 @@ function recoverOutboxAfterAuthChange(db, config2) {
|
|
|
21721
21858
|
const syncingReset = resetSyncingEntries(db);
|
|
21722
21859
|
const staleSyncingReset = 0;
|
|
21723
21860
|
db.setSyncState(key, fingerprint);
|
|
21861
|
+
clearSyncPushBlock(db);
|
|
21724
21862
|
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
21725
21863
|
}
|
|
21726
21864
|
function buildSourceId(config2, localId, type = "obs") {
|
|
@@ -22093,10 +22231,16 @@ function buildSummaryVectorDocument(summary, config2, project, targetOrObservati
|
|
|
22093
22231
|
};
|
|
22094
22232
|
}
|
|
22095
22233
|
async function pushOutbox(db, config2, batchSize = 50, options = {}) {
|
|
22234
|
+
resetStaleSyncingEntries(db);
|
|
22235
|
+
if (isPushBlocked(db)) {
|
|
22236
|
+
return { pushed: 0, failed: 0, skipped: 0, blocked: true };
|
|
22237
|
+
}
|
|
22096
22238
|
const entries = getPendingEntries(db, batchSize);
|
|
22097
22239
|
let pushed = 0;
|
|
22098
22240
|
let failed = 0;
|
|
22099
22241
|
let skipped = 0;
|
|
22242
|
+
let authFailures = 0;
|
|
22243
|
+
let rateLimitFailures = 0;
|
|
22100
22244
|
const batch = [];
|
|
22101
22245
|
for (const entry of entries) {
|
|
22102
22246
|
if (entry.record_type === "summary") {
|
|
@@ -22231,14 +22375,44 @@ async function pushOutbox(db, config2, batchSize = 50, options = {}) {
|
|
|
22231
22375
|
markSynced(db, entryId);
|
|
22232
22376
|
pushed++;
|
|
22233
22377
|
} catch (err) {
|
|
22234
|
-
|
|
22378
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22379
|
+
const kind = classifyOutboxFailure(message);
|
|
22380
|
+
if (kind === "auth")
|
|
22381
|
+
authFailures++;
|
|
22382
|
+
if (kind === "rate_limit")
|
|
22383
|
+
rateLimitFailures++;
|
|
22384
|
+
markFailed(db, entryId, message);
|
|
22235
22385
|
failed++;
|
|
22236
22386
|
}
|
|
22237
22387
|
}
|
|
22238
22388
|
}
|
|
22239
22389
|
}
|
|
22390
|
+
updatePushCooldown(db, { pushed, authFailures, rateLimitFailures });
|
|
22240
22391
|
return { pushed, failed, skipped };
|
|
22241
22392
|
}
|
|
22393
|
+
var PUSH_BLOCK_UNTIL_KEY = "sync_push_blocked_until";
|
|
22394
|
+
var PUSH_BLOCK_REASON_KEY = "sync_push_block_reason";
|
|
22395
|
+
function isPushBlocked(db) {
|
|
22396
|
+
const blockedUntil = parseInt(db.getSyncState(PUSH_BLOCK_UNTIL_KEY) ?? "0", 10);
|
|
22397
|
+
return Number.isFinite(blockedUntil) && blockedUntil > Math.floor(Date.now() / 1000);
|
|
22398
|
+
}
|
|
22399
|
+
function updatePushCooldown(db, result) {
|
|
22400
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22401
|
+
if (result.authFailures > 0) {
|
|
22402
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 365 * 24 * 60 * 60));
|
|
22403
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "auth");
|
|
22404
|
+
return;
|
|
22405
|
+
}
|
|
22406
|
+
if (result.rateLimitFailures > 0) {
|
|
22407
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 2 * 60));
|
|
22408
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "rate_limit");
|
|
22409
|
+
return;
|
|
22410
|
+
}
|
|
22411
|
+
if (result.pushed > 0) {
|
|
22412
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, "0");
|
|
22413
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "");
|
|
22414
|
+
}
|
|
22415
|
+
}
|
|
22242
22416
|
function maybeScrubFleetDocument(doc2, target) {
|
|
22243
22417
|
if (!target.isFleet)
|
|
22244
22418
|
return doc2;
|
|
@@ -22637,7 +22811,7 @@ async function backfillEmbeddings(db, batchSize = 50) {
|
|
|
22637
22811
|
}
|
|
22638
22812
|
|
|
22639
22813
|
// src/packs/recommender.ts
|
|
22640
|
-
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "node:fs";
|
|
22814
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
22641
22815
|
import { join as join5, basename as basename2, dirname as dirname2 } from "node:path";
|
|
22642
22816
|
import { fileURLToPath } from "node:url";
|
|
22643
22817
|
function getPacksDir() {
|
|
@@ -23290,7 +23464,7 @@ function installStdioLivenessGuards() {
|
|
|
23290
23464
|
function buildServer() {
|
|
23291
23465
|
const server = new McpServer({
|
|
23292
23466
|
name: "engrm",
|
|
23293
|
-
version: "0.4.
|
|
23467
|
+
version: "0.4.47"
|
|
23294
23468
|
});
|
|
23295
23469
|
const enabledToolNames = getEnabledToolNames(config2.tool_profile);
|
|
23296
23470
|
const originalTool = server.tool.bind(server);
|
|
@@ -25306,7 +25480,7 @@ async function main() {
|
|
|
25306
25480
|
await server.connect(transport);
|
|
25307
25481
|
}
|
|
25308
25482
|
function shouldStartHttpMode() {
|
|
25309
|
-
return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT)
|
|
25483
|
+
return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT);
|
|
25310
25484
|
}
|
|
25311
25485
|
function resolveHttpPort() {
|
|
25312
25486
|
const raw = process.env.ENGRM_HTTP_PORT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engrm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.47",
|
|
4
4
|
"description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture, live continuity, and Hermes-ready remote MCP support",
|
|
5
5
|
"mcpName": "io.github.dr12hes/engrm",
|
|
6
6
|
"type": "module",
|