@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
package/commands/chat.js
CHANGED
|
@@ -1,110 +1,147 @@
|
|
|
1
|
-
// crosstalk chat — interactive
|
|
1
|
+
// crosstalk chat — interactive agent session on the host (v8-native).
|
|
2
2
|
//
|
|
3
|
-
// Modes
|
|
4
|
-
// crosstalk chat
|
|
5
|
-
// crosstalk chat --shell
|
|
3
|
+
// Modes:
|
|
4
|
+
// crosstalk chat <agent> [--cwd <path>] # interactive session with an agent CLI
|
|
5
|
+
// crosstalk chat --shell # bash escape hatch
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// picks one explicitly.
|
|
7
|
+
// alpha.18: positional `<agent>` replaces the v7 `--agent <name>` flag
|
|
8
|
+
// form. No backwards-compat alias.
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// v8-native has no container — the agent CLIs are installed in the
|
|
11
|
+
// operator's own $HOME (user mode) or the `crosstalk` service user's
|
|
12
|
+
// home (system mode). We just spawn the agent process directly with
|
|
13
|
+
// stdio inherited.
|
|
14
|
+
//
|
|
15
|
+
// Lifecycle: agent runs as a foreground child of `crosstalk chat`. When
|
|
16
|
+
// the operator exits the agent (Ctrl-D / quit / agent finishes), the
|
|
17
|
+
// child exits and we exit with the same status code. SIGINT/SIGTERM
|
|
18
|
+
// received by the parent are forwarded so the agent gets a chance to
|
|
19
|
+
// shut down cleanly before we exit. There's no "session persists after
|
|
20
|
+
// detach" — crosstalk is async-by-design, no live attach model.
|
|
21
|
+
//
|
|
22
|
+
// `<agent>` is required for non-shell mode. The host may have multiple
|
|
23
|
+
// agent CLIs installed; the operator picks one explicitly.
|
|
16
24
|
|
|
17
|
-
import { spawnSync } from 'child_process';
|
|
25
|
+
import { spawn, spawnSync } from 'child_process';
|
|
18
26
|
import { requireInitialized } from '../lib/resolve.js';
|
|
19
|
-
import {
|
|
20
|
-
import { flag, has } from '../lib/argv.js';
|
|
27
|
+
import { flag, has, positionals } from '../lib/argv.js';
|
|
21
28
|
|
|
22
29
|
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
23
30
|
|
|
24
31
|
function usage(exit = 0) {
|
|
25
32
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
26
33
|
w.write(
|
|
27
|
-
`Usage: crosstalk chat
|
|
34
|
+
`Usage: crosstalk chat <agent> [--cwd <path>]
|
|
28
35
|
crosstalk chat --shell
|
|
29
36
|
|
|
30
|
-
Opens an interactive session
|
|
37
|
+
Opens an interactive session with an agent CLI on the host. The agent
|
|
38
|
+
process is a foreground child of this command — when you exit the
|
|
39
|
+
agent, the process is reaped and 'crosstalk chat' returns.
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
agent CLIs installed; the operator must say which one
|
|
41
|
+
The agent name is REQUIRED for any chat mode (except --shell). The host
|
|
42
|
+
may have multiple agent CLIs installed; the operator must say which one
|
|
43
|
+
to invoke.
|
|
34
44
|
|
|
35
45
|
Supported agents: ${KNOWN_AGENTS.join(', ')}.
|
|
36
46
|
|
|
37
47
|
Modes:
|
|
38
|
-
|
|
39
|
-
agent's own
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
\`
|
|
48
|
+
<agent> Interactive with the named agent CLI. First-run OAuth
|
|
49
|
+
happens through the agent's own flow.
|
|
50
|
+
--cwd <path> Change directory before spawning the agent (handy for
|
|
51
|
+
pinning the session to a specific project root).
|
|
52
|
+
--shell Drop into bash. Useful for installing agent CLIs
|
|
53
|
+
(\`npm i -g @anthropic-ai/claude-code\`) or running
|
|
54
|
+
one-time setup commands.
|
|
44
55
|
|
|
45
56
|
Examples:
|
|
46
|
-
crosstalk chat
|
|
47
|
-
crosstalk chat --
|
|
48
|
-
crosstalk chat --shell
|
|
57
|
+
crosstalk chat claude
|
|
58
|
+
crosstalk chat codex --cwd ~/Repos/myproject
|
|
59
|
+
crosstalk chat --shell
|
|
49
60
|
`,
|
|
50
61
|
);
|
|
51
62
|
process.exit(exit);
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Spawn an interactive process attached to the operator's TTY. The
|
|
67
|
+
* child OWNS stdin/stdout/stderr for the duration; when it exits, we
|
|
68
|
+
* exit with the same code. SIGINT/SIGTERM forwarded for clean shutdown.
|
|
69
|
+
*/
|
|
70
|
+
function runInteractive(command, args = [], opts = {}) {
|
|
71
|
+
// Pre-flight: is the binary actually present on PATH? Catching this
|
|
72
|
+
// here gives a clearer error than the child's "command not found"
|
|
73
|
+
// hitting the operator's TTY.
|
|
74
|
+
const which = spawnSync('which', [command], { encoding: 'utf-8' });
|
|
75
|
+
if (which.status !== 0) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`crosstalk chat: '${command}' not found on PATH.\n` +
|
|
78
|
+
` Install it first (e.g., npm install -g @anthropic-ai/claude-code for claude).\n`,
|
|
79
|
+
);
|
|
80
|
+
return 127;
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const child = spawn(command, args, {
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const forward = (sig) => {
|
|
90
|
+
try { child.kill(sig); } catch { /* already gone */ }
|
|
91
|
+
};
|
|
92
|
+
process.on('SIGINT', () => forward('SIGINT'));
|
|
93
|
+
process.on('SIGTERM', () => forward('SIGTERM'));
|
|
94
|
+
|
|
95
|
+
child.on('exit', (code, signal) => {
|
|
96
|
+
// Belt-and-suspenders: if the child reported clean exit but is
|
|
97
|
+
// somehow still alive (rare; some agent CLIs fork sub-processes
|
|
98
|
+
// that hold the PID), SIGKILL to be sure. The async-by-design
|
|
99
|
+
// contract says no live agent persists past chat exit.
|
|
100
|
+
if (child.pid) {
|
|
101
|
+
try { process.kill(child.pid, 0); process.kill(child.pid, 'SIGKILL'); }
|
|
102
|
+
catch { /* expected: process is gone */ }
|
|
103
|
+
}
|
|
104
|
+
if (signal) {
|
|
105
|
+
// Re-raise the signal so the operator's shell sees the same exit semantics.
|
|
106
|
+
process.exit(128 + (signalToNumber(signal) ?? 1));
|
|
107
|
+
}
|
|
108
|
+
resolve(code ?? 0);
|
|
109
|
+
});
|
|
110
|
+
child.on('error', (err) => {
|
|
111
|
+
process.stderr.write(`crosstalk chat: spawn failed: ${err.message}\n`);
|
|
112
|
+
resolve(1);
|
|
113
|
+
});
|
|
65
114
|
});
|
|
66
|
-
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function signalToNumber(sig) {
|
|
118
|
+
// Minimal map — we only forward INT/TERM, so only those need to round-trip.
|
|
119
|
+
return { SIGINT: 2, SIGTERM: 15, SIGHUP: 1, SIGKILL: 9 }[sig];
|
|
67
120
|
}
|
|
68
121
|
|
|
69
122
|
export async function run(argv) {
|
|
70
123
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
71
124
|
|
|
72
|
-
|
|
73
|
-
|
|
125
|
+
// requireInitialized ensures we're being run against a known transport;
|
|
126
|
+
// its return value isn't used here (chat doesn't address the engine API)
|
|
127
|
+
// but the existence check fails fast with a clear error if the operator
|
|
128
|
+
// hasn't run `crosstalk transport init` for this --containername yet.
|
|
129
|
+
requireInitialized(argv);
|
|
130
|
+
|
|
131
|
+
const cwdFlag = flag(argv, '--cwd');
|
|
74
132
|
|
|
75
133
|
if (has(argv, '--shell')) {
|
|
76
|
-
|
|
134
|
+
const shell = process.env.SHELL ?? 'bash';
|
|
135
|
+
return runInteractive(shell, [], { cwd: cwdFlag });
|
|
77
136
|
}
|
|
78
137
|
|
|
79
|
-
const
|
|
138
|
+
const pos = positionals(argv, [
|
|
139
|
+
'--cwd', '--containername', '-c', '--profile', '--server',
|
|
140
|
+
]);
|
|
141
|
+
const agent = pos[0];
|
|
80
142
|
if (!agent) {
|
|
81
|
-
// Query the engine for what's actually installed (vs the theoretical
|
|
82
|
-
// supported set). Gives a smarter error than "supported: claude,
|
|
83
|
-
// codex, gemini, qwen, opencode, agy" when only 1-2 are actually
|
|
84
|
-
// present in the container.
|
|
85
|
-
let installed = null;
|
|
86
|
-
try {
|
|
87
|
-
const r = await api.get('/agents/installed');
|
|
88
|
-
installed = r.installed;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (!(err instanceof ConnectError)) {
|
|
91
|
-
// engine unreachable — fall through with no introspection
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (installed && installed.length === 0) {
|
|
95
|
-
process.stderr.write(
|
|
96
|
-
`crosstalk chat: no agent CLIs installed in this container.\n` +
|
|
97
|
-
` Install one first:\n` +
|
|
98
|
-
` crosstalk chat --shell\n` +
|
|
99
|
-
` npm install -g @anthropic-ai/claude-code # or your agent's installer\n`,
|
|
100
|
-
);
|
|
101
|
-
return 1;
|
|
102
|
-
}
|
|
103
|
-
const list = installed && installed.length > 0
|
|
104
|
-
? `Installed in this container: ${installed.join(', ')}.`
|
|
105
|
-
: `Supported agents: ${KNOWN_AGENTS.join(', ')}.`;
|
|
106
143
|
process.stderr.write(
|
|
107
|
-
`crosstalk chat:
|
|
144
|
+
`crosstalk chat: <agent> required.\n Supported: ${KNOWN_AGENTS.join(', ')}\n`,
|
|
108
145
|
);
|
|
109
146
|
return 1;
|
|
110
147
|
}
|
|
@@ -114,6 +151,5 @@ export async function run(argv) {
|
|
|
114
151
|
);
|
|
115
152
|
return 1;
|
|
116
153
|
}
|
|
117
|
-
|
|
118
|
-
return runInteractive(name, agent);
|
|
154
|
+
return runInteractive(agent, [], { cwd: cwdFlag });
|
|
119
155
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// crosstalk daemon <verb> — internal: runs the engine subprocess.
|
|
2
|
+
//
|
|
3
|
+
// This is what `crosstalk server start` spawns under the hood, and
|
|
4
|
+
// what the systemd unit's ExecStart points at. Operators don't normally
|
|
5
|
+
// type `crosstalk daemon` directly; `server start/stop/restart`
|
|
6
|
+
// + `transport init` are the user-facing wrappers.
|
|
7
|
+
//
|
|
8
|
+
// The engine TypeScript source lives at ./src/*.ts and is executed via tsx.
|
|
9
|
+
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
11
|
+
import { dirname, join, resolve } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { existsSync, statSync } from 'fs';
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
const SUBCOMMANDS = ['dispatch', 'init', 'stop'];
|
|
19
|
+
const STANDALONE_SUBCOMMANDS = new Set(['init']);
|
|
20
|
+
|
|
21
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const repoRoot = resolve(thisDir, '..');
|
|
23
|
+
|
|
24
|
+
function findTransportRoot(startDir) {
|
|
25
|
+
let dir = resolve(startDir);
|
|
26
|
+
while (true) {
|
|
27
|
+
if (existsSync(join(dir, 'CROSSTALK-VERSION')) && statSync(join(dir, 'CROSSTALK-VERSION')).isFile()) {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
const parent = dirname(dir);
|
|
31
|
+
if (parent === dir) return null;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usage(exitCode = 0) {
|
|
37
|
+
const w = exitCode === 0 ? process.stdout : process.stderr;
|
|
38
|
+
w.write(
|
|
39
|
+
`Usage: crosstalk daemon <subcommand> [args...]
|
|
40
|
+
|
|
41
|
+
Subcommands (operator normally doesn't invoke these directly):
|
|
42
|
+
${SUBCOMMANDS.map((s) => ` ${s}`).join('\n')}
|
|
43
|
+
|
|
44
|
+
dispatch Run the engine main loop (HTTP API + dispatch tick).
|
|
45
|
+
This is what 'crosstalk server start' spawns.
|
|
46
|
+
init Scaffold a new transport (called by 'crosstalk transport init').
|
|
47
|
+
stop Signal SIGTERM to the running engine via pidfile.
|
|
48
|
+
|
|
49
|
+
Most subcommands require you to be inside a Crosstalk transport (a
|
|
50
|
+
directory containing CROSSTALK-VERSION). 'init' can run from anywhere
|
|
51
|
+
to scaffold a new transport.
|
|
52
|
+
`,
|
|
53
|
+
);
|
|
54
|
+
process.exit(exitCode);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function run(argv) {
|
|
58
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') usage(argv.length === 0 ? 1 : 0);
|
|
59
|
+
|
|
60
|
+
const sub = argv[0];
|
|
61
|
+
if (!SUBCOMMANDS.includes(sub)) {
|
|
62
|
+
process.stderr.write(`crosstalk daemon: unknown subcommand '${sub}'\n\n`);
|
|
63
|
+
usage(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 'init' is allowed to run outside a transport (it scaffolds one).
|
|
67
|
+
// Every other subcommand walks up from cwd to find a CROSSTALK-VERSION
|
|
68
|
+
// marker and chdir's into the transport root.
|
|
69
|
+
if (!STANDALONE_SUBCOMMANDS.has(sub)) {
|
|
70
|
+
const transportRoot = findTransportRoot(process.cwd());
|
|
71
|
+
if (!transportRoot) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`crosstalk daemon ${sub}: not inside a Crosstalk transport ` +
|
|
74
|
+
`(no CROSSTALK-VERSION found from ${process.cwd()} upward).\n`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
process.chdir(transportRoot);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve src/<sub>.ts (engine TypeScript). tsx is a runtime dep of
|
|
82
|
+
// the merged package; resolve its CLI from node_modules.
|
|
83
|
+
const srcFile = join(repoRoot, 'src', `${sub}.ts`);
|
|
84
|
+
if (!existsSync(srcFile)) {
|
|
85
|
+
process.stderr.write(`crosstalk daemon ${sub}: source file missing — ${srcFile}\n`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tsxCli = (() => {
|
|
90
|
+
try { return require.resolve('tsx/cli', { paths: [repoRoot] }); }
|
|
91
|
+
catch { return join(repoRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'); }
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
// For 'dispatch' (long-running engine main loop), spawn as a child
|
|
95
|
+
// so we can forward signals + exit codes faithfully. For 'init' /
|
|
96
|
+
// 'stop' (one-shots), spawnSync is fine and faster.
|
|
97
|
+
const args = [tsxCli, srcFile, ...argv.slice(1)];
|
|
98
|
+
|
|
99
|
+
if (sub === 'dispatch') {
|
|
100
|
+
const child = spawn(process.execPath, args, {
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
env: process.env,
|
|
103
|
+
});
|
|
104
|
+
const forward = (sig) => child.kill(sig);
|
|
105
|
+
process.on('SIGINT', forward);
|
|
106
|
+
process.on('SIGTERM', forward);
|
|
107
|
+
process.on('SIGHUP', forward);
|
|
108
|
+
return new Promise((res) => {
|
|
109
|
+
child.on('exit', (code, sig) => {
|
|
110
|
+
if (sig) process.kill(process.pid, sig);
|
|
111
|
+
else res(code ?? 0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Stop / init — one-shot, sync.
|
|
117
|
+
const r = spawnSync(process.execPath, args, { stdio: 'inherit', env: process.env });
|
|
118
|
+
return r.status ?? 0;
|
|
119
|
+
}
|
|
120
|
+
|
package/commands/logs.js
CHANGED
|
@@ -1,37 +1,126 @@
|
|
|
1
|
-
// crosstalk logs
|
|
1
|
+
// crosstalk logs <list|tail> — view engine logs.
|
|
2
|
+
//
|
|
3
|
+
// list [--limit N] [--json] Snapshot of recent entries.
|
|
4
|
+
// tail [--since <iso>] Live SSE stream — Ctrl-C to stop.
|
|
5
|
+
//
|
|
6
|
+
// Mirrors llmux's logs noun.
|
|
2
7
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { apiFor } from '../lib/api-client.js';
|
|
9
|
+
import { ConnectError } from '../lib/api-client.js';
|
|
10
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
11
|
+
import { flag, has } from '../lib/argv.js';
|
|
12
|
+
import { getActiveProfile } from '../lib/credentials.js';
|
|
13
|
+
import { DEFAULT_API_PORT } from '../lib/api-client.js';
|
|
7
14
|
|
|
8
15
|
function usage(exit = 0) {
|
|
9
16
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
17
|
w.write(
|
|
11
|
-
`Usage:
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
`Usage:
|
|
19
|
+
crosstalk logs list [--limit N] [--json]
|
|
20
|
+
crosstalk logs tail [--since <iso>]
|
|
21
|
+
crosstalk logs --help
|
|
14
22
|
`,
|
|
15
23
|
);
|
|
16
24
|
process.exit(exit);
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
function fmtEntry(e) {
|
|
28
|
+
const ts = e.ts || e.timestamp || '';
|
|
29
|
+
const lvl = (e.level || 'info').toUpperCase().padEnd(5);
|
|
30
|
+
const msg = e.event || e.message || JSON.stringify(e);
|
|
31
|
+
const fields = e.fields && Object.keys(e.fields).length > 0
|
|
32
|
+
? ' ' + Object.entries(e.fields).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' ')
|
|
33
|
+
: '';
|
|
34
|
+
return `${ts} ${lvl} ${msg}${fields}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export async function run(argv) {
|
|
20
38
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
39
|
+
const sub = argv[0];
|
|
40
|
+
if (!sub) usage(1);
|
|
21
41
|
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
42
|
+
if (sub === 'list') return await runList(argv.slice(1));
|
|
43
|
+
if (sub === 'tail') return await runTail(argv.slice(1));
|
|
44
|
+
|
|
45
|
+
process.stderr.write(`crosstalk logs: unknown subcommand '${sub}'.\n`);
|
|
46
|
+
usage(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runList(argv) {
|
|
50
|
+
const api = apiFor(argv);
|
|
51
|
+
let r;
|
|
52
|
+
try {
|
|
53
|
+
r = await api.get('/api/logs');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
reportAndExit(err, 'crosstalk logs list');
|
|
56
|
+
}
|
|
57
|
+
let entries = r.entries || [];
|
|
58
|
+
const limitStr = flag(argv, '--limit');
|
|
59
|
+
if (limitStr) {
|
|
60
|
+
const n = parseInt(limitStr, 10);
|
|
61
|
+
if (Number.isFinite(n) && n > 0) entries = entries.slice(-n);
|
|
62
|
+
}
|
|
63
|
+
if (has(argv, '--json')) {
|
|
64
|
+
process.stdout.write(JSON.stringify({ entries, currentSeq: r.currentSeq }, null, 2) + '\n');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
if (entries.length === 0) {
|
|
68
|
+
process.stdout.write('(no log entries)\n');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
for (const e of entries) process.stdout.write(fmtEntry(e) + '\n');
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runTail(argv) {
|
|
76
|
+
// SSE streaming via fetch — manual parse since our api-client wrapper
|
|
77
|
+
// assumes JSON response bodies. Token + server URL are read from the
|
|
78
|
+
// active credentials profile, matching the rest of the CLI.
|
|
79
|
+
const profile = getActiveProfile(argv);
|
|
80
|
+
const server = (profile && profile.server) || `http://127.0.0.1:${DEFAULT_API_PORT}`;
|
|
81
|
+
const since = flag(argv, '--since');
|
|
82
|
+
const url = `${server.replace(/\/+$/, '')}/api/logs/stream${since ? `?since=${encodeURIComponent(since)}` : ''}`;
|
|
83
|
+
|
|
84
|
+
const headers = {};
|
|
85
|
+
if (profile && profile.token) headers['Authorization'] = `Bearer ${profile.token}`;
|
|
86
|
+
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await fetch(url, { headers });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new ConnectError(server, err.message || String(err));
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok || !res.body) {
|
|
94
|
+
process.stderr.write(`crosstalk logs tail: stream open failed (HTTP ${res.status}).\n`);
|
|
25
95
|
return 1;
|
|
26
96
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
97
|
+
|
|
98
|
+
const reader = res.body.getReader();
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
let buf = '';
|
|
101
|
+
const onSig = () => process.exit(0);
|
|
102
|
+
process.on('SIGINT', onSig);
|
|
103
|
+
process.on('SIGTERM', onSig);
|
|
104
|
+
|
|
105
|
+
while (true) {
|
|
106
|
+
const { value, done } = await reader.read();
|
|
107
|
+
if (done) break;
|
|
108
|
+
buf += decoder.decode(value, { stream: true });
|
|
109
|
+
// SSE frames are separated by \n\n; each `data:` line is JSON.
|
|
110
|
+
let nl;
|
|
111
|
+
while ((nl = buf.indexOf('\n\n')) !== -1) {
|
|
112
|
+
const frame = buf.slice(0, nl);
|
|
113
|
+
buf = buf.slice(nl + 2);
|
|
114
|
+
const dataLine = frame.split('\n').find((l) => l.startsWith('data:'));
|
|
115
|
+
if (!dataLine) continue;
|
|
116
|
+
const payload = dataLine.slice(5).trimStart();
|
|
117
|
+
try {
|
|
118
|
+
const entry = JSON.parse(payload);
|
|
119
|
+
process.stdout.write(fmtEntry(entry) + '\n');
|
|
120
|
+
} catch {
|
|
121
|
+
process.stdout.write(payload + '\n');
|
|
122
|
+
}
|
|
33
123
|
}
|
|
34
124
|
}
|
|
35
|
-
|
|
36
|
-
return r.status ?? 1;
|
|
125
|
+
return 0;
|
|
37
126
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// crosstalk message <send|replies> — dispatch + poll primitive messages.
|
|
2
|
+
//
|
|
3
|
+
// Replaces v7's `crosstalk run --type primitive` (now `message send`)
|
|
4
|
+
// and the top-level `crosstalk replies` (now `message replies`).
|
|
5
|
+
// Workflows live under the `workflow` noun (compose|run|status).
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
8
|
+
import { apiFor } from '../lib/api-client.js';
|
|
9
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
10
|
+
import { flag, has, positionals } from '../lib/argv.js';
|
|
11
|
+
|
|
12
|
+
const SEND_FLAGS_WITH_VALUE = new Set([
|
|
13
|
+
'--to', '--as', '--fanout', '--channel', '--from',
|
|
14
|
+
'--containername', '-c', '--profile', '--server',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function usage(exit = 0) {
|
|
18
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
19
|
+
w.write(
|
|
20
|
+
`Usage:
|
|
21
|
+
crosstalk message send --to <model>[@<machine>] \\
|
|
22
|
+
[--as <persona>] [--fanout <n>] \\
|
|
23
|
+
[--channel <name|uuid>] [--from <name>] \\
|
|
24
|
+
<body|file|->
|
|
25
|
+
|
|
26
|
+
crosstalk message replies <relPath> [<relPath>...]
|
|
27
|
+
`,
|
|
28
|
+
);
|
|
29
|
+
process.exit(exit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readStdin() {
|
|
33
|
+
try { return readFileSync(0, 'utf-8'); } catch { return ''; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveBody(arg) {
|
|
37
|
+
if (arg == null) return null;
|
|
38
|
+
if (arg === '-') return readStdin();
|
|
39
|
+
if (existsSync(arg)) {
|
|
40
|
+
try {
|
|
41
|
+
if (statSync(arg).isFile()) return readFileSync(arg, 'utf-8');
|
|
42
|
+
} catch { /* fall through to inline */ }
|
|
43
|
+
}
|
|
44
|
+
return arg;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function run(argv) {
|
|
48
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
49
|
+
const sub = argv[0];
|
|
50
|
+
if (!sub) usage(1);
|
|
51
|
+
const rest = argv.slice(1);
|
|
52
|
+
|
|
53
|
+
if (sub === 'send') return await runSend(rest);
|
|
54
|
+
if (sub === 'replies') return await runReplies(rest);
|
|
55
|
+
|
|
56
|
+
process.stderr.write(`crosstalk message: unknown subcommand '${sub}'.\n`);
|
|
57
|
+
usage(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runSend(argv) {
|
|
61
|
+
const api = apiFor(argv);
|
|
62
|
+
|
|
63
|
+
const to = flag(argv, '--to');
|
|
64
|
+
if (!to) {
|
|
65
|
+
process.stderr.write('crosstalk message send: --to <model>[@<machine>] is required\n');
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Body is the last positional. positionals() skips flag values so the
|
|
70
|
+
// body resolves regardless of flag order — `--containername a8 hello`
|
|
71
|
+
// and `hello --containername a8` both work.
|
|
72
|
+
const pos = positionals(argv, [...SEND_FLAGS_WITH_VALUE]);
|
|
73
|
+
const bodyArg = pos[pos.length - 1];
|
|
74
|
+
const body = resolveBody(bodyArg);
|
|
75
|
+
if (body == null || body.length === 0) {
|
|
76
|
+
process.stderr.write('crosstalk message send: missing body (file path, inline string, or `-` for stdin)\n');
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fanoutRaw = flag(argv, '--fanout');
|
|
81
|
+
const fanout = fanoutRaw ? Math.max(1, parseInt(fanoutRaw, 10)) : 1;
|
|
82
|
+
const payload = {
|
|
83
|
+
type: 'primitive',
|
|
84
|
+
to,
|
|
85
|
+
as: flag(argv, '--as'),
|
|
86
|
+
channel: flag(argv, '--channel'),
|
|
87
|
+
from: flag(argv, '--from') ?? process.env.USER ?? 'operator',
|
|
88
|
+
fanout,
|
|
89
|
+
body,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let resp;
|
|
93
|
+
try {
|
|
94
|
+
resp = await api.post('/messages', payload);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
reportAndExit(err, 'crosstalk message send');
|
|
97
|
+
}
|
|
98
|
+
for (const p of (resp.relPaths || [])) process.stdout.write(`Sent: ${p}\n`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runReplies(argv) {
|
|
103
|
+
const api = apiFor(argv);
|
|
104
|
+
const targets = positionals(argv, ['--containername', '-c', '--profile', '--server']);
|
|
105
|
+
if (targets.length === 0) usage(1);
|
|
106
|
+
|
|
107
|
+
let resp;
|
|
108
|
+
try {
|
|
109
|
+
resp = await api.get(`/replies?relPaths=${encodeURIComponent(targets.join(','))}`);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
reportAndExit(err, 'crosstalk message replies');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let pending = 0;
|
|
115
|
+
for (const r of (resp.replies || [])) {
|
|
116
|
+
if (r.status === 'PENDING') {
|
|
117
|
+
process.stdout.write(`PENDING ${r.target}\n`);
|
|
118
|
+
pending++;
|
|
119
|
+
} else {
|
|
120
|
+
const tag = r.status === 'FAILED' ? 'FAILED ' : 'REPLIED ';
|
|
121
|
+
process.stdout.write(`${tag} ${r.target} <- ${r.from} (${r.replyRelPath})\n`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return pending > 0 ? 2 : 0;
|
|
125
|
+
}
|