@raindrop-ai/opencode-plugin 0.0.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 ADDED
@@ -0,0 +1,913 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => plugin
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/config.ts
28
+ var import_node_fs = require("fs");
29
+ var import_node_os = require("os");
30
+ var import_node_path = require("path");
31
+ function loadConfig(projectDirectory) {
32
+ var _a, _b, _c, _d, _e, _f;
33
+ let merged = {};
34
+ const configPaths = [
35
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
36
+ (0, import_node_path.join)(projectDirectory, ".opencode", "raindrop.json")
37
+ ];
38
+ for (const configPath of configPaths) {
39
+ try {
40
+ if ((0, import_node_fs.existsSync)(configPath)) {
41
+ const content = (0, import_node_fs.readFileSync)(configPath, "utf-8");
42
+ const parsed = JSON.parse(content);
43
+ merged = { ...merged, ...parsed };
44
+ }
45
+ } catch (e) {
46
+ }
47
+ }
48
+ return {
49
+ writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
50
+ endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai",
51
+ eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
52
+ userId: merged.user_id,
53
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false
54
+ };
55
+ }
56
+
57
+ // src/shipper.ts
58
+ function getCryptoObj() {
59
+ return globalThis.crypto;
60
+ }
61
+ function randomBytes(length) {
62
+ const c = getCryptoObj();
63
+ const out = new Uint8Array(length);
64
+ if (c == null ? void 0 : c.getRandomValues) {
65
+ c.getRandomValues(out);
66
+ return out;
67
+ }
68
+ for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
69
+ return out;
70
+ }
71
+ function base64Encode(bytes) {
72
+ const buf = globalThis.Buffer;
73
+ if (buf) return buf.from(bytes).toString("base64");
74
+ let binary = "";
75
+ for (let i2 = 0; i2 < bytes.length; i2++) binary += String.fromCharCode(bytes[i2]);
76
+ const btoaFn = globalThis.btoa;
77
+ if (typeof btoaFn === "function") return btoaFn(binary);
78
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
79
+ let out = "";
80
+ let i = 0;
81
+ while (i < binary.length) {
82
+ const c1 = binary.charCodeAt(i++) & 255;
83
+ const c2 = i < binary.length ? binary.charCodeAt(i++) & 255 : NaN;
84
+ const c3 = i < binary.length ? binary.charCodeAt(i++) & 255 : NaN;
85
+ const e1 = c1 >> 2;
86
+ const e2 = (c1 & 3) << 4 | (Number.isNaN(c2) ? 0 : c2 >> 4);
87
+ const e3 = Number.isNaN(c2) ? 64 : (c2 & 15) << 2 | (Number.isNaN(c3) ? 0 : c3 >> 6);
88
+ const e4 = Number.isNaN(c3) ? 64 : c3 & 63;
89
+ out += alphabet.charAt(e1);
90
+ out += alphabet.charAt(e2);
91
+ out += e3 === 64 ? "=" : alphabet.charAt(e3);
92
+ out += e4 === 64 ? "=" : alphabet.charAt(e4);
93
+ }
94
+ return out;
95
+ }
96
+ var SERVICE_NAME = "raindrop.opencode-plugin";
97
+ var PLUGIN_VERSION = "0.1.0";
98
+ var SpanStatusCode = {
99
+ UNSET: 0,
100
+ OK: 1,
101
+ ERROR: 2
102
+ };
103
+ function createSpanIds(parent) {
104
+ const traceId = parent ? parent.traceIdB64 : base64Encode(randomBytes(16));
105
+ const spanId = base64Encode(randomBytes(8));
106
+ return {
107
+ traceIdB64: traceId,
108
+ spanIdB64: spanId,
109
+ parentSpanIdB64: parent ? parent.spanIdB64 : void 0
110
+ };
111
+ }
112
+ function nowUnixNanoString() {
113
+ return String(Date.now() * 1e6);
114
+ }
115
+ function attrString(key, value) {
116
+ if (value === void 0) return void 0;
117
+ return { key, value: { stringValue: value } };
118
+ }
119
+ function attrInt(key, value) {
120
+ if (value === void 0) return void 0;
121
+ if (!Number.isFinite(value)) return void 0;
122
+ return { key, value: { intValue: String(Math.trunc(value)) } };
123
+ }
124
+ function buildOtlpSpan(args) {
125
+ const attrs = args.attributes.filter((x) => x !== void 0);
126
+ const span = {
127
+ traceId: args.ids.traceIdB64,
128
+ spanId: args.ids.spanIdB64,
129
+ name: args.name,
130
+ startTimeUnixNano: args.startTimeUnixNano,
131
+ endTimeUnixNano: args.endTimeUnixNano
132
+ };
133
+ if (args.ids.parentSpanIdB64) span.parentSpanId = args.ids.parentSpanIdB64;
134
+ if (attrs.length) span.attributes = attrs;
135
+ if (args.status) span.status = args.status;
136
+ return span;
137
+ }
138
+ function buildExportTraceServiceRequest(spans) {
139
+ return {
140
+ resourceSpans: [
141
+ {
142
+ resource: {
143
+ attributes: [{ key: "service.name", value: { stringValue: SERVICE_NAME } }]
144
+ },
145
+ scopeSpans: [
146
+ {
147
+ scope: { name: SERVICE_NAME, version: PLUGIN_VERSION },
148
+ spans
149
+ }
150
+ ]
151
+ }
152
+ ]
153
+ };
154
+ }
155
+ function wait(ms) {
156
+ return new Promise((resolve) => setTimeout(resolve, ms));
157
+ }
158
+ function formatEndpoint(endpoint) {
159
+ if (!endpoint) return void 0;
160
+ return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
161
+ }
162
+ function parseRetryAfter(headers) {
163
+ var _a;
164
+ const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
165
+ if (!value) return void 0;
166
+ const asNumber = Number(value);
167
+ if (!Number.isNaN(asNumber)) return asNumber * 1e3;
168
+ const asDate = new Date(value).getTime();
169
+ if (!Number.isNaN(asDate)) {
170
+ const delta = asDate - Date.now();
171
+ return delta > 0 ? delta : 0;
172
+ }
173
+ return void 0;
174
+ }
175
+ function getRetryDelayMs(attemptNumber, previousError) {
176
+ if (previousError !== null && typeof previousError === "object" && "retryAfterMs" in previousError) {
177
+ const v = previousError.retryAfterMs;
178
+ if (typeof v === "number") return Math.max(0, v);
179
+ }
180
+ if (attemptNumber <= 1) return 0;
181
+ return 500 * Math.pow(2, attemptNumber - 2);
182
+ }
183
+ async function withRetry(operation, opName, opts) {
184
+ let lastError = void 0;
185
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
186
+ if (attempt > 1) {
187
+ const delay = getRetryDelayMs(attempt, lastError);
188
+ if (opts.debug) {
189
+ console.warn(
190
+ `[raindrop-ai/opencode-plugin] ${opName} retry ${attempt}/${opts.maxAttempts} in ${delay}ms`
191
+ );
192
+ }
193
+ if (delay > 0) await wait(delay);
194
+ } else if (opts.debug) {
195
+ console.log(
196
+ `[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt}/${opts.maxAttempts}`
197
+ );
198
+ }
199
+ try {
200
+ return await operation();
201
+ } catch (err) {
202
+ lastError = err;
203
+ if (opts.debug) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ console.warn(
206
+ `[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt} failed: ${msg}${attempt === opts.maxAttempts ? " (no more retries)" : ""}`
207
+ );
208
+ }
209
+ if (attempt === opts.maxAttempts) break;
210
+ }
211
+ }
212
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
213
+ }
214
+ async function postJson(url, body, headers, opts) {
215
+ await withRetry(
216
+ async () => {
217
+ const resp = await fetch(url, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json", ...headers },
220
+ body: JSON.stringify(body)
221
+ });
222
+ if (!resp.ok) {
223
+ const text = await resp.text().catch(() => "");
224
+ const err = new Error(
225
+ `HTTP ${resp.status} ${resp.statusText}${text ? `: ${text}` : ""}`
226
+ );
227
+ const retryAfterMs = parseRetryAfter(resp.headers);
228
+ if (typeof retryAfterMs === "number") err.retryAfterMs = retryAfterMs;
229
+ throw err;
230
+ }
231
+ },
232
+ `POST ${url}`,
233
+ opts
234
+ );
235
+ }
236
+ function mergePatches(target, source) {
237
+ var _a, _b, _c, _d;
238
+ const out = { ...target, ...source };
239
+ if (target.properties || source.properties) {
240
+ out.properties = { ...(_a = target.properties) != null ? _a : {}, ...(_b = source.properties) != null ? _b : {} };
241
+ }
242
+ if (target.attachments || source.attachments) {
243
+ out.attachments = [...(_c = target.attachments) != null ? _c : [], ...(_d = source.attachments) != null ? _d : []];
244
+ }
245
+ return out;
246
+ }
247
+ var EventShipper = class {
248
+ constructor(opts) {
249
+ this.buffers = /* @__PURE__ */ new Map();
250
+ this.sticky = /* @__PURE__ */ new Map();
251
+ this.timers = /* @__PURE__ */ new Map();
252
+ this.inFlight = /* @__PURE__ */ new Set();
253
+ var _a, _b;
254
+ this.writeKey = opts.writeKey;
255
+ this.baseUrl = (_a = formatEndpoint(opts.endpoint)) != null ? _a : "https://api.raindrop.ai/v1/";
256
+ this.debug = opts.debug;
257
+ this.partialFlushMs = (_b = opts.partialFlushMs) != null ? _b : 1e3;
258
+ }
259
+ async patch(eventId, patch) {
260
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
261
+ if (!eventId) return;
262
+ if (this.debug) {
263
+ console.log("[raindrop-ai/opencode-plugin] queue patch", {
264
+ eventId,
265
+ userId: patch.userId,
266
+ hasInput: typeof patch.input === "string" && patch.input.length > 0,
267
+ hasOutput: typeof patch.output === "string" && patch.output.length > 0,
268
+ isPending: patch.isPending
269
+ });
270
+ }
271
+ const sticky = (_a = this.sticky.get(eventId)) != null ? _a : {};
272
+ const existing = (_b = this.buffers.get(eventId)) != null ? _b : {};
273
+ const merged = mergePatches(existing, patch);
274
+ merged.isPending = (_e = (_d = (_c = patch.isPending) != null ? _c : existing.isPending) != null ? _d : sticky.isPending) != null ? _e : true;
275
+ this.buffers.set(eventId, merged);
276
+ this.sticky.set(eventId, {
277
+ userId: (_f = merged.userId) != null ? _f : sticky.userId,
278
+ convoId: (_g = merged.convoId) != null ? _g : sticky.convoId,
279
+ eventName: (_h = merged.eventName) != null ? _h : sticky.eventName,
280
+ isPending: (_i = merged.isPending) != null ? _i : sticky.isPending
281
+ });
282
+ const t = this.timers.get(eventId);
283
+ if (t) clearTimeout(t);
284
+ if (merged.isPending === false) {
285
+ await this.flushOne(eventId);
286
+ return;
287
+ }
288
+ const timeout = setTimeout(() => {
289
+ void this.flushOne(eventId).catch(() => {
290
+ });
291
+ }, this.partialFlushMs);
292
+ this.timers.set(eventId, timeout);
293
+ }
294
+ async finish(eventId, patch) {
295
+ await this.patch(eventId, { ...patch, isPending: false });
296
+ }
297
+ async flush() {
298
+ const ids = [...this.buffers.keys()];
299
+ await Promise.all(ids.map((id) => this.flushOne(id)));
300
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
301
+ })));
302
+ }
303
+ async shutdown() {
304
+ for (const t of this.timers.values()) clearTimeout(t);
305
+ this.timers.clear();
306
+ await this.flush();
307
+ }
308
+ async flushOne(eventId) {
309
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
310
+ const timer = this.timers.get(eventId);
311
+ if (timer) {
312
+ clearTimeout(timer);
313
+ this.timers.delete(eventId);
314
+ }
315
+ const accumulated = this.buffers.get(eventId);
316
+ this.buffers.delete(eventId);
317
+ if (!accumulated) return;
318
+ const sticky = (_a = this.sticky.get(eventId)) != null ? _a : {};
319
+ const eventName = (_c = (_b = accumulated.eventName) != null ? _b : sticky.eventName) != null ? _c : "opencode_session";
320
+ const userId = (_d = accumulated.userId) != null ? _d : sticky.userId;
321
+ if (!userId) {
322
+ if (this.debug) {
323
+ console.warn(
324
+ `[raindrop-ai/opencode-plugin] skipping track_partial for ${eventId}: missing userId`
325
+ );
326
+ }
327
+ return;
328
+ }
329
+ const payload = {
330
+ event_id: eventId,
331
+ user_id: userId,
332
+ event: eventName,
333
+ timestamp: (_e = accumulated.timestamp) != null ? _e : (/* @__PURE__ */ new Date()).toISOString(),
334
+ ai_data: {
335
+ input: accumulated.input,
336
+ output: accumulated.output,
337
+ model: accumulated.model,
338
+ convo_id: (_f = accumulated.convoId) != null ? _f : sticky.convoId
339
+ },
340
+ properties: {
341
+ ...(_g = accumulated.properties) != null ? _g : {},
342
+ $context: {
343
+ library: { name: "@raindrop-ai/opencode-plugin", version: PLUGIN_VERSION },
344
+ metadata: { jsRuntime: "node" }
345
+ }
346
+ },
347
+ attachments: accumulated.attachments,
348
+ is_pending: ((_i = (_h = accumulated.isPending) != null ? _h : sticky.isPending) != null ? _i : true) !== false
349
+ };
350
+ const url = `${this.baseUrl}events/track_partial`;
351
+ if (this.debug) {
352
+ console.log("[raindrop-ai/opencode-plugin] sending track_partial", {
353
+ eventId,
354
+ eventName,
355
+ userId,
356
+ isPending: payload["is_pending"],
357
+ inputPreview: typeof accumulated.input === "string" ? accumulated.input.slice(0, 120) : void 0,
358
+ outputPreview: typeof accumulated.output === "string" ? accumulated.output.slice(0, 120) : void 0,
359
+ endpoint: url
360
+ });
361
+ }
362
+ const p = postJson(
363
+ url,
364
+ payload,
365
+ { Authorization: `Bearer ${this.writeKey}` },
366
+ { maxAttempts: 3, debug: this.debug }
367
+ );
368
+ this.inFlight.add(p);
369
+ try {
370
+ try {
371
+ await p;
372
+ if (this.debug) {
373
+ console.log(`[raindrop-ai/opencode-plugin] sent track_partial ${eventId} (${eventName})`);
374
+ }
375
+ } catch (err) {
376
+ if (this.debug) {
377
+ const msg = err instanceof Error ? err.message : String(err);
378
+ console.warn(
379
+ `[raindrop-ai/opencode-plugin] failed to send track_partial (dropping): ${msg}`
380
+ );
381
+ }
382
+ }
383
+ } finally {
384
+ this.inFlight.delete(p);
385
+ }
386
+ if (!payload["is_pending"]) this.sticky.delete(eventId);
387
+ }
388
+ };
389
+ var TraceShipper = class {
390
+ constructor(opts) {
391
+ this.queue = [];
392
+ this.inFlight = /* @__PURE__ */ new Set();
393
+ var _a, _b;
394
+ this.writeKey = opts.writeKey;
395
+ this.baseUrl = (_a = formatEndpoint(opts.endpoint)) != null ? _a : "https://api.raindrop.ai/v1/";
396
+ this.debug = opts.debug;
397
+ this.flushIntervalMs = (_b = opts.flushIntervalMs) != null ? _b : 1e3;
398
+ this.maxBatchSize = 50;
399
+ this.maxQueueSize = 5e3;
400
+ }
401
+ startSpan(args) {
402
+ var _a;
403
+ const ids = createSpanIds(args.parent);
404
+ const started = nowUnixNanoString();
405
+ const attrs = [
406
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
407
+ ];
408
+ if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
409
+ return { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
410
+ }
411
+ endSpan(span, extra) {
412
+ var _a;
413
+ if (span.endTimeUnixNano) return;
414
+ span.endTimeUnixNano = nowUnixNanoString();
415
+ if ((_a = extra == null ? void 0 : extra.attributes) == null ? void 0 : _a.length) span.attributes.push(...extra.attributes);
416
+ let status;
417
+ if ((extra == null ? void 0 : extra.error) !== void 0) {
418
+ const message = extra.error instanceof Error ? extra.error.message : String(extra.error);
419
+ status = { code: SpanStatusCode.ERROR, message };
420
+ }
421
+ const otlp = buildOtlpSpan({
422
+ ids: span.ids,
423
+ name: span.name,
424
+ startTimeUnixNano: span.startTimeUnixNano,
425
+ endTimeUnixNano: span.endTimeUnixNano,
426
+ attributes: span.attributes,
427
+ status
428
+ });
429
+ this.enqueue(otlp);
430
+ }
431
+ enqueue(span) {
432
+ if (this.queue.length >= this.maxQueueSize) this.queue.shift();
433
+ this.queue.push(span);
434
+ if (this.queue.length >= this.maxBatchSize) {
435
+ void this.flush().catch(() => {
436
+ });
437
+ return;
438
+ }
439
+ if (!this.timer) {
440
+ this.timer = setTimeout(() => {
441
+ this.timer = void 0;
442
+ void this.flush().catch(() => {
443
+ });
444
+ }, this.flushIntervalMs);
445
+ }
446
+ }
447
+ async flush() {
448
+ if (this.timer) {
449
+ clearTimeout(this.timer);
450
+ this.timer = void 0;
451
+ }
452
+ while (this.queue.length > 0) {
453
+ const batch = this.queue.splice(0, this.maxBatchSize);
454
+ const body = buildExportTraceServiceRequest(batch);
455
+ const url = `${this.baseUrl}traces`;
456
+ if (this.debug) {
457
+ console.log("[raindrop-ai/opencode-plugin] sending traces batch", {
458
+ spans: batch.length,
459
+ endpoint: url
460
+ });
461
+ }
462
+ const p = postJson(
463
+ url,
464
+ body,
465
+ { Authorization: `Bearer ${this.writeKey}` },
466
+ { maxAttempts: 3, debug: this.debug }
467
+ );
468
+ this.inFlight.add(p);
469
+ try {
470
+ try {
471
+ await p;
472
+ if (this.debug) {
473
+ console.log(`[raindrop-ai/opencode-plugin] sent ${batch.length} spans`);
474
+ }
475
+ } catch (err) {
476
+ if (this.debug) {
477
+ const msg = err instanceof Error ? err.message : String(err);
478
+ console.warn(
479
+ `[raindrop-ai/opencode-plugin] failed to send spans (dropping): ${msg}`
480
+ );
481
+ }
482
+ }
483
+ } finally {
484
+ this.inFlight.delete(p);
485
+ }
486
+ }
487
+ }
488
+ async shutdown() {
489
+ if (this.timer) {
490
+ clearTimeout(this.timer);
491
+ this.timer = void 0;
492
+ }
493
+ await this.flush();
494
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
495
+ })));
496
+ }
497
+ };
498
+
499
+ // src/tracing.ts
500
+ var sessions = /* @__PURE__ */ new Map();
501
+ function getHostname() {
502
+ var _a, _b;
503
+ try {
504
+ const bunGlobal = globalThis;
505
+ if ((_a = bunGlobal.Bun) == null ? void 0 : _a.hostname) return bunGlobal.Bun.hostname;
506
+ return require("os").hostname();
507
+ } catch (e) {
508
+ return (_b = process.env["HOSTNAME"]) != null ? _b : "unknown";
509
+ }
510
+ }
511
+ function getProjectName(worktree) {
512
+ var _a;
513
+ const parts = worktree.split("/").filter(Boolean);
514
+ return (_a = parts[parts.length - 1]) != null ? _a : "unknown";
515
+ }
516
+ function createHooks(client, config, worktree, directory, eventShipper, traceShipper) {
517
+ function log(msg, data) {
518
+ var _a, _b;
519
+ (_b = (_a = client.app.log({
520
+ body: {
521
+ service: "raindrop-opencode",
522
+ level: config.debug ? "info" : "debug",
523
+ message: msg,
524
+ ...data !== void 0 ? { extra: data } : {}
525
+ }
526
+ })) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, () => {
527
+ });
528
+ }
529
+ log("Raindrop tracing hooks registered");
530
+ return {
531
+ // ------------------------------------------------------------------
532
+ // event — session lifecycle + streaming parts
533
+ // ------------------------------------------------------------------
534
+ event: async ({
535
+ event
536
+ }) => {
537
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
538
+ try {
539
+ const props = event.properties;
540
+ const info = props["info"];
541
+ const sessionID = props["sessionID"] || (info == null ? void 0 : info["id"]) || props["id"];
542
+ if (event.type === "session.created") {
543
+ if (!sessionID) {
544
+ log("session.created: no session ID, skipping");
545
+ return;
546
+ }
547
+ const userId = (_a = config.userId) != null ? _a : sessionID;
548
+ const hostname = getHostname();
549
+ const os = process.platform;
550
+ const rootSpan = traceShipper.startSpan({
551
+ name: `OpenCode: ${getProjectName(worktree)}`,
552
+ eventId: sessionID,
553
+ attributes: [
554
+ attrString("workspace", worktree),
555
+ attrString("directory", directory),
556
+ attrString("hostname", hostname),
557
+ attrString("os", os)
558
+ ]
559
+ });
560
+ const state = {
561
+ eventId: sessionID,
562
+ rootSpan,
563
+ turnNumber: 0,
564
+ toolCount: 0,
565
+ startTime: Date.now(),
566
+ currentInput: "",
567
+ currentOutput: "",
568
+ outputParts: /* @__PURE__ */ new Map(),
569
+ reasoningParts: /* @__PURE__ */ new Map(),
570
+ toolCalls: /* @__PURE__ */ new Map(),
571
+ toolStartTimes: /* @__PURE__ */ new Map(),
572
+ toolSpans: /* @__PURE__ */ new Map(),
573
+ toolCallMessageIds: /* @__PURE__ */ new Map(),
574
+ processedMessages: /* @__PURE__ */ new Set(),
575
+ attachments: []
576
+ };
577
+ sessions.set(sessionID, state);
578
+ await eventShipper.patch(sessionID, {
579
+ isPending: true,
580
+ userId,
581
+ convoId: sessionID,
582
+ eventName: config.eventName,
583
+ properties: {
584
+ workspace: worktree,
585
+ directory,
586
+ hostname,
587
+ os,
588
+ turn_count: 0,
589
+ tool_count: 0
590
+ }
591
+ });
592
+ log("Session created", { sessionID, userId });
593
+ } else if (event.type === "message.part.updated") {
594
+ const part = props["part"];
595
+ if (!part) return;
596
+ const partSessionID = part["sessionID"];
597
+ const messageId = part["messageID"];
598
+ if (!partSessionID || !messageId) return;
599
+ const state = sessions.get(partSessionID);
600
+ if (!state) return;
601
+ if (part["type"] === "text" && typeof part["text"] === "string") {
602
+ state.outputParts.set(messageId, part["text"]);
603
+ } else if (part["type"] === "tool" && messageId) {
604
+ const callID = part["callID"];
605
+ const tool = part["tool"];
606
+ const partState = part["state"];
607
+ const input = partState == null ? void 0 : partState["input"];
608
+ if (callID && tool && input) {
609
+ let calls = state.toolCalls.get(messageId);
610
+ if (!calls) {
611
+ calls = [];
612
+ state.toolCalls.set(messageId, calls);
613
+ }
614
+ const existingIdx = calls.findIndex((c) => c.callId === callID);
615
+ const entry = { callId: callID, name: tool, args: JSON.stringify(input) };
616
+ if (existingIdx >= 0) calls[existingIdx] = entry;
617
+ else calls.push(entry);
618
+ state.toolCallMessageIds.set(callID, messageId);
619
+ }
620
+ } else if (part["type"] === "reasoning" && typeof part["text"] === "string") {
621
+ state.reasoningParts.set(messageId, part["text"]);
622
+ } else {
623
+ log(`message.part.updated: skipping part type=${String(part["type"])}`);
624
+ }
625
+ } else if (event.type === "message.updated") {
626
+ const msgInfo = props["info"];
627
+ if (!msgInfo) return;
628
+ const role = msgInfo["role"];
629
+ if (role !== "assistant") return;
630
+ const msgSessionID = msgInfo["sessionID"];
631
+ const messageId = msgInfo["id"];
632
+ const time = msgInfo["time"];
633
+ if (!msgSessionID || !messageId || !(time == null ? void 0 : time["completed"])) return;
634
+ const state = sessions.get(msgSessionID);
635
+ if (!state || !state.currentTurnSpan) return;
636
+ if (state.processedMessages.has(messageId)) return;
637
+ state.processedMessages.add(messageId);
638
+ const tokens = msgInfo["tokens"];
639
+ const inputTokens = (tokens == null ? void 0 : tokens["input"]) || 0;
640
+ const outputTokens = (tokens == null ? void 0 : tokens["output"]) || 0;
641
+ const reasoningTokens = (tokens == null ? void 0 : tokens["reasoning"]) || 0;
642
+ const providerID = msgInfo["providerID"] || "unknown";
643
+ const modelID = msgInfo["modelID"] || "unknown";
644
+ const modelName = `${providerID}/${modelID}`;
645
+ state.model = modelName;
646
+ const outputText = (_b = state.outputParts.get(messageId)) != null ? _b : "";
647
+ state.currentOutput = outputText;
648
+ const llmAttrs = [
649
+ attrString("gen_ai.system", providerID),
650
+ attrString("gen_ai.request.model", modelID),
651
+ attrInt("gen_ai.usage.input_tokens", inputTokens),
652
+ attrInt("gen_ai.usage.output_tokens", outputTokens)
653
+ ];
654
+ if (reasoningTokens > 0) {
655
+ llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
656
+ }
657
+ const llmSpan = traceShipper.startSpan({
658
+ name: modelName,
659
+ parent: state.currentTurnSpan.ids,
660
+ eventId: state.eventId,
661
+ attributes: llmAttrs
662
+ });
663
+ if (typeof time["created"] === "number") {
664
+ llmSpan.startTimeUnixNano = String(time["created"] * 1e6);
665
+ }
666
+ const msgError = msgInfo["error"];
667
+ const errorForSpan = msgError ? `${(_e = (_d = (_c = msgError.data) == null ? void 0 : _c.message) != null ? _d : msgError.name) != null ? _e : "Error"}
668
+
669
+ type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
670
+ traceShipper.endSpan(llmSpan, { error: errorForSpan });
671
+ await eventShipper.patch(state.eventId, {
672
+ model: modelName,
673
+ output: outputText || void 0
674
+ });
675
+ log("LLM span created", {
676
+ messageId,
677
+ modelName,
678
+ inputTokens,
679
+ outputTokens,
680
+ reasoningTokens
681
+ });
682
+ } else if (event.type === "session.idle") {
683
+ if (!sessionID) return;
684
+ const state = sessions.get(String(sessionID));
685
+ if (!state) return;
686
+ if (state.currentTurnSpan) {
687
+ traceShipper.endSpan(state.currentTurnSpan);
688
+ state.currentTurnSpan = void 0;
689
+ }
690
+ await eventShipper.patch(state.eventId, {
691
+ output: state.currentOutput || void 0,
692
+ properties: {
693
+ turn_count: state.turnNumber,
694
+ tool_count: state.toolCount
695
+ }
696
+ });
697
+ log("Session idle \u2014 turn span closed", { sessionID });
698
+ } else if (event.type === "session.deleted") {
699
+ if (!sessionID) return;
700
+ const state = sessions.get(String(sessionID));
701
+ if (!state) return;
702
+ if (state.currentTurnSpan) {
703
+ traceShipper.endSpan(state.currentTurnSpan);
704
+ state.currentTurnSpan = void 0;
705
+ }
706
+ traceShipper.endSpan(state.rootSpan);
707
+ await eventShipper.finish(state.eventId, {
708
+ output: state.currentOutput || void 0,
709
+ model: state.model,
710
+ properties: {
711
+ turn_count: state.turnNumber,
712
+ tool_count: state.toolCount
713
+ }
714
+ });
715
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
716
+ sessions.delete(String(sessionID));
717
+ log("Session deleted \u2014 flushed", { sessionID });
718
+ } else if (event.type === "session.error") {
719
+ const errorSessionID = props["sessionID"] || sessionID;
720
+ if (!errorSessionID) return;
721
+ const state = sessions.get(String(errorSessionID));
722
+ if (!state) return;
723
+ const errorObj = props["error"];
724
+ const errorName = (_g = errorObj == null ? void 0 : errorObj.name) != null ? _g : "UnknownError";
725
+ const errorMessage = (_i = (_h = errorObj == null ? void 0 : errorObj.data) == null ? void 0 : _h.message) != null ? _i : errorName;
726
+ const errorStr = `${errorMessage}
727
+
728
+ type: ${errorName}`;
729
+ if (state.currentTurnSpan) {
730
+ traceShipper.endSpan(state.currentTurnSpan, { error: errorStr });
731
+ state.currentTurnSpan = void 0;
732
+ }
733
+ traceShipper.endSpan(state.rootSpan, { error: errorStr });
734
+ await eventShipper.finish(state.eventId, {
735
+ properties: {
736
+ error: errorStr,
737
+ turn_count: state.turnNumber,
738
+ tool_count: state.toolCount
739
+ }
740
+ });
741
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
742
+ sessions.delete(String(errorSessionID));
743
+ log("Session error handled", { errorSessionID, errorName });
744
+ } else {
745
+ log(`unhandled event: ${event.type}`);
746
+ }
747
+ } catch (err) {
748
+ (_k = (_j = client.app.log({
749
+ body: {
750
+ service: "raindrop-opencode",
751
+ level: "error",
752
+ message: `Error in event hook: ${err instanceof Error ? err.message : String(err)}`
753
+ }
754
+ })) == null ? void 0 : _j.catch) == null ? void 0 : _k.call(_j, () => {
755
+ });
756
+ }
757
+ },
758
+ // ------------------------------------------------------------------
759
+ // chat.message — user sent a message; start a new turn span
760
+ // ------------------------------------------------------------------
761
+ "chat.message": async (messageInput, output) => {
762
+ var _a;
763
+ try {
764
+ const { sessionID } = messageInput;
765
+ const state = sessions.get(sessionID);
766
+ if (!state) return;
767
+ if (state.currentTurnSpan) {
768
+ traceShipper.endSpan(state.currentTurnSpan);
769
+ state.currentTurnSpan = void 0;
770
+ }
771
+ state.turnNumber++;
772
+ state.currentOutput = "";
773
+ const parts = (_a = output == null ? void 0 : output.parts) != null ? _a : [];
774
+ const textParts = [];
775
+ const newAttachments = [];
776
+ for (const part of parts) {
777
+ if (part["type"] === "text" && typeof part["text"] === "string") {
778
+ textParts.push(part["text"]);
779
+ } else if (part["type"] === "file") {
780
+ const mimeType = part["mediaType"];
781
+ const filename = part["filename"];
782
+ const url = part["url"];
783
+ if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
784
+ newAttachments.push({ type: "image", role: "input", name: filename, value: url });
785
+ }
786
+ }
787
+ }
788
+ const userText = textParts.join("\n");
789
+ state.currentInput = userText;
790
+ if (newAttachments.length > 0) state.attachments.push(...newAttachments);
791
+ state.currentTurnSpan = traceShipper.startSpan({
792
+ name: `Turn ${state.turnNumber}`,
793
+ parent: state.rootSpan.ids,
794
+ eventId: state.eventId,
795
+ attributes: [attrInt("turn.number", state.turnNumber)]
796
+ });
797
+ await eventShipper.patch(state.eventId, {
798
+ input: userText || void 0,
799
+ attachments: newAttachments.length > 0 ? newAttachments : void 0,
800
+ properties: { turn_count: state.turnNumber }
801
+ });
802
+ log("Chat message \u2014 new turn started", {
803
+ sessionID,
804
+ turnNumber: state.turnNumber,
805
+ inputLength: userText.length,
806
+ attachments: newAttachments.length
807
+ });
808
+ } catch (err) {
809
+ log("Error in chat.message hook", { error: String(err) });
810
+ }
811
+ },
812
+ // ------------------------------------------------------------------
813
+ // tool.execute.before — record start time and open tool span
814
+ // ------------------------------------------------------------------
815
+ "tool.execute.before": async (toolInput, _output) => {
816
+ try {
817
+ const { tool, sessionID, callID } = toolInput;
818
+ const state = sessions.get(sessionID);
819
+ if (!state || !state.currentTurnSpan) return;
820
+ state.toolStartTimes.set(callID, Date.now());
821
+ const toolSpan = traceShipper.startSpan({
822
+ name: tool,
823
+ parent: state.currentTurnSpan.ids,
824
+ eventId: state.eventId,
825
+ attributes: [attrString("tool.name", tool)]
826
+ });
827
+ state.toolSpans.set(callID, toolSpan);
828
+ log("Tool execute before", { tool, sessionID, callID });
829
+ } catch (err) {
830
+ log("Error in tool.execute.before hook", { error: String(err) });
831
+ }
832
+ },
833
+ // ------------------------------------------------------------------
834
+ // tool.execute.after — finalize tool span
835
+ // ------------------------------------------------------------------
836
+ "tool.execute.after": async (toolInput, result) => {
837
+ var _a;
838
+ try {
839
+ const { tool, sessionID, callID } = toolInput;
840
+ const state = sessions.get(sessionID);
841
+ if (!state) return;
842
+ state.toolCount++;
843
+ state.toolStartTimes.delete(callID);
844
+ state.toolCallMessageIds.delete(callID);
845
+ const toolSpan = state.toolSpans.get(callID);
846
+ state.toolSpans.delete(callID);
847
+ if (toolSpan) {
848
+ const outputPreview = typeof result.output === "string" ? result.output.slice(0, 200) : JSON.stringify((_a = result.output) != null ? _a : "").slice(0, 200);
849
+ traceShipper.endSpan(toolSpan, {
850
+ attributes: [
851
+ attrString("tool.title", result.title),
852
+ attrString("tool.output_preview", outputPreview)
853
+ ]
854
+ });
855
+ }
856
+ await eventShipper.patch(state.eventId, {
857
+ properties: { tool_count: state.toolCount }
858
+ });
859
+ log("Tool execute after", { tool, sessionID, callID });
860
+ } catch (err) {
861
+ log("Error in tool.execute.after hook", { error: String(err) });
862
+ }
863
+ }
864
+ };
865
+ }
866
+
867
+ // src/index.ts
868
+ async function plugin(input) {
869
+ var _a;
870
+ const { client } = input;
871
+ const config = loadConfig(input.directory);
872
+ function appLog(level, message) {
873
+ var _a2, _b;
874
+ (_b = (_a2 = client.app.log({
875
+ body: {
876
+ service: "raindrop-opencode",
877
+ level,
878
+ message
879
+ }
880
+ })) == null ? void 0 : _a2.catch) == null ? void 0 : _b.call(_a2, () => {
881
+ });
882
+ }
883
+ if (!config.writeKey) {
884
+ appLog(
885
+ "warn",
886
+ "RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
887
+ );
888
+ return { hooks: {} };
889
+ }
890
+ if (config.debug) {
891
+ appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
892
+ }
893
+ const eventShipper = new EventShipper({
894
+ writeKey: config.writeKey,
895
+ endpoint: config.endpoint,
896
+ debug: config.debug
897
+ });
898
+ const traceShipper = new TraceShipper({
899
+ writeKey: config.writeKey,
900
+ endpoint: config.endpoint,
901
+ debug: config.debug
902
+ });
903
+ const worktree = (_a = input.worktree) != null ? _a : input.directory;
904
+ const hooks = createHooks(
905
+ client,
906
+ config,
907
+ worktree,
908
+ input.directory,
909
+ eventShipper,
910
+ traceShipper
911
+ );
912
+ return { hooks };
913
+ }