@raindrop-ai/opencode-plugin 0.0.12 → 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
@@ -5,7 +5,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // ../core/dist/chunk-OLYXZCOK.js
8
+ // ../core/dist/chunk-SK6EJEO7.js
9
9
  function getCrypto() {
10
10
  const c = globalThis.crypto;
11
11
  return c;
@@ -52,6 +52,8 @@ function base64Encode(bytes) {
52
52
  function generateId() {
53
53
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
54
54
  }
55
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
56
+ var MAX_RETRY_DELAY_MS = 3e4;
55
57
  function wait(ms) {
56
58
  return new Promise((resolve) => setTimeout(resolve, ms));
57
59
  }
@@ -59,6 +61,46 @@ function formatEndpoint(endpoint) {
59
61
  if (!endpoint) return void 0;
60
62
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
61
63
  }
64
+ function redactUrlForLog(url) {
65
+ try {
66
+ const parsed = new URL(url);
67
+ parsed.username = "";
68
+ parsed.password = "";
69
+ parsed.search = "";
70
+ parsed.hash = "";
71
+ return parsed.toString();
72
+ } catch (e) {
73
+ return "<unparseable-url>";
74
+ }
75
+ }
76
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
77
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
78
+ function rateLimitedLog(key, log) {
79
+ const now = Date.now();
80
+ const last = rateLimitedLogLast.get(key);
81
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
82
+ return false;
83
+ }
84
+ rateLimitedLogLast.set(key, now);
85
+ log();
86
+ return true;
87
+ }
88
+ async function raceWithTimeout(promise, timeoutMs) {
89
+ let timer;
90
+ const settledInTime = await Promise.race([
91
+ promise.then(
92
+ () => true,
93
+ () => true
94
+ ),
95
+ new Promise((resolve) => {
96
+ var _a;
97
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
98
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
99
+ })
100
+ ]);
101
+ if (timer) clearTimeout(timer);
102
+ return settledInTime;
103
+ }
62
104
  function parseRetryAfter(headers) {
63
105
  var _a;
64
106
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -75,12 +117,12 @@ function parseRetryAfter(headers) {
75
117
  function getRetryDelayMs(attemptNumber, previousError) {
76
118
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
77
119
  const v = previousError.retryAfterMs;
78
- if (typeof v === "number") return Math.max(0, v);
120
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
79
121
  }
80
122
  if (attemptNumber <= 1) return 0;
81
123
  const base = 500;
82
124
  const factor = Math.pow(2, attemptNumber - 2);
83
- return base * factor;
125
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
84
126
  }
85
127
  async function withRetry(operation, opName, opts) {
86
128
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -115,7 +157,9 @@ async function withRetry(operation, opName, opts) {
115
157
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
116
158
  }
117
159
  async function postJson(url, body, headers, opts) {
118
- const opName = `POST ${url}`;
160
+ var _a;
161
+ const opName = `POST ${redactUrlForLog(url)}`;
162
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
119
163
  await withRetry(
120
164
  async () => {
121
165
  const resp = await fetch(url, {
@@ -124,7 +168,8 @@ async function postJson(url, body, headers, opts) {
124
168
  "Content-Type": "application/json",
125
169
  ...headers
126
170
  },
127
- body: JSON.stringify(body)
171
+ body: JSON.stringify(body),
172
+ signal: AbortSignal.timeout(timeoutMs)
128
173
  });
129
174
  if (!resp.ok) {
130
175
  const text = await resp.text().catch(() => "");
@@ -141,6 +186,27 @@ async function postJson(url, body, headers, opts) {
141
186
  opts
142
187
  );
143
188
  }
189
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
190
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
191
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
192
+ function resolveMaxTextFieldChars(value) {
193
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
194
+ return Math.floor(value);
195
+ }
196
+ return currentDefaultMaxTextFieldChars;
197
+ }
198
+ function truncateToLimit(text, limit) {
199
+ if (limit > TRUNCATION_MARKER.length) {
200
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
201
+ }
202
+ return text.slice(0, Math.max(0, limit));
203
+ }
204
+ function capText(value, limit) {
205
+ if (typeof value !== "string") return value;
206
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
207
+ if (value.length <= max) return value;
208
+ return truncateToLimit(value, max);
209
+ }
144
210
  var SpanStatusCode = {
145
211
  UNSET: 0,
146
212
  OK: 1,
@@ -283,6 +349,27 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
283
349
  }).catch(() => {
284
350
  });
285
351
  }
352
+ var PROJECT_ID_HEADER = "X-Raindrop-Project-Id";
353
+ var PROJECT_ID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
354
+ function isValidProjectIdSlug(value) {
355
+ return PROJECT_ID_SLUG_PATTERN.test(value);
356
+ }
357
+ function normalizeProjectId(raw, opts) {
358
+ if (typeof raw !== "string") return void 0;
359
+ const trimmed = raw.trim();
360
+ if (!trimmed) return void 0;
361
+ if (!isValidProjectIdSlug(trimmed) && opts.debug) {
362
+ console.warn(
363
+ `${opts.prefix} projectId "${trimmed}" does not match slug ${PROJECT_ID_SLUG_PATTERN.source}; sending anyway \u2014 backend may reject with HTTP 400`
364
+ );
365
+ }
366
+ return trimmed;
367
+ }
368
+ function projectIdHeaders(projectId) {
369
+ return projectId ? { [PROJECT_ID_HEADER]: projectId } : {};
370
+ }
371
+ var SHUTDOWN_DEADLINE_MS = 1e4;
372
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
286
373
  function mergePatches(target, source) {
287
374
  var _a, _b, _c, _d;
288
375
  const out = { ...target, ...source };
@@ -300,6 +387,7 @@ var EventShipper = class {
300
387
  this.sticky = /* @__PURE__ */ new Map();
301
388
  this.timers = /* @__PURE__ */ new Map();
302
389
  this.inFlight = /* @__PURE__ */ new Set();
390
+ this.hasShutdown = false;
303
391
  var _a, _b, _c, _d, _e, _f, _g, _h;
304
392
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
305
393
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -309,10 +397,15 @@ var EventShipper = class {
309
397
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
310
398
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
311
399
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
400
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
312
401
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
313
402
  if (this.debug && this.localDebuggerUrl) {
314
403
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
315
404
  }
405
+ this.projectId = normalizeProjectId(opts.projectId, {
406
+ debug: this.debug,
407
+ prefix: this.prefix
408
+ });
316
409
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
317
410
  this.context = {
318
411
  library: {
@@ -331,10 +424,55 @@ var EventShipper = class {
331
424
  authHeaders() {
332
425
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
333
426
  }
427
+ requestHeaders() {
428
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
429
+ }
430
+ /**
431
+ * Build the retry/timeout options for one POST, honoring the shutdown
432
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
433
+ * the caller must drop the payload (with a rate-limited warning) instead
434
+ * of issuing a request that could outlive process exit.
435
+ *
436
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
437
+ * path is mid-drain takes effect immediately: no further retries, and the
438
+ * per-attempt timeout is clamped to the remaining window. After
439
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
440
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
441
+ * run as a single short attempt rather than regaining the full retry
442
+ * schedule.
443
+ */
444
+ requestOpts() {
445
+ if (this.shutdownDeadlineAt !== void 0) {
446
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
447
+ if (remainingMs <= 0) return null;
448
+ return {
449
+ maxAttempts: 1,
450
+ debug: this.debug,
451
+ sdkName: this.sdkName,
452
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
453
+ };
454
+ }
455
+ if (this.hasShutdown) {
456
+ return {
457
+ maxAttempts: 1,
458
+ debug: this.debug,
459
+ sdkName: this.sdkName,
460
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
461
+ };
462
+ }
463
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
464
+ }
334
465
  async patch(eventId, patch) {
335
466
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
336
467
  if (!this.enabled) return;
337
468
  if (!eventId || !eventId.trim()) return;
469
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
470
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
471
+ patch = { ...patch, input: capText(patch.input, maxChars) };
472
+ }
473
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
474
+ patch = { ...patch, output: capText(patch.output, maxChars) };
475
+ }
338
476
  if (this.debug) {
339
477
  console.log(`${this.prefix} queue patch`, {
340
478
  eventId,
@@ -381,9 +519,16 @@ var EventShipper = class {
381
519
  })));
382
520
  }
383
521
  async shutdown() {
384
- for (const t of this.timers.values()) clearTimeout(t);
385
- this.timers.clear();
386
- await this.flush();
522
+ this.hasShutdown = true;
523
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
524
+ try {
525
+ for (const t of this.timers.values()) clearTimeout(t);
526
+ this.timers.clear();
527
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
528
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
529
+ } finally {
530
+ this.shutdownDeadlineAt = void 0;
531
+ }
387
532
  }
388
533
  async trackSignal(signal) {
389
534
  var _a, _b;
@@ -405,15 +550,19 @@ var EventShipper = class {
405
550
  ];
406
551
  if (!this.writeKey) return;
407
552
  const url = `${this.baseUrl}signals/track`;
553
+ const opts = this.requestOpts();
554
+ if (!opts) {
555
+ this.warnShutdownDrop("signal");
556
+ return;
557
+ }
408
558
  try {
409
- await postJson(url, body, this.authHeaders(), {
410
- maxAttempts: 3,
411
- debug: this.debug,
412
- sdkName: this.sdkName
413
- });
559
+ await postJson(url, body, this.requestHeaders(), opts);
414
560
  } catch (err) {
415
561
  const msg = err instanceof Error ? err.message : String(err);
416
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
562
+ rateLimitedLog(
563
+ `${this.prefix}.send_signal_failed`,
564
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
565
+ );
417
566
  }
418
567
  }
419
568
  async identify(users) {
@@ -437,17 +586,27 @@ var EventShipper = class {
437
586
  if (!this.writeKey) return;
438
587
  if (body.length === 0) return;
439
588
  const url = `${this.baseUrl}users/identify`;
589
+ const opts = this.requestOpts();
590
+ if (!opts) {
591
+ this.warnShutdownDrop("identify");
592
+ return;
593
+ }
440
594
  try {
441
- await postJson(url, body, this.authHeaders(), {
442
- maxAttempts: 3,
443
- debug: this.debug,
444
- sdkName: this.sdkName
445
- });
595
+ await postJson(url, body, this.requestHeaders(), opts);
446
596
  } catch (err) {
447
597
  const msg = err instanceof Error ? err.message : String(err);
448
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
598
+ rateLimitedLog(
599
+ `${this.prefix}.send_identify_failed`,
600
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
601
+ );
449
602
  }
450
603
  }
604
+ warnShutdownDrop(what) {
605
+ rateLimitedLog(
606
+ `${this.prefix}.shutdown_deadline`,
607
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
608
+ );
609
+ }
451
610
  async flushOne(eventId) {
452
611
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
453
612
  if (!this.enabled) return;
@@ -523,11 +682,13 @@ var EventShipper = class {
523
682
  if (!isPending) this.sticky.delete(eventId);
524
683
  return;
525
684
  }
526
- const p = postJson(url, payload, this.authHeaders(), {
527
- maxAttempts: 3,
528
- debug: this.debug,
529
- sdkName: this.sdkName
530
- });
685
+ const opts = this.requestOpts();
686
+ if (!opts) {
687
+ this.warnShutdownDrop(`track_partial ${eventId}`);
688
+ if (!isPending) this.sticky.delete(eventId);
689
+ return;
690
+ }
691
+ const p = postJson(url, payload, this.requestHeaders(), opts);
531
692
  this.inFlight.add(p);
532
693
  try {
533
694
  try {
@@ -537,7 +698,10 @@ var EventShipper = class {
537
698
  }
538
699
  } catch (err) {
539
700
  const msg = err instanceof Error ? err.message : String(err);
540
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
701
+ rateLimitedLog(
702
+ `${this.prefix}.send_track_partial_failed`,
703
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
704
+ );
541
705
  }
542
706
  } finally {
543
707
  this.inFlight.delete(p);
@@ -547,10 +711,113 @@ var EventShipper = class {
547
711
  }
548
712
  }
549
713
  };
714
+ var DEFAULT_SECRET_KEY_NAMES = [
715
+ "apikey",
716
+ "apisecret",
717
+ "apitoken",
718
+ "secretaccesskey",
719
+ "sessiontoken",
720
+ "privatekey",
721
+ "privatekeyid",
722
+ "clientsecret",
723
+ "accesstoken",
724
+ "refreshtoken",
725
+ "oauthtoken",
726
+ "bearertoken",
727
+ "authorization",
728
+ "password",
729
+ "passphrase"
730
+ ];
731
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
732
+ function normalizeKeyName(name) {
733
+ return name.toLowerCase().replace(/[-_.]/g, "");
734
+ }
735
+ function redactSecretsInObject(value, options) {
736
+ var _a, _b;
737
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
738
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
739
+ const seen = /* @__PURE__ */ new WeakSet();
740
+ const walk = (node) => {
741
+ if (node === null || typeof node !== "object") return node;
742
+ if (seen.has(node)) return "[CIRCULAR]";
743
+ seen.add(node);
744
+ if (Array.isArray(node)) {
745
+ return node.map((item) => walk(item));
746
+ }
747
+ const out = {};
748
+ for (const [k, v] of Object.entries(node)) {
749
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
750
+ out[k] = placeholder;
751
+ } else {
752
+ out[k] = walk(v);
753
+ }
754
+ }
755
+ return out;
756
+ };
757
+ return walk(value);
758
+ }
759
+ function buildSecretSet(names) {
760
+ const set = /* @__PURE__ */ new Set();
761
+ for (const name of names) set.add(normalizeKeyName(name));
762
+ return set;
763
+ }
764
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
765
+ "ai.request.providerOptions",
766
+ "ai.response.providerMetadata"
767
+ ];
768
+ function defaultTransformSpan(span) {
769
+ const attrs = span.attributes;
770
+ if (!attrs || attrs.length === 0) return span;
771
+ let nextAttrs;
772
+ for (let i = 0; i < attrs.length; i++) {
773
+ const attr = attrs[i];
774
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
775
+ if (redacted === void 0) continue;
776
+ if (!nextAttrs) nextAttrs = attrs.slice();
777
+ nextAttrs[i] = { key: attr.key, value: redacted };
778
+ }
779
+ if (!nextAttrs) return span;
780
+ return { ...span, attributes: nextAttrs };
781
+ }
782
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
783
+ function redactJsonAttributeValue(key, value) {
784
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
785
+ const json = value.stringValue;
786
+ if (typeof json !== "string" || json.length === 0) return void 0;
787
+ let parsed;
788
+ try {
789
+ parsed = JSON.parse(json);
790
+ } catch (e) {
791
+ return void 0;
792
+ }
793
+ const scrubbed = redactSecretsInObject(parsed);
794
+ let scrubbedJson;
795
+ try {
796
+ scrubbedJson = JSON.stringify(scrubbed);
797
+ } catch (e) {
798
+ return void 0;
799
+ }
800
+ if (scrubbedJson === json) return void 0;
801
+ return { stringValue: scrubbedJson };
802
+ }
803
+ function applyOtelSpanAttributeLimit(limit) {
804
+ var _a, _b;
805
+ try {
806
+ 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;
807
+ if (!raw) return limit;
808
+ const parsed = Number.parseInt(raw, 10);
809
+ if (Number.isFinite(parsed) && parsed > 0) {
810
+ return Math.min(limit, parsed);
811
+ }
812
+ } catch (e) {
813
+ }
814
+ return limit;
815
+ }
550
816
  var TraceShipper = class {
551
817
  constructor(opts) {
552
818
  this.queue = [];
553
819
  this.inFlight = /* @__PURE__ */ new Set();
820
+ this.hasShutdown = false;
554
821
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
555
822
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
556
823
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -568,6 +835,79 @@ var TraceShipper = class {
568
835
  if (this.debug && this.localDebuggerUrl) {
569
836
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
570
837
  }
838
+ this.projectId = normalizeProjectId(opts.projectId, {
839
+ debug: this.debug,
840
+ prefix: this.prefix
841
+ });
842
+ this.transformSpanHook = opts.transformSpan;
843
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
844
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
845
+ }
846
+ /**
847
+ * Cap every string attribute value on the span. O(#attributes) length
848
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
849
+ * pipeline so the default secret-scrub still sees parseable JSON in
850
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
851
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
852
+ * in the surviving prefix).
853
+ *
854
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
855
+ * for span content, matching the Python SDK and the OTel SDK convention.
856
+ */
857
+ capSpanAttributes(span) {
858
+ var _a;
859
+ const maxChars = applyOtelSpanAttributeLimit(
860
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
861
+ );
862
+ const attrs = span.attributes;
863
+ if (!attrs || attrs.length === 0) return span;
864
+ let nextAttrs;
865
+ for (let i = 0; i < attrs.length; i++) {
866
+ const attr = attrs[i];
867
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
868
+ if (typeof value !== "string" || value.length <= maxChars) continue;
869
+ if (!nextAttrs) nextAttrs = attrs.slice();
870
+ nextAttrs[i] = {
871
+ key: attr.key,
872
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
873
+ };
874
+ }
875
+ if (!nextAttrs) return span;
876
+ return { ...span, attributes: nextAttrs };
877
+ }
878
+ /**
879
+ * Apply the user `transformSpan` hook (if any) followed by the default
880
+ * redactor (unless disabled). Returns either the (possibly new) span to
881
+ * ship, or `null` to drop the span entirely.
882
+ *
883
+ * Ordering: user hook runs first so callers can rewrite the span freely
884
+ * (rename attrs, add new ones, scrub things the default doesn't know
885
+ * about). The default redactor then runs on whatever the user produced,
886
+ * acting as the always-on floor for documented BYOK secrets. If the user
887
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
888
+ *
889
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
890
+ * hook can never accidentally ship raw, un-redacted spans.
891
+ */
892
+ redactSpan(span) {
893
+ let current = span;
894
+ if (this.transformSpanHook) {
895
+ try {
896
+ const result = this.transformSpanHook(current);
897
+ if (result === null) return null;
898
+ if (result !== void 0) current = result;
899
+ } catch (err) {
900
+ if (this.debug) {
901
+ const msg = err instanceof Error ? err.message : String(err);
902
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
903
+ }
904
+ return null;
905
+ }
906
+ }
907
+ if (!this.disableDefaultRedaction) {
908
+ current = defaultTransformSpan(current);
909
+ }
910
+ return this.capSpanAttributes(current);
571
911
  }
572
912
  isDebugEnabled() {
573
913
  return this.debug;
@@ -575,6 +915,9 @@ var TraceShipper = class {
575
915
  authHeaders() {
576
916
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
577
917
  }
918
+ requestHeaders() {
919
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
920
+ }
578
921
  startSpan(args) {
579
922
  var _a, _b;
580
923
  const ids = createSpanIds(args.parent);
@@ -585,8 +928,8 @@ var TraceShipper = class {
585
928
  ];
586
929
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
587
930
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
588
- if (this.localDebuggerUrl) {
589
- const openSpan = buildOtlpSpan({
931
+ this.mirrorToLocalDebugger(
932
+ buildOtlpSpan({
590
933
  ids: span.ids,
591
934
  name: span.name,
592
935
  startTimeUnixNano: span.startTimeUnixNano,
@@ -594,16 +937,21 @@ var TraceShipper = class {
594
937
  // placeholder — will be updated on endSpan
595
938
  attributes: span.attributes,
596
939
  status: { code: SpanStatusCode.UNSET }
597
- });
598
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
599
- mirrorTraceExportToLocalDebugger(body, {
600
- baseUrl: this.localDebuggerUrl,
601
- debug: false,
602
- sdkName: this.sdkName
603
- });
604
- }
940
+ })
941
+ );
605
942
  return span;
606
943
  }
944
+ mirrorToLocalDebugger(span) {
945
+ if (!this.localDebuggerUrl) return;
946
+ const redacted = this.redactSpan(span);
947
+ if (redacted === null) return;
948
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
949
+ mirrorTraceExportToLocalDebugger(body, {
950
+ baseUrl: this.localDebuggerUrl,
951
+ debug: false,
952
+ sdkName: this.sdkName
953
+ });
954
+ }
607
955
  endSpan(span, extra) {
608
956
  var _a, _b;
609
957
  if (span.endTimeUnixNano) return;
@@ -625,14 +973,7 @@ var TraceShipper = class {
625
973
  status
626
974
  });
627
975
  this.enqueue(otlp);
628
- if (this.localDebuggerUrl) {
629
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
630
- mirrorTraceExportToLocalDebugger(body, {
631
- baseUrl: this.localDebuggerUrl,
632
- debug: false,
633
- sdkName: this.sdkName
634
- });
635
- }
976
+ this.mirrorToLocalDebugger(otlp);
636
977
  }
637
978
  createSpan(args) {
638
979
  var _a;
@@ -650,14 +991,7 @@ var TraceShipper = class {
650
991
  status: args.status
651
992
  });
652
993
  this.enqueue(otlp);
653
- if (this.localDebuggerUrl) {
654
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
655
- mirrorTraceExportToLocalDebugger(body, {
656
- baseUrl: this.localDebuggerUrl,
657
- debug: false,
658
- sdkName: this.sdkName
659
- });
660
- }
994
+ this.mirrorToLocalDebugger(otlp);
661
995
  }
662
996
  enqueue(span) {
663
997
  if (!this.enabled) return;
@@ -669,10 +1003,12 @@ var TraceShipper = class {
669
1003
  )}`
670
1004
  );
671
1005
  }
1006
+ const redacted = this.redactSpan(span);
1007
+ if (redacted === null) return;
672
1008
  if (this.queue.length >= this.maxQueueSize) {
673
1009
  this.queue.shift();
674
1010
  }
675
- this.queue.push(span);
1011
+ this.queue.push(redacted);
676
1012
  if (this.queue.length >= this.maxBatchSize) {
677
1013
  void this.flush().catch(() => {
678
1014
  });
@@ -695,6 +1031,16 @@ var TraceShipper = class {
695
1031
  while (this.queue.length > 0) {
696
1032
  const batch = this.queue.splice(0, this.maxBatchSize);
697
1033
  if (!this.writeKey) continue;
1034
+ const opts = this.requestOpts();
1035
+ if (!opts) {
1036
+ rateLimitedLog(
1037
+ `${this.prefix}.shutdown_deadline`,
1038
+ () => console.warn(
1039
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1040
+ )
1041
+ );
1042
+ continue;
1043
+ }
698
1044
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
699
1045
  const url = `${this.baseUrl}traces`;
700
1046
  if (this.debug) {
@@ -703,11 +1049,7 @@ var TraceShipper = class {
703
1049
  endpoint: url
704
1050
  });
705
1051
  }
706
- const p = postJson(url, body, this.authHeaders(), {
707
- maxAttempts: 3,
708
- debug: this.debug,
709
- sdkName: this.sdkName
710
- });
1052
+ const p = postJson(url, body, this.requestHeaders(), opts);
711
1053
  this.inFlight.add(p);
712
1054
  try {
713
1055
  try {
@@ -715,21 +1057,61 @@ var TraceShipper = class {
715
1057
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
716
1058
  } catch (err) {
717
1059
  const msg = err instanceof Error ? err.message : String(err);
718
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1060
+ rateLimitedLog(
1061
+ `${this.prefix}.send_spans_failed`,
1062
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1063
+ );
719
1064
  }
720
1065
  } finally {
721
1066
  this.inFlight.delete(p);
722
1067
  }
723
1068
  }
724
1069
  }
1070
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1071
+ requestOpts() {
1072
+ if (this.shutdownDeadlineAt !== void 0) {
1073
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1074
+ if (remainingMs <= 0) return null;
1075
+ return {
1076
+ maxAttempts: 1,
1077
+ debug: this.debug,
1078
+ sdkName: this.sdkName,
1079
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1080
+ };
1081
+ }
1082
+ if (this.hasShutdown) {
1083
+ return {
1084
+ maxAttempts: 1,
1085
+ debug: this.debug,
1086
+ sdkName: this.sdkName,
1087
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1088
+ };
1089
+ }
1090
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1091
+ }
725
1092
  async shutdown() {
726
- if (this.timer) {
727
- clearTimeout(this.timer);
728
- this.timer = void 0;
1093
+ this.hasShutdown = true;
1094
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1095
+ try {
1096
+ if (this.timer) {
1097
+ clearTimeout(this.timer);
1098
+ this.timer = void 0;
1099
+ }
1100
+ const drain = async () => {
1101
+ await this.flush();
1102
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1103
+ })));
1104
+ };
1105
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1106
+ if (!settled) {
1107
+ rateLimitedLog(
1108
+ `${this.prefix}.shutdown_deadline`,
1109
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1110
+ );
1111
+ }
1112
+ } finally {
1113
+ this.shutdownDeadlineAt = void 0;
729
1114
  }
730
- await this.flush();
731
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
732
- })));
733
1115
  }
734
1116
  };
735
1117
 
@@ -742,7 +1124,7 @@ import { existsSync, readFileSync } from "fs";
742
1124
  import { homedir } from "os";
743
1125
  import { join } from "path";
744
1126
  function loadConfig(projectDirectory) {
745
- var _a, _b, _c, _d, _e, _f, _g;
1127
+ var _a, _b, _c, _d, _e, _f, _g, _h;
746
1128
  let merged = {};
747
1129
  const configPaths = [
748
1130
  join(homedir(), ".config", "opencode", "raindrop.json"),
@@ -769,9 +1151,10 @@ function loadConfig(projectDirectory) {
769
1151
  return {
770
1152
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
771
1153
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
772
- eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
773
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
774
- captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
1154
+ projectId: (_e = process.env["RAINDROP_PROJECT_ID"]) != null ? _e : merged.project_id,
1155
+ eventName: (_f = merged.event_name) != null ? _f : "opencode_session",
1156
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = merged.debug) != null ? _g : false,
1157
+ captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_h = merged.capture_system_prompt) != null ? _h : false,
775
1158
  eventMetadata,
776
1159
  localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url)
777
1160
  };
@@ -790,7 +1173,7 @@ function resolveLocalWorkshopUrl(fileValue) {
790
1173
  // package.json
791
1174
  var package_default = {
792
1175
  name: "@raindrop-ai/opencode-plugin",
793
- version: "0.0.12",
1176
+ version: "0.0.14",
794
1177
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
795
1178
  type: "module",
796
1179
  main: "dist/index.js",
@@ -900,6 +1283,86 @@ var TraceShipper2 = class extends TraceShipper {
900
1283
  }
901
1284
  };
902
1285
 
1286
+ // src/bounded.ts
1287
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1288
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1289
+ var MAX_BOUNDED_DEPTH = 12;
1290
+ function truncateToLimit2(text, limit) {
1291
+ if (text.length <= limit) return text;
1292
+ if (limit > TRUNCATION_MARKER2.length) {
1293
+ return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1294
+ }
1295
+ return text.slice(0, limit);
1296
+ }
1297
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1298
+ if (value.length <= limit) return value;
1299
+ return truncateToLimit2(value, limit);
1300
+ }
1301
+ function boundedClone2(value, budget, depth) {
1302
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1303
+ if (typeof value === "string") {
1304
+ if (value.length > budget.remaining) {
1305
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1306
+ budget.remaining = 0;
1307
+ return taken;
1308
+ }
1309
+ budget.remaining -= Math.max(value.length, 1);
1310
+ return value;
1311
+ }
1312
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1313
+ budget.remaining -= 8;
1314
+ return value;
1315
+ }
1316
+ if (typeof value !== "object") {
1317
+ budget.remaining -= 8;
1318
+ return value;
1319
+ }
1320
+ if (depth >= MAX_BOUNDED_DEPTH) {
1321
+ budget.remaining -= 16;
1322
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1323
+ }
1324
+ const withToJson = value;
1325
+ if (typeof withToJson.toJSON === "function") {
1326
+ try {
1327
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1328
+ } catch (e) {
1329
+ }
1330
+ }
1331
+ if (Array.isArray(value)) {
1332
+ const out2 = [];
1333
+ for (const item of value) {
1334
+ if (budget.remaining <= 0) {
1335
+ out2.push(TRUNCATION_MARKER2);
1336
+ break;
1337
+ }
1338
+ out2.push(boundedClone2(item, budget, depth + 1));
1339
+ }
1340
+ return out2;
1341
+ }
1342
+ const out = {};
1343
+ for (const key of Object.keys(value)) {
1344
+ if (budget.remaining <= 0) {
1345
+ out["..."] = TRUNCATION_MARKER2;
1346
+ break;
1347
+ }
1348
+ budget.remaining -= Math.max(key.length, 1);
1349
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1350
+ }
1351
+ return out;
1352
+ }
1353
+ function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
1354
+ var _a;
1355
+ try {
1356
+ if (typeof value === "string") {
1357
+ return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
1358
+ }
1359
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1360
+ return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
1361
+ } catch (e) {
1362
+ return capText2(String(value), limit);
1363
+ }
1364
+ }
1365
+
903
1366
  // src/tracing-linkage-helpers.ts
904
1367
  function createSessionParentMapHelpers({
905
1368
  sessions: sessions2,
@@ -977,6 +1440,15 @@ function createSessionParentMapHelpers({
977
1440
  }
978
1441
 
979
1442
  // src/tracing.ts
1443
+ var ERROR_LOG_INTERVAL_MS = 3e4;
1444
+ var lastErrorLogAt = /* @__PURE__ */ new Map();
1445
+ function rateLimitedErrorLog(key, message) {
1446
+ const now = Date.now();
1447
+ const last = lastErrorLogAt.get(key);
1448
+ if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
1449
+ lastErrorLogAt.set(key, now);
1450
+ console.log(message);
1451
+ }
980
1452
  var sessions = /* @__PURE__ */ new Map();
981
1453
  var taskContexts = /* @__PURE__ */ new Map();
982
1454
  var runningTaskCallsBySession = /* @__PURE__ */ new Map();
@@ -1129,7 +1601,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1129
1601
  const state = sessions.get(partSessionID);
1130
1602
  if (!state) return;
1131
1603
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
1132
- state.outputParts.set(messageId, partObj["text"]);
1604
+ state.outputParts.set(messageId, capText2(partObj["text"]));
1133
1605
  } else if (partObj["type"] === "tool" && messageId) {
1134
1606
  const tool = partObj["tool"];
1135
1607
  const callID = partObj["callID"];
@@ -1159,7 +1631,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1159
1631
  if (typeof errorMessage !== "string" || errorMessage === "")
1160
1632
  throw new Error("message.part.updated: error tool state must have error string");
1161
1633
  const input = toolState["input"];
1162
- const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? input : JSON.stringify(input);
1634
+ const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
1163
1635
  if (endPendingToolSpanWithError(
1164
1636
  state,
1165
1637
  callID,
@@ -1172,7 +1644,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1172
1644
  }
1173
1645
  }
1174
1646
  } else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
1175
- state.reasoningParts.set(messageId, partObj["text"]);
1647
+ state.reasoningParts.set(messageId, capText2(partObj["text"]));
1176
1648
  }
1177
1649
  } else if (event.type === "message.updated") {
1178
1650
  const msgInfo = props["info"];
@@ -1439,7 +1911,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1439
1911
  sessions.delete(String(errorSessionID));
1440
1912
  }
1441
1913
  } catch (err) {
1442
- console.log(
1914
+ rateLimitedErrorLog(
1915
+ "event",
1443
1916
  `[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
1444
1917
  );
1445
1918
  }
@@ -1495,7 +1968,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1495
1968
  ...promptMeta,
1496
1969
  properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
1497
1970
  };
1498
- const userText = textParts.join("\n");
1971
+ const userText = capText2(textParts.join("\n"));
1499
1972
  state.currentInput = userText;
1500
1973
  if (state.parentId) {
1501
1974
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
@@ -1604,7 +2077,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1604
2077
  });
1605
2078
  }
1606
2079
  } catch (err) {
1607
- console.log(
2080
+ rateLimitedErrorLog(
2081
+ "tool.execute.before",
1608
2082
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
1609
2083
  );
1610
2084
  }
@@ -1619,7 +2093,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1619
2093
  if (!state || !state.currentEventId) return;
1620
2094
  if (args === void 0 || args === null)
1621
2095
  throw new Error("tool.execute.after: args is required");
1622
- const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
2096
+ const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
1623
2097
  const startInfo = state.toolSpanStarts.get(callID);
1624
2098
  state.toolSpanStarts.delete(callID);
1625
2099
  const resultMetadata = result.metadata;
@@ -1632,13 +2106,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1632
2106
  const endTimeUnixNano = nowUnixNanoString();
1633
2107
  if (!("output" in result) || result.output === void 0)
1634
2108
  throw new Error("tool.execute.after: result.output is required");
1635
- let toolResult;
1636
- try {
1637
- toolResult = JSON.stringify(result.output);
1638
- } catch (err) {
1639
- const msg = err instanceof Error ? err.message : String(err);
1640
- throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
1641
- }
2109
+ const toolResult = boundedStringify(result.output);
1642
2110
  if (startInfo.liveSpan) {
1643
2111
  traceShipper.endSpan(startInfo.liveSpan, {
1644
2112
  attributes: [
@@ -1667,7 +2135,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1667
2135
  markTaskCallFinished(sessionID, callID);
1668
2136
  }
1669
2137
  } catch (err) {
1670
- console.log(
2138
+ rateLimitedErrorLog(
2139
+ "tool.execute.after",
1671
2140
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1672
2141
  );
1673
2142
  }
@@ -1684,7 +2153,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1684
2153
  state.isCompacting = true;
1685
2154
  }
1686
2155
  } catch (err) {
1687
- console.log(
2156
+ rateLimitedErrorLog(
2157
+ "experimental.session.compacting",
1688
2158
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1689
2159
  );
1690
2160
  }
@@ -1703,11 +2173,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1703
2173
  sessions.set(sessionID, state);
1704
2174
  }
1705
2175
  const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1706
- const TRUNCATION_MARKER = "\n...[truncated]";
2176
+ const TRUNCATION_MARKER3 = "\n...[truncated]";
1707
2177
  const joined = output.system.join("\n\n");
1708
- state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
2178
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
1709
2179
  } catch (err) {
1710
- console.log(
2180
+ rateLimitedErrorLog(
2181
+ "experimental.chat.system.transform",
1711
2182
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1712
2183
  );
1713
2184
  }
@@ -1743,12 +2214,14 @@ async function plugin(input) {
1743
2214
  writeKey: config.writeKey,
1744
2215
  endpoint: config.endpoint,
1745
2216
  debug: config.debug,
2217
+ projectId: config.projectId,
1746
2218
  localDebuggerUrl: config.localWorkshopUrl
1747
2219
  });
1748
2220
  const traceShipper = new TraceShipper2({
1749
2221
  writeKey: config.writeKey,
1750
2222
  endpoint: config.endpoint,
1751
2223
  debug: config.debug,
2224
+ projectId: config.projectId,
1752
2225
  localDebuggerUrl: config.localWorkshopUrl
1753
2226
  });
1754
2227
  const worktree = (_a = input.worktree) != null ? _a : input.directory;