@geminilight/mindos 0.5.37 → 0.5.39
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/auth/route.ts +61 -9
- package/app/app/api/health/route.ts +46 -1
- package/app/app/globals.css +0 -1
- package/app/app/page.tsx +48 -2
- package/app/components/HomeContent.tsx +453 -76
- package/app/components/SidebarLayout.tsx +70 -235
- package/app/components/settings/McpSkillCreateForm.tsx +178 -0
- package/app/components/settings/McpSkillRow.tsx +145 -0
- package/app/components/settings/McpSkillsSection.tsx +71 -307
- package/app/hooks/useAskPanel.ts +117 -0
- package/app/hooks/useLeftPanel.ts +81 -0
- package/app/lib/actions.ts +35 -0
- package/app/lib/core/fs-ops.ts +2 -0
- package/app/lib/core/space-scaffold.ts +103 -0
- package/app/lib/i18n-en.ts +14 -3
- package/app/lib/i18n-zh.ts +14 -3
- package/app/lib/mcp-agents.ts +86 -7
- package/app/package.json +4 -2
- package/package.json +1 -5
- package/scripts/release.sh +18 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { RIGHT_ASK_DEFAULT_WIDTH, RIGHT_ASK_MIN_WIDTH, RIGHT_ASK_MAX_WIDTH } from '@/components/RightAskPanel';
|
|
5
|
+
import { useAskModal } from './useAskModal';
|
|
6
|
+
|
|
7
|
+
export interface AskPanelState {
|
|
8
|
+
askPanelOpen: boolean;
|
|
9
|
+
askPanelWidth: number;
|
|
10
|
+
askMode: 'panel' | 'popup';
|
|
11
|
+
desktopAskPopupOpen: boolean;
|
|
12
|
+
askInitialMessage: string;
|
|
13
|
+
askOpenSource: 'user' | 'guide' | 'guide-next';
|
|
14
|
+
toggleAskPanel: () => void;
|
|
15
|
+
closeAskPanel: () => void;
|
|
16
|
+
closeDesktopAskPopup: () => void;
|
|
17
|
+
handleAskWidthChange: (w: number) => void;
|
|
18
|
+
handleAskWidthCommit: (w: number) => void;
|
|
19
|
+
handleAskModeSwitch: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manages right-side Ask AI panel state: open/close, width, panel/popup mode, initial message.
|
|
24
|
+
* Extracted from SidebarLayout to reduce its state complexity.
|
|
25
|
+
*/
|
|
26
|
+
export function useAskPanel(): AskPanelState {
|
|
27
|
+
const [askPanelOpen, setAskPanelOpen] = useState(false);
|
|
28
|
+
const [askPanelWidth, setAskPanelWidth] = useState(RIGHT_ASK_DEFAULT_WIDTH);
|
|
29
|
+
const [askMode, setAskMode] = useState<'panel' | 'popup'>('panel');
|
|
30
|
+
const [desktopAskPopupOpen, setDesktopAskPopupOpen] = useState(false);
|
|
31
|
+
const [askInitialMessage, setAskInitialMessage] = useState('');
|
|
32
|
+
const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
|
|
33
|
+
|
|
34
|
+
const askModal = useAskModal();
|
|
35
|
+
|
|
36
|
+
// Load persisted width + mode
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
try {
|
|
39
|
+
const stored = localStorage.getItem('right-ask-panel-width');
|
|
40
|
+
if (stored) {
|
|
41
|
+
const w = parseInt(stored, 10);
|
|
42
|
+
if (w >= RIGHT_ASK_MIN_WIDTH && w <= RIGHT_ASK_MAX_WIDTH) setAskPanelWidth(w);
|
|
43
|
+
}
|
|
44
|
+
const mode = localStorage.getItem('ask-mode');
|
|
45
|
+
if (mode === 'popup') setAskMode('popup');
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
const onStorage = (e: StorageEvent) => {
|
|
49
|
+
if (e.key === 'ask-mode' && (e.newValue === 'panel' || e.newValue === 'popup')) {
|
|
50
|
+
setAskMode(e.newValue);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
window.addEventListener('storage', onStorage);
|
|
54
|
+
return () => window.removeEventListener('storage', onStorage);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Bridge useAskModal store → right Ask panel or popup
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (askModal.open) {
|
|
60
|
+
setAskInitialMessage(askModal.initialMessage);
|
|
61
|
+
setAskOpenSource(askModal.source);
|
|
62
|
+
if (askMode === 'popup') {
|
|
63
|
+
setDesktopAskPopupOpen(true);
|
|
64
|
+
} else {
|
|
65
|
+
setAskPanelOpen(true);
|
|
66
|
+
}
|
|
67
|
+
askModal.close();
|
|
68
|
+
}
|
|
69
|
+
}, [askModal.open, askModal.initialMessage, askModal.source, askModal.close, askMode]);
|
|
70
|
+
|
|
71
|
+
const toggleAskPanel = useCallback(() => {
|
|
72
|
+
if (askMode === 'popup') {
|
|
73
|
+
setDesktopAskPopupOpen(v => {
|
|
74
|
+
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
75
|
+
return !v;
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
setAskPanelOpen(v => {
|
|
79
|
+
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
80
|
+
return !v;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}, [askMode]);
|
|
84
|
+
|
|
85
|
+
const closeAskPanel = useCallback(() => setAskPanelOpen(false), []);
|
|
86
|
+
const closeDesktopAskPopup = useCallback(() => setDesktopAskPopupOpen(false), []);
|
|
87
|
+
|
|
88
|
+
const handleAskWidthChange = useCallback((w: number) => setAskPanelWidth(w), []);
|
|
89
|
+
const handleAskWidthCommit = useCallback((w: number) => {
|
|
90
|
+
try { localStorage.setItem('right-ask-panel-width', String(w)); } catch {}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const handleAskModeSwitch = useCallback(() => {
|
|
94
|
+
setAskMode(prev => {
|
|
95
|
+
const next = prev === 'panel' ? 'popup' : 'panel';
|
|
96
|
+
try {
|
|
97
|
+
localStorage.setItem('ask-mode', next);
|
|
98
|
+
window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
|
|
99
|
+
} catch {}
|
|
100
|
+
if (next === 'popup') {
|
|
101
|
+
setAskPanelOpen(false);
|
|
102
|
+
setDesktopAskPopupOpen(true);
|
|
103
|
+
} else {
|
|
104
|
+
setDesktopAskPopupOpen(false);
|
|
105
|
+
setAskPanelOpen(true);
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
askPanelOpen, askPanelWidth, askMode, desktopAskPopupOpen,
|
|
113
|
+
askInitialMessage, askOpenSource,
|
|
114
|
+
toggleAskPanel, closeAskPanel, closeDesktopAskPopup,
|
|
115
|
+
handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { MIN_PANEL_WIDTH, MAX_PANEL_WIDTH_ABS, PANEL_WIDTH } from '@/components/Panel';
|
|
5
|
+
import type { PanelId } from '@/components/ActivityBar';
|
|
6
|
+
import { RAIL_WIDTH_COLLAPSED, RAIL_WIDTH_EXPANDED } from '@/components/ActivityBar';
|
|
7
|
+
|
|
8
|
+
export interface LeftPanelState {
|
|
9
|
+
activePanel: PanelId | null;
|
|
10
|
+
setActivePanel: (p: PanelId | null | ((prev: PanelId | null) => PanelId | null)) => void;
|
|
11
|
+
panelWidth: number | null;
|
|
12
|
+
panelMaximized: boolean;
|
|
13
|
+
railExpanded: boolean;
|
|
14
|
+
railWidth: number;
|
|
15
|
+
panelOpen: boolean;
|
|
16
|
+
effectivePanelWidth: number;
|
|
17
|
+
handlePanelWidthChange: (w: number) => void;
|
|
18
|
+
handlePanelWidthCommit: (w: number) => void;
|
|
19
|
+
handlePanelMaximize: () => void;
|
|
20
|
+
handleExpandedChange: (expanded: boolean) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages left panel state: active panel, width, maximize, rail expansion.
|
|
25
|
+
* Extracted from SidebarLayout to reduce its state complexity.
|
|
26
|
+
*/
|
|
27
|
+
export function useLeftPanel(): LeftPanelState {
|
|
28
|
+
const [activePanel, setActivePanel] = useState<PanelId | null>('files');
|
|
29
|
+
const [panelWidth, setPanelWidth] = useState<number | null>(null);
|
|
30
|
+
const [panelMaximized, setPanelMaximized] = useState(false);
|
|
31
|
+
const [railExpanded, setRailExpanded] = useState(false);
|
|
32
|
+
|
|
33
|
+
// Load persisted rail state
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
try {
|
|
36
|
+
if (localStorage.getItem('rail-expanded') === 'true') setRailExpanded(true);
|
|
37
|
+
} catch {}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Load persisted panel width when activePanel changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!activePanel) return;
|
|
43
|
+
try {
|
|
44
|
+
const stored = localStorage.getItem('left-panel-width');
|
|
45
|
+
if (stored) {
|
|
46
|
+
const w = parseInt(stored, 10);
|
|
47
|
+
if (w >= MIN_PANEL_WIDTH && w <= MAX_PANEL_WIDTH_ABS) {
|
|
48
|
+
setPanelWidth(w);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
setPanelWidth(280);
|
|
54
|
+
}, [activePanel]);
|
|
55
|
+
|
|
56
|
+
// Exit maximize when switching panels
|
|
57
|
+
useEffect(() => { setPanelMaximized(false); }, [activePanel]);
|
|
58
|
+
|
|
59
|
+
const handlePanelWidthChange = useCallback((w: number) => setPanelWidth(w), []);
|
|
60
|
+
const handlePanelWidthCommit = useCallback((w: number) => {
|
|
61
|
+
try { localStorage.setItem('left-panel-width', String(w)); } catch {}
|
|
62
|
+
}, []);
|
|
63
|
+
const handlePanelMaximize = useCallback(() => setPanelMaximized(v => !v), []);
|
|
64
|
+
|
|
65
|
+
const handleExpandedChange = useCallback((expanded: boolean) => {
|
|
66
|
+
setRailExpanded(expanded);
|
|
67
|
+
try { localStorage.setItem('rail-expanded', String(expanded)); } catch {}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const railWidth = railExpanded ? RAIL_WIDTH_EXPANDED : RAIL_WIDTH_COLLAPSED;
|
|
71
|
+
const panelOpen = activePanel !== null;
|
|
72
|
+
const effectivePanelWidth = panelWidth ?? (activePanel ? PANEL_WIDTH[activePanel] : 280);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
activePanel, setActivePanel,
|
|
76
|
+
panelWidth, panelMaximized, railExpanded, railWidth,
|
|
77
|
+
panelOpen, effectivePanelWidth,
|
|
78
|
+
handlePanelWidthChange, handlePanelWidthCommit, handlePanelMaximize,
|
|
79
|
+
handleExpandedChange,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/app/lib/actions.ts
CHANGED
|
@@ -38,3 +38,38 @@ export async function renameFileAction(oldPath: string, newName: string): Promis
|
|
|
38
38
|
return { success: false, error: err instanceof Error ? err.message : 'Failed to rename file' };
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new Mind Space (top-level directory) with README.md + auto-scaffolded INSTRUCTION.md.
|
|
44
|
+
* The description is written into README.md so it appears on the homepage Space card
|
|
45
|
+
* and is loaded by Agent bootstrap as directory context.
|
|
46
|
+
*/
|
|
47
|
+
export async function createSpaceAction(
|
|
48
|
+
name: string,
|
|
49
|
+
description: string
|
|
50
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
51
|
+
try {
|
|
52
|
+
const trimmed = name.trim();
|
|
53
|
+
if (!trimmed) return { success: false, error: 'Space name is required' };
|
|
54
|
+
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
55
|
+
return { success: false, error: 'Space name must not contain path separators' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Strip emoji for clean title in README content
|
|
59
|
+
const cleanName = trimmed.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '') || trimmed;
|
|
60
|
+
const desc = description.trim() || '(Describe the purpose and usage of this space.)';
|
|
61
|
+
const readmeContent = `# ${cleanName}\n\n${desc}\n\n## 📁 Structure\n\n\`\`\`bash\n${trimmed}/\n├── INSTRUCTION.md\n├── README.md\n└── (your files here)\n\`\`\`\n\n## 💡 Usage\n\n(Add usage guidelines for this space.)\n`;
|
|
62
|
+
|
|
63
|
+
// createFile triggers scaffoldIfNewSpace → auto-generates INSTRUCTION.md
|
|
64
|
+
createFile(`${trimmed}/README.md`, readmeContent);
|
|
65
|
+
revalidatePath('/', 'layout');
|
|
66
|
+
return { success: true };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const msg = err instanceof Error ? err.message : 'Failed to create space';
|
|
69
|
+
// Make "already exists" error more user-friendly
|
|
70
|
+
if (msg.includes('already exists')) {
|
|
71
|
+
return { success: false, error: 'A space with this name already exists' };
|
|
72
|
+
}
|
|
73
|
+
return { success: false, error: msg };
|
|
74
|
+
}
|
|
75
|
+
}
|
package/app/lib/core/fs-ops.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolveSafe, assertWithinRoot } from './security';
|
|
4
4
|
import { MindOSError, ErrorCodes } from '@/lib/errors';
|
|
5
|
+
import { scaffoldIfNewSpace } from './space-scaffold';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Reads the content of a file given a relative path from mindRoot.
|
|
@@ -40,6 +41,7 @@ export function createFile(mindRoot: string, filePath: string, initialContent =
|
|
|
40
41
|
}
|
|
41
42
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
42
43
|
fs.writeFileSync(resolved, initialContent, 'utf-8');
|
|
44
|
+
scaffoldIfNewSpace(mindRoot, filePath);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-scaffolding for new top-level directories (Spaces).
|
|
3
|
+
*
|
|
4
|
+
* When a file is created inside a new first-level directory that lacks
|
|
5
|
+
* INSTRUCTION.md, this module generates lightweight scaffolding files
|
|
6
|
+
* so that MindOS Agents can bootstrap correctly in any Space.
|
|
7
|
+
*
|
|
8
|
+
* Design decisions:
|
|
9
|
+
* - Only acts on first-level directories (direct children of mindRoot)
|
|
10
|
+
* - Idempotent: never overwrites existing files
|
|
11
|
+
* - Fail-safe: errors are silently caught (scaffold must never block file ops)
|
|
12
|
+
* - Does NOT modify root README.md (that's the Agent's job per INSTRUCTION.md §5.1)
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
|
|
17
|
+
export const INSTRUCTION_TEMPLATE = (dirName: string) =>
|
|
18
|
+
`# ${dirName} Instruction Set
|
|
19
|
+
|
|
20
|
+
## Goal
|
|
21
|
+
|
|
22
|
+
- Define local execution rules for this directory.
|
|
23
|
+
|
|
24
|
+
## Local Rules
|
|
25
|
+
|
|
26
|
+
- Read root \`INSTRUCTION.md\` first.
|
|
27
|
+
- Then read this directory \`README.md\` for navigation.
|
|
28
|
+
- Keep edits minimal, structured, and traceable.
|
|
29
|
+
|
|
30
|
+
## Execution Order
|
|
31
|
+
|
|
32
|
+
1. Root \`INSTRUCTION.md\`
|
|
33
|
+
2. This directory \`INSTRUCTION.md\`
|
|
34
|
+
3. This directory \`README.md\` and target files
|
|
35
|
+
|
|
36
|
+
## Boundary
|
|
37
|
+
|
|
38
|
+
- Root rules win on conflict.
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
export const README_TEMPLATE = (dirName: string) =>
|
|
42
|
+
`# ${dirName}
|
|
43
|
+
|
|
44
|
+
## 📁 Structure
|
|
45
|
+
|
|
46
|
+
\`\`\`bash
|
|
47
|
+
${dirName}/
|
|
48
|
+
├── INSTRUCTION.md
|
|
49
|
+
├── README.md
|
|
50
|
+
└── (your files here)
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
## 💡 Usage
|
|
54
|
+
|
|
55
|
+
(Describe the purpose and usage of this space.)
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip leading emoji and whitespace from a directory name.
|
|
60
|
+
* e.g. "📖 Learning" → "Learning", "🔄 Workflows" → "Workflows"
|
|
61
|
+
*/
|
|
62
|
+
export function cleanDirName(dirName: string): string {
|
|
63
|
+
// Match leading emoji (Unicode emoji properties) + whitespace
|
|
64
|
+
const cleaned = dirName.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '');
|
|
65
|
+
return cleaned || dirName; // fallback to original if everything was stripped
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* If filePath is inside a top-level directory that lacks INSTRUCTION.md,
|
|
70
|
+
* auto-generate scaffolding files.
|
|
71
|
+
*
|
|
72
|
+
* - Only triggered by createFile (not writeFile) — see spec for rationale
|
|
73
|
+
* - Idempotent: won't overwrite existing files
|
|
74
|
+
* - Only acts on first-level directories (direct children of mindRoot)
|
|
75
|
+
*/
|
|
76
|
+
export function scaffoldIfNewSpace(mindRoot: string, filePath: string): void {
|
|
77
|
+
try {
|
|
78
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
79
|
+
if (parts.length < 2) return; // root-level file, not inside a Space
|
|
80
|
+
|
|
81
|
+
const topDir = parts[0];
|
|
82
|
+
if (topDir.startsWith('.')) return; // skip hidden directories
|
|
83
|
+
|
|
84
|
+
const topDirAbs = path.join(mindRoot, topDir);
|
|
85
|
+
const instructionPath = path.join(topDirAbs, 'INSTRUCTION.md');
|
|
86
|
+
|
|
87
|
+
// Already has INSTRUCTION.md → not a new Space, skip
|
|
88
|
+
if (fs.existsSync(instructionPath)) return;
|
|
89
|
+
|
|
90
|
+
const name = cleanDirName(topDir);
|
|
91
|
+
|
|
92
|
+
// Generate INSTRUCTION.md
|
|
93
|
+
fs.writeFileSync(instructionPath, INSTRUCTION_TEMPLATE(name), 'utf-8');
|
|
94
|
+
|
|
95
|
+
// Generate README.md only if it doesn't exist
|
|
96
|
+
const readmePath = path.join(topDirAbs, 'README.md');
|
|
97
|
+
if (!fs.existsSync(readmePath)) {
|
|
98
|
+
fs.writeFileSync(readmePath, README_TEMPLATE(name), 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Scaffold failure must never block the primary file operation
|
|
102
|
+
}
|
|
103
|
+
}
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -3,11 +3,22 @@ export const en = {
|
|
|
3
3
|
relatedFiles: 'Related Files',
|
|
4
4
|
},
|
|
5
5
|
app: {
|
|
6
|
-
tagline: '
|
|
7
|
-
footer: 'MindOS ·
|
|
6
|
+
tagline: 'You think here, Agents act there.',
|
|
7
|
+
footer: 'MindOS · human-agent collaborative mind system',
|
|
8
8
|
},
|
|
9
9
|
home: {
|
|
10
10
|
recentlyModified: 'Recently Modified',
|
|
11
|
+
recentlyActive: 'Recently Active',
|
|
12
|
+
recentlyEdited: 'Recently Edited',
|
|
13
|
+
allSpaces: 'All Spaces',
|
|
14
|
+
spaces: 'Spaces',
|
|
15
|
+
nFiles: (n: number) => `${n} file${n === 1 ? '' : 's'}`,
|
|
16
|
+
other: 'Other',
|
|
17
|
+
newSpace: 'New Space',
|
|
18
|
+
spaceName: 'Space name',
|
|
19
|
+
spaceDescription: 'What is this space for?',
|
|
20
|
+
createSpace: 'Create',
|
|
21
|
+
cancelCreate: 'Cancel',
|
|
11
22
|
continueEditing: 'Continue editing',
|
|
12
23
|
newNote: 'New Notes',
|
|
13
24
|
plugins: 'Plugins',
|
|
@@ -29,7 +40,7 @@ export const en = {
|
|
|
29
40
|
},
|
|
30
41
|
},
|
|
31
42
|
sidebar: {
|
|
32
|
-
files: '
|
|
43
|
+
files: 'Spaces',
|
|
33
44
|
search: 'Search',
|
|
34
45
|
searchTitle: 'Search',
|
|
35
46
|
askTitle: 'MindOS Agent',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -28,11 +28,22 @@ export const zh = {
|
|
|
28
28
|
relatedFiles: '关联视图',
|
|
29
29
|
},
|
|
30
30
|
app: {
|
|
31
|
-
tagline: '
|
|
32
|
-
footer: 'MindOS ·
|
|
31
|
+
tagline: '你在此思考,Agent 依此行动。',
|
|
32
|
+
footer: 'MindOS · 人机共生知识系统',
|
|
33
33
|
},
|
|
34
34
|
home: {
|
|
35
35
|
recentlyModified: '最近修改',
|
|
36
|
+
recentlyActive: '最近活跃',
|
|
37
|
+
recentlyEdited: '最近编辑',
|
|
38
|
+
allSpaces: '所有空间',
|
|
39
|
+
spaces: '心智空间',
|
|
40
|
+
nFiles: (n: number) => `${n} 个文件`,
|
|
41
|
+
other: '其他',
|
|
42
|
+
newSpace: '新建空间',
|
|
43
|
+
spaceName: '空间名称',
|
|
44
|
+
spaceDescription: '这个空间用来做什么?',
|
|
45
|
+
createSpace: '创建',
|
|
46
|
+
cancelCreate: '取消',
|
|
36
47
|
continueEditing: '继续编辑',
|
|
37
48
|
newNote: '新建笔记',
|
|
38
49
|
plugins: '插件',
|
|
@@ -54,7 +65,7 @@ export const zh = {
|
|
|
54
65
|
},
|
|
55
66
|
},
|
|
56
67
|
sidebar: {
|
|
57
|
-
files: '
|
|
68
|
+
files: '空间',
|
|
58
69
|
search: '搜索',
|
|
59
70
|
searchTitle: '搜索',
|
|
60
71
|
askTitle: 'MindOS Agent',
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -211,17 +211,29 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
|
|
|
211
211
|
const agent = MCP_AGENTS[agentKey];
|
|
212
212
|
if (!agent) return { installed: false };
|
|
213
213
|
|
|
214
|
-
for (const [
|
|
214
|
+
for (const [scopeType, cfgPath] of [['global', agent.global], ['project', agent.project]] as [string, string | null][]) {
|
|
215
215
|
if (!cfgPath) continue;
|
|
216
216
|
const absPath = expandHome(cfgPath);
|
|
217
217
|
if (!fs.existsSync(absPath)) continue;
|
|
218
218
|
try {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
220
|
+
// Handle TOML format (e.g., codex)
|
|
221
|
+
if (agent.format === 'toml') {
|
|
222
|
+
const result = parseTomlMcpEntry(content, agent.key, 'mindos');
|
|
223
|
+
if (result.found && result.entry) {
|
|
224
|
+
const entry = result.entry;
|
|
225
|
+
const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
|
|
226
|
+
return { installed: true, scope: scopeType, transport, configPath: cfgPath };
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// JSON format (default)
|
|
230
|
+
const config = JSON.parse(content);
|
|
231
|
+
const servers = config[agent.key];
|
|
232
|
+
if (servers?.mindos) {
|
|
233
|
+
const entry = servers.mindos;
|
|
234
|
+
const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
|
|
235
|
+
return { installed: true, scope: scopeType, transport, configPath: cfgPath };
|
|
236
|
+
}
|
|
225
237
|
}
|
|
226
238
|
} catch { /* ignore parse errors */ }
|
|
227
239
|
}
|
|
@@ -229,6 +241,73 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
|
|
|
229
241
|
return { installed: false };
|
|
230
242
|
}
|
|
231
243
|
|
|
244
|
+
// Parse TOML to find MCP server entry without external library
|
|
245
|
+
function parseTomlMcpEntry(content: string, sectionKey: string, serverName: string): { found: boolean; entry?: { type?: string; url?: string } } {
|
|
246
|
+
const lines = content.split('\n');
|
|
247
|
+
const targetSection = `[${sectionKey}.${serverName}]`;
|
|
248
|
+
const genericSection = `[${sectionKey}]`;
|
|
249
|
+
|
|
250
|
+
let inTargetSection = false;
|
|
251
|
+
let inGenericSection = false;
|
|
252
|
+
let foundInGeneric = false;
|
|
253
|
+
let entry: { type?: string; url?: string } = {};
|
|
254
|
+
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const trimmed = line.trim();
|
|
257
|
+
|
|
258
|
+
// Check for section headers
|
|
259
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
260
|
+
// Save previous section result if we were in the target
|
|
261
|
+
if (inTargetSection && (entry.type || entry.url)) {
|
|
262
|
+
return { found: true, entry };
|
|
263
|
+
}
|
|
264
|
+
if (foundInGeneric && (entry.type || entry.url)) {
|
|
265
|
+
return { found: true, entry };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
inTargetSection = trimmed === targetSection;
|
|
269
|
+
inGenericSection = trimmed === genericSection;
|
|
270
|
+
foundInGeneric = false;
|
|
271
|
+
entry = {};
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Parse key-value pairs
|
|
276
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.+)$/);
|
|
277
|
+
if (match) {
|
|
278
|
+
const [, key, rawValue] = match;
|
|
279
|
+
// Remove quotes from value
|
|
280
|
+
const value = rawValue.replace(/^["'](.+)["']$/, '$1');
|
|
281
|
+
|
|
282
|
+
if (inTargetSection) {
|
|
283
|
+
if (key === 'type') entry.type = value;
|
|
284
|
+
if (key === 'url') entry.url = value;
|
|
285
|
+
} else if (inGenericSection && key === serverName) {
|
|
286
|
+
// Check if it's a table reference like mindos = { type = "stdio" }
|
|
287
|
+
const tableMatch = rawValue.match(/\{\s*type\s*=\s*["']([^"']+)["'].*?\}/);
|
|
288
|
+
if (tableMatch) {
|
|
289
|
+
entry.type = tableMatch[1];
|
|
290
|
+
}
|
|
291
|
+
const urlMatch = rawValue.match(/url\s*=\s*["']([^"']+)["']/);
|
|
292
|
+
if (urlMatch) {
|
|
293
|
+
entry.url = urlMatch[1];
|
|
294
|
+
}
|
|
295
|
+
foundInGeneric = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check at end of file
|
|
301
|
+
if (inTargetSection && (entry.type || entry.url)) {
|
|
302
|
+
return { found: true, entry };
|
|
303
|
+
}
|
|
304
|
+
if (foundInGeneric && (entry.type || entry.url)) {
|
|
305
|
+
return { found: true, entry };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { found: false };
|
|
309
|
+
}
|
|
310
|
+
|
|
232
311
|
/* ── Agent Presence Detection ──────────────────────────────────────────── */
|
|
233
312
|
|
|
234
313
|
export function detectAgentPresence(agentKey: string): boolean {
|
package/app/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"lint": "eslint",
|
|
11
11
|
"test": "vitest run"
|
|
12
12
|
},
|
|
13
|
-
|
|
13
|
+
"dependencies": {
|
|
14
14
|
"@base-ui/react": "^1.2.0",
|
|
15
15
|
"@codemirror/lang-markdown": "^6.5.0",
|
|
16
16
|
"@codemirror/state": "^6.5.4",
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"class-variance-authority": "^0.7.1",
|
|
35
35
|
"clsx": "^2.1.1",
|
|
36
36
|
"codemirror": "^6.0.2",
|
|
37
|
+
"extend": "^3.0.2",
|
|
37
38
|
"fuse.js": "^7.1.0",
|
|
38
39
|
"github-slugger": "^2.0.0",
|
|
39
40
|
"lucide-react": "^0.577.0",
|
|
41
|
+
"nanoid": "^5.1.0",
|
|
40
42
|
"next": "16.1.6",
|
|
41
43
|
"papaparse": "^5.5.3",
|
|
42
44
|
"pdfjs-dist": "^4.10.38",
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
"tailwind-merge": "^3.5.0",
|
|
53
55
|
"tiptap-markdown": "^0.9.0",
|
|
54
56
|
"tw-animate-css": "^1.4.0",
|
|
55
|
-
|
|
57
|
+
"zod": "^3.23.8"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@tailwindcss/postcss": "^4",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminilight/mindos",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.39",
|
|
4
4
|
"description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mindos",
|
|
@@ -22,10 +22,6 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"mindos": "bin/cli.js"
|
|
24
24
|
},
|
|
25
|
-
"workspaces": [
|
|
26
|
-
"app",
|
|
27
|
-
"mcp"
|
|
28
|
-
],
|
|
29
25
|
"files": [
|
|
30
26
|
"app/",
|
|
31
27
|
"mcp/",
|
package/scripts/release.sh
CHANGED
|
@@ -18,7 +18,21 @@ echo "🧪 Running tests..."
|
|
|
18
18
|
npm test
|
|
19
19
|
echo ""
|
|
20
20
|
|
|
21
|
-
# 3.
|
|
21
|
+
# 3. Verify Next.js build
|
|
22
|
+
echo "🔨 Verifying Next.js build..."
|
|
23
|
+
cd app
|
|
24
|
+
if npx next build 2>&1 | tail -5; then
|
|
25
|
+
echo " ✅ Next.js build succeeded"
|
|
26
|
+
else
|
|
27
|
+
echo "❌ Next.js build failed"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
cd ..
|
|
31
|
+
# Restore any files modified by next build (e.g. next-env.d.ts)
|
|
32
|
+
git checkout -- . 2>/dev/null || true
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# 4. Smoke test: pack → install in temp dir → verify CLI works
|
|
22
36
|
echo "🔍 Smoke testing package..."
|
|
23
37
|
SMOKE_DIR=$(mktemp -d)
|
|
24
38
|
TARBALL=$(npm pack --pack-destination "$SMOKE_DIR" 2>/dev/null | tail -1)
|
|
@@ -79,20 +93,20 @@ cd - >/dev/null
|
|
|
79
93
|
echo " 🟢 Smoke test passed"
|
|
80
94
|
echo ""
|
|
81
95
|
|
|
82
|
-
#
|
|
96
|
+
# 5. Bump version (creates commit + tag automatically)
|
|
83
97
|
echo "📦 Bumping version ($BUMP)..."
|
|
84
98
|
npm version "$BUMP" -m "%s"
|
|
85
99
|
VERSION="v$(node -p "require('./package.json').version")"
|
|
86
100
|
echo " Version: $VERSION"
|
|
87
101
|
echo ""
|
|
88
102
|
|
|
89
|
-
#
|
|
103
|
+
# 6. Push commit + tag
|
|
90
104
|
echo "🚀 Pushing to origin..."
|
|
91
105
|
git push origin main
|
|
92
106
|
git push origin "$VERSION"
|
|
93
107
|
echo ""
|
|
94
108
|
|
|
95
|
-
#
|
|
109
|
+
# 7. Wait for CI
|
|
96
110
|
# Flow: tag push → sync-to-mindos (syncs code + tag to public repo) → public repo publish-npm
|
|
97
111
|
if command -v gh &>/dev/null; then
|
|
98
112
|
echo "⏳ Waiting for sync → publish pipeline..."
|