@raindrop-ai/opencode-plugin 0.0.18 → 0.1.0

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-WKRW55KX.js
27
+ // ../core/dist/chunk-QTUH3BHZ.js
28
28
  function getCrypto() {
29
29
  const c = globalThis.crypto;
30
30
  return c;
@@ -39,6 +39,17 @@ function randomBytes(length) {
39
39
  for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
40
40
  return out;
41
41
  }
42
+ function randomUUID() {
43
+ const cryptoObj = getCrypto();
44
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
45
+ return cryptoObj.randomUUID();
46
+ }
47
+ const b = randomBytes(16);
48
+ b[6] = b[6] & 15 | 64;
49
+ b[8] = b[8] & 63 | 128;
50
+ const hex = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
51
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
52
+ }
42
53
  function base64Encode(bytes) {
43
54
  const maybeBuffer = globalThis.Buffer;
44
55
  if (maybeBuffer) {
@@ -69,7 +80,7 @@ function base64Encode(bytes) {
69
80
  return out;
70
81
  }
71
82
  function generateId() {
72
- return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
83
+ return randomUUID();
73
84
  }
74
85
  function runWithTracingSuppressed(fn) {
75
86
  const hook = globalThis.RAINDROP_SUPPRESS_TRACING;
@@ -406,16 +417,52 @@ function projectIdHeaders(projectId) {
406
417
  var SHUTDOWN_DEADLINE_MS = 1e4;
407
418
  var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
408
419
  function mergePatches(target, source) {
409
- var _a, _b, _c, _d;
420
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
410
421
  const out = { ...target, ...source };
411
422
  if (target.properties || source.properties) {
412
423
  out.properties = { ...(_a = target.properties) != null ? _a : {}, ...(_b = source.properties) != null ? _b : {} };
413
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
+ }
437
+ if (target.featureFlags || source.featureFlags) {
438
+ out.featureFlags = { ...(_f = target.featureFlags) != null ? _f : {}, ...(_g = source.featureFlags) != null ? _g : {} };
439
+ }
414
440
  if (target.attachments || source.attachments) {
415
- out.attachments = [...(_c = target.attachments) != null ? _c : [], ...(_d = source.attachments) != null ? _d : []];
441
+ out.attachments = [...(_h = target.attachments) != null ? _h : [], ...(_i = source.attachments) != null ? _i : []];
416
442
  }
417
443
  return out;
418
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
+ }
419
466
  var EventShipper = class {
420
467
  constructor(opts) {
421
468
  this.buffers = /* @__PURE__ */ new Map();
@@ -664,6 +711,11 @@ var EventShipper = class {
664
711
  return;
665
712
  }
666
713
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
714
+ let telemetryProperties = {};
715
+ try {
716
+ telemetryProperties = eventTelemetryProperties(accumulated);
717
+ } catch (e) {
718
+ }
667
719
  const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
668
720
  const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
669
721
  const payload = {
@@ -673,15 +725,17 @@ var EventShipper = class {
673
725
  timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
674
726
  ai_data: {
675
727
  input: accumulated.input,
676
- output: accumulated.output,
728
+ output: accumulated.error !== void 0 && accumulated.error !== null ? void 0 : accumulated.output,
677
729
  model: accumulated.model,
678
730
  convo_id: convoId
679
731
  },
680
732
  properties: {
681
733
  ...restProperties,
734
+ ...telemetryProperties,
682
735
  ...wizardSession ? { "raindrop.wizardSession": wizardSession } : {},
683
736
  $context: this.context
684
737
  },
738
+ ...accumulated.featureFlags && Object.keys(accumulated.featureFlags).length > 0 ? { feature_flags: accumulated.featureFlags } : {},
685
739
  attachments: accumulated.attachments,
686
740
  is_pending: isPending
687
741
  };
@@ -835,6 +889,13 @@ function redactJsonAttributeValue(key, value) {
835
889
  if (scrubbedJson === json) return void 0;
836
890
  return { stringValue: scrubbedJson };
837
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
+ }
838
899
  function applyOtelSpanAttributeLimit(limit) {
839
900
  var _a, _b;
840
901
  try {
@@ -959,6 +1020,7 @@ var TraceShipper = class {
959
1020
  const started = (_a = args.startTimeUnixNano) != null ? _a : nowUnixNanoString();
960
1021
  const attrs = [
961
1022
  attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1023
+ ...identityAttrs(args),
962
1024
  attrString("ai.operationId", args.operationId)
963
1025
  ];
964
1026
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
@@ -1014,7 +1076,8 @@ var TraceShipper = class {
1014
1076
  var _a;
1015
1077
  const ids = createSpanIds(args.parent);
1016
1078
  const attrs = [
1017
- attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
1079
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1080
+ ...identityAttrs(args)
1018
1081
  ];
1019
1082
  if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
1020
1083
  const otlp = buildOtlpSpan({
@@ -1233,7 +1296,7 @@ function resolveLocalWorkshopUrl(fileValue) {
1233
1296
  // package.json
1234
1297
  var package_default = {
1235
1298
  name: "@raindrop-ai/opencode-plugin",
1236
- version: "0.0.18",
1299
+ version: "0.1.0",
1237
1300
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1238
1301
  type: "module",
1239
1302
  main: "dist/index.js",
@@ -1618,7 +1681,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1618
1681
  // event — session lifecycle + streaming parts
1619
1682
  // ------------------------------------------------------------------
1620
1683
  event: async ({ event }) => {
1621
- var _a, _b, _c, _d, _e, _f, _g;
1684
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1622
1685
  try {
1623
1686
  const props = event.properties;
1624
1687
  const info = props["info"];
@@ -1890,7 +1953,7 @@ type: ${name != null ? name : "UnknownError"}`;
1890
1953
  }
1891
1954
  state.currentRootSpan = void 0;
1892
1955
  const hasAssistantResponse = outputText.trim().length > 0;
1893
- if (!hasAssistantResponse) {
1956
+ if (!hasAssistantResponse && !msgError) {
1894
1957
  await traceShipper.flush();
1895
1958
  state.currentEventId = void 0;
1896
1959
  return;
@@ -1898,11 +1961,20 @@ type: ${name != null ? name : "UnknownError"}`;
1898
1961
  if (!state.parentId) {
1899
1962
  await eventShipper.finish(state.currentEventId, {
1900
1963
  userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
1901
- model: modelName,
1902
- output: outputText,
1964
+ model: modelID,
1965
+ output: msgError ? void 0 : outputText,
1966
+ usage: {
1967
+ promptTokens: inputTokens,
1968
+ completionTokens: outputTokens
1969
+ },
1970
+ error: msgError ? {
1971
+ name: msgError.name,
1972
+ message: (_g = (_f = (_e = msgError.data) == null ? void 0 : _e.message) != null ? _f : msgError.name) != null ? _g : "OpenCode request failed"
1973
+ } : void 0,
1903
1974
  properties: {
1904
1975
  plugin_version: PLUGIN_VERSION,
1905
1976
  message_id: messageId,
1977
+ "ai.provider": providerID,
1906
1978
  ...isCompaction ? { is_compaction: true } : {}
1907
1979
  }
1908
1980
  });
@@ -1942,7 +2014,7 @@ type: ${name != null ? name : "UnknownError"}`;
1942
2014
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1943
2015
  sessions.delete(String(sessionID));
1944
2016
  } else if (event.type === "session.error") {
1945
- const errorSessionID = (_e = props["sessionID"]) != null ? _e : sessionID;
2017
+ const errorSessionID = (_h = props["sessionID"]) != null ? _h : sessionID;
1946
2018
  if (errorSessionID === void 0 || errorSessionID === "")
1947
2019
  throw new Error("session.error: missing sessionID");
1948
2020
  const state = sessions.get(String(errorSessionID));
@@ -1950,7 +2022,7 @@ type: ${name != null ? name : "UnknownError"}`;
1950
2022
  const errorObj = props["error"];
1951
2023
  if (errorObj == null) throw new Error("session.error: missing error object");
1952
2024
  const errorName = errorObj.name;
1953
- const errorMessage = (_g = (_f = errorObj.data) == null ? void 0 : _f.message) != null ? _g : errorObj.name;
2025
+ const errorMessage = (_j = (_i = errorObj.data) == null ? void 0 : _i.message) != null ? _j : errorObj.name;
1954
2026
  if (errorName == null && errorMessage == null)
1955
2027
  throw new Error("session.error: error object must have name or data.message");
1956
2028
  const errorStr = `${errorMessage != null ? errorMessage : errorName}
@@ -2010,7 +2082,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2010
2082
  promptMeta.properties = m["properties"];
2011
2083
  }
2012
2084
  } else if (part["type"] === "file") {
2013
- const mimeType = part["mediaType"];
2085
+ const mimeType = part["mime"];
2014
2086
  const filename = part["filename"];
2015
2087
  const url = part["url"];
2016
2088
  if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
@@ -2244,6 +2316,21 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2244
2316
  }
2245
2317
 
2246
2318
  // src/index.ts
2319
+ function createRaindropHandle(eventShipper, traceShipper) {
2320
+ return {
2321
+ users: {
2322
+ async identify(users) {
2323
+ await eventShipper.identify(users);
2324
+ }
2325
+ },
2326
+ async flush() {
2327
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2328
+ },
2329
+ async shutdown() {
2330
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2331
+ }
2332
+ };
2333
+ }
2247
2334
  async function plugin(input) {
2248
2335
  var _a;
2249
2336
  const config = loadConfig(input.directory);
@@ -2289,5 +2376,5 @@ async function plugin(input) {
2289
2376
  eventShipper,
2290
2377
  traceShipper
2291
2378
  );
2292
- return hooks;
2379
+ return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper) };
2293
2380
  }
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-WKRW55KX.js
8
+ // ../core/dist/chunk-QTUH3BHZ.js
9
9
  function getCrypto() {
10
10
  const c = globalThis.crypto;
11
11
  return c;
@@ -20,6 +20,17 @@ function randomBytes(length) {
20
20
  for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
21
21
  return out;
22
22
  }
23
+ function randomUUID() {
24
+ const cryptoObj = getCrypto();
25
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
26
+ return cryptoObj.randomUUID();
27
+ }
28
+ const b = randomBytes(16);
29
+ b[6] = b[6] & 15 | 64;
30
+ b[8] = b[8] & 63 | 128;
31
+ const hex = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
32
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
33
+ }
23
34
  function base64Encode(bytes) {
24
35
  const maybeBuffer = globalThis.Buffer;
25
36
  if (maybeBuffer) {
@@ -50,7 +61,7 @@ function base64Encode(bytes) {
50
61
  return out;
51
62
  }
52
63
  function generateId() {
53
- return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
64
+ return randomUUID();
54
65
  }
55
66
  function runWithTracingSuppressed(fn) {
56
67
  const hook = globalThis.RAINDROP_SUPPRESS_TRACING;
@@ -387,16 +398,52 @@ function projectIdHeaders(projectId) {
387
398
  var SHUTDOWN_DEADLINE_MS = 1e4;
388
399
  var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
389
400
  function mergePatches(target, source) {
390
- var _a, _b, _c, _d;
401
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
391
402
  const out = { ...target, ...source };
392
403
  if (target.properties || source.properties) {
393
404
  out.properties = { ...(_a = target.properties) != null ? _a : {}, ...(_b = source.properties) != null ? _b : {} };
394
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
+ }
418
+ if (target.featureFlags || source.featureFlags) {
419
+ out.featureFlags = { ...(_f = target.featureFlags) != null ? _f : {}, ...(_g = source.featureFlags) != null ? _g : {} };
420
+ }
395
421
  if (target.attachments || source.attachments) {
396
- out.attachments = [...(_c = target.attachments) != null ? _c : [], ...(_d = source.attachments) != null ? _d : []];
422
+ out.attachments = [...(_h = target.attachments) != null ? _h : [], ...(_i = source.attachments) != null ? _i : []];
397
423
  }
398
424
  return out;
399
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
+ }
400
447
  var EventShipper = class {
401
448
  constructor(opts) {
402
449
  this.buffers = /* @__PURE__ */ new Map();
@@ -645,6 +692,11 @@ var EventShipper = class {
645
692
  return;
646
693
  }
647
694
  const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
695
+ let telemetryProperties = {};
696
+ try {
697
+ telemetryProperties = eventTelemetryProperties(accumulated);
698
+ } catch (e) {
699
+ }
648
700
  const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
649
701
  const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
650
702
  const payload = {
@@ -654,15 +706,17 @@ var EventShipper = class {
654
706
  timestamp: (_i = accumulated.timestamp) != null ? _i : (/* @__PURE__ */ new Date()).toISOString(),
655
707
  ai_data: {
656
708
  input: accumulated.input,
657
- output: accumulated.output,
709
+ output: accumulated.error !== void 0 && accumulated.error !== null ? void 0 : accumulated.output,
658
710
  model: accumulated.model,
659
711
  convo_id: convoId
660
712
  },
661
713
  properties: {
662
714
  ...restProperties,
715
+ ...telemetryProperties,
663
716
  ...wizardSession ? { "raindrop.wizardSession": wizardSession } : {},
664
717
  $context: this.context
665
718
  },
719
+ ...accumulated.featureFlags && Object.keys(accumulated.featureFlags).length > 0 ? { feature_flags: accumulated.featureFlags } : {},
666
720
  attachments: accumulated.attachments,
667
721
  is_pending: isPending
668
722
  };
@@ -816,6 +870,13 @@ function redactJsonAttributeValue(key, value) {
816
870
  if (scrubbedJson === json) return void 0;
817
871
  return { stringValue: scrubbedJson };
818
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
+ }
819
880
  function applyOtelSpanAttributeLimit(limit) {
820
881
  var _a, _b;
821
882
  try {
@@ -940,6 +1001,7 @@ var TraceShipper = class {
940
1001
  const started = (_a = args.startTimeUnixNano) != null ? _a : nowUnixNanoString();
941
1002
  const attrs = [
942
1003
  attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1004
+ ...identityAttrs(args),
943
1005
  attrString("ai.operationId", args.operationId)
944
1006
  ];
945
1007
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
@@ -995,7 +1057,8 @@ var TraceShipper = class {
995
1057
  var _a;
996
1058
  const ids = createSpanIds(args.parent);
997
1059
  const attrs = [
998
- attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
1060
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId),
1061
+ ...identityAttrs(args)
999
1062
  ];
1000
1063
  if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
1001
1064
  const otlp = buildOtlpSpan({
@@ -1214,7 +1277,7 @@ function resolveLocalWorkshopUrl(fileValue) {
1214
1277
  // package.json
1215
1278
  var package_default = {
1216
1279
  name: "@raindrop-ai/opencode-plugin",
1217
- version: "0.0.18",
1280
+ version: "0.1.0",
1218
1281
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1219
1282
  type: "module",
1220
1283
  main: "dist/index.js",
@@ -1599,7 +1662,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1599
1662
  // event — session lifecycle + streaming parts
1600
1663
  // ------------------------------------------------------------------
1601
1664
  event: async ({ event }) => {
1602
- var _a, _b, _c, _d, _e, _f, _g;
1665
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1603
1666
  try {
1604
1667
  const props = event.properties;
1605
1668
  const info = props["info"];
@@ -1871,7 +1934,7 @@ type: ${name != null ? name : "UnknownError"}`;
1871
1934
  }
1872
1935
  state.currentRootSpan = void 0;
1873
1936
  const hasAssistantResponse = outputText.trim().length > 0;
1874
- if (!hasAssistantResponse) {
1937
+ if (!hasAssistantResponse && !msgError) {
1875
1938
  await traceShipper.flush();
1876
1939
  state.currentEventId = void 0;
1877
1940
  return;
@@ -1879,11 +1942,20 @@ type: ${name != null ? name : "UnknownError"}`;
1879
1942
  if (!state.parentId) {
1880
1943
  await eventShipper.finish(state.currentEventId, {
1881
1944
  userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
1882
- model: modelName,
1883
- output: outputText,
1945
+ model: modelID,
1946
+ output: msgError ? void 0 : outputText,
1947
+ usage: {
1948
+ promptTokens: inputTokens,
1949
+ completionTokens: outputTokens
1950
+ },
1951
+ error: msgError ? {
1952
+ name: msgError.name,
1953
+ message: (_g = (_f = (_e = msgError.data) == null ? void 0 : _e.message) != null ? _f : msgError.name) != null ? _g : "OpenCode request failed"
1954
+ } : void 0,
1884
1955
  properties: {
1885
1956
  plugin_version: PLUGIN_VERSION,
1886
1957
  message_id: messageId,
1958
+ "ai.provider": providerID,
1887
1959
  ...isCompaction ? { is_compaction: true } : {}
1888
1960
  }
1889
1961
  });
@@ -1923,7 +1995,7 @@ type: ${name != null ? name : "UnknownError"}`;
1923
1995
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1924
1996
  sessions.delete(String(sessionID));
1925
1997
  } else if (event.type === "session.error") {
1926
- const errorSessionID = (_e = props["sessionID"]) != null ? _e : sessionID;
1998
+ const errorSessionID = (_h = props["sessionID"]) != null ? _h : sessionID;
1927
1999
  if (errorSessionID === void 0 || errorSessionID === "")
1928
2000
  throw new Error("session.error: missing sessionID");
1929
2001
  const state = sessions.get(String(errorSessionID));
@@ -1931,7 +2003,7 @@ type: ${name != null ? name : "UnknownError"}`;
1931
2003
  const errorObj = props["error"];
1932
2004
  if (errorObj == null) throw new Error("session.error: missing error object");
1933
2005
  const errorName = errorObj.name;
1934
- const errorMessage = (_g = (_f = errorObj.data) == null ? void 0 : _f.message) != null ? _g : errorObj.name;
2006
+ const errorMessage = (_j = (_i = errorObj.data) == null ? void 0 : _i.message) != null ? _j : errorObj.name;
1935
2007
  if (errorName == null && errorMessage == null)
1936
2008
  throw new Error("session.error: error object must have name or data.message");
1937
2009
  const errorStr = `${errorMessage != null ? errorMessage : errorName}
@@ -1991,7 +2063,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1991
2063
  promptMeta.properties = m["properties"];
1992
2064
  }
1993
2065
  } else if (part["type"] === "file") {
1994
- const mimeType = part["mediaType"];
2066
+ const mimeType = part["mime"];
1995
2067
  const filename = part["filename"];
1996
2068
  const url = part["url"];
1997
2069
  if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
@@ -2225,6 +2297,21 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
2225
2297
  }
2226
2298
 
2227
2299
  // src/index.ts
2300
+ function createRaindropHandle(eventShipper, traceShipper) {
2301
+ return {
2302
+ users: {
2303
+ async identify(users) {
2304
+ await eventShipper.identify(users);
2305
+ }
2306
+ },
2307
+ async flush() {
2308
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2309
+ },
2310
+ async shutdown() {
2311
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2312
+ }
2313
+ };
2314
+ }
2228
2315
  async function plugin(input) {
2229
2316
  var _a;
2230
2317
  const config = loadConfig(input.directory);
@@ -2270,7 +2357,7 @@ async function plugin(input) {
2270
2357
  eventShipper,
2271
2358
  traceShipper
2272
2359
  );
2273
- return hooks;
2360
+ return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper) };
2274
2361
  }
2275
2362
  export {
2276
2363
  plugin as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raindrop-ai/opencode-plugin",
3
- "version": "0.0.18",
3
+ "version": "0.1.0",
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.0.5"
40
+ "@raindrop-ai/core": "0.1.3"
41
41
  },
42
42
  "tsup": {
43
43
  "entry": [