refacil-sdd-ai 3.0.3 → 4.0.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 +402 -392
- package/bin/cli.js +81 -1217
- package/lib/commands/bus.js +499 -0
- package/lib/commands/compact.js +92 -0
- package/lib/hooks.js +107 -0
- package/lib/ignore-files.js +88 -0
- package/lib/installer.js +265 -0
- package/package.json +6 -2
- package/refacil-bus-diagrams.md +314 -0
- package/skills/archive/SKILL.md +191 -167
- package/skills/guide/SKILL.md +4 -2
- package/skills/setup/SKILL.md +123 -91
- package/skills/update/SKILL.md +81 -0
- package/templates/methodology-guide.md +52 -45
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const busBroker = require('../bus/broker');
|
|
6
|
+
const busSpawn = require('../bus/spawn');
|
|
7
|
+
const busClient = require('../bus/client');
|
|
8
|
+
const busWatch = require('../bus/watch');
|
|
9
|
+
const busPresenter = require('../bus/presenter');
|
|
10
|
+
|
|
11
|
+
function parseBusArgs(argv) {
|
|
12
|
+
const args = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const token = argv[i];
|
|
15
|
+
if (!token || !token.startsWith('--')) continue;
|
|
16
|
+
const key = token.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (next === undefined || next.startsWith('--')) {
|
|
19
|
+
args[key] = true;
|
|
20
|
+
} else {
|
|
21
|
+
args[key] = next;
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function defaultSessionName() {
|
|
29
|
+
return path.basename(process.cwd()) || 'sesion';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function connectOrDie(packageRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const { info } = await busSpawn.ensureBroker(packageRoot);
|
|
35
|
+
const ws = await busClient.connect(info.port);
|
|
36
|
+
return { ws, info };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(` No se pudo conectar al bus: ${err.message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatMessage(m) {
|
|
44
|
+
const target = m.to ? ` → @${m.to}` : '';
|
|
45
|
+
return ` [${m.ts}] ${m.from}${target} (${m.kind}): ${m.text}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function busStart(packageRoot) {
|
|
49
|
+
try {
|
|
50
|
+
const { info, started } = await busSpawn.ensureBroker(packageRoot);
|
|
51
|
+
if (started) {
|
|
52
|
+
console.log(` refacil-bus broker iniciado en 127.0.0.1:${info.port} (pid ${info.pid}).`);
|
|
53
|
+
} else {
|
|
54
|
+
console.log(` refacil-bus broker ya estaba activo en 127.0.0.1:${info.port} (pid ${info.pid}).`);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(` No se pudo iniciar el broker: ${err.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function busStop() {
|
|
63
|
+
const result = busSpawn.stopBroker();
|
|
64
|
+
if (result.stopped) {
|
|
65
|
+
console.log(` refacil-bus broker detenido (pid ${result.info.pid}).`);
|
|
66
|
+
} else if (result.reason === 'no-info') {
|
|
67
|
+
console.log(' refacil-bus broker no está corriendo.');
|
|
68
|
+
} else if (result.reason === 'not-alive') {
|
|
69
|
+
console.log(' refacil-bus broker no estaba vivo; info obsoleta limpiada.');
|
|
70
|
+
} else {
|
|
71
|
+
console.error(` No se pudo detener el broker: ${result.reason}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function busStatus() {
|
|
77
|
+
const status = await busSpawn.isBrokerAlive();
|
|
78
|
+
if (!status.alive) {
|
|
79
|
+
console.log(' refacil-bus broker: INACTIVO');
|
|
80
|
+
if (status.staleInfo) {
|
|
81
|
+
console.log(` (info obsoleta encontrada: pid ${status.staleInfo.pid}, puerto ${status.staleInfo.port})`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const info = status.info;
|
|
86
|
+
const uptimeMs = Date.now() - new Date(info.startedAt).getTime();
|
|
87
|
+
const uptimeMin = Math.floor(uptimeMs / 60000);
|
|
88
|
+
console.log(' refacil-bus broker: ACTIVO');
|
|
89
|
+
console.log(` host: 127.0.0.1`);
|
|
90
|
+
console.log(` puerto: ${info.port}`);
|
|
91
|
+
console.log(` pid: ${info.pid}`);
|
|
92
|
+
console.log(` iniciado: ${info.startedAt}`);
|
|
93
|
+
console.log(` uptime: ${uptimeMin} min`);
|
|
94
|
+
console.log(` info: ${busBroker.BUS_INFO_PATH}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function busServe() {
|
|
98
|
+
busBroker.start().catch((err) => {
|
|
99
|
+
process.stderr.write(`Error arrancando broker: ${err.message}\n`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function busJoin(args, packageRoot) {
|
|
105
|
+
const session = args.session || defaultSessionName();
|
|
106
|
+
const room = args.room;
|
|
107
|
+
const repo = args.repo || process.cwd();
|
|
108
|
+
let intro = args.intro;
|
|
109
|
+
if (!intro) {
|
|
110
|
+
try {
|
|
111
|
+
intro = busPresenter.buildIntro({ repoDir: repo, session });
|
|
112
|
+
} catch (_) {
|
|
113
|
+
intro = `${session} se unió a la sala`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!room) {
|
|
117
|
+
console.error(' Uso: refacil-sdd-ai bus join --room <sala> [--session <s>] [--intro "..."]');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
121
|
+
const reply = await busClient.sendAndWait(
|
|
122
|
+
ws,
|
|
123
|
+
'join',
|
|
124
|
+
{ session, room, repo, intro },
|
|
125
|
+
(d) => d.type === 'system' && d.event === 'joined',
|
|
126
|
+
3000,
|
|
127
|
+
);
|
|
128
|
+
busClient.close(ws);
|
|
129
|
+
if (!reply) {
|
|
130
|
+
console.error(' Timeout uniéndose a la sala.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const members = (reply.detail && reply.detail.members) || [];
|
|
134
|
+
console.log(` Unido a la sala "${room}" como "${session}".`);
|
|
135
|
+
console.log(` Miembros actuales: ${members.join(', ') || '(solo tú)'}`);
|
|
136
|
+
console.log(` Para consultarte: /refacil:ask @${session} "..."`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function busLeave(args, packageRoot) {
|
|
140
|
+
const session = args.session || defaultSessionName();
|
|
141
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
142
|
+
const reply = await busClient.sendAndWait(
|
|
143
|
+
ws,
|
|
144
|
+
'leave',
|
|
145
|
+
{ session },
|
|
146
|
+
(d) => d.type === 'system' && (d.event === 'left' || d.event === 'error'),
|
|
147
|
+
3000,
|
|
148
|
+
);
|
|
149
|
+
busClient.close(ws);
|
|
150
|
+
if (reply && reply.event === 'left') {
|
|
151
|
+
console.log(` "${session}" salió de la sala.`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(` "${session}" no estaba en ninguna sala.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function busSay(args, packageRoot) {
|
|
158
|
+
const session = args.session || defaultSessionName();
|
|
159
|
+
const text = args.text;
|
|
160
|
+
if (!text) {
|
|
161
|
+
console.error(' Uso: refacil-sdd-ai bus say --text "..." [--session <s>]');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
165
|
+
const reply = await busClient.sendAndWait(
|
|
166
|
+
ws,
|
|
167
|
+
'say',
|
|
168
|
+
{ session, text },
|
|
169
|
+
(d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
|
|
170
|
+
3000,
|
|
171
|
+
);
|
|
172
|
+
busClient.close(ws);
|
|
173
|
+
if (reply && reply.event === 'sent') {
|
|
174
|
+
console.log(` Mensaje enviado (id ${reply.detail.id}).`);
|
|
175
|
+
} else {
|
|
176
|
+
const detail = (reply && reply.detail) || 'sin respuesta';
|
|
177
|
+
console.error(` No se pudo enviar: ${detail}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function busAsk(args, packageRoot) {
|
|
183
|
+
const session = args.session || defaultSessionName();
|
|
184
|
+
const to = args.to;
|
|
185
|
+
const text = args.text;
|
|
186
|
+
const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
|
|
187
|
+
if (!to || !text) {
|
|
188
|
+
console.error(' Uso: refacil-sdd-ai bus ask --to <name> --text "..." [--wait N] [--session <s>]');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
192
|
+
const ack = await busClient.sendAndWait(
|
|
193
|
+
ws,
|
|
194
|
+
'ask',
|
|
195
|
+
{ session, to: to.replace(/^@/, ''), text },
|
|
196
|
+
(d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
|
|
197
|
+
3000,
|
|
198
|
+
);
|
|
199
|
+
if (!ack || ack.event !== 'sent') {
|
|
200
|
+
busClient.close(ws);
|
|
201
|
+
const detail = (ack && ack.detail) || 'sin respuesta';
|
|
202
|
+
console.error(` No se pudo enviar la pregunta: ${detail}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
const correlationId = ack.detail.correlationId;
|
|
206
|
+
console.log(` Pregunta enviada a @${to.replace(/^@/, '')} (correlationId ${correlationId}).`);
|
|
207
|
+
|
|
208
|
+
if (waitSec > 0) {
|
|
209
|
+
console.log(` Esperando respuesta hasta ${waitSec}s...`);
|
|
210
|
+
const resp = await busClient.sendAndWait(
|
|
211
|
+
ws,
|
|
212
|
+
'ping',
|
|
213
|
+
{},
|
|
214
|
+
(d) => d.type === 'msg' && d.kind === 'reply' && d.correlationId === correlationId,
|
|
215
|
+
waitSec * 1000,
|
|
216
|
+
);
|
|
217
|
+
busClient.close(ws);
|
|
218
|
+
if (!resp) {
|
|
219
|
+
console.log(` Sin respuesta en ${waitSec}s. Usa /refacil:inbox más tarde para recuperarla.`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
console.log(` Respuesta de @${resp.from}:`);
|
|
223
|
+
console.log(` ${resp.text}`);
|
|
224
|
+
} else {
|
|
225
|
+
busClient.close(ws);
|
|
226
|
+
console.log(' Usa /refacil:inbox para ver respuestas.');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function busReply(args, packageRoot) {
|
|
231
|
+
const session = args.session || defaultSessionName();
|
|
232
|
+
const text = args.text;
|
|
233
|
+
const correlationId = args.correlation || null;
|
|
234
|
+
const to = args.to ? args.to.replace(/^@/, '') : null;
|
|
235
|
+
if (!text) {
|
|
236
|
+
console.error(' Uso: refacil-sdd-ai bus reply --text "..." [--to <name>] [--correlation <id>]');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
240
|
+
const reply = await busClient.sendAndWait(
|
|
241
|
+
ws,
|
|
242
|
+
'reply',
|
|
243
|
+
{ session, text, to, correlationId },
|
|
244
|
+
(d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
|
|
245
|
+
3000,
|
|
246
|
+
);
|
|
247
|
+
busClient.close(ws);
|
|
248
|
+
if (reply && reply.event === 'sent') {
|
|
249
|
+
console.log(` Respuesta enviada (id ${reply.detail.id}).`);
|
|
250
|
+
} else {
|
|
251
|
+
const detail = (reply && reply.detail) || 'sin respuesta';
|
|
252
|
+
console.error(` No se pudo responder: ${detail}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function busHistory(args, packageRoot) {
|
|
258
|
+
const session = args.session || defaultSessionName();
|
|
259
|
+
const n = args.n ? parseInt(args.n, 10) : 20;
|
|
260
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
261
|
+
const reply = await busClient.sendAndWait(
|
|
262
|
+
ws,
|
|
263
|
+
'history',
|
|
264
|
+
{ session, n },
|
|
265
|
+
(d) => d.type === 'history',
|
|
266
|
+
3000,
|
|
267
|
+
);
|
|
268
|
+
busClient.close(ws);
|
|
269
|
+
if (!reply) {
|
|
270
|
+
console.log(' Sin historial.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const msgs = reply.messages || [];
|
|
274
|
+
if (msgs.length === 0) {
|
|
275
|
+
console.log(' Sin historial.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(` Últimos ${msgs.length} mensajes:`);
|
|
279
|
+
for (const m of msgs) console.log(formatMessage(m));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function busInbox(args, packageRoot) {
|
|
283
|
+
const session = args.session || defaultSessionName();
|
|
284
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
285
|
+
const reply = await busClient.sendAndWait(
|
|
286
|
+
ws,
|
|
287
|
+
'inbox',
|
|
288
|
+
{ session },
|
|
289
|
+
(d) => d.type === 'inbox',
|
|
290
|
+
3000,
|
|
291
|
+
);
|
|
292
|
+
busClient.close(ws);
|
|
293
|
+
if (!reply) {
|
|
294
|
+
console.log(' Sin respuesta del broker.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const msgs = reply.messages || [];
|
|
298
|
+
if (msgs.length === 0) {
|
|
299
|
+
console.log(' Sin mensajes nuevos.');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
console.log(` ${msgs.length} mensaje(s) nuevo(s):`);
|
|
303
|
+
for (const m of msgs) console.log(formatMessage(m));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function findFirstUnansweredAsk(messages, session) {
|
|
307
|
+
const asks = messages.filter((m) => m.kind === 'ask' && m.to === session);
|
|
308
|
+
for (const ask of asks) {
|
|
309
|
+
const hasReply = messages.some(
|
|
310
|
+
(m) => m.kind === 'reply' && m.correlationId === ask.correlationId,
|
|
311
|
+
);
|
|
312
|
+
if (!hasReply) return ask;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function printAttendQuestion(msg) {
|
|
318
|
+
console.log(' Pregunta recibida del bus:');
|
|
319
|
+
console.log(` de: @${msg.from}`);
|
|
320
|
+
console.log(` correlationId: ${msg.correlationId || '(sin id)'}`);
|
|
321
|
+
console.log(` texto: ${msg.text}`);
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(' Responde con: /refacil:reply "<respuesta>"');
|
|
324
|
+
console.log(' Luego vuelve a ejecutar /refacil:attend para seguir escuchando.');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function busAttend(args, packageRoot) {
|
|
328
|
+
const session = args.session || defaultSessionName();
|
|
329
|
+
const timeoutSec = args.timeout ? parseInt(args.timeout, 10) : 540;
|
|
330
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
331
|
+
|
|
332
|
+
const hist = await busClient.sendAndWait(
|
|
333
|
+
ws,
|
|
334
|
+
'history',
|
|
335
|
+
{ session, n: 50 },
|
|
336
|
+
(d) => d.type === 'history',
|
|
337
|
+
3000,
|
|
338
|
+
);
|
|
339
|
+
if (hist) {
|
|
340
|
+
const pending = findFirstUnansweredAsk(hist.messages || [], session);
|
|
341
|
+
if (pending) {
|
|
342
|
+
busClient.close(ws);
|
|
343
|
+
printAttendQuestion(pending);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await new Promise((resolve) => {
|
|
349
|
+
let done = false;
|
|
350
|
+
const finish = (v) => {
|
|
351
|
+
if (done) return;
|
|
352
|
+
done = true;
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
ws.removeListener('message', onMessage);
|
|
355
|
+
resolve(v);
|
|
356
|
+
};
|
|
357
|
+
const onMessage = (raw) => {
|
|
358
|
+
let data;
|
|
359
|
+
try { data = JSON.parse(raw.toString()); } catch (_) { return; }
|
|
360
|
+
if (data.type === 'msg' && data.kind === 'ask' && data.to === session) {
|
|
361
|
+
finish({ kind: 'message', msg: data });
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
ws.on('message', onMessage);
|
|
365
|
+
const timer = setTimeout(() => finish({ kind: 'timeout' }), timeoutSec * 1000);
|
|
366
|
+
busClient.send(ws, 'attend', { session });
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
busClient.close(ws);
|
|
370
|
+
|
|
371
|
+
if (result.kind === 'message') {
|
|
372
|
+
printAttendQuestion(result.msg);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(` Sin preguntas en ${timeoutSec}s. Re-ejecuta /refacil:attend para seguir escuchando.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function readPersistedSessions() {
|
|
379
|
+
try {
|
|
380
|
+
return JSON.parse(fs.readFileSync(busBroker.SESSIONS_PATH, 'utf8'));
|
|
381
|
+
} catch (_) {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function busWatchCmd(positional, args, packageRoot) {
|
|
387
|
+
const session = args.session || positional || null;
|
|
388
|
+
let room = args.room || null;
|
|
389
|
+
if (session && !room) {
|
|
390
|
+
const persisted = readPersistedSessions();
|
|
391
|
+
if (persisted[session] && persisted[session].room) {
|
|
392
|
+
room = persisted[session].room;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (!session && !room) {
|
|
396
|
+
console.error(' Uso: refacil-sdd-ai bus watch <session> [--room <sala>]');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const { info } = await busSpawn.ensureBroker(packageRoot);
|
|
401
|
+
await busWatch.start({ session, room, port: info.port });
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error(` No se pudo iniciar el watch: ${err.message}`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function busRooms(packageRoot) {
|
|
409
|
+
const { ws } = await connectOrDie(packageRoot);
|
|
410
|
+
const reply = await busClient.sendAndWait(
|
|
411
|
+
ws,
|
|
412
|
+
'status',
|
|
413
|
+
{},
|
|
414
|
+
(d) => d.type === 'system' && d.event === 'status',
|
|
415
|
+
3000,
|
|
416
|
+
);
|
|
417
|
+
busClient.close(ws);
|
|
418
|
+
if (!reply) {
|
|
419
|
+
console.log(' Sin respuesta del broker.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const rooms = (reply.detail && reply.detail.rooms) || {};
|
|
423
|
+
const names = Object.keys(rooms);
|
|
424
|
+
if (names.length === 0) {
|
|
425
|
+
console.log(' No hay salas activas.');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log(' Salas activas:');
|
|
429
|
+
for (const name of names) {
|
|
430
|
+
const members = rooms[name] || [];
|
|
431
|
+
console.log(` ${name} (${members.length}): ${members.join(', ')}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function openInBrowser(url) {
|
|
436
|
+
const { spawn } = require('child_process');
|
|
437
|
+
const platform = process.platform;
|
|
438
|
+
let cmd;
|
|
439
|
+
let cmdArgs;
|
|
440
|
+
if (platform === 'win32') {
|
|
441
|
+
cmd = 'cmd';
|
|
442
|
+
cmdArgs = ['/c', 'start', '""', url];
|
|
443
|
+
} else if (platform === 'darwin') {
|
|
444
|
+
cmd = 'open';
|
|
445
|
+
cmdArgs = [url];
|
|
446
|
+
} else {
|
|
447
|
+
cmd = 'xdg-open';
|
|
448
|
+
cmdArgs = [url];
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
452
|
+
return true;
|
|
453
|
+
} catch (_) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function busView(packageRoot) {
|
|
459
|
+
try {
|
|
460
|
+
const { info } = await busSpawn.ensureBroker(packageRoot);
|
|
461
|
+
const url = `http://127.0.0.1:${info.port}/`;
|
|
462
|
+
console.log(` refacil-bus view disponible en: ${url}`);
|
|
463
|
+
const opened = openInBrowser(url);
|
|
464
|
+
if (!opened) {
|
|
465
|
+
console.log(' (no se pudo abrir el navegador automáticamente, abre la URL manualmente)');
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.error(` No se pudo iniciar la vista: ${err.message}`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function handleBus(sub, argv, packageRoot) {
|
|
474
|
+
const rest = argv || [];
|
|
475
|
+
const positional = rest.length > 0 && !rest[0].startsWith('--') ? rest[0] : null;
|
|
476
|
+
const args = parseBusArgs(rest);
|
|
477
|
+
|
|
478
|
+
switch (sub) {
|
|
479
|
+
case 'start': return busStart(packageRoot);
|
|
480
|
+
case 'stop': return busStop();
|
|
481
|
+
case 'status': return busStatus();
|
|
482
|
+
case 'serve': return busServe();
|
|
483
|
+
case 'join': return busJoin(args, packageRoot);
|
|
484
|
+
case 'leave': return busLeave(args, packageRoot);
|
|
485
|
+
case 'say': return busSay(args, packageRoot);
|
|
486
|
+
case 'ask': return busAsk(args, packageRoot);
|
|
487
|
+
case 'reply': return busReply(args, packageRoot);
|
|
488
|
+
case 'history': return busHistory(args, packageRoot);
|
|
489
|
+
case 'inbox': return busInbox(args, packageRoot);
|
|
490
|
+
case 'rooms': return busRooms(packageRoot);
|
|
491
|
+
case 'watch': return busWatchCmd(positional, args, packageRoot);
|
|
492
|
+
case 'attend': return busAttend(args, packageRoot);
|
|
493
|
+
case 'view': return busView(packageRoot);
|
|
494
|
+
default:
|
|
495
|
+
console.log('Uso: refacil-sdd-ai bus <start|stop|status|serve|join|leave|say|ask|reply|history|inbox|rooms|watch|attend|view>');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = { handleBus, parseBusArgs };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const compactTelemetry = require('../compact/telemetry');
|
|
4
|
+
|
|
5
|
+
function showCompactStats() {
|
|
6
|
+
const s = compactTelemetry.stats();
|
|
7
|
+
if (s.totalEvents === 0) {
|
|
8
|
+
console.log('\n No hay eventos registrados todavia. Ejecuta comandos Bash para generar telemetria de compactacion.\n');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const sortedRewrites = Object.entries(s.byRule)
|
|
13
|
+
.filter(([, data]) => data.rewriteCount > 0)
|
|
14
|
+
.sort((a, b) => b[1].rewriteSaved - a[1].rewriteSaved);
|
|
15
|
+
const sortedAlreadyCompact = Object.entries(s.byRule)
|
|
16
|
+
.filter(([, data]) => data.alreadyCompactCount > 0)
|
|
17
|
+
.sort((a, b) => b[1].alreadyCompactPotential - a[1].alreadyCompactPotential);
|
|
18
|
+
|
|
19
|
+
console.log(`\n compact-bash stats\n`);
|
|
20
|
+
console.log(` Rewrites por hook: ${s.totalRewrites}`);
|
|
21
|
+
console.log(` Comandos ya compactos detectados (skill/agente): ${s.totalAlreadyCompact}\n`);
|
|
22
|
+
|
|
23
|
+
if (sortedRewrites.length > 0) {
|
|
24
|
+
console.log(' Ahorro aplicado por hook (rewrite):');
|
|
25
|
+
for (const [id, data] of sortedRewrites) {
|
|
26
|
+
const kTokens = (data.rewriteSaved / 1000).toFixed(1);
|
|
27
|
+
console.log(
|
|
28
|
+
` ${id.padEnd(18)} ${String(data.rewriteCount).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const totalHookK = (s.totalSaved / 1000).toFixed(1);
|
|
32
|
+
const hookUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
|
|
33
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
34
|
+
console.log(
|
|
35
|
+
` ${'Total hook'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalHookK.padStart(7)}k tokens (~$${hookUsd} USD)`,
|
|
36
|
+
);
|
|
37
|
+
console.log('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sortedAlreadyCompact.length > 0) {
|
|
41
|
+
console.log(' Ahorro potencial ya capturado por skill/agente (sin rewrite):');
|
|
42
|
+
for (const [id, data] of sortedAlreadyCompact) {
|
|
43
|
+
const kTokens = (data.alreadyCompactPotential / 1000).toFixed(1);
|
|
44
|
+
console.log(
|
|
45
|
+
` ${id.padEnd(18)} ${String(data.alreadyCompactCount).padStart(6)} eventos ~${kTokens.padStart(7)}k tokens potenciales`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const totalAgentK = (s.totalAlreadyCompactPotential / 1000).toFixed(1);
|
|
49
|
+
const agentUsd = ((s.totalAlreadyCompactPotential / 1_000_000) * 3).toFixed(2);
|
|
50
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
51
|
+
console.log(
|
|
52
|
+
` ${'Total skill'.padEnd(18)} ${String(s.totalAlreadyCompact).padStart(6)} eventos ~${totalAgentK.padStart(7)}k tokens (~$${agentUsd} USD)`,
|
|
53
|
+
);
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const totalK = (s.totalObservedPotential / 1000).toFixed(1);
|
|
58
|
+
const totalUsd = ((s.totalObservedPotential / 1_000_000) * 3).toFixed(2);
|
|
59
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
60
|
+
console.log(
|
|
61
|
+
` ${'Total observado'.padEnd(18)} ${String(s.totalEvents).padStart(6)} eventos ~${totalK.padStart(7)}k tokens (~$${totalUsd} USD, Sonnet input)`,
|
|
62
|
+
);
|
|
63
|
+
console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
|
|
64
|
+
if (compactTelemetry.isDisabled()) {
|
|
65
|
+
console.log(' Estado: DESHABILITADO (no se registran nuevos eventos)');
|
|
66
|
+
}
|
|
67
|
+
console.log('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleCompact(sub) {
|
|
71
|
+
switch (sub) {
|
|
72
|
+
case 'stats':
|
|
73
|
+
showCompactStats();
|
|
74
|
+
break;
|
|
75
|
+
case 'disable':
|
|
76
|
+
compactTelemetry.disable();
|
|
77
|
+
console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
|
|
78
|
+
break;
|
|
79
|
+
case 'enable':
|
|
80
|
+
compactTelemetry.enable();
|
|
81
|
+
console.log(' compact-bash habilitado.');
|
|
82
|
+
break;
|
|
83
|
+
case 'clear-log':
|
|
84
|
+
compactTelemetry.clearLog();
|
|
85
|
+
console.log(' compact.log limpiado.');
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
console.log('Uso: refacil-sdd-ai compact <stats|disable|enable|clear-log>');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { handleCompact };
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function installHooks(ideDir, projectRoot) {
|
|
7
|
+
const settingsDir = path.join(projectRoot, ideDir);
|
|
8
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
9
|
+
let settings = {};
|
|
10
|
+
|
|
11
|
+
if (fs.existsSync(settingsPath)) {
|
|
12
|
+
try {
|
|
13
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
14
|
+
} catch (_) {
|
|
15
|
+
settings = {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!settings.hooks) settings.hooks = {};
|
|
20
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
21
|
+
|
|
22
|
+
let changed = false;
|
|
23
|
+
|
|
24
|
+
const hasUpdateHook = settings.hooks.SessionStart.some((h) => h._sdd === true);
|
|
25
|
+
if (!hasUpdateHook) {
|
|
26
|
+
settings.hooks.SessionStart.push({
|
|
27
|
+
_sdd: true,
|
|
28
|
+
matcher: '',
|
|
29
|
+
hooks: [{ type: 'command', command: 'refacil-sdd-ai check-update' }],
|
|
30
|
+
});
|
|
31
|
+
changed = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
35
|
+
|
|
36
|
+
// compact-bash debe correr ANTES de check-review para que el rewrite sea visible
|
|
37
|
+
const hasCompactHook = settings.hooks.PreToolUse.some((h) => h._sdd_compact === true);
|
|
38
|
+
if (!hasCompactHook) {
|
|
39
|
+
settings.hooks.PreToolUse.unshift({
|
|
40
|
+
_sdd_compact: true,
|
|
41
|
+
matcher: 'Bash',
|
|
42
|
+
hooks: [{ type: 'command', command: 'refacil-sdd-ai compact-bash' }],
|
|
43
|
+
});
|
|
44
|
+
changed = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasReviewHook = settings.hooks.PreToolUse.some((h) => h._sdd_review === true);
|
|
48
|
+
if (!hasReviewHook) {
|
|
49
|
+
settings.hooks.PreToolUse.push({
|
|
50
|
+
_sdd_review: true,
|
|
51
|
+
matcher: 'Bash',
|
|
52
|
+
hooks: [{ type: 'command', command: 'refacil-sdd-ai check-review' }],
|
|
53
|
+
});
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!changed) {
|
|
58
|
+
console.log(` Hooks SDD-AI ya configurados en ${ideDir}/settings.json.`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function uninstallHooks(ideDir, projectRoot) {
|
|
68
|
+
const settingsPath = path.join(projectRoot, ideDir, 'settings.json');
|
|
69
|
+
if (!fs.existsSync(settingsPath)) return false;
|
|
70
|
+
|
|
71
|
+
let settings;
|
|
72
|
+
try {
|
|
73
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
74
|
+
} catch (_) {
|
|
75
|
+
console.log(` No se pudieron remover hooks: ${ideDir}/settings.json invalido.`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!settings.hooks) return false;
|
|
80
|
+
|
|
81
|
+
let changed = false;
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(settings.hooks.SessionStart)) {
|
|
84
|
+
const original = settings.hooks.SessionStart.length;
|
|
85
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((h) => h._sdd !== true);
|
|
86
|
+
if (settings.hooks.SessionStart.length !== original) changed = true;
|
|
87
|
+
if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(settings.hooks.PreToolUse)) {
|
|
91
|
+
const original = settings.hooks.PreToolUse.length;
|
|
92
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
93
|
+
(h) => h._sdd_review !== true && h._sdd_compact !== true,
|
|
94
|
+
);
|
|
95
|
+
if (settings.hooks.PreToolUse.length !== original) changed = true;
|
|
96
|
+
if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
100
|
+
|
|
101
|
+
if (!changed) return false;
|
|
102
|
+
|
|
103
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { installHooks, uninstallHooks };
|