@frontera-sdk/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +65 -0
- package/package.json +47 -0
- package/src/api/apps-api.ts +165 -0
- package/src/api/automation-api.ts +140 -0
- package/src/api/platform-api.ts +193 -0
- package/src/api/registry-api.ts +43 -0
- package/src/args.ts +108 -0
- package/src/commands/agent/compose.ts +155 -0
- package/src/commands/agent/index-commands.ts +348 -0
- package/src/commands/agent/resolve.ts +58 -0
- package/src/commands/app/add.ts +78 -0
- package/src/commands/app/deploy.ts +105 -0
- package/src/commands/app/init.ts +53 -0
- package/src/commands/app/list.ts +51 -0
- package/src/commands/app/promote.ts +31 -0
- package/src/commands/app/pull.ts +145 -0
- package/src/commands/app/save.ts +36 -0
- package/src/commands/app/shared.ts +25 -0
- package/src/commands/app/versions.ts +38 -0
- package/src/commands/automation/index-commands.ts +325 -0
- package/src/commands/blueprint/get.ts +160 -0
- package/src/commands/blueprint/list.ts +48 -0
- package/src/commands/blueprint/reserved.ts +40 -0
- package/src/commands/completion.ts +293 -0
- package/src/commands/init.ts +33 -0
- package/src/commands/knowledge/index-commands.ts +140 -0
- package/src/commands/login.ts +103 -0
- package/src/commands/plugin/index-commands.ts +112 -0
- package/src/commands/registry.ts +405 -0
- package/src/commands/skill/index-commands.ts +140 -0
- package/src/commands/types.ts +76 -0
- package/src/config.ts +142 -0
- package/src/context.ts +67 -0
- package/src/errors.ts +30 -0
- package/src/exit.ts +98 -0
- package/src/flag-help.ts +70 -0
- package/src/harness.ts +162 -0
- package/src/heal.ts +418 -0
- package/src/help.ts +128 -0
- package/src/main.ts +204 -0
- package/src/manifest.ts +80 -0
- package/src/output.ts +65 -0
- package/src/pack.ts +18 -0
- package/src/packaging.ts +116 -0
- package/src/project.ts +151 -0
- package/src/prompt.ts +48 -0
- package/src/registry.ts +62 -0
- package/src/secrets.ts +69 -0
- package/src/table.ts +47 -0
- package/src/tar.ts +73 -0
- package/src/template.ts +566 -0
- package/src/vendor/sdk-sources.json +25 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { FronteraClient } from '@frontera-sdk/core/client'
|
|
2
|
+
|
|
3
|
+
export interface AgentDraft {
|
|
4
|
+
agentId?: string
|
|
5
|
+
revision: number
|
|
6
|
+
updatedBy?: string
|
|
7
|
+
updatedAt?: string
|
|
8
|
+
snapshot: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Client for the platform's resource routes.
|
|
13
|
+
*
|
|
14
|
+
* One class rather than one per noun: they share a base URL, a credential and
|
|
15
|
+
* an envelope, and splitting them would multiply construction without
|
|
16
|
+
* separating anything real. The nouns are separated where it matters — in the
|
|
17
|
+
* commands.
|
|
18
|
+
*/
|
|
19
|
+
export class PlatformApi {
|
|
20
|
+
private readonly client: FronteraClient
|
|
21
|
+
|
|
22
|
+
constructor(apiBaseUrl: string, token: string) {
|
|
23
|
+
this.client = new FronteraClient({
|
|
24
|
+
apiBaseUrl,
|
|
25
|
+
credential: { kind: 'apiKey', key: token },
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private get<T>(path: string): Promise<T> {
|
|
30
|
+
return this.client.request<T>(path)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalise a list response.
|
|
35
|
+
*
|
|
36
|
+
* Some routers return `ok(result)` where `result` is itself `{ data: [...] }`
|
|
37
|
+
* — `/v1/apps` and `/v1/workspace-knowledge` both do — so after the SDK
|
|
38
|
+
* unwraps the outer envelope a second one is left. Rather than encode which
|
|
39
|
+
* endpoints double-wrap (a list that would rot), accept either shape and
|
|
40
|
+
* treat anything else as empty.
|
|
41
|
+
*/
|
|
42
|
+
private async getList<T>(path: string): Promise<T[]> {
|
|
43
|
+
const body = await this.get<T[] | { data?: T[] }>(path)
|
|
44
|
+
if (Array.isArray(body)) return body
|
|
45
|
+
const inner = (body as { data?: T[] })?.data
|
|
46
|
+
return Array.isArray(inner) ? inner : []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Identity ──────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
whoami() {
|
|
52
|
+
return this.get<{ orgId: string | null; workspaceId: string | null; authKind: string; principal: string | null }>(
|
|
53
|
+
'/v1/whoami',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Blueprint (read) ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
blueprintObjectTypes() {
|
|
60
|
+
return this.getList<unknown>('/v1/blueprint/object-types')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
blueprintObjectType(apiName: string) {
|
|
64
|
+
return this.get<unknown>(`/v1/blueprint/object-types/${encodeURIComponent(apiName)}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
blueprintMetrics() {
|
|
68
|
+
return this.getList<unknown>('/v1/blueprint/metrics')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How object types relate. Without these the catalog reads as a pile of
|
|
72
|
+
* disconnected tables, and nothing can be joined. */
|
|
73
|
+
blueprintLinkTypes() {
|
|
74
|
+
return this.getList<unknown>('/v1/blueprint/link-types')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Agents ────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
agents() {
|
|
80
|
+
return this.getList<unknown>('/v1/config/agents')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
agent(id: string) {
|
|
84
|
+
return this.get<unknown>(`/v1/config/agents/${encodeURIComponent(id)}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The agent's full composition: prompts, skills, models, knowledge, app
|
|
89
|
+
* installs, MCP servers, channels, automations.
|
|
90
|
+
*
|
|
91
|
+
* `GET /config/agents/:id` returns the config ROW — flat scalars with every
|
|
92
|
+
* relation null — so it answers "what are its settings" and not "what is
|
|
93
|
+
* this agent made of". Nobody can review or rebuild an agent from that.
|
|
94
|
+
*/
|
|
95
|
+
async agentComposition(id: string): Promise<Record<string, unknown>> {
|
|
96
|
+
const body = await this.get<{ data?: Record<string, unknown> } | Record<string, unknown>>(
|
|
97
|
+
`/v1/config/agents/${encodeURIComponent(id)}/snapshot/live`,
|
|
98
|
+
)
|
|
99
|
+
return ((body as { data?: Record<string, unknown> })?.data ?? body ?? {}) as Record<string, unknown>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Frontera Apps in this workspace (not integrations — see `plugin`). */
|
|
103
|
+
platformApps() {
|
|
104
|
+
return this.getList<unknown>('/v1/platform-apps/')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `{ agentId, revision, snapshot }`, or null when nothing is staged. */
|
|
108
|
+
async agentDraft(id: string): Promise<AgentDraft | null> {
|
|
109
|
+
// Wrapped twice: `ok({ data: … })` around the SDK's own envelope.
|
|
110
|
+
const body = await this.get<{ data?: AgentDraft | null } | AgentDraft | null>(
|
|
111
|
+
`/v1/config/agents/${encodeURIComponent(id)}/draft-v2`,
|
|
112
|
+
)
|
|
113
|
+
if (!body) return null
|
|
114
|
+
const inner = (body as { data?: AgentDraft | null }).data
|
|
115
|
+
return inner === undefined ? (body as AgentDraft) : inner
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stage a partial snapshot onto the draft.
|
|
120
|
+
*
|
|
121
|
+
* `expectedRevision` is the whole concurrency story: undefined is
|
|
122
|
+
* collaborative, a number means "the draft I read", and a stale one comes
|
|
123
|
+
* back as DRAFT_CONFLICT rather than overwriting whoever wrote first.
|
|
124
|
+
*/
|
|
125
|
+
async patchAgentDraft(
|
|
126
|
+
id: string,
|
|
127
|
+
patch: Record<string, unknown>,
|
|
128
|
+
expectedRevision?: number,
|
|
129
|
+
): Promise<AgentDraft> {
|
|
130
|
+
const body = await this.client.request<{ data?: AgentDraft } | AgentDraft>(
|
|
131
|
+
`/v1/config/agents/${encodeURIComponent(id)}/draft-v2`,
|
|
132
|
+
{
|
|
133
|
+
method: 'PATCH',
|
|
134
|
+
body: expectedRevision === undefined ? { patch } : { patch, expectedRevision },
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
return ((body as { data?: AgentDraft }).data ?? body) as AgentDraft
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
publishAgent(id: string, args: { notes?: string; expectedRevision?: number } = {}) {
|
|
141
|
+
return this.client.request<unknown>(`/v1/config/agents/${encodeURIComponent(id)}/publish`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: args,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
discardAgentDraft(id: string) {
|
|
148
|
+
return this.client.request<unknown>(`/v1/config/agents/${encodeURIComponent(id)}/draft-v2`, {
|
|
149
|
+
method: 'DELETE',
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
agentVersions(id: string) {
|
|
154
|
+
return this.getList<unknown>(`/v1/config/agents/${encodeURIComponent(id)}/versions`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Skills ────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
workspaceSkills() {
|
|
160
|
+
return this.getList<unknown>('/v1/config/workspace-skills')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
workspaceSkill(id: string) {
|
|
164
|
+
return this.get<unknown>(`/v1/config/workspace-skills/${encodeURIComponent(id)}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Plugins (integrations and MCP servers) ────────────────────────────────
|
|
168
|
+
|
|
169
|
+
pluginInstalls() {
|
|
170
|
+
return this.getList<unknown>('/v1/apps/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pluginCatalog() {
|
|
174
|
+
return this.getList<unknown>('/v1/apps/catalog')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
knowledgeBases(workspaceId: string) {
|
|
180
|
+
return this.getList<unknown>(`/v1/workspace-knowledge?workspaceId=${encodeURIComponent(workspaceId)}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sources in a WORKSPACE knowledge base, keyed by its id.
|
|
185
|
+
*
|
|
186
|
+
* Not `/v1/knowledge/:name/sources` — that is the per-AGENT path, and it
|
|
187
|
+
* defaults `agentName` to "default", so a workspace base's name reached a
|
|
188
|
+
* uuid column and came back as a raw postgres 22P02 rendered as a 500.
|
|
189
|
+
*/
|
|
190
|
+
knowledgeSources(id: string) {
|
|
191
|
+
return this.getList<unknown>(`/v1/workspace-knowledge/${encodeURIComponent(id)}/sources`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface RegistryFile {
|
|
2
|
+
path: string
|
|
3
|
+
content: string
|
|
4
|
+
type: 'ui' | 'lib' | 'style' | 'skill'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RegistryItem {
|
|
8
|
+
name: string
|
|
9
|
+
type: 'ui' | 'lib' | 'style' | 'skill'
|
|
10
|
+
version: string
|
|
11
|
+
description: string
|
|
12
|
+
dependencies: string[]
|
|
13
|
+
registryDependencies: string[]
|
|
14
|
+
files: RegistryFile[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Client for the component registry.
|
|
19
|
+
*
|
|
20
|
+
* Unauthenticated on purpose (see the service router): this serves the same
|
|
21
|
+
* first-party component source that ships in the platform's own bundle, and
|
|
22
|
+
* requiring a credential would stop `frontera app init` scaffolding before a
|
|
23
|
+
* key exists.
|
|
24
|
+
*/
|
|
25
|
+
export class RegistryApi {
|
|
26
|
+
constructor(private readonly apiBaseUrl: string) {}
|
|
27
|
+
|
|
28
|
+
async list(): Promise<Array<Omit<RegistryItem, 'files'>>> {
|
|
29
|
+
const res = await fetch(`${this.apiBaseUrl}/v1/apps/registry`)
|
|
30
|
+
if (!res.ok) throw new Error(`registry unavailable (${res.status})`)
|
|
31
|
+
const body = (await res.json()) as { data: Array<Omit<RegistryItem, 'files'>> }
|
|
32
|
+
return body.data
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The item plus its transitive dependencies, dependencies first. */
|
|
36
|
+
async resolve(name: string): Promise<RegistryItem[]> {
|
|
37
|
+
const res = await fetch(`${this.apiBaseUrl}/v1/apps/registry/${encodeURIComponent(name)}`)
|
|
38
|
+
if (res.status === 404) throw new Error(`unknown registry item: ${name}`)
|
|
39
|
+
if (!res.ok) throw new Error(`registry unavailable (${res.status})`)
|
|
40
|
+
const body = (await res.json()) as { data: RegistryItem[] }
|
|
41
|
+
return body.data
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { UsageError } from './errors'
|
|
2
|
+
|
|
3
|
+
export type FlagKind = 'string' | 'boolean'
|
|
4
|
+
export type FlagSpec = Readonly<Record<string, FlagKind>>
|
|
5
|
+
|
|
6
|
+
export interface ParsedArgs {
|
|
7
|
+
positional: string[]
|
|
8
|
+
flags: Record<string, string | boolean>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Argument parser.
|
|
13
|
+
*
|
|
14
|
+
* Strict on purpose. The parser it replaces returned `args[i + 1]` for any
|
|
15
|
+
* `--flag`, so `--dir --force` set the directory to "--force" and the command
|
|
16
|
+
* ran against a path that does not exist while reporting success. It also
|
|
17
|
+
* ignored unknown flags outright, so a typo silently changed nothing. Both
|
|
18
|
+
* failures are worse for a model caller than for a person: the command appears
|
|
19
|
+
* to work, so nothing prompts a second look.
|
|
20
|
+
*
|
|
21
|
+
* Every rejection is a `UsageError`, which the shell maps to exit 2 — the code
|
|
22
|
+
* that tells a caller to fix the command rather than retry it.
|
|
23
|
+
*/
|
|
24
|
+
export function parseArgs(
|
|
25
|
+
argv: string[],
|
|
26
|
+
spec: FlagSpec,
|
|
27
|
+
aliases: Readonly<Record<string, string>> = {},
|
|
28
|
+
): ParsedArgs {
|
|
29
|
+
const positional: string[] = []
|
|
30
|
+
const flags: Record<string, string | boolean> = {}
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
let token = argv[i]!
|
|
34
|
+
|
|
35
|
+
// Short flags (`-f value`, `-f=value`). Conventional for the common ones,
|
|
36
|
+
// and their absence silently turned `-f` into a positional argument — so a
|
|
37
|
+
// documented example read the file it was given as an agent name instead.
|
|
38
|
+
if (/^-[A-Za-z]/.test(token)) {
|
|
39
|
+
const eqAt = token.indexOf('=')
|
|
40
|
+
const short = (eqAt === -1 ? token.slice(1) : token.slice(1, eqAt))
|
|
41
|
+
const long = aliases[short]
|
|
42
|
+
if (!long) {
|
|
43
|
+
throw new UsageError(
|
|
44
|
+
`unknown flag: -${short}`,
|
|
45
|
+
'run the command with --help to see the flags it accepts',
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
token = eqAt === -1 ? `--${long}` : `--${long}${token.slice(eqAt)}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `--` ends flag parsing. Everything after is positional, even if it
|
|
52
|
+
// starts with dashes — the escape hatch for a literal value.
|
|
53
|
+
if (token === '--') {
|
|
54
|
+
positional.push(...argv.slice(i + 1))
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A lone `-` is the stdin convention, and `-1` is a value. Neither is a
|
|
59
|
+
// flag, so only `--` prefixes are parsed as one.
|
|
60
|
+
if (!token.startsWith('--')) {
|
|
61
|
+
positional.push(token)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const eq = token.indexOf('=')
|
|
66
|
+
const name = eq === -1 ? token.slice(2) : token.slice(2, eq)
|
|
67
|
+
const kind = spec[name]
|
|
68
|
+
|
|
69
|
+
if (!kind) {
|
|
70
|
+
throw new UsageError(
|
|
71
|
+
`unknown flag: --${name}`,
|
|
72
|
+
'run the command with --help to see the flags it accepts',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (kind === 'boolean') {
|
|
77
|
+
if (eq !== -1) {
|
|
78
|
+
throw new UsageError(
|
|
79
|
+
`--${name} is a boolean flag and takes no value`,
|
|
80
|
+
`pass --${name} on its own`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
flags[name] = true
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (eq !== -1) {
|
|
88
|
+
// `--dir=` is a deliberate empty string, not a missing value.
|
|
89
|
+
flags[name] = token.slice(eq + 1)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const value = argv[i + 1]
|
|
94
|
+
// The defect this replaces: a following flag is not a value. `--` is not
|
|
95
|
+
// one either — it terminates parsing, so consuming it would swallow the
|
|
96
|
+
// escape hatch.
|
|
97
|
+
if (value === undefined || value === '--' || (value.startsWith('--') && value.length > 2)) {
|
|
98
|
+
throw new UsageError(
|
|
99
|
+
`--${name} needs a value`,
|
|
100
|
+
`pass it as --${name} <value> or --${name}=<value>`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
flags[name] = value
|
|
104
|
+
i += 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { positional, flags }
|
|
108
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an agent's composition the way someone configuring it needs to see it.
|
|
3
|
+
*
|
|
4
|
+
* Two things this exists to avoid. The default read used to be the config row:
|
|
5
|
+
* flat scalars, every relation null — it told you the temperature and nothing
|
|
6
|
+
* about what the agent is made of. And the snapshot's relations carry only
|
|
7
|
+
* uuids, so printing them raw ("Knowledge: a7452079-…") is technically
|
|
8
|
+
* complete and practically useless. Ids are resolved to names here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type Row = Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
export interface Lookups {
|
|
14
|
+
/** installId → readable plugin name. */
|
|
15
|
+
plugins: Map<string, string>
|
|
16
|
+
/** knowledge config id → readable name. */
|
|
17
|
+
knowledge: Map<string, string>
|
|
18
|
+
/** workspace skill id → readable name. */
|
|
19
|
+
skills: Map<string, string>
|
|
20
|
+
/** Identity the snapshot's config does not carry. */
|
|
21
|
+
identity?: { id?: string; slug?: string; kind?: string; version?: number }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function arr(value: unknown): Row[] {
|
|
25
|
+
return Array.isArray(value) ? (value as Row[]) : []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function str(row: Row, ...keys: string[]): string {
|
|
29
|
+
for (const k of keys) {
|
|
30
|
+
const v = row[k]
|
|
31
|
+
if (typeof v === 'string' && v.length > 0) return v
|
|
32
|
+
}
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function section(title: string, items: string[]): string[] {
|
|
37
|
+
if (items.length === 0) return [`${title}: —`]
|
|
38
|
+
return [`${title} (${items.length})`, ...items.map((i) => ` ${i}`)]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Name if we have one, id if we do not — never a bare uuid with no context. */
|
|
42
|
+
function label(id: string, lookup: Map<string, string>): string {
|
|
43
|
+
const name = lookup.get(id)
|
|
44
|
+
return name ? name : `${id} (unresolved)`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderComposition(snapshot: Row, lookups: Lookups): string {
|
|
48
|
+
const config = (snapshot.config ?? {}) as Row
|
|
49
|
+
const lines: string[] = []
|
|
50
|
+
|
|
51
|
+
lines.push(str(config, 'name') || str(config, 'agentId'))
|
|
52
|
+
if (str(config, 'description')) lines.push(str(config, 'description'))
|
|
53
|
+
lines.push('')
|
|
54
|
+
|
|
55
|
+
for (const [k, v] of [
|
|
56
|
+
['slug', str(config, 'agentId') || (lookups.identity?.slug ?? '')],
|
|
57
|
+
['id', str(config, 'id') || (lookups.identity?.id ?? '')],
|
|
58
|
+
['kind', str(config, 'kind') || (lookups.identity?.kind ?? '')],
|
|
59
|
+
['status', str(config, 'lifecycleStatus')],
|
|
60
|
+
['default mode', str(config, 'defaultMode')],
|
|
61
|
+
['live version', lookups.identity?.version ? String(lookups.identity.version) : ''],
|
|
62
|
+
] as Array<[string, string]>) {
|
|
63
|
+
if (v) lines.push(`${k.padEnd(14)}${v}`)
|
|
64
|
+
}
|
|
65
|
+
lines.push('')
|
|
66
|
+
|
|
67
|
+
lines.push(
|
|
68
|
+
...section(
|
|
69
|
+
'Models',
|
|
70
|
+
arr(snapshot.modelConfigs).map((m) =>
|
|
71
|
+
`${str(m, 'modeKey').padEnd(22)}${str(m, 'modelSlug')}${
|
|
72
|
+
str(m, 'displayName') ? ` (${str(m, 'displayName')})` : ''
|
|
73
|
+
}`,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
'',
|
|
77
|
+
...section(
|
|
78
|
+
'Prompts',
|
|
79
|
+
arr(snapshot.prompts).map((p) => {
|
|
80
|
+
const body = str(p, 'content').replace(/\s+/g, ' ')
|
|
81
|
+
return `${(str(p, 'kind', 'filename') || 'prompt').padEnd(22)}${body.slice(0, 60)}${
|
|
82
|
+
body.length > 60 ? '…' : ''
|
|
83
|
+
}`
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
'',
|
|
87
|
+
// `skills` is an OBJECT ({ workspaceBindings: [...] }), not an array —
|
|
88
|
+
// treating it as a list silently reported "no skills" for every agent
|
|
89
|
+
// that had them.
|
|
90
|
+
...section(
|
|
91
|
+
'Skills',
|
|
92
|
+
arr((snapshot.skills as Row | undefined)?.workspaceBindings).map((s) => {
|
|
93
|
+
const name = label(str(s, 'workspaceSkillId'), lookups.skills)
|
|
94
|
+
return s.enabled === false ? `${name} (disabled)` : name
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
'',
|
|
98
|
+
...section(
|
|
99
|
+
'Knowledge',
|
|
100
|
+
[
|
|
101
|
+
...arr(snapshot.knowledgeAttachments).map((k) =>
|
|
102
|
+
label(str(k, 'knowledgeConfigId'), lookups.knowledge),
|
|
103
|
+
),
|
|
104
|
+
...arr(snapshot.knowledgeConfigs).map((k) => str(k, 'name') || label(str(k, 'id'), lookups.knowledge)),
|
|
105
|
+
],
|
|
106
|
+
),
|
|
107
|
+
'',
|
|
108
|
+
...section('Plugins', [
|
|
109
|
+
...arr(snapshot.appInstalls).map((a) => {
|
|
110
|
+
const name = label(str(a, 'installId'), lookups.plugins)
|
|
111
|
+
const off = a.enabled === false ? ' (disabled)' : ''
|
|
112
|
+
return `${name}${off}`
|
|
113
|
+
}),
|
|
114
|
+
...arr(snapshot.mcpServers).map((m) => `${str(m, 'name', 'serverName', 'mcpServerId')} (mcp)`),
|
|
115
|
+
]),
|
|
116
|
+
'',
|
|
117
|
+
...section(
|
|
118
|
+
'Channels',
|
|
119
|
+
// Channel names need `/v1/channels`, which is not on the programmatic
|
|
120
|
+
// auth surface — widening that for a label is not worth it, so the id is
|
|
121
|
+
// shown plainly rather than dressed up as a name.
|
|
122
|
+
arr(snapshot.channelBindings).map((c) => `channel ${str(c, 'channelId')}`),
|
|
123
|
+
),
|
|
124
|
+
'',
|
|
125
|
+
...section(
|
|
126
|
+
'Automations',
|
|
127
|
+
arr(snapshot.automations).map((a) => str(a, 'name', 'schedule', 'id')),
|
|
128
|
+
),
|
|
129
|
+
'',
|
|
130
|
+
...section(
|
|
131
|
+
'Packs',
|
|
132
|
+
arr(snapshot.appliedPacks).map((p) => str(p, 'name', 'packId', 'id')),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Capabilities are per-app grants and there are often dozens; a count with
|
|
137
|
+
// the approval posture is what a reviewer reads, and --json has the rest.
|
|
138
|
+
const caps = arr(snapshot.appCapabilities)
|
|
139
|
+
const needsApproval = caps.filter((c) => c.requiresApprovalOverride === true).length
|
|
140
|
+
lines.push(
|
|
141
|
+
'',
|
|
142
|
+
caps.length === 0
|
|
143
|
+
? 'Capabilities: —'
|
|
144
|
+
: `Capabilities (${caps.length})` +
|
|
145
|
+
(needsApproval > 0 ? ` — ${needsApproval} require approval` : ''),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const caseDefinition = snapshot.caseDefinition as Row | null | undefined
|
|
149
|
+
if (caseDefinition && typeof caseDefinition === 'object') {
|
|
150
|
+
lines.push('', ...section('Case columns', arr(caseDefinition.columns).map((c) => str(c, 'name', 'key'))))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push('', 'Full document: add --json')
|
|
154
|
+
return lines.join('\n')
|
|
155
|
+
}
|