@ouro.bot/cli 0.1.0-alpha.891 → 0.1.0-alpha.893

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.
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CmuxSocketError = void 0;
37
+ exports.createCmuxClient = createCmuxClient;
38
+ const node_crypto_1 = require("node:crypto");
39
+ const net = __importStar(require("node:net"));
40
+ const cmux_config_1 = require("../../heart/cmux-config");
41
+ const runtime_1 = require("../../nerves/runtime");
42
+ class CmuxSocketError extends Error {
43
+ code;
44
+ constructor(code, message) {
45
+ super(message);
46
+ this.code = code;
47
+ this.name = "CmuxSocketError";
48
+ }
49
+ }
50
+ exports.CmuxSocketError = CmuxSocketError;
51
+ const ACCESS_REFUSED = /access denied|authentication required|invalid password|not authorized|unauthori[sz]ed/i;
52
+ function isRecord(value) {
53
+ return !!value && typeof value === "object" && !Array.isArray(value);
54
+ }
55
+ function parseRecord(line) {
56
+ try {
57
+ const value = JSON.parse(line);
58
+ return isRecord(value) ? value : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function createCmuxClient(connection, options = {}) {
65
+ const timeoutMs = options.timeoutMs ?? 5_000;
66
+ const repair = (0, cmux_config_1.cmuxRepairHint)(options.agentName ?? "<agent>");
67
+ const wrap = (line) => connection.auth.kind === "capability"
68
+ ? `_cmux_capability_v1 ${connection.auth.token} ${line}`
69
+ : line;
70
+ const refusal = (text) => text.startsWith("ERROR:") && ACCESS_REFUSED.test(text)
71
+ ? new CmuxSocketError("auth", `cmux refused the socket connection (${text.slice(0, 200)}); ${repair}`)
72
+ : null;
73
+ const connectError = (error) => new CmuxSocketError("unavailable", `cmux socket is not reachable at ${connection.socketPath} (${String(error.code)}); is cmux running?`);
74
+ /** Opens a connection, runs the password handshake when needed, and hands back a line reader. */
75
+ function open(onLine, onEnd) {
76
+ return new Promise((resolve) => {
77
+ const socket = net.createConnection(connection.socketPath);
78
+ let buffer = "";
79
+ let handshake = connection.auth.kind === "password";
80
+ socket.setEncoding("utf-8");
81
+ socket.on("error", (error) => onEnd(connectError(error)));
82
+ socket.on("close", () => onEnd());
83
+ socket.on("data", (chunk) => {
84
+ buffer += chunk;
85
+ let index = buffer.indexOf("\n");
86
+ while (index >= 0) {
87
+ const line = buffer.slice(0, index).trim();
88
+ buffer = buffer.slice(index + 1);
89
+ index = buffer.indexOf("\n");
90
+ if (!line)
91
+ continue;
92
+ if (handshake) {
93
+ handshake = false;
94
+ if (line.startsWith("ERROR:") && !line.includes("Unknown command 'auth'")) {
95
+ socket.destroy();
96
+ onEnd(new CmuxSocketError("auth", `cmux rejected the socket password (${line.slice(0, 200)}); ${repair}`));
97
+ continue;
98
+ }
99
+ resolve(socket);
100
+ continue;
101
+ }
102
+ onLine(line);
103
+ }
104
+ });
105
+ socket.on("connect", () => {
106
+ if (connection.auth.kind === "password")
107
+ socket.write(`auth ${connection.auth.password}\n`);
108
+ else
109
+ resolve(socket);
110
+ });
111
+ });
112
+ }
113
+ function request(line) {
114
+ return new Promise((resolve, reject) => {
115
+ let settled = false;
116
+ let socket = null;
117
+ const finish = (error, value) => {
118
+ if (settled)
119
+ return;
120
+ settled = true;
121
+ clearTimeout(timer);
122
+ socket?.destroy();
123
+ if (error)
124
+ reject(error);
125
+ else
126
+ resolve(value);
127
+ };
128
+ const timer = setTimeout(() => finish(new CmuxSocketError("timeout", `cmux did not answer within ${timeoutMs} ms`)), timeoutMs);
129
+ void open((response) => finish(refusal(response), response), (error) => finish(error ?? new CmuxSocketError("unavailable", "cmux closed the socket before answering"))).then((opened) => {
130
+ socket = opened;
131
+ opened.write(`${wrap(line)}\n`);
132
+ });
133
+ });
134
+ }
135
+ const v2Line = (method, params) => JSON.stringify({ id: (0, node_crypto_1.randomUUID)(), method, params });
136
+ function parseV2(text) {
137
+ const parsed = parseRecord(text);
138
+ if (!parsed)
139
+ throw new CmuxSocketError(text.startsWith("ERROR:") ? "command_failed" : "protocol", `unexpected cmux response: ${text.slice(0, 200)}`);
140
+ if (parsed.ok === true)
141
+ return isRecord(parsed.result) ? parsed.result : {};
142
+ const error = isRecord(parsed.error) ? parsed.error : {};
143
+ throw new CmuxSocketError(typeof error.code === "string" ? error.code : "error", typeof error.message === "string" && error.message ? error.message : "cmux error");
144
+ }
145
+ return {
146
+ async call(method, params = {}) {
147
+ const text = await request(v2Line(method, params));
148
+ const result = parseV2(text);
149
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_call", message: "cmux socket call answered", meta: { method } });
150
+ return result;
151
+ },
152
+ async command(line) {
153
+ const text = await request(line);
154
+ if (text.startsWith("ERROR:"))
155
+ throw new CmuxSocketError("command_failed", text.slice(0, 500));
156
+ return text;
157
+ },
158
+ stream(params, handlers) {
159
+ let closed = false;
160
+ let socket = null;
161
+ const end = (error) => {
162
+ if (closed)
163
+ return;
164
+ closed = true;
165
+ socket?.destroy();
166
+ handlers.onClose(error);
167
+ };
168
+ void open((line) => {
169
+ if (closed)
170
+ return;
171
+ const refused = refusal(line);
172
+ if (refused)
173
+ return end(refused);
174
+ const frame = parseRecord(line);
175
+ if (!frame)
176
+ return end(new CmuxSocketError("protocol", `unexpected cmux event frame: ${line.slice(0, 200)}`));
177
+ if (frame.ok === false) {
178
+ try {
179
+ parseV2(line);
180
+ }
181
+ catch (error) {
182
+ return end(error);
183
+ }
184
+ }
185
+ handlers.onFrame(frame);
186
+ }, end).then((opened) => {
187
+ socket = opened;
188
+ if (closed)
189
+ opened.destroy();
190
+ else
191
+ opened.write(`${wrap(v2Line("events.stream", params))}\n`);
192
+ });
193
+ return { close: () => end() };
194
+ },
195
+ };
196
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactSecrets = redactSecrets;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ /**
6
+ * Terminal screens and Feed tool input can show credentials. Everything the cmux sense hands to a
7
+ * model (screen text, request previews) goes through this first. It is a best-effort net for
8
+ * secret-shaped strings, not a guarantee, so callers also keep their output bounded.
9
+ */
10
+ const REDACTED = "[redacted]";
11
+ const WHOLE_MATCH = [
12
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
13
+ /\bsk-[A-Za-z0-9_-]{16,}/g,
14
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/g,
15
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
16
+ /\bAKIA[0-9A-Z]{16}\b/g,
17
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
18
+ /\bv1\.[A-Za-z0-9_-]{40,}\.[A-Za-z0-9_-]{40,}/g,
19
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
20
+ ];
21
+ /** Keep the label (group 1), redact the value. */
22
+ const LABELLED_VALUE = [
23
+ /(\b(?:Bearer|Basic)\s+)[A-Za-z0-9._~+/=-]{6,}/gi,
24
+ /(\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s'"&]+)/gi,
25
+ /([?&](?:token|key|api_key|access_token|sig|signature)=)[^&\s]+/gi,
26
+ ];
27
+ function redactSecrets(text) {
28
+ let result = text;
29
+ for (const pattern of WHOLE_MATCH)
30
+ result = result.replace(pattern, REDACTED);
31
+ for (const pattern of LABELLED_VALUE)
32
+ result = result.replace(pattern, `$1${REDACTED}`);
33
+ if (result !== text) {
34
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_redacted", message: "redacted secret-shaped text from cmux output", meta: { removedChars: text.length - result.length } });
35
+ }
36
+ return result;
37
+ }
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CMUX_WAKE_TRIGGER = void 0;
37
+ exports.cmuxStatePath = cmuxStatePath;
38
+ exports.queueEscalation = queueEscalation;
39
+ exports.wakeForEscalations = wakeForEscalations;
40
+ exports.startCmuxSenseApp = startCmuxSenseApp;
41
+ const node_crypto_1 = require("node:crypto");
42
+ const path = __importStar(require("node:path"));
43
+ const cmux_config_1 = require("../../heart/cmux-config");
44
+ const socket_client_1 = require("../../heart/daemon/socket-client");
45
+ const identity_1 = require("../../heart/identity");
46
+ const runtime_credentials_1 = require("../../heart/runtime-credentials");
47
+ const pending_1 = require("../../mind/pending");
48
+ const runtime_1 = require("../../nerves/runtime");
49
+ const attention_1 = require("./attention");
50
+ const client_1 = require("./client");
51
+ const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
52
+ const FEED_POLL_MS = 30_000;
53
+ const SAVE_DELAY_MS = 1_000;
54
+ function cmuxStatePath(agentName) {
55
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "cmux", "state.json");
56
+ }
57
+ /** The private-turn trigger the daemon's policy accepts for a cmux escalation wake. */
58
+ exports.CMUX_WAKE_TRIGGER = "cmux-feed";
59
+ /** cmux waits about 120 seconds; a notice the agent has not read within 30 minutes is dropped unread. */
60
+ const ESCALATION_TTL_MS = 30 * 60_000;
61
+ /** Queues the escalation in the agent's private runtime, as the mail sense queues its notices. */
62
+ function queueEscalation(agent, escalation, nowMs) {
63
+ (0, pending_1.queuePendingMessage)((0, pending_1.getPrivateRuntimePendingDir)(agent), {
64
+ from: "cmux",
65
+ friendId: "self",
66
+ channel: "cmux",
67
+ key: "feed",
68
+ content: escalation.content,
69
+ timestamp: nowMs,
70
+ expiresAt: nowMs + ESCALATION_TTL_MS,
71
+ mode: "reflect",
72
+ });
73
+ }
74
+ /** How many request ids one wake names in its origin references. */
75
+ const WAKE_REF_LIMIT = 20;
76
+ /**
77
+ * Asks the daemon for one private turn for everything a check pass queued: the turn reads every
78
+ * pending message at once, so a burst of Feed requests costs one turn, not one each. A refused or
79
+ * failed wake leaves the messages for the next private turn.
80
+ */
81
+ async function wakeForEscalations(agent, requestIds) {
82
+ const key = requestIds.length === 1 ? requestIds[0] : `${requestIds.length}:${(0, node_crypto_1.createHash)("sha256").update(requestIds.join("\n")).digest("hex").slice(0, 32)}`;
83
+ try {
84
+ const response = await (0, socket_client_1.requestPrivateWake)(agent, undefined, {
85
+ reason: requestIds.length === 1 ? "cmux Feed request" : `${requestIds.length} cmux Feed requests`,
86
+ triggerSource: exports.CMUX_WAKE_TRIGGER,
87
+ budgetClass: "interactive",
88
+ idempotencyKey: `cmux-feed:${agent}:${key}`,
89
+ originRefs: [...requestIds.slice(0, WAKE_REF_LIMIT).map((id) => ({ kind: "cmux-feed", id })), { kind: "sense", id: "cmux" }],
90
+ });
91
+ if (response && !response.ok)
92
+ throw new Error(response.error ?? "the daemon refused the wake");
93
+ }
94
+ catch (error) {
95
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_wake_error", message: "could not wake the private runtime for cmux escalations; they wait for the next private turn", meta: { agent, error: error.message } });
96
+ }
97
+ }
98
+ async function startCmuxSenseApp(options) {
99
+ const agent = options.agentName;
100
+ const nowMs = options.now ?? Date.now;
101
+ const now = () => new Date(nowMs()).toISOString();
102
+ const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
103
+ const resolved = (0, cmux_config_1.resolveCmuxConnection)(agent, machine.ok ? machine.config : {});
104
+ if (!resolved.ok)
105
+ throw new Error(resolved.error);
106
+ const client = (options.createClient ?? ((connection) => (0, client_1.createCmuxClient)(connection, { agentName: agent })))(resolved.connection);
107
+ const escalate = options.escalate ?? ((escalation) => queueEscalation(agent, escalation, nowMs()));
108
+ const wake = options.wake ?? ((requestIds) => wakeForEscalations(agent, requestIds));
109
+ const schedule = options.schedule ?? ((fn, ms) => {
110
+ const timer = setTimeout(fn, ms);
111
+ return () => clearTimeout(timer);
112
+ });
113
+ const statePath = cmuxStatePath(agent);
114
+ const state = (0, attention_1.readCmuxState)(statePath, now());
115
+ state.connected = false;
116
+ let stopped = false;
117
+ let stream = null;
118
+ let attempt = 0;
119
+ let cancelReconnect = null;
120
+ let cancelPoll = null;
121
+ let cancelSave = null;
122
+ let feedChain = Promise.resolve();
123
+ let feedQueued = false;
124
+ let lastWritten = null;
125
+ /** Writes the state file only when something other than the timestamp changed. */
126
+ const save = () => {
127
+ cancelSave?.();
128
+ cancelSave = null;
129
+ const body = JSON.stringify({ ...state, updatedAt: null });
130
+ if (body === lastWritten)
131
+ return;
132
+ state.updatedAt = now();
133
+ // A failed write is retried by the next save; it never breaks the Feed check chain.
134
+ try {
135
+ (0, attention_1.writeCmuxState)(statePath, state);
136
+ lastWritten = body;
137
+ }
138
+ catch (error) {
139
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_state_write_error", message: "could not write the cmux sense state; the next save retries", meta: { agent, error: error.message } });
140
+ }
141
+ };
142
+ const saveSoon = () => {
143
+ cancelSave ??= schedule(save, SAVE_DELAY_MS);
144
+ };
145
+ async function checkFeed() {
146
+ try {
147
+ const items = (0, attention_1.pendingFeedItems)(await client.call("feed.list", { pending_only: true }));
148
+ const queued = [];
149
+ for (const item of items.filter((entry) => !state.escalated.includes(entry.requestId))) {
150
+ // One failing item never blocks the others; it stays unremembered, so the next check retries it.
151
+ try {
152
+ const escalation = (0, attention_1.escalationMessage)(agent, item, (0, attention_1.surfaceForSession)(state, item.workstreamId));
153
+ await escalate(escalation);
154
+ queued.push(escalation.requestId);
155
+ (0, attention_1.rememberEscalation)(state, item.requestId);
156
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_escalated", message: "brought a cmux Feed request to the agent", meta: { agent, kind: item.kind, source: item.source } });
157
+ }
158
+ catch (error) {
159
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_escalation_error", message: "could not record a cmux Feed request; the next check retries", meta: { agent, error: error.message } });
160
+ }
161
+ }
162
+ // One wake per check pass: the private turn reads every queued escalation at once.
163
+ if (queued.length > 0)
164
+ await wake(queued);
165
+ }
166
+ catch (error) {
167
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_feed_check_error", message: "cmux Feed check failed; the next check retries", meta: { agent, error: error.message } });
168
+ }
169
+ save();
170
+ }
171
+ /** Bursts of waiting events share one queued `feed.list` read instead of stacking reads. */
172
+ const requestFeedCheck = () => {
173
+ if (feedQueued)
174
+ return;
175
+ feedQueued = true;
176
+ feedChain = feedChain.then(() => {
177
+ feedQueued = false;
178
+ return checkFeed();
179
+ });
180
+ };
181
+ /** Records the cmux version once per connection; observing works on any version. */
182
+ async function identify() {
183
+ try {
184
+ const result = await client.call("system.identify", {});
185
+ state.cmuxVersion = typeof result.version === "string" ? result.version : null;
186
+ }
187
+ catch {
188
+ state.cmuxVersion = null;
189
+ }
190
+ saveSoon();
191
+ }
192
+ const poll = () => {
193
+ cancelPoll = schedule(() => {
194
+ requestFeedCheck();
195
+ poll();
196
+ }, FEED_POLL_MS);
197
+ };
198
+ function connect() {
199
+ cancelReconnect = null;
200
+ stream = client.stream({ ...(state.seq !== null ? { after_seq: state.seq } : {}), categories: ["feed", "surface"] }, {
201
+ onFrame: (frame) => {
202
+ if (frame.type === "ack") {
203
+ attempt = 0;
204
+ const { reset } = (0, attention_1.applyAck)(state, frame);
205
+ state.connected = true;
206
+ state.lastError = null;
207
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_stream_connected", message: "cmux event stream connected", meta: { agent, reset } });
208
+ void identify();
209
+ requestFeedCheck();
210
+ return;
211
+ }
212
+ if (frame.type !== "event")
213
+ return;
214
+ if ((0, attention_1.applyEventFrame)(state, frame, now()).feedCheck)
215
+ requestFeedCheck();
216
+ saveSoon();
217
+ },
218
+ onClose: (error) => {
219
+ stream = null;
220
+ state.connected = false;
221
+ state.lastError = error?.message ?? null;
222
+ if (stopped)
223
+ return;
224
+ const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
225
+ attempt += 1;
226
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_stream_closed", message: "cmux event stream closed; reconnecting", meta: { agent, delay, error: error?.message ?? null } });
227
+ save();
228
+ cancelReconnect = schedule(connect, delay);
229
+ },
230
+ });
231
+ }
232
+ connect();
233
+ poll();
234
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_sense_started", message: "cmux sense started", meta: { agent, auth: resolved.connection.auth.kind } });
235
+ return {
236
+ statePath,
237
+ async stop() {
238
+ stopped = true;
239
+ cancelReconnect?.();
240
+ cancelPoll?.();
241
+ stream?.close();
242
+ await feedChain;
243
+ state.connected = false;
244
+ save();
245
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_sense_stopped", message: "cmux sense stopped", meta: { agent } });
246
+ },
247
+ };
248
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const agentArgIndex = process.argv.indexOf("--agent");
37
+ const agentName = agentArgIndex >= 0 ? process.argv[agentArgIndex + 1] : undefined;
38
+ if (!agentName) {
39
+ // eslint-disable-next-line no-console -- pre-boot guard: --agent check before imports
40
+ console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/cmux-entry.js --agent ouroboros");
41
+ process.exit(1);
42
+ }
43
+ const runtime_logging_1 = require("../heart/daemon/runtime-logging");
44
+ const runtime_1 = require("../nerves/runtime");
45
+ (0, runtime_logging_1.configureDaemonRuntimeLogger)("cmux");
46
+ (0, runtime_1.emitNervesEvent)({
47
+ component: "senses",
48
+ event: "senses.entry_boot",
49
+ message: "booting cmux entrypoint",
50
+ meta: { entry: "cmux", agentName },
51
+ });
52
+ Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))).then(async ({ readMachineRuntimeCredentialConfig, refreshMachineRuntimeCredentialConfig, waitForRuntimeCredentialBootstrap }) => {
53
+ await waitForRuntimeCredentialBootstrap(agentName);
54
+ if (!readMachineRuntimeCredentialConfig(agentName).ok) {
55
+ const { loadOrCreateMachineIdentity } = await Promise.resolve().then(() => __importStar(require("../heart/machine-identity")));
56
+ await refreshMachineRuntimeCredentialConfig(agentName, loadOrCreateMachineIdentity().machineId, { preserveCachedOnFailure: true }).catch(() => undefined);
57
+ }
58
+ const { startCmuxSenseApp } = await Promise.resolve().then(() => __importStar(require("./cmux/sense")));
59
+ const app = await startCmuxSenseApp({ agentName: agentName });
60
+ const shutdown = () => {
61
+ void app.stop().finally(() => process.exit(0));
62
+ };
63
+ process.once("SIGTERM", shutdown);
64
+ process.once("SIGINT", shutdown);
65
+ })
66
+ .catch((error) => {
67
+ (0, runtime_1.emitNervesEvent)({
68
+ level: "error",
69
+ component: "senses",
70
+ event: "senses.entry_error",
71
+ message: "cmux entrypoint failed",
72
+ meta: { entry: "cmux", agentName, error: error instanceof Error ? error.message : String(error) },
73
+ });
74
+ // eslint-disable-next-line no-console -- fatal startup guard for sense process
75
+ console.error(error instanceof Error ? error.message : String(error));
76
+ process.exit(1);
77
+ });
@@ -35,7 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LocalAudioDevices = exports.KILL_GRACE_MS = exports.defaultProcessHost = exports.PS_ENV = exports.PLAYBACK_DEVICE_NAME = exports.CAPTURE_DEVICE_NAME = void 0;
37
37
  exports.soxCaptureArgs = soxCaptureArgs;
38
- exports.soxPlaybackArgs = soxPlaybackArgs;
38
+ exports.ffmpegPlaybackArgs = ffmpegPlaybackArgs;
39
+ exports.audioToolboxListArgs = audioToolboxListArgs;
40
+ exports.findAudioToolboxDeviceIndex = findAudioToolboxDeviceIndex;
41
+ exports.defaultListAudioToolboxDevices = defaultListAudioToolboxDevices;
39
42
  exports.fileCaptureArgs = fileCaptureArgs;
40
43
  exports.filePlaybackArgs = filePlaybackArgs;
41
44
  exports.defaultKillGroup = defaultKillGroup;
@@ -52,6 +55,7 @@ const runtime_1 = require("../../nerves/runtime");
52
55
  const local_audio_1 = require("./local-audio");
53
56
  const local_audio_mute_1 = require("./local-audio-mute");
54
57
  const RAW_MULAW = ["-t", "raw", "-r", "8000", "-e", "mu-law", "-b", "8", "-c", "1"];
58
+ const AUDIO_PROCESS_NAMES = new Set(["sox", "ffmpeg"]);
55
59
  exports.CAPTURE_DEVICE_NAME = "BlackHole 16ch";
56
60
  exports.PLAYBACK_DEVICE_NAME = "BlackHole 2ch";
57
61
  /** Capture a CoreAudio device as mono 8 kHz mu-law on stdout (channels 1 and 2 mixed). */
@@ -62,9 +66,50 @@ function soxCaptureArgs(device) {
62
66
  // `--buffer 8192` delayed everything the agent heard by about a second.
63
67
  return ["-q", "--buffer", "1024", "--input-buffer", "8192", "-t", "coreaudio", device, ...RAW_MULAW, "-", "remix", "1,2"];
64
68
  }
65
- /** Play raw mono 8 kHz mu-law from stdin to a CoreAudio device. */
66
- function soxPlaybackArgs(device) {
67
- return ["-q", "--buffer", "160", ...RAW_MULAW, "-", "-t", "coreaudio", device];
69
+ /**
70
+ * Play raw mono 8 kHz mu-law from stdin to an AudioToolbox output device, chosen by index.
71
+ *
72
+ * sox's CoreAudio output can deadlock when the process runs at lowered priority (a zsh background
73
+ * job is niced by default, and so is a launchd Background job): its main thread holds a lock inside
74
+ * `AudioDeviceStart` while the device IO thread waits for that lock, so nothing is ever played.
75
+ * Observed live: 5 of 5 niced joins were silent with sox and 2 of 2 spoke with ffmpeg. The probe
76
+ * flags keep ffmpeg from buffering input before it starts playing.
77
+ */
78
+ function ffmpegPlaybackArgs(deviceIndex) {
79
+ return [
80
+ "-hide_banner", "-loglevel", "error", "-nostdin",
81
+ "-probesize", "32", "-analyzeduration", "0", "-fflags", "nobuffer",
82
+ "-f", "mulaw", "-ar", "8000", "-ac", "1", "-i", "pipe:0",
83
+ "-f", "audiotoolbox", "-audio_device_index", String(deviceIndex), "-",
84
+ ];
85
+ }
86
+ /** ffmpeg prints the AudioToolbox devices to its log; `-t 0` means nothing is played. */
87
+ function audioToolboxListArgs() {
88
+ return ["-hide_banner", "-f", "lavfi", "-i", "anullsrc", "-t", "0", "-f", "audiotoolbox", "-list_devices", "true", "-"];
89
+ }
90
+ // "[AudioToolbox @ 0x7b514003c0] [1] BlackHole 2ch, BlackHole2ch_UID"
91
+ const AUDIOTOOLBOX_DEVICE_LINE = /\[(\d+)\]\s+(.+),\s*[^,]*$/;
92
+ /** The index ffmpeg uses for a device with exactly this name, or undefined when it is not listed. */
93
+ function findAudioToolboxDeviceIndex(listing, deviceName) {
94
+ for (const line of listing.split("\n")) {
95
+ const match = AUDIOTOOLBOX_DEVICE_LINE.exec(line.trim());
96
+ if (match && match[2].trim() === deviceName)
97
+ return Number(match[1]);
98
+ }
99
+ return undefined;
100
+ }
101
+ /** Runs ffmpeg's device listing and returns its log. Rejects only when ffmpeg itself cannot run. */
102
+ function defaultListAudioToolboxDevices(ffmpegPath = "ffmpeg", run = (file, args, callback) => {
103
+ (0, child_process_1.execFile)(file, args, { encoding: "utf8", timeout: 5000 }, (error, stdout, stderr) => callback(error, stdout, stderr));
104
+ }) {
105
+ return new Promise((resolve, reject) => {
106
+ run(ffmpegPath, audioToolboxListArgs(), (error, _stdout, stderr) => {
107
+ if (error && typeof error.code !== "number")
108
+ reject(error);
109
+ else
110
+ resolve(stderr);
111
+ });
112
+ });
68
113
  }
69
114
  /** Decode any audio file to the capture stream format (stdout). The caller paces it. */
70
115
  function fileCaptureArgs(inputPath) {
@@ -145,7 +190,7 @@ class LocalAudioDevices {
145
190
  this.stop();
146
191
  const command = this.options.soxPath ?? "sox";
147
192
  const capture = this.spawnTracked(command, this.options.captureArgs);
148
- const playback = this.spawnTracked(command, this.options.playbackArgs);
193
+ const playback = this.spawnTracked(this.options.playbackCommand ?? command, this.options.playbackArgs);
149
194
  this.writePidFile();
150
195
  this.recordStartTimes([...this.running]);
151
196
  this.watchParentSignals();
@@ -254,10 +299,10 @@ class LocalAudioDevices {
254
299
  });
255
300
  return killed;
256
301
  }
257
- /** Only a process that is still sox AND started when we recorded it is ours to kill (pids get reused). */
302
+ /** Only a process that is still one of our audio tools AND started when we recorded it is ours to kill (pids get reused). */
258
303
  isRecordedSox(pid, recordedStart) {
259
304
  const info = this.inspectProcess(pid);
260
- if (!info || path.basename(info.command) !== "sox")
305
+ if (!info || !AUDIO_PROCESS_NAMES.has(path.basename(info.command)))
261
306
  return false;
262
307
  return recordedStart !== "" && info.startTime === recordedStart;
263
308
  }