@frontera-sdk/cli 0.1.0 → 1.43.6
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/package.json +4 -2
- package/src/api/apps-api.ts +13 -1
- package/src/api/automation-api.ts +129 -1
- package/src/api/blueprint-authoring-api.ts +574 -0
- package/src/api/dataset-api.ts +199 -0
- package/src/api/platform-api.ts +300 -0
- package/src/automation-template.ts +224 -0
- package/src/blueprint/compile.ts +371 -0
- package/src/blueprint/dataset-revision.ts +33 -0
- package/src/blueprint/diff.ts +223 -0
- package/src/blueprint/model.ts +227 -0
- package/src/blueprint/projection.ts +254 -0
- package/src/blueprint/render.ts +73 -0
- package/src/blueprint/scaffold.ts +79 -0
- package/src/blueprint/tree.ts +121 -0
- package/src/commands/agent/index-commands.ts +87 -1
- package/src/commands/app/deploy.ts +43 -3
- package/src/commands/app/init.ts +23 -1
- package/src/commands/app/pull.ts +12 -35
- package/src/commands/automation/index-commands.ts +42 -1
- package/src/commands/automation/init.ts +52 -0
- package/src/commands/automation/project-root.ts +58 -0
- package/src/commands/automation/pull.ts +124 -0
- package/src/commands/automation/run.ts +271 -0
- package/src/commands/blueprint/authoring.ts +410 -0
- package/src/commands/blueprint/bind.ts +228 -0
- package/src/commands/blueprint/declarative.ts +1052 -0
- package/src/commands/blueprint/grants.ts +164 -0
- package/src/commands/dataset/index-commands.ts +431 -0
- package/src/commands/knowledge/index-commands.ts +278 -27
- package/src/commands/knowledge/upload-batch.ts +146 -0
- package/src/commands/knowledge/upload-plan.ts +127 -0
- package/src/commands/login.ts +49 -11
- package/src/commands/pack/index-commands.ts +373 -0
- package/src/commands/registry.ts +19 -2
- package/src/commands/secret/index-commands.ts +195 -0
- package/src/commands/skill/bundle-commands.ts +327 -0
- package/src/commands/skill/index-commands.ts +36 -42
- package/src/commands/skill/resolve.ts +34 -0
- package/src/dev-env.ts +114 -0
- package/src/flag-help.ts +34 -0
- package/src/harness.ts +30 -3
- package/src/main.ts +10 -3
- package/src/render-evidence.ts +152 -0
- package/src/template.ts +4 -0
- package/src/untar.ts +44 -0
- package/src/vendor/sdk-sources.json +13 -11
- package/src/commands/blueprint/reserved.ts +0 -40
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { basename } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { CliError } from '../errors'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Datasets, from the CLI.
|
|
8
|
+
*
|
|
9
|
+
* The reason this exists is the one `secret` states for itself: a Blueprint object type
|
|
10
|
+
* names the dataset it reads — `backing.dataset` — and until now the CLI could author
|
|
11
|
+
* that object type while nothing could create the dataset it named. The dataset had to
|
|
12
|
+
* be made in the Console by hand, which is the one step an FDE deploying an
|
|
13
|
+
* organization could not script. `bind` presumed it; `apply` failed at it.
|
|
14
|
+
*
|
|
15
|
+
* All three forms the Console offers are here, because all three are things an FDE has
|
|
16
|
+
* to be able to do without opening it:
|
|
17
|
+
*
|
|
18
|
+
* BLANK declares a column contract and carries no rows. It is the only one a
|
|
19
|
+
* committed tree can fully describe, and the only one a Blueprint binding strictly
|
|
20
|
+
* needs — the pinned revision an object type reads is a contract, not data.
|
|
21
|
+
*
|
|
22
|
+
* SOURCE pulls a relation from a connected Source. The Source itself is still made
|
|
23
|
+
* elsewhere; this names one that exists.
|
|
24
|
+
*
|
|
25
|
+
* FILE uploads a CSV. The bytes live outside the tree by nature, so the file is named
|
|
26
|
+
* by path and never inlined.
|
|
27
|
+
*
|
|
28
|
+
* `uploadPreview` exists because the file form must declare `includedColumns` and
|
|
29
|
+
* nobody can write that list before seeing what the CSV actually holds.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface Options {
|
|
33
|
+
method?: string
|
|
34
|
+
body?: unknown
|
|
35
|
+
/** Multipart. Never set alongside `body` — the two choose different content types. */
|
|
36
|
+
form?: FormData
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SourceSummary {
|
|
40
|
+
id?: string
|
|
41
|
+
displayName?: string
|
|
42
|
+
connectorType?: string
|
|
43
|
+
status?: string
|
|
44
|
+
currentRevision?: { id?: string; revision?: number }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A test that could not connect still answers 200 — the verdict is in the body. */
|
|
48
|
+
export interface ConnectionTest {
|
|
49
|
+
status?: string
|
|
50
|
+
errorCode?: string | null
|
|
51
|
+
errorMessage?: string | null
|
|
52
|
+
latencyMs?: number
|
|
53
|
+
latestConnectionTest?: { status?: string; errorCode?: string | null; errorMessage?: string | null }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UploadPreview {
|
|
57
|
+
columns?: Array<{ name?: string; databaseType?: string; nullable?: boolean; ordinal?: number }>
|
|
58
|
+
rowCount?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DatasetColumn {
|
|
62
|
+
name: string
|
|
63
|
+
databaseType: 'text' | 'numeric' | 'boolean' | 'date' | 'timestamptz'
|
|
64
|
+
nullable: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DatasetSummary {
|
|
68
|
+
id?: string
|
|
69
|
+
apiName?: string
|
|
70
|
+
name?: string
|
|
71
|
+
displayName?: string
|
|
72
|
+
description?: string | null
|
|
73
|
+
currentRevisionId?: string
|
|
74
|
+
updatedAt?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DatasetRevision {
|
|
78
|
+
id?: string
|
|
79
|
+
revision?: number
|
|
80
|
+
schemaDigest?: string
|
|
81
|
+
columns?: Array<{ name?: string; databaseType?: string; nullable?: boolean }>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class DatasetApi {
|
|
85
|
+
constructor(private readonly apiUrl: string, private readonly token: string) {}
|
|
86
|
+
|
|
87
|
+
private async call<T>(path: string, options: Options = {}): Promise<T> {
|
|
88
|
+
const response = await fetch(`${this.apiUrl}${path}`, {
|
|
89
|
+
method: options.form ? 'POST' : (options.method ?? 'GET'),
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${this.token}`,
|
|
92
|
+
// Never set for multipart: `fetch` writes the boundary itself, and a
|
|
93
|
+
// hand-written Content-Type loses it and the body parses as nothing.
|
|
94
|
+
...(options.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
95
|
+
},
|
|
96
|
+
...(options.form ? { body: options.form } : {}),
|
|
97
|
+
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
|
98
|
+
})
|
|
99
|
+
const text = await response.text()
|
|
100
|
+
const payload = text ? JSON.parse(text) : {}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new CliError(payload?.message ?? `Request failed (${response.status}).`, {
|
|
103
|
+
code: payload?.code ?? 'FAILURE',
|
|
104
|
+
...(payload?.details ? { hint: JSON.stringify(payload.details) } : {}),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return (payload?.data ?? payload) as T
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** These routes paginate as `{ items, nextCursor }`; callers want rows. */
|
|
111
|
+
private async page<T>(path: string): Promise<T[]> {
|
|
112
|
+
const body = await this.call<{ items?: T[] } | T[]>(path)
|
|
113
|
+
return Array.isArray(body) ? body : (body.items ?? [])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
list(): Promise<DatasetSummary[]> {
|
|
117
|
+
return this.page<DatasetSummary>('/v1/data-integration/datasets')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get(datasetId: string): Promise<DatasetSummary> {
|
|
121
|
+
return this.call<DatasetSummary>(`/v1/data-integration/datasets/${encodeURIComponent(datasetId)}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
revisions(datasetId: string): Promise<DatasetRevision[]> {
|
|
125
|
+
return this.page<DatasetRevision>(
|
|
126
|
+
`/v1/data-integration/datasets/${encodeURIComponent(datasetId)}/revisions`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
listSources(): Promise<SourceSummary[]> {
|
|
131
|
+
return this.page<SourceSummary>('/v1/data-integration/sources')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Columns and a sample, read from the CSV before anything is created. */
|
|
135
|
+
async uploadPreview(path: string): Promise<UploadPreview> {
|
|
136
|
+
const form = new FormData()
|
|
137
|
+
form.append('file', new Blob([readFileSync(path)]), basename(path))
|
|
138
|
+
return this.call<UploadPreview>('/v1/data-integration/datasets/upload-preview', { form })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Verify a Source revision can be reached.
|
|
143
|
+
*
|
|
144
|
+
* `from-source` refuses a revision without a latest verified test, so this is part of
|
|
145
|
+
* the create path rather than a diagnostic. It writes a test result and moves no data.
|
|
146
|
+
*/
|
|
147
|
+
testSourceRevision(revisionId: string): Promise<ConnectionTest> {
|
|
148
|
+
return this.call<ConnectionTest>(
|
|
149
|
+
`/v1/data-integration/source-revisions/${encodeURIComponent(revisionId)}/test`,
|
|
150
|
+
{ method: 'POST' },
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
createFromSource(payload: {
|
|
155
|
+
apiName: string
|
|
156
|
+
displayName: string
|
|
157
|
+
description?: string | null
|
|
158
|
+
sourceId: string
|
|
159
|
+
schema: string
|
|
160
|
+
relation: string
|
|
161
|
+
includedColumns: string[]
|
|
162
|
+
keyColumns: string[] | null
|
|
163
|
+
deterministicKeyConfirmed: boolean
|
|
164
|
+
}): Promise<DatasetSummary> {
|
|
165
|
+
return this.call<DatasetSummary>('/v1/data-integration/datasets/from-source', {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: payload,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Multipart: the CSV alongside a JSON `payload` part, which is how the route reads it. */
|
|
172
|
+
createFromFile(path: string, payload: {
|
|
173
|
+
apiName: string
|
|
174
|
+
displayName: string
|
|
175
|
+
description?: string | null
|
|
176
|
+
includedColumns: string[]
|
|
177
|
+
keyColumns: string[] | null
|
|
178
|
+
deterministicKeyConfirmed: boolean
|
|
179
|
+
}): Promise<DatasetSummary> {
|
|
180
|
+
const form = new FormData()
|
|
181
|
+
form.append('file', new Blob([readFileSync(path)]), basename(path))
|
|
182
|
+
form.append('payload', JSON.stringify(payload))
|
|
183
|
+
return this.call<DatasetSummary>('/v1/data-integration/datasets/from-file', { form })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createBlank(payload: {
|
|
187
|
+
apiName: string
|
|
188
|
+
displayName: string
|
|
189
|
+
description?: string | null
|
|
190
|
+
columns: DatasetColumn[]
|
|
191
|
+
keyColumns: string[] | null
|
|
192
|
+
deterministicKeyConfirmed: boolean
|
|
193
|
+
}): Promise<DatasetSummary> {
|
|
194
|
+
return this.call<DatasetSummary>('/v1/data-integration/datasets/blank', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: payload,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/api/platform-api.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { FronteraClient } from '@frontera-sdk/core/client'
|
|
2
2
|
|
|
3
|
+
import { CliError } from '../errors'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The service's code for an HTTP status, for responses that carry no `code` of
|
|
7
|
+
* their own — the upload route answers 415 with a bare `{ error }`. Mapped to
|
|
8
|
+
* the same strings `exitCodeFor` already classifies, so an auth failure exits 4
|
|
9
|
+
* ("no retry helps") rather than 1 ("transient").
|
|
10
|
+
*/
|
|
11
|
+
function statusCode(status: number): string {
|
|
12
|
+
if (status === 401) return 'UNAUTHORIZED'
|
|
13
|
+
if (status === 403) return 'FORBIDDEN'
|
|
14
|
+
if (status === 404) return 'NOT_FOUND'
|
|
15
|
+
if (status === 409) return 'CONFLICT'
|
|
16
|
+
if (status === 415 || status === 400 || status === 422) return 'BAD_REQUEST'
|
|
17
|
+
if (status === 429) return 'RATE_LIMITED'
|
|
18
|
+
if (status === 503) return 'SERVICE_UNAVAILABLE'
|
|
19
|
+
return 'FAILURE'
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
export interface AgentDraft {
|
|
4
23
|
agentId?: string
|
|
5
24
|
revision: number
|
|
@@ -164,6 +183,76 @@ export class PlatformApi {
|
|
|
164
183
|
return this.get<unknown>(`/v1/config/workspace-skills/${encodeURIComponent(id)}`)
|
|
165
184
|
}
|
|
166
185
|
|
|
186
|
+
/** Bulk upsert (by name). The route validates asset storage keys, script
|
|
187
|
+
* limits and paths — the CLI does not duplicate those rules. */
|
|
188
|
+
importWorkspaceSkills(skills: unknown[]) {
|
|
189
|
+
return this.client.request<{ created: number; updated: number; errors: Array<{ name: string; message: string }> }>(
|
|
190
|
+
'/v1/config/workspace-skills/import',
|
|
191
|
+
{ method: 'POST', body: { skills } },
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private authHeader(): Record<string, string> {
|
|
196
|
+
const c = this.client.config.credential
|
|
197
|
+
return { authorization: `Bearer ${c.kind === 'apiKey' ? c.key : c.token}` }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Upload one skill asset. Multipart lives here, not in the SDK transport,
|
|
202
|
+
* for the same reason as `AppsApi.multipart` — the shared transport is
|
|
203
|
+
* JSON-only on purpose.
|
|
204
|
+
*/
|
|
205
|
+
async uploadSkillAsset(
|
|
206
|
+
bytes: Uint8Array,
|
|
207
|
+
filename: string,
|
|
208
|
+
mime: string,
|
|
209
|
+
): Promise<{ storagePath: string; mediaType: string }> {
|
|
210
|
+
const form = new FormData()
|
|
211
|
+
form.set('file', new File([new Blob([bytes.buffer as ArrayBuffer])], filename, { type: mime }))
|
|
212
|
+
form.set('purpose', 'skill-asset')
|
|
213
|
+
const res = await fetch(`${this.client.config.apiBaseUrl}/v1/upload`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: this.authHeader(),
|
|
216
|
+
body: form,
|
|
217
|
+
})
|
|
218
|
+
const text = await res.text()
|
|
219
|
+
let parsed: unknown = null
|
|
220
|
+
try {
|
|
221
|
+
parsed = text ? JSON.parse(text) : null
|
|
222
|
+
} catch {
|
|
223
|
+
parsed = text
|
|
224
|
+
}
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
// Not a plain `Error`. Thrown bare, an auth failure here reached the
|
|
227
|
+
// renderer with no code, was reported as INTERNAL_ERROR and exited 1 —
|
|
228
|
+
// "transient, retry" — so a caller retried an upload that no retry could
|
|
229
|
+
// ever fix. The status carries the only fact that decides the exit code.
|
|
230
|
+
const body = parsed as { message?: string; error?: string; code?: string } | null
|
|
231
|
+
throw new CliError(
|
|
232
|
+
body?.message ?? body?.error ?? `asset upload failed with ${res.status}`,
|
|
233
|
+
{
|
|
234
|
+
code: body?.code ?? statusCode(res.status),
|
|
235
|
+
...(res.status === 401
|
|
236
|
+
? { hint: 'set FRONTERA_TOKEN to a valid sk-ws- workspace key, or run `frontera login`' }
|
|
237
|
+
: {}),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
const envelope = parsed as { data?: { storagePath: string; mediaType: string } }
|
|
242
|
+
const data = envelope?.data ?? (parsed as { storagePath: string; mediaType: string })
|
|
243
|
+
return data
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Fetch one skill asset's bytes by workspace + `{sha}.{ext}` filename. */
|
|
247
|
+
async downloadSkillAsset(workspaceId: string, filename: string): Promise<Uint8Array> {
|
|
248
|
+
const res = await fetch(
|
|
249
|
+
`${this.client.config.apiBaseUrl}/v1/skill-assets/${encodeURIComponent(workspaceId)}/${encodeURIComponent(filename)}`,
|
|
250
|
+
{ headers: this.authHeader() },
|
|
251
|
+
)
|
|
252
|
+
if (!res.ok) throw new Error(`asset download failed with ${res.status} (${filename})`)
|
|
253
|
+
return new Uint8Array(await res.arrayBuffer())
|
|
254
|
+
}
|
|
255
|
+
|
|
167
256
|
// ── Plugins (integrations and MCP servers) ────────────────────────────────
|
|
168
257
|
|
|
169
258
|
pluginInstalls() {
|
|
@@ -190,4 +279,215 @@ export class PlatformApi {
|
|
|
190
279
|
knowledgeSources(id: string) {
|
|
191
280
|
return this.getList<unknown>(`/v1/workspace-knowledge/${encodeURIComponent(id)}/sources`)
|
|
192
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a workspace knowledge base.
|
|
285
|
+
*
|
|
286
|
+
* Only name and description travel. Embedding model, chunker and retrieval
|
|
287
|
+
* all have service-side defaults, and a CLI flag for each would let a caller
|
|
288
|
+
* build a base whose vectors no other base can be compared against — a
|
|
289
|
+
* choice that belongs to whoever operates the workspace, not to whoever is
|
|
290
|
+
* seeding files into it today.
|
|
291
|
+
*/
|
|
292
|
+
createKnowledgeBase(workspaceId: string, body: { name: string; description?: string }) {
|
|
293
|
+
return this.client.request<unknown>('/v1/workspace-knowledge', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
body: { workspaceId, ...body },
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Upload one file into a knowledge base.
|
|
301
|
+
*
|
|
302
|
+
* Multipart, so it goes through `fetch` directly rather than the shared
|
|
303
|
+
* transport, for the same reason as `uploadSkillAsset`.
|
|
304
|
+
*
|
|
305
|
+
* Ingestion is asynchronous: a 201 means the source row exists and the
|
|
306
|
+
* Inngest job is queued, NOT that the file is searchable. `frontera
|
|
307
|
+
* knowledge sources` is where the outcome shows up.
|
|
308
|
+
*/
|
|
309
|
+
async uploadKnowledgeSource(
|
|
310
|
+
baseId: string,
|
|
311
|
+
file: { bytes: Uint8Array; filename: string; mime: string },
|
|
312
|
+
opts: { extractionStrategy?: string } = {},
|
|
313
|
+
): Promise<{ id?: string; status?: string }> {
|
|
314
|
+
const form = new FormData()
|
|
315
|
+
form.set(
|
|
316
|
+
'file',
|
|
317
|
+
new File([new Blob([file.bytes.buffer as ArrayBuffer])], file.filename, { type: file.mime }),
|
|
318
|
+
)
|
|
319
|
+
if (opts.extractionStrategy) form.set('extractionStrategy', opts.extractionStrategy)
|
|
320
|
+
|
|
321
|
+
const res = await fetch(
|
|
322
|
+
`${this.client.config.apiBaseUrl}/v1/workspace-knowledge/${encodeURIComponent(baseId)}/sources`,
|
|
323
|
+
{ method: 'POST', headers: this.authHeader(), body: form },
|
|
324
|
+
)
|
|
325
|
+
const text = await res.text()
|
|
326
|
+
let parsed: unknown = null
|
|
327
|
+
try {
|
|
328
|
+
parsed = text ? JSON.parse(text) : null
|
|
329
|
+
} catch {
|
|
330
|
+
parsed = text
|
|
331
|
+
}
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
// The upload route answers 415 with `{ error: "Unsupported file type…" }`
|
|
334
|
+
// and the quota gate with the platform's `{ message }` envelope, so both
|
|
335
|
+
// keys have to be read or the useful sentence is replaced by a number.
|
|
336
|
+
const body = parsed as { message?: string; error?: string } | null
|
|
337
|
+
throw new Error(body?.message ?? body?.error ?? `upload failed with ${res.status}`)
|
|
338
|
+
}
|
|
339
|
+
// `created(source)` wraps `{ source, … }`, and the envelope is stripped by
|
|
340
|
+
// neither `fetch` nor the SDK here — unwrap both layers, tolerating either.
|
|
341
|
+
const envelope = parsed as { data?: { source?: { id?: string; status?: string } } } | null
|
|
342
|
+
const source = envelope?.data?.source ?? (envelope?.data as { id?: string } | undefined)
|
|
343
|
+
return (source ?? {}) as { id?: string; status?: string }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Give an agent access to a knowledge base. Idempotent server-side. */
|
|
347
|
+
attachKnowledgeAgent(baseId: string, agentId: string) {
|
|
348
|
+
return this.client.request<unknown>(
|
|
349
|
+
`/v1/workspace-knowledge/${encodeURIComponent(baseId)}/agents`,
|
|
350
|
+
{ method: 'POST', body: { agentId } },
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Take that access away. The base and its sources are untouched. */
|
|
355
|
+
detachKnowledgeAgent(baseId: string, agentId: string) {
|
|
356
|
+
return this.client.request<unknown>(
|
|
357
|
+
`/v1/workspace-knowledge/${encodeURIComponent(baseId)}/agents/${encodeURIComponent(agentId)}`,
|
|
358
|
+
{ method: 'DELETE' },
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create an agent.
|
|
364
|
+
*
|
|
365
|
+
* It lands in `configuring` with an empty draft, which is why `agent list`
|
|
366
|
+
* does not show it: that route lists agents with a published version. The
|
|
367
|
+
* chain from here is `agent apply` then `agent publish`, and both resolve a
|
|
368
|
+
* slug through `GET /config/agents/:ref`, which does see it.
|
|
369
|
+
*/
|
|
370
|
+
createAgent(body: {
|
|
371
|
+
agentId: string
|
|
372
|
+
name?: string
|
|
373
|
+
description?: string
|
|
374
|
+
kind?: 'conversation' | 'work'
|
|
375
|
+
}) {
|
|
376
|
+
return this.client.request<unknown>('/v1/config/agents', { method: 'POST', body })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Remove a workspace skill. Bindings to agents go with it. */
|
|
380
|
+
deleteWorkspaceSkill(id: string) {
|
|
381
|
+
return this.client.request<unknown>(
|
|
382
|
+
`/v1/config/workspace-skills/${encodeURIComponent(id)}`,
|
|
383
|
+
{ method: 'DELETE' },
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Packs ─────────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/** Every pack in the org, each with this workspace's install row or null. */
|
|
390
|
+
packs() {
|
|
391
|
+
return this.getList<unknown>('/v1/packs/')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async pack(packId: string): Promise<Record<string, unknown>> {
|
|
395
|
+
const body = await this.get<{ data?: Record<string, unknown> } | Record<string, unknown>>(
|
|
396
|
+
`/v1/packs/${encodeURIComponent(packId)}`,
|
|
397
|
+
)
|
|
398
|
+
return ((body as { data?: Record<string, unknown> })?.data ?? body ?? {}) as Record<string, unknown>
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
createPack(manifest: unknown) {
|
|
402
|
+
return this.client.request<unknown>('/v1/packs/', { method: 'POST', body: { manifest } })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
updatePack(packId: string, manifest: unknown) {
|
|
406
|
+
return this.client.request<unknown>(`/v1/packs/${encodeURIComponent(packId)}`, {
|
|
407
|
+
method: 'PUT',
|
|
408
|
+
body: { manifest },
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
deletePack(packId: string) {
|
|
413
|
+
return this.client.request<unknown>(`/v1/packs/${encodeURIComponent(packId)}`, {
|
|
414
|
+
method: 'DELETE',
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
installPack(packId: string) {
|
|
419
|
+
return this.client.request<unknown>(`/v1/packs/${encodeURIComponent(packId)}/install`, {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** `removeApps` also uninstalls the apps the pack brought in. Default false. */
|
|
425
|
+
uninstallPack(packId: string, removeApps = false) {
|
|
426
|
+
return this.client.request<unknown>(`/v1/packs/${encodeURIComponent(packId)}/uninstall`, {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
body: { removeApps },
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Install puts the skills in the workspace; this is what an AGENT loads. */
|
|
433
|
+
applyPackToAgent(packId: string, agentId: string) {
|
|
434
|
+
return this.client.request<unknown>(
|
|
435
|
+
`/v1/packs/${encodeURIComponent(packId)}/agents/${encodeURIComponent(agentId)}/apply`,
|
|
436
|
+
{ method: 'POST' },
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
removePackFromAgent(packId: string, agentId: string) {
|
|
441
|
+
return this.client.request<unknown>(
|
|
442
|
+
`/v1/packs/${encodeURIComponent(packId)}/agents/${encodeURIComponent(agentId)}/apply`,
|
|
443
|
+
{ method: 'DELETE' },
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Workspace secrets ─────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Secret NAMES and what depends on each. Values are never returned by this
|
|
451
|
+
* route — not to a session and not to a key.
|
|
452
|
+
*/
|
|
453
|
+
workspaceSecrets(workspaceId: string) {
|
|
454
|
+
return this.getList<unknown>(`/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets`)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
createWorkspaceSecret(
|
|
458
|
+
workspaceId: string,
|
|
459
|
+
body: { name: string; value: string; description?: string },
|
|
460
|
+
) {
|
|
461
|
+
return this.client.request<unknown>(
|
|
462
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets`,
|
|
463
|
+
{ method: 'POST', body },
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Replace the value of a secret that already exists. */
|
|
468
|
+
updateWorkspaceSecret(
|
|
469
|
+
workspaceId: string,
|
|
470
|
+
name: string,
|
|
471
|
+
body: { value?: string; description?: string },
|
|
472
|
+
) {
|
|
473
|
+
return this.client.request<unknown>(
|
|
474
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets/${encodeURIComponent(name)}`,
|
|
475
|
+
{ method: 'PATCH', body },
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Refused server-side while anything still resolves the secret. */
|
|
480
|
+
deleteWorkspaceSecret(workspaceId: string, name: string) {
|
|
481
|
+
return this.client.request<unknown>(
|
|
482
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets/${encodeURIComponent(name)}`,
|
|
483
|
+
{ method: 'DELETE' },
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Which agents can read this base. */
|
|
488
|
+
knowledgeAttachments(baseId: string) {
|
|
489
|
+
return this.getList<unknown>(
|
|
490
|
+
`/v1/workspace-knowledge/${encodeURIComponent(baseId)}/agents`,
|
|
491
|
+
)
|
|
492
|
+
}
|
|
193
493
|
}
|