@cordfuse/crosstalk 7.0.0-alpha.9 → 8.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.
- package/GUIDE-CLI.md +322 -0
- package/GUIDE-PROMPTS.md +118 -0
- package/LICENSE +21 -0
- package/README.md +422 -61
- package/bin/crosstalk.js +88 -54
- package/commands/agent.js +69 -0
- package/commands/auth.js +273 -0
- package/commands/channel.js +54 -44
- package/commands/chat.js +107 -71
- package/commands/daemon.js +120 -0
- package/commands/logs.js +108 -19
- package/commands/message.js +125 -0
- package/commands/server.js +185 -0
- package/commands/settings.js +49 -0
- package/commands/status.js +37 -13
- package/commands/token.js +136 -0
- package/commands/transport.js +299 -0
- package/commands/version.js +3 -3
- package/commands/workflow.js +234 -0
- package/deploy/crosstalk@.service +62 -0
- package/deploy/install.sh +82 -0
- package/lib/api-client.js +77 -22
- package/lib/credentials.js +207 -0
- package/lib/nativeServer.js +182 -0
- package/lib/resolve.js +106 -37
- package/package.json +27 -4
- package/src/activation.ts +104 -0
- package/src/api.ts +1717 -0
- package/src/auth/enforce.ts +68 -0
- package/src/auth/handlers.ts +266 -0
- package/src/auth/middleware.ts +132 -0
- package/src/auth/setup.ts +263 -0
- package/src/auth/tokens.ts +285 -0
- package/src/auth/users.ts +267 -0
- package/src/dispatch.ts +530 -0
- package/src/dispatchers.ts +91 -0
- package/src/filenames.ts +28 -0
- package/src/frontmatter.ts +26 -0
- package/src/init.ts +116 -0
- package/src/invoke.ts +201 -0
- package/src/log-buffer.ts +67 -0
- package/src/models.ts +300 -0
- package/src/resolve.ts +100 -0
- package/src/state.ts +191 -0
- package/src/stop.ts +37 -0
- package/src/transport.ts +353 -0
- package/src/web/auth-pages.ts +160 -0
- package/src/web/channels.ts +395 -0
- package/src/web/chat-page.ts +636 -0
- package/src/web/chat-pty.ts +254 -0
- package/src/web/dashboard.ts +130 -0
- package/src/web/layout.ts +237 -0
- package/src/web/stubs.ts +510 -0
- package/src/web/workflows.ts +490 -0
- package/src/workflow.ts +470 -0
- package/template/CLAUDE.md +10 -0
- package/template/CROSSTALK-VERSION +1 -0
- package/template/CROSSTALK.md +262 -0
- package/template/PROTOCOL.md +70 -0
- package/template/README.md +64 -0
- package/template/auth/.gitkeep +0 -0
- package/template/auth/README.md +224 -0
- package/template/data/crosstalk.yaml +196 -0
- package/template/gitignore +4 -0
- package/commands/down.js +0 -40
- package/commands/init.js +0 -243
- package/commands/pull.js +0 -22
- package/commands/replies.js +0 -40
- package/commands/restart.js +0 -29
- package/commands/rm.js +0 -109
- package/commands/run.js +0 -115
- package/commands/up.js +0 -135
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// crosstalk transport <init|rm> — scaffold or tear down a transport.
|
|
2
|
+
//
|
|
3
|
+
// init [--containername <name>] [--remote <url>]
|
|
4
|
+
// Idempotent — re-running fills in missing markers without
|
|
5
|
+
// disturbing existing state. Engine scaffold runs via
|
|
6
|
+
// `crosstalk daemon init <path>` (the internal subcommand).
|
|
7
|
+
//
|
|
8
|
+
// rm [--containername <name>] [--force]
|
|
9
|
+
// Stops the engine (best-effort) then deletes <base>/<name>/.
|
|
10
|
+
// Confirmation prompt by default; --force to skip.
|
|
11
|
+
//
|
|
12
|
+
// Replaces v7's bare `crosstalk init` + `crosstalk rm` (both removed).
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
import { createInterface } from 'readline';
|
|
17
|
+
import { join, resolve } from 'path';
|
|
18
|
+
import { has, flag } from '../lib/argv.js';
|
|
19
|
+
import {
|
|
20
|
+
parseContainerName,
|
|
21
|
+
storagePaths,
|
|
22
|
+
isInitialized,
|
|
23
|
+
pickFreePort,
|
|
24
|
+
DEFAULT_CONTAINER_NAME,
|
|
25
|
+
} from '../lib/resolve.js';
|
|
26
|
+
import { stopEngine, resolveSelfBin } from '../lib/nativeServer.js';
|
|
27
|
+
|
|
28
|
+
function usage(exit = 0) {
|
|
29
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
30
|
+
w.write(
|
|
31
|
+
`Usage:
|
|
32
|
+
crosstalk transport init [--containername <name>] [--transport git|local] [--path <dir>] [--remote <url>]
|
|
33
|
+
crosstalk transport rm [--containername <name>] [--force]
|
|
34
|
+
crosstalk transport --help
|
|
35
|
+
|
|
36
|
+
init — scaffold a new transport (or repair an incomplete one).
|
|
37
|
+
rm — stop the engine and delete the entire <base>/<name>/ tree.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--containername <name> Transport name (default: 'crosstalk'). Must be
|
|
41
|
+
lowercase alphanumeric + ._- (no leading . or -).
|
|
42
|
+
-c <name> Shorthand for --containername.
|
|
43
|
+
--remote <url> (init only) set git upstream. Errors if a
|
|
44
|
+
different remote is already set.
|
|
45
|
+
--force, -f (rm only) skip the confirmation prompt.
|
|
46
|
+
|
|
47
|
+
Default is system mode (/var/lib/crosstalk/, runs as the 'crosstalk'
|
|
48
|
+
service user via the bundled systemd unit). CROSSTALK_USER_MODE=1
|
|
49
|
+
switches to user mode (paths under the operator's home).
|
|
50
|
+
`,
|
|
51
|
+
);
|
|
52
|
+
process.exit(exit);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function run(argv) {
|
|
56
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
57
|
+
const sub = argv[0];
|
|
58
|
+
if (!sub) usage(1);
|
|
59
|
+
const rest = argv.slice(1);
|
|
60
|
+
|
|
61
|
+
if (sub === 'init') return await runInit(rest);
|
|
62
|
+
if (sub === 'rm') return await runRm(rest);
|
|
63
|
+
|
|
64
|
+
process.stderr.write(`crosstalk transport: unknown subcommand '${sub}'.\n`);
|
|
65
|
+
usage(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runInit(argv) {
|
|
69
|
+
let name;
|
|
70
|
+
try {
|
|
71
|
+
name = parseContainerName(argv);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
process.stderr.write(`crosstalk transport init: ${err.message}\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const remote = flag(argv, '--remote');
|
|
78
|
+
const transportKind = flag(argv, '--transport') === 'local' ? 'local' : 'git';
|
|
79
|
+
if (transportKind === 'local' && remote) {
|
|
80
|
+
process.stderr.write(`crosstalk transport init: --remote is ignored for --transport local (no git, no upstream).\n`);
|
|
81
|
+
}
|
|
82
|
+
const paths = storagePaths(name);
|
|
83
|
+
// --path <dir>: scaffold the transport at an arbitrary directory (e.g. a
|
|
84
|
+
// mounted share) instead of the managed storage layout. Port + state stay
|
|
85
|
+
// machine-local under the name-keyed storage root; only the transport
|
|
86
|
+
// (template + channels) lives at the path. Pairs with `server start --path`.
|
|
87
|
+
const customPath = flag(argv, '--path');
|
|
88
|
+
const transportDir = customPath ? resolve(customPath) : paths.transportDir;
|
|
89
|
+
const report = [];
|
|
90
|
+
|
|
91
|
+
// ── Phase 1: Storage layout ────────────────────────────────────────
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(transportDir, { recursive: true });
|
|
94
|
+
mkdirSync(paths.crosstalkRoot, { recursive: true });
|
|
95
|
+
mkdirSync(paths.crosstalkState, { recursive: true });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`crosstalk transport init: failed to create storage at ${paths.storageRoot} — ${err.message}\n`,
|
|
99
|
+
);
|
|
100
|
+
if (paths.mode === 'system') {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
` System-mode storage requires write access to ${paths.base}.\n` +
|
|
103
|
+
` Try: sudo mkdir -p ${paths.storageRoot} && sudo chown -R $USER ${paths.storageRoot}\n`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let port;
|
|
110
|
+
if (existsSync(paths.portFile)) {
|
|
111
|
+
const v = Number(readFileSync(paths.portFile, 'utf-8').trim());
|
|
112
|
+
if (Number.isInteger(v) && v > 0 && v < 65536) {
|
|
113
|
+
port = v;
|
|
114
|
+
report.push(` api port: ${port} (existing)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!port) {
|
|
118
|
+
try {
|
|
119
|
+
port = pickFreePort(name);
|
|
120
|
+
writeFileSync(paths.portFile, `${port}\n`);
|
|
121
|
+
report.push(` api port: ${port} (allocated)`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
process.stderr.write(`crosstalk transport init: port allocation failed — ${err.message}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Phase 2: Transport scaffold ────────────────────────────────────
|
|
129
|
+
const crosstalkVersionFile = join(transportDir, 'CROSSTALK-VERSION');
|
|
130
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
131
|
+
// Scaffold via `crosstalk daemon init <path>` — the same self-bin
|
|
132
|
+
// call pattern startEngine() uses.
|
|
133
|
+
const selfBin = resolveSelfBin();
|
|
134
|
+
if (!existsSync(selfBin)) {
|
|
135
|
+
process.stderr.write(
|
|
136
|
+
`crosstalk transport init: CLI entry missing at ${selfBin} — package install looks broken.\n`,
|
|
137
|
+
);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
const initRun = spawnSync(
|
|
141
|
+
process.execPath,
|
|
142
|
+
[selfBin, 'daemon', 'init', transportDir],
|
|
143
|
+
{ stdio: 'pipe' },
|
|
144
|
+
);
|
|
145
|
+
if (initRun.status !== 0) {
|
|
146
|
+
const stderr = initRun.stderr?.toString() ?? '';
|
|
147
|
+
const stdout = initRun.stdout?.toString() ?? '';
|
|
148
|
+
process.stderr.write(`crosstalk transport init: 'daemon init' exited ${initRun.status}\n`);
|
|
149
|
+
if (stderr.trim()) process.stderr.write(` stderr: ${stderr.trim()}\n`);
|
|
150
|
+
if (stdout.trim()) process.stderr.write(` stdout: ${stdout.trim()}\n`);
|
|
151
|
+
return initRun.status ?? 1;
|
|
152
|
+
}
|
|
153
|
+
report.push(` template: scaffolded`);
|
|
154
|
+
} else {
|
|
155
|
+
report.push(` template: present (skipped scaffold)`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Phase 2.5: local transport marker ──────────────────────────────
|
|
159
|
+
// Local (fs) transports carry no git history; persist the medium in the
|
|
160
|
+
// config so the dispatcher self-selects it (no --transport flag needed).
|
|
161
|
+
if (transportKind === 'local') {
|
|
162
|
+
const yamlPath = join(transportDir, 'data', 'crosstalk.yaml');
|
|
163
|
+
try {
|
|
164
|
+
const cur = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : '';
|
|
165
|
+
if (!/^\s*transport\s*:/m.test(cur)) {
|
|
166
|
+
writeFileSync(yamlPath, `transport: local\n${cur}`);
|
|
167
|
+
}
|
|
168
|
+
report.push(` medium: local (fs — no git)`);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
process.stderr.write(`crosstalk transport init: failed to write transport key to ${yamlPath} — ${err.message}\n`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Phase 3: Git init + initial commit ─────────────────────────────
|
|
176
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`crosstalk transport init: transport template missing at ${transportDir}\n` +
|
|
179
|
+
` Engine init didn't write template files. Try 'crosstalk transport rm' then re-init.\n`,
|
|
180
|
+
);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (transportKind === 'git') {
|
|
185
|
+
const gitSteps = [
|
|
186
|
+
['init', '--quiet', '--initial-branch=main'],
|
|
187
|
+
['add', '-A'],
|
|
188
|
+
['commit', '--quiet', '-m', 'initial transport'],
|
|
189
|
+
];
|
|
190
|
+
let gitOk = true;
|
|
191
|
+
for (const args of gitSteps) {
|
|
192
|
+
const gr = spawnSync('git', args, { cwd: transportDir, stdio: 'pipe' });
|
|
193
|
+
if (gr.status !== 0) {
|
|
194
|
+
gitOk = false;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
report.push(gitOk ? ` git: initialized + committed` : ` git: already present (skipped)`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Phase 4: Remote setup (git only) ───────────────────────────────
|
|
202
|
+
if (remote && transportKind === 'git') {
|
|
203
|
+
const cur = spawnSync('git', ['remote', 'get-url', 'origin'], {
|
|
204
|
+
cwd: transportDir,
|
|
205
|
+
stdio: 'pipe',
|
|
206
|
+
});
|
|
207
|
+
const currentUrl = cur.status === 0 ? cur.stdout.toString().trim() : null;
|
|
208
|
+
if (!currentUrl) {
|
|
209
|
+
const rr = spawnSync('git', ['remote', 'add', 'origin', remote], {
|
|
210
|
+
cwd: transportDir,
|
|
211
|
+
stdio: 'pipe',
|
|
212
|
+
});
|
|
213
|
+
if (rr.status !== 0) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
`crosstalk transport init: 'git remote add origin ${remote}' failed — ${rr.stderr?.toString().trim()}\n`,
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
report.push(` remote: ${remote} (added)`);
|
|
219
|
+
}
|
|
220
|
+
} else if (currentUrl === remote) {
|
|
221
|
+
report.push(` remote: ${remote} (already set)`);
|
|
222
|
+
} else {
|
|
223
|
+
process.stderr.write(
|
|
224
|
+
`crosstalk transport init: remote already set to '${currentUrl}'; refusing to overwrite with '${remote}'.\n`,
|
|
225
|
+
);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Phase 5: Summary ───────────────────────────────────────────────
|
|
231
|
+
process.stdout.write(
|
|
232
|
+
`\nTransport '${name}' ready:\n` +
|
|
233
|
+
` storage: ${paths.storageRoot}\n` +
|
|
234
|
+
` transport: ${transportDir}\n` +
|
|
235
|
+
report.join('\n') + '\n' +
|
|
236
|
+
`\nNext: crosstalk server start${customPath ? ` --path ${transportDir}` : (name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`)}\n`,
|
|
237
|
+
);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function confirm(promptText) {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
244
|
+
rl.question(promptText, (ans) => {
|
|
245
|
+
rl.close();
|
|
246
|
+
const yes = /^y(es)?$/i.test(ans.trim());
|
|
247
|
+
resolve(yes);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runRm(argv) {
|
|
253
|
+
let name;
|
|
254
|
+
try {
|
|
255
|
+
name = parseContainerName(argv);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
process.stderr.write(`crosstalk transport rm: ${err.message}\n`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const paths = storagePaths(name);
|
|
262
|
+
const haveStorage = isInitialized(name) || existsSync(paths.storageRoot);
|
|
263
|
+
|
|
264
|
+
if (!haveStorage) {
|
|
265
|
+
process.stderr.write(`crosstalk transport rm: transport '${name}' not found.\n`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!has(argv, '--force') && !has(argv, '-f')) {
|
|
270
|
+
process.stderr.write(
|
|
271
|
+
`Remove '${name}'?\n` +
|
|
272
|
+
` This deletes:\n` +
|
|
273
|
+
` - transport git repo at ${paths.transportDir}\n` +
|
|
274
|
+
` (all message history, channels, replies)\n` +
|
|
275
|
+
` - installed agent CLIs + auth tokens (claude, codex, ...)\n` +
|
|
276
|
+
` - dispatcher state (cursor, heartbeat, errors.log)\n` +
|
|
277
|
+
` Cannot undo.\n`,
|
|
278
|
+
);
|
|
279
|
+
const ok = await confirm('[y/N]: ');
|
|
280
|
+
if (!ok) {
|
|
281
|
+
process.stderr.write('crosstalk transport rm: cancelled.\n');
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
stopEngine(name);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
rmSync(paths.storageRoot, { recursive: true, force: true });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
process.stderr.write(
|
|
292
|
+
`crosstalk transport rm: failed to remove ${paths.storageRoot} — ${err.message}\n`,
|
|
293
|
+
);
|
|
294
|
+
return 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
process.stdout.write(`Removed '${name}'.\n`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
package/commands/version.js
CHANGED
|
@@ -20,13 +20,13 @@ export async function run(argv) {
|
|
|
20
20
|
// Skew detection happens passively in api-client.js via the
|
|
21
21
|
// X-Crosstalk-Engine-Version response header — no duplication here.
|
|
22
22
|
const v = await api.get('/version');
|
|
23
|
-
process.stdout.write(`
|
|
23
|
+
process.stdout.write(`crosstalk engine: ${v.version} (alias: ${v.alias})\n`);
|
|
24
24
|
} catch (err) {
|
|
25
25
|
if (err instanceof ConnectError) {
|
|
26
|
-
process.stdout.write(`
|
|
26
|
+
process.stdout.write(`crosstalk engine: (unreachable on 127.0.0.1:${err.port})\n`);
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
|
-
process.stdout.write(`
|
|
29
|
+
process.stdout.write(`crosstalk engine: (error: ${err.message})\n`);
|
|
30
30
|
return 0;
|
|
31
31
|
}
|
|
32
32
|
return 0;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// crosstalk workflow <compose|run|status> — multi-step workflow ops.
|
|
2
|
+
//
|
|
3
|
+
// compose <prompt> Interactive: engine compiles the prompt into a
|
|
4
|
+
// workflow plan, prints the YAML for review, then
|
|
5
|
+
// submits it. Operator can edit before submit.
|
|
6
|
+
// run <file> Submit an already-authored workflow YAML file
|
|
7
|
+
// (replaces v7's `crosstalk run --type workflow`).
|
|
8
|
+
// status [--id <id>] Snapshot in-flight workflows (parent channel,
|
|
9
|
+
// phase, fanout state, completion). Without --id
|
|
10
|
+
// lists every workflow on the transport.
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdtempSync, unlinkSync } from 'fs';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { apiFor } from '../lib/api-client.js';
|
|
17
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
18
|
+
import { flag, has, positionals } from '../lib/argv.js';
|
|
19
|
+
|
|
20
|
+
function usage(exit = 0) {
|
|
21
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
22
|
+
w.write(
|
|
23
|
+
`Usage:
|
|
24
|
+
crosstalk workflow compose <prompt> [--channel <name|uuid>] [--compiler <model>]
|
|
25
|
+
crosstalk workflow run <file> [--channel <name|uuid>] [--from <name>]
|
|
26
|
+
crosstalk workflow status [--id <childUuid>]
|
|
27
|
+
`,
|
|
28
|
+
);
|
|
29
|
+
process.exit(exit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function run(argv) {
|
|
33
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
34
|
+
const sub = argv[0];
|
|
35
|
+
if (!sub) usage(1);
|
|
36
|
+
const rest = argv.slice(1);
|
|
37
|
+
|
|
38
|
+
if (sub === 'compose') return await runCompose(rest);
|
|
39
|
+
if (sub === 'run') return await runRun(rest);
|
|
40
|
+
if (sub === 'status') return await runStatus(rest);
|
|
41
|
+
|
|
42
|
+
process.stderr.write(`crosstalk workflow: unknown subcommand '${sub}'.\n`);
|
|
43
|
+
usage(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function promptLine(label) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
process.stdout.write(label);
|
|
49
|
+
const stdin = process.stdin;
|
|
50
|
+
stdin.setEncoding('utf-8');
|
|
51
|
+
stdin.resume();
|
|
52
|
+
let buf = '';
|
|
53
|
+
const onData = (chunk) => {
|
|
54
|
+
buf += chunk;
|
|
55
|
+
const nl = buf.indexOf('\n');
|
|
56
|
+
if (nl !== -1) {
|
|
57
|
+
stdin.removeListener('data', onData);
|
|
58
|
+
stdin.pause();
|
|
59
|
+
resolve(buf.slice(0, nl).replace(/\r$/, ''));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
stdin.on('data', onData);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function editInEditor(initial) {
|
|
67
|
+
// Resolve editor: $VISUAL → $EDITOR → vi (POSIX default). Returns the
|
|
68
|
+
// edited contents, or null if the operator quit without saving.
|
|
69
|
+
const editor = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
70
|
+
const tmp = join(mkdtempSync(join(tmpdir(), 'crosstalk-wf-')), 'workflow.yaml');
|
|
71
|
+
writeFileSync(tmp, initial);
|
|
72
|
+
const r = spawnSync(editor, [tmp], { stdio: 'inherit' });
|
|
73
|
+
if (r.status !== 0) {
|
|
74
|
+
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
let edited;
|
|
78
|
+
try { edited = readFileSync(tmp, 'utf-8'); } catch { edited = null; }
|
|
79
|
+
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
80
|
+
return edited;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runCompose(argv) {
|
|
84
|
+
const pos = positionals(argv, [
|
|
85
|
+
'--channel', '--compiler', '--containername', '-c',
|
|
86
|
+
'--profile', '--server',
|
|
87
|
+
]);
|
|
88
|
+
const prompt = pos.join(' ').trim();
|
|
89
|
+
if (!prompt) {
|
|
90
|
+
process.stderr.write('crosstalk workflow compose: <prompt> required.\n');
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const api = apiFor(argv);
|
|
95
|
+
const compilerAgent = flag(argv, '--compiler');
|
|
96
|
+
const channel = flag(argv, '--channel');
|
|
97
|
+
|
|
98
|
+
process.stderr.write('compiling workflow plan...\n');
|
|
99
|
+
let composed;
|
|
100
|
+
try {
|
|
101
|
+
composed = await api.post('/api/workflows/compose', {
|
|
102
|
+
prompt,
|
|
103
|
+
...(compilerAgent ? { compilerAgent } : {}),
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
reportAndExit(err, 'crosstalk workflow compose');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let yaml = composed.yaml;
|
|
110
|
+
process.stdout.write(`\n--- compiled plan (via ${composed.compiler}) ---\n${yaml}\n--- end plan ---\n\n`);
|
|
111
|
+
|
|
112
|
+
while (true) {
|
|
113
|
+
const choice = (await promptLine('Submit? [y]es / [e]dit / [n]o: ')).trim().toLowerCase() || 'n';
|
|
114
|
+
if (choice === 'n' || choice === 'no') {
|
|
115
|
+
process.stdout.write('aborted.\n');
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (choice === 'e' || choice === 'edit') {
|
|
119
|
+
const edited = editInEditor(yaml);
|
|
120
|
+
if (edited == null) {
|
|
121
|
+
process.stderr.write('editor exited non-zero; keeping original yaml.\n');
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
yaml = edited;
|
|
125
|
+
process.stdout.write(`\n--- edited plan ---\n${yaml}\n--- end plan ---\n\n`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (choice === 'y' || choice === 'yes') break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let submitted;
|
|
132
|
+
try {
|
|
133
|
+
submitted = await api.post('/api/workflows/submit', {
|
|
134
|
+
yaml,
|
|
135
|
+
...(channel ? { channel } : {}),
|
|
136
|
+
prompt,
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
reportAndExit(err, 'crosstalk workflow compose (submit)');
|
|
140
|
+
}
|
|
141
|
+
if (submitted.relPath) {
|
|
142
|
+
process.stdout.write(`Workflow dispatched: ${submitted.relPath}\n`);
|
|
143
|
+
}
|
|
144
|
+
if (submitted.childChannel) {
|
|
145
|
+
process.stdout.write(`Child channel: ${submitted.childChannel.slice(0, 8)} (parent: ${(submitted.channel || '').slice(0, 8)})\n`);
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function runRun(argv) {
|
|
151
|
+
const pos = positionals(argv, [
|
|
152
|
+
'--channel', '--from', '--containername', '-c',
|
|
153
|
+
'--profile', '--server',
|
|
154
|
+
]);
|
|
155
|
+
const file = pos[0];
|
|
156
|
+
if (!file) {
|
|
157
|
+
process.stderr.write('crosstalk workflow run: <file> required.\n');
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
if (!existsSync(file)) {
|
|
161
|
+
process.stderr.write(`crosstalk workflow run: file not found — ${file}\n`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
const body = readFileSync(file, 'utf-8');
|
|
165
|
+
if (body.length === 0) {
|
|
166
|
+
process.stderr.write('crosstalk workflow run: file is empty.\n');
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const api = apiFor(argv);
|
|
171
|
+
const payload = {
|
|
172
|
+
type: 'workflow',
|
|
173
|
+
channel: flag(argv, '--channel'),
|
|
174
|
+
from: flag(argv, '--from') ?? process.env.USER ?? 'operator',
|
|
175
|
+
body,
|
|
176
|
+
};
|
|
177
|
+
let resp;
|
|
178
|
+
try {
|
|
179
|
+
resp = await api.post('/messages', payload);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
reportAndExit(err, 'crosstalk workflow run');
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write(`Workflow dispatched: ${resp.relPath}\n`);
|
|
184
|
+
process.stdout.write(`Child channel: ${(resp.childChannel || '').slice(0, 8)} (parent: ${(resp.channel || '').slice(0, 8)})\n`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function runStatus(argv) {
|
|
189
|
+
const api = apiFor(argv);
|
|
190
|
+
const id = flag(argv, '--id');
|
|
191
|
+
if (id) {
|
|
192
|
+
let r;
|
|
193
|
+
try { r = await api.get(`/api/workflows/${encodeURIComponent(id)}`); }
|
|
194
|
+
catch (err) { reportAndExit(err, 'crosstalk workflow status'); }
|
|
195
|
+
printDetail(r);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
let r;
|
|
199
|
+
try { r = await api.get('/api/workflows'); }
|
|
200
|
+
catch (err) { reportAndExit(err, 'crosstalk workflow status'); }
|
|
201
|
+
const list = r.workflows || [];
|
|
202
|
+
if (list.length === 0) {
|
|
203
|
+
process.stdout.write('(no workflows on this transport)\n');
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
for (const w of list) printSummaryLine(w);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printSummaryLine(w) {
|
|
211
|
+
const id = (w.childUuid || '').slice(0, 8);
|
|
212
|
+
const parent = (w.parentName || (w.parentUuid || '').slice(0, 8) || '-');
|
|
213
|
+
const phase = w.phase || (w.isComplete ? 'complete' : 'in-flight');
|
|
214
|
+
const fan = w.fanoutDispatched != null
|
|
215
|
+
? `${w.fanoutReplied}/${w.fanoutDispatched} replies`
|
|
216
|
+
: '';
|
|
217
|
+
process.stdout.write(` ${id} ${parent.padEnd(20)} ${phase.padEnd(12)} ${fan}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printDetail(d) {
|
|
221
|
+
if (!d) {
|
|
222
|
+
process.stdout.write('(no detail)\n');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const lines = [];
|
|
226
|
+
if (d.childUuid) lines.push(`uuid: ${d.childUuid}`);
|
|
227
|
+
if (d.parentUuid) lines.push(`parent: ${d.parentUuid}`);
|
|
228
|
+
if (d.markerRelPath) lines.push(`marker: ${d.markerRelPath}`);
|
|
229
|
+
if (d.phase) lines.push(`phase: ${d.phase}`);
|
|
230
|
+
if (d.fanoutDispatched != null) lines.push(`fanout: ${d.fanoutReplied}/${d.fanoutDispatched} replies`);
|
|
231
|
+
if (d.synthesizeStatus) lines.push(`synth: ${d.synthesizeStatus}`);
|
|
232
|
+
if (d.isComplete != null) lines.push(`complete: ${d.isComplete}`);
|
|
233
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
234
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# crosstalk@<transport>.service — systemd template for v8-native system mode.
|
|
2
|
+
#
|
|
3
|
+
# Install:
|
|
4
|
+
# sudo deploy/install.sh
|
|
5
|
+
# sudo systemctl daemon-reload
|
|
6
|
+
# sudo systemctl enable --now crosstalk@main
|
|
7
|
+
#
|
|
8
|
+
# Instances: one per named transport. The instance name (the `<name>` in
|
|
9
|
+
# `crosstalk@<name>`) becomes the storage subdirectory under /var/lib/
|
|
10
|
+
# crosstalk and the dispatcher alias the engine boots with.
|
|
11
|
+
#
|
|
12
|
+
# alpha.18 monorepo: the daemon is now an internal subcommand of the
|
|
13
|
+
# same `crosstalk` CLI — `crosstalk daemon dispatch …` replaces the
|
|
14
|
+
# dropped `crosstalkd` bin.
|
|
15
|
+
#
|
|
16
|
+
# Prereqs (handled by deploy/install.sh):
|
|
17
|
+
# - 'crosstalk' system user + group exist
|
|
18
|
+
# - /var/lib/crosstalk/<name>/ exists (crosstalk:crosstalk 0750)
|
|
19
|
+
# - /var/log/crosstalk/ exists (crosstalk:crosstalk 0750)
|
|
20
|
+
# - The transport for <name> has been initialized via
|
|
21
|
+
# `sudo -u crosstalk crosstalk transport init --containername <name>`
|
|
22
|
+
|
|
23
|
+
[Unit]
|
|
24
|
+
Description=crosstalk engine (transport: %i)
|
|
25
|
+
Documentation=https://github.com/cordfuse/crosstalk
|
|
26
|
+
After=network.target
|
|
27
|
+
Wants=network-online.target
|
|
28
|
+
|
|
29
|
+
[Service]
|
|
30
|
+
Type=simple
|
|
31
|
+
User=crosstalk
|
|
32
|
+
Group=crosstalk
|
|
33
|
+
Environment=CROSSTALK_USER_MODE=
|
|
34
|
+
Environment=DISPATCH_POLL_SECONDS=30
|
|
35
|
+
Environment=DISPATCH_JSON=true
|
|
36
|
+
WorkingDirectory=/var/lib/crosstalk/%i/transport
|
|
37
|
+
ExecStart=/usr/bin/crosstalk daemon dispatch --alias %i --poll ${DISPATCH_POLL_SECONDS} --json
|
|
38
|
+
Restart=on-failure
|
|
39
|
+
RestartSec=5
|
|
40
|
+
StandardOutput=append:/var/log/crosstalk/%i.log
|
|
41
|
+
StandardError=append:/var/log/crosstalk/%i.log
|
|
42
|
+
|
|
43
|
+
# Hardening — daemon needs nothing outside its own state + transport dirs.
|
|
44
|
+
# Mirrors @cordfuse/llmux v2's llmuxd.service profile.
|
|
45
|
+
NoNewPrivileges=true
|
|
46
|
+
ProtectSystem=strict
|
|
47
|
+
ProtectHome=true
|
|
48
|
+
ReadWritePaths=/var/lib/crosstalk /var/log/crosstalk
|
|
49
|
+
PrivateTmp=true
|
|
50
|
+
PrivateDevices=true
|
|
51
|
+
ProtectKernelTunables=true
|
|
52
|
+
ProtectKernelModules=true
|
|
53
|
+
ProtectControlGroups=true
|
|
54
|
+
RestrictSUIDSGID=true
|
|
55
|
+
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
56
|
+
SystemCallFilter=@system-service
|
|
57
|
+
SystemCallErrorNumber=EPERM
|
|
58
|
+
LimitNOFILE=65536
|
|
59
|
+
CapabilityBoundingSet=
|
|
60
|
+
|
|
61
|
+
[Install]
|
|
62
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# crosstalk v8-native system-mode install.
|
|
3
|
+
#
|
|
4
|
+
# Run as root. Sets up the `crosstalk` system user + group, /etc + /var
|
|
5
|
+
# directories, the systemd template unit, and points operators at the
|
|
6
|
+
# next step (initializing a transport + enabling its instance).
|
|
7
|
+
#
|
|
8
|
+
# Mirrors the @cordfuse/llmux v2 install pattern so Cordfuse family
|
|
9
|
+
# deployments use the same shape: npm-install the binary globally,
|
|
10
|
+
# run this script to set up the OS-level scaffolding, then enable
|
|
11
|
+
# the systemd unit instance per named transport.
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
# ─── prerequisite checks ─────────────────────────────────────────────────
|
|
16
|
+
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
|
|
17
|
+
echo "error: this installer must run as root" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
if ! command -v systemctl >/dev/null 2>&1; then
|
|
21
|
+
echo "error: systemd not detected. v8-native system mode currently requires systemd." >&2
|
|
22
|
+
echo " For non-systemd hosts, run user mode: CROSSTALK_USER_MODE=1 crosstalk server start" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
if ! command -v crosstalk >/dev/null 2>&1; then
|
|
26
|
+
echo "error: crosstalk binary not on PATH." >&2
|
|
27
|
+
echo " Install first: npm install -g @cordfuse/crosstalk" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# ─── service user + group ────────────────────────────────────────────────
|
|
32
|
+
if ! getent group crosstalk >/dev/null; then
|
|
33
|
+
groupadd --system crosstalk
|
|
34
|
+
echo "created group: crosstalk"
|
|
35
|
+
fi
|
|
36
|
+
if ! id crosstalk >/dev/null 2>&1; then
|
|
37
|
+
# --home-dir /var/lib/crosstalk: agent CLI auth tokens (~/.claude/,
|
|
38
|
+
# ~/.codex/, ~/.gemini/) live under the service user's home. Daemon
|
|
39
|
+
# never reaches into /home/* — same $HOME-principle as llmux v2.
|
|
40
|
+
useradd --system --gid crosstalk --home-dir /var/lib/crosstalk \
|
|
41
|
+
--shell /usr/sbin/nologin --comment "crosstalk engine daemon" crosstalk
|
|
42
|
+
echo "created user: crosstalk"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# ─── directories ─────────────────────────────────────────────────────────
|
|
46
|
+
install -d -o root -g crosstalk -m 0750 /etc/crosstalk
|
|
47
|
+
install -d -o crosstalk -g crosstalk -m 0750 /var/lib/crosstalk
|
|
48
|
+
install -d -o crosstalk -g crosstalk -m 0750 /var/log/crosstalk
|
|
49
|
+
# Per-transport state dirs (e.g., /var/lib/crosstalk/main/) are created on
|
|
50
|
+
# first `crosstalk transport init --containername <name>` run by the
|
|
51
|
+
# service user.
|
|
52
|
+
|
|
53
|
+
# ─── systemd unit ────────────────────────────────────────────────────────
|
|
54
|
+
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
55
|
+
install -o root -g root -m 0644 \
|
|
56
|
+
"$SOURCE_DIR/crosstalk@.service" \
|
|
57
|
+
/etc/systemd/system/crosstalk@.service
|
|
58
|
+
|
|
59
|
+
systemctl daemon-reload
|
|
60
|
+
echo "installed: /etc/systemd/system/crosstalk@.service"
|
|
61
|
+
|
|
62
|
+
# ─── next steps ──────────────────────────────────────────────────────────
|
|
63
|
+
cat <<'EOF'
|
|
64
|
+
|
|
65
|
+
crosstalk v8-native system-mode install complete.
|
|
66
|
+
|
|
67
|
+
Initialize a transport (system mode is the default now; no env var needed):
|
|
68
|
+
|
|
69
|
+
sudo -u crosstalk crosstalk transport init --containername main
|
|
70
|
+
|
|
71
|
+
Then enable + start the systemd instance for it:
|
|
72
|
+
|
|
73
|
+
sudo systemctl enable --now crosstalk@main
|
|
74
|
+
sudo systemctl status crosstalk@main
|
|
75
|
+
sudo journalctl -u crosstalk@main -f # or: tail -f /var/log/crosstalk/main.log
|
|
76
|
+
|
|
77
|
+
To run additional named transports, repeat with a different name:
|
|
78
|
+
|
|
79
|
+
sudo -u crosstalk crosstalk transport init --containername staging
|
|
80
|
+
sudo systemctl enable --now crosstalk@staging
|
|
81
|
+
|
|
82
|
+
EOF
|