@ashdev/codex-plugin-sdk 1.18.1 → 1.19.1
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/host-rpc.d.ts +65 -0
- package/dist/host-rpc.d.ts.map +1 -0
- package/dist/host-rpc.js +142 -0
- package/dist/host-rpc.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/request-context.d.ts +32 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +38 -0
- package/dist/request-context.js.map +1 -0
- package/dist/server.d.ts +62 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +85 -10
- package/dist/server.js.map +1 -1
- package/dist/types/capabilities.d.ts +28 -0
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/manifest.d.ts +71 -5
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +7 -0
- package/dist/types/manifest.js.map +1 -1
- package/dist/types/releases.d.ts +256 -0
- package/dist/types/releases.d.ts.map +1 -0
- package/dist/types/releases.js +41 -0
- package/dist/types/releases.js.map +1 -0
- package/dist/types/rpc.d.ts +9 -0
- package/dist/types/rpc.d.ts.map +1 -1
- package/dist/types/rpc.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/host-rpc.js
ADDED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,EACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AA2HnF;;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;IACvB;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC;CACxB;AAkPD;;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;AAcD;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,QAAQ,EAAE,cAAc,GAAG;QACzB,YAAY,EAAE;YAAE,aAAa,EAAE,uBAAuB,CAAA;SAAE,CAAC;KAC1D,CAAC;IACF,4CAA4C;IAC5C,QAAQ,EAAE,qBAAqB,CAAC;IAChC,sEAAsE;IACtE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAsBnF"}
|
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
|
|
148
|
-
//
|
|
149
|
-
//
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
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
|