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
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) + `
|
|
501
|
+
`, "utf-8");
|
|
502
|
+
if (!looksLikePlaceholderAuth(config)) {
|
|
503
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
|
|
483
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 = [
|
|
@@ -1224,6 +1290,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1224
1290
|
db.exec("PRAGMA user_version = 17");
|
|
1225
1291
|
}
|
|
1226
1292
|
}
|
|
1293
|
+
function ensureObservationVectorTable(db) {
|
|
1294
|
+
if (!isVecExtensionLoaded(db))
|
|
1295
|
+
return;
|
|
1296
|
+
db.exec(`
|
|
1297
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
|
|
1298
|
+
observation_id INTEGER PRIMARY KEY,
|
|
1299
|
+
embedding FLOAT[384]
|
|
1300
|
+
);
|
|
1301
|
+
`);
|
|
1302
|
+
const current = getSchemaVersion(db);
|
|
1303
|
+
if (current < 4) {
|
|
1304
|
+
db.exec("PRAGMA user_version = 4");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1227
1307
|
function ensureChatVectorTable(db) {
|
|
1228
1308
|
if (!isVecExtensionLoaded(db))
|
|
1229
1309
|
return;
|
|
@@ -1372,6 +1452,7 @@ class MemDatabase {
|
|
|
1372
1452
|
ensureObservationTypes(this.db);
|
|
1373
1453
|
ensureSessionSummaryColumns(this.db);
|
|
1374
1454
|
ensureChatMessageColumns(this.db);
|
|
1455
|
+
ensureObservationVectorTable(this.db);
|
|
1375
1456
|
ensureChatVectorTable(this.db);
|
|
1376
1457
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1377
1458
|
}
|
|
@@ -2316,6 +2397,10 @@ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
|
2316
2397
|
|
|
2317
2398
|
// src/sync/auth.ts
|
|
2318
2399
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
2400
|
+
var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
|
|
2401
|
+
var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
|
|
2402
|
+
var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
|
|
2403
|
+
var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
|
|
2319
2404
|
function normalizeBaseUrl(url) {
|
|
2320
2405
|
const trimmed = url.trim();
|
|
2321
2406
|
if (!trimmed)
|
|
@@ -2334,7 +2419,7 @@ function getApiKey(config) {
|
|
|
2334
2419
|
const envKey = process.env.ENGRM_TOKEN;
|
|
2335
2420
|
if (envKey && envKey.startsWith("cvk_"))
|
|
2336
2421
|
return envKey;
|
|
2337
|
-
if (config.candengo_api_key && config.candengo_api_key.length > 0) {
|
|
2422
|
+
if (config.candengo_api_key && config.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config)) {
|
|
2338
2423
|
return config.candengo_api_key;
|
|
2339
2424
|
}
|
|
2340
2425
|
return null;
|
|
@@ -2355,6 +2440,23 @@ ${apiKey}
|
|
|
2355
2440
|
${config.namespace}
|
|
2356
2441
|
${config.site_id}`).digest("hex");
|
|
2357
2442
|
}
|
|
2443
|
+
function looksLikePlaceholderConfig(config) {
|
|
2444
|
+
const apiKey = config.candengo_api_key?.trim() ?? "";
|
|
2445
|
+
const siteId = config.site_id?.trim() ?? "";
|
|
2446
|
+
const namespace = config.namespace?.trim() ?? "";
|
|
2447
|
+
const email = config.user_email?.trim().toLowerCase() ?? "";
|
|
2448
|
+
if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
|
|
2449
|
+
return true;
|
|
2450
|
+
}
|
|
2451
|
+
if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email.endsWith(suffix))) {
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
function clearSyncPushBlock(db) {
|
|
2457
|
+
db.setSyncState("sync_push_blocked_until", "0");
|
|
2458
|
+
db.setSyncState("sync_push_block_reason", "");
|
|
2459
|
+
}
|
|
2358
2460
|
function recoverOutboxAfterAuthChange(db, config) {
|
|
2359
2461
|
const fingerprint = getAuthFingerprint(config);
|
|
2360
2462
|
if (!fingerprint) {
|
|
@@ -2373,6 +2475,7 @@ function recoverOutboxAfterAuthChange(db, config) {
|
|
|
2373
2475
|
const syncingReset = resetSyncingEntries(db);
|
|
2374
2476
|
const staleSyncingReset = 0;
|
|
2375
2477
|
db.setSyncState(key, fingerprint);
|
|
2478
|
+
clearSyncPushBlock(db);
|
|
2376
2479
|
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
2377
2480
|
}
|
|
2378
2481
|
function buildSourceId(config, localId, type = "obs") {
|
|
@@ -2938,10 +3041,16 @@ function buildSummaryVectorDocument(summary, config, project, targetOrObservatio
|
|
|
2938
3041
|
};
|
|
2939
3042
|
}
|
|
2940
3043
|
async function pushOutbox(db, config, batchSize = 50, options = {}) {
|
|
3044
|
+
resetStaleSyncingEntries(db);
|
|
3045
|
+
if (isPushBlocked(db)) {
|
|
3046
|
+
return { pushed: 0, failed: 0, skipped: 0, blocked: true };
|
|
3047
|
+
}
|
|
2941
3048
|
const entries = getPendingEntries(db, batchSize);
|
|
2942
3049
|
let pushed = 0;
|
|
2943
3050
|
let failed = 0;
|
|
2944
3051
|
let skipped = 0;
|
|
3052
|
+
let authFailures = 0;
|
|
3053
|
+
let rateLimitFailures = 0;
|
|
2945
3054
|
const batch = [];
|
|
2946
3055
|
for (const entry of entries) {
|
|
2947
3056
|
if (entry.record_type === "summary") {
|
|
@@ -3076,14 +3185,44 @@ async function pushOutbox(db, config, batchSize = 50, options = {}) {
|
|
|
3076
3185
|
markSynced(db, entryId);
|
|
3077
3186
|
pushed++;
|
|
3078
3187
|
} catch (err) {
|
|
3079
|
-
|
|
3188
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3189
|
+
const kind = classifyOutboxFailure(message);
|
|
3190
|
+
if (kind === "auth")
|
|
3191
|
+
authFailures++;
|
|
3192
|
+
if (kind === "rate_limit")
|
|
3193
|
+
rateLimitFailures++;
|
|
3194
|
+
markFailed(db, entryId, message);
|
|
3080
3195
|
failed++;
|
|
3081
3196
|
}
|
|
3082
3197
|
}
|
|
3083
3198
|
}
|
|
3084
3199
|
}
|
|
3200
|
+
updatePushCooldown(db, { pushed, authFailures, rateLimitFailures });
|
|
3085
3201
|
return { pushed, failed, skipped };
|
|
3086
3202
|
}
|
|
3203
|
+
var PUSH_BLOCK_UNTIL_KEY = "sync_push_blocked_until";
|
|
3204
|
+
var PUSH_BLOCK_REASON_KEY = "sync_push_block_reason";
|
|
3205
|
+
function isPushBlocked(db) {
|
|
3206
|
+
const blockedUntil = parseInt(db.getSyncState(PUSH_BLOCK_UNTIL_KEY) ?? "0", 10);
|
|
3207
|
+
return Number.isFinite(blockedUntil) && blockedUntil > Math.floor(Date.now() / 1000);
|
|
3208
|
+
}
|
|
3209
|
+
function updatePushCooldown(db, result) {
|
|
3210
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3211
|
+
if (result.authFailures > 0) {
|
|
3212
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 365 * 24 * 60 * 60));
|
|
3213
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "auth");
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
if (result.rateLimitFailures > 0) {
|
|
3217
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 2 * 60));
|
|
3218
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "rate_limit");
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
if (result.pushed > 0) {
|
|
3222
|
+
db.setSyncState(PUSH_BLOCK_UNTIL_KEY, "0");
|
|
3223
|
+
db.setSyncState(PUSH_BLOCK_REASON_KEY, "");
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3087
3226
|
function maybeScrubFleetDocument(doc, target) {
|
|
3088
3227
|
if (!target.isFleet)
|
|
3089
3228
|
return doc;
|
|
@@ -3368,7 +3507,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3368
3507
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3369
3508
|
risk_score: riskScore,
|
|
3370
3509
|
stacks_detected: stacks,
|
|
3371
|
-
client_version: "0.4.
|
|
3510
|
+
client_version: "0.4.47",
|
|
3372
3511
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3373
3512
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3374
3513
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3559,7 +3698,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
|
3559
3698
|
|
|
3560
3699
|
// src/capture/transcript.ts
|
|
3561
3700
|
import { createHash as createHash4 } from "node:crypto";
|
|
3562
|
-
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3701
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4, statSync, readdirSync } from "node:fs";
|
|
3563
3702
|
import { join as join5 } from "node:path";
|
|
3564
3703
|
import { homedir as homedir3 } from "node:os";
|
|
3565
3704
|
|
|
@@ -4156,7 +4295,11 @@ function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
|
|
|
4156
4295
|
if (transcriptPath)
|
|
4157
4296
|
return transcriptPath;
|
|
4158
4297
|
const encodedCwd = cwd.replace(/\//g, "-");
|
|
4159
|
-
|
|
4298
|
+
const directPath = join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
4299
|
+
if (existsSync4(directPath))
|
|
4300
|
+
return directPath;
|
|
4301
|
+
const discovered = findTranscriptPathBySessionId(sessionId);
|
|
4302
|
+
return discovered ?? directPath;
|
|
4160
4303
|
}
|
|
4161
4304
|
function readTranscript(sessionId, cwd, transcriptPath) {
|
|
4162
4305
|
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
@@ -4179,10 +4322,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4179
4322
|
} catch {
|
|
4180
4323
|
continue;
|
|
4181
4324
|
}
|
|
4182
|
-
const role = entry
|
|
4325
|
+
const role = getTranscriptRole(entry);
|
|
4183
4326
|
if (role !== "user" && role !== "assistant")
|
|
4184
4327
|
continue;
|
|
4185
|
-
const content = entry
|
|
4328
|
+
const content = getTranscriptContent(entry);
|
|
4186
4329
|
if (typeof content === "string") {
|
|
4187
4330
|
messages.push({ role, text: content });
|
|
4188
4331
|
continue;
|
|
@@ -4202,6 +4345,66 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4202
4345
|
}
|
|
4203
4346
|
return messages;
|
|
4204
4347
|
}
|
|
4348
|
+
function readTranscriptToolEvents(sessionId, cwd, transcriptPath) {
|
|
4349
|
+
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4350
|
+
if (!existsSync4(path))
|
|
4351
|
+
return [];
|
|
4352
|
+
let raw;
|
|
4353
|
+
try {
|
|
4354
|
+
raw = readFileSync4(path, "utf-8");
|
|
4355
|
+
} catch {
|
|
4356
|
+
return [];
|
|
4357
|
+
}
|
|
4358
|
+
const toolEvents = [];
|
|
4359
|
+
const toolEventIndexes = new Map;
|
|
4360
|
+
for (const line of raw.split(`
|
|
4361
|
+
`)) {
|
|
4362
|
+
if (!line.trim())
|
|
4363
|
+
continue;
|
|
4364
|
+
let entry;
|
|
4365
|
+
try {
|
|
4366
|
+
entry = JSON.parse(line);
|
|
4367
|
+
} catch {
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4370
|
+
const createdAtEpoch = parseTranscriptTimestamp(entry);
|
|
4371
|
+
const content = getTranscriptContent(entry);
|
|
4372
|
+
if (!Array.isArray(content))
|
|
4373
|
+
continue;
|
|
4374
|
+
for (const block of content) {
|
|
4375
|
+
if (!block || typeof block !== "object")
|
|
4376
|
+
continue;
|
|
4377
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
4378
|
+
const preview = extractToolResultPreview(block.content);
|
|
4379
|
+
const index = toolEventIndexes.get(block.tool_use_id);
|
|
4380
|
+
if (preview && index !== undefined) {
|
|
4381
|
+
toolEvents[index] = {
|
|
4382
|
+
...toolEvents[index],
|
|
4383
|
+
tool_response_preview: preview
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
continue;
|
|
4387
|
+
}
|
|
4388
|
+
if (block.type !== "tool_use")
|
|
4389
|
+
continue;
|
|
4390
|
+
const input = block.input && typeof block.input === "object" ? block.input : {};
|
|
4391
|
+
const toolUseId = typeof block.id === "string" ? block.id : null;
|
|
4392
|
+
const nextEvent = {
|
|
4393
|
+
tool_name: typeof block.name === "string" ? block.name : "Unknown",
|
|
4394
|
+
tool_input_json: JSON.stringify(input),
|
|
4395
|
+
tool_response_preview: null,
|
|
4396
|
+
file_path: extractToolFilePath(input),
|
|
4397
|
+
command: typeof input.command === "string" ? input.command : null,
|
|
4398
|
+
created_at_epoch: createdAtEpoch
|
|
4399
|
+
};
|
|
4400
|
+
toolEvents.push(nextEvent);
|
|
4401
|
+
if (toolUseId) {
|
|
4402
|
+
toolEventIndexes.set(toolUseId, toolEvents.length - 1);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
return toolEvents;
|
|
4407
|
+
}
|
|
4205
4408
|
function resolveHistoryPath(historyPath) {
|
|
4206
4409
|
if (historyPath)
|
|
4207
4410
|
return historyPath;
|
|
@@ -4267,9 +4470,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
|
|
|
4267
4470
|
createdAtEpoch: entry.timestamp
|
|
4268
4471
|
})));
|
|
4269
4472
|
}
|
|
4270
|
-
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
4473
|
+
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath, options = {}) {
|
|
4474
|
+
const embed = options.embed ?? true;
|
|
4271
4475
|
const session = db.getSessionById(sessionId);
|
|
4272
|
-
const
|
|
4476
|
+
const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4477
|
+
const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
|
|
4478
|
+
if (existsSync4(resolvedTranscriptPath)) {
|
|
4479
|
+
try {
|
|
4480
|
+
const stat = statSync(resolvedTranscriptPath);
|
|
4481
|
+
const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
4482
|
+
if (db.getSyncState(syncCursorKey) === cursor) {
|
|
4483
|
+
return { imported: 0, total: 0 };
|
|
4484
|
+
}
|
|
4485
|
+
db.setSyncState(syncCursorKey, cursor);
|
|
4486
|
+
} catch {}
|
|
4487
|
+
}
|
|
4488
|
+
const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
|
|
4273
4489
|
...message,
|
|
4274
4490
|
text: message.text.trim()
|
|
4275
4491
|
})).filter((message) => message.text.length > 0);
|
|
@@ -4331,7 +4547,7 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4331
4547
|
created_at_epoch: createdAtEpoch
|
|
4332
4548
|
});
|
|
4333
4549
|
}
|
|
4334
|
-
if (db.vecAvailable) {
|
|
4550
|
+
if (embed && db.vecAvailable) {
|
|
4335
4551
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
4336
4552
|
if (embedding) {
|
|
4337
4553
|
db.vecChatInsert(row.id, embedding);
|
|
@@ -4341,6 +4557,35 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4341
4557
|
}
|
|
4342
4558
|
return { imported, total: messages.length };
|
|
4343
4559
|
}
|
|
4560
|
+
function syncTranscriptToolEvents(db, config, sessionId, cwd, transcriptPath) {
|
|
4561
|
+
const session = db.getSessionById(sessionId);
|
|
4562
|
+
if (!session)
|
|
4563
|
+
return { imported: 0, total: 0 };
|
|
4564
|
+
if (db.getSessionToolEvents(sessionId, 1).length > 0) {
|
|
4565
|
+
return { imported: 0, total: 0 };
|
|
4566
|
+
}
|
|
4567
|
+
const toolEvents = readTranscriptToolEvents(sessionId, cwd, transcriptPath);
|
|
4568
|
+
if (toolEvents.length === 0)
|
|
4569
|
+
return { imported: 0, total: 0 };
|
|
4570
|
+
let imported = 0;
|
|
4571
|
+
for (const event of toolEvents) {
|
|
4572
|
+
db.insertToolEvent({
|
|
4573
|
+
session_id: sessionId,
|
|
4574
|
+
project_id: session.project_id,
|
|
4575
|
+
tool_name: event.tool_name,
|
|
4576
|
+
tool_input_json: event.tool_input_json,
|
|
4577
|
+
tool_response_preview: event.tool_response_preview,
|
|
4578
|
+
file_path: event.file_path,
|
|
4579
|
+
command: event.command,
|
|
4580
|
+
user_id: config.user_id,
|
|
4581
|
+
device_id: config.device_id,
|
|
4582
|
+
agent: "claude-code",
|
|
4583
|
+
created_at_epoch: event.created_at_epoch ?? undefined
|
|
4584
|
+
});
|
|
4585
|
+
imported++;
|
|
4586
|
+
}
|
|
4587
|
+
return { imported, total: toolEvents.length };
|
|
4588
|
+
}
|
|
4344
4589
|
function dedupeHistoryMessages(messages) {
|
|
4345
4590
|
const deduped = [];
|
|
4346
4591
|
for (const message of messages) {
|
|
@@ -4358,6 +4603,59 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
|
4358
4603
|
const digest = createHash4("sha1").update(text).digest("hex").slice(0, 12);
|
|
4359
4604
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4360
4605
|
}
|
|
4606
|
+
function getTranscriptRole(entry) {
|
|
4607
|
+
return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
|
|
4608
|
+
}
|
|
4609
|
+
function getTranscriptContent(entry) {
|
|
4610
|
+
return entry.content ?? entry.message?.content;
|
|
4611
|
+
}
|
|
4612
|
+
function parseTranscriptTimestamp(entry) {
|
|
4613
|
+
const raw = entry.timestamp ?? entry.message?.timestamp;
|
|
4614
|
+
if (typeof raw !== "string")
|
|
4615
|
+
return null;
|
|
4616
|
+
const epoch = Date.parse(raw);
|
|
4617
|
+
return Number.isFinite(epoch) ? Math.floor(epoch / 1000) : null;
|
|
4618
|
+
}
|
|
4619
|
+
function extractToolResultPreview(content) {
|
|
4620
|
+
if (typeof content === "string")
|
|
4621
|
+
return content.slice(0, 4000);
|
|
4622
|
+
if (Array.isArray(content)) {
|
|
4623
|
+
const text = content.map((item) => {
|
|
4624
|
+
if (typeof item === "string")
|
|
4625
|
+
return item;
|
|
4626
|
+
if (item && typeof item === "object" && typeof item.text === "string")
|
|
4627
|
+
return item.text;
|
|
4628
|
+
return "";
|
|
4629
|
+
}).filter(Boolean).join(`
|
|
4630
|
+
`);
|
|
4631
|
+
return text ? text.slice(0, 4000) : null;
|
|
4632
|
+
}
|
|
4633
|
+
return null;
|
|
4634
|
+
}
|
|
4635
|
+
function extractToolFilePath(input) {
|
|
4636
|
+
for (const key of ["file_path", "path", "target_file"]) {
|
|
4637
|
+
if (typeof input[key] === "string")
|
|
4638
|
+
return input[key];
|
|
4639
|
+
}
|
|
4640
|
+
return null;
|
|
4641
|
+
}
|
|
4642
|
+
function findTranscriptPathBySessionId(sessionId) {
|
|
4643
|
+
const projectsDir = join5(homedir3(), ".claude", "projects");
|
|
4644
|
+
if (!existsSync4(projectsDir))
|
|
4645
|
+
return null;
|
|
4646
|
+
try {
|
|
4647
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
4648
|
+
if (!entry.isDirectory())
|
|
4649
|
+
continue;
|
|
4650
|
+
const candidate = join5(projectsDir, entry.name, `${sessionId}.jsonl`);
|
|
4651
|
+
if (existsSync4(candidate))
|
|
4652
|
+
return candidate;
|
|
4653
|
+
}
|
|
4654
|
+
} catch {
|
|
4655
|
+
return null;
|
|
4656
|
+
}
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4361
4659
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4362
4660
|
const lines = [];
|
|
4363
4661
|
for (const msg of messages) {
|
|
@@ -4931,7 +5229,8 @@ async function main() {
|
|
|
4931
5229
|
try {
|
|
4932
5230
|
if (event.session_id) {
|
|
4933
5231
|
db.completeSession(event.session_id);
|
|
4934
|
-
|
|
5232
|
+
syncTranscriptToolEvents(db, config, event.session_id, event.cwd, event.transcript_path);
|
|
5233
|
+
await syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path, { embed: false });
|
|
4935
5234
|
if (event.last_assistant_message) {
|
|
4936
5235
|
try {
|
|
4937
5236
|
const detected = detectProject(event.cwd);
|
|
@@ -5030,7 +5329,7 @@ async function main() {
|
|
|
5030
5329
|
}
|
|
5031
5330
|
} catch {}
|
|
5032
5331
|
}
|
|
5033
|
-
await pushOnce(db, config, { timeoutMs:
|
|
5332
|
+
await pushOnce(db, config, { timeoutMs: 1500 });
|
|
5034
5333
|
try {
|
|
5035
5334
|
if (event.session_id) {
|
|
5036
5335
|
const metrics = readSessionMetrics(event.session_id);
|