@cordfuse/crosstalk 7.0.0-alpha.5 → 7.0.0-alpha.7
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/README.md +23 -18
- package/bin/crosstalk.js +2 -0
- package/commands/auth.js +218 -0
- package/commands/channel.js +3 -2
- package/commands/chat.js +90 -24
- package/commands/down.js +18 -15
- package/commands/init.js +213 -68
- package/commands/logs.js +8 -10
- package/commands/pull.js +7 -9
- package/commands/replies.js +4 -3
- package/commands/restart.js +16 -11
- package/commands/rm.js +109 -0
- package/commands/run.js +4 -3
- package/commands/status.js +15 -10
- package/commands/up.js +81 -169
- package/commands/version.js +3 -2
- package/lib/api-client.js +43 -17
- package/lib/argv.js +4 -2
- package/lib/resolve.js +192 -0
- package/package.json +1 -1
- package/lib/transport.js +0 -51
package/README.md
CHANGED
|
@@ -19,16 +19,14 @@ Requires: Node.js ≥ 20 + Docker on PATH + git on PATH. Works on Linux, macOS,
|
|
|
19
19
|
## Quickstart
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
# 1. Scaffold a transport.
|
|
23
|
-
crosstalk init
|
|
24
|
-
cd mytransport
|
|
22
|
+
# 1. Scaffold a transport. Runtime-owned at <base>/crosstalk/transport/.
|
|
23
|
+
crosstalk init
|
|
25
24
|
|
|
26
|
-
# 2. Bring up the engine container
|
|
25
|
+
# 2. Bring up the engine container.
|
|
27
26
|
crosstalk up
|
|
28
|
-
# Generates docker-compose.yml
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# host is immediately visible to the engine.
|
|
27
|
+
# Generates <base>/crosstalk/docker-compose.yml and runs `docker
|
|
28
|
+
# compose up -d` for ghcr.io/cordfuse/crosstalk-server:<latest>.
|
|
29
|
+
# No cwd magic — works from any directory.
|
|
32
30
|
|
|
33
31
|
# 3. Install an agent CLI inside the container, then authenticate.
|
|
34
32
|
crosstalk chat --shell
|
|
@@ -36,8 +34,8 @@ crosstalk chat --shell
|
|
|
36
34
|
exit
|
|
37
35
|
crosstalk chat --agent claude --login
|
|
38
36
|
# Auth URL opens in your default browser via xdg-open / open / start.
|
|
39
|
-
#
|
|
40
|
-
#
|
|
37
|
+
# Token saved in the /crosstalk-root volume so subsequent dispatched
|
|
38
|
+
# workers reuse it.
|
|
41
39
|
|
|
42
40
|
# 4. Send your first message.
|
|
43
41
|
crosstalk channel general
|
|
@@ -49,16 +47,19 @@ crosstalk replies <relPath printed by step above>
|
|
|
49
47
|
|
|
50
48
|
## Subcommand surface
|
|
51
49
|
|
|
50
|
+
Every name-resolved verb takes an optional `--containername <name>` (or `-c <name>`) flag. Bare verb = default container (literally named `crosstalk`).
|
|
51
|
+
|
|
52
52
|
| Group | Subcommand | What it does |
|
|
53
53
|
|---|---|---|
|
|
54
54
|
| Meta | `crosstalk version` | Print client + engine versions (warns on skew) |
|
|
55
|
-
| Protocol | `crosstalk init <
|
|
55
|
+
| Protocol | `crosstalk init [-c <name>] [--remote <url>]` | Scaffold a transport at `<base>/<name>/transport/` |
|
|
56
56
|
| Protocol | `crosstalk run --type primitive\|workflow ...` | Dispatch a primitive or workflow |
|
|
57
57
|
| Protocol | `crosstalk replies <relPath>...` | Poll reply status |
|
|
58
|
-
| Protocol | `crosstalk status` |
|
|
58
|
+
| Protocol | `crosstalk status` | Container name, transport host path, engine state, channels |
|
|
59
59
|
| Protocol | `crosstalk channel <name> [--rename\|--delete]` | Channel operations |
|
|
60
|
-
| Lifecycle | `crosstalk up` | Generate docker-compose, start engine container |
|
|
61
|
-
| Lifecycle | `crosstalk down
|
|
60
|
+
| Lifecycle | `crosstalk up` | Generate docker-compose, start the engine container |
|
|
61
|
+
| Lifecycle | `crosstalk down` | Stop the engine container (keep storage) |
|
|
62
|
+
| Lifecycle | `crosstalk rm [--force]` | Remove container AND wipe storage (cannot undo) |
|
|
62
63
|
| Lifecycle | `crosstalk restart` | Down + up |
|
|
63
64
|
| Lifecycle | `crosstalk pull` | Refresh the engine image |
|
|
64
65
|
| Lifecycle | `crosstalk logs [-f]` | Stream the engine container's logs |
|
|
@@ -72,10 +73,14 @@ crosstalk replies <relPath printed by step above>
|
|
|
72
73
|
|
|
73
74
|
## What lives where
|
|
74
75
|
|
|
75
|
-
- **Host** — `crosstalk` binary on PATH
|
|
76
|
-
- **Container** (`
|
|
77
|
-
-
|
|
78
|
-
-
|
|
76
|
+
- **Host** — `crosstalk` binary on PATH. Per-transport storage at `<base>/<name>/` (one tree per container).
|
|
77
|
+
- **Container** (literally named whatever the operator picked at `init`, default `crosstalk`; image `ghcr.io/cordfuse/crosstalk-server`) — `crosstalkd` daemon, bind-mounted from:
|
|
78
|
+
- `<base>/<name>/transport/` → `/var/lib/crosstalk-transport` (runtime-owned git repo)
|
|
79
|
+
- `<base>/<name>/crosstalk-root/` → `/crosstalk-root` (operator CLIs + auth)
|
|
80
|
+
- `<base>/<name>/crosstalk-state/` → `/var/lib/crosstalk-state` (cursor + heartbeat)
|
|
81
|
+
- **`<base>`** is per-OS user-mode: `$XDG_DATA_HOME/crosstalk` on Linux, `~/Library/Application Support/crosstalk` on macOS, `%LOCALAPPDATA%\crosstalk` on Windows. `CROSSTALK_STORAGE_MODE=system` switches to `/var/lib`-style paths.
|
|
82
|
+
- **Engine HTTP API** — `127.0.0.1:<port>`. Default container = port 7000; named containers allocate at init from 7001 upward, recorded in `<base>/<name>/api-port`.
|
|
83
|
+
- **Container identification** — every crosstalkd container gets the Docker label `crosstalk.transport=true`. The client uses the label for inventory queries, not name-pattern matching.
|
|
79
84
|
|
|
80
85
|
---
|
|
81
86
|
|
package/bin/crosstalk.js
CHANGED
package/commands/auth.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// crosstalk auth — manage agent auth tokens/keys.
|
|
2
|
+
//
|
|
3
|
+
// One subverb: `paste`. Operator runs the agent's headless-token
|
|
4
|
+
// generator on their host (`claude setup-token`, etc.), pastes the
|
|
5
|
+
// resulting token at our prompt, and we persist it in the container's
|
|
6
|
+
// env so subsequent dispatch + chat invocations are authenticated.
|
|
7
|
+
//
|
|
8
|
+
// Storage: <base>/<name>/auth.env (0600, operator-UID owned). Docker
|
|
9
|
+
// Compose's `env_file:` references it; each line is `KEY=value`.
|
|
10
|
+
// Existing keys are replaced in-place; new keys append. Empty lines
|
|
11
|
+
// and `#` comments preserved.
|
|
12
|
+
//
|
|
13
|
+
// Why this exists: 5 of the 6 supported agents work cleanly with env-
|
|
14
|
+
// var auth (the headless path), but none of them have a uniform "set
|
|
15
|
+
// the token" UX inside the container. This verb is the operator's
|
|
16
|
+
// one-stop shop — same UX for any agent.
|
|
17
|
+
//
|
|
18
|
+
// Per-agent env var (the audit on 2026-06-14 confirmed each):
|
|
19
|
+
// claude → CLAUDE_CODE_OAUTH_TOKEN (preferred: `claude setup-token`)
|
|
20
|
+
// ANTHROPIC_API_KEY (alternate: Console API key)
|
|
21
|
+
// codex → OPENAI_API_KEY (preferred)
|
|
22
|
+
// CODEX_ACCESS_TOKEN (alternate: device-auth output)
|
|
23
|
+
// gemini → GEMINI_API_KEY
|
|
24
|
+
// qwen → OPENAI_API_KEY (qwen --auth-type openai)
|
|
25
|
+
// agy → none (interactive OAuth only; errors with hint)
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
28
|
+
import { createInterface } from 'readline';
|
|
29
|
+
import { has, flag } from '../lib/argv.js';
|
|
30
|
+
import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
|
|
31
|
+
|
|
32
|
+
const AGENT_ENV = {
|
|
33
|
+
claude: { default: 'CLAUDE_CODE_OAUTH_TOKEN', label: 'OAuth token' },
|
|
34
|
+
codex: { default: 'OPENAI_API_KEY', label: 'API key' },
|
|
35
|
+
gemini: { default: 'GEMINI_API_KEY', label: 'API key' },
|
|
36
|
+
qwen: { default: 'OPENAI_API_KEY', label: 'API key' },
|
|
37
|
+
opencode: { default: 'OPENAI_API_KEY', label: 'provider API key' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const AGENT_GEN_CMD = {
|
|
41
|
+
claude: 'claude setup-token # on a host with a browser',
|
|
42
|
+
codex: 'echo $OPENAI_API_KEY # or platform.openai.com/api-keys',
|
|
43
|
+
gemini: 'echo $GEMINI_API_KEY # or aistudio.google.com/apikey',
|
|
44
|
+
qwen: 'echo $OPENAI_API_KEY # OpenAI-compatible',
|
|
45
|
+
opencode: 'echo $OPENAI_API_KEY # or provider-specific',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Control byte values used by the TTY prompt — avoid embedding raw
|
|
49
|
+
// bytes inline so the file is editor/linter clean.
|
|
50
|
+
const CTRL_C = String.fromCharCode(3); // 0x03 — interrupt
|
|
51
|
+
const CTRL_D = String.fromCharCode(4); // 0x04 — EOF
|
|
52
|
+
const BS = String.fromCharCode(8); // 0x08 — backspace
|
|
53
|
+
const DEL = String.fromCharCode(127); // 0x7F — terminal backspace
|
|
54
|
+
|
|
55
|
+
function usage(exit = 0) {
|
|
56
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
57
|
+
w.write(
|
|
58
|
+
`Usage: crosstalk auth paste --agent <name> [--containername <name>]
|
|
59
|
+
|
|
60
|
+
Persists an auth token/key for an agent CLI into the container's
|
|
61
|
+
environment. Operator generates the value on the host (which has a
|
|
62
|
+
browser) and pastes at the prompt; crosstalk writes it to
|
|
63
|
+
<base>/<name>/auth.env. Subsequent dispatch + chat invocations pick
|
|
64
|
+
it up via the engine container's env.
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--agent <name> One of: claude, codex, gemini, qwen, opencode.
|
|
68
|
+
agy is interactive-only; use 'crosstalk chat
|
|
69
|
+
--agent agy --login' for OAuth instead.
|
|
70
|
+
--containername <name> Which crosstalk container to set the env on
|
|
71
|
+
(default: 'crosstalk').
|
|
72
|
+
-c <name> Shorthand for --containername.
|
|
73
|
+
|
|
74
|
+
Reads from stdin (hidden) — if the input isn't a TTY, the entire stdin
|
|
75
|
+
is read as the token (e.g. piped from a setup script).
|
|
76
|
+
|
|
77
|
+
After paste, run 'crosstalk restart' to pick up the new env.
|
|
78
|
+
`,
|
|
79
|
+
);
|
|
80
|
+
process.exit(exit);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function promptHidden(promptText) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
process.stderr.write(promptText);
|
|
86
|
+
if (!process.stdin.isTTY) {
|
|
87
|
+
let buf = '';
|
|
88
|
+
process.stdin.on('data', (chunk) => { buf += chunk.toString(); });
|
|
89
|
+
process.stdin.on('end', () => resolve(buf.trim()));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof process.stdin.setRawMode !== 'function') {
|
|
93
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
94
|
+
rl.question('', (ans) => { rl.close(); resolve(ans.trim()); });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
process.stdin.setRawMode(true);
|
|
98
|
+
process.stdin.resume();
|
|
99
|
+
process.stdin.setEncoding('utf-8');
|
|
100
|
+
let buf = '';
|
|
101
|
+
const onData = (chunk) => {
|
|
102
|
+
for (const ch of chunk) {
|
|
103
|
+
if (ch === '\r' || ch === '\n') {
|
|
104
|
+
process.stderr.write('\n');
|
|
105
|
+
process.stdin.setRawMode(false);
|
|
106
|
+
process.stdin.pause();
|
|
107
|
+
process.stdin.removeListener('data', onData);
|
|
108
|
+
resolve(buf.trim());
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (ch === CTRL_C || ch === CTRL_D) {
|
|
112
|
+
process.stderr.write('\n');
|
|
113
|
+
process.stdin.setRawMode(false);
|
|
114
|
+
process.exit(130);
|
|
115
|
+
}
|
|
116
|
+
if (ch === BS || ch === DEL) {
|
|
117
|
+
buf = buf.slice(0, -1);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
buf += ch;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
process.stdin.on('data', onData);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Upsert a KEY=value line in env-file content. Preserves other lines +
|
|
128
|
+
// comments. Returns the new content.
|
|
129
|
+
function upsertEnv(existing, key, value) {
|
|
130
|
+
const lines = existing.split('\n');
|
|
131
|
+
let replaced = false;
|
|
132
|
+
const out = lines.map((line) => {
|
|
133
|
+
if (line.startsWith(`${key}=`)) {
|
|
134
|
+
replaced = true;
|
|
135
|
+
return `${key}=${value}`;
|
|
136
|
+
}
|
|
137
|
+
return line;
|
|
138
|
+
});
|
|
139
|
+
if (!replaced) {
|
|
140
|
+
while (out.length > 0 && out[out.length - 1] === '') out.pop();
|
|
141
|
+
out.push(`${key}=${value}`);
|
|
142
|
+
out.push('');
|
|
143
|
+
}
|
|
144
|
+
return out.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function run(argv) {
|
|
148
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
149
|
+
|
|
150
|
+
const pos = argv.filter((a) => !a.startsWith('-'));
|
|
151
|
+
const subverb = pos[0];
|
|
152
|
+
if (subverb !== 'paste') {
|
|
153
|
+
if (subverb) {
|
|
154
|
+
process.stderr.write(`crosstalk auth: unknown subverb '${subverb}'. Try 'paste'.\n`);
|
|
155
|
+
} else {
|
|
156
|
+
usage(1);
|
|
157
|
+
}
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const agent = flag(argv, '--agent');
|
|
162
|
+
if (!agent) {
|
|
163
|
+
process.stderr.write(`crosstalk auth paste: --agent <name> is required.\n`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (agent === 'agy') {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
`crosstalk auth paste: agy has no headless env-var path — its only auth is interactive OAuth.\n` +
|
|
170
|
+
` Use 'crosstalk chat --agent agy --login' to set up auth.\n`,
|
|
171
|
+
);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const cfg = AGENT_ENV[agent];
|
|
176
|
+
if (!cfg) {
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`crosstalk auth paste: unknown agent '${agent}'.\n` +
|
|
179
|
+
` Supported: ${Object.keys(AGENT_ENV).join(', ')}.\n`,
|
|
180
|
+
);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { name, paths } = requireInitialized(argv);
|
|
185
|
+
|
|
186
|
+
if (process.stdin.isTTY) {
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
`\nPasting ${cfg.label} for ${agent} into '${name}' container.\n` +
|
|
189
|
+
` Env var: ${cfg.default}\n` +
|
|
190
|
+
` Storage: ${paths.authEnvFile}\n` +
|
|
191
|
+
`\nGenerate on host:\n ${AGENT_GEN_CMD[agent] ?? '(see agent docs)'}\n` +
|
|
192
|
+
`\nPaste the value below (input hidden, press Enter):\n`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const value = await promptHidden('> ');
|
|
196
|
+
if (!value) {
|
|
197
|
+
process.stderr.write(`crosstalk auth paste: empty input — aborted.\n`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
if (value.includes('\n')) {
|
|
201
|
+
process.stderr.write(
|
|
202
|
+
`crosstalk auth paste: input contains a newline. Tokens are single-line; check copy/paste.\n`,
|
|
203
|
+
);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const existing = existsSync(paths.authEnvFile)
|
|
208
|
+
? readFileSync(paths.authEnvFile, 'utf-8')
|
|
209
|
+
: '';
|
|
210
|
+
const updated = upsertEnv(existing, cfg.default, value);
|
|
211
|
+
writeFileSync(paths.authEnvFile, updated, { mode: 0o600 });
|
|
212
|
+
|
|
213
|
+
process.stdout.write(
|
|
214
|
+
`\nSaved ${cfg.default} to ${paths.authEnvFile}.\n` +
|
|
215
|
+
`Run 'crosstalk restart${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to pick up the new env.\n`,
|
|
216
|
+
);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
package/commands/channel.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// endpoints that haven't been added to the engine yet; they land
|
|
5
5
|
// when the engine gains them.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { apiFor } from '../lib/api-client.js';
|
|
8
8
|
import { reportAndExit } from '../lib/errors.js';
|
|
9
9
|
import { positionals, has, flag } from '../lib/argv.js';
|
|
10
10
|
|
|
@@ -25,7 +25,8 @@ function usage(exit = 0) {
|
|
|
25
25
|
export async function run(argv) {
|
|
26
26
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const api = apiFor(argv);
|
|
29
|
+
const pos = positionals(argv, ['--rename', '--containername', '-c']);
|
|
29
30
|
if (pos.length === 0) usage(1);
|
|
30
31
|
|
|
31
32
|
// `crosstalk channel list` → list channels
|
package/commands/chat.js
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
// fallback, not the primary mechanism.
|
|
24
24
|
|
|
25
25
|
import { spawn, spawnSync } from 'child_process';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
26
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
27
|
+
import { apiFor, ConnectError } from '../lib/api-client.js';
|
|
28
28
|
import { flag, has } from '../lib/argv.js';
|
|
29
29
|
|
|
30
30
|
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
@@ -132,6 +132,17 @@ Examples:
|
|
|
132
132
|
process.exit(exit);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Detect host terminal dims with sane fallbacks. Pass into the
|
|
136
|
+
// container via -e COLUMNS/LINES so the agent CLI renders at the
|
|
137
|
+
// operator's real terminal size instead of docker's 80×24 fallback.
|
|
138
|
+
// (When stdout is piped — like in --login — docker can't auto-detect
|
|
139
|
+
// the host's stdout terminal, which is mac's alpha.6 bug D.)
|
|
140
|
+
function hostTermDims() {
|
|
141
|
+
const cols = process.stdout.columns || process.stderr.columns || 120;
|
|
142
|
+
const rows = process.stdout.rows || process.stderr.rows || 40;
|
|
143
|
+
return { cols, rows };
|
|
144
|
+
}
|
|
145
|
+
|
|
135
146
|
function runInteractive(container, command, env = {}) {
|
|
136
147
|
// Full TTY passthrough — docker exec -it gives the container a real
|
|
137
148
|
// PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
|
|
@@ -147,30 +158,79 @@ function runInteractive(container, command, env = {}) {
|
|
|
147
158
|
return r.status ?? 1;
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
// Per-agent login command. Returns null for agents without an OAuth
|
|
162
|
+
// path (gemini, qwen, opencode) — these need env-var auth, not browser
|
|
163
|
+
// OAuth. The full audit (cachy 2026-06-14) confirmed each agent's
|
|
164
|
+
// surface:
|
|
165
|
+
// claude → `claude auth login` (clean single-line URL output)
|
|
166
|
+
// codex → `codex login` (clean single-line URL output)
|
|
167
|
+
// agy → `agy --print init` (triggers OAuth, clean URL)
|
|
168
|
+
// We use the explicit auth subcommand instead of bare invocation —
|
|
169
|
+
// that's mac's bug D fix root cause. Bare `claude` drops into a TUI
|
|
170
|
+
// with line wraps + winsize sensitivity that broke URL extraction
|
|
171
|
+
// across alpha.3 → alpha.6. The official auth subcommand prints a
|
|
172
|
+
// plain single-line URL on stdout, no TUI involved.
|
|
173
|
+
function loginCommand(agent) {
|
|
174
|
+
if (agent === 'claude') return ['claude', 'auth', 'login'];
|
|
175
|
+
if (agent === 'codex') return ['codex', 'login'];
|
|
176
|
+
if (agent === 'agy') return ['agy', '--print', 'init'];
|
|
177
|
+
return null; // gemini, qwen, opencode — env-based
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Per-agent env-var hint for the no-OAuth agents.
|
|
181
|
+
function envHint(agent) {
|
|
182
|
+
if (agent === 'gemini') return 'GEMINI_API_KEY';
|
|
183
|
+
if (agent === 'qwen') return 'OPENAI_API_KEY (qwen --auth-type openai)';
|
|
184
|
+
if (agent === 'opencode') return 'per-provider keys in opencode config';
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
150
188
|
function runLogin(container, agent) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
// PTY
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
189
|
+
const cmd = loginCommand(agent);
|
|
190
|
+
if (!cmd) {
|
|
191
|
+
const env = envHint(agent);
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
`crosstalk chat --login --agent ${agent}: this agent has no browser OAuth flow.\n` +
|
|
194
|
+
(env ? ` Use environment-based auth instead:\n crosstalk auth paste --agent ${agent}\n (or set ${env} directly in docker-compose.yml)\n` : '') +
|
|
195
|
+
` Documented in GUIDE-CLI.md § Agent auth.\n`,
|
|
196
|
+
);
|
|
197
|
+
return Promise.resolve(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Pass host terminal dims explicitly. Docker exec -t normally sizes
|
|
201
|
+
// the container PTY off its stdout terminal, but we PIPE stdout (to
|
|
202
|
+
// scrape the OAuth URL), so docker has no TTY to size from and falls
|
|
203
|
+
// back to 80×24 — that's mac's bug D root cause. Setting COLUMNS/LINES
|
|
204
|
+
// env vars gets the agent CLI to render at the right size from the
|
|
205
|
+
// start. (Live SIGWINCH updates aren't propagated — docker env is
|
|
206
|
+
// set-at-spawn — but OAuth flows are short; operators don't typically
|
|
207
|
+
// resize the terminal mid-auth.)
|
|
208
|
+
const { cols, rows } = hostTermDims();
|
|
209
|
+
|
|
168
210
|
return new Promise((resolve) => {
|
|
169
211
|
const child = spawn(
|
|
170
212
|
'docker',
|
|
171
|
-
['exec', '-it',
|
|
213
|
+
['exec', '-it',
|
|
214
|
+
'-e', `COLUMNS=${cols}`,
|
|
215
|
+
'-e', `LINES=${rows}`,
|
|
216
|
+
'-e', 'TERM=xterm-256color',
|
|
217
|
+
container, ...cmd],
|
|
172
218
|
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
173
219
|
);
|
|
220
|
+
|
|
221
|
+
// SIGWINCH handler: docker env is set-at-spawn so we can't dynamically
|
|
222
|
+
// resize the in-container PTY without a control channel (would need
|
|
223
|
+
// node-pty or a sidecar process). Best-effort: surface that a resize
|
|
224
|
+
// happened so the operator knows why the TUI may not have responded.
|
|
225
|
+
const onWinch = () => {
|
|
226
|
+
const { cols: c, rows: r } = hostTermDims();
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`\n[crosstalk chat] Terminal resized to ${c}×${r} — in-container PTY won't update mid-session. ` +
|
|
229
|
+
`Exit (Ctrl-C) and retry if rendering is off.\n`,
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
process.on('SIGWINCH', onWinch);
|
|
233
|
+
|
|
174
234
|
let opened = false;
|
|
175
235
|
let buffer = '';
|
|
176
236
|
child.stdout.on('data', (chunk) => {
|
|
@@ -187,16 +247,22 @@ function runLogin(container, agent) {
|
|
|
187
247
|
: `\n[crosstalk chat] Could not auto-open browser. Open this URL manually:\n ${url}\n`,
|
|
188
248
|
);
|
|
189
249
|
});
|
|
190
|
-
child.on('exit', (code) =>
|
|
191
|
-
|
|
250
|
+
child.on('exit', (code) => {
|
|
251
|
+
process.removeListener('SIGWINCH', onWinch);
|
|
252
|
+
resolve(code ?? 0);
|
|
253
|
+
});
|
|
254
|
+
child.on('error', () => {
|
|
255
|
+
process.removeListener('SIGWINCH', onWinch);
|
|
256
|
+
resolve(1);
|
|
257
|
+
});
|
|
192
258
|
});
|
|
193
259
|
}
|
|
194
260
|
|
|
195
261
|
export async function run(argv) {
|
|
196
262
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
197
263
|
|
|
198
|
-
const
|
|
199
|
-
const
|
|
264
|
+
const { name } = requireInitialized(argv);
|
|
265
|
+
const api = apiFor(argv);
|
|
200
266
|
|
|
201
267
|
if (has(argv, '--shell')) {
|
|
202
268
|
return runInteractive(name, 'bash');
|
package/commands/down.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
// crosstalk down — stop
|
|
1
|
+
// crosstalk down — stop a transport's container, keep storage.
|
|
2
|
+
//
|
|
3
|
+
// alpha.6: name-resolved. --volumes flag retires (no named volumes in
|
|
4
|
+
// the bind-mount model). Reversible — next `up` resumes against the
|
|
5
|
+
// preserved <base>/<name>/ storage.
|
|
2
6
|
|
|
3
7
|
import { existsSync } from 'fs';
|
|
4
8
|
import { spawnSync } from 'child_process';
|
|
5
|
-
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
9
|
import { has } from '../lib/argv.js';
|
|
10
|
+
import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
|
|
7
11
|
|
|
8
12
|
function usage(exit = 0) {
|
|
9
13
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
14
|
w.write(
|
|
11
|
-
`Usage: crosstalk down [--
|
|
15
|
+
`Usage: crosstalk down [--containername <name>]
|
|
12
16
|
|
|
13
|
-
Stops the engine container for
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
Stops the engine container for a transport. Storage at <base>/<name>/
|
|
18
|
+
is preserved — next 'crosstalk up' resumes against it. Use
|
|
19
|
+
'crosstalk rm' if you want to wipe storage as well.
|
|
16
20
|
`,
|
|
17
21
|
);
|
|
18
22
|
process.exit(exit);
|
|
@@ -21,17 +25,16 @@ installed CLIs + auth state; you'll have to re-install + re-auth).
|
|
|
21
25
|
export async function run(argv) {
|
|
22
26
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const { name, paths } = requireInitialized(argv);
|
|
29
|
+
if (!existsSync(paths.composeFile)) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
`crosstalk down: '${name}' has no compose file (never brought up). Nothing to do.\n`,
|
|
32
|
+
);
|
|
29
33
|
return 0;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const r = spawnSync('docker', args, { cwd: root, stdio: 'inherit' });
|
|
36
|
+
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
});
|
|
36
39
|
return r.status ?? 1;
|
|
37
40
|
}
|