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.
- package/README.md +5 -4
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7527.943753b6ed829f09.js → 1883.c91a2648af4842fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/{4853-35fdbe6bdbcdc0fc.js → 3165-95515ca5f299a120.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 3995-bee512a7063036ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5329.8b7e4bca684b6eec.js +5 -0
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9238.2cd5f47f4bf7a2c3.js → 8250.4a2c5553a07cdf47.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-84cd419494e16b0f.js → page-c3890e90bd232732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{06ef2eba-d1b99ac6d2ea663c.js → beb5cf10-9adcc10b83930090.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +21 -21
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +21 -21
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +21 -21
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +20 -20
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +20 -20
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +20 -20
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +21 -21
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +21 -21
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +21 -21
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +20 -20
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +21 -21
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +21 -21
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +20 -20
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +20 -20
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +21 -21
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +21 -21
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +21 -21
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +21 -21
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +20 -20
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +21 -21
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +21 -21
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +21 -21
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +21 -21
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +21 -21
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +20 -20
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +21 -21
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +21 -21
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +21 -21
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +21 -21
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +21 -21
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +21 -21
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +21 -21
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +21 -21
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/package.json +1 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/README.md +30 -0
- package/scripts/coord-tracks.json +2 -2
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +46 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/ux/record-page.ts +4 -1
- package/skills/explainer-video/SKILL.md +18 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +58 -16
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +27 -0
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/release-cli.ts +14 -11
- package/src/domains/outbound-alert.ts +5 -5
- package/src/field/field-media.ts +245 -0
- package/src/index.ts +18 -24
- package/src/nexus/api/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +50 -1
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +9 -0
- package/src/notify/llm-budget-alert.ts +119 -0
- package/src/plugins/maker/plugin-maker.ts +208 -0
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +28 -2
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/session/index.ts +23 -10
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
- package/src/telegram.ts +108 -1
- package/src/user-config.ts +4 -0
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1751.50b96ea9c37b49b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0ac5d053f0444d21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-bccce0babceae30f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
- /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { relative } from 'node:path';
|
|
1
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { hasTerminalRunStatus, loadRunLedger, runLedgerDir, runLedgerPath, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
|
|
4
|
+
import { summarizeTerminalLedger } from '../../self-implement/run-ledger-compact.js';
|
|
3
5
|
import { debug } from '../../debug/log.js';
|
|
4
6
|
import { enqueueSoftStop } from '../../harness/control-inbox.js';
|
|
5
7
|
import { listHarnessScreens, readHarnessScreenTail } from '../../harness/harness-screen.js';
|
|
@@ -103,6 +105,10 @@ export interface HarnessApiDeps {
|
|
|
103
105
|
readonly createAcceptanceId?: () => string;
|
|
104
106
|
readonly log?: (event: string, data: Record<string, unknown>) => void;
|
|
105
107
|
readonly createFeedbackEmitter?: (acceptanceId: string) => (env: FeedbackEnvelope) => void | Promise<void>;
|
|
108
|
+
/** 시험 심 — `?finishedSince=` 가 읽는 원장 디렉터리(기본 `runLedgerDir()`). */
|
|
109
|
+
readonly ledgerDir?: string;
|
|
110
|
+
/** 시험 심 — `?finishedSince=` 의 `landed`(gh 실행기·저장소·시간 초과). */
|
|
111
|
+
readonly landed?: LandedPrsDeps;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function json(body: unknown, status = 200): Response {
|
|
@@ -338,10 +344,218 @@ export function handleHarnessAskStatusGet(req: Request, _metaApi: HarnessMetaApi
|
|
|
338
344
|
});
|
|
339
345
|
}
|
|
340
346
|
|
|
341
|
-
/**
|
|
342
|
-
export
|
|
347
|
+
/** One terminated run for `GET /v1/harness/runs?finishedSince=` (iPhone «오늘 끝난 일» · 2026-09-30). */
|
|
348
|
+
export interface HarnessFinishedRun {
|
|
349
|
+
runId: string;
|
|
350
|
+
status: string;
|
|
351
|
+
endedAt: string;
|
|
352
|
+
/** 마지막 `run-status` 의 `stage`(예: `merged`·`pr-declined`) — 칩을 더 정확히 고를 때. */
|
|
353
|
+
stage?: string;
|
|
354
|
+
objective?: string;
|
|
355
|
+
prUrl?: string;
|
|
356
|
+
/** `merged` 이벤트의 값 — 이벤트가 없으면 칸이 없다(«병합 안 됨»으로 접지 않는다). */
|
|
357
|
+
merged?: boolean;
|
|
358
|
+
/** 마지막 `merge-decision` 의 `decision`(`auto`·`hitl`) — 병합 «결정»이지 병합 «사실»이 아니다. */
|
|
359
|
+
mergeDecision?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const FINISHED_RUNS_LIMIT = 20;
|
|
363
|
+
export const FINISHED_RUNS_MAX_FILES = 300;
|
|
364
|
+
export const FINISHED_RUNS_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
365
|
+
const FINISHED_OBJECTIVE_MAX_CHARS = 160;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 원장 디렉터리에서 `sinceMs` 이후에 쓰인 «끝난» 런을 요약한다. 읽기는 묶여 있다 —
|
|
369
|
+
* mtime 최신 {@link FINISHED_RUNS_MAX_FILES} 개만 보고, {@link FINISHED_RUNS_MAX_FILE_BYTES} 를 넘는 파일은 건너뛴다.
|
|
370
|
+
*/
|
|
371
|
+
export function collectFinishedRuns(dir: string, sinceMs: number): { runs: HarnessFinishedRun[]; scannedFiles: number; skippedFiles: number } {
|
|
372
|
+
let names: string[];
|
|
373
|
+
try { names = readdirSync(dir); }
|
|
374
|
+
catch { return { runs: [], scannedFiles: 0, skippedFiles: 0 }; }
|
|
375
|
+
const candidates: Array<{ runId: string; mtimeMs: number; size: number }> = [];
|
|
376
|
+
for (const name of names) {
|
|
377
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
378
|
+
try {
|
|
379
|
+
const st = statSync(join(dir, name));
|
|
380
|
+
if (st.isFile() && st.mtimeMs >= sinceMs) candidates.push({ runId: name.slice(0, -'.jsonl'.length), mtimeMs: st.mtimeMs, size: st.size });
|
|
381
|
+
} catch { /* 사이에 지워졌다 */ }
|
|
382
|
+
}
|
|
383
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
384
|
+
const scanned = candidates.slice(0, FINISHED_RUNS_MAX_FILES);
|
|
385
|
+
let skippedFiles = candidates.length - scanned.length;
|
|
386
|
+
const runs: HarnessFinishedRun[] = [];
|
|
387
|
+
for (const file of scanned) {
|
|
388
|
+
if (file.size > FINISHED_RUNS_MAX_FILE_BYTES) { skippedFiles += 1; continue; }
|
|
389
|
+
let entries: RunLedgerEntry[] | null;
|
|
390
|
+
try { entries = loadRunLedger(file.runId, dir); }
|
|
391
|
+
catch { skippedFiles += 1; continue; }
|
|
392
|
+
if (!entries?.length || !hasTerminalRunStatus(entries)) continue;
|
|
393
|
+
let summary: ReturnType<typeof summarizeTerminalLedger>;
|
|
394
|
+
try { summary = summarizeTerminalLedger(file.runId, entries); }
|
|
395
|
+
catch { skippedFiles += 1; continue; }
|
|
396
|
+
if (!(Date.parse(summary.endedAt) >= sinceMs)) continue;
|
|
397
|
+
const feature = entries.find((entry) => entry.event === 'start' && typeof entry.data.feature === 'string')?.data.feature as string | undefined;
|
|
398
|
+
const objective = feature?.trim().slice(0, FINISHED_OBJECTIVE_MAX_CHARS);
|
|
399
|
+
const mergedEvent = [...entries].reverse().find((entry) => entry.event === 'merged' && typeof entry.data.merged === 'boolean');
|
|
400
|
+
const decision = [...entries].reverse().find((entry) => entry.event === 'merge-decision' && typeof entry.data.decision === 'string');
|
|
401
|
+
const statusEntry = [...entries].reverse().find((entry) => entry.event === 'run-status' && typeof entry.data.stage === 'string');
|
|
402
|
+
runs.push({
|
|
403
|
+
runId: summary.runId,
|
|
404
|
+
status: summary.status,
|
|
405
|
+
endedAt: summary.endedAt,
|
|
406
|
+
...(statusEntry ? { stage: statusEntry.data.stage as string } : {}),
|
|
407
|
+
...(objective ? { objective } : {}),
|
|
408
|
+
...(summary.prUrl ? { prUrl: summary.prUrl } : {}),
|
|
409
|
+
...(mergedEvent ? { merged: mergedEvent.data.merged as boolean } : {}),
|
|
410
|
+
...(decision ? { mergeDecision: decision.data.decision as string } : {}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
runs.sort((a, b) => Date.parse(b.endedAt) - Date.parse(a.endedAt));
|
|
414
|
+
return { runs: runs.slice(0, FINISHED_RUNS_LIMIT), scannedFiles: scanned.length, skippedFiles };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 도는 런의 목표 — 그 런 원장의 첫 `start.data.feature`(≤160자). 원장 경로는 그 항목이 준 디렉터리들만 본다(런 수만큼만 읽는다). */
|
|
418
|
+
export function runObjectiveFromLedgers(runId: string, directories: readonly string[]): string | undefined {
|
|
419
|
+
for (const dir of directories) {
|
|
420
|
+
try {
|
|
421
|
+
if (statSync(runLedgerPath(runId, dir)).size > FINISHED_RUNS_MAX_FILE_BYTES) continue;
|
|
422
|
+
const feature = loadRunLedger(runId, dir)?.find((entry) => entry.event === 'start' && typeof entry.data.feature === 'string')?.data.feature;
|
|
423
|
+
const objective = typeof feature === 'string' ? feature.trim().slice(0, FINISHED_OBJECTIVE_MAX_CHARS) : '';
|
|
424
|
+
if (objective) return objective;
|
|
425
|
+
} catch { /* 없거나 못 읽음 — 다음 디렉터리 */ }
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── 오늘 착지(= main 에 병합된 PR) — 원장은 Pod·시험 우주의 런을 못 보므로 GitHub 이 정본이다 ──
|
|
431
|
+
|
|
432
|
+
/** `landed` 한 줄 — `gh pr list --json number,title,url,mergedAt` 모양 그대로. */
|
|
433
|
+
export interface HarnessLandedPr {
|
|
434
|
+
number: number;
|
|
435
|
+
title: string;
|
|
436
|
+
url: string;
|
|
437
|
+
mergedAt: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export const LANDED_PRS_LIMIT = 30;
|
|
441
|
+
export const LANDED_PRS_TIMEOUT_MS = 4_000;
|
|
442
|
+
const LANDED_CACHE_MS = 60_000;
|
|
443
|
+
const landedCache = new Map<string, { at: number; prs: HarnessLandedPr[]; truncated: boolean }>();
|
|
444
|
+
let cachedRepoSlug: string | null | undefined;
|
|
445
|
+
|
|
446
|
+
export function _resetLandedPrsCacheForTest(): void {
|
|
447
|
+
landedCache.clear();
|
|
448
|
+
cachedRepoSlug = undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface LandedPrsDeps {
|
|
452
|
+
/** gh 실행기(기본 = `git-fs/gh-cli` 의 재시도 게이트웨이 · 비동기). */
|
|
453
|
+
readonly runGh?: (args: string[]) => Promise<{ ok: boolean; exitCode: number; stdout: Buffer | string; stderr: Buffer | string }>;
|
|
454
|
+
/** 저장소 `owner/name` — 기본은 데몬이 아는 저장소 뿌리(`harness.defaultRepo` → cwd)의 origin. */
|
|
455
|
+
readonly repoSlug?: () => Promise<string | null>;
|
|
456
|
+
readonly timeoutMs?: number;
|
|
457
|
+
readonly now?: () => number;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function defaultRepoSlug(): Promise<string | null> {
|
|
461
|
+
if (cachedRepoSlug !== undefined) return cachedRepoSlug;
|
|
462
|
+
const [{ resolveWorktreesRepoRoot }, { slugFromRemote }] = await Promise.all([import('./worktrees.js'), import('./live-shipped.js')]);
|
|
463
|
+
const root = resolveWorktreesRepoRoot();
|
|
464
|
+
if (!root) return (cachedRepoSlug = null);
|
|
465
|
+
const proc = Bun.spawn(['git', '-C', root, 'remote', 'get-url', 'origin'], { stdout: 'pipe', stderr: 'ignore' });
|
|
466
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
467
|
+
return (cachedRepoSlug = code === 0 ? slugFromRemote(out) : null);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function defaultRunGh(args: string[]) {
|
|
471
|
+
const { runGhCliWithResultAsync } = await import('../../git-fs/gh-cli.js');
|
|
472
|
+
return runGhCliWithResultAsync(args);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isLandedPr(value: unknown): value is HarnessLandedPr {
|
|
476
|
+
const v = value as Partial<HarnessLandedPr> | null;
|
|
477
|
+
return !!v && typeof v.number === 'number' && typeof v.title === 'string' && typeof v.url === 'string' && typeof v.mergedAt === 'string';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** `sinceMs` 이후 main 에 병합된 PR(최신순 ≤30). 실패·시간 초과는 던지지 않고 `error` 로 돌려준다. 성공만 60초 캐시한다. */
|
|
481
|
+
export async function listLandedPrs(sinceMs: number, deps: LandedPrsDeps = {}): Promise<{ prs: HarnessLandedPr[]; error?: string; cached?: boolean; truncated?: boolean }> {
|
|
482
|
+
const now = deps.now?.() ?? Date.now();
|
|
483
|
+
const sinceIso = new Date(Math.floor(sinceMs / 60_000) * 60_000).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
484
|
+
const hit = landedCache.get(sinceIso);
|
|
485
|
+
if (hit && now - hit.at < LANDED_CACHE_MS) return { prs: hit.prs, cached: true, ...(hit.truncated ? { truncated: true } : {}) };
|
|
486
|
+
const timeoutMs = deps.timeoutMs ?? LANDED_PRS_TIMEOUT_MS;
|
|
487
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
488
|
+
const timeout = new Promise<{ prs: HarnessLandedPr[]; error: string }>((resolve) => {
|
|
489
|
+
timer = setTimeout(() => resolve({ prs: [], error: `timeout ${timeoutMs}ms` }), timeoutMs);
|
|
490
|
+
});
|
|
491
|
+
const work = (async (): Promise<{ prs: HarnessLandedPr[]; error?: string; truncated?: boolean }> => {
|
|
492
|
+
try {
|
|
493
|
+
const slug = await (deps.repoSlug ?? defaultRepoSlug)();
|
|
494
|
+
if (!slug) return { prs: [], error: 'no-repository' };
|
|
495
|
+
const result = await (deps.runGh ?? defaultRunGh)([
|
|
496
|
+
'pr', 'list', '--repo', slug, '--state', 'merged', '--base', 'main',
|
|
497
|
+
'--search', `merged:>=${sinceIso}`,
|
|
498
|
+
'--json', 'number,title,url,mergedAt', '--limit', String(LANDED_PRS_LIMIT),
|
|
499
|
+
]);
|
|
500
|
+
if (!result.ok) {
|
|
501
|
+
const stderr = String(result.stderr).trim().split('\n')[0] ?? '';
|
|
502
|
+
return { prs: [], error: `gh failed rc=${result.exitCode}${stderr ? `: ${redactSecretText(stderr).slice(0, 120)}` : ''}` };
|
|
503
|
+
}
|
|
504
|
+
const parsed: unknown = JSON.parse(String(result.stdout));
|
|
505
|
+
if (!Array.isArray(parsed)) return { prs: [], error: 'gh returned non-array' };
|
|
506
|
+
const prs = parsed.filter(isLandedPr)
|
|
507
|
+
.filter((pr) => Date.parse(pr.mergedAt) >= sinceMs)
|
|
508
|
+
.sort((a, b) => Date.parse(b.mergedAt) - Date.parse(a.mergedAt));
|
|
509
|
+
// gh 가 상한만큼 줬으면 더 있을 수 있다 — «전부»라고 말하지 않는다.
|
|
510
|
+
const truncated = parsed.length >= LANDED_PRS_LIMIT;
|
|
511
|
+
landedCache.set(sinceIso, { at: now, prs, truncated });
|
|
512
|
+
return { prs, ...(truncated ? { truncated: true } : {}) };
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return { prs: [], error: message(error).slice(0, 120) };
|
|
515
|
+
}
|
|
516
|
+
})();
|
|
517
|
+
try {
|
|
518
|
+
return await Promise.race([work, timeout]);
|
|
519
|
+
} finally {
|
|
520
|
+
if (timer) clearTimeout(timer);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Return the structured shared running-runs observation without human rendering.
|
|
525
|
+
* `?finishedSince=<epoch ms>` 가 있으면(iPhone «오늘» · 2026-09-30) 셋을 더한다 —
|
|
526
|
+
* `finished`(이 우주 원장의 끝난 런 · 최신순 ≤20) · `landed`(그 시각 뒤 main 에 병합된 PR · ≤30 · 실패면 `[]` ⊕ `landedError`) ·
|
|
527
|
+
* 도는 항목마다 `objective`(그 런 원장의 `start.feature`). 없으면 응답은 종전 그대로다(동기 응답). */
|
|
528
|
+
export function handleHarnessRunsGet(req: Request, _metaApi: HarnessMetaApi, deps: HarnessApiDeps = {}): Response | Promise<Response> {
|
|
343
529
|
const runs: RunningRunsResult = (deps.queryRunningRuns ?? queryRunningRuns)({ includeTest: false });
|
|
344
|
-
|
|
530
|
+
const raw = new URL(req.url).searchParams.get('finishedSince');
|
|
531
|
+
if (raw === null) return json(runs);
|
|
532
|
+
const sinceMs = Number(raw);
|
|
533
|
+
if (!raw.trim() || !Number.isFinite(sinceMs) || sinceMs < 0) {
|
|
534
|
+
return json({ error: 'usage: GET /v1/harness/runs?finishedSince=<epoch ms>' }, 400);
|
|
535
|
+
}
|
|
536
|
+
return (async () => {
|
|
537
|
+
const landedWork = listLandedPrs(sinceMs, deps.landed ?? {});
|
|
538
|
+
const dir = deps.ledgerDir ?? runLedgerDir();
|
|
539
|
+
const finished = collectFinishedRuns(dir, sinceMs);
|
|
540
|
+
const entries = runs.entries.map((entry) => {
|
|
541
|
+
const objective = runObjectiveFromLedgers(entry.runId, entry.ledgerDirectories?.length ? entry.ledgerDirectories : [dir]);
|
|
542
|
+
return objective ? { ...entry, objective } : entry;
|
|
543
|
+
});
|
|
544
|
+
const landed = await landedWork;
|
|
545
|
+
debug.log('harness-http', 'runs-finished', {
|
|
546
|
+
sinceMs, count: finished.runs.length, scannedFiles: finished.scannedFiles, skippedFiles: finished.skippedFiles,
|
|
547
|
+
landed: landed.prs.length, landedCached: landed.cached === true, ...(landed.error ? { landedError: landed.error } : {}),
|
|
548
|
+
});
|
|
549
|
+
return json({
|
|
550
|
+
...runs,
|
|
551
|
+
entries,
|
|
552
|
+
finished: finished.runs,
|
|
553
|
+
finishedObservation: { sinceMs, scannedFiles: finished.scannedFiles, skippedFiles: finished.skippedFiles, limit: FINISHED_RUNS_LIMIT },
|
|
554
|
+
landed: landed.prs,
|
|
555
|
+
...(landed.truncated ? { landedTruncated: true } : {}),
|
|
556
|
+
...(landed.error ? { landedError: landed.error } : {}),
|
|
557
|
+
});
|
|
558
|
+
})();
|
|
345
559
|
}
|
|
346
560
|
|
|
347
561
|
/** Return the human-readable skeleton events persisted for one harness run. */
|
|
@@ -198,6 +198,7 @@ import { handleMcpResourceGet, MCP_RESOURCE_ROUTE_PATH } from './mcp-resource-ro
|
|
|
198
198
|
import { APPROVALS_MERGES_PATH, DESIGN_DIRECTION_PATH, DESIGN_PREVIEW_PATH_PREFIX, DESIGN_PREVIEWS_PATH, DESIGN_SYSTEM_PATH, IPA_PATH_PREFIX, MANIFEST_PATH } from './rest-route-paths.js';
|
|
199
199
|
import { handleMergeApprovals } from './merge-approvals.js';
|
|
200
200
|
import { GRAPH_APPROVALS_PATH, handleGraphApprovals } from './graph-approvals.js';
|
|
201
|
+
import { FIELD_SERVER_MAX_BODY_BYTES, FIELD_UPLOADS_PATH, handleFieldUploads } from './field-uploads.js';
|
|
201
202
|
import { handleLiveDetail, LIVE_DETAIL_PATH } from './live-detail.js';
|
|
202
203
|
import { handleLiveShipped, LIVE_SHIPPED_PATH } from './live-shipped.js';
|
|
203
204
|
import { handleMcpWidgetCall, MCP_WIDGET_CALL_ROUTE_PATH, persistWidgetTurnToSessionStore } from './mcp-widget-call-route.js';
|
|
@@ -739,6 +740,8 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
739
740
|
port,
|
|
740
741
|
hostname,
|
|
741
742
|
idleTimeout: 255,
|
|
743
|
+
// Bun 기본 128 MB 로는 현장 업로드(파일 200 MB · 요청 500 MB · JSON base64)가 못 들어온다.
|
|
744
|
+
maxRequestBodySize: FIELD_SERVER_MAX_BODY_BYTES,
|
|
742
745
|
fetch: async (req: Request, srv: unknown) =>
|
|
743
746
|
(await routeRequest(req, opts, srv as BunServerLike, bridge, devProxyRef, { hostname, port })) ??
|
|
744
747
|
new Response(null, { status: 101 }),
|
|
@@ -922,6 +925,12 @@ export async function routeRequest(
|
|
|
922
925
|
authReason: (request) => (opts.metaApi ? bearerCredential(request, opts.metaApi) : undefined),
|
|
923
926
|
});
|
|
924
927
|
}
|
|
928
|
+
// 현장 업로드(owner 전용) — GET 목록 · POST 저장. 저장 규칙 정본 = src/field/field-media.ts.
|
|
929
|
+
if (pathname === FIELD_UPLOADS_PATH && (method === 'GET' || method === 'POST')) {
|
|
930
|
+
return handleFieldUploads(req, {
|
|
931
|
+
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
925
934
|
if (pathname === APPROVALS_MERGES_PATH || pathname.startsWith(`${APPROVALS_MERGES_PATH}/`)) {
|
|
926
935
|
return handleMergeApprovals(req, {
|
|
927
936
|
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// NT1 — LLM 예산 알림 «모양» (UX · 대표 2026-09-30 22:30 재요청 · OP 결정 B6 ⊕ NT1).
|
|
2
|
+
// 숫자·정책은 TC 의 B6 로더가 채운다(`내부 문서 `RFC-llm-policy-single-file-2026-09-30``) — 이 파일은 문구만 정한다.
|
|
3
|
+
//
|
|
4
|
+
// 규칙 셋(대표 가 받은 알림이 어긴 것들):
|
|
5
|
+
// ① 한 알림 = 한 결정·한 행동 — 맨 윗줄이 «무엇이 바뀌었나», 마지막 줄이 «할 일».
|
|
6
|
+
// ② 바뀐 게 없으면 침묵 — 같은 상태(key)면 send=false.
|
|
7
|
+
// ③ 모순 금지 — «한도 참»과 «크레딧으로 진행 중»을 한 알림에 같이 쓰지 않는다. 상태 하나만 고른다.
|
|
8
|
+
// ⊕ 내부 낱말 0 — «자»·«판정»·계정 내부 이름 나열을 쓰지 않는다(계정은 «계정 N개»로 센다).
|
|
9
|
+
|
|
10
|
+
export interface LlmBudgetAccount {
|
|
11
|
+
name: string;
|
|
12
|
+
/** 구독 잔량 %(0~100). 모르면 undefined. */
|
|
13
|
+
subscriptionRemainingPct?: number;
|
|
14
|
+
/** 이 계정의 크레딧 잔액. */
|
|
15
|
+
credits?: number;
|
|
16
|
+
resetInHours?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LlmBudgetSnapshot {
|
|
20
|
+
/** ISO 시각(Asia/Seoul 로 날짜를 자른다). */
|
|
21
|
+
at: string;
|
|
22
|
+
credits: {
|
|
23
|
+
total: number;
|
|
24
|
+
usedToday: number;
|
|
25
|
+
/** 하루 목표 소진량(정책 `credits.pace`). */
|
|
26
|
+
paceTarget: number;
|
|
27
|
+
/** ISO 날짜 — 크레딧이 사라지는 날. */
|
|
28
|
+
expiresAt?: string;
|
|
29
|
+
/** 정책이 «크레딧 먼저»인가(`until` 전). */
|
|
30
|
+
useFirst?: boolean;
|
|
31
|
+
};
|
|
32
|
+
accounts: LlmBudgetAccount[];
|
|
33
|
+
selected: { account?: string; reason: 'subscription' | 'credits' | 'fallback' | 'none' };
|
|
34
|
+
fallback?: { provider: string; remainingPct?: number };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LlmBudgetAlert {
|
|
38
|
+
send: boolean;
|
|
39
|
+
/** 상태 열쇠 — 다음 호출의 prevKey 로 넘긴다. */
|
|
40
|
+
key: string;
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EXPIRY_WARN_DAYS = 7;
|
|
45
|
+
/** 이 시각(서울) 뒤에야 «오늘 페이스가 느리다»를 말한다 — 아침에 0 인 건 정상이다. */
|
|
46
|
+
const PACE_CHECK_HOUR = 18;
|
|
47
|
+
|
|
48
|
+
const n = (v: number) => Math.round(v).toLocaleString('ko-KR');
|
|
49
|
+
|
|
50
|
+
function seoulParts(iso: string): { date: string; hour: number } {
|
|
51
|
+
const d = new Date(iso);
|
|
52
|
+
const f = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23' });
|
|
53
|
+
const p = Object.fromEntries(f.formatToParts(d).map((x) => [x.type, x.value]));
|
|
54
|
+
return { date: `${p.year}-${p.month}-${p.day}`, hour: Number(p.hour) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function daysBetween(fromIso: string, toIso: string): number {
|
|
58
|
+
const a = Date.parse(seoulParts(fromIso).date);
|
|
59
|
+
const b = Date.parse(seoulParts(toIso).date);
|
|
60
|
+
return Math.round((b - a) / 86_400_000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mmdd(iso: string): string {
|
|
64
|
+
const { date } = seoulParts(iso);
|
|
65
|
+
return `${date.slice(5, 7)}-${date.slice(8, 10)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function creditsLine(s: LlmBudgetSnapshot): string {
|
|
69
|
+
const parts = [`오늘 ${n(s.credits.usedToday)} / 목표 ${n(s.credits.paceTarget)}`, `남은 크레딧 ${n(s.credits.total)}`];
|
|
70
|
+
if (s.credits.expiresAt) parts.push(`${mmdd(s.credits.expiresAt)} 만료`);
|
|
71
|
+
return parts.join(' · ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 지금 상태 하나를 고른다(위에서부터 먼저 맞는 것). 아무것도 아니면 침묵. */
|
|
75
|
+
export function buildLlmBudgetAlert(now: LlmBudgetSnapshot, prevKey?: string): LlmBudgetAlert {
|
|
76
|
+
const { date, hour } = seoulParts(now.at);
|
|
77
|
+
const accounts = now.accounts.length;
|
|
78
|
+
let key = 'ok';
|
|
79
|
+
let lines: string[] = [];
|
|
80
|
+
|
|
81
|
+
const expiryDays = now.credits.expiresAt ? daysBetween(now.at, now.credits.expiresAt) : undefined;
|
|
82
|
+
|
|
83
|
+
if (now.selected.reason === 'none') {
|
|
84
|
+
key = 'blocked';
|
|
85
|
+
lines = [
|
|
86
|
+
'⛔ LLM 을 쓸 곳이 없어 작업이 멈췄습니다.',
|
|
87
|
+
`구독 계정 ${accounts}개 한도 · 크레딧·대체 모델도 못 씁니다.`,
|
|
88
|
+
'할 일: 크레딧 사용을 허락하거나 대체 모델 키를 확인해 주세요.',
|
|
89
|
+
];
|
|
90
|
+
} else if (now.selected.reason === 'fallback' && now.fallback) {
|
|
91
|
+
const soonest = now.accounts.map((a) => a.resetInHours).filter((h): h is number => typeof h === 'number').sort((a, b) => a - b)[0];
|
|
92
|
+
key = `fallback:${now.fallback.provider}`;
|
|
93
|
+
lines = [
|
|
94
|
+
`⚠️ codex 를 못 써서 ${now.fallback.provider} 로 일하고 있습니다${typeof now.fallback.remainingPct === 'number' ? `(남은 ${Math.round(now.fallback.remainingPct)}%)` : ''}.`,
|
|
95
|
+
soonest !== undefined ? `codex 는 ${Math.ceil(soonest)}시간 뒤 돌아옵니다.` : 'codex 가 언제 돌아오는지 아직 모릅니다.',
|
|
96
|
+
'할 일: 없음 — 참고만 하세요.',
|
|
97
|
+
];
|
|
98
|
+
} else if (expiryDays !== undefined && expiryDays <= EXPIRY_WARN_DAYS && now.credits.total > 0) {
|
|
99
|
+
key = `expiry:${Math.max(0, expiryDays)}`;
|
|
100
|
+
lines = [
|
|
101
|
+
`💳 크레딧 ${n(now.credits.total)} 이 ${expiryDays <= 0 ? '오늘' : `${expiryDays}일 뒤`} 사라집니다.`,
|
|
102
|
+
creditsLine(now),
|
|
103
|
+
'할 일: 무거운 작업을 지금 맡기면 크레딧으로 돕니다.',
|
|
104
|
+
];
|
|
105
|
+
} else if (now.credits.useFirst && now.credits.total > 0 && hour >= PACE_CHECK_HOUR && now.credits.usedToday < now.credits.paceTarget * 0.5) {
|
|
106
|
+
// 하루 한 번만 — 날짜를 열쇠에 넣는다.
|
|
107
|
+
key = `pace-behind:${date}`;
|
|
108
|
+
lines = [
|
|
109
|
+
now.selected.reason === 'credits'
|
|
110
|
+
? '💳 크레딧을 먼저 쓰는 중인데 오늘 쓴 양이 목표의 절반이 안 됩니다.'
|
|
111
|
+
: '💳 크레딧을 먼저 쓰라는 방침인데 오늘은 구독 계정이 먼저 쓰였습니다.',
|
|
112
|
+
creditsLine(now),
|
|
113
|
+
now.selected.reason === 'credits' ? '할 일: 없음 — 맡긴 일이 적었던 날입니다.' : '할 일: 없음 — 기술 담당이 순서를 고치는 중입니다.',
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const send = key !== 'ok' && key !== prevKey;
|
|
118
|
+
return { send, key, text: lines.join('\n') };
|
|
119
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
5
|
+
import { debug } from '../../debug/log.js';
|
|
6
|
+
import { runGraph } from '../../graph-runner/runner.js';
|
|
7
|
+
import { parseGraphTemplateYaml } from '../../self-implement/graph-yaml.js';
|
|
8
|
+
import { installPlugin } from '../install/plugin-install.js';
|
|
9
|
+
|
|
10
|
+
const CAPABILITIES = ['fs:workdir', 'proc:bun', 'proc:elanous'];
|
|
11
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
|
|
13
|
+
export interface MakePluginDeps {
|
|
14
|
+
codex: (dir: string, prompt: string) => Promise<void>;
|
|
15
|
+
install?: typeof installPlugin;
|
|
16
|
+
runGraph?: typeof runGraph;
|
|
17
|
+
}
|
|
18
|
+
export interface MakePluginOptions {
|
|
19
|
+
request: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
parentDir?: string;
|
|
22
|
+
run?: boolean;
|
|
23
|
+
input?: unknown;
|
|
24
|
+
deps?: MakePluginDeps;
|
|
25
|
+
}
|
|
26
|
+
export interface MakePluginResult {
|
|
27
|
+
status: 'installed' | 'ran' | 'failed';
|
|
28
|
+
dir: string;
|
|
29
|
+
plugin: string;
|
|
30
|
+
graph: string;
|
|
31
|
+
errors: string[];
|
|
32
|
+
runStatus?: Awaited<ReturnType<typeof runGraph>>['status'];
|
|
33
|
+
timings: { scaffold: number; write: number; validate: number; repair?: number; install: number; run?: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function codexWrite(dir: string, prompt: string): Promise<void> {
|
|
37
|
+
const child = Bun.spawn(['codex', 'exec', '-C', dir, '-s', 'workspace-write', '--skip-git-repo-check', '-o', join(dir, '.codex-last-message.txt'), prompt], {
|
|
38
|
+
cwd: dir, stdin: 'ignore', stdout: 'ignore', stderr: 'pipe',
|
|
39
|
+
});
|
|
40
|
+
const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
41
|
+
if (code !== 0) throw new Error(`codex exec failed (${code}): ${stderr.trim().slice(0, 500)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scaffold(dir: string, slug: string, request: string): void {
|
|
45
|
+
mkdirSync(join(dir, 'graphs'));
|
|
46
|
+
mkdirSync(join(dir, 'examples'));
|
|
47
|
+
writeFileSync(join(dir, 'plugin.json'), JSON.stringify({ name: slug, version: '0.1.0', description: request,
|
|
48
|
+
extensions: { 'ai.elanous': { graphs: [`./graphs/${slug}.yaml`], capabilities: CAPABILITIES } } }, null, 2) + '\n');
|
|
49
|
+
writeFileSync(join(dir, 'graphs', `${slug}.yaml`), `graph_id: ${slug}\nversion: 1\nentry_node: main\nterminal_nodes: [done, failed]\nnodes:\n - { node_id: main, kind: agent, recipe: 'cmd:main', max_visits: 1 }\n - { node_id: done, kind: gate, recipe: none, max_visits: 1 }\n - { node_id: failed, kind: gate, recipe: none, max_visits: 1 }\nedges:\n - { from: main, on: outcome, map: { ok: done, fail: failed } }\n`);
|
|
50
|
+
writeFileSync(join(dir, 'graphs', 'recipes.yaml'), `main:\n command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" main'\n timeout_ms: 120000\n`);
|
|
51
|
+
writeFileSync(join(dir, 'graphs', 'run-step.ts'), `import { readFileSync } from 'node:fs';\nconst context = JSON.parse(readFileSync(process.env.ELANOUS_GRAPH_CONTEXT!, 'utf8'));\nconsole.log(JSON.stringify({ outcome: 'ok', input: context.input }));\n`);
|
|
52
|
+
writeFileSync(join(dir, 'examples', 'input.json'), '{}\n');
|
|
53
|
+
writeFileSync(join(dir, 'README.md'), `# ${slug}\n\n${request}\n\nRun the installed graph with examples/input.json as input. No messages are sent.\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function inside(dir: string, path: string): string | undefined {
|
|
57
|
+
const full = resolve(dir, path);
|
|
58
|
+
return full.startsWith(`${dir}${sep}`) ? full : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function realTarget(path: string): string {
|
|
62
|
+
let ancestor = path;
|
|
63
|
+
while (!existsSync(ancestor)) {
|
|
64
|
+
const next = dirname(ancestor);
|
|
65
|
+
if (next === ancestor) throw new Error(`cannot resolve plugin directory: ${path}`);
|
|
66
|
+
ancestor = next;
|
|
67
|
+
}
|
|
68
|
+
return resolve(realpathSync(ancestor), relative(ancestor, path));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertLocalParent(parent: string): void {
|
|
72
|
+
const repoPlugins = realpathSync(resolve(import.meta.dir, '../../../plugins'));
|
|
73
|
+
const target = realTarget(parent);
|
|
74
|
+
if (target === repoPlugins || target.startsWith(`${repoPlugins}${sep}`)) {
|
|
75
|
+
throw new Error('repository plugins/ is not a local plugin directory');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function validatePluginDir(dir: string): Promise<string[]> {
|
|
80
|
+
const errors: string[] = [];
|
|
81
|
+
let manifest: unknown;
|
|
82
|
+
try {
|
|
83
|
+
const file = join(dir, 'plugin.json');
|
|
84
|
+
if (lstatSync(file).isSymbolicLink()) errors.push('안전하지 않은 plugin.json 링크');
|
|
85
|
+
else manifest = JSON.parse(readFileSync(file, 'utf8'));
|
|
86
|
+
}
|
|
87
|
+
catch (error) { errors.push(`plugin.json 파싱 실패: ${String(error)}`); }
|
|
88
|
+
const extension = isRecord(manifest) && isRecord(manifest.extensions) ? manifest.extensions['ai.elanous'] : undefined;
|
|
89
|
+
const graphs = isRecord(extension) ? extension.graphs : undefined;
|
|
90
|
+
if (isRecord(extension) && extension.contributes !== undefined) errors.push('extension contributes 덮어쓰기 금지');
|
|
91
|
+
if (!isRecord(manifest) || typeof manifest.name !== 'string' || !/^[a-z0-9][a-z0-9-]{1,39}$/.test(manifest.name) || typeof manifest.version !== 'string') errors.push('plugin.json 이름/버전 오류');
|
|
92
|
+
if (isRecord(manifest) && (manifest.capabilities !== undefined || manifest.contributes !== undefined || manifest.main !== undefined)) errors.push('plugin.json 최상위 권한/기여/main 덮어쓰기 금지');
|
|
93
|
+
if (!isRecord(extension) || !Array.isArray(extension.capabilities) || JSON.stringify(extension.capabilities) !== JSON.stringify(CAPABILITIES)) {
|
|
94
|
+
errors.push('capabilities 는 ["fs:workdir","proc:bun","proc:elanous"] 이어야 한다');
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(graphs) || graphs.length === 0 || !graphs.every(g => typeof g === 'string')) errors.push('plugin.json graphs 경로 없음');
|
|
97
|
+
let recipes: unknown;
|
|
98
|
+
try {
|
|
99
|
+
const file = join(dir, 'graphs', 'recipes.yaml');
|
|
100
|
+
if (lstatSync(file).isSymbolicLink()) errors.push('안전하지 않은 recipes.yaml 링크');
|
|
101
|
+
else recipes = parseYaml(readFileSync(file, 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
catch (error) { errors.push(`recipes.yaml 파싱 실패: ${String(error)}`); }
|
|
104
|
+
if (!isRecord(recipes)) errors.push('recipes.yaml 은 맵이어야 한다');
|
|
105
|
+
else for (const [id, recipe] of Object.entries(recipes)) {
|
|
106
|
+
if (!isRecord(recipe) || typeof recipe.command !== 'string' || !recipe.command.trim() ||
|
|
107
|
+
(recipe.timeout_ms !== undefined && (!Number.isSafeInteger(recipe.timeout_ms) || Number(recipe.timeout_ms) <= 0))) errors.push(`잘못된 recipe: ${id}`);
|
|
108
|
+
}
|
|
109
|
+
for (const graph of Array.isArray(graphs) ? graphs : []) {
|
|
110
|
+
if (typeof graph !== 'string') continue;
|
|
111
|
+
const file = inside(dir, graph);
|
|
112
|
+
if (!file || !/^\.\/graphs\/[a-zA-Z0-9_-]+\.ya?ml$/.test(graph)) { errors.push(`안전하지 않은 graph 경로: ${graph}`); continue; }
|
|
113
|
+
try {
|
|
114
|
+
if (lstatSync(file).isSymbolicLink()) { errors.push(`안전하지 않은 graph 링크: ${graph}`); continue; }
|
|
115
|
+
const parsed = parseGraphTemplateYaml(readFileSync(file, 'utf8'), file);
|
|
116
|
+
errors.push(...parsed.errors.map(issue => `${issue.path}: ${issue.message}`));
|
|
117
|
+
const template = parsed.template;
|
|
118
|
+
if (!template) continue;
|
|
119
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(template.graphId)) errors.push(`잘못된 graph_id: ${template.graphId}`);
|
|
120
|
+
if (template.terminalNodes.length !== 2 || !template.terminalNodes.includes('done') || !template.terminalNodes.includes('failed')) errors.push('terminal_nodes 는 done, failed 이어야 한다');
|
|
121
|
+
const executionNodes = template.nodes.filter(node => !template.terminalNodes.includes(node.nodeId));
|
|
122
|
+
if (executionNodes.length < 2 || executionNodes.length > 6) errors.push(`실행 노드는 2~6개여야 한다: ${executionNodes.length}개`);
|
|
123
|
+
for (const node of template.nodes) {
|
|
124
|
+
if (node.recipe.startsWith('cmd:')) {
|
|
125
|
+
const id = node.recipe.slice(4);
|
|
126
|
+
if (!isRecord(recipes) || !isRecord(recipes[id]) || typeof recipes[id].command !== 'string' || !recipes[id].command) errors.push(`recipe ${id} 없음`);
|
|
127
|
+
} else if (node.recipe !== 'none') errors.push(`지원하지 않는 recipe: ${node.recipe}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) { errors.push(`${graph} 파싱 실패: ${String(error)}`); }
|
|
130
|
+
}
|
|
131
|
+
const visit = (path: string): void => {
|
|
132
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
133
|
+
const file = join(path, entry.name);
|
|
134
|
+
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) { errors.push(`안전하지 않은 파일: ${file}`); continue; }
|
|
135
|
+
if (entry.isDirectory()) visit(file);
|
|
136
|
+
else if (/\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
137
|
+
const source = readFileSync(file, 'utf8');
|
|
138
|
+
for (const match of source.matchAll(/\b(?:import|export)\s*(?:[\s\S]*?\s+from\s*)?["']([^"']+)["']|\b(?:import|require)\s*\(\s*["']([^"']+)["']/g)) {
|
|
139
|
+
const target = match[1] ?? match[2]!;
|
|
140
|
+
if (/(?:^|\/)src\//.test(target) && (target.startsWith('/') || target.startsWith('.'))) errors.push(`src import 금지: ${file}: ${target}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
try { visit(dir); } catch (error) { errors.push(`파일 검사 실패: ${String(error)}`); }
|
|
146
|
+
try {
|
|
147
|
+
const example = join(dir, 'examples', 'input.json');
|
|
148
|
+
if (lstatSync(example).isSymbolicLink()) errors.push('안전하지 않은 examples/input.json 링크');
|
|
149
|
+
else JSON.parse(readFileSync(example, 'utf8'));
|
|
150
|
+
} catch (error) { errors.push(`examples/input.json 파싱 실패: ${String(error)}`); }
|
|
151
|
+
const step = join(dir, 'graphs', 'run-step.ts');
|
|
152
|
+
try {
|
|
153
|
+
if (!existsSync(step)) errors.push('run-step.ts 없음');
|
|
154
|
+
else if (errors.some(error => error.startsWith('안전하지 않은 파일:'))) return errors;
|
|
155
|
+
else {
|
|
156
|
+
const build = await Bun.build({ entrypoints: [step], target: 'bun' });
|
|
157
|
+
if (!build.success) errors.push(...build.logs.map(log => `run-step.ts 문법 오류: ${log.message}`));
|
|
158
|
+
}
|
|
159
|
+
} catch (error) { errors.push(`run-step.ts 문법 오류: ${String(error)}`); }
|
|
160
|
+
return errors;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function makePlugin({ request, name, parentDir, run, input, deps }: MakePluginOptions): Promise<MakePluginResult> {
|
|
164
|
+
const slug = (name ?? request).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32).replace(/-$/, '') || `plugin-${Date.now()}`;
|
|
165
|
+
if (!request.trim() || !/^[a-z0-9][a-z0-9-]{1,39}$/.test(slug)) throw new Error(`invalid plugin request or name: ${slug}`);
|
|
166
|
+
if (name !== undefined && !/^[a-z0-9][a-z0-9-]{1,31}$/.test(name)) throw new Error(`invalid --name slug: ${name}`);
|
|
167
|
+
const root = elanousStateRoot();
|
|
168
|
+
const parent = resolve(parentDir ?? join(root, 'plugins-local'));
|
|
169
|
+
assertLocalParent(parent);
|
|
170
|
+
const dir = join(parent, slug);
|
|
171
|
+
if (existsSync(dir)) throw new Error(`plugin directory already exists: ${dir}`);
|
|
172
|
+
const timings: MakePluginResult['timings'] = { scaffold: 0, write: 0, validate: 0, install: 0 };
|
|
173
|
+
const result: MakePluginResult = { status: 'failed', dir, plugin: slug, graph: join(dir, 'graphs', `${slug}.yaml`), errors: [], timings };
|
|
174
|
+
const step = async (stage: keyof MakePluginResult['timings'], action: () => void | Promise<void>): Promise<void> => {
|
|
175
|
+
const start = performance.now();
|
|
176
|
+
try { await action(); }
|
|
177
|
+
finally { const ms = Math.round(performance.now() - start); timings[stage] = ms; debug.log('plugin.maker', stage, { ms, errors: result.errors }); }
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
await step('scaffold', () => { mkdirSync(parent, { recursive: true }); mkdirSync(dir); scaffold(dir, slug, request); });
|
|
181
|
+
const codex = deps?.codex ?? codexWrite;
|
|
182
|
+
const prompt = `요청 원문:\n${request}\n\n이 폴더의 뼈대를 수정해 작동하는 로컬 그래프 플러그인을 작성하라. plugin.json extensions["ai.elanous"].graphs 와 capabilities 는 유지하고, graphs/<id>.yaml 은 entry_node, terminal_nodes: [done, failed], nodes 의 recipe: 'cmd:<id>', edges 의 on: outcome, map: { ok, fail } 을 써라. graphs/recipes.yaml 에 모든 cmd:<id> 의 command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" <id>' 와 timeout_ms 를 선언하라. graphs/run-step.ts 의 각 단계 stdout 마지막 줄은 JSON {outcome, ...} 이며 이전 출력은 ELANOUS_GRAPH_CONTEXT JSON 의 outputs 에 있다. src/ import 금지. 엘라누스 기능은 CLI elanous ask --json 또는 elanous research --json 로만 사용. 아무것도 보내지 말 것. 실행 노드는 2~6개. examples/input.json 에 실제로 돌릴 입력을 넣을 것. 폴더 밖에는 쓰지 말 것.`;
|
|
183
|
+
await step('write', () => codex(dir, prompt));
|
|
184
|
+
await step('validate', async () => { result.errors = await validatePluginDir(dir); });
|
|
185
|
+
if (result.errors.length) {
|
|
186
|
+
await step('repair', () => codex(dir, `${prompt}\n\n아래 오류를 모두 수리하라:\n${result.errors.join('\n')}`));
|
|
187
|
+
await step('validate', async () => { result.errors = await validatePluginDir(dir); });
|
|
188
|
+
}
|
|
189
|
+
if (result.errors.length) return result;
|
|
190
|
+
let installedPath = '';
|
|
191
|
+
await step('install', async () => { installedPath = (await (deps?.install ?? installPlugin)(dir, { yes: true, root })).path; });
|
|
192
|
+
result.status = 'installed';
|
|
193
|
+
const graph = JSON.parse(readFileSync(join(dir, 'plugin.json'), 'utf8')).extensions['ai.elanous'].graphs[0] as string;
|
|
194
|
+
result.graph = resolve(installedPath, graph);
|
|
195
|
+
if (run) {
|
|
196
|
+
await step('run', async () => {
|
|
197
|
+
const value = input === undefined ? JSON.parse(readFileSync(join(dir, 'examples', 'input.json'), 'utf8')) : input;
|
|
198
|
+
result.runStatus = (await (deps?.runGraph ?? runGraph)(result.graph, { input: value })).status;
|
|
199
|
+
});
|
|
200
|
+
result.status = result.runStatus === 'done' ? 'ran' : 'failed';
|
|
201
|
+
if (result.status === 'failed') result.errors.push(`graph run: ${result.runStatus}`);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
result.status = 'failed';
|
|
205
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|