@geminilight/mindos 0.5.19 → 0.5.20
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/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/McpTab.tsx +286 -56
- package/app/lib/i18n.ts +16 -0
- package/app/next.config.ts +7 -0
- package/bin/cli.js +133 -1
- package/package.json +1 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { revalidatePath } from 'next/cache';
|
|
3
4
|
import {
|
|
4
5
|
getFileContent,
|
|
5
6
|
saveFileContent,
|
|
@@ -37,6 +38,9 @@ export async function GET(req: NextRequest) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Ops that change file tree structure (sidebar needs refresh)
|
|
42
|
+
const TREE_CHANGING_OPS = new Set(['create_file', 'delete_file', 'rename_file', 'move_file']);
|
|
43
|
+
|
|
40
44
|
// POST /api/file body: { op, path, ...params }
|
|
41
45
|
export async function POST(req: NextRequest) {
|
|
42
46
|
let body: Record<string, unknown>;
|
|
@@ -47,20 +51,24 @@ export async function POST(req: NextRequest) {
|
|
|
47
51
|
if (!filePath || typeof filePath !== 'string') return err('missing path');
|
|
48
52
|
|
|
49
53
|
try {
|
|
54
|
+
let resp: NextResponse;
|
|
55
|
+
|
|
50
56
|
switch (op) {
|
|
51
57
|
|
|
52
58
|
case 'save_file': {
|
|
53
59
|
const { content } = params as { content: string };
|
|
54
60
|
if (typeof content !== 'string') return err('missing content');
|
|
55
61
|
saveFileContent(filePath, content);
|
|
56
|
-
|
|
62
|
+
resp = NextResponse.json({ ok: true });
|
|
63
|
+
break;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
case 'append_to_file': {
|
|
60
67
|
const { content } = params as { content: string };
|
|
61
68
|
if (typeof content !== 'string') return err('missing content');
|
|
62
69
|
appendToFile(filePath, content);
|
|
63
|
-
|
|
70
|
+
resp = NextResponse.json({ ok: true });
|
|
71
|
+
break;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
case 'insert_lines': {
|
|
@@ -68,7 +76,8 @@ export async function POST(req: NextRequest) {
|
|
|
68
76
|
if (typeof after_index !== 'number') return err('missing after_index');
|
|
69
77
|
if (!Array.isArray(lines)) return err('lines must be array');
|
|
70
78
|
insertLines(filePath, after_index, lines);
|
|
71
|
-
|
|
79
|
+
resp = NextResponse.json({ ok: true });
|
|
80
|
+
break;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
case 'update_lines': {
|
|
@@ -78,7 +87,8 @@ export async function POST(req: NextRequest) {
|
|
|
78
87
|
if (start < 0 || end < 0) return err('start/end must be >= 0');
|
|
79
88
|
if (start > end) return err('start must be <= end');
|
|
80
89
|
updateLines(filePath, start, end, lines);
|
|
81
|
-
|
|
90
|
+
resp = NextResponse.json({ ok: true });
|
|
91
|
+
break;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
case 'insert_after_heading': {
|
|
@@ -86,7 +96,8 @@ export async function POST(req: NextRequest) {
|
|
|
86
96
|
if (typeof heading !== 'string') return err('missing heading');
|
|
87
97
|
if (typeof content !== 'string') return err('missing content');
|
|
88
98
|
insertAfterHeading(filePath, heading, content);
|
|
89
|
-
|
|
99
|
+
resp = NextResponse.json({ ok: true });
|
|
100
|
+
break;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
case 'update_section': {
|
|
@@ -94,44 +105,57 @@ export async function POST(req: NextRequest) {
|
|
|
94
105
|
if (typeof heading !== 'string') return err('missing heading');
|
|
95
106
|
if (typeof content !== 'string') return err('missing content');
|
|
96
107
|
updateSection(filePath, heading, content);
|
|
97
|
-
|
|
108
|
+
resp = NextResponse.json({ ok: true });
|
|
109
|
+
break;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
case 'delete_file': {
|
|
101
113
|
deleteFile(filePath);
|
|
102
|
-
|
|
114
|
+
resp = NextResponse.json({ ok: true });
|
|
115
|
+
break;
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
case 'rename_file': {
|
|
106
119
|
const { new_name } = params as { new_name: string };
|
|
107
120
|
if (typeof new_name !== 'string' || !new_name) return err('missing new_name');
|
|
108
121
|
const newPath = renameFile(filePath, new_name);
|
|
109
|
-
|
|
122
|
+
resp = NextResponse.json({ ok: true, newPath });
|
|
123
|
+
break;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
case 'create_file': {
|
|
113
127
|
const { content } = params as { content?: string };
|
|
114
128
|
createFile(filePath, typeof content === 'string' ? content : '');
|
|
115
|
-
|
|
129
|
+
resp = NextResponse.json({ ok: true });
|
|
130
|
+
break;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
case 'move_file': {
|
|
119
134
|
const { to_path } = params as { to_path: string };
|
|
120
135
|
if (typeof to_path !== 'string' || !to_path) return err('missing to_path');
|
|
121
136
|
const result = moveFile(filePath, to_path);
|
|
122
|
-
|
|
137
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
138
|
+
break;
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
case 'append_csv': {
|
|
126
142
|
const { row } = params as { row: string[] };
|
|
127
143
|
if (!Array.isArray(row) || row.length === 0) return err('row must be non-empty array');
|
|
128
144
|
const result = appendCsvRow(filePath, row);
|
|
129
|
-
|
|
145
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
146
|
+
break;
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
default:
|
|
133
150
|
return err(`unknown op: ${op}`);
|
|
134
151
|
}
|
|
152
|
+
|
|
153
|
+
// Invalidate Next.js router cache so sidebar file tree updates
|
|
154
|
+
if (TREE_CHANGING_OPS.has(op)) {
|
|
155
|
+
try { revalidatePath('/', 'layout'); } catch { /* noop in test env */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resp;
|
|
135
159
|
} catch (e) {
|
|
136
160
|
return err((e as Error).message, 500);
|
|
137
161
|
}
|
|
@@ -129,7 +129,7 @@ export async function POST(req: NextRequest) {
|
|
|
129
129
|
try {
|
|
130
130
|
const body = await req.json();
|
|
131
131
|
const { action, name, description, content, enabled } = body as {
|
|
132
|
-
action: 'create' | 'update' | 'delete' | 'toggle';
|
|
132
|
+
action: 'create' | 'update' | 'delete' | 'toggle' | 'read';
|
|
133
133
|
name?: string;
|
|
134
134
|
description?: string;
|
|
135
135
|
content?: string;
|
|
@@ -172,8 +172,11 @@ export async function POST(req: NextRequest) {
|
|
|
172
172
|
return NextResponse.json({ error: 'A skill with this name already exists' }, { status: 409 });
|
|
173
173
|
}
|
|
174
174
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// If content already has frontmatter, use it as-is; otherwise build frontmatter
|
|
176
|
+
const fileContent = content && content.trimStart().startsWith('---')
|
|
177
|
+
? content
|
|
178
|
+
: `---\nname: ${name}\ndescription: ${description || name}\n---\n\n${content || ''}`;
|
|
179
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8');
|
|
177
180
|
return NextResponse.json({ ok: true });
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -199,6 +202,22 @@ export async function POST(req: NextRequest) {
|
|
|
199
202
|
return NextResponse.json({ ok: true });
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
case 'read': {
|
|
206
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
207
|
+
const dirs = [
|
|
208
|
+
path.join(PROJECT_ROOT, 'app', 'data', 'skills', name),
|
|
209
|
+
path.join(PROJECT_ROOT, 'skills', name),
|
|
210
|
+
path.join(userSkillsDir, name),
|
|
211
|
+
];
|
|
212
|
+
for (const dir of dirs) {
|
|
213
|
+
const file = path.join(dir, 'SKILL.md');
|
|
214
|
+
if (fs.existsSync(file)) {
|
|
215
|
+
return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
default:
|
|
203
222
|
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
|
204
223
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
6
6
|
import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
|
|
7
7
|
import FileTree from './FileTree';
|
|
8
8
|
import SearchModal from './SearchModal';
|
|
@@ -45,6 +45,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
45
45
|
const [settingsTab, setSettingsTab] = useState<Tab | undefined>(undefined);
|
|
46
46
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
47
47
|
const { t } = useLocale();
|
|
48
|
+
const router = useRouter();
|
|
48
49
|
|
|
49
50
|
// Shared sync status for collapsed dot & mobile dot
|
|
50
51
|
const { status: syncStatus } = useSyncStatus();
|
|
@@ -54,6 +55,25 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
54
55
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
55
56
|
: undefined;
|
|
56
57
|
|
|
58
|
+
// Refresh file tree when tab becomes visible (catches external changes from
|
|
59
|
+
// MCP agents, CLI edits, or other browser tabs) and periodically while visible.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const onVisible = () => {
|
|
62
|
+
if (document.visibilityState === 'visible') router.refresh();
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
65
|
+
|
|
66
|
+
// Light periodic refresh every 30s while tab is visible
|
|
67
|
+
const interval = setInterval(() => {
|
|
68
|
+
if (document.visibilityState === 'visible') router.refresh();
|
|
69
|
+
}, 30_000);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
73
|
+
clearInterval(interval);
|
|
74
|
+
};
|
|
75
|
+
}, [router]);
|
|
76
|
+
|
|
57
77
|
useEffect(() => {
|
|
58
78
|
const handler = (e: KeyboardEvent) => {
|
|
59
79
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(v => !v); }
|