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.
@@ -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
- var CONFIG_DIR = join(homedir(), ".engrm");
324
- var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
325
- var DB_PATH = join(CONFIG_DIR, "engrm.db");
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 DB_PATH;
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
- return {
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
- if (!existsSync(SETTINGS_PATH)) {
408
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
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(SETTINGS_PATH, "utf-8");
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 ${SETTINGS_PATH}`);
426
+ throw new Error(`Invalid JSON in ${settingsPath}`);
416
427
  }
417
428
  if (typeof parsed !== "object" || parsed === null) {
418
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
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
- return {
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
- if (!existsSync(CONFIG_DIR)) {
480
- mkdirSync(CONFIG_DIR, { recursive: true });
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(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
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(SETTINGS_PATH);
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
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
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.45",
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
- return join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
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.role;
4325
+ const role = getTranscriptRole(entry);
4183
4326
  if (role !== "user" && role !== "assistant")
4184
4327
  continue;
4185
- const content = entry.content;
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 transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
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
- await syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path);
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: 4000 });
5332
+ await pushOnce(db, config, { timeoutMs: 1500 });
5034
5333
  try {
5035
5334
  if (event.session_id) {
5036
5335
  const metrics = readSessionMetrics(event.session_id);