trakt-tools 1.8.5 → 1.8.10

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,18 @@
1
+ import * as openclaw_plugin_sdk_core from 'openclaw/plugin-sdk/core';
2
+ import * as openclaw_plugin_sdk from 'openclaw/plugin-sdk';
3
+
4
+ /**
5
+ * OpenClaw plugin entry for trakt-cli.
6
+ *
7
+ * Registers tools that shell out to the `trakt-cli` binary.
8
+ * Each tool maps to a CLI subcommand (search, history, watchlist, progress).
9
+ */
10
+ declare const _default: {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ configSchema: openclaw_plugin_sdk.OpenClawPluginConfigSchema;
15
+ register: NonNullable<openclaw_plugin_sdk_core.OpenClawPluginDefinition["register"]>;
16
+ } & Pick<openclaw_plugin_sdk_core.OpenClawPluginDefinition, "kind">;
17
+
18
+ export { _default as default };
package/dist/index.js ADDED
@@ -0,0 +1,342 @@
1
+ // src/index.ts
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { execFileSync, execFile } from "child_process";
5
+ import { promisify } from "util";
6
+ import { existsSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ var execFileAsync = promisify(execFile);
10
+ var TOOLS = [
11
+ {
12
+ name: "trakt_search",
13
+ command: "search",
14
+ description: "Search Trakt.tv for movies and TV shows. Returns title, year, trakt_id, and imdb.",
15
+ parameters: Type.Object({
16
+ query: Type.String({ description: "Search query (movie or show title)" }),
17
+ type: Type.Optional(
18
+ Type.Union(
19
+ [Type.Literal("movie"), Type.Literal("show"), Type.Literal("movie,show")],
20
+ { description: "Filter by type (default: movie,show)" }
21
+ )
22
+ )
23
+ })
24
+ },
25
+ {
26
+ name: "trakt_history",
27
+ command: "history",
28
+ description: "View Trakt.tv watch history. Returns recently watched movies and episodes with timestamps.",
29
+ parameters: Type.Object({
30
+ type: Type.Optional(
31
+ Type.Union([Type.Literal("movies"), Type.Literal("shows")], {
32
+ description: "Filter by type"
33
+ })
34
+ ),
35
+ limit: Type.Optional(Type.Number({ description: "Items per page (default 10)" })),
36
+ page: Type.Optional(Type.Number({ description: "Page number" }))
37
+ })
38
+ },
39
+ {
40
+ name: "trakt_history_add",
41
+ command: "history",
42
+ subcommand: "add",
43
+ description: "Mark movies or shows as watched on Trakt.tv. Searches by title and adds to history. Accepts multiple titles.",
44
+ parameters: Type.Object({
45
+ titles: Type.Array(Type.String(), { description: "Title(s) to mark as watched" }),
46
+ type: Type.Optional(
47
+ Type.Union([Type.Literal("movie"), Type.Literal("show")], {
48
+ description: "Content type (default: show)"
49
+ })
50
+ ),
51
+ watched_at: Type.Optional(
52
+ Type.String({
53
+ description: "When watched (YYYY-MM-DD or RFC3339, defaults to now)"
54
+ })
55
+ )
56
+ })
57
+ },
58
+ {
59
+ name: "trakt_history_remove",
60
+ command: "history",
61
+ subcommand: "remove",
62
+ description: "Remove movies or shows from Trakt.tv watch history (undo a mark-as-watched). Searches by title; removing a show wipes ALL its episode plays, removing a movie wipes all its watches. There is no per-watch granular removal.",
63
+ parameters: Type.Object({
64
+ titles: Type.Array(Type.String(), {
65
+ description: "Title(s) to remove from history",
66
+ minItems: 1
67
+ }),
68
+ type: Type.Optional(
69
+ Type.Union([Type.Literal("movie"), Type.Literal("show")], {
70
+ description: "Content type (default: show)"
71
+ })
72
+ )
73
+ })
74
+ },
75
+ {
76
+ name: "trakt_watchlist",
77
+ command: "watchlist",
78
+ description: "View Trakt.tv watchlist. Returns items the user wants to watch, with type, title, year, and added date.",
79
+ parameters: Type.Object({
80
+ type: Type.Optional(
81
+ Type.Union([Type.Literal("movies"), Type.Literal("shows")], {
82
+ description: "Filter by type"
83
+ })
84
+ ),
85
+ limit: Type.Optional(Type.Number({ description: "Items per page (default 10)" })),
86
+ page: Type.Optional(Type.Number({ description: "Page number" }))
87
+ })
88
+ },
89
+ {
90
+ name: "trakt_watchlist_add",
91
+ command: "watchlist",
92
+ subcommand: "add",
93
+ description: "Add movies or shows to the Trakt.tv watchlist. Searches by title and queues matches in one sync call. Accepts multiple titles. Items already on the list are reported under `existing_*` counts.",
94
+ parameters: Type.Object({
95
+ titles: Type.Array(Type.String(), {
96
+ description: "Title(s) to add to the watchlist",
97
+ minItems: 1
98
+ }),
99
+ type: Type.Optional(
100
+ Type.Union([Type.Literal("movie"), Type.Literal("show")], {
101
+ description: "Content type (default: show)"
102
+ })
103
+ )
104
+ })
105
+ },
106
+ {
107
+ name: "trakt_watchlist_remove",
108
+ command: "watchlist",
109
+ subcommand: "remove",
110
+ description: "Remove movies or shows from the Trakt.tv watchlist. Searches by title and removes matches in one sync call. Accepts multiple titles. Items that were not on the list are silent no-ops (deleted_*=0).",
111
+ parameters: Type.Object({
112
+ titles: Type.Array(Type.String(), {
113
+ description: "Title(s) to remove from the watchlist",
114
+ minItems: 1
115
+ }),
116
+ type: Type.Optional(
117
+ Type.Union([Type.Literal("movie"), Type.Literal("show")], {
118
+ description: "Content type (default: show)"
119
+ })
120
+ )
121
+ })
122
+ },
123
+ {
124
+ name: "trakt_calendar",
125
+ command: "calendar",
126
+ description: 'Show upcoming episodes of shows you watch. Returns forward-looking airings within a date window. Use for proactive "what airs next?" workflows \u2014 this is the only trakt tool that returns future data.',
127
+ parameters: Type.Object({
128
+ days: Type.Optional(
129
+ Type.Number({ description: "Number of days to look ahead (default 7)" })
130
+ ),
131
+ start: Type.Optional(
132
+ Type.String({ description: "Start date YYYY-MM-DD (defaults to today)" })
133
+ ),
134
+ new: Type.Optional(
135
+ Type.Boolean({
136
+ description: "Only return series premieres (S01E01 airings)"
137
+ })
138
+ )
139
+ })
140
+ },
141
+ {
142
+ name: "trakt_progress",
143
+ command: "progress",
144
+ description: "Show progress of watchlist TV shows. Returns in-progress shows with episode counts, percentage, and next episode to watch.",
145
+ parameters: Type.Object({
146
+ all: Type.Optional(
147
+ Type.Boolean({
148
+ description: "Include not_started and completed shows (default: in-progress only)"
149
+ })
150
+ )
151
+ })
152
+ },
153
+ {
154
+ name: "trakt_auth",
155
+ command: "auth",
156
+ description: "Set up Trakt.tv authentication. Initiates OAuth device flow using configured client credentials. Only needed for initial setup.",
157
+ parameters: Type.Object({})
158
+ }
159
+ ];
160
+ function toolResult(text) {
161
+ return {
162
+ content: [{ type: "text", text }],
163
+ details: void 0
164
+ };
165
+ }
166
+ function whichBinary(name) {
167
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
168
+ try {
169
+ const result = execFileSync(cmd, [name], { encoding: "utf8" }).trim();
170
+ const first = result.split("\n")[0]?.trim();
171
+ return first || null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ function resolveCliPath(config) {
177
+ if (config?.cliPath && existsSync(config.cliPath)) {
178
+ return config.cliPath;
179
+ }
180
+ const envPath = process.env.TRAKT_CLI_PATH;
181
+ if (envPath && existsSync(envPath)) {
182
+ return envPath;
183
+ }
184
+ for (const name of ["trakt-cli", "trakt-plugin"]) {
185
+ const found = whichBinary(name);
186
+ if (found) return found;
187
+ }
188
+ throw new Error(
189
+ "trakt-cli not found. Install with: go install github.com/omarshahine/trakt-plugin@latest\nOr set TRAKT_CLI_PATH or configure cliPath in plugin settings."
190
+ );
191
+ }
192
+ function isAuthConfigured() {
193
+ return existsSync(join(homedir(), ".trakt.yaml"));
194
+ }
195
+ function buildCliArgs(command, subcommand, params, config) {
196
+ const args = [command];
197
+ if (command === "auth") {
198
+ if (config?.clientId) args.push("--client-id", config.clientId);
199
+ if (config?.clientSecret) args.push("--client-secret", config.clientSecret);
200
+ return args;
201
+ }
202
+ if (subcommand) args.push(subcommand);
203
+ if (command === "search") {
204
+ if (params.query) args.push(String(params.query));
205
+ }
206
+ if (command === "history" && subcommand === "add") {
207
+ const titles = params.titles;
208
+ if (titles) {
209
+ if (params.type) args.push("--type", String(params.type));
210
+ if (params.watched_at) args.push("--watched-at", String(params.watched_at));
211
+ for (const title of titles) {
212
+ args.push(title);
213
+ }
214
+ args.push("--json");
215
+ return args;
216
+ }
217
+ }
218
+ const titleArrayCommands = [
219
+ ["watchlist", "add"],
220
+ ["watchlist", "remove"],
221
+ ["history", "remove"]
222
+ ];
223
+ if (titleArrayCommands.some(([c, s]) => c === command && s === subcommand)) {
224
+ const titles = params.titles;
225
+ if (titles) {
226
+ if (params.type) args.push("--type", String(params.type));
227
+ for (const title of titles) {
228
+ args.push(title);
229
+ }
230
+ args.push("--json");
231
+ return args;
232
+ }
233
+ }
234
+ const skipKeys = /* @__PURE__ */ new Set(["query", "titles"]);
235
+ for (const [key, value] of Object.entries(params)) {
236
+ if (skipKeys.has(key) || value === void 0 || value === null || value === false) continue;
237
+ const flag = `--${key.replace(/_/g, "-")}`;
238
+ if (typeof value === "boolean") {
239
+ args.push(flag);
240
+ } else {
241
+ args.push(flag, String(value));
242
+ }
243
+ }
244
+ args.push("--json");
245
+ return args;
246
+ }
247
+ var index_default = definePluginEntry({
248
+ id: "trakt-tools",
249
+ name: "Trakt",
250
+ description: "Track movies and TV shows using trakt.tv",
251
+ register(api) {
252
+ const config = api.pluginConfig;
253
+ let cliPath;
254
+ try {
255
+ cliPath = resolveCliPath(config);
256
+ } catch (error) {
257
+ const errorMessage = error instanceof Error ? error.message : String(error);
258
+ for (const tool of TOOLS) {
259
+ api.registerTool({
260
+ name: tool.name,
261
+ label: tool.name,
262
+ description: tool.description,
263
+ parameters: tool.parameters,
264
+ async execute() {
265
+ return toolResult(
266
+ JSON.stringify({ success: false, error: errorMessage }, null, 2)
267
+ );
268
+ }
269
+ });
270
+ }
271
+ return;
272
+ }
273
+ for (const tool of TOOLS) {
274
+ api.registerTool({
275
+ name: tool.name,
276
+ label: tool.name,
277
+ description: tool.description,
278
+ parameters: tool.parameters,
279
+ async execute(_id, params) {
280
+ if (tool.command !== "auth" && !isAuthConfigured()) {
281
+ return toolResult(
282
+ JSON.stringify(
283
+ {
284
+ success: false,
285
+ error: "Trakt auth not configured. Run trakt_auth first, or manually: trakt-cli auth --client-id X --client-secret Y"
286
+ },
287
+ null,
288
+ 2
289
+ )
290
+ );
291
+ }
292
+ try {
293
+ const args = buildCliArgs(tool.command, tool.subcommand, params, config);
294
+ const { stdout } = await execFileAsync(cliPath, args, {
295
+ encoding: "utf8",
296
+ timeout: 12e4,
297
+ maxBuffer: 4 * 1024 * 1024
298
+ });
299
+ let result;
300
+ try {
301
+ result = JSON.parse(stdout);
302
+ } catch {
303
+ result = { output: stdout.trim() };
304
+ }
305
+ return toolResult(JSON.stringify(result, null, 2));
306
+ } catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr).trim() : "";
309
+ const errorOutput = stderr ? `${message}
310
+
311
+ stderr: ${stderr}` : message;
312
+ const rateLimitMatch = errorOutput.match(
313
+ /rate limited by trakt api[^\n]*?retry_after=(\d+)/i
314
+ ) || errorOutput.match(/rate limited by trakt api/i);
315
+ if (rateLimitMatch) {
316
+ const retryAfter = rateLimitMatch[1] ? parseInt(rateLimitMatch[1], 10) : null;
317
+ return toolResult(
318
+ JSON.stringify(
319
+ {
320
+ success: false,
321
+ rate_limited: true,
322
+ retry_after_seconds: retryAfter,
323
+ error: "Trakt API rate-limited this request" + (retryAfter != null ? ` (retry after ~${retryAfter}s)` : "") + ". Do NOT retry immediately \u2014 wait out the window before calling any trakt_* tool again, and avoid falling back to other trakt tools (they share the same rate limit bucket).",
324
+ raw: errorOutput
325
+ },
326
+ null,
327
+ 2
328
+ )
329
+ );
330
+ }
331
+ return toolResult(
332
+ JSON.stringify({ success: false, error: errorOutput }, null, 2)
333
+ );
334
+ }
335
+ }
336
+ });
337
+ }
338
+ }
339
+ });
340
+ export {
341
+ index_default as default
342
+ };
@@ -2,7 +2,7 @@
2
2
  "id": "trakt-tools",
3
3
  "name": "Trakt",
4
4
  "description": "Track movies and TV shows using trakt.tv",
5
- "version": "1.8.5",
5
+ "version": "1.8.10",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trakt-tools",
3
- "version": "1.8.5",
3
+ "version": "1.8.10",
4
4
  "description": "OpenClaw plugin for Trakt.tv - track movies and TV shows",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -15,9 +15,10 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "src/",
18
+ "dist/",
19
+ "openclaw.plugin.json",
19
20
  "skills/",
20
- "openclaw.plugin.json"
21
+ "src/"
21
22
  ],
22
23
  "keywords": [
23
24
  "openclaw",
@@ -49,10 +50,13 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^25.5.0",
52
- "typescript": "^5.9.3"
53
+ "typescript": "^5.9.3",
54
+ "tsup": "^8.5.0"
53
55
  },
54
56
  "scripts": {
55
57
  "plugin:check": "npx --yes @openclaw/plugin-inspector inspect --no-openclaw",
56
- "plugin:ci": "npx --yes @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
57
- }
58
+ "plugin:ci": "npx --yes @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute",
59
+ "build": "tsup src/index.ts --format esm --out-dir dist --dts --clean --target node20"
60
+ },
61
+ "main": "./dist/index.js"
58
62
  }