@blastin-dev/clocktopus-cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/src/commands/agent/doctor.d.ts.map +1 -1
  3. package/dist/src/commands/agent/doctor.js +26 -35
  4. package/dist/src/commands/agent/hook.d.ts.map +1 -1
  5. package/dist/src/commands/agent/hook.js +184 -180
  6. package/dist/src/commands/agent/setup.d.ts +0 -22
  7. package/dist/src/commands/agent/setup.d.ts.map +1 -1
  8. package/dist/src/commands/agent/setup.js +40 -62
  9. package/dist/src/lib/agent-config.d.ts +0 -33
  10. package/dist/src/lib/agent-config.d.ts.map +1 -1
  11. package/dist/src/lib/agent-config.js +15 -26
  12. package/dist/src/lib/agent-hook-state.d.ts +2 -6
  13. package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
  14. package/dist/src/lib/agent-hook-state.js +29 -43
  15. package/dist/src/lib/agents.d.ts +0 -63
  16. package/dist/src/lib/agents.d.ts.map +1 -1
  17. package/dist/src/lib/agents.js +19 -26
  18. package/dist/src/lib/auth.d.ts.map +1 -1
  19. package/dist/src/lib/auth.js +11 -0
  20. package/dist/src/lib/claude-settings.d.ts +0 -36
  21. package/dist/src/lib/claude-settings.d.ts.map +1 -1
  22. package/dist/src/lib/claude-settings.js +37 -62
  23. package/dist/src/lib/codex-config.d.ts +0 -79
  24. package/dist/src/lib/codex-config.d.ts.map +1 -1
  25. package/dist/src/lib/codex-config.js +74 -116
  26. package/dist/src/lib/declared-commits.d.ts +43 -0
  27. package/dist/src/lib/declared-commits.d.ts.map +1 -0
  28. package/dist/src/lib/declared-commits.js +114 -0
  29. package/dist/src/lib/declared-commits.test.d.ts +2 -0
  30. package/dist/src/lib/declared-commits.test.d.ts.map +1 -0
  31. package/dist/src/lib/declared-commits.test.js +129 -0
  32. package/dist/src/lib/git-remotes.d.ts +9 -0
  33. package/dist/src/lib/git-remotes.d.ts.map +1 -0
  34. package/dist/src/lib/git-remotes.js +50 -0
  35. package/dist/src/lib/git-remotes.test.d.ts +2 -0
  36. package/dist/src/lib/git-remotes.test.d.ts.map +1 -0
  37. package/dist/src/lib/git-remotes.test.js +52 -0
  38. package/dist/src/lib/opencode-config.d.ts +0 -85
  39. package/dist/src/lib/opencode-config.d.ts.map +1 -1
  40. package/dist/src/lib/opencode-config.js +72 -112
  41. package/package.json +5 -5
@@ -112,20 +112,17 @@ const codexAdapter = {
112
112
  return { removed };
113
113
  },
114
114
  pendingActions() {
115
- // Codex refuses to run a hooks.json it has not been shown, and there is
116
- // no way to approve it from here — the prompt is part of the TUI. Left
117
- // unsaid, this looks exactly like a broken install: telemetry arrives,
118
- // repository context never does.
115
+ // Codex refuses to run a hooks.json it has not been shown, and there is no way to
116
+ // approve it from here — the prompt is part of the TUI. Left unsaid, this looks
117
+ // exactly like a broken install: telemetry arrives, repository context never does.
119
118
  if (!readCodexHooks().exists)
120
119
  return [];
121
120
  const { untrusted } = readCodexHookTrust();
122
121
  if (untrusted.length === 0)
123
122
  return [];
124
- // Says what is missing, never what is already fine. Trust is keyed on a
125
- // hash of the hook entry, so rewriting hooks.json invalidates every
126
- // approval it already had — `trusted` can be stale the moment setup
127
- // runs, and promising the user that half of it is done would be a lie
128
- // as often as not.
123
+ // Says what is missing, never what is already fine. Trust is keyed on a hash of the
124
+ // hook entry, so rewriting hooks.json invalidates every approval it had — `trusted`
125
+ // can be stale the moment setup runs.
129
126
  const missing = untrusted.length === HOOK_EVENT_COUNT
130
127
  ? "its hooks"
131
128
  : `its ${untrusted.join(" and ")} hook`;
@@ -141,12 +138,11 @@ const opencodeAdapter = {
141
138
  label: "OpenCode",
142
139
  provider: "opencode",
143
140
  binary: "opencode",
144
- // The plugin runs inside OpenCode's own event loop and does not await the
145
- // work it starts, so the hook is already off the critical path by the
146
- // time it reaches the CLI. Re-spawning would only add a process.
141
+ // The plugin runs inside OpenCode's own event loop and does not await the work it
142
+ // starts, so the hook is already off the critical path by the time it reaches the CLI.
147
143
  hostRunsHooksAsync: true,
148
- // `session.idle` is the closest thing OpenCode has to a session end, and
149
- // it fires after every turn rather than once.
144
+ // `session.idle` is the closest thing OpenCode has to a session end, and it fires
145
+ // after every turn rather than once.
150
146
  hostRepeatsSessionEnd: true,
151
147
  paths: () => [opencodePluginPath()],
152
148
  read() {
@@ -154,9 +150,8 @@ const opencodeAdapter = {
154
150
  return {
155
151
  token: plugin.config?.token ?? null,
156
152
  endpoint: plugin.config?.endpoint ?? null,
157
- // Both events come from the one plugin file, so they are installed
158
- // together or not at all — unlike the hook agents, where each event is
159
- // a separate entry that can go missing on its own.
153
+ // Both events come from the one plugin file, so they are installed together or not at
154
+ // all — unlike the hook agents, where each event can go missing on its own.
160
155
  hookCommands: plugin.config
161
156
  ? {
162
157
  SessionStart: plugin.config.hookArgv.join(" "),
@@ -195,13 +190,11 @@ export const AGENTS = [
195
190
  codexAdapter,
196
191
  opencodeAdapter,
197
192
  ];
198
- /**
199
- * What is on this machine, and what is already wired up.
200
- *
201
- * Detection is by `--version` rather than by config directory: `~/.claude`
202
- * and `~/.codex` both outlive an uninstall, so a stale directory would
203
- * offer to configure an agent that is no longer there.
204
- */
193
+ // What is on this machine, and what is already wired up.
194
+ //
195
+ // Detection is by `--version` rather than by config directory: `~/.claude` and
196
+ // `~/.codex` both outlive an uninstall, so a stale directory would offer to configure
197
+ // an agent that is no longer there.
205
198
  export function surveyAgents() {
206
199
  return AGENTS.map((agent) => {
207
200
  const version = detectVersion(agent.binary);
@@ -232,8 +225,8 @@ function detectVersion(binary) {
232
225
  stdio: ["ignore", "pipe", "ignore"],
233
226
  timeout: 5000,
234
227
  });
235
- // Both CLIs print a line with the version somewhere in it; the number is
236
- // the only part worth showing, and neither format is worth parsing hard.
228
+ // Both CLIs print a line with the version somewhere in it; the number is the only
229
+ // part worth showing.
237
230
  return (/\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim().split("\n")[0] ?? null);
238
231
  }
239
232
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/lib/auth.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4BnE;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA8CrB;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/lib/auth.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4BnE;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA0DrB;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
@@ -38,6 +38,17 @@ export async function pollForToken(deviceCode, interval) {
38
38
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
39
39
  }),
40
40
  });
41
+ // The web app rate-limits `/device/token` on top of the interval this loop
42
+ // already respects, and a 429 carries Better Auth's own body — no `error`
43
+ // field — so it used to fall through to "Unexpected response" and kill a
44
+ // login that was doing nothing wrong. Back off and keep waiting instead.
45
+ if (response.status === 429) {
46
+ const retryAfter = Number(response.headers.get("x-retry-after"));
47
+ return {
48
+ status: "slow_down",
49
+ newInterval: retryAfter > 0 ? retryAfter + interval : interval + 5,
50
+ };
51
+ }
41
52
  const data = (await response.json());
42
53
  if ("access_token" in data) {
43
54
  return { status: "success", accessToken: data.access_token };
@@ -1,36 +1,7 @@
1
- /**
2
- * Reads and edits `~/.claude/settings.json` on the user's behalf.
3
- *
4
- * Two rules govern everything here, both of them about not destroying a file
5
- * we do not own:
6
- *
7
- * 1. **Never write over JSON we could not parse.** A malformed settings file
8
- * is far more likely to be a half-finished edit than something to
9
- * overwrite, and overwriting it would lose the user's own hooks,
10
- * permissions and MCP servers. Parse failures raise instead.
11
- * 2. **Only ever touch keys we put there.** Merging into `env` and appending
12
- * to `hooks` leaves everything else untouched, and removal matches our
13
- * own hook command rather than clearing the arrays.
14
- */
15
1
  export declare const SETTINGS_FILENAME = "settings.json";
16
2
  /** Env keys `clocktopus agent setup` owns — and the only ones it removes. */
17
3
  export declare const TELEMETRY_ENV_KEYS: readonly ["CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_METRICS_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_METRICS_INCLUDE_SESSION_ID", "OTEL_METRIC_EXPORT_INTERVAL", "CLOCKTOPUS_INGEST_TOKEN", "CLOCKTOPUS_OTEL_ENDPOINT"];
18
- /**
19
- * How often Claude Code's exporter ships metrics.
20
- *
21
- * Also the resolution of every "last export received" answer the status
22
- * command can give — a session that started 30s ago has genuinely not
23
- * exported yet, which is why status treats silence under one interval as
24
- * "waiting" rather than "broken".
25
- */
26
4
  export declare const METRIC_EXPORT_INTERVAL_MS = 60000;
27
- /**
28
- * Seconds Claude Code will wait for the hook before giving up on it.
29
- *
30
- * Comfortably above the hook's own 4s request timeout, so a slow network
31
- * produces the hook's own recorded failure — which `agent doctor` can read
32
- * back — rather than a kill from the host, which leaves no trace anywhere.
33
- */
34
5
  export declare const HOOK_TIMEOUT_SECONDS = 10;
35
6
  export type ClaudeSettings = Record<string, unknown>;
36
7
  export declare class SettingsParseError extends Error {
@@ -45,13 +16,6 @@ export declare function readSettings(path?: string): {
45
16
  modifiedAt: Date | null;
46
17
  settings: ClaudeSettings;
47
18
  };
48
- /**
49
- * Writes settings, keeping a one-deep backup of what was there before.
50
- *
51
- * The rename is what makes it atomic: a crash midway leaves either the old
52
- * file or the new one, never a truncated file that Claude Code would refuse
53
- * to start with.
54
- */
55
19
  export declare function writeSettings(settings: ClaudeSettings, path?: string): {
56
20
  backupPath: string | null;
57
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"claude-settings.d.ts","sourceRoot":"","sources":["../../../src/lib/claude-settings.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,yRAUrB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASrD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAMzC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,YAAY,CAAC,IAAI,SAAiB,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,cAAc,CAAC;CAC1B,CA6BA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,cAAc,EACxB,IAAI,SAAiB,GACpB;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAiB/B;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBzB;AAED,QAAA,MAAM,WAAW,yCAA0C,CAAC;AAqC5D,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9D,cAAc,CA6ChB;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG;IACjE,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;CACvB,CAmCA;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,CAAC,IAAI,SAAiB,GAAG;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACrE,CAmCA"}
1
+ {"version":3,"file":"claude-settings.d.ts","sourceRoot":"","sources":["../../../src/lib/claude-settings.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,yRAUrB,CAAC;AAKX,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAMhD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASrD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAMzC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,YAAY,CAAC,IAAI,SAAiB,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,cAAc,CAAC;CAC1B,CA6BA;AAKD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,cAAc,EACxB,IAAI,SAAiB,GACpB;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAiB/B;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkBzB;AAED,QAAA,MAAM,WAAW,yCAA0C,CAAC;AAgC5D,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9D,cAAc,CA0ChB;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG;IACjE,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;CACvB,CAmCA;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,CAAC,IAAI,SAAiB,GAAG;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACrE,CAmCA"}
@@ -1,20 +1,16 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- /**
5
- * Reads and edits `~/.claude/settings.json` on the user's behalf.
6
- *
7
- * Two rules govern everything here, both of them about not destroying a file
8
- * we do not own:
9
- *
10
- * 1. **Never write over JSON we could not parse.** A malformed settings file
11
- * is far more likely to be a half-finished edit than something to
12
- * overwrite, and overwriting it would lose the user's own hooks,
13
- * permissions and MCP servers. Parse failures raise instead.
14
- * 2. **Only ever touch keys we put there.** Merging into `env` and appending
15
- * to `hooks` leaves everything else untouched, and removal matches our
16
- * own hook command rather than clearing the arrays.
17
- */
4
+ // Reads and edits `~/.claude/settings.json` on the user's behalf.
5
+ //
6
+ // Two rules govern everything here, both about not destroying a file we do not own:
7
+ //
8
+ // 1. Never write over JSON we could not parse. A malformed settings file is far more
9
+ // likely to be a half-finished edit than something to overwrite, and overwriting
10
+ // would lose the user's own hooks, permissions and MCP servers.
11
+ // 2. Only ever touch keys we put there. Merging into `env` and appending to `hooks`
12
+ // leaves everything else alone, and removal matches our own hook command rather
13
+ // than clearing the arrays.
18
14
  export const SETTINGS_FILENAME = "settings.json";
19
15
  /** Env keys `clocktopus agent setup` owns — and the only ones it removes. */
20
16
  export const TELEMETRY_ENV_KEYS = [
@@ -28,22 +24,14 @@ export const TELEMETRY_ENV_KEYS = [
28
24
  "CLOCKTOPUS_INGEST_TOKEN",
29
25
  "CLOCKTOPUS_OTEL_ENDPOINT",
30
26
  ];
31
- /**
32
- * How often Claude Code's exporter ships metrics.
33
- *
34
- * Also the resolution of every "last export received" answer the status
35
- * command can give — a session that started 30s ago has genuinely not
36
- * exported yet, which is why status treats silence under one interval as
37
- * "waiting" rather than "broken".
38
- */
27
+ // How often Claude Code's exporter ships metrics — and the resolution of every "last
28
+ // export received" answer status can give, which is why silence under one interval
29
+ // reads as "waiting" rather than "broken".
39
30
  export const METRIC_EXPORT_INTERVAL_MS = 60_000;
40
- /**
41
- * Seconds Claude Code will wait for the hook before giving up on it.
42
- *
43
- * Comfortably above the hook's own 4s request timeout, so a slow network
44
- * produces the hook's own recorded failure — which `agent doctor` can read
45
- * back — rather than a kill from the host, which leaves no trace anywhere.
46
- */
31
+ // Seconds Claude Code will wait for the hook. Comfortably above the hook's own 4s
32
+ // request timeout, so a slow network produces the hook's own recorded failure which
33
+ // `agent doctor` can read back — rather than a kill from the host, which leaves no
34
+ // trace.
47
35
  export const HOOK_TIMEOUT_SECONDS = 10;
48
36
  export class SettingsParseError extends Error {
49
37
  path;
@@ -54,8 +42,8 @@ export class SettingsParseError extends Error {
54
42
  }
55
43
  }
56
44
  export function claudeConfigDir() {
57
- // Claude Code honours CLAUDE_CONFIG_DIR; following it means setup writes
58
- // where that installation actually reads.
45
+ // Claude Code honours CLAUDE_CONFIG_DIR; following it means setup writes where that
46
+ // installation actually reads.
59
47
  return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
60
48
  }
61
49
  export function settingsPath() {
@@ -67,8 +55,8 @@ export function readSettings(path = settingsPath()) {
67
55
  }
68
56
  const raw = readFileSync(path, "utf8");
69
57
  const modifiedAt = statSync(path).mtime;
70
- // An empty file is a normal state (some installers touch it) and is safe
71
- // to treat as an empty object; anything else that fails to parse is not.
58
+ // An empty file is a normal state (some installers touch it) and is safe to treat as
59
+ // an empty object; anything else that fails to parse is not.
72
60
  if (raw.trim() === "") {
73
61
  return { path, exists: true, modifiedAt, settings: {} };
74
62
  }
@@ -90,13 +78,9 @@ export function readSettings(path = settingsPath()) {
90
78
  throw new SettingsParseError(path);
91
79
  }
92
80
  }
93
- /**
94
- * Writes settings, keeping a one-deep backup of what was there before.
95
- *
96
- * The rename is what makes it atomic: a crash midway leaves either the old
97
- * file or the new one, never a truncated file that Claude Code would refuse
98
- * to start with.
99
- */
81
+ // Writes settings, keeping a one-deep backup. The rename is what makes it atomic: a
82
+ // crash midway leaves either the old file or the new one, never a truncated file
83
+ // Claude Code would refuse to start with.
100
84
  export function writeSettings(settings, path = settingsPath()) {
101
85
  mkdirSync(dirname(path), { recursive: true });
102
86
  let backupPath = null;
@@ -117,13 +101,12 @@ export function buildTelemetryEnv(input) {
117
101
  return {
118
102
  CLAUDE_CODE_ENABLE_TELEMETRY: "1",
119
103
  OTEL_METRICS_EXPORTER: "otlp",
120
- // The receiver answers protobuf with an explicit 415 rather than a
121
- // parse error, but only JSON actually works.
104
+ // The receiver answers protobuf with an explicit 415, but only JSON actually works.
122
105
  OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
123
106
  OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
124
107
  OTEL_EXPORTER_OTLP_HEADERS: `Authorization=Bearer ${input.token}`,
125
- // Without the session id the metric stream cannot be joined to the hook
126
- // stream, and every session loses its repository and branch.
108
+ // Without the session id the metric stream cannot be joined to the hook stream, and
109
+ // every session loses its repository and branch.
127
110
  OTEL_METRICS_INCLUDE_SESSION_ID: "true",
128
111
  OTEL_METRIC_EXPORT_INTERVAL: String(METRIC_EXPORT_INTERVAL_MS),
129
112
  // The hook's own channel. Same token, different transport.
@@ -132,15 +115,10 @@ export function buildTelemetryEnv(input) {
132
115
  };
133
116
  }
134
117
  const HOOK_EVENTS = ["SessionStart", "SessionEnd"];
135
- /**
136
- * Recognises a hook entry as ours, across every spelling we have shipped.
137
- *
138
- * The `claude-hook.mjs` clause matters for upgrades: before this command
139
- * existed the hook was a script inside a checkout of the Clocktopus repo,
140
- * and anyone who set that up by hand still has it. Failing to recognise it
141
- * would leave it installed next to the new one and POST every SessionStart
142
- * twice.
143
- */
118
+ // Recognises a hook entry as ours, across every spelling we have shipped. The
119
+ // `claude-hook.mjs` clause matters for upgrades: before this command existed the hook
120
+ // was a script inside a checkout of this repo, and anyone who set that up by hand
121
+ // still has it. Failing to recognise it would POST every SessionStart twice.
144
122
  function isClocktopusHook(entry) {
145
123
  const command = typeof entry.command === "string" ? entry.command : "";
146
124
  if (/claude-hook\.mjs/.test(command))
@@ -157,8 +135,8 @@ function withoutOurHooks(matchers) {
157
135
  ...matcher,
158
136
  hooks: (matcher.hooks ?? []).filter((entry) => !isClocktopusHook(entry)),
159
137
  }))
160
- // A matcher group that only ever held our hook is ours to remove; one
161
- // that still has entries belongs to the user and stays.
138
+ // A matcher group that only ever held our hook is ours to remove; one that still has
139
+ // entries belongs to the user and stays.
162
140
  .filter((matcher) => (matcher.hooks ?? []).length > 0));
163
141
  }
164
142
  export function applyTelemetrySettings(settings, input) {
@@ -174,9 +152,8 @@ export function applyTelemetrySettings(settings, input) {
174
152
  : {};
175
153
  const hooks = { ...existingHooks };
176
154
  for (const event of HOOK_EVENTS) {
177
- // Remove-then-append rather than append: re-running setup after a
178
- // reinstall must not leave two hooks firing per session, which would
179
- // double every SessionStart POST.
155
+ // Remove-then-append rather than append: re-running setup after a reinstall must not
156
+ // leave two hooks firing per session, which would double every SessionStart POST.
180
157
  hooks[event] = [
181
158
  ...withoutOurHooks(asMatchers(existingHooks[event])),
182
159
  {
@@ -184,10 +161,8 @@ export function applyTelemetrySettings(settings, input) {
184
161
  {
185
162
  type: "command",
186
163
  command: input.hookCommand,
187
- // Both fields are about not making the user wait on telemetry.
188
- // The hook does network I/O its own POST, plus a sweep of
189
- // abandoned sessions at SessionStart — and running it inline
190
- // would add that latency to the start of every session.
164
+ // Both fields are about not making the user wait on telemetry: the hook does network
165
+ // I/O, and running it inline would add that latency to the start of every session.
191
166
  timeout: HOOK_TIMEOUT_SECONDS,
192
167
  async: true,
193
168
  },
@@ -1,48 +1,6 @@
1
- /**
2
- * Reads and edits Codex CLI's configuration on the user's behalf.
3
- *
4
- * Codex splits what Claude Code keeps in one file: telemetry goes in
5
- * `~/.codex/config.toml` under `[otel]`, hooks go in a separate
6
- * `~/.codex/hooks.json`. Both are edited here under the same two rules as
7
- * `claude-settings.ts` — never write over something we could not parse, and
8
- * only ever touch keys we put there — plus a third that TOML forces on us:
9
- *
10
- * **Splice, don't re-serialise.** A round trip through a TOML parser throws
11
- * away every comment and blank line in the file. `config.toml` is a file
12
- * people hand-write and annotate, so only the `[otel]` region is replaced
13
- * textually; everything else survives byte for byte. `writeCodexConfig`
14
- * re-parses the spliced result and refuses to write if anything outside
15
- * `[otel]` moved, which is what makes the textual approach safe.
16
- *
17
- * ## What Codex does with these, and why it matters here
18
- *
19
- * - `otel.exporter` is the **log** exporter, and Codex POSTs to the URL
20
- * verbatim — it does not append `/v1/logs` the way the OTel SDK does. The
21
- * path is therefore part of the configured endpoint, and
22
- * `readCodexTelemetry` strips it back off to recover the receiver base.
23
- * - Codex will not run a `hooks.json` it has not been shown: the first
24
- * session after this file changes prompts "Hooks need review" and
25
- * persists the answer under `[hooks.state]`. Setup cannot do that for the
26
- * user, so it tells them instead — see `setup.ts`.
27
- */
28
1
  export declare const CODEX_CONFIG_FILENAME = "config.toml";
29
2
  export declare const CODEX_HOOKS_FILENAME = "hooks.json";
30
- /**
31
- * The receiver path Codex's log exporter is pointed at.
32
- *
33
- * Load-bearing in both directions: written onto the endpoint because Codex
34
- * sends to the literal URL, and stripped when reading because the hook and
35
- * `/v1/verify` need the base.
36
- */
37
3
  export declare const CODEX_LOGS_PATH = "/v1/logs";
38
- /**
39
- * Seconds Codex will wait for each hook.
40
- *
41
- * SessionEnd is 3 rather than 10 because Codex hard-clamps it to 3 and
42
- * prints `warning: clamping SessionEnd hook timeout to 3s` on every single
43
- * session start otherwise — a permanent warning about a value we chose is
44
- * worse than the shorter budget, and the hook's own request timeout is 4s.
45
- */
46
4
  export declare const CODEX_HOOK_TIMEOUT_SECONDS: {
47
5
  SessionStart: number;
48
6
  SessionEnd: number;
@@ -70,23 +28,7 @@ export declare function buildCodexOtel(input: {
70
28
  token: string;
71
29
  endpoint: string;
72
30
  }): TomlTable;
73
- /**
74
- * Replaces the `[otel]` region of a TOML document, preserving everything else.
75
- *
76
- * Returns the new text. Region detection is deliberately dumb — table
77
- * headers only — and `writeCodexConfig` verifies the result by re-parsing,
78
- * so a document this misreads is refused rather than mangled.
79
- */
80
31
  export declare function spliceOtelSection(text: string, otel: TomlTable | null): string;
81
- /**
82
- * Writes config.toml, keeping a one-deep backup and verifying the splice.
83
- *
84
- * The verification is the point: it re-parses what is about to be written
85
- * and compares every top-level key *except* `otel` against what was there
86
- * before. If the textual splice disturbed anything else — an exotic layout
87
- * the region scanner misread — the write is refused with the user's file
88
- * still intact.
89
- */
90
32
  export declare function writeCodexConfig(input: {
91
33
  path?: string;
92
34
  previousText: string;
@@ -137,27 +79,6 @@ export declare function removeCodexHooks(file: Record<string, unknown>): {
137
79
  export declare function writeCodexHooks(file: Record<string, unknown>, path?: string): {
138
80
  backupPath: string | null;
139
81
  };
140
- /**
141
- * Which of our hooks Codex has been shown and told to run.
142
- *
143
- * Trust is recorded **per hook entry**, not per file:
144
- *
145
- * ```toml
146
- * [hooks.state."/home/you/.codex/hooks.json:session_end:0:0"]
147
- * trusted_hash = "sha256:…"
148
- * ```
149
- *
150
- * The key is `<path>:<event>:<group index>:<hook index>`, with the event in
151
- * snake_case. Per-entry granularity is why this reports each event rather
152
- * than a single boolean: partial trust is a real state, and the one that
153
- * matters. A machine whose `SessionEnd` is trusted and whose `SessionStart`
154
- * is not records spend with no repository attached to it — which is exactly
155
- * what an `async: true` SessionStart used to produce on Codex 0.147, since
156
- * the hook was skipped before it could ever be offered for approval.
157
- *
158
- * The hash is Codex's own and is not recomputed here; changing `hooks.json`
159
- * invalidates it and Codex re-prompts, which is the behaviour we want.
160
- */
161
82
  export declare function readCodexHookTrust(path?: string): {
162
83
  trusted: HookEvent[];
163
84
  untrusted: HookEvent[];
@@ -1 +1 @@
1
- {"version":3,"file":"codex-config.d.ts","sourceRoot":"","sources":["../../../src/lib/codex-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"codex-config.d.ts","sourceRoot":"","sources":["../../../src/lib/codex-config.ts"],"names":[],"mappings":"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"}