nio-cat 0.9.1 → 0.9.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/bin/nio.js +31 -21
- package/package.json +1 -1
- package/src/agents/generic.js +28 -0
- package/src/commands.js +11 -1
- package/src/hub.js +4 -3
- package/src/jobs.js +1 -1
- package/src/remote.js +8 -2
- package/src/telegram.js +1 -1
- package/web/app/app.js +241 -91
- package/web/app/index.html +1 -0
- package/web/app/style.css +32 -4
- package/web/bot.css +2 -3
- package/web/index.html +148 -126
- package/web/og.png +0 -0
- package/web/privadesa.html +1 -1
- package/web/sw.js +1 -1
package/bin/nio.js
CHANGED
|
@@ -46,12 +46,14 @@ const c = {
|
|
|
46
46
|
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const row = (label, value, note) => console.log(` ${c.dim(label.padEnd(10))} ${value}${note ? c.dim(' ' + note) : ''}`);
|
|
50
|
+
|
|
49
51
|
const cfg = config.load();
|
|
50
52
|
const auth = new Auth(undefined, cfg);
|
|
51
53
|
|
|
52
54
|
async function doctor() {
|
|
53
55
|
const agents = await detectAll(cfg, true);
|
|
54
|
-
console.log(c.b('\n Agents
|
|
56
|
+
console.log(c.b('\n Agents\n'));
|
|
55
57
|
for (const a of agents) {
|
|
56
58
|
const mark = a.available ? c.ok('✓') : c.dim('·');
|
|
57
59
|
const v = a.available ? c.dim(a.version || '') : c.dim(`no hi és · ${a.login}`);
|
|
@@ -93,8 +95,8 @@ function openDashboard() {
|
|
|
93
95
|
const online = !flags.local;
|
|
94
96
|
// &qr=1: el panell mostra el QR per al mòbil tot just obrir-se, a la mateixa pestanya.
|
|
95
97
|
const link = pairLink(online ? flags.app || cfg.app : local, local, cfg.token) + '&qr=1';
|
|
96
|
-
if (canOpenBrowser() && openUrl(link)) { panelOpened = true;
|
|
97
|
-
else
|
|
98
|
+
if (canOpenBrowser() && openUrl(link)) { panelOpened = true; row('Panell', online ? (flags.app || cfg.app) + '/app' : local, '(obert al navegador)'); }
|
|
99
|
+
else row('Panell', link);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
async function showPair(publicUrl) {
|
|
@@ -110,18 +112,26 @@ async function showPair(publicUrl) {
|
|
|
110
112
|
const link = pairLink(app, publicUrl, secret), code = makeCode(publicUrl, secret), qrLink = codeLink(app, code);
|
|
111
113
|
const file = writePairPage(link, code, qrLink);
|
|
112
114
|
// El QR ha de cabre al terminal: marge normal, marge d'1 mòdul i, si no hi cap, la pàgina del navegador.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.log(
|
|
118
|
-
console.log(
|
|
115
|
+
if (panelOpened) {
|
|
116
|
+
console.log(`\n ${c.b('Connecta el mòbil')}`);
|
|
117
|
+
console.log(` Al panell del navegador prem ${c.b('«Continua al mòbil»')} i escaneja el codi.`);
|
|
118
|
+
console.log(c.dim(' També pots enganxar aquest codi a Sessions → Tria un ordinador:\n'));
|
|
119
|
+
console.log(` ${code}\n`);
|
|
120
|
+
console.log(c.dim(' Serveix per a un sol dispositiu i caduca en 15 minuts. Per a un altre: nio pair.\n'));
|
|
119
121
|
} else {
|
|
120
|
-
|
|
122
|
+
const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
|
|
123
|
+
const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
|
|
124
|
+
const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
|
|
125
|
+
console.log(`\n ${c.b('Connecta el mòbil')}\n`);
|
|
126
|
+
if (margin) {
|
|
127
|
+
console.log(` Escaneja aquest codi amb la càmera:\n`);
|
|
128
|
+
console.log(terminalQr(qrLink, { margin }));
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
131
|
+
console.log(` ${margin ? 'O copia' : 'Copia'} aquest codi i enganxa'l a Sessions → Tria un ordinador:\n`);
|
|
132
|
+
console.log(` ${code}\n`);
|
|
133
|
+
console.log(c.dim(' Serveix per a un sol dispositiu i caduca en 15 minuts. Per a un altre: nio pair.\n'));
|
|
121
134
|
}
|
|
122
|
-
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
123
|
-
console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
|
|
124
|
-
console.log(c.dim(' Serveix per a un sol dispositiu i caduca en 15 minuts. Per a un altre: nio pair.\n'));
|
|
125
135
|
if (flags.open) {
|
|
126
136
|
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
127
137
|
try {
|
|
@@ -168,7 +178,7 @@ function registerMemory(force) {
|
|
|
168
178
|
if (which(cfg.agents?.codex?.bin || 'codex')) {
|
|
169
179
|
try { out.push(`Codex ${mcp.registerCodex()}`); } catch (e) { out.push(`Codex: ${e.message}`); }
|
|
170
180
|
}
|
|
171
|
-
|
|
181
|
+
row('Memòria', memory.DIR, out.length ? `(${out.join(' · ')})` : '');
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
async function start() {
|
|
@@ -181,7 +191,7 @@ async function start() {
|
|
|
181
191
|
|
|
182
192
|
console.log(`\n ${c.b('nio')} ${c.dim('· agents d’IA al teu ordinador, des del mòbil')}`);
|
|
183
193
|
await doctor();
|
|
184
|
-
|
|
194
|
+
row('Carpetes', cfg.roots.join(', '));
|
|
185
195
|
|
|
186
196
|
// Sense cloudflared no hi ha túnel: en lloc de quedar-nos sense codi QR, connectem per la wifi.
|
|
187
197
|
if (!flags.lan && !flags.url && !flags['no-tunnel'] && !which('cloudflared')) {
|
|
@@ -215,7 +225,7 @@ async function start() {
|
|
|
215
225
|
const tg = new Telegram({ cfg, hub, audit, available: jobs.available });
|
|
216
226
|
tg.hookUrl = jobs.hookUrl;
|
|
217
227
|
tg.start();
|
|
218
|
-
if (cfg.telegram?.token)
|
|
228
|
+
if (cfg.telegram?.token) row('Telegram', 'actiu', `(${(cfg.telegram.allowed || []).length} xats autoritzats)`);
|
|
219
229
|
const control = new Control({ cfg, hub, audit });
|
|
220
230
|
const remote = new Remote({ cfg, audit });
|
|
221
231
|
const routes = [...memory.routes(), ...jobs.routes(), ...voice.routes(cfg), ...tg.routes(), ...control.routes(), ...remote.routes()];
|
|
@@ -228,14 +238,14 @@ async function start() {
|
|
|
228
238
|
});
|
|
229
239
|
|
|
230
240
|
server.listen(cfg.port, host, () => {
|
|
231
|
-
|
|
241
|
+
row('Local', `http://127.0.0.1:${cfg.port}`);
|
|
232
242
|
openDashboard();
|
|
233
243
|
|
|
234
244
|
if (flags.lan) {
|
|
235
245
|
const ip = lanIps()[0];
|
|
236
246
|
if (ip) {
|
|
237
247
|
publicUrl = publicUrl || `http://${ip}:${cfg.port}`;
|
|
238
|
-
|
|
248
|
+
row('Xarxa', publicUrl, '(només la mateixa wifi)');
|
|
239
249
|
}
|
|
240
250
|
}
|
|
241
251
|
|
|
@@ -251,7 +261,7 @@ async function start() {
|
|
|
251
261
|
const named = cfg.tunnel && cfg.tunnel.mode === 'named' && !flags.quick && !flags.url && !flags['no-tunnel'] && !flags.lan ? cfg.tunnel : null;
|
|
252
262
|
if (named) {
|
|
253
263
|
const t = startConfiguredTunnel(named, cfg.port, {
|
|
254
|
-
onReady: () =>
|
|
264
|
+
onReady: () => row('Túnel', `https://${named.host}`, `${c.ok('connectat')} (adreça fixa)`),
|
|
255
265
|
onDown: (code, wait) => console.log(c.warn(` El túnel s’ha tallat (codi ${code}). El torno a obrir d’aquí a ${Math.round(wait / 1000)} s.`)),
|
|
256
266
|
});
|
|
257
267
|
if (t) {
|
|
@@ -264,7 +274,7 @@ async function start() {
|
|
|
264
274
|
if (fixed) {
|
|
265
275
|
const nt = startNamedTunnel(cfg.tunnelToken, () => {
|
|
266
276
|
publicUrl = cfg.fixedUrl;
|
|
267
|
-
|
|
277
|
+
row('Adreça', publicUrl, '(fixa)');
|
|
268
278
|
cfg.lastUrl = publicUrl;
|
|
269
279
|
config.save(cfg);
|
|
270
280
|
showPair(publicUrl);
|
|
@@ -274,7 +284,7 @@ async function start() {
|
|
|
274
284
|
|
|
275
285
|
const t = startTunnel(cfg.port, (url) => {
|
|
276
286
|
publicUrl = url;
|
|
277
|
-
|
|
287
|
+
row('Túnel', url, '(Cloudflare, sense cost)');
|
|
278
288
|
cfg.lastUrl = url;
|
|
279
289
|
config.save(cfg);
|
|
280
290
|
showPair(url);
|
package/package.json
CHANGED
package/src/agents/generic.js
CHANGED
|
@@ -15,6 +15,32 @@ function pickText(ev) {
|
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Grok Build parla el protocol ACP: {method:'session/update', params:{update:{sessionUpdate, content:{text}}}}.
|
|
19
|
+
function handleAcp(ctx, ev, state) {
|
|
20
|
+
const u = ev.params?.update || ev.update;
|
|
21
|
+
if (u && typeof u.sessionUpdate === 'string') {
|
|
22
|
+
const kind = u.sessionUpdate;
|
|
23
|
+
if (kind === 'agent_message_chunk') {
|
|
24
|
+
const t = typeof u.content === 'string' ? u.content : u.content?.text;
|
|
25
|
+
if (t) { state.sawText = true; ctx.stream(t); }
|
|
26
|
+
} else if (kind === 'tool_call') {
|
|
27
|
+
const id = String(u.toolCallId || `tool-${state.n++}`);
|
|
28
|
+
state.tools = state.tools || new Set(); state.tools.add(id);
|
|
29
|
+
ctx.tool({ id, name: u.title || u.kind || 'tool', input: u.rawInput || u.locations || {} });
|
|
30
|
+
} else if (kind === 'tool_call_update' && /completed|failed/i.test(u.status || '')) {
|
|
31
|
+
const out = Array.isArray(u.content) ? u.content.map((c) => c?.content?.text || c?.text || '').join('\n') : '';
|
|
32
|
+
ctx.toolResult({ id: String(u.toolCallId), output: String(u.rawOutput ?? out ?? ''), error: /failed/i.test(u.status) });
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (ev.result && typeof ev.result === 'object' && ev.result.stopReason) {
|
|
37
|
+
ctx.result({ cost: undefined, tokens: ev.result.usage });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (ev.error && typeof ev.error === 'object' && ev.id !== undefined) { ctx.error(ev.error.message || 'Error'); return true; }
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
function handleLine(ctx, line, state) {
|
|
19
45
|
const ev = tryJson(line);
|
|
20
46
|
if (!ev) {
|
|
@@ -22,10 +48,12 @@ function handleLine(ctx, line, state) {
|
|
|
22
48
|
ctx.stream(line + '\n');
|
|
23
49
|
return;
|
|
24
50
|
}
|
|
51
|
+
if (ev.jsonrpc || ev.method || ev.params || ev.update) { if (handleAcp(ctx, ev, state)) return; }
|
|
25
52
|
const type = String(ev.type || ev.event || ev.kind || '');
|
|
26
53
|
const sid = ev.session_id || ev.sessionId || ev.session?.id;
|
|
27
54
|
if (sid) ctx.setNative(sid);
|
|
28
55
|
|
|
56
|
+
if (ev.finished_reason === 'error' && !ev.error) { ctx.error('L’agent ha acabat amb error'); return; }
|
|
29
57
|
if (/error/i.test(type) || ev.status === 'ERROR') {
|
|
30
58
|
ctx.error(ev.error?.message || ev.error || ev.message || 'Error');
|
|
31
59
|
return;
|
package/src/commands.js
CHANGED
|
@@ -54,8 +54,18 @@ function claudeCommands(cwd) {
|
|
|
54
54
|
return [...out.values()].map((c) => ({ ...c, hint: c.hint.slice(0, 110) })).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 150);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const STATUS = { name: 'status', hint: 'Model, carpeta i permisos de la sessió', kind: 'comanda' };
|
|
58
|
+
// Antigravity resol en mode -p les comandes de només lectura i les skills (sense gastar quota).
|
|
59
|
+
const AGY_BUILTIN = [
|
|
60
|
+
{ name: 'usage', hint: 'Ús i límits del compte' }, { name: 'quota', hint: 'Quota restant' }, { name: 'credits', hint: 'Crèdits del compte' },
|
|
61
|
+
{ name: 'model', hint: 'Model actual' }, { name: 'effort', hint: 'Nivell de raonament' }, { name: 'skills', hint: 'Skills disponibles' },
|
|
62
|
+
{ name: 'permissions', hint: 'Regles de permisos' }, { name: 'hooks', hint: 'Hooks configurats' }, { name: 'config', hint: 'Configuració' },
|
|
63
|
+
{ name: 'changelog', hint: 'Novetats de la versió' }, { name: 'help', hint: 'Ajuda' },
|
|
64
|
+
];
|
|
65
|
+
|
|
57
66
|
function agentCommands(agent, cwd) {
|
|
58
|
-
if (agent === 'codex'
|
|
67
|
+
if (agent === 'codex' || agent === 'grok') return [STATUS];
|
|
68
|
+
if (agent === 'antigravity') return [STATUS, ...AGY_BUILTIN.map((c) => ({ ...c, kind: 'comanda' }))];
|
|
59
69
|
return agent === 'claude' ? claudeCommands(cwd) : [];
|
|
60
70
|
}
|
|
61
71
|
|
package/src/hub.js
CHANGED
|
@@ -170,9 +170,10 @@ class Hub extends EventEmitter {
|
|
|
170
170
|
// Codex no té /status en mode headless: ho resolem aquí amb el que sí que sap dir.
|
|
171
171
|
codexStatus(s, bp) {
|
|
172
172
|
const { execFile } = require('child_process');
|
|
173
|
-
|
|
173
|
+
const codex = s.agent === 'codex', name = byId[s.agent]?.name || s.agent;
|
|
174
|
+
execFile(bp, codex ? ['login', 'status'] : ['--version'], { timeout: 8000 }, (err, out, errOut) => {
|
|
174
175
|
const rows = String(out || errOut || '').split('\n').map((l) => l.trim()).filter(Boolean), login = rows.find((l) => /logged|log in|login/i.test(l) && !/^warning/i.test(l)) || rows[0] || (err ? 'No he pogut llegir el compte' : '');
|
|
175
|
-
const lines = [`**Compte:** ${login}`, `**Model:** ${s.model || s.modelUsed || 'el de per defecte'}`, `**Carpeta:** \`${s.cwd}\``, `**Permisos:** ${s.mode}`, `**Conversa:** ${s.nativeId ?
|
|
176
|
+
const lines = [codex ? `**Compte:** ${login}` : `**Versió:** ${login}`, `**Model:** ${s.model || s.modelUsed || 'el de per defecte'}`, `**Carpeta:** \`${s.cwd}\``, `**Permisos:** ${s.mode}`, `**Conversa:** ${s.nativeId ? `continua la de ${name}` : 'encara no ha començat'}`];
|
|
176
177
|
this.push(s, 'text', { text: lines.join('\n\n') });
|
|
177
178
|
this.touch(s);
|
|
178
179
|
});
|
|
@@ -231,7 +232,7 @@ class Hub extends EventEmitter {
|
|
|
231
232
|
|
|
232
233
|
if (!s.title) s.title = text.replace(/\s+/g, ' ').slice(0, 60);
|
|
233
234
|
this.push(s, 'user', { text });
|
|
234
|
-
if (
|
|
235
|
+
if (['codex', 'grok', 'antigravity'].includes(s.agent) && /^\/status\s*$/i.test(text)) return this.codexStatus(s, bp);
|
|
235
236
|
s.state = 'running';
|
|
236
237
|
this.push(s, 'status', { state: 'running' });
|
|
237
238
|
|
package/src/jobs.js
CHANGED
|
@@ -115,7 +115,7 @@ class Jobs {
|
|
|
115
115
|
cwd = cwd || r.cwd;
|
|
116
116
|
rule = r.rule;
|
|
117
117
|
}
|
|
118
|
-
if (!['claude', 'codex', 'demo', 'grok', 'antigravity', 'ollama'].includes(agent)) throw httpErr(400, 'Tria
|
|
118
|
+
if (!['claude', 'codex', 'demo', 'grok', 'antigravity', 'ollama'].includes(agent)) throw httpErr(400, 'Tria un agent o «automàtic»');
|
|
119
119
|
if (!cwd) cwd = this.cfg.roots?.[0];
|
|
120
120
|
if (!cwd || !fs.existsSync(cwd)) throw httpErr(400, 'Aquesta carpeta no existeix');
|
|
121
121
|
if (b.after && !this.get(b.after)) throw httpErr(400, 'La feina anterior no existeix');
|
package/src/remote.js
CHANGED
|
@@ -161,7 +161,7 @@ class Remote {
|
|
|
161
161
|
const buf = await this.grab(w, q);
|
|
162
162
|
const info = await this.info();
|
|
163
163
|
// Un JPEG de pantalla tota negra pesa molt poc: l'ordinador és bloquejat/adormit o falta el permís.
|
|
164
|
-
this.frame = { buf, t: Date.now(), w, q, blank: buf.length < 5000, sw: info.w, sh: info.h };
|
|
164
|
+
this.frame = { buf, t: Date.now(), w, q, hash: crypto.createHash('md5').update(buf).digest('hex').slice(0, 12), blank: buf.length < 5000, sw: info.w, sh: info.h };
|
|
165
165
|
return this.frame;
|
|
166
166
|
} finally {
|
|
167
167
|
this.busy = null;
|
|
@@ -265,14 +265,20 @@ class Remote {
|
|
|
265
265
|
path: '/api/desktop/frame',
|
|
266
266
|
handler: async ({ res, url }) => {
|
|
267
267
|
const f = await this.getFrame(Number(url.searchParams.get('w')), Number(url.searchParams.get('q')));
|
|
268
|
+
const same = url.searchParams.get('h') === f.hash;
|
|
269
|
+
if (same) {
|
|
270
|
+
res.writeHead(204, { 'cache-control': 'no-store', 'x-same': '1', 'x-hash': f.hash, 'x-screen-w': String(f.sw || 0), 'x-screen-h': String(f.sh || 0), 'x-blank': f.blank ? '1' : '0', 'access-control-expose-headers': 'x-same, x-hash, x-screen-w, x-screen-h, x-blank' });
|
|
271
|
+
return res.end();
|
|
272
|
+
}
|
|
268
273
|
res.writeHead(200, {
|
|
269
274
|
'content-type': 'image/jpeg',
|
|
275
|
+
'x-hash': f.hash,
|
|
270
276
|
'content-length': f.buf.length,
|
|
271
277
|
'cache-control': 'no-store',
|
|
272
278
|
'x-screen-w': String(f.sw || 0),
|
|
273
279
|
'x-screen-h': String(f.sh || 0),
|
|
274
280
|
'x-blank': f.blank ? '1' : '0',
|
|
275
|
-
'access-control-expose-headers': 'x-screen-w, x-screen-h, x-blank',
|
|
281
|
+
'access-control-expose-headers': 'x-hash, x-screen-w, x-screen-h, x-blank',
|
|
276
282
|
});
|
|
277
283
|
res.end(f.buf);
|
|
278
284
|
},
|
package/src/telegram.js
CHANGED
|
@@ -284,7 +284,7 @@ class Telegram {
|
|
|
284
284
|
this.stop();
|
|
285
285
|
this.offset = undefined;
|
|
286
286
|
}
|
|
287
|
-
if (['claude', 'codex', 'demo'].includes(body.agent)) t.agent = body.agent;
|
|
287
|
+
if (['claude', 'codex', 'grok', 'antigravity', 'demo'].includes(body.agent)) t.agent = body.agent;
|
|
288
288
|
if (['llegir', 'preguntar', 'tot'].includes(body.mode)) t.mode = body.mode;
|
|
289
289
|
if (typeof body.cwd === 'string' && body.cwd) t.cwd = body.cwd;
|
|
290
290
|
if (typeof body.notify === 'boolean') t.notify = body.notify;
|