@frontera-sdk/cli 1.52.45 → 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/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/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 +30 -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-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-stand-ins.ts +44 -0
- 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 +11 -11
|
@@ -22,7 +22,8 @@ import { isAlive, readState } from '../../local/local-state'
|
|
|
22
22
|
import { recordRun } from '../../local/local-test-baseline'
|
|
23
23
|
import { notRunGroups, notRunWords } from '../../local/local-case-not-run'
|
|
24
24
|
import { evidenceLine, failingCaseEvidence, type CaseEvidence } from '../../local/local-case-evidence'
|
|
25
|
-
import { agentFilesProblem, caseConcurrency, findCaseFiles, runCases, selectCaseFiles, waitedWords } from '../../local/local-test-runner'
|
|
25
|
+
import { agentFilesProblem, caseConcurrency, caseRepeat, findCaseFiles, judgedWords, passRateThreshold, repeatWords, runCases, selectCaseFiles, waitedWords } from '../../local/local-test-runner'
|
|
26
|
+
import { agentScope, customerFacingAgents, customerFacingWords, modelSubstitutions, skippedCases, skippedWords, type AgentScope, type SkippedCases } from '../../local/local-test-scope'
|
|
26
27
|
import { findWorkflowTestFiles, recordWorkflowTestRun, runWorkflowTests, workflowFileState } from '../../local/local-workflow-tests'
|
|
27
28
|
import { vendoredSdkLine, vendoredSdkState } from '../../local/local-vendored-sdk'
|
|
28
29
|
import { withFix } from '../../local/fix-words'
|
|
@@ -145,6 +146,14 @@ export interface CheckStep {
|
|
|
145
146
|
* step does not pass, but these are not failures of the Agent.
|
|
146
147
|
*/
|
|
147
148
|
couldNotRun?: Array<{ what: string; fix: string; cases: string[]; notSent?: number }>
|
|
149
|
+
/** agents: each Agent with cases, and why it is in the check (X1). */
|
|
150
|
+
agents?: AgentScope[]
|
|
151
|
+
/** agents: the cases the check left out, by reason. */
|
|
152
|
+
skipped?: SkippedCases[]
|
|
153
|
+
/** agents: Agents that run on another model here than their files name. */
|
|
154
|
+
modelSubstitutions?: string[]
|
|
155
|
+
/** agents: with --repeat, the cases that passed some runs and failed others. */
|
|
156
|
+
flaky?: string[]
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
/** Run a command in the project, its output on stderr so stdout stays the check's own result. */
|
|
@@ -319,12 +328,23 @@ async function check(ctx: CommandContext) {
|
|
|
319
328
|
const strict = flagBool(ctx, 'strict')
|
|
320
329
|
// How many Agent cases run at once (`--concurrency`), checked before anything starts.
|
|
321
330
|
let concurrency: number | undefined
|
|
331
|
+
let repeat: number | undefined
|
|
332
|
+
let passRate: number | undefined
|
|
322
333
|
try {
|
|
323
334
|
concurrency = caseConcurrency(flagString(ctx, 'concurrency'))
|
|
335
|
+
repeat = caseRepeat(flagString(ctx, 'repeat'))
|
|
336
|
+
passRate = passRateThreshold(flagString(ctx, 'pass-rate'))
|
|
324
337
|
} catch (err) {
|
|
325
|
-
throw new UsageError((err as Error).message, 'frontera local check --
|
|
338
|
+
throw new UsageError((err as Error).message, 'frontera local check --repeat 3')
|
|
339
|
+
}
|
|
340
|
+
if (passRate !== undefined && !repeat) throw new UsageError('--pass-rate is the share of a repeated case’s runs that must pass: give --repeat too', 'frontera local check --repeat 5 --pass-rate 0.8')
|
|
341
|
+
// `--agent a,b`: only those Agents' cases (X1); every Agent's otherwise.
|
|
342
|
+
const namedAgents = (flagString(ctx, 'agent') ?? '').split(',').map((a) => a.trim()).filter(Boolean)
|
|
343
|
+
for (const a of namedAgents) {
|
|
344
|
+
if (!findCaseFiles(root, a).length) throw new UsageError(`no test cases for Agent ${a} (agents/${a}/evals/*.yaml)`, 'name an Agent that has cases, e.g. `frontera local check --agent <slug>`')
|
|
326
345
|
}
|
|
327
346
|
const unchanged = unchangedSincePull(root)
|
|
347
|
+
const customerFacing = customerFacingAgents(root)
|
|
328
348
|
// The tests' writes: a baseline after the apply, restored after the tests unless `--keep-state`.
|
|
329
349
|
const keepState = flagBool(ctx, 'keep-state')
|
|
330
350
|
let testData: CheckTestData | null = null
|
|
@@ -337,7 +357,7 @@ async function check(ctx: CommandContext) {
|
|
|
337
357
|
}
|
|
338
358
|
/** Whether the Agent or workflow tests will run anything, and so may write. */
|
|
339
359
|
const testsWillRun = () =>
|
|
340
|
-
(!skip.has('agents') && !blockedBy('agents', failedSteps, failFast) && findCaseFiles(root).some((f) => f.cases.length > 0))
|
|
360
|
+
(!skip.has('agents') && !blockedBy('agents', failedSteps, failFast) && findCaseFiles(root).some((f) => f.cases.length > 0 && (!namedAgents.length || namedAgents.includes(f.agent))))
|
|
341
361
|
|| (!skip.has('workflows') && !blockedBy('workflows', failedSteps, failFast) && findWorkflowTestFiles(root).some((f) => f.cases.length > 0))
|
|
342
362
|
|
|
343
363
|
const say = (step: CheckStepName, line: string) => ctx.output.note(`── ${step}: ${line}`)
|
|
@@ -387,6 +407,8 @@ async function check(ctx: CommandContext) {
|
|
|
387
407
|
}
|
|
388
408
|
if (step === 'apply') {
|
|
389
409
|
say(step, 'frontera local dev --once')
|
|
410
|
+
// Which Agents serve customers: an operating.json leaves an Agent out of the built-in Chat (X4).
|
|
411
|
+
if (customerFacing.length) ctx.output.note(` note: ${customerFacingWords(customerFacing)}`)
|
|
390
412
|
const started = Date.now()
|
|
391
413
|
const code = await runIn(root, self(['local', 'dev', '--once']))
|
|
392
414
|
// Which items did not apply, and whether each is still as the pull wrote it. `local dev --once` makes the
|
|
@@ -545,12 +567,20 @@ async function check(ctx: CommandContext) {
|
|
|
545
567
|
}
|
|
546
568
|
const serviceUrl = `http://127.0.0.1:${state.ports.service}`
|
|
547
569
|
if (step === 'agents') {
|
|
548
|
-
const files = selectCaseFiles(root)
|
|
570
|
+
const files = selectCaseFiles(root).filter((f) => !namedAgents.length || namedAgents.includes(f.agent))
|
|
549
571
|
if (!files.some((f) => f.cases.length || f.problem)) {
|
|
550
572
|
record({ step, status: 'skipped', note: 'no Agent test cases (agents/<slug>/evals/*.yaml)', ms: 0 })
|
|
551
573
|
continue
|
|
552
574
|
}
|
|
553
|
-
say(step,
|
|
575
|
+
say(step, `frontera local test (${namedAgents.length ? `Agent${namedAgents.length === 1 ? '' : 's'} ${namedAgents.join(', ')}` : 'Agents'})${repeat && repeat > 1 ? `, each case ${repeat} times` : ''}`)
|
|
576
|
+
// Why each Agent is in the check; one unchanged since the pull, or only renamed, has its failures inherited (X1).
|
|
577
|
+
const scope = agentScope(root, files, unchanged, namedAgents)
|
|
578
|
+
for (const a of scope) ctx.output.note(` ${a.agent} (${a.cases} case${a.cases === 1 ? '' : 's'}): ${a.why}`)
|
|
579
|
+
const inheritedAgents = new Set(scope.filter((a) => a.inherited).map((a) => a.agent))
|
|
580
|
+
const skippedHere = skippedCases(root, namedAgents.length ? { agents: namedAgents } : {})
|
|
581
|
+
if (skippedHere.length) ctx.output.note(` ${skippedWords(skippedHere)}`)
|
|
582
|
+
const models = modelSubstitutions(project, state.credentials.organizationId, files.map((f) => f.agent))
|
|
583
|
+
for (const line of models) ctx.output.note(` note: ${line}`)
|
|
554
584
|
const started = Date.now()
|
|
555
585
|
const orgId = state.credentials.organizationId
|
|
556
586
|
const results = await runCases({
|
|
@@ -559,15 +589,21 @@ async function check(ctx: CommandContext) {
|
|
|
559
589
|
files,
|
|
560
590
|
live: false,
|
|
561
591
|
concurrency,
|
|
592
|
+
...(repeat ? { repeat } : {}),
|
|
593
|
+
...(passRate ? { passRate } : {}),
|
|
562
594
|
staleDraft: (slug) => agentFilesProblem(project, orgId, slug),
|
|
563
595
|
onResult: (r) => {
|
|
564
596
|
// The cases not sent after the provider refused every one are said once, below.
|
|
565
597
|
if (r.notRun?.skipped) return
|
|
566
|
-
ctx.output.note(` ${r.passed ? '✓' : r.notRun ? '–' : '✕'} ${r.agent} · ${r.name}${r.passed ? '' : r.notRun ? ` — couldn’t run: ${r.notRun.what}` : ` — ${r.reason}`}${r.passed && r.retried ? ' (run twice: the provider refused the first try for its limits)' : ''}${r.waited?.length ? ` (${waitedWords(r.waited)})` : ''}`)
|
|
598
|
+
ctx.output.note(` ${r.passed ? (r.repeat?.flaky ? '~' : '✓') : r.notRun ? '–' : '✕'} ${r.agent} · ${r.name}${r.passed ? '' : r.notRun ? ` — couldn’t run: ${r.notRun.what}` : ` — ${r.reason}`}${repeatWords(r)}${judgedWords(r)}${r.passed && r.retried ? ' (run twice: the provider refused the first try for its limits)' : ''}${r.waited?.length ? ` (${waitedWords(r.waited)})` : ''}`)
|
|
567
599
|
},
|
|
568
600
|
})
|
|
569
601
|
recordRun(project, results)
|
|
570
|
-
const
|
|
602
|
+
const failing = results.filter((r) => !r.passed && !r.notRun)
|
|
603
|
+
// An inherited Agent's failures are listed and pass unless --strict, as every other step's are.
|
|
604
|
+
const inheritedFails = strict ? [] : failing.filter((r) => inheritedAgents.has(r.agent))
|
|
605
|
+
const bad = failing.filter((r) => !inheritedFails.includes(r))
|
|
606
|
+
const agentData = { agents: scope, ...(skippedHere.length ? { skipped: skippedHere } : {}), ...(models.length ? { modelSubstitutions: models } : {}), ...(results.some((r) => r.repeat?.flaky) ? { flaky: results.filter((r) => r.repeat?.flaky).map((r) => `${r.agent} · ${r.name}`) } : {}) }
|
|
571
607
|
// Each failing case's conversation, last turn's trace and reply, read before the rollback (which keeps the conversation).
|
|
572
608
|
const evidence = await failingCaseEvidence(project, results)
|
|
573
609
|
for (const [i, e] of evidence) ctx.output.note(` ${results[i]!.agent} · ${results[i]!.name}: ${evidenceLine(e)}`)
|
|
@@ -581,8 +617,10 @@ async function check(ctx: CommandContext) {
|
|
|
581
617
|
record({
|
|
582
618
|
step,
|
|
583
619
|
status: bad.length || notRun.length ? 'failed' : 'passed',
|
|
584
|
-
command:
|
|
620
|
+
command: `frontera local test${namedAgents.length === 1 ? ` --agent ${namedAgents[0]}` : ''}${repeat && repeat > 1 ? ` --repeat ${repeat}` : ''}`,
|
|
585
621
|
ms: Date.now() - started,
|
|
622
|
+
...agentData,
|
|
623
|
+
...(inheritedFails.length ? { inherited: inheritedFails.map((r) => `${r.agent} · ${r.name}: ${r.reason ?? 'failed'}`) } : {}),
|
|
586
624
|
...(note ? { note } : {}),
|
|
587
625
|
...(bad.length ? { failures: bad.map((r) => ({ name: `${r.agent} · ${r.name}`, reason: r.reason, ...(evidence.get(results.indexOf(r)) ?? {}) })) } : {}),
|
|
588
626
|
...(notRun.length ? { couldNotRun: notRun.map((g) => ({ what: `couldn’t run: ${g.what}`, fix: g.fix, cases: g.cases, ...(g.skipped ? { notSent: g.skipped } : {}) })) } : {}),
|
|
@@ -629,7 +667,7 @@ async function check(ctx: CommandContext) {
|
|
|
629
667
|
}
|
|
630
668
|
}
|
|
631
669
|
} else if (testData?.kept && testsRan) ctx.output.note('── test data: the tests’ writes were kept (--keep-state); the next `frontera local check` rolls them back')
|
|
632
|
-
const testDataField = testData && testsRan ? { testData } : {}
|
|
670
|
+
const testDataField = { ...(testData && testsRan ? { testData } : {}), ...(customerFacing.length ? { customerFacing } : {}) }
|
|
633
671
|
|
|
634
672
|
if (flagBool(ctx, 'stop')) {
|
|
635
673
|
ctx.output.note('── frontera local stop')
|
|
@@ -676,9 +714,17 @@ export const localCheck: Command = {
|
|
|
676
714
|
noun: 'local',
|
|
677
715
|
verb: 'check',
|
|
678
716
|
args: [],
|
|
679
|
-
flags: { stop: 'boolean', skip: 'string', 'fail-fast': 'boolean', strict: 'boolean', 'keep-state': 'boolean', concurrency: 'string' },
|
|
717
|
+
flags: { stop: 'boolean', skip: 'string', 'fail-fast': 'boolean', strict: 'boolean', 'keep-state': 'boolean', concurrency: 'string', agent: 'string', repeat: 'string', 'pass-rate': 'string' },
|
|
718
|
+
flagHelp: {
|
|
719
|
+
agent: 'check only these Agents’ test cases (a slug, or slugs separated by commas); the other steps run as always',
|
|
720
|
+
repeat: 'run each Agent case N times (1 to 10); a case that passes some runs is flaky, and passes at --pass-rate',
|
|
721
|
+
'pass-rate': 'with --repeat, the share of runs a flaky case must pass (default 0.8, or 80%)',
|
|
722
|
+
},
|
|
723
|
+
details: [
|
|
724
|
+
'The agents step says why each Agent is in it: named with `--agent`, every Agent with cases, or, in a pulled project, whether it changed since the pull. An Agent unchanged since the pull, or changed only in its name, has its failures listed as inherited: they pass unless `--strict`, as every other step\'s inherited failures do.',
|
|
725
|
+
],
|
|
680
726
|
summary: 'The whole gate with one exit code (start, apply, typecheck, unit tests, App checks, Agent and workflow tests): anything changed or added fails it, and failures only in items unchanged since the pull are listed as inherited and pass unless --strict',
|
|
681
|
-
examples: ['frontera local check', 'frontera local check --json', 'frontera local check --strict', 'frontera local check --skip typecheck,apps', 'frontera local check --fail-fast', 'frontera local check --keep-state', 'frontera local check --concurrency 1', 'frontera local check --stop'],
|
|
727
|
+
examples: ['frontera local check', 'frontera local check --json', 'frontera local check --agent claims-triage --repeat 3', 'frontera local check --strict', 'frontera local check --skip typecheck,apps', 'frontera local check --fail-fast', 'frontera local check --keep-state', 'frontera local check --concurrency 1', 'frontera local check --stop'],
|
|
682
728
|
offline: true,
|
|
683
729
|
},
|
|
684
730
|
run: check,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { GovernedActionApi } from '../../api/governed-action-api'
|
|
5
|
+
import { CliError, UsageError } from '../../errors'
|
|
6
|
+
import { loadViaActions, loadWords, readLoadFile } from '../../local/local-data-load'
|
|
7
|
+
import { restoreBaseline, saveBaseline } from '../../local/local-db-snapshot'
|
|
8
|
+
import { actAsLocalPersona, redact } from '../../local/local-personas'
|
|
5
9
|
import { localProject } from '../../local/local-project'
|
|
10
|
+
import { waitForDatabaseReady } from '../../local/local-runtime-health'
|
|
11
|
+
import { readState } from '../../local/local-state'
|
|
6
12
|
import { isoDay, SAMPLE_OPTIONS_FILE, writeSampleOptions } from '../../local/local-sample-remake'
|
|
7
13
|
import { addMissingBackings, generateSyntheticData, loadScenario, parseRegion, projectRegion, REGION_CODES, SCENARIO_CONTRACT, SCENARIO_TEMPLATE } from '../../local/local-synthetic-data'
|
|
8
14
|
import { flagBool, flagString, type Command } from '../types'
|
|
@@ -17,14 +23,27 @@ export const localData: Command = {
|
|
|
17
23
|
meta: {
|
|
18
24
|
noun: 'local',
|
|
19
25
|
verb: 'data',
|
|
20
|
-
args: [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'action', required: false, description: 'load, to load records through governed Actions' },
|
|
28
|
+
{ name: 'file', required: false, description: 'with load: the JSON or YAML file of { action, records } groups' },
|
|
29
|
+
],
|
|
30
|
+
flags: { synthetic: 'boolean', rows: 'string', seed: 'string', region: 'string', today: 'string', 'via-actions': 'boolean', 'no-snapshot': 'boolean', batch: 'string' },
|
|
31
|
+
flagHelp: {
|
|
32
|
+
'via-actions': 'with load: submit each record as the governed Action its group names, as the local submitter persona',
|
|
33
|
+
'no-snapshot': 'with load: don’t save the records first (by default a load saves them, and a failed load puts them back)',
|
|
34
|
+
batch: 'with load: how many records are submitted at once (1 to 16, default 4)',
|
|
35
|
+
},
|
|
36
|
+
summary: "Generate sample data for the project's Blueprint (datasets/*.csv; media-backed types get none), with consistent links, or load records through governed Actions",
|
|
37
|
+
details: [
|
|
38
|
+
...SCENARIO_CONTRACT,
|
|
39
|
+
'`load <file> --via-actions` loads records through the project\'s governed Actions on the local runtime: the file is a list of groups, each `{ action: <apiName>, records: [...] }`, run in its order (put a group whose records name another\'s after it). A record is the Action\'s input, or `{ input, subject }` for an Action on an existing record. Each is submitted as the local `submitter` persona, in batches, with a key from the file and the record, so loading the same file again submits nothing twice. The records are saved first; when a record is refused, the load stops after its group and puts them back (`--no-snapshot` skips both). A record that waits on an approval step is said.',
|
|
40
|
+
],
|
|
41
|
+
examples: ['frontera local data load data/customers.yaml --via-actions', 'frontera local data --synthetic', 'frontera local data --synthetic --rows 500 --seed 42', 'frontera local data --synthetic --region ID', 'frontera local data --synthetic --region 1.29,103.85,20', 'frontera local data --synthetic --today 2026-10-18'],
|
|
25
42
|
offline: true,
|
|
26
43
|
},
|
|
27
44
|
async run(ctx) {
|
|
45
|
+
if (ctx.positional[0] === 'load') return loadCommand(ctx)
|
|
46
|
+
if (ctx.positional.length) throw new UsageError(`unknown data action \`${ctx.positional[0]}\``, '`frontera local data --synthetic`, or `frontera local data load <file> --via-actions`')
|
|
28
47
|
if (!flagBool(ctx, 'synthetic')) throw new UsageError('say what to make', 'run `frontera local data --synthetic`')
|
|
29
48
|
const rows = Number(flagString(ctx, 'rows') ?? 200)
|
|
30
49
|
const seed = Number(flagString(ctx, 'seed') ?? 7)
|
|
@@ -79,3 +98,52 @@ export const localData: Command = {
|
|
|
79
98
|
}
|
|
80
99
|
},
|
|
81
100
|
}
|
|
101
|
+
|
|
102
|
+
/** `local data load <file> --via-actions` (Forge improvement plan L2). */
|
|
103
|
+
async function loadCommand(ctx: Parameters<Command['run']>[0]) {
|
|
104
|
+
const file = ctx.positional[1]
|
|
105
|
+
if (!file) throw new UsageError('name the file to load', 'frontera local data load data/customers.yaml --via-actions')
|
|
106
|
+
if (!flagBool(ctx, 'via-actions')) throw new UsageError('a load goes through governed Actions: say so with --via-actions', '`frontera local data load <file> --via-actions`; sample rows for datasets come from `frontera local data --synthetic`')
|
|
107
|
+
const batch = flagString(ctx, 'batch') === undefined ? 4 : Number(flagString(ctx, 'batch'))
|
|
108
|
+
if (!Number.isInteger(batch) || batch < 1 || batch > 16) throw new UsageError('--batch takes a whole number from 1 to 16', 'e.g. `--batch 4`')
|
|
109
|
+
const project = localProject(ctx.cwd)
|
|
110
|
+
const state = readState(project)
|
|
111
|
+
if (!state || state.phase !== 'ready' || !state.credentials) throw new CliError('the local runtime is not running', { code: 'LOCAL_NOT_RUNNING', hint: 'run `frontera local start` first' })
|
|
112
|
+
const serviceUrl = `http://127.0.0.1:${state.ports.service}`
|
|
113
|
+
const groups = readLoadFile(resolve(ctx.cwd, file))
|
|
114
|
+
const organizationId = state.credentials.organizationId
|
|
115
|
+
const snapshot = !flagBool(ctx, 'no-snapshot')
|
|
116
|
+
if (snapshot) {
|
|
117
|
+
const saved = await ctx.output.task('Saving the records first', () => saveBaseline(project, organizationId, { serviceAnswers: () => waitForDatabaseReady(serviceUrl) }, true))
|
|
118
|
+
if (!saved.ok) throw new CliError(`the records could not be saved first, so nothing was loaded: ${saved.note}`, { code: 'LOCAL_SNAPSHOT_FAILED', hint: '`--no-snapshot` loads without saving them' })
|
|
119
|
+
}
|
|
120
|
+
const persona = await actAsLocalPersona({ meta: { noun: 'action', verb: 'submit' }, cwd: ctx.cwd, apiUrl: serviceUrl, wanted: 'submitter' })
|
|
121
|
+
const client = new GovernedActionApi(serviceUrl, persona.token, persona.workspaceId)
|
|
122
|
+
const outcomes = []
|
|
123
|
+
try {
|
|
124
|
+
for (const group of groups) {
|
|
125
|
+
const [o] = await loadViaActions(file, [group], (action, invocation, key) => client.submit(action, invocation, key) as never, {
|
|
126
|
+
batch,
|
|
127
|
+
subjectType: async (action) => ((await client.publishedAction(action).catch(() => null))?.definition as { subject?: { objectTypeId?: string } } | undefined)?.subject?.objectTypeId ?? null,
|
|
128
|
+
})
|
|
129
|
+
outcomes.push(o!)
|
|
130
|
+
ctx.output.note(` ${o!.refused.length ? '✕' : '✓'} ${loadWords(o!)}`)
|
|
131
|
+
for (const r of o!.refused.slice(0, 3)) ctx.output.note(` record ${r.index + 1}: ${redact(r.reason, persona.token)}`)
|
|
132
|
+
if (o!.refused.length) break
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
throw redact(err, persona.token)
|
|
136
|
+
}
|
|
137
|
+
const stopped = outcomes.some((o) => o.refused.length)
|
|
138
|
+
let putBack: string | null = null
|
|
139
|
+
if (stopped && snapshot) {
|
|
140
|
+
const restored = await ctx.output.task('Putting the records back', () => restoreBaseline(project, organizationId, { serviceAnswers: () => waitForDatabaseReady(serviceUrl) }, { secrets: 'keep', history: 'keep' }))
|
|
141
|
+
putBack = restored.ok ? 'the records are as they were before the load' : `the records could not be put back: ${restored.note}`
|
|
142
|
+
}
|
|
143
|
+
const data = { file, groups: outcomes, ...(stopped ? { stopped: true, putBack } : {}) }
|
|
144
|
+
if (stopped) {
|
|
145
|
+
const at = outcomes[outcomes.length - 1]!
|
|
146
|
+
throw new CliError(`${at.action}: ${at.refused.length} record${at.refused.length === 1 ? '' : 's'} refused, so the load stopped${putBack ? `; ${putBack}` : ''}`, { code: 'LOCAL_LOAD_REFUSED', hint: 'fix the records (or the Action) and load the file again: what was applied is not submitted twice', details: data })
|
|
147
|
+
}
|
|
148
|
+
return { data, text: `Loaded ${file} through governed Actions as the local submitter:\n${outcomes.map((o) => ` ${loadWords(o)}`).join('\n')}` }
|
|
149
|
+
}
|
|
@@ -36,7 +36,7 @@ import { ensureMediaSets, mediaUpload } from '../../local/local-media-sets'
|
|
|
36
36
|
import { fillPlaceholderMedia } from '../../local/local-media-placeholders'
|
|
37
37
|
import { applyChatApp, keyCalls } from '../../local/local-chat-apps'
|
|
38
38
|
import { applyLessons } from '../../local/local-lessons'
|
|
39
|
-
import { applyStandIn, platformOnlyNames, readStandIn, standInNames } from '../../local/local-stand-ins'
|
|
39
|
+
import { applyStandIn, missingGrantInstalls, platformOnlyNames, readStandIn, standInNames, standInPath, writeGrantStandIn } from '../../local/local-stand-ins'
|
|
40
40
|
import { projectProvides } from '../../local/local-adapt'
|
|
41
41
|
import { applyOperatingConfig } from '../../local/local-operating'
|
|
42
42
|
import { localEmbeddingsReachable } from '../../local/local-keys'
|
|
@@ -51,15 +51,18 @@ import { decideStepNotes, keepLiveCurrent, mirrorWorkflowState } from '../../loc
|
|
|
51
51
|
import type { Directory } from '../../local/local-sync-refs'
|
|
52
52
|
import { localProject, type LocalProject } from '../../local/local-project'
|
|
53
53
|
import { isAlive, readState, recordApplied, recordApplying, type LocalCredentials } from '../../local/local-state'
|
|
54
|
-
import { applyErrorText } from '../../local/local-apply-error'
|
|
54
|
+
import { applyErrorText, applyFailureReason } from '../../local/local-apply-error'
|
|
55
|
+
import { sayInFull } from '../../local/local-said-once'
|
|
55
56
|
import { applyFailures, inheritedApplyLine, splitApplyFailures } from '../../local/local-apply-summary'
|
|
56
57
|
import { unchangedSincePull } from '../../local/local-check-inherited'
|
|
58
|
+
import { unreadConfidential, unreadConfidentialLines } from '../../local/local-confidential'
|
|
59
|
+
import { ungrantedReadLines, ungrantedReads } from '../../local/local-ungranted-reads'
|
|
57
60
|
import { readWorkspaceGrants } from '../../local/local-workspace-grants'
|
|
58
61
|
import { ApplyWatchdog, stoppedReapplyingLine } from '../../local/local-apply-watchdog'
|
|
59
62
|
import { hasGeneratedSampleData, pulledSampleData, pulledSampleDataNote, remakeLines, remakeReason, remakeSampleData } from '../../local/local-sample-remake'
|
|
60
63
|
import { appsAwayByRestore } from '../../local/local-app-restore'
|
|
61
64
|
import { BASELINE_RECORD, readBaseline } from '../../local/local-db-snapshot'
|
|
62
|
-
import { canChangeRuntime, filesChangedTrigger, recordApply } from '../../local/local-apply-history'
|
|
65
|
+
import { canChangeRuntime, filesChangedTrigger, readApplies, recordApply } from '../../local/local-apply-history'
|
|
63
66
|
import { failedForRuntime, RUNTIME_DOWN_ERROR, RUNTIME_STOPPED_LINE, runtimeAnswering } from '../../local/local-runtime-health'
|
|
64
67
|
import { noticeLines, projectNotices } from '../../local/local-project-notices'
|
|
65
68
|
import { refreshVendoredSdk, vendoredSdkState } from '../../local/local-vendored-sdk'
|
|
@@ -342,6 +345,27 @@ async function apply(project: LocalProject, target: SyncTarget, opts: ApplyOptio
|
|
|
342
345
|
result = withNotes(await runCli(project, commandFor(target, profiles), opts.auth))
|
|
343
346
|
}
|
|
344
347
|
}
|
|
348
|
+
// A Function that grants itself a workspace Plugin this runtime has no install of: a stand-in of that
|
|
349
|
+
// name is written and installed, and the Function deployed again; its calls say "not connected" here (X2).
|
|
350
|
+
for (let n = 0; !result.ok && target.kind === 'function' && n < 5; n++) {
|
|
351
|
+
const missing = missingGrantInstalls(result.output)
|
|
352
|
+
if (!missing.length) break
|
|
353
|
+
const installs = [...new Set(missing.map((g) => g.install))]
|
|
354
|
+
const made: string[] = []
|
|
355
|
+
for (const install of installs) {
|
|
356
|
+
const wrote = writeGrantStandIn(project.root, install, missing.filter((g) => g.install === install).map((g) => g.capability))
|
|
357
|
+
if (!wrote && !readStandIn(project.root, install)) break
|
|
358
|
+
const added = await applyStandIn(project, install, profiles.workspace, (args) => runCli(project, args, opts.auth))
|
|
359
|
+
if (!added.ok) {
|
|
360
|
+
result = { ...result, output: `${result.output.trim()}\n the stand-in ${standInPath(install)} could not be installed: ${oneLine(added.output)}` }
|
|
361
|
+
break
|
|
362
|
+
}
|
|
363
|
+
made.push(install)
|
|
364
|
+
}
|
|
365
|
+
if (made.length !== installs.length) break
|
|
366
|
+
notes = [notes, ...made.map((i) => ` warning: ${original.file} grants itself the Plugin ${i}, which this runtime doesn’t have: the stand-in ${standInPath(i)} stands in for it, and its calls answer “not connected” here (the workspace’s own install is used there)`)].filter(Boolean).join('\n')
|
|
367
|
+
result = withNotes(await runCli(project, command, opts.auth, bundleEnv))
|
|
368
|
+
}
|
|
345
369
|
// A Function whose build cannot find a file it imports from beside its folder: said as that, first.
|
|
346
370
|
if (!result.ok && target.kind === 'function') result = { ...result, output: missingImportsOutput(project, original.file, result.output) }
|
|
347
371
|
if (!result.ok) return result
|
|
@@ -448,6 +472,14 @@ async function apply(project: LocalProject, target: SyncTarget, opts: ApplyOptio
|
|
|
448
472
|
if (workspaceGrants?.problems.length) return { ok: false, output: workspaceGrants.problems.map((p) => `error [WORKSPACE_GRANTS]: ${p}`).join('\n') }
|
|
449
473
|
const unknownTypes = (workspaceGrants?.grants ?? []).filter((g) => !objectTypes.includes(g.objectType)).map((g) => g.objectType)
|
|
450
474
|
if (unknownTypes.length) result = { ...result, output: `${result.output}\n warning: blueprint/workspace-grants.yaml names ${unknownTypes.join(', ')}, which the project's Blueprint doesn't hold here` }
|
|
475
|
+
// Confidential fields nothing here may read (X7): each reads as hidden, which looks like a mistake once an Agent says so.
|
|
476
|
+
if (target.kind === 'blueprint') {
|
|
477
|
+
const unread = unreadConfidentialLines(unreadConfidential(project.root))
|
|
478
|
+
if (unread.length) result = { ...result, output: [result.output, ...unread].join('\n') }
|
|
479
|
+
// What the project's workflows and Apps read that the linked workspace isn't granted (X2).
|
|
480
|
+
const ungranted = ungrantedReadLines(ungrantedReads(project.root))
|
|
481
|
+
if (ungranted.length) result = { ...result, output: [result.output, ...ungranted].join('\n') }
|
|
482
|
+
}
|
|
451
483
|
for (const step of followUpsFor(target, profiles, { publish: opts.publish, workspaceId: opts.workspaceId, objectTypes, grants: workspaceGrants?.grants ?? [] })) {
|
|
452
484
|
const done = await runCli(project, step.args, opts.auth)
|
|
453
485
|
if (!done.ok && !step.alreadyDone?.test(done.output)) return done
|
|
@@ -807,7 +839,9 @@ async function dev(ctx: CommandContext) {
|
|
|
807
839
|
const mirrored = await mirrorOrgSettings(synced, settingsCalls(serviceUrl, creds), { localEmbeddings: localEmbeddingsReachable() }).catch(() => null)
|
|
808
840
|
if (mirrored) {
|
|
809
841
|
for (const line of mirrored.lines) ctx.output.note(line)
|
|
810
|
-
|
|
842
|
+
// In full once per runtime start, then one line while the list is the same (N9).
|
|
843
|
+
if (sayInFull(project, 'settings-differ', mirrored.differences, state.startedAt)) for (const d of mirrored.differences) ctx.output.note(` differs from the workspace: ${d}`)
|
|
844
|
+
else if (mirrored.differences.length) ctx.output.note(` ${mirrored.differences.length} setting${mirrored.differences.length === 1 ? '' : 's'} differ from the workspace, as before (\`frontera local status\` lists them)`)
|
|
811
845
|
recordSettingsDifferences(project, organizationId, mirrored.differences)
|
|
812
846
|
}
|
|
813
847
|
}
|
|
@@ -870,7 +904,7 @@ async function dev(ctx: CommandContext) {
|
|
|
870
904
|
const missing = result.missing?.length ? result.missing : target.kind === 'bindings' ? missingFromBindings(result.output) : []
|
|
871
905
|
if (missing.length || result.unnamedActions) return { ...base, status: 'blocked', needs: missing, ...(result.unnamedActions ? { unnamedActions: result.unnamedActions } : {}) }
|
|
872
906
|
// A Blueprint published with an Action that didn't deploy keeps how each type and Action came out.
|
|
873
|
-
return { ...base, status: 'failed', error:
|
|
907
|
+
return { ...base, status: 'failed', error: applyFailureReason(result.output), ...failureCodeAndHint(result.output), ...(result.blueprint ? blueprintRecord(result.blueprint) : {}) }
|
|
874
908
|
}
|
|
875
909
|
|
|
876
910
|
const runOne = async (target: SyncTarget) => {
|
|
@@ -1296,6 +1330,30 @@ async function dev(ctx: CommandContext) {
|
|
|
1296
1330
|
}
|
|
1297
1331
|
})())
|
|
1298
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* An Agent whose operating.json was deleted while its agent.json stays: its behavior settings are
|
|
1335
|
+
* taken away on the runtime too, as a change request takes them away in the workspace (X4). Before,
|
|
1336
|
+
* the runtime kept them, and the Agent stayed out of the built-in Chat here.
|
|
1337
|
+
*/
|
|
1338
|
+
const clearDeletedBehavior = async (): Promise<void> => {
|
|
1339
|
+
if (!creds?.session) return
|
|
1340
|
+
const known = loadHashes(project, organizationId)
|
|
1341
|
+
const present = new Set(allTargets(project.root).map((t) => t.key))
|
|
1342
|
+
for (const key of [...known.keys()].filter((k) => k.startsWith('operating:') && !present.has(k))) {
|
|
1343
|
+
const slug = key.slice('operating:'.length)
|
|
1344
|
+
if (!present.has(`agent:${slug}`)) continue
|
|
1345
|
+
const cleared = await applyOperatingConfig({ serviceUrl, creds, slug, file: join(project.root, 'agents', slug, 'operating.json'), document: null })
|
|
1346
|
+
if (!cleared.ok) {
|
|
1347
|
+
ctx.output.note(` warning: agents/${slug}/operating.json was deleted, and its behavior settings couldn’t be taken away here: ${oneLine(cleared.output)}`)
|
|
1348
|
+
continue
|
|
1349
|
+
}
|
|
1350
|
+
ctx.output.note(` ✓ Agent ${slug}: behavior settings taken away (agents/${slug}/operating.json was deleted); publish it and the built-in Chat offers it again`)
|
|
1351
|
+
applied.delete(key)
|
|
1352
|
+
known.delete(key)
|
|
1353
|
+
saveHashes(project, organizationId, known)
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1299
1357
|
// Watch from the start, so a save during the first sync is not missed.
|
|
1300
1358
|
const once = flagBool(ctx, 'once')
|
|
1301
1359
|
// One watcher per project (Forge keeps one running; a second would apply every change twice). `--once` is always allowed.
|
|
@@ -1322,6 +1380,7 @@ async function dev(ctx: CommandContext) {
|
|
|
1322
1380
|
if (filename.toString() === BASELINE_RECORD) afterRestore()
|
|
1323
1381
|
// An Agent's folder, a workflow's or a Function's file deleted: what the runtime holds follows.
|
|
1324
1382
|
if (/^(agents|workflows|functions)([\\/]|$)/.test(filename.toString()) && !existsSync(join(project.root, filename.toString()))) void sweepOrphans()
|
|
1383
|
+
if (/^agents[\\/][^\\/]+[\\/]operating\.json$/.test(filename.toString()) && !existsSync(join(project.root, filename.toString()))) void clearDeletedBehavior()
|
|
1325
1384
|
// Only a change that queued an item starts a round: the watcher's own writes under `.frontera/`,
|
|
1326
1385
|
// a screenshot, a coding agent's settings or a note in docs/ never do.
|
|
1327
1386
|
if (queuePath(filename.toString()) && !busy) schedule()
|
|
@@ -1332,6 +1391,7 @@ async function dev(ctx: CommandContext) {
|
|
|
1332
1391
|
pruneBuild(project, organizationId, new Set(targets.map((t) => t.key)))
|
|
1333
1392
|
// An Agent, workflow or Function whose file was deleted is taken out of service here, as the files now say.
|
|
1334
1393
|
await sweepOrphans()
|
|
1394
|
+
await clearDeletedBehavior()
|
|
1335
1395
|
ctx.output.note(targets.length ? `Applying ${targets.length} item${targets.length === 1 ? '' : 's'} to the local runtime` : 'Nothing to apply yet')
|
|
1336
1396
|
// Files in the applied folders that match no kind: said, not skipped in silence.
|
|
1337
1397
|
for (const line of unappliedFiles(project.root)) {
|
|
@@ -1377,13 +1437,21 @@ async function dev(ctx: CommandContext) {
|
|
|
1377
1437
|
throw new CliError(message, { code: 'LOCAL_APPLY_FAILED', hint: 'fix the files above and run again; `frontera local status` lists each item that didn’t apply', details: { failed: made, ...inheritedField } })
|
|
1378
1438
|
}
|
|
1379
1439
|
const done = targets.length - unchanged
|
|
1440
|
+
// Nothing new here, but the watcher (or another `local dev`) applied these files already: said when, and why (N9).
|
|
1441
|
+
const already = done === 0 ? readApplies(project.stateDir).filter((r) => r.applied > 0).at(-1) : undefined
|
|
1442
|
+
const alreadyField = already ? { alreadyApplied: { at: already.finished, trigger: already.trigger } } : {}
|
|
1443
|
+
const alreadyText = already ? `; already applied by \`frontera local dev\` at ${new Date(already.finished).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} (${already.trigger})` : ''
|
|
1380
1444
|
if (inherited.length) {
|
|
1381
1445
|
const summary = inheritedApplyLine(inherited.length)
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1446
|
+
// Each inherited failure in full once per runtime start, then the one line while they stay the same.
|
|
1447
|
+
const lines = inherited.map((f) => `${f.ref ?? f.item}: ${oneLine(f.error)}`)
|
|
1448
|
+
if (sayInFull(project, 'inherited-failures', lines, state.startedAt)) {
|
|
1449
|
+
ctx.output.note(` ${summary}:`)
|
|
1450
|
+
for (const l of lines) ctx.output.note(` ${l}`)
|
|
1451
|
+
} else ctx.output.note(` ${summary}, as before (\`frontera local status\` lists them)`)
|
|
1452
|
+
return { data: { applied: done, unchanged, failed: 0, ...inheritedField, summary, ...alreadyField, ...(warned.size ? { warnings: [...warned.values()] } : {}) }, text: `Applied ${done}${unchanged ? `, ${unchanged} unchanged` : ''}${alreadyText}; ${summary}` }
|
|
1385
1453
|
}
|
|
1386
|
-
return { data: { applied: done, unchanged, ...(warned.size ? { warnings: [...warned.values()] } : {}) }, text: `Applied ${done}${unchanged ? `, ${unchanged} unchanged` : ''}` }
|
|
1454
|
+
return { data: { applied: done, unchanged, ...alreadyField, ...(warned.size ? { warnings: [...warned.values()] } : {}) }, text: `Applied ${done}${unchanged ? `, ${unchanged} unchanged` : ''}${alreadyText}` }
|
|
1387
1455
|
}
|
|
1388
1456
|
|
|
1389
1457
|
ctx.output.note('Watching datasets/, blueprint/, knowledge/, skills/, mcp/, themes/, functions/, workflows/, agents/, chat-apps/, apps/, brand/ and library.json (Ctrl+C to stop)')
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CliError, UsageError } from '../../errors'
|
|
2
|
+
import { embeddingKeyOptions, embeddingNextStep, EMBEDDING_KEY_SOURCES, recommendedEmbeddingKey } from '../../local/local-embedding-setup'
|
|
3
|
+
import { configuredProviders, setKey } from '../../local/local-keys'
|
|
4
|
+
import { PLATFORM_DEFAULT_EMBEDDING } from '../../local/local-org-settings'
|
|
5
|
+
import { localProject } from '../../local/local-project'
|
|
6
|
+
import { readSnapshot } from '../../local/local-sync-snapshot'
|
|
7
|
+
import { readState } from '../../local/local-state'
|
|
8
|
+
import { type Command } from '../types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `frontera local embeddings [use <provider>]` (Forge improvement plan L9):
|
|
12
|
+
* what the local runtime embeds knowledge with, and real embeddings from a
|
|
13
|
+
* key the person already has. A key is read from the environment and stored
|
|
14
|
+
* as `local key-set` stores one; it is never printed.
|
|
15
|
+
*/
|
|
16
|
+
export const localEmbeddings: Command = {
|
|
17
|
+
meta: {
|
|
18
|
+
noun: 'local',
|
|
19
|
+
verb: 'embeddings',
|
|
20
|
+
args: [
|
|
21
|
+
{ name: 'use', required: false, description: '`use` to store a key from this shell for the runtime’s embeddings' },
|
|
22
|
+
{ name: 'provider', required: false, description: EMBEDDING_KEY_SOURCES.map((s) => s.provider).join(' | ') },
|
|
23
|
+
],
|
|
24
|
+
flags: {},
|
|
25
|
+
summary: 'Say what the local runtime embeds knowledge with, and use a key you already have for real embeddings',
|
|
26
|
+
details: [
|
|
27
|
+
'Without a key the runtime embeds with approximate local vectors (passages found by shared words). An OpenAI, OpenRouter or Gemini key gives a provider’s embedding model; the one that embeds as the workspace does is recommended.',
|
|
28
|
+
'`use <provider>` reads its key from this shell’s environment (OPENAI_API_KEY, OPENROUTER_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY), stores it in the keychain and never prints it. The runtime takes it at its next start.',
|
|
29
|
+
],
|
|
30
|
+
examples: ['frontera local embeddings', 'frontera local embeddings use openai'],
|
|
31
|
+
offline: true,
|
|
32
|
+
},
|
|
33
|
+
async run(ctx) {
|
|
34
|
+
const [sub, provider] = ctx.positional
|
|
35
|
+
const project = localProject(ctx.cwd)
|
|
36
|
+
const state = readState(project)
|
|
37
|
+
const keyless = !!state?.credentials?.modelProviders?.includes('local-embeddings')
|
|
38
|
+
const workspace = readSnapshot(project.root)?.settings?.embedding ?? PLATFORM_DEFAULT_EMBEDDING
|
|
39
|
+
const options = embeddingKeyOptions(process.env, configuredProviders(), workspace)
|
|
40
|
+
|
|
41
|
+
if (sub === undefined) {
|
|
42
|
+
const recommended = recommendedEmbeddingKey(options)
|
|
43
|
+
const next = embeddingNextStep(options, keyless)
|
|
44
|
+
const row = (o: (typeof options)[number]) => ` ${o.label.padEnd(14)} ${o.stored ? 'key stored' : o.envVar ? `key in ${o.envVar}` : 'no key'}${o.matchesWorkspace ? ', embeds as the workspace does' : ''}`
|
|
45
|
+
return {
|
|
46
|
+
data: { running: state?.phase === 'ready', keyless, workspaceEmbedding: workspace, options, recommended: recommended?.provider ?? null, next },
|
|
47
|
+
text: [
|
|
48
|
+
state?.phase === 'ready' ? (keyless ? 'The runtime embeds with the approximate local vectors (no key).' : 'The runtime embeds with a provider’s model.') : 'The local runtime is not running.',
|
|
49
|
+
'Providers that can embed here:',
|
|
50
|
+
...options.map(row),
|
|
51
|
+
`Next: ${next}`,
|
|
52
|
+
].join('\n'),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (sub !== 'use') throw new UsageError(`\`local embeddings\` takes \`use <provider>\`, not "${sub}"`, 'frontera local embeddings use openai')
|
|
56
|
+
const option = options.find((o) => o.provider === provider)
|
|
57
|
+
if (!option) throw new UsageError(provider ? `${provider} gives no embeddings here` : 'name the provider', `one of: ${EMBEDDING_KEY_SOURCES.map((s) => s.provider).join(', ')}`)
|
|
58
|
+
if (!option.envVar) {
|
|
59
|
+
throw new CliError(`no ${option.label} key is in this shell’s environment`, {
|
|
60
|
+
code: 'USAGE',
|
|
61
|
+
hint: `set ${EMBEDDING_KEY_SOURCES.find((s) => s.provider === option.provider)!.vars.join(' or ')} and run it again, or store one at a prompt: \`frontera local key-set ${option.provider}\``,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
setKey(option.provider, process.env[option.envVar]!.trim())
|
|
65
|
+
return {
|
|
66
|
+
data: { provider: option.provider, stored: true, from: option.envVar, matchesWorkspace: option.matchesWorkspace, restart: true },
|
|
67
|
+
text: [
|
|
68
|
+
`Stored the ${option.label} key from ${option.envVar} for the local runtime (its value is not shown).`,
|
|
69
|
+
`The runtime embeds with it from its next start: \`frontera local stop\`, then \`frontera local start\`; the next \`frontera local dev\` sets each knowledge base’s threshold for that model.`,
|
|
70
|
+
...(option.matchesWorkspace ? [] : ['The workspace embeds with another model, so retrieval here ranks a little differently from the workspace’s.']),
|
|
71
|
+
].join('\n'),
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|