elanous 0.2.4 → 0.2.5
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 +3 -3
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +29 -27
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +29 -27
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +29 -27
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +26 -24
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +26 -24
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +26 -24
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +29 -27
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +29 -27
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +29 -27
- 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 +26 -24
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +29 -27
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +29 -27
- package/apps/pwa/out/manifest.webmanifest +5 -2
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +26 -24
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +26 -24
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +29 -27
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +29 -27
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +27 -25
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +29 -27
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +26 -24
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +29 -27
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +29 -27
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +29 -27
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +29 -27
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +29 -27
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +26 -24
- package/apps/pwa/out/sw-strategies.js +44 -0
- package/apps/pwa/out/sw.js +6 -44
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +29 -27
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +29 -27
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +29 -27
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +29 -27
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +29 -27
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +29 -27
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +29 -27
- package/catalog/resources.yaml +6 -6
- package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
- package/graphs/landing/landing-heal.yaml +1 -1
- package/graphs/release/recipes.yaml +8 -1
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +12 -0
- package/plugins/job-coach/connectors/ncs/server.ts +4 -3
- package/plugins/job-coach/graphs/gap-judge.ts +44 -0
- package/plugins/job-coach/graphs/recipes.yaml +27 -0
- package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
- package/plugins/job-coach/graphs/run-step.ts +211 -17
- package/plugins/job-coach/plugin.json +16 -3
- package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
- package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
- package/scripts/backup/elanous-backup.sh +4 -1
- package/scripts/build-docs-site.ts +4 -3
- package/scripts/coord-channel-watch.sh +23 -4
- package/scripts/coord-post.sh +49 -12
- package/scripts/coord-tracks.json +5 -5
- package/scripts/eval-bot-vs-grok.ts +79 -0
- package/scripts/eval-grok-live-baseline.ts +36 -0
- package/scripts/fixtures/front-route-corpus.sample.json +17 -0
- package/scripts/fleet/node-setup.sh +46 -7
- package/scripts/landing-heal/must-fix-match.ts +26 -0
- package/scripts/landing-heal/run-mission.ts +114 -20
- package/scripts/lib/front-route-measurement.ts +131 -0
- package/scripts/lib/tui-sim-keys.ts +42 -0
- package/scripts/measure-front-route-classifier.ts +56 -0
- package/scripts/media/ocr-leak-scan.sh +21 -0
- package/scripts/public-export.ts +2 -3
- package/scripts/public-import-pr.ts +80 -6
- package/scripts/publish-internal-dist.ts +2 -3
- package/scripts/red-sweep-diff.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +72 -0
- package/scripts/release-loop/gate-node.ts +332 -44
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/test-durations.ts +63 -0
- package/scripts/release-loop/upgrade-node.ts +3 -2
- package/scripts/test-audit-coverage-per-file.sh +16 -0
- package/scripts/test-audit-meaningless.ts +88 -0
- package/scripts/test-audit-overlap-and-weak.ts +73 -0
- package/scripts/test-deterministic.ts +27 -5
- package/scripts/tui-sim.ts +59 -14
- package/scripts/ux/bump-installed.sh +20 -0
- package/scripts/ux/lib/cdp.ts +72 -0
- package/scripts/ux/record-page.ts +50 -0
- package/skills/explainer-video/SKILL.md +99 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
- package/skills/explainer-video/engine/build.mjs +431 -0
- package/skills/explainer-video/engine/vo.ts +37 -0
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
- package/src/agent/nested-agent-env.ts +1 -0
- package/src/agent-mission/driver.ts +95 -16
- package/src/agent-mission/mission-cli.ts +6 -0
- package/src/agent-mission/resource-ladder.ts +73 -25
- package/src/auth/acp-token.ts +16 -0
- package/src/auth/github-app-token.ts +112 -0
- package/src/autopilot/gh-rest-fallback.ts +177 -0
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/boot/acp-server.ts +2 -16
- package/src/boot/daemon-tools/web-search.ts +2 -1
- package/src/boot/ws-bridge.ts +11 -0
- package/src/chat/index.ts +37 -8
- package/src/chat/log-scroll-key.ts +12 -0
- package/src/chat/submit-trace.ts +30 -0
- package/src/cli/agent-cli.ts +7 -2
- package/src/cli/decisions-cli.ts +94 -0
- package/src/cli/doctor-cli.ts +40 -54
- package/src/cli/doctor-readiness.ts +16 -24
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/mcp-call.ts +9 -0
- package/src/cli/mcp-gateway-cli.ts +7 -3
- package/src/cli/plugin-cli.ts +109 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +94 -7
- package/src/cli/self-update.ts +7 -4
- package/src/cli/start-cli.ts +33 -14
- package/src/dashboard/acp-boot.ts +3 -0
- package/src/dashboard/index.ts +62 -13
- package/src/dashboard/input/no-provider-reply.ts +13 -0
- package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
- package/src/dashboard/stale-install-check.ts +77 -0
- package/src/decisions/decision-ledger.ts +404 -0
- package/src/domains/outbound-alert.ts +8 -2
- package/src/domains/telegram-kind-route.ts +21 -0
- package/src/execution-loop/dispatch-task.ts +139 -0
- package/src/execution-loop/gate-decision-summary.ts +29 -0
- package/src/execution-loop/gates.ts +197 -0
- package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
- package/src/graph-kinds/plugin-nodes.ts +10 -2
- package/src/graph-runner/runner.ts +35 -5
- package/src/grounding/token.ts +12 -6
- package/src/harness/harness-cli-command.ts +68 -9
- package/src/harness/harness-pod-dispatch.ts +8 -2
- package/src/harness/harness-stop.ts +147 -18
- package/src/harness/self-send-decide.ts +26 -17
- package/src/harness/self-send-target.ts +22 -1
- package/src/harness/worktree-branch-prefix.ts +4 -3
- package/src/index.ts +56 -13
- package/src/instance/resolve.ts +6 -0
- package/src/intake-plane/collect-telegram-saved.ts +5 -0
- package/src/intake-plane/digest.ts +11 -2
- package/src/live/detail-switch.ts +8 -3
- package/src/llm/usable-llm.ts +32 -0
- package/src/loops/registry.ts +27 -5
- package/src/market/publish.ts +59 -3
- package/src/native-tool-catalog.ts +2 -2
- package/src/nexus/api/graph-approvals.ts +125 -0
- package/src/nexus/api/http-server.ts +60 -6
- package/src/nexus/api/log-fabric.ts +10 -1
- package/src/nexus/api/mcp-oauth-hint.ts +10 -9
- package/src/nexus/api/merge-approvals.ts +2 -1
- package/src/nexus/api/meta-api.ts +14 -0
- package/src/nexus/api/plugins-credentials-api.ts +47 -0
- package/src/nexus/api/plugins-market.ts +87 -3
- package/src/nexus/api/pod-credential-api.ts +68 -14
- package/src/nexus/api/public-routes.ts +7 -0
- package/src/nexus/api/trace.ts +57 -6
- package/src/nexus/boot/register-mcp-clients.ts +99 -14
- package/src/nexus/index.ts +33 -32
- package/src/nexus/outbound/router.ts +1 -1
- package/src/onboarding.ts +25 -2
- package/src/plugins/core/host.ts +14 -0
- package/src/plugins/install/market-fetch.ts +4 -3
- package/src/plugins/install/plugin-credentials.ts +106 -0
- package/src/plugins/install/plugin-install.ts +163 -32
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/release-loop/checklist.ts +191 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/self-dev/dev-cli.ts +40 -20
- package/src/self-dev/dev-pipeline.ts +77 -1
- package/src/self-implement/gate-baseline.ts +3 -1
- package/src/self-implement/goal-author.ts +18 -1
- package/src/self-implement/host-regate.ts +6 -2
- package/src/self-implement/orchestrator.ts +7 -2
- package/src/self-implement/seams.ts +4 -2
- package/src/skills/tools/dev-harness.ts +42 -0
- package/src/steward/steward-cards.ts +45 -0
- package/src/steward/triage.ts +50 -14
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
- package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
- package/src/telegram-report.ts +12 -6
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +28 -2
- package/src/util/spawn-sync-output.ts +15 -0
- package/src/version/code-revision.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/free.ts +77 -0
- package/src/web-search/index.ts +2 -0
- package/src/workflow-runtime/deps-bridge.ts +4 -3
- package/src/workflow-runtime/plugin-kind-node.ts +32 -4
- package/src/workflow-runtime/types.ts +2 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
- /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { basename, join, relative } from 'node:path';
|
|
5
5
|
import { loadIntakeLedger, type IntakeItem } from './items.js';
|
|
6
|
+
import { readSavedCursorAt } from './collect-telegram-saved.js';
|
|
6
7
|
import { intakeOutboxDir, kstDay } from './route.js';
|
|
7
8
|
|
|
8
9
|
export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string }
|
|
9
|
-
export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number }
|
|
10
|
+
export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
|
|
10
11
|
|
|
11
12
|
const AXIS_LABEL: Record<string, string> = {
|
|
12
13
|
video_automation: '영상 자동화', agent_basics: '에이전트 기본', agent_applied: '에이전트 응용', unrelated: '그 밖',
|
|
@@ -36,7 +37,10 @@ function readJsonl(file: string): Record<string, unknown>[] {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** `day`(KST YYYY-MM-DD)에 흡수·갈래가 끝난 항목과 그날의 산출 큐를 모은다. */
|
|
39
|
-
|
|
40
|
+
/** Saved Messages silent for more than two days → the digest says so (a quiet 0 must not look like «nothing to do»). */
|
|
41
|
+
const SAVED_SILENCE_MS = 48 * 3600_000;
|
|
42
|
+
|
|
43
|
+
export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date()): IntakeDigest {
|
|
40
44
|
const items = [...loadIntakeLedger(root).items.values()]
|
|
41
45
|
.filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
|
|
42
46
|
const absorbed = items.map((i: IntakeItem): DigestEntry => {
|
|
@@ -53,16 +57,20 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
|
|
|
53
57
|
const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
|
|
54
58
|
const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
|
|
55
59
|
const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
|
|
60
|
+
const lastNewAt = readSavedCursorAt(root);
|
|
61
|
+
const silentMs = lastNewAt ? now.getTime() - Date.parse(lastNewAt) : NaN;
|
|
56
62
|
return {
|
|
57
63
|
day, absorbed, goals, review, grounding,
|
|
58
64
|
release: readJsonl(join(out, 'release', `${day}.jsonl`)).length,
|
|
59
65
|
manual: readJsonl(join(out, 'manual', `${day}.jsonl`)).length,
|
|
66
|
+
...(lastNewAt && silentMs > SAVED_SILENCE_MS ? { savedSilence: { days: Math.floor(silentMs / 86_400_000), lastNewAt } } : {}),
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/** 노트 절 — 축별 묶음 · 노트 링크 · 한 줄 요약 · 골 후보. */
|
|
64
71
|
export function renderDigestMarkdown(d: IntakeDigest): string {
|
|
65
72
|
const L: string[] = [`## 📰 오늘의 흡수 요약 (${d.absorbed.length})`, ''];
|
|
73
|
+
if (d.savedSilence) L.push(`> ⚠️ 텔레그램 «저장된 메시지»에서 새 글을 ${d.savedSilence.days}일째 못 받았다(마지막 새 글 수집 ${kstDay(d.savedSilence.lastNewAt)}) — 저장했는데 안 들어왔다면 저장한 곳(본인 «저장된 메시지»인지)을 확인한다.`, '');
|
|
66
74
|
if (!d.absorbed.length) { L.push('오늘 흡수한 것이 없다.', ''); return L.join('\n'); }
|
|
67
75
|
const byAxis = new Map<string, DigestEntry[]>();
|
|
68
76
|
for (const e of d.absorbed) (byAxis.get(e.axis) ?? byAxis.set(e.axis, []).get(e.axis)!).push(e);
|
|
@@ -89,6 +97,7 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
|
|
|
89
97
|
/** 텔레그램 짧은 판 — 수 · 한 줄 요약 다섯 · 골 후보 · 노트 자리. */
|
|
90
98
|
export function renderDigestTelegram(d: IntakeDigest, opts: { vaultRoot?: string; notePath?: string } = {}): string {
|
|
91
99
|
const L = [`📰 *${d.day} 흡수 ${d.absorbed.length}편*`];
|
|
100
|
+
if (d.savedSilence) L.push(`⚠️ 저장된 메시지 새 글 ${d.savedSilence.days}일째 0 — 저장한 곳 확인`);
|
|
92
101
|
for (const e of d.absorbed.slice(0, 5)) L.push(`• ${e.oneLiner ?? e.noteName ?? e.url ?? e.id}`);
|
|
93
102
|
if (d.absorbed.length > 5) L.push(`… 외 ${d.absorbed.length - 5}편`);
|
|
94
103
|
if (d.goals.length) L.push('', `🔴 엘라누스에 없는 것 ${d.goals.length}건 — 골 후보`);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 상태 = `<state>/live/detail.json` `{ scope: 'all' | '<runId>', until: <epoch ms>, by?, since }`.
|
|
8
8
|
// - 넥서스(`/v1/live/detail`)가 쓰고, 하니스·런(다른 프로세스)은 파일을 읽는다 — 프로세스 경계를 넘는 가장 단순한 관.
|
|
9
9
|
// - `until` 이 지나면 꺼진 것이다(자동 꺼짐 · 파일을 지우는 사람이 없어도 된다).
|
|
10
|
-
// ⛔ 읽기 실패·깨진 파일은 «꺼짐» —
|
|
10
|
+
// ⛔ 읽기 실패·깨진 파일은 기본 «꺼짐» — 단 유효한 미래 ELANOUS_LIVE_DETAIL_UNTIL 은 전체 범위를 켠다.
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
@@ -107,10 +107,15 @@ export function selectLiveDetail(opts: DetailReadOptions = {}): LiveDetailSelect
|
|
|
107
107
|
return { ...selection, state: parsed && parsed.until > now ? parsed : null };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/** 이 런에 상세 관측이 켜져 있나 —
|
|
110
|
+
/** 이 런에 상세 관측이 켜져 있나 — 파일은 5초 캐시, env 만료는 매번 확인한다. */
|
|
111
111
|
export function isLiveDetailOn(runId?: string, opts: DetailReadOptions = {}): boolean {
|
|
112
112
|
const now = opts.now ?? Date.now();
|
|
113
|
-
|
|
113
|
+
const state = selectLiveDetail({ ...opts, now }).state;
|
|
114
|
+
if (state) return matches(state, runId, now);
|
|
115
|
+
const rawUntil = process.env.ELANOUS_LIVE_DETAIL_UNTIL;
|
|
116
|
+
if (!rawUntil || !/^\d+$/.test(rawUntil)) return false;
|
|
117
|
+
const until = Number(rawUntil);
|
|
118
|
+
return Number.isSafeInteger(until) && until > now;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
function matches(state: LiveDetailState | null, runId: string | undefined, now: number): boolean {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { decideProviderForConfig, type ProviderDecision } from '../llm.js';
|
|
2
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
3
|
+
|
|
4
|
+
export interface UsableLlm {
|
|
5
|
+
usable: boolean;
|
|
6
|
+
provider?: string;
|
|
7
|
+
via: 'login' | 'key' | 'local-server' | 'none';
|
|
8
|
+
why: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UsableLlmDeps {
|
|
12
|
+
config?: UserConfig;
|
|
13
|
+
/** The same selector used to construct a runtime LLM; injectable for isolated tests. */
|
|
14
|
+
decide?: (config: UserConfig) => ProviderDecision;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Evaluate the selected route, not the union of discovered credentials or servers. */
|
|
18
|
+
export function resolveUsableLlm(deps: UsableLlmDeps = {}): UsableLlm {
|
|
19
|
+
const decision = (deps.decide ?? decideProviderForConfig)(deps.config ?? getUserConfig());
|
|
20
|
+
const provider = decision.provider.replace(/^auto:/, '');
|
|
21
|
+
// Never echo an untrusted configured provider or a model/URL/credential into output.
|
|
22
|
+
const known = ['openai-codex', 'openai', 'anthropic', 'grok', 'gemini', 'openrouter', 'local'];
|
|
23
|
+
if (provider === 'auto' || decision.auth === 'none') {
|
|
24
|
+
// An explicitly selected provider stays named so callers can offer that provider's fix,
|
|
25
|
+
// not the Codex login (the auto route's default remedy).
|
|
26
|
+
const explicit = !decision.provider.startsWith('auto') && known.includes(provider);
|
|
27
|
+
return { usable: false, ...(explicit ? { provider } : {}), via: 'none', why: 'no usable LLM route selected' };
|
|
28
|
+
}
|
|
29
|
+
if (!known.includes(provider)) return { usable: false, via: 'none', why: 'no usable LLM route selected' };
|
|
30
|
+
const via = decision.auth === 'oauth' ? 'login' : decision.auth === 'apikey' ? 'key' : 'local-server';
|
|
31
|
+
return { usable: true, provider, via, why: `LLM via ${via} (${provider})` };
|
|
32
|
+
}
|
package/src/loops/registry.ts
CHANGED
|
@@ -48,9 +48,13 @@ function graphFiles(dir: string): string[] {
|
|
|
48
48
|
return entries.flatMap(e => e.isDirectory() ? graphFiles(join(dir, e.name)) : e.isFile() && /\.ya?ml$/.test(e.name) ? [join(dir, e.name)] : []).sort();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** A wrapper cron that runs the graph itself declares its loop with a trailing `# elanous-loop=<graph_id>` comment. */
|
|
52
|
+
const LOOP_MARKER = /#\s*elanous-loop=([A-Za-z0-9][\w.-]*)\s*$/;
|
|
53
|
+
|
|
51
54
|
/** Match only the actual graph CLI invocation, not a filename mentioned in a shell argument. */
|
|
52
|
-
export function graphJobMatches(command: string | null, file: string, root: string): boolean {
|
|
55
|
+
export function graphJobMatches(command: string | null, file: string, root: string, graphId?: string): boolean {
|
|
53
56
|
if (!command) return false;
|
|
57
|
+
if (graphId && command.match(LOOP_MARKER)?.[1] === graphId) return true;
|
|
54
58
|
const invocation = /(?:^|&&\s*|;\s*)(?:(?:\S*\/)?(?:bun|node)\s+(?:\S*\/)?bin\/elanous\.mjs|(?:\S*\/)?elanous)\s+(?:--test(?:=\S+|\s+\S+)\s+)?graph\s+run\s+(['"]?)([^\s'";&|]+)\1(?=\s|$|[;&|])/g;
|
|
55
59
|
for (const match of command.matchAll(invocation)) {
|
|
56
60
|
const arg = match[2]!;
|
|
@@ -100,8 +104,17 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
|
|
|
100
104
|
} finally { db.close(); }
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
|
|
108
|
+
* the installed package when there is one, else this checkout. Never the caller's cwd: a loop started
|
|
109
|
+
* from a disposable worktree would schedule a cron into a directory that is about to disappear (🅣 09-29). */
|
|
110
|
+
export function defaultLoopRoot(): string {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
112
|
+
const { installedCronRoot, repoRoot } = require('../domains/schedule-registry.js') as typeof import('../domains/schedule-registry.js');
|
|
113
|
+
return installedCronRoot() ?? repoRoot();
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
104
|
-
const root = resolve(opts.root ??
|
|
117
|
+
const root = resolve(opts.root ?? defaultLoopRoot());
|
|
105
118
|
const stateRoot = opts.stateRoot ?? effectiveInstanceRoot();
|
|
106
119
|
const rows = schedules(opts);
|
|
107
120
|
const loops: LoopEntry[] = [];
|
|
@@ -117,7 +130,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
|
117
130
|
const trigger = header?.trigger && typeof header.trigger === 'object' && !Array.isArray(header.trigger) ? header.trigger as Record<string, unknown> : null;
|
|
118
131
|
const declaredCron = typeof trigger?.cron === 'string' ? trigger.cron : null;
|
|
119
132
|
const events = Array.isArray(trigger?.events) ? trigger.events.filter((e): e is string => typeof e === 'string') : [];
|
|
120
|
-
const matched = rows.filter(row => graphJobMatches(row.command, file, root) && row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
|
|
133
|
+
const matched = rows.filter(row => graphJobMatches(row.command, file, root, doc.graph_id as string) && row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
|
|
121
134
|
if (!declaredCron && !events.length && !matched.length) continue;
|
|
122
135
|
if (!/^[a-zA-Z0-9][\w.-]*$/.test(doc.graph_id)) throw new Error(`unsafe loop graph_id: ${doc.graph_id}`);
|
|
123
136
|
if (loops.some(loop => loop.id === doc.graph_id)) throw new Error(`duplicate loop graph_id: ${doc.graph_id}`);
|
|
@@ -142,6 +155,15 @@ export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntr
|
|
|
142
155
|
return { ...loop, recentRuns: recent };
|
|
143
156
|
}
|
|
144
157
|
|
|
158
|
+
/** The crontab line a loop runs — same shape as the other elanous crons: cron starts in $HOME with a thin PATH,
|
|
159
|
+
* so a bare `elanous graph run <relative file>` fails silently every tick (09-29 steward). */
|
|
160
|
+
export function loopCronCommand(id: string, file: string, root: string, bun = process.execPath): string {
|
|
161
|
+
// Quote only when needed: the other crons (and `graphJobMatches`) use bare absolute paths.
|
|
162
|
+
const q = (value: string) => /^[\w./@+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
163
|
+
const log = `/tmp/elanous-loop-${id.replace(/[^\w.-]/g, '_')}.log`;
|
|
164
|
+
return `cd ${q(root)} && ${q(bun)} bin/elanous.mjs graph run ${q(file)} >> ${log} 2>&1`;
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
export async function setLoopEnabled(id: string, enabled: boolean, yes = false, opts: LoopRegistryOptions = {}): Promise<unknown> {
|
|
146
168
|
const loop = loopStatus(id, opts);
|
|
147
169
|
const action = enabled ? 'start' : 'stop';
|
|
@@ -149,7 +171,7 @@ export async function setLoopEnabled(id: string, enabled: boolean, yes = false,
|
|
|
149
171
|
const pending = loop.jobs.filter(job => job.enabled !== enabled);
|
|
150
172
|
if (!pending.length && loop.jobs.length) return { action, id, changed: false, enabled: loop.enabled };
|
|
151
173
|
const changes = loop.jobs.length ? pending.map(job => ({ action: enabled ? 'enable' : 'disable', id: job.id })) :
|
|
152
|
-
[{ action: 'create', cron: loop.trigger.cron, command:
|
|
174
|
+
[{ action: 'create', cron: loop.trigger.cron, command: loopCronCommand(id, loop.file, resolve(opts.root ?? defaultLoopRoot())) }];
|
|
153
175
|
if (!yes) return { action, id, dryRun: true, changes, note: 'Apply with --yes (crontab backup).' };
|
|
154
176
|
const dispatch = opts.scheduleAction ?? (async (kind: 'create' | 'enable' | 'disable', args: Record<string, unknown>) => {
|
|
155
177
|
const { dispatchScheduleManage } = await import('../domains/schedule-manage-tool.js');
|
|
@@ -170,5 +192,5 @@ export async function setLoopEnabled(id: string, enabled: boolean, yes = false,
|
|
|
170
192
|
export async function runLoop(id: string, dryRun = false, opts: LoopRegistryOptions = {}): Promise<GraphRunState> {
|
|
171
193
|
const loop = loopStatus(id, opts);
|
|
172
194
|
debug.log('loops.registry', 'run', { id, dryRun });
|
|
173
|
-
return runGraph(join(resolve(opts.root ??
|
|
195
|
+
return runGraph(join(resolve(opts.root ?? defaultLoopRoot()), loop.file), { dryRun, deps: { root: opts.stateRoot ?? effectiveInstanceRoot() } });
|
|
174
196
|
}
|
package/src/market/publish.ts
CHANGED
|
@@ -7,6 +7,41 @@ import { debug } from '../debug/log.js';
|
|
|
7
7
|
import { packDirDeterministic } from './tgz.js';
|
|
8
8
|
import { signIndex, verifyIndex, type MarketplaceIndex } from './signed-index.js';
|
|
9
9
|
|
|
10
|
+
type IndexConnector = MarketplaceIndex['plugins'][number]['ai.elanous']['connectors'][number];
|
|
11
|
+
|
|
12
|
+
function normalizeConnectors(connectors: unknown): { ok: true; value: IndexConnector[] } | { ok: false } {
|
|
13
|
+
if (!Array.isArray(connectors)) return { ok: false };
|
|
14
|
+
const value: IndexConnector[] = [];
|
|
15
|
+
const record = (item: unknown): item is Record<string, unknown> =>
|
|
16
|
+
item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
17
|
+
for (const connector of connectors) {
|
|
18
|
+
if (!record(connector) || typeof connector.id !== 'string' || !connector.id) return { ok: false };
|
|
19
|
+
if (Array.isArray(connector.userConfig)) {
|
|
20
|
+
if (typeof connector.kind !== 'string' || !connector.userConfig.every((field: unknown) =>
|
|
21
|
+
record(field) && typeof field.key === 'string' && field.key.trim().length > 0 && typeof field.label === 'string' &&
|
|
22
|
+
typeof field.secret === 'boolean' && (field.env === undefined || typeof field.env === 'string'))) {
|
|
23
|
+
return { ok: false };
|
|
24
|
+
}
|
|
25
|
+
value.push(connector as IndexConnector);
|
|
26
|
+
} else if (Array.isArray(connector.fields)) {
|
|
27
|
+
if ((connector.kind !== undefined && typeof connector.kind !== 'string') || !connector.fields.every((field: unknown) =>
|
|
28
|
+
record(field) && typeof field.name === 'string' && field.name.trim().length > 0 && typeof field.secret === 'boolean' &&
|
|
29
|
+
(field.label === undefined || typeof field.label === 'string') &&
|
|
30
|
+
(field.env === undefined || typeof field.env === 'string'))) {
|
|
31
|
+
return { ok: false };
|
|
32
|
+
}
|
|
33
|
+
value.push({ id: connector.id, kind: (connector.kind ?? 'credentials') as string,
|
|
34
|
+
userConfig: connector.fields.map((field: Record<string, unknown>) => ({
|
|
35
|
+
key: field.name as string, label: (field.label ?? field.name) as string, secret: field.secret === true,
|
|
36
|
+
...(field.env ? { env: field.env as string } : {}),
|
|
37
|
+
})) });
|
|
38
|
+
} else {
|
|
39
|
+
return { ok: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, value };
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
function unpackLocal(archive: Uint8Array, destination: string): void {
|
|
11
46
|
const tar = gunzipSync(archive);
|
|
12
47
|
const field = (part: Buffer) => part.toString('utf8').replace(/\0.*$/, '');
|
|
@@ -113,13 +148,15 @@ export function publishMarket(input: {
|
|
|
113
148
|
skip('invalid-plugin.json'); continue;
|
|
114
149
|
}
|
|
115
150
|
const { category, bundle, ...metadata } = ai as MarketplaceIndex['plugins'][number]['ai.elanous'] & { category?: string; bundle?: unknown };
|
|
116
|
-
if (!Array.isArray(metadata.capabilities) ||
|
|
151
|
+
if (!Array.isArray(metadata.capabilities) ||
|
|
117
152
|
(category !== undefined && typeof category !== 'string')) {
|
|
118
153
|
skip('invalid-plugin.json'); continue;
|
|
119
154
|
}
|
|
120
155
|
if (metadata.pricing !== undefined && metadata.pricing?.model !== 'free') {
|
|
121
156
|
skip('paid-not-allowed-in-M0'); continue;
|
|
122
157
|
}
|
|
158
|
+
const normalized = normalizeConnectors(metadata.connectors);
|
|
159
|
+
if (!normalized.ok) { skip('invalid-connectors'); continue; }
|
|
123
160
|
const bundleItem = (item: unknown): item is string | { from: string; as: string } => typeof item === 'string' ||
|
|
124
161
|
(!!item && typeof item === 'object' && !Array.isArray(item) && typeof (item as { from?: unknown }).from === 'string' &&
|
|
125
162
|
typeof (item as { as?: unknown }).as === 'string');
|
|
@@ -208,7 +245,26 @@ export function publishMarket(input: {
|
|
|
208
245
|
if (message.startsWith('bundle-conflict')) { skip('bundle-conflict'); continue; }
|
|
209
246
|
throw error;
|
|
210
247
|
}
|
|
211
|
-
|
|
248
|
+
let sha256 = createHash('sha256').update(archive).digest('hex');
|
|
249
|
+
const recorded = recordedHashes.get(JSON.stringify([manifest.name, manifest.version]));
|
|
250
|
+
if (recorded && recorded !== sha256) {
|
|
251
|
+
const existingPath = join(input.outDir, manifest.name, manifest.version, `${recorded}.tgz`);
|
|
252
|
+
let sameContent = false;
|
|
253
|
+
if (existsSync(existingPath)) {
|
|
254
|
+
try {
|
|
255
|
+
const existing = readFileSync(existingPath);
|
|
256
|
+
sameContent = createHash('sha256').update(existing).digest('hex') === recorded &&
|
|
257
|
+
gunzipSync(existing).equals(gunzipSync(archive));
|
|
258
|
+
if (sameContent) {
|
|
259
|
+
archive = existing;
|
|
260
|
+
sha256 = recorded;
|
|
261
|
+
}
|
|
262
|
+
} catch { /* An unreadable archive cannot prove immutability. */ }
|
|
263
|
+
}
|
|
264
|
+
if (!sameContent) throw new Error(`version-immutable: ${manifest.name}@${manifest.version}`);
|
|
265
|
+
debug.log('market.publish', 'reused-artifact', { name: manifest.name, version: manifest.version,
|
|
266
|
+
reason: 'same-content-different-gzip' });
|
|
267
|
+
}
|
|
212
268
|
remember(manifest.name, manifest.version, sha256);
|
|
213
269
|
const key = `${manifest.name}/${manifest.version}/${sha256}.tgz`;
|
|
214
270
|
const source: MarketplaceIndex['plugins'][number]['source'] = input.source
|
|
@@ -218,7 +274,7 @@ export function publishMarket(input: {
|
|
|
218
274
|
policy: { installation: 'AVAILABLE', authentication: 'ON_USE' },
|
|
219
275
|
category: category ?? 'Productivity', version: manifest.version,
|
|
220
276
|
description: manifest.description, artifact: { sha256, bytes: archive.length, key },
|
|
221
|
-
'ai.elanous': { ...metadata, pricing: metadata.pricing ?? { model: 'free' } },
|
|
277
|
+
'ai.elanous': { ...metadata, connectors: normalized.value, pricing: metadata.pricing ?? { model: 'free' } },
|
|
222
278
|
});
|
|
223
279
|
archives.push({ key, data: archive, name: dir });
|
|
224
280
|
published.push({ name: manifest.name, version: manifest.version, sha256, bundled });
|
|
@@ -1142,8 +1142,8 @@ export const nativeToolCatalog: NativeToolCatalogEntry[] = [
|
|
|
1142
1142
|
kind: 'web',
|
|
1143
1143
|
aliases: ['WebSearch', 'web_search'],
|
|
1144
1144
|
displayName: 'WebSearch',
|
|
1145
|
-
description: 'Search the public web via a configured provider (Grok live-search / Firecrawl / MCP / custom).',
|
|
1146
|
-
promptSummary: '`WebSearch` (provider-backed web search — Grok live-search by default; pair with WebFetch for full bodies)',
|
|
1145
|
+
description: 'Search the public web via a configured provider (Grok live-search / Firecrawl / MCP / custom); when no key is available, fall back to keyless DuckDuckGo.',
|
|
1146
|
+
promptSummary: '`WebSearch` (provider-backed web search — Grok live-search by default; when no key is available, fall back to keyless DuckDuckGo; pair with WebFetch for full bodies)',
|
|
1147
1147
|
host: ['skill'],
|
|
1148
1148
|
safety: ['network', 'read-only'],
|
|
1149
1149
|
supportsParallel: true,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug } from '../../debug/log.js';
|
|
4
|
+
import { decideGraphApproval, lastJsonObject, type GraphRunState } from '../../graph-runner/runner.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { jsonResponse } from './json-response.js';
|
|
7
|
+
|
|
8
|
+
export const GRAPH_APPROVALS_PATH = '/v1/graph-approvals';
|
|
9
|
+
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
10
|
+
|
|
11
|
+
interface GraphApprovalItem {
|
|
12
|
+
graphId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
nodeId: string;
|
|
15
|
+
message: string;
|
|
16
|
+
since: string;
|
|
17
|
+
path: string[];
|
|
18
|
+
recent: Array<{ nodeId: string; ok: boolean; outcome?: string; summary?: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GraphApprovalsDeps {
|
|
22
|
+
authorize?: (request: Request) => boolean;
|
|
23
|
+
/** Credential the request carries, checked independently of same-origin (`bearerCredential`).
|
|
24
|
+
* A decision needs a bearer token: same-origin alone is not trusted because behind
|
|
25
|
+
* `tailscale serve` every tailnet peer (Pod included) arrives from loopback. */
|
|
26
|
+
authReason?: (request: Request) => string | undefined;
|
|
27
|
+
root?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DECISION_AUTH_REASONS = new Set(['bearer-match', 'temp-token', 'noauth']);
|
|
31
|
+
|
|
32
|
+
function validSegment(value: string): boolean {
|
|
33
|
+
return SEGMENT.test(value) && value !== '.' && value !== '..';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readRun(root: string, graphId: string, runId: string): GraphRunState | null {
|
|
37
|
+
if (!validSegment(graphId) || !validSegment(runId)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const state = JSON.parse(readFileSync(join(root, 'graph-runs', graphId, `${runId}.json`), 'utf8')) as GraphRunState;
|
|
40
|
+
return state.graphId === graphId && state.runId === runId ? state : null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function undecided(state: GraphRunState, root: string): boolean {
|
|
48
|
+
return state.status === 'awaiting-approval' && !!state.pending && !state.pending.decision
|
|
49
|
+
&& Array.isArray(state.path) && !existsSync(join(root, 'graph-runs', state.graphId, `${state.runId}.json.${state.path.length}.decision.json`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function list(root: string): GraphApprovalItem[] {
|
|
53
|
+
const dir = join(root, 'graph-runs');
|
|
54
|
+
let graphs: Dirent[];
|
|
55
|
+
try { graphs = readdirSync(dir, { withFileTypes: true }); }
|
|
56
|
+
catch (error) {
|
|
57
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const items: GraphApprovalItem[] = [];
|
|
61
|
+
for (const graph of graphs) {
|
|
62
|
+
if (!graph.isDirectory() || !validSegment(graph.name)) continue;
|
|
63
|
+
for (const file of readdirSync(join(dir, graph.name), { withFileTypes: true })) {
|
|
64
|
+
if (!file.isFile() || !file.name.endsWith('.json') || file.name.endsWith('.decision.json')) continue;
|
|
65
|
+
const runId = file.name.slice(0, -5);
|
|
66
|
+
const state = readRun(root, graph.name, runId);
|
|
67
|
+
if (!state || !undecided(state, root) || !Array.isArray(state.nodes)) continue;
|
|
68
|
+
const pending = state.pending!;
|
|
69
|
+
items.push({
|
|
70
|
+
graphId: state.graphId, runId: state.runId, nodeId: pending.nodeId, message: pending.message,
|
|
71
|
+
since: pending.since, path: state.path,
|
|
72
|
+
recent: state.nodes.slice(-5).map((node) => {
|
|
73
|
+
const output = lastJsonObject(node.output);
|
|
74
|
+
return {
|
|
75
|
+
nodeId: node.nodeId, ok: node.ok,
|
|
76
|
+
...(typeof output?.outcome === 'string' ? { outcome: output.outcome } : {}),
|
|
77
|
+
...(typeof output?.summary === 'string' ? { summary: output.summary } : {}),
|
|
78
|
+
};
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return items.sort((a, b) => b.since.localeCompare(a.since));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function handleGraphApprovals(req: Request, deps: GraphApprovalsDeps = {}): Promise<Response> {
|
|
87
|
+
const pathname = new URL(req.url).pathname;
|
|
88
|
+
const match = /^\/v1\/graph-approvals\/([^/]+)\/([^/]+)$/.exec(pathname);
|
|
89
|
+
if (pathname !== GRAPH_APPROVALS_PATH && !match) return jsonResponse({ error: 'not-found' }, 404);
|
|
90
|
+
if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
91
|
+
if (pathname === GRAPH_APPROVALS_PATH) {
|
|
92
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
93
|
+
return jsonResponse({ items: list(deps.root ?? effectiveInstanceRoot()) });
|
|
94
|
+
}
|
|
95
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
96
|
+
const authReason = deps.authReason?.(req);
|
|
97
|
+
if (!authReason || !DECISION_AUTH_REASONS.has(authReason)) {
|
|
98
|
+
debug.log('approvals.graph', 'refused', { reason: 'pairing-required', auth: authReason ?? null });
|
|
99
|
+
return jsonResponse({ error: 'pairing-required' }, 403);
|
|
100
|
+
}
|
|
101
|
+
let graphId: string;
|
|
102
|
+
let runId: string;
|
|
103
|
+
try { graphId = decodeURIComponent(match![1]!); runId = decodeURIComponent(match![2]!); }
|
|
104
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
105
|
+
if (!validSegment(graphId) || !validSegment(runId)) return jsonResponse({ error: 'not-found' }, 404);
|
|
106
|
+
let body: unknown;
|
|
107
|
+
try { body = await req.json(); }
|
|
108
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
109
|
+
const decision = body && typeof body === 'object' && !Array.isArray(body) && 'decision' in body ? body.decision : undefined;
|
|
110
|
+
if (decision !== 'approved' && decision !== 'rejected') return jsonResponse({ error: 'bad_request' }, 400);
|
|
111
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
112
|
+
const state = readRun(root, graphId, runId);
|
|
113
|
+
if (!state) return jsonResponse({ error: 'not-found' }, 404);
|
|
114
|
+
if (!undecided(state, root)) return jsonResponse({ error: 'already-decided' }, 409);
|
|
115
|
+
const nodeId = state.pending!.nodeId;
|
|
116
|
+
try {
|
|
117
|
+
decideGraphApproval(graphId, runId, decision, `pwa:${authReason}`, root);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return jsonResponse({ error: 'not-found' }, 404);
|
|
120
|
+
if (error instanceof Error && error.message.startsWith('run is not awaiting an undecided approval:')) return jsonResponse({ error: 'already-decided' }, 409);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
debug.log('approvals.graph', 'decided', { graphId, runId, nodeId, decision, by: `pwa:${authReason}` });
|
|
124
|
+
return jsonResponse({ graphId, runId, decision });
|
|
125
|
+
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
21
|
+
import { ensureAuthToken } from '../../auth/acp-token.js';
|
|
20
22
|
import type { NexusState } from '../state/state.js';
|
|
21
23
|
import { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
|
|
22
24
|
export { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
|
|
@@ -195,6 +197,7 @@ import { handleMcpResourceGet, MCP_RESOURCE_ROUTE_PATH } from './mcp-resource-ro
|
|
|
195
197
|
// ⛔ 라우트 상수를 «디스패처»가 안 쓰고 문자열로 베끼고 있었다(16차 실측) — 잎에서 읽는다.
|
|
196
198
|
import { APPROVALS_MERGES_PATH, DESIGN_DIRECTION_PATH, DESIGN_PREVIEW_PATH_PREFIX, DESIGN_PREVIEWS_PATH, DESIGN_SYSTEM_PATH, IPA_PATH_PREFIX, MANIFEST_PATH } from './rest-route-paths.js';
|
|
197
199
|
import { handleMergeApprovals } from './merge-approvals.js';
|
|
200
|
+
import { GRAPH_APPROVALS_PATH, handleGraphApprovals } from './graph-approvals.js';
|
|
198
201
|
import { handleLiveDetail, LIVE_DETAIL_PATH } from './live-detail.js';
|
|
199
202
|
import { handleLiveShipped, LIVE_SHIPPED_PATH } from './live-shipped.js';
|
|
200
203
|
import { handleMcpWidgetCall, MCP_WIDGET_CALL_ROUTE_PATH, persistWidgetTurnToSessionStore } from './mcp-widget-call-route.js';
|
|
@@ -300,6 +303,7 @@ import {
|
|
|
300
303
|
handleVoiceCost,
|
|
301
304
|
handleVoiceTranscribe,
|
|
302
305
|
checkAuth,
|
|
306
|
+
bearerCredential,
|
|
303
307
|
registerAuthPeerAddress,
|
|
304
308
|
type MetaApiOpts,
|
|
305
309
|
} from './meta-api.js';
|
|
@@ -332,7 +336,10 @@ import { handleTriggersSnapshot } from './triggers.js';
|
|
|
332
336
|
import { handleWorkflowTemplatesList } from './workflow-templates.js';
|
|
333
337
|
import { handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
334
338
|
import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
|
|
335
|
-
import {
|
|
339
|
+
import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
|
|
340
|
+
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
341
|
+
import { handlePluginsIndexGet, handlePluginsGet, handlePluginsMarketRefresh, handlePluginsInstall, handlePluginsRemove } from './plugins-market.js';
|
|
342
|
+
import { handlePluginsCredentialsGet, handlePluginsCredentialsPut } from './plugins-credentials-api.js';
|
|
336
343
|
import {
|
|
337
344
|
handleWorkflowsList,
|
|
338
345
|
} from './workflows.js';
|
|
@@ -369,7 +376,7 @@ import {
|
|
|
369
376
|
parseAgentCliSessionPath,
|
|
370
377
|
} from './agent-cli.js';
|
|
371
378
|
import { handleSessionTurnControl, SESSION_TURN_CONTROL_PATH } from '../../session/session-turn-control.js';
|
|
372
|
-
import { authenticatePodCredential, handlePodGrokCredential, POD_CREDENTIAL_GROK_PATH } from './pod-credential-api.js';
|
|
379
|
+
import { authenticatePodCredential, handlePodGrokCredential, handlePodGithubCredential, POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH } from './pod-credential-api.js';
|
|
373
380
|
import { handleOutboundReport } from './outbound-report.js';
|
|
374
381
|
import { handleSelfEvent } from './self-event.js';
|
|
375
382
|
/** 매매 «실행»은 애드온이다(대표 09-20 결정 ③ · 별도 상용 저장소 · release/trading-export.yaml) — 공개 코어엔 없다.
|
|
@@ -436,6 +443,8 @@ export function probeNexusHttpPort(
|
|
|
436
443
|
export type NexusWsBridgeInit = Omit<WsBridgeOpts, 'hostname' | 'port'>;
|
|
437
444
|
|
|
438
445
|
export interface NexusHttpServerOpts {
|
|
446
|
+
/** Optional installer state root for isolated API consumers and tests. */
|
|
447
|
+
pluginStateRoot?: string;
|
|
439
448
|
state: NexusState;
|
|
440
449
|
registry: TabRegistry;
|
|
441
450
|
eventBus: NexusEventBus;
|
|
@@ -822,19 +831,27 @@ export async function routeRequest(
|
|
|
822
831
|
// Per-route checkAuth calls below stay in place.
|
|
823
832
|
// OPTIONS is a CORS preflight and cannot carry Authorization; existing
|
|
824
833
|
// handlers answer it themselves, so the gate does not swallow it.
|
|
834
|
+
// A runtime-less tab mutation still needs authentication before the
|
|
835
|
+
// dispatcher can return its 503. Do not enable this fallback for other paths.
|
|
836
|
+
const runtimeLessTabMutation = !opts.metaApi && method !== 'GET' && method !== 'OPTIONS'
|
|
837
|
+
&& (pathname === '/v1/nexus/tabs' || pathname.startsWith('/v1/nexus/tabs/'));
|
|
825
838
|
if (
|
|
826
839
|
method !== 'OPTIONS'
|
|
827
840
|
&& pathname.startsWith('/v1/')
|
|
828
841
|
&& !isPublicRoute(method, pathname, { setupMode: false })
|
|
829
842
|
) {
|
|
830
|
-
|
|
843
|
+
const fallbackToken = runtimeLessTabMutation ? ensureAuthToken().token : undefined;
|
|
844
|
+
const offered = req.headers.get('authorization');
|
|
845
|
+
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
846
|
+
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
847
|
+
if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized)) {
|
|
831
848
|
const name = method === 'POST' && pathname === '/v1/tasks' && opts.metaApi
|
|
832
849
|
? matchIngestAuthorization(req)?.name
|
|
833
850
|
: undefined;
|
|
834
851
|
if (name === undefined) {
|
|
835
852
|
debug.log('nexus.auth', 'default-deny', { method, pathname });
|
|
836
853
|
// MCP 클라이언트(Claude Code)에게는 «할 일»을 말한다 — 안 그러면 OAuth 로 가서 /register 405 를 만난다.
|
|
837
|
-
if (pathname === '/v1/mcp') return mcpUnauthorizedResponse();
|
|
854
|
+
if (pathname === '/v1/mcp') return mcpUnauthorizedResponse(new URL(req.url).origin);
|
|
838
855
|
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
839
856
|
}
|
|
840
857
|
debug.log('nexus.auth', 'ingest-token', { name, pathname });
|
|
@@ -843,7 +860,7 @@ export async function routeRequest(
|
|
|
843
860
|
// OAuth 동적 클라이언트 등록(RFC 7591) — Elanous 는 OAuth 를 쓰지 않는다. 405 대신 «이렇게 등록하라»를 돌려준다.
|
|
844
861
|
if (method === 'POST' && pathname === '/register') {
|
|
845
862
|
debug.log('nexus.auth', 'oauth-register-rejected', { pathname });
|
|
846
|
-
return oauthRegistrationRejected();
|
|
863
|
+
return oauthRegistrationRejected(new URL(req.url).origin);
|
|
847
864
|
}
|
|
848
865
|
|
|
849
866
|
// FU2 (2026-05-12) — root-level browser conveniences. The PWA serves
|
|
@@ -899,6 +916,12 @@ export async function routeRequest(
|
|
|
899
916
|
});
|
|
900
917
|
}
|
|
901
918
|
|
|
919
|
+
if (pathname === GRAPH_APPROVALS_PATH || pathname.startsWith(`${GRAPH_APPROVALS_PATH}/`)) {
|
|
920
|
+
return handleGraphApprovals(req, {
|
|
921
|
+
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
922
|
+
authReason: (request) => (opts.metaApi ? bearerCredential(request, opts.metaApi) : undefined),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
902
925
|
if (pathname === APPROVALS_MERGES_PATH || pathname.startsWith(`${APPROVALS_MERGES_PATH}/`)) {
|
|
903
926
|
return handleMergeApprovals(req, {
|
|
904
927
|
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
@@ -1189,6 +1212,25 @@ export async function routeRequest(
|
|
|
1189
1212
|
// Templates POST handled here too (PR κ).
|
|
1190
1213
|
// Config / secrets PUT/POST/DELETE here too (PR μ).
|
|
1191
1214
|
if (method !== 'GET') {
|
|
1215
|
+
if (method === 'POST' && pathname === '/v1/plugins/install') {
|
|
1216
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1217
|
+
return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
|
|
1218
|
+
}
|
|
1219
|
+
const marketRefresh = /^\/v1\/plugins\/markets\/([^/]+)\/refresh$/.exec(pathname);
|
|
1220
|
+
if (method === 'POST' && marketRefresh) {
|
|
1221
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1222
|
+
return handlePluginsMarketRefresh(req, marketRefresh[1]!, opts.metaApi, { root: opts.pluginStateRoot });
|
|
1223
|
+
}
|
|
1224
|
+
const pluginCredentials = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
|
|
1225
|
+
if (method === 'PUT' && pluginCredentials) {
|
|
1226
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1227
|
+
return handlePluginsCredentialsPut(req, pluginCredentials[1]!, opts.metaApi, opts.pluginStateRoot, opts.reloadMcpClients);
|
|
1228
|
+
}
|
|
1229
|
+
const pluginRemove = /^\/v1\/plugins\/([^/]+)$/.exec(pathname);
|
|
1230
|
+
if (method === 'DELETE' && pluginRemove) {
|
|
1231
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1232
|
+
return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
1233
|
+
}
|
|
1192
1234
|
if ((method === 'PUT' || method === 'POST') && pathname.startsWith('/v1/graphs/')) {
|
|
1193
1235
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1194
1236
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -1215,6 +1257,11 @@ export async function routeRequest(
|
|
|
1215
1257
|
if (!gate.ok) return gate.response;
|
|
1216
1258
|
return handlePodGrokCredential(req);
|
|
1217
1259
|
}
|
|
1260
|
+
if (pathname === POD_CREDENTIAL_GITHUB_PATH && method === 'POST') {
|
|
1261
|
+
const gate = await authenticatePodCredential(req, {}, 'gh-credential');
|
|
1262
|
+
if (!gate.ok) return gate.response;
|
|
1263
|
+
return handlePodGithubCredential(req);
|
|
1264
|
+
}
|
|
1218
1265
|
if (pathname === '/v1/harness/ask' && method === 'POST') {
|
|
1219
1266
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
|
|
1220
1267
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -2431,12 +2478,19 @@ export async function routeRequest(
|
|
|
2431
2478
|
// surface (`/v1/workflows` · `~/.elanous/workflows-runs/`) covers the same
|
|
2432
2479
|
// user need.
|
|
2433
2480
|
|
|
2481
|
+
const pluginCredentialsGet = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
|
|
2482
|
+
if (method === 'GET' && pluginCredentialsGet) {
|
|
2483
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2484
|
+
return handlePluginsCredentialsGet(req, pluginCredentialsGet[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
2485
|
+
}
|
|
2434
2486
|
if (method === 'GET' && (pathname === '/v1/plugins/index' || pathname === '/v1/plugins')) {
|
|
2435
2487
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2436
|
-
return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi) : handlePluginsGet(req, opts.metaApi);
|
|
2488
|
+
return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi, opts.pluginStateRoot) : handlePluginsGet(req, opts.metaApi, opts.pluginStateRoot);
|
|
2437
2489
|
}
|
|
2438
2490
|
if (method === 'GET' && pathname === '/v1/graph/kinds') {
|
|
2439
2491
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2492
|
+
try { syncInstalledPluginNodes(opts.pluginStateRoot ?? elanousStateRoot()); }
|
|
2493
|
+
catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
|
|
2440
2494
|
return handleGraphKindsGet(req, opts.metaApi);
|
|
2441
2495
|
}
|
|
2442
2496
|
// F-M1 read + P-F1 raw YAML. Writes live in the mutation block and never touch core graphs/.
|
|
@@ -301,6 +301,15 @@ export function activeStoreNames(deps: LogFabricDeps, nowMs = Date.now()): strin
|
|
|
301
301
|
return names;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/** 등록된 로그 저장소 수(이 인스턴스 ⊕ logs.db 가 있는 우주) — `@active` 가 연 수의 «분모». 못 세면 `null`(0 으로 접지 않는다). */
|
|
305
|
+
export function registeredLogStoreCount(deps: Pick<LogFabricDeps, 'instances'> = {}): number | null {
|
|
306
|
+
try {
|
|
307
|
+
const names = new Set((deps.instances ?? readLogInstances)().filter((view) => view.dbExists).map((view) => view.name));
|
|
308
|
+
names.add(resolveLogInstanceName());
|
|
309
|
+
return names.size;
|
|
310
|
+
} catch { return null; }
|
|
311
|
+
}
|
|
312
|
+
|
|
304
313
|
export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabricDeps = {}): Response {
|
|
305
314
|
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
306
315
|
const url = new URL(req.url);
|
|
@@ -374,7 +383,7 @@ export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabric
|
|
|
374
383
|
ts: new Date().toISOString(),
|
|
375
384
|
...(failedStores.length ? { failedStores } : {}),
|
|
376
385
|
...(selection.storeLimitReached ? { storeLimitReached: true } : {}),
|
|
377
|
-
...(activeStores ? { stores: activeStores } : {}),
|
|
386
|
+
...(activeStores ? { stores: activeStores, registeredStores: registeredLogStoreCount(deps) } : {}),
|
|
378
387
|
}, 200);
|
|
379
388
|
}
|
|
380
389
|
|