@ashdev/codex-plugin-recommendations-echo 1.35.0 → 1.36.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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../sdk-typescript/dist/types/rpc.js
3
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/rpc.js
4
4
  var JSON_RPC_ERROR_CODES = {
5
5
  /** Invalid JSON was received */
6
6
  PARSE_ERROR: -32700,
@@ -14,7 +14,7 @@ var JSON_RPC_ERROR_CODES = {
14
14
  INTERNAL_ERROR: -32603
15
15
  };
16
16
 
17
- // ../sdk-typescript/dist/errors.js
17
+ // node_modules/@ashdev/codex-plugin-sdk/dist/errors.js
18
18
  var PluginError = class extends Error {
19
19
  data;
20
20
  constructor(message, data) {
@@ -34,7 +34,7 @@ var PluginError = class extends Error {
34
34
  }
35
35
  };
36
36
 
37
- // ../sdk-typescript/dist/request-context.js
37
+ // node_modules/@ashdev/codex-plugin-sdk/dist/request-context.js
38
38
  import { AsyncLocalStorage } from "node:async_hooks";
39
39
  var store = new AsyncLocalStorage();
40
40
  function runWithParentRequestId(forwardRequestId, fn) {
@@ -44,7 +44,7 @@ function currentParentRequestId() {
44
44
  return store.getStore();
45
45
  }
46
46
 
47
- // ../sdk-typescript/dist/host-rpc.js
47
+ // node_modules/@ashdev/codex-plugin-sdk/dist/host-rpc.js
48
48
  var HostRpcError = class extends Error {
49
49
  code;
50
50
  data;
@@ -150,7 +150,7 @@ var HostRpcClient = class {
150
150
  }
151
151
  };
152
152
 
153
- // ../sdk-typescript/dist/logger.js
153
+ // node_modules/@ashdev/codex-plugin-sdk/dist/logger.js
154
154
  var LOG_LEVELS = {
155
155
  debug: 0,
156
156
  info: 1,
@@ -215,10 +215,10 @@ function createLogger(options) {
215
215
  return new Logger(options);
216
216
  }
217
217
 
218
- // ../sdk-typescript/dist/server.js
218
+ // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
219
219
  import { createInterface } from "node:readline";
220
220
 
221
- // ../sdk-typescript/dist/storage.js
221
+ // node_modules/@ashdev/codex-plugin-sdk/dist/storage.js
222
222
  var StorageError = class extends Error {
223
223
  code;
224
224
  data;
@@ -361,7 +361,7 @@ var PluginStorage = class {
361
361
  }
362
362
  };
363
363
 
364
- // ../sdk-typescript/dist/server.js
364
+ // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
365
365
  function validateStringFields(params, fields) {
366
366
  if (params === null || params === void 0) {
367
367
  return { field: "params", message: "params is required" };
@@ -575,12 +575,10 @@ function createRecommendationPlugin(options) {
575
575
  // package.json
576
576
  var package_default = {
577
577
  name: "@ashdev/codex-plugin-recommendations-echo",
578
- version: "1.35.0",
578
+ version: "1.36.0",
579
579
  description: "Echo recommendations plugin for testing and debugging the Codex plugin recommendations protocol",
580
580
  main: "dist/index.js",
581
- bin: {
582
- "codex-plugin-recommendations-echo": "dist/index.js"
583
- },
581
+ bin: "dist/index.js",
584
582
  type: "module",
585
583
  files: [
586
584
  "dist",
@@ -588,7 +586,7 @@ var package_default = {
588
586
  ],
589
587
  repository: {
590
588
  type: "git",
591
- url: "git+https://github.com/AshDevFr/codex.git",
589
+ url: "https://github.com/AshDevFr/codex.git",
592
590
  directory: "plugins/recommendations-echo"
593
591
  },
594
592
  scripts: {
@@ -616,7 +614,7 @@ var package_default = {
616
614
  node: ">=22.0.0"
617
615
  },
618
616
  dependencies: {
619
- "@ashdev/codex-plugin-sdk": "file:../sdk-typescript"
617
+ "@ashdev/codex-plugin-sdk": "^1.36.0"
620
618
  },
621
619
  devDependencies: {
622
620
  "@biomejs/biome": "^2.4.4",
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../sdk-typescript/src/types/rpc.ts", "../../sdk-typescript/src/errors.ts", "../../sdk-typescript/src/request-context.ts", "../../sdk-typescript/src/host-rpc.ts", "../../sdk-typescript/src/logger.ts", "../../sdk-typescript/src/server.ts", "../../sdk-typescript/src/storage.ts", "../package.json", "../src/manifest.ts", "../src/recorder.ts", "../src/index.ts"],
4
- "sourcesContent": ["/**\n * JSON-RPC 2.0 types for plugin communication\n */\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: string | number | null;\n method: string;\n params?: unknown;\n /**\n * Reverse-RPC only: id of the forward call this plugin is currently\n * servicing. Tells the host to route the reverse-RPC back to the\n * originating caller's task so emitted events land in that caller's\n * recording broadcaster (and replay correctly in distributed\n * deployments). The SDK stamps this automatically via\n * `AsyncLocalStorage` \u2014 plugin authors don't set it.\n */\n parentRequestId?: string | number | null;\n}\n\nexport interface JsonRpcSuccessResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n result: unknown;\n}\n\nexport interface JsonRpcErrorResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n error: JsonRpcError;\n}\n\nexport interface JsonRpcError {\n code: number;\n message: string;\n data?: unknown;\n}\n\nexport type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;\n\n/**\n * Standard JSON-RPC error codes\n */\nexport const JSON_RPC_ERROR_CODES = {\n /** Invalid JSON was received */\n PARSE_ERROR: -32700,\n /** The JSON sent is not a valid Request object */\n INVALID_REQUEST: -32600,\n /** The method does not exist / is not available */\n METHOD_NOT_FOUND: -32601,\n /** Invalid method parameter(s) */\n INVALID_PARAMS: -32602,\n /** Internal JSON-RPC error */\n INTERNAL_ERROR: -32603,\n} as const;\n\n/**\n * Plugin-specific error codes (in the -32000 to -32099 range)\n */\nexport const PLUGIN_ERROR_CODES = {\n /** Rate limited by external API */\n RATE_LIMITED: -32001,\n /** Resource not found (e.g., series ID doesn't exist) */\n NOT_FOUND: -32002,\n /** Authentication failed (invalid credentials) */\n AUTH_FAILED: -32003,\n /** External API error */\n API_ERROR: -32004,\n /** Plugin configuration error */\n CONFIG_ERROR: -32005,\n} as const;\n", "/**\n * Plugin error classes for structured error handling\n */\n\nimport { type JsonRpcError, PLUGIN_ERROR_CODES } from \"./types/rpc.js\";\n\n/**\n * Base class for plugin errors that map to JSON-RPC errors\n */\nexport abstract class PluginError extends Error {\n abstract readonly code: number;\n readonly data?: unknown;\n\n constructor(message: string, data?: unknown) {\n super(message);\n this.name = this.constructor.name;\n this.data = data;\n }\n\n /**\n * Convert to JSON-RPC error format\n */\n toJsonRpcError(): JsonRpcError {\n return {\n code: this.code,\n message: this.message,\n data: this.data,\n };\n }\n}\n\n/**\n * Thrown when rate limited by an external API\n */\nexport class RateLimitError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.RATE_LIMITED;\n /** Seconds to wait before retrying */\n readonly retryAfterSeconds: number;\n\n constructor(retryAfterSeconds: number, message?: string) {\n super(message ?? `Rate limited, retry after ${retryAfterSeconds}s`, {\n retryAfterSeconds,\n });\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\n/**\n * Thrown when a requested resource is not found\n */\nexport class NotFoundError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.NOT_FOUND;\n}\n\n/**\n * Thrown when authentication fails (invalid credentials)\n */\nexport class AuthError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.AUTH_FAILED;\n\n constructor(message?: string) {\n super(message ?? \"Authentication failed\");\n }\n}\n\n/**\n * Thrown when an external API returns an error\n */\nexport class ApiError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.API_ERROR;\n readonly statusCode: number | undefined;\n\n constructor(message: string, statusCode?: number) {\n super(message, statusCode !== undefined ? { statusCode } : undefined);\n this.statusCode = statusCode;\n }\n}\n\n/**\n * Thrown when the plugin is misconfigured\n */\nexport class ConfigError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.CONFIG_ERROR;\n}\n", "/**\n * Async-local context for the currently-handled forward request.\n *\n * When the SDK dispatches a forward call (e.g. `releases/poll`), it stores\n * the call's `id` in this context for the duration of the handler. Any\n * reverse-RPC the plugin makes while servicing that call (e.g.\n * `releases/record` via `HostRpcClient.call`) reads the id and stamps it as\n * `parentRequestId` on the outgoing request.\n *\n * The host uses `parentRequestId` to route the reverse-RPC back to the\n * originating caller's tokio task, so emitted events land in the recording\n * broadcaster scoped to that task and replay correctly in distributed\n * deployments. Without this stamping, plugins that emit events via\n * reverse-RPC would silently lose them on the worker.\n *\n * Plugin authors don't interact with this directly. The SDK's request\n * dispatch (`server.ts`) sets it; `HostRpcClient.call` reads it.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst store = new AsyncLocalStorage<string | number | null>();\n\n/**\n * Run `fn` with `forwardRequestId` as the current parent. Calls to\n * `currentParentRequestId()` made inside `fn` (or anything it awaits) will\n * see this value.\n */\nexport function runWithParentRequestId<T>(\n forwardRequestId: string | number | null,\n fn: () => Promise<T>,\n): Promise<T> {\n return store.run(forwardRequestId, fn);\n}\n\n/**\n * Snapshot the current forward request id, or `undefined` if no forward\n * request is on the call stack (e.g. background timers in the plugin that\n * fire reverse-RPCs outside a forward-call context \u2014 those won't be replay-\n * eligible, by design, since they don't belong to any task).\n */\nexport function currentParentRequestId(): string | number | null | undefined {\n return store.getStore();\n}\n", "/**\n * Generic host reverse-RPC client.\n *\n * Plugins use this to call host methods outside the storage namespace \u2014\n * notably `releases/list_tracked`, `releases/record`,\n * `releases/source_state/get`, and `releases/source_state/set`. The class is\n * intentionally generic so future reverse-RPC namespaces can reuse it\n * without a per-namespace client.\n *\n * Wire-format and lifecycle mirror `PluginStorage`: send a JSON-RPC request\n * over stdout with a unique id, and resolve when the host's response with\n * the matching id arrives on stdin. The plugin server's main loop calls\n * `handleResponse(line)` on every incoming response; whichever client owns\n * the id resolves it (others no-op silently).\n *\n * The id counter starts at a high value (`1_000_000_000`) so it can never\n * collide with `PluginStorage`'s sequence (`1, 2, 3, ...`). This means the\n * dispatch in the server doesn't need to know which client a response\n * belongs to \u2014 it can fan out to both, and at most one will match.\n */\n\nimport { currentParentRequestId } from \"./request-context.js\";\nimport type { JsonRpcError, JsonRpcRequest } from \"./types/rpc.js\";\n\n/** Write function signature for sending JSON-RPC requests. */\ntype WriteFn = (line: string) => void;\n\n/**\n * Error thrown when a reverse-RPC call fails (host returned a JSON-RPC error,\n * or the client was canceled).\n */\nexport class HostRpcError extends Error {\n constructor(\n message: string,\n public readonly code: number,\n public readonly data?: unknown,\n ) {\n super(message);\n this.name = \"HostRpcError\";\n }\n}\n\n/**\n * Generic reverse-RPC client. Construct one per plugin instance and pass it\n * around via `InitializeParams`.\n */\nexport class HostRpcClient {\n // Start the counter high so it can't collide with PluginStorage's id space.\n // `Number.MAX_SAFE_INTEGER` is far above this, so we have plenty of room\n // before wrapping (and we never expect a single plugin lifetime to issue\n // more than ~9 quintillion calls).\n private nextId = 1_000_000_000;\n private pendingRequests = new Map<\n number,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n }\n >();\n private writeFn: WriteFn;\n\n /**\n * @param writeFn - Optional custom write function (defaults to\n * `process.stdout.write`). Useful for testing.\n */\n constructor(writeFn?: WriteFn) {\n this.writeFn =\n writeFn ??\n ((line: string) => {\n process.stdout.write(line);\n });\n }\n\n /**\n * Send a JSON-RPC request to the host and resolve with the result.\n *\n * @param method - JSON-RPC method name (e.g. `\"releases/list_tracked\"`).\n * @param params - Method-specific params. Pass `undefined` when the method\n * takes no params.\n */\n async call<T = unknown>(method: string, params?: unknown): Promise<T> {\n const id = this.nextId++;\n // Stamp the forward call we're inside so the host can route this\n // reverse-RPC back to the originating caller's task. Lifted from the\n // `request-context` async-local storage that `server.ts` sets around\n // every forward-request handler.\n const parent = currentParentRequestId();\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id,\n method,\n params,\n ...(parent !== undefined ? { parentRequestId: parent } : {}),\n };\n\n return new Promise<T>((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: (v) => resolve(v as T),\n reject,\n });\n try {\n this.writeFn(`${JSON.stringify(request)}\\n`);\n } catch (err) {\n this.pendingRequests.delete(id);\n const message = err instanceof Error ? err.message : \"Unknown write error\";\n reject(new HostRpcError(`Failed to send request: ${message}`, -1));\n }\n });\n }\n\n /**\n * Process an incoming JSON-RPC response line. Returns `true` if this\n * client owned the response id and resolved it, `false` otherwise (so\n * other clients can try).\n *\n * Called by the plugin server's main loop on every response.\n */\n handleResponse(line: string): boolean {\n const trimmed = line.trim();\n if (!trimmed) return false;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return false;\n }\n\n const obj = parsed as Record<string, unknown>;\n if (obj.method !== undefined) return false; // not a response\n const rawId = obj.id;\n if (typeof rawId !== \"number\") return false;\n if (!this.pendingRequests.has(rawId)) return false;\n\n const pending = this.pendingRequests.get(rawId);\n if (!pending) return false;\n this.pendingRequests.delete(rawId);\n\n if (\"error\" in obj && obj.error) {\n const err = obj.error as JsonRpcError;\n pending.reject(new HostRpcError(err.message, err.code, err.data));\n } else {\n pending.resolve(obj.result);\n }\n return true;\n }\n\n /** Reject all pending requests (e.g. on shutdown). */\n cancelAll(): void {\n for (const [, pending] of this.pendingRequests) {\n pending.reject(new HostRpcError(\"Host RPC client stopped\", -1));\n }\n this.pendingRequests.clear();\n }\n}\n", "/**\n * Logging utilities for plugins\n *\n * IMPORTANT: Plugins must ONLY write to stderr for logging.\n * stdout is reserved for JSON-RPC communication.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport interface LoggerOptions {\n /** Plugin name to prefix log messages */\n name: string;\n /** Minimum log level (default: \"info\") */\n level?: LogLevel;\n /** Whether to include timestamps (default: true) */\n timestamps?: boolean;\n}\n\n/**\n * Logger that writes to stderr (safe for plugins)\n */\nexport class Logger {\n private readonly name: string;\n private readonly minLevel: number;\n private readonly timestamps: boolean;\n\n constructor(options: LoggerOptions) {\n this.name = options.name;\n this.minLevel = LOG_LEVELS[options.level ?? \"info\"];\n this.timestamps = options.timestamps ?? true;\n }\n\n private shouldLog(level: LogLevel): boolean {\n return LOG_LEVELS[level] >= this.minLevel;\n }\n\n private format(level: LogLevel, message: string, data?: unknown): string {\n const parts: string[] = [];\n\n if (this.timestamps) {\n parts.push(new Date().toISOString());\n }\n\n parts.push(`[${level.toUpperCase()}]`);\n parts.push(`[${this.name}]`);\n parts.push(message);\n\n if (data !== undefined) {\n if (data instanceof Error) {\n parts.push(`- ${data.message}`);\n if (data.stack) {\n parts.push(`\\n${data.stack}`);\n }\n } else if (typeof data === \"object\") {\n parts.push(`- ${JSON.stringify(data)}`);\n } else {\n parts.push(`- ${String(data)}`);\n }\n }\n\n return parts.join(\" \");\n }\n\n private log(level: LogLevel, message: string, data?: unknown): void {\n if (this.shouldLog(level)) {\n // Write to stderr (not stdout!) - stdout is for JSON-RPC only\n process.stderr.write(`${this.format(level, message, data)}\\n`);\n }\n }\n\n debug(message: string, data?: unknown): void {\n this.log(\"debug\", message, data);\n }\n\n info(message: string, data?: unknown): void {\n this.log(\"info\", message, data);\n }\n\n warn(message: string, data?: unknown): void {\n this.log(\"warn\", message, data);\n }\n\n error(message: string, data?: unknown): void {\n this.log(\"error\", message, data);\n }\n}\n\n/**\n * Create a logger for a plugin\n */\nexport function createLogger(options: LoggerOptions): Logger {\n return new Logger(options);\n}\n", "/**\n * Plugin server - handles JSON-RPC communication over stdio\n *\n * Provides factory functions for creating different plugin types.\n * All plugin types share a common base server that handles:\n * - stdin readline parsing\n * - JSON-RPC error handling\n * - initialize/ping/shutdown lifecycle methods\n *\n * Each plugin type adds its own method routing on top.\n */\n\nimport { createInterface } from \"node:readline\";\nimport { PluginError } from \"./errors.js\";\nimport { HostRpcClient } from \"./host-rpc.js\";\nimport { createLogger, type Logger } from \"./logger.js\";\nimport { runWithParentRequestId } from \"./request-context.js\";\nimport { PluginStorage } from \"./storage.js\";\nimport type {\n BookMetadataProvider,\n MetadataContentType,\n MetadataProvider,\n RecommendationProvider,\n ReleaseSourceProvider,\n SyncProvider,\n} from \"./types/capabilities.js\";\nimport type { PluginManifest, ReleaseSourceCapability } from \"./types/manifest.js\";\nimport type {\n BookMatchParams,\n BookSearchParams,\n MetadataGetParams,\n MetadataMatchParams,\n MetadataSearchParams,\n} from \"./types/protocol.js\";\nimport type {\n ProfileUpdateRequest,\n RecommendationDismissRequest,\n RecommendationRequest,\n} from \"./types/recommendations.js\";\nimport type { ReleasePollRequest } from \"./types/releases.js\";\nimport { JSON_RPC_ERROR_CODES, type JsonRpcRequest, type JsonRpcResponse } from \"./types/rpc.js\";\nimport type { SyncPullRequest, SyncPushRequest } from \"./types/sync.js\";\n\n// =============================================================================\n// Parameter Validation\n// =============================================================================\n\ninterface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validate that the required string fields are present and non-empty\n */\nfunction validateStringFields(params: unknown, fields: string[]): ValidationError | null {\n if (params === null || params === undefined) {\n return { field: \"params\", message: \"params is required\" };\n }\n if (typeof params !== \"object\") {\n return { field: \"params\", message: \"params must be an object\" };\n }\n\n const obj = params as Record<string, unknown>;\n for (const field of fields) {\n const value = obj[field];\n if (value === undefined || value === null) {\n return { field, message: `${field} is required` };\n }\n if (typeof value !== \"string\") {\n return { field, message: `${field} must be a string` };\n }\n if (value.trim() === \"\") {\n return { field, message: `${field} cannot be empty` };\n }\n }\n\n return null;\n}\n\n/**\n * Validate MetadataSearchParams\n */\nfunction validateSearchParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"query\"]);\n}\n\n/**\n * Validate MetadataGetParams\n */\nfunction validateGetParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"externalId\"]);\n}\n\n/**\n * Validate MetadataMatchParams\n */\nfunction validateMatchParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"title\"]);\n}\n\n/**\n * Validate BookSearchParams - requires either isbn or query\n */\nfunction validateBookSearchParams(params: unknown): ValidationError | null {\n if (params === null || params === undefined) {\n return { field: \"params\", message: \"params is required\" };\n }\n if (typeof params !== \"object\") {\n return { field: \"params\", message: \"params must be an object\" };\n }\n\n const obj = params as Record<string, unknown>;\n const hasIsbn = obj.isbn !== undefined && obj.isbn !== null && obj.isbn !== \"\";\n const hasQuery = obj.query !== undefined && obj.query !== null && obj.query !== \"\";\n\n if (!hasIsbn && !hasQuery) {\n return { field: \"isbn/query\", message: \"either isbn or query is required\" };\n }\n\n return null;\n}\n\n/**\n * Validate BookMatchParams\n */\nfunction validateBookMatchParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"title\"]);\n}\n\n/**\n * Create an INVALID_PARAMS error response\n */\nfunction invalidParamsError(id: string | number | null, error: ValidationError): JsonRpcResponse {\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,\n message: `Invalid params: ${error.message}`,\n data: { field: error.field },\n },\n };\n}\n\n// =============================================================================\n// Shared Types\n// =============================================================================\n\n/**\n * Initialize parameters received from Codex\n */\nexport interface InitializeParams {\n /** Admin-level plugin configuration (from plugin settings) */\n adminConfig?: Record<string, unknown>;\n /** Per-user plugin configuration (from user plugin settings) */\n userConfig?: Record<string, unknown>;\n /** Plugin credentials (API keys, tokens, etc.) */\n credentials?: Record<string, string>;\n /**\n * Scoped, writable data directory for this plugin's file storage.\n *\n * The host creates it at `{plugins_dir}/{plugin_name}/` and passes the\n * absolute path here. Unlike {@link storage} (a small DB-backed key-value\n * store with per-connection quotas), this is a real filesystem directory\n * with no quotas \u2014 use it for larger file-based storage (SQLite databases,\n * caches, debug dumps). Absent if the host has no plugin file storage\n * configured.\n */\n dataDir?: string;\n /**\n * Stable identifier of the Codex user this plugin instance is acting for.\n *\n * Sent for user-plugin spawns (sync / recommendation), absent for system\n * plugins. Because a credential-less or shared-key plugin can't derive the\n * user's identity from its credentials, use this (an opaque UUID) to scope\n * data per user in the plugin's own backend.\n */\n userId?: string;\n /**\n * Stable identifier of this user-plugin connection \u2014 the same scope the host\n * uses for {@link storage}. Sent for user-plugin spawns, absent for system\n * plugins. Opaque UUID. Use it when \"this connection\" (rather than the human)\n * is the right granularity.\n */\n userPluginId?: string;\n /**\n * Per-user key-value storage client.\n *\n * Use this to persist data across plugin restarts (e.g., dismissed IDs,\n * cached profiles, user preferences). Storage is scoped per user-plugin\n * instance \u2014 the host resolves the user context automatically.\n */\n storage: PluginStorage;\n /**\n * Generic host reverse-RPC client.\n *\n * Use this to call host methods outside the storage namespace, notably\n * the `releases/*` methods (`releases/list_tracked`, `releases/record`,\n * `releases/source_state/get`, `releases/source_state/set`) for plugins\n * declaring the `releaseSource` capability.\n */\n hostRpc: HostRpcClient;\n}\n\n/**\n * A method router handles capability-specific JSON-RPC methods.\n * Returns a response for known methods, or null to indicate \"not my method\".\n */\ntype MethodRouter = (\n method: string,\n params: unknown,\n id: string | number | null,\n) => Promise<JsonRpcResponse | null>;\n\n// =============================================================================\n// Shared Plugin Server\n// =============================================================================\n\ninterface PluginServerOptions {\n manifest: PluginManifest;\n onInitialize?: ((params: InitializeParams) => void | Promise<void>) | undefined;\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\" | undefined;\n label?: string | undefined;\n router: MethodRouter;\n}\n\n/**\n * Shared plugin server that handles JSON-RPC communication over stdio.\n *\n * Handles the common lifecycle methods (initialize, ping, shutdown) and\n * delegates capability-specific methods to the provided router.\n */\nfunction createPluginServer(options: PluginServerOptions): void {\n const { manifest, onInitialize, logLevel = \"info\", label, router } = options;\n const logger = createLogger({ name: manifest.name, level: logLevel });\n const prefix = label ? `${label} plugin` : \"plugin\";\n const storage = new PluginStorage();\n const hostRpc = new HostRpcClient();\n\n logger.info(`Starting ${prefix}: ${manifest.displayName} v${manifest.version}`);\n\n const rl = createInterface({\n input: process.stdin,\n terminal: false,\n });\n\n rl.on(\"line\", (line) => {\n void handleLine(line, manifest, onInitialize, router, logger, storage, hostRpc);\n });\n\n rl.on(\"close\", () => {\n logger.info(\"stdin closed, shutting down\");\n storage.cancelAll();\n hostRpc.cancelAll();\n process.exit(0);\n });\n\n process.on(\"uncaughtException\", (error) => {\n logger.error(\"Uncaught exception\", error);\n process.exit(1);\n });\n\n process.on(\"unhandledRejection\", (reason) => {\n logger.error(\"Unhandled rejection\", reason);\n });\n}\n\n/**\n * Detect whether a parsed JSON object is a JSON-RPC response (not a request).\n *\n * A response has `id` and either `result` or `error`, but no `method`.\n * A request always has `method`.\n */\nfunction isJsonRpcResponse(obj: Record<string, unknown>): boolean {\n if (obj.method !== undefined) return false;\n if (obj.id === undefined || obj.id === null) return false;\n return \"result\" in obj || \"error\" in obj;\n}\n\nasync function handleLine(\n line: string,\n manifest: PluginManifest,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n router: MethodRouter,\n logger: Logger,\n storage: PluginStorage,\n hostRpc: HostRpcClient,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Try to detect responses (storage or host-rpc) before full request handling.\n // Both come from the host on stdin \u2014 they have id + (result|error) but no\n // method field. The two clients use disjoint id ranges so each can claim\n // ownership without coordination; whichever owns the id resolves it.\n let parsed: Record<string, unknown> | undefined;\n try {\n parsed = JSON.parse(trimmed) as Record<string, unknown>;\n } catch {\n // Will be handled as a parse error below\n }\n\n if (parsed && isJsonRpcResponse(parsed)) {\n logger.debug(\"Routing reverse-RPC response\", { id: parsed.id });\n if (!hostRpc.handleResponse(trimmed)) {\n storage.handleResponse(trimmed);\n }\n return;\n }\n\n let id: string | number | null = null;\n\n try {\n const request = (parsed ?? JSON.parse(trimmed)) as JsonRpcRequest;\n id = request.id;\n\n logger.debug(`Received request: ${request.method}`, { id: request.id });\n\n // Run the request handler inside the parent-request async-local context.\n // Reverse-RPCs the handler issues via `HostRpcClient.call` will read this\n // and stamp `parentRequestId` so the host can route the call back to the\n // originating task. See `request-context.ts`.\n const response = await runWithParentRequestId(request.id, () =>\n handleRequest(request, manifest, onInitialize, router, logger, storage, hostRpc),\n );\n if (response !== null) {\n writeResponse(response);\n }\n } catch (error) {\n if (error instanceof SyntaxError) {\n writeResponse({\n jsonrpc: \"2.0\",\n id: null,\n error: {\n code: JSON_RPC_ERROR_CODES.PARSE_ERROR,\n message: \"Parse error: invalid JSON\",\n },\n });\n } else if (error instanceof PluginError) {\n writeResponse({\n jsonrpc: \"2.0\",\n id,\n error: error.toJsonRpcError(),\n });\n } else {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n logger.error(\"Request failed\", error);\n writeResponse({\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,\n message,\n },\n });\n }\n }\n}\n\nasync function handleRequest(\n request: JsonRpcRequest,\n manifest: PluginManifest,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n router: MethodRouter,\n logger: Logger,\n storage: PluginStorage,\n hostRpc: HostRpcClient,\n): Promise<JsonRpcResponse | null> {\n const { method, params, id } = request;\n\n // Common lifecycle methods\n switch (method) {\n case \"initialize\": {\n const initParams = (params ?? {}) as InitializeParams;\n // Inject the reverse-RPC clients so plugins can persist data and\n // call host-side methods (e.g. releases/list_tracked).\n initParams.storage = storage;\n initParams.hostRpc = hostRpc;\n if (onInitialize) {\n await onInitialize(initParams);\n }\n return { jsonrpc: \"2.0\", id, result: manifest };\n }\n\n case \"ping\":\n return { jsonrpc: \"2.0\", id, result: \"pong\" };\n\n case \"shutdown\": {\n logger.info(\"Shutdown requested\");\n storage.cancelAll();\n hostRpc.cancelAll();\n const response: JsonRpcResponse = { jsonrpc: \"2.0\", id, result: null };\n process.stdout.write(`${JSON.stringify(response)}\\n`, () => {\n process.exit(0);\n });\n // Response already written above; return null so handleLine skips the write\n return null;\n }\n }\n\n // Delegate to capability-specific router\n const response = await router(method, params, id);\n if (response !== null) {\n return response;\n }\n\n // Unknown method\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,\n message: `Method not found: ${method}`,\n },\n };\n}\n\nfunction writeResponse(response: JsonRpcResponse): void {\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n}\n\n// =============================================================================\n// Response Helpers\n// =============================================================================\n\nfunction methodNotFound(id: string | number | null, message: string): JsonRpcResponse {\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,\n message,\n },\n };\n}\n\nfunction success(id: string | number | null, result: unknown): JsonRpcResponse {\n return { jsonrpc: \"2.0\", id, result };\n}\n\n// =============================================================================\n// Metadata Plugin\n// =============================================================================\n\n/**\n * Options for creating a metadata plugin\n */\nexport interface MetadataPluginOptions {\n /** Plugin manifest - must have capabilities.metadataProvider with content types */\n manifest: PluginManifest & {\n capabilities: { metadataProvider: MetadataContentType[] };\n };\n /** Series MetadataProvider implementation (required if \"series\" in metadataProvider) */\n provider?: MetadataProvider;\n /** Book MetadataProvider implementation (required if \"book\" in metadataProvider) */\n bookProvider?: BookMetadataProvider;\n /** Called when plugin receives initialize with credentials/config */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\") */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n/**\n * Create and run a metadata provider plugin\n *\n * Creates a plugin server that handles JSON-RPC communication over stdio.\n * The TypeScript compiler will ensure you implement all required methods.\n *\n * @example\n * ```typescript\n * import { createMetadataPlugin, type MetadataProvider } from \"@ashdev/codex-plugin-sdk\";\n *\n * const provider: MetadataProvider = {\n * async search(params) {\n * return {\n * results: [{\n * externalId: \"123\",\n * title: \"Example\",\n * alternateTitles: [],\n * relevanceScore: 0.95,\n * }],\n * };\n * },\n * async get(params) {\n * return {\n * externalId: params.externalId,\n * externalUrl: \"https://example.com/123\",\n * alternateTitles: [],\n * genres: [],\n * tags: [],\n * authors: [],\n * artists: [],\n * externalLinks: [],\n * };\n * },\n * };\n *\n * createMetadataPlugin({\n * manifest: {\n * name: \"my-plugin\",\n * displayName: \"My Plugin\",\n * version: \"1.0.0\",\n * description: \"Example plugin\",\n * author: \"Me\",\n * protocolVersion: \"1.0\",\n * capabilities: { metadataProvider: [\"series\"] },\n * },\n * provider,\n * });\n * ```\n */\nexport function createMetadataPlugin(options: MetadataPluginOptions): void {\n const { manifest, provider, bookProvider, onInitialize, logLevel } = options;\n\n // Validate that required providers are present based on manifest\n const contentTypes = manifest.capabilities.metadataProvider;\n if (contentTypes.includes(\"series\") && !provider) {\n throw new Error(\n \"Series metadata provider is required when 'series' is in metadataProvider capabilities\",\n );\n }\n if (contentTypes.includes(\"book\") && !bookProvider) {\n throw new Error(\n \"Book metadata provider is required when 'book' is in metadataProvider capabilities\",\n );\n }\n\n const router: MethodRouter = async (method, params, id) => {\n switch (method) {\n // Series metadata methods\n case \"metadata/series/search\": {\n if (!provider) return methodNotFound(id, \"This plugin does not support series metadata\");\n const err = validateSearchParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await provider.search(params as MetadataSearchParams));\n }\n case \"metadata/series/get\": {\n if (!provider) return methodNotFound(id, \"This plugin does not support series metadata\");\n const err = validateGetParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await provider.get(params as MetadataGetParams));\n }\n case \"metadata/series/match\": {\n if (!provider) return methodNotFound(id, \"This plugin does not support series metadata\");\n if (!provider.match) return methodNotFound(id, \"This plugin does not support series match\");\n const err = validateMatchParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await provider.match(params as MetadataMatchParams));\n }\n\n // Book metadata methods\n case \"metadata/book/search\": {\n if (!bookProvider) return methodNotFound(id, \"This plugin does not support book metadata\");\n const err = validateBookSearchParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await bookProvider.search(params as BookSearchParams));\n }\n case \"metadata/book/get\": {\n if (!bookProvider) return methodNotFound(id, \"This plugin does not support book metadata\");\n const err = validateGetParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await bookProvider.get(params as MetadataGetParams));\n }\n case \"metadata/book/match\": {\n if (!bookProvider) return methodNotFound(id, \"This plugin does not support book metadata\");\n if (!bookProvider.match)\n return methodNotFound(id, \"This plugin does not support book match\");\n const err = validateBookMatchParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await bookProvider.match(params as BookMatchParams));\n }\n\n default:\n return null;\n }\n };\n\n createPluginServer({ manifest, onInitialize, logLevel, router });\n}\n\n// =============================================================================\n// Sync Plugin\n// =============================================================================\n\n/**\n * Options for creating a sync provider plugin\n */\nexport interface SyncPluginOptions {\n /** Plugin manifest - must have capabilities.userReadSync: true */\n manifest: PluginManifest & {\n capabilities: { userReadSync: true };\n };\n /** SyncProvider implementation */\n provider: SyncProvider;\n /** Called when plugin receives initialize with credentials/config */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\") */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n/**\n * Create and run a sync provider plugin\n *\n * Creates a plugin server that handles JSON-RPC communication over stdio\n * for sync operations (push/pull reading progress with external services).\n *\n * @example\n * ```typescript\n * import { createSyncPlugin, type SyncProvider } from \"@ashdev/codex-plugin-sdk\";\n *\n * const provider: SyncProvider = {\n * async getUserInfo() {\n * return { externalId: \"123\", username: \"user\" };\n * },\n * async pushProgress(params) {\n * return { success: [], failed: [] };\n * },\n * async pullProgress(params) {\n * return { entries: [], hasMore: false };\n * },\n * };\n *\n * createSyncPlugin({\n * manifest: {\n * name: \"my-sync-plugin\",\n * displayName: \"My Sync Plugin\",\n * version: \"1.0.0\",\n * description: \"Syncs reading progress\",\n * author: \"Me\",\n * protocolVersion: \"1.0\",\n * capabilities: { userReadSync: true },\n * },\n * provider,\n * });\n * ```\n */\nexport function createSyncPlugin(options: SyncPluginOptions): void {\n const { manifest, provider, onInitialize, logLevel } = options;\n\n const router: MethodRouter = async (method, params, id) => {\n switch (method) {\n case \"sync/getUserInfo\":\n return success(id, await provider.getUserInfo());\n case \"sync/pushProgress\":\n return success(id, await provider.pushProgress(params as SyncPushRequest));\n case \"sync/pullProgress\":\n return success(id, await provider.pullProgress(params as SyncPullRequest));\n case \"sync/status\": {\n if (!provider.status) return methodNotFound(id, \"This plugin does not support sync/status\");\n return success(id, await provider.status());\n }\n default:\n return null;\n }\n };\n\n createPluginServer({ manifest, onInitialize, logLevel, label: \"sync\", router });\n}\n\n// =============================================================================\n// Recommendation Plugin\n// =============================================================================\n\n/**\n * Options for creating a recommendation provider plugin\n */\nexport interface RecommendationPluginOptions {\n /** Plugin manifest - must have capabilities.userRecommendationProvider: true */\n manifest: PluginManifest & {\n capabilities: { userRecommendationProvider: true };\n };\n /** RecommendationProvider implementation */\n provider: RecommendationProvider;\n /** Called when plugin receives initialize with credentials/config */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\") */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n/**\n * Create and run a recommendation provider plugin\n *\n * Creates a plugin server that handles JSON-RPC communication over stdio\n * for recommendation operations (get recommendations, update profile, dismiss).\n */\nexport function createRecommendationPlugin(options: RecommendationPluginOptions): void {\n const { manifest, provider, onInitialize, logLevel } = options;\n\n const router: MethodRouter = async (method, params, id) => {\n switch (method) {\n case \"recommendations/get\":\n return success(id, await provider.get(params as RecommendationRequest));\n case \"recommendations/updateProfile\": {\n if (!provider.updateProfile)\n return methodNotFound(id, \"This plugin does not support recommendations/updateProfile\");\n return success(id, await provider.updateProfile(params as ProfileUpdateRequest));\n }\n case \"recommendations/clear\": {\n if (!provider.clear)\n return methodNotFound(id, \"This plugin does not support recommendations/clear\");\n return success(id, await provider.clear());\n }\n case \"recommendations/dismiss\": {\n if (!provider.dismiss)\n return methodNotFound(id, \"This plugin does not support recommendations/dismiss\");\n const err = validateStringFields(params, [\"externalId\"]);\n if (err) return invalidParamsError(id, err);\n return success(id, await provider.dismiss(params as RecommendationDismissRequest));\n }\n default:\n return null;\n }\n };\n\n createPluginServer({ manifest, onInitialize, logLevel, label: \"recommendation\", router });\n}\n\n// =============================================================================\n// Release Source Plugin\n// =============================================================================\n\n/**\n * Validate `releases/poll` parameters. Requires a non-empty `sourceId` string;\n * `etag` is optional.\n */\nfunction validateReleasePollParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"sourceId\"]);\n}\n\n/**\n * Options for creating a release-source plugin.\n */\nexport interface ReleaseSourcePluginOptions {\n /** Plugin manifest. Must declare `capabilities.releaseSource`. */\n manifest: PluginManifest & {\n capabilities: { releaseSource: ReleaseSourceCapability };\n };\n /** ReleaseSourceProvider implementation. */\n provider: ReleaseSourceProvider;\n /** Called when plugin receives initialize with credentials/config. */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\"). */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n/**\n * Create and run a release-source plugin.\n *\n * The host calls `releases/poll` on a schedule (per `release_sources` row).\n * The plugin returns candidates either inline (in the poll response) or by\n * streaming `releases/record` reverse-RPC calls during the poll. Both styles\n * are supported by the host.\n *\n * Plugins typically:\n * 1. Fetch tracked series via `releases/list_tracked`.\n * 2. For each series, GET the upstream feed (with `If-None-Match` from the\n * previous ETag).\n * 3. Parse + filter (language, group blocklist, etc.).\n * 4. Either return all candidates in the poll response or call\n * `releases/record` for each.\n * 5. Persist the new ETag via `releases/source_state/set` (or include it on\n * the poll response).\n *\n * @example\n * ```typescript\n * import { createReleaseSourcePlugin, type ReleaseSourceProvider } from \"@ashdev/codex-plugin-sdk\";\n *\n * const provider: ReleaseSourceProvider = {\n * async poll({ sourceId, etag }) {\n * // ...fetch + parse...\n * return { candidates: [...], etag: \"new-etag\" };\n * },\n * };\n *\n * createReleaseSourcePlugin({ manifest, provider });\n * ```\n */\nexport function createReleaseSourcePlugin(options: ReleaseSourcePluginOptions): void {\n const { manifest, provider, onInitialize, logLevel } = options;\n\n if (!manifest.capabilities.releaseSource) {\n throw new Error(\n \"manifest.capabilities.releaseSource is required for createReleaseSourcePlugin\",\n );\n }\n\n const router: MethodRouter = async (method, params, id) => {\n switch (method) {\n case \"releases/poll\": {\n const err = validateReleasePollParams(params);\n if (err) return invalidParamsError(id, err);\n return success(id, await provider.poll(params as ReleasePollRequest));\n }\n default:\n return null;\n }\n };\n\n createPluginServer({ manifest, onInitialize, logLevel, label: \"release-source\", router });\n}\n", "/**\n * Plugin Storage - key-value storage for per-user plugin data\n *\n * Storage is scoped per user-plugin instance. Plugins only specify a key;\n * the host resolves the user context from the connection.\n *\n * Plugins send storage requests as JSON-RPC calls to the host over stdout\n * and receive responses on stdin. This is the reverse of the normal\n * host-to-plugin request flow.\n *\n * @example\n * ```typescript\n * import { PluginStorage } from \"@ashdev/codex-plugin-sdk\";\n *\n * const storage = new PluginStorage();\n *\n * // Store data\n * await storage.set(\"taste_profile\", { genres: [\"action\", \"drama\"] });\n *\n * // Retrieve data\n * const data = await storage.get(\"taste_profile\");\n *\n * // Store with TTL (expires in 24 hours)\n * const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();\n * await storage.set(\"cache\", { items: [1, 2, 3] }, expires);\n *\n * // List all keys\n * const keys = await storage.list();\n *\n * // Delete a key\n * await storage.delete(\"cache\");\n *\n * // Clear all data\n * await storage.clear();\n * ```\n */\n\nimport type { JsonRpcError, JsonRpcRequest } from \"./types/rpc.js\";\n\n// =============================================================================\n// Storage Types\n// =============================================================================\n\n/** Response from storage/get */\nexport interface StorageGetResponse {\n /** The stored data, or null if key doesn't exist */\n data: unknown | null;\n /** Expiration timestamp (ISO 8601) if TTL was set */\n expiresAt?: string;\n}\n\n/** Response from storage/set */\nexport interface StorageSetResponse {\n /** Always true on success */\n success: boolean;\n}\n\n/** Response from storage/delete */\nexport interface StorageDeleteResponse {\n /** Whether the key existed and was deleted */\n deleted: boolean;\n}\n\n/** Individual key entry from storage/list */\nexport interface StorageKeyEntry {\n /** Storage key name */\n key: string;\n /** Expiration timestamp (ISO 8601) if TTL was set */\n expiresAt?: string;\n /** Last update timestamp (ISO 8601) */\n updatedAt: string;\n}\n\n/** Response from storage/list */\nexport interface StorageListResponse {\n /** All keys for this plugin instance (excluding expired) */\n keys: StorageKeyEntry[];\n}\n\n/** Response from storage/clear */\nexport interface StorageClearResponse {\n /** Number of entries deleted */\n deletedCount: number;\n}\n\n// =============================================================================\n// Storage Error\n// =============================================================================\n\n/** Error from a storage operation */\nexport class StorageError extends Error {\n constructor(\n message: string,\n public readonly code: number,\n public readonly data?: unknown,\n ) {\n super(message);\n this.name = \"StorageError\";\n }\n}\n\n// =============================================================================\n// Plugin Storage Client\n// =============================================================================\n\n/** Write function signature for sending JSON-RPC requests */\ntype WriteFn = (line: string) => void;\n\n/**\n * Client for plugin key-value storage.\n *\n * Sends JSON-RPC requests to the host process over stdout and reads\n * responses on stdin. Each request gets a unique ID so responses can\n * be correlated even if they arrive out of order.\n */\nexport class PluginStorage {\n private nextId = 1;\n private pendingRequests = new Map<\n string | number,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n }\n >();\n private writeFn: WriteFn;\n\n /**\n * Create a new storage client.\n *\n * @param writeFn - Optional custom write function (defaults to process.stdout.write).\n * Useful for testing or custom transport layers.\n */\n constructor(writeFn?: WriteFn) {\n this.writeFn =\n writeFn ??\n ((line: string) => {\n process.stdout.write(line);\n });\n }\n\n /**\n * Get a value by key\n *\n * @param key - Storage key to retrieve\n * @returns The stored data and optional expiration, or null data if key doesn't exist\n */\n async get(key: string): Promise<StorageGetResponse> {\n return (await this.sendRequest(\"storage/get\", { key })) as StorageGetResponse;\n }\n\n /**\n * Set a value by key (upsert - creates or updates)\n *\n * @param key - Storage key\n * @param data - JSON-serializable data to store\n * @param expiresAt - Optional expiration timestamp (ISO 8601)\n * @returns Success indicator\n */\n async set(key: string, data: unknown, expiresAt?: string): Promise<StorageSetResponse> {\n const params: Record<string, unknown> = { key, data };\n if (expiresAt !== undefined) {\n params.expiresAt = expiresAt;\n }\n return (await this.sendRequest(\"storage/set\", params)) as StorageSetResponse;\n }\n\n /**\n * Delete a value by key\n *\n * @param key - Storage key to delete\n * @returns Whether the key existed and was deleted\n */\n async delete(key: string): Promise<StorageDeleteResponse> {\n return (await this.sendRequest(\"storage/delete\", { key })) as StorageDeleteResponse;\n }\n\n /**\n * List all keys for this plugin instance (excluding expired)\n *\n * @returns List of key entries with metadata\n */\n async list(): Promise<StorageListResponse> {\n return (await this.sendRequest(\"storage/list\", {})) as StorageListResponse;\n }\n\n /**\n * Clear all data for this plugin instance\n *\n * @returns Number of entries deleted\n */\n async clear(): Promise<StorageClearResponse> {\n return (await this.sendRequest(\"storage/clear\", {})) as StorageClearResponse;\n }\n\n /**\n * Handle an incoming JSON-RPC response line from the host.\n *\n * Call this method from your readline handler to deliver responses\n * back to pending storage requests.\n */\n handleResponse(line: string): void {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n // Not JSON - ignore\n return;\n }\n\n const obj = parsed as Record<string, unknown>;\n\n // Only handle responses (have \"result\" or \"error\", no \"method\")\n if (obj.method !== undefined) {\n // This is a host-to-plugin request, not a storage response - ignore\n return;\n }\n\n const id = obj.id;\n if (id === undefined || id === null) return;\n\n const pending = this.pendingRequests.get(id as string | number);\n if (!pending) return;\n\n this.pendingRequests.delete(id as string | number);\n\n if (\"error\" in obj && obj.error) {\n const err = obj.error as JsonRpcError;\n pending.reject(new StorageError(err.message, err.code, err.data));\n } else {\n pending.resolve(obj.result);\n }\n }\n\n /**\n * Cancel all pending requests (e.g. on shutdown).\n */\n cancelAll(): void {\n for (const [, pending] of this.pendingRequests) {\n pending.reject(new StorageError(\"Storage client stopped\", -1));\n }\n this.pendingRequests.clear();\n }\n\n // ===========================================================================\n // Internal\n // ===========================================================================\n\n private sendRequest(method: string, params: unknown): Promise<unknown> {\n const id = this.nextId++;\n\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id,\n method,\n params,\n };\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, { resolve, reject });\n\n try {\n this.writeFn(`${JSON.stringify(request)}\\n`);\n } catch (err) {\n this.pendingRequests.delete(id);\n const message = err instanceof Error ? err.message : \"Unknown write error\";\n reject(new StorageError(`Failed to send request: ${message}`, -1));\n }\n });\n }\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-recommendations-echo\",\n \"version\": \"1.35.0\",\n \"description\": \"Echo recommendations plugin for testing and debugging the Codex plugin recommendations protocol\",\n \"main\": \"dist/index.js\",\n \"bin\": {\n \"codex-plugin-recommendations-echo\": \"dist/index.js\"\n },\n \"type\": \"module\",\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/AshDevFr/codex.git\",\n \"directory\": \"plugins/recommendations-echo\"\n },\n \"scripts\": {\n \"build\": \"esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'\",\n \"dev\": \"npm run build -- --watch\",\n \"clean\": \"rm -rf dist\",\n \"start\": \"node dist/index.js\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --passWithNoTests\",\n \"test:watch\": \"vitest\",\n \"prepublishOnly\": \"npm run lint && npm run build\"\n },\n \"keywords\": [\n \"codex\",\n \"plugin\",\n \"recommendations\",\n \"echo\",\n \"testing\"\n ],\n \"author\": \"Codex\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"dependencies\": {\n \"@ashdev/codex-plugin-sdk\": \"file:../sdk-typescript\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.4.4\",\n \"@types/node\": \"^22.0.0\",\n \"esbuild\": \"^0.27.3\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n }\n}\n", "import type { PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\n// Default config values\nexport const DEFAULT_MAX_PAYLOAD_FILES = 500;\n// Number of generic recommendations returned when the library is empty.\nexport const DEFAULT_FALLBACK_COUNT = 3;\n\nexport const manifest = {\n name: \"recommendations-echo\",\n displayName: \"Echo Recommendations Plugin\",\n version: packageJson.version,\n description:\n \"Test recommendations plugin that echoes library seeds back as recommendations. Records every request/response to files for debugging.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n userRecommendationProvider: true,\n // Opt in to enriched series data (metadata/custom metadata) so the\n // enrichment toggles appear and the payloads can be inspected.\n wantsFullMetadata: true,\n },\n configSchema: {\n description: \"Configuration options for the Echo recommendations test plugin\",\n fields: [\n {\n key: \"recordPayloads\",\n label: \"Record Payloads\",\n description:\n \"Write each request and its response to JSON files under the plugin's data directory for debugging.\",\n type: \"boolean\" as const,\n required: false,\n default: true,\n },\n {\n key: \"maxPayloadFiles\",\n label: \"Max Payload Files\",\n description:\n \"Maximum number of recorded payload files to keep; oldest are pruned. Only used when payload recording is enabled.\",\n type: \"number\" as const,\n required: false,\n default: DEFAULT_MAX_PAYLOAD_FILES,\n example: 500,\n },\n ],\n },\n userConfigSchema: {\n description:\n \"Per-user settings for the Echo recommendations plugin. These are sent to the plugin as user config on each run.\",\n fields: [\n {\n key: \"recommendationRules\",\n label: \"Recommendation Rules (JSON)\",\n description:\n 'Optional per-user JSON rules the plugin applies (e.g. {\"excludeGenres\":[\"Ecchi\"]}). Echoed back in the recorded payloads so you can confirm it reaches the plugin.',\n type: \"json\" as const,\n required: false,\n },\n ],\n },\n userDescription:\n \"A debug recommendations plugin: it echoes your library seeds back as recommendations and records all protocol traffic to files. No external account needed.\",\n} as const satisfies PluginManifest & {\n capabilities: { userRecommendationProvider: true };\n};\n", "/**\n * Payload Recorder - debug helper for echo plugins.\n *\n * Writes every request and its matching response to paired JSON files under the\n * plugin's host-provided file-storage directory (`InitializeParams.dataDir`), so\n * the host -> plugin protocol traffic can be inspected without trawling the\n * server logs. This is intended for the echo (test/debug) plugins only.\n *\n * Files for one call share a sortable basename and differ only by suffix:\n *\n * {yyyy-MM-dd-HH-mm-ss}-{id}-{method}-request.json\n * {yyyy-MM-dd-HH-mm-ss}-{id}-{method}-response.json\n *\n * Timestamps are UTC + 24-hour + zero-padded, so lexical sort == chronological\n * sort regardless of the server's timezone. `{id}` is a zero-padded monotonic\n * per-process counter that breaks ties within the same second and pairs the two\n * files. Each file is a JSON envelope holding the payload plus a snapshot of the\n * active config (credentials redacted).\n *\n * All filesystem I/O is best-effort: a failure is logged and swallowed so a disk\n * problem never breaks an RPC response.\n *\n * NOTE: this module is intentionally duplicated across the echo plugins to keep\n * the published SDK surface small. Keep the copies in sync.\n */\n\nimport { mkdir, readdir, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Minimal logger contract (compatible with the SDK `Logger`). */\nexport interface RecorderLogger {\n info(message: string): void;\n warn(message: string): void;\n debug(message: string): void;\n}\n\nexport interface PayloadRecorderOptions {\n /** Plugin name, used for the fallback directory and the file envelope. */\n pluginName: string;\n /** Host-provided file-storage directory (`InitializeParams.dataDir`). */\n dataDir?: string;\n /** Whether recording is on (default: true). */\n enabled?: boolean;\n /** Maximum number of files to keep; oldest are pruned (default: 500). */\n maxFiles?: number;\n /** Active config snapshot to embed in each file (should be pre-redacted). */\n configSnapshot: unknown;\n /** Logger for diagnostics. */\n logger: RecorderLogger;\n /** Clock injection for tests (default: `() => new Date()`). */\n now?: () => Date;\n}\n\n/** Keys whose values are dropped from on-disk config snapshots. */\nconst SECRET_KEY_RE = /token|secret|password|api[-_]?key|credential/i;\nconst REDACTED = \"[REDACTED]\";\n\nconst DEFAULT_MAX_FILES = 500;\n\nfunction pad(value: number, width: number): string {\n return String(value).padStart(width, \"0\");\n}\n\n/** Format a date as `yyyy-MM-dd-HH-mm-ss` in UTC. */\nfunction utcStamp(date: Date): string {\n const y = date.getUTCFullYear();\n const mo = pad(date.getUTCMonth() + 1, 2);\n const d = pad(date.getUTCDate(), 2);\n const h = pad(date.getUTCHours(), 2);\n const mi = pad(date.getUTCMinutes(), 2);\n const s = pad(date.getUTCSeconds(), 2);\n return `${y}-${mo}-${d}-${h}-${mi}-${s}`;\n}\n\n/** Replace non-alphanumeric runs with `_` so a method is filename-safe. */\nfunction sanitizeMethod(method: string): string {\n return method.replace(/[^a-z0-9]+/gi, \"_\").replace(/^_+|_+$/g, \"\");\n}\n\n/**\n * Recursively copy a config object, replacing values under secret-like keys\n * with `[REDACTED]`. Arrays and primitives are returned as-is.\n */\nfunction redactObject(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(redactObject);\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[key] = SECRET_KEY_RE.test(key) ? REDACTED : redactObject(val);\n }\n return out;\n }\n return value;\n}\n\n/**\n * Build a redacted config snapshot from initialize params. Credentials are\n * never included (they are passed separately, not via config); secret-like\n * config keys are additionally redacted as a defensive measure.\n */\nexport function redactConfig(input: {\n adminConfig?: Record<string, unknown>;\n userConfig?: Record<string, unknown>;\n}): { adminConfig: unknown; userConfig: unknown } {\n return {\n adminConfig: redactObject(input.adminConfig ?? {}),\n userConfig: redactObject(input.userConfig ?? {}),\n };\n}\n\ninterface Envelope {\n timestamp: string;\n plugin: string;\n method: string;\n direction: \"request\" | \"response\";\n id: number;\n config: unknown;\n payload: unknown;\n}\n\nexport class PayloadRecorder {\n private readonly pluginName: string;\n private readonly enabled: boolean;\n private readonly maxFiles: number;\n private readonly configSnapshot: unknown;\n private readonly logger: RecorderLogger;\n private readonly now: () => Date;\n private readonly dir: string;\n private readonly usingFallback: boolean;\n private seq = 0;\n private ready: Promise<boolean> | null = null;\n\n constructor(opts: PayloadRecorderOptions) {\n this.pluginName = opts.pluginName;\n this.enabled = opts.enabled ?? true;\n this.maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;\n this.configSnapshot = opts.configSnapshot;\n this.logger = opts.logger;\n this.now = opts.now ?? (() => new Date());\n\n if (opts.dataDir) {\n this.dir = join(opts.dataDir, \"payloads\");\n this.usingFallback = false;\n } else {\n this.dir = join(tmpdir(), `codex-${opts.pluginName}`, \"payloads\");\n this.usingFallback = true;\n }\n }\n\n /** Absolute directory payloads are written to (for logging/tests). */\n get directory(): string {\n return this.dir;\n }\n\n /** Lazily create the payloads directory once; returns false if unusable. */\n private ensureDir(): Promise<boolean> {\n if (!this.ready) {\n this.ready = mkdir(this.dir, { recursive: true })\n .then(() => {\n if (this.usingFallback) {\n this.logger.warn(`No dataDir provided; recording payloads under temp dir ${this.dir}`);\n } else {\n this.logger.debug(`Recording payloads to ${this.dir}`);\n }\n return true;\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to create payload dir ${this.dir}: ${msg}`);\n return false;\n });\n }\n return this.ready;\n }\n\n /**\n * Record a request and its response under one shared, sortable basename.\n * Best-effort: never throws.\n */\n async record(method: string, request: unknown, response: unknown): Promise<void> {\n if (!this.enabled) return;\n if (!(await this.ensureDir())) return;\n\n const id = ++this.seq;\n const date = this.now();\n const base = `${utcStamp(date)}-${pad(id, 4)}-${sanitizeMethod(method)}`;\n const iso = date.toISOString();\n\n await this.writeFile(`${base}-request.json`, {\n timestamp: iso,\n plugin: this.pluginName,\n method,\n direction: \"request\",\n id,\n config: this.configSnapshot,\n payload: request,\n });\n await this.writeFile(`${base}-response.json`, {\n timestamp: iso,\n plugin: this.pluginName,\n method,\n direction: \"response\",\n id,\n config: this.configSnapshot,\n payload: response,\n });\n\n await this.prune();\n }\n\n private async writeFile(name: string, envelope: Envelope): Promise<void> {\n try {\n await writeFile(join(this.dir, name), JSON.stringify(envelope, null, 2), \"utf8\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to write payload file ${name}: ${msg}`);\n }\n }\n\n /** Keep at most `maxFiles` files, deleting the oldest (lexical == chrono). */\n private async prune(): Promise<void> {\n if (this.maxFiles <= 0) return;\n try {\n const files = (await readdir(this.dir)).filter((f) => f.endsWith(\".json\")).sort();\n const excess = files.length - this.maxFiles;\n if (excess <= 0) return;\n for (const file of files.slice(0, excess)) {\n await rm(join(this.dir, file), { force: true });\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to prune payload dir ${this.dir}: ${msg}`);\n }\n }\n}\n", "/**\n * Echo Recommendations Plugin for Codex\n *\n * A minimal test/debug recommendations plugin. It does not talk to any external\n * service: it echoes the user's library seeds back as recommendations (and, when\n * the library is empty, returns a few generic ones). Every recommendation has\n * all fields populated so the host's ingest path is exercised end to end.\n *\n * Its main purpose is debugging the host -> plugin recommendations protocol: it\n * records every request and its response to JSON files under the plugin's data\n * directory (see `recorder.ts`).\n */\n\nimport {\n createLogger,\n createRecommendationPlugin,\n type InitializeParams,\n type ProfileUpdateRequest,\n type ProfileUpdateResponse,\n type Recommendation,\n type RecommendationClearResponse,\n type RecommendationDismissRequest,\n type RecommendationDismissResponse,\n type RecommendationProvider,\n type RecommendationRequest,\n type RecommendationResponse,\n} from \"@ashdev/codex-plugin-sdk\";\nimport { DEFAULT_FALLBACK_COUNT, DEFAULT_MAX_PAYLOAD_FILES, manifest } from \"./manifest.js\";\nimport { PayloadRecorder, redactConfig } from \"./recorder.js\";\n\nconst logger = createLogger({ name: \"recommendations-echo\", level: \"debug\" });\n\n// Payload recorder (set during initialization)\nlet recorder: PayloadRecorder | null = null;\n\n/** Record a request/response pair (best-effort) and return the response. */\nasync function rec<T>(method: string, params: unknown, response: T): Promise<T> {\n await recorder?.record(method, params, response);\n return response;\n}\n\n/** Set the payload recorder (exported for testing) */\nexport function setRecorder(r: PayloadRecorder | null): void {\n recorder = r;\n}\n\n/**\n * Build a deterministic, fully-populated recommendation seeded by the given\n * title. Every optional field is set so the host's ingest path is exercised.\n */\nfunction makeRec(basedOnTitle: string, i: number): Recommendation {\n const externalId = `echo-rec-${i + 1}`;\n return {\n externalId,\n externalUrl: `https://echo.example.com/rec/${externalId}`,\n title: `Echo Recommendation ${i + 1}`,\n coverUrl: \"https://picsum.photos/300/450\",\n summary: `Echo recommendation based on \"${basedOnTitle}\".`,\n genres: [\"Action\", \"Echo\"],\n tags: [\n { name: \"echo\", rank: 90, category: \"Theme\" },\n { name: \"test\", rank: 50, category: \"Demographic\" },\n ],\n score: Math.max(0.1, 1 - i * 0.1),\n reason: `Recommended because you read \"${basedOnTitle}\"`,\n basedOn: [basedOnTitle],\n inLibrary: false,\n status: \"ongoing\",\n format: \"MANGA\",\n countryOfOrigin: \"JP\",\n startYear: 2020 + (i % 5),\n totalVolumeCount: 12,\n totalChapterCount: 100,\n rating: 80 + (i % 20),\n popularity: 1000 + i,\n };\n}\n\n/** Exported for testing */\nexport const provider: RecommendationProvider = {\n async get(params: RecommendationRequest): Promise<RecommendationResponse> {\n const { library, limit, excludeIds = [] } = params;\n const exclude = new Set(excludeIds);\n\n logger.info(\n `Recommendations requested: ${library.length} seeds, limit=${limit ?? \"none\"}, exclude=${excludeIds.length}`,\n );\n\n // Echo each library seed back as a recommendation; if the library is empty,\n // return a few generic ones so the response is never empty.\n const seeds =\n library.length > 0\n ? library.map((e) => e.title)\n : Array.from({ length: DEFAULT_FALLBACK_COUNT }, (_, i) => `Echo Seed ${i + 1}`);\n\n const recommendations = seeds\n .map((title, i) => makeRec(title, i))\n .filter((r) => !exclude.has(r.externalId))\n .slice(0, limit ?? seeds.length);\n\n return rec<RecommendationResponse>(\"recommendations/get\", params, {\n recommendations,\n generatedAt: new Date().toISOString(),\n cached: false,\n });\n },\n\n async updateProfile(params: ProfileUpdateRequest): Promise<ProfileUpdateResponse> {\n logger.info(`Profile update: ${params.entries.length} entries`);\n return rec<ProfileUpdateResponse>(\"recommendations/updateProfile\", params, {\n updated: true,\n entriesProcessed: params.entries.length,\n });\n },\n\n async dismiss(params: RecommendationDismissRequest): Promise<RecommendationDismissResponse> {\n logger.info(`Dismiss ${params.externalId} (reason: ${params.reason ?? \"none\"})`);\n return rec<RecommendationDismissResponse>(\"recommendations/dismiss\", params, {\n dismissed: true,\n });\n },\n\n async clear(): Promise<RecommendationClearResponse> {\n logger.info(\"Clear recommendations\");\n return rec<RecommendationClearResponse>(\"recommendations/clear\", null, { cleared: true });\n },\n};\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\ncreateRecommendationPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n onInitialize(params: InitializeParams) {\n // Set up payload recording (on by default for this debug plugin)\n const recordPayloads = params.adminConfig?.recordPayloads !== false;\n const maxPayloadFiles =\n typeof params.adminConfig?.maxPayloadFiles === \"number\"\n ? params.adminConfig.maxPayloadFiles\n : DEFAULT_MAX_PAYLOAD_FILES;\n recorder = new PayloadRecorder({\n pluginName: manifest.name,\n dataDir: params.dataDir,\n enabled: recordPayloads,\n maxFiles: maxPayloadFiles,\n configSnapshot: redactConfig({\n adminConfig: params.adminConfig,\n userConfig: params.userConfig,\n }),\n logger,\n });\n\n logger.info(`Echo recommendations plugin initialized (recordPayloads: ${recordPayloads})`);\n },\n});\n\nlogger.info(\"Echo recommendations plugin started\");\n"],
5
- "mappings": "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",
3
+ "sources": ["../node_modules/@ashdev/codex-plugin-sdk/src/types/rpc.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/errors.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/request-context.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/host-rpc.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/logger.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/server.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/storage.ts", "../package.json", "../src/manifest.ts", "../src/recorder.ts", "../src/index.ts"],
4
+ "sourcesContent": [null, null, null, null, null, null, null, "{\n \"name\": \"@ashdev/codex-plugin-recommendations-echo\",\n \"version\": \"1.36.0\",\n \"description\": \"Echo recommendations plugin for testing and debugging the Codex plugin recommendations protocol\",\n \"main\": \"dist/index.js\",\n \"bin\": \"dist/index.js\",\n \"type\": \"module\",\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/AshDevFr/codex.git\",\n \"directory\": \"plugins/recommendations-echo\"\n },\n \"scripts\": {\n \"build\": \"esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'\",\n \"dev\": \"npm run build -- --watch\",\n \"clean\": \"rm -rf dist\",\n \"start\": \"node dist/index.js\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --passWithNoTests\",\n \"test:watch\": \"vitest\",\n \"prepublishOnly\": \"npm run lint && npm run build\"\n },\n \"keywords\": [\n \"codex\",\n \"plugin\",\n \"recommendations\",\n \"echo\",\n \"testing\"\n ],\n \"author\": \"Codex\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"dependencies\": {\n \"@ashdev/codex-plugin-sdk\": \"^1.36.0\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.4.4\",\n \"@types/node\": \"^22.0.0\",\n \"esbuild\": \"^0.27.3\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n }\n}\n", "import type { PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\n// Default config values\nexport const DEFAULT_MAX_PAYLOAD_FILES = 500;\n// Number of generic recommendations returned when the library is empty.\nexport const DEFAULT_FALLBACK_COUNT = 3;\n\nexport const manifest = {\n name: \"recommendations-echo\",\n displayName: \"Echo Recommendations Plugin\",\n version: packageJson.version,\n description:\n \"Test recommendations plugin that echoes library seeds back as recommendations. Records every request/response to files for debugging.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n userRecommendationProvider: true,\n // Opt in to enriched series data (metadata/custom metadata) so the\n // enrichment toggles appear and the payloads can be inspected.\n wantsFullMetadata: true,\n },\n configSchema: {\n description: \"Configuration options for the Echo recommendations test plugin\",\n fields: [\n {\n key: \"recordPayloads\",\n label: \"Record Payloads\",\n description:\n \"Write each request and its response to JSON files under the plugin's data directory for debugging.\",\n type: \"boolean\" as const,\n required: false,\n default: true,\n },\n {\n key: \"maxPayloadFiles\",\n label: \"Max Payload Files\",\n description:\n \"Maximum number of recorded payload files to keep; oldest are pruned. Only used when payload recording is enabled.\",\n type: \"number\" as const,\n required: false,\n default: DEFAULT_MAX_PAYLOAD_FILES,\n example: 500,\n },\n ],\n },\n userConfigSchema: {\n description:\n \"Per-user settings for the Echo recommendations plugin. These are sent to the plugin as user config on each run.\",\n fields: [\n {\n key: \"recommendationRules\",\n label: \"Recommendation Rules (JSON)\",\n description:\n 'Optional per-user JSON rules the plugin applies (e.g. {\"excludeGenres\":[\"Ecchi\"]}). Echoed back in the recorded payloads so you can confirm it reaches the plugin.',\n type: \"json\" as const,\n required: false,\n },\n ],\n },\n userDescription:\n \"A debug recommendations plugin: it echoes your library seeds back as recommendations and records all protocol traffic to files. No external account needed.\",\n} as const satisfies PluginManifest & {\n capabilities: { userRecommendationProvider: true };\n};\n", "/**\n * Payload Recorder - debug helper for echo plugins.\n *\n * Writes every request and its matching response to paired JSON files under the\n * plugin's host-provided file-storage directory (`InitializeParams.dataDir`), so\n * the host -> plugin protocol traffic can be inspected without trawling the\n * server logs. This is intended for the echo (test/debug) plugins only.\n *\n * Files for one call share a sortable basename and differ only by suffix:\n *\n * {yyyy-MM-dd-HH-mm-ss}-{id}-{method}-request.json\n * {yyyy-MM-dd-HH-mm-ss}-{id}-{method}-response.json\n *\n * Timestamps are UTC + 24-hour + zero-padded, so lexical sort == chronological\n * sort regardless of the server's timezone. `{id}` is a zero-padded monotonic\n * per-process counter that breaks ties within the same second and pairs the two\n * files. Each file is a JSON envelope holding the payload plus a snapshot of the\n * active config (credentials redacted).\n *\n * All filesystem I/O is best-effort: a failure is logged and swallowed so a disk\n * problem never breaks an RPC response.\n *\n * NOTE: this module is intentionally duplicated across the echo plugins to keep\n * the published SDK surface small. Keep the copies in sync.\n */\n\nimport { mkdir, readdir, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Minimal logger contract (compatible with the SDK `Logger`). */\nexport interface RecorderLogger {\n info(message: string): void;\n warn(message: string): void;\n debug(message: string): void;\n}\n\nexport interface PayloadRecorderOptions {\n /** Plugin name, used for the fallback directory and the file envelope. */\n pluginName: string;\n /** Host-provided file-storage directory (`InitializeParams.dataDir`). */\n dataDir?: string;\n /** Whether recording is on (default: true). */\n enabled?: boolean;\n /** Maximum number of files to keep; oldest are pruned (default: 500). */\n maxFiles?: number;\n /** Active config snapshot to embed in each file (should be pre-redacted). */\n configSnapshot: unknown;\n /** Logger for diagnostics. */\n logger: RecorderLogger;\n /** Clock injection for tests (default: `() => new Date()`). */\n now?: () => Date;\n}\n\n/** Keys whose values are dropped from on-disk config snapshots. */\nconst SECRET_KEY_RE = /token|secret|password|api[-_]?key|credential/i;\nconst REDACTED = \"[REDACTED]\";\n\nconst DEFAULT_MAX_FILES = 500;\n\nfunction pad(value: number, width: number): string {\n return String(value).padStart(width, \"0\");\n}\n\n/** Format a date as `yyyy-MM-dd-HH-mm-ss` in UTC. */\nfunction utcStamp(date: Date): string {\n const y = date.getUTCFullYear();\n const mo = pad(date.getUTCMonth() + 1, 2);\n const d = pad(date.getUTCDate(), 2);\n const h = pad(date.getUTCHours(), 2);\n const mi = pad(date.getUTCMinutes(), 2);\n const s = pad(date.getUTCSeconds(), 2);\n return `${y}-${mo}-${d}-${h}-${mi}-${s}`;\n}\n\n/** Replace non-alphanumeric runs with `_` so a method is filename-safe. */\nfunction sanitizeMethod(method: string): string {\n return method.replace(/[^a-z0-9]+/gi, \"_\").replace(/^_+|_+$/g, \"\");\n}\n\n/**\n * Recursively copy a config object, replacing values under secret-like keys\n * with `[REDACTED]`. Arrays and primitives are returned as-is.\n */\nfunction redactObject(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(redactObject);\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[key] = SECRET_KEY_RE.test(key) ? REDACTED : redactObject(val);\n }\n return out;\n }\n return value;\n}\n\n/**\n * Build a redacted config snapshot from initialize params. Credentials are\n * never included (they are passed separately, not via config); secret-like\n * config keys are additionally redacted as a defensive measure.\n */\nexport function redactConfig(input: {\n adminConfig?: Record<string, unknown>;\n userConfig?: Record<string, unknown>;\n}): { adminConfig: unknown; userConfig: unknown } {\n return {\n adminConfig: redactObject(input.adminConfig ?? {}),\n userConfig: redactObject(input.userConfig ?? {}),\n };\n}\n\ninterface Envelope {\n timestamp: string;\n plugin: string;\n method: string;\n direction: \"request\" | \"response\";\n id: number;\n config: unknown;\n payload: unknown;\n}\n\nexport class PayloadRecorder {\n private readonly pluginName: string;\n private readonly enabled: boolean;\n private readonly maxFiles: number;\n private readonly configSnapshot: unknown;\n private readonly logger: RecorderLogger;\n private readonly now: () => Date;\n private readonly dir: string;\n private readonly usingFallback: boolean;\n private seq = 0;\n private ready: Promise<boolean> | null = null;\n\n constructor(opts: PayloadRecorderOptions) {\n this.pluginName = opts.pluginName;\n this.enabled = opts.enabled ?? true;\n this.maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;\n this.configSnapshot = opts.configSnapshot;\n this.logger = opts.logger;\n this.now = opts.now ?? (() => new Date());\n\n if (opts.dataDir) {\n this.dir = join(opts.dataDir, \"payloads\");\n this.usingFallback = false;\n } else {\n this.dir = join(tmpdir(), `codex-${opts.pluginName}`, \"payloads\");\n this.usingFallback = true;\n }\n }\n\n /** Absolute directory payloads are written to (for logging/tests). */\n get directory(): string {\n return this.dir;\n }\n\n /** Lazily create the payloads directory once; returns false if unusable. */\n private ensureDir(): Promise<boolean> {\n if (!this.ready) {\n this.ready = mkdir(this.dir, { recursive: true })\n .then(() => {\n if (this.usingFallback) {\n this.logger.warn(`No dataDir provided; recording payloads under temp dir ${this.dir}`);\n } else {\n this.logger.debug(`Recording payloads to ${this.dir}`);\n }\n return true;\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to create payload dir ${this.dir}: ${msg}`);\n return false;\n });\n }\n return this.ready;\n }\n\n /**\n * Record a request and its response under one shared, sortable basename.\n * Best-effort: never throws.\n */\n async record(method: string, request: unknown, response: unknown): Promise<void> {\n if (!this.enabled) return;\n if (!(await this.ensureDir())) return;\n\n const id = ++this.seq;\n const date = this.now();\n const base = `${utcStamp(date)}-${pad(id, 4)}-${sanitizeMethod(method)}`;\n const iso = date.toISOString();\n\n await this.writeFile(`${base}-request.json`, {\n timestamp: iso,\n plugin: this.pluginName,\n method,\n direction: \"request\",\n id,\n config: this.configSnapshot,\n payload: request,\n });\n await this.writeFile(`${base}-response.json`, {\n timestamp: iso,\n plugin: this.pluginName,\n method,\n direction: \"response\",\n id,\n config: this.configSnapshot,\n payload: response,\n });\n\n await this.prune();\n }\n\n private async writeFile(name: string, envelope: Envelope): Promise<void> {\n try {\n await writeFile(join(this.dir, name), JSON.stringify(envelope, null, 2), \"utf8\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to write payload file ${name}: ${msg}`);\n }\n }\n\n /** Keep at most `maxFiles` files, deleting the oldest (lexical == chrono). */\n private async prune(): Promise<void> {\n if (this.maxFiles <= 0) return;\n try {\n const files = (await readdir(this.dir)).filter((f) => f.endsWith(\".json\")).sort();\n const excess = files.length - this.maxFiles;\n if (excess <= 0) return;\n for (const file of files.slice(0, excess)) {\n await rm(join(this.dir, file), { force: true });\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n this.logger.warn(`Failed to prune payload dir ${this.dir}: ${msg}`);\n }\n }\n}\n", "/**\n * Echo Recommendations Plugin for Codex\n *\n * A minimal test/debug recommendations plugin. It does not talk to any external\n * service: it echoes the user's library seeds back as recommendations (and, when\n * the library is empty, returns a few generic ones). Every recommendation has\n * all fields populated so the host's ingest path is exercised end to end.\n *\n * Its main purpose is debugging the host -> plugin recommendations protocol: it\n * records every request and its response to JSON files under the plugin's data\n * directory (see `recorder.ts`).\n */\n\nimport {\n createLogger,\n createRecommendationPlugin,\n type InitializeParams,\n type ProfileUpdateRequest,\n type ProfileUpdateResponse,\n type Recommendation,\n type RecommendationClearResponse,\n type RecommendationDismissRequest,\n type RecommendationDismissResponse,\n type RecommendationProvider,\n type RecommendationRequest,\n type RecommendationResponse,\n} from \"@ashdev/codex-plugin-sdk\";\nimport { DEFAULT_FALLBACK_COUNT, DEFAULT_MAX_PAYLOAD_FILES, manifest } from \"./manifest.js\";\nimport { PayloadRecorder, redactConfig } from \"./recorder.js\";\n\nconst logger = createLogger({ name: \"recommendations-echo\", level: \"debug\" });\n\n// Payload recorder (set during initialization)\nlet recorder: PayloadRecorder | null = null;\n\n/** Record a request/response pair (best-effort) and return the response. */\nasync function rec<T>(method: string, params: unknown, response: T): Promise<T> {\n await recorder?.record(method, params, response);\n return response;\n}\n\n/** Set the payload recorder (exported for testing) */\nexport function setRecorder(r: PayloadRecorder | null): void {\n recorder = r;\n}\n\n/**\n * Build a deterministic, fully-populated recommendation seeded by the given\n * title. Every optional field is set so the host's ingest path is exercised.\n */\nfunction makeRec(basedOnTitle: string, i: number): Recommendation {\n const externalId = `echo-rec-${i + 1}`;\n return {\n externalId,\n externalUrl: `https://echo.example.com/rec/${externalId}`,\n title: `Echo Recommendation ${i + 1}`,\n coverUrl: \"https://picsum.photos/300/450\",\n summary: `Echo recommendation based on \"${basedOnTitle}\".`,\n genres: [\"Action\", \"Echo\"],\n tags: [\n { name: \"echo\", rank: 90, category: \"Theme\" },\n { name: \"test\", rank: 50, category: \"Demographic\" },\n ],\n score: Math.max(0.1, 1 - i * 0.1),\n reason: `Recommended because you read \"${basedOnTitle}\"`,\n basedOn: [basedOnTitle],\n inLibrary: false,\n status: \"ongoing\",\n format: \"MANGA\",\n countryOfOrigin: \"JP\",\n startYear: 2020 + (i % 5),\n totalVolumeCount: 12,\n totalChapterCount: 100,\n rating: 80 + (i % 20),\n popularity: 1000 + i,\n };\n}\n\n/** Exported for testing */\nexport const provider: RecommendationProvider = {\n async get(params: RecommendationRequest): Promise<RecommendationResponse> {\n const { library, limit, excludeIds = [] } = params;\n const exclude = new Set(excludeIds);\n\n logger.info(\n `Recommendations requested: ${library.length} seeds, limit=${limit ?? \"none\"}, exclude=${excludeIds.length}`,\n );\n\n // Echo each library seed back as a recommendation; if the library is empty,\n // return a few generic ones so the response is never empty.\n const seeds =\n library.length > 0\n ? library.map((e) => e.title)\n : Array.from({ length: DEFAULT_FALLBACK_COUNT }, (_, i) => `Echo Seed ${i + 1}`);\n\n const recommendations = seeds\n .map((title, i) => makeRec(title, i))\n .filter((r) => !exclude.has(r.externalId))\n .slice(0, limit ?? seeds.length);\n\n return rec<RecommendationResponse>(\"recommendations/get\", params, {\n recommendations,\n generatedAt: new Date().toISOString(),\n cached: false,\n });\n },\n\n async updateProfile(params: ProfileUpdateRequest): Promise<ProfileUpdateResponse> {\n logger.info(`Profile update: ${params.entries.length} entries`);\n return rec<ProfileUpdateResponse>(\"recommendations/updateProfile\", params, {\n updated: true,\n entriesProcessed: params.entries.length,\n });\n },\n\n async dismiss(params: RecommendationDismissRequest): Promise<RecommendationDismissResponse> {\n logger.info(`Dismiss ${params.externalId} (reason: ${params.reason ?? \"none\"})`);\n return rec<RecommendationDismissResponse>(\"recommendations/dismiss\", params, {\n dismissed: true,\n });\n },\n\n async clear(): Promise<RecommendationClearResponse> {\n logger.info(\"Clear recommendations\");\n return rec<RecommendationClearResponse>(\"recommendations/clear\", null, { cleared: true });\n },\n};\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\ncreateRecommendationPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n onInitialize(params: InitializeParams) {\n // Set up payload recording (on by default for this debug plugin)\n const recordPayloads = params.adminConfig?.recordPayloads !== false;\n const maxPayloadFiles =\n typeof params.adminConfig?.maxPayloadFiles === \"number\"\n ? params.adminConfig.maxPayloadFiles\n : DEFAULT_MAX_PAYLOAD_FILES;\n recorder = new PayloadRecorder({\n pluginName: manifest.name,\n dataDir: params.dataDir,\n enabled: recordPayloads,\n maxFiles: maxPayloadFiles,\n configSnapshot: redactConfig({\n adminConfig: params.adminConfig,\n userConfig: params.userConfig,\n }),\n logger,\n });\n\n logger.info(`Echo recommendations plugin initialized (recordPayloads: ${recordPayloads})`);\n },\n});\n\nlogger.info(\"Echo recommendations plugin started\");\n"],
5
+ "mappings": "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",
6
6
  "names": ["manifest", "logger", "response", "manifest", "provider"]
7
7
  }
package/package.json CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "name": "@ashdev/codex-plugin-recommendations-echo",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Echo recommendations plugin for testing and debugging the Codex plugin recommendations protocol",
5
5
  "main": "dist/index.js",
6
- "bin": {
7
- "codex-plugin-recommendations-echo": "dist/index.js"
8
- },
6
+ "bin": "dist/index.js",
9
7
  "type": "module",
10
8
  "files": [
11
9
  "dist",
@@ -13,7 +11,7 @@
13
11
  ],
14
12
  "repository": {
15
13
  "type": "git",
16
- "url": "git+https://github.com/AshDevFr/codex.git",
14
+ "url": "https://github.com/AshDevFr/codex.git",
17
15
  "directory": "plugins/recommendations-echo"
18
16
  },
19
17
  "scripts": {
@@ -41,7 +39,7 @@
41
39
  "node": ">=22.0.0"
42
40
  },
43
41
  "dependencies": {
44
- "@ashdev/codex-plugin-sdk": "file:../sdk-typescript"
42
+ "@ashdev/codex-plugin-sdk": "^1.36.0"
45
43
  },
46
44
  "devDependencies": {
47
45
  "@biomejs/biome": "^2.4.4",