@lardexx/arenacode 2.9.0 → 2.11.0

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/src/automation.js CHANGED
@@ -1003,11 +1003,24 @@ let nativeKind = null;
1003
1003
 
1004
1004
  try {
1005
1005
  if (isLinux) {
1006
+ // Linux has no fallback any more: the FFI backend *is* the backend (the old one
1007
+ // shelled out to xdotool, which is exactly what we do not want).
1006
1008
  nativeBackend = require('./automation/linux').backend;
1007
1009
  nativeKind = 'x11';
1008
1010
  } else if (isWin) {
1009
- nativeBackend = require('./automation/win-dll').backend;
1010
- nativeKind = 'dll';
1011
+ // The DLL ships in the package; PowerShell stays only as a safety net for the case
1012
+ // where it cannot be loaded at all.
1013
+ const dll = require('./automation/win-dll');
1014
+ if (dll.load()) {
1015
+ nativeBackend = dll.backend;
1016
+ nativeKind = 'dll';
1017
+ }
1018
+ } else if (isMac) {
1019
+ const mac = require('./automation/mac');
1020
+ if (mac.available().ok) {
1021
+ nativeBackend = mac.backend;
1022
+ nativeKind = 'mac';
1023
+ }
1011
1024
  }
1012
1025
  } catch (err) {
1013
1026
  nativeBackend = null;
@@ -1023,7 +1036,11 @@ if (nativeBackend) {
1023
1036
  };
1024
1037
 
1025
1038
  Object.assign(automation, {
1026
- native: { kind: nativeKind, name: nativeBackend.name, dll: nativeKind === 'dll' ? require('./automation/win-dll').locate() : null },
1039
+ native: {
1040
+ kind: nativeKind,
1041
+ name: nativeBackend.name,
1042
+ dll: nativeKind === 'dll' ? require('./automation/win-dll').locate() : null,
1043
+ },
1027
1044
  available: () => nativeBackend.available(),
1028
1045
  screenSize: () => nativeBackend.screenSize(),
1029
1046
  windows: () => nativeBackend.windows(),
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ const { attachRefs, decorate } = require('./attach');
14
14
  const { decide, dangerText, normaliseApproval } = require('./approval');
15
15
  const session = require('./session');
16
16
  const { buildMeta, readProjectContext } = require('./meta');
17
+ const { ensureContextFile, shouldSend: shouldSendContext, markSent: markContextSent } = require('./context');
17
18
  const { followTask } = require('./turn');
18
19
  const { c, write, ask, banner, truncate, indentLines } = require('./ui');
19
20
 
@@ -103,6 +104,7 @@ function context(flags) {
103
104
  json: Boolean(flags.json),
104
105
  confirmWrites: Boolean(flags['confirm-writes']),
105
106
  approval: normaliseApproval(flags.proceed ? 'proceed' : flags.ask ? 'ask' : cfg.approval),
107
+ shell: String(flags.shell || process.env.ARENACODE_SHELL || cfg.shell || '').trim(),
106
108
  idleTimeout: Number(flags['idle-timeout'] || cfg.idle_timeout || 180),
107
109
  bridge: new Bridge(url, key, device),
108
110
  };
@@ -194,7 +196,7 @@ function cmdConfig(positional, flags) {
194
196
  const name = key;
195
197
  const val = rest.join(' ');
196
198
  if (!name || val === '') {
197
- write(`usage: ${SELF} config set <url|key|lang|mode|approval|pc_access> <value>`);
199
+ write(`usage: ${SELF} config set <url|key|lang|mode|approval|pc_access|shell> <value>`);
198
200
  return 2;
199
201
  }
200
202
  if (name === 'approval' && !['ask', 'proceed'].includes(String(val))) {
@@ -285,16 +287,42 @@ function agentPrompt() {
285
287
  }
286
288
 
287
289
  /** POST /task exactly like the desktop client does. */
290
+ /**
291
+ * The bridge says it itself: `started_at` in /health changes every time the bridge
292
+ * starts. That is how the client knows a session is new and the sheet has to be sent.
293
+ */
294
+ const bridgeStarts = new Map();
295
+ async function bridgeStartedAt(bridge, url) {
296
+ if (bridgeStarts.has(url)) return bridgeStarts.get(url);
297
+ let startedAt = null;
298
+ try {
299
+ const res = await bridge.health();
300
+ if (res.status === 200 && res.json) startedAt = res.json.started_at ?? null;
301
+ } catch {
302
+ startedAt = null;
303
+ }
304
+ bridgeStarts.set(url, startedAt);
305
+ return startedAt;
306
+ }
307
+
288
308
  async function createTask(ctx, instruction, { extraInstructions = '' } = {}) {
289
309
  const meta = buildMeta(ctx.folder, { lang: ctx.lang, pcAccess: ctx.pcAccess, activeFile: '' });
290
- const projectContext = readProjectContext(ctx.folder);
310
+ // the sheet exists from the moment the folder is used: the agent fills the block
311
+ // between the markers at the end of every task, the client only writes the frame
312
+ ensureContextFile(ctx.folder, { lang: ctx.lang, bridgeUrl: ctx.url });
313
+ const startedAt = await bridgeStartedAt(ctx.bridge, ctx.url);
314
+ const sendContext = shouldSendContext({ folder: ctx.folder, url: ctx.url, startedAt });
315
+ const projectContext = sendContext ? readProjectContext(ctx.folder) : '';
291
316
  const body = {
292
317
  instruction,
293
318
  wait: 0,
294
319
  meta,
295
320
  agent_prompt: agentPrompt(),
296
321
  };
297
- if (projectContext) body.project_context = projectContext;
322
+ if (projectContext) {
323
+ body.project_context = projectContext;
324
+ markContextSent({ folder: ctx.folder, url: ctx.url, startedAt });
325
+ }
298
326
  if (extraInstructions) body.instruction = `${instruction}\n\n${extraInstructions}`;
299
327
 
300
328
  const res = await ctx.bridge.createTask(body);
@@ -337,6 +365,7 @@ async function runInstruction(ctx, text, { attachments = [] } = {}) {
337
365
  const result = await followTask(ctx.bridge, taskId, {
338
366
  folder: ctx.folder,
339
367
  lang: ctx.lang,
368
+ shell: ctx.shell,
340
369
  yes: ctx.yes,
341
370
  approval: ctx.approval,
342
371
  confirmWrites: ctx.confirmWrites,
@@ -400,6 +429,7 @@ async function cmdUi(flags) {
400
429
  yes: ctx.yes,
401
430
  approval: ctx.approval,
402
431
  confirmWrites: ctx.confirmWrites,
432
+ shell: ctx.shell,
403
433
  });
404
434
  await app.start();
405
435
  return 0;
@@ -688,7 +718,7 @@ async function cmdHandshake(flags) {
688
718
  const USAGE = {
689
719
  init: [['init', 'configure bridge URL + API key, then test the connection']],
690
720
  config: [['config [show|path]', 'read the saved configuration'],
691
- ['config set <key> <value>', 'url · key · lang · mode · pc_access']],
721
+ ['config set <key> <value>', 'url · key · lang · mode · pc_access · shell']],
692
722
  ping: [['ping', 'bridge status and API key check']],
693
723
  doctor: [['doctor', 'local checks: node, platform, folder, git, bridge']],
694
724
  ask: [['ask "instruction"', 'run one task and stream the timeline']],
package/src/config.js CHANGED
@@ -34,6 +34,7 @@ const DEFAULTS = {
34
34
  device: '',
35
35
  vision: false,
36
36
  idle_timeout: 180,
37
+ shell: '',
37
38
  };
38
39
 
39
40
  function readConfig() {
package/src/context.js ADDED
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ // The project context sheet: `context_<project>.md` in the open folder.
4
+ //
5
+ // Mined from ArenaCode-x64.exe (the literals are the desktop client's own, UTF-16 in
6
+ // .rdata, built by the function at 0x14002f7f0):
7
+ //
8
+ // '# Contesto del progetto ' + name + '\n\n'
9
+ // 'Sessione: ' + … + ' (' + … + ')\n'
10
+ // '_Nessun task ancora._\n'
11
+ // '<!-- agente:inizio -->\n'
12
+ // '<!-- agente:fine -->\n\n'
13
+ // 'Aggiornato automaticamente da ArenaCode a fine di ogni job. La API key non è scritta qui.\n\n'
14
+ //
15
+ // The client creates the file when the folder opens or a session starts, sends the whole
16
+ // file to the bridge with the first task of a new bridge session (bridge_server.py:
17
+ // "arriva con il primo task di un bridge nuovo"), and never summarises it. The agent
18
+ // rewrites the block between the two HTML comments at the end of every task — the client
19
+ // only writes the frame, never the block.
20
+
21
+ const fs = require('node:fs');
22
+ const os = require('node:os');
23
+ const path = require('node:path');
24
+
25
+ const MARK_START = '<!-- agente:inizio -->';
26
+ const MARK_END = '<!-- agente:fine -->';
27
+
28
+ const TEXT = {
29
+ it: {
30
+ title: '# Contesto del progetto ',
31
+ session: 'Sessione: ',
32
+ noTask: '_Nessun task ancora._',
33
+ footer: 'Aggiornato automaticamente da ArenaCode a fine di ogni job. La API key non è scritta qui.',
34
+ },
35
+ en: {
36
+ title: '# Project context ',
37
+ session: 'Session: ',
38
+ noTask: '_No task yet._',
39
+ footer: 'Updated automatically by ArenaCode at the end of each job. The API key is not written here.',
40
+ },
41
+ es: {
42
+ title: '# Contexto del proyecto ',
43
+ session: 'Sesión: ',
44
+ noTask: '_Ningún trabajo todavía._',
45
+ footer: 'Actualizado automáticamente por ArenaCode al final de cada trabajo. La clave API no se escribe aquí.',
46
+ },
47
+ };
48
+
49
+ function pack(lang) {
50
+ return TEXT[lang] || TEXT.it;
51
+ }
52
+
53
+ /** `Progetto web` → `Progetto-web`; keeps the name readable, drops what a filesystem hates. */
54
+ function projectName(folder) {
55
+ const base = path.basename(path.resolve(String(folder || '.'))) || 'progetto';
56
+ const clean = base
57
+ .replace(/[\\/:*?"<>|]/g, '-')
58
+ .replace(/\s+/g, '-')
59
+ .replace(/^[.-]+/, '')
60
+ .slice(0, 60);
61
+ return clean || 'progetto';
62
+ }
63
+
64
+ function contextFileName(folder) {
65
+ return `context_${projectName(folder)}.md`;
66
+ }
67
+
68
+ function contextFilePath(folder) {
69
+ return path.join(path.resolve(String(folder || '.')), contextFileName(folder));
70
+ }
71
+
72
+ function stamp(date = new Date()) {
73
+ const pad = (n) => String(n).padStart(2, '0');
74
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
75
+ }
76
+
77
+ /** Where the session line's parenthetical comes from: the bridge, or the machine. */
78
+ function sessionHost(bridgeUrl) {
79
+ const raw = String(bridgeUrl || '').trim();
80
+ if (raw) {
81
+ try {
82
+ return new URL(raw.includes('://') ? raw : `https://${raw}`).host;
83
+ } catch {
84
+ return raw;
85
+ }
86
+ }
87
+ return os.hostname();
88
+ }
89
+
90
+ /** The frame the client writes, byte for byte in the desktop's order. */
91
+ function buildTemplate({ folder, lang = 'it', bridgeUrl = '', now = new Date() } = {}) {
92
+ const t = pack(lang);
93
+ return (
94
+ `${t.title}${projectName(folder)}\n` +
95
+ '\n' +
96
+ `${t.session}${stamp(now)} (${sessionHost(bridgeUrl)})\n` +
97
+ `${t.noTask}\n` +
98
+ `${MARK_START}\n` +
99
+ `${MARK_END}\n` +
100
+ '\n' +
101
+ `${t.footer}\n`
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Creates the file if the folder does not have one yet. An existing file is never
107
+ * touched: it is the project's log and the agent's block lives inside it.
108
+ */
109
+ function ensureContextFile(folder, { lang = 'it', bridgeUrl = '', now = new Date() } = {}) {
110
+ const root = path.resolve(String(folder || '.'));
111
+ const file = contextFilePath(root);
112
+ try {
113
+ const stat = fs.statSync(file);
114
+ if (stat.isFile()) return { ok: true, file, created: false, name: contextFileName(root) };
115
+ } catch {
116
+ /* not there yet */
117
+ }
118
+ try {
119
+ fs.mkdirSync(root, { recursive: true });
120
+ fs.writeFileSync(file, buildTemplate({ folder: root, lang, bridgeUrl, now }), { encoding: 'utf8', flag: 'wx' });
121
+ return { ok: true, file, created: true, name: contextFileName(root) };
122
+ } catch (err) {
123
+ // EEXIST: another window created it first — fine, nothing to do
124
+ if (err.code === 'EEXIST') return { ok: true, file, created: false, name: contextFileName(root) };
125
+ return { ok: false, file, created: false, error: err.message };
126
+ }
127
+ }
128
+
129
+ /** Reads the sheet the way the desktop sends it: whole, untouched. */
130
+ function readContext(folder, { maxBytes = 200_000 } = {}) {
131
+ const root = path.resolve(String(folder || '.'));
132
+ let names = [];
133
+ try {
134
+ names = fs.readdirSync(root).filter((name) => /^context_.*\.md$/i.test(name));
135
+ } catch {
136
+ return '';
137
+ }
138
+ if (!names.length) return '';
139
+ // the one for this folder first, then any other context sheet
140
+ const file = names.find((name) => name === contextFileName(root)) || names[0];
141
+ try {
142
+ const full = path.join(root, file);
143
+ if (fs.statSync(full).size > maxBytes) return '';
144
+ return fs.readFileSync(full, 'utf8');
145
+ } catch {
146
+ return '';
147
+ }
148
+ }
149
+
150
+ // ------------------------------------------------------------------ sending it --
151
+ // The bridge writes what it receives to runtime/project_context.txt and tells the agent
152
+ // to read it in parts; it expects it once per bridge session, not on every task.
153
+
154
+ const STATE_FILE = () => path.join(path.dirname(configDir()), 'context-state.json');
155
+
156
+ function configDir() {
157
+ if (process.platform === 'win32') {
158
+ const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
159
+ return path.join(base, 'ArenaCode', 'config.json');
160
+ }
161
+ return path.join(os.homedir(), '.arenacode', 'config.json');
162
+ }
163
+
164
+ function readState() {
165
+ try {
166
+ return JSON.parse(fs.readFileSync(STATE_FILE(), 'utf8'));
167
+ } catch {
168
+ return {};
169
+ }
170
+ }
171
+
172
+ function writeState(state) {
173
+ try {
174
+ fs.mkdirSync(path.dirname(STATE_FILE()), { recursive: true });
175
+ fs.writeFileSync(STATE_FILE(), JSON.stringify(state, null, 2), 'utf8');
176
+ } catch {
177
+ /* a read-only HOME only costs one extra send */
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Should this task carry the project context? Yes on the first task of a bridge
183
+ * session and whenever the folder changes, no for the following tasks.
184
+ */
185
+ function shouldSend({ folder, url = '', startedAt = null } = {}) {
186
+ const state = readState();
187
+ const project = path.resolve(String(folder || '.'));
188
+ if (state.folder !== project) return true;
189
+ if (!startedAt) return false; // same folder, bridge identity unknown: it already got it
190
+ return state.url !== String(url) || Number(state.startedAt) !== Number(startedAt);
191
+ }
192
+
193
+ function markSent({ folder, url = '', startedAt = null } = {}) {
194
+ writeState({ folder: path.resolve(String(folder || '.')), url: String(url), startedAt: startedAt ? Number(startedAt) : null });
195
+ }
196
+
197
+ /** Appends a session line to the sheet when the client opens it again later. */
198
+ function touchSession(folder, { lang = 'it', bridgeUrl = '', now = new Date() } = {}) {
199
+ const file = contextFilePath(folder);
200
+ let text;
201
+ try {
202
+ text = fs.readFileSync(file, 'utf8');
203
+ } catch {
204
+ return { ok: false, error: 'context file missing' };
205
+ }
206
+ const t = pack(lang);
207
+ const line = `${t.session}${stamp(now)} (${sessionHost(bridgeUrl)})\n`;
208
+ if (text.includes(line)) return { ok: true, file, added: false };
209
+ const at = text.indexOf('\n\n');
210
+ const updated = at >= 0 ? `${text.slice(0, at + 2)}${line}${text.slice(at + 2)}` : `${text}${line}`;
211
+ try {
212
+ fs.writeFileSync(file, updated, 'utf8');
213
+ return { ok: true, file, added: true };
214
+ } catch (err) {
215
+ return { ok: false, file, error: err.message };
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ MARK_START,
221
+ MARK_END,
222
+ TEXT,
223
+ projectName,
224
+ contextFileName,
225
+ contextFilePath,
226
+ buildTemplate,
227
+ ensureContextFile,
228
+ readContext,
229
+ shouldSend,
230
+ markSent,
231
+ touchSession,
232
+ sessionHost,
233
+ stamp,
234
+ };
package/src/local.js CHANGED
@@ -39,6 +39,15 @@ function resolveInRoot(root, rel) {
39
39
  return abs;
40
40
  }
41
41
 
42
+ /** `context_<project>.md` — the project log, never committed and never asked about. */
43
+ function contextFiles(root) {
44
+ try {
45
+ return fs.readdirSync(root).filter((name) => /^context_.*\.md$/i.test(name));
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
42
51
  function relativize(root, abs) {
43
52
  const rel = path.relative(root, abs).replace(/\\/g, '/');
44
53
  return rel === '' ? '.' : rel;
@@ -48,22 +57,110 @@ function relativize(root, abs) {
48
57
  // Local terminal (local-exec)
49
58
  // --------------------------------------------------------------------------- //
50
59
 
51
- function shellCommand(shell, command) {
52
- if (process.platform === 'win32') {
53
- switch (shell) {
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`),
62
+ // and otherwise the client takes the user's login shell, like a terminal would.
63
+ const UNIX_SHELLS = {
64
+ sh: '/bin/sh',
65
+ bash: '/bin/bash',
66
+ zsh: '/bin/zsh',
67
+ fish: '/usr/bin/fish',
68
+ dash: '/bin/dash',
69
+ ksh: '/bin/ksh',
70
+ };
71
+
72
+ const WINDOWS_SHELLS = ['powershell', 'pwsh', 'cmd', 'wsl', 'bash'];
73
+
74
+ function shellExists(file) {
75
+ try {
76
+ fs.accessSync(file, fs.constants.X_OK);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /** sh/bash/zsh/dash/ksh: -l login, -c command. fish spells the same as -l -c. */
84
+ function unixShell(name) {
85
+ const wanted = String(name || '').toLowerCase().replace(/\.exe$/, '');
86
+ const alias = { 'git-bash': 'bash', gitbash: 'bash' }[wanted] || wanted;
87
+ let file = UNIX_SHELLS[alias];
88
+ if (!file && (alias === 'sh' || alias.startsWith('/'))) file = alias === 'sh' ? '/bin/sh' : alias;
89
+ if (!file) file = whichPath(alias);
90
+ if (!file) file = UNIX_SHELLS[alias] || null;
91
+ if (!file) return null;
92
+ const base = path.basename(file);
93
+ const args = base === 'fish' ? ['-l', '-c', null] : ['-lc', null];
94
+ return { file, args };
95
+ }
96
+
97
+ function whichPath(name) {
98
+ for (const dir of ['/bin', '/usr/bin', '/usr/local/bin', '/opt/homebrew/bin']) {
99
+ const file = path.join(dir, name);
100
+ if (shellExists(file)) return file;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /** The shell to use when nobody asked for a specific one. */
106
+ function detectShell(platform = process.platform) {
107
+ if (platform === 'win32') {
108
+ if (shellExists('pwsh.exe') || whichPath('pwsh')) return 'pwsh';
109
+ return 'powershell';
110
+ }
111
+ const login = path.basename(process.env.SHELL || '');
112
+ if (login && (UNIX_SHELLS[login] || whichPath(login))) return login;
113
+ return 'sh';
114
+ }
115
+
116
+ /** What the client can offer on this machine — for `sysinfo`, `/shell` and the help. */
117
+ function availableShells(platform = process.platform) {
118
+ if (platform === 'win32') {
119
+ return WINDOWS_SHELLS.filter((name) => name === 'powershell' || name === 'cmd' || Boolean(whichPath(name)));
120
+ }
121
+ const found = Object.keys(UNIX_SHELLS).filter((name) => shellExists(UNIX_SHELLS[name]) || Boolean(whichPath(name)));
122
+ return found.length ? found : ['sh'];
123
+ }
124
+
125
+ /**
126
+ * Command template for one shell. `command` is passed as its own argv entry, never
127
+ * through a second round of quoting, so quotes and `$` survive.
128
+ */
129
+ function shellCommand(shell, command, platform = process.platform) {
130
+ const name = String(shell || detectShell(platform)).toLowerCase().replace(/\.exe$/, '');
131
+ if (platform === 'win32') {
132
+ switch (name) {
54
133
  case 'cmd':
55
134
  return { file: 'cmd.exe', args: ['/d', '/s', '/c', command] };
56
135
  case 'wsl':
57
136
  return { file: 'wsl.exe', args: ['sh', '-lc', command] };
137
+ case 'bash': {
138
+ // Git Bash when it is installed, otherwise the WSL bash
139
+ const gitBash = [
140
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
141
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
142
+ ].find((file) => {
143
+ try {
144
+ return fs.existsSync(file);
145
+ } catch {
146
+ return false;
147
+ }
148
+ });
149
+ return gitBash
150
+ ? { file: gitBash, args: ['-lc', command] }
151
+ : { file: 'wsl.exe', args: ['bash', '-lc', command] };
152
+ }
153
+ case 'pwsh':
154
+ return { file: 'pwsh.exe', args: ['-NoProfile', '-NonInteractive', '-Command', command] };
58
155
  case 'powershell':
156
+ case 'ps':
59
157
  default:
60
- return {
61
- file: shell === 'pwsh' ? 'pwsh.exe' : 'powershell.exe',
62
- args: ['-NoProfile', '-NonInteractive', '-Command', command],
63
- };
158
+ return { file: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', command] };
64
159
  }
65
160
  }
66
- return { file: '/bin/sh', args: ['-lc', command] };
161
+ const template = unixShell(name) || unixShell('sh');
162
+ const args = template.args.map((arg) => (arg === null ? command : arg));
163
+ return { file: template.file, args };
67
164
  }
68
165
 
69
166
  /** Runs a command and collects stdout+stderr and the exit code. */
@@ -96,9 +193,12 @@ async function handleLocalExec(bridge, taskId, request, opts) {
96
193
  const { rid, command, cwd: reqCwd, shell, admin, sandbox_reason: sandboxReason } = request;
97
194
  const lang = opts.lang;
98
195
  const dir = reqCwd ? resolveInRoot(opts.folder, reqCwd) : opts.folder;
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
198
+ const chosenShell = shell || opts.shell || detectShell();
99
199
 
100
200
  write('');
101
- write(` ${c.bold(c.yellow('⌘ terminal'))} ${c.dim('·')} ${c.cyan(shell || 'shell')} ${c.dim('·')} ${c.gray(dir)}`);
201
+ write(` ${c.bold(c.yellow('⌘ terminal'))} ${c.dim('·')} ${c.cyan(chosenShell)} ${c.dim('·')} ${c.gray(dir)}`);
102
202
  write(indentLines(command, ' '));
103
203
  if (admin) write(` ${c.yellow('!')} ${t(lang, 'Richiede i permessi di amministratore: qui il comando gira senza UAC.', 'Needs administrator rights: here it runs without UAC.', 'Necesita permisos de administrador: aquí se ejecuta sin UAC.')}`);
104
204
  if (sandboxReason) write(` ${c.yellow('!')} ${t(lang, 'L’agente chiede di installare sul PC', 'The agent asks to install on the PC', 'El agente pide instalar en el PC')}: ${c.gray(sandboxReason)}`);
@@ -125,7 +225,7 @@ async function handleLocalExec(bridge, taskId, request, opts) {
125
225
  return true;
126
226
  }
127
227
 
128
- const { output, code } = await runShell(command, { cwd: dir, shell });
228
+ const { output, code } = await runShell(command, { cwd: dir, shell: chosenShell });
129
229
  await bridge.localResult(taskId, { rid, output, code });
130
230
  write(` ${code === 0 ? c.green('✓') : c.red('✗')} ${c.gray(`exit ${code}`)}`);
131
231
  return true;
@@ -335,6 +435,8 @@ function runFsOp(op, body, root) {
335
435
  case 'git_commit': {
336
436
  const add = git(['add', '-A'], root);
337
437
  if (!add.ok) return { ok: false, error: add.output };
438
+ // the project log stays out of the history, exactly like the desktop client does
439
+ for (const name of contextFiles(root)) git(['reset', '-q', '--', name], root);
338
440
  const commit = git(['commit', '-m', String(body.content || 'ArenaCode')], root);
339
441
  return { ok: commit.ok, output: commit.output };
340
442
  }
@@ -384,7 +486,9 @@ async function handleLocalFs(bridge, taskId, request, opts) {
384
486
  write(` ${c.bold(c.yellow('▤ files'))} ${c.dim('·')} ${c.cyan(op)} ${c.gray(request.path || '.')}${request.to ? c.gray(` → ${request.to}`) : ''}`);
385
487
 
386
488
  const mutating = ['write', 'replace', 'rm', 'mv', 'mkdir'].includes(op);
387
- const verdict = mutating
489
+ // the project log is saved at once: the card only shows what changed, like the desktop
490
+ const isProjectLog = /^context_.*\.md$/i.test(path.basename(String(request.path || '')));
491
+ const verdict = mutating && !isProjectLog
388
492
  ? decide({
389
493
  approval: opts.approval,
390
494
  mode: session.modeOf(taskId),
@@ -394,7 +498,7 @@ async function handleLocalFs(bridge, taskId, request, opts) {
394
498
  })
395
499
  : 'auto';
396
500
  // --confirm-writes keeps asking even when the client runs in Proceed
397
- const mustAsk = mutating && (verdict === 'ask' || opts.confirmWrites);
501
+ const mustAsk = mutating && !isProjectLog && (verdict === 'ask' || opts.confirmWrites);
398
502
  if (mustAsk) {
399
503
  const key = await askKey(` ${t(lang, 'Applico? [s] sì [n] no', 'Apply? [y] yes [n] no', '¿Aplico? [s] sí [n] no')} `, ['s', 'y', 'n']);
400
504
  if (key === 'n') {
@@ -495,7 +599,8 @@ async function handleLocalUi(bridge, taskId, request, opts) {
495
599
  cpus: os.cpus().length,
496
600
  memory_mb: Math.round(os.totalmem() / 1048576),
497
601
  node: process.version,
498
- shell: require('./meta').which('pwsh') ? 'pwsh' : process.platform === 'win32' ? 'powershell' : 'sh',
602
+ shell: detectShell(),
603
+ shells: availableShells(),
499
604
  };
500
605
  await bridge.localResult(taskId, { rid, output: JSON.stringify(info), code: 0 });
501
606
  write(` ${c.green('✓')} ${c.gray('sysinfo')}`);
@@ -683,6 +788,9 @@ async function handleLocalFile(bridge, taskId, request, opts) {
683
788
 
684
789
  module.exports = {
685
790
  runUiAction,
791
+ shellCommand,
792
+ detectShell,
793
+ availableShells,
686
794
  REFUSED,
687
795
  resolveInRoot,
688
796
  runShell,
package/src/meta.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
+ const { readContext } = require('./context');
6
7
  const { execFileSync } = require('node:child_process');
7
8
 
8
9
  const { CLIENT_NAME, VERSION } = require('./config');
@@ -118,26 +119,12 @@ function buildMeta(folder, { lang = 'it', pcAccess = true, activeFile = '', open
118
119
  }
119
120
 
120
121
  /**
121
- * Project context: the client sends the content of `context_<project>.md` from the
122
- * open folder when a bridge session starts, so the agent can resume the project.
122
+ * Project context: the content of `context_<project>.md` from the open folder, sent to
123
+ * the bridge with the first task of a new session (see src/context.js for the sheet and
124
+ * for the gating).
123
125
  */
124
126
  function readProjectContext(folder) {
125
- const root = path.resolve(folder);
126
- let names = [];
127
- try {
128
- names = fs.readdirSync(root).filter((n) => /^context_.*\.md$/i.test(n));
129
- } catch {
130
- return '';
131
- }
132
- if (!names.length) return '';
133
- const file = path.join(root, names[0]);
134
- try {
135
- const stat = fs.statSync(file);
136
- if (stat.size > MAX_CONTEXT_BYTES) return '';
137
- return fs.readFileSync(file, 'utf8');
138
- } catch {
139
- return '';
140
- }
127
+ return readContext(folder, { maxBytes: MAX_CONTEXT_BYTES });
141
128
  }
142
129
 
143
130
  module.exports = { POLICY, VERSION, buildMeta, workspaceMeta, readProjectContext, detectTools, which };