{"version":3,"file":"index.cjs","sources":["../src/runtime.ts"],"sourcesContent":["/**\n * Plugin runtime — the platform-provided helpers a plugin author receives\n * via the `definePlugin` factory. Framework-agnostic core; Vue-specific\n * `BrowserPluginRuntime` lives in `./vue.ts`.\n *\n * Spec: https://github.com/receptron/mulmoclaude/issues/1110\n */\n\nimport type { ToolDefinition } from \"./types\";\n\n// ============================================================================\n// File I/O — scoped to plugin's data or config root\n// ============================================================================\n\n/**\n * File operations scoped to a single root directory (data or config).\n * All `rel` arguments are POSIX-relative paths. The platform normalises\n * input (`\\` → `/`, `path.posix.normalize`, `ensureInsideBase`) before\n * touching disk, so misuse of `node:path` on Windows still works and\n * `\"../../etc/passwd\"` is rejected.\n *\n * Plugin authors should never need `node:fs` or `node:path`.\n */\nexport interface FileOps {\n  /** Read UTF-8 string. Throws if the file does not exist. */\n  read(rel: string): Promise<string>;\n  /** Read raw bytes. */\n  readBytes(rel: string): Promise<Uint8Array>;\n  /** Write atomically. Creates parent directories as needed. */\n  write(rel: string, content: string | Uint8Array): Promise<void>;\n  /** List basenames in `rel`. */\n  readDir(rel: string): Promise<string[]>;\n  /** mtime (ms since epoch) and byte size. */\n  stat(rel: string): Promise<{ mtimeMs: number; size: number }>;\n  /** Existence check (saves try/catch boilerplate). */\n  exists(rel: string): Promise<boolean>;\n  /** Delete a file. No-op if it does not exist. */\n  unlink(rel: string): Promise<void>;\n}\n\n// ============================================================================\n// Server-side runtime\n// ============================================================================\n\n/**\n * Subset of `RequestInit` the runtime forwards to the underlying fetch.\n * Re-declared here so we don't need a `lib.dom` dependency in the core\n * type declaration.\n */\nexport interface PluginFetchInit {\n  method?: string;\n  headers?: Record<string, string>;\n  body?: string | Uint8Array;\n  signal?: AbortSignal;\n}\n\nexport interface PluginFetchOptions extends PluginFetchInit {\n  /** AbortController timeout. Default 10 000 ms. */\n  timeoutMs?: number;\n  /** When set, requests to a `URL.hostname` not in the list throw. */\n  allowedHosts?: readonly string[];\n}\n\nexport interface PluginFetchJsonOptions<T> extends PluginFetchOptions {\n  /**\n   * Zod-agnostic validator. Pass a function that returns the narrowed\n   * value (or throws). Idiomatic with Zod:\n   *   `parse: (raw) => MySchema.parse(raw)`.\n   *\n   * Required when calling `fetchJson<T>` for any T other than\n   * `unknown` — the overload signatures below enforce this so callers\n   * never get a strongly-typed value out of unvalidated JSON.\n   */\n  parse: (raw: unknown) => T;\n}\n\n/**\n * Default shape for `PluginRuntime['endpoints']` — see\n * `BrowserPluginRuntime['endpoints']` for the same rationale on the\n * Vue side. Plugin authors pin a tighter shape via the `E` type\n * parameter on `PluginRuntime<E>` / `definePlugin<…, E>`.\n */\nexport type DefaultServerPluginEndpoints = Readonly<Record<string, unknown>>;\n\n/**\n * Runtime handed to a plugin's `definePlugin(setup)` factory at load time.\n * The plugin closes over the destructured fields; handlers reference them\n * as bare API calls (no `context.` indirection).\n *\n * `pubsub` / `files.*` / `log` are scoped per plugin. The plugin cannot\n * spell another plugin's channel or file path through the API.\n *\n * Optional `E` type parameter pins the `endpoints` map's shape. Defaults\n * to `DefaultServerPluginEndpoints` for backward compatibility — non-\n * generic usage (`PluginRuntime`) keeps working unchanged (0.3.2).\n */\nexport interface PluginRuntime<E = DefaultServerPluginEndpoints> {\n  /**\n   * Scoped pub/sub publisher. `publish(\"foo\", payload)` is internally\n   * routed to channel `plugin:<pkg>:foo`. The plugin cannot publish to\n   * another plugin's namespace.\n   */\n  pubsub: {\n    publish<T>(eventName: string, payload: T): void;\n  };\n\n  /**\n   * Locale tag the host detected at startup (`\"en\"`, `\"ja\"`, …). The\n   * server side is a snapshot; for reactive updates use the frontend\n   * `BrowserPluginRuntime.locale: Ref<string>` instead.\n   */\n  locale: string;\n\n  /**\n   * Scoped file I/O.\n   *   - `data`:      `~/mulmoclaude/data/plugins/<pkg>/`   — per-plugin private backup target\n   *   - `config`:    `~/mulmoclaude/config/plugins/<pkg>/` — per-plugin private UI state\n   *   - `artifacts`: `~/mulmoclaude/artifacts/`           — SHARED, user-browsable output area\n   *\n   * Unlike `data`/`config` (each sandboxed to a private per-plugin dir),\n   * `artifacts` is rooted at the host's shared artifacts directory so a\n   * plugin can write outputs the user sees in the Files explorer — e.g. a\n   * chart plugin writing `charts/<slug>.chart.json`. Relative paths are\n   * still normalised + traversal-guarded by the host; the plugin owns its\n   * category subdir (`charts/`, `spreadsheets/`, …) by convention.\n   */\n  files: {\n    data: FileOps;\n    config: FileOps;\n    artifacts: FileOps;\n  };\n\n  /**\n   * Logger bridge to the host's logger. Prefix `plugin/<pkg>` is added\n   * automatically. Use this instead of `console.*` so plugin output\n   * lands in the central log files.\n   */\n  log: {\n    debug(msg: string, data?: object): void;\n    info(msg: string, data?: object): void;\n    warn(msg: string, data?: object): void;\n    error(msg: string, data?: object): void;\n  };\n\n  /**\n   * `fetch` wrapper with timeout and (optional) host allowlist. Use\n   * instead of `globalThis.fetch` so timeouts and allowlists are\n   * applied uniformly across all plugins.\n   */\n  fetch(url: string, opts?: PluginFetchOptions): Promise<Response>;\n\n  /**\n   * `fetch` + `response.json()` + optional validator.\n   *\n   * - Without `opts.parse`, the result type is `unknown` — callers\n   *   must narrow before use. This prevents strongly-typed access to\n   *   un-validated remote JSON, which is a frequent type-soundness\n   *   bug (\"the server promised X, then it didn't\").\n   * - With `opts.parse`, the validator's return type narrows the\n   *   promise. Idiomatic with Zod:\n   *   `await fetchJson(url, { parse: (raw) => MySchema.parse(raw) })`.\n   */\n  fetchJson(url: string, opts?: PluginFetchOptions): Promise<unknown>;\n  fetchJson<T>(url: string, opts: PluginFetchJsonOptions<T>): Promise<T>;\n\n  /**\n   * Optional URL map mirroring `BrowserPluginRuntime.endpoints` —\n   * see that field for the rationale. Server-side plugin handlers\n   * rarely need this (they typically service the dispatch endpoint\n   * directly), but the field is defined symmetrically so a\n   * cross-cutting plugin (e.g. one that calls into another plugin's\n   * URL via `runtime.fetch`) doesn't have to import the host's\n   * config.\n   *\n   * Single-dispatch plugins (the common runtime-loaded shape) leave\n   * this `undefined`.\n   */\n  endpoints?: E;\n}\n\n// ============================================================================\n// Plugin factory\n// ============================================================================\n\n/**\n * What a plugin's `setup` function returns at the **loose** level —\n * just `TOOL_DEFINITION`. Whether a matching named handler is\n * type-required is decided by the wrapper type `StrictPluginResult`\n * (used by `definePlugin` below) rather than by this base shape, so\n * the runtime loader's `isPluginFactory` predicate can keep using\n * the loose form when it doesn't yet know the tool name.\n *\n * Codex review #10 caught two iterations of looser-than-intended\n * checks here; the final form below uses an inference helper so the\n * strict requirement actually fires at the `definePlugin` call site.\n */\nexport interface PluginFactoryResult {\n  TOOL_DEFINITION: ToolDefinition;\n  [exportName: string]: unknown;\n}\n\n/**\n * Compile-time strict shape used to constrain `definePlugin`'s setup\n * return type. Extracts the `name` literal out of `T.TOOL_DEFINITION`\n * and demands a handler key matching it. When `name` widens to\n * `string` (no `as const`), the strict check degrades to the loose\n * `PluginFactoryResult` and the runtime loader's load-time warn is\n * the safety net.\n */\nexport type StrictPluginResult<T> = T extends { TOOL_DEFINITION: { name: infer N extends string } }\n  ? string extends N\n    ? PluginFactoryResult\n    : T & { [K in N]: (args: unknown) => unknown | Promise<unknown> }\n  : never;\n// `(args: unknown)` not `(args: never)`: the parameter type is the\n// **contextual** type a plugin author sees when writing\n// `async myTool(args) { ... }` without an explicit annotation. With\n// `never` the plugin's `args` would infer as `never` and any access\n// would fail TS7006 / TS2339; with `unknown` it infers as `unknown`\n// (matching the dispatch route's actual contract — the plugin\n// validates args itself, typically via Zod). Function parameter\n// contravariance still lets the constraint accept any concrete\n// `(args: T) => ...` shape from the author. Codex review #10 iter-3\n// caught the `never` regression.\n\n/**\n * Identity function for type inference. Same philosophy as\n * `defineComponent` in Vue: it does nothing at runtime, just lets\n * TypeScript thread the runtime/result types so the plugin author\n * gets full IntelliSense on `runtime.X` without manual annotations.\n *\n * Generic placement (T inferred from setup's return) lets\n * `StrictPluginResult<T>` extract `TOOL_DEFINITION.name` and require\n * a matching named handler — Codex review #10 iter-2 caught that an\n * earlier `<N extends string, T extends PluginFactoryResult<N>>`\n * shape would not actually narrow `N` at the call site.\n *\n * Plugin authors should declare `name` as a literal (`as const`) so\n * the strict handler check fires:\n *\n *   TOOL_DEFINITION: { type: \"function\" as const, name: \"myTool\" as const, ... }\n *\n * Without `as const`, `N` widens to `string` and the strict check\n * gracefully degrades to the loose runtime warn (see\n * `StrictPluginResult` above).\n *\n * **Annotate the handler parameter explicitly** as `args: unknown`.\n * TypeScript can't propagate the contextual type into the method\n * parameter when it's still inferring `T` from the same return value\n * (circular), so leaving `args` un-annotated trips `noImplicitAny`.\n * Always:\n *\n *   async myTool(args: unknown) { ... }\n *\n * @example\n * ```ts\n * export default definePlugin(({ pubsub, files, locale }) => ({\n *   TOOL_DEFINITION: {\n *     type: \"function\" as const,\n *     name: \"myTool\" as const,\n *     description: \"...\",\n *     parameters: { type: \"object\", properties: {}, required: [] },\n *   },\n *   async myTool(args: unknown) {\n *     // narrow `args` here — typically with Zod\n *     await files.data.write(\"state.json\", JSON.stringify(args));\n *     pubsub.publish(\"changed\", {});\n *     return { ok: true };\n *   },\n * }));\n * ```\n */\nexport function definePlugin<T extends PluginFactoryResult>(\n  setup: (runtime: PluginRuntime) => T & StrictPluginResult<T>,\n): (runtime: PluginRuntime) => T {\n  return setup;\n}\n\n/** Type guard the runtime loader uses to detect factory-shape vs. legacy\n *  raw-export plugins. Exported so other host code can share the test.\n *  Loosened to the widened `PluginFactoryResult<string>` because the\n *  caller (the runtime loader) doesn't know the plugin's tool name at\n *  this point — it pulls `TOOL_DEFINITION` from the factory's return\n *  value to discover it. The strict, name-narrowed form is enforced at\n *  the `definePlugin` call site instead. */\nexport function isPluginFactory(value: unknown): value is (runtime: PluginRuntime) => PluginFactoryResult {\n  return typeof value === \"function\";\n}\n"],"names":[],"mappings":";;AAgRO,SAAS,aACd,OAC+B;AAC/B,SAAO;AACT;AASO,SAAS,gBAAgB,OAA0E;AACxG,SAAO,OAAO,UAAU;AAC1B;;;"}