@noimosai/platform-cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +12 -56
  2. package/bin/noimos-platform.mjs +3 -4
  3. package/dist/cli/cli.js +477 -87
  4. package/dist/cli/command-help.js +349 -23
  5. package/dist/cli/download.js +25 -25
  6. package/dist/cli/input-error.js +48 -0
  7. package/dist/cli/local-agents.js +199 -0
  8. package/dist/cli/mcp-supervisor.js +136 -0
  9. package/dist/cli/noimos-cli.js +72 -0
  10. package/dist/cli/project-archive.js +27 -0
  11. package/dist/cli/project-files.js +25 -0
  12. package/dist/cli/project-sync.js +259 -0
  13. package/dist/cli/setup-lock.js +1 -0
  14. package/dist/cli/setup.js +347 -0
  15. package/dist/cli/skills.js +142 -0
  16. package/dist/cli/team-harness-targets.js +337 -0
  17. package/dist/cli/team-harness.js +1562 -0
  18. package/dist/cli/team-resources.js +222 -0
  19. package/dist/cli/team-skills.js +457 -0
  20. package/dist/cli/update-bootstrap.js +437 -0
  21. package/dist/cli/update-release.js +212 -0
  22. package/dist/cli/version.js +1 -0
  23. package/dist/cli/wallet.js +85 -0
  24. package/dist/client/agent-run.js +1425 -0
  25. package/dist/client/browser.js +905 -0
  26. package/dist/client/distribution.js +2 -0
  27. package/dist/client/evm-wallet.js +214 -0
  28. package/dist/client/execution-context.js +14 -0
  29. package/dist/client/failure-recovery.js +128 -0
  30. package/dist/client/index.js +4 -225
  31. package/dist/client/local-input.js +33 -12
  32. package/dist/client/local-profile-lock.js +52 -0
  33. package/dist/client/local-profile.js +181 -0
  34. package/dist/client/local-projects.js +131 -0
  35. package/dist/client/local-setup.js +93 -0
  36. package/dist/client/realtime-voice.js +184 -0
  37. package/dist/client/tool-name-hints.js +48 -0
  38. package/dist/client/wallet-access.js +214 -0
  39. package/dist/mcp/agent-server.js +345 -0
  40. package/dist/mcp/context.js +86 -0
  41. package/dist/mcp/host-schema.js +76 -0
  42. package/dist/mcp/icon.js +1 -0
  43. package/dist/mcp/index.js +3 -0
  44. package/dist/mcp/instructions.js +26 -0
  45. package/dist/mcp/local-projects.js +42 -0
  46. package/dist/mcp/local-upload.js +39 -0
  47. package/dist/mcp/noimos-runtime.js +18 -0
  48. package/dist/mcp/noimos-server.js +38 -0
  49. package/dist/mcp/operations.js +40 -0
  50. package/dist/mcp/parallel-result.js +17 -0
  51. package/dist/mcp/project-sync.js +105 -0
  52. package/dist/mcp/results-reply.js +15 -0
  53. package/dist/mcp/runtime.js +81 -0
  54. package/dist/mcp/server.js +410 -0
  55. package/dist/mcp/team-skill.js +63 -0
  56. package/dist/mcp/update-notice.js +75 -0
  57. package/dist/mcp/version.js +1 -0
  58. package/dist/schema/accountRequestMethods.js +1 -0
  59. package/dist/schema/boundedBody.js +30 -0
  60. package/dist/schema/cronValidator.js +150 -0
  61. package/dist/schema/platformApiKey.js +14 -0
  62. package/dist/schema/platformAppAuth.js +11 -0
  63. package/dist/schema/platformBillingConstants.js +4 -0
  64. package/dist/schema/platformCapabilityFamilies.js +293 -0
  65. package/dist/schema/platformConnectionCatalog.js +39 -0
  66. package/dist/schema/platformConnectionPolicy.js +33 -0
  67. package/dist/schema/platformCustomTools.js +148 -0
  68. package/dist/schema/platformEndUserApi.js +62 -0
  69. package/dist/schema/platformFunding.js +10 -0
  70. package/dist/schema/platformHttpConstraints.js +12 -0
  71. package/dist/schema/platformLocalSetup.js +25 -0
  72. package/dist/schema/platformMcpTools.js +7 -0
  73. package/dist/schema/platformMetaTools.js +63 -0
  74. package/dist/schema/platformMonitorSchedule.js +123 -0
  75. package/dist/schema/platformMonitors.js +159 -0
  76. package/dist/schema/platformOAuth.js +13 -0
  77. package/dist/schema/platformProjectPermissions.js +33 -0
  78. package/dist/schema/platformPublicExecution.js +202 -21
  79. package/dist/schema/platformRoutes.js +122 -0
  80. package/dist/schema/platformSkills.js +11 -0
  81. package/dist/schema/platformTeamSecretName.js +3 -0
  82. package/dist/schema/platformTeamSkillSlug.js +1 -0
  83. package/dist/schema/platformTeamSkills.js +302 -0
  84. package/dist/schema/platformToolAccess.js +62 -0
  85. package/dist/schema/platformTriggers.js +153 -0
  86. package/dist/schema/platformWorkflows.js +100 -0
  87. package/dist/schema/scheduleTimeZone.js +15 -0
  88. package/dist/schema/toolDiscovery.js +71 -0
  89. package/dist/schema/toolInputRepair.js +122 -0
  90. package/dist/schema/toolResultContext.js +51 -0
  91. package/dist/schema/untrustedContent.js +35 -0
  92. package/npm-shrinkwrap.json +73 -0
  93. package/package.json +6 -3
package/dist/cli/cli.js CHANGED
@@ -1,177 +1,567 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { createReadStream } from "node:fs";
3
- import { configFromEnvironment, PlatformClient, platformToolInputSchema, platformCreateExecutionSchema, platformArtifactIdSchema, platformExecutionListQuerySchema, platformToolDiscoverySchema } from "../client/index.js";
3
+ import { dirname } from "node:path";
4
+ import { configFromEnvironment, PlatformClient, PLATFORM_DISTRIBUTIONS, walletAutoTopUpFromEnvironment, platformQuoteRequestSchema, platformCreateExecutionSchema, platformArtifactIdSchema, platformExecutionIdSchema, platformExecutionListQuerySchema, platformConnectionCatalogQuerySchema, toolDiscoveryInputSchema, toolResultSelectionSchema, TOOL_RESULT_PAGE_MAX_ITEMS, executionReceiptContext } from "../client/index.js";
4
5
  import { downloadArtifact } from "./download.js";
5
6
  import { commandHelp } from "./command-help.js";
7
+ import { CliInputError, cliInputReason, isCliInputFailure, jsonOption } from "./input-error.js";
6
8
  import { uploadLocalInput } from "../client/local-input.js";
7
- export const help = `NoimosAI Developer Platform CLI
9
+ import { localProfilePath, runtimeConfig } from "../client/local-profile.js";
10
+ import { localProjectConfig } from "../client/local-projects.js";
11
+ import { runLocalCommand } from "./setup.js";
12
+ import { isTeamResource, teamResourceCommand } from "./team-resources.js";
13
+ import { runWalletCommand } from "./wallet.js";
14
+ import { PLATFORM_CLI_VERSION } from "./version.js";
15
+ import { AgentRun, agentRunSchema, agentParallelSchema, agentResultSchema, agentResultsSchema, agentResumeSchema, resultFilesFor as resultFiles } from "../client/agent-run.js";
16
+ import { platformTeamQuerySchema } from "../client/index.js";
17
+ import { platformProjectStatusQuerySchema } from "../client/index.js";
18
+ import { deactivateProjectFiles } from "./project-files.js";
19
+ import { projectSyncFactory } from "./project-sync.js";
20
+ import { PLATFORM_META_LIMITS, PLATFORM_TOOL_REQUEST_MAX_BYTES, platformMetaSearchSchema, platformMetaSchemasSchema, platformMetaConnectionsSchema, platformMetaExecuteSchema, platformMetaWorkbenchSchema, platformMetaBashSchema, platformIdempotencyKeySchema, platformJsonObjectSchema, withEvidentValues } from "../client/index.js";
21
+ function metaInput(schema, input) {
22
+ const json = platformJsonObjectSchema.safeParse(input);
23
+ return json.success ? withEvidentValues(schema, json.data) : input;
24
+ }
25
+ const READ_OPTIONS = { path: { type: "string" }, offset: { type: "string" }, limit: { type: "string" }, "max-chars": { type: "string" }, fields: { type: "string" },
26
+ find: { type: "string" }, pick: { type: "string" }, wait: { type: "string" } };
27
+ function findTerms(text) {
28
+ try {
29
+ const list = JSON.parse(text);
30
+ if (Array.isArray(list) && list.every(term => typeof term === "string"))
31
+ return list;
32
+ }
33
+ catch { }
34
+ return [text];
35
+ }
36
+ const agentRead = (values) => ({ ...(values.path === undefined ? {} : { path: jsonOption("--path", values.path) }),
37
+ ...(values.fields === undefined ? {} : { fields: jsonOption("--fields", values.fields) }), ...(values.offset === undefined ? {} : { offset: Number(values.offset) }),
38
+ ...(values.limit === undefined ? {} : { limit: Number(values.limit) }), ...(values["max-chars"] === undefined ? {} : { maxChars: Number(values["max-chars"]) }),
39
+ ...(values.find === undefined ? {} : { find: findTerms(values.find) }),
40
+ ...(values.pick === undefined ? {} : { pick: jsonOption("--pick", values.pick) }), ...(values.wait === undefined ? {} : { waitSeconds: Number(values.wait) }) });
41
+ const missedRequestedOutput = (result) => "resultError" in result || "fileError" in result || "upstreamRefusal" in result;
42
+ function refuseDuplicates(tokens, message) {
43
+ const names = tokens.filter(token => token.kind === "option").map(token => token.name);
44
+ if (new Set(names).size !== names.length)
45
+ throw new CliInputError(message);
46
+ }
47
+ export const help = `Relvo CLI — discover and run project tools
48
+
49
+ tools --query <capability> --limit 1 --with-schemas
50
+ Find a tool and its input schema in one request; add --app for a connected service.
51
+ Search uses monthly free calls; overage is charged.
52
+ run <tool> '<arguments-json>' [--maximum-charge-micros <integer>] --idempotency-key <key>
53
+ Quote, save the exact purchase, submit, wait and select results in one call.
54
+ Optional: --provider <id> --path '<path-array>' --fields '<names-array>' --find '<terms>'
55
+ --pick '<label-to-paths-json>' --limit <n> --wait <1-60 seconds> --file
56
+ resume <run-id> [--wait <seconds>]
57
+ Recover the same saved purchase after a lost reply; never use a fresh key for unknown work.
58
+ parallel ('<request-json>' | - | --input-file <path>)
59
+ Start independent tools together; return every admission and execution ID without waiting.
60
+ result <execution-id> [--path '<path-array>'] [--fields '<names-array>'] [--find '<terms>'] [--wait <1-60 seconds>] [--file]
61
+ Read retained data without paying for another execution. --find keeps the lines of a
62
+ text that hold a term. --file writes the whole path as JSON Lines under ./.relvo/results.
63
+ results <execution-id>... [--path '<path-array>'] [--fields '<names-array>'] [--find '<terms>'] [--wait <1-60 seconds>]
64
+ Read several executions at once, such as every ID parallel returned.
65
+ setup Sign in and connect your installed agents.
66
+ project list | add | use <project-id>
67
+ Approve and switch projects. Add --project <id> to a tool command for one call.
68
+ help <command> | help all
69
+ Load detailed instructions only when needed.
70
+
71
+ 1 USD = 1000000 micros. A budget does not authorize sending, publishing or account changes.
72
+ Scope comes from the developer API key. Provider keys remain server-side.
73
+ Environment: RELVO_API_KEY and RELVO_API_URL, or the login saved by setup.
74
+ Other commands: tool, quote, execute, execution, wait, executions, connections, connect,
75
+ project, usage, doctor, logout, update, skills, team-skills, custom-tools, monitors, triggers,
76
+ wallet, cancel, artifact-url, download, upload. Legacy NoimosAI Platform also supports access.
77
+ `;
78
+ export const fullHelp = `Relvo CLI
8
79
 
9
- Usage: noimos-platform <command>
10
- noimos-platform <command> --help | help <command>
80
+ Usage: relvo <command>
81
+ relvo <command> --help | help <command>
11
82
 
12
- project Show the developer organization and project bound to the API key
83
+ setup --api-url <url> --console-url <origin> [--client auto|claude|codex|terminal]
84
+ [--mcp-runtime registry|installed] [--local] [--no-browser] [--new-project]
85
+ Sign in through the browser and configure every installed agent
86
+ (Claude Code and Codex) with one approval; auto is the default
87
+ doctor Verify the saved login, live catalog and owned MCP connection
88
+ logout Remove the local login; revoke the server key in the console
89
+ update [status|auto on|auto off|pin <version>|rollback]
90
+ Prepare a verified CLI/MCP release for the next process start
91
+ skills list | skills update [--client claude|codex|all]
92
+ Read or explicitly update current first-party guides
93
+ team-skills sync [--client all|<agent>[,<agent>…]] [--project <id>] [--allow-scripts] | team-skills push <SKILL.md, folder, AGENTS.md, CLAUDE.md, agent or rule file> [--kind skill|instructions|agent|rule] [--slug <slug>]
94
+ Copy the team skills, agents, rules and (with --allow-scripts) hooks you may use to every coding agent, or share a local one
95
+ team-skills check [--client <agents>] | apply <snapshot> --yes [--client <agents>] [--allow-scripts | --remove-executable-settings]
96
+ team-skills auto-sync on|off|status
97
+ Detect project changes, ask before importing, and configure local MCP notification checks
98
+ team-skills list | get <slug> | versions <slug> | restore <slug> --version <n> --expected-version <n> | delete <slug>
99
+ Read a team skill's kept versions, restore one, or delete the skill
100
+ login Alias for setup
101
+ wallet sign-in [--api-url <url>] [--local] [--key-file <path>] | wallet top-up --amount <usd> --max <usd> [--key-file <path>]
102
+ Sign in or add funds with an EVM wallet; help wallet explains each
103
+ access list | access set <member-id> (--tools '<json array>' | --all) [legacy NoimosAI Platform]
104
+ Read or change what each member may use, with a key made for it
105
+
106
+ project Show the API key's project; list/add/use approved local projects
13
107
  usage Show the current project's monthly request count and spend
14
- tools [--query <text>] [--category <research|seo|social|generation>]
15
- [--available-only] [--summary]
16
- List tools and USD prices; summary omits schemas and examples
108
+ team [--member-offset <integer>] [--invitation-offset <integer>] [--limit <1-100>]
109
+ Read the selected team and your roles; owners/admins can page members and invitations
110
+ connections [--provider <id>] [--offset <integer>] [--limit <1-20>]
111
+ List project OAuth connections, states and observed grants
112
+ connect <app>
113
+ Print the console pages where you connect an app or add an account
114
+ tools [--query <text>] [--app <name-or-id>] [--category <id>]
115
+ [--provider <id>] [--available-only | --include-unavailable] [--summary | --with-schemas]
116
+ [--max-charge-micros <integer>] [--quote-arguments '<json>']
117
+ [--funding all|relvo|provider|mixed] [--price-unit <unit>] [--sort-by relevance|price_asc]
118
+ [--include '<filter-json>'] [--exclude '<filter-json>']
119
+ [--limit <1-12>] [--mode auto|keyword|semantic]
120
+ Search up to 12 tool summaries and USD prices (default 8)
17
121
  tool <name>
18
122
  Inspect one tool's arguments, example and price
123
+ meta <search|schemas|connections|execute|workbench|bash> '<request-json>'
124
+ Use six session-scoped meta tools; stdin/file input accepts 1 MiB
19
125
  executions [--limit <1-100>] [--before <id>] [--status <state>] [--tool <name>]
20
126
  Recover the API key's project execution history
21
- quote <tool> '<arguments-json>'
127
+ monitors list | popular | tools | delete <id> | resume <id> [--maximum-charge-micros-per-run <n>]
128
+ monitors quote|create --tool <name> [--args '<json>'] [--provider <id>] [--interval 15m|1h|1d | --cron "<5 fields>" [--time-zone <IANA>]] [--name <text>] [--maximum-charge-micros-per-run <n>]
129
+ monitors events [--monitor <id>] [--after <cursor>] [--limit <1-100>]
130
+ monitors destination-add <id> --destination '<json>' | destination-remove <id> --destination-id <dst_id>
131
+ Watch tools for changes, each run charged in full; help monitors explains each
132
+ triggers list | types --app <app> [--connection <id>] [--query <text>] [--offset <n>] [--limit <1-100>] | delete <id> | update <id> [--enabled true|false] [--name <text>]
133
+ triggers create --connection <id> (--tool <name> [--args '<json>'] [--interval 1m|5m|15m|1h] [--items-path </json/pointer>] [--maximum-charge-micros-per-poll <n>] | --type <app.event> [--config '<json>']) [--name <text>] [--destinations '<json>']
134
+ triggers events [--trigger <id>] [--after <cursor>] [--limit <1-100>]
135
+ triggers destination-add <id> --destination '<json>' | destination-remove <id> --destination-id <dst_id>
136
+ Get events from a connected app; help triggers explains each
137
+ custom-tools list | get <name> | versions <name> | delete <name> | restore <name> --version <n> --expected-version <n>
138
+ custom-tools create <slug> | update <name> --expected-version <n> (definition from - or --input-file <path>)
139
+ Add Python or an API endpoint as a tool for the team; help custom-tools explains each
140
+ quote <tool> '<arguments-json>' [--provider <id>] [--session <id>] [--relevance-check auto|light] [--maximum-charge-micros <cap>]
22
141
  Get a free quote without running the tool
142
+ run <tool> '<arguments-json>' [--maximum-charge-micros <integer>] --idempotency-key <key> [--provider <id>] [--session <id>]
143
+ [--path '<path-array>'] [--fields '<names-array>'] [--find '<terms>'] [--pick '<label-to-paths-json>'] [--limit <n>] [--offset <n>] [--max-chars <n>] [--wait <1-60>] [--file] [--accept-external-billing | --decline-external-billing]
144
+ Quote, save, submit and wait for already-authorized work
145
+ resume <run-id> [--wait <seconds>]
146
+ Recover the saved exact purchase; takes run's result selection, never a change to the purchase
147
+ parallel ('<request-json>' | - | --input-file <path>)
148
+ Quote, save and start up to 50 independent calls; read each accepted execution ID with result
23
149
  execute <tool> '<arguments-json>' --maximum-charge-micros <integer>
24
- --price-version <version> --idempotency-key <uuid>
150
+ --price-version <version> --idempotency-key <key> [--provider <id>] [--session <id>] [--relevance-check auto|light] [--raw]
25
151
  Reserve up to the quoted amount and enqueue paid work
26
- execution <id>
27
- Read status, output and the final USD charge
28
- wait <id> [--timeout <seconds>]
152
+ execution <id> [--raw]
153
+ Read status, a bounded output preview and the final USD charge
154
+ result <id> [--path '<JSON-array>'] [--fields '<JSON-array>'] [--offset <n>] [--limit <1-${TOOL_RESULT_PAGE_MAX_ITEMS}>] [--max-chars <1-8000>] [--find '<terms>'] [--pick '<label-to-paths-json>'] [--wait <1-60 seconds>] [--file]
155
+ Read a selected page of the retained full result
156
+ results <id>... [--path '<JSON-array>'] [--fields '<JSON-array>'] [--offset <n>] [--limit <1-${TOOL_RESULT_PAGE_MAX_ITEMS}>] [--max-chars <n>] [--find '<terms>'] [--pick '<label-to-paths-json>'] [--wait <1-60 seconds>]
157
+ Read the same selection of several retained results at once
158
+ wait <id> [--timeout <seconds>] [--raw]
29
159
  Wait for completion (default 480 seconds, maximum 900)
30
- cancel <id>
160
+ cancel <id> [--raw]
31
161
  Request cancellation of work that has not started
32
162
  artifact-url <execution-id> <artifact-id>
33
163
  Get a private output's download URL, valid for up to 5 minutes
34
164
  download <execution-id> <artifact-id> --out <file>
35
165
  Save an artifact after size/SHA-256 checks; never overwrite a file
36
166
  upload <file>
37
- Upload one explicit JPG/PNG/WAV/MP3/MP4 input, at most 12 MiB
167
+ Upload one explicit JPG/PNG/WebP/WAV/MP3/MP4/MOV input, at most 12 MiB
38
168
 
39
- Get a quote first. Use its price version and maximum charge (1 USD = 1000000
40
- micros). Keep the same body and idempotency key when retrying a submission.
169
+ run quotes internally and charges the project's balance; quote separately only when
170
+ the user asks to approve a price. execute needs a quote first: use its price version
171
+ and maximum charge (1 USD = 1000000 micros). Keep the same body and idempotency key
172
+ when retrying a submission.
173
+ Auto routing is the default (--provider auto is equivalent). Use --provider to pin a provider for both quote
174
+ and execute; tools --provider only filters discovery.
41
175
  Execution is asynchronous. Poll at intervals of at least 5 seconds.
42
- Use - in place of arguments-json to read JSON from stdin (at most 64 KiB).
43
- Use --input-file <path> in place of arguments-json to read a JSON file.
176
+ For quote, execute, run and meta, use - in place of arguments-json to read JSON from stdin,
177
+ or --input-file <path> to read a JSON file (at most 1 MiB).
44
178
  wait exits 0 on success, 1 on failed/cancelled/needs_review, 3 on timeout.
45
179
  A timeout leaves accepted work running; read or wait on the same execution ID.
180
+ Large outputs stay in the execution. Use result to select data; --raw explicitly
181
+ returns the full output for execution, wait, execute or cancel.
46
182
 
47
- Standalone tools only: no workspace, brand, knowledge base, dataset, connected
48
- provider account, post, workspace skill or chat-session commands. Scope comes
49
- from the developer API key; supply prompts, public URLs, project files or artifacts.
183
+ Scope comes from the developer API key. OAuth connection IDs belong to this
184
+ project; inspect the selected tool's schema before using one. Workspace, brand,
185
+ knowledge base, dataset, workspace skill and chat-session commands are unavailable.
186
+ Use --project <id> on a tool command to select an already approved login for one call.
187
+ project list/add/use manages saved approvals; reconnect MCP after changing the default.
188
+ Start a managed MCP pinned to one approval with mcp --profile <managed-path> --project <id>.
50
189
 
51
- Environment: NOIMOS_PLATFORM_API_URL, NOIMOS_PLATFORM_API_KEY
52
- Local emulators only: NOIMOS_PLATFORM_ENV=local
190
+ Environment: RELVO_API_URL, RELVO_API_KEY
191
+ Project status: relvo project status [--member-offset <n>] [--invitation-offset <n>] [--limit <1-100>]
192
+ Local emulators only: RELVO_ENV=local
53
193
 
54
- Manage organizations, members, keys and billing at platform.noimosai.com.
194
+ Manage teams, members, keys and billing in the Relvo console.
55
195
  `;
56
- export async function runCli(args, env, output, error, transport = fetch, stdin = process.stdin) {
196
+ export const RELVO_CLI_SURFACE = { distribution: PLATFORM_DISTRIBUTIONS.relvo, help, fullHelp, commandHelp, supports: () => true, savedLogins: true, clientNameHeader: "x-relvo-client-name" };
197
+ export async function runCli(args, env, output, error, transport = fetch, stdin = process.stdin, profile, surface = RELVO_CLI_SURFACE) {
198
+ const { cliCommand, name } = surface.distribution;
199
+ let projectId;
200
+ const projectOption = args.indexOf("--project");
201
+ if (projectOption !== -1) {
202
+ if (!surface.savedLogins) {
203
+ error(`${name} has no saved project logins; use the API key of the project you want. Nothing was sent.`);
204
+ return 2;
205
+ }
206
+ const value = args[projectOption + 1];
207
+ if (!value?.trim() || value.startsWith("-") || args.lastIndexOf("--project") !== projectOption) {
208
+ error("Provide one --project <id>. Nothing was sent.");
209
+ return 2;
210
+ }
211
+ projectId = value;
212
+ args = [...args.slice(0, projectOption), ...args.slice(projectOption + 2)];
213
+ }
214
+ if (args.length === 2 && args[0] === "help" && args[1] === "all") {
215
+ output(surface.fullHelp);
216
+ return 0;
217
+ }
57
218
  if (!args.length || args[0] === "--help" || args[0] === "-h" || args.length === 1 && args[0] === "help") {
58
- output(help);
219
+ output(surface.help);
59
220
  return 0;
60
221
  }
61
222
  if (args[0] === "help" || args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
62
- const detail = commandHelp(args[0] === "help" ? args[1] : args[0]);
223
+ const detail = surface.commandHelp(args[0] === "help" ? args[1] : args[0]);
63
224
  if (!detail || args[0] === "help" && args.length !== 2) {
64
- error("Unknown help command. Run noimos-platform --help.");
225
+ error(`Unknown help command. Run ${cliCommand} --help.`);
65
226
  return 2;
66
227
  }
67
228
  output(detail);
68
229
  return 0;
69
230
  }
70
231
  if (args.length === 1 && args[0] === "--version") {
71
- output("0.1.1");
232
+ output(PLATFORM_CLI_VERSION);
72
233
  return 0;
73
234
  }
235
+ if (!surface.supports(args)) {
236
+ error(`${name} does not offer this command or option. Nothing was sent. Run ${cliCommand} --help.`);
237
+ return 2;
238
+ }
74
239
  let command;
75
240
  let exitCode = 0;
76
241
  try {
77
- if (["quote", "execute"].includes(args[0]) && ["-", "--input-file"].includes(args[2])) {
78
- const file = args[2] === "--input-file" ? args[3] : "-";
242
+ const readInput = async (file, limit = PLATFORM_TOOL_REQUEST_MAX_BYTES) => {
79
243
  if (!file || file.startsWith("--"))
80
- throw new Error("Provide an input file");
81
- const source = file === "-" ? stdin : createReadStream(file);
244
+ throw new CliInputError("Provide an input file");
82
245
  const chunks = [];
83
246
  let bytes = 0;
84
- for await (const chunk of source) {
247
+ for await (const chunk of file === "-" ? stdin : createReadStream(file)) {
85
248
  const buffer = Buffer.from(chunk);
86
249
  bytes += buffer.byteLength;
87
- if (bytes > 65536)
88
- throw new Error("Input exceeds 64 KiB");
250
+ if (bytes > limit)
251
+ throw new CliInputError("Input exceeds its byte limit");
89
252
  chunks.push(buffer);
90
253
  }
91
- args = [args[0], args[1], Buffer.concat(chunks).toString("utf8"), ...args.slice(args[2] === "--input-file" ? 4 : 3)];
254
+ return Buffer.concat(chunks).toString("utf8");
255
+ };
256
+ if (["quote", "execute", "run", "meta"].includes(args[0]) && ["-", "--input-file"].includes(args[2])) {
257
+ const input = await readInput(args[2] === "--input-file" ? args[3] : "-", args[0] === "meta" ? PLATFORM_META_LIMITS.requestBytes : PLATFORM_TOOL_REQUEST_MAX_BYTES);
258
+ args = [args[0], args[1], input, ...args.slice(args[2] === "--input-file" ? 4 : 3)];
259
+ }
260
+ if (args[0] === "parallel") {
261
+ if (!(args.length === 2 || args.length === 3 && args[1] === "--input-file"))
262
+ throw new CliInputError("Provide one parallel request as JSON, - or --input-file <path>");
263
+ const source = args[1], text = source === "--input-file" ? await readInput(args[2]) : source === "-" ? await readInput("-") : source;
264
+ const input = agentParallelSchema.parse(metaInput(agentParallelSchema, JSON.parse(text)));
265
+ command = async (client, config) => {
266
+ const result = await new AgentRun(client, env.RELVO_RUN_DIRECTORY, resultFiles(env), walletAutoTopUpFromEnvironment(env, config)).parallel(input);
267
+ exitCode = result.allAccepted ? 0 : 1;
268
+ return result;
269
+ };
270
+ }
271
+ else if (args.length === 3 && args[0] === "meta") {
272
+ const input = JSON.parse(args[2]);
273
+ if (args[1] === "search") {
274
+ const value = platformMetaSearchSchema.parse(metaInput(platformMetaSearchSchema, input));
275
+ command = client => client.searchMetaTools(value);
276
+ }
277
+ else if (args[1] === "schemas") {
278
+ const value = platformMetaSchemasSchema.parse(metaInput(platformMetaSchemasSchema, input));
279
+ command = client => client.getToolSchemas(value);
280
+ }
281
+ else if (args[1] === "connections") {
282
+ const value = platformMetaConnectionsSchema.parse(metaInput(platformMetaConnectionsSchema, input));
283
+ command = client => client.manageConnections(value);
284
+ }
285
+ else if (args[1] === "execute") {
286
+ const value = platformMetaExecuteSchema.parse(metaInput(platformMetaExecuteSchema, input));
287
+ command = async (client) => { const result = await client.multiExecute(value); if (!result.data.allAccepted)
288
+ exitCode = 1; return result; };
289
+ }
290
+ else if (args[1] === "workbench") {
291
+ const value = platformMetaWorkbenchSchema.parse(metaInput(platformMetaWorkbenchSchema, input));
292
+ command = client => client.remoteWorkbench(value);
293
+ }
294
+ else if (args[1] === "bash") {
295
+ const value = platformMetaBashSchema.parse(metaInput(platformMetaBashSchema, input));
296
+ command = client => client.remoteBash(value);
297
+ }
298
+ else
299
+ throw new CliInputError("Unknown meta tool");
92
300
  }
93
- if (args.length === 2 && args[0] === "upload" && args[1].trim())
301
+ else if (args.length === 2 && args[0] === "upload" && args[1].trim())
94
302
  command = (client) => uploadLocalInput(client, args[1]);
303
+ else if (args[0] === "project" && args[1] === "status") {
304
+ const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: {
305
+ "member-offset": { type: "string" }, "invitation-offset": { type: "string" }, limit: { type: "string" }
306
+ } });
307
+ refuseDuplicates(tokens, "Duplicate project status option");
308
+ const input = platformProjectStatusQuerySchema.parse({ memberOffset: values["member-offset"], invitationOffset: values["invitation-offset"], limit: values.limit });
309
+ command = client => client.projectStatus(input);
310
+ }
311
+ else if (args[0] === "team") {
312
+ const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: {
313
+ "member-offset": { type: "string" }, "invitation-offset": { type: "string" }, limit: { type: "string" }
314
+ } });
315
+ refuseDuplicates(tokens, "Duplicate team option");
316
+ const input = platformTeamQuerySchema.parse({ memberOffset: values["member-offset"], invitationOffset: values["invitation-offset"], limit: values.limit });
317
+ command = client => client.team(input);
318
+ }
95
319
  else if (args.length === 1 && args[0] === "project")
96
320
  command = (client) => client.project();
97
321
  else if (args.length === 1 && args[0] === "usage")
98
322
  command = (client) => client.usage();
323
+ else if (args.length === 2 && args[0] === "connect" && args[1].trim() && !args[1].startsWith("-"))
324
+ command = client => client.connectApp({ app: args[1] });
325
+ else if (args[0] === "connections") {
326
+ const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: { provider: { type: "string" }, offset: { type: "string" }, limit: { type: "string" } } });
327
+ refuseDuplicates(tokens, "Duplicate connection option");
328
+ const input = platformConnectionCatalogQuerySchema.parse({ provider: values.provider, offset: values.offset === undefined ? undefined : Number(values.offset), limit: values.limit === undefined ? undefined : Number(values.limit) });
329
+ command = client => client.connections(input);
330
+ }
99
331
  else if (args[0] === "tools") {
100
332
  const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: {
101
- query: { type: "string" }, category: { type: "string" }, "available-only": { type: "boolean" }, summary: { type: "boolean" },
333
+ query: { type: "string" }, app: { type: "string" }, category: { type: "string" }, provider: { type: "string" }, "available-only": { type: "boolean" }, "include-unavailable": { type: "boolean" }, summary: { type: "boolean" }, "with-schemas": { type: "boolean" }, limit: { type: "string" }, mode: { type: "string" },
334
+ "max-charge-micros": { type: "string" }, "quote-arguments": { type: "string" }, funding: { type: "string" }, "price-unit": { type: "string" }, "sort-by": { type: "string" },
335
+ include: { type: "string" }, exclude: { type: "string" },
102
336
  } });
103
- const names = tokens.filter((token) => token.kind === "option").map((token) => token.name);
104
- if (new Set(names).size !== names.length)
105
- throw new Error("Duplicate discovery option");
106
- const input = platformToolDiscoverySchema.parse({ query: values.query, category: values.category, availableOnly: values["available-only"], summary: values.summary });
107
- command = (client) => client.discoverTools(input);
337
+ refuseDuplicates(tokens, "Duplicate discovery option");
338
+ if (values["available-only"] && values["include-unavailable"])
339
+ throw new CliInputError("Conflicting availability filters");
340
+ if (values.summary && values["with-schemas"])
341
+ throw new CliInputError("Conflicting schema options");
342
+ if (values["max-charge-micros"] !== undefined && !values["max-charge-micros"].trim())
343
+ throw new CliInputError("Invalid cost cap");
344
+ const input = toolDiscoveryInputSchema.parse({ query: values.query, app: values.app, category: values.category, provider: values.provider,
345
+ maxChargeMicros: values["max-charge-micros"] === undefined ? undefined : Number(values["max-charge-micros"]),
346
+ quoteArguments: values["quote-arguments"] === undefined ? undefined : jsonOption("--quote-arguments", values["quote-arguments"]),
347
+ funding: values.funding, priceUnit: values["price-unit"], sortBy: values["sort-by"],
348
+ include: values.include === undefined ? undefined : jsonOption("--include", values.include), exclude: values.exclude === undefined ? undefined : jsonOption("--exclude", values.exclude),
349
+ availableOnly: values["include-unavailable"] ? false : true, summary: !values["with-schemas"], limit: values.limit === undefined ? undefined : Number(values.limit), mode: values.mode });
350
+ command = (client) => client.searchTools(input);
108
351
  }
109
352
  else if (args.length === 2 && args[0] === "tool" && /^[a-z][a-z0-9_]{1,63}$/.test(args[1]))
110
- command = (client) => client.tool(args[1]);
111
- else if (args[0] === "executions" && args.length % 2 === 1) {
112
- const flags = {};
113
- for (let index = 1; index < args.length; index += 2) {
114
- const name = args[index].slice(2);
115
- if (!["--limit", "--before", "--status", "--tool"].includes(args[index]) || Object.hasOwn(flags, name))
116
- throw new Error("Invalid history flag");
117
- flags[name] = name === "limit" ? Number(args[index + 1]) : args[index + 1];
118
- }
119
- const input = platformExecutionListQuerySchema.parse(flags);
353
+ command = (client) => client.inspectTool(args[1]);
354
+ else if (args[0] === "executions") {
355
+ const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: { limit: { type: "string" }, before: { type: "string" }, status: { type: "string" }, tool: { type: "string" } } });
356
+ refuseDuplicates(tokens, "Duplicate history option");
357
+ const input = platformExecutionListQuerySchema.parse({ limit: values.limit === undefined ? undefined : Number(values.limit), before: values.before, status: values.status, tool: values.tool });
120
358
  command = (client) => client.executions(input);
121
359
  }
122
- else if (args[0] === "wait" && (args.length === 2 || args.length === 4 && args[2] === "--timeout") && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
123
- const seconds = args.length === 4 ? Number(args[3]) : 480;
360
+ else if (args[0] === "result") {
361
+ const id = platformExecutionIdSchema.parse(args[1]);
362
+ const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { ...READ_OPTIONS, file: { type: "boolean" } } });
363
+ refuseDuplicates(tokens, "Duplicate result option");
364
+ if (values.file && (values.offset !== undefined || values.limit !== undefined || values["max-chars"] !== undefined || values.find !== undefined))
365
+ throw new CliInputError("--file writes the whole path; omit paging options");
366
+ if (values.file || values.find !== undefined || values.pick !== undefined || values.wait !== undefined) {
367
+ const input = agentResultSchema.parse({ id, ...agentRead(values), ...(values.file ? { file: true } : {}) });
368
+ command = async (client) => {
369
+ const result = await new AgentRun(client, env.RELVO_RUN_DIRECTORY, resultFiles(env)).result(input);
370
+ exitCode = "timedOut" in result && result.timedOut ? 3 : result.status === "succeeded" && !missedRequestedOutput(result) ? 0 : 1;
371
+ return result;
372
+ };
373
+ }
374
+ else {
375
+ const selection = toolResultSelectionSchema.parse({ path: values.path === undefined ? undefined : jsonOption("--path", values.path),
376
+ offset: values.offset === undefined ? undefined : Number(values.offset), limit: values.limit === undefined ? undefined : Number(values.limit),
377
+ maxChars: values["max-chars"] === undefined ? undefined : Number(values["max-chars"]),
378
+ fields: values.fields === undefined ? undefined : jsonOption("--fields", values.fields),
379
+ });
380
+ command = async (client) => {
381
+ const result = await client.readExecutionResult(id, selection);
382
+ if (result.data.status && result.data.status !== "succeeded")
383
+ exitCode = ["queued", "running", "waiting"].includes(result.data.status) ? 3 : 1;
384
+ else if (result.data.upstreamRefusal)
385
+ exitCode = 1;
386
+ return result;
387
+ };
388
+ }
389
+ }
390
+ else if (args[0] === "results") {
391
+ const { values, positionals, tokens } = parseArgs({ args: args.slice(1), tokens: true, allowPositionals: true, options: READ_OPTIONS });
392
+ refuseDuplicates(tokens, "Duplicate results option");
393
+ const input = agentResultsSchema.parse({ ids: positionals, ...agentRead(values) });
394
+ command = async (client) => {
395
+ const reply = await new AgentRun(client, env.RELVO_RUN_DIRECTORY, resultFiles(env)).results(input);
396
+ const pending = reply.results.some(item => "status" in item && ["queued", "running", "waiting"].includes(String(item.status)));
397
+ const delivered = reply.results.every(item => "status" in item && item.status === "succeeded" && !missedRequestedOutput(item));
398
+ exitCode = delivered ? 0 : pending ? 3 : 1;
399
+ return reply;
400
+ };
401
+ }
402
+ else if (args[0] === "wait") {
403
+ const id = platformExecutionIdSchema.parse(args[1]);
404
+ const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { timeout: { type: "string" }, raw: { type: "boolean" } } });
405
+ refuseDuplicates(tokens, "Duplicate wait option");
406
+ const seconds = values.timeout === undefined ? 480 : Number(values.timeout);
124
407
  if (!Number.isInteger(seconds) || seconds < 1 || seconds > 900)
125
- throw new Error("Invalid wait timeout");
408
+ throw new CliInputError("Invalid wait timeout");
126
409
  command = async (client) => {
127
- const result = await client.waitExecution(args[1], seconds * 1000);
128
- exitCode = result.timedOut ? 3 : result.data?.status === "succeeded" ? 0 : 1;
410
+ const result = values.raw ? await client.waitExecution(id, seconds * 1000) : await client.waitExecutionContext(id, seconds * 1000);
411
+ exitCode = result.timedOut ? 3 : result.data?.status === "succeeded" && !result.data.upstreamRefusal ? 0 : 1;
129
412
  return result;
130
413
  };
131
414
  }
132
- else if (args.length === 2 && ["execution", "cancel"].includes(args[0]) && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
133
- command = (client) => args[0] === "execution" ? client.execution(args[1]) : client.cancelExecution(args[1]);
415
+ else if (["execution", "cancel"].includes(args[0])) {
416
+ const id = platformExecutionIdSchema.parse(args[1]);
417
+ const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { raw: { type: "boolean" } } });
418
+ if (tokens.length > 1)
419
+ throw new CliInputError("Duplicate output option");
420
+ command = async (client) => {
421
+ if (args[0] === "execution")
422
+ return values.raw ? client.execution(id) : client.executionContext(id);
423
+ const result = await client.cancelExecution(id);
424
+ return values.raw ? result : executionReceiptContext(result);
425
+ };
134
426
  }
135
- else if (args.length === 5 && args[0] === "download" && args[3] === "--out" && args[4].trim() && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
427
+ else if (args.length === 5 && args[0] === "download" && args[3] === "--out" && args[4].trim()) {
428
+ const id = platformExecutionIdSchema.parse(args[1]);
136
429
  const artifactId = platformArtifactIdSchema.parse(args[2]);
137
- command = (client) => downloadArtifact(client, args[1], artifactId, args[4], transport);
430
+ command = (client) => downloadArtifact(client, id, artifactId, args[4], transport);
138
431
  }
139
- else if (args.length === 3 && args[0] === "artifact-url" && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
432
+ else if (args.length === 3 && args[0] === "artifact-url") {
433
+ const id = platformExecutionIdSchema.parse(args[1]);
140
434
  const artifactId = platformArtifactIdSchema.parse(args[2]);
141
- command = (client) => client.artifactDownload(args[1], artifactId);
435
+ command = (client) => client.artifactDownload(id, artifactId);
436
+ }
437
+ else if ((args[0] === "run" && args.length >= 3) || (args[0] === "resume" && args.length >= 2)) {
438
+ const resume = args[0] === "resume";
439
+ const { values, tokens } = parseArgs({ args: args.slice(resume ? 2 : 3), tokens: true, options: { ...READ_OPTIONS,
440
+ "maximum-charge-micros": { type: "string" }, "idempotency-key": { type: "string" }, provider: { type: "string" }, session: { type: "string" },
441
+ file: { type: "boolean" }, "accept-external-billing": { type: "boolean" }, "decline-external-billing": { type: "boolean" },
442
+ } });
443
+ refuseDuplicates(tokens, "Duplicate run option");
444
+ if (values["accept-external-billing"] && values["decline-external-billing"])
445
+ throw new CliInputError("Conflicting external billing options");
446
+ const { waitSeconds, pick, ...selection } = agentRead(values);
447
+ const view = { waitSeconds, pick, file: values.file, selection };
448
+ if (resume) {
449
+ if (values["maximum-charge-micros"] || values["idempotency-key"] || values.provider || values.session || values["accept-external-billing"] || values["decline-external-billing"])
450
+ throw new CliInputError("Resume retains the original purchase");
451
+ const input = agentResumeSchema.parse({ runId: args[1], ...view });
452
+ command = async (client, config) => {
453
+ const result = await new AgentRun(client, env.RELVO_RUN_DIRECTORY, resultFiles(env), walletAutoTopUpFromEnvironment(env, config)).resume(input);
454
+ exitCode = "timedOut" in result && result.timedOut ? 3 : result.status === "succeeded" && !missedRequestedOutput(result) ? 0 : 1;
455
+ return result;
456
+ };
457
+ }
458
+ else {
459
+ const maximum = values["maximum-charge-micros"];
460
+ if (maximum !== undefined && !/^\d+$/.test(maximum))
461
+ throw new CliInputError("Invalid run ceiling");
462
+ const input = agentRunSchema.parse({ tool: args[1], arguments: jsonOption("<arguments-json>", args[2]), ...(maximum === undefined ? {} : { maximumChargeMicros: Number(maximum) }),
463
+ idempotencyKey: values["idempotency-key"], provider: values.provider, sessionId: values.session,
464
+ ...(values["decline-external-billing"] ? { externalBillingAccepted: false } : {}), ...view });
465
+ command = async (client, config) => {
466
+ const result = await new AgentRun(client, env.RELVO_RUN_DIRECTORY, resultFiles(env), walletAutoTopUpFromEnvironment(env, config)).run(input);
467
+ exitCode = "status" in result ? result.timedOut ? 3 : result.status === "succeeded" && !missedRequestedOutput(result) ? 0 : 1 : 1;
468
+ return result;
469
+ };
470
+ }
142
471
  }
143
- else if (args.length === 3 && args[0] === "quote") {
144
- const input = platformToolInputSchema.parse({ tool: args[1], arguments: JSON.parse(args[2]) });
472
+ else if (args.length >= 3 && args[0] === "quote") {
473
+ const { values, tokens } = parseArgs({ args: args.slice(3), tokens: true, options: { provider: { type: "string" }, session: { type: "string" },
474
+ "relevance-check": { type: "string" }, "maximum-charge-micros": { type: "string" } } });
475
+ refuseDuplicates(tokens, "Duplicate quote option");
476
+ const maximum = values["maximum-charge-micros"];
477
+ if (maximum !== undefined && !/^\d+$/.test(maximum))
478
+ throw new CliInputError("Invalid quote cap");
479
+ const input = platformQuoteRequestSchema.parse({ tool: args[1], arguments: jsonOption("<arguments-json>", args[2]), sessionId: values.session, ...(values.provider === undefined ? {} : { provider: values.provider }),
480
+ ...(values["relevance-check"] === undefined ? {} : { relevanceCheck: values["relevance-check"] }), ...(maximum === undefined ? {} : { maximumChargeMicros: Number(maximum) }) });
145
481
  command = (client) => client.quote(input);
146
482
  }
147
- else if (args.length === 9 && args[0] === "execute") {
148
- const flags = new Map();
149
- for (let index = 3; index < args.length; index += 2) {
150
- if (!["--maximum-charge-micros", "--price-version", "--idempotency-key"].includes(args[index]) || flags.has(args[index]))
151
- throw new Error("Invalid flag");
152
- flags.set(args[index], args[index + 1]);
153
- }
154
- const maximum = flags.get("--maximum-charge-micros"), idempotencyKey = flags.get("--idempotency-key");
155
- if (!maximum || !/^\d+$/.test(maximum) || !idempotencyKey || !/^[A-Za-z0-9._:-]{8,128}$/.test(idempotencyKey))
156
- throw new Error("Invalid execution confirmation");
157
- const input = platformCreateExecutionSchema.parse({ tool: args[1], arguments: JSON.parse(args[2]), maximumChargeMicros: Number(maximum), priceVersion: flags.get("--price-version") });
158
- command = (client) => client.createExecution(input, idempotencyKey);
483
+ else if (args.length >= 3 && args[0] === "execute") {
484
+ const { values, tokens } = parseArgs({ args: args.slice(3), tokens: true, options: {
485
+ "maximum-charge-micros": { type: "string" }, "price-version": { type: "string" }, "idempotency-key": { type: "string" }, provider: { type: "string" }, session: { type: "string" }, raw: { type: "boolean" },
486
+ "relevance-check": { type: "string" },
487
+ } });
488
+ refuseDuplicates(tokens, "Duplicate execution option");
489
+ const maximum = values["maximum-charge-micros"], idempotencyKey = values["idempotency-key"];
490
+ if (!maximum || !/^\d+$/.test(maximum) || !idempotencyKey || !platformIdempotencyKeySchema.safeParse(idempotencyKey).success)
491
+ throw new CliInputError("Invalid execution confirmation");
492
+ const input = platformCreateExecutionSchema.parse({ tool: args[1], arguments: jsonOption("<arguments-json>", args[2]), sessionId: values.session, maximumChargeMicros: Number(maximum), priceVersion: values["price-version"], ...(values.provider === undefined ? {} : { provider: values.provider }),
493
+ ...(values["relevance-check"] === undefined ? {} : { relevanceCheck: values["relevance-check"] }) });
494
+ command = async (client) => {
495
+ const result = await client.createExecution(input, idempotencyKey);
496
+ return values.raw ? result : executionReceiptContext(result);
497
+ };
159
498
  }
499
+ else if (isTeamResource(args[0]))
500
+ command = await teamResourceCommand(args, source => readInput(source));
160
501
  else
161
- throw new Error("Invalid command");
502
+ throw new CliInputError("Invalid command");
162
503
  }
163
- catch {
164
- error(`Invalid command, JSON arguments or input file.\n\n${commandHelp(args[0]) ?? help}`);
504
+ catch (failure) {
505
+ error(`${cliInputReason(failure)}. Nothing was sent. Run ${cliCommand} ${surface.commandHelp(args[0]) ? `help ${args[0]}` : "--help"}.`);
165
506
  return 2;
166
507
  }
167
508
  try {
168
- const client = new PlatformClient(configFromEnvironment(env, "cli"), transport);
169
- const result = await command(client);
170
- output(JSON.stringify(result, null, 2));
509
+ if (projectId && !profile)
510
+ throw new Error(`--project needs a saved login. Run ${cliCommand} setup first.`);
511
+ const config = projectId && profile ? await localProjectConfig(profile, projectId, env, "cli", transport)
512
+ : profile ? await runtimeConfig(env, "cli", profile) : configFromEnvironment(env, "cli", surface.distribution);
513
+ const header = surface.clientNameHeader, client = env.CLAUDECODE === "1" ? "claude-code" : `${cliCommand}-cli`;
514
+ const result = await command(new PlatformClient(config, header ? (input, init) => transport(input, { ...init, headers: { ...init?.headers, [header]: client } }) : transport), config);
515
+ output(JSON.stringify(result));
171
516
  return exitCode;
172
517
  }
173
518
  catch (failure) {
174
- error(failure instanceof Error ? failure.message : "Platform request failed.");
519
+ error(failure instanceof Error ? failure.message : `${name} request failed.`);
175
520
  return 1;
176
521
  }
177
522
  }
523
+ export async function runRelvoCli(args, env, output, error, transport = fetch, stdin = process.stdin, options = {}) {
524
+ if (args[0] === "mcp") {
525
+ try {
526
+ const { values } = parseArgs({ args: args.slice(1), options: { profile: { type: "string" }, mode: { type: "string" }, project: { type: "string" } } });
527
+ if (!values.profile)
528
+ throw new Error("relvo mcp requires the managed --profile path.");
529
+ const { startPlatformMcp } = await import("../mcp/runtime.js");
530
+ const profileHome = dirname(dirname(dirname(values.profile)));
531
+ await startPlatformMcp(options.version, values.profile, values.mode, values.project, id => deactivateProjectFiles(id, env, profileHome), projectSyncFactory(env, profileHome));
532
+ return process.exitCode ? 1 : 0;
533
+ }
534
+ catch {
535
+ error("Relvo MCP could not start. Run relvo doctor to inspect the managed connection.");
536
+ return 1;
537
+ }
538
+ }
539
+ const localFailure = (failure, fallback) => {
540
+ if (!isCliInputFailure(failure)) {
541
+ error(failure instanceof Error ? failure.message : fallback);
542
+ return 1;
543
+ }
544
+ error(`${cliInputReason(failure).replace(/\.$/u, "")}. Nothing was changed. Run relvo help ${args[0]}.`);
545
+ return 2;
546
+ };
547
+ if (args[0] === "wallet" && !args.some((arg) => ["--help", "-h"].includes(arg))) {
548
+ try {
549
+ await runWalletCommand(args, env, output, transport, options);
550
+ return 0;
551
+ }
552
+ catch (failure) {
553
+ return localFailure(failure, "Relvo wallet command failed.");
554
+ }
555
+ }
556
+ const local = ["setup", "login", "doctor", "logout", "skills"].includes(args[0]) || args[0] === "team-skills" && ["sync", "push", "check", "apply", "dismiss", "auto-sync"].includes(args[1]) || args[0] === "project" && ["list", "add", "use"].includes(args[1]);
557
+ if (local && !args.some((arg) => ["--help", "-h"].includes(arg))) {
558
+ try {
559
+ await runLocalCommand(args, env, output, transport, options);
560
+ return 0;
561
+ }
562
+ catch (failure) {
563
+ return localFailure(failure, "Relvo local setup failed.");
564
+ }
565
+ }
566
+ return runCli(args, env, output, error, transport, stdin, localProfilePath(options.home));
567
+ }