@geminilight/mindos 0.5.21 → 0.5.22

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.
@@ -1,16 +1,31 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { resolveSafe } from './security';
4
3
  import { collectAllFiles } from './tree';
5
4
  import { readFile } from './fs-ops';
5
+ import { SearchIndex } from './search-index';
6
6
  import type { SearchResult, SearchOptions } from './types';
7
7
 
8
+ /**
9
+ * Module-level search index singleton.
10
+ * Lazily built on first search, invalidated by `invalidateSearchIndex()`.
11
+ */
12
+ const searchIndex = new SearchIndex();
13
+
14
+ /** Invalidate the core search index. Called from `lib/fs.ts` on write operations. */
15
+ export function invalidateSearchIndex(): void {
16
+ searchIndex.invalidate();
17
+ }
18
+
8
19
  /**
9
20
  * Core literal search — used by MCP tools via REST API.
10
21
  *
11
22
  * This is a **case-insensitive literal string match** with occurrence-density scoring.
12
23
  * It supports scope, file_type, and modified_after filters that MCP tools expose.
13
24
  *
25
+ * Performance: uses an in-memory inverted index to narrow the candidate file set
26
+ * before doing full-text scanning. The index is built lazily on the first query
27
+ * and invalidated on any write operation.
28
+ *
14
29
  * NOTE: The App also has a separate Fuse.js fuzzy search in `lib/fs.ts` for the
15
30
  * browser `⌘K` search overlay. The two coexist intentionally:
16
31
  * - Core search (here): exact literal match, supports filters, used by MCP/API
@@ -20,6 +35,15 @@ export function searchFiles(mindRoot: string, query: string, opts: SearchOptions
20
35
  if (!query.trim()) return [];
21
36
  const { limit = 20, scope, file_type = 'all', modified_after } = opts;
22
37
 
38
+ // Ensure search index is built for this mindRoot
39
+ if (!searchIndex.isBuiltFor(mindRoot)) {
40
+ searchIndex.rebuild(mindRoot);
41
+ }
42
+
43
+ // Use index to get candidate files (or null if index unavailable → full scan)
44
+ const candidates = searchIndex.getCandidates(query);
45
+ const candidateSet = candidates ? new Set(candidates) : null;
46
+
23
47
  let allFiles = collectAllFiles(mindRoot);
24
48
 
25
49
  // Filter by scope (directory prefix)
@@ -34,6 +58,11 @@ export function searchFiles(mindRoot: string, query: string, opts: SearchOptions
34
58
  allFiles = allFiles.filter(f => f.endsWith(ext));
35
59
  }
36
60
 
61
+ // Narrow by index candidates (if available)
62
+ if (candidateSet) {
63
+ allFiles = allFiles.filter(f => candidateSet.has(f));
64
+ }
65
+
37
66
  // Filter by modification time
38
67
  let mtimeThreshold = 0;
39
68
  if (modified_after) {
@@ -1,11 +1,12 @@
1
1
  import path from 'path';
2
+ import { MindOSError, ErrorCodes } from '@/lib/errors';
2
3
 
3
4
  /**
4
5
  * Asserts that a resolved path is within the given root.
5
6
  */
6
7
  export function assertWithinRoot(resolved: string, root: string): void {
7
8
  if (!resolved.startsWith(root + path.sep) && resolved !== root) {
8
- throw new Error('Access denied: path outside MIND_ROOT');
9
+ throw new MindOSError(ErrorCodes.PATH_OUTSIDE_ROOT, 'Access denied: path outside MIND_ROOT', { resolved, root });
9
10
  }
10
11
  }
11
12
 
@@ -35,9 +36,11 @@ export function isRootProtected(filePath: string): boolean {
35
36
  */
36
37
  export function assertNotProtected(filePath: string, operation: string): void {
37
38
  if (isRootProtected(filePath)) {
38
- throw new Error(
39
+ throw new MindOSError(
40
+ ErrorCodes.PROTECTED_FILE,
39
41
  `Protected file: root "${filePath}" cannot be ${operation} via MCP. ` +
40
- `This is a system kernel file (§7 of INSTRUCTION.md). Edit it manually or use a dedicated confirmation workflow.`
42
+ `This is a system kernel file (§7 of INSTRUCTION.md). Edit it manually or use a dedicated confirmation workflow.`,
43
+ { filePath, operation },
41
44
  );
42
45
  }
43
46
  }
@@ -0,0 +1,108 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * Centralized error class for MindOS backend business logic.
5
+ *
6
+ * Every throw in `lib/core/` should use this class so that:
7
+ * 1. API routes can detect it in catch blocks and return structured JSON.
8
+ * 2. Callers can switch on `code` for programmatic handling.
9
+ * 3. `userMessage` provides a translatable, user-safe description.
10
+ */
11
+ export class MindOSError extends Error {
12
+ constructor(
13
+ public code: ErrorCode,
14
+ message: string,
15
+ public context?: Record<string, unknown>,
16
+ public userMessage?: string,
17
+ ) {
18
+ super(message);
19
+ this.name = 'MindOSError';
20
+ }
21
+ }
22
+
23
+ export const ErrorCodes = {
24
+ // File operations
25
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
26
+ FILE_ALREADY_EXISTS: 'FILE_ALREADY_EXISTS',
27
+ PATH_OUTSIDE_ROOT: 'PATH_OUTSIDE_ROOT',
28
+ PROTECTED_FILE: 'PROTECTED_FILE',
29
+ INVALID_PATH: 'INVALID_PATH',
30
+ INVALID_RANGE: 'INVALID_RANGE',
31
+ HEADING_NOT_FOUND: 'HEADING_NOT_FOUND',
32
+ INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',
33
+ // API
34
+ INVALID_REQUEST: 'INVALID_REQUEST',
35
+ MODEL_INIT_FAILED: 'MODEL_INIT_FAILED',
36
+ // Generic
37
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
38
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
39
+ } as const;
40
+
41
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
42
+
43
+ /** Standardized API error response envelope. */
44
+ export interface ApiErrorResponse {
45
+ ok: false;
46
+ error: {
47
+ code: string;
48
+ message: string;
49
+ };
50
+ }
51
+
52
+ /** Extract a human-readable message from an unknown thrown value. */
53
+ export function toErrorMessage(err: unknown): string {
54
+ if (err instanceof Error) return err.message;
55
+ if (typeof err === 'string') return err;
56
+ return String(err);
57
+ }
58
+
59
+ /** Map an ErrorCode to an HTTP status code. */
60
+ function mapCodeToStatus(code: ErrorCode): number {
61
+ switch (code) {
62
+ case ErrorCodes.FILE_NOT_FOUND:
63
+ case ErrorCodes.HEADING_NOT_FOUND:
64
+ return 404;
65
+ case ErrorCodes.FILE_ALREADY_EXISTS:
66
+ return 409;
67
+ case ErrorCodes.PATH_OUTSIDE_ROOT:
68
+ case ErrorCodes.PROTECTED_FILE:
69
+ case ErrorCodes.PERMISSION_DENIED:
70
+ return 403;
71
+ case ErrorCodes.INVALID_PATH:
72
+ case ErrorCodes.INVALID_RANGE:
73
+ case ErrorCodes.INVALID_FILE_TYPE:
74
+ case ErrorCodes.INVALID_REQUEST:
75
+ return 400;
76
+ case ErrorCodes.MODEL_INIT_FAILED:
77
+ case ErrorCodes.INTERNAL_ERROR:
78
+ default:
79
+ return 500;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Build a NextResponse with the standard `{ ok, error }` envelope.
85
+ *
86
+ * If `status` is omitted it is derived from the error code.
87
+ */
88
+ export function apiError(code: ErrorCode, message: string, status?: number): NextResponse<ApiErrorResponse> {
89
+ const effectiveStatus = status ?? mapCodeToStatus(code);
90
+ return NextResponse.json({ ok: false as const, error: { code, message } }, { status: effectiveStatus });
91
+ }
92
+
93
+ /**
94
+ * Convenience: catch an unknown error and return a structured API response.
95
+ *
96
+ * Usage in route handlers:
97
+ * ```ts
98
+ * catch (err) {
99
+ * return handleRouteError(err);
100
+ * }
101
+ * ```
102
+ */
103
+ export function handleRouteError(err: unknown): NextResponse<ApiErrorResponse> {
104
+ if (err instanceof MindOSError) {
105
+ return apiError(err.code, err.message);
106
+ }
107
+ return apiError(ErrorCodes.INTERNAL_ERROR, 'Internal server error', 500);
108
+ }
package/app/lib/fs.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import Fuse, { FuseResultMatch } from 'fuse.js';
4
+ import { MindOSError, ErrorCodes } from '@/lib/errors';
4
5
  import {
5
6
  readFile as coreReadFile,
6
7
  writeFile as coreWriteFile,
@@ -19,6 +20,7 @@ import {
19
20
  isGitRepo as coreIsGitRepo,
20
21
  gitLog as coreGitLog,
21
22
  gitShowFile as coreGitShowFile,
23
+ invalidateSearchIndex,
22
24
  } from './core';
23
25
  import { FileNode } from './core/types';
24
26
  import { SearchMatch } from './types';
@@ -53,6 +55,7 @@ function isCacheValid(): boolean {
53
55
  export function invalidateCache(): void {
54
56
  _cache = null;
55
57
  _searchIndex = null;
58
+ invalidateSearchIndex();
56
59
  }
57
60
 
58
61
  function ensureCache(): FileTreeCache {
@@ -266,9 +269,9 @@ export function updateLines(filePath: string, startIndex: number, endIndex: numb
266
269
 
267
270
  export function deleteLines(filePath: string, startIndex: number, endIndex: number): void {
268
271
  const existing = readLines(filePath);
269
- if (startIndex < 0 || endIndex < 0) throw new Error('Invalid line index: indices must be >= 0');
270
- if (startIndex > endIndex) throw new Error(`Invalid range: start (${startIndex}) > end (${endIndex})`);
271
- if (startIndex >= existing.length) throw new Error(`Invalid line index: start (${startIndex}) >= total lines (${existing.length})`);
272
+ if (startIndex < 0 || endIndex < 0) throw new MindOSError(ErrorCodes.INVALID_RANGE, 'Invalid line index: indices must be >= 0', { startIndex, endIndex });
273
+ if (startIndex > endIndex) throw new MindOSError(ErrorCodes.INVALID_RANGE, `Invalid range: start (${startIndex}) > end (${endIndex})`, { startIndex, endIndex });
274
+ if (startIndex >= existing.length) throw new MindOSError(ErrorCodes.INVALID_RANGE, `Invalid line index: start (${startIndex}) >= total lines (${existing.length})`, { startIndex, totalLines: existing.length });
272
275
  existing.splice(startIndex, endIndex - startIndex + 1);
273
276
  saveFileContent(filePath, existing.join('\n'));
274
277
  }
@@ -104,7 +104,7 @@ export const en = {
104
104
  },
105
105
  settings: {
106
106
  title: 'Settings',
107
- tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', sync: 'Sync', mcp: 'MCP', plugins: 'Plugins', shortcuts: 'Shortcuts' },
107
+ tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', sync: 'Sync', mcp: 'MCP', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents' },
108
108
  ai: {
109
109
  provider: 'Provider',
110
110
  model: 'Model',
@@ -262,6 +262,49 @@ export const en = {
262
262
  configureFor: 'Configure for',
263
263
  configPath: 'Config path',
264
264
  },
265
+ monitoring: {
266
+ system: 'System',
267
+ heapMemory: 'Heap Memory',
268
+ rss: 'RSS',
269
+ uptime: 'Uptime',
270
+ nodeVersion: 'Node',
271
+ application: 'Application',
272
+ requests: 'Requests',
273
+ toolCalls: 'Tool Calls',
274
+ avgResponse: 'Avg Response',
275
+ tokens: 'Tokens',
276
+ errors: 'Errors',
277
+ knowledgeBase: 'Knowledge Base',
278
+ files: 'Files',
279
+ totalSize: 'Total Size',
280
+ rootPath: 'Root',
281
+ mcpStatus: 'Status',
282
+ mcpRunning: 'Running',
283
+ mcpStopped: 'Stopped',
284
+ mcpPort: 'Port',
285
+ autoRefresh: 'Auto-refresh every 5s',
286
+ fetchError: 'Failed to load monitoring data',
287
+ },
288
+ agents: {
289
+ mcpServer: 'MCP Server',
290
+ running: 'Running',
291
+ stopped: 'Not running',
292
+ onPort: (port: number) => `on :${port}`,
293
+ refresh: 'Refresh',
294
+ refreshing: 'Refreshing...',
295
+ connected: 'Connected',
296
+ connectedCount: (n: number) => `Connected (${n})`,
297
+ detectedNotConfigured: 'Detected but not configured',
298
+ detectedCount: (n: number) => `Detected but not configured (${n})`,
299
+ notDetected: 'Not Detected',
300
+ notDetectedCount: (n: number) => `Not Detected (${n})`,
301
+ showAll: 'Show all',
302
+ hideAll: 'Hide',
303
+ connect: 'Connect',
304
+ noAgents: 'No agents detected on this machine.',
305
+ fetchError: 'Failed to load agent data',
306
+ autoRefresh: 'Auto-refresh every 30s',
307
+ },
265
308
  save: 'Save',
266
309
  saved: 'Saved',
267
310
  saveFailed: 'Save failed',
@@ -129,7 +129,7 @@ export const zh = {
129
129
  },
130
130
  settings: {
131
131
  title: '设置',
132
- tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', sync: '同步', mcp: 'MCP', plugins: '插件', shortcuts: '快捷键' },
132
+ tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', sync: '同步', mcp: 'MCP', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents' },
133
133
  ai: {
134
134
  provider: '服务商',
135
135
  model: '模型',
@@ -287,6 +287,49 @@ export const zh = {
287
287
  configureFor: '配置目标',
288
288
  configPath: '配置路径',
289
289
  },
290
+ monitoring: {
291
+ system: '系统',
292
+ heapMemory: '堆内存',
293
+ rss: 'RSS',
294
+ uptime: '运行时间',
295
+ nodeVersion: 'Node',
296
+ application: '应用',
297
+ requests: '请求数',
298
+ toolCalls: '工具调用',
299
+ avgResponse: '平均响应',
300
+ tokens: 'Token',
301
+ errors: '错误',
302
+ knowledgeBase: '知识库',
303
+ files: '文件数',
304
+ totalSize: '总大小',
305
+ rootPath: '根目录',
306
+ mcpStatus: '状态',
307
+ mcpRunning: '运行中',
308
+ mcpStopped: '已停止',
309
+ mcpPort: '端口',
310
+ autoRefresh: '每 5 秒自动刷新',
311
+ fetchError: '加载监控数据失败',
312
+ },
313
+ agents: {
314
+ mcpServer: 'MCP 服务器',
315
+ running: '运行中',
316
+ stopped: '未运行',
317
+ onPort: (port: number) => `端口 :${port}`,
318
+ refresh: '刷新',
319
+ refreshing: '刷新中...',
320
+ connected: '已连接',
321
+ connectedCount: (n: number) => `已连接 (${n})`,
322
+ detectedNotConfigured: '已检测未配置',
323
+ detectedCount: (n: number) => `已检测未配置 (${n})`,
324
+ notDetected: '未检测到',
325
+ notDetectedCount: (n: number) => `未检测到 (${n})`,
326
+ showAll: '显示全部',
327
+ hideAll: '收起',
328
+ connect: '连接',
329
+ noAgents: '本机未检测到任何 Agent。',
330
+ fetchError: '加载 Agent 数据失败',
331
+ autoRefresh: '每 30 秒自动刷新',
332
+ },
290
333
  save: '保存',
291
334
  saved: '已保存',
292
335
  saveFailed: '保存失败',
@@ -0,0 +1,81 @@
1
+ /**
2
+ * In-memory metrics collector for MindOS runtime observability.
3
+ *
4
+ * Singleton — import { metrics } from '@/lib/metrics' in any server-side code.
5
+ * Data resets when the process restarts (no persistence needed).
6
+ */
7
+
8
+ export interface MetricsSnapshot {
9
+ processStartTime: number;
10
+ agentRequests: number;
11
+ toolExecutions: number;
12
+ totalTokens: { input: number; output: number };
13
+ avgResponseTimeMs: number;
14
+ errors: number;
15
+ }
16
+
17
+ const MAX_RESPONSE_TIMES = 100;
18
+
19
+ class MetricsCollector {
20
+ private processStartTime = Date.now();
21
+ private agentRequests = 0;
22
+ private toolExecutions = 0;
23
+ private totalTokens = { input: 0, output: 0 };
24
+ private responseTimes: number[] = [];
25
+ private errors = 0;
26
+
27
+ /** Record a completed agent request with its duration. */
28
+ recordRequest(durationMs: number): void {
29
+ this.agentRequests++;
30
+ this.responseTimes.push(durationMs);
31
+ if (this.responseTimes.length > MAX_RESPONSE_TIMES) {
32
+ this.responseTimes.shift();
33
+ }
34
+ }
35
+
36
+ /** Increment the tool execution counter. */
37
+ recordToolExecution(): void {
38
+ this.toolExecutions++;
39
+ }
40
+
41
+ /** Accumulate token usage. */
42
+ recordTokens(input: number, output: number): void {
43
+ this.totalTokens.input += input;
44
+ this.totalTokens.output += output;
45
+ }
46
+
47
+ /** Increment the error counter. */
48
+ recordError(): void {
49
+ this.errors++;
50
+ }
51
+
52
+ /** Return a read-only snapshot of all metrics. */
53
+ getSnapshot(): MetricsSnapshot {
54
+ const avg =
55
+ this.responseTimes.length > 0
56
+ ? Math.round(this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length)
57
+ : 0;
58
+
59
+ return {
60
+ processStartTime: this.processStartTime,
61
+ agentRequests: this.agentRequests,
62
+ toolExecutions: this.toolExecutions,
63
+ totalTokens: { ...this.totalTokens },
64
+ avgResponseTimeMs: avg,
65
+ errors: this.errors,
66
+ };
67
+ }
68
+
69
+ /** Reset all counters (useful for testing). */
70
+ reset(): void {
71
+ this.processStartTime = Date.now();
72
+ this.agentRequests = 0;
73
+ this.toolExecutions = 0;
74
+ this.totalTokens = { input: 0, output: 0 };
75
+ this.responseTimes = [];
76
+ this.errors = 0;
77
+ }
78
+ }
79
+
80
+ /** Global singleton — shared across all requests in the same Node process. */
81
+ export const metrics = new MetricsCollector();
@@ -3,7 +3,7 @@ import path from "path";
3
3
 
4
4
  const nextConfig: NextConfig = {
5
5
  transpilePackages: ['github-slugger'],
6
- serverExternalPackages: ['pdfjs-dist', 'pdf-parse', 'chokidar'],
6
+ serverExternalPackages: ['pdfjs-dist', 'pdf-parse', 'chokidar', '@mariozechner/pi-ai', '@mariozechner/pi-agent-core'],
7
7
  outputFileTracingRoot: path.join(__dirname),
8
8
  turbopack: {
9
9
  root: path.join(__dirname),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
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",