@geminilight/mindos 0.5.3 → 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.
@@ -15,6 +15,7 @@ export async function GET() {
15
15
  configPath: status.configPath,
16
16
  hasProjectScope: !!agent.project,
17
17
  hasGlobalScope: !!agent.global,
18
+ preferredTransport: agent.preferredTransport,
18
19
  };
19
20
  });
20
21
  return NextResponse.json({ agents });
@@ -4,9 +4,15 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
6
6
 
7
+ interface AgentInstallItem {
8
+ key: string;
9
+ scope: 'project' | 'global';
10
+ transport?: 'stdio' | 'http';
11
+ }
12
+
7
13
  interface InstallRequest {
8
- agents: Array<{ key: string; scope: 'project' | 'global' }>;
9
- transport: 'stdio' | 'http';
14
+ agents: AgentInstallItem[];
15
+ transport: 'stdio' | 'http' | 'auto';
10
16
  url?: string;
11
17
  token?: string;
12
18
  }
@@ -20,20 +26,59 @@ function buildEntry(transport: string, url?: string, token?: string) {
20
26
  return entry;
21
27
  }
22
28
 
29
+ async function verifyHttpConnection(url: string, token?: string): Promise<{ verified: boolean; verifyError?: string }> {
30
+ try {
31
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
32
+ if (token) headers['Authorization'] = `Bearer ${token}`;
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), 2000);
35
+ const res = await fetch(url, {
36
+ method: 'POST',
37
+ headers,
38
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
39
+ signal: controller.signal,
40
+ });
41
+ clearTimeout(timeout);
42
+ if (res.ok) return { verified: true };
43
+ return { verified: false, verifyError: `HTTP ${res.status}` };
44
+ } catch (err) {
45
+ return { verified: false, verifyError: err instanceof Error ? err.message : String(err) };
46
+ }
47
+ }
48
+
23
49
  export async function POST(req: NextRequest) {
24
50
  try {
25
51
  const body = (await req.json()) as InstallRequest;
26
- const { agents, transport, url, token } = body;
27
- const entry = buildEntry(transport, url, token);
28
- const results: Array<{ agent: string; status: string; path?: string; message?: string }> = [];
52
+ const { agents, transport: globalTransport, url, token } = body;
53
+ const results: Array<{
54
+ agent: string;
55
+ status: string;
56
+ path?: string;
57
+ message?: string;
58
+ transport?: string;
59
+ verified?: boolean;
60
+ verifyError?: string;
61
+ }> = [];
29
62
 
30
- for (const { key, scope } of agents) {
63
+ for (const item of agents) {
64
+ const { key, scope } = item;
31
65
  const agent = MCP_AGENTS[key];
32
66
  if (!agent) {
33
67
  results.push({ agent: key, status: 'error', message: `Unknown agent: ${key}` });
34
68
  continue;
35
69
  }
36
70
 
71
+ // Resolve effective transport: agent-level > global-level > auto (use preferredTransport)
72
+ let effectiveTransport: 'stdio' | 'http';
73
+ if (item.transport && item.transport !== 'auto' as string) {
74
+ effectiveTransport = item.transport;
75
+ } else if (globalTransport && globalTransport !== 'auto') {
76
+ effectiveTransport = globalTransport;
77
+ } else {
78
+ effectiveTransport = agent.preferredTransport;
79
+ }
80
+
81
+ const entry = buildEntry(effectiveTransport, url, token);
37
82
  const isGlobal = scope === 'global';
38
83
  const configPath = isGlobal ? agent.global : agent.project;
39
84
  if (!configPath) {
@@ -59,7 +104,17 @@ export async function POST(req: NextRequest) {
59
104
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
60
105
  fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
61
106
 
62
- results.push({ agent: key, status: 'ok', path: configPath });
107
+ const result: typeof results[number] = { agent: key, status: 'ok', path: configPath, transport: effectiveTransport };
108
+
109
+ // Verify http connections
110
+ if (effectiveTransport === 'http') {
111
+ const mcpUrl = (entry as Record<string, unknown>).url as string;
112
+ const verification = await verifyHttpConnection(mcpUrl, token);
113
+ result.verified = verification.verified;
114
+ if (verification.verifyError) result.verifyError = verification.verifyError;
115
+ }
116
+
117
+ results.push(result);
63
118
  } catch (err) {
64
119
  results.push({ agent: key, status: 'error', message: String(err) });
65
120
  }
@@ -0,0 +1,96 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { execSync } from 'child_process';
4
+ import path from 'path';
5
+
6
+ /* ── Agent classification ──────────────────────────────────────── */
7
+
8
+ // Universal agents read directly from ~/.agents/skills/ — no symlink needed.
9
+ const UNIVERSAL_AGENTS = new Set([
10
+ 'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
11
+ 'github-copilot', 'kimi-cli', 'opencode', 'warp',
12
+ ]);
13
+
14
+ // Agents that do NOT support Skills at all
15
+ const SKILL_UNSUPPORTED = new Set(['claude-desktop']);
16
+
17
+ // MCP agent key → npx skills agent name (for non-universal agents)
18
+ // Keys not listed here and not in UNIVERSAL/UNSUPPORTED will use the key as-is.
19
+ const AGENT_NAME_MAP: Record<string, string> = {
20
+ 'claude-code': 'claude-code',
21
+ 'windsurf': 'windsurf',
22
+ 'trae': 'trae',
23
+ 'openclaw': 'openclaw',
24
+ 'codebuddy': 'codebuddy',
25
+ };
26
+
27
+ /* ── POST handler ──────────────────────────────────────────────── */
28
+
29
+ interface SkillInstallRequest {
30
+ skill: 'mindos' | 'mindos-zh';
31
+ agents: string[];
32
+ }
33
+
34
+ export async function POST(req: NextRequest) {
35
+ try {
36
+ const body: SkillInstallRequest = await req.json();
37
+ const { skill, agents } = body;
38
+
39
+ if (!skill || !['mindos', 'mindos-zh'].includes(skill)) {
40
+ return NextResponse.json({ error: 'Invalid skill name' }, { status: 400 });
41
+ }
42
+
43
+ // Source path: project root `skills/` directory
44
+ const source = path.resolve(process.cwd(), 'skills');
45
+
46
+ // Non-universal, skill-capable agents need explicit `-a` for symlink creation
47
+ const additionalAgents = (agents || [])
48
+ .filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
49
+ .map(key => AGENT_NAME_MAP[key] || key);
50
+
51
+ let cmd: string;
52
+ if (additionalAgents.length > 0) {
53
+ // Any -a command will also copy to ~/.agents/skills/ (Universal coverage)
54
+ cmd = `npx skills add "${source}" -s ${skill} -a ${additionalAgents.join(',')} -g -y`;
55
+ } else {
56
+ // Fallback: only install to ~/.agents/skills/ for Universal agents
57
+ cmd = `npx skills add "${source}" -s ${skill} -a universal -g -y`;
58
+ }
59
+
60
+ let stdout = '';
61
+ let stderr = '';
62
+ try {
63
+ stdout = execSync(cmd, {
64
+ encoding: 'utf-8',
65
+ timeout: 30_000,
66
+ env: { ...process.env, NODE_ENV: 'production' },
67
+ stdio: 'pipe',
68
+ });
69
+ } catch (err: unknown) {
70
+ const e = err as { stdout?: string; stderr?: string; message?: string };
71
+ stdout = e.stdout || '';
72
+ stderr = e.stderr || e.message || 'Unknown error';
73
+ return NextResponse.json({
74
+ ok: false,
75
+ skill,
76
+ agents: additionalAgents,
77
+ cmd,
78
+ stdout,
79
+ stderr,
80
+ });
81
+ }
82
+
83
+ return NextResponse.json({
84
+ ok: true,
85
+ skill,
86
+ agents: additionalAgents,
87
+ cmd,
88
+ stdout: stdout.trim(),
89
+ });
90
+ } catch (e) {
91
+ return NextResponse.json(
92
+ { error: e instanceof Error ? e.message : String(e) },
93
+ { status: 500 },
94
+ );
95
+ }
96
+ }
@@ -90,9 +90,10 @@ export async function POST(req: NextRequest) {
90
90
  // Use the same resolved values that will actually be written to config
91
91
  const resolvedAuthToken = authToken ?? current.authToken ?? '';
92
92
  const resolvedWebPassword = webPassword ?? '';
93
- // Only compute needsRestart for re-onboard (setupPending=true means first-time setup)
93
+ // First-time onboard always needs restart (temporary setup port → user's chosen port).
94
+ // Re-onboard only needs restart if port/path/auth/password actually changed.
94
95
  const isFirstTime = current.setupPending === true || !current.mindRoot;
95
- const needsRestart = !isFirstTime && (
96
+ const needsRestart = isFirstTime || (
96
97
  webPort !== (current.port ?? 3000) ||
97
98
  mcpPortNum !== (current.mcpPort ?? 8787) ||
98
99
  resolvedRoot !== (current.mindRoot || '') ||
@@ -101,6 +102,7 @@ export async function POST(req: NextRequest) {
101
102
  );
102
103
 
103
104
  // Build config
105
+ const disabledSkills = body.template === 'zh' ? ['mindos'] : ['mindos-zh'];
104
106
  const config: ServerSettings = {
105
107
  ai: ai ?? current.ai,
106
108
  mindRoot: resolvedRoot,
@@ -110,6 +112,7 @@ export async function POST(req: NextRequest) {
110
112
  webPassword: webPassword ?? '',
111
113
  startMode: current.startMode,
112
114
  setupPending: false, // clear the flag
115
+ disabledSkills,
113
116
  };
114
117
 
115
118
  writeSettings(config);
@@ -8,6 +8,7 @@ import MarkdownView from '@/components/MarkdownView';
8
8
  import JsonView from '@/components/JsonView';
9
9
  import CsvView from '@/components/CsvView';
10
10
  import Backlinks from '@/components/Backlinks';
11
+ import { useRendererState } from '@/lib/renderers/useRendererState';
11
12
  import Breadcrumb from '@/components/Breadcrumb';
12
13
  import MarkdownEditor, { MdViewMode } from '@/components/MarkdownEditor';
13
14
  import TableOfContents from '@/components/TableOfContents';
@@ -45,22 +46,7 @@ export default function ViewPageClient({
45
46
  () => false,
46
47
  );
47
48
 
48
- const useRaw = useSyncExternalStore(
49
- (onStoreChange) => {
50
- const listener = () => onStoreChange();
51
- window.addEventListener('storage', listener);
52
- window.addEventListener('mindos-use-raw-change', listener);
53
- return () => {
54
- window.removeEventListener('storage', listener);
55
- window.removeEventListener('mindos-use-raw-change', listener);
56
- };
57
- },
58
- () => {
59
- const saved = localStorage.getItem('mindos-use-raw');
60
- return saved !== null ? saved === 'true' : false;
61
- },
62
- () => false,
63
- );
49
+ const [useRaw, setUseRaw] = useRendererState<boolean>('_raw', filePath, false);
64
50
  const router = useRouter();
65
51
  const [editing, setEditing] = useState(initialEditing || content === '');
66
52
  const [editContent, setEditContent] = useState(content);
@@ -81,10 +67,8 @@ export default function ViewPageClient({
81
67
  const effectiveUseRaw = hydrated ? useRaw : false;
82
68
 
83
69
  const handleToggleRaw = useCallback(() => {
84
- const next = !useRaw;
85
- localStorage.setItem('mindos-use-raw', String(next));
86
- window.dispatchEvent(new Event('mindos-use-raw-change'));
87
- }, [useRaw]);
70
+ setUseRaw(prev => !prev);
71
+ }, [setUseRaw]);
88
72
 
89
73
  const renderer = resolveRenderer(filePath, extension);
90
74
  const isCsv = extension === 'csv';
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import {
5
5
  Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw,
6
6
  Loader2, ChevronLeft, ChevronRight, AlertTriangle, CheckCircle2,
7
- XCircle, Zap, Brain, SkipForward,
7
+ XCircle, Zap, Brain, SkipForward, Info,
8
8
  } from 'lucide-react';
9
9
  import { useLocale } from '@/lib/LocaleContext';
10
10
  import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
@@ -39,12 +39,16 @@ interface AgentEntry {
39
39
  installed: boolean;
40
40
  hasProjectScope: boolean;
41
41
  hasGlobalScope: boolean;
42
+ preferredTransport: 'stdio' | 'http';
42
43
  }
43
44
 
44
45
  type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
45
46
  interface AgentInstallStatus {
46
47
  state: AgentInstallState;
47
48
  message?: string;
49
+ transport?: string;
50
+ verified?: boolean;
51
+ verifyError?: string;
48
52
  }
49
53
 
50
54
  const TEMPLATES: Array<{ id: Template; icon: React.ReactNode; dirs: string[] }> = [
@@ -469,7 +473,7 @@ function Step3({
469
473
  <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
470
474
  )}
471
475
  <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
472
- <AlertTriangle size={12} /> {s.portRestartWarning}
476
+ <Info size={12} /> {s.portRestartWarning}
473
477
  </p>
474
478
  </div>
475
479
  );
@@ -479,19 +483,20 @@ function Step3({
479
483
  function Step5({
480
484
  agents, agentsLoading, selectedAgents, setSelectedAgents,
481
485
  agentTransport, setAgentTransport, agentScope, setAgentScope,
482
- agentStatuses, s, settingsMcp,
486
+ agentStatuses, s, settingsMcp, template,
483
487
  }: {
484
488
  agents: AgentEntry[];
485
489
  agentsLoading: boolean;
486
490
  selectedAgents: Set<string>;
487
491
  setSelectedAgents: React.Dispatch<React.SetStateAction<Set<string>>>;
488
- agentTransport: 'stdio' | 'http';
489
- setAgentTransport: (v: 'stdio' | 'http') => void;
492
+ agentTransport: 'auto' | 'stdio' | 'http';
493
+ setAgentTransport: (v: 'auto' | 'stdio' | 'http') => void;
490
494
  agentScope: 'global' | 'project';
491
495
  setAgentScope: (v: 'global' | 'project') => void;
492
496
  agentStatuses: Record<string, AgentInstallStatus>;
493
497
  s: ReturnType<typeof useLocale>['t']['setup'];
494
498
  settingsMcp: ReturnType<typeof useLocale>['t']['settings']['mcp'];
499
+ template: Template;
495
500
  }) {
496
501
  const toggleAgent = (key: string) => {
497
502
  setSelectedAgents(prev => {
@@ -501,6 +506,11 @@ function Step5({
501
506
  });
502
507
  };
503
508
 
509
+ const getEffectiveTransport = (agent: AgentEntry) => {
510
+ if (agentTransport === 'auto') return agent.preferredTransport;
511
+ return agentTransport;
512
+ };
513
+
504
514
  const getStatusBadge = (key: string, installed: boolean) => {
505
515
  const st = agentStatuses[key];
506
516
  if (st) {
@@ -567,13 +577,24 @@ function Step5({
567
577
  disabled={agentStatuses[agent.key]?.state === 'installing'}
568
578
  />
569
579
  <span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
580
+ <span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
581
+ style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
582
+ {getEffectiveTransport(agent)}
583
+ </span>
570
584
  {getStatusBadge(agent.key, agent.installed)}
571
585
  </label>
572
586
  ))}
573
587
  </div>
588
+ {/* Skill auto-install hint */}
589
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs"
590
+ style={{ background: 'rgba(100,100,120,0.06)', color: 'var(--muted-foreground)' }}>
591
+ <Brain size={13} className="shrink-0" />
592
+ <span>{s.skillAutoHint(template === 'zh' ? 'mindos-zh' : 'mindos')}</span>
593
+ </div>
574
594
  <div className="grid grid-cols-2 gap-4">
575
595
  <Field label={s.agentTransport}>
576
- <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
596
+ <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'auto' | 'stdio' | 'http')}>
597
+ <option value="auto">{s.agentTransportAuto}</option>
577
598
  <option value="stdio">{settingsMcp.transportStdio}</option>
578
599
  <option value="http">{settingsMcp.transportHttp}</option>
579
600
  </Select>
@@ -660,6 +681,7 @@ function RestartBlock({ s, newPort }: { s: ReturnType<typeof useLocale>['t']['se
660
681
  // ─── Step 6: Review ───────────────────────────────────────────────────────────
661
682
  function Step6({
662
683
  state, selectedAgents, agentStatuses, onRetryAgent, error, needsRestart, maskKey, s,
684
+ skillInstallResult,
663
685
  }: {
664
686
  state: SetupState;
665
687
  selectedAgents: Set<string>;
@@ -669,7 +691,9 @@ function Step6({
669
691
  needsRestart: boolean;
670
692
  maskKey: (key: string) => string;
671
693
  s: ReturnType<typeof useLocale>['t']['setup'];
694
+ skillInstallResult: { ok?: boolean; skill?: string; error?: string } | null;
672
695
  }) {
696
+ const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
673
697
  const rows: [string, string][] = [
674
698
  [s.kbPath, state.mindRoot],
675
699
  [s.template, state.template || '—'],
@@ -683,9 +707,11 @@ function Step6({
683
707
  [s.authToken, state.authToken || '—'],
684
708
  [s.webPassword, state.webPassword ? '••••••••' : '(none)'],
685
709
  [s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
710
+ [s.skillLabel, skillName],
686
711
  ];
687
712
 
688
713
  const failedAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'error');
714
+ const successAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'ok');
689
715
 
690
716
  return (
691
717
  <div className="space-y-5">
@@ -702,6 +728,43 @@ function Step6({
702
728
  </div>
703
729
  ))}
704
730
  </div>
731
+
732
+ {/* Agent verification results */}
733
+ {successAgents.length > 0 && (
734
+ <div className="space-y-1.5">
735
+ <p className="text-xs font-medium" style={{ color: 'var(--muted-foreground)' }}>{s.reviewInstallResults}</p>
736
+ {successAgents.map(([key, st]) => (
737
+ <div key={key} className="flex items-center gap-2 text-xs px-3 py-1.5 rounded"
738
+ style={{ background: 'rgba(34,197,94,0.06)' }}>
739
+ <CheckCircle2 size={11} className="text-green-500 shrink-0" />
740
+ <span style={{ color: 'var(--foreground)' }}>{key}</span>
741
+ <span className="font-mono text-[10px] px-1 py-0.5 rounded"
742
+ style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
743
+ {st.transport || 'stdio'}
744
+ </span>
745
+ {st.transport === 'http' ? (
746
+ st.verified ? (
747
+ <span className="text-[10px] px-1.5 py-0.5 rounded"
748
+ style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
749
+ {s.agentVerified}
750
+ </span>
751
+ ) : (
752
+ <span className="text-[10px] px-1.5 py-0.5 rounded"
753
+ style={{ background: 'rgba(239,168,68,0.12)', color: '#f59e0b' }}
754
+ title={st.verifyError}>
755
+ {s.agentUnverified}
756
+ </span>
757
+ )
758
+ ) : (
759
+ <span className="text-[10px]" style={{ color: 'var(--muted-foreground)' }}>
760
+ {s.agentVerifyNote}
761
+ </span>
762
+ )}
763
+ </div>
764
+ ))}
765
+ </div>
766
+ )}
767
+
705
768
  {failedAgents.length > 0 && (
706
769
  <div className="p-3 rounded-lg space-y-2" style={{ background: 'rgba(239,68,68,0.08)' }}>
707
770
  <p className="text-xs font-medium" style={{ color: '#ef4444' }}>{s.reviewInstallResults}</p>
@@ -723,6 +786,22 @@ function Step6({
723
786
  <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentFailureNote}</p>
724
787
  </div>
725
788
  )}
789
+ {/* Skill install result */}
790
+ {skillInstallResult && (
791
+ <div className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${
792
+ skillInstallResult.ok ? '' : ''
793
+ }`} style={{
794
+ background: skillInstallResult.ok ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)',
795
+ }}>
796
+ {skillInstallResult.ok ? (
797
+ <><CheckCircle2 size={11} className="text-green-500 shrink-0" />
798
+ <span style={{ color: 'var(--foreground)' }}>{s.skillInstalled} — {skillInstallResult.skill}</span></>
799
+ ) : (
800
+ <><XCircle size={11} className="text-red-500 shrink-0" />
801
+ <span style={{ color: '#ef4444' }}>{s.skillFailed}{skillInstallResult.error ? `: ${skillInstallResult.error}` : ''}</span></>
802
+ )}
803
+ </div>
804
+ )}
726
805
  {error && (
727
806
  <div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
728
807
  {s.completeFailed}: {error}
@@ -798,9 +877,10 @@ export default function SetupWizard() {
798
877
  const [agents, setAgents] = useState<AgentEntry[]>([]);
799
878
  const [agentsLoading, setAgentsLoading] = useState(false);
800
879
  const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
801
- const [agentTransport, setAgentTransport] = useState<'stdio' | 'http'>('stdio');
880
+ const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
802
881
  const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
803
882
  const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
883
+ const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
804
884
 
805
885
  // Load existing config as defaults on mount, generate token if none exists
806
886
  useEffect(() => {
@@ -970,7 +1050,13 @@ export default function SetupWizard() {
970
1050
  setAgentStatuses(initialStatuses);
971
1051
 
972
1052
  try {
973
- const agentsPayload = Array.from(selectedAgents).map(key => ({ key, scope: agentScope }));
1053
+ const agentsPayload = Array.from(selectedAgents).map(key => {
1054
+ const agent = agents.find(a => a.key === key);
1055
+ const effectiveTransport = agentTransport === 'auto'
1056
+ ? (agent?.preferredTransport || 'stdio')
1057
+ : agentTransport;
1058
+ return { key, scope: agentScope, transport: effectiveTransport };
1059
+ });
974
1060
  const res = await fetch('/api/mcp/install', {
975
1061
  method: 'POST',
976
1062
  headers: { 'Content-Type': 'application/json' },
@@ -984,8 +1070,14 @@ export default function SetupWizard() {
984
1070
  const data = await res.json();
985
1071
  if (data.results) {
986
1072
  const updated: Record<string, AgentInstallStatus> = {};
987
- for (const r of data.results as Array<{ agent: string; status: string; message?: string }>) {
988
- updated[r.agent] = { state: r.status === 'ok' ? 'ok' : 'error', message: r.message };
1073
+ for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
1074
+ updated[r.agent] = {
1075
+ state: r.status === 'ok' ? 'ok' : 'error',
1076
+ message: r.message,
1077
+ transport: r.transport,
1078
+ verified: r.verified,
1079
+ verifyError: r.verifyError,
1080
+ };
989
1081
  }
990
1082
  setAgentStatuses(updated);
991
1083
  }
@@ -996,6 +1088,20 @@ export default function SetupWizard() {
996
1088
  }
997
1089
  }
998
1090
 
1091
+ // 3. Install skill to agents
1092
+ const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
1093
+ try {
1094
+ const skillRes = await fetch('/api/mcp/install-skill', {
1095
+ method: 'POST',
1096
+ headers: { 'Content-Type': 'application/json' },
1097
+ body: JSON.stringify({ skill: skillName, agents: Array.from(selectedAgents) }),
1098
+ });
1099
+ const skillData = await skillRes.json();
1100
+ setSkillInstallResult(skillData);
1101
+ } catch {
1102
+ setSkillInstallResult({ error: 'Failed to install skill' });
1103
+ }
1104
+
999
1105
  setSubmitting(false);
1000
1106
  setCompleted(true);
1001
1107
 
@@ -1009,11 +1115,15 @@ export default function SetupWizard() {
1009
1115
  const retryAgent = useCallback(async (key: string) => {
1010
1116
  setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
1011
1117
  try {
1118
+ const agent = agents.find(a => a.key === key);
1119
+ const effectiveTransport = agentTransport === 'auto'
1120
+ ? (agent?.preferredTransport || 'stdio')
1121
+ : agentTransport;
1012
1122
  const res = await fetch('/api/mcp/install', {
1013
1123
  method: 'POST',
1014
1124
  headers: { 'Content-Type': 'application/json' },
1015
1125
  body: JSON.stringify({
1016
- agents: [{ key, scope: agentScope }],
1126
+ agents: [{ key, scope: agentScope, transport: effectiveTransport }],
1017
1127
  transport: agentTransport,
1018
1128
  url: `http://localhost:${state.mcpPort}/mcp`,
1019
1129
  token: state.authToken || undefined,
@@ -1021,13 +1131,22 @@ export default function SetupWizard() {
1021
1131
  });
1022
1132
  const data = await res.json();
1023
1133
  if (data.results?.[0]) {
1024
- const r = data.results[0] as { agent: string; status: string; message?: string };
1025
- setAgentStatuses(prev => ({ ...prev, [key]: { state: r.status === 'ok' ? 'ok' : 'error', message: r.message } }));
1134
+ const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
1135
+ setAgentStatuses(prev => ({
1136
+ ...prev,
1137
+ [key]: {
1138
+ state: r.status === 'ok' ? 'ok' : 'error',
1139
+ message: r.message,
1140
+ transport: r.transport,
1141
+ verified: r.verified,
1142
+ verifyError: r.verifyError,
1143
+ },
1144
+ }));
1026
1145
  }
1027
1146
  } catch {
1028
1147
  setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
1029
1148
  }
1030
- }, [agentScope, agentTransport, state.mcpPort, state.authToken]);
1149
+ }, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
1031
1150
 
1032
1151
  return (
1033
1152
  <div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@@ -1075,6 +1194,7 @@ export default function SetupWizard() {
1075
1194
  agentTransport={agentTransport} setAgentTransport={setAgentTransport}
1076
1195
  agentScope={agentScope} setAgentScope={setAgentScope}
1077
1196
  agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
1197
+ template={state.template}
1078
1198
  />
1079
1199
  )}
1080
1200
  {step === 5 && (
@@ -1083,6 +1203,7 @@ export default function SetupWizard() {
1083
1203
  agentStatuses={agentStatuses} onRetryAgent={retryAgent}
1084
1204
  error={error} needsRestart={needsRestart}
1085
1205
  maskKey={maskKey} s={s}
1206
+ skillInstallResult={skillInstallResult}
1086
1207
  />
1087
1208
  )}
1088
1209
 
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
8
8
  icon: '🧩',
9
9
  tags: ['config', 'json', 'settings', 'schema'],
10
10
  builtin: true,
11
+ core: true,
11
12
  entryPath: 'CONFIG.json',
12
13
  match: ({ filePath, extension }) => extension === 'json' && /(^|\/)CONFIG\.json$/i.test(filePath),
13
14
  load: () => import('./ConfigRenderer').then(m => ({ default: m.ConfigRenderer })),
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useState, useMemo, useCallback, useEffect } from 'react';
3
+ import { useMemo, useCallback, useState } from 'react';
4
4
  import { LayoutGrid, Columns, Table2, Settings2 } from 'lucide-react';
5
5
  import type { RendererContext } from '@/lib/renderers/registry';
6
6
  import type { ViewType, CsvConfig } from './types';
7
- import { defaultConfig, loadConfig, saveConfig, parseCSV } from './types';
7
+ import { defaultConfig, parseCSV } from './types';
8
+ import { useRendererState } from '@/lib/renderers/useRendererState';
8
9
  import { TableView } from './TableView';
9
10
  import { GalleryView } from './GalleryView';
10
11
  import { BoardView } from './BoardView';
@@ -18,21 +19,14 @@ const VIEW_TABS: { id: ViewType; icon: React.ReactNode; label: string }[] = [
18
19
 
19
20
  export function CsvRenderer({ filePath, content, saveAction }: RendererContext) {
20
21
  const { headers, rows } = useMemo(() => parseCSV(content), [content]);
21
- const [cfg, setCfg] = useState<CsvConfig>(() => defaultConfig(headers));
22
- const [configLoaded, setConfigLoaded] = useState(false);
22
+ const def = useMemo(() => defaultConfig(headers), [headers]);
23
+ const [cfg, setCfg] = useRendererState<CsvConfig>('csv', filePath, def);
23
24
  const [showConfig, setShowConfig] = useState(false);
24
25
 
25
- useEffect(() => {
26
- setCfg(loadConfig(filePath, headers));
27
- setConfigLoaded(true);
28
- }, [filePath]); // eslint-disable-line react-hooks/exhaustive-deps
29
-
30
26
  const updateConfig = useCallback((next: CsvConfig) => {
31
27
  setCfg(next);
32
- saveConfig(filePath, next);
33
- }, [filePath]);
28
+ }, [setCfg]);
34
29
 
35
- if (!configLoaded) return null;
36
30
  const view = cfg.activeView;
37
31
 
38
32
  return (
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
8
8
  icon: '📊',
9
9
  tags: ['csv', 'table', 'gallery', 'board', 'data'],
10
10
  builtin: true,
11
+ core: true,
11
12
  entryPath: 'Resources/Products.csv',
12
13
  match: ({ extension, filePath }) => extension === 'csv' && !/\bTODO\b/i.test(filePath),
13
14
  load: () => import('./CsvRenderer').then(m => ({ default: m.CsvRenderer })),
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
8
8
  icon: '✅',
9
9
  tags: ['productivity', 'tasks', 'markdown'],
10
10
  builtin: true,
11
+ core: true,
11
12
  entryPath: 'TODO.md',
12
13
  match: ({ filePath }) => /\bTODO\b.*\.(md|csv)$/i.test(filePath),
13
14
  load: () => import('./TodoRenderer').then(m => ({ default: m.TodoRenderer })),