@geminilight/mindos 0.6.22 → 0.6.25

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 (62) hide show
  1. package/README.md +58 -46
  2. package/README_zh.md +58 -46
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/route.ts +100 -0
  5. package/app/app/api/file/import/route.ts +0 -2
  6. package/app/app/api/setup/route.ts +2 -0
  7. package/app/components/Backlinks.tsx +2 -2
  8. package/app/components/Breadcrumb.tsx +1 -1
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/FileTree.tsx +14 -1
  12. package/app/components/GuideCard.tsx +6 -2
  13. package/app/components/HomeContent.tsx +2 -2
  14. package/app/components/RightAskPanel.tsx +17 -10
  15. package/app/components/SearchModal.tsx +3 -3
  16. package/app/components/SidebarLayout.tsx +4 -2
  17. package/app/components/SyncStatusBar.tsx +2 -2
  18. package/app/components/ask/AskContent.tsx +6 -6
  19. package/app/components/ask/FileChip.tsx +1 -1
  20. package/app/components/ask/MentionPopover.tsx +2 -2
  21. package/app/components/ask/MessageList.tsx +1 -1
  22. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  23. package/app/components/explore/UseCaseCard.tsx +2 -2
  24. package/app/components/help/HelpContent.tsx +6 -1
  25. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  26. package/app/components/panels/DiscoverPanel.tsx +3 -3
  27. package/app/components/panels/PanelNavRow.tsx +2 -2
  28. package/app/components/panels/PluginsPanel.tsx +1 -1
  29. package/app/components/panels/SearchPanel.tsx +3 -3
  30. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  31. package/app/components/settings/AiTab.tsx +4 -4
  32. package/app/components/settings/KnowledgeTab.tsx +1 -1
  33. package/app/components/settings/McpTab.tsx +22 -4
  34. package/app/components/settings/UpdateTab.tsx +1 -1
  35. package/app/components/setup/index.tsx +9 -3
  36. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  37. package/app/hooks/useAskPanel.ts +7 -3
  38. package/app/hooks/useFileImport.ts +1 -1
  39. package/app/lib/a2a/agent-card.ts +107 -0
  40. package/app/lib/a2a/index.ts +23 -0
  41. package/app/lib/a2a/task-handler.ts +228 -0
  42. package/app/lib/a2a/types.ts +158 -0
  43. package/app/lib/agent/tools.ts +1 -1
  44. package/app/lib/core/fs-ops.ts +3 -2
  45. package/app/lib/fs.ts +28 -11
  46. package/app/lib/i18n-en.ts +2 -0
  47. package/app/lib/i18n-zh.ts +2 -0
  48. package/app/lib/settings.ts +1 -1
  49. package/bin/cli.js +48 -20
  50. package/bin/commands/agent.js +18 -0
  51. package/bin/commands/api.js +58 -0
  52. package/bin/commands/ask.js +101 -0
  53. package/bin/commands/file.js +286 -0
  54. package/bin/commands/search.js +51 -0
  55. package/bin/commands/space.js +167 -0
  56. package/bin/commands/status.js +69 -0
  57. package/bin/lib/command.js +156 -0
  58. package/mcp/dist/index.cjs +1 -1
  59. package/mcp/src/index.ts +1 -1
  60. package/package.json +1 -1
  61. package/skills/mindos/SKILL.md +2 -2
  62. package/skills/mindos-zh/SKILL.md +2 -2
@@ -0,0 +1,107 @@
1
+ /**
2
+ * A2A Agent Card generator for MindOS.
3
+ * Builds the card dynamically from current settings and MCP tools.
4
+ */
5
+
6
+ import type { AgentCard, AgentSkill } from './types';
7
+
8
+ /** MindOS knowledge-base skills exposed via A2A */
9
+ const KB_SKILLS: AgentSkill[] = [
10
+ {
11
+ id: 'kb-search',
12
+ name: 'Search Knowledge Base',
13
+ description: 'Full-text search across all notes, files, and spaces in the knowledge base.',
14
+ tags: ['search', 'knowledge', 'notes'],
15
+ examples: ['Search for notes about machine learning', 'Find files mentioning project deadlines'],
16
+ inputModes: ['text/plain'],
17
+ outputModes: ['text/plain', 'application/json'],
18
+ },
19
+ {
20
+ id: 'kb-read',
21
+ name: 'Read Note',
22
+ description: 'Read the full content of a specific file in the knowledge base.',
23
+ tags: ['read', 'file', 'content'],
24
+ examples: ['Read the file at Projects/roadmap.md'],
25
+ inputModes: ['text/plain'],
26
+ outputModes: ['text/plain'],
27
+ },
28
+ {
29
+ id: 'kb-write',
30
+ name: 'Write Note',
31
+ description: 'Create or update a note in the knowledge base. Supports .md and .csv files.',
32
+ tags: ['write', 'create', 'update'],
33
+ examples: ['Create a new meeting note at Work/meetings/2026-03-30.md'],
34
+ inputModes: ['text/plain'],
35
+ outputModes: ['text/plain'],
36
+ },
37
+ {
38
+ id: 'kb-list',
39
+ name: 'List Files',
40
+ description: 'List files and directory structure of the knowledge base.',
41
+ tags: ['list', 'tree', 'structure'],
42
+ examples: ['Show the file tree', 'List all spaces'],
43
+ inputModes: ['text/plain'],
44
+ outputModes: ['application/json'],
45
+ },
46
+ {
47
+ id: 'kb-organize',
48
+ name: 'Organize Files',
49
+ description: 'AI-powered file organization into appropriate spaces and directories.',
50
+ tags: ['organize', 'ai', 'structure'],
51
+ examples: ['Organize imported files into relevant spaces'],
52
+ inputModes: ['text/plain'],
53
+ outputModes: ['text/plain'],
54
+ },
55
+ ];
56
+
57
+ /**
58
+ * Build the A2A Agent Card for this MindOS instance.
59
+ * @param baseUrl The publicly reachable base URL (e.g. http://localhost:3456)
60
+ */
61
+ export function buildAgentCard(baseUrl: string): AgentCard {
62
+ let version = process.env.npm_package_version || '0.0.0';
63
+
64
+ // Try reading version from package.json as fallback
65
+ if (version === '0.0.0') {
66
+ try {
67
+ const fs = require('fs');
68
+ const path = require('path');
69
+ const projRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
70
+ const pkg = JSON.parse(fs.readFileSync(path.join(projRoot, 'package.json'), 'utf-8'));
71
+ if (pkg.version) version = pkg.version;
72
+ } catch { /* use default */ }
73
+ }
74
+
75
+ return {
76
+ name: 'MindOS',
77
+ description: 'Personal knowledge management system with AI-powered Spaces, Instructions, and Skills. Store, organize, and retrieve knowledge through natural language.',
78
+ version,
79
+ provider: {
80
+ organization: 'MindOS',
81
+ url: baseUrl,
82
+ },
83
+ supportedInterfaces: [
84
+ {
85
+ url: `${baseUrl}/api/a2a`,
86
+ protocolBinding: 'JSONRPC',
87
+ protocolVersion: '1.0',
88
+ },
89
+ ],
90
+ capabilities: {
91
+ streaming: false, // Phase 1: no streaming
92
+ pushNotifications: false,
93
+ stateTransitionHistory: false,
94
+ },
95
+ defaultInputModes: ['text/plain'],
96
+ defaultOutputModes: ['text/plain', 'application/json'],
97
+ skills: KB_SKILLS,
98
+ securitySchemes: {
99
+ bearer: {
100
+ httpAuthSecurityScheme: {
101
+ scheme: 'Bearer',
102
+ },
103
+ },
104
+ },
105
+ securityRequirements: [{ bearer: [] }],
106
+ };
107
+ }
@@ -0,0 +1,23 @@
1
+ export { buildAgentCard } from './agent-card';
2
+ export { handleSendMessage, handleGetTask, handleCancelTask } from './task-handler';
3
+ export { A2A_ERRORS } from './types';
4
+ export type {
5
+ AgentCard,
6
+ AgentInterface,
7
+ AgentCapabilities,
8
+ AgentSkill,
9
+ SecurityScheme,
10
+ JsonRpcRequest,
11
+ JsonRpcResponse,
12
+ JsonRpcError,
13
+ TaskState,
14
+ MessageRole,
15
+ MessagePart,
16
+ A2AMessage,
17
+ TaskStatus,
18
+ TaskArtifact,
19
+ A2ATask,
20
+ SendMessageParams,
21
+ GetTaskParams,
22
+ CancelTaskParams,
23
+ } from './types';
@@ -0,0 +1,228 @@
1
+ /**
2
+ * A2A Task handler for MindOS.
3
+ * Routes A2A SendMessage requests to internal MCP tools.
4
+ */
5
+
6
+ import { randomUUID } from 'crypto';
7
+ import type {
8
+ A2ATask,
9
+ A2AMessage,
10
+ SendMessageParams,
11
+ GetTaskParams,
12
+ CancelTaskParams,
13
+ TaskState,
14
+ } from './types';
15
+
16
+ /* ── In-memory Task Store (Phase 1) ───────────────────────────────────── */
17
+ // NOTE: In-memory Map is lost on serverless cold starts / process restarts.
18
+ // Acceptable for Phase 1. Phase 2 should use persistent storage if needed.
19
+
20
+ const tasks = new Map<string, A2ATask>();
21
+ const MAX_TASKS = 1000;
22
+
23
+ function pruneOldTasks() {
24
+ if (tasks.size <= MAX_TASKS) return;
25
+ // Remove oldest completed tasks first
26
+ const entries = [...tasks.entries()].sort((a, b) =>
27
+ new Date(a[1].status.timestamp).getTime() - new Date(b[1].status.timestamp).getTime()
28
+ );
29
+ const toRemove = entries.slice(0, tasks.size - MAX_TASKS);
30
+ for (const [id] of toRemove) tasks.delete(id);
31
+ }
32
+
33
+ /* ── Skill Router ─────────────────────────────────────────────────────── */
34
+
35
+ interface SkillRoute {
36
+ pattern: RegExp;
37
+ tool: string;
38
+ extractParams: (text: string) => Record<string, string>;
39
+ }
40
+
41
+ const SKILL_ROUTES: SkillRoute[] = [
42
+ {
43
+ pattern: /^(?:search|find|look\s*up|query)\b/i,
44
+ tool: 'search_notes',
45
+ extractParams: (text) => ({ q: text.replace(/^(?:search|find|look\s*up|query)\s+(?:for\s+)?/i, '').trim() }),
46
+ },
47
+ {
48
+ pattern: /^(?:read|get|show|open|view)\s+(?:the\s+)?(?:file\s+)?(?:at\s+)?(.+\.(?:md|csv))/i,
49
+ tool: 'read_file',
50
+ extractParams: (text) => {
51
+ const match = text.match(/(?:at\s+)?([^\s]+\.(?:md|csv))/i);
52
+ return { path: match?.[1] ?? '' };
53
+ },
54
+ },
55
+ {
56
+ pattern: /^(?:list|show|tree)\s+(?:files|spaces|structure)/i,
57
+ tool: 'list_files',
58
+ extractParams: () => ({}),
59
+ },
60
+ {
61
+ pattern: /^(?:list|show)\s+spaces/i,
62
+ tool: 'list_spaces',
63
+ extractParams: () => ({}),
64
+ },
65
+ ];
66
+
67
+ function routeToTool(text: string): { tool: string; params: Record<string, string> } | null {
68
+ for (const route of SKILL_ROUTES) {
69
+ if (route.pattern.test(text)) {
70
+ return { tool: route.tool, params: route.extractParams(text) };
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /* ── Execute via internal API ─────────────────────────────────────────── */
77
+
78
+ const TOOL_TIMEOUT_MS = 10_000;
79
+
80
+ async function fetchWithTimeout(url: string): Promise<Response> {
81
+ const controller = new AbortController();
82
+ const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
83
+ try {
84
+ const res = await fetch(url, { signal: controller.signal });
85
+ return res;
86
+ } finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+
91
+ /** Sanitize file path: reject traversal attempts */
92
+ function sanitizePath(p: string): string {
93
+ if (!p || p.includes('..') || p.includes('\0')) throw new Error('Invalid path');
94
+ // Normalize double slashes and strip leading slashes
95
+ return p.replace(/\/\//g, '/').replace(/^\/+/, '');
96
+ }
97
+
98
+ async function executeTool(tool: string, params: Record<string, string>): Promise<string> {
99
+ const baseUrl = `http://localhost:${process.env.PORT || 3456}`;
100
+
101
+ switch (tool) {
102
+ case 'search_notes': {
103
+ const q = (params.q || '').slice(0, 500); // limit query length
104
+ const res = await fetchWithTimeout(`${baseUrl}/api/search?q=${encodeURIComponent(q)}`);
105
+ if (!res.ok) throw new Error(`Search failed: ${res.status}`);
106
+ const data = await res.json();
107
+ return JSON.stringify(data, null, 2);
108
+ }
109
+ case 'read_file': {
110
+ const safePath = sanitizePath(params.path || '');
111
+ const res = await fetchWithTimeout(`${baseUrl}/api/file?path=${encodeURIComponent(safePath)}`);
112
+ if (!res.ok) throw new Error(`Read failed: ${res.status}`);
113
+ const data = await res.json();
114
+ return typeof data.content === 'string' ? data.content : JSON.stringify(data);
115
+ }
116
+ case 'list_files': {
117
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
118
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
119
+ const data = await res.json();
120
+ return JSON.stringify(data, null, 2);
121
+ }
122
+ case 'list_spaces': {
123
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
124
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
125
+ const data = await res.json();
126
+ const spaces = (data.tree ?? data.files ?? []).filter((n: { isSpace?: boolean }) => n.isSpace);
127
+ return JSON.stringify(spaces, null, 2);
128
+ }
129
+ default:
130
+ throw new Error(`Unknown tool: ${tool}`);
131
+ }
132
+ }
133
+
134
+ /* ── Public API ────────────────────────────────────────────────────────── */
135
+
136
+ export async function handleSendMessage(params: SendMessageParams): Promise<A2ATask> {
137
+ const taskId = randomUUID();
138
+ const now = new Date().toISOString();
139
+
140
+ // Extract text from message parts
141
+ const text = params.message.parts
142
+ .map(p => p.text ?? (p.data ? JSON.stringify(p.data) : ''))
143
+ .join(' ')
144
+ .trim();
145
+
146
+ if (!text) {
147
+ const failedTask = createTask(taskId, 'TASK_STATE_FAILED', 'Empty message — no text content found.', now);
148
+ tasks.set(taskId, failedTask);
149
+ return failedTask;
150
+ }
151
+
152
+ // Create task in WORKING state
153
+ const task = createTask(taskId, 'TASK_STATE_WORKING', undefined, now);
154
+ task.history = [params.message];
155
+ tasks.set(taskId, task);
156
+ pruneOldTasks();
157
+
158
+ // Route to tool
159
+ const route = routeToTool(text);
160
+
161
+ try {
162
+ let result: string;
163
+ if (route) {
164
+ result = await executeTool(route.tool, route.params);
165
+ } else {
166
+ // Fallback: treat as search query
167
+ result = await executeTool('search_notes', { q: text });
168
+ }
169
+
170
+ // Update task to completed
171
+ task.status = {
172
+ state: 'TASK_STATE_COMPLETED',
173
+ timestamp: new Date().toISOString(),
174
+ };
175
+ task.artifacts = [{
176
+ artifactId: randomUUID(),
177
+ name: 'result',
178
+ parts: [{ text: result, mediaType: 'text/plain' }],
179
+ }];
180
+ task.history.push({
181
+ role: 'ROLE_AGENT',
182
+ parts: [{ text: result }],
183
+ });
184
+
185
+ return task;
186
+ } catch (err) {
187
+ task.status = {
188
+ state: 'TASK_STATE_FAILED',
189
+ message: {
190
+ role: 'ROLE_AGENT',
191
+ parts: [{ text: `Error: ${(err as Error).message}` }],
192
+ },
193
+ timestamp: new Date().toISOString(),
194
+ };
195
+ return task;
196
+ }
197
+ }
198
+
199
+ export function handleGetTask(params: GetTaskParams): A2ATask | null {
200
+ return tasks.get(params.id) ?? null;
201
+ }
202
+
203
+ export function handleCancelTask(params: CancelTaskParams): A2ATask | null {
204
+ const task = tasks.get(params.id);
205
+ if (!task) return null;
206
+
207
+ const terminalStates: TaskState[] = ['TASK_STATE_COMPLETED', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED'];
208
+ if (terminalStates.includes(task.status.state)) return null; // not cancelable
209
+
210
+ task.status = {
211
+ state: 'TASK_STATE_CANCELED',
212
+ timestamp: new Date().toISOString(),
213
+ };
214
+ return task;
215
+ }
216
+
217
+ /* ── Helpers ───────────────────────────────────────────────────────────── */
218
+
219
+ function createTask(id: string, state: TaskState, errorMessage: string | undefined, timestamp: string): A2ATask {
220
+ return {
221
+ id,
222
+ status: {
223
+ state,
224
+ timestamp,
225
+ ...(errorMessage ? { message: { role: 'ROLE_AGENT', parts: [{ text: errorMessage }] } } : {}),
226
+ },
227
+ };
228
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A2A Protocol v1.0 — Core Types for MindOS Agent Card & Task handling.
3
+ * Subset of the full spec: only what's needed for Phase 1 (Server mode).
4
+ * Reference: https://a2a-protocol.org/latest/specification/
5
+ */
6
+
7
+ /* ── Agent Card ────────────────────────────────────────────────────────── */
8
+
9
+ export interface AgentCard {
10
+ name: string;
11
+ description: string;
12
+ version: string;
13
+ provider: {
14
+ organization: string;
15
+ url: string;
16
+ };
17
+ supportedInterfaces: AgentInterface[];
18
+ capabilities: AgentCapabilities;
19
+ defaultInputModes: string[];
20
+ defaultOutputModes: string[];
21
+ skills: AgentSkill[];
22
+ securitySchemes?: Record<string, SecurityScheme>;
23
+ securityRequirements?: Record<string, string[]>[];
24
+ }
25
+
26
+ export interface AgentInterface {
27
+ url: string;
28
+ protocolBinding: 'JSONRPC' | 'GRPC' | 'HTTP_JSON';
29
+ protocolVersion: string;
30
+ }
31
+
32
+ export interface AgentCapabilities {
33
+ streaming: boolean;
34
+ pushNotifications: boolean;
35
+ stateTransitionHistory: boolean;
36
+ }
37
+
38
+ export interface AgentSkill {
39
+ id: string;
40
+ name: string;
41
+ description: string;
42
+ tags?: string[];
43
+ examples?: string[];
44
+ inputModes?: string[];
45
+ outputModes?: string[];
46
+ }
47
+
48
+ export interface SecurityScheme {
49
+ httpAuthSecurityScheme?: {
50
+ scheme: string;
51
+ bearerFormat?: string;
52
+ };
53
+ }
54
+
55
+ /* ── JSON-RPC ──────────────────────────────────────────────────────────── */
56
+
57
+ export interface JsonRpcRequest {
58
+ jsonrpc: '2.0';
59
+ id: string | number;
60
+ method: string;
61
+ params?: Record<string, unknown>;
62
+ }
63
+
64
+ export interface JsonRpcResponse {
65
+ jsonrpc: '2.0';
66
+ id: string | number | null;
67
+ result?: unknown;
68
+ error?: JsonRpcError;
69
+ }
70
+
71
+ export interface JsonRpcError {
72
+ code: number;
73
+ message: string;
74
+ data?: unknown;
75
+ }
76
+
77
+ /* ── A2A Messages & Tasks ──────────────────────────────────────────────── */
78
+
79
+ export type TaskState =
80
+ | 'TASK_STATE_SUBMITTED'
81
+ | 'TASK_STATE_WORKING'
82
+ | 'TASK_STATE_INPUT_REQUIRED'
83
+ | 'TASK_STATE_COMPLETED'
84
+ | 'TASK_STATE_FAILED'
85
+ | 'TASK_STATE_CANCELED'
86
+ | 'TASK_STATE_REJECTED';
87
+
88
+ export type MessageRole = 'ROLE_USER' | 'ROLE_AGENT';
89
+
90
+ export interface MessagePart {
91
+ text?: string;
92
+ data?: unknown;
93
+ mediaType?: string;
94
+ metadata?: Record<string, unknown>;
95
+ }
96
+
97
+ export interface A2AMessage {
98
+ role: MessageRole;
99
+ parts: MessagePart[];
100
+ metadata?: Record<string, unknown>;
101
+ }
102
+
103
+ export interface TaskStatus {
104
+ state: TaskState;
105
+ message?: A2AMessage;
106
+ timestamp: string;
107
+ }
108
+
109
+ export interface TaskArtifact {
110
+ artifactId: string;
111
+ name?: string;
112
+ description?: string;
113
+ parts: MessagePart[];
114
+ }
115
+
116
+ export interface A2ATask {
117
+ id: string;
118
+ contextId?: string;
119
+ status: TaskStatus;
120
+ artifacts?: TaskArtifact[];
121
+ history?: A2AMessage[];
122
+ metadata?: Record<string, unknown>;
123
+ }
124
+
125
+ /* ── A2A Method Params ─────────────────────────────────────────────────── */
126
+
127
+ export interface SendMessageParams {
128
+ message: A2AMessage;
129
+ configuration?: {
130
+ acceptedOutputModes?: string[];
131
+ blocking?: boolean;
132
+ historyLength?: number;
133
+ };
134
+ metadata?: Record<string, unknown>;
135
+ }
136
+
137
+ export interface GetTaskParams {
138
+ id: string;
139
+ historyLength?: number;
140
+ }
141
+
142
+ export interface CancelTaskParams {
143
+ id: string;
144
+ }
145
+
146
+ /* ── Error Codes ───────────────────────────────────────────────────────── */
147
+
148
+ export const A2A_ERRORS = {
149
+ TASK_NOT_FOUND: { code: -32001, message: 'Task not found' },
150
+ TASK_NOT_CANCELABLE: { code: -32002, message: 'Task not cancelable' },
151
+ UNSUPPORTED_OPERATION: { code: -32004, message: 'Unsupported operation' },
152
+ CONTENT_TYPE_NOT_SUPPORTED: { code: -32005, message: 'Content type not supported' },
153
+ PARSE_ERROR: { code: -32700, message: 'Parse error' },
154
+ INVALID_REQUEST: { code: -32600, message: 'Invalid request' },
155
+ METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
156
+ INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
157
+ INTERNAL_ERROR: { code: -32603, message: 'Internal error' },
158
+ } as const;
@@ -532,7 +532,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
532
532
  {
533
533
  name: 'create_file',
534
534
  label: 'Create File',
535
- description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically.',
535
+ description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically. Does NOT create Space scaffolding (INSTRUCTION.md/README.md). Use create_space to create a Space.',
536
536
  parameters: CreateFileParams,
537
537
  execute: safeExecute(async (_id, params: Static<typeof CreateFileParams>) => {
538
538
  createFile(params.path, params.content ?? '');
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolveSafe, assertWithinRoot } from './security';
4
4
  import { MindOSError, ErrorCodes } from '@/lib/errors';
5
- import { scaffoldIfNewSpace, cleanDirName, INSTRUCTION_TEMPLATE, README_TEMPLATE } from './space-scaffold';
5
+ import { cleanDirName, INSTRUCTION_TEMPLATE, README_TEMPLATE } from './space-scaffold';
6
6
 
7
7
  /**
8
8
  * Reads the content of a file given a relative path from mindRoot.
@@ -33,6 +33,8 @@ export function writeFile(mindRoot: string, filePath: string, content: string):
33
33
  /**
34
34
  * Creates a new file. Throws if the file already exists.
35
35
  * Creates parent directories as needed.
36
+ * NOTE: Does NOT auto-scaffold Space files (INSTRUCTION.md/README.md).
37
+ * Use createSpaceFilesystem() or convertToSpace() for explicit Space creation.
36
38
  */
37
39
  export function createFile(mindRoot: string, filePath: string, initialContent = ''): void {
38
40
  const resolved = resolveSafe(mindRoot, filePath);
@@ -41,7 +43,6 @@ export function createFile(mindRoot: string, filePath: string, initialContent =
41
43
  }
42
44
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
43
45
  fs.writeFileSync(resolved, initialContent, 'utf-8');
44
- scaffoldIfNewSpace(mindRoot, filePath);
45
46
  }
46
47
 
47
48
  /**
package/app/lib/fs.ts CHANGED
@@ -59,9 +59,36 @@ const CACHE_TTL_MS = 5_000; // 5 seconds
59
59
 
60
60
  let _treeVersion = 0;
61
61
 
62
+ function buildCache(root: string): FileTreeCache {
63
+ const tree = buildFileTree(root);
64
+ const allFiles: string[] = [];
65
+ function collect(nodes: FileNode[]) {
66
+ for (const n of nodes) {
67
+ if (n.type === 'file') allFiles.push(n.path);
68
+ else if (n.children) collect(n.children);
69
+ }
70
+ }
71
+ collect(tree);
72
+ return { tree, allFiles, timestamp: Date.now() };
73
+ }
74
+
75
+ function sameFileList(a: string[], b: string[]): boolean {
76
+ if (a.length !== b.length) return false;
77
+ const sa = [...a].sort();
78
+ const sb = [...b].sort();
79
+ return sa.every((p, i) => p === sb[i]);
80
+ }
81
+
62
82
  /** Monotonically increasing counter — bumped on every file mutation so the
63
83
  * client can cheaply detect changes without rebuilding the full tree. */
64
84
  export function getTreeVersion(): number {
85
+ if (_cache && !isCacheValid()) {
86
+ const next = buildCache(getMindRoot());
87
+ const changed = !sameFileList(_cache.allFiles, next.allFiles);
88
+ _cache = next;
89
+ _searchIndex = null;
90
+ if (changed) _treeVersion++;
91
+ }
65
92
  return _treeVersion;
66
93
  }
67
94
 
@@ -80,17 +107,7 @@ export function invalidateCache(): void {
80
107
  function ensureCache(): FileTreeCache {
81
108
  if (isCacheValid()) return _cache!;
82
109
  const root = getMindRoot();
83
- const tree = buildFileTree(root);
84
- // Extract all file paths from the tree to avoid a second full traversal.
85
- const allFiles: string[] = [];
86
- function collect(nodes: FileNode[]) {
87
- for (const n of nodes) {
88
- if (n.type === 'file') allFiles.push(n.path);
89
- else if (n.children) collect(n.children);
90
- }
91
- }
92
- collect(tree);
93
- _cache = { tree, allFiles, timestamp: Date.now() };
110
+ _cache = buildCache(root);
94
111
  return _cache;
95
112
  }
96
113
 
@@ -699,6 +699,8 @@ export const en = {
699
699
  newFile: 'New File',
700
700
  importFile: 'Import File',
701
701
  importToSpace: 'Import file to this space',
702
+ copyPath: 'Copy Path',
703
+ pathCopied: 'Path copied',
702
704
  },
703
705
  fileImport: {
704
706
  title: 'Import Files',
@@ -723,6 +723,8 @@ export const zh = {
723
723
  newFile: '新建文件',
724
724
  importFile: '导入文件',
725
725
  importToSpace: '导入文件到此空间',
726
+ copyPath: '复制路径',
727
+ pathCopied: '路径已复制',
726
728
  },
727
729
  fileImport: {
728
730
  title: '导入文件',
@@ -148,7 +148,7 @@ function parseGuideState(raw: unknown): GuideState | undefined {
148
148
  askedAI: obj.askedAI === true,
149
149
  nextStepIndex: typeof obj.nextStepIndex === 'number' ? obj.nextStepIndex : 0,
150
150
  walkthroughStep: typeof obj.walkthroughStep === 'number' ? obj.walkthroughStep : undefined,
151
- walkthroughDismissed: obj.walkthroughDismissed === true ? true : undefined,
151
+ walkthroughDismissed: typeof obj.walkthroughDismissed === 'boolean' ? obj.walkthroughDismissed : undefined,
152
152
  };
153
153
  }
154
154