@getbags/cash 0.2.1 → 0.3.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 +53 -38
- package/SKILL.md +77 -29
- package/dist/api.d.ts +25 -1
- package/dist/api.js +25 -4
- package/dist/detect.d.ts +33 -0
- package/dist/detect.js +105 -0
- package/dist/doctor.d.ts +47 -0
- package/dist/doctor.js +212 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +18 -0
- package/dist/fetch-hints.d.ts +18 -0
- package/dist/fetch-hints.js +17 -0
- package/dist/index.js +68 -50
- package/dist/install-apply.d.ts +29 -0
- package/dist/install-apply.js +55 -0
- package/dist/install-command.d.ts +17 -0
- package/dist/install-command.js +173 -0
- package/dist/install-plan.d.ts +68 -0
- package/dist/install-plan.js +337 -0
- package/dist/mcp-instructions.d.ts +7 -0
- package/dist/mcp-instructions.js +38 -0
- package/dist/mcp-server.d.ts +72 -0
- package/dist/mcp-server.js +293 -0
- package/dist/parse.d.ts +22 -1
- package/dist/parse.js +275 -14
- package/dist/policies.d.ts +26 -0
- package/dist/policies.js +69 -0
- package/package.json +10 -5
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type BalanceResponse } from './api.js';
|
|
2
|
+
import { type ClientId, type ClientPaths, type DetectedEnvironment, type InstallScope } from './detect.js';
|
|
3
|
+
export type DoctorStatus = 'pass' | 'fail' | 'warn';
|
|
4
|
+
export type DoctorCheck = {
|
|
5
|
+
name: string;
|
|
6
|
+
status: DoctorStatus;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
export type DoctorFs = {
|
|
10
|
+
existsSync: (path: string) => boolean;
|
|
11
|
+
readFileSync: (path: string, encoding: 'utf8') => string;
|
|
12
|
+
};
|
|
13
|
+
export type DoctorDeps = {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
home?: string;
|
|
16
|
+
/** When set, check only this scope. When omitted, check both project and user. */
|
|
17
|
+
scope?: InstallScope;
|
|
18
|
+
requestedClients?: ClientId[];
|
|
19
|
+
fs?: DoctorFs;
|
|
20
|
+
loadToken?: () => string | null;
|
|
21
|
+
fetchBalance?: (token: string) => Promise<BalanceResponse>;
|
|
22
|
+
detect?: (opts: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
home: string;
|
|
25
|
+
scope: InstallScope;
|
|
26
|
+
exists: (path: string) => boolean;
|
|
27
|
+
requestedClients?: ClientId[];
|
|
28
|
+
}) => DetectedEnvironment;
|
|
29
|
+
instructionsConstant?: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function checkTokenPresent(token: string | null): DoctorCheck;
|
|
32
|
+
export declare function checkBalanceReadable(token: string | null, fetchBalance: (token: string) => Promise<BalanceResponse>): Promise<DoctorCheck>;
|
|
33
|
+
export declare function checkMcpEntry(client: ClientPaths, scope: InstallScope, fs: DoctorFs): DoctorCheck;
|
|
34
|
+
export declare function checkSkillInstalled(client: ClientPaths, scope: InstallScope, fs: DoctorFs): DoctorCheck;
|
|
35
|
+
export declare function checkRoutingRule(client: ClientPaths, scope: InstallScope, fs: DoctorFs): DoctorCheck;
|
|
36
|
+
export declare function checkInstructionsConstant(text?: string): DoctorCheck;
|
|
37
|
+
/**
|
|
38
|
+
* One-line summary distinguishing agent visibility from wallet funding.
|
|
39
|
+
* Doctor's entire purpose is this distinction.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatDoctorSummary(checks: DoctorCheck[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* Run all doctor checks. Distinguishes "installed but not visible to agents"
|
|
44
|
+
* (MCP/skill/routing fails) from "visible but unfunded" (token/balance fails).
|
|
45
|
+
* By default checks both project and user scopes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runDoctor(deps?: DoctorDeps): Promise<DoctorCheck[]>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { getBalance } from './api.js';
|
|
4
|
+
import { loadCredentials } from './config.js';
|
|
5
|
+
import { detectClients, } from './detect.js';
|
|
6
|
+
import { WALLET_MCP_INSTRUCTIONS } from './mcp-instructions.js';
|
|
7
|
+
import { bagsSkillPath, inspectBagsMcpEntry, instructionsHaveRoutingRule } from './install-plan.js';
|
|
8
|
+
const defaultFs = {
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync: (path, encoding) => readFileSync(path, encoding),
|
|
11
|
+
};
|
|
12
|
+
function readText(fs, path) {
|
|
13
|
+
if (!fs.existsSync(path))
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
return fs.readFileSync(path, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function checkName(kind, client, scope) {
|
|
23
|
+
return `${kind}:${client}:${scope}`;
|
|
24
|
+
}
|
|
25
|
+
export function checkTokenPresent(token) {
|
|
26
|
+
if (token && token.length > 0) {
|
|
27
|
+
return { name: 'token', status: 'pass', message: 'wallet token present in credentials store' };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
name: 'token',
|
|
31
|
+
status: 'fail',
|
|
32
|
+
message: 'no wallet token; run: bags redeem --test (or bags redeem <code>)',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function checkBalanceReadable(token, fetchBalance) {
|
|
36
|
+
if (!token) {
|
|
37
|
+
return {
|
|
38
|
+
name: 'balance',
|
|
39
|
+
status: 'fail',
|
|
40
|
+
message: 'skipped — no token (cannot read balance/caps)',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const bal = await fetchBalance(token);
|
|
45
|
+
if (bal.balance_cents <= 0) {
|
|
46
|
+
return {
|
|
47
|
+
name: 'balance',
|
|
48
|
+
status: 'warn',
|
|
49
|
+
message: `balance 0¢ (unfunded), per-call cap ${bal.per_call_cap_cents}¢, daily cap ${bal.daily_cap_cents}¢, status ${bal.status} — redeem another invite or run: bags redeem --test`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: 'balance',
|
|
54
|
+
status: 'pass',
|
|
55
|
+
message: `balance ${bal.balance_cents}¢, per-call cap ${bal.per_call_cap_cents}¢, daily cap ${bal.daily_cap_cents}¢, status ${bal.status}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
name: 'balance',
|
|
61
|
+
status: 'fail',
|
|
62
|
+
message: err instanceof Error ? err.message : String(err),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function checkMcpEntry(client, scope, fs) {
|
|
67
|
+
const content = readText(fs, client.mcpConfigPath);
|
|
68
|
+
const name = checkName('mcp', client.client, scope);
|
|
69
|
+
const status = inspectBagsMcpEntry(content, client.mcpConfigFormat);
|
|
70
|
+
if (status === 'clean') {
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
status: 'pass',
|
|
74
|
+
message: `bags-cash MCP entry present in ${client.mcpConfigPath}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (status === 'dirty') {
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
status: 'fail',
|
|
81
|
+
message: `bags-cash MCP entry in ${client.mcpConfigPath} has env/headers/extra keys (possible secret) — re-run bags init to strip them`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
status: 'fail',
|
|
87
|
+
message: `bags-cash MCP entry missing in ${client.mcpConfigPath}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function checkSkillInstalled(client, scope, fs) {
|
|
91
|
+
const path = bagsSkillPath(client.skillsDir);
|
|
92
|
+
const name = checkName('skill', client.client, scope);
|
|
93
|
+
if (fs.existsSync(path)) {
|
|
94
|
+
return {
|
|
95
|
+
name,
|
|
96
|
+
status: 'pass',
|
|
97
|
+
message: `skill present at ${path}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
status: 'fail',
|
|
103
|
+
message: `skill missing at ${path}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function checkRoutingRule(client, scope, fs) {
|
|
107
|
+
const content = readText(fs, client.instructionsPath);
|
|
108
|
+
const name = checkName('routing', client.client, scope);
|
|
109
|
+
if (instructionsHaveRoutingRule(content)) {
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
status: 'pass',
|
|
113
|
+
message: `routing-rule block present in ${client.instructionsPath}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
status: 'fail',
|
|
119
|
+
message: `routing-rule block missing in ${client.instructionsPath}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function checkInstructionsConstant(text = WALLET_MCP_INSTRUCTIONS) {
|
|
123
|
+
if (typeof text === 'string' && text.trim().length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
name: 'instructions',
|
|
126
|
+
status: 'pass',
|
|
127
|
+
message: 'WALLET_MCP_INSTRUCTIONS constant is non-empty',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
name: 'instructions',
|
|
132
|
+
status: 'fail',
|
|
133
|
+
message: 'WALLET_MCP_INSTRUCTIONS constant is empty — MCP auto-pick will not work',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One-line summary distinguishing agent visibility from wallet funding.
|
|
138
|
+
* Doctor's entire purpose is this distinction.
|
|
139
|
+
*/
|
|
140
|
+
export function formatDoctorSummary(checks) {
|
|
141
|
+
const visibility = checks.filter((c) => /^(mcp|skill|routing):/.test(c.name));
|
|
142
|
+
const visible = visibility.length > 0 && visibility.some((c) => c.status === 'pass');
|
|
143
|
+
const visibilityOk = visibility.length > 0 && visibility.every((c) => c.status === 'pass');
|
|
144
|
+
const balance = checks.find((c) => c.name === 'balance');
|
|
145
|
+
const token = checks.find((c) => c.name === 'token');
|
|
146
|
+
const funded = balance?.status === 'pass' && token?.status === 'pass';
|
|
147
|
+
if (visibilityOk && funded) {
|
|
148
|
+
return 'Summary: wallet is visible to agents and funded.';
|
|
149
|
+
}
|
|
150
|
+
if (visible && !funded) {
|
|
151
|
+
return 'Summary: wallet is visible to agents but unfunded (or token/balance check failed) — visibility ≠ funding.';
|
|
152
|
+
}
|
|
153
|
+
if (!visible && funded) {
|
|
154
|
+
return 'Summary: wallet is funded but not visible to agents — run bags init (try --scope user).';
|
|
155
|
+
}
|
|
156
|
+
if (visibility.length === 0) {
|
|
157
|
+
return 'Summary: no agent client detected — install Cursor/Claude/Codex or pass --client to force-create, then bags init.';
|
|
158
|
+
}
|
|
159
|
+
return 'Summary: wallet is not fully visible to agents and funding is not confirmed — fix FAIL/WARN checks above.';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Run all doctor checks. Distinguishes "installed but not visible to agents"
|
|
163
|
+
* (MCP/skill/routing fails) from "visible but unfunded" (token/balance fails).
|
|
164
|
+
* By default checks both project and user scopes.
|
|
165
|
+
*/
|
|
166
|
+
export async function runDoctor(deps = {}) {
|
|
167
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
168
|
+
const home = deps.home ?? homedir();
|
|
169
|
+
const scopes = deps.scope ? [deps.scope] : ['project', 'user'];
|
|
170
|
+
const fs = deps.fs ?? defaultFs;
|
|
171
|
+
const detect = deps.detect ?? detectClients;
|
|
172
|
+
const loadToken = deps.loadToken ?? (() => loadCredentials()?.token ?? null);
|
|
173
|
+
const fetchBalance = deps.fetchBalance ?? ((token) => getBalance(token));
|
|
174
|
+
const instructions = deps.instructionsConstant ?? WALLET_MCP_INSTRUCTIONS;
|
|
175
|
+
const token = loadToken();
|
|
176
|
+
const checks = [
|
|
177
|
+
checkTokenPresent(token),
|
|
178
|
+
await checkBalanceReadable(token, fetchBalance),
|
|
179
|
+
checkInstructionsConstant(instructions),
|
|
180
|
+
];
|
|
181
|
+
let anyClient = false;
|
|
182
|
+
const lookedForAll = [];
|
|
183
|
+
for (const scope of scopes) {
|
|
184
|
+
const env = detect({
|
|
185
|
+
cwd,
|
|
186
|
+
home,
|
|
187
|
+
scope,
|
|
188
|
+
exists: (p) => fs.existsSync(p),
|
|
189
|
+
requestedClients: deps.requestedClients,
|
|
190
|
+
});
|
|
191
|
+
for (const p of env.lookedFor) {
|
|
192
|
+
if (!lookedForAll.includes(p))
|
|
193
|
+
lookedForAll.push(p);
|
|
194
|
+
}
|
|
195
|
+
if (env.clients.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
anyClient = true;
|
|
198
|
+
for (const client of env.clients) {
|
|
199
|
+
checks.push(checkMcpEntry(client, scope, fs));
|
|
200
|
+
checks.push(checkSkillInstalled(client, scope, fs));
|
|
201
|
+
checks.push(checkRoutingRule(client, scope, fs));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!anyClient) {
|
|
205
|
+
checks.push({
|
|
206
|
+
name: 'clients',
|
|
207
|
+
status: 'fail',
|
|
208
|
+
message: `no agent client detected (looked for: ${lookedForAll.join(', ') || 'none'})`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return checks;
|
|
212
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export declare const ERROR_REMEDIES: Record<string, string>;
|
|
2
2
|
export declare function formatApiError(code: string, message: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Surface the wallet API's nested upstream diagnostic (data.http_status +
|
|
5
|
+
* data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
|
|
6
|
+
* instead of requiring a raw API call to see why a merchant rejected payment.
|
|
7
|
+
*/
|
|
8
|
+
export declare function withUpstreamDetail(message: string, data: unknown): string;
|
package/dist/errors.js
CHANGED
|
@@ -26,3 +26,21 @@ export function formatApiError(code, message) {
|
|
|
26
26
|
const line = `${code}: ${message}`;
|
|
27
27
|
return remedy ? `${line}\n${remedy}` : line;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Surface the wallet API's nested upstream diagnostic (data.http_status +
|
|
31
|
+
* data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
|
|
32
|
+
* instead of requiring a raw API call to see why a merchant rejected payment.
|
|
33
|
+
*/
|
|
34
|
+
export function withUpstreamDetail(message, data) {
|
|
35
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
36
|
+
return message;
|
|
37
|
+
const record = data;
|
|
38
|
+
const status = record.http_status;
|
|
39
|
+
const response = record.response;
|
|
40
|
+
if (status === undefined && response === undefined)
|
|
41
|
+
return message;
|
|
42
|
+
const body = typeof response === 'string' ? response : response ? JSON.stringify(response) : '';
|
|
43
|
+
const head = status !== undefined ? `upstream http ${String(status)}` : 'upstream';
|
|
44
|
+
const detail = body ? `${head}: ${body.slice(0, 300)}` : head;
|
|
45
|
+
return `${message} (${detail})`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch output diagnostics for failed paid attempts.
|
|
3
|
+
*
|
|
4
|
+
* A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
|
|
5
|
+
* a bad request param) — money gone with nothing to show. When the 402
|
|
6
|
+
* challenge declared a bazaar input schema, point the agent at `bags check`
|
|
7
|
+
* so the retry carries the required fields.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INPUT_SCHEMA_HINT = "this endpoint declares an input schema - run `bags check` to see required fields";
|
|
10
|
+
/** True when payment settled but the upstream response was an error status. */
|
|
11
|
+
export declare function isFailedPaidAttempt(payment: {
|
|
12
|
+
paid?: boolean;
|
|
13
|
+
} | null | undefined, httpStatus: number): boolean;
|
|
14
|
+
/** Should `bags fetch` print the input-schema hint for this fetch result? */
|
|
15
|
+
export declare function shouldPrintInputSchemaHint(payment: {
|
|
16
|
+
paid?: boolean;
|
|
17
|
+
input_schema_declared?: boolean;
|
|
18
|
+
} | null | undefined, httpStatus: number): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch output diagnostics for failed paid attempts.
|
|
3
|
+
*
|
|
4
|
+
* A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
|
|
5
|
+
* a bad request param) — money gone with nothing to show. When the 402
|
|
6
|
+
* challenge declared a bazaar input schema, point the agent at `bags check`
|
|
7
|
+
* so the retry carries the required fields.
|
|
8
|
+
*/
|
|
9
|
+
export const INPUT_SCHEMA_HINT = 'this endpoint declares an input schema - run `bags check` to see required fields';
|
|
10
|
+
/** True when payment settled but the upstream response was an error status. */
|
|
11
|
+
export function isFailedPaidAttempt(payment, httpStatus) {
|
|
12
|
+
return payment?.paid === true && httpStatus >= 400;
|
|
13
|
+
}
|
|
14
|
+
/** Should `bags fetch` print the input-schema hint for this fetch result? */
|
|
15
|
+
export function shouldPrintInputSchemaHint(payment, httpStatus) {
|
|
16
|
+
return isFailedPaidAttempt(payment, httpStatus) && payment?.input_schema_declared === true;
|
|
17
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { checkEndpoint, createDeposit, createKey, discover, getBalance, getResult, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
|
|
3
|
+
import { getBaseUrl, loadCredentials, requireToken, saveCredentials } from './config.js';
|
|
4
|
+
import { INPUT_SCHEMA_HINT, shouldPrintInputSchemaHint } from './fetch-hints.js';
|
|
5
|
+
import { formatDoctorSummary, runDoctor } from './doctor.js';
|
|
6
|
+
import { runInitCommand } from './install-command.js';
|
|
7
|
+
import { runStdioServer } from './mcp-server.js';
|
|
8
|
+
import { policyPatchFromSets } from './policies.js';
|
|
7
9
|
import { parseArgs, USAGE } from './parse.js';
|
|
8
10
|
function centsToDollars(cents) {
|
|
9
|
-
return `$${(cents / 100).toFixed(2)}`;
|
|
11
|
+
return `$${(cents / 100).toFixed(Number.isInteger(cents) ? 2 : 6).replace(/(\.\d{2,}?)0+$/, '$1')}`;
|
|
10
12
|
}
|
|
11
13
|
function fail(message) {
|
|
12
14
|
console.error(message);
|
|
13
15
|
process.exit(1);
|
|
14
16
|
}
|
|
15
|
-
function skillSourcePath() {
|
|
16
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
return join(here, '..', 'SKILL.md');
|
|
18
|
-
}
|
|
19
|
-
function installSkill() {
|
|
20
|
-
const cwd = process.cwd();
|
|
21
|
-
const candidates = [join(cwd, '.cursor', 'skills'), join(cwd, '.claude', 'skills'), join(cwd, '.agents', 'skills')];
|
|
22
|
-
const parent = candidates.find((dir) => existsSync(dir)) ?? join(cwd, '.agents', 'skills');
|
|
23
|
-
const destDir = join(parent, 'bags-cash');
|
|
24
|
-
mkdirSync(destDir, { recursive: true });
|
|
25
|
-
const dest = join(destDir, 'SKILL.md');
|
|
26
|
-
const source = skillSourcePath();
|
|
27
|
-
if (!existsSync(source)) {
|
|
28
|
-
throw new Error(`bundled SKILL.md not found at ${source}`);
|
|
29
|
-
}
|
|
30
|
-
writeFileSync(dest, readFileSync(source, 'utf8'), 'utf8');
|
|
31
|
-
return dest;
|
|
32
|
-
}
|
|
33
17
|
function printDiscoverTable(endpoints) {
|
|
34
18
|
if (endpoints.length === 0) {
|
|
35
19
|
console.log('(no endpoints)');
|
|
@@ -38,7 +22,9 @@ function printDiscoverTable(endpoints) {
|
|
|
38
22
|
const rows = endpoints.map((ep) => ({
|
|
39
23
|
url: ep.url,
|
|
40
24
|
method: ep.method,
|
|
41
|
-
price: typeof ep.price_usd === 'number'
|
|
25
|
+
price: typeof ep.price_usd === 'number'
|
|
26
|
+
? `$${ep.price_usd.toFixed(6).replace(/(\.\d{2,}?)0+$/, '$1')}`
|
|
27
|
+
: String(ep.price_usd),
|
|
42
28
|
merchant: ep.merchant_name ?? '',
|
|
43
29
|
source: ep.source ?? 'bags',
|
|
44
30
|
}));
|
|
@@ -54,37 +40,30 @@ function printDiscoverTable(endpoints) {
|
|
|
54
40
|
function printJson(data) {
|
|
55
41
|
console.log(JSON.stringify(data, null, 2));
|
|
56
42
|
}
|
|
57
|
-
function policyPatchFromSets(sets) {
|
|
58
|
-
const patch = {};
|
|
59
|
-
for (const [key, value] of Object.entries(sets)) {
|
|
60
|
-
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
61
|
-
const num = Number(value);
|
|
62
|
-
patch[camel] =
|
|
63
|
-
Number.isFinite(num) && String(num) === value
|
|
64
|
-
? num
|
|
65
|
-
: value === 'true'
|
|
66
|
-
? true
|
|
67
|
-
: value === 'false'
|
|
68
|
-
? false
|
|
69
|
-
: value;
|
|
70
|
-
}
|
|
71
|
-
return patch;
|
|
72
|
-
}
|
|
73
43
|
async function main() {
|
|
74
44
|
const parsed = parseArgs(process.argv.slice(2));
|
|
75
45
|
if (parsed.command === 'help') {
|
|
76
46
|
console.log(USAGE);
|
|
77
47
|
return;
|
|
78
48
|
}
|
|
49
|
+
if (parsed.command === 'usage') {
|
|
50
|
+
console.log(parsed.message);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
79
53
|
if (parsed.command === 'error') {
|
|
80
54
|
fail(parsed.message);
|
|
81
55
|
}
|
|
82
56
|
try {
|
|
83
57
|
switch (parsed.command) {
|
|
84
58
|
case 'redeem': {
|
|
85
|
-
const result = await redeem(
|
|
59
|
+
const result = await redeem({
|
|
60
|
+
code: parsed.code,
|
|
61
|
+
mode: parsed.test ? 'test' : undefined,
|
|
62
|
+
});
|
|
86
63
|
saveCredentials(result.token);
|
|
87
|
-
console.log(
|
|
64
|
+
console.log(parsed.test
|
|
65
|
+
? `Sandbox wallet ready. Balance: ${centsToDollars(result.balance_cents)} (test mode — Base Sepolia + Solana devnet only)`
|
|
66
|
+
: `Redeemed. Balance: ${centsToDollars(result.balance_cents)}`);
|
|
88
67
|
console.log('Next steps:');
|
|
89
68
|
console.log(' npx @getbags/cash balance');
|
|
90
69
|
console.log(' npx @getbags/cash discover');
|
|
@@ -101,7 +80,7 @@ async function main() {
|
|
|
101
80
|
return;
|
|
102
81
|
}
|
|
103
82
|
case 'discover': {
|
|
104
|
-
const result = await discover(
|
|
83
|
+
const result = await discover(loadCredentials()?.token ?? '', parsed.query);
|
|
105
84
|
if (parsed.format === 'json') {
|
|
106
85
|
printJson(result);
|
|
107
86
|
}
|
|
@@ -111,7 +90,7 @@ async function main() {
|
|
|
111
90
|
return;
|
|
112
91
|
}
|
|
113
92
|
case 'search': {
|
|
114
|
-
const result = await search(
|
|
93
|
+
const result = await search(loadCredentials()?.token ?? '', parsed.query);
|
|
115
94
|
if (parsed.format === 'json') {
|
|
116
95
|
printJson(result);
|
|
117
96
|
}
|
|
@@ -178,7 +157,11 @@ async function main() {
|
|
|
178
157
|
payment_protocol: parsed.paymentProtocol,
|
|
179
158
|
});
|
|
180
159
|
if (result.payment?.paid) {
|
|
181
|
-
|
|
160
|
+
const upstreamNote = result.http_status >= 400 ? ` | upstream HTTP ${result.http_status}` : '';
|
|
161
|
+
console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}${upstreamNote}`);
|
|
162
|
+
if (shouldPrintInputSchemaHint(result.payment, result.http_status)) {
|
|
163
|
+
console.error(INPUT_SCHEMA_HINT);
|
|
164
|
+
}
|
|
182
165
|
}
|
|
183
166
|
else if (result.payment) {
|
|
184
167
|
console.error(`payment: not charged | tx ${result.payment.tx_hash || 'n/a'} | ${result.payment.network || 'n/a'}`);
|
|
@@ -242,6 +225,10 @@ async function main() {
|
|
|
242
225
|
}
|
|
243
226
|
return;
|
|
244
227
|
}
|
|
228
|
+
case 'result': {
|
|
229
|
+
printJson(await getResult(requireToken(), parsed));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
245
232
|
case 'transactions': {
|
|
246
233
|
try {
|
|
247
234
|
const result = await getTransactions(requireToken(), {
|
|
@@ -375,10 +362,41 @@ async function main() {
|
|
|
375
362
|
return;
|
|
376
363
|
}
|
|
377
364
|
case 'init': {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
365
|
+
const { exitCode } = runInitCommand({
|
|
366
|
+
dryRun: parsed.dryRun,
|
|
367
|
+
clients: parsed.clients,
|
|
368
|
+
scope: parsed.scope,
|
|
369
|
+
});
|
|
370
|
+
if (exitCode !== 0)
|
|
371
|
+
process.exit(exitCode);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
case 'mcp': {
|
|
375
|
+
// Stdio transport owns stdout for JSON-RPC — never write logs there.
|
|
376
|
+
try {
|
|
377
|
+
await runStdioServer();
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
case 'doctor': {
|
|
386
|
+
const checks = await runDoctor({
|
|
387
|
+
scope: parsed.scope,
|
|
388
|
+
requestedClients: parsed.clients.length > 0 ? parsed.clients : undefined,
|
|
389
|
+
});
|
|
390
|
+
let failed = false;
|
|
391
|
+
for (const check of checks) {
|
|
392
|
+
const mark = check.status === 'pass' ? 'pass' : check.status === 'warn' ? 'WARN' : 'FAIL';
|
|
393
|
+
console.log(`[${mark}] ${check.name}: ${check.message}`);
|
|
394
|
+
if (check.status === 'fail')
|
|
395
|
+
failed = true;
|
|
396
|
+
}
|
|
397
|
+
console.log(formatDoctorSummary(checks));
|
|
398
|
+
if (failed)
|
|
399
|
+
process.exit(1);
|
|
382
400
|
return;
|
|
383
401
|
}
|
|
384
402
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PlannedOperation } from './install-plan.js';
|
|
2
|
+
export type ApplyFs = {
|
|
3
|
+
existsSync: (path: string) => boolean;
|
|
4
|
+
mkdirSync: (path: string, opts: {
|
|
5
|
+
recursive: boolean;
|
|
6
|
+
mode?: number;
|
|
7
|
+
}) => void;
|
|
8
|
+
writeFileSync: (path: string, content: string, opts: {
|
|
9
|
+
encoding: 'utf8';
|
|
10
|
+
mode?: number;
|
|
11
|
+
}) => void;
|
|
12
|
+
copyFileSync: (src: string, dest: string) => void;
|
|
13
|
+
renameSync: (from: string, to: string) => void;
|
|
14
|
+
chmodSync: (path: string, mode: number) => void;
|
|
15
|
+
readFileSync?: (path: string, encoding: 'utf8') => string;
|
|
16
|
+
};
|
|
17
|
+
/** Strip live wallet tokens from backup payloads (defense in depth). */
|
|
18
|
+
export declare function redactSecretsForBackup(content: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Execute a plan produced by `planInstall`.
|
|
21
|
+
* Dry-run is implemented by not calling this function — there is no parallel path.
|
|
22
|
+
*
|
|
23
|
+
* Backups go to whatever `backupPath` the planner chose (must be under ~/.bags/backups).
|
|
24
|
+
* Writes are atomic: temp file in the same directory, then rename into place.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyInstallPlan(operations: PlannedOperation[], fs?: ApplyFs): {
|
|
27
|
+
written: string[];
|
|
28
|
+
backedUp: string[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const defaultFs = {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync: (path, opts) => {
|
|
6
|
+
mkdirSync(path, opts);
|
|
7
|
+
},
|
|
8
|
+
writeFileSync: (path, content, opts) => {
|
|
9
|
+
writeFileSync(path, content, opts);
|
|
10
|
+
},
|
|
11
|
+
copyFileSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
chmodSync,
|
|
14
|
+
readFileSync: (path, encoding) => readFileSync(path, encoding),
|
|
15
|
+
};
|
|
16
|
+
/** Strip live wallet tokens from backup payloads (defense in depth). */
|
|
17
|
+
export function redactSecretsForBackup(content) {
|
|
18
|
+
return content.replace(/bag_agent_sk_[A-Za-z0-9_]+/g, 'bag_agent_sk_[REDACTED]');
|
|
19
|
+
}
|
|
20
|
+
function readExisting(fs, path) {
|
|
21
|
+
if (fs.readFileSync)
|
|
22
|
+
return fs.readFileSync(path, 'utf8');
|
|
23
|
+
// Fallback for injected fs that only implements copy: copy to a side channel is unavailable.
|
|
24
|
+
// Tests always provide writeFileSync-backed content via exists + prior writes.
|
|
25
|
+
throw new Error(`readFileSync required to backup ${path}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Execute a plan produced by `planInstall`.
|
|
29
|
+
* Dry-run is implemented by not calling this function — there is no parallel path.
|
|
30
|
+
*
|
|
31
|
+
* Backups go to whatever `backupPath` the planner chose (must be under ~/.bags/backups).
|
|
32
|
+
* Writes are atomic: temp file in the same directory, then rename into place.
|
|
33
|
+
*/
|
|
34
|
+
export function applyInstallPlan(operations, fs = defaultFs) {
|
|
35
|
+
const written = [];
|
|
36
|
+
const backedUp = [];
|
|
37
|
+
for (const op of operations) {
|
|
38
|
+
const dir = dirname(op.path);
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
40
|
+
if (op.backupPath && fs.existsSync(op.path)) {
|
|
41
|
+
const backupDir = dirname(op.backupPath);
|
|
42
|
+
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
43
|
+
const raw = readExisting(fs, op.path);
|
|
44
|
+
const redacted = redactSecretsForBackup(raw);
|
|
45
|
+
fs.writeFileSync(op.backupPath, redacted, { encoding: 'utf8', mode: 0o600 });
|
|
46
|
+
fs.chmodSync(op.backupPath, 0o600);
|
|
47
|
+
backedUp.push(op.backupPath);
|
|
48
|
+
}
|
|
49
|
+
const tmpPath = join(dir, `.bags-init-${process.pid}-${Date.now()}.tmp-${Math.random().toString(16).slice(2)}`);
|
|
50
|
+
fs.writeFileSync(tmpPath, op.content, { encoding: 'utf8', mode: 0o644 });
|
|
51
|
+
fs.renameSync(tmpPath, op.path);
|
|
52
|
+
written.push(op.path);
|
|
53
|
+
}
|
|
54
|
+
return { written, backedUp };
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ClientId, type InstallScope } from './detect.js';
|
|
2
|
+
export type InitCommandOptions = {
|
|
3
|
+
dryRun: boolean;
|
|
4
|
+
clients: ClientId[];
|
|
5
|
+
scope: InstallScope;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
home?: string;
|
|
8
|
+
/** Override stdout (tests). Defaults to console.log. */
|
|
9
|
+
log?: (line: string) => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrate `bags init`: detect clients → snapshot → plan → (optional) apply.
|
|
13
|
+
* Informational when no client is detected (does not throw).
|
|
14
|
+
*/
|
|
15
|
+
export declare function runInitCommand(opts: InitCommandOptions): {
|
|
16
|
+
exitCode: number;
|
|
17
|
+
};
|