@geminilight/mindos 0.5.69 → 0.6.0
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 +122 -92
- package/app/app/api/file/import/route.ts +197 -0
- 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 +5 -7
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +68 -11
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/ImportModal.tsx +415 -0
- package/app/components/OnboardingView.tsx +9 -0
- package/app/components/Panel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +96 -8
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/TableOfContents.tsx +1 -0
- package/app/components/agents/AgentDetailContent.tsx +69 -45
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsMcpSection.tsx +16 -12
- package/app/components/agents/AgentsOverviewSection.tsx +37 -36
- package/app/components/agents/AgentsPrimitives.tsx +41 -20
- package/app/components/agents/AgentsSkillsSection.tsx +16 -7
- package/app/components/agents/SkillDetailPopover.tsx +11 -11
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +148 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
- package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
- package/app/components/panels/DiscoverPanel.tsx +88 -2
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useFileImport.ts +191 -0
- package/app/hooks/useFileUpload.ts +11 -0
- 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 +252 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/file-convert.ts +97 -0
- package/app/lib/core/index.ts +1 -1
- package/app/lib/core/organize.ts +105 -0
- package/app/lib/i18n-en.ts +102 -46
- package/app/lib/i18n-zh.ts +101 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +45 -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,17 +1,24 @@
|
|
|
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';
|
|
@@ -147,6 +154,64 @@ function dirnameOf(filePath?: string): string | null {
|
|
|
147
154
|
return normalized.slice(0, idx);
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
function textToolResult(text: string): AgentToolResult<Record<string, never>> {
|
|
158
|
+
return { content: [{ type: 'text', text }], details: {} };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getProtectedPaths(toolName: string, args: Record<string, unknown>): string[] {
|
|
162
|
+
const pathsToCheck: string[] = [];
|
|
163
|
+
if (toolName === 'batch_create_files' && Array.isArray((args as any).files)) {
|
|
164
|
+
(args as any).files.forEach((f: any) => { if (f.path) pathsToCheck.push(f.path); });
|
|
165
|
+
} else {
|
|
166
|
+
const singlePath = (args as any).path ?? (args as any).from_path;
|
|
167
|
+
if (typeof singlePath === 'string') pathsToCheck.push(singlePath);
|
|
168
|
+
}
|
|
169
|
+
return pathsToCheck;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toPiCustomToolDefinitions(tools: AgentTool<any>[]): ToolDefinition[] {
|
|
173
|
+
return tools.map((tool) => ({
|
|
174
|
+
name: tool.name,
|
|
175
|
+
label: tool.label,
|
|
176
|
+
description: tool.description,
|
|
177
|
+
parameters: tool.parameters as any,
|
|
178
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
179
|
+
const args = (params ?? {}) as Record<string, unknown>;
|
|
180
|
+
|
|
181
|
+
if (WRITE_TOOLS.has(tool.name)) {
|
|
182
|
+
for (const filePath of getProtectedPaths(tool.name, args)) {
|
|
183
|
+
try {
|
|
184
|
+
assertNotProtected(filePath, 'modified by AI agent');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
187
|
+
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.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await tool.execute(toolCallId, params, signal, onUpdate as any);
|
|
193
|
+
const outputText = result?.content
|
|
194
|
+
?.filter((p: any) => p.type === 'text')
|
|
195
|
+
.map((p: any) => p.text)
|
|
196
|
+
.join('') ?? '';
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
logAgentOp({
|
|
200
|
+
ts: new Date().toISOString(),
|
|
201
|
+
tool: tool.name,
|
|
202
|
+
params: args,
|
|
203
|
+
result: outputText.startsWith('Error:') ? 'error' : 'ok',
|
|
204
|
+
message: outputText.slice(0, 200),
|
|
205
|
+
});
|
|
206
|
+
} catch {
|
|
207
|
+
// logging must never kill the stream
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
},
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
150
215
|
// ---------------------------------------------------------------------------
|
|
151
216
|
// POST /api/ask
|
|
152
217
|
// ---------------------------------------------------------------------------
|
|
@@ -328,91 +393,60 @@ export async function POST(req: NextRequest) {
|
|
|
328
393
|
const historyMessages = agentMessages.slice(0, -1);
|
|
329
394
|
|
|
330
395
|
// 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
396
|
const requestApiKey = apiKey;
|
|
397
|
+
const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
398
|
+
const requestTools = await getRequestScopedTools();
|
|
399
|
+
const customTools = toPiCustomToolDefinitions(requestTools);
|
|
400
|
+
|
|
401
|
+
const authStorage = AuthStorage.create();
|
|
402
|
+
authStorage.setRuntimeApiKey(provider, requestApiKey);
|
|
403
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
404
|
+
const settingsManager = SettingsManager.inMemory({
|
|
405
|
+
enableSkillCommands: true,
|
|
406
|
+
...(enableThinking && provider === 'anthropic' ? { thinkingBudgets: { medium: thinkingBudget } } : {}),
|
|
407
|
+
...(contextStrategy === 'off' ? { compaction: { enabled: false } } : {}),
|
|
408
|
+
});
|
|
333
409
|
|
|
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
|
-
}
|
|
410
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
411
|
+
cwd: projectRoot,
|
|
412
|
+
settingsManager,
|
|
413
|
+
systemPromptOverride: () => systemPrompt,
|
|
414
|
+
appendSystemPromptOverride: () => [],
|
|
415
|
+
additionalSkillPaths: [
|
|
416
|
+
path.join(projectRoot, 'app', 'data', 'skills'),
|
|
417
|
+
path.join(projectRoot, 'skills'),
|
|
418
|
+
path.join(getMindRoot(), '.skills'),
|
|
419
|
+
],
|
|
420
|
+
});
|
|
421
|
+
await resourceLoader.reload();
|
|
422
|
+
|
|
423
|
+
const { session } = await createAgentSession({
|
|
424
|
+
cwd: projectRoot,
|
|
425
|
+
model,
|
|
426
|
+
thinkingLevel: (enableThinking && provider === 'anthropic') ? 'medium' : 'off',
|
|
427
|
+
authStorage,
|
|
428
|
+
modelRegistry,
|
|
429
|
+
resourceLoader,
|
|
430
|
+
sessionManager: SessionManager.inMemory(),
|
|
431
|
+
settingsManager,
|
|
432
|
+
tools: [],
|
|
433
|
+
customTools,
|
|
434
|
+
});
|
|
374
435
|
|
|
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
|
-
}
|
|
436
|
+
const llmHistoryMessages = convertToLlm(historyMessages);
|
|
437
|
+
await session.newSession({
|
|
438
|
+
setup: async (sessionManager) => {
|
|
439
|
+
for (const message of llmHistoryMessages) {
|
|
440
|
+
sessionManager.appendMessage(message);
|
|
386
441
|
}
|
|
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
442
|
},
|
|
410
|
-
|
|
411
|
-
...(enableThinking && provider === 'anthropic' ? {
|
|
412
|
-
thinkingBudgets: { medium: thinkingBudget },
|
|
413
|
-
} : {}),
|
|
414
443
|
});
|
|
415
444
|
|
|
445
|
+
// ── Loop detection state ──
|
|
446
|
+
const stepHistory: Array<{ tool: string; input: string }> = [];
|
|
447
|
+
let stepCount = 0;
|
|
448
|
+
let loopCooldown = 0;
|
|
449
|
+
|
|
416
450
|
// ── SSE Stream ──
|
|
417
451
|
const encoder = new TextEncoder();
|
|
418
452
|
const requestStartTime = Date.now();
|
|
@@ -424,7 +458,7 @@ export async function POST(req: NextRequest) {
|
|
|
424
458
|
} catch { /* controller may be closed */ }
|
|
425
459
|
}
|
|
426
460
|
|
|
427
|
-
|
|
461
|
+
session.subscribe((event: AgentEvent) => {
|
|
428
462
|
if (isTextDeltaEvent(event)) {
|
|
429
463
|
send({ type: 'text_delta', delta: getTextDelta(event) });
|
|
430
464
|
} else if (isThinkingDeltaEvent(event)) {
|
|
@@ -476,24 +510,20 @@ export async function POST(req: NextRequest) {
|
|
|
476
510
|
if (lastN.every(s => s.tool === lastN[0].tool && s.input === lastN[0].input)) {
|
|
477
511
|
loopCooldown = 3;
|
|
478
512
|
// 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);
|
|
513
|
+
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
514
|
}
|
|
485
515
|
}
|
|
486
516
|
|
|
487
517
|
// Step limit enforcement
|
|
488
518
|
if (stepCount >= stepLimit) {
|
|
489
|
-
|
|
519
|
+
void session.abort();
|
|
490
520
|
}
|
|
491
521
|
|
|
492
522
|
console.log(`[ask] Step ${stepCount}/${stepLimit}`);
|
|
493
523
|
}
|
|
494
524
|
});
|
|
495
525
|
|
|
496
|
-
|
|
526
|
+
session.prompt(lastUserContent).then(() => {
|
|
497
527
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
498
528
|
send({ type: 'done' });
|
|
499
529
|
controller.close();
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
export const runtime = 'nodejs';
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { revalidatePath } from 'next/cache';
|
|
8
|
+
import { sanitizeFileName, convertToMarkdown } from '@/lib/core/file-convert';
|
|
9
|
+
import { resolveSafe } from '@/lib/core/security';
|
|
10
|
+
import { scaffoldIfNewSpace } from '@/lib/core/space-scaffold';
|
|
11
|
+
import { organizeAfterImport } from '@/lib/core/organize';
|
|
12
|
+
import { invalidateSearchIndex } from '@/lib/core/search';
|
|
13
|
+
import { effectiveSopRoot } from '@/lib/settings';
|
|
14
|
+
import { invalidateCache } from '@/lib/fs';
|
|
15
|
+
|
|
16
|
+
const MAX_FILES = 20;
|
|
17
|
+
const MAX_CONTENT_LENGTH = 5 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
type ConflictMode = 'skip' | 'rename' | 'overwrite';
|
|
20
|
+
|
|
21
|
+
interface ImportRequest {
|
|
22
|
+
files: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
content: string;
|
|
25
|
+
encoding?: 'text' | 'base64';
|
|
26
|
+
}>;
|
|
27
|
+
targetSpace?: string;
|
|
28
|
+
organize?: boolean;
|
|
29
|
+
conflict?: ConflictMode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeTargetSpace(raw: unknown): string {
|
|
33
|
+
if (raw === undefined || raw === null) return '';
|
|
34
|
+
if (typeof raw !== 'string') return '';
|
|
35
|
+
return raw.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeFileContent(
|
|
39
|
+
encoding: 'text' | 'base64' | undefined,
|
|
40
|
+
content: string,
|
|
41
|
+
sanitizedName: string,
|
|
42
|
+
): string {
|
|
43
|
+
if (encoding === 'base64') {
|
|
44
|
+
const buf = Buffer.from(content, 'base64');
|
|
45
|
+
if (sanitizedName.toLowerCase().endsWith('.pdf')) {
|
|
46
|
+
return buf.toString('latin1');
|
|
47
|
+
}
|
|
48
|
+
return buf.toString('utf-8');
|
|
49
|
+
}
|
|
50
|
+
return content;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveUniquePath(
|
|
54
|
+
mindRoot: string,
|
|
55
|
+
relPath: string,
|
|
56
|
+
conflict: ConflictMode,
|
|
57
|
+
): { relPath: string; resolved: string; skipped?: string } {
|
|
58
|
+
let rel = relPath.replace(/\\/g, '/');
|
|
59
|
+
let resolved = resolveSafe(mindRoot, rel);
|
|
60
|
+
if (!fs.existsSync(resolved)) {
|
|
61
|
+
return { relPath: rel, resolved };
|
|
62
|
+
}
|
|
63
|
+
if (conflict === 'skip') {
|
|
64
|
+
return { relPath: rel, resolved, skipped: 'file exists' };
|
|
65
|
+
}
|
|
66
|
+
if (conflict === 'overwrite') {
|
|
67
|
+
return { relPath: rel, resolved };
|
|
68
|
+
}
|
|
69
|
+
let n = 0;
|
|
70
|
+
while (fs.existsSync(resolved)) {
|
|
71
|
+
n += 1;
|
|
72
|
+
const dir = path.posix.dirname(rel);
|
|
73
|
+
const base = path.posix.basename(rel);
|
|
74
|
+
const ext = path.posix.extname(base);
|
|
75
|
+
const stem = ext ? base.slice(0, -ext.length) : base;
|
|
76
|
+
const newBase = `${stem}-${n}${ext}`;
|
|
77
|
+
rel = dir && dir !== '.' ? path.posix.join(dir, newBase) : newBase;
|
|
78
|
+
resolved = resolveSafe(mindRoot, rel);
|
|
79
|
+
}
|
|
80
|
+
return { relPath: rel, resolved };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function POST(req: NextRequest) {
|
|
84
|
+
let body: unknown;
|
|
85
|
+
try {
|
|
86
|
+
body = await req.json();
|
|
87
|
+
} catch {
|
|
88
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mindRoot = effectiveSopRoot().trim();
|
|
92
|
+
if (!mindRoot) {
|
|
93
|
+
return NextResponse.json({ error: 'MIND_ROOT is not configured' }, { status: 400 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!body || typeof body !== 'object') {
|
|
97
|
+
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reqBody = body as ImportRequest;
|
|
101
|
+
if (!Array.isArray(reqBody.files)) {
|
|
102
|
+
return NextResponse.json({ error: 'files must be an array' }, { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (reqBody.files.length > MAX_FILES) {
|
|
106
|
+
return NextResponse.json({ error: `At most ${MAX_FILES} files per request` }, { status: 400 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const targetSpaceNorm = normalizeTargetSpace(reqBody.targetSpace);
|
|
110
|
+
const organize = reqBody.organize !== false;
|
|
111
|
+
const conflict: ConflictMode =
|
|
112
|
+
reqBody.conflict === 'skip' || reqBody.conflict === 'overwrite' || reqBody.conflict === 'rename'
|
|
113
|
+
? reqBody.conflict
|
|
114
|
+
: 'rename';
|
|
115
|
+
|
|
116
|
+
const created: Array<{ original: string; path: string }> = [];
|
|
117
|
+
const skipped: Array<{ name: string; reason: string }> = [];
|
|
118
|
+
const errors: Array<{ name: string; error: string }> = [];
|
|
119
|
+
const createdPaths: string[] = [];
|
|
120
|
+
const updatedFiles: string[] = [];
|
|
121
|
+
|
|
122
|
+
for (const entry of reqBody.files) {
|
|
123
|
+
const originalName = typeof entry?.name === 'string' ? entry.name : '';
|
|
124
|
+
try {
|
|
125
|
+
if (typeof entry?.name !== 'string' || typeof entry?.content !== 'string') {
|
|
126
|
+
errors.push({ name: originalName || '(unknown)', error: 'name and content must be strings' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!entry.name.trim()) {
|
|
130
|
+
errors.push({ name: '(empty)', error: 'name must not be empty' });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (entry.content.length > MAX_CONTENT_LENGTH) {
|
|
134
|
+
errors.push({ name: entry.name, error: `content exceeds ${MAX_CONTENT_LENGTH} characters` });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sanitized = sanitizeFileName(entry.name);
|
|
139
|
+
const encoding = entry.encoding === 'base64' ? 'base64' : 'text';
|
|
140
|
+
const raw = decodeFileContent(encoding, entry.content, sanitized);
|
|
141
|
+
const convertResult = convertToMarkdown(sanitized, raw);
|
|
142
|
+
|
|
143
|
+
let relPath = targetSpaceNorm
|
|
144
|
+
? path.posix.join(targetSpaceNorm, convertResult.targetName)
|
|
145
|
+
: convertResult.targetName;
|
|
146
|
+
|
|
147
|
+
const { relPath: finalRel, resolved, skipped: skipReason } = resolveUniquePath(
|
|
148
|
+
mindRoot,
|
|
149
|
+
relPath,
|
|
150
|
+
conflict,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (skipReason) {
|
|
154
|
+
skipped.push({ name: entry.name, reason: skipReason });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
159
|
+
fs.writeFileSync(resolved, convertResult.content, 'utf-8');
|
|
160
|
+
scaffoldIfNewSpace(mindRoot, finalRel);
|
|
161
|
+
|
|
162
|
+
created.push({ original: entry.name, path: finalRel });
|
|
163
|
+
createdPaths.push(finalRel);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
errors.push({ name: originalName || '(unknown)', error: (e as Error).message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (organize && createdPaths.length > 0) {
|
|
170
|
+
try {
|
|
171
|
+
const { readmeUpdated } = organizeAfterImport(mindRoot, createdPaths, targetSpaceNorm);
|
|
172
|
+
if (readmeUpdated && targetSpaceNorm) {
|
|
173
|
+
updatedFiles.push(path.posix.join(targetSpaceNorm, 'README.md'));
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
/* organize is best-effort */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (created.length > 0 || updatedFiles.length > 0) {
|
|
181
|
+
invalidateSearchIndex();
|
|
182
|
+
invalidateCache();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
revalidatePath('/');
|
|
187
|
+
} catch {
|
|
188
|
+
/* noop in test env */
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return NextResponse.json({
|
|
192
|
+
created,
|
|
193
|
+
skipped,
|
|
194
|
+
errors,
|
|
195
|
+
updatedFiles,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -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,
|