Ctrl K
ComunidadGuías…

src/server/features.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Server feature seam — the drop-in point for optional add-ons.
//
// The core mounts only the seam; it registers NOTHING here. An optional add-on (a downstream
// distribution or a private module) imports this and calls registerFeature({...}) for each
// add-on surface BEFORE the server starts, then those get
// mounted at the one `mountFeatures()` call in index.ts. This keeps add-on code entirely out of
// the core tree while giving it a clean, typed way in.
//
// Deeper than the data-side `*.ext.ts` extensions (which are just routes): a feature gets the
// live app + broadcast + auth so it can wire websockets, background workers, recovery, etc.

import type { Express, RequestHandler } from 'express';

export interface FeatureContext {
  app: Express;
  requireAuth: RequestHandler;
  /** Push a server event to all connected clients (same fn the core uses). */
  broadcast: (event: object) => void;
  /** True in passive/standby mode — features should skip background writers/consumers. */
  passive: boolean;
}

export interface ServerFeature {
  readonly name: string;
  /** Mount routes/handlers/consumers. Throwing is contained (logged, other features still load). */
  register(ctx: FeatureContext): void;
  /** Optional: resume an interrupted session for this feature's channel (called by recovery). */
  resumeSession?(session: unknown, prompt: string): Promise<void> | void;
  /**
   * Optional: client-facing capability flags this feature contributes to `/api/features`.
   * A static map or a getter (evaluated per request, so it can honor env). Merged OVER the core
   * flags. This is how an add-on surface turns itself on WITHOUT the core naming it — the core
   * declares none; an add-on's feature declares its own flag (e.g. `{ someSurface: true }`).
   */
  readonly flags?: Record<string, boolean> | (() => Record<string, boolean>);
  /**
   * Optional: sidecar WebSocket proxy routes this feature contributes (url-prefix → localhost port).
   * Merged into the core WS proxy table so a feature's daemon socket is reachable WITHOUT the core
   * naming the prefix. Same generic bridge as `flags`.
   */
  readonly sidecarRoutes?: Record<string, number>;
  /**
   * Optional: MCP server names that must ALWAYS be handed to an engine which narrows its MCP set for
   * speed (rather than only when the prompt happens to mention them). A feature that depends on one
   * of its own MCP servers declares it here, so the core never has to name it. Static or a getter
   * (evaluated per call, so it can honor env) — same shape as `flags`.
   */
  readonly alwaysMcp?: string[] | (() => string[]);
}

const registry: ServerFeature[] = [];

/** Register an add-on feature. Called by an optional add-on before startup. Idempotent by name. */
export function registerFeature(feature: ServerFeature): void {
  if (registry.some((f) => f.name === feature.name)) return;
  registry.push(feature);
}

/** Mount all registered features. The single seam call in index.ts. */
export function mountFeatures(ctx: FeatureContext): void {
  for (const f of registry) {
    try {
      f.register(ctx);
      console.log(`[features] mounted ${f.name}`);
    } catch (err) {
      console.error(`[features] failed to mount ${f.name}:`, (err as Error)?.stack || err);
    }
  }
}

/** Merge every registered feature's contributed capability flags. Merged OVER core flags in
 *  `/api/features` so an add-on feature's declaration wins. The core contributes nothing here. */
export function collectFeatureFlags(): Record<string, boolean> {
  const out: Record<string, boolean> = {};
  for (const f of registry) {
    if (!f.flags) continue;
    Object.assign(out, typeof f.flags === 'function' ? f.flags() : f.flags);
  }
  return out;
}

/** Merge every registered feature's sidecar WS proxy routes (prefix → port). Folded into the
 *  core WS proxy table after startup so feature daemons are reachable. */
export function collectSidecarRoutes(): Record<string, number> {
  const out: Record<string, number> = {};
  for (const f of registry) if (f.sidecarRoutes) Object.assign(out, f.sidecarRoutes);
  return out;
}

/** Every MCP server name registered features insist on always having. The core contributes NONE, so
 *  an engine that narrows its MCP set starts from empty and names no add-on server itself. */
export function collectAlwaysMcp(): Set<string> {
  const out = new Set<string>();
  for (const f of registry) {
    if (!f.alwaysMcp) continue;
    for (const n of typeof f.alwaysMcp === 'function' ? f.alwaysMcp() : f.alwaysMcp) out.add(n);
  }
  return out;
}

/** Let a registered feature resume its own interrupted sessions (used by crash recovery). */
export function resumeFeatureSession(channel: string, session: unknown, prompt: string): boolean {
  const f = registry.find((x) => x.name === channel && x.resumeSession);
  if (!f) return false;
  Promise.resolve(f.resumeSession!(session, prompt)).catch((err) =>
    console.error(`[features] ${channel} resume failed:`, (err as Error).message),
  );
  return true;
}