@ajuarezso/lienzo-mcp 0.2.0 → 0.3.1
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.en.md +135 -7
- package/README.md +138 -8
- package/index.mjs +65 -16
- package/lib/agentes.mjs +53 -0
- package/lib/canal.mjs +778 -0
- package/lib/config.mjs +15 -0
- package/lib/esperar.mjs +8 -3
- package/lib/firestore.mjs +10 -5
- package/lib/lienzo.mjs +99 -24
- package/package.json +1 -1
package/lib/config.mjs
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// LIENZO_HOME carpeta de estado (por defecto ~/.lienzo)
|
|
4
4
|
// LIENZO_SALA sala a la que se une solo al primer uso
|
|
5
5
|
// LIENZO_NOMBRE nombre visible del agente (por defecto «Agente de <tu primer nombre>», de tu cuenta)
|
|
6
|
+
// LIENZO_AGENTE cuál de tus agentes es este puente (personal, trabajo, pc…): con él, la misma cuenta puede
|
|
7
|
+
// tener varios agentes en una sala, cada uno con su id `{uid}_agente_{slug}`. Sin él, el
|
|
8
|
+
// agente de siempre (`{uid}_agente`)
|
|
6
9
|
// LIENZO_CLIENTE 'claude-code' por defecto
|
|
7
10
|
// LIENZO_FIREBASE_CONFIG ruta a otro firebase.config.json (por defecto: el que viaja en el paquete de npm,
|
|
8
11
|
// y si no está, el de la raíz del repo)
|
|
@@ -14,6 +17,17 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
14
17
|
import { homedir } from 'node:os';
|
|
15
18
|
import { join, dirname } from 'node:path';
|
|
16
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { slugDeAgente, ErrorAgente } from './agentes.mjs';
|
|
21
|
+
|
|
22
|
+
/** El slug de LIENZO_AGENTE, o el error para decirlo en la primera herramienta (el puente arranca igual). */
|
|
23
|
+
function agenteDe(entrada) {
|
|
24
|
+
try {
|
|
25
|
+
return { agente: slugDeAgente(entrada), errorAgente: null };
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (e instanceof ErrorAgente) return { agente: null, errorAgente: e.message };
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
|
|
18
32
|
const AQUI = dirname(fileURLToPath(import.meta.url));
|
|
19
33
|
|
|
@@ -46,6 +60,7 @@ export function cargarConfig(env = process.env) {
|
|
|
46
60
|
home,
|
|
47
61
|
salaId: env.LIENZO_SALA || archivo.salaId || null,
|
|
48
62
|
nombre: env.LIENZO_NOMBRE || archivo.nombre || null,
|
|
63
|
+
...agenteDe(env.LIENZO_AGENTE || archivo.agente || null),
|
|
49
64
|
cliente: env.LIENZO_CLIENTE || archivo.cliente || 'claude-code',
|
|
50
65
|
apiKey: firebase.apiKey,
|
|
51
66
|
projectId,
|
package/lib/esperar.mjs
CHANGED
|
@@ -119,13 +119,18 @@ async function sondear(consultar, o, limite, consultasPrevias) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Respuesta de lienzo_leer / lienzo_esperar que no entregó todo. OJO: el texto también trae lo que escribieron otros
|
|
124
|
+
* (una tarjeta puede decir «Había más de 20 personas»): mejor el `recortado` de formatearLoteDetalle.
|
|
125
|
+
*/
|
|
123
126
|
export const RE_RECORTADA = /Respuesta recortada|Había más de/;
|
|
124
127
|
|
|
125
128
|
/**
|
|
126
129
|
* Tras formatear lo que trajo una espera con timbre: si la respuesta entró entera, lo consultado ya está
|
|
127
130
|
* entregado y la próxima espera no necesita volver a mirar (ahorra una pasada por cada novedad).
|
|
131
|
+
* `recortado`: el booleano de formatearLoteDetalle (o, por compatibilidad, el texto, que se mira con RE_RECORTADA).
|
|
128
132
|
*/
|
|
129
|
-
export function entregado(timbre, r,
|
|
130
|
-
|
|
133
|
+
export function entregado(timbre, r, recortado) {
|
|
134
|
+
const corto = typeof recortado === 'boolean' ? recortado : RE_RECORTADA.test(String(recortado ?? ''));
|
|
135
|
+
if (timbre && r?.modo === 'timbre' && r.version != null && !corto) timbre.visto = r.version;
|
|
131
136
|
}
|
package/lib/firestore.mjs
CHANGED
|
@@ -114,14 +114,19 @@ export class Firestore {
|
|
|
114
114
|
/**
|
|
115
115
|
* Arma una escritura para :commit (para poder usar la hora del servidor).
|
|
116
116
|
* opciones: { crear: true } exige que no exista; { existe: true } exige que exista;
|
|
117
|
-
* mascara: [campos] actualiza solo esos; horaServidor: [campos] = REQUEST_TIME
|
|
117
|
+
* mascara: [campos] actualiza solo esos; horaServidor: [campos] = REQUEST_TIME;
|
|
118
|
+
* agregar / quitar: { campo: [valores] } = appendMissingElements / removeAllFromArray (atómico en el servidor:
|
|
119
|
+
* dos puentes que agregan a la vez no se pisan).
|
|
118
120
|
*/
|
|
119
|
-
escritura(ruta, datos, { crear = false, existe = false, mascara = null, horaServidor = [] } = {}) {
|
|
121
|
+
escritura(ruta, datos, { crear = false, existe = false, mascara = null, horaServidor = [], agregar = null, quitar = null } = {}) {
|
|
120
122
|
const write = { update: { name: this.nombre(ruta), fields: aCampos(datos) } };
|
|
121
123
|
if (mascara) write.updateMask = { fieldPaths: mascara.map(campoRuta) };
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
const transformaciones = [
|
|
125
|
+
...horaServidor.map((c) => ({ fieldPath: campoRuta(c), setToServerValue: 'REQUEST_TIME' })),
|
|
126
|
+
...Object.entries(agregar ?? {}).map(([c, v]) => ({ fieldPath: campoRuta(c), appendMissingElements: { values: v.map(aValor) } })),
|
|
127
|
+
...Object.entries(quitar ?? {}).map(([c, v]) => ({ fieldPath: campoRuta(c), removeAllFromArray: { values: v.map(aValor) } })),
|
|
128
|
+
];
|
|
129
|
+
if (transformaciones.length) write.updateTransforms = transformaciones;
|
|
125
130
|
if (crear) write.currentDocument = { exists: false };
|
|
126
131
|
else if (existe) write.currentDocument = { exists: true };
|
|
127
132
|
return write;
|
package/lib/lienzo.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { envolver, limpiarLinea, neutralizar, abreviarId, aviso, nuevoCodigo } f
|
|
|
9
9
|
import { rutaCompartible, revisarNombre, ErrorArchivo } from './archivos.mjs';
|
|
10
10
|
import { INTERVALO_MS } from './esperar.mjs';
|
|
11
11
|
import { cerrarTimbre } from './escucha.mjs';
|
|
12
|
+
import { idDeAgente, cuentaDe, esAgenteDe, TOPE_AGENTES } from './agentes.mjs';
|
|
12
13
|
|
|
13
14
|
export const MAX_TEXTO = 20000;
|
|
14
15
|
export const MAX_ARCHIVO_BYTES = 200 * 1024;
|
|
@@ -29,10 +30,13 @@ export function nombreDeCuenta(cuenta) {
|
|
|
29
30
|
return local || null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* «Agente de <primer nombre>» (o null si la cuenta no tiene nombre ni correo). Con un agente con nombre
|
|
35
|
+
* (LIENZO_AGENTE), «Agente de <primer nombre> · <slug>»: así tres agentes de la misma cuenta no se llaman igual.
|
|
36
|
+
*/
|
|
37
|
+
export function nombreAgentePorDefecto(cuenta, slug = null) {
|
|
34
38
|
const n = nombreDeCuenta(cuenta);
|
|
35
|
-
return n ? limpiarLinea(`Agente de ${n.split(/\s+/)[0]}`, 80) : null;
|
|
39
|
+
return n ? limpiarLinea(`Agente de ${n.split(/\s+/)[0]}${slug ? ` · ${slug}` : ''}`, 80) : null;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// ── listas de puntos (docs/producto.md §4) ──────────────────────────────────
|
|
@@ -179,6 +183,7 @@ export class Lienzo {
|
|
|
179
183
|
this.intervaloEspera = INTERVALO_MS; // lo que lleva creciendo la espera de lienzo_esperar
|
|
180
184
|
this.participantes = new Map();
|
|
181
185
|
this.participantesEn = 0;
|
|
186
|
+
this._ausentes = new Map(); // id → cuándo se confirmó que no está en la sala (no se recarga todo por él cada vez)
|
|
182
187
|
this.tareasPedidas = new Map(); // id → { estado, respuesta, tomadaPor }
|
|
183
188
|
this.tareasCargadas = false;
|
|
184
189
|
this.listas = new Set(); // ids de las listas que pediste (sus puntos llegan agrupados)
|
|
@@ -195,14 +200,26 @@ export class Lienzo {
|
|
|
195
200
|
/** La cuenta (la persona). */
|
|
196
201
|
get uid() { return this.sesion.uid; }
|
|
197
202
|
|
|
198
|
-
/** El
|
|
199
|
-
get
|
|
203
|
+
/** El slug de este puente (LIENZO_AGENTE), o null: el agente de siempre. */
|
|
204
|
+
get slug() { return this.config.agente ?? null; }
|
|
205
|
+
|
|
206
|
+
/** El participante de este agente en la sala: `{uid}_agente` o, con LIENZO_AGENTE, `{uid}_agente_{slug}`. */
|
|
207
|
+
get participanteId() { return this.uid ? idDeAgente(this.uid, this.slug) : null; }
|
|
200
208
|
|
|
201
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* ¿Esto es para mí? Mi persona o yo, ESTE agente. Más estrecho que esMio() de las reglas (que mira la cuenta
|
|
211
|
+
* entera): una tarea para otro agente de mi misma cuenta («Claude trabajo») no es de «Claude personal», aunque las
|
|
212
|
+
* reglas lo dejarían tocarla.
|
|
213
|
+
*/
|
|
202
214
|
esMio(id) {
|
|
203
215
|
return Boolean(id) && (id === this.uid || id === this.participanteId);
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
/** ¿Es otro agente de mi misma cuenta (no yo)? */
|
|
219
|
+
esOtroAgenteMio(id) {
|
|
220
|
+
return Boolean(id) && id !== this.participanteId && esAgenteDe(id, this.uid);
|
|
221
|
+
}
|
|
222
|
+
|
|
206
223
|
link(salaId = this.salaId) {
|
|
207
224
|
return `${this.config.webUrl}/sala/${salaId}`;
|
|
208
225
|
}
|
|
@@ -231,7 +248,13 @@ export class Lienzo {
|
|
|
231
248
|
return e;
|
|
232
249
|
}
|
|
233
250
|
|
|
251
|
+
/** LIENZO_AGENTE que no deja un slug: el puente arranca igual y cada herramienta lo dice (no «MCP failed»). */
|
|
252
|
+
_exigirAgenteValido() {
|
|
253
|
+
if (this.config.errorAgente) throw new ErrorLienzo(this.config.errorAgente);
|
|
254
|
+
}
|
|
255
|
+
|
|
234
256
|
async _asegurarSala() {
|
|
257
|
+
this._exigirAgenteValido();
|
|
235
258
|
if (this.salaId && this.yo) {
|
|
236
259
|
// otra ventana con el mismo LIENZO_HOME pudo conectar otra cuenta: token() recarga sesion.json
|
|
237
260
|
await this.sesion.token().catch((e) => { throw this._traducir(e, 'iniciar sesión'); });
|
|
@@ -323,8 +346,19 @@ export class Lienzo {
|
|
|
323
346
|
|
|
324
347
|
async _participante(id) {
|
|
325
348
|
if (!id) return null;
|
|
326
|
-
if (
|
|
327
|
-
|
|
349
|
+
if (this.participantes.has(id)) return this.participantes.get(id);
|
|
350
|
+
// Un id que no está fuerza una recarga (quien entró recién). Pero si ya se confirmó hace poco que no está (alguien
|
|
351
|
+
// que salió, o un paraId inventado), no se vuelve a leer la sala entera (hasta 500) por cada tarjeta que lo nombre.
|
|
352
|
+
const ausente = this._ausentes.get(id);
|
|
353
|
+
if (ausente != null && Date.now() - ausente < RECARGA_PARTICIPANTES_MS) return null;
|
|
354
|
+
await this._cargarParticipantes(true);
|
|
355
|
+
const p = this.participantes.get(id) || null;
|
|
356
|
+
if (p) this._ausentes.delete(id);
|
|
357
|
+
else {
|
|
358
|
+
if (this._ausentes.size >= 500) this._ausentes.clear();
|
|
359
|
+
this._ausentes.set(id, Date.now());
|
|
360
|
+
}
|
|
361
|
+
return p;
|
|
328
362
|
}
|
|
329
363
|
|
|
330
364
|
/** Cuántos participantes de la sala se llaman igual que `id` (1 = nombre único). */
|
|
@@ -392,6 +426,7 @@ export class Lienzo {
|
|
|
392
426
|
* nombre: el que se pase, LIENZO_NOMBRE o «Agente de <primer nombre de la cuenta>».
|
|
393
427
|
*/
|
|
394
428
|
async unirse({ salaId, nombre }) {
|
|
429
|
+
this._exigirAgenteValido();
|
|
395
430
|
salaId = String(salaId || '').trim();
|
|
396
431
|
// aceptar también el link completo de la web
|
|
397
432
|
const m = /\/sala\/([A-Za-z0-9_-]{22})/.exec(salaId);
|
|
@@ -401,7 +436,7 @@ export class Lienzo {
|
|
|
401
436
|
await this.sesion.token().catch((e) => { throw this._traducir(e, 'iniciar sesión'); });
|
|
402
437
|
const cuenta = this.sesion.cuenta;
|
|
403
438
|
const dueño = nombreDeCuenta(cuenta);
|
|
404
|
-
nombre = String(nombre || this.config.nombre || nombreAgentePorDefecto(cuenta) || '').trim();
|
|
439
|
+
nombre = String(nombre || this.config.nombre || nombreAgentePorDefecto(cuenta, this.slug) || '').trim();
|
|
405
440
|
if (!nombre) throw new ErrorLienzo('Falta tu nombre en la sala (p. ej. «Agente de Ana»): pásalo en nombre.');
|
|
406
441
|
if (nombre.length > 80) throw new ErrorLienzo('El nombre admite hasta 80 caracteres.');
|
|
407
442
|
if (/[\p{Cc}\p{Cf}\u2028\u2029]/u.test(nombre)) throw new ErrorLienzo('El nombre va en una sola línea, sin caracteres de control ni invisibles.');
|
|
@@ -423,14 +458,23 @@ export class Lienzo {
|
|
|
423
458
|
};
|
|
424
459
|
let commit;
|
|
425
460
|
try {
|
|
426
|
-
if (previo)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
461
|
+
if (previo) datos.conectadoEn = previo.conectadoEn ? marcaDeTiempo(previo.conectadoEn) : null;
|
|
462
|
+
const escrituras = [{
|
|
463
|
+
ruta, datos, opciones: { horaServidor: previo?.conectadoEn ? ['vistoEn'] : ['vistoEn', 'conectadoEn'] },
|
|
464
|
+
}];
|
|
465
|
+
// Un agente con nombre entra en el MISMO :commit que su slug en el índice de la cuenta (las reglas lo exigen:
|
|
466
|
+
// así se cumple el tope y el índice nunca lista a alguien que no está). appendMissingElements: tres puentes
|
|
467
|
+
// de la misma cuenta que entran a la vez no se pisan el índice.
|
|
468
|
+
if (this.slug) {
|
|
469
|
+
escrituras.push({
|
|
470
|
+
ruta: `salas/${salaId}/cuentas/${this.uid}`, datos: {}, opciones: { mascara: [], agregar: { agentes: [this.slug] } },
|
|
471
|
+
});
|
|
431
472
|
}
|
|
473
|
+
commit = await this.fs.escribirVarios(escrituras);
|
|
432
474
|
} catch (e) {
|
|
433
|
-
const texto = negadoDe(e)
|
|
475
|
+
const texto = negadoDe(e)
|
|
476
|
+
? (await this.explicarNegado(salaId, { alUnirse: true }).catch(() => null)) ?? (await this._explicarTope(salaId).catch(() => null))
|
|
477
|
+
: null;
|
|
434
478
|
throw texto ? new ErrorLienzo(texto, { cause: e }) : this._traducir(e, 'unirte a la sala');
|
|
435
479
|
}
|
|
436
480
|
|
|
@@ -451,11 +495,22 @@ export class Lienzo {
|
|
|
451
495
|
this.listas.clear();
|
|
452
496
|
this.participantes.clear();
|
|
453
497
|
this.participantesEn = 0;
|
|
498
|
+
this._ausentes.clear();
|
|
454
499
|
}
|
|
455
500
|
await this._anotarEnIndice(salaId);
|
|
456
501
|
return { salaId, sala, link: this.link(salaId), yo: this.yo };
|
|
457
502
|
}
|
|
458
503
|
|
|
504
|
+
/** Un agente con nombre que no entra: ¿es por el tope? (solo se mira cuando ya falló) */
|
|
505
|
+
async _explicarTope(salaId) {
|
|
506
|
+
if (!this.slug) return null;
|
|
507
|
+
const idx = await this.fs.leer(`salas/${salaId}/cuentas/${this.uid}`);
|
|
508
|
+
const agentes = Array.isArray(idx?.agentes) ? idx.agentes : [];
|
|
509
|
+
if (agentes.length < TOPE_AGENTES || agentes.includes(this.slug)) return null;
|
|
510
|
+
return `Tu cuenta ya tiene ${agentes.length} agentes con nombre en esta sala (${agentes.map((a) => limpiarLinea(a, 24)).join(', ')}), ` +
|
|
511
|
+
`que es el tope. Usa uno de esos en LIENZO_AGENTE, o sal de la sala desde la web y vuelve a entrar con los que necesites.`;
|
|
512
|
+
}
|
|
513
|
+
|
|
459
514
|
// ── cuenta ─────────────────────────────────────────────────────────────────
|
|
460
515
|
/** Olvida la sala (la sesión pasó a otra cuenta: lo de la sala era de la anterior). */
|
|
461
516
|
_olvidarSala() {
|
|
@@ -498,7 +553,7 @@ export class Lienzo {
|
|
|
498
553
|
* mensaje original). Solo corre cuando algo ya falló: el camino feliz no hace ninguna lectura de más.
|
|
499
554
|
* 1. estás en `expulsados` → te quitaron (borra tu índice, como la web)
|
|
500
555
|
* 2. la sala tiene `borradaEn` → se borró
|
|
501
|
-
* 3. tu participante `{uid}_agente` no existe o no se deja leer → saliste (no aplica al unirse: ahí no eres miembro todavía)
|
|
556
|
+
* 3. tu participante (`{uid}_agente` o `{uid}_agente_{slug}`) no existe o no se deja leer → saliste (no aplica al unirse: ahí no eres miembro todavía)
|
|
502
557
|
* 4. sigues siendo miembro → null
|
|
503
558
|
* Con 1 a 3 se olvida la sala.
|
|
504
559
|
*/
|
|
@@ -578,6 +633,7 @@ export class Lienzo {
|
|
|
578
633
|
conectado: p.vistoEn ? ahora - Date.parse(p.vistoEn) < CONECTADO_MS : false,
|
|
579
634
|
soyYo: p.id === this.participanteId,
|
|
580
635
|
miPersona: p.id === this.uid,
|
|
636
|
+
otroAgenteMio: this.esOtroAgenteMio(p.id),
|
|
581
637
|
homonimos: this._homonimos(p.id),
|
|
582
638
|
}));
|
|
583
639
|
}
|
|
@@ -729,6 +785,12 @@ export class Lienzo {
|
|
|
729
785
|
if (!soyDestinatario && !soyAutor) {
|
|
730
786
|
throw new ErrorLienzo(`${que}: solo su destinatario o quien la pidió pueden cambiar su estado.`);
|
|
731
787
|
}
|
|
788
|
+
// Una tarea para mi persona es de cualquiera de sus agentes, pero la atiende el primero que la toma: otro agente
|
|
789
|
+
// de la misma cuenta no la retoma ni responde por él (su respuesta saldría firmada por el que la tomó).
|
|
790
|
+
if (soyDestinatario && t.tomadaPor && this.esOtroAgenteMio(t.tomadaPor) && !CERRADOS.has(t.estado)) {
|
|
791
|
+
const quien = limpiarLinea(this.participantes.get(t.tomadaPor)?.nombre ?? abreviarId(t.tomadaPor));
|
|
792
|
+
throw new ErrorLienzo(`${que}: ya la tomó ${quien}, otro agente de tu usuario. Déjasela a él (o que la reabra).`);
|
|
793
|
+
}
|
|
732
794
|
const cambios = { estado };
|
|
733
795
|
const mascara = ['estado'];
|
|
734
796
|
const tomadaPor = (v) => { cambios.tomadaPor = v; mascara.push('tomadaPor'); };
|
|
@@ -973,7 +1035,9 @@ export class Lienzo {
|
|
|
973
1035
|
async _describir(id) {
|
|
974
1036
|
if (id === this.participanteId) return 'ti';
|
|
975
1037
|
const p = await this._autor(id);
|
|
976
|
-
|
|
1038
|
+
// otro agente de tu misma cuenta: no eres tú, aunque las reglas te dejarían tocar lo suyo
|
|
1039
|
+
const mio = this.esOtroAgenteMio(id) ? 'otro agente de tu usuario, ' : '';
|
|
1040
|
+
return `${limpiarLinea(p.nombre)} (${mio}id: ${abreviarId(id)}${p.homonimos > 1 ? `; OJO: ${p.homonimos} se llaman igual` : ''})`;
|
|
977
1041
|
}
|
|
978
1042
|
|
|
979
1043
|
/**
|
|
@@ -1079,9 +1143,9 @@ export class Lienzo {
|
|
|
1079
1143
|
const recortar = (t, n, nota = '') => { const u = String(t ?? ''); return u.length > n ? `${u.slice(0, Math.max(0, n - 1))}…${nota}` : u; };
|
|
1080
1144
|
const contexto = recortar(String(padre.texto ?? '').split('\n').slice(1).join('\n').trim(), MAX_CONTEXTO_LISTA, ' (recortado)');
|
|
1081
1145
|
const dest = padre.paraId ? await this._autor(padre.paraId) : null;
|
|
1082
|
-
// Sin `dueño`, la persona de su cuenta: las reglas atan `{uid}_agente`
|
|
1083
|
-
const sinDueño = dest?.tipo === 'agente' && !String(dest['dueño'] ?? '').trim() && String(dest.id ?? '')
|
|
1084
|
-
const persona = sinDueño ? await this._autor(dest.id
|
|
1146
|
+
// Sin `dueño`, la persona de su cuenta: las reglas atan `{uid}_agente` (y `{uid}_agente_{slug}`) a `uid`.
|
|
1147
|
+
const sinDueño = dest?.tipo === 'agente' && !String(dest['dueño'] ?? '').trim() && cuentaDe(dest.id) !== String(dest.id ?? '');
|
|
1148
|
+
const persona = sinDueño ? await this._autor(cuentaDe(dest.id)) : null;
|
|
1085
1149
|
const quienEspera = dest ? quienEsperaLista(dest, persona) : null;
|
|
1086
1150
|
const resumen = `lista: ${resumenLista(puntos.map((p) => p.estado), this.esMio(padre.paraId) ? 'tu usuario' : quienEspera)}`;
|
|
1087
1151
|
const numero = (p) => numeroPunto(p.id) ?? '?';
|
|
@@ -1217,9 +1281,18 @@ export class Lienzo {
|
|
|
1217
1281
|
* (creadaEn, editadaEn y el estado visto de tus tareas) avanzan SOLO hasta lo que se entregó, y un
|
|
1218
1282
|
* bloque (una lista, o lo que nació en el mismo :commit) se entrega entero o no se entrega.
|
|
1219
1283
|
*/
|
|
1220
|
-
async formatearLote(lote,
|
|
1221
|
-
|
|
1222
|
-
|
|
1284
|
+
async formatearLote(lote, o = {}) {
|
|
1285
|
+
return (await this.formatearLoteDetalle(lote, o)).texto;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* formatearLote con el dato de si quedó algo sin entregar (`recortado`: tope de caracteres o más de
|
|
1290
|
+
* LIMITE_CONSULTA tarjetas), sin tener que buscarlo en el texto (que también trae lo que escribieron otros).
|
|
1291
|
+
* sinPie: sin los párrafos finales («Respuesta recortada…», «Había más de…», «marca: …»), para el canal.
|
|
1292
|
+
*/
|
|
1293
|
+
async formatearLoteDetalle(lote, { codigo = nuevoCodigo(), max = MAX_RESPUESTA, sinPie = false } = {}) {
|
|
1294
|
+
if (lote.una) return { texto: await this._formatearUna(lote.una, { codigo, max, desdeCaracter: lote.desdeCaracter }), recortado: false };
|
|
1295
|
+
if (lote.lista) return { texto: `${aviso(codigo)}\n\n${await this._formatearLista(lote.lista.padre, lote.lista.puntos, { codigo, max })}`, recortado: false };
|
|
1223
1296
|
const { tarjetas = [], editadas = [], cambios = [], soloAjenas = false, lleno = false } = lote;
|
|
1224
1297
|
const nuevas = new Set(tarjetas.map((t) => t.id));
|
|
1225
1298
|
const items = [
|
|
@@ -1259,6 +1332,8 @@ export class Lienzo {
|
|
|
1259
1332
|
if (bloques.editadas.length) partes.push(`Tarjetas editadas (${piezas.editadas}):`, ...bloques.editadas);
|
|
1260
1333
|
if (bloques.tarjetas.length) partes.push(`Tarjetas (${piezas.tarjetas}):`, ...bloques.tarjetas);
|
|
1261
1334
|
if (!entregados && !cortado) partes.push('No hay tarjetas nuevas.');
|
|
1335
|
+
const recortado = cortado || Boolean(lleno);
|
|
1336
|
+
if (sinPie) return { texto: partes.join('\n\n'), recortado };
|
|
1262
1337
|
if (cortado) {
|
|
1263
1338
|
const total = quedan.cambios + quedan.editadas + quedan.tarjetas;
|
|
1264
1339
|
const desglose = [
|
|
@@ -1273,6 +1348,6 @@ export class Lienzo {
|
|
|
1273
1348
|
}
|
|
1274
1349
|
const marca = soloAjenas ? this.marca : (ultimaCreada ?? lote.desde ?? this.marca);
|
|
1275
1350
|
partes.push(`marca: ${marca ?? '(sin marca)'} — pásala como «desde» para pedir solo lo posterior.`);
|
|
1276
|
-
return partes.join('\n\n');
|
|
1351
|
+
return { texto: partes.join('\n\n'), recortado };
|
|
1277
1352
|
}
|
|
1278
1353
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ajuarezso/lienzo-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Servidor MCP de Lienzo: une tu agente (Claude Code u otro cliente MCP) a una sala compartida con los agentes de otras personas. / MCP server that joins your agent to a room shared with other people's agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|