@geminilight/mindos 0.5.67 → 0.5.69

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.
@@ -357,21 +357,29 @@ export async function POST(req: NextRequest) {
357
357
  contextStrategy,
358
358
  ),
359
359
 
360
- // Write-protection: block writes to protected files
360
+ // Write-protection: block writes to protected files gracefully
361
361
  beforeToolCall: async (context: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> => {
362
362
  const { toolCall, args } = context;
363
363
  // toolCall is an object with type "toolCall" and contains the tool name and ID
364
364
  const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
365
365
  if (toolName && WRITE_TOOLS.has(toolName)) {
366
- const filePath = (args as any).path ?? (args as any).from_path;
367
- if (filePath) {
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
+ }
374
+
375
+ for (const filePath of pathsToCheck) {
368
376
  try {
369
377
  assertNotProtected(filePath, 'modified by AI agent');
370
378
  } catch (e) {
371
379
  const errorMsg = e instanceof Error ? e.message : String(e);
372
380
  return {
373
381
  block: true,
374
- reason: `Write-protection error: ${errorMsg}`,
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.`,
375
383
  };
376
384
  }
377
385
  }
@@ -11,6 +11,82 @@ function parseJsonc(text: string): Record<string, unknown> {
11
11
  return JSON.parse(stripped);
12
12
  }
13
13
 
14
+ /** Ensure nested object path exists and return the leaf container */
15
+ function ensureNestedPath(obj: Record<string, unknown>, dotPath: string): Record<string, unknown> {
16
+ const parts = dotPath.split('.').filter(Boolean);
17
+ let current = obj;
18
+ for (const part of parts) {
19
+ if (!current[part] || typeof current[part] !== 'object') {
20
+ current[part] = {};
21
+ }
22
+ current = current[part] as Record<string, unknown>;
23
+ }
24
+ return current;
25
+ }
26
+
27
+ /** Generate a TOML section string for an MCP entry */
28
+ function buildTomlEntry(sectionKey: string, serverName: string, entry: Record<string, unknown>): string {
29
+ const lines: string[] = [];
30
+ lines.push(`[${sectionKey}.${serverName}]`);
31
+ if (entry.type) lines.push(`type = "${entry.type}"`);
32
+ if (entry.command) lines.push(`command = "${entry.command}"`);
33
+ if (entry.url) lines.push(`url = "${entry.url}"`);
34
+ if (Array.isArray(entry.args)) {
35
+ lines.push(`args = [${entry.args.map(a => `"${a}"`).join(', ')}]`);
36
+ }
37
+ if (entry.env && typeof entry.env === 'object') {
38
+ lines.push('');
39
+ lines.push(`[${sectionKey}.${serverName}.env]`);
40
+ for (const [k, v] of Object.entries(entry.env)) {
41
+ lines.push(`${k} = "${v}"`);
42
+ }
43
+ }
44
+ if (entry.headers && typeof entry.headers === 'object') {
45
+ lines.push('');
46
+ lines.push(`[${sectionKey}.${serverName}.headers]`);
47
+ for (const [k, v] of Object.entries(entry.headers)) {
48
+ lines.push(`${k} = "${v}"`);
49
+ }
50
+ }
51
+ return lines.join('\n');
52
+ }
53
+
54
+ /** Replace or append a [section.server] block in a TOML file */
55
+ function mergeTomlEntry(existing: string, sectionKey: string, serverName: string, entry: Record<string, unknown>): string {
56
+ const sectionHeader = `[${sectionKey}.${serverName}]`;
57
+ const envHeader = `[${sectionKey}.${serverName}.env]`;
58
+ const headersHeader = `[${sectionKey}.${serverName}.headers]`;
59
+ const newBlock = buildTomlEntry(sectionKey, serverName, entry);
60
+
61
+ const lines = existing.split('\n');
62
+ const result: string[] = [];
63
+ let skipping = false;
64
+
65
+ for (const line of lines) {
66
+ const trimmed = line.trim();
67
+ if (trimmed === sectionHeader || trimmed === envHeader || trimmed === headersHeader) {
68
+ skipping = true;
69
+ continue;
70
+ }
71
+ if (skipping && trimmed.startsWith('[')) {
72
+ skipping = false;
73
+ }
74
+ if (!skipping) {
75
+ result.push(line);
76
+ }
77
+ }
78
+
79
+ // Remove trailing blank lines before appending
80
+ while (result.length > 0 && result[result.length - 1].trim() === '') {
81
+ result.pop();
82
+ }
83
+ result.push('');
84
+ result.push(newBlock);
85
+ result.push('');
86
+
87
+ return result.join('\n');
88
+ }
89
+
14
90
  interface AgentInstallItem {
15
91
  key: string;
16
92
  scope: 'project' | 'global';
@@ -96,20 +172,31 @@ export async function POST(req: NextRequest) {
96
172
  const absPath = expandHome(configPath);
97
173
 
98
174
  try {
99
- // Read existing config
100
- let config: Record<string, unknown> = {};
101
- if (fs.existsSync(absPath)) {
102
- config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
103
- }
104
-
105
- // Merge — only touch mcpServers.mindos
106
- if (!config[agent.key]) config[agent.key] = {};
107
- (config[agent.key] as Record<string, unknown>).mindos = entry;
108
-
109
- // Write
110
175
  const dir = path.dirname(absPath);
111
176
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
112
- fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
177
+
178
+ if (agent.format === 'toml') {
179
+ // TOML format (e.g. Codex): merge into existing TOML or generate new
180
+ const existing = fs.existsSync(absPath) ? fs.readFileSync(absPath, 'utf-8') : '';
181
+ const merged = mergeTomlEntry(existing, agent.key, 'mindos', entry as Record<string, unknown>);
182
+ fs.writeFileSync(absPath, merged, 'utf-8');
183
+ } else {
184
+ // JSON format (default)
185
+ let config: Record<string, unknown> = {};
186
+ if (fs.existsSync(absPath)) {
187
+ config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
188
+ }
189
+
190
+ // For global scope with nested key (e.g. VS Code: mcp.servers),
191
+ // write to the nested path instead of the flat key
192
+ const useNestedKey = isGlobal && agent.globalNestedKey;
193
+ const container = useNestedKey
194
+ ? ensureNestedPath(config, agent.globalNestedKey!)
195
+ : (() => { if (!config[agent.key]) config[agent.key] = {}; return config[agent.key] as Record<string, unknown>; })();
196
+ container.mindos = entry;
197
+
198
+ fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
199
+ }
113
200
 
114
201
  const result: typeof results[number] = { agent: key, status: 'ok', path: configPath, transport: effectiveTransport };
115
202
 
@@ -66,6 +66,7 @@ export async function POST() {
66
66
 
67
67
  const env: NodeJS.ProcessEnv = {
68
68
  ...process.env,
69
+ MCP_TRANSPORT: 'http',
69
70
  MCP_PORT: String(mcpPort),
70
71
  MCP_HOST: process.env.MCP_HOST || '0.0.0.0',
71
72
  MINDOS_URL: process.env.MINDOS_URL || `http://127.0.0.1:${webPort}`,
@@ -50,7 +50,7 @@ export async function GET(req: NextRequest) {
50
50
  transport: 'http',
51
51
  endpoint,
52
52
  port,
53
- toolCount: running ? 20 : 0,
53
+ toolCount: running ? 23 : 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,
@@ -136,6 +136,8 @@ export default function SkillDetailPopover({
136
136
  setLoadError(false);
137
137
  setCopied(false);
138
138
  setDeleteMsg(null);
139
+ setDeleting(false);
140
+ setToggleBusy(false);
139
141
  fetchContent();
140
142
  }
141
143
  }, [open, skillName, fetchContent]);
@@ -227,7 +227,7 @@ export async function compactMessages(
227
227
 
228
228
  console.log(`[ask] Compacted ${earlyMessages.length} early messages into summary (${summaryText.length} chars)`);
229
229
 
230
- const summaryContent = `[Summary of earlier conversation]\n\n${summaryText}`;
230
+ const summaryContent = `[System Note: Older conversation history has been truncated due to context length limits, but here is an AI-generated summary of what was discussed so far.]\n\n${summaryText}`;
231
231
 
232
232
  // If first recent message is also 'user', merge summary into it to avoid
233
233
  // consecutive user messages (Anthropic rejects user→user sequences).
@@ -316,10 +316,15 @@ export function hardPrune(
316
316
  console.log(`[ask] Hard pruned ${cutIdx} messages, injecting synthetic user message (${messages.length} → ${pruned.length + 1})`);
317
317
  const syntheticUser: UserMessage = {
318
318
  role: 'user',
319
- content: '[Conversation context was pruned due to length. Continuing from here.]',
319
+ content: '[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]',
320
320
  timestamp: Date.now(),
321
321
  };
322
322
  return [syntheticUser as AgentMessage, ...pruned];
323
+ } else if (cutIdx > 0 && pruned.length > 0 && (pruned[0] as any).role === 'user') {
324
+ // If we pruned and the first message IS a user message, prepend the warning to it
325
+ const firstMsg = { ...pruned[0] } as UserMessage;
326
+ firstMsg.content = `[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]\n\n` + firstMsg.content;
327
+ pruned[0] = firstMsg as AgentMessage;
323
328
  }
324
329
 
325
330
  if (cutIdx > 0) {
@@ -12,7 +12,14 @@ const MAX_FILE_CHARS = 20_000;
12
12
 
13
13
  export function truncate(content: string): string {
14
14
  if (content.length <= MAX_FILE_CHARS) return content;
15
- return content.slice(0, MAX_FILE_CHARS) + `\n\n[...truncated — file is ${content.length} chars, showing first ${MAX_FILE_CHARS}]`;
15
+
16
+ // Smart truncation: try to truncate at a natural boundary (newline)
17
+ let cutoff = content.lastIndexOf('\n', MAX_FILE_CHARS);
18
+ if (cutoff === -1) cutoff = MAX_FILE_CHARS;
19
+
20
+ const totalLines = content.split('\n').length;
21
+
22
+ return content.slice(0, cutoff) + `\n\n[...truncated — file is ${content.length} chars (${totalLines} lines), showing first ~${cutoff} chars]\n[Use the read_file_chunk tool to read the rest of the file by specifying start_line and end_line]`;
16
23
  }
17
24
 
18
25
  // ─── Helper: format tool error consistently ────────────────────────────────
@@ -51,6 +58,12 @@ const PathParam = Type.Object({
51
58
  path: Type.String({ description: 'Relative file path' }),
52
59
  });
53
60
 
61
+ const ReadFileChunkParams = Type.Object({
62
+ path: Type.String({ description: 'Relative file path' }),
63
+ start_line: Type.Number({ description: 'Line number to start reading from (1-indexed)' }),
64
+ end_line: Type.Number({ description: 'Line number to stop reading at (1-indexed)' }),
65
+ });
66
+
54
67
  const QueryParam = Type.Object({
55
68
  query: Type.String({ description: 'Search query (case-insensitive)' }),
56
69
  });
@@ -69,6 +82,13 @@ const CreateFileParams = Type.Object({
69
82
  content: Type.Optional(Type.String({ description: 'Initial file content' })),
70
83
  });
71
84
 
85
+ const BatchCreateFileParams = Type.Object({
86
+ files: Type.Array(Type.Object({
87
+ path: Type.String({ description: 'Relative file path (must end in .md or .csv)' }),
88
+ content: Type.String({ description: 'Initial file content' }),
89
+ }), { description: 'List of files to create' }),
90
+ });
91
+
72
92
  const AppendParams = Type.Object({
73
93
  path: Type.String({ description: 'Relative file path' }),
74
94
  content: Type.String({ description: 'Content to append' }),
@@ -86,6 +106,13 @@ const UpdateSectionParams = Type.Object({
86
106
  content: Type.String({ description: 'New content for the section' }),
87
107
  });
88
108
 
109
+ const EditLinesParams = Type.Object({
110
+ path: Type.String({ description: 'Relative file path' }),
111
+ start_line: Type.Number({ description: '1-indexed line number to start replacing' }),
112
+ end_line: Type.Number({ description: '1-indexed line number to stop replacing (inclusive)' }),
113
+ content: Type.String({ description: 'New content to insert in place of those lines' }),
114
+ });
115
+
89
116
  const RenameParams = Type.Object({
90
117
  path: Type.String({ description: 'Current relative file path' }),
91
118
  new_name: Type.String({ description: 'New filename (no path separators, e.g. "new-name.md")' }),
@@ -115,8 +142,8 @@ const CsvAppendParams = Type.Object({
115
142
 
116
143
  // Write-operation tool names — used by beforeToolCall for write-protection
117
144
  export const WRITE_TOOLS = new Set([
118
- 'write_file', 'create_file', 'append_to_file', 'insert_after_heading',
119
- 'update_section', 'delete_file', 'rename_file', 'move_file', 'append_csv',
145
+ 'write_file', 'create_file', 'batch_create_files', 'append_to_file', 'insert_after_heading',
146
+ 'update_section', 'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
120
147
  ]);
121
148
 
122
149
  export const knowledgeBaseTools: AgentTool<any>[] = [
@@ -171,13 +198,39 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
171
198
  {
172
199
  name: 'read_file',
173
200
  label: 'Read File',
174
- description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
201
+ description: 'Read the content of a file by its relative path. Always read a file before modifying it. If the file is too large, it will be truncated. Use read_file_chunk to read specific parts of large files.',
175
202
  parameters: PathParam,
176
203
  execute: safeExecute(async (_id, params: Static<typeof PathParam>) => {
177
204
  return textResult(truncate(getFileContent(params.path)));
178
205
  }),
179
206
  },
180
207
 
208
+ {
209
+ name: 'read_file_chunk',
210
+ label: 'Read File Chunk',
211
+ description: 'Read a specific range of lines from a file. Highly recommended for reading large files that were truncated by read_file.',
212
+ parameters: ReadFileChunkParams,
213
+ execute: safeExecute(async (_id, params: Static<typeof ReadFileChunkParams>) => {
214
+ const content = getFileContent(params.path);
215
+ const lines = content.split('\n');
216
+ const start = Math.max(1, params.start_line);
217
+ const end = Math.min(lines.length, params.end_line);
218
+
219
+ if (start > end) {
220
+ return textResult(`Error: start_line (${start}) is greater than end_line (${end}) or file has fewer lines.`);
221
+ }
222
+
223
+ // Prefix each line with its line number (1-indexed)
224
+ const pad = String(lines.length).length;
225
+ const chunk = lines
226
+ .slice(start - 1, end)
227
+ .map((l, i) => `${String(start + i).padStart(pad, ' ')} | ${l}`)
228
+ .join('\n');
229
+
230
+ return textResult(`Showing lines ${start} to ${end} of ${lines.length}:\n\n${chunk}`);
231
+ }),
232
+ },
233
+
181
234
  {
182
235
  name: 'search',
183
236
  label: 'Search',
@@ -223,6 +276,28 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
223
276
  }),
224
277
  },
225
278
 
279
+ {
280
+ name: 'batch_create_files',
281
+ label: 'Batch Create Files',
282
+ description: 'Create multiple new files in a single operation. Highly recommended when scaffolding new features or projects.',
283
+ parameters: BatchCreateFileParams,
284
+ execute: safeExecute(async (_id, params: Static<typeof BatchCreateFileParams>) => {
285
+ const created: string[] = [];
286
+ const errors: string[] = [];
287
+ for (const file of params.files) {
288
+ try {
289
+ createFile(file.path, file.content);
290
+ created.push(file.path);
291
+ } catch (e) {
292
+ errors.push(`${file.path}: ${formatToolError(e)}`);
293
+ }
294
+ }
295
+ let msg = `Batch creation complete.\nCreated ${created.length} files: ${created.join(', ')}`;
296
+ if (errors.length > 0) msg += `\n\nFailed to create ${errors.length} files:\n${errors.join('\n')}`;
297
+ return textResult(msg);
298
+ }),
299
+ },
300
+
226
301
  {
227
302
  name: 'append_to_file',
228
303
  label: 'Append to File',
@@ -237,7 +312,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
237
312
  {
238
313
  name: 'insert_after_heading',
239
314
  label: 'Insert After Heading',
240
- description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section.',
315
+ description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section. If heading matches fail, use edit_lines instead.',
241
316
  parameters: InsertHeadingParams,
242
317
  execute: safeExecute(async (_id, params: Static<typeof InsertHeadingParams>) => {
243
318
  insertAfterHeading(params.path, params.heading, params.content);
@@ -248,7 +323,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
248
323
  {
249
324
  name: 'update_section',
250
325
  label: 'Update Section',
251
- description: 'Replace the content of a Markdown section identified by its heading. The section spans from the heading to the next heading of equal or higher level.',
326
+ description: 'Replace the content of a Markdown section identified by its heading. The section spans from the heading to the next heading of equal or higher level. If heading matches fail, use edit_lines instead.',
252
327
  parameters: UpdateSectionParams,
253
328
  execute: safeExecute(async (_id, params: Static<typeof UpdateSectionParams>) => {
254
329
  updateSection(params.path, params.heading, params.content);
@@ -256,6 +331,24 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
256
331
  }),
257
332
  },
258
333
 
334
+ {
335
+ name: 'edit_lines',
336
+ label: 'Edit Lines',
337
+ description: 'Replace a specific range of lines with new content. Extremely reliable for precise edits. You must know the exact line numbers (use read_file_chunk to get them).',
338
+ parameters: EditLinesParams,
339
+ execute: safeExecute(async (_id, params: Static<typeof EditLinesParams>) => {
340
+ const { path: fp, start_line, end_line, content } = params;
341
+ const start = Math.max(0, start_line - 1);
342
+ const end = Math.max(0, end_line - 1);
343
+
344
+ const mindRoot = getMindRoot();
345
+ // Import the core function dynamically or it should be added to lib/fs.ts
346
+ const { updateLines } = await import('@/lib/core');
347
+ updateLines(mindRoot, fp, start, end, content.split('\n'));
348
+ return textResult(`Lines ${start_line}-${end_line} replaced in ${fp}`);
349
+ }),
350
+ },
351
+
259
352
  {
260
353
  name: 'delete_file',
261
354
  label: 'Delete File',
@@ -28,11 +28,19 @@ export function findBacklinks(mindRoot: string, targetPath: string): BacklinkEnt
28
28
  const lines = content.split('\n');
29
29
  for (let i = 0; i < lines.length; i++) {
30
30
  if (patterns.some(p => p.test(lines[i]))) {
31
- const start = Math.max(0, i - 1);
32
- const end = Math.min(lines.length - 1, i + 1);
33
- const ctx = lines.slice(start, end + 1).join('\n').trim();
31
+ // Expand to a slightly larger context block for agent comprehension
32
+ // Attempt to find paragraph boundaries (empty lines) or cap at a reasonable size
33
+ let start = i;
34
+ while (start > 0 && start > i - 3 && lines[start].trim() !== '') start--;
35
+ let end = i;
36
+ while (end < lines.length - 1 && end < i + 3 && lines[end].trim() !== '') end++;
37
+
38
+ let ctx = lines.slice(start, end + 1).join('\n').trim();
39
+ // Collapse multiple newlines in the context to save tokens, but keep simple structure
40
+ ctx = ctx.replace(/\n{2,}/g, ' ↵ ');
41
+
34
42
  results.push({ source: filePath, line: i + 1, context: ctx });
35
- break;
43
+ break; // currently only records the first match per file
36
44
  }
37
45
  }
38
46
  }
@@ -91,9 +91,23 @@ export function searchFiles(mindRoot: string, query: string, opts: SearchOptions
91
91
  const index = lowerContent.indexOf(lowerQuery);
92
92
  if (index === -1) continue;
93
93
 
94
- const snippetStart = Math.max(0, index - 60);
95
- const snippetEnd = Math.min(content.length, index + query.length + 60);
96
- let snippet = content.slice(snippetStart, snippetEnd).replace(/\n/g, ' ').trim();
94
+ // Try to find natural boundaries (paragraphs) around the match
95
+ let snippetStart = content.lastIndexOf('\n\n', index);
96
+ if (snippetStart === -1) snippetStart = Math.max(0, index - 200);
97
+ else snippetStart += 2; // skip the newlines
98
+
99
+ let snippetEnd = content.indexOf('\n\n', index);
100
+ if (snippetEnd === -1) snippetEnd = Math.min(content.length, index + query.length + 200);
101
+
102
+ // Prevent massive blocks (cap at ~400 chars total)
103
+ if (index - snippetStart > 200) snippetStart = index - 200;
104
+ if (snippetEnd - index > 200) snippetEnd = index + query.length + 200;
105
+
106
+ let snippet = content.slice(snippetStart, snippetEnd).trim();
107
+
108
+ // Collapse internal whitespace for cleaner search result presentation, but preserve some structure
109
+ snippet = snippet.replace(/\n{3,}/g, '\n\n');
110
+
97
111
  if (snippetStart > 0) snippet = '...' + snippet;
98
112
  if (snippetEnd < content.length) snippet += '...';
99
113
 
package/app/lib/fs.ts CHANGED
@@ -525,8 +525,8 @@ function generateSnippet(
525
525
  }
526
526
  }
527
527
 
528
- const snippetStart = Math.max(0, bestStart - 60);
529
- const snippetEnd = Math.min(content.length, bestEnd + 61);
528
+ const snippetStart = Math.max(0, bestStart - 120);
529
+ const snippetEnd = Math.min(content.length, bestEnd + 120);
530
530
 
531
531
  let start = snippetStart;
532
532
  if (start > 0) {
@@ -539,7 +539,9 @@ function generateSnippet(
539
539
  if (spaceIdx > bestEnd) end = spaceIdx;
540
540
  }
541
541
 
542
- let snippet = content.slice(start, end).replace(/\n/g, ' ').trim();
542
+ let snippet = content.slice(start, end).trim();
543
+ // Collapse multiple newlines into spaces but keep single newlines
544
+ snippet = snippet.replace(/\n{2,}/g, ' ↵ ');
543
545
  if (start > 0) snippet = '...' + snippet;
544
546
  if (end < content.length) snippet = snippet + '...';
545
547
  return snippet;
package/bin/cli.js CHANGED
@@ -421,11 +421,18 @@ const commands = {
421
421
  console.log(yellow('Installing MCP dependencies (first run)...\n'));
422
422
  npmInstall(resolve(ROOT, 'mcp'), '--no-workspaces');
423
423
  }
424
- // Map config env vars to what the MCP server expects
425
- const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
424
+ // `mindos mcp` is the entry point for MCP clients (Claude Code, Cursor, etc.)
425
+ // which communicate over stdin/stdout. Default to stdio; HTTP is handled by
426
+ // `mindos start` via spawnMcp(). Callers can still override via env.
427
+ if (!process.env.MCP_TRANSPORT) {
428
+ process.env.MCP_TRANSPORT = 'stdio';
429
+ }
426
430
  const webPort = process.env.MINDOS_WEB_PORT || '3456';
427
- process.env.MCP_PORT = mcpPort;
428
- process.env.MINDOS_URL = `http://localhost:${webPort}`;
431
+ process.env.MINDOS_URL = process.env.MINDOS_URL || `http://localhost:${webPort}`;
432
+ // Only set MCP_PORT for HTTP mode (stdio doesn't bind any port)
433
+ if (process.env.MCP_TRANSPORT === 'http') {
434
+ process.env.MCP_PORT = process.env.MINDOS_MCP_PORT || '8781';
435
+ }
429
436
  run(`npx tsx src/index.ts`, resolve(ROOT, 'mcp'));
430
437
  },
431
438
 
@@ -62,6 +62,7 @@ export function spawnMcp(verbose = false) {
62
62
 
63
63
  const env = {
64
64
  ...process.env,
65
+ MCP_TRANSPORT: 'http',
65
66
  MCP_PORT: mcpPort,
66
67
  MCP_HOST: process.env.MCP_HOST || '0.0.0.0',
67
68
  MINDOS_URL: process.env.MINDOS_URL || `http://127.0.0.1:${webPort}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.67",
3
+ "version": "0.5.69",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",