@cordfuse/crosstalk 7.0.0-alpha.16 → 7.0.0-alpha.18

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 (75) hide show
  1. package/GUIDE-CLI.md +322 -0
  2. package/GUIDE-PROMPTS.md +112 -0
  3. package/LICENSE +21 -0
  4. package/README.md +337 -62
  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 +121 -0
  11. package/commands/logs.js +108 -19
  12. package/commands/message.js +125 -0
  13. package/commands/server.js +153 -0
  14. package/commands/settings.js +49 -0
  15. package/commands/token.js +136 -0
  16. package/commands/transport.js +272 -0
  17. package/commands/version.js +3 -3
  18. package/commands/workflow.js +234 -0
  19. package/deploy/crosstalk@.service +62 -0
  20. package/deploy/install.sh +82 -0
  21. package/lib/api-client.js +77 -22
  22. package/lib/credentials.js +207 -0
  23. package/lib/nativeServer.js +174 -0
  24. package/lib/resolve.js +95 -43
  25. package/package.json +27 -4
  26. package/src/activation.ts +104 -0
  27. package/src/api.ts +1716 -0
  28. package/src/auth/enforce.ts +68 -0
  29. package/src/auth/handlers.ts +266 -0
  30. package/src/auth/middleware.ts +132 -0
  31. package/src/auth/setup.ts +263 -0
  32. package/src/auth/tokens.ts +285 -0
  33. package/src/auth/users.ts +267 -0
  34. package/src/channel.ts +202 -0
  35. package/src/dispatch.ts +492 -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 +283 -0
  43. package/src/replies.ts +73 -0
  44. package/src/resolve.ts +100 -0
  45. package/src/run.ts +250 -0
  46. package/src/state.ts +190 -0
  47. package/src/status.ts +84 -0
  48. package/src/stop.ts +37 -0
  49. package/src/transport.ts +243 -0
  50. package/src/web/auth-pages.ts +160 -0
  51. package/src/web/channels.ts +395 -0
  52. package/src/web/chat-page.ts +636 -0
  53. package/src/web/chat-pty.ts +238 -0
  54. package/src/web/dashboard.ts +129 -0
  55. package/src/web/layout.ts +237 -0
  56. package/src/web/stubs.ts +510 -0
  57. package/src/web/workflows.ts +490 -0
  58. package/src/workflow.ts +470 -0
  59. package/template/CLAUDE.md +10 -0
  60. package/template/CROSSTALK-VERSION +1 -0
  61. package/template/CROSSTALK.md +258 -0
  62. package/template/PROTOCOL.md +66 -0
  63. package/template/README.md +64 -0
  64. package/template/auth/.gitkeep +0 -0
  65. package/template/auth/README.md +224 -0
  66. package/template/data/crosstalk.yaml +196 -0
  67. package/template/gitignore +4 -0
  68. package/commands/down.js +0 -40
  69. package/commands/init.js +0 -241
  70. package/commands/pull.js +0 -22
  71. package/commands/replies.js +0 -40
  72. package/commands/restart.js +0 -29
  73. package/commands/rm.js +0 -109
  74. package/commands/run.js +0 -115
  75. package/commands/up.js +0 -133
@@ -0,0 +1,69 @@
1
+ // crosstalk agent <list> — installed CLI agents on the host.
2
+ //
3
+ // Mirrors llmux's `agent list`. Talks to /agents/installed on the
4
+ // engine, which is responsible for actually probing PATH; the CLI just
5
+ // presents the result.
6
+
7
+ import { apiFor } from '../lib/api-client.js';
8
+ import { reportAndExit } from '../lib/errors.js';
9
+ import { has } from '../lib/argv.js';
10
+
11
+ function usage(exit = 0) {
12
+ const w = exit === 0 ? process.stdout : process.stderr;
13
+ w.write(
14
+ `Usage:
15
+ crosstalk agent list [--installed] [--json]
16
+ crosstalk agent --help
17
+
18
+ --installed Only show CLIs found on PATH (default: show all known
19
+ agents, marking which are installed/claimed/yaml-known).
20
+ --json Print raw JSON instead of human-readable lines.
21
+ `,
22
+ );
23
+ process.exit(exit);
24
+ }
25
+
26
+ export async function run(argv) {
27
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
28
+ const sub = argv[0];
29
+ if (!sub) usage(1);
30
+
31
+ if (sub !== 'list') {
32
+ process.stderr.write(`crosstalk agent: unknown subcommand '${sub}'.\n`);
33
+ usage(1);
34
+ }
35
+
36
+ const api = apiFor(argv);
37
+ let r;
38
+ try {
39
+ r = await api.get('/agents/installed');
40
+ } catch (err) {
41
+ reportAndExit(err, 'crosstalk agent list');
42
+ }
43
+
44
+ if (has(argv, '--json')) {
45
+ process.stdout.write(JSON.stringify(r, null, 2) + '\n');
46
+ return 0;
47
+ }
48
+
49
+ const installed = new Set(r.installed || []);
50
+ const yamlRef = new Set(r.yaml_referenced || []);
51
+ const claimed = new Set(r.claimed || []);
52
+ const onlyInstalled = has(argv, '--installed');
53
+ const universe = onlyInstalled ? [...installed] : (r.known || []);
54
+
55
+ if (universe.length === 0) {
56
+ process.stdout.write('(no agents)\n');
57
+ return 0;
58
+ }
59
+
60
+ for (const name of universe) {
61
+ const flags = [
62
+ installed.has(name) ? 'installed' : '-',
63
+ yamlRef.has(name) ? 'yaml' : '-',
64
+ claimed.has(name) ? 'claimed' : '-',
65
+ ].join(' ');
66
+ process.stdout.write(` ${name.padEnd(12)} ${flags}\n`);
67
+ }
68
+ return 0;
69
+ }
@@ -0,0 +1,273 @@
1
+ // crosstalk auth <login|logout|whoami|list|use> — CLI-side authentication.
2
+ //
3
+ // Multi-profile (v8 alpha.18): a single CLI can hold credentials for
4
+ // multiple engines (local loopback, hosted prod, etc.) in
5
+ // ~/.config/crosstalk/credentials.json. The active profile drives
6
+ // outbound calls; `--profile <name>` overrides per-invocation.
7
+ //
8
+ // Verbs:
9
+ // login — POST /api/auth/login → bearer token; saves new profile
10
+ // logout — POST /api/auth/logout (best-effort) + delete profile locally
11
+ // whoami — GET /api/account on the active profile
12
+ // list — show all saved profiles + which is active
13
+ // use — mark a saved profile as active
14
+
15
+ import { apiFor, ApiError } from '../lib/api-client.js';
16
+ import {
17
+ saveProfile,
18
+ setActiveProfile,
19
+ deleteActiveProfile,
20
+ getActiveProfile,
21
+ listProfiles,
22
+ credentialsFilePath,
23
+ } from '../lib/credentials.js';
24
+ import { positionals, has, flag } from '../lib/argv.js';
25
+ import { reportAndExit } from '../lib/errors.js';
26
+
27
+ function usage(exit = 0) {
28
+ const w = exit === 0 ? process.stdout : process.stderr;
29
+ w.write(
30
+ `Usage:
31
+ crosstalk auth login [--server <url>] [--username <name>] [--passphrase <pp>] [--profile <name>]
32
+ crosstalk auth logout [--profile <name>] [--no-revoke]
33
+ crosstalk auth whoami [--profile <name>]
34
+ crosstalk auth list
35
+ crosstalk auth use <profile>
36
+ crosstalk auth --help
37
+
38
+ Interactive: --username and --passphrase are prompted when omitted.
39
+ Credentials stored at: ${credentialsFilePath()}
40
+ `,
41
+ );
42
+ process.exit(exit);
43
+ }
44
+
45
+ // Control bytes the raw-mode passphrase reader handles.
46
+ const CTRL_C = String.fromCharCode(3);
47
+ const BS = String.fromCharCode(127);
48
+ const BS_ALT = String.fromCharCode(8);
49
+
50
+ /** Read a single line from stdin with no echo (for passphrase entry). */
51
+ function promptHidden(label) {
52
+ return new Promise((resolve, reject) => {
53
+ if (!process.stdin.isTTY) {
54
+ let data = '';
55
+ process.stdin.setEncoding('utf-8');
56
+ process.stdin.on('data', (chunk) => { data += chunk; });
57
+ process.stdin.once('end', () => resolve(data.replace(/\r?\n$/, '')));
58
+ process.stdin.once('error', reject);
59
+ return;
60
+ }
61
+ process.stdout.write(label);
62
+ const stdin = process.stdin;
63
+ const wasRaw = stdin.isRaw;
64
+ stdin.setRawMode(true);
65
+ stdin.resume();
66
+ stdin.setEncoding('utf-8');
67
+ let buf = '';
68
+ const onData = (ch) => {
69
+ if (ch === CTRL_C) {
70
+ stdin.setRawMode(wasRaw);
71
+ stdin.pause();
72
+ stdin.removeListener('data', onData);
73
+ process.stdout.write('\n');
74
+ reject(new Error('aborted'));
75
+ return;
76
+ }
77
+ if (ch === '\r' || ch === '\n') {
78
+ stdin.setRawMode(wasRaw);
79
+ stdin.pause();
80
+ stdin.removeListener('data', onData);
81
+ process.stdout.write('\n');
82
+ resolve(buf);
83
+ return;
84
+ }
85
+ if (ch === BS || ch === BS_ALT) {
86
+ if (buf.length > 0) buf = buf.slice(0, -1);
87
+ return;
88
+ }
89
+ buf += ch;
90
+ };
91
+ stdin.on('data', onData);
92
+ });
93
+ }
94
+
95
+ function promptLine(label) {
96
+ return new Promise((resolve, reject) => {
97
+ process.stdout.write(label);
98
+ const stdin = process.stdin;
99
+ stdin.setEncoding('utf-8');
100
+ stdin.resume();
101
+ let buf = '';
102
+ const onData = (chunk) => {
103
+ buf += chunk;
104
+ const nl = buf.indexOf('\n');
105
+ if (nl !== -1) {
106
+ stdin.removeListener('data', onData);
107
+ stdin.pause();
108
+ resolve(buf.slice(0, nl).replace(/\r$/, ''));
109
+ }
110
+ };
111
+ stdin.on('data', onData);
112
+ stdin.once('error', reject);
113
+ });
114
+ }
115
+
116
+ export async function run(argv) {
117
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
118
+ const pos = positionals(argv, [
119
+ '--username', '--passphrase', '--server', '--profile',
120
+ ]);
121
+ const sub = pos[0];
122
+ if (!sub) usage(1);
123
+
124
+ if (sub === 'login') return await runLogin(argv);
125
+ if (sub === 'logout') return await runLogout(argv);
126
+ if (sub === 'whoami') return await runWhoami(argv);
127
+ if (sub === 'list') return runList();
128
+ if (sub === 'use') return runUse(pos[1]);
129
+
130
+ process.stderr.write(`crosstalk auth: unknown subcommand '${sub}'.\n`);
131
+ usage(1);
132
+ }
133
+
134
+ async function runLogin(argv) {
135
+ const server = flag(argv, '--server') || 'http://127.0.0.1:7000';
136
+ const profileName = flag(argv, '--profile') || 'default';
137
+ // serverOverride: the explicit --server (or default) wins over any
138
+ // existing active profile. Otherwise a re-login that targets a new
139
+ // engine would silently hit the old profile's URL.
140
+ const api = apiFor(argv, { serverOverride: server });
141
+ let username = flag(argv, '--username');
142
+ let passphrase = flag(argv, '--passphrase');
143
+ try {
144
+ if (!username) username = (await promptLine('username: ')).trim();
145
+ if (!passphrase) passphrase = await promptHidden('passphrase: ');
146
+ } catch (err) {
147
+ process.stderr.write(`crosstalk auth login: ${err.message}\n`);
148
+ return 1;
149
+ }
150
+ if (!username || !passphrase) {
151
+ process.stderr.write('crosstalk auth login: username + passphrase required.\n');
152
+ return 1;
153
+ }
154
+ let r;
155
+ try {
156
+ r = await api.postNoAuth('/api/auth/login', { username, passphrase });
157
+ } catch (err) {
158
+ if (err instanceof ApiError && err.status === 503) {
159
+ process.stderr.write(
160
+ 'crosstalk auth login: setup needed.\n' +
161
+ 'The engine has no admin user yet. Visit /setup in a browser using\n' +
162
+ 'the URL printed at engine boot, then run `crosstalk auth login` again.\n',
163
+ );
164
+ return 1;
165
+ }
166
+ reportAndExit(err, 'crosstalk auth login');
167
+ }
168
+ if (!r || typeof r.token !== 'string') {
169
+ process.stderr.write('crosstalk auth login: malformed response from engine.\n');
170
+ return 1;
171
+ }
172
+ // sas_<tokenId>.<secret> — pull the tokenId out for display + future
173
+ // revoke. Fallback: store null tokenId if the format ever changes.
174
+ const tokenId = parseTokenId(r.token);
175
+ saveProfile(profileName, {
176
+ server,
177
+ username,
178
+ token: r.token,
179
+ tokenId,
180
+ });
181
+ setActiveProfile(profileName);
182
+ process.stdout.write(
183
+ `logged in as ${username} (profile '${profileName}' → ${server})\n` +
184
+ ` credentials stored at ${credentialsFilePath()}\n`,
185
+ );
186
+ return 0;
187
+ }
188
+
189
+ function parseTokenId(token) {
190
+ // Wire format: sas_<id>.<secret>
191
+ const m = /^sas_([^.]+)\./.exec(token);
192
+ return m ? m[1] : null;
193
+ }
194
+
195
+ async function runLogout(argv) {
196
+ const profile = getActiveProfile(argv);
197
+ if (!profile) {
198
+ process.stdout.write('not logged in.\n');
199
+ return 0;
200
+ }
201
+ if (!has(argv, '--no-revoke')) {
202
+ const api = apiFor(argv);
203
+ try {
204
+ await api.post('/api/auth/logout');
205
+ } catch (_err) {
206
+ // Best-effort. Even if the server rejects (token already
207
+ // revoked elsewhere, network gone), still clean up locally.
208
+ }
209
+ }
210
+ const removed = deleteActiveProfile();
211
+ if (removed) {
212
+ process.stdout.write(`logged out (profile '${removed}' removed)\n`);
213
+ } else {
214
+ process.stdout.write('logged out\n');
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ async function runWhoami(argv) {
220
+ if (!getActiveProfile(argv)) {
221
+ process.stderr.write("not logged in. Run 'crosstalk auth login'.\n");
222
+ return 1;
223
+ }
224
+ const api = apiFor(argv);
225
+ let r;
226
+ try {
227
+ r = await api.get('/api/account');
228
+ } catch (err) {
229
+ reportAndExit(err, 'crosstalk auth whoami');
230
+ }
231
+ process.stdout.write(
232
+ `username: ${r.username}\n` +
233
+ `name: ${r.name}\n` +
234
+ `role: ${r.admin ? 'admin' : 'member'}\n` +
235
+ `created: ${r.createdAt}\n`,
236
+ );
237
+ return 0;
238
+ }
239
+
240
+ function runList() {
241
+ const profiles = listProfiles();
242
+ if (profiles.length === 0) {
243
+ process.stdout.write('no profiles. Run `crosstalk auth login` to create one.\n');
244
+ return 0;
245
+ }
246
+ // Compact tabular: active marker, name, username, server.
247
+ const nameW = Math.max(7, ...profiles.map((p) => p.name.length));
248
+ const userW = Math.max(8, ...profiles.map((p) => (p.username || '-').length));
249
+ const header = ` ${'PROFILE'.padEnd(nameW)} ${'USERNAME'.padEnd(userW)} SERVER`;
250
+ process.stdout.write(`${header}\n`);
251
+ for (const p of profiles) {
252
+ const marker = p.active ? '*' : ' ';
253
+ process.stdout.write(
254
+ `${marker} ${p.name.padEnd(nameW)} ${(p.username || '-').padEnd(userW)} ${p.server}\n`,
255
+ );
256
+ }
257
+ return 0;
258
+ }
259
+
260
+ function runUse(name) {
261
+ if (!name) {
262
+ process.stderr.write('crosstalk auth use: profile name required.\n');
263
+ return 1;
264
+ }
265
+ try {
266
+ setActiveProfile(name);
267
+ } catch (err) {
268
+ process.stderr.write(`crosstalk auth use: ${err.message}\n`);
269
+ return 1;
270
+ }
271
+ process.stdout.write(`active profile: ${name}\n`);
272
+ return 0;
273
+ }
@@ -1,22 +1,24 @@
1
- // crosstalk channel <name> — create a channel via POST /channels.
1
+ // crosstalk channel <list|create|rename|delete> — channel management.
2
2
  //
3
- // P4 only ships create. --rename and --delete need PATCH/DELETE
4
- // endpoints that haven't been added to the engine yet; they land
5
- // when the engine gains them.
3
+ // alpha.18 noun-verb shape: every operation has an explicit verb.
4
+ // The old flag-form (`channel <handle> --rename <new>`, `--delete`,
5
+ // bare `channel <name>` create) is gone — clean break, no aliases.
6
6
 
7
7
  import { apiFor } from '../lib/api-client.js';
8
8
  import { reportAndExit } from '../lib/errors.js';
9
- import { positionals, has, flag } from '../lib/argv.js';
9
+ import { positionals, has } from '../lib/argv.js';
10
10
 
11
11
  function usage(exit = 0) {
12
12
  const w = exit === 0 ? process.stdout : process.stderr;
13
13
  w.write(
14
14
  `Usage:
15
15
  crosstalk channel list # list channels
16
- crosstalk channel <name> # create a channel
17
- crosstalk channel <name-or-uuid> --rename <new> # rename
18
- crosstalk channel <name-or-uuid> --delete # delete (with confirmation)
16
+ crosstalk channel create <name> # create a channel
17
+ crosstalk channel rename <handle> <new-name> # rename
18
+ crosstalk channel delete <handle> # delete
19
19
  crosstalk channel --help
20
+
21
+ <handle> is either the channel name or its UUID.
20
22
  `,
21
23
  );
22
24
  process.exit(exit);
@@ -24,13 +26,15 @@ function usage(exit = 0) {
24
26
 
25
27
  export async function run(argv) {
26
28
  if (has(argv, '--help') || has(argv, '-h')) usage(0);
29
+ const pos = positionals(argv, [
30
+ '--containername', '-c', '--profile', '--server',
31
+ ]);
32
+ const sub = pos[0];
33
+ if (!sub) usage(1);
27
34
 
28
35
  const api = apiFor(argv);
29
- const pos = positionals(argv, ['--rename', '--containername', '-c']);
30
- if (pos.length === 0) usage(1);
31
36
 
32
- // `crosstalk channel list` → list channels
33
- if (pos[0] === 'list') {
37
+ if (sub === 'list') {
34
38
  let r;
35
39
  try {
36
40
  r = await api.get('/channels');
@@ -49,47 +53,61 @@ export async function run(argv) {
49
53
  return 0;
50
54
  }
51
55
 
52
- const handle = pos[0];
53
- const renameTo = flag(argv, '--rename');
54
- const wantDelete = has(argv, '--delete');
55
-
56
- if (renameTo && wantDelete) {
57
- process.stderr.write('crosstalk channel: --rename and --delete are mutually exclusive.\n');
58
- return 1;
56
+ if (sub === 'create') {
57
+ const name = pos[1];
58
+ if (!name) {
59
+ process.stderr.write('crosstalk channel create: <name> required.\n');
60
+ return 1;
61
+ }
62
+ let r;
63
+ try {
64
+ r = await api.post('/channels', { name });
65
+ } catch (err) {
66
+ reportAndExit(err, 'crosstalk channel create');
67
+ }
68
+ process.stdout.write(`Created channel: ${r.uuid}\n`);
69
+ process.stdout.write(` name: ${r.name}\n`);
70
+ return 0;
59
71
  }
60
72
 
61
- // Rename: PATCH /channels/<handle> { name: <new> }
62
- if (renameTo) {
73
+ if (sub === 'rename') {
74
+ const handle = pos[1];
75
+ const newName = pos[2];
76
+ if (!handle || !newName) {
77
+ process.stderr.write('crosstalk channel rename: <handle> <new-name> required.\n');
78
+ return 1;
79
+ }
63
80
  let r;
64
81
  try {
65
- r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: renameTo });
82
+ r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: newName });
66
83
  } catch (err) {
67
- reportAndExit(err, 'crosstalk channel');
84
+ reportAndExit(err, 'crosstalk channel rename');
68
85
  }
69
86
  if (r.noop) {
70
- process.stdout.write(`No-op: ${handle} already named ${renameTo}.\n`);
87
+ process.stdout.write(`No-op: ${handle} already named ${newName}.\n`);
71
88
  return 0;
72
89
  }
73
90
  process.stdout.write(`Renamed: ${handle} -> ${r.name} (${r.uuid})\n`);
74
91
  return 0;
75
92
  }
76
93
 
77
- // Delete: DELETE /channels/<handle>?confirm=<name>
78
- // Engine returns 400 if confirm is missing/wrong, with the expected
79
- // value in the error message. Client reads the channel's name first
80
- // and passes it as confirm — operator's explicit --delete flag IS the
81
- // confirmation for now. Interactive prompt deferred (would block on
82
- // stdin in scripts).
83
- if (wantDelete) {
94
+ if (sub === 'delete') {
95
+ const handle = pos[1];
96
+ if (!handle) {
97
+ process.stderr.write('crosstalk channel delete: <handle> required.\n');
98
+ return 1;
99
+ }
100
+ // Engine requires ?confirm=<name>. We resolve the channel's current
101
+ // name first so the verb itself is the operator's explicit consent.
84
102
  let target;
85
103
  try {
86
104
  const all = await api.get('/channels');
87
105
  target = all.channels.find((c) => c.uuid === handle || c.name === handle);
88
106
  } catch (err) {
89
- reportAndExit(err, 'crosstalk channel');
107
+ reportAndExit(err, 'crosstalk channel delete');
90
108
  }
91
109
  if (!target) {
92
- process.stderr.write(`crosstalk channel: '${handle}' not found.\n`);
110
+ process.stderr.write(`crosstalk channel delete: '${handle}' not found.\n`);
93
111
  return 1;
94
112
  }
95
113
  const confirm = target.name ?? target.uuid;
@@ -99,20 +117,12 @@ export async function run(argv) {
99
117
  `/channels/${encodeURIComponent(handle)}?confirm=${encodeURIComponent(confirm)}`,
100
118
  );
101
119
  } catch (err) {
102
- reportAndExit(err, 'crosstalk channel');
120
+ reportAndExit(err, 'crosstalk channel delete');
103
121
  }
104
122
  process.stdout.write(`Deleted channel: ${r.name ?? '(unnamed)'} (${r.uuid})\n`);
105
123
  return 0;
106
124
  }
107
125
 
108
- // No flags → create a channel with the given name.
109
- let r;
110
- try {
111
- r = await api.post('/channels', { name: handle });
112
- } catch (err) {
113
- reportAndExit(err, 'crosstalk channel');
114
- }
115
- process.stdout.write(`Created channel: ${r.uuid}\n`);
116
- process.stdout.write(` name: ${r.name}\n`);
117
- return 0;
126
+ process.stderr.write(`crosstalk channel: unknown subcommand '${sub}'.\n`);
127
+ usage(1);
118
128
  }