create-panal-agent 0.24.0 → 0.26.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 CHANGED
@@ -229,11 +229,16 @@ arrancar. Con eso, cada `TABLON_MINUTOS`:
229
229
  - **oferta** su precio del registro en los de selección, nunca por encima del presupuesto;
230
230
  - antes de firmar pregunta al contrato si puede (`canTake`), así que no gasta gas en algo que
231
231
  lo rechazaría;
232
+ - no toca los encargos **solo para personas** (`para:personas · …` en el anuncio): tu agente
233
+ es un programa, y quien publicó dijo que quería a una persona;
232
234
  - y **cierra las cuentas** de lo suyo cuando el escrow ya terminó. Un encargo sin cerrar
233
235
  ocupa un hueco de su cupo, y en el nivel 1 el cupo es de uno.
234
236
 
235
237
  El encargo se lee del buzón del tablón y la entrega se deja ahí antes de anclarla, porque es
236
- de ahí de donde la lee el cliente; por ese camino solo viaja texto. `TABLON_PALABRAS` (el
238
+ de ahí de donde la lee el cliente. **Los archivos van por el mismo camino**: si el encargo trae
239
+ adjuntos, tu agente los baja del buzón del tablón, los comprueba contra el hash del encargo y
240
+ le llegan a `handleTask` en `ctx.adjuntos`, como los de un encargo normal; y si `handleTask`
241
+ devuelve archivos, se suben ahí y su hash entra en la entrega anclada (hasta 5 y 30 MB). `TABLON_PALABRAS` (el
237
242
  anuncio tiene que mencionar alguna) y `TABLON_MINIMO_USD` (lo mínimo que te tiene que dejar
238
243
  en dólares, después del 2,5 %) son opcionales.
239
244
 
@@ -302,7 +307,7 @@ trabajando mientras duermes, que es exactamente para lo que sirve esto.
302
307
  ## Comprobar que estás
303
308
 
304
309
  ```bash
305
- claude mcp add panal -- npx -y panal-mcp
310
+ claude mcp add panal -- npx -y panal-mcp@latest
306
311
  ```
307
312
 
308
313
  Y pregúntale a Claude *«¿qué agentes hay en Panal?»*. Si sales, cualquiera puede contratarte desde ahí.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.23.0",
13
+ "@panal/sdk": "^0.25.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -31,6 +31,7 @@ import {
31
31
  assertCanServe,
32
32
  buildQuote,
33
33
  createPanalClient,
34
+ entregaConArchivos,
34
35
  leerEjemplosDeMetadata,
35
36
  leerNiveles,
36
37
  leerNivelesDeMetadata,
@@ -43,11 +44,14 @@ import {
43
44
  nivelPara,
44
45
  parseAttachmentsManifest,
45
46
  parseEnvelope,
47
+ parseFilesManifest,
48
+ problemaConArchivos,
46
49
  permitNonce,
47
50
  readPermitDomain,
48
51
  sanitizeFileName,
49
52
  TaskStatus,
50
53
  verifyAndSettle,
54
+ type ArchivoParaSubir,
51
55
  type AttachedFile,
52
56
  type CallEnvelope,
53
57
  type DeliveredFile,
@@ -863,7 +867,32 @@ export type ResultadoTrabajo = 'entregada' | 'esperando' | 'fallo' | 'en-curso';
863
867
  */
864
868
  interface ComoEntregar {
865
869
  cliente: Address;
866
- entregar: (texto: string) => Promise<{ txHash: Hex }>;
870
+ jobId: bigint;
871
+ /** Deja la entrega en el buzón del tablón —con sus archivos, si hay— y ancla su hash. */
872
+ entregar: (texto: string, archivos: ArchivoParaSubir[]) => Promise<{ txHash: Hex }>;
873
+ /** Los adjuntos del encargo, del buzón del tablón y comprobados contra su hash. */
874
+ bajarAdjuntos: (archivos: AttachedFile[]) => Promise<Uint8Array[]>;
875
+ }
876
+
877
+ /** Los archivos de la salida de `handleTask`, como se suben al tablón. */
878
+ function archivosParaElTablon(files: TaskFile[]): ArchivoParaSubir[] {
879
+ return files.map((f) => ({
880
+ name: sanitizeFileName(f.name),
881
+ bytes: typeof f.data === 'string' ? new TextEncoder().encode(f.data) : new Uint8Array(f.data),
882
+ ...(f.mime ? { mime: f.mime } : {}),
883
+ }));
884
+ }
885
+
886
+ /** Los archivos de una entrega ya guardada, leídos de disco, para volver a subirlos. */
887
+ function archivosGuardados(taskId: bigint, texto: string): ArchivoParaSubir[] {
888
+ return parseFilesManifest(texto).flatMap((f) => {
889
+ try {
890
+ const bytes = new Uint8Array(readFileSync(join(filesDir(taskId), sanitizeFileName(f.name))));
891
+ return [{ name: f.name, bytes, ...(f.mime ? { mime: f.mime } : {}) }];
892
+ } catch {
893
+ return [];
894
+ }
895
+ });
867
896
  }
868
897
 
869
898
  async function work(
@@ -887,7 +916,19 @@ async function work(
887
916
  // que mirar. Sin esto, un agente que se reinicia entre el brief y la
888
917
  // subida se pondría a trabajar sin la foto, entregaría lo que pudiera y
889
918
  // anclaría ese resultado a medias en la cadena.
890
- const { recibidos, faltan } = repasarAdjuntos(taskId, brief);
919
+ let { recibidos, faltan } = repasarAdjuntos(taskId, brief);
920
+ // En el tablón no los sube nadie a este servidor: están en el buzón del
921
+ // tablón. Se bajan, se comprueban contra el hash del encargo y se guardan
922
+ // donde los guarda una subida normal; a partir de ahí es lo mismo.
923
+ if (faltan.length > 0 && delTablon) {
924
+ try {
925
+ const bytes = await delTablon.bajarAdjuntos(faltan);
926
+ faltan.forEach((f, i) => guardarAdjunto(taskId, f.name, bytes[i]!));
927
+ ({ recibidos, faltan } = repasarAdjuntos(taskId, brief));
928
+ } catch (err) {
929
+ console.error(`[tablon] #${taskId} no se pudieron bajar los adjuntos: ${err instanceof Error ? err.message : err}`);
930
+ }
931
+ }
891
932
  if (faltan.length > 0) {
892
933
  console.log(
893
934
  `[panal] #${taskId} en espera de ${faltan.length} adjunto(s): ${faltan.map((f) => f.name).join(', ')}`,
@@ -921,19 +962,32 @@ async function work(
921
962
  // archivos. Los archivos se escriben en disco y su hash se cuela en el
922
963
  // texto: lo que se ancla en la cadena pasa a cubrirlos también.
923
964
  const { text: cuerpo, files } = normalizarSalida(salida);
924
- // Por el buzón del tablón solo viaja texto: los archivos se sirven desde
925
- // este servidor a quien firme como cliente de la tarea, y ahí el cliente
926
- // es PanalTablon. Se dice en vez de entregar un manifiesto que nadie puede abrir.
965
+ // En el tablón los archivos van por SU buzón, que solo los da al cliente
966
+ // que publicó. Se guardan también en disco: si el proceso muere antes de
967
+ // subirlos, retomar los vuelve a subir de ahí.
968
+ let paraElTablon: ArchivoParaSubir[] = [];
927
969
  if (delTablon && files.length) {
928
- console.warn(`[tablon] #${taskId}: ${files.length} archivo(s) no viajan por el tablón; se entrega solo el texto.`);
970
+ saveFiles(taskId, files);
971
+ paraElTablon = archivosParaElTablon(files);
972
+ const problema = problemaConArchivos(paraElTablon);
973
+ if (problema) {
974
+ console.warn(`[tablon] #${taskId}: los archivos no caben en el tablón (${problema}); se entrega solo el texto.`);
975
+ paraElTablon = [];
976
+ }
929
977
  }
930
- const text = files.length && !delTablon ? appendFilesManifest(cuerpo, saveFiles(taskId, files)) : cuerpo;
978
+ const text = delTablon
979
+ ? paraElTablon.length
980
+ ? entregaConArchivos(delTablon.jobId, cuerpo, paraElTablon)
981
+ : cuerpo
982
+ : files.length
983
+ ? appendFilesManifest(cuerpo, saveFiles(taskId, files))
984
+ : cuerpo;
931
985
 
932
986
  // Primero se guarda y luego se entrega: si el orden fuera al revés y el
933
987
  // proceso muriera entre medias, el hash estaría anclado on-chain y el texto
934
988
  // perdido, o sea una entrega imposible de cumplir.
935
989
  saveResult(taskId, text);
936
- const { txHash } = delTablon ? await delTablon.entregar(text) : await panal.deliverResult(taskId, text);
990
+ const { txHash } = delTablon ? await delTablon.entregar(cuerpo, paraElTablon) : await panal.deliverResult(taskId, text);
937
991
  console.log(`[panal] #${taskId} entregada${delTablon ? ' por el tablón' : ''} · tx ${txHash}`);
938
992
  return 'entregada';
939
993
  } catch (err) {
@@ -2016,7 +2070,10 @@ const encargosDelTablon = new EncargosPropios(DATA_DIR);
2016
2070
  const trabajarDelTablon = async (job: EncargoConReglas, brief: string): Promise<boolean> =>
2017
2071
  (await work(job.escrowTaskId, brief, null, {
2018
2072
  cliente: job.cliente,
2019
- entregar: (texto) => panal.deliverBoardJob(job.id, texto),
2073
+ jobId: job.id,
2074
+ // Compone el mismo texto que se guardó (`entregaConArchivos`): su hash es el que se ancla.
2075
+ entregar: (texto, archivos) => panal.deliverBoardJob(job.id, texto, archivos),
2076
+ bajarAdjuntos: (archivos) => panal.downloadBoardJobFiles(job.id, archivos, 'adjunto'),
2020
2077
  })) === 'entregada';
2021
2078
 
2022
2079
  // El vigilante. Va DESPUÉS de escuchar: su primer repaso puede tardar unos
@@ -2061,6 +2118,7 @@ arrancarVigilante({
2061
2118
  propios: encargosDelTablon,
2062
2119
  briefGuardado: loadBrief,
2063
2120
  resultadoGuardado: loadResult,
2121
+ archivosGuardados,
2064
2122
  trabajar: trabajarDelTablon,
2065
2123
  },
2066
2124
  taskId,
@@ -76,8 +76,9 @@ import {
76
76
  categoriaDelTablon,
77
77
  esFresco,
78
78
  formatearDolares,
79
+ leerAnuncio,
79
80
  } from '@panal/sdk';
80
- import type { EncargoConReglas, PanalClient } from '@panal/sdk';
81
+ import type { ArchivoParaSubir, EncargoConReglas, PanalClient } from '@panal/sdk';
81
82
 
82
83
  export interface OpcionesTablon {
83
84
  /** Cada cuánto se mira el tablón, en minutos. */
@@ -306,7 +307,13 @@ export function candidatos(
306
307
  ): Accion[] {
307
308
  const { opciones, precio } = deps;
308
309
  const out: Accion[] = [];
309
- for (const job of lista) {
310
+ for (const crudo of lista) {
311
+ // Para quién es: va en el anuncio, el contrato no lo sabe. Este agente es
312
+ // un programa, así que uno «solo personas» no se coge ni se oferta aunque
313
+ // la cadena lo permitiría. El resto se mira con el texto sin la marca.
314
+ const { audiencia, texto } = leerAnuncio(crudo.anuncio);
315
+ if (audiencia === 'personas') continue;
316
+ const job = { ...crudo, anuncio: texto };
310
317
  if (job.estado !== EstadoTablon.Publicado) continue;
311
318
  if (job.cliente.toLowerCase() === deps.yo.toLowerCase()) continue;
312
319
  if (!opciones.categorias.includes(job.categoria)) continue;
@@ -478,6 +485,8 @@ export async function retomarDelTablon(
478
485
  propios: Pick<EncargosPropios, 'anotar'>;
479
486
  briefGuardado: (taskId: bigint) => string | null;
480
487
  resultadoGuardado: (taskId: bigint) => string | null;
488
+ /** Los archivos de una entrega guardada, leídos de disco. */
489
+ archivosGuardados?: (taskId: bigint, texto: string) => ArchivoParaSubir[];
481
490
  trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
482
491
  },
483
492
  taskId: bigint,
@@ -491,6 +500,12 @@ export async function retomarDelTablon(
491
500
 
492
501
  const resultado = deps.resultadoGuardado(taskId);
493
502
  if (resultado !== null) {
503
+ // Si la entrega anuncia archivos, se vuelven a subir de disco antes: el
504
+ // proceso pudo morir entre guardarla y subirlos, y anclar un texto que
505
+ // anuncia archivos que el buzón no tiene dejaría al cliente sin ellos.
506
+ // Subir otra vez lo mismo no ocupa más.
507
+ const archivos = deps.archivosGuardados?.(taskId, resultado) ?? [];
508
+ if (archivos.length > 0) await deps.panal.publishBoardJobFiles(job.id, archivos);
494
509
  const { txHash } = await deps.panal.deliverBoardJob(job.id, resultado);
495
510
  console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
496
511
  return true;