@getbags/cash 0.3.1 → 0.4.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/README.md CHANGED
@@ -117,3 +117,13 @@ Public `search` and `discover` work without credentials. Paid calls still requir
117
117
  `bags result <id> --field response.items --offset 0 --limit 20` and the `wallet_result` MCP tool read retained output without another provider call or payment. Results are private to the wallet and expire after 24 hours. Authentication fields are redacted; provider asset URLs may expire earlier. The fetch API and MCP accept `result_mode: "summary"`.
118
118
 
119
119
  MPP supports bounded one-shot `tempo:charge` with explicit chain/currency checks. Hosted session opening is disabled until durable close can be confirmed. `bags sessions list` shows existing session reservations. Hosted `sessions close` currently reports unsupported cleanup and keeps the hold; it does not report the channel closed. Existing channels and reservations remain owned when cleanup fails.
120
+
121
+ ### Task fit, accepted jobs and asset copies (0.4.0)
122
+
123
+ `bags search "email from LinkedIn" --requirements '{"inputs":{"depth":"full"},"outputs":["email"],"unit":"contact"}' --format json` returns advertised capability fit, health and conservative cost comparisons. Unknown controls, contracts, units or funding remain unknown. Prices are indicative: check the exact method/body before paying.
124
+
125
+ `bags result <id> --refresh` explicitly makes one unpaid GET to an advertised async status URL. It never repeats the initial request, pays a status challenge or forwards your wallet credentials. Providers without a safe polling contract remain explicit; terminal costs are separately labelled when provider-reported.
126
+
127
+ `bags result <id> --retain-asset '<exact-advertised-url>'` explicitly requests a private copy. At most two assets per result, 2 MiB each; copies expire with the retained result. The response includes measured size/SHA256 and an authenticated download URL. Use this option separately from field/page/refresh flags. Plain `bags result <id>` remains a stored read.
128
+
129
+ Registration returns `reusable_skill.markdown`, a small origin/task pointer that refreshes discovery and contracts on each use. It includes no credential or spending authority.
package/dist/api.d.ts CHANGED
@@ -28,6 +28,10 @@ export type DiscoverEndpoint = {
28
28
  source?: 'bags' | 'bazaar' | 'registered';
29
29
  input_schema?: unknown;
30
30
  output_schema?: unknown;
31
+ pricing?: {
32
+ kind?: string;
33
+ indicative?: boolean;
34
+ };
31
35
  score?: number;
32
36
  signals?: {
33
37
  text_match: number;
@@ -123,7 +127,7 @@ export declare function redeem(opts: {
123
127
  }): Promise<RedeemResponse>;
124
128
  export declare function getBalance(token: string): Promise<BalanceResponse>;
125
129
  export declare function discover(token: string, query?: string): Promise<DiscoverResponse>;
126
- export declare function search(token: string, query: string): Promise<DiscoverResponse>;
130
+ export declare function search(token: string, query: string, requirements?: Record<string, unknown>): Promise<DiscoverResponse>;
127
131
  export declare function checkEndpoint(token: string, input: {
128
132
  url: string;
129
133
  method?: string;
@@ -188,4 +192,6 @@ export declare function getResult(token: string, input: {
188
192
  field?: string;
189
193
  offset?: number;
190
194
  limit?: number;
195
+ refresh?: boolean;
196
+ retain_asset_url?: string;
191
197
  }): Promise<Record<string, unknown>>;
package/dist/api.js CHANGED
@@ -98,8 +98,8 @@ export async function discover(token, query) {
98
98
  query: data.query,
99
99
  };
100
100
  }
101
- export async function search(token, query) {
102
- const path = `/api/wallet/search?q=${encodeURIComponent(query)}`;
101
+ export async function search(token, query, requirements) {
102
+ const path = `/api/wallet/search?q=${encodeURIComponent(query)}${requirements ? '&requirements=' + encodeURIComponent(JSON.stringify(requirements)) : ''}`;
103
103
  const body = await apiRequest(path, { token });
104
104
  const data = unwrapPayload(body);
105
105
  return {
@@ -245,7 +245,18 @@ export async function registerOrigin(token, origin, opts = {}) {
245
245
  }
246
246
  /** Read an owned result; never starts another provider call or payment. */
247
247
  export async function getResult(token, input) {
248
+ if (input.retain_asset_url) {
249
+ if (input.refresh || input.field || input.offset !== undefined || input.limit !== undefined)
250
+ throw new Error('Asset retention cannot be combined with read/refresh options');
251
+ return unwrapPayload(await apiRequest('/api/wallet/results', {
252
+ method: 'POST',
253
+ token,
254
+ body: { id: input.id, retain_asset_url: input.retain_asset_url },
255
+ }));
256
+ }
248
257
  const query = new URLSearchParams({ id: input.id });
258
+ if (input.refresh)
259
+ query.set('refresh', 'true');
249
260
  if (input.field)
250
261
  query.set('field', input.field);
251
262
  if (input.offset !== undefined)
package/dist/index.js CHANGED
@@ -22,9 +22,12 @@ function printDiscoverTable(endpoints) {
22
22
  const rows = endpoints.map((ep) => ({
23
23
  url: ep.url,
24
24
  method: ep.method,
25
- price: typeof ep.price_usd === 'number'
26
- ? `$${ep.price_usd.toFixed(6).replace(/(\.\d{2,}?)0+$/, '$1')}`
27
- : String(ep.price_usd),
25
+ price: ep.pricing?.kind === 'unknown' ||
26
+ (ep.source === 'registered' && Number(ep.price_usd) === 0 && ep.pricing?.kind !== 'free')
27
+ ? 'unknown (check)'
28
+ : typeof ep.price_usd === 'number'
29
+ ? `$${ep.price_usd.toFixed(6).replace(/(\.\d{2,}?)0+$/, '$1')}`
30
+ : String(ep.price_usd),
28
31
  merchant: ep.merchant_name ?? '',
29
32
  source: ep.source ?? 'bags',
30
33
  }));
@@ -90,7 +93,7 @@ async function main() {
90
93
  return;
91
94
  }
92
95
  case 'search': {
93
- const result = await search(loadCredentials()?.token ?? '', parsed.query);
96
+ const result = await search(loadCredentials()?.token ?? '', parsed.query, parsed.requirements);
94
97
  if (parsed.format === 'json') {
95
98
  printJson(result);
96
99
  }
@@ -7,10 +7,12 @@ export type WalletMcpApi = {
7
7
  field?: string;
8
8
  offset?: number;
9
9
  limit?: number;
10
+ refresh?: boolean;
11
+ retain_asset_url?: string;
10
12
  }) => Promise<Record<string, unknown>>;
11
13
  getBalance: (token: string) => Promise<BalanceResponse>;
12
14
  discover: (token: string, query?: string) => Promise<DiscoverResponse>;
13
- search: (token: string, query: string) => Promise<DiscoverResponse>;
15
+ search: (token: string, query: string, requirements?: Record<string, unknown>) => Promise<DiscoverResponse>;
14
16
  checkEndpoint: (token: string, input: {
15
17
  url: string;
16
18
  method?: string;
@@ -117,9 +117,20 @@ export function createWalletMcpServer(deps = {}) {
117
117
  description: 'Natural-language search for x402 endpoints. BAGS catalog first, best-effort ecosystem (Bazaar) second. Returns price, description, schemas when available.',
118
118
  inputSchema: {
119
119
  query: z.string().min(1).describe('Natural-language or keyword query'),
120
+ requirements: z
121
+ .object({
122
+ inputs: z
123
+ .record(z.string().min(1).max(100), z.union([z.string().max(256), z.number(), z.boolean(), z.null()]))
124
+ .refine((v) => Object.keys(v).length <= 32, 'At most 32 input controls')
125
+ .optional(),
126
+ outputs: z.array(z.string().min(1).max(100)).max(16).optional(),
127
+ unit: z.string().min(1).max(50).optional(),
128
+ })
129
+ .strict()
130
+ .optional(),
120
131
  },
121
132
  annotations: { readOnlyHint: true, openWorldHint: true },
122
- }, async (args) => textContent(await api.search(resolveToken(deps) ?? '', String(args.query ?? '').trim())));
133
+ }, async (args) => textContent(await api.search(resolveToken(deps) ?? '', String(args.query ?? '').trim(), args.requirements)));
123
134
  server.registerTool('wallet_check_endpoint', {
124
135
  title: 'Check endpoint quote',
125
136
  description: 'Probe an x402 URL for schema + exact price quote without paying (AgentCash check parity). Use before wallet_fetch.',
@@ -139,11 +150,21 @@ export function createWalletMcpServer(deps = {}) {
139
150
  description: 'Retrieve full output, fields or array pages from a previous wallet_fetch result for 24 hours. Does not execute or charge again. Provider asset URLs may expire earlier.',
140
151
  inputSchema: {
141
152
  id: z.string().uuid(),
153
+ refresh: z
154
+ .boolean()
155
+ .optional()
156
+ .describe('Make one unpaid GET to the advertised async status URL; never authorizes payment'),
157
+ retain_asset_url: z
158
+ .string()
159
+ .url()
160
+ .max(8192)
161
+ .optional()
162
+ .describe('Explicitly copy an advertised asset privately, at most two 2 MiB assets for the result lifetime'),
142
163
  field: z.string().max(200).optional(),
143
164
  offset: z.number().int().nonnegative().optional(),
144
165
  limit: z.number().int().min(1).max(100).optional(),
145
166
  },
146
- annotations: { readOnlyHint: true, openWorldHint: false },
167
+ annotations: { readOnlyHint: false, openWorldHint: true },
147
168
  }, async (args) => withToken(async (token) => textContent(await api.getResult(token, args))));
148
169
  server.registerTool('wallet_fetch', {
149
170
  title: 'Wallet fetch (pay + proxy)',
package/dist/parse.d.ts CHANGED
@@ -17,6 +17,8 @@ export type ParsedArgs = {
17
17
  field?: string;
18
18
  offset?: number;
19
19
  limit?: number;
20
+ refresh?: boolean;
21
+ retain_asset_url?: string;
20
22
  } | {
21
23
  command: 'discover';
22
24
  format: 'json' | 'pretty';
@@ -78,6 +80,7 @@ export type ParsedArgs = {
78
80
  } | {
79
81
  command: 'search';
80
82
  query: string;
83
+ requirements?: Record<string, unknown>;
81
84
  format: 'json' | 'pretty';
82
85
  } | {
83
86
  command: 'register';
package/dist/parse.js CHANGED
@@ -7,7 +7,7 @@ const VALID_CLIENTS = ['cursor', 'claude', 'codex'];
7
7
  const COMMAND_USAGE = {
8
8
  redeem: 'usage: bags redeem <code> | bags redeem --test',
9
9
  balance: 'usage: bags balance',
10
- result: 'usage: bags result <id> [--field path] [--offset N] [--limit N]',
10
+ result: 'usage: bags result <id> [--field path] [--offset N] [--limit N] [--refresh] [--retain-asset URL]',
11
11
  discover: 'usage: bags discover [query] [--format json|pretty]',
12
12
  search: 'usage: bags search <query> [--format json|pretty]',
13
13
  check: 'usage: bags check <url> [flags]',
@@ -449,9 +449,18 @@ export function parseArgs(argv) {
449
449
  id,
450
450
  };
451
451
  for (let i = 1; i < rest.length; i += 2) {
452
+ if (rest[i] === '--refresh') {
453
+ out.refresh = true;
454
+ i--;
455
+ continue;
456
+ }
452
457
  const flag = rest[i], value = rest[i + 1];
453
- if (!value || !['--field', '--offset', '--limit'].includes(flag))
458
+ if (!value || !['--field', '--offset', '--limit', '--retain-asset'].includes(flag))
454
459
  return { command: 'error', message: COMMAND_USAGE.result };
460
+ if (flag === '--retain-asset') {
461
+ out.retain_asset_url = value;
462
+ continue;
463
+ }
455
464
  if (flag === '--field')
456
465
  out.field = value;
457
466
  else {
@@ -466,6 +475,9 @@ export function parseArgs(argv) {
466
475
  out.limit = n;
467
476
  }
468
477
  }
478
+ if (out.retain_asset_url &&
479
+ (out.refresh || out.field || out.offset !== undefined || out.limit !== undefined))
480
+ return { command: 'error', message: '--retain-asset cannot be combined with read/refresh flags' };
469
481
  return out;
470
482
  }
471
483
  case 'transactions': {
@@ -647,7 +659,24 @@ export function parseArgs(argv) {
647
659
  if ('error' in parsed)
648
660
  return { command: 'error', message: parsed.error };
649
661
  const queryWords = [];
650
- for (const arg of parsed.rest) {
662
+ let requirements;
663
+ for (let i = 0; i < parsed.rest.length; i++) {
664
+ const arg = parsed.rest[i];
665
+ if (arg === '--requirements') {
666
+ const value = parsed.rest[++i];
667
+ try {
668
+ if (!value || value.length > 4096)
669
+ throw new Error();
670
+ const obj = JSON.parse(value);
671
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
672
+ throw new Error();
673
+ requirements = obj;
674
+ }
675
+ catch {
676
+ return { command: 'error', message: '--requirements must be a bounded JSON object' };
677
+ }
678
+ continue;
679
+ }
651
680
  if (arg.startsWith('-'))
652
681
  return { command: 'error', message: `unknown flag: ${arg}` };
653
682
  queryWords.push(arg);
@@ -655,7 +684,7 @@ export function parseArgs(argv) {
655
684
  const query = queryWords.join(' ').trim();
656
685
  if (!query)
657
686
  return { command: 'error', message: 'usage: bags search <query>' };
658
- return { command: 'search', query, format: parsed.format };
687
+ return { command: 'search', query, format: parsed.format, ...(requirements ? { requirements } : {}) };
659
688
  }
660
689
  case 'register': {
661
690
  const origin = rest[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getbags/cash",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "BAGS Cash CLI \u2014 hosted agent wallet for x402 (balance \u2192 discover \u2192 check \u2192 fetch)",
5
5
  "type": "module",
6
6
  "bin": {