@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.
@@ -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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true });
108
+ resp = NextResponse.json({ ok: true });
109
+ break;
98
110
  }
99
111
 
100
112
  case 'delete_file': {
101
113
  deleteFile(filePath);
102
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true, newPath });
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
- return NextResponse.json({ ok: true });
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
- return NextResponse.json({ ok: true, ...result });
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
- return NextResponse.json({ ok: true, ...result });
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
- const frontmatter = `---\nname: ${name}\ndescription: ${description || name}\n---\n\n${content || ''}`;
176
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), frontmatter, 'utf-8');
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); }