@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.
- 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
package/src/commands/login.ts
CHANGED
|
@@ -78,26 +78,64 @@ export const loginCommand: Command = {
|
|
|
78
78
|
'run `frontera login` in a terminal to be asked for one, or pipe it in: `echo $KEY | frontera login`',
|
|
79
79
|
)
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
// Two credential kinds reach this CLI, and they are not interchangeable:
|
|
82
|
+
//
|
|
83
|
+
// sk-ws- a WORKSPACE key — one workspace's slice of the platform.
|
|
84
|
+
// sk-org- an ORGANIZATION key — the organization-level Blueprint draft,
|
|
85
|
+
// which no workspace credential can reach, because that draft is
|
|
86
|
+
// shared by every workspace in the organization.
|
|
87
|
+
//
|
|
88
|
+
// Both are stored the same way; only the verification below differs, because an
|
|
89
|
+
// organization key belongs to no workspace and `whoami` has none to report.
|
|
90
|
+
const isWorkspaceKey = token.startsWith('sk-ws-')
|
|
91
|
+
const isOrgKey = token.startsWith('sk-org-')
|
|
92
|
+
if (!isWorkspaceKey && !isOrgKey) {
|
|
82
93
|
throw new UsageError(
|
|
83
|
-
'that does not look like a
|
|
84
|
-
'workspace keys start with sk-ws-
|
|
94
|
+
'that does not look like a Frontera key',
|
|
95
|
+
'workspace keys start with sk-ws- (Workspace settings → API Keys); ' +
|
|
96
|
+
'organization keys start with sk-org- (Settings → API keys)',
|
|
85
97
|
)
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
// Shaped as `whoami` actually answers — both fields are nullable there, and
|
|
101
|
+
// declaring them merely optional made the assignment below unassignable.
|
|
102
|
+
let me: { workspaceId: string | null; orgId: string | null } | null = null
|
|
103
|
+
if (isWorkspaceKey) {
|
|
104
|
+
me = await new PlatformApi(ctx.apiUrl, token).whoami().catch(() => null)
|
|
105
|
+
if (!me?.workspaceId) {
|
|
106
|
+
throw new CliError('the token was rejected by this API origin', {
|
|
107
|
+
code: 'UNAUTHORIZED',
|
|
108
|
+
hint: `check the key is enabled, and that --api-url is right (currently ${ctx.apiUrl})`,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Verified against an organization-scoped read instead. A revoked or expired
|
|
113
|
+
// key answers 401 here, which is exactly what login should catch.
|
|
114
|
+
const lifecycle = await fetch(`${ctx.apiUrl}/v1/blueprint/lifecycle`, {
|
|
115
|
+
headers: { authorization: `Bearer ${token}` },
|
|
116
|
+
}).catch(() => null)
|
|
117
|
+
if (!lifecycle || lifecycle.status === 401 || lifecycle.status === 403) {
|
|
118
|
+
throw new CliError('the organization key was rejected by this API origin', {
|
|
119
|
+
code: 'UNAUTHORIZED',
|
|
120
|
+
hint: `check the key is active and carries organization:update, and that --api-url is right (currently ${ctx.apiUrl})`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
writeStoredToken(ctx.apiUrl, token)
|
|
97
126
|
|
|
98
127
|
return {
|
|
99
|
-
data: {
|
|
100
|
-
|
|
128
|
+
data: {
|
|
129
|
+
apiUrl: ctx.apiUrl,
|
|
130
|
+
kind: isOrgKey ? 'organization' : 'workspace',
|
|
131
|
+
workspaceId: me?.workspaceId ?? null,
|
|
132
|
+
orgId: me?.orgId ?? null,
|
|
133
|
+
},
|
|
134
|
+
// An organization key belongs to no workspace, so saying "workspace <id>" for
|
|
135
|
+
// one would be a comforting lie about what the credential reaches.
|
|
136
|
+
text: isOrgKey
|
|
137
|
+
? `Stored an organization key for ${ctx.apiUrl}\n it authors this organization's Blueprint`
|
|
138
|
+
: `Stored a key for ${ctx.apiUrl}\n workspace ${me?.workspaceId}`,
|
|
101
139
|
}
|
|
102
140
|
},
|
|
103
141
|
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { PlatformApi } from '../../api/platform-api'
|
|
4
|
+
import { CliError, UsageError } from '../../errors'
|
|
5
|
+
import { table } from '../../table'
|
|
6
|
+
import { resolveAgentRef } from '../agent/resolve'
|
|
7
|
+
import { flagBool, type Command } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Packs — a named bundle of skills (plus the app kinds and computer setup
|
|
11
|
+
* script they assume), authored once at organization level and installed into a
|
|
12
|
+
* workspace.
|
|
13
|
+
*
|
|
14
|
+
* This is the closest thing the platform has to repeatable delivery: the same
|
|
15
|
+
* capability set can be pushed from a file, installed into a customer's
|
|
16
|
+
* workspace, and applied to a named agent. The routes have existed for a while
|
|
17
|
+
* with no CLI surface at all, which meant every rollout was hand-repeated in
|
|
18
|
+
* the Console.
|
|
19
|
+
*
|
|
20
|
+
* What a pack does NOT carry: agent configuration, Blueprint, knowledge,
|
|
21
|
+
* automations, secrets. It is a skills bundle, not a workspace snapshot — worth
|
|
22
|
+
* knowing before planning a migration around it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface PackRow {
|
|
26
|
+
id?: string
|
|
27
|
+
name?: string
|
|
28
|
+
displayName?: string
|
|
29
|
+
description?: string
|
|
30
|
+
version?: number
|
|
31
|
+
category?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PackListEntry {
|
|
35
|
+
pack?: PackRow
|
|
36
|
+
install?: { id?: string; installedAt?: string; packVersion?: number } | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
40
|
+
|
|
41
|
+
async function resolvePackRef(api: PlatformApi, ref: string): Promise<string> {
|
|
42
|
+
if (UUID.test(ref)) return ref
|
|
43
|
+
|
|
44
|
+
const rows = (await api.packs()) as PackListEntry[]
|
|
45
|
+
const match = rows.find(
|
|
46
|
+
(r) =>
|
|
47
|
+
(r.pack?.name ?? '').toLowerCase() === ref.toLowerCase() ||
|
|
48
|
+
(r.pack?.displayName ?? '').toLowerCase() === ref.toLowerCase(),
|
|
49
|
+
)
|
|
50
|
+
if (match?.pack?.id) return match.pack.id
|
|
51
|
+
|
|
52
|
+
throw new CliError(`no pack named "${ref}"`, {
|
|
53
|
+
code: 'NOT_FOUND',
|
|
54
|
+
hint: `known packs: ${rows.map((r) => r.pack?.name).filter(Boolean).join(', ') || '(none)'}`,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const list: Command = {
|
|
59
|
+
meta: {
|
|
60
|
+
noun: 'pack',
|
|
61
|
+
verb: 'list',
|
|
62
|
+
args: [],
|
|
63
|
+
flags: {},
|
|
64
|
+
summary: 'List packs available to this organization, and which are installed',
|
|
65
|
+
examples: ['frontera pack list', 'frontera pack list --json'],
|
|
66
|
+
},
|
|
67
|
+
async run(ctx) {
|
|
68
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).packs()) as PackListEntry[]
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
data: rows,
|
|
72
|
+
text:
|
|
73
|
+
rows.length === 0
|
|
74
|
+
? 'No packs in this organization.'
|
|
75
|
+
: table(
|
|
76
|
+
// `installed` first among the facts a reader is deciding on: the
|
|
77
|
+
// same pack list serves "what can I install" and "what did I".
|
|
78
|
+
['name', 'installed', 'version', 'description'],
|
|
79
|
+
rows.map((r) => [
|
|
80
|
+
r.pack?.name ?? '?',
|
|
81
|
+
r.install ? 'yes' : '',
|
|
82
|
+
String(r.pack?.version ?? ''),
|
|
83
|
+
r.pack?.description ?? '',
|
|
84
|
+
]),
|
|
85
|
+
[undefined, undefined, undefined, 50],
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const get: Command = {
|
|
92
|
+
meta: {
|
|
93
|
+
noun: 'pack',
|
|
94
|
+
verb: 'get',
|
|
95
|
+
args: [{ name: 'pack', required: true, description: 'pack name or id, from `frontera pack list`' }],
|
|
96
|
+
flags: {},
|
|
97
|
+
summary: 'Show one pack: its manifest and what it installs',
|
|
98
|
+
examples: ['frontera pack get analytics-starter', 'frontera pack get analytics-starter --json'],
|
|
99
|
+
},
|
|
100
|
+
async run(ctx) {
|
|
101
|
+
const ref = ctx.positional[0]
|
|
102
|
+
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
103
|
+
|
|
104
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
105
|
+
const detail = (await api.pack(await resolvePackRef(api, ref))) as {
|
|
106
|
+
pack?: PackRow
|
|
107
|
+
install?: unknown
|
|
108
|
+
// `itemType`/`itemName`, not `kind`/`name` — guessing printed "?" for
|
|
109
|
+
// every row, the same way `knowledge agents` did before it was checked
|
|
110
|
+
// against a real payload.
|
|
111
|
+
items?: Array<{ itemType?: string; itemName?: string; status?: string }>
|
|
112
|
+
agents?: Array<{ agentId?: string; name?: string }>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pack = detail.pack ?? (detail as PackRow)
|
|
116
|
+
const items = detail.items ?? []
|
|
117
|
+
const lines = [
|
|
118
|
+
`${pack.name ?? ref}${pack.version ? ` v${pack.version}` : ''}`,
|
|
119
|
+
...(pack.description ? [` ${pack.description}`] : []),
|
|
120
|
+
'',
|
|
121
|
+
// `items` are what the INSTALL created, so an empty list means one of two
|
|
122
|
+
// very different things. Saying which is the difference between "install
|
|
123
|
+
// it" and "the pack is empty, fix the manifest".
|
|
124
|
+
items.length === 0
|
|
125
|
+
? detail.install
|
|
126
|
+
? ' installed, but it carries no items'
|
|
127
|
+
: ` not installed in this workspace — frontera pack install ${ref}`
|
|
128
|
+
: table(
|
|
129
|
+
['type', 'name', 'status'],
|
|
130
|
+
items.map((i) => [i.itemType ?? '?', i.itemName ?? '', i.status ?? '']),
|
|
131
|
+
),
|
|
132
|
+
]
|
|
133
|
+
if (detail.agents?.length) {
|
|
134
|
+
lines.push('', ` applied to: ${detail.agents.map((a) => a.agentId ?? a.name).join(', ')}`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { data: detail, text: lines.join('\n') }
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const push: Command = {
|
|
142
|
+
meta: {
|
|
143
|
+
noun: 'pack',
|
|
144
|
+
verb: 'push',
|
|
145
|
+
args: [{ name: 'file', required: true, description: 'pack manifest JSON, or `-` for stdin' }],
|
|
146
|
+
flags: { force: 'boolean' },
|
|
147
|
+
summary: 'Create or replace a pack from a manifest file',
|
|
148
|
+
examples: [
|
|
149
|
+
'frontera pack push ./analytics-starter.json',
|
|
150
|
+
'frontera pack push ./analytics-starter.json --force',
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
async run(ctx) {
|
|
154
|
+
const path = ctx.positional[0]
|
|
155
|
+
if (!path) throw new UsageError('missing <file>', 'frontera pack push ./pack.json')
|
|
156
|
+
|
|
157
|
+
let raw: string
|
|
158
|
+
if (path === '-') {
|
|
159
|
+
const chunks: Uint8Array[] = []
|
|
160
|
+
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk)
|
|
161
|
+
raw = Buffer.concat(chunks).toString('utf8')
|
|
162
|
+
} else {
|
|
163
|
+
try {
|
|
164
|
+
raw = readFileSync(path, 'utf8')
|
|
165
|
+
} catch {
|
|
166
|
+
throw new UsageError(`could not read ${path}`, 'check the path, or pipe the manifest in with -')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let manifest: { name?: string; version?: number }
|
|
171
|
+
try {
|
|
172
|
+
manifest = JSON.parse(raw) as { name?: string; version?: number }
|
|
173
|
+
} catch {
|
|
174
|
+
throw new UsageError('the manifest is not valid JSON', 'it should be a pack manifest object')
|
|
175
|
+
}
|
|
176
|
+
if (!manifest.name) {
|
|
177
|
+
throw new UsageError('the manifest has no `name`', 'a pack is addressed by name everywhere else')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
181
|
+
const rows = (await api.packs()) as PackListEntry[]
|
|
182
|
+
const existing = rows.find(
|
|
183
|
+
(r) => (r.pack?.name ?? '').toLowerCase() === manifest.name!.toLowerCase(),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// Replacing a pack rewrites what every workspace that installed it will get
|
|
187
|
+
// on its next install, so it is opt-in rather than implied by pushing the
|
|
188
|
+
// same name twice.
|
|
189
|
+
if (existing?.pack?.id && !flagBool(ctx, 'force')) {
|
|
190
|
+
throw new CliError(`a pack named "${manifest.name}" already exists`, {
|
|
191
|
+
code: 'CONFLICT',
|
|
192
|
+
hint: 'pass --force to replace it — installed workspaces get the new contents on their next install',
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = (existing?.pack?.id
|
|
197
|
+
? await api.updatePack(existing.pack.id, manifest)
|
|
198
|
+
: await api.createPack(manifest)) as { data?: PackRow } | PackRow
|
|
199
|
+
const saved = ((result as { data?: PackRow }).data ?? result) as PackRow
|
|
200
|
+
|
|
201
|
+
// The SERVICE decides the version — a replace increments it regardless of
|
|
202
|
+
// what the manifest says — so echoing the manifest's number told the author
|
|
203
|
+
// their pack was v1 while every workspace would install v2.
|
|
204
|
+
const version = saved.version ?? manifest.version
|
|
205
|
+
return {
|
|
206
|
+
data: saved,
|
|
207
|
+
text: `${existing ? 'replaced' : 'created'} pack ${manifest.name}${version ? ` v${version}` : ''}`,
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const install: Command = {
|
|
213
|
+
meta: {
|
|
214
|
+
noun: 'pack',
|
|
215
|
+
verb: 'install',
|
|
216
|
+
args: [{ name: 'pack', required: true, description: 'pack name or id' }],
|
|
217
|
+
flags: {},
|
|
218
|
+
summary: 'Install a pack into this workspace',
|
|
219
|
+
examples: ['frontera pack install analytics-starter'],
|
|
220
|
+
},
|
|
221
|
+
async run(ctx) {
|
|
222
|
+
const ref = ctx.positional[0]
|
|
223
|
+
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
224
|
+
|
|
225
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
226
|
+
const id = await resolvePackRef(api, ref)
|
|
227
|
+
const result = (await api.installPack(id)) as { skills?: unknown[] }
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
data: result,
|
|
231
|
+
text: [
|
|
232
|
+
`installed ${ref}`,
|
|
233
|
+
// Installing puts the skills in the workspace; an agent loads none of
|
|
234
|
+
// them until the pack is applied to it. Two steps, and the second is the
|
|
235
|
+
// one people forget.
|
|
236
|
+
` next: frontera pack apply ${ref} <agent>`,
|
|
237
|
+
].join('\n'),
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const uninstall: Command = {
|
|
243
|
+
meta: {
|
|
244
|
+
noun: 'pack',
|
|
245
|
+
verb: 'uninstall',
|
|
246
|
+
args: [{ name: 'pack', required: true, description: 'pack name or id' }],
|
|
247
|
+
flags: { 'remove-apps': 'boolean' },
|
|
248
|
+
summary: 'Remove a pack from this workspace',
|
|
249
|
+
examples: ['frontera pack uninstall analytics-starter', 'frontera pack uninstall analytics-starter --remove-apps'],
|
|
250
|
+
},
|
|
251
|
+
async run(ctx) {
|
|
252
|
+
const ref = ctx.positional[0]
|
|
253
|
+
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
254
|
+
|
|
255
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
256
|
+
const id = await resolvePackRef(api, ref)
|
|
257
|
+
const removeApps = flagBool(ctx, 'remove-apps')
|
|
258
|
+
const result = await api.uninstallPack(id, removeApps)
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
data: result,
|
|
262
|
+
text: removeApps
|
|
263
|
+
? `uninstalled ${ref}, including the apps it installed`
|
|
264
|
+
: // Stated because the default surprises people who expect uninstall to
|
|
265
|
+
// undo the whole install.
|
|
266
|
+
`uninstalled ${ref} — apps it installed were LEFT in place (--remove-apps takes them too)`,
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const apply: Command = {
|
|
272
|
+
meta: {
|
|
273
|
+
noun: 'pack',
|
|
274
|
+
verb: 'apply',
|
|
275
|
+
args: [
|
|
276
|
+
{ name: 'pack', required: true, description: 'pack name or id' },
|
|
277
|
+
{ name: 'agent', required: true, description: 'agent slug or id' },
|
|
278
|
+
],
|
|
279
|
+
flags: {},
|
|
280
|
+
summary: 'Give one agent the skills an installed pack carries',
|
|
281
|
+
examples: ['frontera pack apply analytics-starter ava'],
|
|
282
|
+
},
|
|
283
|
+
async run(ctx) {
|
|
284
|
+
const [packRef, agentRef] = ctx.positional
|
|
285
|
+
if (!packRef) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
286
|
+
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
287
|
+
|
|
288
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
289
|
+
const packId = await resolvePackRef(api, packRef)
|
|
290
|
+
const agentId = await resolveAgentRef(api, agentRef)
|
|
291
|
+
const result = await api.applyPackToAgent(packId, agentId)
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
data: result,
|
|
295
|
+
text: [`applied ${packRef} to ${agentRef}`, ` verify: frontera agent get ${agentRef}`].join('\n'),
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The reverse of `apply`, as its own verb rather than `apply --remove`.
|
|
302
|
+
*
|
|
303
|
+
* Two reasons, and the second is not cosmetic: it mirrors `knowledge
|
|
304
|
+
* attach`/`detach`, which is the same relationship — an agent's access to
|
|
305
|
+
* something the workspace holds — and a `--remove` flag on `apply` put the word
|
|
306
|
+
* `remove` in the shell completion script, where it completed `plugin remove`,
|
|
307
|
+
* a verb that is reserved and fails.
|
|
308
|
+
*/
|
|
309
|
+
const detach: Command = {
|
|
310
|
+
meta: {
|
|
311
|
+
noun: 'pack',
|
|
312
|
+
verb: 'detach',
|
|
313
|
+
args: [
|
|
314
|
+
{ name: 'pack', required: true, description: 'pack name or id' },
|
|
315
|
+
{ name: 'agent', required: true, description: 'agent slug or id' },
|
|
316
|
+
],
|
|
317
|
+
flags: {},
|
|
318
|
+
summary: 'Take a pack’s skills away from one agent',
|
|
319
|
+
examples: ['frontera pack detach analytics-starter ava'],
|
|
320
|
+
},
|
|
321
|
+
async run(ctx) {
|
|
322
|
+
const [packRef, agentRef] = ctx.positional
|
|
323
|
+
if (!packRef) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
324
|
+
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
325
|
+
|
|
326
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
327
|
+
const packId = await resolvePackRef(api, packRef)
|
|
328
|
+
const agentId = await resolveAgentRef(api, agentRef)
|
|
329
|
+
await api.removePackFromAgent(packId, agentId)
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
data: { packId, agentId, applied: false },
|
|
333
|
+
text: `detached ${packRef} from ${agentRef} — the pack stays installed in the workspace`,
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const remove: Command = {
|
|
339
|
+
meta: {
|
|
340
|
+
noun: 'pack',
|
|
341
|
+
verb: 'delete',
|
|
342
|
+
args: [{ name: 'pack', required: true, description: 'pack name or id' }],
|
|
343
|
+
flags: {},
|
|
344
|
+
summary: 'Delete a pack from the organization',
|
|
345
|
+
examples: ['frontera pack delete analytics-starter'],
|
|
346
|
+
},
|
|
347
|
+
async run(ctx) {
|
|
348
|
+
const ref = ctx.positional[0]
|
|
349
|
+
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
350
|
+
|
|
351
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
352
|
+
const id = await resolvePackRef(api, ref)
|
|
353
|
+
await api.deletePack(id)
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
data: { id, name: ref, deleted: true },
|
|
357
|
+
// Organization-wide, unlike uninstall — which is why the two verbs are not
|
|
358
|
+
// spelled the same way.
|
|
359
|
+
text: `deleted pack ${ref} from the organization`,
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export const packCommands: Command[] = [
|
|
365
|
+
list,
|
|
366
|
+
get,
|
|
367
|
+
push,
|
|
368
|
+
install,
|
|
369
|
+
uninstall,
|
|
370
|
+
apply,
|
|
371
|
+
detach,
|
|
372
|
+
remove,
|
|
373
|
+
]
|
package/src/commands/registry.ts
CHANGED
|
@@ -14,11 +14,17 @@ import { appPromote } from './app/promote'
|
|
|
14
14
|
import { appVersions } from './app/versions'
|
|
15
15
|
import { blueprintList } from './blueprint/list'
|
|
16
16
|
import { blueprintGet } from './blueprint/get'
|
|
17
|
-
import {
|
|
17
|
+
import { blueprintAuthoringCommands } from './blueprint/authoring'
|
|
18
|
+
import { blueprintDeclarativeCommands } from './blueprint/declarative'
|
|
19
|
+
import { blueprintBind } from './blueprint/bind'
|
|
20
|
+
import { blueprintGrant } from './blueprint/grants'
|
|
18
21
|
import { agentCommands } from './agent/index-commands'
|
|
19
22
|
import { skillCommands } from './skill/index-commands'
|
|
20
23
|
import { pluginCommands } from './plugin/index-commands'
|
|
24
|
+
import { datasetCommands } from './dataset/index-commands'
|
|
21
25
|
import { knowledgeCommands } from './knowledge/index-commands'
|
|
26
|
+
import { packCommands } from './pack/index-commands'
|
|
27
|
+
import { secretCommands } from './secret/index-commands'
|
|
22
28
|
import { automationCommands } from './automation/index-commands'
|
|
23
29
|
import { completionCommand } from './completion'
|
|
24
30
|
import { initCommand } from './init'
|
|
@@ -56,12 +62,20 @@ export const COMMANDS: readonly Command[] = [
|
|
|
56
62
|
...agentCommands,
|
|
57
63
|
...skillCommands,
|
|
58
64
|
...pluginCommands,
|
|
65
|
+
...datasetCommands,
|
|
59
66
|
...knowledgeCommands,
|
|
67
|
+
...packCommands,
|
|
68
|
+
...secretCommands,
|
|
60
69
|
...automationCommands,
|
|
61
70
|
|
|
62
71
|
blueprintList,
|
|
63
72
|
blueprintGet,
|
|
64
|
-
|
|
73
|
+
// Authoring is no longer reserved: the organization API key (`sk-org-`) reaches the
|
|
74
|
+
// organization-level shared draft, which is what these verbs were waiting on.
|
|
75
|
+
...blueprintAuthoringCommands,
|
|
76
|
+
...blueprintDeclarativeCommands,
|
|
77
|
+
blueprintBind,
|
|
78
|
+
blueprintGrant,
|
|
65
79
|
]
|
|
66
80
|
|
|
67
81
|
export function findCommand(noun: string, verb: string | undefined): Command | null {
|
|
@@ -86,7 +100,10 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
86
100
|
agent: 'Agents — read a configuration, stage a change, publish it',
|
|
87
101
|
skill: 'Workspace skills an agent loads at runtime',
|
|
88
102
|
plugin: 'Integrations and MCP servers connected to this workspace',
|
|
103
|
+
dataset: 'Source datasets a Blueprint object type can bind to',
|
|
89
104
|
knowledge: 'Knowledge bases and the sources inside them',
|
|
105
|
+
pack: 'Reusable skill bundles — author once, install per workspace',
|
|
106
|
+
secret: 'Workspace secrets — named here, never printed back',
|
|
90
107
|
automation: 'Automations — TypeScript deployed here, run on a schedule',
|
|
91
108
|
blueprint: 'The shared model of the organization — what an app can read',
|
|
92
109
|
}
|