@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.
Files changed (58) hide show
  1. package/app/app/api/ask/route.ts +122 -92
  2. package/app/app/api/file/import/route.ts +197 -0
  3. package/app/app/api/mcp/agents/route.ts +53 -2
  4. package/app/app/api/mcp/status/route.ts +1 -1
  5. package/app/app/api/skills/route.ts +10 -114
  6. package/app/components/ActivityBar.tsx +5 -7
  7. package/app/components/CreateSpaceModal.tsx +31 -6
  8. package/app/components/FileTree.tsx +68 -11
  9. package/app/components/GuideCard.tsx +197 -131
  10. package/app/components/HomeContent.tsx +85 -18
  11. package/app/components/ImportModal.tsx +415 -0
  12. package/app/components/OnboardingView.tsx +9 -0
  13. package/app/components/Panel.tsx +4 -2
  14. package/app/components/SidebarLayout.tsx +96 -8
  15. package/app/components/SpaceInitToast.tsx +173 -0
  16. package/app/components/TableOfContents.tsx +1 -0
  17. package/app/components/agents/AgentDetailContent.tsx +69 -45
  18. package/app/components/agents/AgentsContentPage.tsx +2 -1
  19. package/app/components/agents/AgentsMcpSection.tsx +16 -12
  20. package/app/components/agents/AgentsOverviewSection.tsx +37 -36
  21. package/app/components/agents/AgentsPrimitives.tsx +41 -20
  22. package/app/components/agents/AgentsSkillsSection.tsx +16 -7
  23. package/app/components/agents/SkillDetailPopover.tsx +11 -11
  24. package/app/components/agents/agents-content-model.ts +16 -8
  25. package/app/components/ask/AskContent.tsx +148 -50
  26. package/app/components/ask/MentionPopover.tsx +16 -8
  27. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  28. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
  29. package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
  30. package/app/components/panels/DiscoverPanel.tsx +88 -2
  31. package/app/components/settings/KnowledgeTab.tsx +61 -0
  32. package/app/components/walkthrough/steps.ts +11 -6
  33. package/app/hooks/useFileImport.ts +191 -0
  34. package/app/hooks/useFileUpload.ts +11 -0
  35. package/app/hooks/useMention.ts +14 -6
  36. package/app/hooks/useSlashCommand.ts +114 -0
  37. package/app/lib/actions.ts +79 -2
  38. package/app/lib/agent/index.ts +1 -1
  39. package/app/lib/agent/prompt.ts +2 -0
  40. package/app/lib/agent/tools.ts +252 -0
  41. package/app/lib/core/create-space.ts +11 -4
  42. package/app/lib/core/file-convert.ts +97 -0
  43. package/app/lib/core/index.ts +1 -1
  44. package/app/lib/core/organize.ts +105 -0
  45. package/app/lib/i18n-en.ts +102 -46
  46. package/app/lib/i18n-zh.ts +101 -45
  47. package/app/lib/mcp-agents.ts +8 -0
  48. package/app/lib/pdf-extract.ts +33 -0
  49. package/app/lib/pi-integration/extensions.ts +45 -0
  50. package/app/lib/pi-integration/mcporter.ts +219 -0
  51. package/app/lib/pi-integration/session-store.ts +62 -0
  52. package/app/lib/pi-integration/skills.ts +116 -0
  53. package/app/lib/settings.ts +1 -1
  54. package/app/next-env.d.ts +1 -1
  55. package/app/next.config.ts +1 -1
  56. package/app/package.json +2 -0
  57. package/mcp/src/index.ts +29 -0
  58. package/package.json +1 -1
@@ -1,17 +1,24 @@
1
1
  export const dynamic = 'force-dynamic';
2
- import { Agent, type AgentEvent, type BeforeToolCallContext, type BeforeToolCallResult, type AfterToolCallContext, type AfterToolCallResult } from '@mariozechner/pi-agent-core';
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 { knowledgeBaseTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
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
- // ── Loop detection state ──
335
- const stepHistory: Array<{ tool: string; input: string }> = [];
336
- let stepCount = 0;
337
- let loopCooldown = 0;
338
-
339
- // ── Create Agent (per-request lifecycle) ──
340
- const agent = new Agent({
341
- initialState: {
342
- systemPrompt,
343
- model,
344
- thinkingLevel: (enableThinking && provider === 'anthropic') ? 'medium' : 'off',
345
- tools: knowledgeBaseTools,
346
- messages: historyMessages,
347
- },
348
- getApiKey: async () => requestApiKey,
349
- toolExecution: 'parallel',
350
-
351
- // Context management: truncate → compact → prune
352
- transformContext: createTransformContext(
353
- systemPrompt,
354
- modelName,
355
- () => model,
356
- apiKey,
357
- contextStrategy,
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
- for (const filePath of pathsToCheck) {
376
- try {
377
- assertNotProtected(filePath, 'modified by AI agent');
378
- } catch (e) {
379
- const errorMsg = e instanceof Error ? e.message : String(e);
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
- agent.subscribe((event: AgentEvent) => {
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
- agent.steer({
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
- agent.abort();
519
+ void session.abort();
490
520
  }
491
521
 
492
522
  console.log(`[ask] Step ${stepCount}/${stepLimit}`);
493
523
  }
494
524
  });
495
525
 
496
- agent.prompt(lastUserContent).then(() => {
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
- // Sort: installed first, then detected, then not found
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 ? 23 : 0,
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,