@cordfuse/crosstalk 7.0.0-alpha.2 → 7.0.0-alpha.3
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 +77 -11
- package/commands/channel.js +61 -13
- package/commands/chat.js +27 -3
- package/commands/init.js +3 -3
- package/commands/up.js +1 -1
- package/commands/version.js +2 -0
- package/lib/api-client.js +37 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,92 @@
|
|
|
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.
|
|
23
|
+
crosstalk init mytransport
|
|
24
|
+
cd mytransport
|
|
25
|
+
|
|
26
|
+
# 2. Bring up the engine container for this transport.
|
|
27
|
+
crosstalk up
|
|
28
|
+
# Generates docker-compose.yml in the transport root and runs
|
|
29
|
+
# `docker compose up -d` for ghcr.io/cordfuse/crosstalk-server:<latest>.
|
|
30
|
+
# Bind-mounts $PWD as the transport — any `git remote add` from the
|
|
31
|
+
# host is immediately visible to the engine.
|
|
32
|
+
|
|
33
|
+
# 3. Install an agent CLI inside the container, then authenticate.
|
|
34
|
+
crosstalk chat --shell
|
|
35
|
+
npm install -g @anthropic-ai/claude-code
|
|
36
|
+
exit
|
|
37
|
+
crosstalk chat --agent claude --login
|
|
38
|
+
# Auth URL opens in your default browser via xdg-open / open / start.
|
|
39
|
+
# Operator never has to copy-paste it. Token saved in the /root
|
|
40
|
+
# volume so subsequent dispatched workers reuse it.
|
|
41
|
+
|
|
42
|
+
# 4. Send your first message.
|
|
43
|
+
crosstalk channel general
|
|
44
|
+
crosstalk run --type primitive --to sonnet "What is the capital of France?"
|
|
45
|
+
crosstalk replies <relPath printed by step above>
|
|
22
46
|
```
|
|
23
47
|
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Subcommand surface
|
|
51
|
+
|
|
52
|
+
| Group | Subcommand | What it does |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Meta | `crosstalk version` | Print client + engine versions (warns on skew) |
|
|
55
|
+
| Protocol | `crosstalk init <dir>` | Scaffold a transport + `git init` |
|
|
56
|
+
| Protocol | `crosstalk run --type primitive\|workflow ...` | Dispatch a primitive or workflow |
|
|
57
|
+
| Protocol | `crosstalk replies <relPath>...` | Poll reply status |
|
|
58
|
+
| Protocol | `crosstalk status` | Engine heartbeat, claimed models, channels |
|
|
59
|
+
| Protocol | `crosstalk channel <name> [--rename\|--delete]` | Channel operations |
|
|
60
|
+
| Lifecycle | `crosstalk up` | Generate docker-compose, start engine container |
|
|
61
|
+
| Lifecycle | `crosstalk down [--volumes]` | Stop the engine container |
|
|
62
|
+
| Lifecycle | `crosstalk restart` | Down + up |
|
|
63
|
+
| Lifecycle | `crosstalk pull` | Refresh the engine image |
|
|
64
|
+
| Lifecycle | `crosstalk logs [-f]` | Stream the engine container's logs |
|
|
65
|
+
| Interactive | `crosstalk chat --agent <name>` | Interactive session with an agent CLI inside the container |
|
|
66
|
+
| Interactive | `crosstalk chat --agent <name> --login` | OAuth flow with browser-open URL handling |
|
|
67
|
+
| Interactive | `crosstalk chat --shell` | Drop into bash inside the container (debug, install CLIs) |
|
|
68
|
+
|
|
69
|
+
`--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).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What lives where
|
|
74
|
+
|
|
75
|
+
- **Host** — `crosstalk` binary on PATH, `~/path/to/mytransport/` (the operator's transport directory, bind-mounted).
|
|
76
|
+
- **Container** (`crosstalk-server`, image `ghcr.io/cordfuse/crosstalk-server`) — `crosstalkd` daemon, dispatch state in named volume `crosstalk-state:/var/lib/crosstalkd-state`, operator-installed agent CLIs + auth tokens in named volume `crosstalk-root:/root`.
|
|
77
|
+
- **Engine HTTP API** — `127.0.0.1:7000` (compose `127.0.0.1:HOST:CONTAINER` port mapping is the actual security boundary; engine binds 0.0.0.0 inside the container).
|
|
78
|
+
- **Transport bind-mount** — `$PWD:/var/lib/crosstalk-transport`. Operator git commands on the host are seen by the engine immediately.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
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).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
24
90
|
## License
|
|
25
91
|
|
|
26
92
|
MIT.
|
package/commands/channel.js
CHANGED
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import { api } 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,7 @@ 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 pos = positionals(argv, []);
|
|
28
|
+
const pos = positionals(argv, ['--rename']);
|
|
31
29
|
if (pos.length === 0) usage(1);
|
|
32
30
|
|
|
33
31
|
// `crosstalk channel list` → list channels
|
|
@@ -50,16 +48,66 @@ export async function run(argv) {
|
|
|
50
48
|
return 0;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const handle = pos[0];
|
|
52
|
+
const renameTo = flag(argv, '--rename');
|
|
53
|
+
const wantDelete = has(argv, '--delete');
|
|
54
|
+
|
|
55
|
+
if (renameTo && wantDelete) {
|
|
56
|
+
process.stderr.write('crosstalk channel: --rename and --delete are mutually exclusive.\n');
|
|
56
57
|
return 1;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
// Rename: PATCH /channels/<handle> { name: <new> }
|
|
61
|
+
if (renameTo) {
|
|
62
|
+
let r;
|
|
63
|
+
try {
|
|
64
|
+
r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: renameTo });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
reportAndExit(err, 'crosstalk channel');
|
|
67
|
+
}
|
|
68
|
+
if (r.noop) {
|
|
69
|
+
process.stdout.write(`No-op: ${handle} already named ${renameTo}.\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(`Renamed: ${handle} -> ${r.name} (${r.uuid})\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Delete: DELETE /channels/<handle>?confirm=<name>
|
|
77
|
+
// Engine returns 400 if confirm is missing/wrong, with the expected
|
|
78
|
+
// value in the error message. Client reads the channel's name first
|
|
79
|
+
// and passes it as confirm — operator's explicit --delete flag IS the
|
|
80
|
+
// confirmation for now. Interactive prompt deferred (would block on
|
|
81
|
+
// stdin in scripts).
|
|
82
|
+
if (wantDelete) {
|
|
83
|
+
let target;
|
|
84
|
+
try {
|
|
85
|
+
const all = await api.get('/channels');
|
|
86
|
+
target = all.channels.find((c) => c.uuid === handle || c.name === handle);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
reportAndExit(err, 'crosstalk channel');
|
|
89
|
+
}
|
|
90
|
+
if (!target) {
|
|
91
|
+
process.stderr.write(`crosstalk channel: '${handle}' not found.\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const confirm = target.name ?? target.uuid;
|
|
95
|
+
let r;
|
|
96
|
+
try {
|
|
97
|
+
r = await api.delete(
|
|
98
|
+
`/channels/${encodeURIComponent(handle)}?confirm=${encodeURIComponent(confirm)}`,
|
|
99
|
+
);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
reportAndExit(err, 'crosstalk channel');
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write(`Deleted channel: ${r.name ?? '(unnamed)'} (${r.uuid})\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// No flags → create a channel with the given name.
|
|
60
108
|
let r;
|
|
61
109
|
try {
|
|
62
|
-
r = await api.post('/channels', { name });
|
|
110
|
+
r = await api.post('/channels', { name: handle });
|
|
63
111
|
} catch (err) {
|
|
64
112
|
reportAndExit(err, 'crosstalk channel');
|
|
65
113
|
}
|
package/commands/chat.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { spawn, spawnSync } from 'child_process';
|
|
26
26
|
import { requireTransportRoot, containerName } from '../lib/transport.js';
|
|
27
|
+
import { api, ConnectError } from '../lib/api-client.js';
|
|
27
28
|
import { flag, has } from '../lib/argv.js';
|
|
28
29
|
|
|
29
30
|
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
@@ -152,10 +153,33 @@ export async function run(argv) {
|
|
|
152
153
|
|
|
153
154
|
const agent = flag(argv, '--agent');
|
|
154
155
|
if (!agent) {
|
|
156
|
+
// Query the engine for what's actually installed (vs the theoretical
|
|
157
|
+
// supported set). Gives a smarter error than "supported: claude,
|
|
158
|
+
// codex, gemini, qwen, opencode, agy" when only 1-2 are actually
|
|
159
|
+
// present in the container.
|
|
160
|
+
let installed = null;
|
|
161
|
+
try {
|
|
162
|
+
const r = await api.get('/agents/installed');
|
|
163
|
+
installed = r.installed;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (!(err instanceof ConnectError)) {
|
|
166
|
+
// engine unreachable — fall through with no introspection
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (installed && installed.length === 0) {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`crosstalk chat: no agent CLIs installed in this container.\n` +
|
|
172
|
+
` Install one first:\n` +
|
|
173
|
+
` crosstalk chat --shell\n` +
|
|
174
|
+
` npm install -g @anthropic-ai/claude-code # or your agent's installer\n`,
|
|
175
|
+
);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
const list = installed && installed.length > 0
|
|
179
|
+
? `Installed in this container: ${installed.join(', ')}.`
|
|
180
|
+
: `Supported agents: ${KNOWN_AGENTS.join(', ')}.`;
|
|
155
181
|
process.stderr.write(
|
|
156
|
-
`crosstalk chat: --agent <name> is required
|
|
157
|
-
`multiple agent CLIs installed; pick one explicitly).\n` +
|
|
158
|
-
`Supported: ${KNOWN_AGENTS.join(', ')}\n`,
|
|
182
|
+
`crosstalk chat: --agent <name> is required.\n ${list}\n`,
|
|
159
183
|
);
|
|
160
184
|
return 1;
|
|
161
185
|
}
|
package/commands/init.js
CHANGED
|
@@ -21,7 +21,7 @@ import { spawnSync } from 'child_process';
|
|
|
21
21
|
import { has, positionals } from '../lib/argv.js';
|
|
22
22
|
|
|
23
23
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
24
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
24
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.3';
|
|
25
25
|
|
|
26
26
|
function usage(exit = 0) {
|
|
27
27
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
@@ -34,9 +34,9 @@ function usage(exit = 0) {
|
|
|
34
34
|
|
|
35
35
|
The image must be pullable (or already present locally). For local dev
|
|
36
36
|
before the GHCR publish pipeline ships, build the image first:
|
|
37
|
-
docker build -t crosstalk-server:7.0.0-alpha.
|
|
37
|
+
docker build -t crosstalk-server:7.0.0-alpha.3 -f server/Dockerfile .
|
|
38
38
|
Then run:
|
|
39
|
-
CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.
|
|
39
|
+
CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.3 crosstalk init mytransport
|
|
40
40
|
`,
|
|
41
41
|
);
|
|
42
42
|
process.exit(exit);
|
package/commands/up.js
CHANGED
|
@@ -16,7 +16,7 @@ import { requireTransportRoot, transportName, composeFile } from '../lib/transpo
|
|
|
16
16
|
import { has } from '../lib/argv.js';
|
|
17
17
|
|
|
18
18
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
19
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
19
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.3';
|
|
20
20
|
const DEFAULT_API_PORT = Number(process.env.CROSSTALK_API_PORT) || 7000;
|
|
21
21
|
|
|
22
22
|
function usage(exit = 0) {
|
package/commands/version.js
CHANGED
|
@@ -16,6 +16,8 @@ const clientPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
|
16
16
|
export async function run(_argv) {
|
|
17
17
|
process.stdout.write(`crosstalk client: ${clientPkg.version}\n`);
|
|
18
18
|
try {
|
|
19
|
+
// Skew detection happens passively in api-client.js via the
|
|
20
|
+
// X-Crosstalk-Engine-Version response header — no duplication here.
|
|
19
21
|
const v = await api.get('/version');
|
|
20
22
|
process.stdout.write(`crosstalkd engine: ${v.version} (alias: ${v.alias})\n`);
|
|
21
23
|
} catch (err) {
|
package/lib/api-client.js
CHANGED
|
@@ -11,6 +11,38 @@
|
|
|
11
11
|
//
|
|
12
12
|
// No auth: the engine binds 127.0.0.1 only and there's no token. Same
|
|
13
13
|
// model as ollama / postgres-on-localhost.
|
|
14
|
+
//
|
|
15
|
+
// Version skew: engine sets X-Crosstalk-Engine-Version on every
|
|
16
|
+
// response. We compare it to the client's package.json version on each
|
|
17
|
+
// call and emit one warning per process if they don't match. Pre-1.0
|
|
18
|
+
// client and engine ship in lockstep; mismatches usually mean the
|
|
19
|
+
// operator upgraded one without the other.
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from 'fs';
|
|
22
|
+
import { dirname, join } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const CLIENT_VERSION = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
return JSON.parse(readFileSync(join(thisDir, '..', 'package.json'), 'utf-8')).version;
|
|
29
|
+
} catch {
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
let skewWarned = false;
|
|
35
|
+
|
|
36
|
+
function checkSkew(engineVersion) {
|
|
37
|
+
if (skewWarned || !engineVersion || engineVersion === CLIENT_VERSION) return;
|
|
38
|
+
skewWarned = true;
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`[WARN] Version skew: client ${CLIENT_VERSION}, engine ${engineVersion}.\n` +
|
|
41
|
+
` Pre-1.0 client and engine ship in lockstep. Likely fix:\n` +
|
|
42
|
+
` crosstalk pull && crosstalk restart # if engine is behind\n` +
|
|
43
|
+
` npm install -g @cordfuse/crosstalk # if client is behind\n\n`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
14
46
|
|
|
15
47
|
export const DEFAULT_API_PORT = 7000;
|
|
16
48
|
|
|
@@ -71,10 +103,13 @@ async function call(method, path, body, opts = {}) {
|
|
|
71
103
|
} catch (err) {
|
|
72
104
|
throw new ConnectError(port, err.message || String(err));
|
|
73
105
|
}
|
|
106
|
+
checkSkew(res.headers.get('x-crosstalk-engine-version'));
|
|
74
107
|
return parseJsonOrThrow(res);
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
export const api = {
|
|
78
|
-
get:
|
|
79
|
-
post:
|
|
111
|
+
get: (path, opts) => call('GET', path, undefined, opts),
|
|
112
|
+
post: (path, body, opts) => call('POST', path, body, opts),
|
|
113
|
+
patch: (path, body, opts) => call('PATCH', path, body, opts),
|
|
114
|
+
delete: (path, opts) => call('DELETE', path, undefined, opts),
|
|
80
115
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.3",
|
|
4
4
|
"description": "Crosstalk client — host-side CLI that talks to the crosstalkd daemon running inside the crosstalk-server container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|