@zgeoff/atc 2.12.0 → 2.13.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 +10 -10
- package/package.json +2 -1
- package/src/agents/agent-adapter.ts +48 -11
- package/src/agents/build-atc-bridge-files.ts +13 -0
- package/src/agents/build-claude-query-options.ts +72 -0
- package/src/agents/build-cli-command.ts +7 -5
- package/src/{daemon → agents}/build-headless-env.ts +12 -6
- package/src/agents/build-hook-settings.ts +8 -2
- package/src/agents/build-restore-mode-args.ts +23 -0
- package/src/agents/claude-adapter.ts +64 -17
- package/src/agents/codex-adapter.ts +5 -0
- package/src/agents/find-claude-permission-mode.ts +24 -0
- package/src/agents/gateway-adapter.ts +38 -11
- package/src/agents/grok-adapter.ts +5 -0
- package/src/agents/make-claude-headless-runner.ts +46 -0
- package/src/agents/plan-pasted-line-input.ts +22 -0
- package/src/agents/plan-typed-line-input.ts +8 -0
- package/src/agents/resolve-claude-permission-mode.ts +13 -0
- package/src/{daemon/start-headless-run.ts → agents/start-claude-headless-run.ts} +6 -53
- package/src/agents/write-atc-bridge.ts +2 -6
- package/src/cli.ts +8 -12
- package/src/client/index.ts +50 -1
- package/src/client/ui.ts +8 -0
- package/src/daemon/build-execution-targets.ts +49 -10
- package/src/daemon/build-imp-provider.ts +68 -0
- package/src/daemon/build-report-trail-entry.ts +4 -2
- package/src/daemon/build-scoped-context.ts +4 -1
- package/src/daemon/build-session-lifecycle.ts +52 -0
- package/src/daemon/build-tar-archive.ts +85 -0
- package/src/daemon/daemon-connection.ts +97 -4
- package/src/daemon/daemon.ts +298 -41
- package/src/daemon/execution-provider.ts +109 -4
- package/src/daemon/hooks.ts +5 -17
- package/src/daemon/imp-client-port.ts +343 -0
- package/src/daemon/imp-harness.ts +618 -0
- package/src/daemon/imp-port-error.ts +18 -0
- package/src/daemon/imp-port.ts +246 -0
- package/src/daemon/imp-provider.ts +601 -0
- package/src/daemon/is-binding-current.ts +43 -0
- package/src/daemon/local-pty-provider.ts +62 -3
- package/src/daemon/materialize-workspace.ts +571 -0
- package/src/daemon/parse-hook-line.ts +31 -0
- package/src/daemon/pick-session-state.ts +15 -0
- package/src/daemon/restore-fleet.ts +41 -23
- package/src/daemon/screen-model.ts +7 -0
- package/src/daemon/sessions.ts +574 -61
- package/src/daemon/start-headless-turn.ts +1 -1
- package/src/daemon/start-session-bridge.ts +294 -0
- package/src/mcp/mcp-tools.ts +9 -2
- package/src/mcp/require-daemon-features.ts +3 -0
- package/src/mcp/run-tool.ts +27 -8
- package/src/protocol/daemon-features.ts +11 -0
- package/src/protocol/protocol.ts +23 -0
- package/src/protocol/request-param-schemas.ts +66 -0
- package/src/report.ts +37 -2
- package/src/run-bridge-tap.ts +241 -0
- package/src/shared/collect-clean-env.ts +9 -1
- package/src/shared/open-bridge-socket.ts +86 -0
- package/src/shared/send-bridge-request.ts +47 -0
- package/src/statusline.ts +34 -3
- package/src/store/fleet-entry.ts +19 -0
- package/src/store/run-migrations.ts +76 -0
- package/src/store/state-store.ts +182 -21
- package/src/store/trail-entry.ts +4 -0
- package/src/store/workspace-materialization.ts +70 -0
- package/src/tap.ts +11 -0
- package/src/workspace/check-url-credentials.ts +59 -0
- package/src/workspace/create-workspace-clone.ts +13 -1
- package/src/workspace/repository-env-vars.ts +22 -0
- package/src/workspace/run-git.ts +2 -18
- /package/src/{daemon → agents}/resolve-headless-executable.ts +0 -0
package/README.md
CHANGED
|
@@ -74,16 +74,16 @@ mark:
|
|
|
74
74
|
|
|
75
75
|
Sessions that need you sort to the top, so the one you want is nearly always first.
|
|
76
76
|
|
|
77
|
-
| Key | Action
|
|
78
|
-
| ------------ |
|
|
79
|
-
| `Ctrl-Space` | open or close the list
|
|
80
|
-
| `Tab` | attach the session that needs you most
|
|
81
|
-
| `Enter` | attach the selected session
|
|
82
|
-
| `/` | filter by name or directory
|
|
83
|
-
| `a` | acknowledge a notification without attaching
|
|
84
|
-
| `K` | kill the selected session
|
|
85
|
-
| `q` | quit the client; sessions keep running
|
|
86
|
-
| `?` | every other key
|
|
77
|
+
| Key | Action |
|
|
78
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `Ctrl-Space` | open or close the list |
|
|
80
|
+
| `Tab` | attach the session that needs you most |
|
|
81
|
+
| `Enter` | attach the selected session |
|
|
82
|
+
| `/` | filter by name or directory |
|
|
83
|
+
| `a` | acknowledge a notification without attaching |
|
|
84
|
+
| `K` | kill the selected session; on a host atc can destroy, a dead session asks once more before forgetting destroys the host |
|
|
85
|
+
| `q` | quit the client; sessions keep running |
|
|
86
|
+
| `?` | every other key |
|
|
87
87
|
|
|
88
88
|
The directory picker lists the directory you ran `atc` from, atc's own spawn history, every project
|
|
89
89
|
under the roots in `config.json`, and your [zoxide](https://github.com/ajeetdsouza/zoxide) list when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/atc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@better-auth/oauth-provider": "1.7.6",
|
|
43
43
|
"@xterm/addon-serialize": "0.14.0",
|
|
44
44
|
"@xterm/headless": "6.0.0",
|
|
45
|
+
"@zgeoff/imp-client": "0.24.0",
|
|
45
46
|
"better-auth": "1.7.6",
|
|
46
47
|
"bun-pty": "0.4.10",
|
|
47
48
|
"citty": "0.2.2",
|
|
@@ -46,6 +46,24 @@ export interface SpawnPlan {
|
|
|
46
46
|
args: string[];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Where a session on a remote host finds atc: the atc binary inside the
|
|
51
|
+
* host, null when the host has none, and the folder the session's own
|
|
52
|
+
* files unpack into.
|
|
53
|
+
*/
|
|
54
|
+
export interface GuestPaths {
|
|
55
|
+
readonly atc: string | null;
|
|
56
|
+
readonly dir: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A spawn on a remote host, with the files the harness reads there, keyed
|
|
61
|
+
* by their path inside the session's guest folder.
|
|
62
|
+
*/
|
|
63
|
+
export interface GuestSpawnPlan extends SpawnPlan {
|
|
64
|
+
readonly files: Readonly<Record<string, string>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
export interface AdapterEvent {
|
|
50
68
|
kind: 'started' | 'needs-input' | 'turn-done' | 'prompt-submitted' | 'ended' | 'heartbeat';
|
|
51
69
|
agentSessionID?: AgentSessionID;
|
|
@@ -101,28 +119,24 @@ export interface ResumeCheck {
|
|
|
101
119
|
readonly transcriptSource?: string;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
interface HeadlessRunRequest {
|
|
122
|
+
export interface HeadlessRunRequest {
|
|
105
123
|
readonly cwd: string;
|
|
106
124
|
readonly prompt: string;
|
|
107
125
|
readonly resume?: AgentSessionID;
|
|
108
|
-
readonly permissionMode?: string;
|
|
109
|
-
|
|
110
|
-
// Settings file the run's CLI is started with, so a headless turn reaches
|
|
111
|
-
// the same backend the session's terminal did.
|
|
112
|
-
readonly settings?: string;
|
|
113
126
|
|
|
114
|
-
// The atc session the run belongs to
|
|
127
|
+
// The atc session the run belongs to.
|
|
115
128
|
readonly sessionID?: SessionID;
|
|
116
129
|
|
|
117
|
-
// Folder of the atc-bridge mod the run's CLI loads, supplied by the adapter.
|
|
118
|
-
readonly pluginDir?: string;
|
|
119
|
-
|
|
120
130
|
// The session's own model and effort, so a headless turn keeps them.
|
|
121
131
|
readonly model?: string;
|
|
122
132
|
readonly effort?: string;
|
|
133
|
+
|
|
134
|
+
// Environment variable names the run's process goes without, such as the
|
|
135
|
+
// credential a session's workspace was cloned with.
|
|
136
|
+
readonly withheldEnv?: readonly string[];
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
interface HeadlessRunEvents {
|
|
139
|
+
export interface HeadlessRunEvents {
|
|
126
140
|
readonly onOutput: (text: string) => void;
|
|
127
141
|
|
|
128
142
|
// The turn's whole final message.
|
|
@@ -186,6 +200,16 @@ export interface SpawnOptionSpecs {
|
|
|
186
200
|
readonly effort: SpawnOptionSpec;
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The input modes the agent's TUI has switched on, as the session's screen
|
|
205
|
+
* model last saw them.
|
|
206
|
+
*/
|
|
207
|
+
export interface TerminalInputModes {
|
|
208
|
+
// Whether the TUI asked for pasted text to arrive between bracketed paste
|
|
209
|
+
// markers (DEC mode 2004).
|
|
210
|
+
readonly bracketedPaste: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
189
213
|
/**
|
|
190
214
|
* Everything specific to one agent CLI: how to spawn it, how to read its
|
|
191
215
|
* hook payloads, where its session names come from, and how to resume a
|
|
@@ -210,6 +234,15 @@ export interface AgentAdapter {
|
|
|
210
234
|
// Absent on a stand-in adapter, which `agents.list` reports as not installed.
|
|
211
235
|
readonly profile?: AgentProfile;
|
|
212
236
|
readonly planSpawn: (opts: SpawnOptions) => SpawnPlan;
|
|
237
|
+
|
|
238
|
+
// Plans a spawn on a remote host; null when this agent cannot run there,
|
|
239
|
+
// such as one whose instrumentation needs atc on a host without it.
|
|
240
|
+
// Absent: the agent runs there as a local spawn plans it, with no files.
|
|
241
|
+
readonly planGuestSpawn?: (opts: SpawnOptions, guest: GuestPaths) => GuestSpawnPlan | null;
|
|
242
|
+
|
|
243
|
+
// The command that exits 0 inside a remote host when the agent there can
|
|
244
|
+
// sign in without a person. Absent: atc runs no check.
|
|
245
|
+
readonly planAuthCheck?: () => readonly string[];
|
|
213
246
|
readonly normalizeHook: (e: HookEvent) => AdapterEvent;
|
|
214
247
|
readonly loadName: (
|
|
215
248
|
source: string,
|
|
@@ -225,4 +258,8 @@ export interface AgentAdapter {
|
|
|
225
258
|
// null for a line that is not one. Absent: atc cannot read this agent's
|
|
226
259
|
// transcript.
|
|
227
260
|
readonly parseTranscriptLine?: (line: string) => TranscriptRow | null;
|
|
261
|
+
|
|
262
|
+
// The PTY writes, in order, that type a line into the agent's TUI and
|
|
263
|
+
// submit it. Absent: the line and a trailing newline go as one write.
|
|
264
|
+
readonly planLineInput?: (text: string, modes: TerminalInputModes) => readonly string[];
|
|
228
265
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ATC_BRIDGE_FILES } from './atc-bridge-files';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The files of the atc-bridge mod, keyed by their path inside the mod's
|
|
5
|
+
* folder, with the atc command the mod runs rendered from `argv`.
|
|
6
|
+
*/
|
|
7
|
+
export function buildATCBridgeFiles(argv: readonly string[]): Readonly<Record<string, string>> {
|
|
8
|
+
return { ...ATC_BRIDGE_FILES, 'hooks/atc-cli.ts': renderATCCLIModule(argv) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function renderATCCLIModule(argv: readonly string[]): string {
|
|
12
|
+
return `export const ATC_CLI: readonly string[] = ${JSON.stringify(argv)};\n`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Options } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { socketPath } from '../shared/config';
|
|
3
|
+
import type { HeadlessRunRequest } from './agent-adapter';
|
|
4
|
+
import { buildHeadlessEnv } from './build-headless-env';
|
|
5
|
+
import { resolveHeadlessExecutable } from './resolve-headless-executable';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One headless turn as the Claude CLI runs it: the neutral turn request plus
|
|
9
|
+
* the binary, permission mode, mod folder, and settings file the Claude and
|
|
10
|
+
* gateway adapters supply.
|
|
11
|
+
*/
|
|
12
|
+
export interface ClaudeHeadlessRunRequest extends HeadlessRunRequest {
|
|
13
|
+
// The Claude Code binary a compiled atc hands the Agent SDK.
|
|
14
|
+
readonly claudeBin: string;
|
|
15
|
+
|
|
16
|
+
// An SDK permission mode; any other value leaves the SDK's default.
|
|
17
|
+
readonly permissionMode?: string;
|
|
18
|
+
|
|
19
|
+
// Settings file the run's CLI is started with, so a headless turn reaches
|
|
20
|
+
// the same backend the session's terminal did.
|
|
21
|
+
readonly settings?: string;
|
|
22
|
+
|
|
23
|
+
// Folder of the atc-bridge mod the run's CLI loads.
|
|
24
|
+
readonly pluginDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PERMISSION_MODES = [
|
|
28
|
+
'default',
|
|
29
|
+
'acceptEdits',
|
|
30
|
+
'bypassPermissions',
|
|
31
|
+
'plan',
|
|
32
|
+
'auto',
|
|
33
|
+
'dontAsk',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
// The effort levels the Agent SDK accepts.
|
|
37
|
+
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Agent SDK options one headless turn runs with, apart from the abort
|
|
41
|
+
* controller and stderr sink the live run owns. A permission mode or effort
|
|
42
|
+
* the SDK does not accept is left out, so the SDK applies its own default.
|
|
43
|
+
*/
|
|
44
|
+
export function buildClaudeQueryOptions(
|
|
45
|
+
opts: ClaudeHeadlessRunRequest,
|
|
46
|
+
compiled: boolean,
|
|
47
|
+
): Options {
|
|
48
|
+
const mode = PERMISSION_MODES.find((m) => m === opts.permissionMode);
|
|
49
|
+
const effort = EFFORT_LEVELS.find((level) => level === opts.effort);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
cwd: opts.cwd,
|
|
53
|
+
env: buildHeadlessEnv({
|
|
54
|
+
socketPath,
|
|
55
|
+
...(opts.pluginDir === undefined ? {} : { pluginDir: opts.pluginDir }),
|
|
56
|
+
...(opts.sessionID === undefined ? {} : { sessionID: opts.sessionID }),
|
|
57
|
+
...(opts.withheldEnv === undefined ? {} : { withheldEnv: opts.withheldEnv }),
|
|
58
|
+
}),
|
|
59
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
60
|
+
...(mode === undefined ? {} : { permissionMode: mode }),
|
|
61
|
+
|
|
62
|
+
// The session's own model and effort, so the turn runs as its terminal
|
|
63
|
+
// did.
|
|
64
|
+
...(opts.model === undefined ? {} : { model: opts.model }),
|
|
65
|
+
...(effort === undefined ? {} : { effort }),
|
|
66
|
+
|
|
67
|
+
// The same generated file the terminal spawn passes, so the turn runs
|
|
68
|
+
// against the session's own backend and instrumentation.
|
|
69
|
+
...(opts.settings === undefined ? {} : { extraArgs: { settings: opts.settings } }),
|
|
70
|
+
...resolveHeadlessExecutable(opts.claudeBin, compiled),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -3,10 +3,12 @@ import { buildCLIArgv } from './build-cli-argv';
|
|
|
3
3
|
/**
|
|
4
4
|
* Command line that wrangled sessions invoke for atc subcommands: under bun
|
|
5
5
|
* the CLI entry path is part of the command; a compiled binary is itself
|
|
6
|
-
* the entry.
|
|
6
|
+
* the entry. A session on a remote host passes the argv of the atc inside
|
|
7
|
+
* that host.
|
|
7
8
|
*/
|
|
8
|
-
export function buildCLICommand(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export function buildCLICommand(
|
|
10
|
+
subcommand: string,
|
|
11
|
+
argv: readonly string[] = buildCLIArgv(),
|
|
12
|
+
): string {
|
|
13
|
+
return `${argv.map((part) => `"${part}"`).join(' ')} ${subcommand}`;
|
|
12
14
|
}
|
|
@@ -5,6 +5,9 @@ interface HeadlessEnvParams {
|
|
|
5
5
|
readonly pluginDir?: string;
|
|
6
6
|
readonly sessionID?: SessionID;
|
|
7
7
|
readonly socketPath: string;
|
|
8
|
+
|
|
9
|
+
// Variables the run goes without, such as a workspace credential.
|
|
10
|
+
readonly withheldEnv?: readonly string[];
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -14,10 +17,13 @@ interface HeadlessEnvParams {
|
|
|
14
17
|
* since the mod stays off outside atc.
|
|
15
18
|
*/
|
|
16
19
|
export function buildHeadlessEnv(params: HeadlessEnvParams): Record<string, string> {
|
|
17
|
-
return collectCleanEnv(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
return collectCleanEnv(
|
|
21
|
+
{
|
|
22
|
+
...(params.pluginDir === undefined ? {} : { CLAUDE_CODE_PLUGIN_DIRS: params.pluginDir }),
|
|
23
|
+
...(params.sessionID === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { ATC_SESSION_ID: params.sessionID, ATC_SOCKET: params.socketPath }),
|
|
26
|
+
},
|
|
27
|
+
params.withheldEnv,
|
|
28
|
+
);
|
|
23
29
|
}
|
|
@@ -26,13 +26,19 @@ export interface HookSettingsProfile {
|
|
|
26
26
|
* a session's backend is decided here rather than by whatever the terminal
|
|
27
27
|
* happened to carry. The credential is never part of it: the helper command
|
|
28
28
|
* supplies that at run time, so it never reaches a file atc writes.
|
|
29
|
+
*
|
|
30
|
+
* A session on a remote host gets the argv of the atc inside that host,
|
|
31
|
+
* whose statusline renders that session's own state alone.
|
|
29
32
|
*/
|
|
30
33
|
export function buildHookSettings(
|
|
31
34
|
profile: HookSettingsProfile,
|
|
32
35
|
statuslinePadding: number,
|
|
36
|
+
cliArgv?: readonly string[],
|
|
33
37
|
): Record<string, unknown> {
|
|
34
38
|
const entry = [
|
|
35
|
-
{
|
|
39
|
+
{
|
|
40
|
+
hooks: [{ type: 'command', command: buildCLICommand('hook-report', cliArgv), timeout: 5 }],
|
|
41
|
+
},
|
|
36
42
|
];
|
|
37
43
|
|
|
38
44
|
const own = {
|
|
@@ -55,7 +61,7 @@ export function buildHookSettings(
|
|
|
55
61
|
// padding.
|
|
56
62
|
statusLine: {
|
|
57
63
|
type: 'command',
|
|
58
|
-
command: buildCLICommand('statusline'),
|
|
64
|
+
command: buildCLICommand('statusline', cliArgv),
|
|
59
65
|
padding: statuslinePadding,
|
|
60
66
|
},
|
|
61
67
|
...(profile.env === undefined || Object.keys(profile.env).length === 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { findClaudePermissionMode } from './find-claude-permission-mode';
|
|
2
|
+
import { findFlagValue } from './find-flag-value';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The arguments that restore a Claude CLI session in its configured
|
|
6
|
+
* permission mode. A resumed session otherwise takes back the mode it was
|
|
7
|
+
* saved in, and only an explicit `--permission-mode` overrides that, so a
|
|
8
|
+
* mode the settings alone set travels as the flag. Nothing is added when
|
|
9
|
+
* the arguments already carry the flag or no mode is configured, so an
|
|
10
|
+
* unconfigured session restores the mode it was saved in.
|
|
11
|
+
*/
|
|
12
|
+
export function buildRestoreModeArgs(
|
|
13
|
+
args: readonly string[],
|
|
14
|
+
settings: Readonly<Record<string, unknown>> | undefined,
|
|
15
|
+
): string[] {
|
|
16
|
+
if (findFlagValue(args, ['--permission-mode']) !== null) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const mode = findClaudePermissionMode([], settings);
|
|
21
|
+
|
|
22
|
+
return mode === null ? [] : ['--permission-mode', mode];
|
|
23
|
+
}
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
AdapterEvent,
|
|
12
12
|
AgentAdapter,
|
|
13
13
|
AgentProfile,
|
|
14
|
+
GuestPaths,
|
|
15
|
+
GuestSpawnPlan,
|
|
14
16
|
HeadlessRunner,
|
|
15
17
|
NameUpdate,
|
|
16
18
|
ResumeCheck,
|
|
@@ -18,10 +20,17 @@ import type {
|
|
|
18
20
|
SpawnOptions,
|
|
19
21
|
SpawnPlan,
|
|
20
22
|
} from './agent-adapter';
|
|
23
|
+
import { buildATCBridgeFiles } from './build-atc-bridge-files';
|
|
21
24
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
25
|
+
import { buildHookSettings } from './build-hook-settings';
|
|
22
26
|
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
27
|
+
import { findClaudePermissionMode } from './find-claude-permission-mode';
|
|
23
28
|
import { findFlagValue } from './find-flag-value';
|
|
29
|
+
import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
|
|
30
|
+
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
24
31
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
32
|
+
import { planTypedLineInput } from './plan-typed-line-input';
|
|
33
|
+
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
25
34
|
import { truncateDetail } from './truncate-detail';
|
|
26
35
|
import { writeATCBridge } from './write-atc-bridge';
|
|
27
36
|
import { writeHookSettings } from './write-hook-settings';
|
|
@@ -53,6 +62,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
53
62
|
|
|
54
63
|
readonly parseTranscriptLine = parseClaudeTranscriptLine;
|
|
55
64
|
|
|
65
|
+
// Claude's TUI submits a line typed with its newline in one write.
|
|
66
|
+
readonly planLineInput = planTypedLineInput;
|
|
67
|
+
|
|
56
68
|
readonly profile: AgentProfile;
|
|
57
69
|
|
|
58
70
|
readonly takesMessages = true;
|
|
@@ -67,7 +79,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
67
79
|
|
|
68
80
|
private readonly bridgeTarget: string | undefined;
|
|
69
81
|
|
|
70
|
-
constructor(config: Config,
|
|
82
|
+
constructor(config: Config, headlessRun: ClaudeHeadlessRun | null = null, bridgeTarget?: string) {
|
|
71
83
|
this.bridgeTarget = bridgeTarget;
|
|
72
84
|
this.config = config;
|
|
73
85
|
|
|
@@ -80,9 +92,13 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
this.headlessRunner =
|
|
83
|
-
|
|
95
|
+
headlessRun === null
|
|
84
96
|
? null
|
|
85
|
-
: (
|
|
97
|
+
: makeClaudeHeadlessRunner(headlessRun, {
|
|
98
|
+
claudeBin: config.claudeBin,
|
|
99
|
+
permissionMode: resolveClaudePermissionMode(config.claudeArgs, undefined),
|
|
100
|
+
pluginDir: () => this.writeBridge(),
|
|
101
|
+
});
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
@@ -90,19 +106,47 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
90
106
|
|
|
91
107
|
return {
|
|
92
108
|
bin: this.config.claudeBin,
|
|
93
|
-
args:
|
|
94
|
-
...buildClaudeOverrideArgs(this.config.claudeArgs, opts),
|
|
95
|
-
'--settings',
|
|
96
|
-
this.settingsFile,
|
|
97
|
-
'--plugin-dir',
|
|
98
|
-
this.writeBridge(),
|
|
99
|
-
...(opts.resume === true ? ['--resume'] : []),
|
|
100
|
-
...(typeof opts.resume === 'string' ? ['--resume', opts.resume] : []),
|
|
101
|
-
...(opts.prompt === '' ? [] : [opts.prompt]),
|
|
102
|
-
],
|
|
109
|
+
args: this.buildArgs(opts, this.settingsFile, this.writeBridge()),
|
|
103
110
|
};
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// A remote session reports through the atc inside its host, so without
|
|
114
|
+
// one it has no instrumentation and cannot run there. Its settings and
|
|
115
|
+
// its copy of the mod travel with it, and it has no statusline.
|
|
116
|
+
planGuestSpawn(opts: SpawnOptions, guest: GuestPaths): GuestSpawnPlan | null {
|
|
117
|
+
if (guest.atc === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const argv = [guest.atc];
|
|
122
|
+
|
|
123
|
+
const bridge = Object.entries(buildATCBridgeFiles(argv)).map(
|
|
124
|
+
([path, content]): [string, string] => [`atc-bridge/${path}`, content],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
bin: this.config.claudeBin,
|
|
129
|
+
args: this.buildArgs(opts, `${guest.dir}/settings.json`, `${guest.dir}/atc-bridge`),
|
|
130
|
+
files: {
|
|
131
|
+
'settings.json': JSON.stringify(buildHookSettings({ id: this.id }, 0, argv), null, 2),
|
|
132
|
+
...Object.fromEntries(bridge),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildArgs(opts: SpawnOptions, settings: string, pluginDir: string): string[] {
|
|
138
|
+
return [
|
|
139
|
+
...buildClaudeOverrideArgs(this.config.claudeArgs, opts),
|
|
140
|
+
'--settings',
|
|
141
|
+
settings,
|
|
142
|
+
'--plugin-dir',
|
|
143
|
+
pluginDir,
|
|
144
|
+
...(opts.resume === true ? ['--resume'] : []),
|
|
145
|
+
...(typeof opts.resume === 'string' ? ['--resume', opts.resume] : []),
|
|
146
|
+
...(opts.prompt === '' ? [] : [opts.prompt]),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
private writeBridge(): string {
|
|
107
151
|
this.bridgeDir ??= writeATCBridge(this.bridgeTarget);
|
|
108
152
|
|
|
@@ -233,12 +277,15 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
233
277
|
return existsSync(session.transcriptSource);
|
|
234
278
|
}
|
|
235
279
|
|
|
236
|
-
// Shell command that re-opens this session outside atc (or anywhere).
|
|
280
|
+
// Shell command that re-opens this session outside atc (or anywhere). A
|
|
281
|
+
// permission mode the configured arguments set travels as an explicit
|
|
282
|
+
// flag, so it overrides the mode the CLI would restore.
|
|
237
283
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
238
|
-
const
|
|
239
|
-
|
|
284
|
+
const configured = findClaudePermissionMode(this.config.claudeArgs, undefined);
|
|
285
|
+
const mode = configured === null ? '' : ` --permission-mode ${toShellArg(configured)}`;
|
|
286
|
+
const resume = agentSessionID === undefined ? '' : ` ${agentSessionID}`;
|
|
240
287
|
|
|
241
|
-
return `cd ${toShellArg(cwd)} && ${resume}`;
|
|
288
|
+
return `cd ${toShellArg(cwd)} && claude${mode} --resume${resume}`;
|
|
242
289
|
}
|
|
243
290
|
}
|
|
244
291
|
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from './agent-adapter';
|
|
21
21
|
import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
22
22
|
import { findFlagValue } from './find-flag-value';
|
|
23
|
+
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
23
24
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
24
25
|
import { truncateDetail } from './truncate-detail';
|
|
25
26
|
|
|
@@ -58,6 +59,10 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
58
59
|
|
|
59
60
|
readonly takesMessages = false;
|
|
60
61
|
|
|
62
|
+
// Codex's TUI keeps a newline that arrives inside a burst of input, so a
|
|
63
|
+
// line is pasted and then submitted.
|
|
64
|
+
readonly planLineInput = planPastedLineInput;
|
|
65
|
+
|
|
61
66
|
private readonly config: Config;
|
|
62
67
|
|
|
63
68
|
constructor(config: Config) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isRecord } from '../shared/report';
|
|
2
|
+
import { findFlagValue } from './find-flag-value';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The permission mode a Claude CLI agent's configuration sets: an explicit
|
|
6
|
+
* `--permission-mode` in its arguments, else the `permissions.defaultMode`
|
|
7
|
+
* its settings hold, or null when neither sets one. The argument wins, as it
|
|
8
|
+
* does for the CLI itself.
|
|
9
|
+
*/
|
|
10
|
+
export function findClaudePermissionMode(
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
settings: Readonly<Record<string, unknown>> | undefined,
|
|
13
|
+
): string | null {
|
|
14
|
+
const flag = findFlagValue(args, ['--permission-mode']);
|
|
15
|
+
|
|
16
|
+
if (flag !== null) {
|
|
17
|
+
return flag;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const permissions = settings?.['permissions'];
|
|
21
|
+
const defaultMode = isRecord(permissions) ? permissions['defaultMode'] : undefined;
|
|
22
|
+
|
|
23
|
+
return typeof defaultMode === 'string' && defaultMode !== '' ? defaultMode : null;
|
|
24
|
+
}
|
|
@@ -16,10 +16,15 @@ import type {
|
|
|
16
16
|
SpawnPlan,
|
|
17
17
|
} from './agent-adapter';
|
|
18
18
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
19
|
+
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
19
20
|
import { ClaudeAdapter } from './claude-adapter';
|
|
20
21
|
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
21
22
|
import { findFlagValue } from './find-flag-value';
|
|
23
|
+
import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
|
|
24
|
+
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
22
25
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
26
|
+
import { planTypedLineInput } from './plan-typed-line-input';
|
|
27
|
+
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
23
28
|
import { writeATCBridge } from './write-atc-bridge';
|
|
24
29
|
import { writeHookSettings } from './write-hook-settings';
|
|
25
30
|
|
|
@@ -43,6 +48,9 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
43
48
|
// The gateway runs the Claude CLI, which writes the same transcript.
|
|
44
49
|
readonly parseTranscriptLine = parseClaudeTranscriptLine;
|
|
45
50
|
|
|
51
|
+
// The gateway runs the Claude CLI, whose TUI takes a line the same way.
|
|
52
|
+
readonly planLineInput = planTypedLineInput;
|
|
53
|
+
|
|
46
54
|
readonly takesMessages = true;
|
|
47
55
|
|
|
48
56
|
readonly profile: AgentProfile;
|
|
@@ -62,7 +70,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
62
70
|
constructor(
|
|
63
71
|
gateway: GatewayConfig,
|
|
64
72
|
config: Config,
|
|
65
|
-
|
|
73
|
+
headlessRun: ClaudeHeadlessRun | null = null,
|
|
66
74
|
bridgeTarget?: string,
|
|
67
75
|
) {
|
|
68
76
|
this.bridgeTarget = bridgeTarget;
|
|
@@ -82,13 +90,14 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
82
90
|
this.claude = new ClaudeAdapter(config);
|
|
83
91
|
|
|
84
92
|
this.headlessRunner =
|
|
85
|
-
|
|
93
|
+
headlessRun === null
|
|
86
94
|
? null
|
|
87
|
-
: (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
95
|
+
: makeClaudeHeadlessRunner(headlessRun, {
|
|
96
|
+
claudeBin: gateway.bin,
|
|
97
|
+
permissionMode: resolveClaudePermissionMode(gateway.args, gateway.settings),
|
|
98
|
+
pluginDir: () => this.writeBridge(),
|
|
99
|
+
settings: () => this.writeSettings(),
|
|
100
|
+
});
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
@@ -96,6 +105,9 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
96
105
|
bin: this.gateway.bin,
|
|
97
106
|
args: [
|
|
98
107
|
...buildClaudeOverrideArgs(this.gateway.args, opts),
|
|
108
|
+
...(opts.resume === false
|
|
109
|
+
? []
|
|
110
|
+
: buildRestoreModeArgs(this.gateway.args, this.gateway.settings)),
|
|
99
111
|
'--settings',
|
|
100
112
|
this.writeSettings(),
|
|
101
113
|
'--plugin-dir',
|
|
@@ -107,6 +119,12 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
// The credential helper runs on the daemon's machine, so a gateway
|
|
123
|
+
// session never runs on a remote host.
|
|
124
|
+
planGuestSpawn(): null {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
normalizeHook(e: HookEvent): AdapterEvent {
|
|
111
129
|
return this.claude.normalizeHook(e);
|
|
112
130
|
}
|
|
@@ -119,14 +137,23 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
119
137
|
return this.claude.canResume(session);
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
// Shell command that re-opens this session outside atc. It
|
|
123
|
-
//
|
|
124
|
-
//
|
|
140
|
+
// Shell command that re-opens this session outside atc. It carries the
|
|
141
|
+
// gateway's configured arguments and its configured permission mode as an
|
|
142
|
+
// explicit flag, so that mode overrides the one the CLI would restore, and
|
|
143
|
+
// the generated settings file, because
|
|
144
|
+
// without it the CLI would resume the session against the default backend.
|
|
125
145
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
146
|
+
const args = [
|
|
147
|
+
...this.gateway.args,
|
|
148
|
+
...buildRestoreModeArgs(this.gateway.args, this.gateway.settings),
|
|
149
|
+
]
|
|
150
|
+
.map((arg) => ` ${toShellArg(arg)}`)
|
|
151
|
+
.join('');
|
|
152
|
+
|
|
126
153
|
const settings = toShellArg(this.writeSettings());
|
|
127
154
|
const resume = agentSessionID === undefined ? '' : ` ${agentSessionID}`;
|
|
128
155
|
|
|
129
|
-
return `cd ${toShellArg(cwd)} && ${this.gateway.bin} --settings ${settings} --resume${resume}`;
|
|
156
|
+
return `cd ${toShellArg(cwd)} && ${this.gateway.bin}${args} --settings ${settings} --resume${resume}`;
|
|
130
157
|
}
|
|
131
158
|
|
|
132
159
|
private writeSettings(): string {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
SpawnPlan,
|
|
21
21
|
} from './agent-adapter';
|
|
22
22
|
import { normalizeHookEventName } from './normalize-hook-event';
|
|
23
|
+
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
23
24
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
24
25
|
import { truncateDetail } from './truncate-detail';
|
|
25
26
|
|
|
@@ -76,6 +77,10 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
76
77
|
|
|
77
78
|
readonly takesMessages = false;
|
|
78
79
|
|
|
80
|
+
// Grok's TUI keeps a newline that arrives inside a burst of input, so a
|
|
81
|
+
// line is pasted and then submitted.
|
|
82
|
+
readonly planLineInput = planPastedLineInput;
|
|
83
|
+
|
|
79
84
|
private readonly config: Config;
|
|
80
85
|
|
|
81
86
|
private readonly hookState = new Map<SessionID, GrokSessionHookState>();
|