@blastin-dev/clocktopus-cli 0.1.4 → 0.2.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 +108 -5
- package/dist/src/commands/agent/disable.d.ts +14 -0
- package/dist/src/commands/agent/disable.d.ts.map +1 -0
- package/dist/src/commands/agent/disable.js +72 -0
- package/dist/src/commands/agent/doctor.d.ts +2 -0
- package/dist/src/commands/agent/doctor.d.ts.map +1 -0
- package/dist/src/commands/agent/doctor.js +235 -0
- package/dist/src/commands/agent/hook.d.ts +2 -0
- package/dist/src/commands/agent/hook.d.ts.map +1 -0
- package/dist/src/commands/agent/hook.js +231 -0
- package/dist/src/commands/agent/setup.d.ts +21 -0
- package/dist/src/commands/agent/setup.d.ts.map +1 -0
- package/dist/src/commands/agent/setup.js +194 -0
- package/dist/src/commands/agent/status.d.ts +2 -0
- package/dist/src/commands/agent/status.d.ts.map +1 -0
- package/dist/src/commands/agent/status.js +160 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +37 -2
- package/dist/src/lib/agent-config.d.ts +41 -0
- package/dist/src/lib/agent-config.d.ts.map +1 -0
- package/dist/src/lib/agent-config.js +143 -0
- package/dist/src/lib/agent-hook-state.d.ts +36 -0
- package/dist/src/lib/agent-hook-state.d.ts.map +1 -0
- package/dist/src/lib/agent-hook-state.js +136 -0
- package/dist/src/lib/agent-receiver.d.ts +26 -0
- package/dist/src/lib/agent-receiver.d.ts.map +1 -0
- package/dist/src/lib/agent-receiver.js +44 -0
- package/dist/src/lib/claude-settings.d.ts +82 -0
- package/dist/src/lib/claude-settings.d.ts.map +1 -0
- package/dist/src/lib/claude-settings.js +271 -0
- package/dist/src/lib/claude-settings.test.d.ts +2 -0
- package/dist/src/lib/claude-settings.test.d.ts.map +1 -0
- package/dist/src/lib/claude-settings.test.js +193 -0
- package/dist/src/lib/config.d.ts +23 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +14 -0
- package/dist/src/lib/format.d.ts +6 -0
- package/dist/src/lib/format.d.ts.map +1 -0
- package/dist/src/lib/format.js +19 -0
- package/dist/src/lib/git.d.ts +3 -0
- package/dist/src/lib/git.d.ts.map +1 -0
- package/dist/src/lib/git.js +30 -0
- package/dist/src/lib/repo-guidance.d.ts +40 -0
- package/dist/src/lib/repo-guidance.d.ts.map +1 -0
- package/dist/src/lib/repo-guidance.js +123 -0
- package/dist/src/lib/validators.d.ts +69 -0
- package/dist/src/lib/validators.d.ts.map +1 -1
- package/dist/src/lib/validators.js +69 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -47,10 +47,10 @@ clocktopus clock (in|out) [--ago <duration> | --at <time>]
|
|
|
47
47
|
Record the start or end of a work session. Defaults to "now" when no
|
|
48
48
|
flag is passed.
|
|
49
49
|
|
|
50
|
-
| Option
|
|
51
|
-
|
|
|
50
|
+
| Option | Description |
|
|
51
|
+
| ------------------ | --------------------------------------------------------------------------------------- |
|
|
52
52
|
| `--ago <duration>` | Backdate by a duration from now (`15m`, `1h`, `1h30m`). Mutually exclusive with `--at`. |
|
|
53
|
-
| `--at <time>`
|
|
53
|
+
| `--at <time>` | Backdate to an absolute wall-clock time today (`HH:mm` or `HH:mm:ss`). |
|
|
54
54
|
|
|
55
55
|
### `clocktopus clock status`
|
|
56
56
|
|
|
@@ -60,10 +60,107 @@ clocktopus clock status [-d <YYYY-MM-DD>]
|
|
|
60
60
|
|
|
61
61
|
Show clock signals for a given date.
|
|
62
62
|
|
|
63
|
-
| Option
|
|
64
|
-
|
|
|
63
|
+
| Option | Description |
|
|
64
|
+
| ------------------- | --------------------------------------------------------------------------- |
|
|
65
65
|
| `-d, --date <date>` | Show signals for a specific date in `YYYY-MM-DD` format. Defaults to today. |
|
|
66
66
|
|
|
67
|
+
### `clocktopus agent setup`
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
clocktopus agent setup [--name <label>] [--force]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Configures this machine to report AI agent spend — token cost, active time,
|
|
74
|
+
and the repository each session worked in — so the true cost of a piece of
|
|
75
|
+
work can be measured as human time × rate **plus** agent spend.
|
|
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.
|
|
80
|
+
|
|
81
|
+
| Option | Description |
|
|
82
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| `--name <label>` | Label for this machine's token. Defaults to the hostname — keep it recognisable, since revocation is per token. |
|
|
84
|
+
| `--force` | Mint a replacement token instead of reusing the one already installed. |
|
|
85
|
+
|
|
86
|
+
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,
|
|
98
|
+
ignore the file.
|
|
99
|
+
|
|
100
|
+
Setup then checks the repository you ran it in, because configuring the
|
|
101
|
+
machine is only half the job — sessions will arrive either way, but two
|
|
102
|
+
things decide whether they mean anything, and neither is visible locally:
|
|
103
|
+
|
|
104
|
+
| Check | If it fails |
|
|
105
|
+
| -------------------------------- | -------------------------------------------------------------------------------- |
|
|
106
|
+
| Repository attached to a project | Spend is recorded, but lands in the unattributed row instead of against a client |
|
|
107
|
+
| Push webhook delivering | No human time entries, so there is nothing to compare the spend against |
|
|
108
|
+
|
|
109
|
+
The second does **not** stop agent→commit links — the SessionEnd hook
|
|
110
|
+
resolves `git rev-list before..after` locally and posts the list, so those
|
|
111
|
+
are created with no webhook involved. What is missing is the other side of
|
|
112
|
+
the comparison: true cost is human time × rate _plus_ agent spend, and one
|
|
113
|
+
term alone is not a smaller answer.
|
|
114
|
+
|
|
115
|
+
Both are reported separately, with the exact next step, because they send
|
|
116
|
+
you to two different places. Setup only tells you — it never changes your
|
|
117
|
+
GitHub settings. Note the webhook URL is **per person, not per repository**,
|
|
118
|
+
so on a shared repo each teammate adds their own.
|
|
119
|
+
|
|
120
|
+
### `clocktopus agent status`
|
|
121
|
+
|
|
122
|
+
Shows what the receiver has actually received. Local configuration is a
|
|
123
|
+
claim; this is the evidence.
|
|
124
|
+
|
|
125
|
+
The two ingest lanes are reported separately because they fail separately:
|
|
126
|
+
|
|
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 |
|
|
131
|
+
|
|
132
|
+
That second failure is why there is no single "connected" line — it is the
|
|
133
|
+
one that silently understates what work cost.
|
|
134
|
+
|
|
135
|
+
### `clocktopus agent doctor`
|
|
136
|
+
|
|
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.
|
|
142
|
+
|
|
143
|
+
The receiver check is the one that cannot be done locally — it crosses the
|
|
144
|
+
network to a different host than the dashboard, so a wrong endpoint and a
|
|
145
|
+
revoked token are otherwise indistinguishable from an idle afternoon.
|
|
146
|
+
|
|
147
|
+
### `clocktopus agent disable`
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
clocktopus agent disable [--revoke]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Removes this machine's telemetry configuration, leaving any hooks and
|
|
154
|
+
environment variables you added yourself untouched.
|
|
155
|
+
|
|
156
|
+
| Option | Description |
|
|
157
|
+
| ---------- | ----------------------------------------------------------- |
|
|
158
|
+
| `--revoke` | Also revoke the ingest token, making it useless everywhere. |
|
|
159
|
+
|
|
160
|
+
Stopping and revoking are separate on purpose: removing the local
|
|
161
|
+
configuration stops _this_ machine reporting, but does nothing about a token
|
|
162
|
+
that has already been copied elsewhere or committed.
|
|
163
|
+
|
|
67
164
|
## Token Storage
|
|
68
165
|
|
|
69
166
|
Credentials are stored in a platform-specific config directory:
|
|
@@ -74,3 +171,9 @@ ig.json`
|
|
|
74
171
|
- **Windows**: `%APPDATA%/clocktopus-cli-nodejs/config.json`
|
|
75
172
|
|
|
76
173
|
Run `clocktopus logout` to clear stored credentials.
|
|
174
|
+
|
|
175
|
+
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.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes this machine's telemetry configuration.
|
|
3
|
+
*
|
|
4
|
+
* Stopping and revoking are separate on purpose. Removing the local
|
|
5
|
+
* configuration stops *this* machine reporting; it does nothing about a
|
|
6
|
+
* token that has already been copied elsewhere or committed to a dotfiles
|
|
7
|
+
* repository. `--revoke` is the one that makes the token useless everywhere,
|
|
8
|
+
* and it is opt-in because the common case — moving machines, reinstalling —
|
|
9
|
+
* should not invalidate a token that CI or another workstation still uses.
|
|
10
|
+
*/
|
|
11
|
+
export declare function disableCommand(options: {
|
|
12
|
+
revoke?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=disable.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { clearHookState } from "../../lib/agent-hook-state.js";
|
|
2
|
+
import { ApiError, request } from "../../lib/api.js";
|
|
3
|
+
import { readSettings, removeTelemetrySettings, SettingsParseError, writeSettings, } from "../../lib/claude-settings.js";
|
|
4
|
+
import { clearAgentConfig, getAgentConfig, isLoggedIn, } from "../../lib/config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Removes this machine's telemetry configuration.
|
|
7
|
+
*
|
|
8
|
+
* Stopping and revoking are separate on purpose. Removing the local
|
|
9
|
+
* configuration stops *this* machine reporting; it does nothing about a
|
|
10
|
+
* token that has already been copied elsewhere or committed to a dotfiles
|
|
11
|
+
* repository. `--revoke` is the one that makes the token useless everywhere,
|
|
12
|
+
* and it is opt-in because the common case — moving machines, reinstalling —
|
|
13
|
+
* should not invalidate a token that CI or another workstation still uses.
|
|
14
|
+
*/
|
|
15
|
+
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}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
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.");
|
|
46
|
+
}
|
|
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.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
clearAgentConfig();
|
|
61
|
+
clearHookState();
|
|
62
|
+
if (removedEnvKeys.length === 0 && !removedHooks) {
|
|
63
|
+
console.log("Nothing to remove — agent telemetry was not configured here.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`✓ Removed ${removedEnvKeys.length} environment variable(s)${removedHooks ? " and the session hooks" : ""} from settings.json.`);
|
|
67
|
+
if (!options.revoke) {
|
|
68
|
+
console.log("\nThe ingest token is still valid. Revoke it with\n" +
|
|
69
|
+
" clocktopus agent disable --revoke");
|
|
70
|
+
}
|
|
71
|
+
console.log("\nRestart Claude Code to stop the current session reporting.");
|
|
72
|
+
}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { isAfter, parseISO } from "date-fns";
|
|
3
|
+
import { findShadowedExports, maskToken, resolveAgentCredentials, } from "../../lib/agent-config.js";
|
|
4
|
+
import { readLastRun } from "../../lib/agent-hook-state.js";
|
|
5
|
+
import { verifyReceiver } from "../../lib/agent-receiver.js";
|
|
6
|
+
import { readInstalledTelemetry, SettingsParseError, TELEMETRY_ENV_KEYS, } from "../../lib/claude-settings.js";
|
|
7
|
+
import { isLoggedIn } from "../../lib/config.js";
|
|
8
|
+
import { formatAgo } from "../../lib/format.js";
|
|
9
|
+
import { fetchRepoStatus, repoChecks } from "../../lib/repo-guidance.js";
|
|
10
|
+
export async function doctorCommand() {
|
|
11
|
+
const checks = [];
|
|
12
|
+
checks.push({
|
|
13
|
+
label: "Logged in",
|
|
14
|
+
ok: isLoggedIn(),
|
|
15
|
+
detail: isLoggedIn() ? "yes" : "no",
|
|
16
|
+
fix: "clocktopus login",
|
|
17
|
+
});
|
|
18
|
+
let installed = null;
|
|
19
|
+
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
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof SettingsParseError) {
|
|
32
|
+
checks.push({
|
|
33
|
+
label: "settings.json",
|
|
34
|
+
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;
|
|
42
|
+
}
|
|
43
|
+
const missingEnv = TELEMETRY_ENV_KEYS.filter((key) => !installed?.env[key]);
|
|
44
|
+
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",
|
|
55
|
+
});
|
|
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;
|
|
62
|
+
checks.push({
|
|
63
|
+
label: "Hooks installed",
|
|
64
|
+
ok: Boolean(hookStart && hookEnd),
|
|
65
|
+
detail: hookStart && hookEnd
|
|
66
|
+
? `SessionStart, SessionEnd → ${hookStart}`
|
|
67
|
+
: `missing ${[!hookStart && "SessionStart", !hookEnd && "SessionEnd"].filter(Boolean).join(", ")}`,
|
|
68
|
+
fix: "clocktopus agent setup",
|
|
69
|
+
});
|
|
70
|
+
if (hookStart) {
|
|
71
|
+
checks.push(checkHookRuns(hookStart));
|
|
72
|
+
}
|
|
73
|
+
const credentials = resolveAgentCredentials();
|
|
74
|
+
checks.push({
|
|
75
|
+
label: "Token in force",
|
|
76
|
+
ok: credentials.token !== null,
|
|
77
|
+
detail: credentials.token
|
|
78
|
+
? `${maskToken(credentials.token)} (from ${credentials.tokenSource})`
|
|
79
|
+
: "none resolved",
|
|
80
|
+
fix: "clocktopus agent setup",
|
|
81
|
+
});
|
|
82
|
+
if (credentials.token && credentials.endpoint) {
|
|
83
|
+
const check = await verifyReceiver(credentials.endpoint, credentials.token);
|
|
84
|
+
checks.push({
|
|
85
|
+
label: "Receiver",
|
|
86
|
+
ok: check.ok,
|
|
87
|
+
detail: check.ok
|
|
88
|
+
? `${credentials.endpoint} accepted the token`
|
|
89
|
+
: check.reason === "invalid_token"
|
|
90
|
+
? `${credentials.endpoint} rejected the token (401)`
|
|
91
|
+
: check.reason === "unexpected_status"
|
|
92
|
+
? `${credentials.endpoint} answered HTTP ${check.status}`
|
|
93
|
+
: `${credentials.endpoint} unreachable — ${check.message}`,
|
|
94
|
+
fix: check.ok || check.reason !== "invalid_token"
|
|
95
|
+
? "Check the endpoint is reachable from this network"
|
|
96
|
+
: "clocktopus agent setup --force (mints a replacement token)",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
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);
|
|
109
|
+
}
|
|
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
|
+
*/
|
|
120
|
+
function checkHookRuns(command) {
|
|
121
|
+
try {
|
|
122
|
+
execSync(command, {
|
|
123
|
+
input: "",
|
|
124
|
+
timeout: 15_000,
|
|
125
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
label: "Hook executes",
|
|
129
|
+
ok: true,
|
|
130
|
+
detail: "command resolved and ran",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
label: "Hook executes",
|
|
136
|
+
ok: false,
|
|
137
|
+
detail: `'${command}' failed — ${error instanceof Error ? error.message.split("\n")[0] : "unknown error"}`,
|
|
138
|
+
fix: "clocktopus agent setup (rewrites the hook with a resolvable path)",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function checkShadowedExports() {
|
|
143
|
+
const shadowed = findShadowedExports();
|
|
144
|
+
if (shadowed.length === 0) {
|
|
145
|
+
return {
|
|
146
|
+
label: "Config conflicts",
|
|
147
|
+
ok: true,
|
|
148
|
+
detail: "settings.json is the only source",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
label: "Config conflicts",
|
|
153
|
+
ok: "warn",
|
|
154
|
+
detail: shadowed
|
|
155
|
+
.map(({ path, keys }) => `${path} exports ${keys.join(", ")}`)
|
|
156
|
+
.join("; "),
|
|
157
|
+
fix: "settings.json is applied over the inherited environment, so these are\n" +
|
|
158
|
+
" ignored inside Claude Code. Remove them — two sources that\n" +
|
|
159
|
+
" disagree is how spend lands on the wrong account.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
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) {
|
|
170
|
+
const lastRun = readLastRun();
|
|
171
|
+
if (!settingsModifiedAt || !lastRun?.at) {
|
|
172
|
+
return {
|
|
173
|
+
label: "Restart",
|
|
174
|
+
ok: "warn",
|
|
175
|
+
detail: "the hook has not run since this configuration was written",
|
|
176
|
+
fix: "Restart Claude Code, then run 'clocktopus agent status'",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const ranAfterConfig = isAfter(parseISO(lastRun.at), settingsModifiedAt);
|
|
180
|
+
return {
|
|
181
|
+
label: "Restart",
|
|
182
|
+
ok: ranAfterConfig ? true : "warn",
|
|
183
|
+
detail: ranAfterConfig
|
|
184
|
+
? "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",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function checkLastHookRun() {
|
|
190
|
+
const lastRun = readLastRun();
|
|
191
|
+
if (!lastRun) {
|
|
192
|
+
return {
|
|
193
|
+
label: "Last hook run",
|
|
194
|
+
ok: "warn",
|
|
195
|
+
detail: "no record — the hook has not run on this machine",
|
|
196
|
+
fix: "Restart Claude Code and start a session",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const failed = lastRun.status === null ||
|
|
200
|
+
lastRun.status === undefined ||
|
|
201
|
+
lastRun.status < 200 ||
|
|
202
|
+
lastRun.status >= 300;
|
|
203
|
+
return {
|
|
204
|
+
label: "Last hook run",
|
|
205
|
+
ok: !failed,
|
|
206
|
+
detail: failed
|
|
207
|
+
? `${formatAgo(lastRun.at)} — ${lastRun.status ? `HTTP ${lastRun.status}` : (lastRun.error ?? "no response")}`
|
|
208
|
+
: `${formatAgo(lastRun.at)} — HTTP ${lastRun.status}`,
|
|
209
|
+
fix: lastRun.status === 401
|
|
210
|
+
? "clocktopus agent setup --force (the token was rejected)"
|
|
211
|
+
: "Check network access to the receiver",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
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}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const failures = checks.filter((check) => check.ok === false).length;
|
|
224
|
+
const warnings = checks.filter((check) => check.ok === "warn").length;
|
|
225
|
+
console.log("");
|
|
226
|
+
if (failures === 0 && warnings === 0) {
|
|
227
|
+
console.log("Everything checks out. 'clocktopus agent status' shows what has arrived.");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`${failures} failing, ${warnings} to review. Fix the first failure above —\n` +
|
|
231
|
+
"the later ones are usually consequences of it.");
|
|
232
|
+
}
|
|
233
|
+
if (failures > 0)
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
@@ -0,0 +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"}
|