elanous 0.2.7 → 0.2.9
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +12 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- 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/doc-draft/graphs/run-step.ts +1 -1
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/cutoff.ts +20 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +45 -7
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +89 -0
- package/scripts/test-diet/node.ts +144 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +153 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/index.ts +9 -9
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/pr-cli.ts +6 -2
- package/src/cli/release-cli.ts +49 -8
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +87 -14
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- 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 +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- 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 +98 -11
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/maturity/feature-maturity.ts +2 -0
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +41 -1
- package/src/nexus/api/meta-api.ts +27 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +139 -0
- package/src/nexus/api/ops-seats-sources.ts +65 -0
- package/src/nexus/api/ops-seats.ts +114 -0
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/setup-claim.ts +21 -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/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +33 -69
- package/src/release-loop/feature-store.ts +327 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/seat-dispatch/ceo-commands.ts +65 -0
- package/src/self-dev/orchestrate.ts +18 -11
- 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/tools/usage-slash.ts +8 -7
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +29 -0
- package/src/telegram-run.ts +83 -13
- package/src/telegram.ts +34 -0
- package/src/thinking-line.ts +28 -20
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/user-config.ts +111 -13
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Live sources for /v1/ops/seats: the coordination PR and merged PRs through gh (automation App identity, proxy variables
|
|
2
|
+
// removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
|
|
3
|
+
import { DecisionLedger } from '../../decisions/decision-ledger.js';
|
|
4
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
5
|
+
import { getUserConfig } from '../../user-config.js';
|
|
6
|
+
import { kstDayRange, type ChannelComment, type MergedPr, type SeatsSources } from './ops-seats.js';
|
|
7
|
+
|
|
8
|
+
const GH_TIMEOUT_MS = 20_000;
|
|
9
|
+
|
|
10
|
+
async function ghLines(args: string[]): Promise<string[] | null> {
|
|
11
|
+
const env: Record<string, string> = {};
|
|
12
|
+
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
|
|
13
|
+
try {
|
|
14
|
+
const { githubAutomationToken } = await import('../../auth/github-app-token.js');
|
|
15
|
+
const token = githubAutomationToken();
|
|
16
|
+
if (token) env.GH_TOKEN = token;
|
|
17
|
+
} catch { /* machine login */ }
|
|
18
|
+
const proc = Bun.spawn(['gh', ...args], { stdin: 'ignore', stdout: 'pipe', stderr: 'ignore', env });
|
|
19
|
+
const timer = setTimeout(() => proc.kill(), GH_TIMEOUT_MS);
|
|
20
|
+
try {
|
|
21
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
22
|
+
return code === 0 ? out.split('\n').filter(Boolean) : null;
|
|
23
|
+
} finally { clearTimeout(timer); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function channelTarget(): { repo: string; pr: string } | null {
|
|
27
|
+
const raw = (getUserConfig().raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
|
|
28
|
+
const m = typeof raw === 'string' ? /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(raw.trim()) : null;
|
|
29
|
+
return m ? { repo: m[1]!, pr: m[2]! } : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nextPatch(version: string): string {
|
|
33
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
34
|
+
return `${major}.${minor}.${patch! + 1}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function liveSeatsSources(): SeatsSources {
|
|
38
|
+
return {
|
|
39
|
+
async channel(date) {
|
|
40
|
+
const target = channelTarget();
|
|
41
|
+
if (!target) return null;
|
|
42
|
+
const lines = await ghLines(['api', '--paginate', `repos/${target.repo}/issues/${target.pr}/comments?since=${kstDayRange(date).start}&per_page=100`,
|
|
43
|
+
'--jq', '.[] | {body: .body, createdAt: .created_at} | tojson']);
|
|
44
|
+
return lines?.map((line) => JSON.parse(line) as ChannelComment) ?? null;
|
|
45
|
+
},
|
|
46
|
+
async merged(date) {
|
|
47
|
+
const target = channelTarget();
|
|
48
|
+
if (!target) return null;
|
|
49
|
+
const { start, end } = kstDayRange(date);
|
|
50
|
+
const lines = await ghLines(['pr', 'list', '--repo', target.repo, '--state', 'merged', '--limit', '300',
|
|
51
|
+
'--search', `merged:${start.slice(0, 19)}Z..${end.slice(0, 19)}Z`, '--json', 'number,title,body,mergedAt', '--jq', '.[] | tojson']);
|
|
52
|
+
return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
|
|
53
|
+
},
|
|
54
|
+
checklist() {
|
|
55
|
+
const dev = devVersion();
|
|
56
|
+
const next = listChecklist(nextPatch(dev)).items;
|
|
57
|
+
const current = listChecklist(dev).items;
|
|
58
|
+
const all: ChecklistItem[] = [...current, ...next];
|
|
59
|
+
return { current: next.length ? next : current, all };
|
|
60
|
+
},
|
|
61
|
+
openDecisionRaisers() {
|
|
62
|
+
return new DecisionLedger().list({ status: 'open' }).map((entry) => entry.raisedBy.agent);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// GET /v1/ops/seats?date=YYYY-MM-DD — one row per seat: latest channel line, PRs landed that day, red items, open decisions.
|
|
2
|
+
// A source that could not be read is null, never 0 («못 읽음» ≠ «없음»). Contract: 내부 문서 `DESIGN-ops3-seat-board-and-public-demo-2026-10-02` §1.
|
|
3
|
+
import { debug, redactSecretText } from '../../debug/log.js';
|
|
4
|
+
import type { ChecklistItem } from '../../release-loop/checklist.js';
|
|
5
|
+
import { canonicalSeatId } from '../../msg/msg-store.js';
|
|
6
|
+
|
|
7
|
+
export const SEATS = [
|
|
8
|
+
{ seat: 'OP', role: 'COO' }, { seat: 'TC', role: 'CTO' }, { seat: 'MK', role: 'CMO' }, { seat: 'UX', role: 'CXO' },
|
|
9
|
+
] as const;
|
|
10
|
+
export const SEATS_CACHE_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
export interface ChannelComment { body: string; createdAt: string }
|
|
13
|
+
export interface MergedPr { number: number; title: string; body: string; mergedAt: string }
|
|
14
|
+
export interface SeatsSources {
|
|
15
|
+
/** Coordination-channel comments created on `date` (KST) or later; null when unreadable. */
|
|
16
|
+
channel(date: string): Promise<ChannelComment[] | null>;
|
|
17
|
+
/** PRs merged on `date` (KST); null when unreadable. */
|
|
18
|
+
merged(date: string): Promise<MergedPr[] | null>;
|
|
19
|
+
/** Checklist items of the version being worked on (counts, red) and of every version known (id → owner). */
|
|
20
|
+
checklist(): { current: ChecklistItem[]; all: ChecklistItem[] } | null;
|
|
21
|
+
/** `raisedBy.agent` of every open decision. */
|
|
22
|
+
openDecisionRaisers(): string[] | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SeatRow {
|
|
26
|
+
seat: string; role: string;
|
|
27
|
+
now: { text: string; at: string } | null;
|
|
28
|
+
landed: Array<{ pr: number; title: string; at: string; checklistId: string | null }> | null;
|
|
29
|
+
blocked: Array<{ id: string; title: string; status: 'red' }> | null;
|
|
30
|
+
pendingDecisions: number | null;
|
|
31
|
+
checklist: { green: number; yellow: number; red: number; done: number } | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function seatOf(value: string | undefined): string | null {
|
|
35
|
+
if (!value) return null;
|
|
36
|
+
try { return canonicalSeatId(value); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clip = (text: string, max: number) => redactSecretText(text).slice(0, max);
|
|
40
|
+
|
|
41
|
+
/** First checklist id that appears as a whole word in the title, else in the body. */
|
|
42
|
+
function checklistIdOf(pr: MergedPr, ids: string[]): string | null {
|
|
43
|
+
for (const text of [pr.title, pr.body]) {
|
|
44
|
+
const found = ids.map((id) => ({ id, at: text.search(new RegExp(`(?<![A-Za-z0-9])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9])`)) }))
|
|
45
|
+
.filter((hit) => hit.at >= 0).sort((a, b) => a.at - b.at || b.id.length - a.id.length);
|
|
46
|
+
if (found.length) return found[0]!.id;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function buildSeatsBoard(date: string, sources: SeatsSources): Promise<{ date: string; seats: SeatRow[] }> {
|
|
52
|
+
const [channel, merged] = await Promise.all([
|
|
53
|
+
sources.channel(date).catch(() => null), sources.merged(date).catch(() => null),
|
|
54
|
+
]);
|
|
55
|
+
let checklist: ReturnType<SeatsSources['checklist']> = null;
|
|
56
|
+
try { checklist = sources.checklist(); } catch { checklist = null; }
|
|
57
|
+
let raisers: string[] | null = null;
|
|
58
|
+
try { raisers = sources.openDecisionRaisers(); } catch { raisers = null; }
|
|
59
|
+
|
|
60
|
+
const owners = new Map<string, string>();
|
|
61
|
+
for (const item of checklist?.all ?? []) { const seat = seatOf(item.owner); if (seat) owners.set(item.id, seat); }
|
|
62
|
+
const ids = [...owners.keys()];
|
|
63
|
+
const landedBySeat = new Map<string, NonNullable<SeatRow['landed']>>();
|
|
64
|
+
for (const pr of merged ?? []) {
|
|
65
|
+
const id = checklistIdOf(pr, ids);
|
|
66
|
+
const seat = id ? owners.get(id) : undefined;
|
|
67
|
+
if (!seat) continue; // ⛔ not by commit author — every seat lands through the same account.
|
|
68
|
+
landedBySeat.set(seat, [...(landedBySeat.get(seat) ?? []), { pr: pr.number, title: clip(pr.title, 80), at: pr.mergedAt, checklistId: id }]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const seats = SEATS.map(({ seat, role }): SeatRow => {
|
|
72
|
+
const latest = channel?.filter((c) => c.body.trimStart().startsWith(`**[${seat}]**`)).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
|
73
|
+
const mine = checklist?.current.filter((item) => seatOf(item.owner) === seat);
|
|
74
|
+
return {
|
|
75
|
+
seat, role,
|
|
76
|
+
now: latest ? { text: clip(latest.body.trimStart().split('\n')[0]!, 120), at: latest.createdAt } : null,
|
|
77
|
+
landed: merged === null || checklist === null ? null : (landedBySeat.get(seat) ?? []).sort((a, b) => a.at.localeCompare(b.at)),
|
|
78
|
+
blocked: mine ? mine.filter((item) => item.status === 'red').map((item) => ({ id: item.id, title: clip(item.title, 80), status: 'red' as const })) : null,
|
|
79
|
+
pendingDecisions: raisers === null ? null : raisers.filter((agent) => seatOf(agent) === seat).length,
|
|
80
|
+
checklist: mine ? {
|
|
81
|
+
green: mine.filter((i) => i.status === 'green').length, yellow: mine.filter((i) => i.status === 'yellow').length,
|
|
82
|
+
red: mine.filter((i) => i.status === 'red').length, done: mine.filter((i) => i.status === 'done').length,
|
|
83
|
+
} : null,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
debug.log('ops.seats', 'built', {
|
|
87
|
+
date, channel: channel === null ? 'unreadable' : channel.length, merged: merged === null ? 'unreadable' : merged.length,
|
|
88
|
+
checklist: checklist !== null, decisions: raisers !== null,
|
|
89
|
+
});
|
|
90
|
+
return { date, seats };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** KST calendar day → [start, end) in UTC ISO. */
|
|
94
|
+
export function kstDayRange(date: string): { start: string; end: string } {
|
|
95
|
+
const start = new Date(`${date}T00:00:00+09:00`);
|
|
96
|
+
return { start: start.toISOString(), end: new Date(start.getTime() + 86_400_000).toISOString() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function todayKst(now = new Date()): string {
|
|
100
|
+
return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul' }).format(now);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One snapshot per date for 60 s; concurrent requests share one build. */
|
|
104
|
+
export function createSeatsCache(sources: SeatsSources, now: () => number = Date.now, ttlMs = SEATS_CACHE_MS) {
|
|
105
|
+
const slots = new Map<string, { at: number; value: Promise<{ date: string; seats: SeatRow[] }> }>();
|
|
106
|
+
return (date: string) => {
|
|
107
|
+
const slot = slots.get(date);
|
|
108
|
+
if (slot && now() - slot.at < ttlMs) return slot.value;
|
|
109
|
+
const value = buildSeatsBoard(date, sources);
|
|
110
|
+
slots.set(date, { at: now(), value });
|
|
111
|
+
value.catch(() => slots.delete(date));
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -44,6 +44,13 @@ function json(body: unknown, status: number): Response {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** The daemon's own sendOutbound path (OB8) — what POST /v1/outbound does, without the HTTP self-call. */
|
|
48
|
+
export async function routeOutboundInProcess(text: string, kind: string): Promise<boolean> {
|
|
49
|
+
const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
|
|
50
|
+
if (result.delivered) recordOutboundEvent(text, kind);
|
|
51
|
+
return result.delivered;
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
/** POST /v1/outbound — fan one message out to the routed channels. */
|
|
48
55
|
export async function handleOutboundReport(req: Request, metaApi: MetaApiOpts): Promise<Response> {
|
|
49
56
|
if (!checkAuth(req, metaApi)) return json({ error: 'unauthorized' }, 401);
|
|
@@ -36,6 +36,12 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
|
|
|
36
36
|
path: '/v1/push/vapid-public-key',
|
|
37
37
|
why: 'PWA fetches the VAPID public key before a bearer is pasted',
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
method: 'GET',
|
|
41
|
+
match: 'exact',
|
|
42
|
+
path: '/v1/me',
|
|
43
|
+
why: 'operator signal only (draw the ops area or not); false unless operator.enabled, no other data',
|
|
44
|
+
},
|
|
39
45
|
{
|
|
40
46
|
method: 'GET',
|
|
41
47
|
match: 'exact',
|
|
@@ -56,6 +62,13 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
|
|
|
56
62
|
selfVerified: true,
|
|
57
63
|
why: 'Pod GitHub App credential relay verifies its own run-scoped gh-credential token before minting a repository-scoped installation token',
|
|
58
64
|
},
|
|
65
|
+
{
|
|
66
|
+
method: 'POST',
|
|
67
|
+
match: 'exact',
|
|
68
|
+
path: '/v1/setup/claim',
|
|
69
|
+
selfVerified: true,
|
|
70
|
+
why: 'one-use setup link is verified by the claim handler without an owner bearer',
|
|
71
|
+
},
|
|
59
72
|
{
|
|
60
73
|
method: 'GET',
|
|
61
74
|
match: 'prefix',
|
|
@@ -76,7 +89,7 @@ export function isPublicRoute(
|
|
|
76
89
|
): boolean {
|
|
77
90
|
for (const route of PUBLIC_ROUTES) {
|
|
78
91
|
if (route.method !== method) continue;
|
|
79
|
-
if (route.path.startsWith('/v1/setup/') && !ctx.setupMode) continue;
|
|
92
|
+
if (route.path.startsWith('/v1/setup/') && route.path !== '/v1/setup/claim' && !ctx.setupMode) continue;
|
|
80
93
|
if (route.match === 'exact') {
|
|
81
94
|
if (pathname === route.path) return true;
|
|
82
95
|
} else if (pathname === route.path || pathname.startsWith(route.path)) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The terminals list must answer at once and must never share the event loop with a ledger scan: the running-runs query
|
|
2
|
+
// reads every run ledger synchronously (~5k files · 0.9–1.8 s on 10-02), which froze typing in the web terminal while the
|
|
3
|
+
// PWA polled the list. This keeps the last result as a snapshot and refreshes it in a separate bun process, on demand.
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { debug } from '../../debug/log.js';
|
|
6
|
+
import type { RunningRunsResult } from '../../self-implement/running-runs.js';
|
|
7
|
+
|
|
8
|
+
export const RUNNING_RUNS_REFRESH_MS = 15_000;
|
|
9
|
+
/** A refresh that has not answered by then is abandoned; the next request starts another. */
|
|
10
|
+
export const RUNNING_RUNS_REFRESH_TIMEOUT_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
export interface RunningRunsSnapshot {
|
|
13
|
+
/** Last result, or null before the first refresh finished (subjects then keep unknown assessments). */
|
|
14
|
+
readonly result: RunningRunsResult | null;
|
|
15
|
+
/** Milliseconds since `result` was computed; null when there is none. */
|
|
16
|
+
readonly ageMs: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Runs the query somewhere other than this event loop and resolves with its JSON output. */
|
|
20
|
+
export type RunningRunsRunner = (includeTest: boolean) => Promise<RunningRunsResult>;
|
|
21
|
+
|
|
22
|
+
export const RUNNING_RUNS_CHILD_SCRIPT = join(import.meta.dir, 'running-runs-child.ts');
|
|
23
|
+
|
|
24
|
+
/** Default runner: `bun running-runs-child.ts [--include-test]` — the child does the ledger I/O, we only parse its stdout. */
|
|
25
|
+
export const childProcessRunner: RunningRunsRunner = async (includeTest) => {
|
|
26
|
+
const proc = Bun.spawn([process.execPath, RUNNING_RUNS_CHILD_SCRIPT, ...(includeTest ? ['--include-test'] : [])], {
|
|
27
|
+
stdout: 'pipe', stderr: 'ignore', stdin: 'ignore', env: process.env,
|
|
28
|
+
});
|
|
29
|
+
const timer = setTimeout(() => proc.kill(), RUNNING_RUNS_REFRESH_TIMEOUT_MS);
|
|
30
|
+
try {
|
|
31
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
32
|
+
if (code !== 0) throw new Error(`running-runs child exit ${code}`);
|
|
33
|
+
return JSON.parse(out) as RunningRunsResult;
|
|
34
|
+
} finally { clearTimeout(timer); }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface RunningRunsBackground {
|
|
38
|
+
/** Never blocks: returns the last snapshot and starts a refresh when it is stale and none is in flight. */
|
|
39
|
+
snapshot(options: { includeTest?: boolean }): RunningRunsSnapshot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createRunningRunsBackground(runner: RunningRunsRunner = childProcessRunner, now: () => number = Date.now, refreshMs = RUNNING_RUNS_REFRESH_MS): RunningRunsBackground {
|
|
43
|
+
const slots = new Map<boolean, { at: number; result: RunningRunsResult }>();
|
|
44
|
+
const flights = new Set<boolean>();
|
|
45
|
+
const refresh = (includeTest: boolean): void => {
|
|
46
|
+
if (flights.has(includeTest)) return;
|
|
47
|
+
flights.add(includeTest);
|
|
48
|
+
const started = now();
|
|
49
|
+
void runner(includeTest)
|
|
50
|
+
.then((result) => {
|
|
51
|
+
slots.set(includeTest, { at: now(), result });
|
|
52
|
+
debug.log('nexus.terminals', 'running-runs-refreshed', { includeTest, ms: now() - started });
|
|
53
|
+
})
|
|
54
|
+
.catch((error: unknown) => {
|
|
55
|
+
debug.log('nexus.terminals', 'running-runs-refresh-failed', { includeTest, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
56
|
+
})
|
|
57
|
+
.finally(() => { flights.delete(includeTest); });
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
snapshot(options) {
|
|
61
|
+
const includeTest = options.includeTest === true;
|
|
62
|
+
const slot = slots.get(includeTest);
|
|
63
|
+
const t = now();
|
|
64
|
+
if (!slot || t - slot.at >= refreshMs) refresh(includeTest);
|
|
65
|
+
return slot ? { result: slot.result, ageMs: t - slot.at } : { result: null, ageMs: null };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Child of running-runs-background.ts: runs the running-runs query (all the ledger I/O) and prints the result as JSON.
|
|
3
|
+
import { queryRunningRuns } from '../../self-implement/running-runs.js';
|
|
4
|
+
|
|
5
|
+
const result = queryRunningRuns({ includeTest: process.argv.includes('--include-test'), caller: 'nexus.terminals' });
|
|
6
|
+
process.stdout.write(JSON.stringify(result));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { claimSetupLinkToken } from '../../auth/setup-link-tokens.js';
|
|
2
|
+
import { jsonResponse } from './json-response.js';
|
|
3
|
+
|
|
4
|
+
/** A setup link is its own credential; this handler never reads the owner bearer. */
|
|
5
|
+
export async function handleSetupClaim(req: Request): Promise<Response> {
|
|
6
|
+
let body: unknown;
|
|
7
|
+
try {
|
|
8
|
+
body = await req.json();
|
|
9
|
+
} catch {
|
|
10
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
11
|
+
}
|
|
12
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)
|
|
13
|
+
|| typeof (body as Record<string, unknown>).t !== 'string'
|
|
14
|
+
|| !(body as { t: string }).t.trim()) {
|
|
15
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = claimSetupLinkToken((body as { t: string }).t);
|
|
19
|
+
if (!result.ok) return jsonResponse({ error: result.reason }, 401);
|
|
20
|
+
return jsonResponse({ scope: 'setup', bearer: result.bearer, expiresAt: result.expiresAt });
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SC1 — 앱 안 «공유용 캡처»(beta) 받는 쪽. PWA 가 화면을 가린 뒤 PNG 로 올린다(티저·현장·보고용).
|
|
2
|
+
// POST /v1/captures body = image/png(≤ 8 MiB) → { id, bytes, droppedChunks }
|
|
3
|
+
// GET /v1/captures → { items: [{ id, bytes, at }] } (최신 먼저 · 50개)
|
|
4
|
+
// GET /v1/captures/<id> → image/png
|
|
5
|
+
// ⛔ 두 번째 방어: 받은 PNG 에서 «그림 아닌» 덩어리(tEXt·iTXt·zTXt·eXIf·iCCP·tIME 등 메타)를 전부 걷고 저장한다 —
|
|
6
|
+
// 화면 쪽 가림이 놓친 것이 파일 메타로 새지 않게. 남기는 것은 그림을 그리는 데 필요한 덩어리뿐이다.
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { debug } from '../../debug/log.js';
|
|
11
|
+
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
12
|
+
import { jsonResponse } from './json-response.js';
|
|
13
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
14
|
+
|
|
15
|
+
export const SHARE_CAPTURES_PATH = '/v1/captures';
|
|
16
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
18
|
+
/** Chunks needed to draw the picture. Everything else (text, EXIF, ICC profile, time…) is dropped. */
|
|
19
|
+
const KEEP = new Set(['IHDR', 'PLTE', 'IDAT', 'IEND', 'tRNS', 'gAMA', 'cHRM', 'sRGB', 'pHYs']);
|
|
20
|
+
const ID = /^share-\d{8}-\d{6}-[0-9a-f]{6}$/;
|
|
21
|
+
|
|
22
|
+
export interface ShareCapturesDeps { configDir?: string; now?: () => Date }
|
|
23
|
+
|
|
24
|
+
/** Rebuild a PNG with only drawing chunks. Returns null when it is not a well-formed PNG. */
|
|
25
|
+
export function stripPngMetadata(input: Uint8Array): { png: Buffer; dropped: string[] } | null {
|
|
26
|
+
const buf = Buffer.from(input);
|
|
27
|
+
if (buf.length < 8 + 12 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
|
|
28
|
+
const parts: Buffer[] = [PNG_SIGNATURE];
|
|
29
|
+
const dropped: string[] = [];
|
|
30
|
+
let offset = 8;
|
|
31
|
+
let sawHeader = false;
|
|
32
|
+
let sawEnd = false;
|
|
33
|
+
while (offset + 12 <= buf.length) {
|
|
34
|
+
const length = buf.readUInt32BE(offset);
|
|
35
|
+
const type = buf.subarray(offset + 4, offset + 8).toString('latin1');
|
|
36
|
+
const end = offset + 12 + length;
|
|
37
|
+
if (end > buf.length || !/^[A-Za-z]{4}$/.test(type)) return null;
|
|
38
|
+
if (!sawHeader && type !== 'IHDR') return null;
|
|
39
|
+
sawHeader = true;
|
|
40
|
+
if (KEEP.has(type)) parts.push(buf.subarray(offset, end)); else dropped.push(type);
|
|
41
|
+
offset = end;
|
|
42
|
+
if (type === 'IEND') { sawEnd = true; break; }
|
|
43
|
+
}
|
|
44
|
+
if (!sawEnd) return null;
|
|
45
|
+
return { png: Buffer.concat(parts), dropped };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function capturesDir(deps: ShareCapturesDeps): string {
|
|
49
|
+
return join(deps.configDir ?? getElanousConfigDir(), 'captures');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function newId(now: Date): string {
|
|
53
|
+
const p = (n: number) => String(n).padStart(2, '0');
|
|
54
|
+
return `share-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}-${randomBytes(3).toString('hex')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function handleShareCaptures(req: Request, metaApi: MetaApiOpts, deps: ShareCapturesDeps = {}): Promise<Response> {
|
|
58
|
+
if (!checkAuth(req, metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
59
|
+
const pathname = new URL(req.url).pathname;
|
|
60
|
+
const dir = capturesDir(deps);
|
|
61
|
+
|
|
62
|
+
if (pathname === SHARE_CAPTURES_PATH && req.method === 'POST') {
|
|
63
|
+
const declared = Number(req.headers.get('content-length') ?? 0);
|
|
64
|
+
if (declared > MAX_BYTES) return jsonResponse({ error: 'too-large', maxBytes: MAX_BYTES }, 413);
|
|
65
|
+
const body = new Uint8Array(await req.arrayBuffer());
|
|
66
|
+
if (body.byteLength > MAX_BYTES) return jsonResponse({ error: 'too-large', maxBytes: MAX_BYTES }, 413);
|
|
67
|
+
const cleaned = stripPngMetadata(body);
|
|
68
|
+
if (!cleaned) return jsonResponse({ error: 'not-png' }, 400);
|
|
69
|
+
const id = newId((deps.now ?? (() => new Date()))());
|
|
70
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
71
|
+
writeFileSync(join(dir, `${id}.png`), cleaned.png, { mode: 0o600, flag: 'wx' });
|
|
72
|
+
debug.log('share-capture', 'stored', { id, bytes: cleaned.png.length, dropped: cleaned.dropped });
|
|
73
|
+
return jsonResponse({ id, bytes: cleaned.png.length, droppedChunks: cleaned.dropped }, 201);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pathname === SHARE_CAPTURES_PATH && req.method === 'GET') {
|
|
77
|
+
const items = existsSync(dir)
|
|
78
|
+
? readdirSync(dir).filter((f) => f.endsWith('.png') && ID.test(f.slice(0, -4))).sort().reverse().slice(0, 50)
|
|
79
|
+
.map((f) => { const s = statSync(join(dir, f)); return { id: f.slice(0, -4), bytes: s.size, at: s.mtime.toISOString() }; })
|
|
80
|
+
: [];
|
|
81
|
+
return jsonResponse({ items });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const id = pathname.slice(SHARE_CAPTURES_PATH.length + 1);
|
|
85
|
+
if (req.method === 'GET' && ID.test(id)) {
|
|
86
|
+
const file = join(dir, `${id}.png`);
|
|
87
|
+
if (!existsSync(file)) return jsonResponse({ error: 'not-found' }, 404);
|
|
88
|
+
return new Response(readFileSync(file), { headers: { 'content-type': 'image/png', 'cache-control': 'no-store' } });
|
|
89
|
+
}
|
|
90
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SK2 (PWA) — 색인이 못 읽은 스킬을 PWA 에도 «안내 ⊕ 확인 뒤 수리»로.
|
|
2
|
+
// GET /v1/skills/problems → { items: [{ id, name, code, fixable, hint? }] } — 경로 대신 해시 id 만 내보낸다
|
|
3
|
+
// POST /v1/skills/problems/repair → body { id } · 고칠 수 있는 것만 고친다(남의 도구 파일 — PWA 가 먼저 확인을 받는다)
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { debug } from '../../debug/log.js';
|
|
6
|
+
import { getSkillIndex, skillIndexProblems, type SkillIndexProblem } from '../../skills/index.js';
|
|
7
|
+
import { applySkillRepair, planSkillRepair } from '../../skills/repair.js';
|
|
8
|
+
import { jsonResponse } from './json-response.js';
|
|
9
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
10
|
+
|
|
11
|
+
export const SKILL_PROBLEMS_PATH = '/v1/skills/problems';
|
|
12
|
+
|
|
13
|
+
export interface SkillProblemsDeps {
|
|
14
|
+
problems?: () => SkillIndexProblem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function problemId(p: SkillIndexProblem): string {
|
|
18
|
+
return createHash('sha256').update(`${p.dir}\u0000${p.name}`).digest('hex').slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function current(deps: SkillProblemsDeps): SkillIndexProblem[] {
|
|
22
|
+
if (deps.problems) return deps.problems();
|
|
23
|
+
getSkillIndex();
|
|
24
|
+
return skillIndexProblems();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleSkillProblems(req: Request, metaApi: MetaApiOpts, deps: SkillProblemsDeps = {}): Promise<Response> {
|
|
28
|
+
if (!checkAuth(req, metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
29
|
+
const pathname = new URL(req.url).pathname;
|
|
30
|
+
if (pathname === SKILL_PROBLEMS_PATH) {
|
|
31
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
32
|
+
const items = current(deps).map((p) => {
|
|
33
|
+
const plan = planSkillRepair(p);
|
|
34
|
+
return {
|
|
35
|
+
id: problemId(p), name: p.name, code: p.code,
|
|
36
|
+
fixable: plan.kind === 'fixable' || plan.kind === 'fixable-permission',
|
|
37
|
+
...(plan.kind === 'permission' || plan.kind === 'manual' ? { hint: plan.hint.replace(p.dir, '…') } : {}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return jsonResponse({ items });
|
|
41
|
+
}
|
|
42
|
+
if (pathname === `${SKILL_PROBLEMS_PATH}/repair`) {
|
|
43
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
44
|
+
let body: unknown;
|
|
45
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
46
|
+
const id = body && typeof body === 'object' && typeof (body as { id?: unknown }).id === 'string' ? (body as { id: string }).id : '';
|
|
47
|
+
const problem = current(deps).find((p) => problemId(p) === id);
|
|
48
|
+
if (!problem) return jsonResponse({ error: 'not-found' }, 404);
|
|
49
|
+
const plan = planSkillRepair(problem);
|
|
50
|
+
if (plan.kind !== 'fixable' && plan.kind !== 'fixable-permission') return jsonResponse({ error: 'not-fixable', hint: plan.hint.replace(problem.dir, '…') }, 409);
|
|
51
|
+
const result = applySkillRepair(plan);
|
|
52
|
+
debug.log('skills.repair', 'pwa-request', { name: problem.name, ok: result.ok });
|
|
53
|
+
return result.ok ? jsonResponse({ ok: true, name: problem.name }) : jsonResponse({ ok: false, reason: result.reason }, 409);
|
|
54
|
+
}
|
|
55
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
56
|
+
}
|
|
@@ -34,6 +34,7 @@ import { classifyPtyOwnerRunUsage, joinPtyLineage, resolveRunTermination, type P
|
|
|
34
34
|
import { loadRunLedger, resolveFederatedRunLedgerDirectories, runLedgerDir } from '../../self-implement/run-ledger.js';
|
|
35
35
|
import { requestRemotePtyControl, type PtyControlAction, type PtyControlPayload, type PtyControlRequestOptions, type PtyControlResult } from '../../pty-shell/pty-control-ipc.js';
|
|
36
36
|
import { encodeSgrMouse, type PtyMouseInput } from '../../pty-shell/pty-mouse.js';
|
|
37
|
+
import { createRunningRunsBackground, type RunningRunsBackground } from './running-runs-background.js';
|
|
37
38
|
import { queryRunningRuns, type RunningRunAssessment, type RunningRunPresence, type RunningRunStatus, type RunningRunsResult } from '../../self-implement/running-runs.js';
|
|
38
39
|
// ⭐P4 §4-1 — 표시 폭(wide-char/CJK/emoji 인지) SSOT. PNG 캔버스 cols 파생용.
|
|
39
40
|
import { cellWidth } from '../../ui/printer.js';
|
|
@@ -494,6 +495,12 @@ function runningRunsCacheFor(
|
|
|
494
495
|
return binding.cache;
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
let liveBackground: RunningRunsBackground | null = null;
|
|
499
|
+
function liveRunningRunsBackground(): RunningRunsBackground {
|
|
500
|
+
liveBackground ??= createRunningRunsBackground();
|
|
501
|
+
return liveBackground;
|
|
502
|
+
}
|
|
503
|
+
|
|
497
504
|
const liveTerminalsListDeps: TerminalsListDeps = {
|
|
498
505
|
ptyManifestTargets,
|
|
499
506
|
listPtyManifestRows,
|
|
@@ -1054,17 +1061,26 @@ export function handleTerminalsList(req: Request, opts: MetaApiOpts, deps: Termi
|
|
|
1054
1061
|
const resolveRunTerminationForRow = (runId: string): RunTermination => deps.runTerminated
|
|
1055
1062
|
? deps.runTerminated(runId, { includeTest })
|
|
1056
1063
|
: 'ledger-indeterminate';
|
|
1057
|
-
const runningRunsCache = runningRunsCacheFor(getRunningRuns, deps.now ?? Date.now);
|
|
1058
1064
|
let runningRuns: RunningRunsResult | null = null;
|
|
1059
1065
|
let runningRunsAgeMs = 0;
|
|
1060
1066
|
let runningRunsQueryFailed = false;
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1067
|
+
if (getRunningRuns === queryRunningRuns) {
|
|
1068
|
+
// Live daemon: never scan ledgers on this event loop (it froze web-terminal typing) — use the last snapshot a
|
|
1069
|
+
// separate process refreshed. Before the first one lands, subjects keep unknown assessments and the age is null.
|
|
1070
|
+
const snap = liveRunningRunsBackground().snapshot({ includeTest });
|
|
1071
|
+
runningRuns = snap.result;
|
|
1072
|
+
if (snap.ageMs === null) runningRunsQueryFailed = true;
|
|
1073
|
+
else runningRunsAgeMs = snap.ageMs;
|
|
1074
|
+
} else {
|
|
1075
|
+
const runningRunsCache = runningRunsCacheFor(getRunningRuns, deps.now ?? Date.now);
|
|
1076
|
+
try {
|
|
1077
|
+
const cached = runningRunsCache({ includeTest });
|
|
1078
|
+
runningRuns = cached.result;
|
|
1079
|
+
runningRunsAgeMs = cached.ageMs;
|
|
1080
|
+
} catch {
|
|
1081
|
+
// Fail-soft and do not cache: subjects keep unknown assessments.
|
|
1082
|
+
runningRunsQueryFailed = true;
|
|
1083
|
+
}
|
|
1068
1084
|
}
|
|
1069
1085
|
const ptyLessSubAgentCollection = collectPtyLessSubAgents(
|
|
1070
1086
|
getLogStore,
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// without touching caller code. The legacy module re-exports this
|
|
6
6
|
// backend's accessor functions to preserve backwards compatibility.
|
|
7
7
|
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
9
10
|
import { dirname } from 'node:path';
|
|
11
|
+
import { acquireLockSync } from '../../../storage/file-lock.js';
|
|
10
12
|
import { SECRETS_VERSION, type SecretsFile } from '../types.js';
|
|
11
13
|
import { secretsPath } from '../paths.js';
|
|
12
14
|
import type { SecretBackend, SecretBackendAvailability, SyncReadableBackend } from './types.js';
|
|
@@ -21,20 +23,47 @@ function readSecretsFile(): SecretsFile {
|
|
|
21
23
|
try {
|
|
22
24
|
const raw = readFileSync(path, 'utf-8');
|
|
23
25
|
const parsed = JSON.parse(raw) as Partial<SecretsFile>;
|
|
24
|
-
if (parsed
|
|
25
|
-
|
|
26
|
-
version
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
} catch {
|
|
30
|
-
|
|
26
|
+
if (parsed?.version !== SECRETS_VERSION || !parsed.secrets || typeof parsed.secrets !== 'object'
|
|
27
|
+
|| Array.isArray(parsed.secrets) || Object.values(parsed.secrets).some(value => typeof value !== 'string')) {
|
|
28
|
+
throw new Error('invalid secrets file version or contents');
|
|
29
|
+
}
|
|
30
|
+
return { version: SECRETS_VERSION, secrets: parsed.secrets };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`cannot read secrets file at ${path}`, { cause: error });
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function writeSecretsFile(s: SecretsFile): void {
|
|
35
37
|
const path = secretsPath();
|
|
36
38
|
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
-
|
|
39
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
40
|
+
const backupTmp = `${path}.${randomUUID()}.bak.tmp`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(tmp, JSON.stringify(s, null, 2), { mode: 0o600, flag: 'wx' });
|
|
43
|
+
if (existsSync(path)) {
|
|
44
|
+
copyFileSync(path, backupTmp);
|
|
45
|
+
chmodSync(backupTmp, 0o600);
|
|
46
|
+
renameSync(backupTmp, `${path}.bak`);
|
|
47
|
+
}
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
} finally {
|
|
50
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
51
|
+
if (existsSync(backupTmp)) unlinkSync(backupTmp);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function modifySecretsFile(change: (s: SecretsFile) => boolean): boolean {
|
|
56
|
+
const path = secretsPath();
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
const release = acquireLockSync(`${path}.lock`);
|
|
59
|
+
try {
|
|
60
|
+
const s = readSecretsFile();
|
|
61
|
+
if (!change(s)) return false;
|
|
62
|
+
writeSecretsFile(s);
|
|
63
|
+
return true;
|
|
64
|
+
} finally {
|
|
65
|
+
release();
|
|
66
|
+
}
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
const syncReadable: SyncReadableBackend = {
|
|
@@ -46,16 +75,14 @@ export const fileBackend: SecretBackend = {
|
|
|
46
75
|
id: 'file',
|
|
47
76
|
async get(id) { return syncReadable.getSync(id); },
|
|
48
77
|
async set(id, value) {
|
|
49
|
-
|
|
50
|
-
s.secrets[id] = value;
|
|
51
|
-
writeSecretsFile(s);
|
|
78
|
+
modifySecretsFile(s => { s.secrets[id] = value; return true; });
|
|
52
79
|
},
|
|
53
80
|
async delete(id) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
return modifySecretsFile(s => {
|
|
82
|
+
if (!Object.hasOwn(s.secrets, id)) return false;
|
|
83
|
+
delete s.secrets[id];
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
59
86
|
},
|
|
60
87
|
async list() { return syncReadable.listSync(); },
|
|
61
88
|
async isAvailable(): Promise<SecretBackendAvailability> {
|
|
@@ -74,5 +101,16 @@ export function readSecretsFileRaw(): SecretsFile {
|
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
export function writeSecretsFileRaw(s: SecretsFile): void {
|
|
77
|
-
|
|
104
|
+
if (s.version !== SECRETS_VERSION || !s.secrets || typeof s.secrets !== 'object'
|
|
105
|
+
|| Array.isArray(s.secrets) || Object.values(s.secrets).some(value => typeof value !== 'string')) {
|
|
106
|
+
throw new Error('invalid secrets file version or contents');
|
|
107
|
+
}
|
|
108
|
+
const path = secretsPath();
|
|
109
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
110
|
+
const release = acquireLockSync(`${path}.lock`);
|
|
111
|
+
try {
|
|
112
|
+
writeSecretsFile(s);
|
|
113
|
+
} finally {
|
|
114
|
+
release();
|
|
115
|
+
}
|
|
78
116
|
}
|