@blastin-dev/clocktopus-cli 0.1.4 → 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.
- package/README.md +163 -5
- package/dist/src/commands/agent/disable.d.ts +21 -0
- package/dist/src/commands/agent/disable.d.ts.map +1 -0
- package/dist/src/commands/agent/disable.js +106 -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 +306 -0
- package/dist/src/commands/agent/hook.d.ts +5 -0
- package/dist/src/commands/agent/hook.d.ts.map +1 -0
- package/dist/src/commands/agent/hook.js +368 -0
- package/dist/src/commands/agent/setup.d.ts +28 -0
- package/dist/src/commands/agent/setup.d.ts.map +1 -0
- package/dist/src/commands/agent/setup.js +333 -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 +182 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +51 -2
- package/dist/src/lib/agent-config.d.ts +56 -0
- package/dist/src/lib/agent-config.d.ts.map +1 -0
- package/dist/src/lib/agent-config.js +168 -0
- package/dist/src/lib/agent-hook-state.d.ts +44 -0
- package/dist/src/lib/agent-hook-state.d.ts.map +1 -0
- package/dist/src/lib/agent-hook-state.js +155 -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/agents.d.ts +115 -0
- package/dist/src/lib/agents.d.ts.map +1 -0
- package/dist/src/lib/agents.js +245 -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/codex-config.d.ts +166 -0
- package/dist/src/lib/codex-config.d.ts.map +1 -0
- package/dist/src/lib/codex-config.js +441 -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/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/opencode-config.d.ts +108 -0
- package/dist/src/lib/opencode-config.d.ts.map +1 -0
- package/dist/src/lib/opencode-config.js +330 -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/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 +7 -4
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { maskToken, resolveAgentCredentials } from "../../lib/agent-config.js";
|
|
4
|
+
import { clearStartState, listStartStates, readStartState, recordLastRun, writeStartState, } from "../../lib/agent-hook-state.js";
|
|
5
|
+
import { AGENTS } from "../../lib/agents.js";
|
|
6
|
+
/**
|
|
7
|
+
* SessionStart / SessionEnd hook → Clocktopus. Serves every agent.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code and Codex both pipe their hook JSON to stdin, and — verified
|
|
10
|
+
* against 2.1.234 and 0.147.0 — they pipe the *same* JSON: `session_id`,
|
|
11
|
+
* `cwd`, `hook_event_name`, `source`, `reason`, same spellings. That is why
|
|
12
|
+
* one subcommand handles both, and also why `--provider` exists: nothing in
|
|
13
|
+
* the payload says which agent sent it, so the answer has to come from how
|
|
14
|
+
* the hook was installed.
|
|
15
|
+
*
|
|
16
|
+
* That JSON is the only place `cwd` appears anywhere in the telemetry
|
|
17
|
+
* pipeline — neither agent's telemetry export carries path, repository or
|
|
18
|
+
* branch data — so without this hook every agent session is unattributable
|
|
19
|
+
* and the ledger can only show totals.
|
|
20
|
+
*
|
|
21
|
+
* Previously a standalone script under `scripts/agent-telemetry/`, which
|
|
22
|
+
* meant `settings.json` had to reference an absolute path inside a checkout
|
|
23
|
+
* of this repository. Shipping it as a subcommand is what makes the feature
|
|
24
|
+
* installable by anyone: the hook is wherever the CLI is.
|
|
25
|
+
*
|
|
26
|
+
* Contract with the host process, all three parts load-bearing: never write
|
|
27
|
+
* to stdout (Claude Code parses it), never throw, always exit 0. Telemetry
|
|
28
|
+
* must not be able to break the session it measures.
|
|
29
|
+
*
|
|
30
|
+
* ## Getting off the session's critical path
|
|
31
|
+
*
|
|
32
|
+
* This hook does network I/O — its own POST, plus a sweep of abandoned
|
|
33
|
+
* sessions at SessionStart — and none of it may be charged to the developer
|
|
34
|
+
* waiting for their session to start.
|
|
35
|
+
*
|
|
36
|
+
* Claude Code solves this for us: `"async": true` in `settings.json` makes
|
|
37
|
+
* it background the hook. Codex does not, or not reliably — behaviour
|
|
38
|
+
* verified by running both binaries:
|
|
39
|
+
*
|
|
40
|
+
* | `async: true` on | Codex 0.147 | Codex 0.148 |
|
|
41
|
+
* | ---------------- | ------------------------------ | -------------------- |
|
|
42
|
+
* | SessionStart | **skips the hook**, with a warning | honoured, no warning |
|
|
43
|
+
* | SessionEnd | runs it synchronously, with a warning | unchanged |
|
|
44
|
+
*
|
|
45
|
+
* The 0.147 row is the dangerous one: a skipped SessionStart costs every
|
|
46
|
+
* Codex session its `cwd`, and with it the repository, the branch and the
|
|
47
|
+
* starting SHA — spend keeps arriving, attributed to nothing, with only a
|
|
48
|
+
* startup warning to say so.
|
|
49
|
+
*
|
|
50
|
+
* Rather than sniff the version, the Codex hooks carry no `async` key at
|
|
51
|
+
* all and this command backgrounds *itself*: it relays the payload to a
|
|
52
|
+
* detached copy and returns in a few milliseconds. One mechanism, correct
|
|
53
|
+
* on every version, and silent on all of them.
|
|
54
|
+
*
|
|
55
|
+
* SessionEnd needs it regardless of version. Codex still hard-clamps that
|
|
56
|
+
* hook to 3s — shorter than this file's own 4s request timeout — so a slow
|
|
57
|
+
* network would see the hook killed mid-POST and the session never closed.
|
|
58
|
+
* The clamp only has to cover a spawn.
|
|
59
|
+
*/
|
|
60
|
+
const TIMEOUT_MS = 4000;
|
|
61
|
+
/**
|
|
62
|
+
* How long a state file must sit untouched before its session is presumed
|
|
63
|
+
* dead. A file only survives SessionEnd if the session never got one —
|
|
64
|
+
* SIGKILL, a crash, or the machine going down — but a long-running session
|
|
65
|
+
* is perfectly normal, so this sits well past a working day rather than at
|
|
66
|
+
* the point a session merely looks idle.
|
|
67
|
+
*/
|
|
68
|
+
const ABANDONED_AFTER_MS = 12 * 60 * 60 * 1000;
|
|
69
|
+
/** Bound on one sweep, so a directory left full of state cannot stall a start. */
|
|
70
|
+
const MAX_SWEEP_PER_RUN = 10;
|
|
71
|
+
function git(cwd, args) {
|
|
72
|
+
try {
|
|
73
|
+
return execFileSync("git", args, {
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
77
|
+
timeout: 2000,
|
|
78
|
+
}).trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function post(endpoint, token, body) {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${endpoint.replace(/\/$/, "")}/v1/agent-session`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
authorization: `Bearer ${token}`,
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify(body),
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
});
|
|
97
|
+
return { status: response.status, error: null };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// Telemetry must never break the session it measures. The failure is
|
|
101
|
+
// recorded for `agent doctor` instead of surfaced here.
|
|
102
|
+
return {
|
|
103
|
+
status: null,
|
|
104
|
+
error: error instanceof Error ? error.message : "request failed",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Closes sessions that were never given a SessionEnd.
|
|
113
|
+
*
|
|
114
|
+
* A state file outlives its session only when the process died without
|
|
115
|
+
* running the hook, and until something closes it the session keeps a null
|
|
116
|
+
* `ended_at` and a null `git_head_after` forever.
|
|
117
|
+
*
|
|
118
|
+
* It deliberately does **not** send `commit_shas`. The honest range for a
|
|
119
|
+
* dead session is unknowable after the fact: `before..HEAD` also contains
|
|
120
|
+
* everything committed in the hours since it died, and the receiver records
|
|
121
|
+
* hook-supplied commits as `declared`, which outranks a `time_window` link
|
|
122
|
+
* and would make a bad guess permanent. Closing the session is enough —
|
|
123
|
+
* with a real `ended_at` the webhook's time-window pass can then link its
|
|
124
|
+
* commits from the commit timestamps, which are bounded by data rather than
|
|
125
|
+
* by assumption.
|
|
126
|
+
*
|
|
127
|
+
* For the same reason it sends its own `ended_at` rather than letting the
|
|
128
|
+
* receiver stamp one: this runs when the *next* session starts, potentially
|
|
129
|
+
* a day later.
|
|
130
|
+
*/
|
|
131
|
+
async function sweepAbandonedSessions(endpoint, token, currentSessionId, fallbackProvider) {
|
|
132
|
+
let swept = 0;
|
|
133
|
+
for (const { sessionId, ageMs, lastActivityAt } of listStartStates()) {
|
|
134
|
+
if (swept >= MAX_SWEEP_PER_RUN)
|
|
135
|
+
return;
|
|
136
|
+
if (sessionId === currentSessionId)
|
|
137
|
+
continue;
|
|
138
|
+
if (ageMs < ABANDONED_AFTER_MS)
|
|
139
|
+
continue;
|
|
140
|
+
swept++;
|
|
141
|
+
const state = readStartState(sessionId);
|
|
142
|
+
// Nothing recoverable without the originating checkout — drop the file
|
|
143
|
+
// rather than resolve its SHA against an unrelated repository.
|
|
144
|
+
if (!state?.cwd) {
|
|
145
|
+
clearStartState(sessionId);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Kept alive only so a repeating SessionEnd could still diff against it.
|
|
149
|
+
// The session already ended properly; sweeping it would replace a real
|
|
150
|
+
// end with this moment's HEAD. Just collect the file.
|
|
151
|
+
if (state.closed) {
|
|
152
|
+
clearStartState(sessionId);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const head = git(state.cwd, ["rev-parse", "HEAD"]);
|
|
156
|
+
if (head) {
|
|
157
|
+
await post(endpoint, token, {
|
|
158
|
+
// The agent that *opened* the session, not the one sweeping it.
|
|
159
|
+
// Closing a Codex session as `claude_code` would leave the real row
|
|
160
|
+
// hanging and create an empty duplicate under the wrong provider.
|
|
161
|
+
provider: readProvider(state.provider) ?? fallbackProvider,
|
|
162
|
+
session_id: sessionId,
|
|
163
|
+
hook_event_name: "SessionEnd",
|
|
164
|
+
cwd: state.cwd,
|
|
165
|
+
reason: "abandoned",
|
|
166
|
+
// Never "now": this session died hours ago, and the receiver would
|
|
167
|
+
// otherwise record a window spanning everything since. The receiver
|
|
168
|
+
// treats this as a fallback and keeps a telemetry-stamped end if it
|
|
169
|
+
// has one; this value only fills the gap when it has none.
|
|
170
|
+
ended_at: lastActivityAt.toISOString(),
|
|
171
|
+
repository_url: git(state.cwd, ["remote", "get-url", "origin"]),
|
|
172
|
+
git_branch: git(state.cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
173
|
+
git_head_before: state.sha,
|
|
174
|
+
git_head_after: head,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
clearStartState(sessionId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reads a provider back off a state file written by an older CLI.
|
|
182
|
+
*
|
|
183
|
+
* Those files have no `provider` at all, and the only agent that could have
|
|
184
|
+
* written one was Claude Code — so `undefined` is not a missing value here,
|
|
185
|
+
* it is `claude_code`. That is the caller's fallback, applied by returning
|
|
186
|
+
* null rather than guessing here.
|
|
187
|
+
*/
|
|
188
|
+
function readProvider(value) {
|
|
189
|
+
const match = AGENTS.find((agent) => agent.provider === value);
|
|
190
|
+
return match?.provider ?? null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Re-runs this command in a detached copy and hands it the payload.
|
|
194
|
+
*
|
|
195
|
+
* `detached: true` is what makes it survive: it puts the child in its own
|
|
196
|
+
* process group, so the host tearing down the session's group at exit —
|
|
197
|
+
* which is exactly when SessionEnd fires — cannot take the POST with it.
|
|
198
|
+
*
|
|
199
|
+
* Waiting for `stdin.end` to flush is not optional. The parent returns
|
|
200
|
+
* immediately afterwards, and an unflushed write would die with it; a hook
|
|
201
|
+
* payload is well under the pipe buffer, so this resolves without waiting
|
|
202
|
+
* for the child to read anything.
|
|
203
|
+
*
|
|
204
|
+
* Returns false if the spawn could not be arranged at all, in which case
|
|
205
|
+
* the caller does the work inline — a slow session start is worth more than
|
|
206
|
+
* a lost one.
|
|
207
|
+
*/
|
|
208
|
+
function detachSelf(provider, payload) {
|
|
209
|
+
const script = process.argv[1] ? resolvePath(process.argv[1]) : null;
|
|
210
|
+
if (!script)
|
|
211
|
+
return Promise.resolve(false);
|
|
212
|
+
try {
|
|
213
|
+
const child = spawn(process.execPath, [script, "agent", "hook", "--provider", provider, "--detached"], { detached: true, stdio: ["pipe", "ignore", "ignore"] });
|
|
214
|
+
child.unref();
|
|
215
|
+
return new Promise((settle) => {
|
|
216
|
+
child.once("error", () => settle(false));
|
|
217
|
+
child.stdin.once("error", () => settle(false));
|
|
218
|
+
child.stdin.end(payload, () => settle(true));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return Promise.resolve(false);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function readStdin() {
|
|
226
|
+
const chunks = [];
|
|
227
|
+
for await (const chunk of process.stdin)
|
|
228
|
+
chunks.push(chunk);
|
|
229
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
230
|
+
}
|
|
231
|
+
async function run(agent, detached) {
|
|
232
|
+
const { token, endpoint } = resolveAgentCredentials(agent.id);
|
|
233
|
+
if (!token || !endpoint) {
|
|
234
|
+
// Not configured. Silence is correct: agent telemetry is opt-in, and a
|
|
235
|
+
// machine that never ran `clocktopus agent setup` should notice nothing.
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const raw = await readStdin();
|
|
239
|
+
if (!raw)
|
|
240
|
+
return;
|
|
241
|
+
// After the empty-stdin check on purpose: `agent doctor` proves the hook
|
|
242
|
+
// command resolves by running it with no payload, and that probe must not
|
|
243
|
+
// leave a stray process behind.
|
|
244
|
+
if (!agent.hostRunsHooksAsync && !detached) {
|
|
245
|
+
if (await detachSelf(agent.provider, raw))
|
|
246
|
+
return;
|
|
247
|
+
// Could not spawn — fall through and do it inline. A slow session start
|
|
248
|
+
// is worth more than a lost session.
|
|
249
|
+
}
|
|
250
|
+
let payload;
|
|
251
|
+
try {
|
|
252
|
+
payload = JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : null;
|
|
258
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
259
|
+
if (!sessionId || !cwd)
|
|
260
|
+
return;
|
|
261
|
+
const eventName = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
262
|
+
const isSessionStart = eventName === "SessionStart";
|
|
263
|
+
// HEAD at SessionStart is the *before* SHA; HEAD at SessionEnd is the
|
|
264
|
+
// *after*. Sending both under one field would let SessionEnd overwrite the
|
|
265
|
+
// start SHA, collapsing the range to a single point and making
|
|
266
|
+
// `git rev-list before..after` — the authoritative commit↔session link —
|
|
267
|
+
// impossible to compute.
|
|
268
|
+
const head = git(cwd, ["rev-parse", "HEAD"]);
|
|
269
|
+
const repositoryUrl = git(cwd, ["remote", "get-url", "origin"]);
|
|
270
|
+
const body = {
|
|
271
|
+
provider: agent.provider,
|
|
272
|
+
session_id: sessionId,
|
|
273
|
+
hook_event_name: eventName,
|
|
274
|
+
cwd,
|
|
275
|
+
source: typeof payload.source === "string" ? payload.source : undefined,
|
|
276
|
+
reason: typeof payload.reason === "string" ? payload.reason : undefined,
|
|
277
|
+
repository_url: repositoryUrl,
|
|
278
|
+
git_branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
279
|
+
...(isSessionStart ? { git_head_before: head } : { git_head_after: head }),
|
|
280
|
+
};
|
|
281
|
+
// Resolve the SHA range into an actual commit list, here on the machine
|
|
282
|
+
// that has the git object graph. The receiver cannot do this — it has no
|
|
283
|
+
// checkout — so a range alone would be unusable.
|
|
284
|
+
//
|
|
285
|
+
// SessionStart and SessionEnd are separate processes, so the starting SHA
|
|
286
|
+
// is stashed on disk under the session id rather than passed in the
|
|
287
|
+
// environment.
|
|
288
|
+
if (isSessionStart) {
|
|
289
|
+
if (head)
|
|
290
|
+
writeStartState(sessionId, { sha: head, cwd, provider: agent.provider });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const before = readStartState(sessionId)?.sha;
|
|
294
|
+
if (before && head && before !== head) {
|
|
295
|
+
const shas = git(cwd, [
|
|
296
|
+
"rev-list",
|
|
297
|
+
"--max-count=200",
|
|
298
|
+
`${before}..${head}`,
|
|
299
|
+
]);
|
|
300
|
+
if (shas)
|
|
301
|
+
body.commit_shas = shas.split("\n").filter(Boolean);
|
|
302
|
+
}
|
|
303
|
+
if (before)
|
|
304
|
+
body.git_head_before = before;
|
|
305
|
+
// A host that ends a session once is done with this file. One that can
|
|
306
|
+
// end it repeatedly — OpenCode, whose SessionEnd is `session.idle` after
|
|
307
|
+
// every turn — is not: deleting it here would cost the session every
|
|
308
|
+
// commit made after its first pause, because the next idle would have no
|
|
309
|
+
// starting SHA to diff against. Rewriting the *original* SHA keeps the
|
|
310
|
+
// range anchored at the session's start, so each idle re-declares the
|
|
311
|
+
// whole range; re-declaring is free, since a link is unique per commit
|
|
312
|
+
// and `declared` already outranks everything else.
|
|
313
|
+
//
|
|
314
|
+
// Rewriting rather than leaving it also refreshes the file's mtime, which
|
|
315
|
+
// is what the sweep ages sessions by — so the 12h abandonment clock runs
|
|
316
|
+
// from the last turn rather than from session start, and a long
|
|
317
|
+
// conversation is not swept out from under itself.
|
|
318
|
+
if (agent.hostRepeatsSessionEnd && before) {
|
|
319
|
+
writeStartState(sessionId, {
|
|
320
|
+
sha: before,
|
|
321
|
+
cwd,
|
|
322
|
+
provider: agent.provider,
|
|
323
|
+
// Already closed in the database. The sweep needs to know that, or
|
|
324
|
+
// it would "close" the session a second time 12h later and stamp it
|
|
325
|
+
// with whatever HEAD had become by then.
|
|
326
|
+
closed: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
clearStartState(sessionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// transcript_path is intentionally NOT forwarded. Claude Code provides it,
|
|
334
|
+
// and it points at the full conversation on disk — exactly the content
|
|
335
|
+
// BLA-421 says we must never collect.
|
|
336
|
+
const result = await post(endpoint, token, body);
|
|
337
|
+
recordLastRun({
|
|
338
|
+
at: new Date().toISOString(),
|
|
339
|
+
event: eventName,
|
|
340
|
+
provider: agent.provider,
|
|
341
|
+
sessionId,
|
|
342
|
+
endpoint,
|
|
343
|
+
tokenPrefix: maskToken(token),
|
|
344
|
+
status: result.status,
|
|
345
|
+
error: result.error,
|
|
346
|
+
repository: repositoryUrl ?? null,
|
|
347
|
+
});
|
|
348
|
+
// After this session's own event, never before it: the sweep talks to the
|
|
349
|
+
// network once per abandoned session and must not delay the event it is
|
|
350
|
+
// piggybacking on.
|
|
351
|
+
if (isSessionStart) {
|
|
352
|
+
await sweepAbandonedSessions(endpoint, token, sessionId, agent.provider);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export async function hookCommand(options) {
|
|
356
|
+
try {
|
|
357
|
+
// An unrecognised provider falls back to Claude Code rather than
|
|
358
|
+
// aborting: a hook installed by an older CLI has no flag at all, and
|
|
359
|
+
// dropping its sessions would be a worse failure than mislabelling a
|
|
360
|
+
// provider we do not know.
|
|
361
|
+
const agent = AGENTS.find((candidate) => candidate.provider === options.provider) ??
|
|
362
|
+
AGENTS[0];
|
|
363
|
+
await run(agent, options.detached === true);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Every failure path is silent by design.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configures this machine's coding agents to report spend to Clocktopus.
|
|
3
|
+
*
|
|
4
|
+
* One command, however many agents are installed. It looks for each
|
|
5
|
+
* supported CLI on PATH and configures what it finds — silently when
|
|
6
|
+
* there is only one, after a prompt when there are several, and from
|
|
7
|
+
* `--agent` when there is neither a person nor a terminal to ask.
|
|
8
|
+
*
|
|
9
|
+
* Everything for a given agent lands in that agent's own config, and
|
|
10
|
+
* nowhere else. A single source of truth per agent is not a tidiness
|
|
11
|
+
* preference: it is what makes `agent doctor` able to say which value is in
|
|
12
|
+
* force. Configuration spread over `.envrc`, a shell profile and a CLI
|
|
13
|
+
* config file can be reported on but never resolved, and this project
|
|
14
|
+
* already lost real spend to exactly that (one session split across two
|
|
15
|
+
* accounts, silently, because two files disagreed about the token).
|
|
16
|
+
*
|
|
17
|
+
* Idempotent by default. Re-running repairs the hooks and refreshes the
|
|
18
|
+
* endpoint while keeping the existing token, so the common case — "did my
|
|
19
|
+
* setup drift?" — costs nothing, and adding a newly-installed agent later
|
|
20
|
+
* reuses the token the first one already has. `--force` mints a
|
|
21
|
+
* replacement instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setupCommand(options: {
|
|
24
|
+
name?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
agent?: string[];
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/setup.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8LhB"}
|