@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/app/api/ask/route.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { getFileContent, getMindRoot } from '@/lib/fs';
|
|
18
|
-
import { getModelConfig } from '@/lib/agent/model';
|
|
18
|
+
import { getModelConfig, hasImages } from '@/lib/agent/model';
|
|
19
19
|
import { getRequestScopedTools, getOrganizeTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
20
20
|
import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
21
21
|
import { toAgentMessages } from '@/lib/agent/to-agent-messages';
|
|
@@ -25,8 +25,16 @@ import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
|
|
|
25
25
|
import { metrics } from '@/lib/metrics';
|
|
26
26
|
import { assertNotProtected } from '@/lib/core';
|
|
27
27
|
import { scanExtensionPaths } from '@/lib/pi-integration/extensions';
|
|
28
|
+
import { createSession, promptStream, closeSession } from '@/lib/acp/session';
|
|
29
|
+
import type { AcpSessionUpdate } from '@/lib/acp/types';
|
|
28
30
|
import type { Message as FrontendMessage } from '@/lib/types';
|
|
29
31
|
|
|
32
|
+
/** Safe JSON parse — returns {} on invalid input */
|
|
33
|
+
function safeParseJson(raw: string | undefined): Record<string, unknown> {
|
|
34
|
+
if (!raw) return {};
|
|
35
|
+
try { return JSON.parse(raw); } catch { return {}; }
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
// ---------------------------------------------------------------------------
|
|
31
39
|
// MindOS SSE format — 6 event types (front-back contract)
|
|
32
40
|
// ---------------------------------------------------------------------------
|
|
@@ -254,6 +262,8 @@ export async function POST(req: NextRequest) {
|
|
|
254
262
|
maxSteps?: number;
|
|
255
263
|
/** 'organize' = lean prompt for file import organize; default = full prompt */
|
|
256
264
|
mode?: 'organize' | 'default';
|
|
265
|
+
/** ACP agent selection: if present, route to ACP instead of MindOS */
|
|
266
|
+
selectedAcpAgent?: { id: string; name: string } | null;
|
|
257
267
|
};
|
|
258
268
|
try {
|
|
259
269
|
body = await req.json();
|
|
@@ -261,7 +271,7 @@ export async function POST(req: NextRequest) {
|
|
|
261
271
|
return apiError(ErrorCodes.INVALID_REQUEST, 'Invalid JSON body', 400);
|
|
262
272
|
}
|
|
263
273
|
|
|
264
|
-
const { messages, currentFile, attachedFiles, uploadedFiles } = body;
|
|
274
|
+
const { messages, currentFile, attachedFiles, uploadedFiles, selectedAcpAgent } = body;
|
|
265
275
|
const isOrganizeMode = body.mode === 'organize';
|
|
266
276
|
|
|
267
277
|
// Read agent config from settings
|
|
@@ -433,15 +443,18 @@ export async function POST(req: NextRequest) {
|
|
|
433
443
|
}
|
|
434
444
|
|
|
435
445
|
try {
|
|
436
|
-
const { model, modelName, apiKey, provider } = getModelConfig();
|
|
446
|
+
const { model, modelName, apiKey, provider } = getModelConfig({ hasImages: hasImages(messages) });
|
|
437
447
|
|
|
438
448
|
// Convert frontend messages to AgentMessage[]
|
|
439
449
|
const agentMessages = toAgentMessages(messages);
|
|
440
450
|
|
|
441
451
|
// Extract the last user message for agent.prompt()
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
452
|
+
const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
453
|
+
const lastUserContent = lastMsg?.role === 'user' ? lastMsg.content : '';
|
|
454
|
+
// Extract images for prompt options (pi-ai ImageContent format, skip stripped)
|
|
455
|
+
const lastUserImages = lastMsg?.role === 'user' && lastMsg.images?.length
|
|
456
|
+
? lastMsg.images.filter((img: any) => img.data).map((img: any) => ({ type: 'image' as const, data: img.data, mimeType: img.mimeType }))
|
|
457
|
+
: undefined;
|
|
445
458
|
|
|
446
459
|
// History = all messages except the last user message (agent.prompt adds it)
|
|
447
460
|
const historyMessages = agentMessages.slice(0, -1);
|
|
@@ -507,15 +520,20 @@ export async function POST(req: NextRequest) {
|
|
|
507
520
|
const requestStartTime = Date.now();
|
|
508
521
|
const stream = new ReadableStream({
|
|
509
522
|
start(controller) {
|
|
523
|
+
let streamClosed = false;
|
|
510
524
|
function send(event: MindOSSSEvent) {
|
|
525
|
+
if (streamClosed) return;
|
|
511
526
|
try {
|
|
512
527
|
controller.enqueue(encoder.encode(`data:${JSON.stringify(event)}\n\n`));
|
|
513
|
-
} catch
|
|
514
|
-
|
|
515
|
-
console.error('[ask] SSE send failed (serialization):', (err as Error).message, 'event type:', (event as { type?: string }).type);
|
|
516
|
-
}
|
|
528
|
+
} catch {
|
|
529
|
+
streamClosed = true;
|
|
517
530
|
}
|
|
518
531
|
}
|
|
532
|
+
function safeClose() {
|
|
533
|
+
if (streamClosed) return;
|
|
534
|
+
streamClosed = true;
|
|
535
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
536
|
+
}
|
|
519
537
|
|
|
520
538
|
let hasContent = false;
|
|
521
539
|
let lastModelError = '';
|
|
@@ -603,19 +621,109 @@ export async function POST(req: NextRequest) {
|
|
|
603
621
|
}
|
|
604
622
|
});
|
|
605
623
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
624
|
+
// ── Route to ACP agent if selected, otherwise use MindOS agent ──
|
|
625
|
+
const runAgent = async () => {
|
|
626
|
+
if (selectedAcpAgent) {
|
|
627
|
+
// Route to ACP agent with real-time streaming
|
|
628
|
+
let acpSessionId: string | undefined;
|
|
629
|
+
try {
|
|
630
|
+
const acpSession = await createSession(selectedAcpAgent.id, {
|
|
631
|
+
cwd: getMindRoot(),
|
|
632
|
+
});
|
|
633
|
+
acpSessionId = acpSession.id;
|
|
634
|
+
|
|
635
|
+
await promptStream(acpSessionId, lastUserContent, (update: AcpSessionUpdate) => {
|
|
636
|
+
switch (update.type) {
|
|
637
|
+
// Text chunks → standard text_delta
|
|
638
|
+
case 'agent_message_chunk':
|
|
639
|
+
case 'text':
|
|
640
|
+
if (update.text) {
|
|
641
|
+
hasContent = true;
|
|
642
|
+
send({ type: 'text_delta', delta: update.text });
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
// Agent thinking → thinking_delta (reuses existing Anthropic thinking UI)
|
|
647
|
+
case 'agent_thought_chunk':
|
|
648
|
+
if (update.text) {
|
|
649
|
+
hasContent = true;
|
|
650
|
+
send({ type: 'thinking_delta', delta: update.text });
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
|
|
654
|
+
// Tool calls → tool_start (reuses existing tool call UI)
|
|
655
|
+
case 'tool_call':
|
|
656
|
+
if (update.toolCall) {
|
|
657
|
+
hasContent = true;
|
|
658
|
+
send({
|
|
659
|
+
type: 'tool_start',
|
|
660
|
+
toolCallId: update.toolCall.toolCallId,
|
|
661
|
+
toolName: update.toolCall.title ?? update.toolCall.kind ?? 'tool',
|
|
662
|
+
args: safeParseJson(update.toolCall.rawInput),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
|
|
667
|
+
// Tool call updates → tool_end when completed/failed
|
|
668
|
+
case 'tool_call_update':
|
|
669
|
+
if (update.toolCall && (update.toolCall.status === 'completed' || update.toolCall.status === 'failed')) {
|
|
670
|
+
send({
|
|
671
|
+
type: 'tool_end',
|
|
672
|
+
toolCallId: update.toolCall.toolCallId,
|
|
673
|
+
output: update.toolCall.rawOutput ?? '',
|
|
674
|
+
isError: update.toolCall.status === 'failed',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
|
|
679
|
+
// Plan → emit as text with structured format
|
|
680
|
+
case 'plan':
|
|
681
|
+
if (update.plan?.entries) {
|
|
682
|
+
const planText = update.plan.entries
|
|
683
|
+
.map(e => {
|
|
684
|
+
const icon = e.status === 'completed' ? '\u2705' : e.status === 'in_progress' ? '\u26a1' : '\u23f3';
|
|
685
|
+
return `${icon} ${e.content}`;
|
|
686
|
+
})
|
|
687
|
+
.join('\n');
|
|
688
|
+
send({ type: 'text_delta', delta: `\n\n${planText}\n\n` });
|
|
689
|
+
}
|
|
690
|
+
break;
|
|
691
|
+
|
|
692
|
+
// Error → stream error
|
|
693
|
+
case 'error':
|
|
694
|
+
send({ type: 'error', message: update.error ?? 'ACP agent error' });
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
send({ type: 'done' });
|
|
700
|
+
} catch (acpErr) {
|
|
701
|
+
const errMsg = acpErr instanceof Error ? acpErr.message : String(acpErr);
|
|
702
|
+
send({ type: 'error', message: `ACP Agent Error: ${errMsg}` });
|
|
703
|
+
} finally {
|
|
704
|
+
if (acpSessionId) {
|
|
705
|
+
await closeSession(acpSessionId).catch(() => {});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
safeClose();
|
|
610
709
|
} else {
|
|
611
|
-
|
|
710
|
+
// Route to MindOS agent (existing logic)
|
|
711
|
+
await session.prompt(lastUserContent, lastUserImages ? { images: lastUserImages } : undefined);
|
|
712
|
+
metrics.recordRequest(Date.now() - requestStartTime);
|
|
713
|
+
if (!hasContent && lastModelError) {
|
|
714
|
+
send({ type: 'error', message: lastModelError });
|
|
715
|
+
} else {
|
|
716
|
+
send({ type: 'done' });
|
|
717
|
+
}
|
|
718
|
+
safeClose();
|
|
612
719
|
}
|
|
613
|
-
|
|
614
|
-
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
runAgent().catch((err) => {
|
|
615
723
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
616
724
|
metrics.recordError();
|
|
617
725
|
send({ type: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
618
|
-
|
|
726
|
+
safeClose();
|
|
619
727
|
});
|
|
620
728
|
},
|
|
621
729
|
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import archiver from 'archiver';
|
|
4
|
+
import { Readable, PassThrough } from 'stream';
|
|
5
|
+
import { getMindRoot } from '@/lib/fs';
|
|
6
|
+
import { readFile } from '@/lib/core/fs-ops';
|
|
7
|
+
import { markdownToHTML, collectExportFiles } from '@/lib/core/export';
|
|
8
|
+
|
|
9
|
+
export async function GET(req: NextRequest) {
|
|
10
|
+
const { searchParams } = req.nextUrl;
|
|
11
|
+
const filePath = searchParams.get('path');
|
|
12
|
+
const format = searchParams.get('format') ?? 'md';
|
|
13
|
+
|
|
14
|
+
if (!filePath) {
|
|
15
|
+
return NextResponse.json({ error: 'Missing path parameter' }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Path traversal defense-in-depth
|
|
19
|
+
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
20
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mindRoot = getMindRoot();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// ── Single file export ──
|
|
27
|
+
if (format === 'md') {
|
|
28
|
+
const content = readFile(mindRoot, filePath);
|
|
29
|
+
const fileName = path.basename(filePath);
|
|
30
|
+
return new NextResponse(content, {
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
33
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (format === 'html') {
|
|
39
|
+
const content = readFile(mindRoot, filePath);
|
|
40
|
+
const title = path.basename(filePath, '.md');
|
|
41
|
+
const html = await markdownToHTML(content, title, filePath);
|
|
42
|
+
const fileName = path.basename(filePath, '.md') + '.html';
|
|
43
|
+
return new NextResponse(html, {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
46
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Directory/Space ZIP export ──
|
|
52
|
+
if (format === 'zip' || format === 'zip-html') {
|
|
53
|
+
const files = collectExportFiles(mindRoot, filePath);
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
return NextResponse.json({ error: 'No exportable files found' }, { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const spaceName = path.basename(filePath);
|
|
59
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
60
|
+
const zipName = `${spaceName}-${date}.zip`;
|
|
61
|
+
|
|
62
|
+
// Create archive
|
|
63
|
+
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
64
|
+
const passThrough = new PassThrough();
|
|
65
|
+
archive.pipe(passThrough);
|
|
66
|
+
|
|
67
|
+
if (format === 'zip-html') {
|
|
68
|
+
// Convert each MD file to HTML
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
if (file.relativePath.endsWith('.md')) {
|
|
71
|
+
const title = path.basename(file.relativePath, '.md');
|
|
72
|
+
const html = await markdownToHTML(file.content, title, file.relativePath);
|
|
73
|
+
const htmlPath = file.relativePath.replace(/\.md$/, '.html');
|
|
74
|
+
archive.append(html, { name: htmlPath });
|
|
75
|
+
} else {
|
|
76
|
+
archive.append(file.content, { name: file.relativePath });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
archive.append(file.content, { name: file.relativePath });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pipe archive errors to the passthrough stream
|
|
86
|
+
archive.on('error', (err) => passThrough.destroy(err));
|
|
87
|
+
void archive.finalize();
|
|
88
|
+
|
|
89
|
+
// Convert Node stream to Web ReadableStream
|
|
90
|
+
const readable = Readable.toWeb(passThrough) as ReadableStream;
|
|
91
|
+
|
|
92
|
+
return new NextResponse(readable, {
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/zip',
|
|
95
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(zipName)}"`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return NextResponse.json({ error: `Unsupported format: ${format}` }, { status: 400 });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : 'Export failed';
|
|
103
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import YAML from 'js-yaml';
|
|
5
|
+
import { getMindRoot } from '@/lib/fs';
|
|
6
|
+
|
|
7
|
+
const WORKFLOWS_DIR = '.mindos/workflows';
|
|
8
|
+
|
|
9
|
+
function getWorkflowsDir(): string {
|
|
10
|
+
return path.join(getMindRoot(), WORKFLOWS_DIR);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WorkflowListItem {
|
|
14
|
+
path: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
stepCount: number;
|
|
19
|
+
mtime: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function listWorkflows(): WorkflowListItem[] {
|
|
24
|
+
const dir = getWorkflowsDir();
|
|
25
|
+
if (!fs.existsSync(dir)) return [];
|
|
26
|
+
|
|
27
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
const items: WorkflowListItem[] = [];
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isFile()) continue;
|
|
32
|
+
if (!/\.flow\.(yaml|yml)$/i.test(entry.name)) continue;
|
|
33
|
+
|
|
34
|
+
const fullPath = path.join(dir, entry.name);
|
|
35
|
+
const relativePath = path.join(WORKFLOWS_DIR, entry.name);
|
|
36
|
+
const stat = fs.statSync(fullPath);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
40
|
+
const parsed = YAML.load(content, { schema: YAML.JSON_SCHEMA }) as Record<string, unknown> | null;
|
|
41
|
+
|
|
42
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
43
|
+
items.push({
|
|
44
|
+
path: relativePath,
|
|
45
|
+
fileName: entry.name,
|
|
46
|
+
title: entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
|
|
47
|
+
stepCount: 0,
|
|
48
|
+
mtime: stat.mtimeMs,
|
|
49
|
+
error: 'Invalid YAML',
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const steps = Array.isArray(parsed.steps) ? parsed.steps : [];
|
|
55
|
+
items.push({
|
|
56
|
+
path: relativePath,
|
|
57
|
+
fileName: entry.name,
|
|
58
|
+
title: typeof parsed.title === 'string' ? parsed.title : entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
|
|
59
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
60
|
+
stepCount: steps.length,
|
|
61
|
+
mtime: stat.mtimeMs,
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
items.push({
|
|
65
|
+
path: relativePath,
|
|
66
|
+
fileName: entry.name,
|
|
67
|
+
title: entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
|
|
68
|
+
stepCount: 0,
|
|
69
|
+
mtime: stat.mtimeMs,
|
|
70
|
+
error: err instanceof Error ? err.message : 'Parse error',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sort by mtime descending (most recent first)
|
|
76
|
+
items.sort((a, b) => b.mtime - a.mtime);
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const BLANK_TEMPLATE = `title: {TITLE}
|
|
81
|
+
description: ""
|
|
82
|
+
|
|
83
|
+
steps:
|
|
84
|
+
- id: step-1
|
|
85
|
+
name: Step 1
|
|
86
|
+
prompt: |
|
|
87
|
+
Describe what this step should do.
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export async function GET() {
|
|
91
|
+
try {
|
|
92
|
+
const workflows = listWorkflows();
|
|
93
|
+
return NextResponse.json({ workflows });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: err instanceof Error ? err.message : 'Failed to list workflows' },
|
|
97
|
+
{ status: 500 },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function POST(req: Request) {
|
|
103
|
+
try {
|
|
104
|
+
const body = await req.json();
|
|
105
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
106
|
+
if (!name) {
|
|
107
|
+
return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Sanitize filename
|
|
111
|
+
const safeName = name.replace(/[/\\:*?"<>|]/g, '-');
|
|
112
|
+
const fileName = `${safeName}.flow.yaml`;
|
|
113
|
+
const dir = getWorkflowsDir();
|
|
114
|
+
|
|
115
|
+
// Create Workflows dir if needed
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const fullPath = path.join(dir, fileName);
|
|
121
|
+
if (fs.existsSync(fullPath)) {
|
|
122
|
+
return NextResponse.json({ error: 'Workflow already exists' }, { status: 409 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Generate content from template or blank
|
|
126
|
+
const template = typeof body.template === 'string' ? body.template : 'blank';
|
|
127
|
+
let content: string;
|
|
128
|
+
|
|
129
|
+
if (template !== 'blank') {
|
|
130
|
+
// Try to read template from templates dir
|
|
131
|
+
const templateDir = path.join(getMindRoot(), WORKFLOWS_DIR);
|
|
132
|
+
const templateFile = fs.readdirSync(templateDir).find(f =>
|
|
133
|
+
f.toLowerCase().includes(template.toLowerCase()) && /\.workflow\.(yaml|yml)$/i.test(f)
|
|
134
|
+
);
|
|
135
|
+
if (templateFile) {
|
|
136
|
+
content = fs.readFileSync(path.join(templateDir, templateFile), 'utf-8');
|
|
137
|
+
// Replace title with user's name
|
|
138
|
+
content = content.replace(/^title:.*$/m, `title: ${name}`);
|
|
139
|
+
} else {
|
|
140
|
+
content = BLANK_TEMPLATE.replace('{TITLE}', name);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
content = BLANK_TEMPLATE.replace('{TITLE}', name);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
147
|
+
|
|
148
|
+
const relativePath = path.join(WORKFLOWS_DIR, fileName);
|
|
149
|
+
return NextResponse.json({ path: relativePath });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return NextResponse.json(
|
|
152
|
+
{ error: err instanceof Error ? err.message : 'Failed to create workflow' },
|
|
153
|
+
{ status: 500 },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/app/app/globals.css
CHANGED
|
@@ -166,10 +166,10 @@ body {
|
|
|
166
166
|
font-family: var(--prose-font-override, 'Lora', Georgia, serif);
|
|
167
167
|
color: var(--prose-body);
|
|
168
168
|
line-height: 1.85;
|
|
169
|
-
font-size: 0.95rem;
|
|
169
|
+
font-size: var(--prose-font-size-override, 0.95rem);
|
|
170
170
|
}
|
|
171
171
|
@media (min-width: 640px) {
|
|
172
|
-
.prose { font-size: 1rem; }
|
|
172
|
+
.prose { font-size: var(--prose-font-size-override, 1rem); }
|
|
173
173
|
}
|
|
174
174
|
.prose h1, .prose h2, .prose h3, .prose h4 {
|
|
175
175
|
font-family: 'IBM Plex Sans', sans-serif;
|
package/app/app/page.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { redirect } from 'next/navigation';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import { readSettings } from '@/lib/settings';
|
|
3
|
-
import { getRecentlyModified, getFileContent, getFileTree } from '@/lib/fs';
|
|
5
|
+
import { getRecentlyModified, getFileContent, getFileTree, getMindRoot } from '@/lib/fs';
|
|
4
6
|
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
5
7
|
import HomeContent from '@/components/HomeContent';
|
|
6
8
|
import type { FileNode } from '@/lib/core/types';
|
|
@@ -52,8 +54,11 @@ function getTopLevelDirs(): SpaceInfo[] {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function getExistingFiles(paths: string[]): string[] {
|
|
57
|
+
const root = getMindRoot();
|
|
55
58
|
return paths.filter(p => {
|
|
56
|
-
try {
|
|
59
|
+
try {
|
|
60
|
+
return fs.existsSync(path.join(root, p));
|
|
61
|
+
} catch { return false; }
|
|
57
62
|
});
|
|
58
63
|
}
|
|
59
64
|
|