@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/LICENSE +0 -2
- package/README.md +129 -20
- package/artifacts/agentpay_vault.wasm +0 -0
- package/artifacts/manifest.json +6 -0
- package/package.json +33 -8
- package/src/agent.mjs +61 -0
- package/src/agents.mjs +106 -0
- package/src/cli.mjs +239 -108
- package/src/config.mjs +8 -14
- package/src/copy-activity.mjs +56 -0
- package/src/copy-commands.mjs +220 -0
- package/src/copy-feed.mjs +278 -0
- package/src/copy-intents.mjs +83 -0
- package/src/copy-runtime.mjs +369 -0
- package/src/inference.mjs +69 -0
- package/src/install.mjs +9 -5
- package/src/mcp.mjs +15 -6
- package/src/repository-analysis.mjs +150 -0
- package/src/repository-service.mjs +17 -0
- package/src/settings.mjs +95 -0
- package/src/setup.mjs +73 -0
- package/src/ui.mjs +6 -1
- package/src/vault-money.mjs +69 -0
- package/ui/assets/index-Bl23n-ZK.js +331 -0
- package/ui/assets/{index-D7tPv-yG.css → index-BzAc-LMS.css} +1 -1
- package/ui/index.html +2 -2
- package/ui/near/testnet.json +741 -347
- package/ui/assets/index-B-PcdIha.js +0 -315
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 {
|
|
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
|
|
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
|
-
|
|
9
|
-
agentpay
|
|
10
|
-
agentpay
|
|
11
|
-
agentpay
|
|
12
|
-
agentpay
|
|
13
|
-
agentpay
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
95
|
+
if (v.version) return emit(version);
|
|
36
96
|
if (v.help || !p.length) return emit(help);
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
42
|
-
|
|
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 '
|
|
45
|
-
if (!
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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],
|
|
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: '
|
|
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 --
|
|
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, {
|
|
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 (
|
|
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
|
-
|
|
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')
|
|
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');
|