@geminilight/mindos 0.2.0 → 0.3.0

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.
@@ -5,6 +5,7 @@ import {
5
5
  saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
6
6
  } from '@/lib/fs';
7
7
  import { assertNotProtected } from '@/lib/core';
8
+ import { logAgentOp } from './log';
8
9
 
9
10
  // Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
10
11
  const MAX_FILE_CHARS = 20_000;
@@ -19,47 +20,67 @@ export function assertWritable(filePath: string): void {
19
20
  assertNotProtected(filePath, 'modified by AI agent');
20
21
  }
21
22
 
23
+ /** Helper: wrap a tool execute fn with agent-op logging */
24
+ function logged<P extends Record<string, unknown>>(
25
+ toolName: string,
26
+ fn: (params: P) => Promise<string>,
27
+ ): (params: P) => Promise<string> {
28
+ return async (params: P) => {
29
+ const ts = new Date().toISOString();
30
+ try {
31
+ const result = await fn(params);
32
+ const isError = result.startsWith('Error:');
33
+ logAgentOp({ ts, tool: toolName, params, result: isError ? 'error' : 'ok', message: result.slice(0, 200) });
34
+ return result;
35
+ } catch (e) {
36
+ const msg = e instanceof Error ? e.message : String(e);
37
+ logAgentOp({ ts, tool: toolName, params, result: 'error', message: msg.slice(0, 200) });
38
+ throw e;
39
+ }
40
+ };
41
+ }
42
+
22
43
  // ─── Knowledge base tools ─────────────────────────────────────────────────────
23
44
 
24
45
  export const knowledgeBaseTools = {
25
46
  list_files: tool({
26
47
  description: 'List the full file tree of the knowledge base. Use this to browse what files exist.',
27
48
  inputSchema: z.object({}),
28
- execute: async () => {
49
+ execute: logged('list_files', async () => {
29
50
  const tree = getFileTree();
30
51
  return JSON.stringify(tree, null, 2);
31
- },
52
+ }),
32
53
  }),
33
54
 
34
55
  read_file: tool({
35
56
  description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
36
57
  inputSchema: z.object({ path: z.string().describe('Relative file path, e.g. "Profile/👤 Identity.md"') }),
37
- execute: async ({ path }) => {
58
+ execute: logged('read_file', async ({ path }) => {
38
59
  try {
39
60
  return truncate(getFileContent(path));
40
61
  } catch (e: unknown) {
41
62
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
42
63
  }
43
- },
64
+ }),
44
65
  }),
45
66
 
46
67
  search: tool({
47
68
  description: 'Full-text search across all files in the knowledge base. Returns matching files with context snippets.',
48
69
  inputSchema: z.object({ query: z.string().describe('Search query (case-insensitive)') }),
49
- execute: async ({ query }) => {
70
+ execute: logged('search', async ({ query }) => {
50
71
  const results = searchFiles(query);
51
72
  if (results.length === 0) return 'No results found.';
52
73
  return results.map(r => `- **${r.path}**: ${r.snippet}`).join('\n');
53
- },
74
+ }),
54
75
  }),
55
76
 
56
77
  get_recent: tool({
57
78
  description: 'Get the most recently modified files in the knowledge base.',
58
79
  inputSchema: z.object({ limit: z.number().min(1).max(50).default(10).describe('Number of files to return') }),
59
- execute: async ({ limit }) => {
80
+ execute: logged('get_recent', async ({ limit }) => {
60
81
  const files = getRecentlyModified(limit);
61
82
  return files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n');
62
- },
83
+ }),
63
84
  }),
64
85
 
65
86
  write_file: tool({
@@ -68,7 +89,7 @@ export const knowledgeBaseTools = {
68
89
  path: z.string().describe('Relative file path'),
69
90
  content: z.string().describe('New full content'),
70
91
  }),
71
- execute: async ({ path, content }) => {
92
+ execute: logged('write_file', async ({ path, content }) => {
72
93
  try {
73
94
  assertWritable(path);
74
95
  saveFileContent(path, content);
@@ -76,7 +97,7 @@ export const knowledgeBaseTools = {
76
97
  } catch (e: unknown) {
77
98
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
78
99
  }
79
- },
100
+ }),
80
101
  }),
81
102
 
82
103
  create_file: tool({
@@ -85,7 +106,7 @@ export const knowledgeBaseTools = {
85
106
  path: z.string().describe('Relative file path (must end in .md or .csv)'),
86
107
  content: z.string().default('').describe('Initial file content'),
87
108
  }),
88
- execute: async ({ path, content }) => {
109
+ execute: logged('create_file', async ({ path, content }) => {
89
110
  try {
90
111
  assertWritable(path);
91
112
  createFile(path, content);
@@ -93,7 +114,7 @@ export const knowledgeBaseTools = {
93
114
  } catch (e: unknown) {
94
115
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
95
116
  }
96
- },
117
+ }),
97
118
  }),
98
119
 
99
120
  append_to_file: tool({
@@ -102,7 +123,7 @@ export const knowledgeBaseTools = {
102
123
  path: z.string().describe('Relative file path'),
103
124
  content: z.string().describe('Content to append'),
104
125
  }),
105
- execute: async ({ path, content }) => {
126
+ execute: logged('append_to_file', async ({ path, content }) => {
106
127
  try {
107
128
  assertWritable(path);
108
129
  appendToFile(path, content);
@@ -110,7 +131,7 @@ export const knowledgeBaseTools = {
110
131
  } catch (e: unknown) {
111
132
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
112
133
  }
113
- },
134
+ }),
114
135
  }),
115
136
 
116
137
  insert_after_heading: tool({
@@ -120,7 +141,7 @@ export const knowledgeBaseTools = {
120
141
  heading: z.string().describe('Heading text to find (e.g. "## Tasks" or just "Tasks")'),
121
142
  content: z.string().describe('Content to insert after the heading'),
122
143
  }),
123
- execute: async ({ path, heading, content }) => {
144
+ execute: logged('insert_after_heading', async ({ path, heading, content }) => {
124
145
  try {
125
146
  assertWritable(path);
126
147
  insertAfterHeading(path, heading, content);
@@ -128,7 +149,7 @@ export const knowledgeBaseTools = {
128
149
  } catch (e: unknown) {
129
150
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
130
151
  }
131
- },
152
+ }),
132
153
  }),
133
154
 
134
155
  update_section: tool({
@@ -138,7 +159,7 @@ export const knowledgeBaseTools = {
138
159
  heading: z.string().describe('Heading text to find (e.g. "## Status")'),
139
160
  content: z.string().describe('New content for the section'),
140
161
  }),
141
- execute: async ({ path, heading, content }) => {
162
+ execute: logged('update_section', async ({ path, heading, content }) => {
142
163
  try {
143
164
  assertWritable(path);
144
165
  updateSection(path, heading, content);
@@ -146,6 +167,6 @@ export const knowledgeBaseTools = {
146
167
  } catch (e: unknown) {
147
168
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
148
169
  }
149
- },
170
+ }),
150
171
  }),
151
172
  };
package/app/lib/i18n.ts CHANGED
@@ -36,6 +36,22 @@ export const messages = {
36
36
  settingsTitle: 'Settings (⌘,)',
37
37
  collapseTitle: 'Collapse sidebar',
38
38
  expandTitle: 'Expand sidebar',
39
+ sync: {
40
+ synced: 'Synced',
41
+ unpushed: 'awaiting push',
42
+ unpushedHint: 'commit(s) not yet pushed to remote — will sync automatically',
43
+ conflicts: 'conflicts',
44
+ conflictsHint: 'file(s) have merge conflicts — open Settings > Sync to resolve',
45
+ syncError: 'Sync error',
46
+ syncOff: 'Sync off',
47
+ syncing: 'Syncing...',
48
+ syncNow: 'Sync now',
49
+ syncDone: 'Sync complete',
50
+ syncFailed: 'Sync failed',
51
+ syncRestored: 'Sync restored',
52
+ enableSync: 'Enable sync',
53
+ enableHint: 'Set up cross-device sync',
54
+ },
39
55
  },
40
56
  search: {
41
57
  placeholder: 'Search files...',
@@ -125,6 +141,18 @@ export const messages = {
125
141
  authTokenResetConfirm: 'Regenerate token? All existing MCP clients will need to update their config.',
126
142
  authTokenMcpPort: 'MCP port',
127
143
  },
144
+ sync: {
145
+ emptyTitle: 'Cross-device Sync',
146
+ emptyDesc: 'Automatically sync your knowledge base across devices via Git.',
147
+ emptyStepsTitle: 'Setup',
148
+ emptyStep1: 'Create a private Git repo (GitHub, GitLab, etc.) or use an existing one.',
149
+ emptyStep2: 'Run this command in your terminal:',
150
+ emptyStep3: 'Follow the prompts to connect your repo. Sync starts automatically.',
151
+ featureAutoCommit: 'Auto-commit on save',
152
+ featureAutoPull: 'Auto-pull from remote',
153
+ featureConflict: 'Conflict detection',
154
+ featureMultiDevice: 'Works across devices',
155
+ },
128
156
  plugins: {
129
157
  title: 'Installed Renderers',
130
158
  builtinBadge: 'built-in',
@@ -138,6 +166,17 @@ export const messages = {
138
166
  saved: 'Saved',
139
167
  saveFailed: 'Save failed',
140
168
  },
169
+ onboarding: {
170
+ subtitle: 'Your knowledge base is empty. Pick a starter template to get going.',
171
+ templates: {
172
+ en: { title: 'English', desc: 'Pre-built structure with Profile, Notes, Projects, and more.' },
173
+ zh: { title: '中文', desc: '预置画像、笔记、项目等中文目录结构。' },
174
+ empty: { title: 'Empty', desc: 'Just the essentials — README, CONFIG, and INSTRUCTION.' },
175
+ },
176
+ importHint: 'Already have notes? Set MIND_ROOT to your existing directory in Settings.',
177
+ syncHint: 'Want cross-device sync? Run',
178
+ syncHintSuffix: 'in the terminal after setup.',
179
+ },
141
180
  shortcuts: [
142
181
  { keys: ['⌘', 'K'], description: 'Search' },
143
182
  { keys: ['⌘', '/'], description: 'Ask AI' },
@@ -147,6 +186,52 @@ export const messages = {
147
186
  { keys: ['Esc'], description: 'Cancel edit / close modal' },
148
187
  { keys: ['@'], description: 'Attach file in Ask AI' },
149
188
  ],
189
+ setup: {
190
+ stepTitles: ['Knowledge Base', 'AI Provider', 'Ports', 'Security', 'Review'],
191
+ // Step 1
192
+ kbPath: 'Knowledge base path',
193
+ kbPathHint: 'Absolute path to your notes directory.',
194
+ kbPathDefault: '~/MindOS',
195
+ template: 'Starter template',
196
+ templateSkip: 'Skip (directory already has files)',
197
+ // Step 2
198
+ aiProvider: 'AI Provider',
199
+ aiProviderHint: 'Choose your preferred AI service.',
200
+ aiSkip: 'Skip — configure later',
201
+ apiKey: 'API Key',
202
+ model: 'Model',
203
+ baseUrl: 'Base URL',
204
+ baseUrlHint: 'Optional. For proxies or OpenAI-compatible APIs.',
205
+ // Step 3
206
+ webPort: 'Web UI port',
207
+ mcpPort: 'MCP server port',
208
+ portHint: 'Valid range: 1024–65535',
209
+ portRestartWarning: 'Port changes take effect after server restart.',
210
+ // Step 4
211
+ authToken: 'Auth Token',
212
+ authTokenHint: 'Bearer token for MCP / API clients. Auto-generated.',
213
+ authTokenSeed: 'Custom seed (optional)',
214
+ authTokenSeedHint: 'Enter a passphrase to derive a deterministic token.',
215
+ generateToken: 'Generate',
216
+ copyToken: 'Copy',
217
+ copiedToken: 'Copied!',
218
+ webPassword: 'Web UI Password',
219
+ webPasswordHint: 'Optional. Protect browser access with a password.',
220
+ // Step 5
221
+ reviewTitle: 'Review Configuration',
222
+ reviewHint: 'Verify your settings before completing setup.',
223
+ keyMasked: (key: string) => key.slice(0, 6) + '•••' + key.slice(-3),
224
+ portChanged: 'Port changed — please restart the server for it to take effect.',
225
+ // Buttons
226
+ back: 'Back',
227
+ next: 'Next',
228
+ complete: 'Complete Setup',
229
+ skip: 'Skip',
230
+ // Status
231
+ completing: 'Saving...',
232
+ completeDone: 'Setup complete!',
233
+ completeFailed: 'Setup failed. Please try again.',
234
+ },
150
235
  },
151
236
  zh: {
152
237
  common: {
@@ -183,6 +268,22 @@ export const messages = {
183
268
  settingsTitle: '设置 (⌘,)',
184
269
  collapseTitle: '收起侧栏',
185
270
  expandTitle: '展开侧栏',
271
+ sync: {
272
+ synced: '已同步',
273
+ unpushed: '待推送',
274
+ unpushedHint: '个提交尚未推送到远程 — 将自动同步',
275
+ conflicts: '冲突',
276
+ conflictsHint: '个文件存在合并冲突 — 前往 设置 > 同步 解决',
277
+ syncError: '同步出错',
278
+ syncOff: '同步关闭',
279
+ syncing: '同步中...',
280
+ syncNow: '立即同步',
281
+ syncDone: '同步完成',
282
+ syncFailed: '同步失败',
283
+ syncRestored: '同步已恢复',
284
+ enableSync: '启用同步',
285
+ enableHint: '设置跨设备同步',
286
+ },
186
287
  },
187
288
  search: {
188
289
  placeholder: '搜索文件...',
@@ -272,6 +373,18 @@ export const messages = {
272
373
  authTokenResetConfirm: '重新生成令牌?所有 MCP 客户端配置都需要更新。',
273
374
  authTokenMcpPort: 'MCP 端口',
274
375
  },
376
+ sync: {
377
+ emptyTitle: '跨设备同步',
378
+ emptyDesc: '通过 Git 自动同步知识库到所有设备。',
379
+ emptyStepsTitle: '配置步骤',
380
+ emptyStep1: '创建一个私有 Git 仓库(GitHub、GitLab 等),或使用现有仓库。',
381
+ emptyStep2: '在终端中运行以下命令:',
382
+ emptyStep3: '按照提示连接仓库,同步将自动开始。',
383
+ featureAutoCommit: '保存时自动提交',
384
+ featureAutoPull: '自动拉取远程更新',
385
+ featureConflict: '冲突检测',
386
+ featureMultiDevice: '多设备同步',
387
+ },
275
388
  plugins: {
276
389
  title: '已安装渲染器',
277
390
  builtinBadge: '内置',
@@ -285,6 +398,17 @@ export const messages = {
285
398
  saved: '已保存',
286
399
  saveFailed: '保存失败',
287
400
  },
401
+ onboarding: {
402
+ subtitle: '知识库为空,选择一个模板快速开始。',
403
+ templates: {
404
+ en: { title: 'English', desc: '预置 Profile、Notes、Projects 等英文目录结构。' },
405
+ zh: { title: '中文', desc: '预置画像、笔记、项目等中文目录结构。' },
406
+ empty: { title: '空白', desc: '仅包含 README、CONFIG 和 INSTRUCTION 基础文件。' },
407
+ },
408
+ importHint: '已有笔记?在设置中将 MIND_ROOT 指向已有目录即可。',
409
+ syncHint: '需要跨设备同步?完成初始化后在终端运行',
410
+ syncHintSuffix: '即可。',
411
+ },
288
412
  shortcuts: [
289
413
  { keys: ['⌘', 'K'], description: '搜索' },
290
414
  { keys: ['⌘', '/'], description: '问 AI' },
@@ -294,6 +418,52 @@ export const messages = {
294
418
  { keys: ['Esc'], description: '取消编辑 / 关闭弹窗' },
295
419
  { keys: ['@'], description: '在 AI 对话中添加附件' },
296
420
  ],
421
+ setup: {
422
+ stepTitles: ['知识库', 'AI 服务商', '端口', '安全', '确认'],
423
+ // Step 1
424
+ kbPath: '知识库路径',
425
+ kbPathHint: '笔记目录的绝对路径。',
426
+ kbPathDefault: '~/MindOS',
427
+ template: '初始模板',
428
+ templateSkip: '跳过(目录已有文件)',
429
+ // Step 2
430
+ aiProvider: 'AI 服务商',
431
+ aiProviderHint: '选择你偏好的 AI 服务。',
432
+ aiSkip: '跳过 — 稍后配置',
433
+ apiKey: 'API 密钥',
434
+ model: '模型',
435
+ baseUrl: '接口地址',
436
+ baseUrlHint: '可选。用于代理或 OpenAI 兼容 API。',
437
+ // Step 3
438
+ webPort: 'Web UI 端口',
439
+ mcpPort: 'MCP 服务端口',
440
+ portHint: '有效范围:1024–65535',
441
+ portRestartWarning: '端口修改需重启服务后生效。',
442
+ // Step 4
443
+ authToken: 'Auth Token',
444
+ authTokenHint: 'MCP / API 客户端使用的 Bearer Token,自动生成。',
445
+ authTokenSeed: '自定义种子(可选)',
446
+ authTokenSeedHint: '输入口令短语生成确定性 Token。',
447
+ generateToken: '生成',
448
+ copyToken: '复制',
449
+ copiedToken: '已复制!',
450
+ webPassword: '网页访问密码',
451
+ webPasswordHint: '可选。设置后浏览器访问需要登录。',
452
+ // Step 5
453
+ reviewTitle: '确认配置',
454
+ reviewHint: '完成设置前请确认以下信息。',
455
+ keyMasked: (key: string) => key.slice(0, 6) + '•••' + key.slice(-3),
456
+ portChanged: '端口已变更 — 请重启服务以使其生效。',
457
+ // Buttons
458
+ back: '上一步',
459
+ next: '下一步',
460
+ complete: '完成设置',
461
+ skip: '跳过',
462
+ // Status
463
+ completing: '保存中...',
464
+ completeDone: '设置完成!',
465
+ completeFailed: '设置失败,请重试。',
466
+ },
297
467
  },
298
468
  } as const;
299
469
 
@@ -5,6 +5,7 @@ import { GraphRenderer } from '@/components/renderers/GraphRenderer';
5
5
  import { TimelineRenderer } from '@/components/renderers/TimelineRenderer';
6
6
  import { SummaryRenderer } from '@/components/renderers/SummaryRenderer';
7
7
  import { ConfigRenderer } from '@/components/renderers/ConfigRenderer';
8
+ import { AgentInspectorRenderer } from '@/components/renderers/AgentInspectorRenderer';
8
9
 
9
10
  registerRenderer({
10
11
  id: 'todo',
@@ -77,3 +78,15 @@ registerRenderer({
77
78
  match: ({ filePath }) => /\b(SUMMARY|summary|Summary|BRIEFING|briefing|Briefing|DAILY|daily|Daily)\b.*\.md$/i.test(filePath),
78
79
  component: SummaryRenderer,
79
80
  });
81
+
82
+ registerRenderer({
83
+ id: 'agent-inspector',
84
+ name: 'Agent Inspector',
85
+ description: 'Visualizes agent tool-call logs as a filterable timeline. Auto-activates on .agent-log.json (JSON Lines format).',
86
+ author: 'MindOS',
87
+ icon: '🔍',
88
+ tags: ['agent', 'inspector', 'log', 'mcp', 'tools'],
89
+ builtin: true,
90
+ match: ({ filePath }) => /\.agent-log\.json$/i.test(filePath),
91
+ component: AgentInspectorRenderer,
92
+ });
@@ -21,12 +21,12 @@ export interface AiConfig {
21
21
  export interface ServerSettings {
22
22
  ai: AiConfig;
23
23
  mindRoot: string; // empty = use env var / default
24
- // Fields managed by CLI only (not edited via GUI, preserved on write)
25
24
  port?: number;
26
25
  mcpPort?: number;
27
26
  authToken?: string;
28
27
  webPassword?: string;
29
28
  startMode?: 'dev' | 'start' | 'daemon';
29
+ setupPending?: boolean; // true → / redirects to /setup
30
30
  }
31
31
 
32
32
  const DEFAULTS: ServerSettings = {
@@ -108,6 +108,9 @@ export function readSettings(): ServerSettings {
108
108
  webPassword: typeof parsed.webPassword === 'string' ? parsed.webPassword : undefined,
109
109
  authToken: typeof parsed.authToken === 'string' ? parsed.authToken : undefined,
110
110
  mcpPort: typeof parsed.mcpPort === 'number' ? parsed.mcpPort : undefined,
111
+ port: typeof parsed.port === 'number' ? parsed.port : undefined,
112
+ startMode: typeof parsed.startMode === 'string' ? parsed.startMode as ServerSettings['startMode'] : undefined,
113
+ setupPending: parsed.setupPending === true ? true : undefined,
111
114
  };
112
115
  } catch {
113
116
  return { ...DEFAULTS, ai: { ...DEFAULTS.ai, providers: { ...DEFAULTS.ai.providers } } };
@@ -123,6 +126,14 @@ export function writeSettings(settings: ServerSettings): void {
123
126
  const merged: Record<string, unknown> = { ...existing, ai: settings.ai, mindRoot: settings.mindRoot };
124
127
  if (settings.webPassword !== undefined) merged.webPassword = settings.webPassword;
125
128
  if (settings.authToken !== undefined) merged.authToken = settings.authToken;
129
+ if (settings.port !== undefined) merged.port = settings.port;
130
+ if (settings.mcpPort !== undefined) merged.mcpPort = settings.mcpPort;
131
+ if (settings.startMode !== undefined) merged.startMode = settings.startMode;
132
+ // setupPending: false/undefined → remove the field (cleanup); true → set it
133
+ if ('setupPending' in settings) {
134
+ if (settings.setupPending) merged.setupPending = true;
135
+ else delete merged.setupPending;
136
+ }
126
137
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
127
138
  }
128
139
 
@@ -146,5 +157,5 @@ export function effectiveAiConfig() {
146
157
  /** Effective MIND_ROOT — settings file can override, env var is fallback */
147
158
  export function effectiveSopRoot(): string {
148
159
  const s = readSettings();
149
- return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), '.mindos', 'my-mind');
160
+ return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS');
150
161
  }
@@ -0,0 +1,45 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Recursively copy `src` to `dest`, skipping files that already exist in dest.
6
+ */
7
+ export function copyRecursive(src: string, dest: string) {
8
+ const stat = fs.statSync(src);
9
+ if (stat.isDirectory()) {
10
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
11
+ for (const entry of fs.readdirSync(src)) {
12
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
13
+ }
14
+ } else {
15
+ // Skip if file already exists
16
+ if (fs.existsSync(dest)) return;
17
+ const dir = path.dirname(dest);
18
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
19
+ fs.copyFileSync(src, dest);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Apply a built-in template (en / zh / empty) to the given directory.
25
+ * Returns true on success, throws on error.
26
+ */
27
+ export function applyTemplate(template: string, destDir: string): void {
28
+ if (!['en', 'zh', 'empty'].includes(template)) {
29
+ throw new Error(`Invalid template: ${template}`);
30
+ }
31
+
32
+ // templates/ is at the repo root (sibling of app/)
33
+ const repoRoot = path.resolve(process.cwd(), '..');
34
+ const templateDir = path.join(repoRoot, 'templates', template);
35
+
36
+ if (!fs.existsSync(templateDir)) {
37
+ throw new Error(`Template "${template}" not found at ${templateDir}`);
38
+ }
39
+
40
+ if (!fs.existsSync(destDir)) {
41
+ fs.mkdirSync(destDir, { recursive: true });
42
+ }
43
+
44
+ copyRecursive(templateDir, destDir);
45
+ }
Binary file
Binary file
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "MindOS",
3
+ "short_name": "MindOS",
4
+ "description": "Personal knowledge OS — browse, edit, and query your second brain.",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0a0a0a",
8
+ "theme_color": "#c8871e",
9
+ "icons": [
10
+ {
11
+ "src": "/icons/icon-192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ },
15
+ {
16
+ "src": "/icons/icon-512.png",
17
+ "sizes": "512x512",
18
+ "type": "image/png"
19
+ },
20
+ {
21
+ "src": "/logo-square.svg",
22
+ "sizes": "any",
23
+ "type": "image/svg+xml"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,66 @@
1
+ // MindOS Service Worker — cache static assets, skip API/dynamic routes
2
+ const CACHE_NAME = 'mindos-v1';
3
+
4
+ const PRECACHE_URLS = [
5
+ '/',
6
+ '/logo-square.svg',
7
+ '/logo.svg',
8
+ ];
9
+
10
+ self.addEventListener('install', (event) => {
11
+ event.waitUntil(
12
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
13
+ );
14
+ self.skipWaiting();
15
+ });
16
+
17
+ self.addEventListener('activate', (event) => {
18
+ event.waitUntil(
19
+ caches.keys().then((keys) =>
20
+ Promise.all(
21
+ keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
22
+ )
23
+ )
24
+ );
25
+ self.clients.claim();
26
+ });
27
+
28
+ self.addEventListener('fetch', (event) => {
29
+ const url = new URL(event.request.url);
30
+
31
+ // Never cache API routes or Next.js internals
32
+ if (
33
+ url.pathname.startsWith('/api/') ||
34
+ url.pathname.startsWith('/_next/') ||
35
+ event.request.method !== 'GET'
36
+ ) {
37
+ return;
38
+ }
39
+
40
+ // Cache-first for static assets (images, fonts, SVGs)
41
+ if (
42
+ url.pathname.startsWith('/icons/') ||
43
+ url.pathname.endsWith('.svg') ||
44
+ url.pathname.endsWith('.png') ||
45
+ url.pathname.endsWith('.woff2')
46
+ ) {
47
+ event.respondWith(
48
+ caches.match(event.request).then((cached) => {
49
+ if (cached) return cached;
50
+ return fetch(event.request).then((response) => {
51
+ if (response.ok) {
52
+ const clone = response.clone();
53
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
54
+ }
55
+ return response;
56
+ });
57
+ })
58
+ );
59
+ return;
60
+ }
61
+
62
+ // Network-first for HTML pages
63
+ event.respondWith(
64
+ fetch(event.request).catch(() => caches.match(event.request))
65
+ );
66
+ });
package/bin/cli.js CHANGED
@@ -445,6 +445,27 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
445
445
  }
446
446
  }
447
447
 
448
+ // 8. Sync status
449
+ if (config?.mindRoot) {
450
+ try {
451
+ const syncStatus = getSyncStatus(config.mindRoot);
452
+ if (!syncStatus.enabled) {
453
+ warn(`Cross-device sync is not configured ${dim('(run `mindos sync init` to set up)')}`);
454
+ } else if (syncStatus.lastError) {
455
+ err(`Sync error: ${syncStatus.lastError}`);
456
+ hasError = true;
457
+ } else if (syncStatus.conflicts && syncStatus.conflicts.length > 0) {
458
+ warn(`Sync has ${syncStatus.conflicts.length} unresolved conflict(s) ${dim('(run `mindos sync conflicts` to view)')}`);
459
+ } else {
460
+ const unpushed = parseInt(syncStatus.unpushed || '0', 10);
461
+ const extra = unpushed > 0 ? ` ${dim(`(${unpushed} unpushed commit(s))`)}` : '';
462
+ ok(`Sync enabled ${dim(syncStatus.remote || 'origin')}${extra}`);
463
+ }
464
+ } catch {
465
+ warn('Could not check sync status');
466
+ }
467
+ }
468
+
448
469
  console.log(hasError
449
470
  ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
450
471
  : `\n${green('All checks passed.')}\n`);