@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
@@ -1,25 +1,3 @@
1
- /**
2
- * Configures this machine's coding agents to report spend to Clocktopus.
3
- *
4
- * One command, however many agents are installed. It looks for each
5
- * supported CLI on PATH and configures what it finds — silently when
6
- * there is only one, after a prompt when there are several, and from
7
- * `--agent` when there is neither a person nor a terminal to ask.
8
- *
9
- * Everything for a given agent lands in that agent's own config, and
10
- * nowhere else. A single source of truth per agent is not a tidiness
11
- * preference: it is what makes `agent doctor` able to say which value is in
12
- * force. Configuration spread over `.envrc`, a shell profile and a CLI
13
- * config file can be reported on but never resolved, and this project
14
- * already lost real spend to exactly that (one session split across two
15
- * accounts, silently, because two files disagreed about the token).
16
- *
17
- * Idempotent by default. Re-running repairs the hooks and refreshes the
18
- * endpoint while keeping the existing token, so the common case — "did my
19
- * setup drift?" — costs nothing, and adding a newly-installed agent later
20
- * reuses the token the first one already has. `--force` mints a
21
- * replacement instead.
22
- */
23
1
  export declare function setupCommand(options: {
24
2
  name?: string;
25
3
  force?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/setup.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8LhB"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/setup.ts"],"names":[],"mappings":"AAsCA,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0LhB"}
@@ -10,28 +10,21 @@ import { isLoggedIn, setAgentConfig } from "../../lib/config.js";
10
10
  import { labelled } from "../../lib/format.js";
11
11
  import { fetchRepoStatus, renderRepoBlock } from "../../lib/repo-guidance.js";
12
12
  import { AgentEndpointSchema, IngestTokenSchema, UserSchema, } from "../../lib/validators.js";
13
- /**
14
- * Configures this machine's coding agents to report spend to Clocktopus.
15
- *
16
- * One command, however many agents are installed. It looks for each
17
- * supported CLI on PATH and configures what it finds silently when
18
- * there is only one, after a prompt when there are several, and from
19
- * `--agent` when there is neither a person nor a terminal to ask.
20
- *
21
- * Everything for a given agent lands in that agent's own config, and
22
- * nowhere else. A single source of truth per agent is not a tidiness
23
- * preference: it is what makes `agent doctor` able to say which value is in
24
- * force. Configuration spread over `.envrc`, a shell profile and a CLI
25
- * config file can be reported on but never resolved, and this project
26
- * already lost real spend to exactly that (one session split across two
27
- * accounts, silently, because two files disagreed about the token).
28
- *
29
- * Idempotent by default. Re-running repairs the hooks and refreshes the
30
- * endpoint while keeping the existing token, so the common case — "did my
31
- * setup drift?" — costs nothing, and adding a newly-installed agent later
32
- * reuses the token the first one already has. `--force` mints a
33
- * replacement instead.
34
- */
13
+ // Configures this machine's coding agents to report spend to Clocktopus.
14
+ //
15
+ // One command, however many agents are installed: it configures what it finds on PATH —
16
+ // silently when there is only one, after a prompt when there are several, and from
17
+ // `--agent` when there is neither a person nor a terminal to ask.
18
+ //
19
+ // Everything for a given agent lands in that agent's own config and nowhere else. A
20
+ // single source of truth per agent is what makes `agent doctor` able to say which value
21
+ // is in force; configuration spread over `.envrc`, a shell profile and a CLI config can
22
+ // be reported on but never resolved, and this project already lost real spend to exactly
23
+ // that.
24
+ //
25
+ // Idempotent by default: re-running repairs the hooks and refreshes the endpoint while
26
+ // keeping the existing token, so adding a newly-installed agent later reuses it.
27
+ // `--force` mints a replacement.
35
28
  export async function setupCommand(options) {
36
29
  if (!isLoggedIn()) {
37
30
  console.error("Not logged in. Run 'clocktopus login' first.");
@@ -72,11 +65,9 @@ export async function setupCommand(options) {
72
65
  reportApiFailure(error, "Failed to reach Clocktopus");
73
66
  return;
74
67
  }
75
- // One machine, one token, however many agents. Reuse before minting:
76
- // every mint leaves another live token on the account, and a machine that
77
- // already has a working one has nothing to gain from a second — the
78
- // reason to re-run setup is almost always a broken hook path or a newly
79
- // installed agent, not a bad token.
68
+ // One machine, one token, however many agents. Every mint leaves another live token on
69
+ // the account, and the reason to re-run setup is almost always a broken hook path or a
70
+ // newly installed agent, not a bad token.
80
71
  let token = options.force ? null : findExistingToken(survey);
81
72
  let tokenId = null;
82
73
  let minted = false;
@@ -121,8 +112,7 @@ export async function setupCommand(options) {
121
112
  });
122
113
  }
123
114
  catch (error) {
124
- // One agent failing must not strand the others half-configured, so
125
- // the loop continues and the failure is reported in the summary.
115
+ // One agent failing must not strand the others half-configured.
126
116
  console.error(`✗ ${entry.agent.label}: ${error instanceof Error ? error.message : "unknown error"}`);
127
117
  }
128
118
  }
@@ -155,10 +145,10 @@ export async function setupCommand(options) {
155
145
  console.log(check.ok
156
146
  ? "✓ The receiver accepted this token."
157
147
  : `✗ The receiver did not accept this token (${describeCheck(check)}).`);
158
- // Configuring this machine is only half the job: sessions will arrive,
159
- // but whether they mean anything depends on the repository being attached
160
- // to a project and on commits reaching us. Both are invisible from here,
161
- // and staying quiet about them is how spend ends up stranded.
148
+ // Configuring this machine is only half the job: sessions will arrive, but whether they
149
+ // mean anything depends on the repository being attached to a project and on commits
150
+ // reaching us. Both are invisible from here, and staying quiet is how spend ends up
151
+ // stranded.
162
152
  const repoStatus = await fetchRepoStatus();
163
153
  if (repoStatus) {
164
154
  for (const line of renderRepoBlock(repoStatus))
@@ -167,12 +157,11 @@ export async function setupCommand(options) {
167
157
  for (const warning of collectWarnings(configured)) {
168
158
  console.log(`\n${warning}`);
169
159
  }
170
- // The single most common reason a correct setup appears dead. Both the
171
- // exporter and the hook read their configuration once, when the process
172
- // starts, so a session already running will never see any of this.
160
+ // The single most common reason a correct setup appears dead: both the exporter and the
161
+ // hook read their configuration once, at process start.
173
162
  console.log(`\nRestart ${labelList(configured.map((c) => c.entry.agent.label))} — the exporter and the hook\nread their configuration once, at process start.`);
174
- // Agent-specific steps a person still has to take, most importantly
175
- // Codex's hook trust prompt, which no command can answer for them.
163
+ // Agent-specific steps a person still has to take, most importantly Codex's hook trust
164
+ // prompt, which no command can answer for them.
176
165
  for (const { entry } of configured) {
177
166
  for (const action of entry.agent.pendingActions()) {
178
167
  console.log(`\n→ ${entry.agent.label}: ${action}`);
@@ -180,15 +169,10 @@ export async function setupCommand(options) {
180
169
  }
181
170
  console.log("\nThen confirm it end to end: clocktopus agent status");
182
171
  }
183
- /**
184
- * Decides which agents to configure, asking only when the answer is
185
- * genuinely ambiguous.
186
- *
187
- * The precedence is about respecting what the caller already told us:
188
- * `--agent` is an explicit instruction, a lone install has no decision in
189
- * it, and a non-interactive shell has nobody to ask — so only a person at a
190
- * terminal with a real choice ever sees a prompt.
191
- */
172
+ // Decides which agents to configure, asking only when the answer is genuinely ambiguous.
173
+ // `--agent` is an explicit instruction, a lone install has no decision in it, and a
174
+ // non-interactive shell has nobody to ask — so only a person at a terminal with a real
175
+ // choice ever sees a prompt.
192
176
  async function selectAgents(survey, installed, requested) {
193
177
  if (requested && requested.length > 0) {
194
178
  const ids = requested.flatMap((value) => value.split(",").map((part) => part.trim().toLowerCase()));
@@ -200,9 +184,9 @@ async function selectAgents(survey, installed, requested) {
200
184
  .join(", ")}.`);
201
185
  process.exit(1);
202
186
  }
203
- // Deliberately not filtered by `installed`: --agent is an instruction,
204
- // and someone pointing it at an agent PATH cannot see has said what they
205
- // want. Setup writes the config; the agent picks it up when it appears.
187
+ // Deliberately not filtered by `installed`: `--agent` is an instruction, and someone
188
+ // pointing it at an agent PATH cannot see has said what they want. Setup writes the
189
+ // config; the agent picks it up when it appears.
206
190
  for (const entry of chosen) {
207
191
  if (!entry.installed) {
208
192
  console.log(`⚠ ${entry.agent.label} was not found on PATH — configuring it anyway.`);
@@ -243,13 +227,8 @@ async function selectAgents(survey, installed, requested) {
243
227
  rl.close();
244
228
  }
245
229
  }
246
- /**
247
- * The token this machine already has, from whichever agent holds one.
248
- *
249
- * Reading across agents is what makes "install Codex next month and re-run
250
- * setup" cost nothing: the new agent inherits the token the old one has
251
- * been using rather than minting a second one against the same machine.
252
- */
230
+ // The token this machine already has, from whichever agent holds one. Reading across
231
+ // agents is what makes "install Codex next month and re-run setup" cost nothing.
253
232
  function findExistingToken(survey) {
254
233
  for (const entry of survey) {
255
234
  try {
@@ -266,9 +245,8 @@ function findExistingToken(survey) {
266
245
  }
267
246
  function collectWarnings(configured) {
268
247
  const warnings = [];
269
- // Only Claude Code applies config `env` over the inherited environment,
270
- // so only Claude Code can silently shadow a shell export. Codex reads its
271
- // token straight out of config.toml and has nothing to shadow.
248
+ // Only Claude Code applies config `env` over the inherited environment, so only it can
249
+ // silently shadow a shell export. Codex reads its token straight out of config.toml.
272
250
  if (configured.some(({ entry }) => entry.agent.id === "claude")) {
273
251
  const shadowed = findShadowedExports();
274
252
  if (shadowed.length > 0) {
@@ -281,8 +259,8 @@ function collectWarnings(configured) {
281
259
  " sources that disagree is how spend ends up on the wrong account.");
282
260
  }
283
261
  }
284
- // A versioned config directory means the ingest token is about to be
285
- // committed. Worth saying plainly; the CLI cannot prevent it.
262
+ // A versioned config directory means the ingest token is about to be committed. Worth
263
+ // saying plainly; the CLI cannot prevent it.
286
264
  const versioned = new Set();
287
265
  for (const { entry } of configured) {
288
266
  for (const path of entry.agent.paths()) {
@@ -1,22 +1,4 @@
1
1
  import type { AgentId, AgentProviderId } from "./agents.js";
2
- /**
3
- * Where the ingest credentials come from, and what shadows what.
4
- *
5
- * The order matters and is not arbitrary. Inside a Claude Code session the
6
- * process environment is what the exporter and the hook actually read, and
7
- * `settings.json` `env` is applied *over* the inherited shell environment —
8
- * so a value in settings.json wins over one exported from `.envrc` or a
9
- * shell profile, silently. That is the failure this resolver is built to
10
- * make visible rather than to paper over: it reports the source alongside
11
- * the value so `doctor` can name the file that is winning.
12
- *
13
- * Codex is the mirror image and needs its own branch. It injects nothing
14
- * into the hook's environment, so `~/.codex/config.toml` is not one source
15
- * among several — it is the only one, and the token has to be read back out
16
- * of the exporter's `Authorization` header. Nothing shadows it, which is
17
- * why the Codex path cannot produce the split-brain failure the Claude path
18
- * is written to detect.
19
- */
20
2
  export type CredentialSource = "environment" | "settings" | "none";
21
3
  export type ResolvedAgentCredentials = {
22
4
  token: string | null;
@@ -33,21 +15,6 @@ export declare function findShadowedExports(cwd?: string): Array<{
33
15
  path: string;
34
16
  keys: string[];
35
17
  }>;
36
- /**
37
- * The command to write into an agent's hook configuration.
38
- *
39
- * Prefers the bare name, but only after confirming that `clocktopus` on
40
- * PATH resolves to *this* executable. Hooks run through a shell whose PATH
41
- * may differ from the interactive one, and a bare name that does not
42
- * resolve there fails silently — the session runs fine and every bit of
43
- * repository context is lost. An absolute path is uglier and survives that;
44
- * `doctor` checks it still exists.
45
- *
46
- * The provider is baked into the command because the two agents send
47
- * *identical* hook payloads — same field names, same event spellings, no
48
- * marker of any kind. Which agent is calling can only be known from how the
49
- * hook was installed.
50
- */
51
18
  export declare function resolveHookCommand(provider: AgentProviderId): {
52
19
  command: string;
53
20
  usesAbsolutePath: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-config.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-config.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK5D;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnE,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,cAAc,EAAE,gBAAgB,CAAC;IACjC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,KAAK,GAAE,OAAkB,GACxB,wBAAwB,CA0C1B;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAmBD,wBAAgB,mBAAmB,CAAC,GAAG,SAAgB,GAAG,KAAK,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC,CA+BD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,GAAG;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAgCA"}
1
+ {"version":3,"file":"agent-config.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-config.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAkB5D,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnE,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,cAAc,EAAE,gBAAgB,CAAC;IACjC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,KAAK,GAAE,OAAkB,GACxB,wBAAwB,CAyC1B;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAcD,wBAAgB,mBAAmB,CAAC,GAAG,SAAgB,GAAG,KAAK,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC,CA+BD;AAYD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,GAAG;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAgCA"}
@@ -27,9 +27,8 @@ export function resolveAgentCredentials(agent = "claude") {
27
27
  }
28
28
  }
29
29
  catch {
30
- // A malformed config file is reported properly by `setup` and `doctor`.
31
- // Credential resolution must not throw — the hook calls it on every
32
- // session start.
30
+ // A malformed config file is reported properly by `setup` and `doctor`. Credential
31
+ // resolution must not throw — the hook calls it on every session start.
33
32
  }
34
33
  return {
35
34
  token: envToken || fileToken || null,
@@ -51,14 +50,9 @@ export function resolveAgentCredentials(agent = "claude") {
51
50
  export function maskToken(token) {
52
51
  return token.length <= 15 ? "…" : `${token.slice(0, 15)}…`;
53
52
  }
54
- /**
55
- * Files that commonly export the same variables, and would be shadowed.
56
- *
57
- * Scanned rather than guessed at, because "your `.envrc` is being ignored"
58
- * is only useful advice when it names the file. The repo-local `.envrc` is
59
- * included because that is exactly how this project was dogfooded before
60
- * the CLI existed.
61
- */
53
+ // Files that commonly export the same variables, and would be shadowed. Scanned rather
54
+ // than guessed at, because "your `.envrc` is being ignored" is only useful advice when it
55
+ // names the file.
62
56
  const SHELL_FILES = [
63
57
  ".bashrc",
64
58
  ".bash_profile",
@@ -95,21 +89,16 @@ export function findShadowedExports(cwd = process.cwd()) {
95
89
  }
96
90
  return found;
97
91
  }
98
- /**
99
- * The command to write into an agent's hook configuration.
100
- *
101
- * Prefers the bare name, but only after confirming that `clocktopus` on
102
- * PATH resolves to *this* executable. Hooks run through a shell whose PATH
103
- * may differ from the interactive one, and a bare name that does not
104
- * resolve there fails silently — the session runs fine and every bit of
105
- * repository context is lost. An absolute path is uglier and survives that;
106
- * `doctor` checks it still exists.
107
- *
108
- * The provider is baked into the command because the two agents send
109
- * *identical* hook payloads — same field names, same event spellings, no
110
- * marker of any kind. Which agent is calling can only be known from how the
111
- * hook was installed.
112
- */
92
+ // The command to write into an agent's hook configuration.
93
+ //
94
+ // Prefers the bare name, but only after confirming `clocktopus` on PATH resolves to *this*
95
+ // executable. Hooks run through a shell whose PATH may differ from the interactive one, and
96
+ // a bare name that does not resolve there fails silently the session runs fine and every
97
+ // bit of repository context is lost. An absolute path is uglier and survives that.
98
+ //
99
+ // The provider is baked into the command because the two agents send identical hook
100
+ // payloads, with no marker of any kind: which agent is calling can only be known from how
101
+ // the hook was installed.
113
102
  export function resolveHookCommand(provider) {
114
103
  const script = process.argv[1] ? resolve(process.argv[1]) : null;
115
104
  const quote = (value) => /[\s"']/.test(value) ? `"${value}"` : value;
@@ -16,6 +16,7 @@ declare const StartStateSchema: z.ZodObject<{
16
16
  cwd: z.ZodOptional<z.ZodString>;
17
17
  provider: z.ZodOptional<z.ZodString>;
18
18
  closed: z.ZodOptional<z.ZodBoolean>;
19
+ startedAt: z.ZodOptional<z.ZodString>;
19
20
  }, z.core.$strip>;
20
21
  export type HookStartState = z.infer<typeof StartStateSchema>;
21
22
  export declare function writeStartState(sessionId: string, state: {
@@ -23,18 +24,13 @@ export declare function writeStartState(sessionId: string, state: {
23
24
  cwd: string;
24
25
  provider: string;
25
26
  closed?: boolean;
27
+ startedAt?: string;
26
28
  }): void;
27
29
  export declare function readStartState(sessionId: string): HookStartState | undefined;
28
30
  export declare function clearStartState(sessionId: string): void;
29
31
  export declare function listStartStates(): Array<{
30
32
  sessionId: string;
31
33
  ageMs: number;
32
- /**
33
- * When the state file was last touched — the newest moment this machine
34
- * has evidence the session existed. The sweep sends it as the abandoned
35
- * session's `ended_at` so the receiver does not stamp its own clock,
36
- * hours later, as the end of a window used to attribute commits.
37
- */
38
34
  lastActivityAt: Date;
39
35
  }>;
40
36
  export declare function recordLastRun(run: HookLastRun): void;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-hook-state.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-hook-state.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6BxB,QAAA,MAAM,aAAa;;;;;;;;;;iBAYjB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAExD,QAAA,MAAM,gBAAgB;;;;;iBAoBpB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAK9D,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACtE,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAgB5E;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,eAAe,IAAI,KAAK,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,cAAc,EAAE,IAAI,CAAC;CACtB,CAAC,CAsBD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAOpD;AAED,wBAAgB,WAAW,IAAI,WAAW,GAAG,IAAI,CAShD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAOrC"}
1
+ {"version":3,"file":"agent-hook-state.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-hook-state.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqBxB,QAAA,MAAM,aAAa;;;;;;;;;;iBAYjB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAExD,QAAA,MAAM,gBAAgB;;;;;;iBAgBpB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAK9D,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IACL,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,IAAI,CAUN;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAe5E;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,eAAe,IAAI,KAAK,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IAId,cAAc,EAAE,IAAI,CAAC;CACtB,CAAC,CAsBD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAOpD;AAED,wBAAgB,WAAW,IAAI,WAAW,GAAG,IAAI,CAShD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAOrC"}
@@ -2,34 +2,26 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync,
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
- /**
6
- * On-disk state the SessionStart/SessionEnd hook keeps between its two
7
- * invocations, plus the record of what the last invocation achieved.
8
- *
9
- * Under the cache directory rather than the repository so it never shows up
10
- * in `git status`. The path is unchanged from the standalone script this
11
- * command replaced (`scripts/agent-telemetry/claude-hook.mjs`, since
12
- * deleted) on purpose: a session that started under the old script must
13
- * still be able to close under the new one, and a sweep must still find
14
- * what the old one abandoned.
15
- */
5
+ // On-disk state the SessionStart/SessionEnd hook keeps between its two invocations, plus
6
+ // the record of what the last invocation achieved.
7
+ //
8
+ // Under the cache directory rather than the repository so it never shows up in
9
+ // `git status`. The path is unchanged from the standalone script this command replaced,
10
+ // on purpose: a session that started under the old script must still close under the new
11
+ // one, and a sweep must still find what the old one abandoned.
16
12
  const CACHE_DIR = join(homedir(), ".cache", "clocktopus");
17
13
  const STATE_DIR = join(CACHE_DIR, "agent-sessions");
18
- /**
19
- * The hook's last outcome, written every run.
20
- *
21
- * This is the only place the pipeline reports on itself from the machine's
22
- * own side. The hook must stay silent — it cannot warn, prompt or print
23
- * so instead it leaves a receipt, and `agent status` / `agent doctor` read
24
- * it. Without this, a token that started returning 401 mid-week is
25
- * invisible locally: the session runs normally and nothing is written
26
- * anywhere the user looks.
27
- */
14
+ // The hook's last outcome, written every run.
15
+ //
16
+ // The only place the pipeline reports on itself from the machine's own side. The hook
17
+ // must stay silent it cannot warn, prompt or print so it leaves a receipt, and
18
+ // `agent status` / `agent doctor` read it. Without this, a token that started returning
19
+ // 401 mid-week is invisible locally.
28
20
  const LAST_RUN_FILE = join(CACHE_DIR, "agent-hook-last.json");
29
21
  const LastRunSchema = z.object({
30
22
  at: z.string(),
31
23
  event: z.string().optional(),
32
- /** Which agent's hook ran — one receipt file is shared by all of them. */
24
+ // One receipt file is shared by all of them.
33
25
  provider: z.string().optional(),
34
26
  sessionId: z.string().optional(),
35
27
  endpoint: z.string().optional(),
@@ -42,32 +34,27 @@ const LastRunSchema = z.object({
42
34
  const StartStateSchema = z.object({
43
35
  sha: z.string(),
44
36
  cwd: z.string().optional(),
45
- /**
46
- * The agent that opened this session.
47
- *
48
- * Recorded because the sweep runs from whichever agent starts *next*, and
49
- * closing a Codex session as `claude_code` would not close it at all — it
50
- * would open a second, empty session row under the wrong provider and
51
- * leave the real one hanging forever.
52
- */
37
+ // The agent that opened this session. Recorded because the sweep runs from whichever
38
+ // agent starts *next*, and closing a Codex session as `claude_code` would open a second,
39
+ // empty session row under the wrong provider and leave the real one hanging.
53
40
  provider: z.string().optional(),
54
- /**
55
- * Set once the session has had a real SessionEnd, and kept only because
56
- * its host can send another one see `hostRepeatsSessionEnd`. The file
57
- * still holds the starting SHA the next SessionEnd needs to diff against,
58
- * but the session is already closed in the database, so the sweep must
59
- * delete it rather than close it a second time with a stale HEAD.
60
- */
41
+ // Set once the session has had a real SessionEnd, and kept only because its host can send
42
+ // another one see `hostRepeatsSessionEnd`. The file still holds the starting SHA the
43
+ // next SessionEnd needs, but the session is already closed, so the sweep must delete it
44
+ // rather than close it again with a stale HEAD.
61
45
  closed: z.boolean().optional(),
46
+ // When SessionStart fired, ISO. Bounds the reflog read SessionEnd declares from —
47
+ // see `declared-commits.ts`. Optional because sessions opened by an older CLI have
48
+ // no such stamp, and those declare nothing rather than read an unbounded reflog.
49
+ startedAt: z.string().optional(),
62
50
  });
63
51
  const stateFile = (sessionId) => join(STATE_DIR, `${sessionId.replace(/[^\w-]/g, "")}.sha`);
64
52
  export function writeStartState(sessionId, state) {
65
53
  try {
66
54
  mkdirSync(STATE_DIR, { recursive: true });
67
- // `cwd` is stored alongside the SHA because the sweep runs from
68
- // whatever repository the *next* session happens to start in.
69
- // Resolving an abandoned session's SHA against the wrong checkout would
70
- // either fail or, worse, succeed against an unrelated history.
55
+ // `cwd` is stored alongside the SHA because the sweep runs from whatever repository the
56
+ // *next* session happens to start in. Resolving an abandoned session's SHA against the
57
+ // wrong checkout would either fail or, worse, succeed against an unrelated history.
71
58
  writeFileSync(stateFile(sessionId), JSON.stringify(state), "utf8");
72
59
  }
73
60
  catch {
@@ -84,8 +71,7 @@ export function readStartState(sessionId) {
84
71
  return parsed.success ? parsed.data : undefined;
85
72
  }
86
73
  catch {
87
- // Files written by earlier versions hold a bare SHA. A session that
88
- // started under the old format must still close correctly.
74
+ // Files written by earlier versions hold a bare SHA.
89
75
  return { sha: raw };
90
76
  }
91
77
  }
@@ -1,27 +1,3 @@
1
- /**
2
- * The agents `clocktopus agent` can wire up, behind one interface.
3
- *
4
- * Everything agent-specific lives here: where the config is, how to detect
5
- * an install, what "configured" means, and how to add or remove our half of
6
- * it. `setup`, `status`, `doctor` and `disable` iterate this list, so
7
- * adding a third agent is a new entry rather than a new branch in five
8
- * commands.
9
- *
10
- * The two supported agents look similar and are not:
11
- *
12
- * | | Claude Code | Codex CLI |
13
- * | ------------- | -------------------------- | -------------------------------- |
14
- * | telemetry | OTLP metrics, real dollars | OTLP **logs**, tokens only |
15
- * | config | `settings.json` (one file) | `config.toml` + `hooks.json` |
16
- * | credentials | env vars it injects | read back out of `config.toml` |
17
- * | takes effect | restart | restart **and** a trust prompt |
18
- *
19
- * The credentials row is the subtle one. Claude Code applies `settings.json`
20
- * `env` to the session, so our hook inherits the token from the process
21
- * environment. Codex injects nothing, so the Codex hook has to read the
22
- * token back out of `config.toml` — which is why `resolveAgentCredentials`
23
- * takes a provider.
24
- */
25
1
  export type AgentId = "claude" | "codex" | "opencode";
26
2
  /** How the ingested session is labelled — must match `AgentProvider` in core. */
27
3
  export type AgentProviderId = "claude_code" | "codex_cli" | "opencode";
@@ -41,28 +17,7 @@ export type AgentAdapter = {
41
17
  provider: AgentProviderId;
42
18
  /** Executable name, used both to detect an install and to name it in help. */
43
19
  binary: string;
44
- /**
45
- * Whether the agent runs hooks off the session's critical path itself.
46
- *
47
- * Claude Code does, given `"async": true`. Codex does not — 0.147 skips
48
- * an async SessionStart outright and 0.148 still forces SessionEnd
49
- * synchronous — so its hooks omit the key and `agent hook` backgrounds
50
- * itself instead. See the header of `commands/agent/hook.ts`.
51
- */
52
20
  hostRunsHooksAsync: boolean;
53
- /**
54
- * Whether the host can fire SessionEnd more than once for one session.
55
- *
56
- * Claude Code and Codex each end a session exactly once. OpenCode has no
57
- * session-ended event at all — the plugin maps `session.idle` to
58
- * SessionEnd, and that fires every time the agent stops and waits for a
59
- * human, so a five-turn conversation sends five of them.
60
- *
61
- * The hook keeps its starting SHA in a state file keyed by session id and
62
- * deletes it on SessionEnd. Deleting it after the first idle would leave
63
- * every later turn with no range to diff, so nothing committed after the
64
- * agent's first pause could ever be declared — see `commands/agent/hook.ts`.
65
- */
66
21
  hostRepeatsSessionEnd: boolean;
67
22
  /** Files this agent's telemetry configuration lives in. */
68
23
  paths(): string[];
@@ -77,18 +32,7 @@ export type AgentAdapter = {
77
32
  remove(): {
78
33
  removed: string[];
79
34
  };
80
- /**
81
- * Anything the user must still do by hand before telemetry flows.
82
- * Empty when the agent is ready apart from a restart.
83
- */
84
35
  pendingActions(): string[];
85
- /**
86
- * Why the installed configuration is behind this CLI, or null when it is
87
- * current. Only defined for agents whose integration is *generated
88
- * source*, which is OpenCode alone: the others store a command string
89
- * that means whatever the installed CLI means, so upgrading the CLI
90
- * upgrades them and there is nothing to be behind.
91
- */
92
36
  staleReason?(): string | null;
93
37
  };
94
38
  /** `true` when the parse error is one of ours, whichever agent raised it. */
@@ -104,12 +48,5 @@ export type AgentSurvey = {
104
48
  /** Set when the agent's config exists but could not be parsed. */
105
49
  unreadable: string | null;
106
50
  };
107
- /**
108
- * What is on this machine, and what is already wired up.
109
- *
110
- * Detection is by `--version` rather than by config directory: `~/.claude`
111
- * and `~/.codex` both outlive an uninstall, so a stale directory would
112
- * offer to configure an agent that is no longer there.
113
- */
114
51
  export declare function surveyAgents(): AgentSurvey[];
115
52
  //# sourceMappingURL=agents.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/lib/agents.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEtD,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACrE,2EAA2E;IAC3E,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;;;OAYG;IACH,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2DAA2D;IAC3D,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,IAAI,IAAI,mBAAmB,CAAC;IAC5B,KAAK,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG;QACtE,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChC;;;OAGG;IACH,cAAc,IAAI,MAAM,EAAE,CAAC;IAC3B;;;;;;OAMG;IACH,WAAW,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAKjE;AAuND,eAAO,MAAM,MAAM,EAAE,SAAS,YAAY,EAIzC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAqB5C"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/lib/agents.ts"],"names":[],"mappings":"AAyDA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEtD,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACrE,2EAA2E;IAC3E,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IAKf,kBAAkB,EAAE,OAAO,CAAC;IAU5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2DAA2D;IAC3D,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,IAAI,IAAI,mBAAmB,CAAC;IAC5B,KAAK,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG;QACtE,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAGhC,cAAc,IAAI,MAAM,EAAE,CAAC;IAI3B,WAAW,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAKjE;AAkND,eAAO,MAAM,MAAM,EAAE,SAAS,YAAY,EAIzC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAOF,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAqB5C"}