@raindrop-ai/opencode-plugin 0.0.19 → 0.1.1

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/index.cjs CHANGED
@@ -24,7 +24,7 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
- // ../core/dist/chunk-267IXHO5.js
27
+ // ../core/dist/chunk-QTUH3BHZ.js
28
28
  function getCrypto() {
29
29
  const c = globalThis.crypto;
30
30
  return c;
@@ -417,19 +417,52 @@ function projectIdHeaders(projectId) {
417
417
  var SHUTDOWN_DEADLINE_MS = 1e4;
418
418
  var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
419
419
  function mergePatches(target, source) {
420
- var _a, _b, _c, _d, _e, _f;
420
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
421
421
  const out = { ...target, ...source };
422
422
  if (target.properties || source.properties) {
423
423
  out.properties = { ...(_a = target.properties) != null ? _a : {}, ...(_b = source.properties) != null ? _b : {} };
424
424
  }
425
+ if (target.usage || source.usage) {
426
+ out.usage = { ...(_c = target.usage) != null ? _c : {} };
427
+ if (((_d = source.usage) == null ? void 0 : _d.promptTokens) !== void 0) {
428
+ out.usage.promptTokens = source.usage.promptTokens;
429
+ }
430
+ if (((_e = source.usage) == null ? void 0 : _e.completionTokens) !== void 0) {
431
+ out.usage.completionTokens = source.usage.completionTokens;
432
+ }
433
+ }
434
+ if (source.error === void 0) {
435
+ out.error = target.error;
436
+ }
425
437
  if (target.featureFlags || source.featureFlags) {
426
- out.featureFlags = { ...(_c = target.featureFlags) != null ? _c : {}, ...(_d = source.featureFlags) != null ? _d : {} };
438
+ out.featureFlags = { ...(_f = target.featureFlags) != null ? _f : {}, ...(_g = source.featureFlags) != null ? _g : {} };
427
439
  }
428
440
  if (target.attachments || source.attachments) {
429
- out.attachments = [...(_e = target.attachments) != null ? _e : [], ...(_f = source.attachments) != null ? _f : []];
441
+ out.attachments = [...(_h = target.attachments) != null ? _h : [], ...(_i = source.attachments) != null ? _i : []];
430
442
  }
431
443
  return out;
432
444
  }
445
+ function isErrorLike(value) {
446
+ return value !== null && typeof value === "object";
447
+ }
448
+ function eventTelemetryProperties(patch) {
449
+ var _a, _b;
450
+ const properties = {};
451
+ if (((_a = patch.usage) == null ? void 0 : _a.promptTokens) !== void 0 && Number.isFinite(patch.usage.promptTokens)) {
452
+ properties["ai.usage.prompt_tokens"] = patch.usage.promptTokens;
453
+ }
454
+ if (((_b = patch.usage) == null ? void 0 : _b.completionTokens) !== void 0 && Number.isFinite(patch.usage.completionTokens)) {
455
+ properties["ai.usage.completion_tokens"] = patch.usage.completionTokens;
456
+ }
457
+ if (patch.error !== void 0 && patch.error !== null) {
458
+ const constructorName = patch.error instanceof Error && patch.error.constructor !== Error ? patch.error.constructor.name : void 0;
459
+ const errorType = patch.error instanceof Error ? patch.error.name && patch.error.name !== "Error" ? patch.error.name : constructorName || patch.error.name || "Error" : isErrorLike(patch.error) && typeof patch.error.name === "string" && patch.error.name.length > 0 ? patch.error.name : "Error";
460
+ const errorMessage = patch.error instanceof Error ? patch.error.message : isErrorLike(patch.error) && typeof patch.error.message === "string" ? patch.error.message : String(patch.error);
461
+ properties["error.type"] = errorType;
462
+ properties["error.message"] = errorMessage;
463
+ }
464
+ return properties;
465
+ }
433
466
  var EventShipper = class {
434
467
  constructor(opts) {
435
468
  this.buffers = /* @__PURE__ */ new Map();
@@ -678,6 +711,11 @@ var EventShipper = class {
678
711
  return;
679
712
  }
680
713
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
714
+ let telemetryProperties = {};
715
+ try {
716
+ telemetryProperties = eventTelemetryProperties(accumulated);
717
+ } catch (e) {
718
+ }
681
719
  const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
682
720
  const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
683
721
  const payload = {
@@ -687,12 +725,13 @@ var EventShipper = class {
687
725
  timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
688
726
  ai_data: {
689
727
  input: accumulated.input,
690
- output: accumulated.output,
728
+ output: accumulated.error !== void 0 && accumulated.error !== null ? void 0 : accumulated.output,
691
729
  model: accumulated.model,
692
730
  convo_id: convoId
693
731
  },
694
732
  properties: {
695
733
  ...restProperties,
734
+ ...telemetryProperties,
696
735
  ...wizardSession ? { "raindrop.wizardSession": wizardSession } : {},
697
736
  $context: this.context
698
737
  },
@@ -850,6 +889,13 @@ function redactJsonAttributeValue(key, value) {
850
889
  if (scrubbedJson === json) return void 0;
851
890
  return { stringValue: scrubbedJson };
852
891
  }
892
+ function identityAttrs(args) {
893
+ return [
894
+ attrString("ai.telemetry.metadata.raindrop.ai.userId", args.userId),
895
+ attrString("ai.telemetry.metadata.raindrop.convoId", args.convoId),
896
+ attrString("ai.telemetry.metadata.raindrop.eventName", args.eventName)
897
+ ];
898
+ }
853
899
  function applyOtelSpanAttributeLimit(limit) {
854
900
  var _a, _b;
855
901
  try {
@@ -974,6 +1020,7 @@ var TraceShipper = class {
974
1020
  const started = (_a = args.startTimeUnixNano) != null ? _a : nowUnixNanoString();
975
1021
  const attrs = [
976
1022
  attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1023
+ ...identityAttrs(args),
977
1024
  attrString("ai.operationId", args.operationId)
978
1025
  ];
979
1026
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
@@ -1029,7 +1076,8 @@ var TraceShipper = class {
1029
1076
  var _a;
1030
1077
  const ids = createSpanIds(args.parent);
1031
1078
  const attrs = [
1032
- attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
1079
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1080
+ ...identityAttrs(args)
1033
1081
  ];
1034
1082
  if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
1035
1083
  const otlp = buildOtlpSpan({
@@ -1248,7 +1296,7 @@ function resolveLocalWorkshopUrl(fileValue) {
1248
1296
  // package.json
1249
1297
  var package_default = {
1250
1298
  name: "@raindrop-ai/opencode-plugin",
1251
- version: "0.0.19",
1299
+ version: "0.1.1",
1252
1300
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1253
1301
  type: "module",
1254
1302
  main: "dist/index.js",
@@ -1528,6 +1576,8 @@ function createSessionState(sessionId) {
1528
1576
  return {
1529
1577
  sessionId,
1530
1578
  currentInput: "",
1579
+ turnOutputParts: [],
1580
+ turnOutputTokens: 0,
1531
1581
  outputParts: /* @__PURE__ */ new Map(),
1532
1582
  reasoningParts: /* @__PURE__ */ new Map(),
1533
1583
  toolSpanStarts: /* @__PURE__ */ new Map(),
@@ -1633,7 +1683,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1633
1683
  // event — session lifecycle + streaming parts
1634
1684
  // ------------------------------------------------------------------
1635
1685
  event: async ({ event }) => {
1636
- var _a, _b, _c, _d, _e, _f, _g;
1686
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1637
1687
  try {
1638
1688
  const props = event.properties;
1639
1689
  const info = props["info"];
@@ -1758,6 +1808,10 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1758
1808
  const outputText = state.outputParts.get(messageId);
1759
1809
  const reasoningText = state.reasoningParts.get(messageId);
1760
1810
  const isToolCallsCompletion = finishReason === "tool-calls";
1811
+ if (outputText !== void 0 && outputText.trim().length > 0) {
1812
+ state.turnOutputParts.push(outputText);
1813
+ }
1814
+ state.turnOutputTokens += outputTokens;
1761
1815
  const hasIntermediateReasoning = reasoningText !== void 0 && reasoningText.trim().length > 0;
1762
1816
  if (outputText === void 0 && !(isToolCallsCompletion && hasIntermediateReasoning))
1763
1817
  return;
@@ -1812,6 +1866,7 @@ type: ${name != null ? name : "UnknownError"}`;
1812
1866
  return;
1813
1867
  }
1814
1868
  if (outputText === void 0) return;
1869
+ const turnOutputText = capText2(state.turnOutputParts.join("\n\n"));
1815
1870
  const rootSpan = state.currentRootSpan;
1816
1871
  const isCompaction = state.isCompacting === true;
1817
1872
  if (state.parentId) {
@@ -1823,7 +1878,7 @@ type: ${name != null ? name : "UnknownError"}`;
1823
1878
  "ai.prompt.messages",
1824
1879
  buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1825
1880
  ),
1826
- attrString("ai.response.text", outputText),
1881
+ attrString("ai.response.text", turnOutputText),
1827
1882
  attrString("gen_ai.system", providerID),
1828
1883
  attrString("gen_ai.request.model", modelID),
1829
1884
  attrString("gen_ai.response.model", modelID),
@@ -1875,7 +1930,7 @@ type: ${name != null ? name : "UnknownError"}`;
1875
1930
  "ai.prompt.messages",
1876
1931
  buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1877
1932
  ),
1878
- attrString("ai.response.text", outputText),
1933
+ attrString("ai.response.text", turnOutputText),
1879
1934
  attrString("gen_ai.system", providerID),
1880
1935
  attrString("gen_ai.request.model", modelID),
1881
1936
  attrString("gen_ai.response.model", modelID),
@@ -1904,8 +1959,8 @@ type: ${name != null ? name : "UnknownError"}`;
1904
1959
  }
1905
1960
  }
1906
1961
  state.currentRootSpan = void 0;
1907
- const hasAssistantResponse = outputText.trim().length > 0;
1908
- if (!hasAssistantResponse) {
1962
+ const hasAssistantResponse = turnOutputText.trim().length > 0;
1963
+ if (!hasAssistantResponse && !msgError) {
1909
1964
  await traceShipper.flush();
1910
1965
  state.currentEventId = void 0;
1911
1966
  return;
@@ -1914,7 +1969,15 @@ type: ${name != null ? name : "UnknownError"}`;
1914
1969
  await eventShipper.finish(state.currentEventId, {
1915
1970
  userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
1916
1971
  model: modelID,
1917
- output: outputText,
1972
+ output: msgError ? void 0 : turnOutputText,
1973
+ usage: {
1974
+ promptTokens: inputTokens,
1975
+ completionTokens: state.turnOutputTokens
1976
+ },
1977
+ error: msgError ? {
1978
+ name: msgError.name,
1979
+ message: (_g = (_f = (_e = msgError.data) == null ? void 0 : _e.message) != null ? _f : msgError.name) != null ? _g : "OpenCode request failed"
1980
+ } : void 0,
1918
1981
  properties: {
1919
1982
  plugin_version: PLUGIN_VERSION,
1920
1983
  message_id: messageId,
@@ -1958,7 +2021,7 @@ type: ${name != null ? name : "UnknownError"}`;
1958
2021
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1959
2022
  sessions.delete(String(sessionID));
1960
2023
  } else if (event.type === "session.error") {
1961
- const errorSessionID = (_e = props["sessionID"]) != null ? _e : sessionID;
2024
+ const errorSessionID = (_h = props["sessionID"]) != null ? _h : sessionID;
1962
2025
  if (errorSessionID === void 0 || errorSessionID === "")
1963
2026
  throw new Error("session.error: missing sessionID");
1964
2027
  const state = sessions.get(String(errorSessionID));
@@ -1966,7 +2029,7 @@ type: ${name != null ? name : "UnknownError"}`;
1966
2029
  const errorObj = props["error"];
1967
2030
  if (errorObj == null) throw new Error("session.error: missing error object");
1968
2031
  const errorName = errorObj.name;
1969
- const errorMessage = (_g = (_f = errorObj.data) == null ? void 0 : _f.message) != null ? _g : errorObj.name;
2032
+ const errorMessage = (_j = (_i = errorObj.data) == null ? void 0 : _i.message) != null ? _j : errorObj.name;
1970
2033
  if (errorName == null && errorMessage == null)
1971
2034
  throw new Error("session.error: error object must have name or data.message");
1972
2035
  const errorStr = `${errorMessage != null ? errorMessage : errorName}
@@ -2043,6 +2106,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2043
2106
  };
2044
2107
  const userText = capText2(textParts.join("\n"));
2045
2108
  state.currentInput = userText;
2109
+ state.turnOutputParts = [];
2110
+ state.turnOutputTokens = 0;
2046
2111
  if (state.parentId) {
2047
2112
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
2048
2113
  return;
@@ -2260,6 +2325,21 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2260
2325
  }
2261
2326
 
2262
2327
  // src/index.ts
2328
+ function createRaindropHandle(eventShipper, traceShipper) {
2329
+ return {
2330
+ users: {
2331
+ async identify(users) {
2332
+ await eventShipper.identify(users);
2333
+ }
2334
+ },
2335
+ async flush() {
2336
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2337
+ },
2338
+ async shutdown() {
2339
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2340
+ }
2341
+ };
2342
+ }
2263
2343
  async function plugin(input) {
2264
2344
  var _a;
2265
2345
  const config = loadConfig(input.directory);
@@ -2305,5 +2385,5 @@ async function plugin(input) {
2305
2385
  eventShipper,
2306
2386
  traceShipper
2307
2387
  );
2308
- return hooks;
2388
+ return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper) };
2309
2389
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,107 @@
1
- import { PluginInput, Hooks } from '@opencode-ai/plugin';
1
+ import { Hooks, PluginInput } from '@opencode-ai/plugin';
2
2
 
3
+ type IdentifyInput = {
4
+ userId: string;
5
+ traits?: Record<string, unknown>;
6
+ };
7
+
8
+ /**
9
+ * Run telemetry egress with OpenTelemetry tracing suppressed.
10
+ *
11
+ * Why this exists
12
+ * ---------------
13
+ * Raindrop integrations ship spans/events over HTTP with the global `fetch`
14
+ * (see {@link ../http.ts `postJson`}). When the host app also runs an OTel
15
+ * fetch/undici instrumentation — e.g. `@vercel/otel`'s `registerOTel`, which
16
+ * every Eve agent installs — that instrumentation wraps *our own* telemetry
17
+ * POSTs in a `fetch POST <endpoint>` span. Those spans are then handed to the
18
+ * very exporter that issued the request, so they get shipped right back to
19
+ * Raindrop and Workshop as standalone "runs" (and, because each export issues
20
+ * another fetch, they feed back on themselves). The result is a run list
21
+ * flooded with `fetch POST .../v1/traces`, `.../events/track_partial` and
22
+ * `.../live` entries that drown out the real agent turns — especially with
23
+ * sub-agents, where every sandbox runs its own instrumentation.
24
+ *
25
+ * The OTel-blessed fix is to mark the active context as "tracing suppressed"
26
+ * around the request; instrumentations check `isTracingSuppressed` and return
27
+ * a no-op span instead of recording one. We do this through a hook stashed on
28
+ * `globalThis` by the Node entrypoint ({@link ../index.node.ts}) so that:
29
+ * - `@opentelemetry/api` / `@opentelemetry/core` stay *optional* — core never
30
+ * hard-depends on them, and the hook is simply absent when they (and thus
31
+ * any instrumentation to suppress) are not installed; and
32
+ * - the browser bundle never pulls in `node:module`, mirroring how core
33
+ * injects `AsyncLocalStorage` via `RAINDROP_ASYNC_LOCAL_STORAGE`.
34
+ *
35
+ * When no hook is present the callback runs unchanged, so suppression is a
36
+ * best-effort no-op rather than a hard requirement.
37
+ */
38
+ /** Hook signature: run `fn` with OTel tracing suppressed, returning its value. */
39
+ type SuppressTracingHook = <T>(fn: () => T) => T;
40
+ declare global {
41
+ var RAINDROP_SUPPRESS_TRACING: SuppressTracingHook | undefined;
42
+ }
43
+
44
+ type ParentSpanContext = {
45
+ traceIdB64: string;
46
+ spanIdB64: string;
47
+ eventId: string;
48
+ };
49
+ interface ContextSpan {
50
+ readonly traceIdB64: string;
51
+ readonly spanIdB64: string;
52
+ readonly eventId: string;
53
+ log?(data: Record<string, unknown>): void;
54
+ }
55
+ interface AsyncLocalStorageLike<T> {
56
+ getStore(): T | undefined;
57
+ run<R>(store: T, callback: () => R): R;
58
+ enterWith?(store: T): void;
59
+ }
60
+ declare abstract class ContextManager {
61
+ abstract getParentSpanIds(): ParentSpanContext | undefined;
62
+ abstract runInContext<R>(span: ContextSpan, callback: () => R): R;
63
+ abstract getCurrentSpan(): ContextSpan | undefined;
64
+ abstract isReady(): boolean;
65
+ }
66
+ declare global {
67
+ var RAINDROP_CONTEXT_MANAGER: (new () => ContextManager) | undefined;
68
+ var RAINDROP_ASYNC_LOCAL_STORAGE: (new <T>() => AsyncLocalStorageLike<T>) | undefined;
69
+ }
70
+
71
+ /**
72
+ * The Raindrop control handle exposed alongside the OpenCode hooks.
73
+ *
74
+ * The plugin's public surface is the framework `Hooks` object, which OpenCode
75
+ * consumes and whose keyspace it owns. Rather than pollute that keyspace with a
76
+ * top-level `users` (as the client-factory wrappers do), the identify surface
77
+ * lives under a namespaced `raindrop` handle — mirroring `@raindrop-ai/eve`'s
78
+ * `raindrop` handle precedent — so application/tool code can attach user traits
79
+ * without waiting for a session hook to fire.
80
+ */
81
+ interface RaindropOpencodeHandle {
82
+ users: {
83
+ /**
84
+ * Attach traits to one or more users (posts to `users/identify`). Buffered
85
+ * in the shared EventShipper and delivered on the next {@link flush} / span
86
+ * flush, so pair a bare identify with `flush()` when there is no active
87
+ * OpenCode session driving delivery.
88
+ */
89
+ identify(users: IdentifyInput | IdentifyInput[]): Promise<void>;
90
+ };
91
+ /** Drain the buffered event + trace shippers. */
92
+ flush(): Promise<void>;
93
+ /** Flush and tear down the shippers (also wired into session teardown). */
94
+ shutdown(): Promise<void>;
95
+ }
96
+ /**
97
+ * The plugin's return value: the OpenCode {@link Hooks} plus the additive
98
+ * {@link RaindropOpencodeHandle}. `raindrop` is optional because the plugin
99
+ * returns bare `{}` (no handle) when tracing is disabled — no write key and no
100
+ * local Workshop destination.
101
+ */
102
+ type RaindropOpencodeHooks = Hooks & {
103
+ raindrop?: RaindropOpencodeHandle;
104
+ };
3
105
  /**
4
106
  * Raindrop plugin for OpenCode.
5
107
  *
@@ -12,6 +114,6 @@ import { PluginInput, Hooks } from '@opencode-ai/plugin';
12
114
  *
13
115
  * Optional config file: ~/.config/opencode/raindrop.json or .opencode/raindrop.json
14
116
  */
15
- declare function plugin(input: PluginInput): Promise<Hooks>;
117
+ declare function plugin(input: PluginInput): Promise<RaindropOpencodeHooks>;
16
118
 
17
- export { plugin as default };
119
+ export { type IdentifyInput, type RaindropOpencodeHandle, type RaindropOpencodeHooks, plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,107 @@
1
- import { PluginInput, Hooks } from '@opencode-ai/plugin';
1
+ import { Hooks, PluginInput } from '@opencode-ai/plugin';
2
2
 
3
+ type IdentifyInput = {
4
+ userId: string;
5
+ traits?: Record<string, unknown>;
6
+ };
7
+
8
+ /**
9
+ * Run telemetry egress with OpenTelemetry tracing suppressed.
10
+ *
11
+ * Why this exists
12
+ * ---------------
13
+ * Raindrop integrations ship spans/events over HTTP with the global `fetch`
14
+ * (see {@link ../http.ts `postJson`}). When the host app also runs an OTel
15
+ * fetch/undici instrumentation — e.g. `@vercel/otel`'s `registerOTel`, which
16
+ * every Eve agent installs — that instrumentation wraps *our own* telemetry
17
+ * POSTs in a `fetch POST <endpoint>` span. Those spans are then handed to the
18
+ * very exporter that issued the request, so they get shipped right back to
19
+ * Raindrop and Workshop as standalone "runs" (and, because each export issues
20
+ * another fetch, they feed back on themselves). The result is a run list
21
+ * flooded with `fetch POST .../v1/traces`, `.../events/track_partial` and
22
+ * `.../live` entries that drown out the real agent turns — especially with
23
+ * sub-agents, where every sandbox runs its own instrumentation.
24
+ *
25
+ * The OTel-blessed fix is to mark the active context as "tracing suppressed"
26
+ * around the request; instrumentations check `isTracingSuppressed` and return
27
+ * a no-op span instead of recording one. We do this through a hook stashed on
28
+ * `globalThis` by the Node entrypoint ({@link ../index.node.ts}) so that:
29
+ * - `@opentelemetry/api` / `@opentelemetry/core` stay *optional* — core never
30
+ * hard-depends on them, and the hook is simply absent when they (and thus
31
+ * any instrumentation to suppress) are not installed; and
32
+ * - the browser bundle never pulls in `node:module`, mirroring how core
33
+ * injects `AsyncLocalStorage` via `RAINDROP_ASYNC_LOCAL_STORAGE`.
34
+ *
35
+ * When no hook is present the callback runs unchanged, so suppression is a
36
+ * best-effort no-op rather than a hard requirement.
37
+ */
38
+ /** Hook signature: run `fn` with OTel tracing suppressed, returning its value. */
39
+ type SuppressTracingHook = <T>(fn: () => T) => T;
40
+ declare global {
41
+ var RAINDROP_SUPPRESS_TRACING: SuppressTracingHook | undefined;
42
+ }
43
+
44
+ type ParentSpanContext = {
45
+ traceIdB64: string;
46
+ spanIdB64: string;
47
+ eventId: string;
48
+ };
49
+ interface ContextSpan {
50
+ readonly traceIdB64: string;
51
+ readonly spanIdB64: string;
52
+ readonly eventId: string;
53
+ log?(data: Record<string, unknown>): void;
54
+ }
55
+ interface AsyncLocalStorageLike<T> {
56
+ getStore(): T | undefined;
57
+ run<R>(store: T, callback: () => R): R;
58
+ enterWith?(store: T): void;
59
+ }
60
+ declare abstract class ContextManager {
61
+ abstract getParentSpanIds(): ParentSpanContext | undefined;
62
+ abstract runInContext<R>(span: ContextSpan, callback: () => R): R;
63
+ abstract getCurrentSpan(): ContextSpan | undefined;
64
+ abstract isReady(): boolean;
65
+ }
66
+ declare global {
67
+ var RAINDROP_CONTEXT_MANAGER: (new () => ContextManager) | undefined;
68
+ var RAINDROP_ASYNC_LOCAL_STORAGE: (new <T>() => AsyncLocalStorageLike<T>) | undefined;
69
+ }
70
+
71
+ /**
72
+ * The Raindrop control handle exposed alongside the OpenCode hooks.
73
+ *
74
+ * The plugin's public surface is the framework `Hooks` object, which OpenCode
75
+ * consumes and whose keyspace it owns. Rather than pollute that keyspace with a
76
+ * top-level `users` (as the client-factory wrappers do), the identify surface
77
+ * lives under a namespaced `raindrop` handle — mirroring `@raindrop-ai/eve`'s
78
+ * `raindrop` handle precedent — so application/tool code can attach user traits
79
+ * without waiting for a session hook to fire.
80
+ */
81
+ interface RaindropOpencodeHandle {
82
+ users: {
83
+ /**
84
+ * Attach traits to one or more users (posts to `users/identify`). Buffered
85
+ * in the shared EventShipper and delivered on the next {@link flush} / span
86
+ * flush, so pair a bare identify with `flush()` when there is no active
87
+ * OpenCode session driving delivery.
88
+ */
89
+ identify(users: IdentifyInput | IdentifyInput[]): Promise<void>;
90
+ };
91
+ /** Drain the buffered event + trace shippers. */
92
+ flush(): Promise<void>;
93
+ /** Flush and tear down the shippers (also wired into session teardown). */
94
+ shutdown(): Promise<void>;
95
+ }
96
+ /**
97
+ * The plugin's return value: the OpenCode {@link Hooks} plus the additive
98
+ * {@link RaindropOpencodeHandle}. `raindrop` is optional because the plugin
99
+ * returns bare `{}` (no handle) when tracing is disabled — no write key and no
100
+ * local Workshop destination.
101
+ */
102
+ type RaindropOpencodeHooks = Hooks & {
103
+ raindrop?: RaindropOpencodeHandle;
104
+ };
3
105
  /**
4
106
  * Raindrop plugin for OpenCode.
5
107
  *
@@ -12,6 +114,6 @@ import { PluginInput, Hooks } from '@opencode-ai/plugin';
12
114
  *
13
115
  * Optional config file: ~/.config/opencode/raindrop.json or .opencode/raindrop.json
14
116
  */
15
- declare function plugin(input: PluginInput): Promise<Hooks>;
117
+ declare function plugin(input: PluginInput): Promise<RaindropOpencodeHooks>;
16
118
 
17
- export { plugin as default };
119
+ export { type IdentifyInput, type RaindropOpencodeHandle, type RaindropOpencodeHooks, plugin as default };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // ../core/dist/chunk-267IXHO5.js
8
+ // ../core/dist/chunk-QTUH3BHZ.js
9
9
  function getCrypto() {
10
10
  const c = globalThis.crypto;
11
11
  return c;
@@ -398,19 +398,52 @@ function projectIdHeaders(projectId) {
398
398
  var SHUTDOWN_DEADLINE_MS = 1e4;
399
399
  var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
400
400
  function mergePatches(target, source) {
401
- var _a, _b, _c, _d, _e, _f;
401
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
402
402
  const out = { ...target, ...source };
403
403
  if (target.properties || source.properties) {
404
404
  out.properties = { ...(_a = target.properties) != null ? _a : {}, ...(_b = source.properties) != null ? _b : {} };
405
405
  }
406
+ if (target.usage || source.usage) {
407
+ out.usage = { ...(_c = target.usage) != null ? _c : {} };
408
+ if (((_d = source.usage) == null ? void 0 : _d.promptTokens) !== void 0) {
409
+ out.usage.promptTokens = source.usage.promptTokens;
410
+ }
411
+ if (((_e = source.usage) == null ? void 0 : _e.completionTokens) !== void 0) {
412
+ out.usage.completionTokens = source.usage.completionTokens;
413
+ }
414
+ }
415
+ if (source.error === void 0) {
416
+ out.error = target.error;
417
+ }
406
418
  if (target.featureFlags || source.featureFlags) {
407
- out.featureFlags = { ...(_c = target.featureFlags) != null ? _c : {}, ...(_d = source.featureFlags) != null ? _d : {} };
419
+ out.featureFlags = { ...(_f = target.featureFlags) != null ? _f : {}, ...(_g = source.featureFlags) != null ? _g : {} };
408
420
  }
409
421
  if (target.attachments || source.attachments) {
410
- out.attachments = [...(_e = target.attachments) != null ? _e : [], ...(_f = source.attachments) != null ? _f : []];
422
+ out.attachments = [...(_h = target.attachments) != null ? _h : [], ...(_i = source.attachments) != null ? _i : []];
411
423
  }
412
424
  return out;
413
425
  }
426
+ function isErrorLike(value) {
427
+ return value !== null && typeof value === "object";
428
+ }
429
+ function eventTelemetryProperties(patch) {
430
+ var _a, _b;
431
+ const properties = {};
432
+ if (((_a = patch.usage) == null ? void 0 : _a.promptTokens) !== void 0 && Number.isFinite(patch.usage.promptTokens)) {
433
+ properties["ai.usage.prompt_tokens"] = patch.usage.promptTokens;
434
+ }
435
+ if (((_b = patch.usage) == null ? void 0 : _b.completionTokens) !== void 0 && Number.isFinite(patch.usage.completionTokens)) {
436
+ properties["ai.usage.completion_tokens"] = patch.usage.completionTokens;
437
+ }
438
+ if (patch.error !== void 0 && patch.error !== null) {
439
+ const constructorName = patch.error instanceof Error && patch.error.constructor !== Error ? patch.error.constructor.name : void 0;
440
+ const errorType = patch.error instanceof Error ? patch.error.name && patch.error.name !== "Error" ? patch.error.name : constructorName || patch.error.name || "Error" : isErrorLike(patch.error) && typeof patch.error.name === "string" && patch.error.name.length > 0 ? patch.error.name : "Error";
441
+ const errorMessage = patch.error instanceof Error ? patch.error.message : isErrorLike(patch.error) && typeof patch.error.message === "string" ? patch.error.message : String(patch.error);
442
+ properties["error.type"] = errorType;
443
+ properties["error.message"] = errorMessage;
444
+ }
445
+ return properties;
446
+ }
414
447
  var EventShipper = class {
415
448
  constructor(opts) {
416
449
  this.buffers = /* @__PURE__ */ new Map();
@@ -659,6 +692,11 @@ var EventShipper = class {
659
692
  return;
660
693
  }
661
694
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
695
+ let telemetryProperties = {};
696
+ try {
697
+ telemetryProperties = eventTelemetryProperties(accumulated);
698
+ } catch (e) {
699
+ }
662
700
  const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
663
701
  const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
664
702
  const payload = {
@@ -668,12 +706,13 @@ var EventShipper = class {
668
706
  timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
669
707
  ai_data: {
670
708
  input: accumulated.input,
671
- output: accumulated.output,
709
+ output: accumulated.error !== void 0 && accumulated.error !== null ? void 0 : accumulated.output,
672
710
  model: accumulated.model,
673
711
  convo_id: convoId
674
712
  },
675
713
  properties: {
676
714
  ...restProperties,
715
+ ...telemetryProperties,
677
716
  ...wizardSession ? { "raindrop.wizardSession": wizardSession } : {},
678
717
  $context: this.context
679
718
  },
@@ -831,6 +870,13 @@ function redactJsonAttributeValue(key, value) {
831
870
  if (scrubbedJson === json) return void 0;
832
871
  return { stringValue: scrubbedJson };
833
872
  }
873
+ function identityAttrs(args) {
874
+ return [
875
+ attrString("ai.telemetry.metadata.raindrop.ai.userId", args.userId),
876
+ attrString("ai.telemetry.metadata.raindrop.convoId", args.convoId),
877
+ attrString("ai.telemetry.metadata.raindrop.eventName", args.eventName)
878
+ ];
879
+ }
834
880
  function applyOtelSpanAttributeLimit(limit) {
835
881
  var _a, _b;
836
882
  try {
@@ -955,6 +1001,7 @@ var TraceShipper = class {
955
1001
  const started = (_a = args.startTimeUnixNano) != null ? _a : nowUnixNanoString();
956
1002
  const attrs = [
957
1003
  attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1004
+ ...identityAttrs(args),
958
1005
  attrString("ai.operationId", args.operationId)
959
1006
  ];
960
1007
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
@@ -1010,7 +1057,8 @@ var TraceShipper = class {
1010
1057
  var _a;
1011
1058
  const ids = createSpanIds(args.parent);
1012
1059
  const attrs = [
1013
- attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
1060
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1061
+ ...identityAttrs(args)
1014
1062
  ];
1015
1063
  if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
1016
1064
  const otlp = buildOtlpSpan({
@@ -1229,7 +1277,7 @@ function resolveLocalWorkshopUrl(fileValue) {
1229
1277
  // package.json
1230
1278
  var package_default = {
1231
1279
  name: "@raindrop-ai/opencode-plugin",
1232
- version: "0.0.19",
1280
+ version: "0.1.1",
1233
1281
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1234
1282
  type: "module",
1235
1283
  main: "dist/index.js",
@@ -1509,6 +1557,8 @@ function createSessionState(sessionId) {
1509
1557
  return {
1510
1558
  sessionId,
1511
1559
  currentInput: "",
1560
+ turnOutputParts: [],
1561
+ turnOutputTokens: 0,
1512
1562
  outputParts: /* @__PURE__ */ new Map(),
1513
1563
  reasoningParts: /* @__PURE__ */ new Map(),
1514
1564
  toolSpanStarts: /* @__PURE__ */ new Map(),
@@ -1614,7 +1664,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1614
1664
  // event — session lifecycle + streaming parts
1615
1665
  // ------------------------------------------------------------------
1616
1666
  event: async ({ event }) => {
1617
- var _a, _b, _c, _d, _e, _f, _g;
1667
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1618
1668
  try {
1619
1669
  const props = event.properties;
1620
1670
  const info = props["info"];
@@ -1739,6 +1789,10 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1739
1789
  const outputText = state.outputParts.get(messageId);
1740
1790
  const reasoningText = state.reasoningParts.get(messageId);
1741
1791
  const isToolCallsCompletion = finishReason === "tool-calls";
1792
+ if (outputText !== void 0 && outputText.trim().length > 0) {
1793
+ state.turnOutputParts.push(outputText);
1794
+ }
1795
+ state.turnOutputTokens += outputTokens;
1742
1796
  const hasIntermediateReasoning = reasoningText !== void 0 && reasoningText.trim().length > 0;
1743
1797
  if (outputText === void 0 && !(isToolCallsCompletion && hasIntermediateReasoning))
1744
1798
  return;
@@ -1793,6 +1847,7 @@ type: ${name != null ? name : "UnknownError"}`;
1793
1847
  return;
1794
1848
  }
1795
1849
  if (outputText === void 0) return;
1850
+ const turnOutputText = capText2(state.turnOutputParts.join("\n\n"));
1796
1851
  const rootSpan = state.currentRootSpan;
1797
1852
  const isCompaction = state.isCompacting === true;
1798
1853
  if (state.parentId) {
@@ -1804,7 +1859,7 @@ type: ${name != null ? name : "UnknownError"}`;
1804
1859
  "ai.prompt.messages",
1805
1860
  buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1806
1861
  ),
1807
- attrString("ai.response.text", outputText),
1862
+ attrString("ai.response.text", turnOutputText),
1808
1863
  attrString("gen_ai.system", providerID),
1809
1864
  attrString("gen_ai.request.model", modelID),
1810
1865
  attrString("gen_ai.response.model", modelID),
@@ -1856,7 +1911,7 @@ type: ${name != null ? name : "UnknownError"}`;
1856
1911
  "ai.prompt.messages",
1857
1912
  buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1858
1913
  ),
1859
- attrString("ai.response.text", outputText),
1914
+ attrString("ai.response.text", turnOutputText),
1860
1915
  attrString("gen_ai.system", providerID),
1861
1916
  attrString("gen_ai.request.model", modelID),
1862
1917
  attrString("gen_ai.response.model", modelID),
@@ -1885,8 +1940,8 @@ type: ${name != null ? name : "UnknownError"}`;
1885
1940
  }
1886
1941
  }
1887
1942
  state.currentRootSpan = void 0;
1888
- const hasAssistantResponse = outputText.trim().length > 0;
1889
- if (!hasAssistantResponse) {
1943
+ const hasAssistantResponse = turnOutputText.trim().length > 0;
1944
+ if (!hasAssistantResponse && !msgError) {
1890
1945
  await traceShipper.flush();
1891
1946
  state.currentEventId = void 0;
1892
1947
  return;
@@ -1895,7 +1950,15 @@ type: ${name != null ? name : "UnknownError"}`;
1895
1950
  await eventShipper.finish(state.currentEventId, {
1896
1951
  userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
1897
1952
  model: modelID,
1898
- output: outputText,
1953
+ output: msgError ? void 0 : turnOutputText,
1954
+ usage: {
1955
+ promptTokens: inputTokens,
1956
+ completionTokens: state.turnOutputTokens
1957
+ },
1958
+ error: msgError ? {
1959
+ name: msgError.name,
1960
+ message: (_g = (_f = (_e = msgError.data) == null ? void 0 : _e.message) != null ? _f : msgError.name) != null ? _g : "OpenCode request failed"
1961
+ } : void 0,
1899
1962
  properties: {
1900
1963
  plugin_version: PLUGIN_VERSION,
1901
1964
  message_id: messageId,
@@ -1939,7 +2002,7 @@ type: ${name != null ? name : "UnknownError"}`;
1939
2002
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1940
2003
  sessions.delete(String(sessionID));
1941
2004
  } else if (event.type === "session.error") {
1942
- const errorSessionID = (_e = props["sessionID"]) != null ? _e : sessionID;
2005
+ const errorSessionID = (_h = props["sessionID"]) != null ? _h : sessionID;
1943
2006
  if (errorSessionID === void 0 || errorSessionID === "")
1944
2007
  throw new Error("session.error: missing sessionID");
1945
2008
  const state = sessions.get(String(errorSessionID));
@@ -1947,7 +2010,7 @@ type: ${name != null ? name : "UnknownError"}`;
1947
2010
  const errorObj = props["error"];
1948
2011
  if (errorObj == null) throw new Error("session.error: missing error object");
1949
2012
  const errorName = errorObj.name;
1950
- const errorMessage = (_g = (_f = errorObj.data) == null ? void 0 : _f.message) != null ? _g : errorObj.name;
2013
+ const errorMessage = (_j = (_i = errorObj.data) == null ? void 0 : _i.message) != null ? _j : errorObj.name;
1951
2014
  if (errorName == null && errorMessage == null)
1952
2015
  throw new Error("session.error: error object must have name or data.message");
1953
2016
  const errorStr = `${errorMessage != null ? errorMessage : errorName}
@@ -2024,6 +2087,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2024
2087
  };
2025
2088
  const userText = capText2(textParts.join("\n"));
2026
2089
  state.currentInput = userText;
2090
+ state.turnOutputParts = [];
2091
+ state.turnOutputTokens = 0;
2027
2092
  if (state.parentId) {
2028
2093
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
2029
2094
  return;
@@ -2241,6 +2306,21 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2241
2306
  }
2242
2307
 
2243
2308
  // src/index.ts
2309
+ function createRaindropHandle(eventShipper, traceShipper) {
2310
+ return {
2311
+ users: {
2312
+ async identify(users) {
2313
+ await eventShipper.identify(users);
2314
+ }
2315
+ },
2316
+ async flush() {
2317
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2318
+ },
2319
+ async shutdown() {
2320
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2321
+ }
2322
+ };
2323
+ }
2244
2324
  async function plugin(input) {
2245
2325
  var _a;
2246
2326
  const config = loadConfig(input.directory);
@@ -2286,7 +2366,7 @@ async function plugin(input) {
2286
2366
  eventShipper,
2287
2367
  traceShipper
2288
2368
  );
2289
- return hooks;
2369
+ return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper) };
2290
2370
  }
2291
2371
  export {
2292
2372
  plugin as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raindrop-ai/opencode-plugin",
3
- "version": "0.0.19",
3
+ "version": "0.1.1",
4
4
  "description": "Raindrop observability plugin for OpenCode — automatic session/event/span tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "@types/node": "^20.11.17",
38
38
  "tsup": "^8.4.0",
39
39
  "typescript": "^5.3.3",
40
- "@raindrop-ai/core": "0.1.2"
40
+ "@raindrop-ai/core": "0.1.3"
41
41
  },
42
42
  "tsup": {
43
43
  "entry": [