@kora-platform/cli 0.7.0-rc1 → 0.8.0-rc1

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 (38) hide show
  1. package/README.md +21 -0
  2. package/dist/api-client.d.ts +250 -93
  3. package/dist/api-client.js +187 -163
  4. package/dist/api-types.d.ts +280 -162
  5. package/dist/artifact-api-client.d.ts +28 -1
  6. package/dist/artifact-api-client.js +33 -0
  7. package/dist/artifact-commands.d.ts +5 -0
  8. package/dist/artifact-commands.js +172 -1
  9. package/dist/audit-commands.d.ts +12 -0
  10. package/dist/audit-commands.js +74 -0
  11. package/dist/auth-commands.d.ts +1 -0
  12. package/dist/auth-commands.js +116 -29
  13. package/dist/command-builders.d.ts +1 -0
  14. package/dist/command-builders.js +1 -0
  15. package/dist/command-groups.js +10 -12
  16. package/dist/command-registry.js +548 -243
  17. package/dist/commands.js +652 -602
  18. package/dist/environment-context.d.ts +9 -0
  19. package/dist/environment-context.js +32 -0
  20. package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
  21. package/dist/extension-commands.js +446 -0
  22. package/dist/files.d.ts +33 -0
  23. package/dist/files.js +247 -12
  24. package/dist/format.d.ts +5 -0
  25. package/dist/format.js +78 -1
  26. package/dist/runner.js +14 -5
  27. package/dist/schema-registry-data.d.ts +272 -569
  28. package/dist/schema-registry-data.js +307 -700
  29. package/dist/session.d.ts +1 -0
  30. package/dist/transport.d.ts +10 -0
  31. package/dist/transport.js +22 -0
  32. package/dist/types.d.ts +2 -1
  33. package/dist/workspace-source.d.ts +1 -0
  34. package/dist/workspace-source.js +13 -0
  35. package/package.json +2 -1
  36. package/dist/integration-api-client.d.ts +0 -29
  37. package/dist/integration-api-client.js +0 -50
  38. package/dist/integration-commands.js +0 -208
package/dist/commands.js CHANGED
@@ -1,19 +1,17 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
1
  import { renderCompletionScript, resolveCompletionCandidates } from "./completion.js";
4
- import { executeAuthLogin, executeAuthLogout, executeAuthWhoami } from "./auth-commands.js";
2
+ import { executeAuthLogin, executeAuthLogout, executeAuthSignup, executeAuthWhoami } from "./auth-commands.js";
5
3
  import { getCliSchema, listCliSchemas } from "./schema-registry.js";
6
4
  import { createPlatformApiClient } from "./api-client.js";
7
5
  import { authProblem, genericProblem, notFoundProblem, usageProblem } from "./cli-errors.js";
8
6
  import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag } from "./command-flags.js";
9
- import { executeArtifactDownload, executeArtifactList, executeArtifactRead, executeArtifactUpload } from "./artifact-commands.js";
10
- import { readImportEntries, readJsonInputSpecifier, readTextInputSpecifier, parseEnvFile } from "./files.js";
7
+ import { executeArtifactDownload, executeArtifactArchive, executeArtifactInspect, executeArtifactInventory, executeArtifactList, executeArtifactPurge, executeArtifactRead, executeArtifactRestore, executeArtifactUpload } from "./artifact-commands.js";
8
+ import { executeAudit } from "./audit-commands.js";
9
+ import { isZipArchivePath, readArchiveBytes, readImportEntries, readJsonInputSpecifier, readWorkspaceTestEntries, readTextInputSpecifier, parseEnvFile, writeReleaseSourceFiles } from "./files.js";
11
10
  import { renderDiffSummary, renderKeyValue, renderPrettyJson, renderSuccess, renderTable } from "./format.js";
12
11
  import { buildTaskDetailPresentation } from "./task-detail.js";
13
12
  import { confirmDestructive } from "./interaction.js";
14
- import { executeIntegrations } from "./integration-commands.js";
15
- import { openBrowser, shouldAutoOpenBrowser } from "./browser.js";
16
- import { isInteractive } from "./prompts.js";
13
+ import { executeExtensions } from "./extension-commands.js";
14
+ import { readCliEnvironmentFlag, readRequiredCliEnvironmentFlag, resolveCliEnvironment } from "./environment-context.js";
17
15
  export async function executeParsedCommand(parsed, context) {
18
16
  const api = createPlatformApiClient({
19
17
  sessionStore: context.sessionStore
@@ -21,6 +19,8 @@ export async function executeParsedCommand(parsed, context) {
21
19
  switch (parsed.definition.id) {
22
20
  case "auth.login":
23
21
  return executeAuthLogin(parsed, context, api);
22
+ case "auth.signup":
23
+ return executeAuthSignup(parsed, context, api);
24
24
  case "auth.logout":
25
25
  return executeAuthLogout(context, api);
26
26
  case "auth.whoami":
@@ -41,10 +41,6 @@ export async function executeParsedCommand(parsed, context) {
41
41
  return executeOrgSelect(parsed, context, api);
42
42
  case "org.settings.get":
43
43
  return executeOrgSettingsGet(parsed, context, api);
44
- case "org.import":
45
- return executeOrgImport(parsed, context, api);
46
- case "org.bundle.show":
47
- return executeOrgBundleShow(parsed, context, api);
48
44
  case "org.reset":
49
45
  return executeOrgReset(parsed, context, api);
50
46
  case "status":
@@ -65,24 +61,29 @@ export async function executeParsedCommand(parsed, context) {
65
61
  return executeWorkflowContext(parsed, context, api);
66
62
  case "workflow.start":
67
63
  return executeWorkflowStart(parsed, context, api);
64
+ case "test.node":
65
+ return executeWorkflowNodeTest(parsed, context, api);
68
66
  case "release.list":
69
67
  return executeReleaseList(parsed, context, api);
70
68
  case "release.get":
71
69
  return executeReleaseGet(parsed, context, api);
72
- case "release.changes":
73
- return executeReleaseChanges(parsed, context, api);
74
- case "release.deployment.get":
75
- return executeReleaseDeploymentGet(parsed, context, api);
76
- case "release.publish":
77
- return executeReleasePublish(parsed, context, api);
78
- case "release.retry":
79
- return executeReleaseRetry(parsed, context, api);
80
- case "release.discard":
81
- return executeReleaseDiscard(parsed, context, api);
82
- case "release.restore":
83
- return executeReleaseRestore(parsed, context, api);
84
- case "release.deactivate":
85
- return executeReleaseDeactivate(parsed, context, api);
70
+ case "release.create":
71
+ return executeReleaseCreate(parsed, context, api);
72
+ case "release.validate":
73
+ return executeReleaseValidate(parsed, context, api);
74
+ case "release.source":
75
+ return executeReleaseSource(parsed, context, api);
76
+ case "environment.list":
77
+ case "environment.get":
78
+ case "environment.create":
79
+ case "environment.rename":
80
+ case "environment.archive":
81
+ case "environment.deploy":
82
+ case "environment.undeploy":
83
+ return executeEnvironment(parsed, context, api);
84
+ case "deployment.list":
85
+ case "deployment.get":
86
+ return executeDeployment(parsed, context, api);
86
87
  case "run.list":
87
88
  return executeRunList(parsed, context, api);
88
89
  case "run.get":
@@ -93,14 +94,27 @@ export async function executeParsedCommand(parsed, context) {
93
94
  return executeRunSteps(parsed, context, api);
94
95
  case "run.abort":
95
96
  return executeRunAbort(parsed, context, api);
97
+ case "audit.list":
98
+ case "audit.get":
99
+ return executeAudit(parsed, context, api, resolveOrgScope);
96
100
  case "artifact.upload":
97
101
  return executeArtifactUpload(parsed, context, api, resolveOrgScope);
98
102
  case "artifact.list":
99
103
  return executeArtifactList(parsed, context, api, resolveOrgScope);
104
+ case "artifact.inventory":
105
+ return executeArtifactInventory(parsed, context, api, resolveOrgScope);
106
+ case "artifact.inspect":
107
+ return executeArtifactInspect(parsed, context, api, resolveOrgScope);
100
108
  case "artifact.download":
101
109
  return executeArtifactDownload(parsed, context, api, resolveOrgScope);
102
110
  case "artifact.read":
103
111
  return executeArtifactRead(parsed, context, api, resolveOrgScope);
112
+ case "artifact.archive":
113
+ return executeArtifactArchive(parsed, context, api, resolveOrgScope);
114
+ case "artifact.restore":
115
+ return executeArtifactRestore(parsed, context, api, resolveOrgScope);
116
+ case "artifact.purge":
117
+ return executeArtifactPurge(parsed, context, api, resolveOrgScope);
104
118
  case "task.list":
105
119
  return executeTaskList(parsed, context, api);
106
120
  case "task.get":
@@ -119,10 +133,6 @@ export async function executeParsedCommand(parsed, context) {
119
133
  case "org-model.capabilities.get":
120
134
  case "org-model.operations.list":
121
135
  case "org-model.operations.get":
122
- case "org-model.connectors.list":
123
- case "org-model.connectors.get":
124
- case "org-model.mcp-servers.list":
125
- case "org-model.mcp-servers.get":
126
136
  return executeOrgModel(parsed, context, api);
127
137
  case "access.members.list":
128
138
  case "access.members.get":
@@ -137,32 +147,21 @@ export async function executeParsedCommand(parsed, context) {
137
147
  case "access.api-keys.create":
138
148
  case "access.api-keys.revoke":
139
149
  return executeAccess(parsed, context, api);
140
- case "integrations.list":
141
- case "integrations.diagnostics":
142
- case "integrations.get":
143
- case "integrations.connections":
144
- case "integrations.actions":
145
- case "integrations.action":
146
- return executeIntegrations(parsed, context, api, resolveOrgScope);
147
- case "mcp.list":
148
- case "mcp.get":
149
- case "mcp.create":
150
- case "mcp.update":
151
- case "mcp.delete":
152
- case "mcp.test":
153
- case "mcp.discover-tools":
154
- case "mcp.connect":
155
- case "mcp.oauth-status":
156
- case "mcp.disconnect":
157
- return executeMcp(parsed, context, api);
158
- case "skills.list":
159
- case "skills.get":
160
- case "skills.create":
161
- case "skills.upload":
162
- case "skills.download":
163
- case "skills.update":
164
- case "skills.delete":
165
- return executeSkills(parsed, context, api);
150
+ case "extensions.export":
151
+ case "extensions.validate":
152
+ case "extensions.publish":
153
+ case "extensions.install":
154
+ case "extensions.grant":
155
+ case "extensions.update":
156
+ case "extensions.built-ins.list":
157
+ case "extensions.built-ins.install":
158
+ case "extensions.list":
159
+ case "extensions.inspect":
160
+ case "extensions.detail":
161
+ case "extensions.disable":
162
+ case "extensions.enable":
163
+ case "extensions.delete":
164
+ return executeExtensions(parsed, context, api, resolveOrgScope);
166
165
  case "env.list":
167
166
  case "env.get":
168
167
  case "env.replace":
@@ -176,12 +175,6 @@ export async function executeParsedCommand(parsed, context) {
176
175
  case "chat.sessions.list":
177
176
  case "chat.sessions.get":
178
177
  case "chat.sessions.delete":
179
- case "chat.changes":
180
- case "chat.validation":
181
- case "chat.draft.get":
182
- case "chat.draft.apply":
183
- case "chat.draft.refresh":
184
- case "chat.workspace-state":
185
178
  return executeChat(parsed, context, api);
186
179
  case "completion.bash":
187
180
  case "completion.zsh":
@@ -191,6 +184,10 @@ export async function executeParsedCommand(parsed, context) {
191
184
  return executeSchemaList();
192
185
  case "schema.get":
193
186
  return executeSchemaGet(parsed);
187
+ case "admin.usage.summary":
188
+ case "admin.usage.workflows":
189
+ case "admin.usage.tools":
190
+ case "admin.usage.events":
194
191
  case "admin.org.deleted.list":
195
192
  case "admin.org.deleted.get":
196
193
  case "admin.org.deleted.restore":
@@ -360,35 +357,13 @@ async function executeOrgSettingsGet(parsed, context, api) {
360
357
  meta: { command: "org settings get", orgId: org.id }
361
358
  };
362
359
  }
363
- async function executeOrgImport(parsed, context, api) {
364
- const { org, session } = await resolveOrgScope(parsed, context, api);
365
- const files = await readImportEntries(readRequiredArg(parsed, "path"));
366
- const data = await api.importProject(session, org.id, { files });
367
- return {
368
- data,
369
- human: renderSuccess(`Imported ${files.length} files into ${org.slug}.`),
370
- kind: "authoring_import_apply",
371
- meta: { command: "org import", orgId: org.id }
372
- };
373
- }
374
- async function executeOrgBundleShow(parsed, context, api) {
375
- const { org, session } = await resolveOrgScope(parsed, context, api);
376
- const releaseId = readOptionalStringFlag(parsed, "release");
377
- const data = await api.getProjectWorkspace(session, org.id, releaseId);
378
- return {
379
- data,
380
- human: renderPrettyJson(data.project),
381
- kind: "authoring_bundle_show",
382
- meta: { command: "org bundle show", orgId: org.id }
383
- };
384
- }
385
360
  async function executeOrgReset(parsed, context, api) {
386
361
  const { org, session } = await resolveOrgScope(parsed, context, api);
387
- await confirmDestructive(parsed, context, `Reset the draft project for ${org.slug}?`);
362
+ await confirmDestructive(parsed, context, `Reset release, runtime, project, and chat state for ${org.slug}?`);
388
363
  await api.resetOrganizationProject(session, org.id);
389
364
  return {
390
365
  data: { reset: true },
391
- human: renderSuccess(`Reset the draft project for ${org.slug}.`),
366
+ human: renderSuccess(`Reset project state for ${org.slug}.`),
392
367
  kind: "authoring_project_reset",
393
368
  meta: { command: "org reset", orgId: org.id }
394
369
  };
@@ -399,8 +374,6 @@ async function executeStatus(parsed, context, api) {
399
374
  return {
400
375
  data,
401
376
  human: renderKeyValue([
402
- { label: "Draft changes", value: data.overview.draftChangeCount },
403
- { label: "Draft has changes", value: data.overview.draftHasChanges },
404
377
  { label: "Pending tasks", value: data.overview.pendingTaskCount },
405
378
  { label: "Runs in flight", value: data.overview.runsInFlight },
406
379
  { label: "Workflow count", value: data.overview.workflowCount }
@@ -411,12 +384,14 @@ async function executeStatus(parsed, context, api) {
411
384
  }
412
385
  async function executeActivityList(parsed, context, api) {
413
386
  const { org, session } = await resolveOrgScope(parsed, context, api);
387
+ const { environment } = resolveCliEnvironment(parsed, session);
414
388
  const focusType = readOptionalStringFlag(parsed, "focus-type");
415
389
  const focusValue = readOptionalStringFlag(parsed, "focus-value");
416
390
  if ((focusType && !focusValue) || (!focusType && focusValue)) {
417
391
  throw usageProblem("Use --focus-type and --focus-value together.", "activity list");
418
392
  }
419
393
  const data = await api.getActivity(session, org.id, {
394
+ environment,
420
395
  ...(focusType ? { focusType: focusType } : {}),
421
396
  ...(focusValue ? { focusValue } : {}),
422
397
  ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
@@ -429,11 +404,12 @@ async function executeActivityList(parsed, context, api) {
429
404
  { key: "occurredAt", label: "Occurred" },
430
405
  { key: "kind", label: "Kind" },
431
406
  { key: "status", label: "Status" },
407
+ { key: "environmentKey", label: "Environment" },
432
408
  { key: "processName", label: "Workflow" },
433
409
  { key: "summary", label: "Summary" }
434
410
  ]),
435
411
  kind: "runtime_activity_list",
436
- meta: { command: "activity list", orgId: org.id }
412
+ meta: { command: "activity list", environment, orgId: org.id }
437
413
  };
438
414
  }
439
415
  async function executeActivityOptions(parsed, context, api) {
@@ -454,7 +430,7 @@ async function executeWorkflowList(parsed, context, api) {
454
430
  human: renderTable(data.processes, [
455
431
  { key: "name", label: "Name" },
456
432
  { key: "latestVersion", label: "Latest" },
457
- { key: "liveVersion", label: "Live" }
433
+ { key: "deployedVersion", label: "Deployed" }
458
434
  ]),
459
435
  kind: "authoring_workflow_list",
460
436
  meta: { command: "workflow list", orgId: org.id }
@@ -511,7 +487,10 @@ async function executeWorkflowStart(parsed, context, api) {
511
487
  ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "workflow start")
512
488
  : undefined;
513
489
  const workflowName = readRequiredArg(parsed, "name");
490
+ const resolvedEnvironment = resolveCliEnvironment(parsed, session);
491
+ announceResolvedEnvironment(parsed, context, resolvedEnvironment.environment);
514
492
  const data = await api.startWorkflow(session, org.id, workflowName, {
493
+ environment: resolvedEnvironment.environment,
515
494
  ...(inputData ? { inputData } : {}),
516
495
  startEvent: {
517
496
  ...(readOptionalStringFlag(parsed, "start-event-name")
@@ -522,24 +501,69 @@ async function executeWorkflowStart(parsed, context, api) {
522
501
  });
523
502
  return {
524
503
  data,
525
- human: renderSuccess(`Started workflow ${workflowName} as ${data.started.workflowId}.`),
504
+ human: renderSuccess(`Started workflow ${workflowName} in ${data.started.environmentKey ?? resolvedEnvironment.environment} as ${data.started.workflowId}.`),
526
505
  kind: "runtime_workflow_start",
527
- meta: { command: "workflow start", orgId: org.id }
506
+ meta: { command: "workflow start", environment: data.started.environmentKey ?? resolvedEnvironment.environment, orgId: org.id }
528
507
  };
529
508
  }
530
- async function executeReleaseList(parsed, context, api) {
509
+ async function executeWorkflowNodeTest(parsed, context, api) {
531
510
  const { org, session } = await resolveOrgScope(parsed, context, api);
532
- const data = await api.listReleases(session, org.id, {
533
- ...(parsed.flags["include-draft"] === true ? { includeDraft: true } : {}),
534
- ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
511
+ const workflowName = readRequiredArg(parsed, "workflow-name");
512
+ const nodeId = readRequiredArg(parsed, "node-id");
513
+ const workspacePath = readOptionalStringFlag(parsed, "workspace") ?? context.cwd;
514
+ const environment = readOptionalStringFlag(parsed, "environment");
515
+ const inputSpecifier = readOptionalStringFlag(parsed, "input");
516
+ const inputData = inputSpecifier
517
+ ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "test node")
518
+ : undefined;
519
+ const data = await api.testWorkflowNode(session, org.id, workflowName, nodeId, {
520
+ ...(environment ? { environment } : {}),
521
+ files: await readWorkspaceTestEntries(workspacePath),
522
+ ...(inputData ? { input: inputData } : {})
535
523
  });
524
+ const test = data.test;
536
525
  return {
537
526
  data,
538
- human: renderTable(data.releases, [
527
+ exitCode: test.status === "passed" ? 0 : 1,
528
+ human: renderWorkflowNodeTestHuman(test),
529
+ kind: "workflow_node_test",
530
+ meta: withCliSummary({
531
+ command: "test node",
532
+ nodeId,
533
+ orgId: org.id,
534
+ workflowName
535
+ }, summarizeWorkflowNodeTest(test))
536
+ };
537
+ }
538
+ async function executeReleaseList(parsed, context, api) {
539
+ const { org, session } = await resolveOrgScope(parsed, context, api);
540
+ const [data, deploymentData] = await Promise.all([
541
+ api.listReleases(session, org.id, {
542
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
543
+ }),
544
+ api.listEnvironmentDeployments(session, org.id, { limit: 100 })
545
+ ]);
546
+ return {
547
+ data: { ...data, deployments: deploymentData.deployments },
548
+ human: renderTable(data.releases.map((release) => {
549
+ const deploymentsForRelease = deploymentData.deployments.filter((deployment) => deployment.releaseId === release.id);
550
+ const latestDeployment = deploymentsForRelease[0] ?? null;
551
+ return {
552
+ createdAt: release.createdAt,
553
+ id: release.id,
554
+ state: typeof release.state === "string" ? release.state : "",
555
+ latestDeployment: latestDeployment ? `${latestDeployment.environmentKey}:${latestDeployment.status}` : "",
556
+ liveEnvironments: deploymentsForRelease
557
+ .filter((deployment) => deployment.isLive)
558
+ .map((deployment) => deployment.environmentKey)
559
+ .join(", ")
560
+ };
561
+ }), [
539
562
  { key: "id", label: "Release" },
540
- { key: "effectiveState", label: "State" },
541
- { key: "deploymentId", label: "Deployment" },
542
- { key: "publishedAt", label: "Published" }
563
+ { key: "state", label: "Artifact state" },
564
+ { key: "liveEnvironments", label: "Live environments" },
565
+ { key: "latestDeployment", label: "Latest deployment" },
566
+ { key: "createdAt", label: "Created" }
543
567
  ]),
544
568
  kind: "releases_release_list",
545
569
  meta: { command: "release list", orgId: org.id }
@@ -555,88 +579,269 @@ async function executeReleaseGet(parsed, context, api) {
555
579
  meta: { command: "release get", orgId: org.id }
556
580
  };
557
581
  }
558
- async function executeReleaseChanges(parsed, context, api) {
582
+ async function executeReleaseValidate(parsed, context, api) {
559
583
  const { org, session } = await resolveOrgScope(parsed, context, api);
560
- const data = await api.getDraftChanges(session, org.id);
561
- return {
562
- data,
563
- human: renderTable(data.changes, [
564
- { key: "changedAt", label: "Changed" },
565
- { key: "action", label: "Action" },
566
- { key: "entityType", label: "Entity" },
567
- { key: "entityKey", label: "Key" }
568
- ]),
569
- kind: "releases_release_changes",
570
- meta: { command: "release changes", orgId: org.id }
571
- };
572
- }
573
- async function executeReleaseDeploymentGet(parsed, context, api) {
574
- const { org, session } = await resolveOrgScope(parsed, context, api);
575
- const data = await api.getDeployment(session, org.id, readRequiredArg(parsed, "deployment-id"));
584
+ const releaseId = readRequiredArg(parsed, "release-id");
585
+ const environment = readOptionalStringFlag(parsed, "environment");
586
+ const data = await api.validateRelease(session, org.id, {
587
+ ...(environment ? { environment } : {}),
588
+ releaseId
589
+ });
590
+ const validation = data.validation;
591
+ const human = validation.releaseReady
592
+ ? renderSuccess(`Release ${validation.releaseId} passed release validation${environment ? ` for ${environment}` : ""}.`)
593
+ : [
594
+ `Release ${validation.releaseId} did not pass release validation${environment ? ` for ${environment}` : ""}.`,
595
+ renderTable(validation.diagnostics, [
596
+ { key: "gate", label: "Gate" },
597
+ { key: "severity", label: "Severity" },
598
+ { key: "code", label: "Code" },
599
+ { key: "path", label: "Path" },
600
+ { key: "message", label: "Message" }
601
+ ])
602
+ ].join("\n");
576
603
  return {
577
604
  data,
578
- human: renderPrettyJson(data.deployment),
579
- kind: "runtime_deployment_get",
580
- meta: { command: "release deployment get", orgId: org.id }
605
+ ...(validation.releaseReady ? {} : { exitCode: 1 }),
606
+ human,
607
+ kind: "releases_release_validate",
608
+ meta: withCliSummary({ command: "release validate", ...(environment ? { environment } : {}), orgId: org.id }, summarizeReleaseValidation(validation))
581
609
  };
582
610
  }
583
- async function executeReleasePublish(parsed, context, api) {
611
+ async function executeReleaseSource(parsed, context, api) {
584
612
  const { org, session } = await resolveOrgScope(parsed, context, api);
585
- const data = await api.publishRelease(session, org.id, readOptionalStringFlag(parsed, "reason"));
613
+ const releaseId = readRequiredArg(parsed, "release-id");
614
+ const outPath = readRequiredStringFlag(parsed, "out");
615
+ const data = await api.getReleaseSource(session, org.id, releaseId);
616
+ const files = [
617
+ ...data.source.files.map((file) => ({
618
+ content: file.content,
619
+ path: file.path
620
+ })),
621
+ ...data.source.assets.map((asset) => ({
622
+ content: asset.content,
623
+ path: asset.path
624
+ }))
625
+ ];
626
+ await writeReleaseSourceFiles(outPath, {
627
+ files,
628
+ metadata: {
629
+ fileCount: files.length,
630
+ releaseId,
631
+ source: "release"
632
+ }
633
+ });
634
+ const summary = `Release ${releaseId} source written to ${outPath}.`;
586
635
  return {
587
- data,
588
- human: renderSuccess(`Published release ${data.published.release.id}.`),
589
- kind: "releases_release_publish",
590
- meta: { command: "release publish", orgId: org.id }
636
+ data: {
637
+ fileCount: files.length,
638
+ releaseId,
639
+ out: outPath
640
+ },
641
+ human: renderSuccess(summary),
642
+ kind: "releases_release_source",
643
+ meta: withCliSummary({ command: "release source", orgId: org.id, releaseId }, summary)
591
644
  };
592
645
  }
593
- async function executeReleaseRetry(parsed, context, api) {
646
+ async function executeReleaseCreate(parsed, context, api) {
594
647
  const { org, session } = await resolveOrgScope(parsed, context, api);
595
- const data = await api.retryReleasePublication(session, org.id, readRequiredArg(parsed, "release-id"));
648
+ const workspacePath = readRequiredArg(parsed, "workspace");
649
+ const label = readOptionalStringFlag(parsed, "label");
650
+ if (isZipArchivePath(workspacePath)) {
651
+ const data = await api.createReleaseArchive(session, org.id, await readArchiveBytes(workspacePath, "release create"), {
652
+ ...(label ? { label } : {})
653
+ });
654
+ const summary = summarizeReleaseCreation(data.created.release.id);
655
+ return {
656
+ data,
657
+ human: renderSuccess(`${summary} Source: ${workspacePath}.`),
658
+ kind: "releases_release_create",
659
+ meta: withCliSummary({ command: "release create", orgId: org.id, releaseId: data.created.release.id }, summary)
660
+ };
661
+ }
662
+ const files = await readImportEntries(workspacePath);
663
+ const data = await api.createRelease(session, org.id, {
664
+ files,
665
+ ...(label ? { label } : {})
666
+ });
667
+ const summary = summarizeReleaseCreation(data.created.release.id);
596
668
  return {
597
669
  data,
598
- human: renderSuccess(`Retried release ${parsed.args["release-id"]}.`),
599
- kind: "releases_release_retry",
600
- meta: { command: "release retry", orgId: org.id }
670
+ human: renderSuccess(`${summary} Source: ${workspacePath}.`),
671
+ kind: "releases_release_create",
672
+ meta: withCliSummary({ command: "release create", orgId: org.id, releaseId: data.created.release.id }, summary)
601
673
  };
602
674
  }
603
- async function executeReleaseDiscard(parsed, context, api) {
675
+ async function executeEnvironment(parsed, context, api) {
604
676
  const { org, session } = await resolveOrgScope(parsed, context, api);
605
- await confirmDestructive(parsed, context, `Discard the draft release for ${org.slug}?`);
606
- const data = await api.discardDraft(session, org.id);
607
- return {
608
- data,
609
- human: renderSuccess(`Discarded draft release ${data.draft.id}.`),
610
- kind: "releases_release_discard",
611
- meta: { command: "release discard", orgId: org.id }
612
- };
677
+ switch (parsed.definition.id) {
678
+ case "environment.list": {
679
+ const data = await api.listEnvironments(session, org.id);
680
+ return {
681
+ data,
682
+ human: renderTable(data.environments.map((entry) => ({
683
+ currentDeploymentId: entry.environment.currentDeploymentId,
684
+ isProductionTarget: entry.environment.isProductionTarget,
685
+ key: entry.environment.key,
686
+ latestStatus: entry.latestDeployment?.status ?? "",
687
+ liveDeploymentId: entry.liveDeployment?.id ?? "",
688
+ name: entry.environment.name,
689
+ status: entry.environment.status
690
+ })), [
691
+ { key: "key", label: "Environment" },
692
+ { key: "name", label: "Name" },
693
+ { key: "status", label: "Status" },
694
+ { key: "isProductionTarget", label: "Production" },
695
+ { key: "liveDeploymentId", label: "Live deployment" },
696
+ { key: "latestStatus", label: "Latest" }
697
+ ]),
698
+ kind: "environment_list",
699
+ meta: { command: "environment list", orgId: org.id }
700
+ };
701
+ }
702
+ case "environment.get": {
703
+ const data = await api.getEnvironment(session, org.id, readRequiredArg(parsed, "environment"));
704
+ return {
705
+ data,
706
+ human: renderPrettyJson(data.environment),
707
+ kind: "environment_get",
708
+ meta: { command: "environment get", orgId: org.id }
709
+ };
710
+ }
711
+ case "environment.create": {
712
+ const key = readRequiredArg(parsed, "environment");
713
+ const copyFrom = readOptionalStringFlag(parsed, "copy-from");
714
+ const copyFromEnvironmentId = copyFrom
715
+ ? await resolveEnvironmentIdForCli(session, org.id, copyFrom, api)
716
+ : undefined;
717
+ const data = await api.createEnvironment(session, org.id, {
718
+ ...(copyFromEnvironmentId ? { copyFromEnvironmentId } : {}),
719
+ key,
720
+ ...(readOptionalStringFlag(parsed, "name") ? { name: readOptionalStringFlag(parsed, "name") } : {})
721
+ });
722
+ return {
723
+ data,
724
+ human: renderSuccess(`Created environment ${data.environment.key}${copyFrom ? ` from ${copyFrom}` : ""}.`),
725
+ kind: "environment_create",
726
+ meta: {
727
+ command: "environment create",
728
+ ...(copyFrom ? { copyFrom } : {}),
729
+ environment: data.environment.key,
730
+ orgId: org.id
731
+ }
732
+ };
733
+ }
734
+ case "environment.rename": {
735
+ const data = await api.renameEnvironment(session, org.id, readRequiredArg(parsed, "environment"), {
736
+ name: readRequiredArg(parsed, "name")
737
+ });
738
+ return {
739
+ data,
740
+ human: renderSuccess(`Renamed environment ${data.environment.key}.`),
741
+ kind: "environment_rename",
742
+ meta: { command: "environment rename", environment: data.environment.key, orgId: org.id }
743
+ };
744
+ }
745
+ case "environment.archive": {
746
+ const environment = readRequiredArg(parsed, "environment");
747
+ await confirmDestructive(parsed, context, `Archive environment ${environment}?`);
748
+ const data = await api.archiveEnvironment(session, org.id, environment);
749
+ return {
750
+ data,
751
+ human: renderSuccess(`Archived environment ${data.environment.key}.`),
752
+ kind: "environment_archive",
753
+ meta: { command: "environment archive", environment: data.environment.key, orgId: org.id }
754
+ };
755
+ }
756
+ case "environment.deploy": {
757
+ const environment = readRequiredArg(parsed, "environment");
758
+ const releaseId = readRequiredArg(parsed, "release");
759
+ const policy = readOptionalStringFlag(parsed, "policy");
760
+ const data = await api.deployEnvironment(session, org.id, environment, {
761
+ ...(policy ? { policy } : {}),
762
+ releaseId
763
+ });
764
+ return {
765
+ data,
766
+ human: renderSuccess(`Deployed release ${releaseId} to ${environment} as ${data.deployment.id}.`),
767
+ kind: "environment_deploy",
768
+ meta: { command: "environment deploy", deploymentId: data.deployment.id, environment, orgId: org.id, releaseId }
769
+ };
770
+ }
771
+ case "environment.undeploy": {
772
+ const environment = readRequiredArg(parsed, "environment");
773
+ await confirmDestructive(parsed, context, `Undeploy the live deployment from ${environment}?`);
774
+ const policy = readOptionalStringFlag(parsed, "policy");
775
+ const data = await api.undeployEnvironment(session, org.id, environment, {
776
+ ...(policy ? { policy } : {})
777
+ });
778
+ return {
779
+ data,
780
+ human: renderSuccess(`Undeployed environment ${environment}.`),
781
+ kind: "environment_undeploy",
782
+ meta: { command: "environment undeploy", deploymentId: data.deployment.id, environment, orgId: org.id }
783
+ };
784
+ }
785
+ default:
786
+ throw genericProblem(`Unhandled environment command ${parsed.definition.id}.`, parsed.definition.id);
787
+ }
613
788
  }
614
- async function executeReleaseRestore(parsed, context, api) {
789
+ async function executeDeployment(parsed, context, api) {
615
790
  const { org, session } = await resolveOrgScope(parsed, context, api);
616
- await confirmDestructive(parsed, context, `Restore release ${parsed.args["release-id"]} into draft?`);
617
- const data = await api.restoreRelease(session, org.id, readRequiredArg(parsed, "release-id"));
618
- return {
619
- data,
620
- human: renderSuccess(`Restored release ${parsed.args["release-id"]} into draft.`),
621
- kind: "releases_release_restore",
622
- meta: { command: "release restore", orgId: org.id }
623
- };
791
+ switch (parsed.definition.id) {
792
+ case "deployment.list": {
793
+ const { environment } = resolveCliEnvironment(parsed, session);
794
+ const data = await api.listEnvironmentDeployments(session, org.id, {
795
+ environment,
796
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
797
+ });
798
+ return {
799
+ data,
800
+ human: renderTable(data.deployments.map((deployment) => ({
801
+ ...deployment,
802
+ live: deployment.isLive ? "yes" : ""
803
+ })), [
804
+ { key: "id", label: "Deployment" },
805
+ { key: "environmentKey", label: "Environment" },
806
+ { key: "live", label: "Live" },
807
+ { key: "releaseId", label: "Release" },
808
+ { key: "status", label: "Status" },
809
+ { key: "coreDeploymentId", label: "Core deployment" },
810
+ { key: "updatedAt", label: "Updated" }
811
+ ]),
812
+ kind: "deployment_list",
813
+ meta: { command: "deployment list", environment, orgId: org.id }
814
+ };
815
+ }
816
+ case "deployment.get": {
817
+ const environment = readOptionalStringFlag(parsed, "environment");
818
+ const data = await api.getEnvironmentDeployment(session, org.id, readRequiredArg(parsed, "deployment"), {
819
+ ...(environment ? { environment } : {})
820
+ });
821
+ return {
822
+ data,
823
+ human: renderPrettyJson(data.deployment),
824
+ kind: "deployment_get",
825
+ meta: { command: "deployment get", deploymentId: data.deployment.id, ...(environment ? { environment } : {}), orgId: org.id }
826
+ };
827
+ }
828
+ default:
829
+ throw genericProblem(`Unhandled deployment command ${parsed.definition.id}.`, parsed.definition.id);
830
+ }
624
831
  }
625
- async function executeReleaseDeactivate(parsed, context, api) {
626
- const { org, session } = await resolveOrgScope(parsed, context, api);
627
- await confirmDestructive(parsed, context, `Deactivate release ${parsed.args["release-id"]}?`);
628
- const policy = readOptionalStringFlag(parsed, "policy");
629
- const data = await api.deactivateRelease(session, org.id, readRequiredArg(parsed, "release-id"), policy);
630
- return {
631
- data,
632
- human: renderSuccess(`Deactivated release ${parsed.args["release-id"]}.`),
633
- kind: "releases_release_deactivate",
634
- meta: { command: "release deactivate", orgId: org.id }
635
- };
832
+ async function resolveEnvironmentIdForCli(session, orgId, environment, api) {
833
+ const data = await api.listEnvironments(session, orgId);
834
+ const match = data.environments.find((entry) => entry.environment.id === environment || entry.environment.key === environment);
835
+ if (!match) {
836
+ throw notFoundProblem(`Environment '${environment}' was not found.`, "environment create");
837
+ }
838
+ return match.environment.id;
636
839
  }
637
840
  async function executeRunList(parsed, context, api) {
638
841
  const { org, session } = await resolveOrgScope(parsed, context, api);
842
+ const resolvedEnvironment = resolveCliEnvironment(parsed, session);
639
843
  const data = await api.listRuns(session, org.id, {
844
+ environment: resolvedEnvironment.environment,
640
845
  ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
641
846
  ...(readOptionalStringFlag(parsed, "status") ? { status: readOptionalStringFlag(parsed, "status") } : {})
642
847
  });
@@ -645,11 +850,12 @@ async function executeRunList(parsed, context, api) {
645
850
  human: renderTable(data.runs, [
646
851
  { key: "workflowId", label: "Workflow ID" },
647
852
  { key: "status", label: "Status" },
853
+ { key: "environmentKey", label: "Environment" },
648
854
  { key: "releaseId", label: "Release" },
649
855
  { key: "startTime", label: "Started" }
650
856
  ]),
651
857
  kind: "runtime_run_list",
652
- meta: { command: "run list", orgId: org.id }
858
+ meta: { command: "run list", environment: resolvedEnvironment.environment, orgId: org.id }
653
859
  };
654
860
  }
655
861
  async function executeRunGet(parsed, context, api) {
@@ -764,43 +970,7 @@ async function executeOrgModel(parsed, context, api) {
764
970
  ? renderPrettyJson(record)
765
971
  : renderTable(editor.operations, [
766
972
  { key: "name", label: "Name" },
767
- { key: "connector", label: "Connector" },
768
- { key: "action", label: "Action" }
769
- ]),
770
- kind: `authoring_${sanitizeKind(section)}_${verb}`,
771
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
772
- };
773
- }
774
- if (section === "connectors") {
775
- const editor = (await api.getConnectorsEditor(session, org.id)).editor;
776
- const record = verb === "get"
777
- ? requireFound(editor.connectors.find((entry) => entry.name === parsed.args.name), `Connector '${parsed.args.name}' was not found.`, parsed.definition.id)
778
- : undefined;
779
- return {
780
- data: verb === "get" ? { connector: record } : { connectors: editor.connectors },
781
- human: verb === "get"
782
- ? renderPrettyJson(record)
783
- : renderTable(editor.connectors, [
784
- { key: "name", label: "Name" },
785
- { key: "type", label: "Type" }
786
- ]),
787
- kind: `authoring_${sanitizeKind(section)}_${verb}`,
788
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
789
- };
790
- }
791
- if (section === "mcp-servers") {
792
- const editor = (await api.getMcpServersEditor(session, org.id)).editor;
793
- const record = verb === "get"
794
- ? requireFound(editor.servers.find((entry) => entry.name === parsed.args.name), `MCP server '${parsed.args.name}' was not found.`, parsed.definition.id)
795
- : undefined;
796
- return {
797
- data: verb === "get" ? { server: record } : { servers: editor.servers },
798
- human: verb === "get"
799
- ? renderPrettyJson(record)
800
- : renderTable(editor.servers, [
801
- { key: "name", label: "Name" },
802
- { key: "transport", label: "Transport" },
803
- { key: "url", label: "URL" }
973
+ { key: "command", label: "Command" }
804
974
  ]),
805
975
  kind: `authoring_${sanitizeKind(section)}_${verb}`,
806
976
  meta: { command: parsed.definition.path.join(" "), orgId: org.id }
@@ -985,353 +1155,41 @@ async function executeAccess(parsed, context, api) {
985
1155
  throw genericProblem(`Unhandled access command ${parsed.definition.id}.`, parsed.definition.id);
986
1156
  }
987
1157
  }
988
- async function executeMcp(parsed, context, api) {
989
- const { org, session } = await resolveOrgScope(parsed, context, api);
990
- switch (parsed.definition.id) {
991
- case "mcp.list": {
992
- const data = await api.listOrgMcpServers(session, org.id);
993
- return {
994
- data,
995
- human: renderTable(data.servers, [
996
- { key: "slug", label: "Slug" },
997
- { key: "displayName", label: "Name" },
998
- { key: "status", label: "Status" },
999
- { key: "authMode", label: "Auth" },
1000
- { key: "url", label: "URL" }
1001
- ]),
1002
- kind: "mcp_servers_list",
1003
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1004
- };
1005
- }
1006
- case "mcp.get": {
1007
- const data = await api.getOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"));
1008
- return {
1009
- data,
1010
- human: renderPrettyJson(data),
1011
- kind: "mcp_servers_get",
1012
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1013
- };
1014
- }
1015
- case "mcp.create": {
1016
- const authMode = readOptionalMcpAuthMode(parsed);
1017
- const timeoutMs = readOptionalNumberFlag(parsed, "timeout-ms");
1018
- const input = {
1019
- displayName: readRequiredStringFlag(parsed, "name"),
1020
- slug: readRequiredStringFlag(parsed, "slug"),
1021
- url: readRequiredStringFlag(parsed, "url"),
1022
- ...(authMode !== undefined ? { authMode } : {}),
1023
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
1024
- };
1025
- const data = await api.createOrgMcpServer(session, org.id, input);
1026
- return {
1027
- data,
1028
- human: renderSuccess(`Created MCP server ${data.server.slug}.`),
1029
- kind: "mcp_servers_create",
1030
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1031
- };
1032
- }
1033
- case "mcp.update": {
1034
- const authMode = readOptionalMcpAuthMode(parsed);
1035
- const displayName = readOptionalStringFlag(parsed, "name");
1036
- const timeoutMs = readOptionalNumberFlag(parsed, "timeout-ms");
1037
- const url = readOptionalStringFlag(parsed, "url");
1038
- const disable = parsed.flags.disable === true;
1039
- const enable = parsed.flags.enable === true;
1040
- if (disable && enable) {
1041
- throw usageProblem("Use either --disable or --enable, not both.", parsed.definition.id);
1042
- }
1043
- const input = {
1044
- ...(displayName !== undefined ? { displayName } : {}),
1045
- ...(url !== undefined ? { url } : {}),
1046
- ...(authMode !== undefined ? { authMode } : {}),
1047
- ...(disable || enable ? { disabled: disable } : {}),
1048
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
1049
- };
1050
- if (Object.keys(input).length === 0) {
1051
- throw usageProblem("Provide at least one MCP server field to update.", parsed.definition.id);
1052
- }
1053
- const data = await api.updateOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"), input);
1054
- return {
1055
- data,
1056
- human: renderSuccess(`Updated MCP server ${data.server.slug}.`),
1057
- kind: "mcp_servers_update",
1058
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1059
- };
1060
- }
1061
- case "mcp.delete": {
1062
- const serverRef = readRequiredArg(parsed, "server");
1063
- await confirmDestructive(parsed, context, `Delete MCP server ${serverRef}?`);
1064
- await api.deleteOrgMcpServer(session, org.id, serverRef);
1065
- return {
1066
- data: {},
1067
- human: renderSuccess(`Deleted MCP server ${serverRef}.`),
1068
- kind: "mcp_servers_delete",
1069
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1070
- };
1071
- }
1072
- case "mcp.test": {
1073
- const data = await api.testOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"));
1074
- if (!data.ok) {
1075
- throw genericProblem(data.server.lastErrorMessage ?? `MCP server ${data.server.slug} test failed.`, parsed.definition.id);
1076
- }
1077
- return {
1078
- data,
1079
- human: renderSuccess(`MCP server ${data.server.slug} is reachable.`),
1080
- kind: "mcp_servers_test",
1081
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1082
- };
1083
- }
1084
- case "mcp.discover-tools": {
1085
- const data = await api.discoverOrgMcpServerTools(session, org.id, readRequiredArg(parsed, "server"));
1086
- return {
1087
- data,
1088
- human: data.tools.length === 0
1089
- ? renderSuccess(`Discovered no tools for ${data.server.slug}.`)
1090
- : renderTable(data.tools, [
1091
- { key: "toolName", label: "Tool" },
1092
- { key: "description", label: "Description" },
1093
- { key: "schemaSha256", label: "Schema" }
1094
- ]),
1095
- kind: "mcp_servers_discover_tools",
1096
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1097
- };
1098
- }
1099
- case "mcp.connect": {
1100
- const serverRef = readRequiredArg(parsed, "server");
1101
- const data = await api.connectOrgMcpServerOAuth(session, org.id, serverRef);
1102
- const explicitOpen = parsed.flags.open === true;
1103
- const shouldOpen = shouldAutoOpenBrowser({
1104
- env: context.env,
1105
- explicitNoOpen: parsed.flags["no-open"] === true,
1106
- explicitOpen,
1107
- interactive: isInteractive(context),
1108
- openBrowserConfig: context.config.openBrowser
1109
- });
1110
- let opened = false;
1111
- if (shouldOpen) {
1112
- opened = await openBrowser(data.authorizationUrl);
1113
- if (explicitOpen && !opened) {
1114
- throw genericProblem(`Could not open a browser automatically. Visit ${data.authorizationUrl}`, parsed.definition.id);
1115
- }
1116
- }
1117
- const finalSession = parsed.flags.wait === true
1118
- ? await pollMcpOAuthSession(api, session, org.id, serverRef, data.sessionId)
1119
- : null;
1120
- if (finalSession && isFailedMcpOAuthSession(finalSession.session.status)) {
1121
- throw genericProblem(finalSession.session.errorMessage
1122
- ?? `MCP OAuth connection ${finalSession.session.status}.`, parsed.definition.id);
1123
- }
1124
- return {
1125
- data: finalSession ? { ...data, session: finalSession.session } : data,
1126
- human: finalSession
1127
- ? renderMcpOAuthSessionHuman(data.authorizationUrl, finalSession.session.status)
1128
- : [
1129
- "MCP OAuth authorization started.",
1130
- `Open: ${data.authorizationUrl}`,
1131
- `Session: ${data.sessionId}`
1132
- ].join("\n"),
1133
- kind: "mcp_servers_oauth_connect",
1134
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1135
- };
1136
- }
1137
- case "mcp.disconnect": {
1138
- const serverRef = readRequiredArg(parsed, "server");
1139
- await confirmDestructive(parsed, context, `Disconnect OAuth for MCP server ${serverRef}?`);
1140
- const data = await api.disconnectOrgMcpServerOAuth(session, org.id, serverRef);
1141
- return {
1142
- data,
1143
- human: renderSuccess(`Disconnected OAuth for MCP server ${data.server.slug}.`),
1144
- kind: "mcp_servers_oauth_disconnect",
1145
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1146
- };
1147
- }
1148
- case "mcp.oauth-status": {
1149
- const data = await api.getOrgMcpServerOAuthStatus(session, org.id, readRequiredArg(parsed, "server"));
1150
- return {
1151
- data,
1152
- human: renderPrettyJson(data.oauth),
1153
- kind: "mcp_servers_oauth_status",
1154
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1155
- };
1156
- }
1157
- default:
1158
- throw genericProblem(`Unhandled MCP command ${parsed.definition.id}.`, parsed.definition.id);
1159
- }
1160
- }
1161
- async function executeSkills(parsed, context, api) {
1162
- const { org, session } = await resolveOrgScope(parsed, context, api);
1163
- switch (parsed.definition.id) {
1164
- case "skills.list": {
1165
- const data = await api.listOrgSkillPackages(session, org.id);
1166
- return {
1167
- data,
1168
- human: renderTable(data.skills, [
1169
- { key: "slug", label: "Slug" },
1170
- { key: "displayName", label: "Name" },
1171
- { key: "status", label: "Status" },
1172
- { key: "currentRevision", label: "Revision" },
1173
- { key: "currentFileCount", label: "Files" },
1174
- { key: "currentPackageSha256", label: "Package SHA" },
1175
- { key: "updatedAt", label: "Updated" }
1176
- ]),
1177
- kind: "skills_list",
1178
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1179
- };
1180
- }
1181
- case "skills.get": {
1182
- const data = await api.getOrgSkillPackage(session, org.id, readRequiredArg(parsed, "skill"));
1183
- return {
1184
- data,
1185
- human: renderPrettyJson(data),
1186
- kind: "skills_get",
1187
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1188
- };
1189
- }
1190
- case "skills.create": {
1191
- const description = readOptionalStringFlag(parsed, "description");
1192
- const dir = readOptionalStringFlag(parsed, "dir");
1193
- const files = dir ? await readImportEntries(dir) : undefined;
1194
- const data = await api.createOrgSkillPackage(session, org.id, {
1195
- ...(description !== undefined ? { description } : {}),
1196
- displayName: readRequiredStringFlag(parsed, "name"),
1197
- ...(files ? { files } : {}),
1198
- slug: readRequiredStringFlag(parsed, "slug")
1199
- });
1200
- return {
1201
- data,
1202
- human: renderSuccess(`Created skill package ${data.skill.slug}.`),
1203
- kind: "skills_create",
1204
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1205
- };
1206
- }
1207
- case "skills.upload": {
1208
- const skillRef = readRequiredArg(parsed, "skill");
1209
- const files = await readImportEntries(readRequiredStringFlag(parsed, "dir"));
1210
- const data = await api.uploadOrgSkillPackageRevision(session, org.id, skillRef, { files });
1211
- return {
1212
- data,
1213
- human: renderSuccess(`Uploaded skill package ${skillRef} revision ${String(data.revision.revision)}.`),
1214
- kind: "skills_upload",
1215
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1216
- };
1217
- }
1218
- case "skills.download": {
1219
- const skillRef = readRequiredArg(parsed, "skill");
1220
- const revision = readOptionalNumberFlag(parsed, "revision");
1221
- const targetDir = readRequiredStringFlag(parsed, "dir");
1222
- const data = revision === undefined
1223
- ? await api.getOrgSkillPackage(session, org.id, skillRef)
1224
- : await api.getOrgSkillPackageRevision(session, org.id, skillRef, revision);
1225
- await writeSkillPackageFiles(targetDir, data.files);
1226
- return {
1227
- data,
1228
- human: renderSuccess(`Downloaded ${data.files.length} skill package files to ${resolve(targetDir)}.`),
1229
- kind: "skills_download",
1230
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1231
- };
1232
- }
1233
- case "skills.update": {
1234
- const displayName = readOptionalStringFlag(parsed, "name");
1235
- const description = readOptionalStringFlag(parsed, "description");
1236
- const disable = parsed.flags.disable === true;
1237
- const enable = parsed.flags.enable === true;
1238
- if (disable && enable) {
1239
- throw usageProblem("Use either --disable or --enable, not both.", parsed.definition.id);
1240
- }
1241
- const input = {
1242
- ...(displayName !== undefined ? { displayName } : {}),
1243
- ...(description !== undefined ? { description } : {}),
1244
- ...(disable || enable ? { disabled: disable } : {})
1245
- };
1246
- if (Object.keys(input).length === 0) {
1247
- throw usageProblem("Provide at least one skill package field to update.", parsed.definition.id);
1248
- }
1249
- const data = await api.updateOrgSkillPackage(session, org.id, readRequiredArg(parsed, "skill"), input);
1250
- return {
1251
- data,
1252
- human: renderSuccess(`Updated skill package ${data.skill.slug}.`),
1253
- kind: "skills_update",
1254
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1255
- };
1256
- }
1257
- case "skills.delete": {
1258
- const skillRef = readRequiredArg(parsed, "skill");
1259
- await confirmDestructive(parsed, context, `Delete skill package ${skillRef}?`);
1260
- await api.deleteOrgSkillPackage(session, org.id, skillRef);
1261
- return {
1262
- data: {},
1263
- human: renderSuccess(`Deleted skill package ${skillRef}.`),
1264
- kind: "skills_delete",
1265
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1266
- };
1267
- }
1268
- default:
1269
- throw genericProblem(`Unhandled skills command ${parsed.definition.id}.`, parsed.definition.id);
1270
- }
1271
- }
1272
- async function writeSkillPackageFiles(targetDir, files) {
1273
- const root = resolve(targetDir);
1274
- for (const file of files) {
1275
- const targetPath = resolve(root, file.path);
1276
- if (!targetPath.startsWith(`${root}/`) && targetPath !== root) {
1277
- throw usageProblem(`Refusing to write skill package path '${file.path}' outside destination.`, "skills.download");
1278
- }
1279
- await mkdir(dirname(targetPath), { recursive: true });
1280
- await writeFile(targetPath, file.content, "utf8");
1281
- }
1282
- }
1283
- async function pollMcpOAuthSession(api, session, orgId, serverRef, oauthSessionId) {
1284
- for (let attempt = 0; attempt < 60; attempt += 1) {
1285
- const result = await api.getOrgMcpServerOAuthSession(session, orgId, serverRef, oauthSessionId);
1286
- if (result.session.status !== "pending") {
1287
- return result;
1288
- }
1289
- await new Promise((resolve) => setTimeout(resolve, 2_000));
1290
- }
1291
- return await api.getOrgMcpServerOAuthSession(session, orgId, serverRef, oauthSessionId);
1292
- }
1293
- function renderMcpOAuthSessionHuman(authorizationUrl, status) {
1294
- if (status === "connected") {
1295
- return renderSuccess("MCP OAuth connection completed.");
1296
- }
1297
- if (isFailedMcpOAuthSession(status)) {
1298
- return "MCP OAuth connection failed.";
1299
- }
1300
- return [
1301
- "MCP OAuth authorization is still pending.",
1302
- `Open: ${authorizationUrl}`
1303
- ].join("\n");
1304
- }
1305
- function isFailedMcpOAuthSession(status) {
1306
- return status === "error" || status === "expired";
1307
- }
1308
1158
  async function executeEnv(parsed, context, api) {
1309
1159
  const { org, session } = await resolveOrgScope(parsed, context, api);
1310
1160
  switch (parsed.definition.id) {
1311
1161
  case "env.list": {
1312
- const data = await api.listRuntimeVariables(session, org.id);
1162
+ const environment = readCliEnvironmentFlag(parsed);
1163
+ const data = await api.listRuntimeVariables(session, org.id, {
1164
+ ...(environment ? { environment } : {})
1165
+ });
1313
1166
  return {
1314
1167
  data,
1315
1168
  human: renderTable(data.variables, [
1316
1169
  { key: "name", label: "Name" },
1317
1170
  { key: "value", label: "Value" },
1171
+ { key: "environmentKey", label: "Environment" },
1318
1172
  { key: "updatedAt", label: "Updated" }
1319
1173
  ]),
1320
1174
  kind: "authoring_env_list",
1321
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1175
+ meta: { command: parsed.definition.path.join(" "), ...(environment ? { environment } : {}), orgId: org.id }
1322
1176
  };
1323
1177
  }
1324
1178
  case "env.get": {
1325
- const variables = (await api.listRuntimeVariables(session, org.id)).variables;
1179
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1180
+ const variables = (await api.listRuntimeVariables(session, org.id, {
1181
+ environment
1182
+ })).variables;
1326
1183
  const variable = requireFound(variables.find((entry) => entry.name === readRequiredArg(parsed, "name")) ?? null, `Environment variable '${parsed.args.name}' was not found.`, parsed.definition.id);
1327
1184
  return {
1328
1185
  data: { variable },
1329
1186
  human: renderPrettyJson(variable),
1330
1187
  kind: "authoring_env_get",
1331
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1188
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1332
1189
  };
1333
1190
  }
1334
1191
  case "env.replace": {
1192
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1335
1193
  const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id);
1336
1194
  const rawVariables = body.variables;
1337
1195
  if (!Array.isArray(rawVariables)) {
@@ -1346,33 +1204,42 @@ async function executeEnv(parsed, context, api) {
1346
1204
  value: String(entry.value)
1347
1205
  };
1348
1206
  });
1349
- const current = (await api.listRuntimeVariables(session, org.id)).variables;
1207
+ const current = (await api.listRuntimeVariables(session, org.id, {
1208
+ environment
1209
+ })).variables;
1350
1210
  const diff = diffVariables(current, nextVariables);
1211
+ announceResolvedEnvironment(parsed, context, environment);
1351
1212
  if (!parsed.json) {
1352
1213
  context.stdout.write(`${renderDiffSummary(diff)}\n`);
1353
1214
  }
1354
- await confirmDestructive(parsed, context, `Replace the full environment for ${org.slug}?`);
1355
- const data = await api.replaceRuntimeVariables(session, org.id, { variables: nextVariables });
1215
+ await confirmDestructive(parsed, context, `Replace runtime variables for ${environment} in ${org.slug}?`);
1216
+ const data = await api.replaceRuntimeVariables(session, org.id, {
1217
+ environment,
1218
+ variables: nextVariables
1219
+ });
1356
1220
  return {
1357
1221
  data,
1358
- human: renderSuccess(`Replaced ${data.variables.length} environment variables.`),
1222
+ human: renderSuccess(`Replaced ${data.variables.length} environment variables in ${environment}.`),
1359
1223
  kind: "authoring_env_replace",
1360
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1224
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1361
1225
  };
1362
1226
  }
1363
1227
  case "env.import": {
1228
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1364
1229
  const filePath = readRequiredArg(parsed, "path-to-.env");
1365
1230
  const content = await readTextInputSpecifier(`@${filePath}`, context.stdin, parsed.definition.id);
1366
1231
  const preview = await parseEnvFile(filePath);
1232
+ announceResolvedEnvironment(parsed, context, environment);
1367
1233
  const data = await api.importRuntimeVariables(session, org.id, {
1368
1234
  content,
1235
+ environment,
1369
1236
  fileName: filePath
1370
1237
  });
1371
1238
  return {
1372
1239
  data,
1373
- human: renderSuccess(`Imported ${data.importedCount} variables from ${filePath}. Parsed ${preview.length}.`),
1240
+ human: renderSuccess(`Imported ${data.importedCount} variables from ${filePath} into ${environment}. Parsed ${preview.length}.`),
1374
1241
  kind: "authoring_env_import",
1375
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1242
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1376
1243
  };
1377
1244
  }
1378
1245
  default:
@@ -1383,40 +1250,50 @@ async function executeSecrets(parsed, context, api) {
1383
1250
  const { org, session } = await resolveOrgScope(parsed, context, api);
1384
1251
  switch (parsed.definition.id) {
1385
1252
  case "secrets.list": {
1386
- const data = await api.listOrgSecrets(session, org.id);
1253
+ const environment = readCliEnvironmentFlag(parsed);
1254
+ const data = await api.listOrgSecrets(session, org.id, {
1255
+ ...(environment ? { environment } : {})
1256
+ });
1387
1257
  return {
1388
1258
  data,
1389
1259
  human: renderTable(data.secrets, [
1390
1260
  { key: "name", label: "Name" },
1391
1261
  { key: "hasValue", label: "Defined" },
1262
+ { key: "environmentKey", label: "Environment" },
1392
1263
  { key: "updatedAt", label: "Updated" }
1393
1264
  ]),
1394
1265
  kind: "authoring_secrets_list",
1395
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1266
+ meta: { command: parsed.definition.path.join(" "), ...(environment ? { environment } : {}), orgId: org.id }
1396
1267
  };
1397
1268
  }
1398
1269
  case "secrets.set": {
1270
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1399
1271
  const value = await readTextInputSpecifier("-", context.stdin, parsed.definition.id);
1272
+ announceResolvedEnvironment(parsed, context, environment);
1400
1273
  const data = await api.upsertOrgSecret(session, org.id, {
1274
+ environment,
1401
1275
  name: readRequiredArg(parsed, "name"),
1402
1276
  value: value.replace(/\r?\n$/u, "")
1403
1277
  });
1404
1278
  return {
1405
1279
  data,
1406
- human: renderSuccess(`Saved secret '${data.secret.name}'.`),
1280
+ human: renderSuccess(`Saved secret '${data.secret.name}' in ${environment}.`),
1407
1281
  kind: "authoring_secrets_set",
1408
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1282
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1409
1283
  };
1410
1284
  }
1411
1285
  case "secrets.delete": {
1286
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1412
1287
  const name = readRequiredArg(parsed, "name");
1413
- await confirmDestructive(parsed, context, `Delete secret ${name} from ${org.slug}?`);
1414
- const data = await api.deleteOrgSecret(session, org.id, name);
1288
+ await confirmDestructive(parsed, context, `Delete secret ${name} from ${environment} in ${org.slug}?`);
1289
+ const data = await api.deleteOrgSecret(session, org.id, name, {
1290
+ environment
1291
+ });
1415
1292
  return {
1416
1293
  data,
1417
- human: renderSuccess(data.deleted ? `Deleted secret '${name}'.` : `Secret '${name}' was not defined.`),
1294
+ human: renderSuccess(data.deleted ? `Deleted secret '${name}' from ${environment}.` : `Secret '${name}' was not defined in ${environment}.`),
1418
1295
  kind: "authoring_secrets_delete",
1419
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1296
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1420
1297
  };
1421
1298
  }
1422
1299
  default:
@@ -1467,62 +1344,6 @@ async function executeChat(parsed, context, api) {
1467
1344
  meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1468
1345
  };
1469
1346
  }
1470
- case "chat.changes": {
1471
- const data = await api.getChatChanges(session, org.id, readRequiredArg(parsed, "session-id"));
1472
- return {
1473
- data,
1474
- human: renderPrettyJson(data.changes),
1475
- kind: "chat_changes_get",
1476
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1477
- };
1478
- }
1479
- case "chat.validation": {
1480
- const data = await api.getChatValidation(session, org.id, readRequiredArg(parsed, "session-id"));
1481
- return {
1482
- data,
1483
- human: renderPrettyJson(data.validation),
1484
- kind: "chat_validation_get",
1485
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1486
- };
1487
- }
1488
- case "chat.draft.get": {
1489
- const data = await api.getChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1490
- return {
1491
- data,
1492
- human: renderPrettyJson(data.draft),
1493
- kind: "chat_draft_get",
1494
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1495
- };
1496
- }
1497
- case "chat.draft.apply": {
1498
- await confirmDestructive(parsed, context, `Apply draft for session ${parsed.args["session-id"]}?`);
1499
- const data = await api.applyChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1500
- return {
1501
- data,
1502
- human: renderSuccess(`Applied draft for session ${parsed.args["session-id"]}.`),
1503
- kind: "chat_draft_apply",
1504
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1505
- };
1506
- }
1507
- case "chat.draft.refresh": {
1508
- await confirmDestructive(parsed, context, `Refresh draft for session ${parsed.args["session-id"]}?`);
1509
- const data = await api.refreshChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1510
- return {
1511
- data,
1512
- human: renderSuccess(`Refreshed draft for session ${parsed.args["session-id"]}.`),
1513
- kind: "chat_draft_refresh",
1514
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1515
- };
1516
- }
1517
- case "chat.workspace-state": {
1518
- const data = await api.getChatWorkspaceState(session, org.id, readRequiredArg(parsed, "session-id"));
1519
- return {
1520
- data,
1521
- human: renderPrettyJson(data.workspaceState),
1522
- kind: "chat_workspace_state_get",
1523
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1524
- };
1525
- }
1526
1347
  default:
1527
1348
  throw genericProblem(`Unhandled chat command ${parsed.definition.id}.`, parsed.definition.id);
1528
1349
  }
@@ -1569,9 +1390,10 @@ async function executeCompletion(parsed) {
1569
1390
  }
1570
1391
  }
1571
1392
  async function executeSchemaGet(parsed) {
1572
- const schema = getCliSchema(readRequiredArg(parsed, "resource-name"));
1393
+ const resourceName = readRequiredArg(parsed, "resource-name");
1394
+ const schema = getCliSchema(resourceName);
1573
1395
  if (!schema) {
1574
- throw notFoundProblem(`Schema '${parsed.args["resource-name"]}' was not found.`, "schema get");
1396
+ throw notFoundProblem(`Schema '${resourceName}' was not found. Use lowercase schema names, not YAML kind names. Run \`schema list\` to see all schemas. Known names include: ${schemaNamePreview()}.`, "schema get");
1575
1397
  }
1576
1398
  return {
1577
1399
  data: { schema },
@@ -1589,9 +1411,99 @@ async function executeSchemaGet(parsed) {
1589
1411
  meta: { command: "schema get" }
1590
1412
  };
1591
1413
  }
1414
+ function schemaNamePreview() {
1415
+ const names = new Set(listCliSchemas().map((schema) => schema.name));
1416
+ return ["process", "operation", "capability", "decision", "manifest", "organization", "assignments"]
1417
+ .filter((name) => names.has(name))
1418
+ .join(", ");
1419
+ }
1592
1420
  async function executeAdmin(parsed, context, api) {
1593
1421
  const session = requireStoredSession(context.session, parsed.definition.id);
1594
1422
  switch (parsed.definition.id) {
1423
+ case "admin.usage.summary": {
1424
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1425
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1426
+ return {
1427
+ data,
1428
+ human: renderAgentUsageSummary(org.id, data.usage),
1429
+ kind: "admin_usage_summary",
1430
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1431
+ };
1432
+ }
1433
+ case "admin.usage.workflows": {
1434
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1435
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1436
+ return {
1437
+ data,
1438
+ human: renderTable(data.usage.workflows.map((workflow) => ({
1439
+ cost: formatUsdMicros(workflow.usdMicros),
1440
+ executions: formatInteger(workflow.agentExecutionCount),
1441
+ model: formatProviderModel(workflow.provider, workflow.model),
1442
+ tokensIn: formatInteger(workflow.tokensIn),
1443
+ tokensOut: formatInteger(workflow.tokensOut),
1444
+ toolCalls: formatInteger(workflow.toolCallCount),
1445
+ workflow: workflow.processName
1446
+ })), [
1447
+ { key: "workflow", label: "Workflow" },
1448
+ { key: "model", label: "Model" },
1449
+ { key: "executions", label: "Executions" },
1450
+ { key: "tokensIn", label: "Tokens in" },
1451
+ { key: "tokensOut", label: "Tokens out" },
1452
+ { key: "toolCalls", label: "Tool calls" },
1453
+ { key: "cost", label: "Cost" }
1454
+ ]),
1455
+ kind: "admin_usage_workflows",
1456
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1457
+ };
1458
+ }
1459
+ case "admin.usage.tools": {
1460
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1461
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1462
+ return {
1463
+ data,
1464
+ human: renderTable(data.usage.tools.map((tool) => ({
1465
+ calls: formatInteger(tool.callCount),
1466
+ failed: formatInteger(tool.failureCount),
1467
+ succeeded: formatInteger(tool.successCount),
1468
+ tool: tool.toolName
1469
+ })), [
1470
+ { key: "tool", label: "Tool" },
1471
+ { key: "calls", label: "Calls" },
1472
+ { key: "succeeded", label: "Succeeded" },
1473
+ { key: "failed", label: "Failed" }
1474
+ ]),
1475
+ kind: "admin_usage_tools",
1476
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1477
+ };
1478
+ }
1479
+ case "admin.usage.events": {
1480
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1481
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1482
+ return {
1483
+ data,
1484
+ human: renderTable(data.usage.recentEvents.map((event) => ({
1485
+ cost: formatUsdMicros(event.usdMicros),
1486
+ event: event.eventId,
1487
+ model: formatProviderModel(event.provider, event.model),
1488
+ occurredAt: event.occurredAt,
1489
+ status: event.sourceStatus === "event_only" ? `${event.status} (event only)` : event.status,
1490
+ tokens: formatInteger(event.tokensIn + event.tokensOut),
1491
+ toolCalls: formatInteger(event.toolCallCount),
1492
+ workflow: event.processName
1493
+ })), [
1494
+ { key: "event", label: "Event" },
1495
+ { key: "workflow", label: "Workflow" },
1496
+ { key: "model", label: "Model" },
1497
+ { key: "status", label: "Status" },
1498
+ { key: "tokens", label: "Tokens" },
1499
+ { key: "toolCalls", label: "Tool calls" },
1500
+ { key: "cost", label: "Cost" },
1501
+ { key: "occurredAt", label: "Occurred at" }
1502
+ ]),
1503
+ kind: "admin_usage_events",
1504
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1505
+ };
1506
+ }
1595
1507
  case "admin.org.deleted.list": {
1596
1508
  const data = await api.listDeletedOrganizations(session);
1597
1509
  return {
@@ -1638,6 +1550,46 @@ async function executeAdmin(parsed, context, api) {
1638
1550
  throw genericProblem(`Unhandled admin command ${parsed.definition.id}.`, parsed.definition.id);
1639
1551
  }
1640
1552
  }
1553
+ function readAgentUsageQuery(parsed) {
1554
+ const from = readOptionalStringFlag(parsed, "from");
1555
+ const to = readOptionalStringFlag(parsed, "to");
1556
+ return {
1557
+ ...(from ? { from } : {}),
1558
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
1559
+ ...(to ? { to } : {})
1560
+ };
1561
+ }
1562
+ function renderAgentUsageSummary(orgId, usage) {
1563
+ const summary = usage.summary;
1564
+ return renderKeyValue([
1565
+ { label: "Organization", value: orgId },
1566
+ { label: "From", value: summary.from },
1567
+ { label: "To", value: summary.to },
1568
+ { label: "Agent executions", value: formatInteger(summary.agentExecutionCount) },
1569
+ { label: "Tokens in", value: formatInteger(summary.tokensIn) },
1570
+ { label: "Tokens out", value: formatInteger(summary.tokensOut) },
1571
+ { label: "Tool calls", value: formatInteger(summary.toolCallCount) },
1572
+ { label: "Cost", value: formatUsdMicros(summary.usdMicros) },
1573
+ { label: "Event-only records", value: formatInteger(summary.sourceEventOnlyCount) }
1574
+ ]);
1575
+ }
1576
+ function formatProviderModel(provider, model) {
1577
+ if (provider && model) {
1578
+ return `${provider}/${model}`;
1579
+ }
1580
+ return provider ?? model ?? "";
1581
+ }
1582
+ function formatInteger(value) {
1583
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value);
1584
+ }
1585
+ function formatUsdMicros(value) {
1586
+ return new Intl.NumberFormat(undefined, {
1587
+ currency: "USD",
1588
+ maximumFractionDigits: 4,
1589
+ minimumFractionDigits: value > 0 && value < 10_000 ? 4 : 2,
1590
+ style: "currency"
1591
+ }).format(value / 1_000_000);
1592
+ }
1641
1593
  async function resolveOrgScope(parsed, context, api) {
1642
1594
  const session = requireStoredSession(context.session, parsed.definition.id);
1643
1595
  const selector = readOptionalStringFlag(parsed, "org");
@@ -1675,16 +1627,109 @@ function isSameSessionIdentity(candidate, session) {
1675
1627
  && candidate.user.id === session.user.id
1676
1628
  && candidate.accessTokenPayload.sub === session.accessTokenPayload.sub;
1677
1629
  }
1678
- function readOptionalMcpAuthMode(parsed) {
1679
- const value = readOptionalStringFlag(parsed, "auth-mode");
1680
- if (value === undefined || value === "none" || value === "oauth") {
1681
- return value;
1682
- }
1683
- throw usageProblem("MCP auth mode must be 'none' or 'oauth'.", parsed.definition.id);
1684
- }
1685
1630
  function optionalNumberValue(key, value) {
1686
1631
  return value !== undefined ? { [key]: value } : {};
1687
1632
  }
1633
+ function withCliSummary(meta, summary) {
1634
+ const normalized = normalizeCliSummary(summary);
1635
+ return normalized ? { ...meta, summary: normalized } : meta;
1636
+ }
1637
+ function normalizeCliSummary(value) {
1638
+ const trimmed = value?.trim();
1639
+ return trimmed && trimmed.length > 0 ? trimmed : null;
1640
+ }
1641
+ function summarizeWorkflowNodeTest(test) {
1642
+ if (test.status === "passed") {
1643
+ const capturedArtifacts = Array.isArray(test.capturedArtifacts)
1644
+ ? test.capturedArtifacts.length
1645
+ : 0;
1646
+ return `Workflow-node test passed${capturedArtifacts > 0 ? ` with ${String(capturedArtifacts)} captured artifact${capturedArtifacts === 1 ? "" : "s"}` : ""}.`;
1647
+ }
1648
+ const label = test.status === "invalid" ? "invalid" : "failed";
1649
+ const error = recordField(test, "error");
1650
+ const message = typeof error.message === "string" && error.message.trim().length > 0
1651
+ ? truncateSummaryText(error.message)
1652
+ : firstDiagnosticMessage(readSummaryDiagnostics(test));
1653
+ return `Workflow-node test ${label}${message ? `: ${message}` : ""}.`;
1654
+ }
1655
+ function renderWorkflowNodeTestHuman(test) {
1656
+ const summary = summarizeWorkflowNodeTest(test);
1657
+ const capturedArtifacts = Array.isArray(test.capturedArtifacts)
1658
+ ? test.capturedArtifacts.length
1659
+ : 0;
1660
+ const logs = Array.isArray(test.logs)
1661
+ ? test.logs.length
1662
+ : 0;
1663
+ const rows = [
1664
+ { label: "Status", value: test.status },
1665
+ { label: "Workflow", value: test.workflowName },
1666
+ { label: "Node", value: test.nodeId },
1667
+ ...(typeof test.operationName === "string"
1668
+ ? [{ label: "Operation", value: test.operationName }]
1669
+ : []),
1670
+ { label: "Captured artifacts", value: capturedArtifacts },
1671
+ { label: "Logs", value: logs > 0 ? `${String(logs)} stream${logs === 1 ? "" : "s"} available` : "none" }
1672
+ ];
1673
+ const diagnostics = readSummaryDiagnostics(test);
1674
+ const error = recordField(test, "error");
1675
+ const actionable = typeof error.message === "string" && error.message.trim().length > 0
1676
+ ? truncateSummaryText(error.message)
1677
+ : firstDiagnosticMessage(diagnostics);
1678
+ return [
1679
+ summary,
1680
+ renderKeyValue(rows),
1681
+ ...(actionable && test.status !== "passed" ? [`Action: ${actionable}`] : [])
1682
+ ].filter((entry) => entry.trim().length > 0).join("\n");
1683
+ }
1684
+ function summarizeReleaseValidation(validation) {
1685
+ const diagnostics = readSummaryDiagnostics(validation);
1686
+ return validation.releaseReady === true
1687
+ ? `Release validation passed${formatDiagnosticSummarySuffix(diagnostics)}.`
1688
+ : `Release validation failed${formatDiagnosticSummarySuffix(diagnostics)}.`;
1689
+ }
1690
+ function summarizeReleaseCreation(releaseId) {
1691
+ return `Release ${releaseId} created. It is not live yet; review and deploy it from /app/releases/${releaseId}.`;
1692
+ }
1693
+ function readSummaryDiagnostics(record) {
1694
+ const diagnostics = record.diagnostics;
1695
+ return Array.isArray(diagnostics)
1696
+ ? diagnostics.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry))
1697
+ : [];
1698
+ }
1699
+ function formatDiagnosticSummarySuffix(diagnostics) {
1700
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1701
+ const warningCount = diagnostics.length - errorCount;
1702
+ const parts = [
1703
+ ...(errorCount > 0 ? [formatDiagnosticCount(errorCount, "error")] : []),
1704
+ ...(warningCount > 0 ? [formatDiagnosticCount(warningCount, "warning")] : [])
1705
+ ];
1706
+ if (parts.length === 0) {
1707
+ return "";
1708
+ }
1709
+ const message = firstDiagnosticMessage(diagnostics);
1710
+ return ` with ${parts.join(" and ")}${message ? `: ${message}` : ""}`;
1711
+ }
1712
+ function formatDiagnosticCount(count, label) {
1713
+ return `${String(count)} ${label}${count === 1 ? "" : "s"}`;
1714
+ }
1715
+ function firstDiagnosticMessage(diagnostics) {
1716
+ for (const diagnostic of diagnostics) {
1717
+ if (typeof diagnostic.message === "string" && diagnostic.message.trim().length > 0) {
1718
+ return truncateSummaryText(diagnostic.message);
1719
+ }
1720
+ }
1721
+ return null;
1722
+ }
1723
+ function truncateSummaryText(value) {
1724
+ const trimmed = value.trim();
1725
+ return trimmed.length <= 180 ? trimmed : `${trimmed.slice(0, 177)}...`;
1726
+ }
1727
+ function recordField(result, key) {
1728
+ const value = result[key];
1729
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1730
+ ? value
1731
+ : {};
1732
+ }
1688
1733
  function readRequiredArg(parsed, name) {
1689
1734
  const value = parsed.args[name];
1690
1735
  if (!value) {
@@ -1692,6 +1737,11 @@ function readRequiredArg(parsed, name) {
1692
1737
  }
1693
1738
  return value;
1694
1739
  }
1740
+ function announceResolvedEnvironment(parsed, context, environment) {
1741
+ if (!parsed.json) {
1742
+ context.stdout.write(`Environment: ${environment}\n`);
1743
+ }
1744
+ }
1695
1745
  function readRequiredIntegerArg(parsed, name) {
1696
1746
  const raw = readRequiredArg(parsed, name);
1697
1747
  const parsedValue = Number(raw);