musecode 0.1.0

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/.env.example ADDED
@@ -0,0 +1,31 @@
1
+ # Required: generate independent random values, for example with
2
+ # node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
3
+ BRIDGE_TOKEN=replace-with-a-random-32-byte-base64url-token
4
+ BRIDGE_APPROVAL_SECRET=replace-with-a-different-random-32-byte-base64url-secret
5
+
6
+ # Required when BRIDGE_REQUIRE_APPROVAL=true. Treat the topic name as a secret.
7
+ BRIDGE_NTFY_TOPIC=replace-with-a-private-ntfy-topic
8
+
9
+ # Core settings
10
+ BRIDGE_PORT=8787
11
+ BRIDGE_WORKDIR=workspace
12
+ BRIDGE_DATA=data
13
+ BRIDGE_REQUIRE_APPROVAL=true
14
+ BRIDGE_APPROVAL_TIMEOUT_MINUTES=30
15
+ BRIDGE_TASK_TIMEOUT_MINUTES=30
16
+ BRIDGE_PERMISSION_MODE=bypassPermissions
17
+ BRIDGE_ALLOWED_TOOLS=
18
+ BRIDGE_MODEL=
19
+
20
+ # Agent executables. Commands on PATH or absolute paths are accepted.
21
+ BRIDGE_CLAUDE_PATH=claude
22
+ BRIDGE_CODEX_PATH=codex
23
+
24
+ # Network and notification settings
25
+ BRIDGE_NTFY_SERVER=https://ntfy.sh
26
+ BRIDGE_PUBLIC_URL=
27
+ BRIDGE_ALLOWED_IPS=
28
+ BRIDGE_IP_HEADER=cf-connecting-ip
29
+
30
+ # Optional path to a JSON config file. Environment variables override JSON values.
31
+ BRIDGE_CONFIG_PATH=config.json
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claude Desktop Bridge contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Claude Desktop Bridge
2
+
3
+ Claude Desktop Bridge is a small, dependency-free Node.js service that lets a remote client hand work to Claude Code or Codex on a Windows PC. It exposes the same capabilities as an HTTP API and an MCP server, keeps task state on disk, and can require a human approval before an agent runs.
4
+
5
+ The bridge listens only on `127.0.0.1`. Remote access is expected to arrive through a tunnel or private network that you configure separately.
6
+
7
+ ## Architecture
8
+
9
+ ```text
10
+ Remote client
11
+ |
12
+ | Bearer-authenticated HTTP
13
+ v
14
+ server.mjs ── /mcp (MCP over HTTP)
15
+ | └─ /api/* + /openapi.json (REST/OpenAPI)
16
+ |
17
+ +─ task store + audit log ─────────────── data/
18
+ +─ inbox queues (to-desktop / to-muse) ─ data/*.jsonl
19
+ +─ Claude Code / Claude Desktop sessions
20
+ +─ Codex Desktop tasks (via the official app-server thread API)
21
+ +─ side threads ───────────────────────── data/threads/
22
+
23
+ Claude Desktop or Codex
24
+ |
25
+ └─ desktop-mcp.mjs / codex-mcp.mjs (local stdio MCP)
26
+ ```
27
+
28
+ - **HTTP API:** `server.mjs` serves MCP JSON-RPC at `POST /mcp` and equivalent REST operations under `/api/*`. `/openapi.json` describes the REST surface.
29
+ - **Claude and Codex agents:** `ask_claude` can start or resume Claude sessions, including delivery into an open Claude Desktop session. `list_codex_sessions` and `read_codex_session` expose real Codex Desktop tasks; `ask_codex` with a session ID continues one through Codex app-server.
30
+ - **Approvals:** when enabled, a task remains pending until the user opens a signed approval link delivered through ntfy. The approval code is derived from a separate HMAC secret.
31
+ - **Side channels:** a per-session side thread lets a remote client coordinate with a read-only Claude fork without adding messages to the user's main conversation.
32
+ - **Inbox:** append-only JSONL queues carry asynchronous messages between the remote client and local desktop agents. The two stdio MCP entry points expose tools to read and write those queues.
33
+ - **Persistence:** tasks, queue cursors, side threads, results, and audit logs live under `data/`. This directory can contain sensitive conversation content and is ignored by Git.
34
+
35
+ ## Requirements
36
+
37
+ - Windows with PowerShell
38
+ - Node.js 20 or newer
39
+ - Claude Code and/or Codex CLI installed and authenticated
40
+ - Optional: [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) for the bundled quick-tunnel launcher
41
+ - Optional: an ntfy server/app for approval notifications
42
+
43
+ The bridge has no npm runtime dependencies.
44
+
45
+ ## Install
46
+
47
+ 1. Clone or copy this directory.
48
+ 2. Create a private environment file:
49
+
50
+ ```powershell
51
+ Copy-Item .env.example .env
52
+ ```
53
+
54
+ 3. Generate two independent secrets and put them in `BRIDGE_TOKEN` and `BRIDGE_APPROVAL_SECRET`:
55
+
56
+ ```powershell
57
+ node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
58
+ ```
59
+
60
+ Run the command twice. If approvals are enabled, also choose a long, unguessable `BRIDGE_NTFY_TOPIC`; the topic name acts as a credential.
61
+
62
+ 4. Set `BRIDGE_CLAUDE_PATH` and `BRIDGE_CODEX_PATH` if the commands are not available on `PATH`.
63
+ 5. Start the local server:
64
+
65
+ ```powershell
66
+ npm start
67
+ ```
68
+
69
+ Or run `./start.ps1` to create a temporary Cloudflare quick tunnel and start the server. `run-server.ps1` is a restart loop suitable for a Windows scheduled task.
70
+
71
+ On first launch, any omitted credentials are generated into the ignored `config.json`. Supplying all credentials through `.env` is recommended because it makes configuration explicit.
72
+
73
+ ## Configuration
74
+
75
+ Configuration precedence is environment variables (including `.env`), then `config.json`, then built-in defaults. Copy `config.example.json` to `config.json` if you prefer JSON. Never commit `.env` or `config.json`.
76
+
77
+ | Variable | Default | Purpose |
78
+ | --- | --- | --- |
79
+ | `BRIDGE_TOKEN` | generated | Bearer token protecting MCP and REST calls |
80
+ | `BRIDGE_APPROVAL_SECRET` | generated | HMAC secret for per-task approval links |
81
+ | `BRIDGE_NTFY_TOPIC` | generated | Private ntfy topic for approval notifications |
82
+ | `BRIDGE_PORT` | `8787` | Local HTTP port |
83
+ | `BRIDGE_WORKDIR` | `workspace` | Working directory for new agent tasks |
84
+ | `BRIDGE_DATA` | `data` | Runtime state directory |
85
+ | `BRIDGE_FILE_DROP` | `%LOCALAPPDATA%\\ClaudeDesktopBridge\\shared-files` | Shared-file storage directory; must be outside the source tree |
86
+ | `BRIDGE_REQUIRE_APPROVAL` | `true` | Require approval before running tasks |
87
+ | `BRIDGE_PERMISSION_MODE` | `bypassPermissions` | Claude Code permission mode |
88
+ | `BRIDGE_ALLOWED_TOOLS` | empty | Comma-separated Claude tool allowlist |
89
+ | `BRIDGE_CLAUDE_PATH` | auto-detected/`claude` | Claude executable or command |
90
+ | `BRIDGE_CODEX_PATH` | `codex.exe` | Codex executable or command |
91
+ | `BRIDGE_PUBLIC_URL` | empty | Fixed public URL used in approval links and Host validation |
92
+ | `BRIDGE_ALLOWED_IPS` | empty | Comma-separated caller IPs or CIDRs |
93
+ | `BRIDGE_IP_HEADER` | `cf-connecting-ip` | Trusted proxy header carrying the caller IP; set empty when unavailable |
94
+ | `BRIDGE_NTFY_SERVER` | `https://ntfy.sh` | ntfy base URL |
95
+ | `BRIDGE_APPROVAL_TIMEOUT_MINUTES` | `30` | Approval expiry |
96
+ | `BRIDGE_TASK_TIMEOUT_MINUTES` | `30` | Agent task timeout |
97
+ | `BRIDGE_MODEL` | empty | Optional Claude model override |
98
+ | `BRIDGE_CONFIG_PATH` | `config.json` | Optional JSON config path |
99
+
100
+ `bypassPermissions` gives Claude broad local access. Keep approval enabled, protect the bearer token and ntfy topic, and use a restricted permission mode plus `BRIDGE_ALLOWED_TOOLS` when full access is unnecessary.
101
+
102
+ ## Connect the local MCP inbox
103
+
104
+ Claude Desktop/Claude Code can launch `desktop-mcp.mjs` as a stdio MCP server. Codex can use `codex-mcp.mjs`. Point the command at your clone and make sure the process sees the same `BRIDGE_DATA` directory as the HTTP server.
105
+
106
+ Example Claude MCP configuration:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "muse-inbox": {
112
+ "command": "node",
113
+ "args": ["C:\\path\\to\\claude-desktop-bridge\\desktop-mcp.mjs"],
114
+ "env": {
115
+ "BRIDGE_DATA": "C:\\path\\to\\claude-desktop-bridge\\data"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Use `codex-mcp.mjs` in the same way for a Codex MCP configuration.
123
+
124
+ ## Send a task
125
+
126
+ With the server running, send a task to Codex over REST:
127
+
128
+ ```powershell
129
+ $headers = @{ Authorization = "Bearer <your-BRIDGE_TOKEN>" }
130
+ $body = @{ prompt = "List the top-level files in the workspace."; wait_seconds = 0 } | ConvertTo-Json
131
+ $task = Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8787/api/ask_codex -Headers $headers -ContentType application/json -Body $body
132
+ $task
133
+ ```
134
+
135
+ If approval is enabled, approve the notification, then poll the returned task ID:
136
+
137
+ ```powershell
138
+ $body = @{ task_id = $task.task_id; wait_seconds = 30 } | ConvertTo-Json
139
+ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8787/api/get_task -Headers $headers -ContentType application/json -Body $body
140
+ ```
141
+
142
+ Replace `ask_codex` with `ask_claude` to route the task to Claude. A client can also initialize MCP at `POST /mcp` and call the same tools through JSON-RPC.
143
+
144
+ For immediate agent-to-agent delivery, send a `send_to_desktop` message beginning with `FOR CODEX`. The bridge writes an acknowledgment back to Muse immediately, selects the most recent Codex Desktop task, and starts the message there. This works while that task is idle because the bridge runs independently as a Windows scheduled task.
145
+
146
+ ## Shared file drop
147
+
148
+ The bearer-authenticated REST API can exchange files up to 25 MB. Files are stored under `BRIDGE_FILE_DROP`, which defaults outside the repository so uploaded content cannot be committed accidentally.
149
+
150
+ - `POST /api/upload_file` accepts JSON with `name`, base64-encoded `content_base64`, and optional `content_type`; it returns a `file_id` plus metadata.
151
+ - `GET /api/download_file?file_id=...` downloads the original bytes with an attachment filename.
152
+ - `GET /api/list_files` returns file IDs, names, sizes, content types, and creation timestamps.
153
+ - `DELETE /api/delete_file?file_id=...` removes the stored file and its metadata.
154
+
155
+ ```bash
156
+ TOKEN="your-BRIDGE_TOKEN"
157
+ BASE64=$(base64 < ./example.txt | tr -d '\n')
158
+ curl -X POST http://127.0.0.1:8787/api/upload_file \
159
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
160
+ --data "{\"name\":\"example.txt\",\"content_base64\":\"$BASE64\",\"content_type\":\"text/plain\"}"
161
+ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8787/api/list_files
162
+ curl -H "Authorization: Bearer $TOKEN" -OJ "http://127.0.0.1:8787/api/download_file?file_id=FILE_ID"
163
+ curl -X DELETE -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8787/api/delete_file?file_id=FILE_ID"
164
+ ```
165
+
166
+ ## Security notes
167
+
168
+ - The service binds to localhost; do not change that unless you also provide equivalent network controls.
169
+ - A tunnel makes the service internet-reachable. Use TLS, keep the bearer token private, pin `BRIDGE_PUBLIC_URL`, and consider an IP allowlist when your proxy provides a trustworthy client-IP header.
170
+ - Approval links contain a task-specific secret. Keep approval enabled for agents with broad filesystem permissions.
171
+ - `data/`, `workspace/`, `.env`, and `config.json` may contain credentials or private conversation content and must remain untracked.
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE)
package/codex-mcp.mjs ADDED
@@ -0,0 +1,64 @@
1
+ // Local stdio MCP server for Codex: read what Muse sent, reply to Muse,
2
+ // participate in side threads, and inspect tasks Muse has run on this PC.
3
+ import readline from "node:readline";
4
+ import { createMcpHandler, postMessage, readMessages, loadTasks, TO_DESKTOP, TO_MUSE } from "./lib.mjs";
5
+ import { appendMessage, readThread, listThreads } from "./threads.mjs";
6
+
7
+ const handle = createMcpHandler({
8
+ name: "muse-inbox",
9
+ version: "1.1.0",
10
+ instructions: "Messages and tasks exchanged between Codex and Meta Muse on the user's phone.",
11
+ tools: [
12
+ {
13
+ name: "read_muse_messages",
14
+ description: "Read messages Meta Muse has left for desktop agents. Returns unread ones and marks them read.",
15
+ inputSchema: { type: "object", properties: { include_read: { type: "boolean", description: "Return the last 50 instead of only unread." } } },
16
+ run: async ({ include_read }) => {
17
+ const messages = readMessages(TO_DESKTOP, { unreadOnly: !include_read });
18
+ return messages.length ? { messages } : { messages: [], note: "No new messages from Muse." };
19
+ },
20
+ },
21
+ {
22
+ name: "send_to_muse",
23
+ description: "Send a message from Codex to Meta Muse. Muse sees it next time it checks desktop messages.",
24
+ inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
25
+ run: async ({ text }) => {
26
+ if (!text?.trim()) throw new Error("text is required");
27
+ return { sent: postMessage(TO_MUSE, text, "codex-desktop") };
28
+ },
29
+ },
30
+ {
31
+ name: "read_side_thread",
32
+ description:
33
+ "Read the side thread between Meta Muse and one of the user's sessions. Messages are labeled muse, " +
34
+ "side-claude, desktop-claude, desktop-codex, or user. Omit session_id to list threads.",
35
+ inputSchema: { type: "object", properties: { session_id: { type: "string" } } },
36
+ run: async ({ session_id }) => (session_id ? { messages: readThread(session_id).slice(-100) } : { threads: listThreads() }),
37
+ },
38
+ {
39
+ name: "post_to_side_thread",
40
+ description: "Post into a session's side thread as desktop-codex. Muse sees it with side_thread_read.",
41
+ inputSchema: { type: "object", properties: { session_id: { type: "string" }, text: { type: "string" } }, required: ["session_id", "text"] },
42
+ run: async ({ session_id, text }) => {
43
+ if (!text?.trim()) throw new Error("text is required");
44
+ return { posted: appendMessage(session_id, "desktop-codex", text) };
45
+ },
46
+ },
47
+ {
48
+ name: "list_muse_tasks",
49
+ description: "List recent tasks Meta Muse sent to agents on this PC, with prompts and results.",
50
+ inputSchema: { type: "object", properties: {} },
51
+ run: async () =>
52
+ Object.values(loadTasks()).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20),
53
+ },
54
+ ],
55
+ });
56
+
57
+ const rl = readline.createInterface({ input: process.stdin });
58
+ rl.on("line", async (line) => {
59
+ if (!line.trim()) return;
60
+ let msg;
61
+ try { msg = JSON.parse(line); } catch { return; }
62
+ const reply = await handle(msg);
63
+ if (reply) process.stdout.write(JSON.stringify(reply) + "\n");
64
+ });
@@ -0,0 +1,153 @@
1
+ // Codex app-server client used to discover, inspect, and continue real Codex threads.
2
+ // The protocol is JSONL over stdio. Keeping this client short-lived avoids owning a
3
+ // second long-running Codex service while still using the same persisted thread store
4
+ // as Codex Desktop.
5
+ import readline from "node:readline";
6
+ import { spawn } from "node:child_process";
7
+
8
+ const FINAL_TURN_STATUSES = new Set(["completed", "failed", "interrupted"]);
9
+
10
+ class CodexAppServer {
11
+ constructor(codexPath, { cwd, timeoutMs = 120_000, onNotification } = {}) {
12
+ this.child = spawn(codexPath, ["app-server", "--stdio"], {
13
+ cwd, windowsHide: true, env: process.env, stdio: ["pipe", "pipe", "pipe"],
14
+ });
15
+ this.pending = new Map();
16
+ this.nextId = 1;
17
+ this.stderr = "";
18
+ this.timeoutMs = timeoutMs;
19
+ this.onNotification = onNotification;
20
+ this.ready = new Promise((resolve, reject) => {
21
+ this.child.once("spawn", resolve);
22
+ this.child.once("error", reject);
23
+ });
24
+ this.child.stderr.on("data", (chunk) => { this.stderr = (this.stderr + chunk).slice(-8_000); });
25
+ readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.handleLine(line));
26
+ this.child.on("close", (code) => {
27
+ const error = new Error(this.stderr.trim() || `Codex app-server exited with code ${code}`);
28
+ for (const { reject, timer } of this.pending.values()) { clearTimeout(timer); reject(error); }
29
+ this.pending.clear();
30
+ });
31
+ }
32
+
33
+ handleLine(line) {
34
+ let msg;
35
+ try { msg = JSON.parse(line); } catch { return; }
36
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
37
+ const pending = this.pending.get(msg.id);
38
+ this.pending.delete(msg.id);
39
+ clearTimeout(pending.timer);
40
+ if (msg.error) pending.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
41
+ else pending.resolve(msg.result);
42
+ return;
43
+ }
44
+ this.onNotification?.(msg);
45
+ }
46
+
47
+ async request(method, params = {}) {
48
+ await this.ready;
49
+ const id = this.nextId++;
50
+ return new Promise((resolve, reject) => {
51
+ const timer = setTimeout(() => {
52
+ this.pending.delete(id);
53
+ reject(new Error(`Timed out waiting for ${method}`));
54
+ }, this.timeoutMs);
55
+ this.pending.set(id, { resolve, reject, timer });
56
+ this.child.stdin.write(JSON.stringify({ method, id, params }) + "\n");
57
+ });
58
+ }
59
+
60
+ notify(method, params = {}) {
61
+ this.child.stdin.write(JSON.stringify({ method, params }) + "\n");
62
+ }
63
+
64
+ close() {
65
+ this.child.stdin.end();
66
+ setTimeout(() => this.child.kill(), 1_000).unref();
67
+ }
68
+ }
69
+
70
+ async function withServer(codexPath, options, fn) {
71
+ const server = new CodexAppServer(codexPath, options);
72
+ try {
73
+ await server.request("initialize", {
74
+ clientInfo: { name: "claude-muse-bridge", title: "Claude Muse Bridge", version: "1.2.0" },
75
+ capabilities: { experimentalApi: true },
76
+ });
77
+ server.notify("initialized", {});
78
+ return await fn(server);
79
+ } finally {
80
+ server.close();
81
+ }
82
+ }
83
+
84
+ export async function listCodexSessions(codexPath, { cwd, limit = 30 } = {}) {
85
+ return withServer(codexPath, { cwd }, async (server) => {
86
+ const result = await server.request("thread/list", {
87
+ limit, sortKey: "recency_at", sortDirection: "desc", sourceKinds: [],
88
+ });
89
+ return result?.data || result?.threads || [];
90
+ });
91
+ }
92
+
93
+ export async function readCodexSession(codexPath, threadId, { cwd } = {}) {
94
+ return withServer(codexPath, { cwd }, async (server) => {
95
+ const result = await server.request("thread/read", { threadId, includeTurns: true });
96
+ return result?.thread || result;
97
+ });
98
+ }
99
+
100
+ function agentText(item) {
101
+ if (!item || item.type !== "agentMessage") return "";
102
+ if (typeof item.text === "string") return item.text;
103
+ if (typeof item.content === "string") return item.content;
104
+ if (Array.isArray(item.content)) return item.content.map((part) => part?.text || "").join("");
105
+ return "";
106
+ }
107
+
108
+ export async function runCodexSessionTurn(codexPath, threadId, prompt, {
109
+ cwd, timeoutMs = 30 * 60_000, onUpdate,
110
+ } = {}) {
111
+ let turnId = null;
112
+ let text = "";
113
+ let finish;
114
+ const completed = new Promise((resolve, reject) => { finish = { resolve, reject }; });
115
+
116
+ const onNotification = (msg) => {
117
+ const method = msg.method || "";
118
+ const params = msg.params || {};
119
+ const notificationTurnId = params.turn?.id || params.turnId;
120
+ if (turnId && notificationTurnId && notificationTurnId !== turnId) return;
121
+ if (method === "item/agentMessage/delta" && typeof params.delta === "string") {
122
+ text += params.delta;
123
+ onUpdate?.(text);
124
+ } else if (method === "item/completed") {
125
+ const completeText = agentText(params.item);
126
+ if (completeText) { text = completeText; onUpdate?.(text); }
127
+ } else if (method === "turn/completed" && (!turnId || notificationTurnId === turnId)) {
128
+ const status = params.turn?.status || "completed";
129
+ if (status === "completed") finish.resolve({ text, turn: params.turn });
130
+ else finish.reject(new Error(params.turn?.error?.message || `Codex turn ${status}`));
131
+ }
132
+ };
133
+
134
+ return withServer(codexPath, { cwd, timeoutMs, onNotification }, async (server) => {
135
+ await server.request("thread/resume", {
136
+ threadId, cwd: cwd || null, approvalPolicy: "never", sandbox: "danger-full-access", excludeTurns: true,
137
+ });
138
+ const started = await server.request("turn/start", {
139
+ threadId,
140
+ input: [{ type: "text", text: prompt }],
141
+ cwd: cwd || null,
142
+ approvalPolicy: "never",
143
+ sandboxPolicy: { type: "dangerFullAccess" },
144
+ turnTrigger: "muse-bridge",
145
+ });
146
+ turnId = started?.turn?.id || started?.id || null;
147
+ const result = await completed;
148
+ if (result.turn?.status && !FINAL_TURN_STATUSES.has(result.turn.status)) {
149
+ throw new Error(`Unexpected Codex turn status: ${result.turn.status}`);
150
+ }
151
+ return { ...result, threadId, turnId };
152
+ });
153
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "port": 8787,
3
+ "workdir": "workspace",
4
+ "permissionMode": "bypassPermissions",
5
+ "allowedTools": [],
6
+ "requireApproval": true,
7
+ "approvalTimeoutMinutes": 30,
8
+ "ntfyServer": "https://ntfy.sh",
9
+ "publicUrl": null,
10
+ "allowedIps": [],
11
+ "ipHeader": "cf-connecting-ip",
12
+ "model": null,
13
+ "taskTimeoutMinutes": 30,
14
+ "claudePath": "claude",
15
+ "codexPath": "codex",
16
+ "token": null,
17
+ "approvalSecret": null,
18
+ "ntfyTopic": null
19
+ }
@@ -0,0 +1,64 @@
1
+ // Local stdio MCP server for Claude Desktop / Claude Code: read what Muse sent, reply to Muse,
2
+ // and see tasks Muse has run. Never exposed to the network.
3
+ import readline from "node:readline";
4
+ import { createMcpHandler, postMessage, readMessages, loadTasks, TO_DESKTOP, TO_MUSE } from "./lib.mjs";
5
+ import { appendMessage, readThread, listThreads } from "./threads.mjs";
6
+
7
+ const handle = createMcpHandler({
8
+ name: "muse-inbox",
9
+ version: "1.0.0",
10
+ instructions: "Messages and tasks exchanged with Meta Muse on the user's phone.",
11
+ tools: [
12
+ {
13
+ name: "read_muse_messages",
14
+ description: "Read messages Meta Muse (on the user's phone) has left for Claude Desktop. Returns unread ones and marks them read.",
15
+ inputSchema: { type: "object", properties: { include_read: { type: "boolean", description: "Return the last 50 instead of only unread." } } },
16
+ run: async ({ include_read }) => {
17
+ const messages = readMessages(TO_DESKTOP, { unreadOnly: !include_read });
18
+ return messages.length ? { messages } : { messages: [], note: "No new messages from Muse." };
19
+ },
20
+ },
21
+ {
22
+ name: "send_to_muse",
23
+ description: "Send a message to Meta Muse on the user's phone. Muse sees it next time it checks read_desktop_messages.",
24
+ inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
25
+ run: async ({ text }) => {
26
+ if (!text?.trim()) throw new Error("text is required");
27
+ return { sent: postMessage(TO_MUSE, text, "claude-desktop") };
28
+ },
29
+ },
30
+ {
31
+ name: "read_side_thread",
32
+ description:
33
+ "Read the side thread between Meta Muse and one of the user's sessions (an agent-to-agent channel kept out of the " +
34
+ "user's conversation). Messages are labeled muse / side-claude / desktop-claude / user. Omit session_id to list threads.",
35
+ inputSchema: { type: "object", properties: { session_id: { type: "string" } } },
36
+ run: async ({ session_id }) => (session_id ? { messages: readThread(session_id).slice(-100) } : { threads: listThreads() }),
37
+ },
38
+ {
39
+ name: "post_to_side_thread",
40
+ description: "Post into a session's side thread with Meta Muse, labeled as desktop-claude. Muse sees it with side_thread_read.",
41
+ inputSchema: { type: "object", properties: { session_id: { type: "string" }, text: { type: "string" } }, required: ["session_id", "text"] },
42
+ run: async ({ session_id, text }) => {
43
+ if (!text?.trim()) throw new Error("text is required");
44
+ return { posted: appendMessage(session_id, "desktop-claude", text) };
45
+ },
46
+ },
47
+ {
48
+ name: "list_muse_tasks",
49
+ description: "List recent tasks Meta Muse sent to Claude on this PC, with prompts and results.",
50
+ inputSchema: { type: "object", properties: {} },
51
+ run: async () =>
52
+ Object.values(loadTasks()).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20),
53
+ },
54
+ ],
55
+ });
56
+
57
+ const rl = readline.createInterface({ input: process.stdin });
58
+ rl.on("line", async (line) => {
59
+ if (!line.trim()) return;
60
+ let msg;
61
+ try { msg = JSON.parse(line); } catch { return; }
62
+ const reply = await handle(msg);
63
+ if (reply) process.stdout.write(JSON.stringify(reply) + "\n");
64
+ });