@geminilight/mindos 0.5.41 → 0.5.42

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.
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { ChevronDown, ChevronRight, ExternalLink, Blocks, Zap } from 'lucide-react';
6
+ import PanelHeader from './PanelHeader';
7
+ import { useLocale } from '@/lib/LocaleContext';
8
+ import { useCases } from '@/components/explore/use-cases';
9
+ import { openAskModal } from '@/hooks/useAskModal';
10
+
11
+ interface DiscoverPanelProps {
12
+ active: boolean;
13
+ maximized?: boolean;
14
+ onMaximize?: () => void;
15
+ }
16
+
17
+ /** Collapsible section with count badge */
18
+ function Section({
19
+ icon,
20
+ title,
21
+ count,
22
+ defaultOpen = true,
23
+ children,
24
+ }: {
25
+ icon: React.ReactNode;
26
+ title: string;
27
+ count?: number;
28
+ defaultOpen?: boolean;
29
+ children: React.ReactNode;
30
+ }) {
31
+ const [open, setOpen] = useState(defaultOpen);
32
+ return (
33
+ <div>
34
+ <button
35
+ onClick={() => setOpen(v => !v)}
36
+ className="flex items-center gap-2 w-full px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
37
+ >
38
+ {open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
39
+ <span className="flex items-center gap-1.5">
40
+ {icon}
41
+ {title}
42
+ </span>
43
+ {count !== undefined && (
44
+ <span className="ml-auto text-2xs tabular-nums opacity-60">{count}</span>
45
+ )}
46
+ </button>
47
+ {open && <div className="pb-2">{children}</div>}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ /** Compact use case row for panel display */
53
+ function UseCaseRow({
54
+ icon,
55
+ title,
56
+ prompt,
57
+ tryLabel,
58
+ }: {
59
+ icon: string;
60
+ title: string;
61
+ prompt: string;
62
+ tryLabel: string;
63
+ }) {
64
+ return (
65
+ <div className="group flex items-center gap-2 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
66
+ <span className="text-sm leading-none shrink-0" suppressHydrationWarning>{icon}</span>
67
+ <span className="text-xs text-foreground truncate flex-1">{title}</span>
68
+ <button
69
+ onClick={() => openAskModal(prompt, 'user')}
70
+ className="opacity-0 group-hover:opacity-100 text-2xs px-2 py-0.5 rounded text-[var(--amber)] bg-[var(--amber-dim)] hover:opacity-80 transition-all duration-150 shrink-0"
71
+ >
72
+ {tryLabel}
73
+ </button>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ /** Coming soon placeholder */
79
+ function ComingSoonSection({
80
+ icon,
81
+ title,
82
+ description,
83
+ comingSoonLabel,
84
+ }: {
85
+ icon: React.ReactNode;
86
+ title: string;
87
+ description: string;
88
+ comingSoonLabel: string;
89
+ }) {
90
+ return (
91
+ <Section icon={icon} title={title} defaultOpen={false}>
92
+ <div className="px-4 py-3 mx-1">
93
+ <p className="text-xs text-muted-foreground leading-relaxed">{description}</p>
94
+ <span className="inline-block mt-2 text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
95
+ {comingSoonLabel}
96
+ </span>
97
+ </div>
98
+ </Section>
99
+ );
100
+ }
101
+
102
+ export default function DiscoverPanel({ active, maximized, onMaximize }: DiscoverPanelProps) {
103
+ const { t } = useLocale();
104
+ const d = t.panels.discover;
105
+ const e = t.explore;
106
+
107
+ /** Type-safe lookup for use case i18n */
108
+ const getUseCaseText = (id: string): { title: string; prompt: string } | undefined => {
109
+ const map: Record<string, { title: string; desc: string; prompt: string }> = {
110
+ c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
111
+ c6: e.c6, c7: e.c7, c8: e.c8, c9: e.c9,
112
+ };
113
+ return map[id];
114
+ };
115
+
116
+ return (
117
+ <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
118
+ <PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
119
+ <div className="flex-1 overflow-y-auto min-h-0">
120
+ {/* Use Cases */}
121
+ <Section icon={<span className="text-xs" suppressHydrationWarning>🎯</span>} title={d.useCases} count={useCases.length}>
122
+ <div className="flex flex-col">
123
+ {useCases.map(uc => {
124
+ const data = getUseCaseText(uc.id);
125
+ if (!data) return null;
126
+ return (
127
+ <UseCaseRow
128
+ key={uc.id}
129
+ icon={uc.icon}
130
+ title={data.title}
131
+ prompt={data.prompt}
132
+ tryLabel={d.tryIt}
133
+ />
134
+ );
135
+ })}
136
+ </div>
137
+ </Section>
138
+
139
+ <div className="mx-4 border-t border-border" />
140
+
141
+ {/* Plugin Market — Coming Soon */}
142
+ <ComingSoonSection
143
+ icon={<Blocks size={11} />}
144
+ title={d.pluginMarket}
145
+ description={d.pluginMarketDesc}
146
+ comingSoonLabel={d.comingSoon}
147
+ />
148
+
149
+ <div className="mx-4 border-t border-border" />
150
+
151
+ {/* Skill Market — Coming Soon */}
152
+ <ComingSoonSection
153
+ icon={<Zap size={11} />}
154
+ title={d.skillMarket}
155
+ description={d.skillMarketDesc}
156
+ comingSoonLabel={d.comingSoon}
157
+ />
158
+
159
+ {/* View all link */}
160
+ <div className="px-4 py-3 mt-2">
161
+ <Link
162
+ href="/explore"
163
+ className="inline-flex items-center gap-1.5 text-xs text-[var(--amber)] hover:opacity-80 transition-opacity"
164
+ >
165
+ {d.viewAll}
166
+ <ExternalLink size={11} />
167
+ </Link>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ );
172
+ }
@@ -32,6 +32,24 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
32
32
  const [contentWidth, setContentWidth] = useState('780px');
33
33
  const [dark, setDark] = useState(true);
34
34
 
35
+ // Update available badge on Update tab
36
+ const [hasUpdate, setHasUpdate] = useState(() => {
37
+ if (typeof window === 'undefined') return false;
38
+ const dismissed = localStorage.getItem('mindos_update_dismissed');
39
+ const latest = localStorage.getItem('mindos_update_latest');
40
+ return !!latest && latest !== dismissed;
41
+ });
42
+ useEffect(() => {
43
+ const onAvail = () => setHasUpdate(true);
44
+ const onDismiss = () => setHasUpdate(false);
45
+ window.addEventListener('mindos:update-available', onAvail);
46
+ window.addEventListener('mindos:update-dismissed', onDismiss);
47
+ return () => {
48
+ window.removeEventListener('mindos:update-available', onAvail);
49
+ window.removeEventListener('mindos:update-dismissed', onDismiss);
50
+ };
51
+ }, []);
52
+
35
53
  const isPanel = variant === 'panel';
36
54
 
37
55
  // Init data when becoming visible
@@ -131,13 +149,13 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
131
149
  const env = data?.envOverrides ?? {};
132
150
  const iconSize = isPanel ? 12 : 13;
133
151
 
134
- const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
152
+ const TABS: { id: Tab; label: string; icon: React.ReactNode; badge?: boolean }[] = [
135
153
  { id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
136
154
  { id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
137
155
  { id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
138
156
  { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
139
157
  { id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
140
- { id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} /> },
158
+ { id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
141
159
  ];
142
160
 
143
161
  const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
@@ -233,6 +251,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
233
251
  >
234
252
  {tabItem.icon}
235
253
  {tabItem.label}
254
+ {tabItem.badge && <span className="w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
236
255
  </button>
237
256
  ))}
238
257
  </div>
@@ -283,6 +302,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
283
302
  >
284
303
  {tabItem.icon}
285
304
  {tabItem.label}
305
+ {tabItem.badge && <span className="w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
286
306
  </button>
287
307
  ))}
288
308
  </div>
@@ -314,6 +334,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
314
334
  )}
315
335
  {tabItem.icon}
316
336
  {tabItem.label}
337
+ {tabItem.badge && <span className="ml-auto w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
317
338
  </button>
318
339
  ))}
319
340
  </nav>
@@ -15,12 +15,19 @@ const LocaleContext = createContext<LocaleContextValue>({
15
15
  t: messages['en'],
16
16
  });
17
17
 
18
+ /** Read locale from localStorage (canonical client source) */
18
19
  function getLocaleSnapshot(): Locale {
19
20
  const saved = localStorage.getItem('locale');
20
21
  return saved === 'zh' ? 'zh' : 'en';
21
22
  }
22
23
 
23
- export function LocaleProvider({ children }: { children: ReactNode }) {
24
+ interface LocaleProviderProps {
25
+ children: ReactNode;
26
+ /** Locale read from cookie on the server — ensures SSR matches client hydration */
27
+ ssrLocale?: Locale;
28
+ }
29
+
30
+ export function LocaleProvider({ children, ssrLocale = 'en' }: LocaleProviderProps) {
24
31
  const locale = useSyncExternalStore(
25
32
  (onStoreChange) => {
26
33
  const listener = () => onStoreChange();
@@ -28,11 +35,14 @@ export function LocaleProvider({ children }: { children: ReactNode }) {
28
35
  return () => window.removeEventListener('mindos-locale-change', listener);
29
36
  },
30
37
  getLocaleSnapshot,
31
- () => 'en' as Locale,
38
+ () => ssrLocale,
32
39
  );
33
40
 
34
41
  const setLocale = (l: Locale) => {
35
42
  localStorage.setItem('locale', l);
43
+ // Sync cookie so next SSR render matches
44
+ document.cookie = `locale=${l};path=/;max-age=31536000;SameSite=Lax`;
45
+ document.documentElement.lang = l === 'zh' ? 'zh' : 'en';
36
46
  window.dispatchEvent(new Event('mindos-locale-change'));
37
47
  };
38
48
 
@@ -46,8 +46,9 @@ export async function renameFileAction(oldPath: string, newName: string): Promis
46
46
  */
47
47
  export async function createSpaceAction(
48
48
  name: string,
49
- description: string
50
- ): Promise<{ success: boolean; error?: string }> {
49
+ description: string,
50
+ parentPath: string = ''
51
+ ): Promise<{ success: boolean; path?: string; error?: string }> {
51
52
  try {
52
53
  const trimmed = name.trim();
53
54
  if (!trimmed) return { success: false, error: 'Space name is required' };
@@ -55,15 +56,25 @@ export async function createSpaceAction(
55
56
  return { success: false, error: 'Space name must not contain path separators' };
56
57
  }
57
58
 
59
+ // Sanitize parentPath — reject traversal attempts
60
+ const cleanParent = parentPath.replace(/\/+$/, '').trim();
61
+ if (cleanParent.includes('..') || cleanParent.startsWith('/') || cleanParent.includes('\\')) {
62
+ return { success: false, error: 'Invalid parent path' };
63
+ }
64
+
65
+ // Build full path: parentPath + name
66
+ const prefix = cleanParent ? cleanParent + '/' : '';
67
+ const fullPath = `${prefix}${trimmed}`;
68
+
58
69
  // Strip emoji for clean title in README content
59
70
  const cleanName = trimmed.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '') || trimmed;
60
71
  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`;
72
+ const readmeContent = `# ${cleanName}\n\n${desc}\n\n## 📁 Structure\n\n\`\`\`bash\n${fullPath}/\n├── INSTRUCTION.md\n├── README.md\n└── (your files here)\n\`\`\`\n\n## 💡 Usage\n\n(Add usage guidelines for this space.)\n`;
62
73
 
63
74
  // createFile triggers scaffoldIfNewSpace → auto-generates INSTRUCTION.md
64
- createFile(`${trimmed}/README.md`, readmeContent);
75
+ createFile(`${fullPath}/README.md`, readmeContent);
65
76
  revalidatePath('/', 'layout');
66
- return { success: true };
77
+ return { success: true, path: fullPath };
67
78
  } catch (err) {
68
79
  const msg = err instanceof Error ? err.message : 'Failed to create space';
69
80
  // Make "already exists" error more user-friendly
@@ -16,14 +16,23 @@ export const en = {
16
16
  other: 'Other',
17
17
  newSpace: 'New Space',
18
18
  spaceName: 'Space name',
19
- spaceDescription: 'What is this space for?',
19
+ spaceDescription: 'Description',
20
+ spaceDescPlaceholder: 'Describe the purpose of this space',
21
+ spaceNameNoSlash: 'Name cannot contain / or \\',
22
+ spaceExists: 'A space with this name already exists',
23
+ spaceCreateFailed: 'Failed to create space',
24
+ noSpacesYet: 'No spaces yet. Create one to organize your knowledge.',
25
+ spaceLocation: 'Location',
26
+ rootLevel: 'Root',
27
+ optional: 'optional',
20
28
  createSpace: 'Create',
21
29
  cancelCreate: 'Cancel',
22
30
  continueEditing: 'Continue editing',
23
31
  newNote: 'New Notes',
24
- plugins: 'Plugins',
32
+ plugins: 'Extensions',
25
33
  showMore: 'Show more',
26
34
  showLess: 'Show less',
35
+ viewAll: 'View all',
27
36
  createToActivate: 'Create {file} to activate',
28
37
  shortcuts: {
29
38
  searchFiles: 'Search files',
@@ -47,6 +56,7 @@ export const en = {
47
56
  settingsTitle: 'Settings',
48
57
  plugins: 'Plugins',
49
58
  agents: 'Agents',
59
+ discover: 'Discover',
50
60
  syncLabel: 'Sync',
51
61
  collapseTitle: 'Collapse sidebar',
52
62
  expandTitle: 'Expand sidebar',
@@ -143,6 +153,18 @@ export const en = {
143
153
  noFile: 'Entry file not found',
144
154
  createFile: 'Create {file} to activate',
145
155
  },
156
+ discover: {
157
+ title: 'Discover',
158
+ useCases: 'Use Cases',
159
+ useCasesDesc: 'Try MindOS scenarios hands-on',
160
+ pluginMarket: 'Plugin Market',
161
+ pluginMarketDesc: 'Extend how files are rendered and edited',
162
+ skillMarket: 'Skill Market',
163
+ skillMarketDesc: 'Add new abilities to your AI agents',
164
+ comingSoon: 'Coming soon',
165
+ viewAll: 'View all use cases',
166
+ tryIt: 'Try',
167
+ },
146
168
  },
147
169
  shortcutPanel: {
148
170
  title: 'Keyboard Shortcuts',
@@ -175,6 +197,10 @@ export const en = {
175
197
  fileCount: (n: number) => `${n} files`,
176
198
  newFile: 'New file',
177
199
  },
200
+ view: {
201
+ saveDirectory: 'Directory',
202
+ saveFileName: 'File name',
203
+ },
178
204
  findInPage: {
179
205
  placeholder: 'Find in document…',
180
206
  matchCount: (current: number, total: number) => `${current} of ${total}`,
@@ -41,14 +41,23 @@ export const zh = {
41
41
  other: '其他',
42
42
  newSpace: '新建空间',
43
43
  spaceName: '空间名称',
44
- spaceDescription: '这个空间用来做什么?',
44
+ spaceDescription: '描述',
45
+ spaceDescPlaceholder: '描述这个空间的用途',
46
+ spaceNameNoSlash: '名称不能包含 / 或 \\',
47
+ spaceExists: '已存在同名空间',
48
+ spaceCreateFailed: '创建空间失败',
49
+ noSpacesYet: '还没有空间,创建一个来整理你的知识吧。',
50
+ spaceLocation: '位置',
51
+ rootLevel: '根目录',
52
+ optional: '可选',
45
53
  createSpace: '创建',
46
54
  cancelCreate: '取消',
47
55
  continueEditing: '继续编辑',
48
56
  newNote: '新建笔记',
49
- plugins: '插件',
57
+ plugins: '插件扩展',
50
58
  showMore: '查看更多',
51
59
  showLess: '收起',
60
+ viewAll: '查看全部',
52
61
  createToActivate: '创建 {file} 以启用此插件',
53
62
  shortcuts: {
54
63
  searchFiles: '搜索文件',
@@ -72,6 +81,7 @@ export const zh = {
72
81
  settingsTitle: '设置',
73
82
  plugins: '插件',
74
83
  agents: '智能体',
84
+ discover: '探索',
75
85
  syncLabel: '同步',
76
86
  collapseTitle: '收起侧栏',
77
87
  expandTitle: '展开侧栏',
@@ -168,6 +178,18 @@ export const zh = {
168
178
  noFile: '入口文件不存在',
169
179
  createFile: '创建 {file} 以激活',
170
180
  },
181
+ discover: {
182
+ title: '探索',
183
+ useCases: '使用案例',
184
+ useCasesDesc: '动手体验 MindOS 的典型场景',
185
+ pluginMarket: '插件市场',
186
+ pluginMarketDesc: '扩展文件的渲染和编辑方式',
187
+ skillMarket: '技能市场',
188
+ skillMarketDesc: '为 AI 智能体添加新能力',
189
+ comingSoon: '即将推出',
190
+ viewAll: '查看所有使用案例',
191
+ tryIt: '试试',
192
+ },
171
193
  },
172
194
  shortcutPanel: {
173
195
  title: '快捷键',
@@ -200,6 +222,10 @@ export const zh = {
200
222
  fileCount: (n: number) => `${n} 个文件`,
201
223
  newFile: '新建文件',
202
224
  },
225
+ view: {
226
+ saveDirectory: '目录',
227
+ saveFileName: '文件名',
228
+ },
203
229
  findInPage: {
204
230
  placeholder: '在文档中查找…',
205
231
  matchCount: (current: number, total: number) => `${current} / ${total}`,
package/app/package.json CHANGED
@@ -8,7 +8,8 @@
8
8
  "build": "next build",
9
9
  "start": "next start -p ${MINDOS_WEB_PORT:-3456}",
10
10
  "lint": "eslint",
11
- "test": "vitest run"
11
+ "test": "vitest run",
12
+ "postinstall": "node ../scripts/fix-postcss-deps.cjs"
12
13
  },
13
14
  "dependencies": {
14
15
  "@base-ui/react": "^1.2.0",
package/bin/cli.js CHANGED
@@ -708,6 +708,18 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
708
708
  process.exit(1);
709
709
  }
710
710
  if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
711
+
712
+ // Silently update installed skills to match the new package
713
+ try {
714
+ const newRoot = getUpdatedRoot();
715
+ const { checkSkillVersions, updateSkill } = await import('./lib/skill-check.js');
716
+ const mismatches = checkSkillVersions(newRoot);
717
+ for (const m of mismatches) {
718
+ updateSkill(m.bundledPath, m.installPath);
719
+ console.log(` ${green('✓')} ${dim(`Skill ${m.name}: v${m.installed} → v${m.bundled}`)}`);
720
+ }
721
+ } catch { /* best-effort */ }
722
+
711
723
  const newVersion = (() => {
712
724
  try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
713
725
  })();
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync, copyFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { createInterface } from 'node:readline';
5
+ import { ROOT } from './constants.js';
6
+ import { bold, dim, cyan, green, yellow, isTTY } from './colors.js';
7
+
8
+ const SKILLS = ['mindos', 'mindos-zh'];
9
+ const INSTALLED_BASE = resolve(homedir(), '.agents', 'skills');
10
+
11
+ /**
12
+ * Simple semver "a > b" comparison (major.minor.patch only).
13
+ * Intentionally inlined (same as update-check.js) to keep this module
14
+ * self-contained — no cross-module dependency for a 10-line function.
15
+ */
16
+ function semverGt(a, b) {
17
+ const pa = a.split('.').map(Number);
18
+ const pb = b.split('.').map(Number);
19
+ for (let i = 0; i < 3; i++) {
20
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
21
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ /**
27
+ * Extract version from `<!-- version: X.Y.Z -->` comment in a file.
28
+ * Returns null if file doesn't exist or has no version tag.
29
+ */
30
+ export function extractSkillVersion(filePath) {
31
+ try {
32
+ const content = readFileSync(filePath, 'utf-8');
33
+ const match = content.match(/<!--\s*version:\s*([\d.]+)\s*-->/);
34
+ return match ? match[1] : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Compare installed vs bundled skill versions.
42
+ * @param {string} [root] — package root to read bundled skills from.
43
+ * Defaults to the static ROOT (fine for startup). Pass the post-update
44
+ * root when called from `mindos update` so we read the NEW package's skills.
45
+ * Returns array of mismatches where bundled > installed.
46
+ */
47
+ export function checkSkillVersions(root) {
48
+ const base = root || ROOT;
49
+ const mismatches = [];
50
+ for (const name of SKILLS) {
51
+ const installPath = resolve(INSTALLED_BASE, name, 'SKILL.md');
52
+ const bundledPath = resolve(base, 'skills', name, 'SKILL.md');
53
+
54
+ if (!existsSync(installPath)) continue;
55
+ if (!existsSync(bundledPath)) continue;
56
+
57
+ const installed = extractSkillVersion(installPath);
58
+ const bundled = extractSkillVersion(bundledPath);
59
+
60
+ if (!installed || !bundled) continue;
61
+ if (semverGt(bundled, installed)) {
62
+ mismatches.push({ name, installed, bundled, installPath, bundledPath });
63
+ }
64
+ }
65
+ return mismatches;
66
+ }
67
+
68
+ /**
69
+ * Copy bundled SKILL.md over the installed version.
70
+ */
71
+ export function updateSkill(bundledPath, installPath) {
72
+ copyFileSync(bundledPath, installPath);
73
+ }
74
+
75
+ /**
76
+ * Print skill update hints and optionally prompt user to update.
77
+ *
78
+ * - TTY + not daemon: interactive readline prompt (default Y)
79
+ * - Non-TTY / daemon / MINDOS_NO_SKILL_CHECK=1: one-line hint, no block
80
+ */
81
+ export async function promptSkillUpdate(mismatches) {
82
+ if (!mismatches || mismatches.length === 0) return;
83
+
84
+ // Print mismatch info
85
+ for (const m of mismatches) {
86
+ console.log(`\n ${yellow('⬆')} Skill ${bold(m.name)}: ${dim(`v${m.installed}`)} → ${cyan(`v${m.bundled}`)}`);
87
+ }
88
+
89
+ // Non-interactive mode: just print hint
90
+ if (!isTTY || process.env.LAUNCHED_BY_LAUNCHD === '1' || process.env.INVOCATION_ID) {
91
+ console.log(` ${dim('Run `mindos start` in a terminal to update interactively.')}`);
92
+ return;
93
+ }
94
+
95
+ // Interactive prompt (10s timeout to avoid blocking startup indefinitely)
96
+ return new Promise((res) => {
97
+ let done = false;
98
+ const finish = () => { if (!done) { done = true; res(); } };
99
+
100
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
101
+ const timer = setTimeout(() => { rl.close(); finish(); }, 10_000);
102
+
103
+ rl.on('close', finish); // handles broken pipe / EOF
104
+
105
+ rl.question(` Update skill${mismatches.length > 1 ? 's' : ''}? ${dim('(Y/n)')} `, (answer) => {
106
+ clearTimeout(timer);
107
+ rl.close();
108
+ const yes = !answer || answer.trim().toLowerCase() !== 'n';
109
+ if (yes) {
110
+ for (const m of mismatches) {
111
+ try {
112
+ updateSkill(m.bundledPath, m.installPath);
113
+ console.log(` ${green('✓')} ${dim(`Updated ${m.name} → v${m.bundled}`)}`);
114
+ } catch (err) {
115
+ console.log(` ${yellow('!')} ${dim(`Failed to update ${m.name}: ${err.message}`)}`);
116
+ }
117
+ }
118
+ }
119
+ finish();
120
+ });
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Main entry: check + prompt. Best-effort, never throws.
126
+ */
127
+ export async function runSkillCheck() {
128
+ if (process.env.MINDOS_NO_SKILL_CHECK === '1') return;
129
+ try {
130
+ const mismatches = checkSkillVersions();
131
+ await promptSkillUpdate(mismatches);
132
+ } catch { /* best-effort, don't block startup */ }
133
+ }
@@ -4,6 +4,7 @@ import { CONFIG_PATH } from './constants.js';
4
4
  import { bold, dim, cyan, green, yellow } from './colors.js';
5
5
  import { getSyncStatus } from './sync.js';
6
6
  import { checkForUpdate, printUpdateHint } from './update-check.js';
7
+ import { runSkillCheck } from './skill-check.js';
7
8
 
8
9
  export function getLocalIP() {
9
10
  try {
@@ -71,5 +72,8 @@ export async function printStartupInfo(webPort, mcpPort) {
71
72
  ]);
72
73
  if (latestVersion) printUpdateHint(latestVersion);
73
74
 
75
+ // Skill version check (best-effort, non-blocking)
76
+ await runSkillCheck();
77
+
74
78
  console.log(`${'─'.repeat(53)}\n`);
75
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
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",
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fix nested postcss dependencies inside next/node_modules.
3
+ *
4
+ * Next.js 16 bundles postcss@8.4.31 which depends on nanoid@^3,
5
+ * picocolors, and source-map-js. When the app's top-level nanoid
6
+ * is v5 (major mismatch), npm's hoisting fails to place nanoid@3
7
+ * where postcss can find it. This script installs the missing
8
+ * sub-dependencies directly into postcss's node_modules.
9
+ *
10
+ * Runs as postinstall — skips silently if postcss is already OK
11
+ * or if next/node_modules/postcss doesn't exist.
12
+ */
13
+
14
+ const { existsSync } = require('fs');
15
+ const { join } = require('path');
16
+ const { execSync } = require('child_process');
17
+
18
+ const postcssDir = join('node_modules', 'next', 'node_modules', 'postcss');
19
+ const depsDir = join(postcssDir, 'node_modules');
20
+
21
+ if (existsSync(postcssDir) && !existsSync(depsDir)) {
22
+ try {
23
+ execSync('npm install --no-save --install-strategy=nested', {
24
+ cwd: postcssDir,
25
+ stdio: 'ignore',
26
+ });
27
+ } catch {
28
+ // Best-effort — build will report the real error if deps are still missing
29
+ }
30
+ }