@lardexx/arenacode 2.11.2 → 2.11.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/README.md CHANGED
@@ -17,13 +17,13 @@ you ──▶ arenacode CLI ──▶ arena-bridge ──▶ agent
17
17
 
18
18
  ```bash
19
19
  npm install -g @lardexx/arenacode
20
- arenacode --version # 2.5.2 (the bridge protocol version this client speaks)
20
+ arenacode --version # the version of the installed package
21
21
  ```
22
22
 
23
23
  Or without installing:
24
24
 
25
25
  ```bash
26
- npx @lardexx/arenacode ask "list the files in my folder"
26
+ npx @lardexx/arenacode # opens the interface (needs a terminal)
27
27
  ```
28
28
 
29
29
  Published as [`@lardexx/arenacode`](https://www.npmjs.com/package/@lardexx/arenacode) — the bare name `arenacode`
@@ -42,10 +42,12 @@ rebuilt from opencode 1.19.5's own source (`packages/tui/src/component/prompt`,
42
42
 
43
43
  ```bash
44
44
  arenacode # opens the TUI on a terminal; on the very first run it asks for URL + key first
45
- arenacode ui # same thing, explicitly
46
- arenacode chat # the plain line-by-line client, if you really want one
47
45
  ```
48
46
 
47
+ The client has **one entry point**: `arenacode`. There are no subcommands — setup, settings, Stop and the bridge
48
+ inspection all live inside the interface (the settings live behind `/settings` or the command palette). Anything else on the command line is refused
49
+ with `command removed: <name>` and exit code 2.
50
+
49
51
  Home screen — two-tone block wordmark, all lowercase (single-storey `a`, stem-and-arm `r`), centred prompt, tips, and the folder you
50
52
  started in at the bottom left:
51
53
 
@@ -181,7 +183,7 @@ terminal **Accessibility** (input) and **Screen Recording** (pixels) in System S
181
183
  On macOS/Linux the accessibility tree is not read: `ui state` returns windows and geometry as elements.
182
184
 
183
185
  **Ask and Proceed / Proceed.** The desktop's Settings switch, `ctrl+x a` here (also `/ask`, `/proceed`,
184
- `--proceed`, and `arenacode config set approval proceed`). The default, **Ask and Proceed**, asks before commands,
186
+ `--proceed`, and the Settings panel). The default, **Ask and Proceed**, asks before commands,
185
187
  file changes and plans; in Computer mode it asks once, on the plan, and after that the commands run on their own
186
188
  while administrator commands still ask. **Proceed** approves its own plan, runs commands and installations and
187
189
  writes files without asking: turning it on shows the desktop's own warning (*"Pericolo: modalità Proceed"*).
@@ -198,46 +200,30 @@ terminal confirmation, `--confirm-writes` adds one for writes and removals.
198
200
  ## Quick start
199
201
 
200
202
  ```bash
201
- # 1. point the client at your bridge (URL + API key come from the bridge's runtime/connection.txt)
202
- arenacode init
203
-
204
- # 2. open the TUI in the folder you want the agent to work on — just cd there first
205
- cd ~/progetto && arenacode
203
+ # 1. cd into the folder you want the agent to work on
204
+ cd ~/progetto
206
205
 
207
- # 3. or stay in the plain CLI
208
- arenacode ask "read package.json and add a lint script"
209
- arenacode chat
206
+ # 2. open the client
207
+ arenacode
210
208
  ```
211
209
 
212
- `init` saves the URL and the API key (the folder is never stored — arenacode works in the directory you start
213
- it from), then tests `/health` and `/auth/check` so you know the
214
- bridge accepts the key before you send anything.
210
+ On the first run the interface asks for the bridge URL and the API key (both are in the bridge's
211
+ `runtime/connection.txt`), saves them, and tests `/health` and `/auth/check` so you know the bridge accepts the
212
+ key before you send anything. The folder is never stored: arenacode works in the directory you start it from.
213
+ Later, `/settings` (or the command palette, `ctrl+p`) reopens them inside the interface.
215
214
 
216
215
  ## Commands
217
216
 
218
217
  | Command | What it does |
219
218
  |---|---|
220
- | `arenacode init` | configure URL and API key; test the connection |
221
- | `arenacode config [show\|path\|set <k> <v>\|clear]` | read or change the saved configuration |
222
- | `arenacode ping` | `/health` + `/auth/check`: version, public URL, queue, key |
223
- | `arenacode doctor` | local checks: node, platform, folder, git, bridge, API key |
224
- | `arenacode` (no args) | full-screen TUI — the default on a terminal |
225
- | `arenacode ui` | full-screen TUI, explicitly |
226
- | `arenacode ask "<instruction>"` | one task, streamed timeline, exit when it finishes |
227
- | `arenacode chat` | interactive session (`/help` shows the in-session commands) |
228
- | `arenacode stop [task-id]` | Stop: cancels the turn and the queued tasks on the bridge |
229
- | `arenacode handshake` | vision/handshake check task, like the client's |
230
-
231
- **Inspect the bridge**
219
+ | `arenacode` | the interface: timeline, input box, cards, settings, Stop, bridge inspection |
220
+ | `arenacode --help` | the options below |
221
+ | `arenacode --version` | the version of the installed package |
232
222
 
233
- | Command | What it does |
234
- |---|---|
235
- | `arenacode tasks [state]` | task queue (`all`, `claimable`, `pending`, `claimed`, `done`, `error`) |
236
- | `arenacode status <task-id>` | state, claimant, result of one task |
237
- | `arenacode events <task-id>` | replayed timeline of a task |
238
- | `arenacode files [path]` | bridge workspace listing (`GET /fs`) |
239
- | `arenacode cat <path>` | preview a text file in the bridge workspace (`GET /file`) |
240
- | `arenacode attach <file…>` | upload files into the bridge workspace (`POST /upload`) |
223
+ Everything else — URL and API key, settings, the task queue, task state and timeline, files on the bridge,
224
+ file uploads, Stop — is inside the interface. Up to 2.11.2 these were subcommands (`init`, `config`, `ping`,
225
+ `doctor`, `ask`, `chat`, `ui`, `tasks`, `status`, `events`, `stop`, `files`, `cat`, `attach`, `handshake`);
226
+ they were removed on request, and the terminal has to be interactive: in a pipe the client prints why and exits.
241
227
 
242
228
  **Options**
243
229
 
@@ -321,7 +307,6 @@ Pin it with any of:
321
307
 
322
308
  ```
323
309
  arenacode --shell zsh
324
- arenacode config set shell zsh
325
310
  /shell # in the TUI: pick from the shells installed on this machine
326
311
  ```
327
312
 
@@ -359,7 +344,7 @@ release it on the bridge with `keys unbind <ID>`).
359
344
 
360
345
  ## Configuration
361
346
 
362
- `arenacode config path` prints the file:
347
+ The file lives at:
363
348
 
364
349
  * Windows — `%APPDATA%\ArenaCode\config.json`
365
350
  * macOS / Linux — `~/.arenacode/config.json`
@@ -390,11 +375,11 @@ Environment variables override the file: `ARENACODE_URL`, `ARENACODE_KEY`, `AREN
390
375
 
391
376
  | Symptom | Fix |
392
377
  |---|---|
393
- | `auth failed: unauthorized` | wrong API key — `arenacode config set key <key>` (from `runtime/connection.txt`) |
378
+ | `auth failed: unauthorized` | wrong API key — `/settings` in the interface, paste the key from `runtime/connection.txt` |
394
379
  | `403 device_mismatch` | key bound to another PC — on the bridge: `python3 bridge_server.py keys unbind <ID>` |
395
- | `bridge unreachable` | tunnel down or wrong URL — check `arenacode ping`, and `/health` on the bridge |
380
+ | `bridge unreachable` | tunnel down or wrong URL — check `/health` on the bridge, then `/settings` in the interface |
396
381
  | agent never answers | the agent is not listening: run `bash listen.sh` in the bridge folder |
397
- | everything hangs after a turn | `--idle-timeout` closes the session; check `arenacode status <id>` |
382
+ | everything hangs after a turn | `--idle-timeout` closes the session; the task state is in the interface |
398
383
 
399
384
  ## Development
400
385
 
@@ -8,7 +8,7 @@ made the TUI unreachable without typing `ui`; a bare invocation now opens the in
8
8
  interface asks for URL + key instead of printing a dead-end notice.
9
9
 
10
10
  2.5.4 made the hints launch-aware: run through npx and every message prints the command you can actually paste
11
- (`npx @lardexx/arenacode init`), plus the one-line way to get the bare `arenacode` command.
11
+ (`npx @lardexx/arenacode`), plus the one-line way to get the bare `arenacode` command.
12
12
 
13
13
  ```bash
14
14
  npm install -g @lardexx/arenacode # then: arenacode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lardexx/arenacode",
3
- "version": "2.11.2",
3
+ "version": "2.11.4",
4
4
  "description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands, file operations and computer automation on your PC (windows-use.dll on Windows, Xlib/XTest on Linux, osascript on macOS). Node port of the ArenaCode desktop client.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -20,7 +20,7 @@
20
20
  "vendor/"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
23
+ "test": "node test/cli-surface.js && node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
24
24
  },
25
25
  "keywords": [
26
26
  "arenacode",
package/src/cli.js CHANGED
@@ -114,7 +114,8 @@ function requireUrl(ctx) {
114
114
  if (ctx.url) return true;
115
115
  write('');
116
116
  write(c.red(' no bridge URL configured.'));
117
- write(` ${c.gray('run:')} ${SELF} init ${c.gray('or')} ${SELF} config set url https://your-bridge.tunnl.gg`);
117
+ write(` ${c.gray('run')} ${SELF} ${c.gray('on a terminal: the first run asks for the URL and the API key.')}`);
118
+ write(` ${c.gray('the URL and the key are also in the bridge runtime/connection.txt;')} ${c.gray('later:')} /settings`);
118
119
  if (SELF !== 'arenacode') write(` ${c.gray('to get the bare command:')} ${INSTALL_HINT}`);
119
120
  return false;
120
121
  }
@@ -133,7 +134,7 @@ async function ensureAuth(ctx) {
133
134
  // commands
134
135
  // --------------------------------------------------------------------------- //
135
136
 
136
- async function cmdInit(flags, { tail = true } = {}) {
137
+ async function firstRunSetup(flags, { tail = true } = {}) {
137
138
  banner(VERSION);
138
139
  const cfg = readConfig();
139
140
  write(c.gray(` config file: ${configPath()}`));
@@ -179,105 +180,6 @@ async function cmdInit(flags, { tail = true } = {}) {
179
180
  return 0;
180
181
  }
181
182
 
182
- function cmdConfig(positional, flags) {
183
- const [action = 'show', key, ...rest] = positional;
184
- const cfg = readConfig();
185
- if (action === 'path') {
186
- write(configPath());
187
- return 0;
188
- }
189
- if (action === 'show') {
190
- const shown = { ...cfg, key: cfg.key ? `${cfg.key.slice(0, 8)}…` : '' };
191
- write(JSON.stringify(shown, null, 2));
192
- write(c.gray(`# ${configPath()}`));
193
- return 0;
194
- }
195
- if (action === 'set') {
196
- const name = key;
197
- const val = rest.join(' ');
198
- if (!name || val === '') {
199
- write(`usage: ${SELF} config set <url|key|lang|mode|approval|pc_access|shell> <value>`);
200
- return 2;
201
- }
202
- if (name === 'approval' && !['ask', 'proceed'].includes(String(val))) {
203
- write(c.red(' ✗ approval must be ask or proceed'));
204
- write(c.gray(` ask ${'Ask and Proceed'}: the client asks before commands, file changes and plans`));
205
- write(c.gray(` proceed Proceed: the client does everything on its own`));
206
- return 2;
207
- }
208
- const patch = {};
209
- if (name === 'pc_access') patch.pc_access = !['false', '0', 'no', 'off'].includes(String(val).toLowerCase());
210
- else if (name === 'url') patch.url = String(val).replace(/\/+$/, '');
211
- else patch[name] = val;
212
- const file = patchConfig(patch);
213
- write(` ${c.green('✓')} ${name} saved ${c.gray(file)}`);
214
- if (name === 'approval' && patch.approval === 'proceed') {
215
- const d = dangerText(cfg.lang);
216
- write('');
217
- write(` ${c.bold(c.red('⚠ ' + d.title))}`);
218
- write(indentLines(c.yellow(d.body), ' '));
219
- }
220
- return 0;
221
- }
222
- if (action === 'clear') {
223
- try {
224
- fs.rmSync(configPath());
225
- } catch {
226
- /* ignore */
227
- }
228
- write(` ${c.green('✓')} config removed`);
229
- return 0;
230
- }
231
- write(`usage: ${SELF} config [show|path|set <key> <value>|clear]`);
232
- return 2;
233
- }
234
-
235
- async function cmdPing(flags) {
236
- const ctx = context(flags);
237
- if (!requireUrl(ctx)) return 1;
238
- const health = await ctx.bridge.health();
239
- if (health.status !== 200) {
240
- write(c.red(` ✗ bridge unreachable: ${(health.json && health.json.error) || health.status}`));
241
- return 1;
242
- }
243
- const h = health.json;
244
- if (ctx.json) {
245
- write(JSON.stringify(h, null, 2));
246
- return 0;
247
- }
248
- write('');
249
- write(` ${c.green('●')} bridge ${c.bold('v' + h.version)} ${c.gray(`· executor=${h.executor_mode} · terminal=${h.terminal ? 'on' : 'off'}`)}`);
250
- write(` ${c.gray('url')} ${h.public_url || c.gray('(no tunnel)')}`);
251
- write(` ${c.gray('tasks')} ${h.tasks.claimable} claimable · ${h.tasks.pending} pending · ${h.tasks.done} done`);
252
- const auth = await ctx.bridge.authCheck();
253
- write(` ${auth.status === 200 ? c.green('✓') : c.red('✗')} auth ${auth.status === 200 ? 'ok' : (auth.json && auth.json.error) || auth.status}`);
254
- return auth.status === 200 ? 0 : 1;
255
- }
256
-
257
- async function cmdDoctor(flags) {
258
- const ctx = context(flags);
259
- write('');
260
- write(` ${c.bold('arenacode doctor')}`);
261
- const line = (ok, label, detail = '') => write(` ${ok ? c.green('✓') : c.red('✗')} ${label.padEnd(18)} ${c.gray(detail)}`);
262
- line(Number(process.versions.node.split('.')[0]) >= 18, 'node', process.version);
263
- line(true, 'platform', `${process.platform} ${os.release()} ${os.arch()}`);
264
- line(Boolean(ctx.url), 'bridge url', ctx.url || 'not configured');
265
- line(true, 'config', configPath());
266
- line(/^[A-Za-z0-9-]{8,128}$/.test(deviceId()), 'device id', deviceId().slice(0, 12) + '…');
267
- line(fs.existsSync(ctx.folder), 'open folder', ctx.folder);
268
- line(Boolean(require('./meta').which('git')), 'git', require('./meta').which('git') ? 'available' : 'missing');
269
- if (ctx.url) {
270
- const health = await ctx.bridge.health();
271
- line(health.status === 200, 'bridge', health.status === 200 ? `v${health.json.version} ${health.json.public_url || ''}` : (health.json && health.json.error) || 'unreachable');
272
- if (health.status === 200) {
273
- const auth = await ctx.bridge.authCheck();
274
- line(auth.status === 200, 'api key', auth.status === 200 ? 'accepted' : (auth.json && auth.json.error) || String(auth.status));
275
- }
276
- }
277
- write('');
278
- return 0;
279
- }
280
-
281
183
  function agentPrompt() {
282
184
  try {
283
185
  return fs.readFileSync(AGENT_PROMPT_FILE, 'utf8');
@@ -383,12 +285,17 @@ async function runInstruction(ctx, text, { attachments = [] } = {}) {
383
285
  }
384
286
  }
385
287
 
386
- async function cmdAsk(positional, flags) {
288
+ /**
289
+ * Runs one instruction and streams the timeline. Internal on purpose: the client no
290
+ * longer exposes subcommands, `arenacode` opens the TUI and this is the code path the
291
+ * TUI itself uses (and the integration test).
292
+ */
293
+ async function runOnce(instruction, flags = {}) {
387
294
  const ctx = context(flags);
388
295
  if (!requireUrl(ctx)) return 1;
389
- const text = positional.join(' ').trim();
296
+ const text = String(Array.isArray(instruction) ? instruction.join(' ') : instruction || '').trim();
390
297
  if (!text) {
391
- write('usage: arenacode ask "your instruction"');
298
+ write(c.red(' no instruction'));
392
299
  return 2;
393
300
  }
394
301
  if (!(await ensureAuth(ctx))) return 1;
@@ -414,7 +321,7 @@ async function cmdUi(flags) {
414
321
  write('');
415
322
  write(` ${c.bold('first run')} ${c.gray('· bridge URL and API key (Enter keeps the value in brackets)')}`);
416
323
  write('');
417
- await cmdInit(flags, { tail: false });
324
+ await firstRunSetup(flags, { tail: false });
418
325
  write('');
419
326
  ctx = context(flags);
420
327
  if (!ctx.url) return 1; // setup was abandoned: nothing to open
@@ -435,354 +342,17 @@ async function cmdUi(flags) {
435
342
  return 0;
436
343
  }
437
344
 
438
- async function cmdChat(flags) {
439
- const ctx = context(flags);
440
- if (!requireUrl(ctx)) return 1;
441
- if (!(await ensureAuth(ctx))) return 1;
442
- banner(VERSION);
443
- write(` ${c.gray('bridge')} ${ctx.url} ${c.gray('folder')} ${ctx.folder}`);
444
- write(` ${c.gray('type a request · /help for commands · Ctrl+D to quit')}`);
445
- write('');
446
-
447
- const readline = require('node:readline');
448
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY });
449
- const prompt = () => {
450
- rl.setPrompt(c.cyan(' › '));
451
- rl.prompt();
452
- };
453
-
454
- return new Promise((resolve) => {
455
- prompt();
456
- rl.on('line', async (line) => {
457
- const input = line.trim();
458
- if (!input) {
459
- prompt();
460
- return;
461
- }
462
- rl.pause();
463
- try {
464
- if (input === '/quit' || input === '/exit') {
465
- rl.close();
466
- return;
467
- }
468
- if (input === '/help') {
469
- write(` ${c.gray('/files [path]')} list the bridge workspace ${c.gray('/cat <path>')} preview a file`);
470
- write(` ${c.gray('/mode agent|plan')} switch mode ${c.gray('/lang it|en|es')} interface language`);
471
- write(` ${c.gray('/folder <dir>')} change the open folder ${c.gray('/ping')} bridge status`);
472
- write(` ${c.gray('/tasks')} queue ${c.gray('/stop <id>')} cancel a turn ${c.gray('/quit')}`);
473
- write('');
474
- } else if (input === '/ping') {
475
- await cmdPing({ ...flags });
476
- } else if (input === '/tasks') {
477
- await cmdTasks([], flags);
478
- } else if (input.startsWith('/files')) {
479
- await cmdFiles(input.split(/\s+/).slice(1), flags);
480
- } else if (input.startsWith('/cat ')) {
481
- await cmdCat([input.slice(5).trim()], flags);
482
- } else if (input.startsWith('/mode ')) {
483
- const mode = input.slice(6).trim() === 'plan' ? 'plan' : 'agent';
484
- patchConfig({ mode });
485
- ctx.mode = mode;
486
- write(` ${c.green('✓')} mode ${mode}`);
487
- } else if (input.startsWith('/lang ')) {
488
- const lang = input.slice(6).trim().slice(0, 2);
489
- patchConfig({ lang });
490
- ctx.lang = lang;
491
- write(` ${c.green('✓')} lang ${lang}`);
492
- } else if (input.startsWith('/folder ')) {
493
- const folder = path.resolve(input.slice(8).trim().replace(/^"|"$/g, ''));
494
- patchConfig({ folder });
495
- ctx.folder = folder;
496
- write(` ${c.green('✓')} folder ${folder}`);
497
- } else if (input.startsWith('/stop')) {
498
- const id = input.split(/\s+/)[1];
499
- if (id) await cmdStop([id], flags);
500
- else write(c.gray(' usage: /stop <task-id>'));
501
- } else {
502
- await runInstruction(ctx, input);
503
- }
504
- } catch (e) {
505
- write(c.red(` ✗ ${e.message}`));
506
- }
507
- write('');
508
- rl.resume();
509
- prompt();
510
- });
511
- rl.on('close', () => {
512
- write('');
513
- resolve(0);
514
- });
515
- });
516
- }
517
-
518
- async function cmdTasks(positional, flags) {
519
- const ctx = context(flags);
520
- if (!requireUrl(ctx)) return 1;
521
- const state = positional[0] || flags.state || 'all';
522
- const res = await ctx.bridge.listTasks(state);
523
- if (res.status !== 200) {
524
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
525
- return 1;
526
- }
527
- if (ctx.json) {
528
- write(JSON.stringify(res.json, null, 2));
529
- return 0;
530
- }
531
- const tasks = res.json.tasks || [];
532
- if (!tasks.length) {
533
- write(c.gray(' (no tasks)'));
534
- return 0;
535
- }
536
- for (const task of tasks) {
537
- const when = new Date(task.created_at * 1000).toLocaleString();
538
- const stateTone = task.state === 'done' ? 'green' : task.state === 'error' ? 'red' : task.state === 'cancelled' ? 'yellow' : 'cyan';
539
- write(` ${c[stateTone](task.state.padEnd(9))} ${c.gray(when)} ${c.bold(task.id)} ${c.gray(truncate(String(task.instruction).split('\n').slice(-1)[0], 60))}`);
540
- }
541
- return 0;
542
- }
543
-
544
- async function cmdStatus(positional, flags) {
545
- const ctx = context(flags);
546
- if (!requireUrl(ctx)) return 1;
547
- let id = positional[0];
548
- if (!id) {
549
- const latest = await latestTaskId(ctx);
550
- if (latest.error) {
551
- write(c.red(` ✗ ${latest.error}`));
552
- return 1;
553
- }
554
- id = latest.id;
555
- if (!id) {
556
- write(c.gray(' no tasks on the bridge yet — run: arenacode ask "…"'));
557
- return 1;
558
- }
559
- write(c.gray(` latest task: ${id}`));
560
- }
561
- const res = await ctx.bridge.getTask(id);
562
- if (res.status !== 200) {
563
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
564
- return 1;
565
- }
566
- if (ctx.json) {
567
- write(JSON.stringify(res.json, null, 2));
568
- return 0;
569
- }
570
- const t = res.json;
571
- write('');
572
- write(` ${c.bold(t.id)} ${c.cyan(t.state)} ${c.gray(new Date(t.created_at * 1000).toLocaleString())}`);
573
- if (t.claimant) write(` ${c.gray('claimant')} ${t.claimant}`);
574
- if (t.error) write(` ${c.red('error')} ${t.error}`);
575
- if (t.output) {
576
- write('');
577
- write(indentLines(String(t.output).trim(), ' │ '));
578
- }
579
- return 0;
580
- }
581
-
582
- async function cmdEvents(positional, flags) {
583
- const ctx = context(flags);
584
- if (!requireUrl(ctx)) return 1;
585
- let id = positional[0];
586
- if (!id) {
587
- const latest = await latestTaskId(ctx);
588
- if (latest.error) {
589
- write(c.red(` ✗ ${latest.error}`));
590
- return 1;
591
- }
592
- id = latest.id;
593
- if (!id) {
594
- write(c.gray(' no tasks on the bridge yet — run: arenacode ask "…"'));
595
- return 1;
596
- }
597
- write(c.gray(` latest task: ${id}`));
598
- }
599
- const res = await ctx.bridge.getTask(id);
600
- if (res.status !== 200) {
601
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
602
- return 1;
603
- }
604
- const { Timeline } = require('./turn');
605
- const timeline = new Timeline({ folder: ctx.folder, lang: ctx.lang });
606
- for (const ev of res.json.events || []) timeline.render(ev);
607
- timeline.newlineIfText();
608
- write(c.gray(` state: ${res.json.state}`));
609
- return 0;
610
- }
611
-
612
- async function cmdStop(positional, flags) {
613
- const ctx = context(flags);
614
- if (!requireUrl(ctx)) return 1;
615
- let id = positional[0];
616
- if (!id) {
617
- const res = await ctx.bridge.listTasks('all');
618
- const live = (res.json.tasks || []).filter((t) => !['done', 'error', 'cancelled'].includes(t.state)).pop();
619
- id = live && live.id;
620
- }
621
- if (!id) {
622
- write(c.gray(' nothing to stop'));
623
- return 0;
624
- }
625
- const res = await ctx.bridge.cancel(id);
626
- if (res.status !== 200) {
627
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
628
- return 1;
629
- }
630
- write(` ${c.yellow('■')} stopped ${id}`);
631
- return 0;
632
- }
633
-
634
- async function cmdFiles(positional, flags) {
635
- const ctx = context(flags);
636
- if (!requireUrl(ctx)) return 1;
637
- const rel = positional[0] || '';
638
- const res = await ctx.bridge.fs(rel);
639
- if (res.status !== 200) {
640
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
641
- return 1;
642
- }
643
- if (ctx.json) {
644
- write(JSON.stringify(res.json, null, 2));
645
- return 0;
646
- }
647
- write(c.gray(` ${res.json.root}/${res.json.path}`));
648
- for (const entry of res.json.entries) {
649
- const size = entry.type === 'dir' ? '<DIR>' : String(entry.size);
650
- write(` ${c.gray(size.padStart(9))} ${entry.type === 'dir' ? c.cyan(entry.name + '/') : entry.name}`);
651
- }
652
- return 0;
653
- }
654
-
655
- async function cmdCat(positional, flags) {
656
- const ctx = context(flags);
657
- if (!requireUrl(ctx)) return 1;
658
- const rel = positional[0];
659
- if (!rel) {
660
- write('usage: arenacode cat <path>');
661
- return 2;
662
- }
663
- const res = await ctx.bridge.file(rel);
664
- if (res.status !== 200) {
665
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
666
- return 1;
667
- }
668
- if (res.json.binary) {
669
- write(c.gray(` binary file (${res.json.size} B)`));
670
- return 0;
671
- }
672
- write(res.json.text);
673
- return 0;
674
- }
675
-
676
- async function cmdAttach(positional, flags) {
677
- const ctx = context(flags);
678
- if (!requireUrl(ctx)) return 1;
679
- if (!positional.length) {
680
- write('usage: arenacode attach <file> [more files]');
681
- return 2;
682
- }
683
- for (const file of positional) {
684
- const abs = path.resolve(file);
685
- if (!fs.existsSync(abs)) {
686
- write(c.yellow(` ! missing: ${file}`));
687
- continue;
688
- }
689
- const name = `attachments/${Date.now()}-${path.basename(abs)}`;
690
- const res = await ctx.bridge.upload(name, fs.readFileSync(abs));
691
- if (res.status !== 200) {
692
- write(c.red(` ✗ ${(res.json && res.json.error) || res.status}`));
693
- return 1;
694
- }
695
- write(` ${c.green('↑')} ${res.json.path} ${c.gray(`(${res.json.size} B)`)}`);
696
- }
697
- return 0;
698
- }
699
-
700
345
  /** Handshake task the desktop client sends to check the bridge and vision. */
701
- async function cmdHandshake(flags) {
702
- const ctx = context(flags);
703
- if (!requireUrl(ctx)) return 1;
704
- if (!(await ensureAuth(ctx))) return 1;
705
- const instruction =
706
- '[ArenaCode handshake] Vision check. The bridge creates the image, not you: do not write, copy or edit any PNG ' +
707
- 'or other image. Run `python3 agentctl.py vision-png <task id>`, open the PNG it prints and look at it, then run ' +
708
- '`python3 agentctl.py --has-vision-enabled:true <task id>` if you can see and describe it, or ' +
709
- '`--has-vision-enabled:false` if you cannot. Reply with one short sentence describing what you see, then go back ' +
710
- 'to listening.';
711
- write('');
712
- write(` ${c.bold('handshake')} ${c.gray('· vision check')}`);
713
- return runInstruction(ctx, instruction);
714
- }
715
-
716
346
 
717
347
  /** Short, command-specific usage: a missing argument must not send you to the full help page. */
718
- const USAGE = {
719
- init: [['init', 'configure bridge URL + API key, then test the connection']],
720
- config: [['config [show|path]', 'read the saved configuration'],
721
- ['config set <key> <value>', 'url · key · lang · mode · pc_access · shell']],
722
- ping: [['ping', 'bridge status and API key check']],
723
- doctor: [['doctor', 'local checks: node, platform, folder, git, bridge']],
724
- ask: [['ask "instruction"', 'run one task and stream the timeline']],
725
- ui: [['ui', 'full-screen TUI']],
726
- chat: [['chat', 'plain line-by-line session']],
727
- tasks: [['tasks [state]', 'task queue (all · claimable · pending · done)']],
728
- status: [['status [task-id]', 'state of a task (default: the latest one)']],
729
- events: [['events [task-id]', 'timeline of a task (default: the latest one)']],
730
- stop: [['stop [task-id]', 'cancel the running turn (default: the live one)']],
731
- files: [['files [path]', 'listing of the folder on the PC']],
732
- cat: [['cat <path>', 'preview a file of the folder on the PC']],
733
- attach: [['attach <file…>', 'upload files to the bridge workspace']],
734
- handshake: [['handshake', 'vision/handshake check task']],
735
- };
736
-
737
- function cmdUsage(name) {
738
- const rows = USAGE[name] || [];
739
- if (!rows.length) return help();
740
- banner(VERSION);
741
- for (const [u, why] of rows) {
742
- write(` ${c.bold('usage')} ${SELF} ${u}`);
743
- write(` ${c.gray(why)}`);
744
- }
745
- write('');
746
- write(` ${c.gray(`every option: ${SELF} --help`)}`);
747
- return 0;
748
- }
749
-
750
- /** `status` / `events` with no argument means "the task I just ran". */
751
- async function latestTaskId(ctx) {
752
- const res = await ctx.bridge.listTasks('all');
753
- if (res.status !== 200) return { error: (res.json && (res.json.error || res.json.hint)) || `HTTP ${res.status}` };
754
- const tasks = (res.json && res.json.tasks) || [];
755
- return { id: tasks.length ? tasks[tasks.length - 1].id : null };
756
- }
757
-
758
348
  function help() {
759
- banner(VERSION);
760
- write(` ${c.bold('usage')} arenacode <command> [options]`);
761
- if (SELF !== 'arenacode') {
762
- write(` ${c.gray('you are running it through npx — every command can be prefixed the same way,')}`);
763
- write(` ${c.gray(`or install it once: ${INSTALL_HINT}`)}`);
764
- }
765
349
  write('');
766
- write(` ${c.cyan('setup')}`);
767
- write(` init configure URL + API key, test the connection`);
768
- write(` config [show|set|path] read or change the saved configuration`);
769
- write(` ping bridge status and API key check`);
770
- write(` doctor local checks (node, git, folder, bridge)`);
350
+ write(` ${c.cyan('ArenaCode CLI')} ${c.gray(`v${VERSION}`)} ${c.gray('· terminal client for arena-bridge')}`);
771
351
  write('');
772
- write(` ${c.cyan('chat')}`);
773
- write(` ask "instruction" run one task and stream the timeline`);
774
- write(` ui full-screen TUI (default on a terminal)`);
775
- write(` chat plain line-by-line session`);
776
- write(` stop [task-id] cancel the running turn (Stop)`);
777
- write(` handshake vision/handshake check task`);
352
+ write(` usage ${c.bold('arenacode')} [options]`);
778
353
  write('');
779
- write(` ${c.cyan('inspect')}`);
780
- write(` tasks [state] task queue on the bridge`);
781
- write(` status <task-id> state and result of a task`);
782
- write(` events <task-id> timeline of a task`);
783
- write(` files [path] bridge workspace listing`);
784
- write(` cat <path> preview a file in the bridge workspace`);
785
- write(` attach <file…> upload files to the bridge workspace`);
354
+ write(` ${c.cyan('the client')}`);
355
+ write(` ${c.bold('arenacode')} open the interface: timeline, input box, cards, settings`);
786
356
  write('');
787
357
  write(` ${c.cyan('options')}`);
788
358
  write(` --folder <dir> open folder (the project the agent works on)`);
@@ -795,7 +365,8 @@ function help() {
795
365
  --proceed Proceed: approve the plan and run everything without asking
796
366
  --confirm-writes ask before write/replace/rm/mv on your folder`);
797
367
  write(` --attach a,b upload files with the task`);
798
- write(` --json raw JSON output where available`);
368
+ write(` -h, --help this text`);
369
+ write(` -v, --version the version`);
799
370
  write('');
800
371
  write(c.gray(' docs: https://github.com/ · bridge: arena-bridge 2.5.2'));
801
372
  write('');
@@ -806,70 +377,25 @@ function help() {
806
377
 
807
378
  async function main(argv) {
808
379
  const { flags, positional } = parseArgs(argv);
809
- // No default here: a bare `arenacode` must be distinguishable from `arenacode chat`, because only
810
- // the bare form opens the TUI. Defaulting to 'chat' sent every plain invocation to the line CLI.
811
- const [first = undefined, ...rest] = positional;
812
- const command = first;
380
+ const command = positional[0];
813
381
 
814
382
  if (flags.version || command === 'version') {
815
383
  write(VERSION);
816
384
  return 0;
817
385
  }
818
- if (flags.help) return command && USAGE[command] ? cmdUsage(command) : help();
819
- if (command === 'help') return help();
820
-
821
- switch (command) {
822
- case 'init':
823
- return cmdInit(flags);
824
- case 'config':
825
- return cmdConfig(rest, flags);
826
- case 'ping':
827
- return cmdPing(flags);
828
- case 'doctor':
829
- return cmdDoctor(flags);
830
- case 'ask':
831
- return cmdAsk(rest, flags);
832
- case 'ui':
833
- case 'tui':
834
- return cmdUi(flags);
835
- case 'chat':
836
- return cmdChat(flags);
837
- case 'tasks':
838
- return cmdTasks(rest, flags);
839
- case 'status':
840
- return cmdStatus(rest, flags);
841
- case 'events':
842
- return cmdEvents(rest, flags);
843
- case 'stop':
844
- return cmdStop(rest, flags);
845
- case 'files':
846
- return cmdFiles(rest, flags);
847
- case 'cat':
848
- return cmdCat(rest, flags);
849
- case 'attach':
850
- return cmdAttach(rest, flags);
851
- case 'handshake':
852
- return cmdHandshake(flags);
853
- default:
854
- if (command === undefined) {
855
- // bare invocation: the interface, when there is a terminal to draw it on
856
- if (process.stdout.isTTY && process.stdin.isTTY) return cmdUi(flags);
857
- help();
858
- return 0;
859
- }
860
- if (command.startsWith('-')) {
861
- // `arenacode "instruction"` convenience form
862
- return cmdAsk(positional, flags);
863
- }
864
- {
865
- // a typo used to dump the whole help with no clue about what went wrong
866
- const known = ['init','config','ping','doctor','ask','ui','chat','tasks','status','events','stop','files','cat','attach','handshake','help','version'];
867
- const near = known.find((k) => k.startsWith(command.slice(0, 3)) || command.startsWith(k.slice(0, 3)));
868
- write(c.red(` unknown command: ${command}`) + (near ? c.gray(` · did you mean ${c.bold(near)}?`) : ''));
869
- }
870
- help();
871
- return 2;
386
+ if (flags.help || command === 'help') return help();
387
+
388
+ if (command !== undefined && !command.startsWith('-')) {
389
+ // The subcommands are gone: one entry point, the interface. Saying it out loud is
390
+ // better than opening the TUI and letting the user wonder why nothing ran.
391
+ write(c.red(` command removed: ${command}`) + c.gray(' · the client opens the interface: arenacode'));
392
+ return 2;
872
393
  }
394
+
395
+ if (process.stdout.isTTY && process.stdin.isTTY) return cmdUi(flags);
396
+ if (!requireUrl(context(flags))) return 1;
397
+ write(c.red(' the interface needs an interactive terminal.'));
398
+ return 1;
873
399
  }
874
400
 
875
- module.exports = { main, parseArgs, MODE_LINES, createTask };
401
+ module.exports = { main, runOnce, parseArgs, firstRunSetup, MODE_LINES, createTask };
package/src/config.js CHANGED
@@ -6,7 +6,9 @@ const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const { execFileSync } = require('node:child_process');
8
8
 
9
- const VERSION = '2.5.2';
9
+ // The version is the package's: a hardcoded copy went stale and the client introduced
10
+ // itself to the bridge as "ArenaCode 2.5.2" while the package was 2.11.2.
11
+ const VERSION = require('../package.json').version;
10
12
  const CLIENT_NAME = `ArenaCode ${VERSION}`;
11
13
 
12
14
  /**
package/src/local.js CHANGED
@@ -58,7 +58,7 @@ function relativize(root, abs) {
58
58
  // --------------------------------------------------------------------------- //
59
59
 
60
60
  // Every shell the client can drive. The bridge may ask for one by name (`shell` in the
61
- // local-exec request), the user can pin one (`arenacode config set shell zsh`, `/shell`),
61
+ // local-exec request), the user can pin one (`/shell` in the interface),
62
62
  // and otherwise the client takes the user's login shell, like a terminal would.
63
63
  const UNIX_SHELLS = {
64
64
  sh: '/bin/sh',
@@ -194,7 +194,7 @@ async function handleLocalExec(bridge, taskId, request, opts) {
194
194
  const lang = opts.lang;
195
195
  const dir = reqCwd ? resolveInRoot(opts.folder, reqCwd) : opts.folder;
196
196
  // the bridge names the shell it wrote the command for; otherwise the user's choice
197
- // from `arenacode config set shell …` / `/shell`, otherwise the login shell
197
+ // from `/shell` (and `--shell`), otherwise the login shell
198
198
  const chosenShell = shell || opts.shell || detectShell();
199
199
 
200
200
  write('');
package/src/tui/app.js CHANGED
@@ -15,7 +15,7 @@ const { getTheme, themeNames } = require('./theme');
15
15
  const { Bridge } = require('../bridge');
16
16
  const { buildMeta, readProjectContext } = require('../meta');
17
17
  const { runShell, runFsOp, resolveInRoot, detectShell, availableShells, shellCommand } = require('../local');
18
- const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, VERSION } = require('../config');
18
+ const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, configPath, VERSION } = require('../config');
19
19
  const { ensureContextFile, touchSession } = require('../context');
20
20
 
21
21
  const { MODE_LINES, modeLabel, nextMode, normaliseMode } = require('../modes');
@@ -485,6 +485,7 @@ class App {
485
485
  if (h) this.push({ kind: 'system', text: `bridge v${h.version} · ${h.executor_mode} · claimable ${h.tasks.claimable}` });
486
486
  else this.toast('bridge unreachable', 'error');
487
487
  } },
488
+ { title: 'Settings (bridge URL and API key)', hint: '/settings', run: () => this.openSettings() },
488
489
  { title: 'Help', hint: 'f1', run: () => this.openHelp() },
489
490
  { title: 'Quit', hint: 'ctrl+x q', run: () => this.quit() },
490
491
  ].map((cmd) => ({ ...cmd, slash: '/' + cmd.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, ''), group: 'Commands' }));
@@ -505,6 +506,7 @@ class App {
505
506
  { slash: '/theme', title: 'Cycle the colour theme', hint: 'ctrl+x t', group: 'Session', run: by('Theme') },
506
507
  { slash: '/shell', title: 'Show or change the shell commands run in', hint: '', group: 'Session', run: () => this.pickShell() },
507
508
  { slash: '/status', title: 'Ping the bridge', hint: '', group: 'Session', run: by('Bridge status') },
509
+ { slash: '/settings', title: 'Bridge URL and API key', hint: '', group: 'Session', run: () => this.openSettings() },
508
510
  { slash: '/help', title: 'Every keybinding', hint: 'f1', group: 'Session', run: () => this.openHelp() },
509
511
  { slash: '/quit', title: 'Leave arenacode', hint: 'ctrl+x q', group: 'Session', run: () => this.quit() },
510
512
  ];
@@ -549,6 +551,56 @@ class App {
549
551
  this.toast(`approval: ${next === 'proceed' ? 'Proceed' : 'Ask and Proceed'}`, next === 'proceed' ? 'warn' : 'ok');
550
552
  }
551
553
 
554
+ /**
555
+ * `/settings` (and the palette): bridge URL and API key. The client has no subcommands
556
+ * any more, so this is the only place they can be changed after the first run.
557
+ */
558
+ async openSettings() {
559
+ this.push({ kind: 'system', text: `settings: ${configPath()}` });
560
+
561
+ const url = String(await this.prompt(`bridge URL [${this.url || 'not set'}]`)).trim();
562
+ if (url && url !== this.url) {
563
+ this.url = url;
564
+ try {
565
+ patchConfig({ url });
566
+ } catch {
567
+ /* read-only HOME: keep it in memory */
568
+ }
569
+ }
570
+ if (!this.url) {
571
+ this.toast('settings: the bridge URL is required', 'warn');
572
+ return;
573
+ }
574
+
575
+ const masked = this.key ? `${String(this.key).slice(0, 8)}…` : 'not set';
576
+ const key = String(await this.prompt(`API key [${masked}]`)).trim();
577
+ if (key) {
578
+ this.key = key;
579
+ try {
580
+ patchConfig({ key });
581
+ } catch {
582
+ /* read-only HOME: keep it in memory */
583
+ }
584
+ }
585
+
586
+ this.bridge = new Bridge(this.url, this.key, deviceId(readConfig()));
587
+ await this.refreshHealth();
588
+ const health = this.state.health;
589
+ if (!health) {
590
+ this.push({ kind: 'system', text: `settings: ${this.url} unreachable — check the URL and the tunnel` });
591
+ this.toast('settings saved · bridge unreachable', 'error');
592
+ return;
593
+ }
594
+ const auth = await this.bridge.authCheck();
595
+ const json = auth.json || {};
596
+ const accepted = auth.status === 200;
597
+ this.push({
598
+ kind: 'system',
599
+ text: `settings: bridge v${health.version} · ${health.executor_mode} · key ${accepted ? 'accepted' : `rejected (${json.error || auth.status})`}`,
600
+ });
601
+ this.toast(accepted ? 'settings saved · key accepted' : 'settings saved · key rejected', accepted ? 'ok' : 'warn');
602
+ }
603
+
552
604
  /** `/shell` (and the double-click on a card's shell line): pick from the shells here. */
553
605
  async pickShell(name = '') {
554
606
  const shells = availableShells();
@@ -674,7 +726,7 @@ class App {
674
726
  return;
675
727
  }
676
728
  if (!this.url) {
677
- this.toast('no bridge URL: run arenacode init', 'error');
729
+ this.toast('no bridge URL: /settings', 'error');
678
730
  return;
679
731
  }
680
732
  const tmp = path.join(require('os').tmpdir(), `arenacode-clip-${stamp()}-${process.pid}.png`);
@@ -710,7 +762,7 @@ class App {
710
762
  return;
711
763
  }
712
764
  if (!this.url) {
713
- this.toast('no bridge URL: run arenacode init', 'error');
765
+ this.toast('no bridge URL: /settings', 'error');
714
766
  return;
715
767
  }
716
768
 
@@ -1537,6 +1589,7 @@ function HELP_ROWS() {
1537
1589
  ['Session', 'ctrl+x l', 'task queue'],
1538
1590
  ['Session', 'ctrl+x a', 'Ask and Proceed / Proceed'],
1539
1591
  ['Session', 'ctrl+x t', 'switch theme'],
1592
+ ['Session', '/settings', 'bridge URL and API key'],
1540
1593
  ['Session', 'ctrl+x d', 'toggle tool output'],
1541
1594
  ['Session', 'pgup / pgdn', 'scroll'],
1542
1595
  ['Session', 'ctrl+l', 'clear the timeline'],