@geminilight/mindos 0.6.23 → 0.6.27

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 (66) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/discover/route.ts +23 -0
  5. package/app/app/api/a2a/route.ts +100 -0
  6. package/app/components/Backlinks.tsx +2 -2
  7. package/app/components/Breadcrumb.tsx +1 -1
  8. package/app/components/CreateSpaceModal.tsx +1 -0
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/GuideCard.tsx +6 -2
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/ImportModal.tsx +3 -0
  14. package/app/components/OnboardingView.tsx +1 -0
  15. package/app/components/RightAskPanel.tsx +4 -2
  16. package/app/components/SearchModal.tsx +3 -3
  17. package/app/components/SidebarLayout.tsx +11 -2
  18. package/app/components/SyncStatusBar.tsx +2 -2
  19. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  20. package/app/components/ask/AskContent.tsx +22 -10
  21. package/app/components/ask/MentionPopover.tsx +2 -2
  22. package/app/components/ask/SessionTabBar.tsx +70 -0
  23. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  24. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  25. package/app/components/explore/UseCaseCard.tsx +2 -2
  26. package/app/components/help/HelpContent.tsx +6 -1
  27. package/app/components/panels/AgentsPanel.tsx +25 -2
  28. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  29. package/app/components/panels/DiscoverPanel.tsx +3 -3
  30. package/app/components/panels/PanelNavRow.tsx +2 -2
  31. package/app/components/panels/PluginsPanel.tsx +1 -1
  32. package/app/components/panels/SearchPanel.tsx +3 -3
  33. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  34. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  35. package/app/components/settings/AiTab.tsx +5 -4
  36. package/app/components/settings/KnowledgeTab.tsx +3 -1
  37. package/app/components/settings/McpTab.tsx +22 -4
  38. package/app/components/settings/SyncTab.tsx +2 -0
  39. package/app/components/settings/UpdateTab.tsx +1 -1
  40. package/app/components/setup/StepDots.tsx +5 -1
  41. package/app/components/setup/index.tsx +9 -3
  42. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  43. package/app/data/skills/mindos/SKILL.md +186 -0
  44. package/app/data/skills/mindos-zh/SKILL.md +185 -0
  45. package/app/hooks/useA2aRegistry.ts +53 -0
  46. package/app/hooks/useAskSession.ts +44 -25
  47. package/app/lib/a2a/a2a-tools.ts +212 -0
  48. package/app/lib/a2a/agent-card.ts +107 -0
  49. package/app/lib/a2a/client.ts +207 -0
  50. package/app/lib/a2a/index.ts +31 -0
  51. package/app/lib/a2a/orchestrator.ts +255 -0
  52. package/app/lib/a2a/task-handler.ts +228 -0
  53. package/app/lib/a2a/types.ts +212 -0
  54. package/app/lib/agent/tools.ts +6 -4
  55. package/app/lib/i18n-en.ts +52 -0
  56. package/app/lib/i18n-zh.ts +52 -0
  57. package/app/next-env.d.ts +1 -1
  58. package/bin/cli.js +183 -164
  59. package/bin/commands/agent.js +110 -0
  60. package/bin/commands/api.js +60 -0
  61. package/bin/commands/ask.js +3 -3
  62. package/bin/commands/file.js +13 -13
  63. package/bin/commands/search.js +51 -0
  64. package/bin/commands/space.js +64 -10
  65. package/bin/lib/command.js +10 -0
  66. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -54,19 +54,44 @@ import { needsBuild, writeBuildStamp, cleanNextDir, ensureAppDeps } from './lib/
54
54
  import { isPortInUse, assertPortFree } from './lib/port.js';
55
55
  import { savePids, clearPids } from './lib/pid.js';
56
56
  import { stopMindos } from './lib/stop.js';
57
- import { getPlatform, ensureMindosDir, waitForHttp, waitForPortFree, runGatewayCommand } from './lib/gateway.js';
58
57
  import { printStartupInfo, getLocalIP } from './lib/startup.js';
59
58
  import { spawnMcp } from './lib/mcp-spawn.js';
60
- import { ensureMcpBundle } from './lib/mcp-build.js';
61
- import { mcpInstall } from './lib/mcp-install.js';
62
- import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, listConflicts, setSyncEnabled } from './lib/sync.js';
63
59
  import { parseArgs } from './lib/command.js';
60
+ import { MCP_AGENTS, detectAgentPresence } from './lib/mcp-agents.js';
61
+
62
+ // Heavy modules — loaded lazily inside command handlers to speed up CLI cold start
63
+ // gateway.js (426 lines), mcp-install.js (335), sync.js (472), mcp-build.js (74)
64
+ const lazy = {
65
+ gateway: () => import('./lib/gateway.js'),
66
+ mcpInstall: () => import('./lib/mcp-install.js'),
67
+ sync: () => import('./lib/sync.js'),
68
+ mcpBuild: () => import('./lib/mcp-build.js'),
69
+ };
70
+
71
+ // Thin wrappers for lazy modules — same API as before, but loaded on first call
72
+ async function getPlatform() { return (await lazy.gateway()).getPlatform(); }
73
+ async function runGatewayCommand(sub) { return (await lazy.gateway()).runGatewayCommand(sub); }
74
+ async function waitForHttp(...a) { return (await lazy.gateway()).waitForHttp(...a); }
75
+ async function waitForPortFree(...a) { return (await lazy.gateway()).waitForPortFree(...a); }
76
+ async function ensureMindosDir() { return (await lazy.gateway()).ensureMindosDir(); }
77
+ async function ensureMcpBundle() { return (await lazy.mcpBuild()).ensureMcpBundle(); }
78
+ async function mcpInstall() { return (await lazy.mcpInstall()).mcpInstall(); }
79
+ async function initSync(...a) { return (await lazy.sync()).initSync(...a); }
80
+ async function startSyncDaemon(...a) { return (await lazy.sync()).startSyncDaemon(...a); }
81
+ async function stopSyncDaemon() { return (await lazy.sync()).stopSyncDaemon(); }
82
+ async function getSyncStatus(...a) { return (await lazy.sync()).getSyncStatus(...a); }
83
+ async function manualSync(...a) { return (await lazy.sync()).manualSync(...a); }
84
+ async function listConflicts(...a) { return (await lazy.sync()).listConflicts(...a); }
85
+ async function setSyncEnabled(...a) { return (await lazy.sync()).setSyncEnabled(...a); }
64
86
 
65
87
  // ── New modular commands ──────────────────────────────────────────────────────
66
88
  import * as fileCmd from './commands/file.js';
67
89
  import * as spaceCmd from './commands/space.js';
68
90
  import * as askCmd from './commands/ask.js';
69
91
  import * as statusCmd from './commands/status.js';
92
+ import * as apiCmd from './commands/api.js';
93
+ import * as agentCmd from './commands/agent.js';
94
+ import * as searchCmd from './commands/search.js';
70
95
 
71
96
  // ── Helpers ───────────────────────────────────────────────────────────────────
72
97
 
@@ -136,24 +161,24 @@ function buildIfNeeded(newRoot) {
136
161
 
137
162
  // ── Commands ──────────────────────────────────────────────────────────────────
138
163
 
139
- const cmd = process.argv[2];
164
+ // ── Unified arg parsing ────────────────────────────────────────────────────
165
+ const { command: cmd, args: cliArgs, flags: cliFlags } = parseArgs(process.argv.slice(2));
140
166
 
141
- // ── --version / -v ──────────────────────────────────────────────────────────
142
- // --help / -h is handled at entry section (resolvedCmd = null → help block)
143
- if (cmd === '--version' || cmd === '-v') {
167
+ if (cliFlags.version || cliFlags.v) {
144
168
  const version = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
145
169
  console.log(`mindos/${version} node/${process.version} ${process.platform}-${process.arch}`);
146
170
  process.exit(0);
147
171
  }
148
172
 
149
- const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
150
- const isVerbose = process.argv.includes('--verbose');
151
- const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose' && a !== '--turbo').join(' ');
173
+ // Backward compat: derive legacy variables from unified flags
174
+ const isDaemon = cliFlags.daemon === true || (!cmd && isDaemonMode());
175
+ const isVerbose = cliFlags.verbose === true;
176
+ const extra = cliArgs.filter(a => !a.startsWith('-')).join(' ');
152
177
 
153
178
  const commands = {
154
179
  // ── onboard ────────────────────────────────────────────────────────────────
155
180
  onboard: async () => {
156
- const daemonFlag = process.argv.includes('--install-daemon') ? ' --install-daemon' : '';
181
+ const daemonFlag = cliFlags['install-daemon'] ? ' --install-daemon' : '';
157
182
  run(`node ${resolve(ROOT, 'scripts/setup.js')}${daemonFlag}`);
158
183
  },
159
184
  init: async () => commands.onboard(),
@@ -201,74 +226,48 @@ const commands = {
201
226
  }
202
227
  const mcpPort = config.mcpPort || 8781;
203
228
  const localIP = getLocalIP();
204
-
205
229
  const localUrl = `http://localhost:${mcpPort}/mcp`;
206
- const sep = '━'.repeat(40);
207
-
208
- console.log(`\n${bold('🔑 Auth token:')} ${cyan(token)}\n`);
209
-
210
- // Claude Code
211
- console.log(`${sep}`);
212
- console.log(`${bold('Claude Code')}`);
213
- console.log(`${sep}`);
214
- console.log(dim('Quick install:') + ` mindos mcp install claude-code -g -y`);
215
- console.log(dim('\nManual config (~/.claude.json):'));
216
- console.log(JSON.stringify({
217
- mcpServers: {
218
- mindos: {
219
- url: localUrl,
220
- headers: { Authorization: `Bearer ${token}` },
221
- },
222
- },
223
- }, null, 2));
224
-
225
- // CodeBuddy (Claude Code Internal)
226
- console.log(`\n${sep}`);
227
- console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
228
- console.log(`${sep}`);
229
- console.log(dim('Quick install:') + ` mindos mcp install codebuddy -g -y`);
230
- console.log(dim('\nManual config (~/.claude-internal/.claude.json):'));
231
- console.log(JSON.stringify({
232
- mcpServers: {
233
- mindos: {
234
- url: localUrl,
235
- headers: { Authorization: `Bearer ${token}` },
236
- },
237
- },
238
- }, null, 2));
239
-
240
- // Cursor
241
- console.log(`\n${sep}`);
242
- console.log(`${bold('Cursor')}`);
243
- console.log(`${sep}`);
244
- console.log(dim('Quick install:') + ` mindos mcp install cursor -g -y`);
245
- console.log(dim('\nManual config (~/.cursor/mcp.json):'));
246
- console.log(JSON.stringify({
247
- mcpServers: {
248
- mindos: {
249
- url: localUrl,
250
- headers: { Authorization: `Bearer ${token}` },
251
- },
252
- },
253
- }, null, 2));
254
-
255
- // Remote
230
+
231
+ if (cliFlags.json) {
232
+ const data = { token, mcpPort, localUrl, remoteUrl: localIP ? `http://${localIP}:${mcpPort}/mcp` : null };
233
+ console.log(JSON.stringify(data, null, 2));
234
+ return;
235
+ }
236
+
237
+ const sep = dim('━'.repeat(40));
238
+ const snippet = (url) => JSON.stringify({
239
+ mcpServers: { mindos: { url, headers: { Authorization: `Bearer ${token}` } } },
240
+ }, null, 2);
241
+
242
+ console.log(`\n${bold('Auth token:')} ${cyan(token)}\n`);
243
+
244
+ // Show installed agents, then up to 3 uninstalled popular ones
245
+ const installed = [];
246
+ const others = [];
247
+ for (const [key, agent] of Object.entries(MCP_AGENTS)) {
248
+ (detectAgentPresence(key) ? installed : others).push([key, agent]);
249
+ }
250
+ const toShow = [...installed.slice(0, 8), ...others.slice(0, Math.max(0, 3 - installed.length))];
251
+
252
+ for (const [key, agent] of toShow) {
253
+ console.log(sep);
254
+ console.log(bold(agent.name));
255
+ console.log(dim('Install:') + ` mindos mcp install ${key} -g -y`);
256
+ if (agent.global) console.log(dim(`Config: ${agent.global}`));
257
+ console.log(snippet(localUrl));
258
+ console.log();
259
+ }
260
+
256
261
  if (localIP) {
257
- const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
258
- console.log(`\n${sep}`);
259
- console.log(`${bold('Remote (other devices)')}`);
260
- console.log(`${sep}`);
261
- console.log(`URL: ${cyan(remoteUrl)}`);
262
- console.log(JSON.stringify({
263
- mcpServers: {
264
- mindos: {
265
- url: remoteUrl,
266
- headers: { Authorization: `Bearer ${token}` },
267
- },
268
- },
269
- }, null, 2));
262
+ console.log(sep);
263
+ console.log(bold('Remote (other devices)'));
264
+ console.log(`URL: ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
265
+ console.log(snippet(`http://${localIP}:${mcpPort}/mcp`));
270
266
  }
271
267
 
268
+ if (toShow.length < installed.length) {
269
+ console.log(dim(`\n +${installed.length - toShow.length} more agents detected. Run \`mindos agent list\` to see all.`));
270
+ }
272
271
  console.log(dim('\nRun `mindos onboard` to regenerate.\n'));
273
272
  },
274
273
 
@@ -286,8 +285,7 @@ const commands = {
286
285
  ensureAppDeps();
287
286
  const mcp = spawnMcp(isVerbose);
288
287
  savePids(process.pid, mcp.pid);
289
- process.on('exit', () => { stopSyncDaemon(); clearPids(); });
290
- // Start sync daemon if enabled
288
+ process.on('exit', () => { stopSyncDaemon().catch(() => {}); clearPids(); });
291
289
  const devMindRoot = process.env.MIND_ROOT;
292
290
  if (devMindRoot) {
293
291
  startSyncDaemon(devMindRoot).catch(() => {});
@@ -308,7 +306,7 @@ const commands = {
308
306
  } catch {}
309
307
  }
310
308
  if (isDaemon) {
311
- const platform = getPlatform();
309
+ const platform = await getPlatform();
312
310
  if (!platform) {
313
311
  console.warn(yellow('Warning: daemon mode not supported on this platform. Falling back to foreground.'));
314
312
  } else {
@@ -401,7 +399,7 @@ const commands = {
401
399
  }
402
400
  const mcp = spawnMcp(isVerbose);
403
401
  savePids(process.pid, mcp.pid);
404
- process.on('exit', () => { stopSyncDaemon(); clearPids(); });
402
+ process.on('exit', () => { stopSyncDaemon().catch(() => {}); clearPids(); });
405
403
  // Start sync daemon if enabled
406
404
  const mindRoot = process.env.MIND_ROOT;
407
405
  if (mindRoot) {
@@ -425,12 +423,12 @@ const commands = {
425
423
  },
426
424
 
427
425
  mcp: async () => {
428
- const sub = process.argv[3];
429
- const restArgs = process.argv.slice(3);
426
+ const sub = cliArgs[0];
427
+ const restArgs = cliArgs;
430
428
  const hasInstallFlags = restArgs.some(a => ['-g', '--global', '-y', '--yes'].includes(a));
431
429
  if (sub === 'install' || hasInstallFlags) { await mcpInstall(); return; }
432
430
  loadConfig();
433
- ensureMcpBundle();
431
+ await ensureMcpBundle();
434
432
  // `mindos mcp` is the entry point for MCP clients (Claude Code, Cursor, etc.)
435
433
  // which communicate over stdin/stdout. Default to stdio; HTTP is handled by
436
434
  // `mindos start` via spawnMcp(). Callers can still override via env.
@@ -490,7 +488,7 @@ const commands = {
490
488
 
491
489
  // ── gateway ────────────────────────────────────────────────────────────────
492
490
  gateway: async () => {
493
- const sub = process.argv[3];
491
+ const sub = cliArgs[0];
494
492
  if (!sub) {
495
493
  const row = (c, d) => ` ${cyan(c.padEnd(32))}${dim(d)}`;
496
494
  console.log(`
@@ -553,25 +551,28 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
553
551
 
554
552
  // ── doctor ─────────────────────────────────────────────────────────────────
555
553
  doctor: async () => {
556
- const ok = (msg) => console.log(` ${green('✔')} ${msg}`);
557
- const err = (msg) => console.log(` ${red('✘')} ${msg}`);
558
- const warn= (msg) => console.log(` ${yellow('!')} ${msg}`);
554
+ const jsonMode = cliFlags.json === true;
555
+ const checks = [];
556
+ const ok = (msg, key) => { checks.push({ status: 'ok', key, msg }); if (!jsonMode) console.log(` ${green('')} ${msg}`); };
557
+ const err = (msg, key) => { checks.push({ status: 'error', key, msg }); if (!jsonMode) console.log(` ${red('✘')} ${msg}`); };
558
+ const warn= (msg, key) => { checks.push({ status: 'warn', key, msg }); if (!jsonMode) console.log(` ${yellow('!')} ${msg}`); };
559
559
 
560
- console.log(`\n${bold('🩺 MindOS Doctor')}\n`);
560
+ if (!jsonMode) console.log(`\n${bold('MindOS Doctor')}\n`);
561
561
  let hasError = false;
562
562
 
563
563
  // 1. config file
564
564
  if (!existsSync(CONFIG_PATH)) {
565
- err(`Config not found at ${dim(CONFIG_PATH)}`);
566
- console.log(`\n ${dim('Run `mindos onboard` to create it.')}\n`);
565
+ err(`Config not found at ${dim(CONFIG_PATH)}`, 'config');
566
+ if (!jsonMode) { console.log(`\n ${dim('Run `mindos onboard` to create it.')}\n`); }
567
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, checks }, null, 2)); }
567
568
  process.exit(1);
568
569
  }
569
570
  let config;
570
571
  try {
571
572
  config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
572
- ok(`Config file found and valid JSON ${dim(CONFIG_PATH)}`);
573
+ ok(`Config file found and valid JSON ${dim(CONFIG_PATH)}`, 'config');
573
574
  } catch {
574
- err(`Config file exists but failed to parse ${dim(CONFIG_PATH)}`);
575
+ err(`Config file exists but failed to parse ${dim(CONFIG_PATH)}`, 'config');
575
576
  hasError = true;
576
577
  }
577
578
 
@@ -654,7 +655,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
654
655
  }
655
656
 
656
657
  // 7. Daemon status
657
- const platform = getPlatform();
658
+ const platform = await getPlatform();
658
659
  if (platform === 'systemd') {
659
660
  try {
660
661
  execSync('systemctl --user is-active mindos', { stdio: 'pipe' });
@@ -675,7 +676,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
675
676
  // 8. Sync status
676
677
  if (config?.mindRoot) {
677
678
  try {
678
- const syncStatus = getSyncStatus(config.mindRoot);
679
+ const syncStatus = await getSyncStatus(config.mindRoot);
679
680
  if (!syncStatus.enabled) {
680
681
  warn(`Cross-device sync is not configured ${dim('(run `mindos sync init` to set up)')}`);
681
682
  } else if (syncStatus.lastError) {
@@ -710,9 +711,14 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
710
711
  warn('Could not check for updates');
711
712
  }
712
713
 
713
- console.log(hasError
714
- ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
715
- : `\n${green('All checks passed.')}\n`);
714
+ if (jsonMode) {
715
+ const hasErr = checks.some(c => c.status === 'error');
716
+ console.log(JSON.stringify({ ok: !hasErr, checks }, null, 2));
717
+ } else {
718
+ console.log(hasError
719
+ ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
720
+ : `\n${green('All checks passed.')}\n`);
721
+ }
716
722
  if (hasError) process.exit(1);
717
723
  },
718
724
 
@@ -760,7 +766,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
760
766
  return;
761
767
  }
762
768
 
763
- const updatePlatform = getPlatform();
769
+ const updatePlatform = await getPlatform();
764
770
  let daemonRunning = false;
765
771
  if (updatePlatform === 'systemd') {
766
772
  try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
@@ -974,7 +980,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
974
980
  try { stopMindos(); } catch { /* may not be running */ }
975
981
 
976
982
  // 2. Remove daemon (skip if platform unsupported)
977
- if (getPlatform()) {
983
+ if (await getPlatform()) {
978
984
  try {
979
985
  await runGatewayCommand('uninstall');
980
986
  } catch {
@@ -1037,14 +1043,14 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1037
1043
  },
1038
1044
 
1039
1045
  // ── logs ───────────────────────────────────────────────────────────────────
1040
- logs: () => {
1041
- ensureMindosDir();
1046
+ logs: async () => {
1047
+ await ensureMindosDir();
1042
1048
  if (!existsSync(LOG_PATH)) {
1043
1049
  console.log(dim(`No log file yet at ${LOG_PATH}`));
1044
1050
  console.log(dim('Logs are created when starting MindOS (mindos start, mindos onboard, or daemon mode).'));
1045
1051
  process.exit(0);
1046
1052
  }
1047
- const noFollow = process.argv.includes('--no-follow');
1053
+ const noFollow = cliFlags['no-follow'] === true;
1048
1054
  if (noFollow) {
1049
1055
  execSync(`tail -n 100 ${LOG_PATH}`, { stdio: 'inherit' });
1050
1056
  } else {
@@ -1054,7 +1060,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1054
1060
 
1055
1061
  // ── config ─────────────────────────────────────────────────────────────────
1056
1062
  config: () => {
1057
- const sub = process.argv[3];
1063
+ const sub = cliArgs[0];
1058
1064
 
1059
1065
  function maskKey(val) {
1060
1066
  if (!val) return val;
@@ -1085,7 +1091,11 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1085
1091
  display.authToken = maskKey(display.authToken);
1086
1092
  if (display.webPassword)
1087
1093
  display.webPassword = maskKey(display.webPassword);
1088
- 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`);
1094
+ if (cliFlags.json) {
1095
+ console.log(JSON.stringify(display, null, 2));
1096
+ return;
1097
+ }
1098
+ 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`);
1089
1099
  console.log(JSON.stringify(display, null, 2));
1090
1100
  console.log();
1091
1101
  return;
@@ -1125,8 +1135,8 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1125
1135
  }
1126
1136
 
1127
1137
  if (sub === 'set') {
1128
- const key = process.argv[4];
1129
- const val = process.argv[5];
1138
+ const key = cliArgs[1];
1139
+ const val = cliArgs[2];
1130
1140
  if (!key || val === undefined) {
1131
1141
  console.error(red('Usage: mindos config set <key> <value>'));
1132
1142
  console.error(dim(' Examples:'));
@@ -1167,7 +1177,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1167
1177
  }
1168
1178
 
1169
1179
  if (sub === 'unset') {
1170
- const key = process.argv[4];
1180
+ const key = cliArgs[1];
1171
1181
  if (!key) {
1172
1182
  console.error(red('Usage: mindos config unset <key>'));
1173
1183
  process.exit(1);
@@ -1215,13 +1225,13 @@ ${bold('Examples:')}
1215
1225
 
1216
1226
  // ── sync ──────────────────────────────────────────────────────────────────
1217
1227
  sync: async () => {
1218
- const sub = process.argv[3];
1228
+ const sub = cliArgs[0];
1219
1229
  loadConfig();
1220
1230
  const mindRoot = process.env.MIND_ROOT;
1221
1231
 
1222
1232
  if (sub === 'init') {
1223
1233
  // Parse --non-interactive --remote <url> --branch <branch> --token <token>
1224
- const args = process.argv.slice(4);
1234
+ const args = cliArgs.slice(1);
1225
1235
  const flagIdx = (flag) => args.indexOf(flag);
1226
1236
  const flagVal = (flag) => { const i = flagIdx(flag); return i >= 0 && i + 1 < args.length ? args[i + 1] : ''; };
1227
1237
  const nonInteractive = args.includes('--non-interactive');
@@ -1242,7 +1252,7 @@ ${bold('Examples:')}
1242
1252
  if (sub === 'now') {
1243
1253
  try {
1244
1254
  console.log(dim('Pulling...'));
1245
- manualSync(mindRoot);
1255
+ await manualSync(mindRoot);
1246
1256
  console.log(green('✔ Sync complete'));
1247
1257
  } catch (err) {
1248
1258
  console.error(red(err.message));
@@ -1252,18 +1262,18 @@ ${bold('Examples:')}
1252
1262
  }
1253
1263
 
1254
1264
  if (sub === 'conflicts') {
1255
- listConflicts(mindRoot);
1265
+ await listConflicts(mindRoot);
1256
1266
  return;
1257
1267
  }
1258
1268
 
1259
1269
  if (sub === 'on') {
1260
- setSyncEnabled(true);
1270
+ await setSyncEnabled(true);
1261
1271
  return;
1262
1272
  }
1263
1273
 
1264
1274
  if (sub === 'off') {
1265
- setSyncEnabled(false);
1266
- stopSyncDaemon();
1275
+ await setSyncEnabled(false);
1276
+ await stopSyncDaemon();
1267
1277
  return;
1268
1278
  }
1269
1279
 
@@ -1278,9 +1288,15 @@ ${bold('Examples:')}
1278
1288
  }
1279
1289
 
1280
1290
  // default: sync status
1281
- const status = getSyncStatus(mindRoot);
1291
+ const status = await getSyncStatus(mindRoot);
1292
+
1293
+ if (cliFlags.json) {
1294
+ console.log(JSON.stringify(status, null, 2));
1295
+ return;
1296
+ }
1297
+
1282
1298
  if (!status.enabled) {
1283
- console.log(`\n${bold('🔄 Sync Status')}`);
1299
+ console.log(`\n${bold('Sync Status')}`);
1284
1300
  console.log(dim(' Not configured. Run `mindos sync init` to set up.\n'));
1285
1301
  return;
1286
1302
  }
@@ -1293,7 +1309,7 @@ ${bold('Examples:')}
1293
1309
  })()
1294
1310
  : 'never';
1295
1311
 
1296
- console.log(`\n${bold('🔄 Sync Status')}`);
1312
+ console.log(`\n${bold('Sync Status')}`);
1297
1313
  console.log(` ${dim('Provider:')} ${cyan(`${status.provider} (${status.remote})`)}`);
1298
1314
  console.log(` ${dim('Branch:')} ${cyan(status.branch)}`);
1299
1315
  console.log(` ${dim('Last sync:')} ${ago}`);
@@ -1307,61 +1323,64 @@ ${bold('Examples:')}
1307
1323
  },
1308
1324
 
1309
1325
  // ── New modular commands (knowledge operations) ──────────────────────────
1310
- file: async () => { const p = parseArgs(process.argv.slice(3)); await fileCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1311
- space: async () => { const p = parseArgs(process.argv.slice(3)); await spaceCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1312
- ask: async () => { const p = parseArgs(process.argv.slice(3)); await askCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1313
- status: async () => { const p = parseArgs(process.argv.slice(3)); await statusCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1326
+ file: async () => fileCmd.run(cliArgs, cliFlags),
1327
+ space: async () => spaceCmd.run(cliArgs, cliFlags),
1328
+ ask: async () => askCmd.run(cliArgs, cliFlags),
1329
+ status: async () => statusCmd.run(cliArgs, cliFlags),
1330
+ api: async () => apiCmd.run(cliArgs, cliFlags),
1331
+ agent: async () => agentCmd.run(cliArgs, cliFlags),
1332
+ search: async () => searchCmd.run(cliArgs, cliFlags),
1314
1333
  };
1315
1334
 
1316
1335
  // ── Entry ─────────────────────────────────────────────────────────────────────
1317
1336
 
1318
- const resolvedCmd = (cmd === '--help' || cmd === '-h') ? null : (cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null));
1337
+ const resolvedCmd = (cliFlags.help || cliFlags.h) ? null : (cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null));
1319
1338
 
1320
1339
  if (!resolvedCmd || !commands[resolvedCmd]) {
1321
1340
  const pkgVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
1322
1341
  const row = (c, d) => ` ${cyan(c.padEnd(36))}${dim(d)}`;
1323
- console.log(`
1324
- ${bold('MindOS CLI')} ${dim(`v${pkgVersion}`)}
1325
-
1326
- ${bold('Core:')}
1327
- ${row('mindos onboard', 'Interactive setup (aliases: init, setup)')}
1328
- ${row('mindos start', 'Start app + MCP server (production)')}
1329
- ${row('mindos start --daemon', 'Start as background OS service')}
1330
- ${row('mindos dev', 'Start in dev mode')}
1331
- ${row('mindos stop', 'Stop running processes')}
1332
- ${row('mindos restart', 'Stop then start again')}
1333
- ${row('mindos build', 'Build for production')}
1334
- ${row('mindos status', 'Show service status overview')}
1335
- ${row('mindos open', 'Open Web UI in browser')}
1336
-
1337
- ${bold('Knowledge:')}
1338
- ${row('mindos file <sub>', 'File operations (list/read/create/delete/search)')}
1339
- ${row('mindos space <sub>', 'Space management (list/create/info)')}
1340
- ${row('mindos ask "<question>"', 'Ask AI using your knowledge base')}
1341
-
1342
- ${bold('MCP:')}
1343
- ${row('mindos mcp', 'Start MCP server only')}
1344
- ${row('mindos mcp install [agent]', 'Install MCP config into Agent')}
1345
- ${row('mindos token', 'Show auth token and MCP config')}
1346
-
1347
- ${bold('Sync:')}
1348
- ${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
1349
-
1350
- ${bold('Gateway (Background Service):')}
1351
- ${row('mindos gateway <sub>', 'Manage service (install/start/stop/status/logs)')}
1352
-
1353
- ${bold('Config & Diagnostics:')}
1354
- ${row('mindos config <sub>', 'View/update config (show/set/unset/validate)')}
1355
- ${row('mindos doctor', 'Health check')}
1356
- ${row('mindos update', 'Update to latest version')}
1357
- ${row('mindos logs', 'Tail service logs')}
1358
-
1359
- ${bold('Global Flags:')}
1360
- ${row('--json', 'Output in JSON (for AI agents)')}
1361
- ${row('--help, -h', 'Show help')}
1362
- ${row('--version, -v', 'Show version')}
1363
- `);
1364
- const isHelp = (cmd === '--help' || cmd === '-h');
1342
+
1343
+ // Command registry — help is generated entirely from this array.
1344
+ // Modular commands provide meta via their exports; inline commands define meta here.
1345
+ const helpRegistry = [
1346
+ { group: 'Core', usage: 'mindos onboard', summary: 'Interactive setup (aliases: init, setup)' },
1347
+ { group: 'Core', usage: 'mindos start', summary: 'Start app + MCP server (production)' },
1348
+ { group: 'Core', usage: 'mindos start --daemon', summary: 'Start as background OS service' },
1349
+ { group: 'Core', usage: 'mindos dev', summary: 'Start in dev mode' },
1350
+ { group: 'Core', usage: 'mindos stop', summary: 'Stop running processes' },
1351
+ { group: 'Core', usage: 'mindos restart', summary: 'Stop then start again' },
1352
+ { group: 'Core', usage: 'mindos build', summary: 'Build for production' },
1353
+ statusCmd.meta,
1354
+ { group: 'Core', usage: 'mindos open', summary: 'Open Web UI in browser' },
1355
+ fileCmd.meta, spaceCmd.meta, searchCmd.meta, askCmd.meta, agentCmd.meta, apiCmd.meta,
1356
+ { group: 'MCP', usage: 'mindos mcp', summary: 'Start MCP server only' },
1357
+ { group: 'MCP', usage: 'mindos mcp install [agent]', summary: 'Install MCP config into Agent' },
1358
+ { group: 'MCP', usage: 'mindos token', summary: 'Show auth token and MCP config' },
1359
+ { group: 'Sync', usage: 'mindos sync', summary: 'Show sync status (init/now/conflicts/on/off)' },
1360
+ { group: 'Gateway', usage: 'mindos gateway <sub>', summary: 'Manage service (install/start/stop/status/logs)' },
1361
+ { group: 'Config', usage: 'mindos config <sub>', summary: 'View/update config (show/set/unset/validate)' },
1362
+ { group: 'Config', usage: 'mindos doctor', summary: 'Health check' },
1363
+ { group: 'Config', usage: 'mindos update', summary: 'Update to latest version' },
1364
+ { group: 'Config', usage: 'mindos uninstall', summary: 'Fully uninstall MindOS' },
1365
+ { group: 'Config', usage: 'mindos logs', summary: 'Tail service logs' },
1366
+ ];
1367
+
1368
+ const groupLabels = [
1369
+ ['Core', 'Core'], ['Knowledge', 'Knowledge'], ['MCP', 'MCP'],
1370
+ ['Sync', 'Sync'], ['Gateway', 'Gateway (Background Service)'], ['Config', 'Config & Diagnostics'],
1371
+ ];
1372
+ const groups = {};
1373
+ for (const e of helpRegistry) {
1374
+ const g = e.group || 'Other';
1375
+ if (!groups[g]) groups[g] = [];
1376
+ groups[g].push(row(e.usage || `mindos ${e.name}`, e.summary));
1377
+ }
1378
+ const sections = groupLabels
1379
+ .filter(([k]) => groups[k])
1380
+ .map(([k, label]) => `${bold(`${label}:`)}\n${groups[k].join('\n')}`);
1381
+
1382
+ console.log(`\n${bold('MindOS CLI')} ${dim(`v${pkgVersion}`)}\n\n${sections.join('\n\n')}\n\n${bold('Global Flags:')}\n${row('--json', 'Output in JSON (for AI agents)')}\n${row('--help, -h', 'Show help')}\n${row('--version, -v', 'Show version')}\n`);
1383
+ const isHelp = cliFlags.help || cliFlags.h;
1365
1384
  process.exit((cmd && !isHelp) ? 1 : 0);
1366
1385
  }
1367
1386