callx402 1.0.1 → 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/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
- callx402 is the ACTION, not the product name; every command dispatches into
30
- the existing v2.0.0 subsystems. Nothing here executes payments by itself.
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
- config list | get <key> | set <key> <value>
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 authorize costs above the approval threshold
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
- console.error(`callx402: ${message}\nValid commands: ${COMMANDS.join(', ')}\nRun 'callx402 --help' for usage.`);
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
- if (flags.help) { process.stdout.write(HELP); process.exit(EXIT.OK); }
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 = readEvidenceInput(args.evidence ?? args.target, require('fs'), require('path'));
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 || '0.1.0', v2Root: require('./subsystems').getV2Root(ctx.config || {}), subsystems },
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 || '0.1.0' };
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, {