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 +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 +22 -3
- package/web/app/app.js +361 -101
- package/web/app/index.html +1 -0
- package/web/app/style.css +48 -4
- package/web/board.css +50 -0
- package/web/board.js +29 -5
- package/web/index.html +67 -8
- package/web/privadesa.html +1 -1
- package/web/sw.js +1 -1
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
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
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
|
-
|
|
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;
|