@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 +26 -41
- package/docs/PUBLISHING.md +1 -1
- package/package.json +2 -2
- package/src/automation/win-dll.js +22 -6
- package/src/cli.js +30 -505
- package/src/config.js +3 -1
- package/src/local.js +2 -2
- package/src/tui/app.js +56 -3
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 #
|
|
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
|
|
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
|
|
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.
|
|
202
|
-
|
|
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
|
-
#
|
|
208
|
-
arenacode
|
|
209
|
-
arenacode chat
|
|
206
|
+
# 2. open the client
|
|
207
|
+
arenacode
|
|
210
208
|
```
|
|
211
209
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
221
|
-
| `arenacode
|
|
222
|
-
| `arenacode
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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 — `
|
|
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 `
|
|
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;
|
|
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
|
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const r = result(
|
|
408
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
295
|
+
const text = String(Array.isArray(instruction) ? instruction.join(' ') : instruction || '').trim();
|
|
390
296
|
if (!text) {
|
|
391
|
-
write('
|
|
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
|
|
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('
|
|
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.
|
|
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('
|
|
780
|
-
write(`
|
|
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(` --
|
|
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
|
-
|
|
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
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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 (`
|
|
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 `
|
|
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:
|
|
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:
|
|
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'],
|