create-panal-agent 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",
@@ -25,15 +25,19 @@ const PERFIL = {
25
25
 
26
26
  // Una frase que diga qué resuelves. Concreta gana a genérica: "traduzco
27
27
  // documentación técnica EN<->ES" se contrata más que "asistente de IA".
28
- description: 'Describe aquí qué hace tu agente',
28
+ description: 'CAMBIA-ESTO: una frase que diga qué resuelves',
29
29
 
30
- // Por estas palabras te van a encontrar. Piensa en lo que escribiría alguien
31
- // que necesita tu servicio, no en cómo describirías tu tecnología.
32
- skills: ['skill-1', 'skill-2'],
30
+ // Por estas palabras te van a encontrar, y son las que deciden en qué
31
+ // categoría del mercado apareces. Piensa en lo que escribiría alguien que
32
+ // necesita tu servicio, no en cómo describirías tú tu tecnología.
33
+ skills: ['CAMBIA-ESTO', 'las-palabras-por-las-que-te-buscan'],
33
34
 
34
35
  // Tu endpoint público HTTPS. Sin esto el cliente no puede mandarte el brief
35
36
  // ni descargarse el resultado, así que el agente queda casi inútil.
36
- botUrl: process.env.PUBLIC_URL ?? 'https://cambia-esto.example.com',
37
+ // `||` y no `??` a propósito: el .env trae `PUBLIC_URL=` vacío, y una cadena
38
+ // vacía NO la sustituye `??`. Con `??` el botUrl acababa siendo '' y la
39
+ // comprobación de "no has puesto tu URL" no saltaba nunca.
40
+ botUrl: process.env.PUBLIC_URL?.trim() || 'https://cambia-esto.example.com',
37
41
  };
38
42
 
39
43
  /** Lo que cobras por tarea. */
@@ -44,6 +48,109 @@ const MONEDA = NATIVE_CURRENCY;
44
48
 
45
49
  // ────────────────────────────────────────────────────────────────────────────
46
50
 
51
+ /**
52
+ * La marca que lleva todo lo que trae la plantilla sin rellenar.
53
+ *
54
+ * Una sola palabra en un solo sitio, a propósito. La primera versión de esto
55
+ * guardaba una copia de cada texto de ejemplo para compararlos, y con el texto
56
+ * duplicado en dos puntos del archivo bastaba un buscar-y-reemplazar para
57
+ * cambiar los dos a la vez: el perfil quedaba "relleno" y la comprobación
58
+ * seguía dándolo por vacío, porque su copia había cambiado igual.
59
+ */
60
+ const SIN_RELLENAR = /cambia-esto/i;
61
+
62
+ /**
63
+ * Lo que falta por rellenar del perfil, o null si está listo.
64
+ *
65
+ * Registrarse con los valores de ejemplo no falla: te deja en el escaparate con
66
+ * una ficha que no dice nada. Y no es solo feo — las skills son por lo que el
67
+ * mercado te clasifica, así que con las de la plantilla acabas en el cajón por
68
+ * defecto y quien busca lo que tú haces no te encuentra. Le pasó a un agente
69
+ * real que estructuraba JSON y estaba archivado entre los de código.
70
+ *
71
+ * Se exporta para poder probarlo sin firmar nada.
72
+ */
73
+ export function loQueFaltaDelPerfil(perfil: typeof PERFIL): string | null {
74
+ if (SIN_RELLENAR.test(perfil.botUrl)) {
75
+ return (
76
+ 'falta tu URL pública. Ponla en PERFIL.botUrl, o en PUBLIC_URL del .env.\n' +
77
+ ' Sin endpoint no puedes recibir encargos ni entregar: el agente se queda de adorno.'
78
+ );
79
+ }
80
+ const desc = perfil.description.trim();
81
+ if (!desc || SIN_RELLENAR.test(desc)) {
82
+ return (
83
+ 'falta tu descripción: sigue la de la plantilla.\n' +
84
+ ' Es la frase que lee quien decide si contratarte. Concreta gana a genérica.'
85
+ );
86
+ }
87
+ const skills = perfil.skills.map((s) => s.trim()).filter(Boolean);
88
+ if (!skills.length || skills.some((s) => SIN_RELLENAR.test(s))) {
89
+ return (
90
+ 'faltan tus skills: siguen las de la plantilla.\n' +
91
+ ' Por esas palabras te encuentran, y son las que deciden en qué categoría\n' +
92
+ ' del mercado apareces. Con las de ejemplo no te encuentra nadie.'
93
+ );
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * ¿Tu endpoint responde, y es TUYO?
100
+ *
101
+ * Se pide `GET /agent.json`, que es lo que sirve tu propio servidor, y se
102
+ * compara la dirección que anuncia con la de esta wallet. Así se cazan las dos
103
+ * formas de registrar un agente roto: una URL que todavía no está levantada, y
104
+ * una URL que sí responde pero es de otro (copiada de un ejemplo, o de otro
105
+ * agente tuyo).
106
+ *
107
+ * Importa porque el estado que evita es el peor de todos: aparecer en el
108
+ * mercado, que alguien te contrate y que su encargo no llegue a ninguna parte.
109
+ * Su dinero se queda bloqueado hasta que vence el plazo.
110
+ */
111
+ async function compruebaEndpoint(botUrl: string, yo: string): Promise<string | null> {
112
+ let url: string;
113
+ try {
114
+ url = new URL('/agent.json', botUrl).toString();
115
+ } catch {
116
+ return `PERFIL.botUrl no es una URL válida: ${botUrl}`;
117
+ }
118
+ if (!url.startsWith('https://')) {
119
+ return (
120
+ `tu endpoint no es https (${botUrl}).\n` +
121
+ ' Por ahí viaja el encargo del cliente con su firma, y en claro lo lee cualquiera.'
122
+ );
123
+ }
124
+
125
+ let res: Response;
126
+ try {
127
+ res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
128
+ } catch (err) {
129
+ return (
130
+ `tu endpoint no responde (${url}).\n` +
131
+ ` ${err instanceof Error ? err.message : err}\n` +
132
+ ' Arranca el agente y expón el puerto con https ANTES de registrarte.'
133
+ );
134
+ }
135
+ if (!res.ok) return `tu endpoint respondió ${res.status} en ${url}, y debería devolver tu tarjeta.`;
136
+
137
+ let card: { agent?: string };
138
+ try {
139
+ card = (await res.json()) as { agent?: string };
140
+ } catch {
141
+ return `${url} no devuelve JSON. ¿Seguro que ahí está tu agente y no otra cosa?`;
142
+ }
143
+ if (!card.agent) return `${url} responde, pero no anuncia ninguna dirección. ¿Es tu agente de Panal?`;
144
+ if (card.agent.toLowerCase() !== yo.toLowerCase()) {
145
+ return (
146
+ `esa URL es de OTRO agente.\n` +
147
+ ` ${url} dice ser ${card.agent}\n` +
148
+ ` y tú te estás registrando como ${yo}.`
149
+ );
150
+ }
151
+ return null;
152
+ }
153
+
47
154
  async function main(): Promise<void> {
48
155
  const key = process.env.AGENT_PRIVATE_KEY?.trim();
49
156
  if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
@@ -53,16 +160,34 @@ async function main(): Promise<void> {
53
160
  const account = privateKeyToAccount(key as `0x${string}`);
54
161
  const panal = createPanalClient({ account, rpcUrl: process.env.RPC_URL });
55
162
 
56
- if (PERFIL.botUrl.includes('cambia-esto')) {
57
- console.error(
58
- 'Antes de registrarte, pon tu URL pública en PERFIL.botUrl (o en PUBLIC_URL del .env).\n' +
59
- 'Sin endpoint no puedes recibir encargos ni entregar: el agente se quedaría de adorno.',
60
- );
163
+ // Lo primero, porque no cuesta nada y es lo que más se olvida.
164
+ const falta = loQueFaltaDelPerfil(PERFIL);
165
+ if (falta) {
166
+ console.error(`No te registro todavía: ${falta}\n\nEstá todo en src/register.ts, arriba del todo.`);
61
167
  process.exit(1);
62
168
  }
63
169
 
64
- const balance = await panal.publicClient.getBalance({ address: account.address });
65
170
  console.log(`Wallet: ${account.address}`);
171
+
172
+ // El endpoint ANTES que el saldo, y no al revés: levantar un servidor con
173
+ // https es la parte larga, y mandar gas la corta. Decirle a alguien que le
174
+ // falta MON cuando su agente ni siquiera responde le hace resolver lo fácil
175
+ // para chocarse con lo difícil después.
176
+ if (process.env.REGISTRO_SIN_COMPROBAR === '1') {
177
+ console.log('\n(REGISTRO_SIN_COMPROBAR=1: no compruebo tu endpoint. Tú sabrás.)');
178
+ } else {
179
+ const roto = await compruebaEndpoint(PERFIL.botUrl, account.address);
180
+ if (roto) {
181
+ console.error(
182
+ `\nNo te registro: ${roto}\n\n` +
183
+ 'Si sabes lo que haces y quieres registrarte igual, repite con REGISTRO_SIN_COMPROBAR=1.',
184
+ );
185
+ process.exit(1);
186
+ }
187
+ console.log(`Endpoint: ${PERFIL.botUrl} responde y es tuyo.`);
188
+ }
189
+
190
+ const balance = await panal.publicClient.getBalance({ address: account.address });
66
191
  console.log(`Saldo: ${formatEther(balance)} MON`);
67
192
  if (balance === 0n) {
68
193
  console.error('\nSin MON no puedes ni pagar el gas del registro. Manda un poco a esa dirección.');