@raindrop-ai/claude-code 0.0.13 → 0.0.15

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-SK6EJEO7.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,112 @@ 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 PROJECT_ID_HEADER = "X-Raindrop-Project-Id";
352
+ var PROJECT_ID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
353
+ function isValidProjectIdSlug(value) {
354
+ return PROJECT_ID_SLUG_PATTERN.test(value);
355
+ }
356
+ function normalizeProjectId(raw, opts) {
357
+ if (typeof raw !== "string") return void 0;
358
+ const trimmed = raw.trim();
359
+ if (!trimmed) return void 0;
360
+ if (!isValidProjectIdSlug(trimmed) && opts.debug) {
361
+ console.warn(
362
+ `${opts.prefix} projectId "${trimmed}" does not match slug ${PROJECT_ID_SLUG_PATTERN.source}; sending anyway \u2014 backend may reject with HTTP 400`
363
+ );
364
+ }
365
+ return trimmed;
366
+ }
367
+ function projectIdHeaders(projectId) {
368
+ return projectId ? { [PROJECT_ID_HEADER]: projectId } : {};
369
+ }
370
+ var SHUTDOWN_DEADLINE_MS = 1e4;
371
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
200
372
  function mergePatches(target, source) {
201
373
  var _a, _b, _c, _d;
202
374
  const out = { ...target, ...source };
@@ -214,7 +386,8 @@ var EventShipper = class {
214
386
  this.sticky = /* @__PURE__ */ new Map();
215
387
  this.timers = /* @__PURE__ */ new Map();
216
388
  this.inFlight = /* @__PURE__ */ new Set();
217
- var _a, _b, _c, _d, _e, _f, _g;
389
+ this.hasShutdown = false;
390
+ var _a, _b, _c, _d, _e, _f, _g, _h;
218
391
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
219
392
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
220
393
  this.enabled = opts.enabled !== false;
@@ -223,11 +396,20 @@ var EventShipper = class {
223
396
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
224
397
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
225
398
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
399
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
400
+ this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
401
+ if (this.debug && this.localDebuggerUrl) {
402
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
403
+ }
404
+ this.projectId = normalizeProjectId(opts.projectId, {
405
+ debug: this.debug,
406
+ prefix: this.prefix
407
+ });
226
408
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
227
409
  this.context = {
228
410
  library: {
229
- name: (_f = opts.libraryName) != null ? _f : "@raindrop-ai/core",
230
- version: (_g = opts.libraryVersion) != null ? _g : "0.0.0"
411
+ name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
412
+ version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
231
413
  },
232
414
  metadata: {
233
415
  jsRuntime: isNode ? "node" : "web",
@@ -241,10 +423,55 @@ var EventShipper = class {
241
423
  authHeaders() {
242
424
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
243
425
  }
426
+ requestHeaders() {
427
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
428
+ }
429
+ /**
430
+ * Build the retry/timeout options for one POST, honoring the shutdown
431
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
432
+ * the caller must drop the payload (with a rate-limited warning) instead
433
+ * of issuing a request that could outlive process exit.
434
+ *
435
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
436
+ * path is mid-drain takes effect immediately: no further retries, and the
437
+ * per-attempt timeout is clamped to the remaining window. After
438
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
439
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
440
+ * run as a single short attempt rather than regaining the full retry
441
+ * schedule.
442
+ */
443
+ requestOpts() {
444
+ if (this.shutdownDeadlineAt !== void 0) {
445
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
446
+ if (remainingMs <= 0) return null;
447
+ return {
448
+ maxAttempts: 1,
449
+ debug: this.debug,
450
+ sdkName: this.sdkName,
451
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
452
+ };
453
+ }
454
+ if (this.hasShutdown) {
455
+ return {
456
+ maxAttempts: 1,
457
+ debug: this.debug,
458
+ sdkName: this.sdkName,
459
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
460
+ };
461
+ }
462
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
463
+ }
244
464
  async patch(eventId, patch) {
245
465
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
246
466
  if (!this.enabled) return;
247
467
  if (!eventId || !eventId.trim()) return;
468
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
469
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
470
+ patch = { ...patch, input: capText(patch.input, maxChars) };
471
+ }
472
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
473
+ patch = { ...patch, output: capText(patch.output, maxChars) };
474
+ }
248
475
  if (this.debug) {
249
476
  console.log(`${this.prefix} queue patch`, {
250
477
  eventId,
@@ -291,9 +518,16 @@ var EventShipper = class {
291
518
  })));
292
519
  }
293
520
  async shutdown() {
294
- for (const t of this.timers.values()) clearTimeout(t);
295
- this.timers.clear();
296
- await this.flush();
521
+ this.hasShutdown = true;
522
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
523
+ try {
524
+ for (const t of this.timers.values()) clearTimeout(t);
525
+ this.timers.clear();
526
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
527
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
528
+ } finally {
529
+ this.shutdownDeadlineAt = void 0;
530
+ }
297
531
  }
298
532
  async trackSignal(signal) {
299
533
  var _a, _b;
@@ -313,16 +547,21 @@ var EventShipper = class {
313
547
  }
314
548
  }
315
549
  ];
550
+ if (!this.writeKey) return;
316
551
  const url = `${this.baseUrl}signals/track`;
552
+ const opts = this.requestOpts();
553
+ if (!opts) {
554
+ this.warnShutdownDrop("signal");
555
+ return;
556
+ }
317
557
  try {
318
- await postJson(url, body, this.authHeaders(), {
319
- maxAttempts: 3,
320
- debug: this.debug,
321
- sdkName: this.sdkName
322
- });
558
+ await postJson(url, body, this.requestHeaders(), opts);
323
559
  } catch (err) {
324
560
  const msg = err instanceof Error ? err.message : String(err);
325
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
561
+ rateLimitedLog(
562
+ `${this.prefix}.send_signal_failed`,
563
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
564
+ );
326
565
  }
327
566
  }
328
567
  async identify(users) {
@@ -343,19 +582,30 @@ var EventShipper = class {
343
582
  traits: (_a = user.traits) != null ? _a : {}
344
583
  };
345
584
  });
585
+ if (!this.writeKey) return;
346
586
  if (body.length === 0) return;
347
587
  const url = `${this.baseUrl}users/identify`;
588
+ const opts = this.requestOpts();
589
+ if (!opts) {
590
+ this.warnShutdownDrop("identify");
591
+ return;
592
+ }
348
593
  try {
349
- await postJson(url, body, this.authHeaders(), {
350
- maxAttempts: 3,
351
- debug: this.debug,
352
- sdkName: this.sdkName
353
- });
594
+ await postJson(url, body, this.requestHeaders(), opts);
354
595
  } catch (err) {
355
596
  const msg = err instanceof Error ? err.message : String(err);
356
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
597
+ rateLimitedLog(
598
+ `${this.prefix}.send_identify_failed`,
599
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
600
+ );
357
601
  }
358
602
  }
603
+ warnShutdownDrop(what) {
604
+ rateLimitedLog(
605
+ `${this.prefix}.shutdown_deadline`,
606
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
607
+ );
608
+ }
359
609
  async flushOne(eventId) {
360
610
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
361
611
  if (!this.enabled) return;
@@ -419,11 +669,25 @@ var EventShipper = class {
419
669
  endpoint: url
420
670
  });
421
671
  }
422
- const p = postJson(url, payload, this.authHeaders(), {
423
- maxAttempts: 3,
424
- debug: this.debug,
425
- sdkName: this.sdkName
426
- });
672
+ if (this.localDebuggerUrl) {
673
+ mirrorPartialEventToLocalDebugger(payload, {
674
+ baseUrl: this.localDebuggerUrl,
675
+ writeKey: this.writeKey,
676
+ debug: this.debug,
677
+ sdkName: this.sdkName
678
+ });
679
+ }
680
+ if (!this.writeKey) {
681
+ if (!isPending) this.sticky.delete(eventId);
682
+ return;
683
+ }
684
+ const opts = this.requestOpts();
685
+ if (!opts) {
686
+ this.warnShutdownDrop(`track_partial ${eventId}`);
687
+ if (!isPending) this.sticky.delete(eventId);
688
+ return;
689
+ }
690
+ const p = postJson(url, payload, this.requestHeaders(), opts);
427
691
  this.inFlight.add(p);
428
692
  try {
429
693
  try {
@@ -433,7 +697,10 @@ var EventShipper = class {
433
697
  }
434
698
  } catch (err) {
435
699
  const msg = err instanceof Error ? err.message : String(err);
436
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
700
+ rateLimitedLog(
701
+ `${this.prefix}.send_track_partial_failed`,
702
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
703
+ );
437
704
  }
438
705
  } finally {
439
706
  this.inFlight.delete(p);
@@ -443,28 +710,114 @@ var EventShipper = class {
443
710
  }
444
711
  }
445
712
  };
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;
713
+ var DEFAULT_SECRET_KEY_NAMES = [
714
+ "apikey",
715
+ "apisecret",
716
+ "apitoken",
717
+ "secretaccesskey",
718
+ "sessiontoken",
719
+ "privatekey",
720
+ "privatekeyid",
721
+ "clientsecret",
722
+ "accesstoken",
723
+ "refreshtoken",
724
+ "oauthtoken",
725
+ "bearertoken",
726
+ "authorization",
727
+ "password",
728
+ "passphrase"
729
+ ];
730
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
731
+ function normalizeKeyName(name) {
732
+ return name.toLowerCase().replace(/[-_.]/g, "");
451
733
  }
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
- });
734
+ function redactSecretsInObject(value, options) {
735
+ var _a, _b;
736
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
737
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
738
+ const seen = /* @__PURE__ */ new WeakSet();
739
+ const walk = (node) => {
740
+ if (node === null || typeof node !== "object") return node;
741
+ if (seen.has(node)) return "[CIRCULAR]";
742
+ seen.add(node);
743
+ if (Array.isArray(node)) {
744
+ return node.map((item) => walk(item));
745
+ }
746
+ const out = {};
747
+ for (const [k, v] of Object.entries(node)) {
748
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
749
+ out[k] = placeholder;
750
+ } else {
751
+ out[k] = walk(v);
752
+ }
753
+ }
754
+ return out;
755
+ };
756
+ return walk(value);
757
+ }
758
+ function buildSecretSet(names) {
759
+ const set = /* @__PURE__ */ new Set();
760
+ for (const name of names) set.add(normalizeKeyName(name));
761
+ return set;
762
+ }
763
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
764
+ "ai.request.providerOptions",
765
+ "ai.response.providerMetadata"
766
+ ];
767
+ function defaultTransformSpan(span) {
768
+ const attrs = span.attributes;
769
+ if (!attrs || attrs.length === 0) return span;
770
+ let nextAttrs;
771
+ for (let i = 0; i < attrs.length; i++) {
772
+ const attr = attrs[i];
773
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
774
+ if (redacted === void 0) continue;
775
+ if (!nextAttrs) nextAttrs = attrs.slice();
776
+ nextAttrs[i] = { key: attr.key, value: redacted };
777
+ }
778
+ if (!nextAttrs) return span;
779
+ return { ...span, attributes: nextAttrs };
780
+ }
781
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
782
+ function redactJsonAttributeValue(key, value) {
783
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
784
+ const json = value.stringValue;
785
+ if (typeof json !== "string" || json.length === 0) return void 0;
786
+ let parsed;
787
+ try {
788
+ parsed = JSON.parse(json);
789
+ } catch (e) {
790
+ return void 0;
791
+ }
792
+ const scrubbed = redactSecretsInObject(parsed);
793
+ let scrubbedJson;
794
+ try {
795
+ scrubbedJson = JSON.stringify(scrubbed);
796
+ } catch (e) {
797
+ return void 0;
798
+ }
799
+ if (scrubbedJson === json) return void 0;
800
+ return { stringValue: scrubbedJson };
801
+ }
802
+ function applyOtelSpanAttributeLimit(limit) {
803
+ var _a, _b;
804
+ try {
805
+ 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;
806
+ if (!raw) return limit;
807
+ const parsed = Number.parseInt(raw, 10);
808
+ if (Number.isFinite(parsed) && parsed > 0) {
809
+ return Math.min(limit, parsed);
810
+ }
811
+ } catch (e) {
812
+ }
813
+ return limit;
462
814
  }
463
815
  var TraceShipper = class {
464
816
  constructor(opts) {
465
817
  this.queue = [];
466
818
  this.inFlight = /* @__PURE__ */ new Set();
467
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
819
+ this.hasShutdown = false;
820
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
468
821
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
469
822
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
470
823
  this.enabled = opts.enabled !== false;
@@ -477,13 +830,83 @@ var TraceShipper = class {
477
830
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
478
831
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
479
832
  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}`);
833
+ this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
834
+ if (this.debug && this.localDebuggerUrl) {
835
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
836
+ }
837
+ this.projectId = normalizeProjectId(opts.projectId, {
838
+ debug: this.debug,
839
+ prefix: this.prefix
840
+ });
841
+ this.transformSpanHook = opts.transformSpan;
842
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
843
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
844
+ }
845
+ /**
846
+ * Cap every string attribute value on the span. O(#attributes) length
847
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
848
+ * pipeline so the default secret-scrub still sees parseable JSON in
849
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
850
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
851
+ * in the surviving prefix).
852
+ *
853
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
854
+ * for span content, matching the Python SDK and the OTel SDK convention.
855
+ */
856
+ capSpanAttributes(span) {
857
+ var _a;
858
+ const maxChars = applyOtelSpanAttributeLimit(
859
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
860
+ );
861
+ const attrs = span.attributes;
862
+ if (!attrs || attrs.length === 0) return span;
863
+ let nextAttrs;
864
+ for (let i = 0; i < attrs.length; i++) {
865
+ const attr = attrs[i];
866
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
867
+ if (typeof value !== "string" || value.length <= maxChars) continue;
868
+ if (!nextAttrs) nextAttrs = attrs.slice();
869
+ nextAttrs[i] = {
870
+ key: attr.key,
871
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
872
+ };
873
+ }
874
+ if (!nextAttrs) return span;
875
+ return { ...span, attributes: nextAttrs };
876
+ }
877
+ /**
878
+ * Apply the user `transformSpan` hook (if any) followed by the default
879
+ * redactor (unless disabled). Returns either the (possibly new) span to
880
+ * ship, or `null` to drop the span entirely.
881
+ *
882
+ * Ordering: user hook runs first so callers can rewrite the span freely
883
+ * (rename attrs, add new ones, scrub things the default doesn't know
884
+ * about). The default redactor then runs on whatever the user produced,
885
+ * acting as the always-on floor for documented BYOK secrets. If the user
886
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
887
+ *
888
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
889
+ * hook can never accidentally ship raw, un-redacted spans.
890
+ */
891
+ redactSpan(span) {
892
+ let current = span;
893
+ if (this.transformSpanHook) {
894
+ try {
895
+ const result = this.transformSpanHook(current);
896
+ if (result === null) return null;
897
+ if (result !== void 0) current = result;
898
+ } catch (err) {
899
+ if (this.debug) {
900
+ const msg = err instanceof Error ? err.message : String(err);
901
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
902
+ }
903
+ return null;
485
904
  }
486
905
  }
906
+ if (!this.disableDefaultRedaction) {
907
+ current = defaultTransformSpan(current);
908
+ }
909
+ return this.capSpanAttributes(current);
487
910
  }
488
911
  isDebugEnabled() {
489
912
  return this.debug;
@@ -491,6 +914,9 @@ var TraceShipper = class {
491
914
  authHeaders() {
492
915
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
493
916
  }
917
+ requestHeaders() {
918
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
919
+ }
494
920
  startSpan(args2) {
495
921
  var _a, _b;
496
922
  const ids = createSpanIds(args2.parent);
@@ -501,8 +927,8 @@ var TraceShipper = class {
501
927
  ];
502
928
  if ((_b = args2.attributes) == null ? void 0 : _b.length) attrs.push(...args2.attributes);
503
929
  const span = { ids, name: args2.name, startTimeUnixNano: started, attributes: attrs };
504
- if (this.localDebuggerUrl) {
505
- const openSpan = buildOtlpSpan({
930
+ this.mirrorToLocalDebugger(
931
+ buildOtlpSpan({
506
932
  ids: span.ids,
507
933
  name: span.name,
508
934
  startTimeUnixNano: span.startTimeUnixNano,
@@ -510,16 +936,21 @@ var TraceShipper = class {
510
936
  // placeholder — will be updated on endSpan
511
937
  attributes: span.attributes,
512
938
  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
- }
939
+ })
940
+ );
521
941
  return span;
522
942
  }
943
+ mirrorToLocalDebugger(span) {
944
+ if (!this.localDebuggerUrl) return;
945
+ const redacted = this.redactSpan(span);
946
+ if (redacted === null) return;
947
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
948
+ mirrorTraceExportToLocalDebugger(body, {
949
+ baseUrl: this.localDebuggerUrl,
950
+ debug: false,
951
+ sdkName: this.sdkName
952
+ });
953
+ }
523
954
  endSpan(span, extra) {
524
955
  var _a, _b;
525
956
  if (span.endTimeUnixNano) return;
@@ -541,14 +972,7 @@ var TraceShipper = class {
541
972
  status
542
973
  });
543
974
  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
- }
975
+ this.mirrorToLocalDebugger(otlp);
552
976
  }
553
977
  createSpan(args2) {
554
978
  var _a;
@@ -566,14 +990,7 @@ var TraceShipper = class {
566
990
  status: args2.status
567
991
  });
568
992
  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
- }
993
+ this.mirrorToLocalDebugger(otlp);
577
994
  }
578
995
  enqueue(span) {
579
996
  if (!this.enabled) return;
@@ -585,10 +1002,12 @@ var TraceShipper = class {
585
1002
  )}`
586
1003
  );
587
1004
  }
1005
+ const redacted = this.redactSpan(span);
1006
+ if (redacted === null) return;
588
1007
  if (this.queue.length >= this.maxQueueSize) {
589
1008
  this.queue.shift();
590
1009
  }
591
- this.queue.push(span);
1010
+ this.queue.push(redacted);
592
1011
  if (this.queue.length >= this.maxBatchSize) {
593
1012
  void this.flush().catch(() => {
594
1013
  });
@@ -610,6 +1029,17 @@ var TraceShipper = class {
610
1029
  }
611
1030
  while (this.queue.length > 0) {
612
1031
  const batch = this.queue.splice(0, this.maxBatchSize);
1032
+ if (!this.writeKey) continue;
1033
+ const opts = this.requestOpts();
1034
+ if (!opts) {
1035
+ rateLimitedLog(
1036
+ `${this.prefix}.shutdown_deadline`,
1037
+ () => console.warn(
1038
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1039
+ )
1040
+ );
1041
+ continue;
1042
+ }
613
1043
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
614
1044
  const url = `${this.baseUrl}traces`;
615
1045
  if (this.debug) {
@@ -618,11 +1048,7 @@ var TraceShipper = class {
618
1048
  endpoint: url
619
1049
  });
620
1050
  }
621
- const p = postJson(url, body, this.authHeaders(), {
622
- maxAttempts: 3,
623
- debug: this.debug,
624
- sdkName: this.sdkName
625
- });
1051
+ const p = postJson(url, body, this.requestHeaders(), opts);
626
1052
  this.inFlight.add(p);
627
1053
  try {
628
1054
  try {
@@ -630,21 +1056,61 @@ var TraceShipper = class {
630
1056
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
631
1057
  } catch (err) {
632
1058
  const msg = err instanceof Error ? err.message : String(err);
633
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1059
+ rateLimitedLog(
1060
+ `${this.prefix}.send_spans_failed`,
1061
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1062
+ );
634
1063
  }
635
1064
  } finally {
636
1065
  this.inFlight.delete(p);
637
1066
  }
638
1067
  }
639
1068
  }
1069
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1070
+ requestOpts() {
1071
+ if (this.shutdownDeadlineAt !== void 0) {
1072
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1073
+ if (remainingMs <= 0) return null;
1074
+ return {
1075
+ maxAttempts: 1,
1076
+ debug: this.debug,
1077
+ sdkName: this.sdkName,
1078
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1079
+ };
1080
+ }
1081
+ if (this.hasShutdown) {
1082
+ return {
1083
+ maxAttempts: 1,
1084
+ debug: this.debug,
1085
+ sdkName: this.sdkName,
1086
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1087
+ };
1088
+ }
1089
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1090
+ }
640
1091
  async shutdown() {
641
- if (this.timer) {
642
- clearTimeout(this.timer);
643
- this.timer = void 0;
1092
+ this.hasShutdown = true;
1093
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1094
+ try {
1095
+ if (this.timer) {
1096
+ clearTimeout(this.timer);
1097
+ this.timer = void 0;
1098
+ }
1099
+ const drain = async () => {
1100
+ await this.flush();
1101
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1102
+ })));
1103
+ };
1104
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1105
+ if (!settled) {
1106
+ rateLimitedLog(
1107
+ `${this.prefix}.shutdown_deadline`,
1108
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1109
+ );
1110
+ }
1111
+ } finally {
1112
+ this.shutdownDeadlineAt = void 0;
644
1113
  }
645
- await this.flush();
646
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
647
- })));
648
1114
  }
649
1115
  };
650
1116
 
@@ -909,18 +1375,85 @@ function transcriptToProperties(summary) {
909
1375
 
910
1376
  // src/event-mapper.ts
911
1377
  var MAX_ATTR_LENGTH = 32768;
1378
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1379
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1380
+ var MAX_BOUNDED_DEPTH = 12;
912
1381
  function truncate(value) {
913
1382
  if (value === void 0) return void 0;
914
1383
  if (value.length <= MAX_ATTR_LENGTH) return value;
915
1384
  const suffix = "\n...[truncated]";
916
1385
  return value.slice(0, MAX_ATTR_LENGTH - suffix.length) + suffix;
917
1386
  }
1387
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1388
+ if (value.length <= limit) return value;
1389
+ if (limit > TRUNCATION_MARKER2.length) {
1390
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1391
+ }
1392
+ return value.slice(0, limit);
1393
+ }
1394
+ function boundedClone2(value, budget, depth) {
1395
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1396
+ if (typeof value === "string") {
1397
+ if (value.length > budget.remaining) {
1398
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1399
+ budget.remaining = 0;
1400
+ return taken;
1401
+ }
1402
+ budget.remaining -= Math.max(value.length, 1);
1403
+ return value;
1404
+ }
1405
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1406
+ budget.remaining -= 8;
1407
+ return value;
1408
+ }
1409
+ if (typeof value !== "object") {
1410
+ budget.remaining -= 8;
1411
+ return value;
1412
+ }
1413
+ if (depth >= MAX_BOUNDED_DEPTH) {
1414
+ budget.remaining -= 16;
1415
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1416
+ }
1417
+ const withToJson = value;
1418
+ if (typeof withToJson.toJSON === "function") {
1419
+ try {
1420
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1421
+ } catch (e) {
1422
+ }
1423
+ }
1424
+ if (Array.isArray(value)) {
1425
+ const out2 = [];
1426
+ for (const item of value) {
1427
+ if (budget.remaining <= 0) {
1428
+ out2.push(TRUNCATION_MARKER2);
1429
+ break;
1430
+ }
1431
+ out2.push(boundedClone2(item, budget, depth + 1));
1432
+ }
1433
+ return out2;
1434
+ }
1435
+ const out = {};
1436
+ for (const key of Object.keys(value)) {
1437
+ if (budget.remaining <= 0) {
1438
+ out["..."] = TRUNCATION_MARKER2;
1439
+ break;
1440
+ }
1441
+ budget.remaining -= Math.max(key.length, 1);
1442
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1443
+ }
1444
+ return out;
1445
+ }
918
1446
  function safeStringify(value) {
919
1447
  if (value === void 0 || value === null) return void 0;
920
1448
  try {
921
- return truncate(JSON.stringify(value));
1449
+ const pruned = boundedClone2(
1450
+ value,
1451
+ { remaining: MAX_ATTR_LENGTH + TRUNCATION_MARKER2.length + 256 },
1452
+ 0
1453
+ );
1454
+ return truncate(JSON.stringify(pruned));
922
1455
  } catch (e) {
923
- return truncate(String(value));
1456
+ return truncate(capText2(String(value), MAX_ATTR_LENGTH + 16));
924
1457
  }
925
1458
  }
926
1459
  var STATE_DIR = join(tmpdir(), "raindrop-claude-code");
@@ -1101,7 +1634,9 @@ async function handleUserPromptSubmit(payload, convoId, config, properties, even
1101
1634
  userId: config.userId,
1102
1635
  convoId,
1103
1636
  eventName: config.eventName,
1104
- input: payload.prompt,
1637
+ // Capped so a pasted multi-MB prompt can't produce an event that the
1638
+ // ingest API rejects (silent data loss) — it lands truncated instead.
1639
+ input: payload.prompt !== void 0 ? capText2(payload.prompt) : void 0,
1105
1640
  model,
1106
1641
  properties
1107
1642
  };
@@ -1224,7 +1759,7 @@ async function handlePostCompact(payload, eventId, convoId, config, properties,
1224
1759
  properties: {
1225
1760
  ...properties,
1226
1761
  compaction_trigger: payload.trigger,
1227
- compact_summary: payload.compact_summary
1762
+ compact_summary: payload.compact_summary !== void 0 ? capText2(payload.compact_summary) : void 0
1228
1763
  }
1229
1764
  });
1230
1765
  }
@@ -1459,7 +1994,7 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
1459
1994
  attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
1460
1995
  attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
1461
1996
  ...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
1462
- attrString("ai.response.text", outputText),
1997
+ attrString("ai.response.text", truncate(outputText)),
1463
1998
  ...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
1464
1999
  ...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
1465
2000
  attrInt("ai.llm_call.index", emittedPhaseIndex)
@@ -1519,7 +2054,7 @@ async function handleStopOrFailure(payload, eventId, convoId, config, properties
1519
2054
  userId: config.userId,
1520
2055
  convoId,
1521
2056
  eventName: config.eventName,
1522
- output,
2057
+ output: output !== void 0 ? capText2(output) : void 0,
1523
2058
  ...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
1524
2059
  properties: {
1525
2060
  ...properties,
@@ -1549,7 +2084,7 @@ import { homedir, userInfo } from "os";
1549
2084
  import { dirname, join as join2 } from "path";
1550
2085
  var CONFIG_PATH = join2(homedir(), ".config", "raindrop", "config.json");
1551
2086
  function loadConfig() {
1552
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
2087
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
1553
2088
  let file = {};
1554
2089
  try {
1555
2090
  if (existsSync3(CONFIG_PATH)) {
@@ -1589,9 +2124,10 @@ function loadConfig() {
1589
2124
  return {
1590
2125
  writeKey: (_c = (_b = process.env["RAINDROP_WRITE_KEY"]) != null ? _b : file.write_key) != null ? _c : "",
1591
2126
  endpoint: (_e = (_d = process.env["RAINDROP_API_URL"]) != null ? _d : file.api_url) != null ? _e : "https://api.raindrop.ai/v1",
1592
- userId: (_g = (_f = process.env["RAINDROP_USER_ID"]) != null ? _f : file.user_id) != null ? _g : systemUser,
1593
- convoId: ((_h = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _h.trim()) || void 0,
1594
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_i = file.debug) != null ? _i : false,
2127
+ projectId: (_f = process.env["RAINDROP_PROJECT_ID"]) != null ? _f : file.project_id,
2128
+ userId: (_h = (_g = process.env["RAINDROP_USER_ID"]) != null ? _g : file.user_id) != null ? _h : systemUser,
2129
+ convoId: ((_i = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _i.trim()) || void 0,
2130
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_j = file.debug) != null ? _j : false,
1595
2131
  enabled: (() => {
1596
2132
  var _a2;
1597
2133
  const env = process.env["RAINDROP_ENABLED"];
@@ -1599,7 +2135,7 @@ function loadConfig() {
1599
2135
  const lower = env.toLowerCase();
1600
2136
  return lower !== "false" && env !== "0";
1601
2137
  })(),
1602
- eventName: (_k = (_j = process.env["RAINDROP_EVENT_NAME"]) != null ? _j : file.event_name) != null ? _k : "claude_code_session",
2138
+ eventName: (_l = (_k = process.env["RAINDROP_EVENT_NAME"]) != null ? _k : file.event_name) != null ? _l : "claude_code_session",
1603
2139
  customProperties,
1604
2140
  selfDiagnostics
1605
2141
  };
@@ -1627,6 +2163,7 @@ import { join as join3 } from "path";
1627
2163
  var DEFAULT_PORT = 5899;
1628
2164
  var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1629
2165
  var HEALTH_TIMEOUT_MS = 300;
2166
+ var MIRROR_TIMEOUT_MS = 2e3;
1630
2167
  var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
1631
2168
  var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
1632
2169
  var CACHE_TTL_MS = 5e3;
@@ -1650,10 +2187,8 @@ function writeCache(url) {
1650
2187
  async function healthPing(baseUrl) {
1651
2188
  const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1652
2189
  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);
2190
+ const signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
2191
+ const resp = await fetch(healthUrl, { signal });
1657
2192
  if (resp.ok) {
1658
2193
  const body = await resp.json();
1659
2194
  if (body.ok) return baseUrl;
@@ -1696,7 +2231,8 @@ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1696
2231
  void fetch(url, {
1697
2232
  method: "POST",
1698
2233
  headers: { "Content-Type": "application/json" },
1699
- body: JSON.stringify(payload)
2234
+ body: JSON.stringify(payload),
2235
+ signal: AbortSignal.timeout(MIRROR_TIMEOUT_MS)
1700
2236
  }).catch(() => {
1701
2237
  });
1702
2238
  }
@@ -1707,6 +2243,35 @@ import { join as join4 } from "path";
1707
2243
  import { tmpdir as tmpdir3 } from "os";
1708
2244
  var STDIN_TIMEOUT_MS = 5e3;
1709
2245
  var STATE_DIR2 = join4(tmpdir3(), "raindrop-claude-code");
2246
+ var DEFAULT_HOOK_FLUSH_DEADLINE_MS = 8e3;
2247
+ var MIN_FLUSH_WINDOW_MS = 250;
2248
+ function hookFlushDeadlineMs() {
2249
+ const raw = process.env["RAINDROP_HOOK_FLUSH_DEADLINE_MS"];
2250
+ if (raw) {
2251
+ const parsed = parseInt(raw, 10);
2252
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
2253
+ }
2254
+ return DEFAULT_HOOK_FLUSH_DEADLINE_MS;
2255
+ }
2256
+ function withDeadline(promise, ms) {
2257
+ return new Promise((resolve2, reject) => {
2258
+ const timer = setTimeout(() => {
2259
+ promise.catch(() => {
2260
+ });
2261
+ resolve2("deadline");
2262
+ }, ms);
2263
+ promise.then(
2264
+ (value) => {
2265
+ clearTimeout(timer);
2266
+ resolve2(value);
2267
+ },
2268
+ (err) => {
2269
+ clearTimeout(timer);
2270
+ reject(err);
2271
+ }
2272
+ );
2273
+ });
2274
+ }
1710
2275
  function getOtlpTraceId(sessionId) {
1711
2276
  try {
1712
2277
  const key = `rootspan_${sessionId}`.replace(/[^a-zA-Z0-9_\-]/g, "_");
@@ -1744,6 +2309,7 @@ function readStdin() {
1744
2309
  });
1745
2310
  }
1746
2311
  async function handleHook() {
2312
+ const startedAtMs = Date.now();
1747
2313
  const raw = await readStdin();
1748
2314
  if (!raw.trim()) {
1749
2315
  return;
@@ -1781,6 +2347,7 @@ async function handleHook() {
1781
2347
  writeKey: config.writeKey,
1782
2348
  endpoint: config.endpoint,
1783
2349
  debug: config.debug,
2350
+ projectId: config.projectId,
1784
2351
  // Only ship to the cloud when we have an API key. When only the local
1785
2352
  // debugger is present the shipper still needs to be instantiated so the
1786
2353
  // event mapper can call patch/finish, but cloud POSTs are skipped.
@@ -1790,6 +2357,7 @@ async function handleHook() {
1790
2357
  writeKey: config.writeKey,
1791
2358
  endpoint: config.endpoint,
1792
2359
  debug: config.debug,
2360
+ projectId: config.projectId,
1793
2361
  enabled: hasCloudKey
1794
2362
  });
1795
2363
  if (debugger_.url) {
@@ -1819,9 +2387,23 @@ async function handleHook() {
1819
2387
  };
1820
2388
  }
1821
2389
  try {
1822
- await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
1823
- await Promise.all([eventShipper.flush(), traceShipper.flush()]);
1824
- await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2390
+ const remainingMs = Math.max(
2391
+ MIN_FLUSH_WINDOW_MS,
2392
+ hookFlushDeadlineMs() - (Date.now() - startedAtMs)
2393
+ );
2394
+ const outcome = await withDeadline(
2395
+ (async () => {
2396
+ await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
2397
+ await Promise.all([eventShipper.flush(), traceShipper.flush()]);
2398
+ await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
2399
+ })(),
2400
+ remainingMs
2401
+ );
2402
+ if (outcome === "deadline" && config.debug) {
2403
+ console.error(
2404
+ `[raindrop-ai/claude-code] hook work exceeded the ${hookFlushDeadlineMs()}ms deadline; exiting without waiting on the network (pending telemetry dropped)`
2405
+ );
2406
+ }
1825
2407
  } catch (err) {
1826
2408
  if (config.debug) {
1827
2409
  console.error(
@@ -2294,6 +2876,7 @@ async function executeTool(args2) {
2294
2876
  writeKey: config.writeKey,
2295
2877
  endpoint: config.endpoint,
2296
2878
  debug: false,
2879
+ projectId: config.projectId,
2297
2880
  enabled: true
2298
2881
  });
2299
2882
  try {
@@ -2465,7 +3048,7 @@ async function main() {
2465
3048
  }
2466
3049
  case "hook": {
2467
3050
  await handleHook();
2468
- break;
3051
+ process.exit(0);
2469
3052
  }
2470
3053
  case "mcp-serve": {
2471
3054
  await startMcpServer();