@frankieai/server 0.1.1 → 0.2.0

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { FrankieConfig, AuthConfig, ChatAttachment, FrankieServerEvent, McpServerConfig, ToolPolicy, ApiSourceConfig } from '@frankieai/core';
2
+ import { FrankieConfig, AuthConfig, WorkspaceSkill, ChatAttachment, FrankieServerEvent, McpServerConfig, ToolPolicy, ApiSourceConfig } from '@frankieai/core';
3
3
  import { AgentMessage, AgentTool } from '@earendil-works/pi-agent-core';
4
4
  import { JWTPayload } from 'jose';
5
5
  import { Models, Model, Api } from '@earendil-works/pi-ai';
@@ -42,8 +42,18 @@ interface RateLimiter {
42
42
  declare class MemoryRateLimiter implements RateLimiter {
43
43
  private readonly hits;
44
44
  private lastSweep;
45
+ /**
46
+ * The longest window any caller has asked for. The sweep horizon has to cover
47
+ * it, or sweeping would discard hits the window still counts.
48
+ */
49
+ private maxWindowMs;
45
50
  take(key: string, limit: number, windowSeconds: number): Promise<boolean>;
46
- /** Drop empty buckets occasionally so a long-lived process does not grow. */
51
+ /**
52
+ * Trim stale hits and drop empty buckets occasionally, so a long-lived process
53
+ * does not grow. The horizon tracks the longest window in use rather than a
54
+ * fixed hour: a shorter one would drop timestamps that window still counts,
55
+ * silently handing the caller a fresh budget mid-window.
56
+ */
47
57
  private sweep;
48
58
  }
49
59
 
@@ -105,29 +115,29 @@ interface ChatSession {
105
115
  id: string;
106
116
  tenantId: string;
107
117
  user: VerifiedUser;
108
- /**
109
- * The raw host-signed token, kept only so it can be forwarded to MCP servers
110
- * the tenant has explicitly opted into. Never written to disk or logs.
111
- */
118
+ /** Raw host-signed token, forwarded only to tenant tools that explicitly opt in. */
112
119
  userToken?: string;
113
- /** Page context the host passed at session creation. */
120
+ origin?: string;
114
121
  context: Record<string, string | number | boolean | null>;
115
- /**
116
- * The conversation transcript. Frankie owns it outright — the agent loop is
117
- * stateless between turns and is handed this array each time — which is what
118
- * makes a session portable to a shared store, and exportable.
119
- */
122
+ /** Agent transcript used to continue the model conversation on the server. */
120
123
  messages: AgentMessage[];
124
+ /** UI transcript kept separately so the exact rendered chat can sync across devices. */
125
+ clientMessages?: unknown[];
126
+ /** User-editable title, otherwise derived from the first user message. */
127
+ title?: string;
121
128
  createdAt: number;
122
129
  lastActiveAt: number;
123
130
  messageCount: number;
124
131
  costUsd: number;
125
- /** Set while a turn is in flight, so a second concurrent turn is rejected. */
126
132
  busy: boolean;
127
133
  }
128
134
  interface SessionStore {
129
135
  create(session: ChatSession): Promise<void>;
130
136
  get(id: string): Promise<ChatSession | undefined>;
137
+ /** List one user's conversations for a tenant, newest first. */
138
+ listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
139
+ /** Workspace-owner history view. This is only exposed through the authenticated control plane. */
140
+ listByTenant(tenantId: string): Promise<ChatSession[]>;
131
141
  update(id: string, patch: Partial<ChatSession>): Promise<void>;
132
142
  delete(id: string): Promise<void>;
133
143
  }
@@ -137,6 +147,8 @@ declare class MemorySessionStore implements SessionStore {
137
147
  constructor(ttlMs?: number);
138
148
  create(session: ChatSession): Promise<void>;
139
149
  get(id: string): Promise<ChatSession | undefined>;
150
+ listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
151
+ listByTenant(tenantId: string): Promise<ChatSession[]>;
140
152
  update(id: string, patch: Partial<ChatSession>): Promise<void>;
141
153
  delete(id: string): Promise<void>;
142
154
  private evictExpired;
@@ -187,13 +199,18 @@ interface TenantStore {
187
199
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
188
200
  }
189
201
  /**
190
- * Turns a `TenantStore` into ready-to-serve `TenantRuntime`s, caching each
191
- * tenant's `TokenService` and resolved model so the model catalogue is walked
192
- * once per tenant rather than once per request.
202
+ * Turns a `TenantStore` into ready-to-serve `TenantRuntime`s.
203
+ *
204
+ * Config is read from the store on every resolve, so a change made through the
205
+ * control plane — a new connection, a widened tool policy, a theme — is picked
206
+ * up on the very next request rather than being pinned to whatever was cached
207
+ * when the tenant was first seen. Only the expensive part, walking the provider
208
+ * catalogue to resolve a model, is memoised (keyed by the model settings), so
209
+ * the hot path stays cheap without ever serving a stale config.
193
210
  */
194
211
  declare class TenantRegistry {
195
212
  private readonly store;
196
- private readonly cache;
213
+ private readonly modelCache;
197
214
  constructor(store: TenantStore);
198
215
  /**
199
216
  * A registry over exactly one tenant. Builds the runtime eagerly so an
@@ -209,8 +226,6 @@ declare class TenantRegistry {
209
226
  * whole fleet.
210
227
  */
211
228
  resolve(publishableKey: string): Promise<TenantRuntime | null>;
212
- /** Drop a cached runtime so the next request rebuilds it from the store. */
213
- evict(publishableKey: string): void;
214
229
  }
215
230
  /** A store over a single resolved config — the self-hosted, one-tenant case. */
216
231
  declare class SingleTenantStore implements TenantStore {
@@ -263,18 +278,34 @@ declare function createMcpHostApp(options: McpHostOptions): Hono;
263
278
  * customer's backend signs with, and the session-token secret) live here for a
264
279
  * self-hosted start; a hosted deployment swaps this file store for a vault.
265
280
  */
281
+ interface RepositoryInstallRecord {
282
+ repository: string;
283
+ status: "installing" | "submitted" | "failed";
284
+ runId?: string;
285
+ pullRequestUrl?: string;
286
+ requiresJwtSecret: boolean;
287
+ failureMessage?: string;
288
+ updatedAt: string;
289
+ }
266
290
  interface TenantRecord {
291
+ skillHistory?: FrankieConfig["skills"];
267
292
  id: string;
268
293
  /** Who owns this tenant — the dashboard user's stable id (e.g. "github:42"). */
269
294
  ownerId?: string;
270
295
  config: FrankieConfig;
271
296
  jwtSecret?: string;
272
297
  sessionSecret: string;
298
+ /** When true, the workspace is protected: it cannot be deleted until unlocked. */
299
+ locked?: boolean;
300
+ /** Durable repository-install state surfaced in the dashboard and setup chat. */
301
+ repositoryInstall?: RepositoryInstallRecord;
273
302
  createdAt: string;
274
303
  updatedAt: string;
275
304
  }
276
305
  /** A tenant record with its secrets stripped — safe to return from the API. */
277
306
  interface PublicTenant {
307
+ skillHistory: FrankieConfig["skills"];
308
+ skills: TenantRecord["config"]["skills"];
278
309
  id: string;
279
310
  ownerId?: string;
280
311
  name: string;
@@ -284,6 +315,38 @@ interface PublicTenant {
284
315
  connection: "knowledge" | "mcp" | "openapi";
285
316
  mcpServers: string[];
286
317
  apis: string[];
318
+ /** Full connection detail so the dashboard can show and pre-fill what's configured. */
319
+ connections: Array<{
320
+ kind: "mcp" | "openapi";
321
+ name: string;
322
+ /** OpenAPI spec URL/path. */
323
+ spec?: string;
324
+ /** MCP server URL. */
325
+ url?: string;
326
+ baseUrl?: string;
327
+ methods?: string[];
328
+ capabilities?: Array<{
329
+ operationId: string;
330
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
331
+ path: string;
332
+ summary?: string;
333
+ }>;
334
+ discoveredTools?: Array<{
335
+ name: string;
336
+ description?: string;
337
+ }>;
338
+ selectedTools?: string[];
339
+ exposeAsMcp?: boolean;
340
+ forwardsToken: boolean;
341
+ /** Whether this source is currently enabled for chat. */
342
+ enabled: boolean;
343
+ /** Configuration health, not a simulated runtime probe. */
344
+ status: "ready" | "configured" | "needs_attention";
345
+ }>;
346
+ /** Protected from deletion until unlocked. */
347
+ locked: boolean;
348
+ /** A submitted or in-progress SDK installation for this workspace. */
349
+ repositoryInstall?: RepositoryInstallRecord;
287
350
  /** The customer's brand theme for the widget, so the dashboard can edit it. */
288
351
  theme: TenantRecord["config"]["theme"];
289
352
  /** Opt-in chat capabilities (attachments, model picker) for the dashboard to edit. */
@@ -292,6 +355,22 @@ interface PublicTenant {
292
355
  updatedAt: string;
293
356
  }
294
357
  declare function toPublicTenant(record: TenantRecord): PublicTenant;
358
+ /**
359
+ * A member of an org. The org is identified by its owner's id (e.g. "github:42"),
360
+ * and members are named by their GitHub login (lowercased) so they can be invited
361
+ * before they've ever signed in. `admin`s can manage members; `member`s can
362
+ * manage the org's workspaces but not its people. The owner is always an admin
363
+ * and is never stored here.
364
+ */
365
+ interface MembershipRecord {
366
+ /** The org's owner id, e.g. "github:42". */
367
+ org: string;
368
+ /** The member's GitHub login, lowercased. */
369
+ login: string;
370
+ role: "admin" | "member";
371
+ invitedBy?: string;
372
+ createdAt: string;
373
+ }
295
374
  /**
296
375
  * A `TenantStore` the control plane can also write to and list. The data plane
297
376
  * only needs `byPublishableKey`; the control plane needs the rest. Both the
@@ -299,9 +378,20 @@ declare function toPublicTenant(record: TenantRecord): PublicTenant;
299
378
  * against the interface.
300
379
  */
301
380
  interface ControlStore extends TenantStore {
381
+ compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
302
382
  list(ownerId?: string): Promise<TenantRecord[]>;
303
383
  get(id: string): Promise<TenantRecord | null>;
304
384
  put(record: TenantRecord): Promise<void>;
385
+ /** Remove a tenant for good. A no-op if it does not exist. */
386
+ delete(id: string): Promise<void>;
387
+ /** All members of the org owned by `org`. */
388
+ listMembers(org: string): Promise<MembershipRecord[]>;
389
+ /** Add or update a member. */
390
+ putMember(member: MembershipRecord): Promise<void>;
391
+ /** Remove a member from an org. A no-op if absent. */
392
+ deleteMember(org: string, login: string): Promise<void>;
393
+ /** Every org membership held by a given login, across all orgs. */
394
+ membershipsForLogin(login: string): Promise<MembershipRecord[]>;
305
395
  }
306
396
  /**
307
397
  * The control plane's persistence, and at the same time a `TenantStore` the
@@ -317,19 +407,28 @@ declare class FileTenantStore implements ControlStore {
317
407
  private readonly env;
318
408
  private index;
319
409
  private byId;
410
+ private members;
320
411
  private loaded;
412
+ private static memberKey;
321
413
  constructor(options: {
322
414
  dir: string;
323
415
  providerKeyEnv?: string;
324
416
  env?: NodeJS.ProcessEnv;
325
417
  });
326
418
  private load;
419
+ private persistMembers;
420
+ listMembers(org: string): Promise<MembershipRecord[]>;
421
+ putMember(member: MembershipRecord): Promise<void>;
422
+ deleteMember(org: string, login: string): Promise<void>;
423
+ membershipsForLogin(login: string): Promise<MembershipRecord[]>;
327
424
  private remember;
328
425
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
329
426
  /** All tenants, or — when an owner is given — only that owner's. */
330
427
  list(ownerId?: string): Promise<TenantRecord[]>;
331
428
  get(id: string): Promise<TenantRecord | null>;
332
429
  put(record: TenantRecord): Promise<void>;
430
+ compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
431
+ delete(id: string): Promise<void>;
333
432
  }
334
433
 
335
434
  /**
@@ -353,6 +452,7 @@ interface Queryable {
353
452
  * read time and never written to the row.
354
453
  */
355
454
  declare class PostgresTenantStore implements ControlStore {
455
+ private readonly snapshots;
356
456
  private readonly db;
357
457
  private readonly providerKeyEnv;
358
458
  private readonly env;
@@ -370,11 +470,18 @@ declare class PostgresTenantStore implements ControlStore {
370
470
  private ensureReady;
371
471
  /** Create the table if it isn't there yet. Safe to call repeatedly. */
372
472
  init(): Promise<void>;
473
+ private memberRow;
474
+ listMembers(org: string): Promise<MembershipRecord[]>;
475
+ putMember(member: MembershipRecord): Promise<void>;
476
+ deleteMember(org: string, login: string): Promise<void>;
477
+ membershipsForLogin(login: string): Promise<MembershipRecord[]>;
373
478
  private parse;
374
479
  byPublishableKey(key: string): Promise<ResolvedConfig | null>;
375
480
  list(ownerId?: string): Promise<TenantRecord[]>;
376
481
  get(id: string): Promise<TenantRecord | null>;
377
482
  put(record: TenantRecord): Promise<void>;
483
+ delete(id: string): Promise<void>;
484
+ compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
378
485
  }
379
486
 
380
487
  interface ResolvedStore {
@@ -405,13 +512,8 @@ declare function resolveStores(env?: NodeJS.ProcessEnv): Promise<ResolvedStores>
405
512
  declare function resolveTenantStore(env?: NodeJS.ProcessEnv): Promise<ResolvedStore>;
406
513
 
407
514
  /**
408
- * A `SessionStore` backed by Postgres, so a user's chat history survives a
409
- * restart and is shared across instances — the hosted default. The whole
410
- * `ChatSession` (transcript included) is stored as one JSON row keyed by id.
411
- *
412
- * A customer who would rather keep transcripts in their own store implements
413
- * `SessionStore` against it and passes it to `createFrankieApp` — this class is
414
- * just the batteries-included option.
515
+ * Hosted session + conversation store. Transcript and UI metadata live together
516
+ * in Postgres, making a signed-in user's chats available from any device.
415
517
  */
416
518
  declare class PostgresSessionStore implements SessionStore {
417
519
  private readonly db;
@@ -421,17 +523,28 @@ declare class PostgresSessionStore implements SessionStore {
421
523
  db: Queryable;
422
524
  ttlMs?: number;
423
525
  });
424
- /** Create the table on first use, once — lazy, so boot never blocks on the DB. */
425
526
  private ensureReady;
426
527
  init(): Promise<void>;
427
528
  create(session: ChatSession): Promise<void>;
428
529
  get(id: string): Promise<ChatSession | undefined>;
530
+ listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
531
+ listByTenant(tenantId: string): Promise<ChatSession[]>;
429
532
  update(id: string, patch: Partial<ChatSession>): Promise<void>;
430
533
  delete(id: string): Promise<void>;
431
534
  }
432
535
 
536
+ interface SkillGenerationInput {
537
+ goal: string;
538
+ tools: string[];
539
+ instructions?: string;
540
+ }
541
+ declare function generateSkill(resolved: ResolvedConfig, input: SkillGenerationInput): Promise<WorkspaceSkill>;
542
+
433
543
  interface ControlOptions {
544
+ generateSkill?: typeof generateSkill;
434
545
  store: ControlStore;
546
+ /** Durable session store shared with the data plane when Postgres is configured. */
547
+ sessions?: SessionStore;
435
548
  /** Bearer token that gates the admin API. A stand-in for real user auth. */
436
549
  adminToken?: string;
437
550
  logger?: (event: Record<string, unknown>) => void;
@@ -548,6 +661,8 @@ interface CollectToolsOptions {
548
661
  config: FrankieConfig;
549
662
  session: ChatSession;
550
663
  fetchImpl?: typeof fetch;
664
+ /** Base URL of the account API for the built-in workspaceAdmin tools. */
665
+ accountApiUrl?: string;
551
666
  onWarning?: (message: string, detail: Record<string, unknown>) => void;
552
667
  }
553
668
  interface CollectedTools {
@@ -594,6 +709,6 @@ declare function buildForwardedHeaders(source: ForwardingConfig, session: ChatSe
594
709
  * configured. Host-supplied page context is fenced off as data: it comes from
595
710
  * a web page and must never be able to redirect the agent.
596
711
  */
597
- declare function buildSystemPrompt(config: FrankieConfig, session: ChatSession): string;
712
+ declare function buildSystemPrompt(config: FrankieConfig, session: ChatSession, availableTools?: string[]): string;
598
713
 
599
714
  export { API_TOOL_PREFIX, type ApiToolSet, AuthError, type BuildApiToolsOptions, type ChatSession, type CollectToolsOptions, type CollectedTools, ConfigError, type ConnectMcpOptions, type ControlOptions, type ControlStore, type CreateAppOptions, FileTenantStore, type ForwardingConfig, MCP_TOOL_PREFIX, type McpConnection, type McpHostOptions, MemoryRateLimiter, MemorySessionStore, MemoryTenantStore, ModelResolutionError, OpenApiError, PostgresSessionStore, PostgresTenantStore, type PublicTenant, type Queryable, type RateLimiter, type ResolvedConfig, type ResolvedModel, type ResolvedStore, type ResolvedStores, type RunTurnParams, type SessionStore, SingleTenantStore, type TenantRecord, TenantRegistry, type TenantRuntime, type TenantStore, TokenService, type VerifiedUser, anonymousUser, applyEnvOverrides, buildApiTools, buildForwardedHeaders, buildSystemPrompt, collectTools, connectMcpTools, createControlApp, createFrankieApp, createMcpHostApp, isOriginAllowed, loadConfigFromFile, loadResolvedConfig, loadSpec, matchPath, modelHasApi, newSessionId, resolveModel, resolveSecrets, resolveStores, resolveTenantStore, runTurn, safeEqual, toPublicTenant };
package/dist/index.js CHANGED
@@ -1,48 +1,48 @@
1
1
  import {
2
2
  createControlApp
3
- } from "./chunk-S7OCXWEP.js";
3
+ } from "./chunk-CYWTCM2O.js";
4
4
  import {
5
- API_TOOL_PREFIX,
6
5
  ConfigError,
7
- MCP_TOOL_PREFIX,
8
- MemoryRateLimiter,
9
6
  MemoryTenantStore,
10
- ModelResolutionError,
11
- OpenApiError,
12
7
  SingleTenantStore,
13
8
  TenantRegistry,
14
9
  applyEnvOverrides,
15
- buildApiTools,
16
- buildForwardedHeaders,
17
10
  buildSystemPrompt,
18
11
  collectTools,
19
- connectMcpTools,
20
12
  createFrankieApp,
21
13
  createMcpHostApp,
22
14
  loadConfigFromFile,
23
15
  loadResolvedConfig,
24
- loadSpec,
25
- matchPath,
26
- modelHasApi,
27
- resolveModel,
28
16
  resolveSecrets,
29
17
  runTurn
30
- } from "./chunk-UUOTMDWW.js";
18
+ } from "./chunk-BA3FZ7JH.js";
31
19
  import {
20
+ API_TOOL_PREFIX,
32
21
  AuthError,
33
22
  FileTenantStore,
23
+ MCP_TOOL_PREFIX,
24
+ MemoryRateLimiter,
34
25
  MemorySessionStore,
26
+ ModelResolutionError,
27
+ OpenApiError,
35
28
  PostgresSessionStore,
36
29
  PostgresTenantStore,
37
30
  TokenService,
38
31
  anonymousUser,
32
+ buildApiTools,
33
+ buildForwardedHeaders,
34
+ connectMcpTools,
39
35
  isOriginAllowed,
36
+ loadSpec,
37
+ matchPath,
38
+ modelHasApi,
40
39
  newSessionId,
40
+ resolveModel,
41
41
  resolveStores,
42
42
  resolveTenantStore,
43
43
  safeEqual,
44
44
  toPublicTenant
45
- } from "./chunk-F227CSY3.js";
45
+ } from "./chunk-R5XGMQR4.js";
46
46
  export {
47
47
  API_TOOL_PREFIX,
48
48
  AuthError,
package/dist/server.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigError,
4
- ModelResolutionError,
5
4
  createFrankieApp,
6
5
  loadResolvedConfig
7
- } from "./chunk-UUOTMDWW.js";
6
+ } from "./chunk-BA3FZ7JH.js";
8
7
  import {
8
+ ModelResolutionError,
9
9
  resolveStores
10
- } from "./chunk-F227CSY3.js";
10
+ } from "./chunk-R5XGMQR4.js";
11
11
 
12
12
  // src/server.ts
13
13
  import { serve } from "@hono/node-server";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankieai/server",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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.1.1"
31
+ "@frankieai/core": "0.2.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.10.0",