@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.
- package/changelog.json +19 -6
- package/dist/heart/daemon/agent-service.js +207 -314
- package/dist/heart/daemon/daemon-cli.js +225 -5
- package/dist/heart/daemon/heartbeat-timer.js +3 -2
- package/dist/heart/daemon/mcp-server.js +128 -1
- package/dist/heart/daemon/ouro-path-installer.js +10 -0
- package/dist/heart/daemon/process-manager.js +11 -0
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/target-resolution.js +4 -0
- package/dist/mind/friends/channel.js +9 -0
- package/dist/mind/prompt.js +14 -0
- package/dist/repertoire/coding/spawner.js +8 -9
- package/dist/repertoire/tools-base.js +11 -3
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/bluebubbles-client.js +48 -0
- package/dist/senses/bluebubbles.js +22 -6
- package/dist/senses/inner-dialog.js +15 -0
- package/dist/senses/pipeline.js +9 -0
- package/dist/senses/shared-turn.js +196 -0
- package/package.json +1 -1
- package/skills/configure-dev-tools.md +41 -69
|
@@ -307,7 +307,11 @@ function usage() {
|
|
|
307
307
|
].join("\n");
|
|
308
308
|
}
|
|
309
309
|
function formatVersionOutput() {
|
|
310
|
-
|
|
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
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|