@frontera-sdk/cli 0.1.0 → 1.43.5

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 (48) hide show
  1. package/package.json +4 -2
  2. package/src/api/apps-api.ts +13 -1
  3. package/src/api/automation-api.ts +129 -1
  4. package/src/api/blueprint-authoring-api.ts +574 -0
  5. package/src/api/dataset-api.ts +199 -0
  6. package/src/api/platform-api.ts +300 -0
  7. package/src/automation-template.ts +224 -0
  8. package/src/blueprint/compile.ts +371 -0
  9. package/src/blueprint/dataset-revision.ts +33 -0
  10. package/src/blueprint/diff.ts +223 -0
  11. package/src/blueprint/model.ts +227 -0
  12. package/src/blueprint/projection.ts +254 -0
  13. package/src/blueprint/render.ts +73 -0
  14. package/src/blueprint/scaffold.ts +79 -0
  15. package/src/blueprint/tree.ts +121 -0
  16. package/src/commands/agent/index-commands.ts +87 -1
  17. package/src/commands/app/deploy.ts +43 -3
  18. package/src/commands/app/init.ts +23 -1
  19. package/src/commands/app/pull.ts +12 -35
  20. package/src/commands/automation/index-commands.ts +42 -1
  21. package/src/commands/automation/init.ts +52 -0
  22. package/src/commands/automation/project-root.ts +58 -0
  23. package/src/commands/automation/pull.ts +124 -0
  24. package/src/commands/automation/run.ts +271 -0
  25. package/src/commands/blueprint/authoring.ts +410 -0
  26. package/src/commands/blueprint/bind.ts +228 -0
  27. package/src/commands/blueprint/declarative.ts +1052 -0
  28. package/src/commands/blueprint/grants.ts +164 -0
  29. package/src/commands/dataset/index-commands.ts +431 -0
  30. package/src/commands/knowledge/index-commands.ts +278 -27
  31. package/src/commands/knowledge/upload-batch.ts +146 -0
  32. package/src/commands/knowledge/upload-plan.ts +127 -0
  33. package/src/commands/login.ts +49 -11
  34. package/src/commands/pack/index-commands.ts +373 -0
  35. package/src/commands/registry.ts +19 -2
  36. package/src/commands/secret/index-commands.ts +195 -0
  37. package/src/commands/skill/bundle-commands.ts +327 -0
  38. package/src/commands/skill/index-commands.ts +36 -42
  39. package/src/commands/skill/resolve.ts +34 -0
  40. package/src/dev-env.ts +114 -0
  41. package/src/flag-help.ts +34 -0
  42. package/src/harness.ts +30 -3
  43. package/src/main.ts +10 -3
  44. package/src/render-evidence.ts +152 -0
  45. package/src/template.ts +4 -0
  46. package/src/untar.ts +44 -0
  47. package/src/vendor/sdk-sources.json +13 -11
  48. 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
+ }
@@ -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
  }