@ouro.bot/cli 0.1.0-alpha.142 → 0.1.0-alpha.145

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.
@@ -307,7 +307,11 @@ function usage() {
307
307
  ].join("\n");
308
308
  }
309
309
  function formatVersionOutput() {
310
- return (0, runtime_metadata_1.getRuntimeMetadata)().version;
310
+ const version = (0, runtime_metadata_1.getRuntimeMetadata)().version;
311
+ const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
312
+ /* v8 ignore start — cosmetic display toggle; dev mode always true in test env */
313
+ return mode === "dev" ? `${version} (dev)` : version;
314
+ /* v8 ignore stop */
311
315
  }
312
316
  function buildStoppedStatusPayload(socketPath) {
313
317
  const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
@@ -914,6 +918,27 @@ function parseMcpServeCommand(args) {
914
918
  throw new Error("mcp-serve requires --agent <name>");
915
919
  return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
916
920
  }
921
+ function parseSetupCommand(args) {
922
+ let tool;
923
+ let agent;
924
+ for (let i = 0; i < args.length; i++) {
925
+ if (args[i] === "--tool" && args[i + 1]) {
926
+ tool = args[++i];
927
+ continue;
928
+ }
929
+ if (args[i] === "--agent" && args[i + 1]) {
930
+ agent = args[++i];
931
+ continue;
932
+ }
933
+ }
934
+ if (!tool)
935
+ throw new Error("setup requires --tool (claude-code | codex)");
936
+ if (tool !== "claude-code" && tool !== "codex")
937
+ throw new Error(`Unknown tool: ${tool}. Supported: claude-code, codex`);
938
+ if (!agent)
939
+ throw new Error("setup requires --agent <name>");
940
+ return { kind: "setup", tool, agent };
941
+ }
917
942
  function parseOuroCommand(args) {
918
943
  const [head, second] = args;
919
944
  if (!head)
@@ -921,6 +946,12 @@ function parseOuroCommand(args) {
921
946
  if (head === "--agent" && second) {
922
947
  return parseOuroCommand(args.slice(2));
923
948
  }
949
+ if (head === "hook") {
950
+ const event = second;
951
+ if (!event)
952
+ throw new Error("hook requires an event name (session-start, stop, post-tool-use)");
953
+ return { kind: "hook", event };
954
+ }
924
955
  if (head === "up")
925
956
  return { kind: "daemon.up" };
926
957
  if (head === "dev") {
@@ -1005,6 +1036,8 @@ function parseOuroCommand(args) {
1005
1036
  return parseLinkCommand(args.slice(1));
1006
1037
  if (head === "mcp-serve")
1007
1038
  return parseMcpServeCommand(args.slice(1));
1039
+ if (head === "setup")
1040
+ return parseSetupCommand(args.slice(1));
1008
1041
  throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
1009
1042
  }
1010
1043
  function defaultStartDaemonProcess(socketPath) {
@@ -1791,12 +1824,53 @@ function executeReminderCommand(command, taskMod) {
1791
1824
  }
1792
1825
  }
1793
1826
  /* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
1827
+ function getDevConfigPath() {
1828
+ return path.join((0, ouro_version_manager_1.getOuroCliHome)(), "dev-config.json");
1829
+ }
1830
+ function readPersistedDevPath() {
1831
+ try {
1832
+ const data = JSON.parse(fs.readFileSync(getDevConfigPath(), "utf-8"));
1833
+ return typeof data.repoPath === "string" ? data.repoPath : null;
1834
+ }
1835
+ catch {
1836
+ return null;
1837
+ }
1838
+ }
1839
+ function persistDevPath(repoPath) {
1840
+ try {
1841
+ fs.mkdirSync(path.dirname(getDevConfigPath()), { recursive: true });
1842
+ fs.writeFileSync(getDevConfigPath(), JSON.stringify({ repoPath }, null, 2));
1843
+ }
1844
+ catch { /* best effort */ }
1845
+ }
1794
1846
  function resolveDevRepoCwd(command, checkExists, deps) {
1795
- if (command.repoPath)
1796
- return path.resolve(command.repoPath);
1847
+ // 1. Explicit --repo-path: use it and persist for next time
1848
+ if (command.repoPath) {
1849
+ const resolved = path.resolve(command.repoPath);
1850
+ persistDevPath(resolved);
1851
+ return resolved;
1852
+ }
1853
+ // 2. Clone request
1797
1854
  if (command.clone)
1798
1855
  return resolveClonePath(command, checkExists, deps);
1799
- return deps.getRepoCwd ? deps.getRepoCwd() : (0, identity_1.getRepoRoot)();
1856
+ // 3. If test/internal deps provide getRepoCwd, use it directly
1857
+ if (deps.getRepoCwd) {
1858
+ return deps.getRepoCwd();
1859
+ }
1860
+ // 4. Check CWD — if we're inside a harness repo, use it
1861
+ const cwd = process.cwd();
1862
+ if (checkExists(path.join(cwd, ".git")) && checkExists(path.join(cwd, "src", "heart", "daemon"))) {
1863
+ persistDevPath(cwd);
1864
+ return cwd;
1865
+ }
1866
+ // 5. Read persisted path from last --repo-path
1867
+ const persisted = readPersistedDevPath();
1868
+ if (persisted && checkExists(path.join(persisted, ".git"))) {
1869
+ deps.writeStdout(`using remembered dev path: ${persisted}`);
1870
+ return persisted;
1871
+ }
1872
+ // 6. Fall back to getRepoRoot (works when running from installed binary → resolves to npm package)
1873
+ return (0, identity_1.getRepoRoot)();
1800
1874
  }
1801
1875
  /* v8 ignore stop */
1802
1876
  /* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
@@ -2075,6 +2149,19 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2075
2149
  }
2076
2150
  /* v8 ignore stop */
2077
2151
  }
2152
+ // Auto-build: always rebuild in dev mode so dist/ matches source
2153
+ /* v8 ignore start -- dev auto-build: execSync in repo cwd, tested manually @preserve */
2154
+ deps.writeStdout(`building from ${repoCwd}...`);
2155
+ try {
2156
+ (0, child_process_1.execSync)("npm run build", { cwd: repoCwd, stdio: "inherit" });
2157
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2158
+ }
2159
+ catch {
2160
+ const message = `build failed in ${repoCwd}. fix compilation errors and retry.`;
2161
+ deps.writeStdout(message);
2162
+ return message;
2163
+ }
2164
+ /* v8 ignore stop */
2078
2165
  /* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
2079
2166
  if (deps.ensureDaemonBootPersistence) {
2080
2167
  try {
@@ -2092,6 +2179,23 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2092
2179
  }
2093
2180
  /* v8 ignore stop */
2094
2181
  }
2182
+ // Disable launchd KeepAlive before killing — prevents the installed daemon from respawning
2183
+ /* v8 ignore start -- dev launchd disable: requires real launchctl + plist on disk @preserve */
2184
+ const launchdDevDeps = {
2185
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd); },
2186
+ existsFile: (p) => fs.existsSync(p),
2187
+ removeFile: (p) => { try {
2188
+ fs.unlinkSync(p);
2189
+ }
2190
+ catch { /* best effort */ } },
2191
+ homeDir: os.homedir(),
2192
+ userUid: process.getuid?.() ?? 0,
2193
+ };
2194
+ if ((0, launchd_1.isDaemonInstalled)(launchdDevDeps)) {
2195
+ (0, launchd_1.uninstallLaunchAgent)(launchdDevDeps);
2196
+ deps.writeStdout("disabled launchd auto-restart for dev mode");
2197
+ }
2198
+ /* v8 ignore stop */
2095
2199
  // Always force-restart in dev mode — you rebuilt, you want this code running
2096
2200
  /* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
2097
2201
  const alive = await deps.checkSocketAlive(deps.socketPath);
@@ -2113,7 +2217,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2113
2217
  /* v8 ignore stop */
2114
2218
  const started = await startDevDaemon(deps.socketPath);
2115
2219
  /* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
2116
- const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})`;
2220
+ const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})\nrun 'ouro up' to return to production mode`;
2117
2221
  deps.writeStdout(message);
2118
2222
  return message;
2119
2223
  }
@@ -2198,6 +2302,122 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2198
2302
  deps.tailLogs();
2199
2303
  return "";
2200
2304
  }
2305
+ // ── hook: handle Claude Code lifecycle hooks ──
2306
+ /* v8 ignore start -- hook handler: reads real stdin, sends to real daemon @preserve */
2307
+ if (command.kind === "hook") {
2308
+ let stdinData = "";
2309
+ try {
2310
+ stdinData = require("fs").readFileSync(0, "utf-8");
2311
+ }
2312
+ catch { /* no stdin */ }
2313
+ let event = {};
2314
+ try {
2315
+ event = JSON.parse(stdinData);
2316
+ }
2317
+ catch { /* malformed */ }
2318
+ const eventType = command.event;
2319
+ const sessionId = event.session_id ?? "unknown";
2320
+ const cwd = event.cwd ?? "";
2321
+ // Build notification content based on event type
2322
+ let content;
2323
+ if (eventType === "session-start") {
2324
+ const model = event.model ?? "";
2325
+ const source = event.source ?? "";
2326
+ content = `[Claude Code session started: ${sessionId}, cwd: ${cwd}${model ? `, model: ${model}` : ""}${source ? `, source: ${source}` : ""}]`;
2327
+ }
2328
+ else if (eventType === "stop") {
2329
+ const lastMsg = event.last_assistant_message ?? "";
2330
+ content = `[Claude Code session ended: ${sessionId}${lastMsg ? `, last: ${lastMsg.slice(0, 200)}` : ""}]`;
2331
+ }
2332
+ else if (eventType === "post-tool-use") {
2333
+ const toolName = event.tool_name ?? "";
2334
+ content = `[Claude Code used ${toolName} in session ${sessionId}]`;
2335
+ }
2336
+ else {
2337
+ content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
2338
+ }
2339
+ // Send to daemon (best effort — daemon may not be running)
2340
+ try {
2341
+ const agents = require("fs").readdirSync(path.join(os.homedir(), "AgentBundles"))
2342
+ .filter((f) => f.endsWith(".ouro"))
2343
+ .map((f) => f.replace(".ouro", ""));
2344
+ for (const agent of agents) {
2345
+ await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: agent, content });
2346
+ }
2347
+ await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: agents[0] }).catch(() => { });
2348
+ }
2349
+ catch { /* daemon not running — silent */ }
2350
+ // Output for Claude Code hook system
2351
+ deps.writeStdout(JSON.stringify({ continue: true }));
2352
+ return JSON.stringify({ continue: true });
2353
+ }
2354
+ /* v8 ignore stop */
2355
+ // ── setup: configure dev tool integration ──
2356
+ if (command.kind === "setup") {
2357
+ const { tool, agent: setupAgent } = command;
2358
+ const sourceRoot = (0, identity_1.getRepoRoot)();
2359
+ const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
2360
+ const mcpServeCommand = runtimeMode === "dev"
2361
+ ? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
2362
+ : `ouro mcp-serve --agent ${setupAgent}`;
2363
+ if (tool === "claude-code") {
2364
+ // 1. Register MCP server with Claude Code
2365
+ const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
2366
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2367
+ // 2. Write hooks config to ~/.claude/settings.json
2368
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2369
+ let settings = {};
2370
+ if (fs.existsSync(settingsPath)) {
2371
+ try {
2372
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
2373
+ }
2374
+ catch { /* start fresh */ }
2375
+ }
2376
+ // Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
2377
+ // Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
2378
+ settings.hooks = {
2379
+ ...(settings.hooks ?? {}),
2380
+ SessionStart: [{ hooks: [{ type: "command", command: "ouro hook session-start", timeout: 5 }] }],
2381
+ Stop: [{ hooks: [{ type: "command", command: "ouro hook stop", timeout: 5 }] }],
2382
+ PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: "ouro hook post-tool-use", timeout: 5 }] }],
2383
+ };
2384
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
2385
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
2386
+ (0, runtime_1.emitNervesEvent)({
2387
+ component: "daemon",
2388
+ event: "daemon.setup_complete",
2389
+ message: "dev tool setup complete",
2390
+ meta: { tool, agent: setupAgent, runtimeMode },
2391
+ });
2392
+ // 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
2393
+ const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2394
+ const agentInstructions = `\n## Agent conversations (ouro)\nWhen using MCP \`send_message\` to talk to an ouro agent, format the exchange clearly:\n- Before the tool call, briefly say what you're asking/telling the agent\n- After the response, quote the agent's reply in a blockquote, then add your reaction\n- Example: **Me → Agent:** "question" / > **Agent:** "response" / Your synthesis here\n`;
2395
+ let existingClaudeMd = "";
2396
+ if (fs.existsSync(claudeMdPath)) {
2397
+ existingClaudeMd = fs.readFileSync(claudeMdPath, "utf-8");
2398
+ }
2399
+ if (!existingClaudeMd.includes("Agent conversations (ouro)")) {
2400
+ fs.writeFileSync(claudeMdPath, existingClaudeMd + agentInstructions);
2401
+ }
2402
+ const message = `setup complete: claude-code + ${setupAgent}\n MCP server registered\n hooks configured\n conversation formatting instructions added`;
2403
+ deps.writeStdout(message);
2404
+ return message;
2405
+ }
2406
+ else {
2407
+ // tool === "codex" (parseSetupCommand validates tool, so this is the only remaining option)
2408
+ const mcpAddCmd = `codex mcp add ouro-${setupAgent} -- ${mcpServeCommand}`;
2409
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2410
+ (0, runtime_1.emitNervesEvent)({
2411
+ component: "daemon",
2412
+ event: "daemon.setup_complete",
2413
+ message: "dev tool setup complete",
2414
+ meta: { tool, agent: setupAgent, runtimeMode },
2415
+ });
2416
+ const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
2417
+ deps.writeStdout(message);
2418
+ return message;
2419
+ }
2420
+ }
2201
2421
  /* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
2202
2422
  // ── mcp-serve: start MCP server in-process on stdin/stdout ──
2203
2423
  if (command.kind === "mcp-serve") {
@@ -57,8 +57,9 @@ class HeartbeatTimer {
57
57
  const nowMs = this.now();
58
58
  let delay;
59
59
  if (lastCompletedAt === null) {
60
- // Never run before — fire immediately
61
- delay = 0;
60
+ // Never run before — fire once after a full cadence (not immediately, to avoid spin loop
61
+ // when the agent has no inner dialog state to write lastCompletedAt)
62
+ delay = cadenceMs;
62
63
  }
63
64
  else {
64
65
  const elapsed = nowMs - lastCompletedAt;
@@ -38,6 +38,9 @@ exports.getToolSchemas = getToolSchemas;
38
38
  const socket_client_1 = require("./socket-client");
39
39
  const agentService = __importStar(require("./agent-service"));
40
40
  const runtime_1 = require("../../nerves/runtime");
41
+ const shared_turn_1 = require("../../senses/shared-turn");
42
+ const session_id_resolver_1 = require("./session-id-resolver");
43
+ const pending_1 = require("../../mind/pending");
41
44
  /**
42
45
  * Maps MCP tool names to daemon command kinds.
43
46
  */
@@ -66,6 +69,8 @@ function createMcpServer(options) {
66
69
  let buffer = "";
67
70
  let running = false;
68
71
  let useContentLengthFraming = true; // default to Content-Length, auto-detect from first message
72
+ // Resolve session ID once per MCP server instance for conversation continuity
73
+ const sessionId = (0, session_id_resolver_1.resolveSessionId)();
69
74
  function writeResponse(response) {
70
75
  const body = JSON.stringify(response);
71
76
  if (useContentLengthFraming) {
@@ -238,6 +243,109 @@ function createMcpServer(options) {
238
243
  const toolName = params.name;
239
244
  const toolArgs = (params.arguments ?? {});
240
245
  /* v8 ignore stop */
246
+ // ── Conversation tools: send_message, check_response ──
247
+ if (toolName === "send_message") {
248
+ /* v8 ignore start — ?? fallback defensive; MCP clients always send message */
249
+ const message = toolArgs.message ?? "";
250
+ /* v8 ignore stop */
251
+ try {
252
+ const result = await (0, shared_turn_1.runSenseTurn)({
253
+ agentName: agent,
254
+ channel: "mcp",
255
+ sessionKey: sessionId,
256
+ friendId,
257
+ userMessage: message,
258
+ });
259
+ writeResponse({
260
+ jsonrpc: "2.0",
261
+ id: request.id,
262
+ result: {
263
+ content: [{ type: "text", text: result.response }],
264
+ isError: false,
265
+ },
266
+ });
267
+ }
268
+ catch (error) {
269
+ /* v8 ignore start — instanceof guard defensive; thrown errors are always Error */
270
+ const errorMessage = error instanceof Error ? error.message : String(error);
271
+ /* v8 ignore stop */
272
+ writeResponse({
273
+ jsonrpc: "2.0",
274
+ id: request.id,
275
+ result: {
276
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
277
+ isError: true,
278
+ },
279
+ });
280
+ }
281
+ return;
282
+ }
283
+ if (toolName === "check_response") {
284
+ const pendingDir = (0, pending_1.getPendingDir)(agent, friendId, "mcp", sessionId);
285
+ const pending = (0, pending_1.drainPending)(pendingDir);
286
+ if (pending.length === 0) {
287
+ writeResponse({
288
+ jsonrpc: "2.0",
289
+ id: request.id,
290
+ result: {
291
+ content: [{ type: "text", text: "no pending messages" }],
292
+ isError: false,
293
+ },
294
+ });
295
+ }
296
+ else {
297
+ const text = pending.map((m) => m.content).join("\n\n---\n\n");
298
+ writeResponse({
299
+ jsonrpc: "2.0",
300
+ id: request.id,
301
+ result: {
302
+ content: [{ type: "text", text }],
303
+ isError: false,
304
+ },
305
+ });
306
+ }
307
+ return;
308
+ }
309
+ // ── delegate: full conversation turn via runSenseTurn ──
310
+ if (toolName === "delegate") {
311
+ /* v8 ignore start — ?? fallback defensive; MCP clients always send task */
312
+ const task = toolArgs.task ?? "";
313
+ /* v8 ignore stop */
314
+ const context = toolArgs.context;
315
+ const delegateMessage = context ? `[delegate] ${task}\n\ncontext: ${context}` : `[delegate] ${task}`;
316
+ try {
317
+ const result = await (0, shared_turn_1.runSenseTurn)({
318
+ agentName: agent,
319
+ channel: "mcp",
320
+ sessionKey: sessionId,
321
+ friendId,
322
+ userMessage: delegateMessage,
323
+ });
324
+ writeResponse({
325
+ jsonrpc: "2.0",
326
+ id: request.id,
327
+ result: {
328
+ content: [{ type: "text", text: result.response }],
329
+ isError: false,
330
+ },
331
+ });
332
+ }
333
+ catch (error) {
334
+ /* v8 ignore start — instanceof guard defensive; thrown errors are always Error */
335
+ const errorMessage = error instanceof Error ? error.message : String(error);
336
+ /* v8 ignore stop */
337
+ writeResponse({
338
+ jsonrpc: "2.0",
339
+ id: request.id,
340
+ result: {
341
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
342
+ isError: true,
343
+ },
344
+ });
345
+ }
346
+ return;
347
+ }
348
+ // ── Legacy daemon/service tools ──
241
349
  const commandKind = TOOL_TO_COMMAND[toolName];
242
350
  if (!commandKind) {
243
351
  writeResponse({
@@ -321,7 +429,7 @@ function createMcpServer(options) {
321
429
  };
322
430
  }
323
431
  /**
324
- * Returns the list of MCP tool schemas for all 13 agent tools.
432
+ * Returns the list of MCP tool schemas for all 15 agent tools.
325
433
  * Each schema follows JSON Schema for inputSchema as required by MCP.
326
434
  */
327
435
  function getToolSchemas() {
@@ -459,6 +567,25 @@ function getToolSchemas() {
459
567
  required: ["summary"],
460
568
  },
461
569
  },
570
+ {
571
+ name: "send_message",
572
+ description: "Send a message to the agent and get a synchronous response. This runs a full agent turn — the agent can use tools, think, and respond. For multi-turn conversations, call repeatedly — the agent remembers prior messages in this session.",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {
576
+ message: { type: "string", description: "The message to send to the agent" },
577
+ },
578
+ required: ["message"],
579
+ },
580
+ },
581
+ {
582
+ name: "check_response",
583
+ description: "Check for pending messages from the agent. Use this after send_message returns a ponder deferral, or to pick up proactive messages the agent has surfaced to you.",
584
+ inputSchema: {
585
+ type: "object",
586
+ properties: {},
587
+ },
588
+ },
462
589
  ];
463
590
  }
464
591
  // MCP server v0.1.0-alpha.140
@@ -39,6 +39,16 @@ const os = __importStar(require("os"));
39
39
  const path = __importStar(require("path"));
40
40
  const runtime_1 = require("../../nerves/runtime");
41
41
  const WRAPPER_SCRIPT = `#!/bin/sh
42
+ # Check for dev mode — if dev-config.json exists, dispatch to the dev repo
43
+ DEV_CONFIG="$HOME/.ouro-cli/dev-config.json"
44
+ if [ -f "$DEV_CONFIG" ]; then
45
+ DEV_REPO=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$DEV_CONFIG','utf-8')).repoPath)}catch{}" 2>/dev/null)
46
+ DEV_ENTRY="$DEV_REPO/dist/heart/daemon/ouro-entry.js"
47
+ if [ -n "$DEV_REPO" ] && [ -e "$DEV_ENTRY" ]; then
48
+ exec node "$DEV_ENTRY" "$@"
49
+ fi
50
+ fi
51
+ # Fall back to installed version
42
52
  ENTRY="$HOME/.ouro-cli/CurrentVersion/node_modules/@ouro.bot/cli/dist/heart/daemon/ouro-entry.js"
43
53
  if [ ! -e "$ENTRY" ]; then
44
54
  echo "ouro not installed. Run: npx ouro.bot" >&2
@@ -125,6 +125,17 @@ class DaemonProcessManager {
125
125
  message: "daemon started managed agent process",
126
126
  meta: { agent, pid: child.pid ?? null, cwd: runCwd },
127
127
  });
128
+ /* v8 ignore start — child process error handler; requires real spawn to trigger */
129
+ child.on("error", (err) => {
130
+ (0, runtime_1.emitNervesEvent)({
131
+ level: "warn",
132
+ component: "daemon",
133
+ event: "daemon.agent_process_error",
134
+ message: "managed agent process emitted error",
135
+ meta: { agent, error: err.message },
136
+ });
137
+ });
138
+ /* v8 ignore stop */
128
139
  child.once("exit", (code, signal) => {
129
140
  this.onExit(state, code, signal);
130
141
  });
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ // Pluggable session ID resolver for MCP conversations.
3
+ // Tries tool-specific methods first, falls back to UUID.
4
+ //
5
+ // Claude Code: walks parent PID chain -> reads ~/.claude/sessions/{pid}.json
6
+ // Codex: reads thread_id from env (future)
7
+ // Fallback: generates UUID
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.resolveSessionId = resolveSessionId;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const crypto_1 = require("crypto");
47
+ const runtime_1 = require("../../nerves/runtime");
48
+ const DEFAULT_CLAUDE_SESSIONS_DIR = path.join(os.homedir(), ".claude", "sessions");
49
+ const MAX_PID_WALK_DEPTH = 10;
50
+ /**
51
+ * Try to read a Claude Code session ID from a PID-keyed session file.
52
+ * Returns the sessionId if found, null otherwise.
53
+ */
54
+ function tryReadClaudeSession(sessionsDir, pid) {
55
+ const sessionFile = path.join(sessionsDir, `${pid}.json`);
56
+ if (!fs.existsSync(sessionFile))
57
+ return null;
58
+ try {
59
+ const raw = fs.readFileSync(sessionFile, "utf-8");
60
+ const data = JSON.parse(raw);
61
+ if (typeof data.sessionId === "string" && data.sessionId.length > 0) {
62
+ return data.sessionId;
63
+ }
64
+ return null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Walk the parent PID chain looking for a Claude Code session file.
72
+ * Starts at the current process PID, walks up to parent, grandparent, etc.
73
+ * Returns the session ID from the first matching file, or null.
74
+ */
75
+ function walkPidChain(sessionsDir) {
76
+ let currentPid = process.pid;
77
+ for (let depth = 0; depth < MAX_PID_WALK_DEPTH; depth++) {
78
+ const sessionId = tryReadClaudeSession(sessionsDir, currentPid);
79
+ if (sessionId) {
80
+ (0, runtime_1.emitNervesEvent)({
81
+ component: "daemon",
82
+ event: "daemon.session_id_pid_walk_hit",
83
+ message: "found Claude session via PID walk",
84
+ meta: { pid: currentPid, depth, sessionId },
85
+ });
86
+ return sessionId;
87
+ }
88
+ // Walk to parent PID
89
+ // On macOS/Linux, process.ppid gives the parent. For deeper ancestry,
90
+ // we'd need /proc/{pid}/stat or ps -o ppid=. For now, we check current + parent.
91
+ if (depth === 0) {
92
+ currentPid = process.ppid;
93
+ }
94
+ else {
95
+ // Cannot walk further without OS-specific process tree APIs
96
+ break;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ /**
102
+ * Resolve a session ID for the current MCP connection.
103
+ * Returns a stable identifier that ties MCP tool calls to a conversation session.
104
+ *
105
+ * Resolution order:
106
+ * 1. Claude Code PID walk: check ~/.claude/sessions/{pid}.json for current + parent PID
107
+ * 2. UUID fallback: generate a random UUID
108
+ */
109
+ function resolveSessionId(options) {
110
+ const sessionsDir = options?.claudeSessionsDir ?? DEFAULT_CLAUDE_SESSIONS_DIR;
111
+ // Try Claude Code PID walk
112
+ const claudeSessionId = walkPidChain(sessionsDir);
113
+ if (claudeSessionId) {
114
+ (0, runtime_1.emitNervesEvent)({
115
+ component: "daemon",
116
+ event: "daemon.session_id_resolved",
117
+ message: "session ID resolved via Claude Code PID walk",
118
+ meta: { sessionId: claudeSessionId, method: "claude-pid-walk" },
119
+ });
120
+ return claudeSessionId;
121
+ }
122
+ // Fallback: UUID
123
+ const sessionId = (0, crypto_1.randomUUID)();
124
+ (0, runtime_1.emitNervesEvent)({
125
+ component: "daemon",
126
+ event: "daemon.session_id_resolved",
127
+ message: "session ID resolved via UUID fallback",
128
+ meta: { sessionId, method: "uuid-fallback" },
129
+ });
130
+ return sessionId;
131
+ }
@@ -34,6 +34,10 @@ function activityPriority(source) {
34
34
  return source === "friend-facing" ? 0 : 1;
35
35
  }
36
36
  function describeDelivery(candidate) {
37
+ if (candidate.channel === "mcp") {
38
+ // MCP can't push (no proactive API), but can queue — dev tool picks up via check_response
39
+ return { mode: "queue_only", reason: "MCP session — message queued for dev tool to pick up via check_response" };
40
+ }
37
41
  if (candidate.channel !== "bluebubbles" && candidate.channel !== "teams") {
38
42
  return { mode: "blocked", reason: "this channel does not support proactive outward delivery yet" };
39
43
  }
@@ -43,6 +43,15 @@ const CHANNEL_CAPABILITIES = {
43
43
  supportsRichCards: false,
44
44
  maxMessageLength: Infinity,
45
45
  },
46
+ mcp: {
47
+ channel: "mcp",
48
+ senseType: "local",
49
+ availableIntegrations: [],
50
+ supportsMarkdown: true,
51
+ supportsStreaming: false,
52
+ supportsRichCards: false,
53
+ maxMessageLength: Infinity,
54
+ },
46
55
  };
47
56
  const DEFAULT_CAPABILITIES = {
48
57
  channel: "cli",