@geminilight/mindos 0.5.10 → 0.5.12

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 (43) hide show
  1. package/README.md +9 -9
  2. package/README_zh.md +9 -9
  3. package/app/README.md +2 -2
  4. package/app/app/api/ask/route.ts +126 -3
  5. package/app/app/api/mcp/install/route.ts +1 -1
  6. package/app/app/api/mcp/status/route.ts +1 -1
  7. package/app/app/api/settings/route.ts +1 -1
  8. package/app/app/api/settings/test-key/route.ts +111 -0
  9. package/app/app/api/setup/route.ts +7 -7
  10. package/app/app/api/sync/route.ts +30 -40
  11. package/app/components/AskModal.tsx +28 -21
  12. package/app/components/ask/MessageList.tsx +62 -3
  13. package/app/components/ask/ToolCallBlock.tsx +89 -0
  14. package/app/components/settings/AiTab.tsx +120 -2
  15. package/app/components/setup/StepReview.tsx +31 -25
  16. package/app/components/setup/index.tsx +6 -3
  17. package/app/instrumentation.ts +19 -0
  18. package/app/lib/agent/prompt.ts +32 -0
  19. package/app/lib/agent/stream-consumer.ts +178 -0
  20. package/app/lib/agent/tools.ts +122 -0
  21. package/app/lib/i18n.ts +18 -0
  22. package/app/lib/types.ts +18 -0
  23. package/app/next-env.d.ts +1 -1
  24. package/app/next.config.ts +1 -1
  25. package/app/package.json +2 -2
  26. package/bin/cli.js +49 -22
  27. package/bin/lib/gateway.js +24 -3
  28. package/bin/lib/mcp-install.js +2 -2
  29. package/bin/lib/mcp-spawn.js +3 -3
  30. package/bin/lib/stop.js +1 -1
  31. package/bin/lib/sync.js +61 -11
  32. package/mcp/README.md +5 -5
  33. package/mcp/src/index.ts +2 -2
  34. package/package.json +4 -2
  35. package/scripts/setup.js +12 -12
  36. package/scripts/upgrade-prompt.md +6 -6
  37. package/assets/images/demo-flow-dark.png +0 -0
  38. package/assets/images/demo-flow-light.png +0 -0
  39. package/assets/images/demo-flow-zh-dark.png +0 -0
  40. package/assets/images/demo-flow-zh-light.png +0 -0
  41. package/assets/images/gui-sync-cv.png +0 -0
  42. package/assets/images/wechat-qr.png +0 -0
  43. package/mcp/package-lock.json +0 -1717
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect } from 'react';
4
- import { Sparkles, Loader2, AlertCircle } from 'lucide-react';
4
+ import { Sparkles, Loader2, AlertCircle, Wrench } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import type { Message } from '@/lib/types';
8
+ import ToolCallBlock from './ToolCallBlock';
8
9
 
9
10
  function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
10
11
  return (
@@ -28,6 +29,57 @@ function AssistantMessage({ content, isStreaming }: { content: string; isStreami
28
29
  );
29
30
  }
30
31
 
32
+ function AssistantMessageWithParts({ message, isStreaming }: { message: Message; isStreaming: boolean }) {
33
+ const parts = message.parts;
34
+ if (!parts || parts.length === 0) {
35
+ // Fallback to plain text rendering
36
+ return message.content ? (
37
+ <AssistantMessage content={message.content} isStreaming={isStreaming} />
38
+ ) : null;
39
+ }
40
+
41
+ // Check if the last part is a running tool call — show a spinner after it
42
+ const lastPart = parts[parts.length - 1];
43
+ const showTrailingSpinner = isStreaming && lastPart.type === 'tool-call' && (lastPart.state === 'running' || lastPart.state === 'pending');
44
+
45
+ return (
46
+ <div>
47
+ {parts.map((part, idx) => {
48
+ if (part.type === 'text') {
49
+ const isLastTextPart = isStreaming && idx === parts.length - 1;
50
+ return part.text ? (
51
+ <AssistantMessage key={idx} content={part.text} isStreaming={isLastTextPart} />
52
+ ) : null;
53
+ }
54
+ if (part.type === 'tool-call') {
55
+ return <ToolCallBlock key={part.toolCallId} part={part} />;
56
+ }
57
+ return null;
58
+ })}
59
+ {showTrailingSpinner && (
60
+ <div className="flex items-center gap-2 py-1 mt-1">
61
+ <Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
62
+ <span className="text-xs text-muted-foreground animate-pulse">Executing tool…</span>
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function StepCounter({ parts, maxSteps }: { parts: Message['parts']; maxSteps?: number }) {
70
+ if (!parts) return null;
71
+ const toolCalls = parts.filter(p => p.type === 'tool-call');
72
+ if (toolCalls.length === 0) return null;
73
+ const lastToolCall = toolCalls[toolCalls.length - 1];
74
+ const toolLabel = lastToolCall.type === 'tool-call' ? lastToolCall.toolName : '';
75
+ return (
76
+ <div className="flex items-center gap-1.5 mt-1.5 text-xs text-muted-foreground/70">
77
+ <Wrench size={10} />
78
+ <span>Step {toolCalls.length}{maxSteps ? `/${maxSteps}` : ''}{toolLabel ? ` — ${toolLabel}` : ''}</span>
79
+ </div>
80
+ );
81
+ }
82
+
31
83
  interface MessageListProps {
32
84
  messages: Message[];
33
85
  isLoading: boolean;
@@ -35,6 +87,7 @@ interface MessageListProps {
35
87
  emptyPrompt: string;
36
88
  suggestions: readonly string[];
37
89
  onSuggestionClick: (text: string) => void;
90
+ maxSteps?: number;
38
91
  labels: {
39
92
  connecting: string;
40
93
  thinking: string;
@@ -49,6 +102,7 @@ export default function MessageList({
49
102
  emptyPrompt,
50
103
  suggestions,
51
104
  onSuggestionClick,
105
+ maxSteps,
52
106
  labels,
53
107
  }: MessageListProps) {
54
108
  const endRef = useRef<HTMLDivElement>(null);
@@ -102,8 +156,13 @@ export default function MessageList({
102
156
  </div>
103
157
  ) : (
104
158
  <div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
105
- {m.content ? (
106
- <AssistantMessage content={m.content} isStreaming={isLoading && i === messages.length - 1} />
159
+ {(m.parts && m.parts.length > 0) || m.content ? (
160
+ <>
161
+ <AssistantMessageWithParts message={m} isStreaming={isLoading && i === messages.length - 1} />
162
+ {isLoading && i === messages.length - 1 && (
163
+ <StepCounter parts={m.parts} maxSteps={maxSteps} />
164
+ )}
165
+ </>
107
166
  ) : isLoading && i === messages.length - 1 ? (
108
167
  <div className="flex items-center gap-2 py-1">
109
168
  <Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { ChevronRight, ChevronDown, Loader2, CheckCircle2, XCircle } from 'lucide-react';
5
+ import type { ToolCallPart } from '@/lib/types';
6
+
7
+ const TOOL_ICONS: Record<string, string> = {
8
+ search: '🔍',
9
+ list_files: '📂',
10
+ read_file: '📖',
11
+ write_file: '✏️',
12
+ create_file: '📄',
13
+ append_to_file: '📝',
14
+ insert_after_heading: '📌',
15
+ update_section: '✏️',
16
+ delete_file: '🗑️',
17
+ rename_file: '📝',
18
+ move_file: '📦',
19
+ get_backlinks: '🔗',
20
+ get_history: '📜',
21
+ get_file_at_version: '⏪',
22
+ get_recent: '🕐',
23
+ append_csv: '📊',
24
+ };
25
+
26
+ function formatInput(input: unknown): string {
27
+ if (!input || typeof input !== 'object') return String(input ?? '');
28
+ const obj = input as Record<string, unknown>;
29
+ const parts: string[] = [];
30
+ for (const val of Object.values(obj)) {
31
+ if (typeof val === 'string') {
32
+ parts.push(val.length > 60 ? `${val.slice(0, 60)}…` : val);
33
+ } else if (Array.isArray(val)) {
34
+ parts.push(`[${val.length} items]`);
35
+ } else if (val !== undefined && val !== null) {
36
+ parts.push(String(val));
37
+ }
38
+ }
39
+ return parts.join(', ');
40
+ }
41
+
42
+ function truncateOutput(output: string, maxLen = 200): string {
43
+ if (output.length <= maxLen) return output;
44
+ return output.slice(0, maxLen) + '…';
45
+ }
46
+
47
+ export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
48
+ const [expanded, setExpanded] = useState(false);
49
+ const icon = TOOL_ICONS[part.toolName] ?? '🔧';
50
+ const inputSummary = formatInput(part.input);
51
+
52
+ return (
53
+ <div className="my-1 rounded-md border border-border/50 bg-muted/30 text-xs font-mono">
54
+ <button
55
+ type="button"
56
+ onClick={() => setExpanded(v => !v)}
57
+ className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/50 transition-colors rounded-md"
58
+ >
59
+ {expanded ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" /> : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
60
+ <span>{icon}</span>
61
+ <span className="text-foreground font-medium">{part.toolName}</span>
62
+ <span className="text-muted-foreground truncate flex-1">({inputSummary})</span>
63
+ <span className="shrink-0 ml-auto">
64
+ {part.state === 'pending' || part.state === 'running' ? (
65
+ <Loader2 size={12} className="animate-spin text-amber-500" />
66
+ ) : part.state === 'done' ? (
67
+ <CheckCircle2 size={12} className="text-success" />
68
+ ) : (
69
+ <XCircle size={12} className="text-error" />
70
+ )}
71
+ </span>
72
+ </button>
73
+ {expanded && (
74
+ <div className="px-2 pb-2 pt-0.5 border-t border-border/30 space-y-1">
75
+ <div className="text-muted-foreground">
76
+ <span className="font-semibold">Input: </span>
77
+ <span className="break-all whitespace-pre-wrap">{JSON.stringify(part.input, null, 2)}</span>
78
+ </div>
79
+ {part.output !== undefined && (
80
+ <div className="text-muted-foreground">
81
+ <span className="font-semibold">Output: </span>
82
+ <span className="break-all whitespace-pre-wrap">{truncateOutput(part.output)}</span>
83
+ </div>
84
+ )}
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ }
@@ -1,9 +1,30 @@
1
1
  'use client';
2
2
 
3
- import { AlertCircle } from 'lucide-react';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { AlertCircle, Loader2 } from 'lucide-react';
4
5
  import type { AiSettings, ProviderConfig, SettingsData } from './types';
5
6
  import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
6
7
 
8
+ type TestState = 'idle' | 'testing' | 'ok' | 'error';
9
+ type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
10
+
11
+ interface TestResult {
12
+ state: TestState;
13
+ latency?: number;
14
+ error?: string;
15
+ code?: ErrorCode;
16
+ }
17
+
18
+ function errorMessage(t: any, code?: ErrorCode): string {
19
+ switch (code) {
20
+ case 'auth_error': return t.settings.ai.testKeyAuthError;
21
+ case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
22
+ case 'rate_limited': return t.settings.ai.testKeyRateLimited;
23
+ case 'network_error': return t.settings.ai.testKeyNetworkError;
24
+ default: return t.settings.ai.testKeyUnknown;
25
+ }
26
+ }
27
+
7
28
  interface AiTabProps {
8
29
  data: SettingsData;
9
30
  updateAi: (patch: Partial<AiSettings>) => void;
@@ -15,13 +36,76 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
15
36
  const envVal = data.envValues ?? {};
16
37
  const provider = data.ai.provider;
17
38
 
18
- function patchProvider(name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) {
39
+ // --- Test key state ---
40
+ const [testResult, setTestResult] = useState<Record<string, TestResult>>({});
41
+ const okTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
42
+ const prevProviderRef = useRef(provider);
43
+
44
+ // Reset test result when provider changes
45
+ useEffect(() => {
46
+ if (prevProviderRef.current !== provider) {
47
+ prevProviderRef.current = provider;
48
+ setTestResult({});
49
+ if (okTimerRef.current) { clearTimeout(okTimerRef.current); okTimerRef.current = undefined; }
50
+ }
51
+ }, [provider]);
52
+
53
+ // Cleanup ok timer
54
+ useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
55
+
56
+ const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
57
+ const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
58
+ setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
59
+
60
+ try {
61
+ const body: Record<string, string> = { provider: providerName };
62
+ if (prov.apiKey) body.apiKey = prov.apiKey;
63
+ if (prov.model) body.model = prov.model;
64
+ if (providerName === 'openai' && prov.baseUrl) body.baseUrl = prov.baseUrl;
65
+
66
+ const res = await fetch('/api/settings/test-key', {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify(body),
70
+ });
71
+ const json = await res.json();
72
+
73
+ if (json.ok) {
74
+ setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
75
+ // Auto-clear after 5s
76
+ if (okTimerRef.current) clearTimeout(okTimerRef.current);
77
+ okTimerRef.current = setTimeout(() => {
78
+ setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
79
+ }, 5000);
80
+ } else {
81
+ setTestResult(prev => ({
82
+ ...prev,
83
+ [providerName]: { state: 'error', error: json.error, code: json.code },
84
+ }));
85
+ }
86
+ } catch {
87
+ setTestResult(prev => ({
88
+ ...prev,
89
+ [providerName]: { state: 'error', code: 'network_error', error: 'Network error' },
90
+ }));
91
+ }
92
+ }, [data.ai.providers]);
93
+
94
+ // Reset test result when key changes
95
+ const patchProviderWithReset = useCallback((name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) => {
96
+ if ('apiKey' in patch) {
97
+ setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
98
+ }
19
99
  updateAi({
20
100
  providers: {
21
101
  ...data.ai.providers,
22
102
  [name]: { ...data.ai.providers?.[name], ...patch },
23
103
  },
24
104
  });
105
+ }, [data.ai.providers, updateAi]);
106
+
107
+ function patchProvider(name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) {
108
+ patchProviderWithReset(name, patch);
25
109
  }
26
110
 
27
111
  const anthropic = data.ai.providers?.anthropic ?? { apiKey: '', model: '' };
@@ -31,6 +115,38 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
31
115
  const activeEnvKey = provider === 'anthropic' ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY;
32
116
  const missingApiKey = !activeApiKey && !activeEnvKey;
33
117
 
118
+ // Test button helper
119
+ const renderTestButton = (providerName: 'anthropic' | 'openai', hasKey: boolean, hasEnv: boolean) => {
120
+ const result = testResult[providerName] ?? { state: 'idle' as TestState };
121
+ const disabled = result.state === 'testing' || (!hasKey && !hasEnv);
122
+
123
+ return (
124
+ <div className="flex items-center gap-2 mt-1.5">
125
+ <button
126
+ type="button"
127
+ disabled={disabled}
128
+ onClick={() => handleTestKey(providerName)}
129
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
130
+ >
131
+ {result.state === 'testing' ? (
132
+ <>
133
+ <Loader2 size={12} className="animate-spin" />
134
+ {t.settings.ai.testKeyTesting}
135
+ </>
136
+ ) : (
137
+ t.settings.ai.testKey
138
+ )}
139
+ </button>
140
+ {result.state === 'ok' && result.latency != null && (
141
+ <span className="text-xs text-success">{t.settings.ai.testKeyOk(result.latency)}</span>
142
+ )}
143
+ {result.state === 'error' && (
144
+ <span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
145
+ )}
146
+ </div>
147
+ );
148
+ };
149
+
34
150
  return (
35
151
  <div className="space-y-5">
36
152
  <Field label={<>{t.settings.ai.provider} <EnvBadge overridden={env.AI_PROVIDER} /></>}>
@@ -60,6 +176,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
60
176
  value={anthropic.apiKey}
61
177
  onChange={v => patchProvider('anthropic', { apiKey: v })}
62
178
  />
179
+ {renderTestButton('anthropic', !!anthropic.apiKey, !!env.ANTHROPIC_API_KEY)}
63
180
  </Field>
64
181
  </>
65
182
  ) : (
@@ -79,6 +196,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
79
196
  value={openai.apiKey}
80
197
  onChange={v => patchProvider('openai', { apiKey: v })}
81
198
  />
199
+ {renderTestButton('openai', !!openai.apiKey, !!env.OPENAI_API_KEY)}
82
200
  </Field>
83
201
  <Field
84
202
  label={<>{t.settings.ai.baseUrl} <EnvBadge overridden={env.OPENAI_BASE_URL} /></>}
@@ -7,7 +7,24 @@ import {
7
7
  import type { SetupState, SetupMessages, AgentInstallStatus } from './types';
8
8
 
9
9
  // ─── Restart Block ────────────────────────────────────────────────────────────
10
- function RestartBlock({ s, newPort }: { s: SetupMessages; newPort: number }) {
10
+
11
+ /** Restart warning banner — shown in the content area */
12
+ export function RestartBanner({ s }: { s: SetupMessages }) {
13
+ return (
14
+ <div className="space-y-2">
15
+ <div className="p-3 rounded-lg text-sm flex items-center gap-2"
16
+ style={{ background: 'color-mix(in srgb, var(--amber) 10%, transparent)', color: 'var(--amber)' }}>
17
+ <AlertTriangle size={14} /> {s.restartRequired}
18
+ </div>
19
+ <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
20
+ {s.restartManual} <code className="font-mono">mindos start</code>
21
+ </p>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ /** Restart button — shown in the bottom navigation bar (same position as Complete/Saving button) */
27
+ export function RestartButton({ s, newPort }: { s: SetupMessages; newPort: number }) {
11
28
  const [restarting, setRestarting] = useState(false);
12
29
  const [done, setDone] = useState(false);
13
30
  const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
@@ -40,34 +57,23 @@ function RestartBlock({ s, newPort }: { s: SetupMessages; newPort: number }) {
40
57
 
41
58
  if (done) {
42
59
  return (
43
- <div className="p-3 rounded-lg text-sm flex items-center gap-2"
44
- style={{ background: 'color-mix(in srgb, var(--success) 10%, transparent)', color: 'var(--success)' }}>
60
+ <span className="flex items-center gap-1.5 px-5 py-2 text-sm font-medium rounded-lg"
61
+ style={{ background: 'color-mix(in srgb, var(--success) 15%, transparent)', color: 'var(--success)' }}>
45
62
  <CheckCircle2 size={14} /> {s.restartDone}
46
- </div>
63
+ </span>
47
64
  );
48
65
  }
49
66
 
50
67
  return (
51
- <div className="space-y-3">
52
- <div className="p-3 rounded-lg text-sm flex items-center gap-2"
53
- style={{ background: 'color-mix(in srgb, var(--amber) 10%, transparent)', color: 'var(--amber)' }}>
54
- <AlertTriangle size={14} /> {s.restartRequired}
55
- </div>
56
- <div className="flex items-center gap-3">
57
- <button
58
- type="button"
59
- onClick={handleRestart}
60
- disabled={restarting}
61
- className="flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-50"
62
- style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
63
- {restarting ? <Loader2 size={13} className="animate-spin" /> : null}
64
- {restarting ? s.restarting : s.restartNow}
65
- </button>
66
- <span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
67
- {s.restartManual} <code className="font-mono">mindos start</code>
68
- </span>
69
- </div>
70
- </div>
68
+ <button
69
+ type="button"
70
+ onClick={handleRestart}
71
+ disabled={restarting}
72
+ className="flex items-center gap-1.5 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
73
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
74
+ {restarting ? <Loader2 size={13} className="animate-spin" /> : null}
75
+ {restarting ? s.restarting : s.restartNow}
76
+ </button>
71
77
  );
72
78
  }
73
79
 
@@ -205,7 +211,7 @@ export default function StepReview({
205
211
  {s.completeFailed}: {error}
206
212
  </div>
207
213
  )}
208
- {needsRestart && setupPhase === 'done' && <RestartBlock s={s} newPort={state.webPort} />}
214
+ {needsRestart && setupPhase === 'done' && <RestartBanner s={s} />}
209
215
  </div>
210
216
  );
211
217
  }
@@ -11,6 +11,7 @@ import StepPorts from './StepPorts';
11
11
  import StepSecurity from './StepSecurity';
12
12
  import StepAgents from './StepAgents';
13
13
  import StepReview from './StepReview';
14
+ import { RestartButton } from './StepReview';
14
15
  import StepDots from './StepDots';
15
16
 
16
17
  // ─── Helpers (shared by handleComplete + retryAgent) ─────────────────────────
@@ -439,14 +440,16 @@ export default function SetupWizard() {
439
440
  {s.next} <ChevronRight size={14} />
440
441
  </button>
441
442
  ) : completed ? (
442
- // After completing: show Done link (no restart needed) or nothing (RestartBlock handles it)
443
- !needsRestart ? (
443
+ // After completing: show Done link or Restart button in the same position
444
+ needsRestart ? (
445
+ <RestartButton s={s} newPort={state.webPort} />
446
+ ) : (
444
447
  <a href="/?welcome=1"
445
448
  className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
446
449
  style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
447
450
  {s.completeDone} &rarr;
448
451
  </a>
449
- ) : null
452
+ )
450
453
  ) : (
451
454
  <button
452
455
  onClick={handleComplete}
@@ -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
+ }
@@ -17,6 +17,38 @@ Tool policy:
17
17
  - Prefer targeted edits (update_section / insert_after_heading / append_to_file) over full overwrite.
18
18
  - Use write_file only when replacing the whole file is required.
19
19
  - INSTRUCTION.md is read-only and must not be modified.
20
+ - Use append_csv for adding rows to CSV files instead of rewriting the whole file.
21
+ - Use get_backlinks before renaming/moving/deleting to understand impact on other files.
22
+
23
+ Destructive operations (use with caution):
24
+ - delete_file: permanently removes a file — cannot be undone
25
+ - move_file: changes file location — may break links in other files
26
+ - write_file: overwrites entire file content — prefer partial edits
27
+ Before executing destructive operations:
28
+ - Before delete_file: list what links to this file (get_backlinks), warn user about impact
29
+ - Before move_file: same — check backlinks first
30
+ - Before write_file (full overwrite): confirm with user that full replacement is intended
31
+ - NEVER chain multiple destructive operations without pausing to summarize what you've done
32
+
33
+ File management tools:
34
+ - rename_file: rename within same directory
35
+ - move_file: move to a different path (reports affected backlinks)
36
+ - get_backlinks: find all files that link to a given file
37
+
38
+ Git history tools:
39
+ - get_history: view commit log for a file
40
+ - get_file_at_version: read file content at a past commit (use get_history first to find hashes)
41
+
42
+ Complex task protocol:
43
+ 1. PLAN: For multi-step tasks, first output a numbered plan
44
+ 2. EXECUTE: Execute steps one by one, reporting progress
45
+ 3. VERIFY: After edits, re-read the file to confirm correctness
46
+ 4. SUMMARIZE: Conclude with a summary and suggest follow-up actions if relevant
47
+
48
+ Step awareness:
49
+ - You have a limited number of steps (configured by user, typically 10-30).
50
+ - If a tool call fails or returns unexpected results, do NOT retry with the same arguments.
51
+ - Try a different approach or ask the user for clarification.
20
52
 
21
53
  Uploaded files:
22
54
  - Users may upload local files (PDF, txt, csv, etc.) via the chat interface.