@frankieai/core 0.2.0 → 0.2.2

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/dist/index.cjs CHANGED
@@ -23,11 +23,15 @@ __export(index_exports, {
23
23
  activeSkills: () => activeSkills,
24
24
  agentConfigSchema: () => agentConfigSchema,
25
25
  apiCapabilitySchema: () => apiCapabilitySchema,
26
+ apiMethods: () => apiMethods,
27
+ apiOperations: () => apiOperations,
26
28
  apiSourceSchema: () => apiSourceSchema,
27
29
  approvedSkillTools: () => approvedSkillTools,
28
30
  assertNoCommittedFrankieKeys: () => assertNoCommittedFrankieKeys,
29
31
  authConfigSchema: () => authConfigSchema,
30
32
  brandingConfigSchema: () => brandingConfigSchema,
33
+ canonicalJson: () => canonicalJson,
34
+ compareApiDocuments: () => compareApiDocuments,
31
35
  defineFrankieConfig: () => defineFrankieConfig,
32
36
  describeTool: () => describeTool,
33
37
  draftSkill: () => draftSkill,
@@ -35,15 +39,18 @@ __export(index_exports, {
35
39
  featuresConfigSchema: () => featuresConfigSchema,
36
40
  frankieConfigSchema: () => frankieConfigSchema,
37
41
  limitsConfigSchema: () => limitsConfigSchema,
42
+ matchPath: () => matchPath,
38
43
  mcpServerSchema: () => mcpServerSchema,
39
44
  modelChoiceSchema: () => modelChoiceSchema,
40
45
  modelConfigSchema: () => modelConfigSchema,
46
+ operationId: () => operationId,
41
47
  parseFrankieConfig: () => parseFrankieConfig,
42
48
  planHostedInstall: () => planHostedInstall,
43
49
  sdkUpdateWorkflow: () => sdkUpdateWorkflow,
44
50
  skillSchema: () => skillSchema,
45
51
  thinkingLevelSchema: () => thinkingLevelSchema,
46
52
  toolPolicySchema: () => toolPolicySchema,
53
+ validateApiDocument: () => validateApiDocument,
47
54
  widgetThemeSchema: () => widgetThemeSchema
48
55
  });
49
56
  module.exports = __toCommonJS(index_exports);
@@ -106,7 +113,7 @@ var skillSchema = import_zod.z.object({
106
113
  function activeSkills(config, availableTools) {
107
114
  const approved = new Set(approvedSkillTools(config));
108
115
  const available = availableTools ? new Set(availableTools) : approved;
109
- return config.skills.filter((skill) => skill.status === "published" && skill.tools.every((tool) => approved.has(tool) && available.has(tool)));
116
+ return config.skills.filter((skill) => skill.status === "published").map((skill) => ({ ...skill, tools: skill.tools.filter((tool) => approved.has(tool) && available.has(tool)) })).filter((skill) => skill.tools.length > 0);
110
117
  }
111
118
  function approvedSkillTools(config) {
112
119
  const candidates = [
@@ -147,10 +154,16 @@ var modelConfigSchema = import_zod2.z.object({
147
154
  */
148
155
  provider: import_zod2.z.string().min(1).default("fireworks"),
149
156
  /**
150
- * Model id within that provider. The default is Kimi K2.7 on Fireworks,
151
- * which serves a native Anthropic-Messages endpoint and a 262K window.
157
+ * Model id within that provider. The default is Kimi K3 on Fireworks, served
158
+ * over the OpenAI-completions API. Crucially for an embedded chat widget, that
159
+ * endpoint returns the model's reasoning in a separate `reasoning_content`
160
+ * field (surfaced as a thinking stream the client gates off), rather than
161
+ * inlining it in the visible text channel — so the model's private planning
162
+ * never leaks into the user-facing reply. The earlier Anthropic-Messages Kimi
163
+ * builds emitted that reasoning as plain text on Fireworks, which no marker or
164
+ * prompt reliably suppressed.
152
165
  */
153
- model: import_zod2.z.string().default("accounts/fireworks/models/kimi-k2p7-code"),
166
+ model: import_zod2.z.string().default("accounts/fireworks/models/kimi-k3"),
154
167
  /** Env var holding the provider key. Never put the key in config itself. */
155
168
  apiKeyEnv: import_zod2.z.string().default("FIREWORKS_API_KEY"),
156
169
  /** Override the provider's base URL (self-hosted gateways, tests). */
@@ -224,7 +237,16 @@ var apiCapabilitySchema = import_zod2.z.object({
224
237
  operationId: import_zod2.z.string().min(1),
225
238
  method: import_zod2.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
226
239
  path: import_zod2.z.string().min(1),
227
- summary: import_zod2.z.string().optional()
240
+ summary: import_zod2.z.string().optional(),
241
+ /**
242
+ * True when this is a write operation the spec requires auth for, but the
243
+ * source has no way to authorize it as the specific calling user — no
244
+ * forwarded user token or claim, only (at best) a static/shared credential.
245
+ * A configured `security` credential never clears this on its own: it
246
+ * authorizes every call the same way regardless of who is chatting, which
247
+ * is a backend gap to flag, not something a spec or credential change fixes.
248
+ */
249
+ authGap: import_zod2.z.boolean().default(false)
228
250
  });
229
251
  var apiSourceSchema = import_zod2.z.object({
230
252
  name: import_zod2.z.string().min(1).regex(/^[a-z0-9][a-z0-9_-]*$/i, "API source names must be alphanumeric with - or _"),
@@ -263,6 +285,17 @@ var apiSourceSchema = import_zod2.z.object({
263
285
  forwardUserTokenAs: import_zod2.z.string().optional(),
264
286
  /** Extra claims from the user token to forward as `x-frankie-<claim>`. */
265
287
  forwardUserClaims: import_zod2.z.array(import_zod2.z.string()).default([]),
288
+ /**
289
+ * Credentials for the spec's own `components.securitySchemes`, keyed by
290
+ * scheme name. Frankie applies each per-operation `security` requirement at
291
+ * the location and with the prefix the scheme declares (an `apiKey` scheme
292
+ * goes to its named header/query/cookie; an `http` `bearer` or `oauth2`
293
+ * scheme becomes an `Authorization: Bearer` header; `http` `basic` takes a
294
+ * `user:pass` value and is base64-encoded automatically). Without this, a
295
+ * spec with per-operation auth falls back to the source's static `headers`
296
+ * for every operation, which is often wrong for at least some of them.
297
+ */
298
+ security: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).default({}),
266
299
  /** Per-call timeout. */
267
300
  timeoutMs: import_zod2.z.number().int().positive().default(3e4),
268
301
  /** Response bodies longer than this are truncated before the model sees them. */
@@ -348,8 +381,16 @@ var frankieConfigSchema = import_zod2.z.object({
348
381
  /** Opt-in end-user chat capabilities (attachments, model picker). */
349
382
  features: featuresConfigSchema
350
383
  });
384
+ var LEAKY_MODEL_MIGRATION = {
385
+ "accounts/fireworks/models/kimi-k2p7-code": "accounts/fireworks/models/kimi-k3"
386
+ };
351
387
  function parseFrankieConfig(input) {
352
- return frankieConfigSchema.parse(input);
388
+ const config = frankieConfigSchema.parse(input);
389
+ if (config.model.provider === "fireworks") {
390
+ const migrated = LEAKY_MODEL_MIGRATION[config.model.model];
391
+ if (migrated) config.model.model = migrated;
392
+ }
393
+ return config;
353
394
  }
354
395
  function defineFrankieConfig(config) {
355
396
  return config;
@@ -387,7 +428,7 @@ function planHostedInstall(input) {
387
428
  const tokenPath = appRoot + "api/frankie/token/route.ts";
388
429
  const needsAuthSetup = !workspace.allowAnonymous && !files[tokenPath] && !firebase && !authJs;
389
430
  if (needsAuthSetup) notes.push("Connect getCurrentUser in the generated token endpoint to your server session. It rejects requests until configured.");
390
- pkg.dependencies = { ...pkg.dependencies, "@frankieai/react": pkg.dependencies["@frankieai/react"] ?? "^0.2.0", ...!workspace.allowAnonymous ? { jose: pkg.dependencies.jose ?? "^6.0.11" } : {} };
431
+ pkg.dependencies = { ...pkg.dependencies, "@frankieai/react": pkg.dependencies["@frankieai/react"] ?? "^0.2.1", ...!workspace.allowAnonymous ? { jose: pkg.dependencies.jose ?? "^6.0.11" } : {} };
391
432
  out.set(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
392
433
  const authProp = workspace.allowAnonymous ? "" : " getUserToken={getUserToken}";
393
434
  const shared = `import { FrankieProvider, FrankieWidget, FrankieChatPage } from "@frankieai/react";
@@ -494,29 +535,34 @@ export async function POST() {
494
535
  } catch {
495
536
  }
496
537
  }
538
+ const middlewareMatchers = nextMiddlewareMatchers(files, sourceRoot, root);
497
539
  const paths = {};
498
- if (!doc) for (const [file, content] of Object.entries(files)) {
540
+ let withheld = 0;
541
+ for (const [file, content] of Object.entries(files)) {
499
542
  if (/\.(test|spec)\.[tj]sx?$/.test(file)) continue;
500
543
  const nextRoute = file.match(/(?:^|\/)app(\/.*)\/route\.[tj]s$/);
501
544
  if (nextRoute) {
502
545
  const path = nextRoute[1].replace(/\/\([^/]+\)/g, "").replace(/\[([^\]]+)\]/g, "{$1}");
503
546
  if (path.includes("...") || /\/(?:auth|frankie)(?:\/|$)/.test(path)) continue;
504
- for (const method of ["GET"]) {
505
- if (new RegExp("export\\s+(?:(?:async\\s+)?function\\s+" + method + "\\b|(?:const|let)\\s+" + method + "\\s*=)").test(content))
506
- addInstallOperation(paths, method, path, file);
547
+ const guarded = matchesAnyMatcher(middlewareMatchers, path);
548
+ for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
549
+ if (new RegExp("export\\s+(?:(?:async\\s+)?function\\s+" + method + "\\b|(?:const|let)\\s+" + method + "\\s*=)").test(content)) {
550
+ if (!addInstallOperation(paths, method, path, file, guarded)) withheld += 1;
551
+ }
507
552
  }
508
553
  } else if (/\.[tj]s$/.test(file) && /(?:routes?|api)\//.test(file)) {
509
- for (const match of content.matchAll(/\b(?:app|router|fastify)\.(get|post|put|patch|delete)(?:\s*<[^;]{1,1600}?>)?\s*\(\s*["']([^"']+)["']/g))
510
- addInstallOperation(paths, match[1].toUpperCase(), match[2].replace(/:([A-Za-z0-9_]+)/g, "{$1}"), file);
554
+ for (const match of content.matchAll(/\b(?:app|router|fastify)\.(get|post|put|patch|delete)(?:\s*<[^;]{1,1600}?>)?\s*\(\s*["']([^"']+)["'](\s*,\s*[A-Za-z_$][\w$]*\s*,)?/g))
555
+ if (!addInstallOperation(paths, match[1].toUpperCase(), match[2].replace(/:([A-Za-z0-9_]+)/g, "{$1}"), file, Boolean(match[3]))) withheld += 1;
511
556
  }
512
557
  }
513
- doc ??= { openapi: "3.0.3", info: { title: workspace.name + " API", version: "1.0.0", description: "Read-only generated route inventory. Review schemas, authorization, and the deployed origin before enabling tools." }, paths };
514
- doc = { ...doc, paths: Object.fromEntries(Object.entries(doc.paths).filter(([, item]) => item.get).map(([path, item]) => [path, { ...item.parameters ? { parameters: item.parameters } : {}, get: item.get }])) };
558
+ doc ??= { openapi: "3.0.3", info: { title: workspace.name + " API", version: "1.0.0", description: "Generated route inventory. Review schemas, authorization, and the deployed origin before enabling tools." }, paths };
559
+ for (const [path, item] of Object.entries(paths)) doc.paths[path] = { ...item, ...doc.paths[path] };
515
560
  const specPath = root + "public/frankie.openapi.json";
516
561
  const operationCount = Object.values(doc.paths).reduce((n, item) => n + Object.keys(item).filter((m) => /^(get|post|put|patch|delete)$/.test(m)).length, 0);
517
562
  out.set(specPath, JSON.stringify(doc, null, 2) + "\n");
518
563
  if (!operationCount) notes.push("No supported API routes detected. The OpenAPI draft has no operations; add the real schema before connecting tools.");
519
- notes.push("Generated OpenAPI is read-only (GET only). Add write operations only to a reviewed, user-authorized schema.");
564
+ if (withheld) notes.push(`Withheld ${withheld} newly discovered write operation(s) with no detected authorization guard (an Express/Fastify middleware argument, or a Next.js middleware.ts matcher covering the route). Add them to the spec yourself once they're protected.`);
565
+ notes.push("Generated OpenAPI includes read and write routes. Review schemas before approving tools; new connections still default to GET-only access.");
520
566
  notes.push("Verify the public spec URL is reachable without dashboard authentication before connecting it.");
521
567
  notes.push("The token endpoint issues a signed user JWT, but your product API must validate it and enforce user/resource permissions before exposing tools.");
522
568
  const envPath = root + ".env.example";
@@ -530,7 +576,7 @@ Workspace: ${workspace.name}
530
576
  ${workspace.allowAnonymous ? "" : "2. Add FRANKIE_JWT_SECRET from this workspace to the server hosting /api/frankie/token. Never commit the value.\n"}3. Run your package manager and commit the updated lockfile (for pnpm: pnpm install --lockfile-only). The remote GitHub installer cannot safely synthesize a lockfile.
531
577
  4. Confirm https://YOUR_APP/frankie.openapi.json is public. If your Next.js middleware protects all paths, explicitly bypass /frankie.openapi.json before connecting it.
532
578
  5. Open ${chatPath}. It is created inside the detected authenticated dashboard group when one exists; add its link to the host sidebar during review.
533
- 6. Connect https://YOUR_APP/frankie.openapi.json in Frankie \u2192 Tools & MCP. The generated document contains only read-only GET operations.
579
+ 6. Connect https://YOUR_APP/frankie.openapi.json in Frankie \u2192 Tools & MCP. The document includes read and write routes; approve tools and choose access limits separately.
534
580
  7. Forwarding sends the Frankie-signed JWT. Your API must validate it and enforce user/resource permissions. It is not automatically interchangeable with a Firebase or OAuth access token.
535
581
  8. Add NEXT_PUBLIC_FRANKIE_KEY to every frontend deployment environment (Preview and Production). ${workspace.allowAnonymous ? "" : "Add FRANKIE_JWT_SECRET only to the server environment running /api/frankie/token; never expose it to the browser.\n"}9. Review conversations in Workspace \u2192 Chat sessions.
536
582
 
@@ -542,8 +588,10 @@ ${notes.map((n) => "- " + n).join("\n")}
542
588
  assertNoCommittedFrankieKeys(planned);
543
589
  return { files: planned, root, chatPath, specPath, operationCount, needsAuthSetup, notes };
544
590
  }
545
- function addInstallOperation(paths, method, path, file) {
546
- if (method !== "GET" || path.includes("*") || !path.startsWith("/") || /\/(?:auth|frankie)(?:\/|$)/.test(path)) return;
591
+ var WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
592
+ function addInstallOperation(paths, method, path, file, guarded) {
593
+ if (path.includes("*") || !path.startsWith("/") || /\/(?:auth|frankie)(?:\/|$)/.test(path)) return true;
594
+ if (WRITE_METHODS.has(method.toLowerCase()) && !guarded) return false;
547
595
  (paths[path] ??= {})[method.toLowerCase()] = {
548
596
  operationId: (method.toLowerCase() + "_" + path).replace(/[^a-zA-Z0-9_]+/g, "_"),
549
597
  summary: method + " " + path,
@@ -552,6 +600,32 @@ function addInstallOperation(paths, method, path, file) {
552
600
  responses: { "200": { description: "Successful response" } },
553
601
  "x-frankie-review-required": true
554
602
  };
603
+ return true;
604
+ }
605
+ function nextMiddlewareMatchers(files, sourceRoot, root) {
606
+ const candidates = [.../* @__PURE__ */ new Set([sourceRoot + "middleware.ts", sourceRoot + "middleware.js", root + "middleware.ts", root + "middleware.js"])];
607
+ for (const candidate of candidates) {
608
+ const content = files[candidate];
609
+ if (typeof content !== "string") continue;
610
+ const config = content.match(/export\s+const\s+config\s*=\s*\{([^}]*)\}/);
611
+ if (!config) continue;
612
+ const matcher = config[1].match(/matcher\s*:\s*(\[[^\]]*\]|["'`][^"'`]*["'`])/);
613
+ if (!matcher) return [];
614
+ const raw = matcher[1];
615
+ return raw.startsWith("[") ? [...raw.matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]) : [raw.slice(1, -1)];
616
+ }
617
+ return [];
618
+ }
619
+ function matchesAnyMatcher(matchers, path) {
620
+ return matchers.some((pattern) => {
621
+ if (/[()|]/.test(pattern)) return false;
622
+ const glob = pattern.replace(/:[A-Za-z0-9_]+\*/g, "**").replace(/:[A-Za-z0-9_]+\+?/g, "*");
623
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
624
+ const trailingDeep = escaped.endsWith("/**");
625
+ const body = trailingDeep ? escaped.slice(0, -3) : escaped;
626
+ const regexBody = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
627
+ return new RegExp(`^${regexBody}$`).test(path);
628
+ });
555
629
  }
556
630
 
557
631
  // src/sdk-updates.ts
@@ -578,20 +652,20 @@ jobs:
578
652
  outputs:
579
653
  base: $GH steps.base.outputs.sha }}
580
654
  steps:
581
- - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
655
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
582
656
  with:
583
657
  persist-credentials: false
584
658
  - id: base
585
659
  run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
586
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
660
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
587
661
  with:
588
- node-version: 22
662
+ node-version: 24
589
663
  - name: Install pinned pnpm
590
664
  run: npm install --global --ignore-scripts pnpm@10.33.0
591
665
  - name: Prepare SDK and lockfile together
592
666
  run: |
593
667
  test -f pnpm-lock.yaml
594
- pnpm --recursive --include-workspace-root update '@frankieai/react@latest' '@frankieai/core@latest' '@frankieai/cli@latest' '@frankieai/server@latest' --lockfile-only --ignore-scripts --ignore-pnpmfile
668
+ pnpm --recursive --include-workspace-root update '@frankieai/react@latest' '@frankieai/core@latest' '@frankieai/cli@latest' '@frankieai/server@latest' --lockfile-only --ignore-scripts --ignore-pnpmfile --config.manage-package-manager-versions=false --config.package-manager-strict-version=false
595
669
  - name: Package allowlisted changes
596
670
  run: |
597
671
  node --input-type=module <<'NODE'
@@ -607,7 +681,7 @@ jobs:
607
681
  mkdirSync('frankie-update-artifact', {recursive:true});
608
682
  writeFileSync('frankie-update-artifact/changes.json', JSON.stringify(files));
609
683
  NODE
610
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
684
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
611
685
  with:
612
686
  name: frankie-sdk-update
613
687
  path: frankie-update-artifact/changes.json
@@ -621,11 +695,11 @@ jobs:
621
695
  pull-requests: write
622
696
  steps:
623
697
  # Separate runner: no checkout, repository hooks, install scripts, or builds.
624
- - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
698
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
625
699
  with:
626
700
  name: frankie-sdk-update
627
701
  path: artifact
628
- - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
702
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
629
703
  env:
630
704
  BASE_SHA: $GH needs.prepare.outputs.base }}
631
705
  with:
@@ -642,6 +716,7 @@ jobs:
642
716
  if (current.data.object.sha !== base) throw Error('Default branch changed. Run the updater again.');
643
717
  const existing = await github.paginate(github.rest.pulls.list, {owner,repo,state:'open',per_page:100});
644
718
  if (existing.some(pr => pr.head.repo?.full_name === owner + '/' + repo && pr.head.ref.startsWith('frankie/sdk-update-'))) { core.notice('An SDK update PR already exists; review it first.'); return; }
719
+ const canonical = value => Array.isArray(value) ? '[' + value.map(canonical).join(',') + ']' : value && typeof value === 'object' ? '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}' : JSON.stringify(value);
645
720
  const clean = value => {
646
721
  const copy = JSON.parse(JSON.stringify(value));
647
722
  for (const group of ['dependencies','devDependencies','optionalDependencies']) {
@@ -649,6 +724,7 @@ jobs:
649
724
  }
650
725
  return copy;
651
726
  };
727
+ let sdkChanged = false;
652
728
  const seen = new Set();
653
729
  for (const file of files) {
654
730
  if (!file || typeof file.path !== 'string' || typeof file.content !== 'string' || seen.has(file.path)) throw Error('Invalid update file');
@@ -659,14 +735,16 @@ jobs:
659
735
  const original = await github.rest.repos.getContent({owner,repo,path:file.path,ref:base});
660
736
  const before = JSON.parse(Buffer.from(original.data.content, 'base64').toString());
661
737
  const after = JSON.parse(file.content);
662
- if (JSON.stringify(clean(before)) !== JSON.stringify(clean(after))) throw Error('Non-SDK manifest change');
738
+ if (canonical(clean(before)) !== canonical(clean(after))) throw Error('Non-SDK manifest change in ' + file.path);
663
739
  for (const group of ['dependencies','devDependencies','optionalDependencies']) {
664
740
  const sdkKeys = value => Object.keys(value[group] || {}).filter(key => key.startsWith('@frankieai/')).sort();
665
741
  if (JSON.stringify(sdkKeys(before)) !== JSON.stringify(sdkKeys(after))) throw Error('SDK dependency added or removed');
742
+ if (sdkKeys(after).some(key => before[group][key] !== after[group][key])) sdkChanged = true;
666
743
  for (const [key, value] of Object.entries(after[group] || {})) if (key.startsWith('@frankieai/') && !/^[~^]?\d+\.\d+\.\d+$/.test(value)) throw Error('Only stable SDK releases are supported');
667
744
  }
668
745
  }
669
746
  }
747
+ if (!sdkChanged) { core.notice('Frankie SDK versions are unchanged; no formatting-only update PR is needed.'); return; }
670
748
  if (!seen.has('pnpm-lock.yaml')) throw Error('SDK update must include the lockfile');
671
749
  const parent = await github.rest.git.getCommit({owner,repo,commit_sha:base});
672
750
  const tree = await github.rest.git.createTree({owner,repo,base_tree:parent.data.tree.sha,tree:files.map(f => ({...f,mode:'100644',type:'blob'}))});
@@ -676,16 +754,132 @@ jobs:
676
754
  await github.rest.git.createRef({owner,repo,ref:'refs/heads/' + branch,sha:commit.data.sha});
677
755
  await github.rest.pulls.create({owner,repo,head:branch,base:context.payload.repository.default_branch,draft:true,title:'Update Frankie SDK',body:'Updates the SDK and pnpm lockfile together. No auto-merge. Review release notes and breaking changes, run CI, and test signed-in chat and tool authorization. PRs made with GITHUB_TOKEN may need a maintainer to trigger CI; this workflow does not bypass that protection.'});
678
756
  `;
757
+
758
+ // src/openapi-review.ts
759
+ var apiMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
760
+ function canonicalJson(value) {
761
+ if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
762
+ if (value && typeof value === "object") return "{" + Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => JSON.stringify(k) + ":" + canonicalJson(v)).join(",") + "}";
763
+ return JSON.stringify(value) ?? "null";
764
+ }
765
+ function validateApiDocument(value) {
766
+ const doc = value;
767
+ if (!doc || typeof doc !== "object" || !/^3\.(0|1)\./.test(doc.openapi) || !doc.paths || typeof doc.paths !== "object" || Array.isArray(doc.paths)) throw new Error("Use an OpenAPI 3.0 or 3.1 document with a paths object.");
768
+ const ids = /* @__PURE__ */ new Set();
769
+ const walk = (node, depth = 0) => {
770
+ if (depth > 80) throw new Error("Spec nesting is too deep.");
771
+ if (!node || typeof node !== "object") return;
772
+ for (const [key, item] of Object.entries(node)) {
773
+ if (["__proto__", "prototype", "constructor"].includes(key)) throw new Error("Spec contains an unsafe property.");
774
+ if (key === "$ref" && (typeof item !== "string" || !item.startsWith("#/"))) throw new Error("Bundle external references into this spec before refreshing.");
775
+ walk(item, depth + 1);
776
+ }
777
+ };
778
+ walk(doc);
779
+ for (const [path, item] of Object.entries(doc.paths)) {
780
+ if (!path.startsWith("/") || !item || typeof item !== "object" || Array.isArray(item) || item.$ref) throw new Error("Use inline path items with absolute API paths.");
781
+ for (const method of apiMethods) {
782
+ const op = item[method];
783
+ if (!op) continue;
784
+ if (typeof op !== "object" || Array.isArray(op)) throw new Error("Invalid API operation.");
785
+ const id = operationId(method, path, op);
786
+ const toolId = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 48);
787
+ if (ids.has(toolId)) throw new Error("Operation IDs must be unique, including their tool-safe names.");
788
+ ids.add(toolId);
789
+ }
790
+ }
791
+ }
792
+ function operationId(method, path, op) {
793
+ return typeof op.operationId === "string" ? op.operationId : `${method}_${path.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "")}`;
794
+ }
795
+ function resolvePointer(doc, ref) {
796
+ let current = doc;
797
+ for (const segment of ref.slice(2).split("/")) {
798
+ if (!current || typeof current !== "object") return void 0;
799
+ current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
800
+ }
801
+ return current;
802
+ }
803
+ function reachableDefinitions(doc, roots) {
804
+ const resolved = {};
805
+ const visit = (node) => {
806
+ if (Array.isArray(node)) {
807
+ for (const entry of node) visit(entry);
808
+ return;
809
+ }
810
+ if (!node || typeof node !== "object") return;
811
+ for (const [key, value] of Object.entries(node)) {
812
+ if (key === "$ref" && typeof value === "string") {
813
+ if (value in resolved) continue;
814
+ const target = resolvePointer(doc, value);
815
+ resolved[value] = target ?? null;
816
+ visit(target);
817
+ continue;
818
+ }
819
+ visit(value);
820
+ }
821
+ };
822
+ for (const root of roots) visit(root);
823
+ return resolved;
824
+ }
825
+ function apiOperations(doc) {
826
+ const operations = /* @__PURE__ */ new Map();
827
+ for (const [path, item] of Object.entries(doc.paths)) {
828
+ const parameters = Object.fromEntries(Object.entries(item).filter(([key]) => !apiMethods.includes(key)));
829
+ for (const method of apiMethods) {
830
+ const operation = item[method];
831
+ if (!operation) continue;
832
+ operations.set(method.toUpperCase() + " " + path, {
833
+ id: operationId(method, path, operation),
834
+ // Fingerprint what the operation's contract actually depends on: itself,
835
+ // the path item's shared fields, the definitions it reaches by `$ref`, and
836
+ // the document-level `security` that applies only where it declares none.
837
+ // Folding in the whole document instead meant one unrelated schema — or an
838
+ // `openapi` version bump — revoked every approval on the source.
839
+ // Document-level `servers` stays out deliberately: a refresh whose servers
840
+ // moved is already rejected unless the operator pinned an explicit baseUrl,
841
+ // and that override wins over the spec anyway (see agent/openapi.ts).
842
+ fingerprint: canonicalJson({
843
+ operation,
844
+ parameters,
845
+ security: operation["security"] === void 0 ? doc.security ?? null : null,
846
+ definitions: reachableDefinitions(doc, [operation, parameters])
847
+ })
848
+ });
849
+ }
850
+ }
851
+ return operations;
852
+ }
853
+ function matchPath(glob, path) {
854
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
855
+ const trailingDeep = escaped.endsWith("/**");
856
+ const body = trailingDeep ? escaped.slice(0, -3) : escaped;
857
+ const pattern = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
858
+ return new RegExp(`^${pattern}$`).test(path);
859
+ }
860
+ function compareApiDocuments(before, after) {
861
+ const old = apiOperations(before), next = apiOperations(after);
862
+ return {
863
+ added: [...next.keys()].filter((key) => !old.has(key)),
864
+ changed: [...next.keys()].filter((key) => old.has(key) && old.get(key).fingerprint !== next.get(key).fingerprint),
865
+ removed: [...old.keys()].filter((key) => !next.has(key)),
866
+ unchangedIds: [...next].filter(([key, value]) => old.get(key)?.fingerprint === value.fingerprint).map(([, value]) => value.id)
867
+ };
868
+ }
679
869
  // Annotate the CommonJS export names for ESM import in node:
680
870
  0 && (module.exports = {
681
871
  activeSkills,
682
872
  agentConfigSchema,
683
873
  apiCapabilitySchema,
874
+ apiMethods,
875
+ apiOperations,
684
876
  apiSourceSchema,
685
877
  approvedSkillTools,
686
878
  assertNoCommittedFrankieKeys,
687
879
  authConfigSchema,
688
880
  brandingConfigSchema,
881
+ canonicalJson,
882
+ compareApiDocuments,
689
883
  defineFrankieConfig,
690
884
  describeTool,
691
885
  draftSkill,
@@ -693,15 +887,18 @@ jobs:
693
887
  featuresConfigSchema,
694
888
  frankieConfigSchema,
695
889
  limitsConfigSchema,
890
+ matchPath,
696
891
  mcpServerSchema,
697
892
  modelChoiceSchema,
698
893
  modelConfigSchema,
894
+ operationId,
699
895
  parseFrankieConfig,
700
896
  planHostedInstall,
701
897
  sdkUpdateWorkflow,
702
898
  skillSchema,
703
899
  thinkingLevelSchema,
704
900
  toolPolicySchema,
901
+ validateApiDocument,
705
902
  widgetThemeSchema
706
903
  });
707
904
  //# sourceMappingURL=index.cjs.map