@frontera-sdk/cli 1.52.42 → 1.52.44
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/commands/local/check.ts +17 -9
- package/src/commands/local/index-commands.ts +7 -1
- package/src/commands/local/snapshot.ts +8 -2
- package/src/commands/local/sync-request.ts +95 -9
- package/src/commands/local/sync.ts +27 -6
- package/src/commands/registry.ts +2 -1
- package/src/local/local-case-evidence.ts +3 -2
- package/src/local/local-db-snapshot.ts +23 -3
- package/src/local/local-dev-references.ts +59 -7
- package/src/local/local-env.ts +21 -0
- package/src/local/local-history-carry.ts +103 -0
- package/src/local/local-key-custodian.ts +64 -0
- package/src/local/local-knowledge-uploads.ts +62 -5
- package/src/local/local-scaffold.ts +48 -13
- package/src/local/local-schemas.ts +1 -1
- package/src/local/local-studio-server.ts +1 -1
- package/src/local/local-supervisor.ts +13 -0
- package/src/local/local-sync-link.ts +43 -2
- package/src/local/local-sync-models.ts +118 -0
- package/src/local/local-sync-plan.ts +19 -9
- package/src/local/local-sync-portable.ts +269 -0
- package/src/local/local-sync-precheck.ts +94 -0
- package/src/local/local-sync-push.ts +45 -7
- package/src/local/local-sync-remote.ts +39 -8
- package/src/local/local-sync-snapshot.ts +9 -1
|
@@ -16,8 +16,11 @@ import { lessonsPath, lessonsProposal } from './local-lessons'
|
|
|
16
16
|
import { importedFiles } from './local-function-shared'
|
|
17
17
|
import { isIgnoredName } from './local-dev-sync'
|
|
18
18
|
import { comparableText, withoutSecrets } from './local-secrets'
|
|
19
|
+
import { projectModels } from './local-sync-models'
|
|
19
20
|
import { deepEqual } from './local-sync-merge'
|
|
21
|
+
import { portableNotes, sameDocumentText, toLocal, toWorkspace } from './local-sync-portable'
|
|
20
22
|
import { itemTitle } from './local-sync-names'
|
|
23
|
+
import { isScaffoldSample } from './local-scaffold'
|
|
21
24
|
import { hash, itemHash, readLocal, safePath, writeLock, type LockEntry, type PlanItem, type RemoteItem, type SyncKind, type SyncLock } from './local-sync-plan'
|
|
22
25
|
import { dropBase, SYNC_DIR, writeBase, type RemoteSnapshot } from './local-sync-snapshot'
|
|
23
26
|
|
|
@@ -55,8 +58,12 @@ const NOT_DELETED: Partial<Record<SyncKind, string>> = {
|
|
|
55
58
|
lessons: 'An Agent’s rules are retired on the platform, where they are reviewed: a change request only proposes new ones',
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Why an item cannot be proposed now. `model`: it names a model the workspace
|
|
63
|
+
* lacks and `sync.models` maps to none; `refused`: the workspace's checks
|
|
64
|
+
* would refuse it (`POST /v1/change-requests/check`), said before proposing.
|
|
65
|
+
*/
|
|
66
|
+
export type BlockCode = 'moved' | 'conflicts' | 'deleted' | 'workspace-deleted' | 'kind' | 'shape' | 'model' | 'refused'
|
|
60
67
|
|
|
61
68
|
/** A skill's file that is not text (an image, a font, a template): uploaded as a skill asset when proposed. */
|
|
62
69
|
export interface PushAsset {
|
|
@@ -96,6 +103,15 @@ export interface PushItem {
|
|
|
96
103
|
/** Why not, when it is not. */
|
|
97
104
|
blocked: BlockCode | null
|
|
98
105
|
reason: string | null
|
|
106
|
+
/** What makes it proposable, when something here can: the command or the choice. */
|
|
107
|
+
fix?: string | null
|
|
108
|
+
/**
|
|
109
|
+
* Still exactly the sample setting up the project wrote (`isScaffoldSample`):
|
|
110
|
+
* proposable, but left out unless named or ticked.
|
|
111
|
+
*/
|
|
112
|
+
sample?: boolean
|
|
113
|
+
/** The models it names that the workspace lacks and `sync.models` maps to none, by local slug. */
|
|
114
|
+
missingModels?: string[]
|
|
99
115
|
/** The platform's id, from the lock (or the snapshot). */
|
|
100
116
|
id: string | null
|
|
101
117
|
/** The revision the item was changed from; for a Blueprint file, the Blueprint's. */
|
|
@@ -165,7 +181,7 @@ function functionImports(root: string, entry: string): string[] {
|
|
|
165
181
|
* service). The lock's paths that still exist come along, so nothing the
|
|
166
182
|
* last pull wrote is left behind.
|
|
167
183
|
*/
|
|
168
|
-
export function itemFiles(root: string, kind: SyncKind, name: string, entry?: LockEntry, opts: { documents?: boolean } = {}): { files: Record<string, string>; notes: string[]; problem?: string; assets?: string[] } {
|
|
184
|
+
export function itemFiles(root: string, kind: SyncKind, name: string, entry?: LockEntry, opts: { documents?: boolean; upstream?: Record<string, string> } = {}): { files: Record<string, string>; notes: string[]; problem?: string; assets?: string[] } {
|
|
169
185
|
const notes: string[] = []
|
|
170
186
|
let paths: string[] = []
|
|
171
187
|
const assets: string[] = []
|
|
@@ -228,6 +244,8 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
|
|
|
228
244
|
// What the last pull wrote for it comes along when it is still here (an agent's files are fixed; the rest may not be).
|
|
229
245
|
if (kind !== 'agent' && kind !== 'knowledge') for (const p of Object.keys(entry?.files ?? {})) if (!paths.includes(p)) paths.push(p)
|
|
230
246
|
const files: Record<string, string> = {}
|
|
247
|
+
const here: Record<string, string> = {}
|
|
248
|
+
const models = projectModels(root)
|
|
231
249
|
for (const path of [...new Set(paths)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
|
|
232
250
|
if (path.endsWith('.workspace') || assets.includes(path)) continue
|
|
233
251
|
const text = readLocal(root, path)
|
|
@@ -236,8 +254,13 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
|
|
|
236
254
|
const clean = withoutSecrets(text)
|
|
237
255
|
files[path] = clean.text
|
|
238
256
|
notes.push(...clean.notes)
|
|
239
|
-
} else
|
|
257
|
+
} else {
|
|
258
|
+
// As the workspace will have it: a mapped model swapped, the multi-turn evaluation cases left here.
|
|
259
|
+
files[path] = toWorkspace(path, text, models, opts.upstream?.[path] ?? null).text
|
|
260
|
+
here[path] = text
|
|
261
|
+
}
|
|
240
262
|
}
|
|
263
|
+
notes.push(...portableNotes(here, models, models.names, opts.upstream))
|
|
241
264
|
return { files, notes, ...(kind === 'skill' ? { assets } : {}) }
|
|
242
265
|
}
|
|
243
266
|
|
|
@@ -305,7 +328,7 @@ export interface CandidateInput {
|
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
|
|
308
|
-
const BLOCK_REASON: Record<Exclude<BlockCode, 'kind' | 'shape'>, string> = {
|
|
331
|
+
const BLOCK_REASON: Record<Exclude<BlockCode, 'kind' | 'shape' | 'model' | 'refused'>, string> = {
|
|
309
332
|
moved: 'Changed in the workspace too since your last pull: pull and merge it first',
|
|
310
333
|
conflicts: 'Has conflicts from a pull: resolve them first',
|
|
311
334
|
deleted: 'Deleted here',
|
|
@@ -389,7 +412,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
389
412
|
if (status === 'new' || status === 'changed') continue
|
|
390
413
|
const deleted = status === 'deleted-local'
|
|
391
414
|
const bringDocuments = kind === 'knowledge' && !!input.documents?.names.has(name)
|
|
392
|
-
const { files, notes, problem, assets: assetPaths } = deleted ? { files: {} as Record<string, string>, notes: [] as string[], problem: undefined, assets: undefined } : itemFiles(root, kind, name, entry, { documents: bringDocuments })
|
|
415
|
+
const { files, notes, problem, assets: assetPaths } = deleted ? { files: {} as Record<string, string>, notes: [] as string[], problem: undefined, assets: undefined } : itemFiles(root, kind, name, entry, { documents: bringDocuments, ...(theirs ? { upstream: theirs.files } : {}) })
|
|
393
416
|
// What a file hash does not cover: a skill's assets, and a knowledge base's documents when they are brought.
|
|
394
417
|
const assetState = kind === 'skill' && !deleted && (assetPaths?.length || theirs?.assets?.length) ? skillAssets(root, name, assetPaths ?? [], theirs) : null
|
|
395
418
|
const docs = bringDocuments && !deleted && input.documents
|
|
@@ -422,6 +445,10 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
422
445
|
base: baseFiles,
|
|
423
446
|
notes,
|
|
424
447
|
}
|
|
448
|
+
if (!deleted && isScaffoldSample(files)) {
|
|
449
|
+
item.sample = true
|
|
450
|
+
notes.push('A sample from setting up the project, not your work: it stays here unless you tick it (or name it to `frontera local sync push`)')
|
|
451
|
+
}
|
|
425
452
|
if (assetState) {
|
|
426
453
|
item.assets = assetState.assets
|
|
427
454
|
for (const a of assetState.assets) if (a.change !== 'same') notes.push(`Asset ${a.path}: ${a.change === 'new' ? 'new' : 'changed'} (${kb(a.bytes)})`)
|
|
@@ -770,9 +797,20 @@ export function adoptApplied(root: string, workspace: string, refs: string[], re
|
|
|
770
797
|
if (!item) continue
|
|
771
798
|
for (const [path, text] of Object.entries(item.files)) {
|
|
772
799
|
const here = readLocal(root, path)
|
|
773
|
-
if (here
|
|
800
|
+
if (here === null || here === text) continue
|
|
801
|
+
if (sameDocument(path, here, text)) {
|
|
774
802
|
writeFileSync(safePath(root, path), text)
|
|
775
803
|
rewritten.push(path)
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
806
|
+
// What the workspace now holds, with this computer's parts kept (a mapped model, the multi-turn cases).
|
|
807
|
+
const portable = toWorkspace(path, here, projectModels(root), text).text
|
|
808
|
+
if (portable !== here && (sameDocument(path, portable, text) || sameDocumentText(path, portable, text))) {
|
|
809
|
+
const local = toLocal(path, text, here, projectModels(root))
|
|
810
|
+
if (local !== here) {
|
|
811
|
+
writeFileSync(safePath(root, path), local)
|
|
812
|
+
rewritten.push(path)
|
|
813
|
+
}
|
|
776
814
|
}
|
|
777
815
|
}
|
|
778
816
|
next.items[ref] = {
|
|
@@ -164,7 +164,8 @@ async function listActions(org: SyncClient): Promise<ActionRow[]> {
|
|
|
164
164
|
const d = ((row as { definition?: unknown } | null)?.definition ?? row) as Partial<ActionRow> | null
|
|
165
165
|
return d && typeof d.id === 'string' && typeof d.apiName === 'string' ? [{ id: d.id, apiName: d.apiName }] : []
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
// An organization that has never published a Blueprint has no draft to read: no Actions (404).
|
|
168
|
+
const draft = rows<unknown>(await org.find('/v1/blueprint/actions?view=draft')).flatMap(pick)
|
|
168
169
|
const published = rows<unknown>(await org.get('/v1/blueprint/actions').catch(() => null)).flatMap(pick)
|
|
169
170
|
return [...new Map([...published, ...draft].map((a) => [a.id, a])).values()]
|
|
170
171
|
}
|
|
@@ -191,8 +192,8 @@ export async function buildDirectory(source: SyncSource, warnings: string[] = []
|
|
|
191
192
|
quietly(warnings, 'Blueprint object types', async () => rows<{ id: string; apiName: string }>(await ws.get('/v1/blueprint/object-types')), []),
|
|
192
193
|
// The model catalog is organization-grain: a workspace key reads its names only, where the platform offers them.
|
|
193
194
|
source.organization
|
|
194
|
-
? quietly(warnings, 'Models', async () => rows<
|
|
195
|
-
: quietly(warnings, 'Models', async () => rows<
|
|
195
|
+
? quietly(warnings, 'Models', async () => rows<CatalogModelRow>(await source.organization!.get('/v1/config/models/catalog')), [])
|
|
196
|
+
: quietly(warnings, 'Models', async () => rows<CatalogModelRow>(await ws.find<unknown>('/v1/sync/models')), []),
|
|
196
197
|
// So are governed Actions' definitions: without the organization key they are not listed.
|
|
197
198
|
source.organization ? quietly<ActionRow[] | null>(warnings, 'Governed Actions', () => listActions(source.organization!), null) : Promise.resolve(null),
|
|
198
199
|
// A platform without the route has none to name: absent, as for Actions.
|
|
@@ -221,7 +222,28 @@ export async function buildDirectory(source: SyncSource, warnings: string[] = []
|
|
|
221
222
|
...(objectSets ? { objectSet: new Map((objectSets ?? []).map((o) => [setNames.get(o.id)!, o.id])) } : {}),
|
|
222
223
|
...(objectDefinitions ? { object: new Map(objectDefinitions.map((o) => [o.slug, o.id])) } : {}),
|
|
223
224
|
}
|
|
224
|
-
|
|
225
|
+
const catalog = models.map((m) => ({ slug: m.slug, name: m.name ?? m.slug, kind: m.kind ?? 'text', ...(m.provider ? { provider: m.provider } : {}), ...(m.modelId ? { modelId: m.modelId } : {}) }))
|
|
226
|
+
return { directory, lists: { agents, workflows, functions, knowledge, apps, skills, objectSets: objectSets ?? [], objectDefinitions, models: catalog } }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A model of the organization's catalog as the workspace lists it; the provider's model id tells two slugs for one model apart. */
|
|
230
|
+
interface CatalogModelRow {
|
|
231
|
+
id: string
|
|
232
|
+
slug: string
|
|
233
|
+
name?: string
|
|
234
|
+
kind?: string
|
|
235
|
+
provider?: string
|
|
236
|
+
modelId?: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A model of the workspace's catalog, kept with the snapshot: what a project's model can be matched against. */
|
|
240
|
+
export interface WorkspaceCatalogModel {
|
|
241
|
+
slug: string
|
|
242
|
+
name: string
|
|
243
|
+
kind: string
|
|
244
|
+
provider?: string
|
|
245
|
+
/** The provider's id for it (`deepseek/deepseek-v4.1-flash`): the same model under another slug has the same one. */
|
|
246
|
+
modelId?: string
|
|
225
247
|
}
|
|
226
248
|
|
|
227
249
|
export interface Lists {
|
|
@@ -235,6 +257,8 @@ export interface Lists {
|
|
|
235
257
|
objectSets?: ObjectSetRow[]
|
|
236
258
|
/** The object definitions an Agent's object tools work on; null when the platform doesn't list them. */
|
|
237
259
|
objectDefinitions?: ObjectDefinitionRow[] | null
|
|
260
|
+
/** The organization's model catalog, as far as the key reads it. */
|
|
261
|
+
models?: WorkspaceCatalogModel[]
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
export interface ReadOptions {
|
|
@@ -1251,7 +1275,13 @@ async function readBlueprint(
|
|
|
1251
1275
|
// The workspace's grants beyond read, from its slice (the organization's model has none of a workspace's).
|
|
1252
1276
|
let grants: RemoteItem | null = null
|
|
1253
1277
|
if (mode === 'organization' && source.organization) {
|
|
1254
|
-
|
|
1278
|
+
// None (404) for an organization that has never published or drafted a Blueprint: nothing to pull.
|
|
1279
|
+
const found = await source.organization.find<unknown>('/v1/blueprint/lifecycle')
|
|
1280
|
+
if (!found) {
|
|
1281
|
+
notes.push('The organization has no Blueprint yet; nothing was pulled into blueprint/')
|
|
1282
|
+
return []
|
|
1283
|
+
}
|
|
1284
|
+
const lifecycle = one<Lifecycle>(found)
|
|
1255
1285
|
fromDraft = !!lifecycle.draft?.definition && (!!opts.draft || !lifecycle.activeRelease?.definition)
|
|
1256
1286
|
answer = { definition: (fromDraft ? lifecycle.draft?.definition : lifecycle.activeRelease?.definition) ?? null, datasetNames: await datasetNames(source.organization) }
|
|
1257
1287
|
if (fromDraft) notes.push(opts.draft ? 'The Blueprint was read from its unpublished draft, as asked' : 'The Blueprint has no published release; its draft was read')
|
|
@@ -1270,7 +1300,8 @@ async function readBlueprint(
|
|
|
1270
1300
|
if (opts.draft && !source.organization) notes.push('Reading the Blueprint’s unpublished draft takes an organization key; its published release was read')
|
|
1271
1301
|
if (opts.draft && source.organization && answer) {
|
|
1272
1302
|
// The draft, cut to the object types the workspace's slice holds now.
|
|
1273
|
-
const
|
|
1303
|
+
const found = await source.organization.find<unknown>('/v1/blueprint/lifecycle')
|
|
1304
|
+
const lifecycle = found ? one<Lifecycle>(found) : ({} as Lifecycle)
|
|
1274
1305
|
const granted = new Set(((answer.definition as { objects?: Array<{ apiName: string }> } | null)?.objects ?? []).map((o) => o.apiName))
|
|
1275
1306
|
if (lifecycle.draft?.definition) {
|
|
1276
1307
|
answer = { definition: sliceDraft(lifecycle.draft.definition, granted), datasetNames: { ...(answer.datasetNames ?? {}), ...(await datasetNames(source.organization)) } }
|
|
@@ -1606,7 +1637,7 @@ async function readEvaluations(source: SyncSource, lists: Lists, directory: Dire
|
|
|
1606
1637
|
* collects what could not be read (a kind the key may not see, a platform
|
|
1607
1638
|
* too old for an export), so the plan says so instead of failing.
|
|
1608
1639
|
*/
|
|
1609
|
-
export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}): Promise<{ items: RemoteItem[]; warnings: string[]; notes: string[]; directory: Directory }> {
|
|
1640
|
+
export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}): Promise<{ items: RemoteItem[]; warnings: string[]; notes: string[]; directory: Directory; models: WorkspaceCatalogModel[] }> {
|
|
1610
1641
|
const warnings: string[] = []
|
|
1611
1642
|
const notes: string[] = []
|
|
1612
1643
|
const { directory, lists } = await buildDirectory(source, warnings)
|
|
@@ -1694,7 +1725,7 @@ export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}):
|
|
|
1694
1725
|
? 'Governed Actions are named only with an organization key (they are organization-wide); an Action step shows as an id until you connect one'
|
|
1695
1726
|
: 'Models and governed Actions are named only with an organization key (both are organization-wide); an LLM step’s model or an Action step shows as an id until you connect one')
|
|
1696
1727
|
}
|
|
1697
|
-
return { items, warnings, notes, directory }
|
|
1728
|
+
return { items, warnings, notes, directory, models: lists.models ?? [] }
|
|
1698
1729
|
}
|
|
1699
1730
|
|
|
1700
1731
|
/**
|
|
@@ -7,7 +7,7 @@ import { writeFileAtomic } from '../fs-atomic'
|
|
|
7
7
|
import { hash, itemHash, safePath, SYNC_KINDS, type RemoteItem, type SyncKind, type SyncLock } from './local-sync-plan'
|
|
8
8
|
import { readOrgSettings, type OrgSettings } from './local-org-settings'
|
|
9
9
|
import type { Portability } from './local-portability'
|
|
10
|
-
import { readWorkspace, type SyncSource } from './local-sync-remote'
|
|
10
|
+
import { readWorkspace, type SyncSource, type WorkspaceCatalogModel } from './local-sync-remote'
|
|
11
11
|
import { REF_KINDS, type Directory, type RefKind, type Unresolved } from './local-sync-refs'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -86,6 +86,12 @@ export interface RemoteSnapshot {
|
|
|
86
86
|
* now written `action/resetLine`) from a change.
|
|
87
87
|
*/
|
|
88
88
|
names?: Partial<Record<RefKind, Record<string, string>>>
|
|
89
|
+
/**
|
|
90
|
+
* The organization's model catalog as of the last read, with each model's
|
|
91
|
+
* provider id, so a model this computer names under another slug is known
|
|
92
|
+
* as the workspace's (`local-sync-models.ts`).
|
|
93
|
+
*/
|
|
94
|
+
models?: WorkspaceCatalogModel[]
|
|
89
95
|
/**
|
|
90
96
|
* Read with an organization key as well: it brings governed Actions (and,
|
|
91
97
|
* for older platforms, the Blueprint). A snapshot read with other keys is
|
|
@@ -423,6 +429,7 @@ export async function fetchWorkspace(root: string, source: SyncSource, opts: Fet
|
|
|
423
429
|
warnings: read.warnings,
|
|
424
430
|
notes: read.notes,
|
|
425
431
|
names: namesOf(read.directory),
|
|
432
|
+
...(read.models.length ? { models: read.models } : {}),
|
|
426
433
|
organizationKey: !!source.organization,
|
|
427
434
|
reader: SNAPSHOT_READER,
|
|
428
435
|
...(settings ? { settings } : {}),
|
|
@@ -522,6 +529,7 @@ export async function fetchWorkspace(root: string, source: SyncSource, opts: Fet
|
|
|
522
529
|
warnings: read?.warnings ?? [],
|
|
523
530
|
notes: read ? read.notes : previous?.notes ?? [],
|
|
524
531
|
...(read ? { names: namesOf(read.directory) } : previous?.names ? { names: previous.names } : {}),
|
|
532
|
+
...(read?.models.length ? { models: read.models } : previous?.models ? { models: previous.models } : {}),
|
|
525
533
|
organizationKey: !!source.organization,
|
|
526
534
|
reader: SNAPSHOT_READER,
|
|
527
535
|
...(settings ? { settings } : previous?.settings ? { settings: previous.settings } : {}),
|