@raindrop-ai/claude-code 0.0.7 → 0.0.9

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 CHANGED
@@ -654,7 +654,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
654
654
 
655
655
  // src/package-info.ts
656
656
  var PACKAGE_NAME = "@raindrop-ai/claude-code";
657
- var PACKAGE_VERSION = "0.0.7";
657
+ var PACKAGE_VERSION = "0.0.8";
658
658
 
659
659
  // src/shipper.ts
660
660
  var EventShipper2 = class extends EventShipper {
@@ -693,9 +693,47 @@ var TraceShipper2 = class extends TraceShipper {
693
693
 
694
694
  // src/transcript.ts
695
695
  import { existsSync, readFileSync } from "fs";
696
+ function isToolResultContentBlock(value) {
697
+ return Boolean(
698
+ value && typeof value === "object" && "type" in value && value["type"] === "tool_result"
699
+ );
700
+ }
701
+ function isTopLevelUserPrompt(entry) {
702
+ var _a;
703
+ if (entry.type !== "user") return false;
704
+ const content = (_a = entry.message) == null ? void 0 : _a.content;
705
+ if (typeof content === "string") return true;
706
+ if (!Array.isArray(content)) return false;
707
+ return content.some((block) => !isToolResultContentBlock(block));
708
+ }
696
709
  function parseTranscript(transcriptPath) {
697
- var _a, _b, _c, _d, _e;
710
+ var _a, _b, _c, _d, _e, _f, _g, _h;
698
711
  try {
712
+ let finalizePhase2 = function() {
713
+ if (activePhase && (activePhase.text || activePhase.toolCalls.length > 0)) {
714
+ currentPhases.push(activePhase);
715
+ }
716
+ activePhase = void 0;
717
+ }, ensurePhase2 = function() {
718
+ if (!activePhase) {
719
+ activePhase = {
720
+ text: "",
721
+ toolCalls: [],
722
+ inputTokens: 0,
723
+ outputTokens: 0,
724
+ cacheReadTokens: 0,
725
+ hasThinking: false
726
+ };
727
+ }
728
+ return activePhase;
729
+ }, isToolResultUserEntry2 = function(entry) {
730
+ var _a2;
731
+ if (entry.type !== "user") return false;
732
+ const content2 = (_a2 = entry.message) == null ? void 0 : _a2.content;
733
+ if (!Array.isArray(content2)) return false;
734
+ return content2.length > 0 && content2.every((b) => isToolResultContentBlock(b));
735
+ };
736
+ var finalizePhase = finalizePhase2, ensurePhase = ensurePhase2, isToolResultUserEntry = isToolResultUserEntry2;
699
737
  if (!existsSync(transcriptPath)) return void 0;
700
738
  const content = readFileSync(transcriptPath, "utf-8");
701
739
  const lines = content.split("\n").filter((l) => l.trim());
@@ -706,10 +744,15 @@ function parseTranscript(transcriptPath) {
706
744
  totalCacheCreationTokens: 0,
707
745
  turnCount: 0,
708
746
  toolsUsed: [],
709
- hasThinking: false
747
+ hasThinking: false,
748
+ lastTurnTextBlocks: [],
749
+ llmCallPhases: []
710
750
  };
711
751
  const toolNames = /* @__PURE__ */ new Set();
712
752
  let lastUsage;
753
+ let currentTurnTextBlocks = [];
754
+ let currentPhases = [];
755
+ let activePhase;
713
756
  for (const line of lines) {
714
757
  let entry;
715
758
  try {
@@ -732,6 +775,15 @@ function parseTranscript(transcriptPath) {
732
775
  }
733
776
  continue;
734
777
  }
778
+ if (isTopLevelUserPrompt(entry)) {
779
+ currentTurnTextBlocks = [];
780
+ finalizePhase2();
781
+ currentPhases = [];
782
+ }
783
+ if (isToolResultUserEntry2(entry)) {
784
+ finalizePhase2();
785
+ continue;
786
+ }
735
787
  if (entry.type !== "assistant") continue;
736
788
  const msg = entry.message;
737
789
  if (!msg) continue;
@@ -761,23 +813,55 @@ function parseTranscript(transcriptPath) {
761
813
  }
762
814
  lastUsage = u;
763
815
  }
816
+ const phase = ensurePhase2();
817
+ const entryTimestamp = typeof entry["timestamp"] === "string" ? entry["timestamp"] : void 0;
818
+ if (entryTimestamp) {
819
+ if (!phase.startTimestamp) phase.startTimestamp = entryTimestamp;
820
+ phase.endTimestamp = entryTimestamp;
821
+ }
822
+ if (msg.model) phase.model = msg.model;
823
+ if (msg.usage) {
824
+ phase.inputTokens += (_f = msg.usage.input_tokens) != null ? _f : 0;
825
+ phase.outputTokens += (_g = msg.usage.output_tokens) != null ? _g : 0;
826
+ phase.cacheReadTokens += (_h = msg.usage.cache_read_input_tokens) != null ? _h : 0;
827
+ }
764
828
  if (Array.isArray(msg.content)) {
765
829
  for (const block of msg.content) {
766
830
  if (block.type === "tool_use" && block.name) {
767
831
  toolNames.add(block.name);
832
+ phase.toolCalls.push({
833
+ id: block.id,
834
+ name: block.name,
835
+ input: block.input
836
+ });
768
837
  }
769
838
  if (block.type === "thinking") {
770
839
  summary.hasThinking = true;
840
+ phase.hasThinking = true;
841
+ }
842
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
843
+ currentTurnTextBlocks.push(block.text);
844
+ if (phase.text) {
845
+ phase.text += "\n" + block.text;
846
+ } else {
847
+ phase.text = block.text;
848
+ }
771
849
  }
772
850
  }
851
+ summary.lastTurnTextBlocks = [...currentTurnTextBlocks];
773
852
  }
774
853
  }
854
+ finalizePhase2();
775
855
  if (lastUsage) {
776
856
  summary.lastTurnInputTokens = lastUsage.input_tokens;
777
857
  summary.lastTurnOutputTokens = lastUsage.output_tokens;
778
858
  summary.lastTurnCacheReadTokens = lastUsage.cache_read_input_tokens;
779
859
  }
780
860
  summary.toolsUsed = [...toolNames].sort();
861
+ if (summary.lastTurnTextBlocks.length > 0) {
862
+ summary.lastTurnFullOutput = summary.lastTurnTextBlocks.join("\n\n");
863
+ }
864
+ summary.llmCallPhases = currentPhases;
781
865
  return summary;
782
866
  } catch (e) {
783
867
  return void 0;
@@ -926,12 +1010,13 @@ function readTimestamp(key) {
926
1010
  return Number.isFinite(ts) ? ts : void 0;
927
1011
  }
928
1012
  async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
929
- var _a;
930
- const convoId = payload.session_id;
1013
+ var _a, _b;
1014
+ const convoId = (_a = config.convoId) != null ? _a : payload.session_id;
931
1015
  const baseProperties = {
932
1016
  ...config.customProperties,
933
1017
  cwd: payload.cwd,
934
1018
  permission_mode: payload.permission_mode,
1019
+ claude_code_session_id: payload.session_id,
935
1020
  plugin_version: PACKAGE_VERSION,
936
1021
  sdk: PACKAGE_NAME,
937
1022
  sdk_version: PACKAGE_VERSION,
@@ -940,7 +1025,7 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
940
1025
  };
941
1026
  switch (payload.hook_event_name) {
942
1027
  case "SessionStart":
943
- writeState(`model_${payload.session_id}`, (_a = payload.model) != null ? _a : "");
1028
+ writeState(`model_${payload.session_id}`, (_b = payload.model) != null ? _b : "");
944
1029
  tryCaptureAppendSystemPrompt(payload.session_id);
945
1030
  break;
946
1031
  case "UserPromptSubmit":
@@ -956,10 +1041,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
956
1041
  handlePostToolUseFailure(payload, getCurrentEventId(payload.session_id), traceShipper);
957
1042
  break;
958
1043
  case "Stop":
959
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper);
1044
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper);
960
1045
  break;
961
1046
  case "StopFailure":
962
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper, {
1047
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper, {
963
1048
  error: payload.error,
964
1049
  error_details: payload.error_details
965
1050
  });
@@ -977,10 +1062,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
977
1062
  handleInstructionsLoaded(payload);
978
1063
  break;
979
1064
  case "PostCompact":
980
- await handlePostCompact(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1065
+ await handlePostCompact(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
981
1066
  break;
982
1067
  case "SessionEnd":
983
- await handleSessionEnd(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1068
+ await handleSessionEnd(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
984
1069
  deleteState(turnEventKey(payload.session_id));
985
1070
  deleteState(rootSpanKey(payload.session_id));
986
1071
  deleteState(`model_${payload.session_id}`);
@@ -1130,11 +1215,11 @@ function handlePermissionDenied(payload, eventId, traceShipper) {
1130
1215
  }
1131
1216
  });
1132
1217
  }
1133
- async function handlePostCompact(payload, eventId, config, properties, eventShipper) {
1218
+ async function handlePostCompact(payload, eventId, convoId, config, properties, eventShipper) {
1134
1219
  await eventShipper.patch(eventId, {
1135
1220
  isPending: true,
1136
1221
  userId: config.userId,
1137
- convoId: payload.session_id,
1222
+ convoId,
1138
1223
  eventName: config.eventName,
1139
1224
  properties: {
1140
1225
  ...properties,
@@ -1328,8 +1413,18 @@ function readAppendSystemPrompt(sessionId) {
1328
1413
  return void 0;
1329
1414
  }
1330
1415
  }
1416
+ function isoToNanoString(iso, fallback) {
1417
+ if (!iso) return fallback;
1418
+ try {
1419
+ const ms = new Date(iso).getTime();
1420
+ if (!Number.isFinite(ms)) return fallback;
1421
+ return String(ms) + "000000";
1422
+ } catch (e) {
1423
+ return fallback;
1424
+ }
1425
+ }
1331
1426
  function enrichFromTranscript(payload, eventId, traceShipper) {
1332
- var _a, _b;
1427
+ var _a, _b, _c, _d;
1333
1428
  if (!payload.transcript_path) {
1334
1429
  return { summary: void 0, props: {} };
1335
1430
  }
@@ -1337,42 +1432,94 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1337
1432
  const summary = parseTranscript(payload.transcript_path);
1338
1433
  if (!summary) return { summary: void 0, props: {} };
1339
1434
  const props = transcriptToProperties(summary);
1340
- const spanInputTokens = (_a = summary.lastTurnInputTokens) != null ? _a : summary.totalInputTokens;
1341
- const spanOutputTokens = (_b = summary.lastTurnOutputTokens) != null ? _b : summary.totalOutputTokens;
1342
- if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1343
- const parent = getParentContext(payload);
1344
- const now = nowUnixNanoString();
1345
- traceShipper.createSpan({
1346
- name: summary.model,
1347
- eventId,
1348
- parent,
1349
- startTimeUnixNano: now,
1350
- endTimeUnixNano: now,
1351
- attributes: [
1352
- attrString("ai.operationId", "generateText"),
1353
- attrString("gen_ai.response.model", summary.model),
1354
- attrString("gen_ai.system", "anthropic"),
1355
- attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1356
- attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1357
- ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1358
- ]
1359
- });
1435
+ const parent = getParentContext(payload);
1436
+ if (summary.llmCallPhases.length > 0) {
1437
+ let emittedPhaseIndex = 0;
1438
+ for (let i = 0; i < summary.llmCallPhases.length; i++) {
1439
+ const phase = summary.llmCallPhases[i];
1440
+ const outputText = phase.text.trim();
1441
+ if (!outputText) {
1442
+ continue;
1443
+ }
1444
+ const model = (_b = (_a = phase.model) != null ? _a : summary.model) != null ? _b : "claude";
1445
+ const now = nowUnixNanoString();
1446
+ const startNano = isoToNanoString(phase.startTimestamp, now);
1447
+ const endNano = isoToNanoString(phase.endTimestamp, startNano);
1448
+ const toolCallNames = phase.toolCalls.map((tc) => tc.name).join(", ");
1449
+ traceShipper.createSpan({
1450
+ name: model,
1451
+ eventId,
1452
+ parent,
1453
+ startTimeUnixNano: startNano,
1454
+ endTimeUnixNano: endNano,
1455
+ attributes: [
1456
+ attrString("ai.operationId", "generateText"),
1457
+ attrString("gen_ai.response.model", model),
1458
+ attrString("gen_ai.system", "anthropic"),
1459
+ attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1460
+ attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1461
+ ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1462
+ attrString("ai.response.text", outputText),
1463
+ ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1464
+ ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1465
+ attrInt("ai.llm_call.index", emittedPhaseIndex)
1466
+ ]
1467
+ });
1468
+ emittedPhaseIndex++;
1469
+ }
1470
+ } else {
1471
+ const spanInputTokens = (_c = summary.lastTurnInputTokens) != null ? _c : summary.totalInputTokens;
1472
+ const spanOutputTokens = (_d = summary.lastTurnOutputTokens) != null ? _d : summary.totalOutputTokens;
1473
+ if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1474
+ const now = nowUnixNanoString();
1475
+ traceShipper.createSpan({
1476
+ name: summary.model,
1477
+ eventId,
1478
+ parent,
1479
+ startTimeUnixNano: now,
1480
+ endTimeUnixNano: now,
1481
+ attributes: [
1482
+ attrString("ai.operationId", "generateText"),
1483
+ attrString("gen_ai.response.model", summary.model),
1484
+ attrString("gen_ai.system", "anthropic"),
1485
+ attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1486
+ attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1487
+ ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1488
+ ]
1489
+ });
1490
+ }
1360
1491
  }
1361
1492
  return { summary, props };
1362
1493
  } catch (e) {
1363
1494
  return { summary: void 0, props: {} };
1364
1495
  }
1365
1496
  }
1366
- async function handleStopOrFailure(payload, eventId, config, properties, eventShipper, traceShipper, extraProperties) {
1497
+ function normalizeAssistantText(value) {
1498
+ return value.trim().replace(/\r\n/g, "\n");
1499
+ }
1500
+ function buildStopOutput(summary, lastAssistantMessage) {
1501
+ const finalMessage = typeof lastAssistantMessage === "string" ? normalizeAssistantText(lastAssistantMessage) : void 0;
1502
+ if (!summary) return finalMessage;
1503
+ const transcriptBlocks = summary.lastTurnTextBlocks.map(normalizeAssistantText).filter(Boolean);
1504
+ if (finalMessage && !transcriptBlocks.includes(finalMessage)) {
1505
+ transcriptBlocks.push(finalMessage);
1506
+ }
1507
+ if (transcriptBlocks.length > 0) {
1508
+ return transcriptBlocks.join("\n\n");
1509
+ }
1510
+ return finalMessage;
1511
+ }
1512
+ async function handleStopOrFailure(payload, eventId, convoId, config, properties, eventShipper, traceShipper, extraProperties) {
1367
1513
  const { summary, props: transcriptProps } = enrichFromTranscript(payload, eventId, traceShipper);
1368
1514
  const instructions = gatherInstructions(payload.session_id);
1369
1515
  const appendSysPrompt = readAppendSystemPrompt(payload.session_id);
1516
+ const output = buildStopOutput(summary, payload.last_assistant_message);
1370
1517
  await eventShipper.patch(eventId, {
1371
1518
  isPending: false,
1372
1519
  userId: config.userId,
1373
- convoId: payload.session_id,
1520
+ convoId,
1374
1521
  eventName: config.eventName,
1375
- output: payload.last_assistant_message,
1522
+ output,
1376
1523
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1377
1524
  properties: {
1378
1525
  ...properties,
@@ -1383,10 +1530,11 @@ async function handleStopOrFailure(payload, eventId, config, properties, eventSh
1383
1530
  }
1384
1531
  });
1385
1532
  }
1386
- async function handleSessionEnd(payload, eventId, config, properties, eventShipper) {
1533
+ async function handleSessionEnd(payload, eventId, convoId, config, properties, eventShipper) {
1387
1534
  await eventShipper.patch(eventId, {
1388
1535
  isPending: false,
1389
1536
  userId: config.userId,
1537
+ convoId,
1390
1538
  eventName: config.eventName,
1391
1539
  properties: {
1392
1540
  ...properties,
@@ -1401,7 +1549,7 @@ import { homedir, userInfo } from "os";
1401
1549
  import { dirname, join as join2 } from "path";
1402
1550
  var CONFIG_PATH = join2(homedir(), ".config", "raindrop", "config.json");
1403
1551
  function loadConfig() {
1404
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
1552
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
1405
1553
  let file = {};
1406
1554
  try {
1407
1555
  if (existsSync3(CONFIG_PATH)) {
@@ -1442,9 +1590,10 @@ function loadConfig() {
1442
1590
  writeKey: (_c = (_b = process.env["RAINDROP_WRITE_KEY"]) != null ? _b : file.write_key) != null ? _c : "",
1443
1591
  endpoint: (_e = (_d = process.env["RAINDROP_API_URL"]) != null ? _d : file.api_url) != null ? _e : "https://api.raindrop.ai/v1",
1444
1592
  userId: (_g = (_f = process.env["RAINDROP_USER_ID"]) != null ? _f : file.user_id) != null ? _g : systemUser,
1445
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_h = file.debug) != null ? _h : false,
1446
- enabled: (_i = file.enabled) != null ? _i : true,
1447
- eventName: (_k = (_j = process.env["RAINDROP_EVENT_NAME"]) != null ? _j : file.event_name) != null ? _k : "claude_code_session",
1593
+ convoId: ((_h = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _h.trim()) || void 0,
1594
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_i = file.debug) != null ? _i : false,
1595
+ enabled: (_j = file.enabled) != null ? _j : true,
1596
+ eventName: (_l = (_k = process.env["RAINDROP_EVENT_NAME"]) != null ? _k : file.event_name) != null ? _l : "claude_code_session",
1448
1597
  customProperties,
1449
1598
  selfDiagnostics
1450
1599
  };
@@ -1617,6 +1766,7 @@ async function handleHook() {
1617
1766
  }
1618
1767
  const mapperConfig = {
1619
1768
  userId: config.userId,
1769
+ convoId: config.convoId,
1620
1770
  debug: config.debug,
1621
1771
  eventName: config.eventName,
1622
1772
  customProperties: config.customProperties
@@ -2348,6 +2498,7 @@ async function main() {
2348
2498
  Environment:
2349
2499
  RAINDROP_WRITE_KEY API write key (alternative to --write-key or config file)
2350
2500
  RAINDROP_USER_ID User ID override
2501
+ RAINDROP_CONVO_ID Conversation/thread ID override
2351
2502
  RAINDROP_EVENT_NAME Custom event name (default: "claude_code_session")
2352
2503
  RAINDROP_PROPERTIES JSON object merged into every event's properties
2353
2504
  RAINDROP_API_URL Custom API endpoint
package/dist/index.cjs CHANGED
@@ -690,7 +690,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
690
690
 
691
691
  // src/package-info.ts
692
692
  var PACKAGE_NAME = "@raindrop-ai/claude-code";
693
- var PACKAGE_VERSION = "0.0.7";
693
+ var PACKAGE_VERSION = "0.0.8";
694
694
 
695
695
  // src/shipper.ts
696
696
  var EventShipper2 = class extends EventShipper {
@@ -733,7 +733,7 @@ var import_node_os = require("os");
733
733
  var import_node_path = require("path");
734
734
  var CONFIG_PATH = (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "raindrop", "config.json");
735
735
  function loadConfig() {
736
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
736
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
737
737
  let file = {};
738
738
  try {
739
739
  if ((0, import_node_fs.existsSync)(CONFIG_PATH)) {
@@ -774,9 +774,10 @@ function loadConfig() {
774
774
  writeKey: (_c = (_b = process.env["RAINDROP_WRITE_KEY"]) != null ? _b : file.write_key) != null ? _c : "",
775
775
  endpoint: (_e = (_d = process.env["RAINDROP_API_URL"]) != null ? _d : file.api_url) != null ? _e : "https://api.raindrop.ai/v1",
776
776
  userId: (_g = (_f = process.env["RAINDROP_USER_ID"]) != null ? _f : file.user_id) != null ? _g : systemUser,
777
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_h = file.debug) != null ? _h : false,
778
- enabled: (_i = file.enabled) != null ? _i : true,
779
- eventName: (_k = (_j = process.env["RAINDROP_EVENT_NAME"]) != null ? _j : file.event_name) != null ? _k : "claude_code_session",
777
+ convoId: ((_h = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _h.trim()) || void 0,
778
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_i = file.debug) != null ? _i : false,
779
+ enabled: (_j = file.enabled) != null ? _j : true,
780
+ eventName: (_l = (_k = process.env["RAINDROP_EVENT_NAME"]) != null ? _k : file.event_name) != null ? _l : "claude_code_session",
780
781
  customProperties,
781
782
  selfDiagnostics
782
783
  };
@@ -806,9 +807,47 @@ var import_node_path2 = require("path");
806
807
 
807
808
  // src/transcript.ts
808
809
  var import_node_fs2 = require("fs");
810
+ function isToolResultContentBlock(value) {
811
+ return Boolean(
812
+ value && typeof value === "object" && "type" in value && value["type"] === "tool_result"
813
+ );
814
+ }
815
+ function isTopLevelUserPrompt(entry) {
816
+ var _a;
817
+ if (entry.type !== "user") return false;
818
+ const content = (_a = entry.message) == null ? void 0 : _a.content;
819
+ if (typeof content === "string") return true;
820
+ if (!Array.isArray(content)) return false;
821
+ return content.some((block) => !isToolResultContentBlock(block));
822
+ }
809
823
  function parseTranscript(transcriptPath) {
810
- var _a, _b, _c, _d, _e;
824
+ var _a, _b, _c, _d, _e, _f, _g, _h;
811
825
  try {
826
+ let finalizePhase2 = function() {
827
+ if (activePhase && (activePhase.text || activePhase.toolCalls.length > 0)) {
828
+ currentPhases.push(activePhase);
829
+ }
830
+ activePhase = void 0;
831
+ }, ensurePhase2 = function() {
832
+ if (!activePhase) {
833
+ activePhase = {
834
+ text: "",
835
+ toolCalls: [],
836
+ inputTokens: 0,
837
+ outputTokens: 0,
838
+ cacheReadTokens: 0,
839
+ hasThinking: false
840
+ };
841
+ }
842
+ return activePhase;
843
+ }, isToolResultUserEntry2 = function(entry) {
844
+ var _a2;
845
+ if (entry.type !== "user") return false;
846
+ const content2 = (_a2 = entry.message) == null ? void 0 : _a2.content;
847
+ if (!Array.isArray(content2)) return false;
848
+ return content2.length > 0 && content2.every((b) => isToolResultContentBlock(b));
849
+ };
850
+ var finalizePhase = finalizePhase2, ensurePhase = ensurePhase2, isToolResultUserEntry = isToolResultUserEntry2;
812
851
  if (!(0, import_node_fs2.existsSync)(transcriptPath)) return void 0;
813
852
  const content = (0, import_node_fs2.readFileSync)(transcriptPath, "utf-8");
814
853
  const lines = content.split("\n").filter((l) => l.trim());
@@ -819,10 +858,15 @@ function parseTranscript(transcriptPath) {
819
858
  totalCacheCreationTokens: 0,
820
859
  turnCount: 0,
821
860
  toolsUsed: [],
822
- hasThinking: false
861
+ hasThinking: false,
862
+ lastTurnTextBlocks: [],
863
+ llmCallPhases: []
823
864
  };
824
865
  const toolNames = /* @__PURE__ */ new Set();
825
866
  let lastUsage;
867
+ let currentTurnTextBlocks = [];
868
+ let currentPhases = [];
869
+ let activePhase;
826
870
  for (const line of lines) {
827
871
  let entry;
828
872
  try {
@@ -845,6 +889,15 @@ function parseTranscript(transcriptPath) {
845
889
  }
846
890
  continue;
847
891
  }
892
+ if (isTopLevelUserPrompt(entry)) {
893
+ currentTurnTextBlocks = [];
894
+ finalizePhase2();
895
+ currentPhases = [];
896
+ }
897
+ if (isToolResultUserEntry2(entry)) {
898
+ finalizePhase2();
899
+ continue;
900
+ }
848
901
  if (entry.type !== "assistant") continue;
849
902
  const msg = entry.message;
850
903
  if (!msg) continue;
@@ -874,23 +927,55 @@ function parseTranscript(transcriptPath) {
874
927
  }
875
928
  lastUsage = u;
876
929
  }
930
+ const phase = ensurePhase2();
931
+ const entryTimestamp = typeof entry["timestamp"] === "string" ? entry["timestamp"] : void 0;
932
+ if (entryTimestamp) {
933
+ if (!phase.startTimestamp) phase.startTimestamp = entryTimestamp;
934
+ phase.endTimestamp = entryTimestamp;
935
+ }
936
+ if (msg.model) phase.model = msg.model;
937
+ if (msg.usage) {
938
+ phase.inputTokens += (_f = msg.usage.input_tokens) != null ? _f : 0;
939
+ phase.outputTokens += (_g = msg.usage.output_tokens) != null ? _g : 0;
940
+ phase.cacheReadTokens += (_h = msg.usage.cache_read_input_tokens) != null ? _h : 0;
941
+ }
877
942
  if (Array.isArray(msg.content)) {
878
943
  for (const block of msg.content) {
879
944
  if (block.type === "tool_use" && block.name) {
880
945
  toolNames.add(block.name);
946
+ phase.toolCalls.push({
947
+ id: block.id,
948
+ name: block.name,
949
+ input: block.input
950
+ });
881
951
  }
882
952
  if (block.type === "thinking") {
883
953
  summary.hasThinking = true;
954
+ phase.hasThinking = true;
955
+ }
956
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
957
+ currentTurnTextBlocks.push(block.text);
958
+ if (phase.text) {
959
+ phase.text += "\n" + block.text;
960
+ } else {
961
+ phase.text = block.text;
962
+ }
884
963
  }
885
964
  }
965
+ summary.lastTurnTextBlocks = [...currentTurnTextBlocks];
886
966
  }
887
967
  }
968
+ finalizePhase2();
888
969
  if (lastUsage) {
889
970
  summary.lastTurnInputTokens = lastUsage.input_tokens;
890
971
  summary.lastTurnOutputTokens = lastUsage.output_tokens;
891
972
  summary.lastTurnCacheReadTokens = lastUsage.cache_read_input_tokens;
892
973
  }
893
974
  summary.toolsUsed = [...toolNames].sort();
975
+ if (summary.lastTurnTextBlocks.length > 0) {
976
+ summary.lastTurnFullOutput = summary.lastTurnTextBlocks.join("\n\n");
977
+ }
978
+ summary.llmCallPhases = currentPhases;
894
979
  return summary;
895
980
  } catch (e) {
896
981
  return void 0;
@@ -1039,12 +1124,13 @@ function readTimestamp(key) {
1039
1124
  return Number.isFinite(ts) ? ts : void 0;
1040
1125
  }
1041
1126
  async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1042
- var _a;
1043
- const convoId = payload.session_id;
1127
+ var _a, _b;
1128
+ const convoId = (_a = config.convoId) != null ? _a : payload.session_id;
1044
1129
  const baseProperties = {
1045
1130
  ...config.customProperties,
1046
1131
  cwd: payload.cwd,
1047
1132
  permission_mode: payload.permission_mode,
1133
+ claude_code_session_id: payload.session_id,
1048
1134
  plugin_version: PACKAGE_VERSION,
1049
1135
  sdk: PACKAGE_NAME,
1050
1136
  sdk_version: PACKAGE_VERSION,
@@ -1053,7 +1139,7 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1053
1139
  };
1054
1140
  switch (payload.hook_event_name) {
1055
1141
  case "SessionStart":
1056
- writeState(`model_${payload.session_id}`, (_a = payload.model) != null ? _a : "");
1142
+ writeState(`model_${payload.session_id}`, (_b = payload.model) != null ? _b : "");
1057
1143
  tryCaptureAppendSystemPrompt(payload.session_id);
1058
1144
  break;
1059
1145
  case "UserPromptSubmit":
@@ -1069,10 +1155,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1069
1155
  handlePostToolUseFailure(payload, getCurrentEventId(payload.session_id), traceShipper);
1070
1156
  break;
1071
1157
  case "Stop":
1072
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper);
1158
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper);
1073
1159
  break;
1074
1160
  case "StopFailure":
1075
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper, {
1161
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper, {
1076
1162
  error: payload.error,
1077
1163
  error_details: payload.error_details
1078
1164
  });
@@ -1090,10 +1176,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1090
1176
  handleInstructionsLoaded(payload);
1091
1177
  break;
1092
1178
  case "PostCompact":
1093
- await handlePostCompact(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1179
+ await handlePostCompact(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
1094
1180
  break;
1095
1181
  case "SessionEnd":
1096
- await handleSessionEnd(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1182
+ await handleSessionEnd(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
1097
1183
  deleteState(turnEventKey(payload.session_id));
1098
1184
  deleteState(rootSpanKey(payload.session_id));
1099
1185
  deleteState(`model_${payload.session_id}`);
@@ -1243,11 +1329,11 @@ function handlePermissionDenied(payload, eventId, traceShipper) {
1243
1329
  }
1244
1330
  });
1245
1331
  }
1246
- async function handlePostCompact(payload, eventId, config, properties, eventShipper) {
1332
+ async function handlePostCompact(payload, eventId, convoId, config, properties, eventShipper) {
1247
1333
  await eventShipper.patch(eventId, {
1248
1334
  isPending: true,
1249
1335
  userId: config.userId,
1250
- convoId: payload.session_id,
1336
+ convoId,
1251
1337
  eventName: config.eventName,
1252
1338
  properties: {
1253
1339
  ...properties,
@@ -1441,8 +1527,18 @@ function readAppendSystemPrompt(sessionId) {
1441
1527
  return void 0;
1442
1528
  }
1443
1529
  }
1530
+ function isoToNanoString(iso, fallback) {
1531
+ if (!iso) return fallback;
1532
+ try {
1533
+ const ms = new Date(iso).getTime();
1534
+ if (!Number.isFinite(ms)) return fallback;
1535
+ return String(ms) + "000000";
1536
+ } catch (e) {
1537
+ return fallback;
1538
+ }
1539
+ }
1444
1540
  function enrichFromTranscript(payload, eventId, traceShipper) {
1445
- var _a, _b;
1541
+ var _a, _b, _c, _d;
1446
1542
  if (!payload.transcript_path) {
1447
1543
  return { summary: void 0, props: {} };
1448
1544
  }
@@ -1450,42 +1546,94 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1450
1546
  const summary = parseTranscript(payload.transcript_path);
1451
1547
  if (!summary) return { summary: void 0, props: {} };
1452
1548
  const props = transcriptToProperties(summary);
1453
- const spanInputTokens = (_a = summary.lastTurnInputTokens) != null ? _a : summary.totalInputTokens;
1454
- const spanOutputTokens = (_b = summary.lastTurnOutputTokens) != null ? _b : summary.totalOutputTokens;
1455
- if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1456
- const parent = getParentContext(payload);
1457
- const now = nowUnixNanoString();
1458
- traceShipper.createSpan({
1459
- name: summary.model,
1460
- eventId,
1461
- parent,
1462
- startTimeUnixNano: now,
1463
- endTimeUnixNano: now,
1464
- attributes: [
1465
- attrString("ai.operationId", "generateText"),
1466
- attrString("gen_ai.response.model", summary.model),
1467
- attrString("gen_ai.system", "anthropic"),
1468
- attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1469
- attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1470
- ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1471
- ]
1472
- });
1549
+ const parent = getParentContext(payload);
1550
+ if (summary.llmCallPhases.length > 0) {
1551
+ let emittedPhaseIndex = 0;
1552
+ for (let i = 0; i < summary.llmCallPhases.length; i++) {
1553
+ const phase = summary.llmCallPhases[i];
1554
+ const outputText = phase.text.trim();
1555
+ if (!outputText) {
1556
+ continue;
1557
+ }
1558
+ const model = (_b = (_a = phase.model) != null ? _a : summary.model) != null ? _b : "claude";
1559
+ const now = nowUnixNanoString();
1560
+ const startNano = isoToNanoString(phase.startTimestamp, now);
1561
+ const endNano = isoToNanoString(phase.endTimestamp, startNano);
1562
+ const toolCallNames = phase.toolCalls.map((tc) => tc.name).join(", ");
1563
+ traceShipper.createSpan({
1564
+ name: model,
1565
+ eventId,
1566
+ parent,
1567
+ startTimeUnixNano: startNano,
1568
+ endTimeUnixNano: endNano,
1569
+ attributes: [
1570
+ attrString("ai.operationId", "generateText"),
1571
+ attrString("gen_ai.response.model", model),
1572
+ attrString("gen_ai.system", "anthropic"),
1573
+ attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1574
+ attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1575
+ ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1576
+ attrString("ai.response.text", outputText),
1577
+ ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1578
+ ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1579
+ attrInt("ai.llm_call.index", emittedPhaseIndex)
1580
+ ]
1581
+ });
1582
+ emittedPhaseIndex++;
1583
+ }
1584
+ } else {
1585
+ const spanInputTokens = (_c = summary.lastTurnInputTokens) != null ? _c : summary.totalInputTokens;
1586
+ const spanOutputTokens = (_d = summary.lastTurnOutputTokens) != null ? _d : summary.totalOutputTokens;
1587
+ if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1588
+ const now = nowUnixNanoString();
1589
+ traceShipper.createSpan({
1590
+ name: summary.model,
1591
+ eventId,
1592
+ parent,
1593
+ startTimeUnixNano: now,
1594
+ endTimeUnixNano: now,
1595
+ attributes: [
1596
+ attrString("ai.operationId", "generateText"),
1597
+ attrString("gen_ai.response.model", summary.model),
1598
+ attrString("gen_ai.system", "anthropic"),
1599
+ attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1600
+ attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1601
+ ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1602
+ ]
1603
+ });
1604
+ }
1473
1605
  }
1474
1606
  return { summary, props };
1475
1607
  } catch (e) {
1476
1608
  return { summary: void 0, props: {} };
1477
1609
  }
1478
1610
  }
1479
- async function handleStopOrFailure(payload, eventId, config, properties, eventShipper, traceShipper, extraProperties) {
1611
+ function normalizeAssistantText(value) {
1612
+ return value.trim().replace(/\r\n/g, "\n");
1613
+ }
1614
+ function buildStopOutput(summary, lastAssistantMessage) {
1615
+ const finalMessage = typeof lastAssistantMessage === "string" ? normalizeAssistantText(lastAssistantMessage) : void 0;
1616
+ if (!summary) return finalMessage;
1617
+ const transcriptBlocks = summary.lastTurnTextBlocks.map(normalizeAssistantText).filter(Boolean);
1618
+ if (finalMessage && !transcriptBlocks.includes(finalMessage)) {
1619
+ transcriptBlocks.push(finalMessage);
1620
+ }
1621
+ if (transcriptBlocks.length > 0) {
1622
+ return transcriptBlocks.join("\n\n");
1623
+ }
1624
+ return finalMessage;
1625
+ }
1626
+ async function handleStopOrFailure(payload, eventId, convoId, config, properties, eventShipper, traceShipper, extraProperties) {
1480
1627
  const { summary, props: transcriptProps } = enrichFromTranscript(payload, eventId, traceShipper);
1481
1628
  const instructions = gatherInstructions(payload.session_id);
1482
1629
  const appendSysPrompt = readAppendSystemPrompt(payload.session_id);
1630
+ const output = buildStopOutput(summary, payload.last_assistant_message);
1483
1631
  await eventShipper.patch(eventId, {
1484
1632
  isPending: false,
1485
1633
  userId: config.userId,
1486
- convoId: payload.session_id,
1634
+ convoId,
1487
1635
  eventName: config.eventName,
1488
- output: payload.last_assistant_message,
1636
+ output,
1489
1637
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1490
1638
  properties: {
1491
1639
  ...properties,
@@ -1496,10 +1644,11 @@ async function handleStopOrFailure(payload, eventId, config, properties, eventSh
1496
1644
  }
1497
1645
  });
1498
1646
  }
1499
- async function handleSessionEnd(payload, eventId, config, properties, eventShipper) {
1647
+ async function handleSessionEnd(payload, eventId, convoId, config, properties, eventShipper) {
1500
1648
  await eventShipper.patch(eventId, {
1501
1649
  isPending: false,
1502
1650
  userId: config.userId,
1651
+ convoId,
1503
1652
  eventName: config.eventName,
1504
1653
  properties: {
1505
1654
  ...properties,
package/dist/index.d.cts CHANGED
@@ -244,6 +244,7 @@ interface RaindropConfig {
244
244
  writeKey: string;
245
245
  endpoint: string;
246
246
  userId: string;
247
+ convoId?: string;
247
248
  debug: boolean;
248
249
  enabled: boolean;
249
250
  eventName: string;
@@ -292,6 +293,7 @@ interface HookPayload {
292
293
  }
293
294
  interface MapperConfig {
294
295
  userId: string;
296
+ convoId?: string;
295
297
  debug: boolean;
296
298
  eventName: string;
297
299
  customProperties: Record<string, unknown>;
@@ -308,8 +310,37 @@ declare function mapHookToRaindrop(payload: HookPayload, config: MapperConfig, e
308
310
  declare function extractAppendSystemPrompt(args: string[]): string | undefined;
309
311
 
310
312
  declare const PACKAGE_NAME = "@raindrop-ai/claude-code";
311
- declare const PACKAGE_VERSION = "0.0.7";
313
+ declare const PACKAGE_VERSION = "0.0.8";
312
314
 
315
+ /** A tool call extracted from an assistant message content block. */
316
+ interface LLMToolCall {
317
+ id?: string;
318
+ name: string;
319
+ input?: Record<string, unknown>;
320
+ }
321
+ /**
322
+ * One LLM-call phase within a turn.
323
+ * A phase = one or more contiguous assistant messages before the next
324
+ * tool-result user entry resets the context.
325
+ */
326
+ interface LLMCallPhase {
327
+ /** Concatenated assistant text blocks in this phase */
328
+ text: string;
329
+ /** Tool calls the model decided to make in this phase */
330
+ toolCalls: LLMToolCall[];
331
+ /** Model that produced this phase */
332
+ model?: string;
333
+ /** Token usage accumulated across assistant messages in this phase */
334
+ inputTokens: number;
335
+ outputTokens: number;
336
+ cacheReadTokens: number;
337
+ /** Whether a thinking block appeared in this phase */
338
+ hasThinking: boolean;
339
+ /** ISO timestamp of the first assistant message in the phase */
340
+ startTimestamp?: string;
341
+ /** ISO timestamp of the last assistant message in the phase */
342
+ endTimestamp?: string;
343
+ }
313
344
  interface TranscriptSummary {
314
345
  /** Aggregated token usage across all turns */
315
346
  totalInputTokens: number;
@@ -338,6 +369,12 @@ interface TranscriptSummary {
338
369
  gitBranch?: string;
339
370
  /** Whether thinking/reasoning content was used */
340
371
  hasThinking: boolean;
372
+ /** Ordered assistant text blocks for the latest top-level user turn */
373
+ lastTurnTextBlocks: string[];
374
+ /** Combined assistant text for the latest top-level user turn */
375
+ lastTurnFullOutput?: string;
376
+ /** Ordered LLM-call phases for the latest top-level user turn */
377
+ llmCallPhases: LLMCallPhase[];
341
378
  }
342
379
  /**
343
380
  * Parse a Claude Code transcript JSONL file and extract a summary.
@@ -425,4 +462,4 @@ declare const TOOL_SCHEMA: {
425
462
  };
426
463
  declare function startMcpServer(): Promise<void>;
427
464
 
428
- export { DEFAULT_CATEGORY_KEYS, EventShipper, type HookPayload, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SelfDiagnosticsConfig, type SelfDiagnosticsSignalDef, type SetupScope, TOOL_SCHEMA, TraceShipper, type TranscriptSummary, detectLocalDebugger, executeTool, extractAppendSystemPrompt, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, normalizeSignals, parseTranscript, resolveCurrentEventId, resolveToolConfig, startMcpServer, transcriptToProperties, updateConfig };
465
+ export { DEFAULT_CATEGORY_KEYS, EventShipper, type HookPayload, type LLMCallPhase, type LLMToolCall, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SelfDiagnosticsConfig, type SelfDiagnosticsSignalDef, type SetupScope, TOOL_SCHEMA, TraceShipper, type TranscriptSummary, detectLocalDebugger, executeTool, extractAppendSystemPrompt, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, normalizeSignals, parseTranscript, resolveCurrentEventId, resolveToolConfig, startMcpServer, transcriptToProperties, updateConfig };
package/dist/index.d.ts CHANGED
@@ -244,6 +244,7 @@ interface RaindropConfig {
244
244
  writeKey: string;
245
245
  endpoint: string;
246
246
  userId: string;
247
+ convoId?: string;
247
248
  debug: boolean;
248
249
  enabled: boolean;
249
250
  eventName: string;
@@ -292,6 +293,7 @@ interface HookPayload {
292
293
  }
293
294
  interface MapperConfig {
294
295
  userId: string;
296
+ convoId?: string;
295
297
  debug: boolean;
296
298
  eventName: string;
297
299
  customProperties: Record<string, unknown>;
@@ -308,8 +310,37 @@ declare function mapHookToRaindrop(payload: HookPayload, config: MapperConfig, e
308
310
  declare function extractAppendSystemPrompt(args: string[]): string | undefined;
309
311
 
310
312
  declare const PACKAGE_NAME = "@raindrop-ai/claude-code";
311
- declare const PACKAGE_VERSION = "0.0.7";
313
+ declare const PACKAGE_VERSION = "0.0.8";
312
314
 
315
+ /** A tool call extracted from an assistant message content block. */
316
+ interface LLMToolCall {
317
+ id?: string;
318
+ name: string;
319
+ input?: Record<string, unknown>;
320
+ }
321
+ /**
322
+ * One LLM-call phase within a turn.
323
+ * A phase = one or more contiguous assistant messages before the next
324
+ * tool-result user entry resets the context.
325
+ */
326
+ interface LLMCallPhase {
327
+ /** Concatenated assistant text blocks in this phase */
328
+ text: string;
329
+ /** Tool calls the model decided to make in this phase */
330
+ toolCalls: LLMToolCall[];
331
+ /** Model that produced this phase */
332
+ model?: string;
333
+ /** Token usage accumulated across assistant messages in this phase */
334
+ inputTokens: number;
335
+ outputTokens: number;
336
+ cacheReadTokens: number;
337
+ /** Whether a thinking block appeared in this phase */
338
+ hasThinking: boolean;
339
+ /** ISO timestamp of the first assistant message in the phase */
340
+ startTimestamp?: string;
341
+ /** ISO timestamp of the last assistant message in the phase */
342
+ endTimestamp?: string;
343
+ }
313
344
  interface TranscriptSummary {
314
345
  /** Aggregated token usage across all turns */
315
346
  totalInputTokens: number;
@@ -338,6 +369,12 @@ interface TranscriptSummary {
338
369
  gitBranch?: string;
339
370
  /** Whether thinking/reasoning content was used */
340
371
  hasThinking: boolean;
372
+ /** Ordered assistant text blocks for the latest top-level user turn */
373
+ lastTurnTextBlocks: string[];
374
+ /** Combined assistant text for the latest top-level user turn */
375
+ lastTurnFullOutput?: string;
376
+ /** Ordered LLM-call phases for the latest top-level user turn */
377
+ llmCallPhases: LLMCallPhase[];
341
378
  }
342
379
  /**
343
380
  * Parse a Claude Code transcript JSONL file and extract a summary.
@@ -425,4 +462,4 @@ declare const TOOL_SCHEMA: {
425
462
  };
426
463
  declare function startMcpServer(): Promise<void>;
427
464
 
428
- export { DEFAULT_CATEGORY_KEYS, EventShipper, type HookPayload, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SelfDiagnosticsConfig, type SelfDiagnosticsSignalDef, type SetupScope, TOOL_SCHEMA, TraceShipper, type TranscriptSummary, detectLocalDebugger, executeTool, extractAppendSystemPrompt, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, normalizeSignals, parseTranscript, resolveCurrentEventId, resolveToolConfig, startMcpServer, transcriptToProperties, updateConfig };
465
+ export { DEFAULT_CATEGORY_KEYS, EventShipper, type HookPayload, type LLMCallPhase, type LLMToolCall, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SelfDiagnosticsConfig, type SelfDiagnosticsSignalDef, type SetupScope, TOOL_SCHEMA, TraceShipper, type TranscriptSummary, detectLocalDebugger, executeTool, extractAppendSystemPrompt, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, normalizeSignals, parseTranscript, resolveCurrentEventId, resolveToolConfig, startMcpServer, transcriptToProperties, updateConfig };
package/dist/index.js CHANGED
@@ -645,7 +645,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
645
645
 
646
646
  // src/package-info.ts
647
647
  var PACKAGE_NAME = "@raindrop-ai/claude-code";
648
- var PACKAGE_VERSION = "0.0.7";
648
+ var PACKAGE_VERSION = "0.0.8";
649
649
 
650
650
  // src/shipper.ts
651
651
  var EventShipper2 = class extends EventShipper {
@@ -688,7 +688,7 @@ import { homedir, userInfo } from "os";
688
688
  import { dirname, join } from "path";
689
689
  var CONFIG_PATH = join(homedir(), ".config", "raindrop", "config.json");
690
690
  function loadConfig() {
691
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
691
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
692
692
  let file = {};
693
693
  try {
694
694
  if (existsSync(CONFIG_PATH)) {
@@ -729,9 +729,10 @@ function loadConfig() {
729
729
  writeKey: (_c = (_b = process.env["RAINDROP_WRITE_KEY"]) != null ? _b : file.write_key) != null ? _c : "",
730
730
  endpoint: (_e = (_d = process.env["RAINDROP_API_URL"]) != null ? _d : file.api_url) != null ? _e : "https://api.raindrop.ai/v1",
731
731
  userId: (_g = (_f = process.env["RAINDROP_USER_ID"]) != null ? _f : file.user_id) != null ? _g : systemUser,
732
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_h = file.debug) != null ? _h : false,
733
- enabled: (_i = file.enabled) != null ? _i : true,
734
- eventName: (_k = (_j = process.env["RAINDROP_EVENT_NAME"]) != null ? _j : file.event_name) != null ? _k : "claude_code_session",
732
+ convoId: ((_h = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _h.trim()) || void 0,
733
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_i = file.debug) != null ? _i : false,
734
+ enabled: (_j = file.enabled) != null ? _j : true,
735
+ eventName: (_l = (_k = process.env["RAINDROP_EVENT_NAME"]) != null ? _k : file.event_name) != null ? _l : "claude_code_session",
735
736
  customProperties,
736
737
  selfDiagnostics
737
738
  };
@@ -761,9 +762,47 @@ import { join as join2 } from "path";
761
762
 
762
763
  // src/transcript.ts
763
764
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
765
+ function isToolResultContentBlock(value) {
766
+ return Boolean(
767
+ value && typeof value === "object" && "type" in value && value["type"] === "tool_result"
768
+ );
769
+ }
770
+ function isTopLevelUserPrompt(entry) {
771
+ var _a;
772
+ if (entry.type !== "user") return false;
773
+ const content = (_a = entry.message) == null ? void 0 : _a.content;
774
+ if (typeof content === "string") return true;
775
+ if (!Array.isArray(content)) return false;
776
+ return content.some((block) => !isToolResultContentBlock(block));
777
+ }
764
778
  function parseTranscript(transcriptPath) {
765
- var _a, _b, _c, _d, _e;
779
+ var _a, _b, _c, _d, _e, _f, _g, _h;
766
780
  try {
781
+ let finalizePhase2 = function() {
782
+ if (activePhase && (activePhase.text || activePhase.toolCalls.length > 0)) {
783
+ currentPhases.push(activePhase);
784
+ }
785
+ activePhase = void 0;
786
+ }, ensurePhase2 = function() {
787
+ if (!activePhase) {
788
+ activePhase = {
789
+ text: "",
790
+ toolCalls: [],
791
+ inputTokens: 0,
792
+ outputTokens: 0,
793
+ cacheReadTokens: 0,
794
+ hasThinking: false
795
+ };
796
+ }
797
+ return activePhase;
798
+ }, isToolResultUserEntry2 = function(entry) {
799
+ var _a2;
800
+ if (entry.type !== "user") return false;
801
+ const content2 = (_a2 = entry.message) == null ? void 0 : _a2.content;
802
+ if (!Array.isArray(content2)) return false;
803
+ return content2.length > 0 && content2.every((b) => isToolResultContentBlock(b));
804
+ };
805
+ var finalizePhase = finalizePhase2, ensurePhase = ensurePhase2, isToolResultUserEntry = isToolResultUserEntry2;
767
806
  if (!existsSync2(transcriptPath)) return void 0;
768
807
  const content = readFileSync2(transcriptPath, "utf-8");
769
808
  const lines = content.split("\n").filter((l) => l.trim());
@@ -774,10 +813,15 @@ function parseTranscript(transcriptPath) {
774
813
  totalCacheCreationTokens: 0,
775
814
  turnCount: 0,
776
815
  toolsUsed: [],
777
- hasThinking: false
816
+ hasThinking: false,
817
+ lastTurnTextBlocks: [],
818
+ llmCallPhases: []
778
819
  };
779
820
  const toolNames = /* @__PURE__ */ new Set();
780
821
  let lastUsage;
822
+ let currentTurnTextBlocks = [];
823
+ let currentPhases = [];
824
+ let activePhase;
781
825
  for (const line of lines) {
782
826
  let entry;
783
827
  try {
@@ -800,6 +844,15 @@ function parseTranscript(transcriptPath) {
800
844
  }
801
845
  continue;
802
846
  }
847
+ if (isTopLevelUserPrompt(entry)) {
848
+ currentTurnTextBlocks = [];
849
+ finalizePhase2();
850
+ currentPhases = [];
851
+ }
852
+ if (isToolResultUserEntry2(entry)) {
853
+ finalizePhase2();
854
+ continue;
855
+ }
803
856
  if (entry.type !== "assistant") continue;
804
857
  const msg = entry.message;
805
858
  if (!msg) continue;
@@ -829,23 +882,55 @@ function parseTranscript(transcriptPath) {
829
882
  }
830
883
  lastUsage = u;
831
884
  }
885
+ const phase = ensurePhase2();
886
+ const entryTimestamp = typeof entry["timestamp"] === "string" ? entry["timestamp"] : void 0;
887
+ if (entryTimestamp) {
888
+ if (!phase.startTimestamp) phase.startTimestamp = entryTimestamp;
889
+ phase.endTimestamp = entryTimestamp;
890
+ }
891
+ if (msg.model) phase.model = msg.model;
892
+ if (msg.usage) {
893
+ phase.inputTokens += (_f = msg.usage.input_tokens) != null ? _f : 0;
894
+ phase.outputTokens += (_g = msg.usage.output_tokens) != null ? _g : 0;
895
+ phase.cacheReadTokens += (_h = msg.usage.cache_read_input_tokens) != null ? _h : 0;
896
+ }
832
897
  if (Array.isArray(msg.content)) {
833
898
  for (const block of msg.content) {
834
899
  if (block.type === "tool_use" && block.name) {
835
900
  toolNames.add(block.name);
901
+ phase.toolCalls.push({
902
+ id: block.id,
903
+ name: block.name,
904
+ input: block.input
905
+ });
836
906
  }
837
907
  if (block.type === "thinking") {
838
908
  summary.hasThinking = true;
909
+ phase.hasThinking = true;
910
+ }
911
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
912
+ currentTurnTextBlocks.push(block.text);
913
+ if (phase.text) {
914
+ phase.text += "\n" + block.text;
915
+ } else {
916
+ phase.text = block.text;
917
+ }
839
918
  }
840
919
  }
920
+ summary.lastTurnTextBlocks = [...currentTurnTextBlocks];
841
921
  }
842
922
  }
923
+ finalizePhase2();
843
924
  if (lastUsage) {
844
925
  summary.lastTurnInputTokens = lastUsage.input_tokens;
845
926
  summary.lastTurnOutputTokens = lastUsage.output_tokens;
846
927
  summary.lastTurnCacheReadTokens = lastUsage.cache_read_input_tokens;
847
928
  }
848
929
  summary.toolsUsed = [...toolNames].sort();
930
+ if (summary.lastTurnTextBlocks.length > 0) {
931
+ summary.lastTurnFullOutput = summary.lastTurnTextBlocks.join("\n\n");
932
+ }
933
+ summary.llmCallPhases = currentPhases;
849
934
  return summary;
850
935
  } catch (e) {
851
936
  return void 0;
@@ -994,12 +1079,13 @@ function readTimestamp(key) {
994
1079
  return Number.isFinite(ts) ? ts : void 0;
995
1080
  }
996
1081
  async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
997
- var _a;
998
- const convoId = payload.session_id;
1082
+ var _a, _b;
1083
+ const convoId = (_a = config.convoId) != null ? _a : payload.session_id;
999
1084
  const baseProperties = {
1000
1085
  ...config.customProperties,
1001
1086
  cwd: payload.cwd,
1002
1087
  permission_mode: payload.permission_mode,
1088
+ claude_code_session_id: payload.session_id,
1003
1089
  plugin_version: PACKAGE_VERSION,
1004
1090
  sdk: PACKAGE_NAME,
1005
1091
  sdk_version: PACKAGE_VERSION,
@@ -1008,7 +1094,7 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1008
1094
  };
1009
1095
  switch (payload.hook_event_name) {
1010
1096
  case "SessionStart":
1011
- writeState(`model_${payload.session_id}`, (_a = payload.model) != null ? _a : "");
1097
+ writeState(`model_${payload.session_id}`, (_b = payload.model) != null ? _b : "");
1012
1098
  tryCaptureAppendSystemPrompt(payload.session_id);
1013
1099
  break;
1014
1100
  case "UserPromptSubmit":
@@ -1024,10 +1110,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1024
1110
  handlePostToolUseFailure(payload, getCurrentEventId(payload.session_id), traceShipper);
1025
1111
  break;
1026
1112
  case "Stop":
1027
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper);
1113
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper);
1028
1114
  break;
1029
1115
  case "StopFailure":
1030
- await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper, traceShipper, {
1116
+ await handleStopOrFailure(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper, traceShipper, {
1031
1117
  error: payload.error,
1032
1118
  error_details: payload.error_details
1033
1119
  });
@@ -1045,10 +1131,10 @@ async function mapHookToRaindrop(payload, config, eventShipper, traceShipper) {
1045
1131
  handleInstructionsLoaded(payload);
1046
1132
  break;
1047
1133
  case "PostCompact":
1048
- await handlePostCompact(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1134
+ await handlePostCompact(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
1049
1135
  break;
1050
1136
  case "SessionEnd":
1051
- await handleSessionEnd(payload, getCurrentEventId(payload.session_id), config, baseProperties, eventShipper);
1137
+ await handleSessionEnd(payload, getCurrentEventId(payload.session_id), convoId, config, baseProperties, eventShipper);
1052
1138
  deleteState(turnEventKey(payload.session_id));
1053
1139
  deleteState(rootSpanKey(payload.session_id));
1054
1140
  deleteState(`model_${payload.session_id}`);
@@ -1198,11 +1284,11 @@ function handlePermissionDenied(payload, eventId, traceShipper) {
1198
1284
  }
1199
1285
  });
1200
1286
  }
1201
- async function handlePostCompact(payload, eventId, config, properties, eventShipper) {
1287
+ async function handlePostCompact(payload, eventId, convoId, config, properties, eventShipper) {
1202
1288
  await eventShipper.patch(eventId, {
1203
1289
  isPending: true,
1204
1290
  userId: config.userId,
1205
- convoId: payload.session_id,
1291
+ convoId,
1206
1292
  eventName: config.eventName,
1207
1293
  properties: {
1208
1294
  ...properties,
@@ -1396,8 +1482,18 @@ function readAppendSystemPrompt(sessionId) {
1396
1482
  return void 0;
1397
1483
  }
1398
1484
  }
1485
+ function isoToNanoString(iso, fallback) {
1486
+ if (!iso) return fallback;
1487
+ try {
1488
+ const ms = new Date(iso).getTime();
1489
+ if (!Number.isFinite(ms)) return fallback;
1490
+ return String(ms) + "000000";
1491
+ } catch (e) {
1492
+ return fallback;
1493
+ }
1494
+ }
1399
1495
  function enrichFromTranscript(payload, eventId, traceShipper) {
1400
- var _a, _b;
1496
+ var _a, _b, _c, _d;
1401
1497
  if (!payload.transcript_path) {
1402
1498
  return { summary: void 0, props: {} };
1403
1499
  }
@@ -1405,42 +1501,94 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1405
1501
  const summary = parseTranscript(payload.transcript_path);
1406
1502
  if (!summary) return { summary: void 0, props: {} };
1407
1503
  const props = transcriptToProperties(summary);
1408
- const spanInputTokens = (_a = summary.lastTurnInputTokens) != null ? _a : summary.totalInputTokens;
1409
- const spanOutputTokens = (_b = summary.lastTurnOutputTokens) != null ? _b : summary.totalOutputTokens;
1410
- if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1411
- const parent = getParentContext(payload);
1412
- const now = nowUnixNanoString();
1413
- traceShipper.createSpan({
1414
- name: summary.model,
1415
- eventId,
1416
- parent,
1417
- startTimeUnixNano: now,
1418
- endTimeUnixNano: now,
1419
- attributes: [
1420
- attrString("ai.operationId", "generateText"),
1421
- attrString("gen_ai.response.model", summary.model),
1422
- attrString("gen_ai.system", "anthropic"),
1423
- attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1424
- attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1425
- ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1426
- ]
1427
- });
1504
+ const parent = getParentContext(payload);
1505
+ if (summary.llmCallPhases.length > 0) {
1506
+ let emittedPhaseIndex = 0;
1507
+ for (let i = 0; i < summary.llmCallPhases.length; i++) {
1508
+ const phase = summary.llmCallPhases[i];
1509
+ const outputText = phase.text.trim();
1510
+ if (!outputText) {
1511
+ continue;
1512
+ }
1513
+ const model = (_b = (_a = phase.model) != null ? _a : summary.model) != null ? _b : "claude";
1514
+ const now = nowUnixNanoString();
1515
+ const startNano = isoToNanoString(phase.startTimestamp, now);
1516
+ const endNano = isoToNanoString(phase.endTimestamp, startNano);
1517
+ const toolCallNames = phase.toolCalls.map((tc) => tc.name).join(", ");
1518
+ traceShipper.createSpan({
1519
+ name: model,
1520
+ eventId,
1521
+ parent,
1522
+ startTimeUnixNano: startNano,
1523
+ endTimeUnixNano: endNano,
1524
+ attributes: [
1525
+ attrString("ai.operationId", "generateText"),
1526
+ attrString("gen_ai.response.model", model),
1527
+ attrString("gen_ai.system", "anthropic"),
1528
+ attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1529
+ attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1530
+ ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1531
+ attrString("ai.response.text", outputText),
1532
+ ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1533
+ ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1534
+ attrInt("ai.llm_call.index", emittedPhaseIndex)
1535
+ ]
1536
+ });
1537
+ emittedPhaseIndex++;
1538
+ }
1539
+ } else {
1540
+ const spanInputTokens = (_c = summary.lastTurnInputTokens) != null ? _c : summary.totalInputTokens;
1541
+ const spanOutputTokens = (_d = summary.lastTurnOutputTokens) != null ? _d : summary.totalOutputTokens;
1542
+ if (summary.model && (spanInputTokens > 0 || spanOutputTokens > 0)) {
1543
+ const now = nowUnixNanoString();
1544
+ traceShipper.createSpan({
1545
+ name: summary.model,
1546
+ eventId,
1547
+ parent,
1548
+ startTimeUnixNano: now,
1549
+ endTimeUnixNano: now,
1550
+ attributes: [
1551
+ attrString("ai.operationId", "generateText"),
1552
+ attrString("gen_ai.response.model", summary.model),
1553
+ attrString("gen_ai.system", "anthropic"),
1554
+ attrInt("gen_ai.usage.prompt_tokens", spanInputTokens),
1555
+ attrInt("gen_ai.usage.completion_tokens", spanOutputTokens),
1556
+ ...summary.lastTurnCacheReadTokens != null && summary.lastTurnCacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", summary.lastTurnCacheReadTokens)] : []
1557
+ ]
1558
+ });
1559
+ }
1428
1560
  }
1429
1561
  return { summary, props };
1430
1562
  } catch (e) {
1431
1563
  return { summary: void 0, props: {} };
1432
1564
  }
1433
1565
  }
1434
- async function handleStopOrFailure(payload, eventId, config, properties, eventShipper, traceShipper, extraProperties) {
1566
+ function normalizeAssistantText(value) {
1567
+ return value.trim().replace(/\r\n/g, "\n");
1568
+ }
1569
+ function buildStopOutput(summary, lastAssistantMessage) {
1570
+ const finalMessage = typeof lastAssistantMessage === "string" ? normalizeAssistantText(lastAssistantMessage) : void 0;
1571
+ if (!summary) return finalMessage;
1572
+ const transcriptBlocks = summary.lastTurnTextBlocks.map(normalizeAssistantText).filter(Boolean);
1573
+ if (finalMessage && !transcriptBlocks.includes(finalMessage)) {
1574
+ transcriptBlocks.push(finalMessage);
1575
+ }
1576
+ if (transcriptBlocks.length > 0) {
1577
+ return transcriptBlocks.join("\n\n");
1578
+ }
1579
+ return finalMessage;
1580
+ }
1581
+ async function handleStopOrFailure(payload, eventId, convoId, config, properties, eventShipper, traceShipper, extraProperties) {
1435
1582
  const { summary, props: transcriptProps } = enrichFromTranscript(payload, eventId, traceShipper);
1436
1583
  const instructions = gatherInstructions(payload.session_id);
1437
1584
  const appendSysPrompt = readAppendSystemPrompt(payload.session_id);
1585
+ const output = buildStopOutput(summary, payload.last_assistant_message);
1438
1586
  await eventShipper.patch(eventId, {
1439
1587
  isPending: false,
1440
1588
  userId: config.userId,
1441
- convoId: payload.session_id,
1589
+ convoId,
1442
1590
  eventName: config.eventName,
1443
- output: payload.last_assistant_message,
1591
+ output,
1444
1592
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1445
1593
  properties: {
1446
1594
  ...properties,
@@ -1451,10 +1599,11 @@ async function handleStopOrFailure(payload, eventId, config, properties, eventSh
1451
1599
  }
1452
1600
  });
1453
1601
  }
1454
- async function handleSessionEnd(payload, eventId, config, properties, eventShipper) {
1602
+ async function handleSessionEnd(payload, eventId, convoId, config, properties, eventShipper) {
1455
1603
  await eventShipper.patch(eventId, {
1456
1604
  isPending: false,
1457
1605
  userId: config.userId,
1606
+ convoId,
1458
1607
  eventName: config.eventName,
1459
1608
  properties: {
1460
1609
  ...properties,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raindrop-ai/claude-code",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Raindrop observability for Claude Code CLI \u2014 automatic session, tool call, and prompt tracing via hooks",
5
5
  "license": "MIT",
6
6
  "type": "module",