@ouro.bot/cli 0.1.0-alpha.37 → 0.1.0-alpha.39
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 +22 -0
- package/dist/heart/daemon/daemon-cli.js +444 -32
- package/dist/heart/daemon/daemon.js +90 -0
- package/dist/heart/daemon/specialist-prompt.js +2 -1
- package/dist/heart/daemon/specialist-tools.js +48 -2
- package/dist/heart/kicks.js +1 -1
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/pending.js +8 -0
- package/dist/mind/prompt.js +126 -2
- package/dist/repertoire/tools-base.js +193 -271
- package/dist/repertoire/tools.js +18 -41
- package/dist/senses/bluebubbles-model.js +10 -0
- package/dist/senses/bluebubbles.js +301 -27
- package/dist/senses/cli.js +73 -50
- package/dist/senses/inner-dialog.js +99 -54
- package/dist/senses/pipeline.js +124 -0
- package/dist/senses/teams.js +264 -63
- package/dist/senses/trust-gate.js +113 -2
- package/package.json +2 -1
package/dist/repertoire/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.teamsTools = exports.finalAnswerTool = exports.tools = void 0;
|
|
3
|
+
exports.REMOTE_BLOCKED_LOCAL_TOOLS = exports.teamsTools = exports.finalAnswerTool = exports.tools = void 0;
|
|
4
4
|
exports.getToolsForChannel = getToolsForChannel;
|
|
5
5
|
exports.isConfirmationRequired = isConfirmationRequired;
|
|
6
6
|
exports.execTool = execTool;
|
|
@@ -10,6 +10,8 @@ const tools_teams_1 = require("./tools-teams");
|
|
|
10
10
|
const tools_bluebubbles_1 = require("./tools-bluebubbles");
|
|
11
11
|
const ado_semantic_1 = require("./ado-semantic");
|
|
12
12
|
const tools_github_1 = require("./tools-github");
|
|
13
|
+
const types_1 = require("../mind/friends/types");
|
|
14
|
+
const channel_1 = require("../mind/friends/channel");
|
|
13
15
|
const runtime_1 = require("../nerves/runtime");
|
|
14
16
|
// Re-export types and constants used by the rest of the codebase
|
|
15
17
|
var tools_base_2 = require("./tools-base");
|
|
@@ -18,39 +20,26 @@ Object.defineProperty(exports, "finalAnswerTool", { enumerable: true, get: funct
|
|
|
18
20
|
var tools_teams_2 = require("./tools-teams");
|
|
19
21
|
Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function () { return tools_teams_2.teamsTools; } });
|
|
20
22
|
// All tool definitions in a single registry
|
|
21
|
-
const allDefinitions = [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...tools_teams_1.teamsToolDefinitions,
|
|
25
|
-
...ado_semantic_1.adoSemanticToolDefinitions,
|
|
26
|
-
...tools_github_1.githubToolDefinitions,
|
|
27
|
-
];
|
|
28
|
-
const REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "git_commit", "gh_cli"]);
|
|
29
|
-
function isRemoteChannel(capabilities) {
|
|
30
|
-
return capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
|
|
31
|
-
}
|
|
32
|
-
function isSharedRemoteContext(friend) {
|
|
33
|
-
const externalIds = friend.externalIds ?? [];
|
|
34
|
-
return externalIds.some((externalId) => externalId.externalId.startsWith("group:") || externalId.provider === "teams-conversation");
|
|
35
|
-
}
|
|
23
|
+
const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
|
|
24
|
+
/** Tool names blocked for untrusted remote contexts. Shared with prompt.ts for restriction messaging. */
|
|
25
|
+
exports.REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "edit_file", "glob", "grep"]);
|
|
36
26
|
function isTrustedRemoteContext(context) {
|
|
37
|
-
if (!context?.friend || !isRemoteChannel(context.channel))
|
|
27
|
+
if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
38
28
|
return false;
|
|
39
|
-
|
|
40
|
-
return trustLevel !== "stranger" && !isSharedRemoteContext(context.friend);
|
|
29
|
+
return (0, types_1.isTrustedLevel)(context.friend.trustLevel);
|
|
41
30
|
}
|
|
42
31
|
function shouldBlockLocalTools(capabilities, context) {
|
|
43
|
-
if (!isRemoteChannel(capabilities))
|
|
32
|
+
if (!(0, channel_1.isRemoteChannel)(capabilities))
|
|
44
33
|
return false;
|
|
45
34
|
return !isTrustedRemoteContext(context);
|
|
46
35
|
}
|
|
47
36
|
function blockedLocalToolMessage() {
|
|
48
|
-
return "I can't do that
|
|
37
|
+
return "I can't do that because my trust level with you isn't high enough for local shell/file operations. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
|
|
49
38
|
}
|
|
50
39
|
function baseToolsForCapabilities(capabilities, context) {
|
|
51
40
|
if (!shouldBlockLocalTools(capabilities, context))
|
|
52
41
|
return tools_base_1.tools;
|
|
53
|
-
return tools_base_1.tools.filter((tool) => !REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
|
|
42
|
+
return tools_base_1.tools.filter((tool) => !exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
|
|
54
43
|
}
|
|
55
44
|
// Apply a single tool preference to a tool schema, returning a new object.
|
|
56
45
|
function applyPreference(tool, pref) {
|
|
@@ -119,7 +108,7 @@ async function execTool(name, args, ctx) {
|
|
|
119
108
|
});
|
|
120
109
|
return `unknown: ${name}`;
|
|
121
110
|
}
|
|
122
|
-
if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
|
|
111
|
+
if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
|
|
123
112
|
const message = blockedLocalToolMessage();
|
|
124
113
|
(0, runtime_1.emitNervesEvent)({
|
|
125
114
|
level: "warn",
|
|
@@ -183,28 +172,16 @@ function summarizeArgs(name, args) {
|
|
|
183
172
|
// Base tools
|
|
184
173
|
if (name === "read_file" || name === "write_file")
|
|
185
174
|
return summarizeKeyValues(args, ["path"]);
|
|
186
|
-
if (name === "
|
|
187
|
-
return summarizeKeyValues(args, ["command"]);
|
|
188
|
-
if (name === "list_directory")
|
|
175
|
+
if (name === "edit_file")
|
|
189
176
|
return summarizeKeyValues(args, ["path"]);
|
|
190
|
-
if (name === "
|
|
191
|
-
return summarizeKeyValues(args, ["
|
|
192
|
-
if (name === "
|
|
177
|
+
if (name === "glob")
|
|
178
|
+
return summarizeKeyValues(args, ["pattern", "cwd"]);
|
|
179
|
+
if (name === "grep")
|
|
180
|
+
return summarizeKeyValues(args, ["pattern", "path", "include"]);
|
|
181
|
+
if (name === "shell")
|
|
193
182
|
return summarizeKeyValues(args, ["command"]);
|
|
194
183
|
if (name === "load_skill")
|
|
195
184
|
return summarizeKeyValues(args, ["name"]);
|
|
196
|
-
if (name === "task_create")
|
|
197
|
-
return summarizeKeyValues(args, ["title", "type", "category", "scheduledAt", "cadence"]);
|
|
198
|
-
if (name === "schedule_reminder")
|
|
199
|
-
return summarizeKeyValues(args, ["title", "scheduledAt", "cadence"]);
|
|
200
|
-
if (name === "task_update_status")
|
|
201
|
-
return summarizeKeyValues(args, ["name", "status"]);
|
|
202
|
-
if (name === "task_board_status")
|
|
203
|
-
return summarizeKeyValues(args, ["status"]);
|
|
204
|
-
if (name === "task_board_action")
|
|
205
|
-
return summarizeKeyValues(args, ["scope"]);
|
|
206
|
-
if (name === "task_board" || name === "task_board_deps" || name === "task_board_sessions")
|
|
207
|
-
return "";
|
|
208
185
|
if (name === "coding_spawn")
|
|
209
186
|
return summarizeKeyValues(args, ["runner", "workdir", "taskRef"]);
|
|
210
187
|
if (name === "coding_status")
|
|
@@ -52,6 +52,15 @@ function buildChatRef(data, threadOriginatorGuid) {
|
|
|
52
52
|
const sessionKey = chatGuid?.trim()
|
|
53
53
|
? `chat:${chatGuid.trim()}`
|
|
54
54
|
: `chat_identifier:${(chatIdentifier ?? "unknown").trim()}`;
|
|
55
|
+
// Extract participant handles from chat.participants (when available from BB API)
|
|
56
|
+
const rawParticipants = Array.isArray(chat?.participants) ? chat.participants : [];
|
|
57
|
+
const participantHandles = rawParticipants
|
|
58
|
+
.map((p) => {
|
|
59
|
+
const rec = asRecord(p);
|
|
60
|
+
const addr = readString(rec, "address") ?? readString(rec, "id");
|
|
61
|
+
return addr ? normalizeHandle(addr) : "";
|
|
62
|
+
})
|
|
63
|
+
.filter(Boolean);
|
|
55
64
|
return {
|
|
56
65
|
chatGuid: chatGuid?.trim() || undefined,
|
|
57
66
|
chatIdentifier: chatIdentifier?.trim() || undefined,
|
|
@@ -61,6 +70,7 @@ function buildChatRef(data, threadOriginatorGuid) {
|
|
|
61
70
|
sendTarget: chatGuid?.trim()
|
|
62
71
|
? { kind: "chat_guid", value: chatGuid.trim() }
|
|
63
72
|
: { kind: "chat_identifier", value: (chatIdentifier ?? "unknown").trim() },
|
|
73
|
+
participantHandles,
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
76
|
function extractSender(data, chat) {
|
|
@@ -35,7 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
37
37
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
38
|
+
exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
|
|
38
39
|
exports.startBlueBubblesApp = startBlueBubblesApp;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
39
41
|
const http = __importStar(require("node:http"));
|
|
40
42
|
const path = __importStar(require("node:path"));
|
|
41
43
|
const core_1 = require("../heart/core");
|
|
@@ -45,6 +47,9 @@ const context_1 = require("../mind/context");
|
|
|
45
47
|
const tokens_1 = require("../mind/friends/tokens");
|
|
46
48
|
const resolver_1 = require("../mind/friends/resolver");
|
|
47
49
|
const store_file_1 = require("../mind/friends/store-file");
|
|
50
|
+
const types_1 = require("../mind/friends/types");
|
|
51
|
+
const channel_1 = require("../mind/friends/channel");
|
|
52
|
+
const pending_1 = require("../mind/pending");
|
|
48
53
|
const prompt_1 = require("../mind/prompt");
|
|
49
54
|
const phrases_1 = require("../mind/phrases");
|
|
50
55
|
const runtime_1 = require("../nerves/runtime");
|
|
@@ -53,6 +58,8 @@ const bluebubbles_client_1 = require("./bluebubbles-client");
|
|
|
53
58
|
const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
|
|
54
59
|
const bluebubbles_session_cleanup_1 = require("./bluebubbles-session-cleanup");
|
|
55
60
|
const debug_activity_1 = require("./debug-activity");
|
|
61
|
+
const trust_gate_1 = require("./trust-gate");
|
|
62
|
+
const pipeline_1 = require("./pipeline");
|
|
56
63
|
const defaultDeps = {
|
|
57
64
|
getAgentName: identity_1.getAgentName,
|
|
58
65
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -84,6 +91,47 @@ function resolveFriendParams(event) {
|
|
|
84
91
|
channel: "bluebubbles",
|
|
85
92
|
};
|
|
86
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if any participant in a group chat is a known family member.
|
|
96
|
+
* Looks up each participant handle in the friend store.
|
|
97
|
+
*/
|
|
98
|
+
async function checkGroupHasFamilyMember(store, event) {
|
|
99
|
+
if (!event.chat.isGroup)
|
|
100
|
+
return false;
|
|
101
|
+
for (const handle of event.chat.participantHandles ?? []) {
|
|
102
|
+
const friend = await store.findByExternalId("imessage-handle", handle);
|
|
103
|
+
if (friend?.trustLevel === "family")
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if an acquaintance shares any group chat with a family member.
|
|
110
|
+
* Compares group-prefixed externalIds between the acquaintance and all family members.
|
|
111
|
+
*/
|
|
112
|
+
async function checkHasExistingGroupWithFamily(store, senderFriend) {
|
|
113
|
+
const trustLevel = senderFriend.trustLevel ?? "friend";
|
|
114
|
+
if (trustLevel !== "acquaintance")
|
|
115
|
+
return false;
|
|
116
|
+
const acquaintanceGroups = new Set((senderFriend.externalIds ?? [])
|
|
117
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
118
|
+
.map((eid) => eid.externalId));
|
|
119
|
+
if (acquaintanceGroups.size === 0)
|
|
120
|
+
return false;
|
|
121
|
+
const allFriends = await (store.listAll?.() ?? Promise.resolve([]));
|
|
122
|
+
for (const friend of allFriends) {
|
|
123
|
+
if (friend.trustLevel !== "family")
|
|
124
|
+
continue;
|
|
125
|
+
const friendGroups = (friend.externalIds ?? [])
|
|
126
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
127
|
+
.map((eid) => eid.externalId);
|
|
128
|
+
for (const group of friendGroups) {
|
|
129
|
+
if (acquaintanceGroups.has(group))
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
87
135
|
function extractMessageText(content) {
|
|
88
136
|
if (typeof content === "string")
|
|
89
137
|
return content;
|
|
@@ -412,28 +460,12 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
412
460
|
});
|
|
413
461
|
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
414
462
|
}
|
|
463
|
+
// ── Adapter setup: friend, session, content, callbacks ──────────
|
|
415
464
|
const store = resolvedDeps.createFriendStore();
|
|
416
465
|
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
417
|
-
const
|
|
466
|
+
const baseContext = await resolver.resolve();
|
|
467
|
+
const context = { ...baseContext, isGroupChat: event.chat.isGroup };
|
|
418
468
|
const replyTarget = createReplyTargetController(event);
|
|
419
|
-
const toolContext = {
|
|
420
|
-
signin: async () => undefined,
|
|
421
|
-
friendStore: store,
|
|
422
|
-
summarize: (0, core_1.createSummarize)(),
|
|
423
|
-
context,
|
|
424
|
-
bluebubblesReplyTarget: {
|
|
425
|
-
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
426
|
-
},
|
|
427
|
-
codingFeedback: {
|
|
428
|
-
send: async (message) => {
|
|
429
|
-
await client.sendText({
|
|
430
|
-
chat: event.chat,
|
|
431
|
-
text: message,
|
|
432
|
-
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
433
|
-
});
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
469
|
const friendId = context.friend.id;
|
|
438
470
|
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
439
471
|
try {
|
|
@@ -451,21 +483,87 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
451
483
|
},
|
|
452
484
|
});
|
|
453
485
|
}
|
|
486
|
+
// Pre-load session (adapter needs existing messages for lane history in content building)
|
|
454
487
|
const existing = resolvedDeps.loadSession(sessPath);
|
|
455
|
-
const
|
|
488
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
456
489
|
? existing.messages
|
|
457
490
|
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
|
|
458
|
-
|
|
491
|
+
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
492
|
+
const userMessage = {
|
|
493
|
+
role: "user",
|
|
494
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages),
|
|
495
|
+
};
|
|
459
496
|
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
|
|
460
497
|
const controller = new AbortController();
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
498
|
+
// BB-specific tool context wrappers
|
|
499
|
+
const summarize = (0, core_1.createSummarize)();
|
|
500
|
+
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
501
|
+
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
502
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
503
|
+
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
504
|
+
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
505
|
+
? false
|
|
506
|
+
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
507
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
464
508
|
try {
|
|
465
|
-
const result = await
|
|
509
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
510
|
+
channel: "bluebubbles",
|
|
511
|
+
capabilities: bbCapabilities,
|
|
512
|
+
messages: [userMessage],
|
|
513
|
+
callbacks,
|
|
514
|
+
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
515
|
+
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath }) },
|
|
516
|
+
pendingDir,
|
|
517
|
+
friendStore: store,
|
|
518
|
+
provider: "imessage-handle",
|
|
519
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
520
|
+
isGroupChat: event.chat.isGroup,
|
|
521
|
+
groupHasFamilyMember,
|
|
522
|
+
hasExistingGroupWithFamily,
|
|
523
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
524
|
+
drainPending: pending_1.drainPending,
|
|
525
|
+
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
526
|
+
...opts,
|
|
527
|
+
toolContext: {
|
|
528
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
529
|
+
signin: async () => undefined,
|
|
530
|
+
...opts?.toolContext,
|
|
531
|
+
summarize,
|
|
532
|
+
bluebubblesReplyTarget: {
|
|
533
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
534
|
+
},
|
|
535
|
+
codingFeedback: {
|
|
536
|
+
send: async (message) => {
|
|
537
|
+
await client.sendText({
|
|
538
|
+
chat: event.chat,
|
|
539
|
+
text: message,
|
|
540
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
postTurn: resolvedDeps.postTurn,
|
|
547
|
+
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
548
|
+
signal: controller.signal,
|
|
549
|
+
});
|
|
550
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
551
|
+
if (!result.gateResult.allowed) {
|
|
552
|
+
// Send auto-reply via BB API if the gate provides one
|
|
553
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
554
|
+
await client.sendText({
|
|
555
|
+
chat: event.chat,
|
|
556
|
+
text: result.gateResult.autoReply,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
handled: true,
|
|
561
|
+
notifiedAgent: false,
|
|
562
|
+
kind: event.kind,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// Gate allowed — flush the agent's reply
|
|
466
566
|
await callbacks.flush();
|
|
467
|
-
resolvedDeps.postTurn(messages, sessPath, result.usage);
|
|
468
|
-
await resolvedDeps.accumulateFriendTokens(store, friendId, result.usage);
|
|
469
567
|
(0, runtime_1.emitNervesEvent)({
|
|
470
568
|
component: "senses",
|
|
471
569
|
event: "senses.bluebubbles_turn_end",
|
|
@@ -541,6 +639,182 @@ function createBlueBubblesWebhookHandler(deps = {}) {
|
|
|
541
639
|
}
|
|
542
640
|
};
|
|
543
641
|
}
|
|
642
|
+
function findImessageHandle(friend) {
|
|
643
|
+
for (const ext of friend.externalIds) {
|
|
644
|
+
if (ext.provider === "imessage-handle" && !ext.externalId.startsWith("group:")) {
|
|
645
|
+
return ext.externalId;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
function scanPendingBlueBubblesFiles(pendingRoot) {
|
|
651
|
+
const results = [];
|
|
652
|
+
let friendIds;
|
|
653
|
+
try {
|
|
654
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return results;
|
|
658
|
+
}
|
|
659
|
+
for (const friendId of friendIds) {
|
|
660
|
+
const bbDir = path.join(pendingRoot, friendId, "bluebubbles");
|
|
661
|
+
let keys;
|
|
662
|
+
try {
|
|
663
|
+
keys = fs.readdirSync(bbDir);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
for (const key of keys) {
|
|
669
|
+
const keyDir = path.join(bbDir, key);
|
|
670
|
+
let files;
|
|
671
|
+
try {
|
|
672
|
+
files = fs.readdirSync(keyDir);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
678
|
+
const filePath = path.join(keyDir, file);
|
|
679
|
+
try {
|
|
680
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
681
|
+
results.push({ friendId, key, filePath, content });
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// skip unreadable files
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return results;
|
|
690
|
+
}
|
|
691
|
+
async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
|
|
692
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
693
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
694
|
+
const client = resolvedDeps.createClient();
|
|
695
|
+
const store = resolvedDeps.createFriendStore();
|
|
696
|
+
const pendingFiles = scanPendingBlueBubblesFiles(root);
|
|
697
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
698
|
+
for (const { friendId, filePath, content } of pendingFiles) {
|
|
699
|
+
let parsed;
|
|
700
|
+
try {
|
|
701
|
+
parsed = JSON.parse(content);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
result.failed++;
|
|
705
|
+
try {
|
|
706
|
+
fs.unlinkSync(filePath);
|
|
707
|
+
}
|
|
708
|
+
catch { /* ignore */ }
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
712
|
+
if (!messageText.trim()) {
|
|
713
|
+
result.skipped++;
|
|
714
|
+
try {
|
|
715
|
+
fs.unlinkSync(filePath);
|
|
716
|
+
}
|
|
717
|
+
catch { /* ignore */ }
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
let friend;
|
|
721
|
+
try {
|
|
722
|
+
friend = await store.get(friendId);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
friend = null;
|
|
726
|
+
}
|
|
727
|
+
if (!friend) {
|
|
728
|
+
result.skipped++;
|
|
729
|
+
try {
|
|
730
|
+
fs.unlinkSync(filePath);
|
|
731
|
+
}
|
|
732
|
+
catch { /* ignore */ }
|
|
733
|
+
(0, runtime_1.emitNervesEvent)({
|
|
734
|
+
level: "warn",
|
|
735
|
+
component: "senses",
|
|
736
|
+
event: "senses.bluebubbles_proactive_no_friend",
|
|
737
|
+
message: "proactive send skipped: friend not found",
|
|
738
|
+
meta: { friendId },
|
|
739
|
+
});
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
743
|
+
result.skipped++;
|
|
744
|
+
try {
|
|
745
|
+
fs.unlinkSync(filePath);
|
|
746
|
+
}
|
|
747
|
+
catch { /* ignore */ }
|
|
748
|
+
(0, runtime_1.emitNervesEvent)({
|
|
749
|
+
component: "senses",
|
|
750
|
+
event: "senses.bluebubbles_proactive_trust_skip",
|
|
751
|
+
message: "proactive send skipped: trust level not allowed",
|
|
752
|
+
meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
|
|
753
|
+
});
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const handle = findImessageHandle(friend);
|
|
757
|
+
if (!handle) {
|
|
758
|
+
result.skipped++;
|
|
759
|
+
try {
|
|
760
|
+
fs.unlinkSync(filePath);
|
|
761
|
+
}
|
|
762
|
+
catch { /* ignore */ }
|
|
763
|
+
(0, runtime_1.emitNervesEvent)({
|
|
764
|
+
level: "warn",
|
|
765
|
+
component: "senses",
|
|
766
|
+
event: "senses.bluebubbles_proactive_no_handle",
|
|
767
|
+
message: "proactive send skipped: no iMessage handle found",
|
|
768
|
+
meta: { friendId },
|
|
769
|
+
});
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const chat = {
|
|
773
|
+
chatIdentifier: handle,
|
|
774
|
+
isGroup: false,
|
|
775
|
+
sessionKey: friendId,
|
|
776
|
+
sendTarget: { kind: "chat_identifier", value: handle },
|
|
777
|
+
participantHandles: [],
|
|
778
|
+
};
|
|
779
|
+
try {
|
|
780
|
+
await client.sendText({ chat, text: messageText });
|
|
781
|
+
result.sent++;
|
|
782
|
+
try {
|
|
783
|
+
fs.unlinkSync(filePath);
|
|
784
|
+
}
|
|
785
|
+
catch { /* ignore */ }
|
|
786
|
+
(0, runtime_1.emitNervesEvent)({
|
|
787
|
+
component: "senses",
|
|
788
|
+
event: "senses.bluebubbles_proactive_sent",
|
|
789
|
+
message: "proactive bluebubbles message sent",
|
|
790
|
+
meta: { friendId, handle },
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
result.failed++;
|
|
795
|
+
(0, runtime_1.emitNervesEvent)({
|
|
796
|
+
level: "error",
|
|
797
|
+
component: "senses",
|
|
798
|
+
event: "senses.bluebubbles_proactive_send_error",
|
|
799
|
+
message: "proactive bluebubbles send failed",
|
|
800
|
+
meta: {
|
|
801
|
+
friendId,
|
|
802
|
+
handle,
|
|
803
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
809
|
+
(0, runtime_1.emitNervesEvent)({
|
|
810
|
+
component: "senses",
|
|
811
|
+
event: "senses.bluebubbles_proactive_drain_complete",
|
|
812
|
+
message: "bluebubbles proactive drain complete",
|
|
813
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
544
818
|
function startBlueBubblesApp(deps = {}) {
|
|
545
819
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
546
820
|
resolvedDeps.createClient();
|