@thirdfy/agent-cli 0.1.46 → 0.2.1

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +16 -1
  3. package/bin/thirdfy-agent.mjs +3 -5037
  4. package/package.json +17 -4
  5. package/scripts/validate-npm-pack.mjs +43 -0
  6. package/src/cli/errors.mjs +103 -0
  7. package/src/cli/flags.mjs +66 -0
  8. package/src/cli/help.mjs +89 -0
  9. package/src/cli/manifest.mjs +422 -0
  10. package/src/cli/options/agent.mjs +9 -0
  11. package/src/cli/options/analytics.mjs +5 -0
  12. package/src/cli/options/auth.mjs +10 -0
  13. package/src/cli/options/bootstrap.mjs +19 -0
  14. package/src/cli/options/chains.mjs +3 -0
  15. package/src/cli/options/credentials.mjs +15 -0
  16. package/src/cli/options/delegation.mjs +22 -0
  17. package/src/cli/options/execution.mjs +24 -0
  18. package/src/cli/options/global.mjs +16 -0
  19. package/src/cli/options/index.mjs +37 -0
  20. package/src/cli/options/polymarket.mjs +3 -0
  21. package/src/cli/options/tracking.mjs +18 -0
  22. package/src/cli/options/wallet.mjs +7 -0
  23. package/src/cli/program.mjs +5 -0
  24. package/src/cli/register.mjs +118 -0
  25. package/src/commands/agent.mjs +194 -0
  26. package/src/commands/bootstrap.mjs +762 -0
  27. package/src/commands/credentials.mjs +102 -0
  28. package/src/commands/delegation.mjs +355 -0
  29. package/src/commands/discovery.mjs +77 -0
  30. package/src/commands/doctor.mjs +320 -0
  31. package/src/commands/execute.mjs +255 -0
  32. package/src/commands/hyperliquid.mjs +51 -0
  33. package/src/commands/index.mjs +6 -0
  34. package/src/commands/polymarket.mjs +89 -0
  35. package/src/commands/prompt.mjs +33 -0
  36. package/src/commands/telemetry.mjs +146 -0
  37. package/src/commands/wallet.mjs +129 -0
  38. package/src/core/args.mjs +68 -0
  39. package/src/core/constants.mjs +31 -0
  40. package/src/core/context.mjs +145 -0
  41. package/src/core/envelope.mjs +20 -0
  42. package/src/core/http.mjs +77 -0
  43. package/src/core/index.mjs +6 -0
  44. package/src/core/runMode.mjs +72 -0
  45. package/src/runtime/actions/selection.mjs +339 -0
  46. package/src/runtime/agentExtract.mjs +52 -0
  47. package/src/runtime/authHelpers.mjs +53 -0
  48. package/src/runtime/bitfinexProbe.mjs +121 -0
  49. package/src/runtime/context.mjs +28 -0
  50. package/src/runtime/createThirdfyAgentRuntime.mjs +2 -0
  51. package/src/runtime/errors.mjs +3 -0
  52. package/src/runtime/execution/amounts.mjs +193 -0
  53. package/src/runtime/execution/lanes.mjs +75 -0
  54. package/src/runtime/execution/payloads.mjs +148 -0
  55. package/src/runtime/execution/runners.mjs +702 -0
  56. package/src/runtime/handlers.mjs +181 -0
  57. package/src/runtime/index.mjs +4 -0
  58. package/src/runtime/main.mjs +204 -0
  59. package/src/runtime/onboardingHints.mjs +54 -0
  60. package/src/runtime/owsSigning.mjs +118 -0
  61. package/src/runtime/providerHints.mjs +414 -0
@@ -0,0 +1,89 @@
1
+ import { loadProfileConfig } from '../core/context.mjs';
2
+ import { negotiateCapabilities } from '../core/http.mjs';
3
+ import { parseBooleanFlag } from '../core/runMode.mjs';
4
+
5
+ function extractAgentKey(value) {
6
+ if (!value || typeof value !== 'object') return '';
7
+ const candidates = [
8
+ value.agentKey,
9
+ value.data?.agentKey,
10
+ value.bootstrap?.agentKey,
11
+ value.registration?.agentKey,
12
+ value.registration?.registration?.agentKey,
13
+ value.data?.registration?.agentKey,
14
+ value.data?.registration?.registration?.agentKey,
15
+ value.owner?.creatorWallet,
16
+ value.data?.owner?.creatorWallet,
17
+ ];
18
+ return String(candidates.find((candidate) => String(candidate || '').trim()) || '').trim();
19
+ }
20
+
21
+ export function resolveConfigAgentKey(flags) {
22
+ const fromFlags = String(flags.agentKey || flags.agentApiKey || '').trim();
23
+ if (fromFlags) return fromFlags;
24
+ const config = loadProfileConfig();
25
+ const fromAgent = String(config?.agent?.agentKey || '').trim();
26
+ if (fromAgent) return fromAgent;
27
+ return extractAgentKey(config);
28
+ }
29
+
30
+ export function createPolymarketCommands({ commandPreflight, printEnvelope }) {
31
+ async function commandPolymarketStatus(ctx, flags) {
32
+ const agentKey = resolveConfigAgentKey(flags);
33
+ const capabilities = await negotiateCapabilities(ctx);
34
+ if (!flags.runMode) flags.runMode = 'agent_wallet';
35
+ if (agentKey) {
36
+ flags.action = 'get_polymarket_funding_status';
37
+ flags.params = JSON.stringify({
38
+ agentKey,
39
+ amountPusd: flags.amountPusd ? String(flags.amountPusd) : '5',
40
+ includeSetupStatus: true,
41
+ });
42
+ } else {
43
+ const params = {
44
+ includeBalance: parseBooleanFlag(flags.includeBalance, true),
45
+ includeGeoblock: parseBooleanFlag(flags.includeGeoblock, true),
46
+ };
47
+ flags.action = 'get_polymarket_setup_status';
48
+ flags.params = JSON.stringify(params);
49
+ }
50
+ await commandPreflight(ctx, flags, capabilities);
51
+ }
52
+
53
+ async function commandPolymarketSetup(ctx, flags) {
54
+ const capabilities = await negotiateCapabilities(ctx);
55
+ flags.action = 'get_polymarket_onboarding_plan';
56
+ const agentKey = resolveConfigAgentKey(flags);
57
+ flags.params =
58
+ flags.params ||
59
+ JSON.stringify({
60
+ ...(agentKey ? { agentKey } : {}),
61
+ });
62
+ if (!flags.runMode) flags.runMode = 'agent_wallet';
63
+ flags.__polymarketSetupGuide = true;
64
+ await commandPreflight(ctx, flags, capabilities);
65
+ }
66
+
67
+ async function commandPolymarketDryRun(ctx, flags) {
68
+ if (!flags.action) flags.action = 'place-polymarket-order';
69
+ if (!flags.params) {
70
+ printEnvelope({
71
+ success: false,
72
+ code: 'MISSING_PARAMS',
73
+ message: 'Provide --params for place_polymarket_order dry-run (tokenID, price, size, side)',
74
+ data: {},
75
+ meta: { apiBase: ctx.apiBase },
76
+ });
77
+ process.exit(1);
78
+ }
79
+ const capabilities = await negotiateCapabilities(ctx);
80
+ if (!flags.runMode) flags.runMode = 'agent_wallet';
81
+ await commandPreflight(ctx, flags, capabilities);
82
+ }
83
+
84
+ return {
85
+ commandPolymarketStatus,
86
+ commandPolymarketSetup,
87
+ commandPolymarketDryRun,
88
+ };
89
+ }
@@ -0,0 +1,33 @@
1
+ import { requireFlag } from '../core/args.mjs';
2
+ import { apiPost } from '../core/http.mjs';
3
+ export function createPromptCommands({ printEnvelope }) {
4
+ async function commandPrompt(ctx, flags, capabilities, promptTokens) {
5
+ const prompt = String(flags.prompt || promptTokens.join(' ') || '').trim();
6
+ if (!prompt) {
7
+ throw new Error('Missing prompt text. Use: thirdfy-agent prompt "<text>"');
8
+ }
9
+ // Jeff rail: deterministic wrapper around existing run contract.
10
+ printEnvelope({
11
+ success: true,
12
+ code: 'PROMPT_RECEIVED',
13
+ message: 'Prompt captured; execute with `jeff trade` or `run` using --run-mode.',
14
+ data: {
15
+ prompt,
16
+ suggestedCommands: [
17
+ 'thirdfy-agent jeff trade --run-mode self --agent-api-key ... --action ... --params ...',
18
+ 'thirdfy-agent jeff trade --run-mode hybrid --agent-api-key ... --action ... --params ...',
19
+ ],
20
+ },
21
+ meta: {
22
+ apiBase: ctx.apiBase,
23
+ profile: ctx.profile || null,
24
+ runMode: getEffectiveRunMode(ctx, flags),
25
+ capabilitiesVersion: capabilities?.contractVersion || null,
26
+ },
27
+ });
28
+ }
29
+
30
+ return {
31
+ commandPrompt,
32
+ };
33
+ }
@@ -0,0 +1,146 @@
1
+ import { requireFlag, parseJsonFlag } from '../core/args.mjs';
2
+ import { apiGet, apiPost } from '../core/http.mjs';
3
+ export function createTelemetryCommands({ printEnvelope, resolveAgentApiKey }) {
4
+ function applySharedPortfolioListingFiltersToQuery(query, flags) {
5
+ if (flags.chainId) query.set('chainId', String(flags.chainId));
6
+ if (flags.network) query.set('networkId', String(flags.network));
7
+ if (flags.networkId) query.set('networkId', String(flags.networkId));
8
+ if (flags.chainType) query.set('chainType', String(flags.chainType));
9
+ if (flags.category) query.set('category', String(flags.category));
10
+ }
11
+
12
+ function buildAgentKeyPortfolioSummaryQuery(flags, { includeLimit = false } = {}) {
13
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
14
+ const query = new URLSearchParams({ agentKey });
15
+ applySharedPortfolioListingFiltersToQuery(query, flags);
16
+ if (includeLimit && flags.limit) query.set('limit', String(flags.limit));
17
+ return { agentKey, query };
18
+ }
19
+
20
+ function metaPortfolioListingFilters(ctx, flags, capabilities, agentKey) {
21
+ return {
22
+ apiBase: ctx.apiBase,
23
+ agentKey,
24
+ chainId: flags.chainId ? Number(flags.chainId) : null,
25
+ networkId: flags.networkId || flags.network || null,
26
+ chainType: flags.chainType || null,
27
+ category: flags.category || null,
28
+ capabilitiesVersion: capabilities?.contractVersion || null,
29
+ };
30
+ }
31
+
32
+ async function commandDataSummary(ctx, flags, capabilities) {
33
+ const { agentKey, query } = buildAgentKeyPortfolioSummaryQuery(flags, { includeLimit: true });
34
+ const response = await apiGet(ctx, `/api/v1/agent/telemetry/summary?${query.toString()}`);
35
+ printEnvelope({
36
+ success: true,
37
+ code: 'DATA_SUMMARY_OK',
38
+ message: 'Agent data summary loaded',
39
+ data: response,
40
+ meta: metaPortfolioListingFilters(ctx, flags, capabilities, agentKey),
41
+ });
42
+ }
43
+
44
+ async function commandAnalyticsPortfolio(ctx, flags, capabilities) {
45
+ const { agentKey, query } = buildAgentKeyPortfolioSummaryQuery(flags);
46
+ const response = await apiGet(ctx, `/api/v1/agent/portfolio/summary?${query.toString()}`);
47
+ printEnvelope({
48
+ success: true,
49
+ code: 'ANALYTICS_PORTFOLIO_OK',
50
+ message: 'Agent portfolio summary loaded',
51
+ data: response,
52
+ meta: metaPortfolioListingFilters(ctx, flags, capabilities, agentKey),
53
+ });
54
+ }
55
+
56
+ async function commandAnalyticsLeaderboard(ctx, flags, capabilities) {
57
+ const query = new URLSearchParams();
58
+ const window = flags.window || '7d';
59
+ const metric = flags.metric || 'roiPct';
60
+ applySharedPortfolioListingFiltersToQuery(query, flags);
61
+ query.set('window', String(window));
62
+ query.set('metric', String(metric));
63
+ if (flags.limit) query.set('limit', String(flags.limit));
64
+ const response = await apiGet(ctx, `/api/v1/agent/portfolio/leaderboard?${query.toString()}`);
65
+ printEnvelope({
66
+ success: true,
67
+ code: 'ANALYTICS_LEADERBOARD_OK',
68
+ message: 'Agent portfolio leaderboard loaded',
69
+ data: response,
70
+ meta: {
71
+ apiBase: ctx.apiBase,
72
+ networkId: flags.networkId || flags.network || null,
73
+ chainType: flags.chainType || null,
74
+ category: flags.category || null,
75
+ window,
76
+ metric,
77
+ capabilitiesVersion: capabilities?.contractVersion || null,
78
+ },
79
+ });
80
+ }
81
+
82
+ async function commandTrackList(ctx, flags, capabilities, kind) {
83
+ const agentKey = requireFlag(flags, 'agentKey', 'Missing --agent-key');
84
+ const query = new URLSearchParams({ agentKey });
85
+ if (flags.limit) query.set('limit', String(flags.limit));
86
+ const response = await apiGet(ctx, `/api/v1/agent/telemetry/${kind}?${query.toString()}`);
87
+ printEnvelope({
88
+ success: true,
89
+ code: kind === 'events' ? 'TRACK_EVENTS_OK' : 'TRACK_ACTIONS_OK',
90
+ message: kind === 'events' ? 'Tracked agent events loaded' : 'Tracked agent actions loaded',
91
+ data: response,
92
+ meta: {
93
+ apiBase: ctx.apiBase,
94
+ agentKey,
95
+ kind,
96
+ },
97
+ });
98
+ }
99
+
100
+ async function commandTrackAction(ctx, flags, capabilities) {
101
+ const agentApiKey = resolveAgentApiKey(flags, 'thirdfy');
102
+ const action = requireFlag(flags, 'action', 'Missing --action');
103
+ const payload = {
104
+ agentApiKey,
105
+ action,
106
+ status: String(flags.status || 'observed'),
107
+ };
108
+ if (flags.agentKey) payload.agentKey = String(flags.agentKey);
109
+ if (flags.eventId) payload.eventId = String(flags.eventId);
110
+ if (flags.idempotencyKey) payload.idempotencyKey = String(flags.idempotencyKey);
111
+ if (flags.category) payload.category = String(flags.category);
112
+ if (flags.source) payload.source = String(flags.source);
113
+ if (flags.provider) payload.provider = String(flags.provider);
114
+ if (flags.protocol) payload.protocol = String(flags.protocol);
115
+ if (flags.namespace) payload.namespace = String(flags.namespace);
116
+ if (flags.actionType) payload.actionType = String(flags.actionType);
117
+ if (flags.chainId) payload.chainId = Number(flags.chainId);
118
+ if (flags.amountUsd) payload.amountUsd = Number(flags.amountUsd);
119
+ if (flags.txHash) payload.txHash = String(flags.txHash);
120
+ if (flags.userDid) payload.userDid = String(flags.userDid);
121
+ if (flags.params) payload.params = parseJsonFlag(flags, 'params');
122
+ if (flags.decision) payload.decision = parseJsonFlag(flags, 'decision');
123
+ if (flags.proof) payload.proof = parseJsonFlag(flags, 'proof');
124
+
125
+ const response = await apiPost(ctx, '/api/v1/agent/telemetry/actions', payload);
126
+ printEnvelope({
127
+ success: Boolean(response?.success !== false),
128
+ code: response?.success === false ? 'TRACK_ACTION_FAILED' : 'TRACK_ACTION_OK',
129
+ message: response?.success === false ? 'Agent action tracking failed' : 'Agent action tracked',
130
+ data: response,
131
+ meta: {
132
+ apiBase: ctx.apiBase,
133
+ action,
134
+ chainId: payload.chainId || null,
135
+ },
136
+ });
137
+ }
138
+
139
+ return {
140
+ commandDataSummary,
141
+ commandAnalyticsPortfolio,
142
+ commandAnalyticsLeaderboard,
143
+ commandTrackList,
144
+ commandTrackAction,
145
+ };
146
+ }
@@ -0,0 +1,129 @@
1
+ import { requireFlag } from '../core/args.mjs';
2
+ import { createCliError } from '../core/envelope.mjs';
3
+ import { loadProfileConfig } from '../core/context.mjs';
4
+
5
+ export function createWalletCommands(deps) {
6
+ const {
7
+ resolveActionSelection,
8
+ prepareActionParamsForFlags,
9
+ enforceOffchainVenueLaneCompatibility,
10
+ enforceChainCompatibility,
11
+ applyHyperliquidAgentWalletAddressDefault,
12
+ executeManagedWalletRun,
13
+ executeSelfRun,
14
+ submitSignedTx,
15
+ printEnvelope,
16
+ } = deps;
17
+
18
+
19
+ async function commandWalletExecute(ctx, flags, capabilities) {
20
+ const normalizedFlags = { ...flags, runMode: 'agent_wallet' };
21
+ const resolved = await resolveActionSelection(ctx, normalizedFlags);
22
+ prepareActionParamsForFlags(normalizedFlags, resolved.resolvedAction);
23
+ enforceChainCompatibility(capabilities, normalizedFlags, 'agent_wallet');
24
+ const managedPreflight = await applyHyperliquidAgentWalletAddressDefault(ctx, normalizedFlags, resolved, 'agent_wallet');
25
+ const skipPreflight = Boolean(normalizedFlags.skipPreflight);
26
+ const result = await executeManagedWalletRun(ctx, normalizedFlags, resolved, { skipPreflight, managedPreflight });
27
+ printEnvelope({
28
+ success: result.success,
29
+ code: result.success ? 'WALLET_EXECUTED' : result.preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
30
+ message: result.success ? 'Managed wallet execution completed' : result.error || 'Managed wallet execution failed',
31
+ data: result,
32
+ meta: {
33
+ apiBase: ctx.apiBase,
34
+ requestedAction: resolved.requestedAction,
35
+ resolvedAction: resolved.resolvedAction,
36
+ runMode: 'agent_wallet',
37
+ idempotencyKey: result.idempotencyKey || null,
38
+ executionWalletAddress: result.executionWalletAddress || null,
39
+ profile: ctx.profile || null,
40
+ capabilitiesVersion: capabilities?.contractVersion || null,
41
+ },
42
+ });
43
+ if (!result.success) process.exit(1);
44
+ }
45
+
46
+
47
+ async function commandWalletSign(ctx, flags, capabilities) {
48
+ const normalizedFlags = { ...flags, runMode: 'self' };
49
+ const resolved = await resolveActionSelection(ctx, normalizedFlags);
50
+ prepareActionParamsForFlags(normalizedFlags, resolved.resolvedAction);
51
+ enforceOffchainVenueLaneCompatibility(resolved.resolvedAction, 'self');
52
+ enforceChainCompatibility(capabilities, normalizedFlags, 'self');
53
+ const selfResult = await executeSelfRun(ctx, normalizedFlags, resolved, {
54
+ skipPreflight: Boolean(normalizedFlags.skipPreflight),
55
+ runMode: 'self',
56
+ });
57
+ printEnvelope({
58
+ success: selfResult.success,
59
+ code: selfResult.success ? 'WALLET_SIGN_READY' : selfResult.preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'WALLET_SIGN_FAILED',
60
+ message: selfResult.success ? 'Unsigned transaction prepared for BYOW signing' : selfResult.error || 'wallet sign failed',
61
+ data: selfResult,
62
+ meta: {
63
+ apiBase: ctx.apiBase,
64
+ requestedAction: resolved.requestedAction,
65
+ resolvedAction: resolved.resolvedAction,
66
+ runMode: 'self',
67
+ profile: ctx.profile || null,
68
+ capabilitiesVersion: capabilities?.contractVersion || null,
69
+ },
70
+ });
71
+ if (!selfResult.success) process.exit(1);
72
+ }
73
+
74
+
75
+ async function commandWalletSubmit(ctx, flags, capabilities) {
76
+ const signedTxHex = String(flags.signedTxHex || flags.signedTx || '').trim();
77
+ if (!signedTxHex) {
78
+ throw createCliError('MISSING_SIGNED_TX', 'wallet submit requires --signed-tx-hex');
79
+ }
80
+ const chainId = Number(flags.chainId || 8453);
81
+ const rpcUrl = String(
82
+ flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
83
+ ).trim();
84
+ const txHash = await submitSignedTx({ rpcUrl, chainId, signedTxHex });
85
+ printEnvelope({
86
+ success: true,
87
+ code: 'WALLET_SUBMITTED',
88
+ message: 'Signed transaction broadcasted',
89
+ data: {
90
+ txHash,
91
+ chainId,
92
+ signerMethod: 'byow',
93
+ executionMode: 'byow_submit',
94
+ },
95
+ meta: {
96
+ apiBase: ctx.apiBase,
97
+ rpcUrl,
98
+ profile: ctx.profile || null,
99
+ capabilitiesVersion: capabilities?.contractVersion || null,
100
+ },
101
+ });
102
+ }
103
+
104
+
105
+ async function commandWalletList(ctx, _flags) {
106
+ const config = loadProfileConfig();
107
+ const wallets = config.wallets && typeof config.wallets === 'object' ? config.wallets : {};
108
+ printEnvelope({
109
+ success: true,
110
+ code: 'WALLET_LIST_OK',
111
+ message: 'Linked wallets from local Thirdfy CLI profile',
112
+ data: {
113
+ primaryEvmWallet: wallets.primaryEvmWallet || null,
114
+ evm: Array.isArray(wallets.evm) ? wallets.evm : [],
115
+ solana: Array.isArray(wallets.solana) ? wallets.solana : [],
116
+ executionWallets: wallets.executionWallets || config.executionWallets || {},
117
+ hint: 'Run `thirdfy-agent login email <email>` to refresh linked wallets and agent execution wallets. Fund executionWallets for agent_wallet writes; linked owner wallets are auth identities.',
118
+ },
119
+ meta: { apiBase: ctx.apiBase },
120
+ });
121
+ }
122
+
123
+ return {
124
+ commandWalletExecute,
125
+ commandWalletSign,
126
+ commandWalletSubmit,
127
+ commandWalletList,
128
+ };
129
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+
3
+ export function parseArgs(argv) {
4
+ const flags = {};
5
+ const positionals = [];
6
+ for (let i = 0; i < argv.length; i += 1) {
7
+ const token = argv[i];
8
+ if (!token.startsWith('--')) {
9
+ positionals.push(token);
10
+ continue;
11
+ }
12
+ const key = toCamel(token.slice(2));
13
+ const next = argv[i + 1];
14
+ if (!next || next.startsWith('--')) {
15
+ flags[key] = true;
16
+ continue;
17
+ }
18
+ flags[key] = next;
19
+ i += 1;
20
+ }
21
+ return { flags, positionals };
22
+ }
23
+
24
+ export function isNonInteractive(flags = {}) {
25
+ return Boolean(flags.notInteractive || flags.ni || process.env.THIRDFY_NOT_INTERACTIVE === '1');
26
+ }
27
+
28
+ export function toCamel(input) {
29
+ return String(input || '')
30
+ .trim()
31
+ .replace(/-([a-z])/g, (_, c) => c.toUpperCase());
32
+ }
33
+
34
+ export function loadEnvFile(filePath) {
35
+ const raw = fs.readFileSync(filePath, 'utf8');
36
+ for (const line of raw.split('\n')) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith('#')) continue;
39
+ const eq = trimmed.indexOf('=');
40
+ if (eq === -1) continue;
41
+ const key = trimmed.slice(0, eq).trim();
42
+ let value = trimmed.slice(eq + 1).trim();
43
+ if (
44
+ (value.startsWith('"') && value.endsWith('"')) ||
45
+ (value.startsWith("'") && value.endsWith("'"))
46
+ ) {
47
+ value = value.slice(1, -1);
48
+ }
49
+ if (!(key in process.env)) process.env[key] = value;
50
+ }
51
+ }
52
+
53
+ export function requireFlag(flags, key, message) {
54
+ if (flags[key] === undefined || flags[key] === null || String(flags[key]).trim() === '') {
55
+ throw new Error(message || `Missing required flag --${key}`);
56
+ }
57
+ return flags[key];
58
+ }
59
+
60
+ export function parseJsonFlag(flags, key) {
61
+ const raw = flags[key];
62
+ if (raw === undefined || raw === null || raw === true) return undefined;
63
+ try {
64
+ return JSON.parse(String(raw));
65
+ } catch {
66
+ throw new Error(`Invalid JSON for --${key}`);
67
+ }
68
+ }
@@ -0,0 +1,31 @@
1
+ export const DEFAULT_API_BASE = 'https://api.thirdfy.com';
2
+ export const DEFAULT_TIMEOUT_MS = 30000;
3
+ export const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
4
+ export const DEFAULT_BITFINEX_BASE_URL = 'https://api.bitfinex.com';
5
+ export const BITFINEX_PERMISSION_PROFILES = {
6
+ funding: {
7
+ id: 'funding',
8
+ credentialRef: 'bitfinex:funding-primary',
9
+ required: {
10
+ funding: { read: 1, write: 1 },
11
+ wallets: { read: 1, write: 0 },
12
+ },
13
+ },
14
+ spot: {
15
+ id: 'spot',
16
+ credentialRef: 'bitfinex:spot-primary',
17
+ required: {
18
+ orders: { read: 1, write: 1 },
19
+ wallets: { read: 1, write: 0 },
20
+ },
21
+ },
22
+ };
23
+ export const RUN_MODES = ['thirdfy', 'self', 'hybrid', 'agent_wallet'];
24
+ export const HYBRID_WALLET_MODES = ['self', 'agent_wallet'];
25
+ export const CUSTODY_MODES = ['managed', 'external'];
26
+ export const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
27
+ export const PROFILE_DEFAULTS = {
28
+ personal: 'agent_wallet',
29
+ builder: 'hybrid',
30
+ network: 'thirdfy',
31
+ };
@@ -0,0 +1,145 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { DEFAULT_CATALOG_CACHE_TTL_MS, PROFILE_DEFAULTS } from './constants.mjs';
5
+ import { createCliError } from './envelope.mjs';
6
+ import { apiGet } from './http.mjs';
7
+ import {
8
+ normalizeCustodyMode,
9
+ normalizeProfileName,
10
+ normalizeRunMode,
11
+ } from './runMode.mjs';
12
+
13
+ const catalogCache = new Map();
14
+
15
+ export function getProfileConfigPath() {
16
+ return path.join(os.homedir(), '.thirdfy', 'config.json');
17
+ }
18
+
19
+ export function loadProfileConfig() {
20
+ try {
21
+ const raw = fs.readFileSync(getProfileConfigPath(), 'utf8');
22
+ const parsed = JSON.parse(raw);
23
+ if (!parsed || typeof parsed !== 'object') return {};
24
+ return parsed;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ export function persistProfileConfig(config) {
31
+ const configPath = getProfileConfigPath();
32
+ const dirPath = path.dirname(configPath);
33
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
34
+ const payload = `${JSON.stringify(config, null, 2)}\n`;
35
+ fs.writeFileSync(configPath, payload, { encoding: 'utf8', mode: 0o600 });
36
+ try {
37
+ fs.chmodSync(configPath, 0o600);
38
+ } catch {
39
+ /* chmod unsupported */
40
+ }
41
+ try {
42
+ fs.chmodSync(dirPath, 0o700);
43
+ } catch {
44
+ /* ignore */
45
+ }
46
+ }
47
+
48
+ export function clonePlainConfigTree(input) {
49
+ const obj = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
50
+ try {
51
+ return structuredClone(obj);
52
+ } catch {
53
+ return JSON.parse(JSON.stringify(obj));
54
+ }
55
+ }
56
+
57
+ export function setNestedConfigValue(config, keyPath, value) {
58
+ const next = clonePlainConfigTree(config || {});
59
+ const parts = String(keyPath || '')
60
+ .split('.')
61
+ .map((v) => v.trim())
62
+ .filter(Boolean);
63
+ const blocked = new Set(['__proto__', 'prototype', 'constructor']);
64
+ if (parts.some((part) => blocked.has(part))) {
65
+ throw createCliError(
66
+ 'CONFIG_KEY_INVALID',
67
+ 'Invalid config key path: reserved segments (__proto__, prototype, constructor) are not allowed.'
68
+ );
69
+ }
70
+ if (!parts.length) return next;
71
+ let cursor = next;
72
+ for (let i = 0; i < parts.length - 1; i += 1) {
73
+ const key = parts[i];
74
+ const existing = cursor[key];
75
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
76
+ cursor[key] = {};
77
+ }
78
+ cursor = cursor[key];
79
+ }
80
+ const finalKey = parts[parts.length - 1];
81
+ if (value === undefined || String(value).trim() === '') {
82
+ delete cursor[finalKey];
83
+ } else {
84
+ cursor[finalKey] = String(value);
85
+ }
86
+ return next;
87
+ }
88
+
89
+ export function resolveProfileState(flags) {
90
+ const persisted = loadProfileConfig();
91
+ const profile = normalizeProfileName(flags.profile || process.env.THIRDFY_PROFILE || persisted.profile || 'personal');
92
+ const runMode = normalizeRunMode(
93
+ flags.runMode ||
94
+ process.env.THIRDFY_RUN_MODE ||
95
+ persisted.runMode ||
96
+ PROFILE_DEFAULTS[profile] ||
97
+ 'thirdfy'
98
+ );
99
+ const custodyMode = normalizeCustodyMode(
100
+ flags.custodyMode || process.env.THIRDFY_CUSTODY_MODE || persisted.custodyMode,
101
+ runMode
102
+ );
103
+ return {
104
+ profile,
105
+ runMode,
106
+ custodyMode,
107
+ configPath: getProfileConfigPath(),
108
+ };
109
+ }
110
+
111
+ export function getCachedActionsCatalogTtlMs() {
112
+ const fromEnv = Number(process.env.THIRDFY_ACTIONS_CATALOG_CACHE_TTL_MS || '');
113
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_CATALOG_CACHE_TTL_MS;
114
+ }
115
+
116
+ export function extractActions(response) {
117
+ if (Array.isArray(response)) return response;
118
+ if (Array.isArray(response.actions)) return response.actions;
119
+ if (Array.isArray(response.data?.actions)) return response.data.actions;
120
+ return [];
121
+ }
122
+
123
+ export async function getCachedActionsCatalog(ctx, flags) {
124
+ const policyAware = Boolean(flags.agentApiKey);
125
+ const cacheKey = policyAware
126
+ ? `policy:${ctx.apiBase}:${String(flags.agentApiKey)}`
127
+ : `public:${ctx.apiBase}`;
128
+ const ttlMs = getCachedActionsCatalogTtlMs();
129
+ const now = Date.now();
130
+ const hit = catalogCache.get(cacheKey);
131
+ if (hit && hit.expiresAt > now) {
132
+ return hit.actions;
133
+ }
134
+ let endpoint = '/api/v1/agent/actions/catalog';
135
+ if (policyAware) {
136
+ endpoint = `/api/v1/agent/actions?agentApiKey=${encodeURIComponent(String(flags.agentApiKey))}`;
137
+ }
138
+ const response = await apiGet(ctx, endpoint);
139
+ const actions = extractActions(response);
140
+ catalogCache.set(cacheKey, {
141
+ actions,
142
+ expiresAt: now + ttlMs,
143
+ });
144
+ return actions;
145
+ }
@@ -0,0 +1,20 @@
1
+ export function createCliError(code, message, payload = {}) {
2
+ const error = new Error(message);
3
+ error.payload = {
4
+ error: code,
5
+ blockedReason: code,
6
+ blockedStage: 'client_validation',
7
+ ...payload,
8
+ };
9
+ return error;
10
+ }
11
+
12
+ export function printEnvelope(payload, { jsonMode = false } = {}) {
13
+ if (jsonMode) {
14
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
15
+ return;
16
+ }
17
+ const status = payload.success ? 'OK' : 'ERROR';
18
+ process.stdout.write(`[${status}] ${payload.code}: ${payload.message}\n`);
19
+ process.stdout.write(`${JSON.stringify(payload.data || {}, null, 2)}\n`);
20
+ }