@geminilight/mindos 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ export async function GET() {
5
+ return NextResponse.json({ ok: true, service: 'mindos' });
6
+ }
@@ -1,54 +1,6 @@
1
1
  export const dynamic = 'force-dynamic';
2
2
  import { NextResponse } from 'next/server';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import os from 'os';
6
-
7
- function expandHome(p: string): string {
8
- return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
9
- }
10
-
11
- interface AgentDef {
12
- name: string;
13
- project: string | null;
14
- global: string;
15
- key: string;
16
- }
17
-
18
- const MCP_AGENTS: Record<string, AgentDef> = {
19
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
20
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
21
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
22
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
23
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
24
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
25
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
26
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
27
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
28
- };
29
-
30
- function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
31
- const agent = MCP_AGENTS[agentKey];
32
- if (!agent) return { installed: false };
33
-
34
- // Check global first, then project
35
- for (const [scope, cfgPath] of [['global', agent.global], ['project', agent.project]] as const) {
36
- if (!cfgPath) continue;
37
- const absPath = expandHome(cfgPath);
38
- if (!fs.existsSync(absPath)) continue;
39
- try {
40
- const config = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
41
- const servers = config[agent.key];
42
- if (servers?.mindos) {
43
- const entry = servers.mindos;
44
- const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
45
- return { installed: true, scope, transport, configPath: cfgPath };
46
- }
47
- } catch { /* ignore parse errors */ }
48
- }
49
-
50
- return { installed: false };
51
- }
3
+ import { MCP_AGENTS, detectInstalled } from '@/lib/mcp-agents';
52
4
 
53
5
  export async function GET() {
54
6
  try {
@@ -2,30 +2,7 @@ export const dynamic = 'force-dynamic';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
- import os from 'os';
6
-
7
- function expandHome(p: string): string {
8
- return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
9
- }
10
-
11
- interface AgentDef {
12
- name: string;
13
- project: string | null;
14
- global: string;
15
- key: string;
16
- }
17
-
18
- const MCP_AGENTS: Record<string, AgentDef> = {
19
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
20
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
21
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
22
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
23
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
24
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
25
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
26
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
27
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
28
- };
5
+ import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
29
6
 
30
7
  interface InstallRequest {
31
8
  agents: Array<{ key: string; scope: 'project' | 'global' }>;
@@ -0,0 +1,22 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+ import { spawn } from 'node:child_process';
4
+ import { resolve } from 'node:path';
5
+
6
+ export async function POST() {
7
+ try {
8
+ // process.cwd() is the Next.js app directory; cli.js is one level up at project root/bin/
9
+ const cliPath = resolve(process.cwd(), '../bin/cli.js');
10
+ const child = spawn(process.execPath, [cliPath, 'start'], {
11
+ detached: true,
12
+ stdio: 'ignore',
13
+ env: process.env,
14
+ });
15
+ child.unref();
16
+ // Give a brief moment for the response to be sent before exiting
17
+ setTimeout(() => process.exit(0), 500);
18
+ return NextResponse.json({ ok: true });
19
+ } catch (err) {
20
+ return NextResponse.json({ error: String(err) }, { status: 500 });
21
+ }
22
+ }
@@ -0,0 +1,35 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+
7
+ function expandHome(p: string): string {
8
+ return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
9
+ }
10
+
11
+ export async function POST(req: NextRequest) {
12
+ try {
13
+ const { path } = await req.json() as { path: string };
14
+ if (!path || typeof path !== 'string') {
15
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
16
+ }
17
+ const abs = expandHome(path.trim());
18
+ const exists = existsSync(abs);
19
+ let empty = true;
20
+ let count = 0;
21
+ if (exists) {
22
+ try {
23
+ const entries = readdirSync(abs).filter(e => !e.startsWith('.'));
24
+ count = entries.length;
25
+ empty = count === 0;
26
+ } catch {
27
+ // unreadable — treat as non-empty
28
+ empty = false;
29
+ }
30
+ }
31
+ return NextResponse.json({ exists, empty, count });
32
+ } catch (err) {
33
+ return NextResponse.json({ error: String(err) }, { status: 500 });
34
+ }
35
+ }
@@ -6,16 +6,27 @@ function isPortInUse(port: number): Promise<boolean> {
6
6
  return new Promise(resolve => {
7
7
  const sock = createConnection({ port, host: '127.0.0.1' });
8
8
  const cleanup = (result: boolean) => { sock.destroy(); resolve(result); };
9
- sock.setTimeout(500, () => cleanup(false));
9
+ sock.setTimeout(500, () => cleanup(true));
10
10
  sock.once('connect', () => cleanup(true));
11
11
  sock.once('error', (err: NodeJS.ErrnoException) => {
12
- // ECONNREFUSED = port open but no listener → not in use
13
- // Other errors (EACCES, ENETUNREACH) = treat as unavailable to be safe
14
12
  resolve(err.code !== 'ECONNREFUSED');
15
13
  });
16
14
  });
17
15
  }
18
16
 
17
+ async function isSelfPort(port: number): Promise<boolean> {
18
+ try {
19
+ const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
20
+ signal: AbortSignal.timeout(800),
21
+ });
22
+ if (!res.ok) return false;
23
+ const data = await res.json() as Record<string, unknown>;
24
+ return data.service === 'mindos';
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
19
30
  async function findFreePort(start: number): Promise<number | null> {
20
31
  for (let p = start; p <= 65535; p++) {
21
32
  if (!await isPortInUse(p)) return p;
@@ -31,10 +42,15 @@ export async function POST(req: NextRequest) {
31
42
  }
32
43
  const inUse = await isPortInUse(port);
33
44
  if (!inUse) {
34
- return NextResponse.json({ available: true });
45
+ return NextResponse.json({ available: true, isSelf: false });
46
+ }
47
+ // Port is occupied — check if it's this MindOS instance
48
+ const self = await isSelfPort(port);
49
+ if (self) {
50
+ return NextResponse.json({ available: true, isSelf: true });
35
51
  }
36
52
  const suggestion = await findFreePort(port + 1);
37
- return NextResponse.json({ available: false, suggestion });
53
+ return NextResponse.json({ available: false, isSelf: false, suggestion });
38
54
  } catch (err) {
39
55
  return NextResponse.json({ error: String(err) }, { status: 500 });
40
56
  }
@@ -0,0 +1,38 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { existsSync, readdirSync, statSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { resolve, join } from 'node:path';
6
+
7
+ function expandHome(p: string): string {
8
+ if (p === '~') return homedir();
9
+ if (p.startsWith('~/') || p.startsWith('~\\')) return resolve(homedir(), p.slice(2));
10
+ return p;
11
+ }
12
+
13
+ export async function POST(req: NextRequest) {
14
+ try {
15
+ const { path } = await req.json() as { path: string };
16
+ if (!path || typeof path !== 'string') {
17
+ return NextResponse.json({ dirs: [] });
18
+ }
19
+ const abs = expandHome(path.trim());
20
+ if (!existsSync(abs)) {
21
+ return NextResponse.json({ dirs: [] });
22
+ }
23
+ try {
24
+ const entries = readdirSync(abs)
25
+ .filter(e => !e.startsWith('.'))
26
+ .filter(e => {
27
+ try { return statSync(join(abs, e)).isDirectory(); } catch { return false; }
28
+ })
29
+ .sort()
30
+ .slice(0, 20);
31
+ return NextResponse.json({ dirs: entries });
32
+ } catch {
33
+ return NextResponse.json({ dirs: [] });
34
+ }
35
+ } catch {
36
+ return NextResponse.json({ dirs: [] });
37
+ }
38
+ }
@@ -5,6 +5,40 @@ import os from 'os';
5
5
  import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
6
6
  import { applyTemplate } from '@/lib/template';
7
7
 
8
+ function maskApiKey(key: string): string {
9
+ if (!key || key.length < 6) return key ? '***' : '';
10
+ return key.slice(0, 6) + '***';
11
+ }
12
+
13
+ export async function GET() {
14
+ try {
15
+ const s = readSettings();
16
+ const home = os.homedir();
17
+ const sep = process.platform === 'win32' ? '\\' : '/';
18
+ const defaultMindRoot = s.mindRoot || [home, 'MindOS', 'mind'].join(sep);
19
+ return NextResponse.json({
20
+ mindRoot: defaultMindRoot,
21
+ homeDir: home,
22
+ platform: process.platform,
23
+ port: s.port ?? 3000,
24
+ mcpPort: s.mcpPort ?? 8787,
25
+ authToken: s.authToken ?? '',
26
+ webPassword: s.webPassword ?? '',
27
+ provider: s.ai.provider,
28
+ anthropicApiKey: maskApiKey(s.ai.providers.anthropic.apiKey),
29
+ anthropicModel: s.ai.providers.anthropic.model,
30
+ openaiApiKey: maskApiKey(s.ai.providers.openai.apiKey),
31
+ openaiModel: s.ai.providers.openai.model,
32
+ openaiBaseUrl: s.ai.providers.openai.baseUrl ?? '',
33
+ });
34
+ } catch (e) {
35
+ return NextResponse.json(
36
+ { error: e instanceof Error ? e.message : String(e) },
37
+ { status: 500 },
38
+ );
39
+ }
40
+ }
41
+
8
42
  function expandHome(p: string): string {
9
43
  if (p.startsWith('~/')) return p.replace('~', os.homedir());
10
44
  if (p === '~') return os.homedir();
@@ -53,6 +87,19 @@ export async function POST(req: NextRequest) {
53
87
  const current = readSettings();
54
88
  const currentPort = current.port ?? 3000;
55
89
 
90
+ // Use the same resolved values that will actually be written to config
91
+ const resolvedAuthToken = authToken ?? current.authToken ?? '';
92
+ const resolvedWebPassword = webPassword ?? '';
93
+ // Only compute needsRestart for re-onboard (setupPending=true means first-time setup)
94
+ const isFirstTime = current.setupPending === true || !current.mindRoot;
95
+ const needsRestart = !isFirstTime && (
96
+ webPort !== (current.port ?? 3000) ||
97
+ mcpPortNum !== (current.mcpPort ?? 8787) ||
98
+ resolvedRoot !== (current.mindRoot || '') ||
99
+ resolvedAuthToken !== (current.authToken ?? '') ||
100
+ resolvedWebPassword !== (current.webPassword ?? '')
101
+ );
102
+
56
103
  // Build config
57
104
  const config: ServerSettings = {
58
105
  ai: ai ?? current.ai,
@@ -70,6 +117,8 @@ export async function POST(req: NextRequest) {
70
117
  return NextResponse.json({
71
118
  ok: true,
72
119
  portChanged: webPort !== currentPort,
120
+ needsRestart,
121
+ newPort: webPort,
73
122
  });
74
123
  } catch (e) {
75
124
  console.error('[/api/setup] Error:', e);
@@ -4,8 +4,9 @@ import SetupWizard from '@/components/SetupWizard';
4
4
 
5
5
  export const dynamic = 'force-dynamic';
6
6
 
7
- export default function SetupPage() {
7
+ export default function SetupPage({ searchParams }: { searchParams: { force?: string } }) {
8
8
  const settings = readSettings();
9
- if (!settings.setupPending) redirect('/');
9
+ const force = searchParams.force === '1';
10
+ if (!settings.setupPending && !force) redirect('/');
10
11
  return <SetupWizard />;
11
12
  }
@@ -8,6 +8,7 @@ import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { getAllRenderers } from '@/lib/renderers/registry';
9
9
  import '@/lib/renderers/index'; // registers all renderers
10
10
  import OnboardingView from './OnboardingView';
11
+ import WelcomeBanner from './WelcomeBanner';
11
12
 
12
13
  interface RecentFile {
13
14
  path: string;
@@ -67,6 +68,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
67
68
 
68
69
  return (
69
70
  <div className="content-width px-4 md:px-6 py-8 md:py-12">
71
+ <WelcomeBanner />
70
72
  {/* Hero */}
71
73
  <div className="mb-10">
72
74
  <div className="flex items-center gap-2 mb-3">
@@ -223,6 +223,15 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
223
223
  {t.settings.ai.restoreFromEnv}
224
224
  </button>
225
225
  )}
226
+ {tab === 'knowledge' && (
227
+ <a
228
+ href="/setup?force=1"
229
+ className="flex items-center gap-1.5 px-4 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
230
+ >
231
+ <RotateCcw size={13} />
232
+ {t.settings.reconfigure}
233
+ </a>
234
+ )}
226
235
  <div className="flex items-center gap-1.5 text-xs">
227
236
  {status === 'saved' && (
228
237
  <><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{t.settings.saved}</span></>