@frontera-sdk/cli 1.53.23 → 1.53.24
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/knowledge-base-api.ts +230 -0
- package/src/commands/knowledge/index-commands.ts +6 -6
- package/src/commands/knowledge-base/index-commands.ts +328 -0
- package/src/commands/local/check.ts +12 -0
- package/src/commands/local/dev.ts +64 -2
- package/src/commands/local/index-commands.ts +34 -10
- package/src/commands/local/init.ts +11 -2
- package/src/commands/local/schema.ts +1 -1
- package/src/commands/local/sync-request.ts +135 -29
- package/src/commands/local/sync.ts +40 -25
- package/src/commands/registry.ts +4 -1
- package/src/commands/types.ts +6 -0
- package/src/flag-help.ts +10 -4
- package/src/forge/touches.ts +29 -0
- package/src/local/local-applied.ts +28 -0
- package/src/local/local-build-summary.ts +1 -0
- package/src/local/local-component-health.ts +7 -2
- package/src/local/local-dev-references.ts +6 -2
- package/src/local/local-dev-sync.ts +43 -4
- package/src/local/local-docker.ts +16 -1
- package/src/local/local-env.ts +19 -0
- package/src/local/local-file-checks.ts +56 -8
- package/src/local/local-knowledge-base-apply.ts +714 -0
- package/src/local/local-knowledge-base-files.ts +195 -0
- package/src/local/local-knowledge-base-pull.ts +458 -0
- package/src/local/local-knowledge-base-sources-off.ts +67 -0
- package/src/local/local-person-steps.ts +2 -1
- package/src/local/local-personas.ts +64 -7
- package/src/local/local-portability.ts +27 -0
- package/src/local/local-ports.ts +4 -2
- package/src/local/local-project-secrets.ts +30 -5
- package/src/local/local-scaffold.ts +2 -0
- package/src/local/local-schema-check.ts +3 -1
- package/src/local/local-schemas.ts +11 -1
- package/src/local/local-search-index.ts +92 -0
- package/src/local/local-secrets.ts +7 -2
- package/src/local/local-seed.ts +17 -0
- package/src/local/local-state.ts +7 -5
- package/src/local/local-studio-server.ts +3 -3
- package/src/local/local-supervisor.ts +45 -8
- package/src/local/local-sync-link.ts +16 -0
- package/src/local/local-sync-names.ts +10 -1
- package/src/local/local-sync-plan.ts +44 -5
- package/src/local/local-sync-push.ts +165 -34
- package/src/local/local-sync-refs.ts +3 -1
- package/src/local/local-sync-remote.ts +44 -12
- package/src/local/local-sync-snapshot.ts +156 -12
- package/src/main.ts +13 -0
- package/src/mcp/mcp-server.ts +3 -0
- package/src/mcp/tools/docs-tools.ts +1 -1
- package/src/mcp/tools/knowledge-base-tools.ts +85 -0
|
@@ -3,8 +3,8 @@ import { basename, join } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import { CliError, UsageError } from '../../errors'
|
|
5
5
|
import { canPrompt, promptSecret } from '../../prompt'
|
|
6
|
-
import { allTargets } from '../../local/local-dev-sync'
|
|
7
6
|
import { pullDocuments, type DocumentsPulled } from '../../local/local-knowledge-documents'
|
|
7
|
+
import { KNOWLEDGE_SIGN_IN, lockedFolders, pullKnowledgeBaseDocuments } from '../../local/local-knowledge-base-pull'
|
|
8
8
|
import { appCodeLine, appCodeVersion, earlierPullShadows, pullAppCode, type AppCodePulled } from '../../local/local-app-code'
|
|
9
9
|
import { remakeLines, remakeReason, remakeSampleData, type SampleRemake } from '../../local/local-sample-remake'
|
|
10
10
|
import { lessonsToPropose, pullLessons, type LessonsPulled } from '../../local/local-lessons'
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
linkTarget,
|
|
19
19
|
namedTarget,
|
|
20
20
|
organizationReader,
|
|
21
|
+
personReader,
|
|
21
22
|
projectFile,
|
|
22
23
|
projectKeyFingerprints,
|
|
23
24
|
projectTargets,
|
|
@@ -93,6 +94,7 @@ import {
|
|
|
93
94
|
usesGovernedActions,
|
|
94
95
|
writeBase,
|
|
95
96
|
type ClearedConflicts,
|
|
97
|
+
type FetchOptions,
|
|
96
98
|
type FetchResult,
|
|
97
99
|
type RemoteSnapshot,
|
|
98
100
|
} from '../../local/local-sync-snapshot'
|
|
@@ -185,31 +187,29 @@ export async function sourceFor(owner: KeyOwner, link: SyncLink, name: string |
|
|
|
185
187
|
const key = getSyncKey(owner, 'workspace')
|
|
186
188
|
if (!key) throw new CliError(`no workspace key for ${name ? `target ${name}` : 'this project'} on this machine`, { code: 'SYNC_NO_KEY', hint: `run \`${targetWords({ name }, 'frontera local sync connect')}\` to store one (the link is in frontera.yaml; keys are per machine)` })
|
|
187
189
|
const { client, notSteward } = await organizationReader(owner, link)
|
|
190
|
+
// After it: a profile that did not know its organization yet is recorded there once.
|
|
191
|
+
const person = await personReader(owner, link)
|
|
188
192
|
return {
|
|
189
193
|
workspace: syncClient(link.platform, key),
|
|
190
194
|
...(client ? { organization: client } : {}),
|
|
191
195
|
workspaceId: link.workspace,
|
|
192
196
|
...(notSteward ? { notSteward } : {}),
|
|
197
|
+
...(person ? { person } : {}),
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
|
|
196
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Items the project holds that the workspace may not: for the plan's "only
|
|
203
|
+
* here", as a push lists them. An Agent's lessons are counted apart.
|
|
204
|
+
*/
|
|
197
205
|
function localItems(project: LocalProject): Array<{ ref: string; kind: SyncKind; name: string; files: string[] }> {
|
|
198
|
-
return
|
|
199
|
-
if (t.kind === 'agent') return [{ ref: `agent/${t.slug}`, kind: 'agent', name: t.slug, files: [t.file] }]
|
|
200
|
-
if (t.kind === 'workflow') return [{ ref: `workflow/${t.slug}`, kind: 'workflow', name: t.slug, files: [t.file] }]
|
|
201
|
-
if (t.kind === 'function') {
|
|
202
|
-
const name = basename(t.file).replace(/\.ts$/, '')
|
|
203
|
-
return [{ ref: `function/${name}`, kind: 'function', name, files: [t.file] }]
|
|
204
|
-
}
|
|
205
|
-
return []
|
|
206
|
-
})
|
|
206
|
+
return [...projectRefs(project.root)].flatMap(([ref, { kind, name, file }]) => (kind === 'lessons' ? [] : [{ ref, kind, name, files: [file] }]))
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function kindsFlag(ctx: CommandContext): Set<SyncKind> | undefined {
|
|
210
210
|
const raw = flagString(ctx, 'kinds')
|
|
211
211
|
if (!raw) return undefined
|
|
212
|
-
const aliases: Record<string, SyncKind> = { agents: 'agent', workflows: 'workflow', functions: 'function', skills: 'skill', plugins: 'plugin', evaluations: 'evaluation', evals: 'evaluation', datasets: 'dataset', themes: 'theme', 'chat-apps': 'chat-app', chatapps: 'chat-app', apps: 'app' }
|
|
212
|
+
const aliases: Record<string, SyncKind> = { 'knowledge-bases': 'knowledge-base', agents: 'agent', workflows: 'workflow', functions: 'function', skills: 'skill', plugins: 'plugin', evaluations: 'evaluation', evals: 'evaluation', datasets: 'dataset', themes: 'theme', 'chat-apps': 'chat-app', chatapps: 'chat-app', apps: 'app' }
|
|
213
213
|
const out = new Set<SyncKind>()
|
|
214
214
|
for (const part of raw.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
215
215
|
const kind = (aliases[part] ?? part) as SyncKind
|
|
@@ -261,9 +261,9 @@ export async function settleConflicts(project: LocalProject, link: SyncLink, sna
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/** Fetch into the snapshot with the project's keys, then bring conflicted items up to the newer version. */
|
|
264
|
-
export async function fetchFor(project: LocalProject, link: SyncLink, opts:
|
|
264
|
+
export async function fetchFor(project: LocalProject, link: SyncLink, opts: Pick<FetchOptions, 'full' | 'knowledgeBaseFolders'> = {}): Promise<FetchResult & { conflicts: ConflictsSettled }> {
|
|
265
265
|
const source = await sourceFor(project, link)
|
|
266
|
-
const result = await fetchWorkspace(project.root, source, { blueprint: link.blueprint, full: opts.full })
|
|
266
|
+
const result = await fetchWorkspace(project.root, source, { blueprint: link.blueprint, full: opts.full, knowledgeBaseFolders: opts.knowledgeBaseFolders })
|
|
267
267
|
await nameLink(project, link, source)
|
|
268
268
|
return { ...result, conflicts: await settleConflicts(project, link, result.snapshot, source) }
|
|
269
269
|
}
|
|
@@ -273,10 +273,10 @@ export async function fetchFor(project: LocalProject, link: SyncLink, opts: { fu
|
|
|
273
273
|
* target into its own snapshot (`target.state`) with its own keys. A named
|
|
274
274
|
* target is never pulled from, so it has no conflicts to settle.
|
|
275
275
|
*/
|
|
276
|
-
export async function fetchTarget(project: LocalProject, target: SyncTarget, opts:
|
|
276
|
+
export async function fetchTarget(project: LocalProject, target: SyncTarget, opts: Pick<FetchOptions, 'full' | 'knowledgeBaseFolders'> = {}): Promise<FetchResult> {
|
|
277
277
|
if (!target.name) return fetchFor(project, target.link, opts)
|
|
278
278
|
const source = await sourceFor(target.owner, target.link, target.name)
|
|
279
|
-
const result = await fetchWorkspace(target.state, source, { blueprint: target.link.blueprint, full: opts.full })
|
|
279
|
+
const result = await fetchWorkspace(target.state, source, { blueprint: target.link.blueprint, full: opts.full, knowledgeBaseFolders: opts.knowledgeBaseFolders })
|
|
280
280
|
await nameLink(project, target.link, source, (named) => writeTarget(project.root, target.name!, { ...named, ...(target.production ? { production: true } : {}) }))
|
|
281
281
|
return result
|
|
282
282
|
}
|
|
@@ -370,7 +370,7 @@ async function refreshLocalCatalog(project: LocalProject): Promise<void> {
|
|
|
370
370
|
if (models?.length) rememberLocalCatalog(project.root, models)
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
export async function planFor(project: LocalProject, link: SyncLink, opts: { kinds?: Set<SyncKind>; drafts?: Set<string>; fetch?: 'auto' | 'always' | 'never'; repair?: 'offline' | 'online' } = {}) {
|
|
373
|
+
export async function planFor(project: LocalProject, link: SyncLink, opts: { kinds?: Set<SyncKind>; drafts?: Set<string>; fetch?: 'auto' | 'always' | 'never'; repair?: 'offline' | 'online'; full?: boolean } = {}) {
|
|
374
374
|
await refreshLocalCatalog(project)
|
|
375
375
|
const local = localItems(project).filter((l) => !opts.kinds || opts.kinds.has(l.kind))
|
|
376
376
|
const readFromBase = (path: string) => readBase(project.root, path)
|
|
@@ -378,14 +378,14 @@ export async function planFor(project: LocalProject, link: SyncLink, opts: { kin
|
|
|
378
378
|
if (opts.drafts?.size) {
|
|
379
379
|
const lock = readLock(project.root)
|
|
380
380
|
const base = lock?.workspace === link.workspace ? lock : null
|
|
381
|
-
const remote = await readWorkspace(await sourceFor(project, link), { kinds: opts.kinds, drafts: opts.drafts, blueprint: link.blueprint })
|
|
381
|
+
const remote = await readWorkspace(await sourceFor(project, link), { kinds: opts.kinds, drafts: opts.drafts, blueprint: link.blueprint, knowledgeBaseFolders: lockedFolders(project.root) })
|
|
382
382
|
return { remote, plan: computePlan(project.root, remote.items, base, local, { base: readFromBase, directory: remote.directory }), lock, snapshot: null, fetched: null, bases: noBases }
|
|
383
383
|
}
|
|
384
384
|
let snapshot = snapshotFor(project, link)
|
|
385
385
|
let fetched: Awaited<ReturnType<typeof fetchFor>> | null = null
|
|
386
386
|
const stale = !snapshot || Date.now() - Date.parse(snapshot.fetchedAt) > FRESH_MS
|
|
387
387
|
if (opts.fetch === 'always' || (opts.fetch !== 'never' && stale)) {
|
|
388
|
-
fetched = await fetchFor(project, link)
|
|
388
|
+
fetched = await fetchFor(project, link, { full: opts.full })
|
|
389
389
|
snapshot = fetched.snapshot
|
|
390
390
|
}
|
|
391
391
|
// Read after the fetch: settling conflicts may have moved it.
|
|
@@ -788,11 +788,14 @@ const pull = async (ctx: CommandContext) => {
|
|
|
788
788
|
const byRef = new Map(remote.items.map((i) => [i.ref, i]))
|
|
789
789
|
// Knowledge bases whose documents come too: every base this pull writes (a pulled Agent answers from them),
|
|
790
790
|
// unless --no-documents; --documents names more, already in the project, to bring theirs again.
|
|
791
|
+
// A Knowledge app base (`knowledge-base/<folder>`) brings its documents only when named.
|
|
791
792
|
const noDocuments = flagBool(ctx, 'no-documents')
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
if (
|
|
793
|
+
const asked = refsFlag(ctx, 'documents')
|
|
794
|
+
const baseDocuments = asked.filter((r) => r.startsWith('knowledge-base/'))
|
|
795
|
+
const documents = asked.filter((r) => !r.startsWith('knowledge-base/')).map((r) => r.replace(/^knowledge\//, ''))
|
|
796
|
+
if (noDocuments && asked.length) throw new UsageError('--documents and --no-documents ask opposite things', 'leave one out: --no-documents pulls no documents; --documents <name> pulls those bases\' documents')
|
|
797
|
+
const noBase = [...documents.filter((n) => !byRef.has(`knowledge/${n}`)), ...baseDocuments.filter((r) => !byRef.has(r) || r === 'knowledge-base/settings')]
|
|
798
|
+
if (noBase.length) throw new UsageError(`no knowledge base ${noBase.join(', ')} in the workspace`, '--documents takes knowledge base names, as `--preview` lists them (knowledge/<name>, or knowledge-base/<name> for the Knowledge app)')
|
|
796
799
|
const byPlan = new Map(plan.map((p) => [p.ref, p]))
|
|
797
800
|
const status = new Map(plan.map((p) => [p.ref, p.status]))
|
|
798
801
|
const clean = (p: PlanItem) => p.status === 'changed' || (p.status === 'conflict' && p.merge?.conflicts === 0)
|
|
@@ -813,13 +816,14 @@ const pull = async (ctx: CommandContext) => {
|
|
|
813
816
|
} else if (kinds) {
|
|
814
817
|
// A kind pulls what is new, changed or merges cleanly; it never replaces local edits unasked.
|
|
815
818
|
chosen = plan.filter((p) => p.status === 'new' || clean(p)).map((p) => p.ref)
|
|
816
|
-
} else if (deleteHere.length || keepLocal.length || documents.length || flagBool(ctx, 'lessons')) {
|
|
819
|
+
} else if (deleteHere.length || keepLocal.length || documents.length || baseDocuments.length || flagBool(ctx, 'lessons')) {
|
|
817
820
|
chosen = []
|
|
818
821
|
} else {
|
|
819
822
|
throw new UsageError('say what to pull', 'name items (`agent/support-concierge`), kinds (`--kinds workflows`), or `--all-changed`; `--preview` lists them')
|
|
820
823
|
}
|
|
821
824
|
// A knowledge base not in the project yet comes with its documents.
|
|
822
825
|
for (const n of documents) if (status.get(`knowledge/${n}`) === 'new' && !chosen.includes(`knowledge/${n}`)) chosen.push(`knowledge/${n}`)
|
|
826
|
+
for (const r of baseDocuments) if (status.get(r) === 'new' && !chosen.includes(r)) chosen.push(r)
|
|
823
827
|
// An item pulled with conflicts waits for `resolve`: a pull writes nothing else for it.
|
|
824
828
|
const waiting = chosen.filter((r) => status.get(r) === 'unresolved')
|
|
825
829
|
chosen = chosen.filter((r) => status.get(r) !== 'unresolved')
|
|
@@ -869,6 +873,15 @@ const pull = async (ctx: CommandContext) => {
|
|
|
869
873
|
docs[n] = id ? await pullDocuments(project.root, client, { name: n, id }) : { written: [], present: [], skipped: [], failed: ['its id is not in the fetched snapshot: fetch again'] }
|
|
870
874
|
}
|
|
871
875
|
}
|
|
876
|
+
if (baseDocuments.length) {
|
|
877
|
+
// Read as the person signed in: a key never reads the Knowledge app.
|
|
878
|
+
const person = await personReader(project, link)
|
|
879
|
+
for (const r of baseDocuments) {
|
|
880
|
+
const id = byRef.get(r)?.id
|
|
881
|
+
const folder = r.slice('knowledge-base/'.length)
|
|
882
|
+
docs[r] = !person ? { written: [], present: [], skipped: [], failed: [KNOWLEDGE_SIGN_IN] } : id ? await pullKnowledgeBaseDocuments(project.root, person.workspace, { folder, id }) : { written: [], present: [], skipped: [], failed: ['its id is not in the fetched snapshot: fetch again'] }
|
|
883
|
+
}
|
|
884
|
+
}
|
|
872
885
|
// Each pulled Custom App's code comes too, into apps/<slug>/, unless --no-app-code; a folder already there is kept,
|
|
873
886
|
// except one an earlier pull left unchanged whose package.json and bun.lock disagree: the deployed source replaces it.
|
|
874
887
|
const appCode: AppCodePulled[] = []
|
|
@@ -930,7 +943,7 @@ const pull = async (ctx: CommandContext) => {
|
|
|
930
943
|
...lessons.removed.map((a) => ` ${a}: no reviewed rules in the workspace any more; agents/${a}/lessons.yaml removed`),
|
|
931
944
|
]
|
|
932
945
|
const docLines = Object.entries(docs).flatMap(([n, d]) => [
|
|
933
|
-
` ${n}: ${plural(d.written.length, 'document')} written${d.present.length ? `, ${d.present.length} already here` : ''}${d.skipped.length ? `, ${d.skipped.length} not a kind local dev uploads (${d.skipped.join(', ')})` : ''}`,
|
|
946
|
+
` ${n}: ${plural(d.written.length, 'document')} written${d.present.length ? `, ${d.present.length} already here` : ''}${d.skipped.length ? `, ${d.skipped.length} ${n.startsWith('knowledge-base/') ? 'kept on the platform by their source' : 'not a kind local dev uploads'} (${d.skipped.join(', ')})` : ''}`,
|
|
934
947
|
...d.failed.map((f) => ` could not read ${f}`),
|
|
935
948
|
])
|
|
936
949
|
// What won't run locally as pulled, with the reason and whose fix it is: the checks `local dev` applies.
|
|
@@ -1310,6 +1323,8 @@ export const localSync: Command = {
|
|
|
1310
1323
|
'frontera local sync pull knowledge/faq',
|
|
1311
1324
|
'frontera local sync pull --kinds knowledge --no-documents',
|
|
1312
1325
|
'frontera local sync pull --documents faq',
|
|
1326
|
+
'frontera local sync pull --kinds knowledge-bases',
|
|
1327
|
+
'frontera local sync pull --documents knowledge-base/hr-policies',
|
|
1313
1328
|
'frontera local sync pull app/ops-console --no-app-code',
|
|
1314
1329
|
'frontera local sync pull agent/support-concierge --lessons',
|
|
1315
1330
|
'frontera local sync diff agent/support-concierge',
|
package/src/commands/registry.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { capabilityCommands } from './capability/index-commands'
|
|
|
37
37
|
import { datasetCommands } from './dataset/index-commands'
|
|
38
38
|
import { sourceCommands } from './source/index-commands'
|
|
39
39
|
import { knowledgeCommands } from './knowledge/index-commands'
|
|
40
|
+
import { knowledgeBaseCommands } from './knowledge-base/index-commands'
|
|
40
41
|
import { mediaCommands } from './media/index-commands'
|
|
41
42
|
import { packCommands } from './pack/index-commands'
|
|
42
43
|
import { secretCommands } from './secret/index-commands'
|
|
@@ -133,6 +134,7 @@ export const COMMANDS: readonly Command[] = [
|
|
|
133
134
|
...datasetCommands,
|
|
134
135
|
...sourceCommands,
|
|
135
136
|
...knowledgeCommands,
|
|
137
|
+
...knowledgeBaseCommands,
|
|
136
138
|
...mediaCommands,
|
|
137
139
|
...packCommands,
|
|
138
140
|
...secretCommands,
|
|
@@ -212,7 +214,8 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
212
214
|
plugin: 'Integrations and MCP servers connected to this workspace',
|
|
213
215
|
dataset: 'Source datasets a Blueprint object type can bind to',
|
|
214
216
|
source: 'Connected Sources — where a dataset’s rows come from',
|
|
215
|
-
knowledge: '
|
|
217
|
+
knowledge: 'Agent knowledge — the bases Agents attach, and their files',
|
|
218
|
+
'knowledge-base': 'Knowledge app bases — search, documents, stewards',
|
|
216
219
|
pack: 'Reusable skill bundles — author once, install per workspace',
|
|
217
220
|
secret: 'Workspace secrets — named here, never printed back',
|
|
218
221
|
voice: 'Voice — Agent phone numbers and outbound calls',
|
package/src/commands/types.ts
CHANGED
|
@@ -86,6 +86,12 @@ export interface CommandMeta {
|
|
|
86
86
|
* is refused, so a noun names its way out without rewording the refusal.
|
|
87
87
|
*/
|
|
88
88
|
sessionAlternative?: string
|
|
89
|
+
/**
|
|
90
|
+
* Session-lane only: on the project's own local runtime, with no `--as`,
|
|
91
|
+
* the command acts as the runtime's owner, whose session the runtime keeps
|
|
92
|
+
* (Studio acts as the same person). Elsewhere a person signs in as usual.
|
|
93
|
+
*/
|
|
94
|
+
localOwner?: boolean
|
|
89
95
|
/**
|
|
90
96
|
* Present → the command is planned but unbuilt, and fails with this message.
|
|
91
97
|
* Reserved in the table rather than omitted: a caller reading "unknown
|
package/src/flag-help.ts
CHANGED
|
@@ -101,7 +101,8 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
101
101
|
// Local runtime (internal FDE tooling)
|
|
102
102
|
source: 'platform checkout to run the local runtime from (default: the one this CLI runs from)',
|
|
103
103
|
image: 'run the local runtime from release images instead of a platform checkout',
|
|
104
|
-
exclude: 'optional components to leave out of the local runtime: jobs, runner, storage',
|
|
104
|
+
exclude: 'optional components to leave out of the local runtime: jobs, runner, storage, search-index',
|
|
105
|
+
with: 'components the local runtime runs only when asked: search-index, which a project with a knowledge-base/ folder runs anyway',
|
|
105
106
|
'no-backup': 'also delete the local database and secrets',
|
|
106
107
|
follow: 'keep printing new log lines as they are written',
|
|
107
108
|
lines: 'how many of the latest log lines to print (default 100)',
|
|
@@ -199,8 +200,11 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
199
200
|
limit: 'maximum rows to return in one page; each surface has its own ceiling',
|
|
200
201
|
'page-token': 'cursor from the previous response’s nextPageToken; omit for the first page',
|
|
201
202
|
as:
|
|
202
|
-
'on the local runtime only: act as a local persona (`submitter`,
|
|
203
|
-
+ '`dispatch_lead
|
|
203
|
+
'on the local runtime only: act as a local persona (`submitter`, an approval step such as '
|
|
204
|
+
+ '`dispatch_lead`, or `knowledge-steward`, `workspace-member`, `outsider` for knowledge bases), '
|
|
205
|
+
+ 'as `frontera local status` lists them; refused anywhere else',
|
|
206
|
+
base: 'a knowledge base by name, id or project folder',
|
|
207
|
+
'credential-from': 'where to read a credential from: `-` for stdin, or a file path. Never an inline value',
|
|
204
208
|
// A metric's query (`blueprint metric query`).
|
|
205
209
|
measure: 'comma-separated measures of the metric to return; all of them when absent',
|
|
206
210
|
'group-by': 'comma-separated dimensions of the metric to break the numbers down by',
|
|
@@ -293,7 +297,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
293
297
|
title: 'the change request’s title; a push that opens a new one needs it',
|
|
294
298
|
why: 'a person-step: why it is needed now, in a sentence',
|
|
295
299
|
changes: 'a person-step: what it changes, in the workspace’s words',
|
|
296
|
-
credential: 'a person-step: who or what it needs (workspace-key, organization-key, steward, admin, second-person, person, registry-owner)',
|
|
300
|
+
credential: 'a person-step: who or what it needs (workspace-key, organization-key, steward, knowledge-steward, admin, second-person, person, registry-owner)',
|
|
297
301
|
delivery: 'a person-step: the delivery it belongs to; steps with the same one show as one card',
|
|
298
302
|
all: 'list the settled ones too',
|
|
299
303
|
browser: 'sign in by approving a code in the browser, as yourself',
|
|
@@ -385,6 +389,8 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
385
389
|
end: 'iso',
|
|
386
390
|
only: 'live|test',
|
|
387
391
|
as: 'persona',
|
|
392
|
+
base: 'name',
|
|
393
|
+
'credential-from': '-|path',
|
|
388
394
|
cursor: 'cursor',
|
|
389
395
|
lifecycle: 'state',
|
|
390
396
|
actions: 'allow_all|read_only|custom',
|
package/src/forge/touches.ts
CHANGED
|
@@ -35,6 +35,7 @@ export const FORGE_ARTIFACT_KINDS = [
|
|
|
35
35
|
'source',
|
|
36
36
|
'dataset',
|
|
37
37
|
'knowledge',
|
|
38
|
+
'knowledge-base',
|
|
38
39
|
'workflow',
|
|
39
40
|
] as const satisfies readonly ForgeArtifactKind[]
|
|
40
41
|
|
|
@@ -55,6 +56,8 @@ export const TOUCHING_VERBS: Record<ForgeArtifactKind, readonly string[]> = {
|
|
|
55
56
|
// `revise` cuts the dataset's next revision from its CSV.
|
|
56
57
|
dataset: ['create', 'upload', 'revise', 'test-source'],
|
|
57
58
|
knowledge: ['create', 'attach', 'detach', 'upload', 'retry', 'delete'],
|
|
59
|
+
// The Knowledge app's bases, recorded through `ACTION_VERBS`: `source sync` and `steward add|remove`.
|
|
60
|
+
'knowledge-base': ['sync', 'steward'],
|
|
58
61
|
// `webhook-test` starts a run on the draft through its webhook, as `run` does.
|
|
59
62
|
// `get`, `lineage`, `list`, `runs` and `validate` are reads — `validate` is
|
|
60
63
|
// `push --dry-run` and writes nothing. `cancel` is deliberately absent: it
|
|
@@ -93,6 +96,20 @@ export const AGENT_PROXY_NOUNS: Record<string, { verbs: readonly string[]; agent
|
|
|
93
96
|
voice: { verbs: ['number-add', 'number-apply', 'number-enable', 'number-disable', 'number-hours', 'number-remove'], agentAt: 0 },
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Verbs whose first positional is an action, only some of which change the
|
|
101
|
+
* artifact: `knowledge-base source test` stores nothing, `source sync` makes
|
|
102
|
+
* the source due now. The action names the verb the touch records, and
|
|
103
|
+
* `refAt` the positional after the action that names the artifact (null: a
|
|
104
|
+
* steward is a person, never a ref).
|
|
105
|
+
*/
|
|
106
|
+
export const ACTION_VERBS: Partial<Record<ForgeArtifactKind, Record<string, { actions: Record<string, string>; refAt: number | null }>>> = {
|
|
107
|
+
'knowledge-base': {
|
|
108
|
+
source: { actions: { sync: 'sync' }, refAt: 0 },
|
|
109
|
+
steward: { actions: { add: 'steward', remove: 'steward' }, refAt: null },
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
/**
|
|
97
114
|
* Verbs whose positional is a FILE, not the artifact's name. `frontera
|
|
98
115
|
* function deploy src/nightly.ts` names a source file; the slug comes from
|
|
@@ -180,6 +197,9 @@ export const MCP_READ_TOOLS = new Set([
|
|
|
180
197
|
'workflow_test_step',
|
|
181
198
|
])
|
|
182
199
|
|
|
200
|
+
/** `frontera mcp`'s knowledge base tools that change nothing; they are not in the service's catalogue. */
|
|
201
|
+
export const KNOWLEDGE_BASE_READ_TOOLS = new Set(['knowledge_base_list', 'knowledge_base_search', 'knowledge_base_status'])
|
|
202
|
+
|
|
183
203
|
/** Tools whose success makes the artifact LIVE. */
|
|
184
204
|
export const MCP_LIVE_TOOLS = new Set([
|
|
185
205
|
'publish_agent', 'promote_platform_app_version', 'workflow_promote_version', 'promote_function_version',
|
|
@@ -219,6 +239,8 @@ function mcpArtifact(tool: string, input: Record<string, unknown>): { kind: Forg
|
|
|
219
239
|
if (tool === 'data_refresh_dataset') return { kind: 'dataset', ref: str('apiName') }
|
|
220
240
|
if (tool === 'promote_platform_app_version') return { kind: 'app', ref: str('app') ?? str('slug') }
|
|
221
241
|
if (/_skill$/.test(tool)) return { kind: 'skill', ref: str('slug') ?? str('skillId') }
|
|
242
|
+
// The Knowledge app's bases, from `frontera mcp`, ahead of Agent knowledge, whose tools also say "knowledge".
|
|
243
|
+
if (tool.startsWith('knowledge_base_')) return KNOWLEDGE_BASE_READ_TOOLS.has(tool) ? null : { kind: 'knowledge-base', ref: str('base') }
|
|
222
244
|
if (/knowledge/.test(tool)) return { kind: 'knowledge', ref: str('knowledgeBaseId') ?? str('name') }
|
|
223
245
|
if (/^create_agent$|^duplicate_agent$/.test(tool)) return { kind: 'agent', ref: null }
|
|
224
246
|
return null
|
|
@@ -398,6 +420,13 @@ export function parseFronteraCommands(line: string): ParsedFronteraCommand[] {
|
|
|
398
420
|
|
|
399
421
|
const kind = NOUN_ALIASES[noun] ?? (isKind(noun) ? noun : null)
|
|
400
422
|
if (!kind) continue
|
|
423
|
+
const acting = ACTION_VERBS[kind]?.[verb]
|
|
424
|
+
if (acting) {
|
|
425
|
+
const [action, ...after] = rest
|
|
426
|
+
const touch = action ? acting.actions[action] : undefined
|
|
427
|
+
if (touch) out.push({ kind, verb: touch, ref: acting.refAt === null ? null : (after[acting.refAt] ?? null), live: false })
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
401
430
|
if (!TOUCHING_VERBS[kind].includes(verb)) continue
|
|
402
431
|
const ref = (FILE_POSITIONAL_VERBS[kind] ?? []).includes(verb)
|
|
403
432
|
? null
|
|
@@ -8,6 +8,8 @@ import type { LocalProject } from './local-project'
|
|
|
8
8
|
import { standInTools } from './local-stand-ins'
|
|
9
9
|
import { importedFiles } from './local-function-shared'
|
|
10
10
|
import { NOT_A_DIFFERENCE_HERE } from './local-org-settings'
|
|
11
|
+
import { KNOWLEDGE_BASE_DIR, KNOWLEDGE_BASE_SETTINGS_FILE } from './local-knowledge-base-files'
|
|
12
|
+
import { agentPicks, appliedBaseIds, pickedFolder } from './local-knowledge-base-apply'
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* What `frontera local dev` has applied to this runtime, by content hash. It
|
|
@@ -71,11 +73,25 @@ function treeHash(dir: string): string | null {
|
|
|
71
73
|
return any ? hash.digest('hex') : null
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/** An agent.json's `knowledgeBases`, or null when it has none or cannot be read. */
|
|
77
|
+
function agentPicksIn(path: string): string[] | null {
|
|
78
|
+
try {
|
|
79
|
+
return agentPicks(JSON.parse(readFileSync(path, 'utf8')))
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
export function contentHash(project: LocalProject, target: SyncTarget): string | null {
|
|
75
86
|
// The Blueprint is applied as one tree, so it changes when any file in it does.
|
|
76
87
|
if (target.kind === 'blueprint') return treeHash(join(project.root, 'blueprint'))
|
|
77
88
|
// A knowledge base, a skill and a slide theme are folders: a new document, reference or image changes them.
|
|
78
89
|
if (target.kind === 'knowledge' || target.kind === 'skill' || target.kind === 'theme' || target.kind === 'brand') return treeHash(join(project.root, dirname(target.file)))
|
|
90
|
+
// A Knowledge app base, and the settings that allow its sources: allowing them applies it again.
|
|
91
|
+
if (target.kind === 'knowledge-base') {
|
|
92
|
+
const tree = treeHash(join(project.root, dirname(target.file)))
|
|
93
|
+
return tree && createHash('sha256').update(tree).update(fileDigest(join(project.root, KNOWLEDGE_BASE_DIR, KNOWLEDGE_BASE_SETTINGS_FILE)) ?? '').digest('hex')
|
|
94
|
+
}
|
|
79
95
|
// A stand-in lists the connection's tools and those the project's files name on it: either changing re-applies it.
|
|
80
96
|
if (target.kind === 'plugin' && target.file.endsWith('.stand-in.json')) return createHash('sha256').update(JSON.stringify(standInTools(project.root, target.name))).update(fileDigest(join(project.root, target.file)) ?? '').digest('hex')
|
|
81
97
|
const own = fileDigest(join(project.root, target.file))
|
|
@@ -93,6 +109,14 @@ export function contentHash(project: LocalProject, target: SyncTarget): string |
|
|
|
93
109
|
hash.update(treeHash(join(project.root, 'functions')) ?? '').update(treeHash(join(project.root, 'vendor')) ?? '')
|
|
94
110
|
for (const path of importedFiles(project.root, target.file).filter((p) => !p.startsWith('functions/')).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) hash.update(path).update('\0').update(fileDigest(join(project.root, path)) ?? '')
|
|
95
111
|
}
|
|
112
|
+
// The bases it picks, by their ids here: a base made again (after a reset) sets its picks again.
|
|
113
|
+
if (target.kind === 'agent') {
|
|
114
|
+
const picks = agentPicksIn(join(project.root, target.file))
|
|
115
|
+
if (picks) {
|
|
116
|
+
const ids = appliedBaseIds(project.stateDir)
|
|
117
|
+
hash.update(JSON.stringify(picks.map((p) => ids[pickedFolder(p) ?? ''] ?? null)))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
96
120
|
// Context and data the pull could not read is assumed on the project's object types: a new one changes it.
|
|
97
121
|
if (target.kind === 'bindings' && /"blueprintUnread"\s*:\s*true/.test(readFileSync(join(project.root, target.file), 'utf8'))) {
|
|
98
122
|
hash.update(treeHash(join(project.root, 'blueprint', 'object-types')) ?? '')
|
|
@@ -389,6 +413,10 @@ export function refOfTarget(target: SyncTarget): string {
|
|
|
389
413
|
return `dataset/${target.apiName}`
|
|
390
414
|
case 'knowledge':
|
|
391
415
|
return `knowledge/${target.name}`
|
|
416
|
+
case 'knowledge-base':
|
|
417
|
+
return `${KNOWLEDGE_BASE_DIR}/${target.name}`
|
|
418
|
+
case 'knowledge-base-settings':
|
|
419
|
+
return 'knowledge-base-settings'
|
|
392
420
|
case 'skill':
|
|
393
421
|
return `skill/${target.name}`
|
|
394
422
|
case 'plugin':
|
|
@@ -474,6 +474,7 @@ export function itemKindsText(rows: BuildRow[]): string {
|
|
|
474
474
|
sharedField: ['shared field', 'shared fields'], objectSet: ['saved object set', 'saved object sets'], action: ['governed Action', 'governed Actions'],
|
|
475
475
|
knowledge: ['knowledge base', 'knowledge bases'], skill: ['skill', 'skills'], plugin: ['Plugin', 'Plugins'], theme: ['slide theme', 'slide themes'],
|
|
476
476
|
'chat-app': ['Chat App', 'Chat Apps'], app: ['Custom App', 'Custom Apps'], 'mcp-server': ['MCP server', 'MCP servers'],
|
|
477
|
+
'knowledge-base': ['knowledge base', 'knowledge bases'], 'knowledge-base-settings': ['Knowledge settings', 'Knowledge settings'],
|
|
477
478
|
}
|
|
478
479
|
const counts = new Map<string, number>()
|
|
479
480
|
for (const r of rows) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { connect } from 'node:net'
|
|
2
2
|
|
|
3
3
|
import { containerState } from './local-docker'
|
|
4
|
-
import {
|
|
4
|
+
import { searchIndexHealth } from './local-search-index'
|
|
5
|
+
import { COMPONENTS, isAlive, type ComponentName, type ComponentState, type LocalState } from './local-state'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Whether each part of a running local runtime is up now, not what the state
|
|
@@ -15,6 +16,8 @@ export interface ComponentProbe {
|
|
|
15
16
|
container: (name: string) => ReturnType<typeof containerState>
|
|
16
17
|
port: (port: number) => Promise<boolean>
|
|
17
18
|
alive: (pid: number) => boolean
|
|
19
|
+
/** The search index's cluster health: null when usable, else why not. Without it, its port decides. */
|
|
20
|
+
searchIndex?: (port: number) => Promise<string | null>
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** A part of the runtime that is down, and why, in words. `cache` is the service's Redis, not a listed component. */
|
|
@@ -49,6 +52,7 @@ const defaultProbe: ComponentProbe = {
|
|
|
49
52
|
},
|
|
50
53
|
port: (port) => portAnswers(port),
|
|
51
54
|
alive: isAlive,
|
|
55
|
+
searchIndex: (port) => searchIndexHealth(port),
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const containerOf = (state: LocalState, c: ComponentState) => c.container ?? `frontera-${state.projectId}-${c.name}`
|
|
@@ -63,6 +67,7 @@ async function whyDown(state: LocalState, c: ComponentState, probe: ComponentPro
|
|
|
63
67
|
} else if (!state.supervisorPid || !probe.alive(state.supervisorPid)) {
|
|
64
68
|
return 'the supervisor exited'
|
|
65
69
|
}
|
|
70
|
+
if (c.name === 'search-index' && c.port && probe.searchIndex) return probe.searchIndex(c.port)
|
|
66
71
|
if (c.port && !(await probe.port(c.port))) return `nothing answers on its port ${c.port}`
|
|
67
72
|
return null
|
|
68
73
|
}
|
|
@@ -108,7 +113,7 @@ export async function downToRestart(state: LocalState, probe: ComponentProbe = d
|
|
|
108
113
|
return down.length ? down : null
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
const ORDER: Array<DownComponent['name']>
|
|
116
|
+
const ORDER = COMPONENTS.flatMap((name): Array<DownComponent['name']> => (name === 'postgres' ? [name, 'cache'] : [name]))
|
|
112
117
|
|
|
113
118
|
/** What brings a down component back, said on its line. */
|
|
114
119
|
export const BRING_BACK = '`frontera local start` brings it back (the data is kept)'
|
|
@@ -344,10 +344,14 @@ export async function resolveTarget(
|
|
|
344
344
|
} catch (err) {
|
|
345
345
|
return { ok: false, output: `error [USAGE]: ${target.file} could not be read: ${(err as Error).message}` }
|
|
346
346
|
}
|
|
347
|
+
// The Agent's Knowledge app bases are set after it applies (`applyAgentKnowledgeBases`), outside its draft,
|
|
348
|
+
// whose format has no such key: the copy `agent apply` sends goes without it.
|
|
349
|
+
const picks = target.kind === 'agent' && !!doc && typeof doc === 'object' && 'knowledgeBases' in doc
|
|
350
|
+
if (picks) delete (doc as Record<string, unknown>).knowledgeBases
|
|
347
351
|
const modelSlugs = target.kind === 'agent' && (Array.isArray((doc as { modelConfigs?: unknown[] })?.modelConfigs) || typeof (doc as { config?: { voice?: { voiceSlug?: unknown } } })?.config?.voice?.voiceSlug === 'string')
|
|
348
352
|
const mcpServers = target.kind === 'agent' && Array.isArray((doc as { mcpServers?: unknown[] })?.mcpServers) && (doc as { mcpServers: unknown[] }).mcpServers.length > 0
|
|
349
353
|
const objectTools = target.kind === 'agent' && Array.isArray((doc as { objectBindings?: unknown[] })?.objectBindings) && (doc as { objectBindings: unknown[] }).objectBindings.length > 0
|
|
350
|
-
if (!kindsIn(doc).size && !modelSlugs && !mcpServers && !objectTools && !governedActionIds(doc).length && !(target.kind === 'workflow' && workflowNeedsLocalizing(doc))) return { ok: true, output: '', doc }
|
|
354
|
+
if (!picks && !kindsIn(doc).size && !modelSlugs && !mcpServers && !objectTools && !governedActionIds(doc).length && !(target.kind === 'workflow' && workflowNeedsLocalizing(doc))) return { ok: true, output: '', doc }
|
|
351
355
|
const dir = await directory()
|
|
352
356
|
const warnings: string[] = []
|
|
353
357
|
// Built-in connections the project has stand-ins for (`local-stand-ins.ts`).
|
|
@@ -432,7 +436,7 @@ export async function resolveTarget(
|
|
|
432
436
|
}
|
|
433
437
|
}
|
|
434
438
|
if (target.kind === 'operating') return { ok: true, doc: resolved, output: warnings.join('\n') }
|
|
435
|
-
if (!kindsIn(doc).size && !warnings.length && JSON.stringify(resolved) === JSON.stringify(readDoc(path))) return { ok: true, output: '', doc }
|
|
439
|
+
if (!picks && !kindsIn(doc).size && !warnings.length && JSON.stringify(resolved) === JSON.stringify(readDoc(path))) return { ok: true, output: '', doc }
|
|
436
440
|
const copy = join('.frontera', 'local', 'resolved', target.file)
|
|
437
441
|
mkdirSync(dirname(join(project.root, copy)), { recursive: true })
|
|
438
442
|
writeFileSync(join(project.root, copy), /\.json$/.test(target.file) ? `${JSON.stringify(resolved, null, 2)}\n` : stringifyYaml(resolved, { lineWidth: 0 }))
|
|
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { agentIdentity, createFlags } from './local-agent-name'
|
|
5
|
+
import { KNOWLEDGE_BASE_DIR, KNOWLEDGE_BASE_DOCUMENTS_DIR, KNOWLEDGE_BASE_FILE, KNOWLEDGE_BASE_SETTINGS_FILE, KNOWLEDGE_BASE_SOURCES_DIR } from './local-knowledge-base-files'
|
|
5
6
|
import { widerGrants, type WorkspaceGrant } from './local-workspace-grants'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -20,6 +21,8 @@ import { widerGrants, type WorkspaceGrant } from './local-workspace-grants'
|
|
|
20
21
|
* functions/<name>/ (with package.json) → function deploy <its entry> (a Function deployed from its own folder)
|
|
21
22
|
* blueprint/** → blueprint apply --yes, then publish and grant (organization profile)
|
|
22
23
|
* knowledge/<name>/knowledge.json → knowledge create, then upload the folder's new documents
|
|
24
|
+
* knowledge-base/<name>/ → the Knowledge app's base, its sources and documents (local-knowledge-base-apply.ts)
|
|
25
|
+
* knowledge-base/settings.json → the organization's Knowledge settings
|
|
23
26
|
* skills/<name>/SKILL.md → skill push skills/<name>
|
|
24
27
|
* mcp/<name>.plugin.json → plugin add mcp (or configure), tools enabled
|
|
25
28
|
* mcp/<name>.stand-in.json → a stand-in Plugin for a built-in connection: its tools answer “not connected”
|
|
@@ -49,6 +52,9 @@ export type SyncTarget =
|
|
|
49
52
|
| { kind: 'function'; key: string; file: string }
|
|
50
53
|
| { kind: 'blueprint'; key: string }
|
|
51
54
|
| { kind: 'knowledge'; key: string; name: string; file: string }
|
|
55
|
+
/** A base in the Knowledge app: `file` is its knowledge-base.json; its sources and documents are in its folder. */
|
|
56
|
+
| { kind: 'knowledge-base'; key: string; name: string; file: string }
|
|
57
|
+
| { kind: 'knowledge-base-settings'; key: string; file: string }
|
|
52
58
|
| { kind: 'skill'; key: string; name: string; file: string }
|
|
53
59
|
| { kind: 'plugin'; key: string; name: string; file: string }
|
|
54
60
|
| { kind: 'theme'; key: string; name: string; file: string }
|
|
@@ -112,6 +118,13 @@ export function targetFor(root: string, absolute: string): SyncTarget | null {
|
|
|
112
118
|
if (top === 'knowledge' && parts.length >= 3 && second && existsSync(join(root, 'knowledge', second, 'knowledge.json'))) {
|
|
113
119
|
return { kind: 'knowledge', key: `knowledge:${second}`, name: second, file: join('knowledge', second, 'knowledge.json') }
|
|
114
120
|
}
|
|
121
|
+
if (top === KNOWLEDGE_BASE_DIR && parts.length === 2 && second === KNOWLEDGE_BASE_SETTINGS_FILE) {
|
|
122
|
+
return { kind: 'knowledge-base-settings', key: 'knowledge-base-settings', file: rel }
|
|
123
|
+
}
|
|
124
|
+
// A Knowledge app base is its file, its sources and its documents.
|
|
125
|
+
if (top === KNOWLEDGE_BASE_DIR && second && existsSync(join(root, KNOWLEDGE_BASE_DIR, second, KNOWLEDGE_BASE_FILE)) && isKnowledgeBaseFile(parts.slice(2))) {
|
|
126
|
+
return { kind: 'knowledge-base', key: `knowledge-base:${second}`, name: second, file: join(KNOWLEDGE_BASE_DIR, second, KNOWLEDGE_BASE_FILE) }
|
|
127
|
+
}
|
|
115
128
|
if (top === 'skills' && parts.length >= 3 && second && existsSync(join(root, 'skills', second, 'SKILL.md'))) {
|
|
116
129
|
return { kind: 'skill', key: `skill:${second}`, name: second, file: join('skills', second, 'SKILL.md') }
|
|
117
130
|
}
|
|
@@ -154,6 +167,14 @@ export function targetFor(root: string, absolute: string): SyncTarget | null {
|
|
|
154
167
|
return null
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
/** The files of a base's folder `local dev` applies: knowledge-base.json, `sources/<source>.json` and anything under `documents/`. */
|
|
171
|
+
function isKnowledgeBaseFile(inFolder: string[]): boolean {
|
|
172
|
+
const [first, second] = inFolder
|
|
173
|
+
if (inFolder.length === 1) return first === KNOWLEDGE_BASE_FILE
|
|
174
|
+
if (first === KNOWLEDGE_BASE_SOURCES_DIR) return inFolder.length === 2 && !!second?.endsWith('.json')
|
|
175
|
+
return first === KNOWLEDGE_BASE_DOCUMENTS_DIR
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
/**
|
|
158
179
|
* The entry of a Function kept in its own folder: the TypeScript file whose
|
|
159
180
|
* `automation({ name: … })` names the folder, else index.ts or src/index.ts.
|
|
@@ -229,11 +250,12 @@ export function allTargets(root: string): SyncTarget[] {
|
|
|
229
250
|
}
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
|
-
for (const top of ['datasets', 'blueprint', 'knowledge', 'skills', 'mcp', 'themes', 'functions', 'workflows', 'agents', 'chat-apps', 'apps', 'mcp-servers', 'brand']) walk(join(root, top), 1)
|
|
253
|
+
for (const top of ['datasets', 'blueprint', 'knowledge', KNOWLEDGE_BASE_DIR, 'skills', 'mcp', 'themes', 'functions', 'workflows', 'agents', 'chat-apps', 'apps', 'mcp-servers', 'brand']) walk(join(root, top), 1)
|
|
233
254
|
if (existsSync(join(root, 'library.json'))) out.set('library', { kind: 'library', key: 'library', file: 'library.json' })
|
|
234
255
|
// Datasets first (object types read them), then the Blueprint (Agents and
|
|
235
256
|
// workflows read its objects), then what Agents use by name (knowledge
|
|
236
|
-
// bases
|
|
257
|
+
// bases, the Knowledge settings before the Knowledge app's bases whose
|
|
258
|
+
// sources they allow, slide themes, skills, plugins), then Functions (workflows and
|
|
237
259
|
// Agents call them), then Agents (a workflow's agent step names one), then workflows, then what
|
|
238
260
|
// each Agent may start, then the Agents' behavior settings (a procedure runs
|
|
239
261
|
// only what its Agent may start), then the Chat Apps their Agents are used in and the Apps that
|
|
@@ -242,7 +264,7 @@ export function allTargets(root: string): SyncTarget[] {
|
|
|
242
264
|
return [...out.values()].sort(byApplyOrder)
|
|
243
265
|
}
|
|
244
266
|
|
|
245
|
-
const ORDER = { brand: -1, dataset: 0, blueprint: 1, knowledge: 2, theme: 2, skill: 3, plugin: 4, function: 5, agent: 6, workflow: 7, bindings: 8, operating: 9, lessons: 9, 'chat-app': 10, app: 10, 'mcp-server': 10, library: 11 }
|
|
267
|
+
const ORDER = { brand: -1, dataset: 0, blueprint: 1, knowledge: 2, 'knowledge-base-settings': 2, theme: 2, 'knowledge-base': 3, skill: 3, plugin: 4, function: 5, agent: 6, workflow: 7, bindings: 8, operating: 9, lessons: 9, 'chat-app': 10, app: 10, 'mcp-server': 10, library: 11 }
|
|
246
268
|
|
|
247
269
|
/** Dependencies first: what a later kind reads or calls is applied before it. */
|
|
248
270
|
export function byApplyOrder(a: SyncTarget, b: SyncTarget): number {
|
|
@@ -297,6 +319,8 @@ export function commandFor(target: SyncTarget, profiles: SyncProfiles): string[]
|
|
|
297
319
|
// Session-only route; `local dev` calls it itself (see local-operating.ts).
|
|
298
320
|
throw new Error('behavior settings are applied over the API, not with a CLI command')
|
|
299
321
|
case 'knowledge':
|
|
322
|
+
case 'knowledge-base':
|
|
323
|
+
case 'knowledge-base-settings':
|
|
300
324
|
case 'plugin':
|
|
301
325
|
case 'bindings':
|
|
302
326
|
case 'theme':
|
|
@@ -437,6 +461,10 @@ export function describeTarget(target: SyncTarget): string {
|
|
|
437
461
|
return 'Blueprint'
|
|
438
462
|
case 'knowledge':
|
|
439
463
|
return `knowledge base ${target.name}`
|
|
464
|
+
case 'knowledge-base':
|
|
465
|
+
return `${KNOWLEDGE_BASE_DIR}/${target.name}`
|
|
466
|
+
case 'knowledge-base-settings':
|
|
467
|
+
return 'the Knowledge settings'
|
|
440
468
|
case 'skill':
|
|
441
469
|
return `skill ${target.name}`
|
|
442
470
|
case 'plugin':
|
|
@@ -505,6 +533,11 @@ export function unappliedReason(root: string, rel: string): string | null {
|
|
|
505
533
|
case 'knowledge':
|
|
506
534
|
if (parts.length === 2) return 'a knowledge base is a folder: knowledge/<name>/knowledge.json ({ "name": "…", "description": "…" }) with its documents beside it'
|
|
507
535
|
return `knowledge/${second}/ has no knowledge.json ({ "name": "…", "description": "…" }), so its documents are not uploaded`
|
|
536
|
+
case KNOWLEDGE_BASE_DIR:
|
|
537
|
+
if (parts.length === 2) return `a knowledge base is a folder: ${KNOWLEDGE_BASE_DIR}/<name>/${KNOWLEDGE_BASE_FILE}, with its API sources in sources/ and its files in documents/; the one file beside the folders is ${KNOWLEDGE_BASE_SETTINGS_FILE} (\`frontera local schema knowledge-base\`)`
|
|
538
|
+
if (!existsSync(join(root, KNOWLEDGE_BASE_DIR, second!, KNOWLEDGE_BASE_FILE))) return `${KNOWLEDGE_BASE_DIR}/${second}/ has no ${KNOWLEDGE_BASE_FILE}, so nothing in it is applied (\`frontera local schema knowledge-base\`)`
|
|
539
|
+
if (parts[2] === KNOWLEDGE_BASE_SOURCES_DIR) return `a source is ${KNOWLEDGE_BASE_DIR}/${second}/${KNOWLEDGE_BASE_SOURCES_DIR}/<source>.json, directly in ${KNOWLEDGE_BASE_SOURCES_DIR}/ (\`frontera local schema knowledge-base-source\`)`
|
|
540
|
+
return `a knowledge base folder holds ${KNOWLEDGE_BASE_FILE}, ${KNOWLEDGE_BASE_SOURCES_DIR}/ and ${KNOWLEDGE_BASE_DOCUMENTS_DIR}/: put the files to upload in ${KNOWLEDGE_BASE_DIR}/${second}/${KNOWLEDGE_BASE_DOCUMENTS_DIR}/`
|
|
508
541
|
case 'skills':
|
|
509
542
|
if (parts.length === 2) return 'a skill is a folder: skills/<name>/SKILL.md'
|
|
510
543
|
return `skills/${second}/ has no SKILL.md, so it is not pushed`
|
|
@@ -546,6 +579,12 @@ export function unappliedFiles(root: string): string[] {
|
|
|
546
579
|
const [top, second] = rel.split(sep)
|
|
547
580
|
// Folders whose files are not each applied: a Function's helpers, a knowledge base's documents, an App's code.
|
|
548
581
|
if (top === 'functions' || top === 'apps' || (top === 'knowledge' && second && existsSync(join(root, 'knowledge', second, 'knowledge.json')))) continue
|
|
582
|
+
// A base's documents are each uploaded; a folder without knowledge-base.json is one line.
|
|
583
|
+
if (top === KNOWLEDGE_BASE_DIR && rel.split(sep)[2] === KNOWLEDGE_BASE_DOCUMENTS_DIR) continue
|
|
584
|
+
if (top === KNOWLEDGE_BASE_DIR && second && rel.split(sep).length === 2 && !existsSync(join(root, KNOWLEDGE_BASE_DIR, second, KNOWLEDGE_BASE_FILE))) {
|
|
585
|
+
out.push(`${rel}/: ${unappliedReason(root, join(rel, '_'))}`)
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
549
588
|
if (top === 'skills' && second && existsSync(join(root, 'skills', second, 'SKILL.md'))) continue
|
|
550
589
|
if (top === 'themes' && second && existsSync(join(root, 'themes', second, 'theme.json'))) continue
|
|
551
590
|
if (top === 'knowledge' || top === 'skills' || top === 'themes') {
|
|
@@ -562,6 +601,6 @@ export function unappliedFiles(root: string): string[] {
|
|
|
562
601
|
}
|
|
563
602
|
}
|
|
564
603
|
}
|
|
565
|
-
for (const top of ['agents', 'blueprint', 'chat-apps', 'datasets', 'knowledge', 'mcp', 'mcp-servers', 'skills', 'themes', 'workflows']) walk(join(root, top), 1)
|
|
604
|
+
for (const top of ['agents', 'blueprint', 'chat-apps', 'datasets', 'knowledge', KNOWLEDGE_BASE_DIR, 'mcp', 'mcp-servers', 'skills', 'themes', 'workflows']) walk(join(root, top), 1)
|
|
566
605
|
return out
|
|
567
606
|
}
|