@cordfuse/crosstalk 6.0.0-alpha.9 → 7.0.0-alpha.10

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.
Files changed (47) hide show
  1. package/README.md +97 -0
  2. package/bin/crosstalk.js +61 -74
  3. package/commands/channel.js +118 -0
  4. package/commands/chat.js +119 -0
  5. package/commands/down.js +40 -0
  6. package/commands/init.js +243 -0
  7. package/commands/logs.js +37 -0
  8. package/commands/pull.js +22 -0
  9. package/commands/replies.js +40 -0
  10. package/commands/restart.js +29 -0
  11. package/commands/rm.js +109 -0
  12. package/commands/run.js +115 -0
  13. package/commands/status.js +90 -0
  14. package/commands/up.js +135 -0
  15. package/commands/version.js +33 -0
  16. package/lib/api-client.js +141 -0
  17. package/lib/argv.js +30 -0
  18. package/lib/errors.js +19 -0
  19. package/lib/resolve.js +191 -0
  20. package/package.json +5 -21
  21. package/src/activation.ts +0 -104
  22. package/src/actor.ts +0 -131
  23. package/src/attach.ts +0 -118
  24. package/src/channel.ts +0 -49
  25. package/src/chat.ts +0 -142
  26. package/src/dispatch.ts +0 -531
  27. package/src/dlq.ts +0 -216
  28. package/src/filenames.ts +0 -28
  29. package/src/frontmatter.ts +0 -26
  30. package/src/init.ts +0 -138
  31. package/src/open.ts +0 -207
  32. package/src/replies.ts +0 -59
  33. package/src/send.ts +0 -122
  34. package/src/state.ts +0 -173
  35. package/src/status.ts +0 -75
  36. package/src/stop.ts +0 -37
  37. package/src/transport.ts +0 -213
  38. package/src/turnq.ts +0 -91
  39. package/src/upgrade.ts +0 -211
  40. package/src/wake.ts +0 -7
  41. package/template/CLAUDE.md +0 -12
  42. package/template/gitignore +0 -4
  43. package/template/upstream/CROSSTALK-VERSION +0 -1
  44. package/template/upstream/CROSSTALK.md +0 -298
  45. package/template/upstream/OPERATOR.md +0 -60
  46. package/template/upstream/PROTOCOL.md +0 -80
  47. package/template/upstream/actors/concierge.md +0 -36
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @cordfuse/crosstalk — host client
2
+
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
+
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
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g @cordfuse/crosstalk
13
+ ```
14
+
15
+ Requires: Node.js ≥ 20 + Docker on PATH + git on PATH. Works on Linux, macOS, Windows (Docker Desktop).
16
+
17
+ ---
18
+
19
+ ## Quickstart
20
+
21
+ ```sh
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>
44
+ ```
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
+
95
+ ## License
96
+
97
+ MIT.
package/bin/crosstalk.js CHANGED
@@ -1,97 +1,84 @@
1
1
  #!/usr/bin/env node
2
- // crosstalk — subcommand dispatcher
2
+ // crosstalk — host-side client for the crosstalkd daemon.
3
3
  //
4
- // Walks up from cwd to find a Crosstalk transport (identified by
5
- // upstream/CROSSTALK-VERSION), then runs the requested subcommand from the
6
- // runtime's installed source via tsx. All args after the subcommand are
7
- // forwarded. `init` is the one subcommand that runs outside a transport
8
- // (it creates one).
4
+ // Thin subcommand dispatcher. Routes operator commands to either:
5
+ // - the engine's HTTP API on 127.0.0.1:<port> (protocol commands)
6
+ // - the local docker daemon (lifecycle: up, down, restart, pull, logs)
7
+ // - `docker exec -it <container>` via a PTY wrapper (chat, shell)
8
+ //
9
+ // Subcommands and their groups:
10
+ //
11
+ // Protocol (HTTP API): run, replies, status, channel, init
12
+ // Lifecycle (docker): up, down, restart, pull, logs
13
+ // Interactive (pty): chat, shell
14
+ // Observability: version
15
+ //
16
+ // P3 only ships `version` as a proof-of-life. Other subcommands land
17
+ // in P4 (protocol), P5 (lifecycle), P6 (interactive).
9
18
 
10
- import { existsSync, statSync } from 'fs';
11
- import { resolve, join, dirname } from 'path';
12
- import { spawnSync, spawn } from 'child_process';
19
+ import { dirname, join } from 'path';
13
20
  import { fileURLToPath } from 'url';
14
- import { createRequire } from 'module';
21
+ import { existsSync } from 'fs';
15
22
 
16
- const SUBCOMMANDS = [
17
- 'dispatch', 'stop', 'send', 'replies', 'wake', 'status', 'init', 'dlq', 'channel',
18
- 'chat', 'open', 'attach', 'upgrade',
19
- ];
20
- const STANDALONE_SUBCOMMANDS = new Set(['init']);
23
+ const SUBCOMMANDS = {
24
+ // each entry is the file in commands/ to load
25
+ version: 'version.js',
26
+ run: 'run.js',
27
+ replies: 'replies.js',
28
+ status: 'status.js',
29
+ channel: 'channel.js',
30
+ init: 'init.js',
31
+ up: 'up.js',
32
+ down: 'down.js',
33
+ rm: 'rm.js',
34
+ restart: 'restart.js',
35
+ pull: 'pull.js',
36
+ logs: 'logs.js',
37
+ chat: 'chat.js',
38
+ };
21
39
 
22
- function findTransportRoot(startDir) {
23
- let dir = resolve(startDir);
24
- while (true) {
25
- const versionFile = join(dir, 'upstream', 'CROSSTALK-VERSION');
26
- if (existsSync(versionFile) && statSync(versionFile).isFile()) return dir;
27
- const parent = dirname(dir);
28
- if (parent === dir) return null;
29
- dir = parent;
30
- }
31
- }
40
+ const argv = process.argv.slice(2);
41
+ const thisDir = dirname(fileURLToPath(import.meta.url));
32
42
 
33
43
  function printUsage(exitCode = 0) {
34
44
  process.stdout.write(
35
- `Usage: crosstalk <subcommand> [args...]\n\nSubcommands:\n` +
36
- SUBCOMMANDS.map((s) => ` ${s}`).join('\n') +
37
- `\n\nMost subcommands require you to be inside a Crosstalk transport\n` +
38
- `(a directory containing upstream/CROSSTALK-VERSION). 'init' can run\n` +
39
- `from anywhere to scaffold a new transport.\n`,
45
+ `Usage: crosstalk <subcommand> [args...]
46
+
47
+ The host-side client for crosstalkd. Most commands talk to the engine's
48
+ HTTP API on 127.0.0.1; lifecycle commands talk to the local docker daemon.
49
+
50
+ Subcommands:
51
+ ${Object.keys(SUBCOMMANDS).map((s) => ` ${s}`).join('\n')}
52
+
53
+ If your engine listens on a non-default port, set CROSSTALK_API_PORT.
54
+
55
+ v7.0.0-dev — many subcommands are not implemented yet (see bin/crosstalk.js).
56
+ `,
40
57
  );
41
58
  process.exit(exitCode);
42
59
  }
43
60
 
44
- const argv = process.argv.slice(2);
45
61
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') printUsage(0);
46
62
 
47
- if (argv[0] === '--version' || argv[0] === 'version') {
48
- const require = createRequire(import.meta.url);
49
- const { version } = require('../package.json');
50
- process.stdout.write(`${version}\n`);
51
- process.exit(0);
63
+ if (argv[0] === '--version') {
64
+ // delegate to the version command so the engine-side number can also
65
+ // be queried in a uniform place
66
+ argv[0] = 'version';
52
67
  }
53
68
 
54
69
  const cmd = argv[0];
55
- if (!SUBCOMMANDS.includes(cmd)) {
56
- console.error(`crosstalk: unknown subcommand '${cmd}'\n`);
70
+ const file = SUBCOMMANDS[cmd];
71
+ if (!file) {
72
+ process.stderr.write(`crosstalk: unknown subcommand '${cmd}'\n\n`);
57
73
  printUsage(1);
58
74
  }
59
75
 
60
- const thisDir = dirname(fileURLToPath(import.meta.url));
61
- const srcFile = join(thisDir, '..', 'src', `${cmd}.ts`);
62
-
63
- let cwd = process.cwd();
64
- if (!STANDALONE_SUBCOMMANDS.has(cmd)) {
65
- const root = findTransportRoot(cwd);
66
- if (!root) {
67
- console.error(
68
- `crosstalk ${cmd}: not inside a Crosstalk transport ` +
69
- `(no upstream/CROSSTALK-VERSION found from ${cwd} upward).`,
70
- );
71
- process.exit(2);
72
- }
73
- cwd = root;
76
+ const modulePath = join(thisDir, '..', 'commands', file);
77
+ if (!existsSync(modulePath)) {
78
+ process.stderr.write(`crosstalk: command file missing for '${cmd}' (expected ${modulePath})\n`);
79
+ process.exit(2);
74
80
  }
75
81
 
76
- const require = createRequire(import.meta.url);
77
- const tsxCli = require.resolve('tsx/cli');
78
-
79
- // dispatch is long-running: use async spawn so SIGTERM/SIGINT forwarded to
80
- // the tsx child kills the whole chain cleanly. All other subcommands are
81
- // short-lived and spawnSync is fine.
82
- if (cmd === 'dispatch') {
83
- const child = spawn(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
84
- cwd,
85
- stdio: 'inherit',
86
- });
87
- const forward = (sig) => { try { child.kill(sig); } catch {} };
88
- process.on('SIGTERM', () => forward('SIGTERM'));
89
- process.on('SIGINT', () => forward('SIGTERM'));
90
- child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
91
- } else {
92
- const r = spawnSync(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
93
- cwd,
94
- stdio: 'inherit',
95
- });
96
- process.exit(r.status ?? 1);
97
- }
82
+ const mod = await import(modulePath);
83
+ const exit = await mod.run(argv.slice(1));
84
+ process.exit(typeof exit === 'number' ? exit : 0);
@@ -0,0 +1,118 @@
1
+ // crosstalk channel <name> — create a channel via POST /channels.
2
+ //
3
+ // P4 only ships create. --rename and --delete need PATCH/DELETE
4
+ // endpoints that haven't been added to the engine yet; they land
5
+ // when the engine gains them.
6
+
7
+ import { apiFor } from '../lib/api-client.js';
8
+ import { reportAndExit } from '../lib/errors.js';
9
+ import { positionals, has, flag } from '../lib/argv.js';
10
+
11
+ function usage(exit = 0) {
12
+ const w = exit === 0 ? process.stdout : process.stderr;
13
+ w.write(
14
+ `Usage:
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)
19
+ crosstalk channel --help
20
+ `,
21
+ );
22
+ process.exit(exit);
23
+ }
24
+
25
+ export async function run(argv) {
26
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
27
+
28
+ const api = apiFor(argv);
29
+ const pos = positionals(argv, ['--rename', '--containername', '-c']);
30
+ if (pos.length === 0) usage(1);
31
+
32
+ // `crosstalk channel list` → list channels
33
+ if (pos[0] === 'list') {
34
+ let r;
35
+ try {
36
+ r = await api.get('/channels');
37
+ } catch (err) {
38
+ reportAndExit(err, 'crosstalk channel list');
39
+ }
40
+ if (r.channels.length === 0) {
41
+ process.stdout.write('(no channels)\n');
42
+ return 0;
43
+ }
44
+ for (const ch of r.channels) {
45
+ const name = ch.name ?? '(unnamed)';
46
+ const parentSuffix = ch.parent ? ` [child of ${ch.parent.slice(0, 8)}]` : '';
47
+ process.stdout.write(`${ch.uuid} ${name}${parentSuffix}\n`);
48
+ }
49
+ return 0;
50
+ }
51
+
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');
58
+ return 1;
59
+ }
60
+
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.
109
+ let r;
110
+ try {
111
+ r = await api.post('/channels', { name: handle });
112
+ } catch (err) {
113
+ reportAndExit(err, 'crosstalk channel');
114
+ }
115
+ process.stdout.write(`Created channel: ${r.uuid}\n`);
116
+ process.stdout.write(` name: ${r.name}\n`);
117
+ return 0;
118
+ }
@@ -0,0 +1,119 @@
1
+ // crosstalk chat — interactive entry into the container.
2
+ //
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)
6
+ //
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.
10
+ //
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.
16
+
17
+ import { spawnSync } from 'child_process';
18
+ import { requireInitialized } from '../lib/resolve.js';
19
+ import { apiFor, ConnectError } from '../lib/api-client.js';
20
+ import { flag, has } from '../lib/argv.js';
21
+
22
+ const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
23
+
24
+ function usage(exit = 0) {
25
+ const w = exit === 0 ? process.stdout : process.stderr;
26
+ w.write(
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.
34
+
35
+ Supported agents: ${KNOWN_AGENTS.join(', ')}.
36
+
37
+ Modes:
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\`).
44
+
45
+ Examples:
46
+ crosstalk chat --agent claude # interactive claude
47
+ crosstalk chat --agent gemini # interactive gemini
48
+ crosstalk chat --shell # bash
49
+ `,
50
+ );
51
+ process.exit(exit);
52
+ }
53
+
54
+ function runInteractive(container, command, env = {}) {
55
+ // Full TTY passthrough — docker exec -it gives the container a real
56
+ // PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
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.
59
+ const envFlags = [];
60
+ for (const [k, v] of Object.entries(env)) {
61
+ envFlags.push('--env', `${k}=${v}`);
62
+ }
63
+ const r = spawnSync('docker', ['exec', '-it', ...envFlags, container, command], {
64
+ stdio: 'inherit',
65
+ });
66
+ return r.status ?? 1;
67
+ }
68
+
69
+ export async function run(argv) {
70
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
71
+
72
+ const { name } = requireInitialized(argv);
73
+ const api = apiFor(argv);
74
+
75
+ if (has(argv, '--shell')) {
76
+ return runInteractive(name, 'bash');
77
+ }
78
+
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
+ }
111
+ if (!KNOWN_AGENTS.includes(agent)) {
112
+ process.stderr.write(
113
+ `crosstalk chat: unknown agent '${agent}'. Supported: ${KNOWN_AGENTS.join(', ')}\n`,
114
+ );
115
+ return 1;
116
+ }
117
+
118
+ return runInteractive(name, agent);
119
+ }
@@ -0,0 +1,40 @@
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.
6
+
7
+ import { existsSync } from 'fs';
8
+ import { spawnSync } from 'child_process';
9
+ import { has } from '../lib/argv.js';
10
+ import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
11
+
12
+ function usage(exit = 0) {
13
+ const w = exit === 0 ? process.stdout : process.stderr;
14
+ w.write(
15
+ `Usage: crosstalk down [--containername <name>]
16
+
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.
20
+ `,
21
+ );
22
+ process.exit(exit);
23
+ }
24
+
25
+ export async function run(argv) {
26
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
27
+
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
+ );
33
+ return 0;
34
+ }
35
+
36
+ const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], {
37
+ stdio: 'inherit',
38
+ });
39
+ return r.status ?? 1;
40
+ }