@geminilight/mindos 0.6.40 → 0.6.41

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 (287) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +18 -18
  4. package/_standalone/.next/build-manifest.json +3 -3
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/react-loadable-manifest.json +5 -1
  10. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/_standalone/.next/server/app/_global-error.html +2 -2
  13. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  21. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  25. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js +2 -2
  27. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  28. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  39. package/_standalone/.next/server/app/api/ask/route.js +1 -1
  40. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  41. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  43. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  44. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  45. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  47. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  48. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  49. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  50. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  51. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  52. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  54. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  56. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  57. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  58. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  60. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  62. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  64. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  65. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  66. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  67. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  68. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  69. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/mcp/install/route.js +1 -1
  71. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/mcp/install-skill/route.js.nft.json +1 -1
  73. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  75. package/_standalone/.next/server/app/api/mcp/status/route.js +1 -1
  76. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  78. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  80. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  83. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  85. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  87. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  88. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  90. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  93. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  94. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  96. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  97. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  99. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  100. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  102. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  103. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  104. package/_standalone/.next/server/app/changes/page.js +1 -1
  105. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  106. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  107. package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
  108. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  109. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/echo/page.js +1 -1
  111. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  112. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  113. package/_standalone/.next/server/app/explore/page.js +1 -1
  114. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  115. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  116. package/_standalone/.next/server/app/help/page.js +2 -2
  117. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  118. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  119. package/_standalone/.next/server/app/login/page.js +1 -1
  120. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  121. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  122. package/_standalone/.next/server/app/page.js +2 -2
  123. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  124. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  125. package/_standalone/.next/server/app/setup/page.js +2 -2
  126. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  127. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  128. package/_standalone/.next/server/app/trash/page.js +3 -3
  129. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  130. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  131. package/_standalone/.next/server/app/view/[...path]/page.js +3 -3
  132. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  133. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  134. package/_standalone/.next/server/app-paths-manifest.json +18 -18
  135. package/_standalone/.next/server/chunks/1550.js +1 -1
  136. package/_standalone/.next/server/chunks/2190.js +11 -0
  137. package/_standalone/.next/server/chunks/{6365.js → 2536.js} +2 -2
  138. package/_standalone/.next/server/chunks/5648.js +2 -0
  139. package/_standalone/.next/server/chunks/8388.js +1 -1
  140. package/_standalone/.next/server/chunks/953.js +1 -1
  141. package/_standalone/.next/server/chunks/9539.js +219 -0
  142. package/_standalone/.next/server/middleware-build-manifest.js +1 -1
  143. package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  144. package/_standalone/.next/server/next-font-manifest.js +1 -1
  145. package/_standalone/.next/server/next-font-manifest.json +1 -1
  146. package/_standalone/.next/server/pages/500.html +2 -2
  147. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  148. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  149. package/_standalone/.next/static/chunks/1053-b70535785cc5aaee.js +29 -0
  150. package/_standalone/.next/static/chunks/{8663-de911d2d395622be.js → 1880-c2a9e76201841c86.js} +1 -1
  151. package/_standalone/.next/static/chunks/3637.0541ac2d0ea7de1f.js +1 -0
  152. package/_standalone/.next/static/chunks/4563-b2a2ce80aff845af.js +6 -0
  153. package/_standalone/.next/static/chunks/6981-3d7dcac2d12a5670.js +1 -0
  154. package/_standalone/.next/static/chunks/7144-5febf62f1a79fe64.js +1 -0
  155. package/_standalone/.next/static/chunks/app/agents/[agentKey]/page-773071a99c4daac2.js +1 -0
  156. package/_standalone/.next/static/chunks/app/agents/page-6102a884b2cb3cfe.js +5 -0
  157. package/_standalone/.next/static/chunks/app/help/page-2325d25b6846ca07.js +1 -0
  158. package/_standalone/.next/static/chunks/app/{layout-9378c1c8d3e5761b.js → layout-42cdbce19f404567.js} +34 -34
  159. package/_standalone/.next/static/chunks/app/{page-9bae420fbbdc5fff.js → page-8c9643b649e01735.js} +1 -1
  160. package/_standalone/.next/static/chunks/app/setup/page-d158b8cb533feb1e.js +1 -0
  161. package/_standalone/.next/static/chunks/app/trash/{page-b61ef2d5cd4f8d73.js → page-e9ab74ffeb96af41.js} +1 -1
  162. package/_standalone/.next/static/chunks/app/view/[...path]/page-764a69a1c8bd4eef.js +12 -0
  163. package/_standalone/.next/static/chunks/{webpack-c28c55d0a6021a6b.js → webpack-7b276daaa930d480.js} +1 -1
  164. package/_standalone/.next/static/css/bc9179074eaf65ae.css +1 -0
  165. package/_standalone/.next/trace +63 -63
  166. package/_standalone/__tests__/api/mcp-install.test.ts +23 -0
  167. package/_standalone/__tests__/cli/agent-routing.test.ts +232 -0
  168. package/_standalone/__tests__/cli/file-subcommands.test.ts +379 -0
  169. package/_standalone/__tests__/core/tools.test.ts +3 -6
  170. package/_standalone/components/FileTree.tsx +3 -2
  171. package/_standalone/components/MarkdownView.tsx +30 -15
  172. package/_standalone/components/RightAskPanel.tsx +36 -6
  173. package/_standalone/components/Sidebar.tsx +3 -3
  174. package/_standalone/components/agents/AgentsMcpSection.tsx +3 -0
  175. package/_standalone/components/settings/McpAgentInstall.tsx +94 -27
  176. package/_standalone/components/settings/McpSkillsSection.tsx +1 -1
  177. package/_standalone/components/settings/McpTab.tsx +484 -340
  178. package/_standalone/components/settings/SettingsContent.tsx +12 -6
  179. package/_standalone/components/settings/types.ts +3 -0
  180. package/_standalone/components/setup/StepAgents.tsx +113 -47
  181. package/_standalone/components/setup/StepReview.tsx +14 -27
  182. package/_standalone/components/setup/types.ts +6 -0
  183. package/_standalone/data/skills/mindos/SKILL.md +92 -92
  184. package/_standalone/data/skills/mindos/references/write-supplement.md +119 -0
  185. package/_standalone/data/skills/mindos-zh/SKILL.md +100 -104
  186. package/_standalone/data/skills/mindos-zh/references/write-supplement.md +119 -0
  187. package/_standalone/lib/i18n/modules/features.ts +4 -4
  188. package/_standalone/lib/i18n/modules/knowledge.ts +4 -0
  189. package/_standalone/lib/i18n/modules/onboarding.ts +40 -30
  190. package/_standalone/lib/i18n/modules/settings.ts +78 -6
  191. package/_standalone/lib/mcp-snippets.ts +5 -1
  192. package/_standalone/tsconfig.tsbuildinfo +1 -1
  193. package/app/app/api/ask/route.ts +3 -2
  194. package/app/app/api/mcp/install/route.ts +2 -1
  195. package/app/app/api/mcp/status/route.ts +14 -6
  196. package/app/app/view/[...path]/ViewPageClient.tsx +12 -27
  197. package/app/components/FileTree.tsx +3 -2
  198. package/app/components/MarkdownView.tsx +30 -15
  199. package/app/components/RightAskPanel.tsx +36 -6
  200. package/app/components/Sidebar.tsx +3 -3
  201. package/app/components/agents/AgentsMcpSection.tsx +3 -0
  202. package/app/components/help/HelpContent.tsx +1 -0
  203. package/app/components/settings/McpAgentInstall.tsx +94 -27
  204. package/app/components/settings/McpSkillsSection.tsx +1 -1
  205. package/app/components/settings/McpTab.tsx +484 -340
  206. package/app/components/settings/SettingsContent.tsx +12 -6
  207. package/app/components/settings/types.ts +3 -0
  208. package/app/components/setup/StepAgents.tsx +113 -47
  209. package/app/components/setup/StepReview.tsx +14 -27
  210. package/app/components/setup/index.tsx +12 -11
  211. package/app/components/setup/types.ts +6 -0
  212. package/app/data/skills/mindos/SKILL.md +92 -92
  213. package/app/data/skills/mindos/references/write-supplement.md +119 -0
  214. package/app/data/skills/mindos-zh/SKILL.md +100 -104
  215. package/app/data/skills/mindos-zh/references/write-supplement.md +119 -0
  216. package/app/lib/fs.ts +0 -6
  217. package/app/lib/i18n/modules/features.ts +4 -4
  218. package/app/lib/i18n/modules/knowledge.ts +4 -0
  219. package/app/lib/i18n/modules/onboarding.ts +40 -30
  220. package/app/lib/i18n/modules/settings.ts +78 -6
  221. package/app/lib/mcp-agents.ts +1 -2
  222. package/app/lib/mcp-snippets.ts +5 -1
  223. package/app/lib/renderers/index.ts +2 -1
  224. package/bin/cli.js +168 -1404
  225. package/bin/commands/agent.js +156 -20
  226. package/bin/commands/api.js +14 -11
  227. package/bin/commands/ask.js +79 -68
  228. package/bin/commands/build.js +26 -0
  229. package/bin/commands/config.js +170 -0
  230. package/bin/commands/dev.js +58 -0
  231. package/bin/commands/doctor.js +205 -0
  232. package/bin/commands/file.js +551 -36
  233. package/bin/commands/gateway.js +42 -0
  234. package/bin/commands/init-skills.js +56 -0
  235. package/bin/commands/logs.js +32 -0
  236. package/bin/commands/mcp-cmd.js +57 -0
  237. package/bin/commands/onboard.js +25 -0
  238. package/bin/commands/open.js +41 -0
  239. package/bin/commands/restart.js +48 -0
  240. package/bin/commands/search.js +16 -14
  241. package/bin/commands/space.js +96 -25
  242. package/bin/commands/start.js +262 -0
  243. package/bin/commands/status.js +2 -2
  244. package/bin/commands/stop.js +14 -0
  245. package/bin/commands/sync-cmd.js +134 -0
  246. package/bin/commands/token.js +98 -0
  247. package/bin/commands/uninstall.js +154 -0
  248. package/bin/commands/update.js +286 -0
  249. package/bin/lib/build.js +1 -1
  250. package/bin/lib/colors.js +8 -7
  251. package/bin/lib/command.js +37 -96
  252. package/bin/lib/config.js +5 -0
  253. package/bin/lib/csv.js +19 -0
  254. package/bin/lib/jsonc.js +12 -0
  255. package/bin/lib/markdown.js +69 -0
  256. package/bin/lib/mcp-agents.js +1 -6
  257. package/bin/lib/mcp-build.js +1 -1
  258. package/bin/lib/mcp-install.js +2 -1
  259. package/bin/lib/one-shot.js +88 -0
  260. package/bin/lib/path-expand.js +9 -0
  261. package/bin/lib/remote.js +65 -0
  262. package/bin/lib/repl.js +167 -0
  263. package/bin/lib/{utils.js → shell.js} +10 -26
  264. package/bin/lib/skill-check.js +1 -1
  265. package/bin/lib/sse-stream.js +167 -0
  266. package/package.json +2 -2
  267. package/scripts/setup.js +182 -120
  268. package/skills/mindos/SKILL.md +92 -92
  269. package/skills/mindos-zh/SKILL.md +100 -104
  270. package/_standalone/.next/server/chunks/1955.js +0 -11
  271. package/_standalone/.next/server/chunks/3680.js +0 -1
  272. package/_standalone/.next/server/chunks/4497.js +0 -219
  273. package/_standalone/.next/server/chunks/5560.js +0 -2
  274. package/_standalone/.next/static/chunks/1053-0adaccc98a752a58.js +0 -29
  275. package/_standalone/.next/static/chunks/3637.f9a42cca59fd5bb5.js +0 -1
  276. package/_standalone/.next/static/chunks/4563-c2afaeacb241d1d0.js +0 -6
  277. package/_standalone/.next/static/chunks/6090-c98268ca726a68d3.js +0 -1
  278. package/_standalone/.next/static/chunks/9371-575600301da5d6bb.js +0 -1
  279. package/_standalone/.next/static/chunks/app/agents/[agentKey]/page-3e08abb495ecd5fd.js +0 -1
  280. package/_standalone/.next/static/chunks/app/agents/page-e7e0f87ad3d765ac.js +0 -5
  281. package/_standalone/.next/static/chunks/app/help/page-3d0e1ceaa4abc243.js +0 -1
  282. package/_standalone/.next/static/chunks/app/setup/page-99ed3d1bb6b8f4ef.js +0 -1
  283. package/_standalone/.next/static/chunks/app/view/[...path]/page-44fa78cbea613a78.js +0 -12
  284. package/_standalone/.next/static/css/d300701f384db50d.css +0 -1
  285. package/_standalone/components/renderers/agent-inspector/manifest.ts +0 -16
  286. /package/_standalone/.next/static/{rZLs1krFuduixvcVNe6q3 → Ij3PFh-a0zi5K_ANoSAW0}/_buildManifest.js +0 -0
  287. /package/_standalone/.next/static/{rZLs1krFuduixvcVNe6q3 → Ij3PFh-a0zi5K_ANoSAW0}/_ssgManifest.js +0 -0
package/bin/cli.js CHANGED
@@ -1,178 +1,68 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * MindOS CLI
4
+ * MindOS CLI — thin router.
5
5
  *
6
- * Usage:
7
- * mindos onboard — interactive setup writes ~/.mindos/config.json
8
- * mindos onboard --install-daemon — setup + install & start as background OS service
9
- * mindos start — start app + MCP server (production, auto-rebuilds if needed)
10
- * mindos start --daemon — install + start as background OS service
11
- * mindos start --verbose — start with verbose MCP logging
12
- * mindos dev — start app + MCP server (dev mode)
13
- * mindos dev --turbopack — start with Turbopack (faster HMR)
14
- * mindos build — build the app for production
15
- * mindos mcp — start MCP server only
16
- * mindos stop — stop running MindOS processes
17
- * mindos restart — stop then start
18
- * mindos open — open Web UI in the default browser
19
- * mindos token — show current auth token and MCP config snippet
20
- * mindos sync — show sync status
21
- * mindos sync init — configure remote git repo for sync
22
- * mindos sync now — manual trigger sync
23
- * mindos sync conflicts — list conflict files
24
- * mindos sync on|off — enable/disable auto-sync
25
- * mindos gateway install — install background service (systemd/launchd)
26
- * mindos gateway uninstall — remove background service
27
- * mindos gateway start — start the background service
28
- * mindos gateway stop — stop the background service
29
- * mindos gateway status — show service status
30
- * mindos gateway logs — tail service logs
31
- * mindos doctor — health check (config, ports, build, daemon)
32
- * mindos uninstall — fully uninstall MindOS (stop, remove daemon, npm uninstall)
33
- * mindos update — update to latest version
34
- * mindos logs — tail service logs (~/.mindos/mindos.log)
35
- * mindos config show — print current config (API keys masked)
36
- * mindos config set <key> <val> — update a single config field
37
- * mindos config unset <key> — remove a config field
38
- * mindos config validate — validate config file
6
+ * All command logic lives in bin/commands/*.js.
7
+ * This file only parses args, dispatches commands, and renders global help.
39
8
  */
40
9
 
41
- import { execSync, execFileSync, spawn as nodeSpawn } from 'node:child_process';
42
- import { existsSync, readFileSync, writeFileSync, rmSync, cpSync, unlinkSync, mkdirSync } from 'node:fs';
43
- import { dirname, resolve } from 'node:path';
44
- import { homedir } from 'node:os';
10
+ import { readFileSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
45
12
 
46
- import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH, MINDOS_DIR } from './lib/constants.js';
47
- import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
13
+ import { ROOT } from './lib/constants.js';
14
+ import { bold, dim, cyan } from './lib/colors.js';
15
+ import { parseArgs, printCommandHelp } from './lib/command.js';
48
16
 
49
- // Resolve the local next binary to avoid npx pulling a mismatched global version
50
- const NEXT_BIN = resolve(ROOT, 'app', 'node_modules', '.bin', 'next');
51
- import { run, npmInstall } from './lib/utils.js';
52
- import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
53
- import { needsBuild, writeBuildStamp, cleanNextDir, ensureAppDeps, hasPrebuiltStandalone } from './lib/build.js';
54
- import { isPortInUse, assertPortFree } from './lib/port.js';
55
- import { savePids, clearPids } from './lib/pid.js';
56
- import { stopMindos, killByPort } from './lib/stop.js';
57
- import { printStartupInfo, getLocalIP } from './lib/startup.js';
58
- import { spawnMcp } from './lib/mcp-spawn.js';
59
- import { parseArgs, EXIT } 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); }
86
-
87
- // ── New modular commands ──────────────────────────────────────────────────────
17
+ // ── Modular commands ──────────────────────────────────────────────────────────
18
+ import * as agentCmd from './commands/agent.js';
19
+ import * as askCmd from './commands/ask.js';
88
20
  import * as fileCmd from './commands/file.js';
89
21
  import * as spaceCmd from './commands/space.js';
90
- import * as askCmd from './commands/ask.js';
22
+ import * as searchCmd from './commands/search.js';
23
+ import * as startCmd from './commands/start.js';
24
+ import * as devCmd from './commands/dev.js';
25
+ import * as stopCmd from './commands/stop.js';
26
+ import * as restartCmd from './commands/restart.js';
27
+ import * as buildCmd from './commands/build.js';
91
28
  import * as statusCmd from './commands/status.js';
29
+ import * as openCmd from './commands/open.js';
30
+ import * as mcpCmd from './commands/mcp-cmd.js';
31
+ import * as tokenCmd from './commands/token.js';
32
+ import * as syncCmd from './commands/sync-cmd.js';
33
+ import * as gatewayCmd from './commands/gateway.js';
34
+ import * as onboardCmd from './commands/onboard.js';
35
+ import * as configCmd from './commands/config.js';
36
+ import * as doctorCmd from './commands/doctor.js';
37
+ import * as updateCmd from './commands/update.js';
38
+ import * as uninstallCmd from './commands/uninstall.js';
39
+ import * as logsCmd from './commands/logs.js';
92
40
  import * as apiCmd from './commands/api.js';
93
- import * as agentCmd from './commands/agent.js';
94
- import * as searchCmd from './commands/search.js';
95
-
96
- // ── Helpers ───────────────────────────────────────────────────────────────────
97
-
98
- /**
99
- * Dynamically resolve the new ROOT after `npm install -g`.
100
- * This is needed because constants are evaluated at module load time.
101
- */
102
- function getUpdatedRoot() {
103
- try {
104
- const mindosBin = execSync('which mindos', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
105
- if (mindosBin) {
106
- // mindos bin is usually at <root>/bin/cli.js or a symlink to it
107
- let cliPath;
108
- try {
109
- cliPath = execSync(`readlink -f "${mindosBin}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
110
- } catch {
111
- try {
112
- cliPath = execSync(`realpath "${mindosBin}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
113
- } catch {
114
- cliPath = mindosBin;
115
- }
116
- }
117
- if (cliPath) {
118
- // cliPath is like /path/to/node_modules/@geminilight/mindos/bin/cli.js
119
- // ROOT is /path/to/node_modules/@geminilight/mindos
120
- return resolve(dirname(cliPath), '..');
121
- }
122
- }
123
- } catch {}
124
- // Fallback to static ROOT
125
- return ROOT;
126
- }
127
-
128
- /**
129
- * Build the app in the given root if the build stamp doesn't match the package version.
130
- * Used by `mindos update` to pre-build before restarting the daemon.
131
- */
132
- function buildIfNeeded(newRoot) {
133
- // Check for prebuilt standalone first (npm package ships with it)
134
- const standaloneServer = resolve(newRoot, '_standalone', 'server.js');
135
- const standaloneStamp = resolve(newRoot, '_standalone', '.mindos-build-version');
136
- if (existsSync(standaloneServer)) {
137
- try {
138
- const builtVersion = readFileSync(standaloneStamp, 'utf-8').trim();
139
- const pkgVersion = JSON.parse(readFileSync(resolve(newRoot, 'package.json'), 'utf-8')).version;
140
- if (builtVersion === pkgVersion) return; // prebuilt standalone matches, no build needed
141
- } catch { /* fall through to legacy build */ }
41
+ import * as initSkillsCmd from './commands/init-skills.js';
42
+
43
+ // ── Command registry ──────────────────────────────────────────────────────────
44
+
45
+ const modules = [
46
+ agentCmd, askCmd,
47
+ fileCmd, spaceCmd, searchCmd,
48
+ startCmd, devCmd, stopCmd, restartCmd, buildCmd, statusCmd, openCmd,
49
+ mcpCmd, tokenCmd,
50
+ syncCmd,
51
+ gatewayCmd,
52
+ onboardCmd, configCmd, doctorCmd, updateCmd, uninstallCmd, logsCmd, apiCmd,
53
+ initSkillsCmd,
54
+ ];
55
+
56
+ const commands = {};
57
+ for (const mod of modules) {
58
+ commands[mod.meta.name] = mod;
59
+ if (mod.meta.aliases) {
60
+ for (const alias of mod.meta.aliases) commands[alias] = mod;
142
61
  }
143
-
144
- const newBuildStamp = resolve(newRoot, 'app', '.next', '.mindos-build-version');
145
- const newNextBin = resolve(newRoot, 'app', 'node_modules', '.bin', 'next');
146
-
147
- let needBuild = true;
148
- try {
149
- const builtVersion = readFileSync(newBuildStamp, 'utf-8').trim();
150
- const pkgVersion = JSON.parse(readFileSync(resolve(newRoot, 'package.json'), 'utf-8')).version;
151
- needBuild = builtVersion !== pkgVersion;
152
- } catch {
153
- needBuild = true;
154
- }
155
-
156
- if (!needBuild) return;
157
-
158
- console.log(yellow('\n Building MindOS (version change detected)...\n'));
159
- const appPkg = resolve(newRoot, 'app', 'package.json');
160
- if (existsSync(appPkg)) {
161
- run('npm install', resolve(newRoot, 'app'));
162
- }
163
- const nextDir = resolve(newRoot, 'app', '.next');
164
- if (existsSync(nextDir)) {
165
- run(`rm -rf "${nextDir}"`, newRoot);
166
- }
167
- run('node scripts/gen-renderer-index.js', newRoot);
168
- run(`${newNextBin} build`, resolve(newRoot, 'app'));
169
- const version = JSON.parse(readFileSync(resolve(newRoot, 'package.json'), 'utf-8')).version;
170
- writeFileSync(newBuildStamp, version, 'utf-8');
171
62
  }
172
63
 
173
- // ── Commands ──────────────────────────────────────────────────────────────────
64
+ // ── Parse args ────────────────────────────────────────────────────────────────
174
65
 
175
- // ── Unified arg parsing ────────────────────────────────────────────────────
176
66
  const { command: cmd, args: cliArgs, flags: cliFlags } = parseArgs(process.argv.slice(2));
177
67
 
178
68
  if (cliFlags.version || cliFlags.v) {
@@ -181,1264 +71,138 @@ if (cliFlags.version || cliFlags.v) {
181
71
  process.exit(0);
182
72
  }
183
73
 
184
- // Backward compat: derive legacy variables from unified flags
185
- const isDaemon = cliFlags.daemon === true || (!cmd && isDaemonMode());
186
- const isVerbose = cliFlags.verbose === true;
187
- const extra = cliArgs.filter(a => !a.startsWith('-')).join(' ');
188
-
189
- const commands = {
190
- // ── onboard ────────────────────────────────────────────────────────────────
191
- onboard: async () => {
192
- const daemonFlag = cliFlags['install-daemon'] ? ' --install-daemon' : '';
193
- run(`node ${resolve(ROOT, 'scripts/setup.js')}${daemonFlag}`);
194
- },
195
- init: async () => commands.onboard(),
196
- setup: async () => commands.onboard(),
197
-
198
- // ── open ───────────────────────────────────────────────────────────────────
199
- open: () => {
200
- loadConfig();
201
- const webPort = process.env.MINDOS_WEB_PORT || '3456';
202
- const url = `http://localhost:${webPort}`;
203
- let cmd;
204
- if (process.platform === 'darwin') {
205
- cmd = 'open';
206
- } else if (process.platform === 'linux') {
207
- // WSL detection
208
- try {
209
- const uname = execSync('uname -r', { encoding: 'utf-8' });
210
- cmd = uname.toLowerCase().includes('microsoft') ? 'wslview' : 'xdg-open';
211
- } catch {
212
- cmd = 'xdg-open';
213
- }
214
- } else {
215
- cmd = 'start';
216
- }
217
- try {
218
- execSync(`${cmd} ${url}`, { stdio: 'ignore' });
219
- console.log(`${green('✔')} Opening ${cyan(url)}`);
220
- } catch {
221
- console.log(dim(`Could not open browser automatically. Visit: ${cyan(url)}`));
222
- }
223
- },
224
-
225
- // ── token ──────────────────────────────────────────────────────────────────
226
- token: () => {
227
- if (!existsSync(CONFIG_PATH)) {
228
- console.error(red('No config found. Run `mindos onboard` first.'));
229
- process.exit(EXIT.ERROR);
230
- }
231
- let config = {};
232
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
233
- const token = config.authToken || '';
234
- if (!token) {
235
- console.log(dim('No auth token set. Run `mindos onboard` to configure one.'));
236
- process.exit(0);
237
- }
238
- const mcpPort = config.mcpPort || 8781;
239
- const localIP = getLocalIP();
240
- const localUrl = `http://localhost:${mcpPort}/mcp`;
241
-
242
- if (cliFlags.json) {
243
- const data = { token, mcpPort, localUrl, remoteUrl: localIP ? `http://${localIP}:${mcpPort}/mcp` : null };
244
- console.log(JSON.stringify(data, null, 2));
245
- return;
246
- }
247
-
248
- const sep = dim('━'.repeat(40));
249
- const snippet = (url) => JSON.stringify({
250
- mcpServers: { mindos: { url, headers: { Authorization: `Bearer ${token}` } } },
251
- }, null, 2);
252
-
253
- console.log(`\n${bold('Auth token:')} ${cyan(token)}\n`);
254
-
255
- // Show installed agents, then up to 3 uninstalled popular ones
256
- const installed = [];
257
- const others = [];
258
- for (const [key, agent] of Object.entries(MCP_AGENTS)) {
259
- (detectAgentPresence(key) ? installed : others).push([key, agent]);
260
- }
261
- const toShow = [...installed.slice(0, 8), ...others.slice(0, Math.max(0, 3 - installed.length))];
262
-
263
- for (const [key, agent] of toShow) {
264
- console.log(sep);
265
- console.log(bold(agent.name));
266
- console.log(dim('Install:') + ` mindos mcp install ${key} -g -y`);
267
- if (agent.global) console.log(dim(`Config: ${agent.global}`));
268
- console.log(snippet(localUrl));
269
- console.log();
270
- }
271
-
272
- if (localIP) {
273
- console.log(sep);
274
- console.log(bold('Remote (other devices)'));
275
- console.log(`URL: ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
276
- console.log(snippet(`http://${localIP}:${mcpPort}/mcp`));
277
- }
278
-
279
- if (toShow.length < installed.length) {
280
- console.log(dim(`\n +${installed.length - toShow.length} more agents detected. Run \`mindos agent list\` to see all.`));
281
- }
282
- console.log(dim('\nRun `mindos onboard` to regenerate.\n'));
283
- },
284
-
285
- // ── dev ────────────────────────────────────────────────────────────────────
286
- dev: async () => {
287
- loadConfig();
288
- if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
289
- if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
290
- process.env.MINDOS_CLI_PATH = resolve(ROOT, 'bin', 'cli.js');
291
- process.env.MINDOS_NODE_BIN = process.execPath;
292
- const webPort = process.env.MINDOS_WEB_PORT;
293
- const mcpPort = process.env.MINDOS_MCP_PORT;
294
- await assertPortFree(Number(webPort), 'web');
295
- await assertPortFree(Number(mcpPort), 'mcp');
296
- ensureAppDeps({ force: true }); // dev mode always needs app/node_modules
297
- const mcp = spawnMcp(isVerbose);
298
- savePids(process.pid, mcp.pid);
299
- process.on('exit', () => { stopSyncDaemon().catch(() => {}); clearPids(); });
300
- const devMindRoot = process.env.MIND_ROOT;
301
- if (devMindRoot) {
302
- startSyncDaemon(devMindRoot).catch(() => {});
303
- }
304
- await printStartupInfo(webPort, mcpPort);
305
- run(`${NEXT_BIN} dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
306
- },
307
-
308
- // ── start ──────────────────────────────────────────────────────────────────
309
- start: async () => {
310
- // Check for incomplete setup
311
- if (existsSync(CONFIG_PATH)) {
312
- try {
313
- const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
314
- if (cfg.setupPending === true) {
315
- console.log(`\n ${yellow('⚠ Setup was not completed.')} Run ${cyan('mindos onboard')} to finish, or ${cyan('mindos config set setupPending false')} to dismiss.\n`);
316
- }
317
- } catch {}
318
- }
319
- if (isDaemon) {
320
- const platform = await getPlatform();
321
- if (!platform) {
322
- console.warn(yellow('Warning: daemon mode not supported on this platform. Falling back to foreground.'));
323
- } else {
324
- loadConfig();
325
- if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
326
- if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
327
- const webPort = process.env.MINDOS_WEB_PORT;
328
- const mcpPort = process.env.MINDOS_MCP_PORT;
329
- console.log(cyan(`Installing MindOS as a background service (${platform})...`));
330
- await runGatewayCommand('install');
331
- // install() already starts the service via launchctl bootstrap + RunAtLoad=true.
332
- // Do NOT call start() here — kickstart -k would kill the just-started process,
333
- // causing a port-conflict race condition with KeepAlive restart loops.
334
- console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
335
- const ready = await waitForHttp(Number(webPort), { retries: 180, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
336
- if (!ready) {
337
- console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
338
- console.error(dim(' Check logs with: mindos logs\n'));
339
- process.exit(EXIT.ERROR);
340
- }
341
- await printStartupInfo(webPort, mcpPort);
342
- // System notification
343
- try {
344
- if (process.platform === 'darwin') {
345
- execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS Ready"'`, { stdio: 'ignore' });
346
- } else if (process.platform === 'linux') {
347
- execSync(`notify-send "MindOS Ready" "http://localhost:${webPort}"`, { stdio: 'ignore' });
348
- }
349
- } catch { /* notification is best-effort */ }
350
- console.log(`${green('✔ MindOS is running as a background service')}`);
351
- console.log(dim(' View logs: mindos logs'));
352
- console.log(dim(' Stop: mindos gateway stop'));
353
- console.log(dim(' Uninstall: mindos gateway uninstall\n'));
354
- return;
355
- }
356
- }
357
- loadConfig();
358
- if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
359
- if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
360
- const webPort = process.env.MINDOS_WEB_PORT;
361
- const mcpPort = process.env.MINDOS_MCP_PORT;
362
-
363
- // Clean up zombie processes from an abandoned GUI setup session.
364
- // setup.js records a temporary port (setupPort) in config; if the user
365
- // closed the browser without completing setup, that process is still
366
- // running. Kill it before we proceed.
367
- // Also read config for auto-migration below (avoids double readFileSync).
368
- let startupCfg = {};
369
- try { startupCfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
370
- if (startupCfg.setupPort && Number(startupCfg.setupPort) !== Number(webPort)) {
371
- killByPort(Number(startupCfg.setupPort));
372
- }
373
-
374
- // ── Auto-migrate user preferences → .mindos/user-preferences.md ────────
375
- try {
376
- const mr = startupCfg.mindRoot;
377
- if (mr && existsSync(mr)) {
378
- const mindosDir = resolve(mr, '.mindos');
379
- const newPath = resolve(mindosDir, 'user-preferences.md');
380
-
381
- if (!existsSync(newPath)) {
382
- // Ensure .mindos/ directory exists
383
- if (!existsSync(mindosDir)) mkdirSync(mindosDir, { recursive: true });
384
-
385
- // Try migrate from previous locations (newest → oldest)
386
- const prevPaths = [
387
- resolve(mindosDir, 'user-rules.md'), // v0.6.x interim
388
- resolve(mr, 'user-skill-rules.md'), // v0.5.x root
389
- ];
390
- let migrated = false;
391
- for (const prev of prevPaths) {
392
- if (existsSync(prev)) {
393
- cpSync(prev, newPath);
394
- unlinkSync(prev);
395
- console.log(` ${green('✓')} ${dim(`Migrated ${prev.split('/').pop()} → .mindos/user-preferences.md`)}`);
396
- migrated = true;
397
- break;
398
- }
399
- }
400
- if (!migrated) {
401
- // Try legacy location (.agents/skills/{name}/user-rules.md)
402
- const isZh = startupCfg.disabledSkills?.includes('mindos');
403
- const sName = isZh ? 'mindos-zh' : 'mindos';
404
- const oldPath = resolve(mr, '.agents', 'skills', sName, 'user-rules.md');
405
- if (existsSync(oldPath)) {
406
- cpSync(oldPath, newPath);
407
- console.log(` ${green('✓')} ${dim('Migrated .agents/skills/ user-rules.md → .mindos/user-preferences.md')}`);
408
- }
409
- }
410
- }
411
- }
412
- } catch { /* best-effort, don't block startup */ }
413
-
414
- // When launched by a daemon manager (launchd/systemd), wait for ports to
415
- // free instead of exiting immediately — the previous instance may still be
416
- // shutting down after a restart/update.
417
- const launchedByDaemon = process.env.LAUNCHED_BY_LAUNCHD === '1'
418
- || !!process.env.INVOCATION_ID; /* systemd sets INVOCATION_ID */
419
-
420
- if (launchedByDaemon) {
421
- const webOk = await waitForPortFree(Number(webPort), { retries: 60, intervalMs: 500 });
422
- const mcpOk = await waitForPortFree(Number(mcpPort), { retries: 60, intervalMs: 500 });
423
- if (!webOk || !mcpOk) {
424
- console.error('Ports still in use after 30s, exiting.');
425
- process.exit(EXIT.ERROR); // KeepAlive will retry after ThrottleInterval
426
- }
427
- } else {
428
- await assertPortFree(Number(webPort), 'web');
429
- await assertPortFree(Number(mcpPort), 'mcp');
430
- }
431
- process.env.MINDOS_CLI_PATH = resolve(ROOT, 'bin', 'cli.js');
432
- process.env.MINDOS_NODE_BIN = process.execPath;
433
- ensureAppDeps();
434
- if (needsBuild()) {
435
- console.log(yellow('Building MindOS (first run or new version detected)...\n'));
436
- cleanNextDir();
437
- run('node scripts/gen-renderer-index.js', ROOT);
438
- run(`${NEXT_BIN} build --webpack`, resolve(ROOT, 'app'));
439
- writeBuildStamp();
440
- }
441
- const mcp = spawnMcp(isVerbose);
442
- savePids(process.pid, mcp.pid);
443
- process.on('exit', () => { stopSyncDaemon().catch(() => {}); clearPids(); });
444
- // Start sync daemon if enabled
445
- const mindRoot = process.env.MIND_ROOT;
446
- if (mindRoot) {
447
- startSyncDaemon(mindRoot).catch(() => {});
448
- }
449
- await printStartupInfo(webPort, mcpPort);
450
- // Prefer prebuilt standalone server (shipped with npm package) over next start.
451
- // Standalone includes its own traced node_modules — no app/node_modules needed.
452
- if (hasPrebuiltStandalone()) {
453
- const standaloneServer = resolve(ROOT, '_standalone', 'server.js');
454
- try {
455
- execFileSync(process.execPath, [standaloneServer], {
456
- cwd: resolve(ROOT, '_standalone'),
457
- stdio: 'inherit',
458
- env: { ...process.env, NODE_ENV: 'production', HOSTNAME: process.env.MINDOS_WEB_HOST || '0.0.0.0', PORT: webPort },
459
- });
460
- } catch (err) {
461
- process.exit(err.status || 1);
462
- }
463
- } else {
464
- run(
465
- `${NEXT_BIN} start -p ${webPort} ${extra}`,
466
- resolve(ROOT, 'app'),
467
- { HOSTNAME: process.env.MINDOS_WEB_HOST || '0.0.0.0' }
468
- );
469
- }
470
- },
471
-
472
- // ── build ──────────────────────────────────────────────────────────────────
473
- build: () => {
474
- ensureAppDeps({ force: true }); // build always needs app/node_modules
475
- cleanNextDir();
476
- run('node scripts/gen-renderer-index.js', ROOT);
477
- run(`${NEXT_BIN} build --webpack ${extra}`, resolve(ROOT, 'app'));
478
- writeBuildStamp();
479
- },
480
-
481
- mcp: async () => {
482
- const sub = cliArgs[0];
483
- const restArgs = cliArgs;
484
- const hasInstallFlags = restArgs.some(a => ['-g', '--global', '-y', '--yes'].includes(a));
485
- if (sub === 'install' || hasInstallFlags) { await mcpInstall(); return; }
486
- loadConfig();
487
- await ensureMcpBundle();
488
- // `mindos mcp` is the entry point for MCP clients (Claude Code, Cursor, etc.)
489
- // which communicate over stdin/stdout. Default to stdio; HTTP is handled by
490
- // `mindos start` via spawnMcp(). Callers can still override via env.
491
- if (!process.env.MCP_TRANSPORT) {
492
- process.env.MCP_TRANSPORT = 'stdio';
493
- }
494
- const webPort = process.env.MINDOS_WEB_PORT || '3456';
495
- process.env.MINDOS_URL = process.env.MINDOS_URL || `http://localhost:${webPort}`;
496
- // Only set MCP_PORT for HTTP mode (stdio doesn't bind any port)
497
- if (process.env.MCP_TRANSPORT === 'http') {
498
- process.env.MCP_PORT = process.env.MINDOS_MCP_PORT || '8781';
499
- }
500
- run(`node dist/index.cjs`, resolve(ROOT, 'mcp'));
501
- },
502
-
503
- // ── stop / restart ─────────────────────────────────────────────────────────
504
- stop: () => stopMindos(),
505
-
506
- restart: async () => {
507
- // Capture old ports BEFORE loadConfig overwrites env vars, so we can
508
- // clean up processes that are still listening on the previous ports
509
- // (e.g. user changed ports in the GUI and config was already saved).
510
- // Sources: (1) MINDOS_OLD_* set by /api/restart when it strips the
511
- // current env, (2) current MINDOS_*_PORT env vars.
512
- const oldWebPort = process.env.MINDOS_OLD_WEB_PORT || process.env.MINDOS_WEB_PORT;
513
- const oldMcpPort = process.env.MINDOS_OLD_MCP_PORT || process.env.MINDOS_MCP_PORT;
514
-
515
- loadConfig();
516
-
517
- // After loadConfig, env vars reflect the NEW config (or old if unchanged).
518
- const newWebPort = Number(process.env.MINDOS_WEB_PORT || '3456');
519
- const newMcpPort = Number(process.env.MINDOS_MCP_PORT || '8781');
520
-
521
- // Collect old ports that differ from new ones — processes may still be
522
- // listening there even though config already points to the new ports.
523
- const extraPorts = [];
524
- if (oldWebPort && Number(oldWebPort) !== newWebPort) extraPorts.push(oldWebPort);
525
- if (oldMcpPort && Number(oldMcpPort) !== newMcpPort) extraPorts.push(oldMcpPort);
526
-
527
- stopMindos({ extraPorts });
528
-
529
- // Wait until ALL ports (old + new) are actually free (up to 15s)
530
- const allPorts = new Set([newWebPort, newMcpPort]);
531
- for (const p of extraPorts) allPorts.add(Number(p));
532
-
533
- const deadline = Date.now() + 15_000;
534
- while (Date.now() < deadline) {
535
- let anyBusy = false;
536
- for (const p of allPorts) {
537
- if (await isPortInUse(p)) { anyBusy = true; break; }
538
- }
539
- if (!anyBusy) break;
540
- await new Promise((r) => setTimeout(r, 500));
541
- }
542
- await commands[getStartMode()]();
543
- },
544
-
545
- // ── gateway ────────────────────────────────────────────────────────────────
546
- gateway: async () => {
547
- const sub = cliArgs[0];
548
- if (!sub) {
549
- const row = (c, d) => ` ${cyan(c.padEnd(32))}${dim(d)}`;
550
- console.log(`
551
- ${bold('mindos gateway')} — manage MindOS as a background OS service
552
-
553
- ${bold('Subcommands:')}
554
- ${row('mindos gateway install', 'Install and enable the service (systemd/launchd)')}
555
- ${row('mindos gateway uninstall', 'Disable and remove the service')}
556
- ${row('mindos gateway start', 'Start the service')}
557
- ${row('mindos gateway stop', 'Stop the service')}
558
- ${row('mindos gateway status', 'Show service status')}
559
- ${row('mindos gateway logs', 'Tail service logs')}
560
-
561
- ${dim('Shortcut: mindos start --daemon → install + start in one step')}
562
- `);
563
- return;
564
- }
565
- await runGatewayCommand(sub);
566
- },
567
-
568
- // ── init-skills ──────────────────────────────────────────────────────────
569
- 'init-skills': async () => {
570
- console.log(`\n${bold('📦 Initialize Skill Rules')}\n`);
571
-
572
- if (!existsSync(CONFIG_PATH)) {
573
- console.log(` ${red('✘')} Config not found. Run ${cyan('mindos onboard')} first.\n`);
574
- process.exit(EXIT.ERROR);
575
- }
576
- let config;
577
- try {
578
- config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
579
- } catch {
580
- console.log(` ${red('✘')} Failed to parse config at ${dim(CONFIG_PATH)}\n`);
581
- process.exit(EXIT.ERROR);
582
- }
583
- const mindRoot = config.mindRoot;
584
- if (!mindRoot || !existsSync(mindRoot)) {
585
- console.log(` ${red('✘')} Knowledge base not found: ${dim(mindRoot || '(not set)')}\n`);
586
- process.exit(EXIT.ERROR);
587
- }
588
-
589
- // Skill operating rules are now built into SKILL.md (shipped with the app).
590
- // This command only initializes .mindos/user-preferences.md for personalization.
591
- const mindosDir = resolve(mindRoot, '.mindos');
592
- const dest = resolve(mindosDir, 'user-preferences.md');
593
- if (existsSync(dest)) {
594
- console.log(` ${dim('skip')} .mindos/user-preferences.md (already exists)\n`);
595
- } else {
596
- if (!existsSync(mindosDir)) mkdirSync(mindosDir, { recursive: true });
597
- const isZh = config.disabledSkills?.includes('mindos');
598
- const lang = isZh ? 'zh' : 'en';
599
- const src = resolve(ROOT, 'templates', 'skill-rules', lang, 'user-rules.md');
600
- if (existsSync(src)) {
601
- cpSync(src, dest);
602
- console.log(` ${green('✓')} .mindos/user-preferences.md created at ${dim(mindosDir)}\n`);
603
- } else {
604
- console.log(` ${dim('skip')} Template not found, create .mindos/user-preferences.md manually if needed.\n`);
605
- }
606
- }
607
- console.log(` ${dim('Note: Operating rules are now built into the app. No install needed.')}\n`);
608
- },
609
-
610
- // ── doctor ─────────────────────────────────────────────────────────────────
611
- doctor: async () => {
612
- const jsonMode = cliFlags.json === true;
613
- const checks = [];
614
- const ok = (msg, key) => { checks.push({ status: 'ok', key, msg }); if (!jsonMode) console.log(` ${green('✔')} ${msg}`); };
615
- const err = (msg, key) => { checks.push({ status: 'error', key, msg }); if (!jsonMode) console.log(` ${red('✘')} ${msg}`); };
616
- const warn= (msg, key) => { checks.push({ status: 'warn', key, msg }); if (!jsonMode) console.log(` ${yellow('!')} ${msg}`); };
617
-
618
- if (!jsonMode) console.log(`\n${bold('MindOS Doctor')}\n`);
619
- let hasError = false;
620
-
621
- // 1. config file
622
- if (!existsSync(CONFIG_PATH)) {
623
- err(`Config not found at ${dim(CONFIG_PATH)}`, 'config');
624
- if (!jsonMode) { console.log(`\n ${dim('Run `mindos onboard` to create it.')}\n`); }
625
- if (jsonMode) { console.log(JSON.stringify({ ok: false, checks }, null, 2)); }
626
- process.exit(EXIT.ERROR);
627
- }
628
- let config;
629
- try {
630
- config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
631
- ok(`Config file found and valid JSON ${dim(CONFIG_PATH)}`, 'config');
632
- } catch {
633
- err(`Config file exists but failed to parse ${dim(CONFIG_PATH)}`, 'config');
634
- hasError = true;
635
- }
636
-
637
- // 2. mindRoot
638
- if (config) {
639
- const mindRoot = config.mindRoot;
640
- if (!mindRoot) {
641
- err('Config missing required field: mindRoot');
642
- hasError = true;
643
- } else if (!existsSync(mindRoot.replace(/^~/, homedir()))) {
644
- warn(`mindRoot path does not exist: ${dim(mindRoot)} (will be created on first start)`);
645
- } else {
646
- ok(`Knowledge base path exists ${dim(mindRoot)}`);
647
- }
648
- }
649
-
650
- // 3. AI config
651
- if (config) {
652
- const provider = config.ai?.provider;
653
- const providers = config.ai?.providers;
654
- const hasAnthropic = providers?.anthropic?.apiKey || config.ai?.anthropicApiKey;
655
- const hasOpenai = providers?.openai?.apiKey || config.ai?.openaiApiKey;
656
- if (!provider) {
657
- warn('AI provider not configured (run `mindos onboard` to set up)');
658
- } else if (provider === 'anthropic' && !hasAnthropic) {
659
- err('AI provider is "anthropic" but no API key found');
660
- hasError = true;
661
- } else if (provider === 'openai' && !hasOpenai) {
662
- err('AI provider is "openai" but no API key found');
663
- hasError = true;
664
- } else {
665
- ok(`AI provider configured ${dim(provider)}`);
666
- }
667
- }
668
-
669
- // 4. Node version
670
- const nodeVersion = process.versions.node;
671
- const [nodeMajor] = nodeVersion.split('.').map(Number);
672
- if (nodeMajor < 18) {
673
- err(`Node.js ${nodeVersion} is below minimum required (18+)`);
674
- hasError = true;
675
- } else {
676
- ok(`Node.js ${nodeVersion}`);
677
- }
678
-
679
- // 4b. npm reachable from /bin/sh
680
- try {
681
- const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
682
- ok(`npm ${npmVersion} reachable`);
683
- } catch {
684
- err('npm not found in PATH — app dependencies cannot be installed');
685
- console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
686
- console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
687
- hasError = true;
688
- }
689
-
690
- // 5. Build
691
- if (hasPrebuiltStandalone()) {
692
- ok('Production build is up to date (prebuilt standalone)');
693
- } else if (!existsSync(resolve(ROOT, 'app', '.next'))) {
694
- warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
695
- } else if (needsBuild()) {
696
- warn(`Build is outdated — will rebuild automatically on next ${dim('mindos start')}`);
697
- } else {
698
- ok('Production build is up to date');
699
- }
700
-
701
- // 6. Ports
702
- const webPort = Number(config?.port || process.env.MINDOS_WEB_PORT || 3456);
703
- const mcpPort = Number(config?.mcpPort || process.env.MINDOS_MCP_PORT || 8781);
704
- const webInUse = await isPortInUse(webPort);
705
- const mcpInUse = await isPortInUse(mcpPort);
706
- if (webInUse) {
707
- ok(`Web server is listening on port ${webPort}`);
708
- } else {
709
- warn(`Web server is not running on port ${webPort}`);
710
- }
711
- if (mcpInUse) {
712
- ok(`MCP server is listening on port ${mcpPort}`);
713
- } else {
714
- warn(`MCP server is not running on port ${mcpPort}`);
715
- }
716
-
717
- // 7. Daemon status
718
- const platform = await getPlatform();
719
- if (platform === 'systemd') {
720
- try {
721
- execSync('systemctl --user is-active mindos', { stdio: 'pipe' });
722
- ok('Systemd service mindos is active');
723
- } catch {
724
- warn('Systemd service mindos is not active (run `mindos gateway start` to start)');
725
- }
726
- } else if (platform === 'launchd') {
727
- try {
728
- const uid = execSync('id -u').toString().trim();
729
- execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
730
- ok('LaunchAgent com.mindos.app is loaded');
731
- } catch {
732
- warn('LaunchAgent com.mindos.app is not loaded (run `mindos gateway start` to start)');
733
- }
734
- }
735
-
736
- // 8. Sync status
737
- if (config?.mindRoot) {
738
- try {
739
- const syncStatus = await getSyncStatus(config.mindRoot);
740
- if (!syncStatus.enabled) {
741
- warn(`Cross-device sync is not configured ${dim('(run `mindos sync init` to set up)')}`);
742
- } else if (syncStatus.lastError) {
743
- err(`Sync error: ${syncStatus.lastError}`);
744
- hasError = true;
745
- } else if (syncStatus.conflicts && syncStatus.conflicts.length > 0) {
746
- warn(`Sync has ${syncStatus.conflicts.length} unresolved conflict(s) ${dim('(run `mindos sync conflicts` to view)')}`);
747
- } else {
748
- const unpushed = parseInt(syncStatus.unpushed || '0', 10);
749
- const extra = unpushed > 0 ? ` ${dim(`(${unpushed} unpushed commit(s))`)}` : '';
750
- ok(`Sync enabled ${dim(syncStatus.remote || 'origin')}${extra}`);
751
- }
752
- } catch {
753
- warn('Could not check sync status');
754
- }
755
- }
756
-
757
- // 9. Update check
758
- try {
759
- const { checkForUpdate } = await import('./lib/update-check.js');
760
- const latestVersion = await Promise.race([
761
- checkForUpdate(),
762
- new Promise(r => setTimeout(() => r(null), 4000)),
763
- ]);
764
- if (latestVersion) {
765
- const currentVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
766
- warn(`Update available: v${currentVersion} → ${bold(`v${latestVersion}`)} ${dim('run `mindos update`')}`);
767
- } else {
768
- ok('MindOS is up to date');
769
- }
770
- } catch {
771
- warn('Could not check for updates');
772
- }
773
-
774
- if (jsonMode) {
775
- const hasErr = checks.some(c => c.status === 'error');
776
- console.log(JSON.stringify({ ok: !hasErr, checks }, null, 2));
777
- } else {
778
- console.log(hasError
779
- ? `\n${red('Some checks failed.')} Run ${cyan('mindos onboard')} to reconfigure.\n`
780
- : `\n${green('All checks passed.')}\n`);
781
- }
782
- if (hasError) process.exit(EXIT.ERROR);
783
- },
784
-
785
- // ── update ─────────────────────────────────────────────────────────────────
786
- update: async () => {
787
- const { writeUpdateStatus, writeUpdateFailed, clearUpdateStatus } = await import('./lib/update-status.js');
788
- const currentVersion = (() => {
789
- try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
790
- })();
791
- console.log(`\n${bold('⬆ Updating MindOS...')} ${dim(`(current: ${currentVersion})`)}\n`);
792
-
793
- // Stage 1: Download
794
- writeUpdateStatus('downloading', { fromVersion: currentVersion });
795
- try {
796
- execSync('npm install -g @geminilight/mindos@latest', { stdio: 'inherit' });
797
- } catch {
798
- writeUpdateFailed('downloading', 'npm install failed', { fromVersion: currentVersion });
799
- console.error(red('Update failed. Try: npm install -g @geminilight/mindos@latest'));
800
- process.exit(EXIT.ERROR);
801
- }
802
- if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
803
-
804
- // Resolve the new installation path (after npm install -g, ROOT is stale)
805
- const updatedRoot = getUpdatedRoot();
806
- const newVersion = (() => {
807
- try { return JSON.parse(readFileSync(resolve(updatedRoot, 'package.json'), 'utf-8')).version; } catch { return '?'; }
808
- })();
809
- const vOpts = { fromVersion: currentVersion, toVersion: newVersion };
810
-
811
- // Stage 2: Skills
812
- writeUpdateStatus('skills', vOpts);
813
- try {
814
- const { checkSkillVersions, updateSkill } = await import('./lib/skill-check.js');
815
- const mismatches = checkSkillVersions(updatedRoot);
816
- for (const m of mismatches) {
817
- updateSkill(m.bundledPath, m.installPath);
818
- console.log(` ${green('✓')} ${dim(`Skill ${m.name}: v${m.installed} → v${m.bundled}`)}`);
819
- }
820
- } catch { /* best-effort */ }
821
-
822
- if (newVersion !== currentVersion) {
823
- console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
824
- } else {
825
- console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
826
- return;
827
- }
828
-
829
- const updatePlatform = await getPlatform();
830
- let daemonRunning = false;
831
- if (updatePlatform === 'systemd') {
832
- try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
833
- } else if (updatePlatform === 'launchd') {
834
- try {
835
- const uid = execSync('id -u').toString().trim();
836
- execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
837
- daemonRunning = true;
838
- } catch {}
839
- }
840
-
841
- if (daemonRunning) {
842
- console.log(cyan('\n Daemon is running — stopping to apply the new version...'));
843
- await runGatewayCommand('stop');
844
-
845
- // Stage 3: Rebuild
846
- writeUpdateStatus('rebuilding', vOpts);
847
- let daemonBuildFailed = '';
848
- try {
849
- buildIfNeeded(updatedRoot);
850
- } catch (err) {
851
- daemonBuildFailed = err instanceof Error ? err.message : String(err);
852
- console.error(yellow(`\n Pre-build failed: ${daemonBuildFailed}`));
853
- console.error(yellow(' Daemon will attempt to rebuild on startup...\n'));
854
- }
855
-
856
- // Stage 4: Restart — always attempt, even if pre-build failed
857
- // (daemon has auto-restart; `mindos start` retries the build)
858
- writeUpdateStatus('restarting', vOpts);
859
- await runGatewayCommand('install');
860
- const updateConfig = (() => {
861
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
862
- })();
863
- const webPort = updateConfig.port ?? 3456;
864
- const mcpPort = updateConfig.mcpPort ?? 8781;
865
- console.log(dim(' (Waiting for Web UI to come back up — first run after update includes a rebuild...)'));
866
- const ready = await waitForHttp(Number(webPort), { retries: 450, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
867
- if (ready) {
868
- const localIP = getLocalIP();
869
- console.log(`\n${'─'.repeat(53)}`);
870
- console.log(`${green('✔')} ${bold(`MindOS updated: ${currentVersion} → ${newVersion}`)}\n`);
871
- console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
872
- if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
873
- console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
874
- console.log(`\n ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}`);
875
- console.log(`${'─'.repeat(53)}\n`);
876
- writeUpdateStatus('done', vOpts);
877
- } else {
878
- const failMsg = daemonBuildFailed
879
- ? `Build failed (${daemonBuildFailed}), server did not come back up`
880
- : 'Server did not come back up in time';
881
- writeUpdateFailed('restarting', failMsg, vOpts);
882
- console.error(red(`✘ ${failMsg}. Check logs: mindos logs\n`));
883
- process.exit(EXIT.ERROR);
884
- }
885
- } else {
886
- // Non-daemon mode: check if a MindOS instance is currently running
887
- // (e.g. user started via `mindos start`, or GUI triggered this update).
888
- // If so, stop it and restart from the NEW installation path.
889
- const updateConfig = (() => {
890
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
891
- })();
892
- const webPort = Number(updateConfig.port ?? 3456);
893
- const mcpPort = Number(updateConfig.mcpPort ?? 8781);
894
-
895
- const wasRunning = await isPortInUse(webPort) || await isPortInUse(mcpPort);
896
-
897
- if (wasRunning) {
898
- console.log(cyan('\n MindOS is running — restarting to apply the new version...'));
899
- stopMindos();
900
- // Wait for ports to free (up to 15s)
901
- const deadline = Date.now() + 15_000;
902
- while (Date.now() < deadline) {
903
- const busy = await isPortInUse(webPort) || await isPortInUse(mcpPort);
904
- if (!busy) break;
905
- await new Promise((r) => setTimeout(r, 500));
906
- }
907
-
908
- // Stage 3: Rebuild
909
- writeUpdateStatus('rebuilding', vOpts);
910
- let buildFailed = '';
911
- try {
912
- buildIfNeeded(updatedRoot);
913
- } catch (err) {
914
- buildFailed = err instanceof Error ? err.message : String(err);
915
- console.error(yellow(`\n Pre-build failed: ${buildFailed}`));
916
- console.error(yellow(' Starting server anyway (it will retry the build)...\n'));
917
- }
918
-
919
- // Stage 4: Restart — always attempt, even if pre-build failed
920
- // (`mindos start` has its own build-on-startup logic)
921
- writeUpdateStatus('restarting', vOpts);
922
- const newCliPath = resolve(updatedRoot, 'bin', 'cli.js');
923
- const childEnv = { ...process.env };
924
- delete childEnv.MINDOS_WEB_PORT;
925
- delete childEnv.MINDOS_MCP_PORT;
926
- delete childEnv.MIND_ROOT;
927
- delete childEnv.AUTH_TOKEN;
928
- delete childEnv.WEB_PASSWORD;
929
- const child = nodeSpawn(
930
- process.execPath, [newCliPath, 'start'],
931
- { detached: true, stdio: 'ignore', env: childEnv },
932
- );
933
- child.unref();
934
-
935
- console.log(dim(' (Waiting for Web UI to come back up...)'));
936
- const ready = await waitForHttp(webPort, { retries: 180, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
937
- if (ready) {
938
- const localIP = getLocalIP();
939
- console.log(`\n${'─'.repeat(53)}`);
940
- console.log(`${green('✔')} ${bold(`MindOS updated: ${currentVersion} → ${newVersion}`)}\n`);
941
- console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
942
- if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
943
- console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
944
- console.log(`\n ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}`);
945
- console.log(`${'─'.repeat(53)}\n`);
946
- writeUpdateStatus('done', vOpts);
947
- } else {
948
- const failMsg = buildFailed
949
- ? `Build failed (${buildFailed}), server did not come back up`
950
- : 'Server did not come back up in time';
951
- writeUpdateFailed('restarting', failMsg, vOpts);
952
- console.error(red(`✘ ${failMsg}. Check logs: mindos logs\n`));
953
- process.exit(EXIT.ERROR);
954
- }
955
- } else {
956
- // No running instance — just build and tell user to start manually
957
- try {
958
- buildIfNeeded(updatedRoot);
959
- } catch (err) {
960
- const msg = err instanceof Error ? err.message : String(err);
961
- console.error(yellow(`\n Pre-build failed: ${msg}`));
962
- console.error(dim(' The build will be retried when you run `mindos start`.'));
963
- }
964
- console.log(`\n${green('✔')} ${bold(`Updated: ${currentVersion} → ${newVersion}`)}`);
965
- console.log(dim(' Run `mindos start` to start the updated version.'));
966
- console.log(` ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}\n`);
967
- }
968
- }
969
- },
970
-
971
- // ── uninstall ───────────────────────────────────────────────────────────────
972
- uninstall: async () => {
973
- const readline = await import('node:readline');
974
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
975
-
976
- // Buffer lines eagerly — readline.question() loses buffered lines when
977
- // piped stdin delivers multiple lines at once (Node.js known behavior).
978
- const lineBuffer = [];
979
- let lineResolve = null;
980
- rl.on('line', (line) => {
981
- if (lineResolve) {
982
- const r = lineResolve;
983
- lineResolve = null;
984
- r(line);
985
- } else {
986
- lineBuffer.push(line);
987
- }
988
- });
989
- // On EOF with no pending resolve, close gracefully
990
- rl.on('close', () => {
991
- if (lineResolve) { lineResolve(''); lineResolve = null; }
992
- });
993
-
994
- function prompt(question) {
995
- process.stdout.write(question + ' ');
996
- if (lineBuffer.length > 0) return Promise.resolve(lineBuffer.shift());
997
- return new Promise((resolve) => { lineResolve = resolve; });
998
- }
999
-
1000
- async function confirm(question) {
1001
- const a = (await prompt(question + ' [y/N]')).trim().toLowerCase();
1002
- return a === 'y' || a === 'yes';
1003
- }
1004
-
1005
- async function askInput(question) {
1006
- return (await prompt(question)).trim();
1007
- }
1008
-
1009
- async function askPassword(question) {
1010
- // Mute echoed keystrokes
1011
- const stdout = process.stdout;
1012
- const origWrite = stdout.write.bind(stdout);
1013
- stdout.write = (chunk, ...args) => {
1014
- // Suppress everything except the prompt itself
1015
- if (typeof chunk === 'string' && chunk.includes(question)) return origWrite(chunk, ...args);
1016
- return true;
1017
- };
1018
- const answer = await prompt(question);
1019
- stdout.write = origWrite;
1020
- console.log(); // newline after hidden input
1021
- return answer.trim();
1022
- }
1023
-
1024
- const done = () => rl.close();
1025
-
1026
- console.log(`\n${bold('🗑 MindOS Uninstall')}\n`);
1027
- console.log(' This will:');
1028
- console.log(` ${green('✓')} Stop running MindOS processes`);
1029
- console.log(` ${green('✓')} Remove background service (if installed)`);
1030
- console.log(` ${green('✓')} Uninstall npm package\n`);
1031
-
1032
- if (!await confirm('Proceed?')) {
1033
- console.log(dim('\n Aborted.\n'));
1034
- done();
1035
- return;
1036
- }
1037
-
1038
- // 1. Stop processes
1039
- console.log(`\n${cyan('Stopping MindOS...')}`);
1040
- try { stopMindos(); } catch { /* may not be running */ }
1041
-
1042
- // 2. Remove daemon (skip if platform unsupported)
1043
- if (await getPlatform()) {
1044
- try {
1045
- await runGatewayCommand('uninstall');
1046
- } catch {
1047
- // Daemon may not be installed — that's fine
1048
- }
1049
- }
1050
-
1051
- // Read config before potentially deleting ~/.mindos/
1052
- let config = {};
1053
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
1054
- const mindRoot = config.mindRoot?.replace(/^~/, homedir());
1055
-
1056
- // 3. Ask to remove ~/.mindos/
1057
- if (existsSync(MINDOS_DIR)) {
1058
- if (await confirm(`Remove config directory (${dim(MINDOS_DIR)})?`)) {
1059
- rmSync(MINDOS_DIR, { recursive: true, force: true });
1060
- console.log(`${green('✔')} Removed ${dim(MINDOS_DIR)}`);
1061
- } else {
1062
- console.log(dim(` Kept ${MINDOS_DIR}`));
1063
- }
1064
- }
1065
-
1066
- // 4. Ask to remove knowledge base (triple protection: confirm → type YES → password)
1067
- if (mindRoot && existsSync(mindRoot)) {
1068
- if (await confirm(`Remove knowledge base (${dim(mindRoot)})?`)) {
1069
- const typed = await askInput(`${yellow('⚠ This is irreversible.')} Type ${bold('YES')} to confirm:`);
1070
- if (typed === 'YES') {
1071
- const webPassword = config.webPassword;
1072
- let authorized = true;
1073
- if (webPassword) {
1074
- const pw = await askPassword('Enter web password:');
1075
- if (pw !== webPassword) {
1076
- console.log(red(' Wrong password. Knowledge base kept.'));
1077
- authorized = false;
1078
- }
1079
- }
1080
- if (authorized) {
1081
- rmSync(mindRoot, { recursive: true, force: true });
1082
- console.log(`${green('✔')} Removed ${dim(mindRoot)}`);
1083
- }
1084
- } else {
1085
- console.log(dim(' Knowledge base kept.'));
1086
- }
1087
- } else {
1088
- console.log(dim(` Kept ${mindRoot}`));
1089
- }
1090
- }
1091
-
1092
- // 5. npm uninstall -g
1093
- console.log(`\n${cyan('Uninstalling npm package...')}`);
1094
- try {
1095
- execSync('npm uninstall -g @geminilight/mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
1096
- } catch {
1097
- console.log(yellow(' npm uninstall failed — you may need to run manually:'));
1098
- console.log(dim(' npm uninstall -g @geminilight/mindos'));
1099
- }
1100
-
1101
- console.log(`\n${green('✔ MindOS uninstalled.')}\n`);
1102
- done();
1103
- },
1104
-
1105
- // ── logs ───────────────────────────────────────────────────────────────────
1106
- logs: async () => {
1107
- await ensureMindosDir();
1108
- if (!existsSync(LOG_PATH)) {
1109
- console.log(dim(`No log file yet at ${LOG_PATH}`));
1110
- console.log(dim('Logs are created when starting MindOS (mindos start, mindos onboard, or daemon mode).'));
1111
- process.exit(0);
1112
- }
1113
- const noFollow = cliFlags['no-follow'] === true;
1114
- if (noFollow) {
1115
- execSync(`tail -n 100 ${LOG_PATH}`, { stdio: 'inherit' });
1116
- } else {
1117
- execSync(`tail -f ${LOG_PATH}`, { stdio: 'inherit' });
1118
- }
1119
- },
1120
-
1121
- // ── config ─────────────────────────────────────────────────────────────────
1122
- config: () => {
1123
- const sub = cliArgs[0];
1124
-
1125
- function maskKey(val) {
1126
- if (!val) return val;
1127
- if (val.length <= 8) return '****';
1128
- return val.slice(0, 6) + '****';
1129
- }
1130
-
1131
- if (sub === 'show') {
1132
- if (!existsSync(CONFIG_PATH)) {
1133
- console.error(red('No config found. Run `mindos onboard` first.'));
1134
- process.exit(EXIT.ERROR);
1135
- }
1136
- let config;
1137
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {
1138
- console.error(red('Failed to parse config file.'));
1139
- process.exit(EXIT.ERROR);
1140
- }
1141
- const display = JSON.parse(JSON.stringify(config));
1142
- if (display.ai?.providers?.anthropic?.apiKey)
1143
- display.ai.providers.anthropic.apiKey = maskKey(display.ai.providers.anthropic.apiKey);
1144
- if (display.ai?.providers?.openai?.apiKey)
1145
- display.ai.providers.openai.apiKey = maskKey(display.ai.providers.openai.apiKey);
1146
- if (display.ai?.anthropicApiKey)
1147
- display.ai.anthropicApiKey = maskKey(display.ai.anthropicApiKey);
1148
- if (display.ai?.openaiApiKey)
1149
- display.ai.openaiApiKey = maskKey(display.ai.openaiApiKey);
1150
- if (display.authToken)
1151
- display.authToken = maskKey(display.authToken);
1152
- if (display.webPassword)
1153
- display.webPassword = maskKey(display.webPassword);
1154
- if (cliFlags.json) {
1155
- console.log(JSON.stringify(display, null, 2));
1156
- return;
1157
- }
1158
- 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`);
1159
- console.log(JSON.stringify(display, null, 2));
1160
- console.log();
1161
- return;
1162
- }
1163
-
1164
- if (sub === 'validate') {
1165
- if (!existsSync(CONFIG_PATH)) {
1166
- console.error(red('No config found. Run `mindos onboard` first.'));
1167
- process.exit(EXIT.ERROR);
1168
- }
1169
- let config;
1170
- try {
1171
- config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
1172
- } catch (e) {
1173
- console.error(red(`✘ Invalid JSON: ${e.message}`));
1174
- process.exit(EXIT.ERROR);
1175
- }
1176
- const issues = [];
1177
- if (!config.mindRoot) issues.push('missing required field: mindRoot');
1178
- if (!config.ai?.provider) issues.push('missing field: ai.provider');
1179
- if (config.ai?.provider === 'anthropic') {
1180
- const key = config.ai?.providers?.anthropic?.apiKey || config.ai?.anthropicApiKey;
1181
- if (!key) issues.push('ai.provider is "anthropic" but no API key found');
1182
- }
1183
- if (config.ai?.provider === 'openai') {
1184
- const key = config.ai?.providers?.openai?.apiKey || config.ai?.openaiApiKey;
1185
- if (!key) issues.push('ai.provider is "openai" but no API key found');
1186
- }
1187
- if (issues.length) {
1188
- console.error(`\n${red('✘ Config has issues:')}`);
1189
- issues.forEach(i => console.error(` ${red('•')} ${i}`));
1190
- console.error(`\n ${dim('Run `mindos onboard` to fix.\n')}`);
1191
- process.exit(EXIT.ERROR);
1192
- }
1193
- console.log(`\n${green('✔ Config is valid')}\n`);
1194
- return;
1195
- }
1196
-
1197
- if (sub === 'set') {
1198
- const key = cliArgs[1];
1199
- const val = cliArgs[2];
1200
- if (!key || val === undefined) {
1201
- console.error(red('Usage: mindos config set <key> <value>'));
1202
- console.error(dim(' Examples:'));
1203
- console.error(dim(' mindos config set port 3002'));
1204
- console.error(dim(' mindos config set mcpPort 8788'));
1205
- console.error(dim(' mindos config set ai.provider openai'));
1206
- process.exit(EXIT.ARGS);
1207
- }
1208
- if (!existsSync(CONFIG_PATH)) {
1209
- console.error(red('No config found. Run `mindos onboard` first.'));
1210
- process.exit(EXIT.ERROR);
1211
- }
1212
- let config;
1213
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {
1214
- console.error(red('Failed to parse config file.'));
1215
- process.exit(EXIT.ERROR);
1216
- }
1217
- const parts = key.split('.');
1218
- let obj = config;
1219
- for (let i = 0; i < parts.length - 1; i++) {
1220
- if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
1221
- obj = obj[parts[i]];
1222
- }
1223
- // Coerce string values to appropriate types
1224
- function coerceValue(v) {
1225
- if (v === 'true') return true;
1226
- if (v === 'false') return false;
1227
- if (v === 'null') return null;
1228
- if (v === '""' || v === "''") return '';
1229
- if (v.trim() !== '' && !isNaN(Number(v))) return Number(v);
1230
- return v;
1231
- }
1232
- const coerced = coerceValue(val);
1233
- obj[parts[parts.length - 1]] = coerced;
1234
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
1235
- console.log(`${green('✔')} Set ${cyan(key)} = ${bold(String(coerced))}`);
1236
- return;
1237
- }
1238
-
1239
- if (sub === 'unset') {
1240
- const key = cliArgs[1];
1241
- if (!key) {
1242
- console.error(red('Usage: mindos config unset <key>'));
1243
- process.exit(EXIT.ARGS);
1244
- }
1245
- if (!existsSync(CONFIG_PATH)) {
1246
- console.error(red('No config found. Run `mindos onboard` first.'));
1247
- process.exit(EXIT.ERROR);
1248
- }
1249
- let config;
1250
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {
1251
- console.error(red('Failed to parse config file.'));
1252
- process.exit(EXIT.ERROR);
1253
- }
1254
- const parts = key.split('.');
1255
- let obj = config;
1256
- for (let i = 0; i < parts.length - 1; i++) {
1257
- if (!obj[parts[i]]) { console.log(dim(`Key "${key}" not found`)); return; }
1258
- obj = obj[parts[i]];
1259
- }
1260
- if (!(parts[parts.length - 1] in obj)) { console.log(dim(`Key "${key}" not found`)); return; }
1261
- delete obj[parts[parts.length - 1]];
1262
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
1263
- console.log(`${green('✔')} Removed ${cyan(key)}`);
1264
- return;
1265
- }
1266
-
1267
- // no subcommand or unknown → show help
1268
- const row = (c, d) => ` ${cyan(c.padEnd(32))}${dim(d)}`;
1269
- console.log(`
1270
- ${bold('mindos config')} — view and update MindOS configuration
1271
-
1272
- ${bold('Subcommands:')}
1273
- ${row('mindos config show', 'Print current config (API keys masked)')}
1274
- ${row('mindos config validate', 'Validate config file')}
1275
- ${row('mindos config set <key> <v>', 'Update a single field (dot-notation supported)')}
1276
- ${row('mindos config unset <key>', 'Remove a config field')}
1277
-
1278
- ${bold('Examples:')}
1279
- ${dim('mindos config set port 3002')}
1280
- ${dim('mindos config set ai.provider openai')}
1281
- ${dim('mindos config set setupPending false')}
1282
- ${dim('mindos config unset webPassword')}
1283
- `);
1284
- },
1285
-
1286
- // ── sync ──────────────────────────────────────────────────────────────────
1287
- sync: async () => {
1288
- const sub = cliArgs[0];
1289
- loadConfig();
1290
- const mindRoot = process.env.MIND_ROOT;
1291
-
1292
- if (sub === 'init') {
1293
- // Flags are already parsed by parseArgs into cliFlags
1294
- const nonInteractive = cliFlags['non-interactive'] === true;
1295
-
1296
- if (nonInteractive) {
1297
- await initSync(mindRoot, {
1298
- nonInteractive: true,
1299
- remote: typeof cliFlags.remote === 'string' ? cliFlags.remote : '',
1300
- token: typeof cliFlags.token === 'string' ? cliFlags.token : '',
1301
- branch: (typeof cliFlags.branch === 'string' ? cliFlags.branch : '') || 'main',
1302
- });
1303
- } else {
1304
- await initSync(mindRoot);
1305
- }
1306
- return;
1307
- }
1308
-
1309
- if (sub === 'now') {
1310
- try {
1311
- console.log(dim('Pulling...'));
1312
- await manualSync(mindRoot);
1313
- console.log(green('✔ Sync complete'));
1314
- } catch (err) {
1315
- console.error(red(err.message));
1316
- process.exit(EXIT.ERROR);
1317
- }
1318
- return;
1319
- }
74
+ // ── Help generation ───────────────────────────────────────────────────────────
75
+ // Summaries come from each module's meta.summary single source of truth.
76
+ // [displayName, module] — displayName may differ from meta.name (e.g. init → onboard).
77
+
78
+ const coreEntries = [
79
+ ['agent', agentCmd],
80
+ ['ask', askCmd],
81
+ ['start', startCmd],
82
+ ['stop', stopCmd],
83
+ ['status', statusCmd],
84
+ ['open', openCmd],
85
+ ['file', fileCmd],
86
+ ['space', spaceCmd],
87
+ ['search', searchCmd],
88
+ ['mcp', mcpCmd],
89
+ ['init', onboardCmd],
90
+ ['config', configCmd],
91
+ ['doctor', doctorCmd],
92
+ ['update', updateCmd],
93
+ ];
94
+
95
+ const additionalEntries = [
96
+ ['dev', devCmd],
97
+ ['build', buildCmd],
98
+ ['restart', restartCmd],
99
+ ['sync', syncCmd],
100
+ ['gateway', gatewayCmd],
101
+ ['token', tokenCmd],
102
+ ['logs', logsCmd],
103
+ ['api', apiCmd],
104
+ ['init-skills', initSkillsCmd],
105
+ ['uninstall', uninstallCmd],
106
+ ];
107
+
108
+ function showGlobalHelp(showAll = false) {
109
+ const pkgVersion = (() => {
110
+ try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; }
111
+ catch { return '?'; }
112
+ })();
113
+ const row = ([name, mod]) => ` ${cyan(name.padEnd(14))}${dim(mod.meta.summary)}`;
114
+
115
+ const lines = [
116
+ '',
117
+ `${bold('MindOS CLI')} ${dim(`v${pkgVersion}`)}`,
118
+ '',
119
+ `${bold('USAGE')}`,
120
+ ` ${cyan('mindos <command> [flags]')}`,
121
+ '',
122
+ `${bold('COMMANDS')}`,
123
+ ...coreEntries.map(row),
124
+ ];
1320
125
 
1321
- if (sub === 'conflicts') {
1322
- await listConflicts(mindRoot);
1323
- return;
1324
- }
126
+ if (showAll) {
127
+ lines.push('', `${bold('ADDITIONAL COMMANDS')}`);
128
+ lines.push(...additionalEntries.map(row));
129
+ }
1325
130
 
1326
- if (sub === 'on') {
1327
- await setSyncEnabled(true);
1328
- return;
1329
- }
131
+ const flagRow = (f, d) => ` ${cyan(f.padEnd(14))}${dim(d)}`;
132
+ lines.push(
133
+ '',
134
+ `${bold('FLAGS')}`,
135
+ flagRow('--help, -h', 'Show help'),
136
+ flagRow('--version, -v', 'Show version'),
137
+ flagRow('--json', 'Output as JSON'),
138
+ '',
139
+ ` ${dim('Run')} ${cyan('mindos <command> --help')} ${dim('for details on any command.')}`,
140
+ );
141
+
142
+ if (!showAll) {
143
+ lines.push(` ${dim('Run')} ${cyan('mindos --all')} ${dim('to see all commands.')}`);
144
+ }
1330
145
 
1331
- if (sub === 'off') {
1332
- await setSyncEnabled(false);
1333
- await stopSyncDaemon();
1334
- return;
1335
- }
146
+ lines.push('');
147
+ console.log(lines.join('\n'));
148
+ }
1336
149
 
1337
- // Unknown subcommand check
1338
- if (sub) {
1339
- const validSubs = ['init', 'now', 'conflicts', 'on', 'off'];
1340
- if (!validSubs.includes(sub)) {
1341
- console.error(red(`Unknown sync subcommand: ${sub}`));
1342
- console.error(dim(`Available: ${validSubs.join(' | ')}`));
1343
- process.exit(EXIT.ARGS);
1344
- }
1345
- }
150
+ /**
151
+ * Show help for a specific command.
152
+ * Delegates to the command's own printHelp() if it exists,
153
+ * otherwise auto-generates from meta.
154
+ */
155
+ function showCommandHelp(mod) {
156
+ if (typeof mod.printHelp === 'function') {
157
+ mod.printHelp();
158
+ } else {
159
+ printCommandHelp(mod);
160
+ }
161
+ }
1346
162
 
1347
- // default: sync status
1348
- const status = await getSyncStatus(mindRoot);
163
+ // ── Entry ─────────────────────────────────────────────────────────────────────
1349
164
 
1350
- if (cliFlags.json) {
1351
- console.log(JSON.stringify(status, null, 2));
1352
- return;
1353
- }
165
+ const showAll = cliFlags.all === true || cliFlags.a === true;
1354
166
 
1355
- if (!status.enabled) {
1356
- console.log(`\n${bold('Sync Status')}`);
1357
- console.log(dim(' Not configured. Run `mindos sync init` to set up.\n'));
1358
- return;
1359
- }
1360
- const ago = status.lastSync
1361
- ? (() => {
1362
- const diff = Date.now() - new Date(status.lastSync).getTime();
1363
- if (diff < 60000) return 'just now';
1364
- if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`;
1365
- return `${Math.floor(diff / 3600000)} hours ago`;
1366
- })()
1367
- : 'never';
167
+ // --help can be boolean (--help) or string (--help agent)
168
+ const helpValue = cliFlags.help || cliFlags.h;
169
+ const hasHelp = helpValue !== undefined && helpValue !== false;
1368
170
 
1369
- console.log(`\n${bold('Sync Status')}`);
1370
- console.log(` ${dim('Provider:')} ${cyan(`${status.provider} (${status.remote})`)}`);
1371
- console.log(` ${dim('Branch:')} ${cyan(status.branch)}`);
1372
- console.log(` ${dim('Last sync:')} ${ago}`);
1373
- console.log(` ${dim('Unpushed:')} ${status.unpushed} commits`);
1374
- console.log(` ${dim('Conflicts:')} ${status.conflicts.length ? yellow(`${status.conflicts.length} file(s)`) : green('none')}`);
1375
- console.log(` ${dim('Auto-sync:')} ${green('● enabled')} ${dim(`(commit: ${status.autoCommitInterval}s, pull: ${status.autoPullInterval / 60}min)`)}`);
1376
- if (status.lastError) {
1377
- console.log(` ${dim('Last error:')} ${red(status.lastError)}`);
1378
- }
1379
- console.log();
1380
- },
171
+ // `mindos --all` → show full help
172
+ if (showAll && !cmd) {
173
+ showGlobalHelp(true);
174
+ process.exit(0);
175
+ }
1381
176
 
1382
- // ── New modular commands (knowledge operations) ──────────────────────────
1383
- file: async () => fileCmd.run(cliArgs, cliFlags),
1384
- space: async () => spaceCmd.run(cliArgs, cliFlags),
1385
- ask: async () => askCmd.run(cliArgs, cliFlags),
1386
- status: async () => statusCmd.run(cliArgs, cliFlags),
1387
- api: async () => apiCmd.run(cliArgs, cliFlags),
1388
- agent: async () => agentCmd.run(cliArgs, cliFlags),
1389
- search: async () => searchCmd.run(cliArgs, cliFlags),
1390
- };
177
+ // `mindos help <cmd>` show help for <cmd>
178
+ if (cmd === 'help') {
179
+ const target = cliArgs[0];
180
+ if (target && commands[target]) {
181
+ showCommandHelp(commands[target]);
182
+ } else {
183
+ showGlobalHelp(showAll);
184
+ }
185
+ process.exit(0);
186
+ }
1391
187
 
1392
- // ── Entry ─────────────────────────────────────────────────────────────────────
188
+ // `mindos --help <cmd>` → parseArgs puts <cmd> as the value of --help
189
+ if (hasHelp && typeof helpValue === 'string' && commands[helpValue]) {
190
+ showCommandHelp(commands[helpValue]);
191
+ process.exit(0);
192
+ }
1393
193
 
1394
- const resolvedCmd = (cliFlags.help || cliFlags.h) ? null : (cmd || (existsSync(CONFIG_PATH) ? getStartMode() : null));
194
+ // Resolve which command to run (or show global help)
195
+ const resolvedCmd = (hasHelp && !cmd) ? null : (cmd || null);
1395
196
 
1396
197
  if (!resolvedCmd || !commands[resolvedCmd]) {
1397
- const pkgVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
1398
- const row = (c, d) => ` ${cyan(c.padEnd(36))}${dim(d)}`;
1399
-
1400
- // Command registry — help is generated entirely from this array.
1401
- // Modular commands provide meta via their exports; inline commands define meta here.
1402
- const helpRegistry = [
1403
- { group: 'Core', usage: 'mindos onboard', summary: 'Interactive setup (aliases: init, setup)' },
1404
- { group: 'Core', usage: 'mindos start', summary: 'Start app + MCP server (production)' },
1405
- { group: 'Core', usage: 'mindos start --daemon', summary: 'Start as background OS service' },
1406
- { group: 'Core', usage: 'mindos dev', summary: 'Start in dev mode' },
1407
- { group: 'Core', usage: 'mindos stop', summary: 'Stop running processes' },
1408
- { group: 'Core', usage: 'mindos restart', summary: 'Stop then start again' },
1409
- { group: 'Core', usage: 'mindos build', summary: 'Build for production' },
1410
- statusCmd.meta,
1411
- { group: 'Core', usage: 'mindos open', summary: 'Open Web UI in browser' },
1412
- fileCmd.meta, spaceCmd.meta, searchCmd.meta, askCmd.meta, agentCmd.meta, apiCmd.meta,
1413
- { group: 'MCP', usage: 'mindos mcp', summary: 'Start MCP server only' },
1414
- { group: 'MCP', usage: 'mindos mcp install [agent]', summary: 'Install MCP config into Agent' },
1415
- { group: 'MCP', usage: 'mindos token', summary: 'Show auth token and MCP config' },
1416
- { group: 'Sync', usage: 'mindos sync', summary: 'Show sync status (init/now/conflicts/on/off)' },
1417
- { group: 'Gateway', usage: 'mindos gateway <sub>', summary: 'Manage service (install/start/stop/status/logs)' },
1418
- { group: 'Config', usage: 'mindos config <sub>', summary: 'View/update config (show/set/unset/validate)' },
1419
- { group: 'Config', usage: 'mindos doctor', summary: 'Health check' },
1420
- { group: 'Config', usage: 'mindos update', summary: 'Update to latest version' },
1421
- { group: 'Config', usage: 'mindos uninstall', summary: 'Fully uninstall MindOS' },
1422
- { group: 'Config', usage: 'mindos logs', summary: 'Tail service logs' },
1423
- ];
1424
-
1425
- const groupLabels = [
1426
- ['Core', 'Core'], ['Knowledge', 'Knowledge'], ['MCP', 'MCP'],
1427
- ['Sync', 'Sync'], ['Gateway', 'Gateway (Background Service)'], ['Config', 'Config & Diagnostics'],
1428
- ];
1429
- const groups = {};
1430
- for (const e of helpRegistry) {
1431
- const g = e.group || 'Other';
1432
- if (!groups[g]) groups[g] = [];
1433
- groups[g].push(row(e.usage || `mindos ${e.name}`, e.summary));
1434
- }
1435
- const sections = groupLabels
1436
- .filter(([k]) => groups[k])
1437
- .map(([k, label]) => `${bold(`${label}:`)}\n${groups[k].join('\n')}`);
198
+ showGlobalHelp(showAll);
199
+ process.exit((cmd && !hasHelp) ? 1 : 0);
200
+ }
1438
201
 
1439
- 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`);
1440
- const isHelp = cliFlags.help || cliFlags.h;
1441
- process.exit((cmd && !isHelp) ? 1 : 0);
202
+ // `mindos <cmd> --help` show help for <cmd> instead of executing it
203
+ if (hasHelp) {
204
+ showCommandHelp(commands[resolvedCmd]);
205
+ process.exit(0);
1442
206
  }
1443
207
 
1444
- commands[resolvedCmd]();
208
+ commands[resolvedCmd].run(cliArgs, cliFlags);