elanous 0.2.7 → 0.2.9

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 (296) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
  51. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +30 -29
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +30 -29
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +30 -29
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +27 -26
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +27 -26
  62. package/apps/pwa/out/chat/index.html +1 -1
  63. package/apps/pwa/out/chat/index.txt +27 -26
  64. package/apps/pwa/out/consult/index.html +1 -1
  65. package/apps/pwa/out/consult/index.txt +30 -29
  66. package/apps/pwa/out/control/index.html +1 -1
  67. package/apps/pwa/out/control/index.txt +30 -29
  68. package/apps/pwa/out/design-check/index.html +1 -1
  69. package/apps/pwa/out/design-check/index.txt +30 -29
  70. package/apps/pwa/out/editor/index.html +1 -1
  71. package/apps/pwa/out/editor/index.txt +30 -29
  72. package/apps/pwa/out/exec/index.html +1 -1
  73. package/apps/pwa/out/exec/index.txt +30 -29
  74. package/apps/pwa/out/field/index.html +1 -1
  75. package/apps/pwa/out/field/index.txt +30 -29
  76. package/apps/pwa/out/healthz +1 -1
  77. package/apps/pwa/out/index.html +1 -1
  78. package/apps/pwa/out/index.txt +27 -26
  79. package/apps/pwa/out/intake/index.html +1 -1
  80. package/apps/pwa/out/intake/index.txt +30 -29
  81. package/apps/pwa/out/live/index.html +1 -1
  82. package/apps/pwa/out/live/index.txt +30 -29
  83. package/apps/pwa/out/market/index.html +1 -1
  84. package/apps/pwa/out/market/index.txt +27 -26
  85. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  86. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  87. package/apps/pwa/out/morning/index.html +1 -1
  88. package/apps/pwa/out/morning/index.txt +30 -29
  89. package/apps/pwa/out/observatory/index.html +1 -1
  90. package/apps/pwa/out/observatory/index.txt +30 -29
  91. package/apps/pwa/out/ops/checklist/index.html +1 -0
  92. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  93. package/apps/pwa/out/ops/release/index.html +1 -0
  94. package/apps/pwa/out/ops/release/index.txt +30 -0
  95. package/apps/pwa/out/reflection/index.html +1 -1
  96. package/apps/pwa/out/reflection/index.txt +28 -27
  97. package/apps/pwa/out/scheduler/index.html +1 -1
  98. package/apps/pwa/out/scheduler/index.txt +30 -29
  99. package/apps/pwa/out/sessions/index.html +1 -1
  100. package/apps/pwa/out/sessions/index.txt +27 -26
  101. package/apps/pwa/out/settings/devices/index.html +1 -1
  102. package/apps/pwa/out/settings/devices/index.txt +30 -29
  103. package/apps/pwa/out/settings/index.html +1 -1
  104. package/apps/pwa/out/settings/index.txt +30 -29
  105. package/apps/pwa/out/setup/done/index.html +1 -1
  106. package/apps/pwa/out/setup/done/index.txt +30 -29
  107. package/apps/pwa/out/setup/index.html +1 -1
  108. package/apps/pwa/out/setup/index.txt +30 -29
  109. package/apps/pwa/out/share/index.html +1 -1
  110. package/apps/pwa/out/share/index.txt +30 -29
  111. package/apps/pwa/out/showroom/index.html +1 -1
  112. package/apps/pwa/out/showroom/index.txt +27 -26
  113. package/apps/pwa/out/tasks/index.html +1 -1
  114. package/apps/pwa/out/tasks/index.txt +30 -29
  115. package/apps/pwa/out/term/index.html +1 -1
  116. package/apps/pwa/out/term/index.txt +30 -29
  117. package/apps/pwa/out/trace/index.html +1 -1
  118. package/apps/pwa/out/trace/index.txt +30 -29
  119. package/apps/pwa/out/vault/index.html +1 -1
  120. package/apps/pwa/out/vault/index.txt +30 -29
  121. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  122. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  123. package/apps/pwa/out/workflows/index.html +1 -1
  124. package/apps/pwa/out/workflows/index.txt +30 -29
  125. package/apps/pwa/out/workspace/index.html +1 -1
  126. package/apps/pwa/out/workspace/index.txt +30 -29
  127. package/apps/pwa/out/worktrees/index.html +1 -1
  128. package/apps/pwa/out/worktrees/index.txt +30 -29
  129. package/graphs/release/recipes.yaml +3 -0
  130. package/graphs/release/release-loop.yaml +3 -1
  131. package/graphs/test-diet/nightly-audit.yaml +18 -0
  132. package/graphs/test-diet/recipes.yaml +12 -0
  133. package/graphs/test-diet/test-diet.yaml +22 -0
  134. package/package.json +2 -1
  135. package/persona-presets/_index.yaml +10 -0
  136. package/persona-presets/_schema.json +146 -0
  137. package/persona-presets/dana.yaml +26 -0
  138. package/persona-presets/haru.yaml +27 -0
  139. package/persona-presets/kai.yaml +27 -0
  140. package/persona-presets/leo.yaml +27 -0
  141. package/persona-presets/mira.yaml +30 -0
  142. package/persona-presets/nari.yaml +27 -0
  143. package/persona-presets/sora.yaml +27 -0
  144. package/persona-presets/yuna.yaml +27 -0
  145. package/plugins/doc-draft/graphs/run-step.ts +1 -1
  146. package/plugins/geo-check/graphs/run-step.ts +34 -10
  147. package/scripts/field-feed/deliver-node.ts +8 -3
  148. package/scripts/field-feed/lib.ts +2 -0
  149. package/scripts/install.sh +20 -11
  150. package/scripts/persona-presets/lint.ts +79 -0
  151. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  152. package/scripts/release-loop/cutoff.ts +20 -2
  153. package/scripts/release-loop/export-check-node.ts +85 -0
  154. package/scripts/release-loop/gate-node.ts +45 -7
  155. package/scripts/release-loop/node-verdict.ts +8 -1
  156. package/scripts/release-loop/npm-publish-node.ts +10 -4
  157. package/scripts/release-loop/ops-upgrade-node.ts +11 -3
  158. package/scripts/release-loop/publish-node.ts +23 -1
  159. package/scripts/release-loop/shard-plan.ts +17 -0
  160. package/scripts/storyboard/storyboard.ts +167 -0
  161. package/scripts/test-diet/lib.ts +89 -0
  162. package/scripts/test-diet/node.ts +144 -0
  163. package/src/acp/auth-methods.ts +30 -0
  164. package/src/acp/server.ts +5 -5
  165. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  166. package/src/auth/setup-link-tokens.ts +153 -0
  167. package/src/budget/unified-usage.ts +75 -0
  168. package/src/channel-bot-token.ts +37 -10
  169. package/src/channel-import/detect.ts +191 -0
  170. package/src/channel-import/run.ts +111 -0
  171. package/src/chat/index.ts +9 -9
  172. package/src/cli/config-test-sync.ts +4 -0
  173. package/src/cli/decisions-cli.ts +13 -3
  174. package/src/cli/doctor-bundle.ts +128 -0
  175. package/src/cli/doctor-cli.ts +36 -1
  176. package/src/cli/doctor-local-llm.ts +95 -0
  177. package/src/cli/import-cli.ts +60 -0
  178. package/src/cli/mcp-login.ts +13 -4
  179. package/src/cli/op-proxy-sync.ts +83 -0
  180. package/src/cli/persona-cli.ts +116 -0
  181. package/src/cli/pr-cli.ts +6 -2
  182. package/src/cli/release-cli.ts +49 -8
  183. package/src/cli/skills-cli.ts +173 -0
  184. package/src/cli/start-cli.ts +13 -4
  185. package/src/codex/setup.ts +15 -13
  186. package/src/dashboard/dashboard-status-lines.ts +45 -0
  187. package/src/dashboard/first-screen-band.ts +35 -0
  188. package/src/dashboard/index.ts +87 -14
  189. package/src/dashboard/provider-slash-runtime.ts +28 -11
  190. package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
  191. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  192. package/src/dashboard/turn-stream-formatter.ts +1 -1
  193. package/src/decisions/decision-cards.ts +291 -0
  194. package/src/decisions/decision-ledger.ts +5 -2
  195. package/src/decisions/discord-decision-cards.ts +133 -0
  196. package/src/decisions/telegram-decision-cards.ts +100 -0
  197. package/src/directives/version-at.ts +77 -0
  198. package/src/discord.ts +17 -0
  199. package/src/domains/outbound-alert.ts +44 -1
  200. package/src/domains/repeated-failure.ts +1 -1
  201. package/src/error-report/report.ts +200 -0
  202. package/src/exec-requests/planner.ts +8 -2
  203. package/src/exec-requests/runner.ts +64 -24
  204. package/src/exec-requests/store.ts +2 -0
  205. package/src/field/field-feed-auto.ts +48 -0
  206. package/src/field/field-reel.ts +8 -1
  207. package/src/hooks/error-report.ts +210 -0
  208. package/src/hooks/receiver.ts +41 -1
  209. package/src/import/apply.ts +128 -0
  210. package/src/import/plan.ts +197 -0
  211. package/src/index.ts +98 -11
  212. package/src/input-core/text-entry.ts +13 -5
  213. package/src/intake-plane/seat-answer.ts +96 -0
  214. package/src/intake-plane/seat-doc-route.ts +17 -3
  215. package/src/maturity/feature-maturity.ts +2 -0
  216. package/src/nexus/api/consult-requests.ts +19 -8
  217. package/src/nexus/api/error-reports.ts +108 -0
  218. package/src/nexus/api/http-server.ts +41 -1
  219. package/src/nexus/api/meta-api.ts +27 -0
  220. package/src/nexus/api/operator.ts +61 -0
  221. package/src/nexus/api/ops-api.ts +139 -0
  222. package/src/nexus/api/ops-seats-sources.ts +65 -0
  223. package/src/nexus/api/ops-seats.ts +114 -0
  224. package/src/nexus/api/outbound-report.ts +7 -0
  225. package/src/nexus/api/public-routes.ts +14 -1
  226. package/src/nexus/api/running-runs-background.ts +68 -0
  227. package/src/nexus/api/running-runs-child.ts +6 -0
  228. package/src/nexus/api/setup-claim.ts +21 -0
  229. package/src/nexus/api/share-captures.ts +91 -0
  230. package/src/nexus/api/skill-problems.ts +56 -0
  231. package/src/nexus/api/terminals.ts +24 -8
  232. package/src/nexus/config/secrets/file-backend.ts +56 -18
  233. package/src/nexus/index.ts +25 -1
  234. package/src/nexus/setup-status.ts +7 -2
  235. package/src/oauth/browser-availability.ts +14 -0
  236. package/src/oauth/codex-quota-policy.ts +16 -0
  237. package/src/onboarding/continue-here.ts +13 -8
  238. package/src/onboarding/first-run-doctor.ts +54 -0
  239. package/src/onboarding/web-first.ts +8 -1
  240. package/src/onboarding.ts +66 -12
  241. package/src/persona/presets.ts +105 -0
  242. package/src/release-loop/checklist.ts +33 -69
  243. package/src/release-loop/feature-store.ts +327 -0
  244. package/src/release-loop/manifest.ts +59 -5
  245. package/src/seat-dispatch/ceo-commands.ts +65 -0
  246. package/src/self-dev/orchestrate.ts +18 -11
  247. package/src/shell-env-bootstrap.ts +44 -7
  248. package/src/skills/connect-claude-plugin.ts +142 -0
  249. package/src/skills/connect.ts +70 -0
  250. package/src/skills/index.ts +29 -1
  251. package/src/skills/repair.ts +124 -0
  252. package/src/skills/runner.ts +11 -2
  253. package/src/skills/tools/usage-slash.ts +8 -7
  254. package/src/skills/trigger-extract.ts +1 -1
  255. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  256. package/src/telegram-commands.ts +29 -0
  257. package/src/telegram-run.ts +83 -13
  258. package/src/telegram.ts +34 -0
  259. package/src/thinking-line.ts +28 -20
  260. package/src/tui/chat/execution-badge.ts +1 -1
  261. package/src/user-config.ts +111 -13
  262. package/src/ux-sim/turn-states.ts +1 -1
  263. package/src/version/packed-revision.json +1 -1
  264. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
  294. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
  295. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
  296. /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
package/src/chat/index.ts CHANGED
@@ -431,7 +431,14 @@ export interface SlashCommand {
431
431
 
432
432
  // Default commands available in the input
433
433
  export const SLASH_COMMANDS: SlashCommand[] = [
434
- { name: 'directive', aliases: [], description: 'Record a directive as a Linear issue' },
434
+ { name: 'help', aliases: ['?'], description: '명령과 키 보기' },
435
+ { name: 'resume', aliases: [], description: '지난 대화 이어 가기 · /resume <id 앞자리>' },
436
+ { name: 'model', aliases: ['m'], description: '모델 바꾸기 — /model <codex|terra|sol|luna|opus|sonnet|grok> (list + current when no arg). OpenAI 는 Codex(Responses API)만. effort 는 /reasoning.', subcommands: ['codex', 'terra', 'sol', 'luna', 'opus', 'sonnet', 'grok'] },
437
+ { name: 'clear', aliases: ['cls'], description: '대화 기록 지우기' },
438
+ { name: 'status', aliases: ['st'], description: '지금 상태 — 모델 · 연결 · 계정' },
439
+ { name: 'remaining', aliases: [], description: '계정별 남은 사용량' },
440
+ { name: 'setup', aliases: [], description: '상세 설정 — /setup · /setup reset(다음 실행 때 마법사 다시)', subcommands: ['help', 'reset'] },
441
+ { name: 'quit', aliases: ['q', 'exit'], description: '나가기' },
435
442
  { name: 'run-skill', aliases: ['rs', 'run'], description: 'Execute a skill via its SKILL.md prompt' },
436
443
  { name: 'ad', aliases: [], description: 'Create an advertising plan from a URL, brief, or attached image' },
437
444
  // B4 — craft rulebook verdict, same resolution as `elanous repo design-check`
@@ -440,10 +447,8 @@ export const SLASH_COMMANDS: SlashCommand[] = [
440
447
  { name: 'design', aliases: ['design-check'], description: 'Design rulebooks this repository declares · /design [--declared] · /design pick [number|id] to choose a design direction', subcommands: ['--declared', 'pick'] },
441
448
  { name: 'provider', aliases: ['p'], description: 'LLM providers — /provider (list) · next (cycle, also Alt+M / pill click) · use <name> · pick (visual picker) · reset', subcommands: ['next', 'use', 'pick', 'picker', 'menu', 'reset', 'list'] },
442
449
  { name: 'reasoning', aliases: ['r', 'think'], description: 'Reasoning level (codex effort + summary, anthropic extended-thinking budget) — /reasoning [off|low|medium|high|xhigh] (cycle when no arg · xhigh 는 모델 상한이 xhigh 이상일 때만 wire 에 실린다, 아니면 high 로 깎인다)', subcommands: ['off', 'low', 'medium', 'high', 'xhigh'] },
443
- { name: 'model', aliases: ['m'], description: 'Switch active model — /model <codex|terra|sol|luna|opus|sonnet|grok> (list + current when no arg). OpenAI 는 Codex(Responses API)만. effort 는 /reasoning.', subcommands: ['codex', 'terra', 'sol', 'luna', 'opus', 'sonnet', 'grok'] },
444
450
  { name: 'local', aliases: ['ll'], description: 'Local OpenAI-compatible LLM — ping / models / test / use', subcommands: ['ping', 'models', 'test', 'use', 'status'] },
445
451
  { name: 'session', aliases: ['sess'], description: 'Session resume — list / load / sync / new (handoff from mobile)', subcommands: ['list', 'load', 'sync', 'new'] },
446
- { name: 'resume', aliases: [], description: 'Pick a past session, or load one by id prefix · /resume <id-prefix>' },
447
452
  { name: 'fork', aliases: [], description: 'Copy this session into a new one' },
448
453
  { name: 'rewind', aliases: [], description: 'Rewind — 과거 user 턴 픽커로 되감기 (원본 보존 · 새 세션 분기 · /rewind <n> 숏컷)' },
449
454
  // TUI 부활 C-a (2026-07-12): autopilot 미션 TUI 표면 — CLI 동등 단일 창구 소비.
@@ -454,12 +459,8 @@ export const SLASH_COMMANDS: SlashCommand[] = [
454
459
  { name: 'sync', aliases: ['s'], description: 'Enter sync mode' },
455
460
  { name: 'plugin', aliases: ['plugins'], description: 'Manage plugins', subcommands: ['list', 'activate', 'deactivate', 'reload'] },
456
461
  { name: 'widget', aliases: ['widgets'], description: 'Manage widgets', subcommands: ['list', 'reload', 'instances'] },
457
- { name: 'clear', aliases: ['cls'], description: 'Clear chat log' },
458
462
  { name: 'log', aliases: [], description: 'Chat Log controls — size / clear / filter / search / freeze / solo / turn / fold / help', subcommands: ['size', 'clear', 'filter', 'search', 'freeze', 'solo', 'turn', 'fold', 'help'] },
459
- { name: 'help', aliases: ['?'], description: 'Show help overlay' },
460
- { name: 'setup', aliases: [], description: 'Onboarding wizard guide — /setup (anchor) · /setup reset (re-run wizard on next boot)', subcommands: ['help', 'reset'] },
461
463
  { name: 'memory', aliases: ['mem'], description: 'Memory ops — list / show / search / add / delete (see `elanous memory --help`)', subcommands: ['list', 'show', 'search', 'add', 'delete', 'index'] },
462
- { name: 'status', aliases: ['st'], description: 'Print the claude-code-style status pills (working dir + git + model)' },
463
464
  { name: 'export', aliases: [], description: 'Export this conversation transcript to a markdown file — /export [path] (default ~/temp/elanous-transcript-<stamp>.md)' },
464
465
  { name: 'delta', aliases: ['diffs'], description: 'Source delta browser — open the latest turn-scoped file diff popup', subcommands: ['open', 'help'] },
465
466
  { name: 'theme', aliases: [], description: 'Theme controls — list / switch / use / reset / preview / export', subcommands: ['list', 'switch', 'use', 'reset', 'preview', 'export'] },
@@ -486,7 +487,6 @@ export const SLASH_COMMANDS: SlashCommand[] = [
486
487
  { name: 'acp', aliases: [], description: 'ACP chat — stream claude-code / codex / gemini replies into the chat pane (not a VW spawn). /acp codex points at the canonical codex app-server path; /acp cas is a synonym.', subcommands: ['claude', 'codex', 'gemini', 'cas', 'cancel', 'status', 'drop'] },
487
488
  { name: 'conv', aliases: [], description: 'Conversation widget/popup host — /conv list · /conv open <session-id> · /conv layout <cascade|tile|stack> · /conv focus <next|prev>.', subcommands: ['list', 'ls', 'open', 'layout', 'focus'] },
488
489
  { name: 'handoff', aliases: [], description: 'H5 P3 cross-agent context handoff: /handoff <from_session_id> <to_brand> [--channels r,p,m] [--prompt "prefix"]. Takes source session snapshot (filtered by channels if observer present) and launches target via adapter registry. Brands: codex · claude · claude-code · gemini · elanous.' },
489
- { name: 'remaining', aliases: [], description: 'What each account has left' },
490
490
  { name: 'agent-room', aliases: [], description: 'H6 P4 VW agent-room — /agent-room <N> <brands...> · /agent-room list · /agent-room close <id> · N ∈ {2,3,4} · brands: codex/claude/gemini/elanous/auto/lll:<m>', subcommands: ['list', 'close', 'preset', 'help'] },
491
491
  { name: 'showroom', aliases: ['sr'], description: 'Showroom v2 multi-LLM lane composer — /showroom (default 2-pane) · /showroom <lane1> <lane2> [<lane3> [<lane4>]] · lane = role:provider[:transport] · roles plan/build/exec/review/reflect · pair with /lane and /relay for cross-lane handoffs.', subcommands: ['help'] },
492
492
  { name: 'reply', aliases: [], description: 'H6 P5 AgentReply — /reply <target-session-id> <message...> · send message to a live session, capture the response · flags: --from · --channels r,m · --idle-ms · --timeout-ms', subcommands: ['help'] },
@@ -503,7 +503,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
503
503
  { name: 'qc', aliases: [], description: 'Quick-control — arm one-shot control mode; next message runs as control, then auto-return to chat.' },
504
504
  { name: 'voice-chat', aliases: ['vc'], description: 'Continuous voice chat mode — speak, elanous replies in voice (Phase 4-5). Subcommands: start / stop / cancel / status. Chord: Alt+R toggles enter/exit anywhere.', subcommands: ['start', 'stop', 'cancel', 'status'] },
505
505
  { name: 'auto-tts', aliases: ['tts', 'autotts'], description: 'Auto-TTS for chat responses (Phase 2) — speaks LLM replies sentence-by-sentence. Subcommands: on / off / toggle / status.', subcommands: ['on', 'off', 'toggle', 'status'] },
506
- { name: 'quit', aliases: ['q', 'exit'], description: 'Exit application' },
506
+ { name: 'directive', aliases: [], description: '(운영) 지시를 Linear 이슈로 기록' },
507
507
  ];
508
508
 
509
509
  /** Every user-visible slash spelling, normalized for runtime-catalog comparison. */
@@ -41,6 +41,9 @@ export const TEST_SYNC_AUX_FILES: readonly string[] = [
41
41
  'llm-fallback.json', // 모델 폴백 체인
42
42
  'auth.json', // oauth 자격 (LLM provider)
43
43
  'identity.json', // self identity — 무해·자기인식 일관
44
+ // POL1 (10-01): the LLM policy (credit use · pace · grants). Without it a tree's test universe resolved «fallback»
45
+ // while production said «credits», and every harness run from a work tree went to grok.
46
+ 'policy/llm.yaml',
44
47
  ];
45
48
 
46
49
  /** 의도적 제외 (복사 금지 — 이유 명시. 목록 변경 시 반드시 사유와 함께). */
@@ -201,6 +204,7 @@ export function syncTestConfig(testDir: string, srcDir: string = prodConfigDir()
201
204
  // 목록 뒤의 `auth.json`(codex 로그인)이 복사되지 않아 격리 우주의 자식이 codex 를 못 썼다.
202
205
  // ⇒ 이미 같은 바이트면 건너뛴다(멱등) — 다르면 종전처럼 복사한다(실패는 그대로 드러난다).
203
206
  if (existsSync(to) && readFileSync(from).equals(readFileSync(to))) { chmodSync(to, 0o600); skippedIdentical.push(f); continue; }
207
+ mkdirSync(dirname(to), { recursive: true });
204
208
  copyFileSync(from, to);
205
209
  chmodSync(to, 0o600);
206
210
  copied.push(f);
@@ -9,6 +9,12 @@ function date(raw?: string): string | undefined {
9
9
  if (!Number.isFinite(parsed.getTime())) throw new Error(`invalid since: ${raw}`);
10
10
  return parsed.toISOString();
11
11
  }
12
+ /** `+6h` → now + 6 hours; otherwise a UTC ISO timestamp (validated by the ledger). */
13
+ function dueAt(raw: string): string {
14
+ const hours = /^\+(\d+(?:\.\d+)?)h$/.exec(raw.trim());
15
+ if (hours) return new Date(Date.now() + Number(hours[1]) * 3600000).toISOString();
16
+ return raw.trim();
17
+ }
12
18
  const kst = (at: string) => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'medium' }).format(new Date(at)) + ' KST';
13
19
  const version = (e: DecisionEntry['version']) => e ? `released=${e.released ?? '-'} · dev=${e.dev ?? '-'}${e.codename ? ` «${e.codename}»` : ''}` : '판 미상';
14
20
  function agent(o?: string): string {
@@ -30,6 +36,7 @@ export function formatDecisionDetail(e: DecisionEntry): string {
30
36
  '선택지 | 결과', ...(e.options.length ? e.options.map(o => `${o.key}: ${o.label} | ${o.consequence}`) : ['원문에 선택지 없음 · 확인 필요']),
31
37
  'skipped' in e.recommendation ? `권고 생략: ${e.recommendation.reason}` : `권고: ${e.recommendation.option} — ${e.recommendation.why}`,
32
38
  `올림: ${e.raisedAt ? kst(e.raisedAt) : '시각 미상'} · ${e.raisedBy.agent} · ${version(e.version)}`,
39
+ ...(e.dueAt ? [`기한: ${kst(e.dueAt)}`] : []),
33
40
  ...(e.importedAt ? [`가져옴: ${kst(e.importedAt)}`] : []),
34
41
  ...(e.status === 'decided' ? [`결정: ${e.decidedAt ? kst(e.decidedAt) : '시각 미상'} · ${e.decidedBy?.kind === 'auto' ? `AUTO ${e.decidedBy.agent} (${e.decidedBy.delegation})` : e.decidedBy?.kind === 'human' ? 'human' : '주체 미상'} · ${e.choice ?? '선택 키 미상'} · ${e.versionAtDecision ? version(e.versionAtDecision) : '결정 당시 판 미상'}`,
35
42
  ...(e.note ? [`메모: ${e.note}`] : [])] : []),
@@ -48,14 +55,17 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
48
55
  .requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
49
56
  .option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
50
57
  .option('--track <track>').option('--agent <agent>').option('--session <id>')
51
- .option('--ref <url>', '참조 (반복)', repeat, [] as string[]).option('--json')
52
- .action((o: { title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; track?: DecisionTrack; agent?: string; session?: string; ref: string[]; json?: boolean }) => fail(() => {
58
+ .option('--ref <url>', '참조 (반복)', repeat, [] as string[])
59
+ .option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
60
+ .option('--json')
61
+ .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; track?: DecisionTrack; agent?: string; session?: string; ref: string[]; json?: boolean }) => fail(() => {
53
62
  if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
54
63
  if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
55
64
  const options: DecisionOption[] = o.option.map(parseOption);
56
65
  const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
57
66
  recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
58
- raisedBy: { agent: agent(o.agent), ...(o.track ? { track: o.track } : {}), ...(o.session ? { session: o.session } : {}) }, ...(o.ref.length ? { refs: o.ref } : {}) });
67
+ raisedBy: { agent: agent(o.agent), ...(o.track ? { track: o.track } : {}), ...(o.session ? { session: o.session } : {}) }, ...(o.ref.length ? { refs: o.ref } : {}),
68
+ ...(o.due ? { dueAt: dueAt(o.due) } : {}) });
59
69
  emit(entry, o.json, `올림: ${formatDecisionRow(entry)}`);
60
70
  }));
61
71
  root.command('list').description('결정 목록 (기본 열린 것)')
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { closeSync, existsSync, lstatSync, mkdtempSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import { homedir, platform, arch } from 'node:os';
4
+ import { join, resolve, sep } from 'node:path';
5
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
6
+ import { resolveCurrentInstance } from '../instance/current.js';
7
+ import { LogStore, logsDbPath, type LogStoreRow } from '../mss/logging/log-store.js';
8
+ import { cliVersion, codeRevision, packageVersion } from '../version/code-revision.js';
9
+ import { runDoctor, defaultReadInstallPrefix, type DoctorOptions, type DoctorReport } from './doctor-cli.js';
10
+
11
+ const REDACTED = '<redacted>';
12
+
13
+ /** Recursively redact named secrets and secret-shaped text before serialization. */
14
+ export function redactForBundle(value: unknown, home: string = homedir(), redactNamedFields: boolean = true): unknown {
15
+ if (typeof value === 'string') {
16
+ let text = value;
17
+ if (home && home !== sep) {
18
+ // Match the home as a path component, including JSON-escaped slashes.
19
+ const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
20
+ text = text.replace(new RegExp(`${escaped}(?=[/\\\\]|$)`, 'g'), '~');
21
+ }
22
+ // By kind, not by the last example found: headers that carry credentials, credentials inside URLs,
23
+ // known token shapes, and any `name=value` / `"name": "value"` whose name says it is a credential.
24
+ return text
25
+ .replace(/\b((?:Proxy-)?Authorization\s*:\s*)[^\r\n"']+/gi, `$1${REDACTED}`)
26
+ .replace(/\b((?:Set-)?Cookie\s*:\s*)[^\r\n"']+/gi, `$1${REDACTED}`)
27
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@"'<>]+@/gi, `$1${REDACTED}@`)
28
+ .replace(/(["']?(?:[\w-]*(?:token|key|secret|password|passwd|pwd|cookie|session|credential|auth))["']?\s*[:=]\s*["']?)[^\s"',}&]+/gi, `$1${REDACTED}`)
29
+ .replace(/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/g, `$1 ${REDACTED}`)
30
+ .replace(/\b(?:sk-[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|glpat-[A-Za-z0-9_-]+|xox[a-z]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35})\b/g, REDACTED)
31
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED);
32
+ }
33
+ if (Array.isArray(value)) return value.map((entry) => redactForBundle(entry, home, redactNamedFields));
34
+ if (value && typeof value === 'object') {
35
+ const result: Record<string, unknown> = Object.create(null);
36
+ for (const [key, entry] of Object.entries(value)) {
37
+ const safeKey = redactForBundle(key, home) as string;
38
+ result[safeKey] = redactNamedFields && /(?:token|key|secret|password|passwd|pwd|authorization|cookie|session|credential|auth)/i.test(key)
39
+ ? REDACTED : redactForBundle(entry, home, redactNamedFields);
40
+ }
41
+ return result;
42
+ }
43
+ return value;
44
+ }
45
+
46
+ export interface DoctorBundleOptions {
47
+ outDir: string;
48
+ now?: Date;
49
+ /** Read-only doctor seams for isolated tests. */
50
+ doctorOptions?: DoctorOptions;
51
+ home?: string;
52
+ configDir?: string;
53
+ logDbPath?: string;
54
+ doctorReport?: DoctorReport;
55
+ }
56
+
57
+ export interface DoctorBundleResult { path: string; files: string[]; bytes: number }
58
+
59
+ /** Bundle only generated, redacted entries. Never put an unprocessed source file into tar. */
60
+ export function buildDoctorBundle({ outDir, now = new Date(), doctorOptions = {}, home = homedir(), configDir = getElanousConfigDir(), logDbPath = logsDbPath(), doctorReport }: DoctorBundleOptions): DoctorBundleResult {
61
+ const instant = now.toISOString();
62
+ const stamp = instant.replace(/[-:]/g, '').slice(0, 15).replace('T', '-');
63
+ mkdirSync(outDir, { recursive: true, mode: 0o700 });
64
+ const destination = resolve(outDir, `elanous-diagnostics-${stamp}.tar.gz`);
65
+ if (existsSync(destination)) throw new Error('diagnostics archive already exists');
66
+ const work = mkdtempSync(join(outDir, '.elanous-diagnostics-'));
67
+ const files: string[] = [];
68
+ const put = (name: string, data: unknown): void => {
69
+ writeFileSync(join(work, name), JSON.stringify(redactForBundle(data, home, name === 'config.redacted.json' || name === 'onboarding.json'), null, 2) + '\n', { mode: 0o600 });
70
+ files.push(name);
71
+ };
72
+ try {
73
+ const report = doctorReport ?? runDoctor(doctorOptions);
74
+ const universe = resolveCurrentInstance({ cwd: () => process.cwd() });
75
+ const packageRoot = resolve(import.meta.dir, '../..');
76
+ const installPrefix = doctorOptions.readInstallPrefix?.() ?? defaultReadInstallPrefix(packageRoot, existsSync);
77
+ let logs: LogStoreRow[] = [];
78
+ let logStatus: 'ok' | 'no-store' = 'no-store';
79
+ if (existsSync(logDbPath)) {
80
+ try {
81
+ const store = LogStore.openReadOnly(logDbPath);
82
+ try {
83
+ logs = store.query({ sinceMs: now.getTime() - 86_400_000, untilMs: now.getTime(), limit: 500 });
84
+ logStatus = 'ok';
85
+ } finally { store.close(); }
86
+ } catch { /* Unreadable store: export no unverified log bytes. */ }
87
+ }
88
+ put('summary.json', {
89
+ version: packageVersion(), sha: codeRevision() ?? 'unknown', cliVersion: cliVersion(),
90
+ os: platform(), arch: arch(), node: process.version, bun: typeof Bun === 'undefined' ? null : Bun.version,
91
+ installPath: installPrefix ?? packageRoot,
92
+ universe: { kind: universe.kind, root: universe.root }, createdAt: instant, logs: logStatus,
93
+ });
94
+ put('doctor.json', report);
95
+ let config: unknown = {};
96
+ const configPath = doctorOptions.configPath ?? join(configDir, 'config.json');
97
+ try {
98
+ if (lstatSync(configPath).isFile()) config = JSON.parse(readFileSync(configPath, 'utf8'));
99
+ } catch { config = { status: 'unavailable' }; }
100
+ put('config.redacted.json', config);
101
+ put('credentials.json', report.credentials.map(({ name, resolved, source }) => ({ name, present: resolved, source })));
102
+ writeFileSync(join(work, 'logs-recent.jsonl'), logs.map((row) => {
103
+ const entry = row as { data: string | null };
104
+ let data = entry.data;
105
+ if (data) {
106
+ try { data = JSON.stringify(redactForBundle(JSON.parse(data), home)); }
107
+ catch { /* Non-JSON log bodies are redacted as plain text below. */ }
108
+ }
109
+ return JSON.stringify(redactForBundle({ ...row, data }, home, false));
110
+ }).join('\n') + (logs.length ? '\n' : ''), { mode: 0o600 });
111
+ files.push('logs-recent.jsonl');
112
+ const onboardingPath = join(configDir, 'onboarding.json');
113
+ try {
114
+ if (lstatSync(onboardingPath).isFile()) put('onboarding.json', JSON.parse(readFileSync(onboardingPath, 'utf8')));
115
+ } catch { /* An absent or unreadable optional record is not exported. */ }
116
+ const archiveFd = openSync(destination, 'wx', 0o600);
117
+ let tarSucceeded = false;
118
+ try {
119
+ const tar = spawnSync('tar', ['-czf', '-', '-C', work, ...files], { stdio: ['ignore', archiveFd, 'pipe'], timeout: 30_000 });
120
+ tarSucceeded = !tar.error && tar.status === 0;
121
+ } finally {
122
+ closeSync(archiveFd);
123
+ if (!tarSucceeded) rmSync(destination, { force: true });
124
+ }
125
+ if (!tarSucceeded) throw new Error('could not create diagnostics archive');
126
+ return { path: destination, files, bytes: statSync(destination).size };
127
+ } finally { rmSync(work, { recursive: true, force: true }); }
128
+ }
@@ -35,6 +35,9 @@ import { gitInstallPlan } from './git-install-plan.js';
35
35
  import { detectDistroFamily, parseOsRelease } from './doctor-distro.js';
36
36
  import { checkPythonEnv } from './python-cli.js';
37
37
  import { describeModes, formatModeLine, type DoctorMode } from './doctor-modes.js';
38
+ import { formatDoctorLocalLlm, probeDoctorLocalLlm, type DoctorLocalLlm } from './doctor-local-llm.js';
39
+ import type { LlmInventory } from '../llm/local-manager/types.js';
40
+ import { buildDoctorBundle } from './doctor-bundle.js';
38
41
 
39
42
  function defaultCheckPythonEnv(): { status: 'ok' | 'fixable' | 'manual'; evidence: string; remedy?: string } {
40
43
  const c = checkPythonEnv(false);
@@ -102,6 +105,8 @@ export interface DoctorReport {
102
105
  capabilitySummary?: DoctorCapabilitySummary;
103
106
  /** Read-only configuration and chat-use status. Present on a successful report. */
104
107
  modes?: DoctorMode[];
108
+ /** Local LLM inventory; CLI probes separately so synchronous credential reports stay read-only and unchanged. */
109
+ localLlm?: DoctorLocalLlm;
105
110
  /** F1 readiness. Present on a successful report. Absent when the report itself failed. */
106
111
  readiness?: ReadinessReport;
107
112
  reason?: string;
@@ -186,6 +191,8 @@ export interface DoctorCliDeps extends DoctorOptions, Pick<DoctorFixDeps, 'home'
186
191
  resolveAdviceProvider?: (config: UserConfig) => { provider: string; model: string; auth: string };
187
192
  confirmAdvice?: (message: string) => Promise<boolean>;
188
193
  adviceInteractive?: boolean;
194
+ /** Local-manager inventory seam; default calls getInventory(). */
195
+ localLlmInventory?: () => Promise<LlmInventory>;
189
196
  }
190
197
 
191
198
  const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
@@ -1141,6 +1148,7 @@ export function formatDoctorReport(report: DoctorReport, options: { credentials?
1141
1148
  ...(report.externalCommandsCatalogUnavailable ? [`External commands catalog unavailable: ${report.externalCommandsCatalogReason ?? 'catalog/external-commands.yaml is unavailable.'}`] : []),
1142
1149
  ...formatCapabilitySummary(capabilitySummary),
1143
1150
  ...(report.modes ? ['모드:', ...report.modes.map((row) => ` ${formatModeLine(row)}`)] : []),
1151
+ ...(report.localLlm ? formatDoctorLocalLlm(report.localLlm) : []),
1144
1152
  ].join('\n');
1145
1153
  }
1146
1154
 
@@ -1208,12 +1216,37 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
1208
1216
  .description('Reports whether credentials resolve and where each resolution comes from')
1209
1217
  .option('--json', 'structured output')
1210
1218
  .option('--credentials', 'show per-credential details in human-readable output')
1219
+ .option('--bundle', 'write a redacted diagnostics archive for support')
1220
+ .option('--out <dir>', 'output directory for --bundle')
1211
1221
  .option('--fix', 'show repairs; git installation asks on a TTY (other repairs require --yes)')
1212
1222
  .option('--advise', 'ask the configured LLM to rank catalog repairs (read-only advice)')
1213
1223
  .option('--yes', 'apply planned doctor repairs (requires --fix)')
1214
1224
  .option('--sudo', 'also run the planned sudo install lines — only where `sudo -n true` works (requires --fix --yes)')
1215
1225
  .option('--restart', 'restart the nexus service when it runs a different version than this installed copy, then verify it (requires --fix --yes · interrupts bots, terminals and running turns)')
1216
- .action(async (opts: { json?: boolean; credentials?: boolean; fix?: boolean; advise?: boolean; yes?: boolean; sudo?: boolean; restart?: boolean }) => {
1226
+ .action(async (opts: { json?: boolean; credentials?: boolean; bundle?: boolean; out?: string; fix?: boolean; advise?: boolean; yes?: boolean; sudo?: boolean; restart?: boolean }) => {
1227
+ if (opts.bundle) {
1228
+ if (opts.fix || opts.advise || opts.yes || opts.sudo || opts.restart) {
1229
+ err.error('--bundle cannot be combined with repair or advice options');
1230
+ setExitCode(1);
1231
+ return;
1232
+ }
1233
+ try {
1234
+ // Same report as `doctor --json`, local LLM section included, so support sees what the user saw.
1235
+ const doctorReport = runDoctor(deps);
1236
+ if (doctorReport.ok) doctorReport.localLlm = await probeDoctorLocalLlm(deps.localLlmInventory);
1237
+ const result = buildDoctorBundle({ outDir: opts.out ?? process.cwd(), doctorOptions: deps, doctorReport });
1238
+ out.log(opts.json ? JSON.stringify(result) : [`Diagnostics: ${result.path} (${result.bytes} bytes)`, 'Contents:', ...result.files.map((file) => ` ${file}`)].join('\n'));
1239
+ } catch {
1240
+ err.error('Could not create diagnostics archive.');
1241
+ setExitCode(1);
1242
+ }
1243
+ return;
1244
+ }
1245
+ if (opts.out !== undefined) {
1246
+ err.error('--out requires --bundle');
1247
+ setExitCode(1);
1248
+ return;
1249
+ }
1217
1250
  if (opts.yes && !opts.fix) {
1218
1251
  err.error('--yes requires --fix');
1219
1252
  setExitCode(1);
@@ -1240,6 +1273,7 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
1240
1273
  setExitCode(1);
1241
1274
  return;
1242
1275
  }
1276
+ report.localLlm = await probeDoctorLocalLlm(deps.localLlmInventory);
1243
1277
  if (!opts.fix && !opts.advise) {
1244
1278
  out.log(opts.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report, { credentials: opts.credentials }));
1245
1279
  if (report.requiredMissing?.length) setExitCode(1);
@@ -1358,6 +1392,7 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
1358
1392
  : undefined;
1359
1393
  // PATH cannot change in this process; re-probe readiness rather than treating a saved block as PATH=ok.
1360
1394
  const currentReport = results || gitInstall?.ran ? runDoctor(deps) : report;
1395
+ if (currentReport.ok) currentReport.localLlm = report.localLlm;
1361
1396
  if (gitInstall?.ran && gitInstall.ok && currentReport.requiredMissing?.includes('git')) {
1362
1397
  gitInstall.detail = 'git init 성공 · 현재 doctor PATH 에서 git 을 찾지 못했습니다. 새 셸에서 다시 확인하세요.';
1363
1398
  }
@@ -0,0 +1,95 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { probeDocker } from '../llm/local-manager/docker-probe.js';
3
+ import { probeLmstudio } from '../llm/local-manager/lmstudio-probe.js';
4
+ import { probeMlx } from '../llm/local-manager/mlx-probe.js';
5
+ import { listNodes } from '../llm/local-manager/node-registry.js';
6
+ import { probeOllama } from '../llm/local-manager/ollama-probe.js';
7
+ import type { LlmInventory, LlmModel, LlmNode, LlmRuntime } from '../llm/local-manager/types.js';
8
+
9
+ export interface DoctorLocalLlm {
10
+ nodes: { kind: 'ollama' | 'lmstudio' | 'mlx' | 'docker'; baseUrl: string; reachable: boolean; models: string[] }[];
11
+ timedOut: boolean;
12
+ /** Inventory lookup failed; absence of servers was not established. */
13
+ failed?: boolean;
14
+ }
15
+
16
+ const kinds = ['ollama', 'lmstudio', 'mlx', 'docker'] as const;
17
+ type Kind = (typeof kinds)[number];
18
+
19
+ const baseUrlFields: Record<Kind, 'ollamaBaseUrl' | 'lmstudioBaseUrl' | 'mlxBaseUrl' | 'dockerBaseUrl'> = {
20
+ ollama: 'ollamaBaseUrl', lmstudio: 'lmstudioBaseUrl', mlx: 'mlxBaseUrl', docker: 'dockerBaseUrl',
21
+ };
22
+
23
+ type Probe = (node: { id: string; isLocal: boolean }, deps: { timeoutMs?: number }) => Promise<{ reachable: boolean; models: readonly LlmModel[]; warnings: readonly string[] }>;
24
+
25
+ /** Doctor shows only this machine, so it probes only the local node — the full inventory also probes every remote
26
+ * fleet node over ssh and took ~7 s cold, past the 3 s cap («시간 초과» with LM Studio running · 10-01). */
27
+ export async function localInventory(deps: {
28
+ nodes?: () => readonly LlmNode[];
29
+ probes?: Record<'lmstudio' | 'ollama' | 'mlx' | 'docker', Probe>;
30
+ now?: () => number;
31
+ } = {}): Promise<LlmInventory> {
32
+ const probes = deps.probes ?? { lmstudio: probeLmstudio, ollama: probeOllama, mlx: probeMlx, docker: probeDocker } as Record<'lmstudio' | 'ollama' | 'mlx' | 'docker', Probe>;
33
+ const read = deps.nodes ?? listNodes;
34
+ const nodes: LlmNode[] = [];
35
+ const models: LlmModel[] = [];
36
+ const warnings: string[] = [];
37
+ for (const node of read().filter((n) => n.isLocal)) {
38
+ const seed = { id: node.id, isLocal: true };
39
+ const order = ['lmstudio', 'ollama', 'mlx', 'docker'] as const;
40
+ const results = await Promise.all(order.map((kind) => probes[kind](seed, { timeoutMs: 2500 })));
41
+ const runtimes: LlmRuntime[] = order.filter((_, i) => results[i]!.reachable);
42
+ for (const [i, r] of results.entries()) { models.push(...r.models); for (const w of r.warnings) warnings.push(`${node.id}:${order[i]}:${w}`); }
43
+ // Probes record base URLs in the registry on success — read the node back for them.
44
+ const fresh = read().find((n) => n.id === node.id) ?? node;
45
+ nodes.push({ ...fresh, reachable: runtimes.length > 0, runtimes });
46
+ }
47
+ return { nodes, models, at: (deps.now ?? Date.now)(), cached: false, warnings };
48
+ }
49
+
50
+ export async function probeDoctorLocalLlm(inventory: () => Promise<LlmInventory> = () => localInventory(), timeoutMs = 3000): Promise<DoctorLocalLlm> {
51
+ let timer: ReturnType<typeof setTimeout> | undefined;
52
+ let result: DoctorLocalLlm;
53
+ try {
54
+ const snapshot = await Promise.race([
55
+ Promise.resolve().then(inventory),
56
+ new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), timeoutMs); }),
57
+ ]);
58
+ result = snapshot === null
59
+ ? { nodes: [], timedOut: true }
60
+ : {
61
+ nodes: snapshot.nodes.filter((node) => node.isLocal).flatMap((node) => kinds.flatMap((kind) => {
62
+ const baseUrl = node[baseUrlFields[kind]];
63
+ if (!node.runtimes.includes(kind) && !baseUrl) return [];
64
+ return [{
65
+ kind,
66
+ baseUrl: baseUrl ?? '',
67
+ reachable: node.reachable === true && node.runtimes.includes(kind),
68
+ models: snapshot.models.filter((model) => model.nodeId === node.id && model.runtime === kind).map((model) => model.id),
69
+ }];
70
+ })),
71
+ timedOut: false,
72
+ };
73
+ } catch {
74
+ result = { nodes: [], timedOut: false, failed: true };
75
+ } finally {
76
+ if (timer !== undefined) clearTimeout(timer);
77
+ }
78
+ debug.log('doctor.local-llm', 'probed', {
79
+ nodes: result.nodes.length,
80
+ reachable: result.nodes.filter((node) => node.reachable).length,
81
+ models: result.nodes.reduce((total, node) => total + node.models.length, 0),
82
+ timedOut: result.timedOut,
83
+ });
84
+ return result;
85
+ }
86
+
87
+ export function formatDoctorLocalLlm(result: DoctorLocalLlm): string[] {
88
+ if (result.timedOut) return ['Local LLM:', ' 시간 초과'];
89
+ if (result.failed) return ['Local LLM:', ' 로컬 LLM 인벤토리를 조회하지 못했다'];
90
+ if (result.nodes.length === 0) return ['Local LLM:', ' 로컬 LLM 서버를 찾지 못했다 — Ollama(:11434) · LM Studio(:1234) 를 띄우면 여기 보인다'];
91
+ return [
92
+ 'Local LLM:',
93
+ ...result.nodes.map((node) => ` ${node.kind} · ${node.baseUrl} · ${node.reachable ? '닿음' : '안 닿음'} · 모델 ${node.models.length}개${node.models.length ? ` · ${node.models.slice(0, 5).join(', ')}` : ''}`),
94
+ ];
95
+ }
@@ -0,0 +1,60 @@
1
+ // EN4 — `elanous import`: one read-only plan over the agents you already use. Applying stays with the sub-commands each item names.
2
+ import type { Command } from 'commander';
3
+ import { homedir } from 'node:os';
4
+ import { debug } from '../debug/log.js';
5
+ import { formatImportPlan, planImport, type ImportPlan } from '../import/plan.js';
6
+ import { applyImport, formatApplied, IMPORT_MODES, undoImport, type ImportMode } from '../import/apply.js';
7
+ import { dirname, join } from 'node:path';
8
+
9
+ export async function buildImportPlan(home = homedir()): Promise<ImportPlan> {
10
+ const { defaultSkillDirs, getUserConfig } = await import('../user-config.js');
11
+ const cfg = getUserConfig();
12
+ const plan = planImport({
13
+ home,
14
+ ...(process.env.CODEX_HOME ? { codexHome: process.env.CODEX_HOME } : {}),
15
+ activeSkillDirs: defaultSkillDirs(cfg),
16
+ mcpServerIds: (cfg.mcp?.servers ?? []).map((server) => server.id),
17
+ });
18
+ for (const item of plan.items) debug.log('import.plan', 'item', { source: item.source, kind: item.kind, status: item.status });
19
+ debug.log('import.plan', 'planned', { sources: plan.sources.filter((s) => s.detected).map((s) => s.id), items: plan.items.length });
20
+ return plan;
21
+ }
22
+
23
+ export function registerImportCommand(program: Command): void {
24
+ program.command('import')
25
+ .description('Show what elanous can bring over from Claude Code, Codex, ~/.agents, OpenClaw and Hermes — a plan first; `--apply` registers skills in place and adds MCP switched off, `--undo` puts everything back')
26
+ .option('--json', 'Print the plan (or the apply result) as JSON')
27
+ .option('--apply', 'Apply the plan: skills in place (ref) or as link/copy · MCP switched off · secrets and hooks only listed')
28
+ .option('--mode <mode>', `How skills come over: ${IMPORT_MODES.join('|')} (default ref — nothing copied)`)
29
+ .option('--pick <ids>', 'Only these item ids (comma-separated, see --json)')
30
+ .option('--undo', 'Undo the last --apply: restore the config bytes and remove links/copies')
31
+ .action(async (opts: { json?: boolean; apply?: boolean; mode?: string; pick?: string; undo?: boolean }) => {
32
+ const { userConfigPath } = await import('../user-config.js');
33
+ const importRoot = join(dirname(userConfigPath()), 'import');
34
+ if (opts.undo) {
35
+ const undone = undoImport(importRoot);
36
+ const { resetSkillIndex } = await import('../skills/index.js');
37
+ resetSkillIndex();
38
+ debug.log('import.apply', 'undone', { found: undone !== null, created: undone?.created.length ?? 0 });
39
+ if (opts.json) console.log(JSON.stringify({ undone: undone !== null }));
40
+ else console.log(undone ? `되돌렸습니다 — 설정을 ${undone.at} 이전으로 돌리고, 만든 것 ${undone.created.length}개를 지웠습니다.` : '되돌릴 가져오기가 없습니다.');
41
+ return;
42
+ }
43
+ const plan = await buildImportPlan();
44
+ if (!opts.apply) { console.log(opts.json ? JSON.stringify(plan, null, 2) : formatImportPlan(plan)); return; }
45
+ if (opts.mode !== undefined && !IMPORT_MODES.includes(opts.mode as ImportMode)) throw new Error(`--mode 는 ${IMPORT_MODES.join('|')} 중 하나다(받은 값: ${opts.mode})`);
46
+ const { getUserConfig, saveUserConfig } = await import('../user-config.js');
47
+ const cfg = getUserConfig();
48
+ const rawMcp = (cfg.raw.mcp && typeof cfg.raw.mcp === 'object' && !Array.isArray(cfg.raw.mcp) ? cfg.raw.mcp : {}) as Record<string, unknown>;
49
+ const manifest = applyImport(plan, { ...(opts.mode ? { mode: opts.mode as ImportMode } : {}), ...(opts.pick ? { pick: opts.pick.split(',').map((id) => id.trim()).filter(Boolean) } : {}) }, {
50
+ home: homedir(), configPath: userConfigPath(), importRoot,
51
+ sources: cfg.skills.sources ?? [],
52
+ rawMcpServers: (Array.isArray(rawMcp.servers) ? rawMcp.servers : []) as Array<Record<string, unknown>>,
53
+ save: (sources, servers) => saveUserConfig({ ...cfg, raw: { ...cfg.raw, mcp: { ...rawMcp, servers } }, skills: { ...cfg.skills, sources } }),
54
+ });
55
+ const { resetSkillIndex } = await import('../skills/index.js');
56
+ resetSkillIndex();
57
+ debug.log('import.apply', 'applied', { mode: manifest.mode, applied: manifest.applied.length, kept: manifest.kept.length, created: manifest.created.length });
58
+ console.log(opts.json ? JSON.stringify({ mode: manifest.mode, applied: manifest.applied, kept: manifest.kept, created: manifest.created.length }, null, 2) : formatApplied(manifest));
59
+ });
60
+ }
@@ -12,6 +12,7 @@ import {
12
12
  } from '../mcp/mcp-oauth.js';
13
13
  import { parseWwwAuthenticate } from '../mcp/client.js';
14
14
  import { debug } from '../debug/log.js';
15
+ import { browserUnavailableReason } from '../oauth/browser-availability.js';
15
16
 
16
17
  const CALLBACK_PATH = '/oauth/callback';
17
18
  const DEFAULT_TIMEOUT_MS = 120_000;
@@ -31,6 +32,8 @@ export interface McpLoginOpts {
31
32
  readConfigFn?: () => { mcp?: { servers: McpServerSpec[] } };
32
33
  fetch?: McpOAuthFetch;
33
34
  openBrowser?: (url: string) => Promise<void>;
35
+ browserEnv?: NodeJS.ProcessEnv;
36
+ browserPlatform?: NodeJS.Platform;
34
37
  createListener?: (handler: (req: IncomingMessage, res: ServerResponse) => void) => Server;
35
38
  /** 발견한 issuer/tokenEndpoint 를 config 에 되쓰는 자리. 테스트 심. */
36
39
  persistDiscoveryFn?: (input: PersistDiscoveryInput) => PersistDiscoveryResult;
@@ -91,10 +94,16 @@ export async function runMcpLogin(opts: McpLoginOpts): Promise<McpLoginResult> {
91
94
  void callbackPromise.catch(() => undefined);
92
95
  out.log(`Open this URL to authorize '${opts.serverId}':`);
93
96
  out.log(authorization.request.url);
94
- try {
95
- await (opts.openBrowser ?? openDefaultBrowser)(authorization.request.url);
96
- } catch {
97
- out.error('Could not open the default browser; open the URL above manually.');
97
+ const why = browserUnavailableReason(opts.browserEnv ?? process.env, opts.browserPlatform ?? process.platform);
98
+ if (why) {
99
+ out.log(why);
100
+ debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
101
+ } else {
102
+ try {
103
+ await (opts.openBrowser ?? openDefaultBrowser)(authorization.request.url);
104
+ } catch {
105
+ out.error('Could not open the default browser; open the URL above manually.');
106
+ }
98
107
  }
99
108
  const callback = await callbackPromise;
100
109
  await exchangeAuthorizationCode(authorization.metadata, authorization.request, callback, {
@@ -0,0 +1,83 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ACP_TOKEN_FILE } from '../auth/token-store.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
7
+ import { userConfigPath } from '../user-config.js';
8
+
9
+ export interface OpProxyConfig {
10
+ host: string;
11
+ bearerFile: string;
12
+ restart: string;
13
+ }
14
+
15
+ /** Read the optional, unnormalized nexus setting from the same config file as the CLI. */
16
+ export function readOpProxyConfig(path = userConfigPath()): OpProxyConfig | undefined {
17
+ try {
18
+ const raw = JSON.parse(readFileSync(path, 'utf8')) as { nexus?: { opProxy?: OpProxyConfig } };
19
+ return raw?.nexus?.opProxy;
20
+ } catch {
21
+ return undefined;
22
+ }
23
+ }
24
+
25
+ type SshExecutor = (argv: string[], input: string) => { status: number | null; error?: Error };
26
+
27
+ export function finishOpProxyRotation(
28
+ proxy: OpProxyConfig | undefined,
29
+ token: string,
30
+ opts: { sync: boolean; ssh?: SshExecutor; tokenFile?: string; out?: (line: string) => void },
31
+ ): boolean | undefined {
32
+ if (!proxy) return undefined;
33
+ if (!opts.sync) {
34
+ (opts.out ?? console.log)('op 프록시 토큰도 갈아야 합니다 — `elanous token rotate --sync-op-proxy`');
35
+ return undefined;
36
+ }
37
+ return syncOpProxyToken(proxy, token, opts);
38
+ }
39
+
40
+ const executeSsh: SshExecutor = (argv, input) => {
41
+ const result = spawnSync('ssh', argv, { input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 });
42
+ return { status: result.status, ...(result.error ? { error: result.error } : {}) };
43
+ };
44
+
45
+ const shellQuote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`;
46
+
47
+ /** Never interpolate the token in a command, diagnostic, or log. Only SSH stdin carries it. */
48
+ export function syncOpProxyToken(
49
+ proxy: OpProxyConfig | undefined,
50
+ token: string,
51
+ opts: { ssh?: SshExecutor; tokenFile?: string; out?: (line: string) => void } = {},
52
+ ): boolean | undefined {
53
+ if (!proxy) return undefined;
54
+ const out = opts.out ?? console.log;
55
+ const host = typeof proxy.host === 'string' ? proxy.host : '';
56
+ const file = typeof proxy.bearerFile === 'string' ? proxy.bearerFile : '';
57
+ const service = typeof proxy.restart === 'string' ? proxy.restart : '';
58
+ const valid = /^[\w][\w.-]*$/.test(host) && file.startsWith('/') && !/[\r\n\0]/.test(file)
59
+ && /^[\w@][\w@.-]*$/.test(service) && !/[\r\n\0]/.test(opts.tokenFile ?? '');
60
+ const remote = `sudo install -m 600 /dev/stdin ${shellQuote(file)} && sudo systemctl restart ${shellQuote(service)}`;
61
+ const manual = valid
62
+ ? `token=$(head -n 1 < ${shellQuote(opts.tokenFile ?? join(getElanousConfigDir(), ACP_TOKEN_FILE))}) && [ -n "$token" ] && printf 'ELANOUS_OP_BEARER=%s\\n' "$token" | ssh ${shellQuote(host)} ${shellQuote(remote)}`
63
+ : 'nexus.opProxy 의 host, bearerFile, restart 를 확인한 뒤 bearer 파일을 갱신하고 서비스를 재시작하세요';
64
+ let reason = '';
65
+ if (!valid) {
66
+ reason = 'nexus.opProxy 설정 오류';
67
+ } else {
68
+ try {
69
+ const result = (opts.ssh ?? executeSsh)([host, remote], `ELANOUS_OP_BEARER=${token}\n`);
70
+ if (result.status === 0 && !result.error) {
71
+ debug.log('auth.op-proxy-sync', 'synced', { host, reason: 'ok' });
72
+ out(`op 프록시 토큰을 바꿨습니다(${host})`);
73
+ return true;
74
+ }
75
+ reason = result.error ? 'ssh 실행 실패' : `ssh 종료 코드 ${result.status ?? '미상'}`;
76
+ } catch {
77
+ reason = 'ssh 실행 실패';
78
+ }
79
+ }
80
+ debug.log('auth.op-proxy-sync', 'failed', { host, reason });
81
+ out(`op 프록시 토큰 동기화 실패(${host}): ${reason} — 직접: ${manual}`);
82
+ return false;
83
+ }