@geminilight/mindos 0.5.68 → 0.5.69
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 +12 -4
- package/app/app/api/mcp/install/route.ts +99 -12
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/components/agents/SkillDetailPopover.tsx +2 -0
- package/app/lib/agent/context.ts +7 -2
- package/app/lib/agent/tools.ts +99 -6
- package/app/lib/core/backlinks.ts +12 -4
- package/app/lib/core/search.ts +17 -3
- package/app/lib/fs.ts +5 -3
- package/package.json +1 -1
package/app/app/api/ask/route.ts
CHANGED
|
@@ -357,21 +357,29 @@ export async function POST(req: NextRequest) {
|
|
|
357
357
|
contextStrategy,
|
|
358
358
|
),
|
|
359
359
|
|
|
360
|
-
// Write-protection: block writes to protected files
|
|
360
|
+
// Write-protection: block writes to protected files gracefully
|
|
361
361
|
beforeToolCall: async (context: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> => {
|
|
362
362
|
const { toolCall, args } = context;
|
|
363
363
|
// toolCall is an object with type "toolCall" and contains the tool name and ID
|
|
364
364
|
const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
|
|
365
365
|
if (toolName && WRITE_TOOLS.has(toolName)) {
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
// Special handling for batch creations where we need to check multiple files
|
|
367
|
+
const pathsToCheck: string[] = [];
|
|
368
|
+
if (toolName === 'batch_create_files' && Array.isArray((args as any).files)) {
|
|
369
|
+
(args as any).files.forEach((f: any) => { if (f.path) pathsToCheck.push(f.path); });
|
|
370
|
+
} else {
|
|
371
|
+
const singlePath = (args as any).path ?? (args as any).from_path;
|
|
372
|
+
if (singlePath) pathsToCheck.push(singlePath);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const filePath of pathsToCheck) {
|
|
368
376
|
try {
|
|
369
377
|
assertNotProtected(filePath, 'modified by AI agent');
|
|
370
378
|
} catch (e) {
|
|
371
379
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
372
380
|
return {
|
|
373
381
|
block: true,
|
|
374
|
-
reason: `Write-protection error: ${errorMsg}
|
|
382
|
+
reason: `Write-protection error: ${errorMsg}. You CANNOT modify ${filePath} because it is system-protected. Please tell the user you don't have permission to do this.`,
|
|
375
383
|
};
|
|
376
384
|
}
|
|
377
385
|
}
|
|
@@ -11,6 +11,82 @@ function parseJsonc(text: string): Record<string, unknown> {
|
|
|
11
11
|
return JSON.parse(stripped);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Ensure nested object path exists and return the leaf container */
|
|
15
|
+
function ensureNestedPath(obj: Record<string, unknown>, dotPath: string): Record<string, unknown> {
|
|
16
|
+
const parts = dotPath.split('.').filter(Boolean);
|
|
17
|
+
let current = obj;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (!current[part] || typeof current[part] !== 'object') {
|
|
20
|
+
current[part] = {};
|
|
21
|
+
}
|
|
22
|
+
current = current[part] as Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Generate a TOML section string for an MCP entry */
|
|
28
|
+
function buildTomlEntry(sectionKey: string, serverName: string, entry: Record<string, unknown>): string {
|
|
29
|
+
const lines: string[] = [];
|
|
30
|
+
lines.push(`[${sectionKey}.${serverName}]`);
|
|
31
|
+
if (entry.type) lines.push(`type = "${entry.type}"`);
|
|
32
|
+
if (entry.command) lines.push(`command = "${entry.command}"`);
|
|
33
|
+
if (entry.url) lines.push(`url = "${entry.url}"`);
|
|
34
|
+
if (Array.isArray(entry.args)) {
|
|
35
|
+
lines.push(`args = [${entry.args.map(a => `"${a}"`).join(', ')}]`);
|
|
36
|
+
}
|
|
37
|
+
if (entry.env && typeof entry.env === 'object') {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(`[${sectionKey}.${serverName}.env]`);
|
|
40
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
41
|
+
lines.push(`${k} = "${v}"`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (entry.headers && typeof entry.headers === 'object') {
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(`[${sectionKey}.${serverName}.headers]`);
|
|
47
|
+
for (const [k, v] of Object.entries(entry.headers)) {
|
|
48
|
+
lines.push(`${k} = "${v}"`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Replace or append a [section.server] block in a TOML file */
|
|
55
|
+
function mergeTomlEntry(existing: string, sectionKey: string, serverName: string, entry: Record<string, unknown>): string {
|
|
56
|
+
const sectionHeader = `[${sectionKey}.${serverName}]`;
|
|
57
|
+
const envHeader = `[${sectionKey}.${serverName}.env]`;
|
|
58
|
+
const headersHeader = `[${sectionKey}.${serverName}.headers]`;
|
|
59
|
+
const newBlock = buildTomlEntry(sectionKey, serverName, entry);
|
|
60
|
+
|
|
61
|
+
const lines = existing.split('\n');
|
|
62
|
+
const result: string[] = [];
|
|
63
|
+
let skipping = false;
|
|
64
|
+
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (trimmed === sectionHeader || trimmed === envHeader || trimmed === headersHeader) {
|
|
68
|
+
skipping = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (skipping && trimmed.startsWith('[')) {
|
|
72
|
+
skipping = false;
|
|
73
|
+
}
|
|
74
|
+
if (!skipping) {
|
|
75
|
+
result.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Remove trailing blank lines before appending
|
|
80
|
+
while (result.length > 0 && result[result.length - 1].trim() === '') {
|
|
81
|
+
result.pop();
|
|
82
|
+
}
|
|
83
|
+
result.push('');
|
|
84
|
+
result.push(newBlock);
|
|
85
|
+
result.push('');
|
|
86
|
+
|
|
87
|
+
return result.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
14
90
|
interface AgentInstallItem {
|
|
15
91
|
key: string;
|
|
16
92
|
scope: 'project' | 'global';
|
|
@@ -96,20 +172,31 @@ export async function POST(req: NextRequest) {
|
|
|
96
172
|
const absPath = expandHome(configPath);
|
|
97
173
|
|
|
98
174
|
try {
|
|
99
|
-
// Read existing config
|
|
100
|
-
let config: Record<string, unknown> = {};
|
|
101
|
-
if (fs.existsSync(absPath)) {
|
|
102
|
-
config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Merge — only touch mcpServers.mindos
|
|
106
|
-
if (!config[agent.key]) config[agent.key] = {};
|
|
107
|
-
(config[agent.key] as Record<string, unknown>).mindos = entry;
|
|
108
|
-
|
|
109
|
-
// Write
|
|
110
175
|
const dir = path.dirname(absPath);
|
|
111
176
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
112
|
-
|
|
177
|
+
|
|
178
|
+
if (agent.format === 'toml') {
|
|
179
|
+
// TOML format (e.g. Codex): merge into existing TOML or generate new
|
|
180
|
+
const existing = fs.existsSync(absPath) ? fs.readFileSync(absPath, 'utf-8') : '';
|
|
181
|
+
const merged = mergeTomlEntry(existing, agent.key, 'mindos', entry as Record<string, unknown>);
|
|
182
|
+
fs.writeFileSync(absPath, merged, 'utf-8');
|
|
183
|
+
} else {
|
|
184
|
+
// JSON format (default)
|
|
185
|
+
let config: Record<string, unknown> = {};
|
|
186
|
+
if (fs.existsSync(absPath)) {
|
|
187
|
+
config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// For global scope with nested key (e.g. VS Code: mcp.servers),
|
|
191
|
+
// write to the nested path instead of the flat key
|
|
192
|
+
const useNestedKey = isGlobal && agent.globalNestedKey;
|
|
193
|
+
const container = useNestedKey
|
|
194
|
+
? ensureNestedPath(config, agent.globalNestedKey!)
|
|
195
|
+
: (() => { if (!config[agent.key]) config[agent.key] = {}; return config[agent.key] as Record<string, unknown>; })();
|
|
196
|
+
container.mindos = entry;
|
|
197
|
+
|
|
198
|
+
fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
199
|
+
}
|
|
113
200
|
|
|
114
201
|
const result: typeof results[number] = { agent: key, status: 'ok', path: configPath, transport: effectiveTransport };
|
|
115
202
|
|
|
@@ -50,7 +50,7 @@ export async function GET(req: NextRequest) {
|
|
|
50
50
|
transport: 'http',
|
|
51
51
|
endpoint,
|
|
52
52
|
port,
|
|
53
|
-
toolCount: running ?
|
|
53
|
+
toolCount: running ? 23 : 0,
|
|
54
54
|
authConfigured,
|
|
55
55
|
// Masked for display; full token only used server-side in snippet generation
|
|
56
56
|
maskedToken: authConfigured ? maskToken(token) : undefined,
|
package/app/lib/agent/context.ts
CHANGED
|
@@ -227,7 +227,7 @@ export async function compactMessages(
|
|
|
227
227
|
|
|
228
228
|
console.log(`[ask] Compacted ${earlyMessages.length} early messages into summary (${summaryText.length} chars)`);
|
|
229
229
|
|
|
230
|
-
const summaryContent = `[
|
|
230
|
+
const summaryContent = `[System Note: Older conversation history has been truncated due to context length limits, but here is an AI-generated summary of what was discussed so far.]\n\n${summaryText}`;
|
|
231
231
|
|
|
232
232
|
// If first recent message is also 'user', merge summary into it to avoid
|
|
233
233
|
// consecutive user messages (Anthropic rejects user→user sequences).
|
|
@@ -316,10 +316,15 @@ export function hardPrune(
|
|
|
316
316
|
console.log(`[ask] Hard pruned ${cutIdx} messages, injecting synthetic user message (${messages.length} → ${pruned.length + 1})`);
|
|
317
317
|
const syntheticUser: UserMessage = {
|
|
318
318
|
role: 'user',
|
|
319
|
-
content: '[
|
|
319
|
+
content: '[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]',
|
|
320
320
|
timestamp: Date.now(),
|
|
321
321
|
};
|
|
322
322
|
return [syntheticUser as AgentMessage, ...pruned];
|
|
323
|
+
} else if (cutIdx > 0 && pruned.length > 0 && (pruned[0] as any).role === 'user') {
|
|
324
|
+
// If we pruned and the first message IS a user message, prepend the warning to it
|
|
325
|
+
const firstMsg = { ...pruned[0] } as UserMessage;
|
|
326
|
+
firstMsg.content = `[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]\n\n` + firstMsg.content;
|
|
327
|
+
pruned[0] = firstMsg as AgentMessage;
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
if (cutIdx > 0) {
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -12,7 +12,14 @@ const MAX_FILE_CHARS = 20_000;
|
|
|
12
12
|
|
|
13
13
|
export function truncate(content: string): string {
|
|
14
14
|
if (content.length <= MAX_FILE_CHARS) return content;
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// Smart truncation: try to truncate at a natural boundary (newline)
|
|
17
|
+
let cutoff = content.lastIndexOf('\n', MAX_FILE_CHARS);
|
|
18
|
+
if (cutoff === -1) cutoff = MAX_FILE_CHARS;
|
|
19
|
+
|
|
20
|
+
const totalLines = content.split('\n').length;
|
|
21
|
+
|
|
22
|
+
return content.slice(0, cutoff) + `\n\n[...truncated — file is ${content.length} chars (${totalLines} lines), showing first ~${cutoff} chars]\n[Use the read_file_chunk tool to read the rest of the file by specifying start_line and end_line]`;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
// ─── Helper: format tool error consistently ────────────────────────────────
|
|
@@ -51,6 +58,12 @@ const PathParam = Type.Object({
|
|
|
51
58
|
path: Type.String({ description: 'Relative file path' }),
|
|
52
59
|
});
|
|
53
60
|
|
|
61
|
+
const ReadFileChunkParams = Type.Object({
|
|
62
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
63
|
+
start_line: Type.Number({ description: 'Line number to start reading from (1-indexed)' }),
|
|
64
|
+
end_line: Type.Number({ description: 'Line number to stop reading at (1-indexed)' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
54
67
|
const QueryParam = Type.Object({
|
|
55
68
|
query: Type.String({ description: 'Search query (case-insensitive)' }),
|
|
56
69
|
});
|
|
@@ -69,6 +82,13 @@ const CreateFileParams = Type.Object({
|
|
|
69
82
|
content: Type.Optional(Type.String({ description: 'Initial file content' })),
|
|
70
83
|
});
|
|
71
84
|
|
|
85
|
+
const BatchCreateFileParams = Type.Object({
|
|
86
|
+
files: Type.Array(Type.Object({
|
|
87
|
+
path: Type.String({ description: 'Relative file path (must end in .md or .csv)' }),
|
|
88
|
+
content: Type.String({ description: 'Initial file content' }),
|
|
89
|
+
}), { description: 'List of files to create' }),
|
|
90
|
+
});
|
|
91
|
+
|
|
72
92
|
const AppendParams = Type.Object({
|
|
73
93
|
path: Type.String({ description: 'Relative file path' }),
|
|
74
94
|
content: Type.String({ description: 'Content to append' }),
|
|
@@ -86,6 +106,13 @@ const UpdateSectionParams = Type.Object({
|
|
|
86
106
|
content: Type.String({ description: 'New content for the section' }),
|
|
87
107
|
});
|
|
88
108
|
|
|
109
|
+
const EditLinesParams = Type.Object({
|
|
110
|
+
path: Type.String({ description: 'Relative file path' }),
|
|
111
|
+
start_line: Type.Number({ description: '1-indexed line number to start replacing' }),
|
|
112
|
+
end_line: Type.Number({ description: '1-indexed line number to stop replacing (inclusive)' }),
|
|
113
|
+
content: Type.String({ description: 'New content to insert in place of those lines' }),
|
|
114
|
+
});
|
|
115
|
+
|
|
89
116
|
const RenameParams = Type.Object({
|
|
90
117
|
path: Type.String({ description: 'Current relative file path' }),
|
|
91
118
|
new_name: Type.String({ description: 'New filename (no path separators, e.g. "new-name.md")' }),
|
|
@@ -115,8 +142,8 @@ const CsvAppendParams = Type.Object({
|
|
|
115
142
|
|
|
116
143
|
// Write-operation tool names — used by beforeToolCall for write-protection
|
|
117
144
|
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',
|
|
145
|
+
'write_file', 'create_file', 'batch_create_files', 'append_to_file', 'insert_after_heading',
|
|
146
|
+
'update_section', 'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
|
|
120
147
|
]);
|
|
121
148
|
|
|
122
149
|
export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
@@ -171,13 +198,39 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
171
198
|
{
|
|
172
199
|
name: 'read_file',
|
|
173
200
|
label: 'Read File',
|
|
174
|
-
description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
|
|
201
|
+
description: 'Read the content of a file by its relative path. Always read a file before modifying it. If the file is too large, it will be truncated. Use read_file_chunk to read specific parts of large files.',
|
|
175
202
|
parameters: PathParam,
|
|
176
203
|
execute: safeExecute(async (_id, params: Static<typeof PathParam>) => {
|
|
177
204
|
return textResult(truncate(getFileContent(params.path)));
|
|
178
205
|
}),
|
|
179
206
|
},
|
|
180
207
|
|
|
208
|
+
{
|
|
209
|
+
name: 'read_file_chunk',
|
|
210
|
+
label: 'Read File Chunk',
|
|
211
|
+
description: 'Read a specific range of lines from a file. Highly recommended for reading large files that were truncated by read_file.',
|
|
212
|
+
parameters: ReadFileChunkParams,
|
|
213
|
+
execute: safeExecute(async (_id, params: Static<typeof ReadFileChunkParams>) => {
|
|
214
|
+
const content = getFileContent(params.path);
|
|
215
|
+
const lines = content.split('\n');
|
|
216
|
+
const start = Math.max(1, params.start_line);
|
|
217
|
+
const end = Math.min(lines.length, params.end_line);
|
|
218
|
+
|
|
219
|
+
if (start > end) {
|
|
220
|
+
return textResult(`Error: start_line (${start}) is greater than end_line (${end}) or file has fewer lines.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Prefix each line with its line number (1-indexed)
|
|
224
|
+
const pad = String(lines.length).length;
|
|
225
|
+
const chunk = lines
|
|
226
|
+
.slice(start - 1, end)
|
|
227
|
+
.map((l, i) => `${String(start + i).padStart(pad, ' ')} | ${l}`)
|
|
228
|
+
.join('\n');
|
|
229
|
+
|
|
230
|
+
return textResult(`Showing lines ${start} to ${end} of ${lines.length}:\n\n${chunk}`);
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
|
|
181
234
|
{
|
|
182
235
|
name: 'search',
|
|
183
236
|
label: 'Search',
|
|
@@ -223,6 +276,28 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
223
276
|
}),
|
|
224
277
|
},
|
|
225
278
|
|
|
279
|
+
{
|
|
280
|
+
name: 'batch_create_files',
|
|
281
|
+
label: 'Batch Create Files',
|
|
282
|
+
description: 'Create multiple new files in a single operation. Highly recommended when scaffolding new features or projects.',
|
|
283
|
+
parameters: BatchCreateFileParams,
|
|
284
|
+
execute: safeExecute(async (_id, params: Static<typeof BatchCreateFileParams>) => {
|
|
285
|
+
const created: string[] = [];
|
|
286
|
+
const errors: string[] = [];
|
|
287
|
+
for (const file of params.files) {
|
|
288
|
+
try {
|
|
289
|
+
createFile(file.path, file.content);
|
|
290
|
+
created.push(file.path);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
errors.push(`${file.path}: ${formatToolError(e)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
let msg = `Batch creation complete.\nCreated ${created.length} files: ${created.join(', ')}`;
|
|
296
|
+
if (errors.length > 0) msg += `\n\nFailed to create ${errors.length} files:\n${errors.join('\n')}`;
|
|
297
|
+
return textResult(msg);
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
|
|
226
301
|
{
|
|
227
302
|
name: 'append_to_file',
|
|
228
303
|
label: 'Append to File',
|
|
@@ -237,7 +312,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
237
312
|
{
|
|
238
313
|
name: 'insert_after_heading',
|
|
239
314
|
label: 'Insert After Heading',
|
|
240
|
-
description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section.',
|
|
315
|
+
description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section. If heading matches fail, use edit_lines instead.',
|
|
241
316
|
parameters: InsertHeadingParams,
|
|
242
317
|
execute: safeExecute(async (_id, params: Static<typeof InsertHeadingParams>) => {
|
|
243
318
|
insertAfterHeading(params.path, params.heading, params.content);
|
|
@@ -248,7 +323,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
248
323
|
{
|
|
249
324
|
name: 'update_section',
|
|
250
325
|
label: 'Update Section',
|
|
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.',
|
|
326
|
+
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. If heading matches fail, use edit_lines instead.',
|
|
252
327
|
parameters: UpdateSectionParams,
|
|
253
328
|
execute: safeExecute(async (_id, params: Static<typeof UpdateSectionParams>) => {
|
|
254
329
|
updateSection(params.path, params.heading, params.content);
|
|
@@ -256,6 +331,24 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
256
331
|
}),
|
|
257
332
|
},
|
|
258
333
|
|
|
334
|
+
{
|
|
335
|
+
name: 'edit_lines',
|
|
336
|
+
label: 'Edit Lines',
|
|
337
|
+
description: 'Replace a specific range of lines with new content. Extremely reliable for precise edits. You must know the exact line numbers (use read_file_chunk to get them).',
|
|
338
|
+
parameters: EditLinesParams,
|
|
339
|
+
execute: safeExecute(async (_id, params: Static<typeof EditLinesParams>) => {
|
|
340
|
+
const { path: fp, start_line, end_line, content } = params;
|
|
341
|
+
const start = Math.max(0, start_line - 1);
|
|
342
|
+
const end = Math.max(0, end_line - 1);
|
|
343
|
+
|
|
344
|
+
const mindRoot = getMindRoot();
|
|
345
|
+
// Import the core function dynamically or it should be added to lib/fs.ts
|
|
346
|
+
const { updateLines } = await import('@/lib/core');
|
|
347
|
+
updateLines(mindRoot, fp, start, end, content.split('\n'));
|
|
348
|
+
return textResult(`Lines ${start_line}-${end_line} replaced in ${fp}`);
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
|
|
259
352
|
{
|
|
260
353
|
name: 'delete_file',
|
|
261
354
|
label: 'Delete File',
|
|
@@ -28,11 +28,19 @@ export function findBacklinks(mindRoot: string, targetPath: string): BacklinkEnt
|
|
|
28
28
|
const lines = content.split('\n');
|
|
29
29
|
for (let i = 0; i < lines.length; i++) {
|
|
30
30
|
if (patterns.some(p => p.test(lines[i]))) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Expand to a slightly larger context block for agent comprehension
|
|
32
|
+
// Attempt to find paragraph boundaries (empty lines) or cap at a reasonable size
|
|
33
|
+
let start = i;
|
|
34
|
+
while (start > 0 && start > i - 3 && lines[start].trim() !== '') start--;
|
|
35
|
+
let end = i;
|
|
36
|
+
while (end < lines.length - 1 && end < i + 3 && lines[end].trim() !== '') end++;
|
|
37
|
+
|
|
38
|
+
let ctx = lines.slice(start, end + 1).join('\n').trim();
|
|
39
|
+
// Collapse multiple newlines in the context to save tokens, but keep simple structure
|
|
40
|
+
ctx = ctx.replace(/\n{2,}/g, ' ↵ ');
|
|
41
|
+
|
|
34
42
|
results.push({ source: filePath, line: i + 1, context: ctx });
|
|
35
|
-
break;
|
|
43
|
+
break; // currently only records the first match per file
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
}
|
package/app/lib/core/search.ts
CHANGED
|
@@ -91,9 +91,23 @@ export function searchFiles(mindRoot: string, query: string, opts: SearchOptions
|
|
|
91
91
|
const index = lowerContent.indexOf(lowerQuery);
|
|
92
92
|
if (index === -1) continue;
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// Try to find natural boundaries (paragraphs) around the match
|
|
95
|
+
let snippetStart = content.lastIndexOf('\n\n', index);
|
|
96
|
+
if (snippetStart === -1) snippetStart = Math.max(0, index - 200);
|
|
97
|
+
else snippetStart += 2; // skip the newlines
|
|
98
|
+
|
|
99
|
+
let snippetEnd = content.indexOf('\n\n', index);
|
|
100
|
+
if (snippetEnd === -1) snippetEnd = Math.min(content.length, index + query.length + 200);
|
|
101
|
+
|
|
102
|
+
// Prevent massive blocks (cap at ~400 chars total)
|
|
103
|
+
if (index - snippetStart > 200) snippetStart = index - 200;
|
|
104
|
+
if (snippetEnd - index > 200) snippetEnd = index + query.length + 200;
|
|
105
|
+
|
|
106
|
+
let snippet = content.slice(snippetStart, snippetEnd).trim();
|
|
107
|
+
|
|
108
|
+
// Collapse internal whitespace for cleaner search result presentation, but preserve some structure
|
|
109
|
+
snippet = snippet.replace(/\n{3,}/g, '\n\n');
|
|
110
|
+
|
|
97
111
|
if (snippetStart > 0) snippet = '...' + snippet;
|
|
98
112
|
if (snippetEnd < content.length) snippet += '...';
|
|
99
113
|
|
package/app/lib/fs.ts
CHANGED
|
@@ -525,8 +525,8 @@ function generateSnippet(
|
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
const snippetStart = Math.max(0, bestStart -
|
|
529
|
-
const snippetEnd = Math.min(content.length, bestEnd +
|
|
528
|
+
const snippetStart = Math.max(0, bestStart - 120);
|
|
529
|
+
const snippetEnd = Math.min(content.length, bestEnd + 120);
|
|
530
530
|
|
|
531
531
|
let start = snippetStart;
|
|
532
532
|
if (start > 0) {
|
|
@@ -539,7 +539,9 @@ function generateSnippet(
|
|
|
539
539
|
if (spaceIdx > bestEnd) end = spaceIdx;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
let snippet = content.slice(start, end).
|
|
542
|
+
let snippet = content.slice(start, end).trim();
|
|
543
|
+
// Collapse multiple newlines into spaces but keep single newlines
|
|
544
|
+
snippet = snippet.replace(/\n{2,}/g, ' ↵ ');
|
|
543
545
|
if (start > 0) snippet = '...' + snippet;
|
|
544
546
|
if (end < content.length) snippet = snippet + '...';
|
|
545
547
|
return snippet;
|
package/package.json
CHANGED