@zgeoff/atc 3.4.1 → 3.4.2
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 +1 -1
- package/src/agents/claude-adapter.ts +4 -3
- package/src/agents/gateway-adapter.ts +2 -2
- package/src/agents/plan-claude-line-input.ts +44 -0
- package/src/mcp/mcp-tools.ts +1 -1
- package/src/test-utils/build-stub-held-gh.ts +5 -7
- package/src/test-utils/build-stub-held-zoxide.ts +4 -6
- package/src/test-utils/create-stub-claude.ts +3 -1
- package/src/test-utils/create-stub-imp-port.ts +9 -0
- package/src/test-utils/{run-legacy-daemon.ts → run-stub-legacy-daemon.ts} +1 -1
- package/src/test-utils/start-stub-source-daemon.ts +1 -1
- /package/src/test-utils/{run-source-daemon.ts → run-stub-source-daemon.ts} +0 -0
package/package.json
CHANGED
|
@@ -48,7 +48,7 @@ import { loadClaudeConfigBundle } from './load-claude-config-bundle';
|
|
|
48
48
|
import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
|
|
49
49
|
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
50
50
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
51
|
-
import {
|
|
51
|
+
import { planClaudeLineInput } from './plan-claude-line-input';
|
|
52
52
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
53
53
|
import { resolveClaudeGlobalConfigPath } from './resolve-claude-global-config-path';
|
|
54
54
|
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
@@ -88,8 +88,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
88
88
|
readonly parseTranscriptLine = parseClaudeTranscriptLine;
|
|
89
89
|
|
|
90
90
|
// Claude's TUI takes a long burst of input as a paste and keeps its
|
|
91
|
-
// newline in the composer, so a line is pasted and then submitted
|
|
92
|
-
|
|
91
|
+
// newline in the composer, so a line is pasted and then submitted, with a
|
|
92
|
+
// leading slash command's name typed so the command still runs.
|
|
93
|
+
readonly planLineInput = planClaudeLineInput;
|
|
93
94
|
|
|
94
95
|
readonly profile: AgentProfile;
|
|
95
96
|
|
|
@@ -38,7 +38,7 @@ import { findFlagValue } from './find-flag-value';
|
|
|
38
38
|
import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
|
|
39
39
|
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
40
40
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
41
|
-
import {
|
|
41
|
+
import { planClaudeLineInput } from './plan-claude-line-input';
|
|
42
42
|
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
43
43
|
import { writeATCBridge } from './write-atc-bridge';
|
|
44
44
|
import { writeHookSettings } from './write-hook-settings';
|
|
@@ -68,7 +68,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
68
68
|
readonly parseTranscriptLine = parseClaudeTranscriptLine;
|
|
69
69
|
|
|
70
70
|
// The gateway runs the Claude CLI, whose TUI takes a line the same way.
|
|
71
|
-
readonly planLineInput =
|
|
71
|
+
readonly planLineInput = planClaudeLineInput;
|
|
72
72
|
|
|
73
73
|
readonly takesMessages = true;
|
|
74
74
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TerminalInputModes } from './agent-adapter';
|
|
2
|
+
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
3
|
+
|
|
4
|
+
const PASTE_START = '\u001B[200~';
|
|
5
|
+
const PASTE_END = '\u001B[201~';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pastes a line into the Claude TUI and submits it, as the pasted line
|
|
9
|
+
* planner does, except for a line that opens with a slash command and an
|
|
10
|
+
* argument. Claude takes a long paste as pasted text and sends it as a
|
|
11
|
+
* message, so such a line has its command name and the spaces after it typed
|
|
12
|
+
* as keys and only the argument pasted, then one carriage return. The
|
|
13
|
+
* command then runs at any argument length. A TUI that has not turned
|
|
14
|
+
* bracketed paste on gets every line as the pasted line planner writes it.
|
|
15
|
+
*/
|
|
16
|
+
export function planClaudeLineInput(text: string, modes: TerminalInputModes): readonly string[] {
|
|
17
|
+
const unmarked = text.replaceAll(PASTE_START, '').replaceAll(PASTE_END, '');
|
|
18
|
+
const command = modes.bracketedPaste ? findSlashCommand(unmarked) : null;
|
|
19
|
+
|
|
20
|
+
if (command === null) {
|
|
21
|
+
return planPastedLineInput(text, modes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [command.typed, `${PASTE_START}${command.argument}${PASTE_END}`, '\r'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A slash, a command name that may hold a plugin namespace, at least one
|
|
28
|
+
// space, then the argument. A path such as `/tmp/out` has a slash after its
|
|
29
|
+
// first name, so it never matches.
|
|
30
|
+
const SLASH_COMMAND = /^(?<typed>\/[A-Za-z0-9][\w:-]* +)(?<argument>.+)$/su;
|
|
31
|
+
|
|
32
|
+
function findSlashCommand(text: string): { typed: string; argument: string } | null {
|
|
33
|
+
const groups = SLASH_COMMAND.exec(text)?.groups;
|
|
34
|
+
const typed = groups?.['typed'];
|
|
35
|
+
const argument = groups?.['argument'];
|
|
36
|
+
|
|
37
|
+
// An argument of spaces alone leaves a bare command, which the line
|
|
38
|
+
// pastes whole like any short line.
|
|
39
|
+
if (typed === undefined || argument === undefined || argument.trim() === '') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { typed, argument };
|
|
44
|
+
}
|
package/src/mcp/mcp-tools.ts
CHANGED
|
@@ -428,7 +428,7 @@ export const MCP_TOOLS: readonly MCPToolDefinition[] = [
|
|
|
428
428
|
annotations: AGENT_FACING_DESTRUCTIVE,
|
|
429
429
|
scope: 'spawn',
|
|
430
430
|
description:
|
|
431
|
-
"Type a line of text into a running session and submit it, as if the operator typed it and pressed enter. atc submits the line the way the session's agent accepts one. Use it to answer a session that is waiting on input. A result of sent means atc wrote the line and its submit key to the session; it does not confirm that the agent took the line or answered it. Read the session's screen or events for that. The tool sends no raw keystrokes.",
|
|
431
|
+
"Type a line of text into a running session and submit it, as if the operator typed it and pressed enter. atc submits the line the way the session's agent accepts one. Use it to answer a session that is waiting on input. atc pastes the line, so its newlines stay in it, and Claude takes a line of about 800 characters or more as pasted text, not as typed words. On a Claude session, a line that starts with a slash command and an argument, such as /goal finish the release, has the command name typed and only the argument pasted, so the command runs at any length. A result of sent means atc wrote the line and its submit key to the session; it does not confirm that the agent took the line or answered it. Read the session's screen or events for that. The tool sends no raw keystrokes.",
|
|
432
432
|
inputSchema: {
|
|
433
433
|
type: 'object',
|
|
434
434
|
properties: {
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The script of a stand-in `gh` whose repository listing waits for the
|
|
3
|
-
* test. `gh config …` prints `https`. Any other command
|
|
4
|
-
* `$HOME/gh-
|
|
5
|
-
*
|
|
3
|
+
* test. `gh config …` prints `https`. Any other command waits while
|
|
4
|
+
* `$HOME/gh-hold` exists, touching `$HOME/gh-held` on each pass of the
|
|
5
|
+
* wait, so the marker appears only once the stand-in is held, then prints
|
|
6
|
+
* one repository, `me/dots`, in the JSON shape `gh repo list` prints.
|
|
6
7
|
*/
|
|
7
8
|
export function buildStubHeldGH(): string {
|
|
8
9
|
return `#!/bin/sh
|
|
9
10
|
case "$1" in
|
|
10
11
|
config) echo https ;;
|
|
11
12
|
*)
|
|
12
|
-
|
|
13
|
-
touch "$HOME/gh-held"
|
|
14
|
-
while [ -f "$HOME/gh-hold" ]; do sleep 0.05; done
|
|
15
|
-
fi
|
|
13
|
+
while [ -f "$HOME/gh-hold" ]; do touch "$HOME/gh-held"; sleep 0.05; done
|
|
16
14
|
echo '[{"nameWithOwner":"me/dots","description":"dotfiles","isPrivate":false,"url":"https://github.com/me/dots","sshUrl":"git@github.com:me/dots.git"}]' ;;
|
|
17
15
|
esac
|
|
18
16
|
`;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The script of a stand-in `zoxide` that lists no directories, and waits
|
|
3
|
-
* for the test first: while `$HOME/zoxide-hold` exists, it
|
|
4
|
-
* `$HOME/zoxide-held`
|
|
3
|
+
* for the test first: while `$HOME/zoxide-hold` exists, it waits for the
|
|
4
|
+
* file to go, touching `$HOME/zoxide-held` on each pass of the wait, so the
|
|
5
|
+
* marker appears only once the stand-in is held.
|
|
5
6
|
*/
|
|
6
7
|
export function buildStubHeldZoxide(): string {
|
|
7
8
|
return `#!/bin/sh
|
|
8
|
-
|
|
9
|
-
touch "$HOME/zoxide-held"
|
|
10
|
-
while [ -f "$HOME/zoxide-hold" ]; do sleep 0.05; done
|
|
11
|
-
fi
|
|
9
|
+
while [ -f "$HOME/zoxide-hold" ]; do touch "$HOME/zoxide-held"; sleep 0.05; done
|
|
12
10
|
`;
|
|
13
11
|
}
|
|
@@ -29,7 +29,8 @@ interface StubClaudeConfig {
|
|
|
29
29
|
* first stub to find it removes it; `fake-claude-tap` runs `atc tap` into
|
|
30
30
|
* `$HOME/tap.jsonl`; `fake-claude-events.jsonl` holds more hook payloads to
|
|
31
31
|
* report, one per line, after the `Notification`; `fake-claude-exit` exits
|
|
32
|
-
* once they are reported
|
|
32
|
+
* once they are reported; `fake-claude-composer-last` runs the composer once
|
|
33
|
+
* they are reported.
|
|
33
34
|
*/
|
|
34
35
|
export function createStubClaude(dir: string, config: StubClaudeConfig): string {
|
|
35
36
|
const atc = config.atc.map((part) => `"${part}"`).join(' ');
|
|
@@ -69,6 +70,7 @@ if [ -f "$HOME/fake-claude-events.jsonl" ]; then
|
|
|
69
70
|
done < "$HOME/fake-claude-events.jsonl"
|
|
70
71
|
fi
|
|
71
72
|
if [ -f "$HOME/fake-claude-exit" ]; then exit 0; fi
|
|
73
|
+
if [ -f "$HOME/fake-claude-composer-last" ]; then exec "${process.execPath}" "${config.composer}"; fi
|
|
72
74
|
while read -r line; do echo "GOT:$line"; done
|
|
73
75
|
`,
|
|
74
76
|
);
|
|
@@ -121,6 +121,9 @@ class StubImpPort implements ImpPort {
|
|
|
121
121
|
// Feature reads still to fail as an unreachable impd before they answer.
|
|
122
122
|
private featureFailures = 0;
|
|
123
123
|
|
|
124
|
+
// Feature reads that failed as an unreachable impd.
|
|
125
|
+
private failedFeatureReads = 0;
|
|
126
|
+
|
|
124
127
|
// Session connections whose opening still throws before it returns, as
|
|
125
128
|
// an invalid authorization header makes the socket's constructor throw.
|
|
126
129
|
private openFailures = 0;
|
|
@@ -198,6 +201,7 @@ class StubImpPort implements ImpPort {
|
|
|
198
201
|
|
|
199
202
|
if (this.featureFailures > 0) {
|
|
200
203
|
this.featureFailures -= 1;
|
|
204
|
+
this.failedFeatureReads += 1;
|
|
201
205
|
|
|
202
206
|
return Promise.reject(new ImpPortError('UNREACHABLE', 'impd did not answer'));
|
|
203
207
|
}
|
|
@@ -1109,6 +1113,11 @@ class StubImpPort implements ImpPort {
|
|
|
1109
1113
|
this.featureFailures = count;
|
|
1110
1114
|
}
|
|
1111
1115
|
|
|
1116
|
+
// How many feature reads failed as an unreachable impd.
|
|
1117
|
+
countFailedFeatureReads(): number {
|
|
1118
|
+
return this.failedFeatureReads;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1112
1121
|
/**
|
|
1113
1122
|
* Refuses the next session request with an impd code and its data, or the
|
|
1114
1123
|
* message of the refusal when it carries one instead.
|
|
@@ -20,7 +20,7 @@ function main() {
|
|
|
20
20
|
const stateDir = process.argv.at(3);
|
|
21
21
|
|
|
22
22
|
if (socketPath === undefined || stateDir === undefined) {
|
|
23
|
-
throw new Error('usage: run-legacy-daemon.ts <socket path> <state dir>');
|
|
23
|
+
throw new Error('usage: run-stub-legacy-daemon.ts <socket path> <state dir>');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
startStubLegacyDaemon(socketPath, { protocol: PROTOCOL_V + 1, owner: 'caller' });
|
|
@@ -13,7 +13,7 @@ import { registerTestCleanup } from './register-test-cleanup';
|
|
|
13
13
|
* a test; disposal stops it sooner, and a second stop does nothing.
|
|
14
14
|
*/
|
|
15
15
|
export async function startStubSourceDaemon(env: Readonly<Record<string, string | undefined>>) {
|
|
16
|
-
const daemon = Bun.spawn([process.execPath, join(import.meta.dir, 'run-source-daemon.ts')], {
|
|
16
|
+
const daemon = Bun.spawn([process.execPath, join(import.meta.dir, 'run-stub-source-daemon.ts')], {
|
|
17
17
|
env: { ATC_TEST_SOURCES: 'fixture', ...env },
|
|
18
18
|
stdout: 'pipe',
|
|
19
19
|
stderr: 'ignore',
|
|
File without changes
|