@zgeoff/atc 3.3.1 → 3.4.1
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 +5 -3
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +31 -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/render-sdk-message.ts +57 -0
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +37 -12
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +122 -39
- 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/resolve-workspace-root.ts +10 -5
- 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/session-runtime.ts +0 -4
- 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/gateway.ts +2 -179
- 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 +88 -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-cli.ts +327 -0
- package/src/run-gateway.ts +51 -18
- 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 +24 -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-execution-target.ts +25 -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-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +26 -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-mock-stored-event.ts +20 -0
- package/src/test-utils/build-mock-stored-row.ts +23 -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-brokered-gateway-adapter.ts +50 -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/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-mcp-stdio-server.ts +20 -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-recording-filter.ts +9 -0
- package/src/test-utils/build-stub-recording-git.ts +8 -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 +117 -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-echo-claude.ts +21 -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/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/create-stub-recording-codex.ts +20 -0
- package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
- 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/register-test-cleanup.ts +63 -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-mcp-authorization.ts +7 -4
- 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 +110 -0
- package/src/test-utils/setup-mcp-http.ts +26 -30
- package/src/test-utils/setup-temp-dir.ts +7 -6
- 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 +29 -8
- package/src/test-utils/start-daemon-process.ts +193 -0
- package/src/test-utils/start-git-http-server.ts +28 -4
- package/src/test-utils/start-mcp-stdio.ts +190 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-answering-listener.ts +65 -0
- package/src/test-utils/start-stub-closing-listener.ts +31 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +105 -0
- package/src/test-utils/start-stub-impd-info.ts +38 -0
- package/src/test-utils/start-stub-impd.ts +143 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
- package/src/test-utils/start-stub-mcp-server.ts +55 -0
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +43 -0
- package/src/test-utils/start-stub-reporter-socket.ts +79 -0
- package/src/test-utils/start-stub-restore-daemon.ts +59 -0
- package/src/test-utils/start-stub-session-bridge.ts +100 -0
- package/src/test-utils/start-stub-silent-server.ts +38 -0
- package/src/test-utils/start-stub-source-daemon.ts +41 -0
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +44 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
- package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
- package/src/test-utils/start-test-daemon.ts +229 -0
- package/src/test-utils/start-tui-harness.ts +291 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
- 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.1",
|
|
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,8 +30,8 @@
|
|
|
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
|
|
34
|
-
"test:gateway-binary": "bash scripts/with-test-home.sh bun test --timeout 20000
|
|
33
|
+
"test:binary": "bash scripts/with-test-home.sh bun test --timeout 20000 e2e/daemon-*.test.ts",
|
|
34
|
+
"test:gateway-binary": "bash scripts/with-test-home.sh bun test --timeout 20000 e2e/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",
|
|
37
37
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
@@ -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,
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { Config } from '../shared/config';
|
|
2
|
-
import type { AgentAdapter } from './agent-adapter';
|
|
2
|
+
import type { AgentAdapter, ClaudeHostPaths } from './agent-adapter';
|
|
3
3
|
import { ClaudeAdapter } from './claude-adapter';
|
|
4
4
|
import { CodexAdapter } from './codex-adapter';
|
|
5
5
|
import { GatewayAdapter } from './gateway-adapter';
|
|
6
6
|
import { GrokAdapter } from './grok-adapter';
|
|
7
7
|
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Where the Claude and gateway adapters write their settings file and their
|
|
11
|
+
* copy of the atc-bridge mod, and the home whose Claude config they read. A
|
|
12
|
+
* field left out takes atc's state folder or the running user's home.
|
|
13
|
+
*/
|
|
14
|
+
interface AgentAdapterPaths extends ClaudeHostPaths {
|
|
15
|
+
readonly bridgeTarget?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* One adapter per registry entry, in registry order. A Claude entry with a
|
|
11
20
|
* base URL is a gateway, and one without is stock Claude.
|
|
@@ -13,7 +22,10 @@ import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
|
13
22
|
export function buildAgentAdapters(
|
|
14
23
|
config: Config,
|
|
15
24
|
headlessRun: ClaudeHeadlessRun | null = null,
|
|
25
|
+
paths: AgentAdapterPaths = {},
|
|
16
26
|
): AgentAdapter[] {
|
|
27
|
+
const { bridgeTarget, ...hostPaths } = paths;
|
|
28
|
+
|
|
17
29
|
return config.agents.map((entry): AgentAdapter => {
|
|
18
30
|
if (entry.kind === 'codex') {
|
|
19
31
|
return new CodexAdapter(entry, config);
|
|
@@ -24,9 +36,15 @@ export function buildAgentAdapters(
|
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
if (entry.baseURL === undefined) {
|
|
27
|
-
return new ClaudeAdapter(entry, config, headlessRun);
|
|
39
|
+
return new ClaudeAdapter(entry, config, headlessRun, bridgeTarget, hostPaths);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
return new GatewayAdapter(
|
|
42
|
+
return new GatewayAdapter(
|
|
43
|
+
{ ...entry, baseURL: entry.baseURL },
|
|
44
|
+
config,
|
|
45
|
+
headlessRun,
|
|
46
|
+
bridgeTarget,
|
|
47
|
+
hostPaths,
|
|
48
|
+
);
|
|
31
49
|
});
|
|
32
50
|
}
|
|
@@ -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 {
|
|
@@ -53,6 +53,11 @@ type CodexHookPayload = z.infer<typeof CODEX_HOOK_PAYLOAD_SCHEMA>;
|
|
|
53
53
|
// The spellings of the Codex CLI's model flag.
|
|
54
54
|
const CODEX_MODEL_FLAGS = ['-m', '--model'];
|
|
55
55
|
|
|
56
|
+
// The Codex CLI flag that runs a terminal without the shared background
|
|
57
|
+
// server, and the flag that connects it to a remote server instead.
|
|
58
|
+
const NO_DAEMON_FLAG = '--no-daemon';
|
|
59
|
+
const REMOTE_FLAG = '--remote';
|
|
60
|
+
|
|
56
61
|
/**
|
|
57
62
|
* The Codex CLI adapter: spawn arguments, hook payload mapping, resume
|
|
58
63
|
* semantics, and session_index.jsonl name-pulling. Hooks are a user-installed
|
|
@@ -91,9 +96,14 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
91
96
|
|
|
92
97
|
private readonly authProfiles: Config['authProfiles'];
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
// The Codex home whose session index holds session titles; unset, it follows
|
|
100
|
+
// `CODEX_HOME` or the user's home at each read.
|
|
101
|
+
private readonly codexHome: string | undefined;
|
|
102
|
+
|
|
103
|
+
constructor(entry: AgentEntry, config?: Pick<Config, 'authProfiles'>, codexHome?: string) {
|
|
95
104
|
this.entry = entry;
|
|
96
105
|
this.authProfiles = config?.authProfiles ?? new Map();
|
|
106
|
+
this.codexHome = codexHome;
|
|
97
107
|
this.id = entry.id;
|
|
98
108
|
|
|
99
109
|
if (entry.auth !== undefined) {
|
|
@@ -113,6 +123,14 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
113
123
|
return {
|
|
114
124
|
bin: this.entry.bin,
|
|
115
125
|
args: [
|
|
126
|
+
// Codex's shared background server keeps the environment of the
|
|
127
|
+
// terminal that started it and runs every thread's hooks and tools
|
|
128
|
+
// there, so a terminal on it reports as that first session. Each
|
|
129
|
+
// terminal therefore runs its own server. The configured arguments
|
|
130
|
+
// already carry the flag, or connect to a remote server, which Codex
|
|
131
|
+
// refuses to combine with it; either way Codex takes no second copy.
|
|
132
|
+
...(this.entry.args.some(isDaemonChoiceFlag) ? [] : [NO_DAEMON_FLAG]),
|
|
133
|
+
|
|
116
134
|
// A model override replaces any model flag the configured arguments
|
|
117
135
|
// carry, and travels as its own argument.
|
|
118
136
|
...(opts.model === undefined
|
|
@@ -323,7 +341,11 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
323
341
|
return Promise.resolve(null);
|
|
324
342
|
}
|
|
325
343
|
|
|
326
|
-
const index = join(
|
|
344
|
+
const index = join(
|
|
345
|
+
this.codexHome ?? resolveAgentHome('CODEX_HOME', '.codex'),
|
|
346
|
+
'session_index.jsonl',
|
|
347
|
+
);
|
|
348
|
+
|
|
327
349
|
let text: string;
|
|
328
350
|
|
|
329
351
|
try {
|
|
@@ -411,3 +433,10 @@ function buildCodexSpawnOptions(codexArgs: readonly string[]): SpawnOptionSpecs
|
|
|
411
433
|
},
|
|
412
434
|
};
|
|
413
435
|
}
|
|
436
|
+
|
|
437
|
+
// Whether a configured argument already decides which server a terminal
|
|
438
|
+
// uses: the flag that runs it without the shared one, or the remote server
|
|
439
|
+
// flag in either of its spellings.
|
|
440
|
+
function isDaemonChoiceFlag(arg: string): boolean {
|
|
441
|
+
return arg === NO_DAEMON_FLAG || arg === REMOTE_FLAG || arg.startsWith(`${REMOTE_FLAG}=`);
|
|
442
|
+
}
|
|
@@ -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 {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isRecord } from '../shared/report';
|
|
2
|
+
import { truncateSummary } from '../shared/truncate-summary';
|
|
3
|
+
|
|
4
|
+
interface RenderableMessage {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly subtype?: string;
|
|
7
|
+
readonly result?: string;
|
|
8
|
+
readonly message?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One structured SDK message → zero or one plain-text line for the session's
|
|
13
|
+
* output pipe: assistant text verbatim, tool calls as compact one-liners,
|
|
14
|
+
* results as a closing line.
|
|
15
|
+
*/
|
|
16
|
+
export function renderSDKMessage(message: RenderableMessage): string | null {
|
|
17
|
+
if (message.type === 'assistant') {
|
|
18
|
+
const inner = message.message;
|
|
19
|
+
|
|
20
|
+
if (!isRecord(inner) || !Array.isArray(inner['content'])) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = inner['content'];
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
|
|
27
|
+
for (const raw of content) {
|
|
28
|
+
const block: unknown = raw;
|
|
29
|
+
|
|
30
|
+
if (!isRecord(block)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
block['type'] === 'text' &&
|
|
36
|
+
typeof block['text'] === 'string' &&
|
|
37
|
+
block['text'].trim() !== ''
|
|
38
|
+
) {
|
|
39
|
+
lines.push(block['text'].replaceAll('\n', '\r\n'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (block['type'] === 'tool_use' && typeof block['name'] === 'string') {
|
|
43
|
+
lines.push(`⚙ ${block['name']} ${truncateSummary(JSON.stringify(block['input'] ?? {}))}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.length === 0 ? null : lines.join('\r\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (message.type === 'result') {
|
|
51
|
+
return message.subtype === 'success'
|
|
52
|
+
? `— headless turn done: ${truncateSummary(message.result ?? '')}`
|
|
53
|
+
: `— headless turn stopped: ${message.subtype ?? 'unknown'}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -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
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
2
|
-
import { isRecord } from '../shared/report';
|
|
3
2
|
import { truncateSummary } from '../shared/truncate-summary';
|
|
4
3
|
import { buildClaudeQueryOptions } from './build-claude-query-options';
|
|
5
4
|
import type { ClaudeHeadlessRunRequest } from './build-claude-query-options';
|
|
5
|
+
import { renderSDKMessage } from './render-sdk-message';
|
|
6
6
|
|
|
7
7
|
interface HeadlessRunHooks {
|
|
8
8
|
readonly onOutput: (text: string) => void;
|
|
@@ -47,7 +47,7 @@ export function startClaudeHeadlessRun(
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
for await (const message of stream) {
|
|
50
|
-
const rendered =
|
|
50
|
+
const rendered = renderSDKMessage(message);
|
|
51
51
|
|
|
52
52
|
if (rendered !== null) {
|
|
53
53
|
hooks.onOutput(`${rendered}\r\n`);
|
|
@@ -84,58 +84,3 @@ export function startClaudeHeadlessRun(
|
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
interface RenderableMessage {
|
|
89
|
-
readonly type: string;
|
|
90
|
-
readonly subtype?: string;
|
|
91
|
-
readonly result?: string;
|
|
92
|
-
readonly message?: unknown;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* One structured SDK message → zero or one plain-text line for the session's
|
|
97
|
-
* output pipe: assistant text verbatim, tool calls as compact one-liners,
|
|
98
|
-
* results as a closing line.
|
|
99
|
-
*/
|
|
100
|
-
export function renderSdkMessage(message: RenderableMessage): string | null {
|
|
101
|
-
if (message.type === 'assistant') {
|
|
102
|
-
const inner = message.message;
|
|
103
|
-
|
|
104
|
-
if (!isRecord(inner) || !Array.isArray(inner['content'])) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const content = inner['content'];
|
|
109
|
-
const lines: string[] = [];
|
|
110
|
-
|
|
111
|
-
for (const raw of content) {
|
|
112
|
-
const block: unknown = raw;
|
|
113
|
-
|
|
114
|
-
if (!isRecord(block)) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
block['type'] === 'text' &&
|
|
120
|
-
typeof block['text'] === 'string' &&
|
|
121
|
-
block['text'].trim() !== ''
|
|
122
|
-
) {
|
|
123
|
-
lines.push(block['text'].replaceAll('\n', '\r\n'));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (block['type'] === 'tool_use' && typeof block['name'] === 'string') {
|
|
127
|
-
lines.push(`⚙ ${block['name']} ${truncateSummary(JSON.stringify(block['input'] ?? {}))}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return lines.length === 0 ? null : lines.join('\r\n');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (message.type === 'result') {
|
|
135
|
-
return message.subtype === 'success'
|
|
136
|
-
? `— headless turn done: ${truncateSummary(message.result ?? '')}`
|
|
137
|
-
: `— headless turn stopped: ${message.subtype ?? 'unknown'}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return null;
|
|
141
|
-
}
|