nio-cat 0.0.0-stage → 0.1.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 +13 -2
- package/bin/nio.js +191 -0
- package/package.json +40 -4
- package/src/agents/antigravity.js +43 -0
- package/src/agents/claude.js +96 -0
- package/src/agents/codex.js +102 -0
- package/src/agents/demo.js +60 -0
- package/src/agents/generic.js +60 -0
- package/src/agents/grok.js +40 -0
- package/src/agents/index.js +45 -0
- package/src/agents/ollama.js +163 -0
- package/src/claude-hook.js +49 -0
- package/src/config.js +64 -0
- package/src/hub.js +375 -0
- package/src/pair.js +61 -0
- package/src/proc.js +143 -0
- package/src/push.js +47 -0
- package/src/server.js +352 -0
- package/src/tunnel.js +28 -0
- package/src/vendor/qrcode.js +2297 -0
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nio
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fes servir els agents d’IA del teu ordinador (Claude Code, Codex, Grok, Antigravity i Ollama) des del mòbil. Segueix-los en directe i aprova cada acció.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx nio-cat
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Executa-ho a la carpeta dels teus projectes i escaneja el codi QR que apareix amb la càmera del mòbil.
|
|
10
|
+
|
|
11
|
+
- Sense `cloudflared`, et connectes per la wifi de casa.
|
|
12
|
+
- Amb `cloudflared` (a macOS: `brew install cloudflared`), des de qualsevol lloc.
|
|
13
|
+
|
|
14
|
+
Opcions: `npx nio-cat help`. Més informació a [nio.cat](https://www.nio.cat).
|
package/bin/nio.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const config = require('../src/config');
|
|
9
|
+
const { Hub } = require('../src/hub');
|
|
10
|
+
const { createServer } = require('../src/server');
|
|
11
|
+
const { detectAll } = require('../src/agents');
|
|
12
|
+
const { startTunnel } = require('../src/tunnel');
|
|
13
|
+
const { pairLink, terminalQr, writePairPage } = require('../src/pair');
|
|
14
|
+
const { which } = require('../src/proc');
|
|
15
|
+
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const flags = {};
|
|
18
|
+
const pos = [];
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a.startsWith('--')) {
|
|
22
|
+
const [k, v] = a.slice(2).split('=');
|
|
23
|
+
if (v !== undefined) flags[k] = v;
|
|
24
|
+
else if (['port', 'url', 'app'].includes(k)) flags[k] = argv[++i];
|
|
25
|
+
else flags[k] = true;
|
|
26
|
+
} else pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
const cmd = ['start', 'pair', 'doctor', 'reset-token', 'help'].includes(pos[0]) ? pos.shift() : 'start';
|
|
29
|
+
|
|
30
|
+
const c = {
|
|
31
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
32
|
+
b: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
33
|
+
ok: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
34
|
+
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const cfg = config.load();
|
|
38
|
+
|
|
39
|
+
async function doctor() {
|
|
40
|
+
const agents = await detectAll(cfg, true);
|
|
41
|
+
console.log(c.b('\n Agents instal·lats\n'));
|
|
42
|
+
for (const a of agents) {
|
|
43
|
+
const mark = a.available ? c.ok('✓') : c.dim('·');
|
|
44
|
+
const v = a.available ? c.dim(a.version || '') : c.dim(`no hi és · ${a.login}`);
|
|
45
|
+
console.log(` ${mark} ${a.name.padEnd(12)} ${v}${a.beta ? c.warn(' beta') : ''}`);
|
|
46
|
+
}
|
|
47
|
+
console.log('');
|
|
48
|
+
return agents;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lanIps() {
|
|
52
|
+
return Object.values(os.networkInterfaces())
|
|
53
|
+
.flat()
|
|
54
|
+
.filter((i) => i && i.family === 'IPv4' && !i.internal)
|
|
55
|
+
.map((i) => i.address);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function showPair(publicUrl) {
|
|
59
|
+
const link = pairLink(flags.app || cfg.app, publicUrl, cfg.token);
|
|
60
|
+
const file = writePairPage(link);
|
|
61
|
+
console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')} ${c.dim('(o obre ' + file + ')')}\n`);
|
|
62
|
+
console.log(terminalQr(link));
|
|
63
|
+
console.log(`\n ${c.dim('Enllaç:')} ${link}\n`);
|
|
64
|
+
console.log(c.dim(' Aquest enllaç dona accés als teus agents. Guarda\'l com una contrasenya.\n'));
|
|
65
|
+
if (flags.open) {
|
|
66
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
67
|
+
try {
|
|
68
|
+
spawn(opener, [file], { stdio: 'ignore', detached: true }).unref();
|
|
69
|
+
} catch {
|
|
70
|
+
/* sense navegador */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function start() {
|
|
76
|
+
if (flags.port) cfg.port = Number(flags.port);
|
|
77
|
+
if (flags.app) cfg.app = flags.app;
|
|
78
|
+
// Sense carpetes a la línia d'ordres, es comparteix la carpeta on s'ha engegat.
|
|
79
|
+
cfg.roots = pos.length ? pos.map((p) => path.resolve(p)) : [process.cwd()];
|
|
80
|
+
cfg.roots = cfg.roots.filter((r) => fs.existsSync(r));
|
|
81
|
+
config.save(cfg);
|
|
82
|
+
|
|
83
|
+
console.log(`\n ${c.b('nio')} ${c.dim('· agents d’IA al teu ordinador, des del mòbil')}`);
|
|
84
|
+
await doctor();
|
|
85
|
+
console.log(` ${c.dim('Carpetes compartides:')} ${cfg.roots.join(', ')}`);
|
|
86
|
+
|
|
87
|
+
// Sense cloudflared no hi ha túnel: en lloc de quedar-nos sense codi QR, connectem per la wifi.
|
|
88
|
+
if (!flags.lan && !flags.url && !flags['no-tunnel'] && !which('cloudflared')) {
|
|
89
|
+
flags.lan = true;
|
|
90
|
+
console.log(c.warn('\n No tinc cloudflared, així que et connecto per la wifi: el mòbil ha d’estar a la mateixa xarxa.'));
|
|
91
|
+
console.log(c.dim(' Per connectar-t’hi des de qualsevol lloc, instal·la’l (a macOS: brew install cloudflared) i torna a engegar nio.'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hub = new Hub(cfg);
|
|
95
|
+
let publicUrl = flags.url || null;
|
|
96
|
+
const host = flags.lan ? '0.0.0.0' : '127.0.0.1';
|
|
97
|
+
const server = createServer({ cfg, hub, getPublicUrl: () => publicUrl });
|
|
98
|
+
|
|
99
|
+
server.on('error', (e) => {
|
|
100
|
+
if (e.code === 'EADDRINUSE') console.error(c.warn(`\n El port ${cfg.port} ja el fa servir un altre programa. Tria\'n un altre: nio --port 7718\n`));
|
|
101
|
+
else console.error(e);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
server.listen(cfg.port, host, () => {
|
|
106
|
+
console.log(` ${c.dim('Local:')} http://127.0.0.1:${cfg.port}`);
|
|
107
|
+
|
|
108
|
+
if (flags.lan) {
|
|
109
|
+
const ip = lanIps()[0];
|
|
110
|
+
if (ip) {
|
|
111
|
+
publicUrl = publicUrl || `http://${ip}:${cfg.port}`;
|
|
112
|
+
console.log(` ${c.dim('Xarxa:')} ${publicUrl} ${c.dim('(només la mateixa wifi)')}`);
|
|
113
|
+
if (!flags.url) {
|
|
114
|
+
// A la xarxa local, l'app es carrega des del mateix daemon (mateix origen).
|
|
115
|
+
flags.app = publicUrl;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (publicUrl) return showPair(publicUrl);
|
|
121
|
+
|
|
122
|
+
if (flags['no-tunnel']) {
|
|
123
|
+
publicUrl = `http://127.0.0.1:${cfg.port}`;
|
|
124
|
+
flags.app = flags.app || publicUrl;
|
|
125
|
+
return showPair(publicUrl);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const t = startTunnel(cfg.port, (url) => {
|
|
129
|
+
publicUrl = url;
|
|
130
|
+
console.log(` ${c.dim('Túnel:')} ${url} ${c.dim('(Cloudflare, sense cost)')}`);
|
|
131
|
+
cfg.lastUrl = url;
|
|
132
|
+
config.save(cfg);
|
|
133
|
+
showPair(url);
|
|
134
|
+
});
|
|
135
|
+
if (!t) {
|
|
136
|
+
console.log(c.warn('\n Falta cloudflared, el túnel que permet connectar-s\'hi des de qualsevol xarxa.'));
|
|
137
|
+
console.log(c.dim(' Instal·la\'l (a macOS: brew install cloudflared) o tria una alternativa:'));
|
|
138
|
+
console.log(c.dim(' nio --lan només dins la mateixa wifi'));
|
|
139
|
+
console.log(c.dim(' nio --url https://… una adreça teva (Tailscale o similar)\n'));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(c.dim(' Obrint el túnel…'));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const bye = () => {
|
|
146
|
+
for (const [sid] of hub.runs) hub.stop(sid);
|
|
147
|
+
process.exit(0);
|
|
148
|
+
};
|
|
149
|
+
process.on('SIGINT', bye);
|
|
150
|
+
process.on('SIGTERM', bye);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
(async () => {
|
|
154
|
+
switch (cmd) {
|
|
155
|
+
case 'doctor':
|
|
156
|
+
await doctor();
|
|
157
|
+
break;
|
|
158
|
+
case 'pair':
|
|
159
|
+
if (!flags.url && !cfg.lastUrl) {
|
|
160
|
+
console.log(' Encara no hi ha cap adreça desada. Engega nio primer.');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
showPair(flags.url || cfg.lastUrl);
|
|
164
|
+
break;
|
|
165
|
+
case 'reset-token':
|
|
166
|
+
cfg.token = crypto.randomBytes(24).toString('base64url');
|
|
167
|
+
cfg.pushSubs = [];
|
|
168
|
+
config.save(cfg);
|
|
169
|
+
console.log(' Clau nova creada. Cap mòbil aparellat abans no hi té accés.');
|
|
170
|
+
break;
|
|
171
|
+
case 'help':
|
|
172
|
+
console.log(`
|
|
173
|
+
${c.b('nio')} · agents d’IA al teu ordinador, des del mòbil
|
|
174
|
+
|
|
175
|
+
nio [carpetes…] Engega nio amb aquestes carpetes (si no n'hi poses, la carpeta actual)
|
|
176
|
+
--lan Connexió dins la mateixa wifi
|
|
177
|
+
--url <adreça> Una adreça pública teva (Tailscale o similar)
|
|
178
|
+
--no-tunnel Només des d'aquest ordinador
|
|
179
|
+
--port <n> Port local (7717 si no dius res)
|
|
180
|
+
--open Obre al navegador la pàgina del codi QR
|
|
181
|
+
nio pair Mostra el codi QR una altra vegada
|
|
182
|
+
nio doctor Llista els agents instal·lats
|
|
183
|
+
nio reset-token Crea una clau nova i desconnecta tots els mòbils
|
|
184
|
+
|
|
185
|
+
Configuració: ${config.FILE}
|
|
186
|
+
`);
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
await start();
|
|
190
|
+
}
|
|
191
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nio-cat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "nio: fes servir Claude Code, Codex, Grok, Antigravity i Ollama del teu ordinador des del mòbil.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "f.cat",
|
|
7
|
+
"homepage": "https://www.nio.cat",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nio": "bin/nio.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"web",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18.17"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node bin/nio.js",
|
|
22
|
+
"demo": "node bin/nio.js --lan",
|
|
23
|
+
"prepack": "node scripts/copy-web.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"claude-code",
|
|
27
|
+
"codex",
|
|
28
|
+
"grok",
|
|
29
|
+
"antigravity",
|
|
30
|
+
"ollama",
|
|
31
|
+
"agents",
|
|
32
|
+
"mobile"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/cescps/nio.git",
|
|
37
|
+
"directory": "daemon"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/cescps/nio/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Google Antigravity CLI (`agy`). Només escriu si té un terminal, així que
|
|
3
|
+
// s'executa dins d'un pseudo-terminal amb `script`.
|
|
4
|
+
const { run } = require('../proc');
|
|
5
|
+
const { handleLine } = require('./generic');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
id: 'antigravity',
|
|
9
|
+
name: 'Antigravity',
|
|
10
|
+
bin: 'agy',
|
|
11
|
+
beta: true,
|
|
12
|
+
login: 'agy (i entra amb el compte de Google)',
|
|
13
|
+
approvals: false,
|
|
14
|
+
|
|
15
|
+
start(ctx) {
|
|
16
|
+
const { session, cfg } = ctx;
|
|
17
|
+
// agy no té "resume" documentat: hi afegim un resum curt de la conversa.
|
|
18
|
+
const prev = ctx
|
|
19
|
+
.history()
|
|
20
|
+
.slice(-8)
|
|
21
|
+
.map((m) => `${m.role === 'user' ? 'Usuari' : 'Assistent'}: ${m.text.slice(0, 600)}`)
|
|
22
|
+
.join('\n');
|
|
23
|
+
const prompt = prev
|
|
24
|
+
? `Context de la conversa:\n${prev}\n\nMissatge nou de l'usuari:\n${ctx.prompt}`
|
|
25
|
+
: ctx.prompt;
|
|
26
|
+
|
|
27
|
+
const args = ['-p', prompt, '--output-format', 'stream-json'];
|
|
28
|
+
if (session.mode === 'tot') args.push('--dangerously-skip-permissions');
|
|
29
|
+
if (session.model) args.push('--model', session.model);
|
|
30
|
+
for (const a of (cfg.agents?.antigravity?.extraArgs || [])) args.push(a);
|
|
31
|
+
|
|
32
|
+
const state = { n: 0 };
|
|
33
|
+
return run({
|
|
34
|
+
bin: ctx.binPath,
|
|
35
|
+
args,
|
|
36
|
+
cwd: session.cwd,
|
|
37
|
+
tty: true,
|
|
38
|
+
onLine: (line) => handleLine(ctx, line, state),
|
|
39
|
+
onErr: (line) => ctx.log(line),
|
|
40
|
+
onExit: (code, err) => ctx.done(code, err),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Claude Code en mode headless (`claude -p … --output-format stream-json`).
|
|
3
|
+
// Les aprovacions arriben al mòbil gràcies a un hook PreToolUse.
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { run, tryJson } = require('../proc');
|
|
6
|
+
|
|
7
|
+
const HOOK = path.join(__dirname, '..', 'claude-hook.js');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
id: 'claude',
|
|
11
|
+
name: 'Claude Code',
|
|
12
|
+
bin: 'claude',
|
|
13
|
+
login: 'claude (i entra amb el teu compte)',
|
|
14
|
+
approvals: true,
|
|
15
|
+
|
|
16
|
+
start(ctx) {
|
|
17
|
+
const { session, prompt, cfg } = ctx;
|
|
18
|
+
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
19
|
+
if (session.nativeId) args.push('--resume', session.nativeId);
|
|
20
|
+
if (session.model) args.push('--model', session.model);
|
|
21
|
+
|
|
22
|
+
const env = {};
|
|
23
|
+
if (session.mode === 'llegir') {
|
|
24
|
+
args.push('--permission-mode', 'plan');
|
|
25
|
+
} else if (session.mode === 'tot') {
|
|
26
|
+
args.push('--permission-mode', 'bypassPermissions');
|
|
27
|
+
} else {
|
|
28
|
+
// "preguntar": el hook envia cada acció que no sigui de lectura al mòbil.
|
|
29
|
+
const settings = {
|
|
30
|
+
hooks: {
|
|
31
|
+
PreToolUse: [
|
|
32
|
+
{
|
|
33
|
+
matcher: '*',
|
|
34
|
+
hooks: [{ type: 'command', command: `node "${HOOK}"`, timeout: 900 }],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
args.push('--settings', JSON.stringify(settings));
|
|
40
|
+
env.NIO_HOOK_URL = ctx.hook.url;
|
|
41
|
+
env.NIO_HOOK_KEY = ctx.hook.key;
|
|
42
|
+
env.NIO_SESSION = session.id;
|
|
43
|
+
}
|
|
44
|
+
for (const a of (cfg.agents?.claude?.extraArgs || [])) args.push(a);
|
|
45
|
+
|
|
46
|
+
return run({
|
|
47
|
+
bin: ctx.binPath,
|
|
48
|
+
args,
|
|
49
|
+
cwd: session.cwd,
|
|
50
|
+
env,
|
|
51
|
+
onLine: (line) => handle(ctx, line),
|
|
52
|
+
onErr: (line) => ctx.log(line),
|
|
53
|
+
onExit: (code, err) => ctx.done(code, err),
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function handle(ctx, line) {
|
|
59
|
+
const ev = tryJson(line);
|
|
60
|
+
if (!ev) return ctx.log(line);
|
|
61
|
+
switch (ev.type) {
|
|
62
|
+
case 'system':
|
|
63
|
+
if (ev.subtype === 'init') {
|
|
64
|
+
if (ev.session_id) ctx.setNative(ev.session_id);
|
|
65
|
+
if (ev.model) ctx.meta({ model: ev.model });
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case 'assistant':
|
|
69
|
+
for (const b of ev.message?.content || []) {
|
|
70
|
+
if (b.type === 'text' && b.text) ctx.text(b.text);
|
|
71
|
+
else if (b.type === 'tool_use') ctx.tool({ id: b.id, name: b.name, input: b.input });
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case 'user':
|
|
75
|
+
for (const b of ev.message?.content || []) {
|
|
76
|
+
if (b.type === 'tool_result') {
|
|
77
|
+
const out = Array.isArray(b.content)
|
|
78
|
+
? b.content.map((c) => c.text || '').join('\n')
|
|
79
|
+
: String(b.content ?? '');
|
|
80
|
+
ctx.toolResult({ id: b.tool_use_id, output: out, error: !!b.is_error });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'result':
|
|
85
|
+
if (ev.session_id) ctx.setNative(ev.session_id);
|
|
86
|
+
ctx.result({
|
|
87
|
+
cost: ev.total_cost_usd,
|
|
88
|
+
ms: ev.duration_ms,
|
|
89
|
+
turns: ev.num_turns,
|
|
90
|
+
error: ev.is_error ? ev.result || ev.subtype : null,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
break; // stream_event, rate_limit_event, etc.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Codex CLI en mode headless (`codex exec --json`).
|
|
3
|
+
// `codex exec` no pot fer preguntes: el mode decideix el sandbox.
|
|
4
|
+
const { run, tryJson } = require('../proc');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: 'codex',
|
|
8
|
+
name: 'Codex',
|
|
9
|
+
bin: 'codex',
|
|
10
|
+
login: 'codex login (amb el compte de ChatGPT)',
|
|
11
|
+
approvals: false,
|
|
12
|
+
|
|
13
|
+
start(ctx) {
|
|
14
|
+
const { session, prompt, cfg } = ctx;
|
|
15
|
+
const args = ['exec', '--json', '--skip-git-repo-check'];
|
|
16
|
+
if (session.mode === 'llegir') args.push('--sandbox', 'read-only');
|
|
17
|
+
else if (session.mode === 'tot') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
18
|
+
else args.push('--sandbox', 'workspace-write');
|
|
19
|
+
if (session.model) args.push('--model', session.model);
|
|
20
|
+
for (const a of (cfg.agents?.codex?.extraArgs || [])) args.push(a);
|
|
21
|
+
if (session.nativeId) args.push('resume', session.nativeId);
|
|
22
|
+
args.push(prompt);
|
|
23
|
+
|
|
24
|
+
const started = new Set();
|
|
25
|
+
return run({
|
|
26
|
+
bin: ctx.binPath,
|
|
27
|
+
args,
|
|
28
|
+
cwd: session.cwd,
|
|
29
|
+
onLine: (line) => handle(ctx, line, started),
|
|
30
|
+
onErr: (line) => ctx.log(line),
|
|
31
|
+
onExit: (code, err) => ctx.done(code, err),
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function toolOf(item) {
|
|
37
|
+
switch (item.type) {
|
|
38
|
+
case 'command_execution':
|
|
39
|
+
return { name: 'Shell', input: { command: item.command } };
|
|
40
|
+
case 'file_change':
|
|
41
|
+
return { name: 'Edita', input: { fitxers: (item.changes || []).map((c) => `${c.kind || ''} ${c.path}`.trim()) } };
|
|
42
|
+
case 'mcp_tool_call':
|
|
43
|
+
return { name: `${item.server || 'mcp'}.${item.tool || ''}`, input: item.arguments || {} };
|
|
44
|
+
case 'web_search':
|
|
45
|
+
return { name: 'Cerca web', input: { query: item.query } };
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handle(ctx, line, started) {
|
|
52
|
+
const ev = tryJson(line);
|
|
53
|
+
if (!ev) return ctx.log(line);
|
|
54
|
+
const item = ev.item || {};
|
|
55
|
+
switch (ev.type) {
|
|
56
|
+
case 'thread.started':
|
|
57
|
+
if (ev.thread_id) ctx.setNative(ev.thread_id);
|
|
58
|
+
break;
|
|
59
|
+
case 'item.started': {
|
|
60
|
+
const t = toolOf(item);
|
|
61
|
+
if (t) {
|
|
62
|
+
started.add(item.id);
|
|
63
|
+
ctx.tool({ id: item.id, ...t });
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case 'item.completed': {
|
|
68
|
+
if (item.type === 'agent_message' && item.text) {
|
|
69
|
+
ctx.text(item.text);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (item.type === 'error') {
|
|
73
|
+
ctx.error(item.message || 'Codex ha donat un error');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const t = toolOf(item);
|
|
77
|
+
if (!t) break;
|
|
78
|
+
if (!started.has(item.id)) ctx.tool({ id: item.id, ...t });
|
|
79
|
+
const out =
|
|
80
|
+
item.aggregated_output ??
|
|
81
|
+
(item.status ? `estat: ${item.status}` : '') ??
|
|
82
|
+
'';
|
|
83
|
+
ctx.toolResult({
|
|
84
|
+
id: item.id,
|
|
85
|
+
output: String(out),
|
|
86
|
+
error: item.status === 'failed' || (item.exit_code != null && item.exit_code !== 0),
|
|
87
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'turn.completed':
|
|
91
|
+
ctx.result({ tokens: ev.usage });
|
|
92
|
+
break;
|
|
93
|
+
case 'turn.failed':
|
|
94
|
+
ctx.error(ev.error?.message || 'Codex no ha pogut acabar la tasca');
|
|
95
|
+
break;
|
|
96
|
+
case 'error':
|
|
97
|
+
ctx.error(ev.message || 'Codex ha donat un error');
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Agent de prova: no necessita cap compte. Serveix per veure com funciona nio
|
|
3
|
+
// (missatges, eines, aprovacions) abans d'instal·lar res.
|
|
4
|
+
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
id: 'demo',
|
|
9
|
+
name: 'Demo',
|
|
10
|
+
bin: null,
|
|
11
|
+
login: 'No cal instal·lar res',
|
|
12
|
+
approvals: true,
|
|
13
|
+
|
|
14
|
+
async detect() {
|
|
15
|
+
return { available: true, version: 'simulat' };
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
start(ctx) {
|
|
19
|
+
let stopped = false;
|
|
20
|
+
(async () => {
|
|
21
|
+
const words = `Entesos. Faig una ullada al projecte per a «${ctx.prompt.slice(0, 80)}».`.split(' ');
|
|
22
|
+
for (const w of words) {
|
|
23
|
+
if (stopped) return;
|
|
24
|
+
ctx.stream(w + ' ');
|
|
25
|
+
await sleep(60);
|
|
26
|
+
}
|
|
27
|
+
ctx.tool({ id: 't1', name: 'Read', input: { file_path: 'README.md' } });
|
|
28
|
+
await sleep(500);
|
|
29
|
+
ctx.toolResult({ id: 't1', output: '# Projecte de prova\nAixò és un README d’exemple.' });
|
|
30
|
+
if (stopped) return;
|
|
31
|
+
|
|
32
|
+
const input = { command: 'npm test' };
|
|
33
|
+
if (ctx.session.mode === 'llegir') {
|
|
34
|
+
ctx.text('Treballo en mode només lectura, així que no executo res. El següent pas seria `npm test`.');
|
|
35
|
+
} else {
|
|
36
|
+
let ok = true;
|
|
37
|
+
if (ctx.session.mode !== 'tot') {
|
|
38
|
+
ok = (await ctx.ask({ tool: 'Bash', input })) === 'allow';
|
|
39
|
+
}
|
|
40
|
+
if (stopped) return;
|
|
41
|
+
ctx.tool({ id: 't2', name: 'Bash', input });
|
|
42
|
+
await sleep(400);
|
|
43
|
+
if (ok) {
|
|
44
|
+
ctx.toolResult({ id: 't2', output: '✓ 12 tests passen' });
|
|
45
|
+
ctx.text('Fet. Tots els tests passen.');
|
|
46
|
+
} else {
|
|
47
|
+
ctx.toolResult({ id: 't2', output: 'Rebutjat des de nio', error: true });
|
|
48
|
+
ctx.text('D’acord, no l’executo. Què vols que faci?');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
ctx.result({ cost: 0, ms: 2400, turns: 1 });
|
|
52
|
+
})()
|
|
53
|
+
.then(() => ctx.done(stopped ? 130 : 0))
|
|
54
|
+
.catch((e) => {
|
|
55
|
+
ctx.error(e.message);
|
|
56
|
+
ctx.done(1);
|
|
57
|
+
});
|
|
58
|
+
return { kill: () => (stopped = true) };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Lector tolerant per a CLI amb formats JSON poc documentats (Grok Build, Antigravity).
|
|
3
|
+
// Si una línia no és JSON, es mostra com a text.
|
|
4
|
+
const { tryJson } = require('../proc');
|
|
5
|
+
|
|
6
|
+
function pickText(ev) {
|
|
7
|
+
if (typeof ev.delta === 'string') return { text: ev.delta, stream: true };
|
|
8
|
+
if (typeof ev.delta?.text === 'string') return { text: ev.delta.text, stream: true };
|
|
9
|
+
if (typeof ev.chunk === 'string') return { text: ev.chunk, stream: true };
|
|
10
|
+
if (typeof ev.text === 'string') return { text: ev.text, stream: /delta|chunk|update|partial/i.test(ev.type || '') };
|
|
11
|
+
if (typeof ev.content === 'string') return { text: ev.content, stream: /delta|chunk|update|partial/i.test(ev.type || '') };
|
|
12
|
+
if (typeof ev.message === 'string' && !/error/i.test(ev.type || '')) return { text: ev.message, stream: false };
|
|
13
|
+
if (typeof ev.response === 'string') return { text: ev.response, stream: false };
|
|
14
|
+
if (typeof ev.result === 'string') return { text: ev.result, stream: false, final: true };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handleLine(ctx, line, state) {
|
|
19
|
+
const ev = tryJson(line);
|
|
20
|
+
if (!ev) {
|
|
21
|
+
// Text pla (format "plain" o sortida sense JSON).
|
|
22
|
+
ctx.stream(line + '\n');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const type = String(ev.type || ev.event || ev.kind || '');
|
|
26
|
+
const sid = ev.session_id || ev.sessionId || ev.session?.id;
|
|
27
|
+
if (sid) ctx.setNative(sid);
|
|
28
|
+
|
|
29
|
+
if (/error/i.test(type) || ev.status === 'ERROR') {
|
|
30
|
+
ctx.error(ev.error?.message || ev.error || ev.message || 'Error');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const toolName = ev.tool_name || ev.toolName || ev.tool?.name || ev.name;
|
|
34
|
+
if (/tool/i.test(type) && toolName) {
|
|
35
|
+
const id = ev.id || ev.tool_call_id || ev.tool?.id || `${toolName}-${state.n++}`;
|
|
36
|
+
if (/result|finish|end|complete|done/i.test(type)) {
|
|
37
|
+
ctx.toolResult({ id, output: String(ev.output ?? ev.result ?? ev.content ?? ''), error: !!ev.is_error });
|
|
38
|
+
} else {
|
|
39
|
+
ctx.tool({ id, name: toolName, input: ev.input || ev.args || ev.arguments || ev.tool?.input || {} });
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const t = pickText(ev);
|
|
44
|
+
if (t) {
|
|
45
|
+
if (t.final && state.sawText) {
|
|
46
|
+
// El resultat final repeteix el text ja rebut en trossos.
|
|
47
|
+
} else if (t.stream) {
|
|
48
|
+
state.sawText = true;
|
|
49
|
+
ctx.stream(t.text);
|
|
50
|
+
} else {
|
|
51
|
+
state.sawText = true;
|
|
52
|
+
ctx.text(t.text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (/result|done|complete|finish/i.test(type)) {
|
|
56
|
+
ctx.result({ cost: ev.total_cost_usd ?? ev.cost, tokens: ev.usage });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { handleLine };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Grok Build (CLI oficial d'xAI) en mode headless: `grok -p … --output-format streaming-json`.
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { run } = require('../proc');
|
|
5
|
+
const { handleLine } = require('./generic');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
id: 'grok',
|
|
9
|
+
name: 'Grok',
|
|
10
|
+
bin: 'grok',
|
|
11
|
+
beta: true,
|
|
12
|
+
login: 'grok login (amb el compte d\'xAI)',
|
|
13
|
+
approvals: false,
|
|
14
|
+
|
|
15
|
+
start(ctx) {
|
|
16
|
+
const { session, prompt, cfg } = ctx;
|
|
17
|
+
// Sessió amb nom propi perquè els missatges següents continuïn la conversa.
|
|
18
|
+
if (!session.nativeId) ctx.setNative(`nio-${crypto.randomBytes(6).toString('hex')}`);
|
|
19
|
+
const args = [
|
|
20
|
+
'--no-auto-update',
|
|
21
|
+
'-p', prompt,
|
|
22
|
+
'--output-format', 'streaming-json',
|
|
23
|
+
'--cwd', session.cwd,
|
|
24
|
+
'-s', session.nativeId,
|
|
25
|
+
];
|
|
26
|
+
if (session.mode === 'tot') args.push('--always-approve');
|
|
27
|
+
if (session.model) args.push('-m', session.model);
|
|
28
|
+
for (const a of (cfg.agents?.grok?.extraArgs || [])) args.push(a);
|
|
29
|
+
|
|
30
|
+
const state = { n: 0 };
|
|
31
|
+
return run({
|
|
32
|
+
bin: ctx.binPath,
|
|
33
|
+
args,
|
|
34
|
+
cwd: session.cwd,
|
|
35
|
+
onLine: (line) => handleLine(ctx, line, state),
|
|
36
|
+
onErr: (line) => ctx.log(line),
|
|
37
|
+
onExit: (code, err) => ctx.done(code, err),
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|