nio-cat 0.9.0 → 0.9.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nio-cat",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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
@@ -12,6 +12,22 @@ const httpErr = (status, message) => Object.assign(new Error(message), { status
12
12
  const TOKEN_RE = /^\d{5,15}:[\w-]{30,60}$/;
13
13
  const clip = (s, n) => (String(s).length > n ? `${String(s).slice(0, n)}…` : String(s));
14
14
 
15
+ // Markdown de l'agent -> HTML de Telegram (negreta, cursiva, codi, enllaços, títols i llistes).
16
+ const esc = (t) => t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
17
+ function toHtml(src) {
18
+ const keep = [];
19
+ const hold = (h) => `\u0000${keep.push(h) - 1}\u0000`;
20
+ let t = String(src).replace(/```[^\n`]*\n?([\s\S]*?)```/g, (_, c) => hold(`<pre>${esc(c.replace(/\n$/, ''))}</pre>`));
21
+ t = t.replace(/`([^`\n]+)`/g, (_, c) => hold(`<code>${esc(c)}</code>`));
22
+ t = esc(t);
23
+ t = t.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
24
+ .replace(/^\s*[-*+]\s+/gm, '• ')
25
+ .replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2">$1</a>')
26
+ .replace(/\*\*([^*\n]+?)\*\*/g, '<b>$1</b>')
27
+ .replace(/(^|[\s(])\*([^*\s][^*\n]*?)\*(?=[\s).,;:!?]|$)/g, '$1<i>$2</i>');
28
+ return t.replace(/\u0000(\d+)\u0000/g, (_, i) => keep[Number(i)]);
29
+ }
30
+
15
31
  class Telegram {
16
32
  constructor({ cfg, hub, audit, available = () => ['claude', 'codex'] }) {
17
33
  this.cfg = cfg;
@@ -153,7 +169,8 @@ class Telegram {
153
169
  this.chatOf.set(sid, chat);
154
170
  this.hub.send(sid, text, this.hookUrl ? this.hookUrl() : undefined);
155
171
  this.audit.add({ type: 'telegram', what: `Missatge de Telegram: ${clip(text, 120)}`, by: `Telegram · ${who}`, session: sid });
156
- return this.send(chat, '👌 Hi treballo.');
172
+ this.call('sendChatAction', { chat_id: chat, action: 'typing' }).catch(() => {});
173
+ return;
157
174
  } catch (e) {
158
175
  return this.send(chat, `No he pogut començar: ${e.message}`);
159
176
  }
@@ -211,7 +228,9 @@ class Telegram {
211
228
  }
212
229
 
213
230
  send(chat, text) {
214
- return this.call('sendMessage', { chat_id: chat, text: clip(text, 4000) }).catch(() => {});
231
+ const raw = clip(text, 3500);
232
+ return this.call('sendMessage', { chat_id: chat, text: toHtml(raw), parse_mode: 'HTML', link_preview_options: { is_disabled: true } })
233
+ .catch(() => this.call('sendMessage', { chat_id: chat, text: raw }).catch(() => {}));
215
234
  }
216
235
 
217
236
  async download(fileId) {
@@ -265,7 +284,7 @@ class Telegram {
265
284
  this.stop();
266
285
  this.offset = undefined;
267
286
  }
268
- 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;
269
288
  if (['llegir', 'preguntar', 'tot'].includes(body.mode)) t.mode = body.mode;
270
289
  if (typeof body.cwd === 'string' && body.cwd) t.cwd = body.cwd;
271
290
  if (typeof body.notify === 'boolean') t.notify = body.notify;