@geminilight/mindos 0.1.1 → 0.1.3
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.
- package/README.md +3 -1
- package/README_zh.md +3 -1
- package/app/app/layout.tsx +5 -11
- package/app/app/view/[...path]/ViewPageClient.tsx +1 -1
- package/app/components/DirView.tsx +14 -8
- package/app/components/SettingsModal.tsx +7 -8
- package/app/components/ShellLayout.tsx +16 -0
- package/app/components/ThemeToggle.tsx +14 -12
- package/app/components/settings/KnowledgeTab.tsx +8 -2
- package/app/lib/LocaleContext.tsx +16 -8
- package/bin/cli.js +138 -5
- package/mcp/src/index.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,9 +122,11 @@ npm link # registers the `mindos` command globally
|
|
|
122
122
|
### 2. Interactive Setup
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
mindos onboard --install-daemon
|
|
125
|
+
mindos onboard --install-daemon
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
> `--install-daemon`: after setup, automatically installs and starts MindOS as a background OS service (survives terminal close, auto-restarts on crash).
|
|
129
|
+
|
|
128
130
|
The setup wizard will guide you through:
|
|
129
131
|
1. Knowledge base path → default `~/.mindos/my-mind`
|
|
130
132
|
2. Choose template language (en / zh)
|
package/README_zh.md
CHANGED
|
@@ -122,9 +122,11 @@ npm link # 将 mindos 命令注册为全局命令
|
|
|
122
122
|
### 2. 交互式配置
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
mindos onboard --install-daemon
|
|
125
|
+
mindos onboard --install-daemon
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
> `--install-daemon`:配置完成后,自动将 MindOS 安装为后台 OS 服务(关闭终端仍运行,崩溃自动重启)。
|
|
129
|
+
|
|
128
130
|
配置向导将引导你完成:
|
|
129
131
|
1. 知识库路径 → 默认 `~/.mindos/my-mind`
|
|
130
132
|
2. 选择模板语言(en / zh / empty)
|
package/app/app/layout.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</SidebarLayout>
|
|
96
|
-
)}
|
|
88
|
+
<ShellLayout fileTree={fileTree}>
|
|
89
|
+
{children}
|
|
90
|
+
</ShellLayout>
|
|
97
91
|
</ErrorBoundary>
|
|
98
92
|
</TooltipProvider>
|
|
99
93
|
</LocaleProvider>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
@@ -26,20 +26,19 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
|
26
26
|
const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
|
|
27
27
|
const { t, locale, setLocale } = useLocale();
|
|
28
28
|
|
|
29
|
-
// Appearance state (localStorage-based)
|
|
30
|
-
const [font, setFont] = useState('lora');
|
|
31
|
-
const [contentWidth, setContentWidth] = useState('780px');
|
|
32
|
-
const [dark, setDark] = useState(
|
|
29
|
+
// Appearance state (localStorage-based) — read directly on mount; this component is client-only
|
|
30
|
+
const [font, setFont] = useState(() => localStorage.getItem('prose-font') ?? 'lora');
|
|
31
|
+
const [contentWidth, setContentWidth] = useState(() => localStorage.getItem('content-width') ?? '780px');
|
|
32
|
+
const [dark, setDark] = useState(() => {
|
|
33
|
+
const stored = localStorage.getItem('theme');
|
|
34
|
+
return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
35
|
+
});
|
|
33
36
|
// Plugin enabled state
|
|
34
37
|
const [pluginStates, setPluginStates] = useState<Record<string, boolean>>({});
|
|
35
38
|
|
|
36
39
|
useEffect(() => {
|
|
37
40
|
if (!open) return;
|
|
38
41
|
apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('load-error'));
|
|
39
|
-
setFont(localStorage.getItem('prose-font') ?? 'lora');
|
|
40
|
-
setContentWidth(localStorage.getItem('content-width') ?? '780px');
|
|
41
|
-
const stored = localStorage.getItem('theme');
|
|
42
|
-
setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
43
42
|
loadDisabledState();
|
|
44
43
|
const initial: Record<string, boolean> = {};
|
|
45
44
|
for (const r of getAllRenderers()) initial[r.id] = isRendererEnabled(r.id);
|
|
@@ -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 {
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
4
|
import { Sun, Moon } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
export default function ThemeToggle() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
<> · MCP URL: <code className="font-mono select-all">
|
|
121
|
-
{
|
|
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,
|
|
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
|
-
|
|
19
|
-
const
|
|
18
|
+
function getLocaleSnapshot(): Locale {
|
|
19
|
+
const saved = localStorage.getItem('locale');
|
|
20
|
+
return saved === 'zh' ? 'zh' : 'en';
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
@@ -126,6 +126,34 @@ function clearBuildLock() {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function ensureAppDeps() {
|
|
130
|
+
// When installed as a global npm package, app/node_modules may not exist.
|
|
131
|
+
// next (and other deps) must be resolvable from app/ for Turbopack to work.
|
|
132
|
+
const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
|
|
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
|
+
}
|
|
149
|
+
console.log(yellow('Installing app dependencies (first run)...\n'));
|
|
150
|
+
// --no-workspaces: prevent npm from hoisting deps to monorepo root.
|
|
151
|
+
// When globally installed, deps must live in app/node_modules/ so that
|
|
152
|
+
// Turbopack can resolve next/package.json from the app/ project directory.
|
|
153
|
+
run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
// ── Port check ────────────────────────────────────────────────────────────────
|
|
130
158
|
|
|
131
159
|
function isPortInUse(port) {
|
|
@@ -237,8 +265,20 @@ const systemd = {
|
|
|
237
265
|
console.log(green('✔ Service installed and enabled'));
|
|
238
266
|
},
|
|
239
267
|
|
|
240
|
-
start() {
|
|
268
|
+
async start() {
|
|
241
269
|
execSync('systemctl --user start mindos', { stdio: 'inherit' });
|
|
270
|
+
// Wait up to 10s for the service to become active
|
|
271
|
+
const ok = await waitForService(() => {
|
|
272
|
+
try {
|
|
273
|
+
const out = execSync('systemctl --user is-active mindos', { encoding: 'utf-8' }).trim();
|
|
274
|
+
return out === 'active';
|
|
275
|
+
} catch { return false; }
|
|
276
|
+
});
|
|
277
|
+
if (!ok) {
|
|
278
|
+
console.error(red('\n✘ Service failed to start. Last log output:'));
|
|
279
|
+
try { execSync(`journalctl --user -u mindos -n 30 --no-pager`, { stdio: 'inherit' }); } catch {}
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
242
282
|
console.log(green('✔ Service started'));
|
|
243
283
|
},
|
|
244
284
|
|
|
@@ -314,8 +354,20 @@ const launchd = {
|
|
|
314
354
|
console.log(green('✔ Service installed'));
|
|
315
355
|
},
|
|
316
356
|
|
|
317
|
-
start() {
|
|
357
|
+
async start() {
|
|
318
358
|
execSync(`launchctl kickstart -k gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
|
|
359
|
+
// Wait up to 10s for the service to become active
|
|
360
|
+
const ok = await waitForService(() => {
|
|
361
|
+
try {
|
|
362
|
+
const out = execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { encoding: 'utf-8' });
|
|
363
|
+
return out.includes('state = running');
|
|
364
|
+
} catch { return false; }
|
|
365
|
+
});
|
|
366
|
+
if (!ok) {
|
|
367
|
+
console.error(red('\n✘ Service failed to start. Last log output:'));
|
|
368
|
+
try { execSync(`tail -n 30 ${LOG_PATH}`, { stdio: 'inherit' }); } catch {}
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
319
371
|
console.log(green('✔ Service started'));
|
|
320
372
|
},
|
|
321
373
|
|
|
@@ -352,6 +404,35 @@ const launchd = {
|
|
|
352
404
|
|
|
353
405
|
// ── gateway dispatcher ────────────────────────────────────────────────────────
|
|
354
406
|
|
|
407
|
+
async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
|
|
408
|
+
for (let i = 0; i < retries; i++) {
|
|
409
|
+
if (check()) return true;
|
|
410
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
411
|
+
}
|
|
412
|
+
return check();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
|
|
416
|
+
process.stdout.write(cyan(` Waiting for ${label} to be ready`));
|
|
417
|
+
for (let i = 0; i < retries; i++) {
|
|
418
|
+
try {
|
|
419
|
+
const { request } = await import('node:http');
|
|
420
|
+
const ok = await new Promise((resolve) => {
|
|
421
|
+
const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
|
|
422
|
+
(res) => { res.resume(); resolve(res.statusCode < 500); });
|
|
423
|
+
req.on('error', () => resolve(false));
|
|
424
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
425
|
+
req.end();
|
|
426
|
+
});
|
|
427
|
+
if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
|
|
428
|
+
} catch { /* not ready yet */ }
|
|
429
|
+
process.stdout.write('.');
|
|
430
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
431
|
+
}
|
|
432
|
+
process.stdout.write(` ${red('✘')}\n`);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
355
436
|
async function runGatewayCommand(sub) {
|
|
356
437
|
const platform = getPlatform();
|
|
357
438
|
if (!platform) {
|
|
@@ -396,7 +477,11 @@ function printStartupInfo(webPort, mcpPort) {
|
|
|
396
477
|
console.log(`\n${'─'.repeat(53)}`);
|
|
397
478
|
console.log(`${bold('🧠 MindOS is starting')}\n`);
|
|
398
479
|
console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
|
|
399
|
-
console.log(`
|
|
480
|
+
if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
|
|
481
|
+
console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
|
|
482
|
+
if (localIP) console.log(` ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
|
|
483
|
+
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}`));
|
|
484
|
+
console.log();
|
|
400
485
|
console.log(bold('Configure MCP in your Agent:'));
|
|
401
486
|
console.log(dim(' Local (same machine):'));
|
|
402
487
|
console.log(block('localhost'));
|
|
@@ -481,6 +566,7 @@ const commands = {
|
|
|
481
566
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
|
|
482
567
|
await assertPortFree(Number(webPort), 'web');
|
|
483
568
|
await assertPortFree(Number(mcpPort), 'mcp');
|
|
569
|
+
ensureAppDeps();
|
|
484
570
|
const mcp = spawnMcp(isVerbose);
|
|
485
571
|
savePids(process.pid, mcp.pid);
|
|
486
572
|
process.on('exit', clearPids);
|
|
@@ -501,6 +587,14 @@ const commands = {
|
|
|
501
587
|
console.log(cyan(`Installing MindOS as a background service (${platform})...`));
|
|
502
588
|
await runGatewayCommand('install');
|
|
503
589
|
await runGatewayCommand('start');
|
|
590
|
+
console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
|
|
591
|
+
console.log(dim(' Follow live progress with: mindos logs\n'));
|
|
592
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
593
|
+
if (!ready) {
|
|
594
|
+
console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
|
|
595
|
+
console.error(dim(' Check logs with: mindos logs\n'));
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
504
598
|
printStartupInfo(webPort, mcpPort);
|
|
505
599
|
console.log(`${green('✔ MindOS is running as a background service')}`);
|
|
506
600
|
console.log(dim(' View logs: mindos logs'));
|
|
@@ -514,6 +608,7 @@ const commands = {
|
|
|
514
608
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
|
|
515
609
|
await assertPortFree(Number(webPort), 'web');
|
|
516
610
|
await assertPortFree(Number(mcpPort), 'mcp');
|
|
611
|
+
ensureAppDeps();
|
|
517
612
|
if (needsBuild()) {
|
|
518
613
|
console.log(yellow('Building MindOS (first run or new version detected)...\n'));
|
|
519
614
|
clearBuildLock();
|
|
@@ -529,6 +624,7 @@ const commands = {
|
|
|
529
624
|
|
|
530
625
|
// ── build ──────────────────────────────────────────────────────────────────
|
|
531
626
|
build: () => {
|
|
627
|
+
ensureAppDeps();
|
|
532
628
|
clearBuildLock();
|
|
533
629
|
run(`npx next build ${extra}`, resolve(ROOT, 'app'));
|
|
534
630
|
writeBuildStamp();
|
|
@@ -634,6 +730,17 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
634
730
|
ok(`Node.js ${nodeVersion}`);
|
|
635
731
|
}
|
|
636
732
|
|
|
733
|
+
// 4b. npm reachable from /bin/sh
|
|
734
|
+
try {
|
|
735
|
+
const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
|
|
736
|
+
ok(`npm ${npmVersion} reachable`);
|
|
737
|
+
} catch {
|
|
738
|
+
err('npm not found in PATH — app dependencies cannot be installed');
|
|
739
|
+
console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
|
|
740
|
+
console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
|
|
741
|
+
hasError = true;
|
|
742
|
+
}
|
|
743
|
+
|
|
637
744
|
// 5. Build
|
|
638
745
|
if (!existsSync(resolve(ROOT, 'app', '.next'))) {
|
|
639
746
|
warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
|
|
@@ -684,7 +791,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
684
791
|
},
|
|
685
792
|
|
|
686
793
|
// ── update ─────────────────────────────────────────────────────────────────
|
|
687
|
-
update: () => {
|
|
794
|
+
update: async () => {
|
|
688
795
|
const currentVersion = (() => {
|
|
689
796
|
try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
|
|
690
797
|
})();
|
|
@@ -702,9 +809,35 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
702
809
|
})();
|
|
703
810
|
if (newVersion !== currentVersion) {
|
|
704
811
|
console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
|
|
705
|
-
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
706
812
|
} else {
|
|
707
813
|
console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// If daemon is running, restart it so the new version takes effect immediately
|
|
818
|
+
const platform = getPlatform();
|
|
819
|
+
let daemonRunning = false;
|
|
820
|
+
if (platform === 'systemd') {
|
|
821
|
+
try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
822
|
+
} else if (platform === 'launchd') {
|
|
823
|
+
try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (daemonRunning) {
|
|
827
|
+
console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
|
|
828
|
+
await runGatewayCommand('stop');
|
|
829
|
+
await runGatewayCommand('start');
|
|
830
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
831
|
+
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
832
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
833
|
+
if (ready) {
|
|
834
|
+
console.log(green('✔ MindOS restarted and ready.\n'));
|
|
835
|
+
} else {
|
|
836
|
+
console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
708
841
|
}
|
|
709
842
|
},
|
|
710
843
|
|
package/mcp/src/index.ts
CHANGED
|
@@ -470,7 +470,8 @@ async function main() {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
expressApp.all(MCP_ENDPOINT, async (req, res) => {
|
|
473
|
-
|
|
473
|
+
// Pass pre-parsed body: express.json() already parsed it, SDK >= 1.7 expects it as 3rd arg
|
|
474
|
+
await transport.handleRequest(req, res, req.body);
|
|
474
475
|
});
|
|
475
476
|
|
|
476
477
|
await server.connect(transport);
|
package/package.json
CHANGED