elanous 0.2.6 → 0.2.8

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 (517) hide show
  1. package/.env.example +10 -7
  2. package/README.md +4 -0
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
  96. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/index.html +1 -1
  123. package/apps/pwa/out/index.txt +21 -21
  124. package/apps/pwa/out/intake/index.html +1 -1
  125. package/apps/pwa/out/intake/index.txt +22 -22
  126. package/apps/pwa/out/live/index.html +1 -1
  127. package/apps/pwa/out/live/index.txt +22 -22
  128. package/apps/pwa/out/market/index.html +1 -1
  129. package/apps/pwa/out/market/index.txt +21 -21
  130. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  131. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  132. package/apps/pwa/out/morning/index.html +1 -1
  133. package/apps/pwa/out/morning/index.txt +22 -22
  134. package/apps/pwa/out/observatory/index.html +1 -1
  135. package/apps/pwa/out/observatory/index.txt +22 -22
  136. package/apps/pwa/out/reflection/index.html +1 -1
  137. package/apps/pwa/out/reflection/index.txt +22 -22
  138. package/apps/pwa/out/scheduler/index.html +1 -1
  139. package/apps/pwa/out/scheduler/index.txt +22 -22
  140. package/apps/pwa/out/sessions/index.html +1 -1
  141. package/apps/pwa/out/sessions/index.txt +21 -21
  142. package/apps/pwa/out/settings/devices/index.html +1 -1
  143. package/apps/pwa/out/settings/devices/index.txt +22 -22
  144. package/apps/pwa/out/settings/index.html +1 -1
  145. package/apps/pwa/out/settings/index.txt +22 -22
  146. package/apps/pwa/out/setup/done/index.html +1 -1
  147. package/apps/pwa/out/setup/done/index.txt +22 -22
  148. package/apps/pwa/out/setup/index.html +1 -1
  149. package/apps/pwa/out/setup/index.txt +22 -22
  150. package/apps/pwa/out/share/index.html +1 -1
  151. package/apps/pwa/out/share/index.txt +22 -22
  152. package/apps/pwa/out/showroom/index.html +1 -1
  153. package/apps/pwa/out/showroom/index.txt +21 -21
  154. package/apps/pwa/out/tasks/index.html +1 -1
  155. package/apps/pwa/out/tasks/index.txt +22 -22
  156. package/apps/pwa/out/term/index.html +1 -1
  157. package/apps/pwa/out/term/index.txt +22 -22
  158. package/apps/pwa/out/trace/index.html +1 -1
  159. package/apps/pwa/out/trace/index.txt +22 -22
  160. package/apps/pwa/out/vault/index.html +1 -1
  161. package/apps/pwa/out/vault/index.txt +22 -22
  162. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  163. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  164. package/apps/pwa/out/workflows/index.html +1 -1
  165. package/apps/pwa/out/workflows/index.txt +22 -22
  166. package/apps/pwa/out/workspace/index.html +1 -1
  167. package/apps/pwa/out/workspace/index.txt +22 -22
  168. package/apps/pwa/out/worktrees/index.html +1 -1
  169. package/apps/pwa/out/worktrees/index.txt +22 -22
  170. package/bin/elanous.cjs +107 -0
  171. package/graphs/field/field-feed.yaml +24 -0
  172. package/graphs/field/recipes.yaml +12 -0
  173. package/graphs/landing/landing-heal.yaml +3 -1
  174. package/graphs/landing/recipes.yaml +3 -0
  175. package/graphs/release/recipes.yaml +10 -0
  176. package/graphs/release/release-loop.yaml +10 -3
  177. package/graphs/test-diet/recipes.yaml +9 -0
  178. package/graphs/test-diet/test-diet.yaml +22 -0
  179. package/package.json +6 -5
  180. package/persona-presets/_index.yaml +10 -0
  181. package/persona-presets/_schema.json +146 -0
  182. package/persona-presets/dana.yaml +26 -0
  183. package/persona-presets/haru.yaml +27 -0
  184. package/persona-presets/kai.yaml +27 -0
  185. package/persona-presets/leo.yaml +27 -0
  186. package/persona-presets/mira.yaml +30 -0
  187. package/persona-presets/nari.yaml +27 -0
  188. package/persona-presets/sora.yaml +27 -0
  189. package/persona-presets/yuna.yaml +27 -0
  190. package/plugins/card-followup/README.md +13 -4
  191. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  192. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  193. package/plugins/card-followup/graphs/run-step.ts +122 -18
  194. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  195. package/plugins/card-followup/plugin.json +1 -1
  196. package/plugins/doc-draft/README.md +13 -0
  197. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  198. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  199. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  200. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  201. package/plugins/doc-draft/plugin.json +24 -0
  202. package/plugins/geo-check/README.md +3 -1
  203. package/plugins/geo-check/graphs/run-step.ts +58 -13
  204. package/plugins/geo-check/plugin.json +1 -1
  205. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  206. package/plugins/job-coach/README.md +1 -1
  207. package/plugins/job-coach/graphs/run-step.ts +2 -2
  208. package/plugins/job-coach/plugin.json +1 -1
  209. package/plugins/job-coach/plugin.ts +1 -1
  210. package/scripts/affinity/endcard.sh +3 -2
  211. package/scripts/affinity/reframe.sh +3 -1
  212. package/scripts/ci-public-export-leak-gate.ts +48 -0
  213. package/scripts/codex-quota-alert.ts +122 -258
  214. package/scripts/coord-tracks.json +4 -4
  215. package/scripts/field-feed/deliver-node.ts +57 -0
  216. package/scripts/field-feed/feed-node.ts +67 -0
  217. package/scripts/field-feed/lib.ts +164 -0
  218. package/scripts/field-feed/reel-node.ts +24 -0
  219. package/scripts/field-feed/start.ts +62 -0
  220. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  221. package/scripts/install.sh +109 -21
  222. package/scripts/landing-heal/clean-checkout.ts +114 -0
  223. package/scripts/landing-heal/run-mission.ts +38 -3
  224. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  225. package/scripts/persona-presets/lint.ts +79 -0
  226. package/scripts/public-export.ts +8 -4
  227. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  228. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  229. package/scripts/release-loop/env-known-failures.ts +31 -0
  230. package/scripts/release-loop/gate-node.ts +108 -22
  231. package/scripts/release-loop/known-issues-node.ts +159 -0
  232. package/scripts/release-loop/npm-publish-node.ts +124 -0
  233. package/scripts/release-loop/ops-upgrade-node.ts +95 -0
  234. package/scripts/release-loop/publish-node.ts +23 -1
  235. package/scripts/release-loop/release-readiness.ts +58 -0
  236. package/scripts/release-loop/shard-plan.ts +17 -0
  237. package/scripts/release-loop/unattended-release.ts +93 -0
  238. package/scripts/storyboard/storyboard.ts +167 -0
  239. package/scripts/test-diet/lib.ts +88 -0
  240. package/scripts/test-diet/node.ts +102 -0
  241. package/scripts/ux/measure-first-text.ts +102 -0
  242. package/scripts/vflow-collect.ts +20 -7
  243. package/scripts/vflow-search.ts +73 -0
  244. package/scripts/video-free-line-test-walker.ts +36 -0
  245. package/scripts/video-pipeline.ts +2 -2
  246. package/skills/explainer-video/SKILL.md +12 -9
  247. package/skills/explainer-video/engine/build.mjs +1 -1
  248. package/skills/explainer-video/engine/reel.mjs +138 -31
  249. package/skills/explainer-video/engine/reel.sh +2 -1
  250. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  251. package/skills/grill-me/SKILL.md +1 -1
  252. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  253. package/src/a2a/server.ts +2 -0
  254. package/src/acp/auth-methods.ts +30 -0
  255. package/src/acp/elanous-aware-peers.ts +30 -0
  256. package/src/acp/server.ts +96 -20
  257. package/src/agent/monad-agent-turn.ts +25 -6
  258. package/src/agent/shared-app-tools.ts +5 -3
  259. package/src/agent-substrate/review-intent.ts +6 -2
  260. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  261. package/src/auth/github-app-token.ts +4 -1
  262. package/src/autopilot/track-agent-shadow.ts +151 -0
  263. package/src/autopilot/track-agent.ts +23 -3
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/channel-import/detect.ts +191 -0
  271. package/src/channel-import/run.ts +111 -0
  272. package/src/chat/chat-cli.ts +4 -1
  273. package/src/chat/short-question.ts +40 -0
  274. package/src/cli/agent-cli.ts +24 -15
  275. package/src/cli/ask-bare.ts +42 -0
  276. package/src/cli/cli-user-error.ts +25 -0
  277. package/src/cli/codex-credit-plan-view.ts +25 -0
  278. package/src/cli/config-test-sync.ts +4 -0
  279. package/src/cli/decisions-cli.ts +13 -3
  280. package/src/cli/dependence-gauge.ts +237 -0
  281. package/src/cli/doctor-bundle.ts +128 -0
  282. package/src/cli/doctor-cli.ts +41 -1
  283. package/src/cli/doctor-local-llm.ts +95 -0
  284. package/src/cli/doctor-modes.ts +48 -0
  285. package/src/cli/import-cli.ts +60 -0
  286. package/src/cli/intake-cli.ts +12 -2
  287. package/src/cli/intake-source-cli.ts +58 -0
  288. package/src/cli/machine-cli.ts +23 -0
  289. package/src/cli/mcp-login.ts +13 -4
  290. package/src/cli/msg-cli.ts +137 -0
  291. package/src/cli/persona-cli.ts +116 -0
  292. package/src/cli/plugin-cli.ts +25 -0
  293. package/src/cli/pr-cli.ts +36 -0
  294. package/src/cli/provider-cli.ts +5 -0
  295. package/src/cli/release-cli.ts +50 -8
  296. package/src/cli/release-run-if-ready.ts +48 -0
  297. package/src/cli/setup-cli.ts +27 -6
  298. package/src/cli/setup-recommend.ts +8 -0
  299. package/src/cli/skills-cli.ts +173 -0
  300. package/src/cli/start-cli.ts +13 -4
  301. package/src/cli/usage-cli.ts +11 -1
  302. package/src/codex/setup.ts +37 -16
  303. package/src/dashboard/index.ts +18 -2
  304. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
  305. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  306. package/src/decisions/decision-cards.ts +291 -0
  307. package/src/decisions/decision-ledger.ts +5 -2
  308. package/src/decisions/discord-decision-cards.ts +133 -0
  309. package/src/decisions/telegram-decision-cards.ts +100 -0
  310. package/src/directives/version-at.ts +77 -0
  311. package/src/discord.ts +73 -6
  312. package/src/domains/core-tools.ts +37 -14
  313. package/src/domains/outbound-alert.ts +94 -12
  314. package/src/domains/repeated-failure.ts +1 -1
  315. package/src/error-report/report.ts +200 -0
  316. package/src/exec-requests/planner.ts +146 -0
  317. package/src/exec-requests/runner.ts +378 -0
  318. package/src/exec-requests/store.ts +78 -0
  319. package/src/field/field-feed-auto.ts +48 -0
  320. package/src/field/field-media.ts +8 -3
  321. package/src/field/field-reel.ts +203 -0
  322. package/src/grok/credential.ts +7 -5
  323. package/src/harness/aside-run-status.ts +33 -0
  324. package/src/harness/browser-verify.ts +48 -17
  325. package/src/harness/harness-cli-command.ts +25 -3
  326. package/src/harness/image-load-state.ts +21 -0
  327. package/src/harness/process-garbage.ts +45 -0
  328. package/src/hooks/error-report.ts +210 -0
  329. package/src/hooks/receiver.ts +41 -1
  330. package/src/import/apply.ts +128 -0
  331. package/src/import/plan.ts +197 -0
  332. package/src/index.ts +168 -21
  333. package/src/input-core/text-entry.ts +13 -5
  334. package/src/instance/leader.ts +13 -1
  335. package/src/intake-plane/collect-github.ts +5 -3
  336. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  337. package/src/intake-plane/discord-seat-work.ts +56 -0
  338. package/src/intake-plane/intake-sources.ts +202 -0
  339. package/src/intake-plane/items.ts +28 -12
  340. package/src/intake-plane/seat-answer.ts +96 -0
  341. package/src/intake-plane/seat-doc-route.ts +221 -0
  342. package/src/intake-plane/submit-intake-work.ts +9 -1
  343. package/src/intake-plane/telegram-seat-work.ts +72 -0
  344. package/src/llm/model-defaults.ts +24 -1
  345. package/src/llm.ts +5 -4
  346. package/src/machines/machine-ledger.ts +127 -0
  347. package/src/market/pack-internal-refs.ts +28 -0
  348. package/src/market/publish.ts +31 -4
  349. package/src/maturity/feature-maturity.ts +65 -0
  350. package/src/model-tier/turn-tier.ts +24 -0
  351. package/src/msg/msg-store.ts +149 -0
  352. package/src/native-tool-catalog.ts +2 -1
  353. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  354. package/src/nexus/api/consult-requests.ts +128 -0
  355. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  356. package/src/nexus/api/error-reports.ts +108 -0
  357. package/src/nexus/api/exec-requests.ts +51 -0
  358. package/src/nexus/api/field-uploads.ts +40 -3
  359. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  360. package/src/nexus/api/graph-approvals.ts +71 -4
  361. package/src/nexus/api/http-server.ts +82 -8
  362. package/src/nexus/api/outbound-report.ts +7 -0
  363. package/src/nexus/api/running-runs-background.ts +68 -0
  364. package/src/nexus/api/running-runs-child.ts +6 -0
  365. package/src/nexus/api/seat-requests.ts +254 -0
  366. package/src/nexus/api/share-captures.ts +91 -0
  367. package/src/nexus/api/skill-problems.ts +56 -0
  368. package/src/nexus/api/terminals.ts +24 -8
  369. package/src/nexus/api/trace-run-scan.ts +37 -0
  370. package/src/nexus/api/trace.ts +19 -3
  371. package/src/nexus/index.ts +28 -1
  372. package/src/nexus/setup-status.ts +11 -7
  373. package/src/notify/llm-budget-alert.ts +16 -10
  374. package/src/oauth/browser-availability.ts +14 -0
  375. package/src/oauth/codex-account-rotation.ts +26 -4
  376. package/src/oauth/codex-account-store.ts +20 -2
  377. package/src/oauth/codex-browser-login.ts +173 -0
  378. package/src/oauth/codex-quota-policy.ts +16 -0
  379. package/src/oauth/codex.ts +4 -1
  380. package/src/oauth/fallback-chain.ts +5 -5
  381. package/src/oauth/store.ts +42 -5
  382. package/src/onboarding/continue-here.ts +34 -0
  383. package/src/onboarding/entry-hints.ts +8 -0
  384. package/src/onboarding/full-screen-io.ts +35 -3
  385. package/src/onboarding/help/skills.md +1 -1
  386. package/src/onboarding/non-interactive.ts +1 -0
  387. package/src/onboarding/subscription-hint.ts +27 -0
  388. package/src/onboarding/web-first.ts +90 -0
  389. package/src/onboarding.ts +287 -26
  390. package/src/persona/presets.ts +105 -0
  391. package/src/plugins/maker/node-install.ts +92 -0
  392. package/src/plugins/maker/node-maker.ts +139 -0
  393. package/src/plugins/maker/plugin-maker.ts +19 -7
  394. package/src/policy/llm-policy.ts +268 -0
  395. package/src/prompt-library/session-guidance.ts +7 -1
  396. package/src/prompt-library/universal-preamble.ts +2 -2
  397. package/src/relay/relay-cli.ts +20 -0
  398. package/src/relay/relay-server.ts +220 -0
  399. package/src/release-loop/checklist.ts +37 -13
  400. package/src/self-dev/decompose.ts +1 -0
  401. package/src/self-dev/dev-pipeline.ts +6 -3
  402. package/src/self-dev/orchestrate.ts +18 -11
  403. package/src/self-dev/run-record-mirror.ts +44 -0
  404. package/src/self-dev/run-store.ts +12 -3
  405. package/src/self-implement/harness-policy-gates.ts +4 -1
  406. package/src/self-implement/report-origin.ts +12 -1
  407. package/src/session/chat.ts +17 -5
  408. package/src/shell-env-bootstrap.ts +44 -7
  409. package/src/skills/connect-claude-plugin.ts +142 -0
  410. package/src/skills/connect.ts +70 -0
  411. package/src/skills/index.ts +29 -1
  412. package/src/skills/repair.ts +124 -0
  413. package/src/skills/runner.ts +11 -2
  414. package/src/skills/shared-agent-skill-roots.ts +25 -0
  415. package/src/skills/trigger-extract.ts +1 -1
  416. package/src/steward/failure-streak.ts +78 -0
  417. package/src/steward/steward-cards.ts +24 -0
  418. package/src/steward/triage-budget.ts +127 -0
  419. package/src/steward/triage-plan.ts +75 -0
  420. package/src/steward/triage.ts +244 -42
  421. package/src/task-cards/card-store.ts +1 -1
  422. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  424. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  426. package/src/telegram-commands.ts +13 -0
  427. package/src/telegram.ts +72 -2
  428. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  429. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  430. package/src/user-config.ts +189 -15
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/video-pipeline/recipes/character.ts +42 -5
  433. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  434. package/src/video-pipeline/recipes/free-line.ts +16 -3
  435. package/src/video-pipeline/recipes/remote.ts +7 -1
  436. package/src/video-pipeline/vflow-reference.ts +109 -0
  437. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  446. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  515. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  516. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  517. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
@@ -0,0 +1,210 @@
1
+ // Error reports from users' devices (ER2 · contract v1, channel 10-01 22:16). The public bot VM receives them on
2
+ // `POST /v1/reports`, checks size, rate and schema, queues them on disk and forwards them over the tailnet to the
3
+ // Primary, which stores them for 30 days and alerts the operator. No S3 credentials live on the VM.
4
+ import { createHash, randomBytes } from 'node:crypto';
5
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ export const REPORT_MAX_BYTES = 64 * 1024;
9
+ /** Queue bounds while the Primary is unreachable: oldest reports beyond these are dropped (logged). */
10
+ export const REPORT_QUEUE_MAX = 5000;
11
+ export const REPORT_QUEUE_MAX_AGE_MS = 7 * 24 * 3600_000;
12
+
13
+ const SURFACES = ['tui', 'pwa', 'cli', 'daemon'] as const;
14
+ export interface ErrorReport {
15
+ kind: 'error-report';
16
+ v: 1;
17
+ code: string;
18
+ message: string;
19
+ stack?: string;
20
+ app: { version: string; sha: string; surface: (typeof SURFACES)[number]; os: string; arch: string };
21
+ nexus: { alive: boolean; version?: string; instance?: string };
22
+ who: { installId: string; email?: string; telegramUser?: string };
23
+ at: string;
24
+ consent: true;
25
+ }
26
+ /** What the VM forwards: the report plus what the receiver derived. */
27
+ export interface ReceivedReport { reportId: string; dedupe: string; receivedAt: string; report: ErrorReport }
28
+
29
+ const str = (v: unknown, max: number): string | undefined => (typeof v === 'string' && v.length > 0 && v.length <= max ? v : undefined);
30
+ const obj = (v: unknown): Record<string, unknown> | undefined => (v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : undefined);
31
+
32
+ /** Strict schema: unknown fields are dropped, a bad required field names itself. */
33
+ export function validateErrorReport(body: unknown): { ok: true; report: ErrorReport } | { ok: false; field: string } {
34
+ const b = obj(body);
35
+ if (!b) return { ok: false, field: 'body' };
36
+ if (b.kind !== 'error-report') return { ok: false, field: 'kind' };
37
+ if (b.v !== 1) return { ok: false, field: 'v' };
38
+ if (b.consent !== true) return { ok: false, field: 'consent' };
39
+ const code = typeof b.code === 'string' && /^[a-z0-9-]{1,64}$/.test(b.code) ? b.code : undefined;
40
+ if (!code) return { ok: false, field: 'code' };
41
+ const message = str(b.message, 2000);
42
+ if (!message) return { ok: false, field: 'message' };
43
+ if (b.stack !== undefined && !str(b.stack, 8000)) return { ok: false, field: 'stack' };
44
+ const app = obj(b.app);
45
+ const version = str(app?.version, 64);
46
+ const sha = str(app?.sha, 64);
47
+ const surface = SURFACES.find((s) => s === app?.surface);
48
+ const os = str(app?.os, 64);
49
+ const arch = str(app?.arch, 32);
50
+ if (!version || !sha || !surface || !os || !arch) return { ok: false, field: 'app' };
51
+ const nexus = obj(b.nexus);
52
+ if (!nexus || typeof nexus.alive !== 'boolean') return { ok: false, field: 'nexus' };
53
+ const who = obj(b.who);
54
+ const installId = typeof who?.installId === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(who.installId) ? who.installId : undefined;
55
+ if (!installId) return { ok: false, field: 'who.installId' };
56
+ const at = typeof b.at === 'string' && Number.isFinite(Date.parse(b.at)) ? b.at : undefined;
57
+ if (!at) return { ok: false, field: 'at' };
58
+ // nexus.instance is accepted only as a hash — a raw instance name can carry a host name.
59
+ const instance = typeof nexus.instance === 'string' && /^[0-9a-f]{12,64}$/.test(nexus.instance) ? nexus.instance : undefined;
60
+ const email = str(who?.email, 254);
61
+ const telegramUser = str(who?.telegramUser, 64);
62
+ return {
63
+ ok: true,
64
+ report: {
65
+ kind: 'error-report', v: 1, code, message, ...(typeof b.stack === 'string' ? { stack: b.stack } : {}),
66
+ app: { version, sha, surface, os, arch },
67
+ nexus: { alive: nexus.alive, ...(str(nexus.version, 64) ? { version: nexus.version as string } : {}), ...(instance ? { instance } : {}) },
68
+ who: { installId, ...(email ? { email } : {}), ...(telegramUser ? { telegramUser } : {}) },
69
+ at, consent: true,
70
+ },
71
+ };
72
+ }
73
+
74
+ /** Same error = same code and same first stack frame. Computed by the receiver, never sent by the device. */
75
+ export function reportDedupe(report: Pick<ErrorReport, 'code' | 'stack'>): string {
76
+ const frame = (report.stack ?? '').split('\n').map((l) => l.trim()).find((l) => l.length > 0 && l !== report.code) ?? '';
77
+ return createHash('sha256').update(`${report.code}\n${frame}`).digest('hex').slice(0, 12);
78
+ }
79
+
80
+ const B32 = '0123456789abcdefghjkmnpqrstvwxyz';
81
+ /** `er_` + 26 chars: 10 time chars then 16 random — sortable by receipt time. */
82
+ export function newReportId(now: number = Date.now()): string {
83
+ let t = '';
84
+ let n = now;
85
+ for (let i = 0; i < 10; i++) { t = B32[n % 32]! + t; n = Math.floor(n / 32); }
86
+ const r = [...randomBytes(16)].map((x) => B32[x % 32]).join('');
87
+ return `er_${t}${r}`;
88
+ }
89
+
90
+ /** In-memory sliding windows: per install 5/min and 20/day, per IP 60/hour; same (install, dedupe) stored once an hour. */
91
+ export class ReportLimiter {
92
+ private readonly hits = new Map<string, number[]>();
93
+ private readonly stored = new Map<string, number>();
94
+ private readonly seen = new Map<string, number[]>();
95
+ constructor(private readonly now: () => number = Date.now) {}
96
+
97
+ private take(key: string, windowMs: number, max: number): number | null {
98
+ const t = this.now();
99
+ const list = (this.hits.get(key) ?? []).filter((x) => t - x < windowMs);
100
+ if (list.length >= max) { this.hits.set(key, list); return Math.ceil((windowMs - (t - list[0]!)) / 1000); }
101
+ list.push(t);
102
+ this.hits.set(key, list);
103
+ return null;
104
+ }
105
+
106
+ /** Returns seconds to wait when limited, otherwise null (and records the hit). */
107
+ admit(installId: string, ip: string): number | null {
108
+ return this.take(`ip:${ip}`, 3600_000, 60)
109
+ ?? this.take(`im:${installId}`, 60_000, 5)
110
+ ?? this.take(`id:${installId}`, 24 * 3600_000, 20);
111
+ }
112
+
113
+ /** Counts this occurrence; true when it should be stored (first of this install+dedupe within an hour). */
114
+ occurrence(installId: string, dedupe: string): { store: boolean; count24h: number } {
115
+ const t = this.now();
116
+ const list = (this.seen.get(dedupe) ?? []).filter((x) => t - x < 24 * 3600_000);
117
+ list.push(t);
118
+ this.seen.set(dedupe, list);
119
+ const key = `${installId}:${dedupe}`;
120
+ const last = this.stored.get(key);
121
+ const store = last === undefined || t - last >= 3600_000;
122
+ if (store) this.stored.set(key, t);
123
+ return { store, count24h: list.length };
124
+ }
125
+ }
126
+
127
+ /** On-disk queue on the VM: one file per report, atomic writes, bounded by count and age. */
128
+ export class ReportQueue {
129
+ readonly directory: string;
130
+ constructor(root: string, private readonly log: (event: string, data: Record<string, unknown>) => void = () => {}) {
131
+ this.directory = join(root, 'hooks', 'reports');
132
+ mkdirSync(this.directory, { recursive: true, mode: 0o700 });
133
+ }
134
+ private path(reportId: string): string { return join(this.directory, `${reportId}.json`); }
135
+ enqueue(item: ReceivedReport): void {
136
+ const path = this.path(item.reportId);
137
+ const temp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
138
+ try {
139
+ const fd = openSync(temp, 'wx', 0o600);
140
+ try { writeSync(fd, JSON.stringify(item)); fsyncSync(fd); } finally { closeSync(fd); }
141
+ renameSync(temp, path);
142
+ } finally { rmSync(temp, { force: true }); }
143
+ }
144
+ /** Oldest first; drops entries past the age or count bound (logged, never silent). */
145
+ entries(now: number = Date.now()): ReceivedReport[] {
146
+ const names = readdirSync(this.directory).filter((n) => /^er_[0-9a-z]{26}\.json$/.test(n)).sort();
147
+ const kept: ReceivedReport[] = [];
148
+ let expired = 0;
149
+ for (const name of names) {
150
+ const full = join(this.directory, name);
151
+ try {
152
+ const item = JSON.parse(readFileSync(full, 'utf8')) as ReceivedReport;
153
+ if (now - Date.parse(item.receivedAt) > REPORT_QUEUE_MAX_AGE_MS) { rmSync(full, { force: true }); expired++; continue; }
154
+ kept.push(item);
155
+ } catch { rmSync(full, { force: true }); }
156
+ }
157
+ const overflow = kept.length - REPORT_QUEUE_MAX;
158
+ if (overflow > 0) for (const item of kept.splice(0, overflow)) rmSync(this.path(item.reportId), { force: true });
159
+ if (expired || overflow > 0) this.log('dropped', { expired, overflow: Math.max(0, overflow) });
160
+ return kept;
161
+ }
162
+ count(): number { return readdirSync(this.directory).filter((n) => /^er_[0-9a-z]{26}\.json$/.test(n)).length; }
163
+ delivered(reportId: string): void { rmSync(this.path(reportId), { force: true }); }
164
+ }
165
+
166
+ /** Handles `POST /v1/reports` on the VM. Pure enough to test without a server. */
167
+ export async function handleReportPost(request: Request, deps: {
168
+ limiter: ReportLimiter; queue: ReportQueue; now?: () => number; ip?: string;
169
+ log?: (event: string, data: Record<string, unknown>) => void; wake?: () => void;
170
+ }): Promise<Response> {
171
+ const now = deps.now ?? Date.now;
172
+ const log = deps.log ?? (() => {});
173
+ const declared = Number(request.headers.get('content-length') ?? '0');
174
+ if (declared > REPORT_MAX_BYTES) { log('rejected', { reason: 'too-large' }); return Response.json({ error: 'too-large' }, { status: 413 }); }
175
+ const raw = await request.arrayBuffer();
176
+ if (raw.byteLength > REPORT_MAX_BYTES) { log('rejected', { reason: 'too-large' }); return Response.json({ error: 'too-large' }, { status: 413 }); }
177
+ let body: unknown;
178
+ try { body = JSON.parse(new TextDecoder().decode(raw)); } catch { return Response.json({ error: 'invalid', field: 'body' }, { status: 400 }); }
179
+ const checked = validateErrorReport(body);
180
+ if (!checked.ok) { log('rejected', { reason: 'invalid', field: checked.field }); return Response.json({ error: 'invalid', field: checked.field }, { status: 400 }); }
181
+ const report = checked.report;
182
+ const ip = deps.ip ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
183
+ const wait = deps.limiter.admit(report.who.installId, ip);
184
+ if (wait !== null) {
185
+ log('rejected', { reason: 'rate-limited' });
186
+ return Response.json({ error: 'rate-limited', retryAfterSec: wait }, { status: 429, headers: { 'Retry-After': String(wait) } });
187
+ }
188
+ const dedupe = reportDedupe(report);
189
+ const reportId = newReportId(now());
190
+ const { store, count24h } = deps.limiter.occurrence(report.who.installId, dedupe);
191
+ if (store) {
192
+ deps.queue.enqueue({ reportId, dedupe, receivedAt: new Date(now()).toISOString(), report });
193
+ deps.wake?.();
194
+ }
195
+ log('accepted', { code: report.code, surface: report.app.surface, stored: store, count24h });
196
+ return Response.json({ reportId, dedupe, count: count24h }, { status: 202 });
197
+ }
198
+
199
+ /** Forward queued reports to the Primary; delivered ones leave the queue, failures wait for the next drain. */
200
+ export async function drainReports(queue: ReportQueue, forward: (item: ReceivedReport) => Promise<boolean>, now: number = Date.now()): Promise<{ delivered: number; failed: number }> {
201
+ let delivered = 0;
202
+ let failed = 0;
203
+ for (const item of queue.entries(now)) {
204
+ let ok = false;
205
+ try { ok = await forward(item); } catch { ok = false; }
206
+ if (ok) { queue.delivered(item.reportId); delivered++; } else { failed++; break; }
207
+ }
208
+ return { delivered, failed };
209
+ }
210
+
@@ -4,6 +4,8 @@ import { userConfigPath } from '../user-config.js';
4
4
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
5
5
  import { HookQueue, type QueuedHook } from './queue.js';
6
6
  import { toExternalTask, verifyWebhook, type HookProvider } from './providers.js';
7
+ import { drainReports, handleReportPost, ReportLimiter, ReportQueue, type ReceivedReport } from './error-report.js';
8
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
9
 
8
10
  export interface HookSecrets {
9
11
  linear?: string;
@@ -18,6 +20,8 @@ export interface HookReceiverOptions {
18
20
  now?: () => number;
19
21
  root?: string;
20
22
  retryBaseMs?: number;
23
+ /** ER2 — forward one queued error report to the Primary; default = POST /v1/reports/ingest over the tailnet. */
24
+ forwardReport?: (item: ReceivedReport) => Promise<boolean>;
21
25
  }
22
26
 
23
27
  /** `/v1/tasks` lives on the Primary's nexus API, not on the control plane — so the address is its own setting. */
@@ -42,8 +46,40 @@ async function forwardToPrimary(event: QueuedHook): Promise<Response> {
42
46
  });
43
47
  }
44
48
 
49
+ async function forwardReportToPrimary(item: ReceivedReport): Promise<boolean> {
50
+ const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { hooks?: { primaryTokenRef?: string; primaryUrl?: unknown } };
51
+ const ref = config.hooks?.primaryTokenRef;
52
+ if (!ref) throw new Error('primary token reference missing');
53
+ const token = await getSecretAsync(ref);
54
+ if (!token) throw new Error('primary token missing');
55
+ const response = await fetch(new URL('/v1/reports/ingest', hooksPrimaryUrl(config)), {
56
+ method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
57
+ body: JSON.stringify(item), signal: AbortSignal.timeout(10_000),
58
+ });
59
+ return response.ok;
60
+ }
61
+
45
62
  export function startHookReceiver(options: HookReceiverOptions): { url: string; stop: () => void; queue: HookQueue } {
46
63
  const queue = new HookQueue(options.root);
64
+ const reportQueue = new ReportQueue(options.root ?? effectiveInstanceRoot(), (event, data) => debug.log('hooks.reports', event, data));
65
+ const reportLimiter = new ReportLimiter(options.now ?? Date.now);
66
+ const forwardReport = options.forwardReport ?? forwardReportToPrimary;
67
+ let reportFailures = 0;
68
+ let reportNextAt = 0;
69
+ let reportsDraining = false;
70
+ async function drainReportQueue(): Promise<void> {
71
+ if (stopped || reportsDraining || Date.now() < reportNextAt) return;
72
+ reportsDraining = true;
73
+ try {
74
+ const result = await drainReports(reportQueue, forwardReport, (options.now ?? Date.now)());
75
+ if (result.delivered) debug.log('hooks.reports', 'forwarded', { delivered: result.delivered });
76
+ if (result.failed) {
77
+ reportFailures++;
78
+ reportNextAt = Date.now() + Math.min(300_000, Math.max(1, options.retryBaseMs ?? 1000) * 2 ** Math.min(reportFailures - 1, 20));
79
+ debug.log('hooks.reports', 'forward-failed', { queued: reportQueue.count(), failures: reportFailures });
80
+ } else reportFailures = 0;
81
+ } finally { reportsDraining = false; }
82
+ }
47
83
  const now = options.now ?? Date.now;
48
84
  const forward = options.forward ?? forwardToPrimary;
49
85
  const attempts = new Map<string, { next: number; failures: number }>();
@@ -75,6 +111,7 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
75
111
  })();
76
112
  }
77
113
  } finally {
114
+ void drainReportQueue();
78
115
  draining = false;
79
116
  if (!stopped) timer = setTimeout(() => { void drain(); }, Math.min(1000, options.retryBaseMs ?? 1000));
80
117
  }
@@ -83,7 +120,10 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
83
120
  async fetch(request) {
84
121
  const url = new URL(request.url);
85
122
  if (request.method === 'GET' && url.pathname === '/hooks/health')
86
- return Response.json({ ok: true, queued: queue.count() });
123
+ return Response.json({ ok: true, queued: queue.count(), reportsQueued: reportQueue.count() });
124
+ if (request.method === 'POST' && url.pathname === '/v1/reports')
125
+ return handleReportPost(request, { limiter: reportLimiter, queue: reportQueue, ...(options.now ? { now: options.now } : {}),
126
+ log: (event, data) => debug.log('hooks.reports', event, data), wake: () => { reportNextAt = 0; void drainReportQueue(); } });
87
127
  if (request.method !== 'POST' || !/^\/hooks\/(linear|asana)$/.test(url.pathname)) return new Response('Not Found', { status: 404 });
88
128
  const provider = url.pathname.slice('/hooks/'.length) as HookProvider;
89
129
  const reject = (reason: string) => {
@@ -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
+ }