@frankieai/server 0.2.4 → 0.2.6

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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createControlApp
4
- } from "./chunk-YDXEBJON.js";
4
+ } from "./chunk-EYR3BV7U.js";
5
5
  import {
6
6
  createFrankieApp,
7
7
  resolveStores
8
- } from "./chunk-7J5Z5MIK.js";
8
+ } from "./chunk-WEHJH4FL.js";
9
9
 
10
10
  // src/control-server.ts
11
11
  import { serve } from "@hono/node-server";
@@ -17,10 +17,10 @@ async function main() {
17
17
  console.error("[frankie] Set FRANKIE_ADMIN_TOKEN to run the control plane.");
18
18
  process.exit(1);
19
19
  }
20
- const { tenants: store, sessions, describe } = await resolveStores();
20
+ const { tenants: store, sessions, knowledge, describe } = await resolveStores();
21
21
  const app = new Hono();
22
- app.route("/", createControlApp({ store, sessions }));
23
- app.route("/", createFrankieApp({ tenants: store, sessions }));
22
+ app.route("/", createControlApp({ store, sessions, knowledge }));
23
+ app.route("/", createFrankieApp({ tenants: store, sessions, knowledge }));
24
24
  serve({ fetch: app.fetch, port, hostname: host }, () => {
25
25
  console.log(
26
26
  JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/control-server.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The control plane, as its own process: the admin API the dashboard and the\n * setup CLI talk to. It writes tenants into FRANKIE_TENANTS_DIR — the same\n * directory the data-plane server reads — so what you create here is served\n * there with no further wiring.\n *\n * FRANKIE_ADMIN_TOKEN=… FRANKIE_TENANTS_DIR=./.frankie/tenants \\\n * pnpm --filter @frankieai/server start:control\n */\nimport { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport { createFrankieApp } from \"./app.js\";\nimport { createControlApp } from \"./control/app.js\";\nimport { resolveStores } from \"./control/resolve-store.js\";\n\nasync function main(): Promise<void> {\n const port = Number(process.env[\"PORT\"] ?? process.env[\"CONTROL_PORT\"] ?? 8081);\n const host = process.env[\"HOST\"] ?? \"0.0.0.0\";\n\n if (!process.env[\"FRANKIE_ADMIN_TOKEN\"]) {\n console.error(\"[frankie] Set FRANKIE_ADMIN_TOKEN to run the control plane.\");\n process.exit(1);\n }\n\n const { tenants: store, sessions, describe } = await resolveStores();\n const app = new Hono();\n app.route(\"/\", createControlApp({ store, sessions }));\n app.route(\"/\", createFrankieApp({ tenants: store, sessions }));\n\n serve({ fetch: app.fetch, port, hostname: host }, () => {\n console.log(\n JSON.stringify({\n ts: new Date().toISOString(),\n level: \"info\",\n event: \"control_listening\",\n store: describe,\n url: `http://${host}:${port}`,\n }),\n );\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;AAUA,SAAS,aAAa;AACtB,SAAS,YAAY;AAKrB,eAAe,OAAsB;AACnC,QAAM,OAAO,OAAO,QAAQ,IAAI,MAAM,KAAK,QAAQ,IAAI,cAAc,KAAK,IAAI;AAC9E,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AAEpC,MAAI,CAAC,QAAQ,IAAI,qBAAqB,GAAG;AACvC,YAAQ,MAAM,6DAA6D;AAC3E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,EAAE,SAAS,OAAO,UAAU,SAAS,IAAI,MAAM,cAAc;AACnE,QAAM,MAAM,IAAI,KAAK;AACrB,MAAI,MAAM,KAAK,iBAAiB,EAAE,OAAO,SAAS,CAAC,CAAC;AACpD,MAAI,MAAM,KAAK,iBAAiB,EAAE,SAAS,OAAO,SAAS,CAAC,CAAC;AAE7D,QAAM,EAAE,OAAO,IAAI,OAAO,MAAM,UAAU,KAAK,GAAG,MAAM;AACtD,YAAQ;AAAA,MACN,KAAK,UAAU;AAAA,QACb,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK;","names":[]}
1
+ {"version":3,"sources":["../src/control-server.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The control plane, as its own process: the admin API the dashboard and the\n * setup CLI talk to. It writes tenants into FRANKIE_TENANTS_DIR — the same\n * directory the data-plane server reads — so what you create here is served\n * there with no further wiring.\n *\n * FRANKIE_ADMIN_TOKEN=… FRANKIE_TENANTS_DIR=./.frankie/tenants \\\n * pnpm --filter @frankieai/server start:control\n */\nimport { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport { createFrankieApp } from \"./app.js\";\nimport { createControlApp } from \"./control/app.js\";\nimport { resolveStores } from \"./control/resolve-store.js\";\n\nasync function main(): Promise<void> {\n const port = Number(process.env[\"PORT\"] ?? process.env[\"CONTROL_PORT\"] ?? 8081);\n const host = process.env[\"HOST\"] ?? \"0.0.0.0\";\n\n if (!process.env[\"FRANKIE_ADMIN_TOKEN\"]) {\n console.error(\"[frankie] Set FRANKIE_ADMIN_TOKEN to run the control plane.\");\n process.exit(1);\n }\n\n const { tenants: store, sessions, knowledge, describe } = await resolveStores();\n const app = new Hono();\n app.route(\"/\", createControlApp({ store, sessions, knowledge }));\n app.route(\"/\", createFrankieApp({ tenants: store, sessions, knowledge }));\n\n serve({ fetch: app.fetch, port, hostname: host }, () => {\n console.log(\n JSON.stringify({\n ts: new Date().toISOString(),\n level: \"info\",\n event: \"control_listening\",\n store: describe,\n url: `http://${host}:${port}`,\n }),\n );\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;AAUA,SAAS,aAAa;AACtB,SAAS,YAAY;AAKrB,eAAe,OAAsB;AACnC,QAAM,OAAO,OAAO,QAAQ,IAAI,MAAM,KAAK,QAAQ,IAAI,cAAc,KAAK,IAAI;AAC9E,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AAEpC,MAAI,CAAC,QAAQ,IAAI,qBAAqB,GAAG;AACvC,YAAQ,MAAM,6DAA6D;AAC3E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,EAAE,SAAS,OAAO,UAAU,WAAW,SAAS,IAAI,MAAM,cAAc;AAC9E,QAAM,MAAM,IAAI,KAAK;AACrB,MAAI,MAAM,KAAK,iBAAiB,EAAE,OAAO,UAAU,UAAU,CAAC,CAAC;AAC/D,MAAI,MAAM,KAAK,iBAAiB,EAAE,SAAS,OAAO,UAAU,UAAU,CAAC,CAAC;AAExE,QAAM,EAAE,OAAO,IAAI,OAAO,MAAM,UAAU,KAAK,GAAG,MAAM;AACtD,YAAQ;AAAA,MACN,KAAK,UAAU;AAAA,QACb,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK;","names":[]}
package/dist/index.d.ts CHANGED
@@ -112,6 +112,26 @@ declare function safeEqual(a: string, b: string): boolean;
112
112
  /** Stable pseudonymous id for an anonymous visitor. */
113
113
  declare function anonymousUser(seed: string): VerifiedUser;
114
114
 
115
+ /** One recorded hand-off to the workspace's team. Stored on the conversation. */
116
+ interface EscalationRecord {
117
+ id: string;
118
+ at: number;
119
+ /** Why the agent escalated (its own words / the visitor's ask). */
120
+ reason: string;
121
+ /** A short recap of what the visitor needs and what was already tried. */
122
+ summary: string;
123
+ /** How to reach the visitor, when they provided it. */
124
+ email?: string;
125
+ name?: string;
126
+ }
127
+ /** What the tool hands back to the app: the record plus who/where it came from. */
128
+ interface EscalationEvent extends EscalationRecord {
129
+ tenantId: string;
130
+ conversationId: string;
131
+ userId: string;
132
+ }
133
+ type EscalationSink = (event: EscalationEvent) => Promise<void>;
134
+
115
135
  interface ChatSession {
116
136
  id: string;
117
137
  tenantId: string;
@@ -130,7 +150,17 @@ interface ChatSession {
130
150
  lastActiveAt: number;
131
151
  messageCount: number;
132
152
  costUsd: number;
153
+ /** True while a chat turn holds this session; see `busySince` for how long. */
133
154
  busy: boolean;
155
+ /**
156
+ * When the running turn took the lock. A turn can only hold the session for
157
+ * as long as its timeout, so a lock older than that is one whose release
158
+ * never landed (the instance was frozen or restarted mid-turn) and is free
159
+ * for the taking — otherwise the session would answer 409 forever.
160
+ */
161
+ busySince?: number;
162
+ /** Human hand-offs the agent made in this conversation (the escalation inbox). */
163
+ escalations?: EscalationRecord[];
134
164
  }
135
165
  interface SessionStore {
136
166
  create(session: ChatSession): Promise<void>;
@@ -163,6 +193,41 @@ declare class MemorySessionStore implements SessionStore {
163
193
  }
164
194
  declare function newSessionId(): string;
165
195
 
196
+ /** One indexed passage of a document, with its embedding vector. */
197
+ interface KnowledgeChunk {
198
+ id: string;
199
+ url: string;
200
+ title: string;
201
+ text: string;
202
+ embedding: number[];
203
+ }
204
+ /** A retrieval result: the passage and how close it was to the query. */
205
+ interface KnowledgeHit {
206
+ url: string;
207
+ title: string;
208
+ text: string;
209
+ score: number;
210
+ }
211
+ /** What the dashboard shows about a workspace's index. */
212
+ interface KnowledgeStats {
213
+ chunks: number;
214
+ pages: number;
215
+ sources: string[];
216
+ ingestedAt?: number;
217
+ }
218
+ interface KnowledgeStore {
219
+ /** Replace a workspace's whole index with a freshly ingested set (atomic swap). */
220
+ replace(tenantId: string, chunks: KnowledgeChunk[], meta: {
221
+ sources: string[];
222
+ pages: number;
223
+ ingestedAt: number;
224
+ }): Promise<void>;
225
+ /** Top-k passages by cosine similarity to the query vector. */
226
+ search(tenantId: string, queryVector: readonly number[], k: number): Promise<KnowledgeHit[]>;
227
+ stats(tenantId: string): Promise<KnowledgeStats>;
228
+ clear(tenantId: string): Promise<void>;
229
+ }
230
+
166
231
  declare class ModelResolutionError extends Error {
167
232
  }
168
233
  interface ResolvedModel {
@@ -249,7 +314,7 @@ declare class SingleTenantStore implements TenantStore {
249
314
  declare class MemoryTenantStore implements TenantStore {
250
315
  private readonly byKey;
251
316
  constructor(tenants?: ReadonlyArray<ResolvedConfig>);
252
- /** Register (or replace) a tenant. Its publishable key is its lookup key. */
317
+ /** Register (or replace) a tenant, under its base key and each persona's key. */
253
318
  add(resolved: ResolvedConfig): void;
254
319
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
255
320
  }
@@ -260,6 +325,8 @@ interface CreateAppOptions {
260
325
  /** Many tenants, each resolved by publishable key at request time — the SaaS case. */
261
326
  tenants?: TenantStore | TenantRegistry;
262
327
  sessions?: SessionStore;
328
+ /** Knowledge-base index for the docs-search tool; in-memory when omitted. */
329
+ knowledge?: KnowledgeStore;
263
330
  rateLimiter?: RateLimiter;
264
331
  logger?: (event: Record<string, unknown>) => void;
265
332
  }
@@ -275,6 +342,8 @@ interface McpHostOptions {
275
342
  logger?: (event: Record<string, unknown>) => void;
276
343
  /** Injected in tests so generated API tools hit a stub rather than the network. */
277
344
  fetchImpl?: typeof fetch;
345
+ /** Shared with the chat API; each request counts against the tenant's message budget. */
346
+ rateLimiter?: RateLimiter;
278
347
  }
279
348
  declare function createMcpHostApp(options: McpHostOptions): Hono;
280
349
 
@@ -302,6 +371,10 @@ interface RepositoryInstallRecord {
302
371
  pullRequestState?: "draft" | "open" | "closed" | "merged";
303
372
  checkRollup?: "success" | "pending" | "failure" | "none";
304
373
  checkedAt?: string;
374
+ /** The current integration stage (see apps/web/lib/integration-status.ts), so Overview renders it on first paint. */
375
+ stage?: string;
376
+ /** The fully-integrated checklist rows that are done (apps/web/lib/integration-checklist.ts) — the funnel position. */
377
+ checklist?: string[];
305
378
  updatedAt: string;
306
379
  }
307
380
  /**
@@ -322,6 +395,21 @@ interface VercelLinkRecord {
322
395
  }
323
396
  /** `VercelLinkRecord` with the token stripped — what the dashboard sees. */
324
397
  type PublicVercelLink = Omit<VercelLinkRecord, "accessToken">;
398
+ /** What the installer read from the repository's own styling (see @frankieai/core inferTheme). */
399
+ interface SuggestedThemeRecord {
400
+ theme: {
401
+ primary: string;
402
+ background?: string;
403
+ foreground?: string;
404
+ radius?: string;
405
+ };
406
+ sources: Array<{
407
+ field: "primary" | "background" | "foreground" | "radius";
408
+ path: string;
409
+ }>;
410
+ repository: string;
411
+ inferredAt: string;
412
+ }
325
413
  interface TenantRecord {
326
414
  apiSpecSync?: Record<string, {
327
415
  repository: string;
@@ -343,8 +431,19 @@ interface TenantRecord {
343
431
  sessionSecret: string;
344
432
  /** When true, the workspace is protected: it cannot be deleted until unlocked. */
345
433
  locked?: boolean;
434
+ /** When true, the dashboard applies a changed repository spec on its own (new operations still land unapproved). */
435
+ apiAutoRefresh?: boolean;
436
+ /** A widget theme inferred from the repository at install time, awaiting Accept / Keep in the Theme editor. */
437
+ suggestedTheme?: SuggestedThemeRecord;
346
438
  /** Durable repository-install state surfaced in the dashboard and setup chat. */
347
439
  repositoryInstall?: RepositoryInstallRecord;
440
+ /**
441
+ * Per-persona repository-install state, keyed by persona id. The primary
442
+ * agent keeps `repositoryInstall`; each embedded persona (see
443
+ * `config.personas`) gets its own install record here, so a workspace can
444
+ * open a separate scoped-install PR per agent.
445
+ */
446
+ personaInstalls?: Record<string, RepositoryInstallRecord>;
348
447
  /** Deployment provider connection, when the workspace has connected Vercel. */
349
448
  vercel?: VercelLinkRecord;
350
449
  createdAt: string;
@@ -407,14 +506,23 @@ interface PublicTenant {
407
506
  }>;
408
507
  /** Protected from deletion until unlocked. */
409
508
  locked: boolean;
509
+ /** Apply a changed repository spec without a click (see TenantRecord). */
510
+ apiAutoRefresh: boolean;
511
+ suggestedTheme?: SuggestedThemeRecord;
410
512
  /** A submitted or in-progress SDK installation for this workspace. */
411
513
  repositoryInstall?: RepositoryInstallRecord;
514
+ /** A submitted or in-progress scoped-install PR per persona id (`config.personas`). */
515
+ personaInstalls?: Record<string, RepositoryInstallRecord>;
412
516
  /** The connected Vercel installation and matched project — never its token. */
413
517
  vercel?: PublicVercelLink;
414
518
  /** The customer's brand theme for the widget, so the dashboard can edit it. */
415
519
  theme: TenantRecord["config"]["theme"];
416
520
  /** Opt-in chat capabilities (attachments, model picker) for the dashboard to edit. */
417
521
  features: TenantRecord["config"]["features"];
522
+ /** Documentation sources indexed for the knowledge-base agent, for the dashboard to edit. */
523
+ knowledgeBase: TenantRecord["config"]["knowledgeBase"];
524
+ /** Embedded page-scoped personas (extra agents) on this workspace, for the dashboard to edit. Their publishable keys are public. */
525
+ personas: TenantRecord["config"]["personas"];
418
526
  /** Editable chat copy: the greeting and the starter cards shown on the empty screen. */
419
527
  agent: {
420
528
  greeting: string;
@@ -453,6 +561,27 @@ interface PlatformAdminRecord {
453
561
  grantedBy: string;
454
562
  createdAt: string;
455
563
  }
564
+ /** Billing plan tiers. Free is the default; Pro lifts the workspace cap. */
565
+ type Plan = "free" | "pro";
566
+ /**
567
+ * A per-org (owner id) billing subscription. One org, one subscription,
568
+ * covering all its workspaces and members — matching the model where inviting
569
+ * someone grants access to every workspace. The Stripe ids are written only by
570
+ * the webhook (platform admin), never by the org itself, so an org can't
571
+ * self-upgrade. Absent record ⇒ the Free plan.
572
+ */
573
+ interface SubscriptionRecord {
574
+ /** The org's owner id, e.g. "github:42". */
575
+ org: string;
576
+ plan: Plan;
577
+ /** Mirrors the Stripe subscription status; anything but active/trialing is treated as Free. */
578
+ status: "active" | "trialing" | "past_due" | "canceled" | "incomplete";
579
+ stripeCustomerId?: string;
580
+ stripeSubscriptionId?: string;
581
+ /** ISO timestamp the current paid period ends, for display. */
582
+ currentPeriodEnd?: string;
583
+ updatedAt: string;
584
+ }
456
585
  interface ControlStore extends TenantStore {
457
586
  listOrganizations(): Promise<string[]>;
458
587
  listPlatformAdmins(): Promise<PlatformAdminRecord[]>;
@@ -473,6 +602,10 @@ interface ControlStore extends TenantStore {
473
602
  deleteMember(org: string, login: string): Promise<void>;
474
603
  /** Every org membership held by a given login, across all orgs. */
475
604
  membershipsForLogin(login: string): Promise<MembershipRecord[]>;
605
+ /** The org's billing subscription, or null for an org on the Free plan. */
606
+ getSubscription(org: string): Promise<SubscriptionRecord | null>;
607
+ /** Create or replace an org's subscription. Written by the Stripe webhook. */
608
+ putSubscription(subscription: SubscriptionRecord): Promise<void>;
476
609
  }
477
610
  /**
478
611
  * The control plane's persistence, and at the same time a `TenantStore` the
@@ -495,6 +628,7 @@ declare class FileTenantStore implements ControlStore {
495
628
  private index;
496
629
  private byId;
497
630
  private members;
631
+ private subscriptions;
498
632
  private loaded;
499
633
  private static memberKey;
500
634
  constructor(options: {
@@ -508,6 +642,9 @@ declare class FileTenantStore implements ControlStore {
508
642
  putMember(member: MembershipRecord): Promise<void>;
509
643
  deleteMember(org: string, login: string): Promise<void>;
510
644
  membershipsForLogin(login: string): Promise<MembershipRecord[]>;
645
+ private persistSubscriptions;
646
+ getSubscription(org: string): Promise<SubscriptionRecord | null>;
647
+ putSubscription(subscription: SubscriptionRecord): Promise<void>;
511
648
  private remember;
512
649
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
513
650
  /** All tenants, or — when an owner is given — only that owner's. */
@@ -515,6 +652,12 @@ declare class FileTenantStore implements ControlStore {
515
652
  get(id: string): Promise<TenantRecord | null>;
516
653
  put(record: TenantRecord): Promise<void>;
517
654
  compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
655
+ /**
656
+ * A record holds the workspace's signing and session secrets and source
657
+ * credentials: owner-only permissions, and written whole (temp file, then
658
+ * rename) so a crash never leaves half a record behind.
659
+ */
660
+ private write;
518
661
  delete(id: string): Promise<void>;
519
662
  }
520
663
 
@@ -557,6 +700,7 @@ declare class PostgresTenantStore implements ControlStore {
557
700
  private ensureReady;
558
701
  /** Create the table if it isn't there yet. Safe to call repeatedly. */
559
702
  init(): Promise<void>;
703
+ private subscriptionRow;
560
704
  private memberRow;
561
705
  private adminRow;
562
706
  listOrganizations(): Promise<string[]>;
@@ -567,6 +711,8 @@ declare class PostgresTenantStore implements ControlStore {
567
711
  listMembers(org: string): Promise<MembershipRecord[]>;
568
712
  putMember(member: MembershipRecord): Promise<void>;
569
713
  deleteMember(org: string, login: string): Promise<void>;
714
+ getSubscription(org: string): Promise<SubscriptionRecord | null>;
715
+ putSubscription(subscription: SubscriptionRecord): Promise<void>;
570
716
  membershipsForLogin(login: string): Promise<MembershipRecord[]>;
571
717
  private parse;
572
718
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
@@ -584,6 +730,7 @@ interface ResolvedStore {
584
730
  interface ResolvedStores {
585
731
  tenants: ControlStore;
586
732
  sessions: SessionStore;
733
+ knowledge: KnowledgeStore;
587
734
  describe: string;
588
735
  }
589
736
  /**
@@ -641,7 +788,8 @@ interface SkillGenerationInput {
641
788
  */
642
789
  toolDetails?: Array<{
643
790
  id: string;
644
- description: string;
791
+ description: string; /** What the tool takes (`describeInputs`), when the inventory knows. */
792
+ inputs?: string;
645
793
  }>;
646
794
  /** Present when refining an existing skill rather than starting fresh. */
647
795
  instructions?: string;
@@ -655,6 +803,8 @@ interface ControlOptions {
655
803
  store: ControlStore;
656
804
  /** Durable session store shared with the data plane when Postgres is configured. */
657
805
  sessions?: SessionStore;
806
+ /** Knowledge index shared with the data plane, so a dashboard-triggered ingest is what the docs-search tool reads. */
807
+ knowledge?: KnowledgeStore;
658
808
  /** Bearer token that gates the admin API. A stand-in for real user auth. */
659
809
  adminToken?: string;
660
810
  /**
@@ -679,6 +829,26 @@ interface ControlOptions {
679
829
  */
680
830
  declare function createControlApp(options: ControlOptions): Hono;
681
831
 
832
+ /** Where and how to compute embeddings — an OpenAI-compatible `/embeddings` endpoint. */
833
+ interface EmbeddingConfig {
834
+ endpoint: string;
835
+ apiKey: string;
836
+ model: string;
837
+ }
838
+ /**
839
+ * Embed a batch of texts. Sends them in bounded requests so a large ingest
840
+ * doesn't build one enormous body. Returns one vector per input, in order.
841
+ */
842
+ declare function embedTexts(texts: string[], config: EmbeddingConfig, fetchImpl?: typeof fetch, batchSize?: number): Promise<number[][]>;
843
+
844
+ /** Everything the retrieval tool needs, injected by the runner from the app. */
845
+ interface KnowledgeDeps {
846
+ store: KnowledgeStore;
847
+ embedding: EmbeddingConfig;
848
+ /** Override the embed call in tests. */
849
+ embedImpl?: typeof embedTexts;
850
+ }
851
+
682
852
  /**
683
853
  * The per-turn handle an API tool uses to run itself in the user's browser
684
854
  * instead of on Frankie's server. `available` says whether a live widget
@@ -713,7 +883,11 @@ interface RunTurnParams {
713
883
  signal: AbortSignal;
714
884
  /** Called with the full transcript once the turn settles, to persist it. */
715
885
  onTranscript?: (messages: AgentMessage[]) => void;
886
+ /** Receives a human-escalation when the built-in escalation tool is called. */
887
+ onEscalation?: EscalationSink;
716
888
  onWarning?: (message: string, detail: Record<string, unknown>) => void;
889
+ /** Knowledge-base index + embedding config for the docs-search tool. */
890
+ knowledge?: KnowledgeDeps;
717
891
  }
718
892
  /**
719
893
  * Run one user turn and stream it back as widget-facing events.
@@ -808,6 +982,10 @@ interface CollectToolsOptions {
808
982
  accountApiUrl?: string;
809
983
  /** Browser channel for API sources marked `execution: "client"`. */
810
984
  clientBridge?: ClientToolBridge;
985
+ /** Knowledge-base index + embedding config for the docs-search tool. */
986
+ knowledge?: KnowledgeDeps;
987
+ /** Receives a human-escalation when the built-in escalation tool is called. */
988
+ onEscalation?: EscalationSink;
811
989
  onWarning?: (message: string, detail: Record<string, unknown>) => void;
812
990
  }
813
991
  interface CollectedTools {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createControlApp
3
- } from "./chunk-YDXEBJON.js";
3
+ } from "./chunk-EYR3BV7U.js";
4
4
  import {
5
5
  ConfigError,
6
6
  applyEnvOverrides,
@@ -42,7 +42,7 @@ import {
42
42
  runTurn,
43
43
  safeEqual,
44
44
  toPublicTenant
45
- } from "./chunk-7J5Z5MIK.js";
45
+ } from "./chunk-WEHJH4FL.js";
46
46
  export {
47
47
  API_TOOL_PREFIX,
48
48
  AuthError,
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  ModelResolutionError,
8
8
  createFrankieApp,
9
9
  resolveStores
10
- } from "./chunk-7J5Z5MIK.js";
10
+ } from "./chunk-WEHJH4FL.js";
11
11
 
12
12
  // src/server.ts
13
13
  import { serve } from "@hono/node-server";
@@ -21,7 +21,7 @@ async function main() {
21
21
  if (multiTenant) {
22
22
  const resolved = await resolveStores();
23
23
  describe = `multi-tenant (${resolved.describe})`;
24
- app = createFrankieApp({ tenants: resolved.tenants, sessions: resolved.sessions });
24
+ app = createFrankieApp({ tenants: resolved.tenants, sessions: resolved.sessions, knowledge: resolved.knowledge });
25
25
  } else {
26
26
  app = createFrankieApp({ resolved: await loadResolvedConfig() });
27
27
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { serve } from \"@hono/node-server\";\nimport { createFrankieApp } from \"./app.js\";\nimport { ModelResolutionError } from \"./agent/models.js\";\nimport { ConfigError, loadResolvedConfig } from \"./config.js\";\nimport { resolveStores } from \"./control/resolve-store.js\";\n\nasync function main(): Promise<void> {\n const port = Number(process.env[\"PORT\"] ?? 8080);\n const host = process.env[\"HOST\"] ?? \"0.0.0.0\";\n\n // Two shapes, one binary. With DATABASE_URL (or FRANKIE_TENANTS_DIR) set, this\n // serves every tenant the control plane wrote, resolved by publishable key —\n // the hosted, multi-tenant shape. Otherwise it loads one frankie.config.json,\n // the self-hosted, single-tenant shape.\n const multiTenant = Boolean(process.env[\"DATABASE_URL\"] || process.env[\"FRANKIE_TENANTS_DIR\"]);\n let app;\n let describe = \"single-tenant\";\n try {\n if (multiTenant) {\n const resolved = await resolveStores();\n describe = `multi-tenant (${resolved.describe})`;\n app = createFrankieApp({ tenants: resolved.tenants, sessions: resolved.sessions });\n } else {\n app = createFrankieApp({ resolved: await loadResolvedConfig() });\n }\n } catch (error) {\n if (error instanceof ConfigError || error instanceof ModelResolutionError) {\n console.error(`[frankie] ${error.message}`);\n process.exit(1);\n }\n throw error;\n }\n\n serve({ fetch: app.fetch, port, hostname: host }, () => {\n console.log(\n JSON.stringify({\n ts: new Date().toISOString(),\n level: \"info\",\n event: \"listening\",\n mode: describe,\n url: `http://${host}:${port}`,\n }),\n );\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;;;AACA,SAAS,aAAa;AAMtB,eAAe,OAAsB;AACnC,QAAM,OAAO,OAAO,QAAQ,IAAI,MAAM,KAAK,IAAI;AAC/C,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AAMpC,QAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK,QAAQ,IAAI,qBAAqB,CAAC;AAC7F,MAAI;AACJ,MAAI,WAAW;AACf,MAAI;AACF,QAAI,aAAa;AACf,YAAM,WAAW,MAAM,cAAc;AACrC,iBAAW,iBAAiB,SAAS,QAAQ;AAC7C,YAAM,iBAAiB,EAAE,SAAS,SAAS,SAAS,UAAU,SAAS,SAAS,CAAC;AAAA,IACnF,OAAO;AACL,YAAM,iBAAiB,EAAE,UAAU,MAAM,mBAAmB,EAAE,CAAC;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe,iBAAiB,sBAAsB;AACzE,cAAQ,MAAM,aAAa,MAAM,OAAO,EAAE;AAC1C,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM;AAAA,EACR;AAEA,QAAM,EAAE,OAAO,IAAI,OAAO,MAAM,UAAU,KAAK,GAAG,MAAM;AACtD,YAAQ;AAAA,MACN,KAAK,UAAU;AAAA,QACb,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { serve } from \"@hono/node-server\";\nimport { createFrankieApp } from \"./app.js\";\nimport { ModelResolutionError } from \"./agent/models.js\";\nimport { ConfigError, loadResolvedConfig } from \"./config.js\";\nimport { resolveStores } from \"./control/resolve-store.js\";\n\nasync function main(): Promise<void> {\n const port = Number(process.env[\"PORT\"] ?? 8080);\n const host = process.env[\"HOST\"] ?? \"0.0.0.0\";\n\n // Two shapes, one binary. With DATABASE_URL (or FRANKIE_TENANTS_DIR) set, this\n // serves every tenant the control plane wrote, resolved by publishable key —\n // the hosted, multi-tenant shape. Otherwise it loads one frankie.config.json,\n // the self-hosted, single-tenant shape.\n const multiTenant = Boolean(process.env[\"DATABASE_URL\"] || process.env[\"FRANKIE_TENANTS_DIR\"]);\n let app;\n let describe = \"single-tenant\";\n try {\n if (multiTenant) {\n const resolved = await resolveStores();\n describe = `multi-tenant (${resolved.describe})`;\n app = createFrankieApp({ tenants: resolved.tenants, sessions: resolved.sessions, knowledge: resolved.knowledge });\n } else {\n app = createFrankieApp({ resolved: await loadResolvedConfig() });\n }\n } catch (error) {\n if (error instanceof ConfigError || error instanceof ModelResolutionError) {\n console.error(`[frankie] ${error.message}`);\n process.exit(1);\n }\n throw error;\n }\n\n serve({ fetch: app.fetch, port, hostname: host }, () => {\n console.log(\n JSON.stringify({\n ts: new Date().toISOString(),\n level: \"info\",\n event: \"listening\",\n mode: describe,\n url: `http://${host}:${port}`,\n }),\n );\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;;;AACA,SAAS,aAAa;AAMtB,eAAe,OAAsB;AACnC,QAAM,OAAO,OAAO,QAAQ,IAAI,MAAM,KAAK,IAAI;AAC/C,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AAMpC,QAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK,QAAQ,IAAI,qBAAqB,CAAC;AAC7F,MAAI;AACJ,MAAI,WAAW;AACf,MAAI;AACF,QAAI,aAAa;AACf,YAAM,WAAW,MAAM,cAAc;AACrC,iBAAW,iBAAiB,SAAS,QAAQ;AAC7C,YAAM,iBAAiB,EAAE,SAAS,SAAS,SAAS,UAAU,SAAS,UAAU,WAAW,SAAS,UAAU,CAAC;AAAA,IAClH,OAAO;AACL,YAAM,iBAAiB,EAAE,UAAU,MAAM,mBAAmB,EAAE,CAAC;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe,iBAAiB,sBAAsB;AACzE,cAAQ,MAAM,aAAa,MAAM,OAAO,EAAE;AAC1C,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM;AAAA,EACR;AAEA,QAAM,EAAE,OAAO,IAAI,OAAO,MAAM,UAAU,KAAK,GAAG,MAAM;AACtD,YAAQ;AAAA,MACN,KAAK,UAAU;AAAA,QACb,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankieai/server",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Frankie cloud agent server — auth, sessions, MCP wiring and streaming for the embedded onboarding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "yaml": "^2.9.0",
29
29
  "zod": "^4.0.0",
30
30
  "pg": "^8.13.0",
31
- "@frankieai/core": "0.2.4"
31
+ "@frankieai/core": "0.2.6"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.10.0",