@geminilight/mindos 0.6.6 → 0.6.8

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.
@@ -103,6 +103,34 @@ function getTurnEndData(e: AgentEvent): { toolResults: Array<{ toolName: string;
103
103
  // Helpers
104
104
  // ---------------------------------------------------------------------------
105
105
 
106
+ /**
107
+ * Strip large fields (file content) from tool args before SSE serialization.
108
+ * The client only needs path/name for progress display, not the full content.
109
+ * This prevents JSON.stringify failures on oversized payloads.
110
+ */
111
+ function sanitizeToolArgs(toolName: string, args: unknown): unknown {
112
+ if (!args || typeof args !== 'object') return args;
113
+ const a = args as Record<string, unknown>;
114
+
115
+ if (toolName === 'batch_create_files' && Array.isArray(a.files)) {
116
+ return {
117
+ ...a,
118
+ files: (a.files as Array<Record<string, unknown>>).map(f => ({
119
+ path: f.path,
120
+ ...(f.description ? { description: f.description } : {}),
121
+ })),
122
+ };
123
+ }
124
+
125
+ if (typeof a.content === 'string' && a.content.length > 200) {
126
+ return { ...a, content: `[${a.content.length} chars]` };
127
+ }
128
+ if (typeof a.text === 'string' && a.text.length > 200) {
129
+ return { ...a, text: `[${a.text.length} chars]` };
130
+ }
131
+ return args;
132
+ }
133
+
106
134
  function readKnowledgeFile(filePath: string): { ok: boolean; content: string; truncated: boolean; error?: string } {
107
135
  try {
108
136
  const raw = getFileContent(filePath);
@@ -457,7 +485,11 @@ export async function POST(req: NextRequest) {
457
485
  function send(event: MindOSSSEvent) {
458
486
  try {
459
487
  controller.enqueue(encoder.encode(`data:${JSON.stringify(event)}\n\n`));
460
- } catch { /* controller may be closed */ }
488
+ } catch (err) {
489
+ if (err instanceof TypeError) {
490
+ console.error('[ask] SSE send failed (serialization):', (err as Error).message, 'event type:', (event as { type?: string }).type);
491
+ }
492
+ }
461
493
  }
462
494
 
463
495
  session.subscribe((event: AgentEvent) => {
@@ -467,11 +499,12 @@ export async function POST(req: NextRequest) {
467
499
  send({ type: 'thinking_delta', delta: getThinkingDelta(event) });
468
500
  } else if (isToolExecutionStartEvent(event)) {
469
501
  const { toolCallId, toolName, args } = getToolExecutionStart(event);
502
+ const safeArgs = sanitizeToolArgs(toolName, args);
470
503
  send({
471
504
  type: 'tool_start',
472
505
  toolCallId,
473
506
  toolName,
474
- args,
507
+ args: safeArgs,
475
508
  });
476
509
  } else if (isToolExecutionEndEvent(event)) {
477
510
  const { toolCallId, output, isError } = getToolExecutionEnd(event);
@@ -1,6 +1,11 @@
1
1
  export const dynamic = 'force-dynamic';
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
  import { NextRequest, NextResponse } from 'next/server';
3
5
  import { revalidatePath } from 'next/cache';
6
+ import { resolveSafe } from '@/lib/core/security';
7
+ import { sanitizeFileName, convertToMarkdown } from '@/lib/core/file-convert';
8
+ import { effectiveSopRoot } from '@/lib/settings';
4
9
  import {
5
10
  getFileContent,
6
11
  saveFileContent,
@@ -57,6 +62,28 @@ export async function GET(req: NextRequest) {
57
62
  }
58
63
  }
59
64
 
65
+ if (op === 'check_conflicts') {
66
+ const names = req.nextUrl.searchParams.get('names');
67
+ const space = req.nextUrl.searchParams.get('space') ?? '';
68
+ if (!names) return err('missing names');
69
+ try {
70
+ const mindRoot = effectiveSopRoot().trim();
71
+ if (!mindRoot) return err('MIND_ROOT not configured');
72
+ const fileNames = names.split(',').map(n => n.trim()).filter(Boolean);
73
+ const conflicts: string[] = [];
74
+ for (const name of fileNames) {
75
+ const sanitized = sanitizeFileName(name);
76
+ const { targetName } = convertToMarkdown(sanitized, '');
77
+ const rel = space ? path.posix.join(space, targetName) : targetName;
78
+ const resolved = resolveSafe(mindRoot, rel);
79
+ if (fs.existsSync(resolved)) conflicts.push(name);
80
+ }
81
+ return NextResponse.json({ conflicts });
82
+ } catch (e) {
83
+ return err((e as Error).message, 500);
84
+ }
85
+ }
86
+
60
87
  if (!filePath) return err('missing path');
61
88
 
62
89
  try {
@@ -42,21 +42,34 @@ function killByPort(port: number) {
42
42
  *
43
43
  * Unlike /api/restart which restarts the entire MindOS (Web + MCP),
44
44
  * this endpoint only restarts the MCP server. The Web UI stays up.
45
+ *
46
+ * When running under Desktop's ProcessManager, the crash handler
47
+ * auto-respawns MCP when it exits. We wait for the port to free,
48
+ * then spawn only if nothing re-bound (i.e. CLI mode with no crash
49
+ * handler). This avoids spawning a duplicate that races for the port.
45
50
  */
46
51
  export async function POST() {
47
52
  try {
48
53
  const cfg = readConfig();
49
- const mcpPort = (cfg.mcpPort as number) ?? 8781;
54
+ const mcpPort = Number(process.env.MINDOS_MCP_PORT) || Number(cfg.mcpPort) || 8781;
50
55
  const webPort = process.env.MINDOS_WEB_PORT || '3456';
51
- const authToken = cfg.authToken as string | undefined;
56
+ const authToken = process.env.AUTH_TOKEN || (cfg.authToken as string | undefined);
57
+ const managed = process.env.MINDOS_MANAGED === '1';
52
58
 
53
59
  // Step 1: Kill process on MCP port
54
60
  killByPort(mcpPort);
55
61
 
56
- // Step 2: Wait briefly for port to free
57
- await new Promise(r => setTimeout(r, 1000));
62
+ if (managed) {
63
+ // Desktop ProcessManager will auto-respawn MCP via its crash handler.
64
+ return NextResponse.json({ ok: true, port: mcpPort, note: 'ProcessManager will respawn' });
65
+ }
66
+
67
+ // Step 2 (CLI mode only): Wait for port to free, then spawn a new MCP
68
+ const portFree = await waitForPortFree(mcpPort, 5000);
69
+ if (!portFree) {
70
+ return NextResponse.json({ error: `MCP port ${mcpPort} still in use after kill` }, { status: 500 });
71
+ }
58
72
 
59
- // Step 3: Spawn new MCP server
60
73
  const root = process.env.MINDOS_PROJECT_ROOT || resolve(process.cwd(), '..');
61
74
  const mcpDir = resolve(root, 'mcp');
62
75
 
@@ -88,3 +101,22 @@ export async function POST() {
88
101
  return NextResponse.json({ error: message }, { status: 500 });
89
102
  }
90
103
  }
104
+
105
+ function isPortInUse(port: number): Promise<boolean> {
106
+ return new Promise((res) => {
107
+ const net = require('net');
108
+ const server = net.createServer();
109
+ server.once('error', () => res(true));
110
+ server.once('listening', () => { server.close(); res(false); });
111
+ server.listen(port, '127.0.0.1');
112
+ });
113
+ }
114
+
115
+ async function waitForPortFree(port: number, timeoutMs: number): Promise<boolean> {
116
+ const start = Date.now();
117
+ while (Date.now() - start < timeoutMs) {
118
+ if (!(await isPortInUse(port))) return true;
119
+ await new Promise(r => setTimeout(r, 300));
120
+ }
121
+ return false;
122
+ }
@@ -63,7 +63,7 @@ export async function GET() {
63
63
  const kbStats = getCachedKbStats(mindRoot);
64
64
 
65
65
  // Detect MCP status from environment / config
66
- const mcpPort = Number(process.env.MCP_PORT) || 3457;
66
+ const mcpPort = Number(process.env.MINDOS_MCP_PORT) || Number(process.env.MCP_PORT) || 8781;
67
67
 
68
68
  return NextResponse.json({
69
69
  system: {
@@ -37,16 +37,20 @@ async function findFreePort(start: number, selfPorts: Set<number>): Promise<numb
37
37
  }
38
38
 
39
39
  /**
40
- * The port this MindOS web server is actually listening on.
41
- * Derived from the incoming request URL — always reliable, no network round-trip.
40
+ * Ports this MindOS instance is known to be using.
42
41
  *
43
- * Note: We intentionally do NOT read settings here. Settings contain *configured*
44
- * ports (webPort / mcpPort), which may not actually be listening yet (e.g. during
45
- * first onboard, or if MCP server hasn't started). Treating configured-but-not-
46
- * listening ports as "self" would mask real conflicts.
42
+ * myWebPort: derived from the incoming request URL always reliable.
43
+ * myMcpPort: from MINDOS_MCP_PORT env var set by CLI / Desktop ProcessManager.
44
+ *
45
+ * We do NOT read ~/.mindos/config.json here. Config contains *configured* ports
46
+ * which may not actually be listening yet (e.g. first onboard before MCP starts).
47
+ * Env vars are only set when a process IS running, so they're safe to trust.
47
48
  */
48
- function getListeningPort(req: NextRequest): number {
49
- return parseInt(req.nextUrl.port || '0', 10);
49
+ function getKnownPorts(req: NextRequest): { myWebPort: number; myMcpPort: number } {
50
+ return {
51
+ myWebPort: parseInt(req.nextUrl.port || '0', 10),
52
+ myMcpPort: Number(process.env.MINDOS_MCP_PORT) || 0,
53
+ };
50
54
  }
51
55
 
52
56
  export async function POST(req: NextRequest) {
@@ -56,10 +60,10 @@ export async function POST(req: NextRequest) {
56
60
  return NextResponse.json({ error: 'Invalid port' }, { status: 400 });
57
61
  }
58
62
 
59
- const myPort = getListeningPort(req);
63
+ const { myWebPort, myMcpPort } = getKnownPorts(req);
60
64
 
61
- // Fast path: if checking the port we're currently listening on, skip network round-trip
62
- if (myPort > 0 && port === myPort) {
65
+ // Fast path: port belongs to this MindOS instance (deterministic, no network)
66
+ if ((myWebPort > 0 && port === myWebPort) || (myMcpPort > 0 && port === myMcpPort)) {
63
67
  return NextResponse.json({ available: true, isSelf: true });
64
68
  }
65
69
 
@@ -67,13 +71,14 @@ export async function POST(req: NextRequest) {
67
71
  if (!inUse) {
68
72
  return NextResponse.json({ available: true, isSelf: false });
69
73
  }
70
- // Port is occupied — check if it's another MindOS instance
74
+ // Port is occupied by something else — check if it's another MindOS instance
71
75
  const self = await isSelfPort(port);
72
76
  if (self) {
73
77
  return NextResponse.json({ available: true, isSelf: true });
74
78
  }
75
79
  const skipPorts = new Set<number>();
76
- if (myPort > 0) skipPorts.add(myPort);
80
+ if (myWebPort > 0) skipPorts.add(myWebPort);
81
+ if (myMcpPort > 0) skipPorts.add(myMcpPort);
77
82
  const suggestion = await findFreePort(port + 1, skipPorts);
78
83
  return NextResponse.json({ available: false, isSelf: false, suggestion });
79
84
  } catch (err) {