@cordfuse/crosstalk 7.0.0-alpha.2 → 7.0.0-alpha.4

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,13 +24,63 @@
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'];
30
- // Conservative URL regex: matches the leading https://...token boundary,
31
- // stops at whitespace/quote/angle. False positives are bounded to --login
32
- // mode where the first URL printed is almost certainly the auth URL.
33
- const URL_RE = /https?:\/\/[^\s<>"']+/;
31
+
32
+ // URL extraction that survives agent TUI rendering. Naive regex against
33
+ // raw stdout fails because agent CLIs (Claude in particular) draw a box
34
+ // around the URL with line wraps every ~N chars; the captured URL is
35
+ // truncated at the first wrap, with ANSI control chars leaking in at
36
+ // the cut point. Real example from alpha.3 macOS test: extracted URL
37
+ // ended with `%18%6b…` (CAN char + box-drawing artifact) instead of
38
+ // the `redirect_uri=…` param, breaking OAuth.
39
+ //
40
+ // Approach:
41
+ // 1. ANSI-strip everything (CSI / OSC / SS3 escapes).
42
+ // 2. Strip control chars (0x00-0x1F except LF, plus DEL).
43
+ // 3. Join line wraps that occur INSIDE a URL-safe character run —
44
+ // `<url-char>\n<url-char>` → `<url-char><url-char>`. Preserves real
45
+ // `<url-char>\n<non-url-char>` boundaries.
46
+ // 4. Match `https?://<url-chars>+` against the joined buffer.
47
+ // 5. Only fire when the character AFTER the matched URL is a true
48
+ // terminator (whitespace, end-of-buffer) — guards against acting
49
+ // on a partial URL still being written.
50
+
51
+ const URL_SAFE_CLASS = "a-zA-Z0-9%/?#&=+\\-._~:@!$()*,;";
52
+ const URL_RE = new RegExp(`https?://[${URL_SAFE_CLASS}]+`);
53
+
54
+ function extractCompleteUrl(rawBuffer) {
55
+ // Strip ANSI CSI escapes (\x1B[ ... terminator).
56
+ let clean = rawBuffer.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, '');
57
+ // Strip ANSI OSC escapes (\x1B] ... BEL or ST).
58
+ clean = clean.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
59
+ // Strip SS3 escapes (\x1BO + one char).
60
+ clean = clean.replace(/\x1BO./g, '');
61
+ // Strip remaining single-char escapes that slipped through.
62
+ clean = clean.replace(/\x1B[^a-zA-Z0-9]/g, '');
63
+ // Strip control chars except LF (0x0A); we use LF for line-wrap rejoin.
64
+ clean = clean.replace(/[\x00-\x09\x0B-\x1F\x7F]/g, '');
65
+ // Rejoin URL-character wrap points: `<url-char>\n<url-char>` becomes
66
+ // `<url-char><url-char>`. Doesn't merge `<url-char>\n<space>` etc.
67
+ const safeRe = new RegExp(`[${URL_SAFE_CLASS}]\\n[${URL_SAFE_CLASS}]`, 'g');
68
+ let prev;
69
+ do {
70
+ prev = clean;
71
+ clean = clean.replace(safeRe, (m) => m[0] + m[2]);
72
+ } while (clean !== prev);
73
+
74
+ const match = clean.match(URL_RE);
75
+ if (!match) return null;
76
+ // Require a terminator after the URL — without it, the URL may still
77
+ // be growing in subsequent stdout chunks. Terminator = whitespace,
78
+ // quote/angle, or end-of-buffer-with-trailing-newline.
79
+ const after = clean.slice(match.index + match[0].length);
80
+ if (after.length === 0) return null; // EOF-as-grow case, wait
81
+ if (!/^[\s<>"']/.test(after)) return null; // URL still continuing
82
+ return match[0];
83
+ }
34
84
 
35
85
  function openInBrowser(url) {
36
86
  const cmd =
@@ -106,27 +156,29 @@ function runLogin(container, agent) {
106
156
  //
107
157
  // No agent-specific "login" subcommand — we just run the agent's
108
158
  // default interactive entry point. First-run auth flow happens
109
- // automatically for every agent we support. Operator can pass
110
- // --agent <name> to target a specific one.
159
+ // automatically for every agent we support.
160
+ //
161
+ // No COLUMNS=1000 env: making the agent render the URL "unwrapped" via
162
+ // a wide terminal claim doesn't reliably propagate to the in-container
163
+ // PTY, and even when it does Claude's TUI hard-wraps anyway. The
164
+ // extractCompleteUrl() function joins wrapped URL fragments on the
165
+ // host side, so the agent's rendering width is irrelevant. As bonus,
166
+ // dropping COLUMNS=1000 fixes daily-chat soft-wrap weirdness in
167
+ // long-form prose (agents see the real host terminal width).
111
168
  return new Promise((resolve) => {
112
169
  const child = spawn(
113
170
  'docker',
114
- [
115
- 'exec', '-it',
116
- '--env', 'COLUMNS=1000', // fallback for browser-open failure
117
- '--env', 'LINES=40',
118
- container,
119
- agent,
120
- ],
171
+ ['exec', '-it', container, agent],
121
172
  { stdio: ['inherit', 'pipe', 'inherit'] },
122
173
  );
123
174
  let opened = false;
175
+ let buffer = '';
124
176
  child.stdout.on('data', (chunk) => {
125
177
  process.stdout.write(chunk);
126
178
  if (opened) return;
127
- const match = chunk.toString().match(URL_RE);
128
- if (!match) return;
129
- const url = match[0];
179
+ buffer += chunk.toString();
180
+ const url = extractCompleteUrl(buffer);
181
+ if (!url) return;
130
182
  opened = true;
131
183
  const ok = openInBrowser(url);
132
184
  process.stdout.write(
@@ -152,10 +204,33 @@ export async function run(argv) {
152
204
 
153
205
  const agent = flag(argv, '--agent');
154
206
  if (!agent) {
207
+ // Query the engine for what's actually installed (vs the theoretical
208
+ // supported set). Gives a smarter error than "supported: claude,
209
+ // codex, gemini, qwen, opencode, agy" when only 1-2 are actually
210
+ // present in the container.
211
+ let installed = null;
212
+ try {
213
+ const r = await api.get('/agents/installed');
214
+ installed = r.installed;
215
+ } catch (err) {
216
+ if (!(err instanceof ConnectError)) {
217
+ // engine unreachable — fall through with no introspection
218
+ }
219
+ }
220
+ if (installed && installed.length === 0) {
221
+ process.stderr.write(
222
+ `crosstalk chat: no agent CLIs installed in this container.\n` +
223
+ ` Install one first:\n` +
224
+ ` crosstalk chat --shell\n` +
225
+ ` npm install -g @anthropic-ai/claude-code # or your agent's installer\n`,
226
+ );
227
+ return 1;
228
+ }
229
+ const list = installed && installed.length > 0
230
+ ? `Installed in this container: ${installed.join(', ')}.`
231
+ : `Supported agents: ${KNOWN_AGENTS.join(', ')}.`;
155
232
  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`,
233
+ `crosstalk chat: --agent <name> is required.\n ${list}\n`,
159
234
  );
160
235
  return 1;
161
236
  }
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.4';
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.4 -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.4 crosstalk init mytransport
40
40
  `,
41
41
  );
42
42
  process.exit(exit);
@@ -18,7 +18,22 @@ export async function run(_argv) {
18
18
 
19
19
  process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
20
20
  if (s.claimed_models.length === 0) {
21
- process.stdout.write(' (no model CLIs found inside the container install one or edit data/models.yaml)\n');
21
+ // Two failure shapes look the same in claimed_models=0: (a) no CLIs
22
+ // installed at all, (b) CLIs installed but data/models.yaml has no
23
+ // entries referencing them. Ask /agents/installed to distinguish so
24
+ // the operator gets a directly-actionable hint.
25
+ let installed = null;
26
+ try {
27
+ const r = await api.get('/agents/installed');
28
+ installed = r.installed;
29
+ } catch { /* ignore — fall back to generic message */ }
30
+ if (installed && installed.length > 0) {
31
+ process.stdout.write(
32
+ ` (${installed.join(', ')} installed in the container but no data/models.yaml entry references them — add one to claim)\n`,
33
+ );
34
+ } else {
35
+ process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/models.yaml)\n');
36
+ }
22
37
  } else {
23
38
  for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
24
39
  }
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.4';
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.4",
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",