@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.cjs CHANGED
@@ -24,124 +24,7 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
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, _g;
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
- let eventMetadata;
49
- const envMeta = process.env["RAINDROP_EVENT_METADATA"];
50
- if (envMeta) {
51
- try {
52
- eventMetadata = JSON.parse(envMeta);
53
- } catch (e) {
54
- }
55
- }
56
- return {
57
- writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
58
- endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
59
- eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
60
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
61
- captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
62
- eventMetadata
63
- };
64
- }
65
-
66
- // package.json
67
- var package_default = {
68
- name: "@raindrop-ai/opencode-plugin",
69
- version: "0.0.11",
70
- description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
71
- type: "module",
72
- main: "dist/index.js",
73
- module: "dist/index.js",
74
- types: "dist/index.d.ts",
75
- license: "MIT",
76
- repository: {
77
- type: "git",
78
- url: "git+https://github.com/raindrop-ai/raindrop-js.git",
79
- directory: "packages/opencode-plugin"
80
- },
81
- homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
82
- bugs: {
83
- url: "https://github.com/raindrop-ai/raindrop-js/issues"
84
- },
85
- exports: {
86
- ".": {
87
- types: "./dist/index.d.ts",
88
- import: "./dist/index.js",
89
- require: "./dist/index.cjs"
90
- }
91
- },
92
- sideEffects: false,
93
- files: [
94
- "dist/**"
95
- ],
96
- scripts: {
97
- build: "tsup",
98
- dev: "tsup --watch",
99
- clean: "rm -rf dist",
100
- test: "cd tests && pnpm test"
101
- },
102
- peerDependencies: {
103
- "@opencode-ai/plugin": ">=1.3.0",
104
- "@opencode-ai/sdk": ">=1.3.0"
105
- },
106
- peerDependenciesMeta: {
107
- "@opencode-ai/sdk": {
108
- optional: true
109
- }
110
- },
111
- devDependencies: {
112
- "@raindrop-ai/core": "workspace:*",
113
- "@opencode-ai/plugin": "^1.3.3",
114
- "@opencode-ai/sdk": "^1.3.3",
115
- "@types/node": "^20.11.17",
116
- tsup: "^8.4.0",
117
- typescript: "^5.3.3"
118
- },
119
- tsup: {
120
- entry: [
121
- "src/index.ts"
122
- ],
123
- format: [
124
- "cjs",
125
- "esm"
126
- ],
127
- dts: {
128
- resolve: true
129
- },
130
- clean: true,
131
- noExternal: [
132
- "@raindrop-ai/core"
133
- ]
134
- },
135
- publishConfig: {
136
- access: "public"
137
- }
138
- };
139
-
140
- // src/package-info.ts
141
- var PLUGIN_NAME = package_default.name;
142
- var PLUGIN_VERSION = package_default.version;
143
-
144
- // ../core/dist/chunk-4UCYIEH4.js
27
+ // ../core/dist/chunk-U5HUTMR5.js
145
28
  function getCrypto() {
146
29
  const c = globalThis.crypto;
147
30
  return c;
@@ -188,6 +71,8 @@ function base64Encode(bytes) {
188
71
  function generateId() {
189
72
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
190
73
  }
74
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
75
+ var MAX_RETRY_DELAY_MS = 3e4;
191
76
  function wait(ms) {
192
77
  return new Promise((resolve) => setTimeout(resolve, ms));
193
78
  }
@@ -195,6 +80,46 @@ function formatEndpoint(endpoint) {
195
80
  if (!endpoint) return void 0;
196
81
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
197
82
  }
83
+ function redactUrlForLog(url) {
84
+ try {
85
+ const parsed = new URL(url);
86
+ parsed.username = "";
87
+ parsed.password = "";
88
+ parsed.search = "";
89
+ parsed.hash = "";
90
+ return parsed.toString();
91
+ } catch (e) {
92
+ return "<unparseable-url>";
93
+ }
94
+ }
95
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
96
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
97
+ function rateLimitedLog(key, log) {
98
+ const now = Date.now();
99
+ const last = rateLimitedLogLast.get(key);
100
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
101
+ return false;
102
+ }
103
+ rateLimitedLogLast.set(key, now);
104
+ log();
105
+ return true;
106
+ }
107
+ async function raceWithTimeout(promise, timeoutMs) {
108
+ let timer;
109
+ const settledInTime = await Promise.race([
110
+ promise.then(
111
+ () => true,
112
+ () => true
113
+ ),
114
+ new Promise((resolve) => {
115
+ var _a;
116
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
117
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
118
+ })
119
+ ]);
120
+ if (timer) clearTimeout(timer);
121
+ return settledInTime;
122
+ }
198
123
  function parseRetryAfter(headers) {
199
124
  var _a;
200
125
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -211,12 +136,12 @@ function parseRetryAfter(headers) {
211
136
  function getRetryDelayMs(attemptNumber, previousError) {
212
137
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
213
138
  const v = previousError.retryAfterMs;
214
- if (typeof v === "number") return Math.max(0, v);
139
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
215
140
  }
216
141
  if (attemptNumber <= 1) return 0;
217
142
  const base = 500;
218
143
  const factor = Math.pow(2, attemptNumber - 2);
219
- return base * factor;
144
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
220
145
  }
221
146
  async function withRetry(operation, opName, opts) {
222
147
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -251,7 +176,9 @@ async function withRetry(operation, opName, opts) {
251
176
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
252
177
  }
253
178
  async function postJson(url, body, headers, opts) {
254
- const opName = `POST ${url}`;
179
+ var _a;
180
+ const opName = `POST ${redactUrlForLog(url)}`;
181
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
255
182
  await withRetry(
256
183
  async () => {
257
184
  const resp = await fetch(url, {
@@ -260,7 +187,8 @@ async function postJson(url, body, headers, opts) {
260
187
  "Content-Type": "application/json",
261
188
  ...headers
262
189
  },
263
- body: JSON.stringify(body)
190
+ body: JSON.stringify(body),
191
+ signal: AbortSignal.timeout(timeoutMs)
264
192
  });
265
193
  if (!resp.ok) {
266
194
  const text = await resp.text().catch(() => "");
@@ -277,6 +205,27 @@ async function postJson(url, body, headers, opts) {
277
205
  opts
278
206
  );
279
207
  }
208
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
209
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
210
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
211
+ function resolveMaxTextFieldChars(value) {
212
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
213
+ return Math.floor(value);
214
+ }
215
+ return currentDefaultMaxTextFieldChars;
216
+ }
217
+ function truncateToLimit(text, limit) {
218
+ if (limit > TRUNCATION_MARKER.length) {
219
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
220
+ }
221
+ return text.slice(0, Math.max(0, limit));
222
+ }
223
+ function capText(value, limit) {
224
+ if (typeof value !== "string") return value;
225
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
226
+ if (value.length <= max) return value;
227
+ return truncateToLimit(value, max);
228
+ }
280
229
  var SpanStatusCode = {
281
230
  UNSET: 0,
282
231
  OK: 1,
@@ -334,6 +283,93 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
334
283
  ]
335
284
  };
336
285
  }
286
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
287
+ var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
288
+ var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
289
+ function readEnvVar(name) {
290
+ var _a;
291
+ try {
292
+ const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
293
+ if (env && typeof env[name] === "string" && env[name].length > 0) {
294
+ return env[name];
295
+ }
296
+ } catch (e) {
297
+ }
298
+ return void 0;
299
+ }
300
+ function readWorkshopEnv() {
301
+ const raw = readEnvVar(WORKSHOP_ENV_VAR);
302
+ if (raw === void 0) return void 0;
303
+ const trimmed = raw.trim();
304
+ if (trimmed.length === 0) return void 0;
305
+ if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
306
+ if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
307
+ if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
308
+ return void 0;
309
+ }
310
+ function isLocalDevHost(hostname) {
311
+ if (!hostname) return false;
312
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
313
+ return true;
314
+ }
315
+ if (hostname.endsWith(".localhost")) return true;
316
+ return false;
317
+ }
318
+ function readRuntimeHostname() {
319
+ try {
320
+ const loc = globalThis == null ? void 0 : globalThis.location;
321
+ if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
322
+ return loc.hostname;
323
+ }
324
+ } catch (e) {
325
+ }
326
+ return void 0;
327
+ }
328
+ function shouldAutoEnableLocalWorkshop() {
329
+ if (isLocalDevHost(readRuntimeHostname())) return true;
330
+ if (readEnvVar("NODE_ENV") === "development") return true;
331
+ return false;
332
+ }
333
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
334
+ var _a, _b, _c;
335
+ if (baseUrl === null) return null;
336
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
337
+ return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
338
+ }
339
+ const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
340
+ if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
341
+ const workshopEnv = readWorkshopEnv();
342
+ if (workshopEnv === "disable") return null;
343
+ if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
344
+ if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
345
+ if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
346
+ return null;
347
+ }
348
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
349
+ var _a;
350
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
351
+ if (!baseUrl) return;
352
+ void postJson(`${baseUrl}traces`, body, {}, {
353
+ maxAttempts: 1,
354
+ debug: (_a = options.debug) != null ? _a : false,
355
+ sdkName: options.sdkName
356
+ }).catch(() => {
357
+ });
358
+ }
359
+ function mirrorPartialEventToLocalDebugger(event, options = {}) {
360
+ var _a;
361
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
362
+ if (!baseUrl) return;
363
+ const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
364
+ void postJson(`${baseUrl}events/track_partial`, event, headers, {
365
+ maxAttempts: 1,
366
+ debug: (_a = options.debug) != null ? _a : false,
367
+ sdkName: options.sdkName
368
+ }).catch(() => {
369
+ });
370
+ }
371
+ var SHUTDOWN_DEADLINE_MS = 1e4;
372
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
337
373
  function mergePatches(target, source) {
338
374
  var _a, _b, _c, _d;
339
375
  const out = { ...target, ...source };
@@ -351,7 +387,8 @@ var EventShipper = class {
351
387
  this.sticky = /* @__PURE__ */ new Map();
352
388
  this.timers = /* @__PURE__ */ new Map();
353
389
  this.inFlight = /* @__PURE__ */ new Set();
354
- var _a, _b, _c, _d, _e, _f, _g;
390
+ this.hasShutdown = false;
391
+ var _a, _b, _c, _d, _e, _f, _g, _h;
355
392
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
356
393
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
357
394
  this.enabled = opts.enabled !== false;
@@ -360,11 +397,16 @@ var EventShipper = class {
360
397
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
361
398
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
362
399
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
400
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
401
+ this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
402
+ if (this.debug && this.localDebuggerUrl) {
403
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
404
+ }
363
405
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
364
406
  this.context = {
365
407
  library: {
366
- name: (_f = opts.libraryName) != null ? _f : "@raindrop-ai/core",
367
- version: (_g = opts.libraryVersion) != null ? _g : "0.0.0"
408
+ name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
409
+ version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
368
410
  },
369
411
  metadata: {
370
412
  jsRuntime: isNode ? "node" : "web",
@@ -378,10 +420,52 @@ var EventShipper = class {
378
420
  authHeaders() {
379
421
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
380
422
  }
423
+ /**
424
+ * Build the retry/timeout options for one POST, honoring the shutdown
425
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
426
+ * the caller must drop the payload (with a rate-limited warning) instead
427
+ * of issuing a request that could outlive process exit.
428
+ *
429
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
430
+ * path is mid-drain takes effect immediately: no further retries, and the
431
+ * per-attempt timeout is clamped to the remaining window. After
432
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
433
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
434
+ * run as a single short attempt rather than regaining the full retry
435
+ * schedule.
436
+ */
437
+ requestOpts() {
438
+ if (this.shutdownDeadlineAt !== void 0) {
439
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
440
+ if (remainingMs <= 0) return null;
441
+ return {
442
+ maxAttempts: 1,
443
+ debug: this.debug,
444
+ sdkName: this.sdkName,
445
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
446
+ };
447
+ }
448
+ if (this.hasShutdown) {
449
+ return {
450
+ maxAttempts: 1,
451
+ debug: this.debug,
452
+ sdkName: this.sdkName,
453
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
454
+ };
455
+ }
456
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
457
+ }
381
458
  async patch(eventId, patch) {
382
459
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
383
460
  if (!this.enabled) return;
384
461
  if (!eventId || !eventId.trim()) return;
462
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
463
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
464
+ patch = { ...patch, input: capText(patch.input, maxChars) };
465
+ }
466
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
467
+ patch = { ...patch, output: capText(patch.output, maxChars) };
468
+ }
385
469
  if (this.debug) {
386
470
  console.log(`${this.prefix} queue patch`, {
387
471
  eventId,
@@ -428,9 +512,16 @@ var EventShipper = class {
428
512
  })));
429
513
  }
430
514
  async shutdown() {
431
- for (const t of this.timers.values()) clearTimeout(t);
432
- this.timers.clear();
433
- await this.flush();
515
+ this.hasShutdown = true;
516
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
517
+ try {
518
+ for (const t of this.timers.values()) clearTimeout(t);
519
+ this.timers.clear();
520
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
521
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
522
+ } finally {
523
+ this.shutdownDeadlineAt = void 0;
524
+ }
434
525
  }
435
526
  async trackSignal(signal) {
436
527
  var _a, _b;
@@ -450,16 +541,21 @@ var EventShipper = class {
450
541
  }
451
542
  }
452
543
  ];
544
+ if (!this.writeKey) return;
453
545
  const url = `${this.baseUrl}signals/track`;
546
+ const opts = this.requestOpts();
547
+ if (!opts) {
548
+ this.warnShutdownDrop("signal");
549
+ return;
550
+ }
454
551
  try {
455
- await postJson(url, body, this.authHeaders(), {
456
- maxAttempts: 3,
457
- debug: this.debug,
458
- sdkName: this.sdkName
459
- });
552
+ await postJson(url, body, this.authHeaders(), opts);
460
553
  } catch (err) {
461
554
  const msg = err instanceof Error ? err.message : String(err);
462
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
555
+ rateLimitedLog(
556
+ `${this.prefix}.send_signal_failed`,
557
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
558
+ );
463
559
  }
464
560
  }
465
561
  async identify(users) {
@@ -480,19 +576,30 @@ var EventShipper = class {
480
576
  traits: (_a = user.traits) != null ? _a : {}
481
577
  };
482
578
  });
579
+ if (!this.writeKey) return;
483
580
  if (body.length === 0) return;
484
581
  const url = `${this.baseUrl}users/identify`;
582
+ const opts = this.requestOpts();
583
+ if (!opts) {
584
+ this.warnShutdownDrop("identify");
585
+ return;
586
+ }
485
587
  try {
486
- await postJson(url, body, this.authHeaders(), {
487
- maxAttempts: 3,
488
- debug: this.debug,
489
- sdkName: this.sdkName
490
- });
588
+ await postJson(url, body, this.authHeaders(), opts);
491
589
  } catch (err) {
492
590
  const msg = err instanceof Error ? err.message : String(err);
493
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
591
+ rateLimitedLog(
592
+ `${this.prefix}.send_identify_failed`,
593
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
594
+ );
494
595
  }
495
596
  }
597
+ warnShutdownDrop(what) {
598
+ rateLimitedLog(
599
+ `${this.prefix}.shutdown_deadline`,
600
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
601
+ );
602
+ }
496
603
  async flushOne(eventId) {
497
604
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
498
605
  if (!this.enabled) return;
@@ -556,11 +663,25 @@ var EventShipper = class {
556
663
  endpoint: url
557
664
  });
558
665
  }
559
- const p = postJson(url, payload, this.authHeaders(), {
560
- maxAttempts: 3,
561
- debug: this.debug,
562
- sdkName: this.sdkName
563
- });
666
+ if (this.localDebuggerUrl) {
667
+ mirrorPartialEventToLocalDebugger(payload, {
668
+ baseUrl: this.localDebuggerUrl,
669
+ writeKey: this.writeKey,
670
+ debug: this.debug,
671
+ sdkName: this.sdkName
672
+ });
673
+ }
674
+ if (!this.writeKey) {
675
+ if (!isPending) this.sticky.delete(eventId);
676
+ return;
677
+ }
678
+ const opts = this.requestOpts();
679
+ if (!opts) {
680
+ this.warnShutdownDrop(`track_partial ${eventId}`);
681
+ if (!isPending) this.sticky.delete(eventId);
682
+ return;
683
+ }
684
+ const p = postJson(url, payload, this.authHeaders(), opts);
564
685
  this.inFlight.add(p);
565
686
  try {
566
687
  try {
@@ -570,7 +691,10 @@ var EventShipper = class {
570
691
  }
571
692
  } catch (err) {
572
693
  const msg = err instanceof Error ? err.message : String(err);
573
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
694
+ rateLimitedLog(
695
+ `${this.prefix}.send_track_partial_failed`,
696
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
697
+ );
574
698
  }
575
699
  } finally {
576
700
  this.inFlight.delete(p);
@@ -580,28 +704,114 @@ var EventShipper = class {
580
704
  }
581
705
  }
582
706
  };
583
- var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
584
- function resolveLocalDebuggerBaseUrl(baseUrl) {
585
- var _a, _b, _c;
586
- 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;
587
- return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
707
+ var DEFAULT_SECRET_KEY_NAMES = [
708
+ "apikey",
709
+ "apisecret",
710
+ "apitoken",
711
+ "secretaccesskey",
712
+ "sessiontoken",
713
+ "privatekey",
714
+ "privatekeyid",
715
+ "clientsecret",
716
+ "accesstoken",
717
+ "refreshtoken",
718
+ "oauthtoken",
719
+ "bearertoken",
720
+ "authorization",
721
+ "password",
722
+ "passphrase"
723
+ ];
724
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
725
+ function normalizeKeyName(name) {
726
+ return name.toLowerCase().replace(/[-_.]/g, "");
588
727
  }
589
- function mirrorTraceExportToLocalDebugger(body, options = {}) {
590
- var _a;
591
- const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
592
- if (!baseUrl) return;
593
- void postJson(`${baseUrl}traces`, body, {}, {
594
- maxAttempts: 1,
595
- debug: (_a = options.debug) != null ? _a : false,
596
- sdkName: options.sdkName
597
- }).catch(() => {
598
- });
728
+ function redactSecretsInObject(value, options) {
729
+ var _a, _b;
730
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
731
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
732
+ const seen = /* @__PURE__ */ new WeakSet();
733
+ const walk = (node) => {
734
+ if (node === null || typeof node !== "object") return node;
735
+ if (seen.has(node)) return "[CIRCULAR]";
736
+ seen.add(node);
737
+ if (Array.isArray(node)) {
738
+ return node.map((item) => walk(item));
739
+ }
740
+ const out = {};
741
+ for (const [k, v] of Object.entries(node)) {
742
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
743
+ out[k] = placeholder;
744
+ } else {
745
+ out[k] = walk(v);
746
+ }
747
+ }
748
+ return out;
749
+ };
750
+ return walk(value);
751
+ }
752
+ function buildSecretSet(names) {
753
+ const set = /* @__PURE__ */ new Set();
754
+ for (const name of names) set.add(normalizeKeyName(name));
755
+ return set;
756
+ }
757
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
758
+ "ai.request.providerOptions",
759
+ "ai.response.providerMetadata"
760
+ ];
761
+ function defaultTransformSpan(span) {
762
+ const attrs = span.attributes;
763
+ if (!attrs || attrs.length === 0) return span;
764
+ let nextAttrs;
765
+ for (let i = 0; i < attrs.length; i++) {
766
+ const attr = attrs[i];
767
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
768
+ if (redacted === void 0) continue;
769
+ if (!nextAttrs) nextAttrs = attrs.slice();
770
+ nextAttrs[i] = { key: attr.key, value: redacted };
771
+ }
772
+ if (!nextAttrs) return span;
773
+ return { ...span, attributes: nextAttrs };
774
+ }
775
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
776
+ function redactJsonAttributeValue(key, value) {
777
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
778
+ const json = value.stringValue;
779
+ if (typeof json !== "string" || json.length === 0) return void 0;
780
+ let parsed;
781
+ try {
782
+ parsed = JSON.parse(json);
783
+ } catch (e) {
784
+ return void 0;
785
+ }
786
+ const scrubbed = redactSecretsInObject(parsed);
787
+ let scrubbedJson;
788
+ try {
789
+ scrubbedJson = JSON.stringify(scrubbed);
790
+ } catch (e) {
791
+ return void 0;
792
+ }
793
+ if (scrubbedJson === json) return void 0;
794
+ return { stringValue: scrubbedJson };
795
+ }
796
+ function applyOtelSpanAttributeLimit(limit) {
797
+ var _a, _b;
798
+ try {
799
+ 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;
800
+ if (!raw) return limit;
801
+ const parsed = Number.parseInt(raw, 10);
802
+ if (Number.isFinite(parsed) && parsed > 0) {
803
+ return Math.min(limit, parsed);
804
+ }
805
+ } catch (e) {
806
+ }
807
+ return limit;
599
808
  }
600
809
  var TraceShipper = class {
601
810
  constructor(opts) {
602
811
  this.queue = [];
603
812
  this.inFlight = /* @__PURE__ */ new Set();
604
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
813
+ this.hasShutdown = false;
814
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
605
815
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
606
816
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
607
817
  this.enabled = opts.enabled !== false;
@@ -614,13 +824,79 @@ var TraceShipper = class {
614
824
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
615
825
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
616
826
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
617
- const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
618
- if (localDebugger) {
619
- this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
620
- if (this.debug) {
621
- console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
827
+ this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
828
+ if (this.debug && this.localDebuggerUrl) {
829
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
830
+ }
831
+ this.transformSpanHook = opts.transformSpan;
832
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
833
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
834
+ }
835
+ /**
836
+ * Cap every string attribute value on the span. O(#attributes) length
837
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
838
+ * pipeline so the default secret-scrub still sees parseable JSON in
839
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
840
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
841
+ * in the surviving prefix).
842
+ *
843
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
844
+ * for span content, matching the Python SDK and the OTel SDK convention.
845
+ */
846
+ capSpanAttributes(span) {
847
+ var _a;
848
+ const maxChars = applyOtelSpanAttributeLimit(
849
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
850
+ );
851
+ const attrs = span.attributes;
852
+ if (!attrs || attrs.length === 0) return span;
853
+ let nextAttrs;
854
+ for (let i = 0; i < attrs.length; i++) {
855
+ const attr = attrs[i];
856
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
857
+ if (typeof value !== "string" || value.length <= maxChars) continue;
858
+ if (!nextAttrs) nextAttrs = attrs.slice();
859
+ nextAttrs[i] = {
860
+ key: attr.key,
861
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
862
+ };
863
+ }
864
+ if (!nextAttrs) return span;
865
+ return { ...span, attributes: nextAttrs };
866
+ }
867
+ /**
868
+ * Apply the user `transformSpan` hook (if any) followed by the default
869
+ * redactor (unless disabled). Returns either the (possibly new) span to
870
+ * ship, or `null` to drop the span entirely.
871
+ *
872
+ * Ordering: user hook runs first so callers can rewrite the span freely
873
+ * (rename attrs, add new ones, scrub things the default doesn't know
874
+ * about). The default redactor then runs on whatever the user produced,
875
+ * acting as the always-on floor for documented BYOK secrets. If the user
876
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
877
+ *
878
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
879
+ * hook can never accidentally ship raw, un-redacted spans.
880
+ */
881
+ redactSpan(span) {
882
+ let current = span;
883
+ if (this.transformSpanHook) {
884
+ try {
885
+ const result = this.transformSpanHook(current);
886
+ if (result === null) return null;
887
+ if (result !== void 0) current = result;
888
+ } catch (err) {
889
+ if (this.debug) {
890
+ const msg = err instanceof Error ? err.message : String(err);
891
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
892
+ }
893
+ return null;
622
894
  }
623
895
  }
896
+ if (!this.disableDefaultRedaction) {
897
+ current = defaultTransformSpan(current);
898
+ }
899
+ return this.capSpanAttributes(current);
624
900
  }
625
901
  isDebugEnabled() {
626
902
  return this.debug;
@@ -638,8 +914,8 @@ var TraceShipper = class {
638
914
  ];
639
915
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
640
916
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
641
- if (this.localDebuggerUrl) {
642
- const openSpan = buildOtlpSpan({
917
+ this.mirrorToLocalDebugger(
918
+ buildOtlpSpan({
643
919
  ids: span.ids,
644
920
  name: span.name,
645
921
  startTimeUnixNano: span.startTimeUnixNano,
@@ -647,16 +923,21 @@ var TraceShipper = class {
647
923
  // placeholder — will be updated on endSpan
648
924
  attributes: span.attributes,
649
925
  status: { code: SpanStatusCode.UNSET }
650
- });
651
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
652
- mirrorTraceExportToLocalDebugger(body, {
653
- baseUrl: this.localDebuggerUrl,
654
- debug: false,
655
- sdkName: this.sdkName
656
- });
657
- }
926
+ })
927
+ );
658
928
  return span;
659
929
  }
930
+ mirrorToLocalDebugger(span) {
931
+ if (!this.localDebuggerUrl) return;
932
+ const redacted = this.redactSpan(span);
933
+ if (redacted === null) return;
934
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
935
+ mirrorTraceExportToLocalDebugger(body, {
936
+ baseUrl: this.localDebuggerUrl,
937
+ debug: false,
938
+ sdkName: this.sdkName
939
+ });
940
+ }
660
941
  endSpan(span, extra) {
661
942
  var _a, _b;
662
943
  if (span.endTimeUnixNano) return;
@@ -678,14 +959,7 @@ var TraceShipper = class {
678
959
  status
679
960
  });
680
961
  this.enqueue(otlp);
681
- if (this.localDebuggerUrl) {
682
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
683
- mirrorTraceExportToLocalDebugger(body, {
684
- baseUrl: this.localDebuggerUrl,
685
- debug: false,
686
- sdkName: this.sdkName
687
- });
688
- }
962
+ this.mirrorToLocalDebugger(otlp);
689
963
  }
690
964
  createSpan(args) {
691
965
  var _a;
@@ -703,14 +977,7 @@ var TraceShipper = class {
703
977
  status: args.status
704
978
  });
705
979
  this.enqueue(otlp);
706
- if (this.localDebuggerUrl) {
707
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
708
- mirrorTraceExportToLocalDebugger(body, {
709
- baseUrl: this.localDebuggerUrl,
710
- debug: false,
711
- sdkName: this.sdkName
712
- });
713
- }
980
+ this.mirrorToLocalDebugger(otlp);
714
981
  }
715
982
  enqueue(span) {
716
983
  if (!this.enabled) return;
@@ -722,10 +989,12 @@ var TraceShipper = class {
722
989
  )}`
723
990
  );
724
991
  }
992
+ const redacted = this.redactSpan(span);
993
+ if (redacted === null) return;
725
994
  if (this.queue.length >= this.maxQueueSize) {
726
995
  this.queue.shift();
727
996
  }
728
- this.queue.push(span);
997
+ this.queue.push(redacted);
729
998
  if (this.queue.length >= this.maxBatchSize) {
730
999
  void this.flush().catch(() => {
731
1000
  });
@@ -747,6 +1016,17 @@ var TraceShipper = class {
747
1016
  }
748
1017
  while (this.queue.length > 0) {
749
1018
  const batch = this.queue.splice(0, this.maxBatchSize);
1019
+ if (!this.writeKey) continue;
1020
+ const opts = this.requestOpts();
1021
+ if (!opts) {
1022
+ rateLimitedLog(
1023
+ `${this.prefix}.shutdown_deadline`,
1024
+ () => console.warn(
1025
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1026
+ )
1027
+ );
1028
+ continue;
1029
+ }
750
1030
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
751
1031
  const url = `${this.baseUrl}traces`;
752
1032
  if (this.debug) {
@@ -755,11 +1035,7 @@ var TraceShipper = class {
755
1035
  endpoint: url
756
1036
  });
757
1037
  }
758
- const p = postJson(url, body, this.authHeaders(), {
759
- maxAttempts: 3,
760
- debug: this.debug,
761
- sdkName: this.sdkName
762
- });
1038
+ const p = postJson(url, body, this.authHeaders(), opts);
763
1039
  this.inFlight.add(p);
764
1040
  try {
765
1041
  try {
@@ -767,21 +1043,61 @@ var TraceShipper = class {
767
1043
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
768
1044
  } catch (err) {
769
1045
  const msg = err instanceof Error ? err.message : String(err);
770
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1046
+ rateLimitedLog(
1047
+ `${this.prefix}.send_spans_failed`,
1048
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1049
+ );
771
1050
  }
772
1051
  } finally {
773
1052
  this.inFlight.delete(p);
774
1053
  }
775
1054
  }
776
1055
  }
1056
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1057
+ requestOpts() {
1058
+ if (this.shutdownDeadlineAt !== void 0) {
1059
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1060
+ if (remainingMs <= 0) return null;
1061
+ return {
1062
+ maxAttempts: 1,
1063
+ debug: this.debug,
1064
+ sdkName: this.sdkName,
1065
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1066
+ };
1067
+ }
1068
+ if (this.hasShutdown) {
1069
+ return {
1070
+ maxAttempts: 1,
1071
+ debug: this.debug,
1072
+ sdkName: this.sdkName,
1073
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1074
+ };
1075
+ }
1076
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1077
+ }
777
1078
  async shutdown() {
778
- if (this.timer) {
779
- clearTimeout(this.timer);
780
- this.timer = void 0;
1079
+ this.hasShutdown = true;
1080
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1081
+ try {
1082
+ if (this.timer) {
1083
+ clearTimeout(this.timer);
1084
+ this.timer = void 0;
1085
+ }
1086
+ const drain = async () => {
1087
+ await this.flush();
1088
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1089
+ })));
1090
+ };
1091
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1092
+ if (!settled) {
1093
+ rateLimitedLog(
1094
+ `${this.prefix}.shutdown_deadline`,
1095
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1096
+ );
1097
+ }
1098
+ } finally {
1099
+ this.shutdownDeadlineAt = void 0;
781
1100
  }
782
- await this.flush();
783
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
784
- })));
785
1101
  }
786
1102
  };
787
1103
 
@@ -789,6 +1105,134 @@ var TraceShipper = class {
789
1105
  var import_async_hooks = require("async_hooks");
790
1106
  globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
791
1107
 
1108
+ // src/config.ts
1109
+ var import_node_fs = require("fs");
1110
+ var import_node_os = require("os");
1111
+ var import_node_path = require("path");
1112
+ function loadConfig(projectDirectory) {
1113
+ var _a, _b, _c, _d, _e, _f, _g;
1114
+ let merged = {};
1115
+ const configPaths = [
1116
+ (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
1117
+ (0, import_node_path.join)(projectDirectory, ".opencode", "raindrop.json")
1118
+ ];
1119
+ for (const configPath of configPaths) {
1120
+ try {
1121
+ if ((0, import_node_fs.existsSync)(configPath)) {
1122
+ const content = (0, import_node_fs.readFileSync)(configPath, "utf-8");
1123
+ const parsed = JSON.parse(content);
1124
+ merged = { ...merged, ...parsed };
1125
+ }
1126
+ } catch (e) {
1127
+ }
1128
+ }
1129
+ let eventMetadata;
1130
+ const envMeta = process.env["RAINDROP_EVENT_METADATA"];
1131
+ if (envMeta) {
1132
+ try {
1133
+ eventMetadata = JSON.parse(envMeta);
1134
+ } catch (e) {
1135
+ }
1136
+ }
1137
+ return {
1138
+ writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
1139
+ endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
1140
+ eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
1141
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
1142
+ captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
1143
+ eventMetadata,
1144
+ localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url)
1145
+ };
1146
+ }
1147
+ function resolveLocalWorkshopUrl(fileValue) {
1148
+ const envValue = process.env["RAINDROP_LOCAL_WORKSHOP_URL"];
1149
+ if (envValue !== void 0) {
1150
+ if (envValue === "" || envValue.toLowerCase() === "null" || envValue.toLowerCase() === "false") {
1151
+ return null;
1152
+ }
1153
+ return envValue;
1154
+ }
1155
+ return fileValue;
1156
+ }
1157
+
1158
+ // package.json
1159
+ var package_default = {
1160
+ name: "@raindrop-ai/opencode-plugin",
1161
+ version: "0.0.13",
1162
+ description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
1163
+ type: "module",
1164
+ main: "dist/index.js",
1165
+ module: "dist/index.js",
1166
+ types: "dist/index.d.ts",
1167
+ license: "MIT",
1168
+ repository: {
1169
+ type: "git",
1170
+ url: "git+https://github.com/raindrop-ai/raindrop-js.git",
1171
+ directory: "packages/opencode-plugin"
1172
+ },
1173
+ homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
1174
+ bugs: {
1175
+ url: "https://github.com/raindrop-ai/raindrop-js/issues"
1176
+ },
1177
+ exports: {
1178
+ ".": {
1179
+ types: "./dist/index.d.ts",
1180
+ import: "./dist/index.js",
1181
+ require: "./dist/index.cjs"
1182
+ }
1183
+ },
1184
+ sideEffects: false,
1185
+ files: [
1186
+ "dist/**"
1187
+ ],
1188
+ scripts: {
1189
+ build: "tsup",
1190
+ dev: "tsup --watch",
1191
+ clean: "rm -rf dist",
1192
+ test: "cd tests && pnpm test"
1193
+ },
1194
+ peerDependencies: {
1195
+ "@opencode-ai/plugin": ">=1.3.0",
1196
+ "@opencode-ai/sdk": ">=1.3.0"
1197
+ },
1198
+ peerDependenciesMeta: {
1199
+ "@opencode-ai/sdk": {
1200
+ optional: true
1201
+ }
1202
+ },
1203
+ devDependencies: {
1204
+ "@raindrop-ai/core": "workspace:*",
1205
+ "@opencode-ai/plugin": "^1.3.3",
1206
+ "@opencode-ai/sdk": "^1.3.3",
1207
+ "@types/node": "^20.11.17",
1208
+ tsup: "^8.4.0",
1209
+ typescript: "^5.3.3"
1210
+ },
1211
+ tsup: {
1212
+ entry: [
1213
+ "src/index.ts"
1214
+ ],
1215
+ format: [
1216
+ "cjs",
1217
+ "esm"
1218
+ ],
1219
+ dts: {
1220
+ resolve: true
1221
+ },
1222
+ clean: true,
1223
+ noExternal: [
1224
+ "@raindrop-ai/core"
1225
+ ]
1226
+ },
1227
+ publishConfig: {
1228
+ access: "public"
1229
+ }
1230
+ };
1231
+
1232
+ // src/package-info.ts
1233
+ var PLUGIN_NAME = package_default.name;
1234
+ var PLUGIN_VERSION = package_default.version;
1235
+
792
1236
  // src/shipper.ts
793
1237
  var EventShipper2 = class extends EventShipper {
794
1238
  constructor(opts) {
@@ -824,6 +1268,86 @@ var TraceShipper2 = class extends TraceShipper {
824
1268
  }
825
1269
  };
826
1270
 
1271
+ // src/bounded.ts
1272
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1273
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1274
+ var MAX_BOUNDED_DEPTH = 12;
1275
+ function truncateToLimit2(text, limit) {
1276
+ if (text.length <= limit) return text;
1277
+ if (limit > TRUNCATION_MARKER2.length) {
1278
+ return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1279
+ }
1280
+ return text.slice(0, limit);
1281
+ }
1282
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1283
+ if (value.length <= limit) return value;
1284
+ return truncateToLimit2(value, limit);
1285
+ }
1286
+ function boundedClone2(value, budget, depth) {
1287
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1288
+ if (typeof value === "string") {
1289
+ if (value.length > budget.remaining) {
1290
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1291
+ budget.remaining = 0;
1292
+ return taken;
1293
+ }
1294
+ budget.remaining -= Math.max(value.length, 1);
1295
+ return value;
1296
+ }
1297
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1298
+ budget.remaining -= 8;
1299
+ return value;
1300
+ }
1301
+ if (typeof value !== "object") {
1302
+ budget.remaining -= 8;
1303
+ return value;
1304
+ }
1305
+ if (depth >= MAX_BOUNDED_DEPTH) {
1306
+ budget.remaining -= 16;
1307
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1308
+ }
1309
+ const withToJson = value;
1310
+ if (typeof withToJson.toJSON === "function") {
1311
+ try {
1312
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1313
+ } catch (e) {
1314
+ }
1315
+ }
1316
+ if (Array.isArray(value)) {
1317
+ const out2 = [];
1318
+ for (const item of value) {
1319
+ if (budget.remaining <= 0) {
1320
+ out2.push(TRUNCATION_MARKER2);
1321
+ break;
1322
+ }
1323
+ out2.push(boundedClone2(item, budget, depth + 1));
1324
+ }
1325
+ return out2;
1326
+ }
1327
+ const out = {};
1328
+ for (const key of Object.keys(value)) {
1329
+ if (budget.remaining <= 0) {
1330
+ out["..."] = TRUNCATION_MARKER2;
1331
+ break;
1332
+ }
1333
+ budget.remaining -= Math.max(key.length, 1);
1334
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1335
+ }
1336
+ return out;
1337
+ }
1338
+ function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
1339
+ var _a;
1340
+ try {
1341
+ if (typeof value === "string") {
1342
+ return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
1343
+ }
1344
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1345
+ return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
1346
+ } catch (e) {
1347
+ return capText2(String(value), limit);
1348
+ }
1349
+ }
1350
+
827
1351
  // src/tracing-linkage-helpers.ts
828
1352
  function createSessionParentMapHelpers({
829
1353
  sessions: sessions2,
@@ -901,6 +1425,15 @@ function createSessionParentMapHelpers({
901
1425
  }
902
1426
 
903
1427
  // src/tracing.ts
1428
+ var ERROR_LOG_INTERVAL_MS = 3e4;
1429
+ var lastErrorLogAt = /* @__PURE__ */ new Map();
1430
+ function rateLimitedErrorLog(key, message) {
1431
+ const now = Date.now();
1432
+ const last = lastErrorLogAt.get(key);
1433
+ if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
1434
+ lastErrorLogAt.set(key, now);
1435
+ console.log(message);
1436
+ }
904
1437
  var sessions = /* @__PURE__ */ new Map();
905
1438
  var taskContexts = /* @__PURE__ */ new Map();
906
1439
  var runningTaskCallsBySession = /* @__PURE__ */ new Map();
@@ -959,6 +1492,19 @@ function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMes
959
1492
  }
960
1493
  return true;
961
1494
  }
1495
+ function unwrapQuotes(s) {
1496
+ if (s.length < 2) return s;
1497
+ const first = s[0];
1498
+ const last = s[s.length - 1];
1499
+ if ((first === '"' || first === "'") && first === last) return s.slice(1, -1);
1500
+ return s;
1501
+ }
1502
+ function buildPromptMessages(systemPrompt, userInput) {
1503
+ const messages = [];
1504
+ if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
1505
+ messages.push({ role: "user", content: unwrapQuotes(userInput) });
1506
+ return JSON.stringify(messages);
1507
+ }
962
1508
  function getHostname() {
963
1509
  var _a;
964
1510
  try {
@@ -1040,7 +1586,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1040
1586
  const state = sessions.get(partSessionID);
1041
1587
  if (!state) return;
1042
1588
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
1043
- state.outputParts.set(messageId, partObj["text"]);
1589
+ state.outputParts.set(messageId, capText2(partObj["text"]));
1044
1590
  } else if (partObj["type"] === "tool" && messageId) {
1045
1591
  const tool = partObj["tool"];
1046
1592
  const callID = partObj["callID"];
@@ -1070,7 +1616,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1070
1616
  if (typeof errorMessage !== "string" || errorMessage === "")
1071
1617
  throw new Error("message.part.updated: error tool state must have error string");
1072
1618
  const input = toolState["input"];
1073
- const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? input : JSON.stringify(input);
1619
+ const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
1074
1620
  if (endPendingToolSpanWithError(
1075
1621
  state,
1076
1622
  callID,
@@ -1083,7 +1629,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1083
1629
  }
1084
1630
  }
1085
1631
  } else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
1086
- state.reasoningParts.set(messageId, partObj["text"]);
1632
+ state.reasoningParts.set(messageId, capText2(partObj["text"]));
1087
1633
  }
1088
1634
  } else if (event.type === "message.updated") {
1089
1635
  const msgInfo = props["info"];
@@ -1184,9 +1730,14 @@ type: ${name != null ? name : "UnknownError"}`;
1184
1730
  const rootSpan = state.currentRootSpan;
1185
1731
  const isCompaction = state.isCompacting === true;
1186
1732
  if (state.parentId) {
1733
+ const userInput = unwrapQuotes(state.currentInput);
1187
1734
  const llmAttrs = [
1188
1735
  attrString("ai.operationId", "generateText"),
1189
- attrString("ai.prompt", state.currentInput),
1736
+ attrString("ai.prompt", userInput),
1737
+ attrString(
1738
+ "ai.prompt.messages",
1739
+ buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1740
+ ),
1190
1741
  attrString("ai.response.text", outputText),
1191
1742
  attrString("gen_ai.system", providerID),
1192
1743
  attrString("gen_ai.request.model", modelID),
@@ -1231,9 +1782,14 @@ type: ${name != null ? name : "UnknownError"}`;
1231
1782
  });
1232
1783
  } else {
1233
1784
  rootSpan.name = isCompaction ? "ai.compaction" : modelName;
1785
+ const userInput = unwrapQuotes(state.currentInput);
1234
1786
  const rootAttrs = [
1235
1787
  attrString("ai.operationId", "generateText"),
1236
- attrString("ai.prompt", state.currentInput),
1788
+ attrString("ai.prompt", userInput),
1789
+ attrString(
1790
+ "ai.prompt.messages",
1791
+ buildPromptMessages(state.currentSystemPrompt, state.currentInput)
1792
+ ),
1237
1793
  attrString("ai.response.text", outputText),
1238
1794
  attrString("gen_ai.system", providerID),
1239
1795
  attrString("gen_ai.request.model", modelID),
@@ -1340,7 +1896,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1340
1896
  sessions.delete(String(errorSessionID));
1341
1897
  }
1342
1898
  } catch (err) {
1343
- console.log(
1899
+ rateLimitedErrorLog(
1900
+ "event",
1344
1901
  `[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
1345
1902
  );
1346
1903
  }
@@ -1396,7 +1953,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1396
1953
  ...promptMeta,
1397
1954
  properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
1398
1955
  };
1399
- const userText = textParts.join("\n");
1956
+ const userText = capText2(textParts.join("\n"));
1400
1957
  state.currentInput = userText;
1401
1958
  if (state.parentId) {
1402
1959
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
@@ -1505,7 +2062,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1505
2062
  });
1506
2063
  }
1507
2064
  } catch (err) {
1508
- console.log(
2065
+ rateLimitedErrorLog(
2066
+ "tool.execute.before",
1509
2067
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
1510
2068
  );
1511
2069
  }
@@ -1520,7 +2078,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1520
2078
  if (!state || !state.currentEventId) return;
1521
2079
  if (args === void 0 || args === null)
1522
2080
  throw new Error("tool.execute.after: args is required");
1523
- const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
2081
+ const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
1524
2082
  const startInfo = state.toolSpanStarts.get(callID);
1525
2083
  state.toolSpanStarts.delete(callID);
1526
2084
  const resultMetadata = result.metadata;
@@ -1533,13 +2091,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1533
2091
  const endTimeUnixNano = nowUnixNanoString();
1534
2092
  if (!("output" in result) || result.output === void 0)
1535
2093
  throw new Error("tool.execute.after: result.output is required");
1536
- let toolResult;
1537
- try {
1538
- toolResult = JSON.stringify(result.output);
1539
- } catch (err) {
1540
- const msg = err instanceof Error ? err.message : String(err);
1541
- throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
1542
- }
2094
+ const toolResult = boundedStringify(result.output);
1543
2095
  if (startInfo.liveSpan) {
1544
2096
  traceShipper.endSpan(startInfo.liveSpan, {
1545
2097
  attributes: [
@@ -1568,7 +2120,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1568
2120
  markTaskCallFinished(sessionID, callID);
1569
2121
  }
1570
2122
  } catch (err) {
1571
- console.log(
2123
+ rateLimitedErrorLog(
2124
+ "tool.execute.after",
1572
2125
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1573
2126
  );
1574
2127
  }
@@ -1585,7 +2138,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1585
2138
  state.isCompacting = true;
1586
2139
  }
1587
2140
  } catch (err) {
1588
- console.log(
2141
+ rateLimitedErrorLog(
2142
+ "experimental.session.compacting",
1589
2143
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1590
2144
  );
1591
2145
  }
@@ -1604,11 +2158,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1604
2158
  sessions.set(sessionID, state);
1605
2159
  }
1606
2160
  const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1607
- const TRUNCATION_MARKER = "\n...[truncated]";
2161
+ const TRUNCATION_MARKER3 = "\n...[truncated]";
1608
2162
  const joined = output.system.join("\n\n");
1609
- state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
2163
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
1610
2164
  } catch (err) {
1611
- console.log(
2165
+ rateLimitedErrorLog(
2166
+ "experimental.chat.system.transform",
1612
2167
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1613
2168
  );
1614
2169
  }
@@ -1624,25 +2179,33 @@ async function plugin(input) {
1624
2179
  console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
1625
2180
  }
1626
2181
  appLog("info", `Loading ${PLUGIN_NAME} v${PLUGIN_VERSION}`);
1627
- if (!config.writeKey) {
2182
+ const resolvedLocalUrl = resolveLocalDebuggerBaseUrl(config.localWorkshopUrl);
2183
+ const hasLocalDestination = resolvedLocalUrl !== null;
2184
+ if (!config.writeKey && !hasLocalDestination) {
1628
2185
  appLog(
1629
2186
  "warn",
1630
- "RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
2187
+ "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."
1631
2188
  );
1632
2189
  return {};
1633
2190
  }
1634
2191
  if (config.debug) {
1635
- appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
2192
+ const destinations = [
2193
+ config.writeKey ? `cloud (${config.endpoint})` : null,
2194
+ resolvedLocalUrl ? `local Workshop (${resolvedLocalUrl})` : null
2195
+ ].filter(Boolean);
2196
+ appLog("info", `Raindrop tracing enabled \u2014 destinations: ${destinations.join(", ")}`);
1636
2197
  }
1637
2198
  const eventShipper = new EventShipper2({
1638
2199
  writeKey: config.writeKey,
1639
2200
  endpoint: config.endpoint,
1640
- debug: config.debug
2201
+ debug: config.debug,
2202
+ localDebuggerUrl: config.localWorkshopUrl
1641
2203
  });
1642
2204
  const traceShipper = new TraceShipper2({
1643
2205
  writeKey: config.writeKey,
1644
2206
  endpoint: config.endpoint,
1645
- debug: config.debug
2207
+ debug: config.debug,
2208
+ localDebuggerUrl: config.localWorkshopUrl
1646
2209
  });
1647
2210
  const worktree = (_a = input.worktree) != null ? _a : input.directory;
1648
2211
  const hooks = createHooks(