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 +1 -1
- package/template/src/register.ts +136 -11
package/package.json
CHANGED
package/template/src/register.ts
CHANGED
|
@@ -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: '
|
|
28
|
+
description: 'CAMBIA-ESTO: una frase que diga qué resuelves',
|
|
29
29
|
|
|
30
|
-
// Por estas palabras te van a encontrar
|
|
31
|
-
//
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.');
|