@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/cli.js CHANGED
@@ -7,7 +7,7 @@ import { randomUUID as randomUUID2 } from "crypto";
7
7
  import { tmpdir } from "os";
8
8
  import { join } from "path";
9
9
 
10
- // ../core/dist/chunk-4UCYIEH4.js
10
+ // ../core/dist/chunk-U5HUTMR5.js
11
11
  function getCrypto() {
12
12
  const c = globalThis.crypto;
13
13
  return c;
@@ -51,6 +51,8 @@ function base64Encode(bytes) {
51
51
  }
52
52
  return out;
53
53
  }
54
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
55
+ var MAX_RETRY_DELAY_MS = 3e4;
54
56
  function wait(ms) {
55
57
  return new Promise((resolve2) => setTimeout(resolve2, ms));
56
58
  }
@@ -58,6 +60,46 @@ function formatEndpoint(endpoint) {
58
60
  if (!endpoint) return void 0;
59
61
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
60
62
  }
63
+ function redactUrlForLog(url) {
64
+ try {
65
+ const parsed = new URL(url);
66
+ parsed.username = "";
67
+ parsed.password = "";
68
+ parsed.search = "";
69
+ parsed.hash = "";
70
+ return parsed.toString();
71
+ } catch (e) {
72
+ return "<unparseable-url>";
73
+ }
74
+ }
75
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
76
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
77
+ function rateLimitedLog(key, log) {
78
+ const now = Date.now();
79
+ const last = rateLimitedLogLast.get(key);
80
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
81
+ return false;
82
+ }
83
+ rateLimitedLogLast.set(key, now);
84
+ log();
85
+ return true;
86
+ }
87
+ async function raceWithTimeout(promise, timeoutMs) {
88
+ let timer;
89
+ const settledInTime = await Promise.race([
90
+ promise.then(
91
+ () => true,
92
+ () => true
93
+ ),
94
+ new Promise((resolve2) => {
95
+ var _a;
96
+ timer = setTimeout(() => resolve2(false), Math.max(0, timeoutMs));
97
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
98
+ })
99
+ ]);
100
+ if (timer) clearTimeout(timer);
101
+ return settledInTime;
102
+ }
61
103
  function parseRetryAfter(headers) {
62
104
  var _a;
63
105
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -74,12 +116,12 @@ function parseRetryAfter(headers) {
74
116
  function getRetryDelayMs(attemptNumber, previousError) {
75
117
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
76
118
  const v = previousError.retryAfterMs;
77
- if (typeof v === "number") return Math.max(0, v);
119
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
78
120
  }
79
121
  if (attemptNumber <= 1) return 0;
80
122
  const base = 500;
81
123
  const factor = Math.pow(2, attemptNumber - 2);
82
- return base * factor;
124
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
83
125
  }
84
126
  async function withRetry(operation, opName, opts) {
85
127
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -114,7 +156,9 @@ async function withRetry(operation, opName, opts) {
114
156
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
115
157
  }
116
158
  async function postJson(url, body, headers, opts) {
117
- const opName = `POST ${url}`;
159
+ var _a;
160
+ const opName = `POST ${redactUrlForLog(url)}`;
161
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
118
162
  await withRetry(
119
163
  async () => {
120
164
  const resp = await fetch(url, {
@@ -123,7 +167,8 @@ async function postJson(url, body, headers, opts) {
123
167
  "Content-Type": "application/json",
124
168
  ...headers
125
169
  },
126
- body: JSON.stringify(body)
170
+ body: JSON.stringify(body),
171
+ signal: AbortSignal.timeout(timeoutMs)
127
172
  });
128
173
  if (!resp.ok) {
129
174
  const text = await resp.text().catch(() => "");
@@ -140,6 +185,27 @@ async function postJson(url, body, headers, opts) {
140
185
  opts
141
186
  );
142
187
  }
188
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
189
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
190
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
191
+ function resolveMaxTextFieldChars(value) {
192
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
193
+ return Math.floor(value);
194
+ }
195
+ return currentDefaultMaxTextFieldChars;
196
+ }
197
+ function truncateToLimit(text, limit) {
198
+ if (limit > TRUNCATION_MARKER.length) {
199
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
200
+ }
201
+ return text.slice(0, Math.max(0, limit));
202
+ }
203
+ function capText(value, limit) {
204
+ if (typeof value !== "string") return value;
205
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
206
+ if (value.length <= max) return value;
207
+ return truncateToLimit(value, max);
208
+ }
143
209
  var SpanStatusCode = {
144
210
  UNSET: 0,
145
211
  OK: 1,
@@ -197,6 +263,93 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
197
263
  ]
198
264
  };
199
265
  }
266
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
267
+ var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
268
+ var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
269
+ function readEnvVar(name) {
270
+ var _a;
271
+ try {
272
+ const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
273
+ if (env && typeof env[name] === "string" && env[name].length > 0) {
274
+ return env[name];
275
+ }
276
+ } catch (e) {
277
+ }
278
+ return void 0;
279
+ }
280
+ function readWorkshopEnv() {
281
+ const raw = readEnvVar(WORKSHOP_ENV_VAR);
282
+ if (raw === void 0) return void 0;
283
+ const trimmed = raw.trim();
284
+ if (trimmed.length === 0) return void 0;
285
+ if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
286
+ if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
287
+ if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
288
+ return void 0;
289
+ }
290
+ function isLocalDevHost(hostname) {
291
+ if (!hostname) return false;
292
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
293
+ return true;
294
+ }
295
+ if (hostname.endsWith(".localhost")) return true;
296
+ return false;
297
+ }
298
+ function readRuntimeHostname() {
299
+ try {
300
+ const loc = globalThis == null ? void 0 : globalThis.location;
301
+ if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
302
+ return loc.hostname;
303
+ }
304
+ } catch (e) {
305
+ }
306
+ return void 0;
307
+ }
308
+ function shouldAutoEnableLocalWorkshop() {
309
+ if (isLocalDevHost(readRuntimeHostname())) return true;
310
+ if (readEnvVar("NODE_ENV") === "development") return true;
311
+ return false;
312
+ }
313
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
314
+ var _a, _b, _c;
315
+ if (baseUrl === null) return null;
316
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
317
+ return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
318
+ }
319
+ const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
320
+ if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
321
+ const workshopEnv = readWorkshopEnv();
322
+ if (workshopEnv === "disable") return null;
323
+ if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
324
+ if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
325
+ if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
326
+ return null;
327
+ }
328
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
329
+ var _a;
330
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
331
+ if (!baseUrl) return;
332
+ void postJson(`${baseUrl}traces`, body, {}, {
333
+ maxAttempts: 1,
334
+ debug: (_a = options.debug) != null ? _a : false,
335
+ sdkName: options.sdkName
336
+ }).catch(() => {
337
+ });
338
+ }
339
+ function mirrorPartialEventToLocalDebugger(event, options = {}) {
340
+ var _a;
341
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
342
+ if (!baseUrl) return;
343
+ const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
344
+ void postJson(`${baseUrl}events/track_partial`, event, headers, {
345
+ maxAttempts: 1,
346
+ debug: (_a = options.debug) != null ? _a : false,
347
+ sdkName: options.sdkName
348
+ }).catch(() => {
349
+ });
350
+ }
351
+ var SHUTDOWN_DEADLINE_MS = 1e4;
352
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
200
353
  function mergePatches(target, source) {
201
354
  var _a, _b, _c, _d;
202
355
  const out = { ...target, ...source };
@@ -214,7 +367,8 @@ var EventShipper = class {
214
367
  this.sticky = /* @__PURE__ */ new Map();
215
368
  this.timers = /* @__PURE__ */ new Map();
216
369
  this.inFlight = /* @__PURE__ */ new Set();
217
- var _a, _b, _c, _d, _e, _f, _g;
370
+ this.hasShutdown = false;
371
+ var _a, _b, _c, _d, _e, _f, _g, _h;
218
372
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
219
373
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
220
374
  this.enabled = opts.enabled !== false;
@@ -223,11 +377,16 @@ var EventShipper = class {
223
377
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
224
378
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
225
379
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
380
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
381
+ this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
382
+ if (this.debug && this.localDebuggerUrl) {
383
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
384
+ }
226
385
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
227
386
  this.context = {
228
387
  library: {
229
- name: (_f = opts.libraryName) != null ? _f : "@raindrop-ai/core",
230
- version: (_g = opts.libraryVersion) != null ? _g : "0.0.0"
388
+ name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
389
+ version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
231
390
  },
232
391
  metadata: {
233
392
  jsRuntime: isNode ? "node" : "web",
@@ -241,10 +400,52 @@ var EventShipper = class {
241
400
  authHeaders() {
242
401
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
243
402
  }
403
+ /**
404
+ * Build the retry/timeout options for one POST, honoring the shutdown
405
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
406
+ * the caller must drop the payload (with a rate-limited warning) instead
407
+ * of issuing a request that could outlive process exit.
408
+ *
409
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
410
+ * path is mid-drain takes effect immediately: no further retries, and the
411
+ * per-attempt timeout is clamped to the remaining window. After
412
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
413
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
414
+ * run as a single short attempt rather than regaining the full retry
415
+ * schedule.
416
+ */
417
+ requestOpts() {
418
+ if (this.shutdownDeadlineAt !== void 0) {
419
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
420
+ if (remainingMs <= 0) return null;
421
+ return {
422
+ maxAttempts: 1,
423
+ debug: this.debug,
424
+ sdkName: this.sdkName,
425
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
426
+ };
427
+ }
428
+ if (this.hasShutdown) {
429
+ return {
430
+ maxAttempts: 1,
431
+ debug: this.debug,
432
+ sdkName: this.sdkName,
433
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
434
+ };
435
+ }
436
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
437
+ }
244
438
  async patch(eventId, patch) {
245
439
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
246
440
  if (!this.enabled) return;
247
441
  if (!eventId || !eventId.trim()) return;
442
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
443
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
444
+ patch = { ...patch, input: capText(patch.input, maxChars) };
445
+ }
446
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
447
+ patch = { ...patch, output: capText(patch.output, maxChars) };
448
+ }
248
449
  if (this.debug) {
249
450
  console.log(`${this.prefix} queue patch`, {
250
451
  eventId,
@@ -291,9 +492,16 @@ var EventShipper = class {
291
492
  })));
292
493
  }
293
494
  async shutdown() {
294
- for (const t of this.timers.values()) clearTimeout(t);
295
- this.timers.clear();
296
- await this.flush();
495
+ this.hasShutdown = true;
496
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
497
+ try {
498
+ for (const t of this.timers.values()) clearTimeout(t);
499
+ this.timers.clear();
500
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
501
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
502
+ } finally {
503
+ this.shutdownDeadlineAt = void 0;
504
+ }
297
505
  }
298
506
  async trackSignal(signal) {
299
507
  var _a, _b;
@@ -313,16 +521,21 @@ var EventShipper = class {
313
521
  }
314
522
  }
315
523
  ];
524
+ if (!this.writeKey) return;
316
525
  const url = `${this.baseUrl}signals/track`;
526
+ const opts = this.requestOpts();
527
+ if (!opts) {
528
+ this.warnShutdownDrop("signal");
529
+ return;
530
+ }
317
531
  try {
318
- await postJson(url, body, this.authHeaders(), {
319
- maxAttempts: 3,
320
- debug: this.debug,
321
- sdkName: this.sdkName
322
- });
532
+ await postJson(url, body, this.authHeaders(), opts);
323
533
  } catch (err) {
324
534
  const msg = err instanceof Error ? err.message : String(err);
325
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
535
+ rateLimitedLog(
536
+ `${this.prefix}.send_signal_failed`,
537
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
538
+ );
326
539
  }
327
540
  }
328
541
  async identify(users) {
@@ -343,19 +556,30 @@ var EventShipper = class {
343
556
  traits: (_a = user.traits) != null ? _a : {}
344
557
  };
345
558
  });
559
+ if (!this.writeKey) return;
346
560
  if (body.length === 0) return;
347
561
  const url = `${this.baseUrl}users/identify`;
562
+ const opts = this.requestOpts();
563
+ if (!opts) {
564
+ this.warnShutdownDrop("identify");
565
+ return;
566
+ }
348
567
  try {
349
- await postJson(url, body, this.authHeaders(), {
350
- maxAttempts: 3,
351
- debug: this.debug,
352
- sdkName: this.sdkName
353
- });
568
+ await postJson(url, body, this.authHeaders(), opts);
354
569
  } catch (err) {
355
570
  const msg = err instanceof Error ? err.message : String(err);
356
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
571
+ rateLimitedLog(
572
+ `${this.prefix}.send_identify_failed`,
573
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
574
+ );
357
575
  }
358
576
  }
577
+ warnShutdownDrop(what) {
578
+ rateLimitedLog(
579
+ `${this.prefix}.shutdown_deadline`,
580
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
581
+ );
582
+ }
359
583
  async flushOne(eventId) {
360
584
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
361
585
  if (!this.enabled) return;
@@ -419,11 +643,25 @@ var EventShipper = class {
419
643
  endpoint: url
420
644
  });
421
645
  }
422
- const p = postJson(url, payload, this.authHeaders(), {
423
- maxAttempts: 3,
424
- debug: this.debug,
425
- sdkName: this.sdkName
426
- });
646
+ if (this.localDebuggerUrl) {
647
+ mirrorPartialEventToLocalDebugger(payload, {
648
+ baseUrl: this.localDebuggerUrl,
649
+ writeKey: this.writeKey,
650
+ debug: this.debug,
651
+ sdkName: this.sdkName
652
+ });
653
+ }
654
+ if (!this.writeKey) {
655
+ if (!isPending) this.sticky.delete(eventId);
656
+ return;
657
+ }
658
+ const opts = this.requestOpts();
659
+ if (!opts) {
660
+ this.warnShutdownDrop(`track_partial ${eventId}`);
661
+ if (!isPending) this.sticky.delete(eventId);
662
+ return;
663
+ }
664
+ const p = postJson(url, payload, this.authHeaders(), opts);
427
665
  this.inFlight.add(p);
428
666
  try {
429
667
  try {
@@ -433,7 +671,10 @@ var EventShipper = class {
433
671
  }
434
672
  } catch (err) {
435
673
  const msg = err instanceof Error ? err.message : String(err);
436
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
674
+ rateLimitedLog(
675
+ `${this.prefix}.send_track_partial_failed`,
676
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
677
+ );
437
678
  }
438
679
  } finally {
439
680
  this.inFlight.delete(p);
@@ -443,28 +684,114 @@ var EventShipper = class {
443
684
  }
444
685
  }
445
686
  };
446
- var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
447
- function resolveLocalDebuggerBaseUrl(baseUrl) {
448
- var _a, _b, _c;
449
- 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;
450
- return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
687
+ var DEFAULT_SECRET_KEY_NAMES = [
688
+ "apikey",
689
+ "apisecret",
690
+ "apitoken",
691
+ "secretaccesskey",
692
+ "sessiontoken",
693
+ "privatekey",
694
+ "privatekeyid",
695
+ "clientsecret",
696
+ "accesstoken",
697
+ "refreshtoken",
698
+ "oauthtoken",
699
+ "bearertoken",
700
+ "authorization",
701
+ "password",
702
+ "passphrase"
703
+ ];
704
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
705
+ function normalizeKeyName(name) {
706
+ return name.toLowerCase().replace(/[-_.]/g, "");
451
707
  }
452
- function mirrorTraceExportToLocalDebugger(body, options = {}) {
453
- var _a;
454
- const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
455
- if (!baseUrl) return;
456
- void postJson(`${baseUrl}traces`, body, {}, {
457
- maxAttempts: 1,
458
- debug: (_a = options.debug) != null ? _a : false,
459
- sdkName: options.sdkName
460
- }).catch(() => {
461
- });
708
+ function redactSecretsInObject(value, options) {
709
+ var _a, _b;
710
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
711
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
712
+ const seen = /* @__PURE__ */ new WeakSet();
713
+ const walk = (node) => {
714
+ if (node === null || typeof node !== "object") return node;
715
+ if (seen.has(node)) return "[CIRCULAR]";
716
+ seen.add(node);
717
+ if (Array.isArray(node)) {
718
+ return node.map((item) => walk(item));
719
+ }
720
+ const out = {};
721
+ for (const [k, v] of Object.entries(node)) {
722
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
723
+ out[k] = placeholder;
724
+ } else {
725
+ out[k] = walk(v);
726
+ }
727
+ }
728
+ return out;
729
+ };
730
+ return walk(value);
731
+ }
732
+ function buildSecretSet(names) {
733
+ const set = /* @__PURE__ */ new Set();
734
+ for (const name of names) set.add(normalizeKeyName(name));
735
+ return set;
736
+ }
737
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
738
+ "ai.request.providerOptions",
739
+ "ai.response.providerMetadata"
740
+ ];
741
+ function defaultTransformSpan(span) {
742
+ const attrs = span.attributes;
743
+ if (!attrs || attrs.length === 0) return span;
744
+ let nextAttrs;
745
+ for (let i = 0; i < attrs.length; i++) {
746
+ const attr = attrs[i];
747
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
748
+ if (redacted === void 0) continue;
749
+ if (!nextAttrs) nextAttrs = attrs.slice();
750
+ nextAttrs[i] = { key: attr.key, value: redacted };
751
+ }
752
+ if (!nextAttrs) return span;
753
+ return { ...span, attributes: nextAttrs };
754
+ }
755
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
756
+ function redactJsonAttributeValue(key, value) {
757
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
758
+ const json = value.stringValue;
759
+ if (typeof json !== "string" || json.length === 0) return void 0;
760
+ let parsed;
761
+ try {
762
+ parsed = JSON.parse(json);
763
+ } catch (e) {
764
+ return void 0;
765
+ }
766
+ const scrubbed = redactSecretsInObject(parsed);
767
+ let scrubbedJson;
768
+ try {
769
+ scrubbedJson = JSON.stringify(scrubbed);
770
+ } catch (e) {
771
+ return void 0;
772
+ }
773
+ if (scrubbedJson === json) return void 0;
774
+ return { stringValue: scrubbedJson };
775
+ }
776
+ function applyOtelSpanAttributeLimit(limit) {
777
+ var _a, _b;
778
+ try {
779
+ 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;
780
+ if (!raw) return limit;
781
+ const parsed = Number.parseInt(raw, 10);
782
+ if (Number.isFinite(parsed) && parsed > 0) {
783
+ return Math.min(limit, parsed);
784
+ }
785
+ } catch (e) {
786
+ }
787
+ return limit;
462
788
  }
463
789
  var TraceShipper = class {
464
790
  constructor(opts) {
465
791
  this.queue = [];
466
792
  this.inFlight = /* @__PURE__ */ new Set();
467
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
793
+ this.hasShutdown = false;
794
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
468
795
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
469
796
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
470
797
  this.enabled = opts.enabled !== false;
@@ -477,13 +804,79 @@ var TraceShipper = class {
477
804
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
478
805
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
479
806
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
480
- const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
481
- if (localDebugger) {
482
- this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
483
- if (this.debug) {
484
- console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
807
+ this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
808
+ if (this.debug && this.localDebuggerUrl) {
809
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
810
+ }
811
+ this.transformSpanHook = opts.transformSpan;
812
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
813
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
814
+ }
815
+ /**
816
+ * Cap every string attribute value on the span. O(#attributes) length
817
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
818
+ * pipeline so the default secret-scrub still sees parseable JSON in
819
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
820
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
821
+ * in the surviving prefix).
822
+ *
823
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
824
+ * for span content, matching the Python SDK and the OTel SDK convention.
825
+ */
826
+ capSpanAttributes(span) {
827
+ var _a;
828
+ const maxChars = applyOtelSpanAttributeLimit(
829
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
830
+ );
831
+ const attrs = span.attributes;
832
+ if (!attrs || attrs.length === 0) return span;
833
+ let nextAttrs;
834
+ for (let i = 0; i < attrs.length; i++) {
835
+ const attr = attrs[i];
836
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
837
+ if (typeof value !== "string" || value.length <= maxChars) continue;
838
+ if (!nextAttrs) nextAttrs = attrs.slice();
839
+ nextAttrs[i] = {
840
+ key: attr.key,
841
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
842
+ };
843
+ }
844
+ if (!nextAttrs) return span;
845
+ return { ...span, attributes: nextAttrs };
846
+ }
847
+ /**
848
+ * Apply the user `transformSpan` hook (if any) followed by the default
849
+ * redactor (unless disabled). Returns either the (possibly new) span to
850
+ * ship, or `null` to drop the span entirely.
851
+ *
852
+ * Ordering: user hook runs first so callers can rewrite the span freely
853
+ * (rename attrs, add new ones, scrub things the default doesn't know
854
+ * about). The default redactor then runs on whatever the user produced,
855
+ * acting as the always-on floor for documented BYOK secrets. If the user
856
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
857
+ *
858
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
859
+ * hook can never accidentally ship raw, un-redacted spans.
860
+ */
861
+ redactSpan(span) {
862
+ let current = span;
863
+ if (this.transformSpanHook) {
864
+ try {
865
+ const result = this.transformSpanHook(current);
866
+ if (result === null) return null;
867
+ if (result !== void 0) current = result;
868
+ } catch (err) {
869
+ if (this.debug) {
870
+ const msg = err instanceof Error ? err.message : String(err);
871
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
872
+ }
873
+ return null;
485
874
  }
486
875
  }
876
+ if (!this.disableDefaultRedaction) {
877
+ current = defaultTransformSpan(current);
878
+ }
879
+ return this.capSpanAttributes(current);
487
880
  }
488
881
  isDebugEnabled() {
489
882
  return this.debug;
@@ -501,8 +894,8 @@ var TraceShipper = class {
501
894
  ];
502
895
  if ((_b = args2.attributes) == null ? void 0 : _b.length) attrs.push(...args2.attributes);
503
896
  const span = { ids, name: args2.name, startTimeUnixNano: started, attributes: attrs };
504
- if (this.localDebuggerUrl) {
505
- const openSpan = buildOtlpSpan({
897
+ this.mirrorToLocalDebugger(
898
+ buildOtlpSpan({
506
899
  ids: span.ids,
507
900
  name: span.name,
508
901
  startTimeUnixNano: span.startTimeUnixNano,
@@ -510,16 +903,21 @@ var TraceShipper = class {
510
903
  // placeholder — will be updated on endSpan
511
904
  attributes: span.attributes,
512
905
  status: { code: SpanStatusCode.UNSET }
513
- });
514
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
515
- mirrorTraceExportToLocalDebugger(body, {
516
- baseUrl: this.localDebuggerUrl,
517
- debug: false,
518
- sdkName: this.sdkName
519
- });
520
- }
906
+ })
907
+ );
521
908
  return span;
522
909
  }
910
+ mirrorToLocalDebugger(span) {
911
+ if (!this.localDebuggerUrl) return;
912
+ const redacted = this.redactSpan(span);
913
+ if (redacted === null) return;
914
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
915
+ mirrorTraceExportToLocalDebugger(body, {
916
+ baseUrl: this.localDebuggerUrl,
917
+ debug: false,
918
+ sdkName: this.sdkName
919
+ });
920
+ }
523
921
  endSpan(span, extra) {
524
922
  var _a, _b;
525
923
  if (span.endTimeUnixNano) return;
@@ -541,14 +939,7 @@ var TraceShipper = class {
541
939
  status
542
940
  });
543
941
  this.enqueue(otlp);
544
- if (this.localDebuggerUrl) {
545
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
546
- mirrorTraceExportToLocalDebugger(body, {
547
- baseUrl: this.localDebuggerUrl,
548
- debug: false,
549
- sdkName: this.sdkName
550
- });
551
- }
942
+ this.mirrorToLocalDebugger(otlp);
552
943
  }
553
944
  createSpan(args2) {
554
945
  var _a;
@@ -566,14 +957,7 @@ var TraceShipper = class {
566
957
  status: args2.status
567
958
  });
568
959
  this.enqueue(otlp);
569
- if (this.localDebuggerUrl) {
570
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
571
- mirrorTraceExportToLocalDebugger(body, {
572
- baseUrl: this.localDebuggerUrl,
573
- debug: false,
574
- sdkName: this.sdkName
575
- });
576
- }
960
+ this.mirrorToLocalDebugger(otlp);
577
961
  }
578
962
  enqueue(span) {
579
963
  if (!this.enabled) return;
@@ -585,10 +969,12 @@ var TraceShipper = class {
585
969
  )}`
586
970
  );
587
971
  }
972
+ const redacted = this.redactSpan(span);
973
+ if (redacted === null) return;
588
974
  if (this.queue.length >= this.maxQueueSize) {
589
975
  this.queue.shift();
590
976
  }
591
- this.queue.push(span);
977
+ this.queue.push(redacted);
592
978
  if (this.queue.length >= this.maxBatchSize) {
593
979
  void this.flush().catch(() => {
594
980
  });
@@ -610,6 +996,17 @@ var TraceShipper = class {
610
996
  }
611
997
  while (this.queue.length > 0) {
612
998
  const batch = this.queue.splice(0, this.maxBatchSize);
999
+ if (!this.writeKey) continue;
1000
+ const opts = this.requestOpts();
1001
+ if (!opts) {
1002
+ rateLimitedLog(
1003
+ `${this.prefix}.shutdown_deadline`,
1004
+ () => console.warn(
1005
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1006
+ )
1007
+ );
1008
+ continue;
1009
+ }
613
1010
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
614
1011
  const url = `${this.baseUrl}traces`;
615
1012
  if (this.debug) {
@@ -618,11 +1015,7 @@ var TraceShipper = class {
618
1015
  endpoint: url
619
1016
  });
620
1017
  }
621
- const p = postJson(url, body, this.authHeaders(), {
622
- maxAttempts: 3,
623
- debug: this.debug,
624
- sdkName: this.sdkName
625
- });
1018
+ const p = postJson(url, body, this.authHeaders(), opts);
626
1019
  this.inFlight.add(p);
627
1020
  try {
628
1021
  try {
@@ -630,21 +1023,61 @@ var TraceShipper = class {
630
1023
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
631
1024
  } catch (err) {
632
1025
  const msg = err instanceof Error ? err.message : String(err);
633
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1026
+ rateLimitedLog(
1027
+ `${this.prefix}.send_spans_failed`,
1028
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1029
+ );
634
1030
  }
635
1031
  } finally {
636
1032
  this.inFlight.delete(p);
637
1033
  }
638
1034
  }
639
1035
  }
1036
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1037
+ requestOpts() {
1038
+ if (this.shutdownDeadlineAt !== void 0) {
1039
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1040
+ if (remainingMs <= 0) return null;
1041
+ return {
1042
+ maxAttempts: 1,
1043
+ debug: this.debug,
1044
+ sdkName: this.sdkName,
1045
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1046
+ };
1047
+ }
1048
+ if (this.hasShutdown) {
1049
+ return {
1050
+ maxAttempts: 1,
1051
+ debug: this.debug,
1052
+ sdkName: this.sdkName,
1053
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1054
+ };
1055
+ }
1056
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1057
+ }
640
1058
  async shutdown() {
641
- if (this.timer) {
642
- clearTimeout(this.timer);
643
- this.timer = void 0;
1059
+ this.hasShutdown = true;
1060
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1061
+ try {
1062
+ if (this.timer) {
1063
+ clearTimeout(this.timer);
1064
+ this.timer = void 0;
1065
+ }
1066
+ const drain = async () => {
1067
+ await this.flush();
1068
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1069
+ })));
1070
+ };
1071
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1072
+ if (!settled) {
1073
+ rateLimitedLog(
1074
+ `${this.prefix}.shutdown_deadline`,
1075
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1076
+ );
1077
+ }
1078
+ } finally {
1079
+ this.shutdownDeadlineAt = void 0;
644
1080
  }
645
- await this.flush();
646
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
647
- })));
648
1081
  }
649
1082
  };
650
1083
 
@@ -909,18 +1342,85 @@ function transcriptToProperties(summary) {
909
1342
 
910
1343
  // src/event-mapper.ts
911
1344
  var MAX_ATTR_LENGTH = 32768;
1345
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1346
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1347
+ var MAX_BOUNDED_DEPTH = 12;
912
1348
  function truncate(value) {
913
1349
  if (value === void 0) return void 0;
914
1350
  if (value.length <= MAX_ATTR_LENGTH) return value;
915
1351
  const suffix = "\n...[truncated]";
916
1352
  return value.slice(0, MAX_ATTR_LENGTH - suffix.length) + suffix;
917
1353
  }
1354
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1355
+ if (value.length <= limit) return value;
1356
+ if (limit > TRUNCATION_MARKER2.length) {
1357
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1358
+ }
1359
+ return value.slice(0, limit);
1360
+ }
1361
+ function boundedClone2(value, budget, depth) {
1362
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1363
+ if (typeof value === "string") {
1364
+ if (value.length > budget.remaining) {
1365
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1366
+ budget.remaining = 0;
1367
+ return taken;
1368
+ }
1369
+ budget.remaining -= Math.max(value.length, 1);
1370
+ return value;
1371
+ }
1372
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1373
+ budget.remaining -= 8;
1374
+ return value;
1375
+ }
1376
+ if (typeof value !== "object") {
1377
+ budget.remaining -= 8;
1378
+ return value;
1379
+ }
1380
+ if (depth >= MAX_BOUNDED_DEPTH) {
1381
+ budget.remaining -= 16;
1382
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1383
+ }
1384
+ const withToJson = value;
1385
+ if (typeof withToJson.toJSON === "function") {
1386
+ try {
1387
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1388
+ } catch (e) {
1389
+ }
1390
+ }
1391
+ if (Array.isArray(value)) {
1392
+ const out2 = [];
1393
+ for (const item of value) {
1394
+ if (budget.remaining <= 0) {
1395
+ out2.push(TRUNCATION_MARKER2);
1396
+ break;
1397
+ }
1398
+ out2.push(boundedClone2(item, budget, depth + 1));
1399
+ }
1400
+ return out2;
1401
+ }
1402
+ const out = {};
1403
+ for (const key of Object.keys(value)) {
1404
+ if (budget.remaining <= 0) {
1405
+ out["..."] = TRUNCATION_MARKER2;
1406
+ break;
1407
+ }
1408
+ budget.remaining -= Math.max(key.length, 1);
1409
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1410
+ }
1411
+ return out;
1412
+ }
918
1413
  function safeStringify(value) {
919
1414
  if (value === void 0 || value === null) return void 0;
920
1415
  try {
921
- return truncate(JSON.stringify(value));
1416
+ const pruned = boundedClone2(
1417
+ value,
1418
+ { remaining: MAX_ATTR_LENGTH + TRUNCATION_MARKER2.length + 256 },
1419
+ 0
1420
+ );
1421
+ return truncate(JSON.stringify(pruned));
922
1422
  } catch (e) {
923
- return truncate(String(value));
1423
+ return truncate(capText2(String(value), MAX_ATTR_LENGTH + 16));
924
1424
  }
925
1425
  }
926
1426
  var STATE_DIR = join(tmpdir(), "raindrop-claude-code");
@@ -1101,7 +1601,9 @@ async function handleUserPromptSubmit(payload, convoId, config, properties, even
1101
1601
  userId: config.userId,
1102
1602
  convoId,
1103
1603
  eventName: config.eventName,
1104
- input: payload.prompt,
1604
+ // Capped so a pasted multi-MB prompt can't produce an event that the
1605
+ // ingest API rejects (silent data loss) — it lands truncated instead.
1606
+ input: payload.prompt !== void 0 ? capText2(payload.prompt) : void 0,
1105
1607
  model,
1106
1608
  properties
1107
1609
  };
@@ -1224,7 +1726,7 @@ async function handlePostCompact(payload, eventId, convoId, config, properties,
1224
1726
  properties: {
1225
1727
  ...properties,
1226
1728
  compaction_trigger: payload.trigger,
1227
- compact_summary: payload.compact_summary
1729
+ compact_summary: payload.compact_summary !== void 0 ? capText2(payload.compact_summary) : void 0
1228
1730
  }
1229
1731
  });
1230
1732
  }
@@ -1459,7 +1961,7 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1459
1961
  attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1460
1962
  attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1461
1963
  ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1462
- attrString("ai.response.text", outputText),
1964
+ attrString("ai.response.text", truncate(outputText)),
1463
1965
  ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1464
1966
  ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1465
1967
  attrInt("ai.llm_call.index", emittedPhaseIndex)
@@ -1519,7 +2021,7 @@ async function handleStopOrFailure(payload, eventId, convoId, config, properties
1519
2021
  userId: config.userId,
1520
2022
  convoId,
1521
2023
  eventName: config.eventName,
1522
- output,
2024
+ output: output !== void 0 ? capText2(output) : void 0,
1523
2025
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1524
2026
  properties: {
1525
2027
  ...properties,
@@ -1627,6 +2129,7 @@ import { join as join3 } from "path";
1627
2129
  var DEFAULT_PORT = 5899;
1628
2130
  var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1629
2131
  var HEALTH_TIMEOUT_MS = 300;
2132
+ var MIRROR_TIMEOUT_MS = 2e3;
1630
2133
  var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
1631
2134
  var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
1632
2135
  var CACHE_TTL_MS = 5e3;
@@ -1650,10 +2153,8 @@ function writeCache(url) {
1650
2153
  async function healthPing(baseUrl) {
1651
2154
  const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1652
2155
  try {
1653
- const controller = new AbortController();
1654
- const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1655
- const resp = await fetch(healthUrl, { signal: controller.signal });
1656
- clearTimeout(timeout);
2156
+ const signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
2157
+ const resp = await fetch(healthUrl, { signal });
1657
2158
  if (resp.ok) {
1658
2159
  const body = await resp.json();
1659
2160
  if (body.ok) return baseUrl;
@@ -1696,7 +2197,8 @@ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1696
2197
  void fetch(url, {
1697
2198
  method: "POST",
1698
2199
  headers: { "Content-Type": "application/json" },
1699
- body: JSON.stringify(payload)
2200
+ body: JSON.stringify(payload),
2201
+ signal: AbortSignal.timeout(MIRROR_TIMEOUT_MS)
1700
2202
  }).catch(() => {
1701
2203
  });
1702
2204
  }
@@ -1707,6 +2209,35 @@ import { join as join4 } from "path";
1707
2209
  import { tmpdir as tmpdir3 } from "os";
1708
2210
  var STDIN_TIMEOUT_MS = 5e3;
1709
2211
  var STATE_DIR2 = join4(tmpdir3(), "raindrop-claude-code");
2212
+ var DEFAULT_HOOK_FLUSH_DEADLINE_MS = 8e3;
2213
+ var MIN_FLUSH_WINDOW_MS = 250;
2214
+ function hookFlushDeadlineMs() {
2215
+ const raw = process.env["RAINDROP_HOOK_FLUSH_DEADLINE_MS"];
2216
+ if (raw) {
2217
+ const parsed = parseInt(raw, 10);
2218
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
2219
+ }
2220
+ return DEFAULT_HOOK_FLUSH_DEADLINE_MS;
2221
+ }
2222
+ function withDeadline(promise, ms) {
2223
+ return new Promise((resolve2, reject) => {
2224
+ const timer = setTimeout(() => {
2225
+ promise.catch(() => {
2226
+ });
2227
+ resolve2("deadline");
2228
+ }, ms);
2229
+ promise.then(
2230
+ (value) => {
2231
+ clearTimeout(timer);
2232
+ resolve2(value);
2233
+ },
2234
+ (err) => {
2235
+ clearTimeout(timer);
2236
+ reject(err);
2237
+ }
2238
+ );
2239
+ });
2240
+ }
1710
2241
  function getOtlpTraceId(sessionId) {
1711
2242
  try {
1712
2243
  const key = `rootspan_${sessionId}`.replace(/[^a-zA-Z0-9_\-]/g, "_");
@@ -1744,6 +2275,7 @@ function readStdin() {
1744
2275
  });
1745
2276
  }
1746
2277
  async function handleHook() {
2278
+ const startedAtMs = Date.now();
1747
2279
  const raw = await readStdin();
1748
2280
  if (!raw.trim()) {
1749
2281
  return;
@@ -1819,9 +2351,23 @@ async function handleHook() {
1819
2351
  };
1820
2352
  }
1821
2353
  try {
1822
- await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
1823
- await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1824
- await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2354
+ const remainingMs = Math.max(
2355
+ MIN_FLUSH_WINDOW_MS,
2356
+ hookFlushDeadlineMs() - (Date.now() - startedAtMs)
2357
+ );
2358
+ const outcome = await withDeadline(
2359
+ (async () => {
2360
+ await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
2361
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2362
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2363
+ })(),
2364
+ remainingMs
2365
+ );
2366
+ if (outcome === "deadline" && config.debug) {
2367
+ console.error(
2368
+ `[raindrop-ai/claude-code] hook work exceeded the ${hookFlushDeadlineMs()}ms deadline; exiting without waiting on the network (pending telemetry dropped)`
2369
+ );
2370
+ }
1825
2371
  } catch (err) {
1826
2372
  if (config.debug) {
1827
2373
  console.error(
@@ -2465,7 +3011,7 @@ async function main() {
2465
3011
  }
2466
3012
  case "hook": {
2467
3013
  await handleHook();
2468
- break;
3014
+ process.exit(0);
2469
3015
  }
2470
3016
  case "mcp-serve": {
2471
3017
  await startMcpServer();