@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.20

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.
Files changed (56) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/assets/ouroboros.png +0 -0
  5. package/dist/heart/config.js +66 -4
  6. package/dist/heart/core.js +75 -2
  7. package/dist/heart/daemon/daemon-cli.js +507 -29
  8. package/dist/heart/daemon/daemon-entry.js +13 -5
  9. package/dist/heart/daemon/daemon.js +42 -9
  10. package/dist/heart/daemon/hatch-animation.js +35 -0
  11. package/dist/heart/daemon/hatch-flow.js +2 -11
  12. package/dist/heart/daemon/hatch-specialist.js +6 -1
  13. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  14. package/dist/heart/daemon/ouro-path-installer.js +177 -0
  15. package/dist/heart/daemon/ouro-uti.js +11 -2
  16. package/dist/heart/daemon/process-manager.js +1 -1
  17. package/dist/heart/daemon/runtime-logging.js +9 -5
  18. package/dist/heart/daemon/runtime-metadata.js +118 -0
  19. package/dist/heart/daemon/sense-manager.js +266 -0
  20. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  21. package/dist/heart/daemon/specialist-prompt.js +98 -0
  22. package/dist/heart/daemon/specialist-tools.js +237 -0
  23. package/dist/heart/daemon/subagent-installer.js +10 -1
  24. package/dist/heart/identity.js +77 -1
  25. package/dist/heart/providers/anthropic.js +19 -2
  26. package/dist/heart/sense-truth.js +61 -0
  27. package/dist/heart/streaming.js +99 -21
  28. package/dist/mind/bundle-manifest.js +58 -0
  29. package/dist/mind/friends/channel.js +8 -0
  30. package/dist/mind/friends/types.js +1 -1
  31. package/dist/mind/prompt.js +77 -3
  32. package/dist/nerves/cli-logging.js +15 -2
  33. package/dist/repertoire/ado-client.js +4 -2
  34. package/dist/repertoire/coding/feedback.js +134 -0
  35. package/dist/repertoire/coding/index.js +4 -1
  36. package/dist/repertoire/coding/manager.js +61 -2
  37. package/dist/repertoire/coding/spawner.js +3 -3
  38. package/dist/repertoire/coding/tools.js +41 -2
  39. package/dist/repertoire/data/ado-endpoints.json +188 -0
  40. package/dist/repertoire/tools-base.js +69 -5
  41. package/dist/repertoire/tools-teams.js +57 -4
  42. package/dist/repertoire/tools.js +44 -11
  43. package/dist/senses/bluebubbles-client.js +433 -0
  44. package/dist/senses/bluebubbles-entry.js +11 -0
  45. package/dist/senses/bluebubbles-media.js +244 -0
  46. package/dist/senses/bluebubbles-model.js +253 -0
  47. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  48. package/dist/senses/bluebubbles.js +421 -0
  49. package/dist/senses/cli.js +293 -133
  50. package/dist/senses/debug-activity.js +107 -0
  51. package/dist/senses/teams.js +173 -54
  52. package/package.json +11 -4
  53. package/subagents/work-doer.md +26 -24
  54. package/subagents/work-merger.md +24 -30
  55. package/subagents/work-planner.md +34 -25
  56. package/dist/inner-worker-entry.js +0 -4
@@ -82,7 +82,7 @@ exports.teamsToolDefinitions = [
82
82
  type: "function",
83
83
  function: {
84
84
  name: "ado_query",
85
- description: "GET or POST (for WIQL read queries) any Azure DevOps API endpoint. Use ado_docs first to look up the correct path.",
85
+ description: "GET or POST (for WIQL read queries) any Azure DevOps API endpoint. Use ado_docs first to look up the correct path and host.",
86
86
  parameters: {
87
87
  type: "object",
88
88
  properties: {
@@ -90,6 +90,7 @@ exports.teamsToolDefinitions = [
90
90
  path: { type: "string", description: "ADO API path after /{org}, e.g. /_apis/wit/wiql" },
91
91
  method: { type: "string", enum: ["GET", "POST"], description: "HTTP method (defaults to GET)" },
92
92
  body: { type: "string", description: "JSON request body (optional, used with POST for WIQL)" },
93
+ host: { type: "string", description: "API host override for non-standard APIs (e.g. 'vsapm.dev.azure.com' for entitlements, 'vssps.dev.azure.com' for users). Omit for standard dev.azure.com." },
93
94
  },
94
95
  required: ["organization", "path"],
95
96
  },
@@ -100,7 +101,7 @@ exports.teamsToolDefinitions = [
100
101
  return "AUTH_REQUIRED:ado -- I need access to Azure DevOps. Please sign in when prompted.";
101
102
  }
102
103
  const method = args.method || "GET";
103
- const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, method, args.organization, args.path, args.body);
104
+ const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, method, args.organization, args.path, args.body, args.host);
104
105
  checkAndRecord403(result, "ado", args.organization, method, ctx);
105
106
  return result;
106
107
  },
@@ -111,7 +112,7 @@ exports.teamsToolDefinitions = [
111
112
  type: "function",
112
113
  function: {
113
114
  name: "ado_mutate",
114
- description: "POST/PATCH/DELETE any Azure DevOps API endpoint for actual mutations. Use ado_docs first to look up the correct path.",
115
+ description: "POST/PATCH/DELETE any Azure DevOps API endpoint for actual mutations. Use ado_docs first to look up the correct path and host.",
115
116
  parameters: {
116
117
  type: "object",
117
118
  properties: {
@@ -119,6 +120,7 @@ exports.teamsToolDefinitions = [
119
120
  organization: { type: "string", description: "Azure DevOps organization name" },
120
121
  path: { type: "string", description: "ADO API path after /{org}" },
121
122
  body: { type: "string", description: "JSON request body (optional)" },
123
+ host: { type: "string", description: "API host override for non-standard APIs (e.g. 'vsapm.dev.azure.com' for entitlements, 'vssps.dev.azure.com' for users). Omit for standard dev.azure.com." },
122
124
  },
123
125
  required: ["method", "organization", "path"],
124
126
  },
@@ -133,7 +135,7 @@ exports.teamsToolDefinitions = [
133
135
  }
134
136
  /* v8 ignore next -- fallback unreachable: method is validated against MUTATE_METHODS above @preserve */
135
137
  const action = METHOD_TO_ACTION[args.method] || args.method;
136
- const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, args.method, args.organization, args.path, args.body);
138
+ const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, args.method, args.organization, args.path, args.body, args.host);
137
139
  checkAndRecord403(result, "ado", args.organization, action, ctx);
138
140
  return result;
139
141
  },
@@ -201,6 +203,53 @@ exports.teamsToolDefinitions = [
201
203
  },
202
204
  integration: "ado",
203
205
  },
206
+ // -- Proactive messaging --
207
+ {
208
+ tool: {
209
+ type: "function",
210
+ function: {
211
+ name: "teams_send_message",
212
+ description: "send a proactive 1:1 Teams message to a user. requires their AAD object ID (use graph_query /users to find it). the message appears as coming from the bot.",
213
+ parameters: {
214
+ type: "object",
215
+ properties: {
216
+ user_id: { type: "string", description: "AAD object ID of the user to message" },
217
+ user_name: { type: "string", description: "display name of the user (for logging)" },
218
+ message: { type: "string", description: "message text to send" },
219
+ tenant_id: { type: "string", description: "tenant ID (optional, defaults to current conversation tenant)" },
220
+ },
221
+ required: ["user_id", "message"],
222
+ },
223
+ },
224
+ },
225
+ /* v8 ignore start -- proactive messaging requires live Teams SDK conversation client @preserve */
226
+ handler: async (args, ctx) => {
227
+ if (!ctx?.botApi) {
228
+ return "proactive messaging is not available -- no bot API context (this tool only works in the Teams channel)";
229
+ }
230
+ try {
231
+ const tenantId = args.tenant_id || ctx.tenantId;
232
+ // Cast to the SDK's ConversationClient shape (kept as `unknown` in ToolContext to avoid type coupling)
233
+ const conversations = ctx.botApi.conversations;
234
+ const conversation = await conversations.create({
235
+ bot: { id: ctx.botApi.id },
236
+ members: [{ id: args.user_id, role: "user", name: args.user_name || args.user_id }],
237
+ tenantId,
238
+ isGroup: false,
239
+ });
240
+ await conversations.activities(conversation.id).create({
241
+ type: "message",
242
+ text: args.message,
243
+ });
244
+ return `message sent to ${args.user_name || args.user_id}`;
245
+ }
246
+ catch (e) {
247
+ return `failed to send proactive message: ${e instanceof Error ? e.message : String(e)}`;
248
+ }
249
+ },
250
+ /* v8 ignore stop */
251
+ confirmationRequired: true,
252
+ },
204
253
  // -- Documentation tools --
205
254
  {
206
255
  tool: {
@@ -268,6 +317,8 @@ function searchEndpoints(entries, query) {
268
317
  ` ${e.description}`,
269
318
  ` Params: ${e.params || "none"}`,
270
319
  ];
320
+ if (e.host)
321
+ lines.push(` Host: ${e.host}`);
271
322
  if (e.scopes)
272
323
  lines.push(` Scopes: ${e.scopes}`);
273
324
  return lines.join("\n");
@@ -304,5 +355,7 @@ function summarizeTeamsArgs(name, args) {
304
355
  return summarizeKeyValues(["query"]);
305
356
  if (name === "ado_docs")
306
357
  return summarizeKeyValues(["query"]);
358
+ if (name === "teams_send_message")
359
+ return summarizeKeyValues(["user_name", "user_id"]);
307
360
  return undefined;
308
361
  }
@@ -19,9 +19,29 @@ Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function (
19
19
  // All tool definitions in a single registry
20
20
  const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
21
21
  const REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "git_commit", "gh_cli"]);
22
- function baseToolsForCapabilities(capabilities) {
23
- const isRemoteChannel = capabilities?.channel === "teams";
24
- if (!isRemoteChannel)
22
+ function isRemoteChannel(capabilities) {
23
+ return capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
24
+ }
25
+ function isSharedRemoteContext(friend) {
26
+ const externalIds = friend.externalIds ?? [];
27
+ return externalIds.some((externalId) => externalId.externalId.startsWith("group:") || externalId.provider === "teams-conversation");
28
+ }
29
+ function isTrustedRemoteContext(context) {
30
+ if (!context?.friend || !isRemoteChannel(context.channel))
31
+ return false;
32
+ const trustLevel = context.friend.trustLevel ?? "stranger";
33
+ return trustLevel !== "stranger" && !isSharedRemoteContext(context.friend);
34
+ }
35
+ function shouldBlockLocalTools(capabilities, context) {
36
+ if (!isRemoteChannel(capabilities))
37
+ return false;
38
+ return !isTrustedRemoteContext(context);
39
+ }
40
+ function blockedLocalToolMessage() {
41
+ return "I can't do that from here because I'm talking to multiple people in a shared remote channel, and local shell/file/git/gh operations could let conversations interfere with each other. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
42
+ }
43
+ function baseToolsForCapabilities(capabilities, context) {
44
+ if (!shouldBlockLocalTools(capabilities, context))
25
45
  return tools_base_1.tools;
26
46
  return tools_base_1.tools.filter((tool) => !REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
27
47
  }
@@ -39,13 +59,15 @@ function applyPreference(tool, pref) {
39
59
  // Base tools (no integration) are always included.
40
60
  // Teams/integration tools are included only if their integration is in availableIntegrations.
41
61
  // When toolPreferences is provided, matching preferences are appended to tool descriptions.
42
- function getToolsForChannel(capabilities, toolPreferences) {
43
- const baseTools = baseToolsForCapabilities(capabilities);
62
+ function getToolsForChannel(capabilities, toolPreferences, context) {
63
+ const baseTools = baseToolsForCapabilities(capabilities, context);
44
64
  if (!capabilities || capabilities.availableIntegrations.length === 0) {
45
65
  return baseTools;
46
66
  }
47
67
  const available = new Set(capabilities.availableIntegrations);
48
- const integrationDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions].filter((d) => d.integration && available.has(d.integration));
68
+ const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
69
+ // Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
70
+ const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
49
71
  if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
50
72
  return [...baseTools, ...integrationDefs.map((d) => d.tool)];
51
73
  }
@@ -87,15 +109,14 @@ async function execTool(name, args, ctx) {
87
109
  });
88
110
  return `unknown: ${name}`;
89
111
  }
90
- const isRemoteChannel = ctx?.context?.channel?.channel === "teams";
91
- if (isRemoteChannel && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
92
- const message = "I can't do that from here because I'm talking to multiple people in a shared remote channel, and local shell/file/git/gh operations could let conversations interfere with each other. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
112
+ if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
113
+ const message = blockedLocalToolMessage();
93
114
  (0, runtime_1.emitNervesEvent)({
94
115
  level: "warn",
95
116
  event: "tool.error",
96
117
  component: "tools",
97
118
  message: "blocked local tool in remote channel",
98
- meta: { name, channel: "teams" },
119
+ meta: { name, channel: ctx?.context?.channel?.channel },
99
120
  });
100
121
  return message;
101
122
  }
@@ -163,7 +184,9 @@ function summarizeArgs(name, args) {
163
184
  if (name === "load_skill")
164
185
  return summarizeKeyValues(args, ["name"]);
165
186
  if (name === "task_create")
166
- return summarizeKeyValues(args, ["title", "type", "category"]);
187
+ return summarizeKeyValues(args, ["title", "type", "category", "scheduledAt", "cadence"]);
188
+ if (name === "schedule_reminder")
189
+ return summarizeKeyValues(args, ["title", "scheduledAt", "cadence"]);
167
190
  if (name === "task_update_status")
168
191
  return summarizeKeyValues(args, ["name", "status"]);
169
192
  if (name === "task_board_status")
@@ -176,6 +199,8 @@ function summarizeArgs(name, args) {
176
199
  return summarizeKeyValues(args, ["runner", "workdir", "taskRef"]);
177
200
  if (name === "coding_status")
178
201
  return summarizeKeyValues(args, ["sessionId"]);
202
+ if (name === "coding_tail")
203
+ return summarizeKeyValues(args, ["sessionId"]);
179
204
  if (name === "coding_send_input")
180
205
  return summarizeKeyValues(args, ["sessionId", "input"]);
181
206
  if (name === "coding_kill")
@@ -195,5 +220,13 @@ function summarizeArgs(name, args) {
195
220
  }
196
221
  if (name === "ado_backlog_list")
197
222
  return summarizeKeyValues(args, ["organization", "project"]);
223
+ if (name === "ado_batch_update")
224
+ return summarizeKeyValues(args, ["organization", "project"]);
225
+ if (name === "ado_create_epic" || name === "ado_create_issue")
226
+ return summarizeKeyValues(args, ["organization", "project", "title"]);
227
+ if (name === "ado_move_items")
228
+ return summarizeKeyValues(args, ["organization", "project", "workItemIds"]);
229
+ if (name === "ado_restructure_backlog")
230
+ return summarizeKeyValues(args, ["organization", "project"]);
198
231
  return summarizeUnknownArgs(args);
199
232
  }
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBlueBubblesClient = createBlueBubblesClient;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const config_1 = require("../heart/config");
6
+ const identity_1 = require("../heart/identity");
7
+ const runtime_1 = require("../nerves/runtime");
8
+ const bluebubbles_model_1 = require("./bluebubbles-model");
9
+ const bluebubbles_media_1 = require("./bluebubbles-media");
10
+ function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
11
+ const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
12
+ const url = new URL(endpoint.replace(/^\//, ""), root);
13
+ url.searchParams.set("password", password);
14
+ return url.toString();
15
+ }
16
+ function asRecord(value) {
17
+ return value && typeof value === "object" && !Array.isArray(value)
18
+ ? value
19
+ : null;
20
+ }
21
+ function readString(record, key) {
22
+ const value = record[key];
23
+ return typeof value === "string" ? value : undefined;
24
+ }
25
+ function extractMessageGuid(payload) {
26
+ if (!payload || typeof payload !== "object")
27
+ return undefined;
28
+ const record = payload;
29
+ const data = record.data && typeof record.data === "object" && !Array.isArray(record.data)
30
+ ? record.data
31
+ : null;
32
+ const candidates = [
33
+ record.messageGuid,
34
+ record.messageId,
35
+ record.guid,
36
+ data?.messageGuid,
37
+ data?.messageId,
38
+ data?.guid,
39
+ ];
40
+ for (const candidate of candidates) {
41
+ if (typeof candidate === "string" && candidate.trim()) {
42
+ return candidate.trim();
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ async function parseJsonBody(response) {
48
+ const raw = await response.text();
49
+ if (!raw.trim())
50
+ return null;
51
+ try {
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function buildRepairUrl(baseUrl, messageGuid, password) {
59
+ const url = buildBlueBubblesApiUrl(baseUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}`, password);
60
+ const parsed = new URL(url);
61
+ parsed.searchParams.set("with", "attachments,payloadData,chats,messageSummaryInfo");
62
+ return parsed.toString();
63
+ }
64
+ function extractChatIdentifierFromGuid(chatGuid) {
65
+ const parts = chatGuid.split(";");
66
+ return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
67
+ }
68
+ function extractChatGuid(value) {
69
+ const record = asRecord(value);
70
+ const candidates = [
71
+ record?.chatGuid,
72
+ record?.guid,
73
+ record?.chat_guid,
74
+ record?.identifier,
75
+ record?.chatIdentifier,
76
+ record?.chat_identifier,
77
+ ];
78
+ for (const candidate of candidates) {
79
+ if (typeof candidate === "string" && candidate.trim()) {
80
+ return candidate.trim();
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ function extractQueriedChatIdentifier(chat, chatGuid) {
86
+ const explicitIdentifier = readString(chat, "chatIdentifier")
87
+ ?? readString(chat, "identifier")
88
+ ?? readString(chat, "chat_identifier");
89
+ if (explicitIdentifier) {
90
+ return explicitIdentifier;
91
+ }
92
+ return extractChatIdentifierFromGuid(chatGuid);
93
+ }
94
+ function extractChatQueryRows(payload) {
95
+ const record = asRecord(payload);
96
+ const data = Array.isArray(record?.data) ? record.data : payload;
97
+ if (!Array.isArray(data)) {
98
+ return [];
99
+ }
100
+ return data.map((entry) => asRecord(entry)).filter((entry) => entry !== null);
101
+ }
102
+ async function resolveChatGuidForIdentifier(config, channelConfig, chatIdentifier) {
103
+ const trimmedIdentifier = chatIdentifier.trim();
104
+ if (!trimmedIdentifier)
105
+ return undefined;
106
+ const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/chat/query", config.password);
107
+ const response = await fetch(url, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({
111
+ limit: 500,
112
+ offset: 0,
113
+ with: ["participants"],
114
+ }),
115
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
116
+ });
117
+ if (!response.ok) {
118
+ return undefined;
119
+ }
120
+ const payload = await parseJsonBody(response);
121
+ const rows = extractChatQueryRows(payload);
122
+ for (const row of rows) {
123
+ const guid = extractChatGuid(row);
124
+ if (!guid)
125
+ continue;
126
+ const identifier = extractQueriedChatIdentifier(row, guid);
127
+ if (identifier === trimmedIdentifier || guid === trimmedIdentifier) {
128
+ return guid;
129
+ }
130
+ }
131
+ return undefined;
132
+ }
133
+ function collectPreviewStrings(value, out, depth = 0) {
134
+ if (depth > 4 || out.length >= 4)
135
+ return;
136
+ if (typeof value === "string") {
137
+ const trimmed = value.trim();
138
+ if (trimmed)
139
+ out.push(trimmed);
140
+ return;
141
+ }
142
+ if (Array.isArray(value)) {
143
+ for (const entry of value)
144
+ collectPreviewStrings(entry, out, depth + 1);
145
+ return;
146
+ }
147
+ const record = asRecord(value);
148
+ if (!record)
149
+ return;
150
+ const preferredKeys = ["title", "summary", "subtitle", "previewText", "siteName", "host", "url"];
151
+ for (const key of preferredKeys) {
152
+ if (out.length >= 4)
153
+ break;
154
+ collectPreviewStrings(record[key], out, depth + 1);
155
+ }
156
+ }
157
+ function extractLinkPreviewText(data) {
158
+ const values = [];
159
+ collectPreviewStrings(data.payloadData, values);
160
+ collectPreviewStrings(data.messageSummaryInfo, values);
161
+ const unique = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
162
+ if (unique.length === 0)
163
+ return undefined;
164
+ return unique.slice(0, 2).join(" — ");
165
+ }
166
+ function applyRepairNotice(event, notice) {
167
+ return {
168
+ ...event,
169
+ requiresRepair: false,
170
+ repairNotice: notice,
171
+ };
172
+ }
173
+ function hydrateTextForAgent(event, rawData) {
174
+ if (event.kind !== "message") {
175
+ return { ...event, requiresRepair: false };
176
+ }
177
+ if (event.balloonBundleId !== "com.apple.messages.URLBalloonProvider") {
178
+ return { ...event, requiresRepair: false };
179
+ }
180
+ const previewText = extractLinkPreviewText(rawData);
181
+ if (!previewText) {
182
+ return { ...event, requiresRepair: false };
183
+ }
184
+ const base = event.text.trim();
185
+ const textForAgent = base
186
+ ? `${base}\n[link preview: ${previewText}]`
187
+ : `[link preview: ${previewText}]`;
188
+ return {
189
+ ...event,
190
+ textForAgent,
191
+ requiresRepair: false,
192
+ };
193
+ }
194
+ function extractRepairData(payload) {
195
+ const record = asRecord(payload);
196
+ return asRecord(record?.data) ?? record;
197
+ }
198
+ function providerSupportsAudioInput(provider) {
199
+ return provider === "azure" || provider === "openai-codex";
200
+ }
201
+ async function resolveChatGuid(chat, config, channelConfig) {
202
+ return chat.chatGuid
203
+ ?? await resolveChatGuidForIdentifier(config, channelConfig, chat.chatIdentifier ?? "");
204
+ }
205
+ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)()) {
206
+ return {
207
+ async sendText(params) {
208
+ const trimmedText = params.text.trim();
209
+ if (!trimmedText) {
210
+ throw new Error("BlueBubbles send requires non-empty text.");
211
+ }
212
+ const resolvedChatGuid = await resolveChatGuid(params.chat, config, channelConfig);
213
+ if (!resolvedChatGuid) {
214
+ throw new Error("BlueBubbles send currently requires chat.chatGuid from the inbound event.");
215
+ }
216
+ const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/text", config.password);
217
+ const body = {
218
+ chatGuid: resolvedChatGuid,
219
+ tempGuid: (0, node_crypto_1.randomUUID)(),
220
+ message: trimmedText,
221
+ };
222
+ if (params.replyToMessageGuid?.trim()) {
223
+ body.method = "private-api";
224
+ body.selectedMessageGuid = params.replyToMessageGuid.trim();
225
+ body.partIndex = 0;
226
+ }
227
+ (0, runtime_1.emitNervesEvent)({
228
+ component: "senses",
229
+ event: "senses.bluebubbles_send_start",
230
+ message: "sending bluebubbles message",
231
+ meta: {
232
+ chatGuid: resolvedChatGuid,
233
+ hasReplyTarget: Boolean(params.replyToMessageGuid?.trim()),
234
+ },
235
+ });
236
+ const response = await fetch(url, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify(body),
240
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
241
+ });
242
+ if (!response.ok) {
243
+ const errorText = await response.text().catch(() => "");
244
+ (0, runtime_1.emitNervesEvent)({
245
+ level: "error",
246
+ component: "senses",
247
+ event: "senses.bluebubbles_send_error",
248
+ message: "bluebubbles send failed",
249
+ meta: {
250
+ status: response.status,
251
+ reason: errorText || "unknown",
252
+ },
253
+ });
254
+ throw new Error(`BlueBubbles send failed (${response.status}): ${errorText || "unknown"}`);
255
+ }
256
+ const payload = await parseJsonBody(response);
257
+ const messageGuid = extractMessageGuid(payload);
258
+ (0, runtime_1.emitNervesEvent)({
259
+ component: "senses",
260
+ event: "senses.bluebubbles_send_end",
261
+ message: "bluebubbles message sent",
262
+ meta: {
263
+ chatGuid: resolvedChatGuid,
264
+ messageGuid: messageGuid ?? null,
265
+ },
266
+ });
267
+ return { messageGuid };
268
+ },
269
+ async editMessage(params) {
270
+ const messageGuid = params.messageGuid.trim();
271
+ const text = params.text.trim();
272
+ if (!messageGuid) {
273
+ throw new Error("BlueBubbles edit requires messageGuid.");
274
+ }
275
+ if (!text) {
276
+ throw new Error("BlueBubbles edit requires non-empty text.");
277
+ }
278
+ const editTimeoutMs = Math.max(channelConfig.requestTimeoutMs, 120000);
279
+ const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}/edit`, config.password);
280
+ const response = await fetch(url, {
281
+ method: "POST",
282
+ headers: { "Content-Type": "application/json" },
283
+ body: JSON.stringify({
284
+ editedMessage: text,
285
+ backwardsCompatibilityMessage: params.backwardsCompatibilityMessage ?? `Edited to: ${text}`,
286
+ partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
287
+ }),
288
+ signal: AbortSignal.timeout(editTimeoutMs),
289
+ });
290
+ if (!response.ok) {
291
+ const errorText = await response.text().catch(() => "");
292
+ throw new Error(`BlueBubbles edit failed (${response.status}): ${errorText || "unknown"}`);
293
+ }
294
+ },
295
+ async setTyping(chat, typing) {
296
+ const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
297
+ if (!resolvedChatGuid) {
298
+ return;
299
+ }
300
+ const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/chat/${encodeURIComponent(resolvedChatGuid)}/typing`, config.password);
301
+ const response = await fetch(url, {
302
+ method: typing ? "POST" : "DELETE",
303
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
304
+ });
305
+ if (!response.ok) {
306
+ const errorText = await response.text().catch(() => "");
307
+ throw new Error(`BlueBubbles typing failed (${response.status}): ${errorText || "unknown"}`);
308
+ }
309
+ },
310
+ async markChatRead(chat) {
311
+ const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
312
+ if (!resolvedChatGuid) {
313
+ return;
314
+ }
315
+ const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/chat/${encodeURIComponent(resolvedChatGuid)}/read`, config.password);
316
+ const response = await fetch(url, {
317
+ method: "POST",
318
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
319
+ });
320
+ if (!response.ok) {
321
+ const errorText = await response.text().catch(() => "");
322
+ throw new Error(`BlueBubbles read failed (${response.status}): ${errorText || "unknown"}`);
323
+ }
324
+ },
325
+ async repairEvent(event) {
326
+ if (!event.requiresRepair) {
327
+ (0, runtime_1.emitNervesEvent)({
328
+ component: "senses",
329
+ event: "senses.bluebubbles_repair_skipped",
330
+ message: "bluebubbles event repair skipped",
331
+ meta: {
332
+ kind: event.kind,
333
+ messageGuid: event.messageGuid,
334
+ },
335
+ });
336
+ return event;
337
+ }
338
+ (0, runtime_1.emitNervesEvent)({
339
+ component: "senses",
340
+ event: "senses.bluebubbles_repair_start",
341
+ message: "repairing bluebubbles event by guid",
342
+ meta: {
343
+ kind: event.kind,
344
+ messageGuid: event.messageGuid,
345
+ eventType: event.eventType,
346
+ },
347
+ });
348
+ const url = buildRepairUrl(config.serverUrl, event.messageGuid, config.password);
349
+ try {
350
+ const response = await fetch(url, {
351
+ method: "GET",
352
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
353
+ });
354
+ if (!response.ok) {
355
+ const errorText = await response.text().catch(() => "");
356
+ const repaired = applyRepairNotice(event, `BlueBubbles repair failed: ${errorText || `HTTP ${response.status}`}`);
357
+ (0, runtime_1.emitNervesEvent)({
358
+ level: "warn",
359
+ component: "senses",
360
+ event: "senses.bluebubbles_repair_error",
361
+ message: "bluebubbles repair request failed",
362
+ meta: {
363
+ messageGuid: event.messageGuid,
364
+ status: response.status,
365
+ reason: errorText || "unknown",
366
+ },
367
+ });
368
+ return repaired;
369
+ }
370
+ const payload = await parseJsonBody(response);
371
+ const data = extractRepairData(payload);
372
+ if (!data || typeof data.guid !== "string") {
373
+ const repaired = applyRepairNotice(event, "BlueBubbles repair failed: invalid message payload");
374
+ (0, runtime_1.emitNervesEvent)({
375
+ level: "warn",
376
+ component: "senses",
377
+ event: "senses.bluebubbles_repair_error",
378
+ message: "bluebubbles repair returned unusable payload",
379
+ meta: {
380
+ messageGuid: event.messageGuid,
381
+ },
382
+ });
383
+ return repaired;
384
+ }
385
+ const normalized = (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
386
+ type: event.eventType,
387
+ data,
388
+ });
389
+ let hydrated = hydrateTextForAgent(normalized, data);
390
+ if (hydrated.kind === "message" &&
391
+ hydrated.balloonBundleId !== "com.apple.messages.URLBalloonProvider" &&
392
+ hydrated.attachments.length > 0) {
393
+ const media = await (0, bluebubbles_media_1.hydrateBlueBubblesAttachments)(hydrated.attachments, config, channelConfig, {
394
+ preferAudioInput: providerSupportsAudioInput((0, identity_1.loadAgentConfig)().provider),
395
+ });
396
+ const transcriptSuffix = media.transcriptAdditions.map((entry) => `[${entry}]`).join("\n");
397
+ const noticeSuffix = media.notices.map((entry) => `[${entry}]`).join("\n");
398
+ const combinedSuffix = [transcriptSuffix, noticeSuffix].filter(Boolean).join("\n");
399
+ hydrated = {
400
+ ...hydrated,
401
+ inputPartsForAgent: media.inputParts.length > 0 ? media.inputParts : undefined,
402
+ textForAgent: [hydrated.textForAgent, combinedSuffix].filter(Boolean).join("\n"),
403
+ };
404
+ }
405
+ (0, runtime_1.emitNervesEvent)({
406
+ component: "senses",
407
+ event: "senses.bluebubbles_repair_end",
408
+ message: "bluebubbles event repaired",
409
+ meta: {
410
+ kind: hydrated.kind,
411
+ messageGuid: hydrated.messageGuid,
412
+ repairedFrom: event.kind,
413
+ },
414
+ });
415
+ return hydrated;
416
+ }
417
+ catch (error) {
418
+ const reason = error instanceof Error ? error.message : String(error);
419
+ (0, runtime_1.emitNervesEvent)({
420
+ level: "warn",
421
+ component: "senses",
422
+ event: "senses.bluebubbles_repair_error",
423
+ message: "bluebubbles repair threw",
424
+ meta: {
425
+ messageGuid: event.messageGuid,
426
+ reason,
427
+ },
428
+ });
429
+ return applyRepairNotice(event, `BlueBubbles repair failed: ${reason}`);
430
+ }
431
+ },
432
+ };
433
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ // Thin entrypoint for `npm run bluebubbles` / `node dist/senses/bluebubbles-entry.js --agent <name>`.
3
+ // Separated from bluebubbles.ts so the BlueBubbles adapter stays testable.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ if (!process.argv.includes("--agent")) {
6
+ // eslint-disable-next-line no-console -- pre-boot guard: --agent check before imports
7
+ console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/bluebubbles-entry.js --agent ouroboros");
8
+ process.exit(1);
9
+ }
10
+ const bluebubbles_1 = require("./bluebubbles");
11
+ (0, bluebubbles_1.startBlueBubblesApp)();