@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 CHANGED
@@ -1,26 +1,92 @@
1
1
  # @cordfuse/crosstalk — host client
2
2
 
3
- > **README is a stub.** Full operator-facing docs land in the docs-consolidation pass. The body below is intentionally short; it covers what's actually implemented today.
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
- The host-side CLI for the Crosstalk protocol. Pairs with `@cordfuse/crosstalkd`, the daemon that runs inside the `crosstalk-server` container.
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
- - Operator installs this package: `npm install -g @cordfuse/crosstalk`
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
- This split mirrors `docker` (host CLI) / `dockerd` (daemon). The client never reads or writes transport files directly — all protocol operations go through the API.
9
+ ## Install
12
10
 
13
- ## Status (v7.0.0-dev.0)
11
+ ```sh
12
+ npm install -g @cordfuse/crosstalk
13
+ ```
14
14
 
15
- - `crosstalk version` works
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
- ## Install (post-publish; today this package is unpublished)
17
+ ---
18
+
19
+ ## Quickstart
19
20
 
20
21
  ```sh
21
- npm install -g @cordfuse/crosstalk
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.
@@ -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 <name> # create a channel
16
- crosstalk channel list # list channels
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
- // Otherwise: create a channel with the given name.
54
- if (has(argv, '--rename') || has(argv, '--delete')) {
55
- process.stderr.write('crosstalk channel: --rename and --delete are not implemented yet (engine API needs PATCH/DELETE).\n');
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
- const name = pos[0];
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 (containers can have ` +
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.2';
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.2 -f server/Dockerfile .
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.2 crosstalk init mytransport
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.2';
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) {
@@ -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: (path, opts) => call('GET', path, undefined, opts),
79
- post: (path, body, opts) => call('POST', path, body, opts),
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.2",
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",