@frankieai/core 0.2.1 → 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 +151 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +150 -22
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -97,10 +97,16 @@ var modelConfigSchema = z2.object({
|
|
|
97
97
|
*/
|
|
98
98
|
provider: z2.string().min(1).default("fireworks"),
|
|
99
99
|
/**
|
|
100
|
-
* Model id within that provider. The default is Kimi
|
|
101
|
-
*
|
|
100
|
+
* Model id within that provider. The default is Kimi K3 on Fireworks, served
|
|
101
|
+
* over the OpenAI-completions API. Crucially for an embedded chat widget, that
|
|
102
|
+
* endpoint returns the model's reasoning in a separate `reasoning_content`
|
|
103
|
+
* field (surfaced as a thinking stream the client gates off), rather than
|
|
104
|
+
* inlining it in the visible text channel — so the model's private planning
|
|
105
|
+
* never leaks into the user-facing reply. The earlier Anthropic-Messages Kimi
|
|
106
|
+
* builds emitted that reasoning as plain text on Fireworks, which no marker or
|
|
107
|
+
* prompt reliably suppressed.
|
|
102
108
|
*/
|
|
103
|
-
model: z2.string().default("accounts/fireworks/models/kimi-
|
|
109
|
+
model: z2.string().default("accounts/fireworks/models/kimi-k3"),
|
|
104
110
|
/** Env var holding the provider key. Never put the key in config itself. */
|
|
105
111
|
apiKeyEnv: z2.string().default("FIREWORKS_API_KEY"),
|
|
106
112
|
/** Override the provider's base URL (self-hosted gateways, tests). */
|
|
@@ -174,7 +180,16 @@ var apiCapabilitySchema = z2.object({
|
|
|
174
180
|
operationId: z2.string().min(1),
|
|
175
181
|
method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
176
182
|
path: z2.string().min(1),
|
|
177
|
-
summary: z2.string().optional()
|
|
183
|
+
summary: z2.string().optional(),
|
|
184
|
+
/**
|
|
185
|
+
* True when this is a write operation the spec requires auth for, but the
|
|
186
|
+
* source has no way to authorize it as the specific calling user — no
|
|
187
|
+
* forwarded user token or claim, only (at best) a static/shared credential.
|
|
188
|
+
* A configured `security` credential never clears this on its own: it
|
|
189
|
+
* authorizes every call the same way regardless of who is chatting, which
|
|
190
|
+
* is a backend gap to flag, not something a spec or credential change fixes.
|
|
191
|
+
*/
|
|
192
|
+
authGap: z2.boolean().default(false)
|
|
178
193
|
});
|
|
179
194
|
var apiSourceSchema = z2.object({
|
|
180
195
|
name: z2.string().min(1).regex(/^[a-z0-9][a-z0-9_-]*$/i, "API source names must be alphanumeric with - or _"),
|
|
@@ -213,6 +228,17 @@ var apiSourceSchema = z2.object({
|
|
|
213
228
|
forwardUserTokenAs: z2.string().optional(),
|
|
214
229
|
/** Extra claims from the user token to forward as `x-frankie-<claim>`. */
|
|
215
230
|
forwardUserClaims: z2.array(z2.string()).default([]),
|
|
231
|
+
/**
|
|
232
|
+
* Credentials for the spec's own `components.securitySchemes`, keyed by
|
|
233
|
+
* scheme name. Frankie applies each per-operation `security` requirement at
|
|
234
|
+
* the location and with the prefix the scheme declares (an `apiKey` scheme
|
|
235
|
+
* goes to its named header/query/cookie; an `http` `bearer` or `oauth2`
|
|
236
|
+
* scheme becomes an `Authorization: Bearer` header; `http` `basic` takes a
|
|
237
|
+
* `user:pass` value and is base64-encoded automatically). Without this, a
|
|
238
|
+
* spec with per-operation auth falls back to the source's static `headers`
|
|
239
|
+
* for every operation, which is often wrong for at least some of them.
|
|
240
|
+
*/
|
|
241
|
+
security: z2.record(z2.string(), z2.string()).default({}),
|
|
216
242
|
/** Per-call timeout. */
|
|
217
243
|
timeoutMs: z2.number().int().positive().default(3e4),
|
|
218
244
|
/** Response bodies longer than this are truncated before the model sees them. */
|
|
@@ -298,8 +324,16 @@ var frankieConfigSchema = z2.object({
|
|
|
298
324
|
/** Opt-in end-user chat capabilities (attachments, model picker). */
|
|
299
325
|
features: featuresConfigSchema
|
|
300
326
|
});
|
|
327
|
+
var LEAKY_MODEL_MIGRATION = {
|
|
328
|
+
"accounts/fireworks/models/kimi-k2p7-code": "accounts/fireworks/models/kimi-k3"
|
|
329
|
+
};
|
|
301
330
|
function parseFrankieConfig(input) {
|
|
302
|
-
|
|
331
|
+
const config = frankieConfigSchema.parse(input);
|
|
332
|
+
if (config.model.provider === "fireworks") {
|
|
333
|
+
const migrated = LEAKY_MODEL_MIGRATION[config.model.model];
|
|
334
|
+
if (migrated) config.model.model = migrated;
|
|
335
|
+
}
|
|
336
|
+
return config;
|
|
303
337
|
}
|
|
304
338
|
function defineFrankieConfig(config) {
|
|
305
339
|
return config;
|
|
@@ -444,20 +478,24 @@ export async function POST() {
|
|
|
444
478
|
} catch {
|
|
445
479
|
}
|
|
446
480
|
}
|
|
481
|
+
const middlewareMatchers = nextMiddlewareMatchers(files, sourceRoot, root);
|
|
447
482
|
const paths = {};
|
|
483
|
+
let withheld = 0;
|
|
448
484
|
for (const [file, content] of Object.entries(files)) {
|
|
449
485
|
if (/\.(test|spec)\.[tj]sx?$/.test(file)) continue;
|
|
450
486
|
const nextRoute = file.match(/(?:^|\/)app(\/.*)\/route\.[tj]s$/);
|
|
451
487
|
if (nextRoute) {
|
|
452
488
|
const path = nextRoute[1].replace(/\/\([^/]+\)/g, "").replace(/\[([^\]]+)\]/g, "{$1}");
|
|
453
489
|
if (path.includes("...") || /\/(?:auth|frankie)(?:\/|$)/.test(path)) continue;
|
|
490
|
+
const guarded = matchesAnyMatcher(middlewareMatchers, path);
|
|
454
491
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
|
|
455
|
-
if (new RegExp("export\\s+(?:(?:async\\s+)?function\\s+" + method + "\\b|(?:const|let)\\s+" + method + "\\s*=)").test(content))
|
|
456
|
-
addInstallOperation(paths, method, path, file);
|
|
492
|
+
if (new RegExp("export\\s+(?:(?:async\\s+)?function\\s+" + method + "\\b|(?:const|let)\\s+" + method + "\\s*=)").test(content)) {
|
|
493
|
+
if (!addInstallOperation(paths, method, path, file, guarded)) withheld += 1;
|
|
494
|
+
}
|
|
457
495
|
}
|
|
458
496
|
} else if (/\.[tj]s$/.test(file) && /(?:routes?|api)\//.test(file)) {
|
|
459
|
-
for (const match of content.matchAll(/\b(?:app|router|fastify)\.(get|post|put|patch|delete)(?:\s*<[^;]{1,1600}?>)?\s*\(\s*["']([^"']+)["']
|
|
460
|
-
addInstallOperation(paths, match[1].toUpperCase(), match[2].replace(/:([A-Za-z0-9_]+)/g, "{$1}"), file);
|
|
497
|
+
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))
|
|
498
|
+
if (!addInstallOperation(paths, match[1].toUpperCase(), match[2].replace(/:([A-Za-z0-9_]+)/g, "{$1}"), file, Boolean(match[3]))) withheld += 1;
|
|
461
499
|
}
|
|
462
500
|
}
|
|
463
501
|
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 };
|
|
@@ -466,6 +504,7 @@ export async function POST() {
|
|
|
466
504
|
const operationCount = Object.values(doc.paths).reduce((n, item) => n + Object.keys(item).filter((m) => /^(get|post|put|patch|delete)$/.test(m)).length, 0);
|
|
467
505
|
out.set(specPath, JSON.stringify(doc, null, 2) + "\n");
|
|
468
506
|
if (!operationCount) notes.push("No supported API routes detected. The OpenAPI draft has no operations; add the real schema before connecting tools.");
|
|
507
|
+
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.`);
|
|
469
508
|
notes.push("Generated OpenAPI includes read and write routes. Review schemas before approving tools; new connections still default to GET-only access.");
|
|
470
509
|
notes.push("Verify the public spec URL is reachable without dashboard authentication before connecting it.");
|
|
471
510
|
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.");
|
|
@@ -492,8 +531,10 @@ ${notes.map((n) => "- " + n).join("\n")}
|
|
|
492
531
|
assertNoCommittedFrankieKeys(planned);
|
|
493
532
|
return { files: planned, root, chatPath, specPath, operationCount, needsAuthSetup, notes };
|
|
494
533
|
}
|
|
495
|
-
|
|
496
|
-
|
|
534
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
535
|
+
function addInstallOperation(paths, method, path, file, guarded) {
|
|
536
|
+
if (path.includes("*") || !path.startsWith("/") || /\/(?:auth|frankie)(?:\/|$)/.test(path)) return true;
|
|
537
|
+
if (WRITE_METHODS.has(method.toLowerCase()) && !guarded) return false;
|
|
497
538
|
(paths[path] ??= {})[method.toLowerCase()] = {
|
|
498
539
|
operationId: (method.toLowerCase() + "_" + path).replace(/[^a-zA-Z0-9_]+/g, "_"),
|
|
499
540
|
summary: method + " " + path,
|
|
@@ -502,6 +543,32 @@ function addInstallOperation(paths, method, path, file) {
|
|
|
502
543
|
responses: { "200": { description: "Successful response" } },
|
|
503
544
|
"x-frankie-review-required": true
|
|
504
545
|
};
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
function nextMiddlewareMatchers(files, sourceRoot, root) {
|
|
549
|
+
const candidates = [.../* @__PURE__ */ new Set([sourceRoot + "middleware.ts", sourceRoot + "middleware.js", root + "middleware.ts", root + "middleware.js"])];
|
|
550
|
+
for (const candidate of candidates) {
|
|
551
|
+
const content = files[candidate];
|
|
552
|
+
if (typeof content !== "string") continue;
|
|
553
|
+
const config = content.match(/export\s+const\s+config\s*=\s*\{([^}]*)\}/);
|
|
554
|
+
if (!config) continue;
|
|
555
|
+
const matcher = config[1].match(/matcher\s*:\s*(\[[^\]]*\]|["'`][^"'`]*["'`])/);
|
|
556
|
+
if (!matcher) return [];
|
|
557
|
+
const raw = matcher[1];
|
|
558
|
+
return raw.startsWith("[") ? [...raw.matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]) : [raw.slice(1, -1)];
|
|
559
|
+
}
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
function matchesAnyMatcher(matchers, path) {
|
|
563
|
+
return matchers.some((pattern) => {
|
|
564
|
+
if (/[()|]/.test(pattern)) return false;
|
|
565
|
+
const glob = pattern.replace(/:[A-Za-z0-9_]+\*/g, "**").replace(/:[A-Za-z0-9_]+\+?/g, "*");
|
|
566
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
567
|
+
const trailingDeep = escaped.endsWith("/**");
|
|
568
|
+
const body = trailingDeep ? escaped.slice(0, -3) : escaped;
|
|
569
|
+
const regexBody = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
|
|
570
|
+
return new RegExp(`^${regexBody}$`).test(path);
|
|
571
|
+
});
|
|
505
572
|
}
|
|
506
573
|
|
|
507
574
|
// src/sdk-updates.ts
|
|
@@ -528,20 +595,20 @@ jobs:
|
|
|
528
595
|
outputs:
|
|
529
596
|
base: $GH steps.base.outputs.sha }}
|
|
530
597
|
steps:
|
|
531
|
-
- uses: actions/checkout@
|
|
598
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
532
599
|
with:
|
|
533
600
|
persist-credentials: false
|
|
534
601
|
- id: base
|
|
535
602
|
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
536
|
-
- uses: actions/setup-node@
|
|
603
|
+
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
|
|
537
604
|
with:
|
|
538
|
-
node-version:
|
|
605
|
+
node-version: 24
|
|
539
606
|
- name: Install pinned pnpm
|
|
540
607
|
run: npm install --global --ignore-scripts pnpm@10.33.0
|
|
541
608
|
- name: Prepare SDK and lockfile together
|
|
542
609
|
run: |
|
|
543
610
|
test -f pnpm-lock.yaml
|
|
544
|
-
pnpm --recursive --include-workspace-root update '@frankieai/react@latest' '@frankieai/core@latest' '@frankieai/cli@latest' '@frankieai/server@latest' --lockfile-only --ignore-scripts --ignore-pnpmfile
|
|
611
|
+
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
|
|
545
612
|
- name: Package allowlisted changes
|
|
546
613
|
run: |
|
|
547
614
|
node --input-type=module <<'NODE'
|
|
@@ -557,7 +624,7 @@ jobs:
|
|
|
557
624
|
mkdirSync('frankie-update-artifact', {recursive:true});
|
|
558
625
|
writeFileSync('frankie-update-artifact/changes.json', JSON.stringify(files));
|
|
559
626
|
NODE
|
|
560
|
-
- uses: actions/upload-artifact@
|
|
627
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
561
628
|
with:
|
|
562
629
|
name: frankie-sdk-update
|
|
563
630
|
path: frankie-update-artifact/changes.json
|
|
@@ -571,11 +638,11 @@ jobs:
|
|
|
571
638
|
pull-requests: write
|
|
572
639
|
steps:
|
|
573
640
|
# Separate runner: no checkout, repository hooks, install scripts, or builds.
|
|
574
|
-
- uses: actions/download-artifact@
|
|
641
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
575
642
|
with:
|
|
576
643
|
name: frankie-sdk-update
|
|
577
644
|
path: artifact
|
|
578
|
-
- uses: actions/github-script@
|
|
645
|
+
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
|
|
579
646
|
env:
|
|
580
647
|
BASE_SHA: $GH needs.prepare.outputs.base }}
|
|
581
648
|
with:
|
|
@@ -592,6 +659,7 @@ jobs:
|
|
|
592
659
|
if (current.data.object.sha !== base) throw Error('Default branch changed. Run the updater again.');
|
|
593
660
|
const existing = await github.paginate(github.rest.pulls.list, {owner,repo,state:'open',per_page:100});
|
|
594
661
|
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; }
|
|
662
|
+
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);
|
|
595
663
|
const clean = value => {
|
|
596
664
|
const copy = JSON.parse(JSON.stringify(value));
|
|
597
665
|
for (const group of ['dependencies','devDependencies','optionalDependencies']) {
|
|
@@ -599,6 +667,7 @@ jobs:
|
|
|
599
667
|
}
|
|
600
668
|
return copy;
|
|
601
669
|
};
|
|
670
|
+
let sdkChanged = false;
|
|
602
671
|
const seen = new Set();
|
|
603
672
|
for (const file of files) {
|
|
604
673
|
if (!file || typeof file.path !== 'string' || typeof file.content !== 'string' || seen.has(file.path)) throw Error('Invalid update file');
|
|
@@ -609,14 +678,16 @@ jobs:
|
|
|
609
678
|
const original = await github.rest.repos.getContent({owner,repo,path:file.path,ref:base});
|
|
610
679
|
const before = JSON.parse(Buffer.from(original.data.content, 'base64').toString());
|
|
611
680
|
const after = JSON.parse(file.content);
|
|
612
|
-
if (
|
|
681
|
+
if (canonical(clean(before)) !== canonical(clean(after))) throw Error('Non-SDK manifest change in ' + file.path);
|
|
613
682
|
for (const group of ['dependencies','devDependencies','optionalDependencies']) {
|
|
614
683
|
const sdkKeys = value => Object.keys(value[group] || {}).filter(key => key.startsWith('@frankieai/')).sort();
|
|
615
684
|
if (JSON.stringify(sdkKeys(before)) !== JSON.stringify(sdkKeys(after))) throw Error('SDK dependency added or removed');
|
|
685
|
+
if (sdkKeys(after).some(key => before[group][key] !== after[group][key])) sdkChanged = true;
|
|
616
686
|
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');
|
|
617
687
|
}
|
|
618
688
|
}
|
|
619
689
|
}
|
|
690
|
+
if (!sdkChanged) { core.notice('Frankie SDK versions are unchanged; no formatting-only update PR is needed.'); return; }
|
|
620
691
|
if (!seen.has('pnpm-lock.yaml')) throw Error('SDK update must include the lockfile');
|
|
621
692
|
const parent = await github.rest.git.getCommit({owner,repo,commit_sha:base});
|
|
622
693
|
const tree = await github.rest.git.createTree({owner,repo,base_tree:parent.data.tree.sha,tree:files.map(f => ({...f,mode:'100644',type:'blob'}))});
|
|
@@ -664,15 +735,71 @@ function validateApiDocument(value) {
|
|
|
664
735
|
function operationId(method, path, op) {
|
|
665
736
|
return typeof op.operationId === "string" ? op.operationId : `${method}_${path.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "")}`;
|
|
666
737
|
}
|
|
738
|
+
function resolvePointer(doc, ref) {
|
|
739
|
+
let current = doc;
|
|
740
|
+
for (const segment of ref.slice(2).split("/")) {
|
|
741
|
+
if (!current || typeof current !== "object") return void 0;
|
|
742
|
+
current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
743
|
+
}
|
|
744
|
+
return current;
|
|
745
|
+
}
|
|
746
|
+
function reachableDefinitions(doc, roots) {
|
|
747
|
+
const resolved = {};
|
|
748
|
+
const visit = (node) => {
|
|
749
|
+
if (Array.isArray(node)) {
|
|
750
|
+
for (const entry of node) visit(entry);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (!node || typeof node !== "object") return;
|
|
754
|
+
for (const [key, value] of Object.entries(node)) {
|
|
755
|
+
if (key === "$ref" && typeof value === "string") {
|
|
756
|
+
if (value in resolved) continue;
|
|
757
|
+
const target = resolvePointer(doc, value);
|
|
758
|
+
resolved[value] = target ?? null;
|
|
759
|
+
visit(target);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
visit(value);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
for (const root of roots) visit(root);
|
|
766
|
+
return resolved;
|
|
767
|
+
}
|
|
667
768
|
function apiOperations(doc) {
|
|
668
769
|
const operations = /* @__PURE__ */ new Map();
|
|
669
|
-
const
|
|
670
|
-
for (const [path, item] of Object.entries(paths)) {
|
|
770
|
+
for (const [path, item] of Object.entries(doc.paths)) {
|
|
671
771
|
const parameters = Object.fromEntries(Object.entries(item).filter(([key]) => !apiMethods.includes(key)));
|
|
672
|
-
for (const method of apiMethods)
|
|
772
|
+
for (const method of apiMethods) {
|
|
773
|
+
const operation = item[method];
|
|
774
|
+
if (!operation) continue;
|
|
775
|
+
operations.set(method.toUpperCase() + " " + path, {
|
|
776
|
+
id: operationId(method, path, operation),
|
|
777
|
+
// Fingerprint what the operation's contract actually depends on: itself,
|
|
778
|
+
// the path item's shared fields, the definitions it reaches by `$ref`, and
|
|
779
|
+
// the document-level `security` that applies only where it declares none.
|
|
780
|
+
// Folding in the whole document instead meant one unrelated schema — or an
|
|
781
|
+
// `openapi` version bump — revoked every approval on the source.
|
|
782
|
+
// Document-level `servers` stays out deliberately: a refresh whose servers
|
|
783
|
+
// moved is already rejected unless the operator pinned an explicit baseUrl,
|
|
784
|
+
// and that override wins over the spec anyway (see agent/openapi.ts).
|
|
785
|
+
fingerprint: canonicalJson({
|
|
786
|
+
operation,
|
|
787
|
+
parameters,
|
|
788
|
+
security: operation["security"] === void 0 ? doc.security ?? null : null,
|
|
789
|
+
definitions: reachableDefinitions(doc, [operation, parameters])
|
|
790
|
+
})
|
|
791
|
+
});
|
|
792
|
+
}
|
|
673
793
|
}
|
|
674
794
|
return operations;
|
|
675
795
|
}
|
|
796
|
+
function matchPath(glob, path) {
|
|
797
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
798
|
+
const trailingDeep = escaped.endsWith("/**");
|
|
799
|
+
const body = trailingDeep ? escaped.slice(0, -3) : escaped;
|
|
800
|
+
const pattern = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
|
|
801
|
+
return new RegExp(`^${pattern}$`).test(path);
|
|
802
|
+
}
|
|
676
803
|
function compareApiDocuments(before, after) {
|
|
677
804
|
const old = apiOperations(before), next = apiOperations(after);
|
|
678
805
|
return {
|
|
@@ -702,6 +829,7 @@ export {
|
|
|
702
829
|
featuresConfigSchema,
|
|
703
830
|
frankieConfigSchema,
|
|
704
831
|
limitsConfigSchema,
|
|
832
|
+
matchPath,
|
|
705
833
|
mcpServerSchema,
|
|
706
834
|
modelChoiceSchema,
|
|
707
835
|
modelConfigSchema,
|