@geminilight/mindos 0.5.2 → 0.5.5

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.
@@ -28,6 +28,7 @@ interface AgentInfo {
28
28
  configPath?: string;
29
29
  hasProjectScope: boolean;
30
30
  hasGlobalScope: boolean;
31
+ preferredTransport: 'stdio' | 'http';
31
32
  }
32
33
 
33
34
  interface SkillInfo {
@@ -133,13 +134,18 @@ function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
133
134
  function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; onRefresh: () => void }) {
134
135
  const m = t.settings?.mcp;
135
136
  const [selected, setSelected] = useState<Set<string>>(new Set());
136
- const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
137
+ const [transport, setTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
137
138
  const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
138
139
  const [httpToken, setHttpToken] = useState('');
139
140
  const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
140
141
  const [installing, setInstalling] = useState(false);
141
142
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
142
143
 
144
+ const getEffectiveTransport = (agent: AgentInfo) => {
145
+ if (transport === 'auto') return agent.preferredTransport;
146
+ return transport;
147
+ };
148
+
143
149
  const toggle = (key: string) => {
144
150
  setSelected(prev => {
145
151
  const next = new Set(prev);
@@ -154,12 +160,21 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
154
160
  setMessage(null);
155
161
  try {
156
162
  const payload = {
157
- agents: [...selected].map(key => ({
158
- key,
159
- scope: scopes[key] || (agents.find(a => a.key === key)?.hasProjectScope ? 'project' : 'global'),
160
- })),
163
+ agents: [...selected].map(key => {
164
+ const agent = agents.find(a => a.key === key);
165
+ const effectiveTransport = transport === 'auto'
166
+ ? (agent?.preferredTransport || 'stdio')
167
+ : transport;
168
+ return {
169
+ key,
170
+ scope: scopes[key] || (agents.find(a => a.key === key)?.hasProjectScope ? 'project' : 'global'),
171
+ transport: effectiveTransport,
172
+ };
173
+ }),
161
174
  transport,
162
175
  ...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
176
+ // For auto mode, pass http settings for agents that need it
177
+ ...(transport === 'auto' ? { url: httpUrl, token: httpToken } : {}),
163
178
  };
164
179
  const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
165
180
  method: 'POST',
@@ -183,6 +198,12 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
183
198
  }
184
199
  };
185
200
 
201
+ // Show http fields if transport is 'http', or 'auto' with any http-preferred agent selected
202
+ const showHttpFields = transport === 'http' || (transport === 'auto' && [...selected].some(key => {
203
+ const agent = agents.find(a => a.key === key);
204
+ return agent?.preferredTransport === 'http';
205
+ }));
206
+
186
207
  return (
187
208
  <div className="space-y-3">
188
209
  <SectionLabel>{m?.agentsTitle ?? 'Agent Configuration'}</SectionLabel>
@@ -198,6 +219,10 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
198
219
  className="rounded border-border accent-amber-500"
199
220
  />
200
221
  <span className="w-28 shrink-0 text-xs">{agent.name}</span>
222
+ <span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
223
+ style={{ background: 'rgba(100,100,120,0.08)' }}>
224
+ {getEffectiveTransport(agent)}
225
+ </span>
201
226
  {agent.installed ? (
202
227
  <>
203
228
  <span className="text-[10px] px-1.5 py-0.5 rounded bg-green-500/15 text-green-500 font-mono">
@@ -225,6 +250,16 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
225
250
 
226
251
  {/* Transport selector */}
227
252
  <div className="flex items-center gap-4 text-xs pt-1">
253
+ <label className="flex items-center gap-1.5 cursor-pointer">
254
+ <input
255
+ type="radio"
256
+ name="transport"
257
+ checked={transport === 'auto'}
258
+ onChange={() => setTransport('auto')}
259
+ className="accent-amber-500"
260
+ />
261
+ {m?.transportAuto ?? 'auto (recommended)'}
262
+ </label>
228
263
  <label className="flex items-center gap-1.5 cursor-pointer">
229
264
  <input
230
265
  type="radio"
@@ -233,7 +268,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
233
268
  onChange={() => setTransport('stdio')}
234
269
  className="accent-amber-500"
235
270
  />
236
- {m?.transportStdio ?? 'stdio (recommended)'}
271
+ {m?.transportStdio ?? 'stdio'}
237
272
  </label>
238
273
  <label className="flex items-center gap-1.5 cursor-pointer">
239
274
  <input
@@ -248,7 +283,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
248
283
  </div>
249
284
 
250
285
  {/* HTTP settings */}
251
- {transport === 'http' && (
286
+ {showHttpFields && (
252
287
  <div className="space-y-2 pl-5 text-xs">
253
288
  <div className="space-y-1">
254
289
  <label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
@@ -379,6 +414,49 @@ function SkillsSection({ t }: { t: any }) {
379
414
  <div className="space-y-3">
380
415
  <SectionLabel>{m?.skillsTitle ?? 'Skills'}</SectionLabel>
381
416
 
417
+ {/* Skill language switcher */}
418
+ {(() => {
419
+ const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
420
+ const currentLang = mindosEnabled ? 'en' : 'zh';
421
+ const handleLangSwitch = async (lang: 'en' | 'zh') => {
422
+ if (lang === currentLang) return;
423
+ if (lang === 'en') {
424
+ await handleToggle('mindos', true);
425
+ await handleToggle('mindos-zh', false);
426
+ } else {
427
+ await handleToggle('mindos-zh', true);
428
+ await handleToggle('mindos', false);
429
+ }
430
+ };
431
+ return (
432
+ <div className="flex items-center gap-2 text-xs">
433
+ <span className="text-muted-foreground">{m?.skillLanguage ?? 'Skill Language'}</span>
434
+ <div className="flex rounded-md border border-border overflow-hidden">
435
+ <button
436
+ onClick={() => handleLangSwitch('en')}
437
+ className={`px-2.5 py-1 text-xs transition-colors ${
438
+ currentLang === 'en'
439
+ ? 'bg-amber-500/15 text-amber-600 font-medium'
440
+ : 'text-muted-foreground hover:bg-muted'
441
+ }`}
442
+ >
443
+ {m?.skillLangEn ?? 'English'}
444
+ </button>
445
+ <button
446
+ onClick={() => handleLangSwitch('zh')}
447
+ className={`px-2.5 py-1 text-xs transition-colors border-l border-border ${
448
+ currentLang === 'zh'
449
+ ? 'bg-amber-500/15 text-amber-600 font-medium'
450
+ : 'text-muted-foreground hover:bg-muted'
451
+ }`}
452
+ >
453
+ {m?.skillLangZh ?? '中文'}
454
+ </button>
455
+ </div>
456
+ </div>
457
+ );
458
+ })()}
459
+
382
460
  {skills.map(skill => (
383
461
  <div key={skill.name} className="border border-border rounded-lg overflow-hidden">
384
462
  <div
@@ -19,7 +19,8 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
19
19
  ) : (
20
20
  <div className="flex flex-col gap-3">
21
21
  {getAllRenderers().map(renderer => {
22
- const enabled = pluginStates[renderer.id] ?? true;
22
+ const isCore = !!renderer.core;
23
+ const enabled = isCore ? true : (pluginStates[renderer.id] ?? true);
23
24
  return (
24
25
  <div
25
26
  key={renderer.id}
@@ -31,7 +32,12 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
31
32
  <div className="min-w-0">
32
33
  <div className="flex items-center gap-2 flex-wrap">
33
34
  <span className="text-sm font-medium text-foreground">{renderer.name}</span>
34
- {renderer.builtin && (
35
+ {isCore && (
36
+ <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-600/15 text-amber-600 font-mono">
37
+ core
38
+ </span>
39
+ )}
40
+ {renderer.builtin && !isCore && (
35
41
  <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono">
36
42
  {t.settings.plugins.builtinBadge}
37
43
  </span>
@@ -51,21 +57,30 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
51
57
  </div>
52
58
  </div>
53
59
 
54
- <button
55
- onClick={() => {
56
- const next = !enabled;
57
- setRendererEnabled(renderer.id, next);
58
- setPluginStates(s => ({ ...s, [renderer.id]: next }));
59
- }}
60
- role="switch"
61
- aria-checked={enabled}
62
- className={`shrink-0 w-9 h-5 rounded-full transition-colors relative ${enabled ? 'bg-amber-600' : 'bg-muted border border-border'}`}
63
- title={enabled ? t.settings.plugins.enabled : t.settings.plugins.disabled}
64
- >
60
+ {isCore ? (
65
61
  <span
66
- className={`absolute top-[3px] w-3.5 h-3.5 rounded-full shadow-sm transition-all ${enabled ? 'left-[18px] bg-white' : 'left-[3px] bg-muted-foreground/50'}`}
67
- />
68
- </button>
62
+ className="shrink-0 w-9 h-5 rounded-full bg-amber-600 relative cursor-not-allowed opacity-60"
63
+ title={t.settings.plugins.coreHint ?? 'Core renderer — always enabled'}
64
+ >
65
+ <span className="absolute top-[3px] left-[18px] w-3.5 h-3.5 rounded-full shadow-sm bg-white" />
66
+ </span>
67
+ ) : (
68
+ <button
69
+ onClick={() => {
70
+ const next = !enabled;
71
+ setRendererEnabled(renderer.id, next);
72
+ setPluginStates(s => ({ ...s, [renderer.id]: next }));
73
+ }}
74
+ role="switch"
75
+ aria-checked={enabled}
76
+ className={`shrink-0 w-9 h-5 rounded-full transition-colors relative ${enabled ? 'bg-amber-600' : 'bg-muted border border-border'}`}
77
+ title={enabled ? t.settings.plugins.enabled : t.settings.plugins.disabled}
78
+ >
79
+ <span
80
+ className={`absolute top-[3px] w-3.5 h-3.5 rounded-full shadow-sm transition-all ${enabled ? 'left-[18px] bg-white' : 'left-[3px] bg-muted-foreground/50'}`}
81
+ />
82
+ </button>
83
+ )}
69
84
  </div>
70
85
  </div>
71
86
  );
package/app/lib/i18n.ts CHANGED
@@ -171,6 +171,7 @@ export const messages = {
171
171
  enabled: 'Enabled',
172
172
  disabled: 'Disabled',
173
173
  matchHint: 'Auto-activates on',
174
+ coreHint: 'Core renderer — always enabled',
174
175
  noPlugins: 'No renderers installed.',
175
176
  comingSoon: 'Plugin marketplace coming soon.',
176
177
  },
@@ -198,6 +199,7 @@ export const messages = {
198
199
  notInstalled: 'Not installed',
199
200
  transportStdio: 'stdio (recommended)',
200
201
  transportHttp: 'http',
202
+ transportAuto: 'auto (recommended)',
201
203
  httpUrl: 'MCP URL',
202
204
  httpToken: 'Auth Token',
203
205
  installSelected: 'Install Selected',
@@ -221,8 +223,10 @@ export const messages = {
221
223
  skillContent: 'Content',
222
224
  skillNameConflict: 'A skill with this name already exists',
223
225
  skillDeleteConfirm: (name: string) => `Delete skill "${name}"? This cannot be undone.`,
224
- },
225
- save: 'Save',
226
+ skillLanguage: 'Skill Language',
227
+ skillLangEn: 'English',
228
+ skillLangZh: '中文',
229
+ }, save: 'Save',
226
230
  saved: 'Saved',
227
231
  saveFailed: 'Save failed',
228
232
  reconfigure: 'Reconfigure',
@@ -292,7 +296,7 @@ export const messages = {
292
296
  webPort: 'Web UI port',
293
297
  mcpPort: 'MCP server port',
294
298
  portHint: 'Valid range: 1024–65535',
295
- portRestartWarning: 'Port changes take effect after server restart.',
299
+ portRestartWarning: 'The service will start on these ports after setup completes.',
296
300
  portInUse: (p: number) => `Port ${p} is already in use.`,
297
301
  portSuggest: (p: number) => `Use ${p}`,
298
302
  portChecking: 'Checking…',
@@ -324,6 +328,17 @@ export const messages = {
324
328
  agentStatusOk: 'configured',
325
329
  agentStatusError: 'failed',
326
330
  agentInstalling: 'Configuring…',
331
+ agentTransportAuto: 'Auto (recommended)',
332
+ agentTransportLabel: 'Transport',
333
+ agentVerified: 'verified',
334
+ agentUnverified: 'unverified',
335
+ agentVerifyNote: 'stdio agents are verified after restart',
336
+ // Skill auto-install
337
+ skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
338
+ skillLabel: 'Skill',
339
+ skillInstalling: 'Installing skill…',
340
+ skillInstalled: 'Skill installed',
341
+ skillFailed: 'Skill install failed',
327
342
  // Step 2 — AI skip card
328
343
  aiSkipTitle: 'Skip for now',
329
344
  aiSkipDesc: 'You can add an API key later in Settings → AI.',
@@ -525,6 +540,7 @@ export const messages = {
525
540
  enabled: '已启用',
526
541
  disabled: '已禁用',
527
542
  matchHint: '自动匹配',
543
+ coreHint: '核心渲染器 — 始终启用',
528
544
  noPlugins: '暂无渲染器。',
529
545
  comingSoon: '插件市场即将上线。',
530
546
  },
@@ -552,6 +568,7 @@ export const messages = {
552
568
  notInstalled: '未安装',
553
569
  transportStdio: 'stdio(推荐)',
554
570
  transportHttp: 'http',
571
+ transportAuto: '自动(推荐)',
555
572
  httpUrl: 'MCP URL',
556
573
  httpToken: '认证 Token',
557
574
  installSelected: '安装选中',
@@ -575,8 +592,10 @@ export const messages = {
575
592
  skillContent: '内容',
576
593
  skillNameConflict: '同名 skill 已存在',
577
594
  skillDeleteConfirm: (name: string) => `确定删除「${name}」?此操作不可撤销。`,
578
- },
579
- save: '保存',
595
+ skillLanguage: 'Skill 语言',
596
+ skillLangEn: 'English',
597
+ skillLangZh: '中文',
598
+ }, save: '保存',
580
599
  saved: '已保存',
581
600
  saveFailed: '保存失败',
582
601
  reconfigure: '重新配置',
@@ -646,7 +665,7 @@ export const messages = {
646
665
  webPort: 'Web UI 端口',
647
666
  mcpPort: 'MCP 服务端口',
648
667
  portHint: '有效范围:1024–65535',
649
- portRestartWarning: '端口修改需重启服务后生效。',
668
+ portRestartWarning: '完成配置后,服务将以这些端口启动。',
650
669
  portInUse: (p: number) => `端口 ${p} 已被占用。`,
651
670
  portSuggest: (p: number) => `使用 ${p}`,
652
671
  portChecking: '检测中…',
@@ -678,6 +697,17 @@ export const messages = {
678
697
  agentStatusOk: '已配置',
679
698
  agentStatusError: '失败',
680
699
  agentInstalling: '配置中…',
700
+ agentTransportAuto: '自动(推荐)',
701
+ agentTransportLabel: '传输方式',
702
+ agentVerified: '已验证',
703
+ agentUnverified: '未验证',
704
+ agentVerifyNote: 'stdio agent 需重启后验证',
705
+ // Skill auto-install
706
+ skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
707
+ skillLabel: 'Skill',
708
+ skillInstalling: '正在安装 Skill…',
709
+ skillInstalled: 'Skill 已安装',
710
+ skillFailed: 'Skill 安装失败',
681
711
  // Step 2 — AI skip card
682
712
  aiSkipTitle: '暂时跳过',
683
713
  aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
@@ -11,18 +11,19 @@ export interface AgentDef {
11
11
  project: string | null;
12
12
  global: string;
13
13
  key: string;
14
+ preferredTransport: 'stdio' | 'http';
14
15
  }
15
16
 
16
17
  export const MCP_AGENTS: Record<string, AgentDef> = {
17
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
18
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
19
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
20
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
21
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
22
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
23
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
24
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
25
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
18
+ 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
19
+ 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers', preferredTransport: 'http' },
20
+ 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
21
+ 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
22
+ 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
23
+ 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
24
+ 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
25
+ 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
+ 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
27
  };
27
28
 
28
29
  export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
@@ -15,6 +15,7 @@ export interface RendererDefinition {
15
15
  icon: string; // emoji or short string
16
16
  tags: string[];
17
17
  builtin: boolean; // true = ships with MindOS; false = user-installed (future)
18
+ core?: boolean; // true = default renderer for a file type, cannot be disabled by user
18
19
  entryPath?: string; // canonical entry file shown on home page (e.g. 'TODO.md')
19
20
  match: (ctx: Pick<RendererContext, 'filePath' | 'extension'>) => boolean;
20
21
  // Provide either `component` (eager) or `load` (lazy). Prefer `load` for code-splitting.
@@ -38,6 +39,9 @@ export function loadDisabledState() {
38
39
  }
39
40
 
40
41
  export function setRendererEnabled(id: string, enabled: boolean) {
42
+ // Core renderers cannot be disabled
43
+ const def = registry.find(r => r.id === id);
44
+ if (def?.core) return;
41
45
  if (enabled) {
42
46
  _disabledIds.delete(id);
43
47
  } else {
@@ -49,6 +53,9 @@ export function setRendererEnabled(id: string, enabled: boolean) {
49
53
  }
50
54
 
51
55
  export function isRendererEnabled(id: string): boolean {
56
+ // Core renderers cannot be disabled
57
+ const def = registry.find(r => r.id === id);
58
+ if (def?.core) return true;
52
59
  return !_disabledIds.has(id);
53
60
  }
54
61
 
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore, useCallback, useRef } from 'react';
4
+
5
+ /**
6
+ * Unified per-file state hook for renderers.
7
+ *
8
+ * Each renderer stores its state under a namespaced localStorage key:
9
+ * `mindos-renderer:{rendererId}:{filePath}`
10
+ *
11
+ * Usage:
12
+ * const [cfg, setCfg] = useRendererState<CsvConfig>('csv', filePath, defaultCfg);
13
+ *
14
+ * The state is reactive — changes from other tabs/windows or from other
15
+ * components calling the setter will trigger a re-render via
16
+ * `useSyncExternalStore`.
17
+ */
18
+
19
+ const CHANGE_EVENT = 'mindos-renderer-state-change';
20
+
21
+ function storageKey(rendererId: string, filePath: string): string {
22
+ return `mindos-renderer:${rendererId}:${filePath}`;
23
+ }
24
+
25
+ export function useRendererState<T>(
26
+ rendererId: string,
27
+ filePath: string,
28
+ defaultValue: T,
29
+ ): [T, (value: T | ((prev: T) => T)) => void] {
30
+ const key = storageKey(rendererId, filePath);
31
+
32
+ // Cache parsed value to maintain referential stability for useSyncExternalStore.
33
+ // Without this, JSON.parse returns a new object on every getSnapshot call,
34
+ // causing Object.is to fail → infinite re-renders for non-primitive types.
35
+ const cacheRef = useRef<{ key: string; raw: string | null; parsed: T }>({ key, raw: null, parsed: defaultValue });
36
+
37
+ // Reset cache when key changes (different file or different renderer)
38
+ if (cacheRef.current.key !== key) {
39
+ cacheRef.current = { key, raw: null, parsed: defaultValue };
40
+ }
41
+
42
+ const state = useSyncExternalStore(
43
+ (onStoreChange) => {
44
+ const listener = () => onStoreChange();
45
+ window.addEventListener('storage', listener);
46
+ window.addEventListener(CHANGE_EVENT, listener);
47
+ return () => {
48
+ window.removeEventListener('storage', listener);
49
+ window.removeEventListener(CHANGE_EVENT, listener);
50
+ };
51
+ },
52
+ () => {
53
+ try {
54
+ const raw = localStorage.getItem(key);
55
+ if (raw === cacheRef.current.raw) return cacheRef.current.parsed;
56
+ if (raw === null) {
57
+ cacheRef.current = { key, raw: null, parsed: defaultValue };
58
+ return defaultValue;
59
+ }
60
+ const parsed = JSON.parse(raw) as T;
61
+ cacheRef.current = { key, raw, parsed };
62
+ return parsed;
63
+ } catch {
64
+ return defaultValue;
65
+ }
66
+ },
67
+ () => defaultValue,
68
+ );
69
+
70
+ const setState = useCallback(
71
+ (value: T | ((prev: T) => T)) => {
72
+ try {
73
+ const current = (() => {
74
+ try {
75
+ const raw = localStorage.getItem(key);
76
+ return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
77
+ } catch {
78
+ return defaultValue;
79
+ }
80
+ })();
81
+ const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;
82
+ const serialized = JSON.stringify(next);
83
+ localStorage.setItem(key, serialized);
84
+ // Update cache eagerly so the next getSnapshot returns stable ref
85
+ cacheRef.current = { key, raw: serialized, parsed: next };
86
+ } catch { /* ignore quota errors */ }
87
+ window.dispatchEvent(new Event(CHANGE_EVENT));
88
+ },
89
+ [key, defaultValue],
90
+ );
91
+
92
+ return [state, setState];
93
+ }
94
+
95
+ /**
96
+ * Non-hook helpers for reading/writing renderer state outside React components.
97
+ */
98
+ export function getRendererState<T>(rendererId: string, filePath: string, defaultValue: T): T {
99
+ if (typeof window === 'undefined') return defaultValue;
100
+ try {
101
+ const raw = localStorage.getItem(storageKey(rendererId, filePath));
102
+ return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
103
+ } catch {
104
+ return defaultValue;
105
+ }
106
+ }
107
+
108
+ export function setRendererState<T>(rendererId: string, filePath: string, value: T): void {
109
+ if (typeof window === 'undefined') return;
110
+ try {
111
+ localStorage.setItem(storageKey(rendererId, filePath), JSON.stringify(value));
112
+ } catch { /* ignore */ }
113
+ window.dispatchEvent(new Event(CHANGE_EVENT));
114
+ }
package/bin/cli.js CHANGED
@@ -550,7 +550,9 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
550
550
  await runGatewayCommand('stop');
551
551
  await runGatewayCommand('install');
552
552
  await runGatewayCommand('start');
553
- const webPort = process.env.MINDOS_WEB_PORT || '3000';
553
+ const webPort = (() => {
554
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).port ?? 3000; } catch { return 3000; }
555
+ })();
554
556
  console.log(dim(' (Waiting for Web UI to come back up...)'));
555
557
  const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
556
558
  if (ready) {
@@ -31,7 +31,7 @@ export async function waitForHttp(port, { retries = 120, intervalMs = 2000, labe
31
31
  try {
32
32
  const { request } = await import('node:http');
33
33
  const ok = await new Promise((resolve) => {
34
- const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
34
+ const req = request({ hostname: '127.0.0.1', port, path: '/api/health', method: 'GET', timeout: 1500 },
35
35
  (res) => { res.resume(); resolve(res.statusCode < 500); });
36
36
  req.on('error', () => resolve(false));
37
37
  req.on('timeout', () => { req.destroy(); resolve(false); });
@@ -4,13 +4,13 @@
4
4
  */
5
5
 
6
6
  export const MCP_AGENTS = {
7
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
8
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
9
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
10
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
11
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
12
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
13
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
14
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
15
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
7
+ 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
8
+ 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers', preferredTransport: 'http' },
9
+ 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
10
+ 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
11
+ 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
12
+ 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
13
+ 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
14
+ 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
15
+ 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
16
16
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
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",