@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.
- package/README.md +91 -36
- package/dist/src/commands/agent/disable.d.ts +8 -1
- package/dist/src/commands/agent/disable.d.ts.map +1 -1
- package/dist/src/commands/agent/disable.js +79 -45
- package/dist/src/commands/agent/doctor.d.ts.map +1 -1
- package/dist/src/commands/agent/doctor.js +157 -95
- package/dist/src/commands/agent/hook.d.ts +4 -1
- package/dist/src/commands/agent/hook.d.ts.map +1 -1
- package/dist/src/commands/agent/hook.js +239 -98
- package/dist/src/commands/agent/setup.d.ts +1 -16
- package/dist/src/commands/agent/setup.d.ts.map +1 -1
- package/dist/src/commands/agent/setup.js +198 -81
- package/dist/src/commands/agent/status.d.ts.map +1 -1
- package/dist/src/commands/agent/status.js +46 -24
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -4
- package/dist/src/lib/agent-config.d.ts +5 -23
- package/dist/src/lib/agent-config.d.ts.map +1 -1
- package/dist/src/lib/agent-config.js +50 -36
- package/dist/src/lib/agent-hook-state.d.ts +11 -7
- package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
- package/dist/src/lib/agent-hook-state.js +34 -29
- package/dist/src/lib/agents.d.ts +52 -0
- package/dist/src/lib/agents.d.ts.map +1 -0
- package/dist/src/lib/agents.js +238 -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 +87 -0
- package/dist/src/lib/codex-config.d.ts.map +1 -0
- package/dist/src/lib/codex-config.js +399 -0
- package/dist/src/lib/codex-config.test.d.ts +2 -0
- package/dist/src/lib/codex-config.test.d.ts.map +1 -0
- package/dist/src/lib/codex-config.test.js +359 -0
- 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 +23 -0
- package/dist/src/lib/opencode-config.d.ts.map +1 -0
- package/dist/src/lib/opencode-config.js +290 -0
- package/dist/src/lib/opencode-config.test.d.ts +2 -0
- package/dist/src/lib/opencode-config.test.d.ts.map +1 -0
- package/dist/src/lib/opencode-config.test.js +140 -0
- 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 {
|
|
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: "
|
|
14
|
-
ok:
|
|
15
|
-
detail:
|
|
16
|
-
|
|
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
|
|
69
|
+
let state;
|
|
19
70
|
try {
|
|
20
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
74
|
+
if (!isConfigParseError(error))
|
|
75
|
+
throw error;
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
label: "Config",
|
|
34
79
|
ok: false,
|
|
35
|
-
detail: "
|
|
36
|
-
fix:
|
|
37
|
-
}
|
|
38
|
-
|
|
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: "
|
|
46
|
-
ok:
|
|
47
|
-
detail:
|
|
48
|
-
?
|
|
49
|
-
:
|
|
50
|
-
|
|
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,
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
const
|
|
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"]
|
|
68
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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:
|
|
176
|
-
fix:
|
|
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),
|
|
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
|
-
: "
|
|
186
|
-
fix:
|
|
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 —
|
|
196
|
-
fix: "Restart
|
|
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(
|
|
215
|
-
console.log("\nAgent telemetry diagnosis
|
|
216
|
-
for (const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
224
|
-
const
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/hook.ts"],"names":[],"mappings":"
|
|
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"}
|