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 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 instal·lats\n'));
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; console.log(` ${c.dim('Panell:')} ${online ? (flags.app || cfg.app) + '/app' : local} ${c.dim('(obert al navegador; --no-open per evitar-ho' + (online ? '; --local per fer servir la còpia d\'aquest ordinador' : '') + ')')}`); }
97
- else console.log(` ${c.dim('Panell:')} ${link}`);
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
- const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
114
- const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
115
- const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
116
- if (margin) {
117
- console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
118
- console.log(terminalQr(qrLink, { margin }));
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
- console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} ${panelOpened ? 'El tens al panell del navegador.' : 'Fes-lo més gran, o obre el panell i prem «Continua al mòbil».'}`);
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
- console.log(` ${c.dim('Memòria:')} ${memory.DIR}${out.length ? c.dim(` (${out.join(' · ')})`) : ''}`);
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
- console.log(` ${c.dim('Carpetes compartides:')} ${cfg.roots.join(', ')}`);
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) console.log(` ${c.dim('Telegram:')} actiu ${c.dim(`(${(cfg.telegram.allowed || []).length} xats autoritzats)`)}`);
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
- console.log(` ${c.dim('Local:')} http://127.0.0.1:${cfg.port}`);
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
- console.log(` ${c.dim('Xarxa:')} ${publicUrl} ${c.dim('(només la mateixa wifi)')}`);
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: () => console.log(` ${c.dim('Túnel:')} https://${named.host} ${c.ok('connectat')} ${c.dim('(adreça fixa)')}`),
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
- console.log(` ${c.dim('Adreça fixa:')} ${publicUrl}`);
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
- console.log(` ${c.dim('Túnel:')} ${url} ${c.dim('(Cloudflare, sense cost)')}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nio-cat",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "nio: fes servir Claude Code, Codex, Grok, Antigravity i Ollama del teu ordinador des del mòbil.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "f.cat",
@@ -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') return [{ name: 'status', hint: 'Compte, model i carpeta de la sessió', kind: 'comanda' }];
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
- execFile(bp, ['login', 'status'], { timeout: 8000 }, (err, out, errOut) => {
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 ? 'continua la de Codex' : 'encara no ha començat'}`];
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 (s.agent === 'codex' && /^\/status\s*$/i.test(text)) return this.codexStatus(s, bp);
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 Claude Code, Codex o «automàtic»');
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;