@frontera-sdk/cli 1.45.11 → 1.45.12
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.45.
|
|
3
|
+
"version": "1.45.12",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"build:release": "bun run scripts/build-release.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@frontera-sdk/automation": "1.45.
|
|
42
|
-
"@frontera-sdk/core": "1.45.
|
|
41
|
+
"@frontera-sdk/automation": "1.45.12",
|
|
42
|
+
"@frontera-sdk/core": "1.45.12",
|
|
43
43
|
"gray-matter": "^4.0.3",
|
|
44
44
|
"yaml": "^2.9.0"
|
|
45
45
|
},
|
|
@@ -26,6 +26,37 @@ export interface AutomationVersionSummary {
|
|
|
26
26
|
createdAt: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** One agent↔automation binding, as the bindings routes return it. */
|
|
30
|
+
export interface AgentAutomationBinding {
|
|
31
|
+
agentId: string
|
|
32
|
+
agentSlug: string
|
|
33
|
+
agentName: string
|
|
34
|
+
enabled: boolean
|
|
35
|
+
requiresApproval: boolean
|
|
36
|
+
waitSeconds: number
|
|
37
|
+
createdBy: string
|
|
38
|
+
createdAt: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One binding seen from the agent's side.
|
|
43
|
+
*
|
|
44
|
+
* `projects` is the field worth reading: a binding can exist and grant nothing
|
|
45
|
+
* when the author has withdrawn `trigger: { agent: true }` from the live
|
|
46
|
+
* version, and a list without it reads as a working permission.
|
|
47
|
+
*/
|
|
48
|
+
export interface AgentBindingView {
|
|
49
|
+
automationSlug: string
|
|
50
|
+
automationId: string
|
|
51
|
+
enabled: boolean
|
|
52
|
+
requiresApproval: boolean
|
|
53
|
+
waitSeconds: number
|
|
54
|
+
projects: boolean
|
|
55
|
+
inertReason: 'no_live_version' | 'not_agent_callable' | 'version_retired' | 'disabled' | null
|
|
56
|
+
createdBy: string
|
|
57
|
+
createdAt: string
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
export interface AutomationDeployResult {
|
|
30
61
|
automationId: string
|
|
31
62
|
versionId: string
|
|
@@ -293,6 +324,44 @@ export class AutomationApi {
|
|
|
293
324
|
)
|
|
294
325
|
}
|
|
295
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Which agents may run this automation from a conversation.
|
|
329
|
+
*
|
|
330
|
+
* Two locks have to be open before any of this matters: the automation's own
|
|
331
|
+
* manifest must declare `trigger: { agent: true }` (the author's half), and a
|
|
332
|
+
* binding must name the agent (the operator's half, these three calls).
|
|
333
|
+
*/
|
|
334
|
+
bindings(slug: string): Promise<AgentAutomationBinding[]> {
|
|
335
|
+
return this.client.request<AgentAutomationBinding[]>(
|
|
336
|
+
`/v1/automations/${encodeURIComponent(slug)}/bindings`,
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
bind(
|
|
341
|
+
slug: string,
|
|
342
|
+
agentSlug: string,
|
|
343
|
+
options: { enabled?: boolean; requiresApproval?: boolean; waitSeconds?: number } = {},
|
|
344
|
+
): Promise<AgentAutomationBinding> {
|
|
345
|
+
return this.client.request<AgentAutomationBinding>(
|
|
346
|
+
`/v1/automations/${encodeURIComponent(slug)}/bindings/${encodeURIComponent(agentSlug)}`,
|
|
347
|
+
{ method: 'PUT', body: options },
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The same question from the agent's side: what may this agent start? */
|
|
352
|
+
agentBindings(agentSlug: string): Promise<AgentBindingView[]> {
|
|
353
|
+
return this.client.request<AgentBindingView[]>(
|
|
354
|
+
`/v1/automations/agent-bindings/${encodeURIComponent(agentSlug)}`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
unbind(slug: string, agentSlug: string): Promise<{ unbound: boolean }> {
|
|
359
|
+
return this.client.request<{ unbound: boolean }>(
|
|
360
|
+
`/v1/automations/${encodeURIComponent(slug)}/bindings/${encodeURIComponent(agentSlug)}`,
|
|
361
|
+
{ method: 'DELETE' },
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
296
365
|
setEnabled(slug: string, enabled: boolean): Promise<{ slug: string; enabled: boolean }> {
|
|
297
366
|
return this.client.request<{ slug: string; enabled: boolean }>(
|
|
298
367
|
`/v1/automations/${encodeURIComponent(slug)}/enabled`,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `frontera automation bind|unbind|bindings` — the operator's half of making an
|
|
3
|
+
* automation callable from a conversation.
|
|
4
|
+
*
|
|
5
|
+
* The author's half is in the manifest (`trigger: { agent: true }`, plus a
|
|
6
|
+
* description of the automation and of every input) and is checked at deploy.
|
|
7
|
+
* These verbs cannot substitute for it: the service refuses a binding whose
|
|
8
|
+
* automation does not declare the flag, and the message says so.
|
|
9
|
+
*
|
|
10
|
+
* `bind` and `unbind` are separate verbs rather than `bind --off`, for the same
|
|
11
|
+
* reason `disable` is not `enable --false`: taking a permission away is done
|
|
12
|
+
* under pressure, and a flag is one more thing to get wrong.
|
|
13
|
+
*/
|
|
14
|
+
import { AutomationApi } from '../../api/automation-api'
|
|
15
|
+
import { UsageError } from '../../errors'
|
|
16
|
+
import { table } from '../../table'
|
|
17
|
+
import { flagString, type Command, type CommandContext } from '../types'
|
|
18
|
+
|
|
19
|
+
function requireSlug(ctx: CommandContext): string {
|
|
20
|
+
const slug = ctx.positional[0]
|
|
21
|
+
if (!slug) {
|
|
22
|
+
throw new UsageError(
|
|
23
|
+
'missing <slug>',
|
|
24
|
+
'run `frontera automation list` — then pass the slug of the one you mean',
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return slug
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireAgent(ctx: CommandContext): string {
|
|
31
|
+
const agent = flagString(ctx, 'agent')
|
|
32
|
+
if (!agent) {
|
|
33
|
+
throw new UsageError(
|
|
34
|
+
'missing --agent <agent-slug>',
|
|
35
|
+
'a binding names exactly one agent — there is no "all agents" form on purpose',
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return agent
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bind: Command = {
|
|
42
|
+
meta: {
|
|
43
|
+
noun: 'automation',
|
|
44
|
+
verb: 'bind',
|
|
45
|
+
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
46
|
+
flags: { agent: 'string', approval: 'string', wait: 'string' },
|
|
47
|
+
summary: 'Let one agent run this automation from a conversation',
|
|
48
|
+
examples: [
|
|
49
|
+
'frontera automation bind reconcile-invoices --agent finance-analyst',
|
|
50
|
+
'frontera automation bind reconcile-invoices --agent analyst --approval auto',
|
|
51
|
+
'frontera automation bind reconcile-invoices --agent analyst --wait 120',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async run(ctx) {
|
|
56
|
+
const slug = requireSlug(ctx)
|
|
57
|
+
const agent = requireAgent(ctx)
|
|
58
|
+
|
|
59
|
+
// `required` is the default at every layer, and it is spelled out here
|
|
60
|
+
// rather than left to the server's default so that `--approval` reads as a
|
|
61
|
+
// choice an operator made either way.
|
|
62
|
+
const approval = flagString(ctx, 'approval') ?? 'required'
|
|
63
|
+
if (approval !== 'required' && approval !== 'auto') {
|
|
64
|
+
throw new UsageError(
|
|
65
|
+
`--approval must be "required" or "auto", got "${approval}"`,
|
|
66
|
+
'`required` asks a human before each call; `auto` does not',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rawWait = flagString(ctx, 'wait')
|
|
71
|
+
const waitSeconds = rawWait === undefined ? undefined : Number(rawWait)
|
|
72
|
+
if (waitSeconds !== undefined && !Number.isInteger(waitSeconds)) {
|
|
73
|
+
throw new UsageError(
|
|
74
|
+
`--wait must be a whole number of seconds, got "${rawWait}"`,
|
|
75
|
+
'how long the agent waits for the run before it is told it is still going (5–300)',
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).bind(
|
|
80
|
+
slug,
|
|
81
|
+
agent,
|
|
82
|
+
{ requiresApproval: approval === 'required', waitSeconds },
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
data: result,
|
|
87
|
+
text:
|
|
88
|
+
`Bound ${slug} to ${result.agentSlug} — approval ${result.requiresApproval ? 'required' : 'not required'}, `
|
|
89
|
+
+ `waits ${result.waitSeconds}s.`,
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const unbind: Command = {
|
|
95
|
+
meta: {
|
|
96
|
+
noun: 'automation',
|
|
97
|
+
verb: 'unbind',
|
|
98
|
+
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
99
|
+
flags: { agent: 'string' },
|
|
100
|
+
summary: 'Stop one agent being able to run this automation',
|
|
101
|
+
examples: ['frontera automation unbind reconcile-invoices --agent finance-analyst'],
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async run(ctx) {
|
|
105
|
+
const slug = requireSlug(ctx)
|
|
106
|
+
const agent = requireAgent(ctx)
|
|
107
|
+
await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).unbind(slug, agent)
|
|
108
|
+
return { data: { unbound: true }, text: `${agent} can no longer run ${slug}.` }
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Both directions of the same question, because an operator asks it from both
|
|
114
|
+
* ends: the function owner asks who can run their code, and a security review
|
|
115
|
+
* asks what one agent can reach. Answering the second by walking every
|
|
116
|
+
* automation's bindings is the kind of fan-out that gets skipped.
|
|
117
|
+
*/
|
|
118
|
+
const bindings: Command = {
|
|
119
|
+
meta: {
|
|
120
|
+
noun: 'automation',
|
|
121
|
+
verb: 'bindings',
|
|
122
|
+
args: [
|
|
123
|
+
{
|
|
124
|
+
name: 'slug',
|
|
125
|
+
required: false,
|
|
126
|
+
description: 'automation slug — omit it and pass --agent to ask from the agent side',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
flags: { agent: 'string' },
|
|
130
|
+
summary: 'List the agents that may run an automation, or what one agent may run',
|
|
131
|
+
examples: [
|
|
132
|
+
'frontera automation bindings reconcile-invoices',
|
|
133
|
+
'frontera automation bindings --agent finance-analyst',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async run(ctx) {
|
|
138
|
+
const client = new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
139
|
+
const agent = flagString(ctx, 'agent')
|
|
140
|
+
const slug = ctx.positional[0]
|
|
141
|
+
|
|
142
|
+
if (!slug && !agent) {
|
|
143
|
+
throw new UsageError(
|
|
144
|
+
'pass an automation slug, or --agent <agent-slug>',
|
|
145
|
+
'the first lists who may run one automation; the second lists what one agent may run',
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (agent) {
|
|
150
|
+
const rows = await client.agentBindings(agent)
|
|
151
|
+
return {
|
|
152
|
+
data: rows,
|
|
153
|
+
text:
|
|
154
|
+
rows.length === 0
|
|
155
|
+
? `${agent} cannot run any automation from a conversation.`
|
|
156
|
+
: table(
|
|
157
|
+
['automation', 'grants a tool', 'approval', 'wait', 'why not'],
|
|
158
|
+
rows.map((b) => [
|
|
159
|
+
b.automationSlug,
|
|
160
|
+
// The load-bearing column. A binding that grants nothing
|
|
161
|
+
// still has a row, and a list that showed only the row would
|
|
162
|
+
// read as a working permission.
|
|
163
|
+
b.projects ? 'yes' : 'no',
|
|
164
|
+
b.requiresApproval ? 'required' : 'auto',
|
|
165
|
+
`${b.waitSeconds}s`,
|
|
166
|
+
b.inertReason ?? '',
|
|
167
|
+
]),
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const rows = await client.bindings(slug!)
|
|
173
|
+
return {
|
|
174
|
+
data: rows,
|
|
175
|
+
text:
|
|
176
|
+
rows.length === 0
|
|
177
|
+
? `No agent can run ${slug} from a conversation.`
|
|
178
|
+
: table(
|
|
179
|
+
['agent', 'name', 'enabled', 'approval', 'wait', 'bound by'],
|
|
180
|
+
rows.map((b) => [
|
|
181
|
+
b.agentSlug,
|
|
182
|
+
b.agentName,
|
|
183
|
+
String(b.enabled),
|
|
184
|
+
b.requiresApproval ? 'required' : 'auto',
|
|
185
|
+
`${b.waitSeconds}s`,
|
|
186
|
+
b.createdBy,
|
|
187
|
+
]),
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const automationBindingCommands: Command[] = [bind, unbind, bindings]
|
|
@@ -11,6 +11,7 @@ import { automationInit } from './init'
|
|
|
11
11
|
import { automationPull } from './pull'
|
|
12
12
|
import { automationRun, automationRuns } from './run'
|
|
13
13
|
import { automationDev } from './dev'
|
|
14
|
+
import { automationBindingCommands } from './bindings'
|
|
14
15
|
import { buildAndExtract } from './build-entry'
|
|
15
16
|
|
|
16
17
|
export { buildAndExtract } from './build-entry'
|
|
@@ -313,4 +314,5 @@ export const automationCommands: Command[] = [
|
|
|
313
314
|
automationPull,
|
|
314
315
|
automationInit,
|
|
315
316
|
automationDev,
|
|
317
|
+
...automationBindingCommands,
|
|
316
318
|
]
|
package/src/flag-help.ts
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* fails if a command declares a flag this file does not describe.
|
|
14
14
|
*/
|
|
15
15
|
export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
16
|
+
// Agent↔automation bindings. `agent` is deliberately singular: a binding
|
|
17
|
+
// names one agent, and there is no wildcard form.
|
|
18
|
+
agent: 'agent slug this binding is for — one agent, no wildcard',
|
|
19
|
+
approval: '"required" (default) asks a human before each call; "auto" does not',
|
|
20
|
+
wait: 'seconds the agent waits for the run before it is told the run is still going (5–300)',
|
|
16
21
|
// Declarative Blueprint authoring — the file tree, and what applying it may do.
|
|
17
22
|
prune: 'also remove artifacts that are on the draft but absent from the file tree',
|
|
18
23
|
dataset: 'dataset name the object type reads through; the revision is resolved at apply time',
|