@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.
- package/app/app/api/init/route.ts +7 -41
- package/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/settings/route.ts +3 -0
- package/app/app/api/setup/generate-token/route.ts +23 -0
- package/app/app/api/setup/route.ts +81 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +22 -2
- package/app/app/setup/page.tsx +9 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +479 -0
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +270 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +15 -1
- package/app/lib/template.ts +45 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +117 -1
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -1,56 +1,22 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
2
|
import { getMindRoot } from '@/lib/fs';
|
|
5
|
-
|
|
6
|
-
function copyRecursive(src: string, dest: string) {
|
|
7
|
-
const stat = fs.statSync(src);
|
|
8
|
-
if (stat.isDirectory()) {
|
|
9
|
-
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
10
|
-
for (const entry of fs.readdirSync(src)) {
|
|
11
|
-
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
12
|
-
}
|
|
13
|
-
} else {
|
|
14
|
-
// Skip if file already exists
|
|
15
|
-
if (fs.existsSync(dest)) return;
|
|
16
|
-
// Ensure parent directory exists
|
|
17
|
-
const dir = path.dirname(dest);
|
|
18
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
19
|
-
fs.copyFileSync(src, dest);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
3
|
+
import { applyTemplate } from '@/lib/template';
|
|
22
4
|
|
|
23
5
|
export async function POST(req: NextRequest) {
|
|
24
6
|
try {
|
|
25
7
|
const body = await req.json();
|
|
26
8
|
const template = body.template as string;
|
|
27
9
|
|
|
28
|
-
if (!['en', 'zh', 'empty'].includes(template)) {
|
|
29
|
-
return NextResponse.json({ error: 'Invalid template' }, { status: 400 });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Resolve template source directory
|
|
33
|
-
// templates/ is at the repo root (sibling of app/)
|
|
34
|
-
const repoRoot = path.resolve(process.cwd(), '..');
|
|
35
|
-
const templateDir = path.join(repoRoot, 'templates', template);
|
|
36
|
-
|
|
37
|
-
if (!fs.existsSync(templateDir)) {
|
|
38
|
-
return NextResponse.json({ error: `Template "${template}" not found` }, { status: 404 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
10
|
const mindRoot = getMindRoot();
|
|
42
|
-
|
|
43
|
-
fs.mkdirSync(mindRoot, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
copyRecursive(templateDir, mindRoot);
|
|
11
|
+
applyTemplate(template, mindRoot);
|
|
47
12
|
|
|
48
13
|
return NextResponse.json({ ok: true, template });
|
|
49
14
|
} catch (e) {
|
|
50
15
|
console.error('[/api/init] Error:', e);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
16
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17
|
+
const status = msg.startsWith('Invalid template') ? 400
|
|
18
|
+
: msg.includes('not found') ? 404
|
|
19
|
+
: 500;
|
|
20
|
+
return NextResponse.json({ error: msg }, { status });
|
|
55
21
|
}
|
|
56
22
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
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
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function GET() {
|
|
54
|
+
try {
|
|
55
|
+
const agents = Object.entries(MCP_AGENTS).map(([key, agent]) => {
|
|
56
|
+
const status = detectInstalled(key);
|
|
57
|
+
return {
|
|
58
|
+
key,
|
|
59
|
+
name: agent.name,
|
|
60
|
+
installed: status.installed,
|
|
61
|
+
scope: status.scope,
|
|
62
|
+
transport: status.transport,
|
|
63
|
+
configPath: status.configPath,
|
|
64
|
+
hasProjectScope: !!agent.project,
|
|
65
|
+
hasGlobalScope: !!agent.global,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return NextResponse.json({ agents });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, 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
|
+
interface InstallRequest {
|
|
31
|
+
agents: Array<{ key: string; scope: 'project' | 'global' }>;
|
|
32
|
+
transport: 'stdio' | 'http';
|
|
33
|
+
url?: string;
|
|
34
|
+
token?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildEntry(transport: string, url?: string, token?: string) {
|
|
38
|
+
if (transport === 'stdio') {
|
|
39
|
+
return { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };
|
|
40
|
+
}
|
|
41
|
+
const entry: Record<string, unknown> = { url: url || 'http://localhost:8787/mcp' };
|
|
42
|
+
if (token) entry.headers = { Authorization: `Bearer ${token}` };
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function POST(req: NextRequest) {
|
|
47
|
+
try {
|
|
48
|
+
const body = (await req.json()) as InstallRequest;
|
|
49
|
+
const { agents, transport, url, token } = body;
|
|
50
|
+
const entry = buildEntry(transport, url, token);
|
|
51
|
+
const results: Array<{ agent: string; status: string; path?: string; message?: string }> = [];
|
|
52
|
+
|
|
53
|
+
for (const { key, scope } of agents) {
|
|
54
|
+
const agent = MCP_AGENTS[key];
|
|
55
|
+
if (!agent) {
|
|
56
|
+
results.push({ agent: key, status: 'error', message: `Unknown agent: ${key}` });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isGlobal = scope === 'global';
|
|
61
|
+
const configPath = isGlobal ? agent.global : agent.project;
|
|
62
|
+
if (!configPath) {
|
|
63
|
+
results.push({ agent: key, status: 'error', message: `${agent.name} does not support ${scope} scope` });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const absPath = expandHome(configPath);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Read existing config
|
|
71
|
+
let config: Record<string, unknown> = {};
|
|
72
|
+
if (fs.existsSync(absPath)) {
|
|
73
|
+
config = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Merge — only touch mcpServers.mindos
|
|
77
|
+
if (!config[agent.key]) config[agent.key] = {};
|
|
78
|
+
(config[agent.key] as Record<string, unknown>).mindos = entry;
|
|
79
|
+
|
|
80
|
+
// Write
|
|
81
|
+
const dir = path.dirname(absPath);
|
|
82
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
84
|
+
|
|
85
|
+
results.push({ agent: key, status: 'ok', path: configPath });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
results.push({ agent: key, status: 'error', message: String(err) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return NextResponse.json({ results });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { readSettings } from '@/lib/settings';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
const settings = readSettings();
|
|
8
|
+
const port = settings.mcpPort ?? 8787;
|
|
9
|
+
const endpoint = `http://127.0.0.1:${port}/mcp`;
|
|
10
|
+
const authConfigured = !!settings.authToken;
|
|
11
|
+
|
|
12
|
+
// Check if MCP server is running
|
|
13
|
+
let running = false;
|
|
14
|
+
let toolCount = 0;
|
|
15
|
+
try {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
18
|
+
const res = await fetch(endpoint, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
});
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
running = true;
|
|
27
|
+
try {
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (data?.result?.tools) toolCount = data.result.tools.length;
|
|
30
|
+
} catch { /* non-JSON response — still running */ }
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Connection refused or timeout — not running
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return NextResponse.json({
|
|
37
|
+
running,
|
|
38
|
+
transport: 'http',
|
|
39
|
+
endpoint,
|
|
40
|
+
port,
|
|
41
|
+
toolCount,
|
|
42
|
+
authConfigured,
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -112,6 +112,9 @@ export async function POST(req: NextRequest) {
|
|
|
112
112
|
mindRoot: body.mindRoot ?? current.mindRoot,
|
|
113
113
|
webPassword: resolvedWebPassword,
|
|
114
114
|
authToken: resolvedAuthToken,
|
|
115
|
+
port: typeof body.port === 'number' ? body.port : current.port,
|
|
116
|
+
mcpPort: typeof body.mcpPort === 'number' ? body.mcpPort : current.mcpPort,
|
|
117
|
+
startMode: body.startMode ?? current.startMode,
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
writeSettings(next);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { randomBytes, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
try {
|
|
7
|
+
const { seed } = await req.json().catch(() => ({} as { seed?: string }));
|
|
8
|
+
let raw: string;
|
|
9
|
+
if (seed && typeof seed === 'string' && seed.trim()) {
|
|
10
|
+
raw = createHash('sha256').update(seed.trim()).digest('hex').slice(0, 24);
|
|
11
|
+
} else {
|
|
12
|
+
raw = randomBytes(12).toString('hex'); // 24 hex chars
|
|
13
|
+
}
|
|
14
|
+
// Format as xxxx-xxxx-xxxx-xxxx-xxxx-xxxx
|
|
15
|
+
const token = raw.match(/.{4}/g)!.join('-');
|
|
16
|
+
return NextResponse.json({ token });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: e instanceof Error ? e.message : String(e) },
|
|
20
|
+
{ status: 500 },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
|
|
6
|
+
import { applyTemplate } from '@/lib/template';
|
|
7
|
+
|
|
8
|
+
function expandHome(p: string): string {
|
|
9
|
+
if (p.startsWith('~/')) return p.replace('~', os.homedir());
|
|
10
|
+
if (p === '~') return os.homedir();
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await req.json();
|
|
17
|
+
const { mindRoot, template, port, mcpPort, authToken, webPassword, ai } = body;
|
|
18
|
+
|
|
19
|
+
// Validate required fields
|
|
20
|
+
if (!mindRoot || typeof mindRoot !== 'string') {
|
|
21
|
+
return NextResponse.json({ error: 'mindRoot is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolvedRoot = expandHome(mindRoot.trim());
|
|
25
|
+
|
|
26
|
+
// Validate ports
|
|
27
|
+
const webPort = typeof port === 'number' ? port : 3000;
|
|
28
|
+
const mcpPortNum = typeof mcpPort === 'number' ? mcpPort : 8787;
|
|
29
|
+
if (webPort < 1024 || webPort > 65535) {
|
|
30
|
+
return NextResponse.json({ error: `Invalid web port: ${webPort}` }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
if (mcpPortNum < 1024 || mcpPortNum > 65535) {
|
|
33
|
+
return NextResponse.json({ error: `Invalid MCP port: ${mcpPortNum}` }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Apply template if mindRoot doesn't exist or is empty
|
|
37
|
+
const dirExists = fs.existsSync(resolvedRoot);
|
|
38
|
+
let dirEmpty = true;
|
|
39
|
+
if (dirExists) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = fs.readdirSync(resolvedRoot).filter(e => !e.startsWith('.'));
|
|
42
|
+
dirEmpty = entries.length === 0;
|
|
43
|
+
} catch { /* treat as empty */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (template && (!dirExists || dirEmpty)) {
|
|
47
|
+
applyTemplate(template, resolvedRoot);
|
|
48
|
+
} else if (!dirExists) {
|
|
49
|
+
fs.mkdirSync(resolvedRoot, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read current running port for portChanged detection
|
|
53
|
+
const current = readSettings();
|
|
54
|
+
const currentPort = current.port ?? 3000;
|
|
55
|
+
|
|
56
|
+
// Build config
|
|
57
|
+
const config: ServerSettings = {
|
|
58
|
+
ai: ai ?? current.ai,
|
|
59
|
+
mindRoot: resolvedRoot,
|
|
60
|
+
port: webPort,
|
|
61
|
+
mcpPort: mcpPortNum,
|
|
62
|
+
authToken: authToken ?? current.authToken,
|
|
63
|
+
webPassword: webPassword ?? '',
|
|
64
|
+
startMode: current.startMode,
|
|
65
|
+
setupPending: false, // clear the flag
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
writeSettings(config);
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
ok: true,
|
|
72
|
+
portChanged: webPort !== currentPort,
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('[/api/setup] Error:', e);
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: e instanceof Error ? e.message : String(e) },
|
|
78
|
+
{ status: 500 },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { readSettings, writeSettings } from '@/lib/settings';
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
|
9
|
+
|
|
10
|
+
function getMindRoot(): string {
|
|
11
|
+
const s = readSettings();
|
|
12
|
+
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SkillInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
path: string;
|
|
19
|
+
source: 'builtin' | 'user';
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
editable: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseSkillMd(content: string): { name: string; description: string } {
|
|
25
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26
|
+
if (!match) return { name: '', description: '' };
|
|
27
|
+
const yaml = match[1];
|
|
28
|
+
const nameMatch = yaml.match(/^name:\s*(.+)/m);
|
|
29
|
+
const descMatch = yaml.match(/^description:\s*>?\s*\n?([\s\S]*?)(?=\n\w|\n---)/m);
|
|
30
|
+
const name = nameMatch ? nameMatch[1].trim() : '';
|
|
31
|
+
let description = '';
|
|
32
|
+
if (descMatch) {
|
|
33
|
+
description = descMatch[1].trim().split('\n').map(l => l.trim()).join(' ').slice(0, 200);
|
|
34
|
+
} else {
|
|
35
|
+
const simpleDesc = yaml.match(/^description:\s*(.+)/m);
|
|
36
|
+
if (simpleDesc) description = simpleDesc[1].trim().slice(0, 200);
|
|
37
|
+
}
|
|
38
|
+
return { name, description };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scanSkillDirs(disabledSkills: string[]): SkillInfo[] {
|
|
42
|
+
const skills: SkillInfo[] = [];
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
|
|
45
|
+
// 1. app/data/skills/ — builtin
|
|
46
|
+
const builtinDir = path.join(PROJECT_ROOT, 'app', 'data', 'skills');
|
|
47
|
+
if (fs.existsSync(builtinDir)) {
|
|
48
|
+
for (const entry of fs.readdirSync(builtinDir, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isDirectory()) continue;
|
|
50
|
+
const skillFile = path.join(builtinDir, entry.name, 'SKILL.md');
|
|
51
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
52
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
53
|
+
const { name, description } = parseSkillMd(content);
|
|
54
|
+
const skillName = name || entry.name;
|
|
55
|
+
seen.add(skillName);
|
|
56
|
+
skills.push({
|
|
57
|
+
name: skillName,
|
|
58
|
+
description,
|
|
59
|
+
path: `app/data/skills/${entry.name}/SKILL.md`,
|
|
60
|
+
source: 'builtin',
|
|
61
|
+
enabled: !disabledSkills.includes(skillName),
|
|
62
|
+
editable: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. skills/ — project root builtin
|
|
68
|
+
const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
69
|
+
if (fs.existsSync(skillsDir)) {
|
|
70
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
71
|
+
if (!entry.isDirectory()) continue;
|
|
72
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
73
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
74
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
75
|
+
const { name, description } = parseSkillMd(content);
|
|
76
|
+
const skillName = name || entry.name;
|
|
77
|
+
if (seen.has(skillName)) continue; // already listed from app/data/skills/
|
|
78
|
+
seen.add(skillName);
|
|
79
|
+
skills.push({
|
|
80
|
+
name: skillName,
|
|
81
|
+
description,
|
|
82
|
+
path: `skills/${entry.name}/SKILL.md`,
|
|
83
|
+
source: 'builtin',
|
|
84
|
+
enabled: !disabledSkills.includes(skillName),
|
|
85
|
+
editable: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. {mindRoot}/.skills/ — user custom
|
|
91
|
+
const mindRoot = getMindRoot();
|
|
92
|
+
const userSkillsDir = path.join(mindRoot, '.skills');
|
|
93
|
+
if (fs.existsSync(userSkillsDir)) {
|
|
94
|
+
for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
|
|
95
|
+
if (!entry.isDirectory()) continue;
|
|
96
|
+
const skillFile = path.join(userSkillsDir, entry.name, 'SKILL.md');
|
|
97
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
98
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
99
|
+
const { name, description } = parseSkillMd(content);
|
|
100
|
+
const skillName = name || entry.name;
|
|
101
|
+
if (seen.has(skillName)) continue;
|
|
102
|
+
seen.add(skillName);
|
|
103
|
+
skills.push({
|
|
104
|
+
name: skillName,
|
|
105
|
+
description,
|
|
106
|
+
path: `{mindRoot}/.skills/${entry.name}/SKILL.md`,
|
|
107
|
+
source: 'user',
|
|
108
|
+
enabled: !disabledSkills.includes(skillName),
|
|
109
|
+
editable: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return skills;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function GET() {
|
|
118
|
+
try {
|
|
119
|
+
const settings = readSettings();
|
|
120
|
+
const disabledSkills = settings.disabledSkills ?? [];
|
|
121
|
+
const skills = scanSkillDirs(disabledSkills);
|
|
122
|
+
return NextResponse.json({ skills });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function POST(req: NextRequest) {
|
|
129
|
+
try {
|
|
130
|
+
const body = await req.json();
|
|
131
|
+
const { action, name, description, content, enabled } = body as {
|
|
132
|
+
action: 'create' | 'update' | 'delete' | 'toggle';
|
|
133
|
+
name?: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
content?: string;
|
|
136
|
+
enabled?: boolean;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const settings = readSettings();
|
|
140
|
+
const mindRoot = getMindRoot();
|
|
141
|
+
const userSkillsDir = path.join(mindRoot, '.skills');
|
|
142
|
+
|
|
143
|
+
// Validate skill name — prevent path traversal
|
|
144
|
+
if (name && !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
145
|
+
return NextResponse.json({ error: 'Invalid skill name. Use lowercase letters, numbers, and hyphens only.' }, { status: 400 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
switch (action) {
|
|
149
|
+
case 'toggle': {
|
|
150
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
151
|
+
const disabled = settings.disabledSkills ?? [];
|
|
152
|
+
if (enabled === false) {
|
|
153
|
+
if (!disabled.includes(name)) disabled.push(name);
|
|
154
|
+
} else {
|
|
155
|
+
const idx = disabled.indexOf(name);
|
|
156
|
+
if (idx >= 0) disabled.splice(idx, 1);
|
|
157
|
+
}
|
|
158
|
+
writeSettings({ ...settings, disabledSkills: disabled });
|
|
159
|
+
return NextResponse.json({ ok: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'create': {
|
|
163
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
164
|
+
// Check for conflicts with builtin
|
|
165
|
+
const builtinDir = path.join(PROJECT_ROOT, 'app', 'data', 'skills', name);
|
|
166
|
+
const skillsRootDir = path.join(PROJECT_ROOT, 'skills', name);
|
|
167
|
+
if (fs.existsSync(builtinDir) || fs.existsSync(skillsRootDir)) {
|
|
168
|
+
return NextResponse.json({ error: 'A built-in skill with this name already exists' }, { status: 409 });
|
|
169
|
+
}
|
|
170
|
+
const skillDir = path.join(userSkillsDir, name);
|
|
171
|
+
if (fs.existsSync(skillDir)) {
|
|
172
|
+
return NextResponse.json({ error: 'A skill with this name already exists' }, { status: 409 });
|
|
173
|
+
}
|
|
174
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
175
|
+
const frontmatter = `---\nname: ${name}\ndescription: ${description || name}\n---\n\n${content || ''}`;
|
|
176
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), frontmatter, 'utf-8');
|
|
177
|
+
return NextResponse.json({ ok: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'update': {
|
|
181
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
182
|
+
const skillDir = path.join(userSkillsDir, name);
|
|
183
|
+
if (!fs.existsSync(skillDir)) {
|
|
184
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
185
|
+
}
|
|
186
|
+
if (content !== undefined) {
|
|
187
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
return NextResponse.json({ ok: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'delete': {
|
|
193
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
194
|
+
const skillDir = path.join(userSkillsDir, name);
|
|
195
|
+
if (!fs.existsSync(skillDir)) {
|
|
196
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
197
|
+
}
|
|
198
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
199
|
+
return NextResponse.json({ ok: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
default:
|
|
203
|
+
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
3
|
+
import { execSync, execFile } from 'child_process';
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
|
|
8
8
|
const MINDOS_DIR = join(homedir(), '.mindos');
|
|
@@ -68,7 +68,7 @@ export async function GET() {
|
|
|
68
68
|
|
|
69
69
|
export async function POST(req: NextRequest) {
|
|
70
70
|
try {
|
|
71
|
-
const body = await req.json() as { action: string };
|
|
71
|
+
const body = await req.json() as { action: string; remote?: string; branch?: string; token?: string };
|
|
72
72
|
const config = loadConfig();
|
|
73
73
|
const mindRoot = config.mindRoot;
|
|
74
74
|
|
|
@@ -77,6 +77,57 @@ export async function POST(req: NextRequest) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
switch (body.action) {
|
|
80
|
+
case 'init': {
|
|
81
|
+
const remote = body.remote?.trim();
|
|
82
|
+
if (!remote) {
|
|
83
|
+
return NextResponse.json({ error: 'Remote URL is required' }, { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate URL format
|
|
87
|
+
const isHTTPS = remote.startsWith('https://');
|
|
88
|
+
const isSSH = /^git@[\w.-]+:.+/.test(remote);
|
|
89
|
+
if (!isHTTPS && !isSSH) {
|
|
90
|
+
return NextResponse.json({ error: 'Invalid remote URL — must be HTTPS or SSH format' }, { status: 400 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if sync is already configured
|
|
94
|
+
if (config.sync?.enabled && isGitRepo(mindRoot) && getRemoteUrl(mindRoot)) {
|
|
95
|
+
return NextResponse.json({ error: 'Sync already configured' }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build the effective remote URL (inject token for HTTPS)
|
|
99
|
+
let effectiveRemote = remote;
|
|
100
|
+
if (isHTTPS && body.token) {
|
|
101
|
+
try {
|
|
102
|
+
const urlObj = new URL(remote);
|
|
103
|
+
urlObj.username = 'oauth2';
|
|
104
|
+
urlObj.password = body.token;
|
|
105
|
+
effectiveRemote = urlObj.toString();
|
|
106
|
+
} catch {
|
|
107
|
+
return NextResponse.json({ error: 'Invalid remote URL' }, { status: 400 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const branch = body.branch?.trim() || 'main';
|
|
112
|
+
|
|
113
|
+
// Call CLI's sync init via execFile (avoids module resolution issues with Turbopack)
|
|
114
|
+
try {
|
|
115
|
+
const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
|
|
116
|
+
const args = ['sync', 'init', '--non-interactive', '--remote', effectiveRemote, '--branch', branch];
|
|
117
|
+
|
|
118
|
+
await new Promise<void>((res, rej) => {
|
|
119
|
+
execFile('node', [cliPath, ...args], { timeout: 30000 }, (err, stdout, stderr) => {
|
|
120
|
+
if (err) rej(new Error(stderr?.trim() || err.message));
|
|
121
|
+
else res();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return NextResponse.json({ success: true, message: 'Sync initialized' });
|
|
125
|
+
} catch (err: unknown) {
|
|
126
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
return NextResponse.json({ error: errMsg }, { status: 400 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
80
131
|
case 'now': {
|
|
81
132
|
if (!isGitRepo(mindRoot)) {
|
|
82
133
|
return NextResponse.json({ error: 'Not a git repository' }, { status: 400 });
|