@raindrop-ai/opencode-plugin 0.0.12 → 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,7 +5,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // ../core/dist/chunk-OLYXZCOK.js
8
+ // ../core/dist/chunk-U5HUTMR5.js
9
9
  function getCrypto() {
10
10
  const c = globalThis.crypto;
11
11
  return c;
@@ -52,6 +52,8 @@ function base64Encode(bytes) {
52
52
  function generateId() {
53
53
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
54
54
  }
55
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
56
+ var MAX_RETRY_DELAY_MS = 3e4;
55
57
  function wait(ms) {
56
58
  return new Promise((resolve) => setTimeout(resolve, ms));
57
59
  }
@@ -59,6 +61,46 @@ function formatEndpoint(endpoint) {
59
61
  if (!endpoint) return void 0;
60
62
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
61
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
+ }
62
104
  function parseRetryAfter(headers) {
63
105
  var _a;
64
106
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -75,12 +117,12 @@ function parseRetryAfter(headers) {
75
117
  function getRetryDelayMs(attemptNumber, previousError) {
76
118
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
77
119
  const v = previousError.retryAfterMs;
78
- 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);
79
121
  }
80
122
  if (attemptNumber <= 1) return 0;
81
123
  const base = 500;
82
124
  const factor = Math.pow(2, attemptNumber - 2);
83
- return base * factor;
125
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
84
126
  }
85
127
  async function withRetry(operation, opName, opts) {
86
128
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -115,7 +157,9 @@ async function withRetry(operation, opName, opts) {
115
157
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
116
158
  }
117
159
  async function postJson(url, body, headers, opts) {
118
- 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;
119
163
  await withRetry(
120
164
  async () => {
121
165
  const resp = await fetch(url, {
@@ -124,7 +168,8 @@ async function postJson(url, body, headers, opts) {
124
168
  "Content-Type": "application/json",
125
169
  ...headers
126
170
  },
127
- body: JSON.stringify(body)
171
+ body: JSON.stringify(body),
172
+ signal: AbortSignal.timeout(timeoutMs)
128
173
  });
129
174
  if (!resp.ok) {
130
175
  const text = await resp.text().catch(() => "");
@@ -141,6 +186,27 @@ async function postJson(url, body, headers, opts) {
141
186
  opts
142
187
  );
143
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
+ }
144
210
  var SpanStatusCode = {
145
211
  UNSET: 0,
146
212
  OK: 1,
@@ -283,6 +349,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
283
349
  }).catch(() => {
284
350
  });
285
351
  }
352
+ var SHUTDOWN_DEADLINE_MS = 1e4;
353
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
286
354
  function mergePatches(target, source) {
287
355
  var _a, _b, _c, _d;
288
356
  const out = { ...target, ...source };
@@ -300,6 +368,7 @@ var EventShipper = class {
300
368
  this.sticky = /* @__PURE__ */ new Map();
301
369
  this.timers = /* @__PURE__ */ new Map();
302
370
  this.inFlight = /* @__PURE__ */ new Set();
371
+ this.hasShutdown = false;
303
372
  var _a, _b, _c, _d, _e, _f, _g, _h;
304
373
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
305
374
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -309,6 +378,7 @@ var EventShipper = class {
309
378
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
310
379
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
311
380
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
381
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
312
382
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
313
383
  if (this.debug && this.localDebuggerUrl) {
314
384
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -331,10 +401,52 @@ var EventShipper = class {
331
401
  authHeaders() {
332
402
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
333
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
+ }
334
439
  async patch(eventId, patch) {
335
440
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
336
441
  if (!this.enabled) return;
337
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
+ }
338
450
  if (this.debug) {
339
451
  console.log(`${this.prefix} queue patch`, {
340
452
  eventId,
@@ -381,9 +493,16 @@ var EventShipper = class {
381
493
  })));
382
494
  }
383
495
  async shutdown() {
384
- for (const t of this.timers.values()) clearTimeout(t);
385
- this.timers.clear();
386
- 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
+ }
387
506
  }
388
507
  async trackSignal(signal) {
389
508
  var _a, _b;
@@ -405,15 +524,19 @@ var EventShipper = class {
405
524
  ];
406
525
  if (!this.writeKey) return;
407
526
  const url = `${this.baseUrl}signals/track`;
527
+ const opts = this.requestOpts();
528
+ if (!opts) {
529
+ this.warnShutdownDrop("signal");
530
+ return;
531
+ }
408
532
  try {
409
- await postJson(url, body, this.authHeaders(), {
410
- maxAttempts: 3,
411
- debug: this.debug,
412
- sdkName: this.sdkName
413
- });
533
+ await postJson(url, body, this.authHeaders(), opts);
414
534
  } catch (err) {
415
535
  const msg = err instanceof Error ? err.message : String(err);
416
- 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
+ );
417
540
  }
418
541
  }
419
542
  async identify(users) {
@@ -437,17 +560,27 @@ var EventShipper = class {
437
560
  if (!this.writeKey) return;
438
561
  if (body.length === 0) return;
439
562
  const url = `${this.baseUrl}users/identify`;
563
+ const opts = this.requestOpts();
564
+ if (!opts) {
565
+ this.warnShutdownDrop("identify");
566
+ return;
567
+ }
440
568
  try {
441
- await postJson(url, body, this.authHeaders(), {
442
- maxAttempts: 3,
443
- debug: this.debug,
444
- sdkName: this.sdkName
445
- });
569
+ await postJson(url, body, this.authHeaders(), opts);
446
570
  } catch (err) {
447
571
  const msg = err instanceof Error ? err.message : String(err);
448
- 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
+ );
449
576
  }
450
577
  }
578
+ warnShutdownDrop(what) {
579
+ rateLimitedLog(
580
+ `${this.prefix}.shutdown_deadline`,
581
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
582
+ );
583
+ }
451
584
  async flushOne(eventId) {
452
585
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
453
586
  if (!this.enabled) return;
@@ -523,11 +656,13 @@ var EventShipper = class {
523
656
  if (!isPending) this.sticky.delete(eventId);
524
657
  return;
525
658
  }
526
- const p = postJson(url, payload, this.authHeaders(), {
527
- maxAttempts: 3,
528
- debug: this.debug,
529
- sdkName: this.sdkName
530
- });
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);
531
666
  this.inFlight.add(p);
532
667
  try {
533
668
  try {
@@ -537,7 +672,10 @@ var EventShipper = class {
537
672
  }
538
673
  } catch (err) {
539
674
  const msg = err instanceof Error ? err.message : String(err);
540
- 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
+ );
541
679
  }
542
680
  } finally {
543
681
  this.inFlight.delete(p);
@@ -547,10 +685,113 @@ var EventShipper = class {
547
685
  }
548
686
  }
549
687
  };
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, "");
708
+ }
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;
789
+ }
550
790
  var TraceShipper = class {
551
791
  constructor(opts) {
552
792
  this.queue = [];
553
793
  this.inFlight = /* @__PURE__ */ new Set();
794
+ this.hasShutdown = false;
554
795
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
555
796
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
556
797
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -568,6 +809,75 @@ var TraceShipper = class {
568
809
  if (this.debug && this.localDebuggerUrl) {
569
810
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
570
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;
875
+ }
876
+ }
877
+ if (!this.disableDefaultRedaction) {
878
+ current = defaultTransformSpan(current);
879
+ }
880
+ return this.capSpanAttributes(current);
571
881
  }
572
882
  isDebugEnabled() {
573
883
  return this.debug;
@@ -585,8 +895,8 @@ var TraceShipper = class {
585
895
  ];
586
896
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
587
897
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
588
- if (this.localDebuggerUrl) {
589
- const openSpan = buildOtlpSpan({
898
+ this.mirrorToLocalDebugger(
899
+ buildOtlpSpan({
590
900
  ids: span.ids,
591
901
  name: span.name,
592
902
  startTimeUnixNano: span.startTimeUnixNano,
@@ -594,16 +904,21 @@ var TraceShipper = class {
594
904
  // placeholder — will be updated on endSpan
595
905
  attributes: span.attributes,
596
906
  status: { code: SpanStatusCode.UNSET }
597
- });
598
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
599
- mirrorTraceExportToLocalDebugger(body, {
600
- baseUrl: this.localDebuggerUrl,
601
- debug: false,
602
- sdkName: this.sdkName
603
- });
604
- }
907
+ })
908
+ );
605
909
  return span;
606
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
+ }
607
922
  endSpan(span, extra) {
608
923
  var _a, _b;
609
924
  if (span.endTimeUnixNano) return;
@@ -625,14 +940,7 @@ var TraceShipper = class {
625
940
  status
626
941
  });
627
942
  this.enqueue(otlp);
628
- if (this.localDebuggerUrl) {
629
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
630
- mirrorTraceExportToLocalDebugger(body, {
631
- baseUrl: this.localDebuggerUrl,
632
- debug: false,
633
- sdkName: this.sdkName
634
- });
635
- }
943
+ this.mirrorToLocalDebugger(otlp);
636
944
  }
637
945
  createSpan(args) {
638
946
  var _a;
@@ -650,14 +958,7 @@ var TraceShipper = class {
650
958
  status: args.status
651
959
  });
652
960
  this.enqueue(otlp);
653
- if (this.localDebuggerUrl) {
654
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
655
- mirrorTraceExportToLocalDebugger(body, {
656
- baseUrl: this.localDebuggerUrl,
657
- debug: false,
658
- sdkName: this.sdkName
659
- });
660
- }
961
+ this.mirrorToLocalDebugger(otlp);
661
962
  }
662
963
  enqueue(span) {
663
964
  if (!this.enabled) return;
@@ -669,10 +970,12 @@ var TraceShipper = class {
669
970
  )}`
670
971
  );
671
972
  }
973
+ const redacted = this.redactSpan(span);
974
+ if (redacted === null) return;
672
975
  if (this.queue.length >= this.maxQueueSize) {
673
976
  this.queue.shift();
674
977
  }
675
- this.queue.push(span);
978
+ this.queue.push(redacted);
676
979
  if (this.queue.length >= this.maxBatchSize) {
677
980
  void this.flush().catch(() => {
678
981
  });
@@ -695,6 +998,16 @@ var TraceShipper = class {
695
998
  while (this.queue.length > 0) {
696
999
  const batch = this.queue.splice(0, this.maxBatchSize);
697
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
+ }
698
1011
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
699
1012
  const url = `${this.baseUrl}traces`;
700
1013
  if (this.debug) {
@@ -703,11 +1016,7 @@ var TraceShipper = class {
703
1016
  endpoint: url
704
1017
  });
705
1018
  }
706
- const p = postJson(url, body, this.authHeaders(), {
707
- maxAttempts: 3,
708
- debug: this.debug,
709
- sdkName: this.sdkName
710
- });
1019
+ const p = postJson(url, body, this.authHeaders(), opts);
711
1020
  this.inFlight.add(p);
712
1021
  try {
713
1022
  try {
@@ -715,21 +1024,61 @@ var TraceShipper = class {
715
1024
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
716
1025
  } catch (err) {
717
1026
  const msg = err instanceof Error ? err.message : String(err);
718
- 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
+ );
719
1031
  }
720
1032
  } finally {
721
1033
  this.inFlight.delete(p);
722
1034
  }
723
1035
  }
724
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
+ }
725
1059
  async shutdown() {
726
- if (this.timer) {
727
- clearTimeout(this.timer);
728
- 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;
729
1081
  }
730
- await this.flush();
731
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
732
- })));
733
1082
  }
734
1083
  };
735
1084
 
@@ -790,7 +1139,7 @@ function resolveLocalWorkshopUrl(fileValue) {
790
1139
  // package.json
791
1140
  var package_default = {
792
1141
  name: "@raindrop-ai/opencode-plugin",
793
- version: "0.0.12",
1142
+ version: "0.0.13",
794
1143
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
795
1144
  type: "module",
796
1145
  main: "dist/index.js",
@@ -900,6 +1249,86 @@ var TraceShipper2 = class extends TraceShipper {
900
1249
  }
901
1250
  };
902
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
+
903
1332
  // src/tracing-linkage-helpers.ts
904
1333
  function createSessionParentMapHelpers({
905
1334
  sessions: sessions2,
@@ -977,6 +1406,15 @@ function createSessionParentMapHelpers({
977
1406
  }
978
1407
 
979
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
+ }
980
1418
  var sessions = /* @__PURE__ */ new Map();
981
1419
  var taskContexts = /* @__PURE__ */ new Map();
982
1420
  var runningTaskCallsBySession = /* @__PURE__ */ new Map();
@@ -1129,7 +1567,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1129
1567
  const state = sessions.get(partSessionID);
1130
1568
  if (!state) return;
1131
1569
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
1132
- state.outputParts.set(messageId, partObj["text"]);
1570
+ state.outputParts.set(messageId, capText2(partObj["text"]));
1133
1571
  } else if (partObj["type"] === "tool" && messageId) {
1134
1572
  const tool = partObj["tool"];
1135
1573
  const callID = partObj["callID"];
@@ -1159,7 +1597,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1159
1597
  if (typeof errorMessage !== "string" || errorMessage === "")
1160
1598
  throw new Error("message.part.updated: error tool state must have error string");
1161
1599
  const input = toolState["input"];
1162
- 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);
1163
1601
  if (endPendingToolSpanWithError(
1164
1602
  state,
1165
1603
  callID,
@@ -1172,7 +1610,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1172
1610
  }
1173
1611
  }
1174
1612
  } else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
1175
- state.reasoningParts.set(messageId, partObj["text"]);
1613
+ state.reasoningParts.set(messageId, capText2(partObj["text"]));
1176
1614
  }
1177
1615
  } else if (event.type === "message.updated") {
1178
1616
  const msgInfo = props["info"];
@@ -1439,7 +1877,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1439
1877
  sessions.delete(String(errorSessionID));
1440
1878
  }
1441
1879
  } catch (err) {
1442
- console.log(
1880
+ rateLimitedErrorLog(
1881
+ "event",
1443
1882
  `[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
1444
1883
  );
1445
1884
  }
@@ -1495,7 +1934,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1495
1934
  ...promptMeta,
1496
1935
  properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
1497
1936
  };
1498
- const userText = textParts.join("\n");
1937
+ const userText = capText2(textParts.join("\n"));
1499
1938
  state.currentInput = userText;
1500
1939
  if (state.parentId) {
1501
1940
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
@@ -1604,7 +2043,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1604
2043
  });
1605
2044
  }
1606
2045
  } catch (err) {
1607
- console.log(
2046
+ rateLimitedErrorLog(
2047
+ "tool.execute.before",
1608
2048
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
1609
2049
  );
1610
2050
  }
@@ -1619,7 +2059,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1619
2059
  if (!state || !state.currentEventId) return;
1620
2060
  if (args === void 0 || args === null)
1621
2061
  throw new Error("tool.execute.after: args is required");
1622
- const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
2062
+ const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
1623
2063
  const startInfo = state.toolSpanStarts.get(callID);
1624
2064
  state.toolSpanStarts.delete(callID);
1625
2065
  const resultMetadata = result.metadata;
@@ -1632,13 +2072,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1632
2072
  const endTimeUnixNano = nowUnixNanoString();
1633
2073
  if (!("output" in result) || result.output === void 0)
1634
2074
  throw new Error("tool.execute.after: result.output is required");
1635
- let toolResult;
1636
- try {
1637
- toolResult = JSON.stringify(result.output);
1638
- } catch (err) {
1639
- const msg = err instanceof Error ? err.message : String(err);
1640
- throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
1641
- }
2075
+ const toolResult = boundedStringify(result.output);
1642
2076
  if (startInfo.liveSpan) {
1643
2077
  traceShipper.endSpan(startInfo.liveSpan, {
1644
2078
  attributes: [
@@ -1667,7 +2101,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1667
2101
  markTaskCallFinished(sessionID, callID);
1668
2102
  }
1669
2103
  } catch (err) {
1670
- console.log(
2104
+ rateLimitedErrorLog(
2105
+ "tool.execute.after",
1671
2106
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1672
2107
  );
1673
2108
  }
@@ -1684,7 +2119,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1684
2119
  state.isCompacting = true;
1685
2120
  }
1686
2121
  } catch (err) {
1687
- console.log(
2122
+ rateLimitedErrorLog(
2123
+ "experimental.session.compacting",
1688
2124
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1689
2125
  );
1690
2126
  }
@@ -1703,11 +2139,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1703
2139
  sessions.set(sessionID, state);
1704
2140
  }
1705
2141
  const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1706
- const TRUNCATION_MARKER = "\n...[truncated]";
2142
+ const TRUNCATION_MARKER3 = "\n...[truncated]";
1707
2143
  const joined = output.system.join("\n\n");
1708
- 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;
1709
2145
  } catch (err) {
1710
- console.log(
2146
+ rateLimitedErrorLog(
2147
+ "experimental.chat.system.transform",
1711
2148
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1712
2149
  );
1713
2150
  }