@blastin-dev/clocktopus-cli 0.2.0 → 0.2.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 (38) hide show
  1. package/README.md +89 -34
  2. package/dist/src/commands/agent/disable.d.ts +8 -1
  3. package/dist/src/commands/agent/disable.d.ts.map +1 -1
  4. package/dist/src/commands/agent/disable.js +79 -45
  5. package/dist/src/commands/agent/doctor.d.ts.map +1 -1
  6. package/dist/src/commands/agent/doctor.js +146 -75
  7. package/dist/src/commands/agent/hook.d.ts +4 -1
  8. package/dist/src/commands/agent/hook.d.ts.map +1 -1
  9. package/dist/src/commands/agent/hook.js +152 -15
  10. package/dist/src/commands/agent/setup.d.ts +17 -10
  11. package/dist/src/commands/agent/setup.d.ts.map +1 -1
  12. package/dist/src/commands/agent/setup.js +208 -69
  13. package/dist/src/commands/agent/status.d.ts.map +1 -1
  14. package/dist/src/commands/agent/status.js +46 -24
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +18 -4
  17. package/dist/src/lib/agent-config.d.ts +18 -3
  18. package/dist/src/lib/agent-config.d.ts.map +1 -1
  19. package/dist/src/lib/agent-config.js +44 -19
  20. package/dist/src/lib/agent-hook-state.d.ts +9 -1
  21. package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
  22. package/dist/src/lib/agent-hook-state.js +21 -2
  23. package/dist/src/lib/agents.d.ts +115 -0
  24. package/dist/src/lib/agents.d.ts.map +1 -0
  25. package/dist/src/lib/agents.js +245 -0
  26. package/dist/src/lib/codex-config.d.ts +166 -0
  27. package/dist/src/lib/codex-config.d.ts.map +1 -0
  28. package/dist/src/lib/codex-config.js +441 -0
  29. package/dist/src/lib/codex-config.test.d.ts +2 -0
  30. package/dist/src/lib/codex-config.test.d.ts.map +1 -0
  31. package/dist/src/lib/codex-config.test.js +359 -0
  32. package/dist/src/lib/opencode-config.d.ts +108 -0
  33. package/dist/src/lib/opencode-config.d.ts.map +1 -0
  34. package/dist/src/lib/opencode-config.js +330 -0
  35. package/dist/src/lib/opencode-config.test.d.ts +2 -0
  36. package/dist/src/lib/opencode-config.test.d.ts.map +1 -0
  37. package/dist/src/lib/opencode-config.test.js +140 -0
  38. package/package.json +2 -1
package/README.md CHANGED
@@ -67,34 +67,77 @@ Show clock signals for a given date.
67
67
  ### `clocktopus agent setup`
68
68
 
69
69
  ```
70
- clocktopus agent setup [--name <label>] [--force]
70
+ clocktopus agent setup [--name <label>] [--force] [--agent <ids>]
71
71
  ```
72
72
 
73
73
  Configures this machine to report AI agent spend — token cost, active time,
74
74
  and the repository each session worked in — so the true cost of a piece of
75
75
  work can be measured as human time × rate **plus** agent spend.
76
76
 
77
- It mints an ingest token, writes the exporter configuration and installs the
78
- `SessionStart` / `SessionEnd` hooks into `~/.claude/settings.json`, then
79
- confirms the receiver accepts the token before reporting success.
77
+ One command, however many agents you have. It looks for each supported CLI
78
+ on PATH and configures what it finds:
79
+
80
+ ```
81
+ Agents on this machine
82
+
83
+ 1 Claude Code 2.1.234 already configured
84
+ 2 Codex CLI 0.147.0 not configured
85
+
86
+ Which should report to Clocktopus? [all]:
87
+ ```
88
+
89
+ With one agent installed there is no question to ask, so it just runs. With
90
+ several it prompts; `--agent` skips the prompt, and a non-interactive shell
91
+ configures everything found and says so.
92
+
93
+ | Agent | `--agent` id | Writes |
94
+ | ----------- | ------------ | --------------------------------------------- |
95
+ | Claude Code | `claude` | `~/.claude/settings.json` |
96
+ | Codex CLI | `codex` | `~/.codex/config.toml`, `~/.codex/hooks.json` |
97
+ | OpenCode | `opencode` | `~/.config/opencode/plugin/clocktopus.js` |
98
+
99
+ It mints **one** ingest token for the machine, writes each agent's exporter
100
+ configuration and `SessionStart` / `SessionEnd` hooks, then confirms the
101
+ receiver accepts the token before reporting success.
80
102
 
81
103
  | Option | Description |
82
104
  | ---------------- | --------------------------------------------------------------------------------------------------------------- |
83
105
  | `--name <label>` | Label for this machine's token. Defaults to the hostname — keep it recognisable, since revocation is per token. |
84
106
  | `--force` | Mint a replacement token instead of reusing the one already installed. |
107
+ | `-a, --agent` | Configure only these agents. Repeatable, or comma-separated: `--agent claude,codex`. |
85
108
 
86
109
  Re-running without `--force` is safe and idempotent: it repairs the hooks and
87
- refreshes the endpoint while keeping the existing token.
88
-
89
- **Restart Claude Code afterwards.** Both the OTLP exporter and the hook read
90
- their configuration once, at process start, so a session that is already open
91
- will not pick any of it up. This is the most common reason a correct setup
92
- appears to do nothing.
93
-
94
- Everything lands in `~/.claude/settings.json` — the exporter environment and
95
- the hooks in one file. That single source of truth is what lets
96
- `agent doctor` say which value is actually in force. Note the ingest token is
97
- stored there in plaintext: if you keep `~/.claude` in a dotfiles repository,
110
+ refreshes the endpoint while keeping the existing token. Installing a second
111
+ agent later and re-running costs nothing — the new one inherits the token the
112
+ first is already using rather than minting a second against the same machine.
113
+
114
+ **Restart the agent afterwards.** Every exporter and hook reads its
115
+ configuration once, at process start, so a session that is already open will
116
+ not pick any of it up. This is the most common reason a correct setup appears
117
+ to do nothing.
118
+
119
+ **OpenCode is a plugin, not a hook.** It has no hook system, so setup
120
+ writes one generated file into `~/.config/opencode/plugin/`, which OpenCode
121
+ auto-loads. Your `opencode.json` is not touched — that file is your model,
122
+ provider and permission configuration, and leaving it alone removes a whole
123
+ class of ways to break your setup. Uninstalling is deleting the file.
124
+
125
+ OpenCode also has `experimental.openTelemetry`, which looks like an easier
126
+ route and is not one: its spans carry your prompts and the model's replies
127
+ verbatim, with no way to switch that off, and no cost or repository. The
128
+ plugin reads OpenCode's own per-message totals instead and sends counts and
129
+ identifiers only.
130
+
131
+ **Codex needs one extra step, once.** It will not run a `hooks.json` it has
132
+ not been shown, so the next Codex session asks — answer _Trust all and
133
+ continue_. Until you do, Codex reports what your sessions cost but not which
134
+ repository they were spent on. `clocktopus agent doctor` reports the state
135
+ until it is approved.
136
+
137
+ Everything for an agent lands in that agent's own config rather than a shell
138
+ profile. That single source of truth per agent is what lets `agent doctor`
139
+ say which value is actually in force. Note the ingest token is stored there
140
+ in plaintext: if you keep `~/.claude` or `~/.codex` in a dotfiles repository,
98
141
  ignore the file.
99
142
 
100
143
  Setup then checks the repository you ran it in, because configuring the
@@ -124,21 +167,27 @@ claim; this is the evidence.
124
167
 
125
168
  The two ingest lanes are reported separately because they fail separately:
126
169
 
127
- | Lane | Carries | If it alone goes quiet |
128
- | ------- | -------------------------------- | -------------------------------------------------------- |
129
- | Hook | repository, branch, commit range | spend is recorded but cannot be attributed to a project |
130
- | Metrics | tokens, cost, active time | sessions show `$0.00`, indistinguishable from cheap work |
170
+ | Lane | Carries | If it alone goes quiet |
171
+ | ----- | -------------------------------- | -------------------------------------------------------- |
172
+ | Hook | repository, branch, commit range | spend is recorded but cannot be attributed to a project |
173
+ | Spend | tokens, cost, active time | sessions show `$0.00`, indistinguishable from cheap work |
174
+
175
+ It also lists every supported agent found on this machine, including any
176
+ that are installed but _not_ reporting — a state the server-side lanes
177
+ cannot show, because those sessions simply never arrive.
131
178
 
132
179
  That second failure is why there is no single "connected" line — it is the
133
180
  one that silently understates what work cost.
134
181
 
135
182
  ### `clocktopus agent doctor`
136
183
 
137
- Checks every link in the chain and names the first one that is broken:
138
- settings file parses, exporter variables present, both hooks installed, the
139
- hook command actually executes, the receiver accepts the token, the
140
- repository is attached to a project and delivering commits, no other file is
141
- shadowing the configuration, and whether Claude Code needs restarting.
184
+ Checks every link in the chain, per agent, and names the first one that is
185
+ broken: the config file parses, the exporter is configured, both hooks are
186
+ installed, the hook command actually executes, Codex has approved its hooks,
187
+ the receiver accepts the token, no other file is shadowing the
188
+ configuration, and whether that agent needs restarting. The repository
189
+ checks — attached to a project, delivering commits — run once at the end,
190
+ since they are facts about the repo rather than about any one agent.
142
191
 
143
192
  The receiver check is the one that cannot be done locally — it crosses the
144
193
  network to a different host than the dashboard, so a wrong endpoint and a
@@ -147,15 +196,21 @@ revoked token are otherwise indistinguishable from an idle afternoon.
147
196
  ### `clocktopus agent disable`
148
197
 
149
198
  ```
150
- clocktopus agent disable [--revoke]
199
+ clocktopus agent disable [--revoke] [--agent <ids>]
151
200
  ```
152
201
 
153
- Removes this machine's telemetry configuration, leaving any hooks and
154
- environment variables you added yourself untouched.
202
+ Removes this machine's telemetry configuration from every agent, leaving any
203
+ hooks and settings you added yourself untouched.
204
+
205
+ | Option | Description |
206
+ | ------------- | --------------------------------------------------------------------------- |
207
+ | `--revoke` | Also revoke the ingest token, making it useless everywhere. |
208
+ | `-a, --agent` | Remove only these agents (`claude`, `codex`), leaving the others reporting. |
155
209
 
156
- | Option | Description |
157
- | ---------- | ----------------------------------------------------------- |
158
- | `--revoke` | Also revoke the ingest token, making it useless everywhere. |
210
+ `--revoke` is ignored when combined with `--agent`: the token is shared by
211
+ every agent on the machine, so killing it while another one still has it
212
+ written would leave that agent posting into a 401 forever which looks
213
+ like an idle week rather than a broken install.
159
214
 
160
215
  Stopping and revoking are separate on purpose: removing the local
161
216
  configuration stops _this_ machine reporting, but does nothing about a token
@@ -173,7 +228,7 @@ ig.json`
173
228
  Run `clocktopus logout` to clear stored credentials.
174
229
 
175
230
  The agent telemetry setup deliberately keeps **no** copy of the ingest token
176
- here. It has to live in `~/.claude/settings.json` for Claude Code's exporter
177
- to read it, and a second copy would be another thing to leak and another
178
- thing to fall out of date. Only the token id (needed to revoke) and its
179
- display prefix are kept.
231
+ here. It has to live in each agent's own config for that agent's exporter to
232
+ read it, and a second copy would be another thing to leak and another thing
233
+ to fall out of date. Only the token id (needed to revoke) and its display
234
+ prefix are kept.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Removes this machine's telemetry configuration.
2
+ * Removes this machine's telemetry configuration, from every agent.
3
3
  *
4
4
  * Stopping and revoking are separate on purpose. Removing the local
5
5
  * configuration stops *this* machine reporting; it does nothing about a
@@ -7,8 +7,15 @@
7
7
  * repository. `--revoke` is the one that makes the token useless everywhere,
8
8
  * and it is opt-in because the common case — moving machines, reinstalling —
9
9
  * should not invalidate a token that CI or another workstation still uses.
10
+ *
11
+ * `--agent` narrows it to one, which is the only way to stop reporting from
12
+ * Codex while leaving Claude Code alone. Without it every agent is cleaned,
13
+ * including ones no longer installed — an uninstalled agent's config file
14
+ * outlives it, and a token left in a file nobody looks at is the worst
15
+ * place for one to sit.
10
16
  */
11
17
  export declare function disableCommand(options: {
12
18
  revoke?: boolean;
19
+ agent?: string[];
13
20
  }): Promise<void>;
14
21
  //# sourceMappingURL=disable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"disable.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/disable.ts"],"names":[],"mappings":"AAcA;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+EhB"}
1
+ {"version":3,"file":"disable.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/disable.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyFhB"}
@@ -1,9 +1,9 @@
1
1
  import { clearHookState } from "../../lib/agent-hook-state.js";
2
+ import { isConfigParseError, surveyAgents } from "../../lib/agents.js";
2
3
  import { ApiError, request } from "../../lib/api.js";
3
- import { readSettings, removeTelemetrySettings, SettingsParseError, writeSettings, } from "../../lib/claude-settings.js";
4
4
  import { clearAgentConfig, getAgentConfig, isLoggedIn, } from "../../lib/config.js";
5
5
  /**
6
- * Removes this machine's telemetry configuration.
6
+ * Removes this machine's telemetry configuration, from every agent.
7
7
  *
8
8
  * Stopping and revoking are separate on purpose. Removing the local
9
9
  * configuration stops *this* machine reporting; it does nothing about a
@@ -11,62 +11,96 @@ import { clearAgentConfig, getAgentConfig, isLoggedIn, } from "../../lib/config.
11
11
  * repository. `--revoke` is the one that makes the token useless everywhere,
12
12
  * and it is opt-in because the common case — moving machines, reinstalling —
13
13
  * should not invalidate a token that CI or another workstation still uses.
14
+ *
15
+ * `--agent` narrows it to one, which is the only way to stop reporting from
16
+ * Codex while leaving Claude Code alone. Without it every agent is cleaned,
17
+ * including ones no longer installed — an uninstalled agent's config file
18
+ * outlives it, and a token left in a file nobody looks at is the worst
19
+ * place for one to sit.
14
20
  */
15
21
  export async function disableCommand(options) {
16
- let removedEnvKeys = [];
17
- let removedHooks = false;
18
- try {
19
- const current = readSettings();
20
- if (current.exists) {
21
- const result = removeTelemetrySettings(current.settings);
22
- removedEnvKeys = result.removedEnvKeys;
23
- removedHooks = result.removedHooks;
24
- if (removedEnvKeys.length > 0 || removedHooks) {
25
- writeSettings(result.settings, current.path);
26
- }
27
- }
28
- }
29
- catch (error) {
30
- if (error instanceof SettingsParseError) {
31
- console.error(`✗ ${error.message}`);
22
+ const survey = surveyAgents();
23
+ const requested = (options.agent ?? []).flatMap((value) => value.split(",").map((part) => part.trim().toLowerCase()));
24
+ if (requested.length > 0) {
25
+ const unknown = requested.filter((id) => !survey.some((entry) => entry.agent.id === id));
26
+ if (unknown.length > 0) {
27
+ console.error(`Unknown agent: ${unknown.join(", ")}. Valid ids: ${survey
28
+ .map((entry) => entry.agent.id)
29
+ .join(", ")}.`);
32
30
  process.exit(1);
33
31
  }
34
- throw error;
35
32
  }
36
- const agent = getAgentConfig();
37
- if (options.revoke) {
38
- if (!agent.tokenId) {
39
- console.log("⚠ No token id on record, so nothing was revoked. This machine's token\n" +
40
- " was minted by an older version or on another machine — revoke it\n" +
41
- " from the dashboard instead.");
42
- }
43
- else if (!isLoggedIn()) {
44
- console.log("⚠ Not logged in, so the token could not be revoked. Run 'clocktopus\n" +
45
- " login' and try again — the local configuration was still removed.");
33
+ const targets = requested.length > 0
34
+ ? survey.filter((entry) => requested.includes(entry.agent.id))
35
+ : survey;
36
+ const cleaned = [];
37
+ for (const entry of targets) {
38
+ try {
39
+ const { removed } = entry.agent.remove();
40
+ if (removed.length > 0)
41
+ cleaned.push({ label: entry.agent.label, removed });
46
42
  }
47
- else {
48
- try {
49
- await request(`/api/agent/ingest-token?tokenId=${encodeURIComponent(agent.tokenId)}`, { method: "DELETE" });
50
- console.log(`✓ Revoked ${agent.tokenPrefix ?? "the ingest token"}.`);
51
- }
52
- catch (error) {
53
- console.error(`✗ Could not revoke the token: ${error instanceof ApiError || error instanceof Error
54
- ? error.message
55
- : "unknown error"}`);
56
- console.error(" The local configuration was still removed.");
43
+ catch (error) {
44
+ if (isConfigParseError(error)) {
45
+ console.error(`✗ ${error.message}`);
46
+ process.exit(1);
57
47
  }
48
+ // A failure on one agent must not leave the others reporting.
49
+ console.error(`✗ ${entry.agent.label}: ${error instanceof Error ? error.message : "unknown error"}`);
58
50
  }
59
51
  }
60
- clearAgentConfig();
61
- clearHookState();
62
- if (removedEnvKeys.length === 0 && !removedHooks) {
52
+ const agent = getAgentConfig();
53
+ // Revoking while another agent on this machine still has the token
54
+ // written would leave that one posting into a 401 forever, which looks
55
+ // like an idle week rather than a broken install.
56
+ const partial = requested.length > 0 && requested.length < survey.length;
57
+ if (options.revoke && partial) {
58
+ console.log("⚠ --revoke was ignored: it kills the token everywhere, and this run only\n" +
59
+ " cleaned some of the agents on this machine. Re-run without --agent to\n" +
60
+ " remove all of them, then revoke.");
61
+ }
62
+ else if (options.revoke) {
63
+ await revokeToken(agent);
64
+ }
65
+ if (!partial) {
66
+ clearAgentConfig();
67
+ clearHookState();
68
+ }
69
+ if (cleaned.length === 0) {
63
70
  console.log("Nothing to remove — agent telemetry was not configured here.");
64
71
  return;
65
72
  }
66
- console.log(`✓ Removed ${removedEnvKeys.length} environment variable(s)${removedHooks ? " and the session hooks" : ""} from settings.json.`);
67
- if (!options.revoke) {
73
+ for (const { label, removed } of cleaned) {
74
+ console.log(`✓ ${label}: removed ${removed.join(", ")}.`);
75
+ }
76
+ if (!options.revoke || partial) {
68
77
  console.log("\nThe ingest token is still valid. Revoke it with\n" +
69
78
  " clocktopus agent disable --revoke");
70
79
  }
71
- console.log("\nRestart Claude Code to stop the current session reporting.");
80
+ console.log(`\nRestart ${cleaned
81
+ .map((c) => c.label)
82
+ .join(" and ")} to stop the current session reporting.`);
83
+ }
84
+ async function revokeToken(agent) {
85
+ if (!agent.tokenId) {
86
+ console.log("⚠ No token id on record, so nothing was revoked. This machine's token\n" +
87
+ " was minted by an older version or on another machine — revoke it\n" +
88
+ " from the dashboard instead.");
89
+ return;
90
+ }
91
+ if (!isLoggedIn()) {
92
+ console.log("⚠ Not logged in, so the token could not be revoked. Run 'clocktopus\n" +
93
+ " login' and try again — the local configuration was still removed.");
94
+ return;
95
+ }
96
+ try {
97
+ await request(`/api/agent/ingest-token?tokenId=${encodeURIComponent(agent.tokenId)}`, { method: "DELETE" });
98
+ console.log(`✓ Revoked ${agent.tokenPrefix ?? "the ingest token"}.`);
99
+ }
100
+ catch (error) {
101
+ console.error(`✗ Could not revoke the token: ${error instanceof ApiError || error instanceof Error
102
+ ? error.message
103
+ : "unknown error"}`);
104
+ console.error(" The local configuration was still removed.");
105
+ }
72
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/doctor.ts"],"names":[],"mappings":"AAoCA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA8GnD"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/doctor.ts"],"names":[],"mappings":"AAuCA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA8CnD"}