@ouro.bot/cli 0.1.0-alpha.819 → 0.1.0-alpha.820

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,12 @@
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.820",
6
+ "changes": [
7
+ "Teach the Sanctuary media catalog contract about media_search so taste and browse questions ground in its evidence instead of rejecting every terminal answer."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.819",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.819",
2
+ "runtimeVersion": "0.1.0-alpha.820",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -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.819</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.820</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanctuaryMediaCatalogRequiredToolCalls = sanctuaryMediaCatalogRequiredToolCalls;
4
4
  const runtime_1 = require("../nerves/runtime");
5
5
  const sanctuary_media_optimization_1 = require("./sanctuary-media-optimization");
6
- const REQUIRED_TOOL_NAMES = ["sanctuary_search_media_catalog"];
6
+ const LEGACY_CATALOG_TOOL = "sanctuary_search_media_catalog";
7
+ const MEDIA_SEARCH_TOOL = "media_search";
8
+ const MEDIA_REQUEST_TOOL = "media_request";
7
9
  const HOUSEHOLD_JARGON = /\b(?:bounded|catalog|inventory|endpoint|backend|data shape|pars(?:e|ed|ing)|json|jellyfin|unmanic|sanctuary_search_media_catalog)\b/iu;
8
10
  const NEGATED_MEDIA_ACCESS = /\b(?:not|never|none|nothing|zero|inaccessible|invisible|unavailable|broken|failed|failing)\b|\b(?:isn[’']t|won[’']t)\b|\bno\s+access\b|\b(?:lost|lacks?|lacking|without)\s+access\b|\bonly\s+\d|\bfewer\s+than\b|\bsome\s+of\b|\b(?:part|portion|subset)\s+of\b|\bi\s+(?:can(?:not|[’']t)|do\s+not|don[’']t|have\s+no)\b|\bi[’']m\s+not\b/iu;
9
11
  const UNSOLICITED_PIVOT = /\b(?:what would you like|what are you in the mood for|would you like me to|do you want me to|want me to|recommend we add)\b/iu;
@@ -84,6 +86,43 @@ function parseCatalogEvidence(result, args) {
84
86
  totalItems: parsed.data.totalItems,
85
87
  matchedItems: parsed.data.matchedItems,
86
88
  titles,
89
+ offShelfTitles: [],
90
+ fromMediaSearch: false,
91
+ };
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * media_search reports shelf state per title, so it can ground both "what do we own"
99
+ * and "what is worth adding" from one read. The legacy catalog read can only prove
100
+ * absence by returning zero matches for an exact title query.
101
+ */
102
+ function parseMediaSearchEvidence(result, args) {
103
+ try {
104
+ const parsed = JSON.parse(result);
105
+ if (!Number.isSafeInteger(parsed.matched) || !Array.isArray(parsed.items))
106
+ return null;
107
+ const titles = [];
108
+ const offShelfTitles = [];
109
+ for (const entry of parsed.items) {
110
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
111
+ continue;
112
+ const item = entry;
113
+ if (typeof item.title !== "string" || !item.title.trim())
114
+ continue;
115
+ titles.push(item.title.trim());
116
+ if (item.on_shelf === false)
117
+ offShelfTitles.push(item.title.trim());
118
+ }
119
+ return {
120
+ query: (0, sanctuary_media_optimization_1.normalizeSanctuaryMediaText)(args.query ?? ""),
121
+ totalItems: parsed.matched,
122
+ matchedItems: parsed.matched,
123
+ titles,
124
+ offShelfTitles,
125
+ fromMediaSearch: true,
87
126
  };
88
127
  }
89
128
  catch {
@@ -126,7 +165,11 @@ function commonAnswerRejection(answer, technicalDetailRequested) {
126
165
  return undefined;
127
166
  }
128
167
  function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
129
- if (!advertisedToolNames.includes("sanctuary_search_media_catalog"))
168
+ // Prefer media_search when the media surface is installed: it grounds both what is
169
+ // on the shelf and what is worth adding. Fall back to the legacy restricted read.
170
+ const catalogTool = advertisedToolNames.includes(MEDIA_SEARCH_TOOL) ? MEDIA_SEARCH_TOOL : LEGACY_CATALOG_TOOL;
171
+ const canRequestMedia = advertisedToolNames.includes(MEDIA_REQUEST_TOOL);
172
+ if (!advertisedToolNames.includes(catalogTool))
130
173
  return undefined;
131
174
  const normalized = (0, sanctuary_media_optimization_1.normalizeSanctuaryMediaText)(request);
132
175
  const mentionsMedia = /\b(?:film|films|movie|movies|shows|tv|jellyfin|watch|shelf|stock|catalog|library|lib)\b/u.test(normalized);
@@ -143,7 +186,7 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
143
186
  const listCount = requestedListCount(normalized);
144
187
  const technicalDetailRequested = /\b(?:technical|technically|implementation|endpoint|backend|debug|detail|internals?)\b/u.test(normalized);
145
188
  let latestEvidence;
146
- const names = [...REQUIRED_TOOL_NAMES];
189
+ const names = [catalogTool];
147
190
  (0, runtime_1.emitNervesEvent)({
148
191
  component: "senses",
149
192
  event: "senses.sanctuary_media_catalog_obligation",
@@ -154,36 +197,38 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
154
197
  names,
155
198
  requireSuccessfulResults: true,
156
199
  retryMessage: asksForCount
157
- ? "Use sanctuary_search_media_catalog before answering, then report only the current shelf count in ordinary household language."
200
+ ? `Use ${catalogTool} before answering, then report only the current shelf count in ordinary household language.`
158
201
  : kind === "visibility"
159
- ? "Use sanctuary_search_media_catalog before answering. Lead with the direct answer from current catalog evidence, report the shelf count in ordinary household language, and stop without sampled titles or a follow-up question."
202
+ ? `Use ${catalogTool} before answering. Lead with the direct answer from current catalog evidence, report the shelf count in ordinary household language, and stop without sampled titles or a follow-up question.`
160
203
  : kind === "title_lookup"
161
- ? "Use sanctuary_search_media_catalog with the requested title before answering. Confirm its presence or absence directly from current catalog evidence."
204
+ ? `Use ${catalogTool} with the requested title before answering. Confirm its presence or absence directly from current catalog evidence.`
162
205
  : kind === "taste_recommendation"
163
- ? "Use sanctuary_search_media_catalog before answering. Make one concise, decisive choice grounded in returned catalog evidence; do not volunteer an AI or 'I cannot watch' disclaimer."
206
+ ? `Use ${catalogTool} before answering. Make one concise, decisive choice grounded in returned catalog evidence; do not volunteer an AI or 'I cannot watch' disclaimer.`
164
207
  : listCount
165
- ? `Use sanctuary_search_media_catalog with limit ${listCount} before answering, then name exactly the returned catalog titles.`
166
- : "Use sanctuary_search_media_catalog before answering and base any named titles on the returned catalog evidence.",
208
+ ? `Use ${catalogTool} with limit ${listCount} before answering, then name exactly the returned catalog titles.`
209
+ : `Use ${catalogTool} before answering and base any named titles on the returned catalog evidence.`,
167
210
  validateRequiredToolResult(name, result, args) {
168
- if (name !== "sanctuary_search_media_catalog")
211
+ if (name !== catalogTool)
169
212
  return false;
170
- const current = parseCatalogEvidence(result, args);
213
+ const current = catalogTool === MEDIA_SEARCH_TOOL ? parseMediaSearchEvidence(result, args) : parseCatalogEvidence(result, args);
171
214
  if (!current)
172
215
  return false;
173
216
  latestEvidence = current;
174
217
  return true;
175
218
  },
176
219
  validateToolCallBeforeDispatch(name, args) {
177
- if (name !== "sanctuary_search_media_catalog")
220
+ if (name !== catalogTool)
178
221
  return undefined;
179
222
  const query = (0, sanctuary_media_optimization_1.normalizeSanctuaryMediaText)(args.query ?? "");
180
223
  if (kind === "title_lookup" && !query)
181
224
  return "Search for the requested title by name before answering whether it is on the shelf.";
182
225
  if (kind === "title_lookup" && query !== requestedTitleQuery)
183
226
  return `Search for the requested title ${requestedTitleQuery} before answering whether it is on the shelf.`;
184
- if (asksForAddition && !query)
227
+ // media_search browses by genre, keyword and year and reports shelf state per
228
+ // title, so an addition question does not need a per-candidate title query.
229
+ if (asksForAddition && !query && catalogTool !== MEDIA_SEARCH_TOOL)
185
230
  return "Search for one candidate title by name before recommending it as an addition.";
186
- if (listCount && Number(args.limit) !== listCount)
231
+ if (listCount && Number(args.limit) !== listCount && catalogTool !== MEDIA_SEARCH_TOOL)
187
232
  return `Set the catalog limit to ${listCount} so the answer is grounded in exactly the requested number of titles.`;
188
233
  return undefined;
189
234
  },
@@ -221,17 +266,31 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
221
266
  }
222
267
  if (kind === "taste_recommendation" && latest) {
223
268
  if (asksForAddition) {
224
- if (!latest.query || latest.matchedItems !== 0 || !includesNormalizedPhrase(answer, latest.query))
269
+ if (latest.fromMediaSearch) {
270
+ // media_search states shelf membership per title, so a recommendation is
271
+ // grounded when the named candidate came back marked absent.
272
+ if (!latest.offShelfTitles.some((title) => includesNormalizedPhrase(answer, title))) {
273
+ return "Recommend only a title the current search returned as not on the shelf.";
274
+ }
275
+ }
276
+ else if (!latest.query || latest.matchedItems !== 0 || !includesNormalizedPhrase(answer, latest.query)) {
225
277
  return "Recommend an addition only after an exact current catalog search shows that candidate is absent.";
226
- if (/\b(?:i|we)\s+(?:added|requested|submitted|queued)\b/iu.test(answer))
278
+ }
279
+ if (!canRequestMedia && /\b(?:i|we)\s+(?:added|requested|submitted|queued)\b/iu.test(answer)) {
227
280
  return "Do not claim the title was added or requested when no media-request action was available.";
281
+ }
228
282
  }
229
283
  else if (!latest.titles.some((title) => includesNormalizedPhrase(answer, title))) {
230
284
  return "Make the choice from a title returned by the current catalog evidence.";
231
285
  }
232
286
  }
233
- if (kind === "other" && latest)
234
- return catalogTitleGroundingRejection(answer, latest, listCount);
287
+ if (kind === "other" && latest) {
288
+ if (!latest.fromMediaSearch)
289
+ return catalogTitleGroundingRejection(answer, latest, listCount);
290
+ if (latest.titles.length > 0 && !latest.titles.some((title) => includesNormalizedPhrase(answer, title))) {
291
+ return "Name titles the current search returned; do not add unverified ones.";
292
+ }
293
+ }
235
294
  return undefined;
236
295
  },
237
296
  };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.819",
3
+ "version": "0.1.0-alpha.820",
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.819",
9
+ "version": "0.1.0-alpha.820",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.819",
3
+ "version": "0.1.0-alpha.820",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },