@geminilight/mindos 0.1.2 → 0.1.4

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.
@@ -1,9 +1,8 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { Geist, Geist_Mono, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
3
- import { headers } from 'next/headers';
4
3
  import './globals.css';
5
4
  import { getFileTree } from '@/lib/fs';
6
- import SidebarLayout from '@/components/SidebarLayout';
5
+ import ShellLayout from '@/components/ShellLayout';
7
6
  import { TooltipProvider } from '@/components/ui/tooltip';
8
7
  import { LocaleProvider } from '@/lib/LocaleContext';
9
8
  import ErrorBoundary from '@/components/ErrorBoundary';
@@ -49,7 +48,7 @@ export const viewport = {
49
48
  viewportFit: 'cover' as const,
50
49
  };
51
50
 
52
- export default async function RootLayout({
51
+ export default function RootLayout({
53
52
  children,
54
53
  }: Readonly<{
55
54
  children: React.ReactNode;
@@ -61,9 +60,6 @@ export default async function RootLayout({
61
60
  console.error('[RootLayout] Failed to load file tree:', err);
62
61
  }
63
62
 
64
- const headersList = await headers();
65
- const isLoginPage = headersList.get('x-pathname') === '/login';
66
-
67
63
  return (
68
64
  <html lang="en" suppressHydrationWarning>
69
65
  <head>
@@ -89,11 +85,9 @@ export default async function RootLayout({
89
85
  <LocaleProvider>
90
86
  <TooltipProvider delay={300}>
91
87
  <ErrorBoundary>
92
- {isLoginPage ? children : (
93
- <SidebarLayout fileTree={fileTree}>
94
- {children}
95
- </SidebarLayout>
96
- )}
88
+ <ShellLayout fileTree={fileTree}>
89
+ {children}
90
+ </ShellLayout>
97
91
  </ErrorBoundary>
98
92
  </TooltipProvider>
99
93
  </LocaleProvider>
@@ -55,7 +55,7 @@ export default function ViewPageClient({
55
55
  },
56
56
  () => {
57
57
  const saved = localStorage.getItem('mindos-use-raw');
58
- return saved !== null ? saved === 'true' : true;
58
+ return saved !== null ? saved === 'true' : false;
59
59
  },
60
60
  () => false,
61
61
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
3
+ import { useState, useSyncExternalStore, useMemo } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
6
6
  import Breadcrumb from '@/components/Breadcrumb';
@@ -33,16 +33,22 @@ function countFiles(node: FileNode): number {
33
33
  const DIR_VIEW_KEY = 'mindos-dir-view';
34
34
 
35
35
  function useDirViewPref() {
36
- const [view, setViewState] = useState<'grid' | 'list'>('grid');
37
-
38
- useEffect(() => {
39
- const saved = localStorage.getItem(DIR_VIEW_KEY);
40
- if (saved === 'list' || saved === 'grid') setViewState(saved);
41
- }, []);
36
+ const view = useSyncExternalStore(
37
+ (onStoreChange) => {
38
+ const listener = () => onStoreChange();
39
+ window.addEventListener('mindos-dir-view-change', listener);
40
+ return () => window.removeEventListener('mindos-dir-view-change', listener);
41
+ },
42
+ () => {
43
+ const saved = localStorage.getItem(DIR_VIEW_KEY);
44
+ return (saved === 'list' || saved === 'grid') ? saved : 'grid';
45
+ },
46
+ () => 'grid' as const,
47
+ );
42
48
 
43
49
  const setView = (v: 'grid' | 'list') => {
44
- setViewState(v);
45
50
  localStorage.setItem(DIR_VIEW_KEY, v);
51
+ window.dispatchEvent(new Event('mindos-dir-view-change'));
46
52
  };
47
53
 
48
54
  return [view, setView] as const;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import SidebarLayout from './SidebarLayout';
5
+ import { FileNode } from '@/lib/types';
6
+
7
+ interface ShellLayoutProps {
8
+ fileTree: FileNode[];
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export default function ShellLayout({ fileTree, children }: ShellLayoutProps) {
13
+ const pathname = usePathname();
14
+ if (pathname === '/login') return <>{children}</>;
15
+ return <SidebarLayout fileTree={fileTree}>{children}</SidebarLayout>;
16
+ }
@@ -1,25 +1,27 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useSyncExternalStore } from 'react';
4
4
  import { Sun, Moon } from 'lucide-react';
5
5
 
6
6
  export default function ThemeToggle() {
7
- const [dark, setDark] = useState(true);
8
-
9
- useEffect(() => {
10
- const stored = localStorage.getItem('theme');
11
- const isDark = stored
12
- ? stored === 'dark'
13
- : window.matchMedia('(prefers-color-scheme: dark)').matches;
14
- setDark(isDark);
15
- document.documentElement.classList.toggle('dark', isDark);
16
- }, []);
7
+ const dark = useSyncExternalStore(
8
+ (onStoreChange) => {
9
+ const listener = () => onStoreChange();
10
+ window.addEventListener('mindos-theme-change', listener);
11
+ return () => window.removeEventListener('mindos-theme-change', listener);
12
+ },
13
+ () => {
14
+ const stored = localStorage.getItem('theme');
15
+ return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
16
+ },
17
+ () => true,
18
+ );
17
19
 
18
20
  const toggle = () => {
19
21
  const next = !dark;
20
- setDark(next);
21
22
  document.documentElement.classList.toggle('dark', next);
22
23
  localStorage.setItem('theme', next ? 'dark' : 'light');
24
+ window.dispatchEvent(new Event('mindos-theme-change'));
23
25
  };
24
26
 
25
27
  return (
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useState, useSyncExternalStore } from 'react';
4
4
  import { Copy, Check, RefreshCw, Trash2 } from 'lucide-react';
5
5
  import type { SettingsData } from './types';
6
6
  import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
@@ -16,6 +16,12 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
16
16
  const env = data.envOverrides ?? {};
17
17
  const k = t.settings.knowledge;
18
18
 
19
+ const origin = useSyncExternalStore(
20
+ () => () => {},
21
+ () => `${window.location.protocol}//${window.location.hostname}`,
22
+ () => 'http://localhost',
23
+ );
24
+
19
25
  const [showPassword, setShowPassword] = useState(false);
20
26
  const isPasswordMasked = data.webPassword === '***set***';
21
27
 
@@ -118,7 +124,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
118
124
  {k.authTokenMcpPort}: <code className="font-mono">{data.mcpPort}</code>
119
125
  {displayToken && (
120
126
  <> &nbsp;·&nbsp; MCP URL: <code className="font-mono select-all">
121
- {typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:${data.mcpPort}/mcp` : `http://localhost:${data.mcpPort}/mcp`}
127
+ {`${origin}:${data.mcpPort}/mcp`}
122
128
  </code></>
123
129
  )}
124
130
  </p>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
3
+ import { createContext, useContext, useSyncExternalStore, ReactNode } from 'react';
4
4
  import { Locale, messages, Messages } from './i18n';
5
5
 
6
6
  interface LocaleContextValue {
@@ -15,17 +15,25 @@ const LocaleContext = createContext<LocaleContextValue>({
15
15
  t: messages['en'],
16
16
  });
17
17
 
18
- export function LocaleProvider({ children }: { children: ReactNode }) {
19
- const [locale, setLocaleState] = useState<Locale>('en');
18
+ function getLocaleSnapshot(): Locale {
19
+ const saved = localStorage.getItem('locale');
20
+ return saved === 'zh' ? 'zh' : 'en';
21
+ }
20
22
 
21
- useEffect(() => {
22
- const saved = localStorage.getItem('locale') as Locale | null;
23
- if (saved === 'zh' || saved === 'en') setLocaleState(saved);
24
- }, []);
23
+ export function LocaleProvider({ children }: { children: ReactNode }) {
24
+ const locale = useSyncExternalStore(
25
+ (onStoreChange) => {
26
+ const listener = () => onStoreChange();
27
+ window.addEventListener('mindos-locale-change', listener);
28
+ return () => window.removeEventListener('mindos-locale-change', listener);
29
+ },
30
+ getLocaleSnapshot,
31
+ () => 'en' as Locale,
32
+ );
25
33
 
26
34
  const setLocale = (l: Locale) => {
27
- setLocaleState(l);
28
35
  localStorage.setItem('locale', l);
36
+ window.dispatchEvent(new Event('mindos-locale-change'));
29
37
  };
30
38
 
31
39
  return (
package/bin/cli.js CHANGED
@@ -131,6 +131,21 @@ function ensureAppDeps() {
131
131
  // next (and other deps) must be resolvable from app/ for Turbopack to work.
132
132
  const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
133
133
  if (!existsSync(appNext)) {
134
+ // Check npm is accessible before trying to run it.
135
+ try {
136
+ execSync('npm --version', { stdio: 'pipe' });
137
+ } catch {
138
+ console.error(red('\n✘ npm not found in PATH.\n'));
139
+ console.error(' MindOS needs npm to install its app dependencies on first run.');
140
+ console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
141
+ console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
142
+ console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
143
+ console.error(' Example:');
144
+ console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
145
+ console.error(dim(' source ~/.profile\n'));
146
+ console.error(' Then run `mindos start` again.\n');
147
+ process.exit(1);
148
+ }
134
149
  console.log(yellow('Installing app dependencies (first run)...\n'));
135
150
  // --no-workspaces: prevent npm from hoisting deps to monorepo root.
136
151
  // When globally installed, deps must live in app/node_modules/ so that
@@ -225,6 +240,7 @@ const systemd = {
225
240
  install() {
226
241
  if (!existsSync(SYSTEMD_DIR)) mkdirSync(SYSTEMD_DIR, { recursive: true });
227
242
  ensureMindosDir();
243
+ const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
228
244
  const unit = [
229
245
  '[Unit]',
230
246
  'Description=MindOS app + MCP server',
@@ -236,6 +252,7 @@ const systemd = {
236
252
  'Restart=on-failure',
237
253
  'RestartSec=3',
238
254
  `Environment=HOME=${homedir()}`,
255
+ `Environment=PATH=${currentPath}`,
239
256
  `EnvironmentFile=-${resolve(MINDOS_DIR, 'env')}`,
240
257
  `StandardOutput=append:${LOG_PATH}`,
241
258
  `StandardError=append:${LOG_PATH}`,
@@ -309,6 +326,9 @@ const launchd = {
309
326
  install() {
310
327
  if (!existsSync(LAUNCHD_DIR)) mkdirSync(LAUNCHD_DIR, { recursive: true });
311
328
  ensureMindosDir();
329
+ // Capture current PATH so the daemon can find npm/node even when launched by
330
+ // launchd (which only sets a minimal PATH and doesn't source shell profiles).
331
+ const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
312
332
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
313
333
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
314
334
  <plist version="1.0">
@@ -327,6 +347,7 @@ const launchd = {
327
347
  <key>EnvironmentVariables</key>
328
348
  <dict>
329
349
  <key>HOME</key><string>${homedir()}</string>
350
+ <key>PATH</key><string>${currentPath}</string>
330
351
  </dict>
331
352
  </dict>
332
353
  </plist>
@@ -334,8 +355,15 @@ const launchd = {
334
355
  writeFileSync(LAUNCHD_PLIST, plist, 'utf-8');
335
356
  console.log(green(`✔ Wrote ${LAUNCHD_PLIST}`));
336
357
  try {
337
- execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
338
- } catch { /* already bootstrapped */ }
358
+ execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'pipe' });
359
+ } catch (e) {
360
+ const msg = e.stderr?.toString() ?? e.message ?? '';
361
+ // Error 5 (ENOENT / already loaded) is benign — service is already bootstrapped
362
+ if (!msg.includes('5:') && !msg.includes('already')) {
363
+ console.error(yellow(` ⚠ launchctl bootstrap: ${msg.trim()}`));
364
+ console.error(dim(' If this persists, try: launchctl bootout gui/$(id -u)/com.mindos.app'));
365
+ }
366
+ }
339
367
  console.log(green('✔ Service installed'));
340
368
  },
341
369
 
@@ -397,6 +425,27 @@ async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
397
425
  return check();
398
426
  }
399
427
 
428
+ async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
429
+ process.stdout.write(cyan(` Waiting for ${label} to be ready`));
430
+ for (let i = 0; i < retries; i++) {
431
+ try {
432
+ const { request } = await import('node:http');
433
+ const ok = await new Promise((resolve) => {
434
+ const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
435
+ (res) => { res.resume(); resolve(res.statusCode < 500); });
436
+ req.on('error', () => resolve(false));
437
+ req.on('timeout', () => { req.destroy(); resolve(false); });
438
+ req.end();
439
+ });
440
+ if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
441
+ } catch { /* not ready yet */ }
442
+ process.stdout.write('.');
443
+ await new Promise(r => setTimeout(r, intervalMs));
444
+ }
445
+ process.stdout.write(` ${red('✘')}\n`);
446
+ return false;
447
+ }
448
+
400
449
  async function runGatewayCommand(sub) {
401
450
  const platform = getPlatform();
402
451
  if (!platform) {
@@ -551,6 +600,14 @@ const commands = {
551
600
  console.log(cyan(`Installing MindOS as a background service (${platform})...`));
552
601
  await runGatewayCommand('install');
553
602
  await runGatewayCommand('start');
603
+ console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
604
+ console.log(dim(' Follow live progress with: mindos logs\n'));
605
+ const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
606
+ if (!ready) {
607
+ console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
608
+ console.error(dim(' Check logs with: mindos logs\n'));
609
+ process.exit(1);
610
+ }
554
611
  printStartupInfo(webPort, mcpPort);
555
612
  console.log(`${green('✔ MindOS is running as a background service')}`);
556
613
  console.log(dim(' View logs: mindos logs'));
@@ -686,6 +743,17 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
686
743
  ok(`Node.js ${nodeVersion}`);
687
744
  }
688
745
 
746
+ // 4b. npm reachable from /bin/sh
747
+ try {
748
+ const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
749
+ ok(`npm ${npmVersion} reachable`);
750
+ } catch {
751
+ err('npm not found in PATH — app dependencies cannot be installed');
752
+ console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
753
+ console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
754
+ hasError = true;
755
+ }
756
+
689
757
  // 5. Build
690
758
  if (!existsSync(resolve(ROOT, 'app', '.next'))) {
691
759
  warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
@@ -736,7 +804,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
736
804
  },
737
805
 
738
806
  // ── update ─────────────────────────────────────────────────────────────────
739
- update: () => {
807
+ update: async () => {
740
808
  const currentVersion = (() => {
741
809
  try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
742
810
  })();
@@ -754,9 +822,35 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
754
822
  })();
755
823
  if (newVersion !== currentVersion) {
756
824
  console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
757
- console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
758
825
  } else {
759
826
  console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
827
+ return;
828
+ }
829
+
830
+ // If daemon is running, restart it so the new version takes effect immediately
831
+ const platform = getPlatform();
832
+ let daemonRunning = false;
833
+ if (platform === 'systemd') {
834
+ try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
835
+ } else if (platform === 'launchd') {
836
+ try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
837
+ }
838
+
839
+ if (daemonRunning) {
840
+ console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
841
+ await runGatewayCommand('stop');
842
+ await runGatewayCommand('start');
843
+ const webPort = process.env.MINDOS_WEB_PORT || '3000';
844
+ console.log(dim(' (Waiting for Web UI to come back up...)'));
845
+ const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
846
+ if (ready) {
847
+ console.log(green('✔ MindOS restarted and ready.\n'));
848
+ } else {
849
+ console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
850
+ process.exit(1);
851
+ }
852
+ } else {
853
+ console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
760
854
  }
761
855
  },
762
856
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",