@ashdev/codex-plugin-metadata-echo 1.0.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/README.md +212 -0
- package/dist/index.js +461 -0
- package/dist/index.js.map +7 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# @ashdev/codex-plugin-metadata-echo
|
|
2
|
+
|
|
3
|
+
A minimal test metadata plugin for the Codex plugin system. Echoes back search queries and provides predictable responses for testing and development.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This plugin serves two purposes:
|
|
8
|
+
|
|
9
|
+
1. **Protocol Validation**: Demonstrates correct implementation of the Codex plugin protocol
|
|
10
|
+
2. **Development Testing**: Provides predictable responses for testing the plugin UI without external API dependencies
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @ashdev/codex-plugin-metadata-echo
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or run directly with npx (no installation required).
|
|
19
|
+
|
|
20
|
+
## Adding the Plugin to Codex
|
|
21
|
+
|
|
22
|
+
### Using npx (Recommended)
|
|
23
|
+
|
|
24
|
+
1. Log in to Codex as an administrator
|
|
25
|
+
2. Navigate to **Settings** > **Plugins**
|
|
26
|
+
3. Click **Add Plugin**
|
|
27
|
+
4. Fill in the form:
|
|
28
|
+
- **Name**: `metadata-echo`
|
|
29
|
+
- **Display Name**: `Echo Metadata Plugin`
|
|
30
|
+
- **Command**: `npx`
|
|
31
|
+
- **Arguments**: `-y @ashdev/codex-plugin-metadata-echo@1.0.0`
|
|
32
|
+
- **Scopes**: Select `series:detail`
|
|
33
|
+
5. Click **Save**
|
|
34
|
+
6. Click **Test Connection** to verify the plugin works
|
|
35
|
+
7. Toggle **Enabled** to activate the plugin
|
|
36
|
+
|
|
37
|
+
### npx Options
|
|
38
|
+
|
|
39
|
+
| Configuration | Arguments | Description |
|
|
40
|
+
|--------------|-----------|-------------|
|
|
41
|
+
| Latest version | `-y @ashdev/codex-plugin-metadata-echo` | Always uses latest |
|
|
42
|
+
| Pinned version | `-y @ashdev/codex-plugin-metadata-echo@1.0.0` | Recommended for production |
|
|
43
|
+
| Fast startup | `-y --prefer-offline @ashdev/codex-plugin-metadata-echo@1.0.0` | Skips version check if cached |
|
|
44
|
+
|
|
45
|
+
**Flags:**
|
|
46
|
+
- `-y`: Auto-confirms installation (required for containers)
|
|
47
|
+
- `--prefer-offline`: Uses cached version without checking npm registry
|
|
48
|
+
|
|
49
|
+
### Using Docker
|
|
50
|
+
|
|
51
|
+
For Docker deployments, use npx with `--prefer-offline` for faster startup:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Command: npx
|
|
55
|
+
Arguments: -y --prefer-offline @ashdev/codex-plugin-metadata-echo@1.0.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Manual Installation (Alternative)
|
|
59
|
+
|
|
60
|
+
For maximum performance, install globally:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @ashdev/codex-plugin-metadata-echo
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then configure:
|
|
67
|
+
- **Command**: `codex-plugin-metadata-echo`
|
|
68
|
+
- **Arguments**: (leave empty)
|
|
69
|
+
|
|
70
|
+
## Using the Plugin
|
|
71
|
+
|
|
72
|
+
Once enabled, the Echo plugin appears in the series detail page:
|
|
73
|
+
|
|
74
|
+
1. Navigate to any series in your library
|
|
75
|
+
2. Click the **Metadata** button (or look for the plugin icon)
|
|
76
|
+
3. Click **Search Echo Metadata Plugin**
|
|
77
|
+
4. Enter any search query
|
|
78
|
+
5. The plugin will echo your query back as search results
|
|
79
|
+
6. Select a result to see the preview
|
|
80
|
+
7. Click **Apply** to test the metadata apply flow
|
|
81
|
+
|
|
82
|
+
This is useful for:
|
|
83
|
+
- Testing the plugin UI without needing real API credentials
|
|
84
|
+
- Verifying the metadata preview and apply workflow
|
|
85
|
+
- Debugging plugin integration issues
|
|
86
|
+
|
|
87
|
+
## Response Behavior
|
|
88
|
+
|
|
89
|
+
### Search (`metadata/search`)
|
|
90
|
+
|
|
91
|
+
Returns two results for any query:
|
|
92
|
+
|
|
93
|
+
1. **Primary result**: Title is `"Echo: {query}"` with `relevanceScore: 1.0`
|
|
94
|
+
2. **Secondary result**: Title is `"Echo Result 2 for: {query}"` with `relevanceScore: 0.8`
|
|
95
|
+
|
|
96
|
+
### Get (`metadata/get`)
|
|
97
|
+
|
|
98
|
+
Returns metadata with the external ID embedded in the title and URL:
|
|
99
|
+
|
|
100
|
+
- Title: `"Echo Series: {externalId}"`
|
|
101
|
+
- External URL: `https://echo.example.com/series/{externalId}`
|
|
102
|
+
- Includes sample genres, tags, authors, and rating
|
|
103
|
+
|
|
104
|
+
### Match (`metadata/match`)
|
|
105
|
+
|
|
106
|
+
Returns a match based on the normalized title:
|
|
107
|
+
|
|
108
|
+
- External ID: `match-{normalized-title}`
|
|
109
|
+
- Confidence: `0.85`
|
|
110
|
+
- Includes one alternative match
|
|
111
|
+
|
|
112
|
+
## As a Reference Implementation
|
|
113
|
+
|
|
114
|
+
Use this plugin as a template for building your own metadata plugins:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import {
|
|
118
|
+
createMetadataPlugin,
|
|
119
|
+
type MetadataProvider,
|
|
120
|
+
} from "@ashdev/codex-plugin-sdk";
|
|
121
|
+
|
|
122
|
+
const provider: MetadataProvider = {
|
|
123
|
+
async search(params) {
|
|
124
|
+
return {
|
|
125
|
+
results: [
|
|
126
|
+
{
|
|
127
|
+
externalId: "123",
|
|
128
|
+
title: "Example",
|
|
129
|
+
alternateTitles: [],
|
|
130
|
+
relevanceScore: 0.95,
|
|
131
|
+
preview: {
|
|
132
|
+
status: "ongoing",
|
|
133
|
+
genres: ["Action"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async get(params) {
|
|
141
|
+
return {
|
|
142
|
+
externalId: params.externalId,
|
|
143
|
+
externalUrl: `https://example.com/${params.externalId}`,
|
|
144
|
+
alternateTitles: [],
|
|
145
|
+
genres: [],
|
|
146
|
+
tags: [],
|
|
147
|
+
authors: [],
|
|
148
|
+
artists: [],
|
|
149
|
+
externalLinks: [],
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Optional: implement match for auto-matching
|
|
154
|
+
async match(params) {
|
|
155
|
+
return {
|
|
156
|
+
match: null,
|
|
157
|
+
confidence: 0,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
createMetadataPlugin({
|
|
163
|
+
manifest: {
|
|
164
|
+
name: "metadata-my-plugin",
|
|
165
|
+
displayName: "My Metadata Plugin",
|
|
166
|
+
version: "1.0.0",
|
|
167
|
+
description: "My custom metadata plugin",
|
|
168
|
+
author: "Me",
|
|
169
|
+
protocolVersion: "1.0",
|
|
170
|
+
capabilities: { metadataProvider: true },
|
|
171
|
+
contentTypes: ["series"],
|
|
172
|
+
scopes: ["series:detail"],
|
|
173
|
+
},
|
|
174
|
+
provider,
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Install dependencies
|
|
182
|
+
npm install
|
|
183
|
+
|
|
184
|
+
# Build the plugin
|
|
185
|
+
npm run build
|
|
186
|
+
|
|
187
|
+
# Type check
|
|
188
|
+
npm run typecheck
|
|
189
|
+
|
|
190
|
+
# Run tests
|
|
191
|
+
npm test
|
|
192
|
+
|
|
193
|
+
# Lint
|
|
194
|
+
npm run lint
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Project Structure
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
plugins/metadata-echo/
|
|
201
|
+
├── src/
|
|
202
|
+
│ └── index.ts # Plugin implementation
|
|
203
|
+
├── dist/
|
|
204
|
+
│ └── index.js # Built bundle (excluded from git)
|
|
205
|
+
├── package.json
|
|
206
|
+
├── tsconfig.json
|
|
207
|
+
└── README.md
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../sdk-typescript/dist/types/rpc.js
|
|
4
|
+
var JSON_RPC_ERROR_CODES = {
|
|
5
|
+
/** Invalid JSON was received */
|
|
6
|
+
PARSE_ERROR: -32700,
|
|
7
|
+
/** The JSON sent is not a valid Request object */
|
|
8
|
+
INVALID_REQUEST: -32600,
|
|
9
|
+
/** The method does not exist / is not available */
|
|
10
|
+
METHOD_NOT_FOUND: -32601,
|
|
11
|
+
/** Invalid method parameter(s) */
|
|
12
|
+
INVALID_PARAMS: -32602,
|
|
13
|
+
/** Internal JSON-RPC error */
|
|
14
|
+
INTERNAL_ERROR: -32603
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ../sdk-typescript/dist/errors.js
|
|
18
|
+
var PluginError = class extends Error {
|
|
19
|
+
data;
|
|
20
|
+
constructor(message, data) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = this.constructor.name;
|
|
23
|
+
this.data = data;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convert to JSON-RPC error format
|
|
27
|
+
*/
|
|
28
|
+
toJsonRpcError() {
|
|
29
|
+
return {
|
|
30
|
+
code: this.code,
|
|
31
|
+
message: this.message,
|
|
32
|
+
data: this.data
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ../sdk-typescript/dist/logger.js
|
|
38
|
+
var LOG_LEVELS = {
|
|
39
|
+
debug: 0,
|
|
40
|
+
info: 1,
|
|
41
|
+
warn: 2,
|
|
42
|
+
error: 3
|
|
43
|
+
};
|
|
44
|
+
var Logger = class {
|
|
45
|
+
name;
|
|
46
|
+
minLevel;
|
|
47
|
+
timestamps;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.name = options.name;
|
|
50
|
+
this.minLevel = LOG_LEVELS[options.level ?? "info"];
|
|
51
|
+
this.timestamps = options.timestamps ?? true;
|
|
52
|
+
}
|
|
53
|
+
shouldLog(level) {
|
|
54
|
+
return LOG_LEVELS[level] >= this.minLevel;
|
|
55
|
+
}
|
|
56
|
+
format(level, message, data) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (this.timestamps) {
|
|
59
|
+
parts.push((/* @__PURE__ */ new Date()).toISOString());
|
|
60
|
+
}
|
|
61
|
+
parts.push(`[${level.toUpperCase()}]`);
|
|
62
|
+
parts.push(`[${this.name}]`);
|
|
63
|
+
parts.push(message);
|
|
64
|
+
if (data !== void 0) {
|
|
65
|
+
if (data instanceof Error) {
|
|
66
|
+
parts.push(`- ${data.message}`);
|
|
67
|
+
if (data.stack) {
|
|
68
|
+
parts.push(`
|
|
69
|
+
${data.stack}`);
|
|
70
|
+
}
|
|
71
|
+
} else if (typeof data === "object") {
|
|
72
|
+
parts.push(`- ${JSON.stringify(data)}`);
|
|
73
|
+
} else {
|
|
74
|
+
parts.push(`- ${String(data)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return parts.join(" ");
|
|
78
|
+
}
|
|
79
|
+
log(level, message, data) {
|
|
80
|
+
if (this.shouldLog(level)) {
|
|
81
|
+
process.stderr.write(`${this.format(level, message, data)}
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
debug(message, data) {
|
|
86
|
+
this.log("debug", message, data);
|
|
87
|
+
}
|
|
88
|
+
info(message, data) {
|
|
89
|
+
this.log("info", message, data);
|
|
90
|
+
}
|
|
91
|
+
warn(message, data) {
|
|
92
|
+
this.log("warn", message, data);
|
|
93
|
+
}
|
|
94
|
+
error(message, data) {
|
|
95
|
+
this.log("error", message, data);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function createLogger(options) {
|
|
99
|
+
return new Logger(options);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ../sdk-typescript/dist/server.js
|
|
103
|
+
import { createInterface } from "node:readline";
|
|
104
|
+
function validateStringFields(params, fields) {
|
|
105
|
+
if (params === null || params === void 0) {
|
|
106
|
+
return { field: "params", message: "params is required" };
|
|
107
|
+
}
|
|
108
|
+
if (typeof params !== "object") {
|
|
109
|
+
return { field: "params", message: "params must be an object" };
|
|
110
|
+
}
|
|
111
|
+
const obj = params;
|
|
112
|
+
for (const field of fields) {
|
|
113
|
+
const value = obj[field];
|
|
114
|
+
if (value === void 0 || value === null) {
|
|
115
|
+
return { field, message: `${field} is required` };
|
|
116
|
+
}
|
|
117
|
+
if (typeof value !== "string") {
|
|
118
|
+
return { field, message: `${field} must be a string` };
|
|
119
|
+
}
|
|
120
|
+
if (value.trim() === "") {
|
|
121
|
+
return { field, message: `${field} cannot be empty` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function validateSearchParams(params) {
|
|
127
|
+
return validateStringFields(params, ["query"]);
|
|
128
|
+
}
|
|
129
|
+
function validateGetParams(params) {
|
|
130
|
+
return validateStringFields(params, ["externalId"]);
|
|
131
|
+
}
|
|
132
|
+
function validateMatchParams(params) {
|
|
133
|
+
return validateStringFields(params, ["title"]);
|
|
134
|
+
}
|
|
135
|
+
function invalidParamsError(id, error) {
|
|
136
|
+
return {
|
|
137
|
+
jsonrpc: "2.0",
|
|
138
|
+
id,
|
|
139
|
+
error: {
|
|
140
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
141
|
+
message: `Invalid params: ${error.message}`,
|
|
142
|
+
data: { field: error.field }
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function createMetadataPlugin(options) {
|
|
147
|
+
const { manifest: manifest2, provider: provider2, onInitialize, logLevel = "info" } = options;
|
|
148
|
+
const logger = createLogger({ name: manifest2.name, level: logLevel });
|
|
149
|
+
logger.info(`Starting plugin: ${manifest2.displayName} v${manifest2.version}`);
|
|
150
|
+
const rl = createInterface({
|
|
151
|
+
input: process.stdin,
|
|
152
|
+
terminal: false
|
|
153
|
+
});
|
|
154
|
+
rl.on("line", (line) => {
|
|
155
|
+
void handleLine(line, manifest2, provider2, onInitialize, logger);
|
|
156
|
+
});
|
|
157
|
+
rl.on("close", () => {
|
|
158
|
+
logger.info("stdin closed, shutting down");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
});
|
|
161
|
+
process.on("uncaughtException", (error) => {
|
|
162
|
+
logger.error("Uncaught exception", error);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
process.on("unhandledRejection", (reason) => {
|
|
166
|
+
logger.error("Unhandled rejection", reason);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function handleLine(line, manifest2, provider2, onInitialize, logger) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (!trimmed)
|
|
172
|
+
return;
|
|
173
|
+
let id = null;
|
|
174
|
+
try {
|
|
175
|
+
const request = JSON.parse(trimmed);
|
|
176
|
+
id = request.id;
|
|
177
|
+
logger.debug(`Received request: ${request.method}`, { id: request.id });
|
|
178
|
+
const response = await handleRequest(request, manifest2, provider2, onInitialize, logger);
|
|
179
|
+
if (response !== null) {
|
|
180
|
+
writeResponse(response);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof SyntaxError) {
|
|
184
|
+
writeResponse({
|
|
185
|
+
jsonrpc: "2.0",
|
|
186
|
+
id: null,
|
|
187
|
+
error: {
|
|
188
|
+
code: JSON_RPC_ERROR_CODES.PARSE_ERROR,
|
|
189
|
+
message: "Parse error: invalid JSON"
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
} else if (error instanceof PluginError) {
|
|
193
|
+
writeResponse({
|
|
194
|
+
jsonrpc: "2.0",
|
|
195
|
+
id,
|
|
196
|
+
error: error.toJsonRpcError()
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
200
|
+
logger.error("Request failed", error);
|
|
201
|
+
writeResponse({
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
id,
|
|
204
|
+
error: {
|
|
205
|
+
code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
206
|
+
message
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function handleRequest(request, manifest2, provider2, onInitialize, logger) {
|
|
213
|
+
const { method, params, id } = request;
|
|
214
|
+
switch (method) {
|
|
215
|
+
case "initialize":
|
|
216
|
+
if (onInitialize) {
|
|
217
|
+
await onInitialize(params);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
jsonrpc: "2.0",
|
|
221
|
+
id,
|
|
222
|
+
result: manifest2
|
|
223
|
+
};
|
|
224
|
+
case "ping":
|
|
225
|
+
return {
|
|
226
|
+
jsonrpc: "2.0",
|
|
227
|
+
id,
|
|
228
|
+
result: "pong"
|
|
229
|
+
};
|
|
230
|
+
case "shutdown": {
|
|
231
|
+
logger.info("Shutdown requested");
|
|
232
|
+
const response = {
|
|
233
|
+
jsonrpc: "2.0",
|
|
234
|
+
id,
|
|
235
|
+
result: null
|
|
236
|
+
};
|
|
237
|
+
process.stdout.write(`${JSON.stringify(response)}
|
|
238
|
+
`, () => {
|
|
239
|
+
process.exit(0);
|
|
240
|
+
});
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
// Series metadata methods (scoped by content type)
|
|
244
|
+
case "metadata/series/search": {
|
|
245
|
+
const validationError = validateSearchParams(params);
|
|
246
|
+
if (validationError) {
|
|
247
|
+
return invalidParamsError(id, validationError);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
jsonrpc: "2.0",
|
|
251
|
+
id,
|
|
252
|
+
result: await provider2.search(params)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
case "metadata/series/get": {
|
|
256
|
+
const validationError = validateGetParams(params);
|
|
257
|
+
if (validationError) {
|
|
258
|
+
return invalidParamsError(id, validationError);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
jsonrpc: "2.0",
|
|
262
|
+
id,
|
|
263
|
+
result: await provider2.get(params)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case "metadata/series/match": {
|
|
267
|
+
if (!provider2.match) {
|
|
268
|
+
return {
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
error: {
|
|
272
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
273
|
+
message: "This plugin does not support match"
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const validationError = validateMatchParams(params);
|
|
278
|
+
if (validationError) {
|
|
279
|
+
return invalidParamsError(id, validationError);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
jsonrpc: "2.0",
|
|
283
|
+
id,
|
|
284
|
+
result: await provider2.match(params)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// Future: book metadata methods
|
|
288
|
+
// case "metadata/book/search":
|
|
289
|
+
// case "metadata/book/get":
|
|
290
|
+
// case "metadata/book/match":
|
|
291
|
+
default:
|
|
292
|
+
return {
|
|
293
|
+
jsonrpc: "2.0",
|
|
294
|
+
id,
|
|
295
|
+
error: {
|
|
296
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
297
|
+
message: `Method not found: ${method}`
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function writeResponse(response) {
|
|
303
|
+
process.stdout.write(`${JSON.stringify(response)}
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/index.ts
|
|
308
|
+
var manifest = {
|
|
309
|
+
name: "metadata-echo",
|
|
310
|
+
displayName: "Echo Metadata Plugin",
|
|
311
|
+
version: "1.0.0",
|
|
312
|
+
description: "Test metadata plugin that echoes back search queries",
|
|
313
|
+
author: "Codex",
|
|
314
|
+
homepage: "https://github.com/AshDevFr/codex",
|
|
315
|
+
protocolVersion: "1.0",
|
|
316
|
+
capabilities: {
|
|
317
|
+
metadataProvider: ["series"]
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var provider = {
|
|
321
|
+
async search(params) {
|
|
322
|
+
return {
|
|
323
|
+
results: [
|
|
324
|
+
{
|
|
325
|
+
externalId: "echo-1",
|
|
326
|
+
title: `Echo: ${params.query}`,
|
|
327
|
+
alternateTitles: [`Echoed Query: ${params.query}`],
|
|
328
|
+
year: (/* @__PURE__ */ new Date()).getFullYear(),
|
|
329
|
+
relevanceScore: 1,
|
|
330
|
+
// Perfect match for echo
|
|
331
|
+
preview: {
|
|
332
|
+
status: "ended",
|
|
333
|
+
genres: ["Test", "Echo"],
|
|
334
|
+
rating: 10,
|
|
335
|
+
description: `Search query echoed: "${params.query}"`
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
externalId: "echo-2",
|
|
340
|
+
title: `Echo Result 2 for: ${params.query}`,
|
|
341
|
+
alternateTitles: [],
|
|
342
|
+
relevanceScore: 0.8,
|
|
343
|
+
preview: {
|
|
344
|
+
status: "ongoing",
|
|
345
|
+
genres: ["Test"],
|
|
346
|
+
description: "A second result for testing pagination"
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
async get(params) {
|
|
353
|
+
return {
|
|
354
|
+
externalId: params.externalId,
|
|
355
|
+
externalUrl: `https://echo.example.com/series/${params.externalId}`,
|
|
356
|
+
title: `Echo Series: ${params.externalId}`,
|
|
357
|
+
alternateTitles: [
|
|
358
|
+
{ title: `Echo Series: ${params.externalId}`, language: "en", titleType: "english" },
|
|
359
|
+
{ title: `\u30A8\u30B3\u30FC\u30B7\u30EA\u30FC\u30BA: ${params.externalId}`, language: "ja", titleType: "native" },
|
|
360
|
+
{ title: `Echo Romanized: ${params.externalId}`, language: "ja-Latn", titleType: "romaji" }
|
|
361
|
+
],
|
|
362
|
+
summary: `This is the full metadata for external ID: ${params.externalId}. It includes a detailed description to test summary handling.`,
|
|
363
|
+
status: "ended",
|
|
364
|
+
year: 2024,
|
|
365
|
+
// Extended metadata fields
|
|
366
|
+
totalBookCount: 10,
|
|
367
|
+
language: "en",
|
|
368
|
+
ageRating: 13,
|
|
369
|
+
readingDirection: "ltr",
|
|
370
|
+
// Taxonomy
|
|
371
|
+
genres: ["Action", "Comedy", "Test", "Echo"],
|
|
372
|
+
tags: ["plugin-test", "echo", "automation", "development"],
|
|
373
|
+
// Credits
|
|
374
|
+
authors: ["Echo Author", "Test Writer"],
|
|
375
|
+
artists: ["Echo Artist"],
|
|
376
|
+
publisher: "Echo Publisher",
|
|
377
|
+
// Media
|
|
378
|
+
coverUrl: "https://picsum.photos/300/450",
|
|
379
|
+
bannerUrl: "https://picsum.photos/800/200",
|
|
380
|
+
// Primary rating
|
|
381
|
+
rating: {
|
|
382
|
+
score: 85,
|
|
383
|
+
voteCount: 100,
|
|
384
|
+
source: "echo"
|
|
385
|
+
},
|
|
386
|
+
// Multiple external ratings for testing aggregation
|
|
387
|
+
externalRatings: [
|
|
388
|
+
{
|
|
389
|
+
score: 85,
|
|
390
|
+
voteCount: 100,
|
|
391
|
+
source: "echo"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
score: 92,
|
|
395
|
+
voteCount: 5e3,
|
|
396
|
+
source: "anilist"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
score: 88,
|
|
400
|
+
voteCount: 2500,
|
|
401
|
+
source: "mal"
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
// External links
|
|
405
|
+
externalLinks: [
|
|
406
|
+
{
|
|
407
|
+
url: `https://echo.example.com/series/${params.externalId}`,
|
|
408
|
+
label: "Echo Provider",
|
|
409
|
+
linkType: "provider"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
url: "https://official-echo.example.com",
|
|
413
|
+
label: "Official Site",
|
|
414
|
+
linkType: "official"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
url: "https://twitter.com/echo_series",
|
|
418
|
+
label: "Twitter",
|
|
419
|
+
linkType: "social"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
url: "https://store.example.com/echo",
|
|
423
|
+
label: "Buy",
|
|
424
|
+
linkType: "purchase"
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
async match(params) {
|
|
430
|
+
const normalizedTitle = params.title.toLowerCase().replace(/\s+/g, "-");
|
|
431
|
+
return {
|
|
432
|
+
match: {
|
|
433
|
+
externalId: `match-${normalizedTitle}`,
|
|
434
|
+
title: params.title,
|
|
435
|
+
alternateTitles: [],
|
|
436
|
+
year: params.year,
|
|
437
|
+
relevanceScore: 0.9,
|
|
438
|
+
preview: {
|
|
439
|
+
status: "ended",
|
|
440
|
+
genres: ["Matched"],
|
|
441
|
+
description: `Matched from title: "${params.title}"`
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
confidence: 0.85,
|
|
445
|
+
alternatives: [
|
|
446
|
+
{
|
|
447
|
+
externalId: "alt-1",
|
|
448
|
+
title: `Alternative: ${params.title}`,
|
|
449
|
+
alternateTitles: [],
|
|
450
|
+
relevanceScore: 0.6
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
createMetadataPlugin({
|
|
457
|
+
manifest,
|
|
458
|
+
provider,
|
|
459
|
+
logLevel: "debug"
|
|
460
|
+
});
|
|
461
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../sdk-typescript/src/types/rpc.ts", "../../sdk-typescript/src/errors.ts", "../../sdk-typescript/src/logger.ts", "../../sdk-typescript/src/server.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\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 * 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\nimport { createInterface } from \"node:readline\";\nimport { PluginError } from \"./errors.js\";\nimport { createLogger, type Logger } from \"./logger.js\";\nimport type { MetadataContentType, MetadataProvider } from \"./types/capabilities.js\";\nimport type { PluginManifest } from \"./types/manifest.js\";\nimport type {\n MetadataGetParams,\n MetadataMatchParams,\n MetadataSearchParams,\n} from \"./types/protocol.js\";\nimport {\n JSON_RPC_ERROR_CODES,\n type JsonRpcError,\n type JsonRpcRequest,\n type JsonRpcResponse,\n} from \"./types/rpc.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 * 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 } as JsonRpcError,\n };\n}\n\n/**\n * Initialize parameters received from Codex\n */\nexport interface InitializeParams {\n /** Plugin configuration */\n config?: Record<string, unknown>;\n /** Plugin credentials (API keys, tokens, etc.) */\n credentials?: Record<string, string>;\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 /** MetadataProvider implementation */\n provider: MetadataProvider;\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, onInitialize, logLevel = \"info\" } = options;\n const logger = createLogger({ name: manifest.name, level: logLevel });\n\n logger.info(`Starting plugin: ${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, provider, onInitialize, logger);\n });\n\n rl.on(\"close\", () => {\n logger.info(\"stdin closed, shutting down\");\n process.exit(0);\n });\n\n // Handle uncaught errors\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// Backwards Compatibility (deprecated)\n// =============================================================================\n\n/**\n * @deprecated Use createMetadataPlugin instead\n */\nexport function createSeriesMetadataPlugin(options: SeriesMetadataPluginOptions): void {\n // Convert legacy options to new format\n const newOptions: MetadataPluginOptions = {\n ...options,\n manifest: {\n ...options.manifest,\n capabilities: {\n ...options.manifest.capabilities,\n metadataProvider: [\"series\"] as MetadataContentType[],\n },\n },\n };\n createMetadataPlugin(newOptions);\n}\n\n/**\n * @deprecated Use MetadataPluginOptions instead\n */\nexport interface SeriesMetadataPluginOptions {\n /** Plugin manifest - must have capabilities.seriesMetadataProvider: true */\n manifest: PluginManifest & {\n capabilities: { seriesMetadataProvider: true };\n };\n /** SeriesMetadataProvider implementation */\n provider: MetadataProvider;\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// Internal Implementation\n// =============================================================================\n\nasync function handleLine(\n line: string,\n manifest: PluginManifest,\n provider: MetadataProvider,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n logger: Logger,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let id: string | number | null = null;\n\n try {\n const request = JSON.parse(trimmed) as JsonRpcRequest;\n id = request.id;\n\n logger.debug(`Received request: ${request.method}`, { id: request.id });\n\n const response = await handleRequest(request, manifest, provider, onInitialize, logger);\n // Shutdown handler writes response directly and returns null\n if (response !== null) {\n writeResponse(response);\n }\n } catch (error) {\n if (error instanceof SyntaxError) {\n // JSON parse error\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 provider: MetadataProvider,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n logger: Logger,\n): Promise<JsonRpcResponse> {\n const { method, params, id } = request;\n\n switch (method) {\n case \"initialize\":\n // Call onInitialize callback if provided (to receive credentials/config)\n if (onInitialize) {\n await onInitialize(params as InitializeParams);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: manifest,\n };\n\n case \"ping\":\n return {\n jsonrpc: \"2.0\",\n id,\n result: \"pong\",\n };\n\n case \"shutdown\": {\n logger.info(\"Shutdown requested\");\n // Write response directly with callback to ensure it's flushed before exit\n const response: JsonRpcResponse = {\n jsonrpc: \"2.0\",\n id,\n result: null,\n };\n process.stdout.write(`${JSON.stringify(response)}\\n`, () => {\n // Callback is called after the write is flushed to the OS\n process.exit(0);\n });\n // Return a sentinel that handleLine will recognize and skip normal writeResponse\n return null as unknown as JsonRpcResponse;\n }\n\n // Series metadata methods (scoped by content type)\n case \"metadata/series/search\": {\n const validationError = validateSearchParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.search(params as MetadataSearchParams),\n };\n }\n\n case \"metadata/series/get\": {\n const validationError = validateGetParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.get(params as MetadataGetParams),\n };\n }\n\n case \"metadata/series/match\": {\n if (!provider.match) {\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,\n message: \"This plugin does not support match\",\n },\n };\n }\n const validationError = validateMatchParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.match(params as MetadataMatchParams),\n };\n }\n\n // Future: book metadata methods\n // case \"metadata/book/search\":\n // case \"metadata/book/get\":\n // case \"metadata/book/match\":\n\n default:\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}\n\nfunction writeResponse(response: JsonRpcResponse): void {\n // Write to stdout - this is the JSON-RPC channel\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n}\n", "/**\n * Echo Plugin - A minimal plugin for testing the Codex plugin protocol\n *\n * This plugin demonstrates the plugin SDK usage and serves as a protocol\n * validation tool. It echoes back search parameters and provides predictable\n * responses for testing.\n */\n\nimport {\n createMetadataPlugin,\n type MetadataContentType,\n type MetadataGetParams,\n type MetadataMatchParams,\n type MetadataMatchResponse,\n type MetadataProvider,\n type MetadataSearchParams,\n type MetadataSearchResponse,\n type PluginManifest,\n type PluginSeriesMetadata,\n} from \"@ashdev/codex-plugin-sdk\";\n\nconst manifest = {\n name: \"metadata-echo\",\n displayName: \"Echo Metadata Plugin\",\n version: \"1.0.0\",\n description: \"Test metadata plugin that echoes back search queries\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.0\",\n capabilities: {\n metadataProvider: [\"series\"] as MetadataContentType[],\n },\n} as const satisfies PluginManifest & {\n capabilities: { metadataProvider: MetadataContentType[] };\n};\n\nconst provider: MetadataProvider = {\n async search(params: MetadataSearchParams): Promise<MetadataSearchResponse> {\n // Echo back the query as search results\n return {\n results: [\n {\n externalId: \"echo-1\",\n title: `Echo: ${params.query}`,\n alternateTitles: [`Echoed Query: ${params.query}`],\n year: new Date().getFullYear(),\n relevanceScore: 1.0, // Perfect match for echo\n preview: {\n status: \"ended\",\n genres: [\"Test\", \"Echo\"],\n rating: 10.0,\n description: `Search query echoed: \"${params.query}\"`,\n },\n },\n {\n externalId: \"echo-2\",\n title: `Echo Result 2 for: ${params.query}`,\n alternateTitles: [],\n relevanceScore: 0.8,\n preview: {\n status: \"ongoing\",\n genres: [\"Test\"],\n description: \"A second result for testing pagination\",\n },\n },\n ],\n };\n },\n\n async get(params: MetadataGetParams): Promise<PluginSeriesMetadata> {\n // Return metadata based on the external ID with all fields populated for testing\n return {\n externalId: params.externalId,\n externalUrl: `https://echo.example.com/series/${params.externalId}`,\n title: `Echo Series: ${params.externalId}`,\n alternateTitles: [\n { title: `Echo Series: ${params.externalId}`, language: \"en\", titleType: \"english\" },\n { title: `\u30A8\u30B3\u30FC\u30B7\u30EA\u30FC\u30BA: ${params.externalId}`, language: \"ja\", titleType: \"native\" },\n { title: `Echo Romanized: ${params.externalId}`, language: \"ja-Latn\", titleType: \"romaji\" },\n ],\n summary: `This is the full metadata for external ID: ${params.externalId}. It includes a detailed description to test summary handling.`,\n status: \"ended\",\n year: 2024,\n\n // Extended metadata fields\n totalBookCount: 10,\n language: \"en\",\n ageRating: 13,\n readingDirection: \"ltr\",\n\n // Taxonomy\n genres: [\"Action\", \"Comedy\", \"Test\", \"Echo\"],\n tags: [\"plugin-test\", \"echo\", \"automation\", \"development\"],\n\n // Credits\n authors: [\"Echo Author\", \"Test Writer\"],\n artists: [\"Echo Artist\"],\n publisher: \"Echo Publisher\",\n\n // Media\n coverUrl: \"https://picsum.photos/300/450\",\n bannerUrl: \"https://picsum.photos/800/200\",\n\n // Primary rating\n rating: {\n score: 85,\n voteCount: 100,\n source: \"echo\",\n },\n\n // Multiple external ratings for testing aggregation\n externalRatings: [\n {\n score: 85,\n voteCount: 100,\n source: \"echo\",\n },\n {\n score: 92,\n voteCount: 5000,\n source: \"anilist\",\n },\n {\n score: 88,\n voteCount: 2500,\n source: \"mal\",\n },\n ],\n\n // External links\n externalLinks: [\n {\n url: `https://echo.example.com/series/${params.externalId}`,\n label: \"Echo Provider\",\n linkType: \"provider\",\n },\n {\n url: \"https://official-echo.example.com\",\n label: \"Official Site\",\n linkType: \"official\",\n },\n {\n url: \"https://twitter.com/echo_series\",\n label: \"Twitter\",\n linkType: \"social\",\n },\n {\n url: \"https://store.example.com/echo\",\n label: \"Buy\",\n linkType: \"purchase\",\n },\n ],\n };\n },\n\n async match(params: MetadataMatchParams): Promise<MetadataMatchResponse> {\n // Return a match based on the title\n const normalizedTitle = params.title.toLowerCase().replace(/\\s+/g, \"-\");\n return {\n match: {\n externalId: `match-${normalizedTitle}`,\n title: params.title,\n alternateTitles: [],\n year: params.year,\n relevanceScore: 0.9,\n preview: {\n status: \"ended\",\n genres: [\"Matched\"],\n description: `Matched from title: \"${params.title}\"`,\n },\n },\n confidence: 0.85,\n alternatives: [\n {\n externalId: \"alt-1\",\n title: `Alternative: ${params.title}`,\n alternateTitles: [],\n relevanceScore: 0.6,\n },\n ],\n };\n },\n};\n\ncreateMetadataPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["manifest", "provider", "manifest", "provider"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ashdev/codex-plugin-metadata-echo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Echo metadata plugin for testing Codex plugin protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/AshDevFr/codex.git",
|
|
15
|
+
"directory": "plugins/metadata-echo"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'",
|
|
19
|
+
"dev": "npm run build -- --watch",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"lint": "biome check .",
|
|
23
|
+
"lint:fix": "biome check --write .",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run --passWithNoTests",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"codex",
|
|
31
|
+
"plugin",
|
|
32
|
+
"echo",
|
|
33
|
+
"testing"
|
|
34
|
+
],
|
|
35
|
+
"author": "Codex",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@ashdev/codex-plugin-sdk": "^1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "^2.3.13",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"esbuild": "^0.24.0",
|
|
47
|
+
"typescript": "^5.7.0",
|
|
48
|
+
"vitest": "^3.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|