@geminilight/mindos 0.5.19 → 0.5.21
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/app/app/api/ask/route.ts +308 -172
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/SettingsModal.tsx +52 -58
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/AiTab.tsx +4 -25
- package/app/components/settings/AppearanceTab.tsx +31 -13
- package/app/components/settings/KnowledgeTab.tsx +13 -28
- package/app/components/settings/McpAgentInstall.tsx +227 -0
- package/app/components/settings/McpServerStatus.tsx +172 -0
- package/app/components/settings/McpSkillsSection.tsx +583 -0
- package/app/components/settings/McpTab.tsx +16 -728
- package/app/components/settings/PluginsTab.tsx +4 -27
- package/app/components/settings/Primitives.tsx +69 -0
- package/app/components/settings/ShortcutsTab.tsx +2 -4
- package/app/components/settings/SyncTab.tsx +8 -24
- package/app/components/settings/types.ts +116 -2
- package/app/lib/agent/context.ts +151 -87
- package/app/lib/agent/index.ts +4 -3
- package/app/lib/agent/model.ts +76 -10
- package/app/lib/agent/stream-consumer.ts +73 -77
- package/app/lib/agent/to-agent-messages.ts +106 -0
- package/app/lib/agent/tools.ts +260 -266
- package/app/lib/i18n-en.ts +480 -0
- package/app/lib/i18n-zh.ts +505 -0
- package/app/lib/i18n.ts +4 -947
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/app/package-lock.json +3258 -3093
- package/app/package.json +6 -3
- package/bin/cli.js +140 -5
- package/package.json +4 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
package/app/lib/agent/tools.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
2
|
+
import type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core';
|
|
3
3
|
import {
|
|
4
4
|
searchFiles, getFileContent, getFileTree, getRecentlyModified,
|
|
5
5
|
saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
|
|
6
6
|
deleteFile, renameFile, moveFile, findBacklinks, gitLog, gitShowFile, appendCsvRow,
|
|
7
7
|
getMindRoot,
|
|
8
8
|
} from '@/lib/fs';
|
|
9
|
-
import { assertNotProtected } from '@/lib/core';
|
|
10
|
-
import { logAgentOp } from './log';
|
|
11
9
|
|
|
12
10
|
// Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
|
|
13
11
|
const MAX_FILE_CHARS = 20_000;
|
|
@@ -17,329 +15,325 @@ export function truncate(content: string): string {
|
|
|
17
15
|
return content.slice(0, MAX_FILE_CHARS) + `\n\n[...truncated — file is ${content.length} chars, showing first ${MAX_FILE_CHARS}]`;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// ─── Helper: format tool error consistently ────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function formatToolError(error: unknown): string {
|
|
21
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Helper: build a text-only AgentToolResult ──────────────────────────────
|
|
25
|
+
|
|
26
|
+
function textResult(text: string): AgentToolResult<Record<string, never>> {
|
|
27
|
+
return { content: [{ type: 'text', text }], details: {} as Record<string, never> };
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* "Cannot read properties of undefined" on every subsequent request.
|
|
31
|
-
*/
|
|
32
|
-
function logged<P extends Record<string, unknown>>(
|
|
33
|
-
toolName: string,
|
|
34
|
-
fn: (params: P) => Promise<string>,
|
|
35
|
-
): (params: P) => Promise<string> {
|
|
36
|
-
return async (params: P) => {
|
|
37
|
-
const ts = new Date().toISOString();
|
|
30
|
+
/** Safe execute wrapper — catches all errors, returns error text (never throws) */
|
|
31
|
+
function safeExecute<T>(
|
|
32
|
+
fn: (toolCallId: string, params: T, signal?: AbortSignal) => Promise<AgentToolResult<any>>,
|
|
33
|
+
): (toolCallId: string, params: T, signal?: AbortSignal) => Promise<AgentToolResult<any>> {
|
|
34
|
+
return async (toolCallId, params, signal) => {
|
|
38
35
|
try {
|
|
39
|
-
|
|
40
|
-
const isError = result.startsWith('Error:');
|
|
41
|
-
try { logAgentOp({ ts, tool: toolName, params, result: isError ? 'error' : 'ok', message: result.slice(0, 200) }); } catch { /* logging must never kill the stream */ }
|
|
42
|
-
return result;
|
|
36
|
+
return await fn(toolCallId, params, signal);
|
|
43
37
|
} catch (e) {
|
|
44
|
-
|
|
45
|
-
try { logAgentOp({ ts, tool: toolName, params, result: 'error', message: msg.slice(0, 200) }); } catch { /* swallow — logging must never kill the stream */ }
|
|
46
|
-
return `Error: ${msg}`;
|
|
38
|
+
return textResult(`Error: ${formatToolError(e)}`);
|
|
47
39
|
}
|
|
48
40
|
};
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
// ───
|
|
43
|
+
// ─── TypeBox Schemas ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const ListFilesParams = Type.Object({
|
|
46
|
+
path: Type.Optional(Type.String({ description: 'Optional subdirectory to list (e.g. "Projects/Products"). Omit to list everything.' })),
|
|
47
|
+
depth: Type.Optional(Type.Number({ description: 'Max tree depth to expand (default 3). Directories deeper than this show item count only.', minimum: 1, maximum: 10 })),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const PathParam = Type.Object({
|
|
51
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const QueryParam = Type.Object({
|
|
55
|
+
query: Type.String({ description: 'Search query (case-insensitive)' }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const LimitParam = Type.Object({
|
|
59
|
+
limit: Type.Optional(Type.Number({ description: 'Number of files to return (default 10)', minimum: 1, maximum: 50 })),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const WriteFileParams = Type.Object({
|
|
63
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
64
|
+
content: Type.String({ description: 'New full content' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const CreateFileParams = Type.Object({
|
|
68
|
+
path: Type.String({ description: 'Relative file path (must end in .md or .csv)' }),
|
|
69
|
+
content: Type.Optional(Type.String({ description: 'Initial file content' })),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const AppendParams = Type.Object({
|
|
73
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
74
|
+
content: Type.String({ description: 'Content to append' }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const InsertHeadingParams = Type.Object({
|
|
78
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
79
|
+
heading: Type.String({ description: 'Heading text to find (e.g. "## Tasks" or just "Tasks")' }),
|
|
80
|
+
content: Type.String({ description: 'Content to insert after the heading' }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const UpdateSectionParams = Type.Object({
|
|
84
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
85
|
+
heading: Type.String({ description: 'Heading text to find (e.g. "## Status")' }),
|
|
86
|
+
content: Type.String({ description: 'New content for the section' }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const RenameParams = Type.Object({
|
|
90
|
+
path: Type.String({ description: 'Current relative file path' }),
|
|
91
|
+
new_name: Type.String({ description: 'New filename (no path separators, e.g. "new-name.md")' }),
|
|
92
|
+
});
|
|
52
93
|
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
const MoveParams = Type.Object({
|
|
95
|
+
from_path: Type.String({ description: 'Current relative file path' }),
|
|
96
|
+
to_path: Type.String({ description: 'New relative file path' }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const HistoryParams = Type.Object({
|
|
100
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
101
|
+
limit: Type.Optional(Type.Number({ description: 'Number of commits to return (default 10)', minimum: 1, maximum: 50 })),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const FileAtVersionParams = Type.Object({
|
|
105
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
106
|
+
commit: Type.String({ description: 'Git commit hash (full or abbreviated)' }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const CsvAppendParams = Type.Object({
|
|
110
|
+
path: Type.String({ description: 'Relative path to .csv file' }),
|
|
111
|
+
row: Type.Array(Type.String(), { description: 'Array of cell values for the new row' }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── Tool Definitions (AgentTool interface) ─────────────────────────────────
|
|
115
|
+
|
|
116
|
+
// Write-operation tool names — used by beforeToolCall for write-protection
|
|
117
|
+
export const WRITE_TOOLS = new Set([
|
|
118
|
+
'write_file', 'create_file', 'append_to_file', 'insert_after_heading',
|
|
119
|
+
'update_section', 'delete_file', 'rename_file', 'move_file', 'append_csv',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
123
|
+
{
|
|
124
|
+
name: 'list_files',
|
|
125
|
+
label: 'List Files',
|
|
55
126
|
description: 'List files in the knowledge base as an indented tree. Directories beyond `depth` show "... (N items)". Pass `path` to list only a subdirectory, or `depth` to control how deep to expand (default 3).',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (tree.length === 0 && !subdir) {
|
|
66
|
-
const root = getMindRoot();
|
|
67
|
-
return `(empty — no .md or .csv files found under mind_root: ${root})`;
|
|
68
|
-
}
|
|
127
|
+
parameters: ListFilesParams,
|
|
128
|
+
execute: safeExecute(async (_id, params: Static<typeof ListFilesParams>) => {
|
|
129
|
+
const { path: subdir, depth: maxDepth } = params;
|
|
130
|
+
const tree = getFileTree();
|
|
131
|
+
|
|
132
|
+
if (tree.length === 0 && !subdir) {
|
|
133
|
+
const root = getMindRoot();
|
|
134
|
+
return textResult(`(empty — no .md or .csv files found under mind_root: ${root})`);
|
|
135
|
+
}
|
|
69
136
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
137
|
+
const limit = maxDepth ?? 3;
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
function walk(nodes: Array<{ name: string; type: string; children?: unknown[] }>, depth: number) {
|
|
140
|
+
for (const n of nodes) {
|
|
141
|
+
lines.push(' '.repeat(depth) + (n.type === 'directory' ? `${n.name}/` : n.name));
|
|
142
|
+
if (n.type === 'directory' && Array.isArray(n.children)) {
|
|
143
|
+
if (depth + 1 < limit) {
|
|
144
|
+
walk(n.children as typeof nodes, depth + 1);
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(' '.repeat(depth + 1) + `... (${n.children.length} items)`);
|
|
81
147
|
}
|
|
82
148
|
}
|
|
83
149
|
}
|
|
150
|
+
}
|
|
84
151
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
current = found.children as typeof current;
|
|
152
|
+
if (subdir) {
|
|
153
|
+
const segments = subdir.replace(/\/$/, '').split('/').filter(Boolean);
|
|
154
|
+
let current: Array<{ name: string; type: string; path?: string; children?: unknown[] }> = tree as any;
|
|
155
|
+
for (const seg of segments) {
|
|
156
|
+
const found = current.find(n => n.name === seg && n.type === 'directory');
|
|
157
|
+
if (!found || !Array.isArray(found.children)) {
|
|
158
|
+
return textResult(`Directory not found: ${subdir}`);
|
|
94
159
|
}
|
|
95
|
-
|
|
96
|
-
} else {
|
|
97
|
-
walk(tree as any, 0);
|
|
160
|
+
current = found.children as typeof current;
|
|
98
161
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
162
|
+
walk(current as any, 0);
|
|
163
|
+
} else {
|
|
164
|
+
walk(tree as any, 0);
|
|
103
165
|
}
|
|
166
|
+
|
|
167
|
+
return textResult(lines.length > 0 ? lines.join('\n') : '(empty directory)');
|
|
104
168
|
}),
|
|
105
|
-
}
|
|
169
|
+
},
|
|
106
170
|
|
|
107
|
-
|
|
171
|
+
{
|
|
172
|
+
name: 'read_file',
|
|
173
|
+
label: 'Read File',
|
|
108
174
|
description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
|
|
109
|
-
|
|
110
|
-
execute:
|
|
111
|
-
|
|
112
|
-
return truncate(getFileContent(path));
|
|
113
|
-
} catch (e: unknown) {
|
|
114
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
115
|
-
}
|
|
175
|
+
parameters: PathParam,
|
|
176
|
+
execute: safeExecute(async (_id, params: Static<typeof PathParam>) => {
|
|
177
|
+
return textResult(truncate(getFileContent(params.path)));
|
|
116
178
|
}),
|
|
117
|
-
}
|
|
179
|
+
},
|
|
118
180
|
|
|
119
|
-
|
|
181
|
+
{
|
|
182
|
+
name: 'search',
|
|
183
|
+
label: 'Search',
|
|
120
184
|
description: 'Full-text search across all files in the knowledge base. Returns matching files with context snippets.',
|
|
121
|
-
|
|
122
|
-
execute:
|
|
123
|
-
const results = searchFiles(query);
|
|
124
|
-
if (results.length === 0) return 'No results found.';
|
|
125
|
-
return results.map(r => `- **${r.path}**: ${r.snippet}`).join('\n');
|
|
185
|
+
parameters: QueryParam,
|
|
186
|
+
execute: safeExecute(async (_id, params: Static<typeof QueryParam>) => {
|
|
187
|
+
const results = searchFiles(params.query);
|
|
188
|
+
if (results.length === 0) return textResult('No results found.');
|
|
189
|
+
return textResult(results.map(r => `- **${r.path}**: ${r.snippet}`).join('\n'));
|
|
126
190
|
}),
|
|
127
|
-
}
|
|
191
|
+
},
|
|
128
192
|
|
|
129
|
-
|
|
193
|
+
{
|
|
194
|
+
name: 'get_recent',
|
|
195
|
+
label: 'Recent Files',
|
|
130
196
|
description: 'Get the most recently modified files in the knowledge base.',
|
|
131
|
-
|
|
132
|
-
execute:
|
|
133
|
-
const files = getRecentlyModified(limit ?? 10);
|
|
134
|
-
return files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n');
|
|
197
|
+
parameters: LimitParam,
|
|
198
|
+
execute: safeExecute(async (_id, params: Static<typeof LimitParam>) => {
|
|
199
|
+
const files = getRecentlyModified(params.limit ?? 10);
|
|
200
|
+
return textResult(files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n'));
|
|
135
201
|
}),
|
|
136
|
-
}
|
|
202
|
+
},
|
|
137
203
|
|
|
138
|
-
|
|
204
|
+
{
|
|
205
|
+
name: 'write_file',
|
|
206
|
+
label: 'Write File',
|
|
139
207
|
description: 'Overwrite the entire content of an existing file. Use read_file first to see current content. Prefer update_section or insert_after_heading for partial edits.',
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
execute: logged('write_file', async ({ path, content }) => {
|
|
145
|
-
try {
|
|
146
|
-
assertWritable(path);
|
|
147
|
-
saveFileContent(path, content);
|
|
148
|
-
return `File written: ${path}`;
|
|
149
|
-
} catch (e: unknown) {
|
|
150
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
151
|
-
}
|
|
208
|
+
parameters: WriteFileParams,
|
|
209
|
+
execute: safeExecute(async (_id, params: Static<typeof WriteFileParams>) => {
|
|
210
|
+
saveFileContent(params.path, params.content);
|
|
211
|
+
return textResult(`File written: ${params.path}`);
|
|
152
212
|
}),
|
|
153
|
-
}
|
|
213
|
+
},
|
|
154
214
|
|
|
155
|
-
|
|
215
|
+
{
|
|
216
|
+
name: 'create_file',
|
|
217
|
+
label: 'Create File',
|
|
156
218
|
description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically.',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
219
|
+
parameters: CreateFileParams,
|
|
220
|
+
execute: safeExecute(async (_id, params: Static<typeof CreateFileParams>) => {
|
|
221
|
+
createFile(params.path, params.content ?? '');
|
|
222
|
+
return textResult(`File created: ${params.path}`);
|
|
160
223
|
}),
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
assertWritable(path);
|
|
164
|
-
createFile(path, content ?? '');
|
|
165
|
-
return `File created: ${path}`;
|
|
166
|
-
} catch (e: unknown) {
|
|
167
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
168
|
-
}
|
|
169
|
-
}),
|
|
170
|
-
}),
|
|
224
|
+
},
|
|
171
225
|
|
|
172
|
-
|
|
226
|
+
{
|
|
227
|
+
name: 'append_to_file',
|
|
228
|
+
label: 'Append to File',
|
|
173
229
|
description: 'Append text to the end of an existing file. A blank line separator is added automatically.',
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
execute: logged('append_to_file', async ({ path, content }) => {
|
|
179
|
-
try {
|
|
180
|
-
assertWritable(path);
|
|
181
|
-
appendToFile(path, content);
|
|
182
|
-
return `Content appended to: ${path}`;
|
|
183
|
-
} catch (e: unknown) {
|
|
184
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
185
|
-
}
|
|
230
|
+
parameters: AppendParams,
|
|
231
|
+
execute: safeExecute(async (_id, params: Static<typeof AppendParams>) => {
|
|
232
|
+
appendToFile(params.path, params.content);
|
|
233
|
+
return textResult(`Content appended to: ${params.path}`);
|
|
186
234
|
}),
|
|
187
|
-
}
|
|
235
|
+
},
|
|
188
236
|
|
|
189
|
-
|
|
237
|
+
{
|
|
238
|
+
name: 'insert_after_heading',
|
|
239
|
+
label: 'Insert After Heading',
|
|
190
240
|
description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section.',
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}),
|
|
196
|
-
execute: logged('insert_after_heading', async ({ path, heading, content }) => {
|
|
197
|
-
try {
|
|
198
|
-
assertWritable(path);
|
|
199
|
-
insertAfterHeading(path, heading, content);
|
|
200
|
-
return `Content inserted after heading "${heading}" in ${path}`;
|
|
201
|
-
} catch (e: unknown) {
|
|
202
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
203
|
-
}
|
|
241
|
+
parameters: InsertHeadingParams,
|
|
242
|
+
execute: safeExecute(async (_id, params: Static<typeof InsertHeadingParams>) => {
|
|
243
|
+
insertAfterHeading(params.path, params.heading, params.content);
|
|
244
|
+
return textResult(`Content inserted after heading "${params.heading}" in ${params.path}`);
|
|
204
245
|
}),
|
|
205
|
-
}
|
|
246
|
+
},
|
|
206
247
|
|
|
207
|
-
|
|
248
|
+
{
|
|
249
|
+
name: 'update_section',
|
|
250
|
+
label: 'Update Section',
|
|
208
251
|
description: 'Replace the content of a Markdown section identified by its heading. The section spans from the heading to the next heading of equal or higher level.',
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
252
|
+
parameters: UpdateSectionParams,
|
|
253
|
+
execute: safeExecute(async (_id, params: Static<typeof UpdateSectionParams>) => {
|
|
254
|
+
updateSection(params.path, params.heading, params.content);
|
|
255
|
+
return textResult(`Section "${params.heading}" updated in ${params.path}`);
|
|
213
256
|
}),
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
assertWritable(path);
|
|
217
|
-
updateSection(path, heading, content);
|
|
218
|
-
return `Section "${heading}" updated in ${path}`;
|
|
219
|
-
} catch (e: unknown) {
|
|
220
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
221
|
-
}
|
|
222
|
-
}),
|
|
223
|
-
}),
|
|
224
|
-
|
|
225
|
-
// ─── New tools (Phase 1a) ──────────────────────────────────────────────────
|
|
257
|
+
},
|
|
226
258
|
|
|
227
|
-
|
|
259
|
+
{
|
|
260
|
+
name: 'delete_file',
|
|
261
|
+
label: 'Delete File',
|
|
228
262
|
description: 'Permanently delete a file from the knowledge base. This is destructive and cannot be undone.',
|
|
229
|
-
|
|
230
|
-
|
|
263
|
+
parameters: PathParam,
|
|
264
|
+
execute: safeExecute(async (_id, params: Static<typeof PathParam>) => {
|
|
265
|
+
deleteFile(params.path);
|
|
266
|
+
return textResult(`File deleted: ${params.path}`);
|
|
231
267
|
}),
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
assertWritable(path);
|
|
235
|
-
deleteFile(path);
|
|
236
|
-
return `File deleted: ${path}`;
|
|
237
|
-
} catch (e: unknown) {
|
|
238
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
239
|
-
}
|
|
240
|
-
}),
|
|
241
|
-
}),
|
|
268
|
+
},
|
|
242
269
|
|
|
243
|
-
|
|
270
|
+
{
|
|
271
|
+
name: 'rename_file',
|
|
272
|
+
label: 'Rename File',
|
|
244
273
|
description: 'Rename a file within its current directory. Only the filename changes, not the directory.',
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
execute: logged('rename_file', async ({ path, new_name }) => {
|
|
250
|
-
try {
|
|
251
|
-
assertWritable(path);
|
|
252
|
-
const newPath = renameFile(path, new_name);
|
|
253
|
-
return `File renamed: ${path} → ${newPath}`;
|
|
254
|
-
} catch (e: unknown) {
|
|
255
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
256
|
-
}
|
|
274
|
+
parameters: RenameParams,
|
|
275
|
+
execute: safeExecute(async (_id, params: Static<typeof RenameParams>) => {
|
|
276
|
+
const newPath = renameFile(params.path, params.new_name);
|
|
277
|
+
return textResult(`File renamed: ${params.path} → ${newPath}`);
|
|
257
278
|
}),
|
|
258
|
-
}
|
|
279
|
+
},
|
|
259
280
|
|
|
260
|
-
|
|
281
|
+
{
|
|
282
|
+
name: 'move_file',
|
|
283
|
+
label: 'Move File',
|
|
261
284
|
description: 'Move a file to a new location. Also returns any files that had backlinks affected by the move.',
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
285
|
+
parameters: MoveParams,
|
|
286
|
+
execute: safeExecute(async (_id, params: Static<typeof MoveParams>) => {
|
|
287
|
+
const result = moveFile(params.from_path, params.to_path);
|
|
288
|
+
const affected = result.affectedFiles.length > 0
|
|
289
|
+
? `\nAffected backlinks in: ${result.affectedFiles.join(', ')}`
|
|
290
|
+
: '';
|
|
291
|
+
return textResult(`File moved: ${params.from_path} → ${result.newPath}${affected}`);
|
|
265
292
|
}),
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
assertWritable(from_path);
|
|
269
|
-
const result = moveFile(from_path, to_path);
|
|
270
|
-
const affected = result.affectedFiles.length > 0
|
|
271
|
-
? `\nAffected backlinks in: ${result.affectedFiles.join(', ')}`
|
|
272
|
-
: '';
|
|
273
|
-
return `File moved: ${from_path} → ${result.newPath}${affected}`;
|
|
274
|
-
} catch (e: unknown) {
|
|
275
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
276
|
-
}
|
|
277
|
-
}),
|
|
278
|
-
}),
|
|
293
|
+
},
|
|
279
294
|
|
|
280
|
-
|
|
295
|
+
{
|
|
296
|
+
name: 'get_backlinks',
|
|
297
|
+
label: 'Backlinks',
|
|
281
298
|
description: 'Find all files that reference a given file path. Useful for understanding connections between notes.',
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const backlinks = findBacklinks(path);
|
|
288
|
-
if (backlinks.length === 0) return `No backlinks found for: ${path}`;
|
|
289
|
-
return backlinks.map(b => `- **${b.source}** (L${b.line}): ${b.context}`).join('\n');
|
|
290
|
-
} catch (e: unknown) {
|
|
291
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
292
|
-
}
|
|
299
|
+
parameters: PathParam,
|
|
300
|
+
execute: safeExecute(async (_id, params: Static<typeof PathParam>) => {
|
|
301
|
+
const backlinks = findBacklinks(params.path);
|
|
302
|
+
if (backlinks.length === 0) return textResult(`No backlinks found for: ${params.path}`);
|
|
303
|
+
return textResult(backlinks.map(b => `- **${b.source}** (L${b.line}): ${b.context}`).join('\n'));
|
|
293
304
|
}),
|
|
294
|
-
}
|
|
305
|
+
},
|
|
295
306
|
|
|
296
|
-
|
|
307
|
+
{
|
|
308
|
+
name: 'get_history',
|
|
309
|
+
label: 'History',
|
|
297
310
|
description: 'Get git commit history for a file. Shows recent commits that modified this file.',
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
parameters: HistoryParams,
|
|
312
|
+
execute: safeExecute(async (_id, params: Static<typeof HistoryParams>) => {
|
|
313
|
+
const commits = gitLog(params.path, params.limit ?? 10);
|
|
314
|
+
if (commits.length === 0) return textResult(`No git history found for: ${params.path}`);
|
|
315
|
+
return textResult(commits.map(c => `- \`${c.hash.slice(0, 7)}\` ${c.date} — ${c.message} (${c.author})`).join('\n'));
|
|
301
316
|
}),
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
const commits = gitLog(path, limit ?? 10);
|
|
305
|
-
if (commits.length === 0) return `No git history found for: ${path}`;
|
|
306
|
-
return commits.map(c => `- \`${c.hash.slice(0, 7)}\` ${c.date} — ${c.message} (${c.author})`).join('\n');
|
|
307
|
-
} catch (e: unknown) {
|
|
308
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
309
|
-
}
|
|
310
|
-
}),
|
|
311
|
-
}),
|
|
317
|
+
},
|
|
312
318
|
|
|
313
|
-
|
|
319
|
+
{
|
|
320
|
+
name: 'get_file_at_version',
|
|
321
|
+
label: 'File at Version',
|
|
314
322
|
description: 'Read the content of a file at a specific git commit. Use get_history first to find commit hashes.',
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}),
|
|
319
|
-
execute: logged('get_file_at_version', async ({ path, commit }) => {
|
|
320
|
-
try {
|
|
321
|
-
const content = gitShowFile(path, commit);
|
|
322
|
-
return truncate(content);
|
|
323
|
-
} catch (e: unknown) {
|
|
324
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
325
|
-
}
|
|
323
|
+
parameters: FileAtVersionParams,
|
|
324
|
+
execute: safeExecute(async (_id, params: Static<typeof FileAtVersionParams>) => {
|
|
325
|
+
return textResult(truncate(gitShowFile(params.path, params.commit)));
|
|
326
326
|
}),
|
|
327
|
-
}
|
|
327
|
+
},
|
|
328
328
|
|
|
329
|
-
|
|
329
|
+
{
|
|
330
|
+
name: 'append_csv',
|
|
331
|
+
label: 'Append CSV Row',
|
|
330
332
|
description: 'Append a row to a CSV file. Values are automatically escaped per RFC 4180.',
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
execute: logged('append_csv', async ({ path, row }) => {
|
|
336
|
-
try {
|
|
337
|
-
assertWritable(path);
|
|
338
|
-
const result = appendCsvRow(path, row);
|
|
339
|
-
return `Row appended to ${path} (now ${result.newRowCount} rows)`;
|
|
340
|
-
} catch (e: unknown) {
|
|
341
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
342
|
-
}
|
|
333
|
+
parameters: CsvAppendParams,
|
|
334
|
+
execute: safeExecute(async (_id, params: Static<typeof CsvAppendParams>) => {
|
|
335
|
+
const result = appendCsvRow(params.path, params.row);
|
|
336
|
+
return textResult(`Row appended to ${params.path} (now ${result.newRowCount} rows)`);
|
|
343
337
|
}),
|
|
344
|
-
}
|
|
345
|
-
|
|
338
|
+
},
|
|
339
|
+
];
|