@blastin-dev/clocktopus-cli 0.3.0 → 0.4.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 +12 -4
- package/dist/src/commands/agent/doctor.js +13 -0
- package/dist/src/commands/agent/status.d.ts +12 -0
- package/dist/src/commands/agent/status.d.ts.map +1 -1
- package/dist/src/commands/agent/status.js +28 -4
- package/dist/src/commands/agent/status.test.d.ts +2 -0
- package/dist/src/commands/agent/status.test.d.ts.map +1 -0
- package/dist/src/commands/agent/status.test.js +58 -0
- package/dist/src/lib/agents.d.ts +1 -0
- package/dist/src/lib/agents.d.ts.map +1 -1
- package/dist/src/lib/agents.js +7 -0
- package/dist/src/lib/auth.d.ts.map +1 -1
- package/dist/src/lib/auth.js +11 -0
- package/dist/src/lib/claude-settings.d.ts +1 -1
- package/dist/src/lib/claude-settings.d.ts.map +1 -1
- package/dist/src/lib/claude-settings.js +7 -0
- package/dist/src/lib/claude-settings.test.js +25 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -182,10 +182,18 @@ one that silently understates what work cost.
|
|
|
182
182
|
### `clocktopus agent doctor`
|
|
183
183
|
|
|
184
184
|
Checks every link in the chain, per agent, and names the first one that is
|
|
185
|
-
broken: the config file parses, the exporter is configured,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
configuration, and whether that agent
|
|
185
|
+
broken: the config file parses, the exporter is configured, every signal the
|
|
186
|
+
receiver reads is being sent, both hooks are installed, the hook command
|
|
187
|
+
actually executes, Codex has approved its hooks, the receiver accepts the
|
|
188
|
+
token, no other file is shadowing the configuration, and whether that agent
|
|
189
|
+
needs restarting.
|
|
190
|
+
|
|
191
|
+
The signals check is there for configurations written before a signal
|
|
192
|
+
existed. Those still hold a valid token and endpoint, so everything else
|
|
193
|
+
passes while one stream never leaves the machine — a Claude Code install
|
|
194
|
+
predating BLA-671 reports cost correctly and no sub-agent runtime at all.
|
|
195
|
+
Re-running `agent setup` merges the missing keys in; nothing needs removing
|
|
196
|
+
first. The repository
|
|
189
197
|
checks — attached to a project, delivering commits — run once at the end,
|
|
190
198
|
since they are facts about the repo rather than about any one agent.
|
|
191
199
|
|
|
@@ -90,6 +90,19 @@ async function agentChecks(entry) {
|
|
|
90
90
|
: `not configured in ${state.paths.join(", ")}`,
|
|
91
91
|
fix: `clocktopus agent setup --agent ${entry.agent.id}`,
|
|
92
92
|
});
|
|
93
|
+
// A configuration written before a lane existed still has a token and an endpoint, so
|
|
94
|
+
// every other check here passes while one signal never leaves the machine. Only worth
|
|
95
|
+
// saying once the agent is configured at all — otherwise the check above covers it.
|
|
96
|
+
if (state.token !== null) {
|
|
97
|
+
checks.push({
|
|
98
|
+
label: "Signals",
|
|
99
|
+
ok: state.missingLanes.length === 0,
|
|
100
|
+
detail: state.missingLanes.length === 0
|
|
101
|
+
? "every signal the receiver reads is configured"
|
|
102
|
+
: `not being sent: ${state.missingLanes.join(", ")}`,
|
|
103
|
+
fix: `clocktopus agent setup --agent ${entry.agent.id} — this configuration predates it`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
93
106
|
// Both events are required, for different reasons: SessionStart is the only source of the
|
|
94
107
|
// *before* SHA and of `cwd`, SessionEnd the only source of the exact commit list. One
|
|
95
108
|
// without the other silently degrades what can be attributed.
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
export declare function statusCommand(): Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* The asymmetric-failure callouts — the reason the lanes are split.
|
|
4
|
+
*
|
|
5
|
+
* Each names the single next action, because "one lane is quiet" is only
|
|
6
|
+
* actionable once you know which half of the pipeline it implicates.
|
|
7
|
+
*/
|
|
8
|
+
export declare function diagnose(input: {
|
|
9
|
+
hookAge: number | null;
|
|
10
|
+
metricsAge: number | null;
|
|
11
|
+
unattributedSessions: number;
|
|
12
|
+
sessions: number;
|
|
13
|
+
}): string[];
|
|
2
14
|
//# sourceMappingURL=status.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/status.ts"],"names":[],"mappings":"AA2BA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/status.ts"],"names":[],"mappings":"AA2BA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA4FnD;AAgBD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,EAAE,CAgDX"}
|
|
@@ -53,7 +53,7 @@ export async function statusCommand() {
|
|
|
53
53
|
console.log(labelled("", `${where}${branch}`, 14));
|
|
54
54
|
}
|
|
55
55
|
if (status.hookLane.attributed === false) {
|
|
56
|
-
console.log(labelled("", "not
|
|
56
|
+
console.log(labelled("", "this repository is not attached to a project", 14));
|
|
57
57
|
}
|
|
58
58
|
console.log(labelled("Spend lane", `${mark(metricsAge)} ${formatAgo(status.metricsLane.lastExportReceivedAt)}`, 14));
|
|
59
59
|
if (status.metricsLane.lastModel) {
|
|
@@ -62,8 +62,11 @@ export async function statusCommand() {
|
|
|
62
62
|
console.log("\nLast 7 days\n");
|
|
63
63
|
console.log(labelled("Sessions", String(status.window.sessions)));
|
|
64
64
|
console.log(labelled("Spend", microUsdToDisplay(status.window.costMicroUsd)));
|
|
65
|
+
// Deliberately not naming a cause. There are four, they are per-repository, and
|
|
66
|
+
// this figure is a `project_id IS NULL` count that knows none of them — only
|
|
67
|
+
// `agent doctor`, standing in the checkout, can say which one applies.
|
|
65
68
|
if (status.window.unattributedSessions > 0) {
|
|
66
|
-
console.log(labelled("Unattributed", `${status.window.unattributedSessions} session(s)
|
|
69
|
+
console.log(labelled("Unattributed", `${status.window.unattributedSessions} session(s) with no project`));
|
|
67
70
|
}
|
|
68
71
|
const local = readLocalSummary();
|
|
69
72
|
if (local.length > 0) {
|
|
@@ -71,7 +74,12 @@ export async function statusCommand() {
|
|
|
71
74
|
for (const line of local)
|
|
72
75
|
console.log(line);
|
|
73
76
|
}
|
|
74
|
-
for (const diagnosis of diagnose({
|
|
77
|
+
for (const diagnosis of diagnose({
|
|
78
|
+
hookAge,
|
|
79
|
+
metricsAge,
|
|
80
|
+
unattributedSessions: status.window.unattributedSessions,
|
|
81
|
+
sessions: status.window.sessions,
|
|
82
|
+
})) {
|
|
75
83
|
console.log(`\n${diagnosis}`);
|
|
76
84
|
}
|
|
77
85
|
}
|
|
@@ -96,7 +104,7 @@ function mark(ageMs) {
|
|
|
96
104
|
* Each names the single next action, because "one lane is quiet" is only
|
|
97
105
|
* actionable once you know which half of the pipeline it implicates.
|
|
98
106
|
*/
|
|
99
|
-
function diagnose(input) {
|
|
107
|
+
export function diagnose(input) {
|
|
100
108
|
const { hookAge, metricsAge } = input;
|
|
101
109
|
const hookLive = hookAge !== null && hookAge <= STALE_AFTER_MS;
|
|
102
110
|
const metricsLive = metricsAge !== null && metricsAge <= STALE_AFTER_MS;
|
|
@@ -122,6 +130,22 @@ function diagnose(input) {
|
|
|
122
130
|
" Check the hook with 'clocktopus agent doctor'.",
|
|
123
131
|
];
|
|
124
132
|
}
|
|
133
|
+
// Last, and only once both lanes are healthy. While the hook is silent every
|
|
134
|
+
// session is unattributed as a *consequence* of that, and the callout above is
|
|
135
|
+
// the one to act on. Reaching here means the pipeline works and the mapping does
|
|
136
|
+
// not, which is a different job in a different place.
|
|
137
|
+
if (input.unattributedSessions > 0) {
|
|
138
|
+
return [
|
|
139
|
+
`⚠ In the last 7 days, ${input.unattributedSessions} of ${input.sessions} sessions had no project.\n` +
|
|
140
|
+
' Their spend is recorded and counted — it lands in the "Unattributed\n' +
|
|
141
|
+
' agent work" row rather than against a client.\n' +
|
|
142
|
+
"\n" +
|
|
143
|
+
" A session takes its project from the repository it ran in, so the fix\n" +
|
|
144
|
+
" is a repository-to-project mapping and nothing on this machine changes\n" +
|
|
145
|
+
" it. Run 'clocktopus agent doctor' inside that checkout: it names which\n" +
|
|
146
|
+
" of the four reasons applies, and what to do about it.",
|
|
147
|
+
];
|
|
148
|
+
}
|
|
125
149
|
return [];
|
|
126
150
|
}
|
|
127
151
|
function readLocalSummary() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/status.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { diagnose } from "./status";
|
|
3
|
+
/**
|
|
4
|
+
* The callouts `agent status` prints under the lane table.
|
|
5
|
+
*
|
|
6
|
+
* Only the ordering is interesting, and it is the whole point: the checks run
|
|
7
|
+
* in pipeline order so the *first* failure is the one shown, because the rest
|
|
8
|
+
* are usually consequences of it.
|
|
9
|
+
*/
|
|
10
|
+
const LIVE = 60_000;
|
|
11
|
+
const STALE = 48 * 60 * 60 * 1000;
|
|
12
|
+
const healthy = { hookAge: LIVE, metricsAge: LIVE, sessions: 10 };
|
|
13
|
+
describe("diagnose", () => {
|
|
14
|
+
it("says nothing when both lanes are live and everything is attributed", () => {
|
|
15
|
+
expect(diagnose({ ...healthy, unattributedSessions: 0 })).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it("names the repository mapping when sessions land with no project", () => {
|
|
18
|
+
const [callout] = diagnose({ ...healthy, unattributedSessions: 3 });
|
|
19
|
+
expect(callout).toContain("3 of 10");
|
|
20
|
+
// The actionable object. Naming only "project" is what left people
|
|
21
|
+
// wondering whether it meant a ticket (BLA-671).
|
|
22
|
+
expect(callout).toContain("repository");
|
|
23
|
+
expect(callout).toContain("agent doctor");
|
|
24
|
+
});
|
|
25
|
+
it("reports a silent hook instead, while one is silent", () => {
|
|
26
|
+
// Every session is unattributed when the hook never runs — no telemetry
|
|
27
|
+
// stream carries path data — so the unattributed count is a symptom here
|
|
28
|
+
// and the hook is the thing to fix. Showing both would send someone to
|
|
29
|
+
// the dashboard to fix a mapping that is already correct.
|
|
30
|
+
const callouts = diagnose({
|
|
31
|
+
hookAge: STALE,
|
|
32
|
+
metricsAge: LIVE,
|
|
33
|
+
sessions: 10,
|
|
34
|
+
unattributedSessions: 10,
|
|
35
|
+
});
|
|
36
|
+
expect(callouts).toHaveLength(1);
|
|
37
|
+
expect(callouts[0]).toContain("hook is not running");
|
|
38
|
+
});
|
|
39
|
+
it("reports a silent exporter before anything else", () => {
|
|
40
|
+
const callouts = diagnose({
|
|
41
|
+
hookAge: LIVE,
|
|
42
|
+
metricsAge: STALE,
|
|
43
|
+
sessions: 10,
|
|
44
|
+
unattributedSessions: 4,
|
|
45
|
+
});
|
|
46
|
+
expect(callouts).toHaveLength(1);
|
|
47
|
+
expect(callouts[0]).toContain("no spend is");
|
|
48
|
+
});
|
|
49
|
+
it("sends someone to doctor when nothing has ever arrived", () => {
|
|
50
|
+
const callouts = diagnose({
|
|
51
|
+
hookAge: null,
|
|
52
|
+
metricsAge: null,
|
|
53
|
+
sessions: 0,
|
|
54
|
+
unattributedSessions: 0,
|
|
55
|
+
});
|
|
56
|
+
expect(callouts[0]).toContain("Nothing has ever arrived");
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/src/lib/agents.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type AgentTelemetryState = {
|
|
|
6
6
|
endpoint: string | null;
|
|
7
7
|
/** Per-event, because a half-installed hook pair degrades silently. */
|
|
8
8
|
hookCommands: Partial<Record<"SessionStart" | "SessionEnd", string>>;
|
|
9
|
+
missingLanes: string[];
|
|
9
10
|
/** Newest mtime across the agent's config files, for the restart check. */
|
|
10
11
|
modifiedAt: Date | null;
|
|
11
12
|
/** Files we would write, whether or not they exist yet. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/lib/agents.ts"],"names":[],"mappings":"AAyDA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEtD,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/lib/agents.ts"],"names":[],"mappings":"AAyDA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEtD,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAIrE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,2EAA2E;IAC3E,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IAKf,kBAAkB,EAAE,OAAO,CAAC;IAU5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2DAA2D;IAC3D,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,IAAI,IAAI,mBAAmB,CAAC;IAC5B,KAAK,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG;QACtE,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAGhC,cAAc,IAAI,MAAM,EAAE,CAAC;IAI3B,WAAW,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAKjE;AA0ND,eAAO,MAAM,MAAM,EAAE,SAAS,YAAY,EAIzC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAOF,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAqB5C"}
|
package/dist/src/lib/agents.js
CHANGED
|
@@ -21,6 +21,11 @@ const claudeAdapter = {
|
|
|
21
21
|
token: installed.env.CLOCKTOPUS_INGEST_TOKEN ?? null,
|
|
22
22
|
endpoint: installed.env.CLOCKTOPUS_OTEL_ENDPOINT ?? null,
|
|
23
23
|
hookCommands: installed.hookCommands,
|
|
24
|
+
// Added by BLA-671, so every install configured before it has a working metrics
|
|
25
|
+
// lane and no sub-agent runtime at all.
|
|
26
|
+
missingLanes: installed.env.OTEL_LOGS_EXPORTER === "otlp"
|
|
27
|
+
? []
|
|
28
|
+
: ["sub-agent runtime"],
|
|
24
29
|
modifiedAt: installed.modifiedAt,
|
|
25
30
|
paths: [installed.path],
|
|
26
31
|
};
|
|
@@ -63,6 +68,7 @@ const codexAdapter = {
|
|
|
63
68
|
token: telemetry.token,
|
|
64
69
|
endpoint: telemetry.endpoint,
|
|
65
70
|
hookCommands: hooks.commands,
|
|
71
|
+
missingLanes: [],
|
|
66
72
|
modifiedAt: newest([telemetry.modifiedAt, hooks.modifiedAt]),
|
|
67
73
|
paths: [telemetry.path, hooks.path],
|
|
68
74
|
};
|
|
@@ -158,6 +164,7 @@ const opencodeAdapter = {
|
|
|
158
164
|
SessionEnd: plugin.config.hookArgv.join(" "),
|
|
159
165
|
}
|
|
160
166
|
: {},
|
|
167
|
+
missingLanes: [],
|
|
161
168
|
modifiedAt: plugin.modifiedAt,
|
|
162
169
|
paths: [plugin.path],
|
|
163
170
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/lib/auth.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4BnE;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/lib/auth.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4BnE;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA0DrB;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
|
package/dist/src/lib/auth.js
CHANGED
|
@@ -38,6 +38,17 @@ export async function pollForToken(deviceCode, interval) {
|
|
|
38
38
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
39
39
|
}),
|
|
40
40
|
});
|
|
41
|
+
// The web app rate-limits `/device/token` on top of the interval this loop
|
|
42
|
+
// already respects, and a 429 carries Better Auth's own body — no `error`
|
|
43
|
+
// field — so it used to fall through to "Unexpected response" and kill a
|
|
44
|
+
// login that was doing nothing wrong. Back off and keep waiting instead.
|
|
45
|
+
if (response.status === 429) {
|
|
46
|
+
const retryAfter = Number(response.headers.get("x-retry-after"));
|
|
47
|
+
return {
|
|
48
|
+
status: "slow_down",
|
|
49
|
+
newInterval: retryAfter > 0 ? retryAfter + interval : interval + 5,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
41
52
|
const data = (await response.json());
|
|
42
53
|
if ("access_token" in data) {
|
|
43
54
|
return { status: "success", accessToken: data.access_token };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const SETTINGS_FILENAME = "settings.json";
|
|
2
2
|
/** Env keys `clocktopus agent setup` owns — and the only ones it removes. */
|
|
3
|
-
export declare const TELEMETRY_ENV_KEYS: readonly ["CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_METRICS_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_METRICS_INCLUDE_SESSION_ID", "OTEL_METRIC_EXPORT_INTERVAL", "CLOCKTOPUS_INGEST_TOKEN", "CLOCKTOPUS_OTEL_ENDPOINT"];
|
|
3
|
+
export declare const TELEMETRY_ENV_KEYS: readonly ["CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_METRICS_EXPORTER", "OTEL_LOGS_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_METRICS_INCLUDE_SESSION_ID", "OTEL_METRIC_EXPORT_INTERVAL", "CLOCKTOPUS_INGEST_TOKEN", "CLOCKTOPUS_OTEL_ENDPOINT"];
|
|
4
4
|
export declare const METRIC_EXPORT_INTERVAL_MS = 60000;
|
|
5
5
|
export declare const HOOK_TIMEOUT_SECONDS = 10;
|
|
6
6
|
export type ClaudeSettings = Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-settings.d.ts","sourceRoot":"","sources":["../../../src/lib/claude-settings.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"claude-settings.d.ts","sourceRoot":"","sources":["../../../src/lib/claude-settings.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,+SAWrB,CAAC;AAKX,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAMhD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASrD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAMzC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,YAAY,CAAC,IAAI,SAAiB,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,cAAc,CAAC;CAC1B,CA6BA;AAKD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,cAAc,EACxB,IAAI,SAAiB,GACpB;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAiB/B;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBzB;AAED,QAAA,MAAM,WAAW,yCAA0C,CAAC;AAgC5D,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9D,cAAc,CA0ChB;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG;IACjE,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;CACvB,CAmCA;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,CAAC,IAAI,SAAiB,GAAG;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACrE,CAmCA"}
|
|
@@ -16,6 +16,7 @@ export const SETTINGS_FILENAME = "settings.json";
|
|
|
16
16
|
export const TELEMETRY_ENV_KEYS = [
|
|
17
17
|
"CLAUDE_CODE_ENABLE_TELEMETRY",
|
|
18
18
|
"OTEL_METRICS_EXPORTER",
|
|
19
|
+
"OTEL_LOGS_EXPORTER",
|
|
19
20
|
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
20
21
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
21
22
|
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
@@ -101,6 +102,12 @@ export function buildTelemetryEnv(input) {
|
|
|
101
102
|
return {
|
|
102
103
|
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
103
104
|
OTEL_METRICS_EXPORTER: "otlp",
|
|
105
|
+
// Sub-agent runtime, and nothing else (BLA-671). `claude_code.active_time.total`
|
|
106
|
+
// on the metric stream measures the main thread alone, so without this a session
|
|
107
|
+
// that delegates its work to `Task` agents reports the coordinator's time and none
|
|
108
|
+
// of theirs. The receiver reads exactly one event off this stream,
|
|
109
|
+
// `subagent_completed`, and never the ones carrying prompts or responses.
|
|
110
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
104
111
|
// The receiver answers protobuf with an explicit 415, but only JSON actually works.
|
|
105
112
|
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
106
113
|
OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
|
|
@@ -115,6 +115,12 @@ describe("buildTelemetryEnv", () => {
|
|
|
115
115
|
// stream, and every session loses its repository and branch.
|
|
116
116
|
expect(env.OTEL_METRICS_INCLUDE_SESSION_ID).toBe("true");
|
|
117
117
|
});
|
|
118
|
+
it("enables the log exporter, the only carrier of sub-agent runtime", () => {
|
|
119
|
+
// BLA-671: `claude_code.active_time.total` on the metric stream measures the
|
|
120
|
+
// main thread, so without this a session that delegates its work to `Task`
|
|
121
|
+
// agents reports the coordinator's time and none of theirs.
|
|
122
|
+
expect(buildTelemetryEnv(INSTALL).OTEL_LOGS_EXPORTER).toBe("otlp");
|
|
123
|
+
});
|
|
118
124
|
it("normalises a trailing slash so paths do not double up", () => {
|
|
119
125
|
const env = buildTelemetryEnv({
|
|
120
126
|
...INSTALL,
|
|
@@ -125,6 +131,13 @@ describe("buildTelemetryEnv", () => {
|
|
|
125
131
|
});
|
|
126
132
|
});
|
|
127
133
|
describe("removeTelemetrySettings", () => {
|
|
134
|
+
it("removes the log exporter again on teardown", () => {
|
|
135
|
+
// Every key setup writes has to be one `agent disable` takes back, or a
|
|
136
|
+
// disabled agent keeps exporting to an endpoint that no longer wants it.
|
|
137
|
+
const installed = applyTelemetrySettings(userSettings(), INSTALL);
|
|
138
|
+
const { settings } = removeTelemetrySettings(installed);
|
|
139
|
+
expect(settings.env.OTEL_LOGS_EXPORTER).toBeUndefined();
|
|
140
|
+
});
|
|
128
141
|
it("removes only what setup added", () => {
|
|
129
142
|
const installed = applyTelemetrySettings(userSettings(), INSTALL);
|
|
130
143
|
const { settings, removedEnvKeys, removedHooks } = removeTelemetrySettings(installed);
|
|
@@ -184,6 +197,18 @@ describe("reading and writing the file", () => {
|
|
|
184
197
|
expect(installed.hookCommands.SessionStart).toBe("clocktopus agent hook");
|
|
185
198
|
expect(installed.hookCommands.SessionEnd).toBe("clocktopus agent hook");
|
|
186
199
|
});
|
|
200
|
+
it("reads back the log exporter, so an older install can be spotted", () => {
|
|
201
|
+
// What `agent doctor` branches on. A settings file written before BLA-671 has a
|
|
202
|
+
// token, an endpoint and both hooks — every other check passes — while sub-agent
|
|
203
|
+
// runtime never leaves the machine. This is the only signal that it is stale.
|
|
204
|
+
const beforeBla671 = applyTelemetrySettings(userSettings(), INSTALL);
|
|
205
|
+
delete beforeBla671.env.OTEL_LOGS_EXPORTER;
|
|
206
|
+
writeSettings(beforeBla671, path);
|
|
207
|
+
expect(readInstalledTelemetry(path).env.OTEL_LOGS_EXPORTER).toBeUndefined();
|
|
208
|
+
writeSettings(applyTelemetrySettings(beforeBla671, INSTALL), path);
|
|
209
|
+
// Re-running setup merges it in rather than needing a teardown first.
|
|
210
|
+
expect(readInstalledTelemetry(path).env.OTEL_LOGS_EXPORTER).toBe("otlp");
|
|
211
|
+
});
|
|
187
212
|
it("reports no telemetry for a settings file that only has the user's own hooks", () => {
|
|
188
213
|
writeFileSync(path, JSON.stringify(userSettings()), "utf8");
|
|
189
214
|
const installed = readInstalledTelemetry(path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastin-dev/clocktopus-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clocktopus": "./dist/bin/clocktopus.js"
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
"zod": "4.4.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
+
"@repo/eslint-config": "0.0.0",
|
|
20
|
+
"@repo/prettier-config": "0.1.0",
|
|
21
|
+
"@repo/typescript-config": "0.0.0",
|
|
19
22
|
"@types/node": "22.15.3",
|
|
20
23
|
"eslint": "9.37.0",
|
|
21
24
|
"eslint-plugin-import-x": "^4.16.1",
|
|
22
25
|
"typescript": "5.9.2",
|
|
23
|
-
"vitest": "^4.1.5"
|
|
24
|
-
"@repo/eslint-config": "0.0.0",
|
|
25
|
-
"@repo/prettier-config": "0.1.0",
|
|
26
|
-
"@repo/typescript-config": "0.0.0"
|
|
26
|
+
"vitest": "^4.1.5"
|
|
27
27
|
},
|
|
28
28
|
"prettier": "@repo/prettier-config",
|
|
29
29
|
"scripts": {
|