gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal PulseAudio native-protocol v13 codec: tagstruct primitives plus the
|
|
3
|
+
* 20-byte pstream frame descriptor. This is an original MIT (repo-licensed)
|
|
4
|
+
* implementation derived only from the official protocol declaration; no
|
|
5
|
+
* PulseAudio/LGPL implementation code is copied.
|
|
6
|
+
*
|
|
7
|
+
* Protocol reference (read-only, PulseAudio v17.0):
|
|
8
|
+
* PROTOCOL; src/pulsecore/native-common.h; src/pulsecore/tagstruct.[ch];
|
|
9
|
+
* src/pulsecore/pstream.[ch]; src/pulse/sample.h.
|
|
10
|
+
*
|
|
11
|
+
* Pure module: no sockets, no cookies, no clocks, no process, no backend IO.
|
|
12
|
+
*/
|
|
13
|
+
export const NATIVE_PROTOCOL_VERSION = 13;
|
|
14
|
+
export const CONTROL_CHANNEL = 0xffffffff;
|
|
15
|
+
export const MAX_CONTROL_PAYLOAD = 64 * 1024;
|
|
16
|
+
export const MAX_FRAMES_PER_PUSH = 32;
|
|
17
|
+
export const PULSE_FRAME_HEADER_SIZE = 20;
|
|
18
|
+
|
|
19
|
+
/** Command values are the C enum order of native-common.h (the enum is the wire value). */
|
|
20
|
+
export const PulseCommand = {
|
|
21
|
+
ERROR: 0, TIMEOUT: 1, REPLY: 2, CREATE_PLAYBACK_STREAM: 3, DELETE_PLAYBACK_STREAM: 4,
|
|
22
|
+
AUTH: 8, SET_CLIENT_NAME: 9, DRAIN_PLAYBACK_STREAM: 12, GET_SERVER_INFO: 20, REQUEST: 61,
|
|
23
|
+
} as const;
|
|
24
|
+
export type PulseCommand = (typeof PulseCommand)[keyof typeof PulseCommand];
|
|
25
|
+
|
|
26
|
+
/** Sample format values from sample.h. S24LE/S24BE are packed 24-bit; S24_32* are not. */
|
|
27
|
+
export const PulseSampleFormat = {
|
|
28
|
+
U8: 0, ALAW: 1, ULAW: 2, S16LE: 3, S16BE: 4, FLOAT32LE: 5, FLOAT32BE: 6,
|
|
29
|
+
S32LE: 7, S32BE: 8, S24LE: 9, S24BE: 10, S24_32LE: 11, S24_32BE: 12,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export interface PulseSampleSpec { readonly format: number; readonly channels: number; readonly rate: number; }
|
|
33
|
+
export interface PulseFrame {
|
|
34
|
+
readonly channel: number;
|
|
35
|
+
readonly offset: bigint;
|
|
36
|
+
readonly flags: number;
|
|
37
|
+
readonly payload: Buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TAG_STRING = 0x74; // 't'
|
|
41
|
+
const TAG_STRING_NULL = 0x4e; // 'N'
|
|
42
|
+
const TAG_U32 = 0x4c; // 'L'
|
|
43
|
+
const TAG_U8 = 0x42; // 'B'
|
|
44
|
+
const TAG_ARBITRARY = 0x78; // 'x'
|
|
45
|
+
const TAG_BOOLEAN_TRUE = 0x31; // '1'
|
|
46
|
+
const TAG_BOOLEAN_FALSE = 0x30; // '0'
|
|
47
|
+
const TAG_USEC = 0x55; // 'U'
|
|
48
|
+
const TAG_SAMPLE_SPEC = 0x61; // 'a'
|
|
49
|
+
const TAG_CHANNEL_MAP = 0x6d; // 'm'
|
|
50
|
+
const TAG_CVOLUME = 0x76; // 'v'
|
|
51
|
+
const TAG_PROPLIST = 0x50; // 'P'
|
|
52
|
+
const NUL = Buffer.from([0]);
|
|
53
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
54
|
+
|
|
55
|
+
/** Generic, data-free failure: never echoes field values or cookie/key contents. */
|
|
56
|
+
function fail(message: string): never { throw new Error(`Pulse protocol: ${message}`); }
|
|
57
|
+
|
|
58
|
+
function appendU32(target: Buffer, value: number, offset: number): void { target.writeUInt32BE(value, offset); }
|
|
59
|
+
|
|
60
|
+
/** Bounded chainable tagstruct writer. */
|
|
61
|
+
export class PulseTagWriter {
|
|
62
|
+
private readonly chunks: Buffer[] = [];
|
|
63
|
+
private size = 0;
|
|
64
|
+
|
|
65
|
+
private append(chunk: Buffer): this {
|
|
66
|
+
if (this.size + chunk.length > MAX_CONTROL_PAYLOAD) fail("tag payload too large");
|
|
67
|
+
this.size += chunk.length;
|
|
68
|
+
this.chunks.push(chunk);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
u32(value: number): this {
|
|
73
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) fail("u32 out of range");
|
|
74
|
+
const head = Buffer.allocUnsafe(5);
|
|
75
|
+
head[0] = TAG_U32;
|
|
76
|
+
appendU32(head, value, 1);
|
|
77
|
+
return this.append(head);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
u8(value: number): this {
|
|
81
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xff) fail("u8 out of range");
|
|
82
|
+
return this.append(Buffer.from([TAG_U8, value]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
string(value: string | null): this {
|
|
86
|
+
if (value === null) return this.append(Buffer.from([TAG_STRING_NULL]));
|
|
87
|
+
if (typeof value !== "string" || value.includes("\0")) fail("invalid string");
|
|
88
|
+
return this.append(Buffer.concat([Buffer.from([TAG_STRING]), Buffer.from(value, "utf8"), NUL]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
arbitrary(value: Uint8Array): this {
|
|
92
|
+
if (!(value instanceof Uint8Array) || value.length > MAX_CONTROL_PAYLOAD) fail("arbitrary out of range");
|
|
93
|
+
const head = Buffer.allocUnsafe(5);
|
|
94
|
+
head[0] = TAG_ARBITRARY;
|
|
95
|
+
appendU32(head, value.length, 1);
|
|
96
|
+
return this.append(head).append(Buffer.from(value.buffer, value.byteOffset, value.length));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
boolean(value: boolean): this {
|
|
100
|
+
if (typeof value !== "boolean") fail("invalid boolean");
|
|
101
|
+
return this.append(Buffer.from([value ? TAG_BOOLEAN_TRUE : TAG_BOOLEAN_FALSE]));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
usec(value: bigint): this {
|
|
105
|
+
if (typeof value !== "bigint" || value < 0n || value > 0xffffffffffffffffn) fail("usec out of range");
|
|
106
|
+
const head = Buffer.allocUnsafe(9);
|
|
107
|
+
head[0] = TAG_USEC;
|
|
108
|
+
head.writeBigUInt64BE(value, 1);
|
|
109
|
+
return this.append(head);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sampleSpec(spec: PulseSampleSpec): this {
|
|
113
|
+
if (!spec || !Number.isInteger(spec.format) || spec.format < 0 || spec.format > 0xff) fail("invalid sample format");
|
|
114
|
+
if (!Number.isInteger(spec.channels) || spec.channels < 1 || spec.channels > 0xff) fail("invalid sample channels");
|
|
115
|
+
if (!Number.isInteger(spec.rate) || spec.rate < 0 || spec.rate > 0xffffffff) fail("invalid sample rate");
|
|
116
|
+
const body = Buffer.allocUnsafe(7);
|
|
117
|
+
body[0] = TAG_SAMPLE_SPEC;
|
|
118
|
+
body[1] = spec.format;
|
|
119
|
+
body[2] = spec.channels;
|
|
120
|
+
appendU32(body, spec.rate, 3);
|
|
121
|
+
return this.append(body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
channelMap(positions: readonly number[]): this {
|
|
125
|
+
if (!Array.isArray(positions) || positions.length < 1 || positions.length > 0xff
|
|
126
|
+
|| !positions.every(position => Number.isInteger(position) && position >= 0 && position <= 0xff))
|
|
127
|
+
fail("invalid channel map");
|
|
128
|
+
return this.append(Buffer.from([TAG_CHANNEL_MAP, positions.length, ...positions]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cvolume(volumes: readonly number[]): this {
|
|
132
|
+
if (!Array.isArray(volumes) || volumes.length < 1 || volumes.length > 0xff
|
|
133
|
+
|| !volumes.every(volume => Number.isInteger(volume) && volume >= 0 && volume <= 0xffffffff))
|
|
134
|
+
fail("invalid cvolume");
|
|
135
|
+
const body = Buffer.allocUnsafe(2 + volumes.length * 4);
|
|
136
|
+
body[0] = TAG_CVOLUME;
|
|
137
|
+
body[1] = volumes.length;
|
|
138
|
+
volumes.forEach((volume, index) => appendU32(body, volume, 2 + index * 4));
|
|
139
|
+
return this.append(body);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Every entry is `string` key + tagged `u32` length + arbitrary raw-length value; `'N'` ends it. */
|
|
143
|
+
proplist(entries: Iterable<readonly [string, string | Uint8Array]>): this {
|
|
144
|
+
const seen = new Set<string>();
|
|
145
|
+
this.append(Buffer.from([TAG_PROPLIST]));
|
|
146
|
+
for (const [key, value] of entries) {
|
|
147
|
+
if (typeof key !== "string" || key.length === 0 || /[\x00-\x1f\x7f]/.test(key)) fail("invalid proplist key");
|
|
148
|
+
if (seen.has(key)) fail("duplicate proplist key");
|
|
149
|
+
seen.add(key);
|
|
150
|
+
const bytes = typeof value === "string" ? Buffer.concat([Buffer.from(value, "utf8"), NUL]) : Buffer.from(value);
|
|
151
|
+
if (bytes.length > MAX_CONTROL_PAYLOAD) fail("proplist value too large");
|
|
152
|
+
this.string(key);
|
|
153
|
+
this.u32(bytes.length);
|
|
154
|
+
this.arbitrary(bytes);
|
|
155
|
+
}
|
|
156
|
+
return this.append(Buffer.from([TAG_STRING_NULL]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
finish(): Buffer { return Buffer.concat(this.chunks, this.size); }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function boundedPulseTagWriter(): PulseTagWriter { return new PulseTagWriter(); }
|
|
163
|
+
|
|
164
|
+
/** Exact-range tagstruct reader; any exhaustion or tag mismatch throws a generic error. */
|
|
165
|
+
export class PulseTagReader {
|
|
166
|
+
private readonly data: Buffer;
|
|
167
|
+
private offset = 0;
|
|
168
|
+
|
|
169
|
+
constructor(data: Buffer) { this.data = data; }
|
|
170
|
+
|
|
171
|
+
private take(count: number): Buffer {
|
|
172
|
+
if (!Number.isInteger(count) || count < 0 || this.offset + count > this.data.length) fail("read out of range");
|
|
173
|
+
const slice = this.data.subarray(this.offset, this.offset + count);
|
|
174
|
+
this.offset += count;
|
|
175
|
+
return slice;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private expect(tag: number): void { if (this.take(1)[0] !== tag) fail("unexpected tag"); }
|
|
179
|
+
|
|
180
|
+
u32(): number { this.expect(TAG_U32); return this.take(4).readUInt32BE(0); }
|
|
181
|
+
u8(): number { this.expect(TAG_U8); return this.take(1)[0]!; }
|
|
182
|
+
|
|
183
|
+
string(): string | null {
|
|
184
|
+
const tag = this.take(1)[0];
|
|
185
|
+
if (tag === TAG_STRING_NULL) return null;
|
|
186
|
+
if (tag !== TAG_STRING) fail("unexpected tag");
|
|
187
|
+
let end = this.offset;
|
|
188
|
+
while (end < this.data.length && this.data[end] !== 0) end++;
|
|
189
|
+
if (end >= this.data.length) fail("unterminated string");
|
|
190
|
+
const bytes = Buffer.from(this.take(end - this.offset));
|
|
191
|
+
this.take(1); // trailing NUL
|
|
192
|
+
try { return utf8.decode(bytes); } catch { return fail("invalid UTF-8 string"); }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
arbitrary(): Buffer {
|
|
196
|
+
this.expect(TAG_ARBITRARY);
|
|
197
|
+
const length = this.take(4).readUInt32BE(0);
|
|
198
|
+
if (length > MAX_CONTROL_PAYLOAD) fail("arbitrary too large");
|
|
199
|
+
return Buffer.from(this.take(length));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
boolean(): boolean {
|
|
203
|
+
const tag = this.take(1)[0];
|
|
204
|
+
if (tag === TAG_BOOLEAN_TRUE) return true;
|
|
205
|
+
if (tag === TAG_BOOLEAN_FALSE) return false;
|
|
206
|
+
return fail("unexpected tag");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
usec(): bigint { this.expect(TAG_USEC); return this.take(8).readBigUInt64BE(0); }
|
|
210
|
+
|
|
211
|
+
sampleSpec(): PulseSampleSpec {
|
|
212
|
+
this.expect(TAG_SAMPLE_SPEC);
|
|
213
|
+
const body = this.take(6);
|
|
214
|
+
return { format: body[0]!, channels: body[1]!, rate: body.readUInt32BE(2) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
channelMap(): number[] {
|
|
218
|
+
this.expect(TAG_CHANNEL_MAP);
|
|
219
|
+
const channels = this.take(1)[0]!;
|
|
220
|
+
if (channels === 0) fail("invalid channel map");
|
|
221
|
+
return [...this.take(channels)];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
cvolume(): number[] {
|
|
225
|
+
this.expect(TAG_CVOLUME);
|
|
226
|
+
const channels = this.take(1)[0]!;
|
|
227
|
+
if (channels === 0) fail("invalid cvolume");
|
|
228
|
+
const body = this.take(channels * 4);
|
|
229
|
+
return Array.from({ length: channels }, (_, index) => body.readUInt32BE(index * 4));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
proplist(): Map<string, Buffer> {
|
|
233
|
+
this.expect(TAG_PROPLIST);
|
|
234
|
+
const entries = new Map<string, Buffer>();
|
|
235
|
+
for (;;) {
|
|
236
|
+
const key = this.string();
|
|
237
|
+
if (key === null) break;
|
|
238
|
+
const declared = this.u32();
|
|
239
|
+
const value = this.arbitrary();
|
|
240
|
+
if (value.length !== declared) fail("proplist length mismatch");
|
|
241
|
+
if (entries.has(key)) fail("duplicate proplist key");
|
|
242
|
+
entries.set(key, value);
|
|
243
|
+
}
|
|
244
|
+
return entries;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
assertEOF(): void { if (this.offset !== this.data.length) fail("trailing bytes"); }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function boundedPulseTagReader(data: Buffer): PulseTagReader { return new PulseTagReader(data); }
|
|
251
|
+
|
|
252
|
+
/** Control frames carry a tagged payload on channel 0xffffffff; PCM memblocks carry raw bytes. */
|
|
253
|
+
function validateDescriptor(channel: number, offset: bigint, flags: number): void {
|
|
254
|
+
if (channel === CONTROL_CHANNEL) {
|
|
255
|
+
if (offset !== 0n) fail("control frame offset must be zero");
|
|
256
|
+
if (flags !== 0) fail("control frame flags must be zero");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if ((flags & 0xff000000) !== 0) fail("unsupported shared-memory frame flags");
|
|
260
|
+
if ((flags & 0xff) > 2) fail("invalid seek mode");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function encodePulseFrame(descriptor: { channel: number; offset: bigint; flags: number }, payload: Uint8Array): Buffer {
|
|
264
|
+
const { channel, offset, flags } = descriptor;
|
|
265
|
+
if (!Number.isInteger(channel) || channel < 0 || channel > 0xffffffff) fail("invalid channel");
|
|
266
|
+
if (typeof offset !== "bigint" || offset < 0n || offset > 0xffffffffffffffffn) fail("invalid offset");
|
|
267
|
+
if (!Number.isInteger(flags) || flags < 0 || flags > 0xffffffff) fail("invalid flags");
|
|
268
|
+
if (!(payload instanceof Uint8Array) || payload.length < 1 || payload.length > MAX_CONTROL_PAYLOAD) fail("invalid frame payload");
|
|
269
|
+
validateDescriptor(channel, offset, flags);
|
|
270
|
+
const header = Buffer.allocUnsafe(PULSE_FRAME_HEADER_SIZE);
|
|
271
|
+
appendU32(header, payload.length, 0);
|
|
272
|
+
appendU32(header, channel, 4);
|
|
273
|
+
appendU32(header, Number(offset >> 32n), 8);
|
|
274
|
+
appendU32(header, Number(offset & 0xffffffffn), 12);
|
|
275
|
+
appendU32(header, flags, 16);
|
|
276
|
+
return Buffer.concat([header, Buffer.from(payload.buffer, payload.byteOffset, payload.length)]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Incremental descriptor decoder: handles fragmented and coalesced frames, bounded per push. */
|
|
280
|
+
export class PulseFrameDecoder {
|
|
281
|
+
private buffer = Buffer.alloc(0);
|
|
282
|
+
|
|
283
|
+
push(chunk: Uint8Array): PulseFrame[] {
|
|
284
|
+
if (chunk.length > 0) this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk.buffer, chunk.byteOffset, chunk.length)]);
|
|
285
|
+
const frames: PulseFrame[] = [];
|
|
286
|
+
for (;;) {
|
|
287
|
+
if (this.buffer.length < PULSE_FRAME_HEADER_SIZE) break;
|
|
288
|
+
const length = this.buffer.readUInt32BE(0);
|
|
289
|
+
const channel = this.buffer.readUInt32BE(4);
|
|
290
|
+
const offset = (BigInt(this.buffer.readUInt32BE(8)) << 32n) | BigInt(this.buffer.readUInt32BE(12));
|
|
291
|
+
const flags = this.buffer.readUInt32BE(16);
|
|
292
|
+
if (length < 1 || length > MAX_CONTROL_PAYLOAD) fail("invalid frame length");
|
|
293
|
+
validateDescriptor(channel, offset, flags);
|
|
294
|
+
if (this.buffer.length < PULSE_FRAME_HEADER_SIZE + length) break;
|
|
295
|
+
const payload = Buffer.from(this.buffer.subarray(PULSE_FRAME_HEADER_SIZE, PULSE_FRAME_HEADER_SIZE + length));
|
|
296
|
+
this.buffer = this.buffer.subarray(PULSE_FRAME_HEADER_SIZE + length);
|
|
297
|
+
frames.push({ channel, offset, flags, payload });
|
|
298
|
+
if (frames.length > MAX_FRAMES_PER_PUSH) fail("too many frames in one push");
|
|
299
|
+
}
|
|
300
|
+
if (this.buffer.length > PULSE_FRAME_HEADER_SIZE + MAX_CONTROL_PAYLOAD) fail("frame buffer too large");
|
|
301
|
+
return frames;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function boundedPulseFrameDecoder(): PulseFrameDecoder { return new PulseFrameDecoder(); }
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PulseAudio native-protocol v13 playback stream for PCM WAV: strict WAV
|
|
3
|
+
* validation + extraction, CREATE_PLAYBACK_STREAM v13, REQUEST-driven PCM
|
|
4
|
+
* memblock writes, DRAIN, DELETE. Original MIT (repo-licensed) implementation
|
|
5
|
+
* derived from the official protocol declaration only; no PulseAudio/LGPL
|
|
6
|
+
* implementation code is copied.
|
|
7
|
+
*
|
|
8
|
+
* Reference (read-only, PulseAudio v17.0): src/pulsecore/protocol-native.c
|
|
9
|
+
* (CREATE/DRAIN/DELETE, playback_stream_request_bytes), src/pulsecore/pstream.c
|
|
10
|
+
* (memblock frames), src/pulse/stream.c (write path), PROTOCOL v13. See
|
|
11
|
+
* odd/tasks/native-audio-backend.md.
|
|
12
|
+
*
|
|
13
|
+
* No stream is created here except by an explicit playPulseWav() call; bytes
|
|
14
|
+
* are validated before any connect/create/PCM. Channel positions are the
|
|
15
|
+
* standard mono(0)/stereo(1,2) mapping (channelmap.h not in the reference set).
|
|
16
|
+
*/
|
|
17
|
+
import { validateNotificationWav } from "./notification-audio.ts";
|
|
18
|
+
import {
|
|
19
|
+
MAX_REQUEST_TIMEOUT_MS, PulseClient, loadPulseCookie, resolvePulseCookiePath, type PulseClientOptions,
|
|
20
|
+
} from "./notification-pulse-client.ts";
|
|
21
|
+
import {
|
|
22
|
+
PulseCommand, PulseSampleFormat, boundedPulseTagWriter, type PulseSampleSpec, type PulseTagReader, type PulseTagWriter,
|
|
23
|
+
} from "./notification-pulse-protocol.ts";
|
|
24
|
+
|
|
25
|
+
export const MAX_PCM_CHUNK = 64 * 1024;
|
|
26
|
+
/** 10 s maximum validated duration plus a 1 s termination margin; the DRAIN ack can wait for the full sound. */
|
|
27
|
+
export const DEFAULT_PLAYBACK_BUDGET_MS = 11000;
|
|
28
|
+
export const MAX_EARLY_REQUESTS = 32;
|
|
29
|
+
const PA_INVALID_INDEX = 0xffffffff;
|
|
30
|
+
const PA_VOLUME_NORM = 0x10000;
|
|
31
|
+
|
|
32
|
+
export interface WavPCM {
|
|
33
|
+
readonly format: number;
|
|
34
|
+
readonly sampleSpec: PulseSampleSpec;
|
|
35
|
+
readonly channelMap: readonly number[];
|
|
36
|
+
readonly data: Buffer;
|
|
37
|
+
readonly frameSize: number;
|
|
38
|
+
readonly durationMs: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PulsePlaybackOptions {
|
|
41
|
+
client?: PulseClient;
|
|
42
|
+
clientOptions?: PulseClientOptions;
|
|
43
|
+
adjustLatency?: boolean;
|
|
44
|
+
budgetMs?: number;
|
|
45
|
+
requestTimeoutMs?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pulseFormatForBits(bits: number): number {
|
|
49
|
+
if (bits === 8) return PulseSampleFormat.U8;
|
|
50
|
+
if (bits === 16) return PulseSampleFormat.S16LE;
|
|
51
|
+
if (bits === 24) return PulseSampleFormat.S24LE;
|
|
52
|
+
if (bits === 32) return PulseSampleFormat.S32LE;
|
|
53
|
+
throw new Error("Pulse stream: unsupported PCM depth");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Validate with the existing strict 9a validator, then extract fmt/data and map to PulseAudio. */
|
|
57
|
+
export function parsePulseWav(bytes: Buffer): WavPCM {
|
|
58
|
+
const durationMs = validateNotificationWav(bytes);
|
|
59
|
+
let offset = 12;
|
|
60
|
+
let bits = 0; let channels = 0; let rate = 0; let data: Buffer | undefined;
|
|
61
|
+
while (offset + 8 <= bytes.length) {
|
|
62
|
+
const id = bytes.toString("ascii", offset, offset + 4);
|
|
63
|
+
const size = bytes.readUInt32LE(offset + 4);
|
|
64
|
+
const start = offset + 8;
|
|
65
|
+
if (id === "fmt ") {
|
|
66
|
+
channels = bytes.readUInt16LE(start + 2);
|
|
67
|
+
rate = bytes.readUInt32LE(start + 4);
|
|
68
|
+
bits = bytes.readUInt16LE(start + 14);
|
|
69
|
+
} else if (id === "data") data = Buffer.from(bytes.subarray(start, start + size));
|
|
70
|
+
offset = start + size + (size % 2);
|
|
71
|
+
}
|
|
72
|
+
if (!data || channels < 1 || channels > 2) throw new Error("Pulse stream: invalid WAV");
|
|
73
|
+
const format = pulseFormatForBits(bits);
|
|
74
|
+
const sampleSpec: PulseSampleSpec = Object.freeze({ format, channels, rate });
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
format, sampleSpec,
|
|
77
|
+
channelMap: Object.freeze(channels === 1 ? [0] : [1, 2]),
|
|
78
|
+
data, frameSize: channels * (bits / 8), durationMs,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** CREATE_PLAYBACK_STREAM v13 args: default sink, all seven flags false, corked/muted false, NORM volume. */
|
|
83
|
+
function buildCreateArgs(pcm: WavPCM, adjustLatency: boolean): PulseTagWriter {
|
|
84
|
+
const volumes = Array.from({ length: pcm.sampleSpec.channels }, () => PA_VOLUME_NORM);
|
|
85
|
+
return boundedPulseTagWriter()
|
|
86
|
+
.sampleSpec(pcm.sampleSpec)
|
|
87
|
+
.channelMap(pcm.channelMap)
|
|
88
|
+
.u32(PA_INVALID_INDEX)
|
|
89
|
+
.string(null)
|
|
90
|
+
.u32(0xffffffff) // maxlength
|
|
91
|
+
.boolean(false) // corked
|
|
92
|
+
.u32(0) // tlength
|
|
93
|
+
.u32(0) // prebuf
|
|
94
|
+
.u32(0) // minreq
|
|
95
|
+
.u32(0) // syncid
|
|
96
|
+
.cvolume(volumes)
|
|
97
|
+
.boolean(false).boolean(false).boolean(false).boolean(false).boolean(false).boolean(false).boolean(false) // 7 flags
|
|
98
|
+
.boolean(false) // muted
|
|
99
|
+
.boolean(adjustLatency)
|
|
100
|
+
.proplist([["media.name", "gentle-shell notification"], ["media.role", "event"]]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** v13 reply: stream/sink indices, missing, buffer attrs, spec, map, device, name, suspended, latency. */
|
|
104
|
+
function parseCreateReply(reader: PulseTagReader, pcm: WavPCM): { streamIndex: number; missing: number } {
|
|
105
|
+
const streamIndex = reader.u32();
|
|
106
|
+
reader.u32(); // sink_input index
|
|
107
|
+
const missing = reader.u32();
|
|
108
|
+
reader.u32(); reader.u32(); reader.u32(); reader.u32(); // maxlength, tlength, prebuf, minreq
|
|
109
|
+
const spec = reader.sampleSpec();
|
|
110
|
+
const map = reader.channelMap();
|
|
111
|
+
reader.u32(); // sink device index
|
|
112
|
+
reader.string(); // sink name
|
|
113
|
+
reader.boolean(); // suspended
|
|
114
|
+
reader.usec(); // configured sink latency
|
|
115
|
+
reader.assertEOF();
|
|
116
|
+
if (spec.format !== pcm.sampleSpec.format || spec.channels !== pcm.sampleSpec.channels || spec.rate !== pcm.sampleSpec.rate)
|
|
117
|
+
throw new Error("Pulse stream: server sample spec mismatch");
|
|
118
|
+
if (map.length !== pcm.sampleSpec.channels) throw new Error("Pulse stream: server channel map mismatch");
|
|
119
|
+
return { streamIndex, missing };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function streamAbortError(): Error { const error = new Error("Pulse stream: aborted"); error.name = "AbortError"; return error; }
|
|
123
|
+
function combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {
|
|
124
|
+
const list = signals.filter((value): value is AbortSignal => value !== undefined);
|
|
125
|
+
if (list.length === 0) return new AbortController().signal;
|
|
126
|
+
if (list.length === 1) return list[0]!;
|
|
127
|
+
return AbortSignal.any(list);
|
|
128
|
+
}
|
|
129
|
+
function unrefTimer(handle: ReturnType<typeof setTimeout>): void { (handle as { unref?: () => void }).unref?.(); }
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Play a validated PCM WAV over an authorized Pulse link. The caller supplies a
|
|
133
|
+
* connected+authenticated `client`, or clientOptions to own the connection.
|
|
134
|
+
* Returns after DRAIN and DELETE acks; closes the connection in all paths.
|
|
135
|
+
*/
|
|
136
|
+
export async function playPulseWav(bytes: Buffer, signal?: AbortSignal, options: PulsePlaybackOptions = {}): Promise<void> {
|
|
137
|
+
const pcm = parsePulseWav(bytes);
|
|
138
|
+
const ownsClient = options.client === undefined;
|
|
139
|
+
const clientOptions = options.clientOptions ?? {};
|
|
140
|
+
const client = options.client ?? new PulseClient(clientOptions);
|
|
141
|
+
const budgetMs = options.budgetMs ?? DEFAULT_PLAYBACK_BUDGET_MS;
|
|
142
|
+
const requestMs = Math.min(options.requestTimeoutMs ?? MAX_REQUEST_TIMEOUT_MS, MAX_REQUEST_TIMEOUT_MS);
|
|
143
|
+
const budget = new AbortController();
|
|
144
|
+
const budgetTimer = setTimeout(() => budget.abort(new Error("Pulse stream: budget exceeded")), budgetMs);
|
|
145
|
+
unrefTimer(budgetTimer);
|
|
146
|
+
const combined = combineSignals(signal, budget.signal);
|
|
147
|
+
const total = pcm.data.length;
|
|
148
|
+
const chunkCap = MAX_PCM_CHUNK - (MAX_PCM_CHUNK % pcm.frameSize);
|
|
149
|
+
let streamIndex: number | undefined;
|
|
150
|
+
let created = false;
|
|
151
|
+
let failure: Error | undefined;
|
|
152
|
+
let demand = 0; let sent = 0;
|
|
153
|
+
let wake: (() => void) | undefined;
|
|
154
|
+
const early: Array<{ index: number; bytes: number }> = [];
|
|
155
|
+
const wakeWaiter = () => { const resolve = wake; wake = undefined; resolve?.(); };
|
|
156
|
+
const listener = (command: number, reader: PulseTagReader) => {
|
|
157
|
+
if (command !== PulseCommand.REQUEST) return;
|
|
158
|
+
let index: number; let wanted: number;
|
|
159
|
+
try { index = reader.u32(); wanted = reader.u32(); reader.assertEOF(); }
|
|
160
|
+
catch { failure ??= new Error("Pulse stream: malformed request"); wakeWaiter(); return; }
|
|
161
|
+
if (streamIndex === undefined) {
|
|
162
|
+
if (early.length >= MAX_EARLY_REQUESTS) { failure ??= new Error("Pulse stream: too many early requests"); wakeWaiter(); return; }
|
|
163
|
+
early.push({ index, bytes: wanted }); return;
|
|
164
|
+
}
|
|
165
|
+
if (index !== streamIndex) { failure ??= new Error("Pulse stream: unknown stream request"); wakeWaiter(); return; }
|
|
166
|
+
demand += wanted;
|
|
167
|
+
wakeWaiter();
|
|
168
|
+
};
|
|
169
|
+
const off = client.onEvent(listener);
|
|
170
|
+
try {
|
|
171
|
+
if (ownsClient) {
|
|
172
|
+
await client.connect(combined);
|
|
173
|
+
const cookie = clientOptions.cookie ?? await loadPulseCookie(resolvePulseCookiePath(clientOptions.env ?? {}), combined);
|
|
174
|
+
await client.authenticate(cookie, {}, combined);
|
|
175
|
+
}
|
|
176
|
+
const reply = await client.request(PulseCommand.CREATE_PLAYBACK_STREAM, buildCreateArgs(pcm, options.adjustLatency === true), combined, requestMs);
|
|
177
|
+
const createdReply = parseCreateReply(reply, pcm);
|
|
178
|
+
streamIndex = createdReply.streamIndex;
|
|
179
|
+
created = true;
|
|
180
|
+
for (const request of early) { if (request.index !== streamIndex) throw new Error("Pulse stream: unknown stream request"); demand += request.bytes; }
|
|
181
|
+
demand += createdReply.missing;
|
|
182
|
+
while (sent < total) {
|
|
183
|
+
if (failure) throw failure;
|
|
184
|
+
if (combined.aborted) throw combined.reason instanceof Error ? combined.reason : streamAbortError();
|
|
185
|
+
const available = Math.min(demand, total - sent);
|
|
186
|
+
const aligned = available - (available % pcm.frameSize);
|
|
187
|
+
if (aligned < pcm.frameSize) {
|
|
188
|
+
await new Promise<void>((resolve, reject) => {
|
|
189
|
+
const onAbort = () => { wake = undefined; reject(combined.reason instanceof Error ? combined.reason : streamAbortError()); };
|
|
190
|
+
combined.addEventListener("abort", onAbort, { once: true });
|
|
191
|
+
wake = () => { combined.removeEventListener("abort", onAbort); resolve(); };
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const chunk = Math.min(aligned, chunkCap);
|
|
196
|
+
await client.sendDataFrame(streamIndex, pcm.data.subarray(sent, sent + chunk), combined, requestMs);
|
|
197
|
+
sent += chunk; demand -= chunk;
|
|
198
|
+
}
|
|
199
|
+
(await client.request(PulseCommand.DRAIN_PLAYBACK_STREAM, boundedPulseTagWriter().u32(streamIndex), combined, requestMs)).assertEOF();
|
|
200
|
+
(await client.request(PulseCommand.DELETE_PLAYBACK_STREAM, boundedPulseTagWriter().u32(streamIndex), combined, requestMs)).assertEOF();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
failure ??= error as Error;
|
|
203
|
+
if (created && streamIndex !== undefined) {
|
|
204
|
+
try { await client.request(PulseCommand.DELETE_PLAYBACK_STREAM, boundedPulseTagWriter().u32(streamIndex), undefined, 500); }
|
|
205
|
+
catch { /* best effort when the link is still usable */ }
|
|
206
|
+
}
|
|
207
|
+
throw failure;
|
|
208
|
+
} finally {
|
|
209
|
+
off();
|
|
210
|
+
clearTimeout(budgetTimer);
|
|
211
|
+
await client.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone PulseAudio worker for the native notification backend. Runs as an
|
|
3
|
+
* owned Node child (`node --experimental-strip-types --max-old-space-size=32`)
|
|
4
|
+
* and speaks a single bounded JSON envelope on stdout:
|
|
5
|
+
* probe -> { schema, ok, available, formats }
|
|
6
|
+
* play -> { schema, ok, played }
|
|
7
|
+
* Failures are private: a generic `{ schema, ok:false }` and a nonzero exit.
|
|
8
|
+
* It reads only argv, the environment and the owner-supplied snapshot; no SDK,
|
|
9
|
+
* UI, extension, config-owner, cookie-content logging or OS audio surface.
|
|
10
|
+
*/
|
|
11
|
+
import { constants } from "node:fs";
|
|
12
|
+
import { open } from "node:fs/promises";
|
|
13
|
+
import { PulseClient } from "./notification-pulse-client.ts";
|
|
14
|
+
import { playPulseWav } from "./notification-pulse-stream.ts";
|
|
15
|
+
|
|
16
|
+
const SCHEMA = "gentle.audio.pulse/v1";
|
|
17
|
+
const MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
function emit(payload: Record<string, unknown>): void {
|
|
20
|
+
process.stdout.write(`${JSON.stringify({ schema: SCHEMA, ...payload })}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Re-read the trusted owner snapshot with the same bounded nofollow pattern. */
|
|
24
|
+
async function readSnapshot(path: string): Promise<Buffer> {
|
|
25
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
26
|
+
try {
|
|
27
|
+
const stat = await handle.stat();
|
|
28
|
+
if (!stat.isFile() || stat.size < 44 || stat.size > MAX_SNAPSHOT_BYTES) throw new Error("invalid snapshot");
|
|
29
|
+
const buffer = Buffer.alloc(MAX_SNAPSHOT_BYTES + 1);
|
|
30
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
31
|
+
if (bytesRead !== stat.size) throw new Error("invalid snapshot");
|
|
32
|
+
return Buffer.from(buffer.subarray(0, bytesRead));
|
|
33
|
+
} finally {
|
|
34
|
+
await handle.close().catch(() => { /* close failure must not mask the outcome */ });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main(): Promise<void> {
|
|
39
|
+
const mode = process.argv[2];
|
|
40
|
+
if (mode === "probe") {
|
|
41
|
+
try {
|
|
42
|
+
const result = await new PulseClient({ env: process.env }).probe();
|
|
43
|
+
emit({ ok: true, available: result.available, formats: result.formats });
|
|
44
|
+
} catch {
|
|
45
|
+
emit({ ok: true, available: false, formats: [] });
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (mode === "play") {
|
|
50
|
+
const snapshot = process.argv[3];
|
|
51
|
+
if (snapshot === undefined) { emit({ ok: false }); process.exitCode = 1; return; }
|
|
52
|
+
try {
|
|
53
|
+
const bytes = await readSnapshot(snapshot);
|
|
54
|
+
await playPulseWav(bytes, undefined, { clientOptions: { env: process.env } });
|
|
55
|
+
emit({ ok: true, played: true });
|
|
56
|
+
} catch {
|
|
57
|
+
emit({ ok: false });
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
emit({ ok: false });
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await main();
|