@geminilight/mindos 0.6.7 → 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.
@@ -401,7 +401,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
401
401
  messages: requestMessages,
402
402
  currentFile,
403
403
  attachedFiles,
404
- uploadedFiles: upload.localAttachments,
404
+ uploadedFiles: upload.localAttachments.map(f => ({
405
+ name: f.name,
406
+ content: f.content.length > 20_000
407
+ ? f.content.slice(0, 20_000) + '\n\n[...truncated to first ~20000 chars]'
408
+ : f.content,
409
+ })),
405
410
  }),
406
411
  signal: controller.signal,
407
412
  });
@@ -0,0 +1,338 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import type { LocalAttachment, Message } from '@/lib/types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Types
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type OrganizePhase = 'idle' | 'organizing' | 'done' | 'error';
11
+
12
+ export interface OrganizeFileChange {
13
+ action: 'create' | 'update' | 'unknown';
14
+ path: string;
15
+ toolCallId: string;
16
+ /** Whether the tool call completed successfully */
17
+ ok: boolean;
18
+ }
19
+
20
+ /** User-facing stage hint derived from SSE events */
21
+ export type OrganizeStageHint =
22
+ | 'connecting'
23
+ | 'analyzing'
24
+ | 'reading'
25
+ | 'thinking'
26
+ | 'writing';
27
+
28
+ export interface AiOrganizeState {
29
+ phase: OrganizePhase;
30
+ changes: OrganizeFileChange[];
31
+ /** Current tool being executed (for live progress display) */
32
+ currentTool: { name: string; path: string } | null;
33
+ /** User-facing stage hint with optional context (e.g. file being read) */
34
+ stageHint: { stage: OrganizeStageHint; detail?: string } | null;
35
+ /** AI's text summary of what it did */
36
+ summary: string;
37
+ error: string | null;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // SSE stream parser — extracts file operations from /api/ask stream
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Strip model chain-of-thought tags that should never be shown to users.
46
+ * Handles both complete `<thinking>...</thinking>` blocks and unclosed trailing tags.
47
+ */
48
+ export function stripThinkingTags(text: string): string {
49
+ let cleaned = text.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
50
+ cleaned = cleaned.replace(/<thinking>[\s\S]*$/gi, '');
51
+ return cleaned.trim();
52
+ }
53
+
54
+ export const CLIENT_TRUNCATE_CHARS = 20_000;
55
+
56
+ const FILE_WRITE_TOOLS = new Set([
57
+ 'create_file', 'write_file', 'batch_create_files',
58
+ 'append_to_file', 'insert_after_heading', 'update_section',
59
+ 'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
60
+ ]);
61
+
62
+ const FILE_READ_TOOLS = new Set([
63
+ 'read_file', 'read_lines', 'search', 'list_files',
64
+ ]);
65
+
66
+ /**
67
+ * Derive a user-facing stage hint from an SSE event.
68
+ * Returns null for events that don't change the stage (e.g. tool_end, done).
69
+ */
70
+ export function deriveStageHint(
71
+ eventType: string,
72
+ toolName: string | undefined,
73
+ args: unknown,
74
+ ): { stage: OrganizeStageHint; detail?: string } | null {
75
+ if (eventType === 'text_delta') {
76
+ return { stage: 'analyzing' };
77
+ }
78
+ if (eventType === 'tool_start' && toolName) {
79
+ if (FILE_WRITE_TOOLS.has(toolName)) {
80
+ return { stage: 'writing', detail: extractPathFromArgs(toolName, args) || undefined };
81
+ }
82
+ if (FILE_READ_TOOLS.has(toolName)) {
83
+ const detail = (args && typeof args === 'object' && 'path' in args && typeof (args as Record<string, unknown>).path === 'string')
84
+ ? (args as Record<string, unknown>).path as string
85
+ : undefined;
86
+ return { stage: 'reading', detail };
87
+ }
88
+ return { stage: 'analyzing' };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function extractPathFromArgs(toolName: string, args: unknown): string {
94
+ if (!args || typeof args !== 'object') return '';
95
+ const a = args as Record<string, unknown>;
96
+ if (typeof a.path === 'string') return a.path;
97
+ if (typeof a.from_path === 'string') return a.from_path;
98
+ if (toolName === 'batch_create_files' && Array.isArray(a.files)) {
99
+ return (a.files as Array<{ path?: string }>)
100
+ .map(f => f.path ?? '')
101
+ .filter(Boolean)
102
+ .join(', ');
103
+ }
104
+ return '';
105
+ }
106
+
107
+ async function consumeOrganizeStream(
108
+ body: ReadableStream<Uint8Array>,
109
+ onProgress: (state: Partial<AiOrganizeState> & { summary?: string }) => void,
110
+ signal?: AbortSignal,
111
+ ): Promise<{ changes: OrganizeFileChange[]; summary: string; toolCallCount: number }> {
112
+ const reader = body.getReader();
113
+ const decoder = new TextDecoder();
114
+ let buffer = '';
115
+
116
+ const changes: OrganizeFileChange[] = [];
117
+ const pendingTools = new Map<string, { name: string; path: string; action: 'create' | 'update' | 'unknown' }>();
118
+ let summary = '';
119
+ let toolCallCount = 0;
120
+
121
+ try {
122
+ while (true) {
123
+ if (signal?.aborted) break;
124
+ const { done, value } = await reader.read();
125
+ if (done) break;
126
+
127
+ buffer += decoder.decode(value, { stream: true });
128
+ const lines = buffer.split('\n');
129
+ buffer = lines.pop() ?? '';
130
+
131
+ for (const line of lines) {
132
+ const trimmed = line.trim();
133
+ if (!trimmed.startsWith('data:')) continue;
134
+ const jsonStr = trimmed.slice(5).trim();
135
+ if (!jsonStr) continue;
136
+
137
+ let event: Record<string, unknown>;
138
+ try { event = JSON.parse(jsonStr); } catch { continue; }
139
+
140
+ const type = event.type as string;
141
+
142
+ switch (type) {
143
+ case 'tool_start': {
144
+ const toolName = event.toolName as string;
145
+ const toolCallId = event.toolCallId as string;
146
+ const args = event.args;
147
+ toolCallCount++;
148
+
149
+ const hint = deriveStageHint(type, toolName, args);
150
+ if (hint) onProgress({ stageHint: hint });
151
+
152
+ if (FILE_WRITE_TOOLS.has(toolName)) {
153
+ const path = extractPathFromArgs(toolName, args);
154
+ let action: 'create' | 'update' | 'unknown' = 'update';
155
+ if (toolName === 'create_file' || toolName === 'batch_create_files') action = 'create';
156
+ else if (toolName === 'delete_file' || toolName === 'rename_file' || toolName === 'move_file') action = 'unknown';
157
+ pendingTools.set(toolCallId, { name: toolName, path, action });
158
+ onProgress({ currentTool: { name: toolName, path } });
159
+ }
160
+ break;
161
+ }
162
+
163
+ case 'tool_end': {
164
+ const toolCallId = event.toolCallId as string;
165
+ const isError = !!event.isError;
166
+ const pending = pendingTools.get(toolCallId);
167
+ if (pending) {
168
+ if (pending.name === 'batch_create_files') {
169
+ for (const p of pending.path.split(', ').filter(Boolean)) {
170
+ changes.push({ action: pending.action, path: p, toolCallId, ok: !isError });
171
+ }
172
+ } else {
173
+ changes.push({ action: pending.action, path: pending.path, toolCallId, ok: !isError });
174
+ }
175
+ pendingTools.delete(toolCallId);
176
+ onProgress({ changes: [...changes], currentTool: null });
177
+ }
178
+ break;
179
+ }
180
+
181
+ case 'text_delta': {
182
+ summary += (event.delta as string) ?? '';
183
+ onProgress({ stageHint: { stage: 'analyzing' }, summary: stripThinkingTags(summary) });
184
+ break;
185
+ }
186
+
187
+ case 'error': {
188
+ throw new Error((event.message as string) || 'AI organize failed');
189
+ }
190
+
191
+ case 'done':
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ } finally {
197
+ reader.releaseLock();
198
+ }
199
+
200
+ return { changes, summary: stripThinkingTags(summary), toolCallCount };
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Hook
205
+ // ---------------------------------------------------------------------------
206
+
207
+ export function useAiOrganize() {
208
+ const [phase, setPhase] = useState<OrganizePhase>('idle');
209
+ const [changes, setChanges] = useState<OrganizeFileChange[]>([]);
210
+ const [currentTool, setCurrentTool] = useState<{ name: string; path: string } | null>(null);
211
+ const [stageHint, setStageHint] = useState<{ stage: OrganizeStageHint; detail?: string } | null>(null);
212
+ const [summary, setSummary] = useState('');
213
+ const [error, setError] = useState<string | null>(null);
214
+ const [toolCallCount, setToolCallCount] = useState(0);
215
+ const abortRef = useRef<AbortController | null>(null);
216
+ const lastEventRef = useRef<number>(0);
217
+
218
+ const start = useCallback(async (files: LocalAttachment[], prompt: string) => {
219
+ setPhase('organizing');
220
+ setChanges([]);
221
+ setCurrentTool(null);
222
+ setStageHint({ stage: 'connecting' });
223
+ setSummary('');
224
+ setError(null);
225
+ setToolCallCount(0);
226
+ lastEventRef.current = Date.now();
227
+
228
+ const controller = new AbortController();
229
+ abortRef.current = controller;
230
+
231
+ const messages: Message[] = [{ role: 'user', content: prompt }];
232
+
233
+ const truncatedFiles = files.map(f => ({
234
+ name: f.name,
235
+ content: f.content.length > CLIENT_TRUNCATE_CHARS
236
+ ? f.content.slice(0, CLIENT_TRUNCATE_CHARS) + '\n\n[...truncated to first ~20000 chars]'
237
+ : f.content,
238
+ }));
239
+
240
+ try {
241
+ const res = await fetch('/api/ask', {
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({
245
+ messages,
246
+ uploadedFiles: truncatedFiles,
247
+ maxSteps: 15,
248
+ }),
249
+ signal: controller.signal,
250
+ });
251
+
252
+ if (!res.ok) {
253
+ let errorMsg = `Request failed (${res.status})`;
254
+ try {
255
+ const errBody = await res.json() as { error?: { message?: string } | string; message?: string };
256
+ if (typeof errBody?.error === 'string') errorMsg = errBody.error;
257
+ else if (typeof errBody?.error === 'object' && errBody.error?.message) errorMsg = errBody.error.message;
258
+ else if (errBody?.message) errorMsg = errBody.message as string;
259
+ } catch {}
260
+ throw new Error(errorMsg);
261
+ }
262
+
263
+ if (!res.body) throw new Error('No response body');
264
+
265
+ const result = await consumeOrganizeStream(
266
+ res.body,
267
+ (partial) => {
268
+ lastEventRef.current = Date.now();
269
+ if (partial.changes) setChanges(partial.changes);
270
+ if (partial.currentTool !== undefined) setCurrentTool(partial.currentTool);
271
+ if (partial.stageHint) setStageHint(partial.stageHint);
272
+ if (partial.summary !== undefined) setSummary(partial.summary);
273
+ },
274
+ controller.signal,
275
+ );
276
+
277
+ setChanges(result.changes);
278
+ setSummary(result.summary);
279
+ setToolCallCount(result.toolCallCount);
280
+ setCurrentTool(null);
281
+ setPhase('done');
282
+ } catch (err) {
283
+ if ((err as Error).name === 'AbortError') {
284
+ setPhase('idle');
285
+ } else {
286
+ setError((err as Error).message);
287
+ setPhase('error');
288
+ }
289
+ } finally {
290
+ abortRef.current = null;
291
+ }
292
+ }, []);
293
+
294
+ const abort = useCallback(() => {
295
+ abortRef.current?.abort();
296
+ }, []);
297
+
298
+ const undoAll = useCallback(async (): Promise<number> => {
299
+ const createdFiles = changes.filter(c => c.action === 'create' && c.ok);
300
+ let reverted = 0;
301
+ for (const file of createdFiles) {
302
+ try {
303
+ const res = await fetch('/api/file', {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({ op: 'delete_file', path: file.path }),
307
+ });
308
+ if (res.ok) reverted++;
309
+ } catch {}
310
+ }
311
+ return reverted;
312
+ }, [changes]);
313
+
314
+ const reset = useCallback(() => {
315
+ setPhase('idle');
316
+ setChanges([]);
317
+ setCurrentTool(null);
318
+ setStageHint(null);
319
+ setSummary('');
320
+ setError(null);
321
+ setToolCallCount(0);
322
+ lastEventRef.current = 0;
323
+ }, []);
324
+
325
+ return {
326
+ phase,
327
+ changes,
328
+ currentTool,
329
+ stageHint,
330
+ summary,
331
+ error,
332
+ toolCallCount,
333
+ start,
334
+ abort,
335
+ undoAll,
336
+ reset,
337
+ };
338
+ }
@@ -4,7 +4,7 @@ import { useState, useCallback, useRef } from 'react';
4
4
  import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
5
5
 
6
6
  export type ImportIntent = 'archive' | 'digest';
7
- export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
7
+ export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done' | 'organizing' | 'organize_review';
8
8
  export type ConflictMode = 'skip' | 'rename' | 'overwrite';
9
9
 
10
10
  export interface ImportFile {
@@ -31,6 +31,37 @@ function formatSize(bytes: number): string {
31
31
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
32
32
  }
33
33
 
34
+ function fileToBase64(file: File): Promise<string> {
35
+ return new Promise((resolve, reject) => {
36
+ const reader = new FileReader();
37
+ reader.onload = () => {
38
+ const dataUrl = reader.result as string;
39
+ const commaIdx = dataUrl.indexOf(',');
40
+ resolve(commaIdx >= 0 ? dataUrl.slice(commaIdx + 1) : '');
41
+ };
42
+ reader.onerror = () => reject(reader.error ?? new Error('FileReader error'));
43
+ reader.readAsDataURL(file);
44
+ });
45
+ }
46
+
47
+ async function extractPdfText(file: File): Promise<string> {
48
+ const base64 = await fileToBase64(file);
49
+ if (!base64) throw new Error('Empty PDF file');
50
+
51
+ const res = await fetch('/api/extract-pdf', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ name: file.name, dataBase64: base64 }),
55
+ });
56
+ if (!res.ok) {
57
+ const err = await res.json().catch(() => ({}));
58
+ throw new Error((err as { error?: string }).error || 'PDF extraction failed');
59
+ }
60
+ const data = await res.json() as { text?: string; extracted?: boolean };
61
+ if (!data.extracted || !data.text) throw new Error('No text extracted from PDF');
62
+ return data.text;
63
+ }
64
+
34
65
  export function useFileImport() {
35
66
  const [files, setFiles] = useState<ImportFile[]>([]);
36
67
  const [step, setStep] = useState<ImportStep>('select');
@@ -85,7 +116,13 @@ export function useFileImport() {
85
116
  for (const f of newFiles) {
86
117
  if (f.error) continue;
87
118
  try {
88
- const text = await f.file.text();
119
+ const ext = getExt(f.name);
120
+ let text: string;
121
+ if (ext === '.pdf') {
122
+ text = await extractPdfText(f.file);
123
+ } else {
124
+ text = await f.file.text();
125
+ }
89
126
  setFiles(prev => prev.map(p =>
90
127
  p.name === f.name && p.size === f.size
91
128
  ? { ...p, content: text, loading: false }
@@ -718,10 +718,47 @@ export const en = {
718
718
  dropOverlay: 'Drop files to import into knowledge base',
719
719
  dropOverlayFormats: 'Supports .md .txt .pdf .csv .json .yaml .html',
720
720
  onboardingHint: 'Already have notes? Import files →',
721
- digestPromptSingle: (name: string) => `Please read ${name}, extract key information and organize it into the appropriate place in my knowledge base.`,
722
- digestPromptMulti: (n: number) => `Please read these ${n} files, extract key information and organize each into the appropriate place in my knowledge base.`,
721
+ digestPromptSingle: (name: string, targetSpace?: string) => {
722
+ const loc = targetSpace ? ` under the "${targetSpace}" space` : '';
723
+ return `The user uploaded "${name}". You MUST:\n1. Read the content from the "USER-UPLOADED FILES" section above\n2. Extract and reorganize the key information into well-structured Markdown notes\n3. Save the result${loc} in the knowledge base — create new files or update existing ones as appropriate\n\nDo NOT just reply with a text summary. You must actually write to the knowledge base.`;
724
+ },
725
+ digestPromptMulti: (n: number, targetSpace?: string) => {
726
+ const loc = targetSpace ? ` under the "${targetSpace}" space` : '';
727
+ return `The user uploaded ${n} files. You MUST:\n1. Read their content from the "USER-UPLOADED FILES" section above\n2. Extract and reorganize the key information into well-structured Markdown notes\n3. Save the results${loc} in the knowledge base — create new files or update existing ones as appropriate\n\nDo NOT just reply with a text summary. You must actually write to the knowledge base.`;
728
+ },
723
729
  arrowTo: '→',
724
730
  remove: 'Remove',
731
+ conflictsFound: (n: number) => `${n} file${n !== 1 ? 's' : ''} already exist${n === 1 ? 's' : ''}`,
732
+ organizeTitle: 'AI Organizing',
733
+ organizeProcessing: 'AI is analyzing and organizing your files...',
734
+ organizeConnecting: 'Connecting to AI...',
735
+ organizeAnalyzing: 'AI is analyzing your files...',
736
+ organizeReading: (detail?: string) => detail ? `Reading ${detail}...` : 'Reading files...',
737
+ organizeThinking: 'AI is thinking deeply...',
738
+ organizeWriting: (detail?: string) => detail ? `Writing ${detail}...` : 'Writing files...',
739
+ organizeElapsed: (seconds: number) => {
740
+ const m = Math.floor(seconds / 60);
741
+ const s = seconds % 60;
742
+ return `${m}:${s.toString().padStart(2, '0')}`;
743
+ },
744
+ organizeCancel: 'Cancel',
745
+ organizeMinimize: 'Continue browsing',
746
+ organizeExpand: 'View',
747
+ organizeCreating: (path: string) => `Creating ${path}`,
748
+ organizeUpdating: (path: string) => `Updating ${path}`,
749
+ organizeReviewTitle: 'Organization Complete',
750
+ organizeErrorTitle: 'Organization Failed',
751
+ organizeReviewDesc: (n: number) => `AI organized your files into ${n} change${n !== 1 ? 's' : ''}`,
752
+ organizeCreated: 'Created',
753
+ organizeUpdated: 'Updated',
754
+ organizeFailed: 'Failed',
755
+ organizeNoChanges: 'AI analyzed your files but made no changes.',
756
+ organizeToolCallsInfo: (n: number) => `AI executed ${n} operation${n > 1 ? 's' : ''} — check knowledge base for updates`,
757
+ organizeError: 'Organization failed',
758
+ organizeRetry: 'Retry',
759
+ organizeDone: 'Done',
760
+ organizeUndoAll: 'Undo All',
761
+ organizeUndoSuccess: (n: number) => `Reverted ${n} file${n !== 1 ? 's' : ''}`,
725
762
  },
726
763
  dirView: {
727
764
  gridView: 'Grid view',
@@ -742,10 +742,47 @@ export const zh = {
742
742
  dropOverlay: '松开鼠标,导入文件到知识库',
743
743
  dropOverlayFormats: '支持 .md .txt .pdf .csv .json .yaml .html',
744
744
  onboardingHint: '已有笔记?导入文件到知识库 →',
745
- digestPromptSingle: (name: string) => `请阅读 ${name},提取关键信息整理到知识库中合适的位置。`,
746
- digestPromptMulti: (n: number) => `请阅读这 ${n} 个文件,提取关键信息分别整理到知识库中合适的位置。`,
745
+ digestPromptSingle: (name: string, targetSpace?: string) => {
746
+ const loc = targetSpace ? `"${targetSpace}" 空间下` : '知识库中合适的位置';
747
+ return `用户上传了「${name}」。你必须:\n1. 从上方「USER-UPLOADED FILES」区域读取文件内容\n2. 提取和重新整理关键信息为结构清晰的 Markdown 笔记\n3. 将整理后的内容保存到${loc}——可以创建新文件,也可以更新已有文件\n\n不要只做文字回复。你必须实际写入知识库。`;
748
+ },
749
+ digestPromptMulti: (n: number, targetSpace?: string) => {
750
+ const loc = targetSpace ? `"${targetSpace}" 空间下` : '知识库中合适的位置';
751
+ return `用户上传了 ${n} 个文件。你必须:\n1. 从上方「USER-UPLOADED FILES」区域读取它们的内容\n2. 提取和重新整理关键信息为结构清晰的 Markdown 笔记\n3. 将整理后的内容保存到${loc}——可以创建新文件,也可以更新已有文件\n\n不要只做文字回复。你必须实际写入知识库。`;
752
+ },
747
753
  arrowTo: '→',
748
754
  remove: '移除',
755
+ conflictsFound: (n: number) => `${n} 个文件已存在`,
756
+ organizeTitle: 'AI 整理中',
757
+ organizeProcessing: 'AI 正在分析和整理你的文件...',
758
+ organizeConnecting: '正在连接 AI...',
759
+ organizeAnalyzing: 'AI 正在分析你的文件...',
760
+ organizeReading: (detail?: string) => detail ? `正在阅读 ${detail}...` : '正在阅读文件...',
761
+ organizeThinking: 'AI 正在深度思考...',
762
+ organizeWriting: (detail?: string) => detail ? `正在写入 ${detail}...` : '正在写入文件...',
763
+ organizeElapsed: (seconds: number) => {
764
+ const m = Math.floor(seconds / 60);
765
+ const s = seconds % 60;
766
+ return `${m}:${s.toString().padStart(2, '0')}`;
767
+ },
768
+ organizeCancel: '取消',
769
+ organizeMinimize: '继续浏览',
770
+ organizeExpand: '查看',
771
+ organizeCreating: (path: string) => `正在创建 ${path}`,
772
+ organizeUpdating: (path: string) => `正在更新 ${path}`,
773
+ organizeReviewTitle: '整理完成',
774
+ organizeErrorTitle: '整理失败',
775
+ organizeReviewDesc: (n: number) => `AI 整理了 ${n} 处变更`,
776
+ organizeCreated: '已创建',
777
+ organizeUpdated: '已更新',
778
+ organizeFailed: '失败',
779
+ organizeNoChanges: 'AI 分析了你的文件,但没有做任何更改。',
780
+ organizeToolCallsInfo: (n: number) => `AI 执行了 ${n} 个操作 — 请检查知识库查看更新`,
781
+ organizeError: '整理失败',
782
+ organizeRetry: '重试',
783
+ organizeDone: '完成',
784
+ organizeUndoAll: '撤销全部',
785
+ organizeUndoSuccess: (n: number) => `已撤销 ${n} 个文件`,
749
786
  },
750
787
  dirView: {
751
788
  gridView: '网格视图',
package/app/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "scripts": {
6
6
  "dev": "next dev -p ${MINDOS_WEB_PORT:-3456}",
7
7
  "prebuild": "node ../scripts/gen-renderer-index.js",
8
- "build": "next build",
8
+ "build": "next build --webpack",
9
9
  "start": "next start -p ${MINDOS_WEB_PORT:-3456}",
10
10
  "lint": "eslint",
11
11
  "test": "vitest run",
package/bin/cli.js CHANGED
@@ -140,7 +140,7 @@ if (cmd === '--version' || cmd === '-v') {
140
140
 
141
141
  const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
142
142
  const isVerbose = process.argv.includes('--verbose');
143
- const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
143
+ const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose' && a !== '--turbo').join(' ');
144
144
 
145
145
  const commands = {
146
146
  // ── onboard ────────────────────────────────────────────────────────────────
@@ -267,10 +267,12 @@ const commands = {
267
267
  // ── dev ────────────────────────────────────────────────────────────────────
268
268
  dev: async () => {
269
269
  loadConfig();
270
+ if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
271
+ if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
270
272
  process.env.MINDOS_CLI_PATH = resolve(ROOT, 'bin', 'cli.js');
271
273
  process.env.MINDOS_NODE_BIN = process.execPath;
272
- const webPort = process.env.MINDOS_WEB_PORT || '3456';
273
- const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
274
+ const webPort = process.env.MINDOS_WEB_PORT;
275
+ const mcpPort = process.env.MINDOS_MCP_PORT;
274
276
  await assertPortFree(Number(webPort), 'web');
275
277
  await assertPortFree(Number(mcpPort), 'mcp');
276
278
  ensureAppDeps();
@@ -303,8 +305,10 @@ const commands = {
303
305
  console.warn(yellow('Warning: daemon mode not supported on this platform. Falling back to foreground.'));
304
306
  } else {
305
307
  loadConfig();
306
- const webPort = process.env.MINDOS_WEB_PORT || '3456';
307
- const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
308
+ if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
309
+ if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
310
+ const webPort = process.env.MINDOS_WEB_PORT;
311
+ const mcpPort = process.env.MINDOS_MCP_PORT;
308
312
  console.log(cyan(`Installing MindOS as a background service (${platform})...`));
309
313
  await runGatewayCommand('install');
310
314
  // install() already starts the service via launchctl bootstrap + RunAtLoad=true.
@@ -334,8 +338,10 @@ const commands = {
334
338
  }
335
339
  }
336
340
  loadConfig();
337
- const webPort = process.env.MINDOS_WEB_PORT || '3456';
338
- const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
341
+ if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
342
+ if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
343
+ const webPort = process.env.MINDOS_WEB_PORT;
344
+ const mcpPort = process.env.MINDOS_MCP_PORT;
339
345
 
340
346
  // ── Auto-migrate user-rules.md to root user-skill-rules.md ─────────────
341
347
  try {
@@ -382,7 +388,7 @@ const commands = {
382
388
  console.log(yellow('Building MindOS (first run or new version detected)...\n'));
383
389
  cleanNextDir();
384
390
  run('node scripts/gen-renderer-index.js', ROOT);
385
- run(`${NEXT_BIN} build`, resolve(ROOT, 'app'));
391
+ run(`${NEXT_BIN} build --webpack`, resolve(ROOT, 'app'));
386
392
  writeBuildStamp();
387
393
  }
388
394
  const mcp = spawnMcp(isVerbose);
@@ -406,7 +412,7 @@ const commands = {
406
412
  ensureAppDeps();
407
413
  cleanNextDir();
408
414
  run('node scripts/gen-renderer-index.js', ROOT);
409
- run(`${NEXT_BIN} build ${extra}`, resolve(ROOT, 'app'));
415
+ run(`${NEXT_BIN} build --webpack ${extra}`, resolve(ROOT, 'app'));
410
416
  writeBuildStamp();
411
417
  },
412
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
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",
@@ -21,7 +21,7 @@ echo ""
21
21
  # 3. Verify Next.js build
22
22
  echo "🔨 Verifying Next.js build..."
23
23
  cd app
24
- if npx next build 2>&1 | tail -5; then
24
+ if npx next build --webpack 2>&1 | tail -5; then
25
25
  echo " ✅ Next.js build succeeded"
26
26
  else
27
27
  echo "❌ Next.js build failed"