@thirdfy/agent-cli 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,8 +42,8 @@ export THIRDFY_OWNER_SESSION_TOKEN="..."
42
42
 
43
43
  thirdfy-agent actions --agent-api-key "$AGENT_API_KEY" --json
44
44
  thirdfy-agent profile init --profile personal --json
45
- thirdfy-agent preflight --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountIn":"1000000"}' --estimated-amount-usd 25 --json
46
- thirdfy-agent run --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountIn":"1000000"}' --estimated-amount-usd 25 --json
45
+ thirdfy-agent preflight --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}' --estimated-amount-usd 25 --json
46
+ thirdfy-agent run --agent-api-key "$AGENT_API_KEY" --action swap --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}' --estimated-amount-usd 25 --json
47
47
  thirdfy-agent intent-status --intent-id "<intentId>" --json
48
48
  ```
49
49
 
@@ -51,12 +51,37 @@ By default, the CLI targets production (`https://api.thirdfy.com`). Use `--api-b
51
51
 
52
52
  ## Auth model
53
53
 
54
- - `AGENT_API_KEY`: execution identity and policy-aware action access (`actions --agent-api-key`, `preflight`, `run`)
55
- - `THIRDFY_AUTH_TOKEN`: Privy-native owner/account auth (`agent register`, `agent key *`, `delegation *`, `credentials *`, `credits balance`)
56
- - `THIRDFY_OWNER_SESSION_TOKEN`: wallet-sign owner auth session (`agent register`, `agent key *`)
54
+ - `AGENT_API_KEY`: execution identity for `preflight`, `run`, `self-exec`, and policy-aware discovery.
55
+ - `THIRDFY_AUTH_TOKEN`: owner/account auth for onboarding and delegation commands.
56
+ - `THIRDFY_OWNER_SESSION_TOKEN`: wallet-sign owner session for onboarding and owner operations.
57
+
58
+ CLI precedence now supports login/config defaults:
59
+
60
+ 1. explicit flag
61
+ 2. environment variable
62
+ 3. `~/.thirdfy/config.json` (from `thirdfy-agent login` / `config set`)
57
63
 
58
64
  Execution-only workflows can run with only `AGENT_API_KEY` after onboarding is complete.
59
65
 
66
+ ### Login / logout / config UX
67
+
68
+ ```bash
69
+ # Store auth defaults once
70
+ thirdfy-agent login --auth-token "$THIRDFY_AUTH_TOKEN" --agent-api-key "$AGENT_API_KEY" --json
71
+
72
+ # Inspect config
73
+ thirdfy-agent whoami --json
74
+ thirdfy-agent config set --key runMode --value self --json
75
+
76
+ # Local-only logout (default)
77
+ thirdfy-agent logout --json
78
+
79
+ # Local clear + server owner-session revoke (if present)
80
+ thirdfy-agent logout --all --json
81
+ ```
82
+
83
+ `logout --all` is provider-agnostic: it attempts owner-session revocation when a session token exists, then always clears local credentials.
84
+
60
85
  ### Wallet-sign onboarding path
61
86
 
62
87
  ```bash
@@ -85,6 +110,12 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
85
110
  - `--run-mode self` -> self-custody rail (`build-tx` flow; default for `profile=personal`)
86
111
  - `--run-mode hybrid` -> self-custody + governance mirror metadata (default for `profile=builder`)
87
112
 
113
+ Auth expectations by mode:
114
+
115
+ - `self`: requires execution identity (`agentApiKey`) today; keyless self lane is blocked with deterministic `SELF_OPEN_DISABLED`.
116
+ - `hybrid`: requires execution identity and governed mirror semantics.
117
+ - `thirdfy`: requires execution identity and delegated governance path.
118
+
88
119
  Selection precedence:
89
120
 
90
121
  1. `--run-mode`
@@ -102,6 +133,28 @@ thirdfy-agent whoami --json
102
133
 
103
134
  `profile use --profile <name>` resets run mode to that profile default unless `--run-mode` is explicitly provided.
104
135
 
136
+ ## Swap amount contract (agent-safe)
137
+
138
+ For `--action swap`, CLI enforces an explicit amount unit contract:
139
+
140
+ - Provide exactly one of:
141
+ - `amountInRaw` (base units integer string), or
142
+ - `amountInHuman` + `tokenInDecimals` (CLI converts to `amountInRaw`)
143
+ - Legacy `amountIn` is accepted only as raw integer compatibility input.
144
+ - Ambiguous payloads (both/none) fail with `AMOUNT_UNIT_AMBIGUOUS`.
145
+
146
+ Examples:
147
+
148
+ ```bash
149
+ # Raw/base units
150
+ thirdfy-agent run --agent-api-key "$AGENT_API_KEY" --action swap \
151
+ --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000","chainId":8453}' --json
152
+
153
+ # Human units + decimals
154
+ thirdfy-agent run --agent-api-key "$AGENT_API_KEY" --action swap \
155
+ --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInHuman":"1.5","tokenInDecimals":6,"chainId":8453}' --json
156
+ ```
157
+
105
158
  ## Jeff shorthand rail
106
159
 
107
160
  Jeff-friendly aliases map directly to core commands:
@@ -118,6 +171,8 @@ Jeff-friendly aliases map directly to core commands:
118
171
  - Profiles: `profile init`, `profile use`, `profile show`, `whoami`
119
172
  - Onboarding: `agent auth challenge`, `agent auth verify`, `agent register`, `agent key rotate`, `agent key revoke`
120
173
  - Governance readiness: `delegation create`, `delegation activate`, `delegation status`, `credentials upsert`, `credentials status`
174
+ - Delegation operations: `delegation show`, `delegation inspect`, `delegation revoke`
175
+ - Delegation execution helpers: `delegation balance`, `delegation redeem`
121
176
  - Account: `credits balance`
122
177
 
123
178
  ## Delegation lifecycle (canonical)
@@ -149,8 +204,53 @@ thirdfy-agent delegation status \
149
204
  --agent-key "0xYOUR_AGENT_KEY" \
150
205
  --verify \
151
206
  --json
207
+
208
+ thirdfy-agent delegation inspect \
209
+ --auth-token "$THIRDFY_AUTH_TOKEN" \
210
+ --agent-key "0xYOUR_AGENT_KEY" \
211
+ --verify \
212
+ --json
213
+
214
+ thirdfy-agent delegation revoke \
215
+ --auth-token "$THIRDFY_AUTH_TOKEN" \
216
+ --agent-key "0xYOUR_AGENT_KEY" \
217
+ --reason "user_revoked" \
218
+ --json
219
+
220
+ thirdfy-agent delegation balance \
221
+ --auth-token "$THIRDFY_AUTH_TOKEN" \
222
+ --agent-key "0xYOUR_AGENT_KEY" \
223
+ --json
224
+
225
+ thirdfy-agent delegation redeem \
226
+ --run-mode thirdfy \
227
+ --agent-api-key "$AGENT_API_KEY" \
228
+ --action swap \
229
+ --params '{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}' \
230
+ --estimated-amount-usd 5 \
231
+ --json
152
232
  ```
153
233
 
234
+ ### MetaMask custodial-compatible lifecycle (new)
235
+
236
+ ```bash
237
+ thirdfy-agent delegation init-custodial \
238
+ --auth-token "$THIRDFY_AUTH_TOKEN" \
239
+ --chain-id 8453 \
240
+ --json
241
+
242
+ thirdfy-agent delegation custodial-grant \
243
+ --auth-token "$THIRDFY_AUTH_TOKEN" \
244
+ --agent-key "0xYOUR_AGENT_KEY" \
245
+ --token-address "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" \
246
+ --max-usd-per-day 250 \
247
+ --json
248
+ ```
249
+
250
+ The CLI keeps compatibility with legacy `delegation create` + `delegation activate` during migration.
251
+
252
+ `delegation show` is a direct alias to `delegation status` for gator-style operator ergonomics.
253
+
154
254
  ## More docs
155
255
 
156
256
  - OpenClaw CLI onboarding flow:
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { randomUUID } from 'crypto';
4
+ import { spawnSync } from 'child_process';
4
5
  import fs from 'fs';
5
6
  import os from 'os';
6
7
  import path from 'path';
@@ -105,6 +106,15 @@ async function main() {
105
106
  case 'config get':
106
107
  await commandProfileShow(context, subFlags);
107
108
  return;
109
+ case 'config set':
110
+ await commandConfigSet(context, subFlags);
111
+ return;
112
+ case 'login':
113
+ await commandLogin(context, subFlags);
114
+ return;
115
+ case 'logout':
116
+ await commandLogout(context, subFlags);
117
+ return;
108
118
  case 'whoami':
109
119
  await commandProfileShow(context, subFlags);
110
120
  return;
@@ -133,12 +143,33 @@ async function main() {
133
143
  case 'delegation create':
134
144
  await commandDelegationCreate(context, subFlags, capabilities);
135
145
  return;
146
+ case 'delegation init-custodial':
147
+ await commandDelegationInitCustodial(context, subFlags, capabilities);
148
+ return;
149
+ case 'delegation custodial-grant':
150
+ await commandDelegationCustodialGrant(context, subFlags, capabilities);
151
+ return;
136
152
  case 'delegation activate':
137
153
  await commandDelegationActivate(context, subFlags, capabilities);
138
154
  return;
139
155
  case 'delegation status':
140
156
  await commandDelegationStatus(context, subFlags, capabilities);
141
157
  return;
158
+ case 'delegation show':
159
+ await commandDelegationStatus(context, subFlags, capabilities);
160
+ return;
161
+ case 'delegation inspect':
162
+ await commandDelegationInspect(context, subFlags, capabilities);
163
+ return;
164
+ case 'delegation balance':
165
+ await commandDelegationBalance(context, subFlags, capabilities);
166
+ return;
167
+ case 'delegation revoke':
168
+ await commandDelegationRevoke(context, subFlags, capabilities);
169
+ return;
170
+ case 'delegation redeem':
171
+ await commandDelegationRedeem(context, subFlags, capabilities);
172
+ return;
142
173
  case 'credentials upsert':
143
174
  await commandCredentialsUpsert(context, subFlags, capabilities);
144
175
  return;
@@ -148,6 +179,9 @@ async function main() {
148
179
  case 'agent register':
149
180
  await commandAgentRegister(context, subFlags, capabilities);
150
181
  return;
182
+ case 'developer bootstrap':
183
+ await commandDeveloperBootstrap(context, subFlags, capabilities);
184
+ return;
151
185
  default:
152
186
  break;
153
187
  }
@@ -182,6 +216,9 @@ async function main() {
182
216
  case 'run':
183
217
  await commandRun(context, subFlags, capabilities);
184
218
  return;
219
+ case 'self-exec':
220
+ await commandSelfExec(context, subFlags, capabilities);
221
+ return;
185
222
  case 'intent-status':
186
223
  await commandIntentStatus(context, subFlags, capabilities);
187
224
  return;
@@ -256,6 +293,162 @@ function parseJsonFlag(flags, key) {
256
293
  }
257
294
  }
258
295
 
296
+ function shouldApplySwapAmountContract(actionName) {
297
+ const action = String(actionName || '').trim().toLowerCase();
298
+ return action === 'swap' || /(^|:|-)swap$/.test(action);
299
+ }
300
+
301
+ function createCliError(code, message, payload = {}) {
302
+ const error = new Error(message);
303
+ error.payload = {
304
+ error: code,
305
+ blockedReason: code,
306
+ blockedStage: 'client_validation',
307
+ ...payload,
308
+ };
309
+ return error;
310
+ }
311
+
312
+ function normalizeIntegerRawAmount(input, code = 'AMOUNT_UNIT_INVALID') {
313
+ const raw = String(input ?? '').trim();
314
+ if (!/^\d+$/.test(raw)) {
315
+ throw createCliError(code, 'amountInRaw must be an integer base-unit string (example: "1000000").');
316
+ }
317
+ return raw;
318
+ }
319
+
320
+ function decimalToRawUnits(amountHuman, decimals) {
321
+ const s = String(amountHuman ?? '').trim();
322
+ if (!/^\d+(\.\d+)?$/.test(s)) {
323
+ throw createCliError('AMOUNT_UNIT_INVALID', 'amountInHuman must be a decimal string (example: "100.5").');
324
+ }
325
+ const [whole, frac = ''] = s.split('.');
326
+ if (decimals === 0) {
327
+ if (frac && /[1-9]/.test(frac)) {
328
+ throw createCliError(
329
+ 'AMOUNT_PRECISION_EXCESS',
330
+ 'amountInHuman has fractional digits but tokenInDecimals is 0 (example: use "2" not "2.5").'
331
+ );
332
+ }
333
+ } else if (frac.length > decimals && /[1-9]/.test(frac.slice(decimals))) {
334
+ throw createCliError(
335
+ 'AMOUNT_PRECISION_EXCESS',
336
+ `amountInHuman has more than ${decimals} fractional digit(s) for tokenInDecimals; round or use amountInRaw.`
337
+ );
338
+ }
339
+ const fracNorm = (frac + '0'.repeat(decimals)).slice(0, decimals);
340
+ const wholePart = BigInt(whole || '0') * 10n ** BigInt(decimals);
341
+ const fracPart = BigInt(fracNorm || '0');
342
+ return (wholePart + fracPart).toString();
343
+ }
344
+
345
+ function normalizeSwapAmountContract(params) {
346
+ const hasRaw = params.amountInRaw !== undefined && params.amountInRaw !== null && String(params.amountInRaw).trim() !== '';
347
+ const hasHuman = params.amountInHuman !== undefined && params.amountInHuman !== null && String(params.amountInHuman).trim() !== '';
348
+ const hasLegacy = params.amountIn !== undefined && params.amountIn !== null && String(params.amountIn).trim() !== '';
349
+ const populated = [hasRaw, hasHuman, hasLegacy].filter(Boolean).length;
350
+ if (populated === 0) {
351
+ throw createCliError(
352
+ 'AMOUNT_UNIT_MISSING',
353
+ 'Missing swap amount: provide one of amountInRaw, amountInHuman, or legacy amountIn.'
354
+ );
355
+ }
356
+ if (populated > 1) {
357
+ throw createCliError(
358
+ 'AMOUNT_UNIT_AMBIGUOUS',
359
+ 'Provide exactly one of amountInRaw, amountInHuman, or legacy amountIn.'
360
+ );
361
+ }
362
+
363
+ if (hasRaw) {
364
+ const amountInRaw = normalizeIntegerRawAmount(params.amountInRaw);
365
+ return {
366
+ params: {
367
+ ...params,
368
+ amountInRaw,
369
+ amountIn: amountInRaw,
370
+ },
371
+ swapInput: {
372
+ amountInRaw,
373
+ amountInHuman: null,
374
+ tokenInDecimals: null,
375
+ unitSource: 'raw',
376
+ },
377
+ };
378
+ }
379
+
380
+ if (hasLegacy) {
381
+ const amountInRaw = normalizeIntegerRawAmount(params.amountIn, 'AMOUNT_UNIT_INVALID');
382
+ return {
383
+ params: {
384
+ ...params,
385
+ amountInRaw,
386
+ amountIn: amountInRaw,
387
+ },
388
+ swapInput: {
389
+ amountInRaw,
390
+ amountInHuman: null,
391
+ tokenInDecimals: null,
392
+ unitSource: 'legacy_raw',
393
+ },
394
+ };
395
+ }
396
+
397
+ const hasDecimals =
398
+ params.tokenInDecimals !== undefined &&
399
+ params.tokenInDecimals !== null &&
400
+ String(params.tokenInDecimals).trim() !== '';
401
+ if (!hasDecimals) {
402
+ throw createCliError(
403
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
404
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
405
+ );
406
+ }
407
+ const decimals = Number(params.tokenInDecimals);
408
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
409
+ throw createCliError(
410
+ 'AMOUNT_UNIT_DECIMALS_REQUIRED',
411
+ 'amountInHuman requires tokenInDecimals (integer 0-36).'
412
+ );
413
+ }
414
+ const amountInHuman = String(params.amountInHuman).trim();
415
+ const amountInRaw = decimalToRawUnits(amountInHuman, decimals);
416
+ return {
417
+ params: {
418
+ ...params,
419
+ amountInHuman,
420
+ amountInRaw,
421
+ amountIn: amountInRaw,
422
+ tokenInDecimals: decimals,
423
+ },
424
+ swapInput: {
425
+ amountInRaw,
426
+ amountInHuman,
427
+ tokenInDecimals: decimals,
428
+ unitSource: 'human',
429
+ },
430
+ };
431
+ }
432
+
433
+ function prepareActionParamsForFlags(flags, resolvedAction) {
434
+ const parsedParams = parseJsonFlag(flags, 'params');
435
+ if (!shouldApplySwapAmountContract(resolvedAction)) {
436
+ flags.__preparedParams = parsedParams;
437
+ flags.__swapInput = null;
438
+ return;
439
+ }
440
+ const normalized = normalizeSwapAmountContract(parsedParams);
441
+ flags.__preparedParams = normalized.params;
442
+ flags.__swapInput = normalized.swapInput;
443
+ }
444
+
445
+ function getPreparedParams(flags) {
446
+ if (flags.__preparedParams && typeof flags.__preparedParams === 'object') {
447
+ return flags.__preparedParams;
448
+ }
449
+ return parseJsonFlag(flags, 'params');
450
+ }
451
+
259
452
  async function commandCatalogsList(ctx, flags, capabilities) {
260
453
  const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
261
454
  const actions = extractActions(response);
@@ -306,6 +499,7 @@ async function commandActions(ctx, flags, capabilities) {
306
499
 
307
500
  async function commandPreflight(ctx, flags, capabilities) {
308
501
  const resolved = await resolveActionSelection(ctx, flags);
502
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
309
503
  const runMode = getEffectiveRunMode(ctx, flags);
310
504
  enforceChainCompatibility(capabilities, flags, runMode);
311
505
  const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
@@ -322,6 +516,7 @@ async function commandPreflight(ctx, flags, capabilities) {
322
516
  resolvedAction: resolved.resolvedAction,
323
517
  runMode,
324
518
  profile: ctx.profile || null,
519
+ swapInput: flags.__swapInput || null,
325
520
  capabilitiesVersion: capabilities?.contractVersion || null,
326
521
  },
327
522
  });
@@ -330,6 +525,7 @@ async function commandPreflight(ctx, flags, capabilities) {
330
525
 
331
526
  async function commandRun(ctx, flags, capabilities) {
332
527
  const resolved = await resolveActionSelection(ctx, flags);
528
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
333
529
  const runMode = getEffectiveRunMode(ctx, flags);
334
530
  enforceChainCompatibility(capabilities, flags, runMode);
335
531
  const skipPreflight = Boolean(flags.skipPreflight);
@@ -350,6 +546,7 @@ async function commandRun(ctx, flags, capabilities) {
350
546
  runMode,
351
547
  profile: ctx.profile || null,
352
548
  skippedPreflight: skipPreflight,
549
+ swapInput: flags.__swapInput || null,
353
550
  capabilitiesVersion: capabilities?.contractVersion || null,
354
551
  },
355
552
  });
@@ -392,12 +589,15 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
392
589
  }
393
590
 
394
591
  async function commandDelegationCreate(ctx, flags, capabilities) {
395
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation create');
592
+ const authHeaders = buildOwnerAuthHeaders(
593
+ flags,
594
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation create'
595
+ );
396
596
  let sessionAccountAddress = String(flags.sessionAccountAddress || '').trim();
397
597
  if (!sessionAccountAddress) {
398
598
  try {
399
599
  const executorAddressResponse = await apiGet(ctx, '/api/v1/agent/delegation/executor-address', {
400
- Authorization: `Bearer ${authToken}`,
600
+ ...authHeaders,
401
601
  });
402
602
  sessionAccountAddress = String(executorAddressResponse?.data?.executorAddress || '').trim();
403
603
  } catch (error) {
@@ -419,7 +619,7 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
419
619
  expirySeconds: Number(flags.expirySeconds || 86_400),
420
620
  };
421
621
  const response = await apiPost(ctx, '/api/v1/agent/delegation/create', payload, {
422
- Authorization: `Bearer ${authToken}`,
622
+ ...authHeaders,
423
623
  });
424
624
  const success = Boolean(response?.success !== false);
425
625
  printEnvelope({
@@ -434,8 +634,74 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
434
634
  });
435
635
  }
436
636
 
637
+ async function commandDelegationInitCustodial(ctx, flags, capabilities) {
638
+ const authHeaders = buildOwnerAuthHeaders(
639
+ flags,
640
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation init-custodial'
641
+ );
642
+ const payload = {
643
+ chainId: Number(flags.chainId || 8453),
644
+ };
645
+ const response = await requestWithRouteFallback(ctx, {
646
+ method: 'POST',
647
+ routes: ['/api/v1/agent/delegation/init-custodial'],
648
+ body: payload,
649
+ headers: {
650
+ ...authHeaders,
651
+ },
652
+ });
653
+ const success = Boolean(response?.success !== false);
654
+ printEnvelope({
655
+ success,
656
+ code: success ? 'DELEGATION_CUSTODIAL_INITIALIZED' : 'DELEGATION_CUSTODIAL_INIT_FAILED',
657
+ message: response?.message || (success ? 'Custodial wallet initialized' : 'Custodial wallet init failed'),
658
+ data: response,
659
+ meta: {
660
+ apiBase: ctx.apiBase,
661
+ capabilitiesVersion: capabilities?.contractVersion || null,
662
+ },
663
+ });
664
+ }
665
+
666
+ async function commandDelegationCustodialGrant(ctx, flags, capabilities) {
667
+ const authHeaders = buildOwnerAuthHeaders(
668
+ flags,
669
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation custodial-grant'
670
+ );
671
+ const payload = {
672
+ chainId: Number(flags.chainId || 8453),
673
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
674
+ tokenAddress: requireFlag(flags, 'tokenAddress', 'Missing --token-address'),
675
+ maxUsdPerDay: Number(requireFlag(flags, 'maxUsdPerDay', 'Missing --max-usd-per-day')),
676
+ periodDuration: Number(flags.periodDuration || 86_400),
677
+ expirySeconds: Number(flags.expirySeconds || 86_400),
678
+ };
679
+ const response = await requestWithRouteFallback(ctx, {
680
+ method: 'POST',
681
+ routes: ['/api/v1/agent/delegation/custodial-grant'],
682
+ body: payload,
683
+ headers: {
684
+ ...authHeaders,
685
+ },
686
+ });
687
+ const success = Boolean(response?.success !== false);
688
+ printEnvelope({
689
+ success,
690
+ code: success ? 'DELEGATION_CUSTODIAL_GRANTED' : 'DELEGATION_CUSTODIAL_GRANT_FAILED',
691
+ message: response?.message || (success ? 'Custodial delegation granted' : 'Custodial delegation grant failed'),
692
+ data: response,
693
+ meta: {
694
+ apiBase: ctx.apiBase,
695
+ capabilitiesVersion: capabilities?.contractVersion || null,
696
+ },
697
+ });
698
+ }
699
+
437
700
  async function commandDelegationActivate(ctx, flags, capabilities) {
438
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation activate');
701
+ const authHeaders = buildOwnerAuthHeaders(
702
+ flags,
703
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation activate'
704
+ );
439
705
  const payload = {
440
706
  agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
441
707
  walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
@@ -454,7 +720,7 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
454
720
  if (flags.permissionData) payload.permissionData = parseJsonFlag(flags, 'permissionData');
455
721
  if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
456
722
  const response = await apiPost(ctx, '/api/v1/agent/delegation/activate', payload, {
457
- Authorization: `Bearer ${authToken}`,
723
+ ...authHeaders,
458
724
  });
459
725
  const success = Boolean(response?.success !== false);
460
726
  printEnvelope({
@@ -470,7 +736,10 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
470
736
  }
471
737
 
472
738
  async function commandDelegationStatus(ctx, flags, capabilities) {
473
- const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for delegation status');
739
+ const authHeaders = buildOwnerAuthHeaders(
740
+ flags,
741
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation status'
742
+ );
474
743
  const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
475
744
  const chainId = Number(flags.chainId || 8453);
476
745
  const verify = Boolean(flags.verify);
@@ -478,7 +747,7 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
478
747
  ? `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`
479
748
  : `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
480
749
  const response = await apiGet(ctx, route, {
481
- Authorization: `Bearer ${authToken}`,
750
+ ...authHeaders,
482
751
  });
483
752
  const normalized = normalizeDelegationStatus(response);
484
753
  printEnvelope({
@@ -495,6 +764,129 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
495
764
  });
496
765
  }
497
766
 
767
+ async function commandDelegationInspect(ctx, flags, capabilities) {
768
+ const authHeaders = buildOwnerAuthHeaders(
769
+ flags,
770
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation inspect'
771
+ );
772
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
773
+ const chainId = Number(flags.chainId || 8453);
774
+ const includeOnchain = Boolean(flags.verify || flags.includeOnchain || flags.onchain);
775
+ const checkRoute = `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
776
+ const checkResponse = await apiGet(ctx, checkRoute, {
777
+ ...authHeaders,
778
+ });
779
+ const checkNormalized = normalizeDelegationStatus(checkResponse);
780
+
781
+ let verifyNormalized = null;
782
+ if (includeOnchain) {
783
+ const verifyRoute = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
784
+ const verifyResponse = await apiGet(ctx, verifyRoute, {
785
+ ...authHeaders,
786
+ });
787
+ verifyNormalized = normalizeDelegationStatus(verifyResponse);
788
+ }
789
+
790
+ printEnvelope({
791
+ success: true,
792
+ code: 'DELEGATION_INSPECT_OK',
793
+ message: includeOnchain ? 'Delegation inspect loaded (with onchain verification)' : 'Delegation inspect loaded',
794
+ data: {
795
+ check: checkNormalized,
796
+ verify: verifyNormalized,
797
+ state: verifyNormalized?.state || checkNormalized.state,
798
+ rawStatus: verifyNormalized?.rawStatus || checkNormalized.rawStatus,
799
+ },
800
+ meta: {
801
+ apiBase: ctx.apiBase,
802
+ includeOnchain,
803
+ governanceState: verifyNormalized?.state || checkNormalized.state || null,
804
+ capabilitiesVersion: capabilities?.contractVersion || null,
805
+ },
806
+ });
807
+ }
808
+
809
+ async function commandDelegationRevoke(ctx, flags, capabilities) {
810
+ const authHeaders = buildOwnerAuthHeaders(
811
+ flags,
812
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation revoke'
813
+ );
814
+ const payload = {
815
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
816
+ reason: String(flags.reason || 'user_revoked').trim(),
817
+ chainId: Number(flags.chainId || 8453),
818
+ };
819
+ const response = await requestWithRouteFallback(ctx, {
820
+ method: 'POST',
821
+ routes: ['/api/v1/agent/delegation/revoke'],
822
+ body: payload,
823
+ headers: {
824
+ ...authHeaders,
825
+ },
826
+ });
827
+ const success = Boolean(response?.success !== false);
828
+ printEnvelope({
829
+ success,
830
+ code: success ? 'DELEGATION_REVOKED' : 'DELEGATION_REVOKE_FAILED',
831
+ message: response?.message || (success ? 'Delegation revoked' : 'Delegation revoke failed'),
832
+ data: response,
833
+ meta: {
834
+ apiBase: ctx.apiBase,
835
+ capabilitiesVersion: capabilities?.contractVersion || null,
836
+ },
837
+ });
838
+ }
839
+
840
+ async function commandDelegationBalance(ctx, flags, capabilities) {
841
+ const authHeaders = buildOwnerAuthHeaders(
842
+ flags,
843
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation balance'
844
+ );
845
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
846
+ const chainId = Number(flags.chainId || 8453);
847
+ const route = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
848
+ const response = await apiGet(ctx, route, {
849
+ ...authHeaders,
850
+ });
851
+ const normalized = normalizeDelegationStatus(response);
852
+ const verifyData = normalized?.data?.onChainVerification || {};
853
+ printEnvelope({
854
+ success: true,
855
+ code: 'DELEGATION_BALANCE_OK',
856
+ message: 'Delegation allowance balance loaded',
857
+ data: {
858
+ state: normalized.state,
859
+ rawStatus: normalized.rawStatus,
860
+ delegatedWalletAddress: normalized?.data?.delegatedWalletAddress || null,
861
+ periodAvailableUsd: verifyData?.periodAvailableUsd ?? null,
862
+ periodAvailableBaseUnits: verifyData?.periodAvailableBaseUnits ?? null,
863
+ verified: verifyData?.verified ?? null,
864
+ isRevoked: verifyData?.isRevoked ?? null,
865
+ onChainVerification: verifyData || null,
866
+ },
867
+ meta: {
868
+ apiBase: ctx.apiBase,
869
+ governanceState: normalized.state || null,
870
+ capabilitiesVersion: capabilities?.contractVersion || null,
871
+ },
872
+ });
873
+ }
874
+
875
+ async function commandDelegationRedeem(ctx, flags, capabilities) {
876
+ const selectedRunMode = getEffectiveRunMode(ctx, flags);
877
+ if (!['thirdfy', 'hybrid'].includes(selectedRunMode)) {
878
+ throw createCliError('DELEGATION_REDEEM_MODE_INVALID', 'delegation redeem supports only --run-mode thirdfy|hybrid');
879
+ }
880
+ const delegatedFlags = {
881
+ ...flags,
882
+ runMode: selectedRunMode,
883
+ };
884
+ if (!delegatedFlags.estimatedAmountUsd) {
885
+ delegatedFlags.estimatedAmountUsd = '5';
886
+ }
887
+ await commandRun(ctx, delegatedFlags, capabilities);
888
+ }
889
+
498
890
  async function commandCredentialsUpsert(ctx, flags, capabilities) {
499
891
  const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
500
892
  const payload = {
@@ -587,11 +979,83 @@ async function commandAgentRegister(ctx, flags, capabilities) {
587
979
  ? '/api/v1/agent/onboarding/register-with-wallet'
588
980
  : '/api/v1/agent/onboarding/register-with-privy';
589
981
  const response = await apiPost(ctx, route, payload, ownerHeaders);
982
+ if (!flags.__suppressOutput) {
983
+ printEnvelope({
984
+ success: true,
985
+ code: 'AGENT_REGISTERED',
986
+ message: 'Agent registered',
987
+ data: response,
988
+ meta: {
989
+ apiBase: ctx.apiBase,
990
+ capabilitiesVersion: capabilities?.contractVersion || null,
991
+ },
992
+ });
993
+ }
994
+ return response;
995
+ }
996
+
997
+ async function commandDeveloperBootstrap(ctx, flags, capabilities) {
998
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
999
+ const name = requireFlag(flags, 'name', 'Missing --name');
1000
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1001
+ let usedWalletFlow = false;
1002
+ const challengeId = String(flags.challengeId || '').trim();
1003
+ const signature = String(flags.signature || '').trim();
1004
+
1005
+ if (!ownerSessionToken && signature) {
1006
+ if (!challengeId) {
1007
+ throw new Error('Missing --challenge-id for wallet bootstrap. Run `agent auth challenge` first.');
1008
+ }
1009
+ const verify = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', {
1010
+ challengeId,
1011
+ signature,
1012
+ agentKey,
1013
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1014
+ });
1015
+ ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
1016
+ usedWalletFlow = true;
1017
+ }
1018
+
1019
+ const registerFlags = {
1020
+ ...flags,
1021
+ agentKey,
1022
+ name,
1023
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1024
+ };
1025
+ const registerResponse = await commandAgentRegister(
1026
+ ctx,
1027
+ { ...registerFlags, __suppressOutput: true },
1028
+ capabilities
1029
+ );
1030
+
1031
+ const config = loadProfileConfig();
1032
+ const next = {
1033
+ ...config,
1034
+ auth: {
1035
+ ...(config.auth && typeof config.auth === 'object' ? config.auth : {}),
1036
+ ...(ownerSessionToken ? { ownerSessionToken } : {}),
1037
+ lastLoginAt: new Date().toISOString(),
1038
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1039
+ },
1040
+ developer: {
1041
+ ...(config.developer && typeof config.developer === 'object' ? config.developer : {}),
1042
+ agentKey,
1043
+ bootstrapCompletedAt: new Date().toISOString(),
1044
+ },
1045
+ };
1046
+ persistProfileConfig(next);
590
1047
  printEnvelope({
591
1048
  success: true,
592
- code: 'AGENT_REGISTERED',
593
- message: 'Agent registered',
594
- data: response,
1049
+ code: 'DEVELOPER_BOOTSTRAP_OK',
1050
+ message: 'Developer bootstrap completed',
1051
+ data: {
1052
+ registration: registerResponse,
1053
+ persisted: {
1054
+ ownerSessionTokenSaved: Boolean(ownerSessionToken),
1055
+ source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
1056
+ configPath: getProfileConfigPath(),
1057
+ },
1058
+ },
595
1059
  meta: {
596
1060
  apiBase: ctx.apiBase,
597
1061
  capabilitiesVersion: capabilities?.contractVersion || null,
@@ -706,10 +1170,140 @@ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
706
1170
  });
707
1171
  }
708
1172
 
1173
+ async function commandSelfExec(ctx, flags, capabilities) {
1174
+ const resolved = await resolveActionSelection(ctx, flags);
1175
+ prepareActionParamsForFlags(flags, resolved.resolvedAction);
1176
+ enforceChainCompatibility(capabilities, flags, 'self');
1177
+ const selfResult = await executeSelfRun(ctx, flags, resolved, {
1178
+ skipPreflight: Boolean(flags.skipPreflight),
1179
+ });
1180
+
1181
+ if (!selfResult?.success) {
1182
+ const blocked = Boolean(selfResult?.preflightBlocked);
1183
+ printEnvelope({
1184
+ success: false,
1185
+ code: blocked ? 'PREFLIGHT_BLOCKED' : 'SELF_EXECUTION_FAILED',
1186
+ message: blocked ? 'Preflight blocked self execution' : 'Self execution preparation failed',
1187
+ data: selfResult || {},
1188
+ meta: {
1189
+ apiBase: ctx.apiBase,
1190
+ runMode: 'self',
1191
+ resolvedAction: resolved.resolvedAction,
1192
+ },
1193
+ });
1194
+ process.exit(1);
1195
+ }
1196
+
1197
+ const txd = resolveUnsignedTx(selfResult);
1198
+ if (!txd?.to || !txd?.data) {
1199
+ printEnvelope({
1200
+ success: false,
1201
+ code: 'SELF_UNSIGNED_TX_MISSING',
1202
+ message: 'Self execution did not return a valid unsigned transaction',
1203
+ data: selfResult || {},
1204
+ meta: {
1205
+ apiBase: ctx.apiBase,
1206
+ runMode: 'self',
1207
+ resolvedAction: resolved.resolvedAction,
1208
+ },
1209
+ });
1210
+ process.exit(1);
1211
+ }
1212
+
1213
+ const chainId = Number(txd.chainId || flags.chainId || 8453);
1214
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
1215
+ const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
1216
+ const rpcUrl = String(flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org').trim();
1217
+ if (!walletAddress) {
1218
+ throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
1219
+ }
1220
+ if (!owsWalletName) {
1221
+ throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
1222
+ }
1223
+
1224
+ const params = getPreparedParams(flags);
1225
+ const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
1226
+ const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
1227
+
1228
+ let balancesBefore = null;
1229
+ if (effectTokenIn && effectTokenOut) {
1230
+ balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1231
+ }
1232
+
1233
+ let submission;
1234
+ try {
1235
+ submission = await signAndBroadcastWithOws({
1236
+ txd,
1237
+ chainId,
1238
+ walletAddress,
1239
+ owsWalletName,
1240
+ rpcUrl,
1241
+ });
1242
+ } catch (error) {
1243
+ throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
1244
+ }
1245
+
1246
+ let receipt;
1247
+ try {
1248
+ receipt = await submission.sent.wait();
1249
+ } catch (error) {
1250
+ throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
1251
+ }
1252
+ const status = Number(receipt?.status ?? 0);
1253
+ if (status !== 1) {
1254
+ throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
1255
+ receiptStatus: receipt?.status ?? null,
1256
+ txHash: submission.sent.hash,
1257
+ });
1258
+ }
1259
+
1260
+ let effectCheck = { checked: false, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null };
1261
+ if (balancesBefore) {
1262
+ const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
1263
+ const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
1264
+ const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
1265
+ const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
1266
+ effectCheck = {
1267
+ checked: true,
1268
+ hasEffect,
1269
+ inDeltaRaw,
1270
+ outDeltaRaw,
1271
+ };
1272
+ if (!hasEffect) {
1273
+ throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
1274
+ txHash: submission.sent.hash,
1275
+ effectCheck,
1276
+ });
1277
+ }
1278
+ }
1279
+
1280
+ printEnvelope({
1281
+ success: true,
1282
+ code: 'SELF_EXECUTED',
1283
+ message: 'Self-custody execution completed',
1284
+ data: {
1285
+ txHash: submission.sent.hash,
1286
+ chainId,
1287
+ blockNumber: receipt?.blockNumber ?? null,
1288
+ status: receipt?.status ?? null,
1289
+ unsignedTx: txd,
1290
+ effectCheck,
1291
+ },
1292
+ meta: {
1293
+ apiBase: ctx.apiBase,
1294
+ runMode: 'self',
1295
+ resolvedAction: resolved.resolvedAction,
1296
+ swapInput: flags.__swapInput || null,
1297
+ },
1298
+ });
1299
+ }
1300
+
709
1301
  async function commandProfileInit(ctx, flags) {
1302
+ const current = loadProfileConfig();
710
1303
  const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
711
1304
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
712
1305
  persistProfileConfig({
1306
+ ...current,
713
1307
  profile,
714
1308
  runMode,
715
1309
  });
@@ -734,6 +1328,7 @@ async function commandProfileUse(ctx, flags) {
734
1328
  // `profile use` intentionally re-applies profile defaults unless caller pins a mode.
735
1329
  const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
736
1330
  persistProfileConfig({
1331
+ ...current,
737
1332
  profile,
738
1333
  runMode,
739
1334
  });
@@ -770,6 +1365,154 @@ async function commandProfileShow(ctx, _flags) {
770
1365
  });
771
1366
  }
772
1367
 
1368
+ async function commandConfigSet(ctx, flags) {
1369
+ const key = String(flags.key || '').trim();
1370
+ const value = flags.value;
1371
+ if (!key) {
1372
+ throw new Error('Missing --key for config set');
1373
+ }
1374
+ const canonicalKey = key.toLowerCase();
1375
+ const hasValue = !(value === undefined || String(value).trim() === '');
1376
+ if (canonicalKey === 'profile' && hasValue) {
1377
+ normalizeProfileName(value);
1378
+ }
1379
+ if (canonicalKey === 'runmode' && hasValue) {
1380
+ normalizeRunMode(value);
1381
+ }
1382
+
1383
+ const current = loadProfileConfig();
1384
+ const next = setNestedConfigValue(current, key, value);
1385
+ persistProfileConfig(next);
1386
+ printEnvelope({
1387
+ success: true,
1388
+ code: 'CONFIG_UPDATED',
1389
+ message: 'Configuration updated',
1390
+ data: {
1391
+ key,
1392
+ value: value === undefined ? null : String(value),
1393
+ configPath: getProfileConfigPath(),
1394
+ },
1395
+ meta: { apiBase: ctx.apiBase },
1396
+ });
1397
+ }
1398
+
1399
+ async function commandLogin(ctx, flags) {
1400
+ const current = loadProfileConfig();
1401
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1402
+ let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
1403
+ let loginSource = 'token';
1404
+
1405
+ if (!authToken && !ownerSessionToken) {
1406
+ const challengeId = String(flags.challengeId || '').trim();
1407
+ const signature = String(flags.signature || '').trim();
1408
+ const agentKey = String(flags.agentKey || '').trim();
1409
+ if (challengeId && signature && agentKey) {
1410
+ const verifyResponse = await requestWithRouteFallback(ctx, {
1411
+ method: 'POST',
1412
+ routes: ['/api/v1/agent/onboarding/wallet/verify'],
1413
+ body: {
1414
+ challengeId,
1415
+ signature,
1416
+ agentKey,
1417
+ ...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
1418
+ },
1419
+ });
1420
+ ownerSessionToken = String(
1421
+ verifyResponse?.ownerSessionToken || verifyResponse?.data?.ownerSessionToken || ''
1422
+ ).trim();
1423
+ loginSource = 'wallet-verify';
1424
+ }
1425
+ }
1426
+
1427
+ if (!authToken && !ownerSessionToken) {
1428
+ throw new Error(
1429
+ 'Missing login credentials: provide --auth-token, --owner-session-token, or challenge verify inputs (--challenge-id/--signature/--agent-key).'
1430
+ );
1431
+ }
1432
+
1433
+ const next = {
1434
+ ...current,
1435
+ auth: {
1436
+ ...(current.auth && typeof current.auth === 'object' ? current.auth : {}),
1437
+ lastLoginAt: new Date().toISOString(),
1438
+ source: loginSource,
1439
+ },
1440
+ };
1441
+ if (authToken) {
1442
+ next.auth.authToken = authToken;
1443
+ delete next.auth.ownerSessionToken;
1444
+ } else if (ownerSessionToken) {
1445
+ next.auth.ownerSessionToken = ownerSessionToken;
1446
+ delete next.auth.authToken;
1447
+ }
1448
+ if (flags.agentApiKey) {
1449
+ next.agent = {
1450
+ ...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
1451
+ apiKey: String(flags.agentApiKey).trim(),
1452
+ };
1453
+ }
1454
+ persistProfileConfig(next);
1455
+ printEnvelope({
1456
+ success: true,
1457
+ code: 'LOGIN_OK',
1458
+ message: 'Login credentials stored',
1459
+ data: {
1460
+ hasAuthToken: Boolean(authToken),
1461
+ hasOwnerSessionToken: Boolean(ownerSessionToken),
1462
+ hasAgentApiKey: Boolean(flags.agentApiKey),
1463
+ configPath: getProfileConfigPath(),
1464
+ },
1465
+ meta: { apiBase: ctx.apiBase },
1466
+ });
1467
+ }
1468
+
1469
+ async function commandLogout(ctx, flags) {
1470
+ const current = loadProfileConfig();
1471
+ const localOnly = flags.all ? false : true;
1472
+ const auth = current.auth && typeof current.auth === 'object' ? current.auth : {};
1473
+ const ownerSessionToken = String(auth.ownerSessionToken || '').trim();
1474
+ let revokeAttempted = false;
1475
+ let revokeSuccess = false;
1476
+ let revokeError = null;
1477
+
1478
+ if (!localOnly && ownerSessionToken) {
1479
+ revokeAttempted = true;
1480
+ try {
1481
+ await requestWithRouteFallback(ctx, {
1482
+ method: 'POST',
1483
+ routes: ['/api/v1/agent/onboarding/wallet/logout', '/api/v1/agent/onboarding/wallet/revoke-session'],
1484
+ body: {},
1485
+ headers: {
1486
+ 'x-owner-session-token': ownerSessionToken,
1487
+ },
1488
+ });
1489
+ revokeSuccess = true;
1490
+ } catch (error) {
1491
+ revokeError = error?.message || 'OWNER_SESSION_REVOKE_FAILED';
1492
+ }
1493
+ }
1494
+
1495
+ const next = { ...current };
1496
+ delete next.auth;
1497
+ if (flags.clearAgentKey) {
1498
+ delete next.agent;
1499
+ }
1500
+ persistProfileConfig(next);
1501
+ printEnvelope({
1502
+ success: true,
1503
+ code: 'LOGOUT_OK',
1504
+ message: 'Local credentials cleared',
1505
+ data: {
1506
+ localOnly,
1507
+ revokeAttempted,
1508
+ revokeSuccess,
1509
+ revokeError,
1510
+ configPath: getProfileConfigPath(),
1511
+ },
1512
+ meta: { apiBase: ctx.apiBase },
1513
+ });
1514
+ }
1515
+
773
1516
  function resolveProfileState(flags) {
774
1517
  const persisted = loadProfileConfig();
775
1518
  const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
@@ -818,8 +1561,60 @@ function loadProfileConfig() {
818
1561
  function persistProfileConfig(config) {
819
1562
  const configPath = getProfileConfigPath();
820
1563
  const dirPath = path.dirname(configPath);
821
- fs.mkdirSync(dirPath, { recursive: true });
822
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
1564
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
1565
+ const payload = `${JSON.stringify(config, null, 2)}\n`;
1566
+ fs.writeFileSync(configPath, payload, { encoding: 'utf8', mode: 0o600 });
1567
+ try {
1568
+ fs.chmodSync(configPath, 0o600);
1569
+ } catch {
1570
+ /* chmod unsupported or restricted (e.g. some Windows FS) */
1571
+ }
1572
+ try {
1573
+ fs.chmodSync(dirPath, 0o700);
1574
+ } catch {
1575
+ /* ignore */
1576
+ }
1577
+ }
1578
+
1579
+ function clonePlainConfigTree(input) {
1580
+ const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
1581
+ try {
1582
+ return structuredClone(obj);
1583
+ } catch {
1584
+ return JSON.parse(JSON.stringify(obj));
1585
+ }
1586
+ }
1587
+
1588
+ function setNestedConfigValue(config, keyPath, value) {
1589
+ const next = clonePlainConfigTree(config || {});
1590
+ const parts = String(keyPath || '')
1591
+ .split('.')
1592
+ .map((v) => v.trim())
1593
+ .filter(Boolean);
1594
+ const blocked = new Set(['__proto__', 'prototype', 'constructor']);
1595
+ if (parts.some((part) => blocked.has(part))) {
1596
+ throw createCliError(
1597
+ 'CONFIG_KEY_INVALID',
1598
+ 'Invalid config key path: reserved segments (__proto__, prototype, constructor) are not allowed.'
1599
+ );
1600
+ }
1601
+ if (!parts.length) return next;
1602
+ let cursor = next;
1603
+ for (let i = 0; i < parts.length - 1; i += 1) {
1604
+ const key = parts[i];
1605
+ const existing = cursor[key];
1606
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
1607
+ cursor[key] = {};
1608
+ }
1609
+ cursor = cursor[key];
1610
+ }
1611
+ const finalKey = parts[parts.length - 1];
1612
+ if (value === undefined || String(value).trim() === '') {
1613
+ delete cursor[finalKey];
1614
+ } else {
1615
+ cursor[finalKey] = String(value);
1616
+ }
1617
+ return next;
823
1618
  }
824
1619
 
825
1620
  function getEffectiveRunMode(ctx, flags) {
@@ -1030,10 +1825,11 @@ async function executeHybridRun(ctx, flags, resolved, options) {
1030
1825
  }
1031
1826
 
1032
1827
  function buildBuildTxPayload(flags, resolvedAction) {
1828
+ const runMode = String(flags.runMode || '').trim().toLowerCase() || 'self';
1033
1829
  const payload = {
1034
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1830
+ agentApiKey: resolveAgentApiKey(flags, runMode),
1035
1831
  action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
1036
- params: parseJsonFlag(flags, 'params'),
1832
+ params: getPreparedParams(flags),
1037
1833
  };
1038
1834
  if (flags.chainId) payload.chainId = Number(flags.chainId);
1039
1835
  if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
@@ -1041,10 +1837,11 @@ function buildBuildTxPayload(flags, resolvedAction) {
1041
1837
  }
1042
1838
 
1043
1839
  function buildIntentPayload(flags, options) {
1840
+ const runMode = String(options.runMode || flags.runMode || '').trim().toLowerCase() || 'thirdfy';
1044
1841
  const payload = {
1045
- agentApiKey: requireFlag(flags, 'agentApiKey', 'Missing --agent-api-key'),
1842
+ agentApiKey: resolveAgentApiKey(flags, runMode),
1046
1843
  action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
1047
- params: parseJsonFlag(flags, 'params'),
1844
+ params: getPreparedParams(flags),
1048
1845
  };
1049
1846
  if (flags.catalog) payload.catalog = String(flags.catalog).trim();
1050
1847
  if (flags.chainId) payload.chainId = Number(flags.chainId);
@@ -1065,6 +1862,22 @@ function buildIntentPayload(flags, options) {
1065
1862
  return payload;
1066
1863
  }
1067
1864
 
1865
+ function resolveAgentApiKey(flags, runMode) {
1866
+ const explicit = String(flags.agentApiKey || '').trim();
1867
+ if (explicit) return explicit;
1868
+ const envKey = String(process.env.THIRDFY_AGENT_API_KEY || '').trim();
1869
+ if (envKey) return envKey;
1870
+ const config = loadProfileConfig();
1871
+ const configKey = String(config?.agent?.apiKey || '').trim();
1872
+ if (configKey) return configKey;
1873
+ if (runMode === 'self') {
1874
+ throw new Error(
1875
+ 'SELF_OPEN_DISABLED: self mode without agent identity is not enabled on this API yet. Set --agent-api-key, THIRDFY_AGENT_API_KEY, or login/config default.'
1876
+ );
1877
+ }
1878
+ throw new Error('Missing --agent-api-key (or THIRDFY_AGENT_API_KEY / config.agent.apiKey)');
1879
+ }
1880
+
1068
1881
  function getActionKey(action) {
1069
1882
  return String(action?.action || action?.id || action?.key || action?.name || '')
1070
1883
  .trim();
@@ -1188,14 +2001,18 @@ async function resolveActionSelection(ctx, flags) {
1188
2001
  }
1189
2002
 
1190
2003
  function getAuthToken(flags, missingMessage) {
1191
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
2004
+ const config = loadProfileConfig();
2005
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
1192
2006
  if (!authToken) throw new Error(missingMessage);
1193
2007
  return authToken;
1194
2008
  }
1195
2009
 
1196
2010
  function buildOwnerAuthHeaders(flags, missingMessage) {
1197
- const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
1198
- const ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
2011
+ const config = loadProfileConfig();
2012
+ const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
2013
+ const ownerSessionToken = String(
2014
+ flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || config?.auth?.ownerSessionToken || ''
2015
+ ).trim();
1199
2016
  if (!authToken && !ownerSessionToken) {
1200
2017
  throw new Error(missingMessage);
1201
2018
  }
@@ -1224,6 +2041,116 @@ async function negotiateCapabilities(ctx) {
1224
2041
  }
1225
2042
  }
1226
2043
 
2044
+ function resolveUnsignedTx(result) {
2045
+ if (result?.unsignedTx && typeof result.unsignedTx === 'object') return result.unsignedTx;
2046
+ if (result?.data?.unsignedTx && typeof result.data.unsignedTx === 'object') return result.data.unsignedTx;
2047
+ if (result?.details?.unsignedTx && typeof result.details.unsignedTx === 'object') return result.details.unsignedTx;
2048
+ return null;
2049
+ }
2050
+
2051
+ async function loadEthers() {
2052
+ const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
2053
+ for (const candidate of candidates) {
2054
+ try {
2055
+ return require(candidate);
2056
+ } catch {
2057
+ // continue
2058
+ }
2059
+ }
2060
+ throw new Error('Could not load ethers dependency for self-exec');
2061
+ }
2062
+
2063
+ function resolveOwsBinary() {
2064
+ const explicit = String(process.env.OWS_CLI_PATH || '').trim();
2065
+ if (explicit && fs.existsSync(explicit)) return explicit;
2066
+ const localBin = path.join(process.cwd(), 'node_modules', '.bin', 'ows');
2067
+ if (fs.existsSync(localBin)) return localBin;
2068
+ const which = spawnSync('which', ['ows'], { encoding: 'utf-8' });
2069
+ const discovered = String(which.stdout || '').trim();
2070
+ if (discovered) return discovered;
2071
+ throw new Error('ows CLI not found. Install it or set OWS_CLI_PATH.');
2072
+ }
2073
+
2074
+ function signUnsignedTxWithOws(unsignedHex, walletName, chainId) {
2075
+ const passphrase = String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim();
2076
+ if (!passphrase) {
2077
+ throw new Error('Set OWS_PASSPHRASE (or OWS_API_KEY) for self-exec signing.');
2078
+ }
2079
+ const ows = resolveOwsBinary();
2080
+ const txHex = String(unsignedHex).startsWith('0x') ? String(unsignedHex).slice(2) : String(unsignedHex);
2081
+ const res = spawnSync(
2082
+ ows,
2083
+ ['sign', 'tx', '--wallet', walletName, '--chain', `eip155:${chainId}`, '--tx', txHex, '--json'],
2084
+ { env: { ...process.env, OWS_PASSPHRASE: passphrase }, encoding: 'utf-8' }
2085
+ );
2086
+ const out = String(res.stdout || '').trim();
2087
+ if (res.status !== 0 || !out) {
2088
+ throw new Error(`ows sign tx failed: ${String(res.stderr || out || res.status).slice(0, 300)}`);
2089
+ }
2090
+ let parsed;
2091
+ try {
2092
+ parsed = JSON.parse(out);
2093
+ } catch {
2094
+ throw new Error(`ows sign tx returned non-json payload: ${out.slice(0, 200)}`);
2095
+ }
2096
+ const signature = String(parsed.signature || '').replace(/^0x/i, '');
2097
+ if (signature.length < 128) {
2098
+ throw new Error('Invalid OWS signature payload');
2099
+ }
2100
+ return {
2101
+ recoveryId: Number(parsed.recovery_id),
2102
+ r: `0x${signature.slice(0, 64)}`,
2103
+ s: `0x${signature.slice(64, 128)}`,
2104
+ };
2105
+ }
2106
+
2107
+ async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletName, rpcUrl }) {
2108
+ const ethers = await loadEthers();
2109
+ const { JsonRpcProvider, Transaction, Signature } = ethers;
2110
+ const provider = new JsonRpcProvider(rpcUrl, chainId);
2111
+ const nonce = await provider.getTransactionCount(walletAddress, 'pending');
2112
+ const fee = await provider.getFeeData();
2113
+ const txFields = {
2114
+ type: 2,
2115
+ chainId,
2116
+ nonce,
2117
+ to: String(txd.to).toLowerCase(),
2118
+ data: txd.data,
2119
+ value: txd.value ? BigInt(txd.value) : 0n,
2120
+ gasLimit: txd.gas ? BigInt(txd.gas) : txd.gasLimit ? BigInt(txd.gasLimit) : 500000n,
2121
+ maxFeePerGas: fee.maxFeePerGas ?? fee.gasPrice ?? 2_000_000_000n,
2122
+ maxPriorityFeePerGas: fee.maxPriorityFeePerGas ?? 1_000_000_000n,
2123
+ };
2124
+ const unsigned = Transaction.from(txFields);
2125
+ const sigParts = signUnsignedTxWithOws(unsigned.unsignedSerialized, owsWalletName, chainId);
2126
+ const sig = Signature.from({ r: sigParts.r, s: sigParts.s, yParity: sigParts.recoveryId });
2127
+ const tx = Transaction.from(unsigned.unsignedSerialized);
2128
+ tx.signature = sig;
2129
+ const sent = await provider.broadcastTransaction(tx.serialized);
2130
+ return { sent };
2131
+ }
2132
+
2133
+ async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
2134
+ const ethers = await loadEthers();
2135
+ const { JsonRpcProvider, Interface } = ethers;
2136
+ const provider = new JsonRpcProvider(rpcUrl);
2137
+ const erc20 = new Interface(['function balanceOf(address owner) view returns (uint256)']);
2138
+ const response = await provider.call({
2139
+ to: tokenAddress,
2140
+ data: erc20.encodeFunctionData('balanceOf', [walletAddress]),
2141
+ });
2142
+ const decoded = erc20.decodeFunctionResult('balanceOf', response);
2143
+ return BigInt(decoded[0].toString()).toString();
2144
+ }
2145
+
2146
+ async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut) {
2147
+ const [inBal, outBal] = await Promise.all([
2148
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn),
2149
+ readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut),
2150
+ ]);
2151
+ return { tokenIn: inBal, tokenOut: outBal };
2152
+ }
2153
+
1227
2154
  async function apiGet(ctx, route, headers = {}) {
1228
2155
  return requestJson(ctx, route, { method: 'GET', headers });
1229
2156
  }
@@ -1308,21 +2235,33 @@ Core commands:
1308
2235
  thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
1309
2236
  thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
1310
2237
  thirdfy-agent profile show [--json]
2238
+ thirdfy-agent config set --key <path> --value <value> [--json]
2239
+ thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--json]
2240
+ thirdfy-agent logout [--all] [--clear-agent-key] [--json]
1311
2241
  thirdfy-agent whoami [--json]
1312
2242
  thirdfy-agent catalogs list [--json]
1313
2243
  thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
1314
2244
  thirdfy-agent delegation create --auth-token <token> --agent-key <key> --owner-address <addr> --token-address <addr> --max-usd-per-day <usd> [--session-account-address <addr>] [--json]
2245
+ thirdfy-agent delegation init-custodial --auth-token <token> [--chain-id <id>] [--json]
2246
+ thirdfy-agent delegation custodial-grant --auth-token <token> --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
1315
2247
  thirdfy-agent delegation activate --auth-token <token> --agent-key <key> --wallet-address <addr> --session-account-address <addr> --delegation-manager <addr> --delegation <json> --signature <hex> [--json]
1316
2248
  thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
2249
+ thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
2250
+ thirdfy-agent delegation inspect --auth-token <token> --agent-key <key> [--verify] [--json]
2251
+ thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
2252
+ thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
2253
+ thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--estimated-amount-usd <usd>] [--json]
1317
2254
  thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
1318
2255
  thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
1319
2256
  thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
1320
2257
  thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
2258
+ thirdfy-agent developer bootstrap --agent-key <key> --name <name> [--owner-session-token <token> | --challenge-id <id> --signature <hex>] [--json]
1321
2259
  thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
1322
2260
  thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1323
2261
  thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
1324
2262
  thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1325
2263
  thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--json]
2264
+ thirdfy-agent self-exec --agent-api-key <key> --action <id> --params <json> --wallet-address <addr> --ows-wallet-name <name> [--rpc-url <url>] [--json]
1326
2265
  thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1327
2266
  thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
1328
2267
  thirdfy-agent jeff status --intent-id <id> [--json]
@@ -1336,6 +2275,7 @@ Global flags:
1336
2275
  --profile <name> personal | builder | network
1337
2276
  --env <file> load env vars from file
1338
2277
  --timeout <ms> request timeout
2278
+ --params <json> for swap: exactly one of {amountInRaw} or {amountInHuman + tokenInDecimals}
1339
2279
  --owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
1340
2280
  --json stable machine-readable output
1341
2281
  --verbose print adapter diagnostics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  "e2e:base:real": "node ./scripts/e2e/base-realtx.mjs",
24
24
  "e2e:delegation:discovery": "node ./scripts/e2e/delegation-discovery.mjs",
25
25
  "e2e:delegation:lifecycle": "node ./scripts/e2e/delegation-lifecycle.mjs",
26
+ "e2e:delegation:commands": "node ./scripts/e2e/delegation-command-parity.mjs",
27
+ "e2e:self:delegation:parity": "node ./scripts/e2e/self-delegation-parity.mjs",
26
28
  "e2e:delegation:prod:certify": "node ./scripts/e2e/prod-delegation-certify.mjs",
27
29
  "e2e:report:last": "node ./scripts/e2e/report-last.mjs",
28
30
  "smoke:cli": "node ./bin/thirdfy-agent.mjs --help && node ./bin/thirdfy-agent.mjs --version --json",