@rulemetric/cli 0.5.3 → 0.6.0

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.
Files changed (160) hide show
  1. package/bin/dev.js +7 -0
  2. package/dist/base-command.js +4 -4
  3. package/dist/{chunk-7DRLX3DN.js → chunk-3POYC5NA.js} +3 -3
  4. package/dist/{chunk-TJ2M5QVL.js → chunk-3QH73QOQ.js} +2 -2
  5. package/dist/{chunk-CXWY5XKD.js → chunk-4LHEN2FS.js} +8 -8
  6. package/dist/{chunk-CROYS75W.js → chunk-7K4ILWS7.js} +3 -3
  7. package/dist/{chunk-TDTHAOXO.js → chunk-DR5WLIOU.js} +2 -2
  8. package/dist/{chunk-ANOWI23I.js → chunk-HWGRTIXN.js} +98 -105
  9. package/dist/chunk-HWGRTIXN.js.map +7 -0
  10. package/dist/{chunk-T76YQCKX.js → chunk-IATQXFJA.js} +2 -2
  11. package/dist/{chunk-346TGPVR.js → chunk-JCYCAQ6U.js} +126 -33
  12. package/dist/chunk-JCYCAQ6U.js.map +7 -0
  13. package/dist/{chunk-AUUHXCJU.js → chunk-JEJ3J5J6.js} +2 -2
  14. package/dist/{chunk-OAQG5WBV.js → chunk-LAKZUE35.js} +2 -2
  15. package/dist/{chunk-JNFIYBFL.js → chunk-N2X35ITW.js} +7 -1
  16. package/dist/{chunk-JNFIYBFL.js.map → chunk-N2X35ITW.js.map} +2 -2
  17. package/dist/{chunk-CCSR73HF.js → chunk-N5A2IQAK.js} +2 -2
  18. package/dist/{chunk-VCG4Q53B.js → chunk-OTOWLUOJ.js} +2 -2
  19. package/dist/{chunk-FTJRMUID.js → chunk-OVGP4OGK.js} +25 -1
  20. package/dist/chunk-OVGP4OGK.js.map +7 -0
  21. package/dist/{chunk-OKCFYPIQ.js → chunk-RCMXTLQF.js} +2 -2
  22. package/dist/{chunk-CGUV6TUA.js → chunk-RUCSRN7F.js} +2 -2
  23. package/dist/{chunk-4UJATBTG.js → chunk-SLL3OBKN.js} +4 -4
  24. package/dist/{chunk-CB43W53J.js → chunk-SZ7VDCD6.js} +3 -3
  25. package/dist/{chunk-N52AHNBH.js → chunk-W4MZXKZ2.js} +6 -2
  26. package/dist/chunk-W4MZXKZ2.js.map +7 -0
  27. package/dist/{chunk-KHJUQFIK.js → chunk-YSEQWCN4.js} +2 -2
  28. package/dist/{chunk-CWFVBGX2.js → chunk-ZRLYHBPN.js} +4 -2
  29. package/dist/chunk-ZRLYHBPN.js.map +7 -0
  30. package/dist/{chunk-ER5DPPHB.js → chunk-ZSGKRCKD.js} +6 -3
  31. package/dist/chunk-ZSGKRCKD.js.map +7 -0
  32. package/dist/commands/antigravity/backfill.js +5 -5
  33. package/dist/commands/auth/create-key.js +4 -4
  34. package/dist/commands/auth/login.js +19 -5
  35. package/dist/commands/auth/login.js.map +2 -2
  36. package/dist/commands/auth/logout.js +4 -4
  37. package/dist/commands/auth/revoke-key.js +4 -4
  38. package/dist/commands/auth/status.js +4 -4
  39. package/dist/commands/catalog/index.js +4 -4
  40. package/dist/commands/catalog/recommend.js +4 -4
  41. package/dist/commands/convert.js +6 -5
  42. package/dist/commands/convert.js.map +2 -2
  43. package/dist/commands/converters.js +4 -4
  44. package/dist/commands/crons/list.js +4 -4
  45. package/dist/commands/crons/run.js +4 -4
  46. package/dist/commands/dashboard.js +4 -4
  47. package/dist/commands/eval-targets/create.js +4 -4
  48. package/dist/commands/eval-targets/list.js +4 -4
  49. package/dist/commands/evals/agent.js +12 -12
  50. package/dist/commands/evals/benchmark.js +5 -5
  51. package/dist/commands/evals/compare.js +4 -4
  52. package/dist/commands/evals/create.js +4 -4
  53. package/dist/commands/evals/optimize-description.js +4 -4
  54. package/dist/commands/evals/optimize.js +4 -4
  55. package/dist/commands/evals/run.js +4 -4
  56. package/dist/commands/gateway/ensure.js +5 -5
  57. package/dist/commands/hooks/install.js +88 -55
  58. package/dist/commands/hooks/install.js.map +2 -2
  59. package/dist/commands/hooks/run.js +9 -7
  60. package/dist/commands/hooks/run.js.map +2 -2
  61. package/dist/commands/hooks/uninstall.js +57 -11
  62. package/dist/commands/hooks/uninstall.js.map +2 -2
  63. package/dist/commands/import.js +4 -4
  64. package/dist/commands/instructions/create.js +4 -4
  65. package/dist/commands/instructions/delete.js +4 -4
  66. package/dist/commands/instructions/fork.js +4 -4
  67. package/dist/commands/instructions/get.js +4 -4
  68. package/dist/commands/instructions/list.js +4 -4
  69. package/dist/commands/instructions/promote.js +4 -4
  70. package/dist/commands/instructions/pull.js +4 -4
  71. package/dist/commands/instructions/upstream.js +4 -4
  72. package/dist/commands/instructions/versions.js +4 -4
  73. package/dist/commands/launcher.js +4 -4
  74. package/dist/commands/org/current.js +9 -9
  75. package/dist/commands/org/list.js +6 -6
  76. package/dist/commands/org/switch.js +7 -7
  77. package/dist/commands/packs/add-instruction.js +4 -4
  78. package/dist/commands/packs/apply.js +4 -4
  79. package/dist/commands/packs/create.js +4 -4
  80. package/dist/commands/packs/fork.js +4 -4
  81. package/dist/commands/packs/get.js +4 -4
  82. package/dist/commands/packs/list.js +4 -4
  83. package/dist/commands/packs/remove-instruction.js +4 -4
  84. package/dist/commands/proxy/env.js +5 -5
  85. package/dist/commands/proxy/logs.js +4 -4
  86. package/dist/commands/proxy/restart.js +4 -4
  87. package/dist/commands/proxy/setup.js +4 -4
  88. package/dist/commands/proxy/start.js +4 -4
  89. package/dist/commands/proxy/status.js +5 -5
  90. package/dist/commands/proxy/stop.js +4 -4
  91. package/dist/commands/research/backfill.js +5 -5
  92. package/dist/commands/research/tail-transcript.js +5 -5
  93. package/dist/commands/service/install.js +12 -6
  94. package/dist/commands/service/install.js.map +2 -2
  95. package/dist/commands/service/status.js +4 -4
  96. package/dist/commands/service/uninstall.js +4 -4
  97. package/dist/commands/sessions/analyze.js +4 -4
  98. package/dist/commands/sessions/end.js +4 -4
  99. package/dist/commands/sessions/import.js +4 -4
  100. package/dist/commands/sessions/list.js +4 -4
  101. package/dist/commands/sessions/start.js +6 -6
  102. package/dist/commands/setup.js +10 -10
  103. package/dist/commands/setup.js.map +2 -2
  104. package/dist/commands/skills/export-approved.js +4 -4
  105. package/dist/commands/skills/install.js +4 -4
  106. package/dist/commands/skills/list.js +4 -4
  107. package/dist/commands/skills/publish.js +4 -4
  108. package/dist/commands/skills/search.js +4 -4
  109. package/dist/commands/skills/update.js +4 -4
  110. package/dist/commands/suggestions/accept.js +4 -4
  111. package/dist/commands/suggestions/dismiss.js +4 -4
  112. package/dist/commands/suggestions/list.js +4 -4
  113. package/dist/commands/suggestions/refresh.js +4 -4
  114. package/dist/dashboard/Dashboard.js +6 -4
  115. package/dist/dashboard/Dashboard.js.map +1 -1
  116. package/dist/dashboard/data.js +3 -1
  117. package/dist/dashboard/launcher/LauncherOverlay.js +5 -3
  118. package/dist/dashboard/launcher/api.js +3 -1
  119. package/dist/dashboard/launcher/useLaunchJobs.js +4 -2
  120. package/dist/lib/active-org-refresh.js +3 -3
  121. package/dist/lib/agent-loop.js +9 -9
  122. package/dist/lib/antigravity-transcript.js +2 -2
  123. package/dist/lib/api-client.js +3 -1
  124. package/dist/lib/eval-benchmark.js +2 -2
  125. package/dist/lib/eval-conversation-handler.js +2 -2
  126. package/dist/lib/gateway-lifecycle.js +7 -3
  127. package/dist/lib/handlers/cron-suggest-instructions.js +2 -2
  128. package/dist/lib/handlers/process-conversation.js +3 -3
  129. package/dist/lib/handlers/process-eval.js +2 -2
  130. package/dist/lib/handlers/process-insights.js +2 -2
  131. package/dist/lib/handlers/process-launch.js +2 -2
  132. package/dist/lib/hooks-config.js +7 -1
  133. package/dist/lib/manual-tasks.js +2 -2
  134. package/dist/lib/research-client.js +2 -2
  135. package/dist/lib/setup-steps.js +8 -4
  136. package/dist/lib/statusline-shim.js +3 -1
  137. package/dist/lib/terminal-target-heartbeat.js +2 -2
  138. package/oclif.manifest.json +2 -2
  139. package/package.json +6 -6
  140. package/dist/chunk-346TGPVR.js.map +0 -7
  141. package/dist/chunk-ANOWI23I.js.map +0 -7
  142. package/dist/chunk-CWFVBGX2.js.map +0 -7
  143. package/dist/chunk-ER5DPPHB.js.map +0 -7
  144. package/dist/chunk-FTJRMUID.js.map +0 -7
  145. package/dist/chunk-N52AHNBH.js.map +0 -7
  146. /package/dist/{chunk-7DRLX3DN.js.map → chunk-3POYC5NA.js.map} +0 -0
  147. /package/dist/{chunk-TJ2M5QVL.js.map → chunk-3QH73QOQ.js.map} +0 -0
  148. /package/dist/{chunk-CXWY5XKD.js.map → chunk-4LHEN2FS.js.map} +0 -0
  149. /package/dist/{chunk-CROYS75W.js.map → chunk-7K4ILWS7.js.map} +0 -0
  150. /package/dist/{chunk-TDTHAOXO.js.map → chunk-DR5WLIOU.js.map} +0 -0
  151. /package/dist/{chunk-T76YQCKX.js.map → chunk-IATQXFJA.js.map} +0 -0
  152. /package/dist/{chunk-AUUHXCJU.js.map → chunk-JEJ3J5J6.js.map} +0 -0
  153. /package/dist/{chunk-OAQG5WBV.js.map → chunk-LAKZUE35.js.map} +0 -0
  154. /package/dist/{chunk-CCSR73HF.js.map → chunk-N5A2IQAK.js.map} +0 -0
  155. /package/dist/{chunk-VCG4Q53B.js.map → chunk-OTOWLUOJ.js.map} +0 -0
  156. /package/dist/{chunk-OKCFYPIQ.js.map → chunk-RCMXTLQF.js.map} +0 -0
  157. /package/dist/{chunk-CGUV6TUA.js.map → chunk-RUCSRN7F.js.map} +0 -0
  158. /package/dist/{chunk-4UJATBTG.js.map → chunk-SLL3OBKN.js.map} +0 -0
  159. /package/dist/{chunk-CB43W53J.js.map → chunk-SZ7VDCD6.js.map} +0 -0
  160. /package/dist/{chunk-KHJUQFIK.js.map → chunk-YSEQWCN4.js.map} +0 -0
@@ -1,12 +1,15 @@
1
1
  import {
2
- detectInstalledTools
3
- } from "./chunk-ANOWI23I.js";
2
+ detectInstalledTools,
3
+ hasRulemetricClaudeHooks,
4
+ mergeClaudeCodeHooks
5
+ } from "./chunk-HWGRTIXN.js";
4
6
  import {
5
- GATEWAY_PORT
6
- } from "./chunk-FTJRMUID.js";
7
+ GATEWAY_PORT,
8
+ isGatewayListening
9
+ } from "./chunk-OVGP4OGK.js";
7
10
  import {
8
11
  apiGet
9
- } from "./chunk-CWFVBGX2.js";
12
+ } from "./chunk-ZRLYHBPN.js";
10
13
  import {
11
14
  getDefaultConfigDir,
12
15
  isAuthenticated
@@ -18,6 +21,8 @@ import { existsSync, readFileSync } from "node:fs";
18
21
  import { homedir } from "node:os";
19
22
  import { join } from "node:path";
20
23
  var CORE_STEP_IDS = ["install", "auth", "hooks", "worker"];
24
+ var OPTIONAL_STEP_IDS = ["deep-capture"];
25
+ var ALL_STEP_IDS = [...CORE_STEP_IDS, ...OPTIONAL_STEP_IDS];
21
26
  function defaultContext() {
22
27
  return {
23
28
  projectPath: process.cwd(),
@@ -74,7 +79,7 @@ async function workerActive() {
74
79
  return false;
75
80
  }
76
81
  }
77
- function projectHooksSettings(projectPath) {
82
+ function readProjectSettings(projectPath) {
78
83
  const settingsPath = join(projectPath, ".claude", "settings.json");
79
84
  let settings = {};
80
85
  let before = null;
@@ -86,14 +91,20 @@ function projectHooksSettings(projectPath) {
86
91
  settings = {};
87
92
  }
88
93
  }
94
+ return { settings, before };
95
+ }
96
+ function projectHooksSettings(projectPath) {
97
+ const { settings, before } = readProjectSettings(projectPath);
98
+ mergeClaudeCodeHooks(settings);
99
+ return { before, after: JSON.stringify(settings, null, 2) };
100
+ }
101
+ function projectProxyEnvSettings(projectPath) {
102
+ const { settings, before } = readProjectSettings(projectPath);
89
103
  const env = { ...settings.env ?? {} };
90
104
  env.HTTPS_PROXY = `http://localhost:${GATEWAY_PORT}`;
91
105
  env.NO_PROXY = "localhost,127.0.0.1,*.supabase.co,*.supabase.in";
92
- if (existsSync(CERT_PATH)) {
93
- env.NODE_EXTRA_CA_CERTS = CERT_PATH;
94
- }
95
- const after = JSON.stringify({ ...settings, env }, null, 2);
96
- return { before, after };
106
+ env.NODE_EXTRA_CA_CERTS = CERT_PATH;
107
+ return { before, after: JSON.stringify({ ...settings, env }, null, 2) };
97
108
  }
98
109
  var WORKER_PLIST_PATH = join(homedir(), "Library", "LaunchAgents", "com.rulemetric.worker.plist");
99
110
  var WORKER_SYSTEMD_PATH = join(homedir(), ".config", "systemd", "user", "rulemetric-worker.service");
@@ -169,6 +180,15 @@ var STEPS = [
169
180
  before,
170
181
  after,
171
182
  masked: true
183
+ },
184
+ // `auth login` (saveToken) also writes auth.json — disclose it so the
185
+ // plan matches every file apply() touches.
186
+ {
187
+ path: join(ctx.configDir, "auth.json"),
188
+ kind: "file",
189
+ before: existsSync(join(ctx.configDir, "auth.json")) ? "(existing session tokens)" : null,
190
+ after: "{ accessToken, refreshToken, expiresAt, email } \u2014 OAuth session tokens (secret)",
191
+ masked: true
172
192
  }
173
193
  ]
174
194
  };
@@ -180,14 +200,17 @@ var STEPS = [
180
200
  {
181
201
  id: "hooks",
182
202
  title: "Install hooks",
183
- command: "rulemetric hooks install",
203
+ // Default onboarding is hooks-only: full session capture WITHOUT the MITM
204
+ // proxy. The proxy/CA is a separate, explicitly-consented `deep-capture`
205
+ // step (below) so the invasive machine-wide + keychain changes are never
206
+ // bundled silently into the core path.
207
+ command: "rulemetric hooks install --no-proxy",
184
208
  async detect(ctx) {
185
209
  const settingsPath = join(ctx.projectPath, ".claude", "settings.json");
186
210
  if (!existsSync(settingsPath)) return false;
187
211
  try {
188
212
  const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
189
- const env = settings.env ?? {};
190
- return typeof env.HTTPS_PROXY === "string";
213
+ return hasRulemetricClaudeHooks(settings);
191
214
  } catch {
192
215
  return false;
193
216
  }
@@ -195,27 +218,25 @@ var STEPS = [
195
218
  async plan(ctx) {
196
219
  const settingsPath = join(ctx.projectPath, ".claude", "settings.json");
197
220
  const { before, after } = projectHooksSettings(ctx.projectPath);
198
- const artifacts = [
199
- { path: settingsPath, kind: "file", before, after }
200
- ];
201
- const tools = detectInstalledTools(ctx.projectPath).filter((t) => t !== "claude_code");
202
- for (const tool of tools) {
203
- artifacts.push({
204
- path: `${ctx.projectPath} (${tool} proxy config)`,
205
- kind: "file",
206
- before: null,
207
- after: `proxy \u2192 http://localhost:${GATEWAY_PORT}`
208
- });
209
- }
210
221
  return {
211
222
  id: "hooks",
212
223
  title: "Install hooks",
213
- command: "rulemetric hooks install",
214
- artifacts
224
+ command: "rulemetric hooks install --no-proxy",
225
+ artifacts: [
226
+ { path: settingsPath, kind: "file", before, after },
227
+ // Disclose what the hooks DO, not just that they're registered — these
228
+ // are the privacy-relevant behaviors a user is consenting to.
229
+ {
230
+ path: "what these hooks capture",
231
+ kind: "service",
232
+ before: null,
233
+ after: "session-end reimports the FULL transcript to the API; post-tool-use records each tool call and snapshots the working tree to a LOCAL shadow git ref (paper-trail, never pushed); user-prompt posts prompt text. All scoped to sessions in this project."
234
+ }
235
+ ]
215
236
  };
216
237
  },
217
238
  async apply() {
218
- runCommand("rulemetric hooks install");
239
+ runCommand("rulemetric hooks install --no-proxy");
219
240
  }
220
241
  },
221
242
  {
@@ -250,6 +271,74 @@ var STEPS = [
250
271
  async apply() {
251
272
  runCommand("rulemetric service install --worker-only");
252
273
  }
274
+ },
275
+ {
276
+ id: "deep-capture",
277
+ title: "Enable deep capture (proxy) \u2014 optional",
278
+ optional: true,
279
+ // The full (proxy-enabled) install. Distinct from the core `hooks` step so
280
+ // the invasive side effects below are an explicit, disclosed opt-in.
281
+ command: "rulemetric hooks install",
282
+ async detect(ctx) {
283
+ const settingsPath = join(ctx.projectPath, ".claude", "settings.json");
284
+ if (!existsSync(settingsPath)) return false;
285
+ try {
286
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
287
+ const env = settings.env ?? {};
288
+ if (typeof env.HTTPS_PROXY !== "string") return false;
289
+ } catch {
290
+ return false;
291
+ }
292
+ return isGatewayListening(GATEWAY_PORT);
293
+ },
294
+ async plan(ctx) {
295
+ const settingsPath = join(ctx.projectPath, ".claude", "settings.json");
296
+ const { before, after } = projectProxyEnvSettings(ctx.projectPath);
297
+ const artifacts = [
298
+ { path: settingsPath, kind: "file", before, after },
299
+ {
300
+ path: "macOS System keychain \u2014 mitmproxy root CA",
301
+ kind: "service",
302
+ before: existsSync(CERT_PATH) ? "(CA generated; trust state unchanged)" : "(no CA yet)",
303
+ after: "Installs + trusts the mitmproxy CA (requires sudo). This lets the proxy DECRYPT Claude Code HTTPS request/response bodies \u2014 the capability that powers rendered-system-prompt / instruction linking."
304
+ },
305
+ {
306
+ path: "launchctl setenv HTTPS_PROXY (machine-wide, all GUI apps)",
307
+ kind: "command",
308
+ before: null,
309
+ after: `http://localhost:${GATEWAY_PORT} \u2014 routes every GUI app's HTTPS through the local gateway`
310
+ },
311
+ {
312
+ path: "VS Code + Cursor user settings (http.proxy)",
313
+ kind: "file",
314
+ before: null,
315
+ after: `http.proxy \u2192 http://localhost:${GATEWAY_PORT} (+ http.proxyStrictSSL=false)`
316
+ },
317
+ {
318
+ path: `gateway process on :${GATEWAY_PORT}`,
319
+ kind: "service",
320
+ before: null,
321
+ after: "started (routes Claude Code \u2192 mitmproxy \u2192 Anthropic; falls back to direct if mitmproxy is down)"
322
+ }
323
+ ];
324
+ for (const tool of detectInstalledTools(ctx.projectPath).filter((t) => t === "cursor" || t === "copilot_agent")) {
325
+ artifacts.push({
326
+ path: `${ctx.projectPath} (${tool} workspace proxy config)`,
327
+ kind: "file",
328
+ before: null,
329
+ after: `proxy \u2192 http://localhost:${GATEWAY_PORT}`
330
+ });
331
+ }
332
+ return {
333
+ id: "deep-capture",
334
+ title: "Enable deep capture (proxy) \u2014 optional",
335
+ command: "rulemetric hooks install",
336
+ artifacts
337
+ };
338
+ },
339
+ async apply() {
340
+ runCommand("rulemetric hooks install");
341
+ }
253
342
  }
254
343
  ];
255
344
  var STEP_BY_ID = new Map(STEPS.map((s) => [s.id, s]));
@@ -259,7 +348,7 @@ function getStep(id) {
259
348
  return step;
260
349
  }
261
350
  function isStepId(value) {
262
- return CORE_STEP_IDS.includes(value);
351
+ return ALL_STEP_IDS.includes(value);
263
352
  }
264
353
  var SAFE_ENV_KEYS = /* @__PURE__ */ new Set([
265
354
  "RULEMETRIC_API_URL",
@@ -291,13 +380,17 @@ async function computeStatus(ctx = defaultContext()) {
291
380
  if (install && !install.done) {
292
381
  install.done = steps.some((s) => s.id !== "install" && s.done);
293
382
  }
294
- const currentStepId = steps.find((s) => !s.done)?.id ?? null;
295
- const coreComplete = steps.every((s) => s.done);
383
+ const coreIds = new Set(CORE_STEP_IDS);
384
+ const coreSteps = steps.filter((s) => coreIds.has(s.id));
385
+ const currentStepId = coreSteps.find((s) => !s.done)?.id ?? null;
386
+ const coreComplete = coreSteps.every((s) => s.done);
296
387
  return { steps, coreComplete, currentStepId };
297
388
  }
298
389
 
299
390
  export {
300
391
  CORE_STEP_IDS,
392
+ OPTIONAL_STEP_IDS,
393
+ ALL_STEP_IDS,
301
394
  defaultContext,
302
395
  maskToken,
303
396
  renderDiff,
@@ -306,4 +399,4 @@ export {
306
399
  isStepId,
307
400
  computeStatus
308
401
  };
309
- //# sourceMappingURL=chunk-346TGPVR.js.map
402
+ //# sourceMappingURL=chunk-JCYCAQ6U.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/setup-steps.ts"],
4
+ "sourcesContent": ["import { execSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { isAuthenticated, getDefaultConfigDir } from './auth.js';\nimport { apiGet } from './api-client.js';\nimport { GATEWAY_PORT, isGatewayListening } from './gateway-lifecycle.js';\nimport { detectInstalledTools, mergeClaudeCodeHooks, hasRulemetricClaudeHooks } from './hooks-config.js';\n\n/**\n * Data model for the four core onboarding steps, shared (by step-ID list +\n * install-monotonicity) with the web reducer `deriveSetupProgress()`.\n *\n * The two surfaces are separate implementations by necessity, but they no\n * longer read *different sources* for the same fact:\n * - Server-observable signals (cli-worker heartbeat / captured session) are\n * unified through the shared `GET /api/setup/status` endpoint \u2014 the CLI's\n * `workerActive()` and the web's `use-setup-progress.ts` both bottom out on\n * that one server-side authority, so they cannot drift on it.\n * - Local signals (auth = `~/.config/rulemetric` creds via `isAuthenticated()`,\n * hooks = cwd `.claude/settings.json`) stay machine-local: the browser\n * cannot read the user's filesystem, so these legitimately differ from the\n * web's account-scoped proxies for the same step (see the design doc's\n * \"Shared step model, two implementations\").\n * Each step exposes:\n * - detect(): is it done? (reads a local/remote signal, never mutates)\n * - plan(): what would apply() write? (read-only, returns diffs/artifacts)\n * - apply(): perform the step (thin \u2014 shells out to existing commands)\n */\n\nexport const CORE_STEP_IDS = ['install', 'auth', 'hooks', 'worker'] as const;\n// Optional steps live in the walk (offered, recommended) but do NOT gate\n// `coreComplete` \u2014 declining them still leaves the user fully onboarded. The\n// proxy/CA \"deep-capture\" upgrade is opt-in precisely because it's invasive\n// (MITM cert, machine-wide proxy); hooks-only capture already works without it.\nexport const OPTIONAL_STEP_IDS = ['deep-capture'] as const;\nexport const ALL_STEP_IDS = [...CORE_STEP_IDS, ...OPTIONAL_STEP_IDS] as const;\nexport type StepId = (typeof ALL_STEP_IDS)[number];\n\nexport type ArtifactKind = 'file' | 'command' | 'service';\n\nexport interface Artifact {\n path: string;\n kind: ArtifactKind;\n before: string | null;\n after: string | null;\n /** True when `after` contains a masked secret (never the raw value). */\n masked?: boolean;\n}\n\nexport interface StepPlan {\n id: StepId;\n title: string;\n command?: string;\n artifacts: Artifact[];\n}\n\nexport interface SetupContext {\n projectPath: string;\n configDir: string;\n}\n\nexport interface SetupStep {\n id: StepId;\n title: string;\n command?: string;\n /** Optional steps are offered in the walk but never gate `coreComplete`. */\n optional?: boolean;\n detect(ctx: SetupContext): Promise<boolean>;\n plan(ctx: SetupContext): Promise<StepPlan>;\n apply(ctx: SetupContext): Promise<void>;\n}\n\nexport interface StepStatus {\n id: StepId;\n done: boolean;\n}\n\nexport interface SetupStatus {\n steps: StepStatus[];\n coreComplete: boolean;\n currentStepId: StepId | null;\n}\n\n/** Build the default context from the current process (cwd + config dir). */\nexport function defaultContext(): SetupContext {\n return {\n projectPath: process.cwd(),\n configDir: getDefaultConfigDir(),\n };\n}\n\nconst CERT_PATH = join(homedir(), '.mitmproxy', 'mitmproxy-ca-cert.pem');\n\n/**\n * Mask a secret: keep the first 4 and last 4 characters, replace the middle\n * with asterisks. Strings too short to reveal safely are fully masked.\n */\nexport function maskToken(s: string): string {\n if (!s) return '';\n if (s.length <= 8) return '*'.repeat(s.length);\n const head = s.slice(0, 4);\n const tail = s.slice(-4);\n return `${head}${'*'.repeat(Math.max(4, s.length - 8))}${tail}`;\n}\n\n/**\n * Simple line-oriented +/- diff for terminal display. Not a real LCS \u2014 good\n * enough to show a human/agent exactly which lines an artifact would change.\n */\nexport function renderDiff(before: string | null, after: string | null): string {\n const beforeLines = before === null ? [] : before.replace(/\\n$/, '').split('\\n');\n const afterLines = after === null ? [] : after.replace(/\\n$/, '').split('\\n');\n const out: string[] = [];\n const max = Math.max(beforeLines.length, afterLines.length);\n for (let i = 0; i < max; i++) {\n const b = beforeLines[i];\n const a = afterLines[i];\n if (b === a) {\n out.push(` ${b}`);\n } else {\n if (b !== undefined) out.push(`-${b}`);\n if (a !== undefined) out.push(`+${a}`);\n }\n }\n return out.join('\\n');\n}\n\n/** Read a single KEY=value from the env file, or null if absent. */\nfunction readEnvValue(configDir: string, key: string): string | null {\n const envPath = join(configDir, 'env');\n if (!existsSync(envPath)) return null;\n try {\n const content = readFileSync(envPath, 'utf-8');\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n return match ? match[1] : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Is a CLI worker heartbeat active for the caller? (best-effort, network)\n *\n * Reads the shared `GET /api/setup/status` endpoint's `workerActive` field\n * rather than re-deriving it from `/api/workers/status`. That endpoint computes\n * the cli-worker heartbeat signal server-side using the SAME window +\n * staleness thresholds + `workerType === 'cli-worker'` filter the web strip\n * uses, so the CLI and web can't drift on this fact by construction (they used\n * to each filter/threshold client-side, which is exactly how they could\n * disagree). Fast-fails after 2.5s so `setup --json` / `setup plan` degrade to\n * \"not running\" instead of blocking on a stalled connection \u2014 detection is a\n * UX signal, not a correctness gate; offline \u2192 false.\n */\nasync function workerActive(): Promise<boolean> {\n if (!isAuthenticated()) return false;\n try {\n const status = await Promise.race([\n apiGet<{\n cliEverConnected?: boolean;\n anyCapturedSession?: boolean;\n workerActive?: boolean;\n }>('/api/setup/status'),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('setup/status timeout')), 2500),\n ),\n ]);\n return status?.workerActive === true;\n } catch {\n return false;\n }\n}\n\n/** Read the project's `.claude/settings.json` for a read-only preview (no throw). */\nfunction readProjectSettings(projectPath: string): { settings: Record<string, unknown>; before: string | null } {\n const settingsPath = join(projectPath, '.claude', 'settings.json');\n let settings: Record<string, unknown> = {};\n let before: string | null = null;\n if (existsSync(settingsPath)) {\n try {\n before = readFileSync(settingsPath, 'utf-8').replace(/\\n$/, '');\n settings = JSON.parse(before) as Record<string, unknown>;\n } catch {\n settings = {};\n }\n }\n return { settings, before };\n}\n\n/**\n * Compute the in-memory `.claude/settings.json` after the RuleMetric session\n * HOOKS are merged, WITHOUT touching disk. Mirrors the default onboarding\n * install (`hooks install --no-proxy`, \u00A71) \u2014 capture hooks only. Deliberately\n * adds NO proxy env / CA / machine-wide config: the default path no longer\n * routes traffic through a MITM proxy, and the plan diff must show exactly that.\n */\nfunction projectHooksSettings(projectPath: string): { before: string | null; after: string } {\n const { settings, before } = readProjectSettings(projectPath);\n mergeClaudeCodeHooks(settings);\n return { before, after: JSON.stringify(settings, null, 2) };\n}\n\n/**\n * The proxy-env addition to `.claude/settings.json` that the deep-capture step\n * (`hooks install`, \u00A72) would make \u2014 for the disclosure diff only. Read-only.\n */\nfunction projectProxyEnvSettings(projectPath: string): { before: string | null; after: string } {\n const { settings, before } = readProjectSettings(projectPath);\n const env = { ...((settings.env as Record<string, string>) ?? {}) };\n env.HTTPS_PROXY = `http://localhost:${GATEWAY_PORT}`;\n env.NO_PROXY = 'localhost,127.0.0.1,*.supabase.co,*.supabase.in';\n env.NODE_EXTRA_CA_CERTS = CERT_PATH;\n return { before, after: JSON.stringify({ ...settings, env }, null, 2) };\n}\n\nconst WORKER_PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', 'com.rulemetric.worker.plist');\nconst WORKER_SYSTEMD_PATH = join(homedir(), '.config', 'systemd', 'user', 'rulemetric-worker.service');\n\n/** Run a command with inherited stdio (interactive-friendly). Thin apply(). */\nfunction runCommand(cmd: string): void {\n execSync(cmd, { stdio: 'inherit' });\n}\n\nexport const STEPS: SetupStep[] = [\n {\n id: 'install',\n title: 'Install the CLI',\n command: 'npm install -g @rulemetric/cli',\n // Monotonic: the CLI must exist to produce any downstream signal, so it\n // greens off auth or worker presence (nothing local phones home before).\n async detect(): Promise<boolean> {\n return isAuthenticated() || (await workerActive());\n },\n async plan(): Promise<StepPlan> {\n return {\n id: 'install',\n title: 'Install the CLI',\n command: 'npm install -g @rulemetric/cli',\n artifacts: [\n {\n path: 'npm install -g @rulemetric/cli',\n kind: 'command',\n before: null,\n after: 'rulemetric --version # verifies the binary is on PATH',\n },\n ],\n };\n },\n async apply(): Promise<void> {\n runCommand('npm install -g @rulemetric/cli');\n },\n },\n {\n id: 'auth',\n title: 'Authenticate',\n command: 'rulemetric auth login',\n async detect(): Promise<boolean> {\n return isAuthenticated();\n },\n async plan(ctx): Promise<StepPlan> {\n const envPath = join(ctx.configDir, 'env');\n // Guard the read like the sibling helpers (readEnvValue /\n // projectHooksSettings) do: a TOCTOU race or permission error between\n // existsSync and readFileSync must NOT throw out of plan(). Fall back to\n // a secret-free marker rather than crashing `setup plan`/`--show`.\n let before: string | null = null;\n if (existsSync(envPath)) {\n try {\n before = maskEnvFile(readFileSync(envPath, 'utf-8'));\n } catch {\n before = '(unreadable)';\n }\n }\n const rawToken = readEnvValue(ctx.configDir, 'RULEMETRIC_ACCESS_TOKEN');\n const tokenDisplay = rawToken ? maskToken(rawToken) : maskToken('<token-from-login>');\n // Reconstruct EXACTLY what `auth login`'s saveEnvFile writes so the diff\n // doesn't show phantom changes: line order [TOKEN, KEY?, URL?], the key\n // preserved (masked) when present, and the URL sourced from\n // process.env.RULEMETRIC_API_URL \u2014 the same value login passes \u2014 and OMITTED\n // when unset (login writes no URL line then). All masking preserved; the raw\n // token/key are never emitted.\n const afterLines = [`RULEMETRIC_ACCESS_TOKEN=${tokenDisplay}`];\n const rawApiKey = readEnvValue(ctx.configDir, 'RULEMETRIC_API_KEY');\n if (rawApiKey) {\n afterLines.push(`RULEMETRIC_API_KEY=${maskToken(rawApiKey)}`);\n }\n const loginUrl = process.env.RULEMETRIC_API_URL;\n if (loginUrl) {\n afterLines.push(`RULEMETRIC_API_URL=${loginUrl}`);\n }\n const after = afterLines.join('\\n');\n return {\n id: 'auth',\n title: 'Authenticate',\n command: 'rulemetric auth login',\n artifacts: [\n {\n path: envPath,\n kind: 'file',\n before,\n after,\n masked: true,\n },\n // `auth login` (saveToken) also writes auth.json \u2014 disclose it so the\n // plan matches every file apply() touches.\n {\n path: join(ctx.configDir, 'auth.json'),\n kind: 'file',\n before: existsSync(join(ctx.configDir, 'auth.json')) ? '(existing session tokens)' : null,\n after: '{ accessToken, refreshToken, expiresAt, email } \u2014 OAuth session tokens (secret)',\n masked: true,\n },\n ],\n };\n },\n async apply(): Promise<void> {\n runCommand('rulemetric auth login');\n },\n },\n {\n id: 'hooks',\n title: 'Install hooks',\n // Default onboarding is hooks-only: full session capture WITHOUT the MITM\n // proxy. The proxy/CA is a separate, explicitly-consented `deep-capture`\n // step (below) so the invasive machine-wide + keychain changes are never\n // bundled silently into the core path.\n command: 'rulemetric hooks install --no-proxy',\n async detect(ctx): Promise<boolean> {\n const settingsPath = join(ctx.projectPath, '.claude', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n try {\n const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as Record<string, unknown>;\n // Ready when capture hooks are actually wired up \u2014 NOT when a proxy env\n // var was written (that greened even against a dead proxy port).\n return hasRulemetricClaudeHooks(settings);\n } catch {\n return false;\n }\n },\n async plan(ctx): Promise<StepPlan> {\n const settingsPath = join(ctx.projectPath, '.claude', 'settings.json');\n const { before, after } = projectHooksSettings(ctx.projectPath);\n return {\n id: 'hooks',\n title: 'Install hooks',\n command: 'rulemetric hooks install --no-proxy',\n artifacts: [\n { path: settingsPath, kind: 'file', before, after },\n // Disclose what the hooks DO, not just that they're registered \u2014 these\n // are the privacy-relevant behaviors a user is consenting to.\n {\n path: 'what these hooks capture',\n kind: 'service',\n before: null,\n after:\n 'session-end reimports the FULL transcript to the API; post-tool-use records each ' +\n 'tool call and snapshots the working tree to a LOCAL shadow git ref (paper-trail, ' +\n 'never pushed); user-prompt posts prompt text. All scoped to sessions in this project.',\n },\n ],\n };\n },\n async apply(): Promise<void> {\n runCommand('rulemetric hooks install --no-proxy');\n },\n },\n {\n id: 'worker',\n title: 'Run the worker',\n command: 'rulemetric service install --worker-only',\n async detect(): Promise<boolean> {\n return workerActive();\n },\n async plan(): Promise<StepPlan> {\n const isMac = process.platform === 'darwin';\n const servicePath = isMac ? WORKER_PLIST_PATH : WORKER_SYSTEMD_PATH;\n // SECURITY: never read the installed plist/systemd unit into `before` \u2014\n // those files embed RULEMETRIC_ACCESS_TOKEN / RULEMETRIC_API_KEY verbatim\n // (service/install.ts writes them from ALLOWED_ENV_VARS), so echoing the\n // file contents would leak the raw secret through `plan`, `--show`, and\n // `--json`. Use a synthetic, secret-free marker for the current state.\n const before = existsSync(servicePath)\n ? `(service already installed at ${servicePath})`\n : null;\n // Concise representation (path + ExecStart node/cli invocation) rather\n // than reconstructing the full plist \u2014 keep read-only + legible.\n const after = isMac\n ? [\n `Label: com.rulemetric.worker`,\n `ProgramArguments: <node> <cli>/bin/run.js evals agent`,\n `RunAtLoad: true, KeepAlive: true`,\n ].join('\\n')\n : [\n `[Service]`,\n `ExecStart=<node> <cli>/bin/run.js evals agent`,\n `Restart=on-failure`,\n ].join('\\n');\n return {\n id: 'worker',\n title: 'Run the worker',\n command: 'rulemetric service install --worker-only',\n artifacts: [\n { path: servicePath, kind: 'service', before, after },\n ],\n };\n },\n async apply(): Promise<void> {\n runCommand('rulemetric service install --worker-only');\n },\n },\n {\n id: 'deep-capture',\n title: 'Enable deep capture (proxy) \u2014 optional',\n optional: true,\n // The full (proxy-enabled) install. Distinct from the core `hooks` step so\n // the invasive side effects below are an explicit, disclosed opt-in.\n command: 'rulemetric hooks install',\n async detect(ctx): Promise<boolean> {\n // \"Done\" means deep capture is actually WORKING: the proxy env is pinned\n // AND the gateway answers on :8787. (Honest readiness \u2014 a configured-but-\n // dead proxy reads as not-done, which is the truth.)\n const settingsPath = join(ctx.projectPath, '.claude', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n try {\n const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as Record<string, unknown>;\n const env = (settings.env ?? {}) as Record<string, unknown>;\n if (typeof env.HTTPS_PROXY !== 'string') return false;\n } catch {\n return false;\n }\n return isGatewayListening(GATEWAY_PORT);\n },\n async plan(ctx): Promise<StepPlan> {\n // FULL DISCLOSURE: every invasive side effect of `hooks install` \u00A72. The\n // whole reason this is a separate opt-in step is that nothing here is\n // hidden \u2014 the diff enumerates the machine-wide + keychain changes that\n // the core hooks step deliberately does NOT make.\n const settingsPath = join(ctx.projectPath, '.claude', 'settings.json');\n const { before, after } = projectProxyEnvSettings(ctx.projectPath);\n const artifacts: Artifact[] = [\n { path: settingsPath, kind: 'file', before, after },\n {\n path: 'macOS System keychain \u2014 mitmproxy root CA',\n kind: 'service',\n before: existsSync(CERT_PATH) ? '(CA generated; trust state unchanged)' : '(no CA yet)',\n after:\n 'Installs + trusts the mitmproxy CA (requires sudo). This lets the ' +\n 'proxy DECRYPT Claude Code HTTPS request/response bodies \u2014 the ' +\n 'capability that powers rendered-system-prompt / instruction linking.',\n },\n {\n path: 'launchctl setenv HTTPS_PROXY (machine-wide, all GUI apps)',\n kind: 'command',\n before: null,\n after: `http://localhost:${GATEWAY_PORT} \u2014 routes every GUI app's HTTPS through the local gateway`,\n },\n {\n path: 'VS Code + Cursor user settings (http.proxy)',\n kind: 'file',\n before: null,\n after: `http.proxy \u2192 http://localhost:${GATEWAY_PORT} (+ http.proxyStrictSSL=false)`,\n },\n {\n path: `gateway process on :${GATEWAY_PORT}`,\n kind: 'service',\n before: null,\n after: 'started (routes Claude Code \u2192 mitmproxy \u2192 Anthropic; falls back to direct if mitmproxy is down)',\n },\n ];\n // Workspace-level editor configs, only if those dirs exist here.\n for (const tool of detectInstalledTools(ctx.projectPath).filter((t) => t === 'cursor' || t === 'copilot_agent')) {\n artifacts.push({\n path: `${ctx.projectPath} (${tool} workspace proxy config)`,\n kind: 'file',\n before: null,\n after: `proxy \u2192 http://localhost:${GATEWAY_PORT}`,\n });\n }\n return {\n id: 'deep-capture',\n title: 'Enable deep capture (proxy) \u2014 optional',\n command: 'rulemetric hooks install',\n artifacts,\n };\n },\n async apply(): Promise<void> {\n runCommand('rulemetric hooks install');\n },\n },\n];\n\nconst STEP_BY_ID = new Map<StepId, SetupStep>(STEPS.map((s) => [s.id, s]));\n\nexport function getStep(id: StepId): SetupStep {\n const step = STEP_BY_ID.get(id);\n if (!step) throw new Error(`Unknown setup step: ${id}`);\n return step;\n}\n\nexport function isStepId(value: string): value is StepId {\n return (ALL_STEP_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Keys whose values are safe to show verbatim in the env-file diff. Everything\n * NOT in this set is masked. Deny-by-default is deliberate: `~/.config/rulemetric/env`\n * can legitimately hold DATABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_JWT_SECRET,\n * GITHUB_TOKEN, RESEND_API_KEY, etc. (see ALLOWED_ENV_VARS in service/install.ts),\n * so an allowlist-to-hide would leak any secret we forgot to enumerate. Revealing\n * only known-safe keys means a future secret added to the file is masked automatically.\n */\nconst SAFE_ENV_KEYS = new Set([\n 'RULEMETRIC_API_URL',\n 'RULEMETRIC_USER_ID',\n 'RULEMETRIC_CLIENT_NAME',\n 'RESEND_FROM_EMAIL',\n 'SENTRY_DSN', // a DSN is not a secret\n 'PG_POOL_MAX',\n 'PG_IDLE_TIMEOUT',\n 'PG_STATEMENT_TIMEOUT_MS',\n 'NODE_ENV',\n]);\n\n/**\n * Mask secret-bearing lines in a raw env file for display. Deny-by-default:\n * every `KEY=value` line has its value masked unless KEY is in SAFE_ENV_KEYS.\n * Non-assignment lines (comments, blanks) pass through untouched.\n */\nfunction maskEnvFile(content: string): string {\n return content\n .replace(/\\n$/, '')\n .split('\\n')\n .map((line) => {\n const match = line.match(/^([A-Z0-9_]+)=(.+)$/);\n if (!match) return line; // comment / blank / non-assignment\n const [, key, value] = match;\n return SAFE_ENV_KEYS.has(key) ? line : `${key}=${maskToken(value)}`;\n })\n .join('\\n');\n}\n\n/**\n * Run every step's detect() and derive the overall status. `currentStepId` is\n * the first incomplete step in core order (mirrors the web reducer).\n */\nexport async function computeStatus(ctx: SetupContext = defaultContext()): Promise<SetupStatus> {\n const steps: StepStatus[] = [];\n for (const step of STEPS) {\n // Sequential: detect() may hit the network; order is stable + cheap.\n // eslint-disable-next-line no-await-in-loop\n const done = await step.detect(ctx);\n steps.push({ id: step.id, done });\n }\n // Monotonicity (mirrors the web reducer's `cliDetected`): the CLI binary must\n // exist to produce ANY downstream signal, so `install` greens whenever auth,\n // hooks, or worker is detected \u2014 even if `install.detect()` alone (which only\n // sees auth/worker, not hooks) came back false. Without this, a user with\n // hooks installed but no live worker sees \"Install the CLI: waiting\" while\n // \"Install hooks: done\" \u2014 a contradiction.\n const install = steps.find((s) => s.id === 'install');\n if (install && !install.done) {\n install.done = steps.some((s) => s.id !== 'install' && s.done);\n }\n\n // coreComplete + currentStepId are derived from CORE steps only \u2014 the optional\n // deep-capture (proxy) step never blocks \"done\" or steals the cursor. Mirrors\n // the web reducer, which excludes its optional (tmux/target) steps the same way.\n const coreIds = new Set<string>(CORE_STEP_IDS);\n const coreSteps = steps.filter((s) => coreIds.has(s.id));\n const currentStepId = coreSteps.find((s) => !s.done)?.id ?? null;\n const coreComplete = coreSteps.every((s) => s.done);\n return { steps, coreComplete, currentStepId };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  apiGet,
7
7
  apiPatch
8
- } from "./chunk-CWFVBGX2.js";
8
+ } from "./chunk-ZRLYHBPN.js";
9
9
 
10
10
  // src/lib/terminal-target-heartbeat.ts
11
11
  var DEFAULT_INTERVAL_MS = 6e4;
@@ -96,4 +96,4 @@ export {
96
96
  runOneHeartbeatCycle,
97
97
  startTerminalTargetHeartbeat
98
98
  };
99
- //# sourceMappingURL=chunk-AUUHXCJU.js.map
99
+ //# sourceMappingURL=chunk-JEJ3J5J6.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiPost
4
- } from "./chunk-CWFVBGX2.js";
4
+ } from "./chunk-ZRLYHBPN.js";
5
5
 
6
6
  // src/lib/research-client.ts
7
7
  function serializeDate(v) {
@@ -46,4 +46,4 @@ export {
46
46
  postObservation,
47
47
  postObservations
48
48
  };
49
- //# sourceMappingURL=chunk-OAQG5WBV.js.map
49
+ //# sourceMappingURL=chunk-LAKZUE35.js.map
@@ -158,6 +158,11 @@ function readCurrentStatusLine(settings) {
158
158
  }
159
159
  return void 0;
160
160
  }
161
+ function removeStatuslineShim(settings) {
162
+ if (!isStatuslineShimInstalled(settings)) return false;
163
+ delete settings.statusLine;
164
+ return true;
165
+ }
161
166
  function isStatuslineShimInstalled(settings) {
162
167
  const current = settings.statusLine;
163
168
  if (!current) return false;
@@ -173,6 +178,7 @@ export {
173
178
  writeStatuslineShim,
174
179
  STATUSLINE_SETTINGS_VALUE,
175
180
  readCurrentStatusLine,
181
+ removeStatuslineShim,
176
182
  isStatuslineShimInstalled
177
183
  };
178
- //# sourceMappingURL=chunk-JNFIYBFL.js.map
184
+ //# sourceMappingURL=chunk-N2X35ITW.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/lib/statusline-shim.ts"],
4
- "sourcesContent": ["import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Path where the shim script lives \u2014 readable by the `statusLine` config key.\nexport const STATUSLINE_SHIM_PATH = join(homedir(), '.claude', 'statusline-rulemetric.sh');\n\n/**\n * Write ~/.claude/statusline-rulemetric.sh.\n *\n * The script is invoked by Claude Code on every statusline refresh cycle.\n * Claude Code feeds a JSON payload on stdin that includes `rate_limits`\n * (same shape as GET /api/oauth/usage) using its own in-memory token \u2014\n * so freshness is tied to the statusline cadence, not the proxy capturing\n * a stale keychain token.\n *\n * The script:\n * 1. Reads stdin once (Claude Code closes it after the payload).\n * 2. If jq is available, extracts each rate_limit window and POSTs it to\n * POST /api/internal/research/observe as an oauth_usage observation.\n * 3. If a `prevCommand` is supplied (the user's original statusLine value),\n * it is executed with the same stdin forwarded so the user's prompt text\n * is still displayed \u2014 we must chain, not replace.\n * 4. Exits 0 regardless so Claude Code always gets a clean statusline.\n *\n * Auth is read from the same priority chain as the hook scripts:\n * ~/.config/rulemetric/env (RULEMETRIC_ACCESS_TOKEN / RULEMETRIC_API_KEY)\n * RULEMETRIC_API_KEY / RULEMETRIC_ACCESS_TOKEN env vars\n *\n * Returns the path written.\n */\nexport function writeStatuslineShim(prevCommand?: string): string {\n const chainBlock = prevCommand\n ? `\n# Chain to the original statusLine command so the user's prompt is preserved.\necho \"$STDIN_PAYLOAD\" | ${prevCommand}`\n : '';\n\n const script = `#!/usr/bin/env bash\n# RuleMetric statusline shim \u2014 auto-generated by \\`rulemetric hooks install --statusline-usage\\`\n# Captures Claude Code rate_limits from statusline stdin and forwards to the research pipeline.\n# DO NOT EDIT \u2014 re-run \\`rulemetric hooks install --statusline-usage\\` to regenerate.\n\nset -euo pipefail\n\n# Read stdin once (Claude Code closes the pipe after the payload).\nSTDIN_PAYLOAD=\"$(cat)\"\n\n# \u2500\u2500 Auth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCONFIG_ENV=\"$HOME/.config/rulemetric/env\"\nif [ -f \"$CONFIG_ENV\" ]; then\n # shellcheck source=/dev/null\n source \"$CONFIG_ENV\"\nfi\n\nAPI_URL=\"\\${RULEMETRIC_API_URL:-https://rulemetric.com}\"\nAUTH_TOKEN=\"\\${RULEMETRIC_ACCESS_TOKEN:-\\${RULEMETRIC_API_KEY:-}}\"\n\nif [ -z \"$AUTH_TOKEN\" ]; then\n # No auth \u2014 still chain so the user's statusline works.\n${chainBlock}\n exit 0\nfi\n\n# \u2500\u2500 Extract + POST rate_limit windows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif ! command -v jq &>/dev/null; then\n # jq missing \u2014 chain and bail; don't break the user's statusline.\n${chainBlock}\n exit 0\nfi\n\nOBSERVED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nCACHE_DIR=\"$HOME/.config/rulemetric\"\nmkdir -p \"$CACHE_DIR\" 2>/dev/null || true\n\nfor WIRE_KEY in five_hour seven_day seven_day_opus; do\n # Map wire key \u2192 limit_type (case statement avoids bash 4 associative arrays).\n case \"$WIRE_KEY\" in\n five_hour) LIMIT_TYPE=\"five_hour_percent\" ;;\n seven_day) LIMIT_TYPE=\"seven_day_percent\" ;;\n seven_day_opus) LIMIT_TYPE=\"seven_day_opus_percent\" ;;\n *) continue ;;\n esac\n\n # Try both wire shapes:\n # Legacy: { \"rate_limits\": { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } } }\n # Current: { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } }\n WINDOW=\"$(echo \"$STDIN_PAYLOAD\" | jq -c \".rate_limits.\\${WIRE_KEY} // .\\${WIRE_KEY} // empty\" 2>/dev/null || true)\"\n\n if [ -z \"$WINDOW\" ] || [ \"$WINDOW\" = \"null\" ]; then\n continue\n fi\n\n USED_PCT=\"$(echo \"$WINDOW\" | jq '.used_percentage // empty' 2>/dev/null || true)\"\n RESETS_AT_EPOCH=\"$(echo \"$WINDOW\" | jq '.resets_at // empty' 2>/dev/null || true)\"\n\n if [ -z \"$USED_PCT\" ] || [ \"$USED_PCT\" = \"null\" ]; then\n continue\n fi\n\n # Convert unix epoch to ISO-8601 if present.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n RESETS_AT_ISO=\"$(date -u -r \"$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || date -u -d \"@$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || echo \"\")\"\n else\n RESETS_AT_ISO=\"\"\n fi\n\n # Skip stale windows. Claude Code can keep re-emitting a cached rate_limits\n # block after the reset time has passed; storing those rows floods the DB and\n # corrupts the \"current limits\" view.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n NOW_EPOCH=\"$(date -u +%s)\"\n if [ \"$RESETS_AT_EPOCH\" -lt \"$NOW_EPOCH\" ] 2>/dev/null; then\n continue\n fi\n else\n NOW_EPOCH=\"$(date -u +%s)\"\n fi\n\n # Local duplicate suppression mirrors the API's 30-minute freshness floor.\n # The statusline refreshes often; if the same window/value/reset is already\n # posted recently, another POST only burns an API request and DB connection.\n CACHE_FILE=\"$CACHE_DIR/statusline-usage-\\${LIMIT_TYPE}.cache\"\n CACHE_KEY=\"\\${LIMIT_TYPE}|\\${USED_PCT}|\\${RESETS_AT_ISO}\"\n if [ -f \"$CACHE_FILE\" ]; then\n read -r CACHED_AT CACHED_KEY < \"$CACHE_FILE\" || true\n if [ \"\\${CACHED_KEY:-}\" = \"$CACHE_KEY\" ] && [ $((NOW_EPOCH - \\${CACHED_AT:-0})) -lt 1800 ] 2>/dev/null; then\n continue\n fi\n fi\n printf '%s %s\\n' \"$NOW_EPOCH\" \"$CACHE_KEY\" > \"$CACHE_FILE.tmp\" 2>/dev/null \\\n && mv \"$CACHE_FILE.tmp\" \"$CACHE_FILE\" 2>/dev/null || true\n\n PAYLOAD=\"$(jq -nc \\\\\n --arg provider \"anthropic\" \\\\\n --arg plan \"unknown\" \\\\\n --argjson value \"$USED_PCT\" \\\\\n --arg limitType \"$LIMIT_TYPE\" \\\\\n --arg observedAt \"$OBSERVED_AT\" \\\\\n --arg resetsAt \"$RESETS_AT_ISO\" \\\\\n '{\n provider: $provider,\n plan: $plan,\n limitType: $limitType,\n value: $value,\n unit: \"percentage\",\n comparator: \"=\",\n sourceType: \"oauth_usage\",\n sourceUrl: null,\n confidence: 1.0,\n observedAt: $observedAt,\n resetsAt: (if $resetsAt == \"\" then null else $resetsAt end)\n }')\"\n\n curl -sf -o /dev/null \\\\\n -X POST \"$API_URL/api/internal/research/observe\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"Authorization: Bearer $AUTH_TOKEN\" \\\\\n --max-time 3 \\\\\n --data \"$PAYLOAD\" >/dev/null 2>&1 &\ndone\n\n${chainBlock}\nexit 0\n`;\n\n writeFileSync(STATUSLINE_SHIM_PATH, script, { encoding: 'utf-8' });\n chmodSync(STATUSLINE_SHIM_PATH, 0o755);\n return STATUSLINE_SHIM_PATH;\n}\n\n// The settings object shape for statusLine (Claude Code v1.x+).\nexport const STATUSLINE_SETTINGS_VALUE = {\n type: 'command' as const,\n command: STATUSLINE_SHIM_PATH,\n refreshInterval: 30,\n};\n\n/**\n * Read the current statusLine command from a settings object, if any.\n * Returns undefined if not set or already points to the shim (idempotent).\n * Handles both the legacy string form and the current object form.\n */\nexport function readCurrentStatusLine(\n settings: Record<string, unknown>,\n): string | undefined {\n const current = settings.statusLine;\n if (!current) return undefined;\n if (typeof current === 'string') {\n if (current === STATUSLINE_SHIM_PATH) return undefined;\n return current;\n }\n if (typeof current === 'object' && current !== null) {\n const cmd = (current as Record<string, unknown>).command;\n if (typeof cmd !== 'string') return undefined;\n if (cmd === STATUSLINE_SHIM_PATH) return undefined; // already installed\n return cmd;\n }\n return undefined;\n}\n\n/**\n * Returns true if the settings object already points to the shim.\n */\nexport function isStatuslineShimInstalled(settings: Record<string, unknown>): boolean {\n const current = settings.statusLine;\n if (!current) return false;\n if (typeof current === 'string') return current === STATUSLINE_SHIM_PATH;\n if (typeof current === 'object' && current !== null) {\n return (current as Record<string, unknown>).command === STATUSLINE_SHIM_PATH;\n }\n return false;\n}\n"],
5
- "mappings": ";AAAA,SAAmC,eAAe,iBAAiB;AACnE,SAAS,eAAe;AACxB,SAAS,YAAY;AAGd,IAAM,uBAAuB,KAAK,QAAQ,GAAG,WAAW,0BAA0B;AA0BlF,SAAS,oBAAoB,aAA8B;AAChE,QAAM,aAAa,cACf;AAAA;AAAA,0BAEoB,WAAW,KAC/B;AAEJ,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBf,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiGV,UAAU;AAAA;AAAA;AAIV,gBAAc,sBAAsB,QAAQ,EAAE,UAAU,QAAQ,CAAC;AACjE,YAAU,sBAAsB,GAAK;AACrC,SAAO;AACT;AAGO,IAAM,4BAA4B;AAAA,EACvC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,iBAAiB;AACnB;AAOO,SAAS,sBACd,UACoB;AACpB,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,UAAU;AAC/B,QAAI,YAAY,qBAAsB,QAAO;AAC7C,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,MAAO,QAAoC;AACjD,QAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAI,QAAQ,qBAAsB,QAAO;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,0BAA0B,UAA4C;AACpF,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO,YAAY;AACpD,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAQ,QAAoC,YAAY;AAAA,EAC1D;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Path where the shim script lives \u2014 readable by the `statusLine` config key.\nexport const STATUSLINE_SHIM_PATH = join(homedir(), '.claude', 'statusline-rulemetric.sh');\n\n/**\n * Write ~/.claude/statusline-rulemetric.sh.\n *\n * The script is invoked by Claude Code on every statusline refresh cycle.\n * Claude Code feeds a JSON payload on stdin that includes `rate_limits`\n * (same shape as GET /api/oauth/usage) using its own in-memory token \u2014\n * so freshness is tied to the statusline cadence, not the proxy capturing\n * a stale keychain token.\n *\n * The script:\n * 1. Reads stdin once (Claude Code closes it after the payload).\n * 2. If jq is available, extracts each rate_limit window and POSTs it to\n * POST /api/internal/research/observe as an oauth_usage observation.\n * 3. If a `prevCommand` is supplied (the user's original statusLine value),\n * it is executed with the same stdin forwarded so the user's prompt text\n * is still displayed \u2014 we must chain, not replace.\n * 4. Exits 0 regardless so Claude Code always gets a clean statusline.\n *\n * Auth is read from the same priority chain as the hook scripts:\n * ~/.config/rulemetric/env (RULEMETRIC_ACCESS_TOKEN / RULEMETRIC_API_KEY)\n * RULEMETRIC_API_KEY / RULEMETRIC_ACCESS_TOKEN env vars\n *\n * Returns the path written.\n */\nexport function writeStatuslineShim(prevCommand?: string): string {\n const chainBlock = prevCommand\n ? `\n# Chain to the original statusLine command so the user's prompt is preserved.\necho \"$STDIN_PAYLOAD\" | ${prevCommand}`\n : '';\n\n const script = `#!/usr/bin/env bash\n# RuleMetric statusline shim \u2014 auto-generated by \\`rulemetric hooks install --statusline-usage\\`\n# Captures Claude Code rate_limits from statusline stdin and forwards to the research pipeline.\n# DO NOT EDIT \u2014 re-run \\`rulemetric hooks install --statusline-usage\\` to regenerate.\n\nset -euo pipefail\n\n# Read stdin once (Claude Code closes the pipe after the payload).\nSTDIN_PAYLOAD=\"$(cat)\"\n\n# \u2500\u2500 Auth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCONFIG_ENV=\"$HOME/.config/rulemetric/env\"\nif [ -f \"$CONFIG_ENV\" ]; then\n # shellcheck source=/dev/null\n source \"$CONFIG_ENV\"\nfi\n\nAPI_URL=\"\\${RULEMETRIC_API_URL:-https://rulemetric.com}\"\nAUTH_TOKEN=\"\\${RULEMETRIC_ACCESS_TOKEN:-\\${RULEMETRIC_API_KEY:-}}\"\n\nif [ -z \"$AUTH_TOKEN\" ]; then\n # No auth \u2014 still chain so the user's statusline works.\n${chainBlock}\n exit 0\nfi\n\n# \u2500\u2500 Extract + POST rate_limit windows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif ! command -v jq &>/dev/null; then\n # jq missing \u2014 chain and bail; don't break the user's statusline.\n${chainBlock}\n exit 0\nfi\n\nOBSERVED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nCACHE_DIR=\"$HOME/.config/rulemetric\"\nmkdir -p \"$CACHE_DIR\" 2>/dev/null || true\n\nfor WIRE_KEY in five_hour seven_day seven_day_opus; do\n # Map wire key \u2192 limit_type (case statement avoids bash 4 associative arrays).\n case \"$WIRE_KEY\" in\n five_hour) LIMIT_TYPE=\"five_hour_percent\" ;;\n seven_day) LIMIT_TYPE=\"seven_day_percent\" ;;\n seven_day_opus) LIMIT_TYPE=\"seven_day_opus_percent\" ;;\n *) continue ;;\n esac\n\n # Try both wire shapes:\n # Legacy: { \"rate_limits\": { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } } }\n # Current: { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } }\n WINDOW=\"$(echo \"$STDIN_PAYLOAD\" | jq -c \".rate_limits.\\${WIRE_KEY} // .\\${WIRE_KEY} // empty\" 2>/dev/null || true)\"\n\n if [ -z \"$WINDOW\" ] || [ \"$WINDOW\" = \"null\" ]; then\n continue\n fi\n\n USED_PCT=\"$(echo \"$WINDOW\" | jq '.used_percentage // empty' 2>/dev/null || true)\"\n RESETS_AT_EPOCH=\"$(echo \"$WINDOW\" | jq '.resets_at // empty' 2>/dev/null || true)\"\n\n if [ -z \"$USED_PCT\" ] || [ \"$USED_PCT\" = \"null\" ]; then\n continue\n fi\n\n # Convert unix epoch to ISO-8601 if present.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n RESETS_AT_ISO=\"$(date -u -r \"$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || date -u -d \"@$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || echo \"\")\"\n else\n RESETS_AT_ISO=\"\"\n fi\n\n # Skip stale windows. Claude Code can keep re-emitting a cached rate_limits\n # block after the reset time has passed; storing those rows floods the DB and\n # corrupts the \"current limits\" view.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n NOW_EPOCH=\"$(date -u +%s)\"\n if [ \"$RESETS_AT_EPOCH\" -lt \"$NOW_EPOCH\" ] 2>/dev/null; then\n continue\n fi\n else\n NOW_EPOCH=\"$(date -u +%s)\"\n fi\n\n # Local duplicate suppression mirrors the API's 30-minute freshness floor.\n # The statusline refreshes often; if the same window/value/reset is already\n # posted recently, another POST only burns an API request and DB connection.\n CACHE_FILE=\"$CACHE_DIR/statusline-usage-\\${LIMIT_TYPE}.cache\"\n CACHE_KEY=\"\\${LIMIT_TYPE}|\\${USED_PCT}|\\${RESETS_AT_ISO}\"\n if [ -f \"$CACHE_FILE\" ]; then\n read -r CACHED_AT CACHED_KEY < \"$CACHE_FILE\" || true\n if [ \"\\${CACHED_KEY:-}\" = \"$CACHE_KEY\" ] && [ $((NOW_EPOCH - \\${CACHED_AT:-0})) -lt 1800 ] 2>/dev/null; then\n continue\n fi\n fi\n printf '%s %s\\n' \"$NOW_EPOCH\" \"$CACHE_KEY\" > \"$CACHE_FILE.tmp\" 2>/dev/null \\\n && mv \"$CACHE_FILE.tmp\" \"$CACHE_FILE\" 2>/dev/null || true\n\n PAYLOAD=\"$(jq -nc \\\\\n --arg provider \"anthropic\" \\\\\n --arg plan \"unknown\" \\\\\n --argjson value \"$USED_PCT\" \\\\\n --arg limitType \"$LIMIT_TYPE\" \\\\\n --arg observedAt \"$OBSERVED_AT\" \\\\\n --arg resetsAt \"$RESETS_AT_ISO\" \\\\\n '{\n provider: $provider,\n plan: $plan,\n limitType: $limitType,\n value: $value,\n unit: \"percentage\",\n comparator: \"=\",\n sourceType: \"oauth_usage\",\n sourceUrl: null,\n confidence: 1.0,\n observedAt: $observedAt,\n resetsAt: (if $resetsAt == \"\" then null else $resetsAt end)\n }')\"\n\n curl -sf -o /dev/null \\\\\n -X POST \"$API_URL/api/internal/research/observe\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"Authorization: Bearer $AUTH_TOKEN\" \\\\\n --max-time 3 \\\\\n --data \"$PAYLOAD\" >/dev/null 2>&1 &\ndone\n\n${chainBlock}\nexit 0\n`;\n\n writeFileSync(STATUSLINE_SHIM_PATH, script, { encoding: 'utf-8' });\n chmodSync(STATUSLINE_SHIM_PATH, 0o755);\n return STATUSLINE_SHIM_PATH;\n}\n\n// The settings object shape for statusLine (Claude Code v1.x+).\nexport const STATUSLINE_SETTINGS_VALUE = {\n type: 'command' as const,\n command: STATUSLINE_SHIM_PATH,\n refreshInterval: 30,\n};\n\n/**\n * Read the current statusLine command from a settings object, if any.\n * Returns undefined if not set or already points to the shim (idempotent).\n * Handles both the legacy string form and the current object form.\n */\nexport function readCurrentStatusLine(\n settings: Record<string, unknown>,\n): string | undefined {\n const current = settings.statusLine;\n if (!current) return undefined;\n if (typeof current === 'string') {\n if (current === STATUSLINE_SHIM_PATH) return undefined;\n return current;\n }\n if (typeof current === 'object' && current !== null) {\n const cmd = (current as Record<string, unknown>).command;\n if (typeof cmd !== 'string') return undefined;\n if (cmd === STATUSLINE_SHIM_PATH) return undefined; // already installed\n return cmd;\n }\n return undefined;\n}\n\n/**\n * Remove our statusline shim from a settings object (uninstall). Only deletes\n * `settings.statusLine` when it points at OUR shim \u2014 a user-owned statusLine is\n * left untouched. Returns true iff it removed the shim. Without this, `hooks\n * uninstall` leaves Claude Code POSTing usage to rulemetric.com via the shim\n * even after the hooks + CLI are gone.\n */\nexport function removeStatuslineShim(settings: Record<string, unknown>): boolean {\n if (!isStatuslineShimInstalled(settings)) return false;\n delete settings.statusLine;\n return true;\n}\n\n/**\n * Returns true if the settings object already points to the shim.\n */\nexport function isStatuslineShimInstalled(settings: Record<string, unknown>): boolean {\n const current = settings.statusLine;\n if (!current) return false;\n if (typeof current === 'string') return current === STATUSLINE_SHIM_PATH;\n if (typeof current === 'object' && current !== null) {\n return (current as Record<string, unknown>).command === STATUSLINE_SHIM_PATH;\n }\n return false;\n}\n"],
5
+ "mappings": ";AAAA,SAAmC,eAAe,iBAAiB;AACnE,SAAS,eAAe;AACxB,SAAS,YAAY;AAGd,IAAM,uBAAuB,KAAK,QAAQ,GAAG,WAAW,0BAA0B;AA0BlF,SAAS,oBAAoB,aAA8B;AAChE,QAAM,aAAa,cACf;AAAA;AAAA,0BAEoB,WAAW,KAC/B;AAEJ,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBf,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiGV,UAAU;AAAA;AAAA;AAIV,gBAAc,sBAAsB,QAAQ,EAAE,UAAU,QAAQ,CAAC;AACjE,YAAU,sBAAsB,GAAK;AACrC,SAAO;AACT;AAGO,IAAM,4BAA4B;AAAA,EACvC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,iBAAiB;AACnB;AAOO,SAAS,sBACd,UACoB;AACpB,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,UAAU;AAC/B,QAAI,YAAY,qBAAsB,QAAO;AAC7C,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,MAAO,QAAoC;AACjD,QAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAI,QAAQ,qBAAsB,QAAO;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AASO,SAAS,qBAAqB,UAA4C;AAC/E,MAAI,CAAC,0BAA0B,QAAQ,EAAG,QAAO;AACjD,SAAO,SAAS;AAChB,SAAO;AACT;AAKO,SAAS,0BAA0B,UAA4C;AACpF,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO,YAAY;AACpD,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAQ,QAAoC,YAAY;AAAA,EAC1D;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -15,7 +15,7 @@ import {
15
15
  apiGet,
16
16
  apiPatch,
17
17
  apiPost
18
- } from "./chunk-CWFVBGX2.js";
18
+ } from "./chunk-ZRLYHBPN.js";
19
19
 
20
20
  // src/lib/handlers/process-insights.ts
21
21
  async function processInsights(payload, helpers) {
@@ -240,4 +240,4 @@ async function buildDataSummaryFromApi(projectPath, projectId) {
240
240
  export {
241
241
  processInsights
242
242
  };
243
- //# sourceMappingURL=chunk-CCSR73HF.js.map
243
+ //# sourceMappingURL=chunk-N5A2IQAK.js.map
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-DGHWRQXL.js";
7
7
  import {
8
8
  apiPatch
9
- } from "./chunk-CWFVBGX2.js";
9
+ } from "./chunk-ZRLYHBPN.js";
10
10
 
11
11
  // src/lib/handlers/process-launch.ts
12
12
  async function resolveWorktreeForLaunch(config) {
@@ -58,4 +58,4 @@ export {
58
58
  resolveWorktreeForLaunch,
59
59
  processLaunch
60
60
  };
61
- //# sourceMappingURL=chunk-VCG4Q53B.js.map
61
+ //# sourceMappingURL=chunk-OTOWLUOJ.js.map
@@ -1,5 +1,6 @@
1
1
  // src/lib/gateway-lifecycle.ts
2
2
  import { spawn } from "node:child_process";
3
+ import { createConnection } from "node:net";
3
4
  import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { join, resolve } from "node:path";
@@ -8,6 +9,27 @@ var GATEWAY_PID_FILE = join(CONFIG_DIR, "gateway.pid");
8
9
  var GATEWAY_LOG_FILE = join(CONFIG_DIR, "gateway.log");
9
10
  var GATEWAY_PORT = 8787;
10
11
  var MITMPROXY_PORT = 8788;
12
+ function isGatewayListening(port = GATEWAY_PORT, timeoutMs = 1500) {
13
+ return new Promise((resolveProbe) => {
14
+ const socket = createConnection({ host: "127.0.0.1", port });
15
+ const finish = (result) => {
16
+ socket.destroy();
17
+ resolveProbe(result);
18
+ };
19
+ socket.setTimeout(timeoutMs);
20
+ socket.once("connect", () => finish(true));
21
+ socket.once("timeout", () => finish(false));
22
+ socket.once("error", () => finish(false));
23
+ });
24
+ }
25
+ async function waitForGatewayListening(port = GATEWAY_PORT, { timeoutMs = 3e3, intervalMs = 150 } = {}) {
26
+ const deadline = Date.now() + timeoutMs;
27
+ for (; ; ) {
28
+ if (await isGatewayListening(port, Math.min(1500, timeoutMs))) return true;
29
+ if (Date.now() >= deadline) return false;
30
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
31
+ }
32
+ }
11
33
  function isGatewayRunning() {
12
34
  if (!existsSync(GATEWAY_PID_FILE)) return false;
13
35
  const content = readFileSync(GATEWAY_PID_FILE, "utf-8").trim();
@@ -88,9 +110,11 @@ export {
88
110
  GATEWAY_LOG_FILE,
89
111
  GATEWAY_PORT,
90
112
  MITMPROXY_PORT,
113
+ isGatewayListening,
114
+ waitForGatewayListening,
91
115
  isGatewayRunning,
92
116
  getGatewayPid,
93
117
  spawnGateway,
94
118
  stopGateway
95
119
  };
96
- //# sourceMappingURL=chunk-FTJRMUID.js.map
120
+ //# sourceMappingURL=chunk-OVGP4OGK.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/gateway-lifecycle.ts"],
4
+ "sourcesContent": ["/**\n * Gateway process lifecycle helpers \u2014 spawn, stop, check status.\n * Used by hooks install/uninstall, session-start hook, and proxy status.\n */\nimport { spawn } from 'node:child_process';\nimport { createConnection } from 'node:net';\nimport { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\nconst CONFIG_DIR = join(homedir(), '.config', 'rulemetric');\nexport const GATEWAY_PID_FILE = join(CONFIG_DIR, 'gateway.pid');\nexport const GATEWAY_LOG_FILE = join(CONFIG_DIR, 'gateway.log');\nexport const GATEWAY_PORT = 8787;\nexport const MITMPROXY_PORT = 8788;\n\n/**\n * TCP liveness probe: does something actually ACCEPT a connection on the port?\n * Distinct from isGatewayRunning() (which only checks the PID is alive) \u2014 a\n * process can be \"running\" per its pidfile while :8787 accepts nothing. This is\n * the check that must gate writing HTTPS_PROXY: never pin Claude Code's proxy\n * at a port with nothing behind it (the fresh-laptop ConnectionRefused footgun).\n */\nexport function isGatewayListening(port: number = GATEWAY_PORT, timeoutMs = 1500): Promise<boolean> {\n return new Promise((resolveProbe) => {\n const socket = createConnection({ host: '127.0.0.1', port });\n const finish = (result: boolean) => {\n socket.destroy();\n resolveProbe(result);\n };\n socket.setTimeout(timeoutMs);\n socket.once('connect', () => finish(true));\n socket.once('timeout', () => finish(false));\n socket.once('error', () => finish(false));\n });\n}\n\n/**\n * Poll isGatewayListening() until it's true or the window elapses. spawnGateway()\n * returns as soon as it writes the PID file \u2014 the detached child hasn't bound\n * :8787 yet (Node bootstrap + module load). A one-shot probe microseconds later\n * fails-closed on every fresh install, so the proxy/deep-capture layer silently\n * never turns on. This tolerates a normal cold-start bind delay.\n */\nexport async function waitForGatewayListening(\n port: number = GATEWAY_PORT,\n { timeoutMs = 3000, intervalMs = 150 }: { timeoutMs?: number; intervalMs?: number } = {},\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n if (await isGatewayListening(port, Math.min(1500, timeoutMs))) return true;\n if (Date.now() >= deadline) return false;\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n\nexport function isGatewayRunning(): boolean {\n if (!existsSync(GATEWAY_PID_FILE)) return false;\n\n const content = readFileSync(GATEWAY_PID_FILE, 'utf-8').trim();\n const pid = parseInt(content, 10);\n if (isNaN(pid)) return false;\n\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function getGatewayPid(): number | null {\n if (!existsSync(GATEWAY_PID_FILE)) return null;\n const content = readFileSync(GATEWAY_PID_FILE, 'utf-8').trim();\n const pid = parseInt(content, 10);\n return isNaN(pid) ? null : pid;\n}\n\nfunction findGatewayEntry(): string {\n // Look for the compiled gateway-entry.js relative to this file's location\n const candidates = [\n resolve(import.meta.dirname, './gateway-entry.js'),\n resolve(import.meta.dirname, '../lib/gateway-entry.js'),\n ];\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n throw new Error(\n 'Could not find gateway-entry.js. Is the CLI built?\\n' +\n 'Searched:\\n' + candidates.map(c => ` ${c}`).join('\\n'),\n );\n}\n\nexport function spawnGateway(cliVersion?: string): number {\n if (isGatewayRunning()) {\n const pid = getGatewayPid();\n if (pid) return pid;\n }\n\n mkdirSync(CONFIG_DIR, { recursive: true });\n\n const entryPath = findGatewayEntry();\n const logFd = openSync(GATEWAY_LOG_FILE, 'a');\n\n const child = spawn(process.execPath, [entryPath], {\n detached: true,\n stdio: ['ignore', logFd, logFd],\n // The gateway inherits the spawning CLI's env. RULEMETRIC_CLI_VERSION\n // identifies the CLI version that started this gateway \u2014 used to\n // detect drift when a newer CLI starts mitmproxy with a different\n // version. RULEMETRIC_CLI_ENTRY is the path to the oclif entry point\n // (process.argv[1]) so the gateway can re-invoke the CLI for\n // `proxy restart` without needing the binary in PATH. Falls back to\n // \"unknown\" if the caller didn't pass them.\n env: {\n ...process.env,\n RULEMETRIC_CLI_VERSION: cliVersion ?? 'unknown',\n RULEMETRIC_CLI_ENTRY: process.argv[1] ?? '',\n },\n });\n\n child.unref();\n\n if (!child.pid) {\n throw new Error('Failed to spawn gateway process');\n }\n\n writeFileSync(GATEWAY_PID_FILE, String(child.pid));\n return child.pid;\n}\n\nexport function stopGateway(): boolean {\n const pid = getGatewayPid();\n if (pid === null) return false;\n\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Process already gone\n }\n\n try { unlinkSync(GATEWAY_PID_FILE); } catch { /* already gone */ }\n return true;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["resolve"]
7
+ }
@@ -17,7 +17,7 @@ import {
17
17
  apiGet,
18
18
  apiPatch,
19
19
  apiPost
20
- } from "./chunk-CWFVBGX2.js";
20
+ } from "./chunk-ZRLYHBPN.js";
21
21
 
22
22
  // src/lib/handlers/process-eval.ts
23
23
  async function processEval(payload, helpers) {
@@ -188,4 +188,4 @@ function buildSummary(results) {
188
188
  export {
189
189
  processEval
190
190
  };
191
- //# sourceMappingURL=chunk-OKCFYPIQ.js.map
191
+ //# sourceMappingURL=chunk-RCMXTLQF.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ApiError,
3
3
  api
4
- } from "./chunk-CWFVBGX2.js";
4
+ } from "./chunk-ZRLYHBPN.js";
5
5
 
6
6
  // src/lib/antigravity-transcript.ts
7
7
  import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, writeFileSync } from "node:fs";
@@ -304,4 +304,4 @@ export {
304
304
  tailAntigravityTranscript,
305
305
  stateApi
306
306
  };
307
- //# sourceMappingURL=chunk-CGUV6TUA.js.map
307
+ //# sourceMappingURL=chunk-RUCSRN7F.js.map
@@ -1,12 +1,12 @@
1
- import {
2
- apiGet
3
- } from "./chunk-CWFVBGX2.js";
4
1
  import {
5
2
  clearActiveOrg,
6
3
  isStale,
7
4
  readActiveOrg,
8
5
  writeActiveOrg
9
6
  } from "./chunk-EKJ2CABV.js";
7
+ import {
8
+ apiGet
9
+ } from "./chunk-ZRLYHBPN.js";
10
10
 
11
11
  // src/lib/active-org-refresh.ts
12
12
  var attempted = false;
@@ -56,4 +56,4 @@ export {
56
56
  bootstrapActiveOrg,
57
57
  _resetRefreshAttempt
58
58
  };
59
- //# sourceMappingURL=chunk-4UJATBTG.js.map
59
+ //# sourceMappingURL=chunk-SLL3OBKN.js.map
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-R56PH7YE.js";
5
5
  import {
6
6
  maybeRefreshActiveOrg
7
- } from "./chunk-4UJATBTG.js";
7
+ } from "./chunk-SLL3OBKN.js";
8
8
  import {
9
9
  ApiError
10
- } from "./chunk-CWFVBGX2.js";
10
+ } from "./chunk-ZRLYHBPN.js";
11
11
  import {
12
12
  isAuthenticated
13
13
  } from "./chunk-W2YERO7E.js";
@@ -60,4 +60,4 @@ ${banner}
60
60
  export {
61
61
  BaseCommand
62
62
  };
63
- //# sourceMappingURL=chunk-CB43W53J.js.map
63
+ //# sourceMappingURL=chunk-SZ7VDCD6.js.map
@@ -1,6 +1,10 @@
1
+ import {
2
+ DEFAULT_API_URL
3
+ } from "./chunk-ZRLYHBPN.js";
4
+
1
5
  // src/dashboard/launcher/api.ts
2
6
  function apiUrl() {
3
- return process.env.RULEMETRIC_API_URL ?? "http://localhost:3000";
7
+ return process.env.RULEMETRIC_API_URL ?? DEFAULT_API_URL;
4
8
  }
5
9
  function authToken() {
6
10
  const t = process.env.RULEMETRIC_API_KEY ?? process.env.RULEMETRIC_ACCESS_TOKEN;
@@ -44,4 +48,4 @@ export {
44
48
  cancelLaunchJob,
45
49
  listLaunchers
46
50
  };
47
- //# sourceMappingURL=chunk-N52AHNBH.js.map
51
+ //# sourceMappingURL=chunk-W4MZXKZ2.js.map