@geminilight/mindos 0.5.9 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/app/app/api/settings/test-key/route.ts +111 -0
  3. package/app/app/api/skills/route.ts +1 -1
  4. package/app/app/api/sync/route.ts +16 -31
  5. package/app/app/globals.css +10 -2
  6. package/app/app/login/page.tsx +1 -1
  7. package/app/app/view/[...path]/ViewPageClient.tsx +6 -1
  8. package/app/app/view/[...path]/not-found.tsx +1 -1
  9. package/app/components/AskModal.tsx +4 -4
  10. package/app/components/Breadcrumb.tsx +2 -2
  11. package/app/components/DirView.tsx +6 -6
  12. package/app/components/FileTree.tsx +2 -2
  13. package/app/components/HomeContent.tsx +7 -7
  14. package/app/components/OnboardingView.tsx +1 -1
  15. package/app/components/SearchModal.tsx +1 -1
  16. package/app/components/SettingsModal.tsx +2 -2
  17. package/app/components/SetupWizard.tsx +1 -1400
  18. package/app/components/Sidebar.tsx +4 -4
  19. package/app/components/SidebarLayout.tsx +9 -0
  20. package/app/components/SyncStatusBar.tsx +3 -3
  21. package/app/components/TableOfContents.tsx +1 -1
  22. package/app/components/UpdateBanner.tsx +1 -1
  23. package/app/components/ask/FileChip.tsx +1 -1
  24. package/app/components/ask/MentionPopover.tsx +4 -4
  25. package/app/components/ask/MessageList.tsx +1 -1
  26. package/app/components/ask/SessionHistory.tsx +2 -2
  27. package/app/components/renderers/config/ConfigRenderer.tsx +1 -1
  28. package/app/components/renderers/csv/BoardView.tsx +2 -2
  29. package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
  30. package/app/components/renderers/csv/GalleryView.tsx +1 -1
  31. package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
  32. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  33. package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
  34. package/app/components/settings/AiTab.tsx +120 -2
  35. package/app/components/settings/KnowledgeTab.tsx +1 -1
  36. package/app/components/settings/McpTab.tsx +27 -23
  37. package/app/components/settings/PluginsTab.tsx +4 -4
  38. package/app/components/settings/Primitives.tsx +1 -1
  39. package/app/components/settings/SyncTab.tsx +8 -8
  40. package/app/components/setup/StepAI.tsx +67 -0
  41. package/app/components/setup/StepAgents.tsx +237 -0
  42. package/app/components/setup/StepDots.tsx +39 -0
  43. package/app/components/setup/StepKB.tsx +237 -0
  44. package/app/components/setup/StepPorts.tsx +121 -0
  45. package/app/components/setup/StepReview.tsx +211 -0
  46. package/app/components/setup/StepSecurity.tsx +78 -0
  47. package/app/components/setup/constants.tsx +13 -0
  48. package/app/components/setup/index.tsx +464 -0
  49. package/app/components/setup/types.ts +53 -0
  50. package/app/instrumentation.ts +19 -0
  51. package/app/lib/i18n.ts +22 -4
  52. package/app/next.config.ts +1 -1
  53. package/bin/cli.js +8 -1
  54. package/bin/lib/sync.js +61 -11
  55. package/package.json +4 -2
  56. package/skills/project-wiki/SKILL.md +92 -63
  57. package/assets/images/demo-flow-dark.png +0 -0
  58. package/assets/images/demo-flow-light.png +0 -0
  59. package/assets/images/demo-flow-zh-dark.png +0 -0
  60. package/assets/images/demo-flow-zh-light.png +0 -0
  61. package/assets/images/gui-sync-cv.png +0 -0
  62. package/assets/images/wechat-qr.png +0 -0
  63. package/mcp/package-lock.json +0 -1717
@@ -0,0 +1,464 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
7
+ import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
8
+ import StepKB from './StepKB';
9
+ import StepAI from './StepAI';
10
+ import StepPorts from './StepPorts';
11
+ import StepSecurity from './StepSecurity';
12
+ import StepAgents from './StepAgents';
13
+ import StepReview from './StepReview';
14
+ import StepDots from './StepDots';
15
+
16
+ // ─── Helpers (shared by handleComplete + retryAgent) ─────────────────────────
17
+
18
+ /** Build a single agent's install payload */
19
+ function buildAgentPayload(
20
+ key: string,
21
+ agents: AgentEntry[],
22
+ transport: 'auto' | 'stdio' | 'http',
23
+ scope: 'global' | 'project',
24
+ ): { key: string; scope: string; transport: string } {
25
+ const agent = agents.find(a => a.key === key);
26
+ const effectiveTransport = transport === 'auto'
27
+ ? (agent?.preferredTransport || 'stdio')
28
+ : transport;
29
+ return { key, scope, transport: effectiveTransport };
30
+ }
31
+
32
+ /** Parse a single install API result into AgentInstallStatus */
33
+ function parseInstallResult(
34
+ r: { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string },
35
+ ): AgentInstallStatus {
36
+ return {
37
+ state: r.status === 'ok' ? 'ok' : 'error',
38
+ message: r.message,
39
+ transport: r.transport,
40
+ verified: r.verified,
41
+ verifyError: r.verifyError,
42
+ };
43
+ }
44
+
45
+ // ─── Phase runners (pure async, no setState — results consumed by caller) ────
46
+
47
+ /** Phase 1: Save setup config. Returns whether restart is needed. Throws on failure. */
48
+ async function saveConfig(state: SetupState): Promise<boolean> {
49
+ const payload = {
50
+ mindRoot: state.mindRoot,
51
+ template: state.template || undefined,
52
+ port: state.webPort,
53
+ mcpPort: state.mcpPort,
54
+ authToken: state.authToken,
55
+ webPassword: state.webPassword,
56
+ ai: state.provider === 'skip' ? undefined : {
57
+ provider: state.provider,
58
+ providers: {
59
+ anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
60
+ openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
61
+ },
62
+ },
63
+ };
64
+ const res = await fetch('/api/setup', {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify(payload),
68
+ });
69
+ const data = await res.json();
70
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
71
+ return !!data.needsRestart;
72
+ }
73
+
74
+ /** Phase 2: Install selected agents. Returns status map. */
75
+ async function installAgents(
76
+ keys: string[],
77
+ agents: AgentEntry[],
78
+ transport: 'auto' | 'stdio' | 'http',
79
+ scope: 'global' | 'project',
80
+ mcpPort: number,
81
+ authToken: string,
82
+ ): Promise<Record<string, AgentInstallStatus>> {
83
+ const agentsPayload = keys.map(k => buildAgentPayload(k, agents, transport, scope));
84
+ const res = await fetch('/api/mcp/install', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({
88
+ agents: agentsPayload,
89
+ transport,
90
+ url: `http://localhost:${mcpPort}/mcp`,
91
+ token: authToken || undefined,
92
+ }),
93
+ });
94
+ const data = await res.json();
95
+ const updated: Record<string, AgentInstallStatus> = {};
96
+ if (data.results) {
97
+ for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
98
+ updated[r.agent] = parseInstallResult(r);
99
+ }
100
+ }
101
+ return updated;
102
+ }
103
+
104
+ /** Phase 3: Install skill to agents. Returns result. */
105
+ async function installSkill(
106
+ template: string,
107
+ agentKeys: string[],
108
+ ): Promise<{ ok?: boolean; skill?: string; error?: string }> {
109
+ const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
110
+ const res = await fetch('/api/mcp/install-skill', {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({ skill: skillName, agents: agentKeys }),
114
+ });
115
+ return await res.json();
116
+ }
117
+
118
+ // ─── Component ───────────────────────────────────────────────────────────────
119
+
120
+ export default function SetupWizard() {
121
+ const { t } = useLocale();
122
+ const s = t.setup;
123
+
124
+ const [step, setStep] = useState(0);
125
+ const [state, setState] = useState<SetupState>({
126
+ mindRoot: '~/MindOS/mind',
127
+ template: 'en',
128
+ provider: 'anthropic',
129
+ anthropicKey: '',
130
+ anthropicModel: 'claude-sonnet-4-6',
131
+ openaiKey: '',
132
+ openaiModel: 'gpt-5.4',
133
+ openaiBaseUrl: '',
134
+ webPort: 3000,
135
+ mcpPort: 8787,
136
+ authToken: '',
137
+ webPassword: '',
138
+ });
139
+ const [homeDir, setHomeDir] = useState('~');
140
+ const [tokenCopied, setTokenCopied] = useState(false);
141
+ const [submitting, setSubmitting] = useState(false);
142
+ const [completed, setCompleted] = useState(false);
143
+ const [error, setError] = useState('');
144
+ const [needsRestart, setNeedsRestart] = useState(false);
145
+
146
+ const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
147
+ const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
148
+
149
+ const [agents, setAgents] = useState<AgentEntry[]>([]);
150
+ const [agentsLoading, setAgentsLoading] = useState(false);
151
+ const [agentsLoaded, setAgentsLoaded] = useState(false);
152
+ const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
153
+ const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
154
+ const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
155
+ const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
156
+ const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
157
+ const [setupPhase, setSetupPhase] = useState<'review' | 'saving' | 'agents' | 'skill' | 'done'>('review');
158
+
159
+ // Load existing config as defaults on mount, generate token if none exists
160
+ useEffect(() => {
161
+ fetch('/api/setup')
162
+ .then(r => r.json())
163
+ .then(data => {
164
+ if (data.homeDir) setHomeDir(data.homeDir);
165
+ setState(prev => ({
166
+ ...prev,
167
+ mindRoot: data.mindRoot || prev.mindRoot,
168
+ webPort: typeof data.port === 'number' ? data.port : prev.webPort,
169
+ mcpPort: typeof data.mcpPort === 'number' ? data.mcpPort : prev.mcpPort,
170
+ authToken: data.authToken || prev.authToken,
171
+ webPassword: data.webPassword || prev.webPassword,
172
+ provider: (data.provider === 'anthropic' || data.provider === 'openai') ? data.provider : prev.provider,
173
+ anthropicModel: data.anthropicModel || prev.anthropicModel,
174
+ openaiModel: data.openaiModel || prev.openaiModel,
175
+ openaiBaseUrl: data.openaiBaseUrl ?? prev.openaiBaseUrl,
176
+ }));
177
+ // Generate a new token only if none exists yet
178
+ if (!data.authToken) {
179
+ fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
180
+ .then(r => r.json())
181
+ .then(tokenData => { if (tokenData.token) setState(p => ({ ...p, authToken: tokenData.token })); })
182
+ .catch(e => console.warn('[SetupWizard] Token generation failed:', e));
183
+ }
184
+ })
185
+ .catch(e => {
186
+ console.warn('[SetupWizard] Failed to load config, generating token as fallback:', e);
187
+ // Fallback: generate token on failure
188
+ fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
189
+ .then(r => r.json())
190
+ .then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
191
+ .catch(e2 => console.warn('[SetupWizard] Fallback token generation also failed:', e2));
192
+ });
193
+ }, []);
194
+
195
+ // Auto-check ports when entering Step 3
196
+ useEffect(() => {
197
+ if (step === STEP_PORTS) {
198
+ checkPort(state.webPort, 'web');
199
+ checkPort(state.mcpPort, 'mcp');
200
+ }
201
+ // eslint-disable-next-line react-hooks/exhaustive-deps
202
+ }, [step]);
203
+
204
+ // Load agents when entering Step 5
205
+ useEffect(() => {
206
+ if (step === STEP_AGENTS && !agentsLoaded && !agentsLoading) {
207
+ setAgentsLoading(true);
208
+ fetch('/api/mcp/agents')
209
+ .then(r => r.json())
210
+ .then(data => {
211
+ if (data.agents) {
212
+ setAgents(data.agents);
213
+ setSelectedAgents(new Set(
214
+ (data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
215
+ ));
216
+ }
217
+ setAgentsLoaded(true);
218
+ })
219
+ .catch(e => { console.warn('[SetupWizard] Failed to load agents:', e); setAgentsLoaded(true); })
220
+ .finally(() => setAgentsLoading(false));
221
+ }
222
+ }, [step, agentsLoaded, agentsLoading]);
223
+
224
+ const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
225
+ setState(prev => ({ ...prev, [key]: val }));
226
+ }, []);
227
+
228
+ const generateToken = useCallback(async (seed?: string) => {
229
+ try {
230
+ const res = await fetch('/api/setup/generate-token', {
231
+ method: 'POST',
232
+ headers: { 'Content-Type': 'application/json' },
233
+ body: JSON.stringify({ seed: seed || undefined }),
234
+ });
235
+ const data = await res.json();
236
+ if (data.token) setState(prev => ({ ...prev, authToken: data.token }));
237
+ } catch (e) { console.warn('[SetupWizard] generateToken failed:', e); }
238
+ }, []);
239
+
240
+ const copyToken = useCallback(() => {
241
+ navigator.clipboard.writeText(state.authToken).catch(() => { /* clipboard unavailable in insecure context */ });
242
+ setTokenCopied(true);
243
+ setTimeout(() => setTokenCopied(false), 2000);
244
+ }, [state.authToken]);
245
+
246
+ const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
247
+ if (port < 1024 || port > 65535) return;
248
+ const setStatus = which === 'web' ? setWebPortStatus : setMcpPortStatus;
249
+ setStatus({ checking: true, available: null, isSelf: false, suggestion: null });
250
+ try {
251
+ const res = await fetch('/api/setup/check-port', {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ port }),
255
+ });
256
+ const data = await res.json();
257
+ setStatus({ checking: false, available: data.available ?? null, isSelf: !!data.isSelf, suggestion: data.suggestion ?? null });
258
+ } catch (e) {
259
+ console.warn('[SetupWizard] checkPort failed:', e);
260
+ setStatus({ checking: false, available: null, isSelf: false, suggestion: null });
261
+ }
262
+ }, []);
263
+
264
+ const portConflict = state.webPort === state.mcpPort;
265
+
266
+ const canNext = () => {
267
+ if (step === STEP_KB) return state.mindRoot.trim().length > 0;
268
+ if (step === STEP_PORTS) {
269
+ if (portConflict) return false;
270
+ if (webPortStatus.checking || mcpPortStatus.checking) return false;
271
+ if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
272
+ return (
273
+ state.webPort >= 1024 && state.webPort <= 65535 &&
274
+ state.mcpPort >= 1024 && state.mcpPort <= 65535
275
+ );
276
+ }
277
+ return true;
278
+ };
279
+
280
+ const handleComplete = async () => {
281
+ setSubmitting(true);
282
+ setError('');
283
+ const agentKeys = Array.from(selectedAgents);
284
+
285
+ // Phase 1: Save config
286
+ setSetupPhase('saving');
287
+ let restartNeeded = false;
288
+ try {
289
+ restartNeeded = await saveConfig(state);
290
+ if (restartNeeded) setNeedsRestart(true);
291
+ } catch (e) {
292
+ setError(e instanceof Error ? e.message : String(e));
293
+ setSetupPhase('review');
294
+ setSubmitting(false);
295
+ return;
296
+ }
297
+
298
+ // Phase 2: Install agents
299
+ setSetupPhase('agents');
300
+ if (agentKeys.length > 0) {
301
+ const initialStatuses: Record<string, AgentInstallStatus> = {};
302
+ for (const key of agentKeys) initialStatuses[key] = { state: 'installing' };
303
+ setAgentStatuses(initialStatuses);
304
+
305
+ try {
306
+ const statuses = await installAgents(agentKeys, agents, agentTransport, agentScope, state.mcpPort, state.authToken);
307
+ setAgentStatuses(statuses);
308
+ } catch (e) {
309
+ console.warn('[SetupWizard] agent batch install failed:', e);
310
+ const errStatuses: Record<string, AgentInstallStatus> = {};
311
+ for (const key of agentKeys) errStatuses[key] = { state: 'error' };
312
+ setAgentStatuses(errStatuses);
313
+ }
314
+ }
315
+
316
+ // Phase 3: Install skill
317
+ setSetupPhase('skill');
318
+ try {
319
+ const skillData = await installSkill(state.template, agentKeys);
320
+ setSkillInstallResult(skillData);
321
+ } catch (e) {
322
+ console.warn('[SetupWizard] skill install failed:', e);
323
+ setSkillInstallResult({ error: 'Failed to install skill' });
324
+ }
325
+
326
+ setSubmitting(false);
327
+ setCompleted(true);
328
+ setSetupPhase('done');
329
+
330
+ if (restartNeeded) {
331
+ // Config changed requiring restart — stay on page, show restart block
332
+ return;
333
+ }
334
+ window.location.href = '/?welcome=1';
335
+ };
336
+
337
+ const retryAgent = useCallback(async (key: string) => {
338
+ setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
339
+ try {
340
+ const payload = buildAgentPayload(key, agents, agentTransport, agentScope);
341
+ const res = await fetch('/api/mcp/install', {
342
+ method: 'POST',
343
+ headers: { 'Content-Type': 'application/json' },
344
+ body: JSON.stringify({
345
+ agents: [payload],
346
+ transport: agentTransport,
347
+ url: `http://localhost:${state.mcpPort}/mcp`,
348
+ token: state.authToken || undefined,
349
+ }),
350
+ });
351
+ const data = await res.json();
352
+ if (data.results?.[0]) {
353
+ const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
354
+ setAgentStatuses(prev => ({ ...prev, [key]: parseInstallResult(r) }));
355
+ }
356
+ } catch (e) {
357
+ console.warn('[SetupWizard] retryAgent failed:', e);
358
+ setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
359
+ }
360
+ }, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
361
+
362
+ return (
363
+ <div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
364
+ role="dialog" aria-modal="true" aria-labelledby="setup-title"
365
+ style={{ background: 'var(--background)' }}>
366
+ <div className="w-full max-w-xl mx-auto px-6 py-12">
367
+ <div className="text-center mb-8">
368
+ <div className="inline-flex items-center gap-2 mb-2">
369
+ <Sparkles size={18} style={{ color: 'var(--amber)' }} />
370
+ <h1 id="setup-title" className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
371
+ MindOS
372
+ </h1>
373
+ </div>
374
+ </div>
375
+
376
+ <div className="flex justify-center">
377
+ <StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} disabled={submitting || completed} />
378
+ </div>
379
+
380
+ <h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
381
+ {s.stepTitles[step]}
382
+ </h2>
383
+
384
+ {step === 0 && <StepKB state={state} update={update} t={t} homeDir={homeDir} />}
385
+ {step === 1 && <StepAI state={state} update={update} s={s} />}
386
+ {step === 2 && (
387
+ <StepPorts
388
+ state={state} update={update}
389
+ webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
390
+ setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
391
+ checkPort={checkPort} portConflict={portConflict} s={s}
392
+ />
393
+ )}
394
+ {step === 3 && (
395
+ <StepSecurity
396
+ authToken={state.authToken} tokenCopied={tokenCopied}
397
+ onCopy={copyToken} onGenerate={generateToken}
398
+ webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
399
+ s={s}
400
+ />
401
+ )}
402
+ {step === 4 && (
403
+ <StepAgents
404
+ agents={agents} agentsLoading={agentsLoading}
405
+ selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
406
+ agentTransport={agentTransport} setAgentTransport={setAgentTransport}
407
+ agentScope={agentScope} setAgentScope={setAgentScope}
408
+ agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
409
+ template={state.template}
410
+ />
411
+ )}
412
+ {step === 5 && (
413
+ <StepReview
414
+ state={state} selectedAgents={selectedAgents}
415
+ agentStatuses={agentStatuses} onRetryAgent={retryAgent}
416
+ error={error} needsRestart={needsRestart}
417
+ s={s}
418
+ skillInstallResult={skillInstallResult}
419
+ setupPhase={setupPhase}
420
+ />
421
+ )}
422
+
423
+ {/* Navigation */}
424
+ <div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
425
+ <button
426
+ onClick={() => setStep(step - 1)}
427
+ disabled={step === 0 || submitting || completed}
428
+ className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
429
+ style={{ color: 'var(--foreground)' }}>
430
+ <ChevronLeft size={14} /> {s.back}
431
+ </button>
432
+
433
+ {step < TOTAL_STEPS - 1 ? (
434
+ <button
435
+ onClick={() => setStep(step + 1)}
436
+ disabled={!canNext()}
437
+ className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
438
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
439
+ {s.next} <ChevronRight size={14} />
440
+ </button>
441
+ ) : completed ? (
442
+ // After completing: show Done link (no restart needed) or nothing (RestartBlock handles it)
443
+ !needsRestart ? (
444
+ <a href="/?welcome=1"
445
+ className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
446
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
447
+ {s.completeDone} &rarr;
448
+ </a>
449
+ ) : null
450
+ ) : (
451
+ <button
452
+ onClick={handleComplete}
453
+ disabled={submitting}
454
+ className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
455
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
456
+ {submitting && <Loader2 size={14} className="animate-spin" />}
457
+ {submitting ? s.completing : s.complete}
458
+ </button>
459
+ )}
460
+ </div>
461
+ </div>
462
+ </div>
463
+ );
464
+ }
@@ -0,0 +1,53 @@
1
+ import type { Messages } from '@/lib/i18n';
2
+
3
+ // ─── i18n type aliases ───────────────────────────────────────────────────────
4
+ export type SetupMessages = Messages['setup'];
5
+ export type McpMessages = Messages['settings']['mcp'];
6
+
7
+ // ─── Template ────────────────────────────────────────────────────────────────
8
+ export type Template = 'en' | 'zh' | 'empty' | '';
9
+
10
+ // ─── Setup state ─────────────────────────────────────────────────────────────
11
+ export interface SetupState {
12
+ mindRoot: string;
13
+ template: Template;
14
+ provider: 'anthropic' | 'openai' | 'skip';
15
+ anthropicKey: string;
16
+ anthropicModel: string;
17
+ openaiKey: string;
18
+ openaiModel: string;
19
+ openaiBaseUrl: string;
20
+ webPort: number;
21
+ mcpPort: number;
22
+ authToken: string;
23
+ webPassword: string;
24
+ }
25
+
26
+ // ─── Port check ──────────────────────────────────────────────────────────────
27
+ export interface PortStatus {
28
+ checking: boolean;
29
+ available: boolean | null;
30
+ isSelf: boolean;
31
+ suggestion: number | null;
32
+ }
33
+
34
+ // ─── Agent types ─────────────────────────────────────────────────────────────
35
+ export interface AgentEntry {
36
+ key: string;
37
+ name: string;
38
+ present: boolean;
39
+ installed: boolean;
40
+ hasProjectScope: boolean;
41
+ hasGlobalScope: boolean;
42
+ preferredTransport: 'stdio' | 'http';
43
+ }
44
+
45
+ export type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
46
+
47
+ export interface AgentInstallStatus {
48
+ state: AgentInstallState;
49
+ message?: string;
50
+ transport?: string;
51
+ verified?: boolean;
52
+ verifyError?: string;
53
+ }
@@ -0,0 +1,19 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
3
+ const { readFileSync } = await import('fs');
4
+ const { join, resolve } = await import('path');
5
+ const { homedir } = await import('os');
6
+ try {
7
+ const configPath = join(homedir(), '.mindos', 'config.json');
8
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
9
+ if (config.sync?.enabled && config.mindRoot) {
10
+ // Resolve absolute path to avoid Turbopack bundling issues
11
+ const syncModule = resolve(process.cwd(), '..', 'bin', 'lib', 'sync.js');
12
+ const { startSyncDaemon } = await import(/* webpackIgnore: true */ syncModule);
13
+ await startSyncDaemon(config.mindRoot);
14
+ }
15
+ } catch {
16
+ // Sync not configured or failed to start — silently skip
17
+ }
18
+ }
19
+ }
package/app/lib/i18n.ts CHANGED
@@ -119,6 +119,15 @@ export const messages = {
119
119
  resetToEnv: 'Reset to env value',
120
120
  restoreFromEnv: 'Restore from env',
121
121
  noApiKey: 'API key is not set. AI features will be unavailable until you add one.',
122
+ testKey: 'Test',
123
+ testKeyTesting: 'Testing...',
124
+ testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
125
+ testKeyAuthError: 'Invalid API key',
126
+ testKeyModelNotFound: 'Model not found',
127
+ testKeyRateLimited: 'Rate limited, try again later',
128
+ testKeyNetworkError: 'Network error',
129
+ testKeyNoKey: 'No API key configured',
130
+ testKeyUnknown: 'Test failed',
122
131
  },
123
132
  appearance: {
124
133
  readingFont: 'Reading font',
@@ -230,7 +239,8 @@ export const messages = {
230
239
  skillLangZh: '中文',
231
240
  selectDetected: 'Select Detected',
232
241
  clearSelection: 'Clear',
233
- }, save: 'Save',
242
+ },
243
+ save: 'Save',
234
244
  saved: 'Saved',
235
245
  saveFailed: 'Save failed',
236
246
  reconfigure: 'Reconfigure',
@@ -286,7 +296,6 @@ export const messages = {
286
296
  kbPathHint: 'Absolute path to your notes directory.',
287
297
  kbPathDefault: '~/MindOS/mind',
288
298
  kbPathUseDefault: (path: string) => `Use ${path}`,
289
- kbPathExists: (n: number) => `Directory already has ${n} file(s) — template will not be applied.`,
290
299
  kbPathHasFiles: (n: number) => `This directory already contains ${n} file${n > 1 ? 's' : ''}. You can skip the template or merge (existing files won't be overwritten).`,
291
300
  kbTemplateSkip: 'Skip template',
292
301
  kbTemplateMerge: 'Choose a template to merge',
@@ -514,6 +523,15 @@ export const messages = {
514
523
  resetToEnv: '恢复为环境变量',
515
524
  restoreFromEnv: '从环境变量恢复',
516
525
  noApiKey: 'API 密钥未设置,AI 功能暂不可用,请在此填写。',
526
+ testKey: '测试',
527
+ testKeyTesting: '测试中...',
528
+ testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
529
+ testKeyAuthError: 'API Key 无效',
530
+ testKeyModelNotFound: '模型不存在',
531
+ testKeyRateLimited: '请求频率限制,稍后重试',
532
+ testKeyNetworkError: '网络错误',
533
+ testKeyNoKey: '未配置 API Key',
534
+ testKeyUnknown: '测试失败',
517
535
  },
518
536
  appearance: {
519
537
  readingFont: '正文字体',
@@ -625,7 +643,8 @@ export const messages = {
625
643
  skillLangZh: '中文',
626
644
  selectDetected: '选择已检测',
627
645
  clearSelection: '清除',
628
- }, save: '保存',
646
+ },
647
+ save: '保存',
629
648
  saved: '已保存',
630
649
  saveFailed: '保存失败',
631
650
  reconfigure: '重新配置',
@@ -681,7 +700,6 @@ export const messages = {
681
700
  kbPathHint: '笔记目录的绝对路径。',
682
701
  kbPathDefault: '~/MindOS/mind',
683
702
  kbPathUseDefault: (path: string) => `使用 ${path}`,
684
- kbPathExists: (n: number) => `目录已有 ${n} 个文件 — 将不会应用模板。`,
685
703
  kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
686
704
  kbTemplateSkip: '跳过模板',
687
705
  kbTemplateMerge: '选择模板合并',
@@ -3,7 +3,7 @@ import path from "path";
3
3
 
4
4
  const nextConfig: NextConfig = {
5
5
  transpilePackages: ['github-slugger'],
6
- serverExternalPackages: ['pdfjs-dist', 'pdf-parse'],
6
+ serverExternalPackages: ['pdfjs-dist', 'pdf-parse', 'chokidar'],
7
7
  outputFileTracingRoot: path.join(__dirname),
8
8
  turbopack: {
9
9
  root: path.join(__dirname),
package/bin/cli.js CHANGED
@@ -803,7 +803,14 @@ ${bold('Examples:')}
803
803
  }
804
804
 
805
805
  if (sub === 'now') {
806
- manualSync(mindRoot);
806
+ try {
807
+ console.log(dim('Pulling...'));
808
+ manualSync(mindRoot);
809
+ console.log(green('✔ Sync complete'));
810
+ } catch (err) {
811
+ console.error(red(err.message));
812
+ process.exit(1);
813
+ }
807
814
  return;
808
815
  }
809
816