@geminilight/mindos 0.1.9 → 0.2.1
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.
- package/README.md +42 -12
- package/README_zh.md +38 -5
- package/app/README.md +1 -1
- package/app/app/api/init/route.ts +56 -0
- package/app/app/api/sync/route.ts +124 -0
- package/app/app/layout.tsx +10 -1
- package/app/app/register-sw.tsx +15 -0
- package/app/components/HomeContent.tsx +8 -2
- package/app/components/OnboardingView.tsx +161 -0
- package/app/components/SettingsModal.tsx +10 -1
- package/app/components/Sidebar.tsx +28 -4
- package/app/components/SyncStatusBar.tsx +273 -0
- package/app/components/renderers/AgentInspectorRenderer.tsx +8 -5
- package/app/components/settings/SyncTab.tsx +311 -0
- package/app/components/settings/types.ts +1 -1
- package/app/lib/agent/log.ts +44 -0
- package/app/lib/agent/tools.ts +39 -18
- package/app/lib/i18n.ts +80 -2
- package/app/lib/renderers/index.ts +13 -0
- package/app/lib/settings.ts +2 -2
- package/app/public/icons/icon-192.png +0 -0
- package/app/public/icons/icon-512.png +0 -0
- package/app/public/manifest.json +26 -0
- package/app/public/sw.js +66 -0
- package/bin/cli.js +214 -10
- package/bin/lib/config.js +12 -1
- package/bin/lib/mcp-install.js +225 -70
- package/bin/lib/startup.js +24 -1
- package/bin/lib/sync.js +367 -0
- package/mcp/src/index.ts +37 -10
- package/package.json +6 -2
- package/scripts/release.sh +56 -0
- package/scripts/setup.js +35 -6
- package/templates/README.md +1 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { RefreshCw, AlertCircle, CheckCircle2, Loader2, GitBranch, Copy, Check, ExternalLink } from 'lucide-react';
|
|
5
|
+
import { SectionLabel } from './Primitives';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
|
|
8
|
+
export interface SyncStatus {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
provider?: string;
|
|
11
|
+
remote?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
lastSync?: string | null;
|
|
14
|
+
lastPull?: string | null;
|
|
15
|
+
unpushed?: string;
|
|
16
|
+
conflicts?: Array<{ file: string; time: string }>;
|
|
17
|
+
lastError?: string | null;
|
|
18
|
+
autoCommitInterval?: number;
|
|
19
|
+
autoPullInterval?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SyncTabProps {
|
|
23
|
+
t: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function timeAgo(iso: string | null | undefined): string {
|
|
27
|
+
if (!iso) return 'never';
|
|
28
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
29
|
+
if (diff < 60000) return 'just now';
|
|
30
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
31
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
32
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Copy-to-clipboard button ──────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
function CopyButton({ text }: { text: string }) {
|
|
38
|
+
const [copied, setCopied] = useState(false);
|
|
39
|
+
const handleCopy = async () => {
|
|
40
|
+
await navigator.clipboard.writeText(text);
|
|
41
|
+
setCopied(true);
|
|
42
|
+
setTimeout(() => setCopied(false), 2000);
|
|
43
|
+
};
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={handleCopy}
|
|
48
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
|
49
|
+
title="Copy command"
|
|
50
|
+
>
|
|
51
|
+
{copied ? <Check size={12} className="text-green-500" /> : <Copy size={12} />}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── Empty state (Task D) ──────────────────────────────────────── */
|
|
57
|
+
|
|
58
|
+
function SyncEmptyState({ t }: { t: any }) {
|
|
59
|
+
const syncT = t.settings?.sync;
|
|
60
|
+
const cmd = 'mindos sync init';
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-6">
|
|
64
|
+
{/* Header */}
|
|
65
|
+
<div className="flex items-center gap-3">
|
|
66
|
+
<div className="w-9 h-9 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
|
67
|
+
<GitBranch size={18} className="text-muted-foreground" />
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<h3 className="text-sm font-medium text-foreground">
|
|
71
|
+
{syncT?.emptyTitle ?? 'Cross-device Sync'}
|
|
72
|
+
</h3>
|
|
73
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
74
|
+
{syncT?.emptyDesc ?? 'Automatically sync your knowledge base across devices via Git.'}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Steps */}
|
|
80
|
+
<div className="space-y-3">
|
|
81
|
+
<SectionLabel>{syncT?.emptyStepsTitle ?? 'Setup'}</SectionLabel>
|
|
82
|
+
<ol className="space-y-2.5 text-xs text-muted-foreground">
|
|
83
|
+
<li className="flex items-start gap-2.5">
|
|
84
|
+
<span className="w-5 h-5 rounded-full bg-muted flex items-center justify-center shrink-0 text-[10px] font-medium text-foreground mt-0.5">1</span>
|
|
85
|
+
<span>{syncT?.emptyStep1 ?? 'Create a private Git repo (GitHub, GitLab, etc.) or use an existing one.'}</span>
|
|
86
|
+
</li>
|
|
87
|
+
<li className="flex items-start gap-2.5">
|
|
88
|
+
<span className="w-5 h-5 rounded-full bg-muted flex items-center justify-center shrink-0 text-[10px] font-medium text-foreground mt-0.5">2</span>
|
|
89
|
+
<div className="flex-1">
|
|
90
|
+
<span>{syncT?.emptyStep2 ?? 'Run this command in your terminal:'}</span>
|
|
91
|
+
<div className="flex items-center gap-1.5 mt-1.5 px-3 py-2 bg-muted rounded-lg font-mono text-xs text-foreground">
|
|
92
|
+
<code className="flex-1 select-all">{cmd}</code>
|
|
93
|
+
<CopyButton text={cmd} />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</li>
|
|
97
|
+
<li className="flex items-start gap-2.5">
|
|
98
|
+
<span className="w-5 h-5 rounded-full bg-muted flex items-center justify-center shrink-0 text-[10px] font-medium text-foreground mt-0.5">3</span>
|
|
99
|
+
<span>{syncT?.emptyStep3 ?? 'Follow the prompts to connect your repo. Sync starts automatically.'}</span>
|
|
100
|
+
</li>
|
|
101
|
+
</ol>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Features */}
|
|
105
|
+
<div className="grid grid-cols-2 gap-2 text-[11px] text-muted-foreground">
|
|
106
|
+
{[
|
|
107
|
+
syncT?.featureAutoCommit ?? 'Auto-commit on save',
|
|
108
|
+
syncT?.featureAutoPull ?? 'Auto-pull from remote',
|
|
109
|
+
syncT?.featureConflict ?? 'Conflict detection',
|
|
110
|
+
syncT?.featureMultiDevice ?? 'Works across devices',
|
|
111
|
+
].map((f, i) => (
|
|
112
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
113
|
+
<CheckCircle2 size={11} className="text-green-500/60 shrink-0" />
|
|
114
|
+
<span>{f}</span>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── Main SyncTab ──────────────────────────────────────────────── */
|
|
123
|
+
|
|
124
|
+
export function SyncTab({ t }: SyncTabProps) {
|
|
125
|
+
const [status, setStatus] = useState<SyncStatus | null>(null);
|
|
126
|
+
const [loading, setLoading] = useState(true);
|
|
127
|
+
const [syncing, setSyncing] = useState(false);
|
|
128
|
+
const [toggling, setToggling] = useState(false);
|
|
129
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
130
|
+
|
|
131
|
+
const fetchStatus = useCallback(async () => {
|
|
132
|
+
try {
|
|
133
|
+
const data = await apiFetch<SyncStatus>('/api/sync');
|
|
134
|
+
setStatus(data);
|
|
135
|
+
} catch {
|
|
136
|
+
setStatus(null);
|
|
137
|
+
} finally {
|
|
138
|
+
setLoading(false);
|
|
139
|
+
}
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
useEffect(() => { fetchStatus(); }, [fetchStatus]);
|
|
143
|
+
|
|
144
|
+
const handleSyncNow = async () => {
|
|
145
|
+
setSyncing(true);
|
|
146
|
+
setMessage(null);
|
|
147
|
+
try {
|
|
148
|
+
await apiFetch('/api/sync', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ action: 'now' }),
|
|
152
|
+
});
|
|
153
|
+
setMessage({ type: 'success', text: 'Sync complete' });
|
|
154
|
+
await fetchStatus();
|
|
155
|
+
} catch {
|
|
156
|
+
setMessage({ type: 'error', text: 'Sync failed' });
|
|
157
|
+
} finally {
|
|
158
|
+
setSyncing(false);
|
|
159
|
+
setTimeout(() => setMessage(null), 3000);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleToggle = async () => {
|
|
164
|
+
if (!status) return;
|
|
165
|
+
setToggling(true);
|
|
166
|
+
setMessage(null);
|
|
167
|
+
const action = status.enabled ? 'off' : 'on';
|
|
168
|
+
try {
|
|
169
|
+
await apiFetch('/api/sync', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ action }),
|
|
173
|
+
});
|
|
174
|
+
await fetchStatus();
|
|
175
|
+
setMessage({ type: 'success', text: status.enabled ? 'Auto-sync disabled' : 'Auto-sync enabled' });
|
|
176
|
+
} catch {
|
|
177
|
+
setMessage({ type: 'error', text: 'Failed to toggle sync' });
|
|
178
|
+
} finally {
|
|
179
|
+
setToggling(false);
|
|
180
|
+
setTimeout(() => setMessage(null), 3000);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (loading) {
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex justify-center py-8">
|
|
187
|
+
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!status || !status.enabled) {
|
|
193
|
+
return <SyncEmptyState t={t} />;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const conflicts = status.conflicts || [];
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="space-y-5">
|
|
200
|
+
<SectionLabel>Sync</SectionLabel>
|
|
201
|
+
|
|
202
|
+
{/* Status overview */}
|
|
203
|
+
<div className="space-y-2 text-sm">
|
|
204
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
<span className="text-muted-foreground w-24 shrink-0">Provider</span>
|
|
206
|
+
<span className="font-mono text-xs">{status.provider}</span>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="flex items-center gap-2">
|
|
209
|
+
<span className="text-muted-foreground w-24 shrink-0">Remote</span>
|
|
210
|
+
<span className="font-mono text-xs truncate" title={status.remote}>{status.remote}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex items-center gap-2">
|
|
213
|
+
<span className="text-muted-foreground w-24 shrink-0">Branch</span>
|
|
214
|
+
<span className="font-mono text-xs">{status.branch}</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<span className="text-muted-foreground w-24 shrink-0">Last sync</span>
|
|
218
|
+
<span className="text-xs">{timeAgo(status.lastSync)}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex items-center gap-2">
|
|
221
|
+
<span className="text-muted-foreground w-24 shrink-0">Unpushed</span>
|
|
222
|
+
<span className="text-xs">{status.unpushed} commits</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="flex items-center gap-2">
|
|
225
|
+
<span className="text-muted-foreground w-24 shrink-0">Auto-sync</span>
|
|
226
|
+
<span className="text-xs">
|
|
227
|
+
commit: {status.autoCommitInterval}s, pull: {Math.floor((status.autoPullInterval || 300) / 60)}min
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Actions */}
|
|
233
|
+
<div className="flex items-center gap-2 pt-2">
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={handleSyncNow}
|
|
237
|
+
disabled={syncing}
|
|
238
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
239
|
+
>
|
|
240
|
+
<RefreshCw size={12} className={syncing ? 'animate-spin' : ''} />
|
|
241
|
+
Sync Now
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={handleToggle}
|
|
246
|
+
disabled={toggling}
|
|
247
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
|
248
|
+
status.enabled
|
|
249
|
+
? 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
|
|
250
|
+
: 'border-green-500/30 text-green-500 hover:bg-green-500/10'
|
|
251
|
+
}`}
|
|
252
|
+
>
|
|
253
|
+
{status.enabled ? 'Disable Auto-sync' : 'Enable Auto-sync'}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Message */}
|
|
258
|
+
{message && (
|
|
259
|
+
<div className="flex items-center gap-1.5 text-xs">
|
|
260
|
+
{message.type === 'success' ? (
|
|
261
|
+
<><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{message.text}</span></>
|
|
262
|
+
) : (
|
|
263
|
+
<><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{/* Conflicts (Task H — enhanced with links) */}
|
|
269
|
+
{conflicts.length > 0 && (
|
|
270
|
+
<div className="pt-2 border-t border-border">
|
|
271
|
+
<SectionLabel>Conflicts ({conflicts.length})</SectionLabel>
|
|
272
|
+
<div className="space-y-1.5">
|
|
273
|
+
{conflicts.map((c, i) => (
|
|
274
|
+
<div key={i} className="flex items-center gap-2 text-xs group">
|
|
275
|
+
<AlertCircle size={12} className="text-red-500 shrink-0" />
|
|
276
|
+
<a
|
|
277
|
+
href={`/view/${encodeURIComponent(c.file)}`}
|
|
278
|
+
className="font-mono truncate hover:text-foreground hover:underline transition-colors"
|
|
279
|
+
title={`Open ${c.file}`}
|
|
280
|
+
>
|
|
281
|
+
{c.file}
|
|
282
|
+
</a>
|
|
283
|
+
<a
|
|
284
|
+
href={`/view/${encodeURIComponent(c.file + '.sync-conflict')}`}
|
|
285
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0 text-muted-foreground hover:text-foreground"
|
|
286
|
+
title="View remote version (.sync-conflict)"
|
|
287
|
+
>
|
|
288
|
+
<ExternalLink size={11} />
|
|
289
|
+
</a>
|
|
290
|
+
<span className="text-muted-foreground shrink-0 ml-auto">{timeAgo(c.time)}</span>
|
|
291
|
+
</div>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
295
|
+
Click a file to view your version. Hover and click <ExternalLink size={10} className="inline" /> to see the remote version.
|
|
296
|
+
</p>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{/* Error */}
|
|
301
|
+
{status.lastError && (
|
|
302
|
+
<div className="pt-2 border-t border-border">
|
|
303
|
+
<div className="flex items-start gap-2 text-xs text-destructive">
|
|
304
|
+
<AlertCircle size={12} className="shrink-0 mt-0.5" />
|
|
305
|
+
<span>{status.lastError}</span>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -24,7 +24,7 @@ export interface SettingsData {
|
|
|
24
24
|
envValues?: Record<string, string>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'plugins' | 'shortcuts';
|
|
27
|
+
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'plugins' | 'shortcuts' | 'sync';
|
|
28
28
|
|
|
29
29
|
export const CONTENT_WIDTHS = [
|
|
30
30
|
{ value: '680px', label: 'Narrow (680px)' },
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getMindRoot } from '@/lib/fs';
|
|
4
|
+
|
|
5
|
+
const LOG_FILE = '.agent-log.json';
|
|
6
|
+
const MAX_SIZE = 500 * 1024; // 500KB
|
|
7
|
+
|
|
8
|
+
interface AgentOpEntry {
|
|
9
|
+
ts: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
params: Record<string, unknown>;
|
|
12
|
+
result: 'ok' | 'error';
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Append an agent operation entry to .agent-log.json (JSON Lines format).
|
|
18
|
+
* Each line is a self-contained JSON object — easy to parse, grep, and tail.
|
|
19
|
+
* Auto-truncates when file exceeds MAX_SIZE.
|
|
20
|
+
*/
|
|
21
|
+
export function logAgentOp(entry: AgentOpEntry): void {
|
|
22
|
+
try {
|
|
23
|
+
const root = getMindRoot();
|
|
24
|
+
const logPath = path.join(root, LOG_FILE);
|
|
25
|
+
|
|
26
|
+
const line = JSON.stringify(entry) + '\n';
|
|
27
|
+
|
|
28
|
+
// Check size and truncate if needed
|
|
29
|
+
if (fs.existsSync(logPath)) {
|
|
30
|
+
const stat = fs.statSync(logPath);
|
|
31
|
+
if (stat.size > MAX_SIZE) {
|
|
32
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
33
|
+
const lines = content.trimEnd().split('\n');
|
|
34
|
+
// Keep the newer half
|
|
35
|
+
const kept = lines.slice(Math.floor(lines.length / 2));
|
|
36
|
+
fs.writeFileSync(logPath, kept.join('\n') + '\n');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.appendFileSync(logPath, line);
|
|
41
|
+
} catch {
|
|
42
|
+
// Logging should never break tool execution
|
|
43
|
+
}
|
|
44
|
+
}
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
|
|
6
6
|
} from '@/lib/fs';
|
|
7
7
|
import { assertNotProtected } from '@/lib/core';
|
|
8
|
+
import { logAgentOp } from './log';
|
|
8
9
|
|
|
9
10
|
// Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
|
|
10
11
|
const MAX_FILE_CHARS = 20_000;
|
|
@@ -19,47 +20,67 @@ export function assertWritable(filePath: string): void {
|
|
|
19
20
|
assertNotProtected(filePath, 'modified by AI agent');
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** Helper: wrap a tool execute fn with agent-op logging */
|
|
24
|
+
function logged<P extends Record<string, unknown>>(
|
|
25
|
+
toolName: string,
|
|
26
|
+
fn: (params: P) => Promise<string>,
|
|
27
|
+
): (params: P) => Promise<string> {
|
|
28
|
+
return async (params: P) => {
|
|
29
|
+
const ts = new Date().toISOString();
|
|
30
|
+
try {
|
|
31
|
+
const result = await fn(params);
|
|
32
|
+
const isError = result.startsWith('Error:');
|
|
33
|
+
logAgentOp({ ts, tool: toolName, params, result: isError ? 'error' : 'ok', message: result.slice(0, 200) });
|
|
34
|
+
return result;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
+
logAgentOp({ ts, tool: toolName, params, result: 'error', message: msg.slice(0, 200) });
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
// ─── Knowledge base tools ─────────────────────────────────────────────────────
|
|
23
44
|
|
|
24
45
|
export const knowledgeBaseTools = {
|
|
25
46
|
list_files: tool({
|
|
26
47
|
description: 'List the full file tree of the knowledge base. Use this to browse what files exist.',
|
|
27
48
|
inputSchema: z.object({}),
|
|
28
|
-
execute: async () => {
|
|
49
|
+
execute: logged('list_files', async () => {
|
|
29
50
|
const tree = getFileTree();
|
|
30
51
|
return JSON.stringify(tree, null, 2);
|
|
31
|
-
},
|
|
52
|
+
}),
|
|
32
53
|
}),
|
|
33
54
|
|
|
34
55
|
read_file: tool({
|
|
35
56
|
description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
|
|
36
57
|
inputSchema: z.object({ path: z.string().describe('Relative file path, e.g. "Profile/👤 Identity.md"') }),
|
|
37
|
-
execute: async ({ path }) => {
|
|
58
|
+
execute: logged('read_file', async ({ path }) => {
|
|
38
59
|
try {
|
|
39
60
|
return truncate(getFileContent(path));
|
|
40
61
|
} catch (e: unknown) {
|
|
41
62
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
42
63
|
}
|
|
43
|
-
},
|
|
64
|
+
}),
|
|
44
65
|
}),
|
|
45
66
|
|
|
46
67
|
search: tool({
|
|
47
68
|
description: 'Full-text search across all files in the knowledge base. Returns matching files with context snippets.',
|
|
48
69
|
inputSchema: z.object({ query: z.string().describe('Search query (case-insensitive)') }),
|
|
49
|
-
execute: async ({ query }) => {
|
|
70
|
+
execute: logged('search', async ({ query }) => {
|
|
50
71
|
const results = searchFiles(query);
|
|
51
72
|
if (results.length === 0) return 'No results found.';
|
|
52
73
|
return results.map(r => `- **${r.path}**: ${r.snippet}`).join('\n');
|
|
53
|
-
},
|
|
74
|
+
}),
|
|
54
75
|
}),
|
|
55
76
|
|
|
56
77
|
get_recent: tool({
|
|
57
78
|
description: 'Get the most recently modified files in the knowledge base.',
|
|
58
79
|
inputSchema: z.object({ limit: z.number().min(1).max(50).default(10).describe('Number of files to return') }),
|
|
59
|
-
execute: async ({ limit }) => {
|
|
80
|
+
execute: logged('get_recent', async ({ limit }) => {
|
|
60
81
|
const files = getRecentlyModified(limit);
|
|
61
82
|
return files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n');
|
|
62
|
-
},
|
|
83
|
+
}),
|
|
63
84
|
}),
|
|
64
85
|
|
|
65
86
|
write_file: tool({
|
|
@@ -68,7 +89,7 @@ export const knowledgeBaseTools = {
|
|
|
68
89
|
path: z.string().describe('Relative file path'),
|
|
69
90
|
content: z.string().describe('New full content'),
|
|
70
91
|
}),
|
|
71
|
-
execute: async ({ path, content }) => {
|
|
92
|
+
execute: logged('write_file', async ({ path, content }) => {
|
|
72
93
|
try {
|
|
73
94
|
assertWritable(path);
|
|
74
95
|
saveFileContent(path, content);
|
|
@@ -76,7 +97,7 @@ export const knowledgeBaseTools = {
|
|
|
76
97
|
} catch (e: unknown) {
|
|
77
98
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
78
99
|
}
|
|
79
|
-
},
|
|
100
|
+
}),
|
|
80
101
|
}),
|
|
81
102
|
|
|
82
103
|
create_file: tool({
|
|
@@ -85,7 +106,7 @@ export const knowledgeBaseTools = {
|
|
|
85
106
|
path: z.string().describe('Relative file path (must end in .md or .csv)'),
|
|
86
107
|
content: z.string().default('').describe('Initial file content'),
|
|
87
108
|
}),
|
|
88
|
-
execute: async ({ path, content }) => {
|
|
109
|
+
execute: logged('create_file', async ({ path, content }) => {
|
|
89
110
|
try {
|
|
90
111
|
assertWritable(path);
|
|
91
112
|
createFile(path, content);
|
|
@@ -93,7 +114,7 @@ export const knowledgeBaseTools = {
|
|
|
93
114
|
} catch (e: unknown) {
|
|
94
115
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
95
116
|
}
|
|
96
|
-
},
|
|
117
|
+
}),
|
|
97
118
|
}),
|
|
98
119
|
|
|
99
120
|
append_to_file: tool({
|
|
@@ -102,7 +123,7 @@ export const knowledgeBaseTools = {
|
|
|
102
123
|
path: z.string().describe('Relative file path'),
|
|
103
124
|
content: z.string().describe('Content to append'),
|
|
104
125
|
}),
|
|
105
|
-
execute: async ({ path, content }) => {
|
|
126
|
+
execute: logged('append_to_file', async ({ path, content }) => {
|
|
106
127
|
try {
|
|
107
128
|
assertWritable(path);
|
|
108
129
|
appendToFile(path, content);
|
|
@@ -110,7 +131,7 @@ export const knowledgeBaseTools = {
|
|
|
110
131
|
} catch (e: unknown) {
|
|
111
132
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
112
133
|
}
|
|
113
|
-
},
|
|
134
|
+
}),
|
|
114
135
|
}),
|
|
115
136
|
|
|
116
137
|
insert_after_heading: tool({
|
|
@@ -120,7 +141,7 @@ export const knowledgeBaseTools = {
|
|
|
120
141
|
heading: z.string().describe('Heading text to find (e.g. "## Tasks" or just "Tasks")'),
|
|
121
142
|
content: z.string().describe('Content to insert after the heading'),
|
|
122
143
|
}),
|
|
123
|
-
execute: async ({ path, heading, content }) => {
|
|
144
|
+
execute: logged('insert_after_heading', async ({ path, heading, content }) => {
|
|
124
145
|
try {
|
|
125
146
|
assertWritable(path);
|
|
126
147
|
insertAfterHeading(path, heading, content);
|
|
@@ -128,7 +149,7 @@ export const knowledgeBaseTools = {
|
|
|
128
149
|
} catch (e: unknown) {
|
|
129
150
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
130
151
|
}
|
|
131
|
-
},
|
|
152
|
+
}),
|
|
132
153
|
}),
|
|
133
154
|
|
|
134
155
|
update_section: tool({
|
|
@@ -138,7 +159,7 @@ export const knowledgeBaseTools = {
|
|
|
138
159
|
heading: z.string().describe('Heading text to find (e.g. "## Status")'),
|
|
139
160
|
content: z.string().describe('New content for the section'),
|
|
140
161
|
}),
|
|
141
|
-
execute: async ({ path, heading, content }) => {
|
|
162
|
+
execute: logged('update_section', async ({ path, heading, content }) => {
|
|
142
163
|
try {
|
|
143
164
|
assertWritable(path);
|
|
144
165
|
updateSection(path, heading, content);
|
|
@@ -146,6 +167,6 @@ export const knowledgeBaseTools = {
|
|
|
146
167
|
} catch (e: unknown) {
|
|
147
168
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
148
169
|
}
|
|
149
|
-
},
|
|
170
|
+
}),
|
|
150
171
|
}),
|
|
151
172
|
};
|
package/app/lib/i18n.ts
CHANGED
|
@@ -36,6 +36,22 @@ export const messages = {
|
|
|
36
36
|
settingsTitle: 'Settings (⌘,)',
|
|
37
37
|
collapseTitle: 'Collapse sidebar',
|
|
38
38
|
expandTitle: 'Expand sidebar',
|
|
39
|
+
sync: {
|
|
40
|
+
synced: 'Synced',
|
|
41
|
+
unpushed: 'awaiting push',
|
|
42
|
+
unpushedHint: 'commit(s) not yet pushed to remote — will sync automatically',
|
|
43
|
+
conflicts: 'conflicts',
|
|
44
|
+
conflictsHint: 'file(s) have merge conflicts — open Settings > Sync to resolve',
|
|
45
|
+
syncError: 'Sync error',
|
|
46
|
+
syncOff: 'Sync off',
|
|
47
|
+
syncing: 'Syncing...',
|
|
48
|
+
syncNow: 'Sync now',
|
|
49
|
+
syncDone: 'Sync complete',
|
|
50
|
+
syncFailed: 'Sync failed',
|
|
51
|
+
syncRestored: 'Sync restored',
|
|
52
|
+
enableSync: 'Enable sync',
|
|
53
|
+
enableHint: 'Set up cross-device sync',
|
|
54
|
+
},
|
|
39
55
|
},
|
|
40
56
|
search: {
|
|
41
57
|
placeholder: 'Search files...',
|
|
@@ -83,7 +99,7 @@ export const messages = {
|
|
|
83
99
|
},
|
|
84
100
|
settings: {
|
|
85
101
|
title: 'Settings',
|
|
86
|
-
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', plugins: 'Plugins', shortcuts: 'Shortcuts' },
|
|
102
|
+
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', sync: 'Sync', plugins: 'Plugins', shortcuts: 'Shortcuts' },
|
|
87
103
|
ai: {
|
|
88
104
|
provider: 'Provider',
|
|
89
105
|
model: 'Model',
|
|
@@ -125,6 +141,18 @@ export const messages = {
|
|
|
125
141
|
authTokenResetConfirm: 'Regenerate token? All existing MCP clients will need to update their config.',
|
|
126
142
|
authTokenMcpPort: 'MCP port',
|
|
127
143
|
},
|
|
144
|
+
sync: {
|
|
145
|
+
emptyTitle: 'Cross-device Sync',
|
|
146
|
+
emptyDesc: 'Automatically sync your knowledge base across devices via Git.',
|
|
147
|
+
emptyStepsTitle: 'Setup',
|
|
148
|
+
emptyStep1: 'Create a private Git repo (GitHub, GitLab, etc.) or use an existing one.',
|
|
149
|
+
emptyStep2: 'Run this command in your terminal:',
|
|
150
|
+
emptyStep3: 'Follow the prompts to connect your repo. Sync starts automatically.',
|
|
151
|
+
featureAutoCommit: 'Auto-commit on save',
|
|
152
|
+
featureAutoPull: 'Auto-pull from remote',
|
|
153
|
+
featureConflict: 'Conflict detection',
|
|
154
|
+
featureMultiDevice: 'Works across devices',
|
|
155
|
+
},
|
|
128
156
|
plugins: {
|
|
129
157
|
title: 'Installed Renderers',
|
|
130
158
|
builtinBadge: 'built-in',
|
|
@@ -138,6 +166,17 @@ export const messages = {
|
|
|
138
166
|
saved: 'Saved',
|
|
139
167
|
saveFailed: 'Save failed',
|
|
140
168
|
},
|
|
169
|
+
onboarding: {
|
|
170
|
+
subtitle: 'Your knowledge base is empty. Pick a starter template to get going.',
|
|
171
|
+
templates: {
|
|
172
|
+
en: { title: 'English', desc: 'Pre-built structure with Profile, Notes, Projects, and more.' },
|
|
173
|
+
zh: { title: '中文', desc: '预置画像、笔记、项目等中文目录结构。' },
|
|
174
|
+
empty: { title: 'Empty', desc: 'Just the essentials — README, CONFIG, and INSTRUCTION.' },
|
|
175
|
+
},
|
|
176
|
+
importHint: 'Already have notes? Set MIND_ROOT to your existing directory in Settings.',
|
|
177
|
+
syncHint: 'Want cross-device sync? Run',
|
|
178
|
+
syncHintSuffix: 'in the terminal after setup.',
|
|
179
|
+
},
|
|
141
180
|
shortcuts: [
|
|
142
181
|
{ keys: ['⌘', 'K'], description: 'Search' },
|
|
143
182
|
{ keys: ['⌘', '/'], description: 'Ask AI' },
|
|
@@ -183,6 +222,22 @@ export const messages = {
|
|
|
183
222
|
settingsTitle: '设置 (⌘,)',
|
|
184
223
|
collapseTitle: '收起侧栏',
|
|
185
224
|
expandTitle: '展开侧栏',
|
|
225
|
+
sync: {
|
|
226
|
+
synced: '已同步',
|
|
227
|
+
unpushed: '待推送',
|
|
228
|
+
unpushedHint: '个提交尚未推送到远程 — 将自动同步',
|
|
229
|
+
conflicts: '冲突',
|
|
230
|
+
conflictsHint: '个文件存在合并冲突 — 前往 设置 > 同步 解决',
|
|
231
|
+
syncError: '同步出错',
|
|
232
|
+
syncOff: '同步关闭',
|
|
233
|
+
syncing: '同步中...',
|
|
234
|
+
syncNow: '立即同步',
|
|
235
|
+
syncDone: '同步完成',
|
|
236
|
+
syncFailed: '同步失败',
|
|
237
|
+
syncRestored: '同步已恢复',
|
|
238
|
+
enableSync: '启用同步',
|
|
239
|
+
enableHint: '设置跨设备同步',
|
|
240
|
+
},
|
|
186
241
|
},
|
|
187
242
|
search: {
|
|
188
243
|
placeholder: '搜索文件...',
|
|
@@ -230,7 +285,7 @@ export const messages = {
|
|
|
230
285
|
},
|
|
231
286
|
settings: {
|
|
232
287
|
title: '设置',
|
|
233
|
-
tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', plugins: '插件', shortcuts: '快捷键' },
|
|
288
|
+
tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', sync: '同步', plugins: '插件', shortcuts: '快捷键' },
|
|
234
289
|
ai: {
|
|
235
290
|
provider: '服务商',
|
|
236
291
|
model: '模型',
|
|
@@ -272,6 +327,18 @@ export const messages = {
|
|
|
272
327
|
authTokenResetConfirm: '重新生成令牌?所有 MCP 客户端配置都需要更新。',
|
|
273
328
|
authTokenMcpPort: 'MCP 端口',
|
|
274
329
|
},
|
|
330
|
+
sync: {
|
|
331
|
+
emptyTitle: '跨设备同步',
|
|
332
|
+
emptyDesc: '通过 Git 自动同步知识库到所有设备。',
|
|
333
|
+
emptyStepsTitle: '配置步骤',
|
|
334
|
+
emptyStep1: '创建一个私有 Git 仓库(GitHub、GitLab 等),或使用现有仓库。',
|
|
335
|
+
emptyStep2: '在终端中运行以下命令:',
|
|
336
|
+
emptyStep3: '按照提示连接仓库,同步将自动开始。',
|
|
337
|
+
featureAutoCommit: '保存时自动提交',
|
|
338
|
+
featureAutoPull: '自动拉取远程更新',
|
|
339
|
+
featureConflict: '冲突检测',
|
|
340
|
+
featureMultiDevice: '多设备同步',
|
|
341
|
+
},
|
|
275
342
|
plugins: {
|
|
276
343
|
title: '已安装渲染器',
|
|
277
344
|
builtinBadge: '内置',
|
|
@@ -285,6 +352,17 @@ export const messages = {
|
|
|
285
352
|
saved: '已保存',
|
|
286
353
|
saveFailed: '保存失败',
|
|
287
354
|
},
|
|
355
|
+
onboarding: {
|
|
356
|
+
subtitle: '知识库为空,选择一个模板快速开始。',
|
|
357
|
+
templates: {
|
|
358
|
+
en: { title: 'English', desc: '预置 Profile、Notes、Projects 等英文目录结构。' },
|
|
359
|
+
zh: { title: '中文', desc: '预置画像、笔记、项目等中文目录结构。' },
|
|
360
|
+
empty: { title: '空白', desc: '仅包含 README、CONFIG 和 INSTRUCTION 基础文件。' },
|
|
361
|
+
},
|
|
362
|
+
importHint: '已有笔记?在设置中将 MIND_ROOT 指向已有目录即可。',
|
|
363
|
+
syncHint: '需要跨设备同步?完成初始化后在终端运行',
|
|
364
|
+
syncHintSuffix: '即可。',
|
|
365
|
+
},
|
|
288
366
|
shortcuts: [
|
|
289
367
|
{ keys: ['⌘', 'K'], description: '搜索' },
|
|
290
368
|
{ keys: ['⌘', '/'], description: '问 AI' },
|