@blastin-dev/clocktopus-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -36
- package/dist/src/commands/agent/disable.d.ts +8 -1
- package/dist/src/commands/agent/disable.d.ts.map +1 -1
- package/dist/src/commands/agent/disable.js +79 -45
- package/dist/src/commands/agent/doctor.d.ts.map +1 -1
- package/dist/src/commands/agent/doctor.js +157 -95
- package/dist/src/commands/agent/hook.d.ts +4 -1
- package/dist/src/commands/agent/hook.d.ts.map +1 -1
- package/dist/src/commands/agent/hook.js +239 -98
- package/dist/src/commands/agent/setup.d.ts +1 -16
- package/dist/src/commands/agent/setup.d.ts.map +1 -1
- package/dist/src/commands/agent/setup.js +198 -81
- package/dist/src/commands/agent/status.d.ts.map +1 -1
- package/dist/src/commands/agent/status.js +46 -24
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -4
- package/dist/src/lib/agent-config.d.ts +5 -23
- package/dist/src/lib/agent-config.d.ts.map +1 -1
- package/dist/src/lib/agent-config.js +50 -36
- package/dist/src/lib/agent-hook-state.d.ts +11 -7
- package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
- package/dist/src/lib/agent-hook-state.js +34 -29
- package/dist/src/lib/agents.d.ts +52 -0
- package/dist/src/lib/agents.d.ts.map +1 -0
- package/dist/src/lib/agents.js +238 -0
- package/dist/src/lib/claude-settings.d.ts +0 -36
- package/dist/src/lib/claude-settings.d.ts.map +1 -1
- package/dist/src/lib/claude-settings.js +37 -62
- package/dist/src/lib/codex-config.d.ts +87 -0
- package/dist/src/lib/codex-config.d.ts.map +1 -0
- package/dist/src/lib/codex-config.js +399 -0
- package/dist/src/lib/codex-config.test.d.ts +2 -0
- package/dist/src/lib/codex-config.test.d.ts.map +1 -0
- package/dist/src/lib/codex-config.test.js +359 -0
- package/dist/src/lib/declared-commits.d.ts +43 -0
- package/dist/src/lib/declared-commits.d.ts.map +1 -0
- package/dist/src/lib/declared-commits.js +114 -0
- package/dist/src/lib/declared-commits.test.d.ts +2 -0
- package/dist/src/lib/declared-commits.test.d.ts.map +1 -0
- package/dist/src/lib/declared-commits.test.js +129 -0
- package/dist/src/lib/git-remotes.d.ts +9 -0
- package/dist/src/lib/git-remotes.d.ts.map +1 -0
- package/dist/src/lib/git-remotes.js +50 -0
- package/dist/src/lib/git-remotes.test.d.ts +2 -0
- package/dist/src/lib/git-remotes.test.d.ts.map +1 -0
- package/dist/src/lib/git-remotes.test.js +52 -0
- package/dist/src/lib/opencode-config.d.ts +23 -0
- package/dist/src/lib/opencode-config.d.ts.map +1 -0
- package/dist/src/lib/opencode-config.js +290 -0
- package/dist/src/lib/opencode-config.test.d.ts +2 -0
- package/dist/src/lib/opencode-config.test.d.ts.map +1 -0
- package/dist/src/lib/opencode-config.test.js +140 -0
- package/package.json +2 -1
|
@@ -1,45 +1,58 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, readFileSync, realpathSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { delimiter, join, resolve } from "node:path";
|
|
4
|
-
import { readInstalledTelemetry } from "./claude-settings.js";
|
|
5
|
-
|
|
4
|
+
import { readInstalledTelemetry, settingsPath } from "./claude-settings.js";
|
|
5
|
+
import { codexConfigPath, readCodexTelemetry } from "./codex-config.js";
|
|
6
|
+
import { opencodePluginPath, readOpencodePlugin } from "./opencode-config.js";
|
|
7
|
+
export function resolveAgentCredentials(agent = "claude") {
|
|
6
8
|
const envToken = process.env.CLOCKTOPUS_INGEST_TOKEN?.trim();
|
|
7
9
|
const envEndpoint = process.env.CLOCKTOPUS_OTEL_ENDPOINT?.trim();
|
|
8
|
-
let
|
|
9
|
-
let
|
|
10
|
+
let fileToken;
|
|
11
|
+
let fileEndpoint;
|
|
10
12
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (agent === "codex") {
|
|
14
|
+
const codex = readCodexTelemetry();
|
|
15
|
+
fileToken = codex.token ?? undefined;
|
|
16
|
+
fileEndpoint = codex.endpoint ?? undefined;
|
|
17
|
+
}
|
|
18
|
+
else if (agent === "opencode") {
|
|
19
|
+
const plugin = readOpencodePlugin();
|
|
20
|
+
fileToken = plugin.config?.token ?? undefined;
|
|
21
|
+
fileEndpoint = plugin.config?.endpoint ?? undefined;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const installed = readInstalledTelemetry();
|
|
25
|
+
fileToken = installed.env.CLOCKTOPUS_INGEST_TOKEN;
|
|
26
|
+
fileEndpoint = installed.env.CLOCKTOPUS_OTEL_ENDPOINT;
|
|
27
|
+
}
|
|
14
28
|
}
|
|
15
29
|
catch {
|
|
16
|
-
// A malformed
|
|
17
|
-
//
|
|
18
|
-
// every session start.
|
|
30
|
+
// A malformed config file is reported properly by `setup` and `doctor`. Credential
|
|
31
|
+
// resolution must not throw — the hook calls it on every session start.
|
|
19
32
|
}
|
|
20
33
|
return {
|
|
21
|
-
token: envToken ||
|
|
22
|
-
endpoint: (envEndpoint ||
|
|
23
|
-
tokenSource: envToken ? "environment" :
|
|
34
|
+
token: envToken || fileToken || null,
|
|
35
|
+
endpoint: (envEndpoint || fileEndpoint || null)?.replace(/\/$/, "") ?? null,
|
|
36
|
+
tokenSource: envToken ? "environment" : fileToken ? "settings" : "none",
|
|
24
37
|
endpointSource: envEndpoint
|
|
25
38
|
? "environment"
|
|
26
|
-
:
|
|
39
|
+
: fileEndpoint
|
|
27
40
|
? "settings"
|
|
28
41
|
: "none",
|
|
42
|
+
sourcePath: agent === "codex"
|
|
43
|
+
? codexConfigPath()
|
|
44
|
+
: agent === "opencode"
|
|
45
|
+
? opencodePluginPath()
|
|
46
|
+
: settingsPath(),
|
|
29
47
|
};
|
|
30
48
|
}
|
|
31
49
|
/** Masks a token for display: never print more than the stored prefix. */
|
|
32
50
|
export function maskToken(token) {
|
|
33
51
|
return token.length <= 15 ? "…" : `${token.slice(0, 15)}…`;
|
|
34
52
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* Scanned rather than guessed at, because "your `.envrc` is being ignored"
|
|
39
|
-
* is only useful advice when it names the file. The repo-local `.envrc` is
|
|
40
|
-
* included because that is exactly how this project was dogfooded before
|
|
41
|
-
* the CLI existed.
|
|
42
|
-
*/
|
|
53
|
+
// Files that commonly export the same variables, and would be shadowed. Scanned rather
|
|
54
|
+
// than guessed at, because "your `.envrc` is being ignored" is only useful advice when it
|
|
55
|
+
// names the file.
|
|
43
56
|
const SHELL_FILES = [
|
|
44
57
|
".bashrc",
|
|
45
58
|
".bash_profile",
|
|
@@ -76,22 +89,23 @@ export function findShadowedExports(cwd = process.cwd()) {
|
|
|
76
89
|
}
|
|
77
90
|
return found;
|
|
78
91
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export function resolveHookCommand() {
|
|
92
|
+
// The command to write into an agent's hook configuration.
|
|
93
|
+
//
|
|
94
|
+
// Prefers the bare name, but only after confirming `clocktopus` on PATH resolves to *this*
|
|
95
|
+
// executable. Hooks run through a shell whose PATH may differ from the interactive one, and
|
|
96
|
+
// a bare name that does not resolve there fails silently — the session runs fine and every
|
|
97
|
+
// bit of repository context is lost. An absolute path is uglier and survives that.
|
|
98
|
+
//
|
|
99
|
+
// The provider is baked into the command because the two agents send identical hook
|
|
100
|
+
// payloads, with no marker of any kind: which agent is calling can only be known from how
|
|
101
|
+
// the hook was installed.
|
|
102
|
+
export function resolveHookCommand(provider) {
|
|
90
103
|
const script = process.argv[1] ? resolve(process.argv[1]) : null;
|
|
91
104
|
const quote = (value) => /[\s"']/.test(value) ? `"${value}"` : value;
|
|
105
|
+
const args = `agent hook --provider ${provider}`;
|
|
92
106
|
if (script && resolvesOnPath("clocktopus", script)) {
|
|
93
107
|
return {
|
|
94
|
-
command:
|
|
108
|
+
command: `clocktopus ${args}`,
|
|
95
109
|
usesAbsolutePath: false,
|
|
96
110
|
binaryPath: script,
|
|
97
111
|
};
|
|
@@ -101,13 +115,13 @@ export function resolveHookCommand() {
|
|
|
101
115
|
// a published package keeps its shebang, but a checkout may not have the
|
|
102
116
|
// executable bit, and the hook must not depend on that.
|
|
103
117
|
return {
|
|
104
|
-
command: `${quote(process.execPath)} ${quote(script)}
|
|
118
|
+
command: `${quote(process.execPath)} ${quote(script)} ${args}`,
|
|
105
119
|
usesAbsolutePath: true,
|
|
106
120
|
binaryPath: script,
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
123
|
return {
|
|
110
|
-
command:
|
|
124
|
+
command: `clocktopus ${args}`,
|
|
111
125
|
usesAbsolutePath: false,
|
|
112
126
|
binaryPath: null,
|
|
113
127
|
};
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
declare const LastRunSchema: z.ZodObject<{
|
|
3
3
|
at: z.ZodString;
|
|
4
4
|
event: z.ZodOptional<z.ZodString>;
|
|
5
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
5
6
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
6
7
|
endpoint: z.ZodOptional<z.ZodString>;
|
|
7
8
|
tokenPrefix: z.ZodOptional<z.ZodString>;
|
|
@@ -13,20 +14,23 @@ export type HookLastRun = z.infer<typeof LastRunSchema>;
|
|
|
13
14
|
declare const StartStateSchema: z.ZodObject<{
|
|
14
15
|
sha: z.ZodString;
|
|
15
16
|
cwd: z.ZodOptional<z.ZodString>;
|
|
17
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
18
|
+
closed: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
startedAt: z.ZodOptional<z.ZodString>;
|
|
16
20
|
}, z.core.$strip>;
|
|
17
21
|
export type HookStartState = z.infer<typeof StartStateSchema>;
|
|
18
|
-
export declare function writeStartState(sessionId: string,
|
|
22
|
+
export declare function writeStartState(sessionId: string, state: {
|
|
23
|
+
sha: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
provider: string;
|
|
26
|
+
closed?: boolean;
|
|
27
|
+
startedAt?: string;
|
|
28
|
+
}): void;
|
|
19
29
|
export declare function readStartState(sessionId: string): HookStartState | undefined;
|
|
20
30
|
export declare function clearStartState(sessionId: string): void;
|
|
21
31
|
export declare function listStartStates(): Array<{
|
|
22
32
|
sessionId: string;
|
|
23
33
|
ageMs: number;
|
|
24
|
-
/**
|
|
25
|
-
* When the state file was last touched — the newest moment this machine
|
|
26
|
-
* has evidence the session existed. The sweep sends it as the abandoned
|
|
27
|
-
* session's `ended_at` so the receiver does not stamp its own clock,
|
|
28
|
-
* hours later, as the end of a window used to attribute commits.
|
|
29
|
-
*/
|
|
30
34
|
lastActivityAt: Date;
|
|
31
35
|
}>;
|
|
32
36
|
export declare function recordLastRun(run: HookLastRun): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-hook-state.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-hook-state.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-hook-state.d.ts","sourceRoot":"","sources":["../../../src/lib/agent-hook-state.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqBxB,QAAA,MAAM,aAAa;;;;;;;;;;iBAYjB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAExD,QAAA,MAAM,gBAAgB;;;;;;iBAgBpB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAK9D,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IACL,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,IAAI,CAUN;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAe5E;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,eAAe,IAAI,KAAK,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IAId,cAAc,EAAE,IAAI,CAAC;CACtB,CAAC,CAsBD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAOpD;AAED,wBAAgB,WAAW,IAAI,WAAW,GAAG,IAAI,CAShD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAOrC"}
|
|
@@ -2,33 +2,27 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync,
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* deleted) on purpose: a session that started under the old script must
|
|
13
|
-
* still be able to close under the new one, and a sweep must still find
|
|
14
|
-
* what the old one abandoned.
|
|
15
|
-
*/
|
|
5
|
+
// On-disk state the SessionStart/SessionEnd hook keeps between its two invocations, plus
|
|
6
|
+
// the record of what the last invocation achieved.
|
|
7
|
+
//
|
|
8
|
+
// Under the cache directory rather than the repository so it never shows up in
|
|
9
|
+
// `git status`. The path is unchanged from the standalone script this command replaced,
|
|
10
|
+
// on purpose: a session that started under the old script must still close under the new
|
|
11
|
+
// one, and a sweep must still find what the old one abandoned.
|
|
16
12
|
const CACHE_DIR = join(homedir(), ".cache", "clocktopus");
|
|
17
13
|
const STATE_DIR = join(CACHE_DIR, "agent-sessions");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* it. Without this, a token that started returning 401 mid-week is
|
|
25
|
-
* invisible locally: the session runs normally and nothing is written
|
|
26
|
-
* anywhere the user looks.
|
|
27
|
-
*/
|
|
14
|
+
// The hook's last outcome, written every run.
|
|
15
|
+
//
|
|
16
|
+
// The only place the pipeline reports on itself from the machine's own side. The hook
|
|
17
|
+
// must stay silent — it cannot warn, prompt or print — so it leaves a receipt, and
|
|
18
|
+
// `agent status` / `agent doctor` read it. Without this, a token that started returning
|
|
19
|
+
// 401 mid-week is invisible locally.
|
|
28
20
|
const LAST_RUN_FILE = join(CACHE_DIR, "agent-hook-last.json");
|
|
29
21
|
const LastRunSchema = z.object({
|
|
30
22
|
at: z.string(),
|
|
31
23
|
event: z.string().optional(),
|
|
24
|
+
// One receipt file is shared by all of them.
|
|
25
|
+
provider: z.string().optional(),
|
|
32
26
|
sessionId: z.string().optional(),
|
|
33
27
|
endpoint: z.string().optional(),
|
|
34
28
|
tokenPrefix: z.string().optional(),
|
|
@@ -40,16 +34,28 @@ const LastRunSchema = z.object({
|
|
|
40
34
|
const StartStateSchema = z.object({
|
|
41
35
|
sha: z.string(),
|
|
42
36
|
cwd: z.string().optional(),
|
|
37
|
+
// The agent that opened this session. Recorded because the sweep runs from whichever
|
|
38
|
+
// agent starts *next*, and closing a Codex session as `claude_code` would open a second,
|
|
39
|
+
// empty session row under the wrong provider and leave the real one hanging.
|
|
40
|
+
provider: z.string().optional(),
|
|
41
|
+
// Set once the session has had a real SessionEnd, and kept only because its host can send
|
|
42
|
+
// another one — see `hostRepeatsSessionEnd`. The file still holds the starting SHA the
|
|
43
|
+
// next SessionEnd needs, but the session is already closed, so the sweep must delete it
|
|
44
|
+
// rather than close it again with a stale HEAD.
|
|
45
|
+
closed: z.boolean().optional(),
|
|
46
|
+
// When SessionStart fired, ISO. Bounds the reflog read SessionEnd declares from —
|
|
47
|
+
// see `declared-commits.ts`. Optional because sessions opened by an older CLI have
|
|
48
|
+
// no such stamp, and those declare nothing rather than read an unbounded reflog.
|
|
49
|
+
startedAt: z.string().optional(),
|
|
43
50
|
});
|
|
44
51
|
const stateFile = (sessionId) => join(STATE_DIR, `${sessionId.replace(/[^\w-]/g, "")}.sha`);
|
|
45
|
-
export function writeStartState(sessionId,
|
|
52
|
+
export function writeStartState(sessionId, state) {
|
|
46
53
|
try {
|
|
47
54
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
48
|
-
// `cwd` is stored alongside the SHA because the sweep runs from
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
writeFileSync(stateFile(sessionId), JSON.stringify({ sha, cwd }), "utf8");
|
|
55
|
+
// `cwd` is stored alongside the SHA because the sweep runs from whatever repository the
|
|
56
|
+
// *next* session happens to start in. Resolving an abandoned session's SHA against the
|
|
57
|
+
// wrong checkout would either fail or, worse, succeed against an unrelated history.
|
|
58
|
+
writeFileSync(stateFile(sessionId), JSON.stringify(state), "utf8");
|
|
53
59
|
}
|
|
54
60
|
catch {
|
|
55
61
|
// Losing the start SHA costs the exact commit list, not the session.
|
|
@@ -65,8 +71,7 @@ export function readStartState(sessionId) {
|
|
|
65
71
|
return parsed.success ? parsed.data : undefined;
|
|
66
72
|
}
|
|
67
73
|
catch {
|
|
68
|
-
// Files written by earlier versions hold a bare SHA.
|
|
69
|
-
// started under the old format must still close correctly.
|
|
74
|
+
// Files written by earlier versions hold a bare SHA.
|
|
70
75
|
return { sha: raw };
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type AgentId = "claude" | "codex" | "opencode";
|
|
2
|
+
/** How the ingested session is labelled — must match `AgentProvider` in core. */
|
|
3
|
+
export type AgentProviderId = "claude_code" | "codex_cli" | "opencode";
|
|
4
|
+
export type AgentTelemetryState = {
|
|
5
|
+
token: string | null;
|
|
6
|
+
endpoint: string | null;
|
|
7
|
+
/** Per-event, because a half-installed hook pair degrades silently. */
|
|
8
|
+
hookCommands: Partial<Record<"SessionStart" | "SessionEnd", string>>;
|
|
9
|
+
/** Newest mtime across the agent's config files, for the restart check. */
|
|
10
|
+
modifiedAt: Date | null;
|
|
11
|
+
/** Files we would write, whether or not they exist yet. */
|
|
12
|
+
paths: string[];
|
|
13
|
+
};
|
|
14
|
+
export type AgentAdapter = {
|
|
15
|
+
id: AgentId;
|
|
16
|
+
label: string;
|
|
17
|
+
provider: AgentProviderId;
|
|
18
|
+
/** Executable name, used both to detect an install and to name it in help. */
|
|
19
|
+
binary: string;
|
|
20
|
+
hostRunsHooksAsync: boolean;
|
|
21
|
+
hostRepeatsSessionEnd: boolean;
|
|
22
|
+
/** Files this agent's telemetry configuration lives in. */
|
|
23
|
+
paths(): string[];
|
|
24
|
+
read(): AgentTelemetryState;
|
|
25
|
+
apply(input: {
|
|
26
|
+
token: string;
|
|
27
|
+
endpoint: string;
|
|
28
|
+
hookCommand: string;
|
|
29
|
+
}): {
|
|
30
|
+
backupPaths: string[];
|
|
31
|
+
};
|
|
32
|
+
remove(): {
|
|
33
|
+
removed: string[];
|
|
34
|
+
};
|
|
35
|
+
pendingActions(): string[];
|
|
36
|
+
staleReason?(): string | null;
|
|
37
|
+
};
|
|
38
|
+
/** `true` when the parse error is one of ours, whichever agent raised it. */
|
|
39
|
+
export declare function isConfigParseError(error: unknown): error is Error;
|
|
40
|
+
export declare const AGENTS: readonly AgentAdapter[];
|
|
41
|
+
export type AgentSurvey = {
|
|
42
|
+
agent: AgentAdapter;
|
|
43
|
+
/** Version string when the binary is on PATH, `null` when it is not. */
|
|
44
|
+
version: string | null;
|
|
45
|
+
installed: boolean;
|
|
46
|
+
/** True once our token is written into this agent's config. */
|
|
47
|
+
configured: boolean;
|
|
48
|
+
/** Set when the agent's config exists but could not be parsed. */
|
|
49
|
+
unreadable: string | null;
|
|
50
|
+
};
|
|
51
|
+
export declare function surveyAgents(): AgentSurvey[];
|
|
52
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +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;IACrE,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;AAkND,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"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { applyTelemetrySettings, readInstalledTelemetry, readSettings, removeTelemetrySettings, SettingsParseError, settingsPath, writeSettings, } from "./claude-settings.js";
|
|
3
|
+
import { applyCodexHooks, applyCodexOtel, CodexConfigParseError, codexConfigPath, codexHooksPath, HOOK_EVENT_COUNT, readCodexConfig, readCodexHooks, readCodexHookTrust, readCodexTelemetry, removeCodexHooks, removeCodexOtel, writeCodexConfig, writeCodexHooks, } from "./codex-config.js";
|
|
4
|
+
import { buildOpencodePlugin, OPENCODE_PLUGIN_VERSION, opencodePluginPath, readOpencodePlugin, removeOpencodePlugin, splitCommandLine, writeOpencodePlugin, } from "./opencode-config.js";
|
|
5
|
+
/** `true` when the parse error is one of ours, whichever agent raised it. */
|
|
6
|
+
export function isConfigParseError(error) {
|
|
7
|
+
return (error instanceof SettingsParseError ||
|
|
8
|
+
error instanceof CodexConfigParseError);
|
|
9
|
+
}
|
|
10
|
+
const claudeAdapter = {
|
|
11
|
+
id: "claude",
|
|
12
|
+
label: "Claude Code",
|
|
13
|
+
provider: "claude_code",
|
|
14
|
+
binary: "claude",
|
|
15
|
+
hostRunsHooksAsync: true,
|
|
16
|
+
hostRepeatsSessionEnd: false,
|
|
17
|
+
paths: () => [settingsPath()],
|
|
18
|
+
read() {
|
|
19
|
+
const installed = readInstalledTelemetry();
|
|
20
|
+
return {
|
|
21
|
+
token: installed.env.CLOCKTOPUS_INGEST_TOKEN ?? null,
|
|
22
|
+
endpoint: installed.env.CLOCKTOPUS_OTEL_ENDPOINT ?? null,
|
|
23
|
+
hookCommands: installed.hookCommands,
|
|
24
|
+
modifiedAt: installed.modifiedAt,
|
|
25
|
+
paths: [installed.path],
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
apply(input) {
|
|
29
|
+
const current = readSettings();
|
|
30
|
+
const next = applyTelemetrySettings(current.settings, input);
|
|
31
|
+
const { backupPath } = writeSettings(next, current.path);
|
|
32
|
+
return { backupPaths: backupPath ? [backupPath] : [] };
|
|
33
|
+
},
|
|
34
|
+
remove() {
|
|
35
|
+
const current = readSettings();
|
|
36
|
+
if (!current.exists)
|
|
37
|
+
return { removed: [] };
|
|
38
|
+
const result = removeTelemetrySettings(current.settings);
|
|
39
|
+
if (result.removedEnvKeys.length === 0 && !result.removedHooks)
|
|
40
|
+
return { removed: [] };
|
|
41
|
+
writeSettings(result.settings, current.path);
|
|
42
|
+
return {
|
|
43
|
+
removed: [
|
|
44
|
+
...result.removedEnvKeys,
|
|
45
|
+
...(result.removedHooks ? ["SessionStart/SessionEnd hooks"] : []),
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
pendingActions: () => [],
|
|
50
|
+
};
|
|
51
|
+
const codexAdapter = {
|
|
52
|
+
id: "codex",
|
|
53
|
+
label: "Codex CLI",
|
|
54
|
+
provider: "codex_cli",
|
|
55
|
+
binary: "codex",
|
|
56
|
+
hostRunsHooksAsync: false,
|
|
57
|
+
hostRepeatsSessionEnd: false,
|
|
58
|
+
paths: () => [codexConfigPath(), codexHooksPath()],
|
|
59
|
+
read() {
|
|
60
|
+
const telemetry = readCodexTelemetry();
|
|
61
|
+
const hooks = readCodexHooks();
|
|
62
|
+
return {
|
|
63
|
+
token: telemetry.token,
|
|
64
|
+
endpoint: telemetry.endpoint,
|
|
65
|
+
hookCommands: hooks.commands,
|
|
66
|
+
modifiedAt: newest([telemetry.modifiedAt, hooks.modifiedAt]),
|
|
67
|
+
paths: [telemetry.path, hooks.path],
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
apply(input) {
|
|
71
|
+
const backupPaths = [];
|
|
72
|
+
const config = readCodexConfig();
|
|
73
|
+
const nextText = applyCodexOtel(config, input);
|
|
74
|
+
const written = writeCodexConfig({
|
|
75
|
+
path: config.path,
|
|
76
|
+
previousText: config.text,
|
|
77
|
+
previousConfig: config.config,
|
|
78
|
+
nextText,
|
|
79
|
+
});
|
|
80
|
+
if (written.backupPath)
|
|
81
|
+
backupPaths.push(written.backupPath);
|
|
82
|
+
const hooks = readCodexHooks();
|
|
83
|
+
const nextHooks = applyCodexHooks(hooks.file, input);
|
|
84
|
+
const hooksWritten = writeCodexHooks(nextHooks, hooks.path);
|
|
85
|
+
if (hooksWritten.backupPath)
|
|
86
|
+
backupPaths.push(hooksWritten.backupPath);
|
|
87
|
+
return { backupPaths };
|
|
88
|
+
},
|
|
89
|
+
remove() {
|
|
90
|
+
const removed = [];
|
|
91
|
+
const config = readCodexConfig();
|
|
92
|
+
if (config.exists) {
|
|
93
|
+
const { nextText, removedKeys } = removeCodexOtel(config);
|
|
94
|
+
if (removedKeys.length > 0) {
|
|
95
|
+
writeCodexConfig({
|
|
96
|
+
path: config.path,
|
|
97
|
+
previousText: config.text,
|
|
98
|
+
previousConfig: config.config,
|
|
99
|
+
nextText,
|
|
100
|
+
});
|
|
101
|
+
removed.push(...removedKeys.map((key) => `otel.${key}`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const hooks = readCodexHooks();
|
|
105
|
+
if (hooks.exists) {
|
|
106
|
+
const result = removeCodexHooks(hooks.file);
|
|
107
|
+
if (result.removed) {
|
|
108
|
+
writeCodexHooks(result.file, hooks.path);
|
|
109
|
+
removed.push("SessionStart/SessionEnd hooks");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { removed };
|
|
113
|
+
},
|
|
114
|
+
pendingActions() {
|
|
115
|
+
// Codex refuses to run a hooks.json it has not been shown, and there is no way to
|
|
116
|
+
// approve it from here — the prompt is part of the TUI. Left unsaid, this looks
|
|
117
|
+
// exactly like a broken install: telemetry arrives, repository context never does.
|
|
118
|
+
if (!readCodexHooks().exists)
|
|
119
|
+
return [];
|
|
120
|
+
const { untrusted } = readCodexHookTrust();
|
|
121
|
+
if (untrusted.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
// Says what is missing, never what is already fine. Trust is keyed on a hash of the
|
|
124
|
+
// hook entry, so rewriting hooks.json invalidates every approval it had — `trusted`
|
|
125
|
+
// can be stale the moment setup runs.
|
|
126
|
+
const missing = untrusted.length === HOOK_EVENT_COUNT
|
|
127
|
+
? "its hooks"
|
|
128
|
+
: `its ${untrusted.join(" and ")} hook`;
|
|
129
|
+
return [
|
|
130
|
+
"Start Codex once and answer 'Trust all and continue' when it asks\n" +
|
|
131
|
+
` about ${missing}. Until then Codex records your spend but not the\n` +
|
|
132
|
+
" repository it was spent on — it will not run an unapproved hook.",
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const opencodeAdapter = {
|
|
137
|
+
id: "opencode",
|
|
138
|
+
label: "OpenCode",
|
|
139
|
+
provider: "opencode",
|
|
140
|
+
binary: "opencode",
|
|
141
|
+
// The plugin runs inside OpenCode's own event loop and does not await the work it
|
|
142
|
+
// starts, so the hook is already off the critical path by the time it reaches the CLI.
|
|
143
|
+
hostRunsHooksAsync: true,
|
|
144
|
+
// `session.idle` is the closest thing OpenCode has to a session end, and it fires
|
|
145
|
+
// after every turn rather than once.
|
|
146
|
+
hostRepeatsSessionEnd: true,
|
|
147
|
+
paths: () => [opencodePluginPath()],
|
|
148
|
+
read() {
|
|
149
|
+
const plugin = readOpencodePlugin();
|
|
150
|
+
return {
|
|
151
|
+
token: plugin.config?.token ?? null,
|
|
152
|
+
endpoint: plugin.config?.endpoint ?? null,
|
|
153
|
+
// Both events come from the one plugin file, so they are installed together or not at
|
|
154
|
+
// all — unlike the hook agents, where each event can go missing on its own.
|
|
155
|
+
hookCommands: plugin.config
|
|
156
|
+
? {
|
|
157
|
+
SessionStart: plugin.config.hookArgv.join(" "),
|
|
158
|
+
SessionEnd: plugin.config.hookArgv.join(" "),
|
|
159
|
+
}
|
|
160
|
+
: {},
|
|
161
|
+
modifiedAt: plugin.modifiedAt,
|
|
162
|
+
paths: [plugin.path],
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
apply(input) {
|
|
166
|
+
const { backupPath } = writeOpencodePlugin(buildOpencodePlugin({
|
|
167
|
+
endpoint: input.endpoint,
|
|
168
|
+
token: input.token,
|
|
169
|
+
hookArgv: splitCommandLine(input.hookCommand),
|
|
170
|
+
}));
|
|
171
|
+
return { backupPaths: backupPath ? [backupPath] : [] };
|
|
172
|
+
},
|
|
173
|
+
remove() {
|
|
174
|
+
return { removed: removeOpencodePlugin() ? ["the Clocktopus plugin"] : [] };
|
|
175
|
+
},
|
|
176
|
+
pendingActions: () => [],
|
|
177
|
+
staleReason() {
|
|
178
|
+
const plugin = readOpencodePlugin();
|
|
179
|
+
if (!plugin.exists || !plugin.config)
|
|
180
|
+
return null;
|
|
181
|
+
if (plugin.version === OPENCODE_PLUGIN_VERSION)
|
|
182
|
+
return null;
|
|
183
|
+
return (`the installed plugin was generated by an older CLI ` +
|
|
184
|
+
`(generation ${plugin.version ?? "unstamped"}, current is ` +
|
|
185
|
+
`${OPENCODE_PLUGIN_VERSION})`);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
export const AGENTS = [
|
|
189
|
+
claudeAdapter,
|
|
190
|
+
codexAdapter,
|
|
191
|
+
opencodeAdapter,
|
|
192
|
+
];
|
|
193
|
+
// What is on this machine, and what is already wired up.
|
|
194
|
+
//
|
|
195
|
+
// Detection is by `--version` rather than by config directory: `~/.claude` and
|
|
196
|
+
// `~/.codex` both outlive an uninstall, so a stale directory would offer to configure
|
|
197
|
+
// an agent that is no longer there.
|
|
198
|
+
export function surveyAgents() {
|
|
199
|
+
return AGENTS.map((agent) => {
|
|
200
|
+
const version = detectVersion(agent.binary);
|
|
201
|
+
let configured = false;
|
|
202
|
+
let unreadable = null;
|
|
203
|
+
try {
|
|
204
|
+
configured = agent.read().token !== null;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (isConfigParseError(error))
|
|
208
|
+
unreadable = error.message;
|
|
209
|
+
else
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
agent,
|
|
214
|
+
version,
|
|
215
|
+
installed: version !== null,
|
|
216
|
+
configured,
|
|
217
|
+
unreadable,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function detectVersion(binary) {
|
|
222
|
+
try {
|
|
223
|
+
const output = execFileSync(binary, ["--version"], {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
226
|
+
timeout: 5000,
|
|
227
|
+
});
|
|
228
|
+
// Both CLIs print a line with the version somewhere in it; the number is the only
|
|
229
|
+
// part worth showing.
|
|
230
|
+
return (/\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim().split("\n")[0] ?? null);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function newest(dates) {
|
|
237
|
+
return dates.reduce((latest, date) => (date && (!latest || date > latest) ? date : latest), null);
|
|
238
|
+
}
|
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads and edits `~/.claude/settings.json` on the user's behalf.
|
|
3
|
-
*
|
|
4
|
-
* Two rules govern everything here, both of them about not destroying a file
|
|
5
|
-
* we do not own:
|
|
6
|
-
*
|
|
7
|
-
* 1. **Never write over JSON we could not parse.** A malformed settings file
|
|
8
|
-
* is far more likely to be a half-finished edit than something to
|
|
9
|
-
* overwrite, and overwriting it would lose the user's own hooks,
|
|
10
|
-
* permissions and MCP servers. Parse failures raise instead.
|
|
11
|
-
* 2. **Only ever touch keys we put there.** Merging into `env` and appending
|
|
12
|
-
* to `hooks` leaves everything else untouched, and removal matches our
|
|
13
|
-
* own hook command rather than clearing the arrays.
|
|
14
|
-
*/
|
|
15
1
|
export declare const SETTINGS_FILENAME = "settings.json";
|
|
16
2
|
/** Env keys `clocktopus agent setup` owns — and the only ones it removes. */
|
|
17
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"];
|
|
18
|
-
/**
|
|
19
|
-
* How often Claude Code's exporter ships metrics.
|
|
20
|
-
*
|
|
21
|
-
* Also the resolution of every "last export received" answer the status
|
|
22
|
-
* command can give — a session that started 30s ago has genuinely not
|
|
23
|
-
* exported yet, which is why status treats silence under one interval as
|
|
24
|
-
* "waiting" rather than "broken".
|
|
25
|
-
*/
|
|
26
4
|
export declare const METRIC_EXPORT_INTERVAL_MS = 60000;
|
|
27
|
-
/**
|
|
28
|
-
* Seconds Claude Code will wait for the hook before giving up on it.
|
|
29
|
-
*
|
|
30
|
-
* Comfortably above the hook's own 4s request timeout, so a slow network
|
|
31
|
-
* produces the hook's own recorded failure — which `agent doctor` can read
|
|
32
|
-
* back — rather than a kill from the host, which leaves no trace anywhere.
|
|
33
|
-
*/
|
|
34
5
|
export declare const HOOK_TIMEOUT_SECONDS = 10;
|
|
35
6
|
export type ClaudeSettings = Record<string, unknown>;
|
|
36
7
|
export declare class SettingsParseError extends Error {
|
|
@@ -45,13 +16,6 @@ export declare function readSettings(path?: string): {
|
|
|
45
16
|
modifiedAt: Date | null;
|
|
46
17
|
settings: ClaudeSettings;
|
|
47
18
|
};
|
|
48
|
-
/**
|
|
49
|
-
* Writes settings, keeping a one-deep backup of what was there before.
|
|
50
|
-
*
|
|
51
|
-
* The rename is what makes it atomic: a crash midway leaves either the old
|
|
52
|
-
* file or the new one, never a truncated file that Claude Code would refuse
|
|
53
|
-
* to start with.
|
|
54
|
-
*/
|
|
55
19
|
export declare function writeSettings(settings: ClaudeSettings, path?: string): {
|
|
56
20
|
backupPath: string | null;
|
|
57
21
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-settings.d.ts","sourceRoot":"","sources":["../../../src/lib/claude-settings.ts"],"names":[],"mappings":"
|
|
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,yRAUrB,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,CAkBzB;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"}
|