callx402 1.0.0 → 1.0.2
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/.github/npm-publish.yml.staged +27 -0
- package/.github/workflows/npm-publish.yml +29 -0
- package/README.md +164 -11
- package/bin/callx402.js +296 -13
- package/core/index.js +35 -6
- package/core/rail.js +233 -0
- package/core/veyline.js +240 -0
- package/docs/README.md +11 -9
- package/docs/action-reference.md +2 -2
- package/docs/cli-help-verified-2026-10-06.txt +36 -0
- package/docs/problem-map.md +30 -15
- package/docs/troubleshooting/cx402-duppay-001-duplicate-payment-risk.md +135 -0
- package/docs/troubleshooting/cx402-paidnores-001-paid-no-result.md +3 -1
- package/docs/troubleshooting/cx402-safretry-001-retry-after-payment.md +13 -1
- package/docs/what-is-callx402.md +5 -4
- package/logo-400.png +0 -0
- package/mcp/README.md +5 -0
- package/mcp/index.js +1 -1
- package/mcp-publisher +0 -0
- package/package.json +9 -18
- package/server.json +23 -1
- package/test/cli-dispatch.test.js +38 -19
- package/test/helpers.js +20 -0
- package/test/integrity.test.js +2 -1
- package/test/intent-pipeline.test.js +2 -3
- package/test/remote.test.js +2 -3
- package/test/safety.test.js +4 -5
- package/test/sdk.test.js +3 -4
- package/test/server.test.js +4 -5
- package/test/subsystems.test.js +7 -8
- package/test/veyline-commands.test.js +193 -0
package/bin/callx402.js
CHANGED
|
@@ -17,25 +17,247 @@ const path = require('path');
|
|
|
17
17
|
|
|
18
18
|
const ROOT = path.resolve(__dirname, '..');
|
|
19
19
|
const core = require(path.join(ROOT, 'core'));
|
|
20
|
+
const rail = require(path.join(ROOT, 'core', 'rail'));
|
|
20
21
|
const { loadConfig, getConfig, setConfig, listConfig } = require(path.join(ROOT, 'core', 'config'));
|
|
21
22
|
const { EXIT } = require(path.join(ROOT, 'core', 'envelope'));
|
|
22
23
|
|
|
23
24
|
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
24
25
|
const VERSION = PKG.version || '1.0.0';
|
|
25
26
|
|
|
26
|
-
const COMMANDS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'config'];
|
|
27
|
+
const COMMANDS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'config', 'evidence', 'explain', 'recover'];
|
|
28
|
+
|
|
29
|
+
// Paid on-demand rail actions that have no CLI command (architecture: the CLI
|
|
30
|
+
// is the free local tier; these run on the paid rail via the quote-then-pay
|
|
31
|
+
// invocation in docs/problem-map.md). Named here so a mistyped/rail-action
|
|
32
|
+
// command gets a routing hint instead of a dead-end usage error.
|
|
33
|
+
const RAIL_ONLY_ACTIONS = {
|
|
34
|
+
safe_retry: 'safe_retry', 'safe-retry': 'safe_retry',
|
|
35
|
+
duplicate_payment_risk: 'duplicate_payment_risk', 'duplicate-payment-risk': 'duplicate_payment_risk',
|
|
36
|
+
failure_classification: 'failure_classification', 'failure-classification': 'failure_classification',
|
|
37
|
+
settlement_interpretation: 'settlement_interpretation', 'settlement-interpretation': 'settlement_interpretation',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Per-command help: `callx402 <command> --help` prints the command's own usage
|
|
41
|
+
// instead of the global help. Free local tier; paid rail counterparts (where
|
|
42
|
+
// they exist) are named with their docs pointer.
|
|
43
|
+
const COMMAND_HELP = {
|
|
44
|
+
diagnose: `callx402 diagnose — run the x402 doctor over supplied evidence.
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
callx402 diagnose --target <url> [--evidence <json|@file>]
|
|
48
|
+
callx402 diagnose --evidence '{"txHash":"0x..."}'
|
|
49
|
+
|
|
50
|
+
Read-only: classifies the failure stage from evidence you supply; performs no
|
|
51
|
+
live probing and changes no state. Requires PAYLOAD_MCP_DOCTOR=1 (exit 3 names
|
|
52
|
+
the flag when it is off).
|
|
53
|
+
|
|
54
|
+
Paid one-off counterpart: the 'diagnose' rail action (quote first, then pay;
|
|
55
|
+
see docs/problem-map.md). The MCP tool x402_diagnose covers the same free
|
|
56
|
+
diagnostic.
|
|
57
|
+
|
|
58
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
59
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
60
|
+
`,
|
|
61
|
+
rescue: `callx402 rescue — incident triage for stuck or failed x402 transactions.
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
callx402 rescue --incident <id> [--network <net>] [--auth]
|
|
65
|
+
|
|
66
|
+
Read-only triage: detects the incident, quotes the rescue, presents the
|
|
67
|
+
free-vs-paid offer. Paid execution is NEVER performed by this command.
|
|
68
|
+
Without --auth (or CALLX402_AUTH_TOKEN / config rescueAuth) it refuses with
|
|
69
|
+
exit 3. Requires PAYLOAD_RESCUE=1.
|
|
70
|
+
|
|
71
|
+
Paid one-off counterpart: the 'rescue' rail action (see docs/problem-map.md).
|
|
72
|
+
|
|
73
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable or auth_required ·
|
|
74
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
75
|
+
`,
|
|
76
|
+
route: `callx402 route — select a tool/route for a goal under a routing policy.
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
callx402 route --goal <text> [--speed fast|balanced|cheap]
|
|
80
|
+
[--networks a,b] [--assets a,b]
|
|
81
|
+
|
|
82
|
+
Selection only: nothing executed, no money moved. Requires PAYLOAD_MCP_ROUTES=1.
|
|
83
|
+
|
|
84
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing --goal) · 3 disabled/unreachable ·
|
|
85
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
86
|
+
`,
|
|
87
|
+
resolve: `callx402 resolve — resolve the settlement state of a payment from evidence.
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
callx402 resolve --evidence '{"txHash":"0x...","network":"base"}'
|
|
91
|
+
callx402 resolve --evidence @evidence.json
|
|
92
|
+
|
|
93
|
+
Read-only analysis: maps evidence to DEFINITELY_PAID, DEFINITELY_NOT_PAID,
|
|
94
|
+
AUTHORIZED_NOT_SETTLED, SETTLEMENT_PENDING, CONFLICT, or UNKNOWN. Never
|
|
95
|
+
queries the chain, never signs, never retries, never repays. Requires
|
|
96
|
+
PAYLOAD_SETTLEMENT_RESOLVER=1.
|
|
97
|
+
|
|
98
|
+
Thin evidence -> settlement UNKNOWN, exit 5. That is the stop sign: do not
|
|
99
|
+
retry, do not repay; gather fresh evidence or escalate.
|
|
100
|
+
|
|
101
|
+
Paid one-off counterpart: the 'resolve' rail action (quote first, then pay;
|
|
102
|
+
see docs/problem-map.md). The MCP tool x402_resolve covers the same free
|
|
103
|
+
diagnostic.
|
|
104
|
+
|
|
105
|
+
Exit codes: 0 ok · 1 failure · 2 usage (malformed evidence JSON) ·
|
|
106
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
107
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
108
|
+
`,
|
|
109
|
+
doctor: `callx402 doctor — alias of diagnose.
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
callx402 doctor --target <url> [--evidence <json|@file>]
|
|
113
|
+
|
|
114
|
+
Identical to diagnose (the envelope's action field reads "diagnose").
|
|
115
|
+
Requires PAYLOAD_MCP_DOCTOR=1.
|
|
116
|
+
|
|
117
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
118
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
119
|
+
`,
|
|
120
|
+
execute: `callx402 execute — run the intent pipeline under an explicit budget.
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
callx402 execute --intent <text> [--max-budget N] [--dry-run]
|
|
124
|
+
[--idempotency-key K] [--approve] [--force-retry]
|
|
125
|
+
|
|
126
|
+
Budget-gated and idempotent. --dry-run plans only. Over-budget plans refuse
|
|
127
|
+
with exit 4 and zero side effects. --force-retry after a stored run whose
|
|
128
|
+
settlement is UNKNOWN is refused (exit 5): it could double-spend.
|
|
129
|
+
|
|
130
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing --intent) ·
|
|
131
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
132
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
133
|
+
`,
|
|
134
|
+
monitor: `callx402 monitor — Sentinel snapshot of x402 incident state.
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
callx402 monitor --once single snapshot (default)
|
|
138
|
+
callx402 monitor --watch [--interval <ms>]
|
|
139
|
+
|
|
140
|
+
Read-only. The default sentinel is inert ("no live tracking") unless
|
|
141
|
+
PAYLOAD_SENTINEL=1 enables the live tracker.
|
|
142
|
+
|
|
143
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
144
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
145
|
+
`,
|
|
146
|
+
status: `callx402 status — per-subsystem reachability report.
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
callx402 status [--json]
|
|
150
|
+
|
|
151
|
+
Requires nothing; works even when every subsystem is disabled. Reports
|
|
152
|
+
reachable/enabled/version/flag for all 15 subsystem handles. Always exits 0
|
|
153
|
+
on success.
|
|
154
|
+
`,
|
|
155
|
+
preflight: `callx402 preflight — preflight checks over a supplied context.
|
|
156
|
+
|
|
157
|
+
Usage:
|
|
158
|
+
callx402 preflight [--json]
|
|
159
|
+
|
|
160
|
+
Read-only: validates setup before a paid run; provisions or repairs nothing.
|
|
161
|
+
Requires PAYLOAD_MCP_PREFLIGHT=1.
|
|
162
|
+
|
|
163
|
+
Paid one-off counterpart: the 'preflight' rail action (see docs/problem-map.md).
|
|
164
|
+
|
|
165
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
166
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
167
|
+
`,
|
|
168
|
+
inspect: `callx402 inspect — capability-graph lookup or stats.
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
callx402 inspect [--query <text>]
|
|
172
|
+
|
|
173
|
+
Lookups only. Requires PAYLOAD_MCP_FABRIC=1; without registered tools it
|
|
174
|
+
returns zero candidates honestly.
|
|
175
|
+
|
|
176
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
177
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
178
|
+
`,
|
|
179
|
+
evidence: `callx402 evidence — show recorded evidence for an operation.
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
callx402 evidence <operationId> [--dir <path>]
|
|
183
|
+
|
|
184
|
+
Reads the Veyline operation ledger: events, latest per-plane states (payment,
|
|
185
|
+
execution, delivery), protocols. Unknown operations are honest ("no events
|
|
186
|
+
recorded"), still exit 0. Requires PAYLOAD_VEYLINE_LEDGER=1. First step in any
|
|
187
|
+
incident: returns what is recorded, nothing invented.
|
|
188
|
+
|
|
189
|
+
Paid one-off counterpart: the 'evidence' rail action (quote first, then pay;
|
|
190
|
+
see docs/problem-map.md). The MCP tool x402_evidence covers the same free
|
|
191
|
+
diagnostic.
|
|
192
|
+
|
|
193
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing operationId) ·
|
|
194
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
195
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
196
|
+
`,
|
|
197
|
+
explain: `callx402 explain — explain an operation's state in plain language.
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
callx402 explain <operationId> [--dir <path>]
|
|
201
|
+
|
|
202
|
+
Assessments from recorded ledger evidence: NO_BASIS, INCOMPLETE (do not retry,
|
|
203
|
+
do not repay), KNOWN_SAFE, RECOVERY_CANDIDATE (run 'recover'), PARTIAL.
|
|
204
|
+
Requires PAYLOAD_VEYLINE_LEDGER=1.
|
|
205
|
+
|
|
206
|
+
Paid one-off counterpart: the 'explain' rail action (see docs/problem-map.md).
|
|
207
|
+
The MCP tool x402_explain covers the same free diagnostic.
|
|
208
|
+
|
|
209
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing operationId) ·
|
|
210
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
211
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
212
|
+
`,
|
|
213
|
+
recover: `callx402 recover — report the safe recovery decision (read-only).
|
|
214
|
+
|
|
215
|
+
Usage:
|
|
216
|
+
callx402 recover <operationId> [--identity <id> | --evidence <json>]
|
|
217
|
+
|
|
218
|
+
Evaluates recorded history against the operation identity and returns the
|
|
219
|
+
recovery decision: RECOVERABLE, SAFE_RETRY, EXECUTED_BUT_UNRECOVERABLE, or
|
|
220
|
+
SETTLEMENT_UNKNOWN, mapped to KNOWN_SAFE or HUMAN_REVIEW. Never charges,
|
|
221
|
+
never executes, never repays. Requires PAYLOAD_VEYLINE_RECOVERY=1.
|
|
222
|
+
|
|
223
|
+
This is the read-only answer to "is a retry safe?" — a SAFE_RETRY verdict
|
|
224
|
+
here is advisory; performing the safe action stays with your own adapters.
|
|
225
|
+
For the paid one-off verdict on a specific retry plan, use the 'safe_retry'
|
|
226
|
+
rail action (quote first, then pay; see docs/problem-map.md). The MCP tool
|
|
227
|
+
x402_recover covers the same free diagnostic.
|
|
228
|
+
|
|
229
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing identity/evidence) ·
|
|
230
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
231
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
232
|
+
`,
|
|
233
|
+
config: `callx402 config — manage local configuration.
|
|
234
|
+
|
|
235
|
+
Usage:
|
|
236
|
+
callx402 config list
|
|
237
|
+
callx402 config get <key>
|
|
238
|
+
callx402 config set <key> <value>
|
|
239
|
+
|
|
240
|
+
Valid keys: mode, remoteUrl, authToken, v2Root, defaultNetwork, defaultAsset,
|
|
241
|
+
defaultBudgetUsd, approvalThresholdUsd, rescueAuth. Values persist to
|
|
242
|
+
~/.config/callx402/config.json (CALLX402_CONFIG overrides the path);
|
|
243
|
+
CALLX402_* environment variables override file values at runtime.
|
|
244
|
+
|
|
245
|
+
Exit codes: 0 ok · 2 usage (unknown key or subcommand).
|
|
246
|
+
`,
|
|
247
|
+
};
|
|
27
248
|
|
|
28
249
|
const HELP = `callx402 ${VERSION} — universal action layer for x402 infrastructure.
|
|
29
|
-
|
|
30
|
-
|
|
250
|
+
Install it and it works: subsystem commands route to the hosted Payload Rail
|
|
251
|
+
by default (free quote first, then pay-per-action). Nothing here executes
|
|
252
|
+
payments by itself.
|
|
31
253
|
|
|
32
254
|
Usage:
|
|
33
255
|
callx402 <command> [args] [options]
|
|
34
256
|
callx402 "natural language intent" intent mode (plans, never executes blindly)
|
|
35
257
|
callx402 --help | --version
|
|
36
258
|
|
|
37
|
-
Commands:
|
|
38
|
-
diagnose run the x402 doctor over supplied evidence [--target
|
|
259
|
+
Commands (hosted by default — zero setup):
|
|
260
|
+
diagnose run the x402 doctor over supplied evidence [--target <url|json>] [--evidence <json|@file>]
|
|
39
261
|
rescue incident triage (auth-gated; read-only without auth) --incident <id>
|
|
40
262
|
route select a tool/route for a goal --goal <text>
|
|
41
263
|
resolve resolve settlement state from evidence --evidence <json|@file>
|
|
@@ -45,12 +267,15 @@ Commands:
|
|
|
45
267
|
status per-subsystem reachable/enabled/version report
|
|
46
268
|
preflight run MCP preflight checks
|
|
47
269
|
inspect capability-graph lookup/stats [--query <text>]
|
|
48
|
-
|
|
270
|
+
evidence show recorded evidence for an operation <operationId> [--dir <path>]
|
|
271
|
+
explain explain an operation's state in plain language <operationId> [--dir <path>]
|
|
272
|
+
recover report the safe recovery decision (read-only) <operationId> [--identity <id> | --evidence <json>]
|
|
273
|
+
config list | get <key> | set <key> <v>
|
|
49
274
|
|
|
50
275
|
Options:
|
|
51
276
|
--json print the full result envelope as JSON
|
|
52
277
|
--timeout <ms> fail the action if it exceeds <ms>
|
|
53
|
-
--approve
|
|
278
|
+
--approve approve the quoted price non-interactively
|
|
54
279
|
--force-retry explicit retry flag (REFUSED when prior settlement is UNKNOWN)
|
|
55
280
|
--dry-run plan only; no execution side effects
|
|
56
281
|
--speed fast|balanced|cheap routing policy hint
|
|
@@ -61,6 +286,15 @@ Options:
|
|
|
61
286
|
|
|
62
287
|
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
63
288
|
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
289
|
+
|
|
290
|
+
First run (no setup): callx402 diagnose --evidence '{"txHash":"0x..."}'
|
|
291
|
+
free quote first, then pay-per-action on the hosted rail.
|
|
292
|
+
callx402 status honest subsystem reachability report
|
|
293
|
+
callx402 'plain words' plans only; nothing executes, no money moves
|
|
294
|
+
Execution modes: hosted rail by default. Set CALLX402_LOCAL=1 to prefer the
|
|
295
|
+
local v2.0.0 runtime tree instead (advanced self-hosted mode): set
|
|
296
|
+
CALLX402_V2_ROOT to its path and enable each subsystem's flag
|
|
297
|
+
(run 'callx402 status' to see flag names).
|
|
64
298
|
`;
|
|
65
299
|
|
|
66
300
|
function parseArgv(argv) {
|
|
@@ -74,6 +308,7 @@ function parseArgv(argv) {
|
|
|
74
308
|
'target', 'speed', 'interval', 'network', 'networks', 'asset', 'assets',
|
|
75
309
|
'providers', 'deadline', 'risk',
|
|
76
310
|
'dryRun', 'approve', 'forceRetry', 'retry', 'auth', 'watch', 'once',
|
|
311
|
+
'dir', 'operationId', 'identity',
|
|
77
312
|
]);
|
|
78
313
|
let i = 0;
|
|
79
314
|
while (i < argv.length) {
|
|
@@ -89,7 +324,7 @@ function parseArgv(argv) {
|
|
|
89
324
|
i += 1;
|
|
90
325
|
continue;
|
|
91
326
|
}
|
|
92
|
-
const NEEDS_VALUE = new Set(['timeout', 'maxBudget', 'approvalThreshold', 'idempotencyKey', 'evidence', 'incident', 'goal', 'intent', 'query', 'target', 'speed', 'interval', 'network', 'networks', 'asset', 'assets', 'providers', 'deadline', 'risk']);
|
|
327
|
+
const NEEDS_VALUE = new Set(['timeout', 'maxBudget', 'approvalThreshold', 'idempotencyKey', 'evidence', 'incident', 'goal', 'intent', 'query', 'target', 'speed', 'interval', 'network', 'networks', 'asset', 'assets', 'providers', 'deadline', 'risk', 'dir', 'operationId', 'identity']);
|
|
93
328
|
if (NEEDS_VALUE.has(name)) {
|
|
94
329
|
if (val !== null) out.flags[name] = val;
|
|
95
330
|
else if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) out.flags[name] = argv[++i];
|
|
@@ -111,8 +346,15 @@ function parseArgv(argv) {
|
|
|
111
346
|
return out;
|
|
112
347
|
}
|
|
113
348
|
|
|
114
|
-
function usageError(message) {
|
|
115
|
-
|
|
349
|
+
function usageError(message, first) {
|
|
350
|
+
let msg = message;
|
|
351
|
+
// A rail-only paid action typed as a command is a routing hint, not a
|
|
352
|
+
// dead end: point at the quote-then-pay invocation instead.
|
|
353
|
+
if (first && RAIL_ONLY_ACTIONS[first] && /^unknown command /.test(message)) {
|
|
354
|
+
msg = `${message} — '${RAIL_ONLY_ACTIONS[first]}' is a paid on-demand rail action, not a CLI command. ` +
|
|
355
|
+
`Get the free quote, then pay deliberately: see docs/problem-map.md`;
|
|
356
|
+
}
|
|
357
|
+
console.error(`callx402: ${msg}\nValid commands: ${COMMANDS.join(', ')}\nRun 'callx402 --help' for usage, or 'callx402 <command> --help' for one command.`);
|
|
116
358
|
process.exit(EXIT.USAGE);
|
|
117
359
|
}
|
|
118
360
|
|
|
@@ -132,17 +374,30 @@ function summarize(envelope) {
|
|
|
132
374
|
function statusSummary(envelope) {
|
|
133
375
|
const lines = [envelope.disposition || 'status'];
|
|
134
376
|
const subs = (envelope.data && envelope.data.subsystems) || {};
|
|
377
|
+
let reachable = 0;
|
|
135
378
|
for (const [name, s] of Object.entries(subs)) {
|
|
379
|
+
if (s.reachable) reachable += 1;
|
|
136
380
|
const mark = !s.reachable ? 'UNREACHABLE' : s.enabled ? 'enabled' : `disabled (${s.flag || 'no FLAG export'})`;
|
|
137
381
|
lines.push(` ${name.padEnd(12)} ${mark}${s.version ? ` v${s.version}` : ''}`);
|
|
138
382
|
}
|
|
383
|
+
if (reachable === 0 && Object.keys(subs).length > 0) {
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push(' No subsystems reachable: subsystem commands need the v2.0.0 tree.');
|
|
386
|
+
lines.push(' Set CALLX402_V2_ROOT to its path (see README "Full subsystem commands").');
|
|
387
|
+
}
|
|
139
388
|
return lines.join('\n');
|
|
140
389
|
}
|
|
141
390
|
|
|
142
391
|
async function main() {
|
|
143
392
|
const { flags, positionals, errors } = parseArgv(process.argv.slice(2));
|
|
144
393
|
|
|
145
|
-
|
|
394
|
+
// Per-command help: `callx402 <command> --help` prints that command's own
|
|
395
|
+
// usage. Bare `--help` (or --help with an unknown word) prints the global help.
|
|
396
|
+
if (flags.help) {
|
|
397
|
+
const cmd = positionals[0];
|
|
398
|
+
if (cmd && COMMAND_HELP[cmd]) { process.stdout.write(COMMAND_HELP[cmd]); process.exit(EXIT.OK); }
|
|
399
|
+
process.stdout.write(HELP); process.exit(EXIT.OK);
|
|
400
|
+
}
|
|
146
401
|
if (flags.version) { process.stdout.write(`callx402 ${VERSION}\n`); process.exit(EXIT.OK); }
|
|
147
402
|
if (errors.length > 0) usageError(errors[0]);
|
|
148
403
|
|
|
@@ -152,8 +407,8 @@ async function main() {
|
|
|
152
407
|
// like natural language (more than one word). A single unknown word is an
|
|
153
408
|
// unknown command (exit 2), per the CLI contract.
|
|
154
409
|
const isCommand = first && COMMANDS.includes(first);
|
|
155
|
-
if (!isCommand && first && first.startsWith('-')) usageError(`unknown input '${first}'
|
|
156
|
-
if (!isCommand && positionals.length === 1 && !/\s/.test(first)) usageError(`unknown command '${first}'
|
|
410
|
+
if (!isCommand && first && first.startsWith('-')) usageError(`unknown input '${first}'`, first);
|
|
411
|
+
if (!isCommand && positionals.length === 1 && !/\s/.test(first)) usageError(`unknown command '${first}'`, first);
|
|
157
412
|
const command = isCommand ? first : (first === undefined ? null : '__intent__');
|
|
158
413
|
|
|
159
414
|
// config handled directly (not a subsystem dispatch)
|
|
@@ -214,6 +469,9 @@ async function main() {
|
|
|
214
469
|
once: !!flags.once,
|
|
215
470
|
interval: toNum(flags.interval),
|
|
216
471
|
network: flags.network,
|
|
472
|
+
dir: flags.dir,
|
|
473
|
+
operationId: flags.operationId,
|
|
474
|
+
identity: flags.identity,
|
|
217
475
|
deadline: flags.deadline,
|
|
218
476
|
risk: flags.risk,
|
|
219
477
|
networks: toList(flags.networks) || (flags.network ? [flags.network] : undefined),
|
|
@@ -232,6 +490,7 @@ async function main() {
|
|
|
232
490
|
if (command === 'execute' && !args.intent && rest.length > 0) args.intent = rest.join(' ');
|
|
233
491
|
if (command === 'inspect' && !args.query && rest.length > 0) args.query = rest.join(' ');
|
|
234
492
|
if (command === 'rescue' && !args.incident && rest.length > 0) args.incident = rest[0];
|
|
493
|
+
if ((command === 'evidence' || command === 'explain' || command === 'recover') && !args.operationId && rest.length > 0) args.operationId = rest[0];
|
|
235
494
|
}
|
|
236
495
|
|
|
237
496
|
if (Number.isNaN(args.timeoutMs)) usageError('--timeout must be a number (ms)');
|
|
@@ -239,6 +498,30 @@ async function main() {
|
|
|
239
498
|
if (args.approvalThreshold !== undefined && Number.isNaN(args.approvalThreshold)) usageError('--approval-threshold must be a number');
|
|
240
499
|
|
|
241
500
|
try {
|
|
501
|
+
// Hosted rail routing (default): subsystem commands run against the live
|
|
502
|
+
// Payload Rail with zero local setup — invoke -> free quote ->
|
|
503
|
+
// payment/authorization -> result. Set CALLX402_LOCAL=1 to prefer the
|
|
504
|
+
// local v2.0.0 runtime tree instead (advanced self-hosted mode).
|
|
505
|
+
if (rail.isRailCommand(actionName) && !rail.preferLocal()) {
|
|
506
|
+
const railResult = await rail.invokeRail(actionName, args, { yes: args.approve, json: args.json });
|
|
507
|
+
if (args.json) {
|
|
508
|
+
console.log(JSON.stringify(railResult, null, 2));
|
|
509
|
+
} else if (railResult.ok) {
|
|
510
|
+
console.log(`\nResult (via ${railResult.via}, invocation ${railResult.invocation_id}):`);
|
|
511
|
+
const ex = railResult.execution;
|
|
512
|
+
if (ex && ex.executed) {
|
|
513
|
+
if (ex.disposition) console.log(ex.disposition);
|
|
514
|
+
if (ex.result !== undefined) console.log(JSON.stringify(ex.result, null, 2));
|
|
515
|
+
} else if (ex && ex.error) {
|
|
516
|
+
console.log(`Note: ${ex.error}`);
|
|
517
|
+
}
|
|
518
|
+
if (railResult.notice) console.log(railResult.notice);
|
|
519
|
+
} else {
|
|
520
|
+
console.error(`callx402: ${railResult.error}`);
|
|
521
|
+
if (railResult.quote) console.error(`Quote was: $${railResult.quote.price_usd} (${railResult.quote.quote_id})`);
|
|
522
|
+
}
|
|
523
|
+
process.exit(railResult.ok ? EXIT.OK : EXIT.FAIL);
|
|
524
|
+
}
|
|
242
525
|
const { result, exitCode } = await core.runAction(actionName, args, { version: VERSION });
|
|
243
526
|
if (args.json) {
|
|
244
527
|
console.log(JSON.stringify(result, null, 2));
|
package/core/index.js
CHANGED
|
@@ -14,10 +14,11 @@ const { loadConfig } = require('./config');
|
|
|
14
14
|
const { getSubsystem, getStatus: statusOf, SUBSYSTEMS } = require('./subsystems');
|
|
15
15
|
const { runIntent } = require('./pipeline');
|
|
16
16
|
const { dispatchRemote } = require('./remote');
|
|
17
|
+
const { actEvidence, actExplain, actRecover } = require('./veyline');
|
|
17
18
|
|
|
18
|
-
const ACTIONS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect'];
|
|
19
|
+
const ACTIONS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'evidence', 'explain', 'recover'];
|
|
19
20
|
|
|
20
|
-
/** Parse --evidence style input: JSON string or @file. */
|
|
21
|
+
/** Parse --evidence style input: JSON string or @file. Strict: bad JSON is a usage error. */
|
|
21
22
|
function readEvidenceInput(input, fs, path) {
|
|
22
23
|
if (input === undefined || input === null || input === '') return {};
|
|
23
24
|
if (typeof input !== 'string') return input;
|
|
@@ -30,6 +31,29 @@ function readEvidenceInput(input, fs, path) {
|
|
|
30
31
|
return JSON.parse(s);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Parse diagnose input. --evidence stays strict JSON (or @file). --target
|
|
36
|
+
* accepts a plain URL or identifier string: when it is not valid JSON it is
|
|
37
|
+
* wrapped as { target } evidence so the documented
|
|
38
|
+
* `diagnose --target <url>` example works as printed.
|
|
39
|
+
*/
|
|
40
|
+
function readDiagnoseInput(args, fs, path) {
|
|
41
|
+
const ev = args.evidence;
|
|
42
|
+
if (ev !== undefined && ev !== null && ev !== '') return readEvidenceInput(ev, fs, path);
|
|
43
|
+
const t = args.target;
|
|
44
|
+
if (t === undefined || t === null || t === '') return {};
|
|
45
|
+
const s = String(t).trim();
|
|
46
|
+
if (s.startsWith('@')) {
|
|
47
|
+
const raw = fs.readFileSync(path.resolve(s.slice(1)), 'utf8');
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(s);
|
|
52
|
+
} catch {
|
|
53
|
+
return { target: s };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
function disabledResult(action, subsystem, handle) {
|
|
34
58
|
return {
|
|
35
59
|
result: fail(action, {
|
|
@@ -48,7 +72,9 @@ function unreachableResult(action, subsystem, handle) {
|
|
|
48
72
|
result: fail(action, {
|
|
49
73
|
subsystem, subsystemStatus: 'unreachable',
|
|
50
74
|
code: 'subsystem_unreachable',
|
|
51
|
-
message: `${subsystem} subsystem could not be loaded (${handle.error || 'unknown error'}). Nothing was executed
|
|
75
|
+
message: `${subsystem} subsystem could not be loaded (${handle.error || 'unknown error'}). Nothing was executed. ` +
|
|
76
|
+
`To enable: set CALLX402_V2_ROOT to the v2.0.0 subsystem tree path (see README "Full subsystem commands"), ` +
|
|
77
|
+
`or run in remote mode (CALLX402_MODE=remote, CALLX402_REMOTE_URL=<your callx402 server>).`,
|
|
52
78
|
disposition: `${subsystem} unreachable: nothing executed.`,
|
|
53
79
|
data: { error: handle.error },
|
|
54
80
|
}),
|
|
@@ -67,7 +93,7 @@ async function actDiagnose(args, ctx) {
|
|
|
67
93
|
const g = requireSubsystem('diagnose', 'doctor', ctx);
|
|
68
94
|
if (!g.handle) return g;
|
|
69
95
|
try {
|
|
70
|
-
const evidence =
|
|
96
|
+
const evidence = readDiagnoseInput(args, require('fs'), require('path'));
|
|
71
97
|
const report = g.handle.module.runMcpDoctor(evidence);
|
|
72
98
|
const whatFailed = report.whatFailed || 'unknown';
|
|
73
99
|
return {
|
|
@@ -269,7 +295,7 @@ async function actStatus(args, ctx) {
|
|
|
269
295
|
result: ok('status', {
|
|
270
296
|
subsystem: null,
|
|
271
297
|
disposition: `callx402 status: ${reachable}/${all.length} subsystems reachable, ${enabledCount}/${all.length} enabled.`,
|
|
272
|
-
data: { version: ctx.version || '
|
|
298
|
+
data: { version: ctx.version || '1.0.0', v2Root: require('./subsystems').getV2Root(ctx.config || {}), subsystems },
|
|
273
299
|
}),
|
|
274
300
|
exitCode: EXIT.OK,
|
|
275
301
|
};
|
|
@@ -314,7 +340,7 @@ async function actInspect(args, ctx) {
|
|
|
314
340
|
*/
|
|
315
341
|
async function runAction(name, args = {}, opts = {}) {
|
|
316
342
|
const config = opts.config || loadConfig();
|
|
317
|
-
const ctx = { deps: opts.deps || {}, config, timeoutMs: opts.timeoutMs, version: opts.version || '
|
|
343
|
+
const ctx = { deps: opts.deps || {}, config, timeoutMs: opts.timeoutMs, version: opts.version || '1.0.0' };
|
|
318
344
|
|
|
319
345
|
// SPEC §5: remote mode dispatches over HTTP to a callx402 server. The deps
|
|
320
346
|
// seam is local-only: an explicit subsystem override means local dispatch.
|
|
@@ -334,6 +360,9 @@ async function runAction(name, args = {}, opts = {}) {
|
|
|
334
360
|
case 'status': return actStatus(args, ctx);
|
|
335
361
|
case 'preflight': return actPreflight(args, ctx);
|
|
336
362
|
case 'inspect': return actInspect(args, ctx);
|
|
363
|
+
case 'evidence': return actEvidence(args, ctx);
|
|
364
|
+
case 'explain': return actExplain(args, ctx);
|
|
365
|
+
case 'recover': return actRecover(args, ctx);
|
|
337
366
|
default:
|
|
338
367
|
return {
|
|
339
368
|
result: fail(name, {
|