@geminilight/mindos 0.6.29 → 0.6.31
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 +10 -4
- package/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
package/app/lib/actions.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { createFile, deleteFile, deleteDirectory, convertToSpace, renameFile, renameSpace, getMindRoot, invalidateCache, collectAllFiles } from '@/lib/fs';
|
|
6
|
+
import { moveToTrash, restoreFromTrash, restoreAsCopy, permanentlyDelete, listTrash, emptyTrash, purgeExpired, type TrashMeta } from '@/lib/core/trash';
|
|
6
7
|
import { createSpaceFilesystem, generateReadmeTemplate } from '@/lib/core/create-space';
|
|
7
8
|
import { INSTRUCTION_TEMPLATE, cleanDirName } from '@/lib/core/space-scaffold';
|
|
8
9
|
import { revalidatePath } from 'next/cache';
|
|
@@ -25,7 +26,8 @@ export async function createFileAction(dirPath: string, fileName: string): Promi
|
|
|
25
26
|
|
|
26
27
|
export async function deleteFileAction(filePath: string): Promise<{ success: boolean; error?: string }> {
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
moveToTrash(getMindRoot(), filePath);
|
|
30
|
+
invalidateCache();
|
|
29
31
|
revalidatePath('/', 'layout');
|
|
30
32
|
return { success: true };
|
|
31
33
|
} catch (err) {
|
|
@@ -59,7 +61,8 @@ export async function deleteFolderAction(
|
|
|
59
61
|
dirPath: string,
|
|
60
62
|
): Promise<{ success: boolean; error?: string }> {
|
|
61
63
|
try {
|
|
62
|
-
|
|
64
|
+
moveToTrash(getMindRoot(), dirPath);
|
|
65
|
+
invalidateCache();
|
|
63
66
|
revalidatePath('/', 'layout');
|
|
64
67
|
return { success: true };
|
|
65
68
|
} catch (err) {
|
|
@@ -84,7 +87,8 @@ export async function deleteSpaceAction(
|
|
|
84
87
|
spacePath: string,
|
|
85
88
|
): Promise<{ success: boolean; error?: string }> {
|
|
86
89
|
try {
|
|
87
|
-
|
|
90
|
+
moveToTrash(getMindRoot(), spacePath);
|
|
91
|
+
invalidateCache();
|
|
88
92
|
revalidatePath('/', 'layout');
|
|
89
93
|
return { success: true };
|
|
90
94
|
} catch (err) {
|
|
@@ -189,3 +193,53 @@ export async function cleanupExamplesAction(): Promise<{ success: boolean; delet
|
|
|
189
193
|
return { success: false, deleted: 0, error: err instanceof Error ? err.message : 'Failed to cleanup' };
|
|
190
194
|
}
|
|
191
195
|
}
|
|
196
|
+
|
|
197
|
+
// ─── Trash Actions ────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
export async function listTrashAction(): Promise<TrashMeta[]> {
|
|
200
|
+
// Purge expired items on each list call (lazy cleanup)
|
|
201
|
+
purgeExpired(getMindRoot());
|
|
202
|
+
return listTrash(getMindRoot());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function restoreFromTrashAction(
|
|
206
|
+
trashId: string,
|
|
207
|
+
mode: 'restore' | 'overwrite' | 'copy' = 'restore',
|
|
208
|
+
): Promise<{ success: boolean; restoredPath?: string; error?: string; conflict?: boolean }> {
|
|
209
|
+
try {
|
|
210
|
+
const root = getMindRoot();
|
|
211
|
+
let result: { restoredPath: string };
|
|
212
|
+
if (mode === 'copy') {
|
|
213
|
+
result = restoreAsCopy(root, trashId);
|
|
214
|
+
} else {
|
|
215
|
+
result = restoreFromTrash(root, trashId, mode === 'overwrite');
|
|
216
|
+
}
|
|
217
|
+
invalidateCache();
|
|
218
|
+
revalidatePath('/', 'layout');
|
|
219
|
+
return { success: true, restoredPath: result.restoredPath };
|
|
220
|
+
} catch (err: unknown) {
|
|
221
|
+
const e = err as Error & { code?: string };
|
|
222
|
+
if (e.code === 'RESTORE_CONFLICT') {
|
|
223
|
+
return { success: false, error: e.message, conflict: true };
|
|
224
|
+
}
|
|
225
|
+
return { success: false, error: e.message ?? 'Failed to restore' };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function permanentlyDeleteAction(trashId: string): Promise<{ success: boolean; error?: string }> {
|
|
230
|
+
try {
|
|
231
|
+
permanentlyDelete(getMindRoot(), trashId);
|
|
232
|
+
return { success: true };
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to delete' };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function emptyTrashAction(): Promise<{ success: boolean; count?: number; error?: string }> {
|
|
239
|
+
try {
|
|
240
|
+
const count = emptyTrash(getMindRoot());
|
|
241
|
+
return { success: true, count };
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to empty trash' };
|
|
244
|
+
}
|
|
245
|
+
}
|
package/app/lib/agent/model.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { getModel as piGetModel, type Model } from '@mariozechner/pi-ai';
|
|
2
2
|
import { effectiveAiConfig } from '@/lib/settings';
|
|
3
3
|
|
|
4
|
+
/** Check if any message in the conversation contains images */
|
|
5
|
+
export function hasImages(messages: Array<{ images?: unknown[] }>): boolean {
|
|
6
|
+
return messages.some(m => m.images && m.images.length > 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Ensure model input includes 'image' when images are present */
|
|
10
|
+
function ensureVisionCapable(model: Model<any>): Model<any> {
|
|
11
|
+
const inputs = model.input as readonly string[];
|
|
12
|
+
if (inputs.includes('image')) return model;
|
|
13
|
+
// Upgrade input to include image — most modern models support it
|
|
14
|
+
return { ...model, input: [...inputs, 'image'] as any };
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
/**
|
|
5
18
|
* Build a pi-ai Model for the configured provider.
|
|
6
19
|
*
|
|
@@ -11,7 +24,7 @@ import { effectiveAiConfig } from '@/lib/settings';
|
|
|
11
24
|
*
|
|
12
25
|
* Returns { model, modelName, apiKey } — Agent needs model + apiKey via getApiKey hook.
|
|
13
26
|
*/
|
|
14
|
-
export function getModelConfig(): {
|
|
27
|
+
export function getModelConfig(options?: { hasImages?: boolean }): {
|
|
15
28
|
model: Model<any>;
|
|
16
29
|
modelName: string;
|
|
17
30
|
apiKey: string;
|
|
@@ -77,7 +90,8 @@ export function getModelConfig(): {
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
|
|
94
|
+
return { model: finalModel, modelName, apiKey: cfg.openaiApiKey, provider: 'openai' };
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
// Anthropic
|
|
@@ -104,5 +118,6 @@ export function getModelConfig(): {
|
|
|
104
118
|
};
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
|
|
122
|
+
return { model: finalModel, modelName, apiKey: cfg.anthropicApiKey, provider: 'anthropic' };
|
|
108
123
|
}
|
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { Message, MessagePart, ToolCallPart, TextPart, ReasoningPart } from '@/lib/types';
|
|
18
18
|
|
|
19
|
+
/** Tools that modify files — trigger files-changed notification on completion */
|
|
20
|
+
const FILE_MUTATING_TOOLS = new Set([
|
|
21
|
+
'write_file', 'create_file', 'batch_create_files',
|
|
22
|
+
'update_section', 'insert_after_heading', 'delete_file',
|
|
23
|
+
'rename_file', 'create_space',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Notify the app that files were changed by the AI agent */
|
|
27
|
+
function notifyFilesChanged() {
|
|
28
|
+
if (typeof window !== 'undefined') {
|
|
29
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export async function consumeUIMessageStream(
|
|
20
34
|
body: ReadableStream<Uint8Array>,
|
|
21
35
|
onUpdate: (message: Message) => void,
|
|
@@ -158,6 +172,10 @@ export async function consumeUIMessageStream(
|
|
|
158
172
|
tc.output = output ?? '';
|
|
159
173
|
tc.state = (event.isError ? 'error' : 'done');
|
|
160
174
|
changed = true;
|
|
175
|
+
// Notify when a file-modifying tool completes successfully
|
|
176
|
+
if (!event.isError && FILE_MUTATING_TOOLS.has(tc.toolName)) {
|
|
177
|
+
notifyFilesChanged();
|
|
178
|
+
}
|
|
161
179
|
}
|
|
162
180
|
break;
|
|
163
181
|
}
|
|
@@ -13,13 +13,36 @@
|
|
|
13
13
|
* - Orphaned tool calls (running/pending from interrupted streams): supply empty result
|
|
14
14
|
* - Reasoning parts: filtered out (display-only, not sent back to LLM)
|
|
15
15
|
*/
|
|
16
|
-
import type { Message as FrontendMessage, ToolCallPart as FrontendToolCallPart } from '@/lib/types';
|
|
16
|
+
import type { Message as FrontendMessage, ToolCallPart as FrontendToolCallPart, ImagePart } from '@/lib/types';
|
|
17
17
|
import type { AgentMessage } from '@mariozechner/pi-agent-core';
|
|
18
18
|
import type { UserMessage, AssistantMessage, ToolResultMessage } from '@mariozechner/pi-ai';
|
|
19
19
|
|
|
20
20
|
// Re-export for convenience
|
|
21
21
|
export type { AgentMessage } from '@mariozechner/pi-agent-core';
|
|
22
22
|
|
|
23
|
+
/** Build multimodal content array for user messages with images */
|
|
24
|
+
function buildUserContent(text: string, images?: ImagePart[]): string | any[] {
|
|
25
|
+
// Filter out stripped images (empty data from persisted sessions)
|
|
26
|
+
const validImages = images?.filter(img => img.data);
|
|
27
|
+
if (!validImages || validImages.length === 0) return text;
|
|
28
|
+
|
|
29
|
+
// Multimodal content: images first, then text
|
|
30
|
+
// Use pi-ai ImageContent format: { type: 'image', data: base64, mimeType }
|
|
31
|
+
// The SDK converts this to the provider-specific format (Anthropic/OpenAI) internally
|
|
32
|
+
const parts: any[] = [];
|
|
33
|
+
for (const img of validImages) {
|
|
34
|
+
parts.push({
|
|
35
|
+
type: 'image',
|
|
36
|
+
data: img.data,
|
|
37
|
+
mimeType: img.mimeType,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (text) {
|
|
41
|
+
parts.push({ type: 'text', text });
|
|
42
|
+
}
|
|
43
|
+
return parts;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
|
|
24
47
|
const result: AgentMessage[] = [];
|
|
25
48
|
|
|
@@ -29,7 +52,7 @@ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
|
|
|
29
52
|
if (msg.role === 'user') {
|
|
30
53
|
result.push({
|
|
31
54
|
role: 'user',
|
|
32
|
-
content: msg.content,
|
|
55
|
+
content: buildUserContent(msg.content, msg.images),
|
|
33
56
|
timestamp,
|
|
34
57
|
} satisfies UserMessage as AgentMessage);
|
|
35
58
|
continue;
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { readSkillContentByName, scanSkillDirs } from '@/lib/pi-integration/skil
|
|
|
11
11
|
import { callMcporterTool, createMcporterAgentTools, listMcporterServers, listMcporterTools } from '@/lib/pi-integration/mcporter';
|
|
12
12
|
import { a2aTools } from '@/lib/a2a/a2a-tools';
|
|
13
13
|
import { acpTools } from '@/lib/acp/acp-tools';
|
|
14
|
+
import { buildLineDiff, collapseDiffContext } from '@/components/changes/line-diff';
|
|
14
15
|
|
|
15
16
|
// Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
|
|
16
17
|
const MAX_FILE_CHARS = 20_000;
|
|
@@ -39,6 +40,38 @@ function textResult(text: string): AgentToolResult<Record<string, never>> {
|
|
|
39
40
|
return { content: [{ type: 'text', text }], details: {} as Record<string, never> };
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Build a compact diff summary for tool output. Max 30 diff lines to avoid bloating agent context. */
|
|
44
|
+
function buildDiffSummary(before: string, after: string): string {
|
|
45
|
+
if (before === after) return '';
|
|
46
|
+
// Skip diff for very large files — LCS is O(n*m), would block agent
|
|
47
|
+
const beforeLines = before.split('\n').length;
|
|
48
|
+
const afterLines = after.split('\n').length;
|
|
49
|
+
if (beforeLines > 2000 || afterLines > 2000) {
|
|
50
|
+
const added = Math.max(0, afterLines - beforeLines);
|
|
51
|
+
const removed = Math.max(0, beforeLines - afterLines);
|
|
52
|
+
return `(~+${added} ~−${removed}, ${afterLines} lines total)\n\n--- changes ---\n (diff skipped — file too large)`;
|
|
53
|
+
}
|
|
54
|
+
const raw = buildLineDiff(before, after);
|
|
55
|
+
const inserts = raw.filter(r => r.type === 'insert').length;
|
|
56
|
+
const deletes = raw.filter(r => r.type === 'delete').length;
|
|
57
|
+
const stats = `+${inserts} −${deletes}`;
|
|
58
|
+
const collapsed = collapseDiffContext(raw, 2);
|
|
59
|
+
const MAX_DIFF_LINES = 30;
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
for (const row of collapsed) {
|
|
62
|
+
if (lines.length >= MAX_DIFF_LINES) { lines.push('... (diff truncated)'); break; }
|
|
63
|
+
if (row.type === 'gap') { lines.push(` ... ${row.count} lines unchanged ...`); continue; }
|
|
64
|
+
const prefix = row.type === 'insert' ? '+' : row.type === 'delete' ? '-' : ' ';
|
|
65
|
+
lines.push(`${prefix} ${row.text}`);
|
|
66
|
+
}
|
|
67
|
+
return `(${stats})\n\n--- changes ---\n${lines.join('\n')}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Safe read — returns empty string if file doesn't exist */
|
|
71
|
+
function safeReadContent(filePath: string): string {
|
|
72
|
+
try { return getFileContent(filePath); } catch { return ''; }
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
/** Safe execute wrapper — catches all errors, returns error text (never throws) */
|
|
43
76
|
function safeExecute<T>(
|
|
44
77
|
fn: (toolCallId: string, params: T, signal?: AbortSignal) => Promise<AgentToolResult<any>>,
|
|
@@ -527,8 +560,10 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
527
560
|
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.',
|
|
528
561
|
parameters: WriteFileParams,
|
|
529
562
|
execute: safeExecute(async (_id, params: Static<typeof WriteFileParams>) => {
|
|
563
|
+
const before = safeReadContent(params.path);
|
|
530
564
|
saveFileContent(params.path, params.content);
|
|
531
|
-
|
|
565
|
+
const diff = buildDiffSummary(before, params.content);
|
|
566
|
+
return textResult(`File written: ${params.path}${diff ? ' ' + diff : ''}`);
|
|
532
567
|
}),
|
|
533
568
|
},
|
|
534
569
|
|
|
@@ -538,8 +573,10 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
538
573
|
description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically. Does NOT create Space scaffolding (INSTRUCTION.md/README.md). Use create_space to create a Space.',
|
|
539
574
|
parameters: CreateFileParams,
|
|
540
575
|
execute: safeExecute(async (_id, params: Static<typeof CreateFileParams>) => {
|
|
541
|
-
|
|
542
|
-
|
|
576
|
+
const content = params.content ?? '';
|
|
577
|
+
createFile(params.path, content);
|
|
578
|
+
const lineCount = content.split('\n').length;
|
|
579
|
+
return textResult(`File created: ${params.path} (+${lineCount})\n\n--- changes ---\n${content.split('\n').slice(0, 30).map(l => '+ ' + l).join('\n')}${lineCount > 30 ? '\n... (truncated)' : ''}`);
|
|
543
580
|
}),
|
|
544
581
|
},
|
|
545
582
|
|
|
@@ -571,8 +608,11 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
571
608
|
description: 'Append text to the end of an existing file. A blank line separator is added automatically.',
|
|
572
609
|
parameters: AppendParams,
|
|
573
610
|
execute: safeExecute(async (_id, params: Static<typeof AppendParams>) => {
|
|
611
|
+
const before = safeReadContent(params.path);
|
|
574
612
|
appendToFile(params.path, params.content);
|
|
575
|
-
|
|
613
|
+
const after = safeReadContent(params.path);
|
|
614
|
+
const diff = buildDiffSummary(before, after);
|
|
615
|
+
return textResult(`Content appended to: ${params.path}${diff ? ' ' + diff : ''}`);
|
|
576
616
|
}),
|
|
577
617
|
},
|
|
578
618
|
|
|
@@ -582,8 +622,11 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
582
622
|
description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section. If heading matches fail, use edit_lines instead.',
|
|
583
623
|
parameters: InsertHeadingParams,
|
|
584
624
|
execute: safeExecute(async (_id, params: Static<typeof InsertHeadingParams>) => {
|
|
625
|
+
const before = safeReadContent(params.path);
|
|
585
626
|
insertAfterHeading(params.path, params.heading, params.content);
|
|
586
|
-
|
|
627
|
+
const after = safeReadContent(params.path);
|
|
628
|
+
const diff = buildDiffSummary(before, after);
|
|
629
|
+
return textResult(`Content inserted after heading "${params.heading}" in ${params.path}${diff ? ' ' + diff : ''}`);
|
|
587
630
|
}),
|
|
588
631
|
},
|
|
589
632
|
|
|
@@ -593,8 +636,11 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
593
636
|
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.',
|
|
594
637
|
parameters: UpdateSectionParams,
|
|
595
638
|
execute: safeExecute(async (_id, params: Static<typeof UpdateSectionParams>) => {
|
|
639
|
+
const before = safeReadContent(params.path);
|
|
596
640
|
updateSection(params.path, params.heading, params.content);
|
|
597
|
-
|
|
641
|
+
const after = safeReadContent(params.path);
|
|
642
|
+
const diff = buildDiffSummary(before, after);
|
|
643
|
+
return textResult(`Section "${params.heading}" updated in ${params.path}${diff ? ' ' + diff : ''}`);
|
|
598
644
|
}),
|
|
599
645
|
},
|
|
600
646
|
|
|
@@ -607,12 +653,13 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
607
653
|
const { path: fp, start_line, end_line, content } = params;
|
|
608
654
|
const start = Math.max(0, start_line - 1);
|
|
609
655
|
const end = Math.max(0, end_line - 1);
|
|
610
|
-
|
|
656
|
+
const before = safeReadContent(fp);
|
|
611
657
|
const mindRoot = getMindRoot();
|
|
612
|
-
// Import the core function dynamically or it should be added to lib/fs.ts
|
|
613
658
|
const { updateLines } = await import('@/lib/core');
|
|
614
659
|
updateLines(mindRoot, fp, start, end, content.split('\n'));
|
|
615
|
-
|
|
660
|
+
const after = safeReadContent(fp);
|
|
661
|
+
const diff = buildDiffSummary(before, after);
|
|
662
|
+
return textResult(`Lines ${start_line}-${end_line} replaced in ${fp}${diff ? ' ' + diff : ''}`);
|
|
616
663
|
}),
|
|
617
664
|
},
|
|
618
665
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { remark } from 'remark';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remarkHtml from 'remark-html';
|
|
6
|
+
|
|
7
|
+
/** Convert wiki-links [[target]] → relative HTML links */
|
|
8
|
+
export function convertWikiLinks(content: string, _currentPath: string): string {
|
|
9
|
+
return content.replace(/\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g, (_match, target: string, display?: string) => {
|
|
10
|
+
const label = display ?? target;
|
|
11
|
+
const href = target.replace(/\s+/g, '%20');
|
|
12
|
+
return `<a href="${href}.html">${label}</a>`;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Convert markdown to a complete standalone HTML document */
|
|
17
|
+
export async function markdownToHTML(content: string, title: string, currentPath = ''): Promise<string> {
|
|
18
|
+
// Convert wiki-links first
|
|
19
|
+
const processed = convertWikiLinks(content, currentPath);
|
|
20
|
+
|
|
21
|
+
const result = await remark()
|
|
22
|
+
.use(remarkGfm)
|
|
23
|
+
.use(remarkHtml, { sanitize: false })
|
|
24
|
+
.process(processed);
|
|
25
|
+
|
|
26
|
+
const body = String(result);
|
|
27
|
+
|
|
28
|
+
return `<!DOCTYPE html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<meta charset="UTF-8">
|
|
32
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
33
|
+
<title>${escapeHtml(title)}</title>
|
|
34
|
+
<style>
|
|
35
|
+
:root { color-scheme: light dark; }
|
|
36
|
+
body {
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
38
|
+
max-width: 720px;
|
|
39
|
+
margin: 2rem auto;
|
|
40
|
+
padding: 0 1.5rem;
|
|
41
|
+
line-height: 1.7;
|
|
42
|
+
color: #1a1a1a;
|
|
43
|
+
background: #fff;
|
|
44
|
+
}
|
|
45
|
+
@media (prefers-color-scheme: dark) {
|
|
46
|
+
body { color: #e0e0e0; background: #1a1a1a; }
|
|
47
|
+
a { color: #6eb5ff; }
|
|
48
|
+
code { background: #2a2a2a; }
|
|
49
|
+
pre { background: #2a2a2a; }
|
|
50
|
+
blockquote { border-color: #444; }
|
|
51
|
+
table, th, td { border-color: #444; }
|
|
52
|
+
}
|
|
53
|
+
h1 { font-size: 1.8rem; font-weight: 700; margin: 2rem 0 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.3rem; }
|
|
54
|
+
h2 { font-size: 1.4rem; font-weight: 600; margin: 1.5rem 0 0.8rem; }
|
|
55
|
+
h3 { font-size: 1.15rem; font-weight: 600; margin: 1.2rem 0 0.5rem; }
|
|
56
|
+
a { color: #0066cc; text-decoration: none; }
|
|
57
|
+
a:hover { text-decoration: underline; }
|
|
58
|
+
code { font-family: 'SF Mono', Menlo, monospace; font-size: 0.875em; background: #f5f5f5; padding: 0.15em 0.4em; border-radius: 3px; }
|
|
59
|
+
pre { background: #f5f5f5; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
|
60
|
+
pre code { background: none; padding: 0; }
|
|
61
|
+
blockquote { margin: 1rem 0; padding: 0.5rem 1rem; border-left: 3px solid #ddd; color: #666; }
|
|
62
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
63
|
+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; }
|
|
64
|
+
th { background: #f8f8f8; font-weight: 600; }
|
|
65
|
+
img { max-width: 100%; height: auto; border-radius: 4px; }
|
|
66
|
+
hr { border: none; border-top: 1px solid #eee; margin: 2rem 0; }
|
|
67
|
+
ul, ol { padding-left: 1.5rem; }
|
|
68
|
+
li { margin: 0.25rem 0; }
|
|
69
|
+
.task-list-item { list-style: none; margin-left: -1.5rem; }
|
|
70
|
+
.task-list-item input { margin-right: 0.5rem; }
|
|
71
|
+
@media print {
|
|
72
|
+
body { max-width: 100%; margin: 0; padding: 1rem; }
|
|
73
|
+
a { color: inherit; text-decoration: underline; }
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
${body}
|
|
79
|
+
<footer style="margin-top:3rem;padding-top:1rem;border-top:1px solid #eee;font-size:0.75rem;color:#999;">
|
|
80
|
+
Exported from MindOS
|
|
81
|
+
</footer>
|
|
82
|
+
</body>
|
|
83
|
+
</html>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function escapeHtml(str: string): string {
|
|
87
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Collect all exportable files in a directory tree */
|
|
91
|
+
export function collectExportFiles(mindRoot: string, dirPath: string): { relativePath: string; content: string }[] {
|
|
92
|
+
const fullDir = path.join(mindRoot, dirPath);
|
|
93
|
+
if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) return [];
|
|
94
|
+
|
|
95
|
+
const results: { relativePath: string; content: string }[] = [];
|
|
96
|
+
const SKIP = new Set(['INSTRUCTION.md', '.DS_Store']);
|
|
97
|
+
|
|
98
|
+
function walk(dir: string, prefix: string) {
|
|
99
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (entry.name.startsWith('.') || SKIP.has(entry.name)) continue;
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
walk(fullPath, relPath);
|
|
106
|
+
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.csv')) {
|
|
107
|
+
try {
|
|
108
|
+
results.push({ relativePath: relPath, content: fs.readFileSync(fullPath, 'utf-8') });
|
|
109
|
+
} catch { /* skip unreadable */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
walk(fullDir, '');
|
|
115
|
+
return results;
|
|
116
|
+
}
|