@geminilight/mindos 0.2.1 → 0.4.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.
Files changed (82) hide show
  1. package/app/app/api/init/route.ts +7 -41
  2. package/app/app/api/mcp/agents/route.ts +72 -0
  3. package/app/app/api/mcp/install/route.ts +95 -0
  4. package/app/app/api/mcp/status/route.ts +47 -0
  5. package/app/app/api/settings/route.ts +3 -0
  6. package/app/app/api/setup/generate-token/route.ts +23 -0
  7. package/app/app/api/setup/route.ts +81 -0
  8. package/app/app/api/skills/route.ts +208 -0
  9. package/app/app/api/sync/route.ts +54 -3
  10. package/app/app/api/update-check/route.ts +52 -0
  11. package/app/app/globals.css +12 -0
  12. package/app/app/layout.tsx +4 -2
  13. package/app/app/login/page.tsx +20 -13
  14. package/app/app/page.tsx +22 -2
  15. package/app/app/setup/page.tsx +9 -0
  16. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  17. package/app/app/view/[...path]/loading.tsx +1 -1
  18. package/app/app/view/[...path]/not-found.tsx +101 -0
  19. package/app/components/AskFab.tsx +1 -1
  20. package/app/components/AskModal.tsx +1 -1
  21. package/app/components/Backlinks.tsx +1 -1
  22. package/app/components/Breadcrumb.tsx +13 -3
  23. package/app/components/CsvView.tsx +5 -6
  24. package/app/components/DirView.tsx +42 -21
  25. package/app/components/FindInPage.tsx +211 -0
  26. package/app/components/HomeContent.tsx +97 -44
  27. package/app/components/JsonView.tsx +1 -2
  28. package/app/components/MarkdownEditor.tsx +1 -2
  29. package/app/components/OnboardingView.tsx +6 -7
  30. package/app/components/SettingsModal.tsx +5 -2
  31. package/app/components/SetupWizard.tsx +479 -0
  32. package/app/components/Sidebar.tsx +1 -1
  33. package/app/components/UpdateBanner.tsx +101 -0
  34. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  35. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  36. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  37. package/app/components/renderers/backlinks/manifest.ts +14 -0
  38. package/app/components/renderers/config/manifest.ts +14 -0
  39. package/app/components/renderers/csv/BoardView.tsx +12 -12
  40. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  41. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  42. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  43. package/app/components/renderers/csv/TableView.tsx +4 -5
  44. package/app/components/renderers/csv/manifest.ts +14 -0
  45. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  46. package/app/components/renderers/diff/manifest.ts +14 -0
  47. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  48. package/app/components/renderers/graph/manifest.ts +14 -0
  49. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  50. package/app/components/renderers/summary/manifest.ts +14 -0
  51. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  52. package/app/components/renderers/timeline/manifest.ts +14 -0
  53. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  54. package/app/components/renderers/todo/manifest.ts +14 -0
  55. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  56. package/app/components/renderers/workflow/manifest.ts +14 -0
  57. package/app/components/settings/McpTab.tsx +549 -0
  58. package/app/components/settings/SyncTab.tsx +139 -50
  59. package/app/components/settings/types.ts +1 -1
  60. package/app/data/pages/home.png +0 -0
  61. package/app/lib/i18n.ts +270 -10
  62. package/app/lib/renderers/index.ts +20 -89
  63. package/app/lib/renderers/registry.ts +4 -1
  64. package/app/lib/settings.ts +15 -1
  65. package/app/lib/template.ts +45 -0
  66. package/app/package.json +1 -0
  67. package/app/types/semver.d.ts +8 -0
  68. package/bin/cli.js +137 -24
  69. package/bin/lib/build.js +53 -18
  70. package/bin/lib/colors.js +3 -1
  71. package/bin/lib/config.js +4 -0
  72. package/bin/lib/constants.js +2 -0
  73. package/bin/lib/debug.js +10 -0
  74. package/bin/lib/startup.js +21 -20
  75. package/bin/lib/stop.js +41 -3
  76. package/bin/lib/sync.js +65 -53
  77. package/bin/lib/update-check.js +94 -0
  78. package/bin/lib/utils.js +2 -2
  79. package/package.json +1 -1
  80. package/scripts/gen-renderer-index.js +57 -0
  81. package/scripts/setup.js +117 -1
  82. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -1,92 +1,23 @@
1
+ /**
2
+ * AUTO-GENERATED by scripts/gen-renderer-index.js — do not edit manually.
3
+ * To regenerate: node scripts/gen-renderer-index.js
4
+ */
1
5
  import { registerRenderer } from './registry';
2
- import { TodoRenderer } from '@/components/renderers/TodoRenderer';
3
- import { CsvRenderer } from '@/components/renderers/CsvRenderer';
4
- import { GraphRenderer } from '@/components/renderers/GraphRenderer';
5
- import { TimelineRenderer } from '@/components/renderers/TimelineRenderer';
6
- import { SummaryRenderer } from '@/components/renderers/SummaryRenderer';
7
- import { ConfigRenderer } from '@/components/renderers/ConfigRenderer';
8
- import { AgentInspectorRenderer } from '@/components/renderers/AgentInspectorRenderer';
6
+ import { manifest as agentInspector } from '@/components/renderers/agent-inspector/manifest';
7
+ import { manifest as backlinks } from '@/components/renderers/backlinks/manifest';
8
+ import { manifest as config } from '@/components/renderers/config/manifest';
9
+ import { manifest as csv } from '@/components/renderers/csv/manifest';
10
+ import { manifest as diff } from '@/components/renderers/diff/manifest';
11
+ import { manifest as graph } from '@/components/renderers/graph/manifest';
12
+ import { manifest as summary } from '@/components/renderers/summary/manifest';
13
+ import { manifest as timeline } from '@/components/renderers/timeline/manifest';
14
+ import { manifest as todo } from '@/components/renderers/todo/manifest';
15
+ import { manifest as workflow } from '@/components/renderers/workflow/manifest';
9
16
 
10
- registerRenderer({
11
- id: 'todo',
12
- name: 'TODO Board',
13
- description: 'Renders TODO.md/TODO.csv as an interactive kanban board grouped by section. Check items off directly — changes are written back to the source file.',
14
- author: 'MindOS',
15
- icon: '✅',
16
- tags: ['productivity', 'tasks', 'markdown'],
17
- builtin: true,
18
- match: ({ filePath }) => /\bTODO\b.*\.(md|csv)$/i.test(filePath),
19
- component: TodoRenderer,
20
- });
17
+ const manifests = [
18
+ agentInspector, backlinks, config, csv, diff, graph, summary, timeline, todo, workflow,
19
+ ];
21
20
 
22
- registerRenderer({
23
- id: 'csv',
24
- name: 'CSV Views',
25
- description: 'Renders any CSV file as Table, Gallery, or Board. Each view is independently configurable — choose which columns map to title, description, tag, and group.',
26
- author: 'MindOS',
27
- icon: '📊',
28
- tags: ['csv', 'table', 'gallery', 'board', 'data'],
29
- builtin: true,
30
- match: ({ extension, filePath }) => extension === 'csv' && !/\bTODO\b/i.test(filePath),
31
- component: CsvRenderer,
32
- });
33
-
34
- registerRenderer({
35
- id: 'config-panel',
36
- name: 'Config Panel',
37
- description: 'Renders CONFIG.json as an editable control panel based on uiSchema/keySpecs. Changes are written back to the JSON file directly.',
38
- author: 'MindOS',
39
- icon: '🧩',
40
- tags: ['config', 'json', 'settings', 'schema'],
41
- builtin: true,
42
- match: ({ filePath, extension }) => extension === 'json' && /(^|\/)CONFIG\.json$/i.test(filePath),
43
- component: ConfigRenderer,
44
- });
45
-
46
- registerRenderer({
47
- id: 'graph',
48
- name: 'Wiki Graph',
49
- description: 'Force-directed graph of wikilink references across all markdown files. Supports Global and Local (2-hop) scope filters.',
50
- author: 'MindOS',
51
- icon: '🕸️',
52
- tags: ['graph', 'wiki', 'links', 'visualization'],
53
- builtin: true,
54
- match: ({ extension }) => extension === 'md',
55
- component: GraphRenderer,
56
- });
57
-
58
- registerRenderer({
59
- id: 'timeline',
60
- name: 'Timeline',
61
- description: 'Renders changelog and journal files as a vertical timeline. Any markdown with ## date headings (e.g. ## 2025-01-15) becomes a card in the feed.',
62
- author: 'MindOS',
63
- icon: '📅',
64
- tags: ['timeline', 'changelog', 'journal', 'history'],
65
- builtin: true,
66
- match: ({ filePath }) => /\b(CHANGELOG|changelog|TIMELINE|timeline|journal|Journal|diary|Diary)\b.*\.md$/i.test(filePath),
67
- component: TimelineRenderer,
68
- });
69
-
70
- registerRenderer({
71
- id: 'summary',
72
- name: 'AI Briefing',
73
- description: 'Streams an AI-generated daily briefing summarizing your most recently modified files — key changes, recurring themes, and suggested next actions.',
74
- author: 'MindOS',
75
- icon: '✨',
76
- tags: ['ai', 'summary', 'briefing', 'daily'],
77
- builtin: true,
78
- match: ({ filePath }) => /\b(SUMMARY|summary|Summary|BRIEFING|briefing|Briefing|DAILY|daily|Daily)\b.*\.md$/i.test(filePath),
79
- component: SummaryRenderer,
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
+ for (const m of manifests) {
22
+ registerRenderer(m);
23
+ }
@@ -15,8 +15,11 @@ export interface RendererDefinition {
15
15
  icon: string; // emoji or short string
16
16
  tags: string[];
17
17
  builtin: boolean; // true = ships with MindOS; false = user-installed (future)
18
+ entryPath?: string; // canonical entry file shown on home page (e.g. 'TODO.md')
18
19
  match: (ctx: Pick<RendererContext, 'filePath' | 'extension'>) => boolean;
19
- component: ComponentType<RendererContext>;
20
+ // Provide either `component` (eager) or `load` (lazy). Prefer `load` for code-splitting.
21
+ component?: ComponentType<RendererContext>;
22
+ load?: () => Promise<{ default: ComponentType<RendererContext> }>;
20
23
  }
21
24
 
22
25
  const registry: RendererDefinition[] = [];
@@ -21,12 +21,13 @@ 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
+ disabledSkills?: string[];
30
31
  }
31
32
 
32
33
  const DEFAULTS: ServerSettings = {
@@ -108,6 +109,10 @@ export function readSettings(): ServerSettings {
108
109
  webPassword: typeof parsed.webPassword === 'string' ? parsed.webPassword : undefined,
109
110
  authToken: typeof parsed.authToken === 'string' ? parsed.authToken : undefined,
110
111
  mcpPort: typeof parsed.mcpPort === 'number' ? parsed.mcpPort : undefined,
112
+ port: typeof parsed.port === 'number' ? parsed.port : undefined,
113
+ startMode: typeof parsed.startMode === 'string' ? parsed.startMode as ServerSettings['startMode'] : undefined,
114
+ setupPending: parsed.setupPending === true ? true : undefined,
115
+ disabledSkills: Array.isArray(parsed.disabledSkills) ? parsed.disabledSkills as string[] : undefined,
111
116
  };
112
117
  } catch {
113
118
  return { ...DEFAULTS, ai: { ...DEFAULTS.ai, providers: { ...DEFAULTS.ai.providers } } };
@@ -123,6 +128,15 @@ export function writeSettings(settings: ServerSettings): void {
123
128
  const merged: Record<string, unknown> = { ...existing, ai: settings.ai, mindRoot: settings.mindRoot };
124
129
  if (settings.webPassword !== undefined) merged.webPassword = settings.webPassword;
125
130
  if (settings.authToken !== undefined) merged.authToken = settings.authToken;
131
+ if (settings.port !== undefined) merged.port = settings.port;
132
+ if (settings.mcpPort !== undefined) merged.mcpPort = settings.mcpPort;
133
+ if (settings.startMode !== undefined) merged.startMode = settings.startMode;
134
+ if (settings.disabledSkills !== undefined) merged.disabledSkills = settings.disabledSkills;
135
+ // setupPending: false/undefined → remove the field (cleanup); true → set it
136
+ if ('setupPending' in settings) {
137
+ if (settings.setupPending) merged.setupPending = true;
138
+ else delete merged.setupPending;
139
+ }
126
140
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
127
141
  }
128
142
 
@@ -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
+ }
package/app/package.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p ${MINDOS_WEB_PORT:-3000}",
7
+ "prebuild": "node ../scripts/gen-renderer-index.js",
7
8
  "build": "next build",
8
9
  "start": "next start -p ${MINDOS_WEB_PORT:-3000}",
9
10
  "lint": "eslint",
@@ -0,0 +1,8 @@
1
+ declare module 'semver' {
2
+ export function gt(v1: string, v2: string): boolean;
3
+ export function lt(v1: string, v2: string): boolean;
4
+ export function gte(v1: string, v2: string): boolean;
5
+ export function lte(v1: string, v2: string): boolean;
6
+ export function eq(v1: string, v2: string): boolean;
7
+ export function valid(v: string | null): string | null;
8
+ }
package/bin/cli.js CHANGED
@@ -33,6 +33,7 @@
33
33
  * mindos logs — tail service logs (~/.mindos/mindos.log)
34
34
  * mindos config show — print current config (API keys masked)
35
35
  * mindos config set <key> <val> — update a single config field
36
+ * mindos config unset <key> — remove a config field
36
37
  * mindos config validate — validate config file
37
38
  */
38
39
 
@@ -58,6 +59,15 @@ import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, l
58
59
  // ── Commands ──────────────────────────────────────────────────────────────────
59
60
 
60
61
  const cmd = process.argv[2];
62
+
63
+ // ── --version / -v ──────────────────────────────────────────────────────────
64
+ // --help / -h is handled at entry section (resolvedCmd = null → help block)
65
+ if (cmd === '--version' || cmd === '-v') {
66
+ const version = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
67
+ console.log(`mindos/${version} node/${process.version} ${process.platform}-${process.arch}`);
68
+ process.exit(0);
69
+ }
70
+
61
71
  const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
62
72
  const isVerbose = process.argv.includes('--verbose');
63
73
  const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
@@ -68,8 +78,8 @@ const commands = {
68
78
  const daemonFlag = process.argv.includes('--install-daemon') ? ' --install-daemon' : '';
69
79
  run(`node ${resolve(ROOT, 'scripts/setup.js')}${daemonFlag}`);
70
80
  },
71
- init: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
72
- setup: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
81
+ init: async () => commands.onboard(),
82
+ setup: async () => commands.onboard(),
73
83
 
74
84
  // ── open ───────────────────────────────────────────────────────────────────
75
85
  open: () => {
@@ -123,8 +133,8 @@ const commands = {
123
133
  console.log(`${sep}`);
124
134
  console.log(`${bold('Claude Code')}`);
125
135
  console.log(`${sep}`);
126
- console.log(dim('一键安装:') + ` mindos mcp install claude-code -g -y`);
127
- console.log(dim('\n手动配置 (~/.claude.json):'));
136
+ console.log(dim('Quick install:') + ` mindos mcp install claude-code -g -y`);
137
+ console.log(dim('\nManual config (~/.claude.json):'));
128
138
  console.log(JSON.stringify({
129
139
  mcpServers: {
130
140
  mindos: {
@@ -138,8 +148,8 @@ const commands = {
138
148
  console.log(`\n${sep}`);
139
149
  console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
140
150
  console.log(`${sep}`);
141
- console.log(dim('一键安装:') + ` mindos mcp install codebuddy -g -y`);
142
- console.log(dim('\n手动配置 (~/.claude-internal/.claude.json):'));
151
+ console.log(dim('Quick install:') + ` mindos mcp install codebuddy -g -y`);
152
+ console.log(dim('\nManual config (~/.claude-internal/.claude.json):'));
143
153
  console.log(JSON.stringify({
144
154
  mcpServers: {
145
155
  mindos: {
@@ -153,8 +163,8 @@ const commands = {
153
163
  console.log(`\n${sep}`);
154
164
  console.log(`${bold('Cursor')}`);
155
165
  console.log(`${sep}`);
156
- console.log(dim('一键安装:') + ` mindos mcp install cursor -g -y`);
157
- console.log(dim('\n手动配置 (~/.cursor/mcp.json):'));
166
+ console.log(dim('Quick install:') + ` mindos mcp install cursor -g -y`);
167
+ console.log(dim('\nManual config (~/.cursor/mcp.json):'));
158
168
  console.log(JSON.stringify({
159
169
  mcpServers: {
160
170
  mindos: {
@@ -168,7 +178,7 @@ const commands = {
168
178
  if (localIP) {
169
179
  const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
170
180
  console.log(`\n${sep}`);
171
- console.log(`${bold('Remote (其他设备)')}`);
181
+ console.log(`${bold('Remote (other devices)')}`);
172
182
  console.log(`${sep}`);
173
183
  console.log(`URL: ${cyan(remoteUrl)}`);
174
184
  console.log(JSON.stringify({
@@ -200,12 +210,21 @@ const commands = {
200
210
  if (devMindRoot) {
201
211
  startSyncDaemon(devMindRoot).catch(() => {});
202
212
  }
203
- printStartupInfo(webPort, mcpPort);
213
+ await printStartupInfo(webPort, mcpPort);
204
214
  run(`npx next dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
205
215
  },
206
216
 
207
217
  // ── start ──────────────────────────────────────────────────────────────────
208
218
  start: async () => {
219
+ // Check for incomplete setup
220
+ if (existsSync(CONFIG_PATH)) {
221
+ try {
222
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
223
+ if (cfg.setupPending === true) {
224
+ console.log(`\n ${yellow('⚠ Setup was not completed.')} Run ${cyan('mindos onboard')} to finish, or ${cyan('mindos config set setupPending false')} to dismiss.\n`);
225
+ }
226
+ } catch {}
227
+ }
209
228
  if (isDaemon) {
210
229
  const platform = getPlatform();
211
230
  if (!platform) {
@@ -225,13 +244,13 @@ const commands = {
225
244
  console.error(dim(' Check logs with: mindos logs\n'));
226
245
  process.exit(1);
227
246
  }
228
- printStartupInfo(webPort, mcpPort);
247
+ await printStartupInfo(webPort, mcpPort);
229
248
  // System notification
230
249
  try {
231
250
  if (process.platform === 'darwin') {
232
- execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS 已就绪"'`, { stdio: 'ignore' });
251
+ execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS Ready"'`, { stdio: 'ignore' });
233
252
  } else if (process.platform === 'linux') {
234
- execSync(`notify-send "MindOS 已就绪" "http://localhost:${webPort}"`, { stdio: 'ignore' });
253
+ execSync(`notify-send "MindOS Ready" "http://localhost:${webPort}"`, { stdio: 'ignore' });
235
254
  }
236
255
  } catch { /* notification is best-effort */ }
237
256
  console.log(`${green('✔ MindOS is running as a background service')}`);
@@ -250,6 +269,7 @@ const commands = {
250
269
  if (needsBuild()) {
251
270
  console.log(yellow('Building MindOS (first run or new version detected)...\n'));
252
271
  cleanNextDir();
272
+ run('node scripts/gen-renderer-index.js', ROOT);
253
273
  run('npx next build', resolve(ROOT, 'app'));
254
274
  writeBuildStamp();
255
275
  }
@@ -261,7 +281,7 @@ const commands = {
261
281
  if (mindRoot) {
262
282
  startSyncDaemon(mindRoot).catch(() => {});
263
283
  }
264
- printStartupInfo(webPort, mcpPort);
284
+ await printStartupInfo(webPort, mcpPort);
265
285
  run(`npx next start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
266
286
  },
267
287
 
@@ -269,6 +289,7 @@ const commands = {
269
289
  build: () => {
270
290
  ensureAppDeps();
271
291
  cleanNextDir();
292
+ run('node scripts/gen-renderer-index.js', ROOT);
272
293
  run(`npx next build ${extra}`, resolve(ROOT, 'app'));
273
294
  writeBuildStamp();
274
295
  },
@@ -466,6 +487,23 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
466
487
  }
467
488
  }
468
489
 
490
+ // 9. Update check
491
+ try {
492
+ const { checkForUpdate } = await import('./lib/update-check.js');
493
+ const latestVersion = await Promise.race([
494
+ checkForUpdate(),
495
+ new Promise(r => setTimeout(() => r(null), 4000)),
496
+ ]);
497
+ if (latestVersion) {
498
+ const currentVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
499
+ warn(`Update available: v${currentVersion} → ${bold(`v${latestVersion}`)} ${dim('run `mindos update`')}`);
500
+ } else {
501
+ ok('MindOS is up to date');
502
+ }
503
+ } catch {
504
+ warn('Could not check for updates');
505
+ }
506
+
469
507
  console.log(hasError
470
508
  ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
471
509
  : `\n${green('All checks passed.')}\n`);
@@ -575,7 +613,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
575
613
  display.authToken = maskKey(display.authToken);
576
614
  if (display.webPassword)
577
615
  display.webPassword = maskKey(display.webPassword);
578
- console.log(`\n${bold('📋 MindOS Config')} ${dim(CONFIG_PATH)}\n`);
616
+ console.log(`\n${bold('📋 MindOS Config')} ${dim(`v${(() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })()}`)} ${dim(CONFIG_PATH)}\n`);
579
617
  console.log(JSON.stringify(display, null, 2));
580
618
  console.log();
581
619
  return;
@@ -640,13 +678,50 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
640
678
  if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
641
679
  obj = obj[parts[i]];
642
680
  }
643
- const coerced = isNaN(Number(val)) ? val : Number(val);
681
+ // Coerce string values to appropriate types
682
+ function coerceValue(v) {
683
+ if (v === 'true') return true;
684
+ if (v === 'false') return false;
685
+ if (v === 'null') return null;
686
+ if (v === '""' || v === "''") return '';
687
+ if (v.trim() !== '' && !isNaN(Number(v))) return Number(v);
688
+ return v;
689
+ }
690
+ const coerced = coerceValue(val);
644
691
  obj[parts[parts.length - 1]] = coerced;
645
692
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
646
693
  console.log(`${green('✔')} Set ${cyan(key)} = ${bold(String(coerced))}`);
647
694
  return;
648
695
  }
649
696
 
697
+ if (sub === 'unset') {
698
+ const key = process.argv[4];
699
+ if (!key) {
700
+ console.error(red('Usage: mindos config unset <key>'));
701
+ process.exit(1);
702
+ }
703
+ if (!existsSync(CONFIG_PATH)) {
704
+ console.error(red('No config found. Run `mindos onboard` first.'));
705
+ process.exit(1);
706
+ }
707
+ let config;
708
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {
709
+ console.error(red('Failed to parse config file.'));
710
+ process.exit(1);
711
+ }
712
+ const parts = key.split('.');
713
+ let obj = config;
714
+ for (let i = 0; i < parts.length - 1; i++) {
715
+ if (!obj[parts[i]]) { console.log(dim(`Key "${key}" not found`)); return; }
716
+ obj = obj[parts[i]];
717
+ }
718
+ if (!(parts[parts.length - 1] in obj)) { console.log(dim(`Key "${key}" not found`)); return; }
719
+ delete obj[parts[parts.length - 1]];
720
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
721
+ console.log(`${green('✔')} Removed ${cyan(key)}`);
722
+ return;
723
+ }
724
+
650
725
  // no subcommand or unknown → show help
651
726
  const row = (c, d) => ` ${cyan(c.padEnd(32))}${dim(d)}`;
652
727
  console.log(`
@@ -656,10 +731,13 @@ ${bold('Subcommands:')}
656
731
  ${row('mindos config show', 'Print current config (API keys masked)')}
657
732
  ${row('mindos config validate', 'Validate config file')}
658
733
  ${row('mindos config set <key> <v>', 'Update a single field (dot-notation supported)')}
734
+ ${row('mindos config unset <key>', 'Remove a config field')}
659
735
 
660
736
  ${bold('Examples:')}
661
737
  ${dim('mindos config set port 3002')}
662
738
  ${dim('mindos config set ai.provider openai')}
739
+ ${dim('mindos config set setupPending false')}
740
+ ${dim('mindos config unset webPassword')}
663
741
  `);
664
742
  },
665
743
 
@@ -670,7 +748,22 @@ ${bold('Examples:')}
670
748
  const mindRoot = process.env.MIND_ROOT;
671
749
 
672
750
  if (sub === 'init') {
673
- await initSync(mindRoot);
751
+ // Parse --non-interactive --remote <url> --branch <branch> --token <token>
752
+ const args = process.argv.slice(4);
753
+ const flagIdx = (flag) => args.indexOf(flag);
754
+ const flagVal = (flag) => { const i = flagIdx(flag); return i >= 0 && i + 1 < args.length ? args[i + 1] : ''; };
755
+ const nonInteractive = args.includes('--non-interactive');
756
+
757
+ if (nonInteractive) {
758
+ await initSync(mindRoot, {
759
+ nonInteractive: true,
760
+ remote: flagVal('--remote'),
761
+ token: flagVal('--token'),
762
+ branch: flagVal('--branch') || 'main',
763
+ });
764
+ } else {
765
+ await initSync(mindRoot);
766
+ }
674
767
  return;
675
768
  }
676
769
 
@@ -695,6 +788,16 @@ ${bold('Examples:')}
695
788
  return;
696
789
  }
697
790
 
791
+ // Unknown subcommand check
792
+ if (sub) {
793
+ const validSubs = ['init', 'now', 'conflicts', 'on', 'off'];
794
+ if (!validSubs.includes(sub)) {
795
+ console.error(red(`Unknown sync subcommand: ${sub}`));
796
+ console.error(dim(`Available: ${validSubs.join(' | ')}`));
797
+ process.exit(1);
798
+ }
799
+ }
800
+
698
801
  // default: sync status
699
802
  const status = getSyncStatus(mindRoot);
700
803
  if (!status.enabled) {
@@ -727,15 +830,16 @@ ${bold('Examples:')}
727
830
 
728
831
  // ── Entry ─────────────────────────────────────────────────────────────────────
729
832
 
730
- const resolvedCmd = cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null);
833
+ const resolvedCmd = (cmd === '--help' || cmd === '-h') ? null : (cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null));
731
834
 
732
835
  if (!resolvedCmd || !commands[resolvedCmd]) {
836
+ const pkgVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
733
837
  const row = (c, d) => ` ${cyan(c.padEnd(36))}${dim(d)}`;
734
838
  console.log(`
735
- ${bold('🧠 MindOS CLI')}
839
+ ${bold('🧠 MindOS CLI')} ${dim(`v${pkgVersion}`)}
736
840
 
737
- ${bold('Usage:')}
738
- ${row('mindos onboard', 'Interactive setup (writes ~/.mindos/config.json)')}
841
+ ${bold('Core:')}
842
+ ${row('mindos onboard', 'Interactive setup (aliases: init, setup)')}
739
843
  ${row('mindos onboard --install-daemon', 'Setup + install & start as background OS service')}
740
844
  ${row('mindos start', 'Start app + MCP server (production, auto-rebuilds if needed)')}
741
845
  ${row('mindos start --daemon', 'Install + start as background OS service (survives terminal close)')}
@@ -745,19 +849,28 @@ ${row('mindos dev --turbopack', 'Start with Turbopack (faster HMR)')}
745
849
  ${row('mindos stop', 'Stop running MindOS processes')}
746
850
  ${row('mindos restart', 'Stop then start again')}
747
851
  ${row('mindos build', 'Build the app for production')}
852
+ ${row('mindos open', 'Open Web UI in the default browser')}
853
+
854
+ ${bold('MCP:')}
748
855
  ${row('mindos mcp', 'Start MCP server only')}
749
856
  ${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
750
- ${row('mindos open', 'Open Web UI in the default browser')}
751
857
  ${row('mindos token', 'Show current auth token and MCP config snippet')}
858
+
859
+ ${bold('Sync:')}
752
860
  ${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
861
+
862
+ ${bold('Gateway (Background Service):')}
753
863
  ${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
864
+
865
+ ${bold('Config & Diagnostics:')}
866
+ ${row('mindos config <subcommand>', 'View/update config (show/validate/set/unset)')}
754
867
  ${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
755
868
  ${row('mindos update', 'Update MindOS to the latest version')}
756
869
  ${row('mindos logs', 'Tail service logs (~/.mindos/mindos.log)')}
757
- ${row('mindos config <subcommand>', 'View/update config (show/validate/set)')}
758
870
  ${row('mindos', 'Start using mode saved in ~/.mindos/config.json')}
759
871
  `);
760
- process.exit(cmd ? 1 : 0);
872
+ const isHelp = (cmd === '--help' || cmd === '-h');
873
+ process.exit((cmd && !isHelp) ? 1 : 0);
761
874
  }
762
875
 
763
876
  commands[resolvedCmd]();
package/bin/lib/build.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
  import { resolve } from 'node:path';
4
- import { ROOT, BUILD_STAMP } from './constants.js';
5
+ import { ROOT, BUILD_STAMP, DEPS_STAMP } from './constants.js';
5
6
  import { red, dim, yellow } from './colors.js';
6
7
  import { run } from './utils.js';
7
8
 
@@ -36,24 +37,58 @@ export function cleanNextDir() {
36
37
  }
37
38
  }
38
39
 
40
+ function depsHash() {
41
+ const lockPath = resolve(ROOT, 'app', 'package-lock.json');
42
+ try {
43
+ const content = readFileSync(lockPath);
44
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function depsChanged() {
51
+ const currentHash = depsHash();
52
+ if (!currentHash) return true;
53
+ try {
54
+ const savedHash = readFileSync(DEPS_STAMP, 'utf-8').trim();
55
+ return savedHash !== currentHash;
56
+ } catch {
57
+ return true;
58
+ }
59
+ }
60
+
61
+ function writeDepsStamp() {
62
+ const hash = depsHash();
63
+ if (hash) {
64
+ try { writeFileSync(DEPS_STAMP, hash, 'utf-8'); } catch {}
65
+ }
66
+ }
67
+
39
68
  export function ensureAppDeps() {
40
69
  const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
41
- if (!existsSync(appNext)) {
42
- try {
43
- execSync('npm --version', { stdio: 'pipe' });
44
- } catch {
45
- console.error(red('\n\u2718 npm not found in PATH.\n'));
46
- console.error(' MindOS needs npm to install its app dependencies on first run.');
47
- console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
48
- console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
49
- console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
50
- console.error(' Example:');
51
- console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
52
- console.error(dim(' source ~/.profile\n'));
53
- console.error(' Then run `mindos start` again.\n');
54
- process.exit(1);
55
- }
56
- console.log(yellow('Installing app dependencies (first run)...\n'));
57
- run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
70
+ const needsInstall = !existsSync(appNext) || depsChanged();
71
+ if (!needsInstall) return;
72
+
73
+ try {
74
+ execSync('npm --version', { stdio: 'pipe' });
75
+ } catch {
76
+ console.error(red('\n\u2718 npm not found in PATH.\n'));
77
+ console.error(' MindOS needs npm to install its app dependencies on first run.');
78
+ console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
79
+ console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
80
+ console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
81
+ console.error(' Example:');
82
+ console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
83
+ console.error(dim(' source ~/.profile\n'));
84
+ console.error(' Then run `mindos start` again.\n');
85
+ process.exit(1);
58
86
  }
87
+
88
+ const label = existsSync(appNext)
89
+ ? 'Updating app dependencies (package-lock.json changed)...\n'
90
+ : 'Installing app dependencies (first run)...\n';
91
+ console.log(yellow(label));
92
+ run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
93
+ writeDepsStamp();
59
94
  }
package/bin/lib/colors.js CHANGED
@@ -1,4 +1,6 @@
1
- export const isTTY = process.stdout.isTTY;
1
+ const noColor = 'NO_COLOR' in process.env;
2
+ const forceColor = process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0';
3
+ export const isTTY = noColor ? false : (forceColor || process.stdout.isTTY);
2
4
  export const bold = (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s;
3
5
  export const dim = (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s;
4
6
  export const cyan = (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s;