elanous 0.2.10 → 0.2.12
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/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-5c05b0da4bc1392c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-e297866051595d81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -0
- package/apps/pwa/out/inside/index.txt +31 -0
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -0
- package/apps/pwa/out/outputs/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -24
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -24
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/drafts/draft-cleanup.yaml +19 -0
- package/graphs/drafts/recipes.yaml +23 -0
- package/graphs/intake/intake-daily.yaml +2 -1
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/ops/secret-exposure.yaml +24 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/video/explainer-line.yaml +2 -2
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/plugins/elanous-hwp/README.md +25 -0
- package/plugins/elanous-hwp/cli.ts +22 -0
- package/plugins/elanous-hwp/converter.ts +237 -0
- package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
- package/plugins/elanous-hwp/plugin.json +16 -0
- package/plugins/elanous-hwp/plugin.ts +40 -0
- package/scripts/ci-public-export-leak-gate.ts +75 -14
- package/scripts/ci-public-leak-gate.ts +44 -9
- package/scripts/context-hooks/claude-stop.sh +4 -0
- package/scripts/context-hooks/emit.ts +24 -0
- package/scripts/context-hooks/post-tool-use.sh +4 -0
- package/scripts/coord-channel-watch.sh +12 -0
- package/scripts/coord-requests-bridge.ts +143 -0
- package/scripts/field-feed/lib.ts +4 -0
- package/scripts/fleet/node-setup.sh +20 -3
- package/scripts/homebrew-formula-bump.ts +69 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/marketing/measure-site-numbers.ts +169 -0
- package/scripts/measure-gate-shards.ts +309 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/mission-request-judge.ts +31 -1
- package/scripts/morning-report.ts +2 -2
- package/scripts/opportunity-scan.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +21 -4
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/cut-branch.ts +84 -0
- package/scripts/release-loop/gate-node.ts +22 -15
- package/scripts/release-loop/mac-smoke-node.ts +229 -0
- package/scripts/release-loop/node-verdict.ts +3 -3
- package/scripts/release-loop/ops-upgrade-node.ts +47 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/tui-node-checks.ts +42 -0
- package/scripts/release-loop/tui-sim-node.ts +32 -8
- package/scripts/release-loop/unattended-release.ts +5 -3
- package/scripts/release-loop/version-node.ts +74 -13
- package/scripts/signal-digest-cycle.ts +2 -7
- package/scripts/signal-digest-photo.ts +11 -0
- package/scripts/tech1-public-measure.ts +94 -0
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-diet/node.ts +1 -1
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/tui-regress.ts +2 -2
- package/scripts/ux/demo-rehearsal.ts +247 -0
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +80 -0
- package/skills/diagram-master/SKILL.md +366 -0
- package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
- package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
- package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
- package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
- package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
- package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
- package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
- package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
- package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
- package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
- package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
- package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
- package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
- package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
- package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
- package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
- package/skills/diagram-master/references/examples/README.md +69 -0
- package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
- package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
- package/skills/diagram-master/references/pyproject.toml +16 -0
- package/skills/diagram-master/references/quality-checklist.md +33 -0
- package/skills/diagram-master/references/routing-guide.md +164 -0
- package/skills/diagram-master/references/uv.lock +1356 -0
- package/skills/google-workspace/SKILL.md +33 -0
- package/skills/google-workspace/scripts/gws-safe.sh +177 -0
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/lecture-note-digitizer/SKILL.md +926 -0
- package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
- package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
- package/skills/lecture-note-digitizer/references/html-template.md +167 -0
- package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
- package/skills/omni-crawl/SKILL.md +21 -11
- package/skills/omni-crawl/scripts/main.ts +6 -6
- package/skills/omni-crawl/src/router.ts +3 -3
- package/skills/omni-digest/SKILL.md +17 -9
- package/skills/omni-digest/digest.md +2 -2
- package/skills/omni-digest/src/fetch.ts +30 -3
- package/skills/pitch-deck/SKILL.md +2 -2
- package/skills/pitch-deck/template.html +2 -2
- package/skills/youtube-master/SKILL.md +163 -0
- package/skills/youtube-master/package-lock.json +558 -0
- package/skills/youtube-master/package.json +13 -0
- package/skills/youtube-master/references/study-note-template.md +124 -0
- package/skills/youtube-master/scripts/main.ts +435 -0
- package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
- package/skills/youtube-master/src/audio.ts +63 -0
- package/skills/youtube-master/src/domain-staging.ts +50 -0
- package/skills/youtube-master/src/env.ts +36 -0
- package/skills/youtube-master/src/obsidian.ts +155 -0
- package/skills/youtube-master/src/router.ts +110 -0
- package/skills/youtube-master/src/stt.ts +199 -0
- package/skills/youtube-master/src/study-note.ts +175 -0
- package/skills/youtube-master/src/summarize.ts +358 -0
- package/skills/youtube-master/src/transcript.ts +76 -0
- package/skills/youtube-master/src/types.ts +91 -0
- package/skills/youtube-master/src/util.ts +93 -0
- package/skills/youtube-master/src/youtube.ts +70 -0
- package/skills/youtube-master/tsconfig.json +13 -0
- package/src/acp/server.ts +5 -2
- package/src/agent-mission/driver.ts +215 -23
- package/src/agent-mission/pty-decision.ts +71 -0
- package/src/agent-mission/review-loop.ts +30 -0
- package/src/autopilot/build/llm-conflict-merge.ts +16 -3
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/tool-registry.ts +2 -0
- package/src/boot/daemon-prompt-turn.ts +5 -0
- package/src/boot/daemon-runtime.ts +9 -3
- package/src/boot/daemon-tools/web-search.ts +3 -0
- package/src/card-followup/core.ts +109 -0
- package/src/chat/index.ts +2 -0
- package/src/cli/drive-input-jail.ts +101 -0
- package/src/cli/hooks-cli.ts +17 -3
- package/src/cli/make-cli.ts +43 -0
- package/src/cli/persona-cli.ts +98 -9
- package/src/cli/plugin-cli.ts +31 -22
- package/src/cli/pod-cli.ts +36 -9
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +51 -15
- package/src/cli/pty-drive-cli.ts +41 -7
- package/src/cli/pty-drive-verdict.ts +40 -0
- package/src/cli/release-cli.ts +63 -10
- package/src/cli/setup-cli.ts +43 -1
- package/src/cli/skills-cli.ts +17 -1
- package/src/context-bus/claude-hooks-install.ts +10 -0
- package/src/context-bus/context-now-surfaces.ts +65 -0
- package/src/context-bus/context-now.ts +104 -0
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/external-events.ts +52 -0
- package/src/context-bus/long-term-memory.ts +108 -0
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +17 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +31 -1
- package/src/decisions/decision-cards.ts +10 -3
- package/src/decisions/decision-ledger.ts +13 -5
- package/src/discord/sprint21-runtime.ts +5 -1
- package/src/discord-self-message.ts +32 -0
- package/src/discord-slash-wire.ts +5 -1
- package/src/discord.ts +17 -1
- package/src/domains/core-tools.ts +3 -2
- package/src/domains/outbound-alert.ts +105 -32
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/domains/telegram-kind-route.ts +51 -13
- package/src/exec-requests/default-outputs.ts +25 -0
- package/src/exec-requests/planner.ts +10 -2
- package/src/exec-requests/runner.ts +20 -6
- package/src/exec-requests/store.ts +3 -0
- package/src/execution-loop/launch-gate.ts +109 -0
- package/src/field/field-reel.ts +5 -1
- package/src/field-note/index.ts +201 -0
- package/src/field-note/telegram.ts +142 -0
- package/src/field-note/transcribe.ts +248 -0
- package/src/graph-kinds/registry.ts +3 -3
- package/src/graph-runner/graph-cli.ts +61 -1
- package/src/graph-runner/graph-grow-llm.ts +47 -0
- package/src/graph-runner/graph-grow.ts +50 -0
- package/src/graph-runner/runner.ts +329 -33
- package/src/grok/agent-search.ts +8 -2
- package/src/guardian/secret-exposure-scan.ts +101 -0
- package/src/harness/harness-cli-command.ts +375 -13
- package/src/harness/harness-pod-dispatch.ts +43 -23
- package/src/harness/harness-seams.ts +38 -4
- package/src/harness/harness-stop.ts +23 -4
- package/src/harness/launch-registry.ts +91 -0
- package/src/harness/process-garbage.ts +5 -3
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/dispatch.ts +82 -0
- package/src/hooks/expose.ts +254 -0
- package/src/hooks/github-shadow.ts +108 -0
- package/src/hooks/providers.ts +71 -5
- package/src/hooks/queue.ts +1 -1
- package/src/hooks/receiver.ts +94 -16
- package/src/index.ts +76 -14
- package/src/intake-plane/categorize.ts +18 -46
- package/src/intake-plane/discord-seat-work.ts +38 -2
- package/src/intake-plane/front-route-classifier.ts +33 -39
- package/src/intake-plane/persona-answer.ts +30 -0
- package/src/intake-plane/runtime-callables.ts +8 -1
- package/src/intake-plane/telegram-seat-work.ts +36 -2
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/intake-plane/wish-folder.ts +67 -0
- package/src/intent-gate/gate.ts +30 -10
- package/src/live/detail-switch.ts +5 -0
- package/src/llm/difficulty.ts +57 -0
- package/src/llm/judge-layer.ts +101 -0
- package/src/llm/mission-router.ts +33 -19
- package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
- package/src/loops/contract/examples/steward.loop.yaml +50 -0
- package/src/loops/contract/loop-agent.schema.json +138 -0
- package/src/loops/contract/validate.ts +64 -0
- package/src/loops/registry.ts +20 -6
- package/src/market/signed-index.ts +4 -0
- package/src/maturity/bot-command-maturity.ts +45 -0
- package/src/maturity/feature-maturity.ts +27 -4
- package/src/model-tier/index.ts +2 -2
- package/src/model-tier/tier-resolver.ts +20 -20
- package/src/model-tier/types.ts +6 -6
- package/src/mss/logging/log-store.ts +15 -0
- package/src/nexus/api/config-model-tier.ts +5 -4
- package/src/nexus/api/fabric-plans.ts +53 -0
- package/src/nexus/api/harness-api.ts +5 -1
- package/src/nexus/api/health.ts +2 -0
- package/src/nexus/api/http-server.ts +67 -8
- package/src/nexus/api/inside-events.ts +226 -0
- package/src/nexus/api/meta-api.ts +21 -8
- package/src/nexus/api/missions.ts +6 -1
- package/src/nexus/api/ops-seats.ts +33 -0
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/outputs-route.ts +44 -0
- package/src/nexus/api/personas.ts +95 -53
- package/src/nexus/api/seat-requests.ts +44 -2
- package/src/nexus/api/sessions-store.ts +17 -38
- package/src/nexus/api/sub-seat-charters.ts +34 -0
- package/src/nexus/api/tabs-mutations.ts +3 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/trace.ts +18 -6
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
- package/src/nexus/api/workflows.ts +132 -6
- package/src/nexus/index.ts +2 -0
- package/src/nexus/kinds/webterm.ts +2 -1
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/nexus/webterm/pty.ts +6 -2
- package/src/onboarding/cli-auth-status.ts +80 -0
- package/src/onboarding/voice-ai.ts +3 -3
- package/src/org/seat-budget.ts +48 -0
- package/src/org/stall-escalation.ts +63 -0
- package/src/outputs/ledger.ts +120 -0
- package/src/persona/persona-todo.ts +79 -0
- package/src/persona/presets.ts +123 -2
- package/src/persona/team-set.ts +155 -0
- package/src/plugins/install/plugin-install.ts +14 -0
- package/src/plugins/maker/market-bundle.ts +110 -0
- package/src/plugins/maker/plugin-maker.ts +254 -11
- package/src/plugins/maker/wizard-generate.ts +103 -0
- package/src/pod-lease/admission.ts +150 -0
- package/src/pod-lease/dependency-state.ts +85 -0
- package/src/pod-lease/host-lease.ts +127 -0
- package/src/project/project-suggestion.ts +16 -0
- package/src/project/session-context.ts +24 -0
- package/src/release-loop/checklist.ts +17 -5
- package/src/release-loop/feature-store.ts +29 -13
- package/src/release-loop/landed-but-yellow.ts +48 -0
- package/src/release-loop/next-md-merge.ts +58 -0
- package/src/release-loop/release-note.ts +59 -4
- package/src/seat-dispatch/seat-ask.ts +130 -0
- package/src/seat-dispatch/seat-questions.ts +100 -0
- package/src/seat-loop/seat-loop.ts +709 -29
- package/src/seat-loop/seat-report.ts +37 -2
- package/src/self-dev/dev-cli.ts +4 -1
- package/src/self-dev/dev-pipeline.ts +37 -1
- package/src/self-dev/draft-sweep.ts +79 -40
- package/src/self-dev/draft-triage-rules.ts +62 -7
- package/src/self-dev/fabric-plan-core.ts +297 -0
- package/src/self-dev/orchestrate.ts +36 -4
- package/src/self-dev/release-path-guard.ts +62 -0
- package/src/self-dev/shard-aggregate.ts +230 -0
- package/src/self-dev/shard-placement.ts +60 -0
- package/src/self-dev/shard-plan.ts +48 -0
- package/src/self-dev/shard-pod-run.ts +77 -0
- package/src/self-dev/shard-run.ts +105 -0
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-baseline.ts +1 -1
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-cli.ts +40 -1
- package/src/self-implement/gate-scope.ts +44 -13
- package/src/self-implement/graph-variant.ts +171 -0
- package/src/self-implement/harness-policy-gates.ts +1 -1
- package/src/self-implement/headless-elanous-driver.ts +27 -3
- package/src/self-implement/host-regate.ts +22 -1
- package/src/self-implement/importer-test-index.ts +2 -1
- package/src/self-implement/l8-merge-queue.ts +130 -0
- package/src/self-implement/orchestrator.ts +257 -9
- package/src/self-implement/route-consumer-test-index.ts +122 -0
- package/src/self-implement/seams.ts +72 -11
- package/src/self-implement/test-seams.ts +2 -0
- package/src/self-implement/uncommitted-work.ts +59 -0
- package/src/session/index.ts +36 -13
- package/src/skills/gift-install.ts +215 -0
- package/src/skills/tools/web-search.ts +4 -1
- package/src/steward/launch.ts +42 -26
- package/src/steward/steward-cards.ts +17 -9
- package/src/steward/triage.ts +46 -14
- package/src/task-cards/card-cli.ts +25 -1
- package/src/task-orchestrator/mission.ts +5 -0
- package/src/task-orchestrator/store.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +43 -17
- package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
- package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +222 -40
- package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
- package/src/task-orchestrator/types.ts +2 -0
- package/src/telegram-card-followup.ts +16 -96
- package/src/telegram-commands.ts +31 -1
- package/src/telegram-project-command.ts +145 -0
- package/src/telegram-report.ts +21 -17
- package/src/telegram.ts +61 -8
- package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
- package/src/user-config.ts +110 -24
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/grok.ts +7 -1
- package/src/web-search/index.ts +28 -1
- package/src/web-search/provider.ts +4 -0
- package/src/web-search/x-intent.ts +5 -0
- package/src/workflow-runtime/executor.ts +277 -47
- package/src/workflow-runtime/index.ts +1 -0
- package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
- package/src/workflow-runtime/plugin-kind-node.ts +1 -1
- package/src/workflow-runtime/schema.ts +79 -4
- package/src/workflow-runtime/types.ts +35 -4
- package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.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-cb6debea56cce4fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.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/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.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/7633-1b3709b0854c9412.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/7951-8b03dbb10b24cd06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.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-d2253d7e58ef1890.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.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-2976df21ad74ab48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.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-64a29fc80b11ab55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.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-202b4e81f33895ea.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/chunks/webpack-e5bea36f183b25e6.js +0 -1
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
- /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,7 @@ import { defaultGraphsDir } from '../self-implement/graph-templates.js';
|
|
|
7
7
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
8
8
|
import type { ExecAttachment } from './store.js';
|
|
9
9
|
import { debug } from '../debug/log.js';
|
|
10
|
+
import { OUTPUT_KINDS, pickDefaultOutput, type OutputKind } from './default-outputs.js';
|
|
10
11
|
|
|
11
12
|
export interface InstalledGraph {
|
|
12
13
|
id: string;
|
|
@@ -21,6 +22,8 @@ export interface ExecPlanItem {
|
|
|
21
22
|
title: string;
|
|
22
23
|
graphId: string;
|
|
23
24
|
inputs: Record<string, unknown>;
|
|
25
|
+
/** Planner fills this for every returned row; older injected plan fixtures may omit it. */
|
|
26
|
+
output?: OutputKind;
|
|
24
27
|
reason?: string;
|
|
25
28
|
/** A5b — indexes of earlier items whose results this item needs (it starts after they finish). */
|
|
26
29
|
after?: number[];
|
|
@@ -147,7 +150,7 @@ export function latestFieldFolder(configRoot = getElanousConfigDir()): string |
|
|
|
147
150
|
|
|
148
151
|
export function execPlanPrompt(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): string {
|
|
149
152
|
const supplied = attachments.map(({ name, path }) => `첨부: ${name} (${/\.(?:png|jpe?g|webp|gif|heic)$/i.test(name) ? '이미지' : '파일'}) — ${path}`).join('\n');
|
|
150
|
-
return `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄과 아래 알려진 값에서 얻을 수 있는 값만 넣는다(모르는 값은 넣지 않는다). image/photo/file 입력에는 알맞은 첨부 경로를 쓸 수 있다. folder 입력에는 첨부가 없거나 첨부 사진이 그 현장 폴더 안에 있을 때만 오늘 현장 폴더를 쓸 수 있다. 다른 현장 사진이나 출처를 모르는 업로드 사진이 있으면 폴더를 추측하지 마라. 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 알 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 단순히 묻는 말에는 없는 그래프를 지어내지 말고 graphId=""로 표시한다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n${fieldFolder ? `알려진 값: 오늘 현장 폴더 = ${fieldFolder}\n` : ''}${supplied ? `${supplied}\n` : ''}한 줄: ${JSON.stringify(text)}`;
|
|
153
|
+
return `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 각 행에는 선택 칸 "output":"report|slides|research|post|video|answer"를 쓸 수 있다: ${Object.entries(OUTPUT_KINDS).map(([kind, entry]) => `${entry.guidance} = ${kind}${entry.fileName ? ` (${entry.fileName})` : ''}`).join(', ')}; 그 외 일은 report로 둔다. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄과 아래 알려진 값에서 얻을 수 있는 값만 넣는다(모르는 값은 넣지 않는다). image/photo/file 입력에는 알맞은 첨부 경로를 쓸 수 있다. folder 입력에는 첨부가 없거나 첨부 사진이 그 현장 폴더 안에 있을 때만 오늘 현장 폴더를 쓸 수 있다. 다른 현장 사진이나 출처를 모르는 업로드 사진이 있으면 폴더를 추측하지 마라. 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 알 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 단순히 묻는 말에는 없는 그래프를 지어내지 말고 graphId=""로 표시한다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n${fieldFolder ? `알려진 값: 오늘 현장 폴더 = ${fieldFolder}\n` : ''}${supplied ? `${supplied}\n` : ''}한 줄: ${JSON.stringify(text)}`;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): Promise<unknown> {
|
|
@@ -183,6 +186,11 @@ export async function planExecRequest(text: string, deps: {
|
|
|
183
186
|
? [...new Set(row.after.filter((n): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) < index))]
|
|
184
187
|
: [];
|
|
185
188
|
const graph = known.get(row.graphId);
|
|
189
|
+
const modelOutput = typeof row.output === 'string' && Object.hasOwn(OUTPUT_KINDS, row.output)
|
|
190
|
+
? row.output as OutputKind : undefined;
|
|
191
|
+
const picked = modelOutput ?? pickDefaultOutput(row.title).kind;
|
|
192
|
+
const output = picked === 'video' && !graph ? 'report' : picked;
|
|
193
|
+
debug.log('exec.plan', 'default-output', { kind: output, source: modelOutput === output ? 'model' : 'rule' });
|
|
186
194
|
const inputs = { ...row.inputs as Record<string, unknown> };
|
|
187
195
|
let folderReason: string | undefined;
|
|
188
196
|
if (graph) {
|
|
@@ -206,7 +214,7 @@ export async function planExecRequest(text: string, deps: {
|
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
return {
|
|
209
|
-
seat: row.seat, title: row.title.trim(), graphId: row.graphId, inputs,
|
|
217
|
+
seat: row.seat, title: row.title.trim(), graphId: row.graphId, inputs, output,
|
|
210
218
|
...(after.length ? { after } : {}),
|
|
211
219
|
...(!graph ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : folderReason ? { reason: folderReason } : {}),
|
|
212
220
|
};
|
|
@@ -9,6 +9,12 @@ import { installedGraphs, planExecRequest, type ExecPlanItem, type InstalledGrap
|
|
|
9
9
|
import { ExecRequestStore, type ExecAttachment, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
|
|
10
10
|
import { answerAsSeat } from '../intake-plane/seat-answer.js';
|
|
11
11
|
import { redactSecrets } from '../task-cards/card-store.js';
|
|
12
|
+
import { recordOutput } from '../outputs/ledger.js';
|
|
13
|
+
import type { OutputKind } from './default-outputs.js';
|
|
14
|
+
|
|
15
|
+
function recordResult(result: ExecResult, id: string, root: string, kind: OutputKind | 'file'): void {
|
|
16
|
+
recordOutput({ source: 'exec', sourceId: id, seat: result.seat, title: result.title, url: result.url, kind }, root);
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
export interface ExecRunnerDeps {
|
|
14
20
|
store?: ExecRequestStore;
|
|
@@ -137,14 +143,19 @@ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root:
|
|
|
137
143
|
for (const entry of entries) {
|
|
138
144
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
139
145
|
const result = resultOf(entry as Record<string, unknown>, seat, item.id, root);
|
|
140
|
-
if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url))
|
|
146
|
+
if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url)) {
|
|
147
|
+
item.results.push(result);
|
|
148
|
+
recordResult(result, item.id, root, seat.output ?? 'file');
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
if (seat.status === 'done' && !item.results.some(existing => existing.seat === seat.seat)) {
|
|
144
153
|
const file = textResultFile(seat, state, root);
|
|
145
154
|
const result = file ? resultOf({ file, title: seat.title }, seat, item.id, root) : null;
|
|
146
|
-
if (result)
|
|
147
|
-
|
|
155
|
+
if (result) {
|
|
156
|
+
item.results.push(result);
|
|
157
|
+
recordResult(result, item.id, root, seat.output ?? 'file');
|
|
158
|
+
} else {
|
|
148
159
|
const keys = [...new Set((state.nodes ?? []).flatMap(node => Object.keys(lastJsonObject(node.output) ?? {})))];
|
|
149
160
|
debug.log('exec.result', 'none', { seat: seat.seat, graphId: seat.graphId, keys });
|
|
150
161
|
}
|
|
@@ -288,7 +299,7 @@ export class ExecRequestRunner {
|
|
|
288
299
|
reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` });
|
|
289
300
|
continue;
|
|
290
301
|
}
|
|
291
|
-
const seat: ExecSeat = { seat: plan.seat, title: plan.title, status: 'done', graphId: 'seat-answer', runId: randomUUID() };
|
|
302
|
+
const seat: ExecSeat = { seat: plan.seat, title: plan.title, status: 'done', graphId: 'seat-answer', runId: randomUUID(), ...(plan.output ? { output: plan.output } : {}) };
|
|
292
303
|
const dir = join(this.root, 'graph-runs', seat.graphId);
|
|
293
304
|
const statePath = join(dir, `${seat.runId}.json`);
|
|
294
305
|
const artifactDir = join(dir, `${seat.runId}.json.artifacts`);
|
|
@@ -299,7 +310,10 @@ export class ExecRequestRunner {
|
|
|
299
310
|
writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
|
|
300
311
|
item.seats.push(seat);
|
|
301
312
|
const result = resultOf({ file, title: answered.title }, seat, id, this.root);
|
|
302
|
-
if (result)
|
|
313
|
+
if (result) {
|
|
314
|
+
item.results.push(result);
|
|
315
|
+
recordResult(result, item.id, this.root, seat.output ?? 'file');
|
|
316
|
+
}
|
|
303
317
|
}
|
|
304
318
|
item.status = 'running';
|
|
305
319
|
aggregate(item);
|
|
@@ -307,7 +321,7 @@ export class ExecRequestRunner {
|
|
|
307
321
|
return;
|
|
308
322
|
}
|
|
309
323
|
item.seats = plans.map(plan => ({ seat: plan.seat, title: plan.title, status: plan.reason || !byId.has(plan.graphId) ? 'failed' : 'waiting', graphId: plan.graphId,
|
|
310
|
-
runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs, ...(plan.after?.length ? { after: plan.after } : {}),
|
|
324
|
+
runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs, ...(plan.output ? { output: plan.output } : {}), ...(plan.after?.length ? { after: plan.after } : {}),
|
|
311
325
|
...(plan.reason || !byId.has(plan.graphId) ? { reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}) }));
|
|
312
326
|
item.status = 'running';
|
|
313
327
|
debug.log('exec-requests', 'planned', { id, seats: item.seats.map(seat => ({ seat: seat.seat, graphId: seat.graphId, status: seat.status })) });
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
5
|
+
import type { OutputKind } from './default-outputs.js';
|
|
5
6
|
|
|
6
7
|
export type SeatStatus = 'waiting' | 'running' | 'done' | 'failed';
|
|
7
8
|
export interface ExecSeat {
|
|
@@ -10,6 +11,8 @@ export interface ExecSeat {
|
|
|
10
11
|
status: SeatStatus;
|
|
11
12
|
graphId: string;
|
|
12
13
|
runId: string;
|
|
14
|
+
/** Requested output category, distinct from the result file MIME kind. */
|
|
15
|
+
output?: OutputKind;
|
|
13
16
|
inputs?: Record<string, unknown>;
|
|
14
17
|
reason?: string;
|
|
15
18
|
/** A5b — indexes of earlier seats this seat waits for. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { extractVerbatimOriginalAsk } from '../self-implement/goal-author.js';
|
|
5
|
+
import { loadRunLedger } from '../self-implement/run-ledger.js';
|
|
6
|
+
import { queryRunningRuns, type QueriedRunningRunsResult } from '../self-implement/running-runs.js';
|
|
7
|
+
import type { BudgetDecision } from '../self-implement/budget-gate.js';
|
|
8
|
+
|
|
9
|
+
export type ActiveRunsForGoal = readonly string[] | 'unknown';
|
|
10
|
+
|
|
11
|
+
/** Match an un-authored CLI request to the verbatim ask recorded in an authored run. */
|
|
12
|
+
export function launchRequestId(text: string): string {
|
|
13
|
+
return `request-${createHash('sha256').update(text).digest('hex').slice(0, 32)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ActiveRunsForGoalDeps {
|
|
17
|
+
queryRuns?: () => QueriedRunningRunsResult;
|
|
18
|
+
loadLedger?: typeof loadRunLedger;
|
|
19
|
+
readGoalDocument?: (path: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Only a live process with an exact goal ID or verbatim authored ask is a duplicate. Incomplete observation is not absence. */
|
|
23
|
+
export function activeRunsForGoal(goalId: string, deps: ActiveRunsForGoalDeps = {}): ActiveRunsForGoal {
|
|
24
|
+
try {
|
|
25
|
+
const observation = (deps.queryRuns ?? (() => queryRunningRuns({ includeTest: true, caller: 'execution-loop.launch-gate', noCache: true })))();
|
|
26
|
+
const loadLedger = deps.loadLedger ?? loadRunLedger;
|
|
27
|
+
const readGoalDocument = deps.readGoalDocument ?? ((path: string) => readFileSync(path, 'utf8'));
|
|
28
|
+
let unknown = observation.completeness !== 'complete' || observation.pty.unreadable.length > 0;
|
|
29
|
+
const unknownReasons = new Set<string>();
|
|
30
|
+
if (observation.completeness !== 'complete') unknownReasons.add(`ledger observation ${observation.completeness}`);
|
|
31
|
+
for (const root of observation.pty.unreadable) unknownReasons.add(`unreadable PTY: ${root}`);
|
|
32
|
+
const active = new Set<string>();
|
|
33
|
+
for (const run of observation.entries) {
|
|
34
|
+
if (run.status === 'ended-unclosed') continue;
|
|
35
|
+
if (run.ledgerDirectories.length === 0) { unknown = true; unknownReasons.add(`${run.runId}: ledger directory absent`); continue; }
|
|
36
|
+
let identified = false;
|
|
37
|
+
for (const directory of run.ledgerDirectories) {
|
|
38
|
+
try {
|
|
39
|
+
const ledger = loadLedger(run.runId, directory);
|
|
40
|
+
if (ledger === null) { unknown = true; unknownReasons.add(`${run.runId}: ledger unavailable`); continue; }
|
|
41
|
+
const ids = new Set(ledger.flatMap((entry) => entry.goalId ? [entry.goalId] : []));
|
|
42
|
+
if (ids.size !== 1) { unknown = true; unknownReasons.add(`${run.runId}: expected one goalId, found ${ids.size}`); continue; }
|
|
43
|
+
identified = true;
|
|
44
|
+
let matches = ids.has(goalId);
|
|
45
|
+
if (!matches && goalId.startsWith('request-') && run.status === 'running' && run.ptyRefs.length > 0) {
|
|
46
|
+
const document = ledger.find((entry) => entry.event === 'start')?.data.goalFile;
|
|
47
|
+
if (typeof document === 'string') {
|
|
48
|
+
try {
|
|
49
|
+
const ask = extractVerbatimOriginalAsk(readGoalDocument(document));
|
|
50
|
+
matches = ask?.range !== undefined && launchRequestId(ask.ask) === goalId;
|
|
51
|
+
} catch { unknown = true; unknownReasons.add(`${run.runId}: goal document unreadable`); }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (matches) {
|
|
55
|
+
if (run.status === 'running' && run.ptyRefs.length > 0) active.add(run.runId);
|
|
56
|
+
else { unknown = true; unknownReasons.add(`${run.runId}: matching ledger without live PTY`); }
|
|
57
|
+
}
|
|
58
|
+
} catch { unknown = true; unknownReasons.add(`${run.runId}: ledger read failed`); }
|
|
59
|
+
}
|
|
60
|
+
if (!identified) unknown = true;
|
|
61
|
+
}
|
|
62
|
+
// Positive evidence wins even if a different process or store is unreadable.
|
|
63
|
+
if (active.size > 0) return [...active].sort();
|
|
64
|
+
if (unknown) {
|
|
65
|
+
try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [...unknownReasons] }); } catch { /* observation is fail-soft */ }
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
} catch (error) {
|
|
70
|
+
try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [error instanceof Error ? error.message : String(error)] }); } catch { /* observation is fail-soft */ }
|
|
71
|
+
return 'unknown';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PreLaunchGateInput {
|
|
76
|
+
goalId: string;
|
|
77
|
+
budget?: BudgetDecision | 'unknown';
|
|
78
|
+
forceLaunch?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PreLaunchGateDeps extends ActiveRunsForGoalDeps {
|
|
82
|
+
activeRuns?: (goalId: string) => ActiveRunsForGoal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PreLaunchGateDecision {
|
|
86
|
+
action: 'proceed' | 'blocked-duplicate' | 'blocked-budget' | 'wait-reset';
|
|
87
|
+
sameGoalActiveRuns: ActiveRunsForGoal;
|
|
88
|
+
budget: BudgetDecision | 'unknown';
|
|
89
|
+
reason: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Force bypasses only a duplicate observation, never an exhausted budget. */
|
|
93
|
+
export function preLaunchGate(input: PreLaunchGateInput, deps: PreLaunchGateDeps = {}): PreLaunchGateDecision {
|
|
94
|
+
const decision = decidePreLaunch(input, deps);
|
|
95
|
+
debug.log('execution-loop.launch-gate', 'decision', { goalId: input.goalId, action: decision.action, sameGoalActiveRuns: decision.sameGoalActiveRuns, budget: decision.budget === 'unknown' ? 'unknown' : decision.budget.action, forceLaunch: input.forceLaunch === true, reason: decision.reason });
|
|
96
|
+
return decision;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function decidePreLaunch(input: PreLaunchGateInput, deps: PreLaunchGateDeps): PreLaunchGateDecision {
|
|
100
|
+
const sameGoalActiveRuns = (deps.activeRuns ?? ((goalId) => activeRunsForGoal(goalId, deps)))(input.goalId);
|
|
101
|
+
const budget = input.budget ?? 'unknown';
|
|
102
|
+
if (budget !== 'unknown' && (budget.action === 'stop' || budget.action === 'wait-reset')) {
|
|
103
|
+
return { action: budget.action === 'stop' ? 'blocked-budget' : 'wait-reset', sameGoalActiveRuns, budget, reason: budget.reasons.join(' · ') || `budget: ${budget.action}` };
|
|
104
|
+
}
|
|
105
|
+
if (sameGoalActiveRuns !== 'unknown' && sameGoalActiveRuns.length > 0 && !input.forceLaunch) {
|
|
106
|
+
return { action: 'blocked-duplicate', sameGoalActiveRuns, budget, reason: `same-goal active runs: ${sameGoalActiveRuns.join(', ')}` };
|
|
107
|
+
}
|
|
108
|
+
return { action: 'proceed', sameGoalActiveRuns, budget, reason: sameGoalActiveRuns === 'unknown' ? 'active runs unknown' : input.forceLaunch && sameGoalActiveRuns.length > 0 ? 'duplicate overridden by force-launch' : 'no confirmed duplicate' };
|
|
109
|
+
}
|
package/src/field/field-reel.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { debug } from '../debug/log.js';
|
|
|
7
7
|
import { mergeCapturedPath } from '../shell-env-bootstrap.js';
|
|
8
8
|
import { listFieldMedia } from './field-media.js';
|
|
9
9
|
import { startFieldFeed, type FieldFeedStart } from './field-feed-auto.js';
|
|
10
|
+
import { recordOutput } from '../outputs/ledger.js';
|
|
10
11
|
|
|
11
12
|
export interface FieldReelResult { ok: boolean; file?: string; seconds: number; error?: string; feed?: FieldFeedStart }
|
|
12
13
|
export type FieldReelRunner = (dir: string, opts: { title: string; sub: string; mode?: 'standard' | 'instant' }) => Promise<FieldReelResult>;
|
|
@@ -137,6 +138,7 @@ async function render(dir: string, job: Job): Promise<void> {
|
|
|
137
138
|
// 현재 렌더의 수신자 스냅샷. 렌더 중 업로드의 수신자는 다음 렌더로 넘긴다.
|
|
138
139
|
const callbacks = job.callbacks;
|
|
139
140
|
const renderOpts = job.opts;
|
|
141
|
+
let reelTitle = renderOpts.title || event;
|
|
140
142
|
job.callbacks = new Map();
|
|
141
143
|
writeStatus(dir, { state: 'rendering', items, startedAt });
|
|
142
144
|
mkdirSync(join(dir, 'reel'), { recursive: true }); // 실행기 계약 — 산출 폴더는 있다(reel.sh 는 스스로 다시 만든다)
|
|
@@ -148,8 +150,9 @@ async function render(dir: string, job: Job): Promise<void> {
|
|
|
148
150
|
let titleLines: string[] = [];
|
|
149
151
|
try { titleLines = readFileSync(join(dir, 'title.txt'), 'utf8').split(/\r?\n/); }
|
|
150
152
|
catch (err) { if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; }
|
|
153
|
+
reelTitle = titleLines[0]?.trim() || renderOpts.title || event;
|
|
151
154
|
result = await (renderOpts.runner ?? defaultFieldReelRunner)(dir, {
|
|
152
|
-
title:
|
|
155
|
+
title: reelTitle,
|
|
153
156
|
sub: titleLines[1]?.trim() || renderOpts.sub || sub,
|
|
154
157
|
...(renderOpts.mode ? { mode: renderOpts.mode } : {}),
|
|
155
158
|
});
|
|
@@ -163,6 +166,7 @@ async function render(dir: string, job: Job): Promise<void> {
|
|
|
163
166
|
writeStatus(dir, { state: result.ok ? 'done' : 'failed', items, startedAt, finishedAt,
|
|
164
167
|
seconds: result.seconds, ...(result.ok ? { file: result.file } : { error: result.error ?? 'render failed' }) });
|
|
165
168
|
debug.log('field.reel', result.ok ? 'done' : 'failed', { event, items, seconds: result.seconds });
|
|
169
|
+
if (result.ok && result.file) recordOutput({ source: 'field-reel', sourceId: event, kind: 'video', title: reelTitle, path: result.file });
|
|
166
170
|
if (result.ok) {
|
|
167
171
|
// EV10b: 엔진이 timeline.json 에 남긴 비전 호출 수·성공·«현장 N» 강등·걸린 ms — codex 잔량과 겹치므로 관측한다(OP 10-01).
|
|
168
172
|
let cardsCache: 'hit' | 'miss' | 'n/a' = 'n/a';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export interface FieldPhoto {
|
|
2
|
+
file: string;
|
|
3
|
+
/** EXIF `YYYY:MM:DD HH:mm:ss`, ISO local time, or ISO time with an offset. */
|
|
4
|
+
exifTakenAt?: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TranscriptEntry {
|
|
8
|
+
speaker: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FieldNoteOptions {
|
|
14
|
+
/** Minutes east of UTC for offset-free EXIF and transcript timestamps; KST by default. */
|
|
15
|
+
timeZoneOffsetMinutes?: number;
|
|
16
|
+
/** Minutes added to every dated photo after interpreting its EXIF timestamp. */
|
|
17
|
+
photoCorrectionMinutes?: number;
|
|
18
|
+
/** Maximum gap between consecutive photos in the same scene; 15 minutes by default. */
|
|
19
|
+
sceneGapMinutes?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TimedPhoto extends FieldPhoto {
|
|
23
|
+
takenAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FieldScene {
|
|
27
|
+
start: string;
|
|
28
|
+
end: string;
|
|
29
|
+
photos: TimedPhoto[];
|
|
30
|
+
transcript: TranscriptEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FieldTimeline {
|
|
34
|
+
scenes: FieldScene[];
|
|
35
|
+
undatedPhotos: FieldPhoto[];
|
|
36
|
+
/** Chronological entries when there are no dated photos to anchor a scene. */
|
|
37
|
+
transcriptWithoutPhotos: TranscriptEntry[];
|
|
38
|
+
timeZoneOffsetMinutes: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FieldNoteLlm {
|
|
42
|
+
/** Only used for a raw transcript string; JSON entries bypass transcription. */
|
|
43
|
+
transcribe?: (source: string) => Promise<TranscriptEntry[]>;
|
|
44
|
+
summarize?: (entries: readonly TranscriptEntry[]) => Promise<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MINUTE = 60_000;
|
|
48
|
+
|
|
49
|
+
function validOffset(minutes: number): boolean {
|
|
50
|
+
return Number.isInteger(minutes) && minutes >= -14 * 60 && minutes <= 14 * 60;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseTime(value: string, offsetMinutes: number): number | null {
|
|
54
|
+
const input = value.trim();
|
|
55
|
+
const local = /^(\d{4})[:-](\d{2})[:-](\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(input);
|
|
56
|
+
const zoned = local ? null : /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:Z|[+-]\d{2}:\d{2})$/.exec(input);
|
|
57
|
+
const match = local ?? zoned;
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const [, year, month, day, hour, minute, second, fraction] = match;
|
|
60
|
+
if (+year! < 100) return null;
|
|
61
|
+
const milliseconds = fraction ? Number(fraction.padEnd(3, '0')) : 0;
|
|
62
|
+
const utc = Date.UTC(+year!, +month! - 1, +day!, +hour!, +minute!, +second!, milliseconds);
|
|
63
|
+
const date = new Date(utc);
|
|
64
|
+
if (date.getUTCFullYear() !== +year! || date.getUTCMonth() + 1 !== +month! ||
|
|
65
|
+
date.getUTCDate() !== +day! || date.getUTCHours() !== +hour! ||
|
|
66
|
+
date.getUTCMinutes() !== +minute! || date.getUTCSeconds() !== +second!) return null;
|
|
67
|
+
if (local) return utc - offsetMinutes * MINUTE;
|
|
68
|
+
// Offset-bearing ISO timestamps are absolute instants, not local wall-clock readings.
|
|
69
|
+
const parsed = Date.parse(input);
|
|
70
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function iso(ms: number): string {
|
|
74
|
+
return new Date(ms).toISOString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Pure alignment: neither mutates its inputs nor consults the machine clock/timezone. */
|
|
78
|
+
export function alignFieldNote(
|
|
79
|
+
photos: readonly FieldPhoto[],
|
|
80
|
+
transcript: readonly TranscriptEntry[],
|
|
81
|
+
options: FieldNoteOptions = {},
|
|
82
|
+
): FieldTimeline {
|
|
83
|
+
const offset = options.timeZoneOffsetMinutes ?? 540;
|
|
84
|
+
const correction = options.photoCorrectionMinutes ?? 0;
|
|
85
|
+
const gap = options.sceneGapMinutes ?? 15;
|
|
86
|
+
if (!validOffset(offset) || !Number.isFinite(correction) || !Number.isFinite(gap) || gap < 0) {
|
|
87
|
+
throw new RangeError('Invalid field-note timezone, photo correction, or scene gap');
|
|
88
|
+
}
|
|
89
|
+
const dated: Array<{ photo: TimedPhoto; time: number; order: number }> = [];
|
|
90
|
+
const undatedPhotos: FieldPhoto[] = [];
|
|
91
|
+
photos.forEach((photo, order) => {
|
|
92
|
+
const parsed = photo.exifTakenAt ? parseTime(photo.exifTakenAt, offset) : null;
|
|
93
|
+
if (parsed === null) undatedPhotos.push({ ...photo });
|
|
94
|
+
else dated.push({ photo: { ...photo, takenAt: iso(parsed + correction * MINUTE) }, time: parsed + correction * MINUTE, order });
|
|
95
|
+
});
|
|
96
|
+
dated.sort((a, b) => a.time - b.time || a.order - b.order);
|
|
97
|
+
|
|
98
|
+
const groups: typeof dated[] = [];
|
|
99
|
+
for (const item of dated) {
|
|
100
|
+
const last = groups.at(-1);
|
|
101
|
+
if (!last || item.time - last[last.length - 1]!.time > gap * MINUTE) groups.push([item]);
|
|
102
|
+
else last.push(item);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entries = transcript.map((entry, order) => {
|
|
106
|
+
const time = parseTime(entry.timestamp, offset);
|
|
107
|
+
if (time === null) throw new RangeError(`Invalid transcript timestamp at index ${order}: ${entry.timestamp}`);
|
|
108
|
+
return { entry: { ...entry }, time, order };
|
|
109
|
+
}).sort((a, b) => a.time - b.time || a.order - b.order);
|
|
110
|
+
|
|
111
|
+
// An utterance belongs to the scene whose photo span [first, last] is nearest in time (inside the span = distance 0);
|
|
112
|
+
// a tie goes to the earlier scene. So a 10:55 line next to an 11:00 photo joins the 11:00 scene, not 09:00.
|
|
113
|
+
const spans = groups.map((group) => ({ start: group[0]!.time, end: group[group.length - 1]!.time }));
|
|
114
|
+
const sceneEntries: Array<Array<(typeof entries)[number]>> = groups.map(() => []);
|
|
115
|
+
for (const item of entries) {
|
|
116
|
+
if (groups.length === 0) break;
|
|
117
|
+
let best = 0;
|
|
118
|
+
let bestDistance = Infinity;
|
|
119
|
+
spans.forEach((span, index) => {
|
|
120
|
+
const distance = item.time < span.start ? span.start - item.time : item.time > span.end ? item.time - span.end : 0;
|
|
121
|
+
if (distance < bestDistance) { best = index; bestDistance = distance; }
|
|
122
|
+
});
|
|
123
|
+
sceneEntries[best]!.push(item);
|
|
124
|
+
}
|
|
125
|
+
const scenes = groups.map((group, index) => {
|
|
126
|
+
const assigned = sceneEntries[index]!;
|
|
127
|
+
const times = [group[0]!.time, group[group.length - 1]!.time, ...assigned.map((item) => item.time)];
|
|
128
|
+
return {
|
|
129
|
+
start: iso(Math.min(...times)), end: iso(Math.max(...times)),
|
|
130
|
+
photos: group.map((item) => item.photo), transcript: assigned.map((item) => item.entry),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
scenes, undatedPhotos,
|
|
135
|
+
transcriptWithoutPhotos: groups.length === 0 ? entries.map((item) => item.entry) : [],
|
|
136
|
+
timeZoneOffsetMinutes: offset,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function localTime(instant: string, offset: number): string {
|
|
141
|
+
const shifted = new Date(Date.parse(instant) + offset * MINUTE).toISOString().slice(0, 16).replace('T', ' ');
|
|
142
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
143
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
144
|
+
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
145
|
+
return `${shifted} UTC${sign}${hours}:${minutes}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function photoLine(photo: FieldPhoto, label: string): string {
|
|
149
|
+
const file = encodeURIComponent(photo.file).replace(/%2F/gi, '/').replace(/[()]/g, (char) => char === '(' ? '%28' : '%29');
|
|
150
|
+
const alt = photo.file.split(/[\\/]/).at(-1)?.replace(/\]/g, '\\]') ?? photo.file;
|
|
151
|
+
return `- ${label} `;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Synchronous, side-effect-free Markdown rendering; summaries are optional placeholders. */
|
|
155
|
+
export function renderFieldNote(timeline: FieldTimeline, summaries: readonly string[] = []): string {
|
|
156
|
+
if (summaries.length > timeline.scenes.length + (timeline.transcriptWithoutPhotos.length ? 1 : 0)) {
|
|
157
|
+
throw new RangeError('More summaries than transcript sections');
|
|
158
|
+
}
|
|
159
|
+
const lines = ['# 현장 노트', ''];
|
|
160
|
+
timeline.scenes.forEach((scene, i) => {
|
|
161
|
+
lines.push(`## 장면 ${i + 1} · ${localTime(scene.start, timeline.timeZoneOffsetMinutes)} – ${localTime(scene.end, timeline.timeZoneOffsetMinutes)}`);
|
|
162
|
+
lines.push('', '### 전사 요약', summaries[i]?.trim() || '_(전사 요약 자리)_', '', '### 사진');
|
|
163
|
+
for (const photo of scene.photos) lines.push(photoLine(photo, localTime(photo.takenAt, timeline.timeZoneOffsetMinutes)));
|
|
164
|
+
lines.push('');
|
|
165
|
+
});
|
|
166
|
+
if (timeline.transcriptWithoutPhotos.length) {
|
|
167
|
+
lines.push('## 사진 없는 전사', '', '### 전사 요약',
|
|
168
|
+
summaries[timeline.scenes.length]?.trim() || '_(전사 요약 자리)_', '', '### 시간순 전사');
|
|
169
|
+
for (const entry of timeline.transcriptWithoutPhotos) {
|
|
170
|
+
lines.push(`- ${entry.timestamp} · ${entry.speaker}: ${entry.text}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
if (timeline.undatedPhotos.length) {
|
|
175
|
+
lines.push('## 시각 없음', '');
|
|
176
|
+
for (const photo of timeline.undatedPhotos) lines.push(photoLine(photo, '시각 없음'));
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** LLM calls are supplied by the caller; JSON transcripts need no transcription call. */
|
|
183
|
+
export async function generateFieldNote(
|
|
184
|
+
photos: readonly FieldPhoto[],
|
|
185
|
+
transcript: readonly TranscriptEntry[] | string,
|
|
186
|
+
llm: FieldNoteLlm = {},
|
|
187
|
+
options: FieldNoteOptions = {},
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
let entries: readonly TranscriptEntry[];
|
|
190
|
+
if (typeof transcript === 'string') {
|
|
191
|
+
if (!llm.transcribe) throw new Error('A transcribe dependency is required for raw transcript input');
|
|
192
|
+
entries = await llm.transcribe(transcript);
|
|
193
|
+
} else entries = transcript;
|
|
194
|
+
const timeline = alignFieldNote(photos, entries, options);
|
|
195
|
+
const sections = timeline.scenes.map((scene) => scene.transcript);
|
|
196
|
+
if (timeline.transcriptWithoutPhotos.length) sections.push(timeline.transcriptWithoutPhotos);
|
|
197
|
+
const summaries = llm.summarize
|
|
198
|
+
? await Promise.all(sections.map((segment) => segment.length ? llm.summarize!(segment) : ''))
|
|
199
|
+
: [];
|
|
200
|
+
return renderFieldNote(timeline, summaries);
|
|
201
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { generateFieldNote, type FieldPhoto, type TranscriptEntry } from './index.js';
|
|
6
|
+
import { transcribeFieldAudio, type FieldAudio, type TranscriptionEngine } from './transcribe.js';
|
|
7
|
+
import type { TgIncoming, TgIncomingAttachment } from '../telegram.js';
|
|
8
|
+
|
|
9
|
+
interface SavedMedia { kind: 'photo' | 'voice' | 'audio'; file: string; receivedAt: string; messageId: number }
|
|
10
|
+
interface NoteWindow { title: string; start: string; end: string; status: 'open' | 'done'; openedBy: number; media: SavedMedia[]; notePath?: string }
|
|
11
|
+
|
|
12
|
+
export interface TelegramFieldNoteDeps {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
download: (fileId: string) => Promise<{ localPath: string }>;
|
|
15
|
+
reply: (ctx: TgIncoming, text: string) => Promise<void>;
|
|
16
|
+
engine?: TranscriptionEngine;
|
|
17
|
+
decodeAudio?: (file: string, recordedAt: string) => FieldAudio;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Decode Telegram's Ogg/Opus (or audio file) into NOTE3a's 16kHz mono float PCM. */
|
|
22
|
+
export function decodeFieldAudio(file: string, recordedAt: string): FieldAudio {
|
|
23
|
+
const result = spawnSync('ffmpeg', ['-v', 'error', '-i', file, '-f', 'f32le', '-ac', '1', '-ar', '16000', 'pipe:1'], { maxBuffer: 1024 * 1024 * 1024 });
|
|
24
|
+
if (result.status !== 0) throw new Error(`음성 디코딩 실패: ${result.stderr.toString().trim() || result.error?.message || 'ffmpeg'}`);
|
|
25
|
+
if (result.stdout.length % 4) throw new Error('음성 디코딩 실패: 잘못된 PCM 길이');
|
|
26
|
+
const bytes = result.stdout;
|
|
27
|
+
const samples = new Float32Array(bytes.length / 4);
|
|
28
|
+
for (let i = 0; i < samples.length; i++) samples[i] = bytes.readFloatLE(i * 4);
|
|
29
|
+
return { samples, sampleRate: 16000, recordedAt };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function windowPath(ctx: TgIncoming, root: string): string {
|
|
33
|
+
const bot = (ctx.botId ?? 'bot').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
34
|
+
return join(root, 'field-notes', 'telegram', bot, String(ctx.chatId), String(ctx.threadId ?? 0), String(ctx.userId), 'window.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readWindow(path: string): NoteWindow | null {
|
|
38
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as NoteWindow : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveWindow(path: string, window: NoteWindow): void {
|
|
42
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
43
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(temp, JSON.stringify(window, null, 2) + '\n');
|
|
46
|
+
renameSync(temp, path);
|
|
47
|
+
} finally {
|
|
48
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseWindow(text: string, now: Date): { title: string; start: string; end: string } | null {
|
|
53
|
+
const match = /^노트:\s*(.+?)\s+(\d{2}):(\d{2})\s*[~~]\s*(\d{2}):(\d{2})$/.exec(text.trim());
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const [, title, h1, m1, h2, m2] = match;
|
|
56
|
+
if (!Number.isFinite(now.getTime()) || +h1! > 23 || +h2! > 23 || +m1! > 59 || +m2! > 59) return null;
|
|
57
|
+
const local = new Date(now.getTime() + 9 * 60 * 60_000).toISOString().slice(0, 10);
|
|
58
|
+
const start = new Date(`${local}T${h1}:${m1}:00+09:00`);
|
|
59
|
+
const end = new Date(`${local}T${h2}:${m2}:00+09:00`);
|
|
60
|
+
if (end <= start) return null;
|
|
61
|
+
return { title: title!.trim(), start: start.toISOString(), end: end.toISOString() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function handleTelegramFieldNote(ctx: TgIncoming, deps: TelegramFieldNoteDeps): Promise<boolean> {
|
|
65
|
+
const text = ctx.text.trim();
|
|
66
|
+
const path = windowPath(ctx, deps.rootDir);
|
|
67
|
+
if (text.startsWith('노트:')) {
|
|
68
|
+
const parsed = parseWindow(text, ctx.receivedAt ? new Date(ctx.receivedAt) : (deps.now ?? (() => new Date()))());
|
|
69
|
+
if (!parsed) await deps.reply(ctx, '노트 시간 형식: 노트: 세미나 18:00~20:00');
|
|
70
|
+
else {
|
|
71
|
+
const previous = readWindow(path);
|
|
72
|
+
if (previous?.status === 'open' && previous.media.length) {
|
|
73
|
+
await deps.reply(ctx, '열린 노트 창이 있습니다 — 먼저 노트 만들기를 완료하세요.');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
saveWindow(path, { ...parsed, status: 'open', openedBy: ctx.messageId, media: [] });
|
|
77
|
+
await deps.reply(ctx, `노트 창 열림 · ${parsed.title} · ${text.slice(text.lastIndexOf(' ') + 1)}`);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (text !== '노트 만들기' && !ctx.attachments.some((a) => a.kind === 'photo' || a.kind === 'voice' || a.kind === 'audio')) return false;
|
|
82
|
+
const window = readWindow(path);
|
|
83
|
+
if (text === '노트 만들기') {
|
|
84
|
+
if (!window || window.status !== 'open') {
|
|
85
|
+
await deps.reply(ctx, '열린 노트 창 없음');
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (window.media.some((item) => item.kind !== 'photo') && !deps.engine) {
|
|
89
|
+
await deps.reply(ctx, '전사 엔진 없음 — NOTE3a 전사 엔진을 설정해 주세요. 모은 파일은 유지됩니다.');
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const photos: FieldPhoto[] = window.media.filter((item) => item.kind === 'photo').map((item) => ({ file: item.file, exifTakenAt: item.receivedAt }));
|
|
94
|
+
const transcript: TranscriptEntry[] = [];
|
|
95
|
+
for (const item of window.media.filter((m) => m.kind !== 'photo')) {
|
|
96
|
+
const audio = (deps.decodeAudio ?? decodeFieldAudio)(item.file, item.receivedAt);
|
|
97
|
+
transcript.push(...await transcribeFieldAudio(audio, deps.engine!));
|
|
98
|
+
}
|
|
99
|
+
const markdown = await generateFieldNote(photos, transcript, {
|
|
100
|
+
summarize: async (entries) => entries.map((entry) => `${entry.timestamp} · ${entry.speaker}: ${entry.text}`).join('\n'),
|
|
101
|
+
});
|
|
102
|
+
const notePath = join(path, '..', `note-${window.openedBy}.md`);
|
|
103
|
+
const noteTemp = `${notePath}.${randomUUID()}.tmp`;
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(noteTemp, `# ${window.title}\n\n시간 창: ${window.start} ~ ${window.end}\n\n${markdown}`);
|
|
106
|
+
renameSync(noteTemp, notePath);
|
|
107
|
+
} finally {
|
|
108
|
+
if (existsSync(noteTemp)) unlinkSync(noteTemp);
|
|
109
|
+
}
|
|
110
|
+
saveWindow(path, { ...window, status: 'done', notePath });
|
|
111
|
+
await deps.reply(ctx, `노트 생성 · ${notePath}`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await deps.reply(ctx, `노트 생성 실패 · ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (!window || window.status !== 'open') return false;
|
|
118
|
+
const media = ctx.attachments.filter((a): a is TgIncomingAttachment & { kind: 'photo' | 'voice' | 'audio' } =>
|
|
119
|
+
a.kind === 'photo' || a.kind === 'voice' || a.kind === 'audio');
|
|
120
|
+
if (!media.length) return false;
|
|
121
|
+
const receivedAt = ctx.receivedAt ?? (deps.now ?? (() => new Date()))().toISOString();
|
|
122
|
+
if (ctx.messageId <= window.openedBy || receivedAt < window.start || receivedAt > window.end) return false;
|
|
123
|
+
const dir = join(path, '..', 'media');
|
|
124
|
+
mkdirSync(dir, { recursive: true });
|
|
125
|
+
try {
|
|
126
|
+
for (const [index, item] of media.entries()) {
|
|
127
|
+
if (window.media.some((saved) => saved.messageId === ctx.messageId && saved.kind === item.kind)) continue;
|
|
128
|
+
const downloaded = await deps.download(item.fileId);
|
|
129
|
+
const extension = item.kind === 'photo' ? 'jpg' : item.kind === 'voice' ? 'ogg'
|
|
130
|
+
: item.mimeType === 'audio/mpeg' ? 'mp3' : item.mimeType === 'audio/ogg' ? 'ogg' : 'audio';
|
|
131
|
+
const file = join(dir, `${ctx.messageId}-${index}.${extension}`);
|
|
132
|
+
try { copyFileSync(downloaded.localPath, file); }
|
|
133
|
+
finally { try { unlinkSync(downloaded.localPath); } catch { /* download may be reused by the caller */ } }
|
|
134
|
+
window.media.push({ kind: item.kind, file, receivedAt, messageId: ctx.messageId });
|
|
135
|
+
saveWindow(path, window);
|
|
136
|
+
}
|
|
137
|
+
await deps.reply(ctx, `노트 파일 모음 · 사진 ${window.media.filter((m) => m.kind === 'photo').length}장 · 음성 ${window.media.filter((m) => m.kind !== 'photo').length}개`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await deps.reply(ctx, `노트 파일 저장 실패 · ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|