@geminilight/mindos 0.1.7 → 0.1.9

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.
@@ -0,0 +1,59 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { ROOT, BUILD_STAMP } from './constants.js';
5
+ import { red, dim, yellow } from './colors.js';
6
+ import { run } from './utils.js';
7
+
8
+ export function needsBuild() {
9
+ const nextDir = resolve(ROOT, 'app', '.next');
10
+ if (!existsSync(nextDir)) return true;
11
+ try {
12
+ const builtVersion = readFileSync(BUILD_STAMP, 'utf-8').trim();
13
+ const currentVersion = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
14
+ return builtVersion !== currentVersion;
15
+ } catch {
16
+ return true;
17
+ }
18
+ }
19
+
20
+ export function writeBuildStamp() {
21
+ const version = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
22
+ writeFileSync(BUILD_STAMP, version, 'utf-8');
23
+ }
24
+
25
+ export function clearBuildLock() {
26
+ const lockFile = resolve(ROOT, 'app', '.next', 'lock');
27
+ if (existsSync(lockFile)) {
28
+ rmSync(lockFile, { force: true });
29
+ }
30
+ }
31
+
32
+ export function cleanNextDir() {
33
+ const nextDir = resolve(ROOT, 'app', '.next');
34
+ if (existsSync(nextDir)) {
35
+ rmSync(nextDir, { recursive: true, force: true });
36
+ }
37
+ }
38
+
39
+ export function ensureAppDeps() {
40
+ const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
41
+ if (!existsSync(appNext)) {
42
+ try {
43
+ execSync('npm --version', { stdio: 'pipe' });
44
+ } catch {
45
+ console.error(red('\n\u2718 npm not found in PATH.\n'));
46
+ console.error(' MindOS needs npm to install its app dependencies on first run.');
47
+ console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
48
+ console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
49
+ console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
50
+ console.error(' Example:');
51
+ console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
52
+ console.error(dim(' source ~/.profile\n'));
53
+ console.error(' Then run `mindos start` again.\n');
54
+ process.exit(1);
55
+ }
56
+ console.log(yellow('Installing app dependencies (first run)...\n'));
57
+ run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ export const isTTY = process.stdout.isTTY;
2
+ export const bold = (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s;
3
+ export const dim = (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s;
4
+ export const cyan = (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s;
5
+ export const green = (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s;
6
+ export const red = (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s;
7
+ export const yellow = (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s;
@@ -0,0 +1,47 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { CONFIG_PATH } from './constants.js';
3
+
4
+ export function loadConfig() {
5
+ if (!existsSync(CONFIG_PATH)) return;
6
+ let config;
7
+ try {
8
+ config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
9
+ } catch {
10
+ console.error(`Warning: failed to parse ${CONFIG_PATH}`);
11
+ return;
12
+ }
13
+
14
+ const set = (key, val) => {
15
+ if (val && !process.env[key]) process.env[key] = String(val);
16
+ };
17
+
18
+ set('MIND_ROOT', config.mindRoot);
19
+ set('MINDOS_WEB_PORT', config.port);
20
+ set('MINDOS_MCP_PORT', config.mcpPort);
21
+ set('AUTH_TOKEN', config.authToken);
22
+ set('WEB_PASSWORD', config.webPassword);
23
+ set('AI_PROVIDER', config.ai?.provider);
24
+
25
+ const providers = config.ai?.providers;
26
+ if (providers) {
27
+ set('ANTHROPIC_API_KEY', providers.anthropic?.apiKey);
28
+ set('ANTHROPIC_MODEL', providers.anthropic?.model);
29
+ set('OPENAI_API_KEY', providers.openai?.apiKey);
30
+ set('OPENAI_MODEL', providers.openai?.model);
31
+ set('OPENAI_BASE_URL', providers.openai?.baseUrl);
32
+ } else {
33
+ set('ANTHROPIC_API_KEY', config.ai?.anthropicApiKey);
34
+ set('ANTHROPIC_MODEL', config.ai?.anthropicModel);
35
+ set('OPENAI_API_KEY', config.ai?.openaiApiKey);
36
+ set('OPENAI_MODEL', config.ai?.openaiModel);
37
+ set('OPENAI_BASE_URL', config.ai?.openaiBaseUrl);
38
+ }
39
+ }
40
+
41
+ export function getStartMode() {
42
+ try {
43
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode || 'start';
44
+ } catch {
45
+ return 'start';
46
+ }
47
+ }
@@ -0,0 +1,13 @@
1
+ import { resolve, dirname } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export const ROOT = resolve(__dirname, '..', '..');
7
+ export const CONFIG_PATH = resolve(homedir(), '.mindos', 'config.json');
8
+ export const PID_PATH = resolve(homedir(), '.mindos', 'mindos.pid');
9
+ export const BUILD_STAMP = resolve(ROOT, 'app', '.next', '.mindos-build-version');
10
+ export const MINDOS_DIR = resolve(homedir(), '.mindos');
11
+ export const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
12
+ export const CLI_PATH = resolve(__dirname, '..', 'cli.js');
13
+ export const NODE_BIN = process.execPath;
@@ -0,0 +1,244 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { MINDOS_DIR, LOG_PATH, CLI_PATH, NODE_BIN } from './constants.js';
6
+ import { green, red, dim, cyan } from './colors.js';
7
+
8
+ // ── Helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ export function getPlatform() {
11
+ if (process.platform === 'darwin') return 'launchd';
12
+ if (process.platform === 'linux') return 'systemd';
13
+ return null;
14
+ }
15
+
16
+ export function ensureMindosDir() {
17
+ if (!existsSync(MINDOS_DIR)) mkdirSync(MINDOS_DIR, { recursive: true });
18
+ }
19
+
20
+ export async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
21
+ for (let i = 0; i < retries; i++) {
22
+ if (check()) return true;
23
+ await new Promise(r => setTimeout(r, intervalMs));
24
+ }
25
+ return check();
26
+ }
27
+
28
+ export async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
29
+ process.stdout.write(cyan(` Waiting for ${label} to be ready`));
30
+ for (let i = 0; i < retries; i++) {
31
+ try {
32
+ const { request } = await import('node:http');
33
+ const ok = await new Promise((resolve) => {
34
+ const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
35
+ (res) => { res.resume(); resolve(res.statusCode < 500); });
36
+ req.on('error', () => resolve(false));
37
+ req.on('timeout', () => { req.destroy(); resolve(false); });
38
+ req.end();
39
+ });
40
+ if (ok) { process.stdout.write(` ${green('\u2714')}\n`); return true; }
41
+ } catch { /* not ready yet */ }
42
+ process.stdout.write('.');
43
+ await new Promise(r => setTimeout(r, intervalMs));
44
+ }
45
+ process.stdout.write(` ${red('\u2718')}\n`);
46
+ return false;
47
+ }
48
+
49
+ function launchctlUid() {
50
+ return execSync('id -u').toString().trim();
51
+ }
52
+
53
+ // ── systemd (Linux) ──────────────────────────────────────────────────────────
54
+
55
+ const SYSTEMD_DIR = resolve(homedir(), '.config', 'systemd', 'user');
56
+ const SYSTEMD_UNIT = resolve(SYSTEMD_DIR, 'mindos.service');
57
+
58
+ const systemd = {
59
+ install() {
60
+ if (!existsSync(SYSTEMD_DIR)) mkdirSync(SYSTEMD_DIR, { recursive: true });
61
+ ensureMindosDir();
62
+ const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
63
+ const unit = [
64
+ '[Unit]',
65
+ 'Description=MindOS app + MCP server',
66
+ 'After=network.target',
67
+ '',
68
+ '[Service]',
69
+ 'Type=simple',
70
+ `ExecStart=${NODE_BIN} ${CLI_PATH} start`,
71
+ 'Restart=on-failure',
72
+ 'RestartSec=3',
73
+ `Environment=HOME=${homedir()}`,
74
+ `Environment=PATH=${currentPath}`,
75
+ `EnvironmentFile=-${resolve(MINDOS_DIR, 'env')}`,
76
+ `StandardOutput=append:${LOG_PATH}`,
77
+ `StandardError=append:${LOG_PATH}`,
78
+ '',
79
+ '[Install]',
80
+ 'WantedBy=default.target',
81
+ ].join('\n');
82
+ writeFileSync(SYSTEMD_UNIT, unit, 'utf-8');
83
+ console.log(green(`\u2714 Wrote ${SYSTEMD_UNIT}`));
84
+ execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
85
+ execSync('systemctl --user enable mindos', { stdio: 'inherit' });
86
+ console.log(green('\u2714 Service installed and enabled'));
87
+ },
88
+
89
+ async start() {
90
+ execSync('systemctl --user start mindos', { stdio: 'inherit' });
91
+ const ok = await waitForService(() => {
92
+ try {
93
+ const out = execSync('systemctl --user is-active mindos', { encoding: 'utf-8' }).trim();
94
+ return out === 'active';
95
+ } catch { return false; }
96
+ });
97
+ if (!ok) {
98
+ console.error(red('\n\u2718 Service failed to start. Last log output:'));
99
+ try { execSync(`journalctl --user -u mindos -n 30 --no-pager`, { stdio: 'inherit' }); } catch {}
100
+ process.exit(1);
101
+ }
102
+ console.log(green('\u2714 Service started'));
103
+ },
104
+
105
+ stop() {
106
+ execSync('systemctl --user stop mindos', { stdio: 'inherit' });
107
+ console.log(green('\u2714 Service stopped'));
108
+ },
109
+
110
+ status() {
111
+ try {
112
+ execSync('systemctl --user status mindos', { stdio: 'inherit' });
113
+ } catch { /* status exits non-zero when stopped */ }
114
+ },
115
+
116
+ logs() {
117
+ execSync(`journalctl --user -u mindos -f`, { stdio: 'inherit' });
118
+ },
119
+
120
+ uninstall() {
121
+ try {
122
+ execSync('systemctl --user disable --now mindos', { stdio: 'inherit' });
123
+ } catch { /* may already be stopped */ }
124
+ if (existsSync(SYSTEMD_UNIT)) {
125
+ rmSync(SYSTEMD_UNIT);
126
+ console.log(green(`\u2714 Removed ${SYSTEMD_UNIT}`));
127
+ }
128
+ execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
129
+ console.log(green('\u2714 Service uninstalled'));
130
+ },
131
+ };
132
+
133
+ // ── launchd (macOS) ──────────────────────────────────────────────────────────
134
+
135
+ const LAUNCHD_DIR = resolve(homedir(), 'Library', 'LaunchAgents');
136
+ const LAUNCHD_PLIST = resolve(LAUNCHD_DIR, 'com.mindos.app.plist');
137
+ const LAUNCHD_LABEL = 'com.mindos.app';
138
+
139
+ const launchd = {
140
+ install() {
141
+ if (!existsSync(LAUNCHD_DIR)) mkdirSync(LAUNCHD_DIR, { recursive: true });
142
+ ensureMindosDir();
143
+ const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
144
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
145
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
146
+ <plist version="1.0">
147
+ <dict>
148
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
149
+ <key>ProgramArguments</key>
150
+ <array>
151
+ <string>${NODE_BIN}</string>
152
+ <string>${CLI_PATH}</string>
153
+ <string>start</string>
154
+ </array>
155
+ <key>RunAtLoad</key><true/>
156
+ <key>KeepAlive</key><true/>
157
+ <key>StandardOutPath</key><string>${LOG_PATH}</string>
158
+ <key>StandardErrorPath</key><string>${LOG_PATH}</string>
159
+ <key>EnvironmentVariables</key>
160
+ <dict>
161
+ <key>HOME</key><string>${homedir()}</string>
162
+ <key>PATH</key><string>${currentPath}</string>
163
+ </dict>
164
+ </dict>
165
+ </plist>
166
+ `;
167
+ writeFileSync(LAUNCHD_PLIST, plist, 'utf-8');
168
+ console.log(green(`\u2714 Wrote ${LAUNCHD_PLIST}`));
169
+ try { execSync(`launchctl bootout gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'pipe' }); } catch {}
170
+ try {
171
+ execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'pipe' });
172
+ } catch (e) {
173
+ const msg = (e.stderr?.toString() ?? e.message ?? '').trim();
174
+ console.error(red(`\n\u2718 launchctl bootstrap failed: ${msg}`));
175
+ console.error(dim(' Try running: launchctl bootout gui/$(id -u)/com.mindos.app then retry.\n'));
176
+ process.exit(1);
177
+ }
178
+ console.log(green('\u2714 Service installed'));
179
+ },
180
+
181
+ async start() {
182
+ execSync(`launchctl kickstart -k gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
183
+ const ok = await waitForService(() => {
184
+ try {
185
+ const out = execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { encoding: 'utf-8' });
186
+ return out.includes('state = running');
187
+ } catch { return false; }
188
+ });
189
+ if (!ok) {
190
+ console.error(red('\n\u2718 Service failed to start. Last log output:'));
191
+ try { execSync(`tail -n 30 ${LOG_PATH}`, { stdio: 'inherit' }); } catch {}
192
+ process.exit(1);
193
+ }
194
+ console.log(green('\u2714 Service started'));
195
+ },
196
+
197
+ stop() {
198
+ try {
199
+ execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
200
+ } catch { /* may not be running */ }
201
+ console.log(green('\u2714 Service stopped'));
202
+ },
203
+
204
+ status() {
205
+ try {
206
+ execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
207
+ } catch {
208
+ console.log(dim('Service is not running'));
209
+ }
210
+ },
211
+
212
+ logs() {
213
+ execSync(`tail -f ${LOG_PATH}`, { stdio: 'inherit' });
214
+ },
215
+
216
+ uninstall() {
217
+ try {
218
+ execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
219
+ } catch { /* may not be running */ }
220
+ if (existsSync(LAUNCHD_PLIST)) {
221
+ rmSync(LAUNCHD_PLIST);
222
+ console.log(green(`\u2714 Removed ${LAUNCHD_PLIST}`));
223
+ }
224
+ console.log(green('\u2714 Service uninstalled'));
225
+ },
226
+ };
227
+
228
+ // ── Gateway dispatcher ───────────────────────────────────────────────────────
229
+
230
+ export async function runGatewayCommand(sub) {
231
+ const platform = getPlatform();
232
+ if (!platform) {
233
+ console.error(red('Daemon mode is not supported on this platform (requires Linux/systemd or macOS/launchd)'));
234
+ process.exit(1);
235
+ }
236
+ const impl = platform === 'systemd' ? systemd : launchd;
237
+ const fn = impl[sub];
238
+ if (!fn) {
239
+ console.error(red(`Unknown gateway subcommand: ${sub}`));
240
+ console.error(dim('Available: install | uninstall | start | stop | status | logs'));
241
+ process.exit(1);
242
+ }
243
+ await fn();
244
+ }
@@ -0,0 +1,156 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { CONFIG_PATH } from './constants.js';
4
+ import { bold, dim, cyan, green, red, yellow } from './colors.js';
5
+ import { expandHome } from './utils.js';
6
+
7
+ export const MCP_AGENTS = {
8
+ 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
9
+ 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
10
+ 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
11
+ 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
12
+ 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
13
+ 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
14
+ 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
15
+ 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
16
+ 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
17
+ };
18
+
19
+ export async function mcpInstall() {
20
+ // Support both `mindos mcp install [agent] [flags]` and `mindos mcp [flags]`
21
+ const sub = process.argv[3];
22
+ const startIdx = sub === 'install' ? 4 : 3;
23
+ const args = process.argv.slice(startIdx);
24
+
25
+ // parse flags
26
+ const hasGlobalFlag = args.includes('-g') || args.includes('--global');
27
+ const hasYesFlag = args.includes('-y') || args.includes('--yes');
28
+ const transportIdx = args.findIndex(a => a === '--transport');
29
+ const urlIdx = args.findIndex(a => a === '--url');
30
+ const tokenIdx = args.findIndex(a => a === '--token');
31
+ const transportArg = transportIdx >= 0 ? args[transportIdx + 1] : null;
32
+ const urlArg = urlIdx >= 0 ? args[urlIdx + 1] : null;
33
+ const tokenArg = tokenIdx >= 0 ? args[tokenIdx + 1] : null;
34
+
35
+ // agent positional arg: first non-flag arg (not preceded by a flag expecting a value)
36
+ const flagsWithValue = new Set(['--transport', '--url', '--token']);
37
+ const agentArg = args.find((a, i) => !a.startsWith('-') && (i === 0 || !flagsWithValue.has(args[i - 1]))) ?? null;
38
+
39
+ const readline = await import('node:readline');
40
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
41
+ const ask = (q) => new Promise(r => rl.question(q, r));
42
+ const choose = async (prompt, options, { defaultIdx = 0, forcePrompt = false } = {}) => {
43
+ if (hasYesFlag && !forcePrompt) return options[defaultIdx];
44
+ console.log(`\n${bold(prompt)}\n`);
45
+ options.forEach((o, i) => console.log(` ${dim(`${i + 1}.`)} ${o.label} ${o.hint ? dim(`(${o.hint})`) : ''}`));
46
+ const ans = await ask(`\n${bold(`Enter number`)} ${dim(`[${defaultIdx + 1}]:`)} `);
47
+ const idx = ans.trim() === '' ? defaultIdx : parseInt(ans.trim(), 10) - 1;
48
+ return options[idx >= 0 && idx < options.length ? idx : defaultIdx];
49
+ };
50
+
51
+ console.log(`\n${bold('🔌 MindOS MCP Install')}\n`);
52
+
53
+ // ── 1. agent ────────────────────────────────────────────────────────────────
54
+ let agentKey = agentArg;
55
+ if (!agentKey) {
56
+ const keys = Object.keys(MCP_AGENTS);
57
+ const picked = await choose('Which Agent would you like to configure?',
58
+ keys.map(k => ({ label: MCP_AGENTS[k].name, hint: k, value: k })), { forcePrompt: true });
59
+ agentKey = picked.value;
60
+ }
61
+
62
+ const agent = MCP_AGENTS[agentKey];
63
+ if (!agent) {
64
+ rl.close();
65
+ console.error(red(`\nUnknown agent: ${agentKey}`));
66
+ console.error(dim(`Supported: ${Object.keys(MCP_AGENTS).join(', ')}`));
67
+ process.exit(1);
68
+ }
69
+
70
+ // ── 2. scope (only ask if agent supports both) ───────────────────────────────
71
+ let isGlobal = hasGlobalFlag;
72
+ if (!hasGlobalFlag) {
73
+ if (agent.project && agent.global) {
74
+ const picked = await choose('Install scope?', [
75
+ { label: 'Project', hint: agent.project, value: 'project' },
76
+ { label: 'Global', hint: agent.global, value: 'global' },
77
+ ]);
78
+ isGlobal = picked.value === 'global';
79
+ } else {
80
+ isGlobal = !agent.project;
81
+ }
82
+ }
83
+
84
+ const configPath = isGlobal ? agent.global : agent.project;
85
+ if (!configPath) {
86
+ rl.close();
87
+ console.error(red(`${agent.name} does not support ${isGlobal ? 'global' : 'project'} scope.`));
88
+ process.exit(1);
89
+ }
90
+
91
+ // ── 3. transport ─────────────────────────────────────────────────────────────
92
+ let transport = transportArg;
93
+ if (!transport) {
94
+ const picked = await choose('Transport type?', [
95
+ { label: 'stdio', hint: 'local, no server process needed (recommended)' },
96
+ { label: 'http', hint: 'URL-based, use when server is running separately or remotely' },
97
+ ]);
98
+ transport = picked.label;
99
+ }
100
+
101
+ // ── 4. url + token (only for http) ───────────────────────────────────────────
102
+ let url = urlArg;
103
+ let token = tokenArg;
104
+
105
+ if (transport === 'http') {
106
+ if (!url) {
107
+ url = hasYesFlag ? 'http://localhost:8787/mcp' : (await ask(`${bold('MCP URL')} ${dim('[http://localhost:8787/mcp]:')} `)).trim() || 'http://localhost:8787/mcp';
108
+ }
109
+
110
+ if (!token) {
111
+ try { token = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).authToken || ''; } catch {}
112
+ if (token) {
113
+ console.log(dim(` Using auth token from ~/.mindos/config.json`));
114
+ } else if (!hasYesFlag) {
115
+ token = (await ask(`${bold('Auth token')} ${dim('(leave blank to skip):')} `)).trim();
116
+ } else {
117
+ console.log(yellow(` Warning: no auth token found in ~/.mindos/config.json — config will have no auth.`));
118
+ console.log(dim(` Run \`mindos onboard\` to set one, or pass --token <token>.`));
119
+ }
120
+ }
121
+ }
122
+
123
+ rl.close();
124
+
125
+ // ── build entry ──────────────────────────────────────────────────────────────
126
+ const entry = transport === 'stdio'
127
+ ? { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } }
128
+ : token
129
+ ? { url, headers: { Authorization: `Bearer ${token}` } }
130
+ : { url };
131
+
132
+ // ── read + merge existing config ─────────────────────────────────────────────
133
+ const absPath = expandHome(configPath);
134
+ let config = {};
135
+ if (existsSync(absPath)) {
136
+ try { config = JSON.parse(readFileSync(absPath, 'utf-8')); } catch {
137
+ console.error(red(`\nFailed to parse existing config: ${absPath}`));
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ if (!config[agent.key]) config[agent.key] = {};
143
+ const existed = !!config[agent.key].mindos;
144
+ config[agent.key].mindos = entry;
145
+
146
+ // ── preview + write ──────────────────────────────────────────────────────────
147
+ console.log(`\n${bold('Preview:')} ${dim(absPath)}\n`);
148
+ console.log(dim(JSON.stringify({ [agent.key]: { mindos: entry } }, null, 2)));
149
+
150
+ const dir = resolve(absPath, '..');
151
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
152
+ writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
153
+
154
+ console.log(`\n${green('\u2714')} ${existed ? 'Updated' : 'Installed'} MindOS MCP for ${bold(agent.name)}`);
155
+ console.log(dim(` Config: ${absPath}\n`));
156
+ }
@@ -0,0 +1,36 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { ROOT } from './constants.js';
5
+ import { bold, red, yellow } from './colors.js';
6
+
7
+ export function spawnMcp(verbose = false) {
8
+ const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
9
+ const webPort = process.env.MINDOS_WEB_PORT || '3000';
10
+ // Ensure mcp/node_modules exists (auto-install on first run)
11
+ const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
12
+ if (!existsSync(mcpSdk)) {
13
+ console.log(yellow('Installing MCP dependencies (first run)...\n'));
14
+ execSync('npm install --prefer-offline --no-workspaces', { cwd: resolve(ROOT, 'mcp'), stdio: 'inherit' });
15
+ }
16
+ const env = {
17
+ ...process.env,
18
+ MCP_PORT: mcpPort,
19
+ MINDOS_URL: `http://localhost:${webPort}`,
20
+ ...(verbose ? { MCP_VERBOSE: '1' } : {}),
21
+ };
22
+ const child = spawn('npx', ['tsx', 'src/index.ts'], {
23
+ cwd: resolve(ROOT, 'mcp'),
24
+ stdio: 'inherit',
25
+ env,
26
+ });
27
+ child.on('error', (err) => {
28
+ if (err.message.includes('EADDRINUSE')) {
29
+ console.error(`\n${red('\u2718')} ${bold(`MCP port ${mcpPort} is already in use`)}`);
30
+ console.error(` ${'Run:'} mindos stop\n`);
31
+ } else {
32
+ console.error(red('MCP server error:'), err.message);
33
+ }
34
+ });
35
+ return child;
36
+ }
package/bin/lib/pid.js ADDED
@@ -0,0 +1,15 @@
1
+ import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { PID_PATH } from './constants.js';
3
+
4
+ export function savePids(...pids) {
5
+ writeFileSync(PID_PATH, pids.filter(Boolean).join('\n'), 'utf-8');
6
+ }
7
+
8
+ export function loadPids() {
9
+ if (!existsSync(PID_PATH)) return [];
10
+ return readFileSync(PID_PATH, 'utf-8').split('\n').map(Number).filter(Boolean);
11
+ }
12
+
13
+ export function clearPids() {
14
+ if (existsSync(PID_PATH)) rmSync(PID_PATH);
15
+ }
@@ -0,0 +1,19 @@
1
+ import { createConnection } from 'node:net';
2
+ import { bold, dim, red } from './colors.js';
3
+
4
+ export function isPortInUse(port) {
5
+ return new Promise((resolve) => {
6
+ const sock = createConnection({ port, host: '127.0.0.1' });
7
+ sock.once('connect', () => { sock.destroy(); resolve(true); });
8
+ sock.once('error', () => { sock.destroy(); resolve(false); });
9
+ });
10
+ }
11
+
12
+ export async function assertPortFree(port, name) {
13
+ if (await isPortInUse(port)) {
14
+ console.error(`\n${red('\u2718')} ${bold(`Port ${port} is already in use`)} ${dim(`(${name})`)}`);
15
+ console.error(`\n ${dim('Stop MindOS:')} mindos stop`);
16
+ console.error(` ${dim('Find the process:')} lsof -i :${port}\n`);
17
+ process.exit(1);
18
+ }
19
+ }
@@ -0,0 +1,51 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { networkInterfaces } from 'node:os';
3
+ import { CONFIG_PATH } from './constants.js';
4
+ import { bold, dim, cyan, green } from './colors.js';
5
+
6
+ export function getLocalIP() {
7
+ try {
8
+ for (const ifaces of Object.values(networkInterfaces())) {
9
+ for (const iface of ifaces) {
10
+ if (iface.family === 'IPv4' && !iface.internal) return iface.address;
11
+ }
12
+ }
13
+ } catch { /* ignore */ }
14
+ return null;
15
+ }
16
+
17
+ export function printStartupInfo(webPort, mcpPort) {
18
+ let config = {};
19
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
20
+ const authToken = config.authToken || '';
21
+ const localIP = getLocalIP();
22
+
23
+ const auth = authToken
24
+ ? `,\n "headers": { "Authorization": "Bearer ${authToken}" }`
25
+ : '';
26
+ const block = (host) =>
27
+ ` {\n "mcpServers": {\n "mindos": {\n "url": "http://${host}:${mcpPort}/mcp"${auth}\n }\n }\n }`;
28
+
29
+ console.log(`\n${'─'.repeat(53)}`);
30
+ console.log(`${bold('🧠 MindOS is starting')}\n`);
31
+ console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
32
+ if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
33
+ console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
34
+ if (localIP) console.log(` ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
35
+ if (localIP) console.log(dim(`\n 💡 Running on a remote server? Open the Network URL (${localIP}) in your browser,\n or use SSH port forwarding: ssh -L ${webPort}:localhost:${webPort} user@${localIP}`));
36
+ console.log();
37
+ console.log(bold('Configure MCP in your Agent:'));
38
+ console.log(dim(' Local (same machine):'));
39
+ console.log(block('localhost'));
40
+ if (localIP) {
41
+ console.log(dim('\n Remote (other device):'));
42
+ console.log(block(localIP));
43
+ }
44
+ if (authToken) {
45
+ console.log(`\n 🔑 ${bold('Auth token:')} ${cyan(authToken)}`);
46
+ console.log(dim(' Run `mindos token` anytime to view it again'));
47
+ }
48
+ console.log(dim('\n Install Skills (optional):'));
49
+ console.log(dim(' npx skills add https://github.com/GeminiLight/MindOS --skill mindos -g -y'));
50
+ console.log(`${'─'.repeat(53)}\n`);
51
+ }