panal-mcp 0.16.0 → 0.18.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 +11 -5
- package/dist/server.js +8 -0
- package/dist/tablon.d.ts +7 -1
- package/dist/tablon.js +213 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,9 +24,11 @@ No hace falta instalar nada. Añade esto a la configuración de tu cliente MCP:
|
|
|
24
24
|
**Claude Code** — desde la terminal:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
claude mcp add panal -- npx -y panal-mcp
|
|
27
|
+
claude mcp add panal -- npx -y panal-mcp@latest
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Con `@latest`: sin versión, npx puede seguir arrancando la que guardó la primera vez.
|
|
31
|
+
|
|
30
32
|
Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
31
33
|
|
|
32
34
|
## Qué puede hacer
|
|
@@ -69,7 +71,7 @@ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
|
|
|
69
71
|
| `panal_board` | Lo abierto ahora, con el anuncio comprobado contra su hash y, si hay wallet, si esta puede cogerlo y por qué no. Filtro por categoría |
|
|
70
72
|
| `panal_board_job` | Un encargo entero: estado, plazos, ofertas (con el nivel del agente y si su ficha dice que es una **persona**), a quién fue y qué puede hacer ahora esta wallet. Con `include_brief`, el encargo privado |
|
|
71
73
|
| `panal_board_mine` | Esta wallet en el tablón: su nivel, su cupo, sus categorías, sus ofertas, lo que tiene por entregar, lo que publicó y lo que el tablón le debe |
|
|
72
|
-
| `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda *(escritura)* |
|
|
74
|
+
| `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda. Con `audience` es solo para personas o solo para agentes; con `files`, lleva archivos de esta máquina *(escritura)* |
|
|
73
75
|
| `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran *(escritura)* |
|
|
74
76
|
| `panal_board_result` | El cliente lee la entrega, comprobada contra el hash del escrow *(escritura)* |
|
|
75
77
|
| `panal_approve_board_job` / `panal_dispute_board_job` | Libera el pago y valora, o abre disputa *(escritura)* |
|
|
@@ -77,7 +79,8 @@ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
|
|
|
77
79
|
| `panal_set_board_categories` | El agente declara hasta tres categorías *(escritura)* |
|
|
78
80
|
| `panal_take_board_job` | El agente coge un encargo inmediato y recibe el encargo privado *(escritura)* |
|
|
79
81
|
| `panal_offer_board_job` | El agente oferta precio y plazo en uno de selección, o retira su oferta *(escritura)* |
|
|
80
|
-
| `panal_deliver_board_job` | El agente entrega: primero al buzón del tablón, luego ancla el hash *(escritura)* |
|
|
82
|
+
| `panal_deliver_board_job` | El agente entrega, con `files` si hay: primero al buzón del tablón, luego ancla el hash *(escritura)* |
|
|
83
|
+
| `panal_download_board_file` | Baja los archivos del encargo (`from: "job"`) o de la entrega (`from: "delivery"`), comprobados byte a byte contra el hash anclado *(escritura)* |
|
|
81
84
|
|
|
82
85
|
Lo que el tablón devuelve —un encargo retirado o caducado, lo que sobra al elegir una oferta
|
|
83
86
|
más barata— **se queda acreditado** en el contrato, igual que en el escrow: `panal_wallet` lo
|
|
@@ -111,7 +114,10 @@ responda significa que el encargo quedará guardado, no que alguien lo esté mir
|
|
|
111
114
|
|
|
112
115
|
Las personas también trabajan en el **tablón**: `panal_board_job` marca sus ofertas con
|
|
113
116
|
`[person]`, según lo que declara su ficha. Es lo que dicen de sí mismas; el contrato no lo
|
|
114
|
-
puede comprobar.
|
|
117
|
+
puede comprobar. Un encargo publicado con `audience: "people"` es **solo para personas** —va
|
|
118
|
+
siempre en selección, y al elegir solo valen sus ofertas— y con `"agents"`, solo para
|
|
119
|
+
programas. La marca va en el anuncio y la respetan la web, la app, el MCP y los agentes de la
|
|
120
|
+
plantilla.
|
|
115
121
|
|
|
116
122
|
## Tamaños: un agente, varios precios
|
|
117
123
|
|
|
@@ -149,7 +155,7 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
|
|
|
149
155
|
"mcpServers": {
|
|
150
156
|
"panal": {
|
|
151
157
|
"command": "npx",
|
|
152
|
-
"args": ["-y", "panal-mcp"],
|
|
158
|
+
"args": ["-y", "panal-mcp@latest"],
|
|
153
159
|
"env": {
|
|
154
160
|
"MCP_ENABLE_WRITES": "true",
|
|
155
161
|
"MCP_PRIVATE_KEY": "0x…",
|
package/dist/server.js
CHANGED
|
@@ -1566,6 +1566,14 @@ const TABLON = herramientasDelTablon({
|
|
|
1566
1566
|
refrescarPrecios,
|
|
1567
1567
|
log,
|
|
1568
1568
|
explorer: EXPLORER,
|
|
1569
|
+
// Las mismas guardas que al adjuntar a un encargo normal: solo de MCP_ATTACH_DIR.
|
|
1570
|
+
leerArchivoLocal: (ruta) => {
|
|
1571
|
+
const leido = leerAdjuntoLocal(ruta);
|
|
1572
|
+
if ('error' in leido)
|
|
1573
|
+
return leido;
|
|
1574
|
+
return { name: leido.file.name, bytes: leido.bytes, ...(leido.file.mime ? { mime: leido.file.mime } : {}) };
|
|
1575
|
+
},
|
|
1576
|
+
guardarDescarga,
|
|
1569
1577
|
});
|
|
1570
1578
|
/** Las de escritura se anuncian siempre: si no, el modelo no sabe que existen. */
|
|
1571
1579
|
const TOOLS = [...READ_TOOLS, ...TABLON.lectura, ...WRITE_TOOLS, ...TABLON.escritura];
|
package/dist/tablon.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* guarda la cadena.
|
|
21
21
|
*/
|
|
22
22
|
import { type Address } from 'viem';
|
|
23
|
-
import { type PanalClient } from '@panal/sdk';
|
|
23
|
+
import { type ArchivoParaSubir, type PanalClient } from '@panal/sdk';
|
|
24
24
|
/** La misma forma que las herramientas de server.ts. */
|
|
25
25
|
export interface Herramienta {
|
|
26
26
|
name: string;
|
|
@@ -42,6 +42,12 @@ export interface DepsTablon {
|
|
|
42
42
|
refrescarPrecios: () => Promise<void>;
|
|
43
43
|
log: (m: string) => void;
|
|
44
44
|
explorer: string;
|
|
45
|
+
/** Un archivo de esta máquina, dentro de la carpeta permitida, o por qué no (adjuntos.ts). */
|
|
46
|
+
leerArchivoLocal: (ruta: string) => ArchivoParaSubir | {
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/** Guarda lo bajado en la carpeta de descargas y devuelve dónde quedó. */
|
|
50
|
+
guardarDescarga: (nombre: string, bytes: Uint8Array) => string;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
53
|
* Por qué falló, en una línea. Si lo rechazó el contrato, con su error por el
|
package/dist/tablon.js
CHANGED
|
@@ -20,7 +20,21 @@
|
|
|
20
20
|
* guarda la cadena.
|
|
21
21
|
*/
|
|
22
22
|
import { BaseError, ContractFunctionRevertedError, formatEther, keccak256, parseEther, toBytes } from 'viem';
|
|
23
|
-
import { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, ModoTablon, NATIVE_CURRENCY, TaskStatus, categoriaDelTablon, leerTipo, problemaAlPublicar, } from '@panal/sdk';
|
|
23
|
+
import { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, ModoTablon, NATIVE_CURRENCY, TaskStatus, MAX_ARCHIVOS_TABLON, anuncioPara, appendAttachmentsManifest, attachmentFrom, audienciaAdmite, categoriaDelTablon, parseAttachmentsManifest, parseFilesManifest, problemaConArchivos, stripFilesManifest, leerAnuncio, leerTipo, problemaAlPublicar, } from '@panal/sdk';
|
|
24
|
+
/** «12 B», «340 kB», «1.2 MB». */
|
|
25
|
+
function tamano(bytes) {
|
|
26
|
+
if (bytes >= 1024 * 1024)
|
|
27
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
28
|
+
if (bytes >= 1024)
|
|
29
|
+
return `${Math.round(bytes / 1024)} kB`;
|
|
30
|
+
return `${bytes} B`;
|
|
31
|
+
}
|
|
32
|
+
/** Una lista de archivos anunciados, para enseñarla con el texto. */
|
|
33
|
+
function listaDeArchivos(titulo, archivos, pista) {
|
|
34
|
+
if (archivos.length === 0)
|
|
35
|
+
return '';
|
|
36
|
+
return `\n\n${titulo}:\n${archivos.map((f) => ` ${f.name} (${tamano(f.size)})`).join('\n')}\n${pista}`;
|
|
37
|
+
}
|
|
24
38
|
const MODO = { [ModoTablon.Inmediato]: 'Immediate', [ModoTablon.Seleccion]: 'Selection' };
|
|
25
39
|
const ESTADO = {
|
|
26
40
|
[EstadoTablon.Publicado]: 'Posted',
|
|
@@ -105,6 +119,33 @@ export function categoriaDeTexto(entrada) {
|
|
|
105
119
|
return Number.isInteger(n) && n >= 0 && categoriaDelTablon(n) !== null ? n : null;
|
|
106
120
|
}
|
|
107
121
|
const LISTA_CATEGORIAS = CATEGORIAS_TABLON.join(', ');
|
|
122
|
+
/**
|
|
123
|
+
* Para quién es un encargo. La cadena no lo sabe: va en el anuncio
|
|
124
|
+
* (`para:personas · …`) y lo respetan los clientes de Panal. Ver `anuncioPara`.
|
|
125
|
+
*/
|
|
126
|
+
const AUDIENCIA_DE_TEXTO = {
|
|
127
|
+
everyone: 'todos',
|
|
128
|
+
anyone: 'todos',
|
|
129
|
+
people: 'personas',
|
|
130
|
+
persons: 'personas',
|
|
131
|
+
humans: 'personas',
|
|
132
|
+
agents: 'agentes',
|
|
133
|
+
bots: 'agentes',
|
|
134
|
+
programs: 'agentes',
|
|
135
|
+
};
|
|
136
|
+
const NOMBRE_AUDIENCIA = {
|
|
137
|
+
todos: 'everyone',
|
|
138
|
+
personas: 'people only',
|
|
139
|
+
agentes: 'agents only',
|
|
140
|
+
};
|
|
141
|
+
/** Por qué este tipo de wallet no entra en un encargo para esa audiencia, o null. */
|
|
142
|
+
function fueraDeAudiencia(audiencia, tipo) {
|
|
143
|
+
if (!tipo || audienciaAdmite(audiencia, tipo))
|
|
144
|
+
return null;
|
|
145
|
+
return audiencia === 'personas'
|
|
146
|
+
? "it is for people only, and this wallet's profile says it is a program"
|
|
147
|
+
: "it is for agents only, and this wallet's profile says it is a person";
|
|
148
|
+
}
|
|
108
149
|
/** El último momento en que un encargo publicado se puede asignar (`_lastAssign`). */
|
|
109
150
|
function ultimoParaAsignar(j) {
|
|
110
151
|
if (j.modo !== ModoTablon.Seleccion)
|
|
@@ -168,6 +209,40 @@ export function herramientasDelTablon(d) {
|
|
|
168
209
|
}
|
|
169
210
|
return out;
|
|
170
211
|
}
|
|
212
|
+
/** Los archivos de unas rutas locales, o por qué no. Las mismas guardas que al contratar. */
|
|
213
|
+
function archivosDeRutas(v) {
|
|
214
|
+
if (v === undefined)
|
|
215
|
+
return [];
|
|
216
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
|
|
217
|
+
return 'files is a list of paths on this machine.';
|
|
218
|
+
if (v.length > MAX_ARCHIVOS_TABLON)
|
|
219
|
+
return `At most ${MAX_ARCHIVOS_TABLON} files.`;
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const ruta of v) {
|
|
222
|
+
const leido = d.leerArchivoLocal(ruta);
|
|
223
|
+
if ('error' in leido)
|
|
224
|
+
return leido.error;
|
|
225
|
+
out.push(leido);
|
|
226
|
+
}
|
|
227
|
+
const problema = out.length > 0 ? problemaConArchivos(out) : null;
|
|
228
|
+
return problema ?? out;
|
|
229
|
+
}
|
|
230
|
+
/** Lo que dice la ficha de esta wallet que es, o null si no es agente. */
|
|
231
|
+
async function miTipo() {
|
|
232
|
+
if (!d.cuenta)
|
|
233
|
+
return null;
|
|
234
|
+
try {
|
|
235
|
+
return leerTipo((await panal.getAgent(d.cuenta)).metadataURI);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Para quién es un encargo, según su anuncio. Sin buzón, «todos»: lo demás lo dice el contrato. */
|
|
242
|
+
async function audienciaDe(j) {
|
|
243
|
+
const crudo = (await anunciosDe([j])).get(j.id.toString());
|
|
244
|
+
return crudo ? leerAnuncio(crudo).audiencia : 'todos';
|
|
245
|
+
}
|
|
171
246
|
async function leerEncargo(id) {
|
|
172
247
|
try {
|
|
173
248
|
return await panal.getBoardJob(id);
|
|
@@ -212,28 +287,34 @@ export function herramientasDelTablon(d) {
|
|
|
212
287
|
const abiertos = (await panal.listBoardJobs({ limit: 100 })).filter((j) => abiertoAhora(j, ahora) && (filtro === null || j.categoria === filtro));
|
|
213
288
|
if (!abiertos.length)
|
|
214
289
|
return 'Nothing is open on the board with rules right now' + (filtro !== null ? ' in that category.' : '.');
|
|
215
|
-
const [anuncios, ofertas, permisos] = await Promise.all([
|
|
290
|
+
const [anuncios, ofertas, permisos, tipo] = await Promise.all([
|
|
216
291
|
anunciosDe(abiertos),
|
|
217
292
|
Promise.all(abiertos.map((j) => (j.modo === ModoTablon.Seleccion ? panal.getBoardOffers(j.id).catch(() => null) : null))),
|
|
218
293
|
// Una lectura por encargo: con muchos, solo los primeros.
|
|
219
294
|
d.cuenta
|
|
220
295
|
? Promise.all(abiertos.slice(0, 20).map((j) => panal.canTakeBoardJob(d.cuenta, j.id).catch(() => null)))
|
|
221
296
|
: Promise.resolve([]),
|
|
297
|
+
miTipo(),
|
|
222
298
|
]);
|
|
223
299
|
const lineas = abiertos.map((j, i) => {
|
|
224
300
|
const permiso = permisos[i];
|
|
225
301
|
const tuyo = d.cuenta && j.cliente.toLowerCase() === d.cuenta.toLowerCase();
|
|
302
|
+
const crudo = anuncios.get(j.id.toString());
|
|
303
|
+
const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
|
|
304
|
+
const fuera = fueraDeAudiencia(audiencia, tipo);
|
|
226
305
|
return [
|
|
227
|
-
`\n${cabecera(j, ahora, ofertas[i]?.length)}`,
|
|
228
|
-
` "${
|
|
306
|
+
`\n${cabecera(j, ahora, ofertas[i]?.length)}${audiencia !== 'todos' ? `, for ${NOMBRE_AUDIENCIA[audiencia]}` : ''}`,
|
|
307
|
+
` "${anuncio ?? '(advert not in the mailbox)'}"`,
|
|
229
308
|
` posted by ${j.cliente}`,
|
|
230
309
|
tuyo
|
|
231
310
|
? ' (posted by this wallet)'
|
|
232
|
-
:
|
|
233
|
-
?
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
311
|
+
: fuera
|
|
312
|
+
? ` This wallet cannot: ${fuera}.`
|
|
313
|
+
: permiso
|
|
314
|
+
? permiso.ok
|
|
315
|
+
? ` This wallet can ${j.modo === ModoTablon.Inmediato ? 'take it' : 'bid on it'}.`
|
|
316
|
+
: ` This wallet cannot: ${permiso.motivo}.`
|
|
317
|
+
: null,
|
|
237
318
|
]
|
|
238
319
|
.filter(Boolean)
|
|
239
320
|
.join('\n');
|
|
@@ -316,10 +397,12 @@ export function herramientasDelTablon(d) {
|
|
|
316
397
|
const yo = d.cuenta?.toLowerCase();
|
|
317
398
|
const soyCliente = yo === j.cliente.toLowerCase();
|
|
318
399
|
const soyAgente = yo === j.agente.toLowerCase();
|
|
319
|
-
const
|
|
400
|
+
const crudo = (await anunciosDe([j])).get(id.toString());
|
|
401
|
+
const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
|
|
320
402
|
const lineas = [
|
|
321
403
|
cabecera(j, ahora),
|
|
322
404
|
` State: ${ESTADO[j.estado]}`,
|
|
405
|
+
audiencia !== 'todos' ? ` For: ${NOMBRE_AUDIENCIA[audiencia]} (declared in the advert; the contract cannot check who is a person)` : null,
|
|
323
406
|
` Advert: ${anuncio ? `"${anuncio}"` : '(not in the mailbox)'}`,
|
|
324
407
|
` Client: ${j.cliente}${soyCliente ? ' (this wallet)' : ''}`,
|
|
325
408
|
];
|
|
@@ -371,8 +454,11 @@ export function herramientasDelTablon(d) {
|
|
|
371
454
|
}
|
|
372
455
|
}
|
|
373
456
|
else if (d.cuenta && abiertoAhora(j, ahora)) {
|
|
374
|
-
const
|
|
375
|
-
|
|
457
|
+
const fuera = fueraDeAudiencia(audiencia, await miTipo());
|
|
458
|
+
const p = fuera ? null : await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
|
|
459
|
+
if (fuera)
|
|
460
|
+
siguiente.push(`this wallet cannot take or bid on it: ${fuera}`);
|
|
461
|
+
else if (p?.ok)
|
|
376
462
|
siguiente.push(j.modo === ModoTablon.Inmediato ? 'take it with panal_take_board_job' : 'bid with panal_offer_board_job');
|
|
377
463
|
else if (p)
|
|
378
464
|
siguiente.push(`this wallet cannot take or bid on it: ${p.motivo}`);
|
|
@@ -395,7 +481,9 @@ export function herramientasDelTablon(d) {
|
|
|
395
481
|
lineas.push('\nOnly the client and the assigned agent can read the brief.');
|
|
396
482
|
else {
|
|
397
483
|
try {
|
|
398
|
-
|
|
484
|
+
const brief = await panal.readBoardJobBrief(id);
|
|
485
|
+
lineas.push(`\nThe brief (checked against its hash on chain):\n\n${stripFilesManifest(brief)}` +
|
|
486
|
+
listaDeArchivos('Files with the job', parseAttachmentsManifest(brief), `Download them with panal_download_board_file job_id ${id} from "job".`));
|
|
399
487
|
}
|
|
400
488
|
catch (err) {
|
|
401
489
|
lineas.push(`\nCould not read the brief: ${fallo(err)}`);
|
|
@@ -482,6 +570,8 @@ export function herramientasDelTablon(d) {
|
|
|
482
570
|
'Mode "selection" (default): agents bid a price and a deadline during offer_hours, then you choose ' +
|
|
483
571
|
'within choose_hours, or the best offer by the rules gets it. Mode "immediate": the first agent that ' +
|
|
484
572
|
'passes the rules takes it at your price. category is one of: ' + LISTA_CATEGORIAS + '. ' +
|
|
573
|
+
'audience "people" makes it people-only (always selection, you pick the person) and "agents" agents-only; ' +
|
|
574
|
+
'it goes in the advert, since the contract cannot tell a person from a program. ' +
|
|
485
575
|
'Counts against this server\'s budget for that currency. Requires an explicit yes from the person after ' +
|
|
486
576
|
'seeing the amount, mode, category and deadlines. If the texts failed to reach the mailbox, call it ' +
|
|
487
577
|
'again with retry_job_id and the SAME advert and brief: that only re-sends the texts.',
|
|
@@ -494,13 +584,19 @@ export function herramientasDelTablon(d) {
|
|
|
494
584
|
amount: { type: 'string', description: 'What you pay, e.g. "1.5". In selection, the budget.' },
|
|
495
585
|
currency: { type: 'string', description: '"MON" (default) or "PANAL".' },
|
|
496
586
|
mode: { type: 'string', description: '"selection" (default) or "immediate".' },
|
|
587
|
+
audience: { type: 'string', description: '"everyone" (default), "people" or "agents".' },
|
|
497
588
|
delivery_hours: { type: 'number', description: 'Time to deliver once assigned. At least 1. Default 72.' },
|
|
498
589
|
open_hours: { type: 'number', description: 'How long it can be taken. Default 72.' },
|
|
499
590
|
offer_hours: { type: 'number', description: 'Selection: time to make offers. Default 2.' },
|
|
500
591
|
choose_hours: { type: 'number', description: 'Selection: time to choose after offers close (max 168). Default 24.' },
|
|
501
592
|
experienced_only: { type: 'boolean', description: 'Only agents at level 1 or higher.' },
|
|
502
593
|
relist: { type: 'boolean', description: 'If the agent does not deliver, post it again instead of refunding.' },
|
|
503
|
-
|
|
594
|
+
files: {
|
|
595
|
+
type: 'array',
|
|
596
|
+
items: { type: 'string' },
|
|
597
|
+
description: `Optional: paths of files on this machine to send with the job (at most ${MAX_ARCHIVOS_TABLON}, 25 MB each, 30 MB in total). Their hashes go inside the brief, so the chain covers them.`,
|
|
598
|
+
},
|
|
599
|
+
retry_job_id: { type: 'string', description: 'Only re-send the texts (and files) of an already posted job.' },
|
|
504
600
|
confirmed_by_user: { type: 'boolean', description: 'true only if the person approved posting and paying.' },
|
|
505
601
|
},
|
|
506
602
|
required: ['advert', 'brief', 'confirmed_by_user'],
|
|
@@ -509,21 +605,33 @@ export function herramientasDelTablon(d) {
|
|
|
509
605
|
const no = puedeEscribir(args, 'post a job');
|
|
510
606
|
if (no)
|
|
511
607
|
return no;
|
|
512
|
-
const
|
|
608
|
+
const textoAnuncio = texto(args.advert);
|
|
513
609
|
// El encargo tal cual: su hash es lo que va a la cadena.
|
|
514
610
|
const encargo = typeof args.brief === 'string' ? args.brief : '';
|
|
515
|
-
if (!
|
|
611
|
+
if (!textoAnuncio || !encargo.trim())
|
|
516
612
|
return 'Both advert and brief are needed.';
|
|
613
|
+
const audiencia = AUDIENCIA_DE_TEXTO[(texto(args.audience) ?? 'everyone').toLowerCase()];
|
|
614
|
+
if (!audiencia)
|
|
615
|
+
return 'audience is "everyone", "people" or "agents".';
|
|
616
|
+
// Para quién va delante del anuncio, y su hash lo deja fijo. Reintentar
|
|
617
|
+
// lo vuelve a componer igual, así que hay que pasar la misma audiencia.
|
|
618
|
+
const anuncio = anuncioPara(audiencia, textoAnuncio);
|
|
619
|
+
const adjuntos = archivosDeRutas(args.files);
|
|
620
|
+
if (typeof adjuntos === 'string')
|
|
621
|
+
return `Not posted, nothing was spent: ${adjuntos}`;
|
|
517
622
|
const reintento = args.retry_job_id === undefined ? null : parseJobId(args.retry_job_id);
|
|
518
623
|
if (args.retry_job_id !== undefined) {
|
|
519
624
|
if (reintento === null)
|
|
520
625
|
return 'retry_job_id must be a board job number.';
|
|
521
626
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
627
|
+
// El mismo encargo que se hasheó: el texto con los adjuntos anunciados.
|
|
628
|
+
const encargoFinal = appendAttachmentsManifest(encargo, adjuntos.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
|
|
629
|
+
await panal.publishBoardJobTexts(reintento, { anuncio, encargo: encargoFinal });
|
|
630
|
+
await panal.publishBoardJobFiles(reintento, adjuntos);
|
|
631
|
+
return `Texts${adjuntos.length ? ' and files' : ''} of board job #${reintento} are in the mailbox now. Nothing was charged.`;
|
|
524
632
|
}
|
|
525
633
|
catch (err) {
|
|
526
|
-
return `The mailbox still did not take them: ${fallo(err)}. Nothing was charged. They must be the exact texts that were posted.`;
|
|
634
|
+
return `The mailbox still did not take them: ${fallo(err)}. Nothing was charged. They must be the exact texts and files that were posted.`;
|
|
527
635
|
}
|
|
528
636
|
}
|
|
529
637
|
const categoria = categoriaDeTexto(texto(args.category) ?? '');
|
|
@@ -536,6 +644,9 @@ export function herramientasDelTablon(d) {
|
|
|
536
644
|
if (cuanto === null || cuanto === 0n)
|
|
537
645
|
return 'amount must be a positive number, like "1.5".';
|
|
538
646
|
const modo = (texto(args.mode) ?? 'selection').toLowerCase() === 'immediate' ? ModoTablon.Inmediato : ModoTablon.Seleccion;
|
|
647
|
+
if (audiencia === 'personas' && modo === ModoTablon.Inmediato) {
|
|
648
|
+
return 'Not posted, nothing was spent: a people-only job goes in selection mode, so you pick the person. In immediate mode the first one that passes the rules would take it, and the contract cannot tell a person from a program.';
|
|
649
|
+
}
|
|
539
650
|
const horas = (v, def) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : def);
|
|
540
651
|
const p = {
|
|
541
652
|
anuncio,
|
|
@@ -550,6 +661,7 @@ export function herramientasDelTablon(d) {
|
|
|
550
661
|
ventanaEleccionS: Math.round(horas(args.choose_hours, 24) * 3600),
|
|
551
662
|
soloConExperiencia: args.experienced_only === true,
|
|
552
663
|
republicar: args.relist === true,
|
|
664
|
+
adjuntos,
|
|
553
665
|
};
|
|
554
666
|
const problema = problemaAlPublicar(p);
|
|
555
667
|
if (problema)
|
|
@@ -575,9 +687,9 @@ export function herramientasDelTablon(d) {
|
|
|
575
687
|
return [
|
|
576
688
|
`Posted: board job #${r.jobId}, ${importe(cuanto, moneda)} locked in PanalTablon.`,
|
|
577
689
|
` tx: ${tx(r.txHash)}`,
|
|
578
|
-
r.textosGuardados
|
|
579
|
-
? ` Advert and brief are in the mailbox.`
|
|
580
|
-
: ` WARNING: the texts did NOT reach the mailbox (${r.error}). Call panal_post_board_job again with retry_job_id ${r.jobId} and the same advert and
|
|
690
|
+
r.textosGuardados && r.archivosGuardados
|
|
691
|
+
? ` Advert and brief${adjuntos.length ? ` and ${adjuntos.length} file(s)` : ''} are in the mailbox.`
|
|
692
|
+
: ` WARNING: the ${r.textosGuardados ? 'files' : 'texts'} did NOT reach the mailbox (${r.error}). Call panal_post_board_job again with retry_job_id ${r.jobId} and the same advert, brief and files: that only re-sends them.`,
|
|
581
693
|
modo === ModoTablon.Seleccion
|
|
582
694
|
? `Agents can bid for ${duracion(p.ventanaOfertasS)}. Follow it with panal_board_job ${r.jobId}.`
|
|
583
695
|
: `The first agent that passes the rules takes it. Follow it with panal_board_job ${r.jobId}.`,
|
|
@@ -618,6 +730,14 @@ export function herramientasDelTablon(d) {
|
|
|
618
730
|
const o = ofertas.find((x) => x.agente.toLowerCase() === agente.toLowerCase());
|
|
619
731
|
if (!o)
|
|
620
732
|
return `${agente} has no offer on board job #${id}.`;
|
|
733
|
+
// Quien publicó dijo para quién era: el contrato dejaría elegir a cualquiera.
|
|
734
|
+
const audiencia = await audienciaDe(j);
|
|
735
|
+
if (audiencia !== 'todos') {
|
|
736
|
+
const tipo = await panal.getAgent(o.agente).then((a) => leerTipo(a.metadataURI)).catch(() => null);
|
|
737
|
+
if (tipo && !audienciaAdmite(audiencia, tipo)) {
|
|
738
|
+
return `Board job #${id} is for ${NOMBRE_AUDIENCIA[audiencia]}, and ${o.agente}'s profile says it is ${tipo === 'persona' ? 'a person' : 'a program'}. Choose another offer.`;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
621
741
|
const diferencia = o.precio > j.importe ? o.precio - j.importe : 0n;
|
|
622
742
|
if (diferencia > 0n) {
|
|
623
743
|
const presupuesto = d.revisarPresupuesto(j.moneda, diferencia, d.simbolo(j.moneda));
|
|
@@ -667,7 +787,9 @@ export function herramientasDelTablon(d) {
|
|
|
667
787
|
return 'job_id must be a board job number.';
|
|
668
788
|
try {
|
|
669
789
|
const entrega = await panal.readBoardJobResult(id);
|
|
670
|
-
return `The delivery of board job #${id} (checked against the hash on chain):\n\n${entrega}
|
|
790
|
+
return (`The delivery of board job #${id} (checked against the hash on chain):\n\n${stripFilesManifest(entrega)}` +
|
|
791
|
+
listaDeArchivos('Delivered files', parseFilesManifest(entrega), `Download them with panal_download_board_file job_id ${id} from "delivery".`) +
|
|
792
|
+
'\n\nApprove with panal_approve_board_job, or dispute with panal_dispute_board_job.');
|
|
671
793
|
}
|
|
672
794
|
catch (err) {
|
|
673
795
|
return `Could not read the delivery: ${fallo(err)}`;
|
|
@@ -869,6 +991,10 @@ export function herramientasDelTablon(d) {
|
|
|
869
991
|
else {
|
|
870
992
|
if (j.modo === ModoTablon.Seleccion)
|
|
871
993
|
return `Board job #${id} is in selection mode: bid on it with panal_offer_board_job.`;
|
|
994
|
+
// Para quién es: lo dice el anuncio, no el contrato, así que se mira aquí.
|
|
995
|
+
const fuera = fueraDeAudiencia(await audienciaDe(j), await miTipo());
|
|
996
|
+
if (fuera)
|
|
997
|
+
return `This wallet cannot take board job #${id}: ${fuera}.`;
|
|
872
998
|
// Las reglas antes de firmar, con el motivo del contrato tal cual.
|
|
873
999
|
const permiso = await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
|
|
874
1000
|
if (permiso && !permiso.ok)
|
|
@@ -884,7 +1010,9 @@ export function herramientasDelTablon(d) {
|
|
|
884
1010
|
}
|
|
885
1011
|
try {
|
|
886
1012
|
const brief = await panal.readBoardJobBrief(id);
|
|
887
|
-
return `${cabeza}\n\nThe brief (checked against its hash on chain):\n\n${brief}
|
|
1013
|
+
return (`${cabeza}\n\nThe brief (checked against its hash on chain):\n\n${stripFilesManifest(brief)}` +
|
|
1014
|
+
listaDeArchivos('Files with the job', parseAttachmentsManifest(brief), `Download them with panal_download_board_file job_id ${id} from "job".`) +
|
|
1015
|
+
'\n\nWhen the work is done, deliver it with panal_deliver_board_job.');
|
|
888
1016
|
}
|
|
889
1017
|
catch (err) {
|
|
890
1018
|
return `${cabeza}\nBut the brief could not be read yet: ${fallo(err)}\nCalling panal_take_board_job again re-reads it without taking twice.`;
|
|
@@ -933,6 +1061,9 @@ export function herramientasDelTablon(d) {
|
|
|
933
1061
|
if (precio === null || precio === 0n)
|
|
934
1062
|
return 'price must be a positive number, like "0.8".';
|
|
935
1063
|
const segundos = typeof args.delivery_hours === 'number' && args.delivery_hours > 0 ? Math.round(args.delivery_hours * 3600) : 0;
|
|
1064
|
+
const fuera = fueraDeAudiencia(await audienciaDe(j), await miTipo());
|
|
1065
|
+
if (fuera)
|
|
1066
|
+
return `This wallet cannot bid on board job #${id}: ${fuera}.`;
|
|
936
1067
|
const permiso = await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
|
|
937
1068
|
if (permiso && !permiso.ok)
|
|
938
1069
|
return `This wallet cannot bid on board job #${id}: ${permiso.motivo}.`;
|
|
@@ -955,13 +1086,18 @@ export function herramientasDelTablon(d) {
|
|
|
955
1086
|
name: 'panal_deliver_board_job',
|
|
956
1087
|
description: 'As the agent of a board job, deliver the work. The text is left in the board\'s mailbox first, signed ' +
|
|
957
1088
|
'with its hash inside, and only then its hash is anchored in the escrow — so the client can always read ' +
|
|
958
|
-
'what was anchored. If the mailbox refuses it, nothing is anchored and it can be retried.
|
|
959
|
-
'
|
|
1089
|
+
'what was anchored. If the mailbox refuses it, nothing is anchored and it can be retried. With files, ' +
|
|
1090
|
+
'they are uploaded first and their hashes go inside the anchored text. Costs gas.',
|
|
960
1091
|
inputSchema: {
|
|
961
1092
|
type: 'object',
|
|
962
1093
|
properties: {
|
|
963
1094
|
job_id: { type: 'string', description: 'The board job number.' },
|
|
964
1095
|
result: { type: 'string', description: 'The finished work, exactly as the client will receive it.' },
|
|
1096
|
+
files: {
|
|
1097
|
+
type: 'array',
|
|
1098
|
+
items: { type: 'string' },
|
|
1099
|
+
description: `Optional: paths of files on this machine to deliver (at most ${MAX_ARCHIVOS_TABLON}, 25 MB each, 30 MB in total).`,
|
|
1100
|
+
},
|
|
965
1101
|
confirmed_by_user: { type: 'boolean', description: 'true only if the person approved delivering this.' },
|
|
966
1102
|
},
|
|
967
1103
|
required: ['job_id', 'result', 'confirmed_by_user'],
|
|
@@ -972,15 +1108,18 @@ export function herramientasDelTablon(d) {
|
|
|
972
1108
|
return no;
|
|
973
1109
|
// Tal cual, sin recortar: lo que se ancla es el hash de estos bytes.
|
|
974
1110
|
const resultado = typeof args.result === 'string' ? args.result : '';
|
|
975
|
-
|
|
1111
|
+
const archivos = archivosDeRutas(args.files);
|
|
1112
|
+
if (typeof archivos === 'string')
|
|
1113
|
+
return `Nothing was delivered: ${archivos}`;
|
|
1114
|
+
if (!resultado.trim() && archivos.length === 0)
|
|
976
1115
|
return 'result is empty: there is nothing to deliver.';
|
|
977
1116
|
const id = parseJobId(args.job_id);
|
|
978
1117
|
if (id === null)
|
|
979
1118
|
return 'job_id must be a board job number.';
|
|
980
1119
|
try {
|
|
981
|
-
const { txHash, resultHash } = await panal.deliverBoardJob(id, resultado);
|
|
1120
|
+
const { txHash, resultHash } = await panal.deliverBoardJob(id, resultado, archivos);
|
|
982
1121
|
d.log(`entregado del tablón #${id}`);
|
|
983
|
-
return (`Board job #${id} delivered.\n tx: ${tx(txHash)}\n result hash: ${resultHash}\n\n` +
|
|
1122
|
+
return (`Board job #${id} delivered${archivos.length ? ` with ${archivos.length} file(s)` : ''}.\n tx: ${tx(txHash)}\n result hash: ${resultHash}\n\n` +
|
|
984
1123
|
'The client can now read it and approve. Once approved, the payment is CREDITED in the escrow: ' +
|
|
985
1124
|
'collect it with panal_withdraw. If the client does nothing for 72 h, panal_close_board_job settles it.');
|
|
986
1125
|
}
|
|
@@ -989,6 +1128,51 @@ export function herramientasDelTablon(d) {
|
|
|
989
1128
|
}
|
|
990
1129
|
},
|
|
991
1130
|
},
|
|
1131
|
+
{
|
|
1132
|
+
name: 'panal_download_board_file',
|
|
1133
|
+
description: 'Download the files of a board job to this machine, each one checked byte by byte against the hash in ' +
|
|
1134
|
+
'the anchored text: from "job", the files the client sent with it (its client and its agent can); from ' +
|
|
1135
|
+
'"delivery", the files the agent delivered (its client can). Signs a read credential (no transaction, ' +
|
|
1136
|
+
'no money). Files land in MCP_DOWNLOAD_DIR.',
|
|
1137
|
+
inputSchema: {
|
|
1138
|
+
type: 'object',
|
|
1139
|
+
properties: {
|
|
1140
|
+
job_id: { type: 'string', description: 'The board job number.' },
|
|
1141
|
+
from: { type: 'string', description: '"job" (the attachments) or "delivery" (the delivered files).' },
|
|
1142
|
+
file: { type: 'string', description: 'Optional: the name of one file. Without it, all of them.' },
|
|
1143
|
+
},
|
|
1144
|
+
required: ['job_id', 'from'],
|
|
1145
|
+
},
|
|
1146
|
+
handler: async (args) => {
|
|
1147
|
+
const no = disponible();
|
|
1148
|
+
if (no)
|
|
1149
|
+
return no;
|
|
1150
|
+
if (!d.cuenta)
|
|
1151
|
+
return 'Downloading needs this server\'s wallet (MCP_PRIVATE_KEY) to sign: there is none.';
|
|
1152
|
+
const id = parseJobId(args.job_id);
|
|
1153
|
+
if (id === null)
|
|
1154
|
+
return 'job_id must be a board job number.';
|
|
1155
|
+
const de = texto(args.from)?.toLowerCase() === 'delivery' ? 'entrega' : texto(args.from)?.toLowerCase() === 'job' ? 'adjunto' : null;
|
|
1156
|
+
if (!de)
|
|
1157
|
+
return 'from is "job" or "delivery".';
|
|
1158
|
+
try {
|
|
1159
|
+
// El hash sale del texto anclado —encargo o entrega—, nunca del buzón.
|
|
1160
|
+
const anunciados = de === 'adjunto' ? parseAttachmentsManifest(await panal.readBoardJobBrief(id)) : parseFilesManifest(await panal.readBoardJobResult(id));
|
|
1161
|
+
const pedido = texto(args.file);
|
|
1162
|
+
const lista = pedido ? anunciados.filter((f) => f.name === pedido) : anunciados;
|
|
1163
|
+
if (anunciados.length === 0)
|
|
1164
|
+
return `Board job #${id} has no files ${de === 'adjunto' ? 'with the job' : 'in its delivery'}.`;
|
|
1165
|
+
if (lista.length === 0)
|
|
1166
|
+
return `"${pedido}" is not one of them: ${anunciados.map((f) => f.name).join(', ')}.`;
|
|
1167
|
+
const bytes = await panal.downloadBoardJobFiles(id, lista, de);
|
|
1168
|
+
const guardados = lista.map((f, i) => ` ${f.name} (${tamano(f.size)}) → ${d.guardarDescarga(f.name, bytes[i])}`);
|
|
1169
|
+
return `Downloaded and checked against the hash on chain:\n${guardados.join('\n')}`;
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
return `Could not download: ${fallo(err)}`;
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
992
1176
|
];
|
|
993
1177
|
return { lectura, escritura };
|
|
994
1178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Servidor MCP de Panal: busca y contrata agentes de IA on-chain desde Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=20"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@panal/sdk": "^0.
|
|
33
|
+
"@panal/sdk": "^0.25.0",
|
|
34
34
|
"viem": "^2.21.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|