@panal/sdk 0.10.0 → 0.11.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/dist/agent-card.d.ts +121 -0
- package/dist/agent-card.js +54 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +64 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La ficha que un agente sirve en `GET /agent.json`.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ ESTE ARCHIVO EXISTE
|
|
5
|
+
*
|
|
6
|
+
* Había dos formatos. El bot colocaba el cobro por llamada en
|
|
7
|
+
* `endpoints.x402Ask`; la plantilla lo ponía en la raíz, como `x402Ask`. Los
|
|
8
|
+
* dos servían la misma información y ningún lector veía las dos, porque cada
|
|
9
|
+
* uno parseaba con un tipo escrito a mano allí donde hacía falta.
|
|
10
|
+
*
|
|
11
|
+
* No fue un descuido de nadie: el tipo `AgentJson` vivía dentro del bot, así
|
|
12
|
+
* que la plantilla —que no depende del bot— no tenía de dónde copiarlo y
|
|
13
|
+
* escribió su propio objeto. Dos implementaciones honestas de una idea que
|
|
14
|
+
* nunca se escribió en un sitio común divergen solas.
|
|
15
|
+
*
|
|
16
|
+
* Aquí está esa idea, en el paquete del que ya dependen la plantilla y el MCP.
|
|
17
|
+
* Quien sirva una ficha, que la sirva con esta forma; quien la lea, que la lea
|
|
18
|
+
* con `leerX402` y `leerMaxBriefChars`, que entienden también la forma vieja.
|
|
19
|
+
*
|
|
20
|
+
* COMPATIBILIDAD
|
|
21
|
+
*
|
|
22
|
+
* Hay agentes desplegados sirviendo el formato antiguo, y no se les puede
|
|
23
|
+
* pedir que se actualicen para seguir siendo contratables. Los lectores
|
|
24
|
+
* aceptan las dos formas y lo seguirán haciendo: quien escribe se moderniza,
|
|
25
|
+
* quien lee perdona. Es la misma regla que ya seguía `agentAddress`.
|
|
26
|
+
*/
|
|
27
|
+
import type { Address } from 'viem';
|
|
28
|
+
/** Cobro por llamada (x402), tal y como lo anuncia un agente. */
|
|
29
|
+
export interface FichaX402 {
|
|
30
|
+
method?: 'POST';
|
|
31
|
+
/** Ruta relativa. `url` gana si están las dos. */
|
|
32
|
+
path?: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
scheme?: string;
|
|
35
|
+
/** Token ERC-20 en el que cobra. */
|
|
36
|
+
asset?: Address;
|
|
37
|
+
assetSymbol?: string;
|
|
38
|
+
/** Precio por llamada, en wei del token, como cadena decimal. */
|
|
39
|
+
amount?: string;
|
|
40
|
+
payTo?: Address;
|
|
41
|
+
howTo?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Cómo mandarle el encargo a un agente, y cuánto texto acepta. */
|
|
44
|
+
export interface FichaPostBrief {
|
|
45
|
+
method?: 'POST';
|
|
46
|
+
path?: string;
|
|
47
|
+
signMessage?: string;
|
|
48
|
+
body?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Tope de caracteres del encargo.
|
|
51
|
+
*
|
|
52
|
+
* Publicarlo es lo que evita que un cliente bloquee el pago con un encargo
|
|
53
|
+
* que el agente va a rechazar. Ausente significa NO LO DICE, que no es lo
|
|
54
|
+
* mismo que «no hay tope»: tratarlo como ilimitado es volver a averiguarlo
|
|
55
|
+
* pagando.
|
|
56
|
+
*/
|
|
57
|
+
maxBriefChars?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Cómo descargar el resultado ya entregado. */
|
|
60
|
+
export interface FichaGetResult {
|
|
61
|
+
method?: 'GET';
|
|
62
|
+
path?: string;
|
|
63
|
+
signMessage?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* La ficha completa. Casi todo es opcional a propósito: los dos motores tienen
|
|
67
|
+
* capacidades distintas —el de la plantilla no lee el registry al servirla, y
|
|
68
|
+
* el del bot sí— y un esquema que obligue a rellenar lo que no se sabe termina
|
|
69
|
+
* rellenándose con mentiras.
|
|
70
|
+
*/
|
|
71
|
+
export interface AgentCard {
|
|
72
|
+
/** La dirección on-chain que este dominio declara suya. Es lo que verifica. */
|
|
73
|
+
agent?: Address;
|
|
74
|
+
/** Alias antiguo de `agent`. Se sigue leyendo; no lo escribas en fichas nuevas. */
|
|
75
|
+
agentAddress?: Address;
|
|
76
|
+
protocol?: 'panal';
|
|
77
|
+
network?: string;
|
|
78
|
+
chainId?: number;
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
skills?: string[];
|
|
82
|
+
price?: {
|
|
83
|
+
amountWei?: string;
|
|
84
|
+
currency?: Address;
|
|
85
|
+
symbol?: string;
|
|
86
|
+
} | null;
|
|
87
|
+
active?: boolean | null;
|
|
88
|
+
contracts?: {
|
|
89
|
+
escrow?: Address;
|
|
90
|
+
registry?: Address;
|
|
91
|
+
token?: Address;
|
|
92
|
+
};
|
|
93
|
+
endpoints?: {
|
|
94
|
+
base?: string | null;
|
|
95
|
+
postBrief?: FichaPostBrief;
|
|
96
|
+
getResult?: FichaGetResult;
|
|
97
|
+
x402Ask?: FichaX402;
|
|
98
|
+
indexer?: string | null;
|
|
99
|
+
};
|
|
100
|
+
/** Alias ANTIGUO de `endpoints.x402Ask`. Se lee; no se escribe. */
|
|
101
|
+
x402Ask?: FichaX402;
|
|
102
|
+
howToHire?: string[];
|
|
103
|
+
}
|
|
104
|
+
/** La dirección que la ficha declara suya, mirando también el alias viejo. */
|
|
105
|
+
export declare function leerDireccion(card: unknown): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* El bloque de cobro por llamada, venga en el sitio nuevo o en el viejo.
|
|
108
|
+
*
|
|
109
|
+
* El sitio canónico gana si están los dos: un agente que sirva ambos está en
|
|
110
|
+
* mitad de una migración, y el nuevo es el que va a seguir manteniendo.
|
|
111
|
+
*/
|
|
112
|
+
export declare function leerX402(card: unknown): FichaX402 | null;
|
|
113
|
+
/**
|
|
114
|
+
* El tope de caracteres del encargo, o `null` si la ficha no lo dice.
|
|
115
|
+
*
|
|
116
|
+
* `null` es NO LO SÉ y hay que tratarlo así. La ficha la sirve un desconocido,
|
|
117
|
+
* así que solo cuenta un entero positivo: un 0 o un negativo harían imposible
|
|
118
|
+
* cualquier encargo, y eso lo decide el agente bajando su tope, no mandando
|
|
119
|
+
* basura en un campo.
|
|
120
|
+
*/
|
|
121
|
+
export declare function leerMaxBriefChars(card: unknown): number | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La ficha que un agente sirve en `GET /agent.json`.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ ESTE ARCHIVO EXISTE
|
|
5
|
+
*
|
|
6
|
+
* Había dos formatos. El bot colocaba el cobro por llamada en
|
|
7
|
+
* `endpoints.x402Ask`; la plantilla lo ponía en la raíz, como `x402Ask`. Los
|
|
8
|
+
* dos servían la misma información y ningún lector veía las dos, porque cada
|
|
9
|
+
* uno parseaba con un tipo escrito a mano allí donde hacía falta.
|
|
10
|
+
*
|
|
11
|
+
* No fue un descuido de nadie: el tipo `AgentJson` vivía dentro del bot, así
|
|
12
|
+
* que la plantilla —que no depende del bot— no tenía de dónde copiarlo y
|
|
13
|
+
* escribió su propio objeto. Dos implementaciones honestas de una idea que
|
|
14
|
+
* nunca se escribió en un sitio común divergen solas.
|
|
15
|
+
*
|
|
16
|
+
* Aquí está esa idea, en el paquete del que ya dependen la plantilla y el MCP.
|
|
17
|
+
* Quien sirva una ficha, que la sirva con esta forma; quien la lea, que la lea
|
|
18
|
+
* con `leerX402` y `leerMaxBriefChars`, que entienden también la forma vieja.
|
|
19
|
+
*
|
|
20
|
+
* COMPATIBILIDAD
|
|
21
|
+
*
|
|
22
|
+
* Hay agentes desplegados sirviendo el formato antiguo, y no se les puede
|
|
23
|
+
* pedir que se actualicen para seguir siendo contratables. Los lectores
|
|
24
|
+
* aceptan las dos formas y lo seguirán haciendo: quien escribe se moderniza,
|
|
25
|
+
* quien lee perdona. Es la misma regla que ya seguía `agentAddress`.
|
|
26
|
+
*/
|
|
27
|
+
/** La dirección que la ficha declara suya, mirando también el alias viejo. */
|
|
28
|
+
export function leerDireccion(card) {
|
|
29
|
+
const c = card;
|
|
30
|
+
const dir = typeof c?.agent === 'string' ? c.agent : typeof c?.agentAddress === 'string' ? c.agentAddress : '';
|
|
31
|
+
return dir || null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* El bloque de cobro por llamada, venga en el sitio nuevo o en el viejo.
|
|
35
|
+
*
|
|
36
|
+
* El sitio canónico gana si están los dos: un agente que sirva ambos está en
|
|
37
|
+
* mitad de una migración, y el nuevo es el que va a seguir manteniendo.
|
|
38
|
+
*/
|
|
39
|
+
export function leerX402(card) {
|
|
40
|
+
const c = card;
|
|
41
|
+
return c?.endpoints?.x402Ask ?? c?.x402Ask ?? null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* El tope de caracteres del encargo, o `null` si la ficha no lo dice.
|
|
45
|
+
*
|
|
46
|
+
* `null` es NO LO SÉ y hay que tratarlo así. La ficha la sirve un desconocido,
|
|
47
|
+
* así que solo cuenta un entero positivo: un 0 o un negativo harían imposible
|
|
48
|
+
* cualquier encargo, y eso lo decide el agente bajando su tope, no mandando
|
|
49
|
+
* basura en un campo.
|
|
50
|
+
*/
|
|
51
|
+
export function leerMaxBriefChars(card) {
|
|
52
|
+
const max = card?.endpoints?.postBrief?.maxBriefChars;
|
|
53
|
+
return typeof max === 'number' && Number.isInteger(max) && max > 0 ? max : null;
|
|
54
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -65,6 +65,31 @@ export interface HireResult {
|
|
|
65
65
|
/** El hash del brief que quedó registrado, para poder probarlo después. */
|
|
66
66
|
taskHash: Hex;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* ¿Esto que manda el indexador es un nombre de PanalNames?
|
|
70
|
+
*
|
|
71
|
+
* Se valida como todo lo que llega de un servicio: si viene a medias se
|
|
72
|
+
* descarta, porque un `origen` inventado haria que la web avisara de una venta
|
|
73
|
+
* que no existio, o peor, que callara una que si.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* La skill pedida y, detrás, versiones cada vez más generales de ella.
|
|
77
|
+
*
|
|
78
|
+
* `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
|
|
79
|
+
* lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
|
|
80
|
+
* un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
|
|
81
|
+
*
|
|
82
|
+
* Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
|
|
83
|
+
* final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
|
|
84
|
+
* Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
|
|
85
|
+
* española y con nada de impuestos: peor que no encontrar a nadie, porque se
|
|
86
|
+
* pagaría al agente equivocado.
|
|
87
|
+
*
|
|
88
|
+
* Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
|
|
89
|
+
* normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
|
|
90
|
+
* búsqueda y no cambia nada.
|
|
91
|
+
*/
|
|
92
|
+
export declare function variantesDeSkill(skill: string): string[];
|
|
68
93
|
export declare class PanalClient {
|
|
69
94
|
readonly network: PanalNetwork;
|
|
70
95
|
readonly addresses: PanalAddresses;
|
|
@@ -290,6 +315,7 @@ export declare class PanalClient {
|
|
|
290
315
|
depth?: number;
|
|
291
316
|
}): Promise<AskResult & {
|
|
292
317
|
agent: Address;
|
|
318
|
+
skill: string;
|
|
293
319
|
}>;
|
|
294
320
|
/**
|
|
295
321
|
* Dónde escucha el x402 de un agente.
|
package/dist/client.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, toBytes } from 'viem';
|
|
19
19
|
import { erc20Abi, escrowAbi, namesAbi, registryAbi } from './abis.js';
|
|
20
|
+
import { leerX402 } from './agent-card.js';
|
|
20
21
|
import { assertPublicUrl, fetchLimited } from './net.js';
|
|
21
22
|
import { X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
22
23
|
import { descend, newEnvelope, remainingBudget } from './envelope.js';
|
|
@@ -33,6 +34,35 @@ const REGISTRY_MAX = 500;
|
|
|
33
34
|
* descarta, porque un `origen` inventado haria que la web avisara de una venta
|
|
34
35
|
* que no existio, o peor, que callara una que si.
|
|
35
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* La skill pedida y, detrás, versiones cada vez más generales de ella.
|
|
39
|
+
*
|
|
40
|
+
* `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
|
|
41
|
+
* lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
|
|
42
|
+
* un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
|
|
43
|
+
*
|
|
44
|
+
* Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
|
|
45
|
+
* final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
|
|
46
|
+
* Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
|
|
47
|
+
* española y con nada de impuestos: peor que no encontrar a nadie, porque se
|
|
48
|
+
* pagaría al agente equivocado.
|
|
49
|
+
*
|
|
50
|
+
* Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
|
|
51
|
+
* normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
|
|
52
|
+
* búsqueda y no cambia nada.
|
|
53
|
+
*/
|
|
54
|
+
export function variantesDeSkill(skill) {
|
|
55
|
+
const palabras = skill.trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
if (palabras.length <= 1)
|
|
57
|
+
return [skill.trim()].filter(Boolean);
|
|
58
|
+
const out = [];
|
|
59
|
+
for (let i = 0; i < palabras.length; i++) {
|
|
60
|
+
const v = palabras.slice(i).join(' ');
|
|
61
|
+
if (!out.includes(v))
|
|
62
|
+
out.push(v);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
36
66
|
function esNombre(v) {
|
|
37
67
|
if (v === null || typeof v !== 'object')
|
|
38
68
|
return false;
|
|
@@ -643,11 +673,31 @@ export class PanalClient {
|
|
|
643
673
|
// Se busca por SKILL, no por texto libre: encontrar a alguien porque la
|
|
644
674
|
// palabra aparece en su descripción no sirve para delegar. Si el indexador
|
|
645
675
|
// no está, `searchAgents` cae solo a la cadena con su texto libre.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
676
|
+
//
|
|
677
|
+
// La búsqueda exige que TODAS las palabras casen, así que una skill de más
|
|
678
|
+
// de dos palabras no encuentra a nadie casi nunca: quien la escribe es un
|
|
679
|
+
// modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
|
|
680
|
+
// Se reintenta quitando palabras POR LA IZQUIERDA porque en inglés el
|
|
681
|
+
// núcleo va al final: "Spanish tax law" → "tax law" → "law". Así se
|
|
682
|
+
// generaliza sin perder de qué se estaba hablando; recortar por la derecha
|
|
683
|
+
// dejaría "Spanish", que casaría con cualquier cosa española.
|
|
684
|
+
let candidates = [];
|
|
685
|
+
let usada = skill;
|
|
686
|
+
for (const intento of variantesDeSkill(skill)) {
|
|
687
|
+
candidates = (await this.searchAgents(intento, { skill: intento }))
|
|
688
|
+
.filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
|
|
689
|
+
.slice(0, options.maxCandidates ?? 5);
|
|
690
|
+
if (candidates.length) {
|
|
691
|
+
usada = intento;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (!candidates.length) {
|
|
696
|
+
throw new X402Error(`Ningún agente activo con la skill "${skill}" publica endpoint.` +
|
|
697
|
+
(variantesDeSkill(skill).length > 1
|
|
698
|
+
? ` Se probó también con ${variantesDeSkill(skill).slice(1).map((v) => `"${v}"`).join(' y ')}.`
|
|
699
|
+
: ''));
|
|
700
|
+
}
|
|
651
701
|
const quotes = [];
|
|
652
702
|
const rechazos = [];
|
|
653
703
|
for (const agent of candidates) {
|
|
@@ -683,7 +733,10 @@ export class PanalClient {
|
|
|
683
733
|
quote: elegido.accept,
|
|
684
734
|
envelope: siguiente,
|
|
685
735
|
});
|
|
686
|
-
|
|
736
|
+
// `skill` es la que de verdad encontró al vendedor, que puede no ser la que
|
|
737
|
+
// pediste: quien llama necesita poder decirlo en su log, o cada búsqueda
|
|
738
|
+
// ensanchada es un cambio de comportamiento invisible.
|
|
739
|
+
return { ...result, agent: elegido.agent.address, skill: usada };
|
|
687
740
|
}
|
|
688
741
|
/**
|
|
689
742
|
* Dónde escucha el x402 de un agente.
|
|
@@ -701,8 +754,11 @@ export class PanalClient {
|
|
|
701
754
|
const url = await assertPublicUrl(new URL('/agent.json', base).toString(), options);
|
|
702
755
|
const res = await fetchLimited(url, { timeoutMs: 10_000 });
|
|
703
756
|
if (res.status === 200) {
|
|
704
|
-
|
|
705
|
-
|
|
757
|
+
// `leerX402` entiende las dos formas. Antes esto solo miraba
|
|
758
|
+
// `endpoints.x402Ask`, así que con la ficha de un agente de plantilla
|
|
759
|
+
// no encontraba nada y caía a la convención: funcionaba de casualidad,
|
|
760
|
+
// y solo mientras el agente escuchara justo en /x402/ask.
|
|
761
|
+
const anunciado = leerX402(JSON.parse(res.text));
|
|
706
762
|
if (anunciado?.url)
|
|
707
763
|
return anunciado.url;
|
|
708
764
|
if (anunciado?.path)
|
package/dist/index.d.ts
CHANGED
|
@@ -36,3 +36,5 @@ export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402Serve
|
|
|
36
36
|
export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
|
|
37
37
|
export type { CallEnvelope } from './envelope.js';
|
|
38
38
|
export type { UrlGuardOptions } from './net.js';
|
|
39
|
+
export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
|
|
40
|
+
export type { AgentCard, FichaGetResult, FichaPostBrief, FichaX402 } from './agent-card.js';
|
package/dist/index.js
CHANGED
|
@@ -33,3 +33,6 @@ export { FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendFilesManifest
|
|
|
33
33
|
export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
|
|
34
34
|
// El sobre que viaja entre agentes: profundidad, presupuesto y detección de ciclos.
|
|
35
35
|
export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
|
|
36
|
+
// La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
|
|
37
|
+
// antiguo. En agent-card.ts está por qué llegó a haber dos.
|
|
38
|
+
export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panal/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json",
|
|
47
47
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
-
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts",
|
|
48
|
+
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts",
|
|
49
49
|
"test:nombres": "tsx test/nombres.test.ts"
|
|
50
50
|
}
|
|
51
51
|
}
|