create-panal-agent 0.14.0 → 0.15.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/README.md CHANGED
@@ -16,6 +16,7 @@ mi-agente/
16
16
  │ ├── agent.ts ← lo único tuyo: qué hace tu agente
17
17
  │ ├── server.ts recibe encargos, entrega y sirve resultados
18
18
  │ └── register.ts te da de alta en el marketplace
19
+ ├── logo.svg tu cara en el mercado, ya dibujada
19
20
  ├── .env con la clave del agente ya creada
20
21
  └── .env.example
21
22
  ```
@@ -49,6 +50,18 @@ PUBLIC_URL=https://tu-dominio.com npm run register
49
50
 
50
51
  Abre `src/register.ts` antes de ese último comando: ahí pones tu descripción, tus skills y tu precio. Eso es tu escaparate.
51
52
 
53
+ ## Tu logo y tus enlaces
54
+
55
+ En el mercado sales entre desconocidos, y un repositorio que se puede abrir dice más de ti que cualquier frase que escribas sobre ti mismo. Todo esto es opcional, y el generador te lo pregunta al crear el proyecto:
56
+
57
+ ```bash
58
+ npx create-panal-agent mi-agente --github tu-usuario/mi-agente --x tu_cuenta
59
+ ```
60
+
61
+ Valen `--logo`, `--web`, `--github`, `--x` y `--telegram`. Lo que no pases se pregunta, y se salta con Enter; con `--no-input` no se pregunta nada. Después se cambia en `PERFIL.links`, dentro de `src/register.ts`, y se vuelve a ejecutar `npm run register`.
62
+
63
+ **El logo no hay que ponerlo.** El generador escribe un `logo.svg` con la inicial de tu agente, tu servidor lo sirve en `/logo` y el registro publica esa URL solo —si responde—. Para poner el tuyo, sobrescribe el archivo: vale `.svg`, `.png` o `.webp`, cuadrado y pequeño (se pinta a 56 px).
64
+
52
65
  ## Cómo funciona por dentro
53
66
 
54
67
  El cliente **bloquea el pago en un escrow antes** de que empieces a trabajar, así que no trabajas gratis. Tú entregas anclando el `keccak256` del resultado en la cadena; el texto se queda contigo y lo sirves por tu endpoint.
package/dist/i18n.js CHANGED
@@ -41,6 +41,7 @@ const en = {
41
41
  Options:
42
42
  --lang <code> Interface language: ${LANG_CODES.join(', ')}
43
43
  --no-input Never prompt. For CI and scripted setups.
44
+ --logo, --web, --github, --x, --telegram Your links. All optional.
44
45
  --help Show this help.
45
46
  --version Show the version.
46
47
 
@@ -67,6 +68,16 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
67
68
  warnLabel: 'Careful:',
68
69
  warnBody: 'the endpoint must be public https. Without it the client cannot send you\nthe brief or download the result, and the agent is decoration.',
69
70
  docs: 'Full guide: https://github.com/AgentHiv/Panal/tree/main/create-agent',
71
+ logoWritten: 'Logo: logo.svg, with your initial. Overwrite it with yours.',
72
+ brand: {
73
+ title: 'Your shop window. All optional — press Enter to skip.',
74
+ logo: 'Logo URL (Enter: publish the one your agent serves)',
75
+ web: 'Website',
76
+ github: 'GitHub (user or user/repo)',
77
+ x: 'X (username)',
78
+ telegram: 'Telegram (username)',
79
+ bad: 'I cannot use "{name}", leaving it empty.',
80
+ },
70
81
  env: {
71
82
  key: 'Private key of your agent\'s DEDICATED wallet.\nIt is the one that gets paid and signs deliveries. Do not use your personal\nwallet: this one lives on a server. It only needs a little MON for gas.',
72
83
  port: 'Server port. Many hosts inject it themselves.',
@@ -137,6 +148,7 @@ const es = {
137
148
  Opciones:
138
149
  --lang <código> Idioma de la interfaz: ${LANG_CODES.join(', ')}
139
150
  --no-input No preguntar nunca. Para CI y instalaciones automatizadas.
151
+ --logo, --web, --github, --x, --telegram Tus enlaces. Todos opcionales.
140
152
  --help Muestra esta ayuda.
141
153
  --version Muestra la versión.
142
154
 
@@ -163,6 +175,16 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
163
175
  warnLabel: 'Ojo:',
164
176
  warnBody: 'el endpoint tiene que ser https y público. Sin él el cliente no puede\nmandarte el encargo ni descargar su resultado, y el agente queda de adorno.',
165
177
  docs: 'Guía completa: https://github.com/AgentHiv/Panal/tree/main/create-agent',
178
+ logoWritten: 'Logo: logo.svg, con tu inicial. Sobrescríbelo con el tuyo.',
179
+ brand: {
180
+ title: 'Tu escaparate. Todo es opcional: pulsa Enter para saltar.',
181
+ logo: 'URL de tu logo (Enter: publica el que sirve tu agente)',
182
+ web: 'Tu web',
183
+ github: 'GitHub (usuario o usuario/repo)',
184
+ x: 'X (usuario)',
185
+ telegram: 'Telegram (usuario)',
186
+ bad: 'No puedo usar «{name}», lo dejo vacío.',
187
+ },
166
188
  env: {
167
189
  key: 'Clave privada de la wallet DEDICADA de tu agente.\nEs la que cobra y la que firma las entregas. No uses tu wallet personal:\nesta vive en un servidor. Solo necesita un poco de MON para el gas.',
168
190
  port: 'Puerto del servidor. Muchos hostings lo inyectan solos.',
@@ -234,6 +256,7 @@ const zh = {
234
256
  选项:
235
257
  --lang <代码> 界面语言: ${LANG_CODES.join(', ')}
236
258
  --no-input 从不提问。适用于 CI 和脚本化部署。
259
+ --logo, --web, --github, --x, --telegram 你的链接。全部可选。
237
260
  --help 显示此帮助。
238
261
  --version 显示版本号。
239
262
 
@@ -260,6 +283,16 @@ const zh = {
260
283
  warnLabel: '注意:',
261
284
  warnBody: '端点必须是公网 https。否则客户既无法把任务发给你,也无法下载结果,\n这个代理就只是摆设。',
262
285
  docs: '完整指南:https://github.com/AgentHiv/Panal/tree/main/create-agent',
286
+ logoWritten: 'Logo:logo.svg,用了你名字的首字母。换成你自己的即可。',
287
+ brand: {
288
+ title: '你的门面。全部可选——直接回车跳过。',
289
+ logo: 'Logo 链接(回车:使用你的代理自己提供的)',
290
+ web: '你的网站',
291
+ github: 'GitHub(用户名,或 用户名/仓库)',
292
+ x: 'X(用户名)',
293
+ telegram: 'Telegram(用户名)',
294
+ bad: '「{name}」用不了,留空。',
295
+ },
263
296
  env: {
264
297
  key: '你的代理【专用】钱包的私钥。\n它负责收款并为交付签名。不要使用你的个人钱包:这个私钥要放在服务器上。\n它只需要少量 MON 用于支付 gas。',
265
298
  port: '服务端口。很多托管平台会自动注入。',
@@ -326,6 +359,7 @@ const hi = {
326
359
  विकल्प:
327
360
  --lang <कोड> इंटरफ़ेस की भाषा: ${LANG_CODES.join(', ')}
328
361
  --no-input कभी न पूछें। CI और स्क्रिप्टेड सेटअप के लिए।
362
+ --logo, --web, --github, --x, --telegram आपके लिंक। सभी वैकल्पिक।
329
363
  --help यह मदद दिखाएँ।
330
364
  --version संस्करण दिखाएँ।
331
365
 
@@ -352,6 +386,16 @@ const hi = {
352
386
  warnLabel: 'ध्यान दें:',
353
387
  warnBody: 'एंडपॉइंट सार्वजनिक https होना चाहिए। इसके बिना क्लाइंट न आपको काम भेज सकता है\nन परिणाम डाउनलोड कर सकता है, और एजेंट बेकार रह जाता है।',
354
388
  docs: 'पूरी गाइड: https://github.com/AgentHiv/Panal/tree/main/create-agent',
389
+ logoWritten: 'लोगो: logo.svg, आपके नाम के पहले अक्षर के साथ। अपना डालने के लिए इसे बदल दें।',
390
+ brand: {
391
+ title: 'आपकी पहचान। सब वैकल्पिक — छोड़ने के लिए Enter दबाएँ।',
392
+ logo: 'लोगो का URL (Enter: आपका एजेंट अपना खुद देता है)',
393
+ web: 'आपकी वेबसाइट',
394
+ github: 'GitHub (उपयोगकर्ता या उपयोगकर्ता/रेपो)',
395
+ x: 'X (उपयोगकर्ता नाम)',
396
+ telegram: 'Telegram (उपयोगकर्ता नाम)',
397
+ bad: '"{name}" काम नहीं आएगा, इसे खाली छोड़ रहा हूँ।',
398
+ },
355
399
  env: {
356
400
  key: 'आपके एजेंट के लिए समर्पित (DEDICATED) वॉलेट की निजी कुंजी।\nयही भुगतान पाती है और डिलीवरी पर हस्ताक्षर करती है। अपना निजी वॉलेट न लगाएँ:\nयह कुंजी सर्वर पर रहती है। इसे गैस के लिए बस थोड़ा MON चाहिए।',
357
401
  port: 'सर्वर पोर्ट। कई होस्टिंग इसे खुद भेजते हैं।',
@@ -422,6 +466,7 @@ const ar = {
422
466
  الخيارات:
423
467
  --lang <رمز> لغة الواجهة: ${LANG_CODES.join(', ')}
424
468
  --no-input لا تسأل أبدًا. للتكامل المستمر والإعداد الآلي.
469
+ --logo, --web, --github, --x, --telegram روابطك. كلها اختيارية.
425
470
  --help إظهار هذه المساعدة.
426
471
  --version إظهار الإصدار.
427
472
 
@@ -448,6 +493,16 @@ const ar = {
448
493
  warnLabel: 'انتبه:',
449
494
  warnBody: 'يجب أن تكون نقطة النهاية https وعامة. بدونها لا يستطيع العميل إرسال الطلب\nإليك ولا تنزيل النتيجة، ويبقى الوكيل بلا فائدة.',
450
495
  docs: 'الدليل الكامل: https://github.com/AgentHiv/Panal/tree/main/create-agent',
496
+ logoWritten: 'الشعار: logo.svg بالحرف الأول من اسمك. استبدله بشعارك.',
497
+ brand: {
498
+ title: 'واجهتك. كل هذا اختياري — اضغط Enter للتخطي.',
499
+ logo: 'رابط شعارك (Enter: يُنشر الشعار الذي يقدّمه وكيلك)',
500
+ web: 'موقعك',
501
+ github: 'GitHub (المستخدم أو المستخدم/المستودع)',
502
+ x: 'X (اسم المستخدم)',
503
+ telegram: 'Telegram (اسم المستخدم)',
504
+ bad: 'لا يمكنني استخدام «{name}»، سأتركه فارغًا.',
505
+ },
451
506
  env: {
452
507
  key: 'المفتاح الخاص بمحفظة وكيلك المخصّصة.\nهي التي تتقاضى الأجر وتوقّع عمليات التسليم. لا تستخدم محفظتك الشخصية:\nهذا المفتاح يعيش على خادم. يكفيه قليل من MON لدفع الرسوم.',
453
508
  port: 'منفذ الخادم. كثير من الاستضافات تضبطه تلقائيًا.',
@@ -517,6 +572,7 @@ const fr = {
517
572
  Options:
518
573
  --lang <code> Langue de l'interface : ${LANG_CODES.join(', ')}
519
574
  --no-input Ne jamais poser de question. Pour la CI et les scripts.
575
+ --logo, --web, --github, --x, --telegram Tes liens. Tous facultatifs.
520
576
  --help Affiche cette aide.
521
577
  --version Affiche la version.
522
578
 
@@ -543,6 +599,16 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
543
599
  warnLabel: 'Attention :',
544
600
  warnBody: 'le point d\'entrée doit être en https public. Sans lui, le client ne peut ni\nvous envoyer la commande ni récupérer le résultat : l\'agent ne sert à rien.',
545
601
  docs: 'Guide complet : https://github.com/AgentHiv/Panal/tree/main/create-agent',
602
+ logoWritten: 'Logo : logo.svg, avec ton initiale. Remplace-le par le tien.',
603
+ brand: {
604
+ title: 'Ta vitrine. Tout est facultatif — Entrée pour passer.',
605
+ logo: 'URL de ton logo (Entrée : celui que sert ton agent)',
606
+ web: 'Ton site',
607
+ github: 'GitHub (utilisateur ou utilisateur/dépôt)',
608
+ x: 'X (nom d\'utilisateur)',
609
+ telegram: 'Telegram (nom d\'utilisateur)',
610
+ bad: 'Je ne peux pas utiliser « {name} », je le laisse vide.',
611
+ },
546
612
  env: {
547
613
  key: 'Clé privée du portefeuille DÉDIÉ de votre agent.\nC\'est elle qui encaisse et qui signe les livraisons. N\'utilisez pas votre\nportefeuille personnel : celle-ci vit sur un serveur. Un peu de MON suffit.',
548
614
  port: 'Port du serveur. Beaucoup d\'hébergeurs l\'injectent eux-mêmes.',
@@ -616,6 +682,7 @@ const pt = {
616
682
  Opções:
617
683
  --lang <código> Idioma da interface: ${LANG_CODES.join(', ')}
618
684
  --no-input Nunca perguntar. Para CI e instalações automatizadas.
685
+ --logo, --web, --github, --x, --telegram Os seus links. Todos opcionais.
619
686
  --help Mostra esta ajuda.
620
687
  --version Mostra a versão.
621
688
 
@@ -642,6 +709,16 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
642
709
  warnLabel: 'Atenção:',
643
710
  warnBody: 'o endpoint tem de ser https e público. Sem ele o cliente não consegue\nenviar-lhe o pedido nem descarregar o resultado, e o agente fica inútil.',
644
711
  docs: 'Guia completo: https://github.com/AgentHiv/Panal/tree/main/create-agent',
712
+ logoWritten: 'Logótipo: logo.svg, com a sua inicial. Substitua-o pelo seu.',
713
+ brand: {
714
+ title: 'A sua montra. Tudo opcional — Enter para saltar.',
715
+ logo: 'URL do seu logótipo (Enter: publica o que o seu agente serve)',
716
+ web: 'O seu site',
717
+ github: 'GitHub (utilizador ou utilizador/repo)',
718
+ x: 'X (utilizador)',
719
+ telegram: 'Telegram (utilizador)',
720
+ bad: 'Não posso usar «{name}», deixo-o vazio.',
721
+ },
645
722
  env: {
646
723
  key: 'Chave privada da carteira DEDICADA do seu agente.\nÉ ela que recebe e que assina as entregas. Não use a sua carteira pessoal:\nesta vive num servidor. Só precisa de um pouco de MON para o gas.',
647
724
  port: 'Porta do servidor. Muitos alojamentos injetam-na sozinhos.',
@@ -713,6 +790,7 @@ const ru = {
713
790
  Опции:
714
791
  --lang <код> Язык интерфейса: ${LANG_CODES.join(', ')}
715
792
  --no-input Никогда не спрашивать. Для CI и автоматической установки.
793
+ --logo, --web, --github, --x, --telegram Ваши ссылки. Все необязательные.
716
794
  --help Показать эту справку.
717
795
  --version Показать версию.
718
796
 
@@ -739,6 +817,16 @@ const ru = {
739
817
  warnLabel: 'Важно:',
740
818
  warnBody: 'эндпоинт должен быть публичным и по https. Без этого клиент не сможет ни\nотправить вам заказ, ни скачать результат, и агент останется декорацией.',
741
819
  docs: 'Полное руководство: https://github.com/AgentHiv/Panal/tree/main/create-agent',
820
+ logoWritten: 'Логотип: logo.svg с первой буквой имени. Замените его своим.',
821
+ brand: {
822
+ title: 'Ваша витрина. Всё необязательно — Enter, чтобы пропустить.',
823
+ logo: 'Ссылка на логотип (Enter: возьмём тот, что отдаёт ваш агент)',
824
+ web: 'Ваш сайт',
825
+ github: 'GitHub (пользователь или пользователь/репозиторий)',
826
+ x: 'X (имя пользователя)',
827
+ telegram: 'Telegram (имя пользователя)',
828
+ bad: 'Не могу использовать «{name}», оставляю пустым.',
829
+ },
742
830
  env: {
743
831
  key: 'Приватный ключ ВЫДЕЛЕННОГО кошелька вашего агента.\nИменно он получает оплату и подписывает сдачу работы. Не используйте личный\nкошелёк: этот ключ живёт на сервере. Ему нужно немного MON только на газ.',
744
832
  port: 'Порт сервера. Многие хостинги подставляют его сами.',
@@ -811,6 +899,7 @@ const bn = {
811
899
  বিকল্প:
812
900
  --lang <কোড> ইন্টারফেসের ভাষা: ${LANG_CODES.join(', ')}
813
901
  --no-input কখনও প্রশ্ন করবে না। CI ও স্ক্রিপ্টেড সেটআপের জন্য।
902
+ --logo, --web, --github, --x, --telegram আপনার লিংক। সবই ঐচ্ছিক।
814
903
  --help এই সাহায্য দেখায়।
815
904
  --version সংস্করণ দেখায়।
816
905
 
@@ -837,6 +926,16 @@ const bn = {
837
926
  warnLabel: 'খেয়াল রাখুন:',
838
927
  warnBody: 'এন্ডপয়েন্ট অবশ্যই সর্বজনীন https হতে হবে। নইলে ক্লায়েন্ট আপনাকে কাজ পাঠাতেও\nপারবে না, ফলাফল নামাতেও পারবে না — এজেন্ট অকেজো থেকে যাবে।',
839
928
  docs: 'সম্পূর্ণ গাইড: https://github.com/AgentHiv/Panal/tree/main/create-agent',
929
+ logoWritten: 'লোগো: logo.svg, আপনার নামের প্রথম অক্ষর দিয়ে। নিজেরটা দিয়ে বদলে নিন।',
930
+ brand: {
931
+ title: 'আপনার সাইনবোর্ড। সবই ঐচ্ছিক — বাদ দিতে Enter চাপুন।',
932
+ logo: 'লোগোর URL (Enter: আপনার এজেন্ট নিজেরটাই দেবে)',
933
+ web: 'আপনার ওয়েবসাইট',
934
+ github: 'GitHub (ইউজার বা ইউজার/রেপো)',
935
+ x: 'X (ইউজারনেম)',
936
+ telegram: 'Telegram (ইউজারনেম)',
937
+ bad: '"{name}" ব্যবহার করা গেল না, খালি রাখছি।',
938
+ },
840
939
  env: {
841
940
  key: 'আপনার এজেন্টের নিবেদিত (DEDICATED) ওয়ালেটের প্রাইভেট কী।\nএটিই অর্থ গ্রহণ করে এবং ডেলিভারিতে স্বাক্ষর করে। ব্যক্তিগত ওয়ালেট ব্যবহার\nকরবেন না: এটি সার্ভারে থাকে। গ্যাসের জন্য সামান্য MON হলেই চলে।',
842
941
  port: 'সার্ভারের পোর্ট। অনেক হোস্টিং নিজেই এটি দেয়।',
@@ -906,6 +1005,7 @@ const ur = {
906
1005
  اختیارات:
907
1006
  --lang <کوڈ> انٹرفیس کی زبان: ${LANG_CODES.join(', ')}
908
1007
  --no-input کبھی نہ پوچھیں۔ CI اور خودکار سیٹ اپ کے لیے۔
1008
+ --logo, --web, --github, --x, --telegram آپ کے لنکس۔ سب اختیاری۔
909
1009
  --help یہ مدد دکھائیں۔
910
1010
  --version ورژن دکھائیں۔
911
1011
 
@@ -932,6 +1032,16 @@ const ur = {
932
1032
  warnLabel: 'خیال رکھیں:',
933
1033
  warnBody: 'اینڈ پوائنٹ کا عوامی https ہونا ضروری ہے۔ اس کے بغیر کلائنٹ نہ آپ کو کام بھیج\nسکتا ہے نہ نتیجہ اتار سکتا ہے، اور ایجنٹ بےکار رہ جاتا ہے۔',
934
1034
  docs: 'مکمل رہنما: https://github.com/AgentHiv/Panal/tree/main/create-agent',
1035
+ logoWritten: 'لوگو: logo.svg، آپ کے نام کے پہلے حرف کے ساتھ۔ اسے اپنے لوگو سے بدل دیں۔',
1036
+ brand: {
1037
+ title: 'آپ کی پہچان۔ سب اختیاری — چھوڑنے کے لیے Enter دبائیں۔',
1038
+ logo: 'لوگو کا URL (Enter: آپ کا ایجنٹ اپنا خود دے گا)',
1039
+ web: 'آپ کی ویب سائٹ',
1040
+ github: 'GitHub (صارف یا صارف/ریپو)',
1041
+ x: 'X (صارف نام)',
1042
+ telegram: 'Telegram (صارف نام)',
1043
+ bad: '«{name}» استعمال نہیں ہو سکتا، خالی چھوڑ رہا ہوں۔',
1044
+ },
935
1045
  env: {
936
1046
  key: 'آپ کے ایجنٹ کے مخصوص (DEDICATED) والیٹ کی نجی کلید۔\nیہی ادائیگی وصول کرتی ہے اور ڈیلیوری پر دستخط کرتی ہے۔ اپنا ذاتی والیٹ نہ\nاستعمال کریں: یہ کلید سرور پر رہتی ہے۔ اسے گیس کے لیے تھوڑا سا MON چاہیے۔',
937
1047
  port: 'سرور کا پورٹ۔ بہت سی ہوسٹنگ خود ہی دے دیتی ہیں۔',
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import { dirname, join, resolve } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
24
24
  import { CATALOG, LANGS, fill, isLang, resolveLang } from './i18n.js';
25
+ import { logoSvg } from './logo.js';
25
26
  const HERE = dirname(fileURLToPath(import.meta.url));
26
27
  /** La plantilla se empaqueta junto al binario (ver `files` del package.json). */
27
28
  const TEMPLATE = resolve(HERE, '..', 'template');
@@ -49,12 +50,41 @@ function fail(msg) {
49
50
  console.error(`\n${msg}\n`);
50
51
  process.exit(1);
51
52
  }
53
+ /** Los enlaces del escaparate, en el mismo orden en que los pinta el mercado. */
54
+ const ENLACES = ['logo', 'web', 'github', 'x', 'telegram'];
55
+ const SIN_ENLACES = { logo: '', web: '', github: '', x: '', telegram: '' };
56
+ /**
57
+ * Lo que se puede escribir dentro de la plantilla, y por qué es tan estrecho.
58
+ *
59
+ * Este valor acaba DENTRO de un literal de TypeScript, en `src/register.ts`.
60
+ * Una comilla, una barra invertida o un salto de línea no dan un enlace mal
61
+ * puesto: dan un archivo que no compila, o —peor— uno que compila haciendo otra
62
+ * cosa. El alfabeto de aquí es el de una URL y el de un nombre de usuario, y
63
+ * nada más.
64
+ *
65
+ * NO valida el enlace. De eso se encarga el SDK al registrar, con reglas por
66
+ * red (`usuario`, `usuario/repo`, `+invitación`…). Aquí solo se comprueba que
67
+ * se pueda escribir sin romper nada, y el tope de 120 es el mismo que aguanta
68
+ * la ficha on-chain.
69
+ */
70
+ const ESCRIBIBLE = /^[A-Za-z0-9:/?#@!&()*+,;=._~%-]{1,120}$/;
71
+ function limpiaEnlace(crudo) {
72
+ const valor = crudo.trim();
73
+ return ESCRIBIBLE.test(valor) ? valor : '';
74
+ }
52
75
  /**
53
76
  * Se admite `--lang es` y `--lang=es`. No es capricho: la primera forma es la
54
77
  * que teclea una persona y la segunda la que escriben los scripts.
55
78
  */
56
79
  function parseArgs(argv) {
57
- const args = { name: null, lang: null, noInput: false, help: false, version: false };
80
+ const args = {
81
+ name: null,
82
+ lang: null,
83
+ noInput: false,
84
+ help: false,
85
+ version: false,
86
+ links: { ...SIN_ENLACES },
87
+ };
58
88
  for (let i = 0; i < argv.length; i++) {
59
89
  const a = argv[i];
60
90
  if (a === '--help' || a === '-h')
@@ -67,8 +97,17 @@ function parseArgs(argv) {
67
97
  args.lang = argv[++i] ?? '';
68
98
  else if (a.startsWith('--lang='))
69
99
  args.lang = a.slice('--lang='.length);
70
- else if (!a.startsWith('-') && args.name === null)
71
- args.name = a;
100
+ else {
101
+ // Los cinco enlaces, en las dos formas: `--x panal` y `--x=panal`.
102
+ const enlace = ENLACES.find((e) => a === `--${e}` || a.startsWith(`--${e}=`));
103
+ if (enlace) {
104
+ const crudo = a.includes('=') ? a.slice(a.indexOf('=') + 1) : (argv[++i] ?? '');
105
+ args.links[enlace] = limpiaEnlace(crudo);
106
+ }
107
+ else if (!a.startsWith('-') && args.name === null) {
108
+ args.name = a;
109
+ }
110
+ }
72
111
  }
73
112
  return args;
74
113
  }
@@ -108,8 +147,49 @@ async function askLang() {
108
147
  rl.close();
109
148
  }
110
149
  }
150
+ /**
151
+ * Las preguntas del escaparate: el logo y los enlaces del creador.
152
+ *
153
+ * Existen por una razón concreta: en el mercado un agente sale entre
154
+ * desconocidos, y un repositorio que se puede abrir dice más de él que
155
+ * cualquier frase que escriba sobre sí mismo. Antes esto solo se podía poner
156
+ * editando `src/register.ts` a mano, y un campo al que hay que ir no lo rellena
157
+ * nadie.
158
+ *
159
+ * TODAS SE SALTAN CON ENTER, y no se preguntan siquiera sin terminal
160
+ * interactiva ni con `--no-input`: quien monta esto en un Dockerfile no puede
161
+ * contestar, y una pregunta ahí cuelga el proceso para siempre.
162
+ *
163
+ * Lo que llega por bandera no se vuelve a preguntar.
164
+ */
165
+ async function askLinks(t, yaPuestos) {
166
+ const { createInterface } = await import('node:readline/promises');
167
+ const puestos = { ...yaPuestos };
168
+ console.log(`\n${c.bold(t.brand.title)}\n`);
169
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
170
+ try {
171
+ for (const clave of ENLACES) {
172
+ if (puestos[clave])
173
+ continue;
174
+ const dicho = (await rl.question(` ${t.brand[clave]}: `)).trim();
175
+ if (!dicho)
176
+ continue;
177
+ const limpio = limpiaEnlace(dicho);
178
+ // Se avisa y se sigue. Un enlace raro no puede costarle a nadie volver a
179
+ // empezar el alta: lo puede poner luego en `src/register.ts`.
180
+ if (!limpio)
181
+ console.log(c.yellow(` ${fill(t.brand.bad, { name: dicho })}`));
182
+ else
183
+ puestos[clave] = limpio;
184
+ }
185
+ }
186
+ finally {
187
+ rl.close();
188
+ }
189
+ return puestos;
190
+ }
111
191
  /** Copia la plantilla sustituyendo los marcadores. */
112
- function copyTemplate(dest, name, t) {
192
+ function copyTemplate(dest, name, links, t) {
113
193
  cpSync(TEMPLATE, dest, { recursive: true });
114
194
  for (const [from, to] of Object.entries(RENAMES)) {
115
195
  const src = join(dest, from);
@@ -117,6 +197,16 @@ function copyTemplate(dest, name, t) {
117
197
  fail(fill(t.errTemplateMissing, { name: from }));
118
198
  renameSync(src, join(dest, to));
119
199
  }
200
+ // Un enlace vacío deja `logo: ''` en el perfil, que es exactamente lo que
201
+ // había antes de que se pudieran pasar: no rellenar nada no cambia nada.
202
+ const marcadores = {
203
+ __NAME__: name,
204
+ __LINK_LOGO__: links.logo,
205
+ __LINK_WEB__: links.web,
206
+ __LINK_GITHUB__: links.github,
207
+ __LINK_X__: links.x,
208
+ __LINK_TELEGRAM__: links.telegram,
209
+ };
120
210
  const walk = (dir) => {
121
211
  for (const entry of readdirSync(dir)) {
122
212
  const full = join(dir, entry);
@@ -125,7 +215,10 @@ function copyTemplate(dest, name, t) {
125
215
  continue;
126
216
  }
127
217
  const before = readFileSync(full, 'utf8');
128
- const after = before.replaceAll('__NAME__', name);
218
+ let after = before;
219
+ for (const [marcador, valor] of Object.entries(marcadores)) {
220
+ after = after.replaceAll(marcador, valor);
221
+ }
129
222
  if (after !== before)
130
223
  writeFileSync(full, after, 'utf8');
131
224
  }
@@ -215,21 +308,31 @@ async function main() {
215
308
  fail(fill(t.errDirExists, { name }));
216
309
  if (!existsSync(TEMPLATE))
217
310
  fail(fill(t.errTemplateMissing, { name: TEMPLATE }));
218
- mkdirSync(dest, { recursive: true });
219
- copyTemplate(dest, name, t);
220
- const envExample = writeEnvExample(dest, t);
311
+ // El escaparate se pregunta AQUÍ: después de validar el nombre y la carpeta,
312
+ // para no hacer cinco preguntas y fallar luego, y antes de copiar nada,
313
+ // porque los enlaces se escriben dentro de la plantilla al copiarla.
314
+ const links = interactivo ? await askLinks(t, args.links) : args.links;
221
315
  // Wallet dedicada, generada aquí. La alternativa —"crea una wallet y pega la
222
316
  // clave"— es donde la gente acaba pegando la de su MetaMask personal, que es
223
317
  // exactamente lo que no debe vivir en un servidor.
224
318
  const privateKey = generatePrivateKey();
225
319
  const address = privateKeyToAccount(privateKey).address;
320
+ mkdirSync(dest, { recursive: true });
321
+ copyTemplate(dest, name, links, t);
322
+ const envExample = writeEnvExample(dest, t);
226
323
  writeFileSync(join(dest, '.env'), envExample.replace('AGENT_PRIVATE_KEY=', `AGENT_PRIVATE_KEY=${privateKey}`), 'utf8');
324
+ // Su cara desde el primer minuto. El servidor de la plantilla ya sirve este
325
+ // archivo en `/logo`, y el registro publica esa URL solo si no has puesto
326
+ // otra: un agente recién generado deja de salir igual que todos los demás sin
327
+ // que haya que hacer nada.
328
+ writeFileSync(join(dest, 'logo.svg'), logoSvg(name, address), 'utf8');
227
329
  // El README del proyecto, en su idioma. Es donde vive lo que no cabe en la
228
330
  // pantalla de alta: cómo se cobra, qué hace cada archivo y qué rompe agentes.
229
331
  writeFileSync(join(dest, 'README.md'), fill(t.readme, { name, address }), 'utf8');
230
332
  console.log(`\n${c.green('✓')} ${c.bold(fill(t.created, { name }))}\n`);
231
333
  console.log(` ${t.walletLabel} ${c.bold(address)}`);
232
- console.log(c.dim(` ${fill(t.walletNote, { name })}\n`));
334
+ console.log(c.dim(` ${fill(t.walletNote, { name })}`));
335
+ console.log(c.dim(` ${t.logoWritten}\n`));
233
336
  console.log(c.bold(`${t.stepsTitle}\n`));
234
337
  console.log(` ${c.bold('1.')} ${t.s1Title}`);
235
338
  console.log(c.dim(` ${fill(t.s1Install, { name })}`));
package/dist/logo.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * El logo con el que nace un agente.
3
+ *
4
+ * Un agente recién generado salía en el mercado con el hexágono gris que se
5
+ * dibuja a partir de su wallet, igual que todos los demás. No es que quedara
6
+ * feo: es que en una lista de agentes indistinguibles nadie recuerda cuál era
7
+ * el suyo, y el sitio donde eso importa es justo el escaparate.
8
+ *
9
+ * Así que se escribe un `logo.svg` en el proyecto. No hace falta hacer nada
10
+ * más: el servidor de la plantilla ya lo sirve en `/logo` y el registro lo
11
+ * publica solo. Y es un archivo normal en la carpeta, así que cambiarlo por el
12
+ * de verdad es sobrescribirlo.
13
+ *
14
+ * POR QUÉ UNA LETRA Y NO EL HEXÁGONO GENERADO. El avatar que ya pinta el
15
+ * mercado sale de la dirección, y copiarlo aquí daría exactamente la misma
16
+ * imagen: un logo que no se distingue del hueco que rellena no es un logo. La
17
+ * inicial del nombre distingue de un vistazo y funciona en cualquier alfabeto,
18
+ * que es más de lo que se puede decir de un icono elegido por nosotros.
19
+ */
20
+ /** La paleta de Panal, la misma que usa el avatar del mercado. */
21
+ const FONDOS = ['#E29A2E', '#836EF9', '#6E7B4E', '#1B1814'];
22
+ /** Sobre cualquiera de esos cuatro, este contraste vale. */
23
+ const TINTA = '#F2EFFA';
24
+ /** Hash estable (djb2), el mismo que el avatar del mercado. */
25
+ function semilla(texto) {
26
+ let h = 5381;
27
+ for (let i = 0; i < texto.length; i++)
28
+ h = ((h << 5) + h + texto.charCodeAt(i)) >>> 0;
29
+ return h;
30
+ }
31
+ /** Un hexágono de lado plano centrado en 48,48. */
32
+ function hexagono(r) {
33
+ const puntos = [];
34
+ for (let k = 0; k < 6; k++) {
35
+ const a = (Math.PI / 3) * k;
36
+ puntos.push(`${(48 + r * Math.cos(a)).toFixed(2)},${(48 + r * Math.sin(a)).toFixed(2)}`);
37
+ }
38
+ return puntos.join(' ');
39
+ }
40
+ /** `&`, `<` y `"` dentro de un nombre romperían el SVG. */
41
+ function escapar(texto) {
42
+ return texto
43
+ .replace(/&/g, '&amp;')
44
+ .replace(/</g, '&lt;')
45
+ .replace(/>/g, '&gt;')
46
+ .replace(/"/g, '&quot;');
47
+ }
48
+ /**
49
+ * La inicial del nombre.
50
+ *
51
+ * Se coge el primer PUNTO DE CÓDIGO, no el primer `char`: un nombre que empiece
52
+ * por un emoji o por un ideograma fuera del plano básico son dos `char`, y
53
+ * quedarse con el primero produce media letra —un carácter de reemplazo— que ni
54
+ * siquiera se puede pintar.
55
+ */
56
+ export function inicial(nombre) {
57
+ const primero = [...nombre.trim()][0] ?? '?';
58
+ return primero.toLocaleUpperCase();
59
+ }
60
+ /** El SVG del logo con el que arranca un agente, listo para escribir. */
61
+ export function logoSvg(nombre, direccion) {
62
+ const h = semilla(direccion || nombre);
63
+ const fondo = FONDOS[h % FONDOS.length];
64
+ const letra = escapar(inicial(nombre));
65
+ return [
66
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96"',
67
+ ` role="img" aria-label="${escapar(nombre)}">`,
68
+ `<title>${escapar(nombre)}</title>`,
69
+ `<polygon points="${hexagono(46)}" fill="${fondo}"/>`,
70
+ `<text x="48" y="49" text-anchor="middle" dominant-baseline="central"`,
71
+ ` font-family="system-ui, -apple-system, Segoe UI, Roboto, sans-serif"`,
72
+ ` font-size="44" font-weight="700" fill="${TINTA}">${letra}</text>`,
73
+ '</svg>',
74
+ '',
75
+ ].join('\n');
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -42,6 +42,6 @@
42
42
  "scripts": {
43
43
  "build": "tsc -p tsconfig.json",
44
44
  "typecheck": "tsc -p tsconfig.json --noEmit",
45
- "test": "tsx test/i18n.test.ts && tsx test/memoria.test.ts && tsx test/reintento.test.ts && tsx test/zip.test.ts && tsx test/adjuntos.test.ts && tsx test/subcontrata.test.ts && tsx test/salida.test.ts && tsx test/vigilante.test.ts && tsx test/scaffold.test.ts"
45
+ "test": "tsx test/i18n.test.ts && tsx test/logo.test.ts && tsx test/memoria.test.ts && tsx test/reintento.test.ts && tsx test/zip.test.ts && tsx test/adjuntos.test.ts && tsx test/subcontrata.test.ts && tsx test/salida.test.ts && tsx test/vigilante.test.ts && tsx test/scaffold.test.ts"
46
46
  }
47
47
  }
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.14.0",
13
+ "@panal/sdk": "^0.15.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -51,13 +51,19 @@ const PERFIL = {
51
51
  links: {
52
52
  // El logo sale en tu tarjeta, en el mercado y en la app. Https, cuadrado y
53
53
  // pequeño: se pinta a 56 px, no hace falta más.
54
- logo: '',
55
- web: '',
54
+ //
55
+ // DÉJALO VACÍO Y NO TIENES QUE HACER NADA: tu agente sirve el `logo.svg`
56
+ // que hay en esta carpeta, y al registrarte se comprueba que responde y se
57
+ // publica esa URL. El generador te escribió uno con la inicial de tu
58
+ // nombre; para poner el tuyo, sobrescribe el archivo (vale .svg, .png o
59
+ // .webp) y vuelve a ejecutar `npm run register`.
60
+ logo: '__LINK_LOGO__',
61
+ web: '__LINK_WEB__',
56
62
  // Tu perfil o el repositorio del agente: valen `usuario` y `usuario/repo`.
57
- github: '',
63
+ github: '__LINK_GITHUB__',
58
64
  // Solo el usuario; también se traga el enlace entero si lo pegas.
59
- x: '',
60
- telegram: '',
65
+ x: '__LINK_X__',
66
+ telegram: '__LINK_TELEGRAM__',
61
67
  },
62
68
  };
63
69
 
@@ -172,6 +178,36 @@ async function compruebaEndpoint(botUrl: string, yo: string): Promise<string | n
172
178
  return null;
173
179
  }
174
180
 
181
+ /**
182
+ * Tu logo, si no lo has puesto a mano pero tu agente sirve uno.
183
+ *
184
+ * La plantilla te deja un `logo.svg` en la carpeta y el servidor lo publica en
185
+ * `/logo`. Que exista el archivo no basta para escribirlo en la cadena: lo que
186
+ * se guarda es una URL, y una URL que no responde es un hueco en la tarjeta que
187
+ * cuesta otra transacción arreglar. Así que se pide de verdad antes de creerlo.
188
+ *
189
+ * NUNCA lanza ni bloquea el registro. Un logo es un extra; quedarse sin
190
+ * registrar por una imagen sería absurdo.
191
+ */
192
+ async function logoQueSirves(botUrl: string): Promise<string> {
193
+ let url: string;
194
+ try {
195
+ url = new URL('/logo', botUrl).toString();
196
+ } catch {
197
+ return '';
198
+ }
199
+ try {
200
+ const res = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(8_000) });
201
+ const tipo = res.headers.get('content-type') ?? '';
202
+ // `image/` y no `200` a secas: un servidor delante que devuelva la página
203
+ // de error en HTML con estado 200 dejaría escrito en la cadena un logo que
204
+ // no es una imagen, y en la tarjeta un hueco sin explicación.
205
+ return res.ok && tipo.startsWith('image/') ? url : '';
206
+ } catch {
207
+ return '';
208
+ }
209
+ }
210
+
175
211
  async function main(): Promise<void> {
176
212
  const key = process.env.AGENT_PRIVATE_KEY?.trim();
177
213
  if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
@@ -215,23 +251,36 @@ async function main(): Promise<void> {
215
251
  process.exit(1);
216
252
  }
217
253
 
218
- console.log(`\nPerfil: ${formatAgentMetadata(PERFIL)}`);
219
- console.log(`Precio: ${formatEther(PRECIO)} ${MONEDA === NATIVE_CURRENCY ? 'MON' : '$PANAL'} por tarea\n`);
220
-
221
254
  // ¿Ya estabas? Registrarse dos veces revierte, así que se actualiza.
222
255
  const existente = await panal.getAgent(account.address).catch(() => null);
223
256
  const yaRegistrado = existente !== null && existente.registeredAt > 0n;
224
257
 
258
+ // Tu logo, si no lo pusiste a mano.
259
+ //
260
+ // Primero se mira si tu agente sirve uno; si no responde se conserva el que
261
+ // ya tuvieras publicado. Eso segundo importa más de lo que parece: sin ello,
262
+ // un corte de red de dos segundos durante un `npm run register` te borraría
263
+ // el logo de la ficha, y no lo dice nadie —la orden termina bien— hasta que
264
+ // alguien mira el mercado y ve el hexágono otra vez.
265
+ const puesto = PERFIL.links.logo.trim();
266
+ const servido = puesto ? '' : await logoQueSirves(PERFIL.botUrl);
267
+ const logo = puesto || servido || existente?.metadata.links.logo || '';
268
+ const perfil = { ...PERFIL, links: { ...PERFIL.links, logo } };
269
+ if (!puesto && logo) console.log(`Logo: ${logo}${servido ? '' : ' (el que ya tenías: tu /logo no responde)'}`);
270
+
271
+ console.log(`\nPerfil: ${formatAgentMetadata(perfil)}`);
272
+ console.log(`Precio: ${formatEther(PRECIO)} ${MONEDA === NATIVE_CURRENCY ? 'MON' : '$PANAL'} por tarea\n`);
273
+
225
274
  if (yaRegistrado) {
226
275
  console.log('Ya estabas registrado: actualizo el perfil y el precio.');
227
- await panal.updateMetadata(PERFIL);
276
+ await panal.updateMetadata(perfil);
228
277
  await panal.updatePrice(PRECIO, MONEDA);
229
278
  if (!existente.active) {
230
279
  await panal.setActive(true);
231
280
  console.log('Y te vuelvo a poner activo.');
232
281
  }
233
282
  } else {
234
- await panal.registerAgent({ metadata: PERFIL, pricePerTask: PRECIO, currency: MONEDA });
283
+ await panal.registerAgent({ metadata: perfil, pricePerTask: PRECIO, currency: MONEDA });
235
284
  console.log('Registrado.');
236
285
  }
237
286
 
@@ -23,7 +23,8 @@
23
23
  import 'dotenv/config';
24
24
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
25
25
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
26
- import { join } from 'node:path';
26
+ import { dirname, join } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
27
28
  import {
28
29
  MAX_FILE_BYTES,
29
30
  appendFilesManifest,
@@ -103,6 +104,21 @@ if (NIVELES.length > 0 && NIVELES_OK.length !== NIVELES.length) {
103
104
  /** El nivel más barato: por debajo de eso, un agente con niveles no trabaja. */
104
105
  const NIVEL_MINIMO = NIVELES_OK[0] ?? null;
105
106
 
107
+ // Se dicen al arrancar. Antes sólo salían dentro del bloque de subcontratación
108
+ // —que no se imprime si no hay presupuesto—, así que un agente con niveles y
109
+ // sin subcontratar arrancaba sin decir una palabra de lo que vende, y la única
110
+ // forma de saber si habían entrado era pedirle la tarjeta.
111
+ if (NIVELES_OK.length > 0) {
112
+ console.log(`Niveles (${NIVELES_OK.length}):`);
113
+ for (const n of NIVELES_OK) {
114
+ const topes = [
115
+ n.maxBriefChars === null ? null : `encargo ${n.maxBriefChars}`,
116
+ n.maxAttachCharsTotal === null ? null : `adjuntos ${n.maxAttachCharsTotal}`,
117
+ ].filter(Boolean);
118
+ console.log(` ${n.name ?? '(sin nombre)'} · ${n.wei} · ${topes.length ? topes.join(', ') : 'topes de siempre'}`);
119
+ }
120
+ }
121
+
106
122
  // Un nivel que se gasta subcontratando lo mismo que cobra trabaja gratis, y si
107
123
  // se pasa, trabaja pagando. No se corrige solo —es una decisión del autor— pero
108
124
  // no puede quedarse callado.
@@ -814,6 +830,60 @@ $('#enviar').onclick = async function(){
814
830
  };
815
831
  </script></body></html>`;
816
832
 
833
+ /**
834
+ * Las rutas por las que se pide tu logo y los archivos donde se busca.
835
+ *
836
+ * El orden importa: si tienes un `logo.png` y un `logo.svg`, gana el SVG, que
837
+ * es el que escribe el generador y el que escala a cualquier tamaño.
838
+ */
839
+ const RUTA_LOGO = /^\/logo(\.(svg|png|webp|jpe?g|gif))?$/;
840
+
841
+ const LOGOS: [string, string][] = [
842
+ ['logo.svg', 'image/svg+xml; charset=utf-8'],
843
+ ['logo.png', 'image/png'],
844
+ ['logo.webp', 'image/webp'],
845
+ ['logo.jpg', 'image/jpeg'],
846
+ ['logo.jpeg', 'image/jpeg'],
847
+ ['logo.gif', 'image/gif'],
848
+ ];
849
+
850
+ /**
851
+ * La carpeta de tu proyecto: este archivo vive en `src/`, así que se sube uno.
852
+ *
853
+ * Se calcula desde el módulo y no desde el directorio de trabajo porque no son
854
+ * lo mismo cuando alguien arranca el agente sin pasar por `npm start`: un
855
+ * `systemd` sin `WorkingDirectory=`, o un Docker con otro `WORKDIR`. Y ese caso
856
+ * NO se cae con estruendo —la clave puede venir de una variable de entorno de
857
+ * verdad en vez del `.env`— así que el agente trabaja igual y lo único que pasa
858
+ * es que su logo devuelve 404 y no llega a publicarse. En silencio.
859
+ */
860
+ const RAIZ = join(dirname(fileURLToPath(import.meta.url)), '..');
861
+
862
+ /**
863
+ * El primer logo que exista, o null si no publicas ninguno.
864
+ *
865
+ * Se mira primero junto al proyecto y después en el directorio de trabajo. El
866
+ * respaldo se queda a propósito: si alguien coloca ahí su logo —que es lo que
867
+ * hacía falta hasta ahora— sigue funcionando igual.
868
+ *
869
+ * Se lee en cada petición y no se cachea en memoria a propósito: cambiar de
870
+ * logo es dejar caer un archivo, y tener que reiniciar el agente —cortando los
871
+ * encargos en curso— para cambiar una imagen sería un precio absurdo. Los
872
+ * clientes ya lo cachean una hora por la cabecera.
873
+ */
874
+ function buscaLogo(): { bytes: Buffer; tipo: string } | null {
875
+ for (const carpeta of [RAIZ, '.']) {
876
+ for (const [archivo, tipo] of LOGOS) {
877
+ try {
878
+ return { bytes: readFileSync(join(carpeta, archivo)), tipo };
879
+ } catch {
880
+ // No está: se prueba el siguiente formato.
881
+ }
882
+ }
883
+ }
884
+ return null;
885
+ }
886
+
817
887
  function json(res: ServerResponse, status: number, body: unknown): void {
818
888
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
819
889
  res.end(JSON.stringify(body));
@@ -1034,6 +1104,44 @@ const server = createServer((req, res) => {
1034
1104
  return;
1035
1105
  }
1036
1106
 
1107
+ // ---- Tu logo, si lo pones ------------------------------------------------
1108
+ //
1109
+ // Un archivo `logo.svg` —o `logo.png`, o `logo.webp`— junto al package.json
1110
+ // y ya. El generador te deja uno escrito con la inicial de tu agente, para
1111
+ // que no salgas sin cara desde el primer minuto: sobrescríbelo con el tuyo
1112
+ // y no hay nada más que tocar.
1113
+ //
1114
+ // El registro guarda la URL, así que la imagen tiene que vivir en algún
1115
+ // sitio; y el sitio natural es el mismo dominio que ya sirves, porque es el
1116
+ // que la cadena ya dice que es tuyo.
1117
+ //
1118
+ // RESPONDE A `/logo` Y A CUALQUIER EXTENSIÓN, y sirve el archivo que de
1119
+ // verdad tengas, sea cual sea la que te pidan. Suena descuidado y no lo es:
1120
+ // los agentes que ya están registrados publicaron `…/logo.svg` y esa URL
1121
+ // está escrita en la cadena, así que cambiar de formato no puede obligarles
1122
+ // a pagar otra transacción. Lo que decide cómo se pinta una imagen es el
1123
+ // `content-type`, no la extensión de la URL.
1124
+ //
1125
+ // Se sirve con CORS abierto a propósito: es una imagen pública que va a
1126
+ // pintarse en escaparates ajenos, y sin la cabecera un `<canvas>` que la
1127
+ // toque para hacer una miniatura se queda a oscuras.
1128
+ if (RUTA_LOGO.test(url.pathname) && (req.method === 'GET' || req.method === 'HEAD')) {
1129
+ const logo = buscaLogo();
1130
+ if (!logo) {
1131
+ json(res, 404, { error: 'este agente no publica logo' });
1132
+ return;
1133
+ }
1134
+ res.writeHead(200, {
1135
+ 'content-type': logo.tipo,
1136
+ 'content-length': logo.bytes.byteLength,
1137
+ 'cache-control': 'public, max-age=3600',
1138
+ 'access-control-allow-origin': '*',
1139
+ 'x-content-type-options': 'nosniff',
1140
+ });
1141
+ res.end(req.method === 'HEAD' ? undefined : logo.bytes);
1142
+ return;
1143
+ }
1144
+
1037
1145
  // Tarjeta de presentación: quién eres y qué sabes hacer.
1038
1146
  //
1039
1147
  // Si cobras por llamada hay que ANUNCIARLO aquí. Durante meses el bot de