@raindrop-ai/claude-code 0.0.13 → 0.0.14

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
@@ -1,4 +1,4 @@
1
- // ../core/dist/chunk-4UCYIEH4.js
1
+ // ../core/dist/chunk-U5HUTMR5.js
2
2
  function getCrypto() {
3
3
  const c = globalThis.crypto;
4
4
  return c;
@@ -42,6 +42,8 @@ function base64Encode(bytes) {
42
42
  }
43
43
  return out;
44
44
  }
45
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
46
+ var MAX_RETRY_DELAY_MS = 3e4;
45
47
  function wait(ms) {
46
48
  return new Promise((resolve) => setTimeout(resolve, ms));
47
49
  }
@@ -49,6 +51,46 @@ function formatEndpoint(endpoint) {
49
51
  if (!endpoint) return void 0;
50
52
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
51
53
  }
54
+ function redactUrlForLog(url) {
55
+ try {
56
+ const parsed = new URL(url);
57
+ parsed.username = "";
58
+ parsed.password = "";
59
+ parsed.search = "";
60
+ parsed.hash = "";
61
+ return parsed.toString();
62
+ } catch (e) {
63
+ return "<unparseable-url>";
64
+ }
65
+ }
66
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
67
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
68
+ function rateLimitedLog(key, log) {
69
+ const now = Date.now();
70
+ const last = rateLimitedLogLast.get(key);
71
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
72
+ return false;
73
+ }
74
+ rateLimitedLogLast.set(key, now);
75
+ log();
76
+ return true;
77
+ }
78
+ async function raceWithTimeout(promise, timeoutMs) {
79
+ let timer;
80
+ const settledInTime = await Promise.race([
81
+ promise.then(
82
+ () => true,
83
+ () => true
84
+ ),
85
+ new Promise((resolve) => {
86
+ var _a;
87
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
88
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
89
+ })
90
+ ]);
91
+ if (timer) clearTimeout(timer);
92
+ return settledInTime;
93
+ }
52
94
  function parseRetryAfter(headers) {
53
95
  var _a;
54
96
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -65,12 +107,12 @@ function parseRetryAfter(headers) {
65
107
  function getRetryDelayMs(attemptNumber, previousError) {
66
108
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
67
109
  const v = previousError.retryAfterMs;
68
- if (typeof v === "number") return Math.max(0, v);
110
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
69
111
  }
70
112
  if (attemptNumber <= 1) return 0;
71
113
  const base = 500;
72
114
  const factor = Math.pow(2, attemptNumber - 2);
73
- return base * factor;
115
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
74
116
  }
75
117
  async function withRetry(operation, opName, opts) {
76
118
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -105,7 +147,9 @@ async function withRetry(operation, opName, opts) {
105
147
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
106
148
  }
107
149
  async function postJson(url, body, headers, opts) {
108
- const opName = `POST ${url}`;
150
+ var _a;
151
+ const opName = `POST ${redactUrlForLog(url)}`;
152
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
109
153
  await withRetry(
110
154
  async () => {
111
155
  const resp = await fetch(url, {
@@ -114,7 +158,8 @@ async function postJson(url, body, headers, opts) {
114
158
  "Content-Type": "application/json",
115
159
  ...headers
116
160
  },
117
- body: JSON.stringify(body)
161
+ body: JSON.stringify(body),
162
+ signal: AbortSignal.timeout(timeoutMs)
118
163
  });
119
164
  if (!resp.ok) {
120
165
  const text = await resp.text().catch(() => "");
@@ -131,6 +176,27 @@ async function postJson(url, body, headers, opts) {
131
176
  opts
132
177
  );
133
178
  }
179
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
180
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
181
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
182
+ function resolveMaxTextFieldChars(value) {
183
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
184
+ return Math.floor(value);
185
+ }
186
+ return currentDefaultMaxTextFieldChars;
187
+ }
188
+ function truncateToLimit(text, limit) {
189
+ if (limit > TRUNCATION_MARKER.length) {
190
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
191
+ }
192
+ return text.slice(0, Math.max(0, limit));
193
+ }
194
+ function capText(value, limit) {
195
+ if (typeof value !== "string") return value;
196
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
197
+ if (value.length <= max) return value;
198
+ return truncateToLimit(value, max);
199
+ }
134
200
  var SpanStatusCode = {
135
201
  UNSET: 0,
136
202
  OK: 1,
@@ -188,6 +254,93 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
188
254
  ]
189
255
  };
190
256
  }
257
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
258
+ var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
259
+ var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
260
+ function readEnvVar(name) {
261
+ var _a;
262
+ try {
263
+ const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
264
+ if (env && typeof env[name] === "string" && env[name].length > 0) {
265
+ return env[name];
266
+ }
267
+ } catch (e) {
268
+ }
269
+ return void 0;
270
+ }
271
+ function readWorkshopEnv() {
272
+ const raw = readEnvVar(WORKSHOP_ENV_VAR);
273
+ if (raw === void 0) return void 0;
274
+ const trimmed = raw.trim();
275
+ if (trimmed.length === 0) return void 0;
276
+ if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
277
+ if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
278
+ if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
279
+ return void 0;
280
+ }
281
+ function isLocalDevHost(hostname) {
282
+ if (!hostname) return false;
283
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
284
+ return true;
285
+ }
286
+ if (hostname.endsWith(".localhost")) return true;
287
+ return false;
288
+ }
289
+ function readRuntimeHostname() {
290
+ try {
291
+ const loc = globalThis == null ? void 0 : globalThis.location;
292
+ if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
293
+ return loc.hostname;
294
+ }
295
+ } catch (e) {
296
+ }
297
+ return void 0;
298
+ }
299
+ function shouldAutoEnableLocalWorkshop() {
300
+ if (isLocalDevHost(readRuntimeHostname())) return true;
301
+ if (readEnvVar("NODE_ENV") === "development") return true;
302
+ return false;
303
+ }
304
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
305
+ var _a, _b, _c;
306
+ if (baseUrl === null) return null;
307
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
308
+ return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
309
+ }
310
+ const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
311
+ if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
312
+ const workshopEnv = readWorkshopEnv();
313
+ if (workshopEnv === "disable") return null;
314
+ if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
315
+ if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
316
+ if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
317
+ return null;
318
+ }
319
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
320
+ var _a;
321
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
322
+ if (!baseUrl) return;
323
+ void postJson(`${baseUrl}traces`, body, {}, {
324
+ maxAttempts: 1,
325
+ debug: (_a = options.debug) != null ? _a : false,
326
+ sdkName: options.sdkName
327
+ }).catch(() => {
328
+ });
329
+ }
330
+ function mirrorPartialEventToLocalDebugger(event, options = {}) {
331
+ var _a;
332
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
333
+ if (!baseUrl) return;
334
+ const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
335
+ void postJson(`${baseUrl}events/track_partial`, event, headers, {
336
+ maxAttempts: 1,
337
+ debug: (_a = options.debug) != null ? _a : false,
338
+ sdkName: options.sdkName
339
+ }).catch(() => {
340
+ });
341
+ }
342
+ var SHUTDOWN_DEADLINE_MS = 1e4;
343
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
191
344
  function mergePatches(target, source) {
192
345
  var _a, _b, _c, _d;
193
346
  const out = { ...target, ...source };
@@ -205,7 +358,8 @@ var EventShipper = class {
205
358
  this.sticky = /* @__PURE__ */ new Map();
206
359
  this.timers = /* @__PURE__ */ new Map();
207
360
  this.inFlight = /* @__PURE__ */ new Set();
208
- var _a, _b, _c, _d, _e, _f, _g;
361
+ this.hasShutdown = false;
362
+ var _a, _b, _c, _d, _e, _f, _g, _h;
209
363
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
210
364
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
211
365
  this.enabled = opts.enabled !== false;
@@ -214,11 +368,16 @@ var EventShipper = class {
214
368
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
215
369
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
216
370
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
371
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
372
+ this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
373
+ if (this.debug && this.localDebuggerUrl) {
374
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
375
+ }
217
376
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
218
377
  this.context = {
219
378
  library: {
220
- name: (_f = opts.libraryName) != null ? _f : "@raindrop-ai/core",
221
- version: (_g = opts.libraryVersion) != null ? _g : "0.0.0"
379
+ name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
380
+ version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
222
381
  },
223
382
  metadata: {
224
383
  jsRuntime: isNode ? "node" : "web",
@@ -232,10 +391,52 @@ var EventShipper = class {
232
391
  authHeaders() {
233
392
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
234
393
  }
394
+ /**
395
+ * Build the retry/timeout options for one POST, honoring the shutdown
396
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
397
+ * the caller must drop the payload (with a rate-limited warning) instead
398
+ * of issuing a request that could outlive process exit.
399
+ *
400
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
401
+ * path is mid-drain takes effect immediately: no further retries, and the
402
+ * per-attempt timeout is clamped to the remaining window. After
403
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
404
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
405
+ * run as a single short attempt rather than regaining the full retry
406
+ * schedule.
407
+ */
408
+ requestOpts() {
409
+ if (this.shutdownDeadlineAt !== void 0) {
410
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
411
+ if (remainingMs <= 0) return null;
412
+ return {
413
+ maxAttempts: 1,
414
+ debug: this.debug,
415
+ sdkName: this.sdkName,
416
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
417
+ };
418
+ }
419
+ if (this.hasShutdown) {
420
+ return {
421
+ maxAttempts: 1,
422
+ debug: this.debug,
423
+ sdkName: this.sdkName,
424
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
425
+ };
426
+ }
427
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
428
+ }
235
429
  async patch(eventId, patch) {
236
430
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
237
431
  if (!this.enabled) return;
238
432
  if (!eventId || !eventId.trim()) return;
433
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
434
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
435
+ patch = { ...patch, input: capText(patch.input, maxChars) };
436
+ }
437
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
438
+ patch = { ...patch, output: capText(patch.output, maxChars) };
439
+ }
239
440
  if (this.debug) {
240
441
  console.log(`${this.prefix} queue patch`, {
241
442
  eventId,
@@ -282,9 +483,16 @@ var EventShipper = class {
282
483
  })));
283
484
  }
284
485
  async shutdown() {
285
- for (const t of this.timers.values()) clearTimeout(t);
286
- this.timers.clear();
287
- await this.flush();
486
+ this.hasShutdown = true;
487
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
488
+ try {
489
+ for (const t of this.timers.values()) clearTimeout(t);
490
+ this.timers.clear();
491
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
492
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
493
+ } finally {
494
+ this.shutdownDeadlineAt = void 0;
495
+ }
288
496
  }
289
497
  async trackSignal(signal) {
290
498
  var _a, _b;
@@ -304,16 +512,21 @@ var EventShipper = class {
304
512
  }
305
513
  }
306
514
  ];
515
+ if (!this.writeKey) return;
307
516
  const url = `${this.baseUrl}signals/track`;
517
+ const opts = this.requestOpts();
518
+ if (!opts) {
519
+ this.warnShutdownDrop("signal");
520
+ return;
521
+ }
308
522
  try {
309
- await postJson(url, body, this.authHeaders(), {
310
- maxAttempts: 3,
311
- debug: this.debug,
312
- sdkName: this.sdkName
313
- });
523
+ await postJson(url, body, this.authHeaders(), opts);
314
524
  } catch (err) {
315
525
  const msg = err instanceof Error ? err.message : String(err);
316
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
526
+ rateLimitedLog(
527
+ `${this.prefix}.send_signal_failed`,
528
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
529
+ );
317
530
  }
318
531
  }
319
532
  async identify(users) {
@@ -334,19 +547,30 @@ var EventShipper = class {
334
547
  traits: (_a = user.traits) != null ? _a : {}
335
548
  };
336
549
  });
550
+ if (!this.writeKey) return;
337
551
  if (body.length === 0) return;
338
552
  const url = `${this.baseUrl}users/identify`;
553
+ const opts = this.requestOpts();
554
+ if (!opts) {
555
+ this.warnShutdownDrop("identify");
556
+ return;
557
+ }
339
558
  try {
340
- await postJson(url, body, this.authHeaders(), {
341
- maxAttempts: 3,
342
- debug: this.debug,
343
- sdkName: this.sdkName
344
- });
559
+ await postJson(url, body, this.authHeaders(), opts);
345
560
  } catch (err) {
346
561
  const msg = err instanceof Error ? err.message : String(err);
347
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
562
+ rateLimitedLog(
563
+ `${this.prefix}.send_identify_failed`,
564
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
565
+ );
348
566
  }
349
567
  }
568
+ warnShutdownDrop(what) {
569
+ rateLimitedLog(
570
+ `${this.prefix}.shutdown_deadline`,
571
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
572
+ );
573
+ }
350
574
  async flushOne(eventId) {
351
575
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
352
576
  if (!this.enabled) return;
@@ -410,11 +634,25 @@ var EventShipper = class {
410
634
  endpoint: url
411
635
  });
412
636
  }
413
- const p = postJson(url, payload, this.authHeaders(), {
414
- maxAttempts: 3,
415
- debug: this.debug,
416
- sdkName: this.sdkName
417
- });
637
+ if (this.localDebuggerUrl) {
638
+ mirrorPartialEventToLocalDebugger(payload, {
639
+ baseUrl: this.localDebuggerUrl,
640
+ writeKey: this.writeKey,
641
+ debug: this.debug,
642
+ sdkName: this.sdkName
643
+ });
644
+ }
645
+ if (!this.writeKey) {
646
+ if (!isPending) this.sticky.delete(eventId);
647
+ return;
648
+ }
649
+ const opts = this.requestOpts();
650
+ if (!opts) {
651
+ this.warnShutdownDrop(`track_partial ${eventId}`);
652
+ if (!isPending) this.sticky.delete(eventId);
653
+ return;
654
+ }
655
+ const p = postJson(url, payload, this.authHeaders(), opts);
418
656
  this.inFlight.add(p);
419
657
  try {
420
658
  try {
@@ -424,7 +662,10 @@ var EventShipper = class {
424
662
  }
425
663
  } catch (err) {
426
664
  const msg = err instanceof Error ? err.message : String(err);
427
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
665
+ rateLimitedLog(
666
+ `${this.prefix}.send_track_partial_failed`,
667
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
668
+ );
428
669
  }
429
670
  } finally {
430
671
  this.inFlight.delete(p);
@@ -434,28 +675,114 @@ var EventShipper = class {
434
675
  }
435
676
  }
436
677
  };
437
- var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
438
- function resolveLocalDebuggerBaseUrl(baseUrl) {
439
- var _a, _b, _c;
440
- 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;
441
- return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
678
+ var DEFAULT_SECRET_KEY_NAMES = [
679
+ "apikey",
680
+ "apisecret",
681
+ "apitoken",
682
+ "secretaccesskey",
683
+ "sessiontoken",
684
+ "privatekey",
685
+ "privatekeyid",
686
+ "clientsecret",
687
+ "accesstoken",
688
+ "refreshtoken",
689
+ "oauthtoken",
690
+ "bearertoken",
691
+ "authorization",
692
+ "password",
693
+ "passphrase"
694
+ ];
695
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
696
+ function normalizeKeyName(name) {
697
+ return name.toLowerCase().replace(/[-_.]/g, "");
698
+ }
699
+ function redactSecretsInObject(value, options) {
700
+ var _a, _b;
701
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
702
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
703
+ const seen = /* @__PURE__ */ new WeakSet();
704
+ const walk = (node) => {
705
+ if (node === null || typeof node !== "object") return node;
706
+ if (seen.has(node)) return "[CIRCULAR]";
707
+ seen.add(node);
708
+ if (Array.isArray(node)) {
709
+ return node.map((item) => walk(item));
710
+ }
711
+ const out = {};
712
+ for (const [k, v] of Object.entries(node)) {
713
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
714
+ out[k] = placeholder;
715
+ } else {
716
+ out[k] = walk(v);
717
+ }
718
+ }
719
+ return out;
720
+ };
721
+ return walk(value);
722
+ }
723
+ function buildSecretSet(names) {
724
+ const set = /* @__PURE__ */ new Set();
725
+ for (const name of names) set.add(normalizeKeyName(name));
726
+ return set;
727
+ }
728
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
729
+ "ai.request.providerOptions",
730
+ "ai.response.providerMetadata"
731
+ ];
732
+ function defaultTransformSpan(span) {
733
+ const attrs = span.attributes;
734
+ if (!attrs || attrs.length === 0) return span;
735
+ let nextAttrs;
736
+ for (let i = 0; i < attrs.length; i++) {
737
+ const attr = attrs[i];
738
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
739
+ if (redacted === void 0) continue;
740
+ if (!nextAttrs) nextAttrs = attrs.slice();
741
+ nextAttrs[i] = { key: attr.key, value: redacted };
742
+ }
743
+ if (!nextAttrs) return span;
744
+ return { ...span, attributes: nextAttrs };
745
+ }
746
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
747
+ function redactJsonAttributeValue(key, value) {
748
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
749
+ const json = value.stringValue;
750
+ if (typeof json !== "string" || json.length === 0) return void 0;
751
+ let parsed;
752
+ try {
753
+ parsed = JSON.parse(json);
754
+ } catch (e) {
755
+ return void 0;
756
+ }
757
+ const scrubbed = redactSecretsInObject(parsed);
758
+ let scrubbedJson;
759
+ try {
760
+ scrubbedJson = JSON.stringify(scrubbed);
761
+ } catch (e) {
762
+ return void 0;
763
+ }
764
+ if (scrubbedJson === json) return void 0;
765
+ return { stringValue: scrubbedJson };
442
766
  }
443
- function mirrorTraceExportToLocalDebugger(body, options = {}) {
444
- var _a;
445
- const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
446
- if (!baseUrl) return;
447
- void postJson(`${baseUrl}traces`, body, {}, {
448
- maxAttempts: 1,
449
- debug: (_a = options.debug) != null ? _a : false,
450
- sdkName: options.sdkName
451
- }).catch(() => {
452
- });
767
+ function applyOtelSpanAttributeLimit(limit) {
768
+ var _a, _b;
769
+ try {
770
+ 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;
771
+ if (!raw) return limit;
772
+ const parsed = Number.parseInt(raw, 10);
773
+ if (Number.isFinite(parsed) && parsed > 0) {
774
+ return Math.min(limit, parsed);
775
+ }
776
+ } catch (e) {
777
+ }
778
+ return limit;
453
779
  }
454
780
  var TraceShipper = class {
455
781
  constructor(opts) {
456
782
  this.queue = [];
457
783
  this.inFlight = /* @__PURE__ */ new Set();
458
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
784
+ this.hasShutdown = false;
785
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
459
786
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
460
787
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
461
788
  this.enabled = opts.enabled !== false;
@@ -468,13 +795,79 @@ var TraceShipper = class {
468
795
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
469
796
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
470
797
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
471
- const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
472
- if (localDebugger) {
473
- this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
474
- if (this.debug) {
475
- console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
798
+ this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
799
+ if (this.debug && this.localDebuggerUrl) {
800
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
801
+ }
802
+ this.transformSpanHook = opts.transformSpan;
803
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
804
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
805
+ }
806
+ /**
807
+ * Cap every string attribute value on the span. O(#attributes) length
808
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
809
+ * pipeline so the default secret-scrub still sees parseable JSON in
810
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
811
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
812
+ * in the surviving prefix).
813
+ *
814
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
815
+ * for span content, matching the Python SDK and the OTel SDK convention.
816
+ */
817
+ capSpanAttributes(span) {
818
+ var _a;
819
+ const maxChars = applyOtelSpanAttributeLimit(
820
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
821
+ );
822
+ const attrs = span.attributes;
823
+ if (!attrs || attrs.length === 0) return span;
824
+ let nextAttrs;
825
+ for (let i = 0; i < attrs.length; i++) {
826
+ const attr = attrs[i];
827
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
828
+ if (typeof value !== "string" || value.length <= maxChars) continue;
829
+ if (!nextAttrs) nextAttrs = attrs.slice();
830
+ nextAttrs[i] = {
831
+ key: attr.key,
832
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
833
+ };
834
+ }
835
+ if (!nextAttrs) return span;
836
+ return { ...span, attributes: nextAttrs };
837
+ }
838
+ /**
839
+ * Apply the user `transformSpan` hook (if any) followed by the default
840
+ * redactor (unless disabled). Returns either the (possibly new) span to
841
+ * ship, or `null` to drop the span entirely.
842
+ *
843
+ * Ordering: user hook runs first so callers can rewrite the span freely
844
+ * (rename attrs, add new ones, scrub things the default doesn't know
845
+ * about). The default redactor then runs on whatever the user produced,
846
+ * acting as the always-on floor for documented BYOK secrets. If the user
847
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
848
+ *
849
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
850
+ * hook can never accidentally ship raw, un-redacted spans.
851
+ */
852
+ redactSpan(span) {
853
+ let current = span;
854
+ if (this.transformSpanHook) {
855
+ try {
856
+ const result = this.transformSpanHook(current);
857
+ if (result === null) return null;
858
+ if (result !== void 0) current = result;
859
+ } catch (err) {
860
+ if (this.debug) {
861
+ const msg = err instanceof Error ? err.message : String(err);
862
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
863
+ }
864
+ return null;
476
865
  }
477
866
  }
867
+ if (!this.disableDefaultRedaction) {
868
+ current = defaultTransformSpan(current);
869
+ }
870
+ return this.capSpanAttributes(current);
478
871
  }
479
872
  isDebugEnabled() {
480
873
  return this.debug;
@@ -492,8 +885,8 @@ var TraceShipper = class {
492
885
  ];
493
886
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
494
887
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
495
- if (this.localDebuggerUrl) {
496
- const openSpan = buildOtlpSpan({
888
+ this.mirrorToLocalDebugger(
889
+ buildOtlpSpan({
497
890
  ids: span.ids,
498
891
  name: span.name,
499
892
  startTimeUnixNano: span.startTimeUnixNano,
@@ -501,16 +894,21 @@ var TraceShipper = class {
501
894
  // placeholder — will be updated on endSpan
502
895
  attributes: span.attributes,
503
896
  status: { code: SpanStatusCode.UNSET }
504
- });
505
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
506
- mirrorTraceExportToLocalDebugger(body, {
507
- baseUrl: this.localDebuggerUrl,
508
- debug: false,
509
- sdkName: this.sdkName
510
- });
511
- }
897
+ })
898
+ );
512
899
  return span;
513
900
  }
901
+ mirrorToLocalDebugger(span) {
902
+ if (!this.localDebuggerUrl) return;
903
+ const redacted = this.redactSpan(span);
904
+ if (redacted === null) return;
905
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
906
+ mirrorTraceExportToLocalDebugger(body, {
907
+ baseUrl: this.localDebuggerUrl,
908
+ debug: false,
909
+ sdkName: this.sdkName
910
+ });
911
+ }
514
912
  endSpan(span, extra) {
515
913
  var _a, _b;
516
914
  if (span.endTimeUnixNano) return;
@@ -532,14 +930,7 @@ var TraceShipper = class {
532
930
  status
533
931
  });
534
932
  this.enqueue(otlp);
535
- if (this.localDebuggerUrl) {
536
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
537
- mirrorTraceExportToLocalDebugger(body, {
538
- baseUrl: this.localDebuggerUrl,
539
- debug: false,
540
- sdkName: this.sdkName
541
- });
542
- }
933
+ this.mirrorToLocalDebugger(otlp);
543
934
  }
544
935
  createSpan(args) {
545
936
  var _a;
@@ -557,14 +948,7 @@ var TraceShipper = class {
557
948
  status: args.status
558
949
  });
559
950
  this.enqueue(otlp);
560
- if (this.localDebuggerUrl) {
561
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
562
- mirrorTraceExportToLocalDebugger(body, {
563
- baseUrl: this.localDebuggerUrl,
564
- debug: false,
565
- sdkName: this.sdkName
566
- });
567
- }
951
+ this.mirrorToLocalDebugger(otlp);
568
952
  }
569
953
  enqueue(span) {
570
954
  if (!this.enabled) return;
@@ -576,10 +960,12 @@ var TraceShipper = class {
576
960
  )}`
577
961
  );
578
962
  }
963
+ const redacted = this.redactSpan(span);
964
+ if (redacted === null) return;
579
965
  if (this.queue.length >= this.maxQueueSize) {
580
966
  this.queue.shift();
581
967
  }
582
- this.queue.push(span);
968
+ this.queue.push(redacted);
583
969
  if (this.queue.length >= this.maxBatchSize) {
584
970
  void this.flush().catch(() => {
585
971
  });
@@ -601,6 +987,17 @@ var TraceShipper = class {
601
987
  }
602
988
  while (this.queue.length > 0) {
603
989
  const batch = this.queue.splice(0, this.maxBatchSize);
990
+ if (!this.writeKey) continue;
991
+ const opts = this.requestOpts();
992
+ if (!opts) {
993
+ rateLimitedLog(
994
+ `${this.prefix}.shutdown_deadline`,
995
+ () => console.warn(
996
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
997
+ )
998
+ );
999
+ continue;
1000
+ }
604
1001
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
605
1002
  const url = `${this.baseUrl}traces`;
606
1003
  if (this.debug) {
@@ -609,11 +1006,7 @@ var TraceShipper = class {
609
1006
  endpoint: url
610
1007
  });
611
1008
  }
612
- const p = postJson(url, body, this.authHeaders(), {
613
- maxAttempts: 3,
614
- debug: this.debug,
615
- sdkName: this.sdkName
616
- });
1009
+ const p = postJson(url, body, this.authHeaders(), opts);
617
1010
  this.inFlight.add(p);
618
1011
  try {
619
1012
  try {
@@ -621,21 +1014,61 @@ var TraceShipper = class {
621
1014
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
622
1015
  } catch (err) {
623
1016
  const msg = err instanceof Error ? err.message : String(err);
624
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1017
+ rateLimitedLog(
1018
+ `${this.prefix}.send_spans_failed`,
1019
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1020
+ );
625
1021
  }
626
1022
  } finally {
627
1023
  this.inFlight.delete(p);
628
1024
  }
629
1025
  }
630
1026
  }
1027
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1028
+ requestOpts() {
1029
+ if (this.shutdownDeadlineAt !== void 0) {
1030
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1031
+ if (remainingMs <= 0) return null;
1032
+ return {
1033
+ maxAttempts: 1,
1034
+ debug: this.debug,
1035
+ sdkName: this.sdkName,
1036
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1037
+ };
1038
+ }
1039
+ if (this.hasShutdown) {
1040
+ return {
1041
+ maxAttempts: 1,
1042
+ debug: this.debug,
1043
+ sdkName: this.sdkName,
1044
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1045
+ };
1046
+ }
1047
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1048
+ }
631
1049
  async shutdown() {
632
- if (this.timer) {
633
- clearTimeout(this.timer);
634
- this.timer = void 0;
1050
+ this.hasShutdown = true;
1051
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1052
+ try {
1053
+ if (this.timer) {
1054
+ clearTimeout(this.timer);
1055
+ this.timer = void 0;
1056
+ }
1057
+ const drain = async () => {
1058
+ await this.flush();
1059
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1060
+ })));
1061
+ };
1062
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1063
+ if (!settled) {
1064
+ rateLimitedLog(
1065
+ `${this.prefix}.shutdown_deadline`,
1066
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1067
+ );
1068
+ }
1069
+ } finally {
1070
+ this.shutdownDeadlineAt = void 0;
635
1071
  }
636
- await this.flush();
637
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
638
- })));
639
1072
  }
640
1073
  };
641
1074
 
@@ -984,18 +1417,85 @@ function transcriptToProperties(summary) {
984
1417
 
985
1418
  // src/event-mapper.ts
986
1419
  var MAX_ATTR_LENGTH = 32768;
1420
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1421
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1422
+ var MAX_BOUNDED_DEPTH = 12;
987
1423
  function truncate(value) {
988
1424
  if (value === void 0) return void 0;
989
1425
  if (value.length <= MAX_ATTR_LENGTH) return value;
990
1426
  const suffix = "\n...[truncated]";
991
1427
  return value.slice(0, MAX_ATTR_LENGTH - suffix.length) + suffix;
992
1428
  }
1429
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1430
+ if (value.length <= limit) return value;
1431
+ if (limit > TRUNCATION_MARKER2.length) {
1432
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1433
+ }
1434
+ return value.slice(0, limit);
1435
+ }
1436
+ function boundedClone2(value, budget, depth) {
1437
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1438
+ if (typeof value === "string") {
1439
+ if (value.length > budget.remaining) {
1440
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1441
+ budget.remaining = 0;
1442
+ return taken;
1443
+ }
1444
+ budget.remaining -= Math.max(value.length, 1);
1445
+ return value;
1446
+ }
1447
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1448
+ budget.remaining -= 8;
1449
+ return value;
1450
+ }
1451
+ if (typeof value !== "object") {
1452
+ budget.remaining -= 8;
1453
+ return value;
1454
+ }
1455
+ if (depth >= MAX_BOUNDED_DEPTH) {
1456
+ budget.remaining -= 16;
1457
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1458
+ }
1459
+ const withToJson = value;
1460
+ if (typeof withToJson.toJSON === "function") {
1461
+ try {
1462
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1463
+ } catch (e) {
1464
+ }
1465
+ }
1466
+ if (Array.isArray(value)) {
1467
+ const out2 = [];
1468
+ for (const item of value) {
1469
+ if (budget.remaining <= 0) {
1470
+ out2.push(TRUNCATION_MARKER2);
1471
+ break;
1472
+ }
1473
+ out2.push(boundedClone2(item, budget, depth + 1));
1474
+ }
1475
+ return out2;
1476
+ }
1477
+ const out = {};
1478
+ for (const key of Object.keys(value)) {
1479
+ if (budget.remaining <= 0) {
1480
+ out["..."] = TRUNCATION_MARKER2;
1481
+ break;
1482
+ }
1483
+ budget.remaining -= Math.max(key.length, 1);
1484
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1485
+ }
1486
+ return out;
1487
+ }
993
1488
  function safeStringify(value) {
994
1489
  if (value === void 0 || value === null) return void 0;
995
1490
  try {
996
- return truncate(JSON.stringify(value));
1491
+ const pruned = boundedClone2(
1492
+ value,
1493
+ { remaining: MAX_ATTR_LENGTH + TRUNCATION_MARKER2.length + 256 },
1494
+ 0
1495
+ );
1496
+ return truncate(JSON.stringify(pruned));
997
1497
  } catch (e) {
998
- return truncate(String(value));
1498
+ return truncate(capText2(String(value), MAX_ATTR_LENGTH + 16));
999
1499
  }
1000
1500
  }
1001
1501
  var STATE_DIR = join2(tmpdir(), "raindrop-claude-code");
@@ -1176,7 +1676,9 @@ async function handleUserPromptSubmit(payload, convoId, config, properties, even
1176
1676
  userId: config.userId,
1177
1677
  convoId,
1178
1678
  eventName: config.eventName,
1179
- input: payload.prompt,
1679
+ // Capped so a pasted multi-MB prompt can't produce an event that the
1680
+ // ingest API rejects (silent data loss) — it lands truncated instead.
1681
+ input: payload.prompt !== void 0 ? capText2(payload.prompt) : void 0,
1180
1682
  model,
1181
1683
  properties
1182
1684
  };
@@ -1299,7 +1801,7 @@ async function handlePostCompact(payload, eventId, convoId, config, properties,
1299
1801
  properties: {
1300
1802
  ...properties,
1301
1803
  compaction_trigger: payload.trigger,
1302
- compact_summary: payload.compact_summary
1804
+ compact_summary: payload.compact_summary !== void 0 ? capText2(payload.compact_summary) : void 0
1303
1805
  }
1304
1806
  });
1305
1807
  }
@@ -1534,7 +2036,7 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1534
2036
  attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1535
2037
  attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1536
2038
  ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1537
- attrString("ai.response.text", outputText),
2039
+ attrString("ai.response.text", truncate(outputText)),
1538
2040
  ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1539
2041
  ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1540
2042
  attrInt("ai.llm_call.index", emittedPhaseIndex)
@@ -1594,7 +2096,7 @@ async function handleStopOrFailure(payload, eventId, convoId, config, properties
1594
2096
  userId: config.userId,
1595
2097
  convoId,
1596
2098
  eventName: config.eventName,
1597
- output,
2099
+ output: output !== void 0 ? capText2(output) : void 0,
1598
2100
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1599
2101
  properties: {
1600
2102
  ...properties,
@@ -1625,6 +2127,7 @@ import { join as join3 } from "path";
1625
2127
  var DEFAULT_PORT = 5899;
1626
2128
  var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1627
2129
  var HEALTH_TIMEOUT_MS = 300;
2130
+ var MIRROR_TIMEOUT_MS = 2e3;
1628
2131
  var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
1629
2132
  var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
1630
2133
  var CACHE_TTL_MS = 5e3;
@@ -1648,10 +2151,8 @@ function writeCache(url) {
1648
2151
  async function healthPing(baseUrl) {
1649
2152
  const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1650
2153
  try {
1651
- const controller = new AbortController();
1652
- const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1653
- const resp = await fetch(healthUrl, { signal: controller.signal });
1654
- clearTimeout(timeout);
2154
+ const signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
2155
+ const resp = await fetch(healthUrl, { signal });
1655
2156
  if (resp.ok) {
1656
2157
  const body = await resp.json();
1657
2158
  if (body.ok) return baseUrl;
@@ -1694,7 +2195,8 @@ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1694
2195
  void fetch(url, {
1695
2196
  method: "POST",
1696
2197
  headers: { "Content-Type": "application/json" },
1697
- body: JSON.stringify(payload)
2198
+ body: JSON.stringify(payload),
2199
+ signal: AbortSignal.timeout(MIRROR_TIMEOUT_MS)
1698
2200
  }).catch(() => {
1699
2201
  });
1700
2202
  }