@zknexus/bap 0.1.0 → 0.1.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 +2 -2
- package/package.json +1 -1
- package/skills/procurement-opportunities/SKILL.md +2 -0
- package/src/cli.mjs +17 -8
- package/src/doctor.mjs +54 -0
- package/src/mcp.mjs +59 -21
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ From the repository checkout, enter the package directory and run the following
|
|
|
12
12
|
npm ci
|
|
13
13
|
npm test
|
|
14
14
|
npm pack
|
|
15
|
-
npx --package=./zknexus-bap-0.1.
|
|
15
|
+
npx --package=./zknexus-bap-0.1.1.tgz bap doctor
|
|
16
16
|
~~~
|
|
17
17
|
|
|
18
|
-
Once
|
|
18
|
+
The 0.1.1 doctor report separates catalog configuration from unsigned quote availability on Base and Solana. Once this version is published, run `npx @zknexus/bap@0.1.1 doctor`.
|
|
19
19
|
|
|
20
20
|
The local storage directory defaults to ~/.bap. Set BAP_HOME to use a separate private profile. Wallets are encrypted with the passphrase entered in the terminal. Keep an encrypted backup and protect the receipt database, which contains the original x402 proofs. MCP clients can request quotes and read results. Each purchase requires separate approval in the terminal.
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ description: Search public procurement by country, inspect official evidence and
|
|
|
7
7
|
|
|
8
8
|
Use this skill when asked to find public tenders or professional-service opportunities in selected countries. Do not infer company authority or submit offers.
|
|
9
9
|
|
|
10
|
+
For Codex, Claude or ChatGPT users, start with https://docs.bidadvisorpartners.com/agents/connect. Use the conversation language for replies and for the language argument of local BAP MCP calls (en or es when supported). If the local BAP tool or wallet is missing, use https://docs.bidadvisorpartners.com/agents/cli for English or https://docs.bidadvisorpartners.com/es/cli for Spanish. Give the user only the first missing step, with the exact command for their client. If the catalog cannot be reached, start with the connection check. Distinguish a published price from a quote with a purchase ID.
|
|
11
|
+
|
|
10
12
|
1. Read https://www.bidadvisorpartners.com/developers and https://www.bidadvisorpartners.com/api/agent/v1. Confirm runtime state, supported networks and prices. Discovery is free. Disabled or unavailable is not zero coverage.
|
|
11
13
|
2. Check https://www.bidadvisorpartners.com/api/coverage. Coverage varies by source; never claim a country is fully covered.
|
|
12
14
|
3. Ask for countries if they are unknown. Search GET https://www.bidadvisorpartners.com/api/agent/v1/opportunities?countries=MX&q=software&lifecycle=open&limit=10 with one to five supported country codes. Historical records use lifecycle=expired; unknown state uses lifecycle=unknown. Do not silently expand countries.
|
package/src/cli.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { stdin, stderr } from 'node:process'
|
|
|
6
6
|
import { createInterface } from 'node:readline/promises'
|
|
7
7
|
import { Writable } from 'node:stream'
|
|
8
8
|
import { parseArgs } from 'node:util'
|
|
9
|
-
import {
|
|
9
|
+
import { doctor } from './doctor.mjs'
|
|
10
|
+
import { apiOrigin, getQuote, networkConfig, resourceUrl, walletBalance } from './network.mjs'
|
|
10
11
|
import { quoteForApproval, sendOriginal, signPurchase } from './payment.mjs'
|
|
11
12
|
import { getReceipt, openReceipts, publicReceipt, quotePurchase, withReceipts } from './receipts.mjs'
|
|
12
13
|
import { createWallet, privateDirectory, unlockWallet, walletAddress } from './wallet.mjs'
|
|
@@ -47,12 +48,19 @@ async function terminalQuestion(prompt, secret = false) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function profile(home, values) {
|
|
51
|
+
function profile(home, values, { allowMissingWallet = false } = {}) {
|
|
51
52
|
const name = required(values.network, 'network')
|
|
52
53
|
const network = networkConfig(name)
|
|
53
|
-
const
|
|
54
|
+
const origin = apiOrigin(values.origin, network)
|
|
55
|
+
const walletName = required(values.wallet, 'wallet')
|
|
56
|
+
let wallet
|
|
57
|
+
try { wallet = walletAddress(home, walletName) }
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (allowMissingWallet && error.code === 'ENOENT') return { name, network, origin, walletMissing: true }
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
54
62
|
if (network.chain !== wallet.chain) throw Error('Wallet and network are incompatible')
|
|
55
|
-
return { wallet: wallet.name, name, network, origin
|
|
63
|
+
return { wallet: wallet.name, name, network, origin }
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
async function main(args) {
|
|
@@ -62,9 +70,10 @@ async function main(args) {
|
|
|
62
70
|
if (command === 'doctor') {
|
|
63
71
|
const values = parse(args.slice(1), { network: textOption, origin: textOption })
|
|
64
72
|
const base = apiOrigin(values.origin, values.network ? networkConfig(values.network) : undefined)
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const report = await doctor(base, values.network)
|
|
74
|
+
output({ version, node: process.version, origin: base, ...report })
|
|
75
|
+
if (report.configuration.state === 'catalog_unavailable') process.exitCode = 1
|
|
76
|
+
return
|
|
68
77
|
}
|
|
69
78
|
if (command === 'skills' && action === 'install') {
|
|
70
79
|
const values = parse(options, { client: textOption })
|
|
@@ -140,7 +149,7 @@ async function main(args) {
|
|
|
140
149
|
if (!values.stdio) throw Error('Supply --stdio')
|
|
141
150
|
const home = privateDirectory()
|
|
142
151
|
const configuration = values.wallet || values.network || values.origin
|
|
143
|
-
? profile(home, values) : { origin: apiOrigin() }
|
|
152
|
+
? profile(home, values, { allowMissingWallet: true }) : { origin: apiOrigin() }
|
|
144
153
|
const { serve } = await import('./mcp.mjs')
|
|
145
154
|
const db = openReceipts(home)
|
|
146
155
|
process.once('exit', () => db.close())
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { networkConfig, requestJson, resourceUrl, validateQuote } from './network.mjs'
|
|
2
|
+
|
|
3
|
+
export async function doctor(origin, networkName, fetcher) {
|
|
4
|
+
const resource = resourceUrl(origin, 'search', { countries: ['MX'], limit: 1 })
|
|
5
|
+
const [catalog, quote] = await Promise.allSettled([
|
|
6
|
+
requestJson(origin + '/api/agent/v1', {}, fetcher),
|
|
7
|
+
requestJson(resource, {}, fetcher)
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
const catalogResponse = catalog.status === 'fulfilled' ? catalog.value : null
|
|
11
|
+
const catalogBody = catalogResponse?.status === 200 ? catalogResponse.body : null
|
|
12
|
+
const configuration = {
|
|
13
|
+
state: typeof catalogBody?.state === 'string' ? catalogBody.state : 'catalog_unavailable',
|
|
14
|
+
httpStatus: catalogResponse?.status ?? null
|
|
15
|
+
}
|
|
16
|
+
const paymentOptions = Array.isArray(catalogBody?.paymentOptions) ? catalogBody.paymentOptions : []
|
|
17
|
+
const names = networkName ? [networkName] : ['base', 'solana']
|
|
18
|
+
const response = quote.status === 'fulfilled' ? quote.value : null
|
|
19
|
+
|
|
20
|
+
const operational = names.map(name => {
|
|
21
|
+
const network = networkConfig(name)
|
|
22
|
+
const result = {
|
|
23
|
+
name,
|
|
24
|
+
network: network.id,
|
|
25
|
+
configured: Array.isArray(catalogBody?.paymentOptions)
|
|
26
|
+
? paymentOptions.some(option => option?.network === network.id)
|
|
27
|
+
: null,
|
|
28
|
+
httpStatus: response?.status ?? null
|
|
29
|
+
}
|
|
30
|
+
if (!response) return { ...result, state: 'probe_failed' }
|
|
31
|
+
if (response.status !== 402) return { ...result, state: 'quote_unavailable' }
|
|
32
|
+
|
|
33
|
+
const challenge = response.body
|
|
34
|
+
if (challenge?.x402Version !== 2 || challenge.resource?.url !== resource ||
|
|
35
|
+
!Array.isArray(challenge.accepts) || challenge.accepts.length === 0) {
|
|
36
|
+
return { ...result, state: 'invalid_quote' }
|
|
37
|
+
}
|
|
38
|
+
if (!challenge.accepts.some(offer => offer?.network === network.id)) {
|
|
39
|
+
return { ...result, state: 'quote_not_offered' }
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const { amount } = validateQuote(challenge, resource, network)
|
|
43
|
+
return { ...result, state: 'quote_available', amountAtomic: amount }
|
|
44
|
+
} catch {
|
|
45
|
+
return { ...result, state: 'invalid_quote' }
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
// Keep the original catalog fields for consumers of the CLI's JSON output.
|
|
51
|
+
state: configuration.state, paymentOptions, configuration,
|
|
52
|
+
quoteProbe: { resource, operational }
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -4,48 +4,86 @@ import { z } from 'zod'
|
|
|
4
4
|
import { getQuote, requestJson, resourceUrl } from './network.mjs'
|
|
5
5
|
import { getReceipt, publicReceipt, quotePurchase } from './receipts.mjs'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const languageInput = z.enum(['en', 'es']).optional()
|
|
8
|
+
.describe('Reply language for BAP instructions. Use es for a user speaking Spanish or en for English. Defaults to en.')
|
|
9
|
+
|
|
10
|
+
const guide = {
|
|
11
|
+
en: 'https://docs.bidadvisorpartners.com/agents/cli',
|
|
12
|
+
es: 'https://docs.bidadvisorpartners.com/es/cli'
|
|
13
|
+
}
|
|
14
|
+
const messages = {
|
|
15
|
+
en: {
|
|
16
|
+
wallet_missing: 'Check the wallet name in your BAP setup. Create that wallet in your terminal or update the MCP registration to use your existing wallet, then reopen the client.',
|
|
17
|
+
wallet_setup_required: 'Follow the quick start to create or select a local wallet and register BAP with its wallet name and network. Then reopen the client and retry your quote.',
|
|
18
|
+
catalog_unavailable: 'BAP returned an error. Run bap doctor in your terminal to check service availability.',
|
|
19
|
+
catalog_unreachable: 'The client could not read the BAP catalog. Run bap doctor in your terminal. If it works there, check network access in this client.',
|
|
20
|
+
quote_unavailable: 'BAP could not prepare a valid quote. Run bap doctor for your network and check the connection before retrying.',
|
|
21
|
+
purchase_unavailable: 'Check the purchase ID and use the same wallet, network and local BAP profile used to request the quote.',
|
|
22
|
+
approve: 'Show the purchase ID to the user. If they choose to pay, they must approve it in their terminal.'
|
|
23
|
+
},
|
|
24
|
+
es: {
|
|
25
|
+
wallet_missing: 'Revisa el nombre de la billetera en la configuración de BAP. Crea esa billetera en tu terminal o configura el MCP con una que ya tengas. Luego vuelve a abrir el cliente.',
|
|
26
|
+
wallet_setup_required: 'Sigue la guía para crear o elegir una billetera local y conectar BAP con el nombre de la billetera y la red. Vuelve a abrir el cliente y pide la cotización de nuevo.',
|
|
27
|
+
catalog_unavailable: 'BAP devolvió un error. Ejecuta bap doctor en tu terminal para comprobar si el servicio está disponible.',
|
|
28
|
+
catalog_unreachable: 'El cliente no pudo consultar el catálogo de BAP. Ejecuta bap doctor en tu terminal. Si funciona allí, revisa el acceso a internet de este cliente.',
|
|
29
|
+
quote_unavailable: 'BAP no pudo preparar una cotización válida. Ejecuta bap doctor para tu red y revisa la conexión antes de reintentar.',
|
|
30
|
+
purchase_unavailable: 'Revisa el ID de compra y usa la misma billetera, red y configuración local de BAP con la que pediste la cotización.',
|
|
31
|
+
approve: 'Muestra al usuario el ID de compra. Si decide pagar, debe aprobar la compra en su terminal.'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createBridge({ db, origin, wallet, name, network, walletMissing, fetcher }) {
|
|
8
36
|
const server = new McpServer({ name: 'bid-advisor-partners-cli', version: '0.1.0' })
|
|
9
37
|
const result = data => ({ content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data })
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
const failure = (code, language, details = {}) => ({
|
|
39
|
+
...result({ code, instruction: messages[language][code], guide: guide[language], ...details }),
|
|
40
|
+
isError: true
|
|
41
|
+
})
|
|
42
|
+
const needsWallet = language => failure(walletMissing ? 'wallet_missing' : 'wallet_setup_required', language)
|
|
43
|
+
const safe = (handler, errorCode) => async (args = {}) => {
|
|
44
|
+
const language = args.language ?? 'en'
|
|
45
|
+
try { return await handler(args, language) }
|
|
46
|
+
catch { return failure(errorCode, language) }
|
|
15
47
|
}
|
|
48
|
+
|
|
16
49
|
server.registerTool('get_agent_catalog', {
|
|
17
|
-
description: 'Free BAP runtime catalog and available networks
|
|
18
|
-
|
|
50
|
+
description: 'Free BAP runtime catalog and available networks. Works without a local wallet.',
|
|
51
|
+
inputSchema: { language: languageInput }, annotations: { readOnlyHint: true }
|
|
52
|
+
}, safe(async (_args, language) => {
|
|
19
53
|
const { status, body } = await requestJson(origin + '/api/agent/v1', {}, fetcher)
|
|
20
|
-
if (status !== 200)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
if (status !== 200) {
|
|
55
|
+
return failure('catalog_unavailable', language, { httpStatus: status })
|
|
56
|
+
}
|
|
57
|
+
return result(body)
|
|
58
|
+
}, 'catalog_unreachable'))
|
|
59
|
+
|
|
60
|
+
const read = operation => safe(async (args, language) => {
|
|
61
|
+
if (!wallet || !network) return needsWallet(language)
|
|
25
62
|
const resource = resourceUrl(origin, operation, args)
|
|
26
63
|
const row = await quotePurchase(db, { resource, wallet, network: name }, () => getQuote(resource, network, fetcher))
|
|
27
|
-
return { ...publicReceipt(row), ...(row.proof ? {} : { instruction:
|
|
28
|
-
})
|
|
64
|
+
return result({ ...publicReceipt(row), ...(row.proof ? {} : { instruction: messages[language].approve }) })
|
|
65
|
+
}, 'quote_unavailable')
|
|
29
66
|
server.registerTool('search_opportunities', {
|
|
30
67
|
description: 'Quote a public procurement search for terminal approval, or return an existing paid result. This tool never signs or sends payment.',
|
|
31
68
|
inputSchema: {
|
|
32
69
|
countries: z.array(z.string().regex(/^[A-Z]{2}$/)).min(1).max(5), q: z.string().max(120).optional(),
|
|
33
70
|
kind: z.enum(['business', 'professional']).optional(), lifecycle: z.enum(['open', 'expired', 'unknown']).optional(),
|
|
34
|
-
page: z.number().int().min(1).max(100).optional(), limit: z.number().int().min(1).max(25).optional()
|
|
71
|
+
page: z.number().int().min(1).max(100).optional(), limit: z.number().int().min(1).max(25).optional(), language: languageInput
|
|
35
72
|
}
|
|
36
73
|
}, read('search'))
|
|
37
74
|
server.registerTool('get_opportunity', {
|
|
38
75
|
description: 'Quote one public procurement detail for terminal approval, or return its existing paid result. This tool never signs or sends payment.',
|
|
39
|
-
inputSchema: { id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/) }
|
|
76
|
+
inputSchema: { id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/), language: languageInput }
|
|
40
77
|
}, read('detail'))
|
|
41
78
|
server.registerTool('get_purchase_status', {
|
|
42
79
|
description: 'Read local purchase state and any delivered result. Payment proof is private.',
|
|
43
|
-
inputSchema: { id: z.string().uuid() }, annotations: { readOnlyHint: true }
|
|
44
|
-
}, safe(({ id }) => {
|
|
80
|
+
inputSchema: { id: z.string().uuid(), language: languageInput }, annotations: { readOnlyHint: true }
|
|
81
|
+
}, safe(({ id }, language) => {
|
|
82
|
+
if (!wallet || !network) return needsWallet(language)
|
|
45
83
|
const row = getReceipt(db, id)
|
|
46
84
|
if (row.wallet !== wallet || row.network !== name || new URL(row.resource).origin !== origin) throw Error('Purchase belongs to another profile')
|
|
47
|
-
return publicReceipt(row)
|
|
48
|
-
}))
|
|
85
|
+
return result(publicReceipt(row))
|
|
86
|
+
}, 'purchase_unavailable'))
|
|
49
87
|
return server
|
|
50
88
|
}
|
|
51
89
|
|