@cordfuse/crosstalk 7.0.0-alpha.3 → 7.0.0-alpha.4
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/commands/chat.js +67 -16
- package/commands/init.js +3 -3
- package/commands/status.js +16 -1
- package/commands/up.js +1 -1
- package/package.json +1 -1
package/commands/chat.js
CHANGED
|
@@ -28,10 +28,59 @@ import { api, 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'];
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
31
|
+
|
|
32
|
+
// URL extraction that survives agent TUI rendering. Naive regex against
|
|
33
|
+
// raw stdout fails because agent CLIs (Claude in particular) draw a box
|
|
34
|
+
// around the URL with line wraps every ~N chars; the captured URL is
|
|
35
|
+
// truncated at the first wrap, with ANSI control chars leaking in at
|
|
36
|
+
// the cut point. Real example from alpha.3 macOS test: extracted URL
|
|
37
|
+
// ended with `%18%6b…` (CAN char + box-drawing artifact) instead of
|
|
38
|
+
// the `redirect_uri=…` param, breaking OAuth.
|
|
39
|
+
//
|
|
40
|
+
// Approach:
|
|
41
|
+
// 1. ANSI-strip everything (CSI / OSC / SS3 escapes).
|
|
42
|
+
// 2. Strip control chars (0x00-0x1F except LF, plus DEL).
|
|
43
|
+
// 3. Join line wraps that occur INSIDE a URL-safe character run —
|
|
44
|
+
// `<url-char>\n<url-char>` → `<url-char><url-char>`. Preserves real
|
|
45
|
+
// `<url-char>\n<non-url-char>` boundaries.
|
|
46
|
+
// 4. Match `https?://<url-chars>+` against the joined buffer.
|
|
47
|
+
// 5. Only fire when the character AFTER the matched URL is a true
|
|
48
|
+
// terminator (whitespace, end-of-buffer) — guards against acting
|
|
49
|
+
// on a partial URL still being written.
|
|
50
|
+
|
|
51
|
+
const URL_SAFE_CLASS = "a-zA-Z0-9%/?#&=+\\-._~:@!$()*,;";
|
|
52
|
+
const URL_RE = new RegExp(`https?://[${URL_SAFE_CLASS}]+`);
|
|
53
|
+
|
|
54
|
+
function extractCompleteUrl(rawBuffer) {
|
|
55
|
+
// Strip ANSI CSI escapes (\x1B[ ... terminator).
|
|
56
|
+
let clean = rawBuffer.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, '');
|
|
57
|
+
// Strip ANSI OSC escapes (\x1B] ... BEL or ST).
|
|
58
|
+
clean = clean.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
|
|
59
|
+
// Strip SS3 escapes (\x1BO + one char).
|
|
60
|
+
clean = clean.replace(/\x1BO./g, '');
|
|
61
|
+
// Strip remaining single-char escapes that slipped through.
|
|
62
|
+
clean = clean.replace(/\x1B[^a-zA-Z0-9]/g, '');
|
|
63
|
+
// Strip control chars except LF (0x0A); we use LF for line-wrap rejoin.
|
|
64
|
+
clean = clean.replace(/[\x00-\x09\x0B-\x1F\x7F]/g, '');
|
|
65
|
+
// Rejoin URL-character wrap points: `<url-char>\n<url-char>` becomes
|
|
66
|
+
// `<url-char><url-char>`. Doesn't merge `<url-char>\n<space>` etc.
|
|
67
|
+
const safeRe = new RegExp(`[${URL_SAFE_CLASS}]\\n[${URL_SAFE_CLASS}]`, 'g');
|
|
68
|
+
let prev;
|
|
69
|
+
do {
|
|
70
|
+
prev = clean;
|
|
71
|
+
clean = clean.replace(safeRe, (m) => m[0] + m[2]);
|
|
72
|
+
} while (clean !== prev);
|
|
73
|
+
|
|
74
|
+
const match = clean.match(URL_RE);
|
|
75
|
+
if (!match) return null;
|
|
76
|
+
// Require a terminator after the URL — without it, the URL may still
|
|
77
|
+
// be growing in subsequent stdout chunks. Terminator = whitespace,
|
|
78
|
+
// quote/angle, or end-of-buffer-with-trailing-newline.
|
|
79
|
+
const after = clean.slice(match.index + match[0].length);
|
|
80
|
+
if (after.length === 0) return null; // EOF-as-grow case, wait
|
|
81
|
+
if (!/^[\s<>"']/.test(after)) return null; // URL still continuing
|
|
82
|
+
return match[0];
|
|
83
|
+
}
|
|
35
84
|
|
|
36
85
|
function openInBrowser(url) {
|
|
37
86
|
const cmd =
|
|
@@ -107,27 +156,29 @@ function runLogin(container, agent) {
|
|
|
107
156
|
//
|
|
108
157
|
// No agent-specific "login" subcommand — we just run the agent's
|
|
109
158
|
// default interactive entry point. First-run auth flow happens
|
|
110
|
-
// automatically for every agent we support.
|
|
111
|
-
//
|
|
159
|
+
// automatically for every agent we support.
|
|
160
|
+
//
|
|
161
|
+
// No COLUMNS=1000 env: making the agent render the URL "unwrapped" via
|
|
162
|
+
// a wide terminal claim doesn't reliably propagate to the in-container
|
|
163
|
+
// PTY, and even when it does Claude's TUI hard-wraps anyway. The
|
|
164
|
+
// extractCompleteUrl() function joins wrapped URL fragments on the
|
|
165
|
+
// host side, so the agent's rendering width is irrelevant. As bonus,
|
|
166
|
+
// dropping COLUMNS=1000 fixes daily-chat soft-wrap weirdness in
|
|
167
|
+
// long-form prose (agents see the real host terminal width).
|
|
112
168
|
return new Promise((resolve) => {
|
|
113
169
|
const child = spawn(
|
|
114
170
|
'docker',
|
|
115
|
-
[
|
|
116
|
-
'exec', '-it',
|
|
117
|
-
'--env', 'COLUMNS=1000', // fallback for browser-open failure
|
|
118
|
-
'--env', 'LINES=40',
|
|
119
|
-
container,
|
|
120
|
-
agent,
|
|
121
|
-
],
|
|
171
|
+
['exec', '-it', container, agent],
|
|
122
172
|
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
123
173
|
);
|
|
124
174
|
let opened = false;
|
|
175
|
+
let buffer = '';
|
|
125
176
|
child.stdout.on('data', (chunk) => {
|
|
126
177
|
process.stdout.write(chunk);
|
|
127
178
|
if (opened) return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
179
|
+
buffer += chunk.toString();
|
|
180
|
+
const url = extractCompleteUrl(buffer);
|
|
181
|
+
if (!url) return;
|
|
131
182
|
opened = true;
|
|
132
183
|
const ok = openInBrowser(url);
|
|
133
184
|
process.stdout.write(
|
package/commands/init.js
CHANGED
|
@@ -21,7 +21,7 @@ import { spawnSync } from 'child_process';
|
|
|
21
21
|
import { has, positionals } from '../lib/argv.js';
|
|
22
22
|
|
|
23
23
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
24
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
24
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.4';
|
|
25
25
|
|
|
26
26
|
function usage(exit = 0) {
|
|
27
27
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
@@ -34,9 +34,9 @@ function usage(exit = 0) {
|
|
|
34
34
|
|
|
35
35
|
The image must be pullable (or already present locally). For local dev
|
|
36
36
|
before the GHCR publish pipeline ships, build the image first:
|
|
37
|
-
docker build -t crosstalk-server:7.0.0-alpha.
|
|
37
|
+
docker build -t crosstalk-server:7.0.0-alpha.4 -f server/Dockerfile .
|
|
38
38
|
Then run:
|
|
39
|
-
CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.
|
|
39
|
+
CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.4 crosstalk init mytransport
|
|
40
40
|
`,
|
|
41
41
|
);
|
|
42
42
|
process.exit(exit);
|
package/commands/status.js
CHANGED
|
@@ -18,7 +18,22 @@ export async function run(_argv) {
|
|
|
18
18
|
|
|
19
19
|
process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
|
|
20
20
|
if (s.claimed_models.length === 0) {
|
|
21
|
-
|
|
21
|
+
// Two failure shapes look the same in claimed_models=0: (a) no CLIs
|
|
22
|
+
// installed at all, (b) CLIs installed but data/models.yaml has no
|
|
23
|
+
// entries referencing them. Ask /agents/installed to distinguish so
|
|
24
|
+
// the operator gets a directly-actionable hint.
|
|
25
|
+
let installed = null;
|
|
26
|
+
try {
|
|
27
|
+
const r = await api.get('/agents/installed');
|
|
28
|
+
installed = r.installed;
|
|
29
|
+
} catch { /* ignore — fall back to generic message */ }
|
|
30
|
+
if (installed && installed.length > 0) {
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
` (${installed.join(', ')} installed in the container but no data/models.yaml entry references them — add one to claim)\n`,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/models.yaml)\n');
|
|
36
|
+
}
|
|
22
37
|
} else {
|
|
23
38
|
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
|
24
39
|
}
|
package/commands/up.js
CHANGED
|
@@ -16,7 +16,7 @@ import { requireTransportRoot, transportName, composeFile } from '../lib/transpo
|
|
|
16
16
|
import { has } from '../lib/argv.js';
|
|
17
17
|
|
|
18
18
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
19
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
19
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.4';
|
|
20
20
|
const DEFAULT_API_PORT = Number(process.env.CROSSTALK_API_PORT) || 7000;
|
|
21
21
|
|
|
22
22
|
function usage(exit = 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.4",
|
|
4
4
|
"description": "Crosstalk client — host-side CLI that talks to the crosstalkd daemon running inside the crosstalk-server container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|