@agentgrant.cash/cli 1.1.0 → 1.3.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/cli/commands/portfolio.js +9 -3
- package/dist/cli/money-helpers.js +26 -3
- package/dist/cli/perfolio-commands/borrow.js +6 -3
- package/dist/cli/perfolio-commands/polymarket.js +12 -2
- package/dist/cli/perfolio-commands/trade.js +18 -7
- package/dist/lib/agent-client.js +25 -2
- package/dist/lib/client.js +22 -7
- package/dist/lib/money-client.js +10 -3
- package/dist/lib/money-input.js +35 -3
- package/dist/lib/portfolio-format.js +22 -3
- package/package.json +1 -1
- package/skills/grant-cash/SKILL.md +23 -1
|
@@ -141,7 +141,7 @@ async function gatherExtras(ctx) {
|
|
|
141
141
|
}
|
|
142
142
|
async function gather(cmd) {
|
|
143
143
|
const ctx = buildContext(cmd);
|
|
144
|
-
const [prefsR, moneyR, extrasR, agentR] = await Promise.allSettled([
|
|
144
|
+
const [prefsR, moneyR, extrasR, agentR, creditR] = await Promise.allSettled([
|
|
145
145
|
ctx.money.connected ? loadDisplayPrefs(ctx) : Promise.resolve(DEFAULT_PREFS),
|
|
146
146
|
ctx.money.connected
|
|
147
147
|
? ctx.money.getPortfolio("full")
|
|
@@ -150,6 +150,9 @@ async function gather(cmd) {
|
|
|
150
150
|
ctx.agent.connected
|
|
151
151
|
? ctx.agent.balance()
|
|
152
152
|
: Promise.resolve(null),
|
|
153
|
+
ctx.agent.connected
|
|
154
|
+
? ctx.agent.credit()
|
|
155
|
+
: Promise.resolve(null),
|
|
153
156
|
]);
|
|
154
157
|
return {
|
|
155
158
|
ctx,
|
|
@@ -159,6 +162,7 @@ async function gather(cmd) {
|
|
|
159
162
|
moneyFailed: moneyR.status === "rejected",
|
|
160
163
|
agent: agentR.status === "fulfilled" ? agentR.value : null,
|
|
161
164
|
agentFailed: agentR.status === "rejected",
|
|
165
|
+
credit: creditR.status === "fulfilled" ? creditR.value : null,
|
|
162
166
|
};
|
|
163
167
|
}
|
|
164
168
|
/** Indent every line of a block by two spaces. */
|
|
@@ -171,7 +175,7 @@ function indent(block) {
|
|
|
171
175
|
export function registerPortfolio(program) {
|
|
172
176
|
const run = async (cmd) => {
|
|
173
177
|
const g = await gather(cmd);
|
|
174
|
-
const { ctx, prefs, money, extras, moneyFailed, agent, agentFailed } = g;
|
|
178
|
+
const { ctx, prefs, money, extras, moneyFailed, agent, agentFailed, credit } = g;
|
|
175
179
|
emit(ctx, {
|
|
176
180
|
investments: money
|
|
177
181
|
? {
|
|
@@ -186,6 +190,8 @@ export function registerPortfolio(program) {
|
|
|
186
190
|
}
|
|
187
191
|
: null,
|
|
188
192
|
spending: agent,
|
|
193
|
+
// The free-credit / wallet split (parity with the app's Spending screen).
|
|
194
|
+
credit,
|
|
189
195
|
}, () => {
|
|
190
196
|
if (!ctx.money.connected && !ctx.agent.connected)
|
|
191
197
|
return "Not connected yet. Run `grant login`.";
|
|
@@ -208,7 +214,7 @@ export function registerPortfolio(program) {
|
|
|
208
214
|
lines.push(ui.dim(" Not connected."));
|
|
209
215
|
}
|
|
210
216
|
else if (agent) {
|
|
211
|
-
lines.push(formatAgentFunds(agent));
|
|
217
|
+
lines.push(formatAgentFunds(agent, credit));
|
|
212
218
|
}
|
|
213
219
|
else {
|
|
214
220
|
lines.push(ui.amber(` ${agentFailed ? "Temporarily unavailable." : "—"}`));
|
|
@@ -121,15 +121,38 @@ export async function loadDisplayPrefs(ctx) {
|
|
|
121
121
|
return DEFAULT_PREFS;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Build the money-side SUCCESS envelope. The whole CLI — money and agent sides —
|
|
126
|
+
* speaks one contract: `{ ok: true, ...data }`. A plain object is spread in; an
|
|
127
|
+
* array / primitive is nested under `data` (spreading an array would splay its
|
|
128
|
+
* indices as keys). A redundant top-level `success` marker is dropped so `ok` is
|
|
129
|
+
* the single source of truth (closes the "trade cmds use {success:true}" gap).
|
|
130
|
+
* Pure + exported for unit testing.
|
|
131
|
+
*/
|
|
132
|
+
export function buildOutPayload(data) {
|
|
133
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
134
|
+
const { success: _drop, ...rest } = data;
|
|
135
|
+
void _drop;
|
|
136
|
+
return { ok: true, ...rest };
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, data };
|
|
139
|
+
}
|
|
140
|
+
/** Build the money-side FAILURE envelope: `{ ok:false, error:{code,message,recoverable} }`. */
|
|
141
|
+
export function buildErrorPayload(msg, opts) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: { code: opts?.code ?? "ERROR", message: msg, recoverable: opts?.recoverable ?? false },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
124
147
|
export function out(ctx, human, data) {
|
|
125
148
|
if (ctx.json)
|
|
126
|
-
console.log(JSON.stringify(data, null, 2));
|
|
149
|
+
console.log(JSON.stringify(buildOutPayload(data), null, 2));
|
|
127
150
|
else
|
|
128
151
|
console.log(human);
|
|
129
152
|
}
|
|
130
|
-
export function fail(msg, json) {
|
|
153
|
+
export function fail(msg, json, opts) {
|
|
131
154
|
if (json)
|
|
132
|
-
console.error(JSON.stringify(
|
|
155
|
+
console.error(JSON.stringify(buildErrorPayload(msg, opts)));
|
|
133
156
|
else
|
|
134
157
|
console.error(pc.red(`✖ ${msg}`));
|
|
135
158
|
process.exit(1);
|
|
@@ -10,24 +10,26 @@ export function registerBorrow(program) {
|
|
|
10
10
|
};
|
|
11
11
|
program.command('borrow').description('Borrow cash against an asset')
|
|
12
12
|
.requiredOption('--against <asset>').requiredOption('--amount <cash>')
|
|
13
|
+
.option('--usd', 'interpret --amount as US dollars, ignoring your display currency')
|
|
13
14
|
.option('--no-wait', 'return immediately without waiting for confirmation')
|
|
14
15
|
.action(async (opts, cmd) => {
|
|
15
16
|
const ctx = buildCtx(cmd);
|
|
16
17
|
requireAuth(ctx);
|
|
17
18
|
const r = await resolver(ctx);
|
|
18
19
|
const collateral = r.resolve(opts.against).symbol;
|
|
19
|
-
const m = await resolveCash(ctx, opts.amount, CASH_DECIMALS);
|
|
20
|
+
const m = await resolveCash(ctx, opts.amount, CASH_DECIMALS, { usd: !!opts.usd });
|
|
20
21
|
await exec(ctx, () => ctx.client.borrow({ collateral, loan: CASH, amount: m.usdString }), opts.wait !== false, `Borrow ${describeCashResolution(m)} against ${opts.against}`);
|
|
21
22
|
});
|
|
22
23
|
program.command('repay').description('Repay borrowed cash')
|
|
23
24
|
.requiredOption('--against <asset>').requiredOption('--amount <cash>')
|
|
25
|
+
.option('--usd', 'interpret --amount as US dollars, ignoring your display currency')
|
|
24
26
|
.option('--no-wait', 'return immediately without waiting for confirmation')
|
|
25
27
|
.action(async (opts, cmd) => {
|
|
26
28
|
const ctx = buildCtx(cmd);
|
|
27
29
|
requireAuth(ctx);
|
|
28
30
|
const r = await resolver(ctx);
|
|
29
31
|
const collateral = r.resolve(opts.against).symbol;
|
|
30
|
-
const m = await resolveCash(ctx, opts.amount, CASH_DECIMALS);
|
|
32
|
+
const m = await resolveCash(ctx, opts.amount, CASH_DECIMALS, { usd: !!opts.usd });
|
|
31
33
|
await exec(ctx, () => ctx.client.repay({ collateral, loan: CASH, amount: m.usdString }), opts.wait !== false, `Repay ${describeCashResolution(m)} against ${opts.against}`);
|
|
32
34
|
});
|
|
33
35
|
program.command('add-collateral').description('Add collateral')
|
|
@@ -52,6 +54,7 @@ export function registerBorrow(program) {
|
|
|
52
54
|
});
|
|
53
55
|
program.command('leverage').description('Add collateral and borrow cash in one step')
|
|
54
56
|
.requiredOption('--asset <asset>').requiredOption('--deposit <n>').requiredOption('--borrow <cash>')
|
|
57
|
+
.option('--usd', 'interpret --borrow as US dollars, ignoring your display currency')
|
|
55
58
|
.option('--no-wait', 'return immediately without waiting for confirmation')
|
|
56
59
|
.action(async (opts, cmd) => {
|
|
57
60
|
const ctx = buildCtx(cmd);
|
|
@@ -59,7 +62,7 @@ export function registerBorrow(program) {
|
|
|
59
62
|
const r = await resolver(ctx);
|
|
60
63
|
const a = r.resolve(opts.asset);
|
|
61
64
|
// --deposit is a quantity of the collateral asset; --borrow is cash (currency-aware).
|
|
62
|
-
const borrow = await resolveCash(ctx, opts.borrow, CASH_DECIMALS);
|
|
65
|
+
const borrow = await resolveCash(ctx, opts.borrow, CASH_DECIMALS, { usd: !!opts.usd });
|
|
63
66
|
await exec(ctx, () => ctx.client.supplyAndBorrow({ collateral: a.symbol, loan: CASH, collateralAmount: assertDecimal(opts.deposit, a.decimals), borrowAmount: borrow.usdString }), opts.wait !== false, `Leverage ${opts.asset} (borrow ${describeCashResolution(borrow)})`);
|
|
64
67
|
});
|
|
65
68
|
program.command('close').description('Close a loan (repay all + withdraw collateral)')
|
|
@@ -4,10 +4,20 @@ import { describeCashResolution, formatMoney, displayDescriptor, fetchPusdBalanc
|
|
|
4
4
|
const PREDICTION_DECIMALS = 6;
|
|
5
5
|
/** Friendly message shown when the betting path isn't enabled yet (503). */
|
|
6
6
|
const BETTING_DISABLED_MSG = 'Prediction betting isn\'t available yet — market browsing is read-only for now.';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* True if an error means the betting path is unavailable, so BOTH `--side yes`
|
|
9
|
+
* and `--side no` surface the same friendly line. The configured-off state comes
|
|
10
|
+
* back as 503 / POLYMARKET_TRADING_NOT_CONFIGURED, but the disabled `no` path was
|
|
11
|
+
* observed returning a bare upstream 502 (and 504 is the same class of gateway
|
|
12
|
+
* outage) — treat those as unavailable too rather than leaking a raw "HTTP 502".
|
|
13
|
+
* Exported for unit testing.
|
|
14
|
+
*/
|
|
8
15
|
export function isTradingDisabled(err) {
|
|
9
16
|
const e = err;
|
|
10
|
-
return e?.status ===
|
|
17
|
+
return (e?.status === 502 ||
|
|
18
|
+
e?.status === 503 ||
|
|
19
|
+
e?.status === 504 ||
|
|
20
|
+
e?.code === 'POLYMARKET_TRADING_NOT_CONFIGURED');
|
|
11
21
|
}
|
|
12
22
|
/** Render a market list into human-readable lines. Exported for unit testing. */
|
|
13
23
|
export function formatMarkets(markets, count) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCtx, requireAuth, resolver, reportTx, resolveCash, resolveCashTargetQty, out, fail } from '../money-helpers.js';
|
|
1
|
+
import { buildCtx, requireAuth, resolver, reportTx, resolveCash, resolveCashTargetQty, forceUsdAmount, out, fail } from '../money-helpers.js';
|
|
2
2
|
import { assertDecimal, describeCashResolution } from '../../lib/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Resolve the sell-side `sellAmount` (always in `from`-token units) for a trade.
|
|
@@ -17,12 +17,13 @@ async function resolveSellSide(ctx, from, spec) {
|
|
|
17
17
|
fail('Use --amount for cash; --for is for selling an asset by cash value.', ctx.json);
|
|
18
18
|
if (!spec.amount)
|
|
19
19
|
fail('Missing --amount (cash to spend).', ctx.json);
|
|
20
|
-
const m = await resolveCash(ctx, spec.amount, from.decimals);
|
|
20
|
+
const m = await resolveCash(ctx, spec.amount, from.decimals, { usd: spec.usd });
|
|
21
21
|
return { sellAmount: m.usdString, note: describeCashResolution(m) };
|
|
22
22
|
}
|
|
23
23
|
// Asset source, cash target → size the quantity from the live price.
|
|
24
24
|
if (spec.forCash) {
|
|
25
|
-
|
|
25
|
+
// `--usd` forces the cash target to dollars (prefix "$") before sizing.
|
|
26
|
+
const r = await resolveCashTargetQty(ctx, from, spec.usd ? forceUsdAmount(spec.forCash) : spec.forCash);
|
|
26
27
|
return {
|
|
27
28
|
sellAmount: r.quantity,
|
|
28
29
|
note: `${describeCashResolution(r)} of ${from.friendly} (≈ ${r.quantity} ${from.friendly})`,
|
|
@@ -40,13 +41,14 @@ function oneOfAmountOrFor(ctx, opts) {
|
|
|
40
41
|
fail('Pass either --amount (a quantity) or --for (a cash value), not both.', ctx.json);
|
|
41
42
|
if (!opts.amount && !opts.for)
|
|
42
43
|
fail('Specify --amount <quantity> or --for <cash value>.', ctx.json);
|
|
43
|
-
return { amount: opts.amount, forCash: opts.for };
|
|
44
|
+
return { amount: opts.amount, forCash: opts.for, usd: opts.usd };
|
|
44
45
|
}
|
|
45
46
|
export function registerTrade(program) {
|
|
46
47
|
program.command('quote').description('Preview a trade (no execution)')
|
|
47
48
|
.requiredOption('--from <asset>').requiredOption('--to <asset>')
|
|
48
49
|
.option('--amount <n>', 'quantity of the "from" asset')
|
|
49
50
|
.option('--for <cash>', 'cash value to sell (e.g. 100, "$50", "₹100")')
|
|
51
|
+
.option('--usd', 'interpret a cash --for value as US dollars, ignoring your display currency')
|
|
50
52
|
.action(async (opts, cmd) => {
|
|
51
53
|
const ctx = buildCtx(cmd);
|
|
52
54
|
requireAuth(ctx);
|
|
@@ -72,23 +74,32 @@ export function registerTrade(program) {
|
|
|
72
74
|
};
|
|
73
75
|
program.command('buy <asset>').description('Buy an asset with cash')
|
|
74
76
|
.requiredOption('--amount <cash>', 'amount of cash to spend (your currency; prefix $/₹ to force one)')
|
|
77
|
+
.option('--usd', 'interpret --amount as US dollars, ignoring your display currency')
|
|
75
78
|
.option('--no-wait', 'return immediately without waiting for on-chain confirmation')
|
|
76
|
-
.action((asset, opts, cmd) => trade(cmd, 'cash', asset, { amount: opts.amount }, opts.wait !== false));
|
|
79
|
+
.action((asset, opts, cmd) => trade(cmd, 'cash', asset, { amount: opts.amount, usd: opts.usd }, opts.wait !== false));
|
|
77
80
|
program.command('sell <asset>').description('Sell an asset — by quantity (--amount) or by cash value (--for)')
|
|
78
81
|
.option('--amount <n>', 'quantity of the asset to sell (e.g. 0.1 bitcoin)')
|
|
79
82
|
.option('--for <cash>', 'cash value to sell instead (e.g. 100, "$50", "₹100")')
|
|
83
|
+
.option('--usd', 'interpret a cash --for value as US dollars, ignoring your display currency')
|
|
80
84
|
.option('--no-wait', 'return immediately without waiting for on-chain confirmation')
|
|
81
85
|
.action((asset, opts, cmd) => {
|
|
82
86
|
const ctx = buildCtx(cmd);
|
|
83
|
-
|
|
87
|
+
// MUST return the promise: commander's parseAsync(...).catch() only routes a
|
|
88
|
+
// rejection through the friendly {ok:false} envelope if the action returns it.
|
|
89
|
+
// Dropping it (a brace body without `return`) orphans the rejection → raw
|
|
90
|
+
// Node stack trace (the reported `convert`/`sell` crash on PAIR_NOT_SUPPORTED).
|
|
91
|
+
return trade(cmd, asset, 'cash', oneOfAmountOrFor(ctx, opts), opts.wait !== false);
|
|
84
92
|
});
|
|
85
93
|
program.command('convert').description('Convert one asset into another — by quantity (--amount) or cash value (--for)')
|
|
86
94
|
.requiredOption('--from <asset>').requiredOption('--to <asset>')
|
|
87
95
|
.option('--amount <n>', 'quantity of the "from" asset')
|
|
88
96
|
.option('--for <cash>', 'cash value of "from" to convert (e.g. 100, "$50", "₹100")')
|
|
97
|
+
.option('--usd', 'interpret a cash --for value as US dollars, ignoring your display currency')
|
|
89
98
|
.option('--no-wait', 'return immediately without waiting for on-chain confirmation')
|
|
90
99
|
.action((opts, cmd) => {
|
|
91
100
|
const ctx = buildCtx(cmd);
|
|
92
|
-
|
|
101
|
+
// Return the promise (see `sell` above) — without it a rejected convert
|
|
102
|
+
// (e.g. PAIR_NOT_SUPPORTED) escapes as an unhandled rejection / raw stack.
|
|
103
|
+
return trade(cmd, opts.from, opts.to, oneOfAmountOrFor(ctx, opts), opts.wait !== false);
|
|
93
104
|
});
|
|
94
105
|
}
|
package/dist/lib/agent-client.js
CHANGED
|
@@ -8,6 +8,16 @@ import { AppError } from "./errors.js";
|
|
|
8
8
|
* a driving agent knows whether to retry (see SKILL.md error playbook).
|
|
9
9
|
*/
|
|
10
10
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* 4xx codes that are nonetheless safe to retry, per the SKILL error playbook.
|
|
13
|
+
* Without this, recoverability was inferred purely from `status >= 500`, so a
|
|
14
|
+
* free-reject like SCHEMA_VALIDATION_FAILED (422) was wrongly tagged
|
|
15
|
+
* `recoverable:false` even though the fix is "correct the body and retry".
|
|
16
|
+
*/
|
|
17
|
+
const RECOVERABLE_CODES = new Set([
|
|
18
|
+
"SCHEMA_VALIDATION_FAILED", // free reject — fix the body, retry
|
|
19
|
+
"X402_FACILITATOR_TIMEOUT", // transient settlement timeout — retry
|
|
20
|
+
]);
|
|
11
21
|
export class AgentClient {
|
|
12
22
|
base;
|
|
13
23
|
apiKey;
|
|
@@ -56,10 +66,14 @@ export class AgentClient {
|
|
|
56
66
|
throw new AppError(`No Grant Cash agent API at ${this.base} (HTTP ${res.status}) — check GRANTCASH_AGENT_URL.`, { status: res.status, code: "BACKEND_UNREACHABLE", recoverable: false });
|
|
57
67
|
}
|
|
58
68
|
if (!res.ok || json?.error?.code) {
|
|
69
|
+
const code = json?.error?.code ?? `HTTP_${res.status}`;
|
|
70
|
+
// Honor an explicit backend `recoverable` if present; otherwise infer it —
|
|
71
|
+
// 5xx (transient) OR a known-recoverable 4xx code (e.g. a free reject).
|
|
72
|
+
const recoverable = json?.error?.recoverable ?? (res.status >= 500 || RECOVERABLE_CODES.has(code));
|
|
59
73
|
throw new AppError(json?.error?.message ?? `Request failed (${res.status})`, {
|
|
60
74
|
status: res.status,
|
|
61
|
-
code
|
|
62
|
-
recoverable
|
|
75
|
+
code,
|
|
76
|
+
recoverable,
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
return json;
|
|
@@ -74,6 +88,15 @@ export class AgentClient {
|
|
|
74
88
|
balance() {
|
|
75
89
|
return this.request("GET", "/balance", { auth: true });
|
|
76
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The chain-agnostic spendable split — sign-up free credit + funds you added +
|
|
93
|
+
* total. Hits `GET /credit` (dual-auth, so the api-key works). This is the same
|
|
94
|
+
* figure the app shows on the Spending screen; `balance()` (/balance) carries
|
|
95
|
+
* the session status, so the portfolio uses both.
|
|
96
|
+
*/
|
|
97
|
+
credit() {
|
|
98
|
+
return this.request("GET", "/credit", { auth: true });
|
|
99
|
+
}
|
|
77
100
|
/**
|
|
78
101
|
* Find a payable endpoint in the curated catalog. Hits `GET /marketplace/x402`
|
|
79
102
|
* (the grouped, public catalog route) — NOT `/marketplace`, which does not
|
package/dist/lib/client.js
CHANGED
|
@@ -70,13 +70,23 @@ export class PerfolioClient {
|
|
|
70
70
|
if (res.status === 428)
|
|
71
71
|
throw new NoSessionError();
|
|
72
72
|
if (!res.ok || json.success === false) {
|
|
73
|
-
|
|
73
|
+
// The `error` field comes in two shapes across backends: a flat string, or a
|
|
74
|
+
// nested `{ code, message, recoverable }` object (kyc / beneficiaries / fiat).
|
|
75
|
+
// Normalize BOTH — never let an object reach `new Error()`, which would
|
|
76
|
+
// stringify it to the useless "[object Object]" the report flagged.
|
|
77
|
+
const rawErr = json.error;
|
|
78
|
+
const nested = rawErr && typeof rawErr === "object"
|
|
79
|
+
? rawErr
|
|
80
|
+
: undefined;
|
|
81
|
+
const errString = typeof rawErr === "string" ? rawErr : undefined;
|
|
82
|
+
const message = nested?.message || errString || json.message || `HTTP ${res.status}`;
|
|
83
|
+
const err = new Error(message);
|
|
74
84
|
err.status = res.status;
|
|
75
|
-
// Prefer the backend's machine code (e.g. 'MIN_ORDER_NOT_MET'
|
|
76
|
-
// the legacy convention where the `error` field
|
|
77
|
-
// (e.g. 'GRANT_REQUIRED'
|
|
78
|
-
|
|
79
|
-
err.
|
|
85
|
+
// Prefer the backend's machine code (e.g. 'MIN_ORDER_NOT_MET' / a nested
|
|
86
|
+
// error.code); fall back to the legacy convention where the `error` field
|
|
87
|
+
// itself carried a code-like string (e.g. 'GRANT_REQUIRED').
|
|
88
|
+
err.code = json.code ?? nested?.code ?? errString;
|
|
89
|
+
err.recoverable = json.recoverable ?? nested?.recoverable;
|
|
80
90
|
err.details = json.details;
|
|
81
91
|
throw err;
|
|
82
92
|
}
|
|
@@ -251,7 +261,12 @@ export class PerfolioClient {
|
|
|
251
261
|
hlTpsl(b) {
|
|
252
262
|
return this.post(this.urls.api, '/hl/tpsl', b);
|
|
253
263
|
}
|
|
254
|
-
hlCancelOrder(b) {
|
|
264
|
+
hlCancelOrder(b) {
|
|
265
|
+
// The backend route reads `req.body.orderId`; send BOTH `orderId` (canonical)
|
|
266
|
+
// and `oid` (alias) so a cancel works regardless of which name the deployed
|
|
267
|
+
// backend expects — fixes the "Missing orderId" the report hit.
|
|
268
|
+
return this.post(this.urls.api, '/hl/cancel-order', { orderId: b.oid, oid: b.oid, asset: b.asset });
|
|
269
|
+
}
|
|
255
270
|
/**
|
|
256
271
|
* Autonomous USDT → Hyperliquid deposit. Hits POST /tx/bridge-deposit, which
|
|
257
272
|
* builds the USDT-approve + Across bridge calls and executes them via the
|
package/dist/lib/money-client.js
CHANGED
|
@@ -84,11 +84,18 @@ export class MoneyClient {
|
|
|
84
84
|
if (res.status === 428)
|
|
85
85
|
throw new NoSessionError();
|
|
86
86
|
if (!res.ok || json.success === false) {
|
|
87
|
-
|
|
87
|
+
// `error` may be a flat string or a nested { code, message, recoverable }
|
|
88
|
+
// object — normalize both so an object never becomes "[object Object]".
|
|
89
|
+
const rawErr = json.error;
|
|
90
|
+
const nested = rawErr && typeof rawErr === "object"
|
|
91
|
+
? rawErr
|
|
92
|
+
: undefined;
|
|
93
|
+
const errString = typeof rawErr === "string" ? rawErr : undefined;
|
|
94
|
+
throw new AppError(nested?.message || errString || json.message || `HTTP ${res.status}`, {
|
|
88
95
|
status: res.status,
|
|
89
|
-
code: json.code ??
|
|
96
|
+
code: json.code ?? nested?.code ?? errString,
|
|
90
97
|
details: json.details,
|
|
91
|
-
recoverable: res.status >= 500,
|
|
98
|
+
recoverable: json.recoverable ?? nested?.recoverable ?? res.status >= 500,
|
|
92
99
|
});
|
|
93
100
|
}
|
|
94
101
|
return json.data;
|
package/dist/lib/money-input.js
CHANGED
|
@@ -77,6 +77,34 @@ function roundToDecimalString(n, decimals) {
|
|
|
77
77
|
export function defaultInputCurrency(prefs) {
|
|
78
78
|
return usesLocalCurrency(prefs) ? prefs.currency.toUpperCase() : 'USD';
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Per-transaction USD ceiling — a fat-finger guard, NOT a product limit. Combined
|
|
82
|
+
* with the display-currency default (a bare "100" can mean a large local amount),
|
|
83
|
+
* an un-bounded input is a real over-spend risk, so anything above this is refused
|
|
84
|
+
* with a recoverable error rather than quietly accepted.
|
|
85
|
+
*/
|
|
86
|
+
export const MAX_CASH_USD = 10_000_000;
|
|
87
|
+
/** Tag an error with a machine code + recoverable flag for the {ok:false} envelope. */
|
|
88
|
+
function amountError(message, code) {
|
|
89
|
+
const e = new Error(message);
|
|
90
|
+
e.code = code;
|
|
91
|
+
e.recoverable = true; // the user can simply re-enter a sane amount
|
|
92
|
+
return e;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reject amounts that resolved to dust (rounds to $0 → opaque SWAP_QUOTE_FAILED
|
|
96
|
+
* downstream) or to an implausibly large value (fat-finger). `describe` renders
|
|
97
|
+
* the user's own amount for the message.
|
|
98
|
+
*/
|
|
99
|
+
export function assertUsdInBounds(usd, describe) {
|
|
100
|
+
if (!(usd > 0)) {
|
|
101
|
+
throw amountError(`${describe()} is too small to transact — please enter a larger amount.`, 'AMOUNT_TOO_SMALL');
|
|
102
|
+
}
|
|
103
|
+
if (usd > MAX_CASH_USD) {
|
|
104
|
+
throw amountError(`${describe()} exceeds the $${MAX_CASH_USD.toLocaleString('en-US')} per-transaction safety limit. ` +
|
|
105
|
+
`If that's really intended, split it into smaller amounts.`, 'AMOUNT_TOO_LARGE');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
80
108
|
/**
|
|
81
109
|
* Resolve a user-entered cash amount into a USD decimal string for the backend.
|
|
82
110
|
*
|
|
@@ -90,6 +118,10 @@ export async function resolveCashToUsd(raw, prefs, decimals, fetchRate) {
|
|
|
90
118
|
const sourceCurrency = (parsed.currency ?? defaultInputCurrency(prefs)).toUpperCase();
|
|
91
119
|
if (sourceCurrency === 'USD') {
|
|
92
120
|
const usdString = assertDecimal(roundToDecimalString(parsed.amount, decimals), decimals);
|
|
121
|
+
// Guard BEFORE returning: a sub-cent USD dust amount rounds to "0" here and
|
|
122
|
+
// would otherwise reach the backend as an opaque SWAP_QUOTE_FAILED; a giant
|
|
123
|
+
// amount is a fat-finger. The rounded value catches both (0 → too-small).
|
|
124
|
+
assertUsdInBounds(Number(usdString), () => fmtCurrency(parsed.amount, 'USD'));
|
|
93
125
|
return {
|
|
94
126
|
usd: Number(usdString),
|
|
95
127
|
usdString,
|
|
@@ -112,9 +144,9 @@ export async function resolveCashToUsd(raw, prefs, decimals, fetchRate) {
|
|
|
112
144
|
}
|
|
113
145
|
const usd = parsed.amount / rate;
|
|
114
146
|
const usdString = roundToDecimalString(usd, decimals);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
// Same dust + fat-finger guard as the USD branch, but described in the user's
|
|
148
|
+
// own currency (e.g. "₹50.00 is too small…").
|
|
149
|
+
assertUsdInBounds(Number(usdString), () => fmtCurrency(parsed.amount, sourceCurrency));
|
|
118
150
|
return {
|
|
119
151
|
usd: Number(usdString),
|
|
120
152
|
usdString: assertDecimal(usdString, decimals),
|
|
@@ -189,8 +189,15 @@ function usdcMinorPlain(minor) {
|
|
|
189
189
|
const n = Number(minor) / 1e6;
|
|
190
190
|
return `$${n.toFixed(n !== 0 && Math.abs(n) < 0.01 ? 4 : 2)}`;
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Render the agent (spending) funds block as indented lines (no section title).
|
|
194
|
+
*
|
|
195
|
+
* When the chain-agnostic `credit` read (GET /credit) is supplied, we render the
|
|
196
|
+
* full split — total spendable, the sign-up free credit, and (if the user has
|
|
197
|
+
* funded a wallet) the funds they added — for parity with the app. Without it we
|
|
198
|
+
* fall back to the `/balance` status shape (USDC/USDT or sign-up-credit total).
|
|
199
|
+
*/
|
|
200
|
+
export function formatAgentFunds(bal, credit) {
|
|
194
201
|
const status = bal.status;
|
|
195
202
|
if (status === "not_set_up" || status === "no_active_session") {
|
|
196
203
|
return [
|
|
@@ -203,10 +210,22 @@ export function formatAgentFunds(bal) {
|
|
|
203
210
|
}
|
|
204
211
|
if (status === "credit_exhausted") {
|
|
205
212
|
return [
|
|
206
|
-
` Sign-up credit used up — balance ${usdcMinorPlain(bal.totalMinor)}`,
|
|
213
|
+
` Sign-up credit used up — balance ${usdcMinorPlain(bal.totalMinor ?? credit?.totalMinor)}`,
|
|
207
214
|
" Add funds to keep spending (`grant fetch`/`grant transfer` need a balance).",
|
|
208
215
|
].join("\n");
|
|
209
216
|
}
|
|
217
|
+
// Prefer the chain-agnostic credit split (free credit + funds you added) when
|
|
218
|
+
// available — this is the same figure the app shows on the Spending screen.
|
|
219
|
+
if (credit && credit.totalMinor !== undefined) {
|
|
220
|
+
const lines = [` Spendable: ${usdcMinorPlain(credit.totalMinor)}`];
|
|
221
|
+
lines.push(` Free credit: ${usdcMinorPlain(credit.grantMinor)}`);
|
|
222
|
+
if (credit.walletMinor !== undefined) {
|
|
223
|
+
lines.push(` Your wallet: ${usdcMinorPlain(credit.walletMinor)}`);
|
|
224
|
+
}
|
|
225
|
+
if (bal.address)
|
|
226
|
+
lines.push(` Address: ${bal.address}`);
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
210
229
|
// active. A funded on-chain session carries live USDC/USDT; a grant-only user
|
|
211
230
|
// carries the chain-agnostic sign-up credit total in minor units.
|
|
212
231
|
if (bal.USDC !== undefined || bal.USDT !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentgrant.cash/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Grant Cash — one CLI for your money (gold) and your agent (pay-per-use services). Routes to the Perfolio backend and the Agent-mode backend behind a single, plain-language surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,13 +34,17 @@ Throughout this skill, **`grant <command>` is shorthand for `npx @agentgrant.cas
|
|
|
34
34
|
## Output (for assistants driving this CLI)
|
|
35
35
|
|
|
36
36
|
- Output is **JSON** when stdout is not a TTY (you captured it) or with `--json`; pretty for people otherwise.
|
|
37
|
-
- Success → `{"ok":true, ...}`. Failure → `{"ok":false,"error":{"code","message","recoverable"}}`.
|
|
37
|
+
- Success → `{"ok":true, ...}`. Failure → `{"ok":false,"error":{"code","message","recoverable"}}`. This is uniform across **every** command — Investments and Spending alike — so always key off `ok`, never a per-command field.
|
|
38
38
|
- `recoverable:true` → safe to retry. `recoverable:false` → STOP; don't retry blindly.
|
|
39
39
|
|
|
40
40
|
## Money amounts (important)
|
|
41
41
|
|
|
42
42
|
Every **cash** amount is in the user's display currency — a bare `50` means ₹50 for an INR user, $50 for a USD user. Force a currency by prefixing: `"$50"`, `"₹100"`, `"100 EUR"`. This applies everywhere cash is entered: `buy --amount`, `borrow --amount`, prediction bets/deposits, `withdraw`, and so on. Asset *quantities* (e.g. `sell bitcoin --amount 0.1`) are not currency.
|
|
43
43
|
|
|
44
|
+
- **Force US dollars with `--usd`.** Every cash command accepts `--usd` to interpret a bare amount as dollars regardless of display currency: `buy`, `sell --for`, `convert --for`, `quote --for`, `borrow`, `repay`, `leverage --borrow`, `hl deposit/withdraw/open`, and `polymarket bet/deposit`. (`--usd` + an already-prefixed amount like `"₹50"` leaves the explicit currency untouched.)
|
|
45
|
+
- **The CLI echoes what it interpreted.** When a conversion happens, responses show both sides — e.g. `"₹50.00 (≈ $0.60)"` — so the display-currency default never silently surprises anyone. Relay that to the user.
|
|
46
|
+
- **Bounds.** A dust amount that rounds to ~$0 is refused cleanly (`AMOUNT_TOO_SMALL`, recoverable — enter more), and an implausibly large amount is refused as a fat-finger guard (`AMOUNT_TOO_LARGE`, recoverable).
|
|
47
|
+
|
|
44
48
|
## Workflow
|
|
45
49
|
|
|
46
50
|
1. `grant status` — confirm you're connected (both Investments and Spending).
|
|
@@ -126,10 +130,24 @@ Every **cash** amount is in the user's display currency — a bare `50` means
|
|
|
126
130
|
- `grant schema <slug>` — the exact pay contract for a catalogued service (no charge).
|
|
127
131
|
- `grant fetch <url> [-m POST] [-b '<json>'] [--price <amount>] [--asset USDC|USDT]` (alias `pay`) — pay for and run a service.
|
|
128
132
|
- `grant transfer <recipient> <amount>` — send money to an address.
|
|
133
|
+
|
|
134
|
+
**Calling a service with `grant fetch` — what to know:**
|
|
135
|
+
- **The response body is base64-encoded.** `result.upstreamResponse.body` is base64 — decode it to read the service's JSON/text. (It's encoded so binary or non-JSON responses survive transit intact.)
|
|
136
|
+
- **POST-with-body is the most reliable shape.** For a POST service, build the JSON `-b '<json>'` from the contract `grant check <url>` / `grant schema <slug>` returns.
|
|
137
|
+
- **GET services with required query params:** run `grant check <url>` first; if the price can't be inferred, pass it explicitly with `--price <amount>` (the value comes from `check`). Put the query params in the URL itself; do **not** send them in `-b` (a GET has no body, so that fails `SCHEMA_VALIDATION_FAILED`).
|
|
138
|
+
- **Payment guardrails (so you don't hit them blind):** `fetch` only pays **https** origins that are **public** — `http://`, `localhost`, `127.0.0.1`, LAN/private IPs and metadata IPs are blocked (anti-SSRF). And the origin must be in the catalog or one you've added via `grant discover` — an un-allowlisted origin returns `MARKETPLACE_NOT_ALLOWED`. A failed call that never settled does not charge you.
|
|
139
|
+
- `grant spending deposit --amount <cash>` (aliases `add`, `top-up`) — move cash from the Investments side into the Spending balance (the agent's pay-per-use money). Amount is in the user's currency — a bare `50` means ₹50 for an INR user, $50 for a USD user. Funds land a few minutes after submitting. There is no reverse: Spending is funded from cash, not the other way around.
|
|
129
140
|
- `grant redeem <code>` — claim an invite or bonus credit toward spending.
|
|
130
141
|
- `grant upgrade` — get a one-time link to secure a full account and add spending funds.
|
|
131
142
|
- `grant revoke` (alias `stop`) — stop all agent spending immediately.
|
|
132
143
|
|
|
144
|
+
**Keeping Spending funded (assistant guidance).** The Spending balance only goes down as the agent pays for services — top it up from the user's cash with `grant spending deposit`:
|
|
145
|
+
- **User asks explicitly** ("move ₹2,000 to spending", "top up my spending balance") → run `grant spending deposit --amount <cash>` directly. No need to ask again.
|
|
146
|
+
- **Running low** (a paid action is about to exceed — or only barely fits — the remaining Spending balance) → before paying, tell the user the balance is low and ask whether to transfer some cash over first. Don't transfer without a yes.
|
|
147
|
+
- **Empty / zero balance** (a `grant fetch`/pay fails for lack of funds, or `grant portfolio` shows Spending at 0) → tell the user their Spending account is empty and suggest moving some money in, **quoted in their local currency** — e.g. *"Your spending balance is empty — shall I move ₹2,000 (≈ $24) over from your cash?"* Then, on confirmation, run `grant spending deposit`.
|
|
148
|
+
|
|
149
|
+
Always state amounts in the user's display currency (never raw USD/USDT or chains), and only transfer proactively after the user agrees — an explicit request is the go-ahead; a low/empty balance is a prompt to ask.
|
|
150
|
+
|
|
133
151
|
### Transactions & self-orientation
|
|
134
152
|
- `grant tx status <txHash>` — check whether a transaction confirmed. `grant tx history [--limit <n>]` — recent transactions.
|
|
135
153
|
- `grant session status` / `grant session grant` — your agent-access (session) status and how to enable it.
|
|
@@ -145,6 +163,9 @@ Every **cash** amount is in the user's display currency — a bare `50` means
|
|
|
145
163
|
| `GUARDRAIL_DENIED` | no | STOP. This is outside the limits the user set — don't retry. Tell the user. |
|
|
146
164
|
| `SIGNER_REVOKED` | no | STOP. Spending was stopped — re-authorize with `grant login`. |
|
|
147
165
|
| `SCHEMA_VALIDATION_FAILED` | yes | Free reject. Run `grant check <url>`, fix the `--body`, then retry. |
|
|
166
|
+
| `MARKETPLACE_NOT_ALLOWED` | no | Origin isn't catalogued/allowlisted — add it with `grant discover <origin>` first (must be public https). |
|
|
167
|
+
| `AMOUNT_TOO_SMALL` | yes | Amount rounds to ~$0 — enter a larger amount. |
|
|
168
|
+
| `AMOUNT_TOO_LARGE` | yes | Above the per-transaction safety limit — split into smaller amounts (or re-check the currency). |
|
|
148
169
|
| `BACKEND_UNREACHABLE` | no | Wrong or unreachable address — check the connection with `grant config show`. |
|
|
149
170
|
| `NETWORK` | yes | Temporary — retry once. |
|
|
150
171
|
|
|
@@ -164,6 +185,7 @@ grant polymarket bet 0x1234 --side yes --amount 25 # bet $25 on "yes"
|
|
|
164
185
|
grant search "web search" --limit 5 # find a live service (Spending)
|
|
165
186
|
grant check https://api.example.com/run # what it costs + needs, no charge
|
|
166
187
|
grant fetch https://api.example.com/run -b '{"q":"gold news today"}' # pay + run
|
|
188
|
+
grant spending deposit --amount 25 # move $25 of cash into Spending
|
|
167
189
|
grant activity --json # full combined history
|
|
168
190
|
grant revoke # stop all agent spending
|
|
169
191
|
```
|