@geminilight/mindos 0.2.1 → 0.4.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 (82) hide show
  1. package/app/app/api/init/route.ts +7 -41
  2. package/app/app/api/mcp/agents/route.ts +72 -0
  3. package/app/app/api/mcp/install/route.ts +95 -0
  4. package/app/app/api/mcp/status/route.ts +47 -0
  5. package/app/app/api/settings/route.ts +3 -0
  6. package/app/app/api/setup/generate-token/route.ts +23 -0
  7. package/app/app/api/setup/route.ts +81 -0
  8. package/app/app/api/skills/route.ts +208 -0
  9. package/app/app/api/sync/route.ts +54 -3
  10. package/app/app/api/update-check/route.ts +52 -0
  11. package/app/app/globals.css +12 -0
  12. package/app/app/layout.tsx +4 -2
  13. package/app/app/login/page.tsx +20 -13
  14. package/app/app/page.tsx +22 -2
  15. package/app/app/setup/page.tsx +9 -0
  16. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  17. package/app/app/view/[...path]/loading.tsx +1 -1
  18. package/app/app/view/[...path]/not-found.tsx +101 -0
  19. package/app/components/AskFab.tsx +1 -1
  20. package/app/components/AskModal.tsx +1 -1
  21. package/app/components/Backlinks.tsx +1 -1
  22. package/app/components/Breadcrumb.tsx +13 -3
  23. package/app/components/CsvView.tsx +5 -6
  24. package/app/components/DirView.tsx +42 -21
  25. package/app/components/FindInPage.tsx +211 -0
  26. package/app/components/HomeContent.tsx +97 -44
  27. package/app/components/JsonView.tsx +1 -2
  28. package/app/components/MarkdownEditor.tsx +1 -2
  29. package/app/components/OnboardingView.tsx +6 -7
  30. package/app/components/SettingsModal.tsx +5 -2
  31. package/app/components/SetupWizard.tsx +479 -0
  32. package/app/components/Sidebar.tsx +1 -1
  33. package/app/components/UpdateBanner.tsx +101 -0
  34. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  35. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  36. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  37. package/app/components/renderers/backlinks/manifest.ts +14 -0
  38. package/app/components/renderers/config/manifest.ts +14 -0
  39. package/app/components/renderers/csv/BoardView.tsx +12 -12
  40. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  41. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  42. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  43. package/app/components/renderers/csv/TableView.tsx +4 -5
  44. package/app/components/renderers/csv/manifest.ts +14 -0
  45. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  46. package/app/components/renderers/diff/manifest.ts +14 -0
  47. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  48. package/app/components/renderers/graph/manifest.ts +14 -0
  49. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  50. package/app/components/renderers/summary/manifest.ts +14 -0
  51. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  52. package/app/components/renderers/timeline/manifest.ts +14 -0
  53. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  54. package/app/components/renderers/todo/manifest.ts +14 -0
  55. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  56. package/app/components/renderers/workflow/manifest.ts +14 -0
  57. package/app/components/settings/McpTab.tsx +549 -0
  58. package/app/components/settings/SyncTab.tsx +139 -50
  59. package/app/components/settings/types.ts +1 -1
  60. package/app/data/pages/home.png +0 -0
  61. package/app/lib/i18n.ts +270 -10
  62. package/app/lib/renderers/index.ts +20 -89
  63. package/app/lib/renderers/registry.ts +4 -1
  64. package/app/lib/settings.ts +15 -1
  65. package/app/lib/template.ts +45 -0
  66. package/app/package.json +1 -0
  67. package/app/types/semver.d.ts +8 -0
  68. package/bin/cli.js +137 -24
  69. package/bin/lib/build.js +53 -18
  70. package/bin/lib/colors.js +3 -1
  71. package/bin/lib/config.js +4 -0
  72. package/bin/lib/constants.js +2 -0
  73. package/bin/lib/debug.js +10 -0
  74. package/bin/lib/startup.js +21 -20
  75. package/bin/lib/stop.js +41 -3
  76. package/bin/lib/sync.js +65 -53
  77. package/bin/lib/update-check.js +94 -0
  78. package/bin/lib/utils.js +2 -2
  79. package/package.json +1 -1
  80. package/scripts/gen-renderer-index.js +57 -0
  81. package/scripts/setup.js +117 -1
  82. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
package/bin/lib/config.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { CONFIG_PATH } from './constants.js';
3
3
 
4
+ let loaded = false;
5
+
4
6
  export function loadConfig() {
7
+ if (loaded) return;
8
+ loaded = true;
5
9
  if (!existsSync(CONFIG_PATH)) return;
6
10
  let config;
7
11
  try {
@@ -11,3 +11,5 @@ export const MINDOS_DIR = resolve(homedir(), '.mindos');
11
11
  export const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
12
12
  export const CLI_PATH = resolve(__dirname, '..', 'cli.js');
13
13
  export const NODE_BIN = process.execPath;
14
+ export const UPDATE_CHECK_PATH = resolve(MINDOS_DIR, 'update-check.json');
15
+ export const DEPS_STAMP = resolve(MINDOS_DIR, 'deps-hash');
@@ -0,0 +1,10 @@
1
+ import { dim } from './colors.js';
2
+
3
+ const enabled = process.env.MINDOS_DEBUG === '1' || process.argv.includes('--verbose');
4
+
5
+ export function debug(...args) {
6
+ if (enabled) {
7
+ const ts = new Date().toISOString().slice(11, 23);
8
+ console.error(dim(`[${ts}]`), ...args);
9
+ }
10
+ }
@@ -3,6 +3,7 @@ import { networkInterfaces } from 'node:os';
3
3
  import { CONFIG_PATH } from './constants.js';
4
4
  import { bold, dim, cyan, green, yellow } from './colors.js';
5
5
  import { getSyncStatus } from './sync.js';
6
+ import { checkForUpdate, printUpdateHint } from './update-check.js';
6
7
 
7
8
  export function getLocalIP() {
8
9
  try {
@@ -15,39 +16,32 @@ export function getLocalIP() {
15
16
  return null;
16
17
  }
17
18
 
18
- export function printStartupInfo(webPort, mcpPort) {
19
+ export async function printStartupInfo(webPort, mcpPort) {
20
+ // Fire update check immediately (non-blocking)
21
+ const updatePromise = checkForUpdate().catch(() => null);
22
+
19
23
  let config = {};
20
24
  try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
21
25
  const authToken = config.authToken || '';
22
26
  const localIP = getLocalIP();
23
27
 
24
- const auth = authToken
25
- ? `,\n "headers": { "Authorization": "Bearer ${authToken}" }`
26
- : '';
27
- const block = (host) =>
28
- ` {\n "mcpServers": {\n "mindos": {\n "url": "http://${host}:${mcpPort}/mcp"${auth}\n }\n }\n }`;
29
-
30
28
  console.log(`\n${'─'.repeat(53)}`);
31
29
  console.log(`${bold('🧠 MindOS is starting')}\n`);
32
30
  console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
33
31
  if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
34
32
  console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
35
33
  if (localIP) console.log(` ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
36
- 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}`));
37
- console.log();
38
- console.log(bold('Configure MCP in your Agent:'));
39
- console.log(dim(' Local (same machine):'));
40
- console.log(block('localhost'));
41
- if (localIP) {
42
- console.log(dim('\n Remote (other device):'));
43
- console.log(block(localIP));
44
- }
34
+
45
35
  if (authToken) {
46
- console.log(`\n 🔑 ${bold('Auth token:')} ${cyan(authToken)}`);
47
- console.log(dim(' Run `mindos token` anytime to view it again'));
36
+ const maskedToken = authToken.length > 8 ? authToken.slice(0, 8) + '····' : (authToken.length > 4 ? authToken.slice(0, 4) + '····' : '····');
37
+ console.log(` ${green('●')} Auth ${cyan(maskedToken)} ${dim('(run `mindos token` for full config)')}`);
48
38
  }
49
- console.log(dim('\n Install Skills (optional):'));
50
- console.log(dim(' npx skills add https://github.com/GeminiLight/MindOS --skill mindos -g -y'));
39
+
40
+ // MCP quick-connect hint
41
+ console.log(`\n ${dim('Quick connect:')} ${cyan('mindos mcp install claude-code -g -y')}`);
42
+ console.log(` ${dim('Full config:')} ${cyan('mindos token')}`);
43
+
44
+ if (localIP) console.log(dim(`\n 💡 Remote? SSH port forwarding: ssh -L ${webPort}:localhost:${webPort} -L ${mcpPort}:localhost:${mcpPort} user@${localIP}`));
51
45
 
52
46
  // Sync status
53
47
  const mindRoot = config.mindRoot;
@@ -70,5 +64,12 @@ export function printStartupInfo(webPort, mcpPort) {
70
64
  } catch { /* sync check is best-effort */ }
71
65
  }
72
66
 
67
+ // Wait for update check result (max 4s, then give up)
68
+ const latestVersion = await Promise.race([
69
+ updatePromise,
70
+ new Promise(r => setTimeout(() => r(null), 4000)),
71
+ ]);
72
+ if (latestVersion) printUpdateHint(latestVersion);
73
+
73
74
  console.log(`${'─'.repeat(53)}\n`);
74
75
  }
package/bin/lib/stop.js CHANGED
@@ -1,13 +1,51 @@
1
1
  import { execSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { green, yellow, dim } from './colors.js';
3
4
  import { loadPids, clearPids } from './pid.js';
5
+ import { CONFIG_PATH } from './constants.js';
6
+
7
+ /**
8
+ * Kill processes listening on the given port.
9
+ * Returns number of processes killed.
10
+ */
11
+ function killByPort(port) {
12
+ let killed = 0;
13
+ try {
14
+ const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
15
+ if (output) {
16
+ for (const p of output.split('\n')) {
17
+ const pid = Number(p);
18
+ if (pid > 0) {
19
+ try { process.kill(pid, 'SIGTERM'); killed++; } catch {}
20
+ }
21
+ }
22
+ }
23
+ } catch {
24
+ // lsof not available or no processes found
25
+ }
26
+ return killed;
27
+ }
4
28
 
5
29
  export function stopMindos() {
6
30
  const pids = loadPids();
7
31
  if (!pids.length) {
8
- console.log(yellow('No PID file found, trying pattern-based stop...'));
9
- try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
10
- try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
32
+ console.log(yellow('No PID file found, trying port-based stop...'));
33
+ // Read ports from config
34
+ let webPort = '3000', mcpPort = '8787';
35
+ try {
36
+ const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
37
+ if (config.port) webPort = String(config.port);
38
+ if (config.mcpPort) mcpPort = String(config.mcpPort);
39
+ } catch {}
40
+ let stopped = 0;
41
+ for (const port of [webPort, mcpPort]) {
42
+ stopped += killByPort(port);
43
+ }
44
+ if (stopped === 0) {
45
+ // Fallback: pkill pattern match (for envs without lsof)
46
+ try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
47
+ try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
48
+ }
11
49
  console.log(green('\u2714 Done'));
12
50
  return;
13
51
  }
package/bin/lib/sync.js CHANGED
@@ -139,52 +139,66 @@ let activePullInterval = null;
139
139
  /**
140
140
  * Interactive sync init — configure remote git repo
141
141
  */
142
- export async function initSync(mindRoot) {
142
+ export async function initSync(mindRoot, opts = {}) {
143
143
  if (!mindRoot) { console.error(red('No mindRoot configured.')); process.exit(1); }
144
144
 
145
- const readline = await import('node:readline');
146
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
147
- const ask = (q) => new Promise(r => rl.question(q, r));
145
+ const nonInteractive = opts.nonInteractive || false;
146
+ let remoteUrl = opts.remote || '';
147
+ let token = opts.token || '';
148
+ let branch = opts.branch || 'main';
148
149
 
149
- // 1. Ensure git repo
150
- if (!isGitRepo(mindRoot)) {
151
- console.log(dim('Initializing git repository...'));
152
- execSync('git init', { cwd: mindRoot, stdio: 'inherit' });
153
- execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }).toString();
154
- }
150
+ if (nonInteractive) {
151
+ // Non-interactive mode: all params from opts
152
+ if (!remoteUrl) {
153
+ throw new Error('Remote URL is required in non-interactive mode');
154
+ }
155
+ } else {
156
+ // Interactive mode: prompt user
157
+ const readline = await import('node:readline');
158
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
+ const ask = (q) => new Promise(r => rl.question(q, r));
160
+
161
+ // 2. Remote URL
162
+ const currentRemote = getRemoteUrl(mindRoot);
163
+ const defaultUrl = currentRemote || '';
164
+ const urlPrompt = currentRemote
165
+ ? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
166
+ : `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
167
+ remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
168
+
169
+ if (!remoteUrl) {
170
+ console.error(red('Remote URL is required.'));
171
+ rl.close();
172
+ process.exit(1);
173
+ }
155
174
 
156
- // 2. Remote URL
157
- const currentRemote = getRemoteUrl(mindRoot);
158
- const defaultUrl = currentRemote || '';
159
- const urlPrompt = currentRemote
160
- ? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
161
- : `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
162
- let remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
175
+ // 3. Token for HTTPS
176
+ if (remoteUrl.startsWith('https://')) {
177
+ token = (await ask(`${bold('Access Token')} ${dim('(GitHub PAT / GitLab PAT, leave empty if SSH)')}: `)).trim();
178
+ }
163
179
 
164
- if (!remoteUrl) {
165
- console.error(red('Remote URL is required.'));
166
180
  rl.close();
167
- process.exit(1);
168
181
  }
169
182
 
170
- // 3. Token for HTTPS
171
- let token = '';
172
- if (remoteUrl.startsWith('https://')) {
173
- token = (await ask(`${bold('Access Token')} ${dim('(GitHub PAT / GitLab PAT, leave empty if SSH)')}: `)).trim();
174
- if (token) {
175
- // Inject token into URL for credential storage
176
- const urlObj = new URL(remoteUrl);
177
- urlObj.username = 'oauth2';
178
- urlObj.password = token;
179
- const authUrl = urlObj.toString();
180
- // Configure credential helper
181
- try { execSync(`git config credential.helper store`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
182
- // Store the credential
183
- try {
184
- const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
185
- execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
186
- } catch {}
187
- }
183
+ // 1. Ensure git repo
184
+ if (!isGitRepo(mindRoot)) {
185
+ if (!nonInteractive) console.log(dim('Initializing git repository...'));
186
+ execSync('git init', { cwd: mindRoot, stdio: 'pipe' });
187
+ try { execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
188
+ }
189
+
190
+ // Handle token for HTTPS
191
+ if (token && remoteUrl.startsWith('https://')) {
192
+ const urlObj = new URL(remoteUrl);
193
+ urlObj.username = 'oauth2';
194
+ urlObj.password = token;
195
+ // Configure credential helper
196
+ try { execSync(`git config credential.helper store`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
197
+ // Store the credential
198
+ try {
199
+ const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
200
+ execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
201
+ } catch {}
188
202
  }
189
203
 
190
204
  // 4. Set remote
@@ -195,50 +209,48 @@ export async function initSync(mindRoot) {
195
209
  }
196
210
 
197
211
  // 5. Test connection
198
- console.log(dim('Testing connection...'));
212
+ if (!nonInteractive) console.log(dim('Testing connection...'));
199
213
  try {
200
- execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe' });
201
- console.log(green('✔ Connection successful'));
214
+ execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe', timeout: 15000 });
215
+ if (!nonInteractive) console.log(green('✔ Connection successful'));
202
216
  } catch {
217
+ const errMsg = 'Remote not reachable — check URL and credentials';
218
+ if (nonInteractive) throw new Error(errMsg);
203
219
  console.error(red('✘ Could not connect to remote. Check your URL and credentials.'));
204
- rl.close();
205
220
  process.exit(1);
206
221
  }
207
222
 
208
- rl.close();
209
-
210
223
  // 6. Save sync config
211
224
  const syncConfig = {
212
225
  enabled: true,
213
226
  provider: 'git',
214
227
  remote: 'origin',
215
- branch: getBranch(mindRoot),
228
+ branch: branch || getBranch(mindRoot),
216
229
  autoCommitInterval: 30,
217
230
  autoPullInterval: 300,
218
231
  };
219
232
  saveSyncConfig(syncConfig);
220
- console.log(green('✔ Sync configured'));
233
+ if (!nonInteractive) console.log(green('✔ Sync configured'));
221
234
 
222
235
  // 7. First sync: pull if remote has content, push otherwise
223
236
  try {
224
237
  const refs = gitExec('git ls-remote --heads origin', mindRoot);
225
238
  if (refs) {
226
- console.log(dim('Pulling from remote...'));
239
+ if (!nonInteractive) console.log(dim('Pulling from remote...'));
227
240
  try {
228
- execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: 'inherit' });
241
+ execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: nonInteractive ? 'pipe' : 'inherit' });
229
242
  } catch {
230
- // Might fail if empty or conflicts that's fine for initial setup
231
- console.log(yellow('Pull completed with warnings. Check for conflicts.'));
243
+ if (!nonInteractive) console.log(yellow('Pull completed with warnings. Check for conflicts.'));
232
244
  }
233
245
  } else {
234
- console.log(dim('Pushing to remote...'));
246
+ if (!nonInteractive) console.log(dim('Pushing to remote...'));
235
247
  autoCommitAndPush(mindRoot);
236
248
  }
237
249
  } catch {
238
- console.log(dim('Performing initial push...'));
250
+ if (!nonInteractive) console.log(dim('Performing initial push...'));
239
251
  autoCommitAndPush(mindRoot);
240
252
  }
241
- console.log(green('✔ Initial sync complete\n'));
253
+ if (!nonInteractive) console.log(green('✔ Initial sync complete\n'));
242
254
  }
243
255
 
244
256
  /**
@@ -0,0 +1,94 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { ROOT, UPDATE_CHECK_PATH } from './constants.js';
4
+ import { bold, dim, cyan, yellow } from './colors.js';
5
+
6
+ const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
7
+
8
+ const REGISTRIES = [
9
+ 'https://registry.npmmirror.com/@geminilight/mindos/latest',
10
+ 'https://registry.npmjs.org/@geminilight/mindos/latest',
11
+ ];
12
+
13
+ /** Simple semver "a > b" comparison (major.minor.patch only). */
14
+ function semverGt(a, b) {
15
+ const pa = a.split('.').map(Number);
16
+ const pb = b.split('.').map(Number);
17
+ for (let i = 0; i < 3; i++) {
18
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
19
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
20
+ }
21
+ return false;
22
+ }
23
+
24
+ function getCurrentVersion() {
25
+ try {
26
+ return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
27
+ } catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+
32
+ function readCache() {
33
+ try {
34
+ return JSON.parse(readFileSync(UPDATE_CHECK_PATH, 'utf-8'));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function writeCache(latestVersion) {
41
+ try {
42
+ writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({
43
+ lastCheck: new Date().toISOString(),
44
+ latestVersion,
45
+ }), 'utf-8');
46
+ } catch { /* best-effort */ }
47
+ }
48
+
49
+ async function fetchLatest() {
50
+ for (const url of REGISTRIES) {
51
+ try {
52
+ const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
53
+ if (res.ok) {
54
+ const data = await res.json();
55
+ return data.version;
56
+ }
57
+ } catch {
58
+ continue;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Check for updates. Returns the latest version string if an update is
66
+ * available, or null if up-to-date / check fails.
67
+ */
68
+ export async function checkForUpdate() {
69
+ if (process.env.MINDOS_NO_UPDATE_CHECK === '1') return null;
70
+
71
+ const current = getCurrentVersion();
72
+ const cache = readCache();
73
+
74
+ // Cache hit — still fresh
75
+ if (cache?.lastCheck) {
76
+ const age = Date.now() - new Date(cache.lastCheck).getTime();
77
+ if (age < TTL_MS) {
78
+ return (cache.latestVersion && semverGt(cache.latestVersion, current))
79
+ ? cache.latestVersion
80
+ : null;
81
+ }
82
+ }
83
+
84
+ // Cache miss or expired — fetch
85
+ const latest = await fetchLatest();
86
+ if (latest) writeCache(latest);
87
+ return (latest && semverGt(latest, current)) ? latest : null;
88
+ }
89
+
90
+ /** Print update hint line if an update is available. */
91
+ export function printUpdateHint(latestVersion) {
92
+ const current = getCurrentVersion();
93
+ console.log(`\n ${yellow('⬆')} ${bold(`MindOS v${latestVersion}`)} available ${dim(`(current: v${current})`)}. Run ${cyan('mindos update')} to upgrade.`);
94
+ }
package/bin/lib/utils.js CHANGED
@@ -6,8 +6,8 @@ import { ROOT } from './constants.js';
6
6
  export function run(command, cwd = ROOT) {
7
7
  try {
8
8
  execSync(command, { cwd, stdio: 'inherit', env: process.env });
9
- } catch {
10
- process.exit(1);
9
+ } catch (err) {
10
+ process.exit(err.status || 1);
11
11
  }
12
12
  }
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Scans app/components/renderers/*/manifest.ts and generates
3
+ // app/lib/renderers/index.ts with auto-discovered imports.
4
+ //
5
+ // Run: node scripts/gen-renderer-index.js
6
+ // Hooked into: npm run build (via prebuild)
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const renderersDir = path.resolve(__dirname, '../app/components/renderers');
14
+ const outputFile = path.resolve(__dirname, '../app/lib/renderers/index.ts');
15
+
16
+ // Scan for manifest.ts in immediate subdirectories
17
+ const dirs = fs.readdirSync(renderersDir, { withFileTypes: true })
18
+ .filter(d => d.isDirectory())
19
+ .filter(d => fs.existsSync(path.join(renderersDir, d.name, 'manifest.ts')))
20
+ .map(d => d.name)
21
+ .sort();
22
+
23
+ if (dirs.length === 0) {
24
+ console.error('No manifest.ts files found in', renderersDir);
25
+ process.exit(1);
26
+ }
27
+
28
+ // kebab-case → camelCase
29
+ function toCamel(s) {
30
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
31
+ }
32
+
33
+ const imports = dirs.map(dir => {
34
+ const varName = toCamel(dir);
35
+ return `import { manifest as ${varName} } from '@/components/renderers/${dir}/manifest';`;
36
+ }).join('\n');
37
+
38
+ const varNames = dirs.map(toCamel);
39
+
40
+ const code = `/**
41
+ * AUTO-GENERATED by scripts/gen-renderer-index.js — do not edit manually.
42
+ * To regenerate: node scripts/gen-renderer-index.js
43
+ */
44
+ import { registerRenderer } from './registry';
45
+ ${imports}
46
+
47
+ const manifests = [
48
+ ${varNames.join(', ')},
49
+ ];
50
+
51
+ for (const m of manifests) {
52
+ registerRenderer(m);
53
+ }
54
+ `;
55
+
56
+ fs.writeFileSync(outputFile, code, 'utf-8');
57
+ console.log(`Generated ${path.relative(process.cwd(), outputFile)} with ${dirs.length} renderers: ${dirs.join(', ')}`);
package/scripts/setup.js CHANGED
@@ -26,7 +26,7 @@ import { homedir, tmpdir, networkInterfaces } from 'node:os';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { createInterface } from 'node:readline';
28
28
  import { pipeline } from 'node:stream/promises';
29
- import { execSync } from 'node:child_process';
29
+ import { execSync, spawn } from 'node:child_process';
30
30
  import { randomBytes, createHash } from 'node:crypto';
31
31
  import { createConnection } from 'node:net';
32
32
 
@@ -41,6 +41,14 @@ const T = {
41
41
  title: { en: '🧠 MindOS Setup', zh: '🧠 MindOS 初始化' },
42
42
  langHint: { en: ' ← → switch language / 切换语言 ↑ ↓ navigate Enter confirm', zh: ' ← → switch language / 切换语言 ↑ ↓ 上下切换 Enter 确认' },
43
43
 
44
+ // mode selection
45
+ modePrompt: { en: 'Setup mode', zh: '配置方式' },
46
+ modeOpts: { en: ['CLI — terminal wizard', 'GUI — browser wizard (recommended)'], zh: ['CLI — 终端向导', 'GUI — 浏览器向导(推荐)'] },
47
+ modeVals: ['cli', 'gui'],
48
+ guiStarting: { en: '⏳ Starting server for GUI setup...', zh: '⏳ 正在启动服务...' },
49
+ guiReady: { en: (url) => `🌐 Complete setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中完成配置: ${url}` },
50
+ guiOpenFailed: { en: (url) => ` Could not open browser automatically. Open this URL manually:\n ${url}`, zh: (url) => ` 无法自动打开浏览器,请手动访问:\n ${url}` },
51
+
44
52
  // step labels
45
53
  step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
46
54
  stepTitles: {
@@ -106,6 +114,8 @@ const T = {
106
114
  cfgKept: { en: '✔ Keeping existing config', zh: '✔ 保留现有配置' },
107
115
  cfgKeptNote: { en: ' Settings from this session were not saved', zh: ' 本次填写的设置未保存' },
108
116
  cfgSaved: { en: '✔ Config saved', zh: '✔ 配置已保存' },
117
+ cfgConfirm: { en: 'Save this configuration?', zh: '保存此配置?' },
118
+ cfgAborted: { en: '✘ Setup cancelled. Run `mindos onboard` to try again.', zh: '✘ 设置已取消。运行 `mindos onboard` 重新开始。' },
109
119
  yesNo: { en: '[y/N]', zh: '[y/N]' },
110
120
  yesNoDefault: { en: '[Y/n]', zh: '[Y/n]' },
111
121
  startNow: { en: 'Start MindOS now?', zh: '现在启动 MindOS?' },
@@ -493,11 +503,95 @@ async function applyTemplate(tpl, mindDir) {
493
503
  }
494
504
  }
495
505
 
506
+ // ── GUI Setup ─────────────────────────────────────────────────────────────────
507
+
508
+ function openBrowser(url) {
509
+ try {
510
+ const platform = process.platform;
511
+ if (platform === 'darwin') {
512
+ execSync(`open "${url}"`, { stdio: 'ignore' });
513
+ } else if (platform === 'linux') {
514
+ // Check for WSL
515
+ const isWSL = existsSync('/proc/version') &&
516
+ readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
517
+ if (isWSL) {
518
+ execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
519
+ } else {
520
+ execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
521
+ }
522
+ } else {
523
+ execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
524
+ }
525
+ return true;
526
+ } catch {
527
+ return false;
528
+ }
529
+ }
530
+
531
+ async function startGuiSetup() {
532
+ // Ensure ~/.mindos directory exists
533
+ mkdirSync(MINDOS_DIR, { recursive: true });
534
+
535
+ // Read or create config, set setupPending
536
+ let config = {};
537
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
538
+ config.setupPending = true;
539
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
540
+
541
+ // Find a free port
542
+ const port = await findFreePort(3000);
543
+ if (config.port === undefined) {
544
+ config.port = port;
545
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
546
+ }
547
+ const usePort = config.port || port;
548
+
549
+ write(c.yellow(t('guiStarting') + '\n'));
550
+
551
+ // Start the server in the background
552
+ const cliPath = resolve(__dirname, '../bin/cli.js');
553
+ const child = spawn(process.execPath, [cliPath, 'start'], {
554
+ detached: true,
555
+ stdio: 'ignore',
556
+ env: { ...process.env, PORT: String(usePort) },
557
+ });
558
+ child.unref();
559
+
560
+ // Wait for the server to be ready
561
+ const { waitForHttp } = await import('../bin/lib/gateway.js');
562
+ const ready = await waitForHttp(usePort, { retries: 60, intervalMs: 1000, label: 'MindOS' });
563
+
564
+ if (!ready) {
565
+ write(c.red('\n✘ Server failed to start.\n'));
566
+ process.exit(1);
567
+ }
568
+
569
+ const url = `http://localhost:${usePort}/setup`;
570
+ console.log(`\n${c.green(tf('guiReady', url))}\n`);
571
+
572
+ const opened = openBrowser(url);
573
+ if (!opened) {
574
+ console.log(c.dim(tf('guiOpenFailed', url)));
575
+ }
576
+
577
+ process.exit(0);
578
+ }
579
+
496
580
  // ── Main ──────────────────────────────────────────────────────────────────────
497
581
 
498
582
  async function main() {
499
583
  console.log(`\n${c.bold(t('title'))}\n\n${c.dim(t('langHint'))}\n`);
500
584
 
585
+ // ── Mode selection: CLI or GUI ───────────────────────────────────────────
586
+ const mode = await select('modePrompt', 'modeOpts', 'modeVals');
587
+
588
+ if (mode === 'gui') {
589
+ await startGuiSetup();
590
+ return;
591
+ }
592
+
593
+ // ── CLI mode continues below ─────────────────────────────────────────────
594
+
501
595
  // ── Early overwrite check ─────────────────────────────────────────────────
502
596
  if (existsSync(CONFIG_PATH)) {
503
597
  let existing = {};
@@ -672,6 +766,28 @@ async function main() {
672
766
  },
673
767
  };
674
768
 
769
+ // ── Configuration Summary & Confirmation ──────────────────────────────────
770
+ const maskPw = (s) => s ? '•'.repeat(Math.min(s.length, 8)) : '';
771
+ const maskTk = (s) => s && s.length > 8 ? s.slice(0, 8) + '····' : (s ? s.slice(0, 4) + '····' : '');
772
+ const sep = '━'.repeat(40);
773
+ write(`\n${sep}\n`);
774
+ write(`${c.bold(uiLang === 'zh' ? '配置摘要' : 'Configuration Summary')}\n`);
775
+ write(`${sep}\n`);
776
+ write(` ${c.dim('Knowledge base:')} ${mindDir}\n`);
777
+ write(` ${c.dim('Web port:')} ${webPort}\n`);
778
+ write(` ${c.dim('MCP port:')} ${mcpPort}\n`);
779
+ write(` ${c.dim('Auth token:')} ${maskTk(authToken)}\n`);
780
+ if (webPassword) write(` ${c.dim('Web password:')} ${maskPw(webPassword)}\n`);
781
+ write(` ${c.dim('AI provider:')} ${config.ai.provider}\n`);
782
+ write(` ${c.dim('Start mode:')} ${startMode}\n`);
783
+ write(`${sep}\n`);
784
+
785
+ const confirmSave = await askYesNoDefault('cfgConfirm');
786
+ if (!confirmSave) {
787
+ console.log(c.red(t('cfgAborted')));
788
+ process.exit(0);
789
+ }
790
+
675
791
  mkdirSync(MINDOS_DIR, { recursive: true });
676
792
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
677
793
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);