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 +42 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +44 -0
- package/skills/trakt/SKILL.md +97 -0
- package/src/index.ts +413 -0
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
|
+
}
|