@geminilight/mindos 0.5.11 → 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.
- package/README.md +9 -9
- package/README_zh.md +9 -9
- package/app/README.md +2 -2
- package/app/app/api/ask/route.ts +126 -3
- package/app/app/api/mcp/install/route.ts +1 -1
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/settings/route.ts +1 -1
- package/app/app/api/setup/route.ts +7 -7
- package/app/app/api/sync/route.ts +21 -16
- package/app/components/AskModal.tsx +28 -21
- package/app/components/ask/MessageList.tsx +62 -3
- package/app/components/ask/ToolCallBlock.tsx +89 -0
- package/app/components/setup/StepReview.tsx +31 -25
- package/app/components/setup/index.tsx +6 -3
- package/app/lib/agent/prompt.ts +32 -0
- package/app/lib/agent/stream-consumer.ts +178 -0
- package/app/lib/agent/tools.ts +122 -0
- package/app/lib/types.ts +18 -0
- package/app/next-env.d.ts +1 -1
- package/app/package.json +2 -2
- package/bin/cli.js +41 -21
- package/bin/lib/gateway.js +24 -3
- package/bin/lib/mcp-install.js +2 -2
- package/bin/lib/mcp-spawn.js +3 -3
- package/bin/lib/stop.js +1 -1
- package/mcp/README.md +5 -5
- package/mcp/src/index.ts +2 -2
- package/package.json +1 -1
- package/scripts/setup.js +12 -12
- package/scripts/upgrade-prompt.md +6 -6
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,24 @@ import {
|
|
|
7
7
|
import type { SetupState, SetupMessages, AgentInstallStatus } from './types';
|
|
8
8
|
|
|
9
9
|
// ─── Restart Block ────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
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
|
-
<
|
|
44
|
-
style={{ background: 'color-mix(in srgb, 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
|
-
</
|
|
63
|
+
</span>
|
|
47
64
|
);
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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' && <
|
|
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
|
|
443
|
-
|
|
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} →
|
|
448
451
|
</a>
|
|
449
|
-
)
|
|
452
|
+
)
|
|
450
453
|
) : (
|
|
451
454
|
<button
|
|
452
455
|
onClick={handleComplete}
|
package/app/lib/agent/prompt.ts
CHANGED
|
@@ -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.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Message, MessagePart, ToolCallPart, TextPart } from '@/lib/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a UIMessageStream SSE response into structured Message parts.
|
|
5
|
+
* The stream format is Server-Sent Events where each data line is a JSON-encoded UIMessageChunk.
|
|
6
|
+
*/
|
|
7
|
+
export async function consumeUIMessageStream(
|
|
8
|
+
body: ReadableStream<Uint8Array>,
|
|
9
|
+
onUpdate: (message: Message) => void,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<Message> {
|
|
12
|
+
const reader = body.getReader();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
let buffer = '';
|
|
15
|
+
|
|
16
|
+
// Mutable working copies — we deep-clone when emitting to React
|
|
17
|
+
const parts: MessagePart[] = [];
|
|
18
|
+
const toolCalls = new Map<string, ToolCallPart>();
|
|
19
|
+
let currentTextId: string | null = null;
|
|
20
|
+
|
|
21
|
+
/** Deep-clone parts into an immutable Message snapshot for React state */
|
|
22
|
+
function buildMessage(): Message {
|
|
23
|
+
const clonedParts: MessagePart[] = parts.map(p => {
|
|
24
|
+
if (p.type === 'text') return { type: 'text' as const, text: p.text };
|
|
25
|
+
return { ...p }; // ToolCallPart — shallow copy is safe (all primitive fields + `input` is replaced, not mutated)
|
|
26
|
+
});
|
|
27
|
+
const textContent = clonedParts
|
|
28
|
+
.filter((p): p is TextPart => p.type === 'text')
|
|
29
|
+
.map(p => p.text)
|
|
30
|
+
.join('');
|
|
31
|
+
return {
|
|
32
|
+
role: 'assistant',
|
|
33
|
+
content: textContent,
|
|
34
|
+
parts: clonedParts,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findOrCreateTextPart(id: string): TextPart {
|
|
39
|
+
if (currentTextId === id) {
|
|
40
|
+
const last = parts[parts.length - 1];
|
|
41
|
+
if (last && last.type === 'text') return last;
|
|
42
|
+
}
|
|
43
|
+
const part: TextPart = { type: 'text', text: '' };
|
|
44
|
+
parts.push(part);
|
|
45
|
+
currentTextId = id;
|
|
46
|
+
return part;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findOrCreateToolCall(toolCallId: string, toolName?: string): ToolCallPart {
|
|
50
|
+
let tc = toolCalls.get(toolCallId);
|
|
51
|
+
if (!tc) {
|
|
52
|
+
tc = {
|
|
53
|
+
type: 'tool-call',
|
|
54
|
+
toolCallId,
|
|
55
|
+
toolName: toolName ?? 'unknown',
|
|
56
|
+
input: undefined,
|
|
57
|
+
state: 'pending',
|
|
58
|
+
};
|
|
59
|
+
toolCalls.set(toolCallId, tc);
|
|
60
|
+
parts.push(tc);
|
|
61
|
+
currentTextId = null; // break text continuity
|
|
62
|
+
}
|
|
63
|
+
return tc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
while (true) {
|
|
68
|
+
if (signal?.aborted) break;
|
|
69
|
+
const { done, value } = await reader.read();
|
|
70
|
+
if (done) break;
|
|
71
|
+
|
|
72
|
+
buffer += decoder.decode(value, { stream: true });
|
|
73
|
+
|
|
74
|
+
// Process complete SSE lines
|
|
75
|
+
const lines = buffer.split('\n');
|
|
76
|
+
buffer = lines.pop() ?? ''; // keep incomplete last line
|
|
77
|
+
|
|
78
|
+
let changed = false;
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
|
|
83
|
+
// SSE format: the ai SDK v6 UIMessageStream uses "d:{json}\n"
|
|
84
|
+
// Also handle standard "data:{json}" for robustness
|
|
85
|
+
let jsonStr: string | null = null;
|
|
86
|
+
if (trimmed.startsWith('d:')) {
|
|
87
|
+
jsonStr = trimmed.slice(2);
|
|
88
|
+
} else if (trimmed.startsWith('data:')) {
|
|
89
|
+
jsonStr = trimmed.slice(5).trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!jsonStr) continue;
|
|
93
|
+
|
|
94
|
+
let chunk: Record<string, unknown>;
|
|
95
|
+
try {
|
|
96
|
+
chunk = JSON.parse(jsonStr);
|
|
97
|
+
} catch {
|
|
98
|
+
continue; // skip malformed lines
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const type = chunk.type as string;
|
|
102
|
+
|
|
103
|
+
switch (type) {
|
|
104
|
+
case 'text-start': {
|
|
105
|
+
findOrCreateTextPart(chunk.id as string);
|
|
106
|
+
changed = true;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'text-delta': {
|
|
110
|
+
const part = findOrCreateTextPart(chunk.id as string);
|
|
111
|
+
part.text += chunk.delta as string;
|
|
112
|
+
changed = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'text-end': {
|
|
116
|
+
// Text part is complete — no state change needed
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'tool-input-start': {
|
|
120
|
+
const tc = findOrCreateToolCall(chunk.toolCallId as string, chunk.toolName as string);
|
|
121
|
+
tc.state = 'running';
|
|
122
|
+
changed = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'tool-input-delta': {
|
|
126
|
+
// Streaming input — we wait for input-available for the complete input
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'tool-input-available': {
|
|
130
|
+
const tc = findOrCreateToolCall(chunk.toolCallId as string, chunk.toolName as string);
|
|
131
|
+
tc.input = chunk.input;
|
|
132
|
+
tc.state = 'running';
|
|
133
|
+
changed = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'tool-output-available': {
|
|
137
|
+
const tc = toolCalls.get(chunk.toolCallId as string);
|
|
138
|
+
if (tc) {
|
|
139
|
+
tc.output = typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output);
|
|
140
|
+
tc.state = 'done';
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'tool-output-error':
|
|
146
|
+
case 'tool-input-error': {
|
|
147
|
+
const tc = toolCalls.get(chunk.toolCallId as string);
|
|
148
|
+
if (tc) {
|
|
149
|
+
tc.output = (chunk.errorText as string) ?? 'Error';
|
|
150
|
+
tc.state = 'error';
|
|
151
|
+
changed = true;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'error': {
|
|
156
|
+
const errorText = (chunk.errorText as string) ?? 'Unknown error';
|
|
157
|
+
parts.push({ type: 'text', text: `\n\n**Error:** ${errorText}` });
|
|
158
|
+
currentTextId = null;
|
|
159
|
+
changed = true;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
// step-start, reasoning-*, metadata, finish — ignored for now
|
|
163
|
+
default:
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Emit once per reader.read() batch, not per SSE line
|
|
169
|
+
if (changed) {
|
|
170
|
+
onUpdate(buildMessage());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
reader.releaseLock();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return buildMessage();
|
|
178
|
+
}
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import {
|
|
4
4
|
searchFiles, getFileContent, getFileTree, getRecentlyModified,
|
|
5
5
|
saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
|
|
6
|
+
deleteFile, renameFile, moveFile, findBacklinks, gitLog, gitShowFile, appendCsvRow,
|
|
6
7
|
} from '@/lib/fs';
|
|
7
8
|
import { assertNotProtected } from '@/lib/core';
|
|
8
9
|
import { logAgentOp } from './log';
|
|
@@ -169,4 +170,125 @@ export const knowledgeBaseTools = {
|
|
|
169
170
|
}
|
|
170
171
|
}),
|
|
171
172
|
}),
|
|
173
|
+
|
|
174
|
+
// ─── New tools (Phase 1a) ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
delete_file: tool({
|
|
177
|
+
description: 'Permanently delete a file from the knowledge base. This is destructive and cannot be undone.',
|
|
178
|
+
inputSchema: z.object({
|
|
179
|
+
path: z.string().describe('Relative file path to delete'),
|
|
180
|
+
}),
|
|
181
|
+
execute: logged('delete_file', async ({ path }) => {
|
|
182
|
+
try {
|
|
183
|
+
assertWritable(path);
|
|
184
|
+
deleteFile(path);
|
|
185
|
+
return `File deleted: ${path}`;
|
|
186
|
+
} catch (e: unknown) {
|
|
187
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
rename_file: tool({
|
|
193
|
+
description: 'Rename a file within its current directory. Only the filename changes, not the directory.',
|
|
194
|
+
inputSchema: z.object({
|
|
195
|
+
path: z.string().describe('Current relative file path'),
|
|
196
|
+
new_name: z.string().describe('New filename (no path separators, e.g. "new-name.md")'),
|
|
197
|
+
}),
|
|
198
|
+
execute: logged('rename_file', async ({ path, new_name }) => {
|
|
199
|
+
try {
|
|
200
|
+
assertWritable(path);
|
|
201
|
+
const newPath = renameFile(path, new_name);
|
|
202
|
+
return `File renamed: ${path} → ${newPath}`;
|
|
203
|
+
} catch (e: unknown) {
|
|
204
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
move_file: tool({
|
|
210
|
+
description: 'Move a file to a new location. Also returns any files that had backlinks affected by the move.',
|
|
211
|
+
inputSchema: z.object({
|
|
212
|
+
from_path: z.string().describe('Current relative file path'),
|
|
213
|
+
to_path: z.string().describe('New relative file path'),
|
|
214
|
+
}),
|
|
215
|
+
execute: logged('move_file', async ({ from_path, to_path }) => {
|
|
216
|
+
try {
|
|
217
|
+
assertWritable(from_path);
|
|
218
|
+
const result = moveFile(from_path, to_path);
|
|
219
|
+
const affected = result.affectedFiles.length > 0
|
|
220
|
+
? `\nAffected backlinks in: ${result.affectedFiles.join(', ')}`
|
|
221
|
+
: '';
|
|
222
|
+
return `File moved: ${from_path} → ${result.newPath}${affected}`;
|
|
223
|
+
} catch (e: unknown) {
|
|
224
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
225
|
+
}
|
|
226
|
+
}),
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
get_backlinks: tool({
|
|
230
|
+
description: 'Find all files that reference a given file path. Useful for understanding connections between notes.',
|
|
231
|
+
inputSchema: z.object({
|
|
232
|
+
path: z.string().describe('Relative file path to find backlinks for'),
|
|
233
|
+
}),
|
|
234
|
+
execute: logged('get_backlinks', async ({ path }) => {
|
|
235
|
+
try {
|
|
236
|
+
const backlinks = findBacklinks(path);
|
|
237
|
+
if (backlinks.length === 0) return `No backlinks found for: ${path}`;
|
|
238
|
+
return backlinks.map(b => `- **${b.source}** (L${b.line}): ${b.context}`).join('\n');
|
|
239
|
+
} catch (e: unknown) {
|
|
240
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
get_history: tool({
|
|
246
|
+
description: 'Get git commit history for a file. Shows recent commits that modified this file.',
|
|
247
|
+
inputSchema: z.object({
|
|
248
|
+
path: z.string().describe('Relative file path'),
|
|
249
|
+
limit: z.number().min(1).max(50).default(10).describe('Number of commits to return'),
|
|
250
|
+
}),
|
|
251
|
+
execute: logged('get_history', async ({ path, limit }) => {
|
|
252
|
+
try {
|
|
253
|
+
const commits = gitLog(path, limit);
|
|
254
|
+
if (commits.length === 0) return `No git history found for: ${path}`;
|
|
255
|
+
return commits.map(c => `- \`${c.hash.slice(0, 7)}\` ${c.date} — ${c.message} (${c.author})`).join('\n');
|
|
256
|
+
} catch (e: unknown) {
|
|
257
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
258
|
+
}
|
|
259
|
+
}),
|
|
260
|
+
}),
|
|
261
|
+
|
|
262
|
+
get_file_at_version: tool({
|
|
263
|
+
description: 'Read the content of a file at a specific git commit. Use get_history first to find commit hashes.',
|
|
264
|
+
inputSchema: z.object({
|
|
265
|
+
path: z.string().describe('Relative file path'),
|
|
266
|
+
commit: z.string().describe('Git commit hash (full or abbreviated)'),
|
|
267
|
+
}),
|
|
268
|
+
execute: logged('get_file_at_version', async ({ path, commit }) => {
|
|
269
|
+
try {
|
|
270
|
+
const content = gitShowFile(path, commit);
|
|
271
|
+
return truncate(content);
|
|
272
|
+
} catch (e: unknown) {
|
|
273
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
}),
|
|
277
|
+
|
|
278
|
+
append_csv: tool({
|
|
279
|
+
description: 'Append a row to a CSV file. Values are automatically escaped per RFC 4180.',
|
|
280
|
+
inputSchema: z.object({
|
|
281
|
+
path: z.string().describe('Relative path to .csv file'),
|
|
282
|
+
row: z.array(z.string()).describe('Array of cell values for the new row'),
|
|
283
|
+
}),
|
|
284
|
+
execute: logged('append_csv', async ({ path, row }) => {
|
|
285
|
+
try {
|
|
286
|
+
assertWritable(path);
|
|
287
|
+
const result = appendCsvRow(path, row);
|
|
288
|
+
return `Row appended to ${path} (now ${result.newRowCount} rows)`;
|
|
289
|
+
} catch (e: unknown) {
|
|
290
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
291
|
+
}
|
|
292
|
+
}),
|
|
293
|
+
}),
|
|
172
294
|
};
|
package/app/lib/types.ts
CHANGED
|
@@ -13,9 +13,27 @@ export interface BacklinkItem {
|
|
|
13
13
|
snippets: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface ToolCallPart {
|
|
17
|
+
type: 'tool-call';
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
toolName: string;
|
|
20
|
+
input: unknown;
|
|
21
|
+
output?: string;
|
|
22
|
+
state: 'pending' | 'running' | 'done' | 'error';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TextPart {
|
|
26
|
+
type: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type MessagePart = TextPart | ToolCallPart;
|
|
31
|
+
|
|
16
32
|
export interface Message {
|
|
17
33
|
role: 'user' | 'assistant';
|
|
18
34
|
content: string;
|
|
35
|
+
/** Structured parts for assistant messages (tool calls + text segments) */
|
|
36
|
+
parts?: MessagePart[];
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
export interface LocalAttachment {
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/app/package.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "next dev -p ${MINDOS_WEB_PORT:-
|
|
6
|
+
"dev": "next dev -p ${MINDOS_WEB_PORT:-3456}",
|
|
7
7
|
"prebuild": "node ../scripts/gen-renderer-index.js",
|
|
8
8
|
"build": "next build",
|
|
9
|
-
"start": "next start -p ${MINDOS_WEB_PORT:-
|
|
9
|
+
"start": "next start -p ${MINDOS_WEB_PORT:-3456}",
|
|
10
10
|
"lint": "eslint",
|
|
11
11
|
"test": "vitest run"
|
|
12
12
|
},
|