@raindrop-ai/claude-code 0.0.1 → 0.0.3

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
@@ -6,7 +6,7 @@ import { randomUUID as randomUUID2 } from "crypto";
6
6
  import { tmpdir } from "os";
7
7
  import { join } from "path";
8
8
 
9
- // ../core/dist/chunk-H6VSZSLN.js
9
+ // ../core/dist/chunk-4UCYIEH4.js
10
10
  function getCrypto() {
11
11
  const c = globalThis.crypto;
12
12
  return c;
@@ -442,11 +442,28 @@ var EventShipper = class {
442
442
  }
443
443
  }
444
444
  };
445
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
446
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
447
+ var _a, _b, _c;
448
+ 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;
449
+ return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
450
+ }
451
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
452
+ var _a;
453
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
454
+ if (!baseUrl) return;
455
+ void postJson(`${baseUrl}traces`, body, {}, {
456
+ maxAttempts: 1,
457
+ debug: (_a = options.debug) != null ? _a : false,
458
+ sdkName: options.sdkName
459
+ }).catch(() => {
460
+ });
461
+ }
445
462
  var TraceShipper = class {
446
463
  constructor(opts) {
447
464
  this.queue = [];
448
465
  this.inFlight = /* @__PURE__ */ new Set();
449
- var _a, _b, _c, _d, _e, _f, _g, _h;
466
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
450
467
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
451
468
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
452
469
  this.enabled = opts.enabled !== false;
@@ -459,6 +476,13 @@ var TraceShipper = class {
459
476
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
460
477
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
461
478
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
479
+ const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
480
+ if (localDebugger) {
481
+ this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
482
+ if (this.debug) {
483
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
484
+ }
485
+ }
462
486
  }
463
487
  isDebugEnabled() {
464
488
  return this.debug;
@@ -475,7 +499,25 @@ var TraceShipper = class {
475
499
  attrString("ai.operationId", args2.operationId)
476
500
  ];
477
501
  if ((_b = args2.attributes) == null ? void 0 : _b.length) attrs.push(...args2.attributes);
478
- return { ids, name: args2.name, startTimeUnixNano: started, attributes: attrs };
502
+ const span = { ids, name: args2.name, startTimeUnixNano: started, attributes: attrs };
503
+ if (this.localDebuggerUrl) {
504
+ const openSpan = buildOtlpSpan({
505
+ ids: span.ids,
506
+ name: span.name,
507
+ startTimeUnixNano: span.startTimeUnixNano,
508
+ endTimeUnixNano: span.startTimeUnixNano,
509
+ // placeholder — will be updated on endSpan
510
+ attributes: span.attributes,
511
+ status: { code: SpanStatusCode.UNSET }
512
+ });
513
+ const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
514
+ mirrorTraceExportToLocalDebugger(body, {
515
+ baseUrl: this.localDebuggerUrl,
516
+ debug: false,
517
+ sdkName: this.sdkName
518
+ });
519
+ }
520
+ return span;
479
521
  }
480
522
  endSpan(span, extra) {
481
523
  var _a, _b;
@@ -498,6 +540,14 @@ var TraceShipper = class {
498
540
  status
499
541
  });
500
542
  this.enqueue(otlp);
543
+ if (this.localDebuggerUrl) {
544
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
545
+ mirrorTraceExportToLocalDebugger(body, {
546
+ baseUrl: this.localDebuggerUrl,
547
+ debug: false,
548
+ sdkName: this.sdkName
549
+ });
550
+ }
501
551
  }
502
552
  createSpan(args2) {
503
553
  var _a;
@@ -515,6 +565,14 @@ var TraceShipper = class {
515
565
  status: args2.status
516
566
  });
517
567
  this.enqueue(otlp);
568
+ if (this.localDebuggerUrl) {
569
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
570
+ mirrorTraceExportToLocalDebugger(body, {
571
+ baseUrl: this.localDebuggerUrl,
572
+ debug: false,
573
+ sdkName: this.sdkName
574
+ });
575
+ }
518
576
  }
519
577
  enqueue(span) {
520
578
  if (!this.enabled) return;
@@ -969,12 +1027,12 @@ async function handleSessionEnd(payload, eventId, config, properties, eventShipp
969
1027
  }
970
1028
 
971
1029
  // src/config.ts
972
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1030
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
973
1031
  import { homedir, userInfo } from "os";
974
- import { join as join2 } from "path";
1032
+ import { dirname, join as join2 } from "path";
975
1033
  var CONFIG_PATH = join2(homedir(), ".config", "raindrop", "config.json");
976
1034
  function loadConfig() {
977
- var _a, _b, _c, _d, _e, _f, _g;
1035
+ var _a, _b, _c, _d, _e, _f, _g, _h;
978
1036
  let file = {};
979
1037
  try {
980
1038
  if (existsSync2(CONFIG_PATH)) {
@@ -993,15 +1051,125 @@ function loadConfig() {
993
1051
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : file.write_key) != null ? _b : "",
994
1052
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : file.api_url) != null ? _d : "https://api.raindrop.ai/v1",
995
1053
  userId: (_f = (_e = process.env["RAINDROP_USER_ID"]) != null ? _e : file.user_id) != null ? _f : systemUser,
996
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false
1054
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false,
1055
+ enabled: (_h = file.enabled) != null ? _h : true
997
1056
  };
998
1057
  }
999
1058
  function getConfigPath() {
1000
1059
  return CONFIG_PATH;
1001
1060
  }
1061
+ function updateConfig(patch) {
1062
+ const dir = dirname(CONFIG_PATH);
1063
+ mkdirSync2(dir, { recursive: true });
1064
+ let existing = {};
1065
+ try {
1066
+ if (existsSync2(CONFIG_PATH)) {
1067
+ existing = JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
1068
+ }
1069
+ } catch (e) {
1070
+ }
1071
+ writeFileSync2(CONFIG_PATH, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n", "utf-8");
1072
+ }
1073
+
1074
+ // src/local-debugger.ts
1075
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1076
+ import { tmpdir as tmpdir2 } from "os";
1077
+ import { join as join3 } from "path";
1078
+ var DEFAULT_PORT = 5899;
1079
+ var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1080
+ var HEALTH_TIMEOUT_MS = 300;
1081
+ var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
1082
+ var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
1083
+ var CACHE_TTL_MS = 5e3;
1084
+ function readCache() {
1085
+ try {
1086
+ if (!existsSync3(CACHE_FILE)) return void 0;
1087
+ const data = JSON.parse(readFileSync3(CACHE_FILE, "utf-8"));
1088
+ if (Date.now() - data.ts > CACHE_TTL_MS) return void 0;
1089
+ return data;
1090
+ } catch (e) {
1091
+ return void 0;
1092
+ }
1093
+ }
1094
+ function writeCache(url) {
1095
+ try {
1096
+ mkdirSync3(CACHE_DIR, { recursive: true });
1097
+ writeFileSync3(CACHE_FILE, JSON.stringify({ url, ts: Date.now() }), "utf-8");
1098
+ } catch (e) {
1099
+ }
1100
+ }
1101
+ async function healthPing(baseUrl) {
1102
+ const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1103
+ try {
1104
+ const controller = new AbortController();
1105
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1106
+ const resp = await fetch(healthUrl, { signal: controller.signal });
1107
+ clearTimeout(timeout);
1108
+ if (resp.ok) {
1109
+ const body = await resp.json();
1110
+ if (body.ok) return baseUrl;
1111
+ }
1112
+ } catch (e) {
1113
+ }
1114
+ return null;
1115
+ }
1116
+ async function detectLocalDebugger(debug) {
1117
+ var _a;
1118
+ const envUrl = resolveLocalDebuggerBaseUrl();
1119
+ if (envUrl) {
1120
+ if (debug) {
1121
+ console.log(`[raindrop-ai/claude-code] Local debugger configured via ${LOCAL_DEBUGGER_ENV_VAR}: ${envUrl}`);
1122
+ }
1123
+ return { url: envUrl, autoDetected: false };
1124
+ }
1125
+ const cached = readCache();
1126
+ if (cached) {
1127
+ if (debug) {
1128
+ console.log(`[raindrop-ai/claude-code] Local debugger cache hit: ${(_a = cached.url) != null ? _a : "not running"}`);
1129
+ }
1130
+ return { url: cached.url, autoDetected: cached.url !== null };
1131
+ }
1132
+ if (debug) {
1133
+ console.log(`[raindrop-ai/claude-code] Probing local debugger at localhost:${DEFAULT_PORT}...`);
1134
+ }
1135
+ const detected = await healthPing(DEFAULT_URL);
1136
+ writeCache(detected);
1137
+ if (detected && debug) {
1138
+ console.log(`[raindrop-ai/claude-code] Local debugger auto-detected at ${detected}`);
1139
+ }
1140
+ return { url: detected, autoDetected: detected !== null };
1141
+ }
1142
+ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1143
+ const url = `${baseUrl}events/track_partial`;
1144
+ if (debug) {
1145
+ console.log(`[raindrop-ai/claude-code] Mirroring event to local debugger: ${url}`);
1146
+ }
1147
+ void fetch(url, {
1148
+ method: "POST",
1149
+ headers: { "Content-Type": "application/json" },
1150
+ body: JSON.stringify(payload)
1151
+ }).catch(() => {
1152
+ });
1153
+ }
1002
1154
 
1003
1155
  // src/hook-handler.ts
1156
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1157
+ import { join as join4 } from "path";
1158
+ import { tmpdir as tmpdir3 } from "os";
1004
1159
  var STDIN_TIMEOUT_MS = 5e3;
1160
+ var STATE_DIR2 = join4(tmpdir3(), "raindrop-claude-code");
1161
+ function getOtlpTraceId(sessionId) {
1162
+ try {
1163
+ const key = `rootspan_${sessionId}`.replace(/[^a-zA-Z0-9_\-]/g, "_");
1164
+ const filePath = join4(STATE_DIR2, key);
1165
+ if (!existsSync4(filePath)) return void 0;
1166
+ const ctx = JSON.parse(readFileSync4(filePath, "utf-8"));
1167
+ if (!ctx.traceIdB64) return void 0;
1168
+ return Buffer.from(ctx.traceIdB64, "base64").toString("hex");
1169
+ } catch (e) {
1170
+ return void 0;
1171
+ }
1172
+ }
1005
1173
  function readStdin() {
1006
1174
  return new Promise((resolve2) => {
1007
1175
  if (process.stdin.isTTY) {
@@ -1041,7 +1209,16 @@ async function handleHook() {
1041
1209
  return;
1042
1210
  }
1043
1211
  const config = loadConfig();
1044
- if (!config.writeKey) {
1212
+ if (!config.enabled) {
1213
+ return;
1214
+ }
1215
+ const debugger_ = await detectLocalDebugger(config.debug);
1216
+ if (debugger_.url && debugger_.autoDetected) {
1217
+ process.env[LOCAL_DEBUGGER_ENV_VAR] = debugger_.url;
1218
+ }
1219
+ const hasCloudKey = Boolean(config.writeKey);
1220
+ const hasDebugger = Boolean(debugger_.url);
1221
+ if (!hasCloudKey && !hasDebugger) {
1045
1222
  return;
1046
1223
  }
1047
1224
  const mapperConfig = {
@@ -1051,13 +1228,44 @@ async function handleHook() {
1051
1228
  const eventShipper = new EventShipper2({
1052
1229
  writeKey: config.writeKey,
1053
1230
  endpoint: config.endpoint,
1054
- debug: config.debug
1231
+ debug: config.debug,
1232
+ // Only ship to the cloud when we have an API key. When only the local
1233
+ // debugger is present the shipper still needs to be instantiated so the
1234
+ // event mapper can call patch/finish, but cloud POSTs are skipped.
1235
+ enabled: hasCloudKey
1055
1236
  });
1056
1237
  const traceShipper = new TraceShipper2({
1057
1238
  writeKey: config.writeKey,
1058
1239
  endpoint: config.endpoint,
1059
- debug: config.debug
1240
+ debug: config.debug,
1241
+ enabled: hasCloudKey
1060
1242
  });
1243
+ if (debugger_.url) {
1244
+ const debuggerUrl = debugger_.url;
1245
+ const origPatch = eventShipper.patch.bind(eventShipper);
1246
+ eventShipper.patch = async (eventId, patch) => {
1247
+ var _a, _b, _c, _d;
1248
+ const traceId = (_a = getOtlpTraceId(payload.session_id)) != null ? _a : payload.session_id;
1249
+ mirrorEventToLocalDebugger(debuggerUrl, {
1250
+ event_id: eventId,
1251
+ user_id: (_b = patch.userId) != null ? _b : config.userId,
1252
+ event: (_c = patch.eventName) != null ? _c : "claude_code_session",
1253
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1254
+ ai_data: {
1255
+ input: patch.input,
1256
+ output: patch.output,
1257
+ model: patch.model,
1258
+ convo_id: patch.convoId
1259
+ },
1260
+ properties: {
1261
+ ...patch.properties,
1262
+ ...payload.session_id ? { trace_id: traceId } : {}
1263
+ },
1264
+ is_pending: (_d = patch.isPending) != null ? _d : true
1265
+ }, config.debug);
1266
+ await origPatch(eventId, patch);
1267
+ };
1268
+ }
1061
1269
  try {
1062
1270
  await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
1063
1271
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
@@ -1072,17 +1280,18 @@ async function handleHook() {
1072
1280
  }
1073
1281
 
1074
1282
  // src/setup.ts
1075
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1283
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1076
1284
  import { homedir as homedir2, userInfo as userInfo2 } from "os";
1077
- import { dirname, join as join3, resolve } from "path";
1285
+ import { dirname as dirname2, join as join5, resolve } from "path";
1078
1286
  import { createInterface } from "readline";
1287
+ import { execSync } from "child_process";
1079
1288
  function getClaudeSettingsPath(scope, cwd) {
1080
1289
  if (scope === "project") {
1081
1290
  return resolve(cwd, ".claude", "settings.json");
1082
1291
  }
1083
- return join3(homedir2(), ".claude", "settings.json");
1292
+ return join5(homedir2(), ".claude", "settings.json");
1084
1293
  }
1085
- var HOOK_EVENTS = [
1294
+ var CORE_HOOK_EVENTS = [
1086
1295
  "SessionStart",
1087
1296
  "UserPromptSubmit",
1088
1297
  "PreToolUse",
@@ -1090,15 +1299,58 @@ var HOOK_EVENTS = [
1090
1299
  "PostToolUseFailure",
1091
1300
  "SubagentStart",
1092
1301
  "SubagentStop",
1093
- "PermissionDenied",
1094
- "PostCompact",
1095
1302
  "Stop",
1096
- "StopFailure",
1097
1303
  "SessionEnd"
1098
1304
  ];
1099
- function makeHookConfig() {
1305
+ var VERSIONED_HOOK_EVENTS = [
1306
+ { minVersion: "2.1.78", events: ["StopFailure"] },
1307
+ { minVersion: "2.1.76", events: ["PostCompact"] },
1308
+ { minVersion: "2.1.89", events: ["PermissionDenied"] }
1309
+ ];
1310
+ function parseVersion(version) {
1311
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
1312
+ if (!match) return void 0;
1313
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
1314
+ }
1315
+ function isVersionAtLeast(current, required) {
1316
+ const req = parseVersion(required);
1317
+ if (!req) return false;
1318
+ for (let i = 0; i < 3; i++) {
1319
+ if (current[i] > req[i]) return true;
1320
+ if (current[i] < req[i]) return false;
1321
+ }
1322
+ return true;
1323
+ }
1324
+ function detectClaudeCodeVersion() {
1325
+ try {
1326
+ const output = execSync("claude --version", {
1327
+ encoding: "utf-8",
1328
+ timeout: 5e3,
1329
+ stdio: ["ignore", "pipe", "ignore"]
1330
+ }).trim();
1331
+ const match = output.match(/(\d+\.\d+\.\d+)/);
1332
+ return match ? match[1] : void 0;
1333
+ } catch (e) {
1334
+ return void 0;
1335
+ }
1336
+ }
1337
+ function getHookEventsForVersion(version) {
1338
+ const events = [...CORE_HOOK_EVENTS];
1339
+ if (!version) {
1340
+ return events;
1341
+ }
1342
+ const parsed = parseVersion(version);
1343
+ if (!parsed) return events;
1344
+ for (const { minVersion, events: versionedEvents } of VERSIONED_HOOK_EVENTS) {
1345
+ if (isVersionAtLeast(parsed, minVersion)) {
1346
+ events.push(...versionedEvents);
1347
+ }
1348
+ }
1349
+ return events;
1350
+ }
1351
+ function makeHookConfig(hookEvents) {
1100
1352
  const hooks = {};
1101
- for (const event of HOOK_EVENTS) {
1353
+ for (const event of hookEvents) {
1102
1354
  hooks[event] = [
1103
1355
  {
1104
1356
  hooks: [
@@ -1129,10 +1381,10 @@ async function runSetup(args2) {
1129
1381
  const scopeLabel = scope === "project" ? "project" : "global";
1130
1382
  console.log("\n Raindrop \xD7 Claude Code \u2014 Setup\n");
1131
1383
  let writeKey = (_c = (_b = args2.writeKey) != null ? _b : process.env["RAINDROP_WRITE_KEY"]) != null ? _c : "";
1132
- if (!writeKey) {
1384
+ if (!writeKey && !args2.localOnly) {
1133
1385
  writeKey = await prompt(" Enter your Raindrop write key: ");
1134
1386
  }
1135
- if (!writeKey) {
1387
+ if (!writeKey && !args2.localOnly) {
1136
1388
  console.error("\n Error: write key is required. Get it from https://app.raindrop.ai\n");
1137
1389
  process.exit(1);
1138
1390
  }
@@ -1144,37 +1396,47 @@ async function runSetup(args2) {
1144
1396
  userId = "unknown";
1145
1397
  }
1146
1398
  }
1147
- const configPath = getConfigPath();
1148
- const configDir = dirname(configPath);
1149
- mkdirSync2(configDir, { recursive: true });
1150
- let existingConfig = {};
1151
- try {
1152
- if (existsSync3(configPath)) {
1153
- existingConfig = JSON.parse(readFileSync3(configPath, "utf-8"));
1399
+ if (writeKey) {
1400
+ const configPath = getConfigPath();
1401
+ const configDir = dirname2(configPath);
1402
+ mkdirSync4(configDir, { recursive: true });
1403
+ let existingConfig = {};
1404
+ try {
1405
+ if (existsSync5(configPath)) {
1406
+ existingConfig = JSON.parse(readFileSync5(configPath, "utf-8"));
1407
+ }
1408
+ } catch (e) {
1154
1409
  }
1155
- } catch (e) {
1410
+ const raindropConfig = {
1411
+ ...existingConfig,
1412
+ write_key: writeKey,
1413
+ user_id: userId
1414
+ };
1415
+ writeFileSync4(configPath, JSON.stringify(raindropConfig, null, 2) + "\n", "utf-8");
1416
+ console.log(` Saved Raindrop config to ${configPath}`);
1417
+ }
1418
+ const ccVersion = detectClaudeCodeVersion();
1419
+ const hookEvents = getHookEventsForVersion(ccVersion);
1420
+ if (ccVersion) {
1421
+ console.log(` Detected Claude Code v${ccVersion}`);
1422
+ } else {
1423
+ console.log(` Could not detect Claude Code version \u2014 registering core hooks only.`);
1424
+ console.log(` Run setup again after installing Claude Code to enable all hooks.`);
1156
1425
  }
1157
- const raindropConfig = {
1158
- ...existingConfig,
1159
- write_key: writeKey,
1160
- user_id: userId
1161
- };
1162
- writeFileSync2(configPath, JSON.stringify(raindropConfig, null, 2) + "\n", "utf-8");
1163
- console.log(` Saved Raindrop config to ${configPath}`);
1164
1426
  const settingsPath = getClaudeSettingsPath(scope, process.cwd());
1165
- const settingsDir = dirname(settingsPath);
1166
- mkdirSync2(settingsDir, { recursive: true });
1427
+ const settingsDir = dirname2(settingsPath);
1428
+ mkdirSync4(settingsDir, { recursive: true });
1167
1429
  let settings = {};
1168
- if (existsSync3(settingsPath)) {
1430
+ if (existsSync5(settingsPath)) {
1169
1431
  try {
1170
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1432
+ settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
1171
1433
  } catch (e) {
1172
1434
  console.warn(` Warning: could not parse ${settingsPath}, creating fresh`);
1173
1435
  settings = {};
1174
1436
  }
1175
1437
  }
1176
1438
  const existingHooks = (_e = settings["hooks"]) != null ? _e : {};
1177
- const newHooks = makeHookConfig();
1439
+ const newHooks = makeHookConfig(hookEvents);
1178
1440
  for (const [event, hookGroups] of Object.entries(newHooks)) {
1179
1441
  const existing = existingHooks[event];
1180
1442
  if (!existing || !Array.isArray(existing)) {
@@ -1193,9 +1455,8 @@ async function runSetup(args2) {
1193
1455
  }
1194
1456
  }
1195
1457
  settings["hooks"] = existingHooks;
1196
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1458
+ writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1197
1459
  console.log(` Updated Claude Code hooks in ${settingsPath} (${scopeLabel})`);
1198
- const { execSync } = await import("child_process");
1199
1460
  const whichCmd = process.platform === "win32" ? "where" : "which";
1200
1461
  try {
1201
1462
  execSync(`${whichCmd} raindrop-claude-code`, { stdio: "ignore" });
@@ -1207,7 +1468,8 @@ async function runSetup(args2) {
1207
1468
  npm install -g @raindrop-ai/claude-code
1208
1469
  `);
1209
1470
  }
1210
- console.log(`
1471
+ if (writeKey) {
1472
+ console.log(`
1211
1473
  Done! Claude Code will now send telemetry to Raindrop.
1212
1474
  Scope: ${scopeLabel}
1213
1475
  User ID: ${userId}
@@ -1215,6 +1477,13 @@ async function runSetup(args2) {
1215
1477
  To verify, start a Claude Code session and check your Raindrop dashboard.
1216
1478
  Set RAINDROP_DEBUG=true for verbose logging.
1217
1479
  `);
1480
+ } else {
1481
+ console.log(`
1482
+ Done! Hooks installed (${scopeLabel}).
1483
+ No write key configured \u2014 events will only be sent to the local debugger.
1484
+ Start it with: npx @raindrop-ai/local-debugger
1485
+ `);
1486
+ }
1218
1487
  }
1219
1488
 
1220
1489
  // src/cli.ts
@@ -1233,7 +1502,8 @@ async function main() {
1233
1502
  await runSetup({
1234
1503
  writeKey: parseFlag("write-key"),
1235
1504
  userId: parseFlag("user-id"),
1236
- scope
1505
+ scope,
1506
+ localOnly: args.includes("--local-only")
1237
1507
  });
1238
1508
  break;
1239
1509
  }
@@ -1241,6 +1511,32 @@ async function main() {
1241
1511
  await handleHook();
1242
1512
  break;
1243
1513
  }
1514
+ case "status": {
1515
+ const config = loadConfig();
1516
+ const result = await detectLocalDebugger(config.debug);
1517
+ console.log("");
1518
+ if (result.url) {
1519
+ console.log(` Local debugger: \x1B[32m\u25CF connected\x1B[0m`);
1520
+ console.log(` URL: ${result.url}`);
1521
+ console.log(` Source: ${result.autoDetected ? "auto-detected" : "RAINDROP_LOCAL_DEBUGGER env var"}`);
1522
+ } else {
1523
+ console.log(` Local debugger: \x1B[2m\u25CB not detected\x1B[0m`);
1524
+ console.log(` Start it with: npx @dawn/local-debugger`);
1525
+ }
1526
+ console.log(` Hooks: ${config.enabled ? "\x1B[32menabled\x1B[0m" : "\x1B[2mdisabled\x1B[0m"}`);
1527
+ console.log("");
1528
+ break;
1529
+ }
1530
+ case "enable": {
1531
+ updateConfig({ enabled: true });
1532
+ console.log(" Raindrop hooks enabled.");
1533
+ break;
1534
+ }
1535
+ case "disable": {
1536
+ updateConfig({ enabled: false });
1537
+ console.log(" Raindrop hooks disabled.");
1538
+ break;
1539
+ }
1244
1540
  case "version":
1245
1541
  case "--version":
1246
1542
  case "-v": {
@@ -1261,16 +1557,21 @@ async function main() {
1261
1557
  --write-key=KEY Raindrop write key (or set RAINDROP_WRITE_KEY)
1262
1558
  --user-id=ID User identifier (defaults to system username)
1263
1559
  --scope=SCOPE "user" (global, default) or "project" (.claude/ in cwd)
1560
+ --local-only Install hooks without a write key (local debugger only)
1264
1561
 
1265
1562
  hook Handle a Claude Code hook event (reads JSON from stdin)
1563
+ status Check local debugger connectivity
1564
+ enable Enable Raindrop hooks
1565
+ disable Disable Raindrop hooks
1266
1566
  version Print version
1267
1567
  help Show this help message
1268
1568
 
1269
1569
  Environment:
1270
- RAINDROP_WRITE_KEY API write key (alternative to --write-key or config file)
1271
- RAINDROP_USER_ID User ID override
1272
- RAINDROP_API_URL Custom API endpoint
1273
- RAINDROP_DEBUG Set to "true" for verbose logging
1570
+ RAINDROP_WRITE_KEY API write key (alternative to --write-key or config file)
1571
+ RAINDROP_USER_ID User ID override
1572
+ RAINDROP_API_URL Custom API endpoint
1573
+ RAINDROP_LOCAL_DEBUGGER Local debugger URL (auto-detected if running on :5899)
1574
+ RAINDROP_DEBUG Set to "true" for verbose logging
1274
1575
  `);
1275
1576
  if (!command || command === "help" || command === "--help" || command === "-h") {
1276
1577
  process.exit(0);
package/dist/index.cjs CHANGED
@@ -24,13 +24,16 @@ __export(src_exports, {
24
24
  PACKAGE_NAME: () => PACKAGE_NAME,
25
25
  PACKAGE_VERSION: () => PACKAGE_VERSION,
26
26
  TraceShipper: () => TraceShipper2,
27
+ detectLocalDebugger: () => detectLocalDebugger,
27
28
  getConfigPath: () => getConfigPath,
28
29
  loadConfig: () => loadConfig,
29
- mapHookToRaindrop: () => mapHookToRaindrop
30
+ mapHookToRaindrop: () => mapHookToRaindrop,
31
+ mirrorEventToLocalDebugger: () => mirrorEventToLocalDebugger,
32
+ updateConfig: () => updateConfig
30
33
  });
31
34
  module.exports = __toCommonJS(src_exports);
32
35
 
33
- // ../core/dist/chunk-H6VSZSLN.js
36
+ // ../core/dist/chunk-4UCYIEH4.js
34
37
  function getCrypto() {
35
38
  const c = globalThis.crypto;
36
39
  return c;
@@ -466,11 +469,28 @@ var EventShipper = class {
466
469
  }
467
470
  }
468
471
  };
472
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
473
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
474
+ var _a, _b, _c;
475
+ 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;
476
+ return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
477
+ }
478
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
479
+ var _a;
480
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
481
+ if (!baseUrl) return;
482
+ void postJson(`${baseUrl}traces`, body, {}, {
483
+ maxAttempts: 1,
484
+ debug: (_a = options.debug) != null ? _a : false,
485
+ sdkName: options.sdkName
486
+ }).catch(() => {
487
+ });
488
+ }
469
489
  var TraceShipper = class {
470
490
  constructor(opts) {
471
491
  this.queue = [];
472
492
  this.inFlight = /* @__PURE__ */ new Set();
473
- var _a, _b, _c, _d, _e, _f, _g, _h;
493
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
474
494
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
475
495
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
476
496
  this.enabled = opts.enabled !== false;
@@ -483,6 +503,13 @@ var TraceShipper = class {
483
503
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
484
504
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
485
505
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
506
+ const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
507
+ if (localDebugger) {
508
+ this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
509
+ if (this.debug) {
510
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
511
+ }
512
+ }
486
513
  }
487
514
  isDebugEnabled() {
488
515
  return this.debug;
@@ -499,7 +526,25 @@ var TraceShipper = class {
499
526
  attrString("ai.operationId", args.operationId)
500
527
  ];
501
528
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
502
- return { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
529
+ const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
530
+ if (this.localDebuggerUrl) {
531
+ const openSpan = buildOtlpSpan({
532
+ ids: span.ids,
533
+ name: span.name,
534
+ startTimeUnixNano: span.startTimeUnixNano,
535
+ endTimeUnixNano: span.startTimeUnixNano,
536
+ // placeholder — will be updated on endSpan
537
+ attributes: span.attributes,
538
+ status: { code: SpanStatusCode.UNSET }
539
+ });
540
+ const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
541
+ mirrorTraceExportToLocalDebugger(body, {
542
+ baseUrl: this.localDebuggerUrl,
543
+ debug: false,
544
+ sdkName: this.sdkName
545
+ });
546
+ }
547
+ return span;
503
548
  }
504
549
  endSpan(span, extra) {
505
550
  var _a, _b;
@@ -522,6 +567,14 @@ var TraceShipper = class {
522
567
  status
523
568
  });
524
569
  this.enqueue(otlp);
570
+ if (this.localDebuggerUrl) {
571
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
572
+ mirrorTraceExportToLocalDebugger(body, {
573
+ baseUrl: this.localDebuggerUrl,
574
+ debug: false,
575
+ sdkName: this.sdkName
576
+ });
577
+ }
525
578
  }
526
579
  createSpan(args) {
527
580
  var _a;
@@ -539,6 +592,14 @@ var TraceShipper = class {
539
592
  status: args.status
540
593
  });
541
594
  this.enqueue(otlp);
595
+ if (this.localDebuggerUrl) {
596
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
597
+ mirrorTraceExportToLocalDebugger(body, {
598
+ baseUrl: this.localDebuggerUrl,
599
+ debug: false,
600
+ sdkName: this.sdkName
601
+ });
602
+ }
542
603
  }
543
604
  enqueue(span) {
544
605
  if (!this.enabled) return;
@@ -662,7 +723,7 @@ var import_node_os = require("os");
662
723
  var import_node_path = require("path");
663
724
  var CONFIG_PATH = (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "raindrop", "config.json");
664
725
  function loadConfig() {
665
- var _a, _b, _c, _d, _e, _f, _g;
726
+ var _a, _b, _c, _d, _e, _f, _g, _h;
666
727
  let file = {};
667
728
  try {
668
729
  if ((0, import_node_fs.existsSync)(CONFIG_PATH)) {
@@ -681,12 +742,25 @@ function loadConfig() {
681
742
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : file.write_key) != null ? _b : "",
682
743
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : file.api_url) != null ? _d : "https://api.raindrop.ai/v1",
683
744
  userId: (_f = (_e = process.env["RAINDROP_USER_ID"]) != null ? _e : file.user_id) != null ? _f : systemUser,
684
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false
745
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false,
746
+ enabled: (_h = file.enabled) != null ? _h : true
685
747
  };
686
748
  }
687
749
  function getConfigPath() {
688
750
  return CONFIG_PATH;
689
751
  }
752
+ function updateConfig(patch) {
753
+ const dir = (0, import_node_path.dirname)(CONFIG_PATH);
754
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
755
+ let existing = {};
756
+ try {
757
+ if ((0, import_node_fs.existsSync)(CONFIG_PATH)) {
758
+ existing = JSON.parse((0, import_node_fs.readFileSync)(CONFIG_PATH, "utf-8"));
759
+ }
760
+ } catch (e) {
761
+ }
762
+ (0, import_node_fs.writeFileSync)(CONFIG_PATH, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n", "utf-8");
763
+ }
690
764
 
691
765
  // src/event-mapper.ts
692
766
  var import_node_fs2 = require("fs");
@@ -1027,13 +1101,97 @@ async function handleSessionEnd(payload, eventId, config, properties, eventShipp
1027
1101
  }
1028
1102
  });
1029
1103
  }
1104
+
1105
+ // src/local-debugger.ts
1106
+ var import_node_fs3 = require("fs");
1107
+ var import_node_os3 = require("os");
1108
+ var import_node_path3 = require("path");
1109
+ var DEFAULT_PORT = 5899;
1110
+ var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1111
+ var HEALTH_TIMEOUT_MS = 300;
1112
+ var CACHE_DIR = (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "raindrop-claude-code");
1113
+ var CACHE_FILE = (0, import_node_path3.join)(CACHE_DIR, "debugger_cache");
1114
+ var CACHE_TTL_MS = 5e3;
1115
+ function readCache() {
1116
+ try {
1117
+ if (!(0, import_node_fs3.existsSync)(CACHE_FILE)) return void 0;
1118
+ const data = JSON.parse((0, import_node_fs3.readFileSync)(CACHE_FILE, "utf-8"));
1119
+ if (Date.now() - data.ts > CACHE_TTL_MS) return void 0;
1120
+ return data;
1121
+ } catch (e) {
1122
+ return void 0;
1123
+ }
1124
+ }
1125
+ function writeCache(url) {
1126
+ try {
1127
+ (0, import_node_fs3.mkdirSync)(CACHE_DIR, { recursive: true });
1128
+ (0, import_node_fs3.writeFileSync)(CACHE_FILE, JSON.stringify({ url, ts: Date.now() }), "utf-8");
1129
+ } catch (e) {
1130
+ }
1131
+ }
1132
+ async function healthPing(baseUrl) {
1133
+ const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1134
+ try {
1135
+ const controller = new AbortController();
1136
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1137
+ const resp = await fetch(healthUrl, { signal: controller.signal });
1138
+ clearTimeout(timeout);
1139
+ if (resp.ok) {
1140
+ const body = await resp.json();
1141
+ if (body.ok) return baseUrl;
1142
+ }
1143
+ } catch (e) {
1144
+ }
1145
+ return null;
1146
+ }
1147
+ async function detectLocalDebugger(debug) {
1148
+ var _a;
1149
+ const envUrl = resolveLocalDebuggerBaseUrl();
1150
+ if (envUrl) {
1151
+ if (debug) {
1152
+ console.log(`[raindrop-ai/claude-code] Local debugger configured via ${LOCAL_DEBUGGER_ENV_VAR}: ${envUrl}`);
1153
+ }
1154
+ return { url: envUrl, autoDetected: false };
1155
+ }
1156
+ const cached = readCache();
1157
+ if (cached) {
1158
+ if (debug) {
1159
+ console.log(`[raindrop-ai/claude-code] Local debugger cache hit: ${(_a = cached.url) != null ? _a : "not running"}`);
1160
+ }
1161
+ return { url: cached.url, autoDetected: cached.url !== null };
1162
+ }
1163
+ if (debug) {
1164
+ console.log(`[raindrop-ai/claude-code] Probing local debugger at localhost:${DEFAULT_PORT}...`);
1165
+ }
1166
+ const detected = await healthPing(DEFAULT_URL);
1167
+ writeCache(detected);
1168
+ if (detected && debug) {
1169
+ console.log(`[raindrop-ai/claude-code] Local debugger auto-detected at ${detected}`);
1170
+ }
1171
+ return { url: detected, autoDetected: detected !== null };
1172
+ }
1173
+ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1174
+ const url = `${baseUrl}events/track_partial`;
1175
+ if (debug) {
1176
+ console.log(`[raindrop-ai/claude-code] Mirroring event to local debugger: ${url}`);
1177
+ }
1178
+ void fetch(url, {
1179
+ method: "POST",
1180
+ headers: { "Content-Type": "application/json" },
1181
+ body: JSON.stringify(payload)
1182
+ }).catch(() => {
1183
+ });
1184
+ }
1030
1185
  // Annotate the CommonJS export names for ESM import in node:
1031
1186
  0 && (module.exports = {
1032
1187
  EventShipper,
1033
1188
  PACKAGE_NAME,
1034
1189
  PACKAGE_VERSION,
1035
1190
  TraceShipper,
1191
+ detectLocalDebugger,
1036
1192
  getConfigPath,
1037
1193
  loadConfig,
1038
- mapHookToRaindrop
1194
+ mapHookToRaindrop,
1195
+ mirrorEventToLocalDebugger,
1196
+ updateConfig
1039
1197
  });
package/dist/index.d.cts CHANGED
@@ -147,6 +147,8 @@ declare class TraceShipper$1 {
147
147
  private queue;
148
148
  private timer;
149
149
  private inFlight;
150
+ /** URL of the local debugger (from RAINDROP_LOCAL_DEBUGGER env var). */
151
+ private localDebuggerUrl;
150
152
  constructor(opts: TraceShipperOptions);
151
153
  isDebugEnabled(): boolean;
152
154
  private authHeaders;
@@ -219,11 +221,19 @@ declare class TraceShipper extends TraceShipper$1 {
219
221
  enqueue(span: OtlpSpan): void;
220
222
  }
221
223
 
224
+ interface ConfigFile {
225
+ write_key?: string;
226
+ api_url?: string;
227
+ user_id?: string;
228
+ debug?: boolean;
229
+ enabled?: boolean;
230
+ }
222
231
  interface RaindropConfig {
223
232
  writeKey: string;
224
233
  endpoint: string;
225
234
  userId: string;
226
235
  debug: boolean;
236
+ enabled: boolean;
227
237
  }
228
238
  /**
229
239
  * Load config with precedence (low -> high):
@@ -233,6 +243,7 @@ interface RaindropConfig {
233
243
  */
234
244
  declare function loadConfig(): RaindropConfig;
235
245
  declare function getConfigPath(): string;
246
+ declare function updateConfig(patch: Partial<ConfigFile>): void;
236
247
 
237
248
  type SetupScope = "user" | "project";
238
249
 
@@ -270,4 +281,25 @@ declare function mapHookToRaindrop(payload: HookPayload, config: MapperConfig, e
270
281
  declare const PACKAGE_NAME = "@raindrop-ai/claude-code";
271
282
  declare const PACKAGE_VERSION = "0.0.1";
272
283
 
273
- export { EventShipper, type HookPayload, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SetupScope, TraceShipper, getConfigPath, loadConfig, mapHookToRaindrop };
284
+ interface LocalDebuggerResult {
285
+ /** The resolved base URL (e.g. "http://localhost:5899/v1/"), or null if not detected. */
286
+ url: string | null;
287
+ /** Whether the debugger was auto-detected (vs explicitly configured via env var). */
288
+ autoDetected: boolean;
289
+ }
290
+ /**
291
+ * Detect whether the local debugger is available.
292
+ *
293
+ * Resolution order:
294
+ * 1. RAINDROP_LOCAL_DEBUGGER env var — use directly (no health check, trust the user)
295
+ * 2. Cached probe result (within TTL)
296
+ * 3. Probe http://localhost:5899/health with a short timeout
297
+ */
298
+ declare function detectLocalDebugger(debug: boolean): Promise<LocalDebuggerResult>;
299
+ /**
300
+ * Mirror an event payload to the local debugger's track_partial endpoint.
301
+ * Fire-and-forget — errors are silently swallowed.
302
+ */
303
+ declare function mirrorEventToLocalDebugger(baseUrl: string, payload: Record<string, unknown>, debug: boolean): void;
304
+
305
+ export { EventShipper, type HookPayload, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SetupScope, TraceShipper, detectLocalDebugger, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, updateConfig };
package/dist/index.d.ts CHANGED
@@ -147,6 +147,8 @@ declare class TraceShipper$1 {
147
147
  private queue;
148
148
  private timer;
149
149
  private inFlight;
150
+ /** URL of the local debugger (from RAINDROP_LOCAL_DEBUGGER env var). */
151
+ private localDebuggerUrl;
150
152
  constructor(opts: TraceShipperOptions);
151
153
  isDebugEnabled(): boolean;
152
154
  private authHeaders;
@@ -219,11 +221,19 @@ declare class TraceShipper extends TraceShipper$1 {
219
221
  enqueue(span: OtlpSpan): void;
220
222
  }
221
223
 
224
+ interface ConfigFile {
225
+ write_key?: string;
226
+ api_url?: string;
227
+ user_id?: string;
228
+ debug?: boolean;
229
+ enabled?: boolean;
230
+ }
222
231
  interface RaindropConfig {
223
232
  writeKey: string;
224
233
  endpoint: string;
225
234
  userId: string;
226
235
  debug: boolean;
236
+ enabled: boolean;
227
237
  }
228
238
  /**
229
239
  * Load config with precedence (low -> high):
@@ -233,6 +243,7 @@ interface RaindropConfig {
233
243
  */
234
244
  declare function loadConfig(): RaindropConfig;
235
245
  declare function getConfigPath(): string;
246
+ declare function updateConfig(patch: Partial<ConfigFile>): void;
236
247
 
237
248
  type SetupScope = "user" | "project";
238
249
 
@@ -270,4 +281,25 @@ declare function mapHookToRaindrop(payload: HookPayload, config: MapperConfig, e
270
281
  declare const PACKAGE_NAME = "@raindrop-ai/claude-code";
271
282
  declare const PACKAGE_VERSION = "0.0.1";
272
283
 
273
- export { EventShipper, type HookPayload, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SetupScope, TraceShipper, getConfigPath, loadConfig, mapHookToRaindrop };
284
+ interface LocalDebuggerResult {
285
+ /** The resolved base URL (e.g. "http://localhost:5899/v1/"), or null if not detected. */
286
+ url: string | null;
287
+ /** Whether the debugger was auto-detected (vs explicitly configured via env var). */
288
+ autoDetected: boolean;
289
+ }
290
+ /**
291
+ * Detect whether the local debugger is available.
292
+ *
293
+ * Resolution order:
294
+ * 1. RAINDROP_LOCAL_DEBUGGER env var — use directly (no health check, trust the user)
295
+ * 2. Cached probe result (within TTL)
296
+ * 3. Probe http://localhost:5899/health with a short timeout
297
+ */
298
+ declare function detectLocalDebugger(debug: boolean): Promise<LocalDebuggerResult>;
299
+ /**
300
+ * Mirror an event payload to the local debugger's track_partial endpoint.
301
+ * Fire-and-forget — errors are silently swallowed.
302
+ */
303
+ declare function mirrorEventToLocalDebugger(baseUrl: string, payload: Record<string, unknown>, debug: boolean): void;
304
+
305
+ export { EventShipper, type HookPayload, type LocalDebuggerResult, type MapperConfig, PACKAGE_NAME, PACKAGE_VERSION, type RaindropConfig, type SetupScope, TraceShipper, detectLocalDebugger, getConfigPath, loadConfig, mapHookToRaindrop, mirrorEventToLocalDebugger, updateConfig };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // ../core/dist/chunk-H6VSZSLN.js
1
+ // ../core/dist/chunk-4UCYIEH4.js
2
2
  function getCrypto() {
3
3
  const c = globalThis.crypto;
4
4
  return c;
@@ -434,11 +434,28 @@ var EventShipper = class {
434
434
  }
435
435
  }
436
436
  };
437
+ var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
438
+ function resolveLocalDebuggerBaseUrl(baseUrl) {
439
+ var _a, _b, _c;
440
+ const resolved = (_b = baseUrl != null ? baseUrl : typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[LOCAL_DEBUGGER_ENV_VAR] : void 0) != null ? _b : null;
441
+ return resolved ? (_c = formatEndpoint(resolved)) != null ? _c : null : null;
442
+ }
443
+ function mirrorTraceExportToLocalDebugger(body, options = {}) {
444
+ var _a;
445
+ const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
446
+ if (!baseUrl) return;
447
+ void postJson(`${baseUrl}traces`, body, {}, {
448
+ maxAttempts: 1,
449
+ debug: (_a = options.debug) != null ? _a : false,
450
+ sdkName: options.sdkName
451
+ }).catch(() => {
452
+ });
453
+ }
437
454
  var TraceShipper = class {
438
455
  constructor(opts) {
439
456
  this.queue = [];
440
457
  this.inFlight = /* @__PURE__ */ new Set();
441
- var _a, _b, _c, _d, _e, _f, _g, _h;
458
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
442
459
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
443
460
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
444
461
  this.enabled = opts.enabled !== false;
@@ -451,6 +468,13 @@ var TraceShipper = class {
451
468
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
452
469
  this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
453
470
  this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
471
+ const localDebugger = typeof process !== "undefined" ? (_i = process.env) == null ? void 0 : _i.RAINDROP_LOCAL_DEBUGGER : void 0;
472
+ if (localDebugger) {
473
+ this.localDebuggerUrl = (_j = resolveLocalDebuggerBaseUrl(localDebugger)) != null ? _j : void 0;
474
+ if (this.debug) {
475
+ console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
476
+ }
477
+ }
454
478
  }
455
479
  isDebugEnabled() {
456
480
  return this.debug;
@@ -467,7 +491,25 @@ var TraceShipper = class {
467
491
  attrString("ai.operationId", args.operationId)
468
492
  ];
469
493
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
470
- return { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
494
+ const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
495
+ if (this.localDebuggerUrl) {
496
+ const openSpan = buildOtlpSpan({
497
+ ids: span.ids,
498
+ name: span.name,
499
+ startTimeUnixNano: span.startTimeUnixNano,
500
+ endTimeUnixNano: span.startTimeUnixNano,
501
+ // placeholder — will be updated on endSpan
502
+ attributes: span.attributes,
503
+ status: { code: SpanStatusCode.UNSET }
504
+ });
505
+ const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
506
+ mirrorTraceExportToLocalDebugger(body, {
507
+ baseUrl: this.localDebuggerUrl,
508
+ debug: false,
509
+ sdkName: this.sdkName
510
+ });
511
+ }
512
+ return span;
471
513
  }
472
514
  endSpan(span, extra) {
473
515
  var _a, _b;
@@ -490,6 +532,14 @@ var TraceShipper = class {
490
532
  status
491
533
  });
492
534
  this.enqueue(otlp);
535
+ if (this.localDebuggerUrl) {
536
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
537
+ mirrorTraceExportToLocalDebugger(body, {
538
+ baseUrl: this.localDebuggerUrl,
539
+ debug: false,
540
+ sdkName: this.sdkName
541
+ });
542
+ }
493
543
  }
494
544
  createSpan(args) {
495
545
  var _a;
@@ -507,6 +557,14 @@ var TraceShipper = class {
507
557
  status: args.status
508
558
  });
509
559
  this.enqueue(otlp);
560
+ if (this.localDebuggerUrl) {
561
+ const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
562
+ mirrorTraceExportToLocalDebugger(body, {
563
+ baseUrl: this.localDebuggerUrl,
564
+ debug: false,
565
+ sdkName: this.sdkName
566
+ });
567
+ }
510
568
  }
511
569
  enqueue(span) {
512
570
  if (!this.enabled) return;
@@ -625,12 +683,12 @@ var TraceShipper2 = class extends TraceShipper {
625
683
  };
626
684
 
627
685
  // src/config.ts
628
- import { existsSync, readFileSync } from "fs";
686
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
629
687
  import { homedir, userInfo } from "os";
630
- import { join } from "path";
688
+ import { dirname, join } from "path";
631
689
  var CONFIG_PATH = join(homedir(), ".config", "raindrop", "config.json");
632
690
  function loadConfig() {
633
- var _a, _b, _c, _d, _e, _f, _g;
691
+ var _a, _b, _c, _d, _e, _f, _g, _h;
634
692
  let file = {};
635
693
  try {
636
694
  if (existsSync(CONFIG_PATH)) {
@@ -649,15 +707,28 @@ function loadConfig() {
649
707
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : file.write_key) != null ? _b : "",
650
708
  endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : file.api_url) != null ? _d : "https://api.raindrop.ai/v1",
651
709
  userId: (_f = (_e = process.env["RAINDROP_USER_ID"]) != null ? _e : file.user_id) != null ? _f : systemUser,
652
- debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false
710
+ debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = file.debug) != null ? _g : false,
711
+ enabled: (_h = file.enabled) != null ? _h : true
653
712
  };
654
713
  }
655
714
  function getConfigPath() {
656
715
  return CONFIG_PATH;
657
716
  }
717
+ function updateConfig(patch) {
718
+ const dir = dirname(CONFIG_PATH);
719
+ mkdirSync(dir, { recursive: true });
720
+ let existing = {};
721
+ try {
722
+ if (existsSync(CONFIG_PATH)) {
723
+ existing = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
724
+ }
725
+ } catch (e) {
726
+ }
727
+ writeFileSync(CONFIG_PATH, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n", "utf-8");
728
+ }
658
729
 
659
730
  // src/event-mapper.ts
660
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
731
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
661
732
  import { randomUUID as randomUUID2 } from "crypto";
662
733
  import { tmpdir } from "os";
663
734
  import { join as join2 } from "path";
@@ -679,7 +750,7 @@ function safeStringify(value) {
679
750
  var STATE_DIR = join2(tmpdir(), "raindrop-claude-code");
680
751
  function ensureStateDir() {
681
752
  try {
682
- mkdirSync(STATE_DIR, { recursive: true });
753
+ mkdirSync2(STATE_DIR, { recursive: true });
683
754
  } catch (e) {
684
755
  }
685
756
  }
@@ -697,7 +768,7 @@ function statePath(key) {
697
768
  function writeState(key, value) {
698
769
  try {
699
770
  ensureStateDir();
700
- writeFileSync(statePath(key), value, "utf-8");
771
+ writeFileSync2(statePath(key), value, "utf-8");
701
772
  } catch (e) {
702
773
  }
703
774
  }
@@ -995,12 +1066,96 @@ async function handleSessionEnd(payload, eventId, config, properties, eventShipp
995
1066
  }
996
1067
  });
997
1068
  }
1069
+
1070
+ // src/local-debugger.ts
1071
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1072
+ import { tmpdir as tmpdir2 } from "os";
1073
+ import { join as join3 } from "path";
1074
+ var DEFAULT_PORT = 5899;
1075
+ var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
1076
+ var HEALTH_TIMEOUT_MS = 300;
1077
+ var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
1078
+ var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
1079
+ var CACHE_TTL_MS = 5e3;
1080
+ function readCache() {
1081
+ try {
1082
+ if (!existsSync3(CACHE_FILE)) return void 0;
1083
+ const data = JSON.parse(readFileSync3(CACHE_FILE, "utf-8"));
1084
+ if (Date.now() - data.ts > CACHE_TTL_MS) return void 0;
1085
+ return data;
1086
+ } catch (e) {
1087
+ return void 0;
1088
+ }
1089
+ }
1090
+ function writeCache(url) {
1091
+ try {
1092
+ mkdirSync3(CACHE_DIR, { recursive: true });
1093
+ writeFileSync3(CACHE_FILE, JSON.stringify({ url, ts: Date.now() }), "utf-8");
1094
+ } catch (e) {
1095
+ }
1096
+ }
1097
+ async function healthPing(baseUrl) {
1098
+ const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
1099
+ try {
1100
+ const controller = new AbortController();
1101
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1102
+ const resp = await fetch(healthUrl, { signal: controller.signal });
1103
+ clearTimeout(timeout);
1104
+ if (resp.ok) {
1105
+ const body = await resp.json();
1106
+ if (body.ok) return baseUrl;
1107
+ }
1108
+ } catch (e) {
1109
+ }
1110
+ return null;
1111
+ }
1112
+ async function detectLocalDebugger(debug) {
1113
+ var _a;
1114
+ const envUrl = resolveLocalDebuggerBaseUrl();
1115
+ if (envUrl) {
1116
+ if (debug) {
1117
+ console.log(`[raindrop-ai/claude-code] Local debugger configured via ${LOCAL_DEBUGGER_ENV_VAR}: ${envUrl}`);
1118
+ }
1119
+ return { url: envUrl, autoDetected: false };
1120
+ }
1121
+ const cached = readCache();
1122
+ if (cached) {
1123
+ if (debug) {
1124
+ console.log(`[raindrop-ai/claude-code] Local debugger cache hit: ${(_a = cached.url) != null ? _a : "not running"}`);
1125
+ }
1126
+ return { url: cached.url, autoDetected: cached.url !== null };
1127
+ }
1128
+ if (debug) {
1129
+ console.log(`[raindrop-ai/claude-code] Probing local debugger at localhost:${DEFAULT_PORT}...`);
1130
+ }
1131
+ const detected = await healthPing(DEFAULT_URL);
1132
+ writeCache(detected);
1133
+ if (detected && debug) {
1134
+ console.log(`[raindrop-ai/claude-code] Local debugger auto-detected at ${detected}`);
1135
+ }
1136
+ return { url: detected, autoDetected: detected !== null };
1137
+ }
1138
+ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
1139
+ const url = `${baseUrl}events/track_partial`;
1140
+ if (debug) {
1141
+ console.log(`[raindrop-ai/claude-code] Mirroring event to local debugger: ${url}`);
1142
+ }
1143
+ void fetch(url, {
1144
+ method: "POST",
1145
+ headers: { "Content-Type": "application/json" },
1146
+ body: JSON.stringify(payload)
1147
+ }).catch(() => {
1148
+ });
1149
+ }
998
1150
  export {
999
1151
  EventShipper2 as EventShipper,
1000
1152
  PACKAGE_NAME,
1001
1153
  PACKAGE_VERSION,
1002
1154
  TraceShipper2 as TraceShipper,
1155
+ detectLocalDebugger,
1003
1156
  getConfigPath,
1004
1157
  loadConfig,
1005
- mapHookToRaindrop
1158
+ mapHookToRaindrop,
1159
+ mirrorEventToLocalDebugger,
1160
+ updateConfig
1006
1161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raindrop-ai/claude-code",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Raindrop observability for Claude Code CLI — automatic session, tool call, and prompt tracing via hooks",
5
5
  "license": "MIT",
6
6
  "type": "module",