@rulemetric/cli 0.6.9 → 0.7.1
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/base-command.js +4 -3
- package/dist/{chunk-SZ7VDCD6.js → chunk-2HK7RGZB.js} +22 -3
- package/dist/chunk-2HK7RGZB.js.map +7 -0
- package/dist/{chunk-RUCSRN7F.js → chunk-2ORJWTNZ.js} +2 -2
- package/dist/{chunk-SLL3OBKN.js → chunk-37LN5O7M.js} +2 -2
- package/dist/chunk-3S7223UR.js +81 -0
- package/dist/chunk-3S7223UR.js.map +7 -0
- package/dist/{chunk-DAJNO4OI.js → chunk-4LYCDS2N.js} +28 -16
- package/dist/chunk-4LYCDS2N.js.map +7 -0
- package/dist/{chunk-UPXIO7CG.js → chunk-4XAWK6S2.js} +8 -8
- package/dist/{chunk-QD6X4L4L.js → chunk-5BAOGNUH.js} +4 -4
- package/dist/chunk-5EDMGK3W.js +39 -0
- package/dist/chunk-5EDMGK3W.js.map +7 -0
- package/dist/{chunk-5ADHO5QF.js → chunk-6NBS5XFS.js} +6 -6
- package/dist/{chunk-BBLTN35F.js → chunk-7DULPSFU.js} +49 -20
- package/dist/chunk-7DULPSFU.js.map +7 -0
- package/dist/chunk-7LVVKQMZ.js +98 -0
- package/dist/chunk-7LVVKQMZ.js.map +7 -0
- package/dist/{chunk-PXVQLD7S.js → chunk-7NNFYWKK.js} +48 -1
- package/dist/chunk-7NNFYWKK.js.map +7 -0
- package/dist/{chunk-DTFXCHHI.js → chunk-DNKSVWMD.js} +4 -4
- package/dist/{chunk-CNMWYYSV.js → chunk-DUJSRBLP.js} +2 -2
- package/dist/chunk-EARWG4EV.js +22 -0
- package/dist/chunk-EARWG4EV.js.map +7 -0
- package/dist/{chunk-6EMZI4Y3.js → chunk-EI5BHWXA.js} +437 -401
- package/dist/chunk-EI5BHWXA.js.map +7 -0
- package/dist/{chunk-HNZLEEG2.js → chunk-EJ3YAFDP.js} +4 -4
- package/dist/{chunk-HNZLEEG2.js.map → chunk-EJ3YAFDP.js.map} +2 -2
- package/dist/{chunk-YSEQWCN4.js → chunk-EYPAAGN5.js} +2 -2
- package/dist/{chunk-JEJ3J5J6.js → chunk-GIUBARWS.js} +2 -2
- package/dist/chunk-GQNJO2P2.js +102 -0
- package/dist/chunk-GQNJO2P2.js.map +7 -0
- package/dist/chunk-ILYDELUE.js +46 -0
- package/dist/chunk-ILYDELUE.js.map +7 -0
- package/dist/chunk-J6QFW4LD.js +55 -0
- package/dist/chunk-J6QFW4LD.js.map +7 -0
- package/dist/{chunk-OTOWLUOJ.js → chunk-JULOLTZS.js} +2 -2
- package/dist/{chunk-DR5WLIOU.js → chunk-KJKBDQ4D.js} +2 -2
- package/dist/{chunk-XN23W5HL.js → chunk-LXKPNATC.js} +93 -7
- package/dist/chunk-LXKPNATC.js.map +7 -0
- package/dist/{chunk-N5A2IQAK.js → chunk-OK3I555A.js} +2 -2
- package/dist/{chunk-XNNOXGP6.js → chunk-OKWCKCPE.js} +38 -19
- package/dist/chunk-OKWCKCPE.js.map +7 -0
- package/dist/{chunk-AHDRN6NE.js → chunk-OO7JDFS4.js} +5 -5
- package/dist/{chunk-N2X35ITW.js → chunk-OQBBVTDG.js} +11 -1
- package/dist/{chunk-N2X35ITW.js.map → chunk-OQBBVTDG.js.map} +2 -2
- package/dist/{chunk-3QH73QOQ.js → chunk-PED7L5GX.js} +2 -2
- package/dist/{chunk-VBGUNFKY.js → chunk-PMI56ED5.js} +5 -5
- package/dist/{chunk-RCMXTLQF.js → chunk-RMTT4KDY.js} +2 -2
- package/dist/{chunk-W4MZXKZ2.js → chunk-UKMLICWS.js} +2 -2
- package/dist/{chunk-3POYC5NA.js → chunk-XK42ZBDE.js} +3 -3
- package/dist/{chunk-ZSGKRCKD.js → chunk-XUV6DDBF.js} +2 -2
- package/dist/{chunk-LAKZUE35.js → chunk-Y3WD5CSW.js} +2 -2
- package/dist/{chunk-3QVMIJUL.js → chunk-YOBA3NAM.js} +5 -5
- package/dist/{chunk-ZRLYHBPN.js → chunk-ZUJBM6HE.js} +20 -2
- package/dist/chunk-ZUJBM6HE.js.map +7 -0
- package/dist/commands/antigravity/backfill.js +5 -4
- package/dist/commands/antigravity/backfill.js.map +1 -1
- package/dist/commands/auth/create-key.js +4 -3
- package/dist/commands/auth/create-key.js.map +1 -1
- package/dist/commands/auth/login.js +13 -9
- package/dist/commands/auth/login.js.map +2 -2
- package/dist/commands/auth/logout.js +4 -3
- package/dist/commands/auth/logout.js.map +1 -1
- package/dist/commands/auth/revoke-key.js +4 -3
- package/dist/commands/auth/revoke-key.js.map +1 -1
- package/dist/commands/auth/signup.js +13 -9
- package/dist/commands/auth/signup.js.map +2 -2
- package/dist/commands/auth/status.js +4 -3
- package/dist/commands/auth/status.js.map +1 -1
- package/dist/commands/catalog/index.js +4 -3
- package/dist/commands/catalog/index.js.map +1 -1
- package/dist/commands/catalog/recommend.js +4 -3
- package/dist/commands/catalog/recommend.js.map +1 -1
- package/dist/commands/convert.js +4 -3
- package/dist/commands/convert.js.map +1 -1
- package/dist/commands/converters.js +4 -3
- package/dist/commands/converters.js.map +1 -1
- package/dist/commands/crons/list.js +4 -3
- package/dist/commands/crons/list.js.map +1 -1
- package/dist/commands/crons/run.js +4 -3
- package/dist/commands/crons/run.js.map +1 -1
- package/dist/commands/dashboard.js +4 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/diagnostics.js +117 -0
- package/dist/commands/diagnostics.js.map +7 -0
- package/dist/commands/eval-targets/create.js +4 -3
- package/dist/commands/eval-targets/create.js.map +1 -1
- package/dist/commands/eval-targets/list.js +4 -3
- package/dist/commands/eval-targets/list.js.map +1 -1
- package/dist/commands/evals/agent.js +41 -20
- package/dist/commands/evals/agent.js.map +2 -2
- package/dist/commands/evals/benchmark.js +5 -4
- package/dist/commands/evals/benchmark.js.map +1 -1
- package/dist/commands/evals/compare.js +4 -3
- package/dist/commands/evals/compare.js.map +1 -1
- package/dist/commands/evals/create.js +4 -3
- package/dist/commands/evals/create.js.map +1 -1
- package/dist/commands/evals/optimize-description.js +4 -3
- package/dist/commands/evals/optimize-description.js.map +1 -1
- package/dist/commands/evals/optimize.js +4 -3
- package/dist/commands/evals/optimize.js.map +1 -1
- package/dist/commands/evals/run.js +4 -3
- package/dist/commands/evals/run.js.map +1 -1
- package/dist/commands/gateway/ensure.js +75 -5
- package/dist/commands/gateway/ensure.js.map +2 -2
- package/dist/commands/hooks/install.js +76 -23
- package/dist/commands/hooks/install.js.map +2 -2
- package/dist/commands/hooks/run.js +4 -3
- package/dist/commands/hooks/run.js.map +1 -1
- package/dist/commands/hooks/uninstall.js +31 -40
- package/dist/commands/hooks/uninstall.js.map +2 -2
- package/dist/commands/import.js +4 -3
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/instructions/create.js +4 -3
- package/dist/commands/instructions/create.js.map +1 -1
- package/dist/commands/instructions/delete.js +4 -3
- package/dist/commands/instructions/delete.js.map +1 -1
- package/dist/commands/instructions/fork.js +4 -3
- package/dist/commands/instructions/fork.js.map +1 -1
- package/dist/commands/instructions/get.js +4 -3
- package/dist/commands/instructions/get.js.map +1 -1
- package/dist/commands/instructions/list.js +4 -3
- package/dist/commands/instructions/list.js.map +1 -1
- package/dist/commands/instructions/promote.js +4 -3
- package/dist/commands/instructions/promote.js.map +1 -1
- package/dist/commands/instructions/pull.js +4 -3
- package/dist/commands/instructions/pull.js.map +1 -1
- package/dist/commands/instructions/upstream.js +4 -3
- package/dist/commands/instructions/upstream.js.map +1 -1
- package/dist/commands/instructions/versions.js +4 -3
- package/dist/commands/instructions/versions.js.map +1 -1
- package/dist/commands/launcher.js +4 -3
- package/dist/commands/launcher.js.map +1 -1
- package/dist/commands/org/current.js +4 -3
- package/dist/commands/org/current.js.map +1 -1
- package/dist/commands/org/list.js +4 -3
- package/dist/commands/org/list.js.map +1 -1
- package/dist/commands/org/switch.js +4 -3
- package/dist/commands/org/switch.js.map +1 -1
- package/dist/commands/packs/add-instruction.js +4 -3
- package/dist/commands/packs/add-instruction.js.map +1 -1
- package/dist/commands/packs/apply.js +4 -3
- package/dist/commands/packs/apply.js.map +1 -1
- package/dist/commands/packs/create.js +4 -3
- package/dist/commands/packs/create.js.map +1 -1
- package/dist/commands/packs/fork.js +4 -3
- package/dist/commands/packs/fork.js.map +1 -1
- package/dist/commands/packs/get.js +4 -3
- package/dist/commands/packs/get.js.map +1 -1
- package/dist/commands/packs/list.js +4 -3
- package/dist/commands/packs/list.js.map +1 -1
- package/dist/commands/packs/remove-instruction.js +4 -3
- package/dist/commands/packs/remove-instruction.js.map +1 -1
- package/dist/commands/proxy/env.js +7 -4
- package/dist/commands/proxy/env.js.map +1 -1
- package/dist/commands/proxy/logs.js +4 -3
- package/dist/commands/proxy/logs.js.map +1 -1
- package/dist/commands/proxy/restart.js +4 -3
- package/dist/commands/proxy/restart.js.map +1 -1
- package/dist/commands/proxy/setup.js +20 -14
- package/dist/commands/proxy/setup.js.map +2 -2
- package/dist/commands/proxy/start.js +29 -15
- package/dist/commands/proxy/start.js.map +2 -2
- package/dist/commands/proxy/status.js +16 -13
- package/dist/commands/proxy/status.js.map +2 -2
- package/dist/commands/proxy/stop.js +21 -12
- package/dist/commands/proxy/stop.js.map +2 -2
- package/dist/commands/research/backfill.js +5 -4
- package/dist/commands/research/backfill.js.map +1 -1
- package/dist/commands/research/tail-transcript.js +5 -4
- package/dist/commands/research/tail-transcript.js.map +1 -1
- package/dist/commands/service/install.js +6 -6
- package/dist/commands/service/status.js +4 -3
- package/dist/commands/service/status.js.map +1 -1
- package/dist/commands/service/uninstall.js +4 -3
- package/dist/commands/service/uninstall.js.map +1 -1
- package/dist/commands/sessions/analyze.js +4 -3
- package/dist/commands/sessions/analyze.js.map +1 -1
- package/dist/commands/sessions/end.js +4 -3
- package/dist/commands/sessions/end.js.map +1 -1
- package/dist/commands/sessions/import.js +4 -3
- package/dist/commands/sessions/import.js.map +1 -1
- package/dist/commands/sessions/list.js +4 -3
- package/dist/commands/sessions/list.js.map +1 -1
- package/dist/commands/sessions/start.js +4 -3
- package/dist/commands/sessions/start.js.map +1 -1
- package/dist/commands/setup.js +10 -6
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills/export-approved.js +4 -3
- package/dist/commands/skills/export-approved.js.map +1 -1
- package/dist/commands/skills/install.js +4 -3
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/list.js +4 -3
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/publish.js +4 -3
- package/dist/commands/skills/publish.js.map +1 -1
- package/dist/commands/skills/search.js +4 -3
- package/dist/commands/skills/search.js.map +1 -1
- package/dist/commands/skills/update.js +4 -3
- package/dist/commands/skills/update.js.map +1 -1
- package/dist/commands/suggestions/accept.js +4 -3
- package/dist/commands/suggestions/accept.js.map +1 -1
- package/dist/commands/suggestions/dismiss.js +4 -3
- package/dist/commands/suggestions/dismiss.js.map +1 -1
- package/dist/commands/suggestions/list.js +4 -3
- package/dist/commands/suggestions/list.js.map +1 -1
- package/dist/commands/suggestions/refresh.js +4 -3
- package/dist/commands/suggestions/refresh.js.map +1 -1
- package/dist/dashboard/Dashboard.js +16 -16
- package/dist/dashboard/data.js +2 -2
- package/dist/dashboard/launcher/ActiveJobsList.js +2 -2
- package/dist/dashboard/launcher/LauncherOverlay.js +7 -7
- package/dist/dashboard/launcher/NewLaunchForm.js +2 -2
- package/dist/dashboard/launcher/api.js +2 -2
- package/dist/dashboard/launcher/useLaunchJobs.js +3 -3
- package/dist/lib/active-org-refresh.js +2 -2
- package/dist/lib/agent-loop.js +19 -17
- package/dist/lib/antigravity-transcript.js +2 -2
- package/dist/lib/api-client.js +1 -1
- package/dist/lib/capture-hosts.js +8 -0
- package/dist/lib/capture-hosts.js.map +7 -0
- package/dist/lib/diagnostics.js +12 -0
- package/dist/lib/diagnostics.js.map +7 -0
- package/dist/lib/editor-proxy-pins.js +14 -0
- package/dist/lib/editor-proxy-pins.js.map +7 -0
- package/dist/lib/ensure-api-key.js +3 -3
- package/dist/lib/eval-benchmark.js +2 -2
- package/dist/lib/eval-conversation-handler.js +2 -2
- package/dist/lib/gateway-entry.js +39 -1
- package/dist/lib/gateway-entry.js.map +2 -2
- package/dist/lib/gateway-identity.js +16 -0
- package/dist/lib/gateway-identity.js.map +7 -0
- package/dist/lib/gateway-lifecycle.js +9 -3
- package/dist/lib/gateway.js +3 -1
- package/dist/lib/handlers/cron-suggest-instructions.js +3 -3
- package/dist/lib/handlers/process-announcement.js +3 -3
- package/dist/lib/handlers/process-changelog.js +2 -2
- package/dist/lib/handlers/process-conversation.js +3 -3
- package/dist/lib/handlers/process-eval.js +2 -2
- package/dist/lib/handlers/process-insights.js +2 -2
- package/dist/lib/handlers/process-launch.js +2 -2
- package/dist/lib/handlers/process-run-cleanup.js +2 -2
- package/dist/lib/handlers/process-session-goal.js +2 -2
- package/dist/lib/hooks-config.js +18 -1
- package/dist/lib/manual-tasks.js +5 -5
- package/dist/lib/pid-identity.js +14 -0
- package/dist/lib/pid-identity.js.map +7 -0
- package/dist/lib/proxy-respawn.js +12 -0
- package/dist/lib/proxy-respawn.js.map +7 -0
- package/dist/lib/research-client.js +2 -2
- package/dist/lib/setup-steps.js +7 -4
- package/dist/lib/statusline-shim.js +3 -1
- package/dist/lib/telemetry.js +4 -2
- package/dist/lib/terminal-target-heartbeat.js +2 -2
- package/oclif.manifest.json +34 -1
- package/package.json +4 -4
- package/dist/chunk-6EMZI4Y3.js.map +0 -7
- package/dist/chunk-BBLTN35F.js.map +0 -7
- package/dist/chunk-DAJNO4OI.js.map +0 -7
- package/dist/chunk-PXVQLD7S.js.map +0 -7
- package/dist/chunk-SZ7VDCD6.js.map +0 -7
- package/dist/chunk-TX2HKJLA.js +0 -55
- package/dist/chunk-TX2HKJLA.js.map +0 -7
- package/dist/chunk-XN23W5HL.js.map +0 -7
- package/dist/chunk-XNNOXGP6.js.map +0 -7
- package/dist/chunk-ZRLYHBPN.js.map +0 -7
- /package/dist/{chunk-RUCSRN7F.js.map → chunk-2ORJWTNZ.js.map} +0 -0
- /package/dist/{chunk-SLL3OBKN.js.map → chunk-37LN5O7M.js.map} +0 -0
- /package/dist/{chunk-UPXIO7CG.js.map → chunk-4XAWK6S2.js.map} +0 -0
- /package/dist/{chunk-QD6X4L4L.js.map → chunk-5BAOGNUH.js.map} +0 -0
- /package/dist/{chunk-5ADHO5QF.js.map → chunk-6NBS5XFS.js.map} +0 -0
- /package/dist/{chunk-DTFXCHHI.js.map → chunk-DNKSVWMD.js.map} +0 -0
- /package/dist/{chunk-CNMWYYSV.js.map → chunk-DUJSRBLP.js.map} +0 -0
- /package/dist/{chunk-YSEQWCN4.js.map → chunk-EYPAAGN5.js.map} +0 -0
- /package/dist/{chunk-JEJ3J5J6.js.map → chunk-GIUBARWS.js.map} +0 -0
- /package/dist/{chunk-OTOWLUOJ.js.map → chunk-JULOLTZS.js.map} +0 -0
- /package/dist/{chunk-DR5WLIOU.js.map → chunk-KJKBDQ4D.js.map} +0 -0
- /package/dist/{chunk-N5A2IQAK.js.map → chunk-OK3I555A.js.map} +0 -0
- /package/dist/{chunk-AHDRN6NE.js.map → chunk-OO7JDFS4.js.map} +0 -0
- /package/dist/{chunk-3QH73QOQ.js.map → chunk-PED7L5GX.js.map} +0 -0
- /package/dist/{chunk-VBGUNFKY.js.map → chunk-PMI56ED5.js.map} +0 -0
- /package/dist/{chunk-RCMXTLQF.js.map → chunk-RMTT4KDY.js.map} +0 -0
- /package/dist/{chunk-W4MZXKZ2.js.map → chunk-UKMLICWS.js.map} +0 -0
- /package/dist/{chunk-3POYC5NA.js.map → chunk-XK42ZBDE.js.map} +0 -0
- /package/dist/{chunk-ZSGKRCKD.js.map → chunk-XUV6DDBF.js.map} +0 -0
- /package/dist/{chunk-LAKZUE35.js.map → chunk-Y3WD5CSW.js.map} +0 -0
- /package/dist/{chunk-3QVMIJUL.js.map → chunk-YOBA3NAM.js.map} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/telemetry.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash, randomUUID } from 'node:crypto';\nimport { hostname, platform } from 'node:os';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { api, DEFAULT_API_URL } from './api-client.js';\nimport { getDefaultConfigDir, loadEnvFileAuth } from './auth.js';\n\n/**\n * CLI onboarding telemetry \u2014 records setup lifecycle events (install \u2192 auth \u2192\n * hooks \u2192 worker \u2192 capture) so a remote machine coming online is VISIBLE\n * without a relayed text report. DISCLOSED (README \"Telemetry\") and OPT-OUTABLE:\n * set RULEMETRIC_NO_TELEMETRY=1 or the cross-tool standard DO_NOT_TRACK=1 and\n * nothing is sent. Best-effort: never throws, never blocks a command.\n *\n * What is sent: a per-machine device id (a random UUID minted once, NOT your\n * hostname), the event name, CLI version, os (`process.platform`), and small\n * non-sensitive flags (e.g. {proxy:true}). No prompts, no code, no file paths.\n */\n\nconst DEVICE_ID_FILE = 'device-id';\n\nexport function telemetryDisabled(): boolean {\n const off = (v: string | undefined) => v === '1' || v === 'true';\n return off(process.env.RULEMETRIC_NO_TELEMETRY) || off(process.env.DO_NOT_TRACK);\n}\n\n/**\n * Stable, non-identifying per-machine id. Minted once and cached in the config\n * dir. Salted-hashed so even the file contents don't reveal the hostname.\n */\nexport function getDeviceId(): string {\n const dir = getDefaultConfigDir();\n const path = join(dir, DEVICE_ID_FILE);\n try {\n if (existsSync(path)) {\n const cached = readFileSync(path, 'utf-8').trim();\n if (cached) return cached;\n }\n } catch {\n /* fall through to mint */\n }\n // Seed from a random UUID (primary) + a hostname hash (stable fallback signal).\n const id = createHash('sha256')\n .update(`${randomUUID()}:${hostname()}`)\n .digest('hex')\n .slice(0, 32);\n try {\n mkdirSync(dir, { recursive: true });\n writeFileSync(path, id, { mode: 0o600 });\n } catch {\n /* non-fatal \u2014 a non-persisted id still lets a single run correlate */\n }\n return id;\n}\n\n/**\n * Fire-and-forget a setup event. Swallows every error (offline, unauth, 4xx) \u2014\n * telemetry must never affect the command the user actually ran.\n */\nexport async function emitSetupEvent(\n event: string,\n props: Record<string, unknown> = {},\n cliVersion?: string,\n): Promise<void> {\n if (telemetryDisabled()) return;\n try {\n // Bounded: without a timeout, a held connection keeps the process alive\n // (and the user waiting at what looks like a finished command) for up to\n // undici's multi-minute default after success was already printed.\n await api('/api/telemetry/cli-events', {\n method: 'POST',\n body: {\n event,\n deviceId: getDeviceId(),\n cliVersion,\n os: platform(),\n props,\n },\n timeoutMs: 5_000,\n });\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Fire-and-forget an onboarding ERROR beacon (availability failure: 5xx / 502 /\n * client timeout / connection refused). Unlike `emitSetupEvent`, this:\n * 1. is UNAUTHENTICATED \u2014 it hits the public POST /api/telemetry/cli-error, so\n * it works before the user has any credentials (the exact pre-auth failures\n * that authenticated telemetry can never report), and\n * 2. is sent DIRECT, bypassing HTTPS_PROXY \u2014 a broken local gateway\n * (HTTPS_PROXY \u2192 :8787) is itself a prime cause of the 502s this beacon\n * exists to report, so routing the beacon through it would suppress the very\n * signal we need. We record whether a proxy WAS configured (`proxied`).\n * Best-effort: swallows every error, bounded to 4s, never throws or blocks.\n */\nexport async function emitErrorBeacon(fields: {\n command?: string;\n statusCode?: number;\n message?: string;\n cliVersion?: string;\n event?: string;\n props?: Record<string, unknown>;\n}): Promise<void> {\n if (telemetryDisabled()) return;\n\n const base = process.env.RULEMETRIC_API_URL ?? loadEnvFileAuth().apiUrl ?? DEFAULT_API_URL;\n const url = `${base}/api/telemetry/cli-error`;\n\n // Send direct: unset the proxy env for the duration of the request, then\n // restore it so subsequent `claude -p` calls still route through the gateway.\n const savedHttps = process.env.HTTPS_PROXY;\n const savedHttp = process.env.HTTP_PROXY;\n const proxied = savedHttps !== undefined || savedHttp !== undefined;\n delete process.env.HTTPS_PROXY;\n delete process.env.HTTP_PROXY;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 4000);\n try {\n await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': `rulemetric-cli/${fields.cliVersion ?? 'unknown'}`,\n },\n // NO Authorization header \u2014 this endpoint is unauthenticated by design.\n body: JSON.stringify({\n event: fields.event ?? 'onboarding_error',\n deviceId: getDeviceId(),\n command: fields.command,\n statusCode: fields.statusCode,\n message: fields.message ? fields.message.slice(0, 500) : undefined,\n cliVersion: fields.cliVersion,\n os: platform(),\n nodeVersion: process.version,\n proxied,\n props: fields.props,\n }),\n signal: controller.signal,\n });\n } catch {\n /* best-effort */\n } finally {\n clearTimeout(timer);\n if (savedHttps !== undefined) process.env.HTTPS_PROXY = savedHttps;\n if (savedHttp !== undefined) process.env.HTTP_PROXY = savedHttp;\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;AAAA,SAAS,YAAY,kBAAkB;AACvC,SAAS,UAAU,gBAAgB;AACnC,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,YAAY;AAgBrB,IAAM,iBAAiB;AAEhB,SAAS,oBAA6B;AAC3C,QAAM,MAAM,CAAC,MAA0B,MAAM,OAAO,MAAM;AAC1D,SAAO,IAAI,QAAQ,IAAI,uBAAuB,KAAK,IAAI,QAAQ,IAAI,YAAY;AACjF;AAMO,SAAS,cAAsB;AACpC,QAAM,MAAM,oBAAoB;AAChC,QAAM,OAAO,KAAK,KAAK,cAAc;AACrC,MAAI;AACF,QAAI,WAAW,IAAI,GAAG;AACpB,YAAM,SAAS,aAAa,MAAM,OAAO,EAAE,KAAK;AAChD,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,QAAM,KAAK,WAAW,QAAQ,EAC3B,OAAO,GAAG,WAAW,CAAC,IAAI,SAAS,CAAC,EAAE,EACtC,OAAO,KAAK,EACZ,MAAM,GAAG,EAAE;AACd,MAAI;AACF,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,kBAAc,MAAM,IAAI,EAAE,MAAM,IAAM,CAAC;AAAA,EACzC,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAMA,eAAsB,eACpB,OACA,QAAiC,CAAC,GAClC,YACe;AACf,MAAI,kBAAkB,EAAG;AACzB,MAAI;AAIF,UAAM,IAAI,6BAA6B;AAAA,MACrC,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,QACA,UAAU,YAAY;AAAA,QACtB;AAAA,QACA,IAAI,SAAS;AAAA,QACb;AAAA,MACF;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAcA,eAAsB,gBAAgB,QAOpB;AAChB,MAAI,kBAAkB,EAAG;AAEzB,QAAM,OAAO,QAAQ,IAAI,sBAAsB,gBAAgB,EAAE,UAAU;AAC3E,QAAM,MAAM,GAAG,IAAI;AAInB,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,YAAY,QAAQ,IAAI;AAC9B,QAAM,UAAU,eAAe,UAAa,cAAc;AAC1D,SAAO,QAAQ,IAAI;AACnB,SAAO,QAAQ,IAAI;AAEnB,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AACvD,MAAI;AACF,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,cAAc,kBAAkB,OAAO,cAAc,SAAS;AAAA,MAChE;AAAA;AAAA,MAEA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,OAAO,SAAS;AAAA,QACvB,UAAU,YAAY;AAAA,QACtB,SAAS,OAAO;AAAA,QAChB,YAAY,OAAO;AAAA,QACnB,SAAS,OAAO,UAAU,OAAO,QAAQ,MAAM,GAAG,GAAG,IAAI;AAAA,QACzD,YAAY,OAAO;AAAA,QACnB,IAAI,SAAS;AAAA,QACb,aAAa,QAAQ;AAAA,QACrB;AAAA,QACA,OAAO,OAAO;AAAA,MAChB,CAAC;AAAA,MACD,QAAQ,WAAW;AAAA,IACrB,CAAC;AAAA,EACH,QAAQ;AAAA,EAER,UAAE;AACA,iBAAa,KAAK;AAClB,QAAI,eAAe,OAAW,SAAQ,IAAI,cAAc;AACxD,QAAI,cAAc,OAAW,SAAQ,IAAI,aAAa;AAAA,EACxD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/lib/proxy-respawn.ts
|
|
2
|
+
var DEAD_CHECKS_BEFORE_RESPAWN = 5;
|
|
3
|
+
var RESPAWN_COOLDOWN_MS = 10 * 6e4;
|
|
4
|
+
var MitmproxyRespawnMonitor = class {
|
|
5
|
+
constructor(deps) {
|
|
6
|
+
this.deps = deps;
|
|
7
|
+
}
|
|
8
|
+
deadStreak = 0;
|
|
9
|
+
lastAttemptAt = Number.NEGATIVE_INFINITY;
|
|
10
|
+
skipLoggedThisStreak = false;
|
|
11
|
+
/** Feed one health-check result (true = mitmproxy answered). */
|
|
12
|
+
onHealthCheck(alive) {
|
|
13
|
+
if (alive) {
|
|
14
|
+
this.deadStreak = 0;
|
|
15
|
+
this.skipLoggedThisStreak = false;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.deadStreak++;
|
|
19
|
+
if (this.deadStreak < DEAD_CHECKS_BEFORE_RESPAWN) return;
|
|
20
|
+
const now = (this.deps.now ?? Date.now)();
|
|
21
|
+
if (now - this.lastAttemptAt < RESPAWN_COOLDOWN_MS) return;
|
|
22
|
+
if (!this.deps.hasCrashEvidence()) {
|
|
23
|
+
if (!this.skipLoggedThisStreak) {
|
|
24
|
+
this.skipLoggedThisStreak = true;
|
|
25
|
+
this.deps.log(
|
|
26
|
+
"mitmproxy is down with no crash evidence (no local proxy.pid) \u2014 not auto-restarting (deliberate `proxy stop` or Docker-managed)"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.lastAttemptAt = now;
|
|
32
|
+
this.deps.log(
|
|
33
|
+
`mitmproxy dead for ${this.deadStreak} consecutive checks \u2014 attempting auto-restart (at most once per ${Math.round(RESPAWN_COOLDOWN_MS / 6e4)}min)`
|
|
34
|
+
);
|
|
35
|
+
if (!this.deps.respawn()) {
|
|
36
|
+
this.deps.log("auto-restart unavailable \u2014 run `rulemetric proxy start` to restore deep capture");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
DEAD_CHECKS_BEFORE_RESPAWN,
|
|
43
|
+
RESPAWN_COOLDOWN_MS,
|
|
44
|
+
MitmproxyRespawnMonitor
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=chunk-ILYDELUE.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/proxy-respawn.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Conservative mitmproxy auto-restart, driven by the gateway's 3s health\n * checks. A mid-session mitmproxy crash otherwise stays silent \u2014 the gateway\n * routes direct by design, so deep capture is simply gone until the next\n * session's session-start hook runs.\n *\n * Guard rails (this must never become a restart loop, and must never fight a\n * deliberate `proxy stop`):\n * - respawn fires only after DEAD_CHECKS_BEFORE_RESPAWN consecutive dead\n * checks (a transient blip never triggers it);\n * - at most one attempt per RESPAWN_COOLDOWN_MS window (in-memory \u2014 a\n * gateway restart resets the window, which is fine: it also resets the\n * dead-streak requirement);\n * - only with crash evidence: a live proxy.pid file. `proxy stop` removes\n * the PID file, so a deliberate stop is never overridden. Docker-managed\n * proxies (pidfile `docker:<name>`) are Docker's to restart.\n *\n * Pure state machine \u2014 time, crash evidence, and the spawn are injected so\n * the transition logic is unit-testable without timers or processes.\n */\n\n/** ~15s of confirmed downtime at the gateway's 3s health cadence. */\nexport const DEAD_CHECKS_BEFORE_RESPAWN = 5;\nexport const RESPAWN_COOLDOWN_MS = 10 * 60_000;\n\nexport interface RespawnMonitorDeps {\n /** Clock override. Used in tests. */\n now?: () => number;\n /** Whether the local proxy.pid file evidences a crash (vs a deliberate stop). */\n hasCrashEvidence: () => boolean;\n /** Fire one detached respawn attempt. Returns false if respawn is unavailable. */\n respawn: () => boolean;\n log: (msg: string) => void;\n}\n\nexport class MitmproxyRespawnMonitor {\n private deadStreak = 0;\n private lastAttemptAt = Number.NEGATIVE_INFINITY;\n private skipLoggedThisStreak = false;\n\n constructor(private readonly deps: RespawnMonitorDeps) {}\n\n /** Feed one health-check result (true = mitmproxy answered). */\n onHealthCheck(alive: boolean): void {\n if (alive) {\n this.deadStreak = 0;\n this.skipLoggedThisStreak = false;\n return;\n }\n\n this.deadStreak++;\n if (this.deadStreak < DEAD_CHECKS_BEFORE_RESPAWN) return;\n\n const now = (this.deps.now ?? Date.now)();\n if (now - this.lastAttemptAt < RESPAWN_COOLDOWN_MS) return;\n\n if (!this.deps.hasCrashEvidence()) {\n if (!this.skipLoggedThisStreak) {\n this.skipLoggedThisStreak = true;\n this.deps.log(\n 'mitmproxy is down with no crash evidence (no local proxy.pid) \u2014 ' +\n 'not auto-restarting (deliberate `proxy stop` or Docker-managed)',\n );\n }\n return;\n }\n\n this.lastAttemptAt = now;\n this.deps.log(\n `mitmproxy dead for ${this.deadStreak} consecutive checks \u2014 attempting auto-restart ` +\n `(at most once per ${Math.round(RESPAWN_COOLDOWN_MS / 60_000)}min)`,\n );\n if (!this.deps.respawn()) {\n this.deps.log('auto-restart unavailable \u2014 run `rulemetric proxy start` to restore deep capture');\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAsBO,IAAM,6BAA6B;AACnC,IAAM,sBAAsB,KAAK;AAYjC,IAAM,0BAAN,MAA8B;AAAA,EAKnC,YAA6B,MAA0B;AAA1B;AAAA,EAA2B;AAAA,EAJhD,aAAa;AAAA,EACb,gBAAgB,OAAO;AAAA,EACvB,uBAAuB;AAAA;AAAA,EAK/B,cAAc,OAAsB;AAClC,QAAI,OAAO;AACT,WAAK,aAAa;AAClB,WAAK,uBAAuB;AAC5B;AAAA,IACF;AAEA,SAAK;AACL,QAAI,KAAK,aAAa,2BAA4B;AAElD,UAAM,OAAO,KAAK,KAAK,OAAO,KAAK,KAAK;AACxC,QAAI,MAAM,KAAK,gBAAgB,oBAAqB;AAEpD,QAAI,CAAC,KAAK,KAAK,iBAAiB,GAAG;AACjC,UAAI,CAAC,KAAK,sBAAsB;AAC9B,aAAK,uBAAuB;AAC5B,aAAK,KAAK;AAAA,UACR;AAAA,QAEF;AAAA,MACF;AACA;AAAA,IACF;AAEA,SAAK,gBAAgB;AACrB,SAAK,KAAK;AAAA,MACR,sBAAsB,KAAK,UAAU,wEACd,KAAK,MAAM,sBAAsB,GAAM,CAAC;AAAA,IACjE;AACA,QAAI,CAAC,KAAK,KAAK,QAAQ,GAAG;AACxB,WAAK,KAAK,IAAI,sFAAiF;AAAA,IACjG;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/lib/diagnostics.ts
|
|
2
|
+
function parseNodeMajorMinor(v) {
|
|
3
|
+
const m = /^v?(\d+)\.(\d+)/.exec(v.trim());
|
|
4
|
+
if (!m) return { major: 0, minor: 0 };
|
|
5
|
+
return { major: Number(m[1]), minor: Number(m[2]) };
|
|
6
|
+
}
|
|
7
|
+
function isProxyBreakingNode(v) {
|
|
8
|
+
const { major, minor } = parseNodeMajorMinor(v);
|
|
9
|
+
if (major === 0) return false;
|
|
10
|
+
return major < 20 || major === 20 && minor < 11;
|
|
11
|
+
}
|
|
12
|
+
function diagnose(input) {
|
|
13
|
+
const oldNode = isProxyBreakingNode(input.nodeVersion);
|
|
14
|
+
if (!input.directApiOk) {
|
|
15
|
+
return {
|
|
16
|
+
severity: "fail",
|
|
17
|
+
headline: "Can't reach the API directly (proxy bypassed).",
|
|
18
|
+
hints: [
|
|
19
|
+
"Check your network/DNS/firewall and that you can reach https://rulemetric.com/health.",
|
|
20
|
+
"If a corporate proxy is required, it must allow rulemetric.com."
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (input.httpsProxySet && input.proxyPathOk === false) {
|
|
25
|
+
const hints = [
|
|
26
|
+
"Direct reachability works but requests through your local proxy fail \u2014 your gateway/mitmproxy is the problem, NOT the API.",
|
|
27
|
+
input.gatewayAlive ? "Gateway (:8787) is up but not routing successfully." : "Gateway (:8787) is NOT listening \u2014 Claude Code/CLI traffic pointed at it will 502. Run `rulemetric hooks install` to restore it.",
|
|
28
|
+
input.mitmproxyAlive ? void 0 : "mitmproxy (:8788) is down \u2014 the gateway should route direct, so a 502 here points at the gateway itself.",
|
|
29
|
+
oldNode ? `Node ${input.nodeVersion} is < 20.11, which breaks the bundled proxy \u2014 upgrade Node to \u2265 20.11, or reinstall hooks with --no-proxy.` : void 0,
|
|
30
|
+
"Quick unblock: `rulemetric hooks install --no-proxy` (captures via hooks, no proxy), or `rulemetric proxy restart`."
|
|
31
|
+
].filter((h) => Boolean(h));
|
|
32
|
+
return { severity: "fail", headline: "Your local proxy/gateway can't reach the API \u2014 this is the 502.", hints };
|
|
33
|
+
}
|
|
34
|
+
if (input.httpsProxySet && oldNode) {
|
|
35
|
+
return {
|
|
36
|
+
severity: "warn",
|
|
37
|
+
headline: `Node ${input.nodeVersion} is < 20.11 with a proxy configured \u2014 the proxy is likely dead.`,
|
|
38
|
+
hints: [
|
|
39
|
+
"Upgrade Node to \u2265 20.11 (older Node breaks the bundled proxy), or run `rulemetric hooks install --no-proxy`."
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
severity: "ok",
|
|
45
|
+
headline: "Healthy \u2014 the API is reachable" + (input.httpsProxySet ? " directly and through your proxy." : " (no proxy configured)."),
|
|
46
|
+
hints: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
parseNodeMajorMinor,
|
|
52
|
+
isProxyBreakingNode,
|
|
53
|
+
diagnose
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-J6QFW4LD.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/diagnostics.ts"],
|
|
4
|
+
"sourcesContent": ["// Pure diagnosis logic for `rulemetric diagnostics` \u2014 kept out of the command so\n// the decision-making (which is the valuable, bug-prone part) is unit-testable\n// without spawning curl or opening sockets. The command does the probing and\n// feeds the raw booleans here.\n\nexport interface DiagnoseInput {\n /** e.g. \"22.17.1\" or \"v22.17.1\" */\n nodeVersion: string;\n /** GET /health succeeded DIRECT (proxy bypassed). */\n directApiOk: boolean;\n /** HTTPS_PROXY is configured in the environment / settings. */\n httpsProxySet: boolean;\n /** The local gateway (:8787) is listening. */\n gatewayAlive: boolean;\n /** mitmproxy (:8788) is listening. */\n mitmproxyAlive: boolean;\n /** GET /health succeeded THROUGH the configured proxy. null when no proxy is set. */\n proxyPathOk: boolean | null;\n}\n\nexport interface Diagnosis {\n severity: 'ok' | 'warn' | 'fail';\n headline: string;\n hints: string[];\n}\n\n/** Parse major/minor from \"22.17.1\" or \"v22.17.1\". Unknown \u2192 {0,0}. */\nexport function parseNodeMajorMinor(v: string): { major: number; minor: number } {\n const m = /^v?(\\d+)\\.(\\d+)/.exec(v.trim());\n if (!m) return { major: 0, minor: 0 };\n return { major: Number(m[1]), minor: Number(m[2]) };\n}\n\n/**\n * Node < 20.11 breaks the bundled proxy (it relies on a module-path API that is\n * undefined on older Node; see the 2026-07-07 field finding). Below 20.11 the\n * proxy is likely dead even\n * though HTTPS_PROXY is set \u2014 a prime cause of a client-side \"502 Bad Gateway\".\n */\nexport function isProxyBreakingNode(v: string): boolean {\n const { major, minor } = parseNodeMajorMinor(v);\n if (major === 0) return false; // couldn't parse \u2014 don't cry wolf\n return major < 20 || (major === 20 && minor < 11);\n}\n\n/**\n * Turn the raw probe results into a single actionable diagnosis. The headline\n * case this exists for: DIRECT reachability works but the PROXY path fails \u2192\n * the user's own local gateway is eating requests and returning 502, even though\n * production is perfectly healthy.\n */\nexport function diagnose(input: DiagnoseInput): Diagnosis {\n const oldNode = isProxyBreakingNode(input.nodeVersion);\n\n // 1. Can't even reach the API directly \u2192 not a proxy problem; network/DNS/down.\n if (!input.directApiOk) {\n return {\n severity: 'fail',\n headline: \"Can't reach the API directly (proxy bypassed).\",\n hints: [\n 'Check your network/DNS/firewall and that you can reach https://rulemetric.com/health.',\n 'If a corporate proxy is required, it must allow rulemetric.com.',\n ],\n };\n }\n\n // 2. Direct works but the PROXY path fails \u2192 the local gateway is the culprit.\n // This is the client-side \"502 Bad Gateway\" class.\n if (input.httpsProxySet && input.proxyPathOk === false) {\n const hints = [\n 'Direct reachability works but requests through your local proxy fail \u2014 your gateway/mitmproxy is the problem, NOT the API.',\n input.gatewayAlive\n ? 'Gateway (:8787) is up but not routing successfully.'\n : 'Gateway (:8787) is NOT listening \u2014 Claude Code/CLI traffic pointed at it will 502. Run `rulemetric hooks install` to restore it.',\n input.mitmproxyAlive ? undefined : 'mitmproxy (:8788) is down \u2014 the gateway should route direct, so a 502 here points at the gateway itself.',\n oldNode\n ? `Node ${input.nodeVersion} is < 20.11, which breaks the bundled proxy \u2014 upgrade Node to \u2265 20.11, or reinstall hooks with --no-proxy.`\n : undefined,\n 'Quick unblock: `rulemetric hooks install --no-proxy` (captures via hooks, no proxy), or `rulemetric proxy restart`.',\n ].filter((h): h is string => Boolean(h));\n return { severity: 'fail', headline: 'Your local proxy/gateway can\\'t reach the API \u2014 this is the 502.', hints };\n }\n\n // 3. Old Node + proxy configured but we couldn't positively confirm the proxy\n // path (e.g. curl unavailable) \u2192 warn, since the proxy is likely dead.\n if (input.httpsProxySet && oldNode) {\n return {\n severity: 'warn',\n headline: `Node ${input.nodeVersion} is < 20.11 with a proxy configured \u2014 the proxy is likely dead.`,\n hints: [\n 'Upgrade Node to \u2265 20.11 (older Node breaks the bundled proxy), or run `rulemetric hooks install --no-proxy`.',\n ],\n };\n }\n\n // 4. Everything reachable.\n return {\n severity: 'ok',\n headline: 'Healthy \u2014 the API is reachable' + (input.httpsProxySet ? ' directly and through your proxy.' : ' (no proxy configured).'),\n hints: [],\n };\n}\n"],
|
|
5
|
+
"mappings": ";AA2BO,SAAS,oBAAoB,GAA6C;AAC/E,QAAM,IAAI,kBAAkB,KAAK,EAAE,KAAK,CAAC;AACzC,MAAI,CAAC,EAAG,QAAO,EAAE,OAAO,GAAG,OAAO,EAAE;AACpC,SAAO,EAAE,OAAO,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,EAAE,CAAC,CAAC,EAAE;AACpD;AAQO,SAAS,oBAAoB,GAAoB;AACtD,QAAM,EAAE,OAAO,MAAM,IAAI,oBAAoB,CAAC;AAC9C,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,QAAQ,MAAO,UAAU,MAAM,QAAQ;AAChD;AAQO,SAAS,SAAS,OAAiC;AACxD,QAAM,UAAU,oBAAoB,MAAM,WAAW;AAGrD,MAAI,CAAC,MAAM,aAAa;AACtB,WAAO;AAAA,MACL,UAAU;AAAA,MACV,UAAU;AAAA,MACV,OAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAIA,MAAI,MAAM,iBAAiB,MAAM,gBAAgB,OAAO;AACtD,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,MAAM,eACF,wDACA;AAAA,MACJ,MAAM,iBAAiB,SAAY;AAAA,MACnC,UACI,QAAQ,MAAM,WAAW,yHACzB;AAAA,MACJ;AAAA,IACF,EAAE,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC;AACvC,WAAO,EAAE,UAAU,QAAQ,UAAU,wEAAoE,MAAM;AAAA,EACjH;AAIA,MAAI,MAAM,iBAAiB,SAAS;AAClC,WAAO;AAAA,MACL,UAAU;AAAA,MACV,UAAU,QAAQ,MAAM,WAAW;AAAA,MACnC,OAAO;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,UAAU,yCAAoC,MAAM,gBAAgB,sCAAsC;AAAA,IAC1G,OAAO,CAAC;AAAA,EACV;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-DGHWRQXL.js";
|
|
7
7
|
import {
|
|
8
8
|
apiPatch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ZUJBM6HE.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-
|
|
61
|
+
//# sourceMappingURL=chunk-JULOLTZS.js.map
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
apiGet,
|
|
12
12
|
apiPost
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZUJBM6HE.js";
|
|
14
14
|
|
|
15
15
|
// src/lib/eval-conversation-handler.ts
|
|
16
16
|
async function processConversationJob(jobConfig, evalTargetId, logger) {
|
|
@@ -217,4 +217,4 @@ async function handleAnalyzeAndPropose(target, evalCases, messages, userFeedback
|
|
|
217
217
|
export {
|
|
218
218
|
processConversationJob
|
|
219
219
|
};
|
|
220
|
-
//# sourceMappingURL=chunk-
|
|
220
|
+
//# sourceMappingURL=chunk-KJKBDQ4D.js.map
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
removeStatuslineShimAnyHome
|
|
3
|
+
} from "./chunk-OQBBVTDG.js";
|
|
4
|
+
|
|
1
5
|
// src/lib/hooks-config.ts
|
|
2
6
|
import { execSync } from "node:child_process";
|
|
3
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, copyFileSync } from "node:fs";
|
|
@@ -150,6 +154,83 @@ function removeClaudeCodeHooks(settings) {
|
|
|
150
154
|
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
151
155
|
return removed > 0;
|
|
152
156
|
}
|
|
157
|
+
var RULEMETRIC_NO_PROXY = "localhost,127.0.0.1,*.supabase.co,*.supabase.in";
|
|
158
|
+
function claudeSettingsInstallPath(configDir, isGlobal) {
|
|
159
|
+
return join(configDir, isGlobal ? "settings.json" : "settings.local.json");
|
|
160
|
+
}
|
|
161
|
+
function removeRulemetricProxyEnv(settings, gatewayPort) {
|
|
162
|
+
const env = settings.env;
|
|
163
|
+
if (!env || typeof env !== "object") return false;
|
|
164
|
+
let removed = false;
|
|
165
|
+
const proxy = env.HTTPS_PROXY;
|
|
166
|
+
if (proxy === `http://localhost:${gatewayPort}` || proxy === `http://127.0.0.1:${gatewayPort}`) {
|
|
167
|
+
delete env.HTTPS_PROXY;
|
|
168
|
+
removed = true;
|
|
169
|
+
}
|
|
170
|
+
if (env.NO_PROXY === RULEMETRIC_NO_PROXY) {
|
|
171
|
+
delete env.NO_PROXY;
|
|
172
|
+
removed = true;
|
|
173
|
+
}
|
|
174
|
+
const ca = env.NODE_EXTRA_CA_CERTS;
|
|
175
|
+
if (typeof ca === "string" && ca.includes(".mitmproxy") && ca.endsWith("mitmproxy-ca-cert.pem")) {
|
|
176
|
+
delete env.NODE_EXTRA_CA_CERTS;
|
|
177
|
+
removed = true;
|
|
178
|
+
}
|
|
179
|
+
if (removed && Object.keys(env).length === 0) delete settings.env;
|
|
180
|
+
return removed;
|
|
181
|
+
}
|
|
182
|
+
function removeRulemetricMachineState(settings, gatewayPort) {
|
|
183
|
+
const removedHooks = removeClaudeCodeHooks(settings);
|
|
184
|
+
const removedProxyEnv = removeRulemetricProxyEnv(settings, gatewayPort);
|
|
185
|
+
const removedStatusline = removeStatuslineShimAnyHome(settings);
|
|
186
|
+
return {
|
|
187
|
+
removedHooks,
|
|
188
|
+
removedProxyEnv,
|
|
189
|
+
removedStatusline,
|
|
190
|
+
removed: removedHooks || removedProxyEnv || removedStatusline
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function migrateSharedClaudeSettings(sharedPath, gatewayPort) {
|
|
194
|
+
const none = {
|
|
195
|
+
migrated: false,
|
|
196
|
+
backupPath: null,
|
|
197
|
+
removedHooks: false,
|
|
198
|
+
removedProxyEnv: false,
|
|
199
|
+
removedStatusline: false,
|
|
200
|
+
removed: false
|
|
201
|
+
};
|
|
202
|
+
if (!existsSync(sharedPath)) return none;
|
|
203
|
+
let settings;
|
|
204
|
+
try {
|
|
205
|
+
settings = JSON.parse(readFileSync(sharedPath, "utf-8"));
|
|
206
|
+
} catch {
|
|
207
|
+
return none;
|
|
208
|
+
}
|
|
209
|
+
const removal = removeRulemetricMachineState(settings, gatewayPort);
|
|
210
|
+
if (!removal.removed) return none;
|
|
211
|
+
const { backupPath } = writeClaudeSettingsWithBackup(sharedPath, settings);
|
|
212
|
+
return { ...removal, migrated: true, backupPath };
|
|
213
|
+
}
|
|
214
|
+
function cleanClaudeSettingsFiles(claudeDir, gatewayPort) {
|
|
215
|
+
const results = [];
|
|
216
|
+
for (const fileName of ["settings.json", "settings.local.json"]) {
|
|
217
|
+
const configPath = join(claudeDir, fileName);
|
|
218
|
+
if (!existsSync(configPath)) continue;
|
|
219
|
+
const { settings, recoveredBackup } = readClaudeSettings(configPath);
|
|
220
|
+
const removal = removeRulemetricMachineState(settings, gatewayPort);
|
|
221
|
+
if (removal.removed) {
|
|
222
|
+
writeClaudeSettingsWithBackup(configPath, settings);
|
|
223
|
+
}
|
|
224
|
+
results.push({
|
|
225
|
+
...removal,
|
|
226
|
+
configPath,
|
|
227
|
+
fileName,
|
|
228
|
+
recoveredBackup,
|
|
229
|
+
changed: removal.removed
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
153
234
|
function writeCursorHooksToDir(cursorDir, binDir) {
|
|
154
235
|
if (!existsSync(cursorDir)) {
|
|
155
236
|
return null;
|
|
@@ -160,14 +241,11 @@ function writeCursorHooksToDir(cursorDir, binDir) {
|
|
|
160
241
|
if (parsed === null) return null;
|
|
161
242
|
const existing = parsed;
|
|
162
243
|
const merged = { ...existing.hooks ?? {} };
|
|
244
|
+
const isRulemetricHook = (h) => typeof h === "object" && h !== null && "command" in h && typeof h.command === "string" && h.command.includes("rulemetric");
|
|
163
245
|
for (const [event, hooks] of Object.entries(generated.hooks)) {
|
|
164
246
|
const current = merged[event] ?? [];
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
);
|
|
168
|
-
if (!hasRulemetric) {
|
|
169
|
-
merged[event] = [...current, ...hooks];
|
|
170
|
-
}
|
|
247
|
+
const siblings = current.filter((h) => !isRulemetricHook(h));
|
|
248
|
+
merged[event] = [...siblings, ...hooks];
|
|
171
249
|
}
|
|
172
250
|
const result = {
|
|
173
251
|
version: existing.version ?? generated.version,
|
|
@@ -514,6 +592,12 @@ export {
|
|
|
514
592
|
mergeClaudeCodeHooks,
|
|
515
593
|
hasRulemetricClaudeHooks,
|
|
516
594
|
removeClaudeCodeHooks,
|
|
595
|
+
RULEMETRIC_NO_PROXY,
|
|
596
|
+
claudeSettingsInstallPath,
|
|
597
|
+
removeRulemetricProxyEnv,
|
|
598
|
+
removeRulemetricMachineState,
|
|
599
|
+
migrateSharedClaudeSettings,
|
|
600
|
+
cleanClaudeSettingsFiles,
|
|
517
601
|
writeCursorHooks,
|
|
518
602
|
writeCursorUserHooks,
|
|
519
603
|
writeCopilotHooks,
|
|
@@ -525,8 +609,10 @@ export {
|
|
|
525
609
|
removeCopilotHooks,
|
|
526
610
|
installMacOSProxyEnv,
|
|
527
611
|
uninstallMacOSProxyEnv,
|
|
612
|
+
getVSCodeUserSettingsPath,
|
|
528
613
|
writeVSCodeUserProxyConfig,
|
|
529
614
|
removeVSCodeUserProxyConfig,
|
|
615
|
+
getCursorUserSettingsPath,
|
|
530
616
|
writeCursorUserProxyConfig,
|
|
531
617
|
removeCursorUserProxyConfig,
|
|
532
618
|
generateAntigravityHooksConfig,
|
|
@@ -535,4 +621,4 @@ export {
|
|
|
535
621
|
removeAntigravityHooks,
|
|
536
622
|
removeAntigravityUserHooks
|
|
537
623
|
};
|
|
538
|
-
//# sourceMappingURL=chunk-
|
|
624
|
+
//# sourceMappingURL=chunk-LXKPNATC.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/hooks-config.ts"],
|
|
4
|
+
"sourcesContent": ["import { execSync } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, copyFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport { removeStatuslineShimAnyHome } from './statusline-shim.js';\n\nexport type DetectedTool = 'claude_code' | 'cursor' | 'copilot_agent' | 'antigravity';\n\n/**\n * Detect which AI coding tools are installed in the project by checking\n * for their config directories.\n */\nexport function detectInstalledTools(projectPath: string): DetectedTool[] {\n const tools: DetectedTool[] = ['claude_code'];\n\n if (existsSync(join(projectPath, '.cursor'))) {\n tools.push('cursor');\n }\n\n if (existsSync(join(projectPath, '.github'))) {\n tools.push('copilot_agent');\n }\n\n // Antigravity's workspace config lives at `.agents/`. The user-level hooks\n // file at ~/.gemini/config/hooks.json is written by writeAntigravityUserHooks()\n // ONLY when ~/.gemini already exists (the tool is present) \u2014 see F2.\n if (existsSync(join(projectPath, '.agents'))) {\n tools.push('antigravity');\n }\n\n return tools;\n}\n\n/**\n * Generate Cursor hooks config object.\n *\n * Cursor uses event names like beforeSubmitPrompt, afterFileEdit, etc.\n * Each event maps to an array of hook objects with a `command` field.\n */\nexport function generateCursorHooksConfig(binDir?: string): object {\n // Event coverage matches Cursor 1.7+ hook API (cursor.com/docs/hooks).\n // `sessionStart` is required so the registry file is written with\n // conversation_id before any tool runs; without it the proxy-based linker\n // path would have nothing to map and we'd drop captures. `sessionEnd`\n // emits the session-end signal once per chat conversation (vs `stop`\n // which fires per-turn \u2014 those go through post-tool-use).\n //\n // binDir: prepended to PATH like the Claude Code hooks \u2014 Cursor is a GUI app\n // launched with launchd's minimal PATH (no nvm/pnpm-global bin), so a bare\n // `rulemetric` silently fails with command-not-found.\n const prefix = binDir ? `PATH=\"${binDir}:$PATH\" ` : '';\n const cmd = (name: string) => ({ command: `${prefix}rulemetric hooks run ${name}` });\n return {\n version: 1,\n hooks: {\n sessionStart: [cmd('session-start')],\n beforeSubmitPrompt: [cmd('user-prompt')],\n afterFileEdit: [cmd('post-tool-use')],\n beforeShellExecution: [cmd('post-tool-use')],\n afterShellExecution: [cmd('post-tool-use')],\n beforeMCPExecution: [cmd('post-tool-use')],\n afterMCPExecution: [cmd('post-tool-use')],\n // `afterAgentResponse` carries the full assistant response text +\n // token usage. Verified empirically via a probe hook on\n // 2026-05-12 \u2014 Cursor passes `text`, `input_tokens`,\n // `output_tokens`, `cache_read_tokens`, `cache_write_tokens`. This\n // is the cleanest path to per-turn assistant content for Cursor\n // (chat traffic itself bypasses the proxy via HTTP/2 \u2014 see\n // docs/audits/2026-05-11-cursor-capture-architecture.md).\n afterAgentResponse: [cmd('assistant-response')],\n // `stop` fires when the agent loop ends for a single turn (status:\n // completed|aborted|error). Per-turn, NOT per-session \u2014 route it to\n // post-tool-use, not session-end. The real session boundary is\n // `sessionEnd` below.\n stop: [cmd('post-tool-use')],\n sessionEnd: [cmd('session-end')],\n },\n };\n}\n\n/**\n * Generate Copilot Agent hooks config object.\n *\n * Copilot uses event names like sessionStart, userPromptSubmitted, etc.\n * Each event maps to an array of hook objects with `type` and `bash` fields.\n */\nexport function generateCopilotHooksConfig(binDir?: string): object {\n // binDir prepended to PATH \u2014 Copilot Agent runs in the same GUI/minimal-PATH\n // environment class as Cursor, so a bare `rulemetric` silently fails.\n const prefix = binDir ? `PATH=\"${binDir}:$PATH\" ` : '';\n const cmd = (name: string) => ({ type: 'command', bash: `${prefix}rulemetric hooks run ${name}` });\n return {\n version: 1,\n hooks: {\n sessionStart: [cmd('session-start')],\n userPromptSubmitted: [cmd('user-prompt')],\n postToolUse: [cmd('post-tool-use')],\n sessionEnd: [cmd('session-end')],\n },\n };\n}\n\ninterface ClaudeHookEntry {\n type: 'command';\n command: string;\n}\n\ninterface ClaudeHookBlock {\n matcher?: string;\n hooks: ClaudeHookEntry[];\n}\n\n/** The `settings.hooks` map RuleMetric installs into a project's `.claude/settings.json`. */\nexport type ClaudeCodeHooksConfig = Record<\n 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop' | 'SessionEnd',\n ClaudeHookBlock[]\n>;\n\n/**\n * Generate the Claude Code hooks map for `.claude/settings.json`.\n *\n * Claude Code keys hook blocks by native event name, each block being\n * `{ matcher?, hooks: [{ type: 'command', command }] }`. Unlike Antigravity\n * these carry NO `RULEMETRIC_HOOK_TOOL` prefix \u2014 `_normalize.sh` defaults to\n * the `claude_code` branch (session_id + transcript_path payload shape).\n *\n * This is the hooks-first, zero-proxy capture path: SessionEnd runs\n * `session-end.sh`, which reimports the whole transcript via\n * `POST /api/sessions/:id/reimport` \u2014 no mitmproxy / CA trust / gateway needed.\n * The rendered system prompt is still proxy-only, so instruction-effectiveness\n * linking is handled separately (Phase 2 synthetic snapshot).\n */\nexport function generateClaudeCodeHooksConfig(binDir?: string): ClaudeCodeHooksConfig {\n // The literal `rulemetric` is always present: it's the marker that install.ts\n // \u00A71 and mergeClaudeCodeHooks use to dedup/strip our hooks (a resolved\n // node+run.js path omits 'rulemetric' in a dev checkout, which would silently\n // break that dedup).\n //\n // `binDir`, when provided, is prepended to PATH so the command resolves even\n // in Claude Code's hook shell \u2014 which runs `/bin/sh -c` with a minimal PATH\n // that does NOT include nvm/pnpm global bin dirs. Without it, a fresh-install\n // user hits `rulemetric: command not found` on every hook, so capture (and\n // the session-start gateway auto-start that keeps the proxy alive) silently\n // fails. Callers resolve the real bin dir at install time and pass it here.\n const prefix = binDir ? `PATH=\"${binDir}:$PATH\" ` : '';\n const mk = (name: string): ClaudeHookEntry => ({\n type: 'command',\n command: `${prefix}rulemetric hooks run ${name}`,\n });\n return {\n SessionStart: [{ hooks: [mk('session-start')] }],\n UserPromptSubmit: [{ hooks: [mk('user-prompt')] }],\n PostToolUse: [{ matcher: '.*', hooks: [mk('post-tool-use')] }],\n Stop: [{ hooks: [mk('assistant-response')] }],\n SessionEnd: [{ hooks: [mk('session-end')] }],\n };\n}\n\n/**\n * Read a Claude Code `settings.json` for merging, WITHOUT ever crashing the\n * install on a fresh machine. A missing file yields `{}`. A malformed file\n * (hand-edited JSONC, a stray trailing comma, a half-written file) is NOT\n * allowed to throw \u2014 that used to hard-fail `hooks install` with a raw\n * SyntaxError and leave the user with NO capture. Instead we back the\n * unparseable file up (so nothing is silently lost) and proceed on a fresh\n * object so capture still installs.\n *\n * Returns the parsed settings plus `recoveredBackup`: the path the corrupt\n * original was copied to, or `null` when the file was absent/valid.\n */\nexport function readClaudeSettings(configPath: string): {\n settings: Record<string, unknown>;\n recoveredBackup: string | null;\n} {\n if (!existsSync(configPath)) return { settings: {}, recoveredBackup: null };\n const raw = readFileSync(configPath, 'utf-8');\n try {\n return { settings: JSON.parse(raw) as Record<string, unknown>, recoveredBackup: null };\n } catch {\n const recoveredBackup = `${configPath}.corrupt.bak`;\n writeFileSync(recoveredBackup, raw);\n return { settings: {}, recoveredBackup };\n }\n}\n\n/**\n * Write a Claude Code `settings.json`, backing up any prior file first so the\n * change is reversible: the `.bak` holds the previous contents for the user to\n * restore manually if a merge ever goes wrong. Creates the parent dir as needed.\n * Returns `backupPath` (the `.bak` written) or `null` when there was no\n * pre-existing file to back up.\n */\nexport function writeClaudeSettingsWithBackup(\n configPath: string,\n settings: Record<string, unknown>,\n): { backupPath: string | null } {\n mkdirSync(dirname(configPath), { recursive: true });\n let backupPath: string | null = null;\n if (existsSync(configPath)) {\n backupPath = `${configPath}.bak`;\n copyFileSync(configPath, backupPath);\n }\n writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\\n');\n return { backupPath };\n}\n\n/**\n * Read a JSON config file for a safe in-place merge. Returns `{}` when the file\n * is absent, the parsed object when it's valid, and `null` when it EXISTS but is\n * unparseable \u2014 in which case the original is first backed up to `.corrupt.bak`.\n * Callers MUST treat `null` as \"skip, do not write\": editor settings.json files\n * are JSONC (comments + trailing commas are legal and common), so a strict\n * JSON.parse throws on valid user files; blindly resetting to `{}` and writing\n * would clobber the user's ENTIRE editor config down to our few keys. Backing up\n * + skipping is the non-destructive floor (mirrors readClaudeSettings).\n */\nfunction readMergeableJson(configPath: string): Record<string, unknown> | null {\n if (!existsSync(configPath)) return {};\n const raw = readFileSync(configPath, 'utf-8');\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n try {\n writeFileSync(`${configPath}.corrupt.bak`, raw);\n } catch {\n /* best-effort backup */\n }\n return null;\n }\n}\n\n/**\n * Merge RuleMetric's Claude Code hooks into an in-memory `.claude/settings.json`\n * object. Claude Code hooks share the same settings.json that install.ts reads,\n * strips (\u00A71), and writes (\u00A75), so this mutates `settings.hooks` in place rather\n * than doing its own file I/O. Idempotent + non-destructive: per event it skips\n * adding when a rulemetric command is already present and otherwise appends,\n * preserving the user's own hooks.\n */\nexport function mergeClaudeCodeHooks(settings: Record<string, unknown>, binDir?: string): void {\n const generated = generateClaudeCodeHooksConfig(binDir);\n const hooks = (settings.hooks ?? {}) as Record<string, ClaudeHookBlock[]>;\n for (const [event, blocks] of Object.entries(generated)) {\n const current = (hooks[event] ?? []) as ClaudeHookBlock[];\n const alreadyPresent = current.some((b) =>\n b?.hooks?.some((h) => typeof h?.command === 'string' && h.command.includes('rulemetric')),\n );\n if (!alreadyPresent) {\n hooks[event] = [...current, ...blocks];\n }\n }\n settings.hooks = hooks;\n}\n\n/**\n * True iff RuleMetric's Claude Code session hooks are present in a settings\n * object \u2014 i.e. capture is actually wired up. This is the honest readiness\n * signal for the onboarding hooks step: capture works when the hooks exist,\n * NOT merely because a proxy env var was written (which greened even when the\n * proxy pointed at a dead port \u2014 the fresh-laptop ConnectionRefused footgun).\n */\nexport function hasRulemetricClaudeHooks(settings: Record<string, unknown>): boolean {\n if (!settings.hooks || typeof settings.hooks !== 'object') return false;\n const hooks = settings.hooks as Record<string, ClaudeHookBlock[]>;\n return Object.values(hooks).some(\n (blocks) =>\n Array.isArray(blocks) &&\n blocks.some((b) =>\n b?.hooks?.some((h) => typeof h?.command === 'string' && h.command.includes('rulemetric')),\n ),\n );\n}\n\n/**\n * Strip RuleMetric's Claude Code hooks from an in-memory settings object while\n * preserving the user's own hook entries. Mirrors install.ts \u00A71 but is reusable\n * for uninstall. Removes emptied event arrays and deletes `settings.hooks` if it\n * ends up empty. Returns true iff anything was removed.\n */\nexport function removeClaudeCodeHooks(settings: Record<string, unknown>): boolean {\n if (!settings.hooks || typeof settings.hooks !== 'object') return false;\n const hooks = settings.hooks as Record<string, ClaudeHookBlock[]>;\n let removed = 0;\n for (const event of Object.keys(hooks)) {\n const entries = hooks[event];\n if (!Array.isArray(entries)) continue;\n const kept = entries.filter((block) => {\n const isRulemetric = block?.hooks?.some(\n (h) => typeof h?.command === 'string' && h.command.includes('rulemetric'),\n );\n if (isRulemetric) removed += 1;\n return !isRulemetric;\n });\n if (kept.length === 0) delete hooks[event];\n else hooks[event] = kept;\n }\n if (Object.keys(hooks).length === 0) delete settings.hooks;\n return removed > 0;\n}\n\n// ---------------------------------------------------------------------------\n// Machine-local Claude Code settings (settings.local.json) + shared-file healing\n// ---------------------------------------------------------------------------\n\n/** The NO_PROXY value `hooks install` writes alongside HTTPS_PROXY. */\nexport const RULEMETRIC_NO_PROXY = 'localhost,127.0.0.1,*.supabase.co,*.supabase.in';\n\n/**\n * The settings file `hooks install` owns for RuleMetric's machine-local state.\n * Project installs write `settings.local.json`: the shared `settings.json` is\n * typically checked into git, and everything we write is machine-local by\n * nature \u2014 hook commands carry a per-user PATH prefix, and the proxy env pins\n * HTTPS_PROXY + a per-user CA path that BREAK Claude Code's HTTPS on any clone\n * without a live gateway on that port. Claude Code merges settings.local.json\n * on top of settings.json, runs hooks from both, and auto-gitignores the local\n * file. Global installs keep writing `~/.claude/settings.json` \u2014 the home file\n * is not in a repo, so machine-local state is safe there.\n */\nexport function claudeSettingsInstallPath(configDir: string, isGlobal: boolean): string {\n return join(configDir, isGlobal ? 'settings.json' : 'settings.local.json');\n}\n\n/**\n * Remove the gateway proxy env keys `hooks install` writes, and ONLY those:\n * each key is deleted solely when its value is one RuleMetric itself produces,\n * so a user-owned corporate proxy / custom CA is never touched.\n * - HTTPS_PROXY: points at the local gateway port (localhost or 127.0.0.1)\n * - NO_PROXY: exactly RULEMETRIC_NO_PROXY\n * - NODE_EXTRA_CA_CERTS: the mitmproxy default CA path \u2014 suffix-matched, not\n * exact, because a shared file poisoned by a teammate's install carries\n * THEIR $HOME prefix\n * Drops the `env` block entirely when the removal empties it. Returns true iff\n * anything was removed.\n */\nexport function removeRulemetricProxyEnv(\n settings: Record<string, unknown>,\n gatewayPort: number,\n): boolean {\n const env = settings.env as Record<string, unknown> | undefined;\n if (!env || typeof env !== 'object') return false;\n let removed = false;\n const proxy = env.HTTPS_PROXY;\n if (proxy === `http://localhost:${gatewayPort}` || proxy === `http://127.0.0.1:${gatewayPort}`) {\n delete env.HTTPS_PROXY;\n removed = true;\n }\n if (env.NO_PROXY === RULEMETRIC_NO_PROXY) {\n delete env.NO_PROXY;\n removed = true;\n }\n const ca = env.NODE_EXTRA_CA_CERTS;\n if (typeof ca === 'string' && ca.includes('.mitmproxy') && ca.endsWith('mitmproxy-ca-cert.pem')) {\n delete env.NODE_EXTRA_CA_CERTS;\n removed = true;\n }\n if (removed && Object.keys(env).length === 0) delete settings.env;\n return removed;\n}\n\nexport interface RulemetricStateRemoval {\n removedHooks: boolean;\n removedProxyEnv: boolean;\n removedStatusline: boolean;\n /** True iff any of the three categories removed something. */\n removed: boolean;\n}\n\n/**\n * Strip ALL RuleMetric-owned machine-local state from an in-memory settings\n * object: session hooks, gateway proxy env keys, and the statusline shim. Each\n * remover has its own user-preserving predicate, so anything the user owns\n * survives untouched. Shared by uninstall and the shared-file migration.\n */\nexport function removeRulemetricMachineState(\n settings: Record<string, unknown>,\n gatewayPort: number,\n): RulemetricStateRemoval {\n const removedHooks = removeClaudeCodeHooks(settings);\n const removedProxyEnv = removeRulemetricProxyEnv(settings, gatewayPort);\n const removedStatusline = removeStatuslineShimAnyHome(settings);\n return {\n removedHooks,\n removedProxyEnv,\n removedStatusline,\n removed: removedHooks || removedProxyEnv || removedStatusline,\n };\n}\n\nexport interface SharedSettingsMigration extends RulemetricStateRemoval {\n /** True iff something was removed and the file was rewritten. */\n migrated: boolean;\n backupPath: string | null;\n}\n\n/**\n * Heal a shared `.claude/settings.json` that an older `hooks install` (this\n * machine's or a teammate's) filled with machine-local state: PATH-prefixed,\n * username-leaking hook commands, the gateway proxy env keys (an HTTPS_PROXY\n * pin breaks Claude Code's HTTPS entirely on any clone without a live gateway\n * on that port), and the $HOME-anchored statusline shim. User-owned hooks/env/\n * statusLine are never touched. The file is rewritten (with the standard\n * backup) ONLY when something was actually removed; an unparseable file is\n * left exactly as-is \u2014 we cannot safely edit what we cannot parse.\n */\nexport function migrateSharedClaudeSettings(\n sharedPath: string,\n gatewayPort: number,\n): SharedSettingsMigration {\n const none: SharedSettingsMigration = {\n migrated: false,\n backupPath: null,\n removedHooks: false,\n removedProxyEnv: false,\n removedStatusline: false,\n removed: false,\n };\n if (!existsSync(sharedPath)) return none;\n let settings: Record<string, unknown>;\n try {\n settings = JSON.parse(readFileSync(sharedPath, 'utf-8')) as Record<string, unknown>;\n } catch {\n return none;\n }\n const removal = removeRulemetricMachineState(settings, gatewayPort);\n if (!removal.removed) return none;\n const { backupPath } = writeClaudeSettingsWithBackup(sharedPath, settings);\n return { ...removal, migrated: true, backupPath };\n}\n\nexport interface ClaudeSettingsCleanup extends RulemetricStateRemoval {\n configPath: string;\n /** `settings.json` or `settings.local.json` \u2014 for log lines. */\n fileName: string;\n recoveredBackup: string | null;\n /** True iff the file was rewritten. */\n changed: boolean;\n}\n\n/**\n * Remove all RuleMetric-owned state from BOTH Claude Code settings files in a\n * project: the shared `settings.json` (legacy installs wrote there) and\n * `settings.local.json` (where current installs put machine-local state). Each\n * file is rewritten (with backup) only when something was actually removed.\n * Returns one entry per file that exists, with per-category flags so the\n * caller can log precisely what was cleaned where.\n */\nexport function cleanClaudeSettingsFiles(\n claudeDir: string,\n gatewayPort: number,\n): ClaudeSettingsCleanup[] {\n const results: ClaudeSettingsCleanup[] = [];\n for (const fileName of ['settings.json', 'settings.local.json'] as const) {\n const configPath = join(claudeDir, fileName);\n if (!existsSync(configPath)) continue;\n const { settings, recoveredBackup } = readClaudeSettings(configPath);\n const removal = removeRulemetricMachineState(settings, gatewayPort);\n if (removal.removed) {\n writeClaudeSettingsWithBackup(configPath, settings);\n }\n results.push({\n ...removal,\n configPath,\n fileName,\n recoveredBackup,\n changed: removal.removed,\n });\n }\n return results;\n}\n\n/**\n * Merge rulemetric hooks into a Cursor `hooks.json` at the given directory.\n *\n * Used by both the project (`<repo>/.cursor/`) and user (`~/.cursor/`) writers.\n * Returns the config file path, or null if the cursor dir doesn't exist.\n */\nfunction writeCursorHooksToDir(cursorDir: string, binDir?: string): string | null {\n if (!existsSync(cursorDir)) {\n return null;\n }\n\n const configPath = join(cursorDir, 'hooks.json');\n const generated = generateCursorHooksConfig(binDir) as {\n version: number;\n hooks: Record<string, Array<{ command: string }>>;\n };\n\n const parsed = readMergeableJson(configPath);\n if (parsed === null) return null; // unparseable \u2014 backed up, don't drop sibling hooks\n const existing = parsed as { version?: number; hooks?: Record<string, unknown[]> };\n\n const merged: Record<string, unknown[]> = { ...(existing.hooks ?? {}) };\n\n const isRulemetricHook = (h: unknown): boolean =>\n typeof h === 'object' &&\n h !== null &&\n 'command' in h &&\n typeof (h as { command: string }).command === 'string' &&\n (h as { command: string }).command.includes('rulemetric');\n\n for (const [event, hooks] of Object.entries(generated.hooks)) {\n const current = merged[event] ?? [];\n // REPLACE prior rulemetric entries, never skip-if-present: a stale command\n // (old format, moved binary, outdated PATH prefix) must be repairable by\n // re-running `hooks install` \u2014 skip-if-present froze broken Cursor capture\n // forever. Sibling (non-rulemetric) hooks are preserved untouched.\n const siblings = current.filter((h: unknown) => !isRulemetricHook(h));\n merged[event] = [...siblings, ...hooks];\n }\n\n const result = {\n version: existing.version ?? generated.version,\n hooks: merged,\n };\n\n writeFileSync(configPath, JSON.stringify(result, null, 2) + '\\n');\n return configPath;\n}\n\n/**\n * Write Cursor hooks config to `<projectPath>/.cursor/hooks.json`.\n * Merges with existing config \u2014 appends rulemetric hooks without duplicating.\n * Returns the config file path, or null if `.cursor/` doesn't exist.\n */\nexport function writeCursorHooks(projectPath: string, binDir?: string): string | null {\n return writeCursorHooksToDir(join(projectPath, '.cursor'), binDir);\n}\n\n/**\n * Write Cursor hooks config to user-level `~/.cursor/hooks.json` so capture\n * works for every Cursor conversation without per-project setup.\n *\n * Returns the config file path, or null if `~/.cursor/` doesn't exist\n * (i.e., Cursor has never been launched on this machine).\n */\nexport function writeCursorUserHooks(binDir?: string, home: string = homedir()): string | null {\n // Only wire user-level Cursor hooks if Cursor is actually present \u2014 never\n // manufacture ~/.cursor for a user who doesn't use it (undisclosed scope,\n // onboarding field report 2026-07-07 F2). `home` is injectable for tests.\n const cursorHome = join(home, '.cursor');\n if (!existsSync(cursorHome)) return null;\n return writeCursorHooksToDir(cursorHome, binDir);\n}\n\n/**\n * Write Copilot Agent hooks config to `.github/hooks/rulemetric.json`.\n * Creates the hooks directory if needed.\n * Returns the config file path, or null if `.github/` doesn't exist.\n */\nexport function writeCopilotHooks(projectPath: string, binDir?: string): string | null {\n const githubDir = join(projectPath, '.github');\n if (!existsSync(githubDir)) {\n return null;\n }\n\n const hooksDir = join(githubDir, 'hooks');\n mkdirSync(hooksDir, { recursive: true });\n\n const configPath = join(hooksDir, 'rulemetric.json');\n const config = generateCopilotHooksConfig(binDir);\n\n writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n');\n return configPath;\n}\n\n/**\n * Strip rulemetric hooks from a Cursor `hooks.json` at the given path.\n * Preserves other hooks and removes empty event arrays.\n * Returns true if changes were made.\n */\nfunction removeCursorHooksFromFile(configPath: string): boolean {\n if (!existsSync(configPath)) {\n return false;\n }\n\n let config: { version?: number; hooks?: Record<string, unknown[]> };\n try {\n config = JSON.parse(readFileSync(configPath, 'utf-8'));\n } catch {\n return false;\n }\n\n if (!config.hooks) {\n return false;\n }\n\n let changed = false;\n for (const [event, hooks] of Object.entries(config.hooks)) {\n const filtered = hooks.filter((h: unknown) => {\n if (typeof h !== 'object' || h === null || !('command' in h)) return true;\n const cmd = (h as { command: string }).command;\n if (typeof cmd === 'string' && cmd.includes('rulemetric')) {\n changed = true;\n return false;\n }\n return true;\n });\n\n if (filtered.length === 0) {\n delete config.hooks[event];\n } else {\n config.hooks[event] = filtered;\n }\n }\n\n if (changed) {\n writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n');\n }\n\n return changed;\n}\n\n/**\n * Remove rulemetric hooks from `<projectPath>/.cursor/hooks.json`.\n */\nexport function removeCursorHooks(projectPath: string): boolean {\n return removeCursorHooksFromFile(join(projectPath, '.cursor', 'hooks.json'));\n}\n\n/**\n * Remove rulemetric hooks from user-level `~/.cursor/hooks.json`.\n */\nexport function removeCursorUserHooks(): boolean {\n return removeCursorHooksFromFile(join(homedir(), '.cursor', 'hooks.json'));\n}\n\n/**\n * Write Cursor proxy config to `.cursor/settings.json`.\n * Sets `http.proxy` and `http.proxyStrictSSL` while preserving existing settings.\n * Returns true if `.cursor/` exists and config was written, false otherwise.\n */\nexport function writeCursorProxyConfig(projectPath: string, gatewayPort: number, certPath: string): boolean {\n const cursorDir = join(projectPath, '.cursor');\n if (!existsSync(cursorDir)) return false;\n\n const settingsPath = join(cursorDir, 'settings.json');\n const settings = readMergeableJson(settingsPath);\n if (settings === null) return false; // unparseable JSONC \u2014 backed up, don't clobber\n\n settings['http.proxy'] = `http://localhost:${gatewayPort}`;\n settings['http.proxyStrictSSL'] = existsSync(certPath);\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n/**\n * Write VS Code proxy config to `.vscode/settings.json` for Copilot capture.\n * Sets `http.proxy`, `http.proxyStrictSSL`, and Copilot-specific override\n * while preserving existing settings.\n * Returns true if config was written, false if `.vscode/` doesn't exist.\n */\nexport function writeVSCodeProxyConfig(projectPath: string, gatewayPort: number, certPath: string): boolean {\n const vscodeDir = join(projectPath, '.vscode');\n // Only touch projects that already use VS Code \u2014 do NOT manufacture a\n // `.vscode/` in every repo (matches writeCursorProxyConfig's contract and this\n // function's own docstring). Creating it pollutes Claude-Code-only repos with\n // an untracked, proxy-pinning settings.json.\n if (!existsSync(vscodeDir)) return false;\n\n const settingsPath = join(vscodeDir, 'settings.json');\n const settings = readMergeableJson(settingsPath);\n if (settings === null) return false; // unparseable JSONC \u2014 backed up, don't clobber\n\n settings['http.proxy'] = `http://localhost:${gatewayPort}`;\n settings['http.proxyStrictSSL'] = existsSync(certPath);\n\n // Copilot-specific override for surgical proxy targeting\n const copilotAdvanced = (settings['github.copilot.advanced'] ?? {}) as Record<string, unknown>;\n copilotAdvanced['debug.overrideProxyUrl'] = `http://localhost:${gatewayPort}`;\n settings['github.copilot.advanced'] = copilotAdvanced;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n/**\n * Remove VS Code proxy config from `.vscode/settings.json`.\n * Removes `http.proxy`, `http.proxyStrictSSL`, and Copilot proxy override.\n * Returns true if changes were made.\n */\nexport function removeVSCodeProxyConfig(projectPath: string): boolean {\n const settingsPath = join(projectPath, '.vscode', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n\n let settings: Record<string, unknown>;\n try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return false; }\n\n let changed = false;\n\n if (settings['http.proxy']) {\n delete settings['http.proxy'];\n delete settings['http.proxyStrictSSL'];\n changed = true;\n }\n\n const copilotAdvanced = settings['github.copilot.advanced'] as Record<string, unknown> | undefined;\n if (copilotAdvanced?.['debug.overrideProxyUrl']) {\n delete copilotAdvanced['debug.overrideProxyUrl'];\n if (Object.keys(copilotAdvanced).length === 0) {\n delete settings['github.copilot.advanced'];\n }\n changed = true;\n }\n\n if (changed) {\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n }\n\n return changed;\n}\n\n/**\n * Remove Copilot Agent hooks by deleting `.github/hooks/rulemetric.json`.\n * Returns true if the file was removed.\n */\nexport function removeCopilotHooks(projectPath: string): boolean {\n const configPath = join(projectPath, '.github', 'hooks', 'rulemetric.json');\n if (!existsSync(configPath)) {\n return false;\n }\n\n unlinkSync(configPath);\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// macOS LaunchAgent for GUI app env vars (HTTPS_PROXY, NODE_EXTRA_CA_CERTS)\n// ---------------------------------------------------------------------------\n\nconst LAUNCH_AGENT_LABEL = 'com.rulemetric.env';\n\nfunction getLaunchAgentPath(): string {\n return join(homedir(), 'Library', 'LaunchAgents', `${LAUNCH_AGENT_LABEL}.plist`);\n}\n\n/**\n * Set HTTPS_PROXY and NODE_EXTRA_CA_CERTS for all GUI apps on macOS.\n *\n * Two layers:\n * 1. `launchctl setenv` \u2014 immediate effect for newly launched apps\n * 2. LaunchAgent plist \u2014 persists across reboots\n *\n * This ensures VS Code / Copilot / Cursor route LLM traffic through the\n * gateway even when launched from Dock or Spotlight (not a terminal).\n *\n * References:\n * - anthropics/claude-code#30318 (same pattern for Claude Desktop)\n * - Snyk IDE docs recommend launchctl setenv for macOS\n * - Apple QA1067 (archived) \u2014 original env var mechanism removed in 10.10\n */\nexport function installMacOSProxyEnv(gatewayPort: number, certPath: string): boolean {\n if (process.platform !== 'darwin') return false;\n\n const proxyUrl = `http://localhost:${gatewayPort}`;\n\n // Layer 1: immediate effect via launchctl setenv\n try {\n execSync(`launchctl setenv HTTPS_PROXY ${proxyUrl}`, { stdio: 'pipe' });\n if (existsSync(certPath)) {\n execSync(`launchctl setenv NODE_EXTRA_CA_CERTS ${certPath}`, { stdio: 'pipe' });\n }\n } catch {\n return false;\n }\n\n // Layer 2: LaunchAgent plist for persistence across reboots\n const plistPath = getLaunchAgentPath();\n const agentsDir = join(homedir(), 'Library', 'LaunchAgents');\n mkdirSync(agentsDir, { recursive: true });\n\n const certLine = existsSync(certPath)\n ? `launchctl setenv NODE_EXTRA_CA_CERTS ${certPath};`\n : '';\n\n // LIVENESS-GUARDED boot command: at every login this re-pins HTTPS_PROXY\n // machine-wide, but the gateway on :8787 is not a supervised service on the\n // end-user path \u2014 it dies on logout and nothing restarts it. Pinning a dead\n // port breaks HTTPS for every GUI app. So probe :8787 first and only setenv\n // when the gateway actually answers; otherwise clear it (the session-start\n // hook will re-pin once Claude Code brings the gateway back up).\n const bootCmd =\n `if nc -z -w 1 127.0.0.1 ${gatewayPort} 2>/dev/null; then ` +\n `launchctl setenv HTTPS_PROXY ${proxyUrl};${certLine} ` +\n `else launchctl unsetenv HTTPS_PROXY; launchctl unsetenv NODE_EXTRA_CA_CERTS; fi`;\n\n const plist = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>${LAUNCH_AGENT_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>/bin/sh</string>\n <string>-c</string>\n <string>${bootCmd}</string>\n </array>\n <key>RunAtLoad</key>\n <true/>\n</dict>\n</plist>\n`;\n\n writeFileSync(plistPath, plist);\n return true;\n}\n\n/**\n * Remove the RuleMetric LaunchAgent and unset env vars.\n */\nexport function uninstallMacOSProxyEnv(): boolean {\n if (process.platform !== 'darwin') return false;\n\n let changed = false;\n\n // Unset env vars for current session\n try {\n execSync('launchctl unsetenv HTTPS_PROXY', { stdio: 'pipe' });\n execSync('launchctl unsetenv NODE_EXTRA_CA_CERTS', { stdio: 'pipe' });\n } catch { /* may not be set */ }\n\n // Remove LaunchAgent plist\n const plistPath = getLaunchAgentPath();\n if (existsSync(plistPath)) {\n unlinkSync(plistPath);\n changed = true;\n }\n\n return changed;\n}\n\n// ---------------------------------------------------------------------------\n// VS Code user-level proxy settings\n// ---------------------------------------------------------------------------\n\nexport function getVSCodeUserSettingsPath(): string {\n if (process.platform === 'darwin') {\n return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'settings.json');\n }\n if (process.platform === 'win32') {\n return join(process.env.APPDATA ?? '', 'Code', 'User', 'settings.json');\n }\n return join(homedir(), '.config', 'Code', 'User', 'settings.json');\n}\n\n/**\n * Set http.proxy in VS Code user settings (not workspace).\n *\n * Workspace-level http.proxy is silently ignored because VS Code restricts\n * it to APPLICATION scope (microsoft/vscode#236932). User-level settings\n * are the only reliable way to configure http.proxy.\n */\nexport function writeVSCodeUserProxyConfig(gatewayPort: number): boolean {\n const settingsPath = getVSCodeUserSettingsPath();\n if (!existsSync(join(settingsPath, '..'))) return false;\n\n const settings = readMergeableJson(settingsPath);\n if (settings === null) return false; // unparseable JSONC \u2014 backed up, don't clobber user config\n\n // Only set if not already configured by the user\n if (settings['http.proxy'] && !(settings['http.proxy'] as string).includes('localhost')) {\n return false; // user has a custom proxy, don't overwrite\n }\n\n settings['http.proxy'] = `http://localhost:${gatewayPort}`;\n settings['http.proxyStrictSSL'] = false;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n/**\n * Remove RuleMetric proxy config from VS Code user settings.\n */\nexport function removeVSCodeUserProxyConfig(): boolean {\n const settingsPath = getVSCodeUserSettingsPath();\n if (!existsSync(settingsPath)) return false;\n\n let settings: Record<string, unknown>;\n try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return false; }\n\n const proxy = settings['http.proxy'] as string | undefined;\n if (!proxy?.includes('localhost')) return false;\n\n delete settings['http.proxy'];\n delete settings['http.proxyStrictSSL'];\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Cursor user-level proxy settings\n// ---------------------------------------------------------------------------\n//\n// Cursor is a VS Code fork and uses the same settings.json shape, but reads\n// from `Cursor/User/settings.json` instead of `Code/User/settings.json`. The\n// `.cursor/settings.json` (workspace) path can't reach Cursor's Electron main\n// process for the same reason VS Code's workspace path can't reach VS Code's\n// \u2014 the http.proxy setting is restricted to APPLICATION scope and only the\n// user-level file is honored.\n\nexport function getCursorUserSettingsPath(): string {\n if (process.platform === 'darwin') {\n return join(homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'settings.json');\n }\n if (process.platform === 'win32') {\n return join(process.env.APPDATA ?? '', 'Cursor', 'User', 'settings.json');\n }\n return join(homedir(), '.config', 'Cursor', 'User', 'settings.json');\n}\n\n/**\n * Set http.proxy in Cursor user settings (not workspace). Mirrors\n * `writeVSCodeUserProxyConfig` \u2014 workspace-level http.proxy is silently\n * ignored due to APPLICATION-scope restriction (Cursor inherits this from\n * VS Code, see microsoft/vscode#236932).\n *\n * Returns true if config was written, false if Cursor isn't installed\n * (User dir absent) or the user already has a non-localhost proxy set.\n */\nexport function writeCursorUserProxyConfig(gatewayPort: number): boolean {\n const settingsPath = getCursorUserSettingsPath();\n if (!existsSync(join(settingsPath, '..'))) return false;\n\n const settings = readMergeableJson(settingsPath);\n if (settings === null) return false; // unparseable JSONC \u2014 backed up, don't clobber user config\n\n if (settings['http.proxy'] && !(settings['http.proxy'] as string).includes('localhost')) {\n return false;\n }\n\n settings['http.proxy'] = `http://localhost:${gatewayPort}`;\n settings['http.proxyStrictSSL'] = false;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n/**\n * Remove RuleMetric proxy config from Cursor user settings.\n */\nexport function removeCursorUserProxyConfig(): boolean {\n const settingsPath = getCursorUserSettingsPath();\n if (!existsSync(settingsPath)) return false;\n\n let settings: Record<string, unknown>;\n try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return false; }\n\n const proxy = settings['http.proxy'] as string | undefined;\n if (!proxy?.includes('localhost')) return false;\n\n delete settings['http.proxy'];\n delete settings['http.proxyStrictSSL'];\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\n return true;\n}\n\n// \u2500\u2500\u2500 Antigravity (Google's Gemini-based agentic IDE) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Antigravity hooks live in `.agents/hooks.json` (workspace) and\n// `~/.gemini/config/hooks.json` (user-level). Events follow the\n// SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop /\n// SessionEnd vocabulary. Each hook command sets `RULEMETRIC_HOOK_TOOL=\n// antigravity` so `_normalize.sh` routes through the Antigravity branch.\n\ninterface AntigravityHookEntry {\n type: 'command';\n command: string;\n timeout?: number;\n}\n\ninterface AntigravityHookBlock {\n matcher?: string;\n hooks: AntigravityHookEntry[];\n}\n\ninterface AntigravityRuleMetricBlock {\n enabled: boolean;\n SessionStart: AntigravityHookBlock[];\n UserPromptSubmit: AntigravityHookBlock[];\n PreToolUse: AntigravityHookBlock[];\n PostToolUse: AntigravityHookBlock[];\n Stop: AntigravityHookBlock[];\n SessionEnd: AntigravityHookBlock[];\n}\n\nexport type AntigravityHooksConfig = { rulemetric: AntigravityRuleMetricBlock };\n\n/** Resolve the binary string to invoke for a hook command. */\nfunction getRulemetricCommand(): string {\n // Prefer the local Node binary + bin/run.js so the hook works even when\n // `rulemetric` isn't on PATH for the GUI Antigravity process.\n const nodeBin = process.execPath;\n // bin/run.js lives next to dist/ in the installed CLI; resolve from\n // import.meta isn't available in a portable way here, so we use the\n // process argv1 if it's pointing at the CLI entry, else fall back to\n // the bare command name. In practice this function is called inside\n // `rulemetric hooks install`, where process.argv[1] is bin/run.js.\n const cliEntry = process.argv[1];\n if (cliEntry && cliEntry.endsWith('run.js')) {\n return `${nodeBin} ${cliEntry}`;\n }\n return 'rulemetric';\n}\n\nexport function generateAntigravityHooksConfig(): AntigravityHooksConfig {\n const cmd = getRulemetricCommand();\n const mk = (name: string): AntigravityHookEntry => ({\n type: 'command',\n command: `RULEMETRIC_HOOK_TOOL=antigravity ${cmd} hooks run ${name}`,\n timeout: 30,\n });\n return {\n rulemetric: {\n enabled: true,\n SessionStart: [{ hooks: [mk('session-start')] }],\n UserPromptSubmit: [{ hooks: [mk('user-prompt')] }],\n PreToolUse: [{ matcher: '.*', hooks: [mk('post-tool-use')] }],\n PostToolUse: [{ matcher: '.*', hooks: [mk('post-tool-use')] }],\n Stop: [{ hooks: [mk('assistant-response')] }],\n SessionEnd: [{ hooks: [mk('session-end')] }],\n },\n };\n}\n\nfunction writeAntigravityHooksToDir(dir: string): string | null {\n const configPath = join(dir, 'hooks.json');\n const existing = readMergeableJson(configPath);\n if (existing === null) return null; // unparseable \u2014 backed up, don't drop sibling blocks\n const generated = generateAntigravityHooksConfig();\n // Antigravity keys hook blocks by name; RuleMetric owns its own block\n // and never touches sibling blocks the user may have added.\n const merged = { ...existing, rulemetric: generated.rulemetric };\n mkdirSync(dir, { recursive: true });\n writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\\n');\n return configPath;\n}\n\nexport function writeAntigravityHooks(projectPath: string): string | null {\n return writeAntigravityHooksToDir(join(projectPath, '.agents'));\n}\n\nexport function writeAntigravityUserHooks(home: string = homedir()): string | null {\n // Only wire user-level Antigravity/Gemini hooks if the tool is actually\n // present \u2014 never manufacture ~/.gemini for a user who doesn't use it\n // (undisclosed scope, onboarding field report 2026-07-07 F2). `home` is\n // injectable for tests.\n const geminiHome = join(home, '.gemini');\n if (!existsSync(geminiHome)) return null;\n return writeAntigravityHooksToDir(join(geminiHome, 'config'));\n}\n\nfunction removeAntigravityHooksFromFile(configPath: string): boolean {\n if (!existsSync(configPath)) return false;\n let existing: Record<string, unknown>;\n try { existing = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { return false; }\n if (!('rulemetric' in existing)) return false;\n delete existing.rulemetric;\n if (Object.keys(existing).length === 0) {\n // No other blocks \u2014 remove the file entirely.\n try { unlinkSync(configPath); } catch { /* ignore */ }\n } else {\n writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\\n');\n }\n return true;\n}\n\nexport function removeAntigravityHooks(projectPath: string): boolean {\n return removeAntigravityHooksFromFile(join(projectPath, '.agents', 'hooks.json'));\n}\n\nexport function removeAntigravityUserHooks(): boolean {\n const modern = removeAntigravityHooksFromFile(join(homedir(), '.gemini', 'config', 'hooks.json'));\n const legacy = removeAntigravityHooksFromFile(join(homedir(), '.gemini', 'hooks.json'));\n return modern || legacy;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
apiGet,
|
|
16
16
|
apiPatch,
|
|
17
17
|
apiPost
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ZUJBM6HE.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-
|
|
243
|
+
//# sourceMappingURL=chunk-OK3I555A.js.map
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
RULEMETRIC_NO_PROXY,
|
|
2
3
|
detectInstalledTools,
|
|
3
4
|
hasRulemetricClaudeHooks,
|
|
4
|
-
mergeClaudeCodeHooks
|
|
5
|
-
|
|
5
|
+
mergeClaudeCodeHooks,
|
|
6
|
+
removeRulemetricMachineState
|
|
7
|
+
} from "./chunk-LXKPNATC.js";
|
|
6
8
|
import {
|
|
7
9
|
GATEWAY_PORT
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-4LYCDS2N.js";
|
|
9
11
|
import {
|
|
10
12
|
apiGet
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZUJBM6HE.js";
|
|
12
14
|
import {
|
|
13
15
|
getDefaultConfigDir,
|
|
14
16
|
isAuthenticated
|
|
@@ -78,8 +80,8 @@ async function workerActive() {
|
|
|
78
80
|
return false;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
function readProjectSettings(projectPath) {
|
|
82
|
-
const settingsPath = join(projectPath, ".claude",
|
|
83
|
+
function readProjectSettings(projectPath, fileName) {
|
|
84
|
+
const settingsPath = join(projectPath, ".claude", fileName);
|
|
83
85
|
let settings = {};
|
|
84
86
|
let before = null;
|
|
85
87
|
if (existsSync(settingsPath)) {
|
|
@@ -93,18 +95,25 @@ function readProjectSettings(projectPath) {
|
|
|
93
95
|
return { settings, before };
|
|
94
96
|
}
|
|
95
97
|
function projectHooksSettings(projectPath) {
|
|
96
|
-
const { settings, before } = readProjectSettings(projectPath);
|
|
98
|
+
const { settings, before } = readProjectSettings(projectPath, "settings.local.json");
|
|
97
99
|
mergeClaudeCodeHooks(settings);
|
|
98
100
|
return { before, after: JSON.stringify(settings, null, 2) };
|
|
99
101
|
}
|
|
100
102
|
function projectProxyEnvSettings(projectPath) {
|
|
101
|
-
const { settings, before } = readProjectSettings(projectPath);
|
|
103
|
+
const { settings, before } = readProjectSettings(projectPath, "settings.local.json");
|
|
102
104
|
const env = { ...settings.env ?? {} };
|
|
103
105
|
env.HTTPS_PROXY = `http://localhost:${GATEWAY_PORT}`;
|
|
104
|
-
env.NO_PROXY =
|
|
106
|
+
env.NO_PROXY = RULEMETRIC_NO_PROXY;
|
|
105
107
|
env.NODE_EXTRA_CA_CERTS = CERT_PATH;
|
|
106
108
|
return { before, after: JSON.stringify({ ...settings, env }, null, 2) };
|
|
107
109
|
}
|
|
110
|
+
function projectSharedSettingsMigration(projectPath) {
|
|
111
|
+
const { settings, before } = readProjectSettings(projectPath, "settings.json");
|
|
112
|
+
if (before === null) return null;
|
|
113
|
+
const { removed } = removeRulemetricMachineState(settings, GATEWAY_PORT);
|
|
114
|
+
if (!removed) return null;
|
|
115
|
+
return { before, after: JSON.stringify(settings, null, 2) };
|
|
116
|
+
}
|
|
108
117
|
var WORKER_PLIST_PATH = join(homedir(), "Library", "LaunchAgents", "com.rulemetric.worker.plist");
|
|
109
118
|
var WORKER_SYSTEMD_PATH = join(homedir(), ".config", "systemd", "user", "rulemetric-worker.service");
|
|
110
119
|
function runCommand(cmd) {
|
|
@@ -210,17 +219,19 @@ var STEPS = [
|
|
|
210
219
|
// user declines the proxy.
|
|
211
220
|
command: "rulemetric hooks install",
|
|
212
221
|
async detect(ctx) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
for (const fileName of ["settings.local.json", "settings.json"]) {
|
|
223
|
+
const settingsPath = join(ctx.projectPath, ".claude", fileName);
|
|
224
|
+
if (!existsSync(settingsPath)) continue;
|
|
225
|
+
try {
|
|
226
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
227
|
+
if (hasRulemetricClaudeHooks(settings)) return true;
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
220
230
|
}
|
|
231
|
+
return false;
|
|
221
232
|
},
|
|
222
233
|
async plan(ctx) {
|
|
223
|
-
const settingsPath = join(ctx.projectPath, ".claude", "settings.json");
|
|
234
|
+
const settingsPath = join(ctx.projectPath, ".claude", "settings.local.json");
|
|
224
235
|
const { before, after } = projectHooksSettings(ctx.projectPath);
|
|
225
236
|
const artifacts = [
|
|
226
237
|
{ path: settingsPath, kind: "file", before, after },
|
|
@@ -238,7 +249,7 @@ var STEPS = [
|
|
|
238
249
|
// CA-trust sudo skips the machine-wide layer and stays hooks-only.
|
|
239
250
|
{
|
|
240
251
|
// The proxy env block `hooks install` §2 adds to the SAME
|
|
241
|
-
// .claude/settings.json (HTTPS_PROXY / NO_PROXY / NODE_EXTRA_CA_CERTS).
|
|
252
|
+
// .claude/settings.local.json (HTTPS_PROXY / NO_PROXY / NODE_EXTRA_CA_CERTS).
|
|
242
253
|
path: `${settingsPath} (proxy env block)`,
|
|
243
254
|
kind: "file",
|
|
244
255
|
...projectProxyEnvSettings(ctx.projectPath)
|
|
@@ -268,6 +279,14 @@ var STEPS = [
|
|
|
268
279
|
after: "started (routes Claude Code \u2192 mitmproxy \u2192 Anthropic; falls back to direct if mitmproxy is down)"
|
|
269
280
|
}
|
|
270
281
|
];
|
|
282
|
+
const sharedMigration = projectSharedSettingsMigration(ctx.projectPath);
|
|
283
|
+
if (sharedMigration) {
|
|
284
|
+
artifacts.push({
|
|
285
|
+
path: `${join(ctx.projectPath, ".claude", "settings.json")} (removes RuleMetric machine-local state left by an older install \u2014 see the local-file artifacts above for where it moves)`,
|
|
286
|
+
kind: "file",
|
|
287
|
+
...sharedMigration
|
|
288
|
+
});
|
|
289
|
+
}
|
|
271
290
|
for (const tool of detectInstalledTools(ctx.projectPath).filter((t) => t === "cursor" || t === "copilot_agent")) {
|
|
272
291
|
artifacts.push({
|
|
273
292
|
path: `${ctx.projectPath} (${tool} workspace proxy config)`,
|
|
@@ -392,4 +411,4 @@ export {
|
|
|
392
411
|
isStepId,
|
|
393
412
|
computeStatus
|
|
394
413
|
};
|
|
395
|
-
//# sourceMappingURL=chunk-
|
|
414
|
+
//# sourceMappingURL=chunk-OKWCKCPE.js.map
|