@frontera-sdk/cli 1.44.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -1
- package/package.json +4 -3
- package/src/api/automation-api.ts +15 -0
- package/src/api/dataset-api.ts +99 -0
- package/src/api/governed-action-api.ts +80 -0
- package/src/api/platform-api.ts +293 -0
- package/src/auth-verify.ts +105 -0
- package/src/binding-registry.ts +87 -0
- package/src/commands/action/deploy.ts +1 -0
- package/src/commands/action/grant.ts +1 -0
- package/src/commands/action/index-commands.ts +8 -0
- package/src/commands/action/prepare.ts +1 -0
- package/src/commands/action/requests.ts +111 -0
- package/src/commands/action/review.ts +1 -0
- package/src/commands/agent/index-commands.ts +189 -7
- package/src/commands/app/init.ts +1 -1
- package/src/commands/app/pull.ts +1 -1
- package/src/commands/auth/add.ts +145 -0
- package/src/commands/auth/current.ts +82 -0
- package/src/commands/auth/index-commands.ts +16 -0
- package/src/commands/auth/list.ts +71 -0
- package/src/commands/auth/remove.ts +80 -0
- package/src/commands/auth/use.ts +84 -0
- package/src/commands/auth/verify.ts +93 -0
- package/src/commands/automation/run.ts +41 -2
- package/src/commands/blueprint/query.ts +294 -0
- package/src/commands/capability/index-commands.ts +334 -0
- package/src/commands/dataset/index-commands.ts +103 -14
- package/src/commands/kit/doctor.ts +101 -0
- package/src/commands/kit/index-commands.ts +7 -0
- package/src/commands/kit/shared.ts +52 -0
- package/src/commands/kit/status.ts +92 -0
- package/src/commands/kit/sync.ts +106 -0
- package/src/commands/kit/vendor.ts +120 -0
- package/src/commands/knowledge/index-commands.ts +165 -0
- package/src/commands/login.ts +64 -84
- package/src/commands/plugin/index-commands.ts +284 -21
- package/src/commands/registry.ts +104 -1
- package/src/commands/setup.ts +248 -0
- package/src/commands/source/index-commands.ts +446 -0
- package/src/commands/types.ts +14 -0
- package/src/config.ts +197 -100
- package/src/credential-store.ts +273 -0
- package/src/dev-env.ts +3 -3
- package/src/exit.ts +29 -2
- package/src/flag-help.ts +65 -3
- package/src/fs-atomic.ts +44 -0
- package/src/harness.ts +155 -4
- package/src/kit.ts +419 -0
- package/src/main.ts +13 -1
- package/src/paths.ts +43 -0
- package/src/profile-migration.ts +101 -0
- package/src/profiles.ts +240 -0
- package/src/project-context.ts +178 -0
- package/src/prompt.ts +23 -0
- package/src/templates/next-app-files.ts +4 -1
- package/src/vendor/kit-assets.json +31 -0
- package/src/vendor/sdk-sources.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ bunx @frontera-sdk/cli --help
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
frontera
|
|
10
|
+
frontera auth add acme-prod --api-url https://api.frontera.example --from -
|
|
11
|
+
frontera auth use acme-prod
|
|
11
12
|
frontera app init shipments-console
|
|
12
13
|
cd shipments-console && bun install
|
|
13
14
|
frontera app dev
|
|
@@ -19,6 +20,69 @@ key never enters browser JavaScript or `.env.local`; the browser receives only
|
|
|
19
20
|
a short-lived, App-scoped token. Existing Vite Apps remain supported with
|
|
20
21
|
`--framework react` and their legacy `dev-host.html` flow.
|
|
21
22
|
|
|
23
|
+
## One machine, many customers
|
|
24
|
+
|
|
25
|
+
A credential is a **profile**, and the working directory selects which profile
|
|
26
|
+
is in play. Two keys for the same deployment coexist under different names, and
|
|
27
|
+
a command run in a customer's directory uses that customer's key with no flag.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
frontera auth add acme-prod --api-url https://api.frontera.example --from -
|
|
31
|
+
frontera auth add globex-dev --api-url https://api.frontera.example --from -
|
|
32
|
+
|
|
33
|
+
cd ~/Customers/acme && frontera auth use acme-prod
|
|
34
|
+
cd ~/Customers/globex && frontera auth use globex-dev
|
|
35
|
+
|
|
36
|
+
frontera auth current --json # which profile, which origin, which scope
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`auth use` writes a gitignored `.frontera/context.json` holding a profile
|
|
40
|
+
**name** — never a token and never an origin. Resolution walks up from the
|
|
41
|
+
working directory and the nearest binding wins, so a nested customer project
|
|
42
|
+
safely overrides its parent.
|
|
43
|
+
|
|
44
|
+
Precedence is explicit over inferred: `--profile` beats `FRONTERA_PROFILE`,
|
|
45
|
+
which beats the directory. `--api-url` must agree with the selected profile's
|
|
46
|
+
origin — a profile's key is never sent to another deployment. `FRONTERA_TOKEN`
|
|
47
|
+
still works for CI and the Computer, and requires an explicit origin alongside
|
|
48
|
+
it for the same reason.
|
|
49
|
+
|
|
50
|
+
Keys live in the OS credential store addressed by profile; `config.json` holds
|
|
51
|
+
metadata and a short fingerprint, never key bytes. A version-1 configuration
|
|
52
|
+
migrates on first use, verifying every copied secret before the old plaintext
|
|
53
|
+
is dropped. `frontera login` remains as a deprecated alias that manages the
|
|
54
|
+
`default` profile.
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `frontera auth add <profile>` | verify a key and store it (stdin or no-echo prompt only) |
|
|
59
|
+
| `frontera auth use <profile>` | bind this directory tree — no initialization needed |
|
|
60
|
+
| `frontera auth current` | the resolved profile, origin, scope and provenance |
|
|
61
|
+
| `frontera auth list` / `verify` / `remove` | what exists, is it still valid, delete it |
|
|
62
|
+
|
|
63
|
+
## Agent harness
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
frontera setup # every coding host installed here
|
|
67
|
+
frontera setup codex
|
|
68
|
+
frontera setup claude
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Codex and Claude Code both install plugins from a marketplace, and both accept a
|
|
72
|
+
local directory as one. The CLI carries the Frontera skills pinned at build
|
|
73
|
+
time, so `setup` materializes a marketplace under
|
|
74
|
+
`~/.local/share/frontera/harness` and points the host at it — nothing published,
|
|
75
|
+
nothing hosted, no network. Upgrading the CLI upgrades the plugin.
|
|
76
|
+
|
|
77
|
+
Nothing needs to be written into a repository. For teams that want the
|
|
78
|
+
instructions versioned with the code — or for Windows checkouts, archives and
|
|
79
|
+
cloud sandboxes — `frontera kit vendor` writes both host projections
|
|
80
|
+
(`.agents/skills` and `.claude/skills`), a bounded managed block in `AGENTS.md`,
|
|
81
|
+
a `CLAUDE.md` importing `@AGENTS.md`, and a committable
|
|
82
|
+
`frontera.kit.lock.json`. `kit status`, `kit sync` and `kit doctor` report
|
|
83
|
+
drift, update generated files without discarding local edits, and diagnose a
|
|
84
|
+
skill reachable from two sources at once.
|
|
85
|
+
|
|
22
86
|
## The commands
|
|
23
87
|
|
|
24
88
|
`frontera <noun> <verb>`. Note that `frontera init` (prepare an existing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"test": "bun test",
|
|
35
35
|
"typecheck": "bunx tsc --noEmit",
|
|
36
36
|
"sync:sdk": "bun run scripts/sync-sdk.ts",
|
|
37
|
+
"sync:kit": "bun run scripts/sync-kit.ts",
|
|
37
38
|
"build:release": "bun run scripts/build-release.ts"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@frontera-sdk/automation": "1.
|
|
41
|
-
"@frontera-sdk/core": "1.
|
|
41
|
+
"@frontera-sdk/automation": "1.45.0",
|
|
42
|
+
"@frontera-sdk/core": "1.45.0",
|
|
42
43
|
"gray-matter": "^4.0.3",
|
|
43
44
|
"yaml": "^2.9.0"
|
|
44
45
|
},
|
|
@@ -232,6 +232,21 @@ export class AutomationApi {
|
|
|
232
232
|
)
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* One run, by id.
|
|
237
|
+
*
|
|
238
|
+
* `runs` lists, and a listing is where a failure is NOTICED — the message it
|
|
239
|
+
* carries is what the handler threw, and everything about how the run got
|
|
240
|
+
* there is in the steps. Fetching the run itself is what makes those two
|
|
241
|
+
* readable together after the fact, rather than only in the tail of a list
|
|
242
|
+
* that has since scrolled.
|
|
243
|
+
*/
|
|
244
|
+
runById(slug: string, runId: string): Promise<AutomationRunSummary> {
|
|
245
|
+
return this.client.request<AutomationRunSummary>(
|
|
246
|
+
`/v1/automations/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}`,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
/** One run's trail. Workspace-scoped server-side, so a guessed id reads
|
|
236
251
|
* nothing. */
|
|
237
252
|
runSteps(runId: string): Promise<AutomationRunStep[]> {
|
package/src/api/dataset-api.ts
CHANGED
|
@@ -127,10 +127,109 @@ export class DatasetApi {
|
|
|
127
127
|
)
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Rows of a published revision — the read half of every create form above.
|
|
132
|
+
*
|
|
133
|
+
* `dataset get` shows the column CONTRACT, which is what a Blueprint binding
|
|
134
|
+
* pins, and says nothing about whether a single row is behind it. A `source`
|
|
135
|
+
* dataset whose relation went empty and a `blank` one that never carried
|
|
136
|
+
* rows present identically there, and both bind without complaint, so the
|
|
137
|
+
* contract alone cannot answer "is there data".
|
|
138
|
+
*
|
|
139
|
+
* Capped at 100 rows a page by the service, cursor-paged. A revision is
|
|
140
|
+
* immutable, so a cursor stays valid.
|
|
141
|
+
*/
|
|
142
|
+
revisionRows(
|
|
143
|
+
revisionId: string,
|
|
144
|
+
body: { limit?: number; cursor?: string } = {},
|
|
145
|
+
): Promise<{
|
|
146
|
+
columns?: Array<{ name?: string; databaseType?: string }>
|
|
147
|
+
rows?: unknown[][]
|
|
148
|
+
nextCursor?: string | null
|
|
149
|
+
readAt?: string
|
|
150
|
+
consistency?: string
|
|
151
|
+
}> {
|
|
152
|
+
return this.call(
|
|
153
|
+
`/v1/data-integration/dataset-revisions/${encodeURIComponent(revisionId)}/rows`,
|
|
154
|
+
{ method: 'POST', body },
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
listSources(): Promise<SourceSummary[]> {
|
|
131
159
|
return this.page<SourceSummary>('/v1/data-integration/sources')
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
getSource(sourceId: string): Promise<SourceSummary & Record<string, unknown>> {
|
|
163
|
+
return this.call(`/v1/data-integration/sources/${encodeURIComponent(sourceId)}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Provision a Source, and the Datasets it carries, in one call.
|
|
168
|
+
*
|
|
169
|
+
* The step that broke every scripted org stand-up: `dataset create --from-source`
|
|
170
|
+
* NAMED a Source, and nothing could make one. Postgres only today — the
|
|
171
|
+
* service takes `connectorType: 'postgres'` as a literal, so a second
|
|
172
|
+
* connector is a service change, not a CLI flag.
|
|
173
|
+
*
|
|
174
|
+
* `config.password` never comes from the definition file. The command reads
|
|
175
|
+
* it from stdin and merges it here, for the reason `secret set` states: a
|
|
176
|
+
* credential in a committed file is a credential in the repository.
|
|
177
|
+
*/
|
|
178
|
+
provisionSource(body: {
|
|
179
|
+
displayName: string
|
|
180
|
+
description?: string | null
|
|
181
|
+
connectorType: 'postgres'
|
|
182
|
+
config: Record<string, unknown>
|
|
183
|
+
datasets: unknown[]
|
|
184
|
+
}): Promise<Record<string, unknown>> {
|
|
185
|
+
return this.call('/v1/data-integration/sources/provision', { method: 'POST', body })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Replace a Source's connection and add Datasets to it.
|
|
190
|
+
*
|
|
191
|
+
* Both concurrency tokens are required by the service and neither is
|
|
192
|
+
* optional here: `expectedUpdatedAt` and `expectedRevision` come from
|
|
193
|
+
* `source get`, and a stale pair is refused rather than merged. Datasets are
|
|
194
|
+
* ADDITIONS — omitting the field leaves every existing one in place, and
|
|
195
|
+
* there is no removal channel on this route.
|
|
196
|
+
*/
|
|
197
|
+
updateSource(
|
|
198
|
+
sourceId: string,
|
|
199
|
+
body: {
|
|
200
|
+
expectedUpdatedAt: string
|
|
201
|
+
expectedRevision: number
|
|
202
|
+
displayName: string
|
|
203
|
+
description?: string | null
|
|
204
|
+
config: Record<string, unknown>
|
|
205
|
+
datasets?: unknown[]
|
|
206
|
+
},
|
|
207
|
+
): Promise<Record<string, unknown>> {
|
|
208
|
+
return this.call(`/v1/data-integration/sources/${encodeURIComponent(sourceId)}`, {
|
|
209
|
+
method: 'PATCH',
|
|
210
|
+
body,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A new connection revision, leaving the Datasets alone. */
|
|
215
|
+
reviseSource(
|
|
216
|
+
sourceId: string,
|
|
217
|
+
body: { expectedRevision: number; config: Record<string, unknown> },
|
|
218
|
+
): Promise<Record<string, unknown>> {
|
|
219
|
+
return this.call(`/v1/data-integration/sources/${encodeURIComponent(sourceId)}/revisions`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
body,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Stops reads without deleting. Datasets bound to it stop refreshing. */
|
|
226
|
+
disableSource(sourceId: string, body: { expectedUpdatedAt: string }): Promise<unknown> {
|
|
227
|
+
return this.call(`/v1/data-integration/sources/${encodeURIComponent(sourceId)}/disable`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
134
233
|
/** Columns and a sample, read from the CSV before anything is created. */
|
|
135
234
|
async uploadPreview(path: string): Promise<UploadPreview> {
|
|
136
235
|
const form = new FormData()
|
|
@@ -13,6 +13,29 @@ import type { ObjectTypeShape, PublishedActionDefinition } from '../blueprint/on
|
|
|
13
13
|
|
|
14
14
|
interface Envelope<T> { error?: boolean; data?: T }
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A row of `GET /v1/blueprint/governed-actions/requests`.
|
|
18
|
+
*
|
|
19
|
+
* Transcribed from a real response, not guessed. The list projection carries
|
|
20
|
+
* no action apiName and no failure text — an earlier version of this interface
|
|
21
|
+
* declared both and rendered two permanently empty columns. `effectCertainty`
|
|
22
|
+
* is the field that actually matters on a failure: it says whether the effect
|
|
23
|
+
* reached the far side, which is the difference between "retry" and "do not".
|
|
24
|
+
*/
|
|
25
|
+
export interface ActionRequestSummary {
|
|
26
|
+
id?: string
|
|
27
|
+
lifecycle?: string
|
|
28
|
+
/** The definition's uuid. The list does not resolve it to an apiName. */
|
|
29
|
+
actionDefinitionId?: string
|
|
30
|
+
actionContractDigest?: string
|
|
31
|
+
effectCertainty?: string
|
|
32
|
+
hasEverDispatched?: boolean
|
|
33
|
+
subjectRef?: unknown
|
|
34
|
+
createdAt?: string
|
|
35
|
+
updatedAt?: string
|
|
36
|
+
deadlineAt?: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
export class GovernedActionApi {
|
|
17
40
|
constructor(private readonly apiUrl: string, private readonly token: string) {}
|
|
18
41
|
|
|
@@ -42,6 +65,63 @@ export class GovernedActionApi {
|
|
|
42
65
|
: payload) as T
|
|
43
66
|
}
|
|
44
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Requests raised against the Actions in this organization.
|
|
70
|
+
*
|
|
71
|
+
* The read half of a plane the CLI could only arm. `action list` says an
|
|
72
|
+
* Action is deployed and `action review` says the write path is active;
|
|
73
|
+
* neither says whether anything has ever run through it, or what happened
|
|
74
|
+
* when it did.
|
|
75
|
+
*
|
|
76
|
+
* Reads only. Invoking stays absent for the reason this module's header
|
|
77
|
+
* gives — both CLI credential kinds present a non-member principal, and the
|
|
78
|
+
* invoke check joins organization membership. The LIST gate is different:
|
|
79
|
+
* `actionRequest: ['read']`, which a key can hold.
|
|
80
|
+
*
|
|
81
|
+
* `nextCursor` rides on the envelope beside `data`, so this cannot go
|
|
82
|
+
* through `call` — that unwraps to `data` and would drop the cursor,
|
|
83
|
+
* silently capping every caller at one page.
|
|
84
|
+
*/
|
|
85
|
+
async listRequests(query: { lifecycle?: string; limit?: number; cursor?: string } = {}): Promise<{
|
|
86
|
+
requests: ActionRequestSummary[]
|
|
87
|
+
nextCursor?: string
|
|
88
|
+
}> {
|
|
89
|
+
const params = new URLSearchParams()
|
|
90
|
+
if (query.lifecycle) params.set('lifecycle', query.lifecycle)
|
|
91
|
+
if (query.limit !== undefined) params.set('limit', String(query.limit))
|
|
92
|
+
if (query.cursor) params.set('cursor', query.cursor)
|
|
93
|
+
const suffix = params.size > 0 ? `?${params}` : ''
|
|
94
|
+
|
|
95
|
+
const response = await fetch(
|
|
96
|
+
`${this.apiUrl}/v1/blueprint/governed-actions/requests${suffix}`,
|
|
97
|
+
{ headers: { authorization: `Bearer ${this.token}` } },
|
|
98
|
+
)
|
|
99
|
+
const text = await response.text()
|
|
100
|
+
let payload: unknown
|
|
101
|
+
try { payload = text ? JSON.parse(text) : null } catch { payload = null }
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const body = payload as { message?: string; code?: string } | null
|
|
105
|
+
throw new CliError(body?.message ?? `${response.status} from /governed-actions/requests`, {
|
|
106
|
+
code: body?.code ?? 'FAILURE',
|
|
107
|
+
...(response.status === 403
|
|
108
|
+
? { hint: 'the credential needs actionRequest:read — mint a key that carries it' }
|
|
109
|
+
: {}),
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const envelope = payload as { data?: ActionRequestSummary[]; nextCursor?: string } | null
|
|
114
|
+
return {
|
|
115
|
+
requests: envelope?.data ?? [],
|
|
116
|
+
...(envelope?.nextCursor ? { nextCursor: envelope.nextCursor } : {}),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One request, with everything the surface records about how it resolved. */
|
|
121
|
+
getRequest(requestId: string): Promise<Record<string, unknown>> {
|
|
122
|
+
return this.call(`/v1/blueprint/governed-actions/requests/${encodeURIComponent(requestId)}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
45
125
|
/** The published Action: its definition and the digest a Binding pins. */
|
|
46
126
|
publishedAction(apiName: string): Promise<{
|
|
47
127
|
definition: PublishedActionDefinition
|
package/src/api/platform-api.ts
CHANGED
|
@@ -97,6 +97,47 @@ export class PlatformApi {
|
|
|
97
97
|
return this.getList<unknown>('/v1/blueprint/link-types')
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Run an object-set query and return rows.
|
|
102
|
+
*
|
|
103
|
+
* The counterpart to nineteen authoring verbs that had none: a caller could
|
|
104
|
+
* create an object type, bind it to a dataset and publish a release without
|
|
105
|
+
* ever seeing a row come back, so a binding that resolved to nothing looked
|
|
106
|
+
* exactly like one that worked.
|
|
107
|
+
*
|
|
108
|
+
* `objectSet` is the expression tree, not a table name — `{ type: 'base',
|
|
109
|
+
* objectType }` is the whole of the simple case and the command builds it.
|
|
110
|
+
* Paging is cursor-based: hand `nextPageToken` back as `pageToken`. There is
|
|
111
|
+
* no offset form; the service refuses `page` rather than silently serving
|
|
112
|
+
* page 1 forever.
|
|
113
|
+
*/
|
|
114
|
+
blueprintQuery(body: {
|
|
115
|
+
objectSet: unknown
|
|
116
|
+
select?: string[]
|
|
117
|
+
orderBy?: Array<{ property: string; dir: 'asc' | 'desc' }>
|
|
118
|
+
pageSize?: number
|
|
119
|
+
pageToken?: string
|
|
120
|
+
}) {
|
|
121
|
+
return this.client.request<{
|
|
122
|
+
rows?: Array<Record<string, unknown>>
|
|
123
|
+
nextPageToken?: string | null
|
|
124
|
+
}>('/v1/blueprint/query', { method: 'POST', body })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One object by primary key.
|
|
129
|
+
*
|
|
130
|
+
* Separate from `blueprintQuery` because it is the check a caller actually
|
|
131
|
+
* runs after a bind — "is this specific record reachable" — and expressing
|
|
132
|
+
* it as a filtered query means constructing a where-node to ask a question
|
|
133
|
+
* with a one-line answer.
|
|
134
|
+
*/
|
|
135
|
+
blueprintInstance(apiName: string, pk: string) {
|
|
136
|
+
return this.get<unknown>(
|
|
137
|
+
`/v1/blueprint/object-types/${encodeURIComponent(apiName)}/instances/${encodeURIComponent(pk)}`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
100
141
|
// ── Agents ────────────────────────────────────────────────────────────────
|
|
101
142
|
|
|
102
143
|
agents() {
|
|
@@ -267,6 +308,217 @@ export class PlatformApi {
|
|
|
267
308
|
return this.getList<unknown>('/v1/apps/catalog')
|
|
268
309
|
}
|
|
269
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Strip the SECOND envelope.
|
|
313
|
+
*
|
|
314
|
+
* The apps router answers `ok({ data: … })`, so after the SDK unwraps the
|
|
315
|
+
* outer `{ data, message, error }` a second `{ data }` is left. Reading a
|
|
316
|
+
* field off that layer silently yields undefined — which is how `plugin add`
|
|
317
|
+
* printed the success text for an OAuth install whose `authUrl` it had, and
|
|
318
|
+
* `plugin verify` rendered a one-row table containing a JSON blob.
|
|
319
|
+
*/
|
|
320
|
+
private static inner<T>(body: unknown): T {
|
|
321
|
+
const wrapper = body as { data?: T } | null
|
|
322
|
+
return (wrapper && typeof wrapper === 'object' && 'data' in wrapper
|
|
323
|
+
? (wrapper.data as T)
|
|
324
|
+
: (body as T))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async plugin(installId: string): Promise<Record<string, unknown>> {
|
|
328
|
+
const body = await this.get<unknown>(`/v1/apps/${encodeURIComponent(installId)}`)
|
|
329
|
+
return PlatformApi.inner<Record<string, unknown>>(body) ?? {}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Install a plugin.
|
|
334
|
+
*
|
|
335
|
+
* `kind` is the catalog entry, not a display name — `frontera plugin catalog`
|
|
336
|
+
* is where it comes from. An OAuth-backed install answers with an `authUrl`
|
|
337
|
+
* and lands unusable until a person opens it, which is why `plugin connect`
|
|
338
|
+
* stays reserved: the CLI can start that handshake and cannot finish it.
|
|
339
|
+
*/
|
|
340
|
+
async createPluginInstall(body: {
|
|
341
|
+
kind: string
|
|
342
|
+
installName?: string
|
|
343
|
+
description?: string | null
|
|
344
|
+
config?: Record<string, unknown>
|
|
345
|
+
}): Promise<{ install?: Record<string, unknown>; authUrl?: string } & Record<string, unknown>> {
|
|
346
|
+
// `config` is REQUIRED by the route, not optional. Omitting it when the
|
|
347
|
+
// caller passed no --file answered a bare "Validation failed", so a plugin
|
|
348
|
+
// whose kind needs no configuration — every OAuth one, since the consent
|
|
349
|
+
// flow supplies it — could not be installed from the CLI at all.
|
|
350
|
+
const raw = await this.client.request<unknown>('/v1/apps/', {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
body: { config: {}, ...body },
|
|
353
|
+
})
|
|
354
|
+
// Two shapes behind one envelope: `created({ data: { install, authUrl } })`
|
|
355
|
+
// when consent is pending, `created({ data: install })` when it is not. The
|
|
356
|
+
// caller reads `authUrl` to decide what to print, so both must land on the
|
|
357
|
+
// same level.
|
|
358
|
+
return PlatformApi.inner(raw) ?? {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Rename, or replace stored configuration. */
|
|
362
|
+
updatePluginInstall(installId: string, body: { installName?: string; config?: Record<string, unknown> }) {
|
|
363
|
+
return this.client.request<unknown>(`/v1/apps/${encodeURIComponent(installId)}`, {
|
|
364
|
+
method: 'PUT',
|
|
365
|
+
body,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Policy, not configuration: whether the install is on, whether its Actions
|
|
371
|
+
* may run at all, and what a newly discovered capability defaults to.
|
|
372
|
+
*/
|
|
373
|
+
patchPluginInstall(
|
|
374
|
+
installId: string,
|
|
375
|
+
body: {
|
|
376
|
+
enabled?: boolean
|
|
377
|
+
actionPolicy?: 'allow_all' | 'read_only' | 'custom'
|
|
378
|
+
newCapabilityDefault?: 'enabled' | 'disabled'
|
|
379
|
+
},
|
|
380
|
+
) {
|
|
381
|
+
return this.client.request<unknown>(`/v1/apps/${encodeURIComponent(installId)}`, {
|
|
382
|
+
method: 'PATCH',
|
|
383
|
+
body,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
deletePluginInstall(installId: string) {
|
|
388
|
+
return this.client.request<unknown>(`/v1/apps/${encodeURIComponent(installId)}`, {
|
|
389
|
+
method: 'DELETE',
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Reachability, credentials and scopes.
|
|
395
|
+
*
|
|
396
|
+
* A failed check still answers 200 — the handler returns `ok(...)`
|
|
397
|
+
* unconditionally and the verdict is in the body, the same shape
|
|
398
|
+
* `dataset test-source` established.
|
|
399
|
+
*/
|
|
400
|
+
async verifyPluginInstall(installId: string): Promise<Record<string, unknown>> {
|
|
401
|
+
const raw = await this.client.request<unknown>(
|
|
402
|
+
`/v1/apps/${encodeURIComponent(installId)}/verify`,
|
|
403
|
+
{ method: 'POST' },
|
|
404
|
+
)
|
|
405
|
+
return PlatformApi.inner<Record<string, unknown>>(raw) ?? {}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Everything one install exposes, whether or not an agent holds it. */
|
|
409
|
+
pluginCapabilities(installId: string) {
|
|
410
|
+
return this.getList<unknown>(`/v1/apps/${encodeURIComponent(installId)}/capabilities`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Agent capabilities ────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* What this agent may actually do.
|
|
417
|
+
*
|
|
418
|
+
* Reads the DRAFT when one exists and the live snapshot otherwise, which is
|
|
419
|
+
* the same rule the grant below writes under — so a list taken straight after
|
|
420
|
+
* a grant shows it, and `agent diff` is where the staged-vs-live difference
|
|
421
|
+
* is visible.
|
|
422
|
+
*/
|
|
423
|
+
agentCapabilities(agentId: string) {
|
|
424
|
+
return this.getList<unknown>(`/v1/apps/agents/${encodeURIComponent(agentId)}/capabilities`)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Grant one capability to an agent.
|
|
429
|
+
*
|
|
430
|
+
* Two things this does that its name does not say. It STAGES onto the agent
|
|
431
|
+
* draft rather than writing live — nothing changes for the running agent
|
|
432
|
+
* until `frontera agent publish`. And it auto-binds the parent install if the
|
|
433
|
+
* agent did not already hold it, so a grant is never half-applied.
|
|
434
|
+
*
|
|
435
|
+
* Refused with a conflict when the capability is an Action and the install
|
|
436
|
+
* is set to `read_only`; that is a policy on the install, fixed with
|
|
437
|
+
* `frontera plugin policy`, not by retrying.
|
|
438
|
+
*/
|
|
439
|
+
grantAgentCapability(agentId: string, capabilityId: string) {
|
|
440
|
+
return this.client.request<unknown>(
|
|
441
|
+
`/v1/apps/agents/${encodeURIComponent(agentId)}/capabilities/${encodeURIComponent(capabilityId)}`,
|
|
442
|
+
{ method: 'POST' },
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
revokeAgentCapability(agentId: string, capabilityId: string) {
|
|
447
|
+
return this.client.request<unknown>(
|
|
448
|
+
`/v1/apps/agents/${encodeURIComponent(agentId)}/capabilities/${encodeURIComponent(capabilityId)}`,
|
|
449
|
+
{ method: 'DELETE' },
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Which installs the agent is bound to, with the account mode each uses. */
|
|
454
|
+
agentInstalls(agentId: string) {
|
|
455
|
+
return this.getList<unknown>(`/v1/apps/agents/${encodeURIComponent(agentId)}/installs`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* `agent_owned` uses one credential the workspace holds; `end_user` makes
|
|
460
|
+
* every caller connect their own. The choice is not reversible by retrying
|
|
461
|
+
* with the other value — it decides whose data the agent reaches.
|
|
462
|
+
*/
|
|
463
|
+
bindAgentInstall(agentId: string, installId: string, accountMode: 'agent_owned' | 'end_user') {
|
|
464
|
+
return this.client.request<unknown>(
|
|
465
|
+
`/v1/apps/agents/${encodeURIComponent(agentId)}/installs/${encodeURIComponent(installId)}`,
|
|
466
|
+
{ method: 'POST', body: { accountMode } },
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
unbindAgentInstall(agentId: string, installId: string) {
|
|
471
|
+
return this.client.request<unknown>(
|
|
472
|
+
`/v1/apps/agents/${encodeURIComponent(agentId)}/installs/${encodeURIComponent(installId)}`,
|
|
473
|
+
{ method: 'DELETE' },
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Agent skills and versions ─────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Bind a workspace skill to an agent.
|
|
481
|
+
*
|
|
482
|
+
* The step that had no verb: `skill push` uploads a skill and nothing loaded
|
|
483
|
+
* it, so the only route from a bundle folder to a running agent went through
|
|
484
|
+
* `pack apply` or the Console.
|
|
485
|
+
*/
|
|
486
|
+
attachAgentSkill(agentId: string, skillId: string) {
|
|
487
|
+
return this.client.request<unknown>(
|
|
488
|
+
`/v1/config/agents/${encodeURIComponent(agentId)}/workspace-skills/${encodeURIComponent(skillId)}`,
|
|
489
|
+
{ method: 'POST' },
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
detachAgentSkill(agentId: string, skillId: string) {
|
|
494
|
+
return this.client.request<unknown>(
|
|
495
|
+
`/v1/config/agents/${encodeURIComponent(agentId)}/workspace-skills/${encodeURIComponent(skillId)}`,
|
|
496
|
+
{ method: 'DELETE' },
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Publish an earlier version's snapshot as a new one.
|
|
502
|
+
*
|
|
503
|
+
* Forward-only: it does not rewind history, it appends. `agent versions`
|
|
504
|
+
* listed what could be gone back to and nothing could go there, so the only
|
|
505
|
+
* recovery from a bad publish was to reconstruct the document by hand.
|
|
506
|
+
*/
|
|
507
|
+
revertAgent(agentId: string, versionId: string, body: { acceptOrphanedKnowledge?: boolean } = {}) {
|
|
508
|
+
return this.client.request<unknown>(
|
|
509
|
+
`/v1/config/agents/${encodeURIComponent(agentId)}/versions/${encodeURIComponent(versionId)}/revert`,
|
|
510
|
+
{ method: 'POST', body },
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Re-run ingestion for a source that failed. Additive and safe to repeat. */
|
|
515
|
+
retryKnowledgeSource(baseId: string, sourceId: string) {
|
|
516
|
+
return this.client.request<unknown>(
|
|
517
|
+
`/v1/workspace-knowledge/${encodeURIComponent(baseId)}/sources/${encodeURIComponent(sourceId)}/retry`,
|
|
518
|
+
{ method: 'POST', body: {} },
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
270
522
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
271
523
|
|
|
272
524
|
knowledgeBases(workspaceId: string) {
|
|
@@ -284,6 +536,47 @@ export class PlatformApi {
|
|
|
284
536
|
return this.getList<unknown>(`/v1/workspace-knowledge/${encodeURIComponent(id)}/sources`)
|
|
285
537
|
}
|
|
286
538
|
|
|
539
|
+
/**
|
|
540
|
+
* One knowledge base by id.
|
|
541
|
+
*
|
|
542
|
+
* `knowledge list` existed and `knowledge get` did not, so the only way to
|
|
543
|
+
* read back what `knowledge create` had just made was to list every base in
|
|
544
|
+
* the workspace and filter client-side.
|
|
545
|
+
*/
|
|
546
|
+
async knowledgeBase(id: string): Promise<Record<string, unknown>> {
|
|
547
|
+
const body = await this.get<{ data?: Record<string, unknown> } | Record<string, unknown>>(
|
|
548
|
+
`/v1/workspace-knowledge/${encodeURIComponent(id)}`,
|
|
549
|
+
)
|
|
550
|
+
return ((body as { data?: Record<string, unknown> })?.data ?? body ?? {}) as Record<string, unknown>
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Retrieval against a base — the read half of `knowledge upload`.
|
|
555
|
+
*
|
|
556
|
+
* Ingestion is asynchronous and a partial batch still exits 0, so "the
|
|
557
|
+
* upload succeeded" and "the corpus is searchable" are different claims and
|
|
558
|
+
* only this one can settle the second. Empty results minutes after an upload
|
|
559
|
+
* usually mean sources are still `processing`, not that retrieval is broken;
|
|
560
|
+
* the command says so rather than printing nothing.
|
|
561
|
+
*/
|
|
562
|
+
async knowledgeSearch(
|
|
563
|
+
baseId: string,
|
|
564
|
+
body: { query: string; topK?: number; threshold?: number },
|
|
565
|
+
): Promise<{ results: Array<Record<string, unknown>> }> {
|
|
566
|
+
const res = await this.client.request<unknown>(
|
|
567
|
+
`/v1/workspace-knowledge/${encodeURIComponent(baseId)}/search`,
|
|
568
|
+
{ method: 'POST', body },
|
|
569
|
+
)
|
|
570
|
+
const payload = PlatformApi.inner<Record<string, unknown>>(res) ?? {}
|
|
571
|
+
// The route answers `{ results }` — named for the retrieval orchestrator it
|
|
572
|
+
// wraps, not for the chunk table underneath. An earlier version read
|
|
573
|
+
// `chunks` and therefore reported "no chunks matched" for every successful
|
|
574
|
+
// search; it went unnoticed because the first base tried was empty, which
|
|
575
|
+
// produces the identical output.
|
|
576
|
+
const rows = (payload.results ?? payload.chunks) as Array<Record<string, unknown>> | undefined
|
|
577
|
+
return { results: Array.isArray(rows) ? rows : [] }
|
|
578
|
+
}
|
|
579
|
+
|
|
287
580
|
/**
|
|
288
581
|
* Create a workspace knowledge base.
|
|
289
582
|
*
|