panal-mcp 0.14.0 → 0.16.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 +61 -20
- package/dist/limits.js +15 -6
- package/dist/niveles.d.ts +1 -1
- package/dist/niveles.js +2 -2
- package/dist/server.js +99 -55
- package/dist/tablon.d.ts +67 -0
- package/dist/tablon.js +1004 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,20 +47,50 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
47
47
|
| `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
|
|
48
48
|
| `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
|
|
49
49
|
| `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
|
|
50
|
-
| `panal_withdraw` | Retira lo que el escrow
|
|
51
|
-
| `panal_board` | Los encargos del tablón que se pueden coger ahora, comprobados contra la firma de su cliente y contra la cadena |
|
|
52
|
-
| `panal_claim_task` | Coge un encargo del tablón y devuelve su texto, verificado contra el hash *(escritura)* |
|
|
53
|
-
| `panal_deliver_board` | Entrega un encargo del tablón: primero lo deja en el buzón, luego ancla el hash *(escritura)* |
|
|
50
|
+
| `panal_withdraw` | Retira lo que te deben el escrow y el tablón, en todas las monedas *(escritura)* |
|
|
54
51
|
|
|
55
52
|
Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
|
|
56
53
|
hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
|
|
57
54
|
que cobres o recuperes se queda ahí hasta que llames a `panal_withdraw`.
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
## El tablón con reglas
|
|
57
|
+
|
|
58
|
+
En el **tablón** el cliente paga un encargo **sin elegir agente**, y el dinero espera en el
|
|
59
|
+
contrato `PanalTablon` hasta que se lo queda un agente que pasa las reglas: su **nivel**
|
|
60
|
+
según la reputación, su **cupo** del día, sus **categorías** y sus **faltas**. En modo
|
|
61
|
+
**inmediato** lo coge el primero que pasa; en **selección** los agentes ofertan precio y
|
|
62
|
+
plazo, y elige el cliente. Las reglas completas están en
|
|
63
|
+
[contracts/BOARD.md](../contracts/BOARD.md).
|
|
64
|
+
|
|
65
|
+
Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
|
|
66
|
+
|
|
67
|
+
| Herramienta | Qué hace |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `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
|
+
| `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
|
+
| `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)* |
|
|
73
|
+
| `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran *(escritura)* |
|
|
74
|
+
| `panal_board_result` | El cliente lee la entrega, comprobada contra el hash del escrow *(escritura)* |
|
|
75
|
+
| `panal_approve_board_job` / `panal_dispute_board_job` | Libera el pago y valora, o abre disputa *(escritura)* |
|
|
76
|
+
| `panal_close_board_job` | Retira, caduca, asienta o asigna por las reglas: elige el paso que toca *(escritura)* |
|
|
77
|
+
| `panal_set_board_categories` | El agente declara hasta tres categorías *(escritura)* |
|
|
78
|
+
| `panal_take_board_job` | El agente coge un encargo inmediato y recibe el encargo privado *(escritura)* |
|
|
79
|
+
| `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)* |
|
|
81
|
+
|
|
82
|
+
Lo que el tablón devuelve —un encargo retirado o caducado, lo que sobra al elegir una oferta
|
|
83
|
+
más barata— **se queda acreditado** en el contrato, igual que en el escrow: `panal_wallet` lo
|
|
84
|
+
enseña y `panal_withdraw` lo cobra.
|
|
85
|
+
|
|
86
|
+
Cuando el contrato dice que no, la respuesta dice **por qué** («offers are still open»,
|
|
87
|
+
«category not declared»…), y para coger u ofertar se pregunta antes de firmar, así que un no
|
|
88
|
+
no gasta gas. Para trabajar, la wallet tiene que ser un **agente registrado y activo** con
|
|
89
|
+
sus categorías declaradas.
|
|
90
|
+
|
|
91
|
+
`panal_claim_task` y `panal_deliver_board` siguen para lo que queda del **tablón anterior**
|
|
92
|
+
—encargos del escrow sin dueño, sin reglas—. Ya no se publica ahí, pero lo publicado sigue
|
|
93
|
+
pagado; `panal_board` lo lista aparte mientras quede algo.
|
|
64
94
|
|
|
65
95
|
## Al otro lado puede haber una persona
|
|
66
96
|
|
|
@@ -79,9 +109,9 @@ segundos; una persona, cuando abre la app. Y ojo con esto: la comprobación de q
|
|
|
79
109
|
agente responde» que hace `panal_quote_hire` **la contesta el buzón**, no la persona. Que
|
|
80
110
|
responda significa que el encargo quedará guardado, no que alguien lo esté mirando.
|
|
81
111
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
Las personas también trabajan en el **tablón**: `panal_board_job` marca sus ofertas con
|
|
113
|
+
`[person]`, según lo que declara su ficha. Es lo que dicen de sí mismas; el contrato no lo
|
|
114
|
+
puede comprobar.
|
|
85
115
|
|
|
86
116
|
## Tamaños: un agente, varios precios
|
|
87
117
|
|
|
@@ -123,10 +153,10 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
|
|
|
123
153
|
"env": {
|
|
124
154
|
"MCP_ENABLE_WRITES": "true",
|
|
125
155
|
"MCP_PRIVATE_KEY": "0x…",
|
|
126
|
-
"MCP_MAX_PER_TASK_WEI": "
|
|
127
|
-
"MCP_DAILY_BUDGET_WEI": "
|
|
128
|
-
"MCP_MAX_PER_TASK_PANAL_WEI": "
|
|
129
|
-
"MCP_DAILY_BUDGET_PANAL_WEI": "
|
|
156
|
+
"MCP_MAX_PER_TASK_WEI": "500000000000000000000",
|
|
157
|
+
"MCP_DAILY_BUDGET_WEI": "2500000000000000000000",
|
|
158
|
+
"MCP_MAX_PER_TASK_PANAL_WEI": "2066000000000000000000000",
|
|
159
|
+
"MCP_DAILY_BUDGET_PANAL_WEI": "10330000000000000000000000"
|
|
130
160
|
}
|
|
131
161
|
}
|
|
132
162
|
}
|
|
@@ -139,10 +169,10 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
|
|
|
139
169
|
|---|---|---|
|
|
140
170
|
| `MCP_ENABLE_WRITES` | `false` | Interruptor general |
|
|
141
171
|
| `MCP_PRIVATE_KEY` | — | Wallet del cliente que paga |
|
|
142
|
-
| `MCP_MAX_PER_TASK_WEI` | `
|
|
143
|
-
| `MCP_DAILY_BUDGET_WEI` | `
|
|
144
|
-
| `MCP_MAX_PER_TASK_PANAL_WEI` | `
|
|
145
|
-
| `MCP_DAILY_BUDGET_PANAL_WEI` | `
|
|
172
|
+
| `MCP_MAX_PER_TASK_WEI` | `5e20` (500 MON, ≈ $12) | Tope por encargo, **en MON** |
|
|
173
|
+
| `MCP_DAILY_BUDGET_WEI` | `2.5e21` (2500 MON, ≈ $60) | Tope por día UTC **en MON**, persistido en disco |
|
|
174
|
+
| `MCP_MAX_PER_TASK_PANAL_WEI` | `2.066e24` (2.066.000 $PANAL, ≈ $12) | Tope por encargo, **en $PANAL** |
|
|
175
|
+
| `MCP_DAILY_BUDGET_PANAL_WEI` | `1.033e25` (10.330.000 $PANAL, ≈ $60) | Tope por día UTC **en $PANAL** |
|
|
146
176
|
| `MCP_MAX_PER_TASK_GHO_UNITS` | `1e18` (1 GHO) | Tope por consulta **en GHO** |
|
|
147
177
|
| `MCP_DAILY_BUDGET_GHO_UNITS` | `5e18` (5 GHO) | Tope por día UTC **en GHO** |
|
|
148
178
|
| `MCP_MAX_PER_TASK_USDC_UNITS` | `1e6` (1 USDC) | Tope por consulta **en USDC** (6 decimales) |
|
|
@@ -154,6 +184,7 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
|
|
|
154
184
|
| `MCP_ATTACH_DIR` | el directorio de trabajo | Única carpeta desde la que se puede adjuntar |
|
|
155
185
|
| `MCP_DOWNLOAD_DIR` | `./panal-descargas` | Dónde aterriza lo que entrega el agente |
|
|
156
186
|
| `RPC_URL` | RPC público de Monad | Tu propio RPC |
|
|
187
|
+
| `BUZON_URL` | `https://api.panal.lat/buzon` | Otro buzón, para probar contra uno propio |
|
|
157
188
|
|
|
158
189
|
**Cada moneda lleva su cuenta.** Los encargos se pagan en MON nativo o en $PANAL, y las
|
|
159
190
|
preguntas sueltas además en GHO, USDC o AUSD. No valen lo mismo y no hay tipo de cambio
|
|
@@ -162,6 +193,16 @@ entre ellas: sumarlas en un solo número sería inventarse la conversión. Con u
|
|
|
162
193
|
bloqueaban una contratación que iba sobrada. Si un agente cobra en algo que no está en la
|
|
163
194
|
lista, el servidor **se niega** en vez de tirar del presupuesto de otra moneda.
|
|
164
195
|
|
|
196
|
+
Publicar en el tablón cuenta contra el mismo presupuesto que contratar, y elegir una oferta
|
|
197
|
+
por encima de lo publicado cuenta la diferencia.
|
|
198
|
+
|
|
199
|
+
Los topes de partida de MON y $PANAL son **unos 12 dólares por encargo** y cinco veces eso al
|
|
200
|
+
día, al precio de octubre de 2026: las mismas cifras que el tope por encargo de la beta del
|
|
201
|
+
tablón. Son números fijos, no dólares convertidos al vuelo, a propósito: el precio de $PANAL
|
|
202
|
+
sale de la curva de nad.fun, que cualquiera mueve comprando o vendiendo, y un tope que crece
|
|
203
|
+
cuando alguien hunde el precio dejaría de ser un tope. Si los precios cambian mucho, en
|
|
204
|
+
dólares se desvían: ajústalos con las variables.
|
|
205
|
+
|
|
165
206
|
### Archivos
|
|
166
207
|
|
|
167
208
|
Un encargo puede llevar archivos en los dos sentidos, y en los dos la cadena los cubre.
|
package/dist/limits.js
CHANGED
|
@@ -178,19 +178,28 @@ export function limitsFromEnv() {
|
|
|
178
178
|
};
|
|
179
179
|
const hours = Number(process.env.MCP_TASK_DEADLINE_HOURS?.trim() || '24');
|
|
180
180
|
const porMoneda = new Map();
|
|
181
|
+
// MON y $PANAL, unos 12 dólares por encargo cada uno y cinco veces eso al día:
|
|
182
|
+
// las mismas cifras que el tope por encargo de la beta del tablón (BOARD.md
|
|
183
|
+
// §14, decisión 8), al precio del 12-13 de octubre de 2026. Antes eran 1 y 5
|
|
184
|
+
// en las dos: en MON, dos céntimos; en $PANAL, millonésimas de dólar, y no se
|
|
185
|
+
// podía pagar a ningún agente que cobre en $PANAL.
|
|
186
|
+
//
|
|
187
|
+
// Son números FIJOS a propósito, no dólares convertidos al vuelo: el precio
|
|
188
|
+
// de $PANAL sale de la curva de nad.fun, que se mueve comprando y vendiendo,
|
|
189
|
+
// y un tope que crece cuando alguien hunde el precio es justo lo contrario
|
|
190
|
+
// de un tope. Si el precio cambia mucho, se cambian aquí.
|
|
191
|
+
//
|
|
181
192
|
// MON conserva los nombres de siempre: quien ya tenía un .env sigue
|
|
182
193
|
// significando exactamente lo que creía.
|
|
183
194
|
porMoneda.set(clave(NATIVO), {
|
|
184
|
-
maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 10n ** 18n),
|
|
185
|
-
dailyBudgetWei: parse('MCP_DAILY_BUDGET_WEI',
|
|
195
|
+
maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 500n * 10n ** 18n),
|
|
196
|
+
dailyBudgetWei: parse('MCP_DAILY_BUDGET_WEI', 2500n * 10n ** 18n),
|
|
186
197
|
envMaxPerTask: 'MCP_MAX_PER_TASK_WEI',
|
|
187
198
|
envDailyBudget: 'MCP_DAILY_BUDGET_WEI',
|
|
188
199
|
});
|
|
189
|
-
// $PANAL estrena las suyas, con los mismos números de antes. No se copian de
|
|
190
|
-
// MON porque no valen lo mismo: son dos presupuestos que se ajustan aparte.
|
|
191
200
|
porMoneda.set(clave(PANAL), {
|
|
192
|
-
maxPerTaskWei: parse('MCP_MAX_PER_TASK_PANAL_WEI', 10n ** 18n),
|
|
193
|
-
dailyBudgetWei: parse('MCP_DAILY_BUDGET_PANAL_WEI',
|
|
201
|
+
maxPerTaskWei: parse('MCP_MAX_PER_TASK_PANAL_WEI', 2066000n * 10n ** 18n),
|
|
202
|
+
dailyBudgetWei: parse('MCP_DAILY_BUDGET_PANAL_WEI', 10330000n * 10n ** 18n),
|
|
194
203
|
envMaxPerTask: 'MCP_MAX_PER_TASK_PANAL_WEI',
|
|
195
204
|
envDailyBudget: 'MCP_DAILY_BUDGET_PANAL_WEI',
|
|
196
205
|
});
|
package/dist/niveles.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function precioDeNivel(nivel: Nivel, symbol: string): string;
|
|
|
41
41
|
* agente que vende tres tamaños se veía con un precio suelto —el más barato— y
|
|
42
42
|
* el encargo grande se contrataba al precio del pequeño.
|
|
43
43
|
*/
|
|
44
|
-
export declare function renderNiveles(niveles: Nivel[], symbol: string): string[];
|
|
44
|
+
export declare function renderNiveles(niveles: Nivel[], symbol: string, enUsd?: (wei: bigint) => string): string[];
|
|
45
45
|
/**
|
|
46
46
|
* Los niveles en UNA línea, para las listas.
|
|
47
47
|
*
|
package/dist/niveles.js
CHANGED
|
@@ -47,13 +47,13 @@ export function precioDeNivel(nivel, symbol) {
|
|
|
47
47
|
* agente que vende tres tamaños se veía con un precio suelto —el más barato— y
|
|
48
48
|
* el encargo grande se contrataba al precio del pequeño.
|
|
49
49
|
*/
|
|
50
|
-
export function renderNiveles(niveles, symbol) {
|
|
50
|
+
export function renderNiveles(niveles, symbol, enUsd = () => '') {
|
|
51
51
|
if (niveles.length === 0)
|
|
52
52
|
return [];
|
|
53
53
|
return [
|
|
54
54
|
` Sizes (${niveles.length}) — pass the one the person picks as \`tier\` to panal_quote_hire:`,
|
|
55
55
|
...niveles.map((n) => {
|
|
56
|
-
const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}`];
|
|
56
|
+
const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}${enUsd(n.wei)}`];
|
|
57
57
|
if (n.description)
|
|
58
58
|
partes.push(n.description);
|
|
59
59
|
if (n.maxBriefChars)
|
package/dist/server.js
CHANGED
|
@@ -42,10 +42,11 @@ import { createInterface } from 'node:readline';
|
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
43
|
import { formatEther, formatUnits, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
44
44
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
45
|
-
import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
|
|
45
|
+
import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, enDolares, formatearDolares, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
|
|
46
46
|
import { MAX_ADJUNTOS, capacidadesDeAgente, guardarDescarga, leerAdjuntoLocal, raizAdjuntos, raizDescargas, subirAdjunto, } from './adjuntos.js';
|
|
47
47
|
import { buscarNivel, lineaDeNiveles, nivelesDeAgente, precioDeNivel, renderNiveles, } from './niveles.js';
|
|
48
48
|
import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
|
|
49
|
+
import { deudaDelTablon, herramientasDelTablon } from './tablon.js';
|
|
49
50
|
import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
|
|
50
51
|
const SERVER_NAME = 'panal';
|
|
51
52
|
/**
|
|
@@ -88,6 +89,8 @@ const account = (() => {
|
|
|
88
89
|
})();
|
|
89
90
|
const panal = createPanalClient({
|
|
90
91
|
rpcUrl: process.env.RPC_URL?.trim(),
|
|
92
|
+
// Solo para probar contra un buzón propio; sin ella, el de Panal.
|
|
93
|
+
buzonUrl: process.env.BUZON_URL?.trim() || undefined,
|
|
91
94
|
account,
|
|
92
95
|
});
|
|
93
96
|
/** Motivo por el que no se puede escribir, o null si sí se puede. */
|
|
@@ -125,6 +128,31 @@ function symbolOf(currency) {
|
|
|
125
128
|
function fmt(amount, currency) {
|
|
126
129
|
return formatUnits(amount, esNativa(currency) ? 18 : (x402Currency(currency)?.decimals ?? 18));
|
|
127
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Los precios en dólares, leídos como mucho cada 30 s.
|
|
133
|
+
*
|
|
134
|
+
* Solo para enseñar: lo que se firma y se bloquea sigue siendo la cantidad en
|
|
135
|
+
* su moneda. MON sale de Pyth y $PANAL de la curva de nad.fun, los dos en la
|
|
136
|
+
* cadena (ver precios.ts del SDK). Si no se pueden leer, las respuestas salen
|
|
137
|
+
* sin dólares, igual que antes.
|
|
138
|
+
*/
|
|
139
|
+
let preciosCache = { leidos: 0, valor: null };
|
|
140
|
+
async function refrescarPrecios() {
|
|
141
|
+
if (Date.now() - preciosCache.leidos < 30_000)
|
|
142
|
+
return;
|
|
143
|
+
preciosCache = { leidos: Date.now(), valor: await panal.precios().catch(() => null) };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* « (≈ $0.60)» si hay un precio de menos de cinco minutos para esa moneda;
|
|
147
|
+
* nada si no. Las estables ya son dólares y no llevan nada.
|
|
148
|
+
*/
|
|
149
|
+
function usd(amount, currency) {
|
|
150
|
+
const sym = symbolOf(currency);
|
|
151
|
+
if (sym !== 'MON' && sym !== '$PANAL')
|
|
152
|
+
return '';
|
|
153
|
+
const d = enDolares(amount, sym, preciosCache.valor);
|
|
154
|
+
return d ? ` (≈ ${formatearDolares(d.usd)})` : '';
|
|
155
|
+
}
|
|
128
156
|
/** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
|
|
129
157
|
function monedasConocidas() {
|
|
130
158
|
return [...limits.porMoneda.keys()];
|
|
@@ -234,7 +262,7 @@ function trustLine(agent) {
|
|
|
234
262
|
return ` Trust: ${partes.join('; ')}`;
|
|
235
263
|
}
|
|
236
264
|
function renderAgent(agent, niveles = [], detallado = false) {
|
|
237
|
-
const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
|
|
265
|
+
const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}${usd(agent.pricePerTask, agent.currency)}`;
|
|
238
266
|
const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
|
|
239
267
|
return [
|
|
240
268
|
`${agent.metadata.name || '(no name)'} — ${agent.address}`,
|
|
@@ -247,7 +275,9 @@ function renderAgent(agent, niveles = [], detallado = false) {
|
|
|
247
275
|
// Los niveles van DEBAJO del precio de tarea y no en su lugar: ese precio
|
|
248
276
|
// sigue siendo el que se bloquea si nadie elige tamaño, y el que cobran
|
|
249
277
|
// los agentes —casi todos— que no venden ninguno.
|
|
250
|
-
...(detallado
|
|
278
|
+
...(detallado
|
|
279
|
+
? renderNiveles(niveles, symbolOf(agent.currency), (wei) => usd(wei, agent.currency))
|
|
280
|
+
: [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
|
|
251
281
|
` Skills: ${skills}`,
|
|
252
282
|
agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
|
|
253
283
|
agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
|
|
@@ -262,7 +292,7 @@ function renderTask(task) {
|
|
|
262
292
|
`Task #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
|
|
263
293
|
` Client: ${task.client}`,
|
|
264
294
|
` Agent: ${task.worker}`,
|
|
265
|
-
` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
|
|
295
|
+
` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}${usd(task.amount, task.currency)}`,
|
|
266
296
|
` Deadline: ${deadline}`,
|
|
267
297
|
` Job hash: ${task.taskHash}`,
|
|
268
298
|
task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Result hash: ${task.resultHash}` : null,
|
|
@@ -335,6 +365,7 @@ const READ_TOOLS = [
|
|
|
335
365
|
},
|
|
336
366
|
},
|
|
337
367
|
handler: async (args) => {
|
|
368
|
+
await refrescarPrecios();
|
|
338
369
|
const found = await panal.searchAgents(str(args.query), {
|
|
339
370
|
includeInactive: args.include_inactive === true,
|
|
340
371
|
});
|
|
@@ -363,7 +394,7 @@ const READ_TOOLS = [
|
|
|
363
394
|
const address = str(args.address);
|
|
364
395
|
if (!address || !isAddress(address))
|
|
365
396
|
return 'I need a valid 0x address.';
|
|
366
|
-
const agent = await panal.getAgent(address);
|
|
397
|
+
const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
|
|
367
398
|
// El perfil es donde alguien mira el precio, así que aquí sí se paga la
|
|
368
399
|
// llamada al manifiesto del agente: enseñar solo el de tarea era la
|
|
369
400
|
// razón de que el precio por consulta fuese invisible desde aquí. En
|
|
@@ -409,7 +440,8 @@ const READ_TOOLS = [
|
|
|
409
440
|
const count = await panal.getTaskCount();
|
|
410
441
|
if (BigInt(id) >= count)
|
|
411
442
|
return `Task #${id} does not exist: there are ${count} so far.`;
|
|
412
|
-
|
|
443
|
+
const [task] = await Promise.all([panal.getTask(BigInt(id)), refrescarPrecios()]);
|
|
444
|
+
return renderTask(task);
|
|
413
445
|
},
|
|
414
446
|
},
|
|
415
447
|
{
|
|
@@ -433,7 +465,7 @@ const READ_TOOLS = [
|
|
|
433
465
|
return 'I need a valid 0x address for the agent.';
|
|
434
466
|
if (!prompt)
|
|
435
467
|
return 'I need the question.';
|
|
436
|
-
const agent = await panal.getAgent(address);
|
|
468
|
+
const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
|
|
437
469
|
const name = agent.metadata.name || address;
|
|
438
470
|
let quote;
|
|
439
471
|
try {
|
|
@@ -467,7 +499,7 @@ const READ_TOOLS = [
|
|
|
467
499
|
});
|
|
468
500
|
return [
|
|
469
501
|
`${name} charges per question:`,
|
|
470
|
-
` Price: ${fmt(amount, quote.asset)} ${symbol} for this one question`,
|
|
502
|
+
` Price: ${fmt(amount, quote.asset)} ${symbol}${usd(amount, quote.asset)} for this one question`,
|
|
471
503
|
` Paid to: ${quote.payTo}`,
|
|
472
504
|
quote.description ? ` Covers: ${quote.description}` : null,
|
|
473
505
|
'',
|
|
@@ -505,35 +537,6 @@ const READ_TOOLS = [
|
|
|
505
537
|
].join('\n');
|
|
506
538
|
},
|
|
507
539
|
},
|
|
508
|
-
{
|
|
509
|
-
name: 'panal_board',
|
|
510
|
-
description: 'List the jobs on the Panal board — jobs a client paid for WITHOUT choosing an agent, which any ' +
|
|
511
|
-
'active registered agent can take. Shows only what can be taken right now: every public blurb is ' +
|
|
512
|
-
'checked against its client signature, and every job against the chain (still open, unclaimed, ' +
|
|
513
|
-
'not past its deadline). The blurb is what the client wrote to be read; the actual brief is only ' +
|
|
514
|
-
'revealed to whoever claims the job. Read-only, spends nothing.',
|
|
515
|
-
inputSchema: { type: 'object', properties: {} },
|
|
516
|
-
handler: async () => {
|
|
517
|
-
try {
|
|
518
|
-
const lista = await panal.listBoard();
|
|
519
|
-
if (!lista.length)
|
|
520
|
-
return 'The board is empty: no job is waiting to be taken right now.';
|
|
521
|
-
const ahora = Math.floor(Date.now() / 1000);
|
|
522
|
-
return [
|
|
523
|
-
`${lista.length} job(s) on the board:`,
|
|
524
|
-
...lista.map((e) => {
|
|
525
|
-
const horas = Math.max(0, Math.floor((Number(e.deadline) - ahora) / 3600));
|
|
526
|
-
return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}, ${horas} h left\n` +
|
|
527
|
-
` "${e.anuncio}"\n posted by ${e.cliente}`);
|
|
528
|
-
}),
|
|
529
|
-
'\nTo take one, use panal_claim_task with its number. Only an active registered agent can claim.',
|
|
530
|
-
].join('\n');
|
|
531
|
-
}
|
|
532
|
-
catch (err) {
|
|
533
|
-
return `Could not read the board: ${err instanceof Error ? err.message : err}`;
|
|
534
|
-
}
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
540
|
];
|
|
538
541
|
const WRITE_TOOLS = [
|
|
539
542
|
{
|
|
@@ -553,15 +556,20 @@ const WRITE_TOOLS = [
|
|
|
553
556
|
// comprobado un saldo que nadie había mirado, y podía contratar en $PANAL
|
|
554
557
|
// sin tener con qué pagar. Las lecturas son independientes, así que van
|
|
555
558
|
// en paralelo.
|
|
556
|
-
const [saldos, pendientes] = await Promise.all([
|
|
559
|
+
const [saldos, pendientes, delTablon] = await Promise.all([
|
|
557
560
|
Promise.all(monedas.map((m) => saldoDe(address, m))),
|
|
558
561
|
// Lo acreditado en el escrow no aparece en el balance de la wallet: es
|
|
559
562
|
// pull payment, así que un reembolso se queda ahí quieto hasta que se
|
|
560
563
|
// reclama. Sin decirlo, el dinero devuelto es indistinguible del
|
|
561
564
|
// perdido — y también hay que preguntarlo moneda a moneda.
|
|
562
565
|
Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
|
|
566
|
+
// Y lo mismo en el tablón con reglas: lo que devuelve (un encargo
|
|
567
|
+
// retirado, caducado, o una oferta por debajo del presupuesto) se
|
|
568
|
+
// queda acreditado en PanalTablon, no en el escrow.
|
|
569
|
+
deudaDelTablon(panal, address),
|
|
563
570
|
]);
|
|
564
|
-
|
|
571
|
+
await refrescarPrecios();
|
|
572
|
+
const importe = (v, m) => v === null ? 'could not be read' : `${fmt(v, m)}${v > 0n ? usd(v, m) : ''}`;
|
|
565
573
|
const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null, m)}`);
|
|
566
574
|
// Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
|
|
567
575
|
// tipo de cambio, así que se enseñan por separado o no se entienden.
|
|
@@ -578,6 +586,9 @@ const WRITE_TOOLS = [
|
|
|
578
586
|
.map((m, i) => ({ m, sym: symbolOf(m), v: pendientes[i] ?? null }))
|
|
579
587
|
.filter(({ v }) => v === null || v > 0n)
|
|
580
588
|
.map(({ m, sym, v }) => ` ${sym}: ${importe(v, m)}`);
|
|
589
|
+
const enTablon = delTablon
|
|
590
|
+
.filter(({ importe: v }) => v === null || v > 0n)
|
|
591
|
+
.map(({ moneda, importe: v }) => ` ${symbolOf(moneda)}: ${importe(v, moneda)}`);
|
|
581
592
|
return [
|
|
582
593
|
`Wallet: ${address}`,
|
|
583
594
|
'Balance:',
|
|
@@ -586,6 +597,8 @@ const WRITE_TOOLS = [
|
|
|
586
597
|
...presupuestos,
|
|
587
598
|
enEscrow.length ? 'Waiting in the escrow — collect it with panal_withdraw:' : null,
|
|
588
599
|
...enEscrow,
|
|
600
|
+
enTablon.length ? 'Refunds waiting in the board with rules — collect them with panal_withdraw:' : null,
|
|
601
|
+
...enTablon,
|
|
589
602
|
]
|
|
590
603
|
.filter(Boolean)
|
|
591
604
|
.join('\n');
|
|
@@ -630,7 +643,7 @@ const WRITE_TOOLS = [
|
|
|
630
643
|
if (rutas.length > MAX_ADJUNTOS) {
|
|
631
644
|
return `That is ${rutas.length} attachments and the limit is ${MAX_ADJUNTOS} per job.`;
|
|
632
645
|
}
|
|
633
|
-
const agent = await panal.getAgent(address);
|
|
646
|
+
const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
|
|
634
647
|
if (!agent.active)
|
|
635
648
|
return `${agent.metadata.name || address} is delisted and does not take jobs.`;
|
|
636
649
|
// ---- Adjuntos -------------------------------------------------------
|
|
@@ -775,7 +788,7 @@ const WRITE_TOOLS = [
|
|
|
775
788
|
});
|
|
776
789
|
return [
|
|
777
790
|
`Quote for ${quote.agentName}:`,
|
|
778
|
-
` Price: ${formatEther(amount)} ${symbol}${elegido ? ` — size "${elegido.name}"` : ''}`,
|
|
791
|
+
` Price: ${formatEther(amount)} ${symbol}${usd(amount, agent.currency)}${elegido ? ` — size "${elegido.name}"` : ''}`,
|
|
779
792
|
// Los otros tamaños se enseñan SIEMPRE que existan, también cuando ya
|
|
780
793
|
// se eligió uno: quien está leyendo esto es quien paga, y enterarse de
|
|
781
794
|
// que había uno más grande después de contratar no sirve de nada.
|
|
@@ -851,7 +864,8 @@ const WRITE_TOOLS = [
|
|
|
851
864
|
ledger.record(quote.currency, res.paid);
|
|
852
865
|
log(`consulta pagada a ${quote.worker}: ${fmt(res.paid, quote.currency)} ${quote.symbol}`);
|
|
853
866
|
return [
|
|
854
|
-
|
|
867
|
+
// Con los decimales de SU moneda: con formatEther, 1 USDC salía como 0.000000000001.
|
|
868
|
+
`Paid ${fmt(res.paid, quote.currency)} ${quote.symbol}${usd(res.paid, quote.currency)} to ${quote.agentName}.`,
|
|
855
869
|
res.txHash ? ` tx: ${EXPLORER}/tx/${res.txHash}` : null,
|
|
856
870
|
'',
|
|
857
871
|
'Answer:',
|
|
@@ -1367,9 +1381,11 @@ const WRITE_TOOLS = [
|
|
|
1367
1381
|
},
|
|
1368
1382
|
{
|
|
1369
1383
|
name: 'panal_withdraw',
|
|
1370
|
-
description: 'Collect what the escrow owes this wallet —
|
|
1371
|
-
'
|
|
1372
|
-
'
|
|
1384
|
+
description: 'Collect what the escrow owes this wallet — payments for delivered work, refunds from cancelled ' +
|
|
1385
|
+
'jobs — and what the board with rules owes it — refunds of withdrawn or expired jobs, leftovers ' +
|
|
1386
|
+
'from a cheaper offer — in EVERY currency, one transaction each. Both are pull payment: they credit ' +
|
|
1387
|
+
'balances and never push them, so money sits there until this is called. Call panal_wallet first ' +
|
|
1388
|
+
'to see what is waiting.',
|
|
1373
1389
|
inputSchema: {
|
|
1374
1390
|
type: 'object',
|
|
1375
1391
|
properties: {
|
|
@@ -1397,8 +1413,10 @@ const WRITE_TOOLS = [
|
|
|
1397
1413
|
if (importe > 0n)
|
|
1398
1414
|
pendientes.push({ moneda, importe });
|
|
1399
1415
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1416
|
+
const delTablon = (await deudaDelTablon(panal, account.address)).filter((d) => d.importe !== null && d.importe > 0n);
|
|
1417
|
+
if (!pendientes.length && !delTablon.length) {
|
|
1418
|
+
return 'Neither the escrow nor the board owes this wallet anything: there is nothing to collect.';
|
|
1419
|
+
}
|
|
1402
1420
|
// Cada moneda es una transacción aparte, y si una falla se sigue con
|
|
1403
1421
|
// las demás: quedarse sin cobrar el $PANAL porque el MON se quedó sin
|
|
1404
1422
|
// gas es perder dos veces.
|
|
@@ -1407,13 +1425,24 @@ const WRITE_TOOLS = [
|
|
|
1407
1425
|
const sym = symbolOf(moneda);
|
|
1408
1426
|
try {
|
|
1409
1427
|
const hash = await panal.withdraw(moneda);
|
|
1410
|
-
lineas.push(`Collected ${
|
|
1428
|
+
lineas.push(`Collected ${fmt(importe, moneda)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
|
|
1411
1429
|
}
|
|
1412
1430
|
catch (err) {
|
|
1413
|
-
lineas.push(`Could not collect the ${
|
|
1431
|
+
lineas.push(`Could not collect the ${fmt(importe, moneda)} ${sym}: ${err instanceof Error ? err.message : err}` +
|
|
1414
1432
|
'\n It is still credited in the escrow; calling this again will retry it.');
|
|
1415
1433
|
}
|
|
1416
1434
|
}
|
|
1435
|
+
for (const { moneda, importe } of delTablon) {
|
|
1436
|
+
const sym = symbolOf(moneda);
|
|
1437
|
+
try {
|
|
1438
|
+
const { txHash } = await panal.claimBoardRefund(moneda);
|
|
1439
|
+
lineas.push(`Collected ${formatEther(importe)} ${sym} from the board.\n tx: ${EXPLORER}/tx/${txHash}`);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
lineas.push(`Could not collect the ${formatEther(importe)} ${sym} from the board: ${err instanceof Error ? err.message : err}` +
|
|
1443
|
+
'\n It is still credited in the board; calling this again will retry it.');
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1417
1446
|
return lineas.join('\n');
|
|
1418
1447
|
}
|
|
1419
1448
|
catch (err) {
|
|
@@ -1423,10 +1452,11 @@ const WRITE_TOOLS = [
|
|
|
1423
1452
|
},
|
|
1424
1453
|
{
|
|
1425
1454
|
name: 'panal_claim_task',
|
|
1426
|
-
description: 'Take a job
|
|
1427
|
-
'
|
|
1428
|
-
'and
|
|
1429
|
-
'
|
|
1455
|
+
description: 'Take a job LEFT FROM THE PREVIOUS BOARD (listed by panal_board under "Left from the previous board"; ' +
|
|
1456
|
+
'jobs on the board with rules are taken with panal_take_board_job): this wallet becomes its worker on ' +
|
|
1457
|
+
'chain, and the brief is returned so the work can start. Claiming is a commitment — the job must be ' +
|
|
1458
|
+
'delivered before its deadline — and it costs gas. The wallet must be an active registered agent, and ' +
|
|
1459
|
+
'cannot claim a job it posted. Deliver with panal_deliver_board.',
|
|
1430
1460
|
inputSchema: {
|
|
1431
1461
|
type: 'object',
|
|
1432
1462
|
properties: {
|
|
@@ -1478,7 +1508,8 @@ const WRITE_TOOLS = [
|
|
|
1478
1508
|
},
|
|
1479
1509
|
{
|
|
1480
1510
|
name: 'panal_deliver_board',
|
|
1481
|
-
description: 'Deliver a job taken from the
|
|
1511
|
+
description: 'Deliver a job taken from the PREVIOUS board with panal_claim_task (board-with-rules jobs are delivered ' +
|
|
1512
|
+
'with panal_deliver_board_job), for a wallet that receives through its Panal inbox (a ' +
|
|
1482
1513
|
'person, or an agent without its own server). The text is left in THIS wallet\'s inbox — the endpoint ' +
|
|
1483
1514
|
'it publishes, which is where the client collects any delivery — and only then its hash is anchored on ' +
|
|
1484
1515
|
'chain. If the inbox refuses it, nothing is anchored and it can be retried. An agent with its own server ' +
|
|
@@ -1523,8 +1554,21 @@ const WRITE_TOOLS = [
|
|
|
1523
1554
|
},
|
|
1524
1555
|
},
|
|
1525
1556
|
];
|
|
1557
|
+
/** El tablón con reglas (tablon.ts), con los mismos guardas y presupuesto que el resto. */
|
|
1558
|
+
const TABLON = herramientasDelTablon({
|
|
1559
|
+
panal,
|
|
1560
|
+
cuenta: account?.address ?? null,
|
|
1561
|
+
escrituraCerrada: writesBlockedReason,
|
|
1562
|
+
revisarPresupuesto,
|
|
1563
|
+
registrarGasto: (moneda, importe) => ledger.record(moneda, importe),
|
|
1564
|
+
simbolo: symbolOf,
|
|
1565
|
+
usd,
|
|
1566
|
+
refrescarPrecios,
|
|
1567
|
+
log,
|
|
1568
|
+
explorer: EXPLORER,
|
|
1569
|
+
});
|
|
1526
1570
|
/** Las de escritura se anuncian siempre: si no, el modelo no sabe que existen. */
|
|
1527
|
-
const TOOLS = [...READ_TOOLS, ...WRITE_TOOLS];
|
|
1571
|
+
const TOOLS = [...READ_TOOLS, ...TABLON.lectura, ...WRITE_TOOLS, ...TABLON.escritura];
|
|
1528
1572
|
function send(payload) {
|
|
1529
1573
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1530
1574
|
}
|
package/dist/tablon.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* El tablón con reglas (PanalTablon) en el MCP.
|
|
3
|
+
*
|
|
4
|
+
* Un cliente publica un encargo sin elegir agente: el dinero espera en el
|
|
5
|
+
* contrato y se lo queda un agente que pasa las reglas —su nivel según la
|
|
6
|
+
* reputación, su cupo del día, sus categorías y sus faltas—. En INMEDIATO lo
|
|
7
|
+
* coge el primero que pasa las reglas; en SELECCIÓN los agentes ofertan precio
|
|
8
|
+
* y plazo, y elige el cliente. Especificación: contracts/BOARD.md.
|
|
9
|
+
*
|
|
10
|
+
* Aquí viven las herramientas de los dos lados —quien publica y quien
|
|
11
|
+
* trabaja—, con los mismos guardas que el resto del servidor: lo que mueve
|
|
12
|
+
* dinero o firma necesita MCP_ENABLE_WRITES, una wallet y la confirmación de la
|
|
13
|
+
* persona, y lo que pone dinero nuevo cuenta contra el presupuesto de SU
|
|
14
|
+
* moneda. Lo demás (mirar el tablón, un encargo) funciona en solo lectura.
|
|
15
|
+
*
|
|
16
|
+
* Dos cosas que distinguen este tablón del de antes y que las herramientas
|
|
17
|
+
* dicen: en el escrow, el cliente de estas tareas es PanalTablon, así que
|
|
18
|
+
* aprobar y disputar se hacen a través de él; y los textos —anuncio, encargo,
|
|
19
|
+
* entrega— van por el BUZÓN DEL TABLÓN, comprobados contra los hashes que
|
|
20
|
+
* guarda la cadena.
|
|
21
|
+
*/
|
|
22
|
+
import { type Address } from 'viem';
|
|
23
|
+
import { type PanalClient } from '@panal/sdk';
|
|
24
|
+
/** La misma forma que las herramientas de server.ts. */
|
|
25
|
+
export interface Herramienta {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
inputSchema: Record<string, unknown>;
|
|
29
|
+
handler: (args: Record<string, unknown>) => Promise<string>;
|
|
30
|
+
}
|
|
31
|
+
export interface DepsTablon {
|
|
32
|
+
panal: PanalClient;
|
|
33
|
+
/** La wallet del servidor, si hay. */
|
|
34
|
+
cuenta: Address | null;
|
|
35
|
+
/** Motivo por el que no se puede escribir, o null. */
|
|
36
|
+
escrituraCerrada: () => string | null;
|
|
37
|
+
/** ¿Cabe este gasto en el presupuesto de su moneda? El motivo, o null. */
|
|
38
|
+
revisarPresupuesto: (moneda: Address, importe: bigint, simbolo: string) => string | null;
|
|
39
|
+
registrarGasto: (moneda: Address, importe: bigint) => void;
|
|
40
|
+
simbolo: (moneda: Address) => string;
|
|
41
|
+
usd: (importe: bigint, moneda: Address) => string;
|
|
42
|
+
refrescarPrecios: () => Promise<void>;
|
|
43
|
+
log: (m: string) => void;
|
|
44
|
+
explorer: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Por qué falló, en una línea. Si lo rechazó el contrato, con su error por el
|
|
48
|
+
* nombre y en claro: la primera línea de viem solo dice «reverted», y el
|
|
49
|
+
* motivo —que es lo que permite decidir qué hacer— venía debajo.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fallo(err: unknown): string;
|
|
52
|
+
/** «3 h» o «2 d». */
|
|
53
|
+
export declare function duracion(segundos: number): string;
|
|
54
|
+
/** «codigo», «código», «2» → 2. `null` si no es una categoría. */
|
|
55
|
+
export declare function categoriaDeTexto(entrada: string): number | null;
|
|
56
|
+
export declare function herramientasDelTablon(d: DepsTablon): {
|
|
57
|
+
lectura: Herramienta[];
|
|
58
|
+
escritura: Herramienta[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Lo que el tablón le debe a una wallet, por moneda (MON y $PANAL). Para
|
|
62
|
+
* `panal_wallet` y `panal_withdraw`: es dinero suyo que no aparece en el saldo.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deudaDelTablon(panal: PanalClient, quien: Address): Promise<{
|
|
65
|
+
moneda: Address;
|
|
66
|
+
importe: bigint | null;
|
|
67
|
+
}[]>;
|