@cordfuse/crosstalk 7.0.0-alpha.9 → 7.0.0-beta.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 +402 -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 +153 -0
- package/commands/settings.js +49 -0
- package/commands/status.js +37 -13
- package/commands/token.js +136 -0
- package/commands/transport.js +270 -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 +173 -0
- package/lib/resolve.js +101 -34
- package/package.json +27 -4
- package/src/activation.ts +104 -0
- package/src/api.ts +1716 -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 +492 -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 +283 -0
- package/src/resolve.ts +100 -0
- package/src/state.ts +190 -0
- package/src/stop.ts +37 -0
- package/src/transport.ts +243 -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 +129 -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,153 @@
|
|
|
1
|
+
// crosstalk server — native engine lifecycle (v8-native).
|
|
2
|
+
//
|
|
3
|
+
// Replaces the v7 docker lifecycle (up/down/restart/pull/logs). The
|
|
4
|
+
// engine runs as a detached host process; state lives under
|
|
5
|
+
// `<base>/<name>/crosstalk-state/` (pid + log files).
|
|
6
|
+
//
|
|
7
|
+
// Subverbs:
|
|
8
|
+
// crosstalk server start [--containername N]
|
|
9
|
+
// crosstalk server stop [--containername N]
|
|
10
|
+
// crosstalk server restart [--containername N]
|
|
11
|
+
// crosstalk server status [--containername N] [--probe]
|
|
12
|
+
// crosstalk server logs [--containername N] [-f|--follow]
|
|
13
|
+
//
|
|
14
|
+
// All honor CROSSTALK_USER_MODE=1 (boolean opt-in). Default is system
|
|
15
|
+
// mode (paths under /var/lib/crosstalk/, runs as `crosstalk` service
|
|
16
|
+
// user under the systemd unit shipped in deploy/crosstalk@.service).
|
|
17
|
+
// CROSSTALK_USER_MODE=1 switches to user mode (paths under
|
|
18
|
+
// ~/.local/share/crosstalk/ on Linux) — intended for solo dev or
|
|
19
|
+
// operators who don't want a system service.
|
|
20
|
+
|
|
21
|
+
import { spawn } from 'child_process';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import { has } from '../lib/argv.js';
|
|
24
|
+
import {
|
|
25
|
+
requireInitialized, storageMode,
|
|
26
|
+
DEFAULT_CONTAINER_NAME,
|
|
27
|
+
} from '../lib/resolve.js';
|
|
28
|
+
import { startEngine, stopEngine, engineStatus } from '../lib/nativeServer.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* In system mode the engine is supposed to run as the `crosstalk`
|
|
32
|
+
* service user under systemd, not as the operator who invoked the CLI.
|
|
33
|
+
* Spawning a detached child from the operator's shell would be the
|
|
34
|
+
* wrong lifecycle. Redirect to systemctl instead — clear hint + exit 2.
|
|
35
|
+
*/
|
|
36
|
+
function redirectToSystemctl(verb, name) {
|
|
37
|
+
const unit = `crosstalk@${name}`;
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`crosstalk server ${verb}: system mode is the default — the engine\n` +
|
|
40
|
+
`runs as the 'crosstalk' service user under systemd, not as a child\n` +
|
|
41
|
+
`of this CLI.\n\n` +
|
|
42
|
+
` Use: sudo systemctl ${verb} ${unit}\n\n` +
|
|
43
|
+
`Or opt into user mode (spawn as yourself, no systemd):\n` +
|
|
44
|
+
` CROSSTALK_USER_MODE=1 crosstalk server ${verb}${name === DEFAULT_CONTAINER_NAME ? '' : ' --containername ' + name}\n`,
|
|
45
|
+
);
|
|
46
|
+
return 2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nameSuffix(name) {
|
|
50
|
+
return name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usage(exit = 0) {
|
|
54
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
55
|
+
w.write(
|
|
56
|
+
`Usage: crosstalk server <subverb> [--containername <name>]
|
|
57
|
+
|
|
58
|
+
Subverbs:
|
|
59
|
+
start start the engine as a detached host process
|
|
60
|
+
stop SIGTERM the running engine; SIGKILL after 5s grace
|
|
61
|
+
restart stop + start
|
|
62
|
+
status [--probe] report pid + uptime; --probe also pings /healthz
|
|
63
|
+
logs [-f|--follow] tail crosstalk-state/crosstalk.log
|
|
64
|
+
|
|
65
|
+
Storage mode defaults to 'system'; set CROSSTALK_USER_MODE=1 for user mode.
|
|
66
|
+
`);
|
|
67
|
+
process.exit(exit);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function cmdStart(argv) {
|
|
71
|
+
const { name } = requireInitialized(argv);
|
|
72
|
+
if (storageMode() === 'system') return redirectToSystemctl('start', name);
|
|
73
|
+
const r = startEngine(name);
|
|
74
|
+
if (!r.ok) {
|
|
75
|
+
process.stderr.write(`crosstalk server start: ${r.error}\n`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
`'${name}' started (pid ${r.pid}, api 127.0.0.1:${r.port}).\n` +
|
|
80
|
+
` logs: crosstalk server logs${nameSuffix(name)} -f\n`,
|
|
81
|
+
);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cmdStop(argv) {
|
|
86
|
+
const { name } = requireInitialized(argv);
|
|
87
|
+
if (storageMode() === 'system') return redirectToSystemctl('stop', name);
|
|
88
|
+
const r = stopEngine(name);
|
|
89
|
+
if (!r.wasRunning) {
|
|
90
|
+
process.stdout.write(`'${name}' was not running.\n`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (r.killed) {
|
|
94
|
+
process.stdout.write(`'${name}' did not shut down within 5s — SIGKILL'd (pid ${r.pid}).\n`);
|
|
95
|
+
} else {
|
|
96
|
+
process.stdout.write(`'${name}' stopped (pid ${r.pid}).\n`);
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function cmdRestart(argv) {
|
|
102
|
+
const { name } = requireInitialized(argv);
|
|
103
|
+
if (storageMode() === 'system') return redirectToSystemctl('restart', name);
|
|
104
|
+
cmdStop(argv);
|
|
105
|
+
return cmdStart(argv);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function cmdStatus(argv) {
|
|
109
|
+
const { name } = requireInitialized(argv);
|
|
110
|
+
const probe = has(argv, '--probe');
|
|
111
|
+
const s = await engineStatus(name, { probeApi: probe });
|
|
112
|
+
if (!s.running) {
|
|
113
|
+
process.stdout.write(`'${name}' is not running.\n Start it: crosstalk server start${nameSuffix(name)}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const lines = [
|
|
117
|
+
`'${name}' is running.`,
|
|
118
|
+
` pid : ${s.pid}`,
|
|
119
|
+
` api : http://127.0.0.1:${s.port}`,
|
|
120
|
+
];
|
|
121
|
+
if (s.uptimeSeconds !== undefined) lines.push(` uptime : ${s.uptimeSeconds}s`);
|
|
122
|
+
if (probe) lines.push(` /healthz : ${s.apiReachable ? 'ok' : 'unreachable'}`);
|
|
123
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function cmdLogs(argv) {
|
|
128
|
+
const { paths } = requireInitialized(argv);
|
|
129
|
+
if (!existsSync(paths.logFile)) {
|
|
130
|
+
process.stdout.write(`No log file yet at ${paths.logFile}.\n (engine may not have been started)\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const follow = has(argv, '-f') || has(argv, '--follow');
|
|
134
|
+
const args = follow ? ['-n', '200', '-f', paths.logFile] : ['-n', '200', paths.logFile];
|
|
135
|
+
const r = spawn('tail', args, { stdio: 'inherit' });
|
|
136
|
+
return new Promise((resolve) => r.on('exit', (code) => resolve(code ?? 0)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function run(argv) {
|
|
140
|
+
if (argv.length === 0 || has(argv, '-h') || has(argv, '--help')) usage(0);
|
|
141
|
+
const sub = argv[0];
|
|
142
|
+
const rest = argv.slice(1);
|
|
143
|
+
switch (sub) {
|
|
144
|
+
case 'start': return cmdStart(rest);
|
|
145
|
+
case 'stop': return cmdStop(rest);
|
|
146
|
+
case 'restart': return cmdRestart(rest);
|
|
147
|
+
case 'status': return cmdStatus(rest);
|
|
148
|
+
case 'logs': return cmdLogs(rest);
|
|
149
|
+
default:
|
|
150
|
+
process.stderr.write(`crosstalk server: unknown subverb '${sub}'\n\n`);
|
|
151
|
+
usage(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// crosstalk settings <show> — current engine settings snapshot.
|
|
2
|
+
|
|
3
|
+
import { apiFor } from '../lib/api-client.js';
|
|
4
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
5
|
+
import { has } from '../lib/argv.js';
|
|
6
|
+
|
|
7
|
+
function usage(exit = 0) {
|
|
8
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
9
|
+
w.write(
|
|
10
|
+
`Usage:
|
|
11
|
+
crosstalk settings show [--json]
|
|
12
|
+
crosstalk settings --help
|
|
13
|
+
`,
|
|
14
|
+
);
|
|
15
|
+
process.exit(exit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function run(argv) {
|
|
19
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
20
|
+
const sub = argv[0];
|
|
21
|
+
if (!sub) usage(1);
|
|
22
|
+
if (sub !== 'show') {
|
|
23
|
+
process.stderr.write(`crosstalk settings: unknown subcommand '${sub}'.\n`);
|
|
24
|
+
usage(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const api = apiFor(argv);
|
|
28
|
+
let r;
|
|
29
|
+
try {
|
|
30
|
+
r = await api.get('/api/settings');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
reportAndExit(err, 'crosstalk settings show');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (has(argv, '--json')) {
|
|
36
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`alias: ${r.alias}\n` +
|
|
42
|
+
`version: ${r.version}\n` +
|
|
43
|
+
`transportRoot: ${r.transportRoot}\n` +
|
|
44
|
+
`apiPort: ${r.apiPort}\n` +
|
|
45
|
+
`claimedModels: ${(r.claimedModels || []).join(', ') || '(none)'}\n` +
|
|
46
|
+
(r.pollSeconds != null ? `pollSeconds: ${r.pollSeconds}\n` : ''),
|
|
47
|
+
);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
package/commands/status.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// crosstalk status —
|
|
1
|
+
// crosstalk status — transport-level snapshot from the engine, fetched via API.
|
|
2
2
|
//
|
|
3
3
|
// alpha.6: also prints the host path of the transport git repo as an
|
|
4
4
|
// escape hatch for operators who want to poke around with raw git.
|
|
@@ -25,22 +25,46 @@ export async function run(argv) {
|
|
|
25
25
|
process.stdout.write('\n');
|
|
26
26
|
|
|
27
27
|
process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
|
29
|
+
|
|
30
|
+
// N1 (alpha.11): surface the install/claim diagnostic regardless of
|
|
31
|
+
// claimed_models count. Alpha.10 only rendered when claimed was zero,
|
|
32
|
+
// which missed the common mixed-state case — operator already had one
|
|
33
|
+
// agent running and installed a second one that's referenced in yaml
|
|
34
|
+
// but not yet claimed. Now the hint fires whenever there's a gap
|
|
35
|
+
// between installed CLIs and claimed CLIs.
|
|
36
|
+
let installed = null;
|
|
37
|
+
let yamlReferenced = null;
|
|
38
|
+
let claimedClis = null;
|
|
39
|
+
try {
|
|
40
|
+
const r = await api.get('/agents/installed');
|
|
41
|
+
installed = r.installed;
|
|
42
|
+
yamlReferenced = r.yaml_referenced;
|
|
43
|
+
claimedClis = r.claimed;
|
|
44
|
+
} catch { /* ignore — best-effort diagnostic */ }
|
|
45
|
+
if (installed) {
|
|
46
|
+
const claimedSet = new Set(claimedClis ?? []);
|
|
47
|
+
const referencedSet = new Set(yamlReferenced ?? []);
|
|
48
|
+
const installedReferencedUnclaimed = installed.filter(
|
|
49
|
+
(b) => referencedSet.has(b) && !claimedSet.has(b),
|
|
50
|
+
);
|
|
51
|
+
const installedNotReferenced = installed.filter((b) => !referencedSet.has(b));
|
|
52
|
+
if (installedReferencedUnclaimed.length > 0) {
|
|
53
|
+
const verb = installedReferencedUnclaimed.length === 1 ? 'is' : 'are';
|
|
36
54
|
process.stdout.write(
|
|
37
|
-
` (${
|
|
55
|
+
` (${installedReferencedUnclaimed.join(', ')} ${verb} installed and referenced in data/crosstalk.yaml ` +
|
|
56
|
+
`but not yet claimed — run 'crosstalk restart' to refresh the dispatcher's claim list)\n`,
|
|
38
57
|
);
|
|
39
|
-
}
|
|
58
|
+
}
|
|
59
|
+
if (installedNotReferenced.length > 0) {
|
|
60
|
+
const verb = installedNotReferenced.length === 1 ? 'references it' : 'references them';
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
` (${installedNotReferenced.join(', ')} installed in the container but no data/crosstalk.yaml entry ${verb} — add one to claim)\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (s.claimed_models.length === 0 && installed.length === 0) {
|
|
40
66
|
process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/crosstalk.yaml)\n');
|
|
41
67
|
}
|
|
42
|
-
} else {
|
|
43
|
-
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
|
44
68
|
}
|
|
45
69
|
process.stdout.write('\n');
|
|
46
70
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// crosstalk token <list|create|revoke|rename> — manage bearer tokens.
|
|
2
|
+
//
|
|
3
|
+
// All endpoints scoped to the currently-authenticated user (the engine
|
|
4
|
+
// enforces ownership; you cannot rename or revoke another user's
|
|
5
|
+
// tokens via the CLI). Admin-only operations on other users' tokens
|
|
6
|
+
// remain web-UI only.
|
|
7
|
+
|
|
8
|
+
import { apiFor } from '../lib/api-client.js';
|
|
9
|
+
import { positionals, has, flag } from '../lib/argv.js';
|
|
10
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
11
|
+
|
|
12
|
+
function usage(exit = 0) {
|
|
13
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
14
|
+
w.write(
|
|
15
|
+
`Usage:
|
|
16
|
+
crosstalk token list # list your tokens
|
|
17
|
+
crosstalk token create <name> # create a new token (prints secret once)
|
|
18
|
+
crosstalk token revoke <tokenId> # revoke a token by id
|
|
19
|
+
crosstalk token rename <tokenId> --name <label> # rename a token's label
|
|
20
|
+
crosstalk token --help
|
|
21
|
+
`,
|
|
22
|
+
);
|
|
23
|
+
process.exit(exit);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtRel(iso) {
|
|
27
|
+
if (!iso) return '—';
|
|
28
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
29
|
+
if (Number.isNaN(ms)) return iso;
|
|
30
|
+
if (ms < 0) return 'in the future';
|
|
31
|
+
if (ms < 5_000) return 'just now';
|
|
32
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago`;
|
|
33
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
|
|
34
|
+
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
35
|
+
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function run(argv) {
|
|
39
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
40
|
+
const pos = positionals(argv, ['--name', '--profile']);
|
|
41
|
+
const sub = pos[0];
|
|
42
|
+
if (!sub) usage(1);
|
|
43
|
+
|
|
44
|
+
const api = apiFor(argv);
|
|
45
|
+
|
|
46
|
+
if (sub === 'list') {
|
|
47
|
+
let r;
|
|
48
|
+
try {
|
|
49
|
+
r = await api.get('/api/tokens');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
reportAndExit(err, 'crosstalk token list');
|
|
52
|
+
}
|
|
53
|
+
const tokens = r.tokens ?? [];
|
|
54
|
+
if (tokens.length === 0) {
|
|
55
|
+
process.stdout.write('(no tokens)\n');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
for (const t of tokens) {
|
|
59
|
+
const created = fmtRel(t.createdAt);
|
|
60
|
+
const used = t.lastUsedAt ? `last ${fmtRel(t.lastUsedAt)}` : 'never used';
|
|
61
|
+
const expires = t.expiresAt ? ` exp:${t.expiresAt}` : '';
|
|
62
|
+
process.stdout.write(`${t.tokenId} ${(t.name || '').padEnd(24)} created ${created} ${used}${expires}\n`);
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (sub === 'create') {
|
|
68
|
+
const name = pos[1];
|
|
69
|
+
if (!name) {
|
|
70
|
+
process.stderr.write('crosstalk token create: <name> required.\n');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
let r;
|
|
74
|
+
try {
|
|
75
|
+
r = await api.post('/api/tokens', { name });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
reportAndExit(err, 'crosstalk token create');
|
|
78
|
+
}
|
|
79
|
+
if (!r || typeof r.plaintextSecret !== 'string') {
|
|
80
|
+
process.stderr.write('crosstalk token create: malformed response from engine.\n');
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(
|
|
84
|
+
`tokenId: ${r.tokenId}\n` +
|
|
85
|
+
`secret: ${r.plaintextSecret}\n` +
|
|
86
|
+
`\n` +
|
|
87
|
+
`Copy the secret now — it won't be shown again. To use it from\n` +
|
|
88
|
+
`another machine: send 'Authorization: Bearer <secret>' on each request.\n`,
|
|
89
|
+
);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (sub === 'revoke') {
|
|
94
|
+
const tokenId = pos[1];
|
|
95
|
+
if (!tokenId) {
|
|
96
|
+
process.stderr.write('crosstalk token revoke: <tokenId> required. Run `crosstalk token list` to see ids.\n');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await api.delete(`/api/tokens/${encodeURIComponent(tokenId)}`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
reportAndExit(err, 'crosstalk token revoke');
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(`revoked ${tokenId}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sub === 'rename') {
|
|
109
|
+
const tokenId = pos[1];
|
|
110
|
+
if (!tokenId) {
|
|
111
|
+
process.stderr.write('crosstalk token rename: <tokenId> required. Run `crosstalk token list` to see ids.\n');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const newName = flag(argv, '--name');
|
|
115
|
+
if (!newName) {
|
|
116
|
+
process.stderr.write('crosstalk token rename: --name <label> required.\n');
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
let r;
|
|
120
|
+
try {
|
|
121
|
+
r = await api.patch(`/api/tokens/${encodeURIComponent(tokenId)}`, { name: newName });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reportAndExit(err, 'crosstalk token rename');
|
|
124
|
+
}
|
|
125
|
+
const updated = r && r.token;
|
|
126
|
+
if (updated) {
|
|
127
|
+
process.stdout.write(`renamed ${tokenId} → ${updated.name}\n`);
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`renamed ${tokenId}\n`);
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.stderr.write(`crosstalk token: unknown subcommand '${sub}'.\n`);
|
|
135
|
+
usage(1);
|
|
136
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
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 } 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>] [--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 paths = storagePaths(name);
|
|
79
|
+
const report = [];
|
|
80
|
+
|
|
81
|
+
// ── Phase 1: Storage layout ────────────────────────────────────────
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(paths.transportDir, { recursive: true });
|
|
84
|
+
mkdirSync(paths.crosstalkRoot, { recursive: true });
|
|
85
|
+
mkdirSync(paths.crosstalkState, { recursive: true });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`crosstalk transport init: failed to create storage at ${paths.storageRoot} — ${err.message}\n`,
|
|
89
|
+
);
|
|
90
|
+
if (paths.mode === 'system') {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
` System-mode storage requires write access to ${paths.base}.\n` +
|
|
93
|
+
` Try: sudo mkdir -p ${paths.storageRoot} && sudo chown -R $USER ${paths.storageRoot}\n`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let port;
|
|
100
|
+
if (existsSync(paths.portFile)) {
|
|
101
|
+
const v = Number(readFileSync(paths.portFile, 'utf-8').trim());
|
|
102
|
+
if (Number.isInteger(v) && v > 0 && v < 65536) {
|
|
103
|
+
port = v;
|
|
104
|
+
report.push(` api port: ${port} (existing)`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!port) {
|
|
108
|
+
try {
|
|
109
|
+
port = pickFreePort(name);
|
|
110
|
+
writeFileSync(paths.portFile, `${port}\n`);
|
|
111
|
+
report.push(` api port: ${port} (allocated)`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
process.stderr.write(`crosstalk transport init: port allocation failed — ${err.message}\n`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Phase 2: Transport scaffold ────────────────────────────────────
|
|
119
|
+
const crosstalkVersionFile = join(paths.transportDir, 'CROSSTALK-VERSION');
|
|
120
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
121
|
+
// Scaffold via `crosstalk daemon init <path>` — the same self-bin
|
|
122
|
+
// call pattern startEngine() uses.
|
|
123
|
+
const selfBin = resolveSelfBin();
|
|
124
|
+
if (!existsSync(selfBin)) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`crosstalk transport init: CLI entry missing at ${selfBin} — package install looks broken.\n`,
|
|
127
|
+
);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
const initRun = spawnSync(
|
|
131
|
+
process.execPath,
|
|
132
|
+
[selfBin, 'daemon', 'init', paths.transportDir],
|
|
133
|
+
{ stdio: 'pipe' },
|
|
134
|
+
);
|
|
135
|
+
if (initRun.status !== 0) {
|
|
136
|
+
const stderr = initRun.stderr?.toString() ?? '';
|
|
137
|
+
const stdout = initRun.stdout?.toString() ?? '';
|
|
138
|
+
process.stderr.write(`crosstalk transport init: 'daemon init' exited ${initRun.status}\n`);
|
|
139
|
+
if (stderr.trim()) process.stderr.write(` stderr: ${stderr.trim()}\n`);
|
|
140
|
+
if (stdout.trim()) process.stderr.write(` stdout: ${stdout.trim()}\n`);
|
|
141
|
+
return initRun.status ?? 1;
|
|
142
|
+
}
|
|
143
|
+
report.push(` template: scaffolded`);
|
|
144
|
+
} else {
|
|
145
|
+
report.push(` template: present (skipped scaffold)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Phase 3: Git init + initial commit ─────────────────────────────
|
|
149
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`crosstalk transport init: transport template missing at ${paths.transportDir}\n` +
|
|
152
|
+
` Engine init didn't write template files. Try 'crosstalk transport rm' then re-init.\n`,
|
|
153
|
+
);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const gitSteps = [
|
|
158
|
+
['init', '--quiet', '--initial-branch=main'],
|
|
159
|
+
['add', '-A'],
|
|
160
|
+
['commit', '--quiet', '-m', 'initial transport'],
|
|
161
|
+
];
|
|
162
|
+
let gitOk = true;
|
|
163
|
+
for (const args of gitSteps) {
|
|
164
|
+
const gr = spawnSync('git', args, { cwd: paths.transportDir, stdio: 'pipe' });
|
|
165
|
+
if (gr.status !== 0) {
|
|
166
|
+
gitOk = false;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
report.push(gitOk ? ` git: initialized + committed` : ` git: already present (skipped)`);
|
|
171
|
+
|
|
172
|
+
// ── Phase 4: Remote setup ──────────────────────────────────────────
|
|
173
|
+
if (remote) {
|
|
174
|
+
const cur = spawnSync('git', ['remote', 'get-url', 'origin'], {
|
|
175
|
+
cwd: paths.transportDir,
|
|
176
|
+
stdio: 'pipe',
|
|
177
|
+
});
|
|
178
|
+
const currentUrl = cur.status === 0 ? cur.stdout.toString().trim() : null;
|
|
179
|
+
if (!currentUrl) {
|
|
180
|
+
const rr = spawnSync('git', ['remote', 'add', 'origin', remote], {
|
|
181
|
+
cwd: paths.transportDir,
|
|
182
|
+
stdio: 'pipe',
|
|
183
|
+
});
|
|
184
|
+
if (rr.status !== 0) {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`crosstalk transport init: 'git remote add origin ${remote}' failed — ${rr.stderr?.toString().trim()}\n`,
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
report.push(` remote: ${remote} (added)`);
|
|
190
|
+
}
|
|
191
|
+
} else if (currentUrl === remote) {
|
|
192
|
+
report.push(` remote: ${remote} (already set)`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stderr.write(
|
|
195
|
+
`crosstalk transport init: remote already set to '${currentUrl}'; refusing to overwrite with '${remote}'.\n`,
|
|
196
|
+
);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Phase 5: Summary ───────────────────────────────────────────────
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
`\nTransport '${name}' ready:\n` +
|
|
204
|
+
` storage: ${paths.storageRoot}\n` +
|
|
205
|
+
` transport: ${paths.transportDir}\n` +
|
|
206
|
+
report.join('\n') + '\n' +
|
|
207
|
+
`\nNext: crosstalk server start${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}\n`,
|
|
208
|
+
);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function confirm(promptText) {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
215
|
+
rl.question(promptText, (ans) => {
|
|
216
|
+
rl.close();
|
|
217
|
+
const yes = /^y(es)?$/i.test(ans.trim());
|
|
218
|
+
resolve(yes);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function runRm(argv) {
|
|
224
|
+
let name;
|
|
225
|
+
try {
|
|
226
|
+
name = parseContainerName(argv);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
process.stderr.write(`crosstalk transport rm: ${err.message}\n`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const paths = storagePaths(name);
|
|
233
|
+
const haveStorage = isInitialized(name) || existsSync(paths.storageRoot);
|
|
234
|
+
|
|
235
|
+
if (!haveStorage) {
|
|
236
|
+
process.stderr.write(`crosstalk transport rm: transport '${name}' not found.\n`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!has(argv, '--force') && !has(argv, '-f')) {
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
`Remove '${name}'?\n` +
|
|
243
|
+
` This deletes:\n` +
|
|
244
|
+
` - transport git repo at ${paths.transportDir}\n` +
|
|
245
|
+
` (all message history, channels, replies)\n` +
|
|
246
|
+
` - installed agent CLIs + auth tokens (claude, codex, ...)\n` +
|
|
247
|
+
` - dispatcher state (cursor, heartbeat, errors.log)\n` +
|
|
248
|
+
` Cannot undo.\n`,
|
|
249
|
+
);
|
|
250
|
+
const ok = await confirm('[y/N]: ');
|
|
251
|
+
if (!ok) {
|
|
252
|
+
process.stderr.write('crosstalk transport rm: cancelled.\n');
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
stopEngine(name);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
rmSync(paths.storageRoot, { recursive: true, force: true });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
process.stderr.write(
|
|
263
|
+
`crosstalk transport rm: failed to remove ${paths.storageRoot} — ${err.message}\n`,
|
|
264
|
+
);
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
process.stdout.write(`Removed '${name}'.\n`);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
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;
|