@dotrino/ia-agent 0.1.1 → 0.2.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/drivers/claude.js +94 -0
- package/index.js +22 -8
- package/package.json +3 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drivers/claude.js — invoca la CLI `claude` (Claude Code) en modo headless.
|
|
3
|
+
*
|
|
4
|
+
* F1: NO streaming. Lanza `claude -p "<texto>" --output-format json [--resume <sid>]`
|
|
5
|
+
* y devuelve el resultado completo cuando termina (igual que dotrino-telegram-claude-bot).
|
|
6
|
+
* F2 migrará esto al SDK (@anthropic-ai/claude-code) con onToken para streaming real.
|
|
7
|
+
*
|
|
8
|
+
* Memoria de sesión: guarda el `session_id` que devuelve Claude y lo pasa como
|
|
9
|
+
* `--resume` en la siguiente vuelta (hilo continuo). Si la sesión se invalida,
|
|
10
|
+
* arranca una nueva.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
import { dirname } from 'node:path'
|
|
14
|
+
|
|
15
|
+
// PATH robusto para systemd/PM2 (PATH mínimo): node + ~/.local/bin (donde vive `claude`).
|
|
16
|
+
const EXTRA_PATH = [dirname(process.execPath), `${process.env.HOME || ''}/.local/bin`].filter(Boolean).join(':')
|
|
17
|
+
|
|
18
|
+
export class ClaudeDriver {
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} [opts.cwd] directorio donde opera Claude (el proyecto). Default process.cwd().
|
|
22
|
+
* @param {string} [opts.bin='claude'] binario a invocar.
|
|
23
|
+
* @param {string[]} [opts.flags=[]] flags extra (p. ej. ['--dangerously-skip-permissions'] para yolo).
|
|
24
|
+
* @param {number} [opts.timeoutMs=0] 0 = sin límite.
|
|
25
|
+
*/
|
|
26
|
+
constructor ({ cwd, bin = 'claude', flags = [], timeoutMs = 0 } = {}) {
|
|
27
|
+
this.cwd = cwd || process.cwd()
|
|
28
|
+
this.bin = bin
|
|
29
|
+
this.flags = flags
|
|
30
|
+
this.timeoutMs = timeoutMs
|
|
31
|
+
this.sessionId = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Envía un mensaje y devuelve { text, sessionId, tokens } cuando termina. */
|
|
35
|
+
async send (text) {
|
|
36
|
+
let out
|
|
37
|
+
try {
|
|
38
|
+
out = await this._run(this._args(text, this.sessionId))
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Sesión inválida/inexistente → empezar una nueva en vez de fallar.
|
|
41
|
+
if (this.sessionId && /no conversation found|session/i.test(e.message || '')) {
|
|
42
|
+
this.sessionId = null
|
|
43
|
+
out = await this._run(this._args(text, null))
|
|
44
|
+
} else throw e
|
|
45
|
+
}
|
|
46
|
+
let result = out, sid = this.sessionId, tokens = 0
|
|
47
|
+
try {
|
|
48
|
+
const j = JSON.parse(out)
|
|
49
|
+
result = (j.result ?? out)
|
|
50
|
+
sid = j.session_id || sid
|
|
51
|
+
tokens = contextTokens(j)
|
|
52
|
+
} catch {}
|
|
53
|
+
if (sid) this.sessionId = sid
|
|
54
|
+
return { text: String(result || '(sin respuesta)'), sessionId: sid, tokens }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_args (text, sid) {
|
|
58
|
+
const args = ['-p', text, '--output-format', 'json', ...this.flags]
|
|
59
|
+
if (sid) args.push('--resume', sid)
|
|
60
|
+
return args
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_run (args) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
// stdin cerrado para que `claude -p` no espere por él.
|
|
66
|
+
const p = spawn(this.bin, args, {
|
|
67
|
+
cwd: this.cwd,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
env: { ...process.env, PATH: `${process.env.PATH || ''}:${EXTRA_PATH}` }
|
|
70
|
+
})
|
|
71
|
+
let out = '', err = ''
|
|
72
|
+
p.stdout.on('data', (d) => { out += d })
|
|
73
|
+
p.stderr.on('data', (d) => { err += d })
|
|
74
|
+
const to = this.timeoutMs > 0
|
|
75
|
+
? setTimeout(() => { try { p.kill('SIGKILL') } catch {}; reject(new Error('timeout')) }, this.timeoutMs)
|
|
76
|
+
: null
|
|
77
|
+
p.on('error', (e) => { if (to) clearTimeout(to); reject(new Error(`no se pudo ejecutar '${this.bin}' (¿instalado y en PATH?): ${e.message}`)) })
|
|
78
|
+
p.on('close', (code) => {
|
|
79
|
+
if (to) clearTimeout(to)
|
|
80
|
+
if (code === 0) resolve(out)
|
|
81
|
+
else reject(new Error(err.slice(0, 500) || `claude salió con código ${code}`))
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Tamaño aproximado del contexto de la última vuelta (tokens enviados). */
|
|
88
|
+
function contextTokens (j) {
|
|
89
|
+
const u = j && j.usage
|
|
90
|
+
if (!u) return 0
|
|
91
|
+
return (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default { ClaudeDriver }
|
package/index.js
CHANGED
|
@@ -4,11 +4,22 @@
|
|
|
4
4
|
* Expone tus CLIs de IA (Claude, OpenCode…) al chat remoto, sobre
|
|
5
5
|
* `@dotrino/remote-agent` (handshake E2E, emparejamiento con vault, revocación).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* F1: driver Claude (no-streaming) — `claude -p --output-format json --resume`.
|
|
8
|
+
* F2: migrar a SDK con streaming de tokens. F4: driver OpenCode.
|
|
9
|
+
*
|
|
10
|
+
* Configuración (env):
|
|
11
|
+
* IA_CWD directorio donde opera Claude (default: el cwd al lanzar).
|
|
12
|
+
* CLAUDE_BIN binario (default 'claude').
|
|
13
|
+
* CLAUDE_FLAGS flags extra, ej. '--dangerously-skip-permissions' (modo yolo).
|
|
14
|
+
* CLAUDE_TIMEOUT segundos, 0 = sin límite.
|
|
10
15
|
*/
|
|
11
16
|
import { startRemoteAgent } from '@dotrino/remote-agent/agent'
|
|
17
|
+
import { ClaudeDriver } from './drivers/claude.js'
|
|
18
|
+
|
|
19
|
+
const CWD = process.env.IA_CWD || process.cwd()
|
|
20
|
+
const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude'
|
|
21
|
+
const CLAUDE_FLAGS = (process.env.CLAUDE_FLAGS || '').split(/\s+/).filter(Boolean)
|
|
22
|
+
const CLAUDE_TIMEOUT = Number(process.env.CLAUDE_TIMEOUT || 0) * 1000
|
|
12
23
|
|
|
13
24
|
export async function startIaAgent (opts = {}) {
|
|
14
25
|
return startRemoteAgent({
|
|
@@ -19,12 +30,15 @@ export async function startIaAgent (opts = {}) {
|
|
|
19
30
|
onReady: opts.onReady,
|
|
20
31
|
onRevoked: opts.onRevoked,
|
|
21
32
|
onSession (session) {
|
|
33
|
+
// Un driver por sesión: cada chat abre su propio hilo de Claude con su contexto.
|
|
34
|
+
const driver = new ClaudeDriver({ cwd: CWD, bin: CLAUDE_BIN, flags: CLAUDE_FLAGS, timeoutMs: CLAUDE_TIMEOUT })
|
|
22
35
|
session.on('message', async (msg) => {
|
|
23
|
-
if (msg?.type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
if (msg?.type !== 'msg' || typeof msg.text !== 'string') return
|
|
37
|
+
try {
|
|
38
|
+
const r = await driver.send(msg.text)
|
|
39
|
+
await session.send({ type: 'done', text: r.text, sessionId: r.sessionId, tokens: r.tokens })
|
|
40
|
+
} catch (e) {
|
|
41
|
+
await session.send({ type: 'error', message: e.message })
|
|
28
42
|
}
|
|
29
43
|
})
|
|
30
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/ia-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agente de Dotrino IA: expone tus CLIs de IA (Claude, OpenCode) al chat remoto de ia.dotrino.com, vía @dotrino/remote-agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
|
-
"bin"
|
|
14
|
+
"bin",
|
|
15
|
+
"drivers"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"start": "node bin/cli.js run"
|