@cordfuse/crosstalk 7.0.0-alpha.1 → 7.0.0-alpha.11
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 +82 -11
- package/bin/crosstalk.js +1 -0
- package/commands/channel.js +63 -14
- package/commands/chat.js +66 -106
- package/commands/down.js +18 -15
- package/commands/init.js +206 -68
- package/commands/logs.js +8 -10
- package/commands/pull.js +7 -9
- package/commands/replies.js +4 -3
- package/commands/restart.js +16 -11
- package/commands/rm.js +109 -0
- package/commands/run.js +9 -15
- package/commands/status.js +53 -9
- package/commands/up.js +81 -75
- package/commands/version.js +5 -2
- package/lib/api-client.js +75 -14
- package/lib/argv.js +4 -2
- package/lib/resolve.js +191 -0
- package/package.json +1 -1
- package/lib/transport.js +0 -51
package/README.md
CHANGED
|
@@ -1,26 +1,97 @@
|
|
|
1
1
|
# @cordfuse/crosstalk — host client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The host-side CLI for the Crosstalk protocol. Pairs with [`@cordfuse/crosstalkd`](https://www.npmjs.com/package/@cordfuse/crosstalkd) — the daemon running inside the [`crosstalk-server`](https://github.com/cordfuse/crosstalk/tree/main/server) container — and the [transport template](https://github.com/cordfuse/crosstalk/tree/main/transport) it scaffolds.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **What Crosstalk is.** An agent-agnostic swarm communication protocol built on git. A git repo is the message bus. Full background: **[github.com/cordfuse/crosstalk](https://github.com/cordfuse/crosstalk#why-crosstalk-exists)**.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Operator runs `docker compose up -d` to bring up the `crosstalk-server` container (which runs `crosstalkd`)
|
|
9
|
-
- Operator runs `crosstalk <subcommand>` — the client talks to the daemon's HTTP API on `127.0.0.1:7000`
|
|
7
|
+
---
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
## Install
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g @cordfuse/crosstalk
|
|
13
|
+
```
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
- All other subcommands — landing in P4–P6
|
|
15
|
+
Requires: Node.js ≥ 20 + Docker on PATH + git on PATH. Works on Linux, macOS, Windows (Docker Desktop).
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
|
-
|
|
22
|
+
# 1. Scaffold a transport. Runtime-owned at <base>/crosstalk/transport/.
|
|
23
|
+
crosstalk init
|
|
24
|
+
|
|
25
|
+
# 2. Bring up the engine container.
|
|
26
|
+
crosstalk up
|
|
27
|
+
# Generates <base>/crosstalk/docker-compose.yml and runs `docker
|
|
28
|
+
# compose up -d` for ghcr.io/cordfuse/crosstalk-server:<latest>.
|
|
29
|
+
# No cwd magic — works from any directory.
|
|
30
|
+
|
|
31
|
+
# 3. Install an agent CLI inside the container, then authenticate.
|
|
32
|
+
crosstalk chat --shell
|
|
33
|
+
npm install -g @anthropic-ai/claude-code
|
|
34
|
+
exit
|
|
35
|
+
crosstalk chat --agent claude --login
|
|
36
|
+
# Auth URL opens in your default browser via xdg-open / open / start.
|
|
37
|
+
# Token saved in the /crosstalk-root volume so subsequent dispatched
|
|
38
|
+
# workers reuse it.
|
|
39
|
+
|
|
40
|
+
# 4. Send your first message.
|
|
41
|
+
crosstalk channel general
|
|
42
|
+
crosstalk run --type primitive --to sonnet "What is the capital of France?"
|
|
43
|
+
crosstalk replies <relPath printed by step above>
|
|
22
44
|
```
|
|
23
45
|
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Subcommand surface
|
|
49
|
+
|
|
50
|
+
Every name-resolved verb takes an optional `--containername <name>` (or `-c <name>`) flag. Bare verb = default container (literally named `crosstalk`).
|
|
51
|
+
|
|
52
|
+
| Group | Subcommand | What it does |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Meta | `crosstalk version` | Print client + engine versions (warns on skew) |
|
|
55
|
+
| Protocol | `crosstalk init [-c <name>] [--remote <url>]` | Scaffold a transport at `<base>/<name>/transport/` |
|
|
56
|
+
| Protocol | `crosstalk run --type primitive\|workflow ...` | Dispatch a primitive or workflow |
|
|
57
|
+
| Protocol | `crosstalk replies <relPath>...` | Poll reply status |
|
|
58
|
+
| Protocol | `crosstalk status` | Container name, transport host path, engine state, channels |
|
|
59
|
+
| Protocol | `crosstalk channel <name> [--rename\|--delete]` | Channel operations |
|
|
60
|
+
| Lifecycle | `crosstalk up` | Generate docker-compose, start the engine container |
|
|
61
|
+
| Lifecycle | `crosstalk down` | Stop the engine container (keep storage) |
|
|
62
|
+
| Lifecycle | `crosstalk rm [--force]` | Remove container AND wipe storage (cannot undo) |
|
|
63
|
+
| Lifecycle | `crosstalk restart` | Down + up |
|
|
64
|
+
| Lifecycle | `crosstalk pull` | Refresh the engine image |
|
|
65
|
+
| Lifecycle | `crosstalk logs [-f]` | Stream the engine container's logs |
|
|
66
|
+
| Interactive | `crosstalk chat --agent <name>` | Interactive session with an agent CLI inside the container |
|
|
67
|
+
| Interactive | `crosstalk chat --agent <name> --login` | OAuth flow with browser-open URL handling |
|
|
68
|
+
| Interactive | `crosstalk chat --shell` | Drop into bash inside the container (debug, install CLIs) |
|
|
69
|
+
|
|
70
|
+
`--agent` is required for any non-shell chat — containers can have multiple installed CLIs, and the operator must pick explicitly. The error message names which ones are actually installed (queried from the engine's `/agents/installed` endpoint).
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## What lives where
|
|
75
|
+
|
|
76
|
+
- **Host** — `crosstalk` binary on PATH. Per-transport storage at `<base>/<name>/` (one tree per container).
|
|
77
|
+
- **Container** (literally named whatever the operator picked at `init`, default `crosstalk`; image `ghcr.io/cordfuse/crosstalk-server`) — `crosstalkd` daemon, bind-mounted from:
|
|
78
|
+
- `<base>/<name>/transport/` → `/var/lib/crosstalk-transport` (runtime-owned git repo)
|
|
79
|
+
- `<base>/<name>/crosstalk-root/` → `/crosstalk-root` (operator CLIs + auth)
|
|
80
|
+
- `<base>/<name>/crosstalk-state/` → `/var/lib/crosstalk-state` (cursor + heartbeat)
|
|
81
|
+
- **`<base>`** is per-OS user-mode: `$XDG_DATA_HOME/crosstalk` on Linux, `~/Library/Application Support/crosstalk` on macOS, `%LOCALAPPDATA%\crosstalk` on Windows. `CROSSTALK_STORAGE_MODE=system` switches to `/var/lib`-style paths.
|
|
82
|
+
- **Engine HTTP API** — `127.0.0.1:<port>`. Default container = port 7000; named containers allocate at init from 7001 upward, recorded in `<base>/<name>/api-port`.
|
|
83
|
+
- **Container identification** — every crosstalkd container gets the Docker label `crosstalk.transport=true`. The client uses the label for inventory queries, not name-pattern matching.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
Mirrors `docker` (host CLI) / `dockerd` (daemon). The split keeps the protocol-layer logic in one place (the engine) and exposes a clean operator surface on the host without forcing operators to manage Node/agent CLIs/OAuth tokens on their bare host.
|
|
90
|
+
|
|
91
|
+
Client makes plain HTTP requests to the engine over loopback. No auth (same model as ollama, postgres-on-localhost, dockerd's local socket; the loopback bind is what gates exposure). Version skew detected via `X-Crosstalk-Engine-Version` response header — client warns once per process when the operator forgot to `crosstalk pull && crosstalk restart` after upgrading the client (or vice versa).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
24
95
|
## License
|
|
25
96
|
|
|
26
97
|
MIT.
|
package/bin/crosstalk.js
CHANGED
package/commands/channel.js
CHANGED
|
@@ -4,21 +4,19 @@
|
|
|
4
4
|
// endpoints that haven't been added to the engine yet; they land
|
|
5
5
|
// when the engine gains them.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { apiFor } from '../lib/api-client.js';
|
|
8
8
|
import { reportAndExit } from '../lib/errors.js';
|
|
9
|
-
import { positionals, has } from '../lib/argv.js';
|
|
9
|
+
import { positionals, has, flag } from '../lib/argv.js';
|
|
10
10
|
|
|
11
11
|
function usage(exit = 0) {
|
|
12
12
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
13
13
|
w.write(
|
|
14
14
|
`Usage:
|
|
15
|
-
crosstalk channel
|
|
16
|
-
crosstalk channel
|
|
15
|
+
crosstalk channel list # list channels
|
|
16
|
+
crosstalk channel <name> # create a channel
|
|
17
|
+
crosstalk channel <name-or-uuid> --rename <new> # rename
|
|
18
|
+
crosstalk channel <name-or-uuid> --delete # delete (with confirmation)
|
|
17
19
|
crosstalk channel --help
|
|
18
|
-
|
|
19
|
-
Not yet implemented (engine API needs PATCH/DELETE first):
|
|
20
|
-
crosstalk channel <name-or-uuid> --rename <new>
|
|
21
|
-
crosstalk channel <name-or-uuid> --delete
|
|
22
20
|
`,
|
|
23
21
|
);
|
|
24
22
|
process.exit(exit);
|
|
@@ -27,7 +25,8 @@ Not yet implemented (engine API needs PATCH/DELETE first):
|
|
|
27
25
|
export async function run(argv) {
|
|
28
26
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
29
27
|
|
|
30
|
-
const
|
|
28
|
+
const api = apiFor(argv);
|
|
29
|
+
const pos = positionals(argv, ['--rename', '--containername', '-c']);
|
|
31
30
|
if (pos.length === 0) usage(1);
|
|
32
31
|
|
|
33
32
|
// `crosstalk channel list` → list channels
|
|
@@ -50,16 +49,66 @@ export async function run(argv) {
|
|
|
50
49
|
return 0;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const handle = pos[0];
|
|
53
|
+
const renameTo = flag(argv, '--rename');
|
|
54
|
+
const wantDelete = has(argv, '--delete');
|
|
55
|
+
|
|
56
|
+
if (renameTo && wantDelete) {
|
|
57
|
+
process.stderr.write('crosstalk channel: --rename and --delete are mutually exclusive.\n');
|
|
56
58
|
return 1;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
// Rename: PATCH /channels/<handle> { name: <new> }
|
|
62
|
+
if (renameTo) {
|
|
63
|
+
let r;
|
|
64
|
+
try {
|
|
65
|
+
r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: renameTo });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
reportAndExit(err, 'crosstalk channel');
|
|
68
|
+
}
|
|
69
|
+
if (r.noop) {
|
|
70
|
+
process.stdout.write(`No-op: ${handle} already named ${renameTo}.\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(`Renamed: ${handle} -> ${r.name} (${r.uuid})\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Delete: DELETE /channels/<handle>?confirm=<name>
|
|
78
|
+
// Engine returns 400 if confirm is missing/wrong, with the expected
|
|
79
|
+
// value in the error message. Client reads the channel's name first
|
|
80
|
+
// and passes it as confirm — operator's explicit --delete flag IS the
|
|
81
|
+
// confirmation for now. Interactive prompt deferred (would block on
|
|
82
|
+
// stdin in scripts).
|
|
83
|
+
if (wantDelete) {
|
|
84
|
+
let target;
|
|
85
|
+
try {
|
|
86
|
+
const all = await api.get('/channels');
|
|
87
|
+
target = all.channels.find((c) => c.uuid === handle || c.name === handle);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
reportAndExit(err, 'crosstalk channel');
|
|
90
|
+
}
|
|
91
|
+
if (!target) {
|
|
92
|
+
process.stderr.write(`crosstalk channel: '${handle}' not found.\n`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const confirm = target.name ?? target.uuid;
|
|
96
|
+
let r;
|
|
97
|
+
try {
|
|
98
|
+
r = await api.delete(
|
|
99
|
+
`/channels/${encodeURIComponent(handle)}?confirm=${encodeURIComponent(confirm)}`,
|
|
100
|
+
);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
reportAndExit(err, 'crosstalk channel');
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(`Deleted channel: ${r.name ?? '(unnamed)'} (${r.uuid})\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// No flags → create a channel with the given name.
|
|
60
109
|
let r;
|
|
61
110
|
try {
|
|
62
|
-
r = await api.post('/channels', { name });
|
|
111
|
+
r = await api.post('/channels', { name: handle });
|
|
63
112
|
} catch (err) {
|
|
64
113
|
reportAndExit(err, 'crosstalk channel');
|
|
65
114
|
}
|
package/commands/chat.js
CHANGED
|
@@ -1,74 +1,50 @@
|
|
|
1
|
-
// crosstalk chat —
|
|
1
|
+
// crosstalk chat — interactive entry into the container.
|
|
2
2
|
//
|
|
3
|
-
// Modes:
|
|
4
|
-
// crosstalk chat
|
|
5
|
-
// crosstalk chat --
|
|
6
|
-
// crosstalk chat --login [--agent X] # OAuth setup flow with browser-open
|
|
7
|
-
// crosstalk chat --shell # bash escape hatch (sysadmin / debug)
|
|
3
|
+
// Modes (alpha.8):
|
|
4
|
+
// crosstalk chat --agent <name> # interactive session with an agent CLI
|
|
5
|
+
// crosstalk chat --shell # bash escape hatch (debug, install CLIs, run setup commands)
|
|
8
6
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// wrapper runs on the HOST — where the browser lives. It intercepts the
|
|
13
|
-
// URL on the agent's stdout and opens it directly via the OS-native
|
|
14
|
-
// browser launcher (`open` / `xdg-open` / `wslview` / `start`). Operator
|
|
15
|
-
// never has to read or copy-paste the URL.
|
|
7
|
+
// --agent is required for non-shell mode. There's no implicit default;
|
|
8
|
+
// containers may have multiple agent CLIs installed, the operator
|
|
9
|
+
// picks one explicitly.
|
|
16
10
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
11
|
+
// `--login` was removed in alpha.8. Per-agent auth is now handled via
|
|
12
|
+
// `data/crosstalk.yaml`'s per-provider `env_file:` (env-injected at
|
|
13
|
+
// agent spawn time inside the engine) plus operator-run setup commands
|
|
14
|
+
// for agents whose auth is a stateful CLI act (e.g. codex). Recipes are
|
|
15
|
+
// documented in transport/auth/README.md.
|
|
21
16
|
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
17
|
+
import { spawnSync } from 'child_process';
|
|
18
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
19
|
+
import { apiFor, ConnectError } from '../lib/api-client.js';
|
|
24
20
|
import { flag, has } from '../lib/argv.js';
|
|
25
21
|
|
|
26
22
|
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
27
|
-
// Conservative URL regex: matches the leading https://...token boundary,
|
|
28
|
-
// stops at whitespace/quote/angle. False positives are bounded to --login
|
|
29
|
-
// mode where the first URL printed is almost certainly the auth URL.
|
|
30
|
-
const URL_RE = /https?:\/\/[^\s<>"']+/;
|
|
31
|
-
|
|
32
|
-
function openInBrowser(url) {
|
|
33
|
-
const cmd =
|
|
34
|
-
process.platform === 'darwin' ? 'open' :
|
|
35
|
-
process.platform === 'win32' ? 'start' :
|
|
36
|
-
process.env['WSL_DISTRO_NAME'] ? 'wslview' :
|
|
37
|
-
/* default Linux */ 'xdg-open';
|
|
38
|
-
try {
|
|
39
|
-
const r = spawnSync(cmd, [url], { stdio: 'ignore' });
|
|
40
|
-
return r.status === 0;
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
23
|
|
|
46
24
|
function usage(exit = 0) {
|
|
47
25
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
48
26
|
w.write(
|
|
49
|
-
`Usage: crosstalk chat
|
|
27
|
+
`Usage: crosstalk chat --agent <name>
|
|
28
|
+
crosstalk chat --shell
|
|
29
|
+
|
|
30
|
+
Opens an interactive session inside the engine container.
|
|
31
|
+
|
|
32
|
+
--agent is REQUIRED for any chat mode. Containers can have multiple
|
|
33
|
+
agent CLIs installed; the operator must say which one to invoke.
|
|
50
34
|
|
|
51
|
-
|
|
52
|
-
PTY-wrapped entry point for everything you'd do inside the container
|
|
53
|
-
(daily chat, OAuth setup, sysadmin).
|
|
35
|
+
Supported agents: ${KNOWN_AGENTS.join(', ')}.
|
|
54
36
|
|
|
55
37
|
Modes:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
--
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
printing the URL unwrapped if no browser is
|
|
63
|
-
reachable (headless / SSH session).
|
|
64
|
-
--shell Drop into bash (install agent CLIs, debug,
|
|
65
|
-
inspect state).
|
|
38
|
+
--agent <name> Interactive with the named agent CLI. Use the
|
|
39
|
+
agent's own keys/commands for first-time setup
|
|
40
|
+
(see transport/auth/README.md for recipes).
|
|
41
|
+
--shell Drop into bash (install agent CLIs, debug, run
|
|
42
|
+
one-time auth setup commands like
|
|
43
|
+
\`codex login --with-api-key\`).
|
|
66
44
|
|
|
67
45
|
Examples:
|
|
68
|
-
crosstalk chat
|
|
46
|
+
crosstalk chat --agent claude # interactive claude
|
|
69
47
|
crosstalk chat --agent gemini # interactive gemini
|
|
70
|
-
crosstalk chat --login # OAuth, claude
|
|
71
|
-
crosstalk chat --login --agent codex # OAuth, codex
|
|
72
48
|
crosstalk chat --shell # bash
|
|
73
49
|
`,
|
|
74
50
|
);
|
|
@@ -78,8 +54,8 @@ Examples:
|
|
|
78
54
|
function runInteractive(container, command, env = {}) {
|
|
79
55
|
// Full TTY passthrough — docker exec -it gives the container a real
|
|
80
56
|
// PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
|
|
81
|
-
// operator's terminal. Agent CLIs
|
|
82
|
-
//
|
|
57
|
+
// operator's terminal. Agent CLIs render their rich TUI normally and
|
|
58
|
+
// SIGWINCH propagates from the host's tty to the container's PTY.
|
|
83
59
|
const envFlags = [];
|
|
84
60
|
for (const [k, v] of Object.entries(env)) {
|
|
85
61
|
envFlags.push('--env', `${k}=${v}`);
|
|
@@ -90,60 +66,48 @@ function runInteractive(container, command, env = {}) {
|
|
|
90
66
|
return r.status ?? 1;
|
|
91
67
|
}
|
|
92
68
|
|
|
93
|
-
function runLogin(container, agent) {
|
|
94
|
-
// Login mode: tee the agent's stdout so we can spot the auth URL and
|
|
95
|
-
// open it in the operator's browser. The container still has a real
|
|
96
|
-
// PTY (docker exec -it allocates one inside the container regardless
|
|
97
|
-
// of host stdio configuration), so the agent CLI renders into a TTY;
|
|
98
|
-
// we just pipe the bytes on the host side to inspect them.
|
|
99
|
-
//
|
|
100
|
-
// No agent-specific "login" subcommand — we just run the agent's
|
|
101
|
-
// default interactive entry point. First-run auth flow happens
|
|
102
|
-
// automatically for every agent we support. Operator can pass
|
|
103
|
-
// --agent <name> to target a specific one.
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
const child = spawn(
|
|
106
|
-
'docker',
|
|
107
|
-
[
|
|
108
|
-
'exec', '-it',
|
|
109
|
-
'--env', 'COLUMNS=1000', // fallback for browser-open failure
|
|
110
|
-
'--env', 'LINES=40',
|
|
111
|
-
container,
|
|
112
|
-
agent,
|
|
113
|
-
],
|
|
114
|
-
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
115
|
-
);
|
|
116
|
-
let opened = false;
|
|
117
|
-
child.stdout.on('data', (chunk) => {
|
|
118
|
-
process.stdout.write(chunk);
|
|
119
|
-
if (opened) return;
|
|
120
|
-
const match = chunk.toString().match(URL_RE);
|
|
121
|
-
if (!match) return;
|
|
122
|
-
const url = match[0];
|
|
123
|
-
opened = true;
|
|
124
|
-
const ok = openInBrowser(url);
|
|
125
|
-
process.stdout.write(
|
|
126
|
-
ok
|
|
127
|
-
? `\n[crosstalk chat] Opened auth URL in your default browser.\n`
|
|
128
|
-
: `\n[crosstalk chat] Could not auto-open browser. Open this URL manually:\n ${url}\n`,
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
child.on('exit', (code) => resolve(code ?? 0));
|
|
132
|
-
child.on('error', () => resolve(1));
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
69
|
export async function run(argv) {
|
|
137
70
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
138
71
|
|
|
139
|
-
const
|
|
140
|
-
const
|
|
72
|
+
const { name } = requireInitialized(argv);
|
|
73
|
+
const api = apiFor(argv);
|
|
141
74
|
|
|
142
75
|
if (has(argv, '--shell')) {
|
|
143
76
|
return runInteractive(name, 'bash');
|
|
144
77
|
}
|
|
145
78
|
|
|
146
|
-
const agent = flag(argv, '--agent')
|
|
79
|
+
const agent = flag(argv, '--agent');
|
|
80
|
+
if (!agent) {
|
|
81
|
+
// Query the engine for what's actually installed (vs the theoretical
|
|
82
|
+
// supported set). Gives a smarter error than "supported: claude,
|
|
83
|
+
// codex, gemini, qwen, opencode, agy" when only 1-2 are actually
|
|
84
|
+
// present in the container.
|
|
85
|
+
let installed = null;
|
|
86
|
+
try {
|
|
87
|
+
const r = await api.get('/agents/installed');
|
|
88
|
+
installed = r.installed;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (!(err instanceof ConnectError)) {
|
|
91
|
+
// engine unreachable — fall through with no introspection
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (installed && installed.length === 0) {
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`crosstalk chat: no agent CLIs installed in this container.\n` +
|
|
97
|
+
` Install one first:\n` +
|
|
98
|
+
` crosstalk chat --shell\n` +
|
|
99
|
+
` npm install -g @anthropic-ai/claude-code # or your agent's installer\n`,
|
|
100
|
+
);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const list = installed && installed.length > 0
|
|
104
|
+
? `Installed in this container: ${installed.join(', ')}.`
|
|
105
|
+
: `Supported agents: ${KNOWN_AGENTS.join(', ')}.`;
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`crosstalk chat: --agent <name> is required.\n ${list}\n`,
|
|
108
|
+
);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
147
111
|
if (!KNOWN_AGENTS.includes(agent)) {
|
|
148
112
|
process.stderr.write(
|
|
149
113
|
`crosstalk chat: unknown agent '${agent}'. Supported: ${KNOWN_AGENTS.join(', ')}\n`,
|
|
@@ -151,9 +115,5 @@ export async function run(argv) {
|
|
|
151
115
|
return 1;
|
|
152
116
|
}
|
|
153
117
|
|
|
154
|
-
if (has(argv, '--login')) {
|
|
155
|
-
return await runLogin(name, agent);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
118
|
return runInteractive(name, agent);
|
|
159
119
|
}
|
package/commands/down.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
// crosstalk down — stop
|
|
1
|
+
// crosstalk down — stop a transport's container, keep storage.
|
|
2
|
+
//
|
|
3
|
+
// alpha.6: name-resolved. --volumes flag retires (no named volumes in
|
|
4
|
+
// the bind-mount model). Reversible — next `up` resumes against the
|
|
5
|
+
// preserved <base>/<name>/ storage.
|
|
2
6
|
|
|
3
7
|
import { existsSync } from 'fs';
|
|
4
8
|
import { spawnSync } from 'child_process';
|
|
5
|
-
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
9
|
import { has } from '../lib/argv.js';
|
|
10
|
+
import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
|
|
7
11
|
|
|
8
12
|
function usage(exit = 0) {
|
|
9
13
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
14
|
w.write(
|
|
11
|
-
`Usage: crosstalk down [--
|
|
15
|
+
`Usage: crosstalk down [--containername <name>]
|
|
12
16
|
|
|
13
|
-
Stops the engine container for
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
Stops the engine container for a transport. Storage at <base>/<name>/
|
|
18
|
+
is preserved — next 'crosstalk up' resumes against it. Use
|
|
19
|
+
'crosstalk rm' if you want to wipe storage as well.
|
|
16
20
|
`,
|
|
17
21
|
);
|
|
18
22
|
process.exit(exit);
|
|
@@ -21,17 +25,16 @@ installed CLIs + auth state; you'll have to re-install + re-auth).
|
|
|
21
25
|
export async function run(argv) {
|
|
22
26
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const { name, paths } = requireInitialized(argv);
|
|
29
|
+
if (!existsSync(paths.composeFile)) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
`crosstalk down: '${name}' has no compose file (never brought up). Nothing to do.\n`,
|
|
32
|
+
);
|
|
29
33
|
return 0;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const r = spawnSync('docker', args, { cwd: root, stdio: 'inherit' });
|
|
36
|
+
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
});
|
|
36
39
|
return r.status ?? 1;
|
|
37
40
|
}
|