@frontera-sdk/cli 1.52.44 → 1.52.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.52.44",
3
+ "version": "1.52.45",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.3.251",
42
42
  "@faker-js/faker": "10.6.0",
43
- "@frontera-sdk/core": "1.52.44",
44
- "@frontera-sdk/functions": "1.52.44",
43
+ "@frontera-sdk/core": "1.52.45",
44
+ "@frontera-sdk/functions": "1.52.45",
45
45
  "ai": "^6.0.116",
46
46
  "axe-core": "4.12.1",
47
47
  "gray-matter": "^4.0.3",
@@ -49,7 +49,7 @@
49
49
  "yaml": "^2.9.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@frontera-sdk/forge-contracts": "1.52.44",
52
+ "@frontera-sdk/forge-contracts": "1.52.45",
53
53
  "@types/bun": "^1.3.14",
54
54
  "typescript": "^5.9.3"
55
55
  }
@@ -44,55 +44,8 @@ export const kitVendor: Command = {
44
44
  requireCompatible()
45
45
 
46
46
  const root = kitRoot(ctx)
47
- const rendered = renderKit()
48
- const lock = readLock(root)
49
47
  const force = flagBool(ctx, 'force')
50
-
51
- const report = writeGenerated(root, rendered.generated, lock, { force })
52
-
53
- const edited: string[] = []
54
- const repaired: string[] = []
55
-
56
- if (rendered.agentsMd !== null) {
57
- const path = join(root, 'AGENTS.md')
58
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
59
- const next = applyManagedBlock(existing, rendered.agentsMd)
60
- if (next !== existing) {
61
- writeFileSync(path, next)
62
- edited.push('AGENTS.md')
63
- }
64
- }
65
-
66
- if (rendered.claudeMd !== null) {
67
- const path = join(root, 'CLAUDE.md')
68
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
69
- const result = applyClaudeImport(existing, rendered.claudeMd)
70
- if (result.content !== existing) {
71
- writeFileSync(path, result.content)
72
- edited.push('CLAUDE.md')
73
- }
74
- if (result.repaired) repaired.push('CLAUDE.md')
75
- }
76
-
77
- // The lock records what WAS written. A conflicted file keeps whatever digest
78
- // it had, so the next run still reports it rather than adopting the local
79
- // edit as ours.
80
- const locked = Object.fromEntries(
81
- Object.entries(rendered.generated).filter(([rel]) => !report.conflicts.includes(rel)),
82
- )
83
- writeLock(root, locked, cliVersion())
84
-
85
- const data = {
86
- root,
87
- kitVersion: KIT.kitVersion,
88
- hosts: KIT.hosts,
89
- written: report.written,
90
- unchanged: report.unchanged,
91
- conflicts: report.conflicts,
92
- edited,
93
- repaired,
94
- lock: LOCK_FILE,
95
- }
48
+ const { data, report, edited, repaired } = vendorKit(root, { force })
96
49
 
97
50
  const lines = [
98
51
  ...report.written.map((f) => ` + ${f}`),
@@ -117,4 +70,65 @@ export const kitVendor: Command = {
117
70
  },
118
71
  }
119
72
 
73
+ /**
74
+ * Write the kit into `root`: the skills in both host directories, the managed
75
+ * AGENTS.md block, the CLAUDE.md import and the lock.
76
+ *
77
+ * Shared with `frontera local init`, so a project folder Forge opens carries the
78
+ * whole kit, the solution skills included, rather than only the CLI skill.
79
+ */
80
+ export function vendorKit(root: string, opts: { force?: boolean } = {}) {
81
+ const rendered = renderKit()
82
+ const lock = readLock(root)
83
+ const force = opts.force === true
84
+
85
+ const report = writeGenerated(root, rendered.generated, lock, { force })
86
+
87
+ const edited: string[] = []
88
+ const repaired: string[] = []
89
+
90
+ if (rendered.agentsMd !== null) {
91
+ const path = join(root, 'AGENTS.md')
92
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
93
+ const next = applyManagedBlock(existing, rendered.agentsMd)
94
+ if (next !== existing) {
95
+ writeFileSync(path, next)
96
+ edited.push('AGENTS.md')
97
+ }
98
+ }
99
+
100
+ if (rendered.claudeMd !== null) {
101
+ const path = join(root, 'CLAUDE.md')
102
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
103
+ const result = applyClaudeImport(existing, rendered.claudeMd)
104
+ if (result.content !== existing) {
105
+ writeFileSync(path, result.content)
106
+ edited.push('CLAUDE.md')
107
+ }
108
+ if (result.repaired) repaired.push('CLAUDE.md')
109
+ }
110
+
111
+ // The lock records what WAS written. A conflicted file keeps whatever digest
112
+ // it had, so the next run still reports it rather than adopting the local
113
+ // edit as ours.
114
+ const locked = Object.fromEntries(
115
+ Object.entries(rendered.generated).filter(([rel]) => !report.conflicts.includes(rel)),
116
+ )
117
+ writeLock(root, locked, cliVersion())
118
+
119
+ const data = {
120
+ root,
121
+ kitVersion: KIT.kitVersion,
122
+ hosts: KIT.hosts,
123
+ written: report.written,
124
+ unchanged: report.unchanged,
125
+ conflicts: report.conflicts,
126
+ edited,
127
+ repaired,
128
+ lock: LOCK_FILE,
129
+ }
130
+
131
+ return { data, report, edited, repaired }
132
+ }
133
+
120
134
  export { CliError }
@@ -10,6 +10,9 @@ import { readLink } from '../../local/local-sync-link'
10
10
  import { generateSyntheticData, addMissingBackings, loadScenario, SCENARIO_TEMPLATE } from '../../local/local-synthetic-data'
11
11
  import { isoDay, writeSampleOptions } from '../../local/local-sample-remake'
12
12
  import { migrateMcpConfigs, writeSolutionScaffold } from '../../local/local-scaffold'
13
+ import { checkCompatibility } from '../../kit'
14
+ import { cliVersion } from '../kit/shared'
15
+ import { vendorKit } from '../kit/vendor'
13
16
  import { flagBool, flagString, type Command, type CommandContext } from '../types'
14
17
  import { localSync } from './sync'
15
18
 
@@ -39,6 +42,7 @@ export const localInit: Command = {
39
42
  kinds: 'string',
40
43
  'no-sample-data': 'boolean',
41
44
  'no-app-code': 'boolean',
45
+ 'no-kit': 'boolean',
42
46
  rows: 'string',
43
47
  seed: 'string',
44
48
  },
@@ -80,9 +84,25 @@ export const localInit: Command = {
80
84
  const at = scaffold.skipped.indexOf(file)
81
85
  if (at >= 0) scaffold.skipped.splice(at, 1)
82
86
  }
87
+ // The whole authoring kit, after the scaffold so its managed block lands in
88
+ // the project's AGENTS.md: the solution skills (delivery, Agents, workflows,
89
+ // Functions, Apps, embeds, review) reach every coding agent working here,
90
+ // not only the CLI skill. `--no-kit` leaves it out, for a machine that uses
91
+ // the installed plugin instead (`frontera kit doctor` finds duplicates).
92
+ const kit = { written: [] as string[], skipped: [] as string[] }
93
+ if (!flagBool(ctx, 'no-kit')) {
94
+ const compat = checkCompatibility(cliVersion())
95
+ if (compat.compatible) {
96
+ const vendored = vendorKit(target, { force })
97
+ kit.written.push(...vendored.report.written, ...vendored.edited)
98
+ kit.skipped.push(...vendored.report.conflicts)
99
+ } else {
100
+ ctx.output.note(` the Frontera skills were not added: ${compat.reason}; run \`frontera kit vendor\` after updating the CLI`)
101
+ }
102
+ }
83
103
  const listed = (files: string[]) => files.filter((f) => !f.startsWith('vendor/'))
84
- const written = [...new Set(listed([...scaffold.written, ...harness.written]))]
85
- const skipped = listed([...scaffold.skipped, ...harness.skipped]).filter((f) => !written.includes(f))
104
+ const written = [...new Set(listed([...scaffold.written, ...harness.written, ...kit.written]))]
105
+ const skipped = listed([...scaffold.skipped, ...harness.skipped, ...kit.skipped]).filter((f) => !written.includes(f))
86
106
 
87
107
  let installed: boolean | null = null
88
108
  if (!flagBool(ctx, 'no-install') && scaffold.written.includes('package.json')) {
package/src/flag-help.ts CHANGED
@@ -276,6 +276,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
276
276
  conflicts: 'the conflicts the picks were chosen against, as `diff --json` names them; refused if they moved',
277
277
  'from-workspace': 'start the project from a platform workspace instead of the examples',
278
278
  'no-sample-data': 'do not generate sample rows for the pulled Blueprint',
279
+ 'no-kit': 'leave out the Frontera skills (the kit) that local init vendors into the project; for a machine that uses the installed plugin instead',
279
280
  'keep-sample-data': 'leave the sample datasets as they are; by default they are made again (same seed, rows and day) after a pull that brings Blueprint items, or when an older generator made them',
280
281
  'remake-sample-data': 'make the sample datasets again with the newest sample rules (same seed, rows and day), even the ones a pull wrote, which local dev otherwise keeps as pulled',
281
282
  synthetic: 'generate sample rows for the Blueprint: values fit each property, links stay consistent',
@@ -21,8 +21,8 @@ const SERVICE = 'frontera-local-model-key'
21
21
  export interface ProviderSpec {
22
22
  id: string
23
23
  label: string
24
- /** The variable the service reads for this provider. */
25
- envVar: string
24
+ /** The variable the service reads for this provider; none for a key only an organization holds. */
25
+ envVar?: string
26
26
  }
27
27
 
28
28
  /** Providers the local runtime can pass a key to; mirrors the service's built-in providers. */
@@ -32,6 +32,8 @@ export const LOCAL_PROVIDERS: readonly ProviderSpec[] = [
32
32
  { id: 'openrouter', label: 'OpenRouter', envVar: 'OPENROUTER_API_KEY' },
33
33
  { id: 'google', label: 'Google Gemini', envVar: 'GOOGLE_GENERATIVE_AI_API_KEY' },
34
34
  { id: 'cohere', label: 'Cohere (reranking)', envVar: 'COHERE_API_KEY' },
35
+ // Agents' web search; stored as the local organization's own key, never an environment variable.
36
+ { id: 'openrouter-web-search', label: 'OpenRouter web search' },
35
37
  ]
36
38
 
37
39
  export function findProvider(id: string): ProviderSpec | undefined {
@@ -100,7 +102,7 @@ export function providerEnv(): Record<string, string> {
100
102
  const env: Record<string, string> = {}
101
103
  for (const p of LOCAL_PROVIDERS) {
102
104
  const key = getKey(p.id)
103
- if (key) env[p.envVar] = key
105
+ if (key && p.envVar) env[p.envVar] = key
104
106
  }
105
107
  return env
106
108
  }
@@ -166,7 +166,7 @@ export async function seedLocalRuntime(args: SeedArgs): Promise<LocalCredentials
166
166
  }
167
167
 
168
168
  /** Provider ids the service stores as organization credentials, each with an `apiKey` field. */
169
- const CREDENTIAL_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'google'])
169
+ const CREDENTIAL_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'google', 'openrouter-web-search'])
170
170
 
171
171
  /** Embedding defaults by provider, in preference order; the service requires 1536-wide vectors. */
172
172
  const EMBEDDING: Array<{ provider: string; model: string }> = [
@@ -386,14 +386,20 @@ export function startStudioServer(opts: StudioServerOptions) {
386
386
  if (state?.credentials?.workspaceId && !headers.has('x-workspace-id')) {
387
387
  headers.set('x-workspace-id', state.credentials.workspaceId)
388
388
  }
389
+ // A page sends the session token it kept (`bearer_token`) itself. After
390
+ // `frontera local reset` that session is gone, so a session the service
391
+ // refuses is answered as the owner; an API key or a signed token is not.
392
+ const kept = headers.get('authorization')?.match(/^Bearer ([A-Za-z0-9]{32})$/)?.[1]
393
+ const fallback = kept && bearer && kept !== bearer ? bearer : null
394
+ const hasBody = req.method !== 'GET' && req.method !== 'HEAD'
395
+ const body = hasBody && fallback ? await req.arrayBuffer() : hasBody ? req.body : undefined
389
396
  try {
390
- const res = await fetch(target, {
391
- method: req.method,
392
- headers,
393
- body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req.body,
394
- redirect: 'manual',
395
- duplex: 'half',
396
- })
397
+ const send = () => fetch(target, { method: req.method, headers, body, redirect: 'manual', duplex: 'half' })
398
+ let res = await send()
399
+ if (res.status === 401 && fallback) {
400
+ headers.set('authorization', `Bearer ${fallback}`)
401
+ res = await send()
402
+ }
397
403
  const out = new Headers(res.headers)
398
404
  out.delete('content-encoding')
399
405
  out.delete('content-length')