@geminilight/mindos 0.5.70 → 0.6.1
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 +124 -92
- package/app/app/api/mcp/agents/route.ts +53 -2
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/skills/route.ts +10 -114
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +33 -2
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/SidebarLayout.tsx +13 -0
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/agents/AgentDetailContent.tsx +32 -17
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsOverviewSection.tsx +1 -14
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +137 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useMention.ts +14 -6
- package/app/hooks/useSlashCommand.ts +114 -0
- package/app/lib/actions.ts +79 -2
- package/app/lib/agent/index.ts +1 -1
- package/app/lib/agent/prompt.ts +2 -0
- package/app/lib/agent/tools.ts +106 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/index.ts +1 -1
- package/app/lib/i18n-en.ts +51 -46
- package/app/lib/i18n-zh.ts +50 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +68 -0
- package/app/lib/pi-integration/mcporter.ts +219 -0
- package/app/lib/pi-integration/session-store.ts +62 -0
- package/app/lib/pi-integration/skills.ts +116 -0
- package/app/lib/settings.ts +1 -1
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -0
- package/mcp/src/index.ts +29 -0
- package/package.json +1 -1
package/app/app/api/ask/route.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
|
-
import
|
|
2
|
+
import type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core';
|
|
3
|
+
import {
|
|
4
|
+
type AgentSessionEvent as AgentEvent,
|
|
5
|
+
AuthStorage,
|
|
6
|
+
convertToLlm,
|
|
7
|
+
createAgentSession,
|
|
8
|
+
DefaultResourceLoader,
|
|
9
|
+
ModelRegistry,
|
|
10
|
+
type ToolDefinition,
|
|
11
|
+
SessionManager,
|
|
12
|
+
SettingsManager,
|
|
13
|
+
} from '@mariozechner/pi-coding-agent';
|
|
3
14
|
import { NextRequest, NextResponse } from 'next/server';
|
|
4
15
|
import fs from 'fs';
|
|
5
16
|
import path from 'path';
|
|
6
17
|
import { getFileContent, getMindRoot } from '@/lib/fs';
|
|
7
18
|
import { getModelConfig } from '@/lib/agent/model';
|
|
8
|
-
import {
|
|
19
|
+
import { getRequestScopedTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
9
20
|
import { AGENT_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
10
21
|
import { toAgentMessages } from '@/lib/agent/to-agent-messages';
|
|
11
|
-
import {
|
|
12
|
-
estimateTokens, estimateStringTokens, getContextLimit,
|
|
13
|
-
createTransformContext,
|
|
14
|
-
} from '@/lib/agent/context';
|
|
15
22
|
import { logAgentOp } from '@/lib/agent/log';
|
|
16
23
|
import { readSettings } from '@/lib/settings';
|
|
17
24
|
import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
|
|
18
25
|
import { metrics } from '@/lib/metrics';
|
|
19
26
|
import { assertNotProtected } from '@/lib/core';
|
|
27
|
+
import { scanExtensionPaths } from '@/lib/pi-integration/extensions';
|
|
20
28
|
import type { Message as FrontendMessage } from '@/lib/types';
|
|
21
29
|
|
|
22
30
|
// ---------------------------------------------------------------------------
|
|
@@ -147,6 +155,64 @@ function dirnameOf(filePath?: string): string | null {
|
|
|
147
155
|
return normalized.slice(0, idx);
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
function textToolResult(text: string): AgentToolResult<Record<string, never>> {
|
|
159
|
+
return { content: [{ type: 'text', text }], details: {} };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getProtectedPaths(toolName: string, args: Record<string, unknown>): string[] {
|
|
163
|
+
const pathsToCheck: string[] = [];
|
|
164
|
+
if (toolName === 'batch_create_files' && Array.isArray((args as any).files)) {
|
|
165
|
+
(args as any).files.forEach((f: any) => { if (f.path) pathsToCheck.push(f.path); });
|
|
166
|
+
} else {
|
|
167
|
+
const singlePath = (args as any).path ?? (args as any).from_path;
|
|
168
|
+
if (typeof singlePath === 'string') pathsToCheck.push(singlePath);
|
|
169
|
+
}
|
|
170
|
+
return pathsToCheck;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toPiCustomToolDefinitions(tools: AgentTool<any>[]): ToolDefinition[] {
|
|
174
|
+
return tools.map((tool) => ({
|
|
175
|
+
name: tool.name,
|
|
176
|
+
label: tool.label,
|
|
177
|
+
description: tool.description,
|
|
178
|
+
parameters: tool.parameters as any,
|
|
179
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
180
|
+
const args = (params ?? {}) as Record<string, unknown>;
|
|
181
|
+
|
|
182
|
+
if (WRITE_TOOLS.has(tool.name)) {
|
|
183
|
+
for (const filePath of getProtectedPaths(tool.name, args)) {
|
|
184
|
+
try {
|
|
185
|
+
assertNotProtected(filePath, 'modified by AI agent');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
188
|
+
return textToolResult(`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.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = await tool.execute(toolCallId, params, signal, onUpdate as any);
|
|
194
|
+
const outputText = result?.content
|
|
195
|
+
?.filter((p: any) => p.type === 'text')
|
|
196
|
+
.map((p: any) => p.text)
|
|
197
|
+
.join('') ?? '';
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
logAgentOp({
|
|
201
|
+
ts: new Date().toISOString(),
|
|
202
|
+
tool: tool.name,
|
|
203
|
+
params: args,
|
|
204
|
+
result: outputText.startsWith('Error:') ? 'error' : 'ok',
|
|
205
|
+
message: outputText.slice(0, 200),
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
// logging must never kill the stream
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
},
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
|
|
150
216
|
// ---------------------------------------------------------------------------
|
|
151
217
|
// POST /api/ask
|
|
152
218
|
// ---------------------------------------------------------------------------
|
|
@@ -328,91 +394,61 @@ export async function POST(req: NextRequest) {
|
|
|
328
394
|
const historyMessages = agentMessages.slice(0, -1);
|
|
329
395
|
|
|
330
396
|
// Capture API key for this request — safe since each POST creates a new Agent instance.
|
|
331
|
-
// Even though JS closures are lexically scoped, being explicit guards against future refactors.
|
|
332
397
|
const requestApiKey = apiKey;
|
|
398
|
+
const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
399
|
+
const requestTools = await getRequestScopedTools();
|
|
400
|
+
const customTools = toPiCustomToolDefinitions(requestTools);
|
|
401
|
+
|
|
402
|
+
const authStorage = AuthStorage.create();
|
|
403
|
+
authStorage.setRuntimeApiKey(provider, requestApiKey);
|
|
404
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
405
|
+
const settingsManager = SettingsManager.inMemory({
|
|
406
|
+
enableSkillCommands: true,
|
|
407
|
+
...(enableThinking && provider === 'anthropic' ? { thinkingBudgets: { medium: thinkingBudget } } : {}),
|
|
408
|
+
...(contextStrategy === 'off' ? { compaction: { enabled: false } } : {}),
|
|
409
|
+
});
|
|
333
410
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// Write-protection: block writes to protected files gracefully
|
|
361
|
-
beforeToolCall: async (context: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> => {
|
|
362
|
-
const { toolCall, args } = context;
|
|
363
|
-
// toolCall is an object with type "toolCall" and contains the tool name and ID
|
|
364
|
-
const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
|
|
365
|
-
if (toolName && WRITE_TOOLS.has(toolName)) {
|
|
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
|
-
}
|
|
411
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
412
|
+
cwd: projectRoot,
|
|
413
|
+
settingsManager,
|
|
414
|
+
systemPromptOverride: () => systemPrompt,
|
|
415
|
+
appendSystemPromptOverride: () => [],
|
|
416
|
+
additionalSkillPaths: [
|
|
417
|
+
path.join(projectRoot, 'app', 'data', 'skills'),
|
|
418
|
+
path.join(projectRoot, 'skills'),
|
|
419
|
+
path.join(getMindRoot(), '.skills'),
|
|
420
|
+
],
|
|
421
|
+
additionalExtensionPaths: scanExtensionPaths(),
|
|
422
|
+
});
|
|
423
|
+
await resourceLoader.reload();
|
|
424
|
+
|
|
425
|
+
const { session } = await createAgentSession({
|
|
426
|
+
cwd: projectRoot,
|
|
427
|
+
model,
|
|
428
|
+
thinkingLevel: (enableThinking && provider === 'anthropic') ? 'medium' : 'off',
|
|
429
|
+
authStorage,
|
|
430
|
+
modelRegistry,
|
|
431
|
+
resourceLoader,
|
|
432
|
+
sessionManager: SessionManager.inMemory(),
|
|
433
|
+
settingsManager,
|
|
434
|
+
tools: [],
|
|
435
|
+
customTools,
|
|
436
|
+
});
|
|
374
437
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
block: true,
|
|
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.`,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
}
|
|
438
|
+
const llmHistoryMessages = convertToLlm(historyMessages);
|
|
439
|
+
await session.newSession({
|
|
440
|
+
setup: async (sessionManager) => {
|
|
441
|
+
for (const message of llmHistoryMessages) {
|
|
442
|
+
sessionManager.appendMessage(message);
|
|
386
443
|
}
|
|
387
|
-
return undefined;
|
|
388
|
-
},
|
|
389
|
-
|
|
390
|
-
// Logging: record all tool executions
|
|
391
|
-
afterToolCall: async (context: AfterToolCallContext): Promise<AfterToolCallResult | undefined> => {
|
|
392
|
-
const ts = new Date().toISOString();
|
|
393
|
-
const { toolCall, args, result, isError } = context;
|
|
394
|
-
const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
|
|
395
|
-
const outputText = result?.content
|
|
396
|
-
?.filter((p: any) => p.type === 'text')
|
|
397
|
-
.map((p: any) => p.text)
|
|
398
|
-
.join('') ?? '';
|
|
399
|
-
try {
|
|
400
|
-
logAgentOp({
|
|
401
|
-
ts,
|
|
402
|
-
tool: toolName ?? 'unknown',
|
|
403
|
-
params: args as Record<string, unknown>,
|
|
404
|
-
result: isError ? 'error' : 'ok',
|
|
405
|
-
message: outputText.slice(0, 200),
|
|
406
|
-
});
|
|
407
|
-
} catch { /* logging must never kill the stream */ }
|
|
408
|
-
return undefined;
|
|
409
444
|
},
|
|
410
|
-
|
|
411
|
-
...(enableThinking && provider === 'anthropic' ? {
|
|
412
|
-
thinkingBudgets: { medium: thinkingBudget },
|
|
413
|
-
} : {}),
|
|
414
445
|
});
|
|
415
446
|
|
|
447
|
+
// ── Loop detection state ──
|
|
448
|
+
const stepHistory: Array<{ tool: string; input: string }> = [];
|
|
449
|
+
let stepCount = 0;
|
|
450
|
+
let loopCooldown = 0;
|
|
451
|
+
|
|
416
452
|
// ── SSE Stream ──
|
|
417
453
|
const encoder = new TextEncoder();
|
|
418
454
|
const requestStartTime = Date.now();
|
|
@@ -424,7 +460,7 @@ export async function POST(req: NextRequest) {
|
|
|
424
460
|
} catch { /* controller may be closed */ }
|
|
425
461
|
}
|
|
426
462
|
|
|
427
|
-
|
|
463
|
+
session.subscribe((event: AgentEvent) => {
|
|
428
464
|
if (isTextDeltaEvent(event)) {
|
|
429
465
|
send({ type: 'text_delta', delta: getTextDelta(event) });
|
|
430
466
|
} else if (isThinkingDeltaEvent(event)) {
|
|
@@ -476,24 +512,20 @@ export async function POST(req: NextRequest) {
|
|
|
476
512
|
if (lastN.every(s => s.tool === lastN[0].tool && s.input === lastN[0].input)) {
|
|
477
513
|
loopCooldown = 3;
|
|
478
514
|
// TODO (metrics): Track loop detection rate — metrics.increment('agent.loop_detected', { model: modelName })
|
|
479
|
-
|
|
480
|
-
role: 'user',
|
|
481
|
-
content: '[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.',
|
|
482
|
-
timestamp: Date.now(),
|
|
483
|
-
} as any);
|
|
515
|
+
void session.steer('[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.');
|
|
484
516
|
}
|
|
485
517
|
}
|
|
486
518
|
|
|
487
519
|
// Step limit enforcement
|
|
488
520
|
if (stepCount >= stepLimit) {
|
|
489
|
-
|
|
521
|
+
void session.abort();
|
|
490
522
|
}
|
|
491
523
|
|
|
492
524
|
console.log(`[ask] Step ${stepCount}/${stepLimit}`);
|
|
493
525
|
}
|
|
494
526
|
});
|
|
495
527
|
|
|
496
|
-
|
|
528
|
+
session.prompt(lastUserContent).then(() => {
|
|
497
529
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
498
530
|
send({ type: 'done' });
|
|
499
531
|
controller.close();
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
2
5
|
import { NextResponse } from 'next/server';
|
|
3
6
|
import {
|
|
4
7
|
MCP_AGENTS,
|
|
@@ -9,6 +12,50 @@ import {
|
|
|
9
12
|
detectAgentInstalledSkills,
|
|
10
13
|
resolveSkillWorkspaceProfile,
|
|
11
14
|
} from '@/lib/mcp-agents';
|
|
15
|
+
import { readSettings } from '@/lib/settings';
|
|
16
|
+
import { scanSkillDirs } from '@/lib/pi-integration/skills';
|
|
17
|
+
import { getMindRoot } from '@/lib/fs';
|
|
18
|
+
|
|
19
|
+
function enrichMindOsAgent(agent: Record<string, unknown>) {
|
|
20
|
+
agent.present = true;
|
|
21
|
+
agent.installed = true;
|
|
22
|
+
agent.scope = 'builtin';
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const settings = readSettings();
|
|
26
|
+
const port = Number(process.env.MINDOS_MCP_PORT) || settings.mcpPort || 8781;
|
|
27
|
+
agent.transport = `http :${port}`;
|
|
28
|
+
} catch {
|
|
29
|
+
agent.transport = 'http :8781';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
34
|
+
const skills = scanSkillDirs({ projectRoot, mindRoot: getMindRoot() });
|
|
35
|
+
const enabledSkills = skills.filter(s => s.enabled);
|
|
36
|
+
agent.installedSkillNames = enabledSkills.map(s => s.name);
|
|
37
|
+
agent.installedSkillCount = enabledSkills.length;
|
|
38
|
+
agent.installedSkillSourcePath = path.join(projectRoot, 'skills');
|
|
39
|
+
agent.skillMode = 'universal';
|
|
40
|
+
agent.skillWorkspacePath = path.join(os.homedir(), '.agents', 'skills');
|
|
41
|
+
} catch { /* skill scan unavailable */ }
|
|
42
|
+
|
|
43
|
+
const mcpConfigPath = path.join(os.homedir(), '.mindos', 'mcp.json');
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
47
|
+
const servers = Object.keys(raw.mcpServers ?? {});
|
|
48
|
+
agent.configuredMcpServers = servers;
|
|
49
|
+
agent.configuredMcpServerCount = servers.length;
|
|
50
|
+
agent.configuredMcpSources = servers.length > 0 ? [`local:${mcpConfigPath}`] : [];
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
|
|
54
|
+
agent.runtimeConversationSignal = true;
|
|
55
|
+
agent.runtimeLastActivityAt = new Date().toISOString();
|
|
56
|
+
agent.hiddenRootPath = path.join(os.homedir(), '.mindos');
|
|
57
|
+
agent.hiddenRootPresent = true;
|
|
58
|
+
}
|
|
12
59
|
|
|
13
60
|
export async function GET() {
|
|
14
61
|
try {
|
|
@@ -30,7 +77,6 @@ export async function GET() {
|
|
|
30
77
|
hasProjectScope: !!agent.project,
|
|
31
78
|
hasGlobalScope: !!agent.global,
|
|
32
79
|
preferredTransport: agent.preferredTransport,
|
|
33
|
-
// Snippet generation fields
|
|
34
80
|
format: agent.format ?? 'json',
|
|
35
81
|
configKey: agent.key,
|
|
36
82
|
globalNestedKey: agent.globalNestedKey,
|
|
@@ -53,8 +99,13 @@ export async function GET() {
|
|
|
53
99
|
};
|
|
54
100
|
});
|
|
55
101
|
|
|
56
|
-
|
|
102
|
+
const mindos = agents.find(a => a.key === 'mindos');
|
|
103
|
+
if (mindos) enrichMindOsAgent(mindos as unknown as Record<string, unknown>);
|
|
104
|
+
|
|
105
|
+
// Sort: mindos first, then installed, then detected, then not found
|
|
57
106
|
agents.sort((a, b) => {
|
|
107
|
+
if (a.key === 'mindos') return -1;
|
|
108
|
+
if (b.key === 'mindos') return 1;
|
|
58
109
|
const rank = (x: typeof a) => x.installed ? 0 : x.present ? 1 : 2;
|
|
59
110
|
return rank(a) - rank(b);
|
|
60
111
|
});
|
|
@@ -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 ? 24 : 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,
|
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { readSettings, writeSettings } from '@/lib/settings';
|
|
7
|
+
import { parseSkillMd, readSkillContentByName, scanSkillDirs } from '@/lib/pi-integration/skills';
|
|
7
8
|
|
|
8
9
|
const PROJECT_ROOT = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
9
10
|
|
|
@@ -12,113 +13,15 @@ function getMindRoot(): string {
|
|
|
12
13
|
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
interface SkillInfo {
|
|
16
|
-
name: string;
|
|
17
|
-
description: string;
|
|
18
|
-
path: string;
|
|
19
|
-
source: 'builtin' | 'user';
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
editable: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseSkillMd(content: string): { name: string; description: string } {
|
|
25
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26
|
-
if (!match) return { name: '', description: '' };
|
|
27
|
-
const yaml = match[1];
|
|
28
|
-
const nameMatch = yaml.match(/^name:\s*(.+)/m);
|
|
29
|
-
const descMatch = yaml.match(/^description:\s*>?\s*\n?([\s\S]*?)(?=\n\w|\n---)/m);
|
|
30
|
-
const name = nameMatch ? nameMatch[1].trim() : '';
|
|
31
|
-
let description = '';
|
|
32
|
-
if (descMatch) {
|
|
33
|
-
description = descMatch[1].trim().split('\n').map(l => l.trim()).join(' ').slice(0, 200);
|
|
34
|
-
} else {
|
|
35
|
-
const simpleDesc = yaml.match(/^description:\s*(.+)/m);
|
|
36
|
-
if (simpleDesc) description = simpleDesc[1].trim().slice(0, 200);
|
|
37
|
-
}
|
|
38
|
-
return { name, description };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function scanSkillDirs(disabledSkills: string[]): SkillInfo[] {
|
|
42
|
-
const skills: SkillInfo[] = [];
|
|
43
|
-
const seen = new Set<string>();
|
|
44
|
-
|
|
45
|
-
// 1. app/data/skills/ — builtin
|
|
46
|
-
const builtinDir = path.join(PROJECT_ROOT, 'app', 'data', 'skills');
|
|
47
|
-
if (fs.existsSync(builtinDir)) {
|
|
48
|
-
for (const entry of fs.readdirSync(builtinDir, { withFileTypes: true })) {
|
|
49
|
-
if (!entry.isDirectory()) continue;
|
|
50
|
-
const skillFile = path.join(builtinDir, entry.name, 'SKILL.md');
|
|
51
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
52
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
53
|
-
const { name, description } = parseSkillMd(content);
|
|
54
|
-
const skillName = name || entry.name;
|
|
55
|
-
seen.add(skillName);
|
|
56
|
-
skills.push({
|
|
57
|
-
name: skillName,
|
|
58
|
-
description,
|
|
59
|
-
path: `app/data/skills/${entry.name}/SKILL.md`,
|
|
60
|
-
source: 'builtin',
|
|
61
|
-
enabled: !disabledSkills.includes(skillName),
|
|
62
|
-
editable: false,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 2. skills/ — project root builtin
|
|
68
|
-
const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
69
|
-
if (fs.existsSync(skillsDir)) {
|
|
70
|
-
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
71
|
-
if (!entry.isDirectory()) continue;
|
|
72
|
-
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
73
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
74
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
75
|
-
const { name, description } = parseSkillMd(content);
|
|
76
|
-
const skillName = name || entry.name;
|
|
77
|
-
if (seen.has(skillName)) continue; // already listed from app/data/skills/
|
|
78
|
-
seen.add(skillName);
|
|
79
|
-
skills.push({
|
|
80
|
-
name: skillName,
|
|
81
|
-
description,
|
|
82
|
-
path: `skills/${entry.name}/SKILL.md`,
|
|
83
|
-
source: 'builtin',
|
|
84
|
-
enabled: !disabledSkills.includes(skillName),
|
|
85
|
-
editable: false,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3. {mindRoot}/.skills/ — user custom
|
|
91
|
-
const mindRoot = getMindRoot();
|
|
92
|
-
const userSkillsDir = path.join(mindRoot, '.skills');
|
|
93
|
-
if (fs.existsSync(userSkillsDir)) {
|
|
94
|
-
for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
|
|
95
|
-
if (!entry.isDirectory()) continue;
|
|
96
|
-
const skillFile = path.join(userSkillsDir, entry.name, 'SKILL.md');
|
|
97
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
98
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
99
|
-
const { name, description } = parseSkillMd(content);
|
|
100
|
-
const skillName = name || entry.name;
|
|
101
|
-
if (seen.has(skillName)) continue;
|
|
102
|
-
seen.add(skillName);
|
|
103
|
-
skills.push({
|
|
104
|
-
name: skillName,
|
|
105
|
-
description,
|
|
106
|
-
path: `{mindRoot}/.skills/${entry.name}/SKILL.md`,
|
|
107
|
-
source: 'user',
|
|
108
|
-
enabled: !disabledSkills.includes(skillName),
|
|
109
|
-
editable: true,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return skills;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
16
|
export async function GET() {
|
|
118
17
|
try {
|
|
119
18
|
const settings = readSettings();
|
|
120
19
|
const disabledSkills = settings.disabledSkills ?? [];
|
|
121
|
-
const skills = scanSkillDirs(
|
|
20
|
+
const skills = scanSkillDirs({
|
|
21
|
+
projectRoot: PROJECT_ROOT,
|
|
22
|
+
mindRoot: getMindRoot(),
|
|
23
|
+
disabledSkills,
|
|
24
|
+
});
|
|
122
25
|
return NextResponse.json({ skills });
|
|
123
26
|
} catch (err) {
|
|
124
27
|
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
@@ -205,18 +108,11 @@ export async function POST(req: NextRequest) {
|
|
|
205
108
|
|
|
206
109
|
case 'read': {
|
|
207
110
|
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
path.join(userSkillsDir, name),
|
|
212
|
-
];
|
|
213
|
-
for (const dir of dirs) {
|
|
214
|
-
const file = path.join(dir, 'SKILL.md');
|
|
215
|
-
if (fs.existsSync(file)) {
|
|
216
|
-
return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
|
|
217
|
-
}
|
|
111
|
+
const content = readSkillContentByName(name, { projectRoot: PROJECT_ROOT, mindRoot });
|
|
112
|
+
if (!content) {
|
|
113
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
218
114
|
}
|
|
219
|
-
return NextResponse.json({
|
|
115
|
+
return NextResponse.json({ content });
|
|
220
116
|
}
|
|
221
117
|
|
|
222
118
|
case 'read-native': {
|
|
@@ -162,7 +162,6 @@ export default function ActivityBar({
|
|
|
162
162
|
role="toolbar"
|
|
163
163
|
aria-label="Navigation"
|
|
164
164
|
aria-orientation="vertical"
|
|
165
|
-
data-walkthrough="activity-bar"
|
|
166
165
|
>
|
|
167
166
|
{/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
|
|
168
167
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
@@ -181,14 +180,15 @@ export default function ActivityBar({
|
|
|
181
180
|
{/* ── Middle: Core panel toggles ── */}
|
|
182
181
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
183
182
|
<RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
|
|
184
|
-
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')}
|
|
185
|
-
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
|
|
183
|
+
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
|
|
184
|
+
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} walkthroughId="echo-panel" />
|
|
186
185
|
<RailButton
|
|
187
186
|
icon={<Bot size={18} />}
|
|
188
187
|
label={t.sidebar.agents}
|
|
189
188
|
active={activePanel === 'agents'}
|
|
190
189
|
expanded={expanded}
|
|
191
190
|
onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
|
|
191
|
+
walkthroughId="agents-panel"
|
|
192
192
|
/>
|
|
193
193
|
<RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
|
|
194
194
|
</div>
|
|
@@ -211,7 +211,6 @@ export default function ActivityBar({
|
|
|
211
211
|
shortcut="⌘,"
|
|
212
212
|
expanded={expanded}
|
|
213
213
|
onClick={() => debounced(onSettingsClick)}
|
|
214
|
-
walkthroughId="settings-button"
|
|
215
214
|
badge={hasUpdate ? (
|
|
216
215
|
<span className={`absolute ${expanded ? 'left-[26px] top-1.5' : 'top-1.5 right-1.5'} w-2 h-2 rounded-full bg-error`} />
|
|
217
216
|
) : undefined}
|
|
@@ -93,17 +93,42 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
|
|
|
93
93
|
if (useAi && aiAvailable) {
|
|
94
94
|
const isZh = document.documentElement.lang === 'zh';
|
|
95
95
|
const prompt = isZh
|
|
96
|
-
? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}
|
|
97
|
-
: `Initialize
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}两个文件均已存在模板,用 write_file 覆盖:\n1. 「${createdPath}/README.md」— 写入空间用途、结构概览、使用指南\n2. 「${createdPath}/INSTRUCTION.md」— 写入 AI Agent 在此空间中的行为规则和操作约定\n\n内容简洁实用,直接使用工具写入。`
|
|
97
|
+
: `Initialize the new Mind Space "${trimmed}" at "${createdPath}/". ${description ? `Description: "${description}". ` : ''}Both files already exist with templates — use write_file to overwrite:\n1. "${createdPath}/README.md" — write purpose, structure overview, usage guidelines\n2. "${createdPath}/INSTRUCTION.md" — write rules for AI agents operating in this space\n\nKeep content concise and actionable. Write files directly using tools.`;
|
|
98
|
+
|
|
99
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
100
|
+
detail: { spaceName: trimmed, spacePath: createdPath, description, state: 'working' },
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// /api/ask returns SSE — use raw fetch and consume the stream
|
|
104
|
+
// so the server-side agent runs to completion.
|
|
105
|
+
fetch('/api/ask', {
|
|
100
106
|
method: 'POST',
|
|
101
107
|
headers: { 'Content-Type': 'application/json' },
|
|
102
108
|
body: JSON.stringify({
|
|
103
109
|
messages: [{ role: 'user', content: prompt }],
|
|
104
|
-
|
|
110
|
+
currentFile: createdPath + '/INSTRUCTION.md',
|
|
105
111
|
}),
|
|
106
|
-
}).
|
|
112
|
+
}).then(async (res) => {
|
|
113
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
114
|
+
const reader = res.body.getReader();
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
const { done } = await reader.read();
|
|
118
|
+
if (done) break;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
124
|
+
detail: { spacePath: createdPath, state: 'done' },
|
|
125
|
+
}));
|
|
126
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
127
|
+
}).catch(() => {
|
|
128
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
129
|
+
detail: { spacePath: createdPath, state: 'error' },
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
close();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
|
|
3
|
+
import { useState, useCallback, useRef, useTransition, useEffect, useSyncExternalStore } from 'react';
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import { FileNode } from '@/lib/types';
|
|
6
6
|
import { encodePath } from '@/lib/utils';
|
|
@@ -17,6 +17,33 @@ function notifyFilesChanged() {
|
|
|
17
17
|
|
|
18
18
|
const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
|
|
19
19
|
|
|
20
|
+
const HIDDEN_FILES_KEY = 'show-hidden-files';
|
|
21
|
+
|
|
22
|
+
function subscribeHiddenFiles(cb: () => void) {
|
|
23
|
+
const handler = (e: StorageEvent) => { if (e.key === HIDDEN_FILES_KEY) cb(); };
|
|
24
|
+
const custom = () => cb();
|
|
25
|
+
window.addEventListener('storage', handler);
|
|
26
|
+
window.addEventListener('mindos:hidden-files-changed', custom);
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('storage', handler);
|
|
29
|
+
window.removeEventListener('mindos:hidden-files-changed', custom);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getShowHiddenFiles() {
|
|
34
|
+
if (typeof window === 'undefined') return false;
|
|
35
|
+
return localStorage.getItem(HIDDEN_FILES_KEY) === 'true';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setShowHiddenFiles(value: boolean) {
|
|
39
|
+
localStorage.setItem(HIDDEN_FILES_KEY, String(value));
|
|
40
|
+
window.dispatchEvent(new Event('mindos:hidden-files-changed'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function useShowHiddenFiles() {
|
|
44
|
+
return useSyncExternalStore(subscribeHiddenFiles, getShowHiddenFiles, () => false);
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
interface FileTreeProps {
|
|
21
48
|
nodes: FileNode[];
|
|
22
49
|
depth?: number;
|
|
@@ -618,9 +645,13 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
618
645
|
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace, onImport }: FileTreeProps) {
|
|
619
646
|
const pathname = usePathname();
|
|
620
647
|
const currentPath = getCurrentFilePath(pathname);
|
|
648
|
+
const showHidden = useShowHiddenFiles();
|
|
621
649
|
|
|
622
650
|
const isInsideDir = depth > 0;
|
|
623
|
-
|
|
651
|
+
let visibleNodes = isInsideDir ? filterVisibleNodes(nodes, !!parentIsSpace) : nodes;
|
|
652
|
+
if (!isInsideDir && !showHidden) {
|
|
653
|
+
visibleNodes = visibleNodes.filter(n => !n.name.startsWith('.'));
|
|
654
|
+
}
|
|
624
655
|
|
|
625
656
|
useEffect(() => {
|
|
626
657
|
if (!currentPath || depth !== 0) return;
|