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
|
@@ -11,6 +11,7 @@ import { getUserConfig } from '../../user-config.js';
|
|
|
11
11
|
import { debug } from '../../debug/log.js';
|
|
12
12
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
13
13
|
import { collectPodArtifacts } from './pod-artifact-return.js';
|
|
14
|
+
import { podBunCacheVolume } from './pod-bun-cache.js';
|
|
14
15
|
import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
|
|
15
16
|
import { readSkillEnvFiles } from './pod-skills.js';
|
|
16
17
|
import { podSourceScript, type PodSource } from './pod-source-receive.js';
|
|
@@ -76,6 +77,7 @@ export interface PodCommandJobInput {
|
|
|
76
77
|
/** 저장소를 clone 한다 — 비공개 저장소라 GitHub 토큰이 Secret 으로 간다(명시 opt-in). 기본은 clone 없이 `~/work` 에서 이미지의 `elanous` 로 돈다. */
|
|
77
78
|
clone?: boolean;
|
|
78
79
|
hostMirror?: string;
|
|
80
|
+
bunCache?: string;
|
|
79
81
|
/** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
|
|
80
82
|
memoryLimit?: string;
|
|
81
83
|
source?: PodSource;
|
|
@@ -94,6 +96,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
94
96
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) throw new Error(`pod command skill name rejected: ${name}`);
|
|
95
97
|
}
|
|
96
98
|
const grok = o.llm === 'grok';
|
|
99
|
+
const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
|
|
97
100
|
const quotedArgs = o.command.map(bashSingleQuote).join(' ');
|
|
98
101
|
const credLines = [
|
|
99
102
|
...(grok ? ['mkdir -p ~/.grok && install -m 600 /creds/grok-auth.json ~/.grok/auth.json'] : []),
|
|
@@ -108,6 +111,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
108
111
|
...(o.clone
|
|
109
112
|
? ['export GH_TOKEN="$(cat /creds/gh-token)" && gh auth setup-git', podSourceScript(o.source ?? { kind: 'default' }, o.repoUrl)]
|
|
110
113
|
: ['mkdir -p ~/work && cd ~/work']),
|
|
114
|
+
...(bunCache ? [bunCache.shellPrefix] : []),
|
|
111
115
|
`set -- ${quotedArgs}`,
|
|
112
116
|
'"$@"',
|
|
113
117
|
'rc=$?',
|
|
@@ -138,14 +142,16 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
138
142
|
{ name: 'ELANOUS_SUBSTRATE', value: 'pod' },
|
|
139
143
|
{ name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
|
|
140
144
|
],
|
|
141
|
-
...(secretKeys || o.hostMirror ? { volumeMounts: [
|
|
145
|
+
...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
|
|
142
146
|
...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
|
|
143
147
|
...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
|
|
148
|
+
...(bunCache ? [bunCache.volumeMount] : []),
|
|
144
149
|
] } : {}),
|
|
145
150
|
}],
|
|
146
|
-
...(secretKeys || o.hostMirror ? { volumes: [
|
|
151
|
+
...(secretKeys || o.hostMirror || bunCache ? { volumes: [
|
|
147
152
|
...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
|
|
148
153
|
...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
|
|
154
|
+
...(bunCache ? [bunCache.volume] : []),
|
|
149
155
|
] } : {}),
|
|
150
156
|
},
|
|
151
157
|
},
|
|
@@ -201,6 +207,7 @@ export interface RunPodCommandOptions {
|
|
|
201
207
|
llm?: 'grok';
|
|
202
208
|
clone?: boolean;
|
|
203
209
|
hostMirror?: string;
|
|
210
|
+
bunCache?: string;
|
|
204
211
|
/** 컨테이너 메모리 한도(기본 16Gi). */
|
|
205
212
|
memoryLimit?: string;
|
|
206
213
|
source?: PodSource;
|
|
@@ -263,6 +270,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
263
270
|
const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR
|
|
264
271
|
?? (options.configHostMirror ?? (() => getUserConfig().pod?.hostMirror))())?.trim() || undefined;
|
|
265
272
|
if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
|
|
273
|
+
const bunCache = options.bunCache?.trim() || undefined;
|
|
274
|
+
if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
|
|
266
275
|
|
|
267
276
|
let member: PodPoolMember | null = null;
|
|
268
277
|
let pool = options.poolScheduler;
|
|
@@ -328,7 +337,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
328
337
|
name, namespace, image: member?.imageRef ?? image,
|
|
329
338
|
...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
|
|
330
339
|
repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
|
|
331
|
-
...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
|
|
340
|
+
...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
|
|
332
341
|
...(options.runId ? { runId: options.runId } : {}),
|
|
333
342
|
});
|
|
334
343
|
kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found']);
|
package/src/telegram.ts
CHANGED
|
@@ -67,6 +67,8 @@ import { runUrlRoute } from './skills/url-route-exec.js';
|
|
|
67
67
|
import { recordRoutedLinkAbsorbed } from './intake-plane/link-ledger.js';
|
|
68
68
|
import { classifyFrontInput } from './intake-plane/front-classifier.js';
|
|
69
69
|
import { effectiveInstanceRoot } from './instance/resolve.js';
|
|
70
|
+
import { getElanousConfigDir } from './elanous-config-dir.js';
|
|
71
|
+
import { defaultFieldEvent, FieldUploadError, parseFieldCaption, resolveFieldMime, saveFieldMedia } from './field/field-media.js';
|
|
70
72
|
|
|
71
73
|
export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
|
|
72
74
|
|
|
@@ -105,6 +107,10 @@ export interface TgIncoming {
|
|
|
105
107
|
botId?: string;
|
|
106
108
|
/** 답장 대상 메시지의 텍스트(force_reply 정정 가로채기용). parseUpdate 가 채운다. */
|
|
107
109
|
replyToText?: string;
|
|
110
|
+
/** 앨범(media group) id — `#현장` 캡션은 앨범의 첫 항목에만 붙으므로 나머지를 같은 행사로 묶는 데 쓴다. */
|
|
111
|
+
mediaGroupId?: string;
|
|
112
|
+
/** 영상 — `#현장` 저장 전용. `attachments` 에 넣지 않는다(LLM 경로의 동작을 안 바꾸려고). */
|
|
113
|
+
video?: TgIncomingAttachment;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
/** Streaming surface passed to the message handler. The handler can
|
|
@@ -200,6 +206,10 @@ export interface TelegramBotOpts {
|
|
|
200
206
|
* independently of the LLM chat path. Best-effort: errors are
|
|
201
207
|
* swallowed by the caller — never blocks chat reply. */
|
|
202
208
|
onTriggerTap?: (event: TgTriggerEvent) => void | Promise<void>;
|
|
209
|
+
/** `#현장` 기본 행사 — 없으면 `slashContext.userConfig.telegram.fieldDefaultEvent` · 그것도 없으면 `field-<로컬 날짜>`. */
|
|
210
|
+
fieldDefaultEvent?: string;
|
|
211
|
+
/** 시험 seam — 현장 폴더 뿌리(기본 `getElanousConfigDir()`). */
|
|
212
|
+
fieldRootDir?: () => string;
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
/** Surface-unification v2 (FU-2) — Telegram trigger event passed to
|
|
@@ -314,6 +324,13 @@ export class TelegramBot {
|
|
|
314
324
|
* the bot under Telegram's ~30/sec global cap without blocking
|
|
315
325
|
* further if load is bursty but average-safe. */
|
|
316
326
|
private readonly globalWindow: number[] = [];
|
|
327
|
+
private readonly fieldDefaultEventOpt?: string;
|
|
328
|
+
private readonly fieldRootDir: () => string;
|
|
329
|
+
/** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
|
|
330
|
+
private readonly fieldGroups = new Map<string, {
|
|
331
|
+
event: string; at: number; chatId: number; threadId?: number; replyTo: number; count: number;
|
|
332
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
333
|
+
}>();
|
|
317
334
|
|
|
318
335
|
constructor(opts: TelegramBotOpts) {
|
|
319
336
|
if (!opts.token) throw new Error('TelegramBot: token required');
|
|
@@ -340,6 +357,8 @@ export class TelegramBot {
|
|
|
340
357
|
this.slashCommands = opts.slashCommands ?? [];
|
|
341
358
|
this.slashContext = opts.slashContext;
|
|
342
359
|
this.voiceAdapter = opts.voiceAdapter;
|
|
360
|
+
this.fieldDefaultEventOpt = opts.fieldDefaultEvent;
|
|
361
|
+
this.fieldRootDir = opts.fieldRootDir ?? getElanousConfigDir;
|
|
343
362
|
if (opts.onTriggerTap) {
|
|
344
363
|
// Assign through `as unknown` to bypass `readonly` + private —
|
|
345
364
|
// the tap is set once at construction and never reassigned.
|
|
@@ -1347,11 +1366,82 @@ export class TelegramBot {
|
|
|
1347
1366
|
return false;
|
|
1348
1367
|
}
|
|
1349
1368
|
|
|
1369
|
+
/** `#현장` 캡션 사진·영상(이미지·영상 문서 포함)을 현장 폴더에 저장하고 한 줄로 답한다.
|
|
1370
|
+
* 처리했으면 true. 저장 규칙은 HTTP 입구와 같은 `saveFieldMedia` 다. */
|
|
1371
|
+
private async tryHandleFieldUpload(ctx: TgIncoming): Promise<boolean> {
|
|
1372
|
+
const media = [
|
|
1373
|
+
...ctx.attachments.filter((a) => a.kind === 'photo'
|
|
1374
|
+
|| (a.kind === 'document' && resolveFieldMime(a.mimeType, a.fileName ?? '') !== null)),
|
|
1375
|
+
...(ctx.video ? [ctx.video] : []),
|
|
1376
|
+
];
|
|
1377
|
+
if (media.length === 0) return false;
|
|
1378
|
+
const now = this.nowImpl();
|
|
1379
|
+
for (const [id, g] of this.fieldGroups) if (now - g.at > 10 * 60_000 && !g.timer) this.fieldGroups.delete(id);
|
|
1380
|
+
const tag = parseFieldCaption(ctx.text);
|
|
1381
|
+
const group = ctx.mediaGroupId ? this.fieldGroups.get(ctx.mediaGroupId) : undefined;
|
|
1382
|
+
if (!tag && !group) return false;
|
|
1383
|
+
const event = tag
|
|
1384
|
+
? tag.event ?? defaultFieldEvent(
|
|
1385
|
+
this.fieldDefaultEventOpt ?? this.slashContext?.userConfig?.telegram?.fieldDefaultEvent, new Date(now))
|
|
1386
|
+
: group!.event;
|
|
1387
|
+
|
|
1388
|
+
let count: number | null = null;
|
|
1389
|
+
let failure: string | null = null;
|
|
1390
|
+
try {
|
|
1391
|
+
const inputs = [];
|
|
1392
|
+
for (const att of media) {
|
|
1393
|
+
const downloaded = await this.downloadFile(att.fileId);
|
|
1394
|
+
try {
|
|
1395
|
+
inputs.push({
|
|
1396
|
+
originalName: att.fileName ?? downloaded.fileName ?? 'media',
|
|
1397
|
+
mimeType: att.mimeType ?? (att.kind === 'photo' ? 'image/jpeg' : undefined),
|
|
1398
|
+
bytes: new Uint8Array(readFileSync(downloaded.localPath)),
|
|
1399
|
+
});
|
|
1400
|
+
} finally {
|
|
1401
|
+
try { unlinkSync(downloaded.localPath); } catch { /* 이미 없음 */ }
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// 글(자막)은 캡션이 달린 항목에만 있다 — 앨범이면 첫 장에만 붙는다(MK 2026-09-30).
|
|
1405
|
+
count = saveFieldMedia(inputs, { rootDir: this.fieldRootDir(), event, device: 'telegram', ...(tag?.text ? { caption: tag.text } : {}) }).count;
|
|
1406
|
+
debug.log('field.upload', 'saved', { surface: 'telegram', files: inputs.length, count, album: !!ctx.mediaGroupId });
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
failure = err instanceof FieldUploadError ? err.code : 'download-failed';
|
|
1409
|
+
debug.log('field.upload', 'refused', { surface: 'telegram', reason: failure });
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (failure !== null) {
|
|
1413
|
+
await this.sendMessage(ctx.chatId, `현장 저장 실패 · ${event} · ${failure}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
const line = (n: number) => `현장 폴더에 저장 · ${event} · 지금 ${n}장`;
|
|
1417
|
+
if (!ctx.mediaGroupId) {
|
|
1418
|
+
await this.sendMessage(ctx.chatId, line(count!), { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
// 앨범은 항목마다 답하지 않는다 — 마지막 항목 뒤 한 번(디바운스)만 최신 수로 답한다.
|
|
1422
|
+
const entry = group ?? { event, at: now, chatId: ctx.chatId, threadId: ctx.threadId, replyTo: ctx.messageId, count: 0 };
|
|
1423
|
+
entry.at = now;
|
|
1424
|
+
entry.count = count!;
|
|
1425
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1426
|
+
entry.timer = setTimeout(() => {
|
|
1427
|
+
entry.timer = undefined;
|
|
1428
|
+
void this.sendMessage(entry.chatId, line(entry.count), { replyTo: entry.replyTo, threadId: entry.threadId })
|
|
1429
|
+
.catch((e) => this.log(`field reply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1430
|
+
}, 1500);
|
|
1431
|
+
(entry.timer as { unref?: () => void }).unref?.();
|
|
1432
|
+
this.fieldGroups.set(ctx.mediaGroupId, entry);
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1350
1436
|
private async handleIncoming(ctx: TgIncoming): Promise<void> {
|
|
1351
1437
|
if (!this.isOwnerAllowed(ctx.userId)) {
|
|
1352
1438
|
await this.refuseMessage(ctx);
|
|
1353
1439
|
return;
|
|
1354
1440
|
}
|
|
1441
|
+
// 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
|
|
1442
|
+
if (await this.tryHandleFieldUpload(ctx)) return;
|
|
1443
|
+
// 영상은 `#현장` 저장 전용으로만 파싱한다 — 그 밖의 영상은 종전처럼 무시한다.
|
|
1444
|
+
if (ctx.video && ctx.attachments.length === 0) return;
|
|
1355
1445
|
// Cascade-zyu U2 — capture utterance intent at update ingress.
|
|
1356
1446
|
try {
|
|
1357
1447
|
const { userIntentLogger } = await import('./user-intent/index.js');
|
|
@@ -2069,6 +2159,7 @@ export class TelegramBot {
|
|
|
2069
2159
|
interface PhotoSize { file_id: string; file_size?: number; width: number; height: number }
|
|
2070
2160
|
interface VoiceLike { file_id: string; file_size?: number; mime_type?: string; duration?: number }
|
|
2071
2161
|
interface DocumentLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string }
|
|
2162
|
+
interface VideoLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string; duration?: number }
|
|
2072
2163
|
|
|
2073
2164
|
interface RawUpdate {
|
|
2074
2165
|
update_id: number;
|
|
@@ -2083,6 +2174,8 @@ interface RawUpdate {
|
|
|
2083
2174
|
voice?: VoiceLike;
|
|
2084
2175
|
audio?: VoiceLike;
|
|
2085
2176
|
document?: DocumentLike;
|
|
2177
|
+
video?: VideoLike;
|
|
2178
|
+
media_group_id?: string;
|
|
2086
2179
|
reply_to_message?: { text?: string; message_id?: number };
|
|
2087
2180
|
};
|
|
2088
2181
|
callback_query?: {
|
|
@@ -2175,8 +2268,20 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
|
|
|
2175
2268
|
});
|
|
2176
2269
|
}
|
|
2177
2270
|
|
|
2271
|
+
// 영상은 `#현장` 저장 전용 — 태그된 캡션이거나 앨범 항목일 때만 싣는다(그 밖의 영상은 종전처럼 무시).
|
|
2272
|
+
const video = m.video && (parseFieldCaption(m.caption) || m.media_group_id)
|
|
2273
|
+
? {
|
|
2274
|
+
kind: 'document' as const,
|
|
2275
|
+
fileId: m.video.file_id,
|
|
2276
|
+
fileSize: m.video.file_size,
|
|
2277
|
+
mimeType: m.video.mime_type ?? 'video/mp4',
|
|
2278
|
+
fileName: m.video.file_name,
|
|
2279
|
+
duration: m.video.duration,
|
|
2280
|
+
}
|
|
2281
|
+
: undefined;
|
|
2282
|
+
|
|
2178
2283
|
// Skip updates with zero signal: no text AND no attachments.
|
|
2179
|
-
if (typeof m.text !== 'string' && attachments.length === 0) return null;
|
|
2284
|
+
if (typeof m.text !== 'string' && attachments.length === 0 && !video) return null;
|
|
2180
2285
|
|
|
2181
2286
|
const isDm = m.chat.type === 'private';
|
|
2182
2287
|
return {
|
|
@@ -2191,6 +2296,8 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
|
|
|
2191
2296
|
isGroup: !isDm,
|
|
2192
2297
|
attachments,
|
|
2193
2298
|
...(m.reply_to_message?.text ? { replyToText: m.reply_to_message.text } : {}),
|
|
2299
|
+
...(m.media_group_id ? { mediaGroupId: m.media_group_id } : {}),
|
|
2300
|
+
...(video ? { video } : {}),
|
|
2194
2301
|
};
|
|
2195
2302
|
}
|
|
2196
2303
|
|
package/src/user-config.ts
CHANGED
|
@@ -833,6 +833,8 @@ export interface TelegramConfig {
|
|
|
833
833
|
/** 발송 목적(kind) → 채널 역할. 명시 `channels` 가 있을 때만 쓴다 — 표에 없는 kind 는 옛 `reportChannel` 로 간다.
|
|
834
834
|
* 기본표는 `DEFAULT_KIND_ROLES`(telegram-report.ts) · 여기 값이 덮어쓴다. */
|
|
835
835
|
kindRoles?: Record<string, string>;
|
|
836
|
+
/** `#현장` 캡션에 행사 슬러그가 없을 때 쓸 기본 행사(예: `marketers-night-2026-10`). 없으면 `field-<로컬 날짜>`. */
|
|
837
|
+
fieldDefaultEvent?: string;
|
|
836
838
|
}
|
|
837
839
|
|
|
838
840
|
const TELEGRAM_DEFAULTS: TelegramConfig = { enabled: false, allowedUsers: [] };
|
|
@@ -4393,6 +4395,7 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
|
|
|
4393
4395
|
testChannel: normalizeTestChannel(tg.testChannel),
|
|
4394
4396
|
...(tg.poller === 'standalone' || tg.poller === 'nexus' ? { poller: tg.poller } : {}),
|
|
4395
4397
|
...(normalizeKindRoles(tg.kindRoles) ? { kindRoles: normalizeKindRoles(tg.kindRoles)! } : {}),
|
|
4398
|
+
...(str(tg.fieldDefaultEvent) ? { fieldDefaultEvent: str(tg.fieldDefaultEvent)! } : {}),
|
|
4396
4399
|
},
|
|
4397
4400
|
discord: {
|
|
4398
4401
|
enabled: dc.enabled === true,
|
|
@@ -5565,6 +5568,7 @@ export function saveUserConfig(
|
|
|
5565
5568
|
// ★ 2026-09-25: `poller`(T1 분리 스위치)가 파서엔 있고 이 whitelist 에 없어 `config set telegram.poller` 가
|
|
5566
5569
|
// 「직렬화 드롭」으로 조용히 사라졌다 — 운영 전환 스위치를 켤 방법이 없었다(07-22 botUsername 과 같은 모양).
|
|
5567
5570
|
poller: cfg.telegram.poller,
|
|
5571
|
+
fieldDefaultEvent: cfg.telegram.fieldDefaultEvent,
|
|
5568
5572
|
}),
|
|
5569
5573
|
discord: stripUndef({
|
|
5570
5574
|
enabled: cfg.discord.enabled,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"65a4c7230c60a536b0f0f0428322c8ddcbcac752"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import type { RunResult } from './ffmpeg.js';
|
|
5
|
+
import { UNOBSERVED, type Recipe, type RecipeCtx, type RecipeResult } from './types.js';
|
|
6
|
+
|
|
7
|
+
type CommandRunner = (bin: string, args: readonly string[], timeoutMs: number, cwd?: string) => RunResult;
|
|
8
|
+
interface ExplainerDeps { readonly run?: CommandRunner; readonly engineDir?: string }
|
|
9
|
+
|
|
10
|
+
const ENGINE = resolve(import.meta.dir, '../../../skills/explainer-video/engine');
|
|
11
|
+
const ENGINE_MS = 600_000;
|
|
12
|
+
const QC_MS = 180_000;
|
|
13
|
+
|
|
14
|
+
function inside(root: string, candidate: string): boolean {
|
|
15
|
+
const rel = relative(root, candidate);
|
|
16
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Resolve existing ancestors as well: an output directory may be a symlink before the engine writes into it.
|
|
20
|
+
function safeOutput(root: string, candidate: string): boolean {
|
|
21
|
+
const base = realpathSync(root);
|
|
22
|
+
if (!inside(resolve(root), resolve(candidate))) return false;
|
|
23
|
+
// Walk up to the deepest component that exists as a directory entry (lstat, not existsSync):
|
|
24
|
+
// a broken symlink is «there» and must be judged, not skipped as missing.
|
|
25
|
+
let path = resolve(candidate);
|
|
26
|
+
for (;;) {
|
|
27
|
+
let isLink: boolean;
|
|
28
|
+
try { isLink = lstatSync(path).isSymbolicLink(); } catch {
|
|
29
|
+
const parent = dirname(path);
|
|
30
|
+
if (parent === path) return false;
|
|
31
|
+
path = parent;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (isLink && !existsSync(path)) return false; // broken link — its target could be created outside the project
|
|
35
|
+
return inside(base, realpathSync(path));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function finish(ctx: RecipeCtx, name: string, result: RecipeResult): RecipeResult {
|
|
40
|
+
ctx.log(`explainer.${name}`, { outcome: result.outcome, ...result.produced, note: result.note });
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scriptPath(ctx: RecipeCtx): string | null {
|
|
45
|
+
const value = ctx.state.script_path;
|
|
46
|
+
return typeof value === 'string' && value.trim() ? resolve(value) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function enginePath(deps: ExplainerDeps, file: string): string | null {
|
|
50
|
+
const path = resolve(deps.engineDir ?? ENGINE, file);
|
|
51
|
+
return existsSync(path) ? path : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runner(deps: ExplainerDeps, ctx: RecipeCtx): CommandRunner {
|
|
55
|
+
if (deps.run) return deps.run;
|
|
56
|
+
return (bin, args, timeoutMs, cwd) => {
|
|
57
|
+
const r = spawnSync(bin, [...args], {
|
|
58
|
+
cwd, timeout: timeoutMs, encoding: 'utf8',
|
|
59
|
+
env: { ...process.env, EXPLAINER_CACHE: resolve(ctx.workdir, 'explainer-cache') },
|
|
60
|
+
});
|
|
61
|
+
return { ok: r.status === 0, code: r.status, signal: r.signal ?? null,
|
|
62
|
+
err: (r.stderr ?? r.error?.message ?? '').trim(), out: (r.stdout ?? '').trim() };
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function unavailable(r: RunResult): boolean {
|
|
67
|
+
return r.signal === null && r.code === null && !r.ok;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function keys(script: unknown): string[] | null {
|
|
71
|
+
if (!script || typeof script !== 'object') return null;
|
|
72
|
+
const s = script as { intro?: { beats?: unknown }; chapters?: unknown; outro?: { beats?: unknown } };
|
|
73
|
+
if (!Array.isArray(s.intro?.beats) || !Array.isArray(s.chapters) || !Array.isArray(s.outro?.beats)) return null;
|
|
74
|
+
const chapters = s.chapters as { beats?: unknown }[];
|
|
75
|
+
if (!chapters.every((c) => c && Array.isArray(c.beats))) return null;
|
|
76
|
+
return [
|
|
77
|
+
...s.intro.beats.map((_, i) => `i-${i}`),
|
|
78
|
+
...chapters.flatMap((c, ci) => (c.beats as unknown[]).map((_, i) => `c${ci + 1}-${i}`)),
|
|
79
|
+
...s.outro.beats.map((_, i) => `o-${i}`),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function explainerScript(): Recipe {
|
|
84
|
+
return async (ctx) => {
|
|
85
|
+
const path = scriptPath(ctx);
|
|
86
|
+
if (!path) return finish(ctx, 'script', { outcome: 'no-facts', note: 'script_path 가 state 에 없다 — 대본 저작이 필요하다' });
|
|
87
|
+
if (!existsSync(path)) return finish(ctx, 'script', { outcome: 'no-facts', note: `대본이 없다: ${path}` });
|
|
88
|
+
let script: unknown;
|
|
89
|
+
try { script = JSON.parse(readFileSync(path, 'utf8')); }
|
|
90
|
+
catch (e) { return finish(ctx, 'script', { outcome: UNOBSERVED, note: `대본 읽기 실패: ${String(e)}` }); }
|
|
91
|
+
const chapters = (script as { chapters?: unknown } | null)?.chapters;
|
|
92
|
+
const beatKeys = keys(script);
|
|
93
|
+
const voiced = (beat: unknown): boolean => beat !== null && typeof beat === 'object' &&
|
|
94
|
+
typeof (beat as { vo?: unknown }).vo === 'string' && (beat as { vo: string }).vo.trim().length > 0;
|
|
95
|
+
if (!beatKeys?.length || !Array.isArray(chapters) || chapters.length === 0 ||
|
|
96
|
+
!chapters.some((chapter: { beats: unknown[] }) => chapter.beats.some(voiced)) ||
|
|
97
|
+
![...(script as { intro: { beats: unknown[] } }).intro.beats,
|
|
98
|
+
...chapters.flatMap((chapter: { beats: unknown[] }) => chapter.beats),
|
|
99
|
+
...(script as { outro: { beats: unknown[] } }).outro.beats].every(voiced)) {
|
|
100
|
+
return finish(ctx, 'script', { outcome: 'no-facts', note: 'intro.beats · chapters[] · outro.beats 에 내용이 없거나 구조가 없다' });
|
|
101
|
+
}
|
|
102
|
+
return finish(ctx, 'script', { outcome: 'ok', produced: { script_path: path } });
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function explainerVo(deps: ExplainerDeps = {}): Recipe {
|
|
107
|
+
return async (ctx) => {
|
|
108
|
+
const path = scriptPath(ctx);
|
|
109
|
+
if (!path || !existsSync(path)) return finish(ctx, 'vo', { outcome: UNOBSERVED, note: 'script_path 를 못 읽는다' });
|
|
110
|
+
const engine = enginePath(deps, 'vo.ts');
|
|
111
|
+
if (!engine) return finish(ctx, 'vo', { outcome: UNOBSERVED, note: 'skills/explainer-video/engine/vo.ts 가 없다' });
|
|
112
|
+
const project = dirname(path);
|
|
113
|
+
const voDir = resolve(project, 'source/vo');
|
|
114
|
+
try {
|
|
115
|
+
const beatKeys = keys(JSON.parse(readFileSync(path, 'utf8')));
|
|
116
|
+
if (!beatKeys?.length) return finish(ctx, 'vo', { outcome: UNOBSERVED, note: '대본의 VO beat 를 못 읽었다' });
|
|
117
|
+
const outputs = beatKeys.flatMap((key) => [
|
|
118
|
+
resolve(voDir, `${key}.mp3`), resolve(voDir, `${key}.json`),
|
|
119
|
+
resolve(voDir, `${key}.tmp.aiff`), resolve(voDir, `${key}.tmp.mp3`),
|
|
120
|
+
]);
|
|
121
|
+
if (![voDir, ...outputs].every((p) => safeOutput(project, p))) {
|
|
122
|
+
return finish(ctx, 'vo', { outcome: UNOBSERVED, note: 'VO 출력이 대본 폴더 밖을 가리킨다' });
|
|
123
|
+
}
|
|
124
|
+
} catch (e) { return finish(ctx, 'vo', { outcome: UNOBSERVED, note: `VO 경로 확인 실패: ${String(e)}` }); }
|
|
125
|
+
let r: RunResult;
|
|
126
|
+
try { r = runner(deps, ctx)('bun', [engine, path], ENGINE_MS, project); }
|
|
127
|
+
catch (e) { return finish(ctx, 'vo', { outcome: UNOBSERVED, note: `bun vo.ts 를 못 불렀다: ${String(e)}` }); }
|
|
128
|
+
if (unavailable(r)) return finish(ctx, 'vo', { outcome: UNOBSERVED, note: `bun vo.ts 를 못 불렀다(code=${r.code} signal=${r.signal}): ${r.err}` });
|
|
129
|
+
if (!r.ok) return finish(ctx, 'vo', { outcome: 'error', note: `VO 엔진 실패: ${r.err.slice(0, 240)}` });
|
|
130
|
+
try {
|
|
131
|
+
const script = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
132
|
+
const beatKeys = keys(script);
|
|
133
|
+
if (!beatKeys?.length) throw new Error('대본에 VO beat 가 없다');
|
|
134
|
+
let seconds = 0;
|
|
135
|
+
for (const key of beatKeys) {
|
|
136
|
+
const audio = resolve(voDir, `${key}.mp3`);
|
|
137
|
+
const alignment = resolve(voDir, `${key}.json`);
|
|
138
|
+
if (!safeOutput(project, audio) || !safeOutput(project, alignment) || !existsSync(audio)) throw new Error(`${key} VO 파일이 없다`);
|
|
139
|
+
const j = JSON.parse(readFileSync(alignment, 'utf8')) as { alignment?: { character_end_times_seconds?: unknown } };
|
|
140
|
+
const ends = j.alignment?.character_end_times_seconds;
|
|
141
|
+
if (!Array.isArray(ends) || ends.length === 0 || !Number.isFinite(ends.at(-1)) || ends.at(-1) <= 0) throw new Error(`${key} 끝 시각을 못 읽었다`);
|
|
142
|
+
seconds += ends.at(-1) as number;
|
|
143
|
+
}
|
|
144
|
+
return finish(ctx, 'vo', { outcome: 'ok', produced: { vo_dir: voDir, narration_seconds: seconds } });
|
|
145
|
+
} catch (e) { return finish(ctx, 'vo', { outcome: UNOBSERVED, note: `VO 산출을 못 쟀다: ${String(e)}` }); }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function explainerBuild(deps: ExplainerDeps = {}): Recipe {
|
|
150
|
+
return async (ctx) => {
|
|
151
|
+
const path = scriptPath(ctx);
|
|
152
|
+
if (!path || !existsSync(path)) return finish(ctx, 'build', { outcome: UNOBSERVED, note: 'script_path 를 못 읽는다' });
|
|
153
|
+
const engine = enginePath(deps, 'build.mjs');
|
|
154
|
+
if (!engine) return finish(ctx, 'build', { outcome: UNOBSERVED, note: 'skills/explainer-video/engine/build.mjs 가 없다' });
|
|
155
|
+
const project = dirname(path);
|
|
156
|
+
const hf = resolve(project, 'hf');
|
|
157
|
+
const timeline = resolve(project, 'timeline.json');
|
|
158
|
+
try {
|
|
159
|
+
mkdirSync(ctx.workdir, { recursive: true });
|
|
160
|
+
if (![hf, resolve(hf, 'assets'), resolve(hf, 'assets/fonts'), resolve(hf, 'assets/vo'), timeline].every((p) => safeOutput(project, p))
|
|
161
|
+
|| !safeOutput(ctx.workdir, resolve(ctx.workdir, 'explainer-cache'))) {
|
|
162
|
+
return finish(ctx, 'build', { outcome: UNOBSERVED, note: '빌드 출력/캐시가 허용된 폴더 밖을 가리킨다' });
|
|
163
|
+
}
|
|
164
|
+
} catch (e) { return finish(ctx, 'build', { outcome: UNOBSERVED, note: `빌드 경로 확인 실패: ${String(e)}` }); }
|
|
165
|
+
let r: RunResult;
|
|
166
|
+
try { r = runner(deps, ctx)('node', [engine, path], ENGINE_MS, project); }
|
|
167
|
+
catch (e) { return finish(ctx, 'build', { outcome: UNOBSERVED, note: `node build.mjs 를 못 불렀다: ${String(e)}` }); }
|
|
168
|
+
if (unavailable(r)) return finish(ctx, 'build', { outcome: UNOBSERVED, note: `node build.mjs 를 못 불렀다(code=${r.code} signal=${r.signal}): ${r.err}` });
|
|
169
|
+
if (!r.ok) {
|
|
170
|
+
const badScript = /unknown diagram|DIAGRAMS\[dia\] is not a function|(?:invalid|missing|unsupported) (?:script|diagram|beat|chapter)/i.test(r.err);
|
|
171
|
+
return finish(ctx, 'build', { outcome: badScript ? 'bad-script' : 'error', note: `빌드 실패: ${r.err.slice(0, 240)}` });
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
if (![resolve(hf, 'index.html'), timeline].every((p) => safeOutput(project, p) && existsSync(p))) {
|
|
175
|
+
return finish(ctx, 'build', { outcome: UNOBSERVED, note: '빌드가 hf/index.html 또는 timeline.json 을 내지 않았다' });
|
|
176
|
+
}
|
|
177
|
+
} catch (e) { return finish(ctx, 'build', { outcome: UNOBSERVED, note: `빌드 산출 확인 실패: ${String(e)}` }); }
|
|
178
|
+
return finish(ctx, 'build', { outcome: 'ok', produced: { hyperframes_project: hf, timeline_path: timeline } });
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function explainerQc(deps: ExplainerDeps = {}): Recipe {
|
|
183
|
+
return async (ctx) => {
|
|
184
|
+
const path = ctx.state.hf_render_path;
|
|
185
|
+
if (typeof path !== 'string' || !path || !existsSync(path)) {
|
|
186
|
+
return finish(ctx, 'qc', { outcome: UNOBSERVED, note: 'hf_render_path 를 못 읽는다' });
|
|
187
|
+
}
|
|
188
|
+
let r: RunResult;
|
|
189
|
+
try {
|
|
190
|
+
r = runner(deps, ctx)('ffmpeg', ['-hide_banner', '-nostats', '-i', resolve(path),
|
|
191
|
+
'-af', 'loudnorm=print_format=json', '-f', 'null', '-'], QC_MS, ctx.workdir);
|
|
192
|
+
} catch (e) { return finish(ctx, 'qc', { outcome: UNOBSERVED, note: `ffmpeg 를 못 불렀다: ${String(e)}` }); }
|
|
193
|
+
if (!r.ok) return finish(ctx, 'qc', { outcome: UNOBSERVED, note: `ffmpeg 음량 측정 실패(code=${r.code} signal=${r.signal}): ${r.err.slice(0, 240)}` });
|
|
194
|
+
const block = [...`${r.err}\n${r.out}`.matchAll(/\{[^{}]*"input_i"[^{}]*"input_tp"[^{}]*\}/g)].at(-1)?.[0];
|
|
195
|
+
try {
|
|
196
|
+
if (!block) throw new Error('loudnorm JSON 이 없다');
|
|
197
|
+
const j = JSON.parse(block) as { input_i?: unknown; input_tp?: unknown };
|
|
198
|
+
const loudness = Number(j.input_i);
|
|
199
|
+
const peak = Number(j.input_tp);
|
|
200
|
+
if (j.input_i == null || j.input_tp == null || !Number.isFinite(loudness) || !Number.isFinite(peak)) throw new Error('통합 음량/트루 피크가 비수다');
|
|
201
|
+
const produced = { loudness_lufs: loudness, true_peak_db: peak };
|
|
202
|
+
const outcome = loudness >= -16 && loudness <= -12 && peak <= -1 ? 'pass' : 'loudness';
|
|
203
|
+
return finish(ctx, 'qc', { outcome, produced, note: `${loudness} LUFS · ${peak} dBTP` });
|
|
204
|
+
} catch (e) { return finish(ctx, 'qc', { outcome: UNOBSERVED, note: `loudnorm 결과를 못 읽었다: ${String(e)}` }); }
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const EXPLAINER: Readonly<Record<string, Recipe>> = {
|
|
209
|
+
'explainer-script': explainerScript(),
|
|
210
|
+
'explainer-vo': explainerVo(),
|
|
211
|
+
'explainer-build': explainerBuild(),
|
|
212
|
+
'explainer-qc': explainerQc(),
|
|
213
|
+
};
|
|
@@ -14,6 +14,7 @@ import { readdirSync, readFileSync } from 'node:fs';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { BROLL } from './recipes/broll.js';
|
|
16
16
|
import { CHARACTER } from './recipes/character.js';
|
|
17
|
+
import { EXPLAINER } from './recipes/explainer.js';
|
|
17
18
|
import { FILM } from './recipes/film.js';
|
|
18
19
|
import { FREE_LINE } from './recipes/free-line.js';
|
|
19
20
|
import { HYPERFRAMES } from './recipes/hyperframes.js';
|
|
@@ -21,8 +22,8 @@ import { UPSTREAM } from './recipes/upstream.js';
|
|
|
21
22
|
import { VLOG } from './recipes/vlog.js';
|
|
22
23
|
import { UNOBSERVED, type Recipe, type RecipeCtx } from './recipes/types.js';
|
|
23
24
|
|
|
24
|
-
/** ⛔ 순서가 뜻이다 — 같은 이름이면 뒤가 이긴다. HYPERFRAMES
|
|
25
|
-
export const ALL_RECIPES: Readonly<Record<string, Recipe>> = { ...UPSTREAM, ...FREE_LINE, ...VLOG, ...FILM, ...CHARACTER, ...HYPERFRAMES, ...BROLL };
|
|
25
|
+
/** ⛔ 순서가 뜻이다 — 같은 이름이면 뒤가 이긴다. HYPERFRAMES · EXPLAINER 는 기존 키를 덮지 않는다. */
|
|
26
|
+
export const ALL_RECIPES: Readonly<Record<string, Recipe>> = { ...UPSTREAM, ...FREE_LINE, ...VLOG, ...FILM, ...CHARACTER, ...HYPERFRAMES, ...BROLL, ...EXPLAINER };
|
|
26
27
|
|
|
27
28
|
export interface GraphSpecLike {
|
|
28
29
|
readonly graph_id: string;
|