openclaw-trakt 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 ADDED
@@ -0,0 +1,42 @@
1
+ # openclaw-trakt
2
+
3
+ OpenClaw plugin for [Trakt.tv](https://trakt.tv). Track movies and TV shows, view watch history, manage your watchlist, and check show progress.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install openclaw-trakt
9
+ ```
10
+
11
+ ## Prerequisites
12
+
13
+ The `trakt-cli` Go binary must be installed and on your PATH:
14
+
15
+ ```bash
16
+ go install github.com/omarshahine/trakt-plugin@latest
17
+ ```
18
+
19
+ Or set `TRAKT_CLI_PATH` environment variable, or configure `cliPath` in plugin settings.
20
+
21
+ ## Configuration
22
+
23
+ | Setting | Description |
24
+ |---------|-------------|
25
+ | `cliPath` | Path to trakt-cli binary (auto-detected on PATH) |
26
+ | `clientId` | Trakt API client ID (from https://trakt.tv/oauth/applications) |
27
+ | `clientSecret` | Trakt API client secret |
28
+
29
+ ## Available Tools
30
+
31
+ | Tool | Description |
32
+ |------|-------------|
33
+ | `trakt_search` | Search for movies and TV shows |
34
+ | `trakt_history` | View watch history |
35
+ | `trakt_history_add` | Mark movies/shows as watched |
36
+ | `trakt_watchlist` | View watchlist |
37
+ | `trakt_progress` | Show watch progress for TV shows |
38
+ | `trakt_auth` | Set up Trakt.tv authentication |
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "trakt",
3
+ "name": "Trakt",
4
+ "description": "Track movies and TV shows using trakt.tv",
5
+ "version": "1.0.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "cliPath": {
10
+ "type": "string",
11
+ "description": "Path to trakt-cli binary",
12
+ "default": "trakt-cli"
13
+ },
14
+ "clientId": {
15
+ "type": "string",
16
+ "description": "Trakt API client ID (from https://trakt.tv/oauth/applications)"
17
+ },
18
+ "clientSecret": {
19
+ "type": "string",
20
+ "description": "Trakt API client secret"
21
+ }
22
+ }
23
+ },
24
+ "uiHints": {
25
+ "cliPath": {
26
+ "label": "CLI Path",
27
+ "help": "Path or command name for the trakt-cli binary. Defaults to finding it on PATH.",
28
+ "placeholder": "trakt-cli"
29
+ },
30
+ "clientId": {
31
+ "label": "Client ID",
32
+ "help": "Trakt API client ID from https://trakt.tv/oauth/applications",
33
+ "sensitive": true
34
+ },
35
+ "clientSecret": {
36
+ "label": "Client Secret",
37
+ "help": "Trakt API client secret from https://trakt.tv/oauth/applications",
38
+ "sensitive": true
39
+ }
40
+ }
41
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "openclaw-trakt",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw plugin for Trakt.tv - track movies and TV shows",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./src/index.ts"
9
+ ]
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "skills/",
14
+ "openclaw.plugin.json"
15
+ ],
16
+ "keywords": [
17
+ "openclaw",
18
+ "openclaw-plugin",
19
+ "trakt",
20
+ "movies",
21
+ "tv-shows",
22
+ "watchlist"
23
+ ],
24
+ "author": {
25
+ "name": "Omar Shahine",
26
+ "email": "omar@shahine.com"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/omarshahine/trakt-plugin.git"
32
+ },
33
+ "homepage": "https://github.com/omarshahine/trakt-plugin#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/omarshahine/trakt-plugin/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.5.0",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: trakt
3
+ description: |
4
+ Search movies/shows, view watch history, check watchlist, track progress, and mark items as watched on Trakt.tv.
5
+ Use when the user asks what they've been watching, what's on their watchlist, what's in progress,
6
+ wants to find a movie or show, mark something as watched, or asks about their Trakt activity.
7
+ license: MIT
8
+ metadata:
9
+ author: Omar Shahine
10
+ version: 2.0.0
11
+ openclaw:
12
+ requires:
13
+ bins: [trakt-cli]
14
+ ---
15
+
16
+ # Trakt Skill
17
+
18
+ View watch history, watchlist, progress, search, and mark items as watched on Trakt.tv.
19
+
20
+ All commands support `--json` for machine-readable output. **Always use `--json` for data processing.**
21
+
22
+ ## Commands
23
+
24
+ ### Progress (In-Progress Shows)
25
+
26
+ Shows which watchlist shows are started but not finished, not started, or completed.
27
+
28
+ ```bash
29
+ trakt-cli progress --json
30
+ trakt-cli progress --all --json
31
+ ```
32
+
33
+ - Default: shows only in-progress items + summary counts
34
+ - `--all`: includes not_started and completed lists
35
+ - JSON output: `{ "in_progress": [...], "summary": { "in_progress": N, "not_started": N, "completed": N } }`
36
+ - Each item: `{ "title", "year", "trakt_id", "aired", "watched", "remaining", "percent", "status", "next_episode" }`
37
+
38
+ ### Watchlist
39
+
40
+ ```bash
41
+ trakt-cli watchlist --json
42
+ trakt-cli watchlist --type shows --limit 100 --json
43
+ trakt-cli watchlist --type movies --json
44
+ ```
45
+
46
+ - JSON output: `{ "items": [{ "type", "title", "year", "trakt_id", "added_at" }], "page", "page_count", "item_count" }`
47
+ - `--type`: filter by `movies` or `shows`
48
+ - `--limit`: items per page (default 10)
49
+ - `--page`: page number
50
+
51
+ ### Watch History
52
+
53
+ ```bash
54
+ trakt-cli history --json
55
+ trakt-cli history --type shows --limit 20 --json
56
+ trakt-cli history --type movies --json
57
+ ```
58
+
59
+ - JSON output: `{ "items": [{ "type", "title", "year", "watched_at", "season", "episode", "show_title" }], ... }`
60
+ - Episodes include `show_title`, `season`, `episode` fields
61
+
62
+ ### Mark as Watched
63
+
64
+ ```bash
65
+ trakt-cli history add "Pluribus" --json
66
+ trakt-cli history add "The Sopranos" "The Wire" --json
67
+ trakt-cli history add --type movie "The Godfather" --json
68
+ trakt-cli history add --watched-at 2025-06-15 "Dark" --json
69
+ ```
70
+
71
+ - Searches by name, prefers exact title matches
72
+ - `--type show` (default) or `--type movie`
73
+ - `--watched-at`: RFC3339 or YYYY-MM-DD (defaults to now)
74
+ - Accepts multiple titles in one call
75
+ - JSON output: `{ "added_episodes": N, "added_movies": N, "not_found_movies": N, "not_found_shows": N }`
76
+
77
+ ### Search
78
+
79
+ ```bash
80
+ trakt-cli search "Shogun" --json
81
+ trakt-cli search "Inception" --type movie --json
82
+ ```
83
+
84
+ - JSON output: `{ "items": [{ "type", "title", "year", "trakt_id", "imdb", "score" }] }`
85
+ - `--type`: `movie`, `show`, or `movie,show` (default)
86
+
87
+ ## Notes
88
+
89
+ - Always use `--json` flag — raw table output is for human use only
90
+ - No shell constructs (pipes, redirects, chaining)
91
+ - Auth stored in `~/.trakt.yaml` (OAuth device flow)
92
+
93
+ ## Changelog
94
+
95
+ - **v2.0.0** — Add `progress` command, `--json` flag for all commands (agent-friendly output)
96
+ - **v1.1.0** — Add `watchlist` command, `--type` filter for `history`, `history add` with `--watched-at`
97
+ - **v1.0.0** — Initial skill (upstream `history` and `search` only)
package/src/index.ts ADDED
@@ -0,0 +1,413 @@
1
+ /**
2
+ * OpenClaw plugin entry for trakt-cli.
3
+ *
4
+ * Registers tool factories that shell out to the `trakt-cli` binary.
5
+ * Each tool maps to a CLI subcommand (search, history, watchlist, progress).
6
+ * Uses the factory pattern so each agent gets per-workspace config resolution.
7
+ */
8
+
9
+ import { execFileSync, execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { existsSync } from 'fs';
12
+ import { homedir } from 'os';
13
+ import { join } from 'path';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ // OpenClaw plugin config (from openclaw.plugin.json configSchema)
18
+ interface PluginConfig {
19
+ cliPath?: string;
20
+ clientId?: string;
21
+ clientSecret?: string;
22
+ }
23
+
24
+ // OpenClaw tool result content block
25
+ interface TextContent {
26
+ type: 'text';
27
+ text: string;
28
+ }
29
+
30
+ // OpenClaw tool definition
31
+ interface OpenClawToolDefinition {
32
+ name: string;
33
+ label: string;
34
+ description: string;
35
+ parameters: Record<string, unknown>;
36
+ execute: (
37
+ toolCallId: string,
38
+ params: Record<string, unknown>,
39
+ signal?: AbortSignal,
40
+ onUpdate?: (partialResult: unknown) => void
41
+ ) => Promise<{ content: TextContent[] }>;
42
+ }
43
+
44
+ // Context provided to factory functions
45
+ interface OpenClawPluginToolContext {
46
+ config?: Record<string, unknown>;
47
+ workspaceDir?: string;
48
+ agentDir?: string;
49
+ }
50
+
51
+ // Factory function type
52
+ type OpenClawPluginToolFactory = (
53
+ ctx: OpenClawPluginToolContext
54
+ ) => OpenClawToolDefinition | OpenClawToolDefinition[] | null | undefined;
55
+
56
+ // OpenClaw plugin registration interface
57
+ interface OpenClawContext {
58
+ config?: PluginConfig;
59
+ registerTool(toolOrFactory: OpenClawToolDefinition | OpenClawPluginToolFactory): void;
60
+ }
61
+
62
+ // Tool definitions — each maps to a CLI subcommand
63
+ const TOOLS: Array<{
64
+ name: string;
65
+ command: string;
66
+ subcommand?: string;
67
+ description: string;
68
+ parameters: Record<string, unknown>;
69
+ }> = [
70
+ {
71
+ name: 'trakt_search',
72
+ command: 'search',
73
+ description:
74
+ 'Search Trakt.tv for movies and TV shows. Returns title, year, trakt_id, imdb, and relevance score.',
75
+ parameters: {
76
+ type: 'object',
77
+ properties: {
78
+ query: {
79
+ type: 'string',
80
+ description: 'Search query (movie or show title)'
81
+ },
82
+ type: {
83
+ type: 'string',
84
+ enum: ['movie', 'show', 'movie,show'],
85
+ description: 'Filter by type (default: movie,show)'
86
+ }
87
+ },
88
+ required: ['query']
89
+ }
90
+ },
91
+ {
92
+ name: 'trakt_history',
93
+ command: 'history',
94
+ description:
95
+ 'View Trakt.tv watch history. Returns recently watched movies and episodes with timestamps.',
96
+ parameters: {
97
+ type: 'object',
98
+ properties: {
99
+ type: {
100
+ type: 'string',
101
+ enum: ['movies', 'shows'],
102
+ description: 'Filter by type'
103
+ },
104
+ limit: {
105
+ type: 'number',
106
+ description: 'Items per page (default 10)'
107
+ },
108
+ page: {
109
+ type: 'number',
110
+ description: 'Page number'
111
+ }
112
+ }
113
+ }
114
+ },
115
+ {
116
+ name: 'trakt_history_add',
117
+ command: 'history',
118
+ subcommand: 'add',
119
+ description:
120
+ 'Mark movies or shows as watched on Trakt.tv. Searches by title and adds to history. Accepts multiple titles.',
121
+ parameters: {
122
+ type: 'object',
123
+ properties: {
124
+ titles: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description: 'Title(s) to mark as watched'
128
+ },
129
+ type: {
130
+ type: 'string',
131
+ enum: ['movie', 'show'],
132
+ description: 'Content type (default: show)'
133
+ },
134
+ watched_at: {
135
+ type: 'string',
136
+ description: 'When watched (YYYY-MM-DD or RFC3339, defaults to now)'
137
+ }
138
+ },
139
+ required: ['titles']
140
+ }
141
+ },
142
+ {
143
+ name: 'trakt_watchlist',
144
+ command: 'watchlist',
145
+ description:
146
+ 'View Trakt.tv watchlist. Returns items the user wants to watch, with type, title, year, and added date.',
147
+ parameters: {
148
+ type: 'object',
149
+ properties: {
150
+ type: {
151
+ type: 'string',
152
+ enum: ['movies', 'shows'],
153
+ description: 'Filter by type'
154
+ },
155
+ limit: {
156
+ type: 'number',
157
+ description: 'Items per page (default 10)'
158
+ },
159
+ page: {
160
+ type: 'number',
161
+ description: 'Page number'
162
+ }
163
+ }
164
+ }
165
+ },
166
+ {
167
+ name: 'trakt_progress',
168
+ command: 'progress',
169
+ description:
170
+ 'Show progress of watchlist TV shows. Returns in-progress shows with episode counts, percentage, and next episode to watch.',
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ all: {
175
+ type: 'boolean',
176
+ description: 'Include not_started and completed shows (default: in-progress only)'
177
+ }
178
+ }
179
+ }
180
+ },
181
+ {
182
+ name: 'trakt_auth',
183
+ command: 'auth',
184
+ description:
185
+ 'Set up Trakt.tv authentication. Initiates OAuth device flow using configured client credentials. Only needed for initial setup.',
186
+ parameters: {
187
+ type: 'object',
188
+ properties: {}
189
+ }
190
+ }
191
+ ];
192
+
193
+ /**
194
+ * Look up a binary on PATH, cross-platform.
195
+ * Uses `which` on Unix and `where.exe` on Windows.
196
+ */
197
+ function whichBinary(name: string): string | null {
198
+ const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
199
+ try {
200
+ const result = execFileSync(cmd, [name], { encoding: 'utf8' }).trim();
201
+ // `where.exe` can return multiple lines; take the first
202
+ const first = result.split('\n')[0]?.trim();
203
+ return first || null;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Resolve the CLI binary path using a discovery chain:
211
+ * 1. Plugin config cliPath
212
+ * 2. Env var TRAKT_CLI_PATH
213
+ * 3. PATH lookup (try both `trakt-cli` and `trakt-plugin`)
214
+ * 4. Error with helpful message
215
+ */
216
+ function resolveCliPath(config?: PluginConfig): string {
217
+ // 1. Plugin config
218
+ if (config?.cliPath && existsSync(config.cliPath)) {
219
+ return config.cliPath;
220
+ }
221
+
222
+ // 2. Env var
223
+ const envPath = process.env.TRAKT_CLI_PATH;
224
+ if (envPath && existsSync(envPath)) {
225
+ return envPath;
226
+ }
227
+
228
+ // 3. PATH lookup — try both binary names
229
+ for (const name of ['trakt-cli', 'trakt-plugin']) {
230
+ const found = whichBinary(name);
231
+ if (found) return found;
232
+ }
233
+
234
+ throw new Error(
235
+ 'trakt-cli not found. Install with: go install github.com/omarshahine/trakt-plugin@latest\n' +
236
+ 'Or set TRAKT_CLI_PATH or configure cliPath in plugin settings.'
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Check if Trakt auth is configured (~/.trakt.yaml exists).
242
+ */
243
+ function isAuthConfigured(): boolean {
244
+ return existsSync(join(homedir(), '.trakt.yaml'));
245
+ }
246
+
247
+ /**
248
+ * Build CLI arguments from tool parameters.
249
+ * Always appends --json for machine-readable output.
250
+ */
251
+ function buildCliArgs(
252
+ command: string,
253
+ subcommand: string | undefined,
254
+ params: Record<string, unknown>,
255
+ config?: PluginConfig
256
+ ): string[] {
257
+ const args: string[] = [command];
258
+
259
+ // Handle auth command specially — uses config credentials
260
+ if (command === 'auth') {
261
+ if (config?.clientId) args.push('--client-id', config.clientId);
262
+ if (config?.clientSecret) args.push('--client-secret', config.clientSecret);
263
+ return args;
264
+ }
265
+
266
+ // Add subcommand if present (e.g., history add)
267
+ if (subcommand) args.push(subcommand);
268
+
269
+ // Handle search: positional query arg
270
+ if (command === 'search') {
271
+ if (params.query) args.push(String(params.query));
272
+ }
273
+
274
+ // Handle history add: positional titles
275
+ if (command === 'history' && subcommand === 'add') {
276
+ const titles = params.titles as string[] | undefined;
277
+ if (titles) {
278
+ // --type and --watched-at go before titles
279
+ if (params.type) args.push('--type', String(params.type));
280
+ if (params.watched_at) args.push('--watched-at', String(params.watched_at));
281
+ for (const title of titles) {
282
+ args.push(title);
283
+ }
284
+ args.push('--json');
285
+ return args;
286
+ }
287
+ }
288
+
289
+ // Map remaining params to CLI flags
290
+ const skipKeys = new Set(['query', 'titles']);
291
+ for (const [key, value] of Object.entries(params)) {
292
+ if (skipKeys.has(key) || value === undefined || value === null || value === false) continue;
293
+
294
+ const flag = `--${key.replace(/_/g, '-')}`;
295
+ if (typeof value === 'boolean') {
296
+ args.push(flag);
297
+ } else {
298
+ args.push(flag, String(value));
299
+ }
300
+ }
301
+
302
+ // Always append --json
303
+ args.push('--json');
304
+ return args;
305
+ }
306
+
307
+ /**
308
+ * OpenClaw plugin activation function.
309
+ * Called by the OpenClaw gateway when the plugin is loaded.
310
+ */
311
+ export default function activate(context: OpenClawContext): void {
312
+ const config = context.config;
313
+
314
+ let cliPath: string;
315
+
316
+ try {
317
+ cliPath = resolveCliPath(config);
318
+ } catch (error) {
319
+ // Defer error to tool execution time — plugin still loads
320
+ const errorMessage = error instanceof Error ? error.message : String(error);
321
+
322
+ for (const tool of TOOLS) {
323
+ context.registerTool(() => ({
324
+ name: tool.name,
325
+ label: tool.name,
326
+ description: tool.description,
327
+ parameters: tool.parameters,
328
+ async execute() {
329
+ return {
330
+ content: [
331
+ {
332
+ type: 'text' as const,
333
+ text: JSON.stringify({ success: false, error: errorMessage }, null, 2)
334
+ }
335
+ ]
336
+ };
337
+ }
338
+ }));
339
+ }
340
+ return;
341
+ }
342
+
343
+ for (const tool of TOOLS) {
344
+ context.registerTool((_ctx: OpenClawPluginToolContext) => ({
345
+ name: tool.name,
346
+ label: tool.name,
347
+ description: tool.description,
348
+ parameters: tool.parameters,
349
+
350
+ async execute(
351
+ _toolCallId: string,
352
+ params: Record<string, unknown>
353
+ ) {
354
+ // Check auth for non-auth tools
355
+ if (tool.command !== 'auth' && !isAuthConfigured()) {
356
+ return {
357
+ content: [
358
+ {
359
+ type: 'text' as const,
360
+ text: JSON.stringify({
361
+ success: false,
362
+ error: 'Trakt auth not configured. Run trakt_auth first, or manually: trakt-cli auth --client-id X --client-secret Y'
363
+ }, null, 2)
364
+ }
365
+ ]
366
+ };
367
+ }
368
+
369
+ try {
370
+ const args = buildCliArgs(tool.command, tool.subcommand, params, config);
371
+ const { stdout } = await execFileAsync(cliPath, args, {
372
+ encoding: 'utf8',
373
+ timeout: 30_000,
374
+ maxBuffer: 1024 * 1024 // 1MB
375
+ });
376
+
377
+ // Try to parse as JSON for structured output
378
+ let result: unknown;
379
+ try {
380
+ result = JSON.parse(stdout);
381
+ } catch {
382
+ result = { output: stdout.trim() };
383
+ }
384
+
385
+ return {
386
+ content: [
387
+ {
388
+ type: 'text' as const,
389
+ text: JSON.stringify(result, null, 2)
390
+ }
391
+ ]
392
+ };
393
+ } catch (error: unknown) {
394
+ const message = error instanceof Error ? error.message : String(error);
395
+ const stderr =
396
+ error && typeof error === 'object' && 'stderr' in error
397
+ ? String((error as { stderr: unknown }).stderr).trim()
398
+ : '';
399
+ const errorOutput = stderr ? `${message}\n\nstderr: ${stderr}` : message;
400
+
401
+ return {
402
+ content: [
403
+ {
404
+ type: 'text' as const,
405
+ text: JSON.stringify({ success: false, error: errorOutput }, null, 2)
406
+ }
407
+ ]
408
+ };
409
+ }
410
+ }
411
+ }));
412
+ }
413
+ }