@blastin-dev/clocktopus-cli 0.1.3 → 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.
Files changed (56) hide show
  1. package/README.md +124 -6
  2. package/dist/src/commands/agent/disable.d.ts +14 -0
  3. package/dist/src/commands/agent/disable.d.ts.map +1 -0
  4. package/dist/src/commands/agent/disable.js +72 -0
  5. package/dist/src/commands/agent/doctor.d.ts +2 -0
  6. package/dist/src/commands/agent/doctor.d.ts.map +1 -0
  7. package/dist/src/commands/agent/doctor.js +235 -0
  8. package/dist/src/commands/agent/hook.d.ts +2 -0
  9. package/dist/src/commands/agent/hook.d.ts.map +1 -0
  10. package/dist/src/commands/agent/hook.js +231 -0
  11. package/dist/src/commands/agent/setup.d.ts +21 -0
  12. package/dist/src/commands/agent/setup.d.ts.map +1 -0
  13. package/dist/src/commands/agent/setup.js +194 -0
  14. package/dist/src/commands/agent/status.d.ts +2 -0
  15. package/dist/src/commands/agent/status.d.ts.map +1 -0
  16. package/dist/src/commands/agent/status.js +160 -0
  17. package/dist/src/commands/clock.d.ts +26 -4
  18. package/dist/src/commands/clock.d.ts.map +1 -1
  19. package/dist/src/commands/clock.js +99 -6
  20. package/dist/src/commands/login.d.ts.map +1 -1
  21. package/dist/src/commands/login.js +5 -5
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +45 -6
  24. package/dist/src/lib/agent-config.d.ts +41 -0
  25. package/dist/src/lib/agent-config.d.ts.map +1 -0
  26. package/dist/src/lib/agent-config.js +143 -0
  27. package/dist/src/lib/agent-hook-state.d.ts +36 -0
  28. package/dist/src/lib/agent-hook-state.d.ts.map +1 -0
  29. package/dist/src/lib/agent-hook-state.js +136 -0
  30. package/dist/src/lib/agent-receiver.d.ts +26 -0
  31. package/dist/src/lib/agent-receiver.d.ts.map +1 -0
  32. package/dist/src/lib/agent-receiver.js +44 -0
  33. package/dist/src/lib/api.d.ts.map +1 -1
  34. package/dist/src/lib/api.js +28 -1
  35. package/dist/src/lib/claude-settings.d.ts +82 -0
  36. package/dist/src/lib/claude-settings.d.ts.map +1 -0
  37. package/dist/src/lib/claude-settings.js +271 -0
  38. package/dist/src/lib/claude-settings.test.d.ts +2 -0
  39. package/dist/src/lib/claude-settings.test.d.ts.map +1 -0
  40. package/dist/src/lib/claude-settings.test.js +193 -0
  41. package/dist/src/lib/config.d.ts +23 -0
  42. package/dist/src/lib/config.d.ts.map +1 -1
  43. package/dist/src/lib/config.js +14 -0
  44. package/dist/src/lib/format.d.ts +6 -0
  45. package/dist/src/lib/format.d.ts.map +1 -0
  46. package/dist/src/lib/format.js +19 -0
  47. package/dist/src/lib/git.d.ts +3 -0
  48. package/dist/src/lib/git.d.ts.map +1 -0
  49. package/dist/src/lib/git.js +30 -0
  50. package/dist/src/lib/repo-guidance.d.ts +40 -0
  51. package/dist/src/lib/repo-guidance.d.ts.map +1 -0
  52. package/dist/src/lib/repo-guidance.js +123 -0
  53. package/dist/src/lib/validators.d.ts +69 -0
  54. package/dist/src/lib/validators.d.ts.map +1 -1
  55. package/dist/src/lib/validators.js +69 -0
  56. package/package.json +7 -5
package/README.md CHANGED
@@ -38,24 +38,142 @@ Clear stored credentials.
38
38
 
39
39
  Display the current authenticated user.
40
40
 
41
- ### `clocktopus clock in`
41
+ ### `clocktopus clock (in|out)`
42
42
 
43
- Record a clock-in signal.
43
+ ```
44
+ clocktopus clock (in|out) [--ago <duration> | --at <time>]
45
+ ```
44
46
 
45
- ### `clocktopus clock out`
47
+ Record the start or end of a work session. Defaults to "now" when no
48
+ flag is passed.
46
49
 
47
- Record a clock-out signal.
50
+ | Option | Description |
51
+ | ------------------ | --------------------------------------------------------------------------------------- |
52
+ | `--ago <duration>` | Backdate by a duration from now (`15m`, `1h`, `1h30m`). Mutually exclusive with `--at`. |
53
+ | `--at <time>` | Backdate to an absolute wall-clock time today (`HH:mm` or `HH:mm:ss`). |
48
54
 
49
55
  ### `clocktopus clock status`
50
56
 
51
- Show signals for a given date.
57
+ ```
58
+ clocktopus clock status [-d <YYYY-MM-DD>]
59
+ ```
60
+
61
+ Show clock signals for a given date.
62
+
63
+ | Option | Description |
64
+ | ------------------- | --------------------------------------------------------------------------- |
65
+ | `-d, --date <date>` | Show signals for a specific date in `YYYY-MM-DD` format. Defaults to today. |
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.
52
163
 
53
164
  ## Token Storage
54
165
 
55
166
  Credentials are stored in a platform-specific config directory:
56
167
 
57
- - **macOS**: `~/Library/Preferences/clocktopus-cli-nodejs/config.json`
168
+ - **macOS**: `~/Library/Preferences/clocktopus-cli-nodejs/conf
169
+ ig.json`
58
170
  - **Linux**: `~/.config/clocktopus-cli-nodejs/config.json`
59
171
  - **Windows**: `%APPDATA%/clocktopus-cli-nodejs/config.json`
60
172
 
61
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,2 @@
1
+ export declare function doctorCommand(): Promise<void>;
2
+ //# sourceMappingURL=doctor.d.ts.map
@@ -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,2 @@
1
+ export declare function hookCommand(): Promise<void>;
2
+ //# sourceMappingURL=hook.d.ts.map
@@ -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"}