@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 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
- > **Not on npm yet.** `@darwin.finance/cli` has not been published. Until it is, install from source
11
- > (below) for testing only.
10
+ ## Install
12
11
 
13
- ## Install (from source, for testing)
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
- Once published, the install line is `npm i -g @darwin.finance/cli`. Always install it — never run it
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
- `darwin mcp` runs the same commands as a local MCP server for desktop AI clients;
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 exits 6
63
- and says to check `darwin orders` rather than run it again. There is no `[y/N]` prompt; use
64
- `--dry-run` to see what a command would send. Writes and reads are distinct commands, so your
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; check `darwin orders` |
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.1";
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 join10 } from "node:path";
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 join9 } from "node:path";
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": "Show what a command would send, and send nothing.",
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 — check `darwin orders` to see whether it went through.",
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 see what it would send.`);
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 unwrap2 = (v) => v && typeof v === "object" && ("untrusted" in v) ? String(v.untrusted ?? "") : typeof v === "string" ? v : "";
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(unwrap2(who?.homeAgentName) || s.profile?.agent_name || s.profile?.agent_id || "unknown agent");
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 unwrap3 = (v) => v && typeof v === "object" && ("untrusted" in v) ? String(v.untrusted ?? "") : typeof v === "string" ? v : "";
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(unwrap3(w.homeAgentName)).slice(0, 120), keyName: typeof w.name === "string" ? clean(w.name).slice(0, 120) : null };
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(unwrap3(first.name)).slice(0, 120), keyName: null };
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(unwrap3(agent.name)).slice(0, 120);
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 unwrap4 = (v) => v && typeof v === "object" && !Array.isArray(v) && ("untrusted" in v) ? v.untrusted : v;
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) => unwrap4(x) === "grant_paused");
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 (unwrap4(data.refusal) === "duplicate" && unwrap4(data.detail) === "nonce_used_by_a_different_order") {
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(unwrap4(v));
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
- for (const p of problems)
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 problems.length ? EXIT.refused : EXIT.ok;
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 && !dryRun2) {
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 = join9(ctx.configDir, "cache", "update-notice");
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 join10(process.env.APPDATA || join10(homedir2(), "AppData", "Roaming"), "darwin");
6314
- return join10(process.env.XDG_CONFIG_HOME || join10(homedir2(), ".config"), "darwin");
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" ? [join10(process.env.SystemRoot || "C:\\Windows", "System32", "rundll32.exe"), ["url.dll,FileProtocolHandler", url]] : ["/usr/bin/xdg-open", [url]];
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();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@darwin.finance/cli",
3
- "version": "1.0.1",
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.1",
9
+ "version": "1.1.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@napi-rs/keyring": "2.1.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darwin.finance/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Darwin CLI \u2014 trade and check your Darwin agent from a terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",