engrm 0.4.44 → 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.
@@ -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) + `
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(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 = [
@@ -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
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
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.44",
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
- return join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
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.role;
4310
+ const role = getTranscriptRole(entry);
4183
4311
  if (role !== "user" && role !== "assistant")
4184
4312
  continue;
4185
- const content = entry.content;
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 transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
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
- await syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path);
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: 4000 });
5317
+ await pushOnce(db, config, { timeoutMs: 1500 });
5034
5318
  try {
5035
5319
  if (event.session_id) {
5036
5320
  const metrics = readSessionMetrics(event.session_id);