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

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 (46) hide show
  1. package/README.md +26 -0
  2. package/bin/crosstalk.js +60 -74
  3. package/commands/channel.js +69 -0
  4. package/commands/chat.js +159 -0
  5. package/commands/down.js +37 -0
  6. package/commands/init.js +105 -0
  7. package/commands/logs.js +39 -0
  8. package/commands/pull.js +24 -0
  9. package/commands/replies.js +39 -0
  10. package/commands/restart.js +24 -0
  11. package/commands/run.js +121 -0
  12. package/commands/status.js +52 -0
  13. package/commands/up.js +129 -0
  14. package/commands/version.js +30 -0
  15. package/lib/api-client.js +80 -0
  16. package/lib/argv.js +28 -0
  17. package/lib/errors.js +19 -0
  18. package/lib/transport.js +51 -0
  19. package/package.json +5 -21
  20. package/src/activation.ts +0 -104
  21. package/src/actor.ts +0 -131
  22. package/src/attach.ts +0 -118
  23. package/src/channel.ts +0 -49
  24. package/src/chat.ts +0 -142
  25. package/src/dispatch.ts +0 -531
  26. package/src/dlq.ts +0 -216
  27. package/src/filenames.ts +0 -28
  28. package/src/frontmatter.ts +0 -26
  29. package/src/init.ts +0 -138
  30. package/src/open.ts +0 -207
  31. package/src/replies.ts +0 -59
  32. package/src/send.ts +0 -122
  33. package/src/state.ts +0 -173
  34. package/src/status.ts +0 -75
  35. package/src/stop.ts +0 -37
  36. package/src/transport.ts +0 -213
  37. package/src/turnq.ts +0 -91
  38. package/src/upgrade.ts +0 -211
  39. package/src/wake.ts +0 -7
  40. package/template/CLAUDE.md +0 -12
  41. package/template/gitignore +0 -4
  42. package/template/upstream/CROSSTALK-VERSION +0 -1
  43. package/template/upstream/CROSSTALK.md +0 -298
  44. package/template/upstream/OPERATOR.md +0 -60
  45. package/template/upstream/PROTOCOL.md +0 -80
  46. package/template/upstream/actors/concierge.md +0 -36
@@ -0,0 +1,24 @@
1
+ // crosstalk restart — restart the engine container.
2
+
3
+ import { existsSync } from 'fs';
4
+ import { spawnSync } from 'child_process';
5
+ import { requireTransportRoot, composeFile } from '../lib/transport.js';
6
+ import { has } from '../lib/argv.js';
7
+
8
+ export async function run(argv) {
9
+ if (has(argv, '--help') || has(argv, '-h')) {
10
+ process.stdout.write('Usage: crosstalk restart\n');
11
+ return 0;
12
+ }
13
+ const root = requireTransportRoot();
14
+ const composeYml = composeFile(root);
15
+ if (!existsSync(composeYml)) {
16
+ process.stderr.write(`crosstalk restart: no docker-compose.yml — run 'crosstalk up' first.\n`);
17
+ return 1;
18
+ }
19
+ const r = spawnSync('docker', ['compose', '-f', composeYml, 'restart'], {
20
+ cwd: root,
21
+ stdio: 'inherit',
22
+ });
23
+ return r.status ?? 1;
24
+ }
@@ -0,0 +1,121 @@
1
+ // crosstalk run — dispatch a primitive or workflow via POST /messages.
2
+ //
3
+ // Mirrors crosstalkd's run surface:
4
+ // crosstalk run --type primitive --to <model>[@<machine>] [--as <persona>]
5
+ // [--fanout <n>] [--channel <name|uuid>] [--from <name>]
6
+ // [--new] <body|file|->
7
+ // crosstalk run --type workflow [--channel <name|uuid>] [--from <name>]
8
+ // [--new] <file|->
9
+
10
+ import { readFileSync, existsSync, statSync } from 'fs';
11
+ import { api } from '../lib/api-client.js';
12
+ import { reportAndExit } from '../lib/errors.js';
13
+ import { flag, has } from '../lib/argv.js';
14
+
15
+ const FLAGS_WITH_VALUE = new Set(['--type', '--to', '--as', '--fanout', '--channel', '--from']);
16
+
17
+ function usage(exit = 0) {
18
+ const w = exit === 0 ? process.stdout : process.stderr;
19
+ w.write(
20
+ `Usage:
21
+ crosstalk run --type primitive --to <model>[@<machine>] \\
22
+ [--as <persona>] [--fanout <n>] \\
23
+ [--channel <name|uuid>] [--from <name>] [--new] \\
24
+ <body|file|->
25
+
26
+ crosstalk run --type workflow [--channel <name|uuid>] \\
27
+ [--from <name>] [--new] <file|->
28
+ `,
29
+ );
30
+ process.exit(exit);
31
+ }
32
+
33
+ function readStdin() {
34
+ try {
35
+ return readFileSync(0, 'utf-8');
36
+ } catch {
37
+ return '';
38
+ }
39
+ }
40
+
41
+ function resolveBody(arg) {
42
+ if (arg == null) return null;
43
+ if (arg === '-') return readStdin();
44
+ if (existsSync(arg)) {
45
+ try {
46
+ if (statSync(arg).isFile()) return readFileSync(arg, 'utf-8');
47
+ } catch { /* fall through to inline */ }
48
+ }
49
+ return arg;
50
+ }
51
+
52
+ export async function run(argv) {
53
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
54
+
55
+ const type = flag(argv, '--type');
56
+ if (type !== 'primitive' && type !== 'workflow') usage(1);
57
+
58
+ // The body is the last non-flag arg. Walk argv backwards skipping
59
+ // flag values.
60
+ let bodyArg;
61
+ for (let i = argv.length - 1; i >= 0; i--) {
62
+ const a = argv[i];
63
+ if (a.startsWith('--')) break;
64
+ // Make sure we're not the value of a flag like `--to <value>`
65
+ const prev = argv[i - 1];
66
+ if (prev && FLAGS_WITH_VALUE.has(prev)) continue;
67
+ bodyArg = a;
68
+ break;
69
+ }
70
+
71
+ const body = resolveBody(bodyArg);
72
+ if (body == null || body.length === 0) {
73
+ process.stderr.write('crosstalk run: missing body (file path, inline string, or `-` for stdin)\n');
74
+ return 1;
75
+ }
76
+
77
+ const channel = flag(argv, '--channel');
78
+ const from = flag(argv, '--from') ?? process.env.USER ?? 'operator';
79
+
80
+ let payload;
81
+ if (type === 'primitive') {
82
+ const to = flag(argv, '--to');
83
+ if (!to) {
84
+ process.stderr.write('crosstalk run --type primitive: --to <model> is required\n');
85
+ return 1;
86
+ }
87
+ const fanoutRaw = flag(argv, '--fanout');
88
+ const fanout = fanoutRaw ? Math.max(1, parseInt(fanoutRaw, 10)) : 1;
89
+ payload = {
90
+ type: 'primitive',
91
+ to,
92
+ as: flag(argv, '--as'),
93
+ channel,
94
+ from,
95
+ fanout,
96
+ body,
97
+ };
98
+ } else {
99
+ payload = {
100
+ type: 'workflow',
101
+ channel,
102
+ from,
103
+ body,
104
+ };
105
+ }
106
+
107
+ let resp;
108
+ try {
109
+ resp = await api.post('/messages', payload);
110
+ } catch (err) {
111
+ reportAndExit(err, 'crosstalk run');
112
+ }
113
+
114
+ if (resp.type === 'primitive') {
115
+ for (const p of resp.relPaths) process.stdout.write(`Sent: ${p}\n`);
116
+ } else {
117
+ process.stdout.write(`Workflow dispatched: ${resp.relPath}\n`);
118
+ process.stdout.write(`Child channel: ${resp.childChannel} (parent: ${resp.channel.slice(0, 8)})\n`);
119
+ }
120
+ return 0;
121
+ }
@@ -0,0 +1,52 @@
1
+ // crosstalk status — mirror of crosstalkd status output, fetched via API.
2
+
3
+ import { api } from '../lib/api-client.js';
4
+ import { reportAndExit } from '../lib/errors.js';
5
+
6
+ export async function run(_argv) {
7
+ let s;
8
+ try {
9
+ s = await api.get('/status');
10
+ } catch (err) {
11
+ reportAndExit(err, 'crosstalk status');
12
+ }
13
+
14
+ process.stdout.write(`Crosstalk transport: ${s.transport_root}\n`);
15
+ process.stdout.write(`Engine alias: ${s.alias}\n`);
16
+ process.stdout.write(`Engine version: ${s.version}\n`);
17
+ process.stdout.write('\n');
18
+
19
+ process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
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');
22
+ } else {
23
+ for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
24
+ }
25
+ process.stdout.write('\n');
26
+
27
+ if (s.heartbeat) {
28
+ const ageS = Math.floor((Date.now() - new Date(s.heartbeat.ts).getTime()) / 1000);
29
+ const label = ageS < 120 ? 'LIVE' : ageS > 300 ? 'STALE' : 'idle';
30
+ process.stdout.write(`Dispatch heartbeat: ${label} (pid ${s.heartbeat.pid}, last tick ${ageS}s ago, ${s.heartbeat.version})\n`);
31
+ } else {
32
+ process.stdout.write('Dispatch heartbeat: never\n');
33
+ }
34
+
35
+ if (s.cursor) {
36
+ process.stdout.write(`Cursor: ${s.cursor.slice(0, 12)} (global, machine-local)\n`);
37
+ } else {
38
+ process.stdout.write('Cursor: (none yet — first tick will seed to HEAD)\n');
39
+ }
40
+ process.stdout.write('\n');
41
+
42
+ process.stdout.write(`Channels: ${s.channels.length}\n`);
43
+ for (const ch of s.channels) {
44
+ const name = ch.name ?? '(unnamed)';
45
+ const parentSuffix = ch.parent ? ` [child of ${ch.parent.slice(0, 8)}]` : '';
46
+ process.stdout.write(` ${ch.uuid.slice(0, 8)}... — ${name}${parentSuffix}\n`);
47
+ }
48
+ process.stdout.write('\n');
49
+
50
+ process.stdout.write(`Infrastructure errors logged: ${s.errors_logged}\n`);
51
+ return 0;
52
+ }
package/commands/up.js ADDED
@@ -0,0 +1,129 @@
1
+ // crosstalk up — bring up the engine container for this transport.
2
+ //
3
+ // First-time use: generates docker-compose.yml in the transport root from
4
+ // a template (operator can edit freely, subsequent `up`s won't clobber).
5
+ // The compose file bind-mounts the transport root as the engine's working
6
+ // tree so git operations work both ways — engine sees operator's local
7
+ // commits and configured remotes immediately, operator sees engine
8
+ // commits (replies, cursor advances) immediately.
9
+ //
10
+ // Subsequent runs: just shell out to `docker compose up -d`.
11
+
12
+ import { writeFileSync, existsSync, appendFileSync, readFileSync } from 'fs';
13
+ import { spawnSync } from 'child_process';
14
+ import { join } from 'path';
15
+ import { requireTransportRoot, transportName, composeFile } from '../lib/transport.js';
16
+ import { has } from '../lib/argv.js';
17
+
18
+ const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
19
+ ?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.1';
20
+ const DEFAULT_API_PORT = Number(process.env.CROSSTALK_API_PORT) || 7000;
21
+
22
+ function usage(exit = 0) {
23
+ const w = exit === 0 ? process.stdout : process.stderr;
24
+ w.write(
25
+ `Usage: crosstalk up
26
+
27
+ Brings up the engine container for the transport in the current directory
28
+ (or any ancestor containing CROSSTALK-VERSION). First-time use generates
29
+ docker-compose.yml in the transport root. Override the image with
30
+ CROSSTALK_IMAGE, the API port with CROSSTALK_API_PORT.
31
+ `,
32
+ );
33
+ process.exit(exit);
34
+ }
35
+
36
+ function renderCompose({ name, image, apiPort, alias, uid, gid }) {
37
+ // Entrypoint runs as root for setup (apt-installed CLIs in /root,
38
+ // npm prefix, SSH key import). It then adjusts a 'crosstalkd' user
39
+ // to match CROSSTALK_UID/GID (operator's host UID), chowns the
40
+ // bind-mounted transport + state dir to that user, and drops via
41
+ // setpriv before exec'ing the dispatcher. Net effect: bind-mount
42
+ // files written by the engine are operator-owned on the host, so
43
+ // \`git remote add\` and friends work without sudo. /root stays
44
+ // root-owned for CLI installs.
45
+ return `# Generated by \`crosstalk up\`. Safe to edit; subsequent \`up\`s
46
+ # only regenerate when this file is missing. Compose config is machine-
47
+ # local — kept out of git via .gitignore by default.
48
+
49
+ services:
50
+ crosstalkd:
51
+ image: ${image}
52
+ container_name: crosstalk-${name}
53
+ restart: unless-stopped
54
+ ports:
55
+ - "127.0.0.1:${apiPort}:7000"
56
+ volumes:
57
+ # Transport bind-mount: \`git remote add\` from the host is visible
58
+ # to the engine on the next tick. No clone-on-startup needed.
59
+ - .:/var/lib/crosstalk-transport
60
+ # Operator-installed agent CLIs (claude, codex, agy, ...) + auth
61
+ # state. Root-owned, dispatcher reads only.
62
+ - crosstalk-root:/root
63
+ # Dispatcher's machine-local state: cursor, heartbeat, errors.log.
64
+ # Operator-UID-owned (chowned by entrypoint to CROSSTALK_UID/GID).
65
+ - crosstalk-state:/var/lib/crosstalkd-state
66
+ environment:
67
+ CROSSTALK_ALIAS: ${alias}
68
+ CROSSTALK_UID: "${uid}"
69
+ CROSSTALK_GID: "${gid}"
70
+ CROSSTALKD_API_PORT: "7000"
71
+ DISPATCH_JSON: "true"
72
+ DISPATCH_POLL_SECONDS: "30"
73
+
74
+ volumes:
75
+ crosstalk-root:
76
+ crosstalk-state:
77
+ `;
78
+ }
79
+
80
+ function ensureGitignored(transportRoot) {
81
+ const path = join(transportRoot, '.gitignore');
82
+ const entry = 'docker-compose.yml\n';
83
+ if (!existsSync(path)) {
84
+ writeFileSync(path, entry);
85
+ return;
86
+ }
87
+ const current = readFileSync(path, 'utf-8');
88
+ if (!current.split('\n').includes('docker-compose.yml')) {
89
+ appendFileSync(path, (current.endsWith('\n') ? '' : '\n') + entry);
90
+ }
91
+ }
92
+
93
+ export async function run(argv) {
94
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
95
+
96
+ const root = requireTransportRoot();
97
+ const composeYml = composeFile(root);
98
+ const generated = !existsSync(composeYml);
99
+
100
+ if (generated) {
101
+ const alias = process.env.CROSSTALK_ALIAS ?? process.env.HOSTNAME ?? transportName(root);
102
+ const uid = typeof process.getuid === 'function' ? process.getuid() : 1000;
103
+ const gid = typeof process.getgid === 'function' ? process.getgid() : 1000;
104
+ const content = renderCompose({
105
+ name: transportName(root),
106
+ image: DEFAULT_IMAGE,
107
+ apiPort: DEFAULT_API_PORT,
108
+ alias,
109
+ uid,
110
+ gid,
111
+ });
112
+ writeFileSync(composeYml, content);
113
+ ensureGitignored(root);
114
+ process.stdout.write(`Generated ${composeYml}\n`);
115
+ }
116
+
117
+ const r = spawnSync('docker', ['compose', '-f', composeYml, 'up', '-d'], {
118
+ cwd: root,
119
+ stdio: 'inherit',
120
+ });
121
+ if (r.status !== 0) {
122
+ process.stderr.write(`crosstalk up: docker compose exited ${r.status}\n`);
123
+ return r.status ?? 1;
124
+ }
125
+ process.stdout.write(
126
+ `\nEngine starting. Check status with 'crosstalk status' (give it ~5s to settle).\n`,
127
+ );
128
+ return 0;
129
+ }
@@ -0,0 +1,30 @@
1
+ // crosstalk version — print client and (if reachable) engine versions.
2
+ //
3
+ // Doesn't fail if the engine isn't reachable; just prints "(engine
4
+ // unreachable)" since you might be running `version` before bringing
5
+ // the container up.
6
+
7
+ import { readFileSync } from 'fs';
8
+ import { dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { api, ConnectError } from '../lib/api-client.js';
11
+
12
+ const thisDir = dirname(fileURLToPath(import.meta.url));
13
+ const pkgPath = join(thisDir, '..', 'package.json');
14
+ const clientPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
15
+
16
+ export async function run(_argv) {
17
+ process.stdout.write(`crosstalk client: ${clientPkg.version}\n`);
18
+ try {
19
+ const v = await api.get('/version');
20
+ process.stdout.write(`crosstalkd engine: ${v.version} (alias: ${v.alias})\n`);
21
+ } catch (err) {
22
+ if (err instanceof ConnectError) {
23
+ process.stdout.write(`crosstalkd engine: (unreachable on 127.0.0.1:${err.port})\n`);
24
+ return 0;
25
+ }
26
+ process.stdout.write(`crosstalkd engine: (error: ${err.message})\n`);
27
+ return 0;
28
+ }
29
+ return 0;
30
+ }
@@ -0,0 +1,80 @@
1
+ // api-client.js — HTTP client over loopback to crosstalkd's local API.
2
+ //
3
+ // All operator commands route through this. The engine binds 127.0.0.1
4
+ // inside the container; docker-compose maps that to the host loopback
5
+ // at the same port. Client connects to http://127.0.0.1:<port>/<path>.
6
+ //
7
+ // Port resolution (in order):
8
+ // 1. --port <n> CLI flag (handled by individual commands)
9
+ // 2. CROSSTALK_API_PORT env var
10
+ // 3. Default 7000
11
+ //
12
+ // No auth: the engine binds 127.0.0.1 only and there's no token. Same
13
+ // model as ollama / postgres-on-localhost.
14
+
15
+ export const DEFAULT_API_PORT = 7000;
16
+
17
+ export function resolvePort(explicit) {
18
+ if (typeof explicit === 'number' && Number.isInteger(explicit)) return explicit;
19
+ const fromEnv = Number(process.env.CROSSTALK_API_PORT);
20
+ if (Number.isInteger(fromEnv) && fromEnv > 0) return fromEnv;
21
+ return DEFAULT_API_PORT;
22
+ }
23
+
24
+ function apiUrl(path, port) {
25
+ return `http://127.0.0.1:${port}${path}`;
26
+ }
27
+
28
+ async function parseJsonOrThrow(res) {
29
+ let body;
30
+ try {
31
+ body = await res.json();
32
+ } catch {
33
+ throw new ApiError(res.status, 'invalid JSON in response');
34
+ }
35
+ if (!res.ok) {
36
+ const msg = body && typeof body.error === 'string' ? body.error : `HTTP ${res.status}`;
37
+ throw new ApiError(res.status, msg);
38
+ }
39
+ return body;
40
+ }
41
+
42
+ export class ApiError extends Error {
43
+ constructor(status, message) {
44
+ super(message);
45
+ this.name = 'ApiError';
46
+ this.status = status;
47
+ }
48
+ }
49
+
50
+ export class ConnectError extends Error {
51
+ constructor(port, cause) {
52
+ super(
53
+ `cannot reach crosstalkd on 127.0.0.1:${port} — is the container running? ` +
54
+ `Try 'crosstalk up' to start it. (underlying: ${cause})`,
55
+ );
56
+ this.name = 'ConnectError';
57
+ this.port = port;
58
+ }
59
+ }
60
+
61
+ async function call(method, path, body, opts = {}) {
62
+ const port = resolvePort(opts.port);
63
+ const init = {
64
+ method,
65
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
66
+ body: body ? JSON.stringify(body) : undefined,
67
+ };
68
+ let res;
69
+ try {
70
+ res = await fetch(apiUrl(path, port), init);
71
+ } catch (err) {
72
+ throw new ConnectError(port, err.message || String(err));
73
+ }
74
+ return parseJsonOrThrow(res);
75
+ }
76
+
77
+ export const api = {
78
+ get: (path, opts) => call('GET', path, undefined, opts),
79
+ post: (path, body, opts) => call('POST', path, body, opts),
80
+ };
package/lib/argv.js ADDED
@@ -0,0 +1,28 @@
1
+ // Tiny argv helpers shared across commands. No yargs / commander
2
+ // dependency — the surfaces are simple enough that hand-parsing is
3
+ // shorter than wiring a framework.
4
+
5
+ export function flag(argv, name) {
6
+ const i = argv.indexOf(name);
7
+ if (i === -1 || i === argv.length - 1) return undefined;
8
+ return argv[i + 1];
9
+ }
10
+
11
+ export function has(argv, name) {
12
+ return argv.includes(name);
13
+ }
14
+
15
+ // All positional (non-flag) args, skipping flag values.
16
+ export function positionals(argv, flagsWithValue) {
17
+ const valueFlags = new Set(flagsWithValue);
18
+ const out = [];
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a.startsWith('--')) {
22
+ if (valueFlags.has(a)) i++;
23
+ continue;
24
+ }
25
+ out.push(a);
26
+ }
27
+ return out;
28
+ }
package/lib/errors.js ADDED
@@ -0,0 +1,19 @@
1
+ // Shared error→stderr formatting for client commands.
2
+ //
3
+ // Pattern: catch known error types (ConnectError, ApiError) and print
4
+ // operator-readable messages with appropriate exit codes. Unknown errors
5
+ // rethrow so they show stack traces — those are real bugs to fix.
6
+
7
+ import { ApiError, ConnectError } from './api-client.js';
8
+
9
+ export function reportAndExit(err, contextHint) {
10
+ if (err instanceof ConnectError) {
11
+ process.stderr.write(`${contextHint}: ${err.message}\n`);
12
+ process.exit(4);
13
+ }
14
+ if (err instanceof ApiError) {
15
+ process.stderr.write(`${contextHint}: ${err.message} (HTTP ${err.status})\n`);
16
+ process.exit(err.status === 400 ? 1 : 3);
17
+ }
18
+ throw err;
19
+ }
@@ -0,0 +1,51 @@
1
+ // transport.js — locate the current Crosstalk transport on the host,
2
+ // derive its conventional names (compose project, container name, etc.).
3
+ //
4
+ // A transport is identified by a CROSSTALK-VERSION file at its root.
5
+ // `crosstalk` walks up from the operator's cwd to find it — same pattern
6
+ // as `git`, `npm`, etc. discovering their root directories.
7
+
8
+ import { existsSync, statSync } from 'fs';
9
+ import { resolve, basename, dirname, join } from 'path';
10
+
11
+ export function findTransportRoot(startDir = process.cwd()) {
12
+ let dir = resolve(startDir);
13
+ for (;;) {
14
+ if (existsSync(join(dir, 'CROSSTALK-VERSION'))) {
15
+ try {
16
+ if (statSync(join(dir, 'CROSSTALK-VERSION')).isFile()) return dir;
17
+ } catch { /* fall through */ }
18
+ }
19
+ const parent = dirname(dir);
20
+ if (parent === dir) return null;
21
+ dir = parent;
22
+ }
23
+ }
24
+
25
+ export function requireTransportRoot(startDir = process.cwd()) {
26
+ const root = findTransportRoot(startDir);
27
+ if (!root) {
28
+ process.stderr.write(
29
+ `crosstalk: not inside a Crosstalk transport ` +
30
+ `(no CROSSTALK-VERSION found from ${resolve(startDir)} upward).\n` +
31
+ `Create one with 'crosstalk init <dir>' or cd into an existing transport.\n`,
32
+ );
33
+ process.exit(2);
34
+ }
35
+ return root;
36
+ }
37
+
38
+ // transport-name is the basename of the transport root. Used for the
39
+ // compose project, container name, volume names — gives each transport
40
+ // on a host distinct docker objects without an explicit registry.
41
+ export function transportName(transportRoot) {
42
+ return basename(transportRoot);
43
+ }
44
+
45
+ export function containerName(transportRoot) {
46
+ return `crosstalk-${transportName(transportRoot)}`;
47
+ }
48
+
49
+ export function composeFile(transportRoot) {
50
+ return join(transportRoot, 'docker-compose.yml');
51
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cordfuse/crosstalk",
3
- "version": "6.0.0-alpha.9",
4
- "description": "Crosstalk runtimeasync messaging between agents over git, across machines.",
3
+ "version": "7.0.0-alpha.1",
4
+ "description": "Crosstalk clienthost-side CLI that talks to the crosstalkd daemon running inside the crosstalk-server container.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/cordfuse/crosstalk",
@@ -14,26 +14,10 @@
14
14
  },
15
15
  "files": [
16
16
  "bin/",
17
- "src/",
18
- "template/"
17
+ "lib/",
18
+ "commands/",
19
+ "README.md"
19
20
  ],
20
- "scripts": {
21
- "build": "tsc --noEmit",
22
- "lint": "tsc --noEmit",
23
- "test": "bun test",
24
- "prepack": "cp -r ../transport template",
25
- "postpack": "rm -rf template"
26
- },
27
- "dependencies": {
28
- "@cordfuse/turnq": "^0.4.2",
29
- "@lydell/node-pty": "^1.2.0-beta.12",
30
- "tsx": "^4.20.0",
31
- "yaml": "^2.8.0"
32
- },
33
- "devDependencies": {
34
- "@types/node": "^22.10.0",
35
- "typescript": "^5.7.0"
36
- },
37
21
  "engines": {
38
22
  "node": ">=20"
39
23
  },
package/src/activation.ts DELETED
@@ -1,104 +0,0 @@
1
- // The activation rule — pure functions, no I/O, exhaustively unit-tested.
2
- //
3
- // One rule (CROSSTALK.md "Activation"):
4
- //
5
- // A message wakes its addressee if it has no `re:` (a new task), or its
6
- // `re:` points at a message the addressee sent.
7
- //
8
- // `re:` is written by the runtime at send time, never inferred at read
9
- // time. It is a string or a list: a reply that answers a batch of N
10
- // messages records ALL N relPaths, so no answered message is ever lost to
11
- // batching.
12
-
13
- export interface ActivationMessage {
14
- from: string;
15
- to: string[];
16
- re?: string | string[];
17
- }
18
-
19
- export function recipients(toField: unknown): string[] {
20
- if (Array.isArray(toField)) return toField.map(String);
21
- if (typeof toField === 'string') return [toField];
22
- return [];
23
- }
24
-
25
- export function reList(reField: unknown): string[] {
26
- if (Array.isArray(reField)) return reField.map(String);
27
- if (typeof reField === 'string') return [reField];
28
- return [];
29
- }
30
-
31
- // A recipient is `actor` or `actor@host`.
32
- export function extractActor(recipient: string): string {
33
- const at = recipient.indexOf('@');
34
- return at === -1 ? recipient : recipient.slice(0, at);
35
- }
36
-
37
- export function targetHost(recipient: string): string | null {
38
- const at = recipient.indexOf('@');
39
- return at === -1 ? null : recipient.slice(at + 1);
40
- }
41
-
42
- export interface RoutingResult {
43
- addressed: boolean;
44
- // Actor was named, but every instance targeted a different host — logged
45
- // by the dispatcher so wrong-host routes are visible, never silent.
46
- wrongHost: boolean;
47
- }
48
-
49
- export function matchRouting(
50
- recipientList: string[],
51
- actorName: string,
52
- thisHost: string,
53
- ): RoutingResult {
54
- let actorNamedAtAll = false;
55
- for (const r of recipientList) {
56
- if (r === 'all') return { addressed: true, wrongHost: false };
57
- if (extractActor(r) !== actorName) continue;
58
- actorNamedAtAll = true;
59
- const host = targetHost(r);
60
- if (host === null || host === thisHost) return { addressed: true, wrongHost: false };
61
- }
62
- return { addressed: false, wrongHost: actorNamedAtAll };
63
- }
64
-
65
- export type WakeDecision = 'wake' | 'skip' | 'wrong-host';
66
-
67
- // `senderOf` resolves a channel relPath to the `from:` of the message there,
68
- // or undefined if no such message exists. A dangling `re:` entry (target
69
- // missing) wakes the addressee — fail open so a message is never silently
70
- // dropped; no loop is possible because the reply to it carries a
71
- // resolvable `re:`.
72
- export function decideWake(
73
- msg: ActivationMessage,
74
- actorName: string,
75
- thisHost: string,
76
- senderOf: (relPath: string) => string | undefined,
77
- ): WakeDecision {
78
- const routing = matchRouting(msg.to, actorName, thisHost);
79
- if (!routing.addressed) return routing.wrongHost ? 'wrong-host' : 'skip';
80
- if (msg.from === actorName) return 'skip';
81
- const targets = reList(msg.re);
82
- if (targets.length === 0) return 'wake';
83
- for (const target of targets) {
84
- const asker = senderOf(target);
85
- if (asker === undefined || asker === actorName) return 'wake';
86
- }
87
- return 'skip';
88
- }
89
-
90
- // Split a channel's pending messages (already sorted by relPath) into
91
- // contiguous batches sized for the actor's concurrency. Contiguous so each
92
- // batch's highest relPath is monotone across batches — the cursor advances
93
- // safely per batch. When pending fits within concurrency, every batch is a
94
- // single message (parallel fan-out); when it exceeds, batches collapse into
95
- // ~concurrency invocations (fan-in stays O(1) per actor).
96
- export function splitForConcurrency<T>(msgs: T[], concurrency: number): T[][] {
97
- if (concurrency <= 1 || msgs.length <= 1) return [msgs];
98
- const chunkSize = Math.max(1, Math.ceil(msgs.length / concurrency));
99
- const out: T[][] = [];
100
- for (let i = 0; i < msgs.length; i += chunkSize) {
101
- out.push(msgs.slice(i, i + chunkSize));
102
- }
103
- return out;
104
- }