@ola-893/agentpay-cli 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.mjs CHANGED
@@ -1,144 +1,275 @@
1
+ import { copyFlags, copyPositionals, runCopyCommand } from './copy-commands.mjs';
2
+ import { selectedAgent } from './agents.mjs';
1
3
  import { parseArgs } from 'node:util';
2
4
  import { fileURLToPath } from 'node:url';
3
- import { demoScenario } from '@ola-893/agentpay-core';
5
+ import { readFile } from 'node:fs/promises';
4
6
  import { projectPath, initProject, readProject, liveProfile, gatewayFor, setPayments } from './config.mjs';
7
+ import { showSettings, setRuntime, setService, setInference, setLimits, reconcileLimits, usdc } from './settings.mjs';
5
8
 
6
- const help = `AgentPay 0.1.0 — payment guardrails for agents on NEAR
9
+ const version = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
10
+ const help = `AgentPay ${version} — configure and run agents with native NEAR payment streams
7
11
 
8
- agentpay init --quick Offline config, no keys/network
9
- agentpay demo Always offline; simulated decisions
10
- agentpay doctor Read-only configuration checks
11
- agentpay install cursor|codex [--dry-run] Project MCP server and guidance
12
- agentpay mcp Stdio MCP using project mode
13
- agentpay ui [--no-open] [--recording testnet|sandbox]
12
+ Named agents and copy trading:
13
+ agentpay agent create NAME | list | use NAME | show [NAME]
14
+ agentpay agent setup --network testnet [--deploy]
15
+ agentpay agent run [--once] Run publication and/or follow a subscribed feed
16
+ agentpay task list | enable ACTIVITY | disable ACTIVITY
17
+ agentpay task enable spot-trading --intents-agent-id ID [--live] [--confidential]
18
+ agentpay task run spot-trading --from ASSET --to ASSET --amount-raw UNITS [--execute]
19
+ agentpay publish set --url ORIGIN --rate USDC_PER_SECOND --recipient SELLER.testnet [--duration SECONDS]
20
+ agentpay publish start | status | history | stop
21
+ agentpay earnings show | claim --from VAULT --recipient-key PATH
22
+ agentpay discover [--activity ACTIVITY] [--id PUBLISHER --url ORIGIN]
23
+ agentpay copy PUBLISHER [--url ORIGIN] Preview a signed offer for free
24
+ agentpay copy start PUBLISHER [--url ORIGIN]
25
+ agentpay copy mode observe|approve|auto
26
+ agentpay copy pending | approve ACTION | reject ACTION
27
+ agentpay copy pause | resume | stop | status | history
28
+ agentpay guard set payment --max-5xx 3 --max-health-failures 2 --reuse true
29
+ agentpay guard set execution --assets A,B --input-asset A --max-input-raw UNITS --copy-input-raw UNITS
30
+ agentpay guard set feed --stale-seconds 45 --feed-failures 3
31
+ agentpay guard show
32
+ agentpay fund USDC --owner-key PATH | withdraw USDC --owner-key PATH
33
+ agentpay balance | status
34
+ Spot trading uses NEAR Intents Agent Connect. Subscription escrow uses NEAR testnet.
14
35
 
15
- Explicit live setup (existing deployment, testnet only):
36
+ Setup (NEAR testnet, funded owner account required):
37
+ agentpay init --network testnet
38
+ agentpay init --network testnet --owner-id YOUR.testnet --owner-key /path/owner.json --deploy
16
39
  agentpay init --network testnet --deployment /path/deployment.json
17
- agentpay payments --enable Allow this profile to pay
18
- agentpay payments --disable Disable OPEN; closure stays available
19
- agentpay demo --pay [--fixture] Real testnet session, then close
20
- agentpay close --session SESSION_ID Approved closure after restart
21
- agentpay reconcile Read-only chain reconciliation
40
+ agentpay fund --amount 100000 --owner-key /path/owner.json Raw USDC units
22
41
 
23
- Common: --project PATH, --json, --help, --version
24
- Local signing trusts the operator host. TEE isolation and gas sponsorship are not implemented.
42
+ Configure payment and service guardrails:
43
+ agentpay config show
44
+ agentpay config limits --budget 0.10 --max-stream 0.01 --max-rate 0.0001 --max-duration 1000 --owner-key /path/owner.json
45
+ agentpay config set --max-5xx 3 --max-transport-errors 2 --max-health-failures 2 --idle-timeout 60 --max-requests 100 --reuse true --poll-seconds 10 --close-on-exit true
46
+ agentpay config reconcile Reconcile an uncertain owner policy update
47
+ agentpay service set --url https://api.example.com --request-path /research --health-path /health --quote-path /quote --tiers basic,premium
48
+ agentpay service serve --repository ./YOUR_REPOSITORY --port 5380
49
+ Serve actual repository analysis using this profile's public vault settings
50
+ Optional --deployment /path/public-service.json; no signing keys are read
51
+ agentpay payments --enable|--disable Disabling OPEN preserves closure
52
+
53
+ Connect inference, then run an agent:
54
+ agentpay inference set --base-url http://localhost:11434/v1 --model YOUR_MODEL
55
+ Optional --max-output-tokens N --timeout-seconds 120 (10-600)
56
+ agentpay inference models --provider near-ai List current models and tool/privacy metadata
57
+ agentpay inference set --provider near-ai --model MODEL_ID
58
+ Uses NEAR_AI_API_KEY; default output cap 2048 tokens
59
+ agentpay inference set --base-url https://api.openai.com/v1 --model YOUR_MODEL --api-key-env OPENAI_API_KEY
60
+ agentpay agent run --prompt "Use the paid API to research my question."
61
+ agentpay install cursor|codex [--dry-run] Use that harness's existing inference
62
+ agentpay install mcp Print stdio config for other MCP clients
63
+ agentpay mcp Stdio tools and background health monitor
64
+
65
+ Operate the same streams directly:
66
+ agentpay request --tier basic Actual guarded API request; opens/reuses as needed
67
+ agentpay stream check [--tier basic] Read-only admission/health decision
68
+ agentpay stream open --tier basic Check policy, then submit OPEN
69
+ agentpay stream status On-chain vault/session state
70
+ agentpay stream close [--session ID] Close and return unused escrow
71
+ agentpay monitor Monitor health/idle limits until stopped
72
+ agentpay reconcile Reconcile uncertain OPEN/CLOSE
73
+ agentpay doctor Read-only key, RPC, balance and policy checks
74
+ agentpay verify [--run /path/run.json] Verify saved testnet receipts (read-only)
75
+ agentpay ui [--run /path/run.json] [--no-open] Replay captured real NEAR runs
76
+
77
+ Common: --project PATH, --agent NAME, --json, --help, --version
78
+ Financial flags and positional fund/withdraw use decimal USDC. Legacy fund --amount uses raw units.
79
+ Only AgentPay-compatible paid APIs are supported. Inference must support OpenAI-compatible chat tool calls, or run through an MCP harness.
80
+ One service and one active stream per profile. Monitor/agent/MCP must be running for automatic health and idle closure; the contract always caps accrual at lease expiry.
81
+ Local signing trusts the host. No TEE, gas sponsor or production security guarantee.
25
82
  `;
83
+ const integer = value => { if (!/^[0-9]+$/.test(value || '')) throw new Error('Expected a positive integer.'); return Number(value); };
84
+ const boolean = value => { if (!['true', 'false'].includes(value)) throw new Error('Use true or false.'); return value === 'true'; };
26
85
 
27
86
  export async function run(argv) {
28
- const { values: v, positionals: p } = parseArgs({ args: argv, allowPositionals: true, strict: true, options: {
29
- quick: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' },
30
- project: { type: 'string', default: '.' }, network: { type: 'string' }, deployment: { type: 'string' }, 'enable-payments': { type: 'boolean' },
31
- enable: { type: 'boolean' }, disable: { type: 'boolean' }, 'dry-run': { type: 'boolean' }, 'no-open': { type: 'boolean' },
32
- recording: { type: 'string', default: 'testnet' }, port: { type: 'string', default: '0' }, pay: { type: 'boolean' }, fixture: { type: 'boolean' }, session: { type: 'string' },
87
+ const strings = ['agent', 'activity', 'id', 'activities', 'rate', 'intents-agent-id', 'intents-key-env', 'intents-grant-env', 'from', 'to', 'amount-raw', 'nova-group', 'nova-account', 'nova-network', 'nova-key-env', 'performance-file', 'recipient-key', 'assets', 'input-asset', 'max-input-raw', 'copy-input-raw', 'max-age', 'max-slippage-bps', 'max-notional', 'stale-seconds', 'feed-failures', 'network', 'deployment', 'session', 'owner-id', 'owner-key', 'recipient', 'amount', 'budget', 'max-stream', 'max-rate', 'max-duration', 'max-5xx', 'max-transport-errors', 'max-health-failures', 'idle-timeout', 'max-requests', 'reuse', 'poll-seconds', 'close-on-exit', 'url', 'health-path', 'quote-path', 'request-path', 'tiers', 'base-url', 'model', 'api-key-env', 'max-steps', 'prompt', 'tier', 'query', 'thinking', 'failure-cooldown', 'provider', 'reasoning-effort', 'max-output-tokens', 'repository', 'basic-rate', 'premium-rate', 'duration', 'run', 'timeout-seconds'];
88
+ const booleans = ['once', 'live', 'confidential', 'execute', 'json', 'help', 'version', 'enable-payments', 'enable', 'disable', 'dry-run', 'no-open', 'deploy'];
89
+ const { values: v, positionals: p, tokens } = parseArgs({ args: argv, allowPositionals: true, strict: true, tokens: true, options: {
90
+ ...Object.fromEntries(strings.map(k => [k, { type: 'string' }])), ...Object.fromEntries(booleans.map(k => [k, { type: 'boolean' }])),
91
+ project: { type: 'string', default: '.' }, recording: { type: 'string', default: 'testnet' }, port: { type: 'string', default: '0' },
92
+ 'vault-name': { type: 'string', default: 'agentpay' }, 'agent-name': { type: 'string', default: 'research-agent' }, rpc: { type: 'string', default: 'https://test.rpc.fastnear.com' }, service: { type: 'string', default: 'http://127.0.0.1:5321' },
33
93
  } });
34
94
  const emit = value => process.stdout.write(typeof value === 'string' ? value + '\n' : JSON.stringify(value, (_, item) => typeof item === 'bigint' ? item.toString() : item, 2) + '\n');
35
- if (v.version) return emit('0.1.0');
95
+ if (v.version) return emit(version);
36
96
  if (v.help || !p.length) return emit(help);
37
- if (p.length > (p[0] === 'install' ? 2 : 1)) throw new Error('Unexpected positional arguments. See --help.');
38
- const project = await projectPath(v.project);
97
+ const allowed = {
98
+ ...copyFlags,
99
+ init: 'network deployment enable-payments owner-id owner-key recipient vault-name agent-name rpc service deploy',
100
+ 'config:set': 'max-5xx max-transport-errors max-health-failures idle-timeout max-requests reuse poll-seconds close-on-exit failure-cooldown',
101
+ 'config:limits': 'budget max-stream max-rate max-duration recipient owner-key', 'config:show': '', 'config:reconcile': '', config: '',
102
+ 'service:set': 'url recipient health-path quote-path request-path tiers', 'service:show': '', service: '',
103
+ 'service:serve': 'repository port deployment basic-rate premium-rate duration',
104
+ 'inference:set': 'provider base-url model api-key-env max-steps thinking reasoning-effort max-output-tokens timeout-seconds', 'inference:models': 'provider', 'inference:show': '', inference: '', 'agent:run': 'prompt max-steps once rpc',
105
+ request: 'tier query', 'stream:check': 'tier', 'stream:open': 'tier', 'stream:status': '', 'stream:close': 'session',
106
+ monitor: '', reconcile: '', close: 'session', verify: 'rpc run', fund: 'amount owner-key', doctor: '', payments: 'enable disable',
107
+ install: 'dry-run', mcp: '', ui: 'no-open recording port run',
108
+ };
109
+ const grouped = ['config', 'service', 'inference', 'agent', 'stream', 'task', 'publish', 'earnings', 'guard', 'copy'];
110
+ let command = grouped.includes(p[0]) && p[1] ? `${p[0]}:${p[1]}` : p[0];
111
+ if (p[0] === 'copy' && /^ap_[a-f0-9]{64}$/.test(p[1] ?? '')) command = 'copy:preview';
112
+ const maxPositionals = copyPositionals.has(command) ? 3 : grouped.includes(p[0]) || ['install', 'fund', 'withdraw'].includes(p[0]) ? 2 : 1;
113
+ if (p.length > maxPositionals) throw new Error('Unexpected positional arguments. See --help.');
114
+ const common = ['project', 'agent', 'json', 'help', 'version'];
115
+ const provided = tokens.filter(token => token.kind === 'option' && !common.includes(token.name)).map(token => token.name);
116
+ if (Object.hasOwn(allowed, command)) {
117
+ const flags = new Set(allowed[command].split(' '));
118
+ for (const flag of provided) if (!flags.has(flag)) throw new Error(`--${flag} is not supported by ${p.join(' ')}.`);
119
+ }
120
+ const workspace = await projectPath(v.project);
121
+ const identityOnly = ['agent:create', 'agent:use', 'agent:list'].includes(command);
122
+ const agent = identityOnly ? null : await selectedAgent(workspace, command === 'agent:show' ? p[2] ?? v.agent : v.agent);
123
+ const project = agent?.path ?? workspace;
124
+ if (command === 'agent:run' && (!v.prompt && provided.includes('max-steps') || v.prompt && (v.once || provided.includes('rpc')))) throw new Error('Use --prompt with --max-steps, or the copy runner with --once and --rpc.');
125
+ if (Object.hasOwn(copyFlags, command) || (command === 'agent:run' && !v.prompt) || (command === 'reconcile' && agent)) {
126
+ const result = await runCopyCommand(command, p, v, workspace, agent, emit, provided);
127
+ if (command === 'reconcile' && !result.resolved) process.exitCode = 1;
128
+ if (result !== undefined) return emit(result);
129
+ return;
130
+ }
39
131
  switch (p[0]) {
40
132
  case 'init': {
41
- if (!v.quick && v.network !== 'testnet') throw new Error('Use --quick or --network testnet with --deployment.');
42
- return emit(await initProject(project, { quick: v.quick, deployment: v.deployment, enablePayments: v['enable-payments'] }));
133
+ if (v.network && v.network !== 'testnet') throw new Error('Only native NEAR testnet is supported.');
134
+ if (!v.deployment) {
135
+ if (v['enable-payments']) throw new Error('Setup leaves payments disabled; enable them separately after funding.');
136
+ const { setupTestnet } = await import('./setup.mjs');
137
+ const result = await setupTestnet(project, { ownerId: v['owner-id'], ownerKeyFile: v['owner-key'], recipientId: v.recipient, vaultLabel: v['vault-name'], agentLabel: v['agent-name'], rpcUrl: v.rpc, serviceUrl: v.service, deploy: v.deploy });
138
+ if (result.readyToDeploy === false) process.exitCode = 1;
139
+ return emit(result);
140
+ }
141
+ return emit(await initProject(project, { deployment: v.deployment, enablePayments: v['enable-payments'] }));
142
+ }
143
+ case 'config': {
144
+ if (p[1] === 'show' || !p[1]) return emit(await showSettings(project));
145
+ if (p[1] === 'reconcile') { const result = await reconcileLimits(project); if (!result.resolved) process.exitCode = 1; return emit(result); }
146
+ if (p[1] === 'limits') {
147
+ const patch = {};
148
+ for (const [flag, key] of [['budget','budget'], ['max-stream','max_deposit'], ['max-rate','max_rate']]) if (v[flag] !== undefined) patch[key] = usdc(v[flag]);
149
+ if (v['max-duration']) patch.max_duration_seconds = integer(v['max-duration']);
150
+ if (v.recipient) patch.recipients = v.recipient.split(',');
151
+ return emit(await setLimits(project, patch, v['owner-key']));
152
+ }
153
+ if (p[1] !== 'set') throw new Error('Use config show, set, limits or reconcile.');
154
+ const patch = {};
155
+ for (const [flag, key] of [['max-5xx','maxConsecutive5xx'], ['max-transport-errors','maxConsecutiveTransportErrors'], ['max-health-failures','maxConsecutiveHealthFailures'], ['idle-timeout','idleTimeoutSeconds'], ['max-requests','maxRequestsPerStream'], ['poll-seconds','healthPollSeconds'], ['failure-cooldown','failureCooldownSeconds']]) if (v[flag] !== undefined) patch[key] = integer(v[flag]);
156
+ for (const [flag, key] of [['reuse','reuseStream'], ['close-on-exit','closeOnExit']]) if (v[flag] !== undefined) patch[key] = boolean(v[flag]);
157
+ return emit(await setRuntime(project, patch));
43
158
  }
44
- case 'demo': {
45
- if (!v.pay) {
46
- if (v.fixture) throw new Error('--fixture requires explicit --pay.');
47
- const demo = await demoScenario();
48
- if (v.json) return emit(demo);
49
- return emit(`Offline simulation — no keys, network or blockchain transactions.\n${demo.events.map(e => `${e.decision}: ${e.reason || 'SESSION_CLOSED'}`).join('\n')}\n10 API requests reused one simulated session.\nSimulated six-decimal USDC: deposit 10000, earned 20, refunded 9980.`);
159
+ case 'service': {
160
+ if (p[1] === 'show' || !p[1]) return emit((await showSettings(project)).service);
161
+ if (p[1] === 'serve') {
162
+ if (!/^\d+$/.test(v.port) || Number(v.port) > 65535) throw new Error('Invalid port.');
163
+ const { readConfig } = await import('@ola-893/agentpay-core/internal-config');
164
+ const config = await readConfig(v.deployment || (await liveProfile(project)).configPath);
165
+ const { startRepositoryService } = await import('./repository-service.mjs');
166
+ const service = await startRepositoryService(config, { repository: v.repository, port: Number(v.port) || 5380,
167
+ rates: { basic: usdc(v['basic-rate'] || '0.000010'), premium: usdc(v['premium-rate'] || '0.000200') }, durationSeconds: v.duration ? integer(v.duration) : 1000 });
168
+ emit({ listening: service.origin, pid: process.pid, ...service.summary, faultInjection: 'Send SIGUSR1 to this process to toggle explicitly injected paid-request 503s; health stays healthy.' });
169
+ let injected = false;
170
+ const toggle = () => { injected = !injected; service.setRequestUnavailable(injected); emit({ injected503: injected, healthUnchanged: true }); };
171
+ process.on('SIGUSR1', toggle);
172
+ const stop = () => { process.removeListener('SIGUSR1', toggle); void service.close(); };
173
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
174
+ return;
50
175
  }
51
- return emit(await liveDemo(project, v.fixture));
176
+ if (p[1] !== 'set') throw new Error('Use service show, set or serve.');
177
+ const patch = {};
178
+ for (const [flag, key] of [['url','url'], ['recipient','recipient'], ['health-path','healthPath'], ['quote-path','quotePath'], ['request-path','requestPath']]) if (v[flag] !== undefined) patch[key] = v[flag];
179
+ if (v.tiers) patch.tiers = v.tiers.split(',');
180
+ return emit(await setService(project, patch));
181
+ }
182
+ case 'inference': {
183
+ if (p[1] === 'show' || !p[1]) return emit((await liveProfile(project)).inference || { configured: false });
184
+ if (p[1] === 'models') { const { listInferenceModels } = await import('./inference.mjs'); return emit(await listInferenceModels({ provider: v.provider })); }
185
+ if (p[1] !== 'set') throw new Error('Use inference show, set or models.');
186
+ return emit(await setInference(project, { provider: v.provider, baseUrl: v['base-url'], model: v.model, apiKeyEnv: v['api-key-env'], maxSteps: v['max-steps'] ? integer(v['max-steps']) : undefined,
187
+ ...(v.thinking !== undefined ? { thinking: boolean(v.thinking) } : {}),
188
+ ...(v['reasoning-effort'] !== undefined ? { reasoningEffort: v['reasoning-effort'] } : {}),
189
+ ...(v['timeout-seconds'] !== undefined ? { timeoutSeconds: integer(v['timeout-seconds']) } : {}),
190
+ ...(v['max-output-tokens'] !== undefined ? { maxOutputTokens: integer(v['max-output-tokens']) } : {}) }));
191
+ }
192
+ case 'agent': {
193
+ if (p[1] !== 'run') throw new Error('Use agent run --prompt "...".');
194
+ const profile = await liveProfile(project);
195
+ if (!profile.inference) throw new Error('Configure inference first with agentpay inference set, or install the MCP integration in your harness.');
196
+ const client = await gatewayFor(project);
197
+ const controller = new AbortController();
198
+ const stop = () => controller.abort(new Error('AGENT_INTERRUPTED'));
199
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
200
+ try { const { runAgent } = await import('./agent.mjs'); return await runAgent(client, { ...profile.inference, ...(v['max-steps'] ? { maxSteps: integer(v['max-steps']) } : {}) }, { prompt: v.prompt, signal: controller.signal, emit }); }
201
+ finally { process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); await client.dispose(); }
202
+ }
203
+ case 'request':
204
+ case 'stream':
205
+ case 'monitor':
206
+ case 'reconcile':
207
+ case 'close': {
208
+ if (p[0] === 'stream' && !['check', 'open', 'status', 'close'].includes(p[1])) throw new Error('Use stream check, open, status or close.');
209
+ const client = await gatewayFor(project);
210
+ try {
211
+ if (p[0] === 'reconcile') { const result = await client.reconcile(); if (!result.resolved) process.exitCode = 1; return emit(result); }
212
+ if (p[0] === 'request') { const { createToolHandlers } = await import('@ola-893/agentpay-core/tools'); return emit(await createToolHandlers(client).agentpay_fetch({ tier: v.tier || client.service.tiers[0], ...(v.query !== undefined ? { query: v.query } : {}) })); }
213
+ if (p[0] === 'monitor') {
214
+ const { startMonitor } = await import('@ola-893/agentpay-core/monitor');
215
+ let finish; const done = new Promise(resolve => { finish = resolve; });
216
+ const stop = () => finish();
217
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
218
+ const monitor = startMonitor(client, { onEvent: emit, onError: error => { emit({ error: error.message }); process.exitCode = 1; finish(); } });
219
+ emit({ monitoring: true, policy: client.runtimePolicy });
220
+ try { await done; } finally { process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); await monitor.stop(); if (client.runtimePolicy.closeOnExit) emit(await client.close()); }
221
+ return;
222
+ }
223
+ if (p[0] === 'close' || p[1] === 'close') return emit(await client.close({ sessionId: v.session }));
224
+ if (p[1] === 'status') return emit(await client.status());
225
+ const checked = await client.check({ tier: v.tier });
226
+ if (p[1] === 'check') return emit(checked);
227
+ if (checked.decision === 'CONTINUE') return emit({ ...checked, reused: true });
228
+ if (checked.decision !== 'OPEN') { process.exitCode = 1; return emit(checked); }
229
+ return emit({ ...await client.open({ approvalId: checked.approvalId }), monitoring: 'Run agentpay monitor for automatic health and idle closure between requests.' });
230
+ } finally { await client.dispose(); }
231
+ }
232
+ case 'verify': {
233
+ const { verifyPublicRun } = await import('@ola-893/agentpay-core/internal-evidence');
234
+ const { bundledWasm, publicRecording } = await import('./setup.mjs');
235
+ return emit(await verifyPublicRun({ recordingFile: v.run || publicRecording, wasmFile: bundledWasm, rpcUrl: v.rpc }));
236
+ }
237
+ case 'fund': {
238
+ if (p[1] && v.amount) throw new Error('Choose a decimal positional amount or legacy --amount.');
239
+ const { fundProject } = await import('./setup.mjs');
240
+ return emit(await fundProject(project, { ownerKeyFile: v['owner-key'], amount: p[1] ? usdc(p[1]) : v.amount, decimal: Boolean(p[1]) }));
52
241
  }
53
242
  case 'doctor': {
54
- const config = await readProject(project);
55
- const [major, minor, patch] = process.versions.node.split('.').map(Number);
56
- const nodeOK = major > 22 || (major === 22 && (minor > 22 || (minor === 22 && patch >= 2)));
57
- if (!nodeOK) throw new Error('Node 22.22.2 or newer is required.');
58
- if (config.mode === 'offline') return emit({ ok: true, mode: 'offline', node: process.versions.node, walletRequired: false, networkChecked: false, next: 'agentpay demo' });
243
+ if (agent) {
244
+ const { agentSetupState, readAgentState } = await import('./agents.mjs');
245
+ const [setup, state] = await Promise.all([agentSetupState(agent), readAgentState(agent)]);
246
+ if (!setup.configured) return emit({ ok: false, agentId: agent.id, setup, next: 'agentpay agent setup --network testnet' });
247
+ if (state.tradePending) process.exitCode = 1;
248
+ }
249
+ await readProject(project);
59
250
  const profile = await liveProfile(project);
60
251
  const { inspectNearProfile } = await import('@ola-893/agentpay-core/near');
61
- const { summary } = await inspectNearProfile(profile.configPath, profile.stateRoot);
62
- const ok = !summary.pendingIntent && !summary.executorBusyOrInterrupted;
252
+ const { summary, config } = await inspectNearProfile(profile.configPath, profile.stateRoot);
253
+ const ok = !process.exitCode && !summary.pendingIntent && !summary.executorBusyOrInterrupted && !summary.policyUpdatePending;
63
254
  if (!ok) process.exitCode = 1;
64
- return emit({ ok, node: process.versions.node, paymentsEnabled: profile.paymentsEnabled, ...summary });
65
- }
66
- case 'payments': {
67
- if (Boolean(v.enable) === Boolean(v.disable)) throw new Error('Choose exactly one of --enable or --disable.');
68
- return emit(await setPayments(project, v.enable));
255
+ return emit({ ok, node: process.versions.node, paymentsEnabled: profile.paymentsEnabled, inferenceConfigured: Boolean(profile.inference), runtimePolicy: config.runtimePolicy, ...summary });
69
256
  }
257
+ case 'payments': { if (Boolean(v.enable) === Boolean(v.disable)) throw new Error('Choose exactly one of --enable or --disable.'); return emit(await setPayments(project, v.enable)); }
70
258
  case 'install': {
259
+ if (p[1] === 'mcp') { const { mcpServer } = await import('./install.mjs'); return emit({ mcpServers: { agentpay: mcpServer(workspace, fileURLToPath(new URL('../bin/agentpay.mjs', import.meta.url)), { agent: agent?.name }) } }); }
71
260
  const { installAgent } = await import('./install.mjs');
72
- return emit(await installAgent(p[1], project, fileURLToPath(new URL('../bin/agentpay.mjs', import.meta.url)), { dryRun: v['dry-run'] }));
73
- }
74
- case 'mcp': {
75
- const { startMcp } = await import('./mcp.mjs');
76
- return startMcp(await gatewayFor(project));
261
+ return emit(await installAgent(p[1], workspace, fileURLToPath(new URL('../bin/agentpay.mjs', import.meta.url)), { dryRun: v['dry-run'], agent: agent?.name }));
77
262
  }
263
+ case 'mcp': { const { startMcp } = await import('./mcp.mjs'); return startMcp(await gatewayFor(project)); }
78
264
  case 'ui': {
79
265
  if (!/^\d+$/.test(v.port) || Number(v.port) > 65535) throw new Error('Invalid port.');
80
266
  const { startUi, openBrowser } = await import('./ui.mjs');
81
- const ui = await startUi({ port: Number(v.port), recording: v.recording });
82
- emit({ url: ui.url, mode: 'recorded', recording: v.recording, readOnly: true });
267
+ const ui = await startUi({ port: Number(v.port), recording: v.recording, runFile: v.run });
268
+ emit({ url: ui.url, mode: 'replay-of-real-near-run', recording: v.recording, readOnly: true });
83
269
  if (!v['no-open']) openBrowser(ui.url);
84
- process.once('SIGINT', () => void ui.close());
85
- process.once('SIGTERM', () => void ui.close());
270
+ process.once('SIGINT', () => void ui.close()); process.once('SIGTERM', () => void ui.close());
86
271
  return;
87
272
  }
88
- case 'reconcile':
89
- case 'close': {
90
- const client = await gatewayFor(project);
91
- try {
92
- if (p[0] === 'reconcile') {
93
- if (!client.reconcile) throw new Error('Reconciliation applies only to live profiles.');
94
- const result = await client.reconcile();
95
- if (!result.resolved) process.exitCode = 1;
96
- return emit(result);
97
- }
98
- return emit(await client.close({ sessionId: v.session }));
99
- } finally { await client.dispose(); }
100
- }
101
273
  default: throw new Error('Unknown command. Run agentpay --help.');
102
274
  }
103
275
  }
104
-
105
- async function liveDemo(project, fixture) {
106
- const config = await readProject(project);
107
- if (config.mode !== 'testnet') throw new Error('--pay requires an explicitly configured testnet profile.');
108
- const profile = await liveProfile(project);
109
- if (!profile.paymentsEnabled) throw new Error('PAYMENTS_DISABLED');
110
- const { readConfig } = await import('@ola-893/agentpay-core/internal-config');
111
- const runtime = await readConfig(profile.configPath);
112
- let provider;
113
- if (fixture) {
114
- if (!['127.0.0.1', 'localhost'].includes(new URL(runtime.serviceUrl).hostname)) throw new Error('Fixture provider must use loopback.');
115
- const { startProvider } = await import('@ola-893/agentpay-core/internal-provider');
116
- provider = await startProvider(runtime);
117
- }
118
- let client, active;
119
- try {
120
- client = await gatewayFor(project);
121
- const opened = await client.check({ tier: 'basic' });
122
- if (opened.decision !== 'OPEN') throw new Error(opened.reason || 'ADMISSION_DENIED');
123
- const openedSession = await client.open({ approvalId: opened.approvalId });
124
- active = openedSession;
125
- const { guardedFetch } = await import('@ola-893/agentpay-core');
126
- const request = guardedFetch(fetch, { client });
127
- for (let i = 0; i < 10; i++) {
128
- const r = await request(`${client.serviceUrl}/research`, { signal: AbortSignal.timeout(60000) });
129
- if (!r.ok) throw new Error('PROVIDER_REQUEST_FAILED');
130
- await r.arrayBuffer();
131
- }
132
- const continued = await client.check();
133
- const closed = await client.close({ sessionId: active.sessionId });
134
- active = null;
135
- const denied = await client.check({ tier: 'premium' });
136
- return { mode: 'testnet', fixture: Boolean(fixture), requestsReusingSession: 10, admission: opened, opened: openedSession, continued, closed, denied, providerPayout: 'claimable; no provider key loaded' };
137
- } finally {
138
- if (active) {
139
- try { await client.close({ sessionId: active.sessionId }); } catch { process.stderr.write('Closure unconfirmed. Run doctor/reconcile; owner recovery or expiry settlement may be needed.\n'); }
140
- }
141
- await client?.dispose();
142
- await provider?.close();
143
- }
144
- }
package/src/config.mjs CHANGED
@@ -2,7 +2,6 @@ import { homedir } from 'node:os';
2
2
  import { join, resolve, dirname } from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { mkdir, realpath, stat } from 'node:fs/promises';
5
- import { OfflineGateway } from '@ola-893/agentpay-core';
6
5
  import { readOptional, atomicWrite, safeTarget } from './files.mjs';
7
6
 
8
7
  export const configHome = () => resolve(process.env.AGENTPAY_HOME || join(homedir(), '.agentpay'));
@@ -11,23 +10,17 @@ const idFor = project => createHash('sha256').update(project).digest('hex');
11
10
 
12
11
  export async function readProject(project) {
13
12
  const text = await readOptional(await safeTarget(project, '.agentpay/config.json'));
14
- if (text === null) throw new Error('Project is not initialized. Run agentpay init --quick.');
13
+ if (text === null) throw new Error('Project is not initialized. Run agentpay init --network testnet.');
15
14
  const config = JSON.parse(text);
16
15
  if (config.version !== 1 || !['offline', 'testnet'].includes(config.mode)) throw new Error('Unsupported AgentPay configuration.');
17
16
  if (config.mode === 'testnet' && config.profileId !== idFor(project)) throw new Error('Live profile belongs to a different project; initialize this project explicitly.');
18
17
  return config;
19
18
  }
20
19
 
21
- export async function initProject(project, { quick = false, deployment, enablePayments = false } = {}) {
20
+ export async function initProject(project, { deployment, enablePayments = false } = {}) {
22
21
  const path = await safeTarget(project, '.agentpay/config.json');
23
22
  if (await readOptional(path)) throw new Error('Project already initialized. Use a separate project for another mode; agentpay payments changes live enablement.');
24
- if (quick) {
25
- if (deployment || enablePayments) throw new Error('--quick is offline and cannot enable payments or import credentials.');
26
- const config = { version: 1, mode: 'offline' };
27
- await atomicWrite(path, JSON.stringify(config, null, 2) + '\n');
28
- return { ...config, keysGenerated: false, next: 'agentpay demo' };
29
- }
30
- if (!deployment) throw new Error('Choose --quick or --network testnet --deployment /absolute/path/deployment.json.');
23
+ if (!deployment) throw new Error('Use --network testnet --deployment /absolute/path/deployment.json.');
31
24
  const { readConfig, readKey } = await import('@ola-893/agentpay-core/internal-config');
32
25
  const sourcePath = resolve(deployment);
33
26
  const source = await readConfig(sourcePath);
@@ -49,7 +42,7 @@ export async function initProject(project, { quick = false, deployment, enablePa
49
42
  if (await readOptional(file)) throw new Error('Credential already exists; refusing to replace it.');
50
43
  await atomicWrite(file, JSON.stringify({ account_id: credential.accountId, public_key: credential.publicKey, private_key: credential.privateKey }));
51
44
  }
52
- const runtime = Object.fromEntries(['network', 'rpcUrl', 'serviceUrl', 'vaultId', 'agentId', 'ownerId', 'recipientId', 'tokenId'].map(k => [k, source[k]]));
45
+ const runtime = Object.fromEntries(['network', 'rpcUrl', 'serviceUrl', 'vaultId', 'agentId', 'ownerId', 'recipientId', 'tokenId', 'runtimePolicy', 'service'].map(k => [k, source[k]]));
53
46
  const configPath = join(directory, 'deployment.json');
54
47
  await atomicWrite(configPath, JSON.stringify({ ...runtime, agentKeyFile, policyKeyFile }, null, 2));
55
48
  await atomicWrite(profilePath, JSON.stringify({ version: 1, paymentsEnabled: Boolean(enablePayments) }));
@@ -69,13 +62,14 @@ export async function liveProfile(project) {
69
62
 
70
63
  export async function setPayments(project, enabled) {
71
64
  const profile = await liveProfile(project);
72
- await atomicWrite(profile.file, JSON.stringify({ version: 1, paymentsEnabled: Boolean(enabled) }));
65
+ const { file, configPath, stateRoot, ...saved } = profile;
66
+ await atomicWrite(profile.file, JSON.stringify({ ...saved, paymentsEnabled: Boolean(enabled) }));
73
67
  return { mode: 'testnet', paymentsEnabled: Boolean(enabled) };
74
68
  }
75
69
 
76
70
  export async function gatewayFor(project) {
77
71
  const config = await readProject(project);
78
- if (config.mode === 'offline') return new OfflineGateway();
72
+ if (config.mode === 'offline') throw new Error('A native NEAR testnet profile is required. Initialize a new project with agentpay init --network testnet.');
79
73
  const { createNearGateway } = await import('@ola-893/agentpay-core/near');
80
- return createNearGateway(await liveProfile(project));
74
+ return createNearGateway({ ...await liveProfile(project), paymentsEnabled: async () => Boolean((await liveProfile(project)).paymentsEnabled) });
81
75
  }
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { usdc } from './settings.mjs';
3
+
4
+ export const activities = Object.freeze({
5
+ 'spot-trading': 'Mirror an eligible spot swap.',
6
+ 'staking-moves': 'Follow validator or staking pool allocations.',
7
+ 'liquidity-ranges': 'Mirror concentrated-liquidity ranges.',
8
+ 'prediction-markets': 'Mirror outcome positions.',
9
+ 'portfolio-rebalance': 'Follow portfolio target weights.',
10
+ 'yield-reallocation': 'Follow lending or yield-vault allocations.',
11
+ 'reward-reinvestment': 'Compound rewards following a published strategy.',
12
+ });
13
+
14
+ export const spot = 'spot-trading';
15
+ export const assetId = value => typeof value === 'string' && /^[a-z0-9][a-z0-9:._-]{0,155}$/i.test(value);
16
+ export const rawAmount = value => typeof value === 'string' && /^[1-9][0-9]{0,37}$/.test(value);
17
+
18
+ export function spotOrder({ from, to, amount }) {
19
+ if (!assetId(from) || !assetId(to) || from === to || !rawAmount(amount)) throw new Error('Specify distinct --from and --to asset IDs and --amount-raw in the input asset’s atomic units.');
20
+ return { origin_asset: from, destination_asset: to, amount };
21
+ }
22
+
23
+ export function executionPolicy(flags, previous = {}) {
24
+ const assets = flags.assets?.split(',') ?? previous.assets;
25
+ const maxInput = flags['max-input-raw'] ?? previous.maxInput;
26
+ const inputAsset = flags['input-asset'] ?? previous.inputAsset;
27
+ const fixedInput = flags['copy-input-raw'] ?? previous.fixedInput;
28
+ const maxAge = Number(flags['max-age'] ?? previous.maxAge ?? 60);
29
+ const slippageBps = Number(flags['max-slippage-bps'] ?? previous.slippageBps ?? 50);
30
+ if (!assets?.length || assets.length > 32 || !assets.every(assetId) || !assets.includes(inputAsset) || !rawAmount(maxInput) || !rawAmount(fixedInput) || BigInt(fixedInput) > BigInt(maxInput)) throw new Error('Set --assets, --input-asset, --max-input-raw and --copy-input-raw. Use one input asset so caps have a consistent unit.');
31
+ if (!Number.isInteger(maxAge) || maxAge < 1 || maxAge > 3600 || !Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > 500) throw new Error('Use --max-age 1–3600 seconds and --max-slippage-bps 0–500.');
32
+ return { assets: [...new Set(assets)], inputAsset, maxInput, fixedInput, maxAge, slippageBps,
33
+ ...(flags['max-notional'] ? { maxNotionalRawUSDC: usdc(flags['max-notional']) } : previous.maxNotionalRawUSDC ? { maxNotionalRawUSDC: previous.maxNotionalRawUSDC } : {}) };
34
+ }
35
+
36
+ export function proposedOrder(action, policy, now = Date.now()) {
37
+ if (!policy) return { reason: 'EXECUTION_POLICY_REQUIRED' };
38
+ if (action.at > now + 5000 || now - action.at > policy.maxAge * 1000 || now >= action.expiresAt) return { reason: 'ACTION_EXPIRED' };
39
+ if (action.from !== policy.inputAsset || !policy.assets.includes(action.to)) return { reason: 'ASSET_NOT_ALLOWED' };
40
+ return { order: spotOrder({ from: action.from, to: action.to, amount: policy.fixedInput }) };
41
+ }
42
+
43
+ export function checkQuote(quote, order, action, policy) {
44
+ if (!rawAmount(quote.amountOut) || !rawAmount(quote.amountIn) || quote.amountIn !== order.amount) return 'UNUSABLE_QUOTE';
45
+ const expected = BigInt(action.amountOut) * BigInt(order.amount);
46
+ const observed = BigInt(quote.amountOut) * BigInt(action.amountIn);
47
+ if (observed * 10000n < expected * BigInt(10000 - policy.slippageBps)) return 'PRICE_MOVED';
48
+ if (policy.maxNotionalRawUSDC) {
49
+ let usd;
50
+ try { usd = BigInt(usdc(String(quote.amountInUsd))); } catch { return 'USD_PRICE_UNAVAILABLE'; }
51
+ if (usd > BigInt(policy.maxNotionalRawUSDC)) return 'NOTIONAL_LIMIT';
52
+ }
53
+ return null;
54
+ }
55
+
56
+ export const operationKey = (agentId, actionId) => createHash('sha256').update(`${agentId}:${actionId}`).digest('hex');