@heyanon-arp/cli 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -333,6 +333,31 @@ var init_api = __esm({
333
333
  async rotateIdentityKey(did, body, signer) {
334
334
  return this.signedRequest("POST", `/v1/agents/${encodeURIComponent(did)}/rotate-identity-key`, body, signer);
335
335
  }
336
+ // ─── Webhook config + secret ────────────────────────────────────
337
+ //
338
+ // All routes are `/me/...` — the server derives the agent from
339
+ // the signer DID, so the CLI never sends a `:did` path parameter.
340
+ // The signedRequest helper signs the canonical request (method +
341
+ // path + query + body) with the agent's identity key; that's the
342
+ // auth contract the SignedRequestGuard checks server-side.
343
+ async getMyWebhookConfig(signer) {
344
+ return this.signedRequest("GET", "/v1/agents/me/webhook-config", null, signer);
345
+ }
346
+ async setMyWebhookConfig(body, signer) {
347
+ return this.signedRequest("POST", "/v1/agents/me/webhook-config", body, signer);
348
+ }
349
+ async getMyWebhookSecretStatus(signer) {
350
+ return this.signedRequest("GET", "/v1/agents/me/webhook-secret", null, signer);
351
+ }
352
+ async initMyWebhookSecret(signer) {
353
+ return this.signedRequest("POST", "/v1/agents/me/webhook-secret/init", {}, signer);
354
+ }
355
+ async rotateStageMyWebhookSecret(signer) {
356
+ return this.signedRequest("POST", "/v1/agents/me/webhook-secret/rotate-stage", {}, signer);
357
+ }
358
+ async rotateCommitMyWebhookSecret(body, signer) {
359
+ return this.signedRequest("POST", "/v1/agents/me/webhook-secret/rotate-commit", body, signer);
360
+ }
336
361
  /**
337
362
  * Ingest a signed envelope. Endpoint is public (no
338
363
  * `X-ARP-Signer-DID` headers) — authentication is the envelope's
@@ -431,9 +456,7 @@ var init_api = __esm({
431
456
  * known but has never been party to an event.
432
457
  */
433
458
  async getActivitySummary(did) {
434
- return this.get(
435
- `/v1/agents/${encodeURIComponent(did)}/activity-summary`
436
- );
459
+ return this.get(`/v1/agents/${encodeURIComponent(did)}/activity-summary`);
437
460
  }
438
461
  /**
439
462
  * Signed `GET /v1/agents/:did/sender-sequence`. Returns the highest
@@ -712,27 +735,17 @@ var import_simple_update_notifier = __toESM(require("simple-update-notifier"));
712
735
  // package.json
713
736
  var package_default = {
714
737
  name: "@heyanon-arp/cli",
715
- version: "0.0.3",
738
+ version: "0.0.5",
716
739
  description: "Command-line client for the Agent Relationship Protocol \u2014 register agents, sign envelopes, run escrowed work cycles on Solana.",
717
740
  license: "MIT",
718
- keywords: [
719
- "arp",
720
- "agent-relationship-protocol",
721
- "did",
722
- "solana",
723
- "escrow",
724
- "ed25519",
725
- "agents",
726
- "a2a",
727
- "cli"
728
- ],
741
+ keywords: ["arp", "agent-relationship-protocol", "did", "solana", "escrow", "ed25519", "agents", "a2a", "cli"],
729
742
  bin: {
730
743
  heyarp: "./dist/cli.js"
731
744
  },
732
745
  publishConfig: {
733
746
  access: "public"
734
747
  },
735
- files: ["dist", "LICENSE", "README.md"],
748
+ files: ["dist", "examples", "LICENSE", "README.md"],
736
749
  engines: {
737
750
  node: ">=22"
738
751
  },
@@ -765,9 +778,6 @@ var package_default = {
765
778
  }
766
779
  };
767
780
 
768
- // src/cli.ts
769
- init_api();
770
-
771
781
  // src/commands/agents.ts
772
782
  var import_chalk2 = __toESM(require("chalk"));
773
783
  init_api();
@@ -789,15 +799,39 @@ function formatGenericError(err, verbose = false) {
789
799
  return `${import_chalk.default.red("Error")} ${message}
790
800
  ${import_chalk.default.gray(err.stack)}`;
791
801
  }
792
- function formatActionError(err, command) {
793
- let cmd = command;
794
- while (cmd.parent) cmd = cmd.parent;
795
- const verbose = !!cmd.opts().trace;
802
+ function toCliErrorJson(err, includeStack = false) {
803
+ const { ApiError: ApiError2 } = (init_api(), __toCommonJS(api_exports));
804
+ if (err instanceof ApiError2) {
805
+ const { code, message: message2, details } = err.payload;
806
+ const out2 = { code, message: message2 };
807
+ if (details !== void 0) out2.details = details;
808
+ return out2;
809
+ }
810
+ const message = err instanceof Error ? err.message : String(err);
811
+ const out = { code: "CLI_ERROR", message };
812
+ if (includeStack && err instanceof Error && err.stack) {
813
+ out.details = { stack: err.stack };
814
+ }
815
+ return out;
816
+ }
817
+ function emitError(err, opts) {
818
+ if (opts.json) {
819
+ console.error(JSON.stringify(toCliErrorJson(err, opts.verbose)));
820
+ return;
821
+ }
796
822
  const { ApiError: ApiError2 } = (init_api(), __toCommonJS(api_exports));
797
823
  if (err instanceof ApiError2) {
798
- return formatApiError(err.payload, verbose);
824
+ console.error(formatApiError(err.payload, opts.verbose));
825
+ } else {
826
+ console.error(formatGenericError(err, opts.verbose));
799
827
  }
800
- return formatGenericError(err, verbose);
828
+ }
829
+ function emitActionError(err, command) {
830
+ let root = command;
831
+ while (root.parent) root = root.parent;
832
+ const verbose = !!root.opts().trace;
833
+ const json = !!command.opts().json;
834
+ emitError(err, { json, verbose });
801
835
  }
802
836
  function formatJson(value) {
803
837
  return JSON.stringify(value, null, 2);
@@ -837,7 +871,18 @@ function supportsUnicodeFrame() {
837
871
  return probe2.includes("UTF-8") || probe2.includes("UTF8");
838
872
  }
839
873
  function printJsonArray(rows) {
840
- console.log(JSON.stringify(rows, null, 2));
874
+ jsonOut(rows);
875
+ }
876
+ function jsonOut(value) {
877
+ console.log(formatJson(value));
878
+ }
879
+ function progress(jsonMode, ...args) {
880
+ if (jsonMode) return;
881
+ console.log(...args);
882
+ }
883
+ function warn(jsonMode, ...args) {
884
+ if (jsonMode) return;
885
+ console.error(...args);
841
886
  }
842
887
  function formatAgentsTable(rows) {
843
888
  if (rows.length === 0) return import_chalk.default.dim("(no agents registered locally)");
@@ -876,11 +921,7 @@ function registerAgentsCommand(root) {
876
921
  async function runAgents(opts) {
877
922
  const limit = parseLimit(opts.limit);
878
923
  const api = new ArpApiClient(opts.server);
879
- if (opts.json) {
880
- console.error(import_chalk2.default.dim(`Server: ${api.serverUrl}`));
881
- } else {
882
- console.log(import_chalk2.default.dim(`Server: ${api.serverUrl}`));
883
- }
924
+ progress(opts.json, import_chalk2.default.dim(`Server: ${api.serverUrl}`));
884
925
  const query = { limit };
885
926
  if (opts.tag && opts.tag.length > 0) query.tag = opts.tag.map((t) => t.trim().toLowerCase());
886
927
  if (opts.query) query.q = opts.query;
@@ -1111,18 +1152,42 @@ function writeStateFile(state) {
1111
1152
  (0, import_node_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
1112
1153
  }
1113
1154
  const body = JSON.stringify(state, null, 2);
1114
- (0, import_node_fs2.writeFileSync)(path, body, { encoding: "utf8", mode: 384 });
1155
+ const tmpPath = `${path}.tmp.${process.pid}`;
1156
+ const fd = (0, import_node_fs2.openSync)(tmpPath, "w", 384);
1157
+ try {
1158
+ (0, import_node_fs2.writeSync)(fd, body, 0, "utf8");
1159
+ (0, import_node_fs2.fsyncSync)(fd);
1160
+ } finally {
1161
+ (0, import_node_fs2.closeSync)(fd);
1162
+ }
1163
+ let chmodOk = true;
1164
+ try {
1165
+ (0, import_node_fs2.chmodSync)(tmpPath, 384);
1166
+ } catch {
1167
+ chmodOk = false;
1168
+ }
1169
+ try {
1170
+ (0, import_node_fs2.renameSync)(tmpPath, path);
1171
+ } catch (err) {
1172
+ try {
1173
+ (0, import_node_fs2.unlinkSync)(tmpPath);
1174
+ } catch {
1175
+ }
1176
+ throw err;
1177
+ }
1115
1178
  try {
1116
1179
  (0, import_node_fs2.chmodSync)(path, 384);
1117
1180
  } catch {
1181
+ chmodOk = false;
1118
1182
  }
1183
+ return { chmodOk };
1119
1184
  }
1120
1185
  function saveAgent(serverOverride, agent) {
1121
1186
  const key = resolveServerUrl(serverOverride);
1122
1187
  const state = readStateFile();
1123
1188
  if (!state.servers[key]) state.servers[key] = { agents: {} };
1124
1189
  state.servers[key].agents[agent.did] = agent;
1125
- writeStateFile(state);
1190
+ return writeStateFile(state);
1126
1191
  }
1127
1192
  function loadAgent(serverOverride, did) {
1128
1193
  const key = resolveServerUrl(serverOverride);
@@ -1145,7 +1210,7 @@ function updateAgentLocal(serverOverride, did, patch) {
1145
1210
  throw new Error(`Cannot update local state \u2014 no record for ${did} on ${key}.`);
1146
1211
  }
1147
1212
  server.agents[did] = { ...server.agents[did], ...patch };
1148
- writeStateFile(state);
1213
+ return writeStateFile(state);
1149
1214
  }
1150
1215
  function resolveSenderAgent(cmdName, serverOverride, explicitFromDid) {
1151
1216
  const resolvedServerUrl = resolveServerUrl(serverOverride);
@@ -1177,8 +1242,8 @@ function listAgents() {
1177
1242
  }
1178
1243
 
1179
1244
  // src/commands/delegation.ts
1180
- var import_sdk4 = require("@heyanon-arp/sdk");
1181
1245
  var import_node_fs4 = require("fs");
1246
+ var import_sdk4 = require("@heyanon-arp/sdk");
1182
1247
  var import_chalk6 = __toESM(require("chalk"));
1183
1248
  init_api();
1184
1249
 
@@ -1256,14 +1321,6 @@ ${verb}.`));
1256
1321
  console.log(formatJson(agent));
1257
1322
  }
1258
1323
 
1259
- // src/commands/wallet.ts
1260
- var import_sdk3 = require("@heyanon-arp/sdk");
1261
- var import_utils = require("@noble/hashes/utils");
1262
- var import_web3 = require("@solana/web3.js");
1263
- var import_node_fs3 = require("fs");
1264
- init_api();
1265
- init_config();
1266
-
1267
1324
  // src/commands/status.ts
1268
1325
  var import_sdk2 = require("@heyanon-arp/sdk");
1269
1326
  var import_chalk5 = __toESM(require("chalk"));
@@ -1349,6 +1406,25 @@ async function runStatus(relationshipId, opts) {
1349
1406
  process.exitCode = 124;
1350
1407
  }
1351
1408
  }
1409
+ async function awaitFsmTransitionAfterAction(input) {
1410
+ const { api, signerDid, signer, relationshipId, untilPhase, waitIntervalSec, waitTimeoutSec, waitVerbose, json } = input;
1411
+ if (!json) {
1412
+ console.log(import_chalk5.default.dim(`
1413
+ [--wait-until ${untilPhase}] polling relationship ${relationshipId} (interval=${waitIntervalSec}s timeout=${waitTimeoutSec}s)`));
1414
+ }
1415
+ const outcome = await runWaitLoop({
1416
+ fetchSummary: () => composeStatus(api, signerDid, relationshipId, signer),
1417
+ waitIntervalSec,
1418
+ waitTimeoutSec,
1419
+ waitVerbose: !!waitVerbose,
1420
+ json: !!json,
1421
+ log: (line) => console.log(line),
1422
+ until: untilPhase
1423
+ });
1424
+ if (outcome.timedOut) {
1425
+ process.exitCode = 124;
1426
+ }
1427
+ }
1352
1428
  async function runWaitLoop(opts) {
1353
1429
  const isTerminal = (s) => s.cycleComplete || s.relationshipState === "closed" || s.relationshipState === "not_found";
1354
1430
  const isActionable = (s) => {
@@ -1382,8 +1458,12 @@ async function runWaitLoop(opts) {
1382
1458
  if (opts.json) opts.log(JSON.stringify(terminalUnmatchedJson ? { ...initial, _waitTimedOut: true } : initial));
1383
1459
  else {
1384
1460
  if (opts.until !== void 0) {
1385
- opts.log(import_chalk5.default.yellow(`
1386
- [--wait] Terminal state (${initial.relationshipState}, cycleComplete=${initial.cycleComplete}) reached before phase '${opts.until}' \u2014 exiting; phase unreachable.`));
1461
+ opts.log(
1462
+ import_chalk5.default.yellow(
1463
+ `
1464
+ [--wait] Terminal state (${initial.relationshipState}, cycleComplete=${initial.cycleComplete}) reached before phase '${opts.until}' \u2014 exiting; phase unreachable.`
1465
+ )
1466
+ );
1387
1467
  } else {
1388
1468
  opts.log(import_chalk5.default.dim(`
1389
1469
  [--wait] Already terminal \u2014 exiting.`));
@@ -1433,7 +1513,11 @@ async function runWaitLoop(opts) {
1433
1513
  if (phaseReached) {
1434
1514
  opts.log(import_chalk5.default.green(`[--wait] Phase '${opts.until}' reached.`));
1435
1515
  } else {
1436
- opts.log(import_chalk5.default.yellow(`[--wait] Terminal state (${next.relationshipState}, cycleComplete=${next.cycleComplete}) reached before phase '${opts.until}' \u2014 phase unreachable.`));
1516
+ opts.log(
1517
+ import_chalk5.default.yellow(
1518
+ `[--wait] Terminal state (${next.relationshipState}, cycleComplete=${next.cycleComplete}) reached before phase '${opts.until}' \u2014 phase unreachable.`
1519
+ )
1520
+ );
1437
1521
  }
1438
1522
  } else {
1439
1523
  opts.log(import_chalk5.default.green(`[--wait] ${isTerminal(next) ? "Cycle terminated" : `Your turn (owner=${next.nextActionOwner})`}.`));
@@ -1448,11 +1532,17 @@ async function runWaitLoop(opts) {
1448
1532
  opts.log(JSON.stringify({ ...last, _waitTimedOut: true }));
1449
1533
  } else {
1450
1534
  if (opts.until !== void 0) {
1451
- opts.log(import_chalk5.default.yellow(`
1452
- [--wait] Timed out after ${opts.waitTimeoutSec}s without reaching phase '${opts.until}' (latest state: ${last.relationshipState}, hint: ${last.nextActionHint}).`));
1535
+ opts.log(
1536
+ import_chalk5.default.yellow(
1537
+ `
1538
+ [--wait] Timed out after ${opts.waitTimeoutSec}s without reaching phase '${opts.until}' (latest state: ${last.relationshipState}, hint: ${last.nextActionHint}).`
1539
+ )
1540
+ );
1453
1541
  } else {
1454
- opts.log(import_chalk5.default.yellow(`
1455
- [--wait] Timed out after ${opts.waitTimeoutSec}s without an actionable or terminal transition (your turn never came, cycle still in flight).`));
1542
+ opts.log(
1543
+ import_chalk5.default.yellow(`
1544
+ [--wait] Timed out after ${opts.waitTimeoutSec}s without an actionable or terminal transition (your turn never came, cycle still in flight).`)
1545
+ );
1456
1546
  }
1457
1547
  }
1458
1548
  return { timedOut: true, last };
@@ -1533,7 +1623,7 @@ function parseWaitInterval(raw) {
1533
1623
  return n;
1534
1624
  }
1535
1625
  function sleep(ms) {
1536
- return new Promise((resolve) => setTimeout(resolve, ms));
1626
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1537
1627
  }
1538
1628
  async function composeStatus(api, signerDid, relationshipId, signer) {
1539
1629
  const [relationshipsOrNull, contractsOrError] = await Promise.all([
@@ -1574,13 +1664,7 @@ async function composeStatus(api, signerDid, relationshipId, signer) {
1574
1664
  const delegations = latestContract ? await fetchAllPages(
1575
1665
  (after) => api.listDelegations(relationshipId, signer, { limit: 100, contractId: latestContract.contractId, ...after ? { after } : {} })
1576
1666
  ) : [];
1577
- const latestDelegation = pickLatestLive(delegations, [
1578
- "proposed",
1579
- "offered",
1580
- "pending_lock_finalization",
1581
- "accepted",
1582
- "awaiting_release_finalization"
1583
- ]);
1667
+ const latestDelegation = pickLatestLive(delegations, ["proposed", "offered", "pending_lock_finalization", "accepted", "awaiting_release_finalization"]);
1584
1668
  const [workLogs, receipts] = await Promise.all([
1585
1669
  latestDelegation ? fetchAllPages(
1586
1670
  (after) => api.listWorkLogs(relationshipId, signer, { limit: 100, delegationId: latestDelegation.delegationId, ...after ? { after } : {} })
@@ -1650,9 +1734,7 @@ function findReceiptForWorkLog(receipts, workLog, allWorkLogs = [workLog]) {
1650
1734
  const expectedResponseHash = (0, import_sdk2.canonicalSha256Hex)(responseBody);
1651
1735
  const matches = receipts.filter((r) => r.requestHash === expectedRequestHash && r.responseHash === expectedResponseHash);
1652
1736
  if (matches.length > 0) return pickLatest(matches);
1653
- const respondedSiblings = allWorkLogs.filter(
1654
- (wl) => wl.delegationId === workLog.delegationId && (wl.responseOutput !== void 0 || wl.responseError !== void 0)
1655
- ).length;
1737
+ const respondedSiblings = allWorkLogs.filter((wl) => wl.delegationId === workLog.delegationId && (wl.responseOutput !== void 0 || wl.responseError !== void 0)).length;
1656
1738
  if (respondedSiblings > 1) return null;
1657
1739
  const sameDelegation = receipts.filter((r) => r.delegationId === workLog.delegationId);
1658
1740
  return pickLatest(sameDelegation);
@@ -1878,6 +1960,12 @@ function stateColor(state) {
1878
1960
  }
1879
1961
 
1880
1962
  // src/commands/wallet.ts
1963
+ var import_node_fs3 = require("fs");
1964
+ var import_sdk3 = require("@heyanon-arp/sdk");
1965
+ var import_utils = require("@noble/hashes/utils");
1966
+ var import_web3 = require("@solana/web3.js");
1967
+ init_api();
1968
+ init_config();
1881
1969
  function bytesToBase64(bytes) {
1882
1970
  return Buffer.from(bytes).toString("base64");
1883
1971
  }
@@ -1967,22 +2055,10 @@ function registerDerivePdas(cmd) {
1967
2055
  ).requiredOption("--delegation-id <id>", "Delegation UUID (canonical `del_<uuid>` or bare `<uuid>` \u2014 both accepted)").option("--server <url>", "Override server URL (used only for --program-id auto-discovery)").option(
1968
2056
  "--program-id <pubkey>",
1969
2057
  `Deployed ARP escrow program id. Precedence: --program-id flag > ARP_ESCROW_PROGRAM_ID env > GET /v1/escrow/protocol-fee (auto-discover) > hardcoded fallback (${FALLBACK_PROGRAM_ID}).`
1970
- ).option(
1971
- "--rpc-url <url>",
1972
- "Accepted for symmetry with `wallet verify-release` but never read \u2014 PDAs are derived locally from `program_id + delegation_id`; no RPC needed."
1973
- ).option(
2058
+ ).option("--rpc-url <url>", "Accepted for symmetry with `wallet verify-release` but never read \u2014 PDAs are derived locally from `program_id + delegation_id`; no RPC needed.").option(
1974
2059
  "--recipient-pubkey <base58>",
1975
2060
  "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id; recipient pubkey has no derivation role."
1976
- ).option(
1977
- "--condition-hash <hex>",
1978
- "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id."
1979
- ).option(
1980
- "--amount-lamports <int>",
1981
- "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id."
1982
- ).option(
1983
- "--expiry-secs <int>",
1984
- "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id."
1985
- ).option("--json", "Emit JSON instead of human text (jq-pipeable: `.lock_pda`, `.escrow_pda`, `.config_pda`, `.lock_id_hex`).", false).action(async (opts) => {
2061
+ ).option("--condition-hash <hex>", "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id.").option("--amount-lamports <int>", "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id.").option("--expiry-secs <int>", "Accepted for symmetry with `wallet create-lock` but never read \u2014 PDAs depend ONLY on program_id + delegation_id.").option("--json", "Emit JSON instead of human text (jq-pipeable: `.lock_pda`, `.escrow_pda`, `.config_pda`, `.lock_id_hex`).", false).action(async (opts) => {
1986
2062
  try {
1987
2063
  const out = await derivePdasHandler(opts);
1988
2064
  if (opts.json) {
@@ -1997,7 +2073,7 @@ function registerDerivePdas(cmd) {
1997
2073
  console.log(`EventAuthPDA: ${out.event_authority_pda}`);
1998
2074
  }
1999
2075
  } catch (err) {
2000
- console.error(err.message);
2076
+ emitError(err, { json: opts.json, verbose: process.argv.includes("--trace") });
2001
2077
  process.exit(1);
2002
2078
  }
2003
2079
  });
@@ -2059,7 +2135,7 @@ function registerVerifyRelease(cmd) {
2059
2135
  process.exitCode = 1;
2060
2136
  }
2061
2137
  } catch (err) {
2062
- console.error(err.message);
2138
+ emitError(err, { json: opts.json, verbose: process.argv.includes("--trace") });
2063
2139
  process.exit(1);
2064
2140
  }
2065
2141
  });
@@ -2100,7 +2176,7 @@ async function verifyReleaseHandler(opts) {
2100
2176
  if (shouldRetrySigCheck) {
2101
2177
  const retryDelaysMs = [3e3, 3e3];
2102
2178
  for (const delay of retryDelaysMs) {
2103
- await new Promise((resolve) => setTimeout(resolve, delay));
2179
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
2104
2180
  const retriedSigs = await conn.getSignaturesForAddress(lockPda, { limit: 5 });
2105
2181
  if (retriedSigs.length > 0) {
2106
2182
  lockSeenInSignatures = true;
@@ -2240,7 +2316,7 @@ function registerCreateLock(cmd) {
2240
2316
  const out = await createLockHandler(opts);
2241
2317
  console.log(JSON.stringify(out, null, 2));
2242
2318
  } catch (err) {
2243
- console.error(err.message);
2319
+ emitError(err, { json: opts.json, verbose: process.argv.includes("--trace") });
2244
2320
  process.exit(1);
2245
2321
  }
2246
2322
  });
@@ -2260,7 +2336,7 @@ async function preflightLockCurrency(api, agent, normalisedDelegationId, contrac
2260
2336
  })
2261
2337
  ]);
2262
2338
  } catch (err) {
2263
- if (err instanceof Error && err.message.includes("NF-R13-05")) throw err;
2339
+ if (err instanceof Error && err.message.includes("LOCK_CURRENCY_NON_NATIVE_SOL")) throw err;
2264
2340
  } finally {
2265
2341
  if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2266
2342
  }
@@ -2284,7 +2360,9 @@ async function preflightLockCurrencyInner(api, agent, normalisedDelegationId, si
2284
2360
  if (contractId !== void 0) {
2285
2361
  let activeContracts;
2286
2362
  try {
2287
- activeContracts = await fetchAllPages((after) => api.listContracts(r.relationshipId, signer, { limit: 100, state: "active", ...after ? { after } : {} }, signal));
2363
+ activeContracts = await fetchAllPages(
2364
+ (after) => api.listContracts(r.relationshipId, signer, { limit: 100, state: "active", ...after ? { after } : {} }, signal)
2365
+ );
2288
2366
  } catch {
2289
2367
  activeContracts = [];
2290
2368
  }
@@ -2293,7 +2371,7 @@ async function preflightLockCurrencyInner(api, agent, normalisedDelegationId, si
2293
2371
  const directAssetId = directContract.rateCurrency?.assetId;
2294
2372
  if (typeof directAssetId === "string" && !directAssetId.endsWith("/slip44:501")) {
2295
2373
  throw new Error(
2296
- `wallet create-lock pre-flight (NF-R13-05): contract ${contractId} is priced in '${directAssetId}' (NOT native SOL). This command only builds native-SOL locks (isNativeSol=1, no SPL Token-2022 path) \u2014 the resulting tx blob would be rejected server-side with ESC_LOCK_CURRENCY_MISMATCH or ESC_LOCK_AMOUNT_DELEGATION_MISMATCH after \`delegation offer\` ships it. Use a SOL-priced contract (e.g. \`--rate-currency 'SOL:solana-mainnet'\` on \`contract propose\`) or wait for SPL-token lock support to land.`
2374
+ `wallet create-lock pre-flight (LOCK_CURRENCY_NON_NATIVE_SOL): contract ${contractId} is priced in '${directAssetId}' (NOT native SOL). This command only builds native-SOL locks (isNativeSol=1, no SPL Token-2022 path) \u2014 the resulting tx blob would be rejected server-side with ESC_LOCK_CURRENCY_MISMATCH or ESC_LOCK_AMOUNT_DELEGATION_MISMATCH after \`delegation offer\` ships it. Use a SOL-priced contract (e.g. \`--rate-currency 'SOL:solana-mainnet'\` on \`contract propose\`) or wait for SPL-token lock support to land.`
2297
2375
  );
2298
2376
  }
2299
2377
  }
@@ -2308,7 +2386,9 @@ async function preflightLockCurrencyInner(api, agent, normalisedDelegationId, si
2308
2386
  if (!match) continue;
2309
2387
  let contracts = [];
2310
2388
  try {
2311
- contracts = await fetchAllPages((after) => api.listContracts(r.relationshipId, signer, { limit: 100, state: "active", ...after ? { after } : {} }, signal));
2389
+ contracts = await fetchAllPages(
2390
+ (after) => api.listContracts(r.relationshipId, signer, { limit: 100, state: "active", ...after ? { after } : {} }, signal)
2391
+ );
2312
2392
  } catch {
2313
2393
  }
2314
2394
  const contract = contracts.find((c) => c.contractId === match.contractId);
@@ -2323,7 +2403,7 @@ async function preflightLockCurrencyInner(api, agent, normalisedDelegationId, si
2323
2403
  const offendingAssetId = isContractNonNative ? contractAssetId : delegationAssetId;
2324
2404
  const sourceLabel = isContractNonNative ? `contract ${match.contractId}` : `delegation ${normalisedDelegationId}`;
2325
2405
  throw new Error(
2326
- `wallet create-lock pre-flight (NF-R13-05): ${sourceLabel} is priced in '${offendingAssetId}' (NOT native SOL). This command only builds native-SOL locks (isNativeSol=1, no SPL Token-2022 path) \u2014 the resulting tx blob would be rejected server-side with ESC_LOCK_CURRENCY_MISMATCH or ESC_LOCK_AMOUNT_DELEGATION_MISMATCH after \`delegation offer\` ships it. Use a SOL-priced contract (e.g. \`--rate-currency 'SOL:solana-mainnet'\` on \`contract propose\`) or wait for SPL-token lock support to land.`
2406
+ `wallet create-lock pre-flight (LOCK_CURRENCY_NON_NATIVE_SOL): ${sourceLabel} is priced in '${offendingAssetId}' (NOT native SOL). This command only builds native-SOL locks (isNativeSol=1, no SPL Token-2022 path) \u2014 the resulting tx blob would be rejected server-side with ESC_LOCK_CURRENCY_MISMATCH or ESC_LOCK_AMOUNT_DELEGATION_MISMATCH after \`delegation offer\` ships it. Use a SOL-priced contract (e.g. \`--rate-currency 'SOL:solana-mainnet'\` on \`contract propose\`) or wait for SPL-token lock support to land.`
2327
2407
  );
2328
2408
  }
2329
2409
  }
@@ -2451,7 +2531,7 @@ function registerSignSettlement(cmd) {
2451
2531
  const out = await signSettlementHandler(opts);
2452
2532
  console.log(JSON.stringify(out, null, 2));
2453
2533
  } catch (err) {
2454
- console.error(err.message);
2534
+ emitError(err, { json: opts.json, verbose: process.argv.includes("--trace") });
2455
2535
  process.exit(1);
2456
2536
  }
2457
2537
  });
@@ -2607,10 +2687,23 @@ function registerOffer(parent) {
2607
2687
  parent.command("offer").description("Open a new delegation under <contract-id> (must be ACTIVE) addressed to <recipient-did>.").argument("<recipient-did>", "Recipient agent DID (did:arp:...)").argument("<contract-id>", "Contract id this delegation operates under (must be ACTIVE)").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--delegation-id <uuid>", "Override the auto-generated delegation id (UUID). Useful for replay / scripting.").option("--title <s>", "Required: human-readable title for the offer").option("--brief <json>", "Optional structured brief (JSON object)").option("--criterion <s>", "acceptance_criteria \u2014 repeatable; pass --criterion once per bullet", collectRepeated, []).option("--deadline <rfc3339>", 'Optional RFC 3339 deadline (e.g. "2026-12-31T23:59:59Z")').option("--amount <s>", 'Optional decimal-as-string amount (e.g. "10.00"). REQUIRES --currency if set.').option("--currency <s>", `Asset identifier: shorthand (${import_sdk4.WELL_KNOWN_ASSET_KEYS.join("|")}) OR raw CAIP-19 string.`).option("--currency-decimals <n>", "Decimal places for base-unit conversion (0-18). Required only when --currency is raw CAIP-19.").option("--currency-symbol <s>", 'Optional UI hint ("USDC", "SOL"). Max 16 chars.').option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).option(
2608
2688
  "--escrow-lock-from-file <path>",
2609
2689
  "Path to JSON output of `heyarp wallet create-lock` (recommended). Contains all 5 escrow_lock fields: signed_tx_blob, lock_id, amount, asset_id, expiry. Mutually exclusive with the inline --escrow-lock-* flags below."
2610
- ).option("--escrow-lock-blob <base64>", "INLINE alternative: the signed Solana tx blob (base64). Requires --escrow-lock-id, --escrow-lock-amount, --escrow-lock-asset-id, --escrow-lock-expiry together.").option("--escrow-lock-id <hex32>", "INLINE lock_id (32-byte hex). Used with --escrow-lock-blob.").option("--escrow-lock-amount <int>", "INLINE lock amount in base units (e.g. lamports for native SOL). Used with --escrow-lock-blob.").option("--escrow-lock-asset-id <caip19>", "INLINE currency CAIP-19 asset_id (e.g. solana:<cluster_id>/slip44:501). Used with --escrow-lock-blob.").option("--escrow-lock-expiry <unix>", "INLINE expiry as unix seconds. Used with --escrow-lock-blob.").option(
2690
+ ).option(
2691
+ "--escrow-lock-blob <base64>",
2692
+ "INLINE alternative: the signed Solana tx blob (base64). Requires --escrow-lock-id, --escrow-lock-amount, --escrow-lock-asset-id, --escrow-lock-expiry together."
2693
+ ).option("--escrow-lock-id <hex32>", "INLINE lock_id (32-byte hex). Used with --escrow-lock-blob.").option("--escrow-lock-amount <int>", "INLINE lock amount in base units (e.g. lamports for native SOL). Used with --escrow-lock-blob.").option("--escrow-lock-asset-id <caip19>", "INLINE currency CAIP-19 asset_id (e.g. solana:<cluster_id>/slip44:501). Used with --escrow-lock-blob.").option("--escrow-lock-expiry <unix>", "INLINE expiry as unix seconds. Used with --escrow-lock-blob.").option(
2611
2694
  "--program-id <pubkey>",
2612
2695
  "Expected ARP escrow program id for pre-flight against the lock file's embedded `program_id`. Precedence: this flag > ARP_ESCROW_PROGRAM_ID env > server protocol-fee endpoint. Mismatch throws BEFORE the envelope ships, so a wrong-program lock never silently fails on chain after the offer was already delivered. Pre-flight is skipped only for old lock files lacking `program_id`, or when no expected value can be resolved."
2613
- ).option("--no-escrow", "Opt-out for test_mode servers. Default REQUIRES the escrow_lock attachment; without --no-escrow, missing flags throw BEFORE the envelope is sent (avoids ESC_LOCK_MISSING POST_COMMIT consuming sender_sequence).").action(async (recipientDid, contractId, opts) => {
2696
+ ).option(
2697
+ "--no-escrow",
2698
+ "Opt-out for test_mode servers. Default REQUIRES the escrow_lock attachment; without --no-escrow, missing flags throw BEFORE the envelope is sent (avoids ESC_LOCK_MISSING POST_COMMIT consuming sender_sequence)."
2699
+ ).option(
2700
+ "--wait-until <phase>",
2701
+ 'Block after delivery until the named FSM phase is reached (e.g. delegation.accepted). One of the UNTIL_PHASES from `heyarp status --help`. Exit code 124 on --wait-timeout. Recovers the "sub-agent exits before counterparty accepts" antipattern.'
2702
+ ).option("--wait-timeout <seconds>", "When --wait-until is set: max wall-clock wait (default 300). Exit code 124 on timeout.").option("--wait-interval <seconds>", "When --wait-until is set: poll cadence (default 3, bound [1, 60]).").option(
2703
+ "--wait-verbose",
2704
+ 'When --wait-until is set: emit one dim line per poll tick showing the current FSM state. Useful for "is it alive or stuck?" diagnosis on long blocks.',
2705
+ false
2706
+ ).action(async (recipientDid, contractId, opts) => {
2614
2707
  await runOffer(recipientDid, contractId, opts);
2615
2708
  });
2616
2709
  }
@@ -2633,9 +2726,7 @@ function assembleEscrowLockAttachment(opts) {
2633
2726
  const someInlineSet = inlineFlags.some((f) => f !== void 0 && f !== "");
2634
2727
  const allInlineSet = inlineFlags.every((f) => f !== void 0 && f !== "");
2635
2728
  if (fromFile && someInlineSet) {
2636
- throw new Error(
2637
- "delegation offer: --escrow-lock-from-file and --escrow-lock-blob/--escrow-lock-* are mutually exclusive. Pick one path."
2638
- );
2729
+ throw new Error("delegation offer: --escrow-lock-from-file and --escrow-lock-blob/--escrow-lock-* are mutually exclusive. Pick one path.");
2639
2730
  }
2640
2731
  if (!fromFile && !someInlineSet) {
2641
2732
  if (opts.escrow === false) return void 0;
@@ -2677,7 +2768,9 @@ function assembleEscrowLockAttachment(opts) {
2677
2768
  } else if (typeof expiryRaw === "string") {
2678
2769
  expiryNum2 = Number(expiryRaw);
2679
2770
  if (String(expiryNum2) !== expiryRaw.trim()) {
2680
- throw new Error(`delegation offer: --escrow-lock-from-file '${fromFile}' has invalid 'expiry' (must be positive integer unix seconds): ${JSON.stringify(expiryRaw)}.`);
2771
+ throw new Error(
2772
+ `delegation offer: --escrow-lock-from-file '${fromFile}' has invalid 'expiry' (must be positive integer unix seconds): ${JSON.stringify(expiryRaw)}.`
2773
+ );
2681
2774
  }
2682
2775
  } else {
2683
2776
  throw new Error(`delegation offer: --escrow-lock-from-file '${fromFile}' has invalid 'expiry' (must be positive integer unix seconds): ${JSON.stringify(expiryRaw)}.`);
@@ -2688,9 +2781,7 @@ function assembleEscrowLockAttachment(opts) {
2688
2781
  let delegationIdFromLock;
2689
2782
  if (p.delegation_id !== void 0) {
2690
2783
  if (typeof p.delegation_id !== "string" || !UUID_RE.test(p.delegation_id)) {
2691
- throw new Error(
2692
- `delegation offer: --escrow-lock-from-file '${fromFile}' has invalid 'delegation_id' (must be a UUID): ${JSON.stringify(p.delegation_id)}.`
2693
- );
2784
+ throw new Error(`delegation offer: --escrow-lock-from-file '${fromFile}' has invalid 'delegation_id' (must be a UUID): ${JSON.stringify(p.delegation_id)}.`);
2694
2785
  }
2695
2786
  delegationIdFromLock = p.delegation_id.toLowerCase();
2696
2787
  }
@@ -2800,9 +2891,34 @@ Reference this delegation on subsequent calls with:`));
2800
2891
  console.log(import_chalk6.default.dim(` heyarp delegation accept ${result.relationshipId} ${delegationId}`));
2801
2892
  console.log(import_chalk6.default.dim(` heyarp delegation decline ${result.relationshipId} ${delegationId}`));
2802
2893
  console.log(import_chalk6.default.dim(` heyarp delegation cancel ${result.relationshipId} ${delegationId}`));
2894
+ if (opts.waitUntil) {
2895
+ const untilPhase = parseUntilPhase(opts.waitUntil);
2896
+ if (untilPhase === void 0) {
2897
+ throw new Error(`delegation offer: --wait-until requires a phase value (got ${JSON.stringify(opts.waitUntil)})`);
2898
+ }
2899
+ await awaitFsmTransitionAfterAction({
2900
+ api,
2901
+ signerDid: sender.did,
2902
+ signer: makeSigner(sender),
2903
+ relationshipId: result.relationshipId,
2904
+ untilPhase,
2905
+ waitIntervalSec: parseWaitInterval(opts.waitInterval),
2906
+ waitTimeoutSec: parseWaitTimeout(opts.waitTimeout),
2907
+ waitVerbose: !!opts.waitVerbose,
2908
+ json: false
2909
+ // delegation offer is a human-text command (printIngestResult is human-text); JSON mode would be a follow-up.
2910
+ });
2911
+ }
2803
2912
  }
2804
2913
  function registerAccept(parent) {
2805
- parent.command("accept").description("Accept a PROPOSED delegation \u2014 promotes to ACCEPTED. Counterparty-only.").argument("<relationship-id>", "Relationship UUID").argument("<delegation-id>", "Delegation UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--contract-id <uuid>", "Override the auto-resolved contract id (default: read from the delegation row)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, delegationId, opts) => {
2914
+ parent.command("accept").description("Accept a PROPOSED delegation \u2014 promotes to ACCEPTED. Counterparty-only.").argument("<relationship-id>", "Relationship UUID").argument("<delegation-id>", "Delegation UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--contract-id <uuid>", "Override the auto-resolved contract id (default: read from the delegation row)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response. Mutually exclusive with --json.", false).option(
2915
+ "--json",
2916
+ 'Machine-readable mode \u2014 emit a single JSON object on stdout ({ok, action:"accept", delegationId, contractId, eventId, relationshipId, relationshipEventIndex, serverTimestamp, serverEventHash}). Prelude + pending-lock poll chatter move off stdout; on failure stderr carries `{code, message}`. Mutually exclusive with --verbose.',
2917
+ false
2918
+ ).option(
2919
+ "--no-wait-for-lock",
2920
+ "Opt-out of the pending-lock pre-flight poll. Default is ON: when the resolved delegation is in `pending_lock_finalization`, the CLI polls until the on-chain lock is confirmed before signing the envelope (avoids the racy DELEGATION_PENDING_LOCK 409 that consumes sender_sequence)."
2921
+ ).option("--lock-wait-timeout <seconds>", "Max wall-clock seconds to wait for pending-lock pre-flight (default 300).", "300").option("--lock-wait-interval <seconds>", "Poll cadence for pending-lock pre-flight in seconds. Bound to [1, 60].", "3").action(async (relationshipId, delegationId, opts) => {
2806
2922
  await runFollowupAction(relationshipId, delegationId, "accept", opts);
2807
2923
  });
2808
2924
  }
@@ -2812,20 +2928,33 @@ function registerDecline(parent) {
2812
2928
  // surface the closed enum at help time so operators
2813
2929
  // don't have to read source to find acceptable values.
2814
2930
  `Required: decline reason code (one of: ${import_sdk4.DECLINE_REASONS.join(", ")}). Carried in body.content.reason so the counterparty's reactor can branch on it.`
2815
- ).option("--reason-detail <s>", 'Optional free-text elaboration alongside --reason (e.g. "rate floor 0.20 USDC"). Max 512 chars.').option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, delegationId, opts) => {
2931
+ ).option("--reason-detail <s>", 'Optional free-text elaboration alongside --reason (e.g. "rate floor 0.20 USDC"). Max 512 chars.').option("--verbose", "Print the full envelope before sending and the full server response", false).option(
2932
+ "--no-wait-for-lock",
2933
+ "Opt-out of the pending-lock pre-flight poll. Default is ON: when the resolved delegation is in `pending_lock_finalization`, the CLI polls until the on-chain lock is confirmed before signing the envelope (avoids the racy DELEGATION_PENDING_LOCK 409 that consumes sender_sequence)."
2934
+ ).option("--lock-wait-timeout <seconds>", "Max wall-clock seconds to wait for pending-lock pre-flight (default 300).", "300").option("--lock-wait-interval <seconds>", "Poll cadence for pending-lock pre-flight in seconds. Bound to [1, 60].", "3").action(async (relationshipId, delegationId, opts) => {
2816
2935
  await runFollowupAction(relationshipId, delegationId, "decline", opts);
2817
2936
  });
2818
2937
  }
2819
2938
  function registerCancel(parent) {
2820
- parent.command("cancel").description("Cancel a PROPOSED delegation \u2014 moves to CANCELED. Offerer-only (the OTHER side uses decline).").argument("<relationship-id>", "Relationship UUID").argument("<delegation-id>", "Delegation UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--contract-id <uuid>", "Override the auto-resolved contract id (default: read from the delegation row)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, delegationId, opts) => {
2939
+ parent.command("cancel").description("Cancel a PROPOSED delegation \u2014 moves to CANCELED. Offerer-only (the OTHER side uses decline).").argument("<relationship-id>", "Relationship UUID").argument("<delegation-id>", "Delegation UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--contract-id <uuid>", "Override the auto-resolved contract id (default: read from the delegation row)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).option(
2940
+ "--no-wait-for-lock",
2941
+ "Opt-out of the pending-lock pre-flight poll. Default is ON: when the resolved delegation is in `pending_lock_finalization`, the CLI polls until the on-chain lock is confirmed before signing the envelope (avoids the racy DELEGATION_PENDING_LOCK 409 that consumes sender_sequence)."
2942
+ ).option("--lock-wait-timeout <seconds>", "Max wall-clock seconds to wait for pending-lock pre-flight (default 300).", "300").option("--lock-wait-interval <seconds>", "Poll cadence for pending-lock pre-flight in seconds. Bound to [1, 60].", "3").action(async (relationshipId, delegationId, opts) => {
2821
2943
  await runFollowupAction(relationshipId, delegationId, "cancel", opts);
2822
2944
  });
2823
2945
  }
2824
2946
  async function runFollowupAction(relationshipId, delegationId, action, opts) {
2825
2947
  const cmdName = `delegation ${action}`;
2948
+ if (opts.verbose && opts.json) {
2949
+ throw new Error(
2950
+ `${cmdName}: --verbose and --json are mutually exclusive. --json emits the structured server response; --verbose adds dumps that would break \`--json | jq\`.`
2951
+ );
2952
+ }
2826
2953
  relationshipId = requireUuidNormalised(cmdName, relationshipId, "<relationship-id>");
2827
2954
  delegationId = requireUuidNormalised(cmdName, delegationId, "<delegation-id>");
2828
2955
  const ttlSeconds = parseTtl(cmdName, opts.ttl);
2956
+ const lockWaitTimeoutSec = parseLockWaitTimeout(cmdName, opts.lockWaitTimeout);
2957
+ const lockWaitIntervalSec = parseLockWaitInterval(cmdName, opts.lockWaitInterval);
2829
2958
  let declinePayload = null;
2830
2959
  if (action === "decline") {
2831
2960
  const reason = parseDeclineReason(cmdName, opts.reason);
@@ -2836,6 +2965,19 @@ async function runFollowupAction(relationshipId, delegationId, action, opts) {
2836
2965
  const sender = resolveSenderAgent(cmdName, opts.server, opts.fromDid);
2837
2966
  const signer = makeSigner(sender);
2838
2967
  const resolved = await resolveDelegationRefs(cmdName, api, signer, { relationshipId, delegationId, action, selfDid: sender.did, contractIdOverride: opts.contractId });
2968
+ if (resolved.state === "pending_lock_finalization" && opts.waitForLock !== false) {
2969
+ await awaitDelegationLockFinalized(cmdName, api, signer, {
2970
+ relationshipId,
2971
+ delegationId,
2972
+ action,
2973
+ timeoutSec: lockWaitTimeoutSec,
2974
+ intervalSec: lockWaitIntervalSec,
2975
+ // Route poll chatter through `progress` so it's suppressed
2976
+ // in --json mode (keeps stdout pure + stderr reserved for
2977
+ // the structured error object).
2978
+ log: (line) => progress(opts.json, line)
2979
+ });
2980
+ }
2839
2981
  const content = {
2840
2982
  action,
2841
2983
  delegation_id: delegationId,
@@ -2846,10 +2988,10 @@ async function runFollowupAction(relationshipId, delegationId, action, opts) {
2846
2988
  if (declinePayload.reasonDetail) content.reason_detail = declinePayload.reasonDetail;
2847
2989
  }
2848
2990
  const body = { type: "delegation", content };
2849
- console.log(import_chalk6.default.dim(`Server: ${api.serverUrl}`));
2850
- console.log(import_chalk6.default.dim(`Sender: ${sender.did}`));
2851
- console.log(import_chalk6.default.dim(`Relationship: ${relationshipId}`));
2852
- console.log(import_chalk6.default.dim(`Delegation: ${delegationId} (action=${action}${action === "decline" ? `, reason=${content.reason}` : ""})`));
2991
+ progress(opts.json, import_chalk6.default.dim(`Server: ${api.serverUrl}`));
2992
+ progress(opts.json, import_chalk6.default.dim(`Sender: ${sender.did}`));
2993
+ progress(opts.json, import_chalk6.default.dim(`Relationship: ${relationshipId}`));
2994
+ progress(opts.json, import_chalk6.default.dim(`Delegation: ${delegationId} (action=${action}${action === "decline" ? `, reason=${content.reason}` : ""})`));
2853
2995
  const result = await sendDelegationEnvelope({
2854
2996
  api,
2855
2997
  sender,
@@ -2859,6 +3001,21 @@ async function runFollowupAction(relationshipId, delegationId, action, opts) {
2859
3001
  verbose: opts.verbose,
2860
3002
  server: opts.server
2861
3003
  });
3004
+ if (opts.json) {
3005
+ jsonOut({
3006
+ ok: true,
3007
+ action,
3008
+ delegationId,
3009
+ contractId: resolved.contractId,
3010
+ eventId: result.eventId,
3011
+ relationshipId: result.relationshipId,
3012
+ relationshipEventIndex: result.relationshipEventIndex,
3013
+ serverTimestamp: result.serverTimestamp,
3014
+ serverEventHash: result.serverEventHash,
3015
+ prevServerEventHash: result.prevServerEventHash ?? null
3016
+ });
3017
+ return;
3018
+ }
2862
3019
  printIngestResult(result);
2863
3020
  }
2864
3021
  async function sendDelegationEnvelope(args) {
@@ -2939,7 +3096,96 @@ async function resolveDelegationRefs(cmdName, api, signer, args) {
2939
3096
  } else {
2940
3097
  recipientDid = row.offererDid;
2941
3098
  }
2942
- return { contractId, recipientDid };
3099
+ return { contractId, recipientDid, state: row.state };
3100
+ }
3101
+ async function awaitDelegationLockFinalized(cmdName, api, signer, args) {
3102
+ const log = args.log ?? ((line) => console.log(line));
3103
+ log(
3104
+ import_chalk6.default.dim(
3105
+ `
3106
+ [--wait-for-lock] delegation ${args.delegationId} is awaiting on-chain lock confirmation; polling until ready (interval=${args.intervalSec}s timeout=${args.timeoutSec}s)`
3107
+ )
3108
+ );
3109
+ const fetchRow = async () => {
3110
+ let after;
3111
+ for (; ; ) {
3112
+ const page = await api.listDelegations(args.relationshipId, signer, { limit: 100, after });
3113
+ const found = page.find((d) => d.delegationId === args.delegationId);
3114
+ if (found) return found;
3115
+ if (page.length < 100) return null;
3116
+ after = page[page.length - 1].id;
3117
+ }
3118
+ };
3119
+ const outcome = await (0, import_sdk4.pollUntil)({
3120
+ fetch: fetchRow,
3121
+ // Match on either "row not found at all" OR "state moved
3122
+ // past pending_lock_finalization". The post-poll branch
3123
+ // disambiguates which condition fired — missing rows error
3124
+ // loud (operator typo or row pruned), present-but-terminal
3125
+ // rows error with the new state, and present-and-actionable
3126
+ // rows return cleanly. Polling on null would loop pointlessly
3127
+ // until the deadline fires and surface a misleading "timed
3128
+ // out" message for what's actually a wrong-id problem.
3129
+ predicate: (row2) => row2 === null || row2.state !== "pending_lock_finalization",
3130
+ intervalMs: args.intervalSec * 1e3,
3131
+ timeoutMs: args.timeoutSec * 1e3,
3132
+ // Swallow ONLY transient errors. A 4xx during the poll is a
3133
+ // new race (auth lost, relationship deleted, shape bug) and
3134
+ // should surface immediately — without this branch the loop
3135
+ // would spin until timeout and report a misleading
3136
+ // "still pending_lock_finalization" reason. 5xx + network
3137
+ // errors are the actual transients we want to ride out.
3138
+ swallowFetchErrors: true,
3139
+ onFetchError: (err) => {
3140
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
3141
+ throw err;
3142
+ }
3143
+ log(import_chalk6.default.dim(` poll: transient fetch error (${err instanceof Error ? err.message : String(err)})`));
3144
+ }
3145
+ });
3146
+ if (outcome.kind === "timeout") {
3147
+ throw new Error(
3148
+ `${cmdName}: --wait-for-lock timed out after ${args.timeoutSec}s; delegation ${args.delegationId} still in pending_lock_finalization. Retry the command (the lock may have confirmed in the meantime), bump --lock-wait-timeout, or pass --no-wait-for-lock to skip the pre-flight and surface the 409 directly.`
3149
+ );
3150
+ }
3151
+ if (outcome.kind === "aborted") {
3152
+ throw new Error(`${cmdName}: --wait-for-lock aborted before delegation ${args.delegationId} exited pending_lock_finalization`);
3153
+ }
3154
+ const row = outcome.value;
3155
+ if (row === null) {
3156
+ throw new Error(
3157
+ `${cmdName}: delegation ${args.delegationId} disappeared from relationship ${args.relationshipId} during --wait-for-lock pre-flight. Re-check the delegation id with \`heyarp delegations ${args.relationshipId}\`.`
3158
+ );
3159
+ }
3160
+ if (row.state !== "offered" && row.state !== "proposed") {
3161
+ throw new Error(
3162
+ `${cmdName}: delegation ${args.delegationId} transitioned to '${row.state}' while waiting for on-chain lock confirmation; cannot ${args.action}. Re-read the delegation timeline with \`heyarp delegations ${args.relationshipId}\` to see the latest state.`
3163
+ );
3164
+ }
3165
+ log(import_chalk6.default.dim(`[--wait-for-lock] delegation ${args.delegationId} ready (state=${row.state}); proceeding with ${args.action}.`));
3166
+ return row;
3167
+ }
3168
+ function parseLockWaitTimeout(cmdName, raw) {
3169
+ if (raw === void 0) return 300;
3170
+ const n = Number(raw);
3171
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
3172
+ throw new Error(`${cmdName}: --lock-wait-timeout must be a positive integer number of seconds (got '${raw}')`);
3173
+ }
3174
+ if (n > 3600) {
3175
+ throw new Error(`${cmdName}: --lock-wait-timeout must be <= 3600 seconds (1h). Got ${n}; if the on-chain lock is taking that long something else is broken.`);
3176
+ }
3177
+ return n;
3178
+ }
3179
+ function parseLockWaitInterval(cmdName, raw) {
3180
+ if (raw === void 0) return 3;
3181
+ const n = Number(raw);
3182
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
3183
+ throw new Error(`${cmdName}: --lock-wait-interval must be a positive integer number of seconds (got '${raw}')`);
3184
+ }
3185
+ if (n < 1 || n > 60) {
3186
+ throw new Error(`${cmdName}: --lock-wait-interval must be in [1, 60] seconds (got ${n}). Sub-second polling spams the server; > 60s defeats the point of the pre-flight.`);
3187
+ }
3188
+ return n;
2943
3189
  }
2944
3190
  function printIngestResult(result) {
2945
3191
  console.log(import_chalk6.default.green("\nDelivered."));
@@ -3071,7 +3317,13 @@ function registerPropose(parent) {
3071
3317
  ).option(
3072
3318
  "--rate-decimals <n>",
3073
3319
  "Decimal places for base-unit conversion (0-18). Required only when --rate-currency is raw CAIP-19; shorthand presets bring their own (USDC=6, SOL=9)."
3074
- ).option("--rate-symbol <s>", 'Optional UI hint ("USDC", "SOL"). Free text, max 16 chars. Shorthand presets set this automatically.').option("--rate-unit <s>", "rate_unit (task|thread|handoff)").option("--pricing <s>", "pricing_model (flat|usage_based). `quote` + `subscription` were dropped from the enum because they had no settlement-layer behaviour; add them back when real semantics ship.").option("--settlement <s>", "settlement_model (prepaid|escrow)").option("--delegation-tag <s>", "allowed_delegation_tags \u2014 repeatable; pass --delegation-tag once per tag", collectRepeated2, []).option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (recipientDid, opts) => {
3320
+ ).option("--rate-symbol <s>", 'Optional UI hint ("USDC", "SOL"). Free text, max 16 chars. Shorthand presets set this automatically.').option("--rate-unit <s>", "rate_unit (task|thread|handoff)").option(
3321
+ "--pricing <s>",
3322
+ "pricing_model (flat|usage_based). `quote` + `subscription` were dropped from the enum because they had no settlement-layer behaviour; add them back when real semantics ship."
3323
+ ).option("--settlement <s>", "settlement_model (prepaid|escrow)").option("--delegation-tag <s>", "allowed_delegation_tags \u2014 repeatable; pass --delegation-tag once per tag", collectRepeated2, []).option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).option(
3324
+ "--wait-until <phase>",
3325
+ 'Block after delivery until the named FSM phase is reached (e.g. contract.active). One of the UNTIL_PHASES from `heyarp status --help`. Exit code 124 on --wait-timeout. Makes "wait for counterparty to sign" a one-liner instead of a separate `heyarp status --wait --until contract.active` invocation.'
3326
+ ).option("--wait-timeout <seconds>", "When --wait-until is set: max wall-clock wait (default 300). Exit code 124 on timeout.").option("--wait-interval <seconds>", "When --wait-until is set: poll cadence (default 3, bound [1, 60]).").option("--wait-verbose", "When --wait-until is set: emit one dim line per poll tick.", false).action(async (recipientDid, opts) => {
3075
3327
  await runPropose(recipientDid, opts);
3076
3328
  });
3077
3329
  }
@@ -3102,9 +3354,29 @@ async function runPropose(recipientDid, opts) {
3102
3354
  Reference this contract on subsequent calls with: heyarp contract <action> ${result.relationshipId} ${contractId}`));
3103
3355
  console.log(import_chalk7.default.dim(` e.g. (counterparty signs) : heyarp contract sign ${result.relationshipId} ${contractId}`));
3104
3356
  console.log(import_chalk7.default.dim(` e.g. (counterparty counters): heyarp contract counter ${result.relationshipId} ${contractId} --rate-amount <new>`));
3357
+ if (opts.waitUntil) {
3358
+ const untilPhase = parseUntilPhase(opts.waitUntil);
3359
+ if (untilPhase === void 0) {
3360
+ throw new Error(`contract propose: --wait-until requires a phase value (got ${JSON.stringify(opts.waitUntil)})`);
3361
+ }
3362
+ await awaitFsmTransitionAfterAction({
3363
+ api,
3364
+ signerDid: sender.did,
3365
+ signer: makeSigner(sender),
3366
+ relationshipId: result.relationshipId,
3367
+ untilPhase,
3368
+ waitIntervalSec: parseWaitInterval(opts.waitInterval),
3369
+ waitTimeoutSec: parseWaitTimeout(opts.waitTimeout),
3370
+ waitVerbose: !!opts.waitVerbose,
3371
+ json: false
3372
+ });
3373
+ }
3105
3374
  }
3106
3375
  function registerCounter(parent) {
3107
- parent.command("counter").description("Reply with revised terms to the latest PROPOSED version of <contract-id> in <relationship-id>.").argument("<relationship-id>", "Relationship UUID").argument("<contract-id>", "Contract UUID (proposed earlier in this relationship)").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Target the previous version explicitly (default: auto-resolve to the latest PROPOSED)").option("--scope <s>", "scope_summary \u2014 short prose describing the revised terms").option("--rate-amount <s>", "rate_amount \u2014 decimal as string. REQUIRES --rate-currency if set.").option("--rate-currency <s>", `Asset identifier: shorthand (${import_sdk5.WELL_KNOWN_ASSET_KEYS.join("|")}) OR raw CAIP-19 string.`).option("--rate-decimals <n>", "Decimal places for base-unit conversion (0-18). Required only when --rate-currency is raw CAIP-19.").option("--rate-symbol <s>", 'Optional UI hint ("USDC", "SOL"). Max 16 chars.').option("--rate-unit <s>", "rate_unit (task|thread|handoff)").option("--pricing <s>", "pricing_model (flat|usage_based). `quote` + `subscription` were dropped from the enum because they had no settlement-layer behaviour; add them back when real semantics ship.").option("--settlement <s>", "settlement_model (prepaid|escrow)").option("--delegation-tag <s>", "allowed_delegation_tags \u2014 repeatable", collectRepeated2, []).option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, contractId, opts) => {
3376
+ parent.command("counter").description("Reply with revised terms to the latest PROPOSED version of <contract-id> in <relationship-id>.").argument("<relationship-id>", "Relationship UUID").argument("<contract-id>", "Contract UUID (proposed earlier in this relationship)").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Target the previous version explicitly (default: auto-resolve to the latest PROPOSED)").option("--scope <s>", "scope_summary \u2014 short prose describing the revised terms").option("--rate-amount <s>", "rate_amount \u2014 decimal as string. REQUIRES --rate-currency if set.").option("--rate-currency <s>", `Asset identifier: shorthand (${import_sdk5.WELL_KNOWN_ASSET_KEYS.join("|")}) OR raw CAIP-19 string.`).option("--rate-decimals <n>", "Decimal places for base-unit conversion (0-18). Required only when --rate-currency is raw CAIP-19.").option("--rate-symbol <s>", 'Optional UI hint ("USDC", "SOL"). Max 16 chars.').option("--rate-unit <s>", "rate_unit (task|thread|handoff)").option(
3377
+ "--pricing <s>",
3378
+ "pricing_model (flat|usage_based). `quote` + `subscription` were dropped from the enum because they had no settlement-layer behaviour; add them back when real semantics ship."
3379
+ ).option("--settlement <s>", "settlement_model (prepaid|escrow)").option("--delegation-tag <s>", "allowed_delegation_tags \u2014 repeatable", collectRepeated2, []).option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, contractId, opts) => {
3108
3380
  await runCounter(relationshipId, contractId, opts);
3109
3381
  });
3110
3382
  }
@@ -3144,7 +3416,11 @@ async function runCounter(relationshipId, contractId, opts) {
3144
3416
  printIngestResult2(result);
3145
3417
  }
3146
3418
  function registerSign(parent) {
3147
- parent.command("sign").description("Promote the latest PROPOSED version of <contract-id> in <relationship-id> to ACTIVE.").argument("<relationship-id>", "Relationship UUID").argument("<contract-id>", "Contract UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Target the version explicitly (default: auto-resolve to the latest PROPOSED)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (relationshipId, contractId, opts) => {
3419
+ parent.command("sign").description("Promote the latest PROPOSED version of <contract-id> in <relationship-id> to ACTIVE.").argument("<relationship-id>", "Relationship UUID").argument("<contract-id>", "Contract UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Target the version explicitly (default: auto-resolve to the latest PROPOSED)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response. Mutually exclusive with --json.", false).option(
3420
+ "--json",
3421
+ 'Machine-readable mode \u2014 emit a single JSON object on stdout ({ok, action:"sign", contractId, version, eventId, relationshipId, relationshipEventIndex, serverTimestamp, serverEventHash}). Prelude moves off stdout; on failure stderr carries `{code, message}`. Mutually exclusive with --verbose.',
3422
+ false
3423
+ ).action(async (relationshipId, contractId, opts) => {
3148
3424
  await runSignOrDecline(relationshipId, contractId, "sign", opts);
3149
3425
  });
3150
3426
  }
@@ -3162,6 +3438,11 @@ function registerDecline2(parent) {
3162
3438
  }
3163
3439
  async function runSignOrDecline(relationshipId, contractId, action, opts) {
3164
3440
  const cmdName = `contract ${action}`;
3441
+ if (opts.verbose && opts.json) {
3442
+ throw new Error(
3443
+ `${cmdName}: --verbose and --json are mutually exclusive. --json emits the structured server response; --verbose adds dumps that would break \`--json | jq\`.`
3444
+ );
3445
+ }
3165
3446
  relationshipId = requireUuidNormalised(cmdName, relationshipId, "<relationship-id>");
3166
3447
  contractId = requireUuidNormalised(cmdName, contractId, "<contract-id>");
3167
3448
  const ttlSeconds = parseTtl2(cmdName, opts.ttl);
@@ -3186,10 +3467,10 @@ async function runSignOrDecline(relationshipId, contractId, action, opts) {
3186
3467
  if (declinePayload.reasonDetail) content.reason_detail = declinePayload.reasonDetail;
3187
3468
  }
3188
3469
  const body = { type: "contract", content };
3189
- console.log(import_chalk7.default.dim(`Server: ${api.serverUrl}`));
3190
- console.log(import_chalk7.default.dim(`Sender: ${sender.did}`));
3191
- console.log(import_chalk7.default.dim(`Relationship: ${relationshipId}`));
3192
- console.log(import_chalk7.default.dim(`Contract id: ${contractId} (v${targetVersion}, action=${action}${action === "decline" ? `, reason=${content.reason}` : ""})`));
3470
+ progress(opts.json, import_chalk7.default.dim(`Server: ${api.serverUrl}`));
3471
+ progress(opts.json, import_chalk7.default.dim(`Sender: ${sender.did}`));
3472
+ progress(opts.json, import_chalk7.default.dim(`Relationship: ${relationshipId}`));
3473
+ progress(opts.json, import_chalk7.default.dim(`Contract id: ${contractId} (v${targetVersion}, action=${action}${action === "decline" ? `, reason=${content.reason}` : ""})`));
3193
3474
  const result = await sendContractEnvelope({
3194
3475
  api,
3195
3476
  sender,
@@ -3199,6 +3480,21 @@ async function runSignOrDecline(relationshipId, contractId, action, opts) {
3199
3480
  verbose: opts.verbose,
3200
3481
  server: opts.server
3201
3482
  });
3483
+ if (opts.json) {
3484
+ jsonOut({
3485
+ ok: true,
3486
+ action,
3487
+ contractId,
3488
+ version: targetVersion,
3489
+ eventId: result.eventId,
3490
+ relationshipId: result.relationshipId,
3491
+ relationshipEventIndex: result.relationshipEventIndex,
3492
+ serverTimestamp: result.serverTimestamp,
3493
+ serverEventHash: result.serverEventHash,
3494
+ prevServerEventHash: result.prevServerEventHash ?? null
3495
+ });
3496
+ return;
3497
+ }
3202
3498
  printIngestResult2(result);
3203
3499
  }
3204
3500
  async function sendContractEnvelope(args) {
@@ -3549,11 +3845,9 @@ async function runDelegations(relationshipId, opts) {
3549
3845
  const state = parseState2(opts.state);
3550
3846
  const api = new ArpApiClient(opts.server);
3551
3847
  const sender = resolveSenderAgent("delegations", opts.server, opts.fromDid);
3552
- if (!opts.json) {
3553
- console.log(import_chalk9.default.dim(`Server: ${api.serverUrl}`));
3554
- console.log(import_chalk9.default.dim(`Signer: ${sender.did}`));
3555
- console.log(import_chalk9.default.dim(`Relationship: ${relationshipId}`));
3556
- }
3848
+ progress(opts.json, import_chalk9.default.dim(`Server: ${api.serverUrl}`));
3849
+ progress(opts.json, import_chalk9.default.dim(`Signer: ${sender.did}`));
3850
+ progress(opts.json, import_chalk9.default.dim(`Relationship: ${relationshipId}`));
3557
3851
  const query = { limit };
3558
3852
  if (state) query.state = state;
3559
3853
  if (opts.contractId) query.contractId = opts.contractId;
@@ -3561,7 +3855,7 @@ async function runDelegations(relationshipId, opts) {
3561
3855
  const signer = makeSigner(sender);
3562
3856
  const rows = await api.listDelegations(relationshipId, signer, query);
3563
3857
  if (opts.json) {
3564
- printJsonArray(rows);
3858
+ jsonOut(rows);
3565
3859
  return;
3566
3860
  }
3567
3861
  if (rows.length === 0) {
@@ -3667,15 +3961,104 @@ function parseLimit3(raw) {
3667
3961
  var import_sdk6 = require("@heyanon-arp/sdk");
3668
3962
  init_api();
3669
3963
  function registerDidDocCommand(root) {
3670
- root.command("did-doc").description("Fetch and pretty-print the DID document for a did:arp:<...>").argument("<did>", "did:arp:<base58btc> identifier").option("--server <url>", "Override ARP server base URL").option("--json", "Emit the DID document as JSON on stdout (default; accepted for symmetry with other commands).", false).action(async (did, opts) => {
3964
+ root.command("did-doc").description(
3965
+ "Fetch and pretty-print the DID document for a did:arp:<...>. Pass `--field <path>` to extract a single value (verification keys, endpoints, etc.) without piping through jq."
3966
+ ).argument("<did>", "did:arp:<base58btc> identifier").option("--server <url>", "Override ARP server base URL").option("--json", "Emit the DID document as JSON on stdout (default; accepted for symmetry with other commands).", false).option(
3967
+ "--field <path>",
3968
+ "Extract a single value via a dotted path. Supports object property access (`id`), numeric array indexes (`verificationMethod.0`), and `#fragment` array selectors that match a verification-method or service entry by its `id` suffix (`verificationMethod.#settlement.publicKeyMultibase`). Scalar values (string / number / boolean) emit raw to stdout for shell composition (`KEY=$(heyarp did-doc ... --field ...)`); objects / arrays emit as pretty-printed JSON. Mutually exclusive with `--json` since `--field` already controls the shape."
3969
+ ).action(async (did, opts) => {
3671
3970
  if (!(0, import_sdk6.isValidDid)(did)) {
3672
3971
  throw new Error(`'${did}' is not a syntactically valid did:arp identifier`);
3673
3972
  }
3973
+ if (opts.json && opts.field !== void 0) {
3974
+ throw new Error("did-doc: --json and --field are mutually exclusive (--field already controls the output shape; --json emits the full doc).");
3975
+ }
3674
3976
  const api = new ArpApiClient(opts.server);
3675
3977
  const doc = await api.getDidDocument(did);
3676
- console.log(formatJson(doc));
3978
+ if (opts.field !== void 0) {
3979
+ emitField(doc, opts.field);
3980
+ return;
3981
+ }
3982
+ jsonOut(doc);
3677
3983
  });
3678
3984
  }
3985
+ function extractField(doc, path) {
3986
+ const parts = path.split(".").filter((p) => p.length > 0);
3987
+ if (parts.length === 0) {
3988
+ throw new Error("did-doc: --field path is empty");
3989
+ }
3990
+ let current = doc;
3991
+ const walked = [];
3992
+ for (const part of parts) {
3993
+ walked.push(part);
3994
+ if (current === null || current === void 0) {
3995
+ throw new Error(
3996
+ `did-doc: --field '${path}': segment '${walked.join(".")}' walked into ${current === null ? "null" : "undefined"} \u2014 the preceding path returned nothing to descend into`
3997
+ );
3998
+ }
3999
+ if (part.startsWith("#")) {
4000
+ if (!Array.isArray(current)) {
4001
+ throw new Error(
4002
+ `did-doc: --field '${path}': fragment selector '${part}' requires an array at '${walked.slice(0, -1).join(".") || "<root>"}', got ${describeShape(current)}`
4003
+ );
4004
+ }
4005
+ const match = current.find((el) => isRecord(el) && typeof el.id === "string" && (el.id === part || el.id.endsWith(part)));
4006
+ if (match === void 0) {
4007
+ const ids = current.map((el) => isRecord(el) && typeof el.id === "string" ? el.id : "(no id)").join(", ");
4008
+ throw new Error(`did-doc: --field '${path}': no array element with id matching '${part}' (available ids: ${ids || "<empty array>"})`);
4009
+ }
4010
+ current = match;
4011
+ } else if (/^\d+$/.test(part)) {
4012
+ if (!Array.isArray(current)) {
4013
+ throw new Error(
4014
+ `did-doc: --field '${path}': numeric index '${part}' requires an array at '${walked.slice(0, -1).join(".") || "<root>"}', got ${describeShape(current)}`
4015
+ );
4016
+ }
4017
+ const idx = Number.parseInt(part, 10);
4018
+ if (idx >= current.length) {
4019
+ throw new Error(`did-doc: --field '${path}': index ${idx} out of bounds (array length ${current.length})`);
4020
+ }
4021
+ current = current[idx];
4022
+ } else {
4023
+ if (!isRecord(current)) {
4024
+ throw new Error(`did-doc: --field '${path}': cannot read property '${part}' from ${describeShape(current)} at '${walked.slice(0, -1).join(".") || "<root>"}'`);
4025
+ }
4026
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
4027
+ throw new Error(
4028
+ `did-doc: --field '${path}': no property '${part}' at '${walked.slice(0, -1).join(".") || "<root>"}' (available: ${Object.keys(current).join(", ")})`
4029
+ );
4030
+ }
4031
+ current = current[part];
4032
+ }
4033
+ }
4034
+ return current;
4035
+ }
4036
+ function emitField(doc, path) {
4037
+ const value = extractField(doc, path);
4038
+ if (value === void 0) {
4039
+ throw new Error(`did-doc: --field '${path}' resolved to undefined \u2014 the DID document has an explicit undefined at this path (probably an issuer bug; please report)`);
4040
+ }
4041
+ const t = typeof value;
4042
+ if (t === "string") {
4043
+ process.stdout.write(`${value}
4044
+ `);
4045
+ return;
4046
+ }
4047
+ if (t === "number" || t === "boolean") {
4048
+ process.stdout.write(`${String(value)}
4049
+ `);
4050
+ return;
4051
+ }
4052
+ jsonOut(value);
4053
+ }
4054
+ function isRecord(value) {
4055
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4056
+ }
4057
+ function describeShape(value) {
4058
+ if (value === null) return "null";
4059
+ if (Array.isArray(value)) return "array";
4060
+ return typeof value;
4061
+ }
3679
4062
 
3680
4063
  // src/commands/doctor.ts
3681
4064
  var import_sdk7 = require("@heyanon-arp/sdk");
@@ -3901,7 +4284,9 @@ function registerEscrowCommands(root) {
3901
4284
  cmd.command("info").description("Show the on-chain protocol fee config + cluster + pause state. Public read; no auth.").option("--server <url>", "Override ARP server base URL").option("--json", "Machine-readable JSON output", false).action(async (opts) => {
3902
4285
  await runEscrowInfo(opts);
3903
4286
  });
3904
- cmd.command("derive-condition-hash").description("Compute canonical condition_hash (32-byte hex) for `heyarp wallet create-lock --condition-hash <hex>`. Fetches the contract row, projects to the canonical subset, and hashes via SDK deriveConditionHash. Mirrors what the on-chain create_lock instruction binds.").argument("<relationship-id>", "Relationship UUID hosting the contract").argument("<contract-id>", "Contract UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Pin a specific contract version (default: latest non-replaced row)").option("--json", "Machine-readable JSON: {contract_id, version, condition_hash_hex, projected_subset}", false).action(async (relationshipId, contractId, opts) => {
4287
+ cmd.command("derive-condition-hash").description(
4288
+ "Compute canonical condition_hash (32-byte hex) for `heyarp wallet create-lock --condition-hash <hex>`. Fetches the contract row, projects to the canonical subset, and hashes via SDK deriveConditionHash. Mirrors what the on-chain create_lock instruction binds."
4289
+ ).argument("<relationship-id>", "Relationship UUID hosting the contract").argument("<contract-id>", "Contract UUID").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--version <n>", "Pin a specific contract version (default: latest non-replaced row)").option("--json", "Machine-readable JSON: {contract_id, version, condition_hash_hex, projected_subset}", false).action(async (relationshipId, contractId, opts) => {
3905
4290
  await runDeriveConditionHash(relationshipId, contractId, opts);
3906
4291
  });
3907
4292
  cmd.command("recover-sequence").description(
@@ -3914,7 +4299,8 @@ async function runEscrowInfo(opts) {
3914
4299
  const api = new ArpApiClient(opts.server);
3915
4300
  const status = await api.getProtocolFee();
3916
4301
  if (opts.json) {
3917
- console.log(JSON.stringify(status));
4302
+ progress(true, import_chalk12.default.dim(`Server: ${api.serverUrl}`));
4303
+ jsonOut(status);
3918
4304
  } else {
3919
4305
  console.log(formatProtocolFeeStatus(api.serverUrl, status));
3920
4306
  }
@@ -4005,6 +4391,8 @@ async function runDeriveConditionHash(relationshipId, contractId, opts) {
4005
4391
  const api = new ArpApiClient(opts.server);
4006
4392
  const sender = resolveSenderAgent("escrow derive-condition-hash", opts.server, opts.fromDid);
4007
4393
  const signer = makeSigner(sender);
4394
+ progress(opts.json, import_chalk12.default.dim(`Server: ${api.serverUrl}`));
4395
+ progress(opts.json, import_chalk12.default.dim(`Signer: ${sender.did}`));
4008
4396
  let contract;
4009
4397
  try {
4010
4398
  contract = await findContractRow(api, signer, relationshipId, contractId, versionPin);
@@ -4018,14 +4406,12 @@ async function runDeriveConditionHash(relationshipId, contractId, opts) {
4018
4406
  const hashBytes = (0, import_sdk8.deriveConditionHash)(subset);
4019
4407
  const hex = (0, import_utils2.bytesToHex)(hashBytes);
4020
4408
  if (opts.json) {
4021
- console.log(
4022
- JSON.stringify({
4023
- contract_id: contract.contractId,
4024
- version: contract.version,
4025
- condition_hash_hex: hex,
4026
- projected_subset: subset
4027
- })
4028
- );
4409
+ jsonOut({
4410
+ contract_id: contract.contractId,
4411
+ version: contract.version,
4412
+ condition_hash_hex: hex,
4413
+ projected_subset: subset
4414
+ });
4029
4415
  return;
4030
4416
  }
4031
4417
  console.log(import_chalk12.default.dim(`Relationship: ${relationshipId}`));
@@ -4065,16 +4451,14 @@ async function runRecoverSequence(opts) {
4065
4451
  }
4066
4452
  const outcome = classifyRecoverOutcome(local, server);
4067
4453
  if (opts.json) {
4068
- console.log(
4069
- JSON.stringify({
4070
- did: sender.did,
4071
- local,
4072
- server,
4073
- ...outcome.kind !== "in-sync" ? { drift: outcome.kind === "behind" ? outcome.drift : -outcome.drift } : {},
4074
- kind: outcome.kind,
4075
- applied: opts.apply === true && outcome.kind === "behind"
4076
- })
4077
- );
4454
+ jsonOut({
4455
+ did: sender.did,
4456
+ local,
4457
+ server,
4458
+ ...outcome.kind !== "in-sync" ? { drift: outcome.kind === "behind" ? outcome.drift : -outcome.drift } : {},
4459
+ kind: outcome.kind,
4460
+ applied: opts.apply === true && outcome.kind === "behind"
4461
+ });
4078
4462
  } else {
4079
4463
  console.log(import_chalk12.default.dim(`DID: ${sender.did}`));
4080
4464
  console.log(import_chalk12.default.dim(`Server: ${api.serverUrl}`));
@@ -4244,191 +4628,539 @@ function parseSince(raw) {
4244
4628
  return n;
4245
4629
  }
4246
4630
 
4247
- // src/commands/guide.ts
4631
+ // src/commands/examples.ts
4632
+ var import_node_fs5 = require("fs");
4633
+ var import_node_path4 = require("path");
4248
4634
  var import_chalk14 = __toESM(require("chalk"));
4249
- function registerGuideCommand(root) {
4250
- root.command("guide").description("Mental-model primer: agent identity, FSM order, multi-DID, receipt-as-closure. Read this before your first signed command.").action(() => {
4251
- console.log(GUIDE);
4635
+ var EXAMPLES = [
4636
+ {
4637
+ name: "worker",
4638
+ filename: "worker-template.py",
4639
+ description: "Autonomous Python worker (handshake \u2192 contract \u2192 delegation \u2192 work \u2192 receipt \u2192 settlement, all auto-mediated; fill in handle_work_request)"
4640
+ }
4641
+ ];
4642
+ var EXAMPLES_DIR = (0, import_node_path4.resolve)(__dirname, "..", "examples");
4643
+ function registerExamplesCommand(root) {
4644
+ const examples = root.command("examples").description("Bundled reference templates (worker, etc.) \u2014 discover, print, or copy to disk. Templates ship inside the npm tarball; no GitHub round-trip required.");
4645
+ examples.command("list").description("List bundled examples. Pair with `heyarp examples show <name>` or `heyarp examples copy <name>`.").option("--json", "JSON output (jq-pipeable)", false).action((opts, cmd) => {
4646
+ try {
4647
+ if (opts.json) {
4648
+ console.log(formatJson(EXAMPLES));
4649
+ return;
4650
+ }
4651
+ console.log(import_chalk14.default.bold("Bundled examples:"));
4652
+ for (const e of EXAMPLES) {
4653
+ console.log(` ${import_chalk14.default.cyan(e.name).padEnd(20)} ${import_chalk14.default.dim(e.filename)}`);
4654
+ console.log(` ${e.description}`);
4655
+ }
4656
+ console.log(import_chalk14.default.dim("\nShow contents: heyarp examples show <name>"));
4657
+ console.log(import_chalk14.default.dim("Save to disk: heyarp examples copy <name> --output ./<filename>"));
4658
+ } catch (err) {
4659
+ emitActionError(err, cmd);
4660
+ process.exitCode = 1;
4661
+ }
4662
+ });
4663
+ examples.command("show").description("Print the example's contents to stdout. Pipe-friendly \u2014 `heyarp examples show worker > my-worker.py` is the lowest-friction install path.").argument("<name>", `Example name (one of: ${EXAMPLES.map((e) => e.name).join(", ")})`).action((name, _opts, cmd) => {
4664
+ try {
4665
+ const entry = lookupOrThrow(name);
4666
+ const contents = readExampleOrThrow(entry);
4667
+ process.stdout.write(contents);
4668
+ } catch (err) {
4669
+ emitActionError(err, cmd);
4670
+ process.exitCode = 1;
4671
+ }
4672
+ });
4673
+ examples.command("copy").description("Copy the example to a path on disk. Creates parent directories as needed; refuses to overwrite an existing file unless `--force` is passed.").argument("<name>", `Example name (one of: ${EXAMPLES.map((e) => e.name).join(", ")})`).option("--output <path>", "Destination file path. Defaults to the bundled filename in the current working directory.").option("--force", "Overwrite the destination file if it already exists.", false).action((name, opts, cmd) => {
4674
+ try {
4675
+ const entry = lookupOrThrow(name);
4676
+ const contents = readExampleOrThrow(entry);
4677
+ const destPath = (0, import_node_path4.resolve)(process.cwd(), opts.output ?? entry.filename);
4678
+ if ((0, import_node_fs5.existsSync)(destPath) && !opts.force) {
4679
+ throw new Error(`refusing to overwrite ${destPath} (pass --force to override)`);
4680
+ }
4681
+ const parentDir = (0, import_node_path4.dirname)(destPath);
4682
+ if (!(0, import_node_fs5.existsSync)(parentDir)) {
4683
+ (0, import_node_fs5.mkdirSync)(parentDir, { recursive: true });
4684
+ }
4685
+ (0, import_node_fs5.writeFileSync)(destPath, contents);
4686
+ console.log(`${import_chalk14.default.green("Wrote")} ${import_chalk14.default.cyan(destPath)} ${import_chalk14.default.dim(`(${contents.length} bytes)`)}`);
4687
+ } catch (err) {
4688
+ emitActionError(err, cmd);
4689
+ process.exitCode = 1;
4690
+ }
4252
4691
  });
4253
4692
  }
4254
- var GUIDE = [
4255
- import_chalk14.default.bold("HeyARP CLI \u2014 agent guide"),
4256
- "",
4257
- import_chalk14.default.bold("1. Identity"),
4258
- " \u2022 Each agent has a `did:arp:<base58btc>` DID + an Ed25519 identity key.",
4259
- " \u2022 Keys live in `~/.arp/agents.json` (or $HEYARP_HOME/agents.json).",
4260
- " \u2022 State file = identity. If two agents share one home dir, EITHER can sign as",
4261
- " the other. For multi-agent setups on one host, set HEYARP_HOME per agent:",
4262
- " HEYARP_HOME=/tmp/agent-alice heyarp register \u2026",
4263
- " HEYARP_HOME=/tmp/agent-bob heyarp register \u2026",
4264
- "",
4265
- import_chalk14.default.bold("2. The full work cycle (5 stages, in order)"),
4266
- " Buyer (caller) and Worker (payee) take turns:",
4267
- "",
4268
- ` handshake buyer \u2192 worker "let's talk"`,
4269
- " handshake_response worker \u2192 buyer accept | decline",
4270
- " contract propose buyer \u2192 worker terms (rate, scope, \u2026)",
4271
- " contract sign worker \u2192 buyer \u2192 state=active",
4272
- " delegation offer buyer \u2192 worker concrete task + amount",
4273
- " delegation accept worker \u2192 buyer \u2192 state=accepted",
4274
- " work request buyer \u2192 worker params payload",
4275
- " work respond worker \u2192 buyer output OR error",
4276
- " receipt propose worker \u2192 buyer verdict + body hashes",
4277
- " receipt cosign buyer \u2192 worker \u2192 state=cosigned \u2713 DONE",
4278
- "",
4279
- " Skipping any stage = the next stage rejects with a state-machine error.",
4280
- " `receipt cosign` is the closure \u2014 without it the work isn't paid for.",
4281
- "",
4282
- import_chalk14.default.bold("3. Escrow (how funds actually move)"),
4283
- " `delegation offer` attaches a signed Solana `create_lock` tx blob \u2014 the",
4284
- " buyer FUNDS escrow up-front before the worker accepts. The worker ",
4285
- " commits work knowing the cash is already locked.",
4286
- " `receipt cosign` carries SETTLEMENT SIGNATURES (Ed25519 over a canonical",
4287
- " digest) from BOTH parties \u2014 these unlock `release_lock` on-chain.",
4288
- " Refund paths:",
4289
- " \u2022 PayerCancellation \u2014 buyer cancels within 10min of offer (1 sig)",
4290
- " \u2022 BothPartiesAgreed \u2014 bilateral cooperative refund (2 sigs)",
4291
- " \u2022 Expired \u2014 permissionless after lock.expiry passes (no sigs)",
4292
- " \u2022 DisputeResolution \u2014 admin split via multisig (V1 backend-only)",
4293
- "",
4294
- import_chalk14.default.bold("4. Wallet + escrow commands (native SOL today; SPL + Token-2022 in a later slice)"),
4295
- " `heyarp wallet create-lock` builds + signs a `create_lock` Solana tx.",
4296
- " Output JSON: {signed_tx_blob, lock_id (32-byte hex), amount, asset_id,",
4297
- " expiry, delegation_id, program_id}. Pipe via `delegation offer",
4298
- " --escrow-lock-from-file <path>` \u2014 delegation_id auto-aligns. Use",
4299
- " `--expiry-secs $(($(date +%s) + 86400*3))` (\u22653d) \u2014 server enforces",
4300
- " lock.expiry \u2265 deadline + DISPUTE_BUFFER (1d).",
4301
- " `heyarp wallet derive-pdas --delegation-id <id>` returns the",
4302
- " deterministic PDAs for ON-CHAIN VERIFICATION:",
4303
- " {lock_id_hex, program_id, lock_pda, escrow_pda, config_pda,",
4304
- " event_authority_pda}. `escrow_pda` holds the escrowed funds;",
4305
- " `config_pda` is the singleton program config; `event_authority_pda`",
4306
- " is the anchor `#[event_cpi]` self-CPI target.",
4307
- " `heyarp escrow derive-condition-hash <rel-id> <contract-id>` computes",
4308
- " the canonical condition_hash for `wallet create-lock --condition-hash`.",
4309
- " `heyarp wallet sign-settlement-release` signs the release / partial",
4310
- " digest. Output `sig` is RAW base64 (NO `ed25519:` prefix). Pass",
4311
- " `--partial-payee-amount <lamports>` to switch the digest to",
4312
- " `ARP-SOLANA-PARTIAL-RELEASE-v1.5`.",
4313
- " `heyarp receipt cosign` attaches both parties' signatures into",
4314
- " `attachments.settlement_signatures` via --settlement-purpose,",
4315
- " --settlement-expires-at, --payer-settlement-{pubkey,sig},",
4316
- " --payee-settlement-{pubkey,sig} (+ --settlement-payee-amount for",
4317
- " partial). Server authorises on-chain release.",
4318
- " `heyarp wallet verify-release --delegation-id <id> --json` is the",
4319
- " post-cycle on-chain assertion. Returns {status, release_method,",
4320
- " lock_state, released, \u2026}. The R15 contract does NOT close the lock",
4321
- " account on release \u2014 `released: true` is decided from the state byte",
4322
- " at offset 185: 1\u2192released_clean, 4\u2192released_partial, 2\u2192released_refunded.",
4323
- " `lock_account_exists: true` post-release is expected, not a bug.",
4324
- "",
4325
- import_chalk14.default.bold("5. Discovery"),
4326
- " `heyarp agents --tag X --tag Y --query Z` \u2014 public catalog, no auth.",
4327
- " AND-semantics across tags. Returns `did:arp:\u2026` DIDs you can hand to",
4328
- " `heyarp send-handshake`. Skip the `--query` filter if your tags are",
4329
- " specific enough; full-text search hits a Mongo `$text` index that needs",
4330
- " the right shape (server returns 500 if it's misconfigured, you can't do",
4331
- " much from the CLI side).",
4332
- "",
4333
- import_chalk14.default.bold("6. Multi-DID disambiguation"),
4334
- " With >1 agent registered locally for one server, `--from-did` is",
4335
- " REQUIRED on every signed command. The resolver does NOT silently pick",
4336
- " one \u2014 it fails with the candidate list. Sole-agent setups auto-pick.",
4337
- "",
4338
- import_chalk14.default.bold("7. Recovering full IDs / hashes"),
4339
- " List commands truncate `did:arp:abc\u2026xyz` and `sha256:abc\u2026xyz` for",
4340
- " readability. To get full values for the next command:",
4341
- " \u2022 `--full-ids` prints UUIDs / DIDs / hashes uncut",
4342
- " \u2022 `--verbose` appends a per-row JSON dump with full payload",
4343
- " \u2022 `--json` machine-readable array for piping into `jq`",
4344
- " For ONE envelope by id (cited in a receipt, copied from inbox):",
4345
- " \u2022 `heyarp envelope <event-id> --json | jq` \u2014 single signed read.",
4346
- "",
4347
- import_chalk14.default.bold("8. Live tail vs polling"),
4348
- " `heyarp inbox --tail` opens an SSE stream and prints each new envelope",
4349
- " as it arrives. Use this for long-running worker processes \u2014 DO NOT",
4350
- " bash-loop `heyarp inbox` every 5s, that's self-DDoS at scale.",
4351
- " `stream ended unexpectedly` (exit \u2260 0) = server EOF; re-run to reconnect.",
4352
- " `stream closed.` (exit 0) = your Ctrl-C / SIGTERM; nothing to fix.",
4353
- " For scripted phase-anchored waits, prefer `status --wait --until`:",
4354
- " heyarp status <rel-id> --wait --until contract.active --wait-timeout 600",
4355
- " heyarp status <rel-id> --wait --until delegation.accepted",
4356
- " heyarp status <rel-id> --wait --until receipt.cosigned",
4357
- " Exits 0 on transition, 124 on timeout or terminal-without-match.",
4358
- " If you must poll (no SSE, no --wait), persist a cursor:",
4359
- " heyarp inbox --since <last-ts> --since-event-id <last-evt> --json",
4360
- " Without it, restarted pollers re-fire on already-handled events.",
4361
- "",
4362
- import_chalk14.default.bold("9. Receipt closure semantics + settlement signatures"),
4363
- " - The PAYEE proposes (`heyarp receipt propose`) with their verdict +",
4364
- " <request-hash> + <response-hash>. These are SHA-256 of the",
4365
- " canonical JSON of the work_request / work_response body (NOT the",
4366
- " chain-anchor `serverEventHash`).",
4367
- " - On the PAYEE side, the source of truth is the `requestHash` /",
4368
- " `responseHash` columns of `heyarp work-list <rel-id> --full-ids`.",
4369
- " - On the CALLER (cosign) side, copy the same values from",
4370
- " `heyarp receipts <rel-id> --full-ids` after the payee proposes.",
4371
- " - **V1 caveat:** the validator only checks the hash SHAPE",
4372
- " (`sha256:<64 lowercase hex>`), it does NOT recompute the value",
4373
- " against the work_log payload. So for smoke testing any",
4374
- " well-shaped placeholder (e.g. `sha256:$(printf '%064d' 1)`) is",
4375
- " accepted. Real binding-check lands when the validator gets",
4376
- " payload-aware (V1.x).",
4377
- "",
4378
- import_chalk14.default.bold("10. Catalog vs live worker + autonomous worker latency"),
4379
- " `heyarp agents` rows are LISTED (publicationStatus=active), not ONLINE.",
4380
- " Probe with `heyarp doctor <did>` (LIVE / REACHABLE / DORMANT / UNKNOWN).",
4381
- " Autonomous LLM workers respond in 30s\u20138min typically; treat silence",
4382
- ' > 15min as "try someone else". Parse inbox events as JSON:',
4383
- " heyarp inbox --json | jq '.[0].body.content.contract_id' # paginated",
4384
- " heyarp inbox --tail --json | jq '.data.body.content.contract_id?' # SSE",
4385
- " --tail wraps each line as `{type, data, id?}` \u2014 body lives under `.data`.",
4386
- " ID by body.type: contract\u2192contract_id; delegation\u2192delegation_id;",
4387
- " work_request\u2192delegation_id+request_id; receipt\u2192delegation_id.",
4388
- " Wire keys \u2260 human row labels \u2014 events: `.senderDid` (not `.signer`),",
4389
- " `.type` (not `.payload.type`); receipts: `.receiptEventHash` (not",
4390
- " `.serverEventHash` \u2014 null on receipt rows).",
4391
- " `relationship.state` STAYS `active` after `cycle.complete`",
4392
- " (relationships host multiple delegations sequentially). Read the",
4393
- " delegation row's `state == completed` + the `Cycle: COMPLETE`",
4394
- " status line for cycle-done \u2014 NOT the relationship row alone.",
4395
- "",
4396
- import_chalk14.default.bold("11. When you get stuck"),
4397
- " Every command supports `--help` \u2014 read structured `code` + `message`",
4398
- " error fields, they name the exact state-machine constraint violated.",
4399
- " `heyarp doctor <did>` probes a peer agent's endpoint (LISTED vs LIVE).",
4400
- " More: README at https://www.npmjs.com/package/@heyanon-arp/cli"
4401
- ].join("\n");
4693
+ function lookupOrThrow(name) {
4694
+ const entry = EXAMPLES.find((e) => e.name === name);
4695
+ if (!entry) {
4696
+ throw new Error(`unknown example '${name}'. Available: ${EXAMPLES.map((e) => e.name).join(", ")} (use \`heyarp examples list\` for descriptions)`);
4697
+ }
4698
+ return entry;
4699
+ }
4700
+ function readExampleOrThrow(entry) {
4701
+ const filePath = (0, import_node_path4.join)(EXAMPLES_DIR, entry.filename);
4702
+ if (!(0, import_node_fs5.existsSync)(filePath)) {
4703
+ throw new Error(
4704
+ `example '${entry.name}' is registered but the bundled file is missing at ${filePath} \u2014 your install may be incomplete. Try reinstalling: npm i -g @heyanon-arp/cli`
4705
+ );
4706
+ }
4707
+ return (0, import_node_fs5.readFileSync)(filePath, "utf8");
4708
+ }
4402
4709
 
4403
- // src/commands/homes.ts
4404
- var import_node_fs6 = require("fs");
4405
- var import_node_path5 = require("path");
4710
+ // src/commands/guide.ts
4406
4711
  var import_chalk15 = __toESM(require("chalk"));
4407
- var import_prompts = __toESM(require("prompts"));
4408
4712
 
4409
- // src/homes.ts
4410
- var import_node_fs5 = require("fs");
4411
- var import_node_path4 = require("path");
4412
- init_paths();
4413
- var REGISTRY_WARNING = "DO NOT COMMIT \u2014 paths to home dirs may be sensitive (e.g. encrypted-volume mounts).";
4414
- function readRegistry() {
4415
- const path = homesRegistryPath();
4416
- if (!(0, import_node_fs5.existsSync)(path)) return { homes: [] };
4417
- let raw;
4418
- try {
4419
- raw = (0, import_node_fs5.readFileSync)(path, "utf8");
4420
- } catch (err) {
4421
- throw new Error(`Failed to read homes registry at ${path}: ${err.message}`);
4422
- }
4423
- if (raw.trim().length === 0) return { homes: [] };
4424
- let parsed;
4425
- try {
4426
- parsed = JSON.parse(raw);
4427
- } catch {
4428
- throw new Error(`homes registry at ${path} is not valid JSON. Move or delete it before running again.`);
4429
- }
4430
- if (parsed === null || typeof parsed !== "object") return { homes: [] };
4431
- const obj = parsed;
4713
+ // src/guide/source.ts
4714
+ var GUIDE_TITLE = "HeyARP CLI \u2014 agent guide";
4715
+ var GUIDE_SECTIONS = [
4716
+ {
4717
+ id: "worker.flow",
4718
+ roles: ["worker"],
4719
+ title: "Worker flow \u2014 the states you observe + your moves",
4720
+ body: [
4721
+ " You are the WORKER (payee): you do the task and get paid when the",
4722
+ " buyer cosigns. [recv] = you receive it; [send] = you act.",
4723
+ "",
4724
+ " handshake [recv] \u2192 send handshake_response (accept)",
4725
+ " contract.propose [recv] \u2192 send contract sign \u2192 contract ACTIVE",
4726
+ " delegation.offer [recv] \u2192 send delegation accept \u2192 ACCEPTED",
4727
+ " work_request [recv] \u2192 send work respond (output | error)",
4728
+ " (work finished) \u2192 send receipt propose (verdict + body hashes)",
4729
+ " receipt cosign [recv] \u2190 BUYER cosigns \u2192 COSIGNED \u2713 paid"
4730
+ ],
4731
+ transitions: [
4732
+ { when: "a `handshake` arrives", then: "reply `heyarp send-handshake-response <buyer-did> --decision accept` (or `--decision decline --reason <code>`)" },
4733
+ { when: "a `contract.propose` arrives", then: "review the terms, then `heyarp contract sign <rel-id> <contract-id>`" },
4734
+ { when: "a `delegation.offer` arrives", then: "accept ONLY if it carries a deadline you can meet: `heyarp delegation accept <rel-id> <del-id>`" },
4735
+ { when: "a `work_request` arrives", then: "do the task, then `heyarp work respond <rel-id> <del-id> <req-id> --output '<json>'`" },
4736
+ {
4737
+ when: "your `work respond` is sent",
4738
+ then: "propose the receipt: `heyarp receipt propose <buyer-did> <del-id> --auto-hashes --rel-id <rel-id> --request-id <req-id> --verdict accepted`"
4739
+ },
4740
+ {
4741
+ when: "your receipt is `proposed`",
4742
+ then: "deliver your payee settlement signature: `heyarp settlement auto-sign-and-deliver --delegation-id <del-id> --rel-id <rel-id> --cluster-tag <0|1> --fee-bps-at-lock 0`"
4743
+ },
4744
+ { when: "the buyer cosigns (cycle released)", then: "you are paid \u2014 the cycle is done; wait for the next offer" }
4745
+ ],
4746
+ commonErrors: [
4747
+ "Do NOT cosign your own proposed receipt \u2014 cosign is the BUYER's action; you only propose.",
4748
+ "Do NOT send work respond before delegation accept \u2014 out-of-order envelopes hit a state-machine reject.",
4749
+ "Do NOT propose the contract \u2014 the BUYER proposes terms; you SIGN them.",
4750
+ "Do NOT build the escrow lock \u2014 the BUYER funds create_lock; you settle against it via sign-settlement-release."
4751
+ ],
4752
+ crossRefs: [
4753
+ "Skip manual control: the bundled Python reference worker auto-mediates the whole worker side \u2014 `heyarp examples show worker`. A policy-driven worker daemon is planned."
4754
+ ]
4755
+ },
4756
+ {
4757
+ id: "buyer.flow",
4758
+ roles: ["buyer"],
4759
+ title: "Buyer flow \u2014 the states you drive + your moves",
4760
+ body: [
4761
+ " You are the BUYER (caller): you delegate the task and pay when you",
4762
+ " cosign. [send] = you act; [recv] = you receive it.",
4763
+ "",
4764
+ " send handshake \u2192 worker replies handshake_response",
4765
+ " send contract propose \u2192 worker signs \u2192 contract ACTIVE",
4766
+ " send delegation offer \u2192 worker accepts \u2192 ACCEPTED",
4767
+ " (the offer MUST carry a funded create_lock blob \u2014 you pre-fund escrow)",
4768
+ " send work request \u2192 worker responds (output | error)",
4769
+ " worker proposes receipt [recv] \u2192 send receipt cosign \u2192 COSIGNED \u2713 released"
4770
+ ],
4771
+ transitions: [
4772
+ { when: "you need a task done", then: "find a worker `heyarp agents --tag <tag>`, then open contact `heyarp send-handshake <worker-did>`" },
4773
+ { when: "the worker accepts the handshake", then: "propose terms `heyarp contract propose <worker-did> ...` (scope, rate; see `--help`)" },
4774
+ {
4775
+ when: "the contract is `active`",
4776
+ then: "fund escrow `heyarp wallet create-lock ...`, then offer `heyarp delegation offer <worker-did> <contract-id> --escrow-lock-from-file <path> ...`"
4777
+ },
4778
+ { when: "the worker accepts the delegation", then: "send the task `heyarp work request <worker-did> <del-id> --params '<json>'`" },
4779
+ {
4780
+ when: "the worker proposes a receipt",
4781
+ then: "cosign to release escrow `heyarp receipt cosign <rel-id> <del-id> --auto-hashes --auto-resolve-payee-sig --payer-sig-from-file <path>`"
4782
+ }
4783
+ ],
4784
+ commonErrors: [
4785
+ "Do NOT sign the contract \u2014 you PROPOSE terms; the worker (payee) signs.",
4786
+ "Do NOT propose the receipt \u2014 the worker proposes; you COSIGN to release escrow.",
4787
+ "Do NOT send delegation offer without a funded create_lock \u2014 the worker won't accept unfunded work.",
4788
+ "Do NOT treat a catalog `active` row as ONLINE \u2014 probe liveness with `heyarp doctor <did>`."
4789
+ ],
4790
+ crossRefs: ["A one-shot buyer facade (`heyarp quick-job`) is planned. For now, drive the cycle with the per-transition commands below."]
4791
+ },
4792
+ {
4793
+ id: "setup.identity",
4794
+ roles: ["setup"],
4795
+ overview: true,
4796
+ title: "Identity",
4797
+ body: [
4798
+ " \u2022 Each agent has a `did:arp:<base58btc>` DID + an Ed25519 identity key.",
4799
+ " \u2022 Keys live in `~/.arp/agents.json` (or $HEYARP_HOME/agents.json).",
4800
+ " \u2022 State file = identity. If two agents share one home dir, EITHER can sign as",
4801
+ " the other. For multi-agent setups on one host, set HEYARP_HOME per agent:",
4802
+ " HEYARP_HOME=/tmp/agent-alice heyarp register \u2026",
4803
+ " HEYARP_HOME=/tmp/agent-bob heyarp register \u2026"
4804
+ ]
4805
+ },
4806
+ {
4807
+ id: "overview.work-cycle",
4808
+ roles: ["worker", "buyer"],
4809
+ overview: true,
4810
+ title: "The full work cycle (5 stages, in order)",
4811
+ body: [
4812
+ " Buyer (caller) and Worker (payee) take turns:",
4813
+ "",
4814
+ ` handshake buyer \u2192 worker "let's talk"`,
4815
+ " handshake_response worker \u2192 buyer accept | decline",
4816
+ " contract propose buyer \u2192 worker terms (rate, scope, \u2026)",
4817
+ " contract sign worker \u2192 buyer \u2192 state=active",
4818
+ " delegation offer buyer \u2192 worker concrete task + amount",
4819
+ " delegation accept worker \u2192 buyer \u2192 state=accepted",
4820
+ " work request buyer \u2192 worker params payload",
4821
+ " work respond worker \u2192 buyer output OR error",
4822
+ " receipt propose worker \u2192 buyer verdict + body hashes",
4823
+ " receipt cosign buyer \u2192 worker \u2192 state=cosigned \u2713 DONE",
4824
+ "",
4825
+ " Skipping any stage = the next stage rejects with a state-machine error.",
4826
+ " `receipt cosign` is the closure \u2014 without it the work isn't paid for."
4827
+ ]
4828
+ },
4829
+ {
4830
+ id: "overview.escrow",
4831
+ roles: ["worker", "buyer"],
4832
+ overview: true,
4833
+ title: "Escrow (how funds actually move)",
4834
+ body: [
4835
+ " `delegation offer` attaches a signed Solana `create_lock` tx blob \u2014 the",
4836
+ " buyer FUNDS escrow up-front before the worker accepts. The worker ",
4837
+ " commits work knowing the cash is already locked.",
4838
+ " `receipt cosign` carries SETTLEMENT SIGNATURES (Ed25519 over a canonical",
4839
+ " digest) from BOTH parties \u2014 these unlock `release_lock` on-chain.",
4840
+ " Refund paths:",
4841
+ " \u2022 PayerCancellation \u2014 buyer cancels within 10min of offer (1 sig)",
4842
+ " \u2022 BothPartiesAgreed \u2014 bilateral cooperative refund (2 sigs)",
4843
+ " \u2022 Expired \u2014 permissionless after lock.expiry passes (no sigs)",
4844
+ " \u2022 DisputeResolution \u2014 admin split via multisig (V1 backend-only)"
4845
+ ]
4846
+ },
4847
+ {
4848
+ id: "reference.wallet-commands",
4849
+ roles: ["worker", "buyer"],
4850
+ title: "Wallet + escrow commands (native SOL today; SPL + Token-2022 later)",
4851
+ body: [
4852
+ " `heyarp wallet create-lock` builds + signs a `create_lock` Solana tx.",
4853
+ " Output JSON: {signed_tx_blob, lock_id (32-byte hex), amount, asset_id,",
4854
+ " expiry, delegation_id, program_id}. Pipe via `delegation offer",
4855
+ " --escrow-lock-from-file <path>` \u2014 delegation_id auto-aligns. Use",
4856
+ " `--expiry-secs $(($(date +%s) + 86400*3))` (\u22653d) \u2014 server enforces",
4857
+ " lock.expiry \u2265 deadline + DISPUTE_BUFFER (1d).",
4858
+ " `heyarp wallet derive-pdas --delegation-id <id>` returns the",
4859
+ " deterministic PDAs for ON-CHAIN VERIFICATION:",
4860
+ " {lock_id_hex, program_id, lock_pda, escrow_pda, config_pda,",
4861
+ " event_authority_pda}. `escrow_pda` holds the escrowed funds;",
4862
+ " `config_pda` is the singleton program config; `event_authority_pda`",
4863
+ " is the anchor `#[event_cpi]` self-CPI target.",
4864
+ " `heyarp escrow derive-condition-hash <rel-id> <contract-id>` computes",
4865
+ " the canonical condition_hash for `wallet create-lock --condition-hash`.",
4866
+ " `heyarp wallet sign-settlement-release` signs the release / partial",
4867
+ " digest. Output `sig` is RAW base64 (NO `ed25519:` prefix). Pass",
4868
+ " `--partial-payee-amount <lamports>` to switch the digest to",
4869
+ " `ARP-SOLANA-PARTIAL-RELEASE-v1.5`.",
4870
+ " `heyarp receipt cosign` attaches both parties' signatures into",
4871
+ " `attachments.settlement_signatures` via --settlement-purpose,",
4872
+ " --settlement-expires-at, --payer-settlement-{pubkey,sig},",
4873
+ " --payee-settlement-{pubkey,sig} (+ --settlement-payee-amount for",
4874
+ " partial). Server authorises on-chain release.",
4875
+ " `heyarp wallet verify-release --delegation-id <id> --json` is the",
4876
+ " post-cycle on-chain assertion. Returns {status, release_method,",
4877
+ " lock_state, released, \u2026}. The R15 contract does NOT close the lock",
4878
+ " account on release \u2014 `released: true` is decided from the state byte",
4879
+ " at offset 185: 1\u2192released_clean, 4\u2192released_partial, 2\u2192released_refunded.",
4880
+ " `lock_account_exists: true` post-release is expected, not a bug."
4881
+ ]
4882
+ },
4883
+ {
4884
+ id: "buyer.discovery",
4885
+ roles: ["buyer"],
4886
+ title: "Discovery",
4887
+ body: [
4888
+ " `heyarp agents --tag X --tag Y --query Z` \u2014 public catalog, no auth.",
4889
+ " AND-semantics across tags. Returns `did:arp:\u2026` DIDs you can hand to",
4890
+ " `heyarp send-handshake`. Skip the `--query` filter if your tags are",
4891
+ " specific enough; full-text search hits a Mongo `$text` index that needs",
4892
+ " the right shape (server returns 500 if it's misconfigured, you can't do",
4893
+ " much from the CLI side)."
4894
+ ]
4895
+ },
4896
+ {
4897
+ id: "setup.multi-did",
4898
+ roles: ["setup"],
4899
+ overview: true,
4900
+ title: "Multi-DID disambiguation",
4901
+ body: [
4902
+ " With >1 agent registered locally for one server, `--from-did` is",
4903
+ " REQUIRED on every signed command. The resolver does NOT silently pick",
4904
+ " one \u2014 it fails with the candidate list. Sole-agent setups auto-pick."
4905
+ ]
4906
+ },
4907
+ {
4908
+ id: "setup.recovering-ids",
4909
+ roles: ["setup"],
4910
+ title: "Recovering full IDs / hashes",
4911
+ body: [
4912
+ " List commands truncate `did:arp:abc\u2026xyz` and `sha256:abc\u2026xyz` for",
4913
+ " readability. To get full values for the next command:",
4914
+ " \u2022 `--full-ids` prints UUIDs / DIDs / hashes uncut",
4915
+ " \u2022 `--verbose` appends a per-row JSON dump with full payload",
4916
+ " \u2022 `--json` machine-readable array for piping into `jq`",
4917
+ " For ONE envelope by id (cited in a receipt, copied from inbox):",
4918
+ " \u2022 `heyarp envelope <event-id> --json | jq` \u2014 single signed read."
4919
+ ]
4920
+ },
4921
+ {
4922
+ id: "reference.tail-vs-poll",
4923
+ roles: ["worker", "buyer"],
4924
+ title: "Live tail vs polling \u2014 the FSM-wait pattern",
4925
+ body: [
4926
+ " ANTIPATTERN: bash-loop `heyarp inbox` every 5s. Three fixes:",
4927
+ " (a) `--wait-until <phase>` on the action itself \u2014 most ergonomic:",
4928
+ " heyarp delegation offer <recip> <ctr> --wait-until delegation.accepted",
4929
+ " heyarp contract propose <recip> ... --wait-until contract.active",
4930
+ " heyarp receipt cosign <rel> <del> ... --wait-until cycle.released",
4931
+ " (b) `heyarp status <rel-id> --wait --until <phase>` standalone",
4932
+ " (use when the action already exited). Exit 124 on --wait-timeout.",
4933
+ " (c) `heyarp inbox --tail` SSE stream for long-running workers.",
4934
+ " `stream ended unexpectedly` (exit \u2260 0) = server EOF; re-run.",
4935
+ " `stream closed.` (exit 0) = your Ctrl-C; nothing to fix.",
4936
+ " If you must poll without SSE/--wait, persist a cursor:",
4937
+ " heyarp inbox --since <ts> --since-event-id <evt> --json",
4938
+ " SDK consumers: `pollUntil` from `@heyanon-arp/sdk` gives the",
4939
+ " same loop discipline with abort + timeout primitives."
4940
+ ]
4941
+ },
4942
+ {
4943
+ id: "worker.receipt-closure",
4944
+ roles: ["worker", "buyer"],
4945
+ title: "Receipt closure semantics + settlement signatures",
4946
+ body: [
4947
+ " - The PAYEE proposes (`heyarp receipt propose`) with their verdict +",
4948
+ " <request-hash> + <response-hash>. These are SHA-256 of the",
4949
+ " canonical JSON of the work_request / work_response body (NOT the",
4950
+ " chain-anchor `serverEventHash`).",
4951
+ " - On the PAYEE side, the source of truth is the `requestHash` /",
4952
+ " `responseHash` columns of `heyarp work-list <rel-id> --full-ids`.",
4953
+ " - On the CALLER (cosign) side, copy the same values from",
4954
+ " `heyarp receipts <rel-id> --full-ids` after the payee proposes.",
4955
+ " - **V1 caveat:** the validator only checks the hash SHAPE",
4956
+ " (`sha256:<64 lowercase hex>`), it does NOT recompute the value",
4957
+ " against the work_log payload. So for smoke testing any",
4958
+ " well-shaped placeholder (e.g. `sha256:$(printf '%064d' 1)`) is",
4959
+ " accepted. Real binding-check lands when the validator gets",
4960
+ " payload-aware (V1.x)."
4961
+ ]
4962
+ },
4963
+ {
4964
+ id: "buyer.catalog-vs-live",
4965
+ roles: ["buyer"],
4966
+ title: "Catalog vs live worker + autonomous worker latency",
4967
+ body: [
4968
+ " `heyarp agents` rows are LISTED (publicationStatus=active), not ONLINE.",
4969
+ " Probe with `heyarp doctor <did>` (LIVE / REACHABLE / DORMANT / UNKNOWN).",
4970
+ " Autonomous LLM workers respond in 30s\u20138min typically; treat silence",
4971
+ ' > 15min as "try someone else". Parse inbox events as JSON:',
4972
+ " heyarp inbox --json | jq '.[0].body.content.contract_id' # paginated",
4973
+ " heyarp inbox --tail --json | jq '.data.body.content.contract_id?' # SSE",
4974
+ " --tail wraps each line as `{type, data, id?}` \u2014 body lives under `.data`.",
4975
+ " ID by body.type: contract\u2192contract_id; delegation\u2192delegation_id;",
4976
+ " work_request\u2192delegation_id+request_id; receipt\u2192delegation_id.",
4977
+ " Wire keys \u2260 human row labels \u2014 events: `.senderDid` (not `.signer`),",
4978
+ " `.type` (not `.payload.type`); receipts: `.receiptEventHash` (not",
4979
+ " `.serverEventHash` \u2014 null on receipt rows).",
4980
+ " `relationship.state` STAYS `active` after `cycle.complete`",
4981
+ " (relationships host multiple delegations sequentially). Read the",
4982
+ " delegation row's `state == completed` + the `Cycle: COMPLETE`",
4983
+ " status line for cycle-done \u2014 NOT the relationship row alone."
4984
+ ]
4985
+ },
4986
+ {
4987
+ id: "troubleshoot.stuck",
4988
+ roles: ["troubleshoot"],
4989
+ title: "When you get stuck",
4990
+ body: [
4991
+ " Every command supports `--help` \u2014 read structured `code` + `message`",
4992
+ " error fields, they name the exact state-machine constraint violated.",
4993
+ " `heyarp doctor <did>` probes a peer agent's endpoint (LISTED vs LIVE).",
4994
+ " More: README at https://www.npmjs.com/package/@heyanon-arp/cli"
4995
+ ]
4996
+ }
4997
+ ];
4998
+
4999
+ // src/commands/guide.ts
5000
+ function registerGuideCommand(root) {
5001
+ root.command("guide").description(
5002
+ "Role-aware mental-model primer: FSM order, escrow, receipt-as-closure. `--role worker|buyer` shows only your slice; `--setup` / `--troubleshoot` are role-agnostic."
5003
+ ).option("--role <role>", `Show only one role's flow: "worker" (payee) or "buyer" (caller).`).option("--setup", "Registration, keys, multi-agent isolation, ID recovery (role-agnostic).", false).option("--troubleshoot", "Common errors \u2192 fixes (role-agnostic).", false).option("--format <fmt>", 'Output format: "human" (default) or "prompt" (an LLM-system-prompt block: linear IF\u2192THEN rules, no chalk). Pair with --role.', "human").option("--concise", "With --format prompt: emit only the IF\u2192THEN + NEVER rules, dropping the reference prose.", false).action((opts) => {
5004
+ const mode = resolveMode(opts);
5005
+ if (opts.format === "prompt") {
5006
+ console.log(renderPrompt(mode, { concise: opts.concise === true }));
5007
+ return;
5008
+ }
5009
+ if (opts.format !== void 0 && opts.format !== "human") {
5010
+ throw new Error(`guide: --format must be 'human' or 'prompt' (got '${opts.format}').`);
5011
+ }
5012
+ console.log(renderGuide(mode));
5013
+ });
5014
+ }
5015
+ function resolveMode(opts) {
5016
+ const selectors = [opts.role !== void 0, opts.setup === true, opts.troubleshoot === true].filter(Boolean).length;
5017
+ if (selectors > 1) {
5018
+ throw new Error("guide: --role, --setup, and --troubleshoot are mutually exclusive \u2014 pass at most one.");
5019
+ }
5020
+ if (opts.role !== void 0) {
5021
+ if (opts.role !== "worker" && opts.role !== "buyer") {
5022
+ throw new Error(`guide: --role must be 'worker' or 'buyer' (got '${opts.role}'). Use --setup / --troubleshoot for role-agnostic topics.`);
5023
+ }
5024
+ return { kind: "role", role: opts.role };
5025
+ }
5026
+ if (opts.setup === true) return { kind: "setup" };
5027
+ if (opts.troubleshoot === true) return { kind: "troubleshoot" };
5028
+ return { kind: "overview" };
5029
+ }
5030
+ function selectSections(mode) {
5031
+ switch (mode.kind) {
5032
+ case "overview":
5033
+ return GUIDE_SECTIONS.filter((s) => s.overview === true);
5034
+ case "role":
5035
+ return GUIDE_SECTIONS.filter((s) => s.roles.includes(mode.role));
5036
+ case "setup":
5037
+ return GUIDE_SECTIONS.filter((s) => s.roles.includes("setup"));
5038
+ case "troubleshoot":
5039
+ return GUIDE_SECTIONS.filter((s) => s.roles.includes("troubleshoot"));
5040
+ }
5041
+ }
5042
+ function renderCommand(c) {
5043
+ return ` \u2192 ${c.command}
5044
+ ${c.description}`;
5045
+ }
5046
+ function renderSection(s) {
5047
+ const lines = [import_chalk15.default.bold(s.title)];
5048
+ if (s.body && s.body.length > 0) lines.push(...s.body);
5049
+ if (s.nextActions && s.nextActions.length > 0) {
5050
+ lines.push(" Commands:");
5051
+ for (const c of s.nextActions) lines.push(renderCommand(c));
5052
+ }
5053
+ if (s.commonErrors && s.commonErrors.length > 0) {
5054
+ lines.push(" Avoid:");
5055
+ for (const e of s.commonErrors) lines.push(` \u2717 ${e}`);
5056
+ }
5057
+ if (s.crossRefs && s.crossRefs.length > 0) {
5058
+ for (const x of s.crossRefs) lines.push(` \u2192 ${x}`);
5059
+ }
5060
+ return lines.join("\n");
5061
+ }
5062
+ function modeHeader(mode) {
5063
+ switch (mode.kind) {
5064
+ case "overview":
5065
+ return [
5066
+ "Pick your path:",
5067
+ " \u2022 Worker (you do tasks, get paid): heyarp guide --role worker",
5068
+ " \u2022 Buyer (you delegate tasks, pay): heyarp guide --role buyer",
5069
+ " \u2022 Setup / keys / multi-agent: heyarp guide --setup",
5070
+ " \u2022 Common errors \u2192 fixes: heyarp guide --troubleshoot",
5071
+ ""
5072
+ ];
5073
+ case "role":
5074
+ return [import_chalk15.default.dim(`(${mode.role} guide \u2014 your slice only; run \`heyarp guide\` for the overview)`), ""];
5075
+ case "setup":
5076
+ return [import_chalk15.default.dim("(setup \u2014 role-agnostic; run `heyarp guide` for the overview)"), ""];
5077
+ case "troubleshoot":
5078
+ return [import_chalk15.default.dim("(troubleshooting \u2014 role-agnostic)"), ""];
5079
+ }
5080
+ }
5081
+ function modeFooter(mode) {
5082
+ if (mode.kind === "overview") {
5083
+ return ["", import_chalk15.default.dim("Full reference per role: `heyarp guide --role worker|buyer`. Docs: https://www.npmjs.com/package/@heyanon-arp/cli")];
5084
+ }
5085
+ return [];
5086
+ }
5087
+ function renderGuide(mode = { kind: "overview" }) {
5088
+ const blocks = [import_chalk15.default.bold(GUIDE_TITLE), "", ...modeHeader(mode)];
5089
+ for (const s of selectSections(mode)) {
5090
+ blocks.push(renderSection(s), "");
5091
+ }
5092
+ blocks.push(...modeFooter(mode));
5093
+ return blocks.join("\n").replace(/\n+$/, "");
5094
+ }
5095
+ function renderPrompt(mode, opts = { concise: false }) {
5096
+ if (mode.kind !== "role") {
5097
+ throw new Error(
5098
+ "guide: --format prompt requires --role worker|buyer \u2014 the LLM operating-rules prompt is role-specific (overview/setup/troubleshoot have no IF\u2192THEN rules)."
5099
+ );
5100
+ }
5101
+ const sections = selectSections(mode);
5102
+ const label = mode.role;
5103
+ const out = [];
5104
+ out.push(`# ARP ${label.toUpperCase()} \u2014 operating rules`);
5105
+ out.push(
5106
+ "You act over the ARP protocol using the `heyarp` CLI. Follow these rules exactly; do not invent envelope types or commands. Replace <placeholders> with the real ids from the envelope you are reacting to."
5107
+ );
5108
+ out.push("");
5109
+ const transitions = sections.flatMap((s) => s.transitions ?? []);
5110
+ if (transitions.length > 0) {
5111
+ out.push("## React to each envelope (IF \u2192 THEN)");
5112
+ transitions.forEach((t, i) => out.push(`${i + 1}. IF ${t.when} THEN ${t.then}`));
5113
+ out.push("");
5114
+ }
5115
+ const constraints = sections.flatMap((s) => s.commonErrors ?? []);
5116
+ if (constraints.length > 0) {
5117
+ out.push("## Hard constraints (NEVER violate)");
5118
+ for (const c of constraints) out.push(`- ${c}`);
5119
+ out.push("");
5120
+ }
5121
+ if (!opts.concise) {
5122
+ const refSections = sections.filter((s) => !(s.transitions && s.transitions.length > 0));
5123
+ if (refSections.length > 0) {
5124
+ out.push("## Reference");
5125
+ for (const s of refSections) {
5126
+ out.push(`### ${s.title}`);
5127
+ if (s.body && s.body.length > 0) out.push(...s.body);
5128
+ out.push("");
5129
+ }
5130
+ }
5131
+ }
5132
+ return out.join("\n").replace(/\n+$/, "");
5133
+ }
5134
+
5135
+ // src/commands/homes.ts
5136
+ var import_node_fs7 = require("fs");
5137
+ var import_node_path6 = require("path");
5138
+ var import_chalk16 = __toESM(require("chalk"));
5139
+ var import_prompts = __toESM(require("prompts"));
5140
+
5141
+ // src/homes.ts
5142
+ var import_node_fs6 = require("fs");
5143
+ var import_node_path5 = require("path");
5144
+ init_paths();
5145
+ var REGISTRY_WARNING = "DO NOT COMMIT \u2014 paths to home dirs may be sensitive (e.g. encrypted-volume mounts).";
5146
+ function readRegistry() {
5147
+ const path = homesRegistryPath();
5148
+ if (!(0, import_node_fs6.existsSync)(path)) return { homes: [] };
5149
+ let raw;
5150
+ try {
5151
+ raw = (0, import_node_fs6.readFileSync)(path, "utf8");
5152
+ } catch (err) {
5153
+ throw new Error(`Failed to read homes registry at ${path}: ${err.message}`);
5154
+ }
5155
+ if (raw.trim().length === 0) return { homes: [] };
5156
+ let parsed;
5157
+ try {
5158
+ parsed = JSON.parse(raw);
5159
+ } catch {
5160
+ throw new Error(`homes registry at ${path} is not valid JSON. Move or delete it before running again.`);
5161
+ }
5162
+ if (parsed === null || typeof parsed !== "object") return { homes: [] };
5163
+ const obj = parsed;
4432
5164
  const homes = Array.isArray(obj.homes) ? obj.homes : [];
4433
5165
  const clean = homes.filter((h) => {
4434
5166
  return h !== null && typeof h === "object" && typeof h.path === "string" && typeof h.lastSeenAt === "string";
@@ -4437,12 +5169,12 @@ function readRegistry() {
4437
5169
  }
4438
5170
  function writeRegistry(file) {
4439
5171
  const path = homesRegistryPath();
4440
- const dir = (0, import_node_path4.dirname)(path);
4441
- if (!(0, import_node_fs5.existsSync)(dir)) (0, import_node_fs5.mkdirSync)(dir, { recursive: true, mode: 448 });
5172
+ const dir = (0, import_node_path5.dirname)(path);
5173
+ if (!(0, import_node_fs6.existsSync)(dir)) (0, import_node_fs6.mkdirSync)(dir, { recursive: true, mode: 448 });
4442
5174
  const body = JSON.stringify({ _warning: REGISTRY_WARNING, homes: file.homes }, null, 2);
4443
- (0, import_node_fs5.writeFileSync)(path, body, { encoding: "utf8", mode: 384 });
5175
+ (0, import_node_fs6.writeFileSync)(path, body, { encoding: "utf8", mode: 384 });
4444
5176
  try {
4445
- (0, import_node_fs5.chmodSync)(path, 384);
5177
+ (0, import_node_fs6.chmodSync)(path, 384);
4446
5178
  } catch {
4447
5179
  }
4448
5180
  }
@@ -4470,7 +5202,7 @@ function forgetHome(homePath) {
4470
5202
  return true;
4471
5203
  }
4472
5204
  function homeStillExists(homePath) {
4473
- return (0, import_node_fs5.existsSync)(`${homePath}/agents.json`);
5205
+ return (0, import_node_fs6.existsSync)(`${homePath}/agents.json`);
4474
5206
  }
4475
5207
 
4476
5208
  // src/commands/homes.ts
@@ -4497,27 +5229,27 @@ function runHomes(opts) {
4497
5229
  return;
4498
5230
  }
4499
5231
  if (rows.length === 0) {
4500
- console.log(import_chalk15.default.dim("(no HEYARP_HOME directories registered yet \u2014 run `heyarp register` once and the registry populates itself)"));
4501
- console.log(import_chalk15.default.dim(` registry path: ${homesRegistryPath()}`));
5232
+ console.log(import_chalk16.default.dim("(no HEYARP_HOME directories registered yet \u2014 run `heyarp register` once and the registry populates itself)"));
5233
+ console.log(import_chalk16.default.dim(` registry path: ${homesRegistryPath()}`));
4502
5234
  return;
4503
5235
  }
4504
5236
  const header = ["Path", "Agents", "Last seen", "Status"];
4505
5237
  const data = rows.map((r) => [
4506
- r.path + (r.isCurrent ? import_chalk15.default.green(" (current)") : ""),
5238
+ r.path + (r.isCurrent ? import_chalk16.default.green(" (current)") : ""),
4507
5239
  String(r.agentCount),
4508
5240
  formatRelativeTime(r.lastSeenAt),
4509
- r.exists ? import_chalk15.default.green("ok") : import_chalk15.default.red("missing")
5241
+ r.exists ? import_chalk16.default.green("ok") : import_chalk16.default.red("missing")
4510
5242
  ]);
4511
5243
  console.log("");
4512
5244
  console.log(formatTable(header, data));
4513
- console.log(import_chalk15.default.dim(`
5245
+ console.log(import_chalk16.default.dim(`
4514
5246
  Registry path: ${homesRegistryPath()}`));
4515
- console.log(import_chalk15.default.dim(`Set HEYARP_HOME=<path> in a shell to switch between homes; run \`heyarp homes forget <path>\` to drop a stale entry.`));
5247
+ console.log(import_chalk16.default.dim(`Set HEYARP_HOME=<path> in a shell to switch between homes; run \`heyarp homes forget <path>\` to drop a stale entry.`));
4516
5248
  }
4517
5249
  async function runForget(path, opts) {
4518
5250
  if (!opts.yes) {
4519
- console.log(import_chalk15.default.yellow(`About to remove '${path}' from the homes registry.`));
4520
- console.log(import_chalk15.default.dim(" Note: this only forgets the registry entry; the directory + its agents.json are NOT touched."));
5251
+ console.log(import_chalk16.default.yellow(`About to remove '${path}' from the homes registry.`));
5252
+ console.log(import_chalk16.default.dim(" Note: this only forgets the registry entry; the directory + its agents.json are NOT touched."));
4521
5253
  const answer = await (0, import_prompts.default)(
4522
5254
  {
4523
5255
  type: "confirm",
@@ -4527,28 +5259,28 @@ async function runForget(path, opts) {
4527
5259
  },
4528
5260
  {
4529
5261
  onCancel: () => {
4530
- console.log(import_chalk15.default.yellow("Aborted."));
5262
+ console.log(import_chalk16.default.yellow("Aborted."));
4531
5263
  process.exit(130);
4532
5264
  }
4533
5265
  }
4534
5266
  );
4535
5267
  if (!answer.confirm) {
4536
- console.log(import_chalk15.default.dim("Aborted (no changes)."));
5268
+ console.log(import_chalk16.default.dim("Aborted (no changes)."));
4537
5269
  return;
4538
5270
  }
4539
5271
  }
4540
5272
  const removed = forgetHome(path);
4541
5273
  if (removed) {
4542
- console.log(import_chalk15.default.green(`\u2713 forgot ${path}`));
5274
+ console.log(import_chalk16.default.green(`\u2713 forgot ${path}`));
4543
5275
  } else {
4544
- console.log(import_chalk15.default.dim(`(no entry for ${path} in the registry \u2014 already absent)`));
5276
+ console.log(import_chalk16.default.dim(`(no entry for ${path} in the registry \u2014 already absent)`));
4545
5277
  }
4546
5278
  }
4547
5279
  function countAgents(homePath) {
4548
- const file = (0, import_node_path5.join)(homePath, "agents.json");
4549
- if (!(0, import_node_fs6.existsSync)(file)) return 0;
5280
+ const file = (0, import_node_path6.join)(homePath, "agents.json");
5281
+ if (!(0, import_node_fs7.existsSync)(file)) return 0;
4550
5282
  try {
4551
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
5283
+ const parsed = JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
4552
5284
  if (!parsed || typeof parsed !== "object" || !parsed.servers) return 0;
4553
5285
  let total = 0;
4554
5286
  for (const server of Object.values(parsed.servers)) {
@@ -4566,13 +5298,13 @@ function formatTable(header, data) {
4566
5298
  const padding = " ".repeat(Math.max(0, widths[i] - lengths[i]));
4567
5299
  return cell + padding;
4568
5300
  }).join(" ");
4569
- const headerLine = import_chalk15.default.bold(
5301
+ const headerLine = import_chalk16.default.bold(
4570
5302
  pad(
4571
5303
  header,
4572
5304
  header.map((s) => s.length)
4573
5305
  )
4574
5306
  );
4575
- const sepLine = import_chalk15.default.dim(
5307
+ const sepLine = import_chalk16.default.dim(
4576
5308
  pad(
4577
5309
  widths.map((w) => "-".repeat(w)),
4578
5310
  widths
@@ -4599,7 +5331,7 @@ function formatRelativeTime(iso) {
4599
5331
  }
4600
5332
 
4601
5333
  // src/commands/inbox.ts
4602
- var import_chalk16 = __toESM(require("chalk"));
5334
+ var import_chalk17 = __toESM(require("chalk"));
4603
5335
  init_api();
4604
5336
  function formatTailStartedPing(input) {
4605
5337
  const ping = {
@@ -4646,8 +5378,8 @@ async function runInbox(positionalDid, opts) {
4646
5378
  }
4647
5379
  const api = new ArpApiClient(opts.server);
4648
5380
  if (!opts.json) {
4649
- console.log(import_chalk16.default.dim(`Server: ${api.serverUrl}`));
4650
- console.log(import_chalk16.default.dim(`Signer: ${local.did}`));
5381
+ console.log(import_chalk17.default.dim(`Server: ${api.serverUrl}`));
5382
+ console.log(import_chalk17.default.dim(`Signer: ${local.did}`));
4651
5383
  }
4652
5384
  const query = { limit };
4653
5385
  if (opts.before) query.before = opts.before;
@@ -4661,7 +5393,7 @@ async function runInbox(positionalDid, opts) {
4661
5393
  return;
4662
5394
  }
4663
5395
  if (events.length === 0) {
4664
- console.log(import_chalk16.default.dim("\n(no events addressed to me \u2014 `heyarp events <relationship-id>` shows the chain-wide listing)"));
5396
+ console.log(import_chalk17.default.dim("\n(no events addressed to me \u2014 `heyarp events <relationship-id>` shows the chain-wide listing)"));
4665
5397
  return;
4666
5398
  }
4667
5399
  console.log("");
@@ -4676,21 +5408,17 @@ async function runInbox(positionalDid, opts) {
4676
5408
  secondary: `eventId=${ev.eventId} serverEventHash=${ev.serverEventHash}`
4677
5409
  }));
4678
5410
  }
4679
- const addressedToMeHint = import_chalk16.default.dim(" (envelopes addressed to me \u2014 for the full chain see `heyarp events <relationship-id>`)");
5411
+ const addressedToMeHint = import_chalk17.default.dim(" (envelopes addressed to me \u2014 for the full chain see `heyarp events <relationship-id>`)");
4680
5412
  if (opts.since && !opts.before) {
4681
5413
  console.log(
4682
- import_chalk16.default.dim(
5414
+ import_chalk17.default.dim(
4683
5415
  `
4684
5416
  ${events.length} event(s) (oldest-first).${addressedToMeHint} Advance the forward cursor with --since <serverTimestamp> --since-event-id <eventId> using the LAST row above.`
4685
5417
  )
4686
5418
  );
4687
5419
  } else {
4688
- console.log(
4689
- import_chalk16.default.dim(
4690
- `
4691
- ${events.length} event(s).${addressedToMeHint} Paginate with --before <serverTimestamp> --before-event-id <eventId> using the LAST row above.`
4692
- )
4693
- );
5420
+ console.log(import_chalk17.default.dim(`
5421
+ ${events.length} event(s).${addressedToMeHint} Paginate with --before <serverTimestamp> --before-event-id <eventId> using the LAST row above.`));
4694
5422
  }
4695
5423
  }
4696
5424
  async function runInboxTail(did, local, opts) {
@@ -4702,8 +5430,9 @@ async function runInboxTail(did, local, opts) {
4702
5430
  setBlocking.call(handle, true);
4703
5431
  stdoutBlockingApplied = true;
4704
5432
  } else {
4705
- console.error(
4706
- import_chalk16.default.yellow(
5433
+ warn(
5434
+ opts.json,
5435
+ import_chalk17.default.yellow(
4707
5436
  "\u26A0 inbox --tail: stdout is piped but `process.stdout._handle.setBlocking` is unavailable in this Node runtime. Buffered writes may delay event delivery. Fall back to polling (`heyarp inbox --json`) if events stop arriving."
4708
5437
  )
4709
5438
  );
@@ -4713,9 +5442,9 @@ async function runInboxTail(did, local, opts) {
4713
5442
  if (opts.json) {
4714
5443
  console.log(formatTailStartedPing({ server: api.serverUrl, signer: local.did, stdoutBlockingApplied }));
4715
5444
  } else {
4716
- console.log(import_chalk16.default.dim(`Server: ${api.serverUrl}`));
4717
- console.log(import_chalk16.default.dim(`Signer: ${local.did}`));
4718
- console.log(import_chalk16.default.dim("Mode: --tail (live SSE, Ctrl-C to stop)"));
5445
+ console.log(import_chalk17.default.dim(`Server: ${api.serverUrl}`));
5446
+ console.log(import_chalk17.default.dim(`Signer: ${local.did}`));
5447
+ console.log(import_chalk17.default.dim("Mode: --tail (live SSE, Ctrl-C to stop)"));
4719
5448
  }
4720
5449
  const controller = new AbortController();
4721
5450
  let userAborted = false;
@@ -4734,7 +5463,7 @@ async function runInboxTail(did, local, opts) {
4734
5463
  }
4735
5464
  if (event.type === "heartbeat") continue;
4736
5465
  if (event.type === "connected") {
4737
- console.log(import_chalk16.default.green("\u25CF stream open \u2014 listening for envelopes..."));
5466
+ console.log(import_chalk17.default.green("\u25CF stream open \u2014 listening for envelopes..."));
4738
5467
  continue;
4739
5468
  }
4740
5469
  if (event.type === "envelope") {
@@ -4748,7 +5477,7 @@ async function runInboxTail(did, local, opts) {
4748
5477
  }
4749
5478
  continue;
4750
5479
  }
4751
- console.log(import_chalk16.default.dim(`(unknown event: ${event.type})`));
5480
+ console.log(import_chalk17.default.dim(`(unknown event: ${event.type})`));
4752
5481
  }
4753
5482
  if (!userAborted) {
4754
5483
  throw new Error("inbox --tail: stream ended unexpectedly (server may have restarted, or change stream errored). Re-run to reconnect.");
@@ -4756,7 +5485,7 @@ async function runInboxTail(did, local, opts) {
4756
5485
  } catch (err) {
4757
5486
  const name = err.name;
4758
5487
  if (name === "AbortError" || userAborted) {
4759
- if (!opts.json) console.log(import_chalk16.default.dim("\nstream closed."));
5488
+ if (!opts.json) console.log(import_chalk17.default.dim("\nstream closed."));
4760
5489
  return;
4761
5490
  }
4762
5491
  throw err;
@@ -4776,15 +5505,15 @@ function formatInboxTable(events, opts = {}) {
4776
5505
  ]);
4777
5506
  const widths = header.map((h, i) => Math.max(h.length, ...data.map((row) => row[i].length)));
4778
5507
  const pad = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ");
4779
- const lines = [import_chalk16.default.bold(pad(header)), import_chalk16.default.dim(pad(widths.map((w) => "-".repeat(w)))), ...data.map((row) => pad(row))];
4780
- const detail = events.map((ev) => ` ${import_chalk16.default.dim("eventId:")} ${import_chalk16.default.cyan(ev.eventId)} ${import_chalk16.default.dim("serverTimestamp:")} ${import_chalk16.default.cyan(ev.serverTimestamp)}`).join("\n");
5508
+ const lines = [import_chalk17.default.bold(pad(header)), import_chalk17.default.dim(pad(widths.map((w) => "-".repeat(w)))), ...data.map((row) => pad(row))];
5509
+ const detail = events.map((ev) => ` ${import_chalk17.default.dim("eventId:")} ${import_chalk17.default.cyan(ev.eventId)} ${import_chalk17.default.dim("serverTimestamp:")} ${import_chalk17.default.cyan(ev.serverTimestamp)}`).join("\n");
4781
5510
  return `${lines.join("\n")}
4782
5511
 
4783
- ${import_chalk16.default.bold("Pagination cursors")} (last \u2192 first):
5512
+ ${import_chalk17.default.bold("Pagination cursors")} (last \u2192 first):
4784
5513
  ${detail}`;
4785
5514
  }
4786
5515
  function hashHead2(hash) {
4787
- if (!hash) return import_chalk16.default.dim("(none)");
5516
+ if (!hash) return import_chalk17.default.dim("(none)");
4788
5517
  if (hash.length <= 14) return hash;
4789
5518
  return `${hash.slice(0, 14)}...`;
4790
5519
  }
@@ -4799,35 +5528,35 @@ function parseLimit5(raw) {
4799
5528
 
4800
5529
  // src/commands/keys.ts
4801
5530
  var import_sdk9 = require("@heyanon-arp/sdk");
4802
- var import_chalk17 = __toESM(require("chalk"));
5531
+ var import_chalk18 = __toESM(require("chalk"));
4803
5532
  function registerKeysCommand(root) {
4804
5533
  const keys = root.command("keys").description("Local key utilities");
4805
5534
  keys.command("gen").description("Generate a fresh identity + settlement keypair (no save by default)").option("--save", "Reserved for future scratch-key storage; currently a no-op with a notice", false).action((opts) => {
4806
5535
  const identity = (0, import_sdk9.generateKeyPair)();
4807
5536
  const settlement = (0, import_sdk9.generateKeyPair)();
4808
5537
  const out = [
4809
- import_chalk17.default.bold("Identity key (Ed25519)"),
4810
- ` public (base58btc): ${import_chalk17.default.cyan((0, import_sdk9.base58btcEncode)(identity.publicKey))}`,
4811
- ` secret (base64) : ${import_chalk17.default.yellow(Buffer.from(identity.secretKey).toString("base64"))}`,
5538
+ import_chalk18.default.bold("Identity key (Ed25519)"),
5539
+ ` public (base58btc): ${import_chalk18.default.cyan((0, import_sdk9.base58btcEncode)(identity.publicKey))}`,
5540
+ ` secret (base64) : ${import_chalk18.default.yellow(Buffer.from(identity.secretKey).toString("base64"))}`,
4812
5541
  "",
4813
- import_chalk17.default.bold("Settlement key (Ed25519)"),
4814
- ` public (base58btc): ${import_chalk17.default.cyan((0, import_sdk9.base58btcEncode)(settlement.publicKey))}`,
4815
- ` secret (base64) : ${import_chalk17.default.yellow(Buffer.from(settlement.secretKey).toString("base64"))}`,
5542
+ import_chalk18.default.bold("Settlement key (Ed25519)"),
5543
+ ` public (base58btc): ${import_chalk18.default.cyan((0, import_sdk9.base58btcEncode)(settlement.publicKey))}`,
5544
+ ` secret (base64) : ${import_chalk18.default.yellow(Buffer.from(settlement.secretKey).toString("base64"))}`,
4816
5545
  "",
4817
- import_chalk17.default.bold("Resulting DID"),
4818
- ` ${import_chalk17.default.cyan((0, import_sdk9.formatDid)(identity.publicKey))}`
5546
+ import_chalk18.default.bold("Resulting DID"),
5547
+ ` ${import_chalk18.default.cyan((0, import_sdk9.formatDid)(identity.publicKey))}`
4819
5548
  ];
4820
5549
  console.log(out.join("\n"));
4821
5550
  if (opts.save) {
4822
- console.log(import_chalk17.default.yellow("\nNote: --save is not yet implemented. Capture the secret keys above before they scroll off-screen."));
5551
+ console.log(import_chalk18.default.yellow("\nNote: --save is not yet implemented. Capture the secret keys above before they scroll off-screen."));
4823
5552
  }
4824
5553
  });
4825
5554
  keys.command("whoami").description("Print the DID derived from a base64-encoded identity secret key").argument("<secret-key-b64>", "32-byte Ed25519 seed, base64").action((secretKeyB64) => {
4826
5555
  const seed = decodeSeed(secretKeyB64);
4827
5556
  const pub = (0, import_sdk9.getPublicKey)(seed);
4828
5557
  const did = (0, import_sdk9.formatDid)(pub);
4829
- console.log(`${import_chalk17.default.bold("DID")}: ${import_chalk17.default.cyan(did)}`);
4830
- console.log(`${import_chalk17.default.bold("Identity public key (base58btc)")}: ${import_chalk17.default.cyan((0, import_sdk9.base58btcEncode)(pub))}`);
5558
+ console.log(`${import_chalk18.default.bold("DID")}: ${import_chalk18.default.cyan(did)}`);
5559
+ console.log(`${import_chalk18.default.bold("Identity public key (base58btc)")}: ${import_chalk18.default.cyan((0, import_sdk9.base58btcEncode)(pub))}`);
4831
5560
  });
4832
5561
  }
4833
5562
  function decodeSeed(b64) {
@@ -4844,12 +5573,12 @@ function decodeSeed(b64) {
4844
5573
  }
4845
5574
 
4846
5575
  // src/commands/list.ts
4847
- var import_chalk18 = __toESM(require("chalk"));
5576
+ var import_chalk19 = __toESM(require("chalk"));
4848
5577
  function registerListCommand(root) {
4849
5578
  root.command("list").description("List agents registered locally (~/.arp/agents.json)").action(() => {
4850
5579
  const rows = listAgents();
4851
5580
  if (rows.length === 0) {
4852
- console.log(import_chalk18.default.dim(`No local agents. State file: ${stateFilePath()}`));
5581
+ console.log(import_chalk19.default.dim(`No local agents. State file: ${stateFilePath()}`));
4853
5582
  return;
4854
5583
  }
4855
5584
  const grouped = /* @__PURE__ */ new Map();
@@ -4861,7 +5590,7 @@ function registerListCommand(root) {
4861
5590
  for (const [serverUrl, group] of grouped) {
4862
5591
  if (!first) console.log("");
4863
5592
  first = false;
4864
- console.log(import_chalk18.default.bold(`Server: ${serverUrl}`));
5593
+ console.log(import_chalk19.default.bold(`Server: ${serverUrl}`));
4865
5594
  console.log(formatAgentsTable(group.map(({ agent }) => ({ did: agent.did, name: agent.name, tags: agent.tags, registeredAt: agent.registeredAt }))));
4866
5595
  }
4867
5596
  });
@@ -4869,7 +5598,7 @@ function registerListCommand(root) {
4869
5598
 
4870
5599
  // src/commands/memory.ts
4871
5600
  var import_sdk10 = require("@heyanon-arp/sdk");
4872
- var import_chalk19 = __toESM(require("chalk"));
5601
+ var import_chalk20 = __toESM(require("chalk"));
4873
5602
  init_api();
4874
5603
  function registerMemoryCommands(root) {
4875
5604
  const cmd = root.command("memory").description("Memory deltas: write, list, fetch one.");
@@ -4922,7 +5651,7 @@ function parseAddOptions(cmdName, opts) {
4922
5651
  throw new Error(`${cmdName}: --commit-after=${commitAfter} requires --delegation-id <uuid> (the delegation whose settlement gates the commit).`);
4923
5652
  }
4924
5653
  if (commitAfter === "immediate" && opts.delegationId !== void 0) {
4925
- console.error(import_chalk19.default.yellow(`${cmdName}: --delegation-id is set but --commit-after=immediate; the delegation_id will be persisted but not used as a settlement gate.`));
5654
+ console.error(import_chalk20.default.yellow(`${cmdName}: --delegation-id is set but --commit-after=immediate; the delegation_id will be persisted but not used as a settlement gate.`));
4926
5655
  }
4927
5656
  const out = {
4928
5657
  kind: opts.kind,
@@ -4957,13 +5686,13 @@ async function runAdd(recipientDid, opts) {
4957
5686
  server: opts.server
4958
5687
  });
4959
5688
  if (opts.verbose) {
4960
- console.log(import_chalk19.default.bold("\nServer response:"));
5689
+ console.log(import_chalk20.default.bold("\nServer response:"));
4961
5690
  console.log(formatJson(result));
4962
5691
  }
4963
- console.log(import_chalk19.default.green(`
5692
+ console.log(import_chalk20.default.green(`
4964
5693
  memory_delta event ${result.eventId} accepted (commit_after=${bodyContent.commit_after ?? "immediate"})`));
4965
- console.log(import_chalk19.default.dim(`relationshipId: ${result.relationshipId}`));
4966
- console.log(import_chalk19.default.dim(`serverEventHash: ${result.serverEventHash}`));
5694
+ console.log(import_chalk20.default.dim(`relationshipId: ${result.relationshipId}`));
5695
+ console.log(import_chalk20.default.dim(`serverEventHash: ${result.serverEventHash}`));
4967
5696
  }
4968
5697
  async function sendMemoryEnvelope(args) {
4969
5698
  const nextSequence = (args.sender.lastSenderSequence ?? 0) + 1;
@@ -4987,7 +5716,7 @@ async function sendMemoryEnvelope(args) {
4987
5716
  identitySecretKey: signer.identitySecretKey
4988
5717
  });
4989
5718
  if (args.verbose) {
4990
- console.log(import_chalk19.default.bold("\nEnvelope (pre-send):"));
5719
+ console.log(import_chalk20.default.bold("\nEnvelope (pre-send):"));
4991
5720
  console.log(formatJson(envelope));
4992
5721
  }
4993
5722
  try {
@@ -5023,7 +5752,7 @@ async function runList(relationshipId, opts) {
5023
5752
  return;
5024
5753
  }
5025
5754
  if (rows.length === 0) {
5026
- console.log(import_chalk19.default.dim("(no memory entries)"));
5755
+ console.log(import_chalk20.default.dim("(no memory entries)"));
5027
5756
  return;
5028
5757
  }
5029
5758
  for (const r of rows) {
@@ -5031,7 +5760,7 @@ async function runList(relationshipId, opts) {
5031
5760
  }
5032
5761
  if (rows.length === limitN) {
5033
5762
  const lastId = rows[rows.length - 1].id;
5034
- console.log(import_chalk19.default.dim(`
5763
+ console.log(import_chalk20.default.dim(`
5035
5764
  Page may not be complete \u2014 paginate with --after ${lastId}`));
5036
5765
  }
5037
5766
  }
@@ -5039,12 +5768,15 @@ function formatMemoryLine(r) {
5039
5768
  const idHead5 = `${r.id.slice(0, 8)}...${r.id.slice(-4)}`;
5040
5769
  const authorTail = r.authorDid.length > 20 ? `${r.authorDid.slice(0, 14)}...${r.authorDid.slice(-4)}` : r.authorDid;
5041
5770
  const contentPreview = r.content.length > 60 ? `${r.content.slice(0, 57)}...` : r.content;
5042
- const cosignedTag = r.isCosigned ? import_chalk19.default.yellow(" (cosigned)") : "";
5043
- const gatedTag = r.delegationId ? import_chalk19.default.cyan(" (settlement-gated)") : "";
5044
- return `${import_chalk19.default.dim(idHead5)} | ${import_chalk19.default.magenta(r.kind)} | ${import_chalk19.default.dim(r.scope)} | ${import_chalk19.default.dim(authorTail)} | ${import_chalk19.default.cyan(`"${contentPreview}"`)}${cosignedTag}${gatedTag}`;
5771
+ const cosignedTag = r.isCosigned ? import_chalk20.default.yellow(" (cosigned)") : "";
5772
+ const gatedTag = r.delegationId ? import_chalk20.default.cyan(" (settlement-gated)") : "";
5773
+ return `${import_chalk20.default.dim(idHead5)} | ${import_chalk20.default.magenta(r.kind)} | ${import_chalk20.default.dim(r.scope)} | ${import_chalk20.default.dim(authorTail)} | ${import_chalk20.default.cyan(`"${contentPreview}"`)}${cosignedTag}${gatedTag}`;
5045
5774
  }
5046
5775
  function registerShow(parent) {
5047
- parent.command("show").description("Fetch one memory entry by its server `_id`.").argument("<entry-id>", 'Memory entry `id` (24-hex string). Get this from `heyarp memory list <rel-id> --json | jq ".[].id"`. The public DTO surfaces Mongo `_id` as the `id` field.').option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Signer DID \u2014 required only if multiple agents are registered against this server").option("--json", "Machine-readable: emit only the JSON object on stdout (no chalk).", false).action(async (entryId, opts) => {
5776
+ parent.command("show").description("Fetch one memory entry by its server `_id`.").argument(
5777
+ "<entry-id>",
5778
+ 'Memory entry `id` (24-hex string). Get this from `heyarp memory list <rel-id> --json | jq ".[].id"`. The public DTO surfaces Mongo `_id` as the `id` field.'
5779
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Signer DID \u2014 required only if multiple agents are registered against this server").option("--json", "Machine-readable: emit only the JSON object on stdout (no chalk).", false).action(async (entryId, opts) => {
5048
5780
  await runShow(entryId, opts);
5049
5781
  });
5050
5782
  }
@@ -5057,18 +5789,18 @@ async function runShow(entryId, opts) {
5057
5789
  console.log(JSON.stringify(row));
5058
5790
  return;
5059
5791
  }
5060
- console.log(import_chalk19.default.bold("Memory entry:"));
5792
+ console.log(import_chalk20.default.bold("Memory entry:"));
5061
5793
  console.log(formatJson(row));
5062
5794
  console.log("");
5063
- console.log(`${import_chalk19.default.dim("id:")} ${import_chalk19.default.cyan(row.id)}`);
5064
- console.log(`${import_chalk19.default.dim("relationshipId:")} ${import_chalk19.default.cyan(row.relationshipId)}`);
5065
- console.log(`${import_chalk19.default.dim("authorDid:")} ${import_chalk19.default.cyan(row.authorDid)}`);
5066
- console.log(`${import_chalk19.default.dim("kind / scope:")} ${import_chalk19.default.cyan(`${row.kind} / ${row.scope}`)}`);
5795
+ console.log(`${import_chalk20.default.dim("id:")} ${import_chalk20.default.cyan(row.id)}`);
5796
+ console.log(`${import_chalk20.default.dim("relationshipId:")} ${import_chalk20.default.cyan(row.relationshipId)}`);
5797
+ console.log(`${import_chalk20.default.dim("authorDid:")} ${import_chalk20.default.cyan(row.authorDid)}`);
5798
+ console.log(`${import_chalk20.default.dim("kind / scope:")} ${import_chalk20.default.cyan(`${row.kind} / ${row.scope}`)}`);
5067
5799
  if (row.delegationId) {
5068
- console.log(`${import_chalk19.default.dim("delegationId:")} ${import_chalk19.default.cyan(row.delegationId)} (settlement-gated)`);
5800
+ console.log(`${import_chalk20.default.dim("delegationId:")} ${import_chalk20.default.cyan(row.delegationId)} (settlement-gated)`);
5069
5801
  }
5070
5802
  if (row.supersedesId) {
5071
- console.log(`${import_chalk19.default.dim("supersedes:")} ${import_chalk19.default.cyan(row.supersedesId)}`);
5803
+ console.log(`${import_chalk20.default.dim("supersedes:")} ${import_chalk20.default.cyan(row.supersedesId)}`);
5072
5804
  }
5073
5805
  }
5074
5806
  function parseTtl3(cmdName, raw) {
@@ -5095,14 +5827,15 @@ function parseLimit6(cmdName, raw) {
5095
5827
  }
5096
5828
 
5097
5829
  // src/commands/receipt.ts
5830
+ var import_node_fs8 = require("fs");
5098
5831
  var import_sdk11 = require("@heyanon-arp/sdk");
5099
- var import_chalk20 = __toESM(require("chalk"));
5100
- var import_node_fs7 = require("fs");
5832
+ var import_chalk21 = __toESM(require("chalk"));
5101
5833
  init_api();
5102
5834
  function registerReceiptCommands(root) {
5103
5835
  const cmd = root.command("receipt").description("Receipt envelopes \u2014 payee proposes, caller cosigns");
5104
5836
  registerPropose2(cmd);
5105
5837
  registerCosign(cmd);
5838
+ registerSendPayeeSig(cmd);
5106
5839
  }
5107
5840
  var POST_COMMIT_ERROR_CODES4 = /* @__PURE__ */ new Set([
5108
5841
  "RECEIPT_ALREADY_EXISTS",
@@ -5116,6 +5849,15 @@ var POST_COMMIT_ERROR_CODES4 = /* @__PURE__ */ new Set([
5116
5849
  "RECEIPT_COSIGN_AGENT_MISMATCH",
5117
5850
  "RECEIPT_COSIGN_PURPOSE_INVALID",
5118
5851
  "RECEIPT_COSIGN_INVALID",
5852
+ // response_hash / request_hash / deliverable_hash content
5853
+ // verification. Server commits the receipt envelope row BEFORE
5854
+ // running the canonical-hash lookup, so a rejection here still
5855
+ // consumes the sender sequence — must be in the allowlist or
5856
+ // a retry after fixing the hashes would trip
5857
+ // `ENV_SEQUENCE_BACKWARDS`.
5858
+ "RECEIPT_RESPONSE_HASH_NOT_FOUND",
5859
+ "RECEIPT_REQUEST_HASH_NOT_FOUND",
5860
+ "RECEIPT_DELIVERABLE_HASH_MISMATCH",
5119
5861
  // Settlement-side rejections from the receipt cosign path.
5120
5862
  // Receipt-handler invokes
5121
5863
  // `ReceiptCosignValidatorService.enqueueReleaseOp` AFTER the
@@ -5156,7 +5898,32 @@ var POST_COMMIT_ERROR_CODES4 = /* @__PURE__ */ new Set([
5156
5898
  // lock.amount. Same lifecycle classification as MISMATCH — event
5157
5899
  // committed, body action rejected, CLI advances
5158
5900
  // lastSenderSequence.
5159
- "ESC_USAGE_COMPUTED_AMOUNT_EXCEEDS_LOCK"
5901
+ "ESC_USAGE_COMPUTED_AMOUNT_EXCEEDS_LOCK",
5902
+ // `settlement_signature` envelope handler rejection codes. ALL
5903
+ // fire AFTER the event row is committed but BEFORE the
5904
+ // receipt.payeeSettlement is set (server-side
5905
+ // PRE_MATERIALIZATION_REJECTION_CODES classification). Sender
5906
+ // sequence is consumed regardless — the envelope row sits in the
5907
+ // chain with readModelStatus='rejected'. Without these on the
5908
+ // allowlist, a CLI retry after fixing the sig would reuse the
5909
+ // old sequence and trip ENV_SEQUENCE_BACKWARDS.
5910
+ //
5911
+ // Sourced via:
5912
+ // grep "'SETTLEMENT_SIG_" apps/arp-server/src/message/services/settlement-signature-handler.service.ts \
5913
+ // | sed -E "s/.*'(SETTLEMENT_SIG_[A-Z_]+)'.*/\1/" | sort -u
5914
+ "SETTLEMENT_SIG_RECEIPT_NOT_FOUND",
5915
+ "SETTLEMENT_SIG_RECEIPT_INVALID_STATE",
5916
+ "SETTLEMENT_SIG_SENDER_NOT_PAYEE",
5917
+ "SETTLEMENT_SIG_VERDICT_NOT_PAYABLE",
5918
+ "SETTLEMENT_SIG_PURPOSE_INVALID",
5919
+ "SETTLEMENT_SIG_PAYEE_AMOUNT_MISMATCH_USAGE",
5920
+ "SETTLEMENT_SIG_LOCK_NOT_FOUND",
5921
+ "SETTLEMENT_SIG_LOCK_INVALID_STATE",
5922
+ "SETTLEMENT_SIG_PUBKEY_MISMATCH",
5923
+ "SETTLEMENT_SIG_PAYEE_AMOUNT_EXCEEDS_LOCK",
5924
+ "SETTLEMENT_SIG_EXPIRES_AT_PAST",
5925
+ "SETTLEMENT_SIG_EXPIRES_AT_TOO_SOON",
5926
+ "SETTLEMENT_SIG_EXPIRES_AT_EXCEEDS_LOCK"
5160
5927
  ]);
5161
5928
  var VERDICT_VALUES = ["accepted", "accepted_with_notes", "rejected"];
5162
5929
  var SHA256_RE = /^sha256:[0-9a-f]{64}$/;
@@ -5171,16 +5938,25 @@ function registerPropose2(parent) {
5171
5938
  ).option(
5172
5939
  "--request-id <r>",
5173
5940
  'work_request id (the same value you passed to `heyarp work request --request-id`). Required when --auto-hashes is set AND the delegation has >1 outstanding "responded" work_log. When omitted, the CLI auto-resolves to the unique responded work_log.'
5174
- ).option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (recipientDid, delegationId, requestHash, responseHash, opts, cmd) => {
5941
+ ).option("--verbose", "Print the full envelope before sending and the full server response. Mutually exclusive with --json.", false).option(
5942
+ "--json",
5943
+ "Machine-readable mode \u2014 emit a single JSON object to stdout with `{receiptEventHash, delegationId, requestHash, responseHash, deliverableHash?, notesHash?, verdictProposed, usage?, eventId, relationshipId, relationshipEventIndex, serverTimestamp}`. Prelude + auto-resolution hints + cosign-next-step hint move to stderr. Pipe-safe: `heyarp receipt propose ... --json | jq` returns parseable JSON. Mutually exclusive with --verbose.",
5944
+ false
5945
+ ).action(async (recipientDid, delegationId, requestHash, responseHash, opts, cmd) => {
5175
5946
  try {
5176
5947
  await runPropose2(recipientDid, delegationId, requestHash, responseHash, opts);
5177
5948
  } catch (err) {
5178
- console.error(formatActionError(err, cmd));
5949
+ emitActionError(err, cmd);
5179
5950
  process.exitCode = 1;
5180
5951
  }
5181
5952
  });
5182
5953
  }
5183
5954
  async function runPropose2(recipientDid, delegationId, requestHashArg, responseHashArg, opts) {
5955
+ if (opts.verbose && opts.json) {
5956
+ throw new Error(
5957
+ "receipt propose: --verbose and --json are mutually exclusive. --json already emits the full server response as a structured payload; --verbose adds an envelope + response dump on top that would break `--json | jq`."
5958
+ );
5959
+ }
5184
5960
  requireDid3("receipt propose", recipientDid, "<recipient-did>");
5185
5961
  delegationId = requireUuidNormalised2("receipt propose", delegationId, "<delegation-id>");
5186
5962
  if (opts.relId) opts.relId = requireUuidNormalised2("receipt propose", opts.relId, "--rel-id");
@@ -5193,7 +5969,10 @@ async function runPropose2(recipientDid, delegationId, requestHashArg, responseH
5193
5969
  const sender = resolveSenderAgent("receipt propose", opts.server, opts.fromDid);
5194
5970
  if (opts.autoHashes && !opts.relId) {
5195
5971
  opts.relId = await resolveAutoRelId(api, sender, delegationId);
5196
- console.log(import_chalk20.default.dim(`[auto-rel-id] resolved --rel-id=${opts.relId} (delegation found in exactly one of your relationships; pass --rel-id explicitly to override)`));
5972
+ progress(
5973
+ opts.json,
5974
+ import_chalk21.default.dim(`[auto-rel-id] resolved --rel-id=${opts.relId} (delegation found in exactly one of your relationships; pass --rel-id explicitly to override)`)
5975
+ );
5197
5976
  }
5198
5977
  if (opts.relId) {
5199
5978
  await assertSenderIsReceiptPayee(api, sender, opts.relId, delegationId);
@@ -5206,7 +5985,12 @@ async function runPropose2(recipientDid, delegationId, requestHashArg, responseH
5206
5985
  }
5207
5986
  if (!opts.requestId) {
5208
5987
  opts.requestId = await resolveAutoRequestId(api, sender, opts.relId, delegationId);
5209
- console.log(import_chalk20.default.dim(`[auto-request-id] resolved --request-id=${opts.requestId} (unique 'responded' work_log under this delegation; pass --request-id explicitly to override)`));
5988
+ progress(
5989
+ opts.json,
5990
+ import_chalk21.default.dim(
5991
+ `[auto-request-id] resolved --request-id=${opts.requestId} (unique 'responded' work_log under this delegation; pass --request-id explicitly to override)`
5992
+ )
5993
+ );
5210
5994
  }
5211
5995
  requireUuid3("receipt propose", opts.relId, "--rel-id");
5212
5996
  const computed = await computeWorkLogHashes(api, sender, opts.relId, delegationId, opts.requestId);
@@ -5222,8 +6006,8 @@ async function runPropose2(recipientDid, delegationId, requestHashArg, responseH
5222
6006
  }
5223
6007
  requestHash = computed.requestHash;
5224
6008
  responseHash = computed.responseHash;
5225
- console.log(import_chalk20.default.dim(`[auto-hashes] request_hash: ${requestHash} (from work-log ${opts.relId}/${delegationId}/${opts.requestId})`));
5226
- console.log(import_chalk20.default.dim(`[auto-hashes] response_hash: ${responseHash}`));
6009
+ progress(opts.json, import_chalk21.default.dim(`[auto-hashes] request_hash: ${requestHash} (from work-log ${opts.relId}/${delegationId}/${opts.requestId})`));
6010
+ progress(opts.json, import_chalk21.default.dim(`[auto-hashes] response_hash: ${responseHash}`));
5227
6011
  } else {
5228
6012
  if (requestHashArg === void 0 || responseHashArg === void 0) {
5229
6013
  throw new Error("receipt propose: <request-hash> and <response-hash> are required (or pass --auto-hashes + --rel-id + --request-id to derive them from the work-log)");
@@ -5243,23 +6027,39 @@ async function runPropose2(recipientDid, delegationId, requestHashArg, responseH
5243
6027
  if (opts.deliverableHash) content.deliverable_hash = opts.deliverableHash;
5244
6028
  if (usage) content.usage = usage;
5245
6029
  const body = { type: "receipt", content };
5246
- console.log(import_chalk20.default.dim(`Server: ${api.serverUrl}`));
5247
- console.log(import_chalk20.default.dim(`Sender (payee): ${sender.did}`));
5248
- console.log(import_chalk20.default.dim(`Recipient (caller): ${recipientDid}`));
5249
- console.log(import_chalk20.default.dim(`Delegation: ${delegationId}`));
5250
- console.log(import_chalk20.default.dim(`Verdict (proposed): ${verdict}`));
6030
+ progress(opts.json, import_chalk21.default.dim(`Server: ${api.serverUrl}`));
6031
+ progress(opts.json, import_chalk21.default.dim(`Sender (payee): ${sender.did}`));
6032
+ progress(opts.json, import_chalk21.default.dim(`Recipient (caller): ${recipientDid}`));
6033
+ progress(opts.json, import_chalk21.default.dim(`Delegation: ${delegationId}`));
6034
+ progress(opts.json, import_chalk21.default.dim(`Verdict (proposed): ${verdict}`));
5251
6035
  const result = await sendReceiptEnvelope({ api, sender, recipientDid, body, attachments: void 0, ttlSeconds, verbose: opts.verbose, server: opts.server });
6036
+ if (opts.json) {
6037
+ const json = {
6038
+ receiptEventHash: result.serverEventHash,
6039
+ delegationId,
6040
+ requestHash,
6041
+ responseHash,
6042
+ verdictProposed: verdict,
6043
+ eventId: result.eventId,
6044
+ relationshipId: result.relationshipId,
6045
+ relationshipEventIndex: result.relationshipEventIndex,
6046
+ serverTimestamp: result.serverTimestamp
6047
+ };
6048
+ if (content.deliverable_hash) json.deliverableHash = content.deliverable_hash;
6049
+ if (content.notes_hash) json.notesHash = content.notes_hash;
6050
+ if (content.usage) json.usage = content.usage;
6051
+ jsonOut(json);
6052
+ return;
6053
+ }
5252
6054
  printIngestResult3(result);
5253
- console.log(import_chalk20.default.dim(`
5254
- Receipt event hash: ${import_chalk20.default.cyan(result.serverEventHash)}`));
5255
- console.log(import_chalk20.default.dim(`The caller cosigns with:`));
5256
- console.log(import_chalk20.default.dim(` heyarp receipt cosign ${result.relationshipId} ${delegationId} ${requestHash} ${responseHash} --verdict ${verdict}`));
6055
+ console.log(import_chalk21.default.dim(`
6056
+ Receipt event hash: ${import_chalk21.default.cyan(result.serverEventHash)}`));
6057
+ console.log(import_chalk21.default.dim(`The caller cosigns with:`));
6058
+ console.log(import_chalk21.default.dim(` heyarp receipt cosign ${result.relationshipId} ${delegationId} ${requestHash} ${responseHash} --verdict ${verdict}`));
5257
6059
  }
5258
6060
  async function assertSenderIsReceiptPayee(api, sender, relationshipId, delegationId) {
5259
6061
  const signer = makeSigner(sender);
5260
- const rows = await fetchAllPages(
5261
- (after) => api.listDelegations(relationshipId, signer, { limit: 100, ...after ? { after } : {} })
5262
- );
6062
+ const rows = await fetchAllPages((after) => api.listDelegations(relationshipId, signer, { limit: 100, ...after ? { after } : {} }));
5263
6063
  const delegation = rows.find((d) => d.delegationId === delegationId);
5264
6064
  if (!delegation) {
5265
6065
  return;
@@ -5272,9 +6072,7 @@ async function assertSenderIsReceiptPayee(api, sender, relationshipId, delegatio
5272
6072
  }
5273
6073
  async function assertSenderIsReceiptCaller(api, sender, relationshipId, delegationId) {
5274
6074
  const signer = makeSigner(sender);
5275
- const rows = await fetchAllPages(
5276
- (after) => api.listDelegations(relationshipId, signer, { limit: 100, ...after ? { after } : {} })
5277
- );
6075
+ const rows = await fetchAllPages((after) => api.listDelegations(relationshipId, signer, { limit: 100, ...after ? { after } : {} }));
5278
6076
  const delegation = rows.find((d) => d.delegationId === delegationId);
5279
6077
  if (!delegation) {
5280
6078
  return;
@@ -5311,7 +6109,9 @@ async function resolveAutoRelId(api, sender, delegationId) {
5311
6109
  }
5312
6110
  async function resolveAutoRequestId(api, sender, relationshipId, delegationId) {
5313
6111
  const signer = makeSigner(sender);
5314
- const logs = await fetchAllPages((after) => api.listWorkLogs(relationshipId, signer, { delegationId, state: "responded", limit: 100, ...after ? { after } : {} }));
6112
+ const logs = await fetchAllPages(
6113
+ (after) => api.listWorkLogs(relationshipId, signer, { delegationId, state: "responded", limit: 100, ...after ? { after } : {} })
6114
+ );
5315
6115
  if (logs.length === 0) {
5316
6116
  throw new Error(
5317
6117
  `receipt propose --auto-hashes: no 'responded' work_log under delegation ${delegationId} in relationship ${relationshipId} \u2014 the payee must send \`work respond\` before a receipt can be proposed. Pass --request-id explicitly if you have a known one.`
@@ -5366,25 +6166,43 @@ function registerCosign(parent) {
5366
6166
  ).option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).option(
5367
6167
  "--settlement-purpose <s>",
5368
6168
  "Settlement digest purpose: `ARP-SOLANA-RELEASE-v1.5` (full) or `ARP-SOLANA-PARTIAL-RELEASE-v1.5` (partial; requires --settlement-payee-amount)."
5369
- ).option("--settlement-expires-at <unix>", "Settlement expires_at (unix seconds) \u2014 must match the value signed by BOTH parties when they ran `heyarp wallet sign-settlement-release`.").option("--payer-settlement-pubkey <b58>", "Payer settlement pubkey (base58) \u2014 must equal `lock.payer`. Mutually exclusive with --payer-sig-from-file.").option("--payer-settlement-sig <base64>", "Payer Ed25519 settlement signature (raw base64, NO `ed25519:` prefix). Mutually exclusive with --payer-sig-from-file.").option("--payee-settlement-pubkey <b58>", "Payee settlement pubkey (base58) \u2014 must equal `lock.payee`. Mutually exclusive with --payee-sig-from-file.").option("--payee-settlement-sig <base64>", "Payee Ed25519 settlement signature (raw base64, NO `ed25519:` prefix). Mutually exclusive with --payee-sig-from-file.").option(
6169
+ ).option(
6170
+ "--settlement-expires-at <unix>",
6171
+ "Settlement expires_at (unix seconds) \u2014 must match the value signed by BOTH parties when they ran `heyarp wallet sign-settlement-release`."
6172
+ ).option("--payer-settlement-pubkey <b58>", "Payer settlement pubkey (base58) \u2014 must equal `lock.payer`. Mutually exclusive with --payer-sig-from-file.").option("--payer-settlement-sig <base64>", "Payer Ed25519 settlement signature (raw base64, NO `ed25519:` prefix). Mutually exclusive with --payer-sig-from-file.").option("--payee-settlement-pubkey <b58>", "Payee settlement pubkey (base58) \u2014 must equal `lock.payee`. Mutually exclusive with --payee-sig-from-file.").option("--payee-settlement-sig <base64>", "Payee Ed25519 settlement signature (raw base64, NO `ed25519:` prefix). Mutually exclusive with --payee-sig-from-file.").option(
6173
+ "--auto-resolve-payee-sig",
6174
+ "Read the payee's settlement signature from `receipt.payeeSettlement` (populated by the worker's `heyarp receipt send-payee-sig` envelope) instead of requiring --payee-settlement-pubkey + --payee-settlement-sig + --settlement-purpose + --settlement-expires-at + --settlement-payee-amount flags. Closes the cross-host-coordination gap that earlier flows worked around via shared /tmp/*.json files. Mutually exclusive with --payee-sig-from-file and the explicit --payee-settlement-* flags. Buyer still supplies the PAYER side themselves (--payer-sig-from-file or --payer-settlement-* flags).",
6175
+ false
6176
+ ).option(
5370
6177
  "--payer-sig-from-file <path>",
5371
6178
  "Read `{settlement_pubkey, sig}` for the PAYER side from the JSON file produced by `heyarp wallet sign-settlement-release --write-to <path>`. Populates --payer-settlement-pubkey + --payer-settlement-sig."
5372
6179
  ).option(
5373
6180
  "--payee-sig-from-file <path>",
5374
6181
  "Read `{settlement_pubkey, sig}` for the PAYEE side from the JSON file produced by `heyarp wallet sign-settlement-release --write-to <path>`. Populates --payee-settlement-pubkey + --payee-settlement-sig."
5375
- ).option("--settlement-payee-amount <int>", "Required ONLY when --settlement-purpose=ARP-SOLANA-PARTIAL-RELEASE-v1.5. Payee receives this many base units; payer is refunded `lock.amount - payee_amount`.").option("--no-settlement", "Opt out of settlement_signatures attachment. Default behavior REQUIRES the full settlement flag set; without --no-settlement, omitting flags errors loud BEFORE the envelope is sent. Use --no-settlement only against a server running in test_mode.").action(async (relationshipId, delegationId, requestHashArg, responseHashArg, opts, cmd) => {
5376
- try {
5377
- await runCosign(relationshipId, delegationId, requestHashArg, responseHashArg, opts);
5378
- } catch (err) {
5379
- console.error(formatActionError(err, cmd));
5380
- process.exitCode = 1;
6182
+ ).option(
6183
+ "--settlement-payee-amount <int>",
6184
+ "Required ONLY when --settlement-purpose=ARP-SOLANA-PARTIAL-RELEASE-v1.5. Payee receives this many base units; payer is refunded `lock.amount - payee_amount`."
6185
+ ).option(
6186
+ "--no-settlement",
6187
+ "Opt out of settlement_signatures attachment. Default behavior REQUIRES the full settlement flag set; without --no-settlement, omitting flags errors loud BEFORE the envelope is sent. Use --no-settlement only against a server running in test_mode."
6188
+ ).option(
6189
+ "--wait-until <phase>",
6190
+ "Block after cosign-delivery until the named FSM phase is reached (e.g. cycle.released \u2014 strongest, waits for on-chain settlement; cycle.complete \u2014 weaker, waits only for the cosigned-receipt row). One of UNTIL_PHASES from `heyarp status --help`. Exit 124 on --wait-timeout."
6191
+ ).option("--wait-timeout <seconds>", "When --wait-until is set: max wall-clock wait (default 300). Exit code 124 on timeout.").option("--wait-interval <seconds>", "When --wait-until is set: poll cadence (default 3, bound [1, 60]).").option("--wait-verbose", "When --wait-until is set: emit one dim line per poll tick.", false).action(
6192
+ async (relationshipId, delegationId, requestHashArg, responseHashArg, opts, cmd) => {
6193
+ try {
6194
+ await runCosign(relationshipId, delegationId, requestHashArg, responseHashArg, opts);
6195
+ } catch (err) {
6196
+ emitActionError(err, cmd);
6197
+ process.exitCode = 1;
6198
+ }
5381
6199
  }
5382
- });
6200
+ );
5383
6201
  }
5384
6202
  function loadSettlementSigFromFile(path, flagPrefix) {
5385
6203
  let raw;
5386
6204
  try {
5387
- raw = (0, import_node_fs7.readFileSync)(path, "utf8");
6205
+ raw = (0, import_node_fs8.readFileSync)(path, "utf8");
5388
6206
  } catch (err) {
5389
6207
  throw new Error(`receipt cosign: failed to read ${flagPrefix} '${path}': ${err.message}`);
5390
6208
  }
@@ -5403,7 +6221,9 @@ function loadSettlementSigFromFile(path, flagPrefix) {
5403
6221
  const digestHex = p.digest_hex;
5404
6222
  const purpose = p.purpose;
5405
6223
  if (typeof pubkey !== "string" || pubkey.length === 0) {
5406
- throw new Error(`receipt cosign: ${flagPrefix} '${path}' missing required field 'settlement_pubkey' (string). Expected JSON output of \`heyarp wallet sign-settlement-release\`.`);
6224
+ throw new Error(
6225
+ `receipt cosign: ${flagPrefix} '${path}' missing required field 'settlement_pubkey' (string). Expected JSON output of \`heyarp wallet sign-settlement-release\`.`
6226
+ );
5407
6227
  }
5408
6228
  if (typeof sig !== "string" || sig.length === 0) {
5409
6229
  throw new Error(`receipt cosign: ${flagPrefix} '${path}' missing required field 'sig' (string). Expected JSON output of \`heyarp wallet sign-settlement-release\`.`);
@@ -5416,6 +6236,43 @@ function loadSettlementSigFromFile(path, flagPrefix) {
5416
6236
  }
5417
6237
  return { settlement_pubkey: pubkey, sig, digest_hex: digestHex, purpose };
5418
6238
  }
6239
+ function applyAutoResolvePayeeSig(cmdName, opts, payeeSettlement) {
6240
+ if (opts.payeeSigFromFile !== void 0 && opts.payeeSigFromFile !== "") {
6241
+ throw new Error(`${cmdName}: --auto-resolve-payee-sig is mutually exclusive with --payee-sig-from-file. Pass either the auto-resolve flag OR the file path, not both.`);
6242
+ }
6243
+ if (opts.payeeSettlementPubkey !== void 0 && opts.payeeSettlementPubkey !== "" || opts.payeeSettlementSig !== void 0 && opts.payeeSettlementSig !== "") {
6244
+ throw new Error(
6245
+ `${cmdName}: --auto-resolve-payee-sig is mutually exclusive with --payee-settlement-pubkey / --payee-settlement-sig. Pass either the auto-resolve flag OR the explicit values, not both.`
6246
+ );
6247
+ }
6248
+ if (!payeeSettlement) {
6249
+ throw new Error(
6250
+ `${cmdName}: --auto-resolve-payee-sig requires receipt.payeeSettlement to be populated, but the field is unset. The worker hasn't sent its 'heyarp receipt send-payee-sig' envelope yet \u2014 wait for it OR fall back to --payee-sig-from-file / --payee-settlement-* flags for out-of-band coordination.`
6251
+ );
6252
+ }
6253
+ opts.payeeSettlementPubkey = payeeSettlement.settlement_pubkey;
6254
+ opts.payeeSettlementSig = payeeSettlement.sig;
6255
+ if (opts.settlementPurpose !== void 0 && opts.settlementPurpose !== "" && opts.settlementPurpose !== payeeSettlement.purpose) {
6256
+ throw new Error(
6257
+ `${cmdName}: --settlement-purpose='${opts.settlementPurpose}' disagrees with the auto-resolved purpose from receipt.payeeSettlement ('${payeeSettlement.purpose}'). Drop --settlement-purpose to use the auto-resolved value, or drop --auto-resolve-payee-sig to use the manual flag.`
6258
+ );
6259
+ }
6260
+ opts.settlementPurpose = payeeSettlement.purpose;
6261
+ if (opts.settlementExpiresAt !== void 0 && opts.settlementExpiresAt !== "" && opts.settlementExpiresAt !== String(payeeSettlement.expires_at)) {
6262
+ throw new Error(
6263
+ `${cmdName}: --settlement-expires-at='${opts.settlementExpiresAt}' disagrees with the auto-resolved expires_at from receipt.payeeSettlement (${payeeSettlement.expires_at}). Drop --settlement-expires-at to use the auto-resolved value, or drop --auto-resolve-payee-sig.`
6264
+ );
6265
+ }
6266
+ opts.settlementExpiresAt = String(payeeSettlement.expires_at);
6267
+ if (payeeSettlement.payee_amount !== void 0) {
6268
+ if (opts.settlementPayeeAmount !== void 0 && opts.settlementPayeeAmount !== "" && opts.settlementPayeeAmount !== payeeSettlement.payee_amount) {
6269
+ throw new Error(
6270
+ `${cmdName}: --settlement-payee-amount='${opts.settlementPayeeAmount}' disagrees with the auto-resolved payee_amount from receipt.payeeSettlement ('${payeeSettlement.payee_amount}'). Drop --settlement-payee-amount to use the auto-resolved value, or drop --auto-resolve-payee-sig.`
6271
+ );
6272
+ }
6273
+ opts.settlementPayeeAmount = payeeSettlement.payee_amount;
6274
+ }
6275
+ }
5419
6276
  function assembleSettlementSignaturesAttachment(opts) {
5420
6277
  let payerPubkey = opts.payerSettlementPubkey;
5421
6278
  let payerSig = opts.payerSettlementSig;
@@ -5463,14 +6320,7 @@ function assembleSettlementSignaturesAttachment(opts) {
5463
6320
  `receipt cosign: --settlement-purpose='${opts.settlementPurpose}' disagrees with the purpose signed in the sig file ('${filePurpose}'). Either drop --settlement-purpose (the file's value will be used) or re-sign with the desired purpose.`
5464
6321
  );
5465
6322
  }
5466
- const settlementFlags = [
5467
- opts.settlementPurpose,
5468
- opts.settlementExpiresAt,
5469
- payerPubkey,
5470
- payerSig,
5471
- payeePubkey,
5472
- payeeSig
5473
- ];
6323
+ const settlementFlags = [opts.settlementPurpose, opts.settlementExpiresAt, payerPubkey, payerSig, payeePubkey, payeeSig];
5474
6324
  const someSet = settlementFlags.some((f) => f !== void 0 && f !== "");
5475
6325
  const allSet = settlementFlags.every((f) => f !== void 0 && f !== "");
5476
6326
  if (!someSet) {
@@ -5489,9 +6339,7 @@ function assembleSettlementSignaturesAttachment(opts) {
5489
6339
  }
5490
6340
  const purpose = opts.settlementPurpose;
5491
6341
  if (purpose !== "ARP-SOLANA-RELEASE-v1.5" && purpose !== "ARP-SOLANA-PARTIAL-RELEASE-v1.5") {
5492
- throw new Error(
5493
- `receipt cosign: --settlement-purpose must be 'ARP-SOLANA-RELEASE-v1.5' or 'ARP-SOLANA-PARTIAL-RELEASE-v1.5', got '${purpose}'.`
5494
- );
6342
+ throw new Error(`receipt cosign: --settlement-purpose must be 'ARP-SOLANA-RELEASE-v1.5' or 'ARP-SOLANA-PARTIAL-RELEASE-v1.5', got '${purpose}'.`);
5495
6343
  }
5496
6344
  const isPartial = purpose === "ARP-SOLANA-PARTIAL-RELEASE-v1.5";
5497
6345
  if (isPartial && (opts.settlementPayeeAmount === void 0 || opts.settlementPayeeAmount === "")) {
@@ -5601,22 +6449,28 @@ async function runCosign(relationshipId, delegationId, requestHashArg, responseH
5601
6449
  content.notes_hash = cosignNotesHash;
5602
6450
  }
5603
6451
  const body = { type: "receipt", content };
5604
- console.log(import_chalk20.default.dim(`Server: ${api.serverUrl}`));
5605
- console.log(import_chalk20.default.dim(`Sender (caller): ${sender.did}`));
5606
- console.log(import_chalk20.default.dim(`Recipient (payee): ${resolved.payeeDid}`));
5607
- console.log(import_chalk20.default.dim(`Delegation: ${delegationId}`));
5608
- console.log(import_chalk20.default.dim(`Verdict (final): ${verdict}`));
5609
- console.log(import_chalk20.default.dim(`Receipt event hash bound: ${resolved.receiptEventHash}`));
6452
+ console.log(import_chalk21.default.dim(`Server: ${api.serverUrl}`));
6453
+ console.log(import_chalk21.default.dim(`Sender (caller): ${sender.did}`));
6454
+ console.log(import_chalk21.default.dim(`Recipient (payee): ${resolved.payeeDid}`));
6455
+ console.log(import_chalk21.default.dim(`Delegation: ${delegationId}`));
6456
+ console.log(import_chalk21.default.dim(`Verdict (final): ${verdict}`));
6457
+ console.log(import_chalk21.default.dim(`Receipt event hash bound: ${resolved.receiptEventHash}`));
5610
6458
  if (cosignNotesHash !== null) {
5611
- console.log(import_chalk20.default.dim(`Notes hash bound: ${cosignNotesHash}`));
6459
+ console.log(import_chalk21.default.dim(`Notes hash bound: ${cosignNotesHash}`));
5612
6460
  } else if (opts.clearNotes) {
5613
- console.log(import_chalk20.default.dim("Notes binding: cleared (--clear-notes)"));
6461
+ console.log(import_chalk21.default.dim("Notes binding: cleared (--clear-notes)"));
6462
+ }
6463
+ if (opts.autoResolvePayeeSig) {
6464
+ applyAutoResolvePayeeSig("receipt cosign", opts, resolved.payeeSettlement);
6465
+ console.log(
6466
+ import_chalk21.default.dim(`Auto-resolved payee sig from receipt.payeeSettlement (purpose=${resolved.payeeSettlement?.purpose}, expires_at=${resolved.payeeSettlement?.expires_at})`)
6467
+ );
5614
6468
  }
5615
6469
  const settlementSigs = assembleSettlementSignaturesAttachment(opts);
5616
6470
  const attachments = { co_signature: cosignature };
5617
6471
  if (settlementSigs) {
5618
6472
  attachments.settlement_signatures = settlementSigs;
5619
- console.log(import_chalk20.default.dim(`Settlement signatures attached: purpose=${settlementSigs.purpose}`));
6473
+ console.log(import_chalk21.default.dim(`Settlement signatures attached: purpose=${settlementSigs.purpose}`));
5620
6474
  }
5621
6475
  const result = await sendReceiptEnvelope({
5622
6476
  api,
@@ -5629,6 +6483,147 @@ async function runCosign(relationshipId, delegationId, requestHashArg, responseH
5629
6483
  server: opts.server
5630
6484
  });
5631
6485
  printIngestResult3(result);
6486
+ if (opts.waitUntil) {
6487
+ const untilPhase = parseUntilPhase(opts.waitUntil);
6488
+ if (untilPhase === void 0) {
6489
+ throw new Error(`receipt cosign: --wait-until requires a phase value (got ${JSON.stringify(opts.waitUntil)})`);
6490
+ }
6491
+ await awaitFsmTransitionAfterAction({
6492
+ api,
6493
+ signerDid: sender.did,
6494
+ signer: makeSigner(sender),
6495
+ relationshipId: result.relationshipId,
6496
+ untilPhase,
6497
+ waitIntervalSec: parseWaitInterval(opts.waitInterval),
6498
+ waitTimeoutSec: parseWaitTimeout(opts.waitTimeout),
6499
+ waitVerbose: !!opts.waitVerbose,
6500
+ json: false
6501
+ });
6502
+ }
6503
+ }
6504
+ function registerSendPayeeSig(parent) {
6505
+ parent.command("send-payee-sig").description("Send the payee's settlement signature to the buyer via a settlement_signature envelope. Replaces /tmp/*.json coordination for cross-host escrow cycles.").argument("<recipient-did>", "Buyer DID (= caller / offerer of the parent delegation; the cycle sends the sig to the side that will cosign)").requiredOption("--delegation-id <id>", "Parent delegation UUID \u2014 must match what was passed to `heyarp wallet sign-settlement-release`.").requiredOption(
6506
+ "--receipt-event-hash <sha256:hex>",
6507
+ "Server-assigned `serverEventHash` of the receipt-propose envelope this signature settles. Read it from the JSON response of `heyarp receipt propose` (the field is `Server event hash` in human output, or `.serverEventHash` in --json). MUST equal the value the digest was signed over \u2014 the server cross-checks against `Receipt.receiptEventHash` and rejects with SETTLEMENT_SIG_RECEIPT_NOT_FOUND otherwise."
6508
+ ).requiredOption(
6509
+ "--sig-from-file <path>",
6510
+ "JSON file produced by `heyarp wallet sign-settlement-release --write-to <path>`. Reads `{settlement_pubkey, sig, purpose, digest_hex}` \u2014 the digest_hex is bound through for cross-file consistency (catches truncated or hand-edited files)."
6511
+ ).requiredOption(
6512
+ "--expires-at <unix>",
6513
+ "Unix-seconds expires_at value baked into the signed digest \u2014 MUST match the value passed to `--expires-at` on `heyarp wallet sign-settlement-release`. Server cross-checks against the buyer-side value at cosign time; wrong value \u2192 ESC_SETTLEMENT_EXPIRES_AT_PAST/_TOO_SOON."
6514
+ ).option(
6515
+ "--payee-amount <int>",
6516
+ "Base-unit decimal-integer payee_amount \u2014 REQUIRED when the sig file's purpose is `ARP-SOLANA-PARTIAL-RELEASE-v1.5` (usage_based contracts). Same value passed to `--partial-payee-amount` on sign-settlement-release. FORBIDDEN for `ARP-SOLANA-RELEASE-v1.5` (full release settles the whole lock)."
6517
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server (= payee)").option("--ttl <seconds>", "Envelope TTL in seconds", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (recipientDid, opts, cmd) => {
6518
+ try {
6519
+ await runSendPayeeSig(recipientDid, opts);
6520
+ } catch (err) {
6521
+ emitActionError(err, cmd);
6522
+ process.exitCode = 1;
6523
+ }
6524
+ });
6525
+ }
6526
+ async function runSendPayeeSig(recipientDid, opts) {
6527
+ const cmdName = "receipt send-payee-sig";
6528
+ requireDid3(cmdName, recipientDid, "<recipient-did>");
6529
+ const delegationId = requireUuidNormalised2(cmdName, opts.delegationId, "--delegation-id");
6530
+ requireSha256(cmdName, opts.receiptEventHash, "--receipt-event-hash");
6531
+ const expiresAtSeconds = parseInteger(cmdName, "--expires-at", opts.expiresAt);
6532
+ if (expiresAtSeconds <= 0) {
6533
+ throw new Error(`${cmdName}: --expires-at must be a positive unix-seconds integer (got ${opts.expiresAt})`);
6534
+ }
6535
+ const ttlSeconds = parseTtl4(cmdName, opts.ttl);
6536
+ const sigFile = loadSettlementSigFromFile(opts.sigFromFile, "--sig-from-file");
6537
+ const isPartial = sigFile.purpose === "ARP-SOLANA-PARTIAL-RELEASE-v1.5";
6538
+ const isFull = sigFile.purpose === "ARP-SOLANA-RELEASE-v1.5";
6539
+ if (!isPartial && !isFull) {
6540
+ throw new Error(
6541
+ `${cmdName}: sig file at '${opts.sigFromFile}' has purpose='${sigFile.purpose}' but only ARP-SOLANA-RELEASE-v1.5 and ARP-SOLANA-PARTIAL-RELEASE-v1.5 are valid on a settlement_signature envelope. (REFUND-v1 sigs ride other paths \u2014 they don't bind to receipt_event_hash, so this envelope isn't the right channel for them.)`
6542
+ );
6543
+ }
6544
+ if (isPartial && (opts.payeeAmount === void 0 || opts.payeeAmount === "")) {
6545
+ throw new Error(
6546
+ `${cmdName}: --payee-amount is REQUIRED when the sig file's purpose is ARP-SOLANA-PARTIAL-RELEASE-v1.5 (the server reconstructs the same digest at cosign and binds payee_amount into it; missing here = unverifiable digest).`
6547
+ );
6548
+ }
6549
+ if (isFull && opts.payeeAmount !== void 0 && opts.payeeAmount !== "") {
6550
+ throw new Error(
6551
+ `${cmdName}: --payee-amount must be OMITTED when the sig file's purpose is ARP-SOLANA-RELEASE-v1.5 (full release settles the whole lock; supplying an amount alongside the RELEASE purpose is almost certainly a wrong-purpose-flag bug).`
6552
+ );
6553
+ }
6554
+ if (opts.payeeAmount !== void 0 && opts.payeeAmount !== "" && !/^[0-9]+$/.test(opts.payeeAmount)) {
6555
+ throw new Error(
6556
+ `${cmdName}: --payee-amount must be a decimal-integer base-unit string (got '${opts.payeeAmount}'); hex/empty/signed/decimal forms are rejected \u2014 same invariant as receipt.usage.computed_amount.`
6557
+ );
6558
+ }
6559
+ const sender = resolveSenderAgent(cmdName, opts.server, opts.fromDid);
6560
+ const api = new ArpApiClient(opts.server);
6561
+ const content = {
6562
+ delegation_id: delegationId,
6563
+ receipt_event_hash: opts.receiptEventHash,
6564
+ purpose: sigFile.purpose,
6565
+ payee_settlement_pubkey: sigFile.settlement_pubkey,
6566
+ sig: sigFile.sig,
6567
+ expires_at: expiresAtSeconds,
6568
+ ...isPartial ? { payee_amount: opts.payeeAmount } : {}
6569
+ };
6570
+ console.log(import_chalk21.default.dim(`Server: ${api.serverUrl}`));
6571
+ console.log(import_chalk21.default.dim(`Sender (payee): ${sender.did}`));
6572
+ console.log(import_chalk21.default.dim(`Recipient (buyer): ${recipientDid}`));
6573
+ console.log(import_chalk21.default.dim(`Delegation: ${delegationId}`));
6574
+ console.log(import_chalk21.default.dim(`Receipt event hash: ${opts.receiptEventHash}`));
6575
+ console.log(import_chalk21.default.dim(`Purpose: ${sigFile.purpose}`));
6576
+ if (isPartial) {
6577
+ console.log(import_chalk21.default.dim(`Payee amount: ${opts.payeeAmount}`));
6578
+ }
6579
+ console.log(import_chalk21.default.dim(`Settlement pubkey: ${sigFile.settlement_pubkey}`));
6580
+ console.log(import_chalk21.default.dim(`Expires at: ${expiresAtSeconds}`));
6581
+ const result = await sendSettlementSignatureEnvelope({
6582
+ api,
6583
+ sender,
6584
+ recipientDid,
6585
+ content,
6586
+ ttlSeconds,
6587
+ verbose: opts.verbose,
6588
+ server: opts.server
6589
+ });
6590
+ printIngestResult3(result);
6591
+ }
6592
+ async function sendSettlementSignatureEnvelope(args) {
6593
+ const nextSequence = (args.sender.lastSenderSequence ?? 0) + 1;
6594
+ const protectedBlock = {
6595
+ protocol_version: "arp/0.1",
6596
+ purpose: import_sdk11.Purpose.ENVELOPE,
6597
+ message_id: (0, import_sdk11.uuidV4)(),
6598
+ sender_did: args.sender.did,
6599
+ recipient_did: args.recipientDid,
6600
+ relationship_id: null,
6601
+ sender_sequence: nextSequence,
6602
+ sender_nonce: (0, import_sdk11.senderNonce)(),
6603
+ timestamp: (0, import_sdk11.rfc3339)(),
6604
+ expires_at: (0, import_sdk11.expiresAt)(args.ttlSeconds),
6605
+ delivery_id: null
6606
+ };
6607
+ const signer = makeSigner(args.sender);
6608
+ const envelope = (0, import_sdk11.signEnvelope)({
6609
+ protected: protectedBlock,
6610
+ body: { type: "settlement_signature", content: args.content },
6611
+ identitySecretKey: signer.identitySecretKey
6612
+ });
6613
+ if (args.verbose) {
6614
+ console.log(import_chalk21.default.bold("\nEnvelope (pre-send):"));
6615
+ console.log(formatJson(envelope));
6616
+ }
6617
+ try {
6618
+ const result = await args.api.ingest(envelope);
6619
+ updateAgentLocal(args.server, args.sender.did, { lastSenderSequence: nextSequence });
6620
+ return result;
6621
+ } catch (err) {
6622
+ if (err instanceof ApiError && POST_COMMIT_ERROR_CODES4.has(err.payload.code)) {
6623
+ updateAgentLocal(args.server, args.sender.did, { lastSenderSequence: nextSequence });
6624
+ }
6625
+ throw err;
6626
+ }
5632
6627
  }
5633
6628
  async function sendReceiptEnvelope(args) {
5634
6629
  const nextSequence = (args.sender.lastSenderSequence ?? 0) + 1;
@@ -5653,7 +6648,7 @@ async function sendReceiptEnvelope(args) {
5653
6648
  attachments: args.attachments
5654
6649
  });
5655
6650
  if (args.verbose) {
5656
- console.log(import_chalk20.default.bold("\nEnvelope (pre-send):"));
6651
+ console.log(import_chalk21.default.bold("\nEnvelope (pre-send):"));
5657
6652
  console.log(formatJson(envelope));
5658
6653
  }
5659
6654
  try {
@@ -5703,15 +6698,21 @@ async function resolveCosignTargets(cmdName, api, signer, args) {
5703
6698
  if (row.callerDid !== args.selfDid) {
5704
6699
  throw new Error(`${cmdName}: this receipt's caller is ${row.callerDid}; only the caller can cosign. Switch with --from-did.`);
5705
6700
  }
5706
- return { payeeDid: row.payeeDid, receiptEventHash: row.receiptEventHash, verdictProposed: row.verdictProposed, notesHash: row.notesHash };
6701
+ return {
6702
+ payeeDid: row.payeeDid,
6703
+ receiptEventHash: row.receiptEventHash,
6704
+ verdictProposed: row.verdictProposed,
6705
+ notesHash: row.notesHash,
6706
+ payeeSettlement: row.payeeSettlement
6707
+ };
5707
6708
  }
5708
6709
  function printIngestResult3(result) {
5709
- console.log(import_chalk20.default.green("\nDelivered."));
5710
- console.log(`${import_chalk20.default.bold("Event id")}: ${import_chalk20.default.cyan(result.eventId)}`);
5711
- console.log(`${import_chalk20.default.bold("Relationship id")}: ${import_chalk20.default.cyan(result.relationshipId)}`);
5712
- console.log(`${import_chalk20.default.bold("Chain index")}: ${import_chalk20.default.cyan(String(result.relationshipEventIndex))}`);
5713
- console.log(`${import_chalk20.default.bold("Server timestamp")}: ${import_chalk20.default.cyan(result.serverTimestamp)}`);
5714
- console.log(`${import_chalk20.default.bold("Server event hash")}: ${import_chalk20.default.cyan(result.serverEventHash)}`);
6710
+ console.log(import_chalk21.default.green("\nDelivered."));
6711
+ console.log(`${import_chalk21.default.bold("Event id")}: ${import_chalk21.default.cyan(result.eventId)}`);
6712
+ console.log(`${import_chalk21.default.bold("Relationship id")}: ${import_chalk21.default.cyan(result.relationshipId)}`);
6713
+ console.log(`${import_chalk21.default.bold("Chain index")}: ${import_chalk21.default.cyan(String(result.relationshipEventIndex))}`);
6714
+ console.log(`${import_chalk21.default.bold("Server timestamp")}: ${import_chalk21.default.cyan(result.serverTimestamp)}`);
6715
+ console.log(`${import_chalk21.default.bold("Server event hash")}: ${import_chalk21.default.cyan(result.serverEventHash)}`);
5715
6716
  }
5716
6717
  function parseVerdict(cmdName, raw) {
5717
6718
  if (raw === void 0 || raw === "") return "accepted";
@@ -5764,7 +6765,7 @@ function requireDid3(cmdName, did, label) {
5764
6765
  }
5765
6766
 
5766
6767
  // src/commands/receipts.ts
5767
- var import_chalk21 = __toESM(require("chalk"));
6768
+ var import_chalk22 = __toESM(require("chalk"));
5768
6769
  init_api();
5769
6770
  var ALLOWED_STATES3 = /* @__PURE__ */ new Set(["proposed", "cosigned"]);
5770
6771
  function registerReceiptsCommand(root) {
@@ -5793,9 +6794,9 @@ async function runReceipts(relationshipId, opts) {
5793
6794
  const api = new ArpApiClient(opts.server);
5794
6795
  const sender = resolveSenderAgent("receipts", opts.server, opts.fromDid);
5795
6796
  if (!opts.json) {
5796
- console.log(import_chalk21.default.dim(`Server: ${api.serverUrl}`));
5797
- console.log(import_chalk21.default.dim(`Signer: ${sender.did}`));
5798
- console.log(import_chalk21.default.dim(`Relationship: ${relationshipId}`));
6797
+ console.log(import_chalk22.default.dim(`Server: ${api.serverUrl}`));
6798
+ console.log(import_chalk22.default.dim(`Signer: ${sender.did}`));
6799
+ console.log(import_chalk22.default.dim(`Relationship: ${relationshipId}`));
5799
6800
  }
5800
6801
  const query = { limit };
5801
6802
  if (state) query.state = state;
@@ -5808,7 +6809,7 @@ async function runReceipts(relationshipId, opts) {
5808
6809
  return;
5809
6810
  }
5810
6811
  if (rows.length === 0) {
5811
- console.log(import_chalk21.default.dim("\n(no receipts for this relationship)"));
6812
+ console.log(import_chalk22.default.dim("\n(no receipts for this relationship)"));
5812
6813
  return;
5813
6814
  }
5814
6815
  console.log("");
@@ -5826,28 +6827,28 @@ async function runReceipts(relationshipId, opts) {
5826
6827
  }));
5827
6828
  }
5828
6829
  const lastId = rows[rows.length - 1].id;
5829
- console.log(import_chalk21.default.dim(`
6830
+ console.log(import_chalk22.default.dim(`
5830
6831
  ${rows.length} receipt row(s). Paginate with --after ${lastId}.`));
5831
6832
  }
5832
6833
  function formatReceiptLine(r, selfDid, opts = {}) {
5833
6834
  const delegationPart = opts.fullIds ? r.delegationId : idHead3(r.delegationId);
5834
6835
  const requestHashPart = opts.fullIds ? r.requestHash : hashHead3(r.requestHash);
5835
- const id = import_chalk21.default.bold(`${delegationPart}/${requestHashPart}`);
6836
+ const id = import_chalk22.default.bold(`${delegationPart}/${requestHashPart}`);
5836
6837
  const state = colorState3(r.state).padEnd(stateColumnWidth3());
5837
6838
  const callerHead = opts.fullIds ? r.callerDid : didHead4(r.callerDid);
5838
6839
  const payeeHead = opts.fullIds ? r.payeeDid : didHead4(r.payeeDid);
5839
- const direction = r.payeeDid === selfDid ? `${import_chalk21.default.bold("me")}(payee) \u2192 ${import_chalk21.default.dim(callerHead)}` : `${import_chalk21.default.dim(payeeHead)}(payee) \u2192 ${import_chalk21.default.bold("me")}`;
6840
+ const direction = r.payeeDid === selfDid ? `${import_chalk22.default.bold("me")}(payee) \u2192 ${import_chalk22.default.dim(callerHead)}` : `${import_chalk22.default.dim(payeeHead)}(payee) \u2192 ${import_chalk22.default.bold("me")}`;
5840
6841
  const verdict = formatVerdict(r);
5841
6842
  const responseTail = opts.fullIds ? `
5842
- ${import_chalk21.default.dim("responseHash:")} ${import_chalk21.default.cyan(r.responseHash)}` : "";
6843
+ ${import_chalk22.default.dim("responseHash:")} ${import_chalk22.default.cyan(r.responseHash)}` : "";
5843
6844
  return `${id} ${state} ${direction} ${verdict}${responseTail}`;
5844
6845
  }
5845
6846
  function colorState3(s) {
5846
6847
  switch (s) {
5847
6848
  case "proposed":
5848
- return import_chalk21.default.yellow("proposed");
6849
+ return import_chalk22.default.yellow("proposed");
5849
6850
  case "cosigned":
5850
- return import_chalk21.default.green("cosigned");
6851
+ return import_chalk22.default.green("cosigned");
5851
6852
  }
5852
6853
  }
5853
6854
  function stateColumnWidth3() {
@@ -5857,11 +6858,11 @@ function formatVerdict(r) {
5857
6858
  const final = r.verdictFinal ?? r.verdictProposed;
5858
6859
  switch (final) {
5859
6860
  case "accepted":
5860
- return import_chalk21.default.green("accepted");
6861
+ return import_chalk22.default.green("accepted");
5861
6862
  case "accepted_with_notes":
5862
- return import_chalk21.default.yellow("accepted_with_notes");
6863
+ return import_chalk22.default.yellow("accepted_with_notes");
5863
6864
  case "rejected":
5864
- return import_chalk21.default.red("rejected");
6865
+ return import_chalk22.default.red("rejected");
5865
6866
  }
5866
6867
  }
5867
6868
  function idHead3(id) {
@@ -5894,9 +6895,9 @@ function parseLimit7(raw) {
5894
6895
 
5895
6896
  // src/commands/register.ts
5896
6897
  var import_node_crypto = require("crypto");
5897
- var import_node_fs8 = require("fs");
6898
+ var import_node_fs9 = require("fs");
5898
6899
  var import_sdk12 = require("@heyanon-arp/sdk");
5899
- var import_chalk22 = __toESM(require("chalk"));
6900
+ var import_chalk23 = __toESM(require("chalk"));
5900
6901
  var import_prompts2 = __toESM(require("prompts"));
5901
6902
  init_api();
5902
6903
  init_paths();
@@ -5956,17 +6957,17 @@ async function runRegister(opts) {
5956
6957
  const publication = parsePublicationMode(opts.publication ?? "active");
5957
6958
  assertJsonRequiresYes(opts);
5958
6959
  const api = new ArpApiClient(opts.server);
5959
- if (!opts.json) console.log(import_chalk22.default.dim(`Server: ${api.serverUrl}`));
6960
+ if (!opts.json) console.log(import_chalk23.default.dim(`Server: ${api.serverUrl}`));
5960
6961
  if (!opts.json) {
5961
6962
  warnIfAgentsAlreadyRegistered(opts.server);
5962
6963
  warnIfOrphanHomesPresent();
5963
6964
  }
5964
6965
  const keys = opts.fromKeys ? loadKeysFromFile(opts.fromKeys) : freshKeys();
5965
6966
  const did = (0, import_sdk12.formatDid)(keys.identityPublicKey);
5966
- if (!opts.json) console.log(import_chalk22.default.dim(`DID will be: ${did}`));
6967
+ if (!opts.json) console.log(import_chalk23.default.dim(`DID will be: ${did}`));
5967
6968
  const answers = await mergeAnswers(opts);
5968
6969
  const scryptSalt = (0, import_node_crypto.randomBytes)(16);
5969
- if (!opts.json) console.log(import_chalk22.default.dim("Deriving scrypt key, this may take a moment..."));
6970
+ if (!opts.json) console.log(import_chalk23.default.dim("Deriving scrypt key, this may take a moment..."));
5970
6971
  const scryptKey = (0, import_sdk12.deriveScryptKey)(answers.password, new Uint8Array(scryptSalt));
5971
6972
  const challenge = await api.issueChallenge("register");
5972
6973
  const challengeBytes = base64UrlNoPadDecode(challenge.challengeB64);
@@ -6045,13 +7046,13 @@ async function runRegister(opts) {
6045
7046
  try {
6046
7047
  recordHome(arpHomeDir());
6047
7048
  } catch (registryErr) {
6048
- if (!opts.json) console.log(import_chalk22.default.dim(`(homes registry write failed: ${registryErr.message})`));
7049
+ if (!opts.json) console.log(import_chalk23.default.dim(`(homes registry write failed: ${registryErr.message})`));
6049
7050
  }
6050
7051
  if (!opts.json) {
6051
- console.log(import_chalk22.default.green("\nRegistered."));
6052
- console.log(`${import_chalk22.default.bold("DID")}: ${import_chalk22.default.cyan(result.did)}`);
6053
- console.log(`${import_chalk22.default.bold("Settlement pubkey")} ${import_chalk22.default.dim("(fund with SOL)")}: ${import_chalk22.default.cyan(settlementPublicKeyB58)}`);
6054
- console.log(import_chalk22.default.bold("DID document:"));
7052
+ console.log(import_chalk23.default.green("\nRegistered."));
7053
+ console.log(`${import_chalk23.default.bold("DID")}: ${import_chalk23.default.cyan(result.did)}`);
7054
+ console.log(`${import_chalk23.default.bold("Settlement pubkey")} ${import_chalk23.default.dim("(fund with SOL)")}: ${import_chalk23.default.cyan(settlementPublicKeyB58)}`);
7055
+ console.log(import_chalk23.default.bold("DID document:"));
6055
7056
  console.log(formatJson(result.didDocument));
6056
7057
  }
6057
7058
  const decision = decideAutoPublish({ publication, defaultEndpointUrl: answers.defaultEndpointUrl });
@@ -6061,30 +7062,33 @@ async function runRegister(opts) {
6061
7062
  const signer = makeSignerFromSecret(keys.identitySecretKey, result.did);
6062
7063
  await api.publishAgent(result.did, signer);
6063
7064
  publicationOutcome = "published";
6064
- if (!opts.json) console.log(import_chalk22.default.green(`
7065
+ if (!opts.json) console.log(import_chalk23.default.green(`
6065
7066
  \u2713 Published \u2014 discoverable now via \`heyarp agents\`.`));
6066
7067
  } catch (err) {
6067
7068
  publicationOutcome = `failed: ${err.message}`;
6068
- if (!opts.json) console.log(import_chalk22.default.yellow(`
6069
- \u26A0 Auto-publish failed (${err.message}). Agent saved as DRAFT \u2014 run \`heyarp publish ${result.did}\` to make it discoverable.`));
7069
+ if (!opts.json)
7070
+ console.log(
7071
+ import_chalk23.default.yellow(`
7072
+ \u26A0 Auto-publish failed (${err.message}). Agent saved as DRAFT \u2014 run \`heyarp publish ${result.did}\` to make it discoverable.`)
7073
+ );
6070
7074
  }
6071
7075
  } else if (decision === "skip-no-endpoint") {
6072
7076
  publicationOutcome = "skip-no-endpoint";
6073
7077
  if (!opts.json) {
6074
- console.log(import_chalk22.default.dim(`
7078
+ console.log(import_chalk23.default.dim(`
6075
7079
  Auto-publish skipped: no --endpoint-url supplied (buyer-only registration). Agent is saved as DRAFT.`));
6076
- console.log(import_chalk22.default.dim(`If this agent should also accept work, re-register with \`--endpoint-url <https://\u2026>\` OR run`));
6077
- console.log(import_chalk22.default.dim(`\`heyarp rotate ${result.did} --endpoint-url <https://\u2026>\` then \`heyarp publish ${result.did}\`.`));
7080
+ console.log(import_chalk23.default.dim(`If this agent should also accept work, re-register with \`--endpoint-url <https://\u2026>\` OR run`));
7081
+ console.log(import_chalk23.default.dim(`\`heyarp rotate ${result.did} --endpoint-url <https://\u2026>\` then \`heyarp publish ${result.did}\`.`));
6078
7082
  }
6079
7083
  } else {
6080
7084
  publicationOutcome = "draft";
6081
7085
  if (!opts.json) {
6082
- console.log(import_chalk22.default.dim(`
7086
+ console.log(import_chalk23.default.dim(`
6083
7087
  Publication mode: DRAFT. Agent is registered but NOT discoverable via \`heyarp agents\`.`));
6084
- console.log(import_chalk22.default.dim(`Run \`heyarp publish ${result.did}\` when ready to appear in the catalog.`));
7088
+ console.log(import_chalk23.default.dim(`Run \`heyarp publish ${result.did}\` when ready to appear in the catalog.`));
6085
7089
  }
6086
7090
  }
6087
- if (!opts.json) console.log(import_chalk22.default.dim(`
7091
+ if (!opts.json) console.log(import_chalk23.default.dim(`
6088
7092
  Local state saved to ${arpHomeDir()}/agents.json (mode 0600).`));
6089
7093
  if (opts.json) {
6090
7094
  console.log(
@@ -6169,7 +7173,7 @@ async function mergeAnswers(opts) {
6169
7173
  }
6170
7174
  const prompted = promptDefs.length > 0 ? await (0, import_prompts2.default)(promptDefs, {
6171
7175
  onCancel: () => {
6172
- console.log(import_chalk22.default.yellow("\nAborted."));
7176
+ console.log(import_chalk23.default.yellow("\nAborted."));
6173
7177
  process.exit(130);
6174
7178
  }
6175
7179
  }) : {};
@@ -6192,16 +7196,16 @@ function warnIfOrphanHomesPresent() {
6192
7196
  try {
6193
7197
  others = listHomes().filter((h) => h.path !== current);
6194
7198
  } catch (registryErr) {
6195
- console.log(import_chalk22.default.dim(`(homes registry unreadable, skipping orphan-home check: ${registryErr.message})`));
7199
+ console.log(import_chalk23.default.dim(`(homes registry unreadable, skipping orphan-home check: ${registryErr.message})`));
6196
7200
  return;
6197
7201
  }
6198
7202
  if (others.length === 0) return;
6199
- const list = others.map((h) => ` \u2022 ${import_chalk22.default.cyan(h.path)} ${import_chalk22.default.dim(`(last seen ${h.lastSeenAt})`)}`).join("\n");
6200
- console.log(import_chalk22.default.yellow(`
7203
+ const list = others.map((h) => ` \u2022 ${import_chalk23.default.cyan(h.path)} ${import_chalk23.default.dim(`(last seen ${h.lastSeenAt})`)}`).join("\n");
7204
+ console.log(import_chalk23.default.yellow(`
6201
7205
  \u26A0 HEYARP_HOME is unset, but other agent homes are registered on this machine:`));
6202
7206
  console.log(list);
6203
7207
  console.log(
6204
- import_chalk22.default.dim(
7208
+ import_chalk23.default.dim(
6205
7209
  ` Registering will create a NEW agent under ${current}.
6206
7210
  If you meant to add to an existing home, abort (Ctrl-C) and re-run with:
6207
7211
  HEYARP_HOME=<path> heyarp register \u2026
@@ -6214,11 +7218,11 @@ function warnIfAgentsAlreadyRegistered(serverOverride) {
6214
7218
  const targetServer = resolveServerUrl(serverOverride);
6215
7219
  const existing = listAgents().filter((row) => row.serverUrl === targetServer);
6216
7220
  if (existing.length === 0) return;
6217
- const list = existing.map((row) => ` \u2022 ${import_chalk22.default.cyan(row.agent.did)}${row.agent.name ? import_chalk22.default.dim(` (${row.agent.name})`) : ""}`).join("\n");
6218
- console.log(import_chalk22.default.yellow("\n\u26A0 ~/.arp/agents.json already has agent(s) for this server:"));
7221
+ const list = existing.map((row) => ` \u2022 ${import_chalk23.default.cyan(row.agent.did)}${row.agent.name ? import_chalk23.default.dim(` (${row.agent.name})`) : ""}`).join("\n");
7222
+ console.log(import_chalk23.default.yellow("\n\u26A0 ~/.arp/agents.json already has agent(s) for this server:"));
6219
7223
  console.log(list);
6220
7224
  console.log(
6221
- import_chalk22.default.dim(
7225
+ import_chalk23.default.dim(
6222
7226
  " After this register completes, you will have multiple local DIDs sharing one state file.\n To keep their state isolated, run with HEYARP_HOME pointing at a per-agent dir, e.g.\n HEYARP_HOME=/tmp/agent-alice heyarp register \u2026\n Otherwise, pass --from-did <did> explicitly on every signed command.\n"
6223
7227
  )
6224
7228
  );
@@ -6238,12 +7242,12 @@ function freshKeys() {
6238
7242
  };
6239
7243
  }
6240
7244
  function loadKeysFromFile(path) {
6241
- if (!(0, import_node_fs8.existsSync)(path)) {
7245
+ if (!(0, import_node_fs9.existsSync)(path)) {
6242
7246
  throw new Error(`--from-keys: file not found at ${path}`);
6243
7247
  }
6244
7248
  let parsed;
6245
7249
  try {
6246
- parsed = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
7250
+ parsed = JSON.parse((0, import_node_fs9.readFileSync)(path, "utf8"));
6247
7251
  } catch (err) {
6248
7252
  throw new Error(`--from-keys: ${path} is not valid JSON: ${err.message}`);
6249
7253
  }
@@ -6285,7 +7289,7 @@ function makeSignerFromSecret(identitySecretKey, did) {
6285
7289
  }
6286
7290
 
6287
7291
  // src/commands/relationships.ts
6288
- var import_chalk23 = __toESM(require("chalk"));
7292
+ var import_chalk24 = __toESM(require("chalk"));
6289
7293
  init_api();
6290
7294
  var ALLOWED_STATES4 = /* @__PURE__ */ new Set(["pending", "active", "paused", "closed"]);
6291
7295
  function registerRelationshipsCommand(root) {
@@ -6305,25 +7309,25 @@ async function runRelationships(positionalDid, opts) {
6305
7309
  const local = explicitDid !== void 0 ? loadAgentOrThrow(opts.server, explicitDid) : resolveSenderAgent("relationships", opts.server, void 0);
6306
7310
  const did = local.did;
6307
7311
  const api = new ArpApiClient(opts.server);
6308
- console.log(import_chalk23.default.dim(`Server: ${api.serverUrl}`));
6309
- console.log(import_chalk23.default.dim(`Signer: ${local.did}`));
7312
+ console.log(import_chalk24.default.dim(`Server: ${api.serverUrl}`));
7313
+ console.log(import_chalk24.default.dim(`Signer: ${local.did}`));
6310
7314
  const query = { limit };
6311
7315
  if (state) query.state = state;
6312
7316
  const signer = makeSigner(local);
6313
7317
  const rows = await api.listRelationships(did, signer, query);
6314
7318
  if (rows.length === 0) {
6315
- console.log(import_chalk23.default.dim("\n(no relationships)"));
7319
+ console.log(import_chalk24.default.dim("\n(no relationships)"));
6316
7320
  return;
6317
7321
  }
6318
7322
  console.log("");
6319
7323
  console.log(formatRelationshipsTable(rows, did));
6320
7324
  if (opts.verbose) {
6321
- console.log(import_chalk23.default.bold("\nFull relationships:"));
7325
+ console.log(import_chalk24.default.bold("\nFull relationships:"));
6322
7326
  for (const r of rows) {
6323
7327
  console.log(formatJson(r));
6324
7328
  }
6325
7329
  }
6326
- console.log(import_chalk23.default.dim(`
7330
+ console.log(import_chalk24.default.dim(`
6327
7331
  ${rows.length} relationship(s).`));
6328
7332
  }
6329
7333
  function formatRelationshipsTable(rows, selfDid) {
@@ -6331,7 +7335,7 @@ function formatRelationshipsTable(rows, selfDid) {
6331
7335
  const data = rows.map((r) => [r.relationshipId, otherPair(r, selfDid), r.state, r.lastEventAt ?? "(none)", String(r.lastEventIndex)]);
6332
7336
  const widths = header.map((h, i) => Math.max(h.length, ...data.map((row) => row[i].length)));
6333
7337
  const pad = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ");
6334
- return [import_chalk23.default.bold(pad(header)), import_chalk23.default.dim(pad(widths.map((w) => "-".repeat(w)))), ...data.map((row) => pad(row))].join("\n");
7338
+ return [import_chalk24.default.bold(pad(header)), import_chalk24.default.dim(pad(widths.map((w) => "-".repeat(w)))), ...data.map((row) => pad(row))].join("\n");
6335
7339
  }
6336
7340
  function otherPair(r, selfDid) {
6337
7341
  if (r.pairDidA === selfDid) return r.pairDidB;
@@ -6356,7 +7360,7 @@ function parseLimit8(raw) {
6356
7360
 
6357
7361
  // src/commands/rotate.ts
6358
7362
  var import_sdk13 = require("@heyanon-arp/sdk");
6359
- var import_chalk24 = __toESM(require("chalk"));
7363
+ var import_chalk25 = __toESM(require("chalk"));
6360
7364
  var import_prompts3 = __toESM(require("prompts"));
6361
7365
  init_api();
6362
7366
  var ROTATION_REASONS = ["scheduled", "compromise", "lost_device", "other"];
@@ -6374,8 +7378,8 @@ async function runRotate(did, opts) {
6374
7378
  throw new Error("rotate: local state is missing ownerId / currentAttestationId. State predates the rotation flow \u2014 please re-register.");
6375
7379
  }
6376
7380
  const api = new ArpApiClient(opts.server);
6377
- console.log(import_chalk24.default.dim(`Server: ${api.serverUrl}`));
6378
- console.log(import_chalk24.default.dim(`Rotating: ${local.did}`));
7381
+ console.log(import_chalk25.default.dim(`Server: ${api.serverUrl}`));
7382
+ console.log(import_chalk25.default.dim(`Rotating: ${local.did}`));
6379
7383
  if (!opts.yes) {
6380
7384
  const confirm = await (0, import_prompts3.default)({
6381
7385
  type: "confirm",
@@ -6384,7 +7388,7 @@ async function runRotate(did, opts) {
6384
7388
  initial: false
6385
7389
  });
6386
7390
  if (!confirm.go) {
6387
- console.log(import_chalk24.default.yellow("Aborted."));
7391
+ console.log(import_chalk25.default.yellow("Aborted."));
6388
7392
  return;
6389
7393
  }
6390
7394
  }
@@ -6460,22 +7464,22 @@ async function runRotate(did, opts) {
6460
7464
  pendingRotation: void 0
6461
7465
  });
6462
7466
  } catch (err) {
6463
- console.error(import_chalk24.default.red("\nServer rotation succeeded but local state write failed."));
6464
- console.error(import_chalk24.default.red("Capture these values now \u2014 the new key is already live server-side:"));
6465
- console.error(` ${import_chalk24.default.bold("identityPublicKeyB58")} : ${newIdentityPublicKeyB58}`);
6466
- console.error(` ${import_chalk24.default.bold("identitySecretKeyB64")} : ${newIdentitySecretKeyB64}`);
6467
- console.error(` ${import_chalk24.default.bold("currentAttestationId")} : ${updated.currentAttestationId}`);
6468
- console.error(import_chalk24.default.dim(`(Also persisted in pendingRotation at ~/.arp/agents.json before the server call.)`));
6469
- console.error(import_chalk24.default.dim(`Underlying error: ${err.message}`));
7467
+ console.error(import_chalk25.default.red("\nServer rotation succeeded but local state write failed."));
7468
+ console.error(import_chalk25.default.red("Capture these values now \u2014 the new key is already live server-side:"));
7469
+ console.error(` ${import_chalk25.default.bold("identityPublicKeyB58")} : ${newIdentityPublicKeyB58}`);
7470
+ console.error(` ${import_chalk25.default.bold("identitySecretKeyB64")} : ${newIdentitySecretKeyB64}`);
7471
+ console.error(` ${import_chalk25.default.bold("currentAttestationId")} : ${updated.currentAttestationId}`);
7472
+ console.error(import_chalk25.default.dim(`(Also persisted in pendingRotation at ~/.arp/agents.json before the server call.)`));
7473
+ console.error(import_chalk25.default.dim(`Underlying error: ${err.message}`));
6470
7474
  throw err;
6471
7475
  }
6472
- console.log(import_chalk24.default.green("\nRotated."));
6473
- console.log(`${import_chalk24.default.bold("DID")}: ${import_chalk24.default.cyan(updated.did)} ${import_chalk24.default.dim("(unchanged)")}`);
6474
- console.log(`${import_chalk24.default.bold("New identity public key")}: ${import_chalk24.default.cyan(newIdentityPublicKeyB58)}`);
6475
- console.log(`${import_chalk24.default.bold("New attestation id")}: ${import_chalk24.default.cyan(updated.currentAttestationId)}`);
6476
- console.log(import_chalk24.default.bold("\nAgent profile:"));
7476
+ console.log(import_chalk25.default.green("\nRotated."));
7477
+ console.log(`${import_chalk25.default.bold("DID")}: ${import_chalk25.default.cyan(updated.did)} ${import_chalk25.default.dim("(unchanged)")}`);
7478
+ console.log(`${import_chalk25.default.bold("New identity public key")}: ${import_chalk25.default.cyan(newIdentityPublicKeyB58)}`);
7479
+ console.log(`${import_chalk25.default.bold("New attestation id")}: ${import_chalk25.default.cyan(updated.currentAttestationId)}`);
7480
+ console.log(import_chalk25.default.bold("\nAgent profile:"));
6477
7481
  console.log(formatJson(updated));
6478
- console.log(import_chalk24.default.dim("\nLocal state updated; old private key is no longer valid."));
7482
+ console.log(import_chalk25.default.dim("\nLocal state updated; old private key is no longer valid."));
6479
7483
  }
6480
7484
  function base64UrlNoPadDecode2(s) {
6481
7485
  const replaced = s.replace(/-/g, "+").replace(/_/g, "/");
@@ -6485,7 +7489,7 @@ function base64UrlNoPadDecode2(s) {
6485
7489
 
6486
7490
  // src/commands/send-handshake.ts
6487
7491
  var import_sdk14 = require("@heyanon-arp/sdk");
6488
- var import_chalk25 = __toESM(require("chalk"));
7492
+ var import_chalk26 = __toESM(require("chalk"));
6489
7493
  init_api();
6490
7494
  function registerSendHandshakeCommand(root) {
6491
7495
  root.command("send-handshake").description("Send a handshake envelope to <recipient-did>. Server creates the relationship row on first contact.").argument("<recipient-did>", "Recipient agent DID (did:arp:...)").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--greeting <s>", "Optional greeting text included in body.content").option("--intent <s>", "Optional intent text included in body.content").option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).action(async (recipientDid, opts) => {
@@ -6498,10 +7502,10 @@ async function runSendHandshake(recipientDid, opts) {
6498
7502
  }
6499
7503
  const ttlSeconds = parseTtl5(opts.ttl);
6500
7504
  const api = new ArpApiClient(opts.server);
6501
- console.log(import_chalk25.default.dim(`Server: ${api.serverUrl}`));
7505
+ console.log(import_chalk26.default.dim(`Server: ${api.serverUrl}`));
6502
7506
  const sender = resolveSenderAgent("send-handshake", opts.server, opts.fromDid);
6503
- console.log(import_chalk25.default.dim(`Sender: ${sender.did}`));
6504
- console.log(import_chalk25.default.dim(`Recipient: ${recipientDid}`));
7507
+ console.log(import_chalk26.default.dim(`Sender: ${sender.did}`));
7508
+ console.log(import_chalk26.default.dim(`Recipient: ${recipientDid}`));
6505
7509
  const content = {};
6506
7510
  if (opts.greeting) content.greeting = opts.greeting;
6507
7511
  if (opts.intent) content.intent = opts.intent;
@@ -6527,28 +7531,28 @@ async function runSendHandshake(recipientDid, opts) {
6527
7531
  identitySecretKey: signer.identitySecretKey
6528
7532
  });
6529
7533
  if (opts.verbose) {
6530
- console.log(import_chalk25.default.bold("\nEnvelope (pre-send):"));
7534
+ console.log(import_chalk26.default.bold("\nEnvelope (pre-send):"));
6531
7535
  console.log(formatJson(envelope));
6532
7536
  }
6533
7537
  const result = await api.ingest(envelope);
6534
7538
  updateAgentLocal(opts.server, sender.did, { lastSenderSequence: nextSequence });
6535
- console.log(import_chalk25.default.green("\nDelivered."));
6536
- console.log(`${import_chalk25.default.bold("Event id")}: ${import_chalk25.default.cyan(result.eventId)}`);
6537
- console.log(`${import_chalk25.default.bold("Relationship id")}: ${import_chalk25.default.cyan(result.relationshipId)}`);
6538
- console.log(`${import_chalk25.default.bold("Chain index")}: ${import_chalk25.default.cyan(String(result.relationshipEventIndex))}`);
6539
- console.log(`${import_chalk25.default.bold("Server timestamp")}: ${import_chalk25.default.cyan(result.serverTimestamp)}`);
6540
- console.log(`${import_chalk25.default.bold("Signed message hash")}: ${import_chalk25.default.cyan(result.signedMessageHash)}`);
6541
- console.log(`${import_chalk25.default.bold("Server event hash")}: ${import_chalk25.default.cyan(result.serverEventHash)}`);
7539
+ console.log(import_chalk26.default.green("\nDelivered."));
7540
+ console.log(`${import_chalk26.default.bold("Event id")}: ${import_chalk26.default.cyan(result.eventId)}`);
7541
+ console.log(`${import_chalk26.default.bold("Relationship id")}: ${import_chalk26.default.cyan(result.relationshipId)}`);
7542
+ console.log(`${import_chalk26.default.bold("Chain index")}: ${import_chalk26.default.cyan(String(result.relationshipEventIndex))}`);
7543
+ console.log(`${import_chalk26.default.bold("Server timestamp")}: ${import_chalk26.default.cyan(result.serverTimestamp)}`);
7544
+ console.log(`${import_chalk26.default.bold("Signed message hash")}: ${import_chalk26.default.cyan(result.signedMessageHash)}`);
7545
+ console.log(`${import_chalk26.default.bold("Server event hash")}: ${import_chalk26.default.cyan(result.serverEventHash)}`);
6542
7546
  if (result.prevServerEventHash) {
6543
- console.log(`${import_chalk25.default.bold("Prev server event hash")}: ${import_chalk25.default.cyan(result.prevServerEventHash)}`);
7547
+ console.log(`${import_chalk26.default.bold("Prev server event hash")}: ${import_chalk26.default.cyan(result.prevServerEventHash)}`);
6544
7548
  } else {
6545
- console.log(`${import_chalk25.default.bold("Prev server event hash")}: ${import_chalk25.default.dim("(null \u2014 first event of this relationship)")}`);
7549
+ console.log(`${import_chalk26.default.bold("Prev server event hash")}: ${import_chalk26.default.dim("(null \u2014 first event of this relationship)")}`);
6546
7550
  }
6547
7551
  if (opts.verbose) {
6548
- console.log(import_chalk25.default.bold("\nFull server response:"));
7552
+ console.log(import_chalk26.default.bold("\nFull server response:"));
6549
7553
  console.log(formatJson(result));
6550
7554
  }
6551
- console.log(import_chalk25.default.dim(`
7555
+ console.log(import_chalk26.default.dim(`
6552
7556
  Local sender_sequence advanced to ${nextSequence}.`));
6553
7557
  }
6554
7558
  function parseTtl5(raw) {
@@ -6565,7 +7569,7 @@ function isDid(s) {
6565
7569
 
6566
7570
  // src/commands/send-handshake-response.ts
6567
7571
  var import_sdk15 = require("@heyanon-arp/sdk");
6568
- var import_chalk26 = __toESM(require("chalk"));
7572
+ var import_chalk27 = __toESM(require("chalk"));
6569
7573
  init_api();
6570
7574
  var ALLOWED_DECISIONS = /* @__PURE__ */ new Set(["accept", "decline"]);
6571
7575
  function registerSendHandshakeResponseCommand(root) {
@@ -6576,7 +7580,11 @@ function registerSendHandshakeResponseCommand(root) {
6576
7580
  // would fire for the accept path too; validate manually
6577
7581
  // after decision is parsed.
6578
7582
  `When --decision=decline: required reason code (one of: ${import_sdk15.DECLINE_REASONS.join(", ")}). Carried in body.content.reason.`
6579
- ).option("--reason-detail <s>", "Optional free-text elaboration alongside --reason (max 512 chars).").option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response", false).option(
7583
+ ).option("--reason-detail <s>", "Optional free-text elaboration alongside --reason (max 512 chars).").option("--ttl <seconds>", "Envelope TTL in seconds (max 86400 = 24h)", "3600").option("--verbose", "Print the full envelope before sending and the full server response. Mutually exclusive with --json.", false).option(
7584
+ "--json",
7585
+ "Machine-readable mode \u2014 emit a single JSON object on stdout ({ok, decision, eventId, relationshipId, relationshipEventIndex, serverTimestamp, serverEventHash, prevServerEventHash}; idempotent short-circuit adds {idempotent:true}). Prelude + banners move off stdout; on failure stderr carries `{code, message}`. Mutually exclusive with --verbose.",
7586
+ false
7587
+ ).option(
6580
7588
  "--force",
6581
7589
  "Skip the pre-send relationship-state probe. Default: when the relationship with <recipient-did> is already 'active', the command short-circuits successfully (the previous response already landed). Pass --force to re-send anyway \u2014 the server still gates with DOM_INVALID_TRANSITION, so this is mostly useful for FSM-guard tests.",
6582
7590
  false
@@ -6585,6 +7593,11 @@ function registerSendHandshakeResponseCommand(root) {
6585
7593
  });
6586
7594
  }
6587
7595
  async function runSendHandshakeResponse(recipientDid, opts) {
7596
+ if (opts.verbose && opts.json) {
7597
+ throw new Error(
7598
+ "send-handshake-response: --verbose and --json are mutually exclusive. --json emits the structured server response; --verbose adds envelope + response dumps that would break `--json | jq`."
7599
+ );
7600
+ }
6588
7601
  if (!isDid2(recipientDid)) {
6589
7602
  throw new Error(`send-handshake-response: <recipient-did> must look like 'did:arp:...' (got '${recipientDid}')`);
6590
7603
  }
@@ -6597,11 +7610,11 @@ async function runSendHandshakeResponse(recipientDid, opts) {
6597
7610
  declinePayload = detail ? { reason, reasonDetail: detail } : { reason };
6598
7611
  }
6599
7612
  const api = new ArpApiClient(opts.server);
6600
- console.log(import_chalk26.default.dim(`Server: ${api.serverUrl}`));
7613
+ progress(opts.json, import_chalk27.default.dim(`Server: ${api.serverUrl}`));
6601
7614
  const sender = resolveSenderAgent("send-handshake-response", opts.server, opts.fromDid);
6602
- console.log(import_chalk26.default.dim(`Sender: ${sender.did}`));
6603
- console.log(import_chalk26.default.dim(`Recipient: ${recipientDid}`));
6604
- console.log(import_chalk26.default.dim(`Decision: ${decision}`));
7615
+ progress(opts.json, import_chalk27.default.dim(`Sender: ${sender.did}`));
7616
+ progress(opts.json, import_chalk27.default.dim(`Recipient: ${recipientDid}`));
7617
+ progress(opts.json, import_chalk27.default.dim(`Decision: ${decision}`));
6605
7618
  const signer = makeSigner(sender);
6606
7619
  if (!opts.force) {
6607
7620
  try {
@@ -6614,10 +7627,29 @@ async function runSendHandshakeResponse(recipientDid, opts) {
6614
7627
  const events = await api.listEvents(existing.relationshipId, signer);
6615
7628
  const previousResponseFromMe = events.find((e) => e.senderDid === sender.did && e.type === "handshake_response");
6616
7629
  if (previousResponseFromMe) {
6617
- console.log(import_chalk26.default.yellow(`
7630
+ if (opts.json) {
7631
+ jsonOut({
7632
+ ok: true,
7633
+ idempotent: true,
7634
+ decision,
7635
+ eventId: previousResponseFromMe.eventId,
7636
+ relationshipId: existing.relationshipId,
7637
+ relationshipEventIndex: previousResponseFromMe.relationshipEventIndex,
7638
+ serverTimestamp: previousResponseFromMe.serverTimestamp,
7639
+ signedMessageHash: previousResponseFromMe.signedMessageHash,
7640
+ serverEventHash: previousResponseFromMe.serverEventHash,
7641
+ prevServerEventHash: previousResponseFromMe.prevServerEventHash ?? null,
7642
+ senderSequence: previousResponseFromMe.senderSequence
7643
+ });
7644
+ return;
7645
+ }
7646
+ progress(opts.json, import_chalk27.default.yellow(`
6618
7647
  [--idempotency] Relationship ${existing.relationshipId} with ${recipientDid} is already 'active'.`));
6619
- console.log(import_chalk26.default.dim(`A previous accept from this signer (event ${previousResponseFromMe.eventId}) landed successfully. Skipping re-send (use --force to override).`));
6620
- console.log(import_chalk26.default.dim(`Last event index: ${existing.lastEventIndex}, last server event hash: ${existing.lastServerEventHash ?? "(none)"}`));
7648
+ progress(
7649
+ opts.json,
7650
+ import_chalk27.default.dim(`A previous accept from this signer (event ${previousResponseFromMe.eventId}) landed successfully. Skipping re-send (use --force to override).`)
7651
+ );
7652
+ progress(opts.json, import_chalk27.default.dim(`Last event index: ${existing.lastEventIndex}, last server event hash: ${existing.lastServerEventHash ?? "(none)"}`));
6621
7653
  return;
6622
7654
  }
6623
7655
  throw new Error(
@@ -6628,7 +7660,7 @@ async function runSendHandshakeResponse(recipientDid, opts) {
6628
7660
  if (probeErr instanceof Error && /CLOSED|terminated|already 'active' from a previous ACCEPT|original initiator/i.test(probeErr.message)) {
6629
7661
  throw probeErr;
6630
7662
  }
6631
- console.log(import_chalk26.default.dim(`(idempotency probe failed; proceeding anyway: ${probeErr.message})`));
7663
+ progress(opts.json, import_chalk27.default.dim(`(idempotency probe failed; proceeding anyway: ${probeErr.message})`));
6632
7664
  }
6633
7665
  }
6634
7666
  const content = { decision };
@@ -6658,28 +7690,43 @@ async function runSendHandshakeResponse(recipientDid, opts) {
6658
7690
  identitySecretKey: signer.identitySecretKey
6659
7691
  });
6660
7692
  if (opts.verbose) {
6661
- console.log(import_chalk26.default.bold("\nEnvelope (pre-send):"));
7693
+ console.log(import_chalk27.default.bold("\nEnvelope (pre-send):"));
6662
7694
  console.log(formatJson(envelope));
6663
7695
  }
6664
7696
  const result = await api.ingest(envelope);
6665
7697
  updateAgentLocal(opts.server, sender.did, { lastSenderSequence: nextSequence });
6666
- console.log(import_chalk26.default.green("\nDelivered."));
6667
- console.log(`${import_chalk26.default.bold("Event id")}: ${import_chalk26.default.cyan(result.eventId)}`);
6668
- console.log(`${import_chalk26.default.bold("Relationship id")}: ${import_chalk26.default.cyan(result.relationshipId)}`);
6669
- console.log(`${import_chalk26.default.bold("Chain index")}: ${import_chalk26.default.cyan(String(result.relationshipEventIndex))}`);
6670
- console.log(`${import_chalk26.default.bold("Server timestamp")}: ${import_chalk26.default.cyan(result.serverTimestamp)}`);
6671
- console.log(`${import_chalk26.default.bold("Signed message hash")}: ${import_chalk26.default.cyan(result.signedMessageHash)}`);
6672
- console.log(`${import_chalk26.default.bold("Server event hash")}: ${import_chalk26.default.cyan(result.serverEventHash)}`);
7698
+ if (opts.json) {
7699
+ jsonOut({
7700
+ ok: true,
7701
+ decision,
7702
+ eventId: result.eventId,
7703
+ relationshipId: result.relationshipId,
7704
+ relationshipEventIndex: result.relationshipEventIndex,
7705
+ serverTimestamp: result.serverTimestamp,
7706
+ signedMessageHash: result.signedMessageHash,
7707
+ serverEventHash: result.serverEventHash,
7708
+ prevServerEventHash: result.prevServerEventHash ?? null,
7709
+ senderSequence: nextSequence
7710
+ });
7711
+ return;
7712
+ }
7713
+ console.log(import_chalk27.default.green("\nDelivered."));
7714
+ console.log(`${import_chalk27.default.bold("Event id")}: ${import_chalk27.default.cyan(result.eventId)}`);
7715
+ console.log(`${import_chalk27.default.bold("Relationship id")}: ${import_chalk27.default.cyan(result.relationshipId)}`);
7716
+ console.log(`${import_chalk27.default.bold("Chain index")}: ${import_chalk27.default.cyan(String(result.relationshipEventIndex))}`);
7717
+ console.log(`${import_chalk27.default.bold("Server timestamp")}: ${import_chalk27.default.cyan(result.serverTimestamp)}`);
7718
+ console.log(`${import_chalk27.default.bold("Signed message hash")}: ${import_chalk27.default.cyan(result.signedMessageHash)}`);
7719
+ console.log(`${import_chalk27.default.bold("Server event hash")}: ${import_chalk27.default.cyan(result.serverEventHash)}`);
6673
7720
  if (result.prevServerEventHash) {
6674
- console.log(`${import_chalk26.default.bold("Prev server event hash")}: ${import_chalk26.default.cyan(result.prevServerEventHash)}`);
7721
+ console.log(`${import_chalk27.default.bold("Prev server event hash")}: ${import_chalk27.default.cyan(result.prevServerEventHash)}`);
6675
7722
  } else {
6676
- console.log(`${import_chalk26.default.bold("Prev server event hash")}: ${import_chalk26.default.dim("(null \u2014 first event of this relationship)")}`);
7723
+ console.log(`${import_chalk27.default.bold("Prev server event hash")}: ${import_chalk27.default.dim("(null \u2014 first event of this relationship)")}`);
6677
7724
  }
6678
7725
  if (opts.verbose) {
6679
- console.log(import_chalk26.default.bold("\nFull server response:"));
7726
+ console.log(import_chalk27.default.bold("\nFull server response:"));
6680
7727
  console.log(formatJson(result));
6681
7728
  }
6682
- console.log(import_chalk26.default.dim(`
7729
+ console.log(import_chalk27.default.dim(`
6683
7730
  Local sender_sequence advanced to ${nextSequence}.`));
6684
7731
  }
6685
7732
  function parseDecision(raw) {
@@ -6732,8 +7779,356 @@ async function findExistingRelationship(api, signer, senderDid, recipientDid) {
6732
7779
  return void 0;
6733
7780
  }
6734
7781
 
7782
+ // src/commands/settlement.ts
7783
+ var import_sdk16 = require("@heyanon-arp/sdk");
7784
+ var import_utils3 = require("@noble/hashes/utils");
7785
+ var import_chalk28 = __toESM(require("chalk"));
7786
+ init_api();
7787
+ var NATIVE_SOL_MINT2 = "11111111111111111111111111111111";
7788
+ var SETTLEMENT_MIN_EXPIRY_HEADROOM_SECS = 120;
7789
+ var SETTLEMENT_REFRESH_HEADROOM_SECS = 600;
7790
+ function registerSettlementCommands(root) {
7791
+ const cmd = root.command("settlement").description("Settlement helpers \u2014 sign + deliver escrow-release signatures in one shot.");
7792
+ cmd.command("auto-sign-and-deliver").description(
7793
+ "As the PAYEE, sign the escrow-release digest for a delegation and deliver the signature to the buyer via a settlement_signature envelope \u2014 one command instead of the did-doc \u2192 derive-condition-hash \u2192 delegations \u2192 wallet sign-settlement-release \u2192 memory ritual. Run it after `heyarp receipt propose`."
7794
+ ).requiredOption("--delegation-id <id>", "Delegation UUID whose receipt is being settled.").option("--rel-id <id>", "Relationship UUID. Auto-resolved from --delegation-id when omitted (the delegation must live in exactly one of your relationships).").option("--mint-pubkey <base58>", `Lock mint. Default native SOL (${NATIVE_SOL_MINT2}); set to the SPL token mint for token-denominated locks.`, NATIVE_SOL_MINT2).option(
7795
+ "--cluster-tag <int>",
7796
+ "Solana cluster the lock lives on: 0 = devnet, 1 = mainnet-beta. MUST match the create_lock cluster or the server-reconstructed release digest will not verify. REQUIRED \u2014 there is no default (a defaulted/wrong cluster silently signs an invalid settlement digest that fails at cosign)."
7797
+ ).option(
7798
+ "--settlement-buffer-secs <int>",
7799
+ "Seconds added to the delegation deadline for the settlement digest expires_at (dispute buffer). Default 86400 (1 day). Ignored when --expires-at is given.",
7800
+ "86400"
7801
+ ).option(
7802
+ "--expires-at <unix>",
7803
+ "Explicit settlement digest expires_at (unix seconds). Overrides deadline+buffer. REQUIRED when the delegation has no deadline (a guessed value risks exceeding the on-chain lock expiry \u2192 server reject). Must be \u2264 the lock's on-chain expiry."
7804
+ ).option(
7805
+ "--partial-payee-amount <int>",
7806
+ "Base-unit payee_amount for a PARTIAL release (usage_based). Default: the receipt's usage.computed_amount when set. Omit for a full release of a flat contract."
7807
+ ).option(
7808
+ "--fee-bps-at-lock <int>",
7809
+ "fee_bps_at_lock denormalised on the Lock at create_lock time (default 0 \u2014 no protocol fee). Pass the lock's actual value on fee-enabled deployments."
7810
+ ).option("--fee-recipient-at-lock <base58>", "fee_recipient_at_lock denormalised on the Lock (default native = 1...1).").option("--server <url>", "Override ARP server base URL.").option("--from-did <did>", "Sender DID (= the payee) \u2014 required only if multiple agents are registered against this server.").option(
7811
+ "--receipt-event-hash <sha256:hex>",
7812
+ "Disambiguate when a delegation has more than one PROPOSED receipt (e.g. multiple work requests). Selects the exact receipt to settle."
7813
+ ).option(
7814
+ "--force",
7815
+ "Re-sign and re-deliver even when a payee settlement signature already exists on the receipt. Use to REPAIR a previously-delivered sig built with wrong digest inputs (fee/mint/cluster); the server replaces the stored payee sig with the later envelope. Default is an idempotent skip.",
7816
+ false
7817
+ ).option(
7818
+ "--json",
7819
+ "Machine-readable mode \u2014 emit a single JSON object on stdout {ok, delegationId, relationshipId, buyerDid, purpose, settlementPubkey, payeeAmount?, expiresAt, eventId, serverEventHash, ...}; idempotent re-run adds {idempotent:true}. Prelude moves to stderr; failures emit {code, message}.",
7820
+ false
7821
+ ).action(async (opts, cmd2) => {
7822
+ try {
7823
+ await runAutoSignAndDeliver(opts);
7824
+ } catch (err) {
7825
+ emitActionError(err, cmd2);
7826
+ process.exitCode = 1;
7827
+ }
7828
+ });
7829
+ }
7830
+ function toBaseUnits(amountDecimal, decimals) {
7831
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(amountDecimal)) {
7832
+ throw new Error(`settlement: amount '${amountDecimal}' is not a non-negative decimal number`);
7833
+ }
7834
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 18) {
7835
+ throw new Error(`settlement: currency decimals must be an integer 0..18 (got ${decimals})`);
7836
+ }
7837
+ const [intPart, fracPart = ""] = amountDecimal.split(".");
7838
+ if (fracPart.length > decimals) {
7839
+ throw new Error(
7840
+ `settlement: amount '${amountDecimal}' has ${fracPart.length} fractional digits but the currency only has ${decimals} decimals \u2014 refusing to truncate a money value`
7841
+ );
7842
+ }
7843
+ const fracPadded = fracPart.padEnd(decimals, "0");
7844
+ const combined = `${intPart}${fracPadded}`.replace(/^0+/, "");
7845
+ return combined === "" ? "0" : combined;
7846
+ }
7847
+ async function findDelegationRow(api, signer, relationshipId, delegationId) {
7848
+ let after;
7849
+ for (let page = 0; page < 50; page++) {
7850
+ const rows = await api.listDelegations(relationshipId, signer, { limit: 100, after });
7851
+ if (rows.length === 0) return void 0;
7852
+ const match = rows.find((d) => d.delegationId === delegationId);
7853
+ if (match) return match;
7854
+ if (rows.length < 100) return void 0;
7855
+ after = rows[rows.length - 1].id;
7856
+ }
7857
+ return void 0;
7858
+ }
7859
+ function selectReceipt(matches, receiptEventHash, cmdName) {
7860
+ if (matches.length === 0) return void 0;
7861
+ if (receiptEventHash !== void 0 && receiptEventHash !== "") {
7862
+ const exact = matches.find((r) => r.receiptEventHash === receiptEventHash);
7863
+ if (!exact) {
7864
+ throw new Error(
7865
+ `${cmdName}: no receipt with receiptEventHash=${receiptEventHash} for this delegation (available: ${matches.map((r) => r.receiptEventHash).join(", ")})`
7866
+ );
7867
+ }
7868
+ return exact;
7869
+ }
7870
+ const proposed = matches.filter((r) => r.state === "proposed");
7871
+ if (proposed.length > 1) {
7872
+ throw new Error(
7873
+ `${cmdName}: ${proposed.length} PROPOSED receipts exist for this delegation \u2014 disambiguate with --receipt-event-hash <sha256:hex> (one of: ${proposed.map((r) => r.receiptEventHash).join(", ")})`
7874
+ );
7875
+ }
7876
+ if (proposed.length === 1) return proposed[0];
7877
+ return matches.reduce((latest, cur) => cur.createdAt > latest.createdAt ? cur : latest);
7878
+ }
7879
+ async function collectReceiptRows(api, signer, relationshipId, delegationId) {
7880
+ const matches = [];
7881
+ let after;
7882
+ for (let page = 0; page < 50; page++) {
7883
+ const rows = await api.listReceipts(relationshipId, signer, { limit: 100, after, delegationId });
7884
+ if (rows.length === 0) break;
7885
+ for (const r of rows) {
7886
+ if (r.delegationId === delegationId) matches.push(r);
7887
+ }
7888
+ if (rows.length < 100) break;
7889
+ after = rows[rows.length - 1].id;
7890
+ }
7891
+ return matches;
7892
+ }
7893
+ function settlementKeyFromDidDoc(didDoc, buyerDid) {
7894
+ const mb = extractField(didDoc, "verificationMethod.#settlement.publicKeyMultibase");
7895
+ if (typeof mb !== "string" || mb.length === 0) {
7896
+ throw new Error(`settlement: buyer ${buyerDid} DID document has no usable #settlement verification key`);
7897
+ }
7898
+ return mb.startsWith("z") ? mb.slice(1) : mb;
7899
+ }
7900
+ async function runAutoSignAndDeliver(opts) {
7901
+ const cmdName = "settlement auto-sign-and-deliver";
7902
+ const delegationId = requireUuidNormalised(cmdName, opts.delegationId, "--delegation-id");
7903
+ if (opts.relId) opts.relId = requireUuidNormalised(cmdName, opts.relId, "--rel-id");
7904
+ const clusterTag = opts.clusterTag;
7905
+ if (clusterTag !== void 0 && clusterTag !== "0" && clusterTag !== "1") {
7906
+ throw new Error(`${cmdName}: --cluster-tag must be '0' (devnet) or '1' (mainnet-beta), got '${clusterTag}'`);
7907
+ }
7908
+ const bufferRaw = opts.settlementBufferSecs ?? "86400";
7909
+ if (!/^[0-9]+$/.test(bufferRaw)) {
7910
+ throw new Error(`${cmdName}: --settlement-buffer-secs must be a non-negative integer (got '${opts.settlementBufferSecs}')`);
7911
+ }
7912
+ const bufferSecs = Number.parseInt(bufferRaw, 10);
7913
+ const api = new ArpApiClient(opts.server);
7914
+ const sender = resolveSenderAgent(cmdName, opts.server, opts.fromDid);
7915
+ const signer = makeSigner(sender);
7916
+ progress(opts.json, import_chalk28.default.dim(`Server: ${api.serverUrl}`));
7917
+ progress(opts.json, import_chalk28.default.dim(`Signer (payee): ${sender.did}`));
7918
+ const relId = opts.relId ?? await resolveAutoRelId(api, sender, delegationId);
7919
+ progress(opts.json, import_chalk28.default.dim(`Relationship: ${relId}`));
7920
+ const receiptMatches = await collectReceiptRows(api, signer, relId, delegationId);
7921
+ const receipt = selectReceipt(receiptMatches, opts.receiptEventHash, cmdName);
7922
+ if (!receipt) {
7923
+ throw new Error(`${cmdName}: no receipt found for delegation ${delegationId} under relationship ${relId}. Run \`heyarp receipt propose\` first.`);
7924
+ }
7925
+ if (receipt.payeeDid !== sender.did) {
7926
+ throw new Error(
7927
+ `${cmdName}: signer ${sender.did} is not the payee on this receipt (payee is ${receipt.payeeDid}). The settlement signature must be signed by the PAYEE \u2014 pass --from-did <payee-did> or run as the payee agent.`
7928
+ );
7929
+ }
7930
+ if (receipt.verdictProposed === "rejected" || receipt.verdictFinal === "rejected") {
7931
+ throw new Error(
7932
+ `${cmdName}: receipt verdict is 'rejected' \u2014 a rejected receipt is not payable; there is no settlement signature to send. (If this is wrong, the payee should re-propose the receipt with an accepting verdict.)`
7933
+ );
7934
+ }
7935
+ if (receipt.state === "cosigned") {
7936
+ if (opts.json) {
7937
+ jsonOut({ ok: true, idempotent: true, alreadyCosigned: true, delegationId, relationshipId: relId, buyerDid: receipt.callerDid });
7938
+ return;
7939
+ }
7940
+ progress(opts.json, import_chalk28.default.yellow("\n[idempotent] Receipt already cosigned \u2014 escrow release is settled. Nothing to send."));
7941
+ return;
7942
+ }
7943
+ if (receipt.payeeSettlement && !opts.force) {
7944
+ const ps = receipt.payeeSettlement;
7945
+ const nowSecs = Math.floor(Date.now() / 1e3);
7946
+ if (ps.expires_at > nowSecs + SETTLEMENT_REFRESH_HEADROOM_SECS) {
7947
+ if (opts.json) {
7948
+ jsonOut({
7949
+ ok: true,
7950
+ idempotent: true,
7951
+ delegationId,
7952
+ relationshipId: relId,
7953
+ buyerDid: receipt.callerDid,
7954
+ purpose: ps.purpose,
7955
+ settlementPubkey: ps.settlement_pubkey,
7956
+ ...ps.payee_amount !== void 0 ? { payeeAmount: ps.payee_amount } : {},
7957
+ expiresAt: ps.expires_at
7958
+ });
7959
+ return;
7960
+ }
7961
+ progress(opts.json, import_chalk28.default.yellow("\n[idempotent] Payee settlement signature already delivered + still valid for this receipt \u2014 skipping re-send."));
7962
+ progress(opts.json, import_chalk28.default.dim(` purpose=${ps.purpose} settlement_pubkey=${ps.settlement_pubkey} expires_at=${ps.expires_at}`));
7963
+ return;
7964
+ }
7965
+ progress(
7966
+ opts.json,
7967
+ import_chalk28.default.yellow(`
7968
+ [refresh] Existing payee settlement sig expires at ${ps.expires_at} (\u2264 now+${SETTLEMENT_REFRESH_HEADROOM_SECS}s) \u2014 re-signing with a fresh expiry.`)
7969
+ );
7970
+ }
7971
+ const buyerDid = receipt.callerDid;
7972
+ const receiptEventHash = receipt.receiptEventHash;
7973
+ const deliverableHash = receipt.deliverableHash ?? receipt.responseHash;
7974
+ progress(opts.json, import_chalk28.default.dim(`Buyer (payer): ${buyerDid}`));
7975
+ progress(opts.json, import_chalk28.default.dim(`Receipt event hash: ${receiptEventHash}`));
7976
+ const delegation = await findDelegationRow(api, signer, relId, delegationId);
7977
+ if (!delegation) {
7978
+ throw new Error(`${cmdName}: delegation ${delegationId} not found under relationship ${relId} (paginated 5000 rows).`);
7979
+ }
7980
+ if (delegation.state !== "accepted") {
7981
+ throw new Error(
7982
+ `${cmdName}: delegation ${delegationId} is in state '${delegation.state ?? "unknown"}' \u2014 only an 'accepted' delegation is settleable (its escrow lock is still LOCKED). A non-accepted delegation's lock has been released / refunded / canceled, so the server would reject the settlement signature post-commit (SETTLEMENT_SIG_LOCK_INVALID_STATE). Nothing to settle.`
7983
+ );
7984
+ }
7985
+ if (!delegation.amount) {
7986
+ throw new Error(`${cmdName}: delegation ${delegationId} has no amount \u2014 cannot derive the lock amount for the release digest.`);
7987
+ }
7988
+ const decimals = delegation.currency?.decimals ?? 9;
7989
+ const lockAmount = toBaseUnits(delegation.amount, decimals);
7990
+ let expiresAt8;
7991
+ if (opts.expiresAt !== void 0 && opts.expiresAt !== "") {
7992
+ if (!/^[0-9]+$/.test(opts.expiresAt)) {
7993
+ throw new Error(`${cmdName}: --expires-at must be a positive unix-seconds integer (got '${opts.expiresAt}')`);
7994
+ }
7995
+ expiresAt8 = Number.parseInt(opts.expiresAt, 10);
7996
+ if (!Number.isInteger(expiresAt8) || expiresAt8 <= 0) {
7997
+ throw new Error(`${cmdName}: --expires-at must be a positive unix-seconds integer (got '${opts.expiresAt}')`);
7998
+ }
7999
+ } else if (delegation.deadline) {
8000
+ expiresAt8 = Math.floor(new Date(delegation.deadline).getTime() / 1e3) + bufferSecs;
8001
+ } else {
8002
+ throw new Error(
8003
+ `${cmdName}: delegation ${delegationId} has no deadline, so a safe settlement expires_at can't be derived (a guessed value risks exceeding the on-chain lock expiry \u2192 post-commit server reject). Pass --expires-at <unix-seconds> \u2264 the lock's expiry.`
8004
+ );
8005
+ }
8006
+ const nowSecsForExpiry = Math.floor(Date.now() / 1e3);
8007
+ const minSafeExpiry = nowSecsForExpiry + SETTLEMENT_MIN_EXPIRY_HEADROOM_SECS;
8008
+ if (!Number.isFinite(expiresAt8) || expiresAt8 <= minSafeExpiry) {
8009
+ const fromExplicit = opts.expiresAt !== void 0 && opts.expiresAt !== "";
8010
+ throw new Error(
8011
+ `${cmdName}: settlement expires_at (${Number.isFinite(expiresAt8) ? expiresAt8 : "NaN"}) is not far enough in the future \u2014 it must be > now+${SETTLEMENT_MIN_EXPIRY_HEADROOM_SECS}s (${minSafeExpiry}) or the server rejects it post-commit (SETTLEMENT_SIG_EXPIRES_AT_*), burning a sender_sequence. ${fromExplicit ? "Pass a later --expires-at <unix-seconds> (still \u2264 the lock's on-chain expiry)." : `Derived from deadline ${JSON.stringify(delegation.deadline)} + buffer ${bufferSecs}s \u2014 the deadline is too close or in the past. Increase --settlement-buffer-secs or pass --expires-at <unix-seconds> \u2264 the lock's expiry.`}`
8012
+ );
8013
+ }
8014
+ const contract = await findContractRow(api, signer, relId, delegation.contractId, void 0);
8015
+ if (contract.settlementModel === "prepaid") {
8016
+ throw new Error(
8017
+ `${cmdName}: contract ${contract.contractId} settlementModel is 'prepaid' (non-escrow). This command delivers an on-chain escrow-release signature; a prepaid contract has no lock to settle and the server would reject post-commit (SETTLEMENT_SIG_LOCK_NOT_FOUND). Nothing to settle on-chain.`
8018
+ );
8019
+ }
8020
+ const subset = projectContractForHash(contract);
8021
+ const conditionHash = (0, import_utils3.bytesToHex)((0, import_sdk16.deriveConditionHash)(subset));
8022
+ const buyerDidDoc = await api.getDidDocument(buyerDid);
8023
+ const payerSettlementPubkey = settlementKeyFromDidDoc(buyerDidDoc, buyerDid);
8024
+ const isUsageBased = contract.pricingModel === "usage_based";
8025
+ let partialPayeeAmount;
8026
+ if (isUsageBased) {
8027
+ const computed = receipt.usage?.computed_amount;
8028
+ if (computed === void 0 || computed === "" || !/^[0-9]+$/.test(computed)) {
8029
+ throw new Error(
8030
+ `${cmdName}: contract ${contract.contractId} is usage_based but the receipt has no usable usage.computed_amount (got ${computed === void 0 ? "unset" : `'${computed}'`}). The server binds the PARTIAL-RELEASE amount to that field; re-propose the receipt with a computed_amount.`
8031
+ );
8032
+ }
8033
+ if (opts.partialPayeeAmount !== void 0 && opts.partialPayeeAmount !== "") {
8034
+ if (!/^[0-9]+$/.test(opts.partialPayeeAmount)) {
8035
+ throw new Error(`${cmdName}: --partial-payee-amount must be a base-unit decimal-integer string (got '${opts.partialPayeeAmount}')`);
8036
+ }
8037
+ if (BigInt(opts.partialPayeeAmount) !== BigInt(computed)) {
8038
+ throw new Error(
8039
+ `${cmdName}: --partial-payee-amount ${opts.partialPayeeAmount} does not match the receipt's usage.computed_amount ${computed}. The server binds the settlement amount to the receipt \u2014 they must be equal; drop the flag or fix the value.`
8040
+ );
8041
+ }
8042
+ }
8043
+ partialPayeeAmount = computed;
8044
+ } else {
8045
+ if (opts.partialPayeeAmount !== void 0 && opts.partialPayeeAmount !== "") {
8046
+ throw new Error(
8047
+ `${cmdName}: contract ${contract.contractId} is ${contract.pricingModel ?? "flat"} (full-release only) \u2014 --partial-payee-amount is not allowed. A PARTIAL-RELEASE digest would be rejected by the server; drop the flag for a full release.`
8048
+ );
8049
+ }
8050
+ partialPayeeAmount = void 0;
8051
+ }
8052
+ if (clusterTag === void 0) {
8053
+ throw new Error(
8054
+ `${cmdName}: --cluster-tag is required (0 = devnet, 1 = mainnet-beta). It binds the cluster into the signed release digest; there is no safe default \u2014 pass the cluster where the create_lock lives.`
8055
+ );
8056
+ }
8057
+ if (opts.feeBpsAtLock !== void 0) {
8058
+ if (!/^[0-9]+$/.test(opts.feeBpsAtLock)) {
8059
+ throw new Error(`${cmdName}: --fee-bps-at-lock must be a non-negative integer (got '${opts.feeBpsAtLock}')`);
8060
+ }
8061
+ if (Number.parseInt(opts.feeBpsAtLock, 10) > 0 && (opts.feeRecipientAtLock === void 0 || opts.feeRecipientAtLock === "")) {
8062
+ throw new Error(
8063
+ `${cmdName}: --fee-bps-at-lock=${opts.feeBpsAtLock} is non-zero, so --fee-recipient-at-lock is also required. Both fee fields are bound into the release digest; defaulting the recipient to native would sign a digest the buyer's cosign rejects. Pass --fee-recipient-at-lock <the lock's fee_recipient_at_lock>.`
8064
+ );
8065
+ }
8066
+ }
8067
+ const signOpts = {
8068
+ server: opts.server,
8069
+ fromDid: opts.fromDid,
8070
+ delegationId,
8071
+ payerSettlementPubkey,
8072
+ payeeSettlementPubkey: sender.settlementPublicKeyB58,
8073
+ mintPubkey: opts.mintPubkey ?? NATIVE_SOL_MINT2,
8074
+ lockAmount,
8075
+ conditionHash,
8076
+ receiptEventHash,
8077
+ deliverableHash,
8078
+ expiresAt: String(expiresAt8),
8079
+ clusterTag,
8080
+ ...opts.feeBpsAtLock !== void 0 ? { feeBpsAtLock: opts.feeBpsAtLock } : {},
8081
+ ...opts.feeRecipientAtLock !== void 0 ? { feeRecipientAtLock: opts.feeRecipientAtLock } : {},
8082
+ ...partialPayeeAmount !== void 0 ? { partialPayeeAmount } : {}
8083
+ };
8084
+ progress(
8085
+ opts.json,
8086
+ import_chalk28.default.dim(`Signing ${partialPayeeAmount !== void 0 ? "PARTIAL" : "full"} release: lock_amount=${lockAmount}, expires_at=${expiresAt8}, cluster_tag=${clusterTag}`)
8087
+ );
8088
+ const signed = await signSettlementHandler(signOpts);
8089
+ const content = {
8090
+ delegation_id: delegationId,
8091
+ receipt_event_hash: receiptEventHash,
8092
+ purpose: signed.purpose,
8093
+ payee_settlement_pubkey: signed.settlement_pubkey,
8094
+ sig: signed.sig,
8095
+ expires_at: expiresAt8,
8096
+ ...partialPayeeAmount !== void 0 ? { payee_amount: partialPayeeAmount } : {}
8097
+ };
8098
+ const result = await sendSettlementSignatureEnvelope({ api, sender, recipientDid: buyerDid, content, ttlSeconds: 3600, verbose: false, server: opts.server });
8099
+ if (opts.json) {
8100
+ jsonOut({
8101
+ ok: true,
8102
+ delegationId,
8103
+ relationshipId: relId,
8104
+ buyerDid,
8105
+ purpose: signed.purpose,
8106
+ settlementPubkey: signed.settlement_pubkey,
8107
+ ...partialPayeeAmount !== void 0 ? { payeeAmount: partialPayeeAmount } : {},
8108
+ lockAmount,
8109
+ expiresAt: expiresAt8,
8110
+ receiptEventHash,
8111
+ eventId: result.eventId,
8112
+ serverEventHash: result.serverEventHash,
8113
+ relationshipEventIndex: result.relationshipEventIndex,
8114
+ serverTimestamp: result.serverTimestamp
8115
+ });
8116
+ return;
8117
+ }
8118
+ console.log(import_chalk28.default.green("\nSettlement signature signed + delivered."));
8119
+ console.log(`${import_chalk28.default.bold("Delegation")}: ${import_chalk28.default.cyan(delegationId)}`);
8120
+ console.log(`${import_chalk28.default.bold("Buyer")}: ${import_chalk28.default.cyan(buyerDid)}`);
8121
+ console.log(`${import_chalk28.default.bold("Purpose")}: ${import_chalk28.default.cyan(signed.purpose)}`);
8122
+ if (partialPayeeAmount !== void 0) {
8123
+ console.log(`${import_chalk28.default.bold("Payee amount")}: ${import_chalk28.default.cyan(partialPayeeAmount)} (partial release)`);
8124
+ }
8125
+ console.log(`${import_chalk28.default.bold("Expires at")}: ${import_chalk28.default.cyan(String(expiresAt8))}`);
8126
+ console.log(`${import_chalk28.default.bold("Delivered event")}: ${import_chalk28.default.cyan(result.serverEventHash)}`);
8127
+ console.log(import_chalk28.default.dim("\nThe buyer cosigns with: heyarp receipt cosign <rel-id> <del-id> --auto-hashes --auto-resolve-payee-sig --payer-sig-from-file <path>"));
8128
+ }
8129
+
6735
8130
  // src/commands/watch.ts
6736
- var import_chalk27 = __toESM(require("chalk"));
8131
+ var import_chalk29 = __toESM(require("chalk"));
6737
8132
  init_api();
6738
8133
  function registerWatchCommand(root) {
6739
8134
  root.command("watch").description("Live tail filtered to a single relationship (SSE). Server-side $match; only envelopes belonging to <rel-id> are streamed.").argument("<relationship-id>", "Relationship UUID to watch").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Signer DID \u2014 required only if multiple agents are registered against this server").option("--verbose", "After each envelope, print the full JSON with a per-row label including eventId + serverEventHash", false).option("--json", "Machine-readable: one NDJSON object per line. Pipe-safe into `jq -c`.", false).option("--full-ids", "Print DIDs + serverEventHash in full (no truncation).", false).action(async (relationshipId, opts) => {
@@ -6744,9 +8139,9 @@ async function runWatch(relationshipId, opts) {
6744
8139
  const local = resolveSenderAgent("watch", opts.server, opts.fromDid);
6745
8140
  const api = new ArpApiClient(opts.server);
6746
8141
  if (!opts.json) {
6747
- console.log(import_chalk27.default.dim(`Server: ${api.serverUrl}`));
6748
- console.log(import_chalk27.default.dim(`Signer: ${local.did}`));
6749
- console.log(import_chalk27.default.dim(`Watching: ${relationshipId}`));
8142
+ console.log(import_chalk29.default.dim(`Server: ${api.serverUrl}`));
8143
+ console.log(import_chalk29.default.dim(`Signer: ${local.did}`));
8144
+ console.log(import_chalk29.default.dim(`Watching: ${relationshipId}`));
6750
8145
  }
6751
8146
  const controller = new AbortController();
6752
8147
  let userAborted = false;
@@ -6765,7 +8160,7 @@ async function runWatch(relationshipId, opts) {
6765
8160
  }
6766
8161
  if (event.type === "heartbeat") continue;
6767
8162
  if (event.type === "connected") {
6768
- console.log(import_chalk27.default.green(`\u25CF stream open \u2014 watching ${relationshipId}`));
8163
+ console.log(import_chalk29.default.green(`\u25CF stream open \u2014 watching ${relationshipId}`));
6769
8164
  continue;
6770
8165
  }
6771
8166
  if (event.type === "envelope") {
@@ -6779,7 +8174,7 @@ async function runWatch(relationshipId, opts) {
6779
8174
  }
6780
8175
  continue;
6781
8176
  }
6782
- console.log(import_chalk27.default.dim(`(unknown event: ${event.type})`));
8177
+ console.log(import_chalk29.default.dim(`(unknown event: ${event.type})`));
6783
8178
  }
6784
8179
  if (!userAborted) {
6785
8180
  throw new Error(`watch ${relationshipId}: stream ended unexpectedly (server may have restarted, or the change stream errored). Re-run to reconnect.`);
@@ -6787,7 +8182,7 @@ async function runWatch(relationshipId, opts) {
6787
8182
  } catch (err) {
6788
8183
  const name = err.name;
6789
8184
  if (name === "AbortError" || userAborted) {
6790
- if (!opts.json) console.log(import_chalk27.default.dim("\nstream closed."));
8185
+ if (!opts.json) console.log(import_chalk29.default.dim("\nstream closed."));
6791
8186
  return;
6792
8187
  }
6793
8188
  throw err;
@@ -6801,21 +8196,21 @@ function formatWatchLine(ev, selfDid, opts = {}) {
6801
8196
  const type = ev.type.padEnd(20);
6802
8197
  const direction = directionLabel2(ev, selfDid, opts);
6803
8198
  const hash = opts.fullIds ? ev.serverEventHash : hashHead4(ev.serverEventHash);
6804
- return `${import_chalk27.default.dim(`[${ts}]`)} ${type} ${direction} ${import_chalk27.default.cyan(hash)}`;
8199
+ return `${import_chalk29.default.dim(`[${ts}]`)} ${type} ${direction} ${import_chalk29.default.cyan(hash)}`;
6805
8200
  }
6806
8201
  function directionLabel2(ev, selfDid, opts = {}) {
6807
8202
  const senderHead = opts.fullIds ? ev.senderDid : didHead5(ev.senderDid);
6808
8203
  const recipientHead = opts.fullIds ? ev.recipientDid : didHead5(ev.recipientDid);
6809
- if (ev.senderDid === selfDid) return `${import_chalk27.default.bold("me")} \u2192 ${import_chalk27.default.dim(recipientHead)}`;
6810
- if (ev.recipientDid === selfDid) return `${import_chalk27.default.dim(senderHead)} \u2192 ${import_chalk27.default.bold("me")}`;
6811
- return `${import_chalk27.default.dim(senderHead)} \u2192 ${import_chalk27.default.dim(recipientHead)}`;
8204
+ if (ev.senderDid === selfDid) return `${import_chalk29.default.bold("me")} \u2192 ${import_chalk29.default.dim(recipientHead)}`;
8205
+ if (ev.recipientDid === selfDid) return `${import_chalk29.default.dim(senderHead)} \u2192 ${import_chalk29.default.bold("me")}`;
8206
+ return `${import_chalk29.default.dim(senderHead)} \u2192 ${import_chalk29.default.dim(recipientHead)}`;
6812
8207
  }
6813
8208
  function didHead5(did) {
6814
8209
  if (did.length <= 20) return did;
6815
8210
  return `${did.slice(0, 20)}...`;
6816
8211
  }
6817
8212
  function hashHead4(hash) {
6818
- if (!hash) return import_chalk27.default.dim("(none)");
8213
+ if (!hash) return import_chalk29.default.dim("(none)");
6819
8214
  if (hash.length <= 14) return hash;
6820
8215
  return `${hash.slice(0, 14)}...`;
6821
8216
  }
@@ -6826,8 +8221,283 @@ function formatClock(iso) {
6826
8221
  return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
6827
8222
  }
6828
8223
 
8224
+ // src/commands/webhook.ts
8225
+ var import_promises = require("timers/promises");
8226
+ var import_chalk30 = __toESM(require("chalk"));
8227
+ init_api();
8228
+ function registerWebhookCommand(root) {
8229
+ const webhook = root.command("webhook").description("Manage outbound webhook delivery \u2014 URL + HMAC secret lifecycle for the calling agent.");
8230
+ registerUrlSubcommands(webhook);
8231
+ registerSecretSubcommands(webhook);
8232
+ }
8233
+ function registerUrlSubcommands(parent) {
8234
+ const url = parent.command("url").description("Outbound webhook URL \u2014 the address the server POSTs deliveries to.");
8235
+ url.command("set").description(
8236
+ "Set / replace the outbound webhook URL. Server validates the URL (scheme + non-private IP) and fires an unsigned probe POST (X-ARP-Probe: 1, 5s timeout) before persisting \u2014 non-2xx response \u2192 400 WEBHOOK_PROBE_FAILED, no persistence."
8237
+ ).argument("<url>", "HTTPS URL the server should POST webhook deliveries to").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option("--json", "JSON output (jq-pipeable)", false).action(async (webhookUrl, opts, cmd) => {
8238
+ try {
8239
+ const local = resolveSenderAgent("webhook url set", opts.server, opts.fromDid);
8240
+ const api = new ArpApiClient(opts.server);
8241
+ const signer = makeSigner(local);
8242
+ const out = await api.setMyWebhookConfig({ webhookUrl }, signer);
8243
+ if (opts.json) {
8244
+ console.log(formatJson(out));
8245
+ return;
8246
+ }
8247
+ console.log(import_chalk30.default.dim(`Server: ${api.serverUrl}`));
8248
+ console.log(import_chalk30.default.dim(`Signer: ${local.did}`));
8249
+ console.log(`${import_chalk30.default.green("Webhook URL set:")} ${import_chalk30.default.cyan(out.webhookUrl ?? "(cleared)")}`);
8250
+ if (!out.webhookSecretRegistered) {
8251
+ console.log(import_chalk30.default.yellow("\nNo HMAC secret registered yet. Run: heyarp webhook secret init"));
8252
+ }
8253
+ } catch (err) {
8254
+ emitActionError(err, cmd);
8255
+ process.exitCode = 1;
8256
+ }
8257
+ });
8258
+ url.command("show").description("Show the calling agent's outbound webhook URL + whether an HMAC secret is registered. Read-only.").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option("--json", "JSON output (jq-pipeable)", false).action(async (opts, cmd) => {
8259
+ try {
8260
+ const local = resolveSenderAgent("webhook url show", opts.server, opts.fromDid);
8261
+ const api = new ArpApiClient(opts.server);
8262
+ const signer = makeSigner(local);
8263
+ const out = await api.getMyWebhookConfig(signer);
8264
+ if (opts.json) {
8265
+ console.log(formatJson(out));
8266
+ return;
8267
+ }
8268
+ console.log(import_chalk30.default.dim(`Server: ${api.serverUrl}`));
8269
+ console.log(import_chalk30.default.dim(`Signer: ${local.did}`));
8270
+ console.log(`${import_chalk30.default.bold("Webhook URL:")} ${out.webhookUrl ? import_chalk30.default.cyan(out.webhookUrl) : import_chalk30.default.dim("(unset \u2014 poll/SSE only)")}`);
8271
+ console.log(`${import_chalk30.default.bold("Secret registered:")} ${out.webhookSecretRegistered ? import_chalk30.default.green("yes") : import_chalk30.default.yellow("no")}`);
8272
+ } catch (err) {
8273
+ emitActionError(err, cmd);
8274
+ process.exitCode = 1;
8275
+ }
8276
+ });
8277
+ url.command("clear").description(
8278
+ "Unset the outbound webhook URL. Disables webhook delivery on the next outbox enqueue; in-flight deliveries finish naturally. Agent reverts to poll / SSE without losing data."
8279
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option("--yes", "Skip the destructive-action confirmation prompt", false).option("--json", "JSON output (jq-pipeable)", false).action(async (opts, cmd) => {
8280
+ try {
8281
+ const local = resolveSenderAgent("webhook url clear", opts.server, opts.fromDid);
8282
+ const api = new ArpApiClient(opts.server);
8283
+ const signer = makeSigner(local);
8284
+ if (!opts.yes) {
8285
+ const answer = await promptYesNo("Clear the webhook URL? Outbound delivery will stop on next outbox enqueue.");
8286
+ if (!answer) {
8287
+ console.log(import_chalk30.default.dim("Aborted."));
8288
+ return;
8289
+ }
8290
+ }
8291
+ const out = await api.setMyWebhookConfig({ webhookUrl: null }, signer);
8292
+ if (opts.json) {
8293
+ console.log(formatJson(out));
8294
+ return;
8295
+ }
8296
+ console.log(`${import_chalk30.default.green("Webhook URL cleared.")} Agent reverts to poll / SSE for new events.`);
8297
+ } catch (err) {
8298
+ emitActionError(err, cmd);
8299
+ process.exitCode = 1;
8300
+ }
8301
+ });
8302
+ }
8303
+ var REDACTED_SECRET_MARKER = "<REDACTED \u2014 read ~/.arp/agents.json>";
8304
+ function parseNonNegativeInt(raw, fieldName) {
8305
+ if (!/^\d+$/.test(raw)) {
8306
+ throw new Error(`${fieldName}: must be a non-negative integer (got '${raw}')`);
8307
+ }
8308
+ const n = Number.parseInt(raw, 10);
8309
+ if (!Number.isSafeInteger(n) || n > 86400) {
8310
+ throw new Error(`${fieldName}: out of range (got '${raw}', max 86400)`);
8311
+ }
8312
+ return n;
8313
+ }
8314
+ function warnIfChmodFailed(result) {
8315
+ if (!result.chmodOk) {
8316
+ console.error(import_chalk30.default.red("WARNING: failed to chmod 0600 on ~/.arp/agents.json \u2014 the file may be world-readable. Check filesystem permissions and re-run."));
8317
+ }
8318
+ }
8319
+ function registerSecretSubcommands(parent) {
8320
+ const secret = parent.command("secret").description("HMAC secret used to sign outbound webhook deliveries (X-ARP-Signature header).");
8321
+ secret.command("init").description(
8322
+ "Generate the calling agent's first HMAC secret. Server returns the plaintext value ONCE; the CLI persists it to ~/.arp/agents.json under `webhookSecretB64` (chmod 600). 409 WEBHOOK_SECRET_ALREADY_REGISTERED if a secret already exists \u2014 use `webhook secret rotate` to change."
8323
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option(
8324
+ "--json",
8325
+ "JSON output (jq-pipeable). Plaintext secret is REDACTED from --json output to keep it out of shell history / CI logs \u2014 the persisted copy in ~/.arp/agents.json is the only retained source.",
8326
+ false
8327
+ ).action(async (opts, cmd) => {
8328
+ try {
8329
+ const local = resolveSenderAgent("webhook secret init", opts.server, opts.fromDid);
8330
+ const api = new ArpApiClient(opts.server);
8331
+ const signer = makeSigner(local);
8332
+ const out = await api.initMyWebhookSecret(signer);
8333
+ const writeResult = updateAgentLocal(opts.server, local.did, { webhookSecretB64: out.webhookSecretB64 });
8334
+ warnIfChmodFailed(writeResult);
8335
+ if (opts.json) {
8336
+ console.log(formatJson({ ...out, webhookSecretB64: REDACTED_SECRET_MARKER, persisted: true }));
8337
+ return;
8338
+ }
8339
+ console.log(import_chalk30.default.dim(`Server: ${api.serverUrl}`));
8340
+ console.log(import_chalk30.default.dim(`Signer: ${local.did}`));
8341
+ console.log(import_chalk30.default.green("Webhook HMAC secret registered + persisted locally."));
8342
+ console.log(import_chalk30.default.dim(" Stored in ~/.arp/agents.json under `webhookSecretB64` (chmod 600)."));
8343
+ console.log(import_chalk30.default.dim(" Your handler reads this on startup to verify X-ARP-Signature."));
8344
+ } catch (err) {
8345
+ emitActionError(err, cmd);
8346
+ process.exitCode = 1;
8347
+ }
8348
+ });
8349
+ secret.command("status").description(
8350
+ "Show the calling agent's server-side webhook-secret lifecycle: which slots are populated (`current`, `pending`, `previous`) and the post-commit grace expiry. Read-only \u2014 does NOT return any secret material. Use during rotation to verify the recipient handler picks up the new secret before commit."
8351
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option("--json", "JSON output (jq-pipeable)", false).action(async (opts, cmd) => {
8352
+ try {
8353
+ const local = resolveSenderAgent("webhook secret status", opts.server, opts.fromDid);
8354
+ const api = new ArpApiClient(opts.server);
8355
+ const signer = makeSigner(local);
8356
+ const out = await api.getMyWebhookSecretStatus(signer);
8357
+ if (opts.json) {
8358
+ console.log(formatJson(out));
8359
+ return;
8360
+ }
8361
+ console.log(import_chalk30.default.dim(`Server: ${api.serverUrl}`));
8362
+ console.log(import_chalk30.default.dim(`Signer: ${local.did}`));
8363
+ console.log(`${import_chalk30.default.bold("Current:")} ${out.currentRegistered ? import_chalk30.default.green("registered") : import_chalk30.default.yellow("NONE \u2014 run `heyarp webhook secret init`")}`);
8364
+ console.log(`${import_chalk30.default.bold("Pending:")} ${out.pendingStaged ? import_chalk30.default.cyan("staged \u2014 awaiting --commit") : import_chalk30.default.dim("\u2014")}`);
8365
+ if (out.previousActive) {
8366
+ console.log(
8367
+ `${import_chalk30.default.bold("Previous:")} ${import_chalk30.default.cyan("grace-active")} (handler should keep verifying against it until ${import_chalk30.default.cyan(out.previousSecretExpiresAt ?? "?")})`
8368
+ );
8369
+ } else {
8370
+ console.log(`${import_chalk30.default.bold("Previous:")} ${import_chalk30.default.dim("\u2014")}`);
8371
+ }
8372
+ } catch (err) {
8373
+ emitActionError(err, cmd);
8374
+ process.exitCode = 1;
8375
+ }
8376
+ });
8377
+ secret.command("rotate").description(
8378
+ "Two-phase HMAC secret rotation with a recipient-grace window. Phase 1 (--stage) primes the recipient handler with the new secret while the server keeps signing with the old one; phase 2 (--commit) flips the server-side signing key and starts a 1h grace window where the old secret is still valid. Run with no sub-flag to do both phases with --wait-before-commit between them."
8379
+ ).option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Explicit sender DID \u2014 required when more than one local agent exists").option(
8380
+ "--stage",
8381
+ "Phase 1 only \u2014 stage a new secret without flipping the server signing key. CLI writes the new secret to `webhookSecretB64Next` for the handler to pre-load.",
8382
+ false
8383
+ ).option("--commit", "Phase 2 only \u2014 flip the server signing key. Requires a prior --stage (rotationId comes from local state).", false).option(
8384
+ "--wait-before-commit <s>",
8385
+ "Auto-mode only: seconds to wait between phase 1 (stage) and phase 2 (commit). Default 30 \u2014 enough for a pm2 / systemd handler restart to pick up `webhookSecretB64Next`. Pass 0 to commit immediately (UNSAFE \u2014 guarantees a brief HMAC-verification gap unless the handler is already running in dual-key mode).",
8386
+ "30"
8387
+ ).option("--json", "JSON output for the commit-phase response", false).action(async (opts, cmd) => {
8388
+ try {
8389
+ if (opts.stage && opts.commit) {
8390
+ throw new Error("webhook secret rotate: --stage and --commit are mutually exclusive");
8391
+ }
8392
+ const local = resolveSenderAgent("webhook secret rotate", opts.server, opts.fromDid);
8393
+ const api = new ArpApiClient(opts.server);
8394
+ const signer = makeSigner(local);
8395
+ if (opts.stage) {
8396
+ await doStage(api, signer, local.did, opts.server, !!opts.json);
8397
+ return;
8398
+ }
8399
+ if (opts.commit) {
8400
+ await doCommitFromLocalState(api, signer, local.did, opts.server, !!opts.json);
8401
+ return;
8402
+ }
8403
+ const waitSeconds = parseNonNegativeInt(opts.waitBeforeCommit ?? "30", "webhook secret rotate --wait-before-commit");
8404
+ const staged = await doStage(api, signer, local.did, opts.server, !!opts.json);
8405
+ if (waitSeconds > 0) {
8406
+ console.log(import_chalk30.default.dim(`Waiting ${waitSeconds}s for the recipient handler to pick up the new secret before commit\u2026`));
8407
+ await (0, import_promises.setTimeout)(waitSeconds * 1e3);
8408
+ } else {
8409
+ console.log(
8410
+ import_chalk30.default.yellow(
8411
+ "--wait-before-commit=0: committing immediately. Recipient must already be running in dual-key mode (verifying against [current, next]) or HMAC verification will gap until the handler restarts."
8412
+ )
8413
+ );
8414
+ }
8415
+ await doCommitWithStaged(api, signer, local.did, opts.server, staged, !!opts.json);
8416
+ } catch (err) {
8417
+ emitActionError(err, cmd);
8418
+ process.exitCode = 1;
8419
+ }
8420
+ });
8421
+ }
8422
+ async function doStage(api, signer, did, server, asJson) {
8423
+ const out = await api.rotateStageMyWebhookSecret(signer);
8424
+ if (out.slot !== "pending" || !out.rotationId) {
8425
+ throw new Error(
8426
+ `webhook secret rotate-stage: unexpected server response \u2014 expected slot='pending' + rotationId, got slot='${out.slot}' rotationId='${out.rotationId ?? ""}'`
8427
+ );
8428
+ }
8429
+ const writeResult = updateAgentLocal(server, did, {
8430
+ webhookSecretB64Next: out.webhookSecretB64,
8431
+ webhookRotationId: out.rotationId
8432
+ });
8433
+ warnIfChmodFailed(writeResult);
8434
+ if (asJson) {
8435
+ console.log(formatJson({ ...out, webhookSecretB64: REDACTED_SECRET_MARKER, persisted: true }));
8436
+ } else {
8437
+ console.log(import_chalk30.default.green("Phase 1 (stage) complete:"));
8438
+ console.log(import_chalk30.default.dim(" New secret staged server-side; server still signs with the CURRENT secret."));
8439
+ console.log(import_chalk30.default.dim(" Stored locally under `webhookSecretB64Next` \u2014 your handler should now verify against BOTH current + next."));
8440
+ console.log(import_chalk30.default.dim(` rotationId: ${out.rotationId} (threaded into the matching commit call).`));
8441
+ }
8442
+ return { rotationId: out.rotationId, pendingSecretB64: out.webhookSecretB64 };
8443
+ }
8444
+ async function doCommitWithStaged(api, signer, did, server, staged, asJson) {
8445
+ const fresh = resolveSenderAgent("webhook secret rotate --commit", server, did);
8446
+ await runCommitAndSwap(api, signer, fresh, server, did, staged.rotationId, staged.pendingSecretB64, asJson);
8447
+ }
8448
+ async function doCommitFromLocalState(api, signer, did, server, asJson) {
8449
+ const fresh = resolveSenderAgent("webhook secret rotate --commit", server, did);
8450
+ if (!fresh.webhookSecretB64Next || !fresh.webhookRotationId) {
8451
+ throw new Error(
8452
+ "webhook secret rotate --commit: no staged secret found in local state. Run `webhook secret rotate --stage` first, or use the no-flag form to do both phases in one invocation."
8453
+ );
8454
+ }
8455
+ await runCommitAndSwap(api, signer, fresh, server, did, fresh.webhookRotationId, fresh.webhookSecretB64Next, asJson);
8456
+ }
8457
+ async function runCommitAndSwap(api, signer, fresh, server, did, rotationId, pendingSecretB64, asJson) {
8458
+ let out;
8459
+ try {
8460
+ out = await api.rotateCommitMyWebhookSecret({ rotationId }, signer);
8461
+ } catch (err) {
8462
+ if (err instanceof ApiError && err.payload.code === "WEBHOOK_SECRET_ROTATION_ID_MISMATCH") {
8463
+ throw new Error(
8464
+ `webhook secret rotate --commit: rotationId mismatch (a concurrent --stage ran). Run \`webhook secret rotate --stage\` again, then commit with the fresh token. Local state PRESERVED \u2014 your current secret still works.`
8465
+ );
8466
+ }
8467
+ throw err;
8468
+ }
8469
+ const writeResult = updateAgentLocal(server, did, {
8470
+ webhookSecretB64Previous: fresh.webhookSecretB64,
8471
+ webhookSecretB64: pendingSecretB64,
8472
+ webhookSecretB64Next: void 0,
8473
+ webhookRotationId: void 0,
8474
+ webhookSecretPreviousExpiresAt: out.previousSecretExpiresAt
8475
+ });
8476
+ warnIfChmodFailed(writeResult);
8477
+ if (asJson) {
8478
+ console.log(formatJson(out));
8479
+ return;
8480
+ }
8481
+ console.log(import_chalk30.default.green("Phase 2 (commit) complete:"));
8482
+ console.log(import_chalk30.default.dim(" Server now signs with the NEW secret. Old secret kept in `webhookSecretB64Previous` for the grace window."));
8483
+ console.log(` ${import_chalk30.default.bold("Grace expires:")} ${import_chalk30.default.cyan(out.previousSecretExpiresAt)} \u2014 after this, your handler can drop the previous secret on next restart.`);
8484
+ }
8485
+ async function promptYesNo(question) {
8486
+ process.stdout.write(`${question} (y/N): `);
8487
+ return new Promise((resolve2) => {
8488
+ const onData = (chunk) => {
8489
+ const answer = chunk.toString("utf8").trim().toLowerCase();
8490
+ process.stdin.pause();
8491
+ process.stdin.off("data", onData);
8492
+ resolve2(answer === "y" || answer === "yes");
8493
+ };
8494
+ process.stdin.resume();
8495
+ process.stdin.on("data", onData);
8496
+ });
8497
+ }
8498
+
6829
8499
  // src/commands/whoami.ts
6830
- var import_chalk28 = __toESM(require("chalk"));
8500
+ var import_chalk31 = __toESM(require("chalk"));
6831
8501
  init_api();
6832
8502
  function registerWhoamiCommand(root) {
6833
8503
  root.command("whoami").description(
@@ -6861,10 +8531,10 @@ function registerWhoamiCommand(root) {
6861
8531
  if (opts.json) {
6862
8532
  console.log(formatJson(localJson));
6863
8533
  } else {
6864
- console.log(import_chalk28.default.bold("Local agent:"));
6865
- console.log(` DID: ${import_chalk28.default.cyan(local.did)}`);
6866
- console.log(` Settlement pubkey: ${import_chalk28.default.cyan(local.settlementPublicKeyB58)}`);
6867
- console.log(` Identity pubkey: ${import_chalk28.default.cyan(local.identityPublicKeyB58)}`);
8534
+ console.log(import_chalk31.default.bold("Local agent:"));
8535
+ console.log(` DID: ${import_chalk31.default.cyan(local.did)}`);
8536
+ console.log(` Settlement pubkey: ${import_chalk31.default.cyan(local.settlementPublicKeyB58)}`);
8537
+ console.log(` Identity pubkey: ${import_chalk31.default.cyan(local.identityPublicKeyB58)}`);
6868
8538
  console.log(` Key mode: ${local.keyMode}`);
6869
8539
  if (local.name) console.log(` Name: ${local.name}`);
6870
8540
  }
@@ -6876,24 +8546,24 @@ function registerWhoamiCommand(root) {
6876
8546
  if (opts.json) {
6877
8547
  console.log(formatJson({ local: localJson, server: agent }));
6878
8548
  } else {
6879
- console.log(import_chalk28.default.dim(`Server: ${api.serverUrl}`));
6880
- console.log(import_chalk28.default.bold("\nLocal agent:"));
6881
- console.log(` DID: ${import_chalk28.default.cyan(local.did)}`);
6882
- console.log(` Settlement pubkey: ${import_chalk28.default.cyan(local.settlementPublicKeyB58)}`);
6883
- console.log(` Identity pubkey: ${import_chalk28.default.cyan(local.identityPublicKeyB58)}`);
6884
- console.log(import_chalk28.default.bold("\nServer profile:"));
8549
+ console.log(import_chalk31.default.dim(`Server: ${api.serverUrl}`));
8550
+ console.log(import_chalk31.default.bold("\nLocal agent:"));
8551
+ console.log(` DID: ${import_chalk31.default.cyan(local.did)}`);
8552
+ console.log(` Settlement pubkey: ${import_chalk31.default.cyan(local.settlementPublicKeyB58)}`);
8553
+ console.log(` Identity pubkey: ${import_chalk31.default.cyan(local.identityPublicKeyB58)}`);
8554
+ console.log(import_chalk31.default.bold("\nServer profile:"));
6885
8555
  console.log(formatJson(agent));
6886
8556
  }
6887
8557
  } catch (err) {
6888
- console.error(formatActionError(err, cmd));
8558
+ emitActionError(err, cmd);
6889
8559
  process.exitCode = 1;
6890
8560
  }
6891
8561
  });
6892
8562
  }
6893
8563
 
6894
8564
  // src/commands/work.ts
6895
- var import_sdk16 = require("@heyanon-arp/sdk");
6896
- var import_chalk29 = __toESM(require("chalk"));
8565
+ var import_sdk17 = require("@heyanon-arp/sdk");
8566
+ var import_chalk32 = __toESM(require("chalk"));
6897
8567
  init_api();
6898
8568
  function registerWorkCommands(root) {
6899
8569
  const cmd = root.command("work").description("Work envelopes inside an ACCEPTED delegation: request / respond");
@@ -6935,17 +8605,17 @@ async function runRequest(recipientDid, delegationId, opts) {
6935
8605
  params
6936
8606
  };
6937
8607
  const body = { type: "work_request", content };
6938
- console.log(import_chalk29.default.dim(`Server: ${api.serverUrl}`));
6939
- console.log(import_chalk29.default.dim(`Sender: ${sender.did}`));
6940
- console.log(import_chalk29.default.dim(`Recipient: ${recipientDid}`));
6941
- console.log(import_chalk29.default.dim(`Delegation: ${delegationId}`));
6942
- console.log(import_chalk29.default.dim(`Request id: ${requestId}`));
8608
+ console.log(import_chalk32.default.dim(`Server: ${api.serverUrl}`));
8609
+ console.log(import_chalk32.default.dim(`Sender: ${sender.did}`));
8610
+ console.log(import_chalk32.default.dim(`Recipient: ${recipientDid}`));
8611
+ console.log(import_chalk32.default.dim(`Delegation: ${delegationId}`));
8612
+ console.log(import_chalk32.default.dim(`Request id: ${requestId}`));
6943
8613
  const result = await sendWorkEnvelope({ api, sender, recipientDid, body, ttlSeconds, verbose: opts.verbose, server: opts.server });
6944
8614
  printIngestResult4(result);
6945
- console.log(import_chalk29.default.dim(`
8615
+ console.log(import_chalk32.default.dim(`
6946
8616
  The payee can reply with:`));
6947
- console.log(import_chalk29.default.dim(` heyarp work respond ${result.relationshipId} ${delegationId} ${requestId} --output '<json>'`));
6948
- console.log(import_chalk29.default.dim(` heyarp work respond ${result.relationshipId} ${delegationId} ${requestId} --error CODE:message`));
8617
+ console.log(import_chalk32.default.dim(` heyarp work respond ${result.relationshipId} ${delegationId} ${requestId} --output '<json>'`));
8618
+ console.log(import_chalk32.default.dim(` heyarp work respond ${result.relationshipId} ${delegationId} ${requestId} --error CODE:message`));
6949
8619
  }
6950
8620
  function registerRespond(parent) {
6951
8621
  parent.command("respond").description("Send a work_response under <relationship-id> for <delegation-id> / <request-id>. Payee-only.").argument("<relationship-id>", "Relationship UUID").argument("<delegation-id>", "Parent delegation id (UUID)").argument("<request-id>", "Request id supplied on the work_request").option("--server <url>", "Override ARP server base URL").option("--from-did <did>", "Sender DID \u2014 required only if multiple agents are registered against this server").option("--output <json>", "Success payload as a JSON object literal. Mutually exclusive with --error and --output-file.").option(
@@ -6970,13 +8640,13 @@ async function runRespond(relationshipId, delegationId, requestId, opts) {
6970
8640
  ...responsePayload
6971
8641
  };
6972
8642
  const body = { type: "work_response", content };
6973
- console.log(import_chalk29.default.dim(`Server: ${api.serverUrl}`));
6974
- console.log(import_chalk29.default.dim(`Sender: ${sender.did}`));
6975
- console.log(import_chalk29.default.dim(`Recipient: ${recipientDid}`));
6976
- console.log(import_chalk29.default.dim(`Relationship: ${relationshipId}`));
6977
- console.log(import_chalk29.default.dim(`Delegation: ${delegationId}`));
6978
- console.log(import_chalk29.default.dim(`Request id: ${requestId}`));
6979
- console.log(import_chalk29.default.dim(`Outcome: ${responsePayload.output ? "success" : "error"}`));
8643
+ console.log(import_chalk32.default.dim(`Server: ${api.serverUrl}`));
8644
+ console.log(import_chalk32.default.dim(`Sender: ${sender.did}`));
8645
+ console.log(import_chalk32.default.dim(`Recipient: ${recipientDid}`));
8646
+ console.log(import_chalk32.default.dim(`Relationship: ${relationshipId}`));
8647
+ console.log(import_chalk32.default.dim(`Delegation: ${delegationId}`));
8648
+ console.log(import_chalk32.default.dim(`Request id: ${requestId}`));
8649
+ console.log(import_chalk32.default.dim(`Outcome: ${responsePayload.output ? "success" : "error"}`));
6980
8650
  const result = await sendWorkEnvelope({ api, sender, recipientDid, body, ttlSeconds, verbose: opts.verbose, server: opts.server });
6981
8651
  printIngestResult4(result);
6982
8652
  }
@@ -6984,25 +8654,25 @@ async function sendWorkEnvelope(args) {
6984
8654
  const nextSequence = (args.sender.lastSenderSequence ?? 0) + 1;
6985
8655
  const protectedBlock = {
6986
8656
  protocol_version: "arp/0.1",
6987
- purpose: import_sdk16.Purpose.ENVELOPE,
6988
- message_id: (0, import_sdk16.uuidV4)(),
8657
+ purpose: import_sdk17.Purpose.ENVELOPE,
8658
+ message_id: (0, import_sdk17.uuidV4)(),
6989
8659
  sender_did: args.sender.did,
6990
8660
  recipient_did: args.recipientDid,
6991
8661
  relationship_id: null,
6992
8662
  sender_sequence: nextSequence,
6993
- sender_nonce: (0, import_sdk16.senderNonce)(),
6994
- timestamp: (0, import_sdk16.rfc3339)(),
6995
- expires_at: (0, import_sdk16.expiresAt)(args.ttlSeconds),
8663
+ sender_nonce: (0, import_sdk17.senderNonce)(),
8664
+ timestamp: (0, import_sdk17.rfc3339)(),
8665
+ expires_at: (0, import_sdk17.expiresAt)(args.ttlSeconds),
6996
8666
  delivery_id: null
6997
8667
  };
6998
8668
  const signer = makeSigner(args.sender);
6999
- const envelope = (0, import_sdk16.signEnvelope)({
8669
+ const envelope = (0, import_sdk17.signEnvelope)({
7000
8670
  protected: protectedBlock,
7001
8671
  body: args.body,
7002
8672
  identitySecretKey: signer.identitySecretKey
7003
8673
  });
7004
8674
  if (args.verbose) {
7005
- console.log(import_chalk29.default.bold("\nEnvelope (pre-send):"));
8675
+ console.log(import_chalk32.default.bold("\nEnvelope (pre-send):"));
7006
8676
  console.log(formatJson(envelope));
7007
8677
  }
7008
8678
  try {
@@ -7042,12 +8712,12 @@ async function resolveResponseRecipient(cmdName, api, signer, args) {
7042
8712
  );
7043
8713
  }
7044
8714
  function printIngestResult4(result) {
7045
- console.log(import_chalk29.default.green("\nDelivered."));
7046
- console.log(`${import_chalk29.default.bold("Event id")}: ${import_chalk29.default.cyan(result.eventId)}`);
7047
- console.log(`${import_chalk29.default.bold("Relationship id")}: ${import_chalk29.default.cyan(result.relationshipId)}`);
7048
- console.log(`${import_chalk29.default.bold("Chain index")}: ${import_chalk29.default.cyan(String(result.relationshipEventIndex))}`);
7049
- console.log(`${import_chalk29.default.bold("Server timestamp")}: ${import_chalk29.default.cyan(result.serverTimestamp)}`);
7050
- console.log(`${import_chalk29.default.bold("Server event hash")}: ${import_chalk29.default.cyan(result.serverEventHash)}`);
8715
+ console.log(import_chalk32.default.green("\nDelivered."));
8716
+ console.log(`${import_chalk32.default.bold("Event id")}: ${import_chalk32.default.cyan(result.eventId)}`);
8717
+ console.log(`${import_chalk32.default.bold("Relationship id")}: ${import_chalk32.default.cyan(result.relationshipId)}`);
8718
+ console.log(`${import_chalk32.default.bold("Chain index")}: ${import_chalk32.default.cyan(String(result.relationshipEventIndex))}`);
8719
+ console.log(`${import_chalk32.default.bold("Server timestamp")}: ${import_chalk32.default.cyan(result.serverTimestamp)}`);
8720
+ console.log(`${import_chalk32.default.bold("Server event hash")}: ${import_chalk32.default.cyan(result.serverEventHash)}`);
7051
8721
  }
7052
8722
  function parseJsonObject(cmdName, flagName, raw) {
7053
8723
  let parsed;
@@ -7085,13 +8755,13 @@ function parseParamsInput(cmdName, opts) {
7085
8755
  return parseJsonObject(cmdName, "--params", opts.params ?? "{}");
7086
8756
  }
7087
8757
  function readJsonObjectFile(cmdName, flagName, path) {
7088
- const { existsSync: existsSync6, readFileSync: readFileSync8 } = require("fs");
7089
- if (!existsSync6(path)) {
8758
+ const { existsSync: existsSync7, readFileSync: readFileSync9 } = require("fs");
8759
+ if (!existsSync7(path)) {
7090
8760
  throw new Error(`${cmdName}: ${flagName} file not found at ${path}`);
7091
8761
  }
7092
8762
  let raw;
7093
8763
  try {
7094
- raw = readFileSync8(path, "utf8");
8764
+ raw = readFileSync9(path, "utf8");
7095
8765
  } catch (err) {
7096
8766
  const detail = err instanceof Error ? err.message : String(err);
7097
8767
  throw new Error(`${cmdName}: failed to read ${flagName} (${path}): ${detail}`);
@@ -7139,7 +8809,7 @@ function parseTtl7(cmdName, raw) {
7139
8809
  }
7140
8810
  function parseRequestId(cmdName, raw) {
7141
8811
  if (raw === void 0 || raw === "") {
7142
- return (0, import_sdk16.uuidV4)();
8812
+ return (0, import_sdk17.uuidV4)();
7143
8813
  }
7144
8814
  if (raw.length === 0) {
7145
8815
  throw new Error(`${cmdName}: --request-id must be a non-empty string`);
@@ -7160,7 +8830,7 @@ function requireDid4(cmdName, did, label) {
7160
8830
  }
7161
8831
 
7162
8832
  // src/commands/work-list.ts
7163
- var import_chalk30 = __toESM(require("chalk"));
8833
+ var import_chalk33 = __toESM(require("chalk"));
7164
8834
  init_api();
7165
8835
  var ALLOWED_STATES5 = /* @__PURE__ */ new Set(["requested", "responded"]);
7166
8836
  function registerWorkListCommand(root) {
@@ -7187,9 +8857,9 @@ async function runWorkList(relationshipId, opts) {
7187
8857
  const api = new ArpApiClient(opts.server);
7188
8858
  const sender = resolveSenderAgent("work-list", opts.server, opts.fromDid);
7189
8859
  if (!opts.json) {
7190
- console.log(import_chalk30.default.dim(`Server: ${api.serverUrl}`));
7191
- console.log(import_chalk30.default.dim(`Signer: ${sender.did}`));
7192
- console.log(import_chalk30.default.dim(`Relationship: ${relationshipId}`));
8860
+ console.log(import_chalk33.default.dim(`Server: ${api.serverUrl}`));
8861
+ console.log(import_chalk33.default.dim(`Signer: ${sender.did}`));
8862
+ console.log(import_chalk33.default.dim(`Relationship: ${relationshipId}`));
7193
8863
  }
7194
8864
  const query = { limit };
7195
8865
  if (state) query.state = state;
@@ -7202,7 +8872,7 @@ async function runWorkList(relationshipId, opts) {
7202
8872
  return;
7203
8873
  }
7204
8874
  if (rows.length === 0) {
7205
- console.log(import_chalk30.default.dim("\n(no work-logs for this relationship)"));
8875
+ console.log(import_chalk33.default.dim("\n(no work-logs for this relationship)"));
7206
8876
  return;
7207
8877
  }
7208
8878
  console.log("");
@@ -7219,36 +8889,36 @@ async function runWorkList(relationshipId, opts) {
7219
8889
  }));
7220
8890
  }
7221
8891
  const lastId = rows[rows.length - 1].id;
7222
- console.log(import_chalk30.default.dim(`
8892
+ console.log(import_chalk33.default.dim(`
7223
8893
  ${rows.length} work-log row(s). Paginate with --after ${lastId}.`));
7224
8894
  }
7225
8895
  function formatWorkLogLine(w, selfDid, opts = {}) {
7226
8896
  const delegationPart = opts.fullIds ? w.delegationId : idHead4(w.delegationId);
7227
8897
  const requestPart = opts.fullIds ? w.requestId : truncate4(w.requestId, 16);
7228
- const id = import_chalk30.default.bold(`${delegationPart}/${requestPart}`);
8898
+ const id = import_chalk33.default.bold(`${delegationPart}/${requestPart}`);
7229
8899
  const state = colorState4(w.state).padEnd(stateColumnWidth4());
7230
8900
  const peerCallerHead = opts.fullIds ? w.callerDid : didHead6(w.callerDid);
7231
8901
  const peerPayeeHead = opts.fullIds ? w.payeeDid : didHead6(w.payeeDid);
7232
- const direction = w.callerDid === selfDid ? `${import_chalk30.default.bold("me")} \u2192 ${import_chalk30.default.dim(peerPayeeHead)}` : `${import_chalk30.default.dim(peerCallerHead)} \u2192 ${import_chalk30.default.bold("me")}`;
8902
+ const direction = w.callerDid === selfDid ? `${import_chalk33.default.bold("me")} \u2192 ${import_chalk33.default.dim(peerPayeeHead)}` : `${import_chalk33.default.dim(peerCallerHead)} \u2192 ${import_chalk33.default.bold("me")}`;
7233
8903
  const outcome = formatOutcome(w);
7234
8904
  return `${id} ${state} ${direction} ${outcome}`;
7235
8905
  }
7236
8906
  function colorState4(s) {
7237
8907
  switch (s) {
7238
8908
  case "requested":
7239
- return import_chalk30.default.yellow("requested");
8909
+ return import_chalk33.default.yellow("requested");
7240
8910
  case "responded":
7241
- return import_chalk30.default.green("responded");
8911
+ return import_chalk33.default.green("responded");
7242
8912
  }
7243
8913
  }
7244
8914
  function stateColumnWidth4() {
7245
8915
  return 9;
7246
8916
  }
7247
8917
  function formatOutcome(w) {
7248
- if (w.state === "requested") return import_chalk30.default.dim("(in flight)");
7249
- if (w.responseError) return import_chalk30.default.red(`error ${w.responseError.code}: ${truncate4(w.responseError.message, 32)}`);
7250
- if (w.responseOutput) return import_chalk30.default.cyan("ok");
7251
- return import_chalk30.default.dim("(empty response)");
8918
+ if (w.state === "requested") return import_chalk33.default.dim("(in flight)");
8919
+ if (w.responseError) return import_chalk33.default.red(`error ${w.responseError.code}: ${truncate4(w.responseError.message, 32)}`);
8920
+ if (w.responseOutput) return import_chalk33.default.cyan("ok");
8921
+ return import_chalk33.default.dim("(empty response)");
7252
8922
  }
7253
8923
  function idHead4(id) {
7254
8924
  if (id.length <= 12) return id;
@@ -7292,11 +8962,14 @@ async function checkForUpdates() {
7292
8962
  async function main() {
7293
8963
  void checkForUpdates();
7294
8964
  const program = new import_commander.Command();
7295
- program.name("heyarp").description("ARP \u2014 Agent Relationship Protocol CLI (talks to apps/arp-server)").version(package_default.version).option(
7296
- "--trace",
7297
- "Surface stack traces and error details on failure.",
7298
- false
7299
- );
8965
+ program.name("heyarp").description("ARP \u2014 Agent Relationship Protocol CLI (talks to apps/arp-server)").version(package_default.version).option("--trace", "Surface stack traces and error details on failure.", false);
8966
+ program.exitOverride();
8967
+ program.configureOutput({
8968
+ writeErr: (str) => {
8969
+ if (process.argv.includes("--json")) return;
8970
+ process.stderr.write(str);
8971
+ }
8972
+ });
7300
8973
  registerConfigCommand(program);
7301
8974
  registerGuideCommand(program);
7302
8975
  registerHomesCommand(program);
@@ -7307,6 +8980,7 @@ async function main() {
7307
8980
  registerDidDocCommand(program);
7308
8981
  registerDoctorCommand(program);
7309
8982
  registerEscrowCommands(program);
8983
+ registerExamplesCommand(program);
7310
8984
  registerWhoamiCommand(program);
7311
8985
  registerLifecycleCommands(program);
7312
8986
  registerRotateCommand(program);
@@ -7328,16 +9002,24 @@ async function main() {
7328
9002
  registerReceiptsCommand(program);
7329
9003
  registerMemoryCommands(program);
7330
9004
  registerWalletCommands(program);
9005
+ registerWebhookCommand(program);
9006
+ registerSettlementCommands(program);
7331
9007
  try {
7332
9008
  await program.parseAsync(process.argv);
7333
9009
  } catch (err) {
7334
- const verbose = !!program.opts().trace;
7335
- if (err instanceof ApiError) {
7336
- console.error(formatApiError(err.payload, verbose));
7337
- } else {
7338
- console.error(formatGenericError(err, verbose));
7339
- }
7340
- process.exit(1);
9010
+ const cerr = err;
9011
+ const isCommanderError = typeof cerr.code === "string" && cerr.code.startsWith("commander.");
9012
+ if (isCommanderError && cerr.exitCode === 0) {
9013
+ process.exit(0);
9014
+ }
9015
+ const json = process.argv.includes("--json");
9016
+ const verbose = process.argv.includes("--trace");
9017
+ const exitCode = typeof cerr.exitCode === "number" && cerr.exitCode !== 0 ? cerr.exitCode : 1;
9018
+ if (isCommanderError && !json) {
9019
+ process.exit(exitCode);
9020
+ }
9021
+ emitError(err, { json, verbose });
9022
+ process.exit(exitCode);
7341
9023
  }
7342
9024
  }
7343
9025
  process.on("unhandledRejection", (reason) => {