@darwin.finance/cli 1.0.1 → 1.1.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 +36 -9
- package/dist/darwin.js +492 -103
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,10 +7,13 @@ your computer's secret store, and turns every agent action into a command — fo
|
|
|
7
7
|
|
|
8
8
|
Chat apps like Claude or ChatGPT don't use the CLI — they connect to Darwin directly instead.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
> (below) for testing only.
|
|
10
|
+
## Install
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
```sh
|
|
13
|
+
npm i -g @darwin.finance/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install from source (for testing)
|
|
14
17
|
|
|
15
18
|
Requires Node.js 20+ and [Bun](https://bun.sh) to build.
|
|
16
19
|
|
|
@@ -23,7 +26,7 @@ npm i -g .
|
|
|
23
26
|
darwin --version
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
Always install it — never run it
|
|
27
30
|
through `npx`: the CLI refuses to read a saved key when it runs from npx or from a project's
|
|
28
31
|
`node_modules`, because a project you are merely working in could substitute its own copy.
|
|
29
32
|
|
|
@@ -43,7 +46,30 @@ What each command does is documented by Darwin itself: the command list is fetch
|
|
|
43
46
|
logged in to (`darwin help`, `darwin <command> --help`, `darwin help --json`), so new commands appear
|
|
44
47
|
without a new release of this program. The API reference: <https://darwin.finance/agents/docs>.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
### Trading (1.1)
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
darwin quote --sell USDC --amount 5 --for SOL # then: darwin order --quote <id> …
|
|
53
|
+
darwin instant --sell SOL --amount 0.1 --for USDC --max-slippage-bps 50
|
|
54
|
+
darwin perps collateral deposit --amount 5 # USDC: agent wallet → its own perps account
|
|
55
|
+
darwin perps order SOL --side long --size 0.01 --type market
|
|
56
|
+
darwin perps protect SOL --sl 120 --tp 160 [--percent 50] # or --cancel tp|sl|both
|
|
57
|
+
darwin perps close SOL [--size 0.01]
|
|
58
|
+
darwin perps collateral withdraw --amount 5 # back to the agent's own wallet
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On a site that supports it, every order is two steps behind the scenes: Darwin first **checks** it
|
|
62
|
+
(the agent can trade, perps is set up, there is free collateral, the position or quote is there) and
|
|
63
|
+
stores exactly what it will send — pinning a close's size and side, and which TP/SL a change
|
|
64
|
+
replaces — then sends it **once**. After sending, the CLI waits a few seconds (reading only) and
|
|
65
|
+
says whether it went through. `--dry-run` runs Darwin's real checks and sends nothing.
|
|
66
|
+
|
|
67
|
+
If an order's answer is lost (exit 6), **don't run the command again**: run the `darwin retry <id>`
|
|
68
|
+
it printed. It only reads what happened — went through, still pending, failed or never sent — and
|
|
69
|
+
never sends anything. `darwin cancel <id>` makes sure an order that hasn't started never runs.
|
|
70
|
+
|
|
71
|
+
`darwin mcp` runs the same commands as a local MCP server for desktop AI clients (writes go through
|
|
72
|
+
the same check-then-send; `check_prepared` is the read-only recovery tool, `cancel_prepared` the cancel);
|
|
47
73
|
`darwin mcp --print-config claude|cursor|gemini` prints the registration.
|
|
48
74
|
|
|
49
75
|
## Security model
|
|
@@ -59,9 +85,10 @@ without a new release of this program. The API reference: <https://darwin.financ
|
|
|
59
85
|
- **No more than the key.** Every command is an ordinary agent API call under the key's own limits;
|
|
60
86
|
Darwin signs server-side under the agent's policy. Nothing here can withdraw or move funds between
|
|
61
87
|
your accounts — no such command exists.
|
|
62
|
-
- **No silent retries, no prompts.** A write is never re-sent. If its answer is lost, the CLI
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
- **No silent retries, no prompts.** A write is never re-sent. If its answer is lost, the CLI reads
|
|
89
|
+
what happened (never re-sends), and if that is still unknown exits 6 with the `darwin retry <id>`
|
|
90
|
+
to run later (on a site without checked orders: check `darwin orders`). There is no `[y/N]`
|
|
91
|
+
prompt; use `--dry-run` to have Darwin check a command without sending anything. Writes and reads are distinct commands, so your
|
|
65
92
|
agent harness can allow reads and ask before writes.
|
|
66
93
|
- **Server text is data.** Text a third party can influence (a token's name, a venue's message) is
|
|
67
94
|
stripped of terminal escapes, control and bidi characters before it reaches a terminal. In JSON
|
|
@@ -79,7 +106,7 @@ without a new release of this program. The API reference: <https://darwin.financ
|
|
|
79
106
|
| 3 | no key / key refused |
|
|
80
107
|
| 4 | refused by Darwin |
|
|
81
108
|
| 5 | rate limited |
|
|
82
|
-
| 6 | a write's outcome is uncertain — don't re-run;
|
|
109
|
+
| 6 | a write's outcome is uncertain — don't re-run; `darwin retry <id>` (or `darwin orders`) |
|
|
83
110
|
| 7 | this CLI is too old |
|
|
84
111
|
| 8 | network unreachable before sending |
|
|
85
112
|
| 10 | the agent is paused |
|
package/dist/darwin.js
CHANGED
|
@@ -58,6 +58,7 @@ Waiting for you to approve… (the code expires in ${minutes} minutes)`,
|
|
|
58
58
|
needAgent: "This API key trades for all your active agents. Name one with --agent <name or id>, or set a default: `darwin profile set-agent <name>`. Your agents: `darwin agents`.",
|
|
59
59
|
mcpPageKey: "This API key is from the MCP page: it reads your whole account and can't run agent commands. Log in with an agent's API key.",
|
|
60
60
|
uncertainWrite: "Darwin may have received this order, but the answer didn't arrive. Don't run the command again — check `darwin orders` to see whether it went through.",
|
|
61
|
+
uncertainWritePrepared: "Darwin may have received this order, but its result didn't come back.",
|
|
61
62
|
paused: "This agent is paused, so it can't trade. Reads still work. Only you can resume it, on the agent's Darwin page.",
|
|
62
63
|
quoteFooter: (quoteId, seconds, sell, amount, forTok) => `Quote ${quoteId} expires in ${seconds}s. To place it: darwin order --quote ${quoteId} --sell ${sell} --amount ${amount} --for ${forTok}`,
|
|
63
64
|
updateAvailable: (latest, current) => `Darwin CLI ${latest} is available (you have ${current}). Update: ${INSTALL_LINE}`,
|
|
@@ -249,7 +250,7 @@ function compareVersions(a, b) {
|
|
|
249
250
|
return x[i] < y[i] ? -1 : 1;
|
|
250
251
|
return 0;
|
|
251
252
|
}
|
|
252
|
-
var VERSION = "1.0
|
|
253
|
+
var VERSION = "1.1.0";
|
|
253
254
|
|
|
254
255
|
// src/http.ts
|
|
255
256
|
function vendor(env) {
|
|
@@ -469,6 +470,7 @@ var init_config = __esm(() => {
|
|
|
469
470
|
var exports_agents = {};
|
|
470
471
|
__export(exports_agents, {
|
|
471
472
|
resolveAgent: () => resolveAgent,
|
|
473
|
+
recoveryAgent: () => recoveryAgent,
|
|
472
474
|
listAgents: () => listAgents
|
|
473
475
|
});
|
|
474
476
|
import { createHash } from "node:crypto";
|
|
@@ -538,6 +540,16 @@ async function resolveAgent(ctx, s, flag) {
|
|
|
538
540
|
}
|
|
539
541
|
throw new CliError(EXIT.usage, "No active agent of yours matches that (a paused, stopped or archived agent isn't listed). Your agents: `darwin agents`.", "agent_not_found", { candidates: agents2.map((a) => ({ id: a.id, name: a.name })) });
|
|
540
542
|
}
|
|
543
|
+
async function recoveryAgent(ctx, s, flag) {
|
|
544
|
+
const raw = ((flag ?? ctx.env.DARWIN_AGENT ?? "").trim() || (s.kind === "agents" ? s.profile?.default_agent ?? "" : "")).trim();
|
|
545
|
+
try {
|
|
546
|
+
return await resolveAgent(ctx, s, flag);
|
|
547
|
+
} catch (e) {
|
|
548
|
+
if (s.kind === "agents" && e instanceof CliError && e.code === "agent_not_found" && /^agr_[A-Za-z0-9_-]{1,64}$/.test(raw) && isAgentId(raw))
|
|
549
|
+
return raw;
|
|
550
|
+
throw e;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
541
553
|
var BASE582, unwrap = (v) => v && typeof v === "object" && ("untrusted" in v) ? String(v.untrusted) : typeof v === "string" ? v : "";
|
|
542
554
|
var init_agents = __esm(() => {
|
|
543
555
|
init_context();
|
|
@@ -553,7 +565,7 @@ var init_agents = __esm(() => {
|
|
|
553
565
|
import { spawn as spawn2 } from "node:child_process";
|
|
554
566
|
import { existsSync } from "node:fs";
|
|
555
567
|
import { homedir as homedir2 } from "node:os";
|
|
556
|
-
import { join as
|
|
568
|
+
import { join as join9 } from "node:path";
|
|
557
569
|
import { createInterface } from "node:readline";
|
|
558
570
|
import { fileURLToPath } from "node:url";
|
|
559
571
|
|
|
@@ -586,7 +598,7 @@ function osKeychain(platform) {
|
|
|
586
598
|
}
|
|
587
599
|
|
|
588
600
|
// src/main.ts
|
|
589
|
-
import { join as
|
|
601
|
+
import { join as join8 } from "node:path";
|
|
590
602
|
|
|
591
603
|
// src/args.ts
|
|
592
604
|
init_context();
|
|
@@ -3629,6 +3641,16 @@ var STATIC_HELP = {
|
|
|
3629
3641
|
summary: "Run Darwin as a local MCP server (stdio) for a desktop AI client, with the API key kept in your secret store.",
|
|
3630
3642
|
flags: { "print-config": "Print the registration for a client, pointing at this installed copy." }
|
|
3631
3643
|
},
|
|
3644
|
+
retry: {
|
|
3645
|
+
usage: "darwin retry <id> [--agent <name or id>]",
|
|
3646
|
+
summary: "What happened to an order whose result didn't come back: it went through, is still pending, failed, or was never sent. It only reads — it never sends anything again.",
|
|
3647
|
+
flags: {}
|
|
3648
|
+
},
|
|
3649
|
+
cancel: {
|
|
3650
|
+
usage: "darwin cancel <id> [--agent <name or id>]",
|
|
3651
|
+
summary: "Make sure an order that hasn't started never runs. One that already started can't be cancelled; it says what happened instead.",
|
|
3652
|
+
flags: {}
|
|
3653
|
+
},
|
|
3632
3654
|
help: { usage: "darwin help [command] [--json]", summary: "This help. --json lists every command with its flags.", flags: {} },
|
|
3633
3655
|
version: { usage: "darwin version", summary: "Print the Darwin CLI version.", flags: {} }
|
|
3634
3656
|
};
|
|
@@ -3636,7 +3658,7 @@ var GLOBAL_HELP = {
|
|
|
3636
3658
|
json: "Print JSON (the default when output isn't a terminal).",
|
|
3637
3659
|
profile: "Use this profile.",
|
|
3638
3660
|
agent: "For an API key that trades for all your agents: which agent (name or id).",
|
|
3639
|
-
"dry-run": "
|
|
3661
|
+
"dry-run": "Have Darwin check what a command would do — the real checks — and send nothing.",
|
|
3640
3662
|
field: "For API fields without their own flag: --field name=value (repeatable).",
|
|
3641
3663
|
quiet: "Less output."
|
|
3642
3664
|
};
|
|
@@ -3704,7 +3726,7 @@ function terminalGuide(kind) {
|
|
|
3704
3726
|
return [
|
|
3705
3727
|
"Spot trades take two steps: `darwin quote --sell <token> --amount <n> --for <token>`, then `darwin order --quote <id>` with the same sell, amount and for, within 30 seconds. `darwin instant` quotes and executes in one step.",
|
|
3706
3728
|
"Before your first order, `darwin grant` shows your limits and today's remaining budget. `darwin docs` is the full API reference.",
|
|
3707
|
-
"If an order's result is uncertain (exit code 6), don't run it again —
|
|
3729
|
+
"If an order's result is uncertain (exit code 6), don't run it again — run the `darwin retry <id>` it printed to see what happened (on a site without it, check `darwin orders`).",
|
|
3708
3730
|
kind === "agents" ? "This API key trades for all your active agents: name one with --agent <name or id> (`darwin agents` lists them). `darwin pause --agent <name>` stops one; only you can resume it, on Darwin." : "`darwin pause` stops this agent trading; only you can resume it, on Darwin."
|
|
3709
3731
|
];
|
|
3710
3732
|
}
|
|
@@ -3726,7 +3748,7 @@ function printCommandHelp(ctx, t, json, c = null) {
|
|
|
3726
3748
|
say(ctx, ` ${f.flag} <${f.type}>${f.required ? " (required)" : ""} ${forTerminal(f.description, c)}`);
|
|
3727
3749
|
if (t.write)
|
|
3728
3750
|
say(ctx, `
|
|
3729
|
-
There is no confirmation prompt: the command runs when you press enter. Use --dry-run to
|
|
3751
|
+
There is no confirmation prompt: the command runs when you press enter. Use --dry-run to have Darwin check it without sending anything.`);
|
|
3730
3752
|
if (t.deprecated)
|
|
3731
3753
|
say(ctx, `
|
|
3732
3754
|
Deprecated since ${t.deprecated.since}; removed after ${t.deprecated.removeAfter}.`);
|
|
@@ -4333,9 +4355,364 @@ function openSession(ctx, flags) {
|
|
|
4333
4355
|
|
|
4334
4356
|
// src/commands.ts
|
|
4335
4357
|
init_agents();
|
|
4358
|
+
|
|
4359
|
+
// src/prepared.ts
|
|
4360
|
+
init_context();
|
|
4361
|
+
init_http();
|
|
4362
|
+
init_redact();
|
|
4363
|
+
init_copy();
|
|
4364
|
+
var PREPARED_ID = /^prp_[0-9A-Za-z]{24}$/;
|
|
4365
|
+
var SIG = /^[1-9A-HJ-NP-Za-km-z]{64,90}$/;
|
|
4366
|
+
function isPreparedWrite(tool) {
|
|
4367
|
+
return tool.write && tool.name !== "pause_agent";
|
|
4368
|
+
}
|
|
4369
|
+
var VERDICTS = new Set(["sent", "landed", "pending", "failed", "refused", "replayed", "uncertain", "never_sent", "not_started"]);
|
|
4370
|
+
var LAND_WAIT_MS = 8000;
|
|
4371
|
+
var POLL_MS = 2000;
|
|
4372
|
+
var obj2 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
4373
|
+
var unwrap2 = (v) => v && typeof v === "object" && !Array.isArray(v) && ("untrusted" in v) ? v.untrusted : v;
|
|
4374
|
+
function cliLines(body, key = "lines") {
|
|
4375
|
+
const l = obj2(obj2(body).cli)[key];
|
|
4376
|
+
return Array.isArray(l) ? l.filter((x) => typeof x === "string").map((x) => clean(x).slice(0, 600)).filter(Boolean).slice(0, 12) : [];
|
|
4377
|
+
}
|
|
4378
|
+
function verdictOf(body) {
|
|
4379
|
+
const v = obj2(body).verdict;
|
|
4380
|
+
return typeof v === "string" && VERDICTS.has(v) ? v : null;
|
|
4381
|
+
}
|
|
4382
|
+
function refusalCode(body) {
|
|
4383
|
+
const r = obj2(obj2(body).result);
|
|
4384
|
+
const d = obj2(r.data);
|
|
4385
|
+
const or = obj2(obj2(obj2(body).outcome).result);
|
|
4386
|
+
const od = obj2(or.data);
|
|
4387
|
+
for (const c of [r.error, d.refusal, d.error, or.error, od.refusal, od.error, obj2(body).error]) {
|
|
4388
|
+
const s = unwrap2(c);
|
|
4389
|
+
if (typeof s === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(s))
|
|
4390
|
+
return s;
|
|
4391
|
+
}
|
|
4392
|
+
return "";
|
|
4393
|
+
}
|
|
4394
|
+
function resultStatus(body) {
|
|
4395
|
+
for (const r of [obj2(obj2(body).result), obj2(obj2(obj2(body).outcome).result)])
|
|
4396
|
+
if (typeof r.status === "number")
|
|
4397
|
+
return r.status;
|
|
4398
|
+
return null;
|
|
4399
|
+
}
|
|
4400
|
+
function exitForVerdict(v, body) {
|
|
4401
|
+
switch (v) {
|
|
4402
|
+
case "sent":
|
|
4403
|
+
case "landed":
|
|
4404
|
+
case "replayed":
|
|
4405
|
+
return EXIT.ok;
|
|
4406
|
+
case "refused":
|
|
4407
|
+
case "failed":
|
|
4408
|
+
case "never_sent": {
|
|
4409
|
+
const code = refusalCode(body);
|
|
4410
|
+
if (code === "grant_paused")
|
|
4411
|
+
return EXIT.paused;
|
|
4412
|
+
if (code === "rate_limited" || resultStatus(body) === 429)
|
|
4413
|
+
return EXIT.rateLimited;
|
|
4414
|
+
return EXIT.refused;
|
|
4415
|
+
}
|
|
4416
|
+
default:
|
|
4417
|
+
return EXIT.uncertain;
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
function orderIdOf(body) {
|
|
4421
|
+
const r = obj2(obj2(body).result);
|
|
4422
|
+
const data = obj2(r.data);
|
|
4423
|
+
const summary = obj2(r.summary);
|
|
4424
|
+
const id = unwrap2(data.orderId) ?? unwrap2(summary.orderId);
|
|
4425
|
+
if (typeof id === "string" && /^[A-Za-z0-9_-]{1,80}$/.test(id))
|
|
4426
|
+
return id;
|
|
4427
|
+
const sig = unwrap2(data.txSignature) ?? unwrap2(summary.txSignature) ?? unwrap2(data.signature);
|
|
4428
|
+
if (typeof sig === "string" && SIG.test(sig))
|
|
4429
|
+
return `${sig.slice(0, 4)}…${sig.slice(-4)}`;
|
|
4430
|
+
return null;
|
|
4431
|
+
}
|
|
4432
|
+
function retryCommand(preparedId, ctxFlags = [], verb = "retry") {
|
|
4433
|
+
return ["darwin", verb, preparedId, ...ctxFlags].join(" ");
|
|
4434
|
+
}
|
|
4435
|
+
var SAFE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
4436
|
+
function recoveryFlags(session, agentId) {
|
|
4437
|
+
const out = [];
|
|
4438
|
+
if (session.profile && SAFE.test(session.profileName))
|
|
4439
|
+
out.push("--profile", session.profileName);
|
|
4440
|
+
if (agentId && SAFE.test(agentId))
|
|
4441
|
+
out.push("--agent", agentId);
|
|
4442
|
+
return out;
|
|
4443
|
+
}
|
|
4444
|
+
async function post(o, path, body, timeoutMs) {
|
|
4445
|
+
return request(o.ctx, o.session.realm, "POST", path, { key: o.session.key, agent: o.agentId ?? undefined, body, timeoutMs });
|
|
4446
|
+
}
|
|
4447
|
+
function plainRefusal(res, catalogue, command, realm) {
|
|
4448
|
+
const err = typeof obj2(res.json).error === "string" ? String(obj2(res.json).error) : "";
|
|
4449
|
+
if (res.status === 401)
|
|
4450
|
+
return { exit: EXIT.auth, message: "Darwin refused this API key (or the agent it named). It may have been revoked or the agent paused; run `darwin whoami`, or ask the owner. Nothing was done." };
|
|
4451
|
+
if (res.status === 426) {
|
|
4452
|
+
const min = typeof obj2(res.json).minCli === "string" && /^\d+\.\d+\.\d+$/.test(String(obj2(res.json).minCli)) ? String(obj2(res.json).minCli) : catalogue.minCli;
|
|
4453
|
+
return { exit: EXIT.upgrade, message: copy.updateRequired(min) };
|
|
4454
|
+
}
|
|
4455
|
+
if (res.status === 429)
|
|
4456
|
+
return { exit: EXIT.rateLimited, message: "Too many requests with this API key right now. Wait a minute and try again. Nothing was done." };
|
|
4457
|
+
if (res.status === 404 && err === "cli_unavailable")
|
|
4458
|
+
return { exit: EXIT.refused, message: "The Darwin CLI isn't available on this site yet. Nothing was done." };
|
|
4459
|
+
if (res.status === 404 && err === "unknown_tool")
|
|
4460
|
+
return { exit: EXIT.usage, message: `\`${command}\` isn't available on ${realm} right now. Nothing was done. Run \`darwin help\` for the current list.` };
|
|
4461
|
+
if (res.status === 400 && err === "invalid_request")
|
|
4462
|
+
return { exit: EXIT.usage, message: "Darwin couldn't read that request. Nothing was done." };
|
|
4463
|
+
return null;
|
|
4464
|
+
}
|
|
4465
|
+
async function runPrepared(o) {
|
|
4466
|
+
const { tool } = o;
|
|
4467
|
+
const path = `/api/agent/v1/tools/call/${encodeURIComponent(tool.name)}/prepare`;
|
|
4468
|
+
let res;
|
|
4469
|
+
try {
|
|
4470
|
+
res = await post(o, path, { arguments: o.args, ...o.dryRun ? { dryRun: true } : {} }, 30000);
|
|
4471
|
+
} catch (e) {
|
|
4472
|
+
if (!(e instanceof NetworkError))
|
|
4473
|
+
throw e;
|
|
4474
|
+
return done(o, EXIT.network, null, { error: "network_error", detail: `${e.message} Nothing was sent; try again.`, sent: false }, [`${clean(e.message)} Nothing was sent; try again.`]);
|
|
4475
|
+
}
|
|
4476
|
+
const pj = obj2(res.json);
|
|
4477
|
+
if (res.status === 404 && pj.error !== "unknown_tool" && pj.error !== "cli_unavailable") {
|
|
4478
|
+
if (!o.dryRun)
|
|
4479
|
+
return { kind: "fallback" };
|
|
4480
|
+
const line = `Darwin can't check orders ahead of time on ${o.session.realm} yet, so nothing was checked or sent.`;
|
|
4481
|
+
return done(o, EXIT.ok, null, { dryRun: true, sent: false, checked: false, detail: line }, [line]);
|
|
4482
|
+
}
|
|
4483
|
+
const plain = plainRefusal(res, o.catalogue, `darwin ${tool.cli.path.join(" ")}`, o.session.realm);
|
|
4484
|
+
if (plain)
|
|
4485
|
+
return done(o, plain.exit, null, { error: pj.error ?? "refused", detail: plain.message, sent: false, status: res.status }, [plain.message]);
|
|
4486
|
+
const result = obj2(pj.result);
|
|
4487
|
+
if (res.status !== 200 || typeof pj.isError !== "boolean") {
|
|
4488
|
+
return done(o, EXIT.unexpected, null, { error: "bad_response", status: res.status, sent: false }, [`Darwin answered HTTP ${res.status}. Nothing was sent.`]);
|
|
4489
|
+
}
|
|
4490
|
+
if (pj.isError) {
|
|
4491
|
+
const code = refusalCode(pj);
|
|
4492
|
+
const exit = code === "grant_paused" ? EXIT.paused : code === "invalid_arguments" || code === "nonce_not_accepted" ? EXIT.usage : code === "rate_limited" ? EXIT.rateLimited : EXIT.refused;
|
|
4493
|
+
return done(o, exit, null, { ...pj }, cliLines(pj).length ? cliLines(pj) : [`Darwin refused this${code ? ` (${code})` : ""}. Nothing was sent.`]);
|
|
4494
|
+
}
|
|
4495
|
+
if (o.dryRun) {
|
|
4496
|
+
const lines = cliLines(pj);
|
|
4497
|
+
if (tool.costsTx)
|
|
4498
|
+
lines.push("Running it for real counts against today's transaction budget.");
|
|
4499
|
+
return done(o, EXIT.ok, null, { ...pj }, lines.length ? lines : ["Dry run: Darwin checked it, and nothing was sent."]);
|
|
4500
|
+
}
|
|
4501
|
+
const preparedId = typeof result.preparedId === "string" && PREPARED_ID.test(result.preparedId) ? result.preparedId : null;
|
|
4502
|
+
if (!preparedId)
|
|
4503
|
+
return done(o, EXIT.unexpected, null, { error: "bad_response", sent: false }, ["Darwin's answer didn't name the prepared order. Nothing was sent."]);
|
|
4504
|
+
return execute(o, preparedId, pj);
|
|
4505
|
+
}
|
|
4506
|
+
async function execute(o, preparedId, prepareBody) {
|
|
4507
|
+
let res = null;
|
|
4508
|
+
let lostAnswer = false;
|
|
4509
|
+
try {
|
|
4510
|
+
res = await post(o, "/api/agent/v1/tools/execute", { preparedId }, 70000);
|
|
4511
|
+
} catch (e) {
|
|
4512
|
+
if (e instanceof CliError && e.code === "redirect_refused") {
|
|
4513
|
+
lostAnswer = true;
|
|
4514
|
+
} else if (!(e instanceof NetworkError)) {
|
|
4515
|
+
throw e;
|
|
4516
|
+
} else if (!e.maybeSent) {
|
|
4517
|
+
return done(o, EXIT.network, preparedId, { error: "network_error", preparedId, sent: false, detail: `${e.message} Nothing was sent.` }, [`${clean(e.message)} Nothing was sent. (It can't run later by itself; \`${retryCommand(preparedId, o.recovery, "cancel")}\` makes sure.)`]);
|
|
4518
|
+
} else {
|
|
4519
|
+
lostAnswer = true;
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
const ej = obj2(res?.json);
|
|
4523
|
+
if (res && !lostAnswer) {
|
|
4524
|
+
if (res.status === 409 && ej.executed === false)
|
|
4525
|
+
return finish(o, preparedId, prepareBody, ej, null);
|
|
4526
|
+
if (res.status === 401) {
|
|
4527
|
+
return done(o, EXIT.auth, preparedId, { error: "unauthorized", preparedId, sent: false }, ["Darwin refused this API key (or the agent it named), so the order was not sent. Run `darwin whoami`, or ask the owner."]);
|
|
4528
|
+
}
|
|
4529
|
+
const plain = plainRefusal(res, o.catalogue, `darwin ${o.tool.cli.path.join(" ")}`, o.session.realm);
|
|
4530
|
+
if (plain)
|
|
4531
|
+
return done(o, plain.exit, preparedId, { error: ej.error ?? "refused", preparedId, sent: false, status: res.status }, [plain.message.replace("Nothing was done.", "The order was not sent.")]);
|
|
4532
|
+
if (res.status === 404) {
|
|
4533
|
+
return done(o, EXIT.refused, preparedId, { error: ej.error ?? "not_found", preparedId, sent: false, status: 404 }, ["Darwin couldn't find this checked order to send, so nothing was sent. Run the command again."]);
|
|
4534
|
+
}
|
|
4535
|
+
if (res.status === 200 && verdictOf(ej)) {
|
|
4536
|
+
const v = verdictOf(ej);
|
|
4537
|
+
if (v !== "uncertain")
|
|
4538
|
+
return finish(o, preparedId, prepareBody, ej, null);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
const lines = res && verdictOf(ej) === "uncertain" ? cliLines(ej) : [];
|
|
4542
|
+
const status = await pollStatus(o, preparedId, (v) => v !== "pending" && v !== "uncertain" && v !== "not_started", LAND_WAIT_MS);
|
|
4543
|
+
return finish(o, preparedId, prepareBody, Object.keys(ej).length ? ej : { preparedId, state: "unknown", verdict: "uncertain" }, status, lines);
|
|
4544
|
+
}
|
|
4545
|
+
async function pollStatus(o, preparedId, stop, budgetMs) {
|
|
4546
|
+
if (o.quietForMcp)
|
|
4547
|
+
budgetMs = Math.min(budgetMs, 4000);
|
|
4548
|
+
const deadline = o.ctx.now() + budgetMs;
|
|
4549
|
+
let last = null;
|
|
4550
|
+
while (o.ctx.now() + POLL_MS <= deadline) {
|
|
4551
|
+
await o.ctx.sleep(POLL_MS);
|
|
4552
|
+
try {
|
|
4553
|
+
const r = await post(o, "/api/agent/v1/tools/status", { preparedId }, Math.max(1000, Math.min(5000, deadline - o.ctx.now())));
|
|
4554
|
+
if (r.status === 200 && verdictOf(r.json)) {
|
|
4555
|
+
last = obj2(r.json);
|
|
4556
|
+
if (stop(verdictOf(last)))
|
|
4557
|
+
return last;
|
|
4558
|
+
}
|
|
4559
|
+
} catch {}
|
|
4560
|
+
}
|
|
4561
|
+
return last;
|
|
4562
|
+
}
|
|
4563
|
+
async function finish(o, preparedId, prepareBody, executeBody, status, firstLines = []) {
|
|
4564
|
+
let st = status;
|
|
4565
|
+
let v = verdictOf(executeBody) ?? "uncertain";
|
|
4566
|
+
const lines = [...firstLines.length ? firstLines : cliLines(executeBody)];
|
|
4567
|
+
if (!st && v === "sent")
|
|
4568
|
+
st = await pollStatus(o, preparedId, (x) => x === "landed" || x === "failed" || x === "refused", LAND_WAIT_MS);
|
|
4569
|
+
const sv = verdictOf(st);
|
|
4570
|
+
if (st && sv) {
|
|
4571
|
+
const stillWaiting = v === "sent" && (sv === "sent" || sv === "pending");
|
|
4572
|
+
if (!stillWaiting) {
|
|
4573
|
+
for (const l of cliLines(st))
|
|
4574
|
+
if (!lines.includes(l))
|
|
4575
|
+
lines.push(l);
|
|
4576
|
+
v = sv;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
const exit = exitForVerdict(v, st ?? executeBody);
|
|
4580
|
+
if (exit === EXIT.uncertain && !lines.some((l) => l.startsWith("Darwin may have received")))
|
|
4581
|
+
lines.unshift(copy.uncertainWritePrepared);
|
|
4582
|
+
const orderId = orderIdOf(executeBody) ?? orderIdOf(st);
|
|
4583
|
+
if (orderId && (v === "sent" || v === "landed" || v === "replayed"))
|
|
4584
|
+
lines.push(`Order ID: ${orderId}`);
|
|
4585
|
+
if (v === "sent")
|
|
4586
|
+
lines.push(`Not confirmed yet. Check it with: ${retryCommand(preparedId, o.recovery)}`);
|
|
4587
|
+
if (exit === EXIT.uncertain)
|
|
4588
|
+
lines.push(`Don't run the command again. Check it with: ${retryCommand(preparedId, o.recovery)}`);
|
|
4589
|
+
const doc = {
|
|
4590
|
+
preparedId,
|
|
4591
|
+
tool: o.tool.name,
|
|
4592
|
+
verdict: v ?? "uncertain",
|
|
4593
|
+
prepare: obj2(prepareBody.result),
|
|
4594
|
+
execute: executeBody,
|
|
4595
|
+
...st && st !== executeBody ? { status: st } : {},
|
|
4596
|
+
...exit === EXIT.uncertain || v === "sent" ? { next: { command: retryCommand(preparedId, o.recovery) } } : {}
|
|
4597
|
+
};
|
|
4598
|
+
return done(o, exit, preparedId, doc, lines);
|
|
4599
|
+
}
|
|
4600
|
+
function done(o, exit, preparedId, doc, lines, toStderr = exit !== EXIT.ok) {
|
|
4601
|
+
if (!o.quietForMcp) {
|
|
4602
|
+
if (o.json) {
|
|
4603
|
+
printJson(o.ctx, doc);
|
|
4604
|
+
if (exit !== EXIT.ok)
|
|
4605
|
+
for (const l of lines)
|
|
4606
|
+
warn(o.ctx, l);
|
|
4607
|
+
} else {
|
|
4608
|
+
for (const l of lines)
|
|
4609
|
+
(toStderr ? warn : say)(o.ctx, l);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
return { kind: "done", exit, preparedId, doc: { ...doc, lines } };
|
|
4613
|
+
}
|
|
4614
|
+
async function checkPrepared(o) {
|
|
4615
|
+
const { ctx, preparedId } = o;
|
|
4616
|
+
const flags = recoveryFlags(o.session, o.agentId);
|
|
4617
|
+
const again = retryCommand(preparedId, flags);
|
|
4618
|
+
let res;
|
|
4619
|
+
try {
|
|
4620
|
+
res = await post(o, o.cancel ? "/api/agent/v1/tools/cancel" : "/api/agent/v1/tools/status", { preparedId }, 20000);
|
|
4621
|
+
} catch (e) {
|
|
4622
|
+
if (e instanceof CliError && e.code === "redirect_refused") {
|
|
4623
|
+
const msg2 = `${clean(e.message)} ${o.cancel ? "Whether it was cancelled is unknown" : "Its outcome is still unknown"}; check again in a moment with \`${again}\`.`;
|
|
4624
|
+
if (o.json)
|
|
4625
|
+
printJson(ctx, { error: "redirect_refused", preparedId, detail: msg2 });
|
|
4626
|
+
warn(ctx, msg2);
|
|
4627
|
+
return EXIT.uncertain;
|
|
4628
|
+
}
|
|
4629
|
+
if (!(e instanceof NetworkError))
|
|
4630
|
+
throw e;
|
|
4631
|
+
const reached = e.maybeSent;
|
|
4632
|
+
const msg = o.cancel && !reached ? `${clean(e.message)} The cancel didn't reach Darwin; try again.` : `${clean(e.message)} ${o.cancel ? "Whether it was cancelled is unknown" : "Its outcome is still unknown"}; check again in a moment with \`${again}\`.`;
|
|
4633
|
+
if (o.json)
|
|
4634
|
+
printJson(ctx, { error: "network_error", preparedId, detail: msg });
|
|
4635
|
+
warn(ctx, msg);
|
|
4636
|
+
return o.cancel && !reached ? EXIT.network : EXIT.uncertain;
|
|
4637
|
+
}
|
|
4638
|
+
const j = obj2(res.json);
|
|
4639
|
+
const err = typeof j.error === "string" ? j.error : "";
|
|
4640
|
+
if (res.status === 401) {
|
|
4641
|
+
const msg = "Can't check this order with this API key and agent right now. Its outcome is still unknown, not failed. A paused agent is one possible cause. Check the agent's History on Darwin.";
|
|
4642
|
+
if (o.json)
|
|
4643
|
+
printJson(ctx, { error: "unauthorized", preparedId, detail: msg });
|
|
4644
|
+
warn(ctx, msg);
|
|
4645
|
+
return EXIT.uncertain;
|
|
4646
|
+
}
|
|
4647
|
+
if (res.status === 404 && (err === "not_found" || err === "prepared_unavailable" || err === "cli_unavailable")) {
|
|
4648
|
+
const msg = err === "not_found" ? "Darwin has no order with that ID for this API key and agent. (Use the ID this terminal printed, with the same profile and --agent.)" : `This site doesn't keep checked orders yet, so there's nothing to look up. Check \`darwin orders\` instead.`;
|
|
4649
|
+
if (o.json)
|
|
4650
|
+
printJson(ctx, { error: err, preparedId, detail: msg });
|
|
4651
|
+
warn(ctx, msg);
|
|
4652
|
+
return EXIT.refused;
|
|
4653
|
+
}
|
|
4654
|
+
if (res.status === 426) {
|
|
4655
|
+
const min = typeof j.minCli === "string" && /^\d+\.\d+\.\d+$/.test(j.minCli) ? j.minCli : "the latest version";
|
|
4656
|
+
const msg = copy.updateRequired(min);
|
|
4657
|
+
if (o.json)
|
|
4658
|
+
printJson(ctx, { error: "cli_upgrade_required", status: 426, preparedId, minCli: min, detail: msg });
|
|
4659
|
+
warn(ctx, msg);
|
|
4660
|
+
return EXIT.upgrade;
|
|
4661
|
+
}
|
|
4662
|
+
if (res.status === 429) {
|
|
4663
|
+
const msg = "Too many requests with this API key right now. Wait a minute and try again.";
|
|
4664
|
+
if (o.json)
|
|
4665
|
+
printJson(ctx, { error: "rate_limited", status: 429, preparedId, detail: msg });
|
|
4666
|
+
warn(ctx, msg);
|
|
4667
|
+
return EXIT.rateLimited;
|
|
4668
|
+
}
|
|
4669
|
+
const v = verdictOf(j);
|
|
4670
|
+
if (res.status !== 200 || !v) {
|
|
4671
|
+
if (o.json)
|
|
4672
|
+
printJson(ctx, { error: "bad_response", status: res.status, preparedId });
|
|
4673
|
+
warn(ctx, `Darwin answered HTTP ${res.status}. The order's outcome is still unknown; try again in a moment.`);
|
|
4674
|
+
return EXIT.uncertain;
|
|
4675
|
+
}
|
|
4676
|
+
const lines = cliLines(j);
|
|
4677
|
+
if (o.cancel && j.cancelled === false)
|
|
4678
|
+
lines.push("It had already started, so it couldn't be cancelled.");
|
|
4679
|
+
const exit = o.cancel ? j.cancelled === true ? EXIT.ok : exitForVerdict(v, j) : exitForVerdict(v, j);
|
|
4680
|
+
const orderId = orderIdOf(obj2(j.outcome));
|
|
4681
|
+
if (orderId && (v === "landed" || v === "sent"))
|
|
4682
|
+
lines.push(`Order ID: ${orderId}`);
|
|
4683
|
+
if (v === "pending" || v === "uncertain")
|
|
4684
|
+
lines.push(`Don't place it again. Check again in a minute: ${again}`);
|
|
4685
|
+
if (v === "not_started" && !o.cancel)
|
|
4686
|
+
lines.push(`To make sure it never runs: ${retryCommand(preparedId, flags, "cancel")}`);
|
|
4687
|
+
if (o.json)
|
|
4688
|
+
printJson(ctx, j);
|
|
4689
|
+
else
|
|
4690
|
+
for (const l of lines)
|
|
4691
|
+
say(ctx, l);
|
|
4692
|
+
return exit;
|
|
4693
|
+
}
|
|
4694
|
+
async function preparedRead(o, preparedId, cancel) {
|
|
4695
|
+
if (!PREPARED_ID.test(preparedId))
|
|
4696
|
+
return { ok: false, body: { error: "invalid_arguments", detail: "preparedId is the prp_… id a write returned." } };
|
|
4697
|
+
try {
|
|
4698
|
+
const r = await post(o, cancel ? "/api/agent/v1/tools/cancel" : "/api/agent/v1/tools/status", { preparedId }, 20000);
|
|
4699
|
+
const j = obj2(r.json);
|
|
4700
|
+
if (r.status === 200 && verdictOf(j))
|
|
4701
|
+
return { ok: true, body: j };
|
|
4702
|
+
if (r.status === 401)
|
|
4703
|
+
return { ok: false, body: { error: "unauthorized", preparedId, outcome: "unknown", detail: "Can't check this order with this API key and agent right now. Its outcome is still unknown, not failed. A paused agent is one possible cause." } };
|
|
4704
|
+
return { ok: false, body: { error: typeof j.error === "string" ? j.error : "bad_response", status: r.status, preparedId, detail: typeof j.detail === "string" ? j.detail : "Darwin couldn't answer that just now." } };
|
|
4705
|
+
} catch (e) {
|
|
4706
|
+
if (!(e instanceof NetworkError))
|
|
4707
|
+
throw e;
|
|
4708
|
+
return { ok: false, body: { error: "network_error", preparedId, outcome: "unknown", detail: `${e.message} Its outcome is still unknown.` } };
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
// src/commands.ts
|
|
4336
4713
|
var OUT_FLAGS = ["json", "format", "quiet", "no-color", "help"];
|
|
4337
4714
|
var jsonOut = (ctx, p) => wantsJson(ctx, { json: has(p, "json"), format: one(p, "format") });
|
|
4338
|
-
var
|
|
4715
|
+
var unwrap3 = (v) => v && typeof v === "object" && ("untrusted" in v) ? String(v.untrusted ?? "") : typeof v === "string" ? v : "";
|
|
4339
4716
|
async function cmdLogout(ctx, p) {
|
|
4340
4717
|
onlyFlags(p, ["profile", "all", "revoke", "force-local", ...OUT_FLAGS], "logout");
|
|
4341
4718
|
assertInstalled(ctx);
|
|
@@ -4465,7 +4842,7 @@ async function cmdWhoami(ctx, p) {
|
|
|
4465
4842
|
exit = EXIT.network;
|
|
4466
4843
|
}
|
|
4467
4844
|
const keyName = who && typeof who.name === "string" ? clean(who.name) : "name unavailable";
|
|
4468
|
-
const agent = clean(
|
|
4845
|
+
const agent = clean(unwrap3(who?.homeAgentName) || s.profile?.agent_name || s.profile?.agent_id || "unknown agent");
|
|
4469
4846
|
const permissions = s.kind === "agents" ? "Read: all agents · Trade: all active agents" : "Read: this agent · Trade: this agent";
|
|
4470
4847
|
const store = s.profile ? storeLabel(ctx, s.profile.store, s.realm, s.profileName) : "DARWIN_API_KEY (environment)";
|
|
4471
4848
|
if (jsonOut(ctx, p))
|
|
@@ -4672,6 +5049,18 @@ async function cmdApi(ctx, p) {
|
|
|
4672
5049
|
say(ctx, l);
|
|
4673
5050
|
return res.status === 200 ? EXIT.ok : res.status === 401 ? EXIT.auth : res.status === 429 ? EXIT.rateLimited : EXIT.refused;
|
|
4674
5051
|
}
|
|
5052
|
+
async function cmdCheckPrepared(ctx, p, cancel) {
|
|
5053
|
+
const name = cancel ? "cancel" : "retry";
|
|
5054
|
+
onlyFlags(p, ["profile", "agent", ...OUT_FLAGS], name);
|
|
5055
|
+
const id = p.positionals[1];
|
|
5056
|
+
if (!id || p.positionals.length > 2 || !PREPARED_ID.test(id)) {
|
|
5057
|
+
throw new CliError(EXIT.usage, `\`darwin ${name}\` takes the ID the order printed: \`darwin ${name} prp_…\`.`, "usage");
|
|
5058
|
+
}
|
|
5059
|
+
const s = openSession(ctx, { profile: one(p, "profile") });
|
|
5060
|
+
const { recoveryAgent: recoveryAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
5061
|
+
const agentId = await recoveryAgent2(ctx, s, one(p, "agent"));
|
|
5062
|
+
return checkPrepared({ ctx, session: s, agentId, json: jsonOut(ctx, p), preparedId: id, cancel });
|
|
5063
|
+
}
|
|
4675
5064
|
|
|
4676
5065
|
// src/login.ts
|
|
4677
5066
|
init_context();
|
|
@@ -5203,7 +5592,7 @@ function readKeyFile(path) {
|
|
|
5203
5592
|
rememberSecret(j.key);
|
|
5204
5593
|
return { realm, agentId: j.agent_id, key: j.key };
|
|
5205
5594
|
}
|
|
5206
|
-
var
|
|
5595
|
+
var unwrap4 = (v) => v && typeof v === "object" && ("untrusted" in v) ? String(v.untrusted ?? "") : typeof v === "string" ? v : "";
|
|
5207
5596
|
async function identify(ctx, realm, key, kind) {
|
|
5208
5597
|
let res;
|
|
5209
5598
|
try {
|
|
@@ -5219,7 +5608,7 @@ async function identify(ctx, realm, key, kind) {
|
|
|
5219
5608
|
throw new CliError(EXIT.rateLimited, "Too many requests with this API key right now. Wait a minute and try again. Nothing was saved.", "rate_limited");
|
|
5220
5609
|
const w = res.json;
|
|
5221
5610
|
if (res.status === 200 && w && isAgentId(w.homeAgentId)) {
|
|
5222
|
-
return { kind: w.kind === "agents" ? "agents" : w.kind === "agent" ? "agent" : null, agentId: w.homeAgentId, agentName: clean(
|
|
5611
|
+
return { kind: w.kind === "agents" ? "agents" : w.kind === "agent" ? "agent" : null, agentId: w.homeAgentId, agentName: clean(unwrap4(w.homeAgentName)).slice(0, 120), keyName: typeof w.name === "string" ? clean(w.name).slice(0, 120) : null };
|
|
5223
5612
|
}
|
|
5224
5613
|
if (res.status !== 404)
|
|
5225
5614
|
throw new CliError(EXIT.unexpected, `Couldn't check this key with ${realm} (HTTP ${res.status}). Nothing was saved.`, "bad_response");
|
|
@@ -5238,7 +5627,7 @@ async function identify(ctx, realm, key, kind) {
|
|
|
5238
5627
|
const first = list[0];
|
|
5239
5628
|
if (!isAgentId(first.id))
|
|
5240
5629
|
throw new CliError(EXIT.unexpected, "Darwin's answer was not what the CLI expected. Nothing was saved.", "bad_response");
|
|
5241
|
-
return { kind, agentId: first.id, agentName: clean(
|
|
5630
|
+
return { kind, agentId: first.id, agentName: clean(unwrap4(first.name)).slice(0, 120), keyName: null };
|
|
5242
5631
|
}
|
|
5243
5632
|
async function importKey(ctx, o) {
|
|
5244
5633
|
const who = await identify(ctx, o.realm, o.key, o.kind);
|
|
@@ -5394,7 +5783,7 @@ async function finishPickup(ctx, p, j, json) {
|
|
|
5394
5783
|
rememberSecret(raw);
|
|
5395
5784
|
const agent = j.agent ?? {};
|
|
5396
5785
|
const agentId = isAgentId(agent.id) ? agent.id : "";
|
|
5397
|
-
const agentName = clean(
|
|
5786
|
+
const agentName = clean(unwrap4(agent.name)).slice(0, 120);
|
|
5398
5787
|
const keyName = typeof j.key_name === "string" ? clean(j.key_name).slice(0, 120) : null;
|
|
5399
5788
|
if (!agentId)
|
|
5400
5789
|
throw new CliError(EXIT.unexpected, "Darwin's pairing answer had no agent id. Revoke the newest key on the agent's Manage tab and pair again.", "bad_response");
|
|
@@ -5447,10 +5836,7 @@ init_context();
|
|
|
5447
5836
|
init_copy();
|
|
5448
5837
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
5449
5838
|
init_http();
|
|
5450
|
-
init_fsx();
|
|
5451
5839
|
init_redact();
|
|
5452
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
5453
|
-
import { join as join8 } from "node:path";
|
|
5454
5840
|
init_agents();
|
|
5455
5841
|
var B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
5456
5842
|
function newNonce() {
|
|
@@ -5554,10 +5940,10 @@ function buildArguments(tool, positionals, p, globals) {
|
|
|
5554
5940
|
}
|
|
5555
5941
|
return args;
|
|
5556
5942
|
}
|
|
5557
|
-
var
|
|
5943
|
+
var unwrap5 = (v) => v && typeof v === "object" && !Array.isArray(v) && ("untrusted" in v) ? v.untrusted : v;
|
|
5558
5944
|
var isPaused = (r) => {
|
|
5559
5945
|
const data = r.data ?? {};
|
|
5560
|
-
return [r.error, data.error, data.refusal].some((x) =>
|
|
5946
|
+
return [r.error, data.error, data.refusal].some((x) => unwrap5(x) === "grant_paused");
|
|
5561
5947
|
};
|
|
5562
5948
|
function exitFor(tool, body) {
|
|
5563
5949
|
if (body.isError !== true)
|
|
@@ -5625,7 +6011,7 @@ async function callTool(o, agentId, limits = {}) {
|
|
|
5625
6011
|
message = copy.uncertainWrite;
|
|
5626
6012
|
else if (exit === EXIT.paused)
|
|
5627
6013
|
message = copy.paused;
|
|
5628
|
-
else if (
|
|
6014
|
+
else if (unwrap5(data.refusal) === "duplicate" && unwrap5(data.detail) === "nonce_used_by_a_different_order") {
|
|
5629
6015
|
message = copy.nonceConflict;
|
|
5630
6016
|
exit = EXIT.refused;
|
|
5631
6017
|
}
|
|
@@ -5651,9 +6037,25 @@ async function callTool(o, agentId, limits = {}) {
|
|
|
5651
6037
|
}
|
|
5652
6038
|
async function runTool(o) {
|
|
5653
6039
|
const { ctx, session, tool, flags } = o;
|
|
6040
|
+
if (isPreparedWrite(tool)) {
|
|
6041
|
+
const agentId2 = await resolveAgent(ctx, session, flags.agent);
|
|
6042
|
+
const r = await runPrepared({ ctx, session, catalogue: o.catalogue, tool, args: o.args, agentId: agentId2, json: o.json, dryRun: flags.dryRun, recovery: recoveryFlags(session, agentId2) });
|
|
6043
|
+
if (r.kind === "done") {
|
|
6044
|
+
if (r.doc.error === "unknown_tool" && o.onUnknownTool)
|
|
6045
|
+
await o.onUnknownTool().catch(() => {});
|
|
6046
|
+
return r.exit;
|
|
6047
|
+
}
|
|
6048
|
+
if (flags.dryRun)
|
|
6049
|
+
return EXIT.ok;
|
|
6050
|
+
return runDirect(o, agentId2);
|
|
6051
|
+
}
|
|
5654
6052
|
if (flags.dryRun)
|
|
5655
6053
|
return dryRun(o);
|
|
5656
6054
|
const agentId = tool.name === "list_agents" ? null : await resolveAgent(ctx, session, flags.agent);
|
|
6055
|
+
return runDirect(o, agentId);
|
|
6056
|
+
}
|
|
6057
|
+
async function runDirect(o, agentId) {
|
|
6058
|
+
const { ctx, session, tool } = o;
|
|
5657
6059
|
const out = await callTool(o, agentId);
|
|
5658
6060
|
if (out.message === "unknown_tool") {
|
|
5659
6061
|
if (o.onUnknownTool)
|
|
@@ -5661,12 +6063,12 @@ async function runTool(o) {
|
|
|
5661
6063
|
out.message = `\`darwin ${tool.cli.path.join(" ")}\` isn't available on ${session.realm} right now. Nothing was done. The command list was refreshed — see \`darwin help\`.`;
|
|
5662
6064
|
}
|
|
5663
6065
|
const result = out.body?.result ?? null;
|
|
5664
|
-
if (out.exit === EXIT.ok && result && QUOTE_TOOLS.has(tool.name))
|
|
5665
|
-
rememberQuote(o, result);
|
|
5666
6066
|
if (o.json) {
|
|
5667
6067
|
printJson(ctx, result ? { ...result, ...out.nonce ? { nonce: out.nonce } : {} } : { error: out.body?.error ?? "failed", detail: out.message, ...out.nonce ? { nonce: out.nonce } : {} });
|
|
5668
6068
|
if (out.message)
|
|
5669
6069
|
warn(ctx, out.message);
|
|
6070
|
+
for (const l of cliLines(out.body, "notes"))
|
|
6071
|
+
warn(ctx, l);
|
|
5670
6072
|
if (out.exit === EXIT.ok)
|
|
5671
6073
|
jsonFooter(o, out, result);
|
|
5672
6074
|
else if (out.nonce && tool.write)
|
|
@@ -5677,6 +6079,8 @@ async function runTool(o) {
|
|
|
5677
6079
|
if (out.exit === EXIT.ok && result) {
|
|
5678
6080
|
for (const l of renderResult(tool.name, session.kind, result, x))
|
|
5679
6081
|
say(ctx, l);
|
|
6082
|
+
for (const l of cliLines(out.body, "notes"))
|
|
6083
|
+
say(ctx, l);
|
|
5680
6084
|
try {
|
|
5681
6085
|
if (await afterHumanSuccess(o, result, agentId) === "failed")
|
|
5682
6086
|
return EXIT.refused;
|
|
@@ -5815,7 +6219,7 @@ function jsonFooter(o, out, result) {
|
|
|
5815
6219
|
const { ctx, tool, args } = o;
|
|
5816
6220
|
const summary = result?.summary ?? {};
|
|
5817
6221
|
const data = result?.data ?? {};
|
|
5818
|
-
const text = (v) => cell(
|
|
6222
|
+
const text = (v) => cell(unwrap5(v));
|
|
5819
6223
|
if (tool.name === "get_spot_quote") {
|
|
5820
6224
|
const quoteId = typeof data.quoteId === "string" ? data.quoteId : typeof summary.quoteId === "string" ? summary.quoteId : null;
|
|
5821
6225
|
const exp = typeof data.expiresAtMs === "number" ? data.expiresAtMs : null;
|
|
@@ -5831,75 +6235,12 @@ function jsonFooter(o, out, result) {
|
|
|
5831
6235
|
}
|
|
5832
6236
|
}
|
|
5833
6237
|
var agentFor = (o, out) => o.session.kind === "agents" ? o.flags.agent || o.session.profile?.default_agent || out.agentId || "this agent" : o.session.profile?.agent_name || "this agent";
|
|
5834
|
-
function quotesFile(o) {
|
|
5835
|
-
return join8(o.ctx.configDir, "cache", `quotes-${o.session.realm}-${createHash2("sha256").update(o.session.key).digest("hex").slice(0, 16)}.json`);
|
|
5836
|
-
}
|
|
5837
|
-
function readQuotes(o) {
|
|
5838
|
-
try {
|
|
5839
|
-
const t = readPrivate(quotesFile(o), 64 * 1024, { requirePrivateMode: false });
|
|
5840
|
-
const v = t ? JSON.parse(t) : [];
|
|
5841
|
-
return Array.isArray(v) ? v.filter((q) => !!q && typeof q.id === "string" && typeof q.expiresAtMs === "number") : [];
|
|
5842
|
-
} catch {
|
|
5843
|
-
return [];
|
|
5844
|
-
}
|
|
5845
|
-
}
|
|
5846
|
-
function rememberQuote(o, result) {
|
|
5847
|
-
const data = result.data ?? {};
|
|
5848
|
-
const sell = result.summary?.sell ?? {};
|
|
5849
|
-
if (typeof data.quoteId !== "string" || !/^[A-Za-z0-9_-]{1,80}$/.test(data.quoteId) || typeof data.expiresAtMs !== "number")
|
|
5850
|
-
return;
|
|
5851
|
-
const rec = {
|
|
5852
|
-
id: data.quoteId,
|
|
5853
|
-
sell: String(o.args.sell ?? ""),
|
|
5854
|
-
for: String(o.args.for ?? ""),
|
|
5855
|
-
amount: typeof sell.amount === "string" ? sell.amount : String(o.args.amount ?? ""),
|
|
5856
|
-
expiresAtMs: data.expiresAtMs
|
|
5857
|
-
};
|
|
5858
|
-
const now = o.ctx.now();
|
|
5859
|
-
const keep = readQuotes(o).filter((q) => q.expiresAtMs > now - 3600000 && q.id !== rec.id).slice(-19);
|
|
5860
|
-
try {
|
|
5861
|
-
writePrivate(quotesFile(o), JSON.stringify([...keep, rec]));
|
|
5862
|
-
} catch {}
|
|
5863
|
-
}
|
|
5864
|
-
var sameToken = (a, b) => {
|
|
5865
|
-
const mint = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
5866
|
-
return mint.test(a.trim()) || mint.test(b.trim()) ? a.trim() === b.trim() : a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
5867
|
-
};
|
|
5868
|
-
var sameAmount = (a, b) => {
|
|
5869
|
-
const n = (s) => (s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s).replace(/^0+(?=\d)/, "");
|
|
5870
|
-
return n(a.trim()) === n(b.trim());
|
|
5871
|
-
};
|
|
5872
6238
|
function dryRun(o) {
|
|
5873
6239
|
const { ctx, session, tool } = o;
|
|
5874
6240
|
const named = (o.flags.agent ?? ctx.env.DARWIN_AGENT ?? "").trim() || (session.kind === "agents" ? session.profile?.default_agent ?? "" : "");
|
|
5875
6241
|
if (session.kind === "agents" && tool.name !== "list_agents" && !named)
|
|
5876
6242
|
throw new CliError(EXIT.usage, copy.needAgent, "agent_required");
|
|
5877
6243
|
const agentId = named || null;
|
|
5878
|
-
const checked = [];
|
|
5879
|
-
const problems = [];
|
|
5880
|
-
const unchecked = [];
|
|
5881
|
-
if (tool.name === "place_spot_order" && typeof o.args.quoteId === "string") {
|
|
5882
|
-
const q = readQuotes(o).find((r) => r.id === o.args.quoteId);
|
|
5883
|
-
if (!q) {
|
|
5884
|
-
unchecked.push("the quote: this terminal didn't make it (or made it over an hour ago), so only Darwin can say whether it exists");
|
|
5885
|
-
} else {
|
|
5886
|
-
if (q.expiresAtMs <= ctx.now())
|
|
5887
|
-
problems.push(`the quote expired at ${new Date(q.expiresAtMs).toLocaleTimeString("en-US")}; get a new one with \`darwin quote\``);
|
|
5888
|
-
else
|
|
5889
|
-
checked.push(`the quote is one this terminal made, and it has ${Math.ceil((q.expiresAtMs - ctx.now()) / 1000)}s left`);
|
|
5890
|
-
const diffs = [
|
|
5891
|
-
!sameToken(String(o.args.sell ?? ""), q.sell) ? `--sell (quote: ${clean(q.sell)})` : "",
|
|
5892
|
-
!sameToken(String(o.args.for ?? ""), q.for) ? `--for (quote: ${clean(q.for)})` : "",
|
|
5893
|
-
!sameAmount(String(o.args.amount ?? ""), q.amount) ? `--amount (quote: ${q.amount})` : ""
|
|
5894
|
-
].filter(Boolean);
|
|
5895
|
-
if (diffs.length)
|
|
5896
|
-
problems.push(`this doesn't match the quote: ${diffs.join(", ")}`);
|
|
5897
|
-
else
|
|
5898
|
-
checked.push("--sell, --amount and --for match the quote");
|
|
5899
|
-
}
|
|
5900
|
-
}
|
|
5901
|
-
if (tool.write)
|
|
5902
|
-
unchecked.push("the balance, the agent's limits and the price — Darwin checks those when the command really runs");
|
|
5903
6244
|
const preview = {
|
|
5904
6245
|
dryRun: true,
|
|
5905
6246
|
sent: false,
|
|
@@ -5909,28 +6250,18 @@ function dryRun(o) {
|
|
|
5909
6250
|
costsTx: tool.costsTx,
|
|
5910
6251
|
realm: session.realm,
|
|
5911
6252
|
agent: agentId,
|
|
5912
|
-
arguments: o.args
|
|
5913
|
-
note: tool.idempotency ? "An order ID (nonce) is generated when the command really runs." : undefined
|
|
6253
|
+
arguments: o.args
|
|
5914
6254
|
};
|
|
5915
|
-
if (o.json)
|
|
6255
|
+
if (o.json)
|
|
5916
6256
|
printJson(ctx, preview);
|
|
5917
|
-
|
|
5918
|
-
warn(ctx, `Dry run check failed: ${p}`);
|
|
5919
|
-
return EXIT.ok;
|
|
5920
|
-
} else {
|
|
6257
|
+
else {
|
|
5921
6258
|
say(ctx, `Dry run — nothing was sent. Would run: darwin ${tool.cli.path.join(" ")} on ${session.realm}${agentId ? ` for agent ${clean(agentId)}` : ""}`);
|
|
5922
6259
|
for (const [k, v] of Object.entries(o.args)) {
|
|
5923
6260
|
const flag = tool.cli.args[k]?.flag;
|
|
5924
6261
|
say(ctx, flag ? ` --${flag} ${cell(v)}` : tool.cli.positional.includes(k) ? ` <${clean(k)}> ${cell(v)}` : ` --field ${clean(k)}=${cell(v)}`);
|
|
5925
6262
|
}
|
|
5926
|
-
for (const c of checked)
|
|
5927
|
-
say(ctx, `✓ ${c}`);
|
|
5928
|
-
for (const p of problems)
|
|
5929
|
-
say(ctx, `✗ ${p}`);
|
|
5930
|
-
for (const u of unchecked)
|
|
5931
|
-
say(ctx, `Not checked: ${u}`);
|
|
5932
6263
|
}
|
|
5933
|
-
return
|
|
6264
|
+
return EXIT.ok;
|
|
5934
6265
|
}
|
|
5935
6266
|
|
|
5936
6267
|
// src/mcp.ts
|
|
@@ -5950,7 +6281,29 @@ function printConfig(ctx, client, profile) {
|
|
|
5950
6281
|
`;
|
|
5951
6282
|
throw new CliError(EXIT.usage, "--print-config takes claude, cursor or gemini.", "usage");
|
|
5952
6283
|
}
|
|
6284
|
+
var PREPARED_TOOL_DEFS = [
|
|
6285
|
+
{
|
|
6286
|
+
name: "check_prepared",
|
|
6287
|
+
title: "What happened to a write",
|
|
6288
|
+
description: "READ-ONLY. What happened to a write whose result was uncertain, by the preparedId the write returned: it went through, is still pending, failed, or was never sent. Never sends anything; call it instead of repeating a write.",
|
|
6289
|
+
inputSchema: { type: "object", properties: { preparedId: { type: "string", pattern: "^prp_[0-9A-Za-z]{24}$" } }, required: ["preparedId"], additionalProperties: false },
|
|
6290
|
+
annotations: { title: "What happened to a write", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
6291
|
+
},
|
|
6292
|
+
{
|
|
6293
|
+
name: "cancel_prepared",
|
|
6294
|
+
title: "Make sure a write never runs",
|
|
6295
|
+
description: "Make sure a prepared write that has not started never runs (by its preparedId). A write that already started can't be cancelled; the answer says what happened instead.",
|
|
6296
|
+
inputSchema: { type: "object", properties: { preparedId: { type: "string", pattern: "^prp_[0-9A-Za-z]{24}$" } }, required: ["preparedId"], additionalProperties: false },
|
|
6297
|
+
annotations: { title: "Make sure a write never runs", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
6298
|
+
}
|
|
6299
|
+
];
|
|
5953
6300
|
function toolDefs(c) {
|
|
6301
|
+
return [...catalogueToolDefs(c), ...PREPARED_TOOL_DEFS.map((d) => ({
|
|
6302
|
+
...d,
|
|
6303
|
+
inputSchema: c.globals.agentParam ? { ...d.inputSchema, properties: { ...d.inputSchema.properties, agent: AGENT_PROP } } : d.inputSchema
|
|
6304
|
+
}))];
|
|
6305
|
+
}
|
|
6306
|
+
function catalogueToolDefs(c) {
|
|
5954
6307
|
return c.tools.map((t) => {
|
|
5955
6308
|
let schema = t.inputSchema;
|
|
5956
6309
|
if (c.globals.agentParam && t.name !== "list_agents") {
|
|
@@ -6055,6 +6408,20 @@ function toolResult(value, isError) {
|
|
|
6055
6408
|
return { content: [{ type: "text", text: JSON.stringify(v) }], structuredContent: v, isError };
|
|
6056
6409
|
}
|
|
6057
6410
|
async function mcpCall(ctx, session, catalogue, params, refresh) {
|
|
6411
|
+
if (params.name === "check_prepared" || params.name === "cancel_prepared") {
|
|
6412
|
+
const raw2 = params.arguments ?? {};
|
|
6413
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2) || typeof raw2.preparedId !== "string")
|
|
6414
|
+
return toolResult({ error: "invalid_arguments", detail: "preparedId is required." }, true);
|
|
6415
|
+
let agentArg2;
|
|
6416
|
+
if (raw2.agent !== undefined) {
|
|
6417
|
+
if (typeof raw2.agent !== "string" || !raw2.agent.trim())
|
|
6418
|
+
return toolResult({ error: "invalid_arguments", detail: "`agent` must be an agent id or name." }, true);
|
|
6419
|
+
agentArg2 = raw2.agent.trim();
|
|
6420
|
+
}
|
|
6421
|
+
const agentId2 = await recoveryAgent(ctx, session, agentArg2);
|
|
6422
|
+
const r = await preparedRead({ ctx, session, agentId: agentId2 }, raw2.preparedId, params.name === "cancel_prepared");
|
|
6423
|
+
return toolResult(r.body, !r.ok);
|
|
6424
|
+
}
|
|
6058
6425
|
const tool = catalogue.tools.find((t) => t.name === params.name);
|
|
6059
6426
|
if (!tool)
|
|
6060
6427
|
return toolResult({ error: "unknown_tool", detail: "No such tool; list the tools again.", sent: false }, true);
|
|
@@ -6072,6 +6439,24 @@ async function mcpCall(ctx, session, catalogue, params, refresh) {
|
|
|
6072
6439
|
delete args.agent;
|
|
6073
6440
|
}
|
|
6074
6441
|
const agentId = tool.name === "list_agents" ? null : await resolveAgent(ctx, session, agentArg);
|
|
6442
|
+
if (isPreparedWrite(tool)) {
|
|
6443
|
+
const r = await runPrepared({ ctx, session, catalogue, tool, args, agentId, json: true, dryRun: false, quietForMcp: true, recovery: recoveryFlags(session, agentId) });
|
|
6444
|
+
if (r.kind === "done") {
|
|
6445
|
+
if (r.exit === EXIT.ok)
|
|
6446
|
+
return toolResult(r.doc, false);
|
|
6447
|
+
if (r.exit === EXIT.uncertain && r.preparedId) {
|
|
6448
|
+
return toolResult({
|
|
6449
|
+
...r.doc,
|
|
6450
|
+
error: "outcome_unknown",
|
|
6451
|
+
sent: "maybe",
|
|
6452
|
+
preparedId: r.preparedId,
|
|
6453
|
+
...agentId ? { agent: agentId } : {},
|
|
6454
|
+
detail: `Darwin may have received this write, but its result didn't come back. Do NOT call this tool again — call check_prepared with preparedId "${r.preparedId}"${agentId ? ` and agent "${agentId}"` : ""} to see what happened.`
|
|
6455
|
+
}, true);
|
|
6456
|
+
}
|
|
6457
|
+
return toolResult(r.doc, true);
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6075
6460
|
const out = await callTool({ ctx, session, catalogue, tool, args, flags: { json: true, dryRun: false, quiet: true }, onStaleCatalogue: refresh }, agentId);
|
|
6076
6461
|
const result = out.body?.result ?? null;
|
|
6077
6462
|
if (out.exit === EXIT.uncertain) {
|
|
@@ -6216,6 +6601,10 @@ async function dispatch(p, ctx) {
|
|
|
6216
6601
|
return cmdApi(ctx, p);
|
|
6217
6602
|
case "mcp":
|
|
6218
6603
|
return cmdMcp(ctx, p);
|
|
6604
|
+
case "retry":
|
|
6605
|
+
return cmdCheckPrepared(ctx, p, false);
|
|
6606
|
+
case "cancel":
|
|
6607
|
+
return cmdCheckPrepared(ctx, p, true);
|
|
6219
6608
|
}
|
|
6220
6609
|
if (first === "market-status" && !has(p, "help"))
|
|
6221
6610
|
return cmdMarketStatus(ctx, p);
|
|
@@ -6258,7 +6647,7 @@ async function runCatalogueCommand(ctx, p, json) {
|
|
|
6258
6647
|
const dryRun2 = has(p, "dry-run");
|
|
6259
6648
|
let { catalogue } = await loadCatalogue(ctx, session.realm, session.kind, { key: session.key, refresh: dryRun2 ? "offline" : "never" });
|
|
6260
6649
|
let hit = resolve(catalogue, p.positionals);
|
|
6261
|
-
if (!hit
|
|
6650
|
+
if (!hit) {
|
|
6262
6651
|
catalogue = (await loadCatalogue(ctx, session.realm, session.kind, { key: session.key, refresh: "force" })).catalogue;
|
|
6263
6652
|
hit = resolve(catalogue, p.positionals);
|
|
6264
6653
|
}
|
|
@@ -6293,7 +6682,7 @@ function updateNotice(ctx, c) {
|
|
|
6293
6682
|
return;
|
|
6294
6683
|
if (compareVersions(VERSION, c.latestCli) >= 0)
|
|
6295
6684
|
return;
|
|
6296
|
-
const stamp =
|
|
6685
|
+
const stamp = join8(ctx.configDir, "cache", "update-notice");
|
|
6297
6686
|
try {
|
|
6298
6687
|
const last = Number(readPrivate(stamp, 64, { requirePrivateMode: false }) ?? "0");
|
|
6299
6688
|
if (ctx.now() - last < 24 * 3600000)
|
|
@@ -6310,8 +6699,8 @@ function configDir() {
|
|
|
6310
6699
|
if (process.env.DARWIN_CONFIG_DIR)
|
|
6311
6700
|
return process.env.DARWIN_CONFIG_DIR;
|
|
6312
6701
|
if (process.platform === "win32")
|
|
6313
|
-
return
|
|
6314
|
-
return
|
|
6702
|
+
return join9(process.env.APPDATA || join9(homedir2(), "AppData", "Roaming"), "darwin");
|
|
6703
|
+
return join9(process.env.XDG_CONFIG_HOME || join9(homedir2(), ".config"), "darwin");
|
|
6315
6704
|
}
|
|
6316
6705
|
async function readAll() {
|
|
6317
6706
|
const chunks = [];
|
|
@@ -6366,7 +6755,7 @@ var ctx = {
|
|
|
6366
6755
|
cwd: process.cwd(),
|
|
6367
6756
|
platform: process.platform,
|
|
6368
6757
|
openUrl: (url) => {
|
|
6369
|
-
const [cmd, args] = process.platform === "darwin" ? ["/usr/bin/open", [url]] : process.platform === "win32" ? [
|
|
6758
|
+
const [cmd, args] = process.platform === "darwin" ? ["/usr/bin/open", [url]] : process.platform === "win32" ? [join9(process.env.SystemRoot || "C:\\Windows", "System32", "rundll32.exe"), ["url.dll,FileProtocolHandler", url]] : ["/usr/bin/xdg-open", [url]];
|
|
6370
6759
|
if (!existsSync(cmd))
|
|
6371
6760
|
return;
|
|
6372
6761
|
spawn2(cmd, args, { stdio: "ignore", detached: true }).unref();
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darwin.finance/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@darwin.finance/cli",
|
|
9
|
-
"version": "1.0
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@napi-rs/keyring": "2.1.0"
|