panal-mcp 0.14.0 → 0.15.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/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,7 +42,7 @@ 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';
@@ -125,6 +125,31 @@ function symbolOf(currency) {
125
125
  function fmt(amount, currency) {
126
126
  return formatUnits(amount, esNativa(currency) ? 18 : (x402Currency(currency)?.decimals ?? 18));
127
127
  }
128
+ /**
129
+ * Los precios en dólares, leídos como mucho cada 30 s.
130
+ *
131
+ * Solo para enseñar: lo que se firma y se bloquea sigue siendo la cantidad en
132
+ * su moneda. MON sale de Pyth y $PANAL de la curva de nad.fun, los dos en la
133
+ * cadena (ver precios.ts del SDK). Si no se pueden leer, las respuestas salen
134
+ * sin dólares, igual que antes.
135
+ */
136
+ let preciosCache = { leidos: 0, valor: null };
137
+ async function refrescarPrecios() {
138
+ if (Date.now() - preciosCache.leidos < 30_000)
139
+ return;
140
+ preciosCache = { leidos: Date.now(), valor: await panal.precios().catch(() => null) };
141
+ }
142
+ /**
143
+ * « (≈ $0.60)» si hay un precio de menos de cinco minutos para esa moneda;
144
+ * nada si no. Las estables ya son dólares y no llevan nada.
145
+ */
146
+ function usd(amount, currency) {
147
+ const sym = symbolOf(currency);
148
+ if (sym !== 'MON' && sym !== '$PANAL')
149
+ return '';
150
+ const d = enDolares(amount, sym, preciosCache.valor);
151
+ return d ? ` (≈ ${formatearDolares(d.usd)})` : '';
152
+ }
128
153
  /** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
129
154
  function monedasConocidas() {
130
155
  return [...limits.porMoneda.keys()];
@@ -234,7 +259,7 @@ function trustLine(agent) {
234
259
  return ` Trust: ${partes.join('; ')}`;
235
260
  }
236
261
  function renderAgent(agent, niveles = [], detallado = false) {
237
- const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
262
+ const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}${usd(agent.pricePerTask, agent.currency)}`;
238
263
  const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
239
264
  return [
240
265
  `${agent.metadata.name || '(no name)'} — ${agent.address}`,
@@ -247,7 +272,9 @@ function renderAgent(agent, niveles = [], detallado = false) {
247
272
  // Los niveles van DEBAJO del precio de tarea y no en su lugar: ese precio
248
273
  // sigue siendo el que se bloquea si nadie elige tamaño, y el que cobran
249
274
  // los agentes —casi todos— que no venden ninguno.
250
- ...(detallado ? renderNiveles(niveles, symbolOf(agent.currency)) : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
275
+ ...(detallado
276
+ ? renderNiveles(niveles, symbolOf(agent.currency), (wei) => usd(wei, agent.currency))
277
+ : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
251
278
  ` Skills: ${skills}`,
252
279
  agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
253
280
  agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
@@ -262,7 +289,7 @@ function renderTask(task) {
262
289
  `Task #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
263
290
  ` Client: ${task.client}`,
264
291
  ` Agent: ${task.worker}`,
265
- ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
292
+ ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}${usd(task.amount, task.currency)}`,
266
293
  ` Deadline: ${deadline}`,
267
294
  ` Job hash: ${task.taskHash}`,
268
295
  task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Result hash: ${task.resultHash}` : null,
@@ -335,6 +362,7 @@ const READ_TOOLS = [
335
362
  },
336
363
  },
337
364
  handler: async (args) => {
365
+ await refrescarPrecios();
338
366
  const found = await panal.searchAgents(str(args.query), {
339
367
  includeInactive: args.include_inactive === true,
340
368
  });
@@ -363,7 +391,7 @@ const READ_TOOLS = [
363
391
  const address = str(args.address);
364
392
  if (!address || !isAddress(address))
365
393
  return 'I need a valid 0x address.';
366
- const agent = await panal.getAgent(address);
394
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
367
395
  // El perfil es donde alguien mira el precio, así que aquí sí se paga la
368
396
  // llamada al manifiesto del agente: enseñar solo el de tarea era la
369
397
  // razón de que el precio por consulta fuese invisible desde aquí. En
@@ -409,7 +437,8 @@ const READ_TOOLS = [
409
437
  const count = await panal.getTaskCount();
410
438
  if (BigInt(id) >= count)
411
439
  return `Task #${id} does not exist: there are ${count} so far.`;
412
- return renderTask(await panal.getTask(BigInt(id)));
440
+ const [task] = await Promise.all([panal.getTask(BigInt(id)), refrescarPrecios()]);
441
+ return renderTask(task);
413
442
  },
414
443
  },
415
444
  {
@@ -433,7 +462,7 @@ const READ_TOOLS = [
433
462
  return 'I need a valid 0x address for the agent.';
434
463
  if (!prompt)
435
464
  return 'I need the question.';
436
- const agent = await panal.getAgent(address);
465
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
437
466
  const name = agent.metadata.name || address;
438
467
  let quote;
439
468
  try {
@@ -467,7 +496,7 @@ const READ_TOOLS = [
467
496
  });
468
497
  return [
469
498
  `${name} charges per question:`,
470
- ` Price: ${fmt(amount, quote.asset)} ${symbol} for this one question`,
499
+ ` Price: ${fmt(amount, quote.asset)} ${symbol}${usd(amount, quote.asset)} for this one question`,
471
500
  ` Paid to: ${quote.payTo}`,
472
501
  quote.description ? ` Covers: ${quote.description}` : null,
473
502
  '',
@@ -515,7 +544,7 @@ const READ_TOOLS = [
515
544
  inputSchema: { type: 'object', properties: {} },
516
545
  handler: async () => {
517
546
  try {
518
- const lista = await panal.listBoard();
547
+ const [lista] = await Promise.all([panal.listBoard(), refrescarPrecios()]);
519
548
  if (!lista.length)
520
549
  return 'The board is empty: no job is waiting to be taken right now.';
521
550
  const ahora = Math.floor(Date.now() / 1000);
@@ -523,7 +552,7 @@ const READ_TOOLS = [
523
552
  `${lista.length} job(s) on the board:`,
524
553
  ...lista.map((e) => {
525
554
  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` +
555
+ return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}${usd(e.amount, e.currency)}, ${horas} h left\n` +
527
556
  ` "${e.anuncio}"\n posted by ${e.cliente}`);
528
557
  }),
529
558
  '\nTo take one, use panal_claim_task with its number. Only an active registered agent can claim.',
@@ -561,7 +590,8 @@ const WRITE_TOOLS = [
561
590
  // perdido — y también hay que preguntarlo moneda a moneda.
562
591
  Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
563
592
  ]);
564
- const importe = (v, m) => (v === null ? 'could not be read' : fmt(v, m));
593
+ await refrescarPrecios();
594
+ const importe = (v, m) => v === null ? 'could not be read' : `${fmt(v, m)}${v > 0n ? usd(v, m) : ''}`;
565
595
  const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null, m)}`);
566
596
  // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
567
597
  // tipo de cambio, así que se enseñan por separado o no se entienden.
@@ -630,7 +660,7 @@ const WRITE_TOOLS = [
630
660
  if (rutas.length > MAX_ADJUNTOS) {
631
661
  return `That is ${rutas.length} attachments and the limit is ${MAX_ADJUNTOS} per job.`;
632
662
  }
633
- const agent = await panal.getAgent(address);
663
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
634
664
  if (!agent.active)
635
665
  return `${agent.metadata.name || address} is delisted and does not take jobs.`;
636
666
  // ---- Adjuntos -------------------------------------------------------
@@ -775,7 +805,7 @@ const WRITE_TOOLS = [
775
805
  });
776
806
  return [
777
807
  `Quote for ${quote.agentName}:`,
778
- ` Price: ${formatEther(amount)} ${symbol}${elegido ? ` — size "${elegido.name}"` : ''}`,
808
+ ` Price: ${formatEther(amount)} ${symbol}${usd(amount, agent.currency)}${elegido ? ` — size "${elegido.name}"` : ''}`,
779
809
  // Los otros tamaños se enseñan SIEMPRE que existan, también cuando ya
780
810
  // se eligió uno: quien está leyendo esto es quien paga, y enterarse de
781
811
  // que había uno más grande después de contratar no sirve de nada.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.14.0",
3
+ "version": "0.15.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.20.0",
33
+ "@panal/sdk": "^0.21.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {