@lardexx/arenacode 2.11.1 → 2.11.3

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.1",
3
+ "version": "2.11.3",
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",
@@ -285,6 +285,20 @@ function tool(kind, args = {}) {
285
285
  }
286
286
 
287
287
  /** The value inside {"ok":true,"result":…}, or {ok:false,error} unchanged. */
288
+ /**
289
+ * The request windows-use expects for typing. The two forms are not interchangeable:
290
+ * with the coordinates of the field it is `type_tool` (`loc` is required, the DLL
291
+ * answers "loc: field required" otherwise), without them it is `type_focus_tool`,
292
+ * which writes into whatever holds the focus.
293
+ */
294
+ function typeRequest(text, x = null, y = null) {
295
+ const value = String(text);
296
+ const located = Number.isFinite(x) && Number.isFinite(y);
297
+ return located
298
+ ? { tool: TOOLS.type, args: { loc: [Math.round(x), Math.round(y)], text: value } }
299
+ : { tool: TOOLS.type_focus, args: { text: value } };
300
+ }
301
+
288
302
  function result(answer) {
289
303
  if (!answer || typeof answer !== 'object') return { ok: false, error: 'no answer' };
290
304
  if (answer.ok === false) return { ok: false, error: answer.error || 'windows-use error' };
@@ -400,12 +414,13 @@ const backend = {
400
414
  },
401
415
 
402
416
  typeText(text, x = null, y = null) {
403
- if (x !== null && y !== null) {
404
- const clicked = backend.click(x, y);
405
- if (!clicked.ok) return clicked;
406
- }
407
- const r = result(tool('type', { text: String(text) }));
408
- return r.ok ? { ok: true, chars: String(text).length, typed: String(text).length } : r;
417
+ // windows-use has two forms and they are not interchangeable: type_tool needs the
418
+ // coordinates of the field (loc), type_focus_tool writes into the focused element.
419
+ // Without loc the DLL answers "loc: field required".
420
+ const value = String(text);
421
+ const r = result(raw(typeRequest(value, x, y)));
422
+ if (!r.ok) return r;
423
+ return { ok: true, chars: value.length, typed: value.length };
409
424
  },
410
425
 
411
426
  pressKeys(combo) {
@@ -484,6 +499,7 @@ module.exports = {
484
499
  load,
485
500
  version,
486
501
  tool,
502
+ typeRequest,
487
503
  result,
488
504
  raw,
489
505
  backend,
package/src/cli.js CHANGED
@@ -133,7 +133,7 @@ async function ensureAuth(ctx) {
133
133
  // commands
134
134
  // --------------------------------------------------------------------------- //
135
135
 
136
- async function cmdInit(flags, { tail = true } = {}) {
136
+ async function firstRunSetup(flags, { tail = true } = {}) {
137
137
  banner(VERSION);
138
138
  const cfg = readConfig();
139
139
  write(c.gray(` config file: ${configPath()}`));
@@ -179,105 +179,6 @@ async function cmdInit(flags, { tail = true } = {}) {
179
179
  return 0;
180
180
  }
181
181
 
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
182
  function agentPrompt() {
282
183
  try {
283
184
  return fs.readFileSync(AGENT_PROMPT_FILE, 'utf8');
@@ -383,12 +284,17 @@ async function runInstruction(ctx, text, { attachments = [] } = {}) {
383
284
  }
384
285
  }
385
286
 
386
- async function cmdAsk(positional, flags) {
287
+ /**
288
+ * Runs one instruction and streams the timeline. Internal on purpose: the client no
289
+ * longer exposes subcommands, `arenacode` opens the TUI and this is the code path the
290
+ * TUI itself uses (and the integration test).
291
+ */
292
+ async function runOnce(instruction, flags = {}) {
387
293
  const ctx = context(flags);
388
294
  if (!requireUrl(ctx)) return 1;
389
- const text = positional.join(' ').trim();
295
+ const text = String(Array.isArray(instruction) ? instruction.join(' ') : instruction || '').trim();
390
296
  if (!text) {
391
- write('usage: arenacode ask "your instruction"');
297
+ write(c.red(' no instruction'));
392
298
  return 2;
393
299
  }
394
300
  if (!(await ensureAuth(ctx))) return 1;
@@ -414,7 +320,7 @@ async function cmdUi(flags) {
414
320
  write('');
415
321
  write(` ${c.bold('first run')} ${c.gray('· bridge URL and API key (Enter keeps the value in brackets)')}`);
416
322
  write('');
417
- await cmdInit(flags, { tail: false });
323
+ await firstRunSetup(flags, { tail: false });
418
324
  write('');
419
325
  ctx = context(flags);
420
326
  if (!ctx.url) return 1; // setup was abandoned: nothing to open
@@ -435,354 +341,17 @@ async function cmdUi(flags) {
435
341
  return 0;
436
342
  }
437
343
 
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
344
  /** 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
345
 
717
346
  /** 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
347
  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
348
  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)`);
349
+ write(` ${c.cyan('ArenaCode CLI')} ${c.gray(`v${VERSION}`)} ${c.gray('· terminal client for arena-bridge')}`);
771
350
  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`);
351
+ write(` usage ${c.bold('arenacode')} [options]`);
778
352
  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`);
353
+ write(` ${c.cyan('the client')}`);
354
+ write(` ${c.bold('arenacode')} open the interface: timeline, input box, cards, settings`);
786
355
  write('');
787
356
  write(` ${c.cyan('options')}`);
788
357
  write(` --folder <dir> open folder (the project the agent works on)`);
@@ -795,7 +364,8 @@ function help() {
795
364
  --proceed Proceed: approve the plan and run everything without asking
796
365
  --confirm-writes ask before write/replace/rm/mv on your folder`);
797
366
  write(` --attach a,b upload files with the task`);
798
- write(` --json raw JSON output where available`);
367
+ write(` -h, --help this text`);
368
+ write(` -v, --version the version`);
799
369
  write('');
800
370
  write(c.gray(' docs: https://github.com/ · bridge: arena-bridge 2.5.2'));
801
371
  write('');
@@ -806,70 +376,25 @@ function help() {
806
376
 
807
377
  async function main(argv) {
808
378
  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;
379
+ const command = positional[0];
813
380
 
814
381
  if (flags.version || command === 'version') {
815
382
  write(VERSION);
816
383
  return 0;
817
384
  }
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;
385
+ if (flags.help || command === 'help') return help();
386
+
387
+ if (command !== undefined && !command.startsWith('-')) {
388
+ // The subcommands are gone: one entry point, the interface. Saying it out loud is
389
+ // better than opening the TUI and letting the user wonder why nothing ran.
390
+ write(c.red(` command removed: ${command}`) + c.gray(' · the client opens the interface: arenacode'));
391
+ return 2;
872
392
  }
393
+
394
+ if (process.stdout.isTTY && process.stdin.isTTY) return cmdUi(flags);
395
+ if (!requireUrl(context(flags))) return 1;
396
+ write(c.red(' the interface needs an interactive terminal.'));
397
+ return 1;
873
398
  }
874
399
 
875
- module.exports = { main, parseArgs, MODE_LINES, createTask };
400
+ 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'],