@frankieai/core 0.2.7 → 0.2.8

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.js CHANGED
@@ -449,7 +449,14 @@ var agentConfigSchema = z2.object({
449
449
  maxBudgetUsd: z2.number().positive().optional(),
450
450
  tools: toolPolicySchema.prefault({}),
451
451
  /** Show tool activity in the transcript. */
452
- showToolActivity: z2.boolean().default(true)
452
+ showToolActivity: z2.boolean().default(true),
453
+ /**
454
+ * Ask the user before any action that changes data runs. The chat shows the
455
+ * action with Approve and Cancel; it runs only on Approve, with exactly the
456
+ * inputs shown. Reads never ask. Applies to chats from an SDK that can show
457
+ * the card; older SDK versions and headless callers run as before.
458
+ */
459
+ confirmWrites: z2.boolean().default(true)
453
460
  });
454
461
  var authConfigSchema = z2.object({
455
462
  /**
@@ -581,7 +588,13 @@ var frankieConfigSchema = z2.object({
581
588
  * is the primary persona. Each entry resolves to its own one-agent config via
582
589
  * its own publishable key, so the data plane still sees one config per key.
583
590
  */
584
- personas: z2.array(personaProfileSchema).max(8).default([])
591
+ personas: z2.array(personaProfileSchema).max(8).default([]),
592
+ /**
593
+ * Which agent a *resolved* config is: absent for the primary (product)
594
+ * agent, the persona's id (support, docs, …) otherwise. Set only by
595
+ * resolvePersonaConfig at request time; never stored.
596
+ */
597
+ agentId: z2.string().min(1).max(64).optional()
585
598
  });
586
599
  var LEAKY_MODEL_MIGRATION = {
587
600
  "accounts/fireworks/models/kimi-k2p7-code": "accounts/fireworks/models/kimi-k3"
@@ -604,6 +617,7 @@ function resolvePersonaConfig(config, publishableKey) {
604
617
  return parseFrankieConfig({
605
618
  ...config,
606
619
  personas: [],
620
+ agentId: persona.id,
607
621
  branding: { ...config.branding, ...persona.branding ?? {} },
608
622
  auth: { ...config.auth, publishableKey: persona.publishableKey, allowAnonymous: persona.allowAnonymous ?? config.auth.allowAnonymous },
609
623
  pagePatterns: persona.pagePatterns,
@@ -1274,6 +1288,65 @@ function relativeImport(fromFile, module) {
1274
1288
  return up === 0 ? `./${rel}` : rel;
1275
1289
  }
1276
1290
 
1291
+ // src/install-prompt.ts
1292
+ function installPrompt(options = {}) {
1293
+ const workspace = options.workspaceId || "<workspace-id>";
1294
+ const key = options.publishableKey || "<publishable-key>";
1295
+ const anonymous = options.allowAnonymous ?? false;
1296
+ const cli = "npx @frankieai/cli@latest setup --workspace " + workspace + " --key " + key + (anonymous ? " --anonymous" : "");
1297
+ return `Add Frankie, an in-app AI assistant, to this app. Work in this repository only and keep the change small.
1298
+
1299
+ ## What Frankie is
1300
+ - The chat is a React component (@frankieai/react) that runs in this app's frontend.
1301
+ - The agent runs on Frankie's hosted cloud. Do not add a Frankie server, a model provider or any AI API key to this project.
1302
+ - Actions run in the user's browser against this app's own API, with the user's existing session. Do not add API credentials for Frankie.
1303
+ - Workspace ID: ${workspace}
1304
+ - Publishable key: ${key} (public, safe in the browser)
1305
+ - FRANKIE_JWT_SECRET is server-only. Never write its value anywhere, never print it, never commit it. Only reference it as an environment variable.
1306
+
1307
+ ## Steps
1308
+ 1. Inspect the repository: the framework, the package manager, and how users sign in today (Auth.js/NextAuth, Firebase session cookies, Supabase, Clerk, a custom session\u2026). In a monorepo, pick the app users sign in to and tell me which one you chose.
1309
+
1310
+ 2. If it is a Next.js App Router app, use Frankie's installer instead of writing files by hand:
1311
+ - Make sure the git tree is clean and the default branch is checked out.
1312
+ - Preview: ${cli} --dry-run
1313
+ - Monorepo: add --app <path-to-app>, for example --app apps/dashboard
1314
+ - Then run it with --no-pr to write the files and commit them on a new branch.
1315
+ - Read the generated FRANKIE_SETUP.md and app/api/frankie/token/route.ts. If the installer could not detect sign-in, implement getCurrentUser in lib/frankie-auth.ts using the app's existing session (see step 3b), and commit that too.
1316
+
1317
+ 3. Otherwise (any other React app), add it by hand:
1318
+ a. Install @frankieai/react with the project's package manager.
1319
+ b. Mount it once, at the root of the signed-in part of the app (a client component in frameworks that need one):
1320
+ import { FrankieProvider, FrankieWidget } from "@frankieai/react";
1321
+ import "@frankieai/react/styles.css";
1322
+ <FrankieProvider publishableKey={/* the public key from env */}${anonymous ? "" : `
1323
+ getUserToken={async () => {
1324
+ const res = await fetch("/api/frankie/token", { method: "POST" });
1325
+ return res.ok ? (await res.json()).token : undefined;
1326
+ }}`}>
1327
+ <FrankieWidget />
1328
+ </FrankieProvider>
1329
+ Read the key from a public environment variable using the framework's convention (NEXT_PUBLIC_FRANKIE_KEY, VITE_FRANKIE_KEY, \u2026).
1330
+ If the app's API is on a different origin than its pages, also pass api={{ baseUrl: "<that origin>", credentials: "include" }}.${anonymous ? "" : `
1331
+ c. Add a server endpoint POST /api/frankie/token on the app's own backend:
1332
+ - Look up the signed-in user with the session code the app already uses. Never invent a new auth system.
1333
+ - Not signed in: respond 401.
1334
+ - Signed in: respond { "token": "<jwt>" }, a JWT signed HS256 with process.env.FRANKIE_JWT_SECRET, subject = the user's stable id, optional name and email claims, expiring in 5 minutes (the "jose" package works well).`}
1335
+
1336
+ 4. Add the variables to .env.example (or the project's equivalent) with empty values and a one-line comment each: the public key variable${anonymous ? "" : " and FRANKIE_JWT_SECRET (server only)"}. Do not put real values in any committed file.
1337
+
1338
+ 5. Run the project's typecheck, lint and build, and fix anything this change broke. Do not change unrelated code.
1339
+
1340
+ 6. Finish with a short summary:
1341
+ - The files you added or changed, and the branch they are on.
1342
+ - What I still need to do:
1343
+ - Set the variables in my deployment and in my local env file. The values are in Frankie, under the workspace's Keys & settings.
1344
+ - Add every origin the app runs on, including local development like http://localhost:3000, to Allowed sites in Frankie (Keys & settings).
1345
+ - Push the branch and open a pull request titled "Install Frankie".
1346
+ - Connect this repository on the workspace's Install tab so Frankie tracks the pull request.
1347
+ - Anything you could not do, and why.`;
1348
+ }
1349
+
1277
1350
  // src/sdk-updates.ts
1278
1351
  function sdkUpdateWorkflow(mode) {
1279
1352
  if (mode !== "manual" && mode !== "automatic") throw new Error("Invalid update mode.");
@@ -1569,21 +1642,20 @@ function normalizeColor(raw) {
1569
1642
  }
1570
1643
  function cssBlocks(source) {
1571
1644
  const blocks = [];
1572
- let segmentStart = 0;
1573
- for (let at = 0; at < source.length; ) {
1574
- const open = source.indexOf("{", at);
1575
- if (open === -1) break;
1576
- const nextOpen = source.indexOf("{", open + 1);
1577
- const close = source.indexOf("}", open + 1);
1578
- if (close === -1) break;
1579
- if (nextOpen !== -1 && nextOpen < close) {
1580
- segmentStart = open + 1;
1581
- at = open + 1;
1582
- continue;
1645
+ let lastBrace = -1;
1646
+ let open = -1;
1647
+ let selectorStart = 0;
1648
+ for (let index = 0; index < source.length; index++) {
1649
+ const char = source.charCodeAt(index);
1650
+ if (char === 123) {
1651
+ selectorStart = lastBrace + 1;
1652
+ open = index;
1653
+ lastBrace = index;
1654
+ } else if (char === 125) {
1655
+ if (open !== -1) blocks.push([source.slice(selectorStart, open), source.slice(open + 1, index)]);
1656
+ open = -1;
1657
+ lastBrace = index;
1583
1658
  }
1584
- blocks.push([source.slice(segmentStart, open), source.slice(open + 1, close)]);
1585
- segmentStart = close + 1;
1586
- at = close + 1;
1587
1659
  }
1588
1660
  return blocks;
1589
1661
  }
@@ -1634,6 +1706,61 @@ function radiusOption(value) {
1634
1706
  const rem = match[2] === "px" ? parseFloat(match[1]) / 16 : parseFloat(match[1]);
1635
1707
  return rem < 0.525 ? "0.35" : rem < 0.9 ? "0.7" : "1.1";
1636
1708
  }
1709
+ function saturationLightness(hex) {
1710
+ const [r, g, b] = [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16) / 255);
1711
+ const max = Math.max(r, g, b);
1712
+ const min = Math.min(r, g, b);
1713
+ const lightness = (max + min) / 2;
1714
+ const saturation = max === min ? 0 : (max - min) / (1 - Math.abs(2 * lightness - 1));
1715
+ return [saturation, lightness];
1716
+ }
1717
+ function isVivid(hex) {
1718
+ if (!hex || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
1719
+ const [saturation, lightness] = saturationLightness(hex);
1720
+ return saturation >= 0.35 && lightness >= 0.2 && lightness <= 0.8;
1721
+ }
1722
+ var BUTTON_SELECTOR = /(?:^|[\s,>+~])(?:button|\.btn[\w-]*|\.button[\w-]*|\[type=["']?submit["']?\]|\.primary[\w-]*)(?![\w-])/i;
1723
+ var PAGE_SELECTOR = /^(?:html|body|:root)(?:\s*,\s*(?:html|body|:root))*$/i;
1724
+ function pageColors(css, vars) {
1725
+ const out = {};
1726
+ const source = stripComments(css);
1727
+ const declarations = (body) => body.split(";").filter((part) => part.length <= 300).map((part) => {
1728
+ const colon = part.indexOf(":");
1729
+ if (colon === -1) return void 0;
1730
+ let value = part.slice(colon + 1).trim();
1731
+ if (value.toLowerCase().endsWith("!important")) value = value.slice(0, -"!important".length).trim();
1732
+ return [part.slice(0, colon).trim().toLowerCase(), value];
1733
+ }).filter((entry) => Boolean(entry));
1734
+ const colorOf = (value) => resolve(value, vars) ?? value.split(/\s+(?![^(]*\))/).map((part) => resolve(part, vars)).find(Boolean);
1735
+ for (const [rawSelector, body] of cssBlocks(source)) {
1736
+ const selector = rawSelector.split(";").pop().trim();
1737
+ if (selector.length > 1e3 || /\.dark|\[data-theme|:hover|:active|:disabled|:focus/.test(selector)) continue;
1738
+ if (PAGE_SELECTOR.test(selector)) {
1739
+ for (const [property, value] of declarations(body)) {
1740
+ if (!out.background && (property === "background" || property === "background-color")) out.background = colorOf(value);
1741
+ if (!out.foreground && property === "color") out.foreground = colorOf(value);
1742
+ }
1743
+ } else if (!out.button && BUTTON_SELECTOR.test(selector) && !/ghost|outline|secondary|link|danger|red|cancel/i.test(selector)) {
1744
+ for (const [property, value] of declarations(body)) {
1745
+ if (property === "background" || property === "background-color" || /^--[\w-]*(?:bg|background)$/.test(property)) {
1746
+ const color = colorOf(value);
1747
+ if (isVivid(color)) {
1748
+ out.button = color;
1749
+ break;
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+ const counts = /* @__PURE__ */ new Map();
1756
+ const seen = /var\(\s*(--[\w-]+)\s*[,)]/g;
1757
+ for (let match = seen.exec(source); match; match = seen.exec(source)) {
1758
+ const color = resolve("var(" + match[1] + ")", vars);
1759
+ if (isVivid(color)) counts.set(color, (counts.get(color) ?? 0) + 1);
1760
+ }
1761
+ out.mostUsed = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
1762
+ return out;
1763
+ }
1637
1764
  var CSS_PRIORITY = [/(?:^|\/)app\/globals\.css$/, /(?:^|\/)styles\/globals\.css$/, /globals\.css$/, /\.css$/];
1638
1765
  var PRIMARY_VARS = ["--primary", "--color-primary", "--brand", "--color-brand", "--accent", "--color-accent"];
1639
1766
  function rank(path) {
@@ -1662,6 +1789,13 @@ function inferTheme(files, root = "") {
1662
1789
  for (const name of ["--background", "--color-background"]) if (vars.has(name)) set("background", resolve(vars.get(name), vars), varPath.get(name));
1663
1790
  for (const name of ["--foreground", "--color-foreground"]) if (vars.has(name)) set("foreground", resolve(vars.get(name), vars), varPath.get(name));
1664
1791
  if (vars.has("--radius")) set("radius", radiusOption(vars.get("--radius")), varPath.get("--radius"));
1792
+ const guessedPrimary = [];
1793
+ for (const path of cssFiles) {
1794
+ const used = pageColors(files[path], vars);
1795
+ set("background", used.background, path);
1796
+ set("foreground", used.foreground, path);
1797
+ guessedPrimary.push([used.button ?? used.mostUsed, path]);
1798
+ }
1665
1799
  for (const path of Object.keys(files).filter((p) => inRoot(p) && /(?:^|\/)tailwind\.config\.[cm]?[jt]s$/.test(p)).sort()) {
1666
1800
  const source = files[path];
1667
1801
  for (const key of ["primary", "brand", "accent"]) {
@@ -1683,10 +1817,241 @@ function inferTheme(files, root = "") {
1683
1817
  } catch {
1684
1818
  }
1685
1819
  }
1820
+ for (const [color, path] of guessedPrimary) set("primary", color, path);
1686
1821
  if (!theme.primary) return void 0;
1687
1822
  return { theme: { ...theme, primary: theme.primary }, sources };
1688
1823
  }
1824
+
1825
+ // src/conversation-log.ts
1826
+ function userLabel(user) {
1827
+ if (user.displayName?.trim()) return user.displayName.trim();
1828
+ if (user.email?.trim()) return user.email.trim();
1829
+ if (user.anonymous) return "Anonymous visitor";
1830
+ return "Signed-in user \xB7 " + shortId(user.id);
1831
+ }
1832
+ function shortId(id) {
1833
+ const plain = id.replace(/^[a-z]+[:_]/i, "");
1834
+ return plain.length > 8 ? plain.slice(0, 8) : plain;
1835
+ }
1836
+ function conversationLog(conversation, options = {}) {
1837
+ const { user, messages, agentMessages } = conversation;
1838
+ const tools = messages.flatMap(toolsOf);
1839
+ const failedTools = tools.filter((tool) => tool.status === "failed" || tool.status === "denied").length;
1840
+ const out = [];
1841
+ out.push("# Frankie conversation log", "");
1842
+ out.push(
1843
+ "Frankie is an AI assistant embedded in a customer's app. This log has the chat as the person saw it, then the agent's full trace: model messages, tool calls with their inputs, tool results and errors. Use it to explain what happened and why.",
1844
+ ""
1845
+ );
1846
+ out.push("## Summary", "");
1847
+ out.push(`- Conversation: ${conversation.title || "Untitled"} (\`${conversation.id}\`)`);
1848
+ if (options.workspace) out.push(`- Workspace: ${options.workspace.name} (\`${options.workspace.id}\`)`);
1849
+ out.push(`- Person: ${userLabel(user)} (\`${user.id}\`, ${user.anonymous ? "anonymous" : "signed in"})`);
1850
+ if (options.agent) out.push(`- Agent: ${options.agent}`);
1851
+ out.push(`- Started: ${iso(conversation.createdAt)}`);
1852
+ out.push(`- Last active: ${iso(conversation.updatedAt)}`);
1853
+ out.push(`- Messages: ${messages.length} \xB7 Actions: ${tools.length}${failedTools ? ` (${failedTools} failed)` : ""}`);
1854
+ out.push(`- Cost: ${conversation.costUsd > 0 ? "$" + conversation.costUsd.toFixed(4) : "not reported"}`);
1855
+ out.push(`- State: ${conversation.busy ? "still responding" : "finished"}`);
1856
+ out.push("");
1857
+ const escalations = conversation.escalations ?? [];
1858
+ if (escalations.length) {
1859
+ out.push("## Handed to the team", "");
1860
+ for (const escalation of escalations) {
1861
+ const contact = [escalation.name, escalation.email].filter(Boolean).join(", ");
1862
+ out.push(`- ${iso(escalation.at)}: ${escalation.reason}${contact ? ` (left ${contact})` : ""}`);
1863
+ if (escalation.summary) out.push(` ${escalation.summary.replace(/\n+/g, " ")}`);
1864
+ }
1865
+ out.push("");
1866
+ }
1867
+ out.push("## Chat (what the person saw)", "");
1868
+ if (!messages.length) out.push("_The chat widget saved no messages for this conversation._", "");
1869
+ for (const message of messages) {
1870
+ const item = record(message);
1871
+ if (!item) continue;
1872
+ const role = str(item.role) ?? "message";
1873
+ const speaker = role === "user" ? `Person (${userLabel(user)})` : role === "assistant" ? options.agent ?? "Agent" : role;
1874
+ const at = num(item.createdAt);
1875
+ out.push(`### ${speaker}${at ? ` \xB7 ${iso(at)}` : ""}`, "");
1876
+ const text = textOf(item);
1877
+ if (text) out.push(clip(text), "");
1878
+ for (const file of list(item.attachments)) out.push(`_Attached: ${str(file.name) ?? "file"} (${str(file.mimeType) ?? "unknown type"})_`);
1879
+ const messageTools = toolsOf(item);
1880
+ if (messageTools.length) {
1881
+ out.push("Actions:");
1882
+ for (const tool of messageTools) {
1883
+ const detail = str(tool.error) ?? str(tool.detail);
1884
+ out.push(`- [${str(tool.status) ?? "done"}] ${str(tool.label) ?? str(tool.name) ?? "tool"}${tool.name ? ` (\`${str(tool.name)}\`)` : ""}${detail ? `: ${oneLine(detail, 300)}` : ""}`);
1885
+ }
1886
+ }
1887
+ const error = str(item.error);
1888
+ if (error || item.status === "error") out.push("", `**This reply didn't finish:** ${error ?? "no error was recorded"}`);
1889
+ out.push("");
1890
+ }
1891
+ out.push("## Agent trace (model messages, tool calls and results)", "");
1892
+ if (!agentMessages.length) out.push("_The server recorded no agent messages for this conversation._", "");
1893
+ agentMessages.forEach((message, index) => {
1894
+ const item = record(message);
1895
+ if (!item) return;
1896
+ const role = str(item.role) ?? "message";
1897
+ const at = num(item.timestamp);
1898
+ if (role === "toolResult" || role === "tool_result") {
1899
+ const failed = item.isError === true;
1900
+ out.push(`### ${index + 1}. Tool result \xB7 \`${str(item.toolName) ?? "tool"}\`${failed ? " \xB7 ERROR" : ""}${at ? ` \xB7 ${iso(at)}` : ""}`, "");
1901
+ out.push(fence(contentText(item.content)), "");
1902
+ return;
1903
+ }
1904
+ const heading = [`${index + 1}. ${role === "assistant" ? "Assistant" : role === "user" ? "User" : role}`];
1905
+ if (role === "assistant") {
1906
+ if (str(item.model)) heading.push(str(item.model));
1907
+ if (str(item.stopReason)) heading.push(`stop: ${str(item.stopReason)}`);
1908
+ }
1909
+ if (at) heading.push(iso(at));
1910
+ out.push(`### ${heading.join(" \xB7 ")}`, "");
1911
+ const errorMessage = str(item.errorMessage) ?? str(item.error);
1912
+ if (errorMessage) out.push(`**Error:** ${errorMessage}`, "");
1913
+ if (typeof item.content === "string") {
1914
+ out.push(clip(item.content), "");
1915
+ return;
1916
+ }
1917
+ const before = out.length;
1918
+ for (const block of list(item.content)) {
1919
+ const type = str(block.type);
1920
+ if (type === "text") {
1921
+ if (str(block.text)) out.push(clip(str(block.text)), "");
1922
+ } else if (type === "thinking") {
1923
+ if (str(block.thinking) && block.redacted !== true) out.push("Thinking:", fence(str(block.thinking)), "");
1924
+ } else if (type === "toolCall") out.push(`Tool call \`${str(block.name) ?? "tool"}\`${str(block.id) ? ` (id \`${str(block.id)}\`)` : ""}:`, fence(json(block.arguments ?? {}), "json"), "");
1925
+ else if (type === "image") out.push(`_[image: ${str(block.mimeType) ?? "image"}, not included]_`, "");
1926
+ else if (type) out.push(`${type}:`, fence(json(stripLarge(block)), "json"), "");
1927
+ }
1928
+ if (out.length === before && !errorMessage) out.push("_(empty: no text and no tool calls)_", "");
1929
+ });
1930
+ return out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1931
+ }
1932
+ function conversationLogFileName(conversation) {
1933
+ const slug = (conversation.title || "conversation").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "conversation";
1934
+ return `frankie-${slug}-${conversation.id.slice(0, 12)}.md`;
1935
+ }
1936
+ var BLOCK_LIMIT = 12e3;
1937
+ function record(value) {
1938
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1939
+ }
1940
+ function list(value) {
1941
+ return Array.isArray(value) ? value.map(record).filter((item) => Boolean(item)) : [];
1942
+ }
1943
+ function str(value) {
1944
+ return typeof value === "string" && value.trim() ? value : void 0;
1945
+ }
1946
+ function num(value) {
1947
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1948
+ }
1949
+ function iso(at) {
1950
+ const date = new Date(at);
1951
+ return Number.isNaN(date.getTime()) ? "unknown time" : date.toISOString().replace(/\.\d{3}Z$/, "Z");
1952
+ }
1953
+ function toolsOf(message) {
1954
+ return list(record(message)?.tools);
1955
+ }
1956
+ function textOf(item) {
1957
+ if (typeof item.text === "string") return item.text;
1958
+ if (typeof item.content === "string") return item.content;
1959
+ return contentText(item.content);
1960
+ }
1961
+ function contentText(content) {
1962
+ if (typeof content === "string") return content;
1963
+ return list(content).map((block) => block.type === "image" ? `[image: ${str(block.mimeType) ?? "image"}, not included]` : str(block.text) ?? "").filter(Boolean).join("\n");
1964
+ }
1965
+ function stripLarge(block) {
1966
+ const out = {};
1967
+ for (const [key, value] of Object.entries(block)) {
1968
+ if (/signature$/i.test(key)) continue;
1969
+ out[key] = typeof value === "string" && value.length > 2e3 && /^[A-Za-z0-9+/=]+$/.test(value) ? `[${value.length} characters of binary data]` : value;
1970
+ }
1971
+ return out;
1972
+ }
1973
+ function json(value) {
1974
+ try {
1975
+ return JSON.stringify(value, null, 2) ?? String(value);
1976
+ } catch {
1977
+ return String(value);
1978
+ }
1979
+ }
1980
+ function clip(text, limit = BLOCK_LIMIT) {
1981
+ return text.length > limit ? `${text.slice(0, limit)}
1982
+ \u2026 [${text.length - limit} more characters cut]` : text;
1983
+ }
1984
+ function oneLine(text, limit) {
1985
+ const flat = text.replace(/\s+/g, " ").trim();
1986
+ return flat.length > limit ? `${flat.slice(0, limit)}\u2026` : flat;
1987
+ }
1988
+ function fence(text, lang = "") {
1989
+ const body = clip(text || "(empty)");
1990
+ const longest = Math.max(2, ...Array.from(body.matchAll(/`+/g), (match) => match[0].length));
1991
+ const ticks = "`".repeat(longest + 1);
1992
+ return `${ticks}${lang}
1993
+ ${body}
1994
+ ${ticks}`;
1995
+ }
1996
+
1997
+ // src/api-sync.ts
1998
+ var API_SYNC_WORKFLOW_PATH = ".github/workflows/frankie-api-sync.yml";
1999
+ function apiSyncWorkflow(options) {
2000
+ const { url, workspace, api, path } = options;
2001
+ if (!/^https:\/\/[a-z0-9.-]+(?::\d+)?(?:\/[\w./-]*)?$/i.test(url) && !/^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/.test(url)) throw new Error("Invalid Frankie URL.");
2002
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(workspace)) throw new Error("Invalid workspace id.");
2003
+ if (!/^[\w.-]{1,80}$/.test(api)) throw new Error("Invalid API name.");
2004
+ if (!isSafeRepositoryPath(path)) throw new Error("Choose a JSON or YAML file in the repository, like public/openapi.json.");
2005
+ return template2.replaceAll("__PATH__", path).replaceAll("__URL__", url.replace(/\/+$/, "")).replaceAll("__WORKSPACE__", workspace).replaceAll("__API__", api);
2006
+ }
2007
+ function isSafeRepositoryPath(path) {
2008
+ return /^[\w@.-]+(?:\/[\w@.-]+)*\.(?:json|ya?ml)$/i.test(path) && !path.split("/").some((part) => part === ".." || part === ".");
2009
+ }
2010
+ var template2 = String.raw`# Managed by Frankie: keeps Frankie's copy of your API description in step.
2011
+ # frankie-api-sync: __WORKSPACE__/__API__
2012
+ # Runs when __PATH__ changes on the default branch. No secrets: GitHub's OIDC
2013
+ # token proves which repository and commit this is. New or changed actions
2014
+ # stay off in Frankie until you allow them.
2015
+ name: Frankie API sync
2016
+ on:
2017
+ push:
2018
+ paths:
2019
+ - "__PATH__"
2020
+ workflow_dispatch:
2021
+ permissions:
2022
+ contents: read
2023
+ id-token: write
2024
+ concurrency:
2025
+ group: frankie-api-sync
2026
+ cancel-in-progress: true
2027
+ jobs:
2028
+ sync:
2029
+ if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
2030
+ runs-on: ubuntu-latest
2031
+ timeout-minutes: 5
2032
+ steps:
2033
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
2034
+ with:
2035
+ persist-credentials: false
2036
+ sparse-checkout: __PATH__
2037
+ sparse-checkout-cone-mode: false
2038
+ - name: Send the API description to Frankie
2039
+ env:
2040
+ FRANKIE_URL: "__URL__"
2041
+ FRANKIE_WORKSPACE: "__WORKSPACE__"
2042
+ FRANKIE_API: "__API__"
2043
+ SPEC_PATH: "__PATH__"
2044
+ run: |
2045
+ test -f "$SPEC_PATH"
2046
+ token="$(curl -sSf -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=frankie" | jq -r .value)"
2047
+ jq -n --arg workspace "$FRANKIE_WORKSPACE" --arg api "$FRANKIE_API" --arg path "$SPEC_PATH" --rawfile spec "$SPEC_PATH" \
2048
+ '{workspace: $workspace, api: $api, path: $path, spec: $spec}' > "$RUNNER_TEMP/frankie-api.json"
2049
+ curl -sS --fail-with-body -X POST "$FRANKIE_URL/github/api-sync" \
2050
+ -H "Authorization: Bearer $token" -H "content-type: application/json" \
2051
+ --data "@$RUNNER_TEMP/frankie-api.json"
2052
+ `;
1689
2053
  export {
2054
+ API_SYNC_WORKFLOW_PATH,
1690
2055
  FIREWORKS_MODELS,
1691
2056
  SKILL_TOOL_NAME,
1692
2057
  STANDARD_AGENT_IDS,
@@ -1697,12 +2062,15 @@ export {
1697
2062
  apiMethods,
1698
2063
  apiOperations,
1699
2064
  apiSourceSchema,
2065
+ apiSyncWorkflow,
1700
2066
  approvedSkillTools,
1701
2067
  assertNoCommittedFrankieKeys,
1702
2068
  authConfigSchema,
1703
2069
  brandingConfigSchema,
1704
2070
  canonicalJson,
1705
2071
  compareApiDocuments,
2072
+ conversationLog,
2073
+ conversationLogFileName,
1706
2074
  defineFrankieConfig,
1707
2075
  describeTool,
1708
2076
  draftSkill,
@@ -1713,7 +2081,9 @@ export {
1713
2081
  insertImport,
1714
2082
  installAgentIds,
1715
2083
  installBranch,
2084
+ installPrompt,
1716
2085
  installTitle,
2086
+ isSafeRepositoryPath,
1717
2087
  knowledgeBaseSchema,
1718
2088
  knowledgeSourceSchema,
1719
2089
  limitsConfigSchema,
@@ -1743,6 +2113,7 @@ export {
1743
2113
  thinkingLevelSchema,
1744
2114
  toolDescriptions,
1745
2115
  toolPolicySchema,
2116
+ userLabel,
1746
2117
  validateApiDocument,
1747
2118
  widgetThemeSchema,
1748
2119
  withDependencies