@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 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.0.tgz bap --help
15
+ npx --package=./zknexus-bap-0.1.1.tgz bap doctor
16
16
  ~~~
17
17
 
18
- Once the package is published, users can run npx @zknexus/bap@0.1.0 doctor.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zknexus/bap",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Bid Advisor Partners CLI and local MCP bridge for public procurement and x402",
5
5
  "type": "module",
6
6
  "bin": { "bap": "src/cli.mjs" },
@@ -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 { apiOrigin, getQuote, networkConfig, requestJson, resourceUrl, walletBalance } from './network.mjs'
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 wallet = walletAddress(home, required(values.wallet, 'wallet'))
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: apiOrigin(values.origin, network) }
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 { status, body } = await requestJson(base + '/api/agent/v1')
66
- if (status !== 200) throw Error('Catalog unavailable. HTTP ' + status)
67
- return output({ version, node: process.version, origin: base, state: body.state, paymentOptions: body.paymentOptions })
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
- export function createBridge({ db, origin, wallet, name, network, fetcher }) {
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 safe = handler => async args => {
11
- try { return result(await handler(args)) }
12
- catch {
13
- return { isError: true, content: [{ type: 'text', text: 'Request unavailable. Check the runtime catalog or the purchase ID. Use the terminal to approve or recover a payment.' }] }
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', annotations: { readOnlyHint: true }
18
- }, safe(async () => {
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) throw Error('Catalog unavailable')
21
- return body
22
- }))
23
- const read = operation => safe(async args => {
24
- if (!wallet || !network) throw Error('Wallet and network are required')
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: 'Ask the user to approve this purchase ID from their terminal. The MCP bridge cannot approve or sign it.' }) }
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