elanous 0.2.4 → 0.2.6

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 (455) hide show
  1. package/README.md +7 -6
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -233,6 +233,14 @@ export const BUILTIN_DIR = resolve(import.meta.dir, '..', '..', '..', 'plugins')
233
233
  export const PACKS_DIR = resolve(import.meta.dir, '..', '..', '..', 'packs');
234
234
  export const USER_DIR = join(homedir(), '.claude', 'plugins');
235
235
 
236
+ /** 코드 없이 스킬·그래프만 싣는 팩인가 — 진입점이 기본값(`./plugin.ts`)이고 파일이 없으며, 스킬·그래프를 선언하거나 `skills/` 폴더가 있다. */
237
+ export function isSkillOnlyPack(pluginDir: string, manifest: { main: string; contributes?: { skills?: unknown[]; graphs?: unknown[] } }, hasLegacyEntry: boolean): boolean {
238
+ if (hasLegacyEntry) return false;
239
+ if (manifest.main !== './plugin.ts') return false; // 명시한 진입점이 없으면 그건 결함이다 — 경고를 유지한다.
240
+ const declares = (manifest.contributes?.skills?.length ?? 0) > 0 || (manifest.contributes?.graphs?.length ?? 0) > 0;
241
+ return declares || existsSync(join(pluginDir, 'skills'));
242
+ }
243
+
236
244
  export class PluginHost {
237
245
  private available = new Map<string, PluginEntry>();
238
246
  private discoveredNodeKinds = new Map<string, NodeKindEntry>();
@@ -614,6 +622,12 @@ export class PluginHost {
614
622
  // ~/.claude/plugins/{cache,data,marketplaces} internal state)
615
623
  // are silently skipped: they never intended to be plugins.
616
624
  const looksLikePlugin = hasLegacyEntry || !manifestLoad.inferred;
625
+ // 스킬·그래프만 싣는 팩(공식 `elanous-basics` 등)은 코드 진입점이 없다 — 매니페스트가 `main` 을
626
+ // 적지 않아 기본값 `./plugin.ts` 가 들어온 것뿐이다. 부팅마다 «main not found» 경고를 내지 않는다.
627
+ if (looksLikePlugin && isSkillOnlyPack(pluginDir, manifestLoad.manifest, hasLegacyEntry)) {
628
+ debug.log('plugin.discovery', 'skill-only', { plugin: manifestLoad.manifest.id });
629
+ continue;
630
+ }
617
631
  if (looksLikePlugin) {
618
632
  this.hooks.log(`⚠ plugin "${manifestLoad.manifest.id}" main not found: ${manifestLoad.manifest.main}`);
619
633
  debug.log('plugin.discovery', 'error', {
@@ -186,9 +186,10 @@ async function fetchMarketIndex(name: string, opts: MarketFetchOptions): Promise
186
186
  const load = async (file: string): Promise<Uint8Array> => {
187
187
  const url = new URL(file, market.url);
188
188
  let response: Response;
189
- try { response = await fetcher(url.href); }
190
- catch (error) { throw new MarketFetchError('io', `market fetch failed (${file}): ${error instanceof Error ? error.message : String(error)}`); }
189
+ try { response = await fetcher(url.href, { redirect: 'error' }); }
190
+ catch { throw new MarketFetchError('io', `market fetch failed (${file}): network error`); }
191
191
  if (!response.ok) throw new MarketFetchError('io', `market fetch failed (${file}): HTTP ${response.status}`);
192
+ if (response.redirected || (response.url && response.url !== url.href)) throw new MarketFetchError('io', `market fetch was redirected (${file})`);
192
193
  if (!response.body) throw new MarketFetchError('io', `market fetch failed (${file}): missing response body`);
193
194
  const reader = response.body.getReader();
194
195
  const chunks: Uint8Array[] = [];
@@ -203,7 +204,7 @@ async function fetchMarketIndex(name: string, opts: MarketFetchOptions): Promise
203
204
  }
204
205
  } catch (error) {
205
206
  if (error instanceof MarketFetchError) throw error;
206
- throw new MarketFetchError('io', `market fetch failed (${file}): ${error instanceof Error ? error.message : String(error)}`);
207
+ throw new MarketFetchError('io', `market fetch failed (${file}): network error`);
207
208
  } finally {
208
209
  void reader.cancel().catch(() => {});
209
210
  reader.releaseLock();
@@ -0,0 +1,106 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { closeSync, existsSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { elanousStateRoot } from '../../autopilot/state-paths.js';
5
+ import { loadPluginManifestFromDir } from '../core/manifest.js';
6
+ import { listInstalledPlugins, withLedgerLock } from './plugin-install.js';
7
+
8
+ const PLUGIN_NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
9
+ const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
10
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
11
+
12
+ type FieldStatus = { name: string; env: string; set: boolean };
13
+
14
+ function credentialPath(name: string, root: string): string {
15
+ if (!PLUGIN_NAME.test(name)) throw new Error('invalid plugin name');
16
+ return join(root, 'plugins', 'credentials', `${name}.json`);
17
+ }
18
+
19
+ function declaredFields(name: string, root: string): Array<{ name: string; env: string }> {
20
+ if (!PLUGIN_NAME.test(name)) throw new Error('invalid plugin name');
21
+ const installed = listInstalledPlugins(root).filter(plugin => plugin.name === name);
22
+ if (installed.length === 0) throw new Error(`plugin not installed: ${name}`);
23
+ const fields = new Map<string, string>();
24
+ for (const plugin of installed) {
25
+ const manifest = loadPluginManifestFromDir(plugin.path, { id: plugin.name }).manifest;
26
+ for (const connector of manifest.contributes.connectors ?? []) {
27
+ for (const field of [
28
+ ...(connector.fields ?? []).map(item => ({ name: item.name, env: item.env })),
29
+ ...(connector.userConfig ?? []).map(item => ({ name: item.key, env: item.env })),
30
+ ]) {
31
+ if (!FIELD_NAME.test(field.name)) throw new Error('invalid plugin credential field name');
32
+ const env = field.env === undefined
33
+ ? `ELANOUS_PLUGIN_${name.replaceAll('-', '_').toUpperCase()}_${field.name.replaceAll('-', '_').toUpperCase()}`
34
+ : field.env;
35
+ if (typeof env !== 'string' || !ENV_NAME.test(env)) throw new Error('invalid plugin credential environment name');
36
+ if (fields.has(field.name) && fields.get(field.name) !== env) throw new Error('conflicting plugin credential environment name');
37
+ fields.set(field.name, env);
38
+ }
39
+ }
40
+ }
41
+ if (new Set(fields.values()).size !== fields.size) throw new Error('duplicate plugin credential environment name');
42
+ return [...fields].map(([fieldName, env]) => ({ name: fieldName, env })).sort((a, b) => a.name.localeCompare(b.name));
43
+ }
44
+
45
+ function readCredentials(path: string): Record<string, string> {
46
+ if (!existsSync(path)) return Object.create(null) as Record<string, string>;
47
+ if (!lstatSync(path).isFile()) throw new Error('invalid plugin credential file');
48
+ let parsed: unknown;
49
+ try { parsed = JSON.parse(readFileSync(path, 'utf8')); }
50
+ catch { throw new Error('invalid plugin credential file'); }
51
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)
52
+ || Object.entries(parsed).some(([key, value]) => !FIELD_NAME.test(key) || typeof value !== 'string')) {
53
+ throw new Error('invalid plugin credential file');
54
+ }
55
+ return Object.assign(Object.create(null) as Record<string, string>, parsed);
56
+ }
57
+
58
+ /** Update one installed plugin's declared fields. null removes a field; omitted fields remain intact. */
59
+ export function setPluginCredentials(name: string, fields: Record<string, string | null>, root = elanousStateRoot()): { set: string[] } {
60
+ const path = credentialPath(name, root);
61
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) throw new Error('invalid plugin credential fields');
62
+ const allowed = new Set(declaredFields(name, root).map(field => field.name));
63
+ for (const [key, value] of Object.entries(fields)) {
64
+ if (!allowed.has(key) || (typeof value !== 'string' && value !== null) || (typeof value === 'string' && value.includes('\0'))) {
65
+ throw new Error('invalid plugin credential field');
66
+ }
67
+ }
68
+ return withLedgerLock(root, () => {
69
+ const current = readCredentials(path);
70
+ for (const [key, value] of Object.entries(fields)) {
71
+ if (value === null) delete current[key];
72
+ else current[key] = value;
73
+ }
74
+ if (Object.keys(fields).length === 0) return { set: [] };
75
+ mkdirSync(join(root, 'plugins', 'credentials'), { recursive: true, mode: 0o700 });
76
+ const temp = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
77
+ try {
78
+ const fd = openSync(temp, 'wx', 0o600);
79
+ try {
80
+ fchmodSync(fd, 0o600);
81
+ writeFileSync(fd, JSON.stringify(current));
82
+ fsyncSync(fd);
83
+ } finally { closeSync(fd); }
84
+ renameSync(temp, path);
85
+ } finally { rmSync(temp, { force: true }); }
86
+ return { set: Object.keys(fields).filter(key => Object.hasOwn(current, key)).sort() };
87
+ });
88
+ }
89
+
90
+ /** Public metadata only: values never leave the credential store. */
91
+ export function credentialStatus(name: string, root = elanousStateRoot()): { fields: FieldStatus[] } {
92
+ const declared = declaredFields(name, root);
93
+ const saved = readCredentials(credentialPath(name, root));
94
+ return { fields: declared.map(field => ({ ...field, set: Object.hasOwn(saved, field.name) })) };
95
+ }
96
+
97
+ /** Environment entries intended only for this plugin's child process. */
98
+ export function pluginEnv(name: string, root = elanousStateRoot()): Record<string, string> {
99
+ const declared = declaredFields(name, root);
100
+ const saved = readCredentials(credentialPath(name, root));
101
+ const env = Object.create(null) as Record<string, string>;
102
+ for (const field of declared) {
103
+ if (Object.hasOwn(saved, field.name)) env[field.env] = saved[field.name]!;
104
+ }
105
+ return env;
106
+ }
@@ -1,12 +1,15 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
6
6
  import { dlopen, FFIType } from 'bun:ffi';
7
7
  import { loadPluginManifestFromDir, type PluginManifest } from '../core/manifest.js';
8
+ import { loadPluginNodes } from '../../graph-kinds/plugin-nodes.js';
9
+ import { getNodeKindRegistration, listNodeKinds, unregisterPluginNodeKind } from '../../graph-kinds/registry.js';
8
10
  import { verifyIndex, type MarketplaceIndex, type VerifyIndexResult } from '../../market/signed-index.js';
9
11
  import { elanousStateRoot } from '../../autopilot/state-paths.js';
12
+ import { ensureMarketIndex, MarketFetchError, type MarketFetchOptions } from './market-fetch.js';
10
13
 
11
14
  const NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
12
15
  const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/;
@@ -18,7 +21,7 @@ export type InstallEvent =
18
21
  | { event: 'verify'; signature: 'ok' | 'missing' | 'bad'; scan: 'safe' | 'caution' | 'dangerous' }
19
22
  | { event: 'consent'; capabilities: string[]; required: boolean }
20
23
  | { event: 'credentials'; connectors: Array<{ id: string; fields: Array<{ name: string; secret: boolean }> }> }
21
- | { event: 'registered'; kinds: string[]; graphs: string[]; skills: string[] }
24
+ | { event: 'registered'; /** Vocab file paths, not graph node kinds. */ kinds: string[]; nodes: string[]; nodeErrors: number; graphs: string[]; skills: string[] }
22
25
  | { event: 'done'; plugin: string; version: string; path: string };
23
26
 
24
27
  export interface InstalledPlugin {
@@ -27,6 +30,8 @@ export interface InstalledPlugin {
27
30
  market: string;
28
31
  path: string;
29
32
  sha256: string | null;
33
+ /** Absent for installations recorded before timestamps were introduced. */
34
+ installedAt?: string;
30
35
  }
31
36
 
32
37
  export interface InstallOptions {
@@ -34,12 +39,15 @@ export interface InstallOptions {
34
39
  marketDir?: string;
35
40
  cwd?: string;
36
41
  allowUnsigned?: boolean;
42
+ refresh?: boolean;
37
43
  yes?: boolean;
38
44
  trustedKeys?: ReadonlyArray<{ keyId: string; publicKey: string }>;
39
45
  verifySignature?: typeof verifyIndex;
40
46
  hashArtifact?: (bytes: Uint8Array) => string;
41
47
  onEvent?: (event: InstallEvent) => void;
42
48
  consent?: (capabilities: string[]) => boolean | Promise<boolean>;
49
+ fetcher?: MarketFetchOptions['fetcher'];
50
+ configPath?: string;
43
51
  }
44
52
 
45
53
  export class PluginInstallError extends Error {
@@ -115,28 +123,8 @@ function marketEntry(spec: string, opts: InstallOptions): { market: string; entr
115
123
  return { market, entry: matches[0]!, signature };
116
124
  }
117
125
 
118
- function unpackArchive(bytes: Uint8Array, target: string): void {
119
- const archive = join(dirname(target), 'artifact.tgz');
120
- writeFileSync(archive, bytes);
121
- try {
122
- const entries = child('tar', ['-tzf', archive]).split('\n');
123
- for (const item of entries) {
124
- if (item === '.' || item === './') continue;
125
- inside(target, item.replace(/^\.\//, ''));
126
- }
127
- // Links and special entries can write outside the destination before a post-extract scan.
128
- const types = child('tar', ['-tvzf', archive]).split('\n');
129
- if (types.some(line => line && !['-', 'd'].includes(line[0]!))) fail('scan', 'artifact contains links or special files');
130
- mkdirSync(target);
131
- child('tar', ['-xzf', archive, '-C', target, '--no-same-owner', '--no-same-permissions']);
132
- safeTree(target);
133
- } finally {
134
- rmSync(archive, { force: true });
135
- }
136
- }
137
-
138
- export function resolvePluginSource(spec: string, target: string, opts: InstallOptions = {}): { market: string; sha256: string | null; signature: 'ok' | 'missing'; expectedName?: string; expectedVersion?: string } {
139
- if (/^[a-z0-9][a-z0-9-]{1,39}@[a-z0-9][a-z0-9-]{1,39}$/.test(spec) && !existsSync(resolve(opts.cwd ?? process.cwd(), spec))) {
126
+ /** Offline: a signed index placed in `<markets>/<name>/` without a configured URL (pre-fetch behaviour — kept). */
127
+ function localMarketSource(spec: string, target: string, opts: InstallOptions): ResolvedPluginSource {
140
128
  const { market, entry, signature } = marketEntry(spec, opts);
141
129
  const source = entry.source;
142
130
  const sourceRoot = dirname(indexPath(market, opts));
@@ -159,7 +147,100 @@ export function resolvePluginSource(spec: string, target: string, opts: InstallO
159
147
  fail('io', `market entry has no verifiable artifact: ${entry.name}`);
160
148
  }
161
149
  return { market, sha256: entry.artifact?.sha256 ?? null, signature, expectedName: entry.name, expectedVersion: entry.version };
150
+ }
151
+
152
+ function hasLocalMarketIndex(market: string, opts: InstallOptions): boolean {
153
+ const base = resolve(opts.marketDir ?? join(opts.root ?? elanousStateRoot(), 'markets'));
154
+ return [join(base, market, 'marketplace.json'), join(base, market, '.agents', 'plugins', 'marketplace.json')].some(existsSync);
155
+ }
156
+
157
+ function unpackArchive(bytes: Uint8Array, target: string): void {
158
+ const archive = join(dirname(target), 'artifact.tgz');
159
+ writeFileSync(archive, bytes);
160
+ try {
161
+ const entries = child('tar', ['-tzf', archive]).split('\n');
162
+ for (const item of entries) {
163
+ if (item === '.' || item === './') continue;
164
+ inside(target, item.replace(/^\.\//, ''));
165
+ }
166
+ // Links and special entries can write outside the destination before a post-extract scan.
167
+ const types = child('tar', ['-tvzf', archive]).split('\n');
168
+ if (types.some(line => line && !['-', 'd'].includes(line[0]!))) fail('scan', 'artifact contains links or special files');
169
+ mkdirSync(target);
170
+ child('tar', ['-xzf', archive, '-C', target, '--no-same-owner', '--no-same-permissions']);
171
+ safeTree(target);
172
+ } finally {
173
+ rmSync(archive, { force: true });
174
+ }
175
+ }
176
+
177
+ type ResolvedPluginSource = { market: string; sha256: string | null; signature: 'ok' | 'missing'; expectedName?: string; expectedVersion?: string };
178
+
179
+ function marketSpec(spec: string, opts: InstallOptions): boolean {
180
+ return /^[a-z0-9][a-z0-9-]{1,39}@[a-z0-9][a-z0-9-]{1,39}$/.test(spec) && !existsSync(resolve(opts.cwd ?? process.cwd(), spec));
181
+ }
182
+
183
+ async function fetchedMarketSource(spec: string, target: string, opts: InstallOptions): Promise<ResolvedPluginSource> {
184
+ const [name, marketName] = spec.split('@') as [string, string];
185
+ let marketIndex: Awaited<ReturnType<typeof ensureMarketIndex>>;
186
+ try {
187
+ marketIndex = await ensureMarketIndex(marketName, {
188
+ root: opts.root, marketDir: opts.marketDir, configPath: opts.configPath, fetcher: opts.fetcher,
189
+ refresh: opts.refresh, verifySignature: opts.verifySignature, trustedKeys: opts.trustedKeys,
190
+ });
191
+ } catch (error) {
192
+ if (error instanceof MarketFetchError && /^market not configured: /.test(error.message) && hasLocalMarketIndex(marketName, opts)) {
193
+ return localMarketSource(spec, target, opts);
194
+ }
195
+ if (error instanceof MarketFetchError) throw new PluginInstallError(error.reason, error.message);
196
+ throw error;
197
+ }
198
+ const matches = marketIndex.index.plugins.filter(entry => entry.name === name);
199
+ if (matches.length !== 1) fail('io', `plugin not found or ambiguous in ${marketName}: ${name}`);
200
+ const entry = matches[0]!;
201
+ const artifact = entry.artifact;
202
+ if (!artifact || typeof artifact.key !== 'string' || !artifact.key || !HASH.test(artifact.sha256)
203
+ || !Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) fail('io', 'invalid artifact key, hash or size');
204
+ // Check the key as written, before URL resolution folds `..`/`.` or percent-encoded separators away.
205
+ if (!/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(artifact.key) || artifact.key.split('/').some(part => part === '.' || part === '..')) fail('io', 'unsafe artifact key');
206
+ const base = new URL(marketIndex.market.url);
207
+ const url = new URL(artifact.key, base);
208
+ if (url.protocol !== 'https:' || url.origin !== base.origin || !url.pathname.startsWith(base.pathname)
209
+ || url.username || url.password || url.search || url.hash || /[\\?#]/.test(artifact.key)) fail('io', 'unsafe artifact key');
210
+ let response: Response;
211
+ // The URL checks above hold only if no redirect moves the download elsewhere.
212
+ try { response = await (opts.fetcher ?? fetch)(url.href, { redirect: 'error' }); }
213
+ catch { fail('io', 'artifact fetch failed: network error'); }
214
+ if (!response.ok || !response.body) fail('io', `artifact fetch failed: HTTP ${response.status}`);
215
+ if (response.redirected || (response.url && response.url !== url.href)) fail('io', 'artifact fetch was redirected');
216
+ const reader = response.body.getReader();
217
+ const chunks: Uint8Array[] = [];
218
+ let length = 0;
219
+ try {
220
+ for (;;) {
221
+ const { done, value } = await reader.read();
222
+ if (done) break;
223
+ length += value.byteLength;
224
+ if (length > artifact.bytes) fail('scan', 'artifact sha256 or size mismatch');
225
+ chunks.push(value);
226
+ }
227
+ } finally {
228
+ void reader.cancel().catch(() => {});
229
+ reader.releaseLock();
230
+ }
231
+ const bytes = new Uint8Array(length);
232
+ let offset = 0;
233
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
234
+ if (length !== artifact.bytes || (opts.hashArtifact ?? (data => createHash('sha256').update(data).digest('hex')))(bytes).toLowerCase() !== artifact.sha256.toLowerCase()) {
235
+ fail('scan', 'artifact sha256 or size mismatch');
162
236
  }
237
+ unpackArchive(bytes, target);
238
+ return { market: marketName, sha256: artifact.sha256, signature: marketIndex.signature, expectedName: name, expectedVersion: entry.version };
239
+ }
240
+
241
+ export function resolvePluginSource(spec: string, target: string, opts: InstallOptions = {}): ResolvedPluginSource {
242
+ // Synchronous callers keep the offline contract: a signed index already placed under `<markets>/<name>/`.
243
+ if (marketSpec(spec, opts)) return localMarketSource(spec, target, opts);
163
244
  const local = resolve(opts.cwd ?? process.cwd(), spec);
164
245
  if (existsSync(local)) {
165
246
  copyPlugin(realpathSync(local), target);
@@ -187,19 +268,63 @@ function gitSource(url: string, destination: string, path?: unknown, sha?: unkno
187
268
  }
188
269
  }
189
270
 
271
+ function hasSkill(dir: string): boolean {
272
+ if (!existsSync(dir)) return false;
273
+ return readdirSync(dir, { withFileTypes: true }).some(entry => entry.isDirectory() && existsSync(join(dir, entry.name, 'SKILL.md')));
274
+ }
275
+
276
+ function inspectPluginNodes(packageRoot: string, manifest: PluginManifest): { nodes: string[]; nodeErrors: number } {
277
+ if (!manifest.contributes.nodes?.length) return { nodes: [], nodeErrors: 0 };
278
+ // Registration validates each staged definition. Use a private identity so existing installations
279
+ // cannot reject an otherwise valid kind as a duplicate; only this inspection's entries are removed.
280
+ const inspectionId = `inspect-${randomUUID().replaceAll('-', '')}`;
281
+ let nodeErrors = 0;
282
+ const nodes: string[] = [];
283
+ try {
284
+ nodeErrors = loadPluginNodes(packageRoot, { ...manifest, id: inspectionId }).errors.length;
285
+ } finally {
286
+ for (const entry of listNodeKinds()) {
287
+ if (entry.plugin !== inspectionId) continue;
288
+ nodes.push(`${manifest.id}:${entry.kind.slice(inspectionId.length + 1)}`);
289
+ const registered = getNodeKindRegistration(entry.graph, entry.kind);
290
+ if (registered) unregisterPluginNodeKind(entry.graph, entry.kind, inspectionId, registered);
291
+ }
292
+ }
293
+ return { nodes, nodeErrors };
294
+ }
295
+
296
+ function declaresMain(packageRoot: string): boolean {
297
+ for (const file of ['plugin.json', join('.codex-plugin', 'plugin.json')]) {
298
+ const path = join(packageRoot, file);
299
+ if (!existsSync(path)) continue;
300
+ try {
301
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
302
+ if (raw && typeof raw === 'object' && 'main' in raw) return true;
303
+ } catch { return true; }
304
+ }
305
+ return !existsSync(join(packageRoot, 'plugin.json')) && !existsSync(join(packageRoot, '.codex-plugin', 'plugin.json'));
306
+ }
307
+
190
308
  export async function installPlugin(spec: string, opts: InstallOptions = {}): Promise<InstalledPlugin> {
191
309
  const root = resolve(opts.root ?? elanousStateRoot());
192
310
  const stageRoot = join(root, 'plugins', '.staging');
193
311
  mkdirSync(stageRoot, { recursive: true });
194
312
  const stage = mkdtempSync(join(stageRoot, 'plugin-'));
195
313
  try {
196
- const source = resolvePluginSource(spec, join(stage, 'package'), opts);
314
+ const source = marketSpec(spec, opts)
315
+ ? await fetchedMarketSource(spec, join(stage, 'package'), opts)
316
+ : resolvePluginSource(spec, join(stage, 'package'), opts);
197
317
  const manifest: PluginManifest = loadPluginManifestFromDir(join(stage, 'package'), { id: source.expectedName ?? basename(spec) }).manifest;
198
318
  if (!NAME.test(manifest.id) || !VERSION.test(manifest.version)) fail('io', 'invalid plugin name or version');
199
319
  if (source.expectedName && (manifest.id !== source.expectedName || manifest.version !== source.expectedVersion)) fail('conflict', 'market name or version differs from manifest');
200
320
  const packageRoot = join(stage, 'package');
201
321
  const mainPath = inside(packageRoot, manifest.main);
202
- if (!existsSync(mainPath) || !realpathSync(mainPath).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin main not found or unsafe: ${manifest.main}`);
322
+ // A skills/graphs-only pack (official `elanous-basics`) declares no `main` — the default `./plugin.ts` is not a promise.
323
+ // …but only a pack that ships something to install (skills or graphs) may omit it.
324
+ const shipsAssets = hasSkill(join(packageRoot, 'skills')) || (manifest.contributes.graphs?.length ?? 0) > 0;
325
+ if (existsSync(mainPath) || declaresMain(packageRoot) || !shipsAssets) {
326
+ if (!existsSync(mainPath) || !realpathSync(mainPath).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin main not found or unsafe: ${manifest.main}`);
327
+ }
203
328
  const name = manifest.id;
204
329
  const version = manifest.version;
205
330
  for (const asset of [...(manifest.contributes.graphs ?? []), ...(manifest.contributes.vocab ?? [])]) {
@@ -228,12 +353,13 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
228
353
  .map(entry => entry.name).sort()
229
354
  : []
230
355
  : manifest.contributes.skills.map(skill => skill.name ?? skill.id ?? '').filter(Boolean);
231
- emit({ event: 'registered', kinds, graphs, skills });
356
+ const { nodes, nodeErrors } = inspectPluginNodes(packageRoot, manifest);
357
+ emit({ event: 'registered', kinds, nodes, nodeErrors, graphs, skills });
232
358
  const installed = withLedgerLock(root, () => {
233
359
  if (existsSync(destination)) fail('conflict', `plugin already installed: ${name}@${version}`);
234
360
  mkdirSync(dirname(destination), { recursive: true });
235
361
  renameSync(join(stage, 'package'), destination);
236
- const item = { name, version, market: source.market, path: destination, sha256: source.sha256 };
362
+ const item = { name, version, market: source.market, path: destination, sha256: source.sha256, installedAt: new Date().toISOString() };
237
363
  try {
238
364
  writeLedger(root, [...listInstalledPlugins(root), item]);
239
365
  } catch (error) {
@@ -323,7 +449,8 @@ function writeLedger(root: string, entries: InstalledPlugin[]): void {
323
449
  const unique = new Map(entries.map(entry => [`${entry.market}/${entry.name}/${entry.version}`, entry]));
324
450
  const temp = `${ledger}.${process.pid}.tmp`;
325
451
  try {
326
- writeFileSync(temp, JSON.stringify([...unique.values()].map(({ name, version, market, sha256 }) => ({ name, version, market, sha256 })), null, 2));
452
+ writeFileSync(temp, JSON.stringify([...unique.values()].map(({ name, version, market, sha256, installedAt }) =>
453
+ ({ name, version, market, sha256, ...(installedAt === undefined ? {} : { installedAt }) })), null, 2));
327
454
  renameSync(temp, ledger);
328
455
  } finally {
329
456
  rmSync(temp, { force: true });
@@ -334,7 +461,7 @@ export function listInstalledPlugins(root = elanousStateRoot()): InstalledPlugin
334
461
  const base = join(root, 'plugins');
335
462
  if (!existsSync(base)) return [];
336
463
  const result: InstalledPlugin[] = [];
337
- let ledger: Array<Pick<InstalledPlugin, 'name' | 'version' | 'market' | 'sha256'>> = [];
464
+ let ledger: Array<Pick<InstalledPlugin, 'name' | 'version' | 'market' | 'sha256' | 'installedAt'>> = [];
338
465
  try {
339
466
  ledger = JSON.parse(readFileSync(join(base, 'installed.json'), 'utf8'));
340
467
  if (!Array.isArray(ledger)) ledger = [];
@@ -348,8 +475,12 @@ export function listInstalledPlugins(root = elanousStateRoot()): InstalledPlugin
348
475
  const path = join(base, market.name, name.name, version.name);
349
476
  try {
350
477
  const manifest = loadPluginManifestFromDir(path, { id: name.name }).manifest;
351
- if (manifest.id === name.name && manifest.version === version.name) result.push({ name: name.name, version: version.name, market: market.name, path,
352
- sha256: ledger.find(entry => entry.name === name.name && entry.version === version.name && entry.market === market.name)?.sha256 ?? null });
478
+ if (manifest.id === name.name && manifest.version === version.name) {
479
+ const recorded = ledger.find(entry => entry.name === name.name && entry.version === version.name && entry.market === market.name);
480
+ result.push({ name: name.name, version: version.name, market: market.name, path,
481
+ sha256: recorded?.sha256 ?? null,
482
+ ...(typeof recorded?.installedAt === 'string' ? { installedAt: recorded.installedAt } : {}) });
483
+ }
353
484
  } catch { /* malformed packages are not installed plugins */ }
354
485
  }
355
486
  }