@ashdev/codex-plugin-sdk 1.18.1 → 1.19.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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Generic host reverse-RPC client.
3
+ *
4
+ * Plugins use this to call host methods outside the storage namespace —
5
+ * notably `releases/list_tracked`, `releases/record`,
6
+ * `releases/source_state/get`, and `releases/source_state/set`. The class is
7
+ * intentionally generic so future reverse-RPC namespaces can reuse it
8
+ * without a per-namespace client.
9
+ *
10
+ * Wire-format and lifecycle mirror `PluginStorage`: send a JSON-RPC request
11
+ * over stdout with a unique id, and resolve when the host's response with
12
+ * the matching id arrives on stdin. The plugin server's main loop calls
13
+ * `handleResponse(line)` on every incoming response; whichever client owns
14
+ * the id resolves it (others no-op silently).
15
+ *
16
+ * The id counter starts at a high value (`1_000_000_000`) so it can never
17
+ * collide with `PluginStorage`'s sequence (`1, 2, 3, ...`). This means the
18
+ * dispatch in the server doesn't need to know which client a response
19
+ * belongs to — it can fan out to both, and at most one will match.
20
+ */
21
+ /** Write function signature for sending JSON-RPC requests. */
22
+ type WriteFn = (line: string) => void;
23
+ /**
24
+ * Error thrown when a reverse-RPC call fails (host returned a JSON-RPC error,
25
+ * or the client was canceled).
26
+ */
27
+ export declare class HostRpcError extends Error {
28
+ readonly code: number;
29
+ readonly data?: unknown | undefined;
30
+ constructor(message: string, code: number, data?: unknown | undefined);
31
+ }
32
+ /**
33
+ * Generic reverse-RPC client. Construct one per plugin instance and pass it
34
+ * around via `InitializeParams`.
35
+ */
36
+ export declare class HostRpcClient {
37
+ private nextId;
38
+ private pendingRequests;
39
+ private writeFn;
40
+ /**
41
+ * @param writeFn - Optional custom write function (defaults to
42
+ * `process.stdout.write`). Useful for testing.
43
+ */
44
+ constructor(writeFn?: WriteFn);
45
+ /**
46
+ * Send a JSON-RPC request to the host and resolve with the result.
47
+ *
48
+ * @param method - JSON-RPC method name (e.g. `"releases/list_tracked"`).
49
+ * @param params - Method-specific params. Pass `undefined` when the method
50
+ * takes no params.
51
+ */
52
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
53
+ /**
54
+ * Process an incoming JSON-RPC response line. Returns `true` if this
55
+ * client owned the response id and resolved it, `false` otherwise (so
56
+ * other clients can try).
57
+ *
58
+ * Called by the plugin server's main loop on every response.
59
+ */
60
+ handleResponse(line: string): boolean;
61
+ /** Reject all pending requests (e.g. on shutdown). */
62
+ cancelAll(): void;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=host-rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-rpc.d.ts","sourceRoot":"","sources":["../src/host-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,8DAA8D;AAC9D,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtC;;;GAGG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,IAAI,CAAC,EAAE,OAAO;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,YAAA;CAKjC;AAED;;;GAGG;AACH,qBAAa,aAAa;IAKxB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,eAAe,CAMnB;IACJ,OAAO,CAAC,OAAO,CAAU;IAEzB;;;OAGG;gBACS,OAAO,CAAC,EAAE,OAAO;IAQ7B;;;;;;OAMG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IA8BrE;;;;;;OAMG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IA8BrC,sDAAsD;IACtD,SAAS,IAAI,IAAI;CAMlB"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Generic host reverse-RPC client.
3
+ *
4
+ * Plugins use this to call host methods outside the storage namespace —
5
+ * notably `releases/list_tracked`, `releases/record`,
6
+ * `releases/source_state/get`, and `releases/source_state/set`. The class is
7
+ * intentionally generic so future reverse-RPC namespaces can reuse it
8
+ * without a per-namespace client.
9
+ *
10
+ * Wire-format and lifecycle mirror `PluginStorage`: send a JSON-RPC request
11
+ * over stdout with a unique id, and resolve when the host's response with
12
+ * the matching id arrives on stdin. The plugin server's main loop calls
13
+ * `handleResponse(line)` on every incoming response; whichever client owns
14
+ * the id resolves it (others no-op silently).
15
+ *
16
+ * The id counter starts at a high value (`1_000_000_000`) so it can never
17
+ * collide with `PluginStorage`'s sequence (`1, 2, 3, ...`). This means the
18
+ * dispatch in the server doesn't need to know which client a response
19
+ * belongs to — it can fan out to both, and at most one will match.
20
+ */
21
+ import { currentParentRequestId } from "./request-context.js";
22
+ /**
23
+ * Error thrown when a reverse-RPC call fails (host returned a JSON-RPC error,
24
+ * or the client was canceled).
25
+ */
26
+ export class HostRpcError extends Error {
27
+ code;
28
+ data;
29
+ constructor(message, code, data) {
30
+ super(message);
31
+ this.code = code;
32
+ this.data = data;
33
+ this.name = "HostRpcError";
34
+ }
35
+ }
36
+ /**
37
+ * Generic reverse-RPC client. Construct one per plugin instance and pass it
38
+ * around via `InitializeParams`.
39
+ */
40
+ export class HostRpcClient {
41
+ // Start the counter high so it can't collide with PluginStorage's id space.
42
+ // `Number.MAX_SAFE_INTEGER` is far above this, so we have plenty of room
43
+ // before wrapping (and we never expect a single plugin lifetime to issue
44
+ // more than ~9 quintillion calls).
45
+ nextId = 1_000_000_000;
46
+ pendingRequests = new Map();
47
+ writeFn;
48
+ /**
49
+ * @param writeFn - Optional custom write function (defaults to
50
+ * `process.stdout.write`). Useful for testing.
51
+ */
52
+ constructor(writeFn) {
53
+ this.writeFn =
54
+ writeFn ??
55
+ ((line) => {
56
+ process.stdout.write(line);
57
+ });
58
+ }
59
+ /**
60
+ * Send a JSON-RPC request to the host and resolve with the result.
61
+ *
62
+ * @param method - JSON-RPC method name (e.g. `"releases/list_tracked"`).
63
+ * @param params - Method-specific params. Pass `undefined` when the method
64
+ * takes no params.
65
+ */
66
+ async call(method, params) {
67
+ const id = this.nextId++;
68
+ // Stamp the forward call we're inside so the host can route this
69
+ // reverse-RPC back to the originating caller's task. Lifted from the
70
+ // `request-context` async-local storage that `server.ts` sets around
71
+ // every forward-request handler.
72
+ const parent = currentParentRequestId();
73
+ const request = {
74
+ jsonrpc: "2.0",
75
+ id,
76
+ method,
77
+ params,
78
+ ...(parent !== undefined ? { parentRequestId: parent } : {}),
79
+ };
80
+ return new Promise((resolve, reject) => {
81
+ this.pendingRequests.set(id, {
82
+ resolve: (v) => resolve(v),
83
+ reject,
84
+ });
85
+ try {
86
+ this.writeFn(`${JSON.stringify(request)}\n`);
87
+ }
88
+ catch (err) {
89
+ this.pendingRequests.delete(id);
90
+ const message = err instanceof Error ? err.message : "Unknown write error";
91
+ reject(new HostRpcError(`Failed to send request: ${message}`, -1));
92
+ }
93
+ });
94
+ }
95
+ /**
96
+ * Process an incoming JSON-RPC response line. Returns `true` if this
97
+ * client owned the response id and resolved it, `false` otherwise (so
98
+ * other clients can try).
99
+ *
100
+ * Called by the plugin server's main loop on every response.
101
+ */
102
+ handleResponse(line) {
103
+ const trimmed = line.trim();
104
+ if (!trimmed)
105
+ return false;
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(trimmed);
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ const obj = parsed;
114
+ if (obj.method !== undefined)
115
+ return false; // not a response
116
+ const rawId = obj.id;
117
+ if (typeof rawId !== "number")
118
+ return false;
119
+ if (!this.pendingRequests.has(rawId))
120
+ return false;
121
+ const pending = this.pendingRequests.get(rawId);
122
+ if (!pending)
123
+ return false;
124
+ this.pendingRequests.delete(rawId);
125
+ if ("error" in obj && obj.error) {
126
+ const err = obj.error;
127
+ pending.reject(new HostRpcError(err.message, err.code, err.data));
128
+ }
129
+ else {
130
+ pending.resolve(obj.result);
131
+ }
132
+ return true;
133
+ }
134
+ /** Reject all pending requests (e.g. on shutdown). */
135
+ cancelAll() {
136
+ for (const [, pending] of this.pendingRequests) {
137
+ pending.reject(new HostRpcError("Host RPC client stopped", -1));
138
+ }
139
+ this.pendingRequests.clear();
140
+ }
141
+ }
142
+ //# sourceMappingURL=host-rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-rpc.js","sourceRoot":"","sources":["../src/host-rpc.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -53,8 +53,9 @@
53
53
  * @packageDocumentation
54
54
  */
55
55
  export { ApiError, AuthError, ConfigError, NotFoundError, PluginError, RateLimitError, } from "./errors.js";
56
+ export { HostRpcClient, HostRpcError } from "./host-rpc.js";
56
57
  export { createLogger, Logger, type LoggerOptions, type LogLevel } from "./logger.js";
57
- export { createMetadataPlugin, createRecommendationPlugin, createSyncPlugin, type InitializeParams, type MetadataPluginOptions, type RecommendationPluginOptions, type SyncPluginOptions, } from "./server.js";
58
+ export { createMetadataPlugin, createRecommendationPlugin, createReleaseSourcePlugin, createSyncPlugin, type InitializeParams, type MetadataPluginOptions, type RecommendationPluginOptions, type ReleaseSourcePluginOptions, type SyncPluginOptions, } from "./server.js";
58
59
  export { PluginStorage, type StorageClearResponse, type StorageDeleteResponse, StorageError, type StorageGetResponse, type StorageKeyEntry, type StorageListResponse, type StorageSetResponse, } from "./storage.js";
59
60
  export * from "./types/index.js";
60
61
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGtF,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AAGtB,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGtF,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AAGtB,cAAc,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -54,10 +54,12 @@
54
54
  */
55
55
  // Errors
56
56
  export { ApiError, AuthError, ConfigError, NotFoundError, PluginError, RateLimitError, } from "./errors.js";
57
+ // Host RPC (generic reverse-RPC client for non-storage host methods)
58
+ export { HostRpcClient, HostRpcError } from "./host-rpc.js";
57
59
  // Logger
58
60
  export { createLogger, Logger } from "./logger.js";
59
61
  // Server
60
- export { createMetadataPlugin, createRecommendationPlugin, createSyncPlugin, } from "./server.js";
62
+ export { createMetadataPlugin, createRecommendationPlugin, createReleaseSourcePlugin, createSyncPlugin, } from "./server.js";
61
63
  // Storage
62
64
  export { PluginStorage, StorageError, } from "./storage.js";
63
65
  // Types (all types re-exported from barrel)
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,SAAS;AACT,OAAO,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,EAAqC,MAAM,aAAa,CAAC;AAEtF,SAAS;AACT,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,GAKjB,MAAM,aAAa,CAAC;AAErB,UAAU;AACV,OAAO,EACL,aAAa,EAGb,YAAY,GAKb,MAAM,cAAc,CAAC;AAEtB,4CAA4C;AAC5C,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,SAAS;AACT,OAAO,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,aAAa,EACb,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,qEAAqE;AACrE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE5D,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,EAAqC,MAAM,aAAa,CAAC;AAEtF,SAAS;AACT,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,gBAAgB,GAMjB,MAAM,aAAa,CAAC;AAErB,UAAU;AACV,OAAO,EACL,aAAa,EAGb,YAAY,GAKb,MAAM,cAAc,CAAC;AAEtB,4CAA4C;AAC5C,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Async-local context for the currently-handled forward request.
3
+ *
4
+ * When the SDK dispatches a forward call (e.g. `releases/poll`), it stores
5
+ * the call's `id` in this context for the duration of the handler. Any
6
+ * reverse-RPC the plugin makes while servicing that call (e.g.
7
+ * `releases/record` via `HostRpcClient.call`) reads the id and stamps it as
8
+ * `parentRequestId` on the outgoing request.
9
+ *
10
+ * The host uses `parentRequestId` to route the reverse-RPC back to the
11
+ * originating caller's tokio task, so emitted events land in the recording
12
+ * broadcaster scoped to that task and replay correctly in distributed
13
+ * deployments. Without this stamping, plugins that emit events via
14
+ * reverse-RPC would silently lose them on the worker.
15
+ *
16
+ * Plugin authors don't interact with this directly. The SDK's request
17
+ * dispatch (`server.ts`) sets it; `HostRpcClient.call` reads it.
18
+ */
19
+ /**
20
+ * Run `fn` with `forwardRequestId` as the current parent. Calls to
21
+ * `currentParentRequestId()` made inside `fn` (or anything it awaits) will
22
+ * see this value.
23
+ */
24
+ export declare function runWithParentRequestId<T>(forwardRequestId: string | number | null, fn: () => Promise<T>): Promise<T>;
25
+ /**
26
+ * Snapshot the current forward request id, or `undefined` if no forward
27
+ * request is on the call stack (e.g. background timers in the plugin that
28
+ * fire reverse-RPCs outside a forward-call context — those won't be replay-
29
+ * eligible, by design, since they don't belong to any task).
30
+ */
31
+ export declare function currentParentRequestId(): string | number | null | undefined;
32
+ //# sourceMappingURL=request-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,EACxC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAE3E"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Async-local context for the currently-handled forward request.
3
+ *
4
+ * When the SDK dispatches a forward call (e.g. `releases/poll`), it stores
5
+ * the call's `id` in this context for the duration of the handler. Any
6
+ * reverse-RPC the plugin makes while servicing that call (e.g.
7
+ * `releases/record` via `HostRpcClient.call`) reads the id and stamps it as
8
+ * `parentRequestId` on the outgoing request.
9
+ *
10
+ * The host uses `parentRequestId` to route the reverse-RPC back to the
11
+ * originating caller's tokio task, so emitted events land in the recording
12
+ * broadcaster scoped to that task and replay correctly in distributed
13
+ * deployments. Without this stamping, plugins that emit events via
14
+ * reverse-RPC would silently lose them on the worker.
15
+ *
16
+ * Plugin authors don't interact with this directly. The SDK's request
17
+ * dispatch (`server.ts`) sets it; `HostRpcClient.call` reads it.
18
+ */
19
+ import { AsyncLocalStorage } from "node:async_hooks";
20
+ const store = new AsyncLocalStorage();
21
+ /**
22
+ * Run `fn` with `forwardRequestId` as the current parent. Calls to
23
+ * `currentParentRequestId()` made inside `fn` (or anything it awaits) will
24
+ * see this value.
25
+ */
26
+ export function runWithParentRequestId(forwardRequestId, fn) {
27
+ return store.run(forwardRequestId, fn);
28
+ }
29
+ /**
30
+ * Snapshot the current forward request id, or `undefined` if no forward
31
+ * request is on the call stack (e.g. background timers in the plugin that
32
+ * fire reverse-RPCs outside a forward-call context — those won't be replay-
33
+ * eligible, by design, since they don't belong to any task).
34
+ */
35
+ export function currentParentRequestId() {
36
+ return store.getStore();
37
+ }
38
+ //# sourceMappingURL=request-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,KAAK,GAAG,IAAI,iBAAiB,EAA0B,CAAC;AAE9D;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,gBAAwC,EACxC,EAAoB;IAEpB,OAAO,KAAK,CAAC,GAAG,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC1B,CAAC"}
package/dist/server.d.ts CHANGED
@@ -9,9 +9,10 @@
9
9
  *
10
10
  * Each plugin type adds its own method routing on top.
11
11
  */
12
+ import { HostRpcClient } from "./host-rpc.js";
12
13
  import { PluginStorage } from "./storage.js";
13
- import type { BookMetadataProvider, MetadataContentType, MetadataProvider, RecommendationProvider, SyncProvider } from "./types/capabilities.js";
14
- import type { PluginManifest } from "./types/manifest.js";
14
+ import type { BookMetadataProvider, MetadataContentType, MetadataProvider, RecommendationProvider, ReleaseSourceProvider, SyncProvider } from "./types/capabilities.js";
15
+ import type { PluginManifest, ReleaseSourceCapability } from "./types/manifest.js";
15
16
  /**
16
17
  * Initialize parameters received from Codex
17
18
  */
@@ -30,6 +31,15 @@ export interface InitializeParams {
30
31
  * instance — the host resolves the user context automatically.
31
32
  */
32
33
  storage: PluginStorage;
34
+ /**
35
+ * Generic host reverse-RPC client.
36
+ *
37
+ * Use this to call host methods outside the storage namespace, notably
38
+ * the `releases/*` methods (`releases/list_tracked`, `releases/record`,
39
+ * `releases/source_state/get`, `releases/source_state/set`) for plugins
40
+ * declaring the `releaseSource` capability.
41
+ */
42
+ hostRpc: HostRpcClient;
33
43
  }
34
44
  /**
35
45
  * Options for creating a metadata plugin
@@ -178,4 +188,54 @@ export interface RecommendationPluginOptions {
178
188
  * for recommendation operations (get recommendations, update profile, dismiss).
179
189
  */
180
190
  export declare function createRecommendationPlugin(options: RecommendationPluginOptions): void;
191
+ /**
192
+ * Options for creating a release-source plugin.
193
+ */
194
+ export interface ReleaseSourcePluginOptions {
195
+ /** Plugin manifest. Must declare `capabilities.releaseSource`. */
196
+ manifest: PluginManifest & {
197
+ capabilities: {
198
+ releaseSource: ReleaseSourceCapability;
199
+ };
200
+ };
201
+ /** ReleaseSourceProvider implementation. */
202
+ provider: ReleaseSourceProvider;
203
+ /** Called when plugin receives initialize with credentials/config. */
204
+ onInitialize?: (params: InitializeParams) => void | Promise<void>;
205
+ /** Log level (default: "info"). */
206
+ logLevel?: "debug" | "info" | "warn" | "error";
207
+ }
208
+ /**
209
+ * Create and run a release-source plugin.
210
+ *
211
+ * The host calls `releases/poll` on a schedule (per `release_sources` row).
212
+ * The plugin returns candidates either inline (in the poll response) or by
213
+ * streaming `releases/record` reverse-RPC calls during the poll. Both styles
214
+ * are supported by the host.
215
+ *
216
+ * Plugins typically:
217
+ * 1. Fetch tracked series via `releases/list_tracked`.
218
+ * 2. For each series, GET the upstream feed (with `If-None-Match` from the
219
+ * previous ETag).
220
+ * 3. Parse + filter (language, group blocklist, etc.).
221
+ * 4. Either return all candidates in the poll response or call
222
+ * `releases/record` for each.
223
+ * 5. Persist the new ETag via `releases/source_state/set` (or include it on
224
+ * the poll response).
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * import { createReleaseSourcePlugin, type ReleaseSourceProvider } from "@ashdev/codex-plugin-sdk";
229
+ *
230
+ * const provider: ReleaseSourceProvider = {
231
+ * async poll({ sourceId, etag }) {
232
+ * // ...fetch + parse...
233
+ * return { candidates: [...], etag: "new-etag" };
234
+ * },
235
+ * };
236
+ *
237
+ * createReleaseSourcePlugin({ manifest, provider });
238
+ * ```
239
+ */
240
+ export declare function createReleaseSourcePlugin(options: ReleaseSourcePluginOptions): void;
181
241
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA0H1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC;CACxB;AAkOD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,QAAQ,EAAE,cAAc,GAAG;QACzB,YAAY,EAAE;YAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAA;SAAE,CAAC;KAC3D,CAAC;IACF,wFAAwF;IACxF,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,oFAAoF;IACpF,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,qEAAqE;IACrE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAmEzE;AAMD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,EAAE,cAAc,GAAG;QACzB,YAAY,EAAE;YAAE,YAAY,EAAE,IAAI,CAAA;SAAE,CAAC;KACtC,CAAC;IACF,kCAAkC;IAClC,QAAQ,EAAE,YAAY,CAAC;IACvB,qEAAqE;IACrE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAqBjE;AAMD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,QAAQ,EAAE,cAAc,GAAG;QACzB,YAAY,EAAE;YAAE,0BAA0B,EAAE,IAAI,CAAA;SAAE,CAAC;KACpD,CAAC;IACF,4CAA4C;IAC5C,QAAQ,EAAE,sBAAsB,CAAC;IACjC,qEAAqE;IACrE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,2BAA2B,GAAG,IAAI,CA8BrF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -11,7 +11,9 @@
11
11
  */
12
12
  import { createInterface } from "node:readline";
13
13
  import { PluginError } from "./errors.js";
14
+ import { HostRpcClient } from "./host-rpc.js";
14
15
  import { createLogger } from "./logger.js";
16
+ import { runWithParentRequestId } from "./request-context.js";
15
17
  import { PluginStorage } from "./storage.js";
16
18
  import { JSON_RPC_ERROR_CODES } from "./types/rpc.js";
17
19
  /**
@@ -106,17 +108,19 @@ function createPluginServer(options) {
106
108
  const logger = createLogger({ name: manifest.name, level: logLevel });
107
109
  const prefix = label ? `${label} plugin` : "plugin";
108
110
  const storage = new PluginStorage();
111
+ const hostRpc = new HostRpcClient();
109
112
  logger.info(`Starting ${prefix}: ${manifest.displayName} v${manifest.version}`);
110
113
  const rl = createInterface({
111
114
  input: process.stdin,
112
115
  terminal: false,
113
116
  });
114
117
  rl.on("line", (line) => {
115
- void handleLine(line, manifest, onInitialize, router, logger, storage);
118
+ void handleLine(line, manifest, onInitialize, router, logger, storage, hostRpc);
116
119
  });
117
120
  rl.on("close", () => {
118
121
  logger.info("stdin closed, shutting down");
119
122
  storage.cancelAll();
123
+ hostRpc.cancelAll();
120
124
  process.exit(0);
121
125
  });
122
126
  process.on("uncaughtException", (error) => {
@@ -140,13 +144,14 @@ function isJsonRpcResponse(obj) {
140
144
  return false;
141
145
  return "result" in obj || "error" in obj;
142
146
  }
143
- async function handleLine(line, manifest, onInitialize, router, logger, storage) {
147
+ async function handleLine(line, manifest, onInitialize, router, logger, storage, hostRpc) {
144
148
  const trimmed = line.trim();
145
149
  if (!trimmed)
146
150
  return;
147
- // Try to detect storage responses before full request handling.
148
- // Storage responses come from the host on stdin — they have id + (result|error)
149
- // but no method field.
151
+ // Try to detect responses (storage or host-rpc) before full request handling.
152
+ // Both come from the host on stdin — they have id + (result|error) but no
153
+ // method field. The two clients use disjoint id ranges so each can claim
154
+ // ownership without coordination; whichever owns the id resolves it.
150
155
  let parsed;
151
156
  try {
152
157
  parsed = JSON.parse(trimmed);
@@ -155,8 +160,10 @@ async function handleLine(line, manifest, onInitialize, router, logger, storage)
155
160
  // Will be handled as a parse error below
156
161
  }
157
162
  if (parsed && isJsonRpcResponse(parsed)) {
158
- logger.debug("Routing storage response", { id: parsed.id });
159
- storage.handleResponse(trimmed);
163
+ logger.debug("Routing reverse-RPC response", { id: parsed.id });
164
+ if (!hostRpc.handleResponse(trimmed)) {
165
+ storage.handleResponse(trimmed);
166
+ }
160
167
  return;
161
168
  }
162
169
  let id = null;
@@ -164,7 +171,11 @@ async function handleLine(line, manifest, onInitialize, router, logger, storage)
164
171
  const request = (parsed ?? JSON.parse(trimmed));
165
172
  id = request.id;
166
173
  logger.debug(`Received request: ${request.method}`, { id: request.id });
167
- const response = await handleRequest(request, manifest, onInitialize, router, logger, storage);
174
+ // Run the request handler inside the parent-request async-local context.
175
+ // Reverse-RPCs the handler issues via `HostRpcClient.call` will read this
176
+ // and stamp `parentRequestId` so the host can route the call back to the
177
+ // originating task. See `request-context.ts`.
178
+ const response = await runWithParentRequestId(request.id, () => handleRequest(request, manifest, onInitialize, router, logger, storage, hostRpc));
168
179
  if (response !== null) {
169
180
  writeResponse(response);
170
181
  }
@@ -201,14 +212,16 @@ async function handleLine(line, manifest, onInitialize, router, logger, storage)
201
212
  }
202
213
  }
203
214
  }
204
- async function handleRequest(request, manifest, onInitialize, router, logger, storage) {
215
+ async function handleRequest(request, manifest, onInitialize, router, logger, storage, hostRpc) {
205
216
  const { method, params, id } = request;
206
217
  // Common lifecycle methods
207
218
  switch (method) {
208
219
  case "initialize": {
209
220
  const initParams = (params ?? {});
210
- // Inject the storage client so plugins can persist data
221
+ // Inject the reverse-RPC clients so plugins can persist data and
222
+ // call host-side methods (e.g. releases/list_tracked).
211
223
  initParams.storage = storage;
224
+ initParams.hostRpc = hostRpc;
212
225
  if (onInitialize) {
213
226
  await onInitialize(initParams);
214
227
  }
@@ -219,6 +232,7 @@ async function handleRequest(request, manifest, onInitialize, router, logger, st
219
232
  case "shutdown": {
220
233
  logger.info("Shutdown requested");
221
234
  storage.cancelAll();
235
+ hostRpc.cancelAll();
222
236
  const response = { jsonrpc: "2.0", id, result: null };
223
237
  process.stdout.write(`${JSON.stringify(response)}\n`, () => {
224
238
  process.exit(0);
@@ -475,4 +489,65 @@ export function createRecommendationPlugin(options) {
475
489
  };
476
490
  createPluginServer({ manifest, onInitialize, logLevel, label: "recommendation", router });
477
491
  }
492
+ // =============================================================================
493
+ // Release Source Plugin
494
+ // =============================================================================
495
+ /**
496
+ * Validate `releases/poll` parameters. Requires a non-empty `sourceId` string;
497
+ * `etag` is optional.
498
+ */
499
+ function validateReleasePollParams(params) {
500
+ return validateStringFields(params, ["sourceId"]);
501
+ }
502
+ /**
503
+ * Create and run a release-source plugin.
504
+ *
505
+ * The host calls `releases/poll` on a schedule (per `release_sources` row).
506
+ * The plugin returns candidates either inline (in the poll response) or by
507
+ * streaming `releases/record` reverse-RPC calls during the poll. Both styles
508
+ * are supported by the host.
509
+ *
510
+ * Plugins typically:
511
+ * 1. Fetch tracked series via `releases/list_tracked`.
512
+ * 2. For each series, GET the upstream feed (with `If-None-Match` from the
513
+ * previous ETag).
514
+ * 3. Parse + filter (language, group blocklist, etc.).
515
+ * 4. Either return all candidates in the poll response or call
516
+ * `releases/record` for each.
517
+ * 5. Persist the new ETag via `releases/source_state/set` (or include it on
518
+ * the poll response).
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * import { createReleaseSourcePlugin, type ReleaseSourceProvider } from "@ashdev/codex-plugin-sdk";
523
+ *
524
+ * const provider: ReleaseSourceProvider = {
525
+ * async poll({ sourceId, etag }) {
526
+ * // ...fetch + parse...
527
+ * return { candidates: [...], etag: "new-etag" };
528
+ * },
529
+ * };
530
+ *
531
+ * createReleaseSourcePlugin({ manifest, provider });
532
+ * ```
533
+ */
534
+ export function createReleaseSourcePlugin(options) {
535
+ const { manifest, provider, onInitialize, logLevel } = options;
536
+ if (!manifest.capabilities.releaseSource) {
537
+ throw new Error("manifest.capabilities.releaseSource is required for createReleaseSourcePlugin");
538
+ }
539
+ const router = async (method, params, id) => {
540
+ switch (method) {
541
+ case "releases/poll": {
542
+ const err = validateReleasePollParams(params);
543
+ if (err)
544
+ return invalidParamsError(id, err);
545
+ return success(id, await provider.poll(params));
546
+ }
547
+ default:
548
+ return null;
549
+ }
550
+ };
551
+ createPluginServer({ manifest, onInitialize, logLevel, label: "release-source", router });
552
+ }
478
553
  //# sourceMappingURL=server.js.map