@geminilight/mindos 0.3.0 → 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 (76) hide show
  1. package/app/app/api/mcp/agents/route.ts +72 -0
  2. package/app/app/api/mcp/install/route.ts +95 -0
  3. package/app/app/api/mcp/status/route.ts +47 -0
  4. package/app/app/api/skills/route.ts +208 -0
  5. package/app/app/api/sync/route.ts +54 -3
  6. package/app/app/api/update-check/route.ts +52 -0
  7. package/app/app/globals.css +12 -0
  8. package/app/app/layout.tsx +4 -2
  9. package/app/app/login/page.tsx +20 -13
  10. package/app/app/page.tsx +17 -2
  11. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  12. package/app/app/view/[...path]/loading.tsx +1 -1
  13. package/app/app/view/[...path]/not-found.tsx +101 -0
  14. package/app/components/AskFab.tsx +1 -1
  15. package/app/components/AskModal.tsx +1 -1
  16. package/app/components/Backlinks.tsx +1 -1
  17. package/app/components/Breadcrumb.tsx +13 -3
  18. package/app/components/CsvView.tsx +5 -6
  19. package/app/components/DirView.tsx +42 -21
  20. package/app/components/FindInPage.tsx +211 -0
  21. package/app/components/HomeContent.tsx +97 -44
  22. package/app/components/JsonView.tsx +1 -2
  23. package/app/components/MarkdownEditor.tsx +1 -2
  24. package/app/components/OnboardingView.tsx +6 -7
  25. package/app/components/SettingsModal.tsx +5 -2
  26. package/app/components/SetupWizard.tsx +4 -4
  27. package/app/components/Sidebar.tsx +1 -1
  28. package/app/components/UpdateBanner.tsx +101 -0
  29. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  30. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  31. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  32. package/app/components/renderers/backlinks/manifest.ts +14 -0
  33. package/app/components/renderers/config/manifest.ts +14 -0
  34. package/app/components/renderers/csv/BoardView.tsx +12 -12
  35. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  36. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  37. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  38. package/app/components/renderers/csv/TableView.tsx +4 -5
  39. package/app/components/renderers/csv/manifest.ts +14 -0
  40. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  41. package/app/components/renderers/diff/manifest.ts +14 -0
  42. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  43. package/app/components/renderers/graph/manifest.ts +14 -0
  44. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  45. package/app/components/renderers/summary/manifest.ts +14 -0
  46. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  47. package/app/components/renderers/timeline/manifest.ts +14 -0
  48. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  49. package/app/components/renderers/todo/manifest.ts +14 -0
  50. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  51. package/app/components/renderers/workflow/manifest.ts +14 -0
  52. package/app/components/settings/McpTab.tsx +549 -0
  53. package/app/components/settings/SyncTab.tsx +139 -50
  54. package/app/components/settings/types.ts +1 -1
  55. package/app/data/pages/home.png +0 -0
  56. package/app/lib/i18n.ts +178 -10
  57. package/app/lib/renderers/index.ts +20 -89
  58. package/app/lib/renderers/registry.ts +4 -1
  59. package/app/lib/settings.ts +3 -0
  60. package/app/package.json +1 -0
  61. package/app/types/semver.d.ts +8 -0
  62. package/bin/cli.js +137 -24
  63. package/bin/lib/build.js +53 -18
  64. package/bin/lib/colors.js +3 -1
  65. package/bin/lib/config.js +4 -0
  66. package/bin/lib/constants.js +2 -0
  67. package/bin/lib/debug.js +10 -0
  68. package/bin/lib/startup.js +21 -20
  69. package/bin/lib/stop.js +41 -3
  70. package/bin/lib/sync.js +65 -53
  71. package/bin/lib/update-check.js +94 -0
  72. package/bin/lib/utils.js +2 -2
  73. package/package.json +1 -1
  74. package/scripts/gen-renderer-index.js +57 -0
  75. package/scripts/setup.js +24 -0
  76. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
package/app/package.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p ${MINDOS_WEB_PORT:-3000}",
7
+ "prebuild": "node ../scripts/gen-renderer-index.js",
7
8
  "build": "next build",
8
9
  "start": "next start -p ${MINDOS_WEB_PORT:-3000}",
9
10
  "lint": "eslint",
@@ -0,0 +1,8 @@
1
+ declare module 'semver' {
2
+ export function gt(v1: string, v2: string): boolean;
3
+ export function lt(v1: string, v2: string): boolean;
4
+ export function gte(v1: string, v2: string): boolean;
5
+ export function lte(v1: string, v2: string): boolean;
6
+ export function eq(v1: string, v2: string): boolean;
7
+ export function valid(v: string | null): string | null;
8
+ }
package/bin/cli.js CHANGED
@@ -33,6 +33,7 @@
33
33
  * mindos logs — tail service logs (~/.mindos/mindos.log)
34
34
  * mindos config show — print current config (API keys masked)
35
35
  * mindos config set <key> <val> — update a single config field
36
+ * mindos config unset <key> — remove a config field
36
37
  * mindos config validate — validate config file
37
38
  */
38
39
 
@@ -58,6 +59,15 @@ import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, l
58
59
  // ── Commands ──────────────────────────────────────────────────────────────────
59
60
 
60
61
  const cmd = process.argv[2];
62
+
63
+ // ── --version / -v ──────────────────────────────────────────────────────────
64
+ // --help / -h is handled at entry section (resolvedCmd = null → help block)
65
+ if (cmd === '--version' || cmd === '-v') {
66
+ const version = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
67
+ console.log(`mindos/${version} node/${process.version} ${process.platform}-${process.arch}`);
68
+ process.exit(0);
69
+ }
70
+
61
71
  const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
62
72
  const isVerbose = process.argv.includes('--verbose');
63
73
  const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
@@ -68,8 +78,8 @@ const commands = {
68
78
  const daemonFlag = process.argv.includes('--install-daemon') ? ' --install-daemon' : '';
69
79
  run(`node ${resolve(ROOT, 'scripts/setup.js')}${daemonFlag}`);
70
80
  },
71
- init: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
72
- setup: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
81
+ init: async () => commands.onboard(),
82
+ setup: async () => commands.onboard(),
73
83
 
74
84
  // ── open ───────────────────────────────────────────────────────────────────
75
85
  open: () => {
@@ -123,8 +133,8 @@ const commands = {
123
133
  console.log(`${sep}`);
124
134
  console.log(`${bold('Claude Code')}`);
125
135
  console.log(`${sep}`);
126
- console.log(dim('一键安装:') + ` mindos mcp install claude-code -g -y`);
127
- console.log(dim('\n手动配置 (~/.claude.json):'));
136
+ console.log(dim('Quick install:') + ` mindos mcp install claude-code -g -y`);
137
+ console.log(dim('\nManual config (~/.claude.json):'));
128
138
  console.log(JSON.stringify({
129
139
  mcpServers: {
130
140
  mindos: {
@@ -138,8 +148,8 @@ const commands = {
138
148
  console.log(`\n${sep}`);
139
149
  console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
140
150
  console.log(`${sep}`);
141
- console.log(dim('一键安装:') + ` mindos mcp install codebuddy -g -y`);
142
- console.log(dim('\n手动配置 (~/.claude-internal/.claude.json):'));
151
+ console.log(dim('Quick install:') + ` mindos mcp install codebuddy -g -y`);
152
+ console.log(dim('\nManual config (~/.claude-internal/.claude.json):'));
143
153
  console.log(JSON.stringify({
144
154
  mcpServers: {
145
155
  mindos: {
@@ -153,8 +163,8 @@ const commands = {
153
163
  console.log(`\n${sep}`);
154
164
  console.log(`${bold('Cursor')}`);
155
165
  console.log(`${sep}`);
156
- console.log(dim('一键安装:') + ` mindos mcp install cursor -g -y`);
157
- console.log(dim('\n手动配置 (~/.cursor/mcp.json):'));
166
+ console.log(dim('Quick install:') + ` mindos mcp install cursor -g -y`);
167
+ console.log(dim('\nManual config (~/.cursor/mcp.json):'));
158
168
  console.log(JSON.stringify({
159
169
  mcpServers: {
160
170
  mindos: {
@@ -168,7 +178,7 @@ const commands = {
168
178
  if (localIP) {
169
179
  const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
170
180
  console.log(`\n${sep}`);
171
- console.log(`${bold('Remote (其他设备)')}`);
181
+ console.log(`${bold('Remote (other devices)')}`);
172
182
  console.log(`${sep}`);
173
183
  console.log(`URL: ${cyan(remoteUrl)}`);
174
184
  console.log(JSON.stringify({
@@ -200,12 +210,21 @@ const commands = {
200
210
  if (devMindRoot) {
201
211
  startSyncDaemon(devMindRoot).catch(() => {});
202
212
  }
203
- printStartupInfo(webPort, mcpPort);
213
+ await printStartupInfo(webPort, mcpPort);
204
214
  run(`npx next dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
205
215
  },
206
216
 
207
217
  // ── start ──────────────────────────────────────────────────────────────────
208
218
  start: async () => {
219
+ // Check for incomplete setup
220
+ if (existsSync(CONFIG_PATH)) {
221
+ try {
222
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
223
+ if (cfg.setupPending === true) {
224
+ console.log(`\n ${yellow('⚠ Setup was not completed.')} Run ${cyan('mindos onboard')} to finish, or ${cyan('mindos config set setupPending false')} to dismiss.\n`);
225
+ }
226
+ } catch {}
227
+ }
209
228
  if (isDaemon) {
210
229
  const platform = getPlatform();
211
230
  if (!platform) {
@@ -225,13 +244,13 @@ const commands = {
225
244
  console.error(dim(' Check logs with: mindos logs\n'));
226
245
  process.exit(1);
227
246
  }
228
- printStartupInfo(webPort, mcpPort);
247
+ await printStartupInfo(webPort, mcpPort);
229
248
  // System notification
230
249
  try {
231
250
  if (process.platform === 'darwin') {
232
- execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS 已就绪"'`, { stdio: 'ignore' });
251
+ execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS Ready"'`, { stdio: 'ignore' });
233
252
  } else if (process.platform === 'linux') {
234
- execSync(`notify-send "MindOS 已就绪" "http://localhost:${webPort}"`, { stdio: 'ignore' });
253
+ execSync(`notify-send "MindOS Ready" "http://localhost:${webPort}"`, { stdio: 'ignore' });
235
254
  }
236
255
  } catch { /* notification is best-effort */ }
237
256
  console.log(`${green('✔ MindOS is running as a background service')}`);
@@ -250,6 +269,7 @@ const commands = {
250
269
  if (needsBuild()) {
251
270
  console.log(yellow('Building MindOS (first run or new version detected)...\n'));
252
271
  cleanNextDir();
272
+ run('node scripts/gen-renderer-index.js', ROOT);
253
273
  run('npx next build', resolve(ROOT, 'app'));
254
274
  writeBuildStamp();
255
275
  }
@@ -261,7 +281,7 @@ const commands = {
261
281
  if (mindRoot) {
262
282
  startSyncDaemon(mindRoot).catch(() => {});
263
283
  }
264
- printStartupInfo(webPort, mcpPort);
284
+ await printStartupInfo(webPort, mcpPort);
265
285
  run(`npx next start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
266
286
  },
267
287
 
@@ -269,6 +289,7 @@ const commands = {
269
289
  build: () => {
270
290
  ensureAppDeps();
271
291
  cleanNextDir();
292
+ run('node scripts/gen-renderer-index.js', ROOT);
272
293
  run(`npx next build ${extra}`, resolve(ROOT, 'app'));
273
294
  writeBuildStamp();
274
295
  },
@@ -466,6 +487,23 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
466
487
  }
467
488
  }
468
489
 
490
+ // 9. Update check
491
+ try {
492
+ const { checkForUpdate } = await import('./lib/update-check.js');
493
+ const latestVersion = await Promise.race([
494
+ checkForUpdate(),
495
+ new Promise(r => setTimeout(() => r(null), 4000)),
496
+ ]);
497
+ if (latestVersion) {
498
+ const currentVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
499
+ warn(`Update available: v${currentVersion} → ${bold(`v${latestVersion}`)} ${dim('run `mindos update`')}`);
500
+ } else {
501
+ ok('MindOS is up to date');
502
+ }
503
+ } catch {
504
+ warn('Could not check for updates');
505
+ }
506
+
469
507
  console.log(hasError
470
508
  ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
471
509
  : `\n${green('All checks passed.')}\n`);
@@ -575,7 +613,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
575
613
  display.authToken = maskKey(display.authToken);
576
614
  if (display.webPassword)
577
615
  display.webPassword = maskKey(display.webPassword);
578
- console.log(`\n${bold('📋 MindOS Config')} ${dim(CONFIG_PATH)}\n`);
616
+ console.log(`\n${bold('📋 MindOS Config')} ${dim(`v${(() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })()}`)} ${dim(CONFIG_PATH)}\n`);
579
617
  console.log(JSON.stringify(display, null, 2));
580
618
  console.log();
581
619
  return;
@@ -640,13 +678,50 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
640
678
  if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
641
679
  obj = obj[parts[i]];
642
680
  }
643
- const coerced = isNaN(Number(val)) ? val : Number(val);
681
+ // Coerce string values to appropriate types
682
+ function coerceValue(v) {
683
+ if (v === 'true') return true;
684
+ if (v === 'false') return false;
685
+ if (v === 'null') return null;
686
+ if (v === '""' || v === "''") return '';
687
+ if (v.trim() !== '' && !isNaN(Number(v))) return Number(v);
688
+ return v;
689
+ }
690
+ const coerced = coerceValue(val);
644
691
  obj[parts[parts.length - 1]] = coerced;
645
692
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
646
693
  console.log(`${green('✔')} Set ${cyan(key)} = ${bold(String(coerced))}`);
647
694
  return;
648
695
  }
649
696
 
697
+ if (sub === 'unset') {
698
+ const key = process.argv[4];
699
+ if (!key) {
700
+ console.error(red('Usage: mindos config unset <key>'));
701
+ process.exit(1);
702
+ }
703
+ if (!existsSync(CONFIG_PATH)) {
704
+ console.error(red('No config found. Run `mindos onboard` first.'));
705
+ process.exit(1);
706
+ }
707
+ let config;
708
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {
709
+ console.error(red('Failed to parse config file.'));
710
+ process.exit(1);
711
+ }
712
+ const parts = key.split('.');
713
+ let obj = config;
714
+ for (let i = 0; i < parts.length - 1; i++) {
715
+ if (!obj[parts[i]]) { console.log(dim(`Key "${key}" not found`)); return; }
716
+ obj = obj[parts[i]];
717
+ }
718
+ if (!(parts[parts.length - 1] in obj)) { console.log(dim(`Key "${key}" not found`)); return; }
719
+ delete obj[parts[parts.length - 1]];
720
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
721
+ console.log(`${green('✔')} Removed ${cyan(key)}`);
722
+ return;
723
+ }
724
+
650
725
  // no subcommand or unknown → show help
651
726
  const row = (c, d) => ` ${cyan(c.padEnd(32))}${dim(d)}`;
652
727
  console.log(`
@@ -656,10 +731,13 @@ ${bold('Subcommands:')}
656
731
  ${row('mindos config show', 'Print current config (API keys masked)')}
657
732
  ${row('mindos config validate', 'Validate config file')}
658
733
  ${row('mindos config set <key> <v>', 'Update a single field (dot-notation supported)')}
734
+ ${row('mindos config unset <key>', 'Remove a config field')}
659
735
 
660
736
  ${bold('Examples:')}
661
737
  ${dim('mindos config set port 3002')}
662
738
  ${dim('mindos config set ai.provider openai')}
739
+ ${dim('mindos config set setupPending false')}
740
+ ${dim('mindos config unset webPassword')}
663
741
  `);
664
742
  },
665
743
 
@@ -670,7 +748,22 @@ ${bold('Examples:')}
670
748
  const mindRoot = process.env.MIND_ROOT;
671
749
 
672
750
  if (sub === 'init') {
673
- await initSync(mindRoot);
751
+ // Parse --non-interactive --remote <url> --branch <branch> --token <token>
752
+ const args = process.argv.slice(4);
753
+ const flagIdx = (flag) => args.indexOf(flag);
754
+ const flagVal = (flag) => { const i = flagIdx(flag); return i >= 0 && i + 1 < args.length ? args[i + 1] : ''; };
755
+ const nonInteractive = args.includes('--non-interactive');
756
+
757
+ if (nonInteractive) {
758
+ await initSync(mindRoot, {
759
+ nonInteractive: true,
760
+ remote: flagVal('--remote'),
761
+ token: flagVal('--token'),
762
+ branch: flagVal('--branch') || 'main',
763
+ });
764
+ } else {
765
+ await initSync(mindRoot);
766
+ }
674
767
  return;
675
768
  }
676
769
 
@@ -695,6 +788,16 @@ ${bold('Examples:')}
695
788
  return;
696
789
  }
697
790
 
791
+ // Unknown subcommand check
792
+ if (sub) {
793
+ const validSubs = ['init', 'now', 'conflicts', 'on', 'off'];
794
+ if (!validSubs.includes(sub)) {
795
+ console.error(red(`Unknown sync subcommand: ${sub}`));
796
+ console.error(dim(`Available: ${validSubs.join(' | ')}`));
797
+ process.exit(1);
798
+ }
799
+ }
800
+
698
801
  // default: sync status
699
802
  const status = getSyncStatus(mindRoot);
700
803
  if (!status.enabled) {
@@ -727,15 +830,16 @@ ${bold('Examples:')}
727
830
 
728
831
  // ── Entry ─────────────────────────────────────────────────────────────────────
729
832
 
730
- const resolvedCmd = cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null);
833
+ const resolvedCmd = (cmd === '--help' || cmd === '-h') ? null : (cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null));
731
834
 
732
835
  if (!resolvedCmd || !commands[resolvedCmd]) {
836
+ const pkgVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
733
837
  const row = (c, d) => ` ${cyan(c.padEnd(36))}${dim(d)}`;
734
838
  console.log(`
735
- ${bold('🧠 MindOS CLI')}
839
+ ${bold('🧠 MindOS CLI')} ${dim(`v${pkgVersion}`)}
736
840
 
737
- ${bold('Usage:')}
738
- ${row('mindos onboard', 'Interactive setup (writes ~/.mindos/config.json)')}
841
+ ${bold('Core:')}
842
+ ${row('mindos onboard', 'Interactive setup (aliases: init, setup)')}
739
843
  ${row('mindos onboard --install-daemon', 'Setup + install & start as background OS service')}
740
844
  ${row('mindos start', 'Start app + MCP server (production, auto-rebuilds if needed)')}
741
845
  ${row('mindos start --daemon', 'Install + start as background OS service (survives terminal close)')}
@@ -745,19 +849,28 @@ ${row('mindos dev --turbopack', 'Start with Turbopack (faster HMR)')}
745
849
  ${row('mindos stop', 'Stop running MindOS processes')}
746
850
  ${row('mindos restart', 'Stop then start again')}
747
851
  ${row('mindos build', 'Build the app for production')}
852
+ ${row('mindos open', 'Open Web UI in the default browser')}
853
+
854
+ ${bold('MCP:')}
748
855
  ${row('mindos mcp', 'Start MCP server only')}
749
856
  ${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
750
- ${row('mindos open', 'Open Web UI in the default browser')}
751
857
  ${row('mindos token', 'Show current auth token and MCP config snippet')}
858
+
859
+ ${bold('Sync:')}
752
860
  ${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
861
+
862
+ ${bold('Gateway (Background Service):')}
753
863
  ${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
864
+
865
+ ${bold('Config & Diagnostics:')}
866
+ ${row('mindos config <subcommand>', 'View/update config (show/validate/set/unset)')}
754
867
  ${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
755
868
  ${row('mindos update', 'Update MindOS to the latest version')}
756
869
  ${row('mindos logs', 'Tail service logs (~/.mindos/mindos.log)')}
757
- ${row('mindos config <subcommand>', 'View/update config (show/validate/set)')}
758
870
  ${row('mindos', 'Start using mode saved in ~/.mindos/config.json')}
759
871
  `);
760
- process.exit(cmd ? 1 : 0);
872
+ const isHelp = (cmd === '--help' || cmd === '-h');
873
+ process.exit((cmd && !isHelp) ? 1 : 0);
761
874
  }
762
875
 
763
876
  commands[resolvedCmd]();
package/bin/lib/build.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
  import { resolve } from 'node:path';
4
- import { ROOT, BUILD_STAMP } from './constants.js';
5
+ import { ROOT, BUILD_STAMP, DEPS_STAMP } from './constants.js';
5
6
  import { red, dim, yellow } from './colors.js';
6
7
  import { run } from './utils.js';
7
8
 
@@ -36,24 +37,58 @@ export function cleanNextDir() {
36
37
  }
37
38
  }
38
39
 
40
+ function depsHash() {
41
+ const lockPath = resolve(ROOT, 'app', 'package-lock.json');
42
+ try {
43
+ const content = readFileSync(lockPath);
44
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function depsChanged() {
51
+ const currentHash = depsHash();
52
+ if (!currentHash) return true;
53
+ try {
54
+ const savedHash = readFileSync(DEPS_STAMP, 'utf-8').trim();
55
+ return savedHash !== currentHash;
56
+ } catch {
57
+ return true;
58
+ }
59
+ }
60
+
61
+ function writeDepsStamp() {
62
+ const hash = depsHash();
63
+ if (hash) {
64
+ try { writeFileSync(DEPS_STAMP, hash, 'utf-8'); } catch {}
65
+ }
66
+ }
67
+
39
68
  export function ensureAppDeps() {
40
69
  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'));
70
+ const needsInstall = !existsSync(appNext) || depsChanged();
71
+ if (!needsInstall) return;
72
+
73
+ try {
74
+ execSync('npm --version', { stdio: 'pipe' });
75
+ } catch {
76
+ console.error(red('\n\u2718 npm not found in PATH.\n'));
77
+ console.error(' MindOS needs npm to install its app dependencies on first run.');
78
+ console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
79
+ console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
80
+ console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
81
+ console.error(' Example:');
82
+ console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
83
+ console.error(dim(' source ~/.profile\n'));
84
+ console.error(' Then run `mindos start` again.\n');
85
+ process.exit(1);
58
86
  }
87
+
88
+ const label = existsSync(appNext)
89
+ ? 'Updating app dependencies (package-lock.json changed)...\n'
90
+ : 'Installing app dependencies (first run)...\n';
91
+ console.log(yellow(label));
92
+ run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
93
+ writeDepsStamp();
59
94
  }
package/bin/lib/colors.js CHANGED
@@ -1,4 +1,6 @@
1
- export const isTTY = process.stdout.isTTY;
1
+ const noColor = 'NO_COLOR' in process.env;
2
+ const forceColor = process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
3
+ export const isTTY = noColor ? false : (forceColor || process.stdout.isTTY);
2
4
  export const bold = (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s;
3
5
  export const dim = (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s;
4
6
  export const cyan = (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s;
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
  }