@lardexx/arenacode 2.10.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/README.md +35 -0
- package/package.json +2 -2
- package/src/cli.js +29 -2
- package/src/context.js +234 -0
- package/src/local.js +15 -2
- package/src/meta.js +5 -18
- package/src/tui/app.js +9 -0
package/README.md
CHANGED
|
@@ -271,6 +271,41 @@ Every file path is resolved **inside the open folder**; paths that escape it are
|
|
|
271
271
|
the folder itself. Local operations run with your user's permissions — `--confirm-writes` adds a confirmation for
|
|
272
272
|
mutating file operations, `--yes` skips confirmations for terminal commands.
|
|
273
273
|
|
|
274
|
+
### Project context
|
|
275
|
+
|
|
276
|
+
Every folder gets its own log, `context_<project>.md`, next to the files you are working on. It is the desktop
|
|
277
|
+
client's file, with the desktop's own template (mined from `ArenaCode-x64.exe`):
|
|
278
|
+
|
|
279
|
+
```markdown
|
|
280
|
+
# Contesto del progetto progetto-web
|
|
281
|
+
|
|
282
|
+
Sessione: 2026-11-02 21:45 (nome-del-bridge.tunnl.gg)
|
|
283
|
+
_Nessun task ancora._
|
|
284
|
+
<!-- agente:inizio -->
|
|
285
|
+
<!-- agente:fine -->
|
|
286
|
+
|
|
287
|
+
Aggiornato automaticamente da ArenaCode a fine di ogni job. La API key non è scritta qui.
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The title, the session label, the placeholder and the footer follow the interface language (`it`, `en`, `es`).
|
|
291
|
+
What happens when:
|
|
292
|
+
|
|
293
|
+
* **The folder opens or a session starts** → the client creates the file if it is missing, and adds a `Sessione:`
|
|
294
|
+
line for the new session. An existing file is **never overwritten**: the agent's block lives inside it.
|
|
295
|
+
* **At the end of every task** the agent rewrites the block between `<!-- agente:inizio -->` and
|
|
296
|
+
`<!-- agente:fine -->` (date, task id, files changed, commits, decisions, what is still open, how it was checked —
|
|
297
|
+
at most 40 lines). The client saves it **at once, without Accept or Reject**: the card only shows what changed.
|
|
298
|
+
* **The first task of a new bridge session** carries the whole file as `project_context`, so the bridge writes it
|
|
299
|
+
to `runtime/project_context.txt` and tells the agent to read it in parts (`agentctl.py context --part N`). Later
|
|
300
|
+
tasks do not repeat it — a new bridge (`/health` → `started_at`) or a different folder makes it travel again.
|
|
301
|
+
The file is never summarised or truncated.
|
|
302
|
+
* **Commits** never include it: `git add -A` runs, then the sheet is unstaged — `git add -A, never
|
|
303
|
+
context_<project>.md`, as the desktop puts it.
|
|
304
|
+
|
|
305
|
+
`test/context.js` checks the template verbatim in the three languages, the "create once, never overwrite" rule,
|
|
306
|
+
the send-once-per-session rule and the git exclusion; the integration test checks that a real task creates the
|
|
307
|
+
sheet and that the bridge receives it.
|
|
308
|
+
|
|
274
309
|
### Shells
|
|
275
310
|
|
|
276
311
|
Terminal cards run in a real shell, and which one is not fixed:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lardexx/arenacode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
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/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
|
|
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"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"arenacode",
|
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
|
|
|
@@ -286,16 +287,42 @@ function agentPrompt() {
|
|
|
286
287
|
}
|
|
287
288
|
|
|
288
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
|
+
|
|
289
308
|
async function createTask(ctx, instruction, { extraInstructions = '' } = {}) {
|
|
290
309
|
const meta = buildMeta(ctx.folder, { lang: ctx.lang, pcAccess: ctx.pcAccess, activeFile: '' });
|
|
291
|
-
|
|
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) : '';
|
|
292
316
|
const body = {
|
|
293
317
|
instruction,
|
|
294
318
|
wait: 0,
|
|
295
319
|
meta,
|
|
296
320
|
agent_prompt: agentPrompt(),
|
|
297
321
|
};
|
|
298
|
-
if (projectContext)
|
|
322
|
+
if (projectContext) {
|
|
323
|
+
body.project_context = projectContext;
|
|
324
|
+
markContextSent({ folder: ctx.folder, url: ctx.url, startedAt });
|
|
325
|
+
}
|
|
299
326
|
if (extraInstructions) body.instruction = `${instruction}\n\n${extraInstructions}`;
|
|
300
327
|
|
|
301
328
|
const res = await ctx.bridge.createTask(body);
|
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;
|
|
@@ -426,6 +435,8 @@ function runFsOp(op, body, root) {
|
|
|
426
435
|
case 'git_commit': {
|
|
427
436
|
const add = git(['add', '-A'], root);
|
|
428
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);
|
|
429
440
|
const commit = git(['commit', '-m', String(body.content || 'ArenaCode')], root);
|
|
430
441
|
return { ok: commit.ok, output: commit.output };
|
|
431
442
|
}
|
|
@@ -475,7 +486,9 @@ async function handleLocalFs(bridge, taskId, request, opts) {
|
|
|
475
486
|
write(` ${c.bold(c.yellow('▤ files'))} ${c.dim('·')} ${c.cyan(op)} ${c.gray(request.path || '.')}${request.to ? c.gray(` → ${request.to}`) : ''}`);
|
|
476
487
|
|
|
477
488
|
const mutating = ['write', 'replace', 'rm', 'mv', 'mkdir'].includes(op);
|
|
478
|
-
|
|
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
|
|
479
492
|
? decide({
|
|
480
493
|
approval: opts.approval,
|
|
481
494
|
mode: session.modeOf(taskId),
|
|
@@ -485,7 +498,7 @@ async function handleLocalFs(bridge, taskId, request, opts) {
|
|
|
485
498
|
})
|
|
486
499
|
: 'auto';
|
|
487
500
|
// --confirm-writes keeps asking even when the client runs in Proceed
|
|
488
|
-
const mustAsk = mutating && (verdict === 'ask' || opts.confirmWrites);
|
|
501
|
+
const mustAsk = mutating && !isProjectLog && (verdict === 'ask' || opts.confirmWrites);
|
|
489
502
|
if (mustAsk) {
|
|
490
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']);
|
|
491
504
|
if (key === 'n') {
|
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
|
|
122
|
-
*
|
|
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
|
-
|
|
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 };
|
package/src/tui/app.js
CHANGED
|
@@ -16,6 +16,7 @@ const { Bridge } = require('../bridge');
|
|
|
16
16
|
const { buildMeta, readProjectContext } = require('../meta');
|
|
17
17
|
const { runShell, runFsOp, resolveInRoot, detectShell, availableShells, shellCommand } = require('../local');
|
|
18
18
|
const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, VERSION } = require('../config');
|
|
19
|
+
const { ensureContextFile, touchSession } = require('../context');
|
|
19
20
|
|
|
20
21
|
const { MODE_LINES, modeLabel, nextMode, normaliseMode } = require('../modes');
|
|
21
22
|
const { attachRefs, attachImage, decorate, strings } = require('../attach');
|
|
@@ -90,6 +91,14 @@ class App {
|
|
|
90
91
|
|
|
91
92
|
// ---------------------------------------------------------------- lifecycle
|
|
92
93
|
async start() {
|
|
94
|
+
// the sheet belongs to the folder, not to the task: it is there as soon as the
|
|
95
|
+
// folder is open, and it records this session next to the previous ones
|
|
96
|
+
try {
|
|
97
|
+
const made = ensureContextFile(this.folder, { lang: this.lang, bridgeUrl: this.url });
|
|
98
|
+
if (!made.created) touchSession(this.folder, { lang: this.lang, bridgeUrl: this.url });
|
|
99
|
+
} catch {
|
|
100
|
+
/* a read-only folder must not stop the client */
|
|
101
|
+
}
|
|
93
102
|
this.term.enter();
|
|
94
103
|
this.term.on('key', (chunk) => {
|
|
95
104
|
this.parser.feed(chunk);
|