Ctrl K
ComunidadGuías…

src/index.ts

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

Archivo original ↗Preguntar a KiBot
// Shraga — public library surface.
//
// The FIRST-CLASS way to consume Shraga: `import { createShraga } from 'shraga'`, configure with
// typed options, register/extend programmatically through the same seams the built-ins use
// (features, engines, the event bus, verified webhooks, data-side extensions), then `start()`.
//
//   import { createShraga } from 'shraga';
//   const app = createShraga({ port: 3032, dataDir: './data', authProvider: 'local' });
//   app.registerFeature(myFeature);
//   app.on('gmail', (payload) => { … });
//   await app.start();
//   // … app.stop() to shut down.
//
// The run-from-source entry (`src/server/index.ts`, used by prod `bun run src/server/index.ts`) and
// the `shraga` CLI both dogfood this exact surface — createShraga(fromEnv()).start(). File-based
// `data/extensions/*.ext.ts` drop-ins and `SHRAGA_OVERLAY` keep working: they reach the SAME
// programmatic registries, just via a different door.

// Type-only imports below: importing this module must NOT eagerly load the server graph (which
// resolves DATA_DIR at import time). The runtime boot is a dynamic import inside start(), AFTER
// options have been mapped onto the environment.
import type { BootRegistrations, ServerHandle } from './server/boot.ts';
import type { ServerFeature, FeatureContext } from './server/features.ts';
import type { AgentEngine, EngineModel, EngineStreamOpts } from './server/engine/types.ts';
import type { ExtRegisterFn, ExtensionContext } from './server/extensions.ts';
import type { WebhookOptions } from './server/events/webhook.ts';
import type { ShragaEvent, ShragaEventMap, PayloadOf } from './server/events/types.ts';
import type { PolicyOptions, PolicyFile } from './server/security/policy.ts';

export type {
  PolicyFile,
  ServerHandle,
  ServerFeature,
  FeatureContext,
  AgentEngine,
  EngineModel,
  EngineStreamOpts,
  ExtRegisterFn,
  ExtensionContext,
  WebhookOptions,
  ShragaEvent,
  ShragaEventMap,
  PayloadOf,
};

/** Typed configuration for a Shraga instance. Follows the repo's ModuleOptions convention:
 *  defaults live here and are merged with the caller's partial. Anything not modelled is still
 *  reachable via `env` (Shraga is heavily env-driven) — those are set before the server boots. */
export class ShragaOptions {
  /** HTTP/WS port. Default 3032 (or PORT env). */
  port?: number;
  /** Data directory (flat-file storage root). Default ./data (or DATA_DIR env). */
  dataDir?: string;
  /** Auth backend. 'local' (default) = username/password; 'firebase' needs the firebase add-on. */
  authProvider?: 'local' | 'firebase';
  /** Directory of the built client to serve (index.html + assets). Default = shraga's shipped
   *  `dist/client`. A consumer shipping its own UI (e.g. the EE client build) points this at its
   *  own dist. Precedence: this option > SHRAGA_CLIENT_DIR env > default. */
  clientDir?: string;
  /** Passive mode — HTTP serving only, no schedulers/consumers/background writers (standby twins). */
  passive?: boolean;
  /** Install process SIGTERM/SIGINT handlers (default true — the standalone server/CLI wants them).
   *  A library embedder owning its own lifecycle sets false and uses stop(). */
  installSignalHandlers?: boolean = true;
  /** OPT-IN runtime plug-and-play (default false). When true, the ServerHandle returned by start()
   *  can register extensions/webhooks and subscribe to events AFTER boot (they mount on the live
   *  extension Router / event bus). When false, those handle methods throw. Pre-start registration is
   *  unaffected either way. Features & engines are NEVER runtime-registerable (they mount at boot). */
  runtimeRegistration?: boolean = false;
  /** Arbitrary extra environment to apply before boot (e.g. ANTHROPIC_API_KEY, SHRAGA_FEAT_*). */
  env?: Record<string, string>;
  /** Security policy seams. `migrate` seeds `data/security/policy.json` ONCE, when it's first generated (no file, no
   *  `.migrated` marker): it receives the default draft (legacy whitelist already bound as operator) and returns the
   *  final draft. The result is validated and saved like any policy; an invalid draft or a throw fails closed (owners
   *  only). Never runs on a PASSIVE standby (it runs on promotion if the file is still missing). Later edits go through
   *  the Owner Console, not this hook. */
  security?: { migrate?: PolicyOptions['migrate'] };
}

export interface ShragaInstance {
  /** Register a server feature (routes/WS/consumers) — the same seam Slack uses. */
  registerFeature(feature: ServerFeature): this;
  /** Register a pluggable agent engine (runtime). */
  registerEngine(engine: AgentEngine): this;
  /** Register a programmatic extension — the same shape as a data/extensions/*.ext.ts default export. */
  registerExtension(fn: ExtRegisterFn): this;
  /** Declare a verified vendor webhook (public POST /api/webhooks/<source> → typed event). */
  registerWebhook<K extends string>(opts: WebhookOptions<K>): this;
  /** Subscribe to a typed event source on the in-process bus. */
  on<K extends string>(source: K, handler: (payload: PayloadOf<K>, evt: ShragaEvent<K>) => void): this;
  /** Publish an event onto the bus (after start()). */
  emit<K extends string>(source: K, payload: PayloadOf<K>): this;
  /** Boot HTTP + WS. Resolves once listening. Idempotent — repeated calls return the same handle. */
  start(): Promise<ServerHandle>;
  /** Drain and shut down without exiting the process. */
  stop(): Promise<void>;
  /** The live Express app (advanced use) — available after start(). */
  readonly app: ServerHandle['app'] | undefined;
  /** Publish onto the bus directly (advanced use) — available after start(). */
  readonly emitEvent: ServerHandle['emitEvent'] | undefined;
}

class Shraga implements ShragaInstance {
  public options: ShragaOptions;
  private reg: Required<Omit<BootRegistrations, 'security'>> ={ features: [], engines: [], extensions: [], eventSubs: [] };
  private handle: ServerHandle | null = null;
  private starting: Promise<ServerHandle> | null = null;

  constructor(options?: Partial<ShragaOptions>) {
    this.options = { ...new ShragaOptions(), ...options };
    // Map options onto the environment NOW, before any server module (which resolves DATA_DIR /
    // AUTH_PROVIDER at import time) can load. start()'s dynamic import happens strictly after this.
    this.applyEnv();
  }

  private applyEnv(): void {
    const o = this.options;
    if (o.port != null) process.env.PORT = String(o.port);
    if (o.dataDir != null) process.env.DATA_DIR = o.dataDir;
    if (o.authProvider != null) process.env.AUTH_PROVIDER = o.authProvider;
    if (o.clientDir != null) process.env.SHRAGA_CLIENT_DIR = o.clientDir;
    if (o.passive != null) process.env.SHRAGA_PASSIVE = o.passive ? '1' : '0';
    if (o.installSignalHandlers === false) process.env.SHRAGA_INSTALL_SIGNALS = '0';
    if (o.runtimeRegistration != null) process.env.SHRAGA_RUNTIME_REGISTRATION = o.runtimeRegistration ? '1' : '0';
    for (const [k, v] of Object.entries(o.env ?? {})) process.env[k] = v;
  }

  private assertNotStarted(what: string): void {
    if (this.handle || this.starting) throw new Error(`[shraga] ${what} must be called before start()`);
  }

  registerFeature(feature: ServerFeature): this { this.assertNotStarted('registerFeature'); this.reg.features.push(feature); return this; }
  registerEngine(engine: AgentEngine): this { this.assertNotStarted('registerEngine'); this.reg.engines.push(engine); return this; }
  registerExtension(fn: ExtRegisterFn): this { this.assertNotStarted('registerExtension'); this.reg.extensions.push(fn); return this; }

  registerWebhook<K extends string>(opts: WebhookOptions<K>): this {
    this.assertNotStarted('registerWebhook');
    // A webhook is just an extension that mounts a verified route on the extension router (before the
    // SPA catch-all) — identical to a file-based webhook. Reuse the seam; don't invent a parallel one.
    this.reg.extensions.push((_router, ctx: ExtensionContext) => { ctx.registerWebhook(opts); });
    return this;
  }

  on<K extends string>(source: K, handler: (payload: PayloadOf<K>, evt: ShragaEvent<K>) => void): this {
    this.assertNotStarted('on');
    this.reg.eventSubs.push({ source, handler: handler as (payload: unknown, evt: unknown) => void });
    return this;
  }

  emit<K extends string>(source: K, payload: PayloadOf<K>): this {
    if (!this.handle) throw new Error('[shraga] emit() requires start() first');
    this.handle.emitEvent(source, payload);
    return this;
  }

  async start(): Promise<ServerHandle> {
    if (this.handle) return this.handle;
    if (this.starting) return this.starting;
    this.starting = (async () => {
      const { bootServer } = await import('./server/boot.ts');
      this.handle = await bootServer({ ...this.reg, security: this.options.security });
      return this.handle;
    })();
    return this.starting;
  }

  async stop(): Promise<void> {
    if (this.handle) { await this.handle.stop(); this.handle = null; }
    this.starting = null;
  }

  get app() { return this.handle?.app; }
  get emitEvent() { return this.handle?.emitEvent; }
}

/** Create a Shraga instance. Configure it, register/extend, then `await instance.start()`. */
export function createShraga(options?: Partial<ShragaOptions>): ShragaInstance {
  return new Shraga(options);
}

/** Derive options from the environment (PORT / DATA_DIR / AUTH_PROVIDER / SHRAGA_PASSIVE). The
 *  run-from-source entry and the CLI pass this straight into createShraga. */
export function fromEnv(): Partial<ShragaOptions> {
  const passive = process.env.SHRAGA_PASSIVE ?? process.env.UNCLAW_PASSIVE;
  const runtimeReg = process.env.SHRAGA_RUNTIME_REGISTRATION;
  return {
    port: process.env.PORT ? Number(process.env.PORT) : undefined,
    dataDir: process.env.DATA_DIR || undefined,
    authProvider: (process.env.AUTH_PROVIDER as 'local' | 'firebase' | undefined) || undefined,
    clientDir: process.env.SHRAGA_CLIENT_DIR || undefined,
    passive: passive === '1' || passive === 'true' ? true : undefined,
    runtimeRegistration: runtimeReg === '1' || runtimeReg === 'true' ? true : undefined,
  };
}