@zgeoff/atc 3.3.1 → 3.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/package.json +4 -2
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +11 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +13 -1
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +48 -15
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +85 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway.ts +4 -2
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +15 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +22 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- package/src/test-utils/build-stub-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/{fixture-imp-port.ts → build-stub-imp-port.ts} +119 -63
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -0
- package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +120 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +5 -5
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +99 -0
- package/src/test-utils/setup-mcp-http.ts +19 -31
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +25 -8
- package/src/test-utils/start-daemon-process.ts +192 -0
- package/src/test-utils/start-git-http-server.ts +21 -3
- package/src/test-utils/start-mcp-stdio.ts +195 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +103 -0
- package/src/test-utils/start-stub-impd-info.ts +32 -0
- package/src/test-utils/start-stub-impd.ts +138 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +166 -0
- package/src/test-utils/start-stub-mcp-server.ts +50 -0
- package/src/test-utils/start-stub-refusing-listener.ts +38 -0
- package/src/test-utils/start-stub-reporter-socket.ts +77 -0
- package/src/test-utils/start-stub-restore-daemon.ts +58 -0
- package/src/test-utils/start-stub-session-bridge.ts +98 -0
- package/src/test-utils/start-stub-silent-server.ts +33 -0
- package/src/test-utils/start-stub-source-daemon.ts +39 -0
- package/src/test-utils/start-stub-stalled-listener.ts +40 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +70 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +26 -0
- package/src/test-utils/start-test-daemon.ts +215 -0
- package/src/test-utils/start-tui-harness.ts +284 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +59 -12
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- package/src/test-utils/start-legacy-daemon.ts +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/atc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Terminal control tower for coding-agent sessions",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/atc#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/atc/issues",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lint:fix": "oxlint --type-aware --type-check --report-unused-disable-directives-severity error --fix",
|
|
31
31
|
"test": "bash scripts/with-test-home.sh bun test --parallel --timings=bun-test-timings.json",
|
|
32
32
|
"test:timings": "bash scripts/with-test-home.sh bun test --parallel --timings=bun-test-timings.json --update-timings",
|
|
33
|
-
"test:binary": "bash scripts/with-test-home.sh bun test --timeout 20000 e2e/daemon
|
|
33
|
+
"test:binary": "bash scripts/with-test-home.sh bun test --timeout 20000 e2e/daemon-*.test.ts",
|
|
34
34
|
"test:gateway-binary": "bash scripts/with-test-home.sh bun test --timeout 20000 src/gateway.test.ts",
|
|
35
35
|
"test:atc-bridge": "bash scripts/with-test-home.sh bash scripts/test-atc-bridge.sh",
|
|
36
36
|
"test:isolation": "bash scripts/check-test-isolation.sh",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@commitlint/cli": "21.2.0",
|
|
60
|
+
"@faker-js/faker": "10.6.0",
|
|
60
61
|
"@stylistic/eslint-plugin": "5.10.0",
|
|
61
62
|
"@tsconfig/strictest": "2.0.8",
|
|
62
63
|
"@types/bun": "1.4.2",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"oxfmt": "0.57.0",
|
|
69
70
|
"oxlint": "1.73.0",
|
|
70
71
|
"oxlint-tsgolint": "0.24.0",
|
|
72
|
+
"tiny-invariant": "1.3.3",
|
|
71
73
|
"tslib": "2.8.1",
|
|
72
74
|
"type-fest": "5.8.0",
|
|
73
75
|
"typescript": "7.0.2"
|
|
@@ -179,6 +179,16 @@ export interface HeadlessRunEvents {
|
|
|
179
179
|
readonly onNeedsYou: (msg: string) => void;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Where a Claude-family adapter writes its generated settings files and
|
|
184
|
+
* where it finds the user's home, whose Claude config it reads. A field
|
|
185
|
+
* left out takes atc's state folder or the running user's home.
|
|
186
|
+
*/
|
|
187
|
+
export interface ClaudeHostPaths {
|
|
188
|
+
readonly stateDir?: string;
|
|
189
|
+
readonly homeDir?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
182
192
|
export type HeadlessRunner = (
|
|
183
193
|
opts: HeadlessRunRequest,
|
|
184
194
|
hooks: HeadlessRunEvents,
|
|
@@ -6,8 +6,6 @@ import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
|
6
6
|
* the CLI entry path follows the runtime; a compiled binary is itself the
|
|
7
7
|
* entry.
|
|
8
8
|
*/
|
|
9
|
-
export function buildCLIArgv(): string[] {
|
|
10
|
-
return
|
|
11
|
-
? [process.execPath]
|
|
12
|
-
: [process.execPath, join(import.meta.dir, '..', 'cli.ts')];
|
|
9
|
+
export function buildCLIArgv(compiled: boolean = isCompiledBinary()): string[] {
|
|
10
|
+
return compiled ? [process.execPath] : [process.execPath, join(import.meta.dir, '..', 'cli.ts')];
|
|
13
11
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
AgentAdapter,
|
|
20
20
|
AgentProfile,
|
|
21
21
|
AuthSelection,
|
|
22
|
+
ClaudeHostPaths,
|
|
22
23
|
GuestPaths,
|
|
23
24
|
GuestSpawnPlan,
|
|
24
25
|
HeadlessRunner,
|
|
@@ -106,13 +107,17 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
106
107
|
|
|
107
108
|
private readonly bridgeTarget: string | undefined;
|
|
108
109
|
|
|
110
|
+
private readonly hostPaths: ClaudeHostPaths;
|
|
111
|
+
|
|
109
112
|
constructor(
|
|
110
113
|
entry: AgentEntry,
|
|
111
114
|
config: Pick<Config, 'authProfiles'>,
|
|
112
115
|
headlessRun: ClaudeHeadlessRun | null = null,
|
|
113
116
|
bridgeTarget?: string,
|
|
117
|
+
hostPaths: ClaudeHostPaths = {},
|
|
114
118
|
) {
|
|
115
119
|
this.bridgeTarget = bridgeTarget;
|
|
120
|
+
this.hostPaths = hostPaths;
|
|
116
121
|
this.entry = entry;
|
|
117
122
|
this.authProfiles = config.authProfiles;
|
|
118
123
|
this.id = entry.id;
|
|
@@ -282,8 +287,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
282
287
|
);
|
|
283
288
|
|
|
284
289
|
const bundle = loadClaudeConfigBundle(
|
|
285
|
-
resolveAgentHome('CLAUDE_CONFIG_DIR', '.claude'),
|
|
290
|
+
resolveAgentHome('CLAUDE_CONFIG_DIR', '.claude', this.hostPaths.homeDir),
|
|
286
291
|
launch.env.CLAUDE_CONFIG_DIR,
|
|
292
|
+
this.hostPaths.homeDir,
|
|
287
293
|
);
|
|
288
294
|
|
|
289
295
|
const userSettings = bundle['settings.json'];
|
|
@@ -406,7 +412,11 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
private writeSettings(): string {
|
|
409
|
-
this.settingsFile ??= writeHookSettings(
|
|
415
|
+
this.settingsFile ??= writeHookSettings(
|
|
416
|
+
this.buildSettingsProfile({}),
|
|
417
|
+
this.hostPaths.stateDir,
|
|
418
|
+
this.hostPaths.homeDir,
|
|
419
|
+
);
|
|
410
420
|
|
|
411
421
|
return this.settingsFile;
|
|
412
422
|
}
|
|
@@ -420,7 +430,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
420
430
|
// A session on the daemon's machine reads the user's own Claude config,
|
|
421
431
|
// so trust for the clone is that config's entry for the clone alone.
|
|
422
432
|
updateLocalWorkspaceTrust(root: string): Promise<() => Promise<void>> {
|
|
423
|
-
return updateClaudeProjectTrust(resolveClaudeGlobalConfigPath(), root);
|
|
433
|
+
return updateClaudeProjectTrust(resolveClaudeGlobalConfigPath(this.hostPaths.homeDir), root);
|
|
424
434
|
}
|
|
425
435
|
|
|
426
436
|
normalizeHook(e: HookEvent): AdapterEvent {
|
|
@@ -91,9 +91,14 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
91
91
|
|
|
92
92
|
private readonly authProfiles: Config['authProfiles'];
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// The Codex home whose session index holds session titles; unset, it follows
|
|
95
|
+
// `CODEX_HOME` or the user's home at each read.
|
|
96
|
+
private readonly codexHome: string | undefined;
|
|
97
|
+
|
|
98
|
+
constructor(entry: AgentEntry, config?: Pick<Config, 'authProfiles'>, codexHome?: string) {
|
|
95
99
|
this.entry = entry;
|
|
96
100
|
this.authProfiles = config?.authProfiles ?? new Map();
|
|
101
|
+
this.codexHome = codexHome;
|
|
97
102
|
this.id = entry.id;
|
|
98
103
|
|
|
99
104
|
if (entry.auth !== undefined) {
|
|
@@ -323,7 +328,11 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
323
328
|
return Promise.resolve(null);
|
|
324
329
|
}
|
|
325
330
|
|
|
326
|
-
const index = join(
|
|
331
|
+
const index = join(
|
|
332
|
+
this.codexHome ?? resolveAgentHome('CODEX_HOME', '.codex'),
|
|
333
|
+
'session_index.jsonl',
|
|
334
|
+
);
|
|
335
|
+
|
|
327
336
|
let text: string;
|
|
328
337
|
|
|
329
338
|
try {
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
AgentAdapter,
|
|
14
14
|
AgentProfile,
|
|
15
15
|
AuthSelection,
|
|
16
|
+
ClaudeHostPaths,
|
|
16
17
|
GuestPaths,
|
|
17
18
|
GuestSpawnPlan,
|
|
18
19
|
HeadlessRunner,
|
|
@@ -87,13 +88,17 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
87
88
|
|
|
88
89
|
private readonly bridgeTarget: string | undefined;
|
|
89
90
|
|
|
91
|
+
private readonly hostPaths: ClaudeHostPaths;
|
|
92
|
+
|
|
90
93
|
constructor(
|
|
91
94
|
gateway: GatewayConfig,
|
|
92
95
|
config: Config,
|
|
93
96
|
headlessRun: ClaudeHeadlessRun | null = null,
|
|
94
97
|
bridgeTarget?: string,
|
|
98
|
+
hostPaths: ClaudeHostPaths = {},
|
|
95
99
|
) {
|
|
96
100
|
this.bridgeTarget = bridgeTarget;
|
|
101
|
+
this.hostPaths = hostPaths;
|
|
97
102
|
this.gateway = gateway;
|
|
98
103
|
this.config = config;
|
|
99
104
|
this.id = gateway.id;
|
|
@@ -121,6 +126,9 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
121
126
|
env: {},
|
|
122
127
|
},
|
|
123
128
|
config,
|
|
129
|
+
null,
|
|
130
|
+
undefined,
|
|
131
|
+
hostPaths,
|
|
124
132
|
);
|
|
125
133
|
|
|
126
134
|
this.headlessRunner =
|
|
@@ -433,14 +441,18 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
433
441
|
}
|
|
434
442
|
|
|
435
443
|
private writeSettings(): string {
|
|
436
|
-
this.settingsFile ??= writeHookSettings(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
this.settingsFile ??= writeHookSettings(
|
|
445
|
+
{
|
|
446
|
+
id: this.id,
|
|
447
|
+
env: { ANTHROPIC_BASE_URL: this.gateway.baseURL, ...this.gateway.env },
|
|
448
|
+
...(this.gateway.apiKeyHelper === undefined
|
|
449
|
+
? {}
|
|
450
|
+
: { apiKeyHelper: this.gateway.apiKeyHelper }),
|
|
451
|
+
...(this.gateway.settings === undefined ? {} : { settings: this.gateway.settings }),
|
|
452
|
+
},
|
|
453
|
+
this.hostPaths.stateDir,
|
|
454
|
+
this.hostPaths.homeDir,
|
|
455
|
+
);
|
|
444
456
|
|
|
445
457
|
return this.settingsFile;
|
|
446
458
|
}
|
|
@@ -90,8 +90,13 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
90
90
|
|
|
91
91
|
private readonly hookState = new Map<SessionID, GrokSessionHookState>();
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// Grok's home directory, or null to read it from GROK_HOME (or ~/.grok)
|
|
94
|
+
// each time it is needed.
|
|
95
|
+
private readonly home: string | null;
|
|
96
|
+
|
|
97
|
+
constructor(entry: AgentEntry, home: string | null = null) {
|
|
94
98
|
this.entry = entry;
|
|
99
|
+
this.home = home;
|
|
95
100
|
this.id = entry.id;
|
|
96
101
|
|
|
97
102
|
this.profile = {
|
|
@@ -130,7 +135,7 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
130
135
|
const named: AdapterEvent = {
|
|
131
136
|
...base,
|
|
132
137
|
...(agentSessionID !== undefined && payload.cwd !== undefined
|
|
133
|
-
? { nameSource: buildGrokNameSource(agentSessionID, payload.cwd) }
|
|
138
|
+
? { nameSource: buildGrokNameSource(this.resolveHome(), agentSessionID, payload.cwd) }
|
|
134
139
|
: {}),
|
|
135
140
|
};
|
|
136
141
|
|
|
@@ -213,7 +218,7 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
loadName(source: string, namedBy: 'user' | 'auto' | 'agent'): Promise<NameUpdate | null> {
|
|
216
|
-
const parsed = readSummary(source) ?? readSummary(findGrokSummary(source));
|
|
221
|
+
const parsed = readSummary(source) ?? readSummary(findGrokSummary(this.resolveHome(), source));
|
|
217
222
|
|
|
218
223
|
if (parsed === null) {
|
|
219
224
|
return Promise.resolve(null);
|
|
@@ -277,6 +282,10 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
277
282
|
return event;
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
private resolveHome(): string {
|
|
286
|
+
return this.home ?? resolveAgentHome('GROK_HOME', '.grok');
|
|
287
|
+
}
|
|
288
|
+
|
|
280
289
|
// A Map preserves insertion order, so the first key is the oldest entry.
|
|
281
290
|
private removeOldestHookState(): void {
|
|
282
291
|
const oldest = this.hookState.keys().next().value;
|
|
@@ -287,14 +296,8 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
287
296
|
}
|
|
288
297
|
}
|
|
289
298
|
|
|
290
|
-
function buildGrokNameSource(sessionID: AgentSessionID, cwd: string): string {
|
|
291
|
-
return join(
|
|
292
|
-
resolveAgentHome('GROK_HOME', '.grok'),
|
|
293
|
-
'sessions',
|
|
294
|
-
encodeURIComponent(cwd),
|
|
295
|
-
sessionID,
|
|
296
|
-
'summary.json',
|
|
297
|
-
);
|
|
299
|
+
function buildGrokNameSource(home: string, sessionID: AgentSessionID, cwd: string): string {
|
|
300
|
+
return join(home, 'sessions', encodeURIComponent(cwd), sessionID, 'summary.json');
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
function buildTurnDoneEvent(named: Readonly<AdapterEvent>, payload: GrokHookPayload): AdapterEvent {
|
|
@@ -323,14 +326,14 @@ function readSummary(source: string | null): Record<string, unknown> | null {
|
|
|
323
326
|
}
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
function findGrokSummary(source: string): string | null {
|
|
329
|
+
function findGrokSummary(home: string, source: string): string | null {
|
|
327
330
|
const sessionID = source.split('/').at(-2);
|
|
328
331
|
|
|
329
332
|
if (sessionID === undefined || sessionID === '') {
|
|
330
333
|
return null;
|
|
331
334
|
}
|
|
332
335
|
|
|
333
|
-
const root = join(
|
|
336
|
+
const root = join(home, 'sessions');
|
|
334
337
|
|
|
335
338
|
try {
|
|
336
339
|
for (const group of readdirSync(root)) {
|
|
@@ -17,17 +17,19 @@ import { buildClaudeBundleSettings } from './build-claude-bundle-settings';
|
|
|
17
17
|
* starts with a dot. A symlink ships only when what it resolves to could
|
|
18
18
|
* ship by its own path: a file whose name starts with a dot never does, and
|
|
19
19
|
* neither does a file in the host's folder outside the shipped entries. A
|
|
20
|
-
* missing entry ships nothing.
|
|
20
|
+
* missing entry ships nothing. `homeDir` is the user's home, which decides
|
|
21
|
+
* whether the host folder is the default one.
|
|
21
22
|
*/
|
|
22
23
|
export function loadClaudeConfigBundle(
|
|
23
24
|
hostDir: string,
|
|
24
25
|
guestDir: string,
|
|
26
|
+
homeDir: string = resolveHomeDir(),
|
|
25
27
|
): Record<string, GuestFile> {
|
|
26
28
|
const hostSettings = readSettings(join(hostDir, 'settings.json'));
|
|
27
29
|
|
|
28
30
|
const settings = buildClaudeBundleSettings(
|
|
29
31
|
hostSettings,
|
|
30
|
-
buildHostDirSpellings(hostDir),
|
|
32
|
+
buildHostDirSpellings(hostDir, homeDir),
|
|
31
33
|
guestDir,
|
|
32
34
|
);
|
|
33
35
|
|
|
@@ -66,10 +68,8 @@ export function loadClaudeConfigBundle(
|
|
|
66
68
|
// The ways a command can spell the host's config folder: its path, and
|
|
67
69
|
// when it is the default folder in the user's home, the home-relative forms
|
|
68
70
|
// a shell expands.
|
|
69
|
-
function buildHostDirSpellings(hostDir: string): string[] {
|
|
70
|
-
return hostDir === join(
|
|
71
|
-
? [hostDir, '~/.claude', '$HOME/.claude']
|
|
72
|
-
: [hostDir];
|
|
71
|
+
function buildHostDirSpellings(hostDir: string, homeDir: string): string[] {
|
|
72
|
+
return hostDir === join(homeDir, '.claude') ? [hostDir, '~/.claude', '$HOME/.claude'] : [hostDir];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// The host's settings, or none when the file is missing or is not JSON,
|
|
@@ -4,8 +4,12 @@ import { buildCodexHookFile } from './build-codex-hook-file';
|
|
|
4
4
|
* Print the Codex hook entries to stdout. The operator merges them into
|
|
5
5
|
* `$CODEX_HOME/hooks.json` and trusts them once in the Codex TUI — Codex
|
|
6
6
|
* parses untrusted hooks but never runs them. atc never writes the user's
|
|
7
|
-
* own Codex config.
|
|
7
|
+
* own Codex config. `write` takes the printed text, stdout by default.
|
|
8
8
|
*/
|
|
9
|
-
export function printCodexHookFile(
|
|
10
|
-
|
|
9
|
+
export function printCodexHookFile(
|
|
10
|
+
write: (text: string) => void = (text) => {
|
|
11
|
+
process.stdout.write(text);
|
|
12
|
+
},
|
|
13
|
+
): void {
|
|
14
|
+
write(buildCodexHookFile());
|
|
11
15
|
}
|
|
@@ -12,10 +12,15 @@ const GROK_HOOK_EVENTS = [
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Print the Grok hook file to stdout. The operator copies it to
|
|
15
|
-
* `$GROK_HOME/hooks/atc-reporter.json`; atc never writes that path.
|
|
15
|
+
* `$GROK_HOME/hooks/atc-reporter.json`; atc never writes that path. `write`
|
|
16
|
+
* takes the printed text, stdout by default.
|
|
16
17
|
*/
|
|
17
|
-
export function printGrokHookFile(
|
|
18
|
-
|
|
18
|
+
export function printGrokHookFile(
|
|
19
|
+
write: (text: string) => void = (text) => {
|
|
20
|
+
process.stdout.write(text);
|
|
21
|
+
},
|
|
22
|
+
): void {
|
|
23
|
+
write(buildGrokHookFile());
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function buildGrokHookFile(): string {
|
|
@@ -4,10 +4,14 @@ import { resolveHomeDir } from '../shared/resolve-home-dir';
|
|
|
4
4
|
/**
|
|
5
5
|
* An agent CLI's home directory: the named environment variable when it is
|
|
6
6
|
* set and non-empty, otherwise the given default directory under the user's
|
|
7
|
-
* home
|
|
7
|
+
* home `homeDir`.
|
|
8
8
|
*/
|
|
9
|
-
export function resolveAgentHome(
|
|
9
|
+
export function resolveAgentHome(
|
|
10
|
+
envVar: string,
|
|
11
|
+
defaultDirName: string,
|
|
12
|
+
homeDir: string = resolveHomeDir(),
|
|
13
|
+
): string {
|
|
10
14
|
const home = process.env[envVar];
|
|
11
15
|
|
|
12
|
-
return home !== undefined && home !== '' ? home : join(
|
|
16
|
+
return home !== undefined && home !== '' ? home : join(homeDir, defaultDirName);
|
|
13
17
|
}
|
|
@@ -7,12 +7,12 @@ import { resolveHomeDir } from '../shared/resolve-home-dir';
|
|
|
7
7
|
* in, folder trust included, resolved as the CLI resolves it: a legacy
|
|
8
8
|
* `.config.json` in its config folder when one exists, otherwise
|
|
9
9
|
* `.claude.json` in `$CLAUDE_CONFIG_DIR`, or in the user's home when that
|
|
10
|
-
* is unset or empty.
|
|
10
|
+
* is unset or empty, where `homeDir` is the user's home.
|
|
11
11
|
*/
|
|
12
|
-
export function resolveClaudeGlobalConfigPath(): string {
|
|
12
|
+
export function resolveClaudeGlobalConfigPath(homeDir: string = resolveHomeDir()): string {
|
|
13
13
|
const configDir = process.env['CLAUDE_CONFIG_DIR'];
|
|
14
14
|
const custom = configDir !== undefined && configDir !== '' ? configDir : null;
|
|
15
|
-
const legacy = join(custom ?? join(
|
|
15
|
+
const legacy = join(custom ?? join(homeDir, '.claude'), '.config.json');
|
|
16
16
|
|
|
17
|
-
return existsSync(legacy) ? legacy : join(custom ??
|
|
17
|
+
return existsSync(legacy) ? legacy : join(custom ?? homeDir, '.claude.json');
|
|
18
18
|
}
|
|
@@ -3,6 +3,7 @@ import { readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promise
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { isRecord } from '../shared/report';
|
|
5
5
|
import { withClaudeConfigLock } from './with-claude-config-lock';
|
|
6
|
+
import type { ClaudeConfigLockOptions } from './with-claude-config-lock';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Accepts the Claude CLI's folder trust for one exact directory in its
|
|
@@ -15,30 +16,36 @@ import { withClaudeConfigLock } from './with-claude-config-lock';
|
|
|
15
16
|
* Resolves to a function that takes the trust back, for a launch that
|
|
16
17
|
* fails before the CLI starts: it puts back the entry as it was, unless
|
|
17
18
|
* something has changed the entry since, which leaves it as it stands.
|
|
19
|
+
* The lock options apply to both the trust and its taking back.
|
|
18
20
|
*/
|
|
19
21
|
export async function updateClaudeProjectTrust(
|
|
20
22
|
configPath: string,
|
|
21
23
|
root: string,
|
|
24
|
+
lockOptions: ClaudeConfigLockOptions = {},
|
|
22
25
|
): Promise<() => Promise<void>> {
|
|
23
|
-
const previous = await withClaudeConfigLock(
|
|
24
|
-
|
|
26
|
+
const previous = await withClaudeConfigLock(
|
|
27
|
+
configPath,
|
|
28
|
+
async () => {
|
|
29
|
+
const config = await loadClaudeConfig(configPath);
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const projects = isRecord(config['projects']) ? config['projects'] : {};
|
|
32
|
+
const entry = projects[root];
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
if (isRecord(entry) && entry['hasTrustDialogAccepted'] === true) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
const base = isRecord(entry) ? entry : {};
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
await writeClaudeConfig(configPath, {
|
|
41
|
+
...config,
|
|
42
|
+
projects: { ...projects, [root]: { ...base, hasTrustDialogAccepted: true } },
|
|
43
|
+
});
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
return { entry: isRecord(entry) ? entry : null };
|
|
46
|
+
},
|
|
47
|
+
lockOptions,
|
|
48
|
+
);
|
|
42
49
|
|
|
43
50
|
if (previous === null) {
|
|
44
51
|
return () => Promise.resolve();
|
|
@@ -50,22 +57,26 @@ export async function updateClaudeProjectTrust(
|
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
return () =>
|
|
53
|
-
withClaudeConfigLock(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
withClaudeConfigLock(
|
|
61
|
+
configPath,
|
|
62
|
+
async () => {
|
|
63
|
+
const config = await loadClaudeConfig(configPath);
|
|
64
|
+
|
|
65
|
+
const projects = isRecord(config['projects']) ? config['projects'] : {};
|
|
66
|
+
|
|
67
|
+
if (JSON.stringify(projects[root]) !== written) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { [root]: _trusted, ...others } = projects;
|
|
72
|
+
|
|
73
|
+
await writeClaudeConfig(configPath, {
|
|
74
|
+
...config,
|
|
75
|
+
projects: previous.entry === null ? others : { ...others, [root]: previous.entry },
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
lockOptions,
|
|
79
|
+
);
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
// The config as the CLI last wrote it; a missing file is an empty config,
|
|
@@ -12,6 +12,15 @@ const REFRESH_MS = 1000;
|
|
|
12
12
|
const WAIT_MS = 5000;
|
|
13
13
|
const RETRY_MS = 50;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Tuning a caller may pass: how often a held lock's age is refreshed, and a
|
|
17
|
+
* callback run each time the lock is found held by someone else.
|
|
18
|
+
*/
|
|
19
|
+
export interface ClaudeConfigLockOptions {
|
|
20
|
+
readonly refreshMs?: number;
|
|
21
|
+
readonly onBusy?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Runs the callback while holding the lock the Claude CLI takes before it
|
|
17
26
|
* writes its global config: a directory beside the file named for it with
|
|
@@ -25,6 +34,7 @@ const RETRY_MS = 50;
|
|
|
25
34
|
export async function withClaudeConfigLock<T>(
|
|
26
35
|
configPath: string,
|
|
27
36
|
run: () => Promise<T>,
|
|
37
|
+
options: ClaudeConfigLockOptions = {},
|
|
28
38
|
): Promise<T> {
|
|
29
39
|
const lockPath = `${configPath}.lock`;
|
|
30
40
|
const deadline = Date.now() + WAIT_MS;
|
|
@@ -34,6 +44,8 @@ export async function withClaudeConfigLock<T>(
|
|
|
34
44
|
let held = await tryCreateLockDir(lockPath);
|
|
35
45
|
|
|
36
46
|
while (held === null) {
|
|
47
|
+
options.onBusy?.();
|
|
48
|
+
|
|
37
49
|
if (Date.now() > deadline) {
|
|
38
50
|
throw new Error(`timed out waiting for the Claude config lock ${lockPath}`);
|
|
39
51
|
}
|
|
@@ -52,7 +64,7 @@ export async function withClaudeConfigLock<T>(
|
|
|
52
64
|
|
|
53
65
|
owned = refreshed ?? owned;
|
|
54
66
|
})();
|
|
55
|
-
}, REFRESH_MS);
|
|
67
|
+
}, options.refreshMs ?? REFRESH_MS);
|
|
56
68
|
|
|
57
69
|
try {
|
|
58
70
|
return await run();
|
|
@@ -6,14 +6,17 @@ import { buildCLIArgv } from './build-cli-argv';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Writes the atc-bridge mod that every Claude session loads with
|
|
9
|
-
* `--plugin-dir`, with the atc command it runs rendered
|
|
10
|
-
* and returns its folder. A file whose content already
|
|
11
|
-
* untouched, since any write reloads the mod in every running
|
|
12
|
-
* the folder is never cleared, since Claude Code keeps its own
|
|
13
|
-
* there.
|
|
9
|
+
* `--plugin-dir`, with the atc command it runs rendered in (by default this
|
|
10
|
+
* install's own), and returns its folder. A file whose content already
|
|
11
|
+
* matches is left untouched, since any write reloads the mod in every running
|
|
12
|
+
* session, and the folder is never cleared, since Claude Code keeps its own
|
|
13
|
+
* type files there.
|
|
14
14
|
*/
|
|
15
|
-
export function writeATCBridge(
|
|
16
|
-
|
|
15
|
+
export function writeATCBridge(
|
|
16
|
+
dir: string = join(stateDir, 'atc-bridge'),
|
|
17
|
+
argv: readonly string[] = buildCLIArgv(),
|
|
18
|
+
): string {
|
|
19
|
+
const files = buildATCBridgeFiles(argv);
|
|
17
20
|
|
|
18
21
|
for (const [path, content] of Object.entries(files)) {
|
|
19
22
|
const file = join(dir, path);
|
|
@@ -8,13 +8,19 @@ import type { HookSettingsProfile } from './build-hook-settings';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Writes the settings file passed to a wrangled session as
|
|
11
|
-
* `claude --settings`, one per agent id, and returns
|
|
11
|
+
* `claude --settings`, one per agent id, into the folder `dir`, and returns
|
|
12
|
+
* its path. The statusline padding comes from the Claude settings in the
|
|
13
|
+
* user's home `homeDir`.
|
|
12
14
|
*/
|
|
13
|
-
export function writeHookSettings(
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export function writeHookSettings(
|
|
16
|
+
profile: HookSettingsProfile,
|
|
17
|
+
dir: string = stateDir,
|
|
18
|
+
homeDir: string = resolveHomeDir(),
|
|
19
|
+
): string {
|
|
20
|
+
const file = join(dir, `hook-settings-${profile.id}.json`);
|
|
21
|
+
const settings = buildHookSettings(profile, readStatuslinePadding(homeDir));
|
|
16
22
|
|
|
17
|
-
mkdirSync(
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
18
24
|
writeFileSync(file, JSON.stringify(settings, null, 2));
|
|
19
25
|
|
|
20
26
|
return file;
|
|
@@ -24,9 +30,9 @@ export function writeHookSettings(profile: HookSettingsProfile): string {
|
|
|
24
30
|
* The padding on the user's own statusline, so the chained one lines up with
|
|
25
31
|
* it. An unreadable or unconfigured setting is no padding.
|
|
26
32
|
*/
|
|
27
|
-
function readStatuslinePadding(): number {
|
|
33
|
+
function readStatuslinePadding(homeDir: string): number {
|
|
28
34
|
try {
|
|
29
|
-
const raw = readFileSync(join(
|
|
35
|
+
const raw = readFileSync(join(homeDir, '.claude', 'settings.json'), 'utf8');
|
|
30
36
|
const user: unknown = JSON.parse(raw);
|
|
31
37
|
const statusLine = isRecord(user) ? user['statusLine'] : undefined;
|
|
32
38
|
const padding = isRecord(statusLine) ? statusLine['padding'] : undefined;
|