@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 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
- // Conservative URL regex: matches the leading https://...token boundary,
32
- // stops at whitespace/quote/angle. False positives are bounded to --login
33
- // mode where the first URL printed is almost certainly the auth URL.
34
- const URL_RE = /https?:\/\/[^\s<>"']+/;
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. Operator can pass
111
- // --agent <name> to target a specific one.
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
- const match = chunk.toString().match(URL_RE);
129
- if (!match) return;
130
- const url = match[0];
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.3';
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.3 -f server/Dockerfile .
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.3 crosstalk init mytransport
39
+ CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.4 crosstalk init mytransport
40
40
  `,
41
41
  );
42
42
  process.exit(exit);
@@ -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
- process.stdout.write(' (no model CLIs found inside the container install one or edit data/models.yaml)\n');
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.3';
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",
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",