@geminilight/mindos 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +17 -2
- 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 +4 -4
- 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 +178 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -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 +24 -0
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -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
|
+
}
|
|
@@ -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 });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { gt } from 'semver';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
// Read version from package.json (not process.env.npm_package_version — unavailable in daemon mode)
|
|
8
|
+
let current = '0.0.0';
|
|
9
|
+
try {
|
|
10
|
+
const pkgPath = resolve(process.cwd(), '..', 'package.json');
|
|
11
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
12
|
+
current = pkg.version;
|
|
13
|
+
} catch {}
|
|
14
|
+
|
|
15
|
+
// npm registry sources: prefer China mirror, fallback to official
|
|
16
|
+
const REGISTRIES = [
|
|
17
|
+
'https://registry.npmmirror.com/@geminilight/mindos/latest',
|
|
18
|
+
'https://registry.npmjs.org/@geminilight/mindos/latest',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export async function GET() {
|
|
22
|
+
let latest = current;
|
|
23
|
+
|
|
24
|
+
for (const url of REGISTRIES) {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
signal: AbortSignal.timeout(3000),
|
|
28
|
+
next: { revalidate: 300 }, // 5-minute ISR cache
|
|
29
|
+
});
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
latest = data.version;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let hasUpdate = false;
|
|
41
|
+
try {
|
|
42
|
+
hasUpdate = gt(latest, current);
|
|
43
|
+
} catch {
|
|
44
|
+
// Invalid version string from registry — treat as no update
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
current,
|
|
49
|
+
latest,
|
|
50
|
+
hasUpdate,
|
|
51
|
+
});
|
|
52
|
+
}
|
package/app/app/globals.css
CHANGED
|
@@ -119,6 +119,9 @@ body {
|
|
|
119
119
|
body { @apply bg-background text-foreground; }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* Utility: IBM Plex Mono display heading font */
|
|
123
|
+
.font-display { font-family: 'IBM Plex Mono', var(--font-ibm-plex-mono), monospace; }
|
|
124
|
+
|
|
122
125
|
/* Content width — controlled by Settings > Appearance */
|
|
123
126
|
:root { --content-width: 780px; }
|
|
124
127
|
.content-width { max-width: var(--content-width-override, var(--content-width)); margin-left: auto; margin-right: auto; }
|
|
@@ -287,6 +290,15 @@ body {
|
|
|
287
290
|
button, a { -webkit-tap-highlight-color: transparent; }
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
/* Global focus-visible ring for interactive elements */
|
|
294
|
+
button:focus-visible,
|
|
295
|
+
a:focus-visible,
|
|
296
|
+
[role="button"]:focus-visible {
|
|
297
|
+
outline: 2px solid var(--amber);
|
|
298
|
+
outline-offset: 2px;
|
|
299
|
+
border-radius: 4px;
|
|
300
|
+
}
|
|
301
|
+
|
|
290
302
|
/* ─── Tiptap WYSIWYG editor ─────────────────────────────────────────── */
|
|
291
303
|
.wysiwyg-wrapper {
|
|
292
304
|
height: 100%;
|
package/app/app/layout.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
|
|
7
7
|
import { LocaleProvider } from '@/lib/LocaleContext';
|
|
8
8
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
9
9
|
import RegisterSW from './register-sw';
|
|
10
|
+
import UpdateBanner from '@/components/UpdateBanner';
|
|
10
11
|
|
|
11
12
|
const geistSans = Geist({
|
|
12
13
|
variable: '--font-geist-sans',
|
|
@@ -20,13 +21,13 @@ const geistMono = Geist_Mono({
|
|
|
20
21
|
|
|
21
22
|
const ibmPlexMono = IBM_Plex_Mono({
|
|
22
23
|
variable: '--font-ibm-plex-mono',
|
|
23
|
-
weight: ['400', '
|
|
24
|
+
weight: ['400', '600'],
|
|
24
25
|
subsets: ['latin'],
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
const ibmPlexSans = IBM_Plex_Sans({
|
|
28
29
|
variable: '--font-ibm-plex-sans',
|
|
29
|
-
weight: ['
|
|
30
|
+
weight: ['400', '500', '600'],
|
|
30
31
|
subsets: ['latin'],
|
|
31
32
|
});
|
|
32
33
|
|
|
@@ -91,6 +92,7 @@ export default function RootLayout({
|
|
|
91
92
|
suppressHydrationWarning
|
|
92
93
|
>
|
|
93
94
|
<LocaleProvider>
|
|
95
|
+
<UpdateBanner />
|
|
94
96
|
<TooltipProvider delay={300}>
|
|
95
97
|
<ErrorBoundary>
|
|
96
98
|
<ShellLayout fileTree={fileTree}>
|