starmux 0.10.0 → 0.12.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 (48) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +184 -59
  3. package/SECURITY.md +3 -3
  4. package/app/README.md +6 -2
  5. package/dist/src/access-control.js +23 -20
  6. package/dist/src/access-control.js.map +1 -1
  7. package/dist/src/access-dashboard.js +8 -2
  8. package/dist/src/access-dashboard.js.map +1 -1
  9. package/dist/src/cli/instances.js +87 -14
  10. package/dist/src/cli/instances.js.map +1 -1
  11. package/dist/src/cli/starmux-status.js +122 -0
  12. package/dist/src/cli/starmux-status.js.map +1 -0
  13. package/dist/src/cli/starmux.js +222 -49
  14. package/dist/src/cli/starmux.js.map +1 -1
  15. package/dist/src/deny-dashboard.js +2 -2
  16. package/dist/src/deny-dashboard.js.map +1 -1
  17. package/dist/src/instance-runtime.js +11 -2
  18. package/dist/src/instance-runtime.js.map +1 -1
  19. package/dist/src/instance-store.js +3 -1
  20. package/dist/src/instance-store.js.map +1 -1
  21. package/dist/src/ops-dashboard.js +4 -4
  22. package/dist/src/ops-dashboard.js.map +1 -1
  23. package/dist/src/permissions-dashboard.js +2 -2
  24. package/dist/src/permissions-dashboard.js.map +1 -1
  25. package/dist/src/starmux-migration.js +1 -1
  26. package/dist/src/starmux-migration.js.map +1 -1
  27. package/docs/ACCESS_CONTROL.md +5 -5
  28. package/docs/CLIENTS.md +11 -19
  29. package/docs/CONNECT_CHATGPT.md +4 -6
  30. package/docs/INSTALL.md +9 -5
  31. package/docs/INSTANCES.md +5 -2
  32. package/docs/OAUTH.md +6 -14
  33. package/docs/PLATFORMS.md +4 -4
  34. package/docs/README.md +2 -11
  35. package/docs/REMOTE_SETUP.md +13 -13
  36. package/docs/SECURITY.md +5 -5
  37. package/docs/assets/starmux-app-en.png +0 -0
  38. package/docs/assets/starmux-architecture.svg +315 -0
  39. package/package.json +1 -1
  40. package/docs/ARCHITECTURE.md +0 -51
  41. package/docs/MIGRATION.md +0 -37
  42. package/docs/RELEASE_STATUS.md +0 -22
  43. package/docs/TYPESCRIPT_MIGRATION.md +0 -56
  44. package/docs/VALIDATION_OAUTH.md +0 -79
  45. package/docs/VALIDATION_TUNNEL.md +0 -39
  46. package/docs/VALIDATION_WORKBUDDY.md +0 -59
  47. package/docs/assets/mcp-connection-route.svg +0 -104
  48. package/docs/assets/mcp-gateway-simple.svg +0 -62
@@ -0,0 +1,122 @@
1
+ import { createAccessControl } from '../access-control.js';
2
+ import { exists, instancePaths } from '../instance-store.js';
3
+ import { shellQuote } from '../platform.js';
4
+ async function authentication(instance, status) {
5
+ const file = instancePaths(instance).access;
6
+ try {
7
+ const state = await (await createAccessControl(file, { initialize: false })).snapshot();
8
+ if (status.phase === 'running' && status.health === 'ok' && status.accessMode !== state.mode) {
9
+ return { mode: null, activeKeys: 0, oauth: null, problem: 'Authentication changed while checking. Run status again.' };
10
+ }
11
+ const incomplete = status.phase !== 'running' && instance.requiredAccess !== 'ingress' && state.mode !== instance.requiredAccess;
12
+ return { mode: state.mode, activeKeys: state.credentials.filter(c => c.status === 'active').length,
13
+ oauth: state.oauth ? { issuer: state.oauth.issuer, resource: state.oauth.resource, clientId: state.oauth.clientId, scope: state.oauth.scope, tokenAuth: state.oauth.tokenEndpointAuthMethod } : null,
14
+ problem: incomplete ? `This instance requires ${instance.requiredAccess} authentication before starting.` : null };
15
+ }
16
+ catch (error) {
17
+ // A new, never-started instance has an ingress default, but a lost store must
18
+ // never be presented as an intentional switch to unauthenticated access.
19
+ if (error.code === 'ENOENT' && status.phase === 'stopped' && instance.requiredAccess === 'ingress' && !await exists(file + '.initialized')) {
20
+ return { mode: 'ingress', activeKeys: 0, oauth: null, problem: null };
21
+ }
22
+ return { mode: null, activeKeys: 0, oauth: null, problem: 'Authentication settings are unavailable. Inspect them with the Auth command below.' };
23
+ }
24
+ }
25
+ export async function formatStarmuxStatus(instance, status, config, options = {}) {
26
+ const auth = await authentication(instance, status);
27
+ // Omit `-i default` for the default instance to keep everyday single-instance output clean.
28
+ const omitSelector = options.isDefault === true && instance.name === 'default';
29
+ const selector = /^[A-Za-z0-9_.-]+$/.test(instance.name) && !instance.name.startsWith('-') ? instance.name : shellQuote(instance.name);
30
+ const flag = omitSelector ? '' : (instance.name.startsWith('-') ? ` --instance=${selector}` : ` -i ${selector}`);
31
+ const command = (action) => `starmux ${action}${flag}`;
32
+ const ready = status.phase === 'running' && status.health === 'ok';
33
+ const hasProblem = Boolean(status.configProblem || status.runtimeProblem || auth.problem);
34
+ const indicator = hasProblem ? '!' : ready ? '●' : status.phase === 'running' ? '◐' : '○';
35
+ const labels = {
36
+ ingress: 'None (no token required)',
37
+ 'api-key': `API key (Bearer token; ${auth.activeKeys} active ${auth.activeKeys === 1 ? 'key' : 'keys'})`,
38
+ oauth: 'OAuth (browser sign-in)'
39
+ };
40
+ const saved = status.phase === 'running' ? ' (saved; live state unavailable)' : ' (saved for next start)';
41
+ const row = (label, value) => ' ' + label.padEnd(16) + value;
42
+ const codingCmd = command('up --code');
43
+ const mode = !config
44
+ ? 'Unavailable'
45
+ : config.accessRole === 'trusted'
46
+ ? (config.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)')
47
+ : config.mode === 'read-write'
48
+ ? '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];
59
+ if (ready && status.port) {
60
+ lines.push(row('Listen', `http://127.0.0.1:${status.port}/mcp (this computer only)`));
61
+ }
62
+ lines.push(row('Workspace', config?.projectRoot ?? 'Unavailable'), row('Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'), row('Runtime', runtime));
63
+ if (ready && status.port && auth.mode === 'oauth' && auth.oauth) {
64
+ lines.push(row('Client URL', auth.oauth.resource), row('Issuer', auth.oauth.issuer));
65
+ }
66
+ if (status.configProblem || status.runtimeProblem)
67
+ lines.push(row('Problem', status.configProblem ?? status.runtimeProblem));
68
+ if (auth.problem)
69
+ lines.push(row('Auth issue', auth.problem));
70
+ const keyCommand = command('auth key "My AI Client"');
71
+ if (ready && !auth.problem) {
72
+ lines.push('', 'Connect an AI client (MCP settings)');
73
+ lines.push(row('Transport', 'Streamable HTTP'));
74
+ if (auth.mode === 'ingress') {
75
+ lines.push(row('URL (Local)', `http://127.0.0.1:${status.port}/mcp (AI client on this computer)`), row('URL (Remote)', `https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`), row('Authentication', `None (optional Bearer API key: ${keyCommand})`));
76
+ }
77
+ else if (auth.mode === 'api-key') {
78
+ lines.push(row('URL (Local)', `http://127.0.0.1:${status.port}/mcp (AI client on this computer)`), row('URL (Remote)', `https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`), row('Authentication', `Bearer API key (${auth.activeKeys} active ${auth.activeKeys === 1 ? 'key' : 'keys'})`), row('Header', 'Authorization: Bearer <YOUR_API_KEY>'));
79
+ if (!auth.activeKeys) {
80
+ lines.push(row('Create key', `${keyCommand} (no active keys; required before connecting)`));
81
+ }
82
+ }
83
+ else if (auth.mode === 'oauth' && auth.oauth) {
84
+ lines.push(row('URL (Remote)', auth.oauth.resource), row('Authentication', 'OAuth (browser sign-in)'), row('Issuer', auth.oauth.issuer), row('Client ID', `${auth.oauth.clientId} (Scope: ${auth.oauth.scope} · Token auth: ${auth.oauth.tokenAuth})`));
85
+ }
86
+ }
87
+ lines.push('', 'Commands');
88
+ if (auth.problem) {
89
+ lines.push(row('Check auth', command('auth')));
90
+ }
91
+ else if (!ready) {
92
+ if (status.phase === 'stopped' && !status.configProblem && !status.runtimeProblem) {
93
+ if (auth.mode === 'api-key' && !auth.activeKeys)
94
+ lines.push(row('Create key', keyCommand));
95
+ lines.push(row('Start', command('up')), row('Start coding', codingCmd));
96
+ }
97
+ else {
98
+ lines.push(row('Diagnose', command('doctor')));
99
+ }
100
+ }
101
+ else {
102
+ if (auth.mode === 'api-key' && auth.activeKeys) {
103
+ lines.push(row('Verify', `STARMUX_TOKEN=<key> ${command('verify')}`), row('New API key', keyCommand));
104
+ }
105
+ else if (auth.mode === 'oauth') {
106
+ lines.push(row('Check OAuth', command('auth oauth doctor')), row('Verify', command('verify')));
107
+ }
108
+ else {
109
+ lines.push(row('Verify', command('verify')));
110
+ }
111
+ }
112
+ if (status.configState === 'pending')
113
+ lines.push(row('Apply config', command('config apply')));
114
+ lines.push(row('Auth & keys', command('auth')));
115
+ if (ready)
116
+ lines.push(row('Dashboard', `${command('dashboard')} (view & copy settings in browser)`));
117
+ lines.push(row('Logs', command('logs -f')));
118
+ if (status.phase === 'running')
119
+ lines.push(row('Stop', command('down')));
120
+ return lines.join('\n') + '\n';
121
+ }
122
+ //# sourceMappingURL=starmux-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starmux-status.js","sourceRoot":"","sources":["../../../src/cli/starmux-status.ts"],"names":[],"mappings":"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","sourcesContent":["import {createAccessControl} from '../access-control.js';\nimport type {instanceStatus} from '../instance-runtime.js';\nimport {exists,instancePaths,type Instance,type InstanceConfig} from '../instance-store.js';\nimport {shellQuote} from '../platform.js';\n\ntype Status=Awaited<ReturnType<typeof instanceStatus>>;\ntype AuthMode='ingress'|'api-key'|'oauth';\ntype OAuthSummary={issuer:string;resource:string;clientId:string;scope:string;tokenAuth:string};\ntype AuthSummary={mode:AuthMode|null;activeKeys:number;oauth:OAuthSummary|null;problem:string|null};\n\nasync function authentication(instance:Instance,status:Status):Promise<AuthSummary>{\n const file=instancePaths(instance).access;\n try{\n const state=await (await createAccessControl(file,{initialize:false})).snapshot();\n if(status.phase==='running'&&status.health==='ok'&&status.accessMode!==state.mode){\n return {mode:null,activeKeys:0,oauth:null,problem:'Authentication changed while checking. Run status again.'};\n }\n const incomplete=status.phase!=='running'&&instance.requiredAccess!=='ingress'&&state.mode!==instance.requiredAccess;\n return {mode:state.mode,activeKeys:state.credentials.filter(c=>c.status==='active').length,\n oauth:state.oauth?{issuer:state.oauth.issuer,resource:state.oauth.resource,clientId:state.oauth.clientId,scope:state.oauth.scope,tokenAuth:state.oauth.tokenEndpointAuthMethod}:null,\n problem:incomplete?`This instance requires ${instance.requiredAccess} authentication before starting.`:null};\n }catch(error){\n // A new, never-started instance has an ingress default, but a lost store must\n // never be presented as an intentional switch to unauthenticated access.\n if((error as NodeJS.ErrnoException).code==='ENOENT'&&status.phase==='stopped'&&instance.requiredAccess==='ingress'&&!await exists(file+'.initialized')){\n return {mode:'ingress',activeKeys:0,oauth:null,problem:null};\n }\n return {mode:null,activeKeys:0,oauth:null,problem:'Authentication settings are unavailable. Inspect them with the Auth command below.'};\n }\n}\n\nexport async function formatStarmuxStatus(instance:Instance,status:Status,config:InstanceConfig|undefined,options:{isDefault?:boolean}={}){\n const auth=await authentication(instance,status);\n // Omit `-i default` for the default instance to keep everyday single-instance output clean.\n const omitSelector=options.isDefault===true&&instance.name==='default';\n const selector=/^[A-Za-z0-9_.-]+$/.test(instance.name)&&!instance.name.startsWith('-')?instance.name:shellQuote(instance.name);\n const flag=omitSelector?'':(instance.name.startsWith('-')?` --instance=${selector}`:` -i ${selector}`);\n const command=(action:string)=>`starmux ${action}${flag}`;\n const ready=status.phase==='running'&&status.health==='ok';\n const hasProblem=Boolean(status.configProblem||status.runtimeProblem||auth.problem);\n const indicator=hasProblem?'!':ready?'●':status.phase==='running'?'◐':'○';\n const labels={\n ingress:'None (no token required)',\n 'api-key':`API key (Bearer token; ${auth.activeKeys} active ${auth.activeKeys===1?'key':'keys'})`,\n oauth:'OAuth (browser sign-in)'\n };\n const saved=status.phase==='running'?' (saved; live state unavailable)':' (saved for next start)';\n const row=(label:string,value:string)=>' '+label.padEnd(16)+value;\n const codingCmd=command('up --code');\n const mode=!config\n ?'Unavailable'\n :config.accessRole==='trusted'\n ?(config.writeScope==='home-only'?'Coding (Home sandbox · edits, shell & git)':'Coding (OS account · edits, shell & git)')\n :config.mode==='read-write'\n ?'File writes (restricted)'\n :`Read-only (run '${codingCmd}' to enable edits & shell)`;\n const runtime=status.executionMode==='background'\n ?'Background · continues after this terminal closes'\n :status.executionMode==='foreground-owned'\n ?(status.runtime?.launcher==='app'?'App-owned · stops when the app closes':'Foreground · Ctrl+C stops this instance')\n :'Stopped';\n const title=omitSelector\n ?`${indicator} Starmux is ${status.phase}`\n :`${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;\n const lines=['',title];\n if(ready&&status.port){\n lines.push(row('Listen',`http://127.0.0.1:${status.port}/mcp (this computer only)`));\n }\n lines.push(\n row('Workspace',config?.projectRoot??'Unavailable'),\n row('Mode',mode),\n row('Auth',auth.mode?labels[auth.mode]+(ready?'':saved):'Unavailable'),\n row('Runtime',runtime)\n );\n if(ready&&status.port&&auth.mode==='oauth'&&auth.oauth){\n lines.push(row('Client URL',auth.oauth.resource),row('Issuer',auth.oauth.issuer));\n }\n if(status.configProblem||status.runtimeProblem)lines.push(row('Problem',status.configProblem??status.runtimeProblem!));\n if(auth.problem)lines.push(row('Auth issue',auth.problem));\n\n const keyCommand=command('auth key \"My AI Client\"');\n if(ready&&!auth.problem){\n lines.push('','Connect an AI client (MCP settings)');\n lines.push(row('Transport','Streamable HTTP'));\n if(auth.mode==='ingress'){\n lines.push(\n row('URL (Local)',`http://127.0.0.1:${status.port}/mcp (AI client on this computer)`),\n row('URL (Remote)',`https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`),\n row('Authentication',`None (optional Bearer API key: ${keyCommand})`)\n );\n }else if(auth.mode==='api-key'){\n lines.push(\n row('URL (Local)',`http://127.0.0.1:${status.port}/mcp (AI client on this computer)`),\n row('URL (Remote)',`https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`),\n row('Authentication',`Bearer API key (${auth.activeKeys} active ${auth.activeKeys===1?'key':'keys'})`),\n row('Header','Authorization: Bearer <YOUR_API_KEY>')\n );\n if(!auth.activeKeys){\n lines.push(row('Create key',`${keyCommand} (no active keys; required before connecting)`));\n }\n }else if(auth.mode==='oauth'&&auth.oauth){\n lines.push(\n row('URL (Remote)',auth.oauth.resource),\n row('Authentication','OAuth (browser sign-in)'),\n row('Issuer',auth.oauth.issuer),\n row('Client ID',`${auth.oauth.clientId} (Scope: ${auth.oauth.scope} · Token auth: ${auth.oauth.tokenAuth})`)\n );\n }\n }\n\n lines.push('','Commands');\n if(auth.problem){\n lines.push(row('Check auth',command('auth')));\n }else if(!ready){\n if(status.phase==='stopped'&&!status.configProblem&&!status.runtimeProblem){\n if(auth.mode==='api-key'&&!auth.activeKeys)lines.push(row('Create key',keyCommand));\n lines.push(row('Start',command('up')),row('Start coding',codingCmd));\n }else{\n lines.push(row('Diagnose',command('doctor')));\n }\n }else{\n if(auth.mode==='api-key'&&auth.activeKeys){\n lines.push(\n row('Verify',`STARMUX_TOKEN=<key> ${command('verify')}`),\n row('New API key',keyCommand)\n );\n }else if(auth.mode==='oauth'){\n lines.push(\n row('Check OAuth',command('auth oauth doctor')),\n row('Verify',command('verify'))\n );\n }else{\n lines.push(row('Verify',command('verify')));\n }\n }\n if(status.configState==='pending')lines.push(row('Apply config',command('config apply')));\n lines.push(row('Auth & keys',command('auth')));\n if(ready)lines.push(row('Dashboard',`${command('dashboard')} (view & copy settings in browser)`));\n lines.push(row('Logs',command('logs -f')));\n if(status.phase==='running')lines.push(row('Stop',command('down')));\n return lines.join('\\n')+'\\n';\n}\n"]}
@@ -9,10 +9,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
9
9
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
10
10
  import { createAccessControl } from '../access-control.js';
11
11
  import { instanceStatus, instanceRequest, runInstance } from '../instance-runtime.js';
12
- import { atomicPrivate, availablePort, changeRegistry, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig } from '../instance-store.js';
12
+ import { atomicPrivate, availablePort, changeRegistry, configJSONSchema, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig } from '../instance-store.js';
13
13
  import { handle } from './instances.js';
14
- import { desktopCommanderEntry, packageRoot } from '../runtime-paths.js';
15
- const version = '0.10.0';
14
+ import { desktopCommanderEntry } from '../runtime-paths.js';
15
+ import { shellQuote } from '../platform.js';
16
+ import { formatStarmuxStatus } from './starmux-status.js';
17
+ const version = '0.12.0';
16
18
  const strings = ['instance', 'port', 'lines', 'expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'revision', 'patch', 'file', 'output', 'name', 'target', 'mode', 'app-parent-pid'];
17
19
  const booleans = ['help', 'version', 'json', 'write', 'code', 'read', 'yes', 'foreground', 'follow', 'archived', 'all', 'effective', 'link', 'allow-loopback-http', 'token-stdin', 'no-open', 'token'];
18
20
  const options = {};
@@ -39,22 +41,50 @@ function fail(message) { throw Error(message); }
39
41
  const absolute = (value) => path.resolve(value === '~' ? homedir() : value.startsWith('~/') ? path.join(homedir(), value.slice(2)) : value);
40
42
  const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
41
43
  function help() {
42
- const topic = command === 'help' ? positional[0] : command;
44
+ const requested = command === 'help' ? positional[0] : command;
45
+ const topic = requested === 'stop' ? 'down' : requested;
43
46
  const sections = {
44
47
  up: 'up [path] [--read | --write | --code] [-y] [--port N] [--foreground]\n Initialize and start. First use defaults to CWD; later starts reuse saved settings.',
45
48
  init: 'init [path] [--read | --write | --code] [-y] [--port N]\n Save settings without starting. --write and --code enable full account coding permissions.',
46
- auth: 'auth\n auth key "Client name" [--expires 30d]\n auth mode ingress|api-key|oauth\n auth revoke ID\n auth oauth init-keycloak --resource URL --callback URL\n auth oauth setup --issuer URL --resource URL --client-id ID --subject USER_ID\n auth oauth doctor',
47
- config: 'config show|path|edit|validate|diff|apply|schema|recover\n config import FILE [-i NAME] [--link] [-y]\n config copy NEW_NAME -i SOURCE\n config rename NEW_NAME -i SOURCE\n config default NAME\n config archive|restore -i NAME\n config export --output FILE\n config relink --file FILE | manage\n config save --file FILE --revision REV\n config patch --patch JSON --revision REV [-y]\n config secret set|remove NAME',
49
+ auth: 'auth\n auth key "Client name" [--expires 30d]\n auth mode none|ingress|api-key|oauth\n auth revoke ID\n auth oauth init-keycloak --resource URL --callback URL\n auth oauth setup --issuer URL --resource URL --client-id ID --subject USER_ID\n auth oauth doctor',
50
+ config: 'config [show] [--effective]\n config edit | diff | apply | validate | path | schema | recover\n config default NAME | rename NEW_NAME -i SOURCE | copy NEW_NAME -i SOURCE\n config archive -i NAME | restore -i NAME | import FILE [-i NAME] [--link]\n config export [--output FILE] | secret set|remove NAME',
48
51
  dashboard: 'dashboard [--no-open | --token]\n Open the local Access console. --no-open prints a private login link.',
49
- verify: 'verify [--token-stdin]\n Test MCP initialization and a directory read. Authenticated instances use STARMUX_TOKEN or stdin.',
52
+ verify: 'verify [--token-stdin]\n Test MCP initialization, tool discovery and workspace read. Authenticated instances use STARMUX_TOKEN or stdin.',
50
53
  doctor: 'doctor\n Check Node, bundled dependencies, workspace, configuration, port and authentication.',
51
54
  logs: 'logs [-f | --follow] [--lines 1..200]\n Follow continues across restarts. Ctrl+C exits the follower only.',
52
55
  migrate: 'migrate [registry|setup|live] [-y]\n No source: preview. Select a source to migrate, retaining originals.',
53
- status: 'status [--all]\n Show the selected instance, its actual endpoint and Dashboard.',
54
- ls: 'ls [--archived]\n List local instances.',
55
- down: 'down (alias: stop)\n Stop the selected instance.'
56
+ status: 'status [--all]\n Show the selected instance, its actual endpoint and connection guide.',
57
+ ls: 'ls [--archived]\n List local workspace instances.',
58
+ rm: 'rm [NAME] (alias for: config archive)\n Stop and archive an instance.',
59
+ down: 'down (alias: stop)\n Stop the selected instance.',
60
+ permissions: 'permissions [status | token]\n permissions disable-tool|enable-tool|deny-directory|allow-directory TARGET\n Manage the selected instance’s tool and directory permissions.',
61
+ privacy: 'privacy [--all] [--target PATH]\n Check privacy access through the running instance.'
56
62
  };
57
- process.stdout.write(`Starmux ${version}\n\n${sections[topic ?? ''] ?? 'Connect AI clients to your computer’s files and development tools.\n\n starmux up [path] [--code] Start in the background\n starmux init [path] Save settings\n starmux status Show local endpoints\n starmux verify | doctor Check tools or environment\n starmux logs [-f] Read logs\n starmux down Stop\n starmux auth Manage client access\n starmux dashboard Open local Access console\n starmux ls List instances\n starmux config Advanced configuration\n starmux migrate Import an old installation'}\n\nGlobal: -i, --instance NAME_OR_ID --json -h, --help\nUse starmux COMMAND --help for details.\nDocumentation: ${path.join(packageRoot, 'docs', 'README.md')}\n`);
63
+ if (topic && topic !== 'help' && !sections[topic])
64
+ fail('Unknown command: ' + topic + '. Run starmux --help.');
65
+ const overview = [
66
+ 'Connect AI clients to your computer’s files, shell and development tools.',
67
+ '',
68
+ 'Workspace Lifecycle:',
69
+ ' starmux up [path] [--code] Start in the background (default: Read-only)',
70
+ ' starmux down Stop the running instance (alias: stop)',
71
+ ' starmux status Show health, endpoint and connection guide',
72
+ ' starmux logs [-f] View or stream gateway logs',
73
+ '',
74
+ 'Access & Verification:',
75
+ ' starmux auth Manage auth mode, API keys and OAuth',
76
+ ' starmux dashboard Open the local Web Access Console',
77
+ ' starmux verify Test local MCP handshake and workspace read',
78
+ ' starmux doctor Check runtime, config, port and auth state',
79
+ '',
80
+ 'Multi-Instance & Config:',
81
+ ' starmux ls List workspace instances',
82
+ ' starmux rm [name] Stop and archive an instance',
83
+ ' starmux init [path] Save instance settings without starting',
84
+ ' starmux config Inspect, edit or apply configuration',
85
+ ' starmux migrate Import settings from a legacy installation'
86
+ ].join('\n');
87
+ process.stdout.write(`Starmux ${version}\n\n${sections[topic ?? ''] ?? overview}\n\nGlobal: -i, --instance NAME_OR_ID --json -h, --help\nUse starmux COMMAND --help for details.\n`);
58
88
  }
59
89
  async function confirm(message) {
60
90
  if (yes())
@@ -118,6 +148,16 @@ async function configure() {
118
148
  await rememberConsent(instance, config);
119
149
  return instance;
120
150
  }
151
+ async function isDefaultInstance(instance) {
152
+ const registry = await readRegistry().catch(() => ({ defaultInstanceId: null, instances: [] }));
153
+ return instance.name === 'default' && (registry.defaultInstanceId === instance.id || registry.instances.filter(i => !i.archived).length <= 1);
154
+ }
155
+ async function instanceFlag(instance) {
156
+ if (await isDefaultInstance(instance))
157
+ return '';
158
+ const selector = /^[A-Za-z0-9_.-]+$/.test(instance.name) && !instance.name.startsWith('-') ? instance.name : shellQuote(instance.name);
159
+ return instance.name.startsWith('-') ? `--instance=${selector}` : `-i ${selector}`;
160
+ }
121
161
  async function show(instance) {
122
162
  const status = await instanceStatus(instance);
123
163
  if (json) {
@@ -125,12 +165,7 @@ async function show(instance) {
125
165
  return;
126
166
  }
127
167
  const config = status.phase === 'running' ? status.active?.config : (await loadInstanceConfig(instance).catch(() => null))?.config;
128
- process.stdout.write(`\nStarmux · ${instance.name} · ${status.phase}\nWorkspace ${config?.projectRoot ?? 'Unavailable'}\nMode ${config?.accessRole === 'trusted' ? (config.writeScope === 'home-only' ? 'Coding (Home sandbox)' : 'Coding (OS account)') : config?.mode === 'read-write' ? 'File writes (restricted)' : 'Read-only'}\nRuntime ${status.executionMode}\n`);
129
- if (status.port && status.phase === 'running')
130
- process.stdout.write(`MCP http://127.0.0.1:${status.port}/mcp\nDashboard http://127.0.0.1:${status.port}/access\n`);
131
- if (status.configProblem || status.runtimeProblem)
132
- process.stdout.write(`Problem ${status.configProblem ?? status.runtimeProblem}\n`);
133
- process.stdout.write(`Logs starmux logs -f -i ${JSON.stringify(instance.name)}\n`);
168
+ process.stdout.write(await formatStarmuxStatus(instance, status, config, { isDefault: await isDefaultInstance(instance) }));
134
169
  }
135
170
  async function start(instance) {
136
171
  const state = await instanceStatus(instance);
@@ -145,7 +180,7 @@ async function start(instance) {
145
180
  if (!await availablePort(config.port))
146
181
  fail(`Port ${config.port} is in use. Choose another with starmux up --port PORT.`);
147
182
  if (flags.foreground || string('app-parent-pid')) {
148
- await runInstance(instance, { json, ...(string('app-parent-pid') ? { appParentPid: Number(string('app-parent-pid')) } : {}) });
183
+ await runInstance(instance, { json, ...(!json ? { onReady: () => show(instance) } : {}), ...(string('app-parent-pid') ? { appParentPid: Number(string('app-parent-pid')) } : {}) });
149
184
  return;
150
185
  }
151
186
  const log = path.join(instancePaths(instance).logs, 'supervisor.log');
@@ -189,10 +224,13 @@ async function verify(instance) {
189
224
  fail('Start this instance with starmux up first.');
190
225
  const token = await bearer();
191
226
  const client = new Client({ name: 'starmux-verify', version });
192
- const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${status.port}/mcp`), { requestInit: { headers: token ? { Authorization: 'Bearer ' + token } : {} } });
227
+ const endpoint = `http://127.0.0.1:${status.port}/mcp`;
228
+ const transport = new StreamableHTTPClientTransport(new URL(endpoint), { requestInit: { headers: token ? { Authorization: 'Bearer ' + token } : {} } });
229
+ let toolCount = 0;
193
230
  try {
194
231
  await client.connect(transport);
195
- await client.listTools();
232
+ const listed = await client.listTools();
233
+ toolCount = listed.tools.length;
196
234
  const result = await client.callTool({ name: 'list_directory', arguments: { path: '.', depth: 0 } });
197
235
  if (result.isError)
198
236
  fail('Directory read failed.');
@@ -205,8 +243,15 @@ async function verify(instance) {
205
243
  }
206
244
  if (json)
207
245
  print({ result: 'PASS', instanceId: instance.id, port: status.port, remoteClientVerified: false });
208
- else
209
- process.stdout.write('PASS · MCP handshake, tool discovery and workspace read.\n');
246
+ else {
247
+ const cfg = status.active?.config;
248
+ const mode = cfg?.accessRole === 'trusted' ? 'Coding mode' : 'Read-only mode';
249
+ const root = cfg?.projectRoot ?? '.';
250
+ process.stdout.write(`✓ MCP handshake ${endpoint}\n` +
251
+ `✓ Tool discovery ${toolCount} tools available (${mode})\n` +
252
+ `✓ Workspace read ${root}\n` +
253
+ `PASS · MCP handshake, tool discovery and workspace read.\n`);
254
+ }
210
255
  }
211
256
  function expires(value) {
212
257
  if (!value)
@@ -222,36 +267,85 @@ async function auth(instance) {
222
267
  const op = positional[0];
223
268
  if (!op) {
224
269
  const state = await access.snapshot();
225
- if (json)
270
+ if (json) {
226
271
  print(state);
227
- else {
228
- process.stdout.write(`Authentication: ${state.mode}\n`);
229
- for (const c of state.credentials)
230
- process.stdout.write(`${c.id} ${c.name} ${c.status}\n`);
231
- for (const c of state.oauthConnections)
232
- process.stdout.write(`${c.id} OAuth ${c.clientId} ${c.revokedAt ? 'revoked' : 'active'}\n`);
272
+ return;
233
273
  }
274
+ const flag = await instanceFlag(instance);
275
+ const cmd = (action) => `starmux ${action}${flag ? ' ' + flag : ''}`;
276
+ const activeKeys = state.credentials.filter(c => c.status === 'active').length;
277
+ const modeDesc = state.mode === 'ingress'
278
+ ? 'None (no Starmux token · local or ingress proxy)'
279
+ : state.mode === 'api-key'
280
+ ? `API key (Bearer token · ${activeKeys} active)`
281
+ : 'OAuth (browser sign-in)';
282
+ const lines = [
283
+ ...(flag ? [`Instance ${instance.name}`] : []),
284
+ `Auth mode ${modeDesc}`,
285
+ `API keys ${activeKeys} active (${state.credentials.length} total)`,
286
+ `OAuth ${state.oauth ? `${state.oauth.issuer} (${state.oauthConnections.filter(c => !c.revokedAt).length} active sessions)` : 'Not configured'}`
287
+ ];
288
+ if (state.credentials.length) {
289
+ lines.push('', 'API Keys');
290
+ for (const c of state.credentials) {
291
+ const exp = c.expiresAt ? ` expires ${c.expiresAt.slice(0, 10)}` : '';
292
+ lines.push(` ${c.id.padEnd(26)} ${c.name.padEnd(20)} ${c.status}${exp}`);
293
+ }
294
+ }
295
+ if (state.oauthConnections.length) {
296
+ lines.push('', 'OAuth Sessions');
297
+ for (const c of state.oauthConnections) {
298
+ lines.push(` ${c.id.padEnd(26)} OAuth ${c.clientId.padEnd(14)} ${c.revokedAt ? 'revoked' : 'active'}`);
299
+ }
300
+ }
301
+ lines.push('', 'Commands', ` Create API key ${cmd('auth key "My AI Client"')}`, ` Switch mode ${cmd('auth mode <none|api-key|oauth>')}`, ...(activeKeys > 0 || state.oauthConnections.some(c => !c.revokedAt) ? [` Revoke access ${cmd('auth revoke <ID>')}`] : []));
302
+ process.stdout.write(lines.join('\n') + '\n');
234
303
  return;
235
304
  }
236
305
  if (op === 'key') {
237
306
  const result = await access.create(positional[1] ?? '', expires(string('expires')), true);
238
307
  await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = 'api-key'; });
239
- if (json)
308
+ if (json) {
240
309
  print({ ...result, mode: 'api-key' });
241
- else
242
- process.stdout.write(`API key enabled for ${result.name}\nID: ${result.id}\n\n${result.key}\n\nCopy this key now; it is shown once.\n`);
310
+ return;
311
+ }
312
+ const cfg = (await loadInstanceConfig(instance).catch(() => null))?.config;
313
+ const port = cfg?.port ?? 18893;
314
+ process.stdout.write(`API key enabled for ${result.name} (ID: ${result.id})\n` +
315
+ `Copy this key now; it is shown only once:\n\n` +
316
+ ` ${result.key}\n\n` +
317
+ `Option A — Fill in AI client form UI:\n` +
318
+ ` Transport Streamable HTTP\n` +
319
+ ` URL (Local) http://127.0.0.1:${port}/mcp\n` +
320
+ ` URL (Remote) https://<your-domain-or-tunnel>/mcp\n` +
321
+ ` Authentication Bearer API key\n` +
322
+ ` Header Authorization: Bearer ${result.key}\n\n` +
323
+ `Option B — Or paste JSON config (mcpServers):\n` +
324
+ ` {\n` +
325
+ ` "mcpServers": {\n` +
326
+ ` "starmux": {\n` +
327
+ ` "type": "streamable-http",\n` +
328
+ ` "url": "https://<your-domain-or-tunnel>/mcp",\n` +
329
+ ` "headers": {\n` +
330
+ ` "Authorization": "Bearer ${result.key}"\n` +
331
+ ` }\n` +
332
+ ` }\n` +
333
+ ` }\n` +
334
+ ` }\n`);
243
335
  return;
244
336
  }
245
337
  if (op === 'mode') {
246
- const mode = positional[1];
338
+ const raw = positional[1];
339
+ const mode = raw === 'none' ? 'ingress' : raw;
247
340
  if (mode !== 'ingress' && mode !== 'api-key' && mode !== 'oauth')
248
- fail('Choose ingress, api-key or oauth.');
341
+ fail('Choose none (or ingress), api-key or oauth.');
249
342
  await access.setMode(mode);
250
343
  await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = mode; });
344
+ const label = mode === 'ingress' ? 'none (ingress)' : mode;
251
345
  if (json)
252
346
  print(await access.snapshot());
253
347
  else
254
- process.stdout.write('Authentication: ' + mode + '\n');
348
+ process.stdout.write('Authentication: ' + label + '\n');
255
349
  return;
256
350
  }
257
351
  if (op === 'revoke') {
@@ -321,15 +415,16 @@ async function logs(instance) {
321
415
  fail('--lines must be 1..200.');
322
416
  const file = path.join(instancePaths(instance).logs, 'core.log');
323
417
  let offset = 0;
324
- if (await exists(file)) {
325
- const text = await readTrusted(file, true, 32 * 1024 * 1024);
326
- offset = Buffer.byteLength(text);
327
- const output = text.split('\n').slice(-count).join('\n');
328
- if (json)
329
- print({ instanceId: instance.id, output });
330
- else
331
- process.stdout.write(output);
332
- }
418
+ const text = await exists(file) ? await readTrusted(file, true, 32 * 1024 * 1024) : '';
419
+ offset = Buffer.byteLength(text);
420
+ const lines = text.split('\n');
421
+ if (text.endsWith('\n'))
422
+ lines.pop();
423
+ const output = lines.slice(-count).join('\n') + (text.endsWith('\n') ? '\n' : '');
424
+ if (json)
425
+ print({ instanceId: instance.id, output });
426
+ else
427
+ process.stdout.write(output || 'No logs yet.\n');
333
428
  if (!flags.follow)
334
429
  return;
335
430
  let done = false;
@@ -370,6 +465,10 @@ async function logs(instance) {
370
465
  async function configCommand() {
371
466
  const op = positional[0] ?? 'show';
372
467
  const selector = string('instance');
468
+ if (op === 'schema') {
469
+ print(configJSONSchema());
470
+ return;
471
+ }
373
472
  if (op === 'import') {
374
473
  const file = positional[1];
375
474
  if (!file)
@@ -432,6 +531,57 @@ async function configCommand() {
432
531
  args.push('--approve-trusted-developer', '--approve-unrestricted');
433
532
  await handle('config', args);
434
533
  }
534
+ function validateSubcommand() {
535
+ if (command !== 'config' && command !== 'auth')
536
+ return;
537
+ const op = positional[0] ?? (command === 'config' ? 'show' : '');
538
+ let name = command + (op ? ' ' + op : '');
539
+ let count = positional.length - (positional.length ? 1 : 0);
540
+ let minimum = 0;
541
+ let maximum = 0;
542
+ let supported;
543
+ if (command === 'config') {
544
+ const commands = { show: ['effective'], path: [], edit: [], validate: [], diff: [], apply: ['revision', 'yes'], schema: [], recover: [],
545
+ import: ['name', 'link', 'yes'], copy: [], rename: [], default: [], archive: [], restore: [], export: ['output'], relink: ['file'], manage: [],
546
+ save: ['file', 'revision', 'yes'], patch: ['patch', 'revision', 'yes'], secret: [] };
547
+ supported = commands[op];
548
+ if (['import', 'copy', 'rename', 'default'].includes(op))
549
+ minimum = maximum = 1;
550
+ if (op === 'relink') {
551
+ minimum = string('file') ? 0 : 1;
552
+ maximum = minimum;
553
+ }
554
+ if (op === 'secret') {
555
+ minimum = maximum = 2;
556
+ if (!['set', 'remove'].includes(positional[1] ?? ''))
557
+ fail('Usage: starmux config secret set|remove NAME');
558
+ }
559
+ }
560
+ else if (op === 'oauth') {
561
+ const action = positional[1] ?? 'status';
562
+ name += ' ' + action;
563
+ count = Math.max(0, positional.length - 2);
564
+ const actions = { status: [], doctor: [],
565
+ 'init-keycloak': ['resource', 'callback', 'allow-loopback-http'],
566
+ setup: ['issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'allow-loopback-http'] };
567
+ supported = actions[action];
568
+ if (flags.subject !== undefined && flags.subjects !== undefined)
569
+ fail('Use either --subject or --subjects.');
570
+ }
571
+ else {
572
+ const commands = { '': [], key: ['expires'], mode: [], revoke: [] };
573
+ supported = commands[op];
574
+ if (['key', 'mode', 'revoke'].includes(op))
575
+ minimum = maximum = 1;
576
+ }
577
+ if (!supported)
578
+ fail('Unknown command: ' + name + '. See starmux ' + command + ' --help.');
579
+ if (count < minimum || count > maximum || positional.slice(1).some(value => !value.trim()))
580
+ fail('Invalid arguments for starmux ' + name + '. See starmux ' + command + ' --help.');
581
+ for (const key of Object.keys(flags))
582
+ if (!['instance', 'json', 'help', 'version'].includes(key) && !supported.includes(key))
583
+ fail('Option --' + key + ' is not supported by ' + name + '.');
584
+ }
435
585
  async function main() {
436
586
  if (flags.version) {
437
587
  process.stdout.write(version + '\n');
@@ -443,15 +593,20 @@ async function main() {
443
593
  }
444
594
  const allowed = {
445
595
  up: ['port', 'read', 'write', 'code', 'yes', 'foreground', 'app-parent-pid'], init: ['port', 'read', 'write', 'code', 'yes'],
446
- status: ['all'], ls: ['archived'], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'], migrate: ['yes'],
596
+ status: ['all'], ls: ['archived'], rm: [], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'], migrate: ['yes'],
447
597
  auth: ['expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'allow-loopback-http'],
448
598
  config: ['yes', 'archived', 'effective', 'link', 'name', 'revision', 'patch', 'file', 'output'], permissions: ['target', 'mode'], privacy: ['all', 'target']
449
599
  };
450
600
  for (const key of Object.keys(flags))
451
601
  if (!['instance', 'json', 'help', 'version'].includes(key) && !allowed[command]?.includes(key))
452
602
  fail('Option --' + key + ' is not supported by ' + command + '.');
603
+ if (string('instance') !== undefined && !string('instance').trim())
604
+ fail('--instance requires a non-empty name or ID.');
605
+ validateSubcommand();
453
606
  if (['status', 'ls', 'down', 'stop', 'logs', 'doctor', 'verify', 'dashboard'].includes(command) && positional.length)
454
607
  fail('Unexpected argument: ' + positional[0]);
608
+ if (command === 'rm' && positional.length > 1)
609
+ fail('Usage: starmux rm [NAME]');
455
610
  if (command === 'auth' && ['key', 'mode', 'revoke'].includes(positional[0] ?? '') && positional.length !== 2)
456
611
  fail('Usage: starmux auth ' + positional[0] + ' VALUE');
457
612
  if (command === 'migrate' && positional.length > 1)
@@ -469,8 +624,10 @@ async function main() {
469
624
  await start(instance);
470
625
  else if (json)
471
626
  print({ result: 'SAVED', instance });
472
- else
473
- process.stdout.write(`Saved ${instance.name}. Start with starmux up -i ${JSON.stringify(instance.name)}\n`);
627
+ else {
628
+ const flag = await instanceFlag(instance);
629
+ process.stdout.write(`Saved ${instance.name}. Start with starmux up${flag ? ' ' + flag : ''}\n`);
630
+ }
474
631
  return;
475
632
  }
476
633
  if (command === 'migrate') {
@@ -481,6 +638,11 @@ async function main() {
481
638
  await handle('instance', ['list', ...(flags.archived ? ['--archived'] : []), ...(json ? ['--json'] : [])]);
482
639
  return;
483
640
  }
641
+ if (command === 'rm') {
642
+ const target = positional[0] ?? string('instance') ?? (await selected()).id;
643
+ await handle('instance', ['archive', target, ...(json ? ['--json'] : [])]);
644
+ return;
645
+ }
484
646
  if (command === 'config') {
485
647
  await configCommand();
486
648
  return;
@@ -525,8 +687,19 @@ async function main() {
525
687
  fail('Configured port is in use: ' + loaded.config.port);
526
688
  if (json)
527
689
  print({ result: 'PASS', node: process.version, instanceId: instance.id, configState: state.configState });
528
- else
529
- process.stdout.write('PASS · Node, dependencies, workspace, configuration, port and authentication.\n');
690
+ else {
691
+ const isDef = await isDefaultInstance(instance);
692
+ const access = await (await createAccessControl(instancePaths(instance).access, { initialize: false })).snapshot();
693
+ const modeLabel = loaded.config.accessRole === 'trusted' ? 'Coding mode' : 'Read-only mode';
694
+ const authLabel = access.mode === 'ingress' ? 'None (no token required)' : access.mode === 'api-key' ? `API key (${access.credentials.filter(c => c.status === 'active').length} active)` : 'OAuth';
695
+ const portState = state.phase === 'running' ? `Running on 127.0.0.1:${state.port}` : `Stopped · port ${loaded.config.port} available`;
696
+ process.stdout.write(`✓ Runtime Node ${process.version} & Desktop Commander engine ready\n` +
697
+ `✓ Service ${portState}${isDef ? '' : ` (instance: ${instance.name})`}\n` +
698
+ `✓ Workspace ${loaded.config.projectRoot} (${modeLabel})\n` +
699
+ `✓ Configuration ${instance.source.path} (${state.configState})\n` +
700
+ `✓ Authentication ${authLabel}\n` +
701
+ `PASS · Node, dependencies, workspace, configuration, port and authentication.\n`);
702
+ }
530
703
  }
531
704
  else {
532
705
  const args = [...positional, '--instance', instance.id];