@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.
Files changed (72) hide show
  1. package/GUIDE-CLI.md +322 -0
  2. package/GUIDE-PROMPTS.md +118 -0
  3. package/LICENSE +21 -0
  4. package/README.md +422 -61
  5. package/bin/crosstalk.js +88 -54
  6. package/commands/agent.js +69 -0
  7. package/commands/auth.js +273 -0
  8. package/commands/channel.js +54 -44
  9. package/commands/chat.js +107 -71
  10. package/commands/daemon.js +120 -0
  11. package/commands/logs.js +108 -19
  12. package/commands/message.js +125 -0
  13. package/commands/server.js +185 -0
  14. package/commands/settings.js +49 -0
  15. package/commands/status.js +37 -13
  16. package/commands/token.js +136 -0
  17. package/commands/transport.js +299 -0
  18. package/commands/version.js +3 -3
  19. package/commands/workflow.js +234 -0
  20. package/deploy/crosstalk@.service +62 -0
  21. package/deploy/install.sh +82 -0
  22. package/lib/api-client.js +77 -22
  23. package/lib/credentials.js +207 -0
  24. package/lib/nativeServer.js +182 -0
  25. package/lib/resolve.js +106 -37
  26. package/package.json +27 -4
  27. package/src/activation.ts +104 -0
  28. package/src/api.ts +1717 -0
  29. package/src/auth/enforce.ts +68 -0
  30. package/src/auth/handlers.ts +266 -0
  31. package/src/auth/middleware.ts +132 -0
  32. package/src/auth/setup.ts +263 -0
  33. package/src/auth/tokens.ts +285 -0
  34. package/src/auth/users.ts +267 -0
  35. package/src/dispatch.ts +530 -0
  36. package/src/dispatchers.ts +91 -0
  37. package/src/filenames.ts +28 -0
  38. package/src/frontmatter.ts +26 -0
  39. package/src/init.ts +116 -0
  40. package/src/invoke.ts +201 -0
  41. package/src/log-buffer.ts +67 -0
  42. package/src/models.ts +300 -0
  43. package/src/resolve.ts +100 -0
  44. package/src/state.ts +191 -0
  45. package/src/stop.ts +37 -0
  46. package/src/transport.ts +353 -0
  47. package/src/web/auth-pages.ts +160 -0
  48. package/src/web/channels.ts +395 -0
  49. package/src/web/chat-page.ts +636 -0
  50. package/src/web/chat-pty.ts +254 -0
  51. package/src/web/dashboard.ts +130 -0
  52. package/src/web/layout.ts +237 -0
  53. package/src/web/stubs.ts +510 -0
  54. package/src/web/workflows.ts +490 -0
  55. package/src/workflow.ts +470 -0
  56. package/template/CLAUDE.md +10 -0
  57. package/template/CROSSTALK-VERSION +1 -0
  58. package/template/CROSSTALK.md +262 -0
  59. package/template/PROTOCOL.md +70 -0
  60. package/template/README.md +64 -0
  61. package/template/auth/.gitkeep +0 -0
  62. package/template/auth/README.md +224 -0
  63. package/template/data/crosstalk.yaml +196 -0
  64. package/template/gitignore +4 -0
  65. package/commands/down.js +0 -40
  66. package/commands/init.js +0 -243
  67. package/commands/pull.js +0 -22
  68. package/commands/replies.js +0 -40
  69. package/commands/restart.js +0 -29
  70. package/commands/rm.js +0 -109
  71. package/commands/run.js +0 -115
  72. package/commands/up.js +0 -135
@@ -0,0 +1,185 @@
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 { join, resolve } from 'path';
24
+ import { has, flag } from '../lib/argv.js';
25
+ import {
26
+ requireInitialized, storageMode, parseContainerName,
27
+ DEFAULT_CONTAINER_NAME,
28
+ } from '../lib/resolve.js';
29
+ import { startEngine, stopEngine, engineStatus } from '../lib/nativeServer.js';
30
+
31
+ /**
32
+ * In system mode the engine is supposed to run as the `crosstalk`
33
+ * service user under systemd, not as the operator who invoked the CLI.
34
+ * Spawning a detached child from the operator's shell would be the
35
+ * wrong lifecycle. Redirect to systemctl instead — clear hint + exit 2.
36
+ */
37
+ function redirectToSystemctl(verb, name) {
38
+ const unit = `crosstalk@${name}`;
39
+ process.stderr.write(
40
+ `crosstalk server ${verb}: system mode is the default — the engine\n` +
41
+ `runs as the 'crosstalk' service user under systemd, not as a child\n` +
42
+ `of this CLI.\n\n` +
43
+ ` Use: sudo systemctl ${verb} ${unit}\n\n` +
44
+ `Or opt into user mode (spawn as yourself, no systemd):\n` +
45
+ ` CROSSTALK_USER_MODE=1 crosstalk server ${verb}${name === DEFAULT_CONTAINER_NAME ? '' : ' --containername ' + name}\n`,
46
+ );
47
+ return 2;
48
+ }
49
+
50
+ function nameSuffix(name) {
51
+ return name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`;
52
+ }
53
+
54
+ function usage(exit = 0) {
55
+ const w = exit === 0 ? process.stdout : process.stderr;
56
+ w.write(
57
+ `Usage: crosstalk server <subverb> [--containername <name>]
58
+
59
+ Subverbs:
60
+ start [--path <dir>] start the engine; --path runs it on a transport at an
61
+ arbitrary directory (e.g. a mounted share), state stays
62
+ machine-local (user mode only)
63
+ stop SIGTERM the running engine; SIGKILL after 5s grace
64
+ restart stop + start
65
+ status [--probe] report pid + uptime; --probe also pings /healthz
66
+ logs [-f|--follow] tail crosstalk-state/crosstalk.log
67
+
68
+ Storage mode defaults to 'system'; set CROSSTALK_USER_MODE=1 for user mode.
69
+ `);
70
+ process.exit(exit);
71
+ }
72
+
73
+ async function cmdStart(argv) {
74
+ // --path <dir>: run the engine on a transport at an arbitrary directory
75
+ // (e.g. a mounted SMB/NFS share) instead of the managed storage layout.
76
+ // State (cursor/heartbeat/pid/port) stays machine-local, keyed by name.
77
+ const transportPath = flag(argv, '--path');
78
+ if (transportPath) {
79
+ if (storageMode() === 'system') {
80
+ process.stderr.write(`crosstalk server start: --path is user-mode only (set CROSSTALK_USER_MODE=1).\n`);
81
+ return 1;
82
+ }
83
+ if (!existsSync(join(transportPath, 'CROSSTALK-VERSION'))) {
84
+ process.stderr.write(
85
+ `crosstalk server start: '${transportPath}' is not a crosstalk transport (no CROSSTALK-VERSION).\n` +
86
+ ` Scaffold it first: crosstalk daemon init ${transportPath}\n`,
87
+ );
88
+ return 1;
89
+ }
90
+ const name = parseContainerName(argv);
91
+ const r = startEngine(name, { transportPath });
92
+ if (!r.ok) {
93
+ process.stderr.write(`crosstalk server start: ${r.error}\n`);
94
+ return 1;
95
+ }
96
+ process.stdout.write(
97
+ `'${name}' started on ${resolve(transportPath)} (pid ${r.pid}, api 127.0.0.1:${r.port}).\n` +
98
+ ` state stays machine-local; logs: crosstalk server logs${nameSuffix(name)} -f\n`,
99
+ );
100
+ return 0;
101
+ }
102
+
103
+ const { name } = requireInitialized(argv);
104
+ if (storageMode() === 'system') return redirectToSystemctl('start', name);
105
+ const r = startEngine(name);
106
+ if (!r.ok) {
107
+ process.stderr.write(`crosstalk server start: ${r.error}\n`);
108
+ return 1;
109
+ }
110
+ process.stdout.write(
111
+ `'${name}' started (pid ${r.pid}, api 127.0.0.1:${r.port}).\n` +
112
+ ` logs: crosstalk server logs${nameSuffix(name)} -f\n`,
113
+ );
114
+ return 0;
115
+ }
116
+
117
+ function cmdStop(argv) {
118
+ const { name } = requireInitialized(argv);
119
+ if (storageMode() === 'system') return redirectToSystemctl('stop', name);
120
+ const r = stopEngine(name);
121
+ if (!r.wasRunning) {
122
+ process.stdout.write(`'${name}' was not running.\n`);
123
+ return 0;
124
+ }
125
+ if (r.killed) {
126
+ process.stdout.write(`'${name}' did not shut down within 5s — SIGKILL'd (pid ${r.pid}).\n`);
127
+ } else {
128
+ process.stdout.write(`'${name}' stopped (pid ${r.pid}).\n`);
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ async function cmdRestart(argv) {
134
+ const { name } = requireInitialized(argv);
135
+ if (storageMode() === 'system') return redirectToSystemctl('restart', name);
136
+ cmdStop(argv);
137
+ return cmdStart(argv);
138
+ }
139
+
140
+ async function cmdStatus(argv) {
141
+ const { name } = requireInitialized(argv);
142
+ const probe = has(argv, '--probe');
143
+ const s = await engineStatus(name, { probeApi: probe });
144
+ if (!s.running) {
145
+ process.stdout.write(`'${name}' is not running.\n Start it: crosstalk server start${nameSuffix(name)}\n`);
146
+ return 0;
147
+ }
148
+ const lines = [
149
+ `'${name}' is running.`,
150
+ ` pid : ${s.pid}`,
151
+ ` api : http://127.0.0.1:${s.port}`,
152
+ ];
153
+ if (s.uptimeSeconds !== undefined) lines.push(` uptime : ${s.uptimeSeconds}s`);
154
+ if (probe) lines.push(` /healthz : ${s.apiReachable ? 'ok' : 'unreachable'}`);
155
+ process.stdout.write(lines.join('\n') + '\n');
156
+ return 0;
157
+ }
158
+
159
+ function cmdLogs(argv) {
160
+ const { paths } = requireInitialized(argv);
161
+ if (!existsSync(paths.logFile)) {
162
+ process.stdout.write(`No log file yet at ${paths.logFile}.\n (engine may not have been started)\n`);
163
+ return 0;
164
+ }
165
+ const follow = has(argv, '-f') || has(argv, '--follow');
166
+ const args = follow ? ['-n', '200', '-f', paths.logFile] : ['-n', '200', paths.logFile];
167
+ const r = spawn('tail', args, { stdio: 'inherit' });
168
+ return new Promise((resolve) => r.on('exit', (code) => resolve(code ?? 0)));
169
+ }
170
+
171
+ export async function run(argv) {
172
+ if (argv.length === 0 || has(argv, '-h') || has(argv, '--help')) usage(0);
173
+ const sub = argv[0];
174
+ const rest = argv.slice(1);
175
+ switch (sub) {
176
+ case 'start': return cmdStart(rest);
177
+ case 'stop': return cmdStop(rest);
178
+ case 'restart': return cmdRestart(rest);
179
+ case 'status': return cmdStatus(rest);
180
+ case 'logs': return cmdLogs(rest);
181
+ default:
182
+ process.stderr.write(`crosstalk server: unknown subverb '${sub}'\n\n`);
183
+ usage(1);
184
+ }
185
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- // crosstalk status — mirror of crosstalkd status output, fetched via API.
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
- if (s.claimed_models.length === 0) {
29
- let installed = null;
30
- try {
31
- const r = await api.get('/agents/installed');
32
- installed = r.installed;
33
- } catch { /* ignore fall back to generic message */ }
34
- if (installed && installed.length > 0) {
35
- const verb = installed.length === 1 ? 'references it' : 'references them';
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
- ` (${installed.join(', ')} installed in the container but no data/crosstalk.yaml entry ${verb} — add one to claim)\n`,
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
- } else {
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
+ }