@geminilight/mindos 0.6.32 → 0.6.33

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.
package/app/lib/fs.ts CHANGED
@@ -24,6 +24,10 @@ import {
24
24
  gitLog as coreGitLog,
25
25
  gitShowFile as coreGitShowFile,
26
26
  invalidateSearchIndex,
27
+ updateSearchIndexFile,
28
+ addSearchIndexFile,
29
+ removeSearchIndexFile,
30
+ LinkIndex,
27
31
  summarizeTopLevelSpaces,
28
32
  appendContentChange as coreAppendContentChange,
29
33
  listContentChanges as coreListContentChanges,
@@ -96,12 +100,62 @@ function isCacheValid(): boolean {
96
100
  return _cache !== null && (Date.now() - _cache.timestamp) < CACHE_TTL_MS;
97
101
  }
98
102
 
103
+ /** Module-level link index singleton. Lazily built on first graph/backlink access. */
104
+ const _linkIndex = new LinkIndex();
105
+
106
+ /** Get the link index, ensuring it's built for the current mindRoot. */
107
+ export function getLinkIndex(): LinkIndex {
108
+ const root = getMindRoot();
109
+ if (!_linkIndex.isBuiltFor(root)) {
110
+ _linkIndex.rebuild(root);
111
+ }
112
+ return _linkIndex;
113
+ }
114
+
99
115
  /** Invalidate cache — call after any write/create/delete/rename operation */
100
116
  export function invalidateCache(): void {
101
117
  _cache = null;
102
118
  _searchIndex = null;
103
119
  _treeVersion++;
104
120
  invalidateSearchIndex();
121
+ _linkIndex.invalidate();
122
+ }
123
+
124
+ /**
125
+ * Invalidate cache after a single file was modified (content write, line edit, append).
126
+ * Tree cache is cleared (file list/mtime changed), but search index is updated
127
+ * incrementally for just this file — O(tokens) instead of O(all-files).
128
+ */
129
+ function invalidateCacheForFile(filePath: string): void {
130
+ _cache = null;
131
+ _searchIndex = null;
132
+ _treeVersion++;
133
+ updateSearchIndexFile(getMindRoot(), filePath);
134
+ if (_linkIndex.isBuilt()) _linkIndex.updateFile(getMindRoot(), filePath);
135
+ }
136
+
137
+ /**
138
+ * Invalidate cache after a new file was created.
139
+ * Tree cache is cleared, search index gets incremental addFile.
140
+ */
141
+ function invalidateCacheForNewFile(filePath: string): void {
142
+ _cache = null;
143
+ _searchIndex = null;
144
+ _treeVersion++;
145
+ addSearchIndexFile(getMindRoot(), filePath);
146
+ if (_linkIndex.isBuilt()) _linkIndex.updateFile(getMindRoot(), filePath);
147
+ }
148
+
149
+ /**
150
+ * Invalidate cache after a file was deleted.
151
+ * Tree cache is cleared, search index gets incremental removeFile.
152
+ */
153
+ function invalidateCacheForDeletedFile(filePath: string): void {
154
+ _cache = null;
155
+ _searchIndex = null;
156
+ _treeVersion++;
157
+ removeSearchIndexFile(filePath);
158
+ if (_linkIndex.isBuilt()) _linkIndex.removeFile(filePath);
105
159
  }
106
160
 
107
161
  function ensureCache(): FileTreeCache {
@@ -336,18 +390,18 @@ export function getFileContent(filePath: string): string {
336
390
  /** Atomically writes content to a file given a relative path from MIND_ROOT. */
337
391
  export function saveFileContent(filePath: string, content: string): void {
338
392
  coreWriteFile(getMindRoot(), filePath, content);
339
- invalidateCache();
393
+ invalidateCacheForFile(filePath);
340
394
  }
341
395
 
342
396
  /** Creates a new file at the given relative path. Creates parent dirs as needed. */
343
397
  export function createFile(filePath: string, initialContent = ''): void {
344
398
  coreCreateFile(getMindRoot(), filePath, initialContent);
345
- invalidateCache();
399
+ invalidateCacheForNewFile(filePath);
346
400
  }
347
401
 
348
402
  export function deleteFile(filePath: string): void {
349
403
  coreDeleteFile(getMindRoot(), filePath);
350
- invalidateCache();
404
+ invalidateCacheForDeletedFile(filePath);
351
405
  }
352
406
 
353
407
  /** Renames a file. newName must be a plain filename (no path separators). */
@@ -384,12 +438,12 @@ export function readLines(filePath: string): string[] {
384
438
 
385
439
  export function insertLines(filePath: string, afterIndex: number, lines: string[]): void {
386
440
  coreInsertLines(getMindRoot(), filePath, afterIndex, lines);
387
- invalidateCache();
441
+ invalidateCacheForFile(filePath);
388
442
  }
389
443
 
390
444
  export function updateLines(filePath: string, startIndex: number, endIndex: number, newLines: string[]): void {
391
445
  coreUpdateLines(getMindRoot(), filePath, startIndex, endIndex, newLines);
392
- invalidateCache();
446
+ invalidateCacheForFile(filePath);
393
447
  }
394
448
 
395
449
  export function deleteLines(filePath: string, startIndex: number, endIndex: number): void {
@@ -405,17 +459,17 @@ export function deleteLines(filePath: string, startIndex: number, endIndex: numb
405
459
 
406
460
  export function appendToFile(filePath: string, content: string): void {
407
461
  coreAppendToFile(getMindRoot(), filePath, content);
408
- invalidateCache();
462
+ invalidateCacheForFile(filePath);
409
463
  }
410
464
 
411
465
  export function insertAfterHeading(filePath: string, heading: string, content: string): void {
412
466
  coreInsertAfterHeading(getMindRoot(), filePath, heading, content);
413
- invalidateCache();
467
+ invalidateCacheForFile(filePath);
414
468
  }
415
469
 
416
470
  export function updateSection(filePath: string, heading: string, newContent: string): void {
417
471
  coreUpdateSection(getMindRoot(), filePath, heading, newContent);
418
- invalidateCache();
472
+ invalidateCacheForFile(filePath);
419
473
  }
420
474
 
421
475
  /** App-level search result (extends core SearchResult with Fuse.js match details) */
@@ -649,7 +703,10 @@ export function purgeExpiredTrash() {
649
703
  }
650
704
 
651
705
  export function findBacklinks(targetPath: string): BacklinkEntry[] {
652
- const { allFiles } = ensureCache();
653
- return coreFindBacklinks(getMindRoot(), targetPath, allFiles);
706
+ const mindRoot = getMindRoot();
707
+ // Use LinkIndex for O(1) source lookup, then only scan matching files
708
+ const linkIndex = getLinkIndex();
709
+ const linkingSources = linkIndex.getBacklinks(targetPath);
710
+ return coreFindBacklinks(mindRoot, targetPath, linkingSources);
654
711
  }
655
712
 
@@ -15,8 +15,13 @@ function getSnapshot(): string[] {
15
15
  }
16
16
  }
17
17
 
18
+ function getClientSnapshot(): string[] {
19
+ return cachedPins;
20
+ }
21
+
22
+ const SERVER_SNAPSHOT: string[] = [];
18
23
  function getServerSnapshot(): string[] {
19
- return [];
24
+ return SERVER_SNAPSHOT;
20
25
  }
21
26
 
22
27
  // Lazy init — avoid calling getSnapshot() at module load time during SSR
@@ -62,7 +67,7 @@ function writePins(pins: string[]): void {
62
67
 
63
68
  export function usePinnedFiles() {
64
69
  ensureInit();
65
- const pins = useSyncExternalStore(subscribe, () => cachedPins, getServerSnapshot);
70
+ const pins = useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);
66
71
 
67
72
  const isPinned = useCallback((path: string) => pins.includes(path), [pins]);
68
73
 
@@ -70,6 +70,23 @@ export const knowledgeEn = {
70
70
  cleanupExamplesButton: 'Clean up',
71
71
  cleanupExamplesDone: 'Example files removed',
72
72
  },
73
+ pulse: {
74
+ title: 'Knowledge Pulse',
75
+ manage: 'Manage',
76
+ connectTitle: 'Connect your AI tools',
77
+ connectDesc: 'Let Cursor, Claude Code, and ChatGPT read your notes.',
78
+ connectAction: 'Connect Agent',
79
+ active: 'Connected',
80
+ detected: 'Detected',
81
+ notFound: 'Not found',
82
+ running: 'Online',
83
+ offline: 'Offline',
84
+ summary: (agents: number, skills: number) => `${agents} Agent${agents !== 1 ? 's' : ''} · ${skills} Skills`,
85
+ skillCount: (enabled: number, total: number) => `${enabled}/${total} skills`,
86
+ showMore: (n: number) => `${n} more agent${n !== 1 ? 's' : ''}`,
87
+ showLess: 'Show less',
88
+ otherDetected: 'Detected',
89
+ },
73
90
  fileTree: {
74
91
  newFileTitle: 'New file in this directory',
75
92
  rename: 'Rename',
@@ -118,6 +135,9 @@ export const knowledgeEn = {
118
135
  expiresIn: (days: number) => `Expires in ${days} day${days !== 1 ? 's' : ''}`,
119
136
  restored: 'Restored successfully',
120
137
  deleted: 'Permanently deleted',
138
+ movedToTrash: 'Deleted',
139
+ undo: 'Undo',
140
+ undoFailed: 'Undo failed',
121
141
  emptied: (count: number) => `Emptied ${count} item${count !== 1 ? 's' : ''} from Trash`,
122
142
  restoreConflict: 'A file with this name already exists at the original location.',
123
143
  overwrite: 'Overwrite',
@@ -126,6 +146,11 @@ export const knowledgeEn = {
126
146
  undoDelete: 'Undo',
127
147
  viewInTrash: 'View in Trash',
128
148
  fileDeleted: (name: string) => `"${name}" moved to Trash`,
149
+ cancel: 'Cancel',
150
+ justNow: 'just now',
151
+ minutesAgo: (m: number) => `${m}m ago`,
152
+ hoursAgo: (h: number) => `${h}h ago`,
153
+ daysAgo: (d: number) => `${d}d ago`,
129
154
  },
130
155
  export: {
131
156
  title: 'Export',
@@ -144,6 +169,7 @@ export const knowledgeEn = {
144
169
  exporting: 'Exporting...',
145
170
  exportButton: 'Export',
146
171
  cancel: 'Cancel',
172
+ close: 'Done',
147
173
  done: 'Export Complete',
148
174
  downloadAgain: 'Download Again',
149
175
  fileCount: (n: number) => `${n} file${n !== 1 ? 's' : ''}`,
@@ -263,6 +289,11 @@ export const knowledgeEn = {
263
289
  view: {
264
290
  saveDirectory: 'Directory',
265
291
  saveFileName: 'File name',
292
+ delete: 'Delete',
293
+ deleteConfirm: (name: string) => `Delete "${name}"? It will be moved to Trash.`,
294
+ cancel: 'Cancel',
295
+ rename: 'Rename',
296
+ dismiss: 'Dismiss',
266
297
  },
267
298
  findInPage: {
268
299
  placeholder: 'Find in document…',
@@ -341,6 +372,23 @@ export const knowledgeZh = {
341
372
  cleanupExamplesButton: '一键清理',
342
373
  cleanupExamplesDone: '示例文件已清理',
343
374
  },
375
+ pulse: {
376
+ title: '知识脉搏',
377
+ manage: '管理',
378
+ connectTitle: '连接你的 AI 工具',
379
+ connectDesc: '让 Cursor、Claude Code 等 AI 读到你的笔记。',
380
+ connectAction: '连接 Agent',
381
+ active: '已连接',
382
+ detected: '已检测',
383
+ notFound: '未发现',
384
+ running: '在线',
385
+ offline: '离线',
386
+ summary: (agents: number, skills: number) => `${agents} 个 Agent · ${skills} 个技能`,
387
+ skillCount: (enabled: number, total: number) => `${enabled}/${total} 个技能`,
388
+ showMore: (n: number) => `还有 ${n} 个`,
389
+ showLess: '收起',
390
+ otherDetected: '已检测到',
391
+ },
344
392
  fileTree: {
345
393
  newFileTitle: '在此目录新建文件',
346
394
  rename: '重命名',
@@ -389,6 +437,9 @@ export const knowledgeZh = {
389
437
  expiresIn: (days: number) => `${days} 天后过期`,
390
438
  restored: '已恢复',
391
439
  deleted: '已彻底删除',
440
+ movedToTrash: '已删除',
441
+ undo: '撤销',
442
+ undoFailed: '撤销失败',
392
443
  emptied: (count: number) => `已清空 ${count} 个项目`,
393
444
  restoreConflict: '原位置已有同名文件。',
394
445
  overwrite: '覆盖',
@@ -397,6 +448,11 @@ export const knowledgeZh = {
397
448
  undoDelete: '撤销',
398
449
  viewInTrash: '查看回收站',
399
450
  fileDeleted: (name: string) => `「${name}」已移至回收站`,
451
+ cancel: '取消',
452
+ justNow: '刚刚',
453
+ minutesAgo: (m: number) => `${m} 分钟前`,
454
+ hoursAgo: (h: number) => `${h} 小时前`,
455
+ daysAgo: (d: number) => `${d} 天前`,
400
456
  },
401
457
  export: {
402
458
  title: '导出',
@@ -415,6 +471,7 @@ export const knowledgeZh = {
415
471
  exporting: '导出中...',
416
472
  exportButton: '导出',
417
473
  cancel: '取消',
474
+ close: '完成',
418
475
  done: '导出完成',
419
476
  downloadAgain: '再次下载',
420
477
  fileCount: (n: number) => `${n} 个文件`,
@@ -534,6 +591,11 @@ export const knowledgeZh = {
534
591
  view: {
535
592
  saveDirectory: '目录',
536
593
  saveFileName: '文件名',
594
+ delete: '删除',
595
+ deleteConfirm: (name: string) => `确定删除「${name}」?文件将移至回收站。`,
596
+ cancel: '取消',
597
+ rename: '重命名',
598
+ dismiss: '关闭',
537
599
  },
538
600
  findInPage: {
539
601
  placeholder: '在文档中查找…',
package/app/lib/toast.ts CHANGED
@@ -15,9 +15,10 @@ export interface Toast {
15
15
  message: string;
16
16
  type: 'success' | 'error' | 'info';
17
17
  duration: number;
18
+ action?: { label: string; onClick: () => void };
18
19
  }
19
20
 
20
- type ToastInput = { message: string; type?: Toast['type']; duration?: number };
21
+ type ToastInput = { message: string; type?: Toast['type']; duration?: number; action?: Toast['action'] };
21
22
 
22
23
  const DEFAULT_DURATION = 2000;
23
24
  const MAX_TOASTS = 3;
@@ -51,6 +52,7 @@ function addToast(input: ToastInput) {
51
52
  message: input.message,
52
53
  type: input.type ?? 'info',
53
54
  duration: input.duration ?? DEFAULT_DURATION,
55
+ action: input.action,
54
56
  };
55
57
  toasts = [...toasts, t].slice(-MAX_TOASTS);
56
58
  emit();
@@ -68,6 +70,10 @@ function toast(message: string, opts?: { type?: Toast['type']; duration?: number
68
70
  toast.success = (message: string, duration?: number) =>
69
71
  addToast({ message, type: 'success', duration });
70
72
 
73
+ /** Show a toast with an undo action (5 second default) */
74
+ toast.undo = (message: string, onUndo: () => void, opts?: { duration?: number; label?: string }) =>
75
+ addToast({ message, type: 'info', duration: opts?.duration ?? 5000, action: { label: opts?.label ?? 'Undo', onClick: onUndo } });
76
+
71
77
  /** Show an error toast */
72
78
  toast.error = (message: string, duration?: number) =>
73
79
  addToast({ message, type: 'error', duration });
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
@@ -7,6 +7,7 @@
7
7
  "dev": "npx tsx scripts/generate-explore.ts && next dev -p ${MINDOS_WEB_PORT:-3456}",
8
8
  "prebuild": "npx tsx scripts/generate-explore.ts && node ../scripts/gen-renderer-index.js",
9
9
  "build": "next build --webpack",
10
+ "postbuild": "node ../scripts/write-build-stamp.js",
10
11
  "start": "next start -p ${MINDOS_WEB_PORT:-3456}",
11
12
  "lint": "eslint",
12
13
  "test": "vitest run",
@@ -54,6 +55,7 @@
54
55
  "react": "19.2.3",
55
56
  "react-dom": "19.2.3",
56
57
  "react-markdown": "^10.1.0",
58
+ "react-virtuoso": "^4.18.4",
57
59
  "rehype-highlight": "^7.0.2",
58
60
  "rehype-raw": "^7.0.0",
59
61
  "rehype-slug": "^6.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.32",
3
+ "version": "0.6.33",
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",
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # parse-syncinclude.sh — Parse .syncinclude and export shell variables.
4
+ #
5
+ # Usage:
6
+ # eval "$(scripts/parse-syncinclude.sh)"
7
+ # # Now you have: SYNC_DIRS, ROOT_FILES, WIKI_EXCLUDES, GLOBAL_EXCLUDES, BLOCKED_PATTERNS
8
+ #
9
+ # Or source it:
10
+ # source <(scripts/parse-syncinclude.sh)
11
+ #
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
+ SYNCFILE="$REPO_ROOT/.syncinclude"
17
+
18
+ if [ ! -f "$SYNCFILE" ]; then
19
+ echo "echo '::error::.syncinclude not found at $SYNCFILE'" >&2
20
+ exit 1
21
+ fi
22
+
23
+ # ─── Parser ───────────────────────────────────────────────────────────────────
24
+ # Simple YAML-subset parser: reads "section:" headers and " - value" items.
25
+ # No external dependencies (no yq/python needed).
26
+
27
+ current_section=""
28
+ DIRS=()
29
+ FILES=()
30
+ WIKI_EX=()
31
+ GLOBAL_EX=()
32
+ BLOCKED=()
33
+
34
+ while IFS= read -r line; do
35
+ # Skip comments and blank lines
36
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
37
+ [[ -z "${line// /}" ]] && continue
38
+
39
+ # Section header (no leading whitespace, ends with colon)
40
+ if [[ "$line" =~ ^([a-z_]+): ]]; then
41
+ current_section="${BASH_REMATCH[1]}"
42
+ continue
43
+ fi
44
+
45
+ # List item (leading whitespace + dash)
46
+ if [[ "$line" =~ ^[[:space:]]+-[[:space:]]+(.*) ]]; then
47
+ value="${BASH_REMATCH[1]}"
48
+ # Strip surrounding quotes
49
+ value="${value#\"}"
50
+ value="${value%\"}"
51
+ value="${value#\'}"
52
+ value="${value%\'}"
53
+
54
+ case "$current_section" in
55
+ directories) DIRS+=("$value") ;;
56
+ root_files) FILES+=("$value") ;;
57
+ wiki_exclude) WIKI_EX+=("$value") ;;
58
+ global_exclude) GLOBAL_EX+=("$value") ;;
59
+ blocked_patterns) BLOCKED+=("$value") ;;
60
+ esac
61
+ fi
62
+ done < "$SYNCFILE"
63
+
64
+ # ─── Output ───────────────────────────────────────────────────────────────────
65
+ # Print shell variable assignments that can be eval'd by the caller.
66
+
67
+ # Space-separated lists (for simple iteration)
68
+ echo "SYNC_DIRS='${DIRS[*]:-}'"
69
+ echo "ROOT_FILES='${FILES[*]:-}'"
70
+
71
+ # Build rsync --exclude flags
72
+ wiki_excludes=""
73
+ for ex in "${WIKI_EX[@]:-}"; do
74
+ [ -n "$ex" ] && wiki_excludes="$wiki_excludes --exclude='$ex'"
75
+ done
76
+ echo "WIKI_EXCLUDES='$wiki_excludes'"
77
+
78
+ global_excludes=""
79
+ for ex in "${GLOBAL_EX[@]:-}"; do
80
+ [ -n "$ex" ] && global_excludes="$global_excludes --exclude='$ex'"
81
+ done
82
+ echo "GLOBAL_EXCLUDES='$global_excludes'"
83
+
84
+ # Newline-separated for safe iteration (patterns may contain spaces/globs)
85
+ blocked_str=""
86
+ for pat in "${BLOCKED[@]:-}"; do
87
+ [ -n "$pat" ] && blocked_str="$blocked_str$pat"$'\n'
88
+ done
89
+ # Use heredoc-style to safely pass patterns with special chars
90
+ cat <<BLOCKED_EOF
91
+ BLOCKED_PATTERNS='$(echo -n "$blocked_str")'
92
+ BLOCKED_EOF
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Write .mindos-build-version stamp after `next build`.
4
+ *
5
+ * This ensures Desktop's isNextBuildCurrent() recognizes builds
6
+ * done via `npm run build` (app/package.json) without going through
7
+ * the CLI's `mindos build` command.
8
+ *
9
+ * The stamp file is read by:
10
+ * - desktop/src/mindos-runtime-layout.ts (isNextBuildCurrent)
11
+ * - bin/lib/build.js (needsBuild / writeBuildStamp)
12
+ *
13
+ * Safe to run multiple times (idempotent).
14
+ */
15
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
16
+ const { resolve } = require('path');
17
+
18
+ const STAMP_NAME = '.mindos-build-version';
19
+ const appDir = resolve(__dirname, '..', 'app');
20
+ const nextDir = resolve(appDir, '.next');
21
+ const stampPath = resolve(nextDir, STAMP_NAME);
22
+ const pkgPath = resolve(__dirname, '..', 'package.json');
23
+
24
+ try {
25
+ if (!existsSync(nextDir)) {
26
+ // next build didn't produce output — nothing to stamp
27
+ process.exit(0);
28
+ }
29
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
30
+ const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
31
+ if (!version) {
32
+ console.warn('[write-build-stamp] No version in package.json, skipping');
33
+ process.exit(0);
34
+ }
35
+ writeFileSync(stampPath, version, 'utf-8');
36
+ console.log(`[write-build-stamp] ${version} → ${stampPath}`);
37
+ } catch (err) {
38
+ // Non-fatal — build succeeded, stamp is a bonus
39
+ console.warn('[write-build-stamp] Failed:', err.message);
40
+ }