@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.
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
@@ -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
- if (!token.startsWith('sk-ws-')) {
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 workspace key',
84
- 'workspace keys start with sk-ws- and are created in Workspace settings → API Keys',
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
- const me = await new PlatformApi(ctx.apiUrl, token).whoami().catch(() => null)
89
- if (!me?.workspaceId) {
90
- throw new CliError('the token was rejected by this API origin', {
91
- code: 'UNAUTHORIZED',
92
- hint: `check the key is enabled, and that --api-url is right (currently ${ctx.apiUrl})`,
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: { apiUrl: ctx.apiUrl, workspaceId: me.workspaceId, orgId: me.orgId },
100
- text: `Stored a key for ${ctx.apiUrl}\n workspace ${me.workspaceId}`,
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
+ ]
@@ -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 { blueprintReserved } from './blueprint/reserved'
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
- ...blueprintReserved,
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
  }