@frontera-sdk/cli 1.52.38 → 1.52.40

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 (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
package/README.md CHANGED
@@ -112,6 +112,10 @@ project) and `frontera app init` (scaffold a new app) are different commands.
112
112
 
113
113
  `0` success · `1` transient · `2` usage · `3` conflict · `4` auth. Branch on
114
114
  them rather than on message text.
115
+ Governed Action conflicts (a stale record version or approval, a record that
116
+ already has an open request of the Action, a moved approval step) exit `3`,
117
+ by code or by the refusal's `reason`; an idempotency key reused for another
118
+ request exits `2` (`exit.ts`).
115
119
 
116
120
  ## Project configuration
117
121
 
@@ -137,6 +141,11 @@ gitignored `.frontera/state.json`. Existing projects that already use
137
141
  Next Apps consume the published `@frontera-sdk/core` and
138
142
  `@frontera-sdk/blueprint` packages. Existing Vite Apps retain vendored SDK
139
143
  source; `frontera app sdk sync` refreshes only that generated tree.
144
+ On a local runtime started from a platform checkout (source mode),
145
+ `frontera app sdk use-local` packs the checkout's `@frontera-sdk/*` packages
146
+ into the App's `vendor/frontera-sdk/` and points `package.json` at them; commit
147
+ that folder, and `--npm` switches back. It is a local development command and
148
+ is kept out of the customer CLI reference.
140
149
 
141
150
  ## What `app init` does
142
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.52.38",
3
+ "version": "1.52.40",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -40,14 +40,16 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.3.251",
42
42
  "@faker-js/faker": "10.6.0",
43
- "@frontera-sdk/core": "1.52.38",
44
- "@frontera-sdk/functions": "1.52.38",
43
+ "@frontera-sdk/core": "1.52.40",
44
+ "@frontera-sdk/functions": "1.52.40",
45
45
  "ai": "^6.0.116",
46
+ "axe-core": "4.12.1",
46
47
  "gray-matter": "^4.0.3",
48
+ "playwright-core": "1.63.0",
47
49
  "yaml": "^2.9.0"
48
50
  },
49
51
  "devDependencies": {
50
- "@frontera-sdk/forge-contracts": "1.52.38",
52
+ "@frontera-sdk/forge-contracts": "1.52.40",
51
53
  "@types/bun": "^1.3.14",
52
54
  "typescript": "^5.9.3"
53
55
  }
package/src/adopt.ts CHANGED
@@ -3,6 +3,7 @@ import { join } from 'node:path'
3
3
 
4
4
  import { UsageError } from './errors'
5
5
  import { nextSkillFiles } from './templates/next-skills'
6
+ import { SDK_RANGE } from './templates/next-app-files'
6
7
 
7
8
  /**
8
9
  * Turn an existing Next.js project into a Frontera App.
@@ -167,8 +168,8 @@ export interface AdoptionPlan {
167
168
  * project rather than by reading this list.
168
169
  */
169
170
  const REQUIRED_DEPENDENCIES: Record<string, string> = {
170
- '@frontera-sdk/blueprint': '^1.43.10',
171
- '@frontera-sdk/core': '^1.43.10',
171
+ '@frontera-sdk/blueprint': SDK_RANGE,
172
+ '@frontera-sdk/core': SDK_RANGE,
172
173
  '@tanstack/react-query': '^5.90.21',
173
174
  'class-variance-authority': '^0.7.1',
174
175
  clsx: '^2.1.1',
@@ -193,6 +194,8 @@ const NEXT_CONFIG_KEYS = [
193
194
  ['trailingSlash', 'true'],
194
195
  ['images', '{ unoptimized: true }'],
195
196
  ['transpilePackages', "['@frontera-sdk/core', '@frontera-sdk/blueprint', '@frontera-sdk/agents']"],
197
+ // Without it `next dev` rewrites AGENTS.md on every run, and a deploy refuses the build as stale.
198
+ ['agentRules', 'false'],
196
199
  ] as const
197
200
 
198
201
  export function planAdoption(target: AdoptionTarget): AdoptionPlan {
@@ -322,6 +325,7 @@ const nextConfig: NextConfig = {
322
325
  trailingSlash: true,
323
326
  images: { unoptimized: true },
324
327
  transpilePackages: ['@frontera-sdk/core', '@frontera-sdk/blueprint', '@frontera-sdk/agents'],
328
+ agentRules: false,
325
329
  }
326
330
 
327
331
  export default nextConfig
@@ -27,6 +27,49 @@ export interface AppVersionSummary {
27
27
  } | null
28
28
  }
29
29
 
30
+ export interface DeploymentCheck {
31
+ id: 'build' | 'render' | 'contract' | 'a11y'
32
+ label: string
33
+ status: 'passed' | 'failed' | 'missing'
34
+ detail: string
35
+ required: boolean
36
+ }
37
+
38
+ export interface DeploymentRow {
39
+ id: string
40
+ label: string | null
41
+ producer: { kind: 'person' | 'workspace-key' | 'agent-key'; id: string; name: string | null }
42
+ session: { id: string; harness: string | null } | null
43
+ branch: string | null
44
+ commit: string | null
45
+ dirty: boolean
46
+ commitAuthor: string | null
47
+ changeRequestId: string | null
48
+ createdAt: string
49
+ productionAt: string | null
50
+ staged: boolean
51
+ pruned: { at: string; restorable: boolean } | null
52
+ /** `production`, `branch:<name>`, `session:<id>`. */
53
+ serves: string[]
54
+ renderEvidence: Record<string, unknown>
55
+ fileCount: number
56
+ totalBytes: number
57
+ url: string | null
58
+ }
59
+
60
+ export interface DeploymentLedger {
61
+ app: { id: string; slug: string; displayName: string }
62
+ production: {
63
+ deployment: string | null
64
+ label: string | null
65
+ pinned: { at: string; by: string | null; byName: string | null; reason: string | null; undoTo: string | null } | null
66
+ }
67
+ previews: Array<{ kind: 'branch' | 'session'; name: string; deployment: string | null; label: string | null; updatedAt: string; url: string | null }>
68
+ policy: { protectedApps: boolean; keysMayPromote: boolean; requiredChecks: string[] }
69
+ canPromote: boolean
70
+ deployments: DeploymentRow[]
71
+ }
72
+
30
73
  export interface AppTokenResponse {
31
74
  token: string
32
75
  expiresAt: number
@@ -129,12 +172,16 @@ export class AppsApi {
129
172
  }
130
173
 
131
174
  publish(appId: string, args: {
132
- version: string
175
+ /** An optional release label, unique per App. The deployment gets its own id. */
176
+ label?: string | null
177
+ /** Go live once the required checks pass (`--prod`); otherwise a preview. */
178
+ prod?: boolean
179
+ /** With `prod`: a staged production deployment, served by nothing until promoted. */
180
+ stage?: boolean
133
181
  parentVersion?: string | null
134
182
  dist: Uint8Array
135
183
  source: Uint8Array
136
184
  manifest?: unknown
137
- promote: boolean
138
185
  /** §4.4 — whether this tree had been rendered since its last source change. */
139
186
  renderEvidence?: unknown
140
187
  /** Recorded build/package metadata; never trusted for authorization. */
@@ -143,20 +190,30 @@ export class AppsApi {
143
190
  thumbnail?: Uint8Array | null
144
191
  }) {
145
192
  const form = new FormData()
146
- form.set('version', args.version)
193
+ if (args.label) form.set('label', args.label)
194
+ form.set('prod', args.prod ? 'true' : 'false')
195
+ if (args.prod && args.stage) form.set('stage', 'true')
147
196
  if (args.parentVersion) form.set('parentVersion', args.parentVersion)
148
197
  form.set('dist', new Blob([args.dist], { type: 'application/gzip' }), 'dist.tgz')
149
198
  form.set('source', new Blob([args.source], { type: 'application/gzip' }), 'src.tgz')
150
199
  if (args.manifest) form.set('manifest', JSON.stringify(args.manifest))
151
- form.set('promote', args.promote ? 'true' : 'false')
152
200
  if (args.renderEvidence) form.set('renderEvidence', JSON.stringify(args.renderEvidence))
153
201
  if (args.provenance) form.set('provenance', JSON.stringify(args.provenance))
154
202
  if (args.thumbnail && args.thumbnail.byteLength > 0) {
155
203
  form.set('thumbnail', new Blob([args.thumbnail], { type: 'image/png' }), 'thumbnail.png')
156
204
  }
157
205
  return this.multipart<{
206
+ /** The deployment's id (`dpl_…`); `version` is the same, for older services. */
207
+ deployment?: string
158
208
  version: string
209
+ label?: string | null
159
210
  promoted: boolean
211
+ staged?: boolean
212
+ /** Asked for production and refused: the permission, the pin, or a required check. */
213
+ promoteRefused?: { message: string; code: string | null; checks?: DeploymentCheck[] }
214
+ branch?: string
215
+ branchUrl?: string
216
+ sessionUrl?: string
160
217
  fileCount: number
161
218
  totalBytes: number
162
219
  /** Where a person opens this exact build — composed by the service,
@@ -208,6 +265,31 @@ export class AppsApi {
208
265
  )
209
266
  }
210
267
 
268
+ /** The deployment ledger: production and its pin, the preview links, every deployment and what serves it. */
269
+ deployments(appId: string): Promise<DeploymentLedger> {
270
+ return this.client.request<DeploymentLedger>(`/v1/platform-apps/${appId}/deployments`)
271
+ }
272
+
273
+ checks(appId: string, deployment: string): Promise<{ deployment: string; checks: DeploymentCheck[] }> {
274
+ return this.client.request(`/v1/platform-apps/${appId}/deployments/${encodeURIComponent(deployment)}/checks`)
275
+ }
276
+
277
+ promote(appId: string, body: { deployment: string; force?: boolean; reason?: string }) {
278
+ return this.client.request<{ promoted: string; label: string | null; checks: DeploymentCheck[]; forced: string[]; liveUrl?: string }>(`/v1/platform-apps/${appId}/promote`, { method: 'POST', body })
279
+ }
280
+
281
+ rollback(appId: string, body: { deployment?: string; reason: string }) {
282
+ return this.client.request<{ from: string | null; fromLabel?: string | null; to: string; label: string | null; pinned: true }>(`/v1/platform-apps/${appId}/rollback`, { method: 'POST', body })
283
+ }
284
+
285
+ undoRollback(appId: string) {
286
+ return this.client.request<{ restored: string | null; label: string | null; pinned: false }>(`/v1/platform-apps/${appId}/rollback/undo`, { method: 'POST', body: {} })
287
+ }
288
+
289
+ unpin(appId: string, reason: string) {
290
+ return this.client.request<{ pinned: false; production: string | null; productionLabel?: string | null }>(`/v1/platform-apps/${appId}/unpin`, { method: 'POST', body: { reason } })
291
+ }
292
+
211
293
  deploy(appId: string, version: string) {
212
294
  return this.client.request<{ deployedVersion: string }>(`/v1/platform-apps/${appId}/deploy`, {
213
295
  method: 'POST',
@@ -96,6 +96,8 @@ export class BlueprintAuthoringApi {
96
96
  throw new CliError(payload?.message ?? `${res.status} ${text.slice(0, 200)}`, {
97
97
  code: (payload?.code as string) ?? 'FAILURE',
98
98
  hint: detail ?? 'frontera blueprint status',
99
+ // Kept whole: `plan` reads a refusal's reason from it (`withServiceActionChecks`).
100
+ ...(payload?.details !== undefined ? { details: payload.details } : {}),
99
101
  })
100
102
  }
101
103
 
@@ -456,14 +458,29 @@ export class BlueprintAuthoringApi {
456
458
  return this.call('/v1/blueprint/actions?view=draft')
457
459
  }
458
460
 
459
- createAction(action: Record<string, unknown>, expectedRevision: number) {
460
- return this.call('/v1/blueprint/actions', { method: 'POST', body: { expectedRevision, action } })
461
+ /**
462
+ * `inherited`: the Action is as the workspace it was pulled from holds it, so
463
+ * a semantic-type conflict it has is the workspace's. The local runtime then
464
+ * takes it as the workspace runs it; any other service ignores it.
465
+ */
466
+ createAction(action: Record<string, unknown>, expectedRevision: number, inherited = false) {
467
+ return this.call('/v1/blueprint/actions', { method: 'POST', body: { expectedRevision, action, ...(inherited ? { inherited } : {}) } })
468
+ }
469
+
470
+ /**
471
+ * The checks a create or update of this Action runs, against the current
472
+ * draft, with nothing written (`POST /v1/blueprint/actions/check`). Create
473
+ * or update is the service's call, by the action's apiName. A refusal
474
+ * throws as the write would.
475
+ */
476
+ checkAction(action: Record<string, unknown>, inherited = false): Promise<{ operation: 'create' | 'update' }> {
477
+ return this.call('/v1/blueprint/actions/check', { method: 'POST', body: { action, ...(inherited ? { inherited } : {}) } })
461
478
  }
462
479
 
463
- updateAction(apiName: string, action: Record<string, unknown>, expectedRevision: number) {
480
+ updateAction(apiName: string, action: Record<string, unknown>, expectedRevision: number, inherited = false) {
464
481
  return this.call(`/v1/blueprint/actions/${encodeURIComponent(apiName)}`, {
465
482
  method: 'PUT',
466
- body: { expectedRevision, action },
483
+ body: { expectedRevision, action, ...(inherited ? { inherited } : {}) },
467
484
  })
468
485
  }
469
486
 
@@ -498,22 +515,24 @@ export class BlueprintAuthoringApi {
498
515
  return this.call('/v1/blueprint/org-workspaces')
499
516
  }
500
517
 
501
- orgAgents(): Promise<Array<{ id?: string; name?: string }>> {
518
+ /** The organization's Agents, unpublished ones too (`published`), each with its slug (`agentId`). */
519
+ orgAgents(): Promise<Array<{ id?: string; name?: string; agentId?: string; slug?: string; published?: boolean }>> {
502
520
  return this.call('/v1/blueprint/org-agents')
503
521
  }
504
522
 
505
- workspaceGrants(workspaceId: string): Promise<Array<{ objectTypeApiName?: string }>> {
523
+ workspaceGrants(workspaceId: string): Promise<Array<{ objectTypeApiName?: string; permissions?: string[] }>> {
506
524
  return this.call(`/v1/blueprint/grants/workspace/${encodeURIComponent(workspaceId)}`)
507
525
  }
508
526
 
509
- agentGrants(agentId: string): Promise<Array<{ objectTypeApiName?: string }>> {
527
+ agentGrants(agentId: string): Promise<Array<{ objectTypeApiName?: string; permissions?: string[] }>> {
510
528
  return this.call(`/v1/blueprint/grants/agent/${encodeURIComponent(agentId)}`)
511
529
  }
512
530
 
513
- grantWorkspace(workspaceId: string, objectTypeApiName: string) {
531
+ /** `permissions` sets the grant's whole list (`read`, `read_sensitive`); left out, it is `read`. */
532
+ grantWorkspace(workspaceId: string, objectTypeApiName: string, permissions?: string[]) {
514
533
  return this.call('/v1/blueprint/grants/workspace', {
515
534
  method: 'POST',
516
- body: { workspaceId, objectTypeApiName },
535
+ body: { workspaceId, objectTypeApiName, ...(permissions ? { permissions } : {}) },
517
536
  })
518
537
  }
519
538
 
@@ -524,10 +543,15 @@ export class BlueprintAuthoringApi {
524
543
  )
525
544
  }
526
545
 
527
- grantAgent(agentId: string, objectTypeApiName: string) {
546
+ /**
547
+ * `permissions` is the grant's list (`read`, `read_sensitive`, `act`); left out, it is `read`.
548
+ * A re-grant keeps an `act` the grant already holds: the platform takes it away only when
549
+ * the request names it in `removePermissions`, as `local dev` does.
550
+ */
551
+ grantAgent(agentId: string, objectTypeApiName: string, permissions?: string[]) {
528
552
  return this.call('/v1/blueprint/grants/agent', {
529
553
  method: 'POST',
530
- body: { agentId, objectTypeApiName },
554
+ body: { agentId, objectTypeApiName, ...(permissions ? { permissions } : {}) },
531
555
  })
532
556
  }
533
557
 
@@ -327,6 +327,35 @@ export class DatasetApi {
327
327
  )
328
328
  }
329
329
 
330
+ /**
331
+ * A new revision of a managed (file) dataset from a CSV whose columns now
332
+ * allow empty values the current revision refuses: `POST
333
+ * /datasets/:id/revisions/from-file`, multipart with the CSV as `file` and
334
+ * the revision it follows as `expectedRevision`. The service allows only a
335
+ * widening of nullability; an added, dropped or retyped column (or a key
336
+ * column turned nullable) is a 409 naming it, and a file that changes
337
+ * nothing a 409 `DATASET_REVISION_UNCHANGED`. A runtime that predates the
338
+ * route answers 404, said here in words.
339
+ */
340
+ async reviseFromFile(datasetId: string, path: string, expectedRevision: number): Promise<DatasetSummary> {
341
+ const form = new FormData()
342
+ form.append('file', new Blob([readFileSync(path)]), basename(path))
343
+ form.append('expectedRevision', String(expectedRevision))
344
+ try {
345
+ return await this.call<DatasetSummary>(`/v1/data-integration/datasets/${encodeURIComponent(datasetId)}/revisions/from-file`, { form })
346
+ } catch (err) {
347
+ // An unknown route may answer 404 in words, not JSON.
348
+ const unknownRoute = err instanceof SyntaxError || (err instanceof CliError && (err.code === 'NOT_FOUND' || /not found|404/i.test(err.message)) && !/dataset/i.test(err.message))
349
+ if (unknownRoute) {
350
+ throw new CliError('This Frontera release cannot cut a new revision of a file dataset.', {
351
+ code: 'NOT_SUPPORTED',
352
+ hint: 'Keep the columns as the dataset has them, or declare a column `nullable: true` in its definition before its first load. On a local runtime, `frontera local reset` creates every dataset again from its definition.',
353
+ })
354
+ }
355
+ throw err
356
+ }
357
+ }
358
+
330
359
  createBlank(payload: {
331
360
  apiName: string
332
361
  displayName: string
@@ -39,6 +39,41 @@ export interface ActionRequestSummary {
39
39
  }
40
40
 
41
41
  /** A row of `GET /v1/blueprint/governed-actions/discovery`. */
42
+ /**
43
+ * The refusal for invoking an Action with no deployment (409, `details.reason:
44
+ * ACTION_NOT_DEPLOYED`), as "Action <name> isn't deployed: <why>", or null
45
+ * when the body is some other refusal. `ACTION_FORBIDDEN` is a genuine
46
+ * authorization refusal and is left to the credential handling.
47
+ */
48
+ export function notDeployedError(details: unknown, path: string): CliError | null {
49
+ const d = (details && typeof details === 'object' ? details : null) as { reason?: unknown; deploymentStatus?: unknown; readinessReason?: unknown; details?: unknown } | null
50
+ const inner = d && d.reason === undefined && d.details && typeof d.details === 'object' ? (d.details as typeof d) : d
51
+ if (!inner || inner.reason !== 'ACTION_NOT_DEPLOYED') return null
52
+ const name = decodeURIComponent(/\/actions\/([^/?]+)/.exec(path)?.[1] ?? 'it')
53
+ const why = typeof inner.readinessReason === 'string' ? inner.readinessReason : typeof inner.deploymentStatus === 'string' ? `its deployment is ${inner.deploymentStatus}` : 'it has no active deployment'
54
+ return new CliError(`Action ${name} isn't deployed: ${why}`, {
55
+ code: 'ACTION_NOT_DEPLOYED',
56
+ hint: name === 'it' ? 'frontera action list shows each Action’s deployment and why it cannot run' : `\`frontera action deploy ${name}\` builds its write path; \`frontera action list\` shows why each Action cannot run (in a local project, \`frontera local dev\` deploys it)`,
57
+ details: { reason: 'ACTION_NOT_DEPLOYED', action: name === 'it' ? null : name, deploymentStatus: inner.deploymentStatus ?? null, readinessReason: inner.readinessReason ?? null },
58
+ })
59
+ }
60
+
61
+ /** One published Action and whether it can run, as `?view=deployable` answers. */
62
+ export interface DeployableActionEntry {
63
+ definition: { apiName: string; displayName?: string; id?: string }
64
+ contractDigest?: string
65
+ availability?: string
66
+ executable?: boolean
67
+ deploymentStatus?: string
68
+ reason?: string
69
+ /**
70
+ * What the deployed Action runs with that its owner should fix, without
71
+ * stopping it: a value it offers that the semantic type it writes refuses
72
+ * (`ACTION_SEMANTIC_TYPE_CONFLICT`). A request with that value is refused when submitted.
73
+ */
74
+ warnings?: Array<{ code: string; path?: string; message: string }>
75
+ }
76
+
42
77
  export interface ActionDiscovery {
43
78
  actionDefinitionId: string
44
79
  apiName: string
@@ -111,11 +146,18 @@ export class GovernedActionApi {
111
146
 
112
147
  if (!response.ok) {
113
148
  const body = payload as { message?: string; code?: string; details?: unknown } | null
149
+ // An Action that is published but has no deployment: said as that, not as a conflict.
150
+ const undeployed = notDeployedError(body?.details, path)
151
+ if (undeployed) throw undeployed
114
152
  const credential = credentialFailure(response.status, body?.message, { token: this.token, code: body?.code, details: body?.details })
115
153
  if (credential) throw credential
116
- throw new CliError(body?.message ?? `${response.status} from ${path}`, {
154
+ // A refusal whose reason is only in its issues ("Mutation Plan is invalid.") says the reason in the message.
155
+ const issues = issueLines(body?.details)
156
+ const said = body?.message ?? `${response.status} from ${path}`
157
+ throw new CliError(issues.length ? `${said} ${issues.join('; ')}` : said, {
117
158
  code: body?.code ?? 'FAILURE',
118
- ...(body?.details ? { hint: JSON.stringify(body.details) } : {}),
159
+ ...(body?.details ? { hint: issues.length ? issues.join('\n') : JSON.stringify(body.details) } : {}),
160
+ ...(body?.details ? { details: body.details } : {}),
119
161
  // The status is what separates "refused" from "unknown": `action
120
162
  // submit` offers a retry only for the second.
121
163
  cause: { status: response.status, details: body?.details },
@@ -275,6 +317,27 @@ export class GovernedActionApi {
275
317
  return this.call('/v1/blueprint/actions')
276
318
  }
277
319
 
320
+ /**
321
+ * Every published Action with its readiness, the inert ones included —
322
+ * `?view=deployable`. The plain list keeps only Actions that can be
323
+ * invoked, so one published and switched off was missing from it. Null
324
+ * when the service predates the view (it refuses the query).
325
+ */
326
+ async listDeployableActions(): Promise<DeployableActionEntry[] | null> {
327
+ try {
328
+ return await this.call<DeployableActionEntry[]>('/v1/blueprint/actions?view=deployable')
329
+ } catch (err) {
330
+ const status = ((err as { cause?: { status?: number } }).cause)?.status
331
+ if (status === 400 || status === 422) return null
332
+ throw err
333
+ }
334
+ }
335
+
336
+ /** The Actions authored on the draft, published or not — `?view=draft`. */
337
+ listDraftActions(): Promise<Array<Record<string, unknown>>> {
338
+ return this.call('/v1/blueprint/actions?view=draft')
339
+ }
340
+
278
341
  /**
279
342
  * The ACTIVE view, never the draft: a deployment is checked against the
280
343
  * release the write path will actually read, and a draft-only property would
@@ -433,3 +496,28 @@ export class GovernedActionApi {
433
496
  return this.call(`/v1/blueprint/governed-actions/admin/bindings${query}`)
434
497
  }
435
498
  }
499
+
500
+ /**
501
+ * A validation refusal's issues (`details.issues: [{ kind?, path, message }]`,
502
+ * `kind` being `invocation_input` or `invocation` for a submission), one line
503
+ * each as `path: message`, and a stale version's `details.currentVersion` as
504
+ * the version to submit instead; empty when the details carry neither.
505
+ */
506
+ export function issueLines(details: unknown): string[] {
507
+ const d = (details && typeof details === 'object' ? details : {}) as { issues?: unknown; currentVersion?: unknown }
508
+ const lines: string[] = []
509
+ if (Array.isArray(d.issues)) {
510
+ for (const i of d.issues) {
511
+ if (!i || typeof i !== 'object') continue
512
+ const { path, message } = i as { path?: unknown; message?: unknown }
513
+ if (typeof message !== 'string' || !message) continue
514
+ const steps = Array.isArray(path) ? path.map(String) : typeof path === 'string' && path ? [path] : []
515
+ const where = steps.join('.')
516
+ lines.push(where ? `${where}: ${message}` : message)
517
+ }
518
+ }
519
+ if (typeof d.currentVersion === 'number' || typeof d.currentVersion === 'string') {
520
+ lines.push(`the record is now at version ${String(d.currentVersion)}: read it again and submit with expectedVersion ${String(d.currentVersion)}`)
521
+ }
522
+ return [...new Set(lines)].slice(0, 8)
523
+ }
@@ -1,5 +1,6 @@
1
1
  import { FronteraClient } from '@frontera-sdk/core/client'
2
2
 
3
+ import type { MetricQueryBody, MetricQueryResult } from '../blueprint/metric-query'
3
4
  import { organizationHeaders, selectedOrganization } from '../organization'
4
5
 
5
6
  import { CliError } from '../errors'
@@ -22,6 +23,22 @@ function statusCode(status: number): string {
22
23
  return 'FAILURE'
23
24
  }
24
25
 
26
+ /**
27
+ * How a knowledge search cut its results. The cutoff is a maximum cosine
28
+ * distance (0 to 2, lower is stricter) while `score` is higher-is-better:
29
+ * with similarity scoring a score is `1 - distance / 2`, so the cutoff lets
30
+ * through scores above `minScore`.
31
+ */
32
+ export interface KnowledgeRetrieval {
33
+ maxDistance?: number
34
+ /** `base`: the base's own match threshold; `request`: `--threshold`. */
35
+ thresholdFrom?: 'base' | 'request'
36
+ scoring?: 'similarity' | 'fusion' | 'rerank'
37
+ minScore?: number | null
38
+ /** The nearest chunk's distance when none was kept; null when the base has nothing searchable. */
39
+ nearestDistance?: number | null
40
+ }
41
+
25
42
  export interface AgentDraft {
26
43
  agentId?: string
27
44
  revision: number
@@ -120,12 +137,49 @@ export class PlatformApi {
120
137
  return this.getList<unknown>('/v1/blueprint/metrics')
121
138
  }
122
139
 
140
+ /** One published metric, with its definition (measures, dimensions, timeseries). */
141
+ blueprintMetric(apiName: string) {
142
+ return this.get<unknown>(`/v1/blueprint/metrics/${encodeURIComponent(apiName)}`)
143
+ }
144
+
145
+ /** A metric's numbers, as an App's `useMetric` and an Agent's `blueprint_query_metric` read them (`blueprint/metric-query.ts`). */
146
+ blueprintMetricQuery(apiName: string, body: MetricQueryBody) {
147
+ return this.client.request<MetricQueryResult>(`/v1/blueprint/metrics/${encodeURIComponent(apiName)}/query`, { method: 'POST', body })
148
+ }
149
+
150
+ /**
151
+ * An aggregation over an object set (`POST /v1/blueprint/aggregate`). The
152
+ * metric command reads the newest value of a metric's timeseries with it: the
153
+ * point a `--range` window is counted back from.
154
+ */
155
+ blueprintAggregate(body: unknown) {
156
+ return this.client.request<{ rows?: Array<Record<string, unknown>> }>('/v1/blueprint/aggregate', { method: 'POST', body })
157
+ }
158
+
123
159
  /** How object types relate. Without these the catalog reads as a pile of
124
160
  * disconnected tables, and nothing can be joined. */
125
161
  blueprintLinkTypes() {
126
162
  return this.getList<unknown>('/v1/blueprint/link-types')
127
163
  }
128
164
 
165
+ /**
166
+ * The saved object sets this credential can read, as the route answered.
167
+ *
168
+ * A workspace key reads its workspace's slice (`/v1/sync/object-sets`, rows
169
+ * name their object type); an organization key is refused there without a
170
+ * workspace and reads the organization's (`/v1/blueprint/object-sets`).
171
+ * Rows are normalized by `normalizeObjectSets`.
172
+ */
173
+ async blueprintObjectSets(): Promise<unknown> {
174
+ try {
175
+ return await this.get<unknown>('/v1/sync/object-sets')
176
+ } catch (err) {
177
+ const status = (err as { status?: number }).status
178
+ if (status !== 400 && status !== 401 && status !== 403 && status !== 404) throw err
179
+ return this.get<unknown>('/v1/blueprint/object-sets')
180
+ }
181
+ }
182
+
129
183
  /**
130
184
  * Run an object-set query and return rows.
131
185
  *
@@ -148,6 +202,8 @@ export class PlatformApi {
148
202
  pageToken?: string
149
203
  /** Distance ordering from a location; checked by the service. */
150
204
  nearest?: unknown
205
+ /** Each row of an editable type carries `_meta.recordVersion`, what an Action's `expectedSubjectVersion` names. */
206
+ recordVersions?: boolean
151
207
  }) {
152
208
  return this.client.request<{
153
209
  rows?: Array<Record<string, unknown>>
@@ -175,6 +231,11 @@ export class PlatformApi {
175
231
  return this.getList<unknown>('/v1/config/agents')
176
232
  }
177
233
 
234
+ /** Every Agent in the workspace, drafts never published included (the sync listing; `agents()` lists published ones). */
235
+ allAgents() {
236
+ return this.getList<unknown>('/v1/sync/agents')
237
+ }
238
+
178
239
  agent(id: string) {
179
240
  return this.get<unknown>(`/v1/config/agents/${encodeURIComponent(id)}`)
180
241
  }
@@ -304,6 +365,21 @@ export class PlatformApi {
304
365
  })
305
366
  }
306
367
 
368
+ /** Archive an Agent: out of service, its conversations kept; `restoreAgent` puts it back. The platform's delete is this archive. */
369
+ archiveAgent(id: string) {
370
+ return this.client.request<unknown>(`/v1/config/agents/${encodeURIComponent(id)}`, {
371
+ method: 'DELETE',
372
+ })
373
+ }
374
+
375
+ /** Put an archived Agent back into service, as it was. */
376
+ restoreAgent(id: string) {
377
+ return this.client.request<unknown>(`/v1/config/agents/${encodeURIComponent(id)}/restore`, {
378
+ method: 'POST',
379
+ body: {},
380
+ })
381
+ }
382
+
307
383
  agentVersions(id: string) {
308
384
  return this.getList<unknown>(`/v1/config/agents/${encodeURIComponent(id)}/versions`)
309
385
  }
@@ -657,7 +733,7 @@ export class PlatformApi {
657
733
  async knowledgeSearch(
658
734
  baseId: string,
659
735
  body: { query: string; topK?: number; threshold?: number },
660
- ): Promise<{ results: Array<Record<string, unknown>> }> {
736
+ ): Promise<{ results: Array<Record<string, unknown>>; retrieval?: KnowledgeRetrieval }> {
661
737
  const res = await this.client.request<unknown>(
662
738
  `/v1/workspace-knowledge/${encodeURIComponent(baseId)}/search`,
663
739
  { method: 'POST', body },
@@ -669,7 +745,16 @@ export class PlatformApi {
669
745
  // search; it went unnoticed because the first base tried was empty, which
670
746
  // produces the identical output.
671
747
  const rows = (payload.results ?? payload.chunks) as Array<Record<string, unknown>> | undefined
672
- return { results: Array.isArray(rows) ? rows : [] }
748
+ // How the cutoff applied and how to read `score` against it; absent from a service that predates it.
749
+ const retrieval = payload.retrieval && typeof payload.retrieval === 'object' ? (payload.retrieval as KnowledgeRetrieval) : undefined
750
+ return { results: Array.isArray(rows) ? rows : [], ...(retrieval ? { retrieval } : {}) }
751
+ }
752
+
753
+ /** The organization's embedding model (`/v1/config/embedding`); both null when it has chosen none. */
754
+ async embeddingSetting(): Promise<{ provider: string | null; model: string | null }> {
755
+ const body = PlatformApi.inner<{ provider?: unknown; model?: unknown } | null>(await this.get<unknown>('/v1/config/embedding'))
756
+ const text = (v: unknown) => (typeof v === 'string' && v ? v : null)
757
+ return { provider: text(body?.provider), model: text(body?.model) }
673
758
  }
674
759
 
675
760
  /**