@ouro.bot/cli 0.1.0-alpha.851 → 0.1.0-alpha.853
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/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.853",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The Butler's override grab sends the release's quality and languages, which Sonarr v4 requires (it returned HTTP 500 without them)"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.852",
|
|
12
|
+
"changes": [
|
|
13
|
+
"An orientation hold no longer blocks read-only tools from the agent's own MCP servers (readOnlyHint); the Butler's media lookups are marked read-only. The media tools treat a 200 with an empty body as success, so clearing a stuck download goes on to re-search"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.851",
|
|
6
18
|
"changes": [
|
|
@@ -63,7 +63,9 @@ async function req(base, path, { key, header = "X-Api-Key", method = "GET", body
|
|
|
63
63
|
throw new ServiceError(`${serviceOf(base)}_http_${res.status}`, `${serviceOf(base)} returned ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`)
|
|
64
64
|
}
|
|
65
65
|
if (res.status === 204) return null
|
|
66
|
-
|
|
66
|
+
// Sonarr/Radarr answer some DELETEs with 200 and an empty body; that is success, not a parse failure.
|
|
67
|
+
const raw = await res.text()
|
|
68
|
+
return raw.trim() ? JSON.parse(raw) : null
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function serviceOf(base) {
|
|
@@ -1010,7 +1012,10 @@ export async function mediaReleaseSearch(a) {
|
|
|
1010
1012
|
raw = await sonarr("/release", { query: { seriesId: serviceId, seasonNumber: a.season_number }, timeoutMs: SEARCH_TIMEOUT_MS })
|
|
1011
1013
|
} else return { result: "season_number_or_episode_ids_required" }
|
|
1012
1014
|
const seen = new Set()
|
|
1013
|
-
|
|
1015
|
+
const unique = (Array.isArray(raw) ? raw : []).filter((r) => (seen.has(r.guid) ? false : seen.add(r.guid)))
|
|
1016
|
+
// Sonarr's override grab needs the release's own quality and languages objects back.
|
|
1017
|
+
const rawByGuid = new Map(unique.map((r) => [r.guid, r]))
|
|
1018
|
+
let rows = unique.map((r) => releaseRow(a.kind, r))
|
|
1014
1019
|
const total = rows.length
|
|
1015
1020
|
if (a.query) {
|
|
1016
1021
|
const q = String(a.query).toLowerCase()
|
|
@@ -1018,7 +1023,11 @@ export async function mediaReleaseSearch(a) {
|
|
|
1018
1023
|
}
|
|
1019
1024
|
rows.sort((x, y) => Number(y.seeders ?? 0) - Number(x.seeders ?? 0))
|
|
1020
1025
|
const limit = Math.min(Math.max(Number(a.limit) || 40, 1), 100)
|
|
1021
|
-
for (const r of rows)
|
|
1026
|
+
for (const r of rows) {
|
|
1027
|
+
const source = rawByGuid.get(r.guid)
|
|
1028
|
+
cacheRelease(`${a.kind}:${r.guid}`, { ...r, service_id: serviceId, season_number: a.season_number ?? null,
|
|
1029
|
+
quality_raw: source?.quality ?? null, languages_raw: source?.languages ?? null })
|
|
1030
|
+
}
|
|
1022
1031
|
return { kind: a.kind, service_id: serviceId, total_found: total, matched: rows.length, releases: rows.slice(0, limit) }
|
|
1023
1032
|
}
|
|
1024
1033
|
|
|
@@ -1047,7 +1056,10 @@ export async function mediaReleaseGrab(a) {
|
|
|
1047
1056
|
const own = new Set((await seriesEpisodes(seriesId)).map((e) => e.id))
|
|
1048
1057
|
const foreign = a.episode_ids.filter((id) => !own.has(id))
|
|
1049
1058
|
if (foreign.length) return { result: "episode_id_not_for_series", foreign_episode_ids: foreign }
|
|
1050
|
-
|
|
1059
|
+
// Sonarr v4 refuses an override without quality and languages (ArgumentNullException, HTTP 500).
|
|
1060
|
+
if (!cached.quality_raw) return { result: "release_quality_unknown", message: "Sonarr's search did not report this release's quality; search again." }
|
|
1061
|
+
Object.assign(body, { seriesId, episodeIds: a.episode_ids.map(Number), shouldOverride: true,
|
|
1062
|
+
quality: cached.quality_raw, languages: Array.isArray(cached.languages_raw) ? cached.languages_raw : [] })
|
|
1051
1063
|
if (a.download_client_id !== undefined && a.download_client_id !== null) body.downloadClientId = Number(a.download_client_id)
|
|
1052
1064
|
}
|
|
1053
1065
|
}
|
|
@@ -1135,9 +1147,10 @@ export async function mediaManualImport(a) {
|
|
|
1135
1147
|
|
|
1136
1148
|
// ------------------------------------------------------------------- schema
|
|
1137
1149
|
|
|
1138
|
-
const TOOLS = [
|
|
1150
|
+
export const TOOLS = [
|
|
1139
1151
|
{
|
|
1140
1152
|
name: "media_search",
|
|
1153
|
+
annotations: { readOnlyHint: true },
|
|
1141
1154
|
description: "Search or browse the household media catalogue. Use `query` for a title lookup: partial and misspelled titles are matched against the library first (Sonarr, Radarr, Jellyfin) and returned as `library_matches` with their kind and service id, and when nothing in the library matches, `lookup_candidates` (TVDB/TMDB) cover titles not in the library. Never say a title does not exist without reading both; use `genres`/`keywords`/`year_range` to browse by vibe (e.g. keywords ['autumn','thanksgiving'] for cozy fall films). `on_shelf: true` restricts to what is already downloaded, `false` to what is not, omit for both. Every item says whether it is on the shelf and carries its tmdb_id so a follow-up media_request needs no second lookup.",
|
|
1142
1155
|
inputSchema: { type: "object", properties: {
|
|
1143
1156
|
query: { type: "string", description: "Title or free text. Omit to browse by filters." },
|
|
@@ -1163,6 +1176,7 @@ const TOOLS = [
|
|
|
1163
1176
|
},
|
|
1164
1177
|
{
|
|
1165
1178
|
name: "media_request_status",
|
|
1179
|
+
annotations: { readOnlyHint: true },
|
|
1166
1180
|
description: "One call, every stage: request, indexer search, download, file on disk, plus acquisition-chain health and a deterministic `diagnosis` block naming what is stuck, why, and the fix. Use this for any 'is it here yet?' or 'why hasn't X downloaded?' question. A queue row is not proof of progress: a release sitting at zero bytes comes back as stalled, not as downloading. Never infer the cause yourself — read diagnosis.summary.",
|
|
1167
1181
|
inputSchema: { type: "object", properties: {
|
|
1168
1182
|
request_id: { type: "string", description: "e.g. jellyseerr:42" },
|
|
@@ -1181,6 +1195,7 @@ const TOOLS = [
|
|
|
1181
1195
|
},
|
|
1182
1196
|
{
|
|
1183
1197
|
name: "media_chain_health",
|
|
1198
|
+
annotations: { readOnlyHint: true },
|
|
1184
1199
|
description: "Health of the whole acquisition chain: Prowlarr indexers, their failure state, and whether Prowlarr is syncing to Sonarr/Radarr. Call this when several things are stuck at once, or before telling anyone a specific title is the problem — a dead chain blocks everything and is the single most likely cause.",
|
|
1185
1200
|
inputSchema: { type: "object", properties: {} },
|
|
1186
1201
|
},
|
|
@@ -1213,6 +1228,7 @@ const TOOLS = [
|
|
|
1213
1228
|
},
|
|
1214
1229
|
{
|
|
1215
1230
|
name: "media_episodes",
|
|
1231
|
+
annotations: { readOnlyHint: true },
|
|
1216
1232
|
description: "List a Sonarr series' episodes (optionally one season): episode_id, SxxEyy code, scene_code (the numbering release names use, when Sonarr has a mapping), title, air date, has_file. Use it to see what the TVDB numbering calls each episode before searching for releases by hand.",
|
|
1217
1233
|
inputSchema: { type: "object", properties: {
|
|
1218
1234
|
service_id: { type: "number", description: "Sonarr series id." },
|
|
@@ -1221,6 +1237,7 @@ const TOOLS = [
|
|
|
1221
1237
|
},
|
|
1222
1238
|
{
|
|
1223
1239
|
name: "media_indexer_search",
|
|
1240
|
+
annotations: { readOnlyHint: true },
|
|
1224
1241
|
description: "Free-text search across every indexer (Prowlarr), sorted by seeders. Discovery only, it grabs nothing. Use it when Sonarr/Radarr's own search finds nothing: try the scene code from media_episodes, 'Season N', the year, or an episode title, to learn whether any release exists and how it is named. Zero seeders means it cannot download right now.",
|
|
1225
1242
|
inputSchema: { type: "object", properties: {
|
|
1226
1243
|
query: { type: "string" },
|
|
@@ -1229,6 +1246,7 @@ const TOOLS = [
|
|
|
1229
1246
|
},
|
|
1230
1247
|
{
|
|
1231
1248
|
name: "media_release_search",
|
|
1249
|
+
annotations: { readOnlyHint: true },
|
|
1232
1250
|
description: "Interactive release search for a series or movie already in the library: what the indexers offer and how Sonarr/Radarr parsed each release (full_season, parsed_season, parsed_episodes, mapped_episodes), whether it is rejected and why, and whether it is blocklisted. Series: give season_number or episode_ids. Can take a minute or more. `query` filters returned titles. Use it when the automatic search found nothing, then grab with media_release_grab.",
|
|
1233
1251
|
inputSchema: { type: "object", properties: {
|
|
1234
1252
|
kind: { type: "string", enum: ["series", "movie"] },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.853</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -67,11 +67,15 @@ function mcpToolsAsDefinitions(view) {
|
|
|
67
67
|
});
|
|
68
68
|
return {
|
|
69
69
|
tool: schema,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
reason: "
|
|
74
|
-
|
|
70
|
+
// A read-only hint is trusted only from the agent's own bundle (agent.json mcpServers),
|
|
71
|
+
// so an orientation hold still lets the agent inspect through those tools.
|
|
72
|
+
riskProfile: entry.source === "builtin" && tool.annotations?.readOnlyHint === true
|
|
73
|
+
? { mutates: "none", risk: "low", reason: "read-only tool from the agent's own MCP server" }
|
|
74
|
+
: {
|
|
75
|
+
mutates: "external_side_effect",
|
|
76
|
+
risk: "high",
|
|
77
|
+
reason: "MCP tools may mutate external systems",
|
|
78
|
+
},
|
|
75
79
|
handler: async (args, ctx) => {
|
|
76
80
|
if (ctx?.agentName !== binding.agentName || ctx.agentRoot !== binding.agentRoot) {
|
|
77
81
|
(0, runtime_1.emitNervesEvent)({
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.853",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.853",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|