starmux 0.13.1 → 0.14.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.
Files changed (142) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -28
  3. package/dist/src/access-control.js +1 -2
  4. package/dist/src/access-dashboard.js +2 -3
  5. package/dist/src/account-access-policy.js +54 -0
  6. package/dist/src/authorized-workspace-roots.js +0 -1
  7. package/dist/src/batch-read-policy.js +30 -0
  8. package/dist/src/cli/instances.js +6 -175
  9. package/dist/src/cli/oauth.js +0 -1
  10. package/dist/src/cli/starmux-status.js +10 -14
  11. package/dist/src/cli/starmux-supervisor.js +0 -1
  12. package/dist/src/cli/starmux.js +13 -25
  13. package/dist/src/config-schemas.js +0 -9
  14. package/dist/src/configured-mcp-upstreams.js +0 -1
  15. package/dist/src/contracts.js +0 -1
  16. package/dist/src/current-permission-probe.js +0 -1
  17. package/dist/src/dc-workspace-http.js +57 -33
  18. package/dist/src/deny-dashboard.js +2 -3
  19. package/dist/src/desktop-commander-facade.js +4 -4
  20. package/dist/src/explicit-deny-rules.js +2 -3
  21. package/dist/src/global-core-control.js +2 -3
  22. package/dist/src/home-write-sandbox.js +0 -1
  23. package/dist/src/http-host-policy.js +20 -0
  24. package/dist/src/instance-runtime.js +3 -3
  25. package/dist/src/instance-store.js +28 -22
  26. package/dist/src/instance-worker.js +2 -3
  27. package/dist/src/invocation.js +1 -2
  28. package/dist/src/local-desktop-read-probe.js +5 -88
  29. package/dist/src/managed-workspace-policy.js +38 -4
  30. package/dist/src/mcp-results.js +0 -1
  31. package/dist/src/mcp-upstream-router.js +0 -1
  32. package/dist/src/oauth-keycloak.js +0 -1
  33. package/dist/src/oauth-resource.js +0 -1
  34. package/dist/src/ops-dashboard.js +2 -179
  35. package/dist/src/permissions-dashboard.js +3 -4
  36. package/dist/src/platform.js +3 -4
  37. package/dist/src/privacy-diagnostics.js +0 -1
  38. package/dist/src/private-mcp-activity.js +0 -1
  39. package/dist/src/private-store-lock.js +0 -1
  40. package/dist/src/project-dev-presets.js +0 -1
  41. package/dist/src/project-git-workflow.js +0 -1
  42. package/dist/src/project-workspace-policy.js +10 -6
  43. package/dist/src/runtime-identity.js +0 -14
  44. package/dist/src/runtime-paths.js +0 -3
  45. package/dist/src/tool-dispatch.js +45 -46
  46. package/dist/src/validation.js +0 -1
  47. package/dist/src/workspace-grants.js +1 -2
  48. package/package.json +5 -15
  49. package/scripts/prepare.mjs +1 -1
  50. package/CONTRIBUTING.md +0 -18
  51. package/SECURITY.md +0 -7
  52. package/app/README.md +0 -29
  53. package/dist/src/access-control.js.map +0 -1
  54. package/dist/src/access-dashboard.js.map +0 -1
  55. package/dist/src/authorized-workspace-roots.js.map +0 -1
  56. package/dist/src/cli/access.js +0 -53
  57. package/dist/src/cli/access.js.map +0 -1
  58. package/dist/src/cli/community-remote-revoke.js +0 -32
  59. package/dist/src/cli/community-remote-revoke.js.map +0 -1
  60. package/dist/src/cli/instances.js.map +0 -1
  61. package/dist/src/cli/live-home-settings.js +0 -35
  62. package/dist/src/cli/live-home-settings.js.map +0 -1
  63. package/dist/src/cli/mac-mcp-gateway.js +0 -257
  64. package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
  65. package/dist/src/cli/oauth.js.map +0 -1
  66. package/dist/src/cli/permissions-owner.js +0 -27
  67. package/dist/src/cli/permissions-owner.js.map +0 -1
  68. package/dist/src/cli/service-setup.js +0 -26
  69. package/dist/src/cli/service-setup.js.map +0 -1
  70. package/dist/src/cli/setup-wizard.js +0 -149
  71. package/dist/src/cli/setup-wizard.js.map +0 -1
  72. package/dist/src/cli/starmux-status.js.map +0 -1
  73. package/dist/src/cli/starmux-supervisor.js.map +0 -1
  74. package/dist/src/cli/starmux.js.map +0 -1
  75. package/dist/src/cli/verify-setup.js +0 -53
  76. package/dist/src/cli/verify-setup.js.map +0 -1
  77. package/dist/src/community-remote-revocations.js +0 -135
  78. package/dist/src/community-remote-revocations.js.map +0 -1
  79. package/dist/src/config-schemas.js.map +0 -1
  80. package/dist/src/configured-mcp-upstreams.js.map +0 -1
  81. package/dist/src/contracts.js.map +0 -1
  82. package/dist/src/current-permission-probe.js.map +0 -1
  83. package/dist/src/dc-workspace-http.js.map +0 -1
  84. package/dist/src/deny-dashboard.js.map +0 -1
  85. package/dist/src/desktop-commander-facade.js.map +0 -1
  86. package/dist/src/explicit-deny-rules.js.map +0 -1
  87. package/dist/src/gateway-status-format.js +0 -66
  88. package/dist/src/gateway-status-format.js.map +0 -1
  89. package/dist/src/global-core-control.js.map +0 -1
  90. package/dist/src/home-write-sandbox.js.map +0 -1
  91. package/dist/src/instance-migration.js +0 -138
  92. package/dist/src/instance-migration.js.map +0 -1
  93. package/dist/src/instance-runtime.js.map +0 -1
  94. package/dist/src/instance-store.js.map +0 -1
  95. package/dist/src/instance-worker.js.map +0 -1
  96. package/dist/src/invocation.js.map +0 -1
  97. package/dist/src/live-profile-home-settings.js +0 -80
  98. package/dist/src/live-profile-home-settings.js.map +0 -1
  99. package/dist/src/local-desktop-read-probe.js.map +0 -1
  100. package/dist/src/managed-workspace-policy.js.map +0 -1
  101. package/dist/src/mcp-results.js.map +0 -1
  102. package/dist/src/mcp-upstream-router.js.map +0 -1
  103. package/dist/src/oauth-keycloak.js.map +0 -1
  104. package/dist/src/oauth-resource.js.map +0 -1
  105. package/dist/src/operator-cli.js +0 -559
  106. package/dist/src/operator-cli.js.map +0 -1
  107. package/dist/src/ops-dashboard.js.map +0 -1
  108. package/dist/src/permissions-dashboard.js.map +0 -1
  109. package/dist/src/platform.js.map +0 -1
  110. package/dist/src/privacy-diagnostics.js.map +0 -1
  111. package/dist/src/private-mcp-activity.js.map +0 -1
  112. package/dist/src/private-store-lock.js.map +0 -1
  113. package/dist/src/project-dev-presets.js.map +0 -1
  114. package/dist/src/project-git-workflow.js.map +0 -1
  115. package/dist/src/project-workspace-policy.js.map +0 -1
  116. package/dist/src/read-scope.js +0 -44
  117. package/dist/src/read-scope.js.map +0 -1
  118. package/dist/src/release1-onboarding.js +0 -120
  119. package/dist/src/release1-onboarding.js.map +0 -1
  120. package/dist/src/release1-service.js +0 -131
  121. package/dist/src/release1-service.js.map +0 -1
  122. package/dist/src/remote-auth.js +0 -47
  123. package/dist/src/remote-auth.js.map +0 -1
  124. package/dist/src/runtime-identity.js.map +0 -1
  125. package/dist/src/runtime-paths.js.map +0 -1
  126. package/dist/src/starmux-migration.js +0 -172
  127. package/dist/src/starmux-migration.js.map +0 -1
  128. package/dist/src/tool-dispatch.js.map +0 -1
  129. package/dist/src/validation.js.map +0 -1
  130. package/dist/src/workspace-grants.js.map +0 -1
  131. package/docs/ACCESS_CONTROL.md +0 -51
  132. package/docs/CLIENTS.md +0 -22
  133. package/docs/CONNECT_CHATGPT.md +0 -148
  134. package/docs/INSTALL.md +0 -62
  135. package/docs/INSTANCES.md +0 -83
  136. package/docs/OAUTH.md +0 -116
  137. package/docs/PLATFORMS.md +0 -29
  138. package/docs/README.md +0 -16
  139. package/docs/REMOTE_SETUP.md +0 -280
  140. package/docs/SECURITY.md +0 -15
  141. package/src/dc-workspace-http.mjs +0 -3
  142. package/test/fixtures/p1-fixture.txt +0 -2
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Mac MCP Gateway contributors
3
+ Copyright (c) 2026 Starmux contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -24,7 +24,7 @@ Cloud AI assistants (like **ChatGPT Web**, **Claude**, **Codex**, or **WorkBuddy
24
24
 
25
25
  - **Batteries-Included Workspace Tools:** Powered by a built-in [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) engine (installed automatically via npm). Out of the box, your AI can read/write files, search code, run Shell commands, and manage Git.
26
26
  - **Code on Any Host (macOS, Linux, Windows & Edge Devices):** Many AI clients (like **ChatGPT Desktop** or **WorkBuddy**) have no native Linux or ARM64 client. Because Starmux runs anywhere **Node.js 22.12+** runs—including **macOS**, **Linux (`x64` / `arm64`)**, **NVIDIA Jetson**, headless servers, and **Windows**—you can use your favorite AI client on your main computer or browser to develop directly on any target machine.
27
- - **Safe by Default:** Starts in strict **Read-Only** mode (`starmux up`). Explicitly opt into **Coding Mode** (`starmux up --code`) only when you want AI to edit files or run terminal commands.
27
+ - **Two Clear Permission Modes:** Starts in global **Read-Only** mode (`starmux up`): AI can read files your OS account and the upstream server permit, while writes and arbitrary execution are blocked. Explicitly opt into **Coding Mode** (`starmux up --code`) to use native tools with your OS account permissions.
28
28
  - **Your Network, Your Keys:** No vendor-hosted relay required. Protect your gateway with Bearer **API Keys**, browser-based **OAuth**, or your existing **Tunnel / Reverse Proxy** access controls. Revoke access instantly at any time.
29
29
 
30
30
  ---
@@ -44,7 +44,7 @@ npm install -g starmux
44
44
  ```bash
45
45
  cd /path/to/your/project
46
46
 
47
- # Option A: Safe read-only mode (default — inspect files & code)
47
+ # Option A: Global read-only mode (default — inspect files & code)
48
48
  starmux up
49
49
 
50
50
  # Option B: Full coding mode (edit files, run Shell & Git)
@@ -53,28 +53,18 @@ starmux up --code
53
53
 
54
54
  Starmux initializes itself and runs in the background on port `18893` (coding mode asks for confirmation the first time):
55
55
 
56
- ```text
57
- ● Starmux is running
58
- Listen http://127.0.0.1:18893/mcp (this computer only)
59
- Workspace /path/to/your/project
60
- Mode Read-only (run 'starmux up --code' to enable edits & shell)
61
- Auth None (no token required)
62
- Runtime Background · continues after this terminal closes
63
-
64
- Connect an AI client (MCP settings)
65
- Transport Streamable HTTP
66
- URL (Local) http://127.0.0.1:18893/mcp (AI client on this computer)
67
- URL (Remote) https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:18893)
68
- Authentication None (optional Bearer API key: starmux auth key "My AI Client")
69
-
70
- Commands
71
- Verify starmux verify
72
- Auth & keys starmux auth
73
- Dashboard starmux dashboard (view & copy settings in browser)
74
- Logs starmux logs -f
75
- Stop starmux down
56
+ Switch back from Coding mode with
57
+
58
+ ```
59
+ starmux up --read
76
60
  ```
77
61
 
62
+ Add `-i NAME` for a named instance. Both `up` and `status` show the mode-switch command.
63
+
64
+ `Project` provides context for relative file paths in Read-only mode; it does not restrict access to that directory. Read-only applies across the computer wherever the OS account and upstream server permit reading. Coding forwards native operations with the account's permissions. Neither mode uses Starmux directory grants or path exclusions.
65
+
66
+ In Read-only mode, `read_multiple_files` supports up to 20 files and 4 MiB of combined file sizes per batch. Missing files retain Desktop Commander's per-file errors alongside successful contents. Oversized batches return `BATCH_READ_COUNT_LIMIT` or `BATCH_READ_SIZE_LIMIT` with guidance to split the request.
67
+
78
68
  ### 3. Add Starmux to your AI client
79
69
 
80
70
  Need a Bearer API key for remote access? Generate one first (optional for local use):
@@ -176,7 +166,7 @@ See [Client Compatibility](docs/CLIENTS.md) and [Remote Setup](docs/REMOTE_SETUP
176
166
 
177
167
  ---
178
168
 
179
- ## Everyday CLI & Multi-Workspace
169
+ ## Everyday CLI & Multiple Instances
180
170
 
181
171
  Manage your gateway with simple, human-friendly commands:
182
172
 
@@ -190,11 +180,11 @@ starmux logs -f # Stream live gateway logs
190
180
  starmux down # Stop the gateway (alias: stop)
191
181
  ```
192
182
 
193
- After initialization, `starmux up` reuses your saved workspace. Use `starmux up .` to switch it to your current directory, or pass `--foreground` to keep the process in your terminal.
183
+ After initialization, `starmux up` reuses your instance's saved project context. Use `starmux up .` to change it to your current directory, or pass `--foreground` to keep the process in your terminal. Only a new instance defaults to the current directory when no path is supplied.
194
184
 
195
- ### Advanced: Running multiple workspaces (`-i`)
185
+ ### Advanced: Running multiple instances (`-i`)
196
186
 
197
- *(Optional)* By default, Starmux manages a single `default` workspace so you never have to pass extra flags. When you need to expose multiple projects simultaneously or run different permission modes on the same machine, pass `-i <name>` to create isolated instances with their own ports, credentials, policies, and logs:
187
+ *(Optional)* By default, Starmux manages a single `default` instance so you never have to pass extra flags. To use different project contexts or permission modes on the same machine, pass `-i <name>` to create instances with their own ports, credentials, configuration, and logs. Instances share the OS account; they are not filesystem sandboxes:
198
188
 
199
189
  ```bash
200
190
  starmux up ~/work/repo-a -i work --code
@@ -238,8 +228,8 @@ Explore the `docs/` guides by topic:
238
228
  - [Access Control & API Keys](docs/ACCESS_CONTROL.md) — Creating/revoking Bearer API keys, expiration policies, and using the local Web Access Console.
239
229
  - [ChatGPT Web & OAuth 2.0](docs/OAUTH.md) & [Connect ChatGPT](docs/CONNECT_CHATGPT.md) — Configuring Keycloak OAuth 2.0 + PKCE and OpenAI Secure MCP Tunnel.
240
230
  - **⚙️ Advanced Configuration & Security**
241
- - [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing multiple isolated workspaces, hot-reloading `config.jsonc`, mounting extra `stdio` MCP upstreams, and secrets.
242
- - [Security & Workspace Permissions](docs/SECURITY.md) — Understanding Read-Only vs. Coding Mode, directory grants, tool deny rules, and macOS privacy boundaries.
231
+ - [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing instances, hot-reloading `config.jsonc`, mounting extra `stdio` MCP upstreams, and secrets.
232
+ - [Security & Permissions](docs/SECURITY.md) — Understanding global Read-Only vs. Coding Mode, upstream permissions, and macOS privacy boundaries.
243
233
  - [Client Compatibility](docs/CLIENTS.md) & [Platform Details](docs/PLATFORMS.md) — Tested AI client matrix and macOS / Linux / Windows runtime specifics.
244
234
  - **🛠️ Reference & Project**
245
235
  - [Documentation Index](docs/README.md) · [Installation](docs/INSTALL.md) · [Contributing](CONTRIBUTING.md)
@@ -12,7 +12,7 @@ const schema = z.object({ version: z.literal(1), mode: z.enum(['ingress', 'api-k
12
12
  oauth: oauthConfigSchema.optional(), oauthConnections: z.array(oauthConnectionSchema).max(512).default([]) }).strict();
13
13
  const digest = (value) => createHash('sha256').update(value).digest('hex');
14
14
  const equal = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
15
- export const defaultAccessFile = () => process.env.MAC_MCP_ACCESS_FILE || path.join(homedir(), '.config', 'starmux', 'access-v1.json');
15
+ export const defaultAccessFile = () => process.env.STARMUX_ACCESS_FILE || path.join(homedir(), '.config', 'starmux', 'access-v1.json');
16
16
  const active = (c) => !c.revokedAt && (!c.expiresAt || Date.parse(c.expiresAt) > Date.now());
17
17
  export async function createAccessControl(file = defaultAccessFile(), options = {}) {
18
18
  if (!path.isAbsolute(file))
@@ -222,4 +222,3 @@ export async function createAccessControl(file = defaultAccessFile(), options =
222
222
  }
223
223
  return { file, snapshot, create, setMode, revoke, authenticate, ownerToken, authorized, configureOAuth, revokeOAuth, oauthSettings };
224
224
  }
225
- //# sourceMappingURL=access-control.js.map
@@ -8,8 +8,8 @@ const e = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<':
8
8
  export function accessPage(state, created) {
9
9
  const form = (op, fields, label) => `<form method="post" action="/access"><input type="hidden" name="op" value="${op}">${fields}<label>Owner management token <input type="password" name="token" required autocomplete="off"></label><button>${label}</button></form>`;
10
10
  return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Starmux · Access Control</title><style>body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 24px;background:#f5f7fa;color:#243249}section{background:white;border:1px solid #dae0e8;border-radius:12px;padding:24px;margin:20px 0}input,select,button{font:inherit;padding:8px;margin:8px}label{display:block}code{overflow-wrap:anywhere}td,th{text-align:left;padding:10px;border-bottom:1px solid #ddd}table{width:100%}</style><h1>Starmux · Access Control</h1>
11
- <p><a href="/dashboard">Dashboard</a> · <a href="/permissions">Workspace permissions</a></p>
12
- <section><h2>${state.mode === 'ingress' ? 'None (local or ingress-managed)' : state.mode === 'oauth' ? 'OAuth' : 'Bearer API Key'}</h2><p>Permission: Owner default. Credentials do not change workspace, tool or operating-system permissions.</p><p>${state.mode === 'ingress' ? 'Starmux does not verify a token. Restrict remote access through your HTTPS proxy or tunnel.' : 'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p>
11
+ <p><a href="/dashboard">Dashboard</a></p>
12
+ <section><h2>${state.mode === 'ingress' ? 'None (local or ingress-managed)' : state.mode === 'oauth' ? 'OAuth' : 'Bearer API Key'}</h2><p>Credentials do not change the instance’s mode or operating-system permissions.</p><p>${state.mode === 'ingress' ? 'Starmux does not verify a token. Restrict remote access through your HTTPS proxy or tunnel.' : 'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p>
13
13
  <table><tr><th>MCP Client Field</th><th>Value to Copy</th></tr>
14
14
  <tr><td>Transport</td><td><code>Streamable HTTP</code></td></tr>
15
15
  <tr><td>URL (Local)</td><td><code>http://127.0.0.1:&lt;PORT&gt;/mcp</code></td></tr>
@@ -74,4 +74,3 @@ export async function accessPost(req, access) {
74
74
  return { status: 400 };
75
75
  }
76
76
  }
77
- //# sourceMappingURL=access-dashboard.js.map
@@ -0,0 +1,54 @@
1
+ import { realpath, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { GatewayError, errorCode } from './validation.js';
5
+ import { prepareBatchRead } from './batch-read-policy.js';
6
+ // Reviewed Desktop Commander operations. Tool annotations are hints, not an
7
+ // authorization boundary; unknown tools cannot opt themselves into read-only.
8
+ const readOnlyTools = new Set(['health_check', 'get_config', 'read_file', 'read_multiple_files',
9
+ 'list_directory', 'get_file_info', 'start_search', 'get_more_search_results', 'stop_search',
10
+ 'list_searches', 'read_process_output', 'list_sessions', 'list_processes', 'get_usage_stats', 'get_recent_tool_calls']);
11
+ export const isReadOnlyTool = (name) => readOnlyTools.has(name);
12
+ export async function createAccountAccessPolicy(root, mode = 'read-only') {
13
+ if (!path.isAbsolute(root))
14
+ throw new GatewayError('ROOT_REQUIRED');
15
+ const base = await realpath(root);
16
+ if (!(await stat(base)).isDirectory())
17
+ throw new GatewayError('NOT_DIRECTORY');
18
+ function absolute(raw) {
19
+ if (typeof raw !== 'string' || !raw || raw.includes('\0'))
20
+ throw new GatewayError('INVALID_PATH');
21
+ const expanded = raw === '~' ? homedir() : raw.startsWith('~/') ? path.join(homedir(), raw.slice(2)) : raw;
22
+ return path.resolve(base, expanded);
23
+ }
24
+ const policy = {
25
+ root: base, mode, developerCommands: 'off',
26
+ async validateGitPath() { throw new GatewayError('READ_ONLY_MODE'); },
27
+ async prepare(name, args = {}) {
28
+ if (!isReadOnlyTool(name))
29
+ throw new GatewayError('READ_ONLY_MODE');
30
+ if (name === 'read_multiple_files')
31
+ return prepareBatchRead(args, policy.prepare);
32
+ if (['read_file', 'list_directory', 'get_file_info', 'start_search'].includes(name)) {
33
+ let target = absolute(args.path);
34
+ if (name === 'read_file') {
35
+ // Canonicalize existing links for accurate batch byte accounting, while
36
+ // letting upstream retain its native missing-file errors.
37
+ try {
38
+ target = await realpath(target);
39
+ const info = await stat(target);
40
+ if (info.isFile() && info.size > 1024 * 1024)
41
+ throw new GatewayError('FILE_TOO_LARGE');
42
+ }
43
+ catch (error) {
44
+ if (errorCode(error) !== 'ENOENT')
45
+ throw error;
46
+ }
47
+ }
48
+ return { name, arguments: { ...args, path: target } };
49
+ }
50
+ return { name, arguments: args };
51
+ }
52
+ };
53
+ return Object.freeze(policy);
54
+ }
@@ -74,4 +74,3 @@ export function createAuthorizedWorkspacePolicy({ basePolicy, linkedGitWorktrees
74
74
  }
75
75
  });
76
76
  }
77
- //# sourceMappingURL=authorized-workspace-roots.js.map
@@ -0,0 +1,30 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { GatewayError, errorCode } from './validation.js';
3
+ // Validate every file before forwarding any part of a batch upstream.
4
+ export async function prepareBatchRead(args, prepare) {
5
+ if (!args || Object.keys(args).some(key => key !== 'paths') || !Array.isArray(args.paths) ||
6
+ args.paths.length < 1 || args.paths.some(p => typeof p !== 'string'))
7
+ throw new GatewayError('BATCH_READ_ARGUMENTS_DENIED');
8
+ if (args.paths.length > 20)
9
+ throw new GatewayError('BATCH_READ_COUNT_LIMIT');
10
+ const paths = [];
11
+ let bytes = 0;
12
+ for (const raw of args.paths) {
13
+ const invocation = await prepare('read_file', { path: raw });
14
+ const target = invocation.arguments?.path;
15
+ if (typeof target !== 'string')
16
+ throw new GatewayError('BATCH_READ_PATH_INVALID');
17
+ const st = await lstat(target).catch(error => {
18
+ // Authorization already succeeded. Keep missing paths in the upstream
19
+ // batch so Desktop Commander can report ENOENT beside successful reads.
20
+ if (errorCode(error) === 'ENOENT')
21
+ return null;
22
+ throw error;
23
+ });
24
+ bytes += st?.size ?? 0;
25
+ if (bytes > 4 * 1024 * 1024)
26
+ throw new GatewayError('BATCH_READ_SIZE_LIMIT');
27
+ paths.push(target);
28
+ }
29
+ return { name: 'read_multiple_files', arguments: { paths } };
30
+ }
@@ -1,4 +1,3 @@
1
- import { createInterface } from 'node:readline/promises';
2
1
  import { spawn } from 'node:child_process';
3
2
  import { readFile, writeFile, mkdtemp, rm, realpath } from 'node:fs/promises';
4
3
  import { tmpdir, homedir } from 'node:os';
@@ -7,9 +6,9 @@ import { createAccessControl } from '../access-control.js';
7
6
  import { createWorkspaceGrants } from '../workspace-grants.js';
8
7
  import { createDenyRules } from '../explicit-deny-rules.js';
9
8
  import { createPrivateDirectory } from '../platform.js';
10
- import { instanceStatus, instanceRequest, runInstance } from '../instance-runtime.js';
11
- import { atomicPrivate, changeRegistry, configJSONSchema, sourceRevision, configTemplate, createInstance, exists, fingerprint, instanceConfigSchema, instanceEnv, instancePaths, instancesRoot, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig, withLock } from '../instance-store.js';
12
- const booleans = new Set(['--json', '--all', '--link', '--archived', '--approve-trusted-developer', '--approve-unrestricted', '--confirm', '--effective', '--allow-loopback-http']);
9
+ import { instanceStatus, instanceRequest } from '../instance-runtime.js';
10
+ import { atomicPrivate, changeRegistry, configJSONSchema, sourceRevision, configTemplate, validateConfig, withLock, createInstance, exists, fingerprint, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort } from '../instance-store.js';
11
+ const booleans = new Set(['--json', '--all', '--link', '--archived', '--approve-trusted-developer', '--approve-unrestricted', '--effective', '--allow-loopback-http']);
13
12
  function options(args) {
14
13
  const flags = new Map();
15
14
  const positional = [];
@@ -44,75 +43,10 @@ function requireApprovals(config, flags) {
44
43
  if (config.accessRole === 'trusted' && process.platform !== 'darwin' && !flags.has('--approve-unrestricted'))
45
44
  throw Error('UNRESTRICTED_APPROVAL_REQUIRED: add --approve-unrestricted');
46
45
  }
47
- async function wizard(flags, current) {
48
- let project = flags.get('--project') ?? current?.projectRoot;
49
- let role = flags.get('--access-role') ?? current?.accessRole ?? 'restricted';
50
- let port = Number(flags.get('--port') ?? current?.port ?? await suggestPort());
51
- if (!project) {
52
- if (!process.stdin.isTTY)
53
- throw Error('PROJECT_REQUIRED: use --project /absolute/directory');
54
- const rl = createInterface({ input: process.stdin, output: process.stderr });
55
- try {
56
- project = await rl.question('Work directory: ');
57
- role = (await rl.question('Capability (restricted / trusted) [restricted]: ')) || 'restricted';
58
- if (role === 'trusted' && (await rl.question('Enable native tools and Shell as your OS account? Type yes: ')) === 'yes') {
59
- flags.set('--approve-trusted-developer', 'true');
60
- if (process.platform !== 'darwin')
61
- flags.set('--approve-unrestricted', 'true');
62
- }
63
- port = Number((await rl.question(`Local port [${port}]: `)) || port);
64
- }
65
- finally {
66
- rl.close();
67
- }
68
- }
69
- const config = instanceConfigSchema.parse({ ...current, schemaVersion: 1, projectRoot: await realpath(path.resolve(project)), port,
70
- accessRole: role, mode: current?.accessRole === role ? current.mode : role === 'trusted' ? 'read-write' : 'read-only', writeScope: current?.accessRole === role ? current.writeScope : role === 'trusted' && process.platform === 'darwin' ? 'home-only' : 'unrestricted', connection: flags.get('--connection') ?? current?.connection ?? 'local' });
71
- requireApprovals(config, flags);
72
- return validateConfig(config);
73
- }
74
- export async function maybeHandleInstances(argv) {
75
- let command = argv[0] ?? 'help';
76
- if (command === 'start-setup' && !process.env.MAC_MCP_INTERNAL_SETUP_OWNER_PID) {
77
- command = 'run';
78
- argv = ['run', '--profile', 'setup', ...argv.slice(1)];
79
- }
80
- if (command === 'doctor-setup') {
81
- command = 'doctor';
82
- argv = ['doctor', '--profile', 'setup', ...argv.slice(1)];
83
- }
84
- const supported = ['instance', 'config', 'run', 'status', 'stop', 'restart', 'logs', 'doctor', 'verify', 'access', 'permissions', 'privacy', 'setup'];
85
- if (!supported.includes(command))
86
- return false;
87
- try {
88
- const explicit = argv.includes('--instance');
89
- const registry = await readRegistry();
90
- const managed = registry.instances.length > 0;
91
- const firstWizard = command === 'setup' && argv.length === 1 && process.stdin.isTTY;
92
- if (!['instance', 'config'].includes(command) && !explicit && !managed && !firstWizard)
93
- return false;
94
- // A retained legacy profile remains available until the owner migrates it.
95
- const profileIndex = argv.indexOf('--profile');
96
- if (profileIndex >= 0 && !registry.instances.some(i => i.legacy === argv[profileIndex + 1]) && argv[profileIndex + 1] !== 'auto') {
97
- if (registry.instances.some(i => i.legacy))
98
- throw Error('LEGACY_PROFILE_NOT_MIGRATED_USE_INSTANCE: select --instance explicitly');
99
- return false;
100
- }
101
- await handle(command, argv.slice(1));
102
- }
103
- catch (error) {
104
- if (argv.includes('--json'))
105
- output({ schemaVersion: 1, error: message(error) });
106
- else
107
- process.stderr.write(message(error) + '\n');
108
- process.exitCode = 1;
109
- }
110
- return true;
111
- }
112
46
  export async function handle(command, args) {
113
47
  const { flags, positional } = options(args);
114
48
  const json = flags.has('--json');
115
- const known = new Set([...booleans, '--instance', '--name', '--project', '--port', '--access-role', '--connection', '--profile', '--app-parent-pid', '--revision', '--patch', '--file', '--output', '--lines', '--expires', '--issuer', '--resource', '--resource-aliases', '--client-id', '--subjects', '--registration', '--token-auth', '--callback', '--target', '--mode']);
49
+ const known = new Set([...booleans, '--instance', '--name', '--project', '--port', '--access-role', '--connection', '--app-parent-pid', '--revision', '--patch', '--file', '--output', '--lines', '--expires', '--issuer', '--resource', '--resource-aliases', '--client-id', '--subjects', '--registration', '--token-auth', '--callback', '--target', '--mode']);
116
50
  for (const flag of flags.keys())
117
51
  if (!known.has(flag))
118
52
  throw Error('UNKNOWN_OPTION: ' + flag);
@@ -152,12 +86,6 @@ export async function handle(command, args) {
152
86
  }
153
87
  return;
154
88
  }
155
- if (op === 'create') {
156
- const config = await wizard(flags);
157
- const instance = await createInstance(positional[1] ?? flags.get('--name') ?? 'default', { config });
158
- output({ instance, message: 'Saved. Start with: starmux up -i ' + JSON.stringify(instance.name) });
159
- return;
160
- }
161
89
  if (op === 'import') {
162
90
  const file = positional[1];
163
91
  if (!file)
@@ -168,11 +96,6 @@ export async function handle(command, args) {
168
96
  message: flags.has('--link') ? 'Bound to original file. Changes require apply.' : 'Imported a private copy. The original file is no longer used.' });
169
97
  return;
170
98
  }
171
- if (op === 'migrate') {
172
- const { migrateLegacy } = await import('../instance-migration.js');
173
- output(await migrateLegacy(positional[1], flags.has('--confirm'), flags.get('--name')));
174
- return;
175
- }
176
99
  const instance = await resolveInstance(positional[1] ?? flags.get('--instance'), homedir(), op === 'restore');
177
100
  if (op === 'copy') {
178
101
  const config = { ...(await loadInstanceConfig(instance)).config, port: await suggestPort() };
@@ -222,47 +145,9 @@ export async function handle(command, args) {
222
145
  process.stdout.write(`Restored instance '${instance.name}'.\n`);
223
146
  return;
224
147
  }
225
- if (command === 'status' && !flags.has('--instance') && (!json || flags.has('--all')))
226
- return handle('instance', ['list', ...(json ? ['--json'] : [])]);
227
- const profile = flags.get('--profile');
228
- let selector = flags.get('--instance');
229
- if (profile && profile !== 'auto') {
230
- const legacy = (await readRegistry()).instances.find(i => i.legacy === profile);
231
- if (!legacy)
232
- throw Error('LEGACY_PROFILE_NOT_MIGRATED');
233
- selector = legacy.id;
234
- }
235
- if (command === 'setup' && !(await readRegistry()).instances.length) {
236
- await handle('instance', ['create', 'default', ...args]);
237
- return;
238
- }
148
+ const selector = flags.get('--instance');
239
149
  const instance = await resolveInstance(selector);
240
150
  const p = instancePaths(instance);
241
- if (command === 'status') {
242
- output(await instanceStatus(instance));
243
- return;
244
- }
245
- if (command === 'run') {
246
- if (flags.has('--port'))
247
- throw Error('INSTANCE_PORT_OVERRIDE_DENIED: edit configuration and apply');
248
- await runInstance(instance, { json, ...(flags.has('--app-parent-pid') ? { appParentPid: Number(flags.get('--app-parent-pid')) } : {}) });
249
- return;
250
- }
251
- if (command === 'stop') {
252
- output({ result: (await instanceRequest(instance, 'stop')) ? 'STOPPED' : 'ALREADY_STOPPED', instanceId: instance.id });
253
- return;
254
- }
255
- if (command === 'restart') {
256
- await handle('config', ['apply', '--instance', instance.id, ...(json ? ['--json'] : [])]);
257
- return;
258
- }
259
- if (command === 'setup') {
260
- const current = await loadInstanceConfig(instance);
261
- const config = await wizard(flags, current.config);
262
- await patchInstanceConfig(instance, config, current.revision);
263
- output({ result: 'SAVED', instanceId: instance.id, message: 'Apply changes to restart the running instance.' });
264
- return;
265
- }
266
151
  if (command === 'config') {
267
152
  const op = positional[0] ?? 'show';
268
153
  if (op === 'path') {
@@ -444,29 +329,6 @@ export async function handle(command, args) {
444
329
  }
445
330
  throw Error('CONFIG_ACTION_INVALID');
446
331
  }
447
- if (command === 'logs') {
448
- const file = path.join(p.logs, 'core.log');
449
- const lines = Math.min(200, Math.max(1, Number(flags.get('--lines') ?? 60)));
450
- if (!Number.isSafeInteger(Number(flags.get('--lines') ?? 60)) || Number(flags.get('--lines') ?? 60) < 1 || Number(flags.get('--lines') ?? 60) > 200)
451
- throw Error('INVALID_LINES: expected 1..200');
452
- const text = await exists(file) ? (await readTrusted(file, true, 32 * 1024 * 1024)).split('\n').slice(-lines).join('\n') : 'No logs yet.';
453
- if (json)
454
- output({ instanceId: instance.id, output: text });
455
- else
456
- process.stdout.write(text + '\n');
457
- return;
458
- }
459
- if (command === 'access') {
460
- process.env.MAC_MCP_ACCESS_FILE = p.access;
461
- const clean = args.filter((_, i) => args[i] !== '--instance' && args[i - 1] !== '--instance' && args[i] !== '--profile' && args[i - 1] !== '--profile');
462
- await (await import('./access.js')).accessCommand(clean);
463
- if (positional[0] === 'mode') {
464
- const mode = positional[1];
465
- if (mode === 'ingress' || mode === 'api-key' || mode === 'oauth')
466
- await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = mode; });
467
- }
468
- return;
469
- }
470
332
  if (command === 'permissions') {
471
333
  const grants = await createWorkspaceGrants({ file: p.grants });
472
334
  const deny = await createDenyRules({ file: p.deny });
@@ -482,15 +344,6 @@ export async function handle(command, args) {
482
344
  }
483
345
  return;
484
346
  }
485
- const loaded = await loadInstanceConfig(instance);
486
- if (command === 'doctor') {
487
- const { ensureAccessReady } = await import('../instance-store.js');
488
- await ensureAccessReady(instance);
489
- const status = await instanceStatus(instance);
490
- output({ result: 'PASS', instanceId: instance.id, source: instance.source, config: loaded.config, configState: status.configState,
491
- message: 'Configuration and access state validated. Run verify after starting to check real tools.' });
492
- return;
493
- }
494
347
  if (command === 'privacy') {
495
348
  const status = await instanceStatus(instance);
496
349
  if (!status.runtime?.port)
@@ -498,27 +351,5 @@ export async function handle(command, args) {
498
351
  output(await (await import('../privacy-diagnostics.js')).privacyReport({ port: status.runtime.port, home: homedir(), all: flags.has('--all'), target: flags.get('--target') ?? null }));
499
352
  return;
500
353
  }
501
- if (command === 'verify') {
502
- const state = await instanceRequest(instance, 'status');
503
- if (state?.phase !== 'running' || !state.port)
504
- throw Error('INSTANCE_NOT_RUNNING');
505
- const endpoint = `http://127.0.0.1:${state.port}`;
506
- const health = await fetch(endpoint + '/healthz', { signal: AbortSignal.timeout(3000) }).then(r => r.json());
507
- if (health.instanceId !== instance.id)
508
- throw Error('INSTANCE_HEALTH_IDENTITY_MISMATCH');
509
- const request = async (method, params) => {
510
- const reply = await fetch(endpoint + '/mcp', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...(process.env.MAC_MCP_API_KEY ? { authorization: 'Bearer ' + process.env.MAC_MCP_API_KEY } : {}) }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(15000) });
511
- if (!reply.ok)
512
- throw Error(reply.status === 401 ? 'VERIFICATION_CREDENTIAL_REQUIRED: set MAC_MCP_API_KEY' : 'MCP_VERIFICATION_FAILED');
513
- const data = await reply.json();
514
- if (data.error || data.result?.isError)
515
- throw Error('MCP_VERIFICATION_FAILED');
516
- return data.result;
517
- };
518
- await request('tools/list', {});
519
- await request('tools/call', { name: 'list_directory', arguments: { path: '.', depth: 0 } });
520
- output({ result: 'PASS', instanceId: instance.id, port: state.port, remoteClientVerified: false });
521
- return;
522
- }
354
+ throw Error('INSTANCE_COMMAND_UNSUPPORTED');
523
355
  }
524
- //# sourceMappingURL=instances.js.map
@@ -71,4 +71,3 @@ export async function oauthCommand(args) {
71
71
  process.stdout.write(' Configuration saved. Enable explicitly: starmux auth mode oauth\n');
72
72
  process.stdout.write(' Remote ChatGPT authorization is not verified by this command.\n\n');
73
73
  }
74
- //# sourceMappingURL=oauth.js.map
@@ -1,5 +1,5 @@
1
1
  import { createAccessControl } from '../access-control.js';
2
- import { exists, instancePaths } from '../instance-store.js';
2
+ import { exists, instancePaths, usesGlobalPermissions } from '../instance-store.js';
3
3
  import { shellQuote } from '../platform.js';
4
4
  async function authentication(instance, status) {
5
5
  const file = instancePaths(instance).access;
@@ -23,6 +23,9 @@ async function authentication(instance, status) {
23
23
  }
24
24
  }
25
25
  export async function formatStarmuxStatus(instance, status, config, options = {}) {
26
+ // Pending edits must never be shown as permissions of the running instance.
27
+ if (status.phase === 'running')
28
+ config = status.active?.config;
26
29
  const auth = await authentication(instance, status);
27
30
  // Omit `-i default` for the default instance to keep everyday single-instance output clean.
28
31
  const omitSelector = options.isDefault === true && instance.name === 'default';
@@ -40,26 +43,20 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
40
43
  const saved = status.phase === 'running' ? ' (saved; live state unavailable)' : ' (saved for next start)';
41
44
  const row = (label, value) => ' ' + label.padEnd(16) + value;
42
45
  const codingCmd = command('up --code');
46
+ const globalMode = status.phase === 'running' ? status.runtime?.permissionMode === 'global' : !!config && usesGlobalPermissions(config);
43
47
  const mode = !config
44
48
  ? 'Unavailable'
45
49
  : config.accessRole === 'trusted'
46
- ? (config.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)')
50
+ ? `${config.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)'} (run '${command('up --read')}' to return to read-only)`
47
51
  : config.mode === 'read-write'
48
52
  ? 'File writes (restricted)'
49
- : `Read-only (run '${codingCmd}' to enable edits & shell)`;
50
- const runtime = status.executionMode === 'background'
51
- ? 'Background · continues after this terminal closes'
52
- : status.executionMode === 'foreground-owned'
53
- ? (status.runtime?.launcher === 'app' ? 'App-owned · stops when the app closes' : 'Foreground · Ctrl+C stops this instance')
54
- : 'Stopped';
55
- const title = omitSelector
56
- ? `${indicator} Starmux is ${status.phase}`
57
- : `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
58
- const lines = ['', title];
53
+ : `Read-only (${globalMode ? 'global · OS account' : 'project scope'}) (run '${codingCmd}' to enable edits & shell)`;
54
+ const title = `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
55
+ const lines = ['', title, row('Instance', instance.id)];
59
56
  if (ready && status.port) {
60
57
  lines.push(row('Listen', `http://127.0.0.1:${status.port}/mcp (this computer only)`));
61
58
  }
62
- lines.push(row('Workspace', config?.projectRoot ?? 'Unavailable'), row('Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'), row('Runtime', runtime));
59
+ lines.push(row('Project', config?.projectRoot ?? 'Unavailable'), row('Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'));
63
60
  if (ready && status.port && auth.mode === 'oauth' && auth.oauth) {
64
61
  lines.push(row('Client URL', auth.oauth.resource), row('Issuer', auth.oauth.issuer));
65
62
  }
@@ -119,4 +116,3 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
119
116
  lines.push(row('Stop', command('down')));
120
117
  return lines.join('\n') + '\n';
121
118
  }
122
- //# sourceMappingURL=starmux-status.js.map
@@ -8,4 +8,3 @@ catch (error) {
8
8
  process.stderr.write(String(error) + '\n');
9
9
  process.exitCode = 1;
10
10
  }
11
- //# sourceMappingURL=starmux-supervisor.js.map