@frontera-sdk/cli 1.52.44 → 1.52.46
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 -4
- package/src/api/apps-api.ts +3 -1
- package/src/api/automation-api.ts +32 -0
- package/src/api/credential-failure.ts +1 -1
- package/src/api/governed-action-api.ts +24 -0
- package/src/api/media-api.ts +24 -0
- package/src/api/platform-api.ts +13 -0
- package/src/binding-registry.ts +9 -2
- package/src/blueprint/compile.ts +3 -1
- package/src/blueprint/ontology-edit-plan.ts +71 -4
- package/src/blueprint/org-name-check.ts +64 -0
- package/src/blueprint/tree.ts +28 -0
- package/src/commands/action/deploy.ts +22 -5
- package/src/commands/action/index-commands.ts +2 -0
- package/src/commands/action/retire.ts +60 -0
- package/src/commands/agent/compose.ts +1 -0
- package/src/commands/agent/index-commands.ts +42 -0
- package/src/commands/app/deploy.ts +1 -1
- package/src/commands/app/dev.ts +2 -2
- package/src/commands/app/save.ts +1 -1
- package/src/commands/auth/add.ts +5 -1
- package/src/commands/auth/current.ts +6 -0
- package/src/commands/auth/index-commands.ts +3 -0
- package/src/commands/auth/login.ts +84 -0
- package/src/commands/auth/use.ts +21 -2
- package/src/commands/automation/bindings.ts +54 -10
- package/src/commands/automation/index-commands.ts +2 -3
- package/src/commands/blueprint/bind.ts +12 -1
- package/src/commands/blueprint/declarative.ts +6 -2
- package/src/commands/conversation/index-commands.ts +66 -0
- package/src/commands/conversation/local-runtime.ts +18 -0
- package/src/commands/kit/vendor.ts +62 -48
- package/src/commands/local/check.ts +57 -11
- package/src/commands/local/data.ts +75 -7
- package/src/commands/local/dev.ts +77 -9
- package/src/commands/local/embeddings.ts +74 -0
- package/src/commands/local/index-commands.ts +133 -11
- package/src/commands/local/init.ts +22 -2
- package/src/commands/local/new.ts +78 -0
- package/src/commands/local/publish.ts +34 -0
- package/src/commands/local/steps.ts +105 -0
- package/src/commands/local/sync-request.ts +69 -23
- package/src/commands/local/sync.ts +2 -0
- package/src/commands/media/index-commands.ts +58 -3
- package/src/commands/registry.ts +11 -3
- package/src/commands/trace/index-commands.ts +56 -0
- package/src/commands/workspace/index-commands.ts +14 -7
- package/src/config.ts +4 -1
- package/src/dev-broker.ts +15 -1
- package/src/device-login.ts +143 -0
- package/src/exit.ts +36 -8
- package/src/flag-help.ts +31 -0
- package/src/forge/claude-adapter.ts +17 -4
- package/src/forge/permission-rules.ts +158 -0
- package/src/forge/touches.ts +7 -6
- package/src/function-outside-imports.ts +13 -2
- package/src/function-source.ts +90 -0
- package/src/harness.ts +4 -4
- package/src/key-reach.ts +64 -0
- package/src/local/local-actions.ts +15 -3
- package/src/local/local-apply-error.ts +26 -0
- package/src/local/local-apps.ts +9 -3
- package/src/local/local-blueprint-schemas.ts +15 -1
- package/src/local/local-confidential.ts +81 -0
- package/src/local/local-data-load.ts +130 -0
- package/src/local/local-dev-references.ts +24 -4
- package/src/local/local-embedding-setup.ts +62 -0
- package/src/local/local-jobs.ts +108 -0
- package/src/local/local-keys.ts +5 -3
- package/src/local/local-new.ts +148 -0
- package/src/local/local-org-settings.ts +1 -1
- package/src/local/local-person-steps.ts +340 -0
- package/src/local/local-personas.ts +16 -5
- package/src/local/local-portability.ts +22 -1
- package/src/local/local-publish.ts +26 -0
- package/src/local/local-request-evidence.ts +35 -6
- package/src/local/local-runtime-history.ts +4 -3
- package/src/local/local-said-once.ts +51 -0
- package/src/local/local-scaffold.ts +40 -2
- package/src/local/local-schemas.ts +60 -11
- package/src/local/local-seed.ts +1 -1
- package/src/local/local-stand-ins.ts +44 -0
- package/src/local/local-studio-server.ts +13 -7
- package/src/local/local-supervisor.ts +22 -0
- package/src/local/local-sync-models.ts +5 -2
- package/src/local/local-sync-names.ts +3 -0
- package/src/local/local-sync-portable.ts +64 -15
- package/src/local/local-sync-precheck.ts +30 -1
- package/src/local/local-sync-push.ts +81 -8
- package/src/local/local-sync-remote.ts +10 -7
- package/src/local/local-test-models.ts +173 -0
- package/src/local/local-test-runner.ts +232 -25
- package/src/local/local-test-scope.ts +174 -0
- package/src/local/local-ungranted-reads.ts +55 -0
- package/src/local/local-writers.ts +171 -0
- package/src/main.ts +75 -17
- package/src/mcp/browser/ax-snapshot.ts +13 -1
- package/src/mcp/browser/page-scripts.ts +1 -0
- package/src/mcp/mcp-conversations.ts +171 -0
- package/src/mcp/mcp-server.ts +4 -1
- package/src/mcp/mcp-traces.ts +7 -2
- package/src/mcp/tools/agent-tools.ts +94 -2
- package/src/mcp/tools/project-tools.ts +149 -13
- package/src/mcp/tools/test-tools.ts +64 -13
- package/src/output.ts +22 -7
- package/src/pack.ts +16 -0
- package/src/project-context.ts +18 -5
- package/src/project.ts +4 -0
- package/src/remote-guard.ts +52 -0
- package/src/vendor/kit-assets.json +67 -6
|
@@ -410,6 +410,47 @@ const discard: Command = {
|
|
|
410
410
|
},
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
/**
|
|
414
|
+
* `frontera agent behavior clear <agent>`: take an Agent's behavior settings
|
|
415
|
+
* away on its draft (Forge improvement plan X4; P5 A15). An Agent whose live
|
|
416
|
+
* version has behavior settings serves customers, and the built-in Chat
|
|
417
|
+
* neither offers nor runs it; before, putting one back in Chat took a raw API
|
|
418
|
+
* call with a session token. Publishing the draft makes it take effect, which
|
|
419
|
+
* stays the person's step.
|
|
420
|
+
*/
|
|
421
|
+
const behavior: Command = {
|
|
422
|
+
meta: {
|
|
423
|
+
noun: 'agent',
|
|
424
|
+
verb: 'behavior',
|
|
425
|
+
args: [
|
|
426
|
+
{ name: 'action', required: true, description: 'clear: take every behavior setting away on the draft' },
|
|
427
|
+
{ name: 'agent', required: true, description: 'agent id or slug' },
|
|
428
|
+
],
|
|
429
|
+
flags: {},
|
|
430
|
+
summary: 'Take an Agent\'s behavior settings away on its draft, so once published it serves the built-in Chat again',
|
|
431
|
+
details: [
|
|
432
|
+
'An Agent whose live version has behavior settings (procedures, guardrails, variables, session defaults or Analysis) serves customers: the built-in Chat neither offers nor runs it. `clear` stages none on the draft; `frontera agent publish <agent>` makes it take effect. In a project, deleting `agents/<slug>/operating.json` and proposing the Agent does the same.',
|
|
433
|
+
],
|
|
434
|
+
examples: ['frontera agent behavior clear support-desk', 'frontera agent publish support-desk'],
|
|
435
|
+
},
|
|
436
|
+
async run(ctx) {
|
|
437
|
+
const action = ctx.positional[0]
|
|
438
|
+
if (action !== 'clear') throw new UsageError(action ? `\`agent behavior ${action}\` is not a command` : 'say what to do: clear', 'frontera agent behavior clear <agent>')
|
|
439
|
+
const ref = ctx.positional[1]
|
|
440
|
+
if (!ref) throw new UsageError('missing <agent>', 'frontera agent behavior clear <id-or-slug> — see `frontera agent list`')
|
|
441
|
+
const client = api(ctx)
|
|
442
|
+
const id = await resolveAgentRef(client, ref)
|
|
443
|
+
const view = await client.agentOperatingConfig(id).catch(() => null)
|
|
444
|
+
const had = view ? (view.draft ?? view.live) != null : null
|
|
445
|
+
if (had === false) return { data: { agent: ref, cleared: false, reason: 'no behavior settings' }, text: `${ref} has no behavior settings: it already serves the built-in Chat once published.` }
|
|
446
|
+
await client.stageAgentOperatingConfig(id, null)
|
|
447
|
+
return {
|
|
448
|
+
data: { agent: ref, cleared: true, staged: true },
|
|
449
|
+
text: [`Cleared ${ref}'s behavior settings on its draft.`, ` Publish it to take effect: \`frontera agent publish ${ref}\`. Once published, the built-in Chat offers it again.`].join('\n'),
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
|
|
413
454
|
const archive: Command = {
|
|
414
455
|
meta: {
|
|
415
456
|
noun: 'agent',
|
|
@@ -615,6 +656,7 @@ export const agentCommands: Command[] = [
|
|
|
615
656
|
publish,
|
|
616
657
|
diff,
|
|
617
658
|
discard,
|
|
659
|
+
behavior,
|
|
618
660
|
archive,
|
|
619
661
|
restore,
|
|
620
662
|
versions,
|
|
@@ -284,7 +284,7 @@ export const appDeploy: Command = {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
const client = new AppsApi(ctx.apiUrl, ctx.token)
|
|
287
|
-
const app = await client.ensureApp(project.slug, project.displayName, project.appId)
|
|
287
|
+
const app = await client.ensureApp(project.slug, project.displayName, project.appId, { icon: project.icon })
|
|
288
288
|
|
|
289
289
|
ctx.output.note(
|
|
290
290
|
` bundle ${distFiles.length} files, ${formatBytes(distFiles.reduce((n, f) => n + f.bytes.byteLength, 0))}`,
|
package/src/commands/app/dev.ts
CHANGED
|
@@ -109,7 +109,7 @@ export const appDev: Command = {
|
|
|
109
109
|
}
|
|
110
110
|
if (installed) ctx.output.note(`[dev] ${installed.summary}`)
|
|
111
111
|
const apps = new AppsApi(ctx.apiUrl, ctx.token)
|
|
112
|
-
const app = await apps.ensureApp(project.slug, project.displayName, project.appId)
|
|
112
|
+
const app = await apps.ensureApp(project.slug, project.displayName, project.appId, { icon: project.icon })
|
|
113
113
|
writeAppId(project.root, app.id)
|
|
114
114
|
// The solution this App is in, if any: where status reads how this server's sessions fare.
|
|
115
115
|
const solution = enclosingSolution(project.root)
|
|
@@ -132,7 +132,7 @@ export const appDev: Command = {
|
|
|
132
132
|
appId,
|
|
133
133
|
mint: (id) => apps.appToken(id),
|
|
134
134
|
// Gone from the runtime (`local snapshot restore`): registered again by its slug, as `app dev` started.
|
|
135
|
-
reRegister: async () => (await apps.ensureApp(project.slug, project.displayName, null)).id,
|
|
135
|
+
reRegister: async () => (await apps.ensureApp(project.slug, project.displayName, null, { icon: project.icon })).id,
|
|
136
136
|
onReRegistered: (id) => {
|
|
137
137
|
if (id === appId) return
|
|
138
138
|
ctx.output.note(`[dev] the runtime no longer had ${app.slug} (a restore of its database?): registered it again as ${id}; \`frontera app deploy\` puts its code back`)
|
package/src/commands/app/save.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const appSave: Command = {
|
|
|
21
21
|
|
|
22
22
|
// save and deploy are the only verbs that may bring an app into
|
|
23
23
|
// existence; every read verb resolves instead.
|
|
24
|
-
const app = await client.ensureApp(project.slug, project.displayName, project.appId)
|
|
24
|
+
const app = await client.ensureApp(project.slug, project.displayName, project.appId, { icon: project.icon })
|
|
25
25
|
const { tgz } = packDirectory(project.root, ctx.output, 'packaged')
|
|
26
26
|
const res = await client.saveDraft(app.id, tgz, project.parentVersion)
|
|
27
27
|
|
package/src/commands/auth/add.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { verifyKey } from '../../auth-verify'
|
|
|
2
2
|
import { ensureMigrated } from '../../config'
|
|
3
3
|
import { credentialStore } from '../../credential-store'
|
|
4
4
|
import { CliError, UsageError } from '../../errors'
|
|
5
|
+
import { keyReach, reachWords } from '../../key-reach'
|
|
5
6
|
import { canPrompt, promptConfirm, promptSecret } from '../../prompt'
|
|
6
7
|
import {
|
|
7
8
|
assertApiUrl,
|
|
@@ -129,8 +130,11 @@ export const authAdd: Command = {
|
|
|
129
130
|
replaced: Boolean(existing),
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
// What the key can reach: an organization key acts only in the workspaces on its list (X3).
|
|
134
|
+
const reach = verified.kind === 'organization' ? await keyReach(apiUrl, token) : null
|
|
135
|
+
if (reach) Object.assign(data, { workspaces: reach })
|
|
132
136
|
const scope = verified.kind === 'organization'
|
|
133
|
-
?
|
|
137
|
+
? `it authors this organization's Blueprint${reach ? `, and ${reachWords(reach)}` : ''}`
|
|
134
138
|
: `workspace ${verified.workspaceId}`
|
|
135
139
|
|
|
136
140
|
return {
|
|
@@ -2,6 +2,7 @@ import { credentialStore } from '../../credential-store'
|
|
|
2
2
|
import { describeSelection } from '../../config'
|
|
3
3
|
import { CliError } from '../../errors'
|
|
4
4
|
import { listProfiles } from '../../profiles'
|
|
5
|
+
import { findContext } from '../../project-context'
|
|
5
6
|
import { flagString, type Command } from '../types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -80,11 +81,16 @@ export const authCurrent: Command = {
|
|
|
80
81
|
? "organization Blueprint author"
|
|
81
82
|
: `workspace ${metadata.workspaceId ?? 'unknown'}`
|
|
82
83
|
|
|
84
|
+
// The binding's organization profile, for organization-scoped commands here (X3).
|
|
85
|
+
const found = selection.source === 'directory' ? findContext(ctx.cwd) : null
|
|
86
|
+
const org = found?.organizationProfile ?? null
|
|
87
|
+
if (org) Object.assign(data, { organizationProfile: org })
|
|
83
88
|
const lines = [
|
|
84
89
|
`${selection.profile}${where}`,
|
|
85
90
|
` ${metadata.apiUrl} — ${scope}`,
|
|
86
91
|
` key ${data.hasSecret ? metadata.fingerprint : 'MISSING — run `frontera auth add ' + selection.profile + '`'}`,
|
|
87
92
|
]
|
|
93
|
+
if (org) lines.push(` This project uses ${found!.profile} for workspace commands and ${org} for organization commands${found!.organizationTrusted ? '' : ' (not bound on this machine: run `frontera auth use` here again)'}`)
|
|
88
94
|
if (envToken) {
|
|
89
95
|
lines.push(' FRONTERA_TOKEN is set and takes precedence over this profile’s stored key')
|
|
90
96
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { authAdd } from './add'
|
|
2
2
|
import { authCurrent } from './current'
|
|
3
3
|
import { authList } from './list'
|
|
4
|
+
import { authLogin, authLogout } from './login'
|
|
4
5
|
import { authRemove } from './remove'
|
|
5
6
|
import { authUse } from './use'
|
|
6
7
|
import { authVerify } from './verify'
|
|
@@ -13,4 +14,6 @@ export const authCommands: readonly Command[] = [
|
|
|
13
14
|
authUse,
|
|
14
15
|
authVerify,
|
|
15
16
|
authRemove,
|
|
17
|
+
authLogin,
|
|
18
|
+
authLogout,
|
|
16
19
|
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describeSelection } from '../../config'
|
|
2
|
+
import { credentialStore } from '../../credential-store'
|
|
3
|
+
import { awaitDeviceSession, forgetDeviceSession, readDeviceSession, requestDeviceCode, saveDeviceSession, sessionEmail } from '../../device-login'
|
|
4
|
+
import { CliError, UsageError } from '../../errors'
|
|
5
|
+
import { flagBool, flagString, type Command, type CommandContext } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `frontera auth login --browser` and `frontera auth logout` (Forge
|
|
9
|
+
* improvement plan N5): a person's session for the commands that act as a
|
|
10
|
+
* person, approved in the browser. The profile's key stays what every other
|
|
11
|
+
* command uses; the session is kept beside it, used only by those commands,
|
|
12
|
+
* and lasts 12 hours. No token is ever printed, pasted or copied out of a
|
|
13
|
+
* browser.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function openBrowser(url: string): void {
|
|
17
|
+
const command = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', url] : ['xdg-open', url]
|
|
18
|
+
try {
|
|
19
|
+
Bun.spawn(command, { stdout: 'ignore', stderr: 'ignore' }).unref()
|
|
20
|
+
} catch {
|
|
21
|
+
// Opening is convenience only; the address is always printed.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function selected(ctx: CommandContext) {
|
|
26
|
+
const { selection, metadata } = await describeSelection({ profile: flagString(ctx, 'profile') }, { cwd: ctx.cwd })
|
|
27
|
+
if (!selection.profile || !metadata) {
|
|
28
|
+
throw new CliError('no Frontera profile is selected for this directory', { code: 'PROFILE_NOT_SELECTED', hint: 'pick one with `frontera auth use <profile>`, or name it with --profile' })
|
|
29
|
+
}
|
|
30
|
+
return { profile: selection.profile, apiUrl: metadata.apiUrl }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const authLogin: Command = {
|
|
34
|
+
meta: {
|
|
35
|
+
noun: 'auth',
|
|
36
|
+
verb: 'login',
|
|
37
|
+
args: [],
|
|
38
|
+
flags: { browser: 'boolean', 'no-open': 'boolean' },
|
|
39
|
+
flagHelp: { 'no-open': 'print the approval address without opening a browser' },
|
|
40
|
+
summary: 'Sign in as yourself in the browser, for the commands that act as a person',
|
|
41
|
+
details: [
|
|
42
|
+
'Prints a code and opens the platform’s approval page; approve the code there while signed in. The CLI then keeps a session for the selected profile, beside its key, and uses it only for commands that act as a person (they refuse API keys). The session lasts 12 hours; `frontera auth logout` ends it sooner.',
|
|
43
|
+
'Approve only a code you started yourself, in your own terminal, just now.',
|
|
44
|
+
],
|
|
45
|
+
examples: ['frontera auth login --browser', 'frontera auth login --browser --profile acme --no-open'],
|
|
46
|
+
offline: true,
|
|
47
|
+
},
|
|
48
|
+
async run(ctx) {
|
|
49
|
+
if (!flagBool(ctx, 'browser')) throw new UsageError('say how to sign in', 'frontera auth login --browser')
|
|
50
|
+
const { profile, apiUrl } = await selected(ctx)
|
|
51
|
+
const store = credentialStore()
|
|
52
|
+
const existing = await readDeviceSession(store, profile, apiUrl)
|
|
53
|
+
if (existing) {
|
|
54
|
+
return { data: { profile, signedIn: true, email: existing.email ?? null, expiresAt: existing.expiresAt, reused: true }, text: `Already signed in for ${profile}${existing.email ? ` as ${existing.email}` : ''}, until ${existing.expiresAt}. \`frontera auth logout\` ends it.` }
|
|
55
|
+
}
|
|
56
|
+
const code = await requestDeviceCode(apiUrl)
|
|
57
|
+
ctx.output.note(`To sign in, open ${code.verificationUri} and enter the code ${code.userCode}`)
|
|
58
|
+
if (!flagBool(ctx, 'no-open')) openBrowser(code.verificationUriComplete)
|
|
59
|
+
const session = await ctx.output.task('Waiting for you to approve it in the browser', () => awaitDeviceSession(apiUrl, code))
|
|
60
|
+
const email = await sessionEmail(apiUrl, session.token)
|
|
61
|
+
await saveDeviceSession(store, profile, { ...session, ...(email ? { email } : {}) })
|
|
62
|
+
return {
|
|
63
|
+
data: { profile, signedIn: true, email, expiresAt: session.expiresAt, reused: false },
|
|
64
|
+
text: `Signed in for ${profile}${email ? ` as ${email}` : ''} until ${session.expiresAt}. Commands that act as a person use this session; every other command keeps using the profile's key.`,
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const authLogout: Command = {
|
|
70
|
+
meta: {
|
|
71
|
+
noun: 'auth',
|
|
72
|
+
verb: 'logout',
|
|
73
|
+
args: [],
|
|
74
|
+
flags: {},
|
|
75
|
+
summary: 'End the browser sign-in for the selected profile; its key stays',
|
|
76
|
+
examples: ['frontera auth logout', 'frontera auth logout --profile acme'],
|
|
77
|
+
offline: true,
|
|
78
|
+
},
|
|
79
|
+
async run(ctx) {
|
|
80
|
+
const { profile } = await selected(ctx)
|
|
81
|
+
const forgotten = await forgetDeviceSession(credentialStore(), profile)
|
|
82
|
+
return { data: { profile, signedOut: forgotten }, text: forgotten ? `Signed out of ${profile}'s browser session; its key stays.` : `No browser session for ${profile}.` }
|
|
83
|
+
},
|
|
84
|
+
}
|
package/src/commands/auth/use.ts
CHANGED
|
@@ -25,10 +25,15 @@ export const authUse: Command = {
|
|
|
25
25
|
noun: 'auth',
|
|
26
26
|
verb: 'use',
|
|
27
27
|
args: [{ name: 'profile', required: true, description: 'profile to select for this directory tree' }],
|
|
28
|
-
flags: { dir: 'string' },
|
|
28
|
+
flags: { dir: 'string', 'org-profile': 'string' },
|
|
29
|
+
flagHelp: { 'org-profile': 'the organization profile organization-scoped commands use here (Blueprint authoring, governed Actions), beside a workspace profile on the same origin' },
|
|
29
30
|
summary: 'Select a profile for this directory and everything under it',
|
|
31
|
+
details: [
|
|
32
|
+
'`--org-profile <name>` names the organization profile for organization-scoped commands beside a workspace profile: in this directory, workspace commands use the workspace key and organization commands the organization key, with no `--profile`. It is named, never guessed from the origin: two customers can share one host.',
|
|
33
|
+
],
|
|
30
34
|
examples: [
|
|
31
35
|
'frontera auth use acme-prod',
|
|
36
|
+
'frontera auth use acme-prod --org-profile acme-org',
|
|
32
37
|
'frontera auth use globex-dev --dir ~/Customers/globex',
|
|
33
38
|
],
|
|
34
39
|
offline: true,
|
|
@@ -53,8 +58,20 @@ export const authUse: Command = {
|
|
|
53
58
|
})
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
// An organization profile for organization-scoped commands here (X3): an organization key on the same origin.
|
|
62
|
+
const orgProfile = flagString(ctx, 'org-profile')
|
|
63
|
+
let orgMetadata: ReturnType<typeof requireProfile> | null = null
|
|
64
|
+
if (orgProfile) {
|
|
65
|
+
assertProfileName(orgProfile)
|
|
66
|
+
orgMetadata = requireProfile(orgProfile)
|
|
67
|
+
if (metadata.credentialKind !== 'workspace') throw new UsageError(`${name} is an organization profile; --org-profile goes beside a workspace profile`, `frontera auth use <workspace profile> --org-profile ${orgProfile}`)
|
|
68
|
+
if (orgMetadata.credentialKind !== 'organization') throw new UsageError(`${orgProfile} holds a workspace key, not an organization key`, 'add the organization key as its own profile: `frontera auth add <name> --api-url <origin>` with an sk-org- key')
|
|
69
|
+
if (orgMetadata.apiUrl !== metadata.apiUrl) throw new UsageError(`${orgProfile} is for ${orgMetadata.apiUrl}, and ${name} for ${metadata.apiUrl}`, 'both profiles must reach the same deployment')
|
|
70
|
+
if ((await store.get(orgProfile)) === null) throw new CliError(`profile "${orgProfile}" has no stored key`, { code: 'PROFILE_SECRET_MISSING', hint: `re-add it with \`frontera auth add ${orgProfile} --api-url ${orgMetadata.apiUrl}\`` })
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
const { root, source } = resolveBindingRoot(ctx.cwd, flagString(ctx, 'dir'))
|
|
57
|
-
const path = writeContext(root, name)
|
|
74
|
+
const path = writeContext(root, name, process.env, orgProfile)
|
|
58
75
|
const gitignore = ensureGitignored(root)
|
|
59
76
|
|
|
60
77
|
const scope = metadata.credentialKind === 'organization'
|
|
@@ -73,10 +90,12 @@ export const authUse: Command = {
|
|
|
73
90
|
workspaceId: metadata.workspaceId,
|
|
74
91
|
orgId: metadata.orgId,
|
|
75
92
|
fingerprint: metadata.fingerprint,
|
|
93
|
+
...(orgProfile ? { organizationProfile: orgProfile } : {}),
|
|
76
94
|
},
|
|
77
95
|
text: [
|
|
78
96
|
`Selected ${name} for ${root}`,
|
|
79
97
|
` ${metadata.apiUrl} — ${scope}`,
|
|
98
|
+
...(orgProfile ? [` This project uses ${name} for workspace commands and ${orgProfile} for organization commands`] : []),
|
|
80
99
|
` wrote ${relative(root, path) || path}${gitignore === 'present' ? '' : `, ${gitignore} .gitignore entry`}`,
|
|
81
100
|
].join('\n'),
|
|
82
101
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `frontera function bind|unbind|bindings` — the operator's half of making an
|
|
3
|
-
* automation callable from a conversation
|
|
3
|
+
* automation callable from a conversation, or startable from a Custom App
|
|
4
|
+
* (`--app`, X6).
|
|
4
5
|
*
|
|
5
6
|
* The author's half is in the manifest (`trigger: { agent: true }`, plus a
|
|
6
7
|
* description of the automation and of every input) and is checked at deploy.
|
|
@@ -31,22 +32,37 @@ function requireAgent(ctx: CommandContext): string {
|
|
|
31
32
|
const agent = flagString(ctx, 'agent')
|
|
32
33
|
if (!agent) {
|
|
33
34
|
throw new UsageError(
|
|
34
|
-
'missing --agent <agent-slug>',
|
|
35
|
-
'a binding names exactly one agent — there is no "all
|
|
35
|
+
'missing --agent <agent-slug> or --app <app-slug>',
|
|
36
|
+
'a binding names exactly one agent or one Custom App — there is no "all" form on purpose',
|
|
36
37
|
)
|
|
37
38
|
}
|
|
38
39
|
return agent
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* `--app <slug>`: the App lane (X6). A Custom App's code holds no secrets, so
|
|
44
|
+
* it starts a Function bound to it, as the person using it. One of `--agent`
|
|
45
|
+
* and `--app`, never both: they are different permissions.
|
|
46
|
+
*/
|
|
47
|
+
function appOf(ctx: CommandContext): string | null {
|
|
48
|
+
const app = flagString(ctx, 'app')
|
|
49
|
+
if (app === undefined) return null
|
|
50
|
+
if (flagString(ctx, 'agent') !== undefined) throw new UsageError('pass --agent or --app, not both', 'an Agent binding and an App binding are separate permissions: make each with its own command')
|
|
51
|
+
if (!/^[a-z0-9-]+$/.test(app)) throw new UsageError(`--app takes a Custom App's slug, got "${app}"`, '`frontera app list` shows each App’s slug')
|
|
52
|
+
return app
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
const bind: Command = {
|
|
42
56
|
meta: {
|
|
43
57
|
noun: 'function',
|
|
44
58
|
verb: 'bind',
|
|
45
59
|
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
46
|
-
flags: { agent: 'string', approval: 'string', wait: 'string' },
|
|
47
|
-
|
|
60
|
+
flags: { agent: 'string', app: 'string', approval: 'string', wait: 'string', off: 'boolean' },
|
|
61
|
+
flagHelp: { app: 'a Custom App by its slug: the App may start the Function as the person using it', off: 'with --app: keep the binding, switched off' },
|
|
62
|
+
summary: 'Let one agent run this automation from a conversation, or one Custom App start it',
|
|
48
63
|
examples: [
|
|
49
64
|
'frontera function bind reconcile-invoices --agent finance-analyst',
|
|
65
|
+
'frontera function bind start-return --app returns-desk',
|
|
50
66
|
'frontera function bind reconcile-invoices --agent analyst --approval auto',
|
|
51
67
|
'frontera function bind reconcile-invoices --agent analyst --wait 120',
|
|
52
68
|
],
|
|
@@ -54,6 +70,15 @@ const bind: Command = {
|
|
|
54
70
|
|
|
55
71
|
async run(ctx) {
|
|
56
72
|
const slug = requireSlug(ctx)
|
|
73
|
+
const app = appOf(ctx)
|
|
74
|
+
if (app) {
|
|
75
|
+
if (flagString(ctx, 'approval') !== undefined || flagString(ctx, 'wait') !== undefined) throw new UsageError('--approval and --wait are an Agent binding’s', 'an App starts a Function when a person clicks, so there is no approval card or wait')
|
|
76
|
+
const result = await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).bindApp(slug, app, { enabled: ctx.flags.off !== true })
|
|
77
|
+
return {
|
|
78
|
+
data: result,
|
|
79
|
+
text: `${result.appDisplayName} (${result.appSlug}) may start ${slug}${result.enabled ? '' : ' — switched off'}.${result.inertReason === 'no_live_version' ? ` ${slug} has no live version yet, so the binding starts nothing until it is promoted.` : ''}\nIn a project, apps/${app}.json lists it after a pull; a change request carries that file.`,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
57
82
|
const agent = requireAgent(ctx)
|
|
58
83
|
|
|
59
84
|
// `required` is the default at every layer, and it is spelled out here
|
|
@@ -96,13 +121,19 @@ const unbind: Command = {
|
|
|
96
121
|
noun: 'function',
|
|
97
122
|
verb: 'unbind',
|
|
98
123
|
args: [{ name: 'slug', required: true, description: 'automation slug' }],
|
|
99
|
-
flags: { agent: 'string' },
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
flags: { agent: 'string', app: 'string' },
|
|
125
|
+
flagHelp: { app: 'a Custom App by its slug: it may no longer start the Function' },
|
|
126
|
+
summary: 'Stop one agent being able to run this automation, or one Custom App starting it',
|
|
127
|
+
examples: ['frontera function unbind reconcile-invoices --agent finance-analyst', 'frontera function unbind start-return --app returns-desk'],
|
|
102
128
|
},
|
|
103
129
|
|
|
104
130
|
async run(ctx) {
|
|
105
131
|
const slug = requireSlug(ctx)
|
|
132
|
+
const app = appOf(ctx)
|
|
133
|
+
if (app) {
|
|
134
|
+
await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).unbindApp(slug, app)
|
|
135
|
+
return { data: { unbound: true }, text: `${app} can no longer start ${slug}.` }
|
|
136
|
+
}
|
|
106
137
|
const agent = requireAgent(ctx)
|
|
107
138
|
await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).unbind(slug, agent)
|
|
108
139
|
return { data: { unbound: true }, text: `${agent} can no longer run ${slug}.` }
|
|
@@ -126,10 +157,12 @@ const bindings: Command = {
|
|
|
126
157
|
description: 'automation slug — omit it and pass --agent to ask from the agent side',
|
|
127
158
|
},
|
|
128
159
|
],
|
|
129
|
-
flags: { agent: 'string' },
|
|
130
|
-
|
|
160
|
+
flags: { agent: 'string', apps: 'boolean' },
|
|
161
|
+
flagHelp: { apps: 'the Custom Apps that may start it, instead of the agents' },
|
|
162
|
+
summary: 'List the agents (or Custom Apps) that may run an automation, or what one agent may run',
|
|
131
163
|
examples: [
|
|
132
164
|
'frontera function bindings reconcile-invoices',
|
|
165
|
+
'frontera function bindings start-return --apps',
|
|
133
166
|
'frontera function bindings --agent finance-analyst',
|
|
134
167
|
],
|
|
135
168
|
},
|
|
@@ -139,6 +172,17 @@ const bindings: Command = {
|
|
|
139
172
|
const agent = flagString(ctx, 'agent')
|
|
140
173
|
const slug = ctx.positional[0]
|
|
141
174
|
|
|
175
|
+
if (ctx.flags.apps === true) {
|
|
176
|
+
if (!slug) throw new UsageError('--apps lists one Function’s Apps: pass its slug', 'frontera function bindings <slug> --apps')
|
|
177
|
+
const rows = await client.appBindings(slug)
|
|
178
|
+
return {
|
|
179
|
+
data: rows,
|
|
180
|
+
text: rows.length === 0
|
|
181
|
+
? `No Custom App can start ${slug}.`
|
|
182
|
+
: table(['app', 'name', 'enabled', 'starts it now', 'bound by'], rows.map((b) => [b.appSlug, b.appDisplayName, String(b.enabled), b.projects === false ? `no (${b.inertReason === 'no_live_version' ? 'nothing live' : 'switched off'})` : 'yes', b.createdBy])),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
142
186
|
if (!slug && !agent) {
|
|
143
187
|
throw new UsageError(
|
|
144
188
|
'pass an automation slug, or --agent <agent-slug>',
|
|
@@ -6,8 +6,7 @@ import { validateManifest } from '@frontera-sdk/functions'
|
|
|
6
6
|
|
|
7
7
|
import { AutomationApi } from '../../api/automation-api'
|
|
8
8
|
import { UsageError } from '../../errors'
|
|
9
|
-
import {
|
|
10
|
-
import { packDirectory } from '../../pack'
|
|
9
|
+
import { packFunctionSource } from '../../pack'
|
|
11
10
|
import { automationInit } from './init'
|
|
12
11
|
import { automationPull } from './pull'
|
|
13
12
|
import { automationRun, automationRuns } from './run'
|
|
@@ -120,7 +119,7 @@ const deploy: Command = {
|
|
|
120
119
|
let source: Uint8Array | undefined
|
|
121
120
|
if (projectRoot) {
|
|
122
121
|
// With the files it imports from outside its folder, so a pull brings a Function that builds.
|
|
123
|
-
source =
|
|
122
|
+
source = packFunctionSource(projectRoot, resolve(file), ctx.output, 'source').tgz
|
|
124
123
|
} else {
|
|
125
124
|
// Degrade honestly rather than refuse. Authoring inside this monorepo is
|
|
126
125
|
// still supported and simply cannot retain source — there is no
|
|
@@ -61,6 +61,17 @@ async function datasetRevision(client: BlueprintAuthoringApi, name: string) {
|
|
|
61
61
|
return current
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The object type file's `backing` once bound: the dataset and the mapping,
|
|
66
|
+
* merged into what the file says, never in place of it, so `editable` and the
|
|
67
|
+
* rest stay (Forge improvement plan X11; a rebind lost the editable fields,
|
|
68
|
+
* P5 A4).
|
|
69
|
+
*/
|
|
70
|
+
export function boundBacking(before: unknown, dataset: string, mapping: Record<string, unknown>): Record<string, unknown> {
|
|
71
|
+
const kept = before && typeof before === 'object' && !Array.isArray(before) ? (before as Record<string, unknown>) : {}
|
|
72
|
+
return { ...kept, dataset, mapping }
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
export const blueprintBind: Command = {
|
|
65
76
|
meta: {
|
|
66
77
|
noun: 'blueprint',
|
|
@@ -213,7 +224,7 @@ export const blueprintBind: Command = {
|
|
|
213
224
|
}
|
|
214
225
|
writeTree(treeRoot, [{
|
|
215
226
|
...file,
|
|
216
|
-
document: { ...file.document, backing:
|
|
227
|
+
document: { ...file.document, backing: boundBacking(file.document.backing, datasetName, written) },
|
|
217
228
|
}])
|
|
218
229
|
treeUpdated = true
|
|
219
230
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { metricRuleProblems } from '../../blueprint/metric-rules'
|
|
25
25
|
import { actionRuleProblems, type ActionRuleProblems, type InheritedConflict } from '../../blueprint/action-rules'
|
|
26
26
|
import { pickCurrentRevision } from '../../blueprint/dataset-revision'
|
|
27
|
+
import { nameCheckWords, orgNameCheck } from '../../blueprint/org-name-check'
|
|
27
28
|
import {
|
|
28
29
|
assertPrunesLeaveNoDanglingReference,
|
|
29
30
|
diff,
|
|
@@ -1693,9 +1694,12 @@ export const blueprintNew: Command = {
|
|
|
1693
1694
|
document: scaffold(kind, apiName, options),
|
|
1694
1695
|
}
|
|
1695
1696
|
writeTree(directory, [file])
|
|
1697
|
+
// In a project linked to a workspace: is the name already the organization's, outside the slice (X11)?
|
|
1698
|
+
const check = await orgNameCheck(ctx.cwd, kind, apiName)
|
|
1699
|
+
const warning = nameCheckWords(kind, apiName, check)
|
|
1696
1700
|
return {
|
|
1697
|
-
data: { path: relative },
|
|
1698
|
-
text: `Wrote ${relative}. Edit it, then \`frontera blueprint plan\`.`,
|
|
1701
|
+
data: { path: relative, organizationName: check },
|
|
1702
|
+
text: [`Wrote ${relative}. Edit it, then \`frontera blueprint plan\`.`, ...(warning ? [warning] : [])].join('\n'),
|
|
1699
1703
|
}
|
|
1700
1704
|
},
|
|
1701
1705
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { UsageError } from '../../errors'
|
|
2
|
+
import { conversationTranscript, searchConversations, type TranscriptTurn } from '../../mcp/mcp-conversations'
|
|
3
|
+
import { flagBool, flagString, type Command } from '../types'
|
|
4
|
+
import { projectRuntime } from './local-runtime'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `frontera conversation get|search`: the project's local runtime's
|
|
8
|
+
* conversations, read whole (Forge improvement plan N7), the same reads as
|
|
9
|
+
* the MCP tools `conversation_get` and `conversation_search`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function turnLines(turns: TranscriptTurn[]): string[] {
|
|
13
|
+
return turns.flatMap((t) => [
|
|
14
|
+
`Turn ${t.turn}${t.procedure?.name ? ` · procedure ${t.procedure.name}` : ''}`,
|
|
15
|
+
...(t.user ? [` Person: ${t.user}`] : []),
|
|
16
|
+
...t.toolCalls.map((c) => ` Tool: ${c.name}${c.ok ? '' : c.waiting ? ' (waiting on a person)' : ' (no result)'}`),
|
|
17
|
+
` Agent: ${t.reply || '(no reply)'}`,
|
|
18
|
+
...(t.replyChecks?.length ? [` Reply check changed this reply (${t.replyChecks.length})`] : []),
|
|
19
|
+
'',
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const conversationGet: Command = {
|
|
24
|
+
meta: {
|
|
25
|
+
noun: 'conversation',
|
|
26
|
+
verb: 'get',
|
|
27
|
+
args: [{ name: 'conversation', required: true, description: 'The conversation id (a test case’s, or one `conversation search` found)' }],
|
|
28
|
+
flags: { 'no-traces': 'boolean' },
|
|
29
|
+
flagHelp: { 'no-traces': 'leave out what each turn’s trace says (its procedure and the reply check)' },
|
|
30
|
+
summary: 'Read one conversation on the project’s local runtime: every turn, tool call, procedure and reply check',
|
|
31
|
+
examples: ['frontera conversation get 6f1c…', 'frontera conversation get 6f1c… --json'],
|
|
32
|
+
offline: true,
|
|
33
|
+
},
|
|
34
|
+
async run(ctx) {
|
|
35
|
+
const id = ctx.positional[0]
|
|
36
|
+
if (!id) throw new UsageError('missing <conversation>', '`frontera conversation search` lists them')
|
|
37
|
+
const link = projectRuntime(ctx.cwd)
|
|
38
|
+
const transcript = await conversationTranscript(link, id, { traces: !flagBool(ctx, 'no-traces') })
|
|
39
|
+
if (!transcript) throw new UsageError(`no conversation ${id} on the local runtime`, '`frontera conversation search` lists them')
|
|
40
|
+
return { data: transcript, text: [`Conversation ${id}: ${transcript.turns.length} turn${transcript.turns.length === 1 ? '' : 's'}`, '', ...turnLines(transcript.turns)].join('\n').trimEnd() }
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const conversationSearch: Command = {
|
|
45
|
+
meta: {
|
|
46
|
+
noun: 'conversation',
|
|
47
|
+
verb: 'search',
|
|
48
|
+
args: [{ name: 'query', required: false, description: 'Words the conversation contains' }],
|
|
49
|
+
flags: { agent: 'string', tests: 'boolean', limit: 'string' },
|
|
50
|
+
flagHelp: { agent: 'only this Agent’s conversations (its slug)', tests: 'only conversations test runs started', limit: 'at most this many (default 20)' },
|
|
51
|
+
summary: 'Find conversations on the project’s local runtime, newest first',
|
|
52
|
+
examples: ['frontera conversation search "refund"', 'frontera conversation search --agent sdr --tests'],
|
|
53
|
+
offline: true,
|
|
54
|
+
},
|
|
55
|
+
async run(ctx) {
|
|
56
|
+
const link = projectRuntime(ctx.cwd)
|
|
57
|
+
const limit = Number(flagString(ctx, 'limit') ?? 20)
|
|
58
|
+
const hits = await searchConversations(link, { query: ctx.positional[0], agent: flagString(ctx, 'agent'), tests: flagBool(ctx, 'tests'), limit: Number.isFinite(limit) ? limit : 20 })
|
|
59
|
+
return {
|
|
60
|
+
data: { conversations: hits },
|
|
61
|
+
text: hits.length ? hits.map((h) => `${h.conversationId} ${h.updatedAt ?? ''} ${h.agent ?? ''} ${h.title ?? ''}`.trimEnd()).join('\n') : 'No conversations match.',
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const conversationCommands: readonly Command[] = [conversationGet, conversationSearch]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CliError } from '../../errors'
|
|
2
|
+
import { localProject } from '../../local/local-project'
|
|
3
|
+
import { runtimeLink, type RuntimeLink } from '../../mcp/mcp-runtime'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The local runtime `frontera conversation` and `frontera trace` read (Forge
|
|
7
|
+
* improvement plan N7): the one of the project this folder is in. Reading a
|
|
8
|
+
* workspace's conversations, which are customer data, from a coding agent is
|
|
9
|
+
* an open question the plan leaves to its owner, so these commands never
|
|
10
|
+
* reach a workspace.
|
|
11
|
+
*/
|
|
12
|
+
export function projectRuntime(cwd: string): RuntimeLink {
|
|
13
|
+
const link = runtimeLink(localProject(cwd))
|
|
14
|
+
if (typeof link === 'string') {
|
|
15
|
+
throw new CliError(link, { code: 'LOCAL_NOT_RUNNING', hint: 'run it in the project folder with its local runtime started (`frontera local start`); a workspace’s conversations are read in the product, not from here' })
|
|
16
|
+
}
|
|
17
|
+
return link
|
|
18
|
+
}
|