elanous 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- package/README.md +4 -0
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +10 -0
- package/graphs/release/release-loop.yaml +10 -3
- package/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +6 -5
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/card-followup/README.md +13 -4
- package/plugins/card-followup/graphs/card-followup.yaml +3 -1
- package/plugins/card-followup/graphs/recipes.yaml +5 -1
- package/plugins/card-followup/graphs/run-step.ts +122 -18
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +1 -1
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +3 -1
- package/plugins/geo-check/graphs/run-step.ts +58 -13
- package/plugins/geo-check/plugin.json +1 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +1 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/field-feed/deliver-node.ts +57 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +164 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +109 -21
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +108 -22
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +124 -0
- package/scripts/release-loop/ops-upgrade-node.ts +95 -0
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +12 -9
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +138 -31
- package/skills/explainer-video/engine/reel.sh +2 -1
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +96 -20
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +23 -3
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +41 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/plugin-cli.ts +25 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +50 -8
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +37 -16
- package/src/dashboard/index.ts +18 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +73 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +94 -12
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +146 -0
- package/src/exec-requests/runner.ts +378 -0
- package/src/exec-requests/store.ts +78 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +203 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +168 -21
- package/src/input-core/text-entry.ts +13 -5
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +221 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +128 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +40 -3
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +71 -4
- package/src/nexus/api/http-server.ts +82 -8
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +28 -1
- package/src/nexus/setup-status.ts +11 -7
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +34 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +90 -0
- package/src/onboarding.ts +287 -26
- package/src/persona/presets.ts +105 -0
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +19 -7
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/release-loop/checklist.ts +37 -13
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/session/chat.ts +17 -5
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/skills/trigger-extract.ts +1 -1
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram-commands.ts +13 -0
- package/src/telegram.ts +72 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +189 -15
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -10,6 +10,7 @@ import type { GuardianSpec } from './guardian/types.js';
|
|
|
10
10
|
import { ptyAvailable } from './pty-shell/registry.js';
|
|
11
11
|
import { krFlowAvailable } from './skills/tools/kr-flow.js';
|
|
12
12
|
import { SELF_COGNITION_MCP_CATALOG_ENTRIES } from './tool-runtime/self-cognition-runtimes.js';
|
|
13
|
+
import { SKILLS_LIST_DESCRIPTION } from './onboarding/entry-hints.js';
|
|
13
14
|
|
|
14
15
|
export type { NativeToolHost } from './tool-surface.js';
|
|
15
16
|
import type { NativeToolHost } from './tool-surface.js';
|
|
@@ -549,7 +550,7 @@ export const nativeToolCatalog: NativeToolCatalogEntry[] = [
|
|
|
549
550
|
kind: 'other',
|
|
550
551
|
aliases: ['ElanousSkillsList'],
|
|
551
552
|
displayName: 'ElanousSkillsList',
|
|
552
|
-
description:
|
|
553
|
+
description: SKILLS_LIST_DESCRIPTION,
|
|
553
554
|
promptSummary: '`elanous_skills_list` (list installed skill names before skill_exec when the exact name is unknown)',
|
|
554
555
|
host: ['skill', 'tui', 'mcp'],
|
|
555
556
|
safety: ['read-only'],
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { buildUserConfig, saveUserConfig, userConfigPath } from '../../user-config.js';
|
|
2
|
+
|
|
3
|
+
function json(body: unknown, status = 200): Response {
|
|
4
|
+
return new Response(JSON.stringify(body), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { 'content-type': 'application/json' },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** GET/PUT /v1/config/chat-fast-path. Authentication is enforced by the HTTP dispatcher. */
|
|
11
|
+
export async function handleChatFastPathConfig(req: Request): Promise<Response> {
|
|
12
|
+
if (req.method === 'GET') {
|
|
13
|
+
return json({ enabled: buildUserConfig(userConfigPath()).chat.fastPath });
|
|
14
|
+
}
|
|
15
|
+
if (req.method !== 'PUT') return json({ error: 'method-not-allowed' }, 405);
|
|
16
|
+
|
|
17
|
+
let body: unknown;
|
|
18
|
+
try { body = await req.json(); }
|
|
19
|
+
catch { return json({ error: 'invalid-json' }, 400); }
|
|
20
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)
|
|
21
|
+
|| Object.keys(body).length !== 1 || typeof (body as { enabled?: unknown }).enabled !== 'boolean') {
|
|
22
|
+
return json({ error: 'invalid-shape' }, 400);
|
|
23
|
+
}
|
|
24
|
+
const path = userConfigPath();
|
|
25
|
+
const cfg = buildUserConfig(path);
|
|
26
|
+
saveUserConfig({ ...cfg, chat: { ...cfg.chat, fastPath: (body as { enabled: boolean }).enabled } }, path);
|
|
27
|
+
return json({ enabled: buildUserConfig(path).chat.fastPath });
|
|
28
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { routeOutbound } from '../outbound/router.js';
|
|
6
|
+
import { getUserConfig } from '../../user-config.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import { jsonResponse } from './json-response.js';
|
|
9
|
+
|
|
10
|
+
export const CONSULT_REQUESTS_PATH = '/v1/consult-requests';
|
|
11
|
+
|
|
12
|
+
interface ConsultRequest {
|
|
13
|
+
receiptId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
org: string | null;
|
|
16
|
+
kind: 'company' | 'personal';
|
|
17
|
+
interest: 'A' | 'B';
|
|
18
|
+
contact: string;
|
|
19
|
+
consent: true;
|
|
20
|
+
receivedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConsultRequestsDeps {
|
|
24
|
+
root?: () => string;
|
|
25
|
+
now?: () => string;
|
|
26
|
+
/** Owner alert. Default = the daemon's own in-process router (the same one `/v1/outbound` uses).
|
|
27
|
+
* ⛔ Never `sendOutbound` here: it is a synchronous curl to this very daemon's `/v1/outbound`, so inside the
|
|
28
|
+
* daemon it blocks the event loop until it times out (~25 s · `unreachable`) and the alert is lost (CS1 · 10-01). */
|
|
29
|
+
send?: (text: string, kind: string) => unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function file(root: string): string {
|
|
33
|
+
return join(root, 'consult-requests', 'requests.jsonl');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function append(path: string, entry: ConsultRequest): void {
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
38
|
+
const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
39
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
42
|
+
const fd = openSync(temp, 'r');
|
|
43
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
44
|
+
renameSync(temp, path);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
try { unlinkSync(temp); } catch { /* no temp file to remove */ }
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function records(path: string): ConsultRequest[] {
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line) as ConsultRequest);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rejected(field: string): Response {
|
|
61
|
+
debug.log('consult.request', 'rejected', { field });
|
|
62
|
+
return jsonResponse({ error: 'bad_request', field }, 400);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function singleLine(value: string): string {
|
|
66
|
+
return value.trim().replace(/[\r\n\u2028\u2029]+/g, ' ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function handleConsultRequests(req: Request, deps: ConsultRequestsDeps = {}): Promise<Response> {
|
|
70
|
+
if (req.method !== 'GET' && req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
71
|
+
const path = file((deps.root ?? effectiveInstanceRoot)());
|
|
72
|
+
if (req.method === 'GET') {
|
|
73
|
+
const rawLimit = new URL(req.url).searchParams.get('limit');
|
|
74
|
+
const limit = rawLimit === null ? 20 : Number(rawLimit);
|
|
75
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) return rejected('limit');
|
|
76
|
+
try {
|
|
77
|
+
const items = records(path).reverse().slice(0, limit).map(({ receiptId, name, org, kind, interest, receivedAt }) =>
|
|
78
|
+
({ receiptId, name, org, kind, interest, receivedAt }));
|
|
79
|
+
return jsonResponse({ items });
|
|
80
|
+
} catch {
|
|
81
|
+
return jsonResponse({ error: 'journal-unavailable' }, 503);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let body: unknown;
|
|
86
|
+
try { body = await req.json(); }
|
|
87
|
+
catch { return rejected('body'); }
|
|
88
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return rejected('body');
|
|
89
|
+
const input = body as Record<string, unknown>;
|
|
90
|
+
for (const field of ['name', 'contact'] as const) {
|
|
91
|
+
if (typeof input[field] !== 'string' || !input[field].trim()) return rejected(field);
|
|
92
|
+
}
|
|
93
|
+
if (input.org !== undefined && (typeof input.org !== 'string' || !input.org.trim())) return rejected('org');
|
|
94
|
+
if (input.kind !== 'company' && input.kind !== 'personal') return rejected('kind');
|
|
95
|
+
if (input.interest !== 'A' && input.interest !== 'B') return rejected('interest');
|
|
96
|
+
if (input.consent !== true) return rejected('consent');
|
|
97
|
+
|
|
98
|
+
const entry: ConsultRequest = {
|
|
99
|
+
receiptId: `R-${randomUUID()}`,
|
|
100
|
+
name: singleLine(input.name as string),
|
|
101
|
+
org: typeof input.org === 'string' ? singleLine(input.org) : null,
|
|
102
|
+
kind: input.kind,
|
|
103
|
+
interest: input.interest,
|
|
104
|
+
contact: (input.contact as string).trim(),
|
|
105
|
+
consent: true,
|
|
106
|
+
receivedAt: (deps.now ?? (() => new Date().toISOString()))(),
|
|
107
|
+
};
|
|
108
|
+
try { append(path, entry); }
|
|
109
|
+
catch {
|
|
110
|
+
debug.log('consult.request', 'rejected', { field: 'journal' });
|
|
111
|
+
return jsonResponse({ error: 'journal-unavailable' }, 503);
|
|
112
|
+
}
|
|
113
|
+
debug.log('consult.request', 'accepted', { receiptId: entry.receiptId });
|
|
114
|
+
const alert = `📮 상담 문의 ${entry.receiptId} · ${entry.name}(${entry.kind === 'personal' ? '개인' : (entry.org ?? '회사')}) · 관심 ${entry.interest}\n앱에서 보기`;
|
|
115
|
+
// Fire and forget — the receipt is already on disk; the answer must not wait for the channel.
|
|
116
|
+
void Promise.resolve()
|
|
117
|
+
.then(() => (deps.send ?? notifyInProcess)(alert, 'alert'))
|
|
118
|
+
.catch((error: unknown) => { debug.log('consult.request', 'notify-failed', { receiptId: entry.receiptId, reason: error instanceof Error ? error.message : String(error) }); });
|
|
119
|
+
return jsonResponse({ receiptId: entry.receiptId, receivedAt: entry.receivedAt }, 202);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function notifyInProcess(text: string, kind: string): Promise<void> {
|
|
123
|
+
const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
|
|
124
|
+
debug.log('consult.request', result.delivered ? 'notified' : 'notify-failed', {
|
|
125
|
+
delivered: result.delivered,
|
|
126
|
+
channels: result.channels.map((channel) => `${channel.type}:${channel.ok ? 'ok' : 'fail'}`),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -66,6 +66,8 @@ export interface NexusDiscordTriggerBotOpts {
|
|
|
66
66
|
* commands (/cc·/fork·/voice-join·…) route into the same composed
|
|
67
67
|
* message pipeline. Absent ⇒ interactions ignored (legacy). */
|
|
68
68
|
onInteraction?: (raw: Record<string, unknown>) => void | Promise<void>;
|
|
69
|
+
/** EV12c — guild text channels answered like DMs (`discord.chatChannels`). Absent ⇒ DM-only. */
|
|
70
|
+
guildTextChannels?: string[];
|
|
69
71
|
/** Optional logger. Defaults to `[discord/trigger]` prefix. */
|
|
70
72
|
log?: (msg: string) => void;
|
|
71
73
|
/** Test seam — pre-built DiscordBot (factory skips construction +
|
|
@@ -156,6 +158,7 @@ export function createNexusDiscordTriggerBot(
|
|
|
156
158
|
const bot = opts.bot ?? new DiscordBot({
|
|
157
159
|
token: opts.token,
|
|
158
160
|
allowedUsers: opts.allowedUsers,
|
|
161
|
+
...(opts.guildTextChannels?.length ? { guildTextChannels: opts.guildTextChannels } : {}),
|
|
159
162
|
// Chat path: the injected self-turn handler (M4b nexus wire) or the
|
|
160
163
|
// legacy trigger-only no-op. Workflows wanting a chat reply can
|
|
161
164
|
// still declare a `chatTrigger` (routes via `/v1/workflows/chat/*`).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ER2 — the Primary side of error reports. The bot VM forwards each accepted report here over the tailnet
|
|
2
|
+
// (ingest token). It is stored under <instance>/error-reports/YYYY/MM/DD/<reportId>.json for 30 days, and the
|
|
3
|
+
// operator gets one Telegram line per distinct error per hour — never the message, stack or identity.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import type { ReceivedReport } from '../../hooks/error-report.js';
|
|
9
|
+
import { validateErrorReport } from '../../hooks/error-report.js';
|
|
10
|
+
import { jsonResponse } from './json-response.js';
|
|
11
|
+
import { routeOutboundInProcess } from './outbound-report.js';
|
|
12
|
+
|
|
13
|
+
export const ERROR_REPORT_KEEP_DAYS = 30;
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
|
|
16
|
+
export interface ErrorReportIngestDeps {
|
|
17
|
+
root?: () => string;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
/** Operator alert. Default = the daemon's in-process router (never a self-call · OB8). */
|
|
20
|
+
alert?: (text: string, kind: string) => Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function storeDir(root: string): string { return join(root, 'error-reports'); }
|
|
24
|
+
|
|
25
|
+
/** Removes day folders older than the keep window. Returns how many days were removed. */
|
|
26
|
+
export function pruneErrorReports(root: string, now: number): number {
|
|
27
|
+
const base = storeDir(root);
|
|
28
|
+
if (!existsSync(base)) return 0;
|
|
29
|
+
let removed = 0;
|
|
30
|
+
for (const year of readdirSync(base).filter((n) => /^\d{4}$/.test(n))) {
|
|
31
|
+
for (const month of readdirSync(join(base, year)).filter((n) => /^\d{2}$/.test(n))) {
|
|
32
|
+
for (const day of readdirSync(join(base, year, month)).filter((n) => /^\d{2}$/.test(n))) {
|
|
33
|
+
const at = Date.parse(`${year}-${month}-${day}T00:00:00Z`);
|
|
34
|
+
if (Number.isFinite(at) && now - at > ERROR_REPORT_KEEP_DAYS * DAY_MS + DAY_MS) {
|
|
35
|
+
rmSync(join(base, year, month, day), { recursive: true, force: true });
|
|
36
|
+
removed++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One alert per dedupe per hour, remembered on disk so a restart does not repeat it. */
|
|
45
|
+
function shouldAlert(root: string, dedupe: string, now: number): boolean {
|
|
46
|
+
const path = join(storeDir(root), 'alerts.json');
|
|
47
|
+
let seen: Record<string, number> = {};
|
|
48
|
+
try { seen = JSON.parse(readFileSync(path, 'utf8')) as Record<string, number>; } catch { /* first alert */ }
|
|
49
|
+
for (const [k, t] of Object.entries(seen)) if (now - t > DAY_MS) delete seen[k];
|
|
50
|
+
const last = seen[dedupe];
|
|
51
|
+
if (last !== undefined && now - last < 3600_000) return false;
|
|
52
|
+
seen[dedupe] = now;
|
|
53
|
+
mkdirSync(storeDir(root), { recursive: true, mode: 0o700 });
|
|
54
|
+
writeFileSync(path, JSON.stringify(seen), { mode: 0o600 });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function errorReportAlertText(item: ReceivedReport, count24h: number): string {
|
|
59
|
+
const r = item.report;
|
|
60
|
+
return `🧯 오류 보고 ${r.code} · v${r.app.version} · ${r.app.surface} · 같은 오류 24h ${count24h}회 · ${item.reportId}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Same dedupe stored in the last 24 h, counted from the store itself. */
|
|
64
|
+
function count24h(root: string, dedupe: string, now: number): number {
|
|
65
|
+
let n = 0;
|
|
66
|
+
for (const offset of [0, 1]) {
|
|
67
|
+
const d = new Date(now - offset * DAY_MS);
|
|
68
|
+
const dir = join(storeDir(root), String(d.getUTCFullYear()), String(d.getUTCMonth() + 1).padStart(2, '0'), String(d.getUTCDate()).padStart(2, '0'));
|
|
69
|
+
if (!existsSync(dir)) continue;
|
|
70
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
|
|
71
|
+
try {
|
|
72
|
+
const item = JSON.parse(readFileSync(join(dir, name), 'utf8')) as ReceivedReport;
|
|
73
|
+
if (item.dedupe === dedupe && now - Date.parse(item.receivedAt) < DAY_MS) n++;
|
|
74
|
+
} catch { /* a broken file is not counted */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** POST /v1/reports/ingest — the caller (router) has already checked the ingest token. */
|
|
81
|
+
export async function handleErrorReportIngest(req: Request, deps: ErrorReportIngestDeps = {}): Promise<Response> {
|
|
82
|
+
const root = (deps.root ?? effectiveInstanceRoot)();
|
|
83
|
+
const now = (deps.now ?? Date.now)();
|
|
84
|
+
let body: unknown;
|
|
85
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'invalid', field: 'body' }, 400); }
|
|
86
|
+
const item = body as Partial<ReceivedReport>;
|
|
87
|
+
if (typeof item.reportId !== 'string' || !/^er_[0-9a-z]{26}$/.test(item.reportId)) return jsonResponse({ error: 'invalid', field: 'reportId' }, 400);
|
|
88
|
+
if (typeof item.dedupe !== 'string' || !/^[0-9a-f]{12}$/.test(item.dedupe)) return jsonResponse({ error: 'invalid', field: 'dedupe' }, 400);
|
|
89
|
+
if (typeof item.receivedAt !== 'string' || !Number.isFinite(Date.parse(item.receivedAt))) return jsonResponse({ error: 'invalid', field: 'receivedAt' }, 400);
|
|
90
|
+
const checked = validateErrorReport(item.report);
|
|
91
|
+
if (!checked.ok) return jsonResponse({ error: 'invalid', field: `report.${checked.field}` }, 400);
|
|
92
|
+
const stored: ReceivedReport = { reportId: item.reportId, dedupe: item.dedupe, receivedAt: item.receivedAt, report: checked.report };
|
|
93
|
+
const at = new Date(Date.parse(stored.receivedAt));
|
|
94
|
+
const dir = join(storeDir(root), String(at.getUTCFullYear()), String(at.getUTCMonth() + 1).padStart(2, '0'), String(at.getUTCDate()).padStart(2, '0'));
|
|
95
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
96
|
+
writeFileSync(join(dir, `${stored.reportId}.json`), JSON.stringify(stored), { mode: 0o600 });
|
|
97
|
+
let pruned = 0;
|
|
98
|
+
try { pruned = pruneErrorReports(root, now); } catch { /* pruning never blocks a receipt */ }
|
|
99
|
+
const count = count24h(root, stored.dedupe, now);
|
|
100
|
+
const alerted = shouldAlert(root, stored.dedupe, now);
|
|
101
|
+
debug.log('error-report.ingest', 'stored', { code: stored.report.code, surface: stored.report.app.surface, count24h: count, alerted, pruned });
|
|
102
|
+
if (alerted) {
|
|
103
|
+
const alert = deps.alert ?? routeOutboundInProcess;
|
|
104
|
+
void Promise.resolve().then(() => alert(errorReportAlertText(stored, count), 'alert'))
|
|
105
|
+
.catch((error: unknown) => debug.log('error-report.ingest', 'alert-failed', { reason: error instanceof Error ? error.message : String(error) }));
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse({ stored: true, reportId: stored.reportId, count }, 202);
|
|
108
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { ExecRequestRunner } from '../../exec-requests/runner.js';
|
|
3
|
+
import { jsonResponse } from './json-response.js';
|
|
4
|
+
|
|
5
|
+
const mime: Record<string, string> = {
|
|
6
|
+
'.pdf': 'application/pdf', '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
|
|
7
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp',
|
|
8
|
+
'.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function handleExecRequests(req: Request, runner: ExecRequestRunner): Promise<Response> {
|
|
12
|
+
const path = new URL(req.url).pathname;
|
|
13
|
+
if (path === '/v1/exec-requests') {
|
|
14
|
+
if (req.method === 'POST') {
|
|
15
|
+
let body: unknown;
|
|
16
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
17
|
+
const text = body && typeof body === 'object' && !Array.isArray(body) ? (body as { text?: unknown }).text : undefined;
|
|
18
|
+
if (typeof text !== 'string' || !text.trim()) return jsonResponse({ error: 'bad_request', reason: 'text required' }, 400);
|
|
19
|
+
const item = runner.submit(text.trim());
|
|
20
|
+
return jsonResponse({ id: item.id, status: 'planning' }, 202);
|
|
21
|
+
}
|
|
22
|
+
if (req.method === 'GET') return jsonResponse({ items: runner.store.list().map(item => {
|
|
23
|
+
const current = runner.get(item.id) ?? item;
|
|
24
|
+
return { id: current.id, text: current.text, createdAt: current.createdAt, status: current.status,
|
|
25
|
+
seats: current.seats.map(({ seat, title, status }) => ({ seat, title, status })), resultCount: current.results.length };
|
|
26
|
+
}) });
|
|
27
|
+
return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
28
|
+
}
|
|
29
|
+
const file = /^\/v1\/exec-requests\/([^/]+)\/files\/([^/]+)$/.exec(path);
|
|
30
|
+
if (file) {
|
|
31
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
32
|
+
let id: string, name: string;
|
|
33
|
+
try { id = decodeURIComponent(file[1]!); name = decodeURIComponent(file[2]!); }
|
|
34
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
35
|
+
const path = runner.file(id, name);
|
|
36
|
+
if (!path) return jsonResponse({ error: 'not-found' }, 404);
|
|
37
|
+
return new Response(Bun.file(path), { headers: { 'content-type': mime[extname(path).toLowerCase()] ?? 'application/octet-stream', 'x-content-type-options': 'nosniff' } });
|
|
38
|
+
}
|
|
39
|
+
const detail = /^\/v1\/exec-requests\/([^/]+)$/.exec(path);
|
|
40
|
+
if (detail) {
|
|
41
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
42
|
+
let id: string;
|
|
43
|
+
try { id = decodeURIComponent(detail[1]!); } catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
44
|
+
const item = runner.get(id);
|
|
45
|
+
if (!item) return jsonResponse({ error: 'not-found' }, 404);
|
|
46
|
+
const { seats, ...rest } = item;
|
|
47
|
+
return jsonResponse({ ...rest, seats: seats.map(({ seat, title, status, graphId, runId, reason }) =>
|
|
48
|
+
({ seat, title, status, graphId, runId, ...(reason ? { reason } : {}) })) });
|
|
49
|
+
}
|
|
50
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
51
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// 저장 규칙은 src/field/field-media.ts 가 유일한 정본이고 텔레그램 `#현장` 도 같은 함수를 쓴다.
|
|
3
3
|
|
|
4
4
|
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { readFieldReelStatus, scheduleFieldReel, fieldReelFile, hasFieldReelFile, type FieldReelOptions } from '../../field/field-reel.js';
|
|
5
6
|
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
6
7
|
import {
|
|
7
8
|
FIELD_MAX_FILE_BYTES,
|
|
8
9
|
FIELD_MAX_REQUEST_BYTES,
|
|
9
10
|
FieldUploadError,
|
|
11
|
+
defaultFieldEvent,
|
|
10
12
|
fieldEventDir,
|
|
11
13
|
isFieldSlug,
|
|
12
14
|
listFieldMedia,
|
|
@@ -16,6 +18,9 @@ import {
|
|
|
16
18
|
import { jsonResponse } from './json-response.js';
|
|
17
19
|
|
|
18
20
|
export const FIELD_UPLOADS_PATH = '/v1/field/uploads';
|
|
21
|
+
export const FIELD_REEL_PATH = '/v1/field/reel';
|
|
22
|
+
export const FIELD_REEL_FILE_PATH = '/v1/field/reel/file';
|
|
23
|
+
const reelUrl = (event: string) => `${FIELD_REEL_FILE_PATH}?event=${encodeURIComponent(event)}`;
|
|
19
24
|
|
|
20
25
|
/** 멀티파트 경계·헤더 여유. */
|
|
21
26
|
const MULTIPART_SLACK_BYTES = 4 * 1024 * 1024;
|
|
@@ -29,6 +34,9 @@ interface FieldUploadsDeps {
|
|
|
29
34
|
/** config 뿌리 — 기본 `getElanousConfigDir()`(데몬의 다른 저장소와 같은 해석). 시험은 temp 를 준다. */
|
|
30
35
|
rootDir?: () => string;
|
|
31
36
|
now?: () => Date;
|
|
37
|
+
reel?: FieldReelOptions;
|
|
38
|
+
/** 설정 `telegram.fieldDefaultEvent` — 앱이 «오늘» 행사 칸의 기본값으로 받는다(비면 field-<날짜>). */
|
|
39
|
+
configuredEvent?: () => string | undefined;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
function errorResponse(error: FieldUploadError): Response {
|
|
@@ -52,6 +60,7 @@ async function readMultipart(req: Request): Promise<FieldMediaInput[]> {
|
|
|
52
60
|
catch { throw new FieldUploadError('bad-request', 'invalid multipart body'); }
|
|
53
61
|
const files = form.getAll('file').filter((v): v is File => typeof v !== 'string');
|
|
54
62
|
const captured = parseCapturedAtList(form.getAll('capturedAt'));
|
|
63
|
+
const captions = form.getAll('caption').filter((v): v is string => typeof v === 'string');
|
|
55
64
|
let total = 0;
|
|
56
65
|
const inputs: FieldMediaInput[] = [];
|
|
57
66
|
for (const [i, file] of files.entries()) {
|
|
@@ -63,6 +72,7 @@ async function readMultipart(req: Request): Promise<FieldMediaInput[]> {
|
|
|
63
72
|
mimeType: file.type,
|
|
64
73
|
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
65
74
|
...(captured[i] ? { capturedAt: captured[i] } : {}),
|
|
75
|
+
...((captions.length === 1 ? i === 0 : i < captions.length) ? { caption: captions[i] } : {}),
|
|
66
76
|
});
|
|
67
77
|
}
|
|
68
78
|
return inputs;
|
|
@@ -89,22 +99,48 @@ async function readJson(req: Request): Promise<FieldMediaInput[]> {
|
|
|
89
99
|
...(typeof f.mimeType === 'string' ? { mimeType: f.mimeType } : {}),
|
|
90
100
|
bytes: new Uint8Array(Buffer.from(f.dataBase64, 'base64')),
|
|
91
101
|
...(capturedAt !== undefined ? { capturedAt } : {}),
|
|
102
|
+
...(typeof f.caption === 'string' ? { caption: f.caption } : {}),
|
|
92
103
|
};
|
|
93
104
|
});
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
export async function handleFieldUploads(req: Request, deps: FieldUploadsDeps = {}): Promise<Response> {
|
|
97
108
|
const url = new URL(req.url);
|
|
98
|
-
if (url.pathname
|
|
109
|
+
if (![FIELD_UPLOADS_PATH, FIELD_REEL_PATH, FIELD_REEL_FILE_PATH].includes(url.pathname)) return jsonResponse({ error: 'not-found' }, 404);
|
|
99
110
|
if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
100
111
|
const rootDir = (deps.rootDir ?? getElanousConfigDir)();
|
|
101
112
|
const event = url.searchParams.get('event') ?? '';
|
|
113
|
+
// event 없는 GET = 기본 행사 이름만(앱 «오늘» 행사 칸 · 텔레그램 `#현장` 과 같은 기본).
|
|
114
|
+
if (req.method === 'GET' && !url.searchParams.has('event')) {
|
|
115
|
+
const defaultEvent = defaultFieldEvent((deps.configuredEvent ?? (() => undefined))(), (deps.now ?? (() => new Date()))());
|
|
116
|
+
return jsonResponse({ defaultEvent });
|
|
117
|
+
}
|
|
102
118
|
if (!isFieldSlug(event)) return errorResponse(new FieldUploadError('bad-slug', 'event slug must match ^[a-z0-9][a-z0-9._-]{0,63}$'));
|
|
103
119
|
|
|
120
|
+
const dir = fieldEventDir(rootDir, event);
|
|
121
|
+
if (url.pathname !== FIELD_UPLOADS_PATH && req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
122
|
+
if (url.pathname === FIELD_REEL_PATH) {
|
|
123
|
+
const status = readFieldReelStatus(dir);
|
|
124
|
+
if (!status) return jsonResponse({ error: 'not-found' }, 404);
|
|
125
|
+
return jsonResponse({ event, state: status.state, items: status.items,
|
|
126
|
+
...(status.seconds !== undefined ? { seconds: status.seconds } : {}),
|
|
127
|
+
...(status.finishedAt ? { finishedAt: status.finishedAt } : {}),
|
|
128
|
+
...(status.state === 'failed' && status.error ? { error: status.error } : {}),
|
|
129
|
+
...(status.state === 'done' && hasFieldReelFile(fieldReelFile(dir)) ? { url: reelUrl(event) } : {}) });
|
|
130
|
+
}
|
|
131
|
+
if (url.pathname === FIELD_REEL_FILE_PATH) {
|
|
132
|
+
const status = readFieldReelStatus(dir);
|
|
133
|
+
const file = fieldReelFile(dir);
|
|
134
|
+
if (status?.state !== 'done' || !hasFieldReelFile(file)) return jsonResponse({ error: 'not-found' }, 404);
|
|
135
|
+
return new Response(Bun.file(file), { headers: { 'content-type': 'video/mp4' } });
|
|
136
|
+
}
|
|
104
137
|
if (req.method === 'GET') {
|
|
105
|
-
const files = listFieldMedia(
|
|
138
|
+
const files = listFieldMedia(dir);
|
|
139
|
+
const status = readFieldReelStatus(dir);
|
|
106
140
|
debug.log('field.upload', 'listed', { count: files.length });
|
|
107
|
-
return jsonResponse({ event, count: files.length, files
|
|
141
|
+
return jsonResponse({ event, count: files.length, files,
|
|
142
|
+
...(status ? { reel: { status: status.state, updatedAt: status.updatedAt,
|
|
143
|
+
...(status.state === 'done' && hasFieldReelFile(fieldReelFile(dir)) ? { url: reelUrl(event) } : {}) } } : {}) });
|
|
108
144
|
}
|
|
109
145
|
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
110
146
|
|
|
@@ -131,6 +167,7 @@ export async function handleFieldUploads(req: Request, deps: FieldUploadsDeps =
|
|
|
131
167
|
debug.log('field.upload', 'saved', {
|
|
132
168
|
surface: 'http', files: result.saved.length, bytes: result.saved.reduce((n, s) => n + s.bytes, 0), count: result.count,
|
|
133
169
|
});
|
|
170
|
+
scheduleFieldReel(result.dir, deps.reel);
|
|
134
171
|
return jsonResponse({ ok: true, event, saved: result.saved, count: result.count });
|
|
135
172
|
} catch (error) {
|
|
136
173
|
if (error instanceof FieldUploadError) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// EV10d — «게시 대기» 피드 초안: 승인 노드가 `{"kind":"feed-preview"}` 이면 그 런의 현장폴더 초안
|
|
2
|
+
// (`<folder>/feed/feed-draft.json`)이 정본이다(계약 = MK 10-01 · field-feed 그래프). 경로는 «런 상태의
|
|
3
|
+
// input.folder» 에서만 구하고, 그 폴더는 `<configDir>/field/<slug>` 꼴일 때만 받는다(realpath 로 탈출 거부).
|
|
4
|
+
// 사람의 «수정 저장»은 허용된 칸만 고쳐 그 파일을 덮는다 — 승인 상태는 바꾸지 않는다.
|
|
5
|
+
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, join, normalize, sep } from 'node:path';
|
|
7
|
+
import { fieldEventDir, isFieldSlug } from '../../field/field-media.js';
|
|
8
|
+
import type { GraphRunState } from '../../graph-runner/runner.js';
|
|
9
|
+
|
|
10
|
+
export interface FeedSlide { image: string; caption: string; include: boolean; [key: string]: unknown }
|
|
11
|
+
export interface FeedDraft {
|
|
12
|
+
kind: 'feed-draft';
|
|
13
|
+
version: 1;
|
|
14
|
+
revision: number;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
updatedBy: string;
|
|
17
|
+
slides: FeedSlide[];
|
|
18
|
+
caption: { hook: string; body: string };
|
|
19
|
+
hashtags: string[];
|
|
20
|
+
cover: { text: string; sub: string; image?: string; [key: string]: unknown };
|
|
21
|
+
location: string | null;
|
|
22
|
+
reel: string | null;
|
|
23
|
+
brand?: { name?: string; handle?: string; avatar?: string | null };
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fields a person may change from the PWA — everything else (rendered*, source, folder…) is preserved. */
|
|
28
|
+
export interface FeedEdit {
|
|
29
|
+
slides: Array<{ image: string; caption?: string; include?: boolean }>;
|
|
30
|
+
caption: { hook: string; body: string };
|
|
31
|
+
hashtags: string[];
|
|
32
|
+
cover: { text: string; sub: string };
|
|
33
|
+
location: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const FEED_DRAFT_FILE = join('feed', 'feed-draft.json');
|
|
37
|
+
|
|
38
|
+
export function isFeedPreviewMessage(message: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(message) as unknown;
|
|
41
|
+
return !!parsed && typeof parsed === 'object' && (parsed as { kind?: unknown }).kind === 'feed-preview';
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function real(path: string): string | null {
|
|
46
|
+
try { return realpathSync(path); } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The run's field folder when it is exactly `<configDir>/field/<slug>` (after resolving links), else null. */
|
|
50
|
+
export function feedFolderFor(state: Pick<GraphRunState, 'input'>, configDir: string): string | null {
|
|
51
|
+
const input = state.input as { folder?: unknown } | undefined;
|
|
52
|
+
const folder = typeof input?.folder === 'string' ? input.folder : null;
|
|
53
|
+
if (!folder) return null;
|
|
54
|
+
const slug = basename(folder);
|
|
55
|
+
if (!isFieldSlug(slug)) return null;
|
|
56
|
+
const resolved = real(folder);
|
|
57
|
+
const expected = real(fieldEventDir(configDir, slug));
|
|
58
|
+
if (!resolved || !expected || resolved !== expected) return null;
|
|
59
|
+
if (dirname(resolved) !== real(join(configDir, 'field'))) return null;
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function str(value: unknown): string { return typeof value === 'string' ? value : ''; }
|
|
64
|
+
|
|
65
|
+
export function readFeedDraft(folder: string): FeedDraft | null {
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(readFileSync(join(folder, FEED_DRAFT_FILE), 'utf8')) as Partial<FeedDraft>;
|
|
68
|
+
if (raw.kind !== 'feed-draft' || raw.version !== 1 || !Array.isArray(raw.slides)) return null;
|
|
69
|
+
return raw as FeedDraft;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FeedEditResult = { ok: true; draft: FeedDraft } | { ok: false; error: string };
|
|
74
|
+
|
|
75
|
+
/** Apply a person's edit to the current draft. Slides must be the same set of images (order may change,
|
|
76
|
+
* removal is `include:false`), so no new path can enter the draft. */
|
|
77
|
+
export function applyFeedEdit(current: FeedDraft, edit: unknown, now = new Date()): FeedEditResult {
|
|
78
|
+
if (!edit || typeof edit !== 'object') return { ok: false, error: 'bad_request' };
|
|
79
|
+
const e = edit as Partial<FeedEdit>;
|
|
80
|
+
if (!Array.isArray(e.slides) || !e.caption || typeof e.caption !== 'object' || !Array.isArray(e.hashtags)
|
|
81
|
+
|| !e.cover || typeof e.cover !== 'object') return { ok: false, error: 'bad_request' };
|
|
82
|
+
const original = new Map(current.slides.map((slide) => [slide.image, slide]));
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
const slides: FeedSlide[] = [];
|
|
85
|
+
for (const raw of e.slides) {
|
|
86
|
+
const image = str((raw as { image?: unknown })?.image);
|
|
87
|
+
const base = original.get(image);
|
|
88
|
+
if (!base || seen.has(image)) return { ok: false, error: 'unknown-image' };
|
|
89
|
+
seen.add(image);
|
|
90
|
+
const caption = (raw as { caption?: unknown }).caption;
|
|
91
|
+
const include = (raw as { include?: unknown }).include;
|
|
92
|
+
slides.push({ ...base, ...(typeof caption === 'string' ? { caption } : {}), ...(typeof include === 'boolean' ? { include } : {}) });
|
|
93
|
+
}
|
|
94
|
+
if (seen.size !== original.size) return { ok: false, error: 'missing-image' };
|
|
95
|
+
const location = e.location === null ? null : typeof e.location === 'string' ? e.location : current.location;
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
draft: {
|
|
99
|
+
...current,
|
|
100
|
+
slides,
|
|
101
|
+
caption: { ...current.caption, hook: str(e.caption.hook), body: str(e.caption.body) },
|
|
102
|
+
hashtags: e.hashtags.filter((tag): tag is string => typeof tag === 'string').map((tag) => tag.trim()).filter(Boolean),
|
|
103
|
+
cover: { ...current.cover, text: str(e.cover.text), sub: str(e.cover.sub) },
|
|
104
|
+
location,
|
|
105
|
+
revision: (Number.isFinite(current.revision) ? current.revision : 0) + 1,
|
|
106
|
+
updatedBy: 'human',
|
|
107
|
+
updatedAt: now.toISOString(),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeFeedDraft(folder: string, draft: FeedDraft): void {
|
|
113
|
+
const target = join(folder, FEED_DRAFT_FILE);
|
|
114
|
+
const temp = `${target}.tmp-${process.pid}`;
|
|
115
|
+
writeFileSync(temp, `${JSON.stringify(draft, null, 2)}\n`);
|
|
116
|
+
renameSync(temp, target);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const MEDIA_RE = /^(?:feed\/[^/\\]+\.(?:png|jpe?g|webp)|reel\/reel-9x16\.mp4)$/i;
|
|
120
|
+
|
|
121
|
+
/** Absolute file for a folder-relative media path, only `feed/*.png|jpg|jpeg|webp` and `reel/reel-9x16.mp4`. */
|
|
122
|
+
export function resolveFeedMedia(folder: string, relative: string): string | null {
|
|
123
|
+
if (!relative || relative.startsWith('/') || relative.includes('\\') || relative.includes('\0')) return null;
|
|
124
|
+
const clean = normalize(relative).split(sep).join('/');
|
|
125
|
+
if (clean !== relative || !MEDIA_RE.test(clean)) return null;
|
|
126
|
+
const file = join(folder, clean);
|
|
127
|
+
if (!existsSync(file)) return null;
|
|
128
|
+
const resolved = real(file);
|
|
129
|
+
const root = real(folder);
|
|
130
|
+
if (!resolved || !root || !resolved.startsWith(`${root}${sep}`)) return null;
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function mediaContentType(path: string): string {
|
|
135
|
+
const lower = path.toLowerCase();
|
|
136
|
+
if (lower.endsWith('.png')) return 'image/png';
|
|
137
|
+
if (lower.endsWith('.webp')) return 'image/webp';
|
|
138
|
+
if (lower.endsWith('.mp4')) return 'video/mp4';
|
|
139
|
+
return 'image/jpeg';
|
|
140
|
+
}
|