@gondoai/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -94
  2. package/package.json +1 -1
  3. package/src/cli.mjs +114 -83
package/README.md CHANGED
@@ -1,118 +1,48 @@
1
1
  # Gondo CLI
2
2
 
3
- Give a coding agent access to [Gondo](https://www.gondo.ai) to create employees, build and test their jobs, and inspect results. The CLI calls Gondo's HTTP API and loads its current authoring guide from the service.
3
+ Use Gondo Admin capabilities from a coding agent or terminal. Requires Node.js 22.12+, a Gondo account with Pro access, and an API key created by an account admin.
4
4
 
5
- Requires **Node.js 22.12 or later** and a Gondo API key. API access requires Pro, an active Pro trial, or complimentary Pro. Installing the CLI does not enable API access.
5
+ ## Start
6
6
 
7
- ## Get started
7
+ Save `GONDO_API_KEY=…` in a private `gondo.env` file outside source control, then run:
8
8
 
9
- 1. In Gondo, open **Account → API keys** and create a key. This requires an account admin. The key is all your agent needs.
10
- 2. Save the key in a local file called `gondo.env`, outside your source repository:
11
-
12
- ```dotenv
13
- GONDO_API_KEY=your-api-key
14
- ```
15
-
16
- 3. From any directory, read the guide:
17
-
18
- ```bash
19
- npx --yes @gondoai/cli --env-file /absolute/path/to/gondo.env guide
20
- ```
21
-
22
- No Gondo app checkout is needed. To install a persistent `gondo` command instead:
23
-
24
- ```bash
25
- npm install --global @gondoai/cli
26
- gondo --env-file /absolute/path/to/gondo.env guide
9
+ ```sh
10
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env guide
27
11
  ```
28
12
 
29
- `--env-file` accepts dotenv syntax, including quoted values. Existing environment variables take precedence over the file. If `GONDO_API_KEY` is already set in your agent's environment, omit the flag. The CLI never loads an env file implicitly. It connects to `https://runtime.gondo.ai` and discovers the account belonging to your key automatically. For development, set `GONDO_API_URL` to another runtime; `GONDO_ACCOUNT_ID` remains an optional override for existing setups. Overrides do not change which account a key can access.
30
-
31
- Keep the key out of prompts, source control, and shared logs. Give your agent the local env-file path, not the key text. Connect required integrations using the setup commands below or in Gondo; enable Public browsing for public websites.
32
-
33
- ## Give this to your agent
13
+ The key identifies its account automatically. Optional `GONDO_API_URL` and `GONDO_ACCOUNT_ID` overrides support development. Environment variables override the explicitly selected env file. Never paste credentials into prompts or command arguments.
34
14
 
35
- Use a coding agent with terminal access, such as Codex or Claude Code. Replace the task and local file path:
15
+ ## Shared capabilities
36
16
 
37
- > Use Gondo to build an employee that **[describe the task]**. Your credentials are in **[/absolute/path/to/gondo.env]**; do not print them. Start by running `npx --yes @gondoai/cli --env-file /absolute/path/to/gondo.env guide`. Read the workflow and node guides as directed, then inspect the available integrations. Create a new employee and job, validate it, test with **[agreed inputs]**, and inspect the actual run output. Publish the new job while leaving it disabled for my review. Do not change existing jobs or send messages unless my task explicitly requires it. Report the employee and job IDs and the test results.
38
-
39
- Commands shown as `gondo …` in the served guide can all be run as `npx --yes @gondoai/cli --env-file /absolute/path/to/gondo.env …`. If an older guide mentions `pnpm gondo` or the app checkout, use this npm command instead.
40
-
41
- ## Useful commands
42
-
43
- After a global install, with credentials in `gondo.env`:
44
-
45
- ```bash
46
- gondo --env-file ./gondo.env guide --topic workflows
47
- gondo --env-file ./gondo.env guide --topic nodes
48
- gondo --env-file ./gondo.env list /integrations
49
- gondo --env-file ./gondo.env employees list
50
- gondo --env-file ./gondo.env workflows list
51
- gondo --help
52
- gondo --version
17
+ ```sh
18
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env tools list
19
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env tools describe gondo_upsert_workflow_node
20
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env call gondo_upsert_workflow_node --file arguments.json
21
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env call load_skill --file skill.json --session os_existing
22
+ npx --yes @gondoai/cli@0.2.0 --env-file /absolute/path/gondo.env workflows get <id> --source active --format json
23
+ npx --yes @gondoai/cli@0.2.0 --help
53
24
  ```
54
25
 
55
- The command name remains `workflows`; Gondo calls them jobs in the app. The guide documents authoring, execution, file upload/download, resumable workspaces, browser login handoffs, and run inspection. Human approvals are completed by a signed-in person in Gondo.
26
+ The server supplies Admin's existing guides, tool names, descriptions and schemas. The CLI contains no separate authoring manual. `guide --topic workflows` returns the authoritative workflow bundle. Convenience commands adapt to the same operations. Workflow reads return one definition; use `--editor-state` explicitly for the complete editor state.
56
27
 
57
- API responses are JSON. Exit codes: **0** success, **1** failure, **2** user action required. Tests and investigation code execute real actions through your connected integrations. The CLI does not retry mutations automatically; inspect the run or execution after a timeout before trying again.
28
+ Use the host's native web search, delegation and authorization. The catalog preserves Admin's `requireConfirmation` flag; the host applies its authorization policy before invocation, including authorization already given by the user. Required workflow human reviews and login stay in signed-in Gondo; interactive calls return action links. File tools return session artifacts; download inspection images and view them in the host before making visual claims.
58
29
 
59
- ## Development and publishing
30
+ Integration management requires `integrations:manage`. Creation follows Admin defaults. Change availability explicitly with `integrations enable|disable <id>`. Submit credentials using `integrations credentials <id> --file <private-file>` or stdin. Credential replacement never implicitly enables a connection. Custom API, custom-tool and browser management are available through the shared catalog.
60
31
 
61
- The service must support `GET /api/operator/me` before publishing this key-only CLI release. That endpoint authenticates the key and returns its account; the Pro requirements still apply.
32
+ Responses are JSON. Exit codes: **0** success, **1** failure, **2** action required. Mutations are never automatically retried. After an uncertain write, inspect its run or execution before proceeding. Existing session, binary transfer, private webhook output and attempt download commands remain available through `--help`.
62
33
 
63
- This repository owns the standalone CLI. Workflow schemas, authoring guides, and authorization remain in the Gondo service. The initial client was extracted from the app's existing operator CLI; it has one runtime dependency and no build step.
34
+ ## Development and release
64
35
 
65
- ```bash
36
+ ```sh
66
37
  npm ci
67
38
  npm test
68
- ```
69
-
70
- Tests exercise the packed npm artifact installed in a separate temporary directory, including its executable, env-file loading, HTTP authentication, and YAML requests. No Gondo credentials or live account are required.
71
-
72
- To publish the prepared `@gondoai/cli@0.1.0` release, sign in with an npm account that can publish to the `gondoai` organisation:
73
-
74
- ```bash
75
- npm login
76
- npm whoami
77
39
  npm pack --dry-run
78
- npm publish --access public
79
- ```
80
-
81
- Publishing runs the tests again. Complete npm's authentication/2FA prompt when requested. Then verify the registry install from any other directory:
82
-
83
- ```bash
84
- npx --yes @gondoai/cli@0.1.0 --version
85
- npx --yes @gondoai/cli@0.1.0 --env-file /absolute/path/to/gondo.env guide
86
40
  ```
87
41
 
88
- `0.1.0` is prepared for its first publication; it is not published by creating or pushing this repository. The npm package is `@gondoai/cli`; its installed executable is `gondo`. Future releases need a new version number. The package is public with no open-source license grant (`UNLICENSED`).
89
-
90
- ## Connection setup and API-key scopes
91
-
92
- All keys have `operator` access. In Account → API keys, admins can additionally select **Manage integrations** (`integrations:manage`) when creating a key. Existing keys do not gain this permission automatically; create a replacement and revoke the old key to change permissions. Ordinary keys can already use connected APIs, including writes; this additional scope controls connection setup, not provider API permissions. Keys cannot grant scopes or approve human reviews.
93
-
94
- ```sh
95
- gondo integrations providers
96
- gondo integrations provider clio
97
- gondo integrations create --provider clio --name "Demo Clio"
98
- gondo integrations credentials <id> --file ./private-credentials.json --variant <variant>
99
- gondo integrations test <id>
100
- gondo integrations enable <id>
101
- ```
102
-
103
- Only prebuilt providers with supplied credentials are supported. Credential JSON/YAML must match the displayed schema; `--file -` reads stdin. Do not pass secret values as arguments. Connections start disabled, and enablement is explicit. Replacement credentials are validated separately before an atomic switch; failed checks leave live credentials untouched. Concurrent replacements return a conflict rather than overwriting each other. `checked: false` means no provider check was available. Use `integrations list|get <id>`, `disable <id>`, or `update <id> --file settings.json` (fields: `label`, `maxScope`, `enabledToolsets`, `disabledTools`). Provider metadata lists valid tool names; `maxScope` accepts `read`, `write`, `admin`. These settings govern named tools, not direct API code.
104
-
105
- ## Webhooks and workflow documents
106
-
107
- ```sh
108
- gondo workflows webhook get <workflow-id>
109
- gondo workflows webhook configure <workflow-id> --output ./private-webhook.json
110
- gondo attempts files list <attempt-id>
111
- gondo attempts files download <attempt-id> <artifact-id> --output-dir ./outputs
112
- ```
42
+ Tests include an installed packed artifact, authenticated HTTP transport, server capability-version checks, private credential files and binary downloads.
113
43
 
114
- Webhook configuration requires a new private output file and never prints its secret. Repeating `configure` preserves existing credentials; explicitly use `rotate-secret` with another output file to replace a lost secret. Trigger the returned URL with curl using its `headerName` and `secret`, then inspect runs. No mutations are automatically retried.
44
+ **Deploy the paired app release exposing capability protocol version 2 before publishing CLI 0.2.0.** This release requires `/operator/tools` and the shared `/operator/guide`; discovery on an older server fails clearly without falling back to stale instructions. This is the initial supported CLI contract; older CLI releases are not supported.
115
45
 
116
- Workflow downloads target an attempt, not a CLI workspace session. Downloads refuse unsafe filenames or overwrites, remove partial files, verify available size/checksum metadata and cap files without size metadata at 512 MiB. Storage requests never carry the Gondo API key.
46
+ The [shared implementation brief](https://github.com/commandable/commandable-app-v1/blob/main/docs/unified-authoring.md) documents the paired app change and verification.
117
47
 
118
- Deploy the backend API-key scope migration and matching runtime before releasing this CLI. No SharePoint file-transfer changes are part of this release.
48
+ After that deployment, an authorized npm maintainer can publish with `npm publish --access public`. Creating or merging these PRs does not itself publish the npm package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gondoai/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Let coding agents build, test, and manage employees and jobs on Gondo.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/cli.mjs CHANGED
@@ -6,7 +6,7 @@ import { Readable, Transform } from 'node:stream'
6
6
  import { pipeline } from 'node:stream/promises'
7
7
  import { parseArgs, parseEnv } from 'node:util'
8
8
  import packageJson from '../package.json' with { type: 'json' }
9
- import { parseDocument } from 'yaml'
9
+ import { parseDocument, stringify } from 'yaml'
10
10
 
11
11
  export const HELP = `Gondo account operator CLI
12
12
 
@@ -16,11 +16,14 @@ Load a local env file with: gondo --env-file ./gondo.env guide
16
16
  Use gondo --version to show the installed CLI version.
17
17
  Account admins create keys in Account Settings. Never put a key in source code.
18
18
 
19
- gondo guide [--topic overview|workflows|nodes]
19
+ gondo guide [--topic overview|workflows]
20
+ gondo tools list|describe <existing-admin-tool-name>
21
+ gondo call <existing-admin-tool-name> --file arguments.json [--session <id>] [--output-dir <dir>]
22
+ Tool names, descriptions and argument schemas come from the server. Host and interactive tools return required actions.
20
23
  gondo list [namespace-path] [--limit 25]
21
24
  gondo read <namespace-path>
22
25
  gondo employees list|get <id>|create --file employee.yaml|update <id> --file changes.json|delete <id>
23
- gondo workflows list|get <id>|create --employee <id> --name <name>
26
+ gondo workflows list|get <id> [--source active|draft|published] [--format json|yaml] [--editor-state]|create --employee <id> --name <name>
24
27
  gondo workflows save-draft <id> --file workflow.yaml
25
28
  gondo workflows validate <id> [--file workflow.yaml] [--source draft|published]
26
29
  gondo workflows export <id> [--source active|draft|published] [--output workflow.yaml]
@@ -29,7 +32,7 @@ gondo workflows rename <id> --name <name>
29
32
  gondo runs list|get|attempts|events|definition|cancel <run-id>
30
33
  gondo attempts get|events|definition|cancel <attempt-id>
31
34
  gondo runs test|start <workflow-id> [--input input.json]
32
- gondo exec --file investigate.js --integrations ref_one,ref_two [--session <id>] [--attach <path> ...] [--output-dir <dir>]
35
+ gondo exec --file investigate.js --integrations ref_one,ref_two [--channels ref_one,ref_two] [--session <id>] [--attach <path> ...] [--output-dir <dir>]
33
36
  gondo sessions create|list|get <id>|close <id>
34
37
  gondo files list <session-id>|upload <session-id> --file <path>
35
38
  gondo files download <file-id> --session <id> --output-dir <dir>
@@ -45,7 +48,7 @@ gondo attempts files list <attempt-id>
45
48
  gondo attempts files download <attempt-id> <artifact-id> --output-dir <dir>
46
49
 
47
50
  Keys have operator access; integration changes also require integrations:manage.
48
- New integrations are disabled. Save credentials, inspect the check result, then enable explicitly.
51
+ New integrations follow Admin defaults (enabled). Saving credentials never re-enables an existing connection.
49
52
  Webhook configure/rotate-secret require --output; secrets never appear on stdout. Configure never rotates an existing secret.
50
53
 
51
54
  Requires Pro, including active Pro trials. Responses are JSON. Exit 0: success; 1: failure; 2: user action required.
@@ -72,6 +75,9 @@ const options = {
72
75
  'name': { type: 'string' },
73
76
  'integrations': { type: 'string' },
74
77
  'source': { type: 'string' },
78
+ 'format': { type: 'string' },
79
+ 'editor-state': { type: 'boolean' },
80
+ 'channels': { type: 'string' },
75
81
  'topic': { type: 'string' },
76
82
  'limit': { type: 'string' },
77
83
  'help': { type: 'boolean', short: 'h' },
@@ -93,6 +99,15 @@ function segment(value, label = 'ID') {
93
99
  return encodeURIComponent(result)
94
100
  }
95
101
 
102
+ function positiveInteger(value, label) {
103
+ const text = value.trim()
104
+ const number = Number(text)
105
+ if (!/^\d+$/.test(text) || !Number.isSafeInteger(number) || number <= 0)
106
+ throw new Error(`${label} must be a positive safe integer`)
107
+
108
+ return number
109
+ }
110
+
96
111
  async function readStructured(file, stdin = process.stdin) {
97
112
  required(file, '--file or --input')
98
113
  let source
@@ -140,18 +155,37 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
140
155
 
141
156
  const request = { method: 'GET', path: '', query: {}, body: undefined, output: values.output, ...(values['env-file'] ? { envFile: values['env-file'] } : {}) }
142
157
 
158
+ // Convenience commands are argument adapters to the shared Admin operations.
159
+ const call = (name, args, presentation) => {
160
+ request.path = `/operator/tools/${name}/call`
161
+ request.method = 'POST'
162
+ request.query = {}
163
+ request.body = { arguments: args, ...(values.session ? { sessionId: values.session } : {}), ...(presentation ? { presentation } : {}) }
164
+ request.requiresCapabilities = 2
165
+ }
166
+
143
167
  if (command === 'guide') {
144
168
  request.path = '/operator/guide'
145
169
 
146
170
  request.query.topic = values.topic ?? 'overview'
171
+ request.requiresCapabilities = 2
172
+ }
173
+ else if (command === 'tools') {
174
+ if (action === 'list')
175
+ request.path = '/operator/tools'
176
+ else if (action === 'describe')
177
+ request.path = `/operator/tools/${segment(id, 'tool name')}`
178
+ request.requiresCapabilities = 2
179
+ }
180
+ else if (command === 'call') {
181
+ call(segment(action, 'tool name'), await readStructured(values.file, stdin))
182
+ request.outputDir = values['output-dir']
147
183
  }
148
184
  else if (command === 'list' || command === 'read') {
149
- request.path = '/operator/resources'
150
-
151
- request.query = { action: command, path: command === 'read' ? required(action, 'namespace path') : action ?? '/' }
152
-
153
- if (values.limit)
154
- request.query.limit = values.limit
185
+ call(command === 'list' ? 'gondo_list' : 'gondo_read', {
186
+ path: command === 'read' ? required(action, 'namespace path') : action ?? '/',
187
+ ...(command === 'list' && values.limit !== undefined ? { limit: positiveInteger(values.limit, '--limit') } : {}),
188
+ })
155
189
  }
156
190
  else if (command === 'exec') {
157
191
  request.path = '/code/execute'
@@ -160,6 +194,7 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
160
194
 
161
195
  request.body = {
162
196
  code: await readFile(required(values.file, '--file'), 'utf8'),
197
+ channels: values.channels?.split(',').map(ref => ref.trim()).filter(Boolean) ?? [],
163
198
  integrations: values.integrations?.split(',').map(ref => ref.trim()).filter(Boolean) ?? [],
164
199
  ...(values.session ? { sessionId: values.session } : {}),
165
200
  }
@@ -205,23 +240,19 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
205
240
  request.body = { integration: required(values.integration, '--integration') }
206
241
  }
207
242
  else if (command === 'employees') {
208
- if (action === 'list') {
243
+ if (action === 'list')
209
244
  request.path = '/employees'
210
- }
211
- else if (action === 'create') {
212
- request.path = '/employees'
213
-
214
- request.method = 'POST'
215
-
216
- request.body = await readStructured(values.file)
217
- }
218
- else if (['get', 'update', 'delete'].includes(action)) {
245
+ else if (action === 'get')
219
246
  request.path = `/employees/${segment(id)}`
220
-
221
- request.method = { get: 'GET', update: 'PATCH', delete: 'DELETE' }[action]
222
-
223
- if (action === 'update')
224
- request.body = await readStructured(values.file)
247
+ else if (['create', 'update', 'delete'].includes(action)) {
248
+ const body = action === 'delete' ? {} : await readStructured(values.file)
249
+ const { allowedIntegrationRefs, allowedChannelRefs, ...rest } = body
250
+ call(`${action}_employee`, {
251
+ ...rest,
252
+ ...(action !== 'create' ? { employee_id: required(id, 'employee ID') } : {}),
253
+ ...(allowedIntegrationRefs !== undefined ? { allowed_integration_refs: allowedIntegrationRefs ?? [] } : {}),
254
+ ...(allowedChannelRefs !== undefined ? { allowed_channel_refs: allowedChannelRefs ?? [] } : {}),
255
+ })
225
256
  }
226
257
  }
227
258
  else if (command === 'integrations') {
@@ -233,10 +264,13 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
233
264
  else if (action === 'list')
234
265
  request.path = base
235
266
  else if (action === 'create') {
236
- request.path = base
237
- request.method = 'POST'
238
- request.body = { providerKey: required(values.provider, '--provider'), label: required(values.name, '--name'), ...(values.reference ? { referenceId: values.reference } : {}) }
267
+ call('gondo_add_prebuilt_api_integration', {
268
+ type: required(values.provider, '--provider'), label: required(values.name, '--name'),
269
+ ...(values.reference ? { reference_id: values.reference } : {}),
270
+ })
239
271
  }
272
+ else if (action === 'enable' || action === 'disable')
273
+ call('gondo_set_integration_enabled', { integration_id: required(id, 'integration ID'), enabled: action === 'enable' })
240
274
  else if (action === 'get')
241
275
  request.path = `${base}/${segment(id)}`
242
276
  else if (action === 'update') {
@@ -244,7 +278,7 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
244
278
  request.method = 'PATCH'
245
279
  request.body = await readStructured(values.file)
246
280
  }
247
- else if (['credentials', 'test', 'enable', 'disable'].includes(action)) {
281
+ else if (['credentials', 'test'].includes(action)) {
248
282
  request.path = `${base}/${segment(id)}/${action}`
249
283
  request.method = 'POST'
250
284
  if (action === 'credentials') {
@@ -280,56 +314,45 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
280
314
  }
281
315
  }
282
316
  else if (command === 'workflows') {
283
- if (action === 'list') {
284
- request.path = '/workflows'
285
- }
286
- else if (action === 'create') {
317
+ if (action === 'list')
287
318
  request.path = '/workflows'
288
-
289
- request.method = 'POST'
290
-
291
- request.body = { employeeId: required(values.employee, '--employee'), name: required(values.name, '--name') }
292
- }
319
+ else if (action === 'create')
320
+ call('create_new_workflow', { employee_id: required(values.employee, '--employee'), suggested_name: required(values.name, '--name') })
293
321
  else {
294
- const base = `/workflows/${segment(id)}`
295
-
296
- const routes = {
297
- 'get': ['GET', '/editor'],
298
- 'save-draft': ['PUT', '/draft'],
299
- 'discard-draft': ['DELETE', '/draft'],
300
- 'validate': ['POST', '/validate'],
301
- 'export': ['POST', '/yaml'],
302
- 'publish': ['POST', '/publish'],
303
- 'enable': ['PATCH', ''],
304
- 'disable': ['PATCH', ''],
305
- 'rename': ['PATCH', ''],
306
- 'delete': ['DELETE', ''],
322
+ const workflowId = required(id, 'workflow ID')
323
+ const base = `/workflows/${segment(workflowId)}`
324
+ if (action === 'get')
325
+ call('gondo_read', { path: base }, { source: values.source ?? 'active', format: values.format ?? 'json', editorState: values['editor-state'] ?? false })
326
+ else if (action === 'save-draft') {
327
+ const body = await definitionBody(values.file)
328
+ call('gondo_put_workflow', { op: 'replace', workflow_id: workflowId, definition_yaml: body.definitionYaml ?? stringify(body.definition) })
307
329
  }
308
-
309
- const route = routes[action]
310
-
311
- if (route) {
312
- request.method = route[0]
313
-
314
- request.path = `${base}${route[1]}`
315
-
316
- if (action === 'save-draft')
317
- request.body = await definitionBody(values.file)
318
-
319
- if (action === 'validate')
320
- request.body = { source: values.source ?? 'draft', ...(values.file ? await definitionBody(values.file) : {}) }
321
-
322
- if (action === 'export')
323
- request.body = { source: values.source ?? 'active' }
324
-
325
- if (action === 'publish')
326
- request.body = {}
327
-
328
- if (action === 'enable' || action === 'disable')
329
- request.body = { enabled: action === 'enable' }
330
-
331
- if (action === 'rename')
332
- request.body = { name: required(values.name, '--name') }
330
+ else if (action === 'publish')
331
+ call('gondo_publish_workflow', { workflow_id: workflowId })
332
+ else if (action === 'rename')
333
+ call('gondo_update_workflow_settings', { workflow_id: workflowId, name: required(values.name, '--name') })
334
+ else if (action === 'validate' && !values.file && !values.source)
335
+ call('gondo_validate_workflow', { workflow_id: workflowId })
336
+ else {
337
+ const routes = {
338
+ 'discard-draft': ['DELETE', '/draft'],
339
+ 'validate': ['POST', '/validate'],
340
+ 'export': ['POST', '/yaml'],
341
+ 'enable': ['PATCH', ''],
342
+ 'disable': ['PATCH', ''],
343
+ 'delete': ['DELETE', ''],
344
+ }
345
+ const route = routes[action]
346
+ if (route) {
347
+ request.method = route[0]
348
+ request.path = `${base}${route[1]}`
349
+ if (action === 'validate')
350
+ request.body = { source: values.source ?? 'draft', ...(values.file ? await definitionBody(values.file) : {}) }
351
+ if (action === 'export')
352
+ request.body = { source: values.source ?? 'active' }
353
+ if (action === 'enable' || action === 'disable')
354
+ request.body = { enabled: action === 'enable' }
355
+ }
333
356
  }
334
357
  }
335
358
  }
@@ -337,8 +360,8 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
337
360
  if (action === 'list') {
338
361
  request.path = '/runs'
339
362
 
340
- if (values.limit)
341
- request.query.limit = values.limit
363
+ if (values.limit !== undefined)
364
+ request.query.limit = positiveInteger(values.limit, '--limit')
342
365
  }
343
366
  else if (action === 'test' || action === 'start') {
344
367
  request.path = '/runs'
@@ -371,7 +394,7 @@ export async function buildRequest(argv, { stdin = process.stdin } = {}) {
371
394
  if (!request.path)
372
395
  throw new Error('Unknown command. Use --help for supported commands.')
373
396
 
374
- const maxPositionals = command === 'integrations' && ['providers', 'list', 'create'].includes(action) ? 2 : command === 'workflows' && action === 'webhook' ? 4 : command === 'attempts' && action === 'files' ? (id === 'download' ? 5 : 4) : ['guide', 'exec'].includes(command) ? 1 : ['list', 'read'].includes(command) ? 2 : 3
397
+ const maxPositionals = command === 'call' ? 2 : command === 'tools' && action === 'list' ? 2 : command === 'integrations' && ['providers', 'list', 'create'].includes(action) ? 2 : command === 'workflows' && action === 'webhook' ? 4 : command === 'attempts' && action === 'files' ? (id === 'download' ? 5 : 4) : ['guide', 'exec'].includes(command) ? 1 : ['list', 'read'].includes(command) ? 2 : 3
375
398
 
376
399
  if (positionals.length > maxPositionals)
377
400
  throw new Error('Unexpected positional argument. Use --help for command syntax.')
@@ -426,6 +449,7 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
426
449
  let request
427
450
  let secretHandle
428
451
  let secretWritten = false
452
+ let toolCallStarted = false
429
453
  try {
430
454
  request = await buildRequest(argv, { stdin })
431
455
 
@@ -480,12 +504,14 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
480
504
  let response
481
505
 
482
506
  try {
507
+ if (next.path.startsWith('/operator/tools/') && next.path.endsWith('/call'))
508
+ toolCallStarted = true
483
509
  response = await fetchImpl(url, {
484
510
  method: next.method,
485
511
  headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json', ...uploadHeaders },
486
512
  ...(uploadStream ? { body: uploadStream, duplex: 'half' } : next.body !== undefined ? { body: JSON.stringify(next.body) } : {}),
487
513
  redirect: 'error',
488
- signal: AbortSignal.timeout(120000),
514
+ signal: AbortSignal.timeout(next.path === '/code/execute' || next.path?.endsWith('/call') ? 420000 : 120000),
489
515
  })
490
516
  }
491
517
  finally {
@@ -495,6 +521,9 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
495
521
  if (next.binary && response.ok)
496
522
  return { response }
497
523
 
524
+ if (next.requiresCapabilities && [404, 405].includes(response.status))
525
+ throw new Error('This Gondo server does not support CLI 0.2.0 shared capabilities. Deploy the matching app release before using this command.')
526
+
498
527
  const text = await response.text()
499
528
  let result
500
529
 
@@ -505,6 +534,8 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
505
534
  throw new Error(`Runtime returned non-JSON (HTTP ${response.status}). Check GONDO_API_URL points to the runtime.`)
506
535
  }
507
536
 
537
+ if (next.requiresCapabilities && response.ok && result?.version !== next.requiresCapabilities)
538
+ throw new Error('This Gondo server has an incompatible capability version. Deploy the app release supporting CLI 0.2.0.')
508
539
  return { response, result }
509
540
  }
510
541
 
@@ -621,7 +652,7 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
621
652
  }
622
653
 
623
654
  let received = await send(request)
624
- if (request.path === '/code/execute')
655
+ if (request.path === '/code/execute' || received.result?.executionId)
625
656
  recovery = { sessionId: received.result?.sessionId, executionId: received.result?.executionId }
626
657
 
627
658
  if (request.outputDir && received.result?.files?.length) {
@@ -717,7 +748,7 @@ export async function runCli(argv, { env = process.env, fetchImpl = fetch, stdou
717
748
  if (request?.secretOutput && secretHandle && !secretWritten)
718
749
  message += ' Webhook state may have changed; inspect it before explicitly rotating again. No automatic retry was performed.'
719
750
 
720
- stderr.write(`${JSON.stringify({ error: message, ...(request?.path === '/code/execute' ? { sessionId: request.body?.sessionId, ...recovery, recovery: 'Inspect the session and execution before continuing; do not repeat this execution automatically.' } : {}) })}\n`)
751
+ stderr.write(`${JSON.stringify({ error: message, ...(toolCallStarted || request?.path === '/code/execute' || request?.body?.sessionId || recovery ? { sessionId: request.body?.sessionId, ...recovery, recovery: toolCallStarted ? 'The tool call may have changed state. Inspect the affected resources and any session or execution before continuing; do not repeat this call automatically.' : 'Inspect the session and execution before continuing; do not repeat this execution automatically.' } : {}) })}\n`)
721
752
 
722
753
  return 1
723
754
  }