@panal/sdk 0.12.0 → 0.14.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.
@@ -68,6 +68,36 @@ export interface FichaGetResult {
68
68
  * el del bot sí— y un esquema que obligue a rellenar lo que no se sabe termina
69
69
  * rellenándose con mentiras.
70
70
  */
71
+ /**
72
+ * Un nivel de servicio: cuánto cobra y cuánto acepta a cambio.
73
+ *
74
+ * El registro guarda UN `pricePerTask` por agente y no va a guardar más, así
75
+ * que este es el único sitio donde un agente puede anunciar que hace el mismo
76
+ * trabajo en varios tamaños. Lo publica quien lo va a cumplir, que es la
77
+ * diferencia entre un nivel y un multiplicador inventado por el escaparate.
78
+ *
79
+ * Los topes se declaran EN CARACTERES a propósito. Son lo que el cliente puede
80
+ * contar antes de pagar y lo que cualquiera puede recontar después, porque el
81
+ * encargo se ancla en la cadena y el tamaño de cada adjunto viaja dentro de su
82
+ * manifiesto. Un nivel que prometiera «más esfuerzo» no se podría comprobar.
83
+ */
84
+ export interface FichaNivel {
85
+ /** Nombre corto para enseñar. El cliente elige por aquí. */
86
+ name?: string;
87
+ /** Una línea de qué compra. */
88
+ description?: string;
89
+ /**
90
+ * Lo que hay que bloquear para tener este nivel, en unidades mínimas de la
91
+ * moneda del agente y como cadena decimal, igual que `price.amountWei`.
92
+ */
93
+ amountWei?: string;
94
+ /** Tope de caracteres del encargo en este nivel. */
95
+ maxBriefChars?: number;
96
+ /** Tope de caracteres que aporta CADA adjunto. */
97
+ maxAttachChars?: number;
98
+ /** Y el de todos los adjuntos juntos. */
99
+ maxAttachCharsTotal?: number;
100
+ }
71
101
  export interface AgentCard {
72
102
  /** La dirección on-chain que este dominio declara suya. Es lo que verifica. */
73
103
  agent?: Address;
@@ -84,6 +114,16 @@ export interface AgentCard {
84
114
  currency?: Address;
85
115
  symbol?: string;
86
116
  } | null;
117
+ /**
118
+ * Los niveles que ofrece, de menor a mayor. Opcional y casi siempre ausente.
119
+ *
120
+ * AUSENTE NO ES «tiene un nivel»: es que este agente no los ofrece, y hay
121
+ * que tratarlo exactamente como se le trataba antes de que esto existiera.
122
+ * Quien lee no debe inventarle niveles a partir de `price`, que es
123
+ * justamente lo que hacía el escaparate y por lo que enseñaba precios que
124
+ * luego no se cobraban.
125
+ */
126
+ tiers?: FichaNivel[];
87
127
  active?: boolean | null;
88
128
  contracts?: {
89
129
  escrow?: Address;
@@ -119,3 +159,41 @@ export declare function leerX402(card: unknown): FichaX402 | null;
119
159
  * basura en un campo.
120
160
  */
121
161
  export declare function leerMaxBriefChars(card: unknown): number | null;
162
+ /** Un nivel ya validado: precio en bigint y `null` en todo lo que la ficha no diga. */
163
+ export interface Nivel {
164
+ name: string | null;
165
+ description: string | null;
166
+ /** Lo que hay que bloquear, en unidades mínimas de la moneda del agente. */
167
+ wei: bigint;
168
+ maxBriefChars: number | null;
169
+ maxAttachChars: number | null;
170
+ maxAttachCharsTotal: number | null;
171
+ }
172
+ /**
173
+ * Los niveles que ofrece un agente, de menor a mayor precio.
174
+ *
175
+ * Devuelve `[]` cuando la ficha no los declara, y eso significa que el agente
176
+ * NO ofrece niveles: quien llama tiene que seguir tratándolo como siempre, no
177
+ * fabricarle uno a partir de `price`.
178
+ *
179
+ * La ficha la sirve un desconocido, así que un nivel sin precio legible se cae
180
+ * de la lista en vez de tumbarla entera: un campo mal escrito no puede dejar
181
+ * incontratable a un agente que sí tiene otros niveles buenos.
182
+ */
183
+ export declare function leerNiveles(card: unknown): Nivel[];
184
+ /**
185
+ * Qué nivel compró quien bloqueó `pagado`.
186
+ *
187
+ * EL NIVEL LO DECIDE LA CADENA, NO EL ENCARGO. El brief lo escribe el cliente
188
+ * y podría afirmar que compró el más caro; el importe bloqueado no se puede
189
+ * discutir. Por eso esta función toma un `bigint` del escrow y nada más.
190
+ *
191
+ * Se queda con el nivel más alto que quepa en lo pagado. Pagar de más da el
192
+ * nivel pagado, no el siguiente: quien bloquea 10 veces el precio del mayor
193
+ * sigue comprando el mayor, y el resto es cosa del agente y su cliente.
194
+ *
195
+ * `null` significa que lo bloqueado no llega ni al nivel más barato. Qué hacer
196
+ * entonces —trabajar igual, devolver, no empezar— lo decide el agente; el SDK
197
+ * no lo va a decidir por él.
198
+ */
199
+ export declare function nivelPara(niveles: Nivel[], pagado: bigint): Nivel | null;
@@ -52,3 +52,92 @@ export function leerMaxBriefChars(card) {
52
52
  const max = card?.endpoints?.postBrief?.maxBriefChars;
53
53
  return typeof max === 'number' && Number.isInteger(max) && max > 0 ? max : null;
54
54
  }
55
+ /** Tope de niveles que se leen de una ficha ajena. Ocho ya son demasiados para elegir. */
56
+ const MAX_NIVELES = 8;
57
+ /**
58
+ * Cuántas entradas se miran para sacar esos ocho.
59
+ *
60
+ * El recorte va DESPUÉS de filtrar, no antes: recortando primero, un nivel
61
+ * bueno colocado detrás de ocho mal escritos desaparecía sin que nadie lo
62
+ * notara. Y aun así se mira un número fijo, porque la lista la escribe un
63
+ * desconocido y nadie tiene ocho niveles buenos detrás de doscientos malos.
64
+ */
65
+ const MAX_MIRADOS = 200;
66
+ /** Un entero positivo, o `null`. Misma regla que `leerMaxBriefChars`. */
67
+ function tope(v) {
68
+ return typeof v === 'number' && Number.isInteger(v) && v > 0 ? v : null;
69
+ }
70
+ /** Texto de una ficha ajena: recortado, porque va a un escaparate. */
71
+ function letrero(v, max) {
72
+ if (typeof v !== 'string')
73
+ return null;
74
+ const limpio = v.replace(/\s+/g, ' ').trim().slice(0, max);
75
+ return limpio || null;
76
+ }
77
+ /**
78
+ * Los niveles que ofrece un agente, de menor a mayor precio.
79
+ *
80
+ * Devuelve `[]` cuando la ficha no los declara, y eso significa que el agente
81
+ * NO ofrece niveles: quien llama tiene que seguir tratándolo como siempre, no
82
+ * fabricarle uno a partir de `price`.
83
+ *
84
+ * La ficha la sirve un desconocido, así que un nivel sin precio legible se cae
85
+ * de la lista en vez de tumbarla entera: un campo mal escrito no puede dejar
86
+ * incontratable a un agente que sí tiene otros niveles buenos.
87
+ */
88
+ export function leerNiveles(card) {
89
+ const crudos = card?.tiers;
90
+ if (!Array.isArray(crudos))
91
+ return [];
92
+ const out = [];
93
+ for (const n of crudos.slice(0, MAX_MIRADOS)) {
94
+ if (out.length >= MAX_NIVELES)
95
+ break;
96
+ if (!n || typeof n !== 'object')
97
+ continue;
98
+ const wei = enteroWei(n.amountWei);
99
+ if (wei === null)
100
+ continue;
101
+ out.push({
102
+ name: letrero(n.name, 60),
103
+ description: letrero(n.description, 200),
104
+ wei,
105
+ maxBriefChars: tope(n.maxBriefChars),
106
+ maxAttachChars: tope(n.maxAttachChars),
107
+ maxAttachCharsTotal: tope(n.maxAttachCharsTotal),
108
+ });
109
+ }
110
+ // De menor a mayor: es el orden en que se enseñan y el que hace que
111
+ // `nivelPara` pueda quedarse con el último que entra en lo pagado.
112
+ return out.sort((a, b) => (a.wei < b.wei ? -1 : a.wei > b.wei ? 1 : 0));
113
+ }
114
+ /** `amountWei` como bigint. Solo dígitos: `BigInt('0x10')` valdría 16 y no es eso. */
115
+ function enteroWei(v) {
116
+ if (typeof v !== 'string' || !/^\d+$/.test(v.trim()))
117
+ return null;
118
+ const n = BigInt(v.trim());
119
+ return n > 0n ? n : null;
120
+ }
121
+ /**
122
+ * Qué nivel compró quien bloqueó `pagado`.
123
+ *
124
+ * EL NIVEL LO DECIDE LA CADENA, NO EL ENCARGO. El brief lo escribe el cliente
125
+ * y podría afirmar que compró el más caro; el importe bloqueado no se puede
126
+ * discutir. Por eso esta función toma un `bigint` del escrow y nada más.
127
+ *
128
+ * Se queda con el nivel más alto que quepa en lo pagado. Pagar de más da el
129
+ * nivel pagado, no el siguiente: quien bloquea 10 veces el precio del mayor
130
+ * sigue comprando el mayor, y el resto es cosa del agente y su cliente.
131
+ *
132
+ * `null` significa que lo bloqueado no llega ni al nivel más barato. Qué hacer
133
+ * entonces —trabajar igual, devolver, no empezar— lo decide el agente; el SDK
134
+ * no lo va a decidir por él.
135
+ */
136
+ export function nivelPara(niveles, pagado) {
137
+ let elegido = null;
138
+ for (const n of niveles) {
139
+ if (n.wei <= pagado)
140
+ elegido = n;
141
+ }
142
+ return elegido;
143
+ }
package/dist/client.d.ts CHANGED
@@ -90,6 +90,18 @@ export interface HireResult {
90
90
  * búsqueda y no cambia nada.
91
91
  */
92
92
  export declare function variantesDeSkill(skill: string): string[];
93
+ /**
94
+ * Las variantes que se pueden buscar de verdad, dada la lista de permitidas.
95
+ *
96
+ * Va aparte y exportada porque es LA regla que impide que un agente de código
97
+ * acabe pagando a uno de vídeo. Sin lista devuelve todas, que es como se
98
+ * comportaba esto antes de que la lista existiera.
99
+ *
100
+ * La comparación normaliza espacios y mayúsculas: la lista la escribe una
101
+ * persona en su `agent.ts` y la skill la escribe un modelo, y no van a coincidir
102
+ * en el formato.
103
+ */
104
+ export declare function variantesPermitidas(skill: string, permitidas?: string[]): string[];
93
105
  export declare class PanalClient {
94
106
  readonly network: PanalNetwork;
95
107
  readonly addresses: PanalAddresses;
@@ -313,6 +325,20 @@ export declare class PanalClient {
313
325
  envelope?: CallEnvelope | null;
314
326
  /** Saltos permitidos al abrir una cadena nueva. */
315
327
  depth?: number;
328
+ /**
329
+ * LAS ÚNICAS SKILLS QUE SE PUEDEN COMPRAR. Sin esto, cualquiera.
330
+ *
331
+ * Existe por la degradación de aquí abajo. Quien pide la skill suele ser
332
+ * un modelo, y al no encontrar a nadie se recorta por la izquierda:
333
+ * "python video encoding" acaba buscando "video". Para un agente de
334
+ * código eso es contratar a un agente de vídeo con su dinero, y lo peor
335
+ * es que el resultado parece razonable — pagó, le contestaron, entregó.
336
+ *
337
+ * Con esta lista, una variante que no esté en ella NI SE BUSCA. La
338
+ * comprobación va antes de cotizar, así que un intento prohibido no
339
+ * cuesta ni una petición.
340
+ */
341
+ skillsPermitidas?: string[];
316
342
  }): Promise<AskResult & {
317
343
  agent: Address;
318
344
  skill: string;
package/dist/client.js CHANGED
@@ -63,6 +63,21 @@ export function variantesDeSkill(skill) {
63
63
  }
64
64
  return out;
65
65
  }
66
+ /**
67
+ * Las variantes que se pueden buscar de verdad, dada la lista de permitidas.
68
+ *
69
+ * Va aparte y exportada porque es LA regla que impide que un agente de código
70
+ * acabe pagando a uno de vídeo. Sin lista devuelve todas, que es como se
71
+ * comportaba esto antes de que la lista existiera.
72
+ *
73
+ * La comparación normaliza espacios y mayúsculas: la lista la escribe una
74
+ * persona en su `agent.ts` y la skill la escribe un modelo, y no van a coincidir
75
+ * en el formato.
76
+ */
77
+ export function variantesPermitidas(skill, permitidas) {
78
+ const blancas = permitidas?.map((p) => p.trim().replace(/\s+/g, ' ').toLowerCase()).filter(Boolean);
79
+ return variantesDeSkill(skill).filter((v) => !blancas || blancas.includes(v.replace(/\s+/g, ' ').toLowerCase()));
80
+ }
66
81
  function esNombre(v) {
67
82
  if (v === null || typeof v !== 'object')
68
83
  return false;
@@ -188,11 +203,16 @@ export class PanalClient {
188
203
  catch {
189
204
  continue;
190
205
  }
206
+ // La ficha en crudo, si el indexador la manda. Los enlaces del creador
207
+ // salen de ahí, y quedarse con ella entera es además más fiel que
208
+ // recomponerla: lo que este SDK no entienda se conserva tal cual.
209
+ const uri = typeof raw.metadataURI === 'string' ? raw.metadataURI : '';
191
210
  const metadata = {
192
211
  name: typeof raw.name === 'string' ? raw.name : '',
193
212
  description: typeof raw.description === 'string' ? raw.description : '',
194
213
  skills,
195
214
  botUrl: typeof raw.botUrl === 'string' ? raw.botUrl : null,
215
+ links: parseAgentMetadata(uri).links,
196
216
  };
197
217
  out.push({
198
218
  address: getAddress(address),
@@ -201,7 +221,7 @@ export class PanalClient {
201
221
  currency: getAddress(typeof raw.currency === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.currency) ? raw.currency : NATIVE_CURRENCY),
202
222
  active: raw.active !== false,
203
223
  registeredAt,
204
- metadataURI: formatAgentMetadata(metadata),
224
+ metadataURI: uri || formatAgentMetadata(metadata),
205
225
  metadata,
206
226
  // Solo `true` cuenta como verificado. Un indexador viejo no manda el
207
227
  // campo, y tratar «no lo sé» como «sí» es justo al revés de lo que
@@ -681,9 +701,18 @@ export class PanalClient {
681
701
  // núcleo va al final: "Spanish tax law" → "tax law" → "law". Así se
682
702
  // generaliza sin perder de qué se estaba hablando; recortar por la derecha
683
703
  // dejaría "Spanish", que casaría con cualquier cosa española.
704
+ // Las variantes que este agente tiene permitido comprar. Sin lista, todas.
705
+ const permitidas = options.skillsPermitidas;
706
+ const variantes = variantesPermitidas(skill, permitidas);
707
+ if (!variantes.length) {
708
+ throw new X402Error(`Este agente no tiene permitido subcontratar "${skill}". ` +
709
+ (permitidas?.length
710
+ ? `Sólo puede comprar: ${permitidas.join(', ')}.`
711
+ : 'No tiene ninguna skill permitida.'));
712
+ }
684
713
  let candidates = [];
685
714
  let usada = skill;
686
- for (const intento of variantesDeSkill(skill)) {
715
+ for (const intento of variantes) {
687
716
  candidates = (await this.searchAgents(intento, { skill: intento }))
688
717
  .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
689
718
  .slice(0, options.maxCandidates ?? 5);
package/dist/index.d.ts CHANGED
@@ -23,8 +23,8 @@ export { PanalClient, createPanalClient } from './client.js';
23
23
  export type { PanalClientOptions, HireParams, HireResult } from './client.js';
24
24
  export { monad, monadTestnet, addressesFor, chainFor, MAINNET_ADDRESSES, TESTNET_ADDRESSES, NATIVE_CURRENCY, FEE_BPS, AUTO_RELEASE_SECONDS, DISPUTE_TIMEOUT_SECONDS, } from './chains.js';
25
25
  export type { PanalNetwork, PanalAddresses } from './chains.js';
26
- export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, parseAgentMetadata, } from './types.js';
27
- export type { Agent, AgentMetadata, Task } from './types.js';
26
+ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, } from './types.js';
27
+ export type { Agent, AgentLinkKey, AgentLinks, AgentMetadata, Task } from './types.js';
28
28
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
29
29
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
30
30
  export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
@@ -38,5 +38,5 @@ export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402Serve
38
38
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
39
39
  export type { CallEnvelope } from './envelope.js';
40
40
  export type { UrlGuardOptions } from './net.js';
41
- export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
42
- export type { AgentCard, FichaGetResult, FichaPostBrief, FichaX402 } from './agent-card.js';
41
+ export { leerDireccion, leerMaxBriefChars, leerNiveles, leerX402, nivelPara } from './agent-card.js';
42
+ export type { AgentCard, FichaGetResult, FichaNivel, FichaPostBrief, FichaX402, Nivel } from './agent-card.js';
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@
21
21
  */
22
22
  export { PanalClient, createPanalClient } from './client.js';
23
23
  export { monad, monadTestnet, addressesFor, chainFor, MAINNET_ADDRESSES, TESTNET_ADDRESSES, NATIVE_CURRENCY, FEE_BPS, AUTO_RELEASE_SECONDS, DISPUTE_TIMEOUT_SECONDS, } from './chains.js';
24
- export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, parseAgentMetadata, } from './types.js';
24
+ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, } from './types.js';
25
25
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
26
26
  // x402: pagar a otro agente por una consulta, sin escrow y sin humano.
27
27
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
@@ -39,4 +39,4 @@ export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaym
39
39
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
40
40
  // La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
41
41
  // antiguo. En agent-card.ts está por qué llegó a haber dos.
42
- export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
42
+ export { leerDireccion, leerMaxBriefChars, leerNiveles, leerX402, nivelPara } from './agent-card.js';
package/dist/llm.js CHANGED
@@ -42,8 +42,18 @@ export const PROVEEDORES = {
42
42
  dialecto: 'gemini',
43
43
  modeloSugerido: 'gemini-2.0-flash',
44
44
  },
45
- kimi: { baseUrl: 'https://api.moonshot.ai/v1', dialecto: 'openai', modeloSugerido: 'moonshot-v1-8k' },
46
- moonshot: { baseUrl: 'https://api.moonshot.ai/v1', dialecto: 'openai', modeloSugerido: 'moonshot-v1-8k' },
45
+ // `moonshot-v1-8k` era la sugerencia y es de la generación anterior: medido
46
+ // contra la API en agosto de 2026, es además el único de la lista que
47
+ // devolvía 429 por límite de cuota. Quien escribía `LLM_PROVIDER=kimi` sin
48
+ // más se llevaba el peor modelo disponible.
49
+ //
50
+ // `kimi-k2.6` es de propósito general y cabe de sobra en el timeout por
51
+ // defecto. `kimi-k3` responde mejor y tarda bastante más —medido: entre 40 s
52
+ // y 114 s en encargos normales, contra un timeout de 120 s—, así que se
53
+ // elige a mano con LLM_MODEL y no por defecto. Los `kimi-k2.7-code` son los
54
+ // rápidos, y para código.
55
+ kimi: { baseUrl: 'https://api.moonshot.ai/v1', dialecto: 'openai', modeloSugerido: 'kimi-k2.6' },
56
+ moonshot: { baseUrl: 'https://api.moonshot.ai/v1', dialecto: 'openai', modeloSugerido: 'kimi-k2.6' },
47
57
  grok: { baseUrl: 'https://api.x.ai/v1', dialecto: 'openai', modeloSugerido: 'grok-2-vision-1212' },
48
58
  xai: { baseUrl: 'https://api.x.ai/v1', dialecto: 'openai', modeloSugerido: 'grok-2-vision-1212' },
49
59
  glm: { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', dialecto: 'openai', modeloSugerido: 'glm-4v' },
package/dist/types.d.ts CHANGED
@@ -39,7 +39,33 @@ export interface AgentMetadata {
39
39
  * se entrega por otros medios y en la cadena solo queda su hash.
40
40
  */
41
41
  botUrl: string | null;
42
+ /**
43
+ * El logo y los enlaces del creador, si los publicó. Todo opcional.
44
+ *
45
+ * Van en la misma cadena, como tokens `clave:valor`:
46
+ *
47
+ * "Lint · Revisa contratos · solidity · logo:https://lint.dev/l.png · github:lintlabs"
48
+ *
49
+ * Se leen aparte por lo mismo que `bot:`: sin apartarlos, el `logo:` de un
50
+ * agente saldría de skill suya. Un token solo cuenta si su valor vale —una
51
+ * `https://` de verdad, o un usuario con forma de usuario—, porque la
52
+ * descripción es texto libre y alguien va a escribir «web: la mejor» dentro.
53
+ */
54
+ links: AgentLinks;
42
55
  }
56
+ /** Las claves de marca que entiende el marketplace, en el orden en que se pintan. */
57
+ export declare const AGENT_LINK_KEYS: readonly ["logo", "web", "github", "x", "telegram"];
58
+ export type AgentLinkKey = (typeof AGENT_LINK_KEYS)[number];
59
+ /** Cadena vacía = el agente no lo publicó. */
60
+ export type AgentLinks = Record<AgentLinkKey, string>;
61
+ /**
62
+ * Deja un valor de marca como se guarda, o vacío si no sirve.
63
+ *
64
+ * Acepta las tres formas que la gente usa de verdad —`@panal`, `panal` y
65
+ * `https://x.com/panal`— y guarda siempre la misma. La referencia es
66
+ * `src/lib/marca.ts` del marketplace: si cambias una, cambia la otra.
67
+ */
68
+ export declare function normalizeAgentLink(key: AgentLinkKey, raw: string): string;
43
69
  /** Un agente del registry, con su metadata ya interpretada. */
44
70
  export interface Agent {
45
71
  /** La dirección del agente: la que ejecuta el trabajo y cobra. */
@@ -119,7 +145,9 @@ export interface Task {
119
145
  * descripción desplazaría las skills a otro segmento y dejaría la ficha del
120
146
  * agente descuadrada sin ningún error visible.
121
147
  */
122
- export declare function formatAgentMetadata(meta: AgentMetadata): string;
148
+ export declare function formatAgentMetadata(meta: Omit<AgentMetadata, 'links'> & {
149
+ links?: Partial<AgentLinks>;
150
+ }): string;
123
151
  /**
124
152
  * Lee el metadata de un agente. Nunca lanza: un agente puede haberse registrado
125
153
  * con cualquier cadena, y un marketplace que se rompe por una ficha mal escrita
package/dist/types.js CHANGED
@@ -23,6 +23,71 @@ export const TASK_STATUS_LABEL = {
23
23
  [TaskStatus.Disputed]: 'disputed',
24
24
  [TaskStatus.Cancelled]: 'cancelled',
25
25
  };
26
+ /** Las claves de marca que entiende el marketplace, en el orden en que se pintan. */
27
+ export const AGENT_LINK_KEYS = ['logo', 'web', 'github', 'x', 'telegram'];
28
+ const NO_LINKS = { logo: '', web: '', github: '', x: '', telegram: '' };
29
+ /**
30
+ * Deja un valor de marca como se guarda, o vacío si no sirve.
31
+ *
32
+ * Acepta las tres formas que la gente usa de verdad —`@panal`, `panal` y
33
+ * `https://x.com/panal`— y guarda siempre la misma. La referencia es
34
+ * `src/lib/marca.ts` del marketplace: si cambias una, cambia la otra.
35
+ */
36
+ export function normalizeAgentLink(key, raw) {
37
+ const value = raw.trim().slice(0, 120);
38
+ if (!value)
39
+ return '';
40
+ // Un espacio o un `·` por dentro invalida en vez de borrarse: borrarlos
41
+ // convertiría «dos palabras» en el usuario `dospalabras`, que es de otro.
42
+ if (/[\u00b7\s]/.test(value))
43
+ return '';
44
+ if (key === 'logo' || key === 'web') {
45
+ try {
46
+ const u = new URL(value);
47
+ return u.protocol === 'https:' && u.hostname.includes('.') ? value : '';
48
+ }
49
+ catch {
50
+ return '';
51
+ }
52
+ }
53
+ const hosts = {
54
+ github: /^(www\.)?github\.com$/i,
55
+ x: /^(www\.)?(x|twitter)\.com$/i,
56
+ telegram: /^(www\.)?(t\.me|telegram\.me)$/i,
57
+ };
58
+ let user = value.replace(/^@/, '');
59
+ if (/^https?:\/\//i.test(user)) {
60
+ try {
61
+ const u = new URL(user);
62
+ if (!hosts[key].test(u.hostname))
63
+ return '';
64
+ user = u.pathname.replace(/^\/+|\/+$/g, '');
65
+ }
66
+ catch {
67
+ return '';
68
+ }
69
+ }
70
+ user = user.replace(/^@/, '');
71
+ if (!user)
72
+ return '';
73
+ const simple = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
74
+ if (key === 'github')
75
+ return simple.test(user) || /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(user) ? user : '';
76
+ if (key === 'telegram')
77
+ return simple.test(user) || /^\+[A-Za-z0-9_-]{5,64}$/.test(user) ? user : '';
78
+ return simple.test(user) ? user : '';
79
+ }
80
+ /** `github:panal/lint` → la pareja, o null si no es un token de marca válido. */
81
+ function parseLinkToken(segment) {
82
+ const i = segment.indexOf(':');
83
+ if (i <= 0)
84
+ return null;
85
+ const key = segment.slice(0, i).trim().toLowerCase();
86
+ if (!AGENT_LINK_KEYS.includes(key))
87
+ return null;
88
+ const value = normalizeAgentLink(key, segment.slice(i + 1));
89
+ return value ? [key, value] : null;
90
+ }
26
91
  /**
27
92
  * Compone el metadata en el formato que espera el marketplace.
28
93
  *
@@ -35,6 +100,13 @@ export function formatAgentMetadata(meta) {
35
100
  const parts = [clean(meta.name), clean(meta.description), meta.skills.map(clean).join(', ')];
36
101
  if (meta.botUrl)
37
102
  parts.push(`bot:${meta.botUrl.trim()}`);
103
+ // La marca al final: primero lo que el protocolo necesita, luego lo que el
104
+ // creador quiere enseñar. Sin enlaces sale exactamente la ficha de siempre.
105
+ for (const key of AGENT_LINK_KEYS) {
106
+ const value = normalizeAgentLink(key, meta.links?.[key] ?? '');
107
+ if (value)
108
+ parts.push(`${key}:${value}`);
109
+ }
38
110
  return parts.join(' · ');
39
111
  }
40
112
  /**
@@ -48,6 +120,7 @@ export function parseAgentMetadata(metadataURI) {
48
120
  .map((s) => s.trim())
49
121
  .filter(Boolean);
50
122
  let botUrl = null;
123
+ const links = { ...NO_LINKS };
51
124
  const rest = [];
52
125
  for (const seg of segments) {
53
126
  const candidate = seg.toLowerCase().startsWith('bot:') ? seg.slice(4).trim() : null;
@@ -55,6 +128,14 @@ export function parseAgentMetadata(metadataURI) {
55
128
  botUrl = candidate;
56
129
  continue;
57
130
  }
131
+ const link = parseLinkToken(seg);
132
+ // El primero manda: una ficha con dos `logo:` no puede quedar a merced del
133
+ // orden en que se lean.
134
+ if (link) {
135
+ if (!links[link[0]])
136
+ links[link[0]] = link[1];
137
+ continue;
138
+ }
58
139
  rest.push(seg);
59
140
  }
60
141
  // Los campos que falten quedan vacíos en vez de desplazar a los siguientes:
@@ -68,5 +149,6 @@ export function parseAgentMetadata(metadataURI) {
68
149
  .map((s) => s.trim())
69
150
  .filter(Boolean),
70
151
  botUrl,
152
+ links,
71
153
  };
72
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",