@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (48) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +4 -1
  3. package/dist/heart/config.js +34 -0
  4. package/dist/heart/core.js +41 -2
  5. package/dist/heart/daemon/daemon-cli.js +293 -46
  6. package/dist/heart/daemon/daemon.js +3 -0
  7. package/dist/heart/daemon/hatch-animation.js +28 -0
  8. package/dist/heart/daemon/hatch-flow.js +3 -1
  9. package/dist/heart/daemon/hatch-specialist.js +6 -1
  10. package/dist/heart/daemon/log-tailer.js +146 -0
  11. package/dist/heart/daemon/os-cron.js +260 -0
  12. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  13. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  14. package/dist/heart/daemon/ouro-entry.js +0 -0
  15. package/dist/heart/daemon/ouro-path-installer.js +161 -0
  16. package/dist/heart/daemon/process-manager.js +18 -1
  17. package/dist/heart/daemon/runtime-logging.js +9 -5
  18. package/dist/heart/daemon/specialist-orchestrator.js +186 -0
  19. package/dist/heart/daemon/specialist-prompt.js +61 -0
  20. package/dist/heart/daemon/specialist-session.js +177 -0
  21. package/dist/heart/daemon/specialist-tools.js +132 -0
  22. package/dist/heart/daemon/task-scheduler.js +4 -1
  23. package/dist/heart/identity.js +28 -3
  24. package/dist/heart/providers/anthropic.js +3 -0
  25. package/dist/heart/streaming.js +3 -0
  26. package/dist/mind/associative-recall.js +23 -2
  27. package/dist/mind/context.js +85 -1
  28. package/dist/mind/friends/channel.js +8 -0
  29. package/dist/mind/friends/types.js +1 -1
  30. package/dist/mind/memory.js +62 -0
  31. package/dist/mind/pending.js +93 -0
  32. package/dist/mind/prompt-refresh.js +20 -0
  33. package/dist/mind/prompt.js +101 -0
  34. package/dist/nerves/coverage/file-completeness.js +14 -4
  35. package/dist/repertoire/tools-base.js +92 -0
  36. package/dist/repertoire/tools.js +3 -3
  37. package/dist/senses/bluebubbles-client.js +279 -0
  38. package/dist/senses/bluebubbles-entry.js +11 -0
  39. package/dist/senses/bluebubbles-model.js +253 -0
  40. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  41. package/dist/senses/bluebubbles.js +332 -0
  42. package/dist/senses/cli.js +89 -8
  43. package/dist/senses/inner-dialog.js +15 -0
  44. package/dist/senses/session-lock.js +119 -0
  45. package/dist/senses/teams.js +1 -0
  46. package/package.json +4 -3
  47. package/subagents/README.md +3 -1
  48. package/subagents/work-merger.md +33 -2
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resetPsycheCache = resetPsycheCache;
37
+ exports.buildSessionSummary = buildSessionSummary;
37
38
  exports.runtimeInfoSection = runtimeInfoSection;
38
39
  exports.contextSection = contextSection;
39
40
  exports.buildSystem = buildSystem;
@@ -43,6 +44,7 @@ const core_1 = require("../heart/core");
43
44
  const tools_1 = require("../repertoire/tools");
44
45
  const skills_1 = require("../repertoire/skills");
45
46
  const identity_1 = require("../heart/identity");
47
+ const os = __importStar(require("os"));
46
48
  const channel_1 = require("./friends/channel");
47
49
  const runtime_1 = require("../nerves/runtime");
48
50
  const first_impressions_1 = require("./first-impressions");
@@ -73,6 +75,94 @@ function loadPsyche() {
73
75
  function resetPsycheCache() {
74
76
  _psycheCache = null;
75
77
  }
78
+ const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
79
+ function resolveFriendName(friendId, friendsDir, agentName) {
80
+ if (friendId === "self")
81
+ return agentName;
82
+ try {
83
+ const raw = fs.readFileSync(path.join(friendsDir, `${friendId}.json`), "utf-8");
84
+ const record = JSON.parse(raw);
85
+ return record.name ?? friendId;
86
+ }
87
+ catch {
88
+ return friendId;
89
+ }
90
+ }
91
+ function buildSessionSummary(options) {
92
+ const { sessionsDir, friendsDir, agentName, currentFriendId, currentChannel, currentKey, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, } = options;
93
+ if (!fs.existsSync(sessionsDir))
94
+ return "";
95
+ const now = Date.now();
96
+ const entries = [];
97
+ let friendDirs;
98
+ try {
99
+ friendDirs = fs.readdirSync(sessionsDir);
100
+ }
101
+ catch {
102
+ return "";
103
+ }
104
+ for (const friendId of friendDirs) {
105
+ const friendPath = path.join(sessionsDir, friendId);
106
+ let channels;
107
+ try {
108
+ channels = fs.readdirSync(friendPath);
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ for (const channel of channels) {
114
+ const channelPath = path.join(friendPath, channel);
115
+ let keys;
116
+ try {
117
+ keys = fs.readdirSync(channelPath);
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ for (const keyFile of keys) {
123
+ if (!keyFile.endsWith(".json"))
124
+ continue;
125
+ const key = keyFile.replace(/\.json$/, "");
126
+ // Exclude current session
127
+ if (friendId === currentFriendId && channel === currentChannel && key === currentKey) {
128
+ continue;
129
+ }
130
+ const filePath = path.join(channelPath, keyFile);
131
+ let mtimeMs;
132
+ try {
133
+ mtimeMs = fs.statSync(filePath).mtimeMs;
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ if (now - mtimeMs > activeThresholdMs)
139
+ continue;
140
+ const displayName = resolveFriendName(friendId, friendsDir, agentName);
141
+ entries.push({ friendId, displayName, channel, key, lastActivityMs: mtimeMs });
142
+ }
143
+ }
144
+ }
145
+ if (entries.length === 0)
146
+ return "";
147
+ // Sort by most recent first
148
+ entries.sort((a, b) => b.lastActivityMs - a.lastActivityMs);
149
+ const lines = ["## active sessions"];
150
+ for (const entry of entries) {
151
+ const ago = formatTimeAgo(now - entry.lastActivityMs);
152
+ lines.push(`- ${entry.displayName}/${entry.channel}/${entry.key} (last: ${ago})`);
153
+ }
154
+ return lines.join("\n");
155
+ }
156
+ function formatTimeAgo(ms) {
157
+ const minutes = Math.floor(ms / 60000);
158
+ if (minutes < 60)
159
+ return `${minutes}m ago`;
160
+ const hours = Math.floor(minutes / 60);
161
+ if (hours < 24)
162
+ return `${hours}h ago`;
163
+ const days = Math.floor(hours / 24);
164
+ return `${days}d ago`;
165
+ }
76
166
  function soulSection() {
77
167
  return loadPsyche().soul;
78
168
  }
@@ -108,6 +198,9 @@ function runtimeInfoSection(channel) {
108
198
  if (channel === "cli") {
109
199
  lines.push("i introduce myself on boot with a fun random greeting.");
110
200
  }
201
+ else if (channel === "bluebubbles") {
202
+ lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
203
+ }
111
204
  else {
112
205
  lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
113
206
  }
@@ -238,6 +331,14 @@ async function buildSystem(channel = "cli", options, context) {
238
331
  toolsSection(channel, options),
239
332
  skillsSection(),
240
333
  taskBoardSection(),
334
+ buildSessionSummary({
335
+ sessionsDir: path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions"),
336
+ friendsDir: path.join((0, identity_1.getAgentRoot)(), "friends"),
337
+ agentName: (0, identity_1.getAgentName)(),
338
+ currentFriendId: context?.friend?.id,
339
+ currentChannel: channel,
340
+ currentKey: "session",
341
+ }),
241
342
  memoryFriendToolContractSection(),
242
343
  toolBehaviorSection(options),
243
344
  contextSection(context),
@@ -11,12 +11,22 @@ exports.isTypeOnlyFile = isTypeOnlyFile;
11
11
  exports.checkFileCompleteness = checkFileCompleteness;
12
12
  /**
13
13
  * Determines if a source file is type-only (no executable code).
14
- * A file is type-only if it contains no function, class, or const declarations.
14
+ * A file is type-only if it contains no function, class, or mutable declarations.
15
+ * `const ... as const` declarations are treated as type-equivalent (frozen
16
+ * compile-time values with no side effects).
15
17
  */
16
18
  function isTypeOnlyFile(source) {
17
- // Look for executable code markers: function, class, const/let/var declarations
18
- const executablePattern = /\b(function|class|const|let|var)\s/;
19
- return !executablePattern.test(source);
19
+ const lines = source.split("\n");
20
+ for (const line of lines) {
21
+ const trimmed = line.trim();
22
+ // Skip lines that are const+as-const (type-equivalent frozen values)
23
+ if (/\bconst\s/.test(trimmed) && /\bas\s+const\b/.test(trimmed))
24
+ continue;
25
+ // Check for executable code markers
26
+ if (/\b(function|class|const|let|var)\s/.test(trimmed))
27
+ return false;
28
+ }
29
+ return true;
20
30
  }
21
31
  /**
22
32
  * Check that all production files have at least one emitNervesEvent call.
@@ -41,6 +41,7 @@ const skills_1 = require("./skills");
41
41
  const config_1 = require("../heart/config");
42
42
  const runtime_1 = require("../nerves/runtime");
43
43
  const identity_1 = require("../heart/identity");
44
+ const os = __importStar(require("os"));
44
45
  const tasks_1 = require("./tasks");
45
46
  const tools_1 = require("./coding/tools");
46
47
  const memory_1 = require("../mind/memory");
@@ -602,6 +603,97 @@ exports.baseToolDefinitions = [
602
603
  }
603
604
  },
604
605
  },
606
+ // -- cross-session awareness --
607
+ {
608
+ tool: {
609
+ type: "function",
610
+ function: {
611
+ name: "query_session",
612
+ description: "read the last messages from another session. use this to check on a conversation with a friend or review your own inner dialog.",
613
+ parameters: {
614
+ type: "object",
615
+ properties: {
616
+ friendId: { type: "string", description: "the friend UUID (or 'self')" },
617
+ channel: { type: "string", description: "the channel: cli, teams, or inner" },
618
+ key: { type: "string", description: "session key (defaults to 'session')" },
619
+ messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
620
+ },
621
+ required: ["friendId", "channel"],
622
+ },
623
+ },
624
+ },
625
+ handler: async (args, ctx) => {
626
+ try {
627
+ const friendId = args.friendId;
628
+ const channel = args.channel;
629
+ const key = args.key || "session";
630
+ const count = parseInt(args.messageCount || "20", 10);
631
+ const sessFile = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel, `${key}.json`);
632
+ const raw = fs.readFileSync(sessFile, "utf-8");
633
+ const data = JSON.parse(raw);
634
+ const messages = (data.messages || [])
635
+ .filter((m) => m.role !== "system");
636
+ const tail = messages.slice(-count);
637
+ if (tail.length === 0)
638
+ return "session exists but has no non-system messages.";
639
+ const transcript = tail.map((m) => `[${m.role}] ${m.content}`).join("\n");
640
+ // LLM summarization when summarize function is available
641
+ if (ctx?.summarize) {
642
+ const trustLevel = ctx.context?.friend?.trustLevel ?? "family";
643
+ const isSelfQuery = friendId === "self";
644
+ const instruction = isSelfQuery
645
+ ? "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning."
646
+ : `summarize this session transcript. the person asking has trust level: ${trustLevel}. family=full transparency, friend=share work and general topics but protect other people's identities, acquaintance=very guarded minimal disclosure.`;
647
+ return await ctx.summarize(transcript, instruction);
648
+ }
649
+ return transcript;
650
+ }
651
+ catch {
652
+ return "no session found for that friend/channel/key combination.";
653
+ }
654
+ },
655
+ },
656
+ {
657
+ tool: {
658
+ type: "function",
659
+ function: {
660
+ name: "send_message",
661
+ description: "send a message to a friend's session. the message is queued as a pending file and delivered when the target session drains its queue.",
662
+ parameters: {
663
+ type: "object",
664
+ properties: {
665
+ friendId: { type: "string", description: "the friend UUID (or 'self')" },
666
+ channel: { type: "string", description: "the channel: cli, teams, or inner" },
667
+ key: { type: "string", description: "session key (defaults to 'session')" },
668
+ content: { type: "string", description: "the message content to send" },
669
+ },
670
+ required: ["friendId", "channel", "content"],
671
+ },
672
+ },
673
+ },
674
+ handler: async (args) => {
675
+ const friendId = args.friendId;
676
+ const channel = args.channel;
677
+ const key = args.key || "session";
678
+ const content = args.content;
679
+ const now = Date.now();
680
+ const pendingDir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "pending", friendId, channel, key);
681
+ fs.mkdirSync(pendingDir, { recursive: true });
682
+ const fileName = `${now}-${Math.random().toString(36).slice(2, 10)}.json`;
683
+ const filePath = path.join(pendingDir, fileName);
684
+ const envelope = {
685
+ from: (0, identity_1.getAgentName)(),
686
+ friendId,
687
+ channel,
688
+ key,
689
+ content,
690
+ timestamp: now,
691
+ };
692
+ fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
693
+ const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
694
+ return `message queued for delivery to ${friendId} on ${channel}/${key}. preview: "${preview}". it will be delivered when their session is next active.`;
695
+ },
696
+ },
605
697
  ...tools_1.codingToolDefinitions,
606
698
  ];
607
699
  // Backward-compat: extract just the OpenAI tool schemas
@@ -20,7 +20,7 @@ Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function (
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
22
  function baseToolsForCapabilities(capabilities) {
23
- const isRemoteChannel = capabilities?.channel === "teams";
23
+ const isRemoteChannel = capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
24
24
  if (!isRemoteChannel)
25
25
  return tools_base_1.tools;
26
26
  return tools_base_1.tools.filter((tool) => !REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
@@ -87,7 +87,7 @@ async function execTool(name, args, ctx) {
87
87
  });
88
88
  return `unknown: ${name}`;
89
89
  }
90
- const isRemoteChannel = ctx?.context?.channel?.channel === "teams";
90
+ const isRemoteChannel = ctx?.context?.channel?.channel === "teams" || ctx?.context?.channel?.channel === "bluebubbles";
91
91
  if (isRemoteChannel && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
92
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.";
93
93
  (0, runtime_1.emitNervesEvent)({
@@ -95,7 +95,7 @@ async function execTool(name, args, ctx) {
95
95
  event: "tool.error",
96
96
  component: "tools",
97
97
  message: "blocked local tool in remote channel",
98
- meta: { name, channel: "teams" },
98
+ meta: { name, channel: ctx?.context?.channel?.channel },
99
99
  });
100
100
  return message;
101
101
  }
@@ -0,0 +1,279 @@
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 runtime_1 = require("../nerves/runtime");
7
+ const bluebubbles_model_1 = require("./bluebubbles-model");
8
+ function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
9
+ const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
10
+ const url = new URL(endpoint.replace(/^\//, ""), root);
11
+ url.searchParams.set("password", password);
12
+ return url.toString();
13
+ }
14
+ function asRecord(value) {
15
+ return value && typeof value === "object" && !Array.isArray(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function extractMessageGuid(payload) {
20
+ if (!payload || typeof payload !== "object")
21
+ return undefined;
22
+ const record = payload;
23
+ const data = record.data && typeof record.data === "object" && !Array.isArray(record.data)
24
+ ? record.data
25
+ : null;
26
+ const candidates = [
27
+ record.messageGuid,
28
+ record.messageId,
29
+ record.guid,
30
+ data?.messageGuid,
31
+ data?.messageId,
32
+ data?.guid,
33
+ ];
34
+ for (const candidate of candidates) {
35
+ if (typeof candidate === "string" && candidate.trim()) {
36
+ return candidate.trim();
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+ async function parseJsonBody(response) {
42
+ const raw = await response.text();
43
+ if (!raw.trim())
44
+ return null;
45
+ try {
46
+ return JSON.parse(raw);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function buildRepairUrl(baseUrl, messageGuid, password) {
53
+ const url = buildBlueBubblesApiUrl(baseUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}`, password);
54
+ const parsed = new URL(url);
55
+ parsed.searchParams.set("with", "attachments,payloadData,chats,messageSummaryInfo");
56
+ return parsed.toString();
57
+ }
58
+ function collectPreviewStrings(value, out, depth = 0) {
59
+ if (depth > 4 || out.length >= 4)
60
+ return;
61
+ if (typeof value === "string") {
62
+ const trimmed = value.trim();
63
+ if (trimmed)
64
+ out.push(trimmed);
65
+ return;
66
+ }
67
+ if (Array.isArray(value)) {
68
+ for (const entry of value)
69
+ collectPreviewStrings(entry, out, depth + 1);
70
+ return;
71
+ }
72
+ const record = asRecord(value);
73
+ if (!record)
74
+ return;
75
+ const preferredKeys = ["title", "summary", "subtitle", "previewText", "siteName", "host", "url"];
76
+ for (const key of preferredKeys) {
77
+ if (out.length >= 4)
78
+ break;
79
+ collectPreviewStrings(record[key], out, depth + 1);
80
+ }
81
+ }
82
+ function extractLinkPreviewText(data) {
83
+ const values = [];
84
+ collectPreviewStrings(data.payloadData, values);
85
+ collectPreviewStrings(data.messageSummaryInfo, values);
86
+ const unique = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
87
+ if (unique.length === 0)
88
+ return undefined;
89
+ return unique.slice(0, 2).join(" — ");
90
+ }
91
+ function applyRepairNotice(event, notice) {
92
+ return {
93
+ ...event,
94
+ requiresRepair: false,
95
+ repairNotice: notice,
96
+ };
97
+ }
98
+ function hydrateTextForAgent(event, rawData) {
99
+ if (event.kind !== "message") {
100
+ return { ...event, requiresRepair: false };
101
+ }
102
+ if (event.balloonBundleId !== "com.apple.messages.URLBalloonProvider") {
103
+ return { ...event, requiresRepair: false };
104
+ }
105
+ const previewText = extractLinkPreviewText(rawData);
106
+ if (!previewText) {
107
+ return { ...event, requiresRepair: false };
108
+ }
109
+ const base = event.text.trim();
110
+ const textForAgent = base
111
+ ? `${base}\n[link preview: ${previewText}]`
112
+ : `[link preview: ${previewText}]`;
113
+ return {
114
+ ...event,
115
+ textForAgent,
116
+ requiresRepair: false,
117
+ };
118
+ }
119
+ function extractRepairData(payload) {
120
+ const record = asRecord(payload);
121
+ return asRecord(record?.data) ?? record;
122
+ }
123
+ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)()) {
124
+ return {
125
+ async sendText(params) {
126
+ const trimmedText = params.text.trim();
127
+ if (!trimmedText) {
128
+ throw new Error("BlueBubbles send requires non-empty text.");
129
+ }
130
+ if (!params.chat.chatGuid) {
131
+ throw new Error("BlueBubbles send currently requires chat.chatGuid from the inbound event.");
132
+ }
133
+ const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/text", config.password);
134
+ const body = {
135
+ chatGuid: params.chat.chatGuid,
136
+ tempGuid: (0, node_crypto_1.randomUUID)(),
137
+ message: trimmedText,
138
+ };
139
+ if (params.replyToMessageGuid?.trim()) {
140
+ body.method = "private-api";
141
+ body.selectedMessageGuid = params.replyToMessageGuid.trim();
142
+ body.partIndex = 0;
143
+ }
144
+ (0, runtime_1.emitNervesEvent)({
145
+ component: "senses",
146
+ event: "senses.bluebubbles_send_start",
147
+ message: "sending bluebubbles message",
148
+ meta: {
149
+ chatGuid: params.chat.chatGuid,
150
+ hasReplyTarget: Boolean(params.replyToMessageGuid?.trim()),
151
+ },
152
+ });
153
+ const response = await fetch(url, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify(body),
157
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
158
+ });
159
+ if (!response.ok) {
160
+ const errorText = await response.text().catch(() => "");
161
+ (0, runtime_1.emitNervesEvent)({
162
+ level: "error",
163
+ component: "senses",
164
+ event: "senses.bluebubbles_send_error",
165
+ message: "bluebubbles send failed",
166
+ meta: {
167
+ status: response.status,
168
+ reason: errorText || "unknown",
169
+ },
170
+ });
171
+ throw new Error(`BlueBubbles send failed (${response.status}): ${errorText || "unknown"}`);
172
+ }
173
+ const payload = await parseJsonBody(response);
174
+ const messageGuid = extractMessageGuid(payload);
175
+ (0, runtime_1.emitNervesEvent)({
176
+ component: "senses",
177
+ event: "senses.bluebubbles_send_end",
178
+ message: "bluebubbles message sent",
179
+ meta: {
180
+ chatGuid: params.chat.chatGuid,
181
+ messageGuid: messageGuid ?? null,
182
+ },
183
+ });
184
+ return { messageGuid };
185
+ },
186
+ async repairEvent(event) {
187
+ if (!event.requiresRepair) {
188
+ (0, runtime_1.emitNervesEvent)({
189
+ component: "senses",
190
+ event: "senses.bluebubbles_repair_skipped",
191
+ message: "bluebubbles event repair skipped",
192
+ meta: {
193
+ kind: event.kind,
194
+ messageGuid: event.messageGuid,
195
+ },
196
+ });
197
+ return event;
198
+ }
199
+ (0, runtime_1.emitNervesEvent)({
200
+ component: "senses",
201
+ event: "senses.bluebubbles_repair_start",
202
+ message: "repairing bluebubbles event by guid",
203
+ meta: {
204
+ kind: event.kind,
205
+ messageGuid: event.messageGuid,
206
+ eventType: event.eventType,
207
+ },
208
+ });
209
+ const url = buildRepairUrl(config.serverUrl, event.messageGuid, config.password);
210
+ try {
211
+ const response = await fetch(url, {
212
+ method: "GET",
213
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
214
+ });
215
+ if (!response.ok) {
216
+ const errorText = await response.text().catch(() => "");
217
+ const repaired = applyRepairNotice(event, `BlueBubbles repair failed: ${errorText || `HTTP ${response.status}`}`);
218
+ (0, runtime_1.emitNervesEvent)({
219
+ level: "warn",
220
+ component: "senses",
221
+ event: "senses.bluebubbles_repair_error",
222
+ message: "bluebubbles repair request failed",
223
+ meta: {
224
+ messageGuid: event.messageGuid,
225
+ status: response.status,
226
+ reason: errorText || "unknown",
227
+ },
228
+ });
229
+ return repaired;
230
+ }
231
+ const payload = await parseJsonBody(response);
232
+ const data = extractRepairData(payload);
233
+ if (!data || typeof data.guid !== "string") {
234
+ const repaired = applyRepairNotice(event, "BlueBubbles repair failed: invalid message payload");
235
+ (0, runtime_1.emitNervesEvent)({
236
+ level: "warn",
237
+ component: "senses",
238
+ event: "senses.bluebubbles_repair_error",
239
+ message: "bluebubbles repair returned unusable payload",
240
+ meta: {
241
+ messageGuid: event.messageGuid,
242
+ },
243
+ });
244
+ return repaired;
245
+ }
246
+ const normalized = (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
247
+ type: event.eventType,
248
+ data,
249
+ });
250
+ const hydrated = hydrateTextForAgent(normalized, data);
251
+ (0, runtime_1.emitNervesEvent)({
252
+ component: "senses",
253
+ event: "senses.bluebubbles_repair_end",
254
+ message: "bluebubbles event repaired",
255
+ meta: {
256
+ kind: hydrated.kind,
257
+ messageGuid: hydrated.messageGuid,
258
+ repairedFrom: event.kind,
259
+ },
260
+ });
261
+ return hydrated;
262
+ }
263
+ catch (error) {
264
+ const reason = error instanceof Error ? error.message : String(error);
265
+ (0, runtime_1.emitNervesEvent)({
266
+ level: "warn",
267
+ component: "senses",
268
+ event: "senses.bluebubbles_repair_error",
269
+ message: "bluebubbles repair threw",
270
+ meta: {
271
+ messageGuid: event.messageGuid,
272
+ reason,
273
+ },
274
+ });
275
+ return applyRepairNotice(event, `BlueBubbles repair failed: ${reason}`);
276
+ }
277
+ },
278
+ };
279
+ }
@@ -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)();