@blastin-dev/clocktopus-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +91 -36
  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 +157 -95
  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 +239 -98
  10. package/dist/src/commands/agent/setup.d.ts +1 -16
  11. package/dist/src/commands/agent/setup.d.ts.map +1 -1
  12. package/dist/src/commands/agent/setup.js +198 -81
  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 +5 -23
  18. package/dist/src/lib/agent-config.d.ts.map +1 -1
  19. package/dist/src/lib/agent-config.js +50 -36
  20. package/dist/src/lib/agent-hook-state.d.ts +11 -7
  21. package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
  22. package/dist/src/lib/agent-hook-state.js +34 -29
  23. package/dist/src/lib/agents.d.ts +52 -0
  24. package/dist/src/lib/agents.d.ts.map +1 -0
  25. package/dist/src/lib/agents.js +238 -0
  26. package/dist/src/lib/claude-settings.d.ts +0 -36
  27. package/dist/src/lib/claude-settings.d.ts.map +1 -1
  28. package/dist/src/lib/claude-settings.js +37 -62
  29. package/dist/src/lib/codex-config.d.ts +87 -0
  30. package/dist/src/lib/codex-config.d.ts.map +1 -0
  31. package/dist/src/lib/codex-config.js +399 -0
  32. package/dist/src/lib/codex-config.test.d.ts +2 -0
  33. package/dist/src/lib/codex-config.test.d.ts.map +1 -0
  34. package/dist/src/lib/codex-config.test.js +359 -0
  35. package/dist/src/lib/declared-commits.d.ts +43 -0
  36. package/dist/src/lib/declared-commits.d.ts.map +1 -0
  37. package/dist/src/lib/declared-commits.js +114 -0
  38. package/dist/src/lib/declared-commits.test.d.ts +2 -0
  39. package/dist/src/lib/declared-commits.test.d.ts.map +1 -0
  40. package/dist/src/lib/declared-commits.test.js +129 -0
  41. package/dist/src/lib/git-remotes.d.ts +9 -0
  42. package/dist/src/lib/git-remotes.d.ts.map +1 -0
  43. package/dist/src/lib/git-remotes.js +50 -0
  44. package/dist/src/lib/git-remotes.test.d.ts +2 -0
  45. package/dist/src/lib/git-remotes.test.d.ts.map +1 -0
  46. package/dist/src/lib/git-remotes.test.js +52 -0
  47. package/dist/src/lib/opencode-config.d.ts +23 -0
  48. package/dist/src/lib/opencode-config.d.ts.map +1 -0
  49. package/dist/src/lib/opencode-config.js +290 -0
  50. package/dist/src/lib/opencode-config.test.d.ts +2 -0
  51. package/dist/src/lib/opencode-config.test.d.ts.map +1 -0
  52. package/dist/src/lib/opencode-config.test.js +140 -0
  53. package/package.json +2 -1
@@ -3,81 +3,140 @@ import { isAfter, parseISO } from "date-fns";
3
3
  import { findShadowedExports, maskToken, resolveAgentCredentials, } from "../../lib/agent-config.js";
4
4
  import { readLastRun } from "../../lib/agent-hook-state.js";
5
5
  import { verifyReceiver } from "../../lib/agent-receiver.js";
6
- import { readInstalledTelemetry, SettingsParseError, TELEMETRY_ENV_KEYS, } from "../../lib/claude-settings.js";
6
+ import { isConfigParseError, surveyAgents } from "../../lib/agents.js";
7
7
  import { isLoggedIn } from "../../lib/config.js";
8
8
  import { formatAgo } from "../../lib/format.js";
9
9
  import { fetchRepoStatus, repoChecks } from "../../lib/repo-guidance.js";
10
10
  export async function doctorCommand() {
11
+ const sections = [];
12
+ sections.push({
13
+ title: "This machine",
14
+ checks: [
15
+ {
16
+ label: "Logged in",
17
+ ok: isLoggedIn(),
18
+ detail: isLoggedIn() ? "yes" : "no",
19
+ fix: "clocktopus login",
20
+ },
21
+ ],
22
+ });
23
+ const survey = surveyAgents();
24
+ const relevant = survey.filter((entry) => entry.installed || entry.configured || entry.unreadable);
25
+ if (relevant.length === 0) {
26
+ sections[0].checks.push({
27
+ label: "Agents",
28
+ ok: false,
29
+ detail: `none of ${survey.map((e) => e.agent.binary).join(", ")} found on PATH`,
30
+ fix: "Install a supported agent, then run 'clocktopus agent setup'",
31
+ });
32
+ }
33
+ for (const entry of relevant) {
34
+ sections.push({
35
+ title: `${entry.agent.label}${entry.version ? ` ${entry.version}` : ""}`,
36
+ checks: await agentChecks(entry),
37
+ });
38
+ }
39
+ // Past this point the pipeline works; these decide whether what it carries can be turned
40
+ // into a number. Both are server-side facts about the repository, not about any one agent.
41
+ const tail = [];
42
+ const repoStatus = await fetchRepoStatus();
43
+ if (repoStatus)
44
+ tail.push(...repoChecks(repoStatus));
45
+ tail.push(checkLastHookRun());
46
+ sections.push({ title: "Repository and delivery", checks: tail });
47
+ report(sections);
48
+ }
49
+ async function agentChecks(entry) {
11
50
  const checks = [];
51
+ if (entry.unreadable) {
52
+ return [
53
+ {
54
+ label: "Config",
55
+ ok: false,
56
+ detail: "could not be parsed",
57
+ fix: entry.unreadable,
58
+ },
59
+ ];
60
+ }
12
61
  checks.push({
13
- label: "Logged in",
14
- ok: isLoggedIn(),
15
- detail: isLoggedIn() ? "yes" : "no",
16
- fix: "clocktopus login",
62
+ label: "Installed",
63
+ ok: entry.installed ? true : "warn",
64
+ detail: entry.installed
65
+ ? `${entry.agent.binary} ${entry.version ?? ""}`.trim()
66
+ : `${entry.agent.binary} is not on PATH`,
67
+ fix: "Config here is harmless, but nothing will report until it is installed",
17
68
  });
18
- let installed = null;
69
+ let state;
19
70
  try {
20
- installed = readInstalledTelemetry();
21
- checks.push({
22
- label: "settings.json",
23
- ok: installed.exists,
24
- detail: installed.exists
25
- ? `${installed.path} (modified ${formatAgo(installed.modifiedAt?.toISOString() ?? null)})`
26
- : `${installed.path} does not exist`,
27
- fix: "clocktopus agent setup",
28
- });
71
+ state = entry.agent.read();
29
72
  }
30
73
  catch (error) {
31
- if (error instanceof SettingsParseError) {
32
- checks.push({
33
- label: "settings.json",
74
+ if (!isConfigParseError(error))
75
+ throw error;
76
+ return [
77
+ {
78
+ label: "Config",
34
79
  ok: false,
35
- detail: "is not valid JSON",
36
- fix: "Fix or move the file, then run 'clocktopus agent setup'",
37
- });
38
- report(checks);
39
- return;
40
- }
41
- throw error;
80
+ detail: "could not be parsed",
81
+ fix: error.message,
82
+ },
83
+ ];
42
84
  }
43
- const missingEnv = TELEMETRY_ENV_KEYS.filter((key) => !installed?.env[key]);
44
85
  checks.push({
45
- label: "Exporter config",
46
- ok: missingEnv.length === 0,
47
- detail: missingEnv.length === 0
48
- ? `all ${TELEMETRY_ENV_KEYS.length} variables set`
49
- : // Listing nine names when none are set is noise; the useful
50
- // signal there is simply "nothing is configured".
51
- missingEnv.length === TELEMETRY_ENV_KEYS.length
52
- ? "not configured in settings.json"
53
- : `missing ${missingEnv.join(", ")}`,
54
- fix: "clocktopus agent setup",
86
+ label: "Config",
87
+ ok: state.token !== null,
88
+ detail: state.token !== null
89
+ ? `${state.paths.join(", ")} (modified ${formatAgo(state.modifiedAt?.toISOString() ?? null)})`
90
+ : `not configured in ${state.paths.join(", ")}`,
91
+ fix: `clocktopus agent setup --agent ${entry.agent.id}`,
55
92
  });
56
- // Both events are required, and for different reasons: SessionStart is the
57
- // only source of the *before* SHA and of `cwd`, SessionEnd is the only
58
- // source of the exact commit list. One without the other silently
59
- // degrades what can be attributed.
60
- const hookStart = installed.hookCommands.SessionStart;
61
- const hookEnd = installed.hookCommands.SessionEnd;
93
+ // Both events are required, for different reasons: SessionStart is the only source of the
94
+ // *before* SHA and of `cwd`, SessionEnd the only source of the exact commit list. One
95
+ // without the other silently degrades what can be attributed.
96
+ const hookStart = state.hookCommands.SessionStart;
97
+ const hookEnd = state.hookCommands.SessionEnd;
62
98
  checks.push({
63
99
  label: "Hooks installed",
64
100
  ok: Boolean(hookStart && hookEnd),
65
101
  detail: hookStart && hookEnd
66
102
  ? `SessionStart, SessionEnd → ${hookStart}`
67
- : `missing ${[!hookStart && "SessionStart", !hookEnd && "SessionEnd"].filter(Boolean).join(", ")}`,
68
- fix: "clocktopus agent setup",
103
+ : `missing ${[!hookStart && "SessionStart", !hookEnd && "SessionEnd"]
104
+ .filter(Boolean)
105
+ .join(", ")}`,
106
+ fix: `clocktopus agent setup --agent ${entry.agent.id}`,
69
107
  });
70
- if (hookStart) {
108
+ if (hookStart)
71
109
  checks.push(checkHookRuns(hookStart));
110
+ // A warning, not a failure: a plugin from an older generation still reports. What it does
111
+ // not have is whatever the newer one fixed, and nothing else in this report would say so
112
+ // — the token is valid, the hook resolves, the receiver answers.
113
+ const stale = entry.agent.staleReason?.() ?? null;
114
+ if (stale) {
115
+ checks.push({
116
+ label: "Generated config",
117
+ ok: "warn",
118
+ detail: stale,
119
+ fix: `clocktopus agent setup --agent ${entry.agent.id}`,
120
+ });
72
121
  }
73
- const credentials = resolveAgentCredentials();
122
+ for (const action of entry.agent.pendingActions()) {
123
+ checks.push({
124
+ label: "Hooks enabled",
125
+ ok: "warn",
126
+ detail: "installed but not yet approved by the agent",
127
+ fix: action,
128
+ });
129
+ }
130
+ const credentials = resolveAgentCredentials(entry.agent.id);
74
131
  checks.push({
75
132
  label: "Token in force",
76
133
  ok: credentials.token !== null,
77
134
  detail: credentials.token
78
- ? `${maskToken(credentials.token)} (from ${credentials.tokenSource})`
135
+ ? `${maskToken(credentials.token)} (from ${credentials.tokenSource === "settings"
136
+ ? credentials.sourcePath
137
+ : credentials.tokenSource})`
79
138
  : "none resolved",
80
- fix: "clocktopus agent setup",
139
+ fix: `clocktopus agent setup --agent ${entry.agent.id}`,
81
140
  });
82
141
  if (credentials.token && credentials.endpoint) {
83
142
  const check = await verifyReceiver(credentials.endpoint, credentials.token);
@@ -96,27 +155,20 @@ export async function doctorCommand() {
96
155
  : "clocktopus agent setup --force (mints a replacement token)",
97
156
  });
98
157
  }
99
- // Past this point the pipeline works; these two decide whether what it
100
- // carries can be turned into a number. Both are server-side facts about
101
- // the repository, not about this machine.
102
- const repoStatus = await fetchRepoStatus();
103
- if (repoStatus)
104
- checks.push(...repoChecks(repoStatus));
105
- checks.push(checkShadowedExports());
106
- checks.push(checkRestartNeeded(installed.modifiedAt));
107
- checks.push(checkLastHookRun());
108
- report(checks);
158
+ // Only Claude Code applies config `env` over the inherited environment, so only it can be
159
+ // shadowed by a shell export.
160
+ if (entry.agent.id === "claude")
161
+ checks.push(checkShadowedExports());
162
+ checks.push(checkRestartNeeded(entry, state.modifiedAt));
163
+ return checks;
109
164
  }
110
- /**
111
- * Runs the installed hook command with empty stdin.
112
- *
113
- * This is a genuine no-op the hook returns immediately when stdin carries
114
- * no payload so it sends nothing and records nothing, while still proving
115
- * the exact command line in settings.json resolves and executes. Worth
116
- * checking directly: hooks run under a shell whose PATH may differ from the
117
- * interactive one, and a command that fails to resolve there fails
118
- * silently, costing every session its repository context.
119
- */
165
+ // Runs the installed hook command with empty stdin.
166
+ //
167
+ // A genuine no-op — the hook returns immediately when stdin carries no payload — so it
168
+ // sends and records nothing while still proving the exact command line in the agent's
169
+ // config resolves and executes. Worth checking directly: hooks run under a shell whose
170
+ // PATH may differ from the interactive one, and a command that fails to resolve there
171
+ // fails silently, costing every session its repository context.
120
172
  function checkHookRuns(command) {
121
173
  try {
122
174
  execSync(command, {
@@ -159,31 +211,34 @@ function checkShadowedExports() {
159
211
  " disagree is how spend lands on the wrong account.",
160
212
  };
161
213
  }
162
- /**
163
- * Did the configuration change after the last session used it?
164
- *
165
- * The exporter and the hook both read their configuration once, at process
166
- * start, so a session already open when setup ran is still using the old
167
- * values. This is the most common reason a correct configuration looks dead.
168
- */
169
- function checkRestartNeeded(settingsModifiedAt) {
214
+ // Did the configuration change after the last session used it?
215
+ //
216
+ // The exporter and the hook both read their configuration once, at process start, so a
217
+ // session already open when setup ran is still using the old values. This is the most
218
+ // common reason a correct configuration looks dead.
219
+ //
220
+ // Scoped to the agent whose hook last ran: one receipt file is shared by all of them, and
221
+ // comparing Claude Code's last run against Codex's config mtime would report a restart
222
+ // that has already happened, or miss one that has not.
223
+ function checkRestartNeeded(entry, configModifiedAt) {
170
224
  const lastRun = readLastRun();
171
- if (!settingsModifiedAt || !lastRun?.at) {
225
+ const ranThisAgent = lastRun?.provider === entry.agent.provider;
226
+ if (!configModifiedAt || !lastRun?.at || !ranThisAgent) {
172
227
  return {
173
228
  label: "Restart",
174
229
  ok: "warn",
175
- detail: "the hook has not run since this configuration was written",
176
- fix: "Restart Claude Code, then run 'clocktopus agent status'",
230
+ detail: `${entry.agent.label} has not run a hook since this configuration was written`,
231
+ fix: `Restart ${entry.agent.label}, then run 'clocktopus agent status'`,
177
232
  };
178
233
  }
179
- const ranAfterConfig = isAfter(parseISO(lastRun.at), settingsModifiedAt);
234
+ const ranAfterConfig = isAfter(parseISO(lastRun.at), configModifiedAt);
180
235
  return {
181
236
  label: "Restart",
182
237
  ok: ranAfterConfig ? true : "warn",
183
238
  detail: ranAfterConfig
184
239
  ? "a session has run since the last configuration change"
185
- : "settings.json changed after the last session started",
186
- fix: "Restart Claude Code so the exporter and hook pick up the new values",
240
+ : "the config changed after the last session started",
241
+ fix: `Restart ${entry.agent.label} so the exporter and hook pick up the new values`,
187
242
  };
188
243
  }
189
244
  function checkLastHookRun() {
@@ -192,36 +247,43 @@ function checkLastHookRun() {
192
247
  return {
193
248
  label: "Last hook run",
194
249
  ok: "warn",
195
- detail: "no record — the hook has not run on this machine",
196
- fix: "Restart Claude Code and start a session",
250
+ detail: "no record — no hook has run on this machine",
251
+ fix: "Restart your agent and start a session",
197
252
  };
198
253
  }
199
254
  const failed = lastRun.status === null ||
200
255
  lastRun.status === undefined ||
201
256
  lastRun.status < 200 ||
202
257
  lastRun.status >= 300;
258
+ const who = lastRun.provider ? ` (${lastRun.provider})` : "";
203
259
  return {
204
260
  label: "Last hook run",
205
261
  ok: !failed,
206
262
  detail: failed
207
- ? `${formatAgo(lastRun.at)} — ${lastRun.status ? `HTTP ${lastRun.status}` : (lastRun.error ?? "no response")}`
208
- : `${formatAgo(lastRun.at)} — HTTP ${lastRun.status}`,
263
+ ? `${formatAgo(lastRun.at)}${who} — ${lastRun.status ? `HTTP ${lastRun.status}` : (lastRun.error ?? "no response")}`
264
+ : `${formatAgo(lastRun.at)}${who} — HTTP ${lastRun.status}`,
209
265
  fix: lastRun.status === 401
210
266
  ? "clocktopus agent setup --force (the token was rejected)"
211
267
  : "Check network access to the receiver",
212
268
  };
213
269
  }
214
- function report(checks) {
215
- console.log("\nAgent telemetry diagnosis\n");
216
- for (const check of checks) {
217
- const symbol = check.ok === true ? "✓" : check.ok === "warn" ? "⚠" : "✗";
218
- console.log(` ${symbol} ${check.label.padEnd(18)}${check.detail}`);
219
- if (check.ok !== true && check.fix) {
220
- console.log(` → ${check.fix}`);
270
+ function report(sections) {
271
+ console.log("\nAgent telemetry diagnosis");
272
+ for (const section of sections) {
273
+ if (section.checks.length === 0)
274
+ continue;
275
+ console.log(`\n${section.title}\n`);
276
+ for (const check of section.checks) {
277
+ const symbol = check.ok === true ? "✓" : check.ok === "warn" ? "⚠" : "✗";
278
+ console.log(` ${symbol} ${check.label.padEnd(18)}${check.detail}`);
279
+ if (check.ok !== true && check.fix) {
280
+ console.log(` → ${check.fix}`);
281
+ }
221
282
  }
222
283
  }
223
- const failures = checks.filter((check) => check.ok === false).length;
224
- const warnings = checks.filter((check) => check.ok === "warn").length;
284
+ const all = sections.flatMap((section) => section.checks);
285
+ const failures = all.filter((check) => check.ok === false).length;
286
+ const warnings = all.filter((check) => check.ok === "warn").length;
225
287
  console.log("");
226
288
  if (failures === 0 && warnings === 0) {
227
289
  console.log("Everything checks out. 'clocktopus agent status' shows what has arrived.");
@@ -1,2 +1,5 @@
1
- export declare function hookCommand(): Promise<void>;
1
+ export declare function hookCommand(options: {
2
+ provider?: string;
3
+ detached?: boolean;
4
+ }): Promise<void>;
2
5
  //# sourceMappingURL=hook.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/hook.ts"],"names":[],"mappings":"AAuRA,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAMjD"}
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/hook.ts"],"names":[],"mappings":"AAmcA,wBAAsB,WAAW,CAAC,OAAO,EAAE;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB"}