engrm 0.4.45 → 0.4.46
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 +396 -118
- package/dist/hooks/elicitation-result.js +81 -15
- package/dist/hooks/post-tool-use.js +250 -23
- package/dist/hooks/pre-compact.js +249 -23
- package/dist/hooks/sentinel.js +81 -15
- package/dist/hooks/session-start.js +105 -17
- package/dist/hooks/stop.js +311 -27
- package/dist/hooks/user-prompt-submit.js +81 -15
- package/dist/server.js +193 -34
- package/package.json +1 -1
package/dist/hooks/stop.js
CHANGED
|
@@ -320,11 +320,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
320
320
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
321
321
|
import { join } from "node:path";
|
|
322
322
|
import { createHash } from "node:crypto";
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
function resolveConfigDir() {
|
|
324
|
+
return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
|
|
325
|
+
}
|
|
326
|
+
function resolveSettingsPath() {
|
|
327
|
+
return join(resolveConfigDir(), "settings.json");
|
|
328
|
+
}
|
|
329
|
+
function resolveDbPath() {
|
|
330
|
+
return join(resolveConfigDir(), "engrm.db");
|
|
331
|
+
}
|
|
332
|
+
function resolveAuthBackupPath() {
|
|
333
|
+
return join(resolveConfigDir(), "auth-backup.json");
|
|
334
|
+
}
|
|
326
335
|
function getDbPath() {
|
|
327
|
-
return
|
|
336
|
+
return resolveDbPath();
|
|
328
337
|
}
|
|
329
338
|
function generateDeviceId() {
|
|
330
339
|
const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
@@ -347,7 +356,7 @@ function generateDeviceId() {
|
|
|
347
356
|
return `${host}-${suffix}`;
|
|
348
357
|
}
|
|
349
358
|
function createDefaultConfig() {
|
|
350
|
-
|
|
359
|
+
const merged = {
|
|
351
360
|
candengo_url: "",
|
|
352
361
|
candengo_api_key: "",
|
|
353
362
|
site_id: "",
|
|
@@ -402,24 +411,26 @@ function createDefaultConfig() {
|
|
|
402
411
|
},
|
|
403
412
|
tool_profile: "full"
|
|
404
413
|
};
|
|
414
|
+
return merged;
|
|
405
415
|
}
|
|
406
416
|
function loadConfig() {
|
|
407
|
-
|
|
408
|
-
|
|
417
|
+
const settingsPath = resolveSettingsPath();
|
|
418
|
+
if (!existsSync(settingsPath)) {
|
|
419
|
+
throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
|
|
409
420
|
}
|
|
410
|
-
const raw = readFileSync(
|
|
421
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
411
422
|
let parsed;
|
|
412
423
|
try {
|
|
413
424
|
parsed = JSON.parse(raw);
|
|
414
425
|
} catch {
|
|
415
|
-
throw new Error(`Invalid JSON in ${
|
|
426
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
416
427
|
}
|
|
417
428
|
if (typeof parsed !== "object" || parsed === null) {
|
|
418
|
-
throw new Error(`Config at ${
|
|
429
|
+
throw new Error(`Config at ${settingsPath} is not a JSON object`);
|
|
419
430
|
}
|
|
420
431
|
const config = parsed;
|
|
421
432
|
const defaults = createDefaultConfig();
|
|
422
|
-
|
|
433
|
+
const merged = {
|
|
423
434
|
candengo_url: asString(config["candengo_url"], defaults.candengo_url),
|
|
424
435
|
candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
|
|
425
436
|
site_id: asString(config["site_id"], defaults.site_id),
|
|
@@ -474,16 +485,27 @@ function loadConfig() {
|
|
|
474
485
|
},
|
|
475
486
|
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
476
487
|
};
|
|
488
|
+
if (looksLikePlaceholderAuth(merged)) {
|
|
489
|
+
return restoreAuthBackup(merged) ?? merged;
|
|
490
|
+
}
|
|
491
|
+
return merged;
|
|
477
492
|
}
|
|
478
493
|
function saveConfig(config) {
|
|
479
|
-
|
|
480
|
-
|
|
494
|
+
const configDir = resolveConfigDir();
|
|
495
|
+
const settingsPath = resolveSettingsPath();
|
|
496
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
497
|
+
if (!existsSync(configDir)) {
|
|
498
|
+
mkdirSync(configDir, { recursive: true });
|
|
481
499
|
}
|
|
482
|
-
writeFileSync(
|
|
500
|
+
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
|
|
483
501
|
`, "utf-8");
|
|
502
|
+
if (!looksLikePlaceholderAuth(config)) {
|
|
503
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
|
|
504
|
+
`, "utf-8");
|
|
505
|
+
}
|
|
484
506
|
}
|
|
485
507
|
function configExists() {
|
|
486
|
-
return existsSync(
|
|
508
|
+
return existsSync(resolveSettingsPath());
|
|
487
509
|
}
|
|
488
510
|
function asString(value, fallback) {
|
|
489
511
|
return typeof value === "string" ? value : fallback;
|
|
@@ -537,6 +559,50 @@ function asTeams(value, fallback) {
|
|
|
537
559
|
return fallback;
|
|
538
560
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
539
561
|
}
|
|
562
|
+
function looksLikePlaceholderAuth(config) {
|
|
563
|
+
const apiKey = config.candengo_api_key.trim();
|
|
564
|
+
const siteId = config.site_id.trim();
|
|
565
|
+
const namespace = config.namespace.trim();
|
|
566
|
+
const email = config.user_email.trim().toLowerCase();
|
|
567
|
+
if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
|
|
568
|
+
return true;
|
|
569
|
+
if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
|
|
570
|
+
return true;
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
function extractAuthBackup(config) {
|
|
574
|
+
return {
|
|
575
|
+
candengo_url: config.candengo_url,
|
|
576
|
+
candengo_api_key: config.candengo_api_key,
|
|
577
|
+
site_id: config.site_id,
|
|
578
|
+
namespace: config.namespace,
|
|
579
|
+
user_id: config.user_id,
|
|
580
|
+
user_email: config.user_email,
|
|
581
|
+
teams: config.teams
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function restoreAuthBackup(config) {
|
|
585
|
+
const authBackupPath = resolveAuthBackupPath();
|
|
586
|
+
if (!existsSync(authBackupPath))
|
|
587
|
+
return null;
|
|
588
|
+
try {
|
|
589
|
+
const raw = readFileSync(authBackupPath, "utf-8");
|
|
590
|
+
const parsed = JSON.parse(raw);
|
|
591
|
+
const restored = {
|
|
592
|
+
...config,
|
|
593
|
+
candengo_url: asString(parsed["candengo_url"], config.candengo_url),
|
|
594
|
+
candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
|
|
595
|
+
site_id: asString(parsed["site_id"], config.site_id),
|
|
596
|
+
namespace: asString(parsed["namespace"], config.namespace),
|
|
597
|
+
user_id: asString(parsed["user_id"], config.user_id),
|
|
598
|
+
user_email: asString(parsed["user_email"], config.user_email),
|
|
599
|
+
teams: asTeams(parsed["teams"], config.teams)
|
|
600
|
+
};
|
|
601
|
+
return looksLikePlaceholderAuth(restored) ? null : restored;
|
|
602
|
+
} catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
540
606
|
|
|
541
607
|
// src/storage/migrations.ts
|
|
542
608
|
var MIGRATIONS = [
|
|
@@ -2316,6 +2382,10 @@ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
|
2316
2382
|
|
|
2317
2383
|
// src/sync/auth.ts
|
|
2318
2384
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
2385
|
+
var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
|
|
2386
|
+
var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
|
|
2387
|
+
var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
|
|
2388
|
+
var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
|
|
2319
2389
|
function normalizeBaseUrl(url) {
|
|
2320
2390
|
const trimmed = url.trim();
|
|
2321
2391
|
if (!trimmed)
|
|
@@ -2334,7 +2404,7 @@ function getApiKey(config) {
|
|
|
2334
2404
|
const envKey = process.env.ENGRM_TOKEN;
|
|
2335
2405
|
if (envKey && envKey.startsWith("cvk_"))
|
|
2336
2406
|
return envKey;
|
|
2337
|
-
if (config.candengo_api_key && config.candengo_api_key.length > 0) {
|
|
2407
|
+
if (config.candengo_api_key && config.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config)) {
|
|
2338
2408
|
return config.candengo_api_key;
|
|
2339
2409
|
}
|
|
2340
2410
|
return null;
|
|
@@ -2355,6 +2425,23 @@ ${apiKey}
|
|
|
2355
2425
|
${config.namespace}
|
|
2356
2426
|
${config.site_id}`).digest("hex");
|
|
2357
2427
|
}
|
|
2428
|
+
function looksLikePlaceholderConfig(config) {
|
|
2429
|
+
const apiKey = config.candengo_api_key?.trim() ?? "";
|
|
2430
|
+
const siteId = config.site_id?.trim() ?? "";
|
|
2431
|
+
const namespace = config.namespace?.trim() ?? "";
|
|
2432
|
+
const email = config.user_email?.trim().toLowerCase() ?? "";
|
|
2433
|
+
if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email.endsWith(suffix))) {
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
function clearSyncPushBlock(db) {
|
|
2442
|
+
db.setSyncState("sync_push_blocked_until", "0");
|
|
2443
|
+
db.setSyncState("sync_push_block_reason", "");
|
|
2444
|
+
}
|
|
2358
2445
|
function recoverOutboxAfterAuthChange(db, config) {
|
|
2359
2446
|
const fingerprint = getAuthFingerprint(config);
|
|
2360
2447
|
if (!fingerprint) {
|
|
@@ -2373,6 +2460,7 @@ function recoverOutboxAfterAuthChange(db, config) {
|
|
|
2373
2460
|
const syncingReset = resetSyncingEntries(db);
|
|
2374
2461
|
const staleSyncingReset = 0;
|
|
2375
2462
|
db.setSyncState(key, fingerprint);
|
|
2463
|
+
clearSyncPushBlock(db);
|
|
2376
2464
|
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
2377
2465
|
}
|
|
2378
2466
|
function buildSourceId(config, localId, type = "obs") {
|
|
@@ -2938,10 +3026,16 @@ function buildSummaryVectorDocument(summary, config, project, targetOrObservatio
|
|
|
2938
3026
|
};
|
|
2939
3027
|
}
|
|
2940
3028
|
async function pushOutbox(db, config, batchSize = 50, options = {}) {
|
|
3029
|
+
resetStaleSyncingEntries(db);
|
|
3030
|
+
if (isPushBlocked(db)) {
|
|
3031
|
+
return { pushed: 0, failed: 0, skipped: 0, blocked: true };
|
|
3032
|
+
}
|
|
2941
3033
|
const entries = getPendingEntries(db, batchSize);
|
|
2942
3034
|
let pushed = 0;
|
|
2943
3035
|
let failed = 0;
|
|
2944
3036
|
let skipped = 0;
|
|
3037
|
+
let authFailures = 0;
|
|
3038
|
+
let rateLimitFailures = 0;
|
|
2945
3039
|
const batch = [];
|
|
2946
3040
|
for (const entry of entries) {
|
|
2947
3041
|
if (entry.record_type === "summary") {
|
|
@@ -3076,14 +3170,44 @@ async function pushOutbox(db, config, batchSize = 50, options = {}) {
|
|
|
3076
3170
|
markSynced(db, entryId);
|
|
3077
3171
|
pushed++;
|
|
3078
3172
|
} catch (err) {
|
|
3079
|
-
|
|
3173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3174
|
+
const kind = classifyOutboxFailure(message);
|
|
3175
|
+
if (kind === "auth")
|
|
3176
|
+
authFailures++;
|
|
3177
|
+
if (kind === "rate_limit")
|
|
3178
|
+
rateLimitFailures++;
|
|
3179
|
+
markFailed(db, entryId, message);
|
|
3080
3180
|
failed++;
|
|
3081
3181
|
}
|
|
3082
3182
|
}
|
|
3083
3183
|
}
|
|
3084
3184
|
}
|
|
3185
|
+
updatePushCooldown(db, { pushed, authFailures, rateLimitFailures });
|
|
3085
3186
|
return { pushed, failed, skipped };
|
|
3086
3187
|
}
|
|
3188
|
+
var PUSH_BLOCK_UNTIL_KEY = "sync_push_blocked_until";
|
|
3189
|
+
var PUSH_BLOCK_REASON_KEY = "sync_push_block_reason";
|
|
3190
|
+
function isPushBlocked(db) {
|
|
3191
|
+
const blockedUntil = parseInt(db.getSyncState(PUSH_BLOCK_UNTIL_KEY) ?? "0", 10);
|
|
3192
|
+
return Number.isFinite(blockedUntil) && blockedUntil > Math.floor(Date.now() / 1000);
|
|
3193
|
+
}
|
|
3194
|
+
function updatePushCooldown(db, result) {
|
|
3195
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3196
|
+
if (result.authFailures > 0) {
|
|
3197
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 365 * 24 * 60 * 60));
|
|
3198
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "auth");
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
if (result.rateLimitFailures > 0) {
|
|
3202
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 2 * 60));
|
|
3203
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "rate_limit");
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
if (result.pushed > 0) {
|
|
3207
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, "0");
|
|
3208
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "");
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3087
3211
|
function maybeScrubFleetDocument(doc, target) {
|
|
3088
3212
|
if (!target.isFleet)
|
|
3089
3213
|
return doc;
|
|
@@ -3368,7 +3492,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3368
3492
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3369
3493
|
risk_score: riskScore,
|
|
3370
3494
|
stacks_detected: stacks,
|
|
3371
|
-
client_version: "0.4.
|
|
3495
|
+
client_version: "0.4.46",
|
|
3372
3496
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3373
3497
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3374
3498
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3559,7 +3683,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
|
3559
3683
|
|
|
3560
3684
|
// src/capture/transcript.ts
|
|
3561
3685
|
import { createHash as createHash4 } from "node:crypto";
|
|
3562
|
-
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3686
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4, statSync, readdirSync } from "node:fs";
|
|
3563
3687
|
import { join as join5 } from "node:path";
|
|
3564
3688
|
import { homedir as homedir3 } from "node:os";
|
|
3565
3689
|
|
|
@@ -4156,7 +4280,11 @@ function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
|
|
|
4156
4280
|
if (transcriptPath)
|
|
4157
4281
|
return transcriptPath;
|
|
4158
4282
|
const encodedCwd = cwd.replace(/\//g, "-");
|
|
4159
|
-
|
|
4283
|
+
const directPath = join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
4284
|
+
if (existsSync4(directPath))
|
|
4285
|
+
return directPath;
|
|
4286
|
+
const discovered = findTranscriptPathBySessionId(sessionId);
|
|
4287
|
+
return discovered ?? directPath;
|
|
4160
4288
|
}
|
|
4161
4289
|
function readTranscript(sessionId, cwd, transcriptPath) {
|
|
4162
4290
|
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
@@ -4179,10 +4307,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4179
4307
|
} catch {
|
|
4180
4308
|
continue;
|
|
4181
4309
|
}
|
|
4182
|
-
const role = entry
|
|
4310
|
+
const role = getTranscriptRole(entry);
|
|
4183
4311
|
if (role !== "user" && role !== "assistant")
|
|
4184
4312
|
continue;
|
|
4185
|
-
const content = entry
|
|
4313
|
+
const content = getTranscriptContent(entry);
|
|
4186
4314
|
if (typeof content === "string") {
|
|
4187
4315
|
messages.push({ role, text: content });
|
|
4188
4316
|
continue;
|
|
@@ -4202,6 +4330,66 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4202
4330
|
}
|
|
4203
4331
|
return messages;
|
|
4204
4332
|
}
|
|
4333
|
+
function readTranscriptToolEvents(sessionId, cwd, transcriptPath) {
|
|
4334
|
+
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4335
|
+
if (!existsSync4(path))
|
|
4336
|
+
return [];
|
|
4337
|
+
let raw;
|
|
4338
|
+
try {
|
|
4339
|
+
raw = readFileSync4(path, "utf-8");
|
|
4340
|
+
} catch {
|
|
4341
|
+
return [];
|
|
4342
|
+
}
|
|
4343
|
+
const toolEvents = [];
|
|
4344
|
+
const toolEventIndexes = new Map;
|
|
4345
|
+
for (const line of raw.split(`
|
|
4346
|
+
`)) {
|
|
4347
|
+
if (!line.trim())
|
|
4348
|
+
continue;
|
|
4349
|
+
let entry;
|
|
4350
|
+
try {
|
|
4351
|
+
entry = JSON.parse(line);
|
|
4352
|
+
} catch {
|
|
4353
|
+
continue;
|
|
4354
|
+
}
|
|
4355
|
+
const createdAtEpoch = parseTranscriptTimestamp(entry);
|
|
4356
|
+
const content = getTranscriptContent(entry);
|
|
4357
|
+
if (!Array.isArray(content))
|
|
4358
|
+
continue;
|
|
4359
|
+
for (const block of content) {
|
|
4360
|
+
if (!block || typeof block !== "object")
|
|
4361
|
+
continue;
|
|
4362
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
4363
|
+
const preview = extractToolResultPreview(block.content);
|
|
4364
|
+
const index = toolEventIndexes.get(block.tool_use_id);
|
|
4365
|
+
if (preview && index !== undefined) {
|
|
4366
|
+
toolEvents[index] = {
|
|
4367
|
+
...toolEvents[index],
|
|
4368
|
+
tool_response_preview: preview
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (block.type !== "tool_use")
|
|
4374
|
+
continue;
|
|
4375
|
+
const input = block.input && typeof block.input === "object" ? block.input : {};
|
|
4376
|
+
const toolUseId = typeof block.id === "string" ? block.id : null;
|
|
4377
|
+
const nextEvent = {
|
|
4378
|
+
tool_name: typeof block.name === "string" ? block.name : "Unknown",
|
|
4379
|
+
tool_input_json: JSON.stringify(input),
|
|
4380
|
+
tool_response_preview: null,
|
|
4381
|
+
file_path: extractToolFilePath(input),
|
|
4382
|
+
command: typeof input.command === "string" ? input.command : null,
|
|
4383
|
+
created_at_epoch: createdAtEpoch
|
|
4384
|
+
};
|
|
4385
|
+
toolEvents.push(nextEvent);
|
|
4386
|
+
if (toolUseId) {
|
|
4387
|
+
toolEventIndexes.set(toolUseId, toolEvents.length - 1);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
return toolEvents;
|
|
4392
|
+
}
|
|
4205
4393
|
function resolveHistoryPath(historyPath) {
|
|
4206
4394
|
if (historyPath)
|
|
4207
4395
|
return historyPath;
|
|
@@ -4267,9 +4455,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
|
|
|
4267
4455
|
createdAtEpoch: entry.timestamp
|
|
4268
4456
|
})));
|
|
4269
4457
|
}
|
|
4270
|
-
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
4458
|
+
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath, options = {}) {
|
|
4459
|
+
const embed = options.embed ?? true;
|
|
4271
4460
|
const session = db.getSessionById(sessionId);
|
|
4272
|
-
const
|
|
4461
|
+
const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4462
|
+
const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
|
|
4463
|
+
if (existsSync4(resolvedTranscriptPath)) {
|
|
4464
|
+
try {
|
|
4465
|
+
const stat = statSync(resolvedTranscriptPath);
|
|
4466
|
+
const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
4467
|
+
if (db.getSyncState(syncCursorKey) === cursor) {
|
|
4468
|
+
return { imported: 0, total: 0 };
|
|
4469
|
+
}
|
|
4470
|
+
db.setSyncState(syncCursorKey, cursor);
|
|
4471
|
+
} catch {}
|
|
4472
|
+
}
|
|
4473
|
+
const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
|
|
4273
4474
|
...message,
|
|
4274
4475
|
text: message.text.trim()
|
|
4275
4476
|
})).filter((message) => message.text.length > 0);
|
|
@@ -4331,7 +4532,7 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4331
4532
|
created_at_epoch: createdAtEpoch
|
|
4332
4533
|
});
|
|
4333
4534
|
}
|
|
4334
|
-
if (db.vecAvailable) {
|
|
4535
|
+
if (embed && db.vecAvailable) {
|
|
4335
4536
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
4336
4537
|
if (embedding) {
|
|
4337
4538
|
db.vecChatInsert(row.id, embedding);
|
|
@@ -4341,6 +4542,35 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4341
4542
|
}
|
|
4342
4543
|
return { imported, total: messages.length };
|
|
4343
4544
|
}
|
|
4545
|
+
function syncTranscriptToolEvents(db, config, sessionId, cwd, transcriptPath) {
|
|
4546
|
+
const session = db.getSessionById(sessionId);
|
|
4547
|
+
if (!session)
|
|
4548
|
+
return { imported: 0, total: 0 };
|
|
4549
|
+
if (db.getSessionToolEvents(sessionId, 1).length > 0) {
|
|
4550
|
+
return { imported: 0, total: 0 };
|
|
4551
|
+
}
|
|
4552
|
+
const toolEvents = readTranscriptToolEvents(sessionId, cwd, transcriptPath);
|
|
4553
|
+
if (toolEvents.length === 0)
|
|
4554
|
+
return { imported: 0, total: 0 };
|
|
4555
|
+
let imported = 0;
|
|
4556
|
+
for (const event of toolEvents) {
|
|
4557
|
+
db.insertToolEvent({
|
|
4558
|
+
session_id: sessionId,
|
|
4559
|
+
project_id: session.project_id,
|
|
4560
|
+
tool_name: event.tool_name,
|
|
4561
|
+
tool_input_json: event.tool_input_json,
|
|
4562
|
+
tool_response_preview: event.tool_response_preview,
|
|
4563
|
+
file_path: event.file_path,
|
|
4564
|
+
command: event.command,
|
|
4565
|
+
user_id: config.user_id,
|
|
4566
|
+
device_id: config.device_id,
|
|
4567
|
+
agent: "claude-code",
|
|
4568
|
+
created_at_epoch: event.created_at_epoch ?? undefined
|
|
4569
|
+
});
|
|
4570
|
+
imported++;
|
|
4571
|
+
}
|
|
4572
|
+
return { imported, total: toolEvents.length };
|
|
4573
|
+
}
|
|
4344
4574
|
function dedupeHistoryMessages(messages) {
|
|
4345
4575
|
const deduped = [];
|
|
4346
4576
|
for (const message of messages) {
|
|
@@ -4358,6 +4588,59 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
|
4358
4588
|
const digest = createHash4("sha1").update(text).digest("hex").slice(0, 12);
|
|
4359
4589
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4360
4590
|
}
|
|
4591
|
+
function getTranscriptRole(entry) {
|
|
4592
|
+
return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
|
|
4593
|
+
}
|
|
4594
|
+
function getTranscriptContent(entry) {
|
|
4595
|
+
return entry.content ?? entry.message?.content;
|
|
4596
|
+
}
|
|
4597
|
+
function parseTranscriptTimestamp(entry) {
|
|
4598
|
+
const raw = entry.timestamp ?? entry.message?.timestamp;
|
|
4599
|
+
if (typeof raw !== "string")
|
|
4600
|
+
return null;
|
|
4601
|
+
const epoch = Date.parse(raw);
|
|
4602
|
+
return Number.isFinite(epoch) ? Math.floor(epoch / 1000) : null;
|
|
4603
|
+
}
|
|
4604
|
+
function extractToolResultPreview(content) {
|
|
4605
|
+
if (typeof content === "string")
|
|
4606
|
+
return content.slice(0, 4000);
|
|
4607
|
+
if (Array.isArray(content)) {
|
|
4608
|
+
const text = content.map((item) => {
|
|
4609
|
+
if (typeof item === "string")
|
|
4610
|
+
return item;
|
|
4611
|
+
if (item && typeof item === "object" && typeof item.text === "string")
|
|
4612
|
+
return item.text;
|
|
4613
|
+
return "";
|
|
4614
|
+
}).filter(Boolean).join(`
|
|
4615
|
+
`);
|
|
4616
|
+
return text ? text.slice(0, 4000) : null;
|
|
4617
|
+
}
|
|
4618
|
+
return null;
|
|
4619
|
+
}
|
|
4620
|
+
function extractToolFilePath(input) {
|
|
4621
|
+
for (const key of ["file_path", "path", "target_file"]) {
|
|
4622
|
+
if (typeof input[key] === "string")
|
|
4623
|
+
return input[key];
|
|
4624
|
+
}
|
|
4625
|
+
return null;
|
|
4626
|
+
}
|
|
4627
|
+
function findTranscriptPathBySessionId(sessionId) {
|
|
4628
|
+
const projectsDir = join5(homedir3(), ".claude", "projects");
|
|
4629
|
+
if (!existsSync4(projectsDir))
|
|
4630
|
+
return null;
|
|
4631
|
+
try {
|
|
4632
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
4633
|
+
if (!entry.isDirectory())
|
|
4634
|
+
continue;
|
|
4635
|
+
const candidate = join5(projectsDir, entry.name, `${sessionId}.jsonl`);
|
|
4636
|
+
if (existsSync4(candidate))
|
|
4637
|
+
return candidate;
|
|
4638
|
+
}
|
|
4639
|
+
} catch {
|
|
4640
|
+
return null;
|
|
4641
|
+
}
|
|
4642
|
+
return null;
|
|
4643
|
+
}
|
|
4361
4644
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4362
4645
|
const lines = [];
|
|
4363
4646
|
for (const msg of messages) {
|
|
@@ -4931,7 +5214,8 @@ async function main() {
|
|
|
4931
5214
|
try {
|
|
4932
5215
|
if (event.session_id) {
|
|
4933
5216
|
db.completeSession(event.session_id);
|
|
4934
|
-
|
|
5217
|
+
syncTranscriptToolEvents(db, config, event.session_id, event.cwd, event.transcript_path);
|
|
5218
|
+
await syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path, { embed: false });
|
|
4935
5219
|
if (event.last_assistant_message) {
|
|
4936
5220
|
try {
|
|
4937
5221
|
const detected = detectProject(event.cwd);
|
|
@@ -5030,7 +5314,7 @@ async function main() {
|
|
|
5030
5314
|
}
|
|
5031
5315
|
} catch {}
|
|
5032
5316
|
}
|
|
5033
|
-
await pushOnce(db, config, { timeoutMs:
|
|
5317
|
+
await pushOnce(db, config, { timeoutMs: 1500 });
|
|
5034
5318
|
try {
|
|
5035
5319
|
if (event.session_id) {
|
|
5036
5320
|
const metrics = readSessionMetrics(event.session_id);
|
|
@@ -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 = [
|