@thirdfy/agent-cli 0.1.6 → 0.1.8
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/CHANGELOG.md +57 -0
- package/README.md +155 -5
- package/bin/thirdfy-agent.mjs +1190 -20
- package/package.json +9 -2
package/bin/thirdfy-agent.mjs
CHANGED
|
@@ -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';
|
|
@@ -18,6 +19,7 @@ const DEFAULT_API_BASE = 'https://api.thirdfy.com';
|
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
19
20
|
const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
|
|
20
21
|
const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
|
|
22
|
+
const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
|
|
21
23
|
const PROFILE_DEFAULTS = {
|
|
22
24
|
personal: 'self',
|
|
23
25
|
builder: 'hybrid',
|
|
@@ -105,9 +107,21 @@ async function main() {
|
|
|
105
107
|
case 'config get':
|
|
106
108
|
await commandProfileShow(context, subFlags);
|
|
107
109
|
return;
|
|
110
|
+
case 'config set':
|
|
111
|
+
await commandConfigSet(context, subFlags);
|
|
112
|
+
return;
|
|
113
|
+
case 'login':
|
|
114
|
+
await commandLogin(context, subFlags);
|
|
115
|
+
return;
|
|
116
|
+
case 'logout':
|
|
117
|
+
await commandLogout(context, subFlags);
|
|
118
|
+
return;
|
|
108
119
|
case 'whoami':
|
|
109
120
|
await commandProfileShow(context, subFlags);
|
|
110
121
|
return;
|
|
122
|
+
case 'doctor self':
|
|
123
|
+
await commandDoctorSelf(context, subFlags);
|
|
124
|
+
return;
|
|
111
125
|
default:
|
|
112
126
|
break;
|
|
113
127
|
}
|
|
@@ -115,6 +129,9 @@ async function main() {
|
|
|
115
129
|
const capabilities = await negotiateCapabilities(context);
|
|
116
130
|
|
|
117
131
|
switch (commandKey) {
|
|
132
|
+
case 'managed run':
|
|
133
|
+
await commandManagedRun(context, subFlags, capabilities);
|
|
134
|
+
return;
|
|
118
135
|
case 'jeff trade':
|
|
119
136
|
await commandRun(context, subFlags, capabilities);
|
|
120
137
|
return;
|
|
@@ -133,12 +150,33 @@ async function main() {
|
|
|
133
150
|
case 'delegation create':
|
|
134
151
|
await commandDelegationCreate(context, subFlags, capabilities);
|
|
135
152
|
return;
|
|
153
|
+
case 'delegation init-custodial':
|
|
154
|
+
await commandDelegationInitCustodial(context, subFlags, capabilities);
|
|
155
|
+
return;
|
|
156
|
+
case 'delegation custodial-grant':
|
|
157
|
+
await commandDelegationCustodialGrant(context, subFlags, capabilities);
|
|
158
|
+
return;
|
|
136
159
|
case 'delegation activate':
|
|
137
160
|
await commandDelegationActivate(context, subFlags, capabilities);
|
|
138
161
|
return;
|
|
139
162
|
case 'delegation status':
|
|
140
163
|
await commandDelegationStatus(context, subFlags, capabilities);
|
|
141
164
|
return;
|
|
165
|
+
case 'delegation show':
|
|
166
|
+
await commandDelegationStatus(context, subFlags, capabilities);
|
|
167
|
+
return;
|
|
168
|
+
case 'delegation inspect':
|
|
169
|
+
await commandDelegationInspect(context, subFlags, capabilities);
|
|
170
|
+
return;
|
|
171
|
+
case 'delegation balance':
|
|
172
|
+
await commandDelegationBalance(context, subFlags, capabilities);
|
|
173
|
+
return;
|
|
174
|
+
case 'delegation revoke':
|
|
175
|
+
await commandDelegationRevoke(context, subFlags, capabilities);
|
|
176
|
+
return;
|
|
177
|
+
case 'delegation redeem':
|
|
178
|
+
await commandDelegationRedeem(context, subFlags, capabilities);
|
|
179
|
+
return;
|
|
142
180
|
case 'credentials upsert':
|
|
143
181
|
await commandCredentialsUpsert(context, subFlags, capabilities);
|
|
144
182
|
return;
|
|
@@ -148,6 +186,9 @@ async function main() {
|
|
|
148
186
|
case 'agent register':
|
|
149
187
|
await commandAgentRegister(context, subFlags, capabilities);
|
|
150
188
|
return;
|
|
189
|
+
case 'developer bootstrap':
|
|
190
|
+
await commandDeveloperBootstrap(context, subFlags, capabilities);
|
|
191
|
+
return;
|
|
151
192
|
default:
|
|
152
193
|
break;
|
|
153
194
|
}
|
|
@@ -167,6 +208,14 @@ async function main() {
|
|
|
167
208
|
await commandAgentAuthVerify(context, subFlags, capabilities);
|
|
168
209
|
return;
|
|
169
210
|
}
|
|
211
|
+
if (commandKey3 === 'managed wallet init') {
|
|
212
|
+
await commandDelegationInitCustodial(context, subFlags, capabilities);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (commandKey3 === 'managed wallet grant') {
|
|
216
|
+
await commandDelegationCustodialGrant(context, subFlags, capabilities);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
170
219
|
|
|
171
220
|
const rootCommand = parsed.positionals[0];
|
|
172
221
|
switch (rootCommand) {
|
|
@@ -182,6 +231,9 @@ async function main() {
|
|
|
182
231
|
case 'run':
|
|
183
232
|
await commandRun(context, subFlags, capabilities);
|
|
184
233
|
return;
|
|
234
|
+
case 'self-exec':
|
|
235
|
+
await commandSelfExec(context, subFlags, capabilities);
|
|
236
|
+
return;
|
|
185
237
|
case 'intent-status':
|
|
186
238
|
await commandIntentStatus(context, subFlags, capabilities);
|
|
187
239
|
return;
|
|
@@ -256,6 +308,162 @@ function parseJsonFlag(flags, key) {
|
|
|
256
308
|
}
|
|
257
309
|
}
|
|
258
310
|
|
|
311
|
+
function shouldApplySwapAmountContract(actionName) {
|
|
312
|
+
const action = String(actionName || '').trim().toLowerCase();
|
|
313
|
+
return action === 'swap' || /(^|:|-)swap$/.test(action);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createCliError(code, message, payload = {}) {
|
|
317
|
+
const error = new Error(message);
|
|
318
|
+
error.payload = {
|
|
319
|
+
error: code,
|
|
320
|
+
blockedReason: code,
|
|
321
|
+
blockedStage: 'client_validation',
|
|
322
|
+
...payload,
|
|
323
|
+
};
|
|
324
|
+
return error;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeIntegerRawAmount(input, code = 'AMOUNT_UNIT_INVALID') {
|
|
328
|
+
const raw = String(input ?? '').trim();
|
|
329
|
+
if (!/^\d+$/.test(raw)) {
|
|
330
|
+
throw createCliError(code, 'amountInRaw must be an integer base-unit string (example: "1000000").');
|
|
331
|
+
}
|
|
332
|
+
return raw;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function decimalToRawUnits(amountHuman, decimals) {
|
|
336
|
+
const s = String(amountHuman ?? '').trim();
|
|
337
|
+
if (!/^\d+(\.\d+)?$/.test(s)) {
|
|
338
|
+
throw createCliError('AMOUNT_UNIT_INVALID', 'amountInHuman must be a decimal string (example: "100.5").');
|
|
339
|
+
}
|
|
340
|
+
const [whole, frac = ''] = s.split('.');
|
|
341
|
+
if (decimals === 0) {
|
|
342
|
+
if (frac && /[1-9]/.test(frac)) {
|
|
343
|
+
throw createCliError(
|
|
344
|
+
'AMOUNT_PRECISION_EXCESS',
|
|
345
|
+
'amountInHuman has fractional digits but tokenInDecimals is 0 (example: use "2" not "2.5").'
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
} else if (frac.length > decimals && /[1-9]/.test(frac.slice(decimals))) {
|
|
349
|
+
throw createCliError(
|
|
350
|
+
'AMOUNT_PRECISION_EXCESS',
|
|
351
|
+
`amountInHuman has more than ${decimals} fractional digit(s) for tokenInDecimals; round or use amountInRaw.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const fracNorm = (frac + '0'.repeat(decimals)).slice(0, decimals);
|
|
355
|
+
const wholePart = BigInt(whole || '0') * 10n ** BigInt(decimals);
|
|
356
|
+
const fracPart = BigInt(fracNorm || '0');
|
|
357
|
+
return (wholePart + fracPart).toString();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSwapAmountContract(params) {
|
|
361
|
+
const hasRaw = params.amountInRaw !== undefined && params.amountInRaw !== null && String(params.amountInRaw).trim() !== '';
|
|
362
|
+
const hasHuman = params.amountInHuman !== undefined && params.amountInHuman !== null && String(params.amountInHuman).trim() !== '';
|
|
363
|
+
const hasLegacy = params.amountIn !== undefined && params.amountIn !== null && String(params.amountIn).trim() !== '';
|
|
364
|
+
const populated = [hasRaw, hasHuman, hasLegacy].filter(Boolean).length;
|
|
365
|
+
if (populated === 0) {
|
|
366
|
+
throw createCliError(
|
|
367
|
+
'AMOUNT_UNIT_MISSING',
|
|
368
|
+
'Missing swap amount: provide one of amountInRaw, amountInHuman, or legacy amountIn.'
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (populated > 1) {
|
|
372
|
+
throw createCliError(
|
|
373
|
+
'AMOUNT_UNIT_AMBIGUOUS',
|
|
374
|
+
'Provide exactly one of amountInRaw, amountInHuman, or legacy amountIn.'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (hasRaw) {
|
|
379
|
+
const amountInRaw = normalizeIntegerRawAmount(params.amountInRaw);
|
|
380
|
+
return {
|
|
381
|
+
params: {
|
|
382
|
+
...params,
|
|
383
|
+
amountInRaw,
|
|
384
|
+
amountIn: amountInRaw,
|
|
385
|
+
},
|
|
386
|
+
swapInput: {
|
|
387
|
+
amountInRaw,
|
|
388
|
+
amountInHuman: null,
|
|
389
|
+
tokenInDecimals: null,
|
|
390
|
+
unitSource: 'raw',
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (hasLegacy) {
|
|
396
|
+
const amountInRaw = normalizeIntegerRawAmount(params.amountIn, 'AMOUNT_UNIT_INVALID');
|
|
397
|
+
return {
|
|
398
|
+
params: {
|
|
399
|
+
...params,
|
|
400
|
+
amountInRaw,
|
|
401
|
+
amountIn: amountInRaw,
|
|
402
|
+
},
|
|
403
|
+
swapInput: {
|
|
404
|
+
amountInRaw,
|
|
405
|
+
amountInHuman: null,
|
|
406
|
+
tokenInDecimals: null,
|
|
407
|
+
unitSource: 'legacy_raw',
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const hasDecimals =
|
|
413
|
+
params.tokenInDecimals !== undefined &&
|
|
414
|
+
params.tokenInDecimals !== null &&
|
|
415
|
+
String(params.tokenInDecimals).trim() !== '';
|
|
416
|
+
if (!hasDecimals) {
|
|
417
|
+
throw createCliError(
|
|
418
|
+
'AMOUNT_UNIT_DECIMALS_REQUIRED',
|
|
419
|
+
'amountInHuman requires tokenInDecimals (integer 0-36).'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const decimals = Number(params.tokenInDecimals);
|
|
423
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
|
|
424
|
+
throw createCliError(
|
|
425
|
+
'AMOUNT_UNIT_DECIMALS_REQUIRED',
|
|
426
|
+
'amountInHuman requires tokenInDecimals (integer 0-36).'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const amountInHuman = String(params.amountInHuman).trim();
|
|
430
|
+
const amountInRaw = decimalToRawUnits(amountInHuman, decimals);
|
|
431
|
+
return {
|
|
432
|
+
params: {
|
|
433
|
+
...params,
|
|
434
|
+
amountInHuman,
|
|
435
|
+
amountInRaw,
|
|
436
|
+
amountIn: amountInRaw,
|
|
437
|
+
tokenInDecimals: decimals,
|
|
438
|
+
},
|
|
439
|
+
swapInput: {
|
|
440
|
+
amountInRaw,
|
|
441
|
+
amountInHuman,
|
|
442
|
+
tokenInDecimals: decimals,
|
|
443
|
+
unitSource: 'human',
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function prepareActionParamsForFlags(flags, resolvedAction) {
|
|
449
|
+
const parsedParams = parseJsonFlag(flags, 'params');
|
|
450
|
+
if (!shouldApplySwapAmountContract(resolvedAction)) {
|
|
451
|
+
flags.__preparedParams = parsedParams;
|
|
452
|
+
flags.__swapInput = null;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const normalized = normalizeSwapAmountContract(parsedParams);
|
|
456
|
+
flags.__preparedParams = normalized.params;
|
|
457
|
+
flags.__swapInput = normalized.swapInput;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getPreparedParams(flags) {
|
|
461
|
+
if (flags.__preparedParams && typeof flags.__preparedParams === 'object') {
|
|
462
|
+
return flags.__preparedParams;
|
|
463
|
+
}
|
|
464
|
+
return parseJsonFlag(flags, 'params');
|
|
465
|
+
}
|
|
466
|
+
|
|
259
467
|
async function commandCatalogsList(ctx, flags, capabilities) {
|
|
260
468
|
const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
|
|
261
469
|
const actions = extractActions(response);
|
|
@@ -306,6 +514,7 @@ async function commandActions(ctx, flags, capabilities) {
|
|
|
306
514
|
|
|
307
515
|
async function commandPreflight(ctx, flags, capabilities) {
|
|
308
516
|
const resolved = await resolveActionSelection(ctx, flags);
|
|
517
|
+
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
309
518
|
const runMode = getEffectiveRunMode(ctx, flags);
|
|
310
519
|
enforceChainCompatibility(capabilities, flags, runMode);
|
|
311
520
|
const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved.resolvedAction);
|
|
@@ -322,6 +531,7 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
322
531
|
resolvedAction: resolved.resolvedAction,
|
|
323
532
|
runMode,
|
|
324
533
|
profile: ctx.profile || null,
|
|
534
|
+
swapInput: flags.__swapInput || null,
|
|
325
535
|
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
326
536
|
},
|
|
327
537
|
});
|
|
@@ -330,6 +540,7 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
330
540
|
|
|
331
541
|
async function commandRun(ctx, flags, capabilities) {
|
|
332
542
|
const resolved = await resolveActionSelection(ctx, flags);
|
|
543
|
+
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
333
544
|
const runMode = getEffectiveRunMode(ctx, flags);
|
|
334
545
|
enforceChainCompatibility(capabilities, flags, runMode);
|
|
335
546
|
const skipPreflight = Boolean(flags.skipPreflight);
|
|
@@ -337,6 +548,26 @@ async function commandRun(ctx, flags, capabilities) {
|
|
|
337
548
|
skipPreflight,
|
|
338
549
|
});
|
|
339
550
|
const normalized = backendResult.normalized;
|
|
551
|
+
if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
|
|
552
|
+
const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
|
|
553
|
+
printEnvelope({
|
|
554
|
+
success: true,
|
|
555
|
+
code: 'SELF_EXECUTED',
|
|
556
|
+
message: 'Self-custody execution completed',
|
|
557
|
+
data: executed,
|
|
558
|
+
meta: {
|
|
559
|
+
apiBase: ctx.apiBase,
|
|
560
|
+
requestedAction: resolved.requestedAction,
|
|
561
|
+
resolvedAction: resolved.resolvedAction,
|
|
562
|
+
runMode,
|
|
563
|
+
profile: ctx.profile || null,
|
|
564
|
+
skippedPreflight: skipPreflight,
|
|
565
|
+
swapInput: flags.__swapInput || null,
|
|
566
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
340
571
|
printEnvelope({
|
|
341
572
|
success: normalized.success,
|
|
342
573
|
code: backendResult.code,
|
|
@@ -350,6 +581,7 @@ async function commandRun(ctx, flags, capabilities) {
|
|
|
350
581
|
runMode,
|
|
351
582
|
profile: ctx.profile || null,
|
|
352
583
|
skippedPreflight: skipPreflight,
|
|
584
|
+
swapInput: flags.__swapInput || null,
|
|
353
585
|
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
354
586
|
},
|
|
355
587
|
});
|
|
@@ -392,12 +624,15 @@ async function commandCreditsBalance(ctx, flags, capabilities) {
|
|
|
392
624
|
}
|
|
393
625
|
|
|
394
626
|
async function commandDelegationCreate(ctx, flags, capabilities) {
|
|
395
|
-
const
|
|
627
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
628
|
+
flags,
|
|
629
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation create'
|
|
630
|
+
);
|
|
396
631
|
let sessionAccountAddress = String(flags.sessionAccountAddress || '').trim();
|
|
397
632
|
if (!sessionAccountAddress) {
|
|
398
633
|
try {
|
|
399
634
|
const executorAddressResponse = await apiGet(ctx, '/api/v1/agent/delegation/executor-address', {
|
|
400
|
-
|
|
635
|
+
...authHeaders,
|
|
401
636
|
});
|
|
402
637
|
sessionAccountAddress = String(executorAddressResponse?.data?.executorAddress || '').trim();
|
|
403
638
|
} catch (error) {
|
|
@@ -419,7 +654,7 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
|
|
|
419
654
|
expirySeconds: Number(flags.expirySeconds || 86_400),
|
|
420
655
|
};
|
|
421
656
|
const response = await apiPost(ctx, '/api/v1/agent/delegation/create', payload, {
|
|
422
|
-
|
|
657
|
+
...authHeaders,
|
|
423
658
|
});
|
|
424
659
|
const success = Boolean(response?.success !== false);
|
|
425
660
|
printEnvelope({
|
|
@@ -434,8 +669,74 @@ async function commandDelegationCreate(ctx, flags, capabilities) {
|
|
|
434
669
|
});
|
|
435
670
|
}
|
|
436
671
|
|
|
672
|
+
async function commandDelegationInitCustodial(ctx, flags, capabilities) {
|
|
673
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
674
|
+
flags,
|
|
675
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation init-custodial'
|
|
676
|
+
);
|
|
677
|
+
const payload = {
|
|
678
|
+
chainId: Number(flags.chainId || 8453),
|
|
679
|
+
};
|
|
680
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
681
|
+
method: 'POST',
|
|
682
|
+
routes: ['/api/v1/agent/delegation/init-custodial'],
|
|
683
|
+
body: payload,
|
|
684
|
+
headers: {
|
|
685
|
+
...authHeaders,
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
const success = Boolean(response?.success !== false);
|
|
689
|
+
printEnvelope({
|
|
690
|
+
success,
|
|
691
|
+
code: success ? 'DELEGATION_CUSTODIAL_INITIALIZED' : 'DELEGATION_CUSTODIAL_INIT_FAILED',
|
|
692
|
+
message: response?.message || (success ? 'Custodial wallet initialized' : 'Custodial wallet init failed'),
|
|
693
|
+
data: response,
|
|
694
|
+
meta: {
|
|
695
|
+
apiBase: ctx.apiBase,
|
|
696
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function commandDelegationCustodialGrant(ctx, flags, capabilities) {
|
|
702
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
703
|
+
flags,
|
|
704
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation custodial-grant'
|
|
705
|
+
);
|
|
706
|
+
const payload = {
|
|
707
|
+
chainId: Number(flags.chainId || 8453),
|
|
708
|
+
agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
|
|
709
|
+
tokenAddress: requireFlag(flags, 'tokenAddress', 'Missing --token-address'),
|
|
710
|
+
maxUsdPerDay: Number(requireFlag(flags, 'maxUsdPerDay', 'Missing --max-usd-per-day')),
|
|
711
|
+
periodDuration: Number(flags.periodDuration || 86_400),
|
|
712
|
+
expirySeconds: Number(flags.expirySeconds || 86_400),
|
|
713
|
+
};
|
|
714
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
715
|
+
method: 'POST',
|
|
716
|
+
routes: ['/api/v1/agent/delegation/custodial-grant'],
|
|
717
|
+
body: payload,
|
|
718
|
+
headers: {
|
|
719
|
+
...authHeaders,
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
const success = Boolean(response?.success !== false);
|
|
723
|
+
printEnvelope({
|
|
724
|
+
success,
|
|
725
|
+
code: success ? 'DELEGATION_CUSTODIAL_GRANTED' : 'DELEGATION_CUSTODIAL_GRANT_FAILED',
|
|
726
|
+
message: response?.message || (success ? 'Custodial delegation granted' : 'Custodial delegation grant failed'),
|
|
727
|
+
data: response,
|
|
728
|
+
meta: {
|
|
729
|
+
apiBase: ctx.apiBase,
|
|
730
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
437
735
|
async function commandDelegationActivate(ctx, flags, capabilities) {
|
|
438
|
-
const
|
|
736
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
737
|
+
flags,
|
|
738
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation activate'
|
|
739
|
+
);
|
|
439
740
|
const payload = {
|
|
440
741
|
agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
|
|
441
742
|
walletAddress: requireFlag(flags, 'walletAddress', 'Missing --wallet-address'),
|
|
@@ -454,7 +755,7 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
|
|
|
454
755
|
if (flags.permissionData) payload.permissionData = parseJsonFlag(flags, 'permissionData');
|
|
455
756
|
if (flags.modelAConstraints) payload.modelAConstraints = parseJsonFlag(flags, 'modelAConstraints');
|
|
456
757
|
const response = await apiPost(ctx, '/api/v1/agent/delegation/activate', payload, {
|
|
457
|
-
|
|
758
|
+
...authHeaders,
|
|
458
759
|
});
|
|
459
760
|
const success = Boolean(response?.success !== false);
|
|
460
761
|
printEnvelope({
|
|
@@ -470,7 +771,10 @@ async function commandDelegationActivate(ctx, flags, capabilities) {
|
|
|
470
771
|
}
|
|
471
772
|
|
|
472
773
|
async function commandDelegationStatus(ctx, flags, capabilities) {
|
|
473
|
-
const
|
|
774
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
775
|
+
flags,
|
|
776
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation status'
|
|
777
|
+
);
|
|
474
778
|
const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
|
|
475
779
|
const chainId = Number(flags.chainId || 8453);
|
|
476
780
|
const verify = Boolean(flags.verify);
|
|
@@ -478,7 +782,7 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
|
|
|
478
782
|
? `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`
|
|
479
783
|
: `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
|
|
480
784
|
const response = await apiGet(ctx, route, {
|
|
481
|
-
|
|
785
|
+
...authHeaders,
|
|
482
786
|
});
|
|
483
787
|
const normalized = normalizeDelegationStatus(response);
|
|
484
788
|
printEnvelope({
|
|
@@ -495,6 +799,129 @@ async function commandDelegationStatus(ctx, flags, capabilities) {
|
|
|
495
799
|
});
|
|
496
800
|
}
|
|
497
801
|
|
|
802
|
+
async function commandDelegationInspect(ctx, flags, capabilities) {
|
|
803
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
804
|
+
flags,
|
|
805
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation inspect'
|
|
806
|
+
);
|
|
807
|
+
const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
|
|
808
|
+
const chainId = Number(flags.chainId || 8453);
|
|
809
|
+
const includeOnchain = Boolean(flags.verify || flags.includeOnchain || flags.onchain);
|
|
810
|
+
const checkRoute = `/api/v1/agent/delegation/check?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
|
|
811
|
+
const checkResponse = await apiGet(ctx, checkRoute, {
|
|
812
|
+
...authHeaders,
|
|
813
|
+
});
|
|
814
|
+
const checkNormalized = normalizeDelegationStatus(checkResponse);
|
|
815
|
+
|
|
816
|
+
let verifyNormalized = null;
|
|
817
|
+
if (includeOnchain) {
|
|
818
|
+
const verifyRoute = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
|
|
819
|
+
const verifyResponse = await apiGet(ctx, verifyRoute, {
|
|
820
|
+
...authHeaders,
|
|
821
|
+
});
|
|
822
|
+
verifyNormalized = normalizeDelegationStatus(verifyResponse);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
printEnvelope({
|
|
826
|
+
success: true,
|
|
827
|
+
code: 'DELEGATION_INSPECT_OK',
|
|
828
|
+
message: includeOnchain ? 'Delegation inspect loaded (with onchain verification)' : 'Delegation inspect loaded',
|
|
829
|
+
data: {
|
|
830
|
+
check: checkNormalized,
|
|
831
|
+
verify: verifyNormalized,
|
|
832
|
+
state: verifyNormalized?.state || checkNormalized.state,
|
|
833
|
+
rawStatus: verifyNormalized?.rawStatus || checkNormalized.rawStatus,
|
|
834
|
+
},
|
|
835
|
+
meta: {
|
|
836
|
+
apiBase: ctx.apiBase,
|
|
837
|
+
includeOnchain,
|
|
838
|
+
governanceState: verifyNormalized?.state || checkNormalized.state || null,
|
|
839
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function commandDelegationRevoke(ctx, flags, capabilities) {
|
|
845
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
846
|
+
flags,
|
|
847
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation revoke'
|
|
848
|
+
);
|
|
849
|
+
const payload = {
|
|
850
|
+
agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
|
|
851
|
+
reason: String(flags.reason || 'user_revoked').trim(),
|
|
852
|
+
chainId: Number(flags.chainId || 8453),
|
|
853
|
+
};
|
|
854
|
+
const response = await requestWithRouteFallback(ctx, {
|
|
855
|
+
method: 'POST',
|
|
856
|
+
routes: ['/api/v1/agent/delegation/revoke'],
|
|
857
|
+
body: payload,
|
|
858
|
+
headers: {
|
|
859
|
+
...authHeaders,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
const success = Boolean(response?.success !== false);
|
|
863
|
+
printEnvelope({
|
|
864
|
+
success,
|
|
865
|
+
code: success ? 'DELEGATION_REVOKED' : 'DELEGATION_REVOKE_FAILED',
|
|
866
|
+
message: response?.message || (success ? 'Delegation revoked' : 'Delegation revoke failed'),
|
|
867
|
+
data: response,
|
|
868
|
+
meta: {
|
|
869
|
+
apiBase: ctx.apiBase,
|
|
870
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function commandDelegationBalance(ctx, flags, capabilities) {
|
|
876
|
+
const authHeaders = buildOwnerAuthHeaders(
|
|
877
|
+
flags,
|
|
878
|
+
'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation balance'
|
|
879
|
+
);
|
|
880
|
+
const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
|
|
881
|
+
const chainId = Number(flags.chainId || 8453);
|
|
882
|
+
const route = `/api/v1/agent/delegation/verify?agentKey=${encodeURIComponent(agentKey)}&chainId=${encodeURIComponent(String(chainId))}`;
|
|
883
|
+
const response = await apiGet(ctx, route, {
|
|
884
|
+
...authHeaders,
|
|
885
|
+
});
|
|
886
|
+
const normalized = normalizeDelegationStatus(response);
|
|
887
|
+
const verifyData = normalized?.data?.onChainVerification || {};
|
|
888
|
+
printEnvelope({
|
|
889
|
+
success: true,
|
|
890
|
+
code: 'DELEGATION_BALANCE_OK',
|
|
891
|
+
message: 'Delegation allowance balance loaded',
|
|
892
|
+
data: {
|
|
893
|
+
state: normalized.state,
|
|
894
|
+
rawStatus: normalized.rawStatus,
|
|
895
|
+
delegatedWalletAddress: normalized?.data?.delegatedWalletAddress || null,
|
|
896
|
+
periodAvailableUsd: verifyData?.periodAvailableUsd ?? null,
|
|
897
|
+
periodAvailableBaseUnits: verifyData?.periodAvailableBaseUnits ?? null,
|
|
898
|
+
verified: verifyData?.verified ?? null,
|
|
899
|
+
isRevoked: verifyData?.isRevoked ?? null,
|
|
900
|
+
onChainVerification: verifyData || null,
|
|
901
|
+
},
|
|
902
|
+
meta: {
|
|
903
|
+
apiBase: ctx.apiBase,
|
|
904
|
+
governanceState: normalized.state || null,
|
|
905
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function commandDelegationRedeem(ctx, flags, capabilities) {
|
|
911
|
+
const selectedRunMode = getEffectiveRunMode(ctx, flags);
|
|
912
|
+
if (!['thirdfy', 'hybrid'].includes(selectedRunMode)) {
|
|
913
|
+
throw createCliError('DELEGATION_REDEEM_MODE_INVALID', 'delegation redeem supports only --run-mode thirdfy|hybrid');
|
|
914
|
+
}
|
|
915
|
+
const delegatedFlags = {
|
|
916
|
+
...flags,
|
|
917
|
+
runMode: selectedRunMode,
|
|
918
|
+
};
|
|
919
|
+
if (!delegatedFlags.estimatedAmountUsd) {
|
|
920
|
+
delegatedFlags.estimatedAmountUsd = '5';
|
|
921
|
+
}
|
|
922
|
+
await commandRun(ctx, delegatedFlags, capabilities);
|
|
923
|
+
}
|
|
924
|
+
|
|
498
925
|
async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
499
926
|
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
|
|
500
927
|
const payload = {
|
|
@@ -587,11 +1014,83 @@ async function commandAgentRegister(ctx, flags, capabilities) {
|
|
|
587
1014
|
? '/api/v1/agent/onboarding/register-with-wallet'
|
|
588
1015
|
: '/api/v1/agent/onboarding/register-with-privy';
|
|
589
1016
|
const response = await apiPost(ctx, route, payload, ownerHeaders);
|
|
1017
|
+
if (!flags.__suppressOutput) {
|
|
1018
|
+
printEnvelope({
|
|
1019
|
+
success: true,
|
|
1020
|
+
code: 'AGENT_REGISTERED',
|
|
1021
|
+
message: 'Agent registered',
|
|
1022
|
+
data: response,
|
|
1023
|
+
meta: {
|
|
1024
|
+
apiBase: ctx.apiBase,
|
|
1025
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
return response;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function commandDeveloperBootstrap(ctx, flags, capabilities) {
|
|
1033
|
+
const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
|
|
1034
|
+
const name = requireFlag(flags, 'name', 'Missing --name');
|
|
1035
|
+
let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
|
|
1036
|
+
let usedWalletFlow = false;
|
|
1037
|
+
const challengeId = String(flags.challengeId || '').trim();
|
|
1038
|
+
const signature = String(flags.signature || '').trim();
|
|
1039
|
+
|
|
1040
|
+
if (!ownerSessionToken && signature) {
|
|
1041
|
+
if (!challengeId) {
|
|
1042
|
+
throw new Error('Missing --challenge-id for wallet bootstrap. Run `agent auth challenge` first.');
|
|
1043
|
+
}
|
|
1044
|
+
const verify = await apiPost(ctx, '/api/v1/agent/onboarding/wallet/verify', {
|
|
1045
|
+
challengeId,
|
|
1046
|
+
signature,
|
|
1047
|
+
agentKey,
|
|
1048
|
+
...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
|
|
1049
|
+
});
|
|
1050
|
+
ownerSessionToken = String(verify?.ownerSessionToken || verify?.data?.ownerSessionToken || '').trim();
|
|
1051
|
+
usedWalletFlow = true;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const registerFlags = {
|
|
1055
|
+
...flags,
|
|
1056
|
+
agentKey,
|
|
1057
|
+
name,
|
|
1058
|
+
...(ownerSessionToken ? { ownerSessionToken } : {}),
|
|
1059
|
+
};
|
|
1060
|
+
const registerResponse = await commandAgentRegister(
|
|
1061
|
+
ctx,
|
|
1062
|
+
{ ...registerFlags, __suppressOutput: true },
|
|
1063
|
+
capabilities
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const config = loadProfileConfig();
|
|
1067
|
+
const next = {
|
|
1068
|
+
...config,
|
|
1069
|
+
auth: {
|
|
1070
|
+
...(config.auth && typeof config.auth === 'object' ? config.auth : {}),
|
|
1071
|
+
...(ownerSessionToken ? { ownerSessionToken } : {}),
|
|
1072
|
+
lastLoginAt: new Date().toISOString(),
|
|
1073
|
+
source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
|
|
1074
|
+
},
|
|
1075
|
+
developer: {
|
|
1076
|
+
...(config.developer && typeof config.developer === 'object' ? config.developer : {}),
|
|
1077
|
+
agentKey,
|
|
1078
|
+
bootstrapCompletedAt: new Date().toISOString(),
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
1081
|
+
persistProfileConfig(next);
|
|
590
1082
|
printEnvelope({
|
|
591
1083
|
success: true,
|
|
592
|
-
code: '
|
|
593
|
-
message: '
|
|
594
|
-
data:
|
|
1084
|
+
code: 'DEVELOPER_BOOTSTRAP_OK',
|
|
1085
|
+
message: 'Developer bootstrap completed',
|
|
1086
|
+
data: {
|
|
1087
|
+
registration: registerResponse,
|
|
1088
|
+
persisted: {
|
|
1089
|
+
ownerSessionTokenSaved: Boolean(ownerSessionToken),
|
|
1090
|
+
source: usedWalletFlow ? 'wallet-verify-bootstrap' : 'bootstrap',
|
|
1091
|
+
configPath: getProfileConfigPath(),
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
595
1094
|
meta: {
|
|
596
1095
|
apiBase: ctx.apiBase,
|
|
597
1096
|
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
@@ -706,10 +1205,156 @@ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
|
|
|
706
1205
|
});
|
|
707
1206
|
}
|
|
708
1207
|
|
|
1208
|
+
async function commandSelfExec(ctx, flags, capabilities) {
|
|
1209
|
+
const resolved = await resolveActionSelection(ctx, flags);
|
|
1210
|
+
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
1211
|
+
enforceChainCompatibility(capabilities, flags, 'self');
|
|
1212
|
+
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
|
1213
|
+
skipPreflight: Boolean(flags.skipPreflight),
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
if (!selfResult?.success) {
|
|
1217
|
+
const blocked = Boolean(selfResult?.preflightBlocked);
|
|
1218
|
+
printEnvelope({
|
|
1219
|
+
success: false,
|
|
1220
|
+
code: blocked ? 'PREFLIGHT_BLOCKED' : 'SELF_EXECUTION_FAILED',
|
|
1221
|
+
message: blocked ? 'Preflight blocked self execution' : 'Self execution preparation failed',
|
|
1222
|
+
data: selfResult || {},
|
|
1223
|
+
meta: {
|
|
1224
|
+
apiBase: ctx.apiBase,
|
|
1225
|
+
runMode: 'self',
|
|
1226
|
+
resolvedAction: resolved.resolvedAction,
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, selfResult);
|
|
1233
|
+
|
|
1234
|
+
printEnvelope({
|
|
1235
|
+
success: true,
|
|
1236
|
+
code: 'SELF_EXECUTED',
|
|
1237
|
+
message: 'Self-custody execution completed',
|
|
1238
|
+
data: executed,
|
|
1239
|
+
meta: {
|
|
1240
|
+
apiBase: ctx.apiBase,
|
|
1241
|
+
runMode: 'self',
|
|
1242
|
+
resolvedAction: resolved.resolvedAction,
|
|
1243
|
+
swapInput: flags.__swapInput || null,
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function commandDoctorSelf(ctx, flags) {
|
|
1249
|
+
const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
|
|
1250
|
+
const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
|
|
1251
|
+
const rpcUrl = String(
|
|
1252
|
+
flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
|
|
1253
|
+
).trim();
|
|
1254
|
+
const passphrasePresent = Boolean(String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim());
|
|
1255
|
+
const checks = [];
|
|
1256
|
+
|
|
1257
|
+
checks.push({
|
|
1258
|
+
name: 'wallet-address',
|
|
1259
|
+
ok: Boolean(walletAddress),
|
|
1260
|
+
detail: walletAddress || 'missing --wallet-address / MARKET_MAKER_WALLET_ADDRESS',
|
|
1261
|
+
});
|
|
1262
|
+
checks.push({
|
|
1263
|
+
name: 'ows-wallet-name',
|
|
1264
|
+
ok: Boolean(owsWalletName),
|
|
1265
|
+
detail: owsWalletName || 'missing --ows-wallet-name / OWS_WALLET_NAME',
|
|
1266
|
+
});
|
|
1267
|
+
checks.push({
|
|
1268
|
+
name: 'ows-passphrase',
|
|
1269
|
+
ok: passphrasePresent,
|
|
1270
|
+
detail: passphrasePresent ? 'present' : 'missing OWS_PASSPHRASE (or OWS_API_KEY fallback)',
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
try {
|
|
1274
|
+
resolveOwsBinary();
|
|
1275
|
+
checks.push({ name: 'ows-binary', ok: true, detail: 'found' });
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
checks.push({ name: 'ows-binary', ok: false, detail: error?.message || 'not found' });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
try {
|
|
1281
|
+
await loadEthers();
|
|
1282
|
+
checks.push({ name: 'ethers-runtime', ok: true, detail: 'loaded' });
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
checks.push({ name: 'ethers-runtime', ok: false, detail: error?.message || 'missing ethers dependency' });
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
const ethers = await loadEthers();
|
|
1289
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1290
|
+
const block = await provider.getBlockNumber();
|
|
1291
|
+
checks.push({ name: 'rpc-connectivity', ok: Number.isFinite(block), detail: `latestBlock=${block}` });
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
checks.push({ name: 'rpc-connectivity', ok: false, detail: error?.message || 'rpc check failed' });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const ok = checks.every((entry) => entry.ok);
|
|
1297
|
+
printEnvelope({
|
|
1298
|
+
success: ok,
|
|
1299
|
+
code: ok ? 'DOCTOR_SELF_OK' : 'DOCTOR_SELF_FAILED',
|
|
1300
|
+
message: ok ? 'Self lane signer diagnostics passed' : 'Self lane signer diagnostics found blocking issues',
|
|
1301
|
+
data: {
|
|
1302
|
+
checks,
|
|
1303
|
+
hints: [
|
|
1304
|
+
'For one-command self execution use: thirdfy-agent run --run-mode self --broadcast ...',
|
|
1305
|
+
'Set --effect-check strict|relaxed|off to control post-trade effect validation behavior.',
|
|
1306
|
+
],
|
|
1307
|
+
},
|
|
1308
|
+
meta: {
|
|
1309
|
+
apiBase: ctx.apiBase,
|
|
1310
|
+
runMode: 'self',
|
|
1311
|
+
rpcUrl,
|
|
1312
|
+
},
|
|
1313
|
+
});
|
|
1314
|
+
if (!ok) process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function commandManagedRun(ctx, flags, capabilities) {
|
|
1318
|
+
const mode = normalizeManagedRunMode(flags.runMode || 'thirdfy');
|
|
1319
|
+
const managedFlags = { ...flags, runMode: mode };
|
|
1320
|
+
const resolved = await resolveActionSelection(ctx, managedFlags);
|
|
1321
|
+
prepareActionParamsForFlags(managedFlags, resolved.resolvedAction);
|
|
1322
|
+
enforceChainCompatibility(capabilities, managedFlags, mode);
|
|
1323
|
+
const skipPreflight = Boolean(managedFlags.skipPreflight);
|
|
1324
|
+
const backendResult = await runExecutionByMode(mode, ctx, managedFlags, resolved, {
|
|
1325
|
+
skipPreflight,
|
|
1326
|
+
});
|
|
1327
|
+
const normalized = backendResult.normalized;
|
|
1328
|
+
printEnvelope({
|
|
1329
|
+
success: normalized.success,
|
|
1330
|
+
code: normalized.success ? 'MANAGED_RUN_QUEUED' : backendResult.code,
|
|
1331
|
+
message: normalized.success
|
|
1332
|
+
? mode === 'hybrid'
|
|
1333
|
+
? 'Managed easy-wallet hybrid execution prepared'
|
|
1334
|
+
: 'Managed easy-wallet execution queued'
|
|
1335
|
+
: backendResult.message,
|
|
1336
|
+
data: normalized,
|
|
1337
|
+
meta: {
|
|
1338
|
+
apiBase: ctx.apiBase,
|
|
1339
|
+
requestedAction: resolved.requestedAction,
|
|
1340
|
+
resolvedAction: resolved.resolvedAction,
|
|
1341
|
+
lane: 'managed_easy_wallet',
|
|
1342
|
+
runMode: mode,
|
|
1343
|
+
skippedPreflight: skipPreflight,
|
|
1344
|
+
profile: ctx.profile || null,
|
|
1345
|
+
swapInput: managedFlags.__swapInput || null,
|
|
1346
|
+
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
if (!normalized.success) process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
709
1352
|
async function commandProfileInit(ctx, flags) {
|
|
1353
|
+
const current = loadProfileConfig();
|
|
710
1354
|
const profile = normalizeProfileName(flags.profile || flags.name || 'personal');
|
|
711
1355
|
const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
|
|
712
1356
|
persistProfileConfig({
|
|
1357
|
+
...current,
|
|
713
1358
|
profile,
|
|
714
1359
|
runMode,
|
|
715
1360
|
});
|
|
@@ -734,6 +1379,7 @@ async function commandProfileUse(ctx, flags) {
|
|
|
734
1379
|
// `profile use` intentionally re-applies profile defaults unless caller pins a mode.
|
|
735
1380
|
const runMode = normalizeRunMode(flags.runMode || PROFILE_DEFAULTS[profile]);
|
|
736
1381
|
persistProfileConfig({
|
|
1382
|
+
...current,
|
|
737
1383
|
profile,
|
|
738
1384
|
runMode,
|
|
739
1385
|
});
|
|
@@ -770,6 +1416,154 @@ async function commandProfileShow(ctx, _flags) {
|
|
|
770
1416
|
});
|
|
771
1417
|
}
|
|
772
1418
|
|
|
1419
|
+
async function commandConfigSet(ctx, flags) {
|
|
1420
|
+
const key = String(flags.key || '').trim();
|
|
1421
|
+
const value = flags.value;
|
|
1422
|
+
if (!key) {
|
|
1423
|
+
throw new Error('Missing --key for config set');
|
|
1424
|
+
}
|
|
1425
|
+
const canonicalKey = key.toLowerCase();
|
|
1426
|
+
const hasValue = !(value === undefined || String(value).trim() === '');
|
|
1427
|
+
if (canonicalKey === 'profile' && hasValue) {
|
|
1428
|
+
normalizeProfileName(value);
|
|
1429
|
+
}
|
|
1430
|
+
if (canonicalKey === 'runmode' && hasValue) {
|
|
1431
|
+
normalizeRunMode(value);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const current = loadProfileConfig();
|
|
1435
|
+
const next = setNestedConfigValue(current, key, value);
|
|
1436
|
+
persistProfileConfig(next);
|
|
1437
|
+
printEnvelope({
|
|
1438
|
+
success: true,
|
|
1439
|
+
code: 'CONFIG_UPDATED',
|
|
1440
|
+
message: 'Configuration updated',
|
|
1441
|
+
data: {
|
|
1442
|
+
key,
|
|
1443
|
+
value: value === undefined ? null : String(value),
|
|
1444
|
+
configPath: getProfileConfigPath(),
|
|
1445
|
+
},
|
|
1446
|
+
meta: { apiBase: ctx.apiBase },
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function commandLogin(ctx, flags) {
|
|
1451
|
+
const current = loadProfileConfig();
|
|
1452
|
+
const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || '').trim();
|
|
1453
|
+
let ownerSessionToken = String(flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || '').trim();
|
|
1454
|
+
let loginSource = 'token';
|
|
1455
|
+
|
|
1456
|
+
if (!authToken && !ownerSessionToken) {
|
|
1457
|
+
const challengeId = String(flags.challengeId || '').trim();
|
|
1458
|
+
const signature = String(flags.signature || '').trim();
|
|
1459
|
+
const agentKey = String(flags.agentKey || '').trim();
|
|
1460
|
+
if (challengeId && signature && agentKey) {
|
|
1461
|
+
const verifyResponse = await requestWithRouteFallback(ctx, {
|
|
1462
|
+
method: 'POST',
|
|
1463
|
+
routes: ['/api/v1/agent/onboarding/wallet/verify'],
|
|
1464
|
+
body: {
|
|
1465
|
+
challengeId,
|
|
1466
|
+
signature,
|
|
1467
|
+
agentKey,
|
|
1468
|
+
...(flags.walletAddress ? { walletAddress: String(flags.walletAddress).trim() } : {}),
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
ownerSessionToken = String(
|
|
1472
|
+
verifyResponse?.ownerSessionToken || verifyResponse?.data?.ownerSessionToken || ''
|
|
1473
|
+
).trim();
|
|
1474
|
+
loginSource = 'wallet-verify';
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (!authToken && !ownerSessionToken) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
'Missing login credentials: provide --auth-token, --owner-session-token, or challenge verify inputs (--challenge-id/--signature/--agent-key).'
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const next = {
|
|
1485
|
+
...current,
|
|
1486
|
+
auth: {
|
|
1487
|
+
...(current.auth && typeof current.auth === 'object' ? current.auth : {}),
|
|
1488
|
+
lastLoginAt: new Date().toISOString(),
|
|
1489
|
+
source: loginSource,
|
|
1490
|
+
},
|
|
1491
|
+
};
|
|
1492
|
+
if (authToken) {
|
|
1493
|
+
next.auth.authToken = authToken;
|
|
1494
|
+
delete next.auth.ownerSessionToken;
|
|
1495
|
+
} else if (ownerSessionToken) {
|
|
1496
|
+
next.auth.ownerSessionToken = ownerSessionToken;
|
|
1497
|
+
delete next.auth.authToken;
|
|
1498
|
+
}
|
|
1499
|
+
if (flags.agentApiKey) {
|
|
1500
|
+
next.agent = {
|
|
1501
|
+
...(current.agent && typeof current.agent === 'object' ? current.agent : {}),
|
|
1502
|
+
apiKey: String(flags.agentApiKey).trim(),
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
persistProfileConfig(next);
|
|
1506
|
+
printEnvelope({
|
|
1507
|
+
success: true,
|
|
1508
|
+
code: 'LOGIN_OK',
|
|
1509
|
+
message: 'Login credentials stored',
|
|
1510
|
+
data: {
|
|
1511
|
+
hasAuthToken: Boolean(authToken),
|
|
1512
|
+
hasOwnerSessionToken: Boolean(ownerSessionToken),
|
|
1513
|
+
hasAgentApiKey: Boolean(flags.agentApiKey),
|
|
1514
|
+
configPath: getProfileConfigPath(),
|
|
1515
|
+
},
|
|
1516
|
+
meta: { apiBase: ctx.apiBase },
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function commandLogout(ctx, flags) {
|
|
1521
|
+
const current = loadProfileConfig();
|
|
1522
|
+
const localOnly = flags.all ? false : true;
|
|
1523
|
+
const auth = current.auth && typeof current.auth === 'object' ? current.auth : {};
|
|
1524
|
+
const ownerSessionToken = String(auth.ownerSessionToken || '').trim();
|
|
1525
|
+
let revokeAttempted = false;
|
|
1526
|
+
let revokeSuccess = false;
|
|
1527
|
+
let revokeError = null;
|
|
1528
|
+
|
|
1529
|
+
if (!localOnly && ownerSessionToken) {
|
|
1530
|
+
revokeAttempted = true;
|
|
1531
|
+
try {
|
|
1532
|
+
await requestWithRouteFallback(ctx, {
|
|
1533
|
+
method: 'POST',
|
|
1534
|
+
routes: ['/api/v1/agent/onboarding/wallet/logout', '/api/v1/agent/onboarding/wallet/revoke-session'],
|
|
1535
|
+
body: {},
|
|
1536
|
+
headers: {
|
|
1537
|
+
'x-owner-session-token': ownerSessionToken,
|
|
1538
|
+
},
|
|
1539
|
+
});
|
|
1540
|
+
revokeSuccess = true;
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
revokeError = error?.message || 'OWNER_SESSION_REVOKE_FAILED';
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const next = { ...current };
|
|
1547
|
+
delete next.auth;
|
|
1548
|
+
if (flags.clearAgentKey) {
|
|
1549
|
+
delete next.agent;
|
|
1550
|
+
}
|
|
1551
|
+
persistProfileConfig(next);
|
|
1552
|
+
printEnvelope({
|
|
1553
|
+
success: true,
|
|
1554
|
+
code: 'LOGOUT_OK',
|
|
1555
|
+
message: 'Local credentials cleared',
|
|
1556
|
+
data: {
|
|
1557
|
+
localOnly,
|
|
1558
|
+
revokeAttempted,
|
|
1559
|
+
revokeSuccess,
|
|
1560
|
+
revokeError,
|
|
1561
|
+
configPath: getProfileConfigPath(),
|
|
1562
|
+
},
|
|
1563
|
+
meta: { apiBase: ctx.apiBase },
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
773
1567
|
function resolveProfileState(flags) {
|
|
774
1568
|
const persisted = loadProfileConfig();
|
|
775
1569
|
const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
|
|
@@ -800,6 +1594,14 @@ function normalizeRunMode(value) {
|
|
|
800
1594
|
throw new Error(`Unsupported --run-mode "${value}". Supported: ${RUN_MODES.join(', ')}`);
|
|
801
1595
|
}
|
|
802
1596
|
|
|
1597
|
+
function normalizeManagedRunMode(value) {
|
|
1598
|
+
const mode = normalizeRunMode(value || 'thirdfy');
|
|
1599
|
+
if (mode === 'self') {
|
|
1600
|
+
throw new Error('Managed lane does not support run-mode=self. Use thirdfy or hybrid.');
|
|
1601
|
+
}
|
|
1602
|
+
return mode;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
803
1605
|
function getProfileConfigPath() {
|
|
804
1606
|
return path.join(os.homedir(), '.thirdfy', 'config.json');
|
|
805
1607
|
}
|
|
@@ -818,8 +1620,60 @@ function loadProfileConfig() {
|
|
|
818
1620
|
function persistProfileConfig(config) {
|
|
819
1621
|
const configPath = getProfileConfigPath();
|
|
820
1622
|
const dirPath = path.dirname(configPath);
|
|
821
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
822
|
-
|
|
1623
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
1624
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
1625
|
+
fs.writeFileSync(configPath, payload, { encoding: 'utf8', mode: 0o600 });
|
|
1626
|
+
try {
|
|
1627
|
+
fs.chmodSync(configPath, 0o600);
|
|
1628
|
+
} catch {
|
|
1629
|
+
/* chmod unsupported or restricted (e.g. some Windows FS) */
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
fs.chmodSync(dirPath, 0o700);
|
|
1633
|
+
} catch {
|
|
1634
|
+
/* ignore */
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function clonePlainConfigTree(input) {
|
|
1639
|
+
const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
|
1640
|
+
try {
|
|
1641
|
+
return structuredClone(obj);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return JSON.parse(JSON.stringify(obj));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function setNestedConfigValue(config, keyPath, value) {
|
|
1648
|
+
const next = clonePlainConfigTree(config || {});
|
|
1649
|
+
const parts = String(keyPath || '')
|
|
1650
|
+
.split('.')
|
|
1651
|
+
.map((v) => v.trim())
|
|
1652
|
+
.filter(Boolean);
|
|
1653
|
+
const blocked = new Set(['__proto__', 'prototype', 'constructor']);
|
|
1654
|
+
if (parts.some((part) => blocked.has(part))) {
|
|
1655
|
+
throw createCliError(
|
|
1656
|
+
'CONFIG_KEY_INVALID',
|
|
1657
|
+
'Invalid config key path: reserved segments (__proto__, prototype, constructor) are not allowed.'
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
if (!parts.length) return next;
|
|
1661
|
+
let cursor = next;
|
|
1662
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
1663
|
+
const key = parts[i];
|
|
1664
|
+
const existing = cursor[key];
|
|
1665
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
1666
|
+
cursor[key] = {};
|
|
1667
|
+
}
|
|
1668
|
+
cursor = cursor[key];
|
|
1669
|
+
}
|
|
1670
|
+
const finalKey = parts[parts.length - 1];
|
|
1671
|
+
if (value === undefined || String(value).trim() === '') {
|
|
1672
|
+
delete cursor[finalKey];
|
|
1673
|
+
} else {
|
|
1674
|
+
cursor[finalKey] = String(value);
|
|
1675
|
+
}
|
|
1676
|
+
return next;
|
|
823
1677
|
}
|
|
824
1678
|
|
|
825
1679
|
function getEffectiveRunMode(ctx, flags) {
|
|
@@ -1030,10 +1884,11 @@ async function executeHybridRun(ctx, flags, resolved, options) {
|
|
|
1030
1884
|
}
|
|
1031
1885
|
|
|
1032
1886
|
function buildBuildTxPayload(flags, resolvedAction) {
|
|
1887
|
+
const runMode = String(flags.runMode || '').trim().toLowerCase() || 'self';
|
|
1033
1888
|
const payload = {
|
|
1034
|
-
agentApiKey:
|
|
1889
|
+
agentApiKey: resolveAgentApiKey(flags, runMode),
|
|
1035
1890
|
action: String(resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
|
|
1036
|
-
params:
|
|
1891
|
+
params: getPreparedParams(flags),
|
|
1037
1892
|
};
|
|
1038
1893
|
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
1039
1894
|
if (flags.walletAddress) payload.walletAddress = String(flags.walletAddress).trim();
|
|
@@ -1041,10 +1896,11 @@ function buildBuildTxPayload(flags, resolvedAction) {
|
|
|
1041
1896
|
}
|
|
1042
1897
|
|
|
1043
1898
|
function buildIntentPayload(flags, options) {
|
|
1899
|
+
const runMode = String(options.runMode || flags.runMode || '').trim().toLowerCase() || 'thirdfy';
|
|
1044
1900
|
const payload = {
|
|
1045
|
-
agentApiKey:
|
|
1901
|
+
agentApiKey: resolveAgentApiKey(flags, runMode),
|
|
1046
1902
|
action: String(options.resolvedAction || requireFlag(flags, 'action', 'Missing --action')).trim(),
|
|
1047
|
-
params:
|
|
1903
|
+
params: getPreparedParams(flags),
|
|
1048
1904
|
};
|
|
1049
1905
|
if (flags.catalog) payload.catalog = String(flags.catalog).trim();
|
|
1050
1906
|
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
@@ -1065,6 +1921,22 @@ function buildIntentPayload(flags, options) {
|
|
|
1065
1921
|
return payload;
|
|
1066
1922
|
}
|
|
1067
1923
|
|
|
1924
|
+
function resolveAgentApiKey(flags, runMode) {
|
|
1925
|
+
const explicit = String(flags.agentApiKey || '').trim();
|
|
1926
|
+
if (explicit) return explicit;
|
|
1927
|
+
const envKey = String(process.env.THIRDFY_AGENT_API_KEY || '').trim();
|
|
1928
|
+
if (envKey) return envKey;
|
|
1929
|
+
const config = loadProfileConfig();
|
|
1930
|
+
const configKey = String(config?.agent?.apiKey || '').trim();
|
|
1931
|
+
if (configKey) return configKey;
|
|
1932
|
+
if (runMode === 'self') {
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
'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.'
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
throw new Error('Missing --agent-api-key (or THIRDFY_AGENT_API_KEY / config.agent.apiKey)');
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1068
1940
|
function getActionKey(action) {
|
|
1069
1941
|
return String(action?.action || action?.id || action?.key || action?.name || '')
|
|
1070
1942
|
.trim();
|
|
@@ -1188,14 +2060,18 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
1188
2060
|
}
|
|
1189
2061
|
|
|
1190
2062
|
function getAuthToken(flags, missingMessage) {
|
|
1191
|
-
const
|
|
2063
|
+
const config = loadProfileConfig();
|
|
2064
|
+
const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
|
|
1192
2065
|
if (!authToken) throw new Error(missingMessage);
|
|
1193
2066
|
return authToken;
|
|
1194
2067
|
}
|
|
1195
2068
|
|
|
1196
2069
|
function buildOwnerAuthHeaders(flags, missingMessage) {
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
2070
|
+
const config = loadProfileConfig();
|
|
2071
|
+
const authToken = String(flags.authToken || process.env.THIRDFY_AUTH_TOKEN || config?.auth?.authToken || '').trim();
|
|
2072
|
+
const ownerSessionToken = String(
|
|
2073
|
+
flags.ownerSessionToken || process.env.THIRDFY_OWNER_SESSION_TOKEN || config?.auth?.ownerSessionToken || ''
|
|
2074
|
+
).trim();
|
|
1199
2075
|
if (!authToken && !ownerSessionToken) {
|
|
1200
2076
|
throw new Error(missingMessage);
|
|
1201
2077
|
}
|
|
@@ -1224,6 +2100,281 @@ async function negotiateCapabilities(ctx) {
|
|
|
1224
2100
|
}
|
|
1225
2101
|
}
|
|
1226
2102
|
|
|
2103
|
+
function resolveUnsignedTx(result) {
|
|
2104
|
+
if (result?.unsignedTx && typeof result.unsignedTx === 'object') return result.unsignedTx;
|
|
2105
|
+
if (result?.data?.unsignedTx && typeof result.data.unsignedTx === 'object') return result.data.unsignedTx;
|
|
2106
|
+
if (result?.details?.unsignedTx && typeof result.details.unsignedTx === 'object') return result.details.unsignedTx;
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
function parseBooleanFlag(value, defaultValue = false) {
|
|
2111
|
+
if (value === undefined || value === null) return defaultValue;
|
|
2112
|
+
if (typeof value === 'boolean') return value;
|
|
2113
|
+
const normalized = String(value).trim().toLowerCase();
|
|
2114
|
+
if (!normalized) return defaultValue;
|
|
2115
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
2116
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
2117
|
+
return defaultValue;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function normalizeEffectCheckMode(value) {
|
|
2121
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
2122
|
+
if (!normalized) return 'relaxed';
|
|
2123
|
+
if (EFFECT_CHECK_MODES.includes(normalized)) return normalized;
|
|
2124
|
+
throw createCliError(
|
|
2125
|
+
'EFFECT_CHECK_MODE_INVALID',
|
|
2126
|
+
`Unsupported --effect-check "${value}". Supported: ${EFFECT_CHECK_MODES.join(', ')}.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
async function sleepMs(ms) {
|
|
2131
|
+
const delay = Number(ms || 0);
|
|
2132
|
+
if (!Number.isFinite(delay) || delay <= 0) return;
|
|
2133
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async function resolveEffectCheckResult({
|
|
2137
|
+
mode,
|
|
2138
|
+
rpcUrl,
|
|
2139
|
+
walletAddress,
|
|
2140
|
+
tokenIn,
|
|
2141
|
+
tokenOut,
|
|
2142
|
+
balancesBefore,
|
|
2143
|
+
retries,
|
|
2144
|
+
waitMs,
|
|
2145
|
+
}) {
|
|
2146
|
+
const baseResult = {
|
|
2147
|
+
checked: true,
|
|
2148
|
+
mode,
|
|
2149
|
+
hasEffect: false,
|
|
2150
|
+
inDeltaRaw: '0',
|
|
2151
|
+
outDeltaRaw: '0',
|
|
2152
|
+
attempts: 0,
|
|
2153
|
+
retries: Math.max(0, Number(retries || 0)),
|
|
2154
|
+
};
|
|
2155
|
+
if (mode === 'off') {
|
|
2156
|
+
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
|
|
2157
|
+
}
|
|
2158
|
+
if (!tokenIn || !tokenOut || !balancesBefore) {
|
|
2159
|
+
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
|
|
2160
|
+
}
|
|
2161
|
+
const maxAttempts = Math.max(1, Number(retries || 0) + 1);
|
|
2162
|
+
let last = null;
|
|
2163
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2164
|
+
const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
|
|
2165
|
+
const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
|
|
2166
|
+
const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
|
|
2167
|
+
const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
|
|
2168
|
+
last = {
|
|
2169
|
+
checked: true,
|
|
2170
|
+
mode,
|
|
2171
|
+
hasEffect,
|
|
2172
|
+
inDeltaRaw,
|
|
2173
|
+
outDeltaRaw,
|
|
2174
|
+
attempts: attempt,
|
|
2175
|
+
retries: maxAttempts - 1,
|
|
2176
|
+
};
|
|
2177
|
+
if (hasEffect) return last;
|
|
2178
|
+
if (attempt < maxAttempts) await sleepMs(waitMs);
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
...(last || baseResult),
|
|
2182
|
+
warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
2187
|
+
const txd = resolveUnsignedTx(selfResult);
|
|
2188
|
+
if (!txd?.to || !txd?.data) {
|
|
2189
|
+
throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
|
|
2190
|
+
}
|
|
2191
|
+
const chainId = Number(txd.chainId || flags.chainId || 8453);
|
|
2192
|
+
const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
|
|
2193
|
+
const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
|
|
2194
|
+
const rpcUrl = String(
|
|
2195
|
+
flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
|
|
2196
|
+
).trim();
|
|
2197
|
+
if (!walletAddress) {
|
|
2198
|
+
throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
|
|
2199
|
+
}
|
|
2200
|
+
if (!owsWalletName) {
|
|
2201
|
+
throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const params = getPreparedParams(flags);
|
|
2205
|
+
const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
|
|
2206
|
+
const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
|
|
2207
|
+
const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
|
|
2208
|
+
const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
|
|
2209
|
+
const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
|
|
2210
|
+
|
|
2211
|
+
let balancesBefore = null;
|
|
2212
|
+
if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
|
|
2213
|
+
balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
let submission;
|
|
2217
|
+
try {
|
|
2218
|
+
submission = await signAndBroadcastWithOws({
|
|
2219
|
+
txd,
|
|
2220
|
+
chainId,
|
|
2221
|
+
walletAddress,
|
|
2222
|
+
owsWalletName,
|
|
2223
|
+
rpcUrl,
|
|
2224
|
+
});
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
let receipt;
|
|
2230
|
+
try {
|
|
2231
|
+
receipt = await submission.sent.wait();
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
|
|
2234
|
+
}
|
|
2235
|
+
const status = Number(receipt?.status ?? 0);
|
|
2236
|
+
if (status !== 1) {
|
|
2237
|
+
throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
|
|
2238
|
+
receiptStatus: receipt?.status ?? null,
|
|
2239
|
+
txHash: submission.sent.hash,
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const effectCheck = await resolveEffectCheckResult({
|
|
2244
|
+
mode: effectMode,
|
|
2245
|
+
rpcUrl,
|
|
2246
|
+
walletAddress,
|
|
2247
|
+
tokenIn: effectTokenIn,
|
|
2248
|
+
tokenOut: effectTokenOut,
|
|
2249
|
+
balancesBefore,
|
|
2250
|
+
retries: effectRetries,
|
|
2251
|
+
waitMs: effectWaitMs,
|
|
2252
|
+
});
|
|
2253
|
+
if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
|
|
2254
|
+
throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
|
|
2255
|
+
txHash: submission.sent.hash,
|
|
2256
|
+
effectCheck,
|
|
2257
|
+
mode: effectMode,
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
return {
|
|
2262
|
+
txHash: submission.sent.hash,
|
|
2263
|
+
chainId,
|
|
2264
|
+
blockNumber: receipt?.blockNumber ?? null,
|
|
2265
|
+
status: receipt?.status ?? null,
|
|
2266
|
+
unsignedTx: txd,
|
|
2267
|
+
effectCheck,
|
|
2268
|
+
lane: 'self',
|
|
2269
|
+
signer: 'ows',
|
|
2270
|
+
resolvedAction,
|
|
2271
|
+
apiBase: ctx.apiBase,
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async function loadEthers() {
|
|
2276
|
+
const candidates = ['ethers', path.join(process.cwd(), 'node_modules', 'ethers')];
|
|
2277
|
+
for (const candidate of candidates) {
|
|
2278
|
+
try {
|
|
2279
|
+
return require(candidate);
|
|
2280
|
+
} catch {
|
|
2281
|
+
// continue
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
throw new Error('Could not load ethers dependency for self-exec');
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function resolveOwsBinary() {
|
|
2288
|
+
const explicit = String(process.env.OWS_CLI_PATH || '').trim();
|
|
2289
|
+
if (explicit && fs.existsSync(explicit)) return explicit;
|
|
2290
|
+
const localBin = path.join(process.cwd(), 'node_modules', '.bin', 'ows');
|
|
2291
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
2292
|
+
const which = spawnSync('which', ['ows'], { encoding: 'utf-8' });
|
|
2293
|
+
const discovered = String(which.stdout || '').trim();
|
|
2294
|
+
if (discovered) return discovered;
|
|
2295
|
+
throw new Error('ows CLI not found. Install it or set OWS_CLI_PATH.');
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function signUnsignedTxWithOws(unsignedHex, walletName, chainId) {
|
|
2299
|
+
const passphrase = String(process.env.OWS_PASSPHRASE || process.env.OWS_API_KEY || '').trim();
|
|
2300
|
+
if (!passphrase) {
|
|
2301
|
+
throw new Error('Set OWS_PASSPHRASE (or OWS_API_KEY) for self-exec signing.');
|
|
2302
|
+
}
|
|
2303
|
+
const ows = resolveOwsBinary();
|
|
2304
|
+
const txHex = String(unsignedHex).startsWith('0x') ? String(unsignedHex).slice(2) : String(unsignedHex);
|
|
2305
|
+
const res = spawnSync(
|
|
2306
|
+
ows,
|
|
2307
|
+
['sign', 'tx', '--wallet', walletName, '--chain', `eip155:${chainId}`, '--tx', txHex, '--json'],
|
|
2308
|
+
{ env: { ...process.env, OWS_PASSPHRASE: passphrase }, encoding: 'utf-8' }
|
|
2309
|
+
);
|
|
2310
|
+
const out = String(res.stdout || '').trim();
|
|
2311
|
+
if (res.status !== 0 || !out) {
|
|
2312
|
+
throw new Error(`ows sign tx failed: ${String(res.stderr || out || res.status).slice(0, 300)}`);
|
|
2313
|
+
}
|
|
2314
|
+
let parsed;
|
|
2315
|
+
try {
|
|
2316
|
+
parsed = JSON.parse(out);
|
|
2317
|
+
} catch {
|
|
2318
|
+
throw new Error(`ows sign tx returned non-json payload: ${out.slice(0, 200)}`);
|
|
2319
|
+
}
|
|
2320
|
+
const signature = String(parsed.signature || '').replace(/^0x/i, '');
|
|
2321
|
+
if (signature.length < 128) {
|
|
2322
|
+
throw new Error('Invalid OWS signature payload');
|
|
2323
|
+
}
|
|
2324
|
+
return {
|
|
2325
|
+
recoveryId: Number(parsed.recovery_id),
|
|
2326
|
+
r: `0x${signature.slice(0, 64)}`,
|
|
2327
|
+
s: `0x${signature.slice(64, 128)}`,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletName, rpcUrl }) {
|
|
2332
|
+
const ethers = await loadEthers();
|
|
2333
|
+
const { JsonRpcProvider, Transaction, Signature } = ethers;
|
|
2334
|
+
const provider = new JsonRpcProvider(rpcUrl, chainId);
|
|
2335
|
+
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
|
2336
|
+
const fee = await provider.getFeeData();
|
|
2337
|
+
const txFields = {
|
|
2338
|
+
type: 2,
|
|
2339
|
+
chainId,
|
|
2340
|
+
nonce,
|
|
2341
|
+
to: String(txd.to).toLowerCase(),
|
|
2342
|
+
data: txd.data,
|
|
2343
|
+
value: txd.value ? BigInt(txd.value) : 0n,
|
|
2344
|
+
gasLimit: txd.gas ? BigInt(txd.gas) : txd.gasLimit ? BigInt(txd.gasLimit) : 500000n,
|
|
2345
|
+
maxFeePerGas: fee.maxFeePerGas ?? fee.gasPrice ?? 2_000_000_000n,
|
|
2346
|
+
maxPriorityFeePerGas: fee.maxPriorityFeePerGas ?? 1_000_000_000n,
|
|
2347
|
+
};
|
|
2348
|
+
const unsigned = Transaction.from(txFields);
|
|
2349
|
+
const sigParts = signUnsignedTxWithOws(unsigned.unsignedSerialized, owsWalletName, chainId);
|
|
2350
|
+
const sig = Signature.from({ r: sigParts.r, s: sigParts.s, yParity: sigParts.recoveryId });
|
|
2351
|
+
const tx = Transaction.from(unsigned.unsignedSerialized);
|
|
2352
|
+
tx.signature = sig;
|
|
2353
|
+
const sent = await provider.broadcastTransaction(tx.serialized);
|
|
2354
|
+
return { sent };
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
|
|
2358
|
+
const ethers = await loadEthers();
|
|
2359
|
+
const { JsonRpcProvider, Interface } = ethers;
|
|
2360
|
+
const provider = new JsonRpcProvider(rpcUrl);
|
|
2361
|
+
const erc20 = new Interface(['function balanceOf(address owner) view returns (uint256)']);
|
|
2362
|
+
const response = await provider.call({
|
|
2363
|
+
to: tokenAddress,
|
|
2364
|
+
data: erc20.encodeFunctionData('balanceOf', [walletAddress]),
|
|
2365
|
+
});
|
|
2366
|
+
const decoded = erc20.decodeFunctionResult('balanceOf', response);
|
|
2367
|
+
return BigInt(decoded[0].toString()).toString();
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut) {
|
|
2371
|
+
const [inBal, outBal] = await Promise.all([
|
|
2372
|
+
readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn),
|
|
2373
|
+
readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut),
|
|
2374
|
+
]);
|
|
2375
|
+
return { tokenIn: inBal, tokenOut: outBal };
|
|
2376
|
+
}
|
|
2377
|
+
|
|
1227
2378
|
async function apiGet(ctx, route, headers = {}) {
|
|
1228
2379
|
return requestJson(ctx, route, { method: 'GET', headers });
|
|
1229
2380
|
}
|
|
@@ -1308,21 +2459,37 @@ Core commands:
|
|
|
1308
2459
|
thirdfy-agent profile init [--profile personal|builder|network] [--run-mode thirdfy|self|hybrid] [--json]
|
|
1309
2460
|
thirdfy-agent profile use --profile <name> [--run-mode thirdfy|self|hybrid] [--json]
|
|
1310
2461
|
thirdfy-agent profile show [--json]
|
|
2462
|
+
thirdfy-agent config set --key <path> --value <value> [--json]
|
|
2463
|
+
thirdfy-agent login [--auth-token <token> | --owner-session-token <token>] [--agent-api-key <key>] [--json]
|
|
2464
|
+
thirdfy-agent logout [--all] [--clear-agent-key] [--json]
|
|
1311
2465
|
thirdfy-agent whoami [--json]
|
|
1312
2466
|
thirdfy-agent catalogs list [--json]
|
|
1313
2467
|
thirdfy-agent actions [--catalog <id>] [--provider <id>] [--agent-api-key <key>] [--chain-id <id>] [--run-mode <mode>] [--json]
|
|
1314
2468
|
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]
|
|
2469
|
+
thirdfy-agent delegation init-custodial [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
|
|
2470
|
+
thirdfy-agent delegation custodial-grant [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
|
|
1315
2471
|
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
2472
|
thirdfy-agent delegation status --auth-token <token> --agent-key <key> [--verify] [--json]
|
|
2473
|
+
thirdfy-agent delegation show --auth-token <token> --agent-key <key> [--verify] [--json]
|
|
2474
|
+
thirdfy-agent delegation inspect --auth-token <token> --agent-key <key> [--verify] [--json]
|
|
2475
|
+
thirdfy-agent delegation balance --auth-token <token> --agent-key <key> [--chain-id <id>] [--json]
|
|
2476
|
+
thirdfy-agent delegation revoke --auth-token <token> --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
|
|
2477
|
+
thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--estimated-amount-usd <usd>] [--json]
|
|
2478
|
+
thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
|
|
2479
|
+
thirdfy-agent managed wallet grant [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
|
|
2480
|
+
thirdfy-agent managed run --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--idempotency-key <key>] [--json]
|
|
1317
2481
|
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
|
|
1318
2482
|
thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
|
|
1319
2483
|
thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
|
|
1320
2484
|
thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
|
|
2485
|
+
thirdfy-agent developer bootstrap --agent-key <key> --name <name> [--owner-session-token <token> | --challenge-id <id> --signature <hex>] [--json]
|
|
1321
2486
|
thirdfy-agent agent register --agent-key <key> --name <name> [--auth-token <token> | --owner-session-token <token>] [--json]
|
|
1322
2487
|
thirdfy-agent agent key rotate [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
|
|
1323
2488
|
thirdfy-agent agent key revoke [--agent-key <key>] [--auth-token <token> | --owner-session-token <token>] [--json]
|
|
1324
2489
|
thirdfy-agent preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
|
|
1325
|
-
thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--idempotency-key <key>] [--json]
|
|
2490
|
+
thirdfy-agent run --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--broadcast] [--idempotency-key <key>] [--json]
|
|
2491
|
+
thirdfy-agent self-exec --agent-api-key <key> --action <id> --params <json> --wallet-address <addr> --ows-wallet-name <name> [--rpc-url <url>] [--effect-check strict|relaxed|off] [--json]
|
|
2492
|
+
thirdfy-agent doctor self [--wallet-address <addr>] [--ows-wallet-name <name>] [--rpc-url <url>] [--json]
|
|
1326
2493
|
thirdfy-agent jeff preflight --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
|
|
1327
2494
|
thirdfy-agent jeff trade --agent-api-key <key> --action <id> --params <json> [--run-mode <mode>] [--json]
|
|
1328
2495
|
thirdfy-agent jeff status --intent-id <id> [--json]
|
|
@@ -1336,6 +2503,9 @@ Global flags:
|
|
|
1336
2503
|
--profile <name> personal | builder | network
|
|
1337
2504
|
--env <file> load env vars from file
|
|
1338
2505
|
--timeout <ms> request timeout
|
|
2506
|
+
--params <json> for swap: exactly one of {amountInRaw} or {amountInHuman + tokenInDecimals}
|
|
2507
|
+
--broadcast with run --run-mode self, sign and broadcast using OWS
|
|
2508
|
+
--effect-check <mode> self-exec check mode: strict | relaxed | off (default: relaxed)
|
|
1339
2509
|
--owner-session-token <token> owner session token (or THIRDFY_OWNER_SESSION_TOKEN)
|
|
1340
2510
|
--json stable machine-readable output
|
|
1341
2511
|
--verbose print adapter diagnostics
|