@raindrop-ai/opencode-plugin 0.0.11 → 0.0.13

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.js CHANGED
@@ -5,124 +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
- // src/config.ts
9
- import { existsSync, readFileSync } from "fs";
10
- import { homedir } from "os";
11
- import { join } from "path";
12
- function loadConfig(projectDirectory) {
13
- var _a, _b, _c, _d, _e, _f, _g;
14
- let merged = {};
15
- const configPaths = [
16
- join(homedir(), ".config", "opencode", "raindrop.json"),
17
- join(projectDirectory, ".opencode", "raindrop.json")
18
- ];
19
- for (const configPath of configPaths) {
20
- try {
21
- if (existsSync(configPath)) {
22
- const content = readFileSync(configPath, "utf-8");
23
- const parsed = JSON.parse(content);
24
- merged = { ...merged, ...parsed };
25
- }
26
- } catch (e) {
27
- }
28
- }
29
- let eventMetadata;
30
- const envMeta = process.env["RAINDROP_EVENT_METADATA"];
31
- if (envMeta) {
32
- try {
33
- eventMetadata = JSON.parse(envMeta);
34
- } catch (e) {
35
- }
36
- }
37
- return {
38
- writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
39
- endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
40
- eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
41
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
42
- captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
43
- eventMetadata
44
- };
45
- }
46
-
47
- // package.json
48
- var package_default = {
49
- name: "@raindrop-ai/opencode-plugin",
50
- version: "0.0.11",
51
- description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
52
- type: "module",
53
- main: "dist/index.js",
54
- module: "dist/index.js",
55
- types: "dist/index.d.ts",
56
- license: "MIT",
57
- repository: {
58
- type: "git",
59
- url: "git+https://github.com/raindrop-ai/raindrop-js.git",
60
- directory: "packages/opencode-plugin"
61
- },
62
- homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
63
- bugs: {
64
- url: "https://github.com/raindrop-ai/raindrop-js/issues"
65
- },
66
- exports: {
67
- ".": {
68
- types: "./dist/index.d.ts",
69
- import: "./dist/index.js",
70
- require: "./dist/index.cjs"
71
- }
72
- },
73
- sideEffects: false,
74
- files: [
75
- "dist/**"
76
- ],
77
- scripts: {
78
- build: "tsup",
79
- dev: "tsup --watch",
80
- clean: "rm -rf dist",
81
- test: "cd tests && pnpm test"
82
- },
83
- peerDependencies: {
84
- "@opencode-ai/plugin": ">=1.3.0",
85
- "@opencode-ai/sdk": ">=1.3.0"
86
- },
87
- peerDependenciesMeta: {
88
- "@opencode-ai/sdk": {
89
- optional: true
90
- }
91
- },
92
- devDependencies: {
93
- "@raindrop-ai/core": "workspace:*",
94
- "@opencode-ai/plugin": "^1.3.3",
95
- "@opencode-ai/sdk": "^1.3.3",
96
- "@types/node": "^20.11.17",
97
- tsup: "^8.4.0",
98
- typescript: "^5.3.3"
99
- },
100
- tsup: {
101
- entry: [
102
- "src/index.ts"
103
- ],
104
- format: [
105
- "cjs",
106
- "esm"
107
- ],
108
- dts: {
109
- resolve: true
110
- },
111
- clean: true,
112
- noExternal: [
113
- "@raindrop-ai/core"
114
- ]
115
- },
116
- publishConfig: {
117
- access: "public"
118
- }
119
- };
120
-
121
- // src/package-info.ts
122
- var PLUGIN_NAME = package_default.name;
123
- var PLUGIN_VERSION = package_default.version;
124
-
125
- // ../core/dist/chunk-4UCYIEH4.js
8
+ // ../core/dist/chunk-U5HUTMR5.js
126
9
  function getCrypto() {
127
10
  const c = globalThis.crypto;
128
11
  return c;
@@ -169,6 +52,8 @@ function base64Encode(bytes) {
169
52
  function generateId() {
170
53
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
171
54
  }
55
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
56
+ var MAX_RETRY_DELAY_MS = 3e4;
172
57
  function wait(ms) {
173
58
  return new Promise((resolve) => setTimeout(resolve, ms));
174
59
  }
@@ -176,6 +61,46 @@ function formatEndpoint(endpoint) {
176
61
  if (!endpoint) return void 0;
177
62
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
178
63
  }
64
+ function redactUrlForLog(url) {
65
+ try {
66
+ const parsed = new URL(url);
67
+ parsed.username = "";
68
+ parsed.password = "";
69
+ parsed.search = "";
70
+ parsed.hash = "";
71
+ return parsed.toString();
72
+ } catch (e) {
73
+ return "<unparseable-url>";
74
+ }
75
+ }
76
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
77
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
78
+ function rateLimitedLog(key, log) {
79
+ const now = Date.now();
80
+ const last = rateLimitedLogLast.get(key);
81
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
82
+ return false;
83
+ }
84
+ rateLimitedLogLast.set(key, now);
85
+ log();
86
+ return true;
87
+ }
88
+ async function raceWithTimeout(promise, timeoutMs) {
89
+ let timer;
90
+ const settledInTime = await Promise.race([
91
+ promise.then(
92
+ () => true,
93
+ () => true
94
+ ),
95
+ new Promise((resolve) => {
96
+ var _a;
97
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
98
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
99
+ })
100
+ ]);
101
+ if (timer) clearTimeout(timer);
102
+ return settledInTime;
103
+ }
179
104
  function parseRetryAfter(headers) {
180
105
  var _a;
181
106
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -192,12 +117,12 @@ function parseRetryAfter(headers) {
192
117
  function getRetryDelayMs(attemptNumber, previousError) {
193
118
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
194
119
  const v = previousError.retryAfterMs;
195
- if (typeof v === "number") return Math.max(0, v);
120
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
196
121
  }
197
122
  if (attemptNumber <= 1) return 0;
198
123
  const base = 500;
199
124
  const factor = Math.pow(2, attemptNumber - 2);
200
- return base * factor;
125
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
201
126
  }
202
127
  async function withRetry(operation, opName, opts) {
203
128
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -232,7 +157,9 @@ async function withRetry(operation, opName, opts) {
232
157
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
233
158
  }
234
159
  async function postJson(url, body, headers, opts) {
235
- const opName = `POST ${url}`;
160
+ var _a;
161
+ const opName = `POST ${redactUrlForLog(url)}`;
162
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
236
163
  await withRetry(
237
164
  async () => {
238
165
  const resp = await fetch(url, {
@@ -241,7 +168,8 @@ async function postJson(url, body, headers, opts) {
241
168
  "Content-Type": "application/json",
242
169
  ...headers
243
170
  },
244
- body: JSON.stringify(body)
171
+ body: JSON.stringify(body),
172
+ signal: AbortSignal.timeout(timeoutMs)
245
173
  });
246
174
  if (!resp.ok) {
247
175
  const text = await resp.text().catch(() => "");
@@ -258,6 +186,27 @@ async function postJson(url, body, headers, opts) {
258
186
  opts
259
187
  );
260
188
  }
189
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
190
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
191
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
192
+ function resolveMaxTextFieldChars(value) {
193
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
194
+ return Math.floor(value);
195
+ }
196
+ return currentDefaultMaxTextFieldChars;
197
+ }
198
+ function truncateToLimit(text, limit) {
199
+ if (limit > TRUNCATION_MARKER.length) {
200
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
201
+ }
202
+ return text.slice(0, Math.max(0, limit));
203
+ }
204
+ function capText(value, limit) {
205
+ if (typeof value !== "string") return value;
206
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
207
+ if (value.length <= max) return value;
208
+ return truncateToLimit(value, max);
209
+ }
261
210
  var SpanStatusCode = {
262
211
  UNSET: 0,
263
212
  OK: 1,
@@ -315,6 +264,93 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
315
264
  ]
316
265
  };
317
266
  }
267
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
268
+ var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
269
+ var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
270
+ function readEnvVar(name) {
271
+ var _a;
272
+ try {
273
+ const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
274
+ if (env && typeof env[name] === "string" && env[name].length > 0) {
275
+ return env[name];
276
+ }
277
+ } catch (e) {
278
+ }
279
+ return void 0;
280
+ }
281
+ function readWorkshopEnv() {
282
+ const raw = readEnvVar(WORKSHOP_ENV_VAR);
283
+ if (raw === void 0) return void 0;
284
+ const trimmed = raw.trim();
285
+ if (trimmed.length === 0) return void 0;
286
+ if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
287
+ if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
288
+ if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
289
+ return void 0;
290
+ }
291
+ function isLocalDevHost(hostname) {
292
+ if (!hostname) return false;
293
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
294
+ return true;
295
+ }
296
+ if (hostname.endsWith(".localhost")) return true;
297
+ return false;
298
+ }
299
+ function readRuntimeHostname() {
300
+ try {
301
+ const loc = globalThis == null ? void 0 : globalThis.location;
302
+ if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
303
+ return loc.hostname;
304
+ }
305
+ } catch (e) {
306
+ }
307
+ return void 0;
308
+ }
309
+ function shouldAutoEnableLocalWorkshop() {
310
+ if (isLocalDevHost(readRuntimeHostname())) return true;
311
+ if (readEnvVar("NODE_ENV") === "development") return true;
312
+ return false;
313
+ }
314
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
315
+ var _a, _b, _c;
316
+ if (baseUrl === null) return null;
317
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
318
+ return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
319
+ }
320
+ const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
321
+ if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
322
+ const workshopEnv = readWorkshopEnv();
323
+ if (workshopEnv === "disable") return null;
324
+ if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
325
+ if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
326
+ if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
327
+ return null;
328
+ }
329
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
330
+ var _a;
331
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
332
+ if (!baseUrl) return;
333
+ void postJson(`${baseUrl}traces`, body, {}, {
334
+ maxAttempts: 1,
335
+ debug: (_a = options.debug) != null ? _a : false,
336
+ sdkName: options.sdkName
337
+ }).catch(() => {
338
+ });
339
+ }
340
+ function mirrorPartialEventToLocalDebugger(event, options = {}) {
341
+ var _a;
342
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
343
+ if (!baseUrl) return;
344
+ const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
345
+ void postJson(`${baseUrl}events/track_partial`, event, headers, {
346
+ maxAttempts: 1,
347
+ debug: (_a = options.debug) != null ? _a : false,
348
+ sdkName: options.sdkName
349
+ }).catch(() => {
350
+ });
351
+ }
352
+ var SHUTDOWN_DEADLINE_MS = 1e4;
353
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
318
354
  function mergePatches(target, source) {
319
355
  var _a, _b, _c, _d;
320
356
  const out = { ...target, ...source };
@@ -332,7 +368,8 @@ var EventShipper = class {
332
368
  this.sticky = /* @__PURE__ */ new Map();
333
369
  this.timers = /* @__PURE__ */ new Map();
334
370
  this.inFlight = /* @__PURE__ */ new Set();
335
- var _a, _b, _c, _d, _e, _f, _g;
371
+ this.hasShutdown = false;
372
+ var _a, _b, _c, _d, _e, _f, _g, _h;
336
373
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
337
374
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
338
375
  this.enabled = opts.enabled !== false;
@@ -341,11 +378,16 @@ var EventShipper = class {
341
378
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
342
379
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
343
380
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
381
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
382
+ this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
383
+ if (this.debug && this.localDebuggerUrl) {
384
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
385
+ }
344
386
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
345
387
  this.context = {
346
388
  library: {
347
- name: (_f = opts.libraryName) != null ? _f : "@raindrop-ai/core",
348
- version: (_g = opts.libraryVersion) != null ? _g : "0.0.0"
389
+ name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
390
+ version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
349
391
  },
350
392
  metadata: {
351
393
  jsRuntime: isNode ? "node" : "web",
@@ -359,10 +401,52 @@ var EventShipper = class {
359
401
  authHeaders() {
360
402
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
361
403
  }
404
+ /**
405
+ * Build the retry/timeout options for one POST, honoring the shutdown
406
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
407
+ * the caller must drop the payload (with a rate-limited warning) instead
408
+ * of issuing a request that could outlive process exit.
409
+ *
410
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
411
+ * path is mid-drain takes effect immediately: no further retries, and the
412
+ * per-attempt timeout is clamped to the remaining window. After
413
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
414
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
415
+ * run as a single short attempt rather than regaining the full retry
416
+ * schedule.
417
+ */
418
+ requestOpts() {
419
+ if (this.shutdownDeadlineAt !== void 0) {
420
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
421
+ if (remainingMs <= 0) return null;
422
+ return {
423
+ maxAttempts: 1,
424
+ debug: this.debug,
425
+ sdkName: this.sdkName,
426
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
427
+ };
428
+ }
429
+ if (this.hasShutdown) {
430
+ return {
431
+ maxAttempts: 1,
432
+ debug: this.debug,
433
+ sdkName: this.sdkName,
434
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
435
+ };
436
+ }
437
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
438
+ }
362
439
  async patch(eventId, patch) {
363
440
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
364
441
  if (!this.enabled) return;
365
442
  if (!eventId || !eventId.trim()) return;
443
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
444
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
445
+ patch = { ...patch, input: capText(patch.input, maxChars) };
446
+ }
447
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
448
+ patch = { ...patch, output: capText(patch.output, maxChars) };
449
+ }
366
450
  if (this.debug) {
367
451
  console.log(`${this.prefix} queue patch`, {
368
452
  eventId,
@@ -409,9 +493,16 @@ var EventShipper = class {
409
493
  })));
410
494
  }
411
495
  async shutdown() {
412
- for (const t of this.timers.values()) clearTimeout(t);
413
- this.timers.clear();
414
- await this.flush();
496
+ this.hasShutdown = true;
497
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
498
+ try {
499
+ for (const t of this.timers.values()) clearTimeout(t);
500
+ this.timers.clear();
501
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
502
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
503
+ } finally {
504
+ this.shutdownDeadlineAt = void 0;
505
+ }
415
506
  }
416
507
  async trackSignal(signal) {
417
508
  var _a, _b;
@@ -431,16 +522,21 @@ var EventShipper = class {
431
522
  }
432
523
  }
433
524
  ];
525
+ if (!this.writeKey) return;
434
526
  const url = `${this.baseUrl}signals/track`;
527
+ const opts = this.requestOpts();
528
+ if (!opts) {
529
+ this.warnShutdownDrop("signal");
530
+ return;
531
+ }
435
532
  try {
436
- await postJson(url, body, this.authHeaders(), {
437
- maxAttempts: 3,
438
- debug: this.debug,
439
- sdkName: this.sdkName
440
- });
533
+ await postJson(url, body, this.authHeaders(), opts);
441
534
  } catch (err) {
442
535
  const msg = err instanceof Error ? err.message : String(err);
443
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
536
+ rateLimitedLog(
537
+ `${this.prefix}.send_signal_failed`,
538
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
539
+ );
444
540
  }
445
541
  }
446
542
  async identify(users) {
@@ -461,19 +557,30 @@ var EventShipper = class {
461
557
  traits: (_a = user.traits) != null ? _a : {}
462
558
  };
463
559
  });
560
+ if (!this.writeKey) return;
464
561
  if (body.length === 0) return;
465
562
  const url = `${this.baseUrl}users/identify`;
563
+ const opts = this.requestOpts();
564
+ if (!opts) {
565
+ this.warnShutdownDrop("identify");
566
+ return;
567
+ }
466
568
  try {
467
- await postJson(url, body, this.authHeaders(), {
468
- maxAttempts: 3,
469
- debug: this.debug,
470
- sdkName: this.sdkName
471
- });
569
+ await postJson(url, body, this.authHeaders(), opts);
472
570
  } catch (err) {
473
571
  const msg = err instanceof Error ? err.message : String(err);
474
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
572
+ rateLimitedLog(
573
+ `${this.prefix}.send_identify_failed`,
574
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
575
+ );
475
576
  }
476
577
  }
578
+ warnShutdownDrop(what) {
579
+ rateLimitedLog(
580
+ `${this.prefix}.shutdown_deadline`,
581
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
582
+ );
583
+ }
477
584
  async flushOne(eventId) {
478
585
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
479
586
  if (!this.enabled) return;
@@ -537,11 +644,25 @@ var EventShipper = class {
537
644
  endpoint: url
538
645
  });
539
646
  }
540
- const p = postJson(url, payload, this.authHeaders(), {
541
- maxAttempts: 3,
542
- debug: this.debug,
543
- sdkName: this.sdkName
544
- });
647
+ if (this.localDebuggerUrl) {
648
+ mirrorPartialEventToLocalDebugger(payload, {
649
+ baseUrl: this.localDebuggerUrl,
650
+ writeKey: this.writeKey,
651
+ debug: this.debug,
652
+ sdkName: this.sdkName
653
+ });
654
+ }
655
+ if (!this.writeKey) {
656
+ if (!isPending) this.sticky.delete(eventId);
657
+ return;
658
+ }
659
+ const opts = this.requestOpts();
660
+ if (!opts) {
661
+ this.warnShutdownDrop(`track_partial ${eventId}`);
662
+ if (!isPending) this.sticky.delete(eventId);
663
+ return;
664
+ }
665
+ const p = postJson(url, payload, this.authHeaders(), opts);
545
666
  this.inFlight.add(p);
546
667
  try {
547
668
  try {
@@ -551,7 +672,10 @@ var EventShipper = class {
551
672
  }
552
673
  } catch (err) {
553
674
  const msg = err instanceof Error ? err.message : String(err);
554
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
675
+ rateLimitedLog(
676
+ `${this.prefix}.send_track_partial_failed`,
677
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
678
+ );
555
679
  }
556
680
  } finally {
557
681
  this.inFlight.delete(p);
@@ -561,28 +685,114 @@ var EventShipper = class {
561
685
  }
562
686
  }
563
687
  };
564
- var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
565
- function resolveLocalDebuggerBaseUrl(baseUrl) {
566
- var _a, _b, _c;
567
- const resolved = (_b = baseUrl != null ? baseUrl : typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[LOCAL_DEBUGGER_ENV_VAR] : void 0) != null ? _b : null;
568
- return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
688
+ var DEFAULT_SECRET_KEY_NAMES = [
689
+ "apikey",
690
+ "apisecret",
691
+ "apitoken",
692
+ "secretaccesskey",
693
+ "sessiontoken",
694
+ "privatekey",
695
+ "privatekeyid",
696
+ "clientsecret",
697
+ "accesstoken",
698
+ "refreshtoken",
699
+ "oauthtoken",
700
+ "bearertoken",
701
+ "authorization",
702
+ "password",
703
+ "passphrase"
704
+ ];
705
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
706
+ function normalizeKeyName(name) {
707
+ return name.toLowerCase().replace(/[-_.]/g, "");
569
708
  }
570
- function mirrorTraceExportToLocalDebugger(body, options = {}) {
571
- var _a;
572
- const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
573
- if (!baseUrl) return;
574
- void postJson(`${baseUrl}traces`, body, {}, {
575
- maxAttempts: 1,
576
- debug: (_a = options.debug) != null ? _a : false,
577
- sdkName: options.sdkName
578
- }).catch(() => {
579
- });
709
+ function redactSecretsInObject(value, options) {
710
+ var _a, _b;
711
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
712
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
713
+ const seen = /* @__PURE__ */ new WeakSet();
714
+ const walk = (node) => {
715
+ if (node === null || typeof node !== "object") return node;
716
+ if (seen.has(node)) return "[CIRCULAR]";
717
+ seen.add(node);
718
+ if (Array.isArray(node)) {
719
+ return node.map((item) => walk(item));
720
+ }
721
+ const out = {};
722
+ for (const [k, v] of Object.entries(node)) {
723
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
724
+ out[k] = placeholder;
725
+ } else {
726
+ out[k] = walk(v);
727
+ }
728
+ }
729
+ return out;
730
+ };
731
+ return walk(value);
732
+ }
733
+ function buildSecretSet(names) {
734
+ const set = /* @__PURE__ */ new Set();
735
+ for (const name of names) set.add(normalizeKeyName(name));
736
+ return set;
737
+ }
738
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
739
+ "ai.request.providerOptions",
740
+ "ai.response.providerMetadata"
741
+ ];
742
+ function defaultTransformSpan(span) {
743
+ const attrs = span.attributes;
744
+ if (!attrs || attrs.length === 0) return span;
745
+ let nextAttrs;
746
+ for (let i = 0; i < attrs.length; i++) {
747
+ const attr = attrs[i];
748
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
749
+ if (redacted === void 0) continue;
750
+ if (!nextAttrs) nextAttrs = attrs.slice();
751
+ nextAttrs[i] = { key: attr.key, value: redacted };
752
+ }
753
+ if (!nextAttrs) return span;
754
+ return { ...span, attributes: nextAttrs };
755
+ }
756
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
757
+ function redactJsonAttributeValue(key, value) {
758
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
759
+ const json = value.stringValue;
760
+ if (typeof json !== "string" || json.length === 0) return void 0;
761
+ let parsed;
762
+ try {
763
+ parsed = JSON.parse(json);
764
+ } catch (e) {
765
+ return void 0;
766
+ }
767
+ const scrubbed = redactSecretsInObject(parsed);
768
+ let scrubbedJson;
769
+ try {
770
+ scrubbedJson = JSON.stringify(scrubbed);
771
+ } catch (e) {
772
+ return void 0;
773
+ }
774
+ if (scrubbedJson === json) return void 0;
775
+ return { stringValue: scrubbedJson };
776
+ }
777
+ function applyOtelSpanAttributeLimit(limit) {
778
+ var _a, _b;
779
+ try {
780
+ const raw = (_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
781
+ if (!raw) return limit;
782
+ const parsed = Number.parseInt(raw, 10);
783
+ if (Number.isFinite(parsed) && parsed > 0) {
784
+ return Math.min(limit, parsed);
785
+ }
786
+ } catch (e) {
787
+ }
788
+ return limit;
580
789
  }
581
790
  var TraceShipper = class {
582
791
  constructor(opts) {
583
792
  this.queue = [];
584
793
  this.inFlight = /* @__PURE__ */ new Set();
585
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
794
+ this.hasShutdown = false;
795
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
586
796
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
587
797
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
588
798
  this.enabled = opts.enabled !== false;
@@ -595,13 +805,79 @@ var TraceShipper = class {
595
805
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
596
806
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
597
807
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
598
- const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
599
- if (localDebugger) {
600
- this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
601
- if (this.debug) {
602
- console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
808
+ this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
809
+ if (this.debug && this.localDebuggerUrl) {
810
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
811
+ }
812
+ this.transformSpanHook = opts.transformSpan;
813
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
814
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
815
+ }
816
+ /**
817
+ * Cap every string attribute value on the span. O(#attributes) length
818
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
819
+ * pipeline so the default secret-scrub still sees parseable JSON in
820
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
821
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
822
+ * in the surviving prefix).
823
+ *
824
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
825
+ * for span content, matching the Python SDK and the OTel SDK convention.
826
+ */
827
+ capSpanAttributes(span) {
828
+ var _a;
829
+ const maxChars = applyOtelSpanAttributeLimit(
830
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
831
+ );
832
+ const attrs = span.attributes;
833
+ if (!attrs || attrs.length === 0) return span;
834
+ let nextAttrs;
835
+ for (let i = 0; i < attrs.length; i++) {
836
+ const attr = attrs[i];
837
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
838
+ if (typeof value !== "string" || value.length <= maxChars) continue;
839
+ if (!nextAttrs) nextAttrs = attrs.slice();
840
+ nextAttrs[i] = {
841
+ key: attr.key,
842
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
843
+ };
844
+ }
845
+ if (!nextAttrs) return span;
846
+ return { ...span, attributes: nextAttrs };
847
+ }
848
+ /**
849
+ * Apply the user `transformSpan` hook (if any) followed by the default
850
+ * redactor (unless disabled). Returns either the (possibly new) span to
851
+ * ship, or `null` to drop the span entirely.
852
+ *
853
+ * Ordering: user hook runs first so callers can rewrite the span freely
854
+ * (rename attrs, add new ones, scrub things the default doesn't know
855
+ * about). The default redactor then runs on whatever the user produced,
856
+ * acting as the always-on floor for documented BYOK secrets. If the user
857
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
858
+ *
859
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
860
+ * hook can never accidentally ship raw, un-redacted spans.
861
+ */
862
+ redactSpan(span) {
863
+ let current = span;
864
+ if (this.transformSpanHook) {
865
+ try {
866
+ const result = this.transformSpanHook(current);
867
+ if (result === null) return null;
868
+ if (result !== void 0) current = result;
869
+ } catch (err) {
870
+ if (this.debug) {
871
+ const msg = err instanceof Error ? err.message : String(err);
872
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
873
+ }
874
+ return null;
603
875
  }
604
876
  }
877
+ if (!this.disableDefaultRedaction) {
878
+ current = defaultTransformSpan(current);
879
+ }
880
+ return this.capSpanAttributes(current);
605
881
  }
606
882
  isDebugEnabled() {
607
883
  return this.debug;
@@ -619,8 +895,8 @@ var TraceShipper = class {
619
895
  ];
620
896
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
621
897
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
622
- if (this.localDebuggerUrl) {
623
- const openSpan = buildOtlpSpan({
898
+ this.mirrorToLocalDebugger(
899
+ buildOtlpSpan({
624
900
  ids: span.ids,
625
901
  name: span.name,
626
902
  startTimeUnixNano: span.startTimeUnixNano,
@@ -628,16 +904,21 @@ var TraceShipper = class {
628
904
  // placeholder — will be updated on endSpan
629
905
  attributes: span.attributes,
630
906
  status: { code: SpanStatusCode.UNSET }
631
- });
632
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
633
- mirrorTraceExportToLocalDebugger(body, {
634
- baseUrl: this.localDebuggerUrl,
635
- debug: false,
636
- sdkName: this.sdkName
637
- });
638
- }
907
+ })
908
+ );
639
909
  return span;
640
910
  }
911
+ mirrorToLocalDebugger(span) {
912
+ if (!this.localDebuggerUrl) return;
913
+ const redacted = this.redactSpan(span);
914
+ if (redacted === null) return;
915
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
916
+ mirrorTraceExportToLocalDebugger(body, {
917
+ baseUrl: this.localDebuggerUrl,
918
+ debug: false,
919
+ sdkName: this.sdkName
920
+ });
921
+ }
641
922
  endSpan(span, extra) {
642
923
  var _a, _b;
643
924
  if (span.endTimeUnixNano) return;
@@ -659,14 +940,7 @@ var TraceShipper = class {
659
940
  status
660
941
  });
661
942
  this.enqueue(otlp);
662
- if (this.localDebuggerUrl) {
663
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
664
- mirrorTraceExportToLocalDebugger(body, {
665
- baseUrl: this.localDebuggerUrl,
666
- debug: false,
667
- sdkName: this.sdkName
668
- });
669
- }
943
+ this.mirrorToLocalDebugger(otlp);
670
944
  }
671
945
  createSpan(args) {
672
946
  var _a;
@@ -684,14 +958,7 @@ var TraceShipper = class {
684
958
  status: args.status
685
959
  });
686
960
  this.enqueue(otlp);
687
- if (this.localDebuggerUrl) {
688
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
689
- mirrorTraceExportToLocalDebugger(body, {
690
- baseUrl: this.localDebuggerUrl,
691
- debug: false,
692
- sdkName: this.sdkName
693
- });
694
- }
961
+ this.mirrorToLocalDebugger(otlp);
695
962
  }
696
963
  enqueue(span) {
697
964
  if (!this.enabled) return;
@@ -703,10 +970,12 @@ var TraceShipper = class {
703
970
  )}`
704
971
  );
705
972
  }
973
+ const redacted = this.redactSpan(span);
974
+ if (redacted === null) return;
706
975
  if (this.queue.length >= this.maxQueueSize) {
707
976
  this.queue.shift();
708
977
  }
709
- this.queue.push(span);
978
+ this.queue.push(redacted);
710
979
  if (this.queue.length >= this.maxBatchSize) {
711
980
  void this.flush().catch(() => {
712
981
  });
@@ -728,6 +997,17 @@ var TraceShipper = class {
728
997
  }
729
998
  while (this.queue.length > 0) {
730
999
  const batch = this.queue.splice(0, this.maxBatchSize);
1000
+ if (!this.writeKey) continue;
1001
+ const opts = this.requestOpts();
1002
+ if (!opts) {
1003
+ rateLimitedLog(
1004
+ `${this.prefix}.shutdown_deadline`,
1005
+ () => console.warn(
1006
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1007
+ )
1008
+ );
1009
+ continue;
1010
+ }
731
1011
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
732
1012
  const url = `${this.baseUrl}traces`;
733
1013
  if (this.debug) {
@@ -736,11 +1016,7 @@ var TraceShipper = class {
736
1016
  endpoint: url
737
1017
  });
738
1018
  }
739
- const p = postJson(url, body, this.authHeaders(), {
740
- maxAttempts: 3,
741
- debug: this.debug,
742
- sdkName: this.sdkName
743
- });
1019
+ const p = postJson(url, body, this.authHeaders(), opts);
744
1020
  this.inFlight.add(p);
745
1021
  try {
746
1022
  try {
@@ -748,21 +1024,61 @@ var TraceShipper = class {
748
1024
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
749
1025
  } catch (err) {
750
1026
  const msg = err instanceof Error ? err.message : String(err);
751
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1027
+ rateLimitedLog(
1028
+ `${this.prefix}.send_spans_failed`,
1029
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1030
+ );
752
1031
  }
753
1032
  } finally {
754
1033
  this.inFlight.delete(p);
755
1034
  }
756
1035
  }
757
1036
  }
1037
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1038
+ requestOpts() {
1039
+ if (this.shutdownDeadlineAt !== void 0) {
1040
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1041
+ if (remainingMs <= 0) return null;
1042
+ return {
1043
+ maxAttempts: 1,
1044
+ debug: this.debug,
1045
+ sdkName: this.sdkName,
1046
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1047
+ };
1048
+ }
1049
+ if (this.hasShutdown) {
1050
+ return {
1051
+ maxAttempts: 1,
1052
+ debug: this.debug,
1053
+ sdkName: this.sdkName,
1054
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1055
+ };
1056
+ }
1057
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1058
+ }
758
1059
  async shutdown() {
759
- if (this.timer) {
760
- clearTimeout(this.timer);
761
- this.timer = void 0;
1060
+ this.hasShutdown = true;
1061
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1062
+ try {
1063
+ if (this.timer) {
1064
+ clearTimeout(this.timer);
1065
+ this.timer = void 0;
1066
+ }
1067
+ const drain = async () => {
1068
+ await this.flush();
1069
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1070
+ })));
1071
+ };
1072
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1073
+ if (!settled) {
1074
+ rateLimitedLog(
1075
+ `${this.prefix}.shutdown_deadline`,
1076
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1077
+ );
1078
+ }
1079
+ } finally {
1080
+ this.shutdownDeadlineAt = void 0;
762
1081
  }
763
- await this.flush();
764
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
765
- })));
766
1082
  }
767
1083
  };
768
1084
 
@@ -770,6 +1086,134 @@ var TraceShipper = class {
770
1086
  import { AsyncLocalStorage } from "async_hooks";
771
1087
  globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
772
1088
 
1089
+ // src/config.ts
1090
+ import { existsSync, readFileSync } from "fs";
1091
+ import { homedir } from "os";
1092
+ import { join } from "path";
1093
+ function loadConfig(projectDirectory) {
1094
+ var _a, _b, _c, _d, _e, _f, _g;
1095
+ let merged = {};
1096
+ const configPaths = [
1097
+ join(homedir(), ".config", "opencode", "raindrop.json"),
1098
+ join(projectDirectory, ".opencode", "raindrop.json")
1099
+ ];
1100
+ for (const configPath of configPaths) {
1101
+ try {
1102
+ if (existsSync(configPath)) {
1103
+ const content = readFileSync(configPath, "utf-8");
1104
+ const parsed = JSON.parse(content);
1105
+ merged = { ...merged, ...parsed };
1106
+ }
1107
+ } catch (e) {
1108
+ }
1109
+ }
1110
+ let eventMetadata;
1111
+ const envMeta = process.env["RAINDROP_EVENT_METADATA"];
1112
+ if (envMeta) {
1113
+ try {
1114
+ eventMetadata = JSON.parse(envMeta);
1115
+ } catch (e) {
1116
+ }
1117
+ }
1118
+ return {
1119
+ writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
1120
+ endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
1121
+ eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
1122
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
1123
+ captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
1124
+ eventMetadata,
1125
+ localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url)
1126
+ };
1127
+ }
1128
+ function resolveLocalWorkshopUrl(fileValue) {
1129
+ const envValue = process.env["RAINDROP_LOCAL_WORKSHOP_URL"];
1130
+ if (envValue !== void 0) {
1131
+ if (envValue === "" || envValue.toLowerCase() === "null" || envValue.toLowerCase() === "false") {
1132
+ return null;
1133
+ }
1134
+ return envValue;
1135
+ }
1136
+ return fileValue;
1137
+ }
1138
+
1139
+ // package.json
1140
+ var package_default = {
1141
+ name: "@raindrop-ai/opencode-plugin",
1142
+ version: "0.0.13",
1143
+ description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1144
+ type: "module",
1145
+ main: "dist/index.js",
1146
+ module: "dist/index.js",
1147
+ types: "dist/index.d.ts",
1148
+ license: "MIT",
1149
+ repository: {
1150
+ type: "git",
1151
+ url: "git+https://github.com/raindrop-ai/raindrop-js.git",
1152
+ directory: "packages/opencode-plugin"
1153
+ },
1154
+ homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
1155
+ bugs: {
1156
+ url: "https://github.com/raindrop-ai/raindrop-js/issues"
1157
+ },
1158
+ exports: {
1159
+ ".": {
1160
+ types: "./dist/index.d.ts",
1161
+ import: "./dist/index.js",
1162
+ require: "./dist/index.cjs"
1163
+ }
1164
+ },
1165
+ sideEffects: false,
1166
+ files: [
1167
+ "dist/**"
1168
+ ],
1169
+ scripts: {
1170
+ build: "tsup",
1171
+ dev: "tsup --watch",
1172
+ clean: "rm -rf dist",
1173
+ test: "cd tests && pnpm test"
1174
+ },
1175
+ peerDependencies: {
1176
+ "@opencode-ai/plugin": ">=1.3.0",
1177
+ "@opencode-ai/sdk": ">=1.3.0"
1178
+ },
1179
+ peerDependenciesMeta: {
1180
+ "@opencode-ai/sdk": {
1181
+ optional: true
1182
+ }
1183
+ },
1184
+ devDependencies: {
1185
+ "@raindrop-ai/core": "workspace:*",
1186
+ "@opencode-ai/plugin": "^1.3.3",
1187
+ "@opencode-ai/sdk": "^1.3.3",
1188
+ "@types/node": "^20.11.17",
1189
+ tsup: "^8.4.0",
1190
+ typescript: "^5.3.3"
1191
+ },
1192
+ tsup: {
1193
+ entry: [
1194
+ "src/index.ts"
1195
+ ],
1196
+ format: [
1197
+ "cjs",
1198
+ "esm"
1199
+ ],
1200
+ dts: {
1201
+ resolve: true
1202
+ },
1203
+ clean: true,
1204
+ noExternal: [
1205
+ "@raindrop-ai/core"
1206
+ ]
1207
+ },
1208
+ publishConfig: {
1209
+ access: "public"
1210
+ }
1211
+ };
1212
+
1213
+ // src/package-info.ts
1214
+ var PLUGIN_NAME = package_default.name;
1215
+ var PLUGIN_VERSION = package_default.version;
1216
+
773
1217
  // src/shipper.ts
774
1218
  var EventShipper2 = class extends EventShipper {
775
1219
  constructor(opts) {
@@ -805,6 +1249,86 @@ var TraceShipper2 = class extends TraceShipper {
805
1249
  }
806
1250
  };
807
1251
 
1252
+ // src/bounded.ts
1253
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1254
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1255
+ var MAX_BOUNDED_DEPTH = 12;
1256
+ function truncateToLimit2(text, limit) {
1257
+ if (text.length <= limit) return text;
1258
+ if (limit > TRUNCATION_MARKER2.length) {
1259
+ return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1260
+ }
1261
+ return text.slice(0, limit);
1262
+ }
1263
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1264
+ if (value.length <= limit) return value;
1265
+ return truncateToLimit2(value, limit);
1266
+ }
1267
+ function boundedClone2(value, budget, depth) {
1268
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1269
+ if (typeof value === "string") {
1270
+ if (value.length > budget.remaining) {
1271
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1272
+ budget.remaining = 0;
1273
+ return taken;
1274
+ }
1275
+ budget.remaining -= Math.max(value.length, 1);
1276
+ return value;
1277
+ }
1278
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1279
+ budget.remaining -= 8;
1280
+ return value;
1281
+ }
1282
+ if (typeof value !== "object") {
1283
+ budget.remaining -= 8;
1284
+ return value;
1285
+ }
1286
+ if (depth >= MAX_BOUNDED_DEPTH) {
1287
+ budget.remaining -= 16;
1288
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1289
+ }
1290
+ const withToJson = value;
1291
+ if (typeof withToJson.toJSON === "function") {
1292
+ try {
1293
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1294
+ } catch (e) {
1295
+ }
1296
+ }
1297
+ if (Array.isArray(value)) {
1298
+ const out2 = [];
1299
+ for (const item of value) {
1300
+ if (budget.remaining <= 0) {
1301
+ out2.push(TRUNCATION_MARKER2);
1302
+ break;
1303
+ }
1304
+ out2.push(boundedClone2(item, budget, depth + 1));
1305
+ }
1306
+ return out2;
1307
+ }
1308
+ const out = {};
1309
+ for (const key of Object.keys(value)) {
1310
+ if (budget.remaining <= 0) {
1311
+ out["..."] = TRUNCATION_MARKER2;
1312
+ break;
1313
+ }
1314
+ budget.remaining -= Math.max(key.length, 1);
1315
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1316
+ }
1317
+ return out;
1318
+ }
1319
+ function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
1320
+ var _a;
1321
+ try {
1322
+ if (typeof value === "string") {
1323
+ return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
1324
+ }
1325
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1326
+ return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
1327
+ } catch (e) {
1328
+ return capText2(String(value), limit);
1329
+ }
1330
+ }
1331
+
808
1332
  // src/tracing-linkage-helpers.ts
809
1333
  function createSessionParentMapHelpers({
810
1334
  sessions: sessions2,
@@ -882,6 +1406,15 @@ function createSessionParentMapHelpers({
882
1406
  }
883
1407
 
884
1408
  // src/tracing.ts
1409
+ var ERROR_LOG_INTERVAL_MS = 3e4;
1410
+ var lastErrorLogAt = /* @__PURE__ */ new Map();
1411
+ function rateLimitedErrorLog(key, message) {
1412
+ const now = Date.now();
1413
+ const last = lastErrorLogAt.get(key);
1414
+ if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
1415
+ lastErrorLogAt.set(key, now);
1416
+ console.log(message);
1417
+ }
885
1418
  var sessions = /* @__PURE__ */ new Map();
886
1419
  var taskContexts = /* @__PURE__ */ new Map();
887
1420
  var runningTaskCallsBySession = /* @__PURE__ */ new Map();
@@ -940,6 +1473,19 @@ function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMes
940
1473
  }
941
1474
  return true;
942
1475
  }
1476
+ function unwrapQuotes(s) {
1477
+ if (s.length < 2) return s;
1478
+ const first = s[0];
1479
+ const last = s[s.length - 1];
1480
+ if ((first === '"' || first === "'") && first === last) return s.slice(1, -1);
1481
+ return s;
1482
+ }
1483
+ function buildPromptMessages(systemPrompt, userInput) {
1484
+ const messages = [];
1485
+ if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
1486
+ messages.push({ role: "user", content: unwrapQuotes(userInput) });
1487
+ return JSON.stringify(messages);
1488
+ }
943
1489
  function getHostname() {
944
1490
  var _a;
945
1491
  try {
@@ -1021,7 +1567,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1021
1567
  const state = sessions.get(partSessionID);
1022
1568
  if (!state) return;
1023
1569
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
1024
- state.outputParts.set(messageId, partObj["text"]);
1570
+ state.outputParts.set(messageId, capText2(partObj["text"]));
1025
1571
  } else if (partObj["type"] === "tool" && messageId) {
1026
1572
  const tool = partObj["tool"];
1027
1573
  const callID = partObj["callID"];
@@ -1051,7 +1597,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1051
1597
  if (typeof errorMessage !== "string" || errorMessage === "")
1052
1598
  throw new Error("message.part.updated: error tool state must have error string");
1053
1599
  const input = toolState["input"];
1054
- const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? input : JSON.stringify(input);
1600
+ const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
1055
1601
  if (endPendingToolSpanWithError(
1056
1602
  state,
1057
1603
  callID,
@@ -1064,7 +1610,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1064
1610
  }
1065
1611
  }
1066
1612
  } else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
1067
- state.reasoningParts.set(messageId, partObj["text"]);
1613
+ state.reasoningParts.set(messageId, capText2(partObj["text"]));
1068
1614
  }
1069
1615
  } else if (event.type === "message.updated") {
1070
1616
  const msgInfo = props["info"];
@@ -1165,9 +1711,14 @@ type: ${name != null ? name : "UnknownError"}`;
1165
1711
  const rootSpan = state.currentRootSpan;
1166
1712
  const isCompaction = state.isCompacting === true;
1167
1713
  if (state.parentId) {
1714
+ const userInput = unwrapQuotes(state.currentInput);
1168
1715
  const llmAttrs = [
1169
1716
  attrString("ai.operationId", "generateText"),
1170
- attrString("ai.prompt", state.currentInput),
1717
+ attrString("ai.prompt", userInput),
1718
+ attrString(
1719
+ "ai.prompt.messages",
1720
+ buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1721
+ ),
1171
1722
  attrString("ai.response.text", outputText),
1172
1723
  attrString("gen_ai.system", providerID),
1173
1724
  attrString("gen_ai.request.model", modelID),
@@ -1212,9 +1763,14 @@ type: ${name != null ? name : "UnknownError"}`;
1212
1763
  });
1213
1764
  } else {
1214
1765
  rootSpan.name = isCompaction ? "ai.compaction" : modelName;
1766
+ const userInput = unwrapQuotes(state.currentInput);
1215
1767
  const rootAttrs = [
1216
1768
  attrString("ai.operationId", "generateText"),
1217
- attrString("ai.prompt", state.currentInput),
1769
+ attrString("ai.prompt", userInput),
1770
+ attrString(
1771
+ "ai.prompt.messages",
1772
+ buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1773
+ ),
1218
1774
  attrString("ai.response.text", outputText),
1219
1775
  attrString("gen_ai.system", providerID),
1220
1776
  attrString("gen_ai.request.model", modelID),
@@ -1321,7 +1877,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1321
1877
  sessions.delete(String(errorSessionID));
1322
1878
  }
1323
1879
  } catch (err) {
1324
- console.log(
1880
+ rateLimitedErrorLog(
1881
+ "event",
1325
1882
  `[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
1326
1883
  );
1327
1884
  }
@@ -1377,7 +1934,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1377
1934
  ...promptMeta,
1378
1935
  properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
1379
1936
  };
1380
- const userText = textParts.join("\n");
1937
+ const userText = capText2(textParts.join("\n"));
1381
1938
  state.currentInput = userText;
1382
1939
  if (state.parentId) {
1383
1940
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
@@ -1486,7 +2043,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1486
2043
  });
1487
2044
  }
1488
2045
  } catch (err) {
1489
- console.log(
2046
+ rateLimitedErrorLog(
2047
+ "tool.execute.before",
1490
2048
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
1491
2049
  );
1492
2050
  }
@@ -1501,7 +2059,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1501
2059
  if (!state || !state.currentEventId) return;
1502
2060
  if (args === void 0 || args === null)
1503
2061
  throw new Error("tool.execute.after: args is required");
1504
- const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
2062
+ const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
1505
2063
  const startInfo = state.toolSpanStarts.get(callID);
1506
2064
  state.toolSpanStarts.delete(callID);
1507
2065
  const resultMetadata = result.metadata;
@@ -1514,13 +2072,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1514
2072
  const endTimeUnixNano = nowUnixNanoString();
1515
2073
  if (!("output" in result) || result.output === void 0)
1516
2074
  throw new Error("tool.execute.after: result.output is required");
1517
- let toolResult;
1518
- try {
1519
- toolResult = JSON.stringify(result.output);
1520
- } catch (err) {
1521
- const msg = err instanceof Error ? err.message : String(err);
1522
- throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
1523
- }
2075
+ const toolResult = boundedStringify(result.output);
1524
2076
  if (startInfo.liveSpan) {
1525
2077
  traceShipper.endSpan(startInfo.liveSpan, {
1526
2078
  attributes: [
@@ -1549,7 +2101,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1549
2101
  markTaskCallFinished(sessionID, callID);
1550
2102
  }
1551
2103
  } catch (err) {
1552
- console.log(
2104
+ rateLimitedErrorLog(
2105
+ "tool.execute.after",
1553
2106
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1554
2107
  );
1555
2108
  }
@@ -1566,7 +2119,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1566
2119
  state.isCompacting = true;
1567
2120
  }
1568
2121
  } catch (err) {
1569
- console.log(
2122
+ rateLimitedErrorLog(
2123
+ "experimental.session.compacting",
1570
2124
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1571
2125
  );
1572
2126
  }
@@ -1585,11 +2139,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1585
2139
  sessions.set(sessionID, state);
1586
2140
  }
1587
2141
  const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1588
- const TRUNCATION_MARKER = "\n...[truncated]";
2142
+ const TRUNCATION_MARKER3 = "\n...[truncated]";
1589
2143
  const joined = output.system.join("\n\n");
1590
- state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
2144
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
1591
2145
  } catch (err) {
1592
- console.log(
2146
+ rateLimitedErrorLog(
2147
+ "experimental.chat.system.transform",
1593
2148
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1594
2149
  );
1595
2150
  }
@@ -1605,25 +2160,33 @@ async function plugin(input) {
1605
2160
  console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
1606
2161
  }
1607
2162
  appLog("info", `Loading ${PLUGIN_NAME} v${PLUGIN_VERSION}`);
1608
- if (!config.writeKey) {
2163
+ const resolvedLocalUrl = resolveLocalDebuggerBaseUrl(config.localWorkshopUrl);
2164
+ const hasLocalDestination = resolvedLocalUrl !== null;
2165
+ if (!config.writeKey && !hasLocalDestination) {
1609
2166
  appLog(
1610
2167
  "warn",
1611
- "RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
2168
+ "RAINDROP_WRITE_KEY not set and no local Workshop daemon detected \u2014 Raindrop tracing disabled. Set RAINDROP_WRITE_KEY for cloud, or RAINDROP_LOCAL_WORKSHOP_URL / RAINDROP_LOCAL_DEBUGGER for local-only mode."
1612
2169
  );
1613
2170
  return {};
1614
2171
  }
1615
2172
  if (config.debug) {
1616
- appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
2173
+ const destinations = [
2174
+ config.writeKey ? `cloud (${config.endpoint})` : null,
2175
+ resolvedLocalUrl ? `local Workshop (${resolvedLocalUrl})` : null
2176
+ ].filter(Boolean);
2177
+ appLog("info", `Raindrop tracing enabled \u2014 destinations: ${destinations.join(", ")}`);
1617
2178
  }
1618
2179
  const eventShipper = new EventShipper2({
1619
2180
  writeKey: config.writeKey,
1620
2181
  endpoint: config.endpoint,
1621
- debug: config.debug
2182
+ debug: config.debug,
2183
+ localDebuggerUrl: config.localWorkshopUrl
1622
2184
  });
1623
2185
  const traceShipper = new TraceShipper2({
1624
2186
  writeKey: config.writeKey,
1625
2187
  endpoint: config.endpoint,
1626
- debug: config.debug
2188
+ debug: config.debug,
2189
+ localDebuggerUrl: config.localWorkshopUrl
1627
2190
  });
1628
2191
  const worktree = (_a = input.worktree) != null ? _a : input.directory;
1629
2192
  const hooks = createHooks(