elanous 0.2.5 → 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 (300) hide show
  1. package/README.md +5 -4
  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/{7527.943753b6ed829f09.js → 1883.c91a2648af4842fd.js} +1 -1
  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/{8918-482b30ce2a8ebf4c.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/{4853-35fdbe6bdbcdc0fc.js → 3165-95515ca5f299a120.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 3995-bee512a7063036ce.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.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/5329.8b7e4bca684b6eec.js +5 -0
  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/{5348-bbf838e119661eee.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.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/{9238.2cd5f47f4bf7a2c3.js → 8250.4a2c5553a07cdf47.js} +1 -1
  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/{4daa2798-1363d725c79f3593.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/{f6f3850d-7f324c6566002113.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-84cd419494e16b0f.js → page-c3890e90bd232732.js} +1 -1
  84. package/apps/pwa/out/_next/static/chunks/{06ef2eba-d1b99ac6d2ea663c.js → beb5cf10-9adcc10b83930090.js} +1 -1
  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-2bf59c720fe9542c.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  92. package/apps/pwa/out/approvals/index.html +1 -1
  93. package/apps/pwa/out/approvals/index.txt +21 -21
  94. package/apps/pwa/out/autopilot/index.html +1 -1
  95. package/apps/pwa/out/autopilot/index.txt +21 -21
  96. package/apps/pwa/out/board/index.html +1 -1
  97. package/apps/pwa/out/board/index.txt +21 -21
  98. package/apps/pwa/out/botlab/index.html +1 -1
  99. package/apps/pwa/out/botlab/index.txt +20 -20
  100. package/apps/pwa/out/bots/index.html +1 -1
  101. package/apps/pwa/out/bots/index.txt +20 -20
  102. package/apps/pwa/out/chat/index.html +1 -1
  103. package/apps/pwa/out/chat/index.txt +20 -20
  104. package/apps/pwa/out/control/index.html +1 -1
  105. package/apps/pwa/out/control/index.txt +21 -21
  106. package/apps/pwa/out/design-check/index.html +1 -1
  107. package/apps/pwa/out/design-check/index.txt +21 -21
  108. package/apps/pwa/out/editor/index.html +1 -1
  109. package/apps/pwa/out/editor/index.txt +21 -21
  110. package/apps/pwa/out/healthz +1 -1
  111. package/apps/pwa/out/icons/elanous-16.png +0 -0
  112. package/apps/pwa/out/icons/elanous-180.png +0 -0
  113. package/apps/pwa/out/icons/elanous-192.png +0 -0
  114. package/apps/pwa/out/icons/elanous-32.png +0 -0
  115. package/apps/pwa/out/icons/elanous-512.png +0 -0
  116. package/apps/pwa/out/index.html +1 -1
  117. package/apps/pwa/out/index.txt +20 -20
  118. package/apps/pwa/out/intake/index.html +1 -1
  119. package/apps/pwa/out/intake/index.txt +21 -21
  120. package/apps/pwa/out/live/index.html +1 -1
  121. package/apps/pwa/out/live/index.txt +21 -21
  122. package/apps/pwa/out/market/index.html +1 -1
  123. package/apps/pwa/out/market/index.txt +20 -20
  124. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  125. package/apps/pwa/out/missions/placeholder/index.txt +20 -20
  126. package/apps/pwa/out/morning/index.html +1 -1
  127. package/apps/pwa/out/morning/index.txt +21 -21
  128. package/apps/pwa/out/observatory/index.html +1 -1
  129. package/apps/pwa/out/observatory/index.txt +21 -21
  130. package/apps/pwa/out/reflection/index.html +1 -1
  131. package/apps/pwa/out/reflection/index.txt +21 -21
  132. package/apps/pwa/out/scheduler/index.html +1 -1
  133. package/apps/pwa/out/scheduler/index.txt +21 -21
  134. package/apps/pwa/out/sessions/index.html +1 -1
  135. package/apps/pwa/out/sessions/index.txt +20 -20
  136. package/apps/pwa/out/settings/devices/index.html +1 -1
  137. package/apps/pwa/out/settings/devices/index.txt +21 -21
  138. package/apps/pwa/out/settings/index.html +1 -1
  139. package/apps/pwa/out/settings/index.txt +21 -21
  140. package/apps/pwa/out/setup/done/index.html +1 -1
  141. package/apps/pwa/out/setup/done/index.txt +21 -21
  142. package/apps/pwa/out/setup/index.html +1 -1
  143. package/apps/pwa/out/setup/index.txt +21 -21
  144. package/apps/pwa/out/share/index.html +1 -1
  145. package/apps/pwa/out/share/index.txt +21 -21
  146. package/apps/pwa/out/showroom/index.html +1 -1
  147. package/apps/pwa/out/showroom/index.txt +20 -20
  148. package/apps/pwa/out/tasks/index.html +1 -1
  149. package/apps/pwa/out/tasks/index.txt +21 -21
  150. package/apps/pwa/out/term/index.html +1 -1
  151. package/apps/pwa/out/term/index.txt +21 -21
  152. package/apps/pwa/out/trace/index.html +1 -1
  153. package/apps/pwa/out/trace/index.txt +21 -21
  154. package/apps/pwa/out/vault/index.html +1 -1
  155. package/apps/pwa/out/vault/index.txt +21 -21
  156. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  157. package/apps/pwa/out/workflows/chat-ui/index.txt +21 -21
  158. package/apps/pwa/out/workflows/index.html +1 -1
  159. package/apps/pwa/out/workflows/index.txt +21 -21
  160. package/apps/pwa/out/workspace/index.html +1 -1
  161. package/apps/pwa/out/workspace/index.txt +21 -21
  162. package/apps/pwa/out/worktrees/index.html +1 -1
  163. package/apps/pwa/out/worktrees/index.txt +21 -21
  164. package/graphs/release/recipes.yaml +3 -0
  165. package/graphs/release/release-loop.yaml +3 -1
  166. package/package.json +1 -1
  167. package/plugins/card-followup/README.md +33 -0
  168. package/plugins/card-followup/examples/card-sample.json +7 -0
  169. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  170. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  171. package/plugins/card-followup/graphs/run-step.ts +172 -0
  172. package/plugins/card-followup/plugin.json +25 -0
  173. package/plugins/geo-check/README.md +13 -0
  174. package/plugins/geo-check/examples/brand-sample.json +8 -0
  175. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  176. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  177. package/plugins/geo-check/graphs/run-step.ts +177 -0
  178. package/plugins/geo-check/plugin.json +24 -0
  179. package/plugins/job-coach/README.md +30 -0
  180. package/scripts/coord-tracks.json +2 -2
  181. package/scripts/daemon-port-baseline.txt +1 -2
  182. package/scripts/lib/android-test-device.ts +32 -0
  183. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  184. package/scripts/release-loop/gate-diff.ts +29 -0
  185. package/scripts/release-loop/gate-node.ts +46 -13
  186. package/scripts/release-loop/shard-plan.ts +47 -0
  187. package/scripts/ux/record-page.ts +4 -1
  188. package/skills/explainer-video/SKILL.md +18 -0
  189. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  190. package/skills/explainer-video/engine/build.mjs +1 -1
  191. package/skills/explainer-video/engine/reel.mjs +128 -0
  192. package/skills/explainer-video/engine/reel.sh +8 -0
  193. package/skills/explainer-video/engine/vo.ts +58 -16
  194. package/src/autopilot/mission-origin.ts +20 -4
  195. package/src/autopilot/mission-phase-track.ts +11 -1
  196. package/src/autopilot/track-agent.ts +3 -3
  197. package/src/autopilot/track-courier.ts +2 -2
  198. package/src/cli/nexus-ios-bind.ts +25 -8
  199. package/src/cli/phone-link.ts +117 -0
  200. package/src/cli/plugin-cli.ts +27 -0
  201. package/src/cli/private-ledger-commands.ts +31 -0
  202. package/src/cli/release-cli.ts +14 -11
  203. package/src/domains/outbound-alert.ts +5 -5
  204. package/src/field/field-media.ts +245 -0
  205. package/src/index.ts +18 -24
  206. package/src/nexus/api/field-uploads.ts +143 -0
  207. package/src/nexus/api/graph-approvals.ts +50 -1
  208. package/src/nexus/api/harness-api.ts +219 -5
  209. package/src/nexus/api/http-server.ts +9 -0
  210. package/src/notify/llm-budget-alert.ts +119 -0
  211. package/src/plugins/maker/plugin-maker.ts +208 -0
  212. package/src/relay/channel.ts +184 -0
  213. package/src/relay/mux.ts +182 -0
  214. package/src/relay/test-vectors.json +29 -0
  215. package/src/release-loop/checklist.ts +28 -2
  216. package/src/seat-address/seat-address.ts +34 -0
  217. package/src/self-implement/run-ledger-compact.ts +1 -1
  218. package/src/session/index.ts +23 -10
  219. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  220. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  221. package/src/telegram.ts +108 -1
  222. package/src/user-config.ts +4 -0
  223. package/src/version/packed-revision.json +1 -1
  224. package/src/video-pipeline/recipes/explainer.ts +213 -0
  225. package/src/video-pipeline/walk-line.ts +3 -2
  226. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/1751.50b96ea9c37b49b6.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  233. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  241. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  250. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  251. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  252. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  253. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  254. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  255. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  256. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  257. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  258. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  259. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0ac5d053f0444d21.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/morning/page-bccce0babceae30f.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  300. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -0,0 +1,172 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, extname, isAbsolute, join } from 'node:path';
4
+
5
+ type Data = Record<string, unknown>;
6
+ const object = (v: unknown): Data => v && typeof v === 'object' && !Array.isArray(v) ? v as Data : {};
7
+ const text = (v: unknown): string => typeof v === 'string' ? v.trim() : '';
8
+ const fields = ['name', 'title', 'company', 'email', 'phone', 'url', 'linkedin', 'language'] as const;
9
+ const clean = (v: unknown) => text(v) || null;
10
+ const line = (v: unknown) => text(v).replace(/[\r\n|]/g, ' ');
11
+
12
+ // Find the first balanced object, respecting JSON string escapes and braces in quoted text.
13
+ function firstObject(raw: string): Data {
14
+ for (let start = raw.indexOf('{'); start !== -1; start = raw.indexOf('{', start + 1)) {
15
+ let depth = 0, quoted = false, escaped = false;
16
+ for (let i = start; i < raw.length; i++) {
17
+ const ch = raw[i];
18
+ if (quoted) {
19
+ if (escaped) escaped = false;
20
+ else if (ch === '\\') escaped = true;
21
+ else if (ch === '"') quoted = false;
22
+ } else if (ch === '"') quoted = true;
23
+ else if (ch === '{') depth++;
24
+ else if (ch === '}' && --depth === 0) {
25
+ try {
26
+ const parsed = JSON.parse(raw.slice(start, i + 1));
27
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Data;
28
+ } catch { /* try the next object */ }
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ throw new Error(`JSON 객체 없음: ${raw.slice(0, 200)}`);
34
+ }
35
+
36
+ async function cli(bin: string, args: string[]): Promise<string> {
37
+ const child = Bun.spawn([bin, ...args], { stdout: 'pipe', stderr: 'pipe', env: process.env });
38
+ const [stdout, stderr, status] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
39
+ if (status !== 0) throw new Error(`${args[0]} 실패: ${(stderr || stdout).slice(0, 200)}`);
40
+ return stdout;
41
+ }
42
+
43
+ function absolute(path: string, label: string): string {
44
+ const expanded = path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
45
+ if (!isAbsolute(expanded)) throw new Error(`${label}: 절대 경로 필요`);
46
+ return expanded;
47
+ }
48
+
49
+ function sources(raw: string): { title: string; url: string; snippet: string }[] {
50
+ const data = object(JSON.parse(raw));
51
+ // research --json exposes the human-readable result in `output`.
52
+ const output = text(data.output);
53
+ return output.split('\n').flatMap(row => {
54
+ const match = row.match(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/) ?? row.match(/(https?:\/\/[^\s)]+)/);
55
+ if (!match) return [];
56
+ return [{ title: match[2] ? match[1]! : row.replace(match[0], '').trim(), url: match[2] ?? match[1]!, snippet: row.replace(match[0], '').trim() }];
57
+ });
58
+ }
59
+
60
+ const contextFile = process.env.ELANOUS_GRAPH_CONTEXT;
61
+ if (!contextFile) throw new Error('ELANOUS_GRAPH_CONTEXT 필요');
62
+ const context = object(JSON.parse(readFileSync(contextFile, 'utf8')));
63
+ const input = object(context.input);
64
+ const outputs = object(context.outputs);
65
+ const step = process.argv[2];
66
+ const elanous = process.env.CARD_FOLLOWUP_ELANOUS_BIN || 'elanous';
67
+ const codex = process.env.CARD_FOLLOWUP_CODEX_BIN || 'codex';
68
+ const result = (data: Data) => console.log(JSON.stringify(data.outcome ? data : { outcome: 'ok', ...data }));
69
+
70
+ try {
71
+ if (step === 'read-card') {
72
+ const image = absolute(text(input.image), 'image');
73
+ if (!/\.(png|jpe?g|heic)$/i.test(extname(image))) throw new Error('image: png/jpg/heic 필요');
74
+ if (!existsSync(image)) throw new Error('image: 파일 없음');
75
+ const raw = await cli(codex, ['exec', '-i', image, '명함 사진에서 읽을 수 있는 값만 JSON 객체로 출력하라: name, title, company, email, phone, url, linkedin, language. 빈 칸은 null. 추측 금지.']);
76
+ const card = firstObject(raw);
77
+ const normalized = Object.fromEntries(fields.map(field => [field, clean(card[field])]));
78
+ result({ card: normalized, image });
79
+ } else if (step === 'research') {
80
+ const read = object(outputs['read-card']);
81
+ const card = object(read.card ?? read);
82
+ const company = text(card.company), name = text(card.name);
83
+ if (!company && !name) throw new Error('조사할 이름·회사 없음');
84
+ const queries = [company || name, [name, company].filter(Boolean).join(' ')];
85
+ const hits = (await Promise.all(queries.map(query => cli(elanous, ['--test', 'research', '--json', '--limit', '5', query])))).flatMap(sources);
86
+ const unique = [...new Map(hits.map(hit => [hit.url, hit])).values()];
87
+ // Only the exact result text may be reported as a company fact, never a model-invented summary.
88
+ result({ summary: unique.length ? unique[0]!.title : null, news: unique.slice(1, 3).map(hit => hit.title), sources: unique });
89
+ } else if (step === 'draft') {
90
+ const read = object(outputs['read-card']);
91
+ const card = object(read.card ?? read);
92
+ const research = object(outputs.research);
93
+ const evidence = (Array.isArray(research.sources) ? research.sources.map(object) : [])
94
+ .filter(source => /^https?:\/\//.test(text(source.url)));
95
+ const language = text(input.language) || text(card.language) || 'ko';
96
+ if (!['ko', 'en'].includes(language)) throw new Error('language: ko 또는 en 필요');
97
+ const contextLine = text(input.context);
98
+ const numbered = evidence.map((source, index) => ({ id: `S${index + 1}`, title: text(source.title), snippet: text(source.snippet) }));
99
+ const marker = /\s*\[S(\d+)\]/g;
100
+ const strip = (value: string) => value.replace(marker, '').trim();
101
+ const sentencesOf = (value: string) => value.split(/(?<=[.!?。!?])\s+|\n+/).map(s => s.trim()).filter(Boolean);
102
+ const ask = async (payload: Data): Promise<Data> => {
103
+ const response = JSON.parse(await cli(elanous, ['--test', 'ask', '--json', JSON.stringify(payload)])) as Data;
104
+ return firstObject(typeof response.reply === 'string' ? response.reply : JSON.stringify(response));
105
+ };
106
+ const instruction = 'JSON 객체만 출력: subject, body (120~200자 · [S#] 표시는 글자 수에서 뺀다), linkedin (300자 이하), question. context가 있으면 body에 그 문구를 그대로 한 번 포함. '
107
+ + '명함 필드(이름·직함·회사)와 context 는 출처 없이 써도 된다. 그 밖의 회사·사람·시장 사실은 evidence 에 있는 것만 쓰고, 그 문장 끝에 근거 번호를 [S1] 처럼 붙인다. '
108
+ + 'evidence 에 없는 사실(매출·순위·규모·수상·최근 소식 등)은 쓰지 말 것. email이 없으면 메일 주소를 만들지 말 것. 어떤 메시지도 보내지 말 것.';
109
+ let feedback = '';
110
+ let accepted: { warnings: string[]; draft: Data; facts: { text: string; url: string }[] } | undefined;
111
+ let lastProblem = '';
112
+ for (let attempt = 0; attempt < 3 && !accepted; attempt++) {
113
+ const draft = await ask({ task: 'draft', instruction: instruction + feedback, card, context: contextLine, sender: text(input.sender), language, evidence: numbered });
114
+ const parts = { subject: text(draft.subject), body: text(draft.body), linkedin: text(draft.linkedin), question: text(draft.question) };
115
+ if (!parts.subject || !parts.body || !parts.linkedin || !parts.question) { lastProblem = '초안 필수 필드 없음'; feedback = ' 이전 응답에 필드가 빠졌다.'; continue; }
116
+ const body = strip(parts.body), linkedin = strip(parts.linkedin);
117
+ const shape: string[] = [];
118
+ if (body.length < 120 || body.length > 200) shape.push(`메일 본문 120~200자 범위 밖(${body.length}자)`);
119
+ // LinkedIn 한도는 한 번 다시 부르고, 그래도 넘으면 막지 않고 보고서에 경고로 남긴다(보내기 전 사람이 고친다).
120
+ const linkedinOver = linkedin.length > 300;
121
+ if (linkedinOver && attempt === 0) shape.push(`LinkedIn 300자 초과(${linkedin.length}자)`);
122
+ if (contextLine && !body.includes(contextLine)) shape.push('메일 본문에 context 없음');
123
+ const sentences = Object.values(parts).flatMap(sentencesOf).map(sentence => ({
124
+ raw: sentence, text: strip(sentence), cites: [...sentence.matchAll(marker)].map(match => Number(match[1])),
125
+ }));
126
+ const badCite = sentences.find(sentence => sentence.cites.some(n => n < 1 || n > evidence.length));
127
+ if (badCite) shape.push(`없는 출처 번호: ${badCite.raw}`);
128
+ if (shape.length) { lastProblem = shape.join(' · '); feedback = ` 이전 초안 문제: ${lastProblem}. 모두 고쳐 다시 작성하라.`; continue; }
129
+ // Every sentence is judged against the card, the context and only the evidence it cites — a company name alone is identity, not a claim.
130
+ const verdict = await ask({
131
+ task: 'verify',
132
+ instruction: '각 문장에 대해, 받는 사람·그 회사·시장에 관한 사실 주장이 명함 필드, context, 또는 그 문장이 인용한 evidence 로 뒷받침되는지 판정하라. 인사·호칭·명함에 있는 이름/회사/직함 언급·제안·질문·보내는 사람의 의도는 사실 주장이 아니다. JSON 객체만 출력: {"unsupported":[뒷받침 안 되는 문장 번호]}',
133
+ card, context: contextLine,
134
+ sentences: sentences.map((sentence, i) => ({ i, text: sentence.text, evidence: sentence.cites.map(n => numbered[n - 1]) })),
135
+ });
136
+ if (!Array.isArray(verdict.unsupported)) throw new Error('사실 검증 응답을 읽지 못했다');
137
+ const unsupported = verdict.unsupported.map(Number).filter(i => Number.isInteger(i) && sentences[i]).map(i => sentences[i]!.text);
138
+ if (unsupported.length) { lastProblem = `출처 없는 회사 사실: ${unsupported.join(' / ')}`; feedback = ` 이전 초안에서 근거 없는 문장: ${unsupported.join(' / ')}. 그 문장을 빼거나 evidence 로 뒷받침되는 문장으로 바꿔라.`; continue; }
139
+ const facts = sentences.flatMap(sentence => sentence.cites.map(n => ({ text: sentence.text, url: text(evidence[n - 1]!.url) })));
140
+ accepted = { warnings: linkedinOver ? ['LinkedIn 문구 300자 초과 — 전송 전 수정 필요'] : [], draft: { subject: strip(parts.subject), body, linkedin, question: strip(parts.question) }, facts: [...new Map(facts.map(f => [`${f.text}\n${f.url}`, f])).values()] };
141
+ }
142
+ if (!accepted) throw new Error(lastProblem || '초안 생성 실패');
143
+ result({ ...accepted.draft, companyFacts: accepted.facts, destination: card.email ? ['email', 'LinkedIn'] : ['LinkedIn'], warnings: accepted.warnings, language });
144
+ } else if (step === 'report') {
145
+ const read = object(outputs['read-card']);
146
+ const card = object(read.card ?? read);
147
+ const research = object(outputs.research), draft = object(outputs.draft);
148
+ if (!text(draft.subject) || !text(draft.linkedin) || !text(draft.question)) throw new Error('초안 없음');
149
+ const fallback = dirname(contextFile).endsWith('.json.contexts') ? dirname(contextFile).slice(0, -'.json.contexts'.length) : dirname(contextFile);
150
+ const outDir = input.outDir === undefined ? fallback : absolute(text(input.outDir), 'outDir');
151
+ const hits = Array.isArray(research.sources) ? research.sources.map(object) : [];
152
+ const warnings = Array.isArray(draft.warnings) ? draft.warnings.map(text).filter(Boolean) : [];
153
+ const md = ['# 네트워킹 팔로업 초안', '', '## 명함', '| 필드 | 값 |', '| --- | --- |',
154
+ ...fields.map(field => `| ${field} | ${line(card[field]) || 'null'} |`), '',
155
+ '## 조사 요약', hits.length ? line(research.summary) : '조사 결과 없음',
156
+ ...((Array.isArray(research.news) ? research.news : []).map(v => `- ${line(v)}`)),
157
+ '## 출처', ...(hits.length ? hits.map(hit => `- ${line(hit.title)} — ${text(hit.url)}`) : ['- 조사 결과 없음']), '',
158
+ '## 보낼 곳', ...((Array.isArray(draft.destination) ? draft.destination : []).map(v => `- ${line(v)}`)), '',
159
+ '## 팔로업 메일', `제목: ${line(draft.subject)}`, '', text(draft.body), '',
160
+ '## LinkedIn 초대 문구', text(draft.linkedin), '', '## 대화 이어 갈 질문', text(draft.question), '',
161
+ '## 초안의 사실과 출처', ...((Array.isArray(draft.companyFacts) && draft.companyFacts.length ? draft.companyFacts : ['출처를 인용한 문장 없음']).map(v => typeof v === 'string' ? `- ${v}` : `- ${line(object(v).text)} — ${text(object(v).url)}`)),
162
+ ...(warnings.length ? ['## 경고', ...warnings.map(v => `- ${v}`)] : []), '', '※ 초안만 생성했습니다. 전송은 사람이 결정합니다.', ''].join('\n');
163
+ mkdirSync(outDir, { recursive: true, mode: 0o700 });
164
+ const report = { card, research, draft, warnings, sent: false };
165
+ const markdown = join(outDir, 'followup.md'), json = join(outDir, 'followup.json');
166
+ writeFileSync(markdown, md, { mode: 0o600 });
167
+ writeFileSync(json, JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
168
+ result({ markdown, json });
169
+ } else throw new Error(`unknown step: ${step}`);
170
+ } catch (error) {
171
+ result({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) });
172
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
+ "name": "card-followup",
4
+ "version": "0.1.0",
5
+ "description": "Business card photo to researched follow-up and LinkedIn drafts. Drafts are never sent.",
6
+ "extensions": {
7
+ "ai.elanous": {
8
+ "graphs": [
9
+ "./graphs/card-followup.yaml"
10
+ ],
11
+ "capabilities": [
12
+ "fs:workdir",
13
+ "proc:bun",
14
+ "proc:elanous",
15
+ "proc:codex"
16
+ ],
17
+ "connectors": [],
18
+ "pricing": {
19
+ "model": "free"
20
+ },
21
+ "trust": "official",
22
+ "category": "Marketing"
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ # geo-check
2
+
3
+ Run from the repository without installing the plugin:
4
+
5
+ ```sh
6
+ elanous graph run plugins/geo-check/graphs/geo-check.yaml --input '{"brand":"Elanous","domain":"elanous.ai"}' --json
7
+ ```
8
+
9
+ Inputs: `brand` (required name), `domain` (optional website; without it, citation is unknown rather than no), `questions` (optional array of up to 8 questions; `[]` generates five unbranded customer questions), `engines` (optional array of up to 3 distinct provider names; defaults to `openai-codex` and `grok`), and `outDir` (optional output directory, default beside the graph run state file). Questions are passed to each engine via `elanous ask --json`, and searched with `elanous research --json --limit 5`. Each step's final stdout line is JSON with `calls` and `errors`. For test fixtures, `GEO_CHECK_ELANOUS_BIN` substitutes the CLI executable.
10
+
11
+ Outputs: `report.md` has a one-line summary, question × engine mention/citation/rank/error table, evidence-linked suggestions when any question × engine cell is missing a mention or citation (no suggestions when no cells are missing), and the first 400 characters of each answer. `report.json` contains the same data and full answers. One engine can fail while the other completes; if every answer fails, the graph ends at `failed`. Research failures leave empty URL lists and an error count. The table is **a sample at that instant**, not a stable brand ranking: model answers change between runs.
12
+
13
+ Release note metadata: feat — New geo-check plugin measures whether AI answer engines mention and cite your brand, and suggests fixes · kind: feat · document: plugins/geo-check/README.md · target: next.
@@ -0,0 +1,8 @@
1
+ {
2
+ "brand": "Moonfern Atelier",
3
+ "domain": "moonfern.example",
4
+ "questions": [
5
+ "Which studio makes hand-painted stationery for small events?",
6
+ "Where can I find custom illustrated invitation cards?"
7
+ ]
8
+ }
@@ -0,0 +1,20 @@
1
+ graph_id: geo-check
2
+ version: 1
3
+ entry_node: questions
4
+ terminal_nodes: [done, failed]
5
+ nodes:
6
+ - { node_id: questions, kind: agent, recipe: 'cmd:questions', max_visits: 1 }
7
+ - { node_id: answers, kind: agent, recipe: 'cmd:answers', max_visits: 1 }
8
+ - { node_id: web, kind: agent, recipe: 'cmd:web', max_visits: 1 }
9
+ - { node_id: score, kind: agent, recipe: 'cmd:score', max_visits: 1 }
10
+ - { node_id: suggest, kind: agent, recipe: 'cmd:suggest', max_visits: 1 }
11
+ - { node_id: report, kind: agent, recipe: 'cmd:report', max_visits: 1 }
12
+ - { node_id: done, kind: gate, max_visits: 1 }
13
+ - { node_id: failed, kind: gate, max_visits: 1 }
14
+ edges:
15
+ - { from: questions, on: outcome, map: { ok: answers, fail: failed } }
16
+ - { from: answers, on: outcome, map: { ok: web, fail: failed } }
17
+ - { from: web, on: outcome, map: { ok: score, fail: failed } }
18
+ - { from: score, on: outcome, map: { ok: suggest, fail: failed } }
19
+ - { from: suggest, on: outcome, map: { ok: report, fail: failed } }
20
+ - { from: report, on: outcome, map: { ok: done, fail: failed } }
@@ -0,0 +1,18 @@
1
+ questions:
2
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" questions'
3
+ timeout_ms: 120000
4
+ answers:
5
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" answers'
6
+ timeout_ms: 600000
7
+ web:
8
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" web'
9
+ timeout_ms: 600000
10
+ score:
11
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" score'
12
+ timeout_ms: 30000
13
+ suggest:
14
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" suggest'
15
+ timeout_ms: 120000
16
+ report:
17
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" report'
18
+ timeout_ms: 30000
@@ -0,0 +1,177 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ type Data = Record<string, unknown>;
5
+ const object = (v: unknown): Data => v !== null && typeof v === 'object' && !Array.isArray(v) ? v as Data : {};
6
+ const text = (v: unknown): string => typeof v === 'string' ? v.trim() : '';
7
+ const strings = (v: unknown): string[] => Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string' && !!x.trim()) : [];
8
+ const rows = (v: unknown): Data[] => Array.isArray(v) ? v.map(object) : [];
9
+ const line = (v: unknown): string => text(v).replace(/[\r\n|]+/g, ' ');
10
+ const errorLine = (e: unknown): string => line(e instanceof Error ? e.message : String(e)).slice(0, 240) || 'unknown error';
11
+ try {
12
+ const contextPath = process.env.ELANOUS_GRAPH_CONTEXT ?? '';
13
+ const context = object(JSON.parse(readFileSync(contextPath, 'utf8')));
14
+ const input = object(context.input);
15
+ const outputs = object(context.outputs);
16
+ const stateDirectory = dirname(contextPath);
17
+ if (!stateDirectory.endsWith('.json.contexts')) throw new Error('invalid graph run context');
18
+ const defaultOutDir = stateDirectory.slice(0, -'.json.contexts'.length);
19
+ const brand = text(input.brand);
20
+ if (!brand) throw new Error('brand is required');
21
+ const engines = input.engines === undefined ? ['openai-codex', 'grok'] : strings(input.engines);
22
+ if (!engines.length || engines.length > 3 || engines.length !== new Set(engines).size || (input.engines !== undefined && (!Array.isArray(input.engines) || strings(input.engines).length !== input.engines.length))) throw new Error('engines must contain 1 to 3 distinct names');
23
+ const cli = process.env.GEO_CHECK_ELANOUS_BIN || 'elanous';
24
+
25
+ async function invoke(args: string[], engine?: string): Promise<unknown> {
26
+ const proc = Bun.spawn([cli, ...args], {
27
+ env: { ...process.env, ...(engine ? { ELANOUS_LLM_PROVIDER: engine } : {}) },
28
+ stdout: 'pipe', stderr: 'pipe',
29
+ });
30
+ const [stdout, stderr, exit] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
31
+ if (exit !== 0) throw new Error(line(stderr || stdout) || `CLI exited ${exit}`);
32
+ const last = stdout.trim().split('\n').at(-1);
33
+ if (!last) throw new Error('CLI returned no JSON');
34
+ return JSON.parse(last) as unknown;
35
+ }
36
+ function reply(raw: unknown): string {
37
+ const result = object(raw);
38
+ if (typeof result.reply !== 'string') throw new Error('ask returned no reply');
39
+ return result.reply;
40
+ }
41
+ function parseArray(raw: string): unknown {
42
+ const cleaned = raw.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
43
+ return JSON.parse(cleaned) as unknown;
44
+ }
45
+ function domains(v: string): string {
46
+ const raw = v.trim().replace(/^https?:\/\//i, '').replace(/^www\./i, '').replace(/\/+$/, '');
47
+ return raw.split('/')[0]?.toLowerCase().replace(/:\d+$/, '') ?? '';
48
+ }
49
+ function urls(v: unknown): string[] {
50
+ if (Array.isArray(v)) return v.flatMap(urls);
51
+ if (typeof v === 'string') return [...v.matchAll(/https?:\/\/[^\s<>"')\]]+/gi)].map(m => m[0]!.replace(/[.,;!?]+$/, ''));
52
+ if (v && typeof v === 'object') return Object.entries(v).flatMap(([key, item]) => key === 'url' && typeof item === 'string' ? [item] : urls(item));
53
+ return [];
54
+ }
55
+ // `elanous research --json` = { query, output, metadata } — result links are `- [title](url)` lines in `output`.
56
+ // Only those links count; the query string or other fields may name the brand domain without it being a result.
57
+ function resultUrls(raw: unknown): string[] {
58
+ const out = object(raw).output;
59
+ if (typeof out !== 'string') return [];
60
+ return [...out.matchAll(/^\s*-\s*\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gm)].map(m => m[1]!);
61
+ }
62
+ // A bare domain in the answer text («see elanous.ai») is a citation too, not only an https:// link.
63
+ function mentionsHost(answer: string, host: string): boolean {
64
+ if (!host) return false;
65
+ const escaped = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ return new RegExp(`(^|[^a-z0-9.-])(?:www\\.)?${escaped}(?![a-z0-9-])`, 'i').test(answer);
67
+ }
68
+ function cited(url: string, host: string): boolean {
69
+ try {
70
+ const name = new URL(url).hostname.toLowerCase().replace(/^www\./, '');
71
+ return name === host || name.endsWith(`.${host}`);
72
+ } catch { return false; }
73
+ }
74
+ function firstMention(answer: string, name: string): number | null {
75
+ const lower = answer.toLocaleLowerCase();
76
+ const needle = name.toLocaleLowerCase();
77
+ let at = lower.indexOf(needle);
78
+ while (at >= 0) {
79
+ const before = at ? lower[at - 1]! : '';
80
+ const after = lower[at + needle.length] ?? '';
81
+ if (!/[\p{L}\p{N}_]/u.test(before) && !/[\p{L}\p{N}_]/u.test(after)) return at;
82
+ at = lower.indexOf(needle, at + 1);
83
+ }
84
+ return null;
85
+ }
86
+ function output(result: Data, calls = 0, errors = 0): void {
87
+ console.log(JSON.stringify({ ...result, calls, errors }));
88
+ }
89
+
90
+ async function run(step: string): Promise<void> {
91
+ if (step === 'questions') {
92
+ if (input.questions !== undefined && !Array.isArray(input.questions)) throw new Error('questions must be an array');
93
+ const provided = strings(input.questions);
94
+ if (provided.length > 8 || (Array.isArray(input.questions) && provided.length !== input.questions.length)) throw new Error('questions must contain at most 8 nonempty strings');
95
+ if (provided.length) { output({ questions: provided }); return; }
96
+ try {
97
+ const response = reply(await invoke(['ask', '--json', `Generate exactly 5 potential customer questions about the category of ${brand}. Respond ONLY with a JSON array of 5 question strings. Do not include the brand name in any question.`]));
98
+ const generated = parseArray(response);
99
+ if (!Array.isArray(generated) || generated.length !== 5 || strings(generated).length !== 5 || strings(generated).some(q => q.toLocaleLowerCase().includes(brand.toLocaleLowerCase()))) throw new Error('ask returned invalid or branded questions');
100
+ output({ questions: generated }, 1);
101
+ } catch (e) { output({ outcome: 'fail', questions: [], error: errorLine(e) }, 1, 1); }
102
+ } else if (step === 'answers') {
103
+ const questions = strings(object(outputs.questions).questions);
104
+ if (!questions.length || questions.length > 8) throw new Error('missing questions');
105
+ const answers: Data[] = [];
106
+ let failures = 0;
107
+ for (const question of questions) for (const engine of engines) {
108
+ try { answers.push({ question, engine, answer: reply(await invoke(['ask', '--json', question], engine)) }); }
109
+ catch (e) { failures++; answers.push({ question, engine, error: errorLine(e) }); }
110
+ }
111
+ output({ answers, outcome: failures === answers.length ? 'fail' : 'ok' }, answers.length, failures);
112
+ } else if (step === 'web') {
113
+ const questions = strings(object(outputs.questions).questions);
114
+ const web: Data[] = [];
115
+ let failures = 0;
116
+ for (const question of questions) {
117
+ try { web.push({ question, urls: resultUrls(await invoke(['research', '--json', '--limit', '5', question])).slice(0, 5) }); }
118
+ catch (e) { failures++; web.push({ question, urls: [], error: errorLine(e) }); }
119
+ }
120
+ output({ web }, questions.length, failures);
121
+ } else if (step === 'score') {
122
+ const answers = rows(object(outputs.answers).answers);
123
+ const web = rows(object(outputs.web).web);
124
+ if (!answers.length) throw new Error('missing answers');
125
+ const domain = domains(text(input.domain));
126
+ const table = answers.map(cell => {
127
+ const answer = text(cell.answer);
128
+ if (cell.error) return { question: cell.question, engine: cell.engine, error: cell.error, mention: false, citation: domain ? false : null, rank: null };
129
+ const position = firstMention(answer, brand);
130
+ const candidates = [...urls(answer), ...strings(web.find(row => row.question === cell.question)?.urls)];
131
+ return { question: cell.question, engine: cell.engine, mention: position !== null,
132
+ citation: domain ? candidates.some(url => cited(url, domain)) || mentionsHost(answer, domain) : null, rank: position === null ? null : answer.slice(0, position).trim().split(/\s+/).filter(Boolean).length + 1 };
133
+ });
134
+ output({ table }, 0, table.filter(cell => cell.error).length);
135
+ } else if (step === 'suggest') {
136
+ const table = rows(object(outputs.score).table);
137
+ if (!table.length) throw new Error('missing score table');
138
+ const gaps = table.filter(cell => cell.error || !cell.mention || cell.citation === false).map(cell => ({ question: cell.question, engine: cell.engine, error: cell.error ?? null, mention: cell.mention, citation: cell.citation }));
139
+ if (!gaps.length) { output({ suggestions: [] }); return; }
140
+ try {
141
+ const raw = parseArray(reply(await invoke(['ask', '--json', `Using ONLY this score table, return a JSON array of 3 to 5 improvement suggestions. Each must have "suggestion" (FAQ question, FAQPage/Organization structured data, comparison document, etc.) and "evidence" containing a question and engine from the missing cells. Evidence cells: ${JSON.stringify(gaps)}. Score table: ${JSON.stringify(table)}`])));
142
+ if (!Array.isArray(raw) || raw.length < 3 || raw.length > 5) throw new Error('invalid suggestion count');
143
+ const suggestions = raw.map(item => {
144
+ const candidate = object(item);
145
+ const evidence = object(candidate.evidence);
146
+ const match = gaps.find(cell => cell.question === evidence.question && cell.engine === evidence.engine);
147
+ if (!text(candidate.suggestion) || !match) throw new Error('suggestion missing valid score evidence');
148
+ return { suggestion: text(candidate.suggestion), evidence: { question: match.question, engine: match.engine } };
149
+ });
150
+ output({ suggestions }, 1);
151
+ } catch (e) { output({ outcome: 'fail', suggestions: [], error: errorLine(e) }, 1, 1); }
152
+ } else if (step === 'report') {
153
+ const table = rows(object(outputs.score).table);
154
+ const answers = rows(object(outputs.answers).answers);
155
+ const suggestions = rows(object(outputs.suggest).suggestions);
156
+ if (!table.length) throw new Error('missing score table');
157
+ const outDir = text(input.outDir) || defaultOutDir;
158
+ const success = table.filter(cell => !cell.error);
159
+ const citationSummary = text(input.domain) ? `${success.filter(cell => cell.citation).length}/${success.length} cite ${text(input.domain)}.` : 'Citation unknown (no domain provided).';
160
+ const summary = `${brand}: ${success.filter(cell => cell.mention).length}/${success.length} available answers mention the brand; ${citationSummary}`;
161
+ const markdown = [`# GEO check — ${line(brand)}`, '', summary, '', '| Question | Engine | Mention | Citation | Rank | Error |', '| --- | --- | --- | --- | --- | --- |',
162
+ ...table.map(cell => `| ${line(cell.question)} | ${line(cell.engine)} | ${cell.mention ? 'yes' : 'no'} | ${cell.citation === null ? 'unknown' : cell.citation ? 'yes' : 'no'} | ${cell.rank ?? '—'} | ${line(cell.error)} |`),
163
+ '', '## Suggestions', ...suggestions.map(item => `- ${line(item.suggestion)} — ${line(object(item.evidence).question)} / ${line(object(item.evidence).engine)}`),
164
+ '', '## Answers', ...answers.map(cell => `### ${line(cell.question)} — ${line(cell.engine)}\n\n${line(cell.error) || text(cell.answer).slice(0, 400)}\n`), ''].join('\n');
165
+ mkdirSync(outDir, { recursive: true });
166
+ const md = join(outDir, 'report.md');
167
+ const json = join(outDir, 'report.json');
168
+ writeFileSync(md, markdown, { mode: 0o600 });
169
+ writeFileSync(json, JSON.stringify({ brand, domain: text(input.domain), summary, table, suggestions, answers }, null, 2) + '\n', { mode: 0o600 });
170
+ output({ report: md, reportJson: json, summary }, 0, table.filter(cell => cell.error).length);
171
+ } else throw new Error(`unknown geo-check step: ${step}`);
172
+ }
173
+
174
+ await run(process.argv[2] ?? '');
175
+ } catch (e) {
176
+ console.log(JSON.stringify({ outcome: 'fail', error: errorLine(e), calls: 0, errors: 1 }));
177
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
+ "name": "geo-check",
4
+ "version": "0.1.0",
5
+ "description": "Measure brand mentions and citations in AI answers and web results.",
6
+ "extensions": {
7
+ "ai.elanous": {
8
+ "graphs": [
9
+ "./graphs/geo-check.yaml"
10
+ ],
11
+ "capabilities": [
12
+ "fs:workdir",
13
+ "proc:bun",
14
+ "proc:elanous"
15
+ ],
16
+ "connectors": [],
17
+ "pricing": {
18
+ "model": "free"
19
+ },
20
+ "trust": "official",
21
+ "category": "Marketing"
22
+ }
23
+ }
24
+ }
@@ -12,6 +12,36 @@
12
12
  elanous graph run /absolute/path/to/installed/job-coach/graphs/report-enterprise.yaml --input '{"interview":"/private/path/to/fictional-enterprise.md"}' --json
13
13
  ```
14
14
 
15
+ ### 기업 모드 샘플 입력 (가상 정보만)
16
+
17
+ 다음은 **가상 인물** 민서 하와 **가상 회사** 푸른달 스튜디오의 예시다. 이미지 경로는 실제 사진이 아닌 플레이스홀더이며, 인터뷰 안의 참고용 표기일 뿐이다. 그래프 입력 필드는 `interview` 하나로, 이미지 파일을 읽거나 전송하는 입력은 없다. 비공개 작업 디렉터리에 `fictional-enterprise.md`로 저장한 뒤 위 명령의 `interview` 경로를 그 파일의 절대 경로로 바꾼다.
18
+
19
+ ```markdown
20
+ ## 기업 개요
21
+ 푸른달 스튜디오 (가상 회사). 이미지 참고 경로: /placeholder/images/fictional-company.png
22
+
23
+ ## 교육 대상
24
+ 민서 하 (가상 인물), 문서 기획 담당자
25
+
26
+ ## 현업 과제
27
+ 가상 행사 안내 문서의 기획
28
+
29
+ ## 교육 목표
30
+ 행사 안내 문서를 요구사항에 맞게 기획한다
31
+
32
+ ## 현재 역량
33
+ 문서 작성 경험 (자기보고)
34
+
35
+ ## 교육 제약
36
+ 예산과 승인 여부 미확인
37
+ ```
38
+
39
+ ### 입력·출력과 프라이버시 경계
40
+
41
+ 입력은 기업 모드 인터뷰 Markdown **파일의 비공개 절대 경로**를 담은 `--input '{"interview":"/private/path/to/fictional-enterprise.md"}'`이다. 위 여섯 `##` 섹션이 필요하다. 실제 직원 이름·사진·인사정보·사내 기밀을 샘플, 명령 인자 또는 공개 저장소에 넣지 않는다. 실제 인터뷰를 사용할 경우에도 민감 정보를 제거하고 접근 제한된 저장소에서만 실행한다. 그래프 러너는 입력·노드 stdout을 런 상태와 컨텍스트 JSON에 저장하므로 **파일 경로만 인자로 넘겨도 보고서와 런 상태에는 인터뷰에서 파생된 직무·역량이 남는다**. 파일 경로가 개인정보 비노출이나 무저장을 보장하는 것은 아니다.
42
+
43
+ 출력은 CLI의 상태 JSON(`statePath`와 `done`/`failed`) 및 그 상태 경로와 같은 디렉터리의 `<run-id>/report.md`다. 보고서는 아래 일곱 섹션으로 구성된다. NCS 조회가 실패하면 `failed`로 끝나며 확인되지 않은 과정·URL 및 내부 승인 여부는 `미확인`으로 남긴다. **No-send boundary:** 이 그래프 실행은 보고서를 로컬에 작성하는 경로이며 메신저·이메일로 보고서를 보내거나 마켓에 게시하는 단계가 없다. `--no-send` 플래그를 지원한다고 가정하지 않는다. 다만 NCS API 조회 및 외부 과정 조사는 네트워크 요청이므로 오프라인·무외부전송 실행이 아니다. 보고서 배포나 마켓 게시/시장 출시는 이 플러그인 README의 범위 밖이다.
44
+
15
45
  기업 보고서는 `기업 개요 및 교육 대상`, `교육 니즈`, `직무·과제 및 NCS`, `AI 적합성`, `역량 진단`, `교육 로드맵`, `교육 명세·운영 경로` 순서다. NCS 조회에 실패하면 매칭을 발명하지 않고 런을 실패로 종료한다. `run-report` 스킬은 개인 보고서 진입이며 기업 모드는 다섯 개 기업 스킬(`enterprise-needs`, `enterprise-task-analysis`, `enterprise-ai-fit`, `enterprise-competency`, `enterprise-roadmap`)과 기업 그래프를 이용한다.
16
46
 
17
47
  ## 실행
@@ -2,9 +2,9 @@
2
2
  "_comment": "조율 채널 세션 신원의 정본(09-30 역할 재편). id = 2글자 신원(발신 접두 **[TC]**) · title = 직함 · alias = 옛 한 글자(기록 읽기·전환기 발신용) · mark = 옛 표지. 세션을 더하려면 여기 한 줄만 더한다 — coord-post.sh · coord-channel-watch.sh · build-docs-site.ts · mission-phase-track.ts 가 이 파일을 읽는다.",
3
3
  "tracks": [
4
4
  { "id": "OP", "title": "COO", "alias": "S", "mark": "🅢", "owns": ["판올림", "컷", "체크리스트", "분배", "통합", "로드맵", "기획", "총괄", "전략", "인계", "release cut", "roadmap", "plan", "strategy"] },
5
- { "id": "MK", "title": "CMO", "alias": "T", "mark": "🅣", "owns": ["블로그", "콘텐츠", "발행", "광고", "프로모션", "소구점", "마케팅", "영상", "비디오", "티저", "하이퍼프레임", "렌더", "blog", "content", "marketing", "ads", "video", "teaser", "render", "B-roll", "상표", "변리사", "브랜드", "trademark", "brand"] },
5
+ { "id": "MK", "title": "CMO", "alias": "T", "mark": "🅣", "owns": ["블로그", "콘텐츠", "발행", "광고", "프로모션", "소구점", "마케팅", "영상", "비디오", "티저", "하이퍼프레임", "렌더", "blog", "content", "marketing", "ads", "video", "teaser", "render", "B-roll", "상표", "변리사", "브랜드", "trademark", "brand", "홈페이지", "웹사이트", "elanous.ai", "elanvital.ai", "website"] },
6
6
  { "id": "TC", "title": "CTO", "alias": "O", "mark": "🅞", "owns": ["하니스", "게이트", "원장", "리뷰", "관측", "릴리스 루프", "흡수", "Obsidian", "매뉴얼", "문서", "기술", "harness", "gate", "ledger", "review", "observability", "intake", "manual", "docs"] },
7
- { "id": "UX", "title": "CXO", "alias": "F", "mark": "🅕", "owns": ["PWA", "웹", "웹사이트", "elanous.ai", "탭", "화면", "UI", "프론트엔드", "안드로이드", "아이폰", "데스크톱", "텔레그램", "디스코드", "메신저", "봇", "frontend", "web", "tab", "android", "ios", "desktop", "telegram", "discord", "bot"] },
7
+ { "id": "UX", "title": "CXO", "alias": "F", "mark": "🅕", "owns": ["PWA", "웹", "탭", "화면", "UI", "프론트엔드", "안드로이드", "아이폰", "데스크톱", "텔레그램", "디스코드", "메신저", "봇", "frontend", "web", "tab", "android", "ios", "desktop", "telegram", "discord", "bot"] },
8
8
  { "id": "E", "mark": "🅔", "owns": [] }
9
9
  ]
10
10
  }
@@ -1,9 +1,8 @@
1
1
  # Daemon port literal baseline: non-comment lines in non-test src/**/*.ts, per file.
2
2
  # New files and increases fail. Migrate callers to the daemon endpoint resolver, then --update.
3
- # total=13 files=6
3
+ # total=12 files=5
4
4
  1 src/autopilot/proposal/draft-plan.ts
5
5
  6 src/index.ts
6
- 1 src/nexus/api/mcp-oauth-hint.ts
7
6
  1 src/nexus/default-port.ts
8
7
  2 src/task-orchestrator/surfaces/pod-command-job.ts
9
8
  2 src/task-orchestrator/surfaces/self-implement-pod.ts
@@ -0,0 +1,32 @@
1
+ // 계측 시험(`connectedDebugAndroidTest`)을 «어느 기기»에서 돌리나.
2
+ // ⛔ Gradle 은 시험이 끝나면 앱을 «지운다»(데이터까지). 실기기에서 돌면 대표 폰의 앱이 사라진다
3
+ // (2026-09-30 실측: pr land 의 android-gate 가 USB 로 꽂힌 폴드 8 에서 돌아 설치본이 지워졌다).
4
+ // ⇒ 에뮬레이터(`emulator-*`)만 고른다. 실기기를 쓰려면 serial 을 «명시»해야 한다.
5
+
6
+ export type TestDevicePick =
7
+ | { kind: 'use'; serial: string }
8
+ | { kind: 'skip'; reason: string };
9
+
10
+ /** `adb devices` 출력에서 «device» 상태인 serial 들. */
11
+ export function attachedSerials(adbDevicesOutput: string): string[] {
12
+ return adbDevicesOutput
13
+ .split('\n')
14
+ .slice(1)
15
+ .map((line) => line.trim().split(/\s+/))
16
+ .filter((cols) => cols.length >= 2 && cols[1] === 'device')
17
+ .map((cols) => cols[0]!);
18
+ }
19
+
20
+ export function pickInstrumentedTestDevice(adbDevicesOutput: string, explicitSerial?: string): TestDevicePick {
21
+ const serials = attachedSerials(adbDevicesOutput);
22
+ if (explicitSerial) {
23
+ return serials.includes(explicitSerial)
24
+ ? { kind: 'use', serial: explicitSerial }
25
+ : { kind: 'skip', reason: `지정한 기기 ${explicitSerial} 가 연결돼 있지 않다` };
26
+ }
27
+ const emulator = serials.find((s) => s.startsWith('emulator-'));
28
+ if (emulator) return { kind: 'use', serial: emulator };
29
+ return serials.length > 0
30
+ ? { kind: 'skip', reason: `에뮬레이터가 없다 — 실기기 ${serials.length}대에서는 돌리지 않는다(시험이 앱과 데이터를 지운다) · 실기기를 쓰려면 ELANOUS_ANDROID_TEST_SERIAL=<serial>` }
31
+ : { kind: 'skip', reason: '연결된 기기·에뮬레이터가 없다' };
32
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bun
2
+ import { addItem, checklistGate, listChecklist, summarizeChecklist } from '../../src/release-loop/checklist.js';
3
+ import { emitNodeResult, errorResult, readGraphContext, type GraphContext } from './node-verdict.js';
4
+
5
+ export function runChecklistGate(context: GraphContext = readGraphContext()) {
6
+ const version = context.input.version;
7
+ const data = listChecklist(version);
8
+ const gate = checklistGate(version);
9
+ const counts = summarizeChecklist(data);
10
+ const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}`;
11
+ if (gate.ok && gate.moved.length) {
12
+ const [major, minor, patch] = version.split('.').map(Number);
13
+ const next = `${major}.${minor}.${patch! + 1}`;
14
+ for (const id of gate.moved) {
15
+ const item = data.items.find((candidate) => candidate.id === id)!;
16
+ if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
17
+ try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}) }); }
18
+ catch (error) {
19
+ if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
20
+ }
21
+ }
22
+ }
23
+ return { outcome: gate.ok ? 'ok' as const : 'fail' as const, verdict: gate.ok ? 'pass' as const : 'fail' as const, summary, ...gate };
24
+ }
25
+
26
+ if (import.meta.main) {
27
+ try {
28
+ const result = runChecklistGate();
29
+ emitNodeResult(result);
30
+ process.exitCode = result.outcome === 'ok' ? 0 : 1;
31
+ } catch (error) {
32
+ emitNodeResult(errorResult(error));
33
+ process.exitCode = 2;
34
+ }
35
+ }