@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/assets/ouroboros.png +0 -0
- package/dist/heart/config.js +66 -4
- package/dist/heart/core.js +75 -2
- package/dist/heart/daemon/daemon-cli.js +523 -33
- package/dist/heart/daemon/daemon-entry.js +13 -5
- package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
- package/dist/heart/daemon/daemon.js +42 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +2 -11
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/runtime-logging.js +9 -5
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +98 -0
- package/dist/heart/daemon/specialist-tools.js +237 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/daemon/wrapper-publish-guard.js +48 -0
- package/dist/heart/identity.js +77 -1
- package/dist/heart/providers/anthropic.js +19 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +99 -21
- package/dist/mind/bundle-manifest.js +58 -0
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +77 -3
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-base.js +69 -5
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +44 -11
- package/dist/senses/bluebubbles-client.js +433 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles-model.js +253 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles.js +421 -0
- package/dist/senses/cli.js +293 -133
- package/dist/senses/debug-activity.js +107 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +11 -4
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- 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
|
}
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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:
|
|
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)();
|