@raindrop-ai/opencode-plugin 0.0.6 → 0.0.8

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.
Files changed (3) hide show
  1. package/dist/index.cjs +158 -18
  2. package/dist/index.js +158 -18
  3. package/package.json +5 -5
package/dist/index.cjs CHANGED
@@ -29,7 +29,7 @@ var import_node_fs = require("fs");
29
29
  var import_node_os = require("os");
30
30
  var import_node_path = require("path");
31
31
  function loadConfig(projectDirectory) {
32
- var _a, _b, _c, _d, _e, _f;
32
+ var _a, _b, _c, _d, _e, _f, _g;
33
33
  let merged = {};
34
34
  const configPaths = [
35
35
  (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
@@ -58,6 +58,7 @@ function loadConfig(projectDirectory) {
58
58
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
59
59
  eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
60
60
  debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
61
+ captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
61
62
  eventMetadata
62
63
  };
63
64
  }
@@ -65,7 +66,7 @@ function loadConfig(projectDirectory) {
65
66
  // package.json
66
67
  var package_default = {
67
68
  name: "@raindrop-ai/opencode-plugin",
68
- version: "0.0.6",
69
+ version: "0.0.8",
69
70
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
70
71
  type: "module",
71
72
  main: "dist/index.cjs",
@@ -89,8 +90,8 @@ var package_default = {
89
90
  test: "cd tests && pnpm test"
90
91
  },
91
92
  peerDependencies: {
92
- "@opencode-ai/plugin": ">=1.2.0",
93
- "@opencode-ai/sdk": ">=1.2.0"
93
+ "@opencode-ai/plugin": ">=1.3.0",
94
+ "@opencode-ai/sdk": ">=1.3.0"
94
95
  },
95
96
  peerDependenciesMeta: {
96
97
  "@opencode-ai/sdk": {
@@ -99,8 +100,8 @@ var package_default = {
99
100
  },
100
101
  devDependencies: {
101
102
  "@raindrop-ai/core": "workspace:*",
102
- "@opencode-ai/plugin": "^1.2.0",
103
- "@opencode-ai/sdk": "^1.2.0",
103
+ "@opencode-ai/plugin": "^1.3.3",
104
+ "@opencode-ai/sdk": "^1.3.3",
104
105
  "@types/node": "^20.11.17",
105
106
  tsup: "^8.4.0",
106
107
  typescript: "^5.3.3"
@@ -130,7 +131,7 @@ var package_default = {
130
131
  var PLUGIN_NAME = package_default.name;
131
132
  var PLUGIN_VERSION = package_default.version;
132
133
 
133
- // ../core/dist/chunk-FTBZHS25.js
134
+ // ../core/dist/chunk-4UCYIEH4.js
134
135
  function getCrypto() {
135
136
  const c = globalThis.crypto;
136
137
  return c;
@@ -375,6 +376,7 @@ var EventShipper = class {
375
376
  console.log(`${this.prefix} queue patch`, {
376
377
  eventId,
377
378
  userId: patch.userId,
379
+ convoId: patch.convoId,
378
380
  eventName: patch.eventName,
379
381
  hasInput: typeof patch.input === "string" && patch.input.length > 0,
380
382
  hasOutput: typeof patch.output === "string" && patch.output.length > 0,
@@ -503,16 +505,18 @@ var EventShipper = class {
503
505
  return;
504
506
  }
505
507
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
508
+ const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
509
+ const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
506
510
  const payload = {
507
511
  event_id: eventId,
508
512
  user_id: userId,
509
513
  event: eventName,
510
- timestamp: (_f = accumulated.timestamp) != null ? _f : (/* @__PURE__ */ new Date()).toISOString(),
514
+ timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
511
515
  ai_data: {
512
516
  input: accumulated.input,
513
517
  output: accumulated.output,
514
518
  model: accumulated.model,
515
- convo_id: (_g = accumulated.convoId) != null ? _g : sticky.convoId
519
+ convo_id: convoId
516
520
  },
517
521
  properties: {
518
522
  ...restProperties,
@@ -520,7 +524,7 @@ var EventShipper = class {
520
524
  $context: this.context
521
525
  },
522
526
  attachments: accumulated.attachments,
523
- is_pending: ((_i = (_h = accumulated.isPending) != null ? _h : sticky.isPending) != null ? _i : true) !== false
527
+ is_pending: isPending
524
528
  };
525
529
  const url = `${this.baseUrl}events/track_partial`;
526
530
  if (this.debug) {
@@ -528,7 +532,8 @@ var EventShipper = class {
528
532
  eventId,
529
533
  eventName,
530
534
  userId,
531
- isPending: payload.is_pending,
535
+ convoId,
536
+ isPending,
532
537
  inputPreview: typeof accumulated.input === "string" ? accumulated.input.slice(0, 120) : void 0,
533
538
  outputPreview: typeof accumulated.output === "string" ? accumulated.output.slice(0, 120) : void 0,
534
539
  attachments: (_k = (_j = accumulated.attachments) == null ? void 0 : _j.length) != null ? _k : 0,
@@ -560,17 +565,33 @@ var EventShipper = class {
560
565
  } finally {
561
566
  this.inFlight.delete(p);
562
567
  }
563
- const isPending = payload.is_pending;
564
568
  if (!isPending) {
565
569
  this.sticky.delete(eventId);
566
570
  }
567
571
  }
568
572
  };
573
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
574
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
575
+ var _a, _b, _c;
576
+ const resolved = (_b = baseUrl != null ? baseUrl : typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[LOCAL_DEBUGGER_ENV_VAR] : void 0) != null ? _b : null;
577
+ return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
578
+ }
579
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
580
+ var _a;
581
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
582
+ if (!baseUrl) return;
583
+ void postJson(`${baseUrl}traces`, body, {}, {
584
+ maxAttempts: 1,
585
+ debug: (_a = options.debug) != null ? _a : false,
586
+ sdkName: options.sdkName
587
+ }).catch(() => {
588
+ });
589
+ }
569
590
  var TraceShipper = class {
570
591
  constructor(opts) {
571
592
  this.queue = [];
572
593
  this.inFlight = /* @__PURE__ */ new Set();
573
- var _a, _b, _c, _d, _e, _f, _g, _h;
594
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
574
595
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
575
596
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
576
597
  this.enabled = opts.enabled !== false;
@@ -583,6 +604,13 @@ var TraceShipper = class {
583
604
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
584
605
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
585
606
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
607
+ const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
608
+ if (localDebugger) {
609
+ this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
610
+ if (this.debug) {
611
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
612
+ }
613
+ }
586
614
  }
587
615
  isDebugEnabled() {
588
616
  return this.debug;
@@ -599,7 +627,25 @@ var TraceShipper = class {
599
627
  attrString("ai.operationId", args.operationId)
600
628
  ];
601
629
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
602
- return { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
630
+ const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
631
+ if (this.localDebuggerUrl) {
632
+ const openSpan = buildOtlpSpan({
633
+ ids: span.ids,
634
+ name: span.name,
635
+ startTimeUnixNano: span.startTimeUnixNano,
636
+ endTimeUnixNano: span.startTimeUnixNano,
637
+ // placeholder — will be updated on endSpan
638
+ attributes: span.attributes,
639
+ status: { code: SpanStatusCode.UNSET }
640
+ });
641
+ const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
642
+ mirrorTraceExportToLocalDebugger(body, {
643
+ baseUrl: this.localDebuggerUrl,
644
+ debug: false,
645
+ sdkName: this.sdkName
646
+ });
647
+ }
648
+ return span;
603
649
  }
604
650
  endSpan(span, extra) {
605
651
  var _a, _b;
@@ -622,6 +668,14 @@ var TraceShipper = class {
622
668
  status
623
669
  });
624
670
  this.enqueue(otlp);
671
+ if (this.localDebuggerUrl) {
672
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
673
+ mirrorTraceExportToLocalDebugger(body, {
674
+ baseUrl: this.localDebuggerUrl,
675
+ debug: false,
676
+ sdkName: this.sdkName
677
+ });
678
+ }
625
679
  }
626
680
  createSpan(args) {
627
681
  var _a;
@@ -639,6 +693,14 @@ var TraceShipper = class {
639
693
  status: args.status
640
694
  });
641
695
  this.enqueue(otlp);
696
+ if (this.localDebuggerUrl) {
697
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
698
+ mirrorTraceExportToLocalDebugger(body, {
699
+ baseUrl: this.localDebuggerUrl,
700
+ debug: false,
701
+ sdkName: this.sdkName
702
+ });
703
+ }
642
704
  }
643
705
  enqueue(span) {
644
706
  if (!this.enabled) return;
@@ -713,6 +775,10 @@ var TraceShipper = class {
713
775
  }
714
776
  };
715
777
 
778
+ // ../core/dist/index.node.js
779
+ var import_async_hooks = require("async_hooks");
780
+ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
781
+
716
782
  // src/shipper.ts
717
783
  var EventShipper2 = class extends EventShipper {
718
784
  constructor(opts) {
@@ -891,7 +957,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
891
957
  const props = event.properties;
892
958
  const info = props["info"];
893
959
  const sessionID = (_b = (_a = props["sessionID"]) != null ? _a : info == null ? void 0 : info["id"]) != null ? _b : props["id"];
894
- const sessionEventTypes = ["session.created", "session.idle", "session.deleted", "session.error"];
960
+ const sessionEventTypes = ["session.created", "session.compacted", "session.idle", "session.deleted", "session.error"];
895
961
  if (sessionEventTypes.includes(event.type) && (sessionID === void 0 || sessionID === "")) {
896
962
  throw new Error(`event ${event.type}: missing sessionID in properties`);
897
963
  }
@@ -906,6 +972,10 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
906
972
  }
907
973
  state.parentId = parentID;
908
974
  if (state.parentId) mapChildSessionToParent.set(sessionID, { parentId: state.parentId });
975
+ } else if (event.type === "session.compacted") {
976
+ const state = sessions.get(String(sessionID));
977
+ if (!state) return;
978
+ state.isCompacting = true;
909
979
  } else if (event.type === "message.part.updated") {
910
980
  const part = props["part"];
911
981
  if (part == null || typeof part !== "object")
@@ -1015,12 +1085,19 @@ type: ${name != null ? name : "UnknownError"}`;
1015
1085
  attrString("ai.response.text", reasoningText),
1016
1086
  attrString("gen_ai.system", providerID),
1017
1087
  attrString("gen_ai.request.model", modelID),
1088
+ attrString("gen_ai.response.model", modelID),
1018
1089
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1019
1090
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1020
1091
  ];
1021
1092
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1022
1093
  llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1023
1094
  }
1095
+ if (state.currentSystemPrompt) {
1096
+ llmAttrs.push(
1097
+ attrString("gen_ai.prompt.0.role", "system"),
1098
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1099
+ );
1100
+ }
1024
1101
  const llmParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : state.currentRootSpan.ids;
1025
1102
  const llmSpan = traceShipper.startSpan({
1026
1103
  name: modelName,
@@ -1037,6 +1114,7 @@ type: ${name != null ? name : "UnknownError"}`;
1037
1114
  }
1038
1115
  if (outputText === void 0) return;
1039
1116
  const rootSpan = state.currentRootSpan;
1117
+ const isCompaction = state.isCompacting === true;
1040
1118
  if (state.parentId) {
1041
1119
  const llmAttrs = [
1042
1120
  attrString("ai.operationId", "generateText"),
@@ -1044,12 +1122,19 @@ type: ${name != null ? name : "UnknownError"}`;
1044
1122
  attrString("ai.response.text", outputText),
1045
1123
  attrString("gen_ai.system", providerID),
1046
1124
  attrString("gen_ai.request.model", modelID),
1125
+ attrString("gen_ai.response.model", modelID),
1047
1126
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1048
1127
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1049
1128
  ];
1050
1129
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1051
1130
  llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1052
1131
  }
1132
+ if (state.currentSystemPrompt) {
1133
+ llmAttrs.push(
1134
+ attrString("gen_ai.prompt.0.role", "system"),
1135
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1136
+ );
1137
+ }
1053
1138
  if (!state.parentTaskSpanIds) {
1054
1139
  traceShipper.endSpan(rootSpan, {
1055
1140
  error: "Missing strict parent task for child session"
@@ -1077,23 +1162,36 @@ type: ${name != null ? name : "UnknownError"}`;
1077
1162
  error: errorForSpan
1078
1163
  });
1079
1164
  } else {
1080
- rootSpan.name = modelName;
1165
+ rootSpan.name = isCompaction ? "ai.compaction" : modelName;
1081
1166
  const rootAttrs = [
1082
1167
  attrString("ai.operationId", "generateText"),
1083
1168
  attrString("ai.prompt", state.currentInput),
1084
1169
  attrString("ai.response.text", outputText),
1085
1170
  attrString("gen_ai.system", providerID),
1086
1171
  attrString("gen_ai.request.model", modelID),
1172
+ attrString("gen_ai.response.model", modelID),
1087
1173
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1088
1174
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1089
1175
  ];
1176
+ if (isCompaction) {
1177
+ rootAttrs.push(attrString("is_compaction", "true"));
1178
+ }
1090
1179
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1091
1180
  rootAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1092
1181
  }
1182
+ if (state.currentSystemPrompt) {
1183
+ rootAttrs.push(
1184
+ attrString("gen_ai.prompt.0.role", "system"),
1185
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1186
+ );
1187
+ }
1093
1188
  traceShipper.endSpan(rootSpan, {
1094
1189
  attributes: rootAttrs,
1095
1190
  error: errorForSpan
1096
1191
  });
1192
+ if (isCompaction) {
1193
+ state.isCompacting = false;
1194
+ }
1097
1195
  }
1098
1196
  state.currentRootSpan = void 0;
1099
1197
  const hasAssistantResponse = outputText.trim().length > 0;
@@ -1108,7 +1206,8 @@ type: ${name != null ? name : "UnknownError"}`;
1108
1206
  model: modelName,
1109
1207
  output: outputText,
1110
1208
  properties: {
1111
- plugin_version: PLUGIN_VERSION
1209
+ plugin_version: PLUGIN_VERSION,
1210
+ ...isCompaction ? { is_compaction: true } : {}
1112
1211
  }
1113
1212
  });
1114
1213
  }
@@ -1125,6 +1224,7 @@ type: ${name != null ? name : "UnknownError"}`;
1125
1224
  if (state.currentEventId) {
1126
1225
  state.currentEventId = void 0;
1127
1226
  }
1227
+ state.isCompacting = false;
1128
1228
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1129
1229
  if (state.parentId) {
1130
1230
  cleanupSessionParentMap(String(sessionID));
@@ -1252,7 +1352,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1252
1352
  }
1253
1353
  state.currentEventId = generateId();
1254
1354
  state.currentRootSpan = traceShipper.startSpan({
1255
- name: "ai.event",
1355
+ name: state.isCompacting ? "ai.compaction" : "ai.event",
1256
1356
  eventId: state.currentEventId,
1257
1357
  attributes: [
1258
1358
  attrString("workspace", worktree),
@@ -1408,6 +1508,46 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1408
1508
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1409
1509
  );
1410
1510
  }
1511
+ },
1512
+ // ------------------------------------------------------------------
1513
+ // experimental.session.compacting — fires before compaction LLM call
1514
+ // ------------------------------------------------------------------
1515
+ "experimental.session.compacting": async (input, _output) => {
1516
+ try {
1517
+ const sessionID = input.sessionID;
1518
+ if (!sessionID) return;
1519
+ const state = sessions.get(sessionID);
1520
+ if (state) {
1521
+ state.isCompacting = true;
1522
+ }
1523
+ } catch (err) {
1524
+ console.log(
1525
+ `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1526
+ );
1527
+ }
1528
+ },
1529
+ // ------------------------------------------------------------------
1530
+ // experimental.chat.system.transform — capture system prompt (read-only)
1531
+ // ------------------------------------------------------------------
1532
+ "experimental.chat.system.transform": async (input, output) => {
1533
+ try {
1534
+ if (!config.captureSystemPrompt) return;
1535
+ const sessionID = input.sessionID;
1536
+ if (!sessionID) return;
1537
+ let state = sessions.get(sessionID);
1538
+ if (!state) {
1539
+ state = createSessionState(sessionID);
1540
+ sessions.set(sessionID, state);
1541
+ }
1542
+ const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1543
+ const TRUNCATION_MARKER = "\n...[truncated]";
1544
+ const joined = output.system.join("\n\n");
1545
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
1546
+ } catch (err) {
1547
+ console.log(
1548
+ `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1549
+ );
1550
+ }
1411
1551
  }
1412
1552
  };
1413
1553
  }
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from "fs";
10
10
  import { homedir } from "os";
11
11
  import { join } from "path";
12
12
  function loadConfig(projectDirectory) {
13
- var _a, _b, _c, _d, _e, _f;
13
+ var _a, _b, _c, _d, _e, _f, _g;
14
14
  let merged = {};
15
15
  const configPaths = [
16
16
  join(homedir(), ".config", "opencode", "raindrop.json"),
@@ -39,6 +39,7 @@ function loadConfig(projectDirectory) {
39
39
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
40
40
  eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
41
41
  debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
42
+ captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
42
43
  eventMetadata
43
44
  };
44
45
  }
@@ -46,7 +47,7 @@ function loadConfig(projectDirectory) {
46
47
  // package.json
47
48
  var package_default = {
48
49
  name: "@raindrop-ai/opencode-plugin",
49
- version: "0.0.6",
50
+ version: "0.0.8",
50
51
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
51
52
  type: "module",
52
53
  main: "dist/index.cjs",
@@ -70,8 +71,8 @@ var package_default = {
70
71
  test: "cd tests && pnpm test"
71
72
  },
72
73
  peerDependencies: {
73
- "@opencode-ai/plugin": ">=1.2.0",
74
- "@opencode-ai/sdk": ">=1.2.0"
74
+ "@opencode-ai/plugin": ">=1.3.0",
75
+ "@opencode-ai/sdk": ">=1.3.0"
75
76
  },
76
77
  peerDependenciesMeta: {
77
78
  "@opencode-ai/sdk": {
@@ -80,8 +81,8 @@ var package_default = {
80
81
  },
81
82
  devDependencies: {
82
83
  "@raindrop-ai/core": "workspace:*",
83
- "@opencode-ai/plugin": "^1.2.0",
84
- "@opencode-ai/sdk": "^1.2.0",
84
+ "@opencode-ai/plugin": "^1.3.3",
85
+ "@opencode-ai/sdk": "^1.3.3",
85
86
  "@types/node": "^20.11.17",
86
87
  tsup: "^8.4.0",
87
88
  typescript: "^5.3.3"
@@ -111,7 +112,7 @@ var package_default = {
111
112
  var PLUGIN_NAME = package_default.name;
112
113
  var PLUGIN_VERSION = package_default.version;
113
114
 
114
- // ../core/dist/chunk-FTBZHS25.js
115
+ // ../core/dist/chunk-4UCYIEH4.js
115
116
  function getCrypto() {
116
117
  const c = globalThis.crypto;
117
118
  return c;
@@ -356,6 +357,7 @@ var EventShipper = class {
356
357
  console.log(`${this.prefix} queue patch`, {
357
358
  eventId,
358
359
  userId: patch.userId,
360
+ convoId: patch.convoId,
359
361
  eventName: patch.eventName,
360
362
  hasInput: typeof patch.input === "string" && patch.input.length > 0,
361
363
  hasOutput: typeof patch.output === "string" && patch.output.length > 0,
@@ -484,16 +486,18 @@ var EventShipper = class {
484
486
  return;
485
487
  }
486
488
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
489
+ const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
490
+ const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
487
491
  const payload = {
488
492
  event_id: eventId,
489
493
  user_id: userId,
490
494
  event: eventName,
491
- timestamp: (_f = accumulated.timestamp) != null ? _f : (/* @__PURE__ */ new Date()).toISOString(),
495
+ timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
492
496
  ai_data: {
493
497
  input: accumulated.input,
494
498
  output: accumulated.output,
495
499
  model: accumulated.model,
496
- convo_id: (_g = accumulated.convoId) != null ? _g : sticky.convoId
500
+ convo_id: convoId
497
501
  },
498
502
  properties: {
499
503
  ...restProperties,
@@ -501,7 +505,7 @@ var EventShipper = class {
501
505
  $context: this.context
502
506
  },
503
507
  attachments: accumulated.attachments,
504
- is_pending: ((_i = (_h = accumulated.isPending) != null ? _h : sticky.isPending) != null ? _i : true) !== false
508
+ is_pending: isPending
505
509
  };
506
510
  const url = `${this.baseUrl}events/track_partial`;
507
511
  if (this.debug) {
@@ -509,7 +513,8 @@ var EventShipper = class {
509
513
  eventId,
510
514
  eventName,
511
515
  userId,
512
- isPending: payload.is_pending,
516
+ convoId,
517
+ isPending,
513
518
  inputPreview: typeof accumulated.input === "string" ? accumulated.input.slice(0, 120) : void 0,
514
519
  outputPreview: typeof accumulated.output === "string" ? accumulated.output.slice(0, 120) : void 0,
515
520
  attachments: (_k = (_j = accumulated.attachments) == null ? void 0 : _j.length) != null ? _k : 0,
@@ -541,17 +546,33 @@ var EventShipper = class {
541
546
  } finally {
542
547
  this.inFlight.delete(p);
543
548
  }
544
- const isPending = payload.is_pending;
545
549
  if (!isPending) {
546
550
  this.sticky.delete(eventId);
547
551
  }
548
552
  }
549
553
  };
554
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
555
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
556
+ var _a, _b, _c;
557
+ const resolved = (_b = baseUrl != null ? baseUrl : typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[LOCAL_DEBUGGER_ENV_VAR] : void 0) != null ? _b : null;
558
+ return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
559
+ }
560
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
561
+ var _a;
562
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
563
+ if (!baseUrl) return;
564
+ void postJson(`${baseUrl}traces`, body, {}, {
565
+ maxAttempts: 1,
566
+ debug: (_a = options.debug) != null ? _a : false,
567
+ sdkName: options.sdkName
568
+ }).catch(() => {
569
+ });
570
+ }
550
571
  var TraceShipper = class {
551
572
  constructor(opts) {
552
573
  this.queue = [];
553
574
  this.inFlight = /* @__PURE__ */ new Set();
554
- var _a, _b, _c, _d, _e, _f, _g, _h;
575
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
555
576
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
556
577
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
557
578
  this.enabled = opts.enabled !== false;
@@ -564,6 +585,13 @@ var TraceShipper = class {
564
585
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
565
586
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
566
587
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
588
+ const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
589
+ if (localDebugger) {
590
+ this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
591
+ if (this.debug) {
592
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
593
+ }
594
+ }
567
595
  }
568
596
  isDebugEnabled() {
569
597
  return this.debug;
@@ -580,7 +608,25 @@ var TraceShipper = class {
580
608
  attrString("ai.operationId", args.operationId)
581
609
  ];
582
610
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
583
- return { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
611
+ const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
612
+ if (this.localDebuggerUrl) {
613
+ const openSpan = buildOtlpSpan({
614
+ ids: span.ids,
615
+ name: span.name,
616
+ startTimeUnixNano: span.startTimeUnixNano,
617
+ endTimeUnixNano: span.startTimeUnixNano,
618
+ // placeholder — will be updated on endSpan
619
+ attributes: span.attributes,
620
+ status: { code: SpanStatusCode.UNSET }
621
+ });
622
+ const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
623
+ mirrorTraceExportToLocalDebugger(body, {
624
+ baseUrl: this.localDebuggerUrl,
625
+ debug: false,
626
+ sdkName: this.sdkName
627
+ });
628
+ }
629
+ return span;
584
630
  }
585
631
  endSpan(span, extra) {
586
632
  var _a, _b;
@@ -603,6 +649,14 @@ var TraceShipper = class {
603
649
  status
604
650
  });
605
651
  this.enqueue(otlp);
652
+ if (this.localDebuggerUrl) {
653
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
654
+ mirrorTraceExportToLocalDebugger(body, {
655
+ baseUrl: this.localDebuggerUrl,
656
+ debug: false,
657
+ sdkName: this.sdkName
658
+ });
659
+ }
606
660
  }
607
661
  createSpan(args) {
608
662
  var _a;
@@ -620,6 +674,14 @@ var TraceShipper = class {
620
674
  status: args.status
621
675
  });
622
676
  this.enqueue(otlp);
677
+ if (this.localDebuggerUrl) {
678
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
679
+ mirrorTraceExportToLocalDebugger(body, {
680
+ baseUrl: this.localDebuggerUrl,
681
+ debug: false,
682
+ sdkName: this.sdkName
683
+ });
684
+ }
623
685
  }
624
686
  enqueue(span) {
625
687
  if (!this.enabled) return;
@@ -694,6 +756,10 @@ var TraceShipper = class {
694
756
  }
695
757
  };
696
758
 
759
+ // ../core/dist/index.node.js
760
+ import { AsyncLocalStorage } from "async_hooks";
761
+ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
762
+
697
763
  // src/shipper.ts
698
764
  var EventShipper2 = class extends EventShipper {
699
765
  constructor(opts) {
@@ -872,7 +938,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
872
938
  const props = event.properties;
873
939
  const info = props["info"];
874
940
  const sessionID = (_b = (_a = props["sessionID"]) != null ? _a : info == null ? void 0 : info["id"]) != null ? _b : props["id"];
875
- const sessionEventTypes = ["session.created", "session.idle", "session.deleted", "session.error"];
941
+ const sessionEventTypes = ["session.created", "session.compacted", "session.idle", "session.deleted", "session.error"];
876
942
  if (sessionEventTypes.includes(event.type) && (sessionID === void 0 || sessionID === "")) {
877
943
  throw new Error(`event ${event.type}: missing sessionID in properties`);
878
944
  }
@@ -887,6 +953,10 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
887
953
  }
888
954
  state.parentId = parentID;
889
955
  if (state.parentId) mapChildSessionToParent.set(sessionID, { parentId: state.parentId });
956
+ } else if (event.type === "session.compacted") {
957
+ const state = sessions.get(String(sessionID));
958
+ if (!state) return;
959
+ state.isCompacting = true;
890
960
  } else if (event.type === "message.part.updated") {
891
961
  const part = props["part"];
892
962
  if (part == null || typeof part !== "object")
@@ -996,12 +1066,19 @@ type: ${name != null ? name : "UnknownError"}`;
996
1066
  attrString("ai.response.text", reasoningText),
997
1067
  attrString("gen_ai.system", providerID),
998
1068
  attrString("gen_ai.request.model", modelID),
1069
+ attrString("gen_ai.response.model", modelID),
999
1070
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1000
1071
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1001
1072
  ];
1002
1073
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1003
1074
  llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1004
1075
  }
1076
+ if (state.currentSystemPrompt) {
1077
+ llmAttrs.push(
1078
+ attrString("gen_ai.prompt.0.role", "system"),
1079
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1080
+ );
1081
+ }
1005
1082
  const llmParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : state.currentRootSpan.ids;
1006
1083
  const llmSpan = traceShipper.startSpan({
1007
1084
  name: modelName,
@@ -1018,6 +1095,7 @@ type: ${name != null ? name : "UnknownError"}`;
1018
1095
  }
1019
1096
  if (outputText === void 0) return;
1020
1097
  const rootSpan = state.currentRootSpan;
1098
+ const isCompaction = state.isCompacting === true;
1021
1099
  if (state.parentId) {
1022
1100
  const llmAttrs = [
1023
1101
  attrString("ai.operationId", "generateText"),
@@ -1025,12 +1103,19 @@ type: ${name != null ? name : "UnknownError"}`;
1025
1103
  attrString("ai.response.text", outputText),
1026
1104
  attrString("gen_ai.system", providerID),
1027
1105
  attrString("gen_ai.request.model", modelID),
1106
+ attrString("gen_ai.response.model", modelID),
1028
1107
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1029
1108
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1030
1109
  ];
1031
1110
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1032
1111
  llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1033
1112
  }
1113
+ if (state.currentSystemPrompt) {
1114
+ llmAttrs.push(
1115
+ attrString("gen_ai.prompt.0.role", "system"),
1116
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1117
+ );
1118
+ }
1034
1119
  if (!state.parentTaskSpanIds) {
1035
1120
  traceShipper.endSpan(rootSpan, {
1036
1121
  error: "Missing strict parent task for child session"
@@ -1058,23 +1143,36 @@ type: ${name != null ? name : "UnknownError"}`;
1058
1143
  error: errorForSpan
1059
1144
  });
1060
1145
  } else {
1061
- rootSpan.name = modelName;
1146
+ rootSpan.name = isCompaction ? "ai.compaction" : modelName;
1062
1147
  const rootAttrs = [
1063
1148
  attrString("ai.operationId", "generateText"),
1064
1149
  attrString("ai.prompt", state.currentInput),
1065
1150
  attrString("ai.response.text", outputText),
1066
1151
  attrString("gen_ai.system", providerID),
1067
1152
  attrString("gen_ai.request.model", modelID),
1153
+ attrString("gen_ai.response.model", modelID),
1068
1154
  attrInt("gen_ai.usage.input_tokens", inputTokens),
1069
1155
  attrInt("gen_ai.usage.output_tokens", outputTokens)
1070
1156
  ];
1157
+ if (isCompaction) {
1158
+ rootAttrs.push(attrString("is_compaction", "true"));
1159
+ }
1071
1160
  if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
1072
1161
  rootAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
1073
1162
  }
1163
+ if (state.currentSystemPrompt) {
1164
+ rootAttrs.push(
1165
+ attrString("gen_ai.prompt.0.role", "system"),
1166
+ attrString("gen_ai.prompt.0.content", state.currentSystemPrompt)
1167
+ );
1168
+ }
1074
1169
  traceShipper.endSpan(rootSpan, {
1075
1170
  attributes: rootAttrs,
1076
1171
  error: errorForSpan
1077
1172
  });
1173
+ if (isCompaction) {
1174
+ state.isCompacting = false;
1175
+ }
1078
1176
  }
1079
1177
  state.currentRootSpan = void 0;
1080
1178
  const hasAssistantResponse = outputText.trim().length > 0;
@@ -1089,7 +1187,8 @@ type: ${name != null ? name : "UnknownError"}`;
1089
1187
  model: modelName,
1090
1188
  output: outputText,
1091
1189
  properties: {
1092
- plugin_version: PLUGIN_VERSION
1190
+ plugin_version: PLUGIN_VERSION,
1191
+ ...isCompaction ? { is_compaction: true } : {}
1093
1192
  }
1094
1193
  });
1095
1194
  }
@@ -1106,6 +1205,7 @@ type: ${name != null ? name : "UnknownError"}`;
1106
1205
  if (state.currentEventId) {
1107
1206
  state.currentEventId = void 0;
1108
1207
  }
1208
+ state.isCompacting = false;
1109
1209
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1110
1210
  if (state.parentId) {
1111
1211
  cleanupSessionParentMap(String(sessionID));
@@ -1233,7 +1333,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1233
1333
  }
1234
1334
  state.currentEventId = generateId();
1235
1335
  state.currentRootSpan = traceShipper.startSpan({
1236
- name: "ai.event",
1336
+ name: state.isCompacting ? "ai.compaction" : "ai.event",
1237
1337
  eventId: state.currentEventId,
1238
1338
  attributes: [
1239
1339
  attrString("workspace", worktree),
@@ -1389,6 +1489,46 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1389
1489
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1390
1490
  );
1391
1491
  }
1492
+ },
1493
+ // ------------------------------------------------------------------
1494
+ // experimental.session.compacting — fires before compaction LLM call
1495
+ // ------------------------------------------------------------------
1496
+ "experimental.session.compacting": async (input, _output) => {
1497
+ try {
1498
+ const sessionID = input.sessionID;
1499
+ if (!sessionID) return;
1500
+ const state = sessions.get(sessionID);
1501
+ if (state) {
1502
+ state.isCompacting = true;
1503
+ }
1504
+ } catch (err) {
1505
+ console.log(
1506
+ `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1507
+ );
1508
+ }
1509
+ },
1510
+ // ------------------------------------------------------------------
1511
+ // experimental.chat.system.transform — capture system prompt (read-only)
1512
+ // ------------------------------------------------------------------
1513
+ "experimental.chat.system.transform": async (input, output) => {
1514
+ try {
1515
+ if (!config.captureSystemPrompt) return;
1516
+ const sessionID = input.sessionID;
1517
+ if (!sessionID) return;
1518
+ let state = sessions.get(sessionID);
1519
+ if (!state) {
1520
+ state = createSessionState(sessionID);
1521
+ sessions.set(sessionID, state);
1522
+ }
1523
+ const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1524
+ const TRUNCATION_MARKER = "\n...[truncated]";
1525
+ const joined = output.system.join("\n\n");
1526
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
1527
+ } catch (err) {
1528
+ console.log(
1529
+ `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1530
+ );
1531
+ }
1392
1532
  }
1393
1533
  };
1394
1534
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raindrop-ai/opencode-plugin",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Raindrop observability plugin for OpenCode — automatic session/event/span tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -24,8 +24,8 @@
24
24
  "test": "cd tests && pnpm test"
25
25
  },
26
26
  "peerDependencies": {
27
- "@opencode-ai/plugin": ">=1.2.0",
28
- "@opencode-ai/sdk": ">=1.2.0"
27
+ "@opencode-ai/plugin": ">=1.3.0",
28
+ "@opencode-ai/sdk": ">=1.3.0"
29
29
  },
30
30
  "peerDependenciesMeta": {
31
31
  "@opencode-ai/sdk": {
@@ -34,8 +34,8 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "@raindrop-ai/core": "workspace:*",
37
- "@opencode-ai/plugin": "^1.2.0",
38
- "@opencode-ai/sdk": "^1.2.0",
37
+ "@opencode-ai/plugin": "^1.3.3",
38
+ "@opencode-ai/sdk": "^1.3.3",
39
39
  "@types/node": "^20.11.17",
40
40
  "tsup": "^8.4.0",
41
41
  "typescript": "^5.3.3"