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
@@ -0,0 +1,128 @@
1
+ // EN7 — `elanous import --apply` / `--undo`: run the EN4 plan. Skills register in place (EN9 `withConnectedSource`, `ref` default)
2
+ // or as link/copy under the import folder; MCP joins `mcp.servers[]` switched off (EN8 `convertMcpEntry`); hooks, logins,
3
+ // secrets and chat-bot tokens are only listed — never copied, never printed. Undo restores the config bytes and removes what apply made.
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { convertMcpEntry } from '../skills/connect-claude-plugin.js';
7
+ import { withConnectedSource } from '../skills/connect.js';
8
+ import type { SkillSource } from '../user-config.js';
9
+ import type { ImportItem, ImportPlan } from './plan.js';
10
+
11
+ export type ImportMode = 'ref' | 'link' | 'copy';
12
+ export const IMPORT_MODES: readonly ImportMode[] = ['ref', 'link', 'copy'];
13
+
14
+ export interface ImportManifest {
15
+ at: string;
16
+ mode: ImportMode;
17
+ configPath: string;
18
+ /** Config bytes before apply; null when there was no config file. */
19
+ backup: string | null;
20
+ /** Files and links apply created, removed on undo. */
21
+ created: string[];
22
+ applied: Array<{ id: string; how: string }>;
23
+ kept: Array<{ id: string; status: string; why: string }>;
24
+ }
25
+
26
+ export interface ApplyDeps {
27
+ home: string;
28
+ configPath: string;
29
+ /** Where links/copies, the manifest and the config backup live. */
30
+ importRoot: string;
31
+ sources: readonly SkillSource[];
32
+ rawMcpServers: ReadonlyArray<Record<string, unknown>>;
33
+ /** Persist the new skills sources and raw MCP server rows (the config writer). */
34
+ save: (sources: SkillSource[], rawServers: Array<Record<string, unknown>>) => void;
35
+ now?: () => Date;
36
+ }
37
+
38
+ const manifestPath = (importRoot: string) => join(importRoot, 'last-apply.json');
39
+ const expand = (path: string, home: string) => (path.startsWith('~') ? join(home, path.slice(1)) : path);
40
+
41
+ function readMcpSpec(item: ImportItem, home: string): unknown {
42
+ try {
43
+ const json = JSON.parse(readFileSync(expand(item.path, home), 'utf8')) as Record<string, unknown>;
44
+ const table = item.source === 'claude' ? json.mcpServers : (json.mcp as Record<string, unknown> | undefined)?.servers;
45
+ return (table as Record<string, unknown> | undefined)?.[item.name];
46
+ } catch { return undefined; }
47
+ }
48
+
49
+ export function readImportManifest(importRoot: string): ImportManifest | null {
50
+ try { return JSON.parse(readFileSync(manifestPath(importRoot), 'utf8')) as ImportManifest; } catch { return null; }
51
+ }
52
+
53
+ export function applyImport(plan: ImportPlan, opts: { mode?: ImportMode; pick?: readonly string[] }, deps: ApplyDeps): ImportManifest {
54
+ if (readImportManifest(deps.importRoot)) throw new Error('이미 적용한 가져오기가 있습니다 — 먼저 `elanous import --undo`');
55
+ const mode = opts.mode ?? 'ref';
56
+ const picked = opts.pick?.length ? new Set(opts.pick) : null;
57
+ const manifest: ImportManifest = {
58
+ at: (deps.now ?? (() => new Date()))().toISOString(), mode, configPath: deps.configPath,
59
+ backup: existsSync(deps.configPath) ? readFileSync(deps.configPath, 'utf8') : null, created: [], applied: [], kept: [],
60
+ };
61
+ let sources = [...deps.sources];
62
+ const rawServers = [...deps.rawMcpServers];
63
+ const keep = (item: ImportItem, why: string) => manifest.kept.push({ id: item.id, status: item.status, why });
64
+ mkdirSync(deps.importRoot, { recursive: true });
65
+
66
+ for (const item of plan.items) {
67
+ if (picked && !picked.has(item.id)) continue;
68
+ if (item.status === 'same') continue;
69
+ if (item.status !== 'new') {
70
+ keep(item, item.status === 'secret' || item.status === 'archived-only'
71
+ ? `보관만 — 옮기지도 켜지도 않는다${item.next ? ` · 따로: \`${item.next}\`` : ''}`
72
+ : item.status === 'conflict' ? '같은 이름 · 다른 내용 — 그대로 둔다' : '형식을 아직 옮기지 않는다');
73
+ continue;
74
+ }
75
+ if (item.kind === 'skill') {
76
+ const skillDir = expand(item.path, deps.home);
77
+ if (mode === 'ref') {
78
+ const root = join(skillDir, '..');
79
+ sources = withConnectedSource(sources, root, item.source);
80
+ manifest.applied.push({ id: item.id, how: `ref ${item.path}` });
81
+ continue;
82
+ }
83
+ const target = join(deps.importRoot, 'skills', item.source, item.name);
84
+ if (existsSync(target)) { keep(item, `이미 있다: ${target}`); continue; }
85
+ mkdirSync(join(target, '..'), { recursive: true });
86
+ if (mode === 'link') symlinkSync(skillDir, target, 'dir');
87
+ else cpSync(skillDir, target, { recursive: true, dereference: true });
88
+ manifest.created.push(target);
89
+ sources = withConnectedSource(sources, join(deps.importRoot, 'skills', item.source), `import:${item.source}`);
90
+ manifest.applied.push({ id: item.id, how: `${mode} ${item.path}` });
91
+ continue;
92
+ }
93
+ if (item.kind === 'mcp' && (item.source === 'claude' || item.source === 'openclaw')) {
94
+ const converted = convertMcpEntry(item.source, item.name, readMcpSpec(item, deps.home), '');
95
+ if ('skipped' in converted) { keep(item, converted.skipped); continue; }
96
+ if (rawServers.some((row) => row.id === item.name)) { keep(item, '같은 id 가 이미 있다'); continue; }
97
+ rawServers.push({ ...converted, id: item.name });
98
+ manifest.applied.push({ id: item.id, how: 'mcp 꺼진 채로' });
99
+ continue;
100
+ }
101
+ keep(item, '이 판에서는 옮기지 않는다');
102
+ }
103
+
104
+ if (manifest.applied.length) deps.save(sources, rawServers);
105
+ writeFileSync(manifestPath(deps.importRoot), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
106
+ return manifest;
107
+ }
108
+
109
+ /** Restore the config bytes and remove what apply created. Returns null when nothing was applied. */
110
+ export function undoImport(importRoot: string): ImportManifest | null {
111
+ const manifest = readImportManifest(importRoot);
112
+ if (!manifest) return null;
113
+ if (manifest.backup === null) rmSync(manifest.configPath, { force: true });
114
+ else writeFileSync(manifest.configPath, manifest.backup);
115
+ for (const path of manifest.created) rmSync(path, { recursive: true, force: true });
116
+ rmSync(manifestPath(importRoot), { force: true });
117
+ return manifest;
118
+ }
119
+
120
+ export function formatApplied(m: ImportManifest): string {
121
+ const lines = [`가져왔습니다(${m.mode}) — ${m.applied.length}개 · 그대로 둔 것 ${m.kept.length}개. 되돌리려면: elanous import --undo`];
122
+ for (const a of m.applied.slice(0, 12)) lines.push(` + ${a.id} — ${a.how}`);
123
+ if (m.applied.length > 12) lines.push(` … 외 ${m.applied.length - 12}개`);
124
+ for (const k of m.kept.slice(0, 12)) lines.push(` · ${k.id} [${k.status}] — ${k.why}`);
125
+ if (m.kept.length > 12) lines.push(` … 외 ${m.kept.length - 12}개`);
126
+ if (m.applied.some((a) => a.how.startsWith('mcp'))) lines.push('MCP 서버는 꺼진 채로 넣었습니다. 믿을 수 있는 것만 켜 주세요(켠 뒤 데몬 재시작).');
127
+ return lines.join('\n');
128
+ }
@@ -0,0 +1,197 @@
1
+ // EN4 — `elanous import`: detect → plan on one screen, writes nothing (RESEARCH-entrances-for-codex-claude-openclaw-hermes-users §5a·§5b).
2
+ // Each item names the existing sub-command that applies it (connect · nexus channel-bot import); `--apply` is EN7 (apply.ts).
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { findSkillFolders } from '../skills/connect.js';
7
+ import { detectChannelImports, SOURCE_LABEL } from '../channel-import/detect.js';
8
+
9
+ export type ImportSourceId = 'agents' | 'claude' | 'codex' | 'openclaw' | 'hermes';
10
+ export type ImportItemKind = 'skill' | 'instructions' | 'mcp' | 'hooks' | 'credentials' | 'channel-bot';
11
+ /** `same` already reachable/identical · `conflict` same name, different content · `archived-only` kept, never run · `secret` moved only on an explicit ask. */
12
+ export type ImportItemStatus = 'new' | 'same' | 'conflict' | 'unsupported' | 'archived-only' | 'secret';
13
+
14
+ export interface ImportSourceInfo { id: ImportSourceId; root: string; detected: boolean }
15
+ export interface ImportItem {
16
+ /** `<source>:<kind>:<name>` — what `import --apply --pick` takes. */
17
+ id: string;
18
+ source: ImportSourceId;
19
+ kind: ImportItemKind;
20
+ name: string;
21
+ /** `~`-relative path the item was found in. */
22
+ path: string;
23
+ status: ImportItemStatus;
24
+ /** The existing command that applies this item; absent when nothing applies it yet. */
25
+ next?: string;
26
+ note?: string;
27
+ }
28
+ export interface ImportPlan { sources: ImportSourceInfo[]; items: ImportItem[] }
29
+
30
+ export interface ImportPlanInput {
31
+ home: string;
32
+ /** `$CODEX_HOME` when set. */
33
+ codexHome?: string;
34
+ /** Skill directories elanous reads today (defaultSkillDirs). */
35
+ activeSkillDirs: readonly string[];
36
+ /** Ids of MCP servers already in elanous config. */
37
+ mcpServerIds: readonly string[];
38
+ }
39
+
40
+ export const IMPORT_STATUSES: readonly ImportItemStatus[] = ['new', 'same', 'conflict', 'unsupported', 'archived-only', 'secret'];
41
+
42
+ function sourceRoots(input: ImportPlanInput): Record<ImportSourceId, string> {
43
+ const { home } = input;
44
+ const openclaw = [join(home, '.openclaw'), join(home, '.clawdbot'), join(home, '.moltbot')].find((p) => existsSync(p)) ?? join(home, '.openclaw');
45
+ return {
46
+ agents: join(home, '.agents'),
47
+ claude: join(home, '.claude'),
48
+ codex: input.codexHome || join(home, '.codex'),
49
+ openclaw,
50
+ hermes: join(home, '.hermes'),
51
+ };
52
+ }
53
+
54
+ const tilde = (path: string, home: string) => (path.startsWith(home) ? `~${path.slice(home.length)}` : path);
55
+ const hash = (path: string) => { try { return createHash('sha256').update(readFileSync(path)).digest('hex'); } catch { return null; } };
56
+ const readText = (path: string) => { try { return readFileSync(path, 'utf8'); } catch { return null; } };
57
+ const readJson = (path: string): Record<string, unknown> | null => {
58
+ const raw = readText(path);
59
+ if (raw === null) return null;
60
+ try { const v = JSON.parse(raw) as unknown; return v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : null; } catch { return null; }
61
+ };
62
+ const keysOf = (value: unknown): string[] => (value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value as object) : []);
63
+ const hasSecretFields = (spec: unknown): boolean => {
64
+ if (!spec || typeof spec !== 'object') return false;
65
+ const s = spec as Record<string, unknown>;
66
+ return keysOf(s.env).length > 0 || keysOf(s.headers).length > 0 || keysOf(s.http_headers).length > 0;
67
+ };
68
+
69
+ /** Skill folders of one root, each judged against what elanous already reads. */
70
+ function skillItems(source: ImportSourceId, root: string, input: ImportPlanInput, next?: string): Array<Omit<ImportItem, 'id'>> {
71
+ const names = findSkillFolders(root);
72
+ const rootActive = input.activeSkillDirs.includes(root);
73
+ return names.map((name): Omit<ImportItem, 'id'> => {
74
+ const path = tilde(join(root, name), input.home);
75
+ if (rootActive) return { source, kind: 'skill', name, path, status: 'same', note: '이미 읽는 폴더' };
76
+ const mine = hash(join(root, name, 'SKILL.md'));
77
+ const twin = input.activeSkillDirs.map((dir) => join(dir, name, 'SKILL.md')).find((p) => existsSync(p));
78
+ if (twin) {
79
+ return hash(twin) === mine
80
+ ? { source, kind: 'skill', name, path, status: 'same', note: `같은 내용이 이미 있다: ${tilde(twin, input.home)}` }
81
+ : { source, kind: 'skill', name, path, status: 'conflict', note: `같은 이름 · 다른 내용: ${tilde(twin, input.home)}` };
82
+ }
83
+ return { source, kind: 'skill', name, path, status: 'new', ...(next ? { next } : { note: '적용 명령은 0.2.9 `import --apply`' }) };
84
+ });
85
+ }
86
+
87
+ function mcpItem(source: ImportSourceId, name: string, path: string, secret: boolean, input: ImportPlanInput): Omit<ImportItem, 'id'> {
88
+ if (secret) return { source, kind: 'mcp', name, path, status: 'secret', note: 'env·헤더 값이 있다 — 묻기 전까지 옮기지 않는다' };
89
+ if (input.mcpServerIds.includes(name)) return { source, kind: 'mcp', name, path, status: 'same', note: '같은 id 가 이미 있다' };
90
+ return { source, kind: 'mcp', name, path, status: 'new', note: '0.2.9 `import --apply` 가 꺼진 채로 넣는다' };
91
+ }
92
+
93
+ /** `[mcp_servers.<name>]` tables of a Codex config.toml; `env`/`headers` keys or sub-tables count as secret. */
94
+ function codexMcp(raw: string): Array<{ name: string; secret: boolean }> {
95
+ const out = new Map<string, boolean>();
96
+ let current: string | null = null;
97
+ for (const line of raw.split(/\r?\n/)) {
98
+ const table = /^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/.exec(line)?.[1]?.trim();
99
+ if (table !== undefined) {
100
+ const m = /^mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.([A-Za-z_]+))?$/.exec(table);
101
+ current = m ? (m[1] ?? m[2]!) : null;
102
+ if (current) out.set(current, (out.get(current) ?? false) || ['env', 'headers', 'http_headers'].includes(m![3] ?? ''));
103
+ continue;
104
+ }
105
+ if (current && /^\s*(env|headers|http_headers)\s*=/.test(line)) out.set(current, true);
106
+ }
107
+ return [...out].map(([name, secret]) => ({ name, secret }));
108
+ }
109
+
110
+ /** Top-level keys under `mcp_servers:` in a Hermes config.yaml (two-space indent). */
111
+ function hermesMcp(raw: string): string[] {
112
+ const block = /^mcp_servers:\s*\n((?:[ \t]+.*\n?|\s*\n)*)/m.exec(raw)?.[1] ?? '';
113
+ return [...block.matchAll(/^ {2}([A-Za-z0-9_.-]+):/gm)].map((m) => m[1]!);
114
+ }
115
+
116
+ export function planImport(input: ImportPlanInput): ImportPlan {
117
+ const roots = sourceRoots(input);
118
+ const sources = (Object.keys(roots) as ImportSourceId[]).map((id) => ({ id, root: tilde(roots[id], input.home), detected: existsSync(roots[id]) }));
119
+ const items: Array<Omit<ImportItem, 'id'>> = [];
120
+ const home = input.home;
121
+ const at = (p: string) => tilde(p, home);
122
+
123
+ if (existsSync(roots.agents)) {
124
+ items.push(...skillItems('agents', join(roots.agents, 'skills'), input));
125
+ if (existsSync(join(roots.agents, 'AGENTS.md'))) items.push({ source: 'agents', kind: 'instructions', name: 'AGENTS.md', path: at(join(roots.agents, 'AGENTS.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
126
+ }
127
+
128
+ if (existsSync(roots.claude)) {
129
+ items.push(...skillItems('claude', join(roots.claude, 'skills'), input, 'elanous connect claude'));
130
+ if (existsSync(join(roots.claude, 'CLAUDE.md'))) items.push({ source: 'claude', kind: 'instructions', name: 'CLAUDE.md', path: at(join(roots.claude, 'CLAUDE.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
131
+ const claudeJson = join(home, '.claude.json');
132
+ for (const [name, spec] of Object.entries((readJson(claudeJson)?.mcpServers ?? {}) as Record<string, unknown>)) items.push(mcpItem('claude', name, at(claudeJson), hasSecretFields(spec), input));
133
+ const settings = join(roots.claude, 'settings.json');
134
+ for (const event of keysOf(readJson(settings)?.hooks)) items.push({ source: 'claude', kind: 'hooks', name: event, path: at(settings), status: 'archived-only', note: '훅은 보관만 · 켜지 않는다' });
135
+ if (existsSync(join(roots.claude, '.credentials.json'))) items.push({ source: 'claude', kind: 'credentials', name: 'login', path: at(join(roots.claude, '.credentials.json')), status: 'secret', note: '로그인은 옮기지 않는다 · 엘라누스에서 따로 한 번' });
136
+ }
137
+
138
+ if (existsSync(roots.codex)) {
139
+ items.push(...skillItems('codex', join(roots.codex, 'skills'), input, 'elanous connect codex'));
140
+ if (existsSync(join(roots.codex, 'AGENTS.md'))) items.push({ source: 'codex', kind: 'instructions', name: 'AGENTS.md', path: at(join(roots.codex, 'AGENTS.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
141
+ const toml = join(roots.codex, 'config.toml');
142
+ const raw = readText(toml);
143
+ for (const server of raw ? codexMcp(raw) : []) items.push(mcpItem('codex', server.name, at(toml), server.secret, input));
144
+ if (existsSync(join(roots.codex, 'auth.json'))) items.push({ source: 'codex', kind: 'credentials', name: 'login', path: at(join(roots.codex, 'auth.json')), status: 'secret', note: '로그인은 옮기지 않는다 · 엘라누스에서 따로 한 번' });
145
+ }
146
+
147
+ if (existsSync(roots.openclaw)) {
148
+ items.push(...skillItems('openclaw', join(roots.openclaw, 'skills'), input));
149
+ const cfg = join(roots.openclaw, 'openclaw.json');
150
+ const servers = (readJson(cfg)?.mcp as Record<string, unknown> | undefined)?.servers;
151
+ for (const [name, spec] of Object.entries((servers && typeof servers === 'object' ? servers : {}) as Record<string, unknown>)) items.push(mcpItem('openclaw', name, at(cfg), hasSecretFields(spec), input));
152
+ }
153
+
154
+ if (existsSync(roots.hermes)) {
155
+ items.push(...skillItems('hermes', join(roots.hermes, 'skills'), input));
156
+ const yaml = join(roots.hermes, 'config.yaml');
157
+ const raw = readText(yaml);
158
+ for (const name of raw ? hermesMcp(raw) : []) items.push({ source: 'hermes', kind: 'mcp', name, path: at(yaml), status: 'unsupported', note: 'YAML 형식은 아직 변환하지 않는다' });
159
+ }
160
+
161
+ // EN13 — chat bots: token values stay masked; the existing import command stores them after the user agrees.
162
+ const bots = detectChannelImports(home);
163
+ for (const c of bots.candidates) {
164
+ const source: ImportSourceId | null = c.source === 'openclaw' ? 'openclaw' : c.source === 'hermes' ? 'hermes' : null;
165
+ if (!source) continue;
166
+ items.push({ source, kind: 'channel-bot', name: c.platform, path: c.file, status: 'secret', next: 'elanous nexus channel-bot import', note: `${SOURCE_LABEL[c.source]} 봇 토큰(가림)` });
167
+ }
168
+ for (const u of bots.unreadable) {
169
+ const source: ImportSourceId | null = u.file.includes('.hermes') ? 'hermes' : u.file.includes('.openclaw') || u.file.includes('.clawdbot') || u.file.includes('.moltbot') ? 'openclaw' : null;
170
+ if (source) items.push({ source, kind: 'channel-bot', name: 'bot', path: u.file, status: 'unsupported', note: u.reason });
171
+ }
172
+ return { sources, items: items.map((item) => ({ ...item, id: `${item.source}:${item.kind}:${item.name}` })) };
173
+ }
174
+
175
+ const KIND_LABEL: Record<ImportItemKind, string> = { skill: '스킬', instructions: '지시 파일', mcp: 'MCP', hooks: '훅', credentials: '로그인', 'channel-bot': '채팅 봇' };
176
+
177
+ /** One screen: per source, counts by status, then the items that need a decision. */
178
+ export function formatImportPlan(plan: ImportPlan): string {
179
+ const lines: string[] = ['가져오기 계획 — 아무것도 쓰지 않았습니다.', ''];
180
+ const detected = plan.sources.filter((s) => s.detected);
181
+ if (!detected.length) return [...lines, '쓰던 에이전트를 찾지 못했습니다(~/.agents · ~/.claude · ~/.codex · ~/.openclaw · ~/.hermes).'].join('\n');
182
+ for (const source of detected) {
183
+ const mine = plan.items.filter((i) => i.source === source.id);
184
+ const counts = IMPORT_STATUSES.map((s) => [s, mine.filter((i) => i.status === s).length] as const).filter(([, n]) => n > 0);
185
+ lines.push(`■ ${source.id} (${source.root}) — ${mine.length ? counts.map(([s, n]) => `${s} ${n}`).join(' · ') : '항목 없음'}`);
186
+ for (const kind of Object.keys(KIND_LABEL) as ImportItemKind[]) {
187
+ const group = mine.filter((i) => i.kind === kind && i.status !== 'same');
188
+ if (!group.length) continue;
189
+ const shown = group.slice(0, 6).map((i) => `${i.name}[${i.status}]`).join(', ');
190
+ lines.push(` ${KIND_LABEL[kind]}: ${shown}${group.length > 6 ? ` 외 ${group.length - 6}개` : ''}`);
191
+ }
192
+ }
193
+ const nexts = [...new Set(plan.items.filter((i) => i.next && i.status !== 'same').map((i) => i.next!))];
194
+ lines.push('', '한 번에: `elanous import --apply` (스킬은 그 자리 등록 · MCP 는 꺼진 채로 · 비밀·훅은 보관만 · 되돌리기 `--undo`)');
195
+ lines.push(nexts.length ? `따로: ${nexts.map((n) => `\`${n}\``).join(' · ')}` : '따로 적용할 명령이 있는 항목은 없습니다.', '상태: new · same · conflict · unsupported · archived-only · secret — 자세히는 `elanous import --json`');
196
+ return lines.join('\n');
197
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,9 @@ import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
9
9
  import { registerDocsCommands } from './cli/docs-cli.js';
10
10
  import { registerA2ACommands } from './cli/a2a-cli.js';
11
11
  import { registerPluginCommands } from './cli/plugin-cli.js';
12
+ import { registerConnectCommand, registerSkillsCommands } from './cli/skills-cli.js';
13
+ import { registerImportCommand } from './cli/import-cli.js';
14
+ import { registerPersonaCommands } from './cli/persona-cli.js';
12
15
  import { registerFleetCommands } from './cli/fleet-cli.js';
13
16
  import { readPipedStdin } from './cli/piped-stdin.js';
14
17
  import { writeStdoutJson } from './cli/stdout-json.js';
@@ -885,6 +888,10 @@ registerCardCommand(program);
885
888
  registerLaunchHeadCommands(program);
886
889
  registerA2ACommands(program);
887
890
  registerPluginCommands(program);
891
+ registerSkillsCommands(program);
892
+ registerConnectCommand(program);
893
+ registerImportCommand(program);
894
+ registerPersonaCommands(program);
888
895
 
889
896
  const marketCmd = program.command('market').description('Signed plugin marketplace');
890
897
  marketCmd.command('publish').description('Publish a local signed marketplace index and archives')
@@ -4177,6 +4184,9 @@ const selfOrchestrateCmd = selfCmd
4177
4184
  });
4178
4185
  process.env[HARNESS_RUN_ID_ENV] = runId;
4179
4186
  reportRunId = runId;
4187
+ // HS1 — `harness stop <runId>` must find this process from the moment the run ID exists.
4188
+ try { (await import('./self-dev/orchestrate.js')).writeRunPidRecord(runId); }
4189
+ catch (error) { debug.log('self-dev.orchestrate', 'early-pid-failed', { runId, error: String(error) }, { level: 'warn' }); }
4180
4190
  const createdAt = prior?.createdAt ?? Date.now();
4181
4191
  const { bindOrchestrateRunLedger } = await import('./self-dev/self-orchestrate-runtime.js');
4182
4192
  const { checkpoint } = bindOrchestrateRunLedger({
@@ -4302,7 +4312,26 @@ const selfOrchestrateCmd = selfCmd
4302
4312
  grokApiKeyOptIn = getUserConfig().harness?.pod?.grokApiKeyOptIn === true;
4303
4313
  const credential = resolveGrokCredential();
4304
4314
  const { resolveCodexQuotaPolicy, codexPolicyAllowsCredits, codexPolicyAllowsFallback } = await import('./oauth/codex-quota-policy.js');
4305
- const quotaPolicy = resolveCodexQuotaPolicy(getUserConfig().llm).policy;
4315
+ const resolvedQuota = resolveCodexQuotaPolicy(getUserConfig().llm);
4316
+ const quotaPolicy = resolvedQuota.policy;
4317
+ // POL1 — say which policy this launch uses and where it came from; warn when a test universe disagrees with production.
4318
+ try {
4319
+ const { effectiveInstanceRoot, prodInstanceRoot } = await import('./instance/resolve.js');
4320
+ const { describeLaunchQuotaPolicy } = await import('./oauth/codex-quota-policy.js');
4321
+ const root = effectiveInstanceRoot();
4322
+ const kind = root === prodInstanceRoot() ? 'prod' as const : 'test' as const;
4323
+ let production: ReturnType<typeof resolveCodexQuotaPolicy> | undefined;
4324
+ if (kind === 'test') {
4325
+ try {
4326
+ const { readFileSync } = await import('node:fs');
4327
+ const prodLlm = (JSON.parse(readFileSync(`${prodInstanceRoot()}/config.json`, 'utf8')) as { llm?: Record<string, unknown> }).llm;
4328
+ production = resolveCodexQuotaPolicy(prodLlm);
4329
+ } catch { /* no production config on this machine */ }
4330
+ }
4331
+ const described = describeLaunchQuotaPolicy({ current: resolvedQuota, universe: { kind, root }, ...(production ? { production } : {}) });
4332
+ debug.log('self-implement.pod', 'quota-policy', { policy: resolvedQuota.policy, source: resolvedQuota.source, universe: kind, productionPolicy: production?.policy ?? null });
4333
+ if (!opts.json) { ui.info(described.line); if (described.warning) ui.warn(described.warning); }
4334
+ } catch { /* the line is advice; the launch goes on */ }
4306
4335
  const plan = planPodProvider({
4307
4336
  codexCandidates: inspectCodexRotation().candidates,
4308
4337
  // Per-account caps (대표 default:97) override the Pod default cap — without them default was dropped at 60%.
@@ -7258,16 +7287,31 @@ loginCmd
7258
7287
  .description('Sign in to OpenAI Codex via the ChatGPT device-code flow')
7259
7288
  .option('--replace-codex-cli-login', 'if the Codex CLI (~/.codex/auth.json) is signed in to a different account, replace it with this login')
7260
7289
  .action(async (opts: { replaceCodexCliLogin?: boolean }) => {
7261
- ui.header('OpenAI Codex — device-code sign-in');
7290
+ // OB5d — say why there is no browser (ssh · no display) and what to do instead; otherwise try the browser first.
7291
+ const { browserUnavailableReason } = await import('./oauth/browser-availability.js');
7292
+ const noBrowser = browserUnavailableReason();
7293
+ let browserState: Awaited<ReturnType<typeof import('./oauth/codex-browser-login.js')['loginWithCodexBrowser']>> | null = null;
7294
+ if (!noBrowser) {
7295
+ ui.header('OpenAI Codex — 브라우저 로그인');
7296
+ console.log(' 브라우저에서 로그인을 마치세요 — 끝나면 자동으로 이어집니다.');
7297
+ try {
7298
+ const { loginWithCodexBrowser } = await import('./oauth/codex-browser-login.js');
7299
+ browserState = await loginWithCodexBrowser();
7300
+ } catch {
7301
+ console.log(' 브라우저 로그인이 안 됐습니다 — 코드로 이어갑니다.');
7302
+ }
7303
+ }
7304
+ if (!browserState) ui.header('OpenAI Codex — 코드로 로그인');
7305
+ if (noBrowser) console.log(` ${noBrowser}`);
7262
7306
  try {
7263
- const state = await loginWithCodex({
7307
+ const state = browserState ?? await loginWithCodex({
7264
7308
  onProgress: (p) => {
7265
7309
  if (p.type === 'user_code' && p.userCode) {
7266
7310
  console.log('');
7267
- console.log(` 1) Open in any browser: ${p.loginUrl}`);
7268
- console.log(` 2) Enter this code: ${p.userCode}`);
7311
+ console.log(` 1) 아무 브라우저에서 열기: ${p.loginUrl}`);
7312
+ console.log(` 2) 코드 입력: ${p.userCode}`);
7269
7313
  console.log('');
7270
- console.log(' Waiting for sign-in… (Ctrl+C to cancel)');
7314
+ console.log(' 로그인을 기다리는 중… (취소: Ctrl+C)');
7271
7315
  }
7272
7316
  if (p.type === 'polling' && p.pollAttempt === 1) {
7273
7317
  process.stdout.write(' .');
@@ -9797,6 +9841,23 @@ nexusChannelBotCmd
9797
9841
  process.exit(result.exitCode);
9798
9842
  });
9799
9843
 
9844
+ // EN13 — bring an existing Telegram/Discord bot over from OpenClaw · Hermes · the previous elanous (~/.monad).
9845
+ nexusChannelBotCmd
9846
+ .command('import')
9847
+ .description('Find Telegram/Discord bot settings you already use (OpenClaw, Hermes, previous elanous), preview them with the token masked, and store them after you agree.')
9848
+ .option('--yes', 'Import without asking')
9849
+ .action(async (opts: { yes?: boolean }) => {
9850
+ const { runChannelImport } = await import('./channel-import/run.js');
9851
+ const { createInterface } = await import('node:readline/promises');
9852
+ const ask = process.stdin.isTTY ? async (question: string): Promise<boolean> => {
9853
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9854
+ try { const answer = (await rl.question(question)).trim(); return answer === '' || /^(y|yes|예|네)$/i.test(answer); }
9855
+ finally { rl.close(); }
9856
+ } : undefined;
9857
+ const result = await runChannelImport({ yes: opts.yes, ...(ask ? { ask } : {}) });
9858
+ process.exit(result.exitCode);
9859
+ });
9860
+
9800
9861
  // FU A6-real P5 (2026-05-11) — interactive Firecrawl setup.
9801
9862
  // Walks the user through CLI detection + API key entry + persist to
9802
9863
  // `registry.discovery.firecrawl.apiKey`. `--api-key <value>` skips
@@ -10053,10 +10114,14 @@ tokenCmd
10053
10114
  .description('Rotate the ACP admin token')
10054
10115
  .option('--grace-ms <ms>', 'Previous-token grace period in milliseconds', (value: string) => Number(value))
10055
10116
  .option('--show-token', 'Print the complete new token')
10056
- .action(async (opts: { graceMs?: number; showToken?: boolean }) => {
10117
+ .option('--sync-op-proxy', 'Update the configured op proxy bearer over SSH after rotation')
10118
+ .action(async (opts: { graceMs?: number; showToken?: boolean; syncOpProxy?: boolean }) => {
10057
10119
  if (opts.graceMs !== undefined && (!Number.isFinite(opts.graceMs) || opts.graceMs < 0)) {
10058
10120
  throw new Error('--grace-ms must be a non-negative finite number');
10059
10121
  }
10122
+ if (opts.syncOpProxy && opts.showToken) {
10123
+ throw new Error('--sync-op-proxy cannot be combined with --show-token');
10124
+ }
10060
10125
  const [{ rotateAdminToken }, { getElanousConfigDir }] = await Promise.all([
10061
10126
  import('./auth/token-store.js'),
10062
10127
  import('./elanous-config-dir.js'),
@@ -10067,9 +10132,17 @@ tokenCmd
10067
10132
  });
10068
10133
  if (opts.showToken) {
10069
10134
  console.log(result.newActive);
10070
- return;
10071
- }
10072
- console.log(`ACP admin token rotated: length=${result.newActive.length} prefix=${result.newActive.slice(0, 4)}`);
10135
+ } else {
10136
+ console.log(opts.syncOpProxy
10137
+ ? `ACP admin token rotated: length=${result.newActive.length}`
10138
+ : `ACP admin token rotated: length=${result.newActive.length} prefix=${result.newActive.slice(0, 4)}`);
10139
+ }
10140
+ const { readOpProxyConfig, finishOpProxyRotation } = await import('./cli/op-proxy-sync.js');
10141
+ finishOpProxyRotation(readOpProxyConfig(), result.newActive, {
10142
+ sync: opts.syncOpProxy === true,
10143
+ tokenFile: `${getElanousConfigDir()}/acp-token`,
10144
+ ...(opts.showToken ? { out: (line: string) => console.error(line) } : {}),
10145
+ });
10073
10146
  });
10074
10147
 
10075
10148
  const acpCmd = program.command('acp').description('Agent Client Protocol — spawn ACP agents (claude-code, codex, gemini)');
@@ -10461,6 +10534,14 @@ async function main(): Promise<void> {
10461
10534
  // elanous --acp-server --transport=unix-socket --socket-path=/tmp/x.sock
10462
10535
  // elanous --acp-server --transport=websocket --port=31415
10463
10536
  // elanous --acp-server --transport=websocket --no-auth # skip token check
10537
+ if (rawArgs.includes('--acp-server') && rawArgs.includes('--login')) {
10538
+ // EN10a — an ACP client's terminal auth re-launches `elanous --acp-server --login`: run the sign-in in that terminal and exit with its status.
10539
+ const { ACP_LOGIN_SUBCOMMAND, currentLaunch } = await import('./acp/auth-methods.js');
10540
+ const launch = currentLaunch();
10541
+ const { spawnSync } = await import('node:child_process');
10542
+ const run = spawnSync(launch.command, [...launch.args, ...ACP_LOGIN_SUBCOMMAND], { stdio: 'inherit' });
10543
+ process.exit(run.status ?? 1);
10544
+ }
10464
10545
  if (rawArgs.includes('--acp-server')) {
10465
10546
  const { bootAcpServer, parseAcpBootArgs } = await import('./boot/acp-server.js');
10466
10547
  const { createDaemonRuntime } = await import('./boot/daemon-runtime.js');
@@ -10751,12 +10832,18 @@ async function main(): Promise<void> {
10751
10832
  * 회귀 방어 = `src/cli-entry.test.ts`(실물 `bin/elanous.mjs` 를 spawn 한다).
10752
10833
  */
10753
10834
  export function runCli(): void {
10754
- main().catch((err) => {
10835
+ main().catch(async (err) => {
10755
10836
  closeTui();
10756
10837
  if (isCliUserError(err)) {
10757
10838
  console.error(formatCliUserError(err));
10758
10839
  process.exit(1);
10759
10840
  }
10841
+ // ER1 — a real crash is reported once (consented · scrubbed · never blocks more than the send timeout).
10842
+ try {
10843
+ const { reportError } = await import('./error-report/report.js');
10844
+ const outcome = await reportError({ code: 'cli-crash', message: err instanceof Error ? `${err.name}: ${err.message}` : String(err), ...(err instanceof Error && err.stack ? { stack: err.stack } : {}), surface: 'cli' });
10845
+ if (outcome.sent) console.error(`오류를 운영팀에 알렸습니다${outcome.reportId ? ` (보고 번호 ${outcome.reportId})` : ''}.`);
10846
+ } catch { /* the crash itself is what matters */ }
10760
10847
  throw err;
10761
10848
  });
10762
10849
  }
@@ -40,15 +40,23 @@ export function keyEventToTextInsertion(
40
40
  return preservePrintableText(ev, name);
41
41
  }
42
42
 
43
+ /** Hangul Compatibility Jamo (ㄱ…ㅣ · ㅋㅋ · ㅠㅠ). The key parser maps a lone jamo keystroke to the QWERTY key
44
+ * on the same position so hotkeys work under the Korean IME — but as TEXT it must stay the jamo the person typed
45
+ * (U1 · 2026-10-02: «ㅋㅋㅋ ㅠㅠ» came out as «zzz bb» in the chat input). */
46
+ const COMPAT_JAMO = /^[\u3131-\u318E]$/;
47
+
43
48
  function preservePrintableText(ev: KeyEvent, name: string): string {
49
+ // The chat composer passes the TUI key (original bytes on `raw`); other composers pass a KeyEvent (`sequence`).
50
+ const sequence = ev.sequence ?? (ev as { raw?: string }).raw;
51
+ if (sequence && name.length === 1 && COMPAT_JAMO.test(sequence)) return sequence;
44
52
  if (
45
- ev.sequence
46
- && ev.sequence.length === 1
53
+ sequence
54
+ && sequence.length === 1
47
55
  && name.length === 1
48
- && ev.sequence !== name
49
- && ev.sequence.toLowerCase() === name.toLowerCase()
56
+ && sequence !== name
57
+ && sequence.toLowerCase() === name.toLowerCase()
50
58
  ) {
51
- return ev.sequence;
59
+ return sequence;
52
60
  }
53
61
  if (name.length === 1) return ev.shift ? name.toUpperCase() : name;
54
62
  return name;
@@ -0,0 +1,96 @@
1
+ // EV12d — 계획기가 맞는 그래프를 못 고르면(«요청에 맞는 설치된 실행 그래프가 없습니다») «문서 요청 실패» 대신
2
+ // 그 자리가 직접 답한다: 역할 파일(내부 문서 `<id>`) ⊕ 그 자리가 owner 인 체크리스트 칸 ⊕ 최근 요청 몇 줄을
3
+ // 맥락으로 LLM 한 번 → 같은 채널 회신(본문). 맥락에 없는 것은 «모른다»고 답하게 한다(지어내지 않는다).
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { debug } from '../debug/log.js';
7
+ import { ExecRequestStore, type ExecRequest } from '../exec-requests/store.js';
8
+ import { devVersion, listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
9
+ import { resolveSeat } from '../seat-address/seat-address.js';
10
+
11
+ const ROLE_CHARS = 6_000;
12
+
13
+ /** Cut by code points, never inside a surrogate pair. A `.slice` through an emoji (🎉 · 🟢) leaves a lone surrogate and
14
+ * the model API rejects the whole request («Codex API 400: Bad Request» · 10-02 07:4x — every «@CMO …» question failed). */
15
+ function cut(text: string, max: number): string {
16
+ const chars = Array.from(text);
17
+ return chars.length > max ? chars.slice(0, max).join('') : text;
18
+ }
19
+ const ITEM_LIMIT = 25;
20
+ const RECENT_LIMIT = 5;
21
+
22
+ export interface SeatAnswerDeps {
23
+ readRole?: (seatId: string) => string | null;
24
+ ownedItems?: (seatId: string) => ChecklistItem[];
25
+ recentRequests?: () => ExecRequest[];
26
+ complete?: (prompt: string) => Promise<string>;
27
+ }
28
+
29
+ function defaultReadRole(seatId: string): string | null {
30
+ const path = fileURLToPath(new URL(`../../docs/roles/${seatId}.md`, import.meta.url));
31
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
32
+ }
33
+
34
+ function nextPatch(version: string): string | null {
35
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
36
+ return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : null;
37
+ }
38
+
39
+ /** Items this seat owns in the dev version's checklist and the next one (where 10-28 work lives). */
40
+ function defaultOwnedItems(seatId: string): ChecklistItem[] {
41
+ const versions = [devVersion().replace(/-.*$/, '')];
42
+ const next = nextPatch(versions[0]!);
43
+ if (next) versions.push(next);
44
+ const items: ChecklistItem[] = [];
45
+ for (const version of versions) {
46
+ try { items.push(...listChecklist(version).items.filter((item) => item.owner === seatId)); }
47
+ catch { /* a version without a checklist contributes nothing */ }
48
+ }
49
+ return items;
50
+ }
51
+
52
+ async function defaultComplete(prompt: string): Promise<string> {
53
+ const { streamLLM } = await import('../llm.js');
54
+ const { tierModel } = await import('../llm/model-defaults.js');
55
+ return streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('better') });
56
+ }
57
+
58
+ const STATUS_MARK: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴', done: '✅' };
59
+
60
+ export function seatAnswerPrompt(input: { title: string; seatId: string; question: string; role: string | null; items: ChecklistItem[]; recent: ExecRequest[] }): string {
61
+ const items = input.items.slice(0, ITEM_LIMIT)
62
+ .map((item) => `${STATUS_MARK[item.status] ?? item.status} ${item.id} ${cut(item.title, 140)}${item.evidence ? ` — 근거: ${cut(item.evidence, 160)}` : ''}`)
63
+ .join('\n') || '(담당 칸 없음)';
64
+ const recent = input.recent.slice(0, RECENT_LIMIT)
65
+ .map((request) => `${request.createdAt.slice(0, 16)} ${request.status} · ${cut(request.text.replace(/\s+/g, ' '), 120)}`)
66
+ .join('\n') || '(최근 요청 없음)';
67
+ return [
68
+ `너는 엘라누스 팀의 ${input.title}(${input.seatId}) 자리다. 아래 «역할 문서 · 담당 칸 · 최근 요청»만 근거로 질문에 답한다.`,
69
+ '규칙: 한국어 존댓말 · 12줄 이내 · 맥락에 없는 사실·수치·일정은 지어내지 말고 «모른다/확인 필요»라고 쓴다 · 담당 칸을 인용할 땐 칸 ID 를 붙인다 · 내부 파일 경로·토큰·계정은 쓰지 않는다.',
70
+ `\n## 역할 문서\n${cut(input.role ?? '(역할 문서 없음)', ROLE_CHARS)}`,
71
+ `\n## 담당 칸(체크리스트 owner=${input.seatId})\n${items}`,
72
+ `\n## 최근 자리 요청\n${recent}`,
73
+ `\n## 질문\n${input.question}`,
74
+ ].join('\n');
75
+ }
76
+
77
+ /** The seat's own answer, or null when the seat is unknown or the model gave nothing usable. */
78
+ export async function answerAsSeat(seatAddress: string, question: string, deps: SeatAnswerDeps = {}): Promise<{ title: string; text: string } | null> {
79
+ const seat = resolveSeat(seatAddress);
80
+ if (!seat?.title) return null;
81
+ const role = (deps.readRole ?? defaultReadRole)(seat.id);
82
+ const items = (deps.ownedItems ?? defaultOwnedItems)(seat.id);
83
+ const recent = (deps.recentRequests ?? (() => new ExecRequestStore().list()))();
84
+ const raw = await (deps.complete ?? defaultComplete)(seatAnswerPrompt({ title: seat.title, seatId: seat.id, question, role, items, recent }));
85
+ const text = raw.trim();
86
+ debug.log('intake.seat-doc', 'seat-answer', { seat: seat.id, roleFile: role !== null, items: items.length, recent: Math.min(recent.length, RECENT_LIMIT), chars: text.length });
87
+ return text ? { title: seat.title, text: cut(text, 3_500) } : null;
88
+ }
89
+
90
+ /** «No installed graph fits» — the planner's own failure wording, so the seat should answer instead. */
91
+ export function isNoGraphFailure(result: { status: string; summary?: string; seats?: Array<{ graphId: string; reason?: string }> } | null): boolean {
92
+ if (!result || result.status !== 'failed' || !result.seats?.length) return false;
93
+ if (!result.seats.every((seat) => !seat.graphId)) return false;
94
+ const NO_GRAPH = /설치된 실행 그래프가 없습니다/;
95
+ return NO_GRAPH.test(result.summary ?? '') || result.seats.some((seat) => NO_GRAPH.test(seat.reason ?? ''));
96
+ }