Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extensions/trakt-manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Trakt Manager Changelog

## [Update] - 2026-09-23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The latest change replaced {PR_MERGE_DATE} with a fixed date. Changelog titles must keep {PR_MERGE_DATE} so release tooling can set the real merge date. Restore the placeholder before merging.

Suggested change
## [Update] - 2026-09-23
## [Update] - {PR_MERGE_DATE}

Rule Used: What: Changelog entries must use {PR_MERGE_DATE} placeholder in titles, be placed at the top of the file, and maintain descending version order. Why: Standardized placeholders and consistent ordering ensure changelogs are maintainable and merge da... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/trakt-manager/CHANGELOG.md
Line: 3

Comment:
The latest change replaced `{PR_MERGE_DATE}` with a fixed date. Changelog titles must keep `{PR_MERGE_DATE}` so release tooling can set the real merge date. Restore the placeholder before merging.

```suggestion
## [Update] - {PR_MERGE_DATE}
```

**Rule Used:** What: Changelog entries must use `{PR_MERGE_DATE}` placeholder in titles, be placed at the top of the file, and maintain descending version order.  Why: Standardized placeholders and consistent ordering ensure changelogs are maintainable and merge da... ([source](https://app.greptile.com/raycast/-/custom-context?memory=c2214c11-df56-490a-b1c0-09a385df481a))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


### Added

- **AI tools for personal lists** — Raycast AI can now read your Trakt lists, check whether a title is on one, create, rename or delete a list, and add or remove movies, shows, seasons and episodes. Building a themed list takes a single batched call, and every change asks for confirmation naming the list and the items as Trakt holds them.
- List names keep their emoji and non-Latin characters when matched, so "🎬 Oscars 2026" and "Oscars 2026" stay distinct lists.

## [Update] - 2026-09-19

### Added
Expand Down
3 changes: 2 additions & 1 deletion extensions/trakt-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ Raycast AI can act on your Trakt account using the same sign-in as the commands
- _"What episode am I on in Breaking Bad?"_
- _"Rate The Bear 9 out of 10"_
- _"What should I watch next?"_
- _"Create a list called Emmy 2025 winners and add these shows to it"_

Anything that changes your account — marking as watched, rating, or editing the watchlist — asks for confirmation first, and shows exactly which title it resolved so it cannot act on the wrong one.
Anything that changes your account — marking as watched, rating, editing the watchlist or your lists — asks for confirmation first, and shows exactly which title it resolved so it cannot act on the wrong one.

When several titles share a name, mention the year (_"Butterfly 2025"_) to remove the ambiguity.

Expand Down
103 changes: 103 additions & 0 deletions extensions/trakt-manager/ai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ instructions: |
- Format season and episode numbers clearly as S{season}E{episode} (e.g. S01E03) alongside episode titles.
- When presenting ratings, format them out of 10 (e.g. 8.2/10).

Personal Lists:
- To build a themed list (award winners, a marathon, a top 10): `create-list`, then ONE `add-to-list` call carrying every item. Resolve each title with `search-movies` / `search-shows` first; the confirmation names every item from Trakt, so titles are never needed alongside the IDs.
- CRITICAL: `add-to-list` and `remove-from-list` take COMMA-SEPARATED values in a SINGLE call: `movieTraktIds: "329862,16662"`, `showTraktIds: "154784"`, `seasons: "154784:1"` (showTraktId:seasonNumber), `episodes: "154784:1:3"` (showTraktId:season:episode). Never call them once per title, and never pass a bare season or episode ID. At most 50 items per call. The confirmation lists every resolved title from Trakt before writing.
- Use `get-lists` to find a list's `listId`. `found` is true only for an exact list name; lists whose name merely contains the query stay in `lists` and must be confirmed with the user.
- To check whether a title is on a list (e.g. "Is Parasite on my Oscars list?"), call `get-lists` with the list (`listId` or `query`) and `itemQuery` (plus `itemYear` when known). Read `inList` and trust a negative ONLY when `exhaustive: true`. Never answer from a page of `items`. With `itemTraktId`, always pass `itemType`. For a specific season or episode, also pass `seasonNumber` (and `episodeNumber`), or put them in the query ("Severance season 2", "Severance S01E03") — season/episode Trakt IDs are not what the other tools return.
- `create-list` reuses a list only when the name matches after folding case and accents. A nearby name that differs by emoji or punctuation comes back `ambiguous: true` without writing: ask the user which list they mean. Lists are `private` unless the user asks otherwise; `link` means anyone with the share link.
- Emojis are allowed in list names (e.g. "🎬 Oscars 2026"). For ranked lists, pass `displayNumbers: true`.
- Renaming with `update-list` keeps the list's `listId` (Trakt keeps the slug). Use `delete-list` only when the user explicitly asks to delete the whole list; to take titles out, use `remove-from-list`.
- Read `added`, `alreadyPresent`, `notFound` and `removed` / `notOnList` and the tool `message`: HTTP success is not proof an item was stored or removed.
- Free Trakt accounts cap lists and list items; a quota error means the account limit was reached, not that the feature is missing.

Write Actions & Safety:
- Before performing any write action (watchlist, history, ratings), resolve the correct `traktId` first. `title` / `showTitle` on write tools is optional and ignored: the confirmation names the item Trakt holds for that ID.
- To add an item to the watchlist, use `add-to-watchlist`. Read `alreadyPresent` and the tool `message`: HTTP success is not a write when the title was already there, or when Trakt stored nothing.
Expand All @@ -41,6 +52,7 @@ instructions: |
- To remove a movie, TV show, or episode from watch history, use `remove-from-history`.
- To rate a movie, TV show, or episode, use `rate-media` with a whole number between 1 and 10. Decimal scores are rejected, so round before calling and tell the user which score you used. Seasons cannot be rated through these tools.
- To remove a rating from a movie, TV show, or episode, use `remove-rating`. Season ratings can appear in `get-ratings` but cannot be added or removed through these tools.
- To create, rename, delete or fill a personal list, use `create-list`, `update-list`, `delete-list`, `add-to-list` and `remove-from-list` (see Personal Lists).
- All write and destructive tools automatically prompt the user with a confirmation dialog before executing. The dialog names the item by looking its Trakt ID up on Trakt, so a wrong ID surfaces there rather than silently modifying the wrong item.

evals:
Expand Down Expand Up @@ -332,3 +344,94 @@ evals:
name: "search-shows"
arguments:
title: "Severance"

- input: "@trakt-manager what personal lists do I have on Trakt?"
mocks:
get-lists:
found: true
exhaustive: true
message: "Found 2 list(s)."
lists:
- traktId: 30481001
slug: "oscars-2026"
listId: "oscars-2026"
name: "🎬 Oscars 2026"
privacy: "private"
itemCount: 7
- traktId: 30481002
slug: "rewatch-pile"
listId: "rewatch-pile"
name: "Rewatch pile"
privacy: "private"
itemCount: 12
expected:
- callsTool:
name: "get-lists"

# Membership must be checked by the tool over every item, not read off a page of items.
- input: "@trakt-manager is Parasite on my Oscars 2026 list?"
mocks:
get-lists:
found: true
inList: true
exhaustive: true
message: '"Parasite" is on "🎬 Oscars 2026".'
lists:
- traktId: 30481001
slug: "oscars-2026"
listId: "oscars-2026"
name: "🎬 Oscars 2026"
privacy: "private"
itemCount: 7
matchedItems:
- listEntryId: 901
type: "movie"
title: "Parasite"
year: 2019
traktId: 329862
totalItems: 7
expected:
- callsTool:
name: "get-lists"
arguments:
itemQuery: "Parasite"

- input: "@trakt-manager create a list called 🎬 Oscars 2026 and add Parasite to it"
mocks:
search-movies:
data:
- traktId: 329862
title: "Parasite"
year: 2019
matchesForTitle: 1
truncated: false
hasMore: false
create-list:
success: true
alreadyExisted: false
message: 'Created the private list "🎬 Oscars 2026".'
listId: "oscars-2026"
list:
traktId: 30481001
slug: "oscars-2026"
listId: "oscars-2026"
name: "🎬 Oscars 2026"
privacy: "private"
itemCount: 0
add-to-list:
success: true
message: 'Added 1 item(s) to "🎬 Oscars 2026".'
added: 1
alreadyPresent: 0
notFound: 0
listItemCount: 1
expected:
- callsTool:
name: "create-list"
arguments:
name: "🎬 Oscars 2026"
- callsTool:
name: "add-to-list"
arguments:
listId: "oscars-2026"
movieTraktIds: "329862"
33 changes: 32 additions & 1 deletion extensions/trakt-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@
"name": "get-user-stats",
"title": "Get User Stats",
"description": "Get your lifetime Trakt viewing statistics including watch times, movies, shows, and ratings counts"
},
{
"name": "get-lists",
"title": "Get Personal Lists",
"description": "Get your Trakt personal lists, read a list's items, or check whether a title is on a list"
},
{
"name": "create-list",
"title": "Create Personal List",
"description": "Create a new personal list on your Trakt account, with optional privacy, numbering and sorting"
},
{
"name": "update-list",
"title": "Update Personal List",
"description": "Rename a Trakt personal list or change its description, privacy, numbering or sorting"
},
{
"name": "delete-list",
"title": "Delete Personal List",
"description": "Permanently delete a Trakt personal list and every item on it"
},
{
"name": "add-to-list",
"title": "Add to Personal List",
"description": "Add movies, TV shows, seasons and episodes to a Trakt personal list in a single batch"
},
{
"name": "remove-from-list",
"title": "Remove from Personal List",
"description": "Remove movies, TV shows, seasons and episodes from a Trakt personal list"
}
],
"contributors": [
Expand Down Expand Up @@ -198,7 +228,8 @@
"fmt": "prettier '**/*' -wu && syncpack-format",
"lint": "ray lint",
"publish": "ray publish",
"pull": "ray pull-contributions"
"pull": "ray pull-contributions",
"test": "npx --yes tsx --test src/tools/*.test.ts"
},
"title": "Trakt Manager"
}
5 changes: 4 additions & 1 deletion extensions/trakt-manager/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export const initTraktClient = () => {
body,
...fetchOptions,
});
const json = await response.json();
// DELETE answers 204 with an empty body; parsing it as JSON would turn a success into a 500.
// Any other unparseable body still throws and lands in the catch below, as before.
const text = await response.text();
const json: unknown = text ? JSON.parse(text) : undefined;

// Uncomment this line to log API requests
// console.log(
Expand Down
96 changes: 96 additions & 0 deletions extensions/trakt-manager/src/lib/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ import {
TraktUserStatsSchema,
TraktIdLookupQuerySchema,
TraktIdLookupSchema,
TraktListEntriesSchema,
TraktListItemsBodySchema,
TraktListSchema,
TraktListsSchema,
TraktListWriteBodySchema,
} from "./schema";

const TraktListPathSchema = z.object({
id: z.string().default("me"),
listId: z.string(),
});

const c = initContract();

const TraktMovieContract = c.router({
Expand Down Expand Up @@ -445,6 +455,92 @@ const TraktUserContract = c.router({
}),
summary: "Get user stats",
},
getLists: {
method: "GET",
path: "/users/:id/lists",
responses: {
200: TraktListsSchema,
},
pathParams: z.object({
id: z.string().default("me"),
}),
query: TraktPaginationSchema,
summary: "Get personal lists (paginated, limit clamped per endpoint)",
},
getList: {
method: "GET",
path: "/users/:id/lists/:listId",
responses: {
200: TraktListSchema,
404: z.unknown(),
},
pathParams: TraktListPathSchema,
summary: "Get one personal list",
},
createList: {
method: "POST",
path: "/users/:id/lists",
responses: {
201: z.unknown(),
},
pathParams: z.object({
id: z.string().default("me"),
}),
body: TraktListWriteBodySchema.extend({ name: z.string() }),
summary: "Create a personal list",
},
updateList: {
method: "PUT",
path: "/users/:id/lists/:listId",
responses: {
200: z.unknown(),
},
pathParams: TraktListPathSchema,
body: TraktListWriteBodySchema,
summary: "Update a personal list (the slug is kept when the name changes)",
},
deleteList: {
method: "DELETE",
path: "/users/:id/lists/:listId",
responses: {
204: z.unknown(),
},
pathParams: TraktListPathSchema,
body: c.noBody(),
summary: "Delete a personal list and every item on it",
},
getListItems: {
method: "GET",
path: "/users/:id/lists/:listId/items/:type",
responses: {
200: TraktListEntriesSchema,
},
pathParams: TraktListPathSchema.extend({
type: z.literal("movie,show,season,episode"),
}),
query: TraktPaginationSchema,
summary: "Get movie, show, season and episode items on a personal list",
},
addListItems: {
method: "POST",
path: "/users/:id/lists/:listId/items",
responses: {
201: z.unknown(),
},
pathParams: TraktListPathSchema,
body: TraktListItemsBodySchema,
summary: "Add items to a personal list",
},
removeListItems: {
method: "POST",
path: "/users/:id/lists/:listId/items/remove",
responses: {
200: z.unknown(),
},
pathParams: TraktListPathSchema,
body: TraktListItemsBodySchema,
summary: "Remove items from a personal list",
},
});

const TraktSearchContract = c.router({
Expand Down
Loading