@geminilight/mindos 0.5.69 → 0.6.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.
Files changed (58) hide show
  1. package/app/app/api/ask/route.ts +122 -92
  2. package/app/app/api/file/import/route.ts +197 -0
  3. package/app/app/api/mcp/agents/route.ts +53 -2
  4. package/app/app/api/mcp/status/route.ts +1 -1
  5. package/app/app/api/skills/route.ts +10 -114
  6. package/app/components/ActivityBar.tsx +5 -7
  7. package/app/components/CreateSpaceModal.tsx +31 -6
  8. package/app/components/FileTree.tsx +68 -11
  9. package/app/components/GuideCard.tsx +197 -131
  10. package/app/components/HomeContent.tsx +85 -18
  11. package/app/components/ImportModal.tsx +415 -0
  12. package/app/components/OnboardingView.tsx +9 -0
  13. package/app/components/Panel.tsx +4 -2
  14. package/app/components/SidebarLayout.tsx +96 -8
  15. package/app/components/SpaceInitToast.tsx +173 -0
  16. package/app/components/TableOfContents.tsx +1 -0
  17. package/app/components/agents/AgentDetailContent.tsx +69 -45
  18. package/app/components/agents/AgentsContentPage.tsx +2 -1
  19. package/app/components/agents/AgentsMcpSection.tsx +16 -12
  20. package/app/components/agents/AgentsOverviewSection.tsx +37 -36
  21. package/app/components/agents/AgentsPrimitives.tsx +41 -20
  22. package/app/components/agents/AgentsSkillsSection.tsx +16 -7
  23. package/app/components/agents/SkillDetailPopover.tsx +11 -11
  24. package/app/components/agents/agents-content-model.ts +16 -8
  25. package/app/components/ask/AskContent.tsx +148 -50
  26. package/app/components/ask/MentionPopover.tsx +16 -8
  27. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  28. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
  29. package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
  30. package/app/components/panels/DiscoverPanel.tsx +88 -2
  31. package/app/components/settings/KnowledgeTab.tsx +61 -0
  32. package/app/components/walkthrough/steps.ts +11 -6
  33. package/app/hooks/useFileImport.ts +191 -0
  34. package/app/hooks/useFileUpload.ts +11 -0
  35. package/app/hooks/useMention.ts +14 -6
  36. package/app/hooks/useSlashCommand.ts +114 -0
  37. package/app/lib/actions.ts +79 -2
  38. package/app/lib/agent/index.ts +1 -1
  39. package/app/lib/agent/prompt.ts +2 -0
  40. package/app/lib/agent/tools.ts +252 -0
  41. package/app/lib/core/create-space.ts +11 -4
  42. package/app/lib/core/file-convert.ts +97 -0
  43. package/app/lib/core/index.ts +1 -1
  44. package/app/lib/core/organize.ts +105 -0
  45. package/app/lib/i18n-en.ts +102 -46
  46. package/app/lib/i18n-zh.ts +101 -45
  47. package/app/lib/mcp-agents.ts +8 -0
  48. package/app/lib/pdf-extract.ts +33 -0
  49. package/app/lib/pi-integration/extensions.ts +45 -0
  50. package/app/lib/pi-integration/mcporter.ts +219 -0
  51. package/app/lib/pi-integration/session-store.ts +62 -0
  52. package/app/lib/pi-integration/skills.ts +116 -0
  53. package/app/lib/settings.ts +1 -1
  54. package/app/next-env.d.ts +1 -1
  55. package/app/next.config.ts +1 -1
  56. package/app/package.json +2 -0
  57. package/mcp/src/index.ts +29 -0
  58. package/package.json +1 -1
@@ -7,11 +7,27 @@ import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
7
7
  import { apiFetch } from '@/lib/api';
8
8
  import { copyToClipboard } from '@/lib/clipboard';
9
9
  import { formatBytes, formatUptime } from '@/lib/format';
10
+ import { setShowHiddenFiles } from '@/components/FileTree';
11
+ import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
10
12
 
11
13
  export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
12
14
  const env = data.envOverrides ?? {};
13
15
  const k = t.settings.knowledge;
14
16
 
17
+ // Hidden files toggle
18
+ const [showHidden, setShowHidden] = useState(() =>
19
+ typeof window !== 'undefined' && localStorage.getItem('show-hidden-files') === 'true'
20
+ );
21
+
22
+ // Example files cleanup
23
+ const [exampleCount, setExampleCount] = useState<number | null>(null);
24
+ const [cleaningUp, setCleaningUp] = useState(false);
25
+ const [cleanupResult, setCleanupResult] = useState<number | null>(null);
26
+
27
+ useEffect(() => {
28
+ scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch(() => {});
29
+ }, []);
30
+
15
31
  // Guide state toggle
16
32
  const [guideActive, setGuideActive] = useState<boolean | null>(null);
17
33
  const [guideDismissed, setGuideDismissed] = useState(false);
@@ -130,6 +146,51 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
130
146
  />
131
147
  </Field>
132
148
 
149
+ <div className="flex items-center justify-between">
150
+ <div>
151
+ <div className="text-sm text-foreground">{k.showHiddenFiles}</div>
152
+ <div className="text-xs text-muted-foreground mt-0.5">{k.showHiddenFilesHint}</div>
153
+ </div>
154
+ <Toggle checked={showHidden} onChange={() => {
155
+ const next = !showHidden;
156
+ setShowHidden(next);
157
+ setShowHiddenFiles(next);
158
+ }} />
159
+ </div>
160
+
161
+ {exampleCount !== null && exampleCount > 0 && cleanupResult === null && (
162
+ <div className="flex items-center justify-between">
163
+ <div>
164
+ <div className="text-sm text-foreground">{k.cleanupExamples}</div>
165
+ <div className="text-xs text-muted-foreground mt-0.5">{k.cleanupExamplesHint}</div>
166
+ </div>
167
+ <button
168
+ onClick={async () => {
169
+ if (!confirm(k.cleanupExamplesConfirm(exampleCount))) return;
170
+ setCleaningUp(true);
171
+ const r = await cleanupExamplesAction();
172
+ setCleaningUp(false);
173
+ if (r.success) {
174
+ setCleanupResult(r.deleted);
175
+ setExampleCount(0);
176
+ }
177
+ }}
178
+ disabled={cleaningUp}
179
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
180
+ >
181
+ {cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
182
+ {k.cleanupExamplesButton}
183
+ <span className="ml-1 tabular-nums text-2xs opacity-70">{exampleCount}</span>
184
+ </button>
185
+ </div>
186
+ )}
187
+ {cleanupResult !== null && (
188
+ <div className="flex items-center gap-2 text-xs text-success">
189
+ <Check size={14} />
190
+ {k.cleanupExamplesDone(cleanupResult)}
191
+ </div>
192
+ )}
193
+
133
194
  <div className="border-t border-border pt-5">
134
195
  <SectionLabel>Security</SectionLabel>
135
196
  </div>
@@ -1,10 +1,9 @@
1
1
  /** Walkthrough step anchors — these data-walkthrough attributes are added to target components */
2
2
  export type WalkthroughAnchor =
3
- | 'activity-bar'
4
3
  | 'files-panel'
5
4
  | 'ask-button'
6
- | 'search-button'
7
- | 'settings-button';
5
+ | 'agents-panel'
6
+ | 'echo-panel';
8
7
 
9
8
  export interface WalkthroughStep {
10
9
  anchor: WalkthroughAnchor;
@@ -12,10 +11,16 @@ export interface WalkthroughStep {
12
11
  position: 'right' | 'bottom';
13
12
  }
14
13
 
14
+ /**
15
+ * 4-step value-driven walkthrough aligned with the Dual-Layer Wedge strategy:
16
+ * 0. Project Memory (foundation)
17
+ * 1. AI That Already Knows You (wedge)
18
+ * 2. Multi-Agent Sharing (differentiation)
19
+ * 3. Echo — Cognitive Compound Interest (retention seed)
20
+ */
15
21
  export const walkthroughSteps: WalkthroughStep[] = [
16
- { anchor: 'activity-bar', position: 'right' },
17
22
  { anchor: 'files-panel', position: 'right' },
18
23
  { anchor: 'ask-button', position: 'right' },
19
- { anchor: 'search-button', position: 'right' },
20
- { anchor: 'settings-button', position: 'right' },
24
+ { anchor: 'agents-panel', position: 'right' },
25
+ { anchor: 'echo-panel', position: 'right' },
21
26
  ];
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
5
+
6
+ export type ImportIntent = 'archive' | 'digest';
7
+ export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
8
+ export type ConflictMode = 'skip' | 'rename' | 'overwrite';
9
+
10
+ export interface ImportFile {
11
+ file: File;
12
+ name: string;
13
+ size: number;
14
+ content: string | null;
15
+ loading: boolean;
16
+ error: string | null;
17
+ }
18
+
19
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
20
+ const MAX_PDF_SIZE = 12 * 1024 * 1024;
21
+ const MAX_FILES = 20;
22
+
23
+ function getExt(name: string): string {
24
+ const idx = name.lastIndexOf('.');
25
+ return idx >= 0 ? name.slice(idx).toLowerCase() : '';
26
+ }
27
+
28
+ function formatSize(bytes: number): string {
29
+ if (bytes < 1024) return `${bytes} B`;
30
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
31
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
32
+ }
33
+
34
+ export function useFileImport() {
35
+ const [files, setFiles] = useState<ImportFile[]>([]);
36
+ const [step, setStep] = useState<ImportStep>('select');
37
+ const [intent, setIntent] = useState<ImportIntent>('archive');
38
+ const [targetSpace, setTargetSpace] = useState('');
39
+ const [conflict, setConflict] = useState<ConflictMode>('rename');
40
+ const [importing, setImporting] = useState(false);
41
+ const [result, setResult] = useState<{
42
+ created: Array<{ original: string; path: string }>;
43
+ skipped: Array<{ name: string; reason: string }>;
44
+ errors: Array<{ name: string; error: string }>;
45
+ updatedFiles: string[];
46
+ } | null>(null);
47
+ const inputRef = useRef<HTMLInputElement>(null);
48
+
49
+ const addFiles = useCallback(async (fileList: FileList | File[]) => {
50
+ const incoming = Array.from(fileList).slice(0, MAX_FILES);
51
+ const newFiles: ImportFile[] = [];
52
+
53
+ for (const file of incoming) {
54
+ const ext = getExt(file.name);
55
+ const maxSize = ext === '.pdf' ? MAX_PDF_SIZE : MAX_FILE_SIZE;
56
+
57
+ let error: string | null = null;
58
+ if (!ALLOWED_IMPORT_EXTENSIONS.has(ext)) {
59
+ error = 'unsupported';
60
+ } else if (file.size > maxSize) {
61
+ error = 'tooLarge';
62
+ }
63
+
64
+ newFiles.push({
65
+ file,
66
+ name: file.name,
67
+ size: file.size,
68
+ content: null,
69
+ loading: !error,
70
+ error,
71
+ });
72
+ }
73
+
74
+ setFiles(prev => {
75
+ const merged = [...prev];
76
+ for (const f of newFiles) {
77
+ const isDup = merged.some(m =>
78
+ m.name === f.name && m.size === f.size
79
+ );
80
+ if (!isDup && merged.length < MAX_FILES) merged.push(f);
81
+ }
82
+ return merged;
83
+ });
84
+
85
+ for (const f of newFiles) {
86
+ if (f.error) continue;
87
+ try {
88
+ const text = await f.file.text();
89
+ setFiles(prev => prev.map(p =>
90
+ p.name === f.name && p.size === f.size
91
+ ? { ...p, content: text, loading: false }
92
+ : p
93
+ ));
94
+ } catch {
95
+ setFiles(prev => prev.map(p =>
96
+ p.name === f.name && p.size === f.size
97
+ ? { ...p, loading: false, error: 'readFailed' }
98
+ : p
99
+ ));
100
+ }
101
+ }
102
+ }, []);
103
+
104
+ const removeFile = useCallback((index: number) => {
105
+ setFiles(prev => prev.filter((_, i) => i !== index));
106
+ }, []);
107
+
108
+ const clearFiles = useCallback(() => {
109
+ setFiles([]);
110
+ setStep('select');
111
+ setResult(null);
112
+ }, []);
113
+
114
+ const validFiles = files.filter(f => !f.error && f.content !== null);
115
+ const allReady = files.length > 0 && files.every(f => !f.loading);
116
+ const hasErrors = files.some(f => f.error);
117
+
118
+ const doArchive = useCallback(async () => {
119
+ if (validFiles.length === 0) return;
120
+ setImporting(true);
121
+ setStep('importing');
122
+
123
+ try {
124
+ const payload = {
125
+ files: validFiles.map(f => ({
126
+ name: f.name,
127
+ content: f.content!,
128
+ })),
129
+ targetSpace: targetSpace || undefined,
130
+ conflict,
131
+ organize: true,
132
+ };
133
+
134
+ const res = await fetch('/api/file/import', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify(payload),
138
+ });
139
+
140
+ const data = await res.json();
141
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
142
+
143
+ setResult(data);
144
+ setStep('done');
145
+ } catch (err) {
146
+ setResult({
147
+ created: [],
148
+ skipped: [],
149
+ errors: [{ name: '*', error: (err as Error).message }],
150
+ updatedFiles: [],
151
+ });
152
+ setStep('done');
153
+ } finally {
154
+ setImporting(false);
155
+ }
156
+ }, [validFiles, targetSpace, conflict]);
157
+
158
+ const reset = useCallback(() => {
159
+ setFiles([]);
160
+ setStep('select');
161
+ setIntent('archive');
162
+ setTargetSpace('');
163
+ setConflict('rename');
164
+ setImporting(false);
165
+ setResult(null);
166
+ }, []);
167
+
168
+ return {
169
+ files,
170
+ step,
171
+ intent,
172
+ targetSpace,
173
+ conflict,
174
+ importing,
175
+ result,
176
+ inputRef,
177
+ validFiles,
178
+ allReady,
179
+ hasErrors,
180
+ addFiles,
181
+ removeFile,
182
+ clearFiles,
183
+ setStep,
184
+ setIntent,
185
+ setTargetSpace,
186
+ setConflict,
187
+ doArchive,
188
+ reset,
189
+ formatSize,
190
+ };
191
+ }
@@ -115,6 +115,16 @@ export function useFileUpload() {
115
115
  setUploadError('');
116
116
  }, []);
117
117
 
118
+ const injectFiles = useCallback((files: LocalAttachment[]) => {
119
+ setLocalAttachments(prev => {
120
+ const merged = [...prev];
121
+ for (const item of files) {
122
+ if (!merged.some(m => m.name === item.name)) merged.push(item);
123
+ }
124
+ return merged;
125
+ });
126
+ }, []);
127
+
118
128
  return {
119
129
  localAttachments,
120
130
  uploadError,
@@ -122,5 +132,6 @@ export function useFileUpload() {
122
132
  pickFiles,
123
133
  removeAttachment,
124
134
  clearAttachments,
135
+ injectFiles,
125
136
  };
126
137
  }
@@ -27,19 +27,27 @@ export function useMention() {
27
27
  }, [loadFiles]);
28
28
 
29
29
  const updateMentionFromInput = useCallback(
30
- (val: string) => {
31
- const atIdx = val.lastIndexOf('@');
30
+ (val: string, cursorPos?: number) => {
31
+ const pos = cursorPos ?? val.length;
32
+ const before = val.slice(0, pos);
33
+ const atIdx = before.lastIndexOf('@');
32
34
  if (atIdx === -1) {
33
35
  setMentionQuery(null);
34
36
  return;
35
37
  }
36
- const before = val[atIdx - 1];
37
- if (atIdx > 0 && before !== ' ') {
38
+ if (atIdx > 0 && before[atIdx - 1] !== ' ' && before[atIdx - 1] !== '\n') {
38
39
  setMentionQuery(null);
39
40
  return;
40
41
  }
41
- const query = val.slice(atIdx + 1).toLowerCase();
42
- const filtered = allFiles.filter((f) => f.toLowerCase().includes(query)).slice(0, 30);
42
+ const query = before.slice(atIdx + 1);
43
+ if (query.includes(' ') || query.includes('\n')) {
44
+ setMentionQuery(null);
45
+ setMentionResults([]);
46
+ setMentionIndex(0);
47
+ return;
48
+ }
49
+ const q = query.toLowerCase();
50
+ const filtered = allFiles.filter((f) => f.toLowerCase().includes(q)).slice(0, 30);
43
51
  if (filtered.length === 0) {
44
52
  setMentionQuery(null);
45
53
  setMentionResults([]);
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useEffect, useRef } from 'react';
4
+ import type { SkillInfo } from '@/components/settings/types';
5
+
6
+ export interface SlashItem {
7
+ type: 'skill';
8
+ name: string;
9
+ description: string;
10
+ }
11
+
12
+ function safeFetchSkills(): Promise<SkillInfo[]> {
13
+ return fetch('/api/skills')
14
+ .then((r) => (r.ok ? r.json() : { skills: [] }))
15
+ .then((data) => (Array.isArray(data?.skills) ? data.skills : []))
16
+ .catch(() => [] as SkillInfo[]);
17
+ }
18
+
19
+ export function useSlashCommand() {
20
+ const [allSkills, setAllSkills] = useState<SkillInfo[]>([]);
21
+ const [slashQuery, setSlashQuery] = useState<string | null>(null);
22
+ const [slashResults, setSlashResults] = useState<SlashItem[]>([]);
23
+ const [slashIndex, setSlashIndex] = useState(0);
24
+ const loaded = useRef(false);
25
+
26
+ const loadSkills = useCallback(async () => {
27
+ const skills = await safeFetchSkills();
28
+ setAllSkills(skills.filter((s) => s.enabled));
29
+ loaded.current = true;
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ loadSkills();
34
+ const handler = () => loadSkills();
35
+ window.addEventListener('mindos:skills-changed', handler);
36
+ return () => window.removeEventListener('mindos:skills-changed', handler);
37
+ }, [loadSkills]);
38
+
39
+ const updateSlashFromInput = useCallback(
40
+ (val: string, cursorPos: number) => {
41
+ const before = val.slice(0, cursorPos);
42
+ const slashIdx = before.lastIndexOf('/');
43
+
44
+ if (slashIdx === -1) {
45
+ setSlashQuery(null);
46
+ return;
47
+ }
48
+
49
+ // `/` must be at line start or preceded by whitespace
50
+ if (slashIdx > 0 && before[slashIdx - 1] !== ' ' && before[slashIdx - 1] !== '\n') {
51
+ setSlashQuery(null);
52
+ return;
53
+ }
54
+
55
+ // No space in the typed query — slash commands are single tokens
56
+ const query = before.slice(slashIdx + 1);
57
+ if (query.includes(' ')) {
58
+ setSlashQuery(null);
59
+ return;
60
+ }
61
+
62
+ if (!loaded.current) {
63
+ loadSkills();
64
+ setSlashQuery(null);
65
+ return;
66
+ }
67
+
68
+ const q = query.toLowerCase();
69
+ const items: SlashItem[] = allSkills
70
+ .filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q))
71
+ .slice(0, 20)
72
+ .map((s) => ({ type: 'skill', name: s.name, description: s.description }));
73
+
74
+ if (items.length === 0) {
75
+ setSlashQuery(null);
76
+ setSlashResults([]);
77
+ setSlashIndex(0);
78
+ return;
79
+ }
80
+
81
+ setSlashQuery(query);
82
+ setSlashResults(items);
83
+ setSlashIndex(0);
84
+ },
85
+ [allSkills, loadSkills],
86
+ );
87
+
88
+ const navigateSlash = useCallback(
89
+ (direction: 'up' | 'down') => {
90
+ if (slashResults.length === 0) return;
91
+ if (direction === 'down') {
92
+ setSlashIndex((i) => Math.min(i + 1, slashResults.length - 1));
93
+ } else {
94
+ setSlashIndex((i) => Math.max(i - 1, 0));
95
+ }
96
+ },
97
+ [slashResults.length],
98
+ );
99
+
100
+ const resetSlash = useCallback(() => {
101
+ setSlashQuery(null);
102
+ setSlashResults([]);
103
+ setSlashIndex(0);
104
+ }, []);
105
+
106
+ return {
107
+ slashQuery,
108
+ slashResults,
109
+ slashIndex,
110
+ updateSlashFromInput,
111
+ navigateSlash,
112
+ resetSlash,
113
+ };
114
+ }
@@ -1,7 +1,10 @@
1
1
  'use server';
2
2
 
3
- import { createFile, deleteFile, deleteDirectory, convertToSpace, renameFile, renameSpace, getMindRoot, invalidateCache } from '@/lib/fs';
4
- import { createSpaceFilesystem } from '@/lib/core/create-space';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { createFile, deleteFile, deleteDirectory, convertToSpace, renameFile, renameSpace, getMindRoot, invalidateCache, collectAllFiles } from '@/lib/fs';
6
+ import { createSpaceFilesystem, generateReadmeTemplate } from '@/lib/core/create-space';
7
+ import { INSTRUCTION_TEMPLATE, cleanDirName } from '@/lib/core/space-scaffold';
5
8
  import { revalidatePath } from 'next/cache';
6
9
 
7
10
  export async function createFileAction(dirPath: string, fileName: string): Promise<{ success: boolean; filePath?: string; error?: string }> {
@@ -112,3 +115,77 @@ export async function createSpaceAction(
112
115
  return { success: false, error: msg };
113
116
  }
114
117
  }
118
+
119
+ /**
120
+ * Revert AI-generated space content back to scaffold templates.
121
+ * Called when user discards AI initialization from SpaceInitToast.
122
+ */
123
+ export async function revertSpaceInitAction(
124
+ spacePath: string,
125
+ name: string,
126
+ description: string,
127
+ ): Promise<{ success: boolean; error?: string }> {
128
+ try {
129
+ const mindRoot = getMindRoot();
130
+ const absDir = path.resolve(mindRoot, spacePath);
131
+ if (!absDir.startsWith(mindRoot)) {
132
+ return { success: false, error: 'Invalid path' };
133
+ }
134
+
135
+ const readmePath = path.join(absDir, 'README.md');
136
+ const instructionPath = path.join(absDir, 'INSTRUCTION.md');
137
+
138
+ const readmeContent = generateReadmeTemplate(spacePath, name, description);
139
+ fs.writeFileSync(readmePath, readmeContent, 'utf-8');
140
+
141
+ const dirName = cleanDirName(name);
142
+ fs.writeFileSync(instructionPath, INSTRUCTION_TEMPLATE(dirName), 'utf-8');
143
+
144
+ invalidateCache();
145
+ revalidatePath('/', 'layout');
146
+ return { success: true };
147
+ } catch (err) {
148
+ return { success: false, error: err instanceof Error ? err.message : 'Failed to revert' };
149
+ }
150
+ }
151
+
152
+ const EXAMPLE_PREFIX = '🧪_example_';
153
+
154
+ export async function scanExampleFilesAction(): Promise<{ files: string[] }> {
155
+ const all = collectAllFiles();
156
+ const examples = all.filter(f => path.basename(f).startsWith(EXAMPLE_PREFIX));
157
+ return { files: examples };
158
+ }
159
+
160
+ export async function cleanupExamplesAction(): Promise<{ success: boolean; deleted: number; error?: string }> {
161
+ try {
162
+ const { files } = await scanExampleFilesAction();
163
+ if (files.length === 0) return { success: true, deleted: 0 };
164
+
165
+ const root = getMindRoot();
166
+ for (const relPath of files) {
167
+ const absPath = path.resolve(root, relPath);
168
+ if (absPath.startsWith(root) && fs.existsSync(absPath)) {
169
+ fs.unlinkSync(absPath);
170
+ }
171
+ }
172
+
173
+ // Clean up empty directories left behind
174
+ const dirs = new Set(files.map(f => path.dirname(path.resolve(root, f))));
175
+ const sortedDirs = [...dirs].sort((a, b) => b.length - a.length);
176
+ for (const dir of sortedDirs) {
177
+ try {
178
+ if (dir.startsWith(root) && dir !== root) {
179
+ const entries = fs.readdirSync(dir);
180
+ if (entries.length === 0) fs.rmdirSync(dir);
181
+ }
182
+ } catch { /* directory not empty or already removed */ }
183
+ }
184
+
185
+ invalidateCache();
186
+ revalidatePath('/', 'layout');
187
+ return { success: true, deleted: files.length };
188
+ } catch (err) {
189
+ return { success: false, deleted: 0, error: err instanceof Error ? err.message : 'Failed to cleanup' };
190
+ }
191
+ }
@@ -1,5 +1,5 @@
1
1
  export { getModelConfig } from './model';
2
- export { knowledgeBaseTools, WRITE_TOOLS, truncate } from './tools';
2
+ export { getRequestScopedTools, knowledgeBaseTools, WRITE_TOOLS, truncate } from './tools';
3
3
  export { AGENT_SYSTEM_PROMPT } from './prompt';
4
4
  export {
5
5
  estimateTokens, estimateStringTokens, getContextLimit, needsCompact,
@@ -27,6 +27,8 @@ Persona: Methodical, strictly objective, execution-oriented. Zero fluff. Never u
27
27
 
28
28
  - **Auto-loaded**: Configs, instructions, and SKILL.md are already in your context. Do not search for them unless explicitly asked.
29
29
  - **Uploaded Files**: Local files attached by the user appear in the "⚠️ USER-UPLOADED FILES" section below. Use this content directly. Do NOT use tools to read/search them.
30
+ - **Skills**: Use the list_skills and load_skill tools to discover available skills on demand.
31
+ - **MCP**: The MindOS MCP server is built-in. Use list_mcp_tools and call_mcp_tool to inspect and invoke additional MCP tools configured in ~/.mindos/mcp.json.
30
32
 
31
33
  ## Output
32
34