@ouro.bot/cli 0.1.0-alpha.927 → 0.1.0-alpha.928

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,14 @@
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.928",
6
+ "changes": [
7
+ "Teams can send private local PNG and JPEG images into the current conversation, with bounded inline bytes, family trust, no-send enforcement and honest activity receipts.",
8
+ "Preserves the existing Teams integration schema order when adding inline image delivery.",
9
+ "Records private-image preparation and Teams acceptance in structured telemetry without logging image bytes, paths or captions."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.927",
6
14
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.927",
2
+ "runtimeVersion": "0.1.0-alpha.928",
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.927</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.928</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -101,10 +101,14 @@ async function defaultEncodeVariant(input) {
101
101
  }
102
102
  async function normalizeImageForVision(params) {
103
103
  const maxVlmBytes = params.maxVlmBytes ?? exports.MAX_VLM_IMAGE_BYTES;
104
+ if (params.maxEdge !== undefined && (!Number.isInteger(params.maxEdge) || params.maxEdge < 1)) {
105
+ throw new Error("Image maximum edge must be a positive integer");
106
+ }
104
107
  const originalMimeType = params.attachment.mimeType?.trim().toLowerCase();
105
108
  const originalByteCount = params.attachment.byteCount;
106
109
  const outputDir = materializedOutputDir(params.agentRoot, params.attachment.id);
107
110
  if (originalMimeType
111
+ && params.maxEdge === undefined
108
112
  && SAFE_VLM_MIME_TYPES.has(originalMimeType)
109
113
  && typeof originalByteCount === "number"
110
114
  && originalByteCount <= maxVlmBytes) {
@@ -149,8 +153,11 @@ async function normalizeImageForVision(params) {
149
153
  probe = await probeImage({ sourcePath: workingSourcePath });
150
154
  }
151
155
  const targetFormats = probe.hasAlpha ? ["png", "jpeg"] : ["jpeg"];
156
+ const requestedMaxEdge = params.maxEdge;
157
+ const sizes = requestedMaxEdge === undefined ? SIZE_STEPS
158
+ : [...new Set([requestedMaxEdge, ...SIZE_STEPS.filter((size) => size < requestedMaxEdge)])];
152
159
  for (const targetFormat of targetFormats) {
153
- for (const maxEdge of SIZE_STEPS) {
160
+ for (const maxEdge of sizes) {
154
161
  const qualities = targetFormat === "jpeg" ? JPEG_QUALITIES : [undefined];
155
162
  for (const quality of qualities) {
156
163
  const variant = await encodeVariant({
@@ -1184,6 +1184,9 @@ function checkFirstClassMcpTrust(context) {
1184
1184
  return allow;
1185
1185
  }
1186
1186
  function checkTrustLevelGuardrails(toolName, args, context) {
1187
+ if (toolName === "teams_send_image" && context.trustLevel !== "family") {
1188
+ return deny("sending local images in Teams requires family trust.");
1189
+ }
1187
1190
  const cacheSyncResult = mailCacheSyncShellGuardrail(toolName, args, context);
1188
1191
  if (!cacheSyncResult.allowed)
1189
1192
  return cacheSyncResult;
@@ -9,6 +9,7 @@ const ado_client_1 = require("./ado-client");
9
9
  const graph_endpoints_json_1 = __importDefault(require("./data/graph-endpoints.json"));
10
10
  const ado_endpoints_json_1 = __importDefault(require("./data/ado-endpoints.json"));
11
11
  const runtime_1 = require("../nerves/runtime");
12
+ const teams_image_1 = require("../senses/teams-image");
12
13
  const MUTATE_METHODS = ["POST", "PATCH", "DELETE"];
13
14
  const DEFAULT_ADO_QUERY = "SELECT [System.Id], [System.Title], [System.State], [System.AssignedTo] FROM WorkItems WHERE [System.AssignedTo] = @Me AND [System.State] <> 'Closed' ORDER BY [System.ChangedDate] DESC";
14
15
  exports.teamsToolDefinitions = [
@@ -283,6 +284,43 @@ exports.teamsToolDefinitions = [
283
284
  integration: "ado",
284
285
  summaryKeys: ["query"],
285
286
  },
287
+ {
288
+ tool: {
289
+ type: "function",
290
+ function: {
291
+ name: "teams_send_image",
292
+ description: "Send a local PNG or JPEG image into the current Teams conversation, without publishing it or using OneDrive. Requires family trust. Images are resized/compressed for inline delivery. An activity receipt confirms Teams acceptance, not client rendering.",
293
+ parameters: {
294
+ type: "object",
295
+ properties: {
296
+ path: { type: "string", description: "Absolute path to a local PNG or JPEG image" },
297
+ caption: { type: "string", maxLength: teams_image_1.MAX_TEAMS_IMAGE_CAPTION_LENGTH, description: "Optional short caption (up to 1024 characters)" },
298
+ },
299
+ required: ["path"],
300
+ additionalProperties: false,
301
+ },
302
+ },
303
+ },
304
+ handler: async (args, ctx) => {
305
+ if (ctx?.noSend || ctx?.habitSession || ctx?.autonomousTurnKind) {
306
+ throw new Error("Teams image sending is unavailable in no-send or autonomous turns");
307
+ }
308
+ if (ctx?.context?.friend.trustLevel !== "family" || ctx.context.channel.channel !== "teams") {
309
+ throw new Error("Sending local images in Teams requires a family-trusted Teams conversation");
310
+ }
311
+ if (!ctx.teamsSendImage || !ctx.agentName || !ctx.agentRoot) {
312
+ throw new Error("Teams image sending requires an authenticated current-conversation sender and agent owner");
313
+ }
314
+ const caption = args.caption ?? "";
315
+ if (caption.length > teams_image_1.MAX_TEAMS_IMAGE_CAPTION_LENGTH)
316
+ throw new Error("Teams image caption exceeds 1024 characters");
317
+ const attachment = await (0, teams_image_1.prepareTeamsImage)({ path: args.path, agentName: ctx.agentName, agentRoot: ctx.agentRoot });
318
+ const activityId = await ctx.teamsSendImage(attachment, caption);
319
+ return `image accepted by Teams in the current conversation (activity ${activityId}); client rendering is not verified`;
320
+ },
321
+ summaryKeys: ["path"],
322
+ riskProfile: { mutates: "external_side_effect", risk: "high", reason: "sends local image bytes to the current Teams conversation" },
323
+ },
286
324
  ];
287
325
  function searchEndpoints(entries, query) {
288
326
  (0, runtime_1.emitNervesEvent)({
@@ -164,7 +164,8 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
164
164
  }
165
165
  else {
166
166
  const available = new Set(capabilities?.availableIntegrations ?? []);
167
- const integrations = available.size === 0 ? [] : [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions]
167
+ const integrations = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions]
168
+ .filter((definition) => available.size > 0 || definition.tool.function.name === "teams_send_image")
168
169
  .filter((definition) => definition.integration ? available.has(definition.integration) : capabilities?.channel === "teams")
169
170
  .map((definition) => {
170
171
  const preference = definition.integration ? toolPreferences?.[definition.integration] : undefined;
@@ -197,7 +198,9 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
197
198
  ordinary.push(tools_surface_1.surfaceToolDefinition);
198
199
  }
199
200
  }
200
- ordinary = ordinary.filter((definition) => (!relationship || relationship.advertisedToolNames.includes(definition.tool.function.name))
201
+ ordinary = ordinary.filter((definition) => (definition.tool.function.name !== "teams_send_image" || (!context?.noSend && !context?.habitSession && !context?.autonomousTurnKind))
202
+ &&
203
+ (!relationship || relationship.advertisedToolNames.includes(definition.tool.function.name))
201
204
  && (!definition.requiredCapability || providerCapabilities?.has(definition.requiredCapability) === true));
202
205
  const engine = [
203
206
  ...(context?.noSend ? [] : [tools_flow_1.ponderTool]),
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH = exports.MAX_TEAMS_INLINE_IMAGE_BYTES = void 0;
37
+ exports.prepareTeamsImage = prepareTeamsImage;
38
+ exports.createTeamsImageSender = createTeamsImageSender;
39
+ const fs = __importStar(require("node:fs/promises"));
40
+ const path = __importStar(require("node:path"));
41
+ const image_normalize_1 = require("../heart/attachments/image-normalize");
42
+ const cli_local_file_1 = require("../heart/attachments/sources/cli-local-file");
43
+ const runtime_1 = require("../nerves/runtime");
44
+ exports.MAX_TEAMS_INLINE_IMAGE_BYTES = 56 * 1024;
45
+ exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH = 1024;
46
+ function imageMime(bytes) {
47
+ if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
48
+ return "image/png";
49
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255)
50
+ return "image/jpeg";
51
+ throw new Error("Teams image must contain PNG or JPEG bytes");
52
+ }
53
+ async function readBoundedImage(filePath, maxBytes) {
54
+ const file = await fs.open(filePath, "r");
55
+ try {
56
+ const stat = await file.stat();
57
+ if (!stat.isFile() || stat.size === 0 || stat.size > maxBytes) {
58
+ throw new Error(`Teams image must be a nonempty regular file under ${maxBytes} bytes`);
59
+ }
60
+ // Bound the actual read as well as the stat, including a file that grows after the stat.
61
+ const bytes = Buffer.alloc(stat.size + 1);
62
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
63
+ if (bytesRead !== stat.size)
64
+ throw new Error("Teams image changed while reading; nothing was sent");
65
+ return bytes.subarray(0, bytesRead);
66
+ }
67
+ finally {
68
+ await file.close();
69
+ }
70
+ }
71
+ async function prepareTeamsImage(input) {
72
+ if (!path.isAbsolute(input.path))
73
+ throw new Error("Teams image path must be absolute");
74
+ const original = await readBoundedImage(input.path, image_normalize_1.MAX_ATTACHMENT_DOWNLOAD_BYTES_IMAGE);
75
+ const mimeType = imageMime(original);
76
+ const attachment = (0, cli_local_file_1.buildCliLocalFileAttachmentRecord)({ path: input.path, mimeType, byteCount: original.length });
77
+ const normalized = await (0, image_normalize_1.normalizeImageForVision)({
78
+ attachment, sourcePath: input.path, agentName: input.agentName, agentRoot: input.agentRoot,
79
+ maxEdge: 1024, maxVlmBytes: exports.MAX_TEAMS_INLINE_IMAGE_BYTES,
80
+ });
81
+ const bytes = await readBoundedImage(normalized.path, exports.MAX_TEAMS_INLINE_IMAGE_BYTES);
82
+ const contentType = imageMime(bytes);
83
+ if (normalized.mimeType !== contentType)
84
+ throw new Error("Normalized Teams image MIME does not match its bytes");
85
+ (0, runtime_1.emitNervesEvent)({
86
+ component: "channels", event: "channel.teams_image_prepared",
87
+ message: "private image prepared for inline Teams delivery",
88
+ meta: { mimeType: contentType, byteCount: bytes.length, maxEdge: 1024 },
89
+ });
90
+ return {
91
+ contentType,
92
+ contentUrl: `data:${contentType};base64,${bytes.toString("base64")}`,
93
+ name: `${path.basename(input.path, path.extname(input.path)).slice(0, 100)}${contentType === "image/png" ? ".png" : ".jpg"}`,
94
+ };
95
+ }
96
+ function createTeamsImageSender(send, replyToId, entities = []) {
97
+ return async (attachment, caption) => {
98
+ if (caption.length > exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH)
99
+ throw new Error("Teams image caption exceeds 1024 characters");
100
+ const message = {
101
+ type: "message", text: caption, replyToId, attachments: [attachment],
102
+ entities, channelData: { feedbackLoopEnabled: true },
103
+ };
104
+ if (Buffer.byteLength(JSON.stringify(message), "utf8") > 80 * 1024) {
105
+ throw new Error("Teams image message exceeds the inline payload budget; nothing was sent");
106
+ }
107
+ const receipt = await send(message);
108
+ if (!receipt || typeof receipt !== "object" || !("id" in receipt)
109
+ || typeof receipt.id !== "string" || !receipt.id.trim()) {
110
+ throw new Error("Teams image delivery is unknown: no activity receipt returned; do not retry automatically");
111
+ }
112
+ (0, runtime_1.emitNervesEvent)({
113
+ component: "channels", event: "channel.teams_image_accepted",
114
+ message: "Teams accepted an inline image activity; client rendering is not verified",
115
+ meta: { activityId: receipt.id, mimeType: attachment.contentType },
116
+ });
117
+ return receipt.id;
118
+ };
119
+ }
@@ -78,6 +78,7 @@ const session_transaction_1 = require("../mind/session-transaction");
78
78
  const teamsFailoverStates = new Map();
79
79
  const pending_1 = require("../mind/pending");
80
80
  const continuity_1 = require("./continuity");
81
+ const teams_image_1 = require("./teams-image");
81
82
  // AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
82
83
  // Teams messages. Required by Teams AI UX best practices.
83
84
  function aiLabelEntities() {
@@ -721,6 +722,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
721
722
  summarize: (0, core_1.createSummarize)("human"),
722
723
  tenantId: teamsContext.tenantId,
723
724
  botApi: teamsContext.botApi,
725
+ teamsSendImage: teamsContext.teamsSendImage,
724
726
  } : {}) };
725
727
  let currentText = text;
726
728
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(owner) ?? undefined;
@@ -1181,6 +1183,7 @@ function registerBotHandlers(app, label) {
1181
1183
  githubConnectionName: tenantOAuth.githubConnectionName,
1182
1184
  /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
1183
1185
  botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
1186
+ teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
1184
1187
  };
1185
1188
  /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
1186
1189
  const ctxSend = async (t) => {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.927",
3
+ "version": "0.1.0-alpha.928",
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.927",
9
+ "version": "0.1.0-alpha.928",
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.927",
3
+ "version": "0.1.0-alpha.928",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },