@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.
- package/README.md +2 -2
- package/dist/src/commands/agent/doctor.d.ts.map +1 -1
- package/dist/src/commands/agent/doctor.js +26 -35
- package/dist/src/commands/agent/hook.d.ts.map +1 -1
- package/dist/src/commands/agent/hook.js +184 -180
- package/dist/src/commands/agent/setup.d.ts +0 -22
- package/dist/src/commands/agent/setup.d.ts.map +1 -1
- package/dist/src/commands/agent/setup.js +40 -62
- package/dist/src/lib/agent-config.d.ts +0 -33
- package/dist/src/lib/agent-config.d.ts.map +1 -1
- package/dist/src/lib/agent-config.js +15 -26
- package/dist/src/lib/agent-hook-state.d.ts +2 -6
- package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
- package/dist/src/lib/agent-hook-state.js +29 -43
- package/dist/src/lib/agents.d.ts +0 -63
- package/dist/src/lib/agents.d.ts.map +1 -1
- package/dist/src/lib/agents.js +19 -26
- package/dist/src/lib/auth.d.ts.map +1 -1
- package/dist/src/lib/auth.js +11 -0
- package/dist/src/lib/claude-settings.d.ts +0 -36
- package/dist/src/lib/claude-settings.d.ts.map +1 -1
- package/dist/src/lib/claude-settings.js +37 -62
- package/dist/src/lib/codex-config.d.ts +0 -79
- package/dist/src/lib/codex-config.d.ts.map +1 -1
- package/dist/src/lib/codex-config.js +74 -116
- package/dist/src/lib/declared-commits.d.ts +43 -0
- package/dist/src/lib/declared-commits.d.ts.map +1 -0
- package/dist/src/lib/declared-commits.js +114 -0
- package/dist/src/lib/declared-commits.test.d.ts +2 -0
- package/dist/src/lib/declared-commits.test.d.ts.map +1 -0
- package/dist/src/lib/declared-commits.test.js +129 -0
- package/dist/src/lib/git-remotes.d.ts +9 -0
- package/dist/src/lib/git-remotes.d.ts.map +1 -0
- package/dist/src/lib/git-remotes.js +50 -0
- package/dist/src/lib/git-remotes.test.d.ts +2 -0
- package/dist/src/lib/git-remotes.test.d.ts.map +1 -0
- package/dist/src/lib/git-remotes.test.js +52 -0
- package/dist/src/lib/opencode-config.d.ts +0 -85
- package/dist/src/lib/opencode-config.d.ts.map +1 -1
- package/dist/src/lib/opencode-config.js +72 -112
- package/package.json +5 -5
package/dist/src/lib/agents.js
CHANGED
|
@@ -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
|
-
//
|
|
117
|
-
//
|
|
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
|
-
//
|
|
126
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
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,
|
|
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"}
|
package/dist/src/lib/auth.js
CHANGED
|
@@ -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":"
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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":"
|
|
1
|
+
{"version":3,"file":"codex-config.d.ts","sourceRoot":"","sources":["../../../src/lib/codex-config.ts"],"names":[],"mappings":"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"}
|