@rulemetric/cli 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JM6BQSRE.js → chunk-2WWGG2A5.js} +11 -11
- package/dist/{chunk-4A7FKR2O.js → chunk-42A3HEEH.js} +50 -3
- package/dist/chunk-42A3HEEH.js.map +7 -0
- package/dist/{chunk-PDURR7O3.js → chunk-46LMTDRE.js} +2 -2
- package/dist/{chunk-ISDA2QFX.js → chunk-4QGATD5Y.js} +209 -2
- package/dist/chunk-4QGATD5Y.js.map +7 -0
- package/dist/{chunk-L674RNKQ.js → chunk-ANOWI23I.js} +51 -1
- package/dist/chunk-ANOWI23I.js.map +7 -0
- package/dist/{chunk-3LHLSY7V.js → chunk-C7SRRSPA.js} +6 -6
- package/dist/{chunk-B3HILODD.js → chunk-CLC7ZFH2.js} +6 -6
- package/dist/{chunk-PIOVE3TN.js → chunk-FUS5C4SB.js} +2 -2
- package/dist/{chunk-PHLMIZKM.js → chunk-JNFIYBFL.js} +30 -5
- package/dist/chunk-JNFIYBFL.js.map +7 -0
- package/dist/{chunk-RDIH4AG2.js → chunk-MTLAIJED.js} +3 -3
- package/dist/{chunk-NIDLJO6F.js → chunk-QSN77T7C.js} +23 -1
- package/dist/{chunk-NIDLJO6F.js.map → chunk-QSN77T7C.js.map} +2 -2
- package/dist/chunk-SO3T35U7.js +80 -0
- package/dist/chunk-SO3T35U7.js.map +7 -0
- package/dist/{chunk-ELL34R3K.js → chunk-XMWHTOCC.js} +3 -3
- package/dist/{chunk-3IU7GFXS.js → chunk-YEBBX4HQ.js} +2 -2
- package/dist/commands/evals/agent.js +12 -14
- package/dist/commands/evals/agent.js.map +2 -2
- package/dist/commands/hooks/emit-instructions.js +25 -0
- package/dist/commands/hooks/emit-instructions.js.map +7 -0
- package/dist/commands/hooks/install.js +18 -24
- package/dist/commands/hooks/install.js.map +2 -2
- package/dist/commands/hooks/uninstall.js +4 -4
- package/dist/commands/hooks/uninstall.js.map +2 -2
- package/dist/commands/research/backfill.js +3 -3
- package/dist/commands/research/tail-transcript.js +3 -3
- package/dist/commands/service/install.js +186 -11
- package/dist/commands/service/install.js.map +2 -2
- package/dist/commands/service/uninstall.js +33 -2
- package/dist/commands/service/uninstall.js.map +2 -2
- package/dist/dashboard/Dashboard.js +12 -12
- package/dist/dashboard/data.js +2 -2
- package/dist/{dist-I6BCOLNG.js → dist-NEECHU47.js} +6 -2
- package/dist/lib/agent-loop.js +12 -12
- package/dist/lib/handlers/process-announcement.js +3 -3
- package/dist/lib/handlers/process-changelog.js +3 -3
- package/dist/lib/handlers/process-launch.js +3 -3
- package/dist/lib/handlers/process-run-cleanup.js +4 -4
- package/dist/lib/handlers/process-session-goal.js +2 -2
- package/dist/lib/hooks-config.js +7 -1
- package/dist/lib/instruction-snapshot.js +8 -0
- package/dist/lib/instruction-snapshot.js.map +7 -0
- package/dist/lib/manual-tasks.js +3 -3
- package/dist/lib/statusline-shim.js +1 -1
- package/dist/lib/worktree.js +2 -2
- package/oclif.manifest.json +1014 -977
- package/package.json +18 -15
- package/dist/chunk-4A7FKR2O.js.map +0 -7
- package/dist/chunk-ISDA2QFX.js.map +0 -7
- package/dist/chunk-L674RNKQ.js.map +0 -7
- package/dist/chunk-PHLMIZKM.js.map +0 -7
- /package/dist/{chunk-JM6BQSRE.js.map → chunk-2WWGG2A5.js.map} +0 -0
- /package/dist/{chunk-PDURR7O3.js.map → chunk-46LMTDRE.js.map} +0 -0
- /package/dist/{chunk-3LHLSY7V.js.map → chunk-C7SRRSPA.js.map} +0 -0
- /package/dist/{chunk-B3HILODD.js.map → chunk-CLC7ZFH2.js.map} +0 -0
- /package/dist/{chunk-PIOVE3TN.js.map → chunk-FUS5C4SB.js.map} +0 -0
- /package/dist/{chunk-RDIH4AG2.js.map → chunk-MTLAIJED.js.map} +0 -0
- /package/dist/{chunk-ELL34R3K.js.map → chunk-XMWHTOCC.js.map} +0 -0
- /package/dist/{chunk-3IU7GFXS.js.map → chunk-YEBBX4HQ.js.map} +0 -0
- /package/dist/{dist-I6BCOLNG.js.map → dist-NEECHU47.js.map} +0 -0
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runAgentLoop
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-2WWGG2A5.js";
|
|
4
4
|
import "../../chunk-JX5UI6RW.js";
|
|
5
|
+
import "../../chunk-QJXAULRC.js";
|
|
5
6
|
import "../../chunk-EHABPBGH.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
7
|
+
import "../../chunk-XMWHTOCC.js";
|
|
8
|
+
import "../../chunk-C7SRRSPA.js";
|
|
9
|
+
import "../../chunk-FUS5C4SB.js";
|
|
9
10
|
import "../../chunk-W53GKIZQ.js";
|
|
10
|
-
import "../../chunk-
|
|
11
|
+
import "../../chunk-YEBBX4HQ.js";
|
|
11
12
|
import "../../chunk-XZXS2W24.js";
|
|
12
13
|
import "../../chunk-RQ2TMLKG.js";
|
|
13
14
|
import "../../chunk-YBNW7ARH.js";
|
|
14
15
|
import "../../chunk-FZKLLNDS.js";
|
|
15
|
-
import "../../chunk-
|
|
16
|
+
import "../../chunk-CLC7ZFH2.js";
|
|
17
|
+
import "../../chunk-DGHWRQXL.js";
|
|
16
18
|
import "../../chunk-K3BFZ7KG.js";
|
|
17
|
-
import "../../chunk-
|
|
19
|
+
import "../../chunk-QSN77T7C.js";
|
|
18
20
|
import {
|
|
19
21
|
closeDb
|
|
20
|
-
} from "../../chunk-
|
|
22
|
+
} from "../../chunk-42A3HEEH.js";
|
|
21
23
|
import "../../chunk-BD7CY42U.js";
|
|
22
|
-
import "../../chunk-
|
|
23
|
-
import "../../chunk-DGHWRQXL.js";
|
|
24
|
+
import "../../chunk-IWYCRLEK.js";
|
|
24
25
|
import "../../chunk-J7N3DLH6.js";
|
|
25
26
|
import "../../chunk-52WIYRZH.js";
|
|
26
|
-
import "../../chunk-IWYCRLEK.js";
|
|
27
27
|
import "../../chunk-EKP32DLN.js";
|
|
28
|
-
import "../../chunk-
|
|
28
|
+
import "../../chunk-4QGATD5Y.js";
|
|
29
29
|
import "../../chunk-OQSQC7VB.js";
|
|
30
30
|
import "../../chunk-KRBQLMOP.js";
|
|
31
31
|
import "../../chunk-3TIMQ3O6.js";
|
|
@@ -133,8 +133,6 @@ var enabled = false;
|
|
|
133
133
|
function initSentry(config) {
|
|
134
134
|
if (enabled)
|
|
135
135
|
return false;
|
|
136
|
-
if (process.env.NODE_ENV === "production")
|
|
137
|
-
return false;
|
|
138
136
|
const dsn = process.env.SENTRY_DSN;
|
|
139
137
|
if (!dsn)
|
|
140
138
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../packages/telemetry/src/init.ts", "../../../../../packages/telemetry/src/otlp-headers.ts", "../../../../../packages/telemetry/src/hono-middleware.ts", "../../../../../packages/telemetry/src/mcp-instrumentation.ts", "../../../../../packages/telemetry/src/span-attributes.ts", "../../../../../packages/telemetry/src/sentry.ts", "../../../src/commands/evals/agent.ts"],
|
|
4
|
-
"sourcesContent": ["import { trace, context, type Tracer } from '@opentelemetry/api';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport {\n BatchSpanProcessor,\n ConsoleSpanExporter,\n SimpleSpanProcessor,\n} from '@opentelemetry/sdk-trace-node';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport { parseOtlpHeaders } from './otlp-headers.js';\n\nlet sdk: NodeSDK | null = null;\n\nexport interface TelemetryConfig {\n serviceName: string;\n serviceVersion?: string;\n}\n\n/**\n * Boot states for `initTelemetry`. Returned discriminator lets callers\n * emit a precise boot log:\n * - 'otlp' \u2014 spans ship over OTLP to a real backend (Honeycomb etc.)\n * - 'console' \u2014 spans print to stdout as JSON (Fly logs ingest them);\n * the \"open + zero third-party\" default\n * - 'off' \u2014 telemetry globally disabled (TELEMETRY_ENABLED=false)\n * OR a prior `initTelemetry()` already booted the SDK\n * in this process (idempotent skip)\n */\nexport type TelemetryBootState = 'otlp' | 'console' | 'silent' | 'off';\n\nexport function isTelemetryEnabled(): boolean {\n return process.env.TELEMETRY_ENABLED !== 'false';\n}\n\n/**\n * Initialize OpenTelemetry tracing. Two transport modes, both built on\n * the same OTel SDK \u2014 only the exporter changes:\n *\n * - OTLP backend \u2192 set `OTEL_EXPORTER_OTLP_ENDPOINT` (+ optional\n * `OTEL_EXPORTER_OTLP_HEADERS` for vendor auth)\n * - Console (stdout) \u2192 the default when no endpoint is set\n *\n * The console path is intentionally on-by-default. Without a real backend\n * configured, spans are written to stdout as JSON and end up in Fly's log\n * stream alongside everything else \u2014 searchable, structured, and you\n * stay inside the open OpenTelemetry spec without paying a third party.\n * Swap to OTLP later by setting one env var; no code change needed.\n *\n * Returns `'off'` when:\n * - TELEMETRY_ENABLED=false (operator kill switch \u2014 beats any config)\n * - The SDK was already started in this process (idempotent skip)\n *\n * See packages/telemetry/src/init.test.ts for the full state table.\n */\nexport function initTelemetry(config: TelemetryConfig): TelemetryBootState {\n if (!isTelemetryEnabled()) {\n return 'off';\n }\n if (sdk !== null) {\n // OTel's global tracer provider gets set on first start(); calling\n // start() twice triggers an SDK warning. Treat second-call as a no-op.\n return 'off';\n }\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.1.0',\n });\n\n const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n let mode: TelemetryBootState;\n let spanProcessor: BatchSpanProcessor | SimpleSpanProcessor | undefined;\n\n if (endpoint) {\n // OTLP path. Headers via OTEL_EXPORTER_OTLP_HEADERS (W3C convention,\n // comma-separated key=value pairs). Parsed explicitly rather than\n // relying on SDK-version-specific auto-discovery \u2014 predictable boot\n // matters more than tracking whatever the current default is.\n // Honeycomb: x-honeycomb-team=hcaik_xxx (plus optional dataset)\n // Grafana Cloud: authorization=Basic <base64creds>\n const headers = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);\n const exporter = new OTLPTraceExporter({\n url: `${endpoint}/v1/traces`,\n headers,\n });\n spanProcessor = new BatchSpanProcessor(exporter);\n mode = 'otlp';\n } else if (process.env.OTEL_SPAN_EXPORT === 'none') {\n // Silent path: trace context still propagates (so trace IDs are available\n // for correlation and an OTLP endpoint can be added later with zero code\n // change), but NO span processor is registered \u2014 nothing is written to\n // stdout. This exists because the ConsoleSpanExporter below writes one\n // JSON span per ended span, and on the local launchd API (no OTLP\n // endpoint, fixed log file, no Fly-managed rotation) that grew api-stdout\n // to 370MB / ~400k spans. Set OTEL_SPAN_EXPORT=none on that process; Fly\n // leaves it unset so console spans stay grep-able in its managed log stream.\n spanProcessor = undefined;\n mode = 'silent';\n } else {\n // Console (stdout) path. SimpleSpanProcessor \u2014 not batched \u2014 so spans\n // appear in chronological order in the log stream. Acceptable cost for\n // tracing: a span is written per ended span, vs. a batched POST for OTLP.\n // Logs that would otherwise need a UI to make sense (trace tree\n // visualization) are still queryable via `flyctl logs | grep traceId=...`\n // or any log-aggregator that ingests Fly's stream.\n const exporter = new ConsoleSpanExporter();\n spanProcessor = new SimpleSpanProcessor(exporter);\n mode = 'console';\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors: spanProcessor ? [spanProcessor] : [],\n });\n\n sdk.start();\n return mode;\n}\n\nexport async function shutdownTelemetry(): Promise<void> {\n if (sdk) {\n await sdk.shutdown();\n sdk = null;\n }\n}\n\nexport function getTracer(name?: string): Tracer {\n return trace.getTracer(name ?? 'rulemetric');\n}\n\nexport function getTraceId(): string | undefined {\n const span = trace.getSpan(context.active());\n return span?.spanContext().traceId;\n}\n", "// Parse the `OTEL_EXPORTER_OTLP_HEADERS` env var into a plain headers object\n// that the OTLP exporter constructor accepts.\n//\n// OpenTelemetry SDK env-vars spec (\u00A710) defines this as a comma-separated list\n// of `key=value` pairs. We parse it ourselves rather than relying on the\n// `@opentelemetry/exporter-trace-otlp-http` package's auto-discovery, because\n// that auto-discovery has shifted between SDK versions and we want predictable\n// behaviour at boot.\n//\n// Real-world usage:\n// - Honeycomb: `x-honeycomb-team=hcaik_xxx` (and optionally `,x-honeycomb-dataset=...`)\n// - Grafana Cloud: `authorization=Basic <base64>` (note: base64 padding can be `==`)\n//\n// Edge cases (covered by `otlp-headers.test.ts`):\n// - undefined or empty \u2192 {} (caller will get a no-headers exporter)\n// - whitespace around `=` and `,` is trimmed (operator-friendly)\n// - only the first `=` is the separator, so base64 padding (==) survives in\n// values\n// - malformed entries (no `=`) are skipped silently\n// - empty keys are dropped (never inject a \"\" header)\n// - duplicate keys: last value wins, matching URL search-params semantics\n\nexport function parseOtlpHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) return {};\n const out: Record<string, string> = {};\n for (const entry of raw.split(',')) {\n const eq = entry.indexOf('=');\n if (eq === -1) continue;\n const key = entry.slice(0, eq).trim();\n const value = entry.slice(eq + 1).trim();\n if (!key) continue;\n out[key] = value;\n }\n return out;\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport type { MiddlewareHandler } from 'hono';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function telemetryMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n if (!isTelemetryEnabled()) {\n await next();\n return;\n }\n\n const tracer = getTracer();\n const method = c.req.method;\n const path = c.req.path;\n\n await tracer.startActiveSpan(\n `${method} ${path}`,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.method': method,\n 'http.url': c.req.url,\n 'http.route': path,\n },\n },\n async (span) => {\n try {\n await next();\n\n span.setAttribute('http.status_code', c.res.status);\n\n const userId = c.get('userId' as never);\n if (userId) {\n span.setAttribute('rulemetric.user_id', userId as string);\n }\n\n if (c.res.status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function instrumentMcpTool<TArgs, TResult>(\n name: string,\n handler: (args: TArgs) => Promise<TResult>,\n): (args: TArgs) => Promise<TResult> {\n return async (args: TArgs) => {\n if (!isTelemetryEnabled()) {\n return handler(args);\n }\n\n const tracer = getTracer();\n return tracer.startActiveSpan(\n `mcp.tool.${name}`,\n { kind: SpanKind.INTERNAL },\n async (span) => {\n try {\n const result = await handler(args);\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { trace, context as otelContext } from '@opentelemetry/api';\n\n/**\n * Set attributes on the currently-active span, if one exists. Safe no-op\n * when no span is active (e.g. running outside of a Hono request handler,\n * or with telemetry disabled).\n *\n * Use this from business-logic code that wants to expose a discriminator\n * for telemetry queries \u2014 e.g. \"which branch did `resolveCallerOrgId` take\"\n * in `apps/api/src/routes/sessions.ts`. Span attributes are searchable in\n * any OpenTelemetry backend (Honeycomb, Grafana Cloud, ...) and in\n * `flyctl logs` when the console exporter is active.\n *\n * Keeping this thin wrapper in the telemetry package means application\n * code never imports `@opentelemetry/api` directly \u2014 the package boundary\n * stays the only place that knows about the OTel SDK shape.\n */\nexport function setSpanAttributes(\n attributes: Record<string, string | number | boolean>,\n): void {\n const span = trace.getSpan(otelContext.active());\n if (!span) return;\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n}\n", "// Thin wrapper around @sentry/node so callers don't have to guard every\n// captureException with `if (process.env.SENTRY_DSN)`. The wrapper handles\n// three concerns:\n//\n// 1. Init only when SENTRY_DSN is set. Otherwise every entry point becomes\n// a silent no-op \u2014 never throws, never logs noise, never opens a\n// network transport.\n// 2. Idempotent init. Double-init is a Sentry SDK foot-gun (the second\n// call wins, but the warning lands at runtime). The module-level\n// `enabled` flag short-circuits the second call.\n// 3. tracesSampleRate = 0 by default. We already have OpenTelemetry\n// shipping spans to a tracing backend; paying Sentry's quota for the\n// same data isn't useful. Sentry's job here is errors-only.\n//\n// Wiring:\n// - `apps/api/src/index.ts` \u2192 initSentry({ serviceName: 'rulemetric-api' })\n// - `apps/api/src/middleware/auth.ts` \u2192 setSentryUser(userId) after JWT verify\n// - `apps/cli/src/commands/evals/listen.ts` \u2192 initSentry({ serviceName: 'rulemetric-worker' })\n// - `apps/cli/src/lib/graphile-worker-runner.ts` runner.events.on('job:failed') \u2192 captureException\n//\n// All four hooks are no-op-safe when DSN is unset, so the wiring doesn't\n// require feature-flag plumbing.\n\nimport * as Sentry from '@sentry/node';\n\nlet enabled = false;\n\nexport interface InitSentryConfig {\n serviceName: string;\n}\n\n/**\n * Initialize Sentry if SENTRY_DSN is set and we're NOT in production.\n *\n * Production gate (`NODE_ENV === 'production'`) is intentional: Sentry is\n * scoped to dev/test environments only. The Fly env doesn't get a\n * SENTRY_DSN secret, so the no-DSN check would already cover the common\n * case \u2014 this gate is belt+suspenders against a misconfigured Docker\n * layer or an accidental `flyctl secrets set`. To enable prod error\n * coverage later, drop the `NODE_ENV === 'production'` branch below.\n *\n * Returns true when init actually ran, false when it was skipped (DSN\n * unset, prior call already initialized, or in production). Idempotent.\n */\nexport function initSentry(config: InitSentryConfig): boolean {\n if (enabled) return false;\n if (process.env.NODE_ENV === 'production') return false;\n const dsn = process.env.SENTRY_DSN;\n if (!dsn) return false;\n\n Sentry.init({\n dsn,\n serverName: config.serviceName,\n environment: process.env.NODE_ENV ?? 'development',\n // We rely on OpenTelemetry for tracing \u2014 Sentry stays errors-only here.\n // Override at the env-var level if you ever want Sentry performance too:\n // `SENTRY_TRACES_SAMPLE_RATE=0.05` (Sentry SDK reads it natively).\n tracesSampleRate: 0,\n });\n\n enabled = true;\n return true;\n}\n\n/**\n * Report an exception. Coerces non-Error throws to Error so Sentry's\n * grouping/stack-trace logic has something to work with. Silent no-op when\n * SENTRY_DSN is unset.\n */\nexport function captureException(\n err: unknown,\n context?: { extra?: Record<string, unknown>; tags?: Record<string, string> },\n): void {\n if (!enabled) return;\n const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));\n Sentry.captureException(e, context);\n}\n\n/**\n * Attach (or clear, with null) the active user on the Sentry scope. Called\n * from the API auth middleware so any captured exception during the request\n * carries `user.id`. Silent no-op when SENTRY_DSN is unset.\n */\nexport function setSentryUser(userId: string | null): void {\n if (!enabled) return;\n if (userId === null) {\n Sentry.setUser(null);\n return;\n }\n Sentry.setUser({ id: userId });\n}\n\n/**\n * Flush any queued events with a timeout. Critical to call before\n * `process.exit()` in worker shutdown \u2014 Sentry batches events and the SIGTERM\n * path was previously dropping the final exception (the one we'd most want\n * to see). Resolves immediately when SENTRY_DSN is unset.\n */\nexport async function flushSentry(timeoutMs: number): Promise<void> {\n if (!enabled) return;\n await Sentry.flush(timeoutMs);\n}\n\n/**\n * Reset the module-level state. Test-only \u2014 exported so test suites can\n * unwind the singleton between cases without `vi.resetModules` plumbing in\n * every block.\n */\nexport function _resetSentryForTests(): void {\n enabled = false;\n}\n", "import { initSentry, flushSentry, initTelemetry, shutdownTelemetry } from '@rulemetric/telemetry';\nimport { BaseCommand } from '../../base-command.js';\nimport { loadToken } from '../../lib/auth.js';\nimport { runAgentLoop } from '../../lib/agent-loop.js';\nimport { closeDb } from '@rulemetric/db';\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execFileSync } from 'node:child_process';\n\n// Anchored under ~/.config/rulemetric (like gateway.pid), NOT os.tmpdir(), to\n// avoid launch-context TMPDIR divergence (launchd vs SSH/sudo/CI).\nconst AGENT_LOCK_PATH = join(homedir(), '.config', 'rulemetric', 'evals-agent.lock');\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n // ESRCH = no such process (stale); EPERM = exists but owned by another user\n return (e as { code?: string }).code === 'EPERM';\n }\n}\n\n// Defends against PID reuse: a SIGKILL/OOM leaves the lock behind and the OS may\n// later assign that PID to an unrelated process \u2014 `pidAlive` alone would then\n// wedge `evals agent` (refuse to start forever). Confirm the holder is actually\n// an evals-agent before treating the lock as held; otherwise it's reclaimable.\nfunction isLiveAgent(pid: number): boolean {\n if (!pidAlive(pid)) return false;\n try {\n const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return /run\\.js\\s+evals\\s+agent|evals\\s+agent/.test(cmd);\n } catch {\n // ps unavailable/failed \u2014 fall back to liveness only (conservative: held)\n return true;\n }\n}\n\n// Single-instance guard. Each `evals agent` daemon holds its own postgres-js\n// pool; an orphaned, unsupervised fleet (4 stale agents found alive 3+ days on\n// 2026-06-21) was a confirmed driver of the recurring Supavisor 200-client\n// saturation. ATOMIC acquire via O_EXCL (flag 'wx') closes the TOCTOU window\n// where two simultaneously-starting agents could both acquire. On EEXIST the\n// holder is liveness-checked; a stale lock (holder gone) is reclaimed once.\n// See docs/incidents/2026-06-21-pool-exhaustion.md.\nfunction acquireAgentLock(): { ok: true } | { ok: false; holder: number } {\n try {\n mkdirSync(dirname(AGENT_LOCK_PATH), { recursive: true });\n } catch {\n /* ignore \u2014 writeFileSync will surface a real problem */\n }\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n // 'wx' = O_CREAT | O_EXCL: fails atomically with EEXIST if it exists.\n writeFileSync(AGENT_LOCK_PATH, String(process.pid), { flag: 'wx' });\n return { ok: true };\n } catch (e) {\n if ((e as { code?: string }).code !== 'EEXIST') throw e;\n let holder = 0;\n try {\n holder = Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim());\n } catch {\n /* unreadable \u2014 treat as stale */\n }\n if (holder && holder !== process.pid && isLiveAgent(holder)) {\n return { ok: false, holder };\n }\n // stale (holder gone) / ours / unreadable \u2014 drop it and retry the atomic create once\n try { unlinkSync(AGENT_LOCK_PATH); } catch { /* raced with another reclaimer */ }\n }\n }\n // Last resort after a reclaim race \u2014 best-effort non-exclusive write.\n try { writeFileSync(AGENT_LOCK_PATH, String(process.pid)); } catch { /* ignore */ }\n return { ok: true };\n}\n\nfunction releaseAgentLock(): void {\n try {\n if (\n existsSync(AGENT_LOCK_PATH) &&\n Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim()) === process.pid\n ) {\n unlinkSync(AGENT_LOCK_PATH);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * `rulemetric evals agent` \u2014 Phase 4b parallel-run thin agent.\n *\n * Polls the /api/work endpoints (Phase 4a) instead of binding a Postgres\n * LISTEN/NOTIFY connection. This is the scaffold for the cutover described\n * in docs/plans/2026-05-25-thin-agent-migration.md; the default worker is\n * the sole worker mode since Phase 4c deleted the graphile path.\n *\n * Both commands can run side-by-side on the same user during the cutover \u2014\n * the API's atomic SKIP-LOCKED claim guarantees a job is handed to exactly\n * one agent. Picking which one runs is a launchd / shell choice.\n */\nexport default class EvalAgentCommand extends BaseCommand {\n static override description =\n 'The worker daemon: long-polls /api/work/next, claims jobs, and runs them locally. Installed as a launchd service by `rulemetric service install`.';\n\n static override examples = [\n '<%= config.bin %> evals agent',\n ];\n\n async run(): Promise<void> {\n await this.parse(EvalAgentCommand);\n this.requireAuth();\n\n // Single-instance guard \u2014 refuse to start a second agent. Each extra agent\n // holds its own DB pool; an orphaned fleet was a confirmed cause of pooler\n // saturation (pool-exhaustion RCA 2026-06-21).\n const lock = acquireAgentLock();\n if (!lock.ok) {\n this.log(\n `Another rulemetric evals agent is already running (pid ${lock.holder}). ` +\n 'Refusing to start a second instance.',\n );\n return;\n }\n\n process.env.PG_APPLICATION_NAME ||= 'rulemetric-agent';\n\n const sentryEnabled = initSentry({ serviceName: 'rulemetric-agent' });\n if (sentryEnabled) this.log('[sentry] error capture enabled');\n\n const otelMode = initTelemetry({ serviceName: 'rulemetric-agent' });\n if (otelMode === 'otlp') {\n this.log(`[otel] traces exporting to ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}`);\n } else if (otelMode === 'console') {\n this.log('[otel] cron spans printing to stdout (set OTEL_EXPORTER_OTLP_ENDPOINT to ship to a backend)');\n } else {\n this.log('[otel] disabled (TELEMETRY_ENABLED=false)');\n }\n\n // Early auth sanity check \u2014 the agent loop also reads the token on every\n // request via api-client, but failing here gives a friendlier error than\n // a 401 thirty seconds into the first long-poll.\n const token = loadToken();\n if (!token && !process.env.RULEMETRIC_API_KEY) {\n this.error('No auth token found. Run `rulemetric auth login` first.');\n }\n\n this.log('Starting thin agent (Phase 4b parallel-run)...');\n this.log('Long-polling /api/work/next. Press Ctrl+C to stop.\\n');\n\n const controller = new AbortController();\n\n const shutdown = async (signal: string) => {\n this.log(`\\n[${signal}] shutting down agent...`);\n controller.abort();\n // Flush telemetry + drain the pool on the way out. All members run\n // concurrently under Promise.allSettled (flushSentry 2s, closeDb 5s), so\n // the total stays well under launchd's ~20s shutdown grace.\n await Promise.allSettled([\n flushSentry(2_000),\n shutdownTelemetry(),\n // Drain the postgres-js pool so this agent's Supavisor client sessions\n // are released (Terminate) instead of orphaned as ghosts on every\n // launchd bounce. Pool-exhaustion RCA 2026-06-21.\n closeDb(),\n ]);\n releaseAgentLock();\n process.exit(0);\n };\n process.on('SIGINT', () => { void shutdown('SIGINT'); });\n process.on('SIGTERM', () => { void shutdown('SIGTERM'); });\n\n try {\n await runAgentLoop({\n signal: controller.signal,\n log: (msg) => this.log(msg),\n });\n } finally {\n releaseAgentLock();\n // Drain on the throw path too (defense-in-depth). The signal-handler drain\n // covers normal shutdown; a thrown runAgentLoop must not skip it. closeDb\n // is idempotent, so the happy-path SIGTERM drain stays a no-op.\n await closeDb();\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,eAA4B;AAC5C,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAClC,SACE,oBACA,qBACA,2BACK;AACP,SAAS,gBAAgB;AACzB,SAAS,mBAAmB,4BAA4B;;;ACalD,SAAU,iBAAiB,KAAuB;AACtD,MAAI,CAAC;AAAK,WAAO,CAAA;AACjB,QAAM,MAA8B,CAAA;AACpC,aAAW,SAAS,IAAI,MAAM,GAAG,GAAG;AAClC,UAAM,KAAK,MAAM,QAAQ,GAAG;AAC5B,QAAI,OAAO;AAAI;AACf,UAAM,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,KAAI;AACnC,UAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,EAAE,KAAI;AACtC,QAAI,CAAC;AAAK;AACV,QAAI,GAAG,IAAI;EACb;AACA,SAAO;AACT;;;ADtBA,IAAI,MAAsB;AAmBpB,SAAU,qBAAkB;AAChC,SAAO,QAAQ,IAAI,sBAAsB;AAC3C;AAsBM,SAAU,cAAc,QAAuB;AACnD,MAAI,CAAC,mBAAkB,GAAI;AACzB,WAAO;EACT;AACA,MAAI,QAAQ,MAAM;AAGhB,WAAO;EACT;AAEA,QAAM,WAAW,IAAI,SAAS;IAC5B,CAAC,iBAAiB,GAAG,OAAO;IAC5B,CAAC,oBAAoB,GAAG,OAAO,kBAAkB;GAClD;AAED,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI;AACJ,MAAI;AAEJ,MAAI,UAAU;AAOZ,UAAM,UAAU,iBAAiB,QAAQ,IAAI,0BAA0B;AACvE,UAAM,WAAW,IAAI,kBAAkB;MACrC,KAAK,GAAG,QAAQ;MAChB;KACD;AACD,oBAAgB,IAAI,mBAAmB,QAAQ;AAC/C,WAAO;EACT,WAAW,QAAQ,IAAI,qBAAqB,QAAQ;AASlD,oBAAgB;AAChB,WAAO;EACT,OAAO;AAOL,UAAM,WAAW,IAAI,oBAAmB;AACxC,oBAAgB,IAAI,oBAAoB,QAAQ;AAChD,WAAO;EACT;AAEA,QAAM,IAAI,QAAQ;IAChB;IACA,gBAAgB,gBAAgB,CAAC,aAAa,IAAI,CAAA;GACnD;AAED,MAAI,MAAK;AACT,SAAO;AACT;AAEA,eAAsB,oBAAiB;AACrC,MAAI,KAAK;AACP,UAAM,IAAI,SAAQ;AAClB,UAAM;EACR;AACF;;;AE7HA,SAAS,UAAU,sBAAsB;;;ACAzC,SAAS,YAAAA,WAAU,kBAAAC,uBAAsB;;;ACAzC,SAAS,SAAAC,QAAO,WAAW,mBAAmB;;;
|
|
4
|
+
"sourcesContent": ["import { trace, context, type Tracer } from '@opentelemetry/api';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport {\n BatchSpanProcessor,\n ConsoleSpanExporter,\n SimpleSpanProcessor,\n} from '@opentelemetry/sdk-trace-node';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport { parseOtlpHeaders } from './otlp-headers.js';\n\nlet sdk: NodeSDK | null = null;\n\nexport interface TelemetryConfig {\n serviceName: string;\n serviceVersion?: string;\n}\n\n/**\n * Boot states for `initTelemetry`. Returned discriminator lets callers\n * emit a precise boot log:\n * - 'otlp' \u2014 spans ship over OTLP to a real backend (Honeycomb etc.)\n * - 'console' \u2014 spans print to stdout as JSON (Fly logs ingest them);\n * the \"open + zero third-party\" default\n * - 'off' \u2014 telemetry globally disabled (TELEMETRY_ENABLED=false)\n * OR a prior `initTelemetry()` already booted the SDK\n * in this process (idempotent skip)\n */\nexport type TelemetryBootState = 'otlp' | 'console' | 'silent' | 'off';\n\nexport function isTelemetryEnabled(): boolean {\n return process.env.TELEMETRY_ENABLED !== 'false';\n}\n\n/**\n * Initialize OpenTelemetry tracing. Two transport modes, both built on\n * the same OTel SDK \u2014 only the exporter changes:\n *\n * - OTLP backend \u2192 set `OTEL_EXPORTER_OTLP_ENDPOINT` (+ optional\n * `OTEL_EXPORTER_OTLP_HEADERS` for vendor auth)\n * - Console (stdout) \u2192 the default when no endpoint is set\n *\n * The console path is intentionally on-by-default. Without a real backend\n * configured, spans are written to stdout as JSON and end up in Fly's log\n * stream alongside everything else \u2014 searchable, structured, and you\n * stay inside the open OpenTelemetry spec without paying a third party.\n * Swap to OTLP later by setting one env var; no code change needed.\n *\n * Returns `'off'` when:\n * - TELEMETRY_ENABLED=false (operator kill switch \u2014 beats any config)\n * - The SDK was already started in this process (idempotent skip)\n *\n * See packages/telemetry/src/init.test.ts for the full state table.\n */\nexport function initTelemetry(config: TelemetryConfig): TelemetryBootState {\n if (!isTelemetryEnabled()) {\n return 'off';\n }\n if (sdk !== null) {\n // OTel's global tracer provider gets set on first start(); calling\n // start() twice triggers an SDK warning. Treat second-call as a no-op.\n return 'off';\n }\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.1.0',\n });\n\n const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n let mode: TelemetryBootState;\n let spanProcessor: BatchSpanProcessor | SimpleSpanProcessor | undefined;\n\n if (endpoint) {\n // OTLP path. Headers via OTEL_EXPORTER_OTLP_HEADERS (W3C convention,\n // comma-separated key=value pairs). Parsed explicitly rather than\n // relying on SDK-version-specific auto-discovery \u2014 predictable boot\n // matters more than tracking whatever the current default is.\n // Honeycomb: x-honeycomb-team=hcaik_xxx (plus optional dataset)\n // Grafana Cloud: authorization=Basic <base64creds>\n const headers = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);\n const exporter = new OTLPTraceExporter({\n url: `${endpoint}/v1/traces`,\n headers,\n });\n spanProcessor = new BatchSpanProcessor(exporter);\n mode = 'otlp';\n } else if (process.env.OTEL_SPAN_EXPORT === 'none') {\n // Silent path: trace context still propagates (so trace IDs are available\n // for correlation and an OTLP endpoint can be added later with zero code\n // change), but NO span processor is registered \u2014 nothing is written to\n // stdout. This exists because the ConsoleSpanExporter below writes one\n // JSON span per ended span, and on the local launchd API (no OTLP\n // endpoint, fixed log file, no Fly-managed rotation) that grew api-stdout\n // to 370MB / ~400k spans. Set OTEL_SPAN_EXPORT=none on that process; Fly\n // leaves it unset so console spans stay grep-able in its managed log stream.\n spanProcessor = undefined;\n mode = 'silent';\n } else {\n // Console (stdout) path. SimpleSpanProcessor \u2014 not batched \u2014 so spans\n // appear in chronological order in the log stream. Acceptable cost for\n // tracing: a span is written per ended span, vs. a batched POST for OTLP.\n // Logs that would otherwise need a UI to make sense (trace tree\n // visualization) are still queryable via `flyctl logs | grep traceId=...`\n // or any log-aggregator that ingests Fly's stream.\n const exporter = new ConsoleSpanExporter();\n spanProcessor = new SimpleSpanProcessor(exporter);\n mode = 'console';\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors: spanProcessor ? [spanProcessor] : [],\n });\n\n sdk.start();\n return mode;\n}\n\nexport async function shutdownTelemetry(): Promise<void> {\n if (sdk) {\n await sdk.shutdown();\n sdk = null;\n }\n}\n\nexport function getTracer(name?: string): Tracer {\n return trace.getTracer(name ?? 'rulemetric');\n}\n\nexport function getTraceId(): string | undefined {\n const span = trace.getSpan(context.active());\n return span?.spanContext().traceId;\n}\n", "// Parse the `OTEL_EXPORTER_OTLP_HEADERS` env var into a plain headers object\n// that the OTLP exporter constructor accepts.\n//\n// OpenTelemetry SDK env-vars spec (\u00A710) defines this as a comma-separated list\n// of `key=value` pairs. We parse it ourselves rather than relying on the\n// `@opentelemetry/exporter-trace-otlp-http` package's auto-discovery, because\n// that auto-discovery has shifted between SDK versions and we want predictable\n// behaviour at boot.\n//\n// Real-world usage:\n// - Honeycomb: `x-honeycomb-team=hcaik_xxx` (and optionally `,x-honeycomb-dataset=...`)\n// - Grafana Cloud: `authorization=Basic <base64>` (note: base64 padding can be `==`)\n//\n// Edge cases (covered by `otlp-headers.test.ts`):\n// - undefined or empty \u2192 {} (caller will get a no-headers exporter)\n// - whitespace around `=` and `,` is trimmed (operator-friendly)\n// - only the first `=` is the separator, so base64 padding (==) survives in\n// values\n// - malformed entries (no `=`) are skipped silently\n// - empty keys are dropped (never inject a \"\" header)\n// - duplicate keys: last value wins, matching URL search-params semantics\n\nexport function parseOtlpHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) return {};\n const out: Record<string, string> = {};\n for (const entry of raw.split(',')) {\n const eq = entry.indexOf('=');\n if (eq === -1) continue;\n const key = entry.slice(0, eq).trim();\n const value = entry.slice(eq + 1).trim();\n if (!key) continue;\n out[key] = value;\n }\n return out;\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport type { MiddlewareHandler } from 'hono';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function telemetryMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n if (!isTelemetryEnabled()) {\n await next();\n return;\n }\n\n const tracer = getTracer();\n const method = c.req.method;\n const path = c.req.path;\n\n await tracer.startActiveSpan(\n `${method} ${path}`,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.method': method,\n 'http.url': c.req.url,\n 'http.route': path,\n },\n },\n async (span) => {\n try {\n await next();\n\n span.setAttribute('http.status_code', c.res.status);\n\n const userId = c.get('userId' as never);\n if (userId) {\n span.setAttribute('rulemetric.user_id', userId as string);\n }\n\n if (c.res.status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function instrumentMcpTool<TArgs, TResult>(\n name: string,\n handler: (args: TArgs) => Promise<TResult>,\n): (args: TArgs) => Promise<TResult> {\n return async (args: TArgs) => {\n if (!isTelemetryEnabled()) {\n return handler(args);\n }\n\n const tracer = getTracer();\n return tracer.startActiveSpan(\n `mcp.tool.${name}`,\n { kind: SpanKind.INTERNAL },\n async (span) => {\n try {\n const result = await handler(args);\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { trace, context as otelContext } from '@opentelemetry/api';\n\n/**\n * Set attributes on the currently-active span, if one exists. Safe no-op\n * when no span is active (e.g. running outside of a Hono request handler,\n * or with telemetry disabled).\n *\n * Use this from business-logic code that wants to expose a discriminator\n * for telemetry queries \u2014 e.g. \"which branch did `resolveCallerOrgId` take\"\n * in `apps/api/src/routes/sessions.ts`. Span attributes are searchable in\n * any OpenTelemetry backend (Honeycomb, Grafana Cloud, ...) and in\n * `flyctl logs` when the console exporter is active.\n *\n * Keeping this thin wrapper in the telemetry package means application\n * code never imports `@opentelemetry/api` directly \u2014 the package boundary\n * stays the only place that knows about the OTel SDK shape.\n */\nexport function setSpanAttributes(\n attributes: Record<string, string | number | boolean>,\n): void {\n const span = trace.getSpan(otelContext.active());\n if (!span) return;\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n}\n", "// Thin wrapper around @sentry/node so callers don't have to guard every\n// captureException with `if (process.env.SENTRY_DSN)`. The wrapper handles\n// three concerns:\n//\n// 1. Init only when SENTRY_DSN is set. Otherwise every entry point becomes\n// a silent no-op \u2014 never throws, never logs noise, never opens a\n// network transport.\n// 2. Idempotent init. Double-init is a Sentry SDK foot-gun (the second\n// call wins, but the warning lands at runtime). The module-level\n// `enabled` flag short-circuits the second call.\n// 3. tracesSampleRate = 0 by default. We already have OpenTelemetry\n// shipping spans to a tracing backend; paying Sentry's quota for the\n// same data isn't useful. Sentry's job here is errors-only.\n//\n// Wiring:\n// - `apps/api/src/index.ts` \u2192 initSentry({ serviceName: 'rulemetric-api' })\n// - `apps/api/src/middleware/auth.ts` \u2192 setSentryUser(userId) after JWT verify\n// - `apps/cli/src/commands/evals/agent.ts` \u2192 initSentry({ serviceName: 'rulemetric-agent' })\n// (the thin-agent worker \u2014 Phase 4c moved the init here from the old\n// graphile `listen.ts`; that path no longer inits Sentry)\n//\n// All four hooks are no-op-safe when DSN is unset, so the wiring doesn't\n// require feature-flag plumbing.\n\nimport * as Sentry from '@sentry/node';\n\nlet enabled = false;\n\nexport interface InitSentryConfig {\n serviceName: string;\n}\n\n/**\n * Initialize Sentry if SENTRY_DSN is set. Presence of the DSN is now the\n * single switch: set it (incl. as a Fly secret in prod) to enable error\n * capture, leave it unset for a silent no-op.\n *\n * Previously gated to non-production by policy; that gate was dropped so\n * production errors actually get reported. Errors-only (`tracesSampleRate:\n * 0`) keeps Sentry quota bounded \u2014 OpenTelemetry still owns tracing.\n *\n * Returns true when init actually ran, false when it was skipped (DSN unset\n * or a prior call already initialized). Idempotent.\n */\nexport function initSentry(config: InitSentryConfig): boolean {\n if (enabled) return false;\n const dsn = process.env.SENTRY_DSN;\n if (!dsn) return false;\n\n Sentry.init({\n dsn,\n serverName: config.serviceName,\n environment: process.env.NODE_ENV ?? 'development',\n // We rely on OpenTelemetry for tracing \u2014 Sentry stays errors-only here.\n // Override at the env-var level if you ever want Sentry performance too:\n // `SENTRY_TRACES_SAMPLE_RATE=0.05` (Sentry SDK reads it natively).\n tracesSampleRate: 0,\n });\n\n enabled = true;\n return true;\n}\n\n/**\n * Report an exception. Coerces non-Error throws to Error so Sentry's\n * grouping/stack-trace logic has something to work with. Silent no-op when\n * SENTRY_DSN is unset.\n */\nexport function captureException(\n err: unknown,\n context?: { extra?: Record<string, unknown>; tags?: Record<string, string> },\n): void {\n if (!enabled) return;\n const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));\n Sentry.captureException(e, context);\n}\n\n/**\n * Attach (or clear, with null) the active user on the Sentry scope. Called\n * from the API auth middleware so any captured exception during the request\n * carries `user.id`. Silent no-op when SENTRY_DSN is unset.\n */\nexport function setSentryUser(userId: string | null): void {\n if (!enabled) return;\n if (userId === null) {\n Sentry.setUser(null);\n return;\n }\n Sentry.setUser({ id: userId });\n}\n\n/**\n * Flush any queued events with a timeout. Critical to call before\n * `process.exit()` in worker shutdown \u2014 Sentry batches events and the SIGTERM\n * path was previously dropping the final exception (the one we'd most want\n * to see). Resolves immediately when SENTRY_DSN is unset.\n */\nexport async function flushSentry(timeoutMs: number): Promise<void> {\n if (!enabled) return;\n await Sentry.flush(timeoutMs);\n}\n\n/**\n * Reset the module-level state. Test-only \u2014 exported so test suites can\n * unwind the singleton between cases without `vi.resetModules` plumbing in\n * every block.\n */\nexport function _resetSentryForTests(): void {\n enabled = false;\n}\n", "import { initSentry, flushSentry, initTelemetry, shutdownTelemetry } from '@rulemetric/telemetry';\nimport { BaseCommand } from '../../base-command.js';\nimport { loadToken } from '../../lib/auth.js';\nimport { runAgentLoop } from '../../lib/agent-loop.js';\nimport { closeDb } from '@rulemetric/db';\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execFileSync } from 'node:child_process';\n\n// Anchored under ~/.config/rulemetric (like gateway.pid), NOT os.tmpdir(), to\n// avoid launch-context TMPDIR divergence (launchd vs SSH/sudo/CI).\nconst AGENT_LOCK_PATH = join(homedir(), '.config', 'rulemetric', 'evals-agent.lock');\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n // ESRCH = no such process (stale); EPERM = exists but owned by another user\n return (e as { code?: string }).code === 'EPERM';\n }\n}\n\n// Defends against PID reuse: a SIGKILL/OOM leaves the lock behind and the OS may\n// later assign that PID to an unrelated process \u2014 `pidAlive` alone would then\n// wedge `evals agent` (refuse to start forever). Confirm the holder is actually\n// an evals-agent before treating the lock as held; otherwise it's reclaimable.\nfunction isLiveAgent(pid: number): boolean {\n if (!pidAlive(pid)) return false;\n try {\n const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return /run\\.js\\s+evals\\s+agent|evals\\s+agent/.test(cmd);\n } catch {\n // ps unavailable/failed \u2014 fall back to liveness only (conservative: held)\n return true;\n }\n}\n\n// Single-instance guard. Each `evals agent` daemon holds its own postgres-js\n// pool; an orphaned, unsupervised fleet (4 stale agents found alive 3+ days on\n// 2026-06-21) was a confirmed driver of the recurring Supavisor 200-client\n// saturation. ATOMIC acquire via O_EXCL (flag 'wx') closes the TOCTOU window\n// where two simultaneously-starting agents could both acquire. On EEXIST the\n// holder is liveness-checked; a stale lock (holder gone) is reclaimed once.\n// See docs/incidents/2026-06-21-pool-exhaustion.md.\nfunction acquireAgentLock(): { ok: true } | { ok: false; holder: number } {\n try {\n mkdirSync(dirname(AGENT_LOCK_PATH), { recursive: true });\n } catch {\n /* ignore \u2014 writeFileSync will surface a real problem */\n }\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n // 'wx' = O_CREAT | O_EXCL: fails atomically with EEXIST if it exists.\n writeFileSync(AGENT_LOCK_PATH, String(process.pid), { flag: 'wx' });\n return { ok: true };\n } catch (e) {\n if ((e as { code?: string }).code !== 'EEXIST') throw e;\n let holder = 0;\n try {\n holder = Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim());\n } catch {\n /* unreadable \u2014 treat as stale */\n }\n if (holder && holder !== process.pid && isLiveAgent(holder)) {\n return { ok: false, holder };\n }\n // stale (holder gone) / ours / unreadable \u2014 drop it and retry the atomic create once\n try { unlinkSync(AGENT_LOCK_PATH); } catch { /* raced with another reclaimer */ }\n }\n }\n // Last resort after a reclaim race \u2014 best-effort non-exclusive write.\n try { writeFileSync(AGENT_LOCK_PATH, String(process.pid)); } catch { /* ignore */ }\n return { ok: true };\n}\n\nfunction releaseAgentLock(): void {\n try {\n if (\n existsSync(AGENT_LOCK_PATH) &&\n Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim()) === process.pid\n ) {\n unlinkSync(AGENT_LOCK_PATH);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * `rulemetric evals agent` \u2014 Phase 4b parallel-run thin agent.\n *\n * Polls the /api/work endpoints (Phase 4a) instead of binding a Postgres\n * LISTEN/NOTIFY connection. This is the scaffold for the cutover described\n * in docs/plans/2026-05-25-thin-agent-migration.md; the default worker is\n * the sole worker mode since Phase 4c deleted the graphile path.\n *\n * Both commands can run side-by-side on the same user during the cutover \u2014\n * the API's atomic SKIP-LOCKED claim guarantees a job is handed to exactly\n * one agent. Picking which one runs is a launchd / shell choice.\n */\nexport default class EvalAgentCommand extends BaseCommand {\n static override description =\n 'The worker daemon: long-polls /api/work/next, claims jobs, and runs them locally. Installed as a launchd service by `rulemetric service install`.';\n\n static override examples = [\n '<%= config.bin %> evals agent',\n ];\n\n async run(): Promise<void> {\n await this.parse(EvalAgentCommand);\n this.requireAuth();\n\n // Single-instance guard \u2014 refuse to start a second agent. Each extra agent\n // holds its own DB pool; an orphaned fleet was a confirmed cause of pooler\n // saturation (pool-exhaustion RCA 2026-06-21).\n const lock = acquireAgentLock();\n if (!lock.ok) {\n this.log(\n `Another rulemetric evals agent is already running (pid ${lock.holder}). ` +\n 'Refusing to start a second instance.',\n );\n return;\n }\n\n process.env.PG_APPLICATION_NAME ||= 'rulemetric-agent';\n\n const sentryEnabled = initSentry({ serviceName: 'rulemetric-agent' });\n if (sentryEnabled) this.log('[sentry] error capture enabled');\n\n const otelMode = initTelemetry({ serviceName: 'rulemetric-agent' });\n if (otelMode === 'otlp') {\n this.log(`[otel] traces exporting to ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}`);\n } else if (otelMode === 'console') {\n this.log('[otel] cron spans printing to stdout (set OTEL_EXPORTER_OTLP_ENDPOINT to ship to a backend)');\n } else {\n this.log('[otel] disabled (TELEMETRY_ENABLED=false)');\n }\n\n // Early auth sanity check \u2014 the agent loop also reads the token on every\n // request via api-client, but failing here gives a friendlier error than\n // a 401 thirty seconds into the first long-poll.\n const token = loadToken();\n if (!token && !process.env.RULEMETRIC_API_KEY) {\n this.error('No auth token found. Run `rulemetric auth login` first.');\n }\n\n this.log('Starting thin agent (Phase 4b parallel-run)...');\n this.log('Long-polling /api/work/next. Press Ctrl+C to stop.\\n');\n\n const controller = new AbortController();\n\n const shutdown = async (signal: string) => {\n this.log(`\\n[${signal}] shutting down agent...`);\n controller.abort();\n // Flush telemetry + drain the pool on the way out. All members run\n // concurrently under Promise.allSettled (flushSentry 2s, closeDb 5s), so\n // the total stays well under launchd's ~20s shutdown grace.\n await Promise.allSettled([\n flushSentry(2_000),\n shutdownTelemetry(),\n // Drain the postgres-js pool so this agent's Supavisor client sessions\n // are released (Terminate) instead of orphaned as ghosts on every\n // launchd bounce. Pool-exhaustion RCA 2026-06-21.\n closeDb(),\n ]);\n releaseAgentLock();\n process.exit(0);\n };\n process.on('SIGINT', () => { void shutdown('SIGINT'); });\n process.on('SIGTERM', () => { void shutdown('SIGTERM'); });\n\n try {\n await runAgentLoop({\n signal: controller.signal,\n log: (msg) => this.log(msg),\n });\n } finally {\n releaseAgentLock();\n // Drain on the throw path too (defense-in-depth). The signal-handler drain\n // covers normal shutdown; a thrown runAgentLoop must not skip it. closeDb\n // is idempotent, so the happy-path SIGTERM drain stays a no-op.\n await closeDb();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["SpanKind", "SpanStatusCode", "trace"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildInstructionSnapshot
|
|
3
|
+
} from "../../chunk-SO3T35U7.js";
|
|
4
|
+
import "../../chunk-NSBPE2FW.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/hooks/emit-instructions.ts
|
|
7
|
+
import { Command, Flags } from "@oclif/core";
|
|
8
|
+
var HooksEmitInstructions = class _HooksEmitInstructions extends Command {
|
|
9
|
+
static description = "Print a synthetic instruction snapshot (on-disk CLAUDE.md) as JSON for zero-proxy instruction linking";
|
|
10
|
+
static hidden = true;
|
|
11
|
+
static flags = {
|
|
12
|
+
"project-dir": Flags.string({ description: "Project directory to scan (defaults to cwd)" })
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(_HooksEmitInstructions);
|
|
16
|
+
const snapshot = buildInstructionSnapshot(flags["project-dir"] ?? process.cwd());
|
|
17
|
+
if (snapshot) {
|
|
18
|
+
process.stdout.write(JSON.stringify(snapshot));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export {
|
|
23
|
+
HooksEmitInstructions as default
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=emit-instructions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/commands/hooks/emit-instructions.ts"],
|
|
4
|
+
"sourcesContent": ["import { Command, Flags } from '@oclif/core';\nimport { buildInstructionSnapshot } from '../../lib/instruction-snapshot.js';\n\n/**\n * Prints a synthetic instruction snapshot (built from on-disk CLAUDE.md files) as\n * JSON on stdout. Consumed by `session-end.sh`, which POSTs it to\n * `/api/sessions/:id/context-snapshots` so `link-instructions` can link\n * `claude_md` instructions on a hooks-only (zero-proxy) session.\n *\n * Extends raw oclif `Command` (not BaseCommand): it does no network or auth \u2014 it\n * only reads local files \u2014 and stdout MUST be clean JSON for the curl in\n * session-end.sh. Prints nothing when the project has no instruction files.\n */\nexport default class HooksEmitInstructions extends Command {\n static override description =\n 'Print a synthetic instruction snapshot (on-disk CLAUDE.md) as JSON for zero-proxy instruction linking';\n\n static override hidden = true;\n\n static override flags = {\n 'project-dir': Flags.string({ description: 'Project directory to scan (defaults to cwd)' }),\n };\n\n async run(): Promise<void> {\n const { flags } = await this.parse(HooksEmitInstructions);\n const snapshot = buildInstructionSnapshot(flags['project-dir'] ?? process.cwd());\n if (snapshot) {\n process.stdout.write(JSON.stringify(snapshot));\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;AAAA,SAAS,SAAS,aAAa;AAa/B,IAAqB,wBAArB,MAAqB,+BAA8B,QAAQ;AAAA,EACzD,OAAgB,cACd;AAAA,EAEF,OAAgB,SAAS;AAAA,EAEzB,OAAgB,QAAQ;AAAA,IACtB,eAAe,MAAM,OAAO,EAAE,aAAa,8CAA8C,CAAC;AAAA,EAC5F;AAAA,EAEA,MAAM,MAAqB;AACzB,UAAM,EAAE,MAAM,IAAI,MAAM,KAAK,MAAM,sBAAqB;AACxD,UAAM,WAAW,yBAAyB,MAAM,aAAa,KAAK,QAAQ,IAAI,CAAC;AAC/E,QAAI,UAAU;AACZ,cAAQ,OAAO,MAAM,KAAK,UAAU,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
isStatuslineShimInstalled,
|
|
4
4
|
readCurrentStatusLine,
|
|
5
5
|
writeStatuslineShim
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-JNFIYBFL.js";
|
|
7
7
|
import {
|
|
8
8
|
detectTmux
|
|
9
9
|
} from "../../chunk-BO76WKJR.js";
|
|
10
10
|
import {
|
|
11
11
|
installMacOSProxyEnv,
|
|
12
|
+
mergeClaudeCodeHooks,
|
|
13
|
+
removeClaudeCodeHooks,
|
|
12
14
|
writeAntigravityHooks,
|
|
13
15
|
writeAntigravityUserHooks,
|
|
14
16
|
writeCopilotHooks,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
writeCursorUserProxyConfig,
|
|
19
21
|
writeVSCodeProxyConfig,
|
|
20
22
|
writeVSCodeUserProxyConfig
|
|
21
|
-
} from "../../chunk-
|
|
23
|
+
} from "../../chunk-ANOWI23I.js";
|
|
22
24
|
import "../../chunk-KRBQLMOP.js";
|
|
23
25
|
import {
|
|
24
26
|
GATEWAY_PORT,
|
|
@@ -93,23 +95,11 @@ var HooksInstall = class _HooksInstall extends BaseCommand {
|
|
|
93
95
|
if (existsSync(configPath)) {
|
|
94
96
|
settings = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!Array.isArray(entries)) continue;
|
|
102
|
-
const kept = entries.filter((entry) => {
|
|
103
|
-
const isRulemetric = entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes("rulemetric"));
|
|
104
|
-
if (isRulemetric) removed += 1;
|
|
105
|
-
return !isRulemetric;
|
|
106
|
-
});
|
|
107
|
-
if (kept.length === 0) delete hooks[event];
|
|
108
|
-
else hooks[event] = kept;
|
|
109
|
-
}
|
|
110
|
-
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
111
|
-
if (removed > 0) this.log(`[OK] Removed ${removed} legacy rulemetric session hook(s) (no longer needed)`);
|
|
112
|
-
}
|
|
98
|
+
const refreshedHooks = removeClaudeCodeHooks(settings);
|
|
99
|
+
mergeClaudeCodeHooks(settings);
|
|
100
|
+
this.log(
|
|
101
|
+
refreshedHooks ? "[OK] Refreshed RuleMetric Claude Code session hooks \u2192 .claude/settings.json" : "[OK] Installed RuleMetric Claude Code session hooks \u2192 .claude/settings.json"
|
|
102
|
+
);
|
|
113
103
|
if (!noProxy) {
|
|
114
104
|
const hasMitmproxy = this.findBinary("mitmdump");
|
|
115
105
|
if (!hasMitmproxy) {
|
|
@@ -244,16 +234,20 @@ var HooksInstall = class _HooksInstall extends BaseCommand {
|
|
|
244
234
|
this.log("");
|
|
245
235
|
this.log("The API service starts on login and restarts on crash.");
|
|
246
236
|
} else if (!noProxy) {
|
|
247
|
-
this.log(" 1. Start
|
|
248
|
-
this.log(" 2. Start
|
|
237
|
+
this.log(" 1. Start Claude Code \u2014 sessions are captured by hooks immediately");
|
|
238
|
+
this.log(" 2. Start the capture proxy for full depth: rulemetric proxy start");
|
|
249
239
|
this.log("");
|
|
250
|
-
this.log("
|
|
240
|
+
this.log("Hooks capture every session (lifecycle + transcript). The proxy adds the");
|
|
241
|
+
this.log("rendered system prompt (instruction-effectiveness linking) + cross-tool capture.");
|
|
251
242
|
this.log("");
|
|
252
243
|
this.log("To capture traffic from other clients (Python, curl, etc.):");
|
|
253
244
|
this.log(' eval "$(rulemetric proxy env --all)"');
|
|
254
245
|
} else {
|
|
255
|
-
this.log(" 1. Start Claude Code \u2014
|
|
256
|
-
this.log("
|
|
246
|
+
this.log(" 1. Start Claude Code \u2014 sessions are captured by the hooks just installed");
|
|
247
|
+
this.log("");
|
|
248
|
+
this.log("No proxy means no mitmproxy / CA cert needed. The hooks capture full");
|
|
249
|
+
this.log("sessions on their own; re-run without --no-proxy to also capture system");
|
|
250
|
+
this.log("prompts for instruction-effectiveness linking.");
|
|
257
251
|
}
|
|
258
252
|
await bootstrapActiveOrg();
|
|
259
253
|
const tmux = await detectTmux();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/commands/hooks/install.ts"],
|
|
4
|
-
"sourcesContent": ["import { Flags } from '@oclif/core';\nimport { execFileSync, execSync, spawnSync } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { BaseCommand } from '../../base-command.js';\nimport { isGatewayRunning, spawnGateway, GATEWAY_PORT } from '../../lib/gateway-lifecycle.js';\n\n// True iff `rulemetric service install` has wired the gateway up as a launchd\n// agent. When this returns true, hooks install must NOT spawn its own\n// detached gateway \u2014 two processes fighting for :8787 means one crash-loops\n// and Claude Code's HTTPS_PROXY breaks.\nfunction isGatewayLaunchdManaged(): boolean {\n try {\n execFileSync('launchctl', ['list', 'com.rulemetric.gateway'], {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n return true;\n } catch {\n // Non-zero exit = label not loaded\n return false;\n }\n}\nimport {\n writeStatuslineShim,\n readCurrentStatusLine,\n isStatuslineShimInstalled,\n STATUSLINE_SETTINGS_VALUE,\n} from '../../lib/statusline-shim.js';\nimport {\n writeCursorProxyConfig,\n writeCursorUserProxyConfig,\n writeVSCodeProxyConfig,\n installMacOSProxyEnv,\n writeVSCodeUserProxyConfig,\n writeCursorHooks,\n writeCursorUserHooks,\n writeCopilotHooks,\n writeAntigravityHooks,\n writeAntigravityUserHooks,\n} from '../../lib/hooks-config.js';\nimport { bootstrapActiveOrg } from '../../lib/active-org-refresh.js';\nimport { detectTmux } from '../../lib/detect-tmux.js';\n\nconst CERT_PATH = join(homedir(), '.mitmproxy', 'mitmproxy-ca-cert.pem');\n\nexport default class HooksInstall extends BaseCommand {\n static override description = 'Install session tracking hooks and context capture proxy for Claude Code';\n\n static override examples = [\n '<%= config.bin %> hooks install',\n '<%= config.bin %> hooks install --global',\n '<%= config.bin %> hooks install --no-proxy',\n ];\n\n static override flags = {\n 'no-proxy': Flags.boolean({\n default: false,\n description: 'Skip proxy/gateway configuration (hooks only)',\n }),\n 'project-dir': Flags.string({\n description: 'Project directory to install hooks in (defaults to cwd)',\n }),\n global: Flags.boolean({\n char: 'g',\n default: false,\n description: 'Install hooks globally (~/.claude/settings.json) so all projects are tracked',\n }),\n 'statusline-usage': Flags.boolean({\n default: false,\n description: 'Install the statusline shim so Claude Code pushes live rate_limits on every refresh (makes /usage rings update in real time without needing the proxy)',\n }),\n };\n\n async run(): Promise<void> {\n const { flags } = await this.parse(HooksInstall);\n const isGlobal = flags.global;\n const projectPath = isGlobal ? homedir() : (flags['project-dir'] ?? process.cwd());\n const configDir = join(projectPath, '.claude');\n const configPath = join(configDir, 'settings.json');\n const noProxy = flags['no-proxy'];\n\n if (isGlobal) {\n this.log(`Installing hooks globally \u2192 ${configPath}`);\n }\n\n // Read or create .claude/settings.json\n let settings: Record<string, unknown> = {};\n if (existsSync(configPath)) {\n settings = JSON.parse(readFileSync(configPath, 'utf-8'));\n }\n\n // \u2500\u2500 1. Remove legacy rulemetric hooks (session data now comes from the\n // proxy + local file providers). Only strip entries whose command invokes\n // rulemetric \u2014 the user's own hooks must survive an install run.\n if (settings.hooks && typeof settings.hooks === 'object') {\n const hooks = settings.hooks as Record<string, Array<{ matcher?: string; hooks?: Array<{ command?: string }> }>>;\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((entry) => {\n const isRulemetric = entry?.hooks?.some((h) => typeof h?.command === 'string' && h.command.includes('rulemetric'));\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 if (removed > 0) this.log(`[OK] Removed ${removed} legacy rulemetric session hook(s) (no longer needed)`);\n }\n\n // \u2500\u2500 2. Configure gateway + proxy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (!noProxy) {\n // Ensure mitmproxy is available\n const hasMitmproxy = this.findBinary('mitmdump');\n if (!hasMitmproxy) {\n this.log('');\n this.log('[!!] mitmproxy not found \u2014 installing...');\n const installed = this.installMitmproxy();\n if (!installed) {\n this.log('[!!] Could not install mitmproxy. Skipping proxy config.');\n this.log(' Install manually: pipx install mitmproxy');\n this.log(' Then re-run: rulemetric hooks install');\n }\n }\n\n // Generate CA cert if missing\n if (!existsSync(CERT_PATH)) {\n this.log('[..] Generating CA certificate...');\n const mitmdump = this.findBinary('mitmdump');\n if (mitmdump) {\n spawnSync(mitmdump, ['--set', 'listen_port=0', '-q'], {\n timeout: 5000,\n stdio: 'ignore',\n });\n if (existsSync(CERT_PATH)) {\n this.log(`[OK] CA certificate generated at ${CERT_PATH}`);\n } else {\n this.log('[!!] Could not generate CA cert');\n }\n }\n } else {\n this.log('[OK] CA certificate found');\n }\n\n // Trust CA cert in system store (if not already trusted)\n if (existsSync(CERT_PATH) && !this.isCACertTrusted()) {\n this.log('[..] Installing CA certificate into system trust store (requires admin)...');\n const trusted = this.trustCACert();\n if (trusted) {\n this.log('[OK] CA certificate trusted by system');\n } else {\n this.log('[!!] Could not install CA cert automatically.');\n if (process.platform === 'darwin') {\n this.log(` Run manually: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${CERT_PATH}`);\n } else if (process.platform === 'linux') {\n this.log(` Run manually: sudo cp ${CERT_PATH} /usr/local/share/ca-certificates/mitmproxy-ca.crt && sudo update-ca-certificates`);\n }\n }\n } else if (existsSync(CERT_PATH)) {\n this.log('[OK] CA certificate already trusted');\n }\n\n // Set permanent HTTPS_PROXY and NODE_EXTRA_CA_CERTS in settings.json\n // These point to the gateway (always running), not mitmproxy directly\n const env = (settings.env ?? {}) as Record<string, string>;\n env.HTTPS_PROXY = `http://localhost:${GATEWAY_PORT}`;\n // Bypass proxy for non-Anthropic traffic (Supabase auth, npm, etc.)\n env.NO_PROXY = 'localhost,127.0.0.1,*.supabase.co,*.supabase.in';\n if (existsSync(CERT_PATH)) {\n env.NODE_EXTRA_CA_CERTS = CERT_PATH;\n }\n settings.env = env;\n this.log(`[OK] HTTPS_PROXY set to gateway on :${GATEWAY_PORT} (permanent)`);\n\n // Configure Cursor proxy (if .cursor/ exists)\n const cursorProxy = writeCursorProxyConfig(projectPath, GATEWAY_PORT, CERT_PATH);\n if (cursorProxy) {\n this.log('[OK] Cursor proxy configured \u2192 .cursor/settings.json');\n }\n\n // Configure VS Code proxy for Copilot capture (workspace settings)\n const vscodeProxy = writeVSCodeProxyConfig(projectPath, GATEWAY_PORT, CERT_PATH);\n if (vscodeProxy) {\n this.log('[OK] VS Code workspace proxy configured \u2192 .vscode/settings.json');\n }\n\n // Set http.proxy in VS Code user settings (workspace-level is silently ignored\n // for http.proxy due to APPLICATION scope \u2014 microsoft/vscode#236932)\n const vscodeUserProxy = writeVSCodeUserProxyConfig(GATEWAY_PORT);\n if (vscodeUserProxy) {\n this.log('[OK] VS Code user proxy configured \u2192 ~/Library/Application Support/Code/User/settings.json');\n }\n\n // Same for Cursor (VS Code fork inherits the APPLICATION-scope restriction).\n const cursorUserProxy = writeCursorUserProxyConfig(GATEWAY_PORT);\n if (cursorUserProxy) {\n this.log('[OK] Cursor user proxy configured \u2192 ~/Library/Application Support/Cursor/User/settings.json');\n }\n\n // Set HTTPS_PROXY for all GUI apps via launchctl + LaunchAgent (macOS)\n // Copilot reads process.env.HTTPS_PROXY directly \u2014 VS Code settings alone\n // are insufficient because workspace http.proxy is ignored and extensions\n // may bypass vscode-proxy-agent (microsoft/vscode#12588)\n const macProxy = installMacOSProxyEnv(GATEWAY_PORT, CERT_PATH);\n if (macProxy) {\n this.log('[OK] HTTPS_PROXY set for GUI apps (launchctl + LaunchAgent)');\n }\n\n // Start gateway if not already running. Three possible owners (in order\n // of precedence):\n // 1. launchd (`rulemetric service install` plist) \u2014 authoritative;\n // hooks must NOT spawn a competing copy that would fight for :8787\n // 2. legacy PID-file (this same code path from a previous run)\n // 3. nothing \u2014 spawn a fresh detached process\n if (isGatewayLaunchdManaged()) {\n this.log('[OK] Gateway managed by launchd (com.rulemetric.gateway)');\n } else if (isGatewayRunning()) {\n this.log('[OK] Gateway already running');\n } else {\n try {\n const pid = spawnGateway(this.config.version);\n this.log(`[OK] Gateway started (PID ${pid})`);\n } catch (err) {\n this.log(`[!!] Could not start gateway: ${(err as Error).message}`);\n this.log(' The session-start hook will auto-start it on next Claude Code session.');\n }\n }\n }\n\n // \u2500\u2500 3. Write Cursor + Copilot hook configs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // These are independent of the proxy (they capture editor lifecycle\n // events, not LLM traffic), so they run regardless of --no-proxy. Both\n // helpers no-op when the target directory (.cursor/ or .github/)\n // doesn't exist.\n if (!isGlobal) {\n const cursorHooks = writeCursorHooks(projectPath);\n if (cursorHooks) {\n this.log(`[OK] Cursor hooks configured \u2192 ${cursorHooks}`);\n }\n\n const copilotHooks = writeCopilotHooks(projectPath);\n if (copilotHooks) {\n this.log(`[OK] Copilot Agent hooks configured \u2192 ${copilotHooks}`);\n }\n\n const antigravityHooks = writeAntigravityHooks(projectPath);\n if (antigravityHooks) {\n this.log(`[OK] Antigravity workspace hooks configured \u2192 ${antigravityHooks}`);\n }\n }\n\n // User-level Antigravity hooks at ~/.gemini/config/hooks.json. Required\n // because the GUI Antigravity process is launched once per machine and\n // covers any workspace the user opens, not just the install-time cwd.\n const antigravityUserHooks = writeAntigravityUserHooks();\n if (antigravityUserHooks) {\n this.log(`[OK] Antigravity user hooks configured \u2192 ${antigravityUserHooks}`);\n }\n\n // User-level Cursor hooks (~/.cursor/hooks.json) are required for global\n // capture because Cursor's chat traffic bypasses HTTP proxies (HTTP/2\n // multiplexed persistent connection \u2014 see cursor.com/docs/hooks). Without\n // hooks, we get zero Cursor visibility for sessions outside this project.\n // Runs in both --global and per-project modes since the user file is\n // machine-wide either way.\n const cursorUserHooks = writeCursorUserHooks();\n if (cursorUserHooks) {\n this.log(`[OK] Cursor user hooks configured \u2192 ${cursorUserHooks}`);\n }\n\n // \u2500\u2500 4. Statusline shim (rate_limits freshness) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // When --statusline-usage is set, write ~/.claude/statusline-rulemetric.sh\n // and point settings.statusLine at it. If the user already has a statusLine\n // command configured, chain it so their prompt text is preserved.\n if (flags['statusline-usage']) {\n if (isStatuslineShimInstalled(settings)) {\n this.log('[OK] Statusline shim already installed');\n } else {\n const prevCmd = readCurrentStatusLine(settings);\n writeStatuslineShim(prevCmd);\n settings.statusLine = STATUSLINE_SETTINGS_VALUE;\n if (prevCmd) {\n this.log(`[OK] Statusline shim installed \u2192 ${STATUSLINE_SETTINGS_VALUE.command} (chains to: ${prevCmd})`);\n } else {\n this.log(`[OK] Statusline shim installed \u2192 ${STATUSLINE_SETTINGS_VALUE.command}`);\n }\n this.log(' Rate-limit rings will now update on Claude Code\\'s statusline refresh cadence');\n }\n }\n\n // \u2500\u2500 5. Write Claude Code settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n mkdirSync(configDir, { recursive: true });\n writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\\n');\n\n this.log('');\n this.log('Setup complete! Next steps:');\n if (isGlobal) {\n this.log(' 1. Install always-on API: rulemetric service install');\n this.log(' 2. Start Claude Code in any project \u2014 all sessions tracked automatically');\n this.log('');\n this.log('The API service starts on login and restarts on crash.');\n } else if (!noProxy) {\n this.log(' 1. Start the capture proxy: rulemetric proxy start');\n this.log(' 2. Start Claude Code \u2014 context will be captured automatically');\n this.log('');\n this.log('The gateway routes traffic direct when the proxy is off \u2014 Claude Code always works.');\n this.log('');\n this.log('To capture traffic from other clients (Python, curl, etc.):');\n this.log(' eval \"$(rulemetric proxy env --all)\"');\n } else {\n this.log(' 1. Start Claude Code \u2014 session events will be tracked');\n this.log(' (Run without --no-proxy to also capture system prompts)');\n }\n\n // Ensure the active-org cache is populated before the user starts a\n // Claude Code session. BaseCommand fired the guarded refresh earlier\n // but may have raced with auth init or hit a transient error; this\n // explicit retry guarantees the cache reflects the user's current\n // active_org_id at the end of `hooks install`.\n await bootstrapActiveOrg();\n\n // Soft tmux capability check \u2014 live message send needs tmux on the\n // user's machine. We do not auto-install; just surface one line.\n const tmux = await detectTmux();\n this.log('');\n if (tmux.available) {\n this.log(`[OK] tmux found${tmux.version ? ` (v${tmux.version})` : ''} \u2014 live send enabled`);\n } else {\n this.log('[!!] tmux not found \u2014 live send will fall back to opening Terminal. Install with: brew install tmux');\n }\n }\n\n private findBinary(name: string): string | null {\n try {\n return execSync(`which ${name} 2>/dev/null`, { encoding: 'utf-8' }).trim() || null;\n } catch {\n return null;\n }\n }\n\n private installMitmproxy(): boolean {\n for (const [cmd, args] of [\n ['pipx', ['install', 'mitmproxy']],\n ['uv', ['tool', 'install', 'mitmproxy']],\n ['brew', ['install', 'mitmproxy']],\n ] as const) {\n if (this.findBinary(cmd)) {\n this.log(` Installing via ${cmd}...`);\n const result = spawnSync(cmd, [...args], { stdio: 'inherit' });\n if (result.status === 0) return true;\n }\n }\n return false;\n }\n\n private isCACertTrusted(): boolean {\n if (process.platform === 'darwin') {\n const result = spawnSync('security', ['verify-cert', '-c', CERT_PATH], {\n stdio: 'pipe',\n });\n return result.status === 0;\n }\n\n if (process.platform === 'linux') {\n return existsSync('/usr/local/share/ca-certificates/mitmproxy-ca.crt');\n }\n\n if (process.platform === 'win32') {\n // On Windows, check if cert is in the Root store\n const result = spawnSync('certutil', ['-verify', CERT_PATH], {\n stdio: 'pipe',\n });\n return result.status === 0;\n }\n\n return false;\n }\n\n private trustCACert(): boolean {\n if (process.platform === 'darwin') {\n const result = spawnSync('sudo', [\n 'security', 'add-trusted-cert', '-d', '-r', 'trustRoot',\n '-k', '/Library/Keychains/System.keychain', CERT_PATH,\n ], { stdio: 'inherit' });\n return result.status === 0;\n }\n\n if (process.platform === 'linux') {\n const cp = spawnSync('sudo', [\n 'cp', CERT_PATH, '/usr/local/share/ca-certificates/mitmproxy-ca.crt',\n ], { stdio: 'inherit' });\n if (cp.status !== 0) return false;\n const update = spawnSync('sudo', ['update-ca-certificates'], { stdio: 'inherit' });\n return update.status === 0;\n }\n\n if (process.platform === 'win32') {\n const result = spawnSync('certutil', [\n '-addstore', '-f', 'Root', CERT_PATH,\n ], { stdio: 'inherit' });\n return result.status === 0;\n }\n\n return false;\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { Flags } from '@oclif/core';\nimport { execFileSync, execSync, spawnSync } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { BaseCommand } from '../../base-command.js';\nimport { isGatewayRunning, spawnGateway, GATEWAY_PORT } from '../../lib/gateway-lifecycle.js';\n\n// True iff `rulemetric service install` has wired the gateway up as a launchd\n// agent. When this returns true, hooks install must NOT spawn its own\n// detached gateway \u2014 two processes fighting for :8787 means one crash-loops\n// and Claude Code's HTTPS_PROXY breaks.\nfunction isGatewayLaunchdManaged(): boolean {\n try {\n execFileSync('launchctl', ['list', 'com.rulemetric.gateway'], {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n return true;\n } catch {\n // Non-zero exit = label not loaded\n return false;\n }\n}\nimport {\n writeStatuslineShim,\n readCurrentStatusLine,\n isStatuslineShimInstalled,\n STATUSLINE_SETTINGS_VALUE,\n} from '../../lib/statusline-shim.js';\nimport {\n mergeClaudeCodeHooks,\n removeClaudeCodeHooks,\n writeCursorProxyConfig,\n writeCursorUserProxyConfig,\n writeVSCodeProxyConfig,\n installMacOSProxyEnv,\n writeVSCodeUserProxyConfig,\n writeCursorHooks,\n writeCursorUserHooks,\n writeCopilotHooks,\n writeAntigravityHooks,\n writeAntigravityUserHooks,\n} from '../../lib/hooks-config.js';\nimport { bootstrapActiveOrg } from '../../lib/active-org-refresh.js';\nimport { detectTmux } from '../../lib/detect-tmux.js';\n\nconst CERT_PATH = join(homedir(), '.mitmproxy', 'mitmproxy-ca-cert.pem');\n\nexport default class HooksInstall extends BaseCommand {\n static override description = 'Install session tracking hooks and context capture proxy for Claude Code';\n\n static override examples = [\n '<%= config.bin %> hooks install',\n '<%= config.bin %> hooks install --global',\n '<%= config.bin %> hooks install --no-proxy',\n ];\n\n static override flags = {\n 'no-proxy': Flags.boolean({\n default: false,\n description: 'Skip proxy/gateway configuration (hooks only)',\n }),\n 'project-dir': Flags.string({\n description: 'Project directory to install hooks in (defaults to cwd)',\n }),\n global: Flags.boolean({\n char: 'g',\n default: false,\n description: 'Install hooks globally (~/.claude/settings.json) so all projects are tracked',\n }),\n 'statusline-usage': Flags.boolean({\n default: false,\n description: 'Install the statusline shim so Claude Code pushes live rate_limits on every refresh (makes /usage rings update in real time without needing the proxy)',\n }),\n };\n\n async run(): Promise<void> {\n const { flags } = await this.parse(HooksInstall);\n const isGlobal = flags.global;\n const projectPath = isGlobal ? homedir() : (flags['project-dir'] ?? process.cwd());\n const configDir = join(projectPath, '.claude');\n const configPath = join(configDir, 'settings.json');\n const noProxy = flags['no-proxy'];\n\n if (isGlobal) {\n this.log(`Installing hooks globally \u2192 ${configPath}`);\n }\n\n // Read or create .claude/settings.json\n let settings: Record<string, unknown> = {};\n if (existsSync(configPath)) {\n settings = JSON.parse(readFileSync(configPath, 'utf-8'));\n }\n\n // \u2500\u2500 1. Install Claude Code session-tracking hooks (hooks-first, zero-proxy\n // capture). Strip any stale rulemetric entries first \u2014 handles old command\n // formats, preserves the user's own hooks \u2014 then merge the current canonical\n // set. Runs regardless of --no-proxy: this is the whole point of the hooks\n // path, which captures full sessions (lifecycle + SessionEnd transcript\n // reimport) without mitmproxy / CA trust / gateway. The proxy adds the\n // rendered system prompt (instruction-linking) + cross-tool capture on top.\n const refreshedHooks = removeClaudeCodeHooks(settings);\n mergeClaudeCodeHooks(settings);\n this.log(\n refreshedHooks\n ? '[OK] Refreshed RuleMetric Claude Code session hooks \u2192 .claude/settings.json'\n : '[OK] Installed RuleMetric Claude Code session hooks \u2192 .claude/settings.json',\n );\n\n // \u2500\u2500 2. Configure gateway + proxy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (!noProxy) {\n // Ensure mitmproxy is available\n const hasMitmproxy = this.findBinary('mitmdump');\n if (!hasMitmproxy) {\n this.log('');\n this.log('[!!] mitmproxy not found \u2014 installing...');\n const installed = this.installMitmproxy();\n if (!installed) {\n this.log('[!!] Could not install mitmproxy. Skipping proxy config.');\n this.log(' Install manually: pipx install mitmproxy');\n this.log(' Then re-run: rulemetric hooks install');\n }\n }\n\n // Generate CA cert if missing\n if (!existsSync(CERT_PATH)) {\n this.log('[..] Generating CA certificate...');\n const mitmdump = this.findBinary('mitmdump');\n if (mitmdump) {\n spawnSync(mitmdump, ['--set', 'listen_port=0', '-q'], {\n timeout: 5000,\n stdio: 'ignore',\n });\n if (existsSync(CERT_PATH)) {\n this.log(`[OK] CA certificate generated at ${CERT_PATH}`);\n } else {\n this.log('[!!] Could not generate CA cert');\n }\n }\n } else {\n this.log('[OK] CA certificate found');\n }\n\n // Trust CA cert in system store (if not already trusted)\n if (existsSync(CERT_PATH) && !this.isCACertTrusted()) {\n this.log('[..] Installing CA certificate into system trust store (requires admin)...');\n const trusted = this.trustCACert();\n if (trusted) {\n this.log('[OK] CA certificate trusted by system');\n } else {\n this.log('[!!] Could not install CA cert automatically.');\n if (process.platform === 'darwin') {\n this.log(` Run manually: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${CERT_PATH}`);\n } else if (process.platform === 'linux') {\n this.log(` Run manually: sudo cp ${CERT_PATH} /usr/local/share/ca-certificates/mitmproxy-ca.crt && sudo update-ca-certificates`);\n }\n }\n } else if (existsSync(CERT_PATH)) {\n this.log('[OK] CA certificate already trusted');\n }\n\n // Set permanent HTTPS_PROXY and NODE_EXTRA_CA_CERTS in settings.json\n // These point to the gateway (always running), not mitmproxy directly\n const env = (settings.env ?? {}) as Record<string, string>;\n env.HTTPS_PROXY = `http://localhost:${GATEWAY_PORT}`;\n // Bypass proxy for non-Anthropic traffic (Supabase auth, npm, etc.)\n env.NO_PROXY = 'localhost,127.0.0.1,*.supabase.co,*.supabase.in';\n if (existsSync(CERT_PATH)) {\n env.NODE_EXTRA_CA_CERTS = CERT_PATH;\n }\n settings.env = env;\n this.log(`[OK] HTTPS_PROXY set to gateway on :${GATEWAY_PORT} (permanent)`);\n\n // Configure Cursor proxy (if .cursor/ exists)\n const cursorProxy = writeCursorProxyConfig(projectPath, GATEWAY_PORT, CERT_PATH);\n if (cursorProxy) {\n this.log('[OK] Cursor proxy configured \u2192 .cursor/settings.json');\n }\n\n // Configure VS Code proxy for Copilot capture (workspace settings)\n const vscodeProxy = writeVSCodeProxyConfig(projectPath, GATEWAY_PORT, CERT_PATH);\n if (vscodeProxy) {\n this.log('[OK] VS Code workspace proxy configured \u2192 .vscode/settings.json');\n }\n\n // Set http.proxy in VS Code user settings (workspace-level is silently ignored\n // for http.proxy due to APPLICATION scope \u2014 microsoft/vscode#236932)\n const vscodeUserProxy = writeVSCodeUserProxyConfig(GATEWAY_PORT);\n if (vscodeUserProxy) {\n this.log('[OK] VS Code user proxy configured \u2192 ~/Library/Application Support/Code/User/settings.json');\n }\n\n // Same for Cursor (VS Code fork inherits the APPLICATION-scope restriction).\n const cursorUserProxy = writeCursorUserProxyConfig(GATEWAY_PORT);\n if (cursorUserProxy) {\n this.log('[OK] Cursor user proxy configured \u2192 ~/Library/Application Support/Cursor/User/settings.json');\n }\n\n // Set HTTPS_PROXY for all GUI apps via launchctl + LaunchAgent (macOS)\n // Copilot reads process.env.HTTPS_PROXY directly \u2014 VS Code settings alone\n // are insufficient because workspace http.proxy is ignored and extensions\n // may bypass vscode-proxy-agent (microsoft/vscode#12588)\n const macProxy = installMacOSProxyEnv(GATEWAY_PORT, CERT_PATH);\n if (macProxy) {\n this.log('[OK] HTTPS_PROXY set for GUI apps (launchctl + LaunchAgent)');\n }\n\n // Start gateway if not already running. Three possible owners (in order\n // of precedence):\n // 1. launchd (`rulemetric service install` plist) \u2014 authoritative;\n // hooks must NOT spawn a competing copy that would fight for :8787\n // 2. legacy PID-file (this same code path from a previous run)\n // 3. nothing \u2014 spawn a fresh detached process\n if (isGatewayLaunchdManaged()) {\n this.log('[OK] Gateway managed by launchd (com.rulemetric.gateway)');\n } else if (isGatewayRunning()) {\n this.log('[OK] Gateway already running');\n } else {\n try {\n const pid = spawnGateway(this.config.version);\n this.log(`[OK] Gateway started (PID ${pid})`);\n } catch (err) {\n this.log(`[!!] Could not start gateway: ${(err as Error).message}`);\n this.log(' The session-start hook will auto-start it on next Claude Code session.');\n }\n }\n }\n\n // \u2500\u2500 3. Write Cursor + Copilot hook configs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // These are independent of the proxy (they capture editor lifecycle\n // events, not LLM traffic), so they run regardless of --no-proxy. Both\n // helpers no-op when the target directory (.cursor/ or .github/)\n // doesn't exist.\n if (!isGlobal) {\n const cursorHooks = writeCursorHooks(projectPath);\n if (cursorHooks) {\n this.log(`[OK] Cursor hooks configured \u2192 ${cursorHooks}`);\n }\n\n const copilotHooks = writeCopilotHooks(projectPath);\n if (copilotHooks) {\n this.log(`[OK] Copilot Agent hooks configured \u2192 ${copilotHooks}`);\n }\n\n const antigravityHooks = writeAntigravityHooks(projectPath);\n if (antigravityHooks) {\n this.log(`[OK] Antigravity workspace hooks configured \u2192 ${antigravityHooks}`);\n }\n }\n\n // User-level Antigravity hooks at ~/.gemini/config/hooks.json. Required\n // because the GUI Antigravity process is launched once per machine and\n // covers any workspace the user opens, not just the install-time cwd.\n const antigravityUserHooks = writeAntigravityUserHooks();\n if (antigravityUserHooks) {\n this.log(`[OK] Antigravity user hooks configured \u2192 ${antigravityUserHooks}`);\n }\n\n // User-level Cursor hooks (~/.cursor/hooks.json) are required for global\n // capture because Cursor's chat traffic bypasses HTTP proxies (HTTP/2\n // multiplexed persistent connection \u2014 see cursor.com/docs/hooks). Without\n // hooks, we get zero Cursor visibility for sessions outside this project.\n // Runs in both --global and per-project modes since the user file is\n // machine-wide either way.\n const cursorUserHooks = writeCursorUserHooks();\n if (cursorUserHooks) {\n this.log(`[OK] Cursor user hooks configured \u2192 ${cursorUserHooks}`);\n }\n\n // \u2500\u2500 4. Statusline shim (rate_limits freshness) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // When --statusline-usage is set, write ~/.claude/statusline-rulemetric.sh\n // and point settings.statusLine at it. If the user already has a statusLine\n // command configured, chain it so their prompt text is preserved.\n if (flags['statusline-usage']) {\n if (isStatuslineShimInstalled(settings)) {\n this.log('[OK] Statusline shim already installed');\n } else {\n const prevCmd = readCurrentStatusLine(settings);\n writeStatuslineShim(prevCmd);\n settings.statusLine = STATUSLINE_SETTINGS_VALUE;\n if (prevCmd) {\n this.log(`[OK] Statusline shim installed \u2192 ${STATUSLINE_SETTINGS_VALUE.command} (chains to: ${prevCmd})`);\n } else {\n this.log(`[OK] Statusline shim installed \u2192 ${STATUSLINE_SETTINGS_VALUE.command}`);\n }\n this.log(' Rate-limit rings will now update on Claude Code\\'s statusline refresh cadence');\n }\n }\n\n // \u2500\u2500 5. Write Claude Code settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n mkdirSync(configDir, { recursive: true });\n writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\\n');\n\n this.log('');\n this.log('Setup complete! Next steps:');\n if (isGlobal) {\n this.log(' 1. Install always-on API: rulemetric service install');\n this.log(' 2. Start Claude Code in any project \u2014 all sessions tracked automatically');\n this.log('');\n this.log('The API service starts on login and restarts on crash.');\n } else if (!noProxy) {\n this.log(' 1. Start Claude Code \u2014 sessions are captured by hooks immediately');\n this.log(' 2. Start the capture proxy for full depth: rulemetric proxy start');\n this.log('');\n this.log('Hooks capture every session (lifecycle + transcript). The proxy adds the');\n this.log('rendered system prompt (instruction-effectiveness linking) + cross-tool capture.');\n this.log('');\n this.log('To capture traffic from other clients (Python, curl, etc.):');\n this.log(' eval \"$(rulemetric proxy env --all)\"');\n } else {\n // --no-proxy still installs the Claude Code session hooks above, so full\n // sessions ARE captured (lifecycle + SessionEnd transcript reimport) with\n // no mitmproxy / CA trust / gateway. Only the proxy-exclusive layer\n // (rendered system prompt \u2192 instruction-linking, cross-tool) is skipped.\n this.log(' 1. Start Claude Code \u2014 sessions are captured by the hooks just installed');\n this.log('');\n this.log('No proxy means no mitmproxy / CA cert needed. The hooks capture full');\n this.log('sessions on their own; re-run without --no-proxy to also capture system');\n this.log('prompts for instruction-effectiveness linking.');\n }\n\n // Ensure the active-org cache is populated before the user starts a\n // Claude Code session. BaseCommand fired the guarded refresh earlier\n // but may have raced with auth init or hit a transient error; this\n // explicit retry guarantees the cache reflects the user's current\n // active_org_id at the end of `hooks install`.\n await bootstrapActiveOrg();\n\n // Soft tmux capability check \u2014 live message send needs tmux on the\n // user's machine. We do not auto-install; just surface one line.\n const tmux = await detectTmux();\n this.log('');\n if (tmux.available) {\n this.log(`[OK] tmux found${tmux.version ? ` (v${tmux.version})` : ''} \u2014 live send enabled`);\n } else {\n this.log('[!!] tmux not found \u2014 live send will fall back to opening Terminal. Install with: brew install tmux');\n }\n }\n\n private findBinary(name: string): string | null {\n try {\n return execSync(`which ${name} 2>/dev/null`, { encoding: 'utf-8' }).trim() || null;\n } catch {\n return null;\n }\n }\n\n private installMitmproxy(): boolean {\n for (const [cmd, args] of [\n ['pipx', ['install', 'mitmproxy']],\n ['uv', ['tool', 'install', 'mitmproxy']],\n ['brew', ['install', 'mitmproxy']],\n ] as const) {\n if (this.findBinary(cmd)) {\n this.log(` Installing via ${cmd}...`);\n const result = spawnSync(cmd, [...args], { stdio: 'inherit' });\n if (result.status === 0) return true;\n }\n }\n return false;\n }\n\n private isCACertTrusted(): boolean {\n if (process.platform === 'darwin') {\n const result = spawnSync('security', ['verify-cert', '-c', CERT_PATH], {\n stdio: 'pipe',\n });\n return result.status === 0;\n }\n\n if (process.platform === 'linux') {\n return existsSync('/usr/local/share/ca-certificates/mitmproxy-ca.crt');\n }\n\n if (process.platform === 'win32') {\n // On Windows, check if cert is in the Root store\n const result = spawnSync('certutil', ['-verify', CERT_PATH], {\n stdio: 'pipe',\n });\n return result.status === 0;\n }\n\n return false;\n }\n\n private trustCACert(): boolean {\n if (process.platform === 'darwin') {\n const result = spawnSync('sudo', [\n 'security', 'add-trusted-cert', '-d', '-r', 'trustRoot',\n '-k', '/Library/Keychains/System.keychain', CERT_PATH,\n ], { stdio: 'inherit' });\n return result.status === 0;\n }\n\n if (process.platform === 'linux') {\n const cp = spawnSync('sudo', [\n 'cp', CERT_PATH, '/usr/local/share/ca-certificates/mitmproxy-ca.crt',\n ], { stdio: 'inherit' });\n if (cp.status !== 0) return false;\n const update = spawnSync('sudo', ['update-ca-certificates'], { stdio: 'inherit' });\n return update.status === 0;\n }\n\n if (process.platform === 'win32') {\n const result = spawnSync('certutil', [\n '-addstore', '-f', 'Root', CERT_PATH,\n ], { stdio: 'inherit' });\n return result.status === 0;\n }\n\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
removeAntigravityHooks,
|
|
3
3
|
removeAntigravityUserHooks,
|
|
4
|
+
removeClaudeCodeHooks,
|
|
4
5
|
removeCopilotHooks,
|
|
5
6
|
removeCursorHooks,
|
|
6
7
|
removeCursorUserHooks,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
removeVSCodeProxyConfig,
|
|
9
10
|
removeVSCodeUserProxyConfig,
|
|
10
11
|
uninstallMacOSProxyEnv
|
|
11
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-ANOWI23I.js";
|
|
12
13
|
import {
|
|
13
14
|
stopGateway
|
|
14
15
|
} from "../../chunk-FTJRMUID.js";
|
|
@@ -38,9 +39,8 @@ var HooksUninstall = class extends BaseCommand {
|
|
|
38
39
|
}
|
|
39
40
|
const settings = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
40
41
|
let changed = false;
|
|
41
|
-
if (settings
|
|
42
|
-
|
|
43
|
-
this.log("Removed hooks from .claude/settings.json");
|
|
42
|
+
if (removeClaudeCodeHooks(settings)) {
|
|
43
|
+
this.log("Removed RuleMetric hooks from .claude/settings.json");
|
|
44
44
|
changed = true;
|
|
45
45
|
}
|
|
46
46
|
const env = settings.env;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/commands/hooks/uninstall.ts"],
|
|
4
|
-
"sourcesContent": ["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { BaseCommand } from '../../base-command.js';\nimport { stopGateway } from '../../lib/gateway-lifecycle.js';\nimport { removeCursorHooks, removeCursorUserHooks, removeCopilotHooks, removeAntigravityHooks, removeAntigravityUserHooks, removeVSCodeProxyConfig, uninstallMacOSProxyEnv, removeVSCodeUserProxyConfig, removeCursorUserProxyConfig } from '../../lib/hooks-config.js';\n\nexport default class HooksUninstall extends BaseCommand {\n static override description = 'Remove session tracking hooks from the current project';\n\n static override examples = [\n '<%= config.bin %> hooks uninstall',\n ];\n\n async run(): Promise<void> {\n const configPath = join(process.cwd(), '.claude', 'settings.json');\n\n if (!existsSync(configPath)) {\n this.log('No .claude/settings.json found \u2014 nothing to uninstall.');\n return;\n }\n\n const settings: Record<string, unknown> = JSON.parse(readFileSync(configPath, 'utf-8'));\n\n let changed = false;\n\n // Remove hooks\n if (settings
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { BaseCommand } from '../../base-command.js';\nimport { stopGateway } from '../../lib/gateway-lifecycle.js';\nimport { removeClaudeCodeHooks, removeCursorHooks, removeCursorUserHooks, removeCopilotHooks, removeAntigravityHooks, removeAntigravityUserHooks, removeVSCodeProxyConfig, uninstallMacOSProxyEnv, removeVSCodeUserProxyConfig, removeCursorUserProxyConfig } from '../../lib/hooks-config.js';\n\nexport default class HooksUninstall extends BaseCommand {\n static override description = 'Remove session tracking hooks from the current project';\n\n static override examples = [\n '<%= config.bin %> hooks uninstall',\n ];\n\n async run(): Promise<void> {\n const configPath = join(process.cwd(), '.claude', 'settings.json');\n\n if (!existsSync(configPath)) {\n this.log('No .claude/settings.json found \u2014 nothing to uninstall.');\n return;\n }\n\n const settings: Record<string, unknown> = JSON.parse(readFileSync(configPath, 'utf-8'));\n\n let changed = false;\n\n // Remove only RuleMetric's hooks \u2014 preserve the user's own. The old wholesale\n // `delete settings.hooks` also nuked unrelated project hooks.\n if (removeClaudeCodeHooks(settings)) {\n this.log('Removed RuleMetric hooks from .claude/settings.json');\n changed = true;\n }\n\n // Remove gateway proxy env\n const env = settings.env as Record<string, string> | undefined;\n if (env?.HTTPS_PROXY || env?.NODE_EXTRA_CA_CERTS) {\n delete env.HTTPS_PROXY;\n delete env.NODE_EXTRA_CA_CERTS;\n if (Object.keys(env).length === 0) {\n delete settings.env;\n }\n this.log('Removed HTTPS_PROXY and NODE_EXTRA_CA_CERTS from .claude/settings.json');\n changed = true;\n }\n\n if (changed) {\n writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\\n');\n } else {\n this.log('No hooks or proxy config found \u2014 nothing to uninstall.');\n }\n\n // Remove Cursor hooks\n // Remove Cursor and Copilot hooks\n if (removeCursorHooks(process.cwd())) {\n this.log('Removed RuleMetric hooks from .cursor/hooks.json');\n changed = true;\n }\n if (removeCursorUserHooks()) {\n this.log('Removed RuleMetric hooks from ~/.cursor/hooks.json');\n changed = true;\n }\n\n // Remove Cursor proxy config\n const cursorSettingsPath = join(process.cwd(), '.cursor', 'settings.json');\n if (existsSync(cursorSettingsPath)) {\n try {\n const cursorSettings = JSON.parse(readFileSync(cursorSettingsPath, 'utf-8'));\n if (cursorSettings['http.proxy']?.includes('localhost')) {\n delete cursorSettings['http.proxy'];\n delete cursorSettings['http.proxyStrictSSL'];\n writeFileSync(cursorSettingsPath, JSON.stringify(cursorSettings, null, 2) + '\\n');\n this.log('Removed proxy config from .cursor/settings.json');\n changed = true;\n }\n } catch { /* not valid JSON, skip */ }\n }\n\n // Remove Copilot hooks\n if (removeCopilotHooks(process.cwd())) {\n this.log('Removed RuleMetric hooks from .github/hooks/');\n changed = true;\n }\n\n // Remove Antigravity hooks (workspace + user)\n if (removeAntigravityHooks(process.cwd())) {\n this.log('Removed RuleMetric block from .agents/hooks.json');\n changed = true;\n }\n if (removeAntigravityUserHooks()) {\n this.log('Removed RuleMetric block from ~/.gemini/config/hooks.json');\n changed = true;\n }\n\n // Remove VS Code proxy config (workspace + user settings)\n if (removeVSCodeProxyConfig(process.cwd())) {\n this.log('Removed proxy config from .vscode/settings.json');\n changed = true;\n }\n if (removeVSCodeUserProxyConfig()) {\n this.log('Removed proxy config from VS Code user settings');\n changed = true;\n }\n if (removeCursorUserProxyConfig()) {\n this.log('Removed proxy config from Cursor user settings');\n changed = true;\n }\n\n // Remove macOS LaunchAgent + launchctl env vars\n if (uninstallMacOSProxyEnv()) {\n this.log('Removed HTTPS_PROXY LaunchAgent');\n changed = true;\n }\n\n // Stop gateway\n if (stopGateway()) {\n this.log('Gateway stopped.');\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AAKrB,IAAqB,iBAArB,cAA4C,YAAY;AAAA,EACtD,OAAgB,cAAc;AAAA,EAE9B,OAAgB,WAAW;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAM,MAAqB;AACzB,UAAM,aAAa,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe;AAEjE,QAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,WAAK,IAAI,6DAAwD;AACjE;AAAA,IACF;AAEA,UAAM,WAAoC,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAEtF,QAAI,UAAU;AAId,QAAI,sBAAsB,QAAQ,GAAG;AACnC,WAAK,IAAI,qDAAqD;AAC9D,gBAAU;AAAA,IACZ;AAGA,UAAM,MAAM,SAAS;AACrB,QAAI,KAAK,eAAe,KAAK,qBAAqB;AAChD,aAAO,IAAI;AACX,aAAO,IAAI;AACX,UAAI,OAAO,KAAK,GAAG,EAAE,WAAW,GAAG;AACjC,eAAO,SAAS;AAAA,MAClB;AACA,WAAK,IAAI,wEAAwE;AACjF,gBAAU;AAAA,IACZ;AAEA,QAAI,SAAS;AACX,oBAAc,YAAY,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,IAAI;AAAA,IACpE,OAAO;AACL,WAAK,IAAI,6DAAwD;AAAA,IACnE;AAIA,QAAI,kBAAkB,QAAQ,IAAI,CAAC,GAAG;AACpC,WAAK,IAAI,kDAAkD;AAC3D,gBAAU;AAAA,IACZ;AACA,QAAI,sBAAsB,GAAG;AAC3B,WAAK,IAAI,oDAAoD;AAC7D,gBAAU;AAAA,IACZ;AAGA,UAAM,qBAAqB,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe;AACzE,QAAI,WAAW,kBAAkB,GAAG;AAClC,UAAI;AACF,cAAM,iBAAiB,KAAK,MAAM,aAAa,oBAAoB,OAAO,CAAC;AAC3E,YAAI,eAAe,YAAY,GAAG,SAAS,WAAW,GAAG;AACvD,iBAAO,eAAe,YAAY;AAClC,iBAAO,eAAe,qBAAqB;AAC3C,wBAAc,oBAAoB,KAAK,UAAU,gBAAgB,MAAM,CAAC,IAAI,IAAI;AAChF,eAAK,IAAI,iDAAiD;AAC1D,oBAAU;AAAA,QACZ;AAAA,MACF,QAAQ;AAAA,MAA6B;AAAA,IACvC;AAGA,QAAI,mBAAmB,QAAQ,IAAI,CAAC,GAAG;AACrC,WAAK,IAAI,8CAA8C;AACvD,gBAAU;AAAA,IACZ;AAGA,QAAI,uBAAuB,QAAQ,IAAI,CAAC,GAAG;AACzC,WAAK,IAAI,kDAAkD;AAC3D,gBAAU;AAAA,IACZ;AACA,QAAI,2BAA2B,GAAG;AAChC,WAAK,IAAI,2DAA2D;AACpE,gBAAU;AAAA,IACZ;AAGA,QAAI,wBAAwB,QAAQ,IAAI,CAAC,GAAG;AAC1C,WAAK,IAAI,iDAAiD;AAC1D,gBAAU;AAAA,IACZ;AACA,QAAI,4BAA4B,GAAG;AACjC,WAAK,IAAI,iDAAiD;AAC1D,gBAAU;AAAA,IACZ;AACA,QAAI,4BAA4B,GAAG;AACjC,WAAK,IAAI,gDAAgD;AACzD,gBAAU;AAAA,IACZ;AAGA,QAAI,uBAAuB,GAAG;AAC5B,WAAK,IAAI,iCAAiC;AAC1C,gBAAU;AAAA,IACZ;AAGA,QAAI,YAAY,GAAG;AACjB,WAAK,IAAI,kBAAkB;AAAA,IAC7B;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
scanAllTranscripts
|
|
3
|
-
} from "../../chunk-WA4MWYRW.js";
|
|
4
1
|
import {
|
|
5
2
|
postObservations
|
|
6
3
|
} from "../../chunk-UF73WX2J.js";
|
|
4
|
+
import {
|
|
5
|
+
scanAllTranscripts
|
|
6
|
+
} from "../../chunk-WA4MWYRW.js";
|
|
7
7
|
import {
|
|
8
8
|
BaseCommand
|
|
9
9
|
} from "../../chunk-S56LOTGQ.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
postObservations
|
|
3
|
+
} from "../../chunk-UF73WX2J.js";
|
|
1
4
|
import {
|
|
2
5
|
loadOffsets,
|
|
3
6
|
saveOffsets,
|
|
4
7
|
tailJsonl
|
|
5
8
|
} from "../../chunk-WA4MWYRW.js";
|
|
6
|
-
import {
|
|
7
|
-
postObservations
|
|
8
|
-
} from "../../chunk-UF73WX2J.js";
|
|
9
9
|
import {
|
|
10
10
|
BaseCommand
|
|
11
11
|
} from "../../chunk-S56LOTGQ.js";
|