@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.
- package/README.md +97 -0
- package/bin/crosstalk.js +61 -74
- package/commands/channel.js +118 -0
- package/commands/chat.js +119 -0
- package/commands/down.js +40 -0
- package/commands/init.js +243 -0
- package/commands/logs.js +37 -0
- package/commands/pull.js +22 -0
- package/commands/replies.js +40 -0
- package/commands/restart.js +29 -0
- package/commands/rm.js +109 -0
- package/commands/run.js +115 -0
- package/commands/status.js +90 -0
- package/commands/up.js +135 -0
- package/commands/version.js +33 -0
- package/lib/api-client.js +141 -0
- package/lib/argv.js +30 -0
- package/lib/errors.js +19 -0
- package/lib/resolve.js +191 -0
- package/package.json +5 -21
- package/src/activation.ts +0 -104
- package/src/actor.ts +0 -131
- package/src/attach.ts +0 -118
- package/src/channel.ts +0 -49
- package/src/chat.ts +0 -142
- package/src/dispatch.ts +0 -531
- package/src/dlq.ts +0 -216
- package/src/filenames.ts +0 -28
- package/src/frontmatter.ts +0 -26
- package/src/init.ts +0 -138
- package/src/open.ts +0 -207
- package/src/replies.ts +0 -59
- package/src/send.ts +0 -122
- package/src/state.ts +0 -173
- package/src/status.ts +0 -75
- package/src/stop.ts +0 -37
- package/src/transport.ts +0 -213
- package/src/turnq.ts +0 -91
- package/src/upgrade.ts +0 -211
- package/src/wake.ts +0 -7
- package/template/CLAUDE.md +0 -12
- package/template/gitignore +0 -4
- package/template/upstream/CROSSTALK-VERSION +0 -1
- package/template/upstream/CROSSTALK.md +0 -298
- package/template/upstream/OPERATOR.md +0 -60
- package/template/upstream/PROTOCOL.md +0 -80
- 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 —
|
|
2
|
+
// crosstalk — host-side client for the crosstalkd daemon.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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 {
|
|
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 {
|
|
21
|
+
import { existsSync } from 'fs';
|
|
15
22
|
|
|
16
|
-
const SUBCOMMANDS =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
77
|
-
const
|
|
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
|
+
}
|
package/commands/chat.js
ADDED
|
@@ -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
|
+
}
|
package/commands/down.js
ADDED
|
@@ -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
|
+
}
|