dexterm-cli 0.0.0-stage → 0.1.1
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/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Apple Speech STT engine (task-1282). On-device SFSpeechRecognizer via a tiny Swift helper compiled
|
|
2
|
+
// on the fly with `swiftc` — no key, no network, streaming partials (the spike's recommended primary
|
|
3
|
+
// engine). The compiled helper is cached under ~/.dexterm/voice-helper; it is rebuilt only when the
|
|
4
|
+
// source hash changes.
|
|
5
|
+
//
|
|
6
|
+
// LIVE CAVEAT (disclosed in the handoff): this path needs macOS Speech + Microphone authorization,
|
|
7
|
+
// which can only be granted at the Mac by a person the first time it runs. available() reports whether
|
|
8
|
+
// swiftc is present and the helper compiles; it cannot assert that TCC authorization will be granted.
|
|
9
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { APPLE_HELPER_SWIFT } from "./apple-helper.swift.js";
|
|
15
|
+
const CACHE_DIR = process.env.DEXTERM_VOICE_HELPER_DIR || join(homedir(), '.dexterm', 'voice-helper');
|
|
16
|
+
const SRC = join(CACHE_DIR, 'recognize.swift');
|
|
17
|
+
const BIN = join(CACHE_DIR, 'recognize');
|
|
18
|
+
const SWIFTC = process.env.DEXTERM_SWIFTC || 'swiftc';
|
|
19
|
+
function which(bin) {
|
|
20
|
+
if (bin.includes('/'))
|
|
21
|
+
return existsSync(bin);
|
|
22
|
+
for (const p of (process.env.PATH ?? '').split(':'))
|
|
23
|
+
if (p && existsSync(join(p, bin)))
|
|
24
|
+
return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** Compile the helper if the cached binary is missing or older than the current source. Returns BIN or null. */
|
|
28
|
+
export function ensureHelper() {
|
|
29
|
+
if (!which(SWIFTC))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
33
|
+
const hash = createHash('sha256').update(APPLE_HELPER_SWIFT).digest('hex');
|
|
34
|
+
const stamp = join(CACHE_DIR, '.srchash');
|
|
35
|
+
const fresh = existsSync(BIN) && existsSync(stamp) && readFileSync(stamp, 'utf8') === hash && statSync(BIN).size > 0;
|
|
36
|
+
if (fresh)
|
|
37
|
+
return BIN;
|
|
38
|
+
writeFileSync(SRC, APPLE_HELPER_SWIFT);
|
|
39
|
+
const r = compileSync(SRC, BIN);
|
|
40
|
+
if (!r)
|
|
41
|
+
return null;
|
|
42
|
+
writeFileSync(stamp, hash);
|
|
43
|
+
return BIN;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function compileSync(src, out) {
|
|
50
|
+
// The compile is a one-time cost (cached by source hash); doing it synchronously keeps ensureHelper
|
|
51
|
+
// simple. It is only reached from available() / listen(), both already async from the caller's view.
|
|
52
|
+
try {
|
|
53
|
+
execFileSync(SWIFTC, ['-O', '-o', out, src], { timeout: 60_000, stdio: 'ignore' });
|
|
54
|
+
return existsSync(out);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class AppleStt {
|
|
61
|
+
name = 'apple';
|
|
62
|
+
label = 'Apple Speech (on-device, no key)';
|
|
63
|
+
async available() {
|
|
64
|
+
if (process.platform !== 'darwin')
|
|
65
|
+
return false;
|
|
66
|
+
if (!which(SWIFTC))
|
|
67
|
+
return false;
|
|
68
|
+
return ensureHelper() !== null;
|
|
69
|
+
}
|
|
70
|
+
async listen() {
|
|
71
|
+
const bin = ensureHelper();
|
|
72
|
+
if (!bin)
|
|
73
|
+
throw new Error('Apple Speech helper unavailable (need swiftc)');
|
|
74
|
+
const t0 = Date.now();
|
|
75
|
+
const child = spawn(bin, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
76
|
+
let partial = '';
|
|
77
|
+
let firstPartialMs = null;
|
|
78
|
+
let finalText = null;
|
|
79
|
+
let finalConfidence = 1;
|
|
80
|
+
let err = '';
|
|
81
|
+
let buf = '';
|
|
82
|
+
let readySeen = false;
|
|
83
|
+
let readyResolve = null;
|
|
84
|
+
const ready = new Promise((r) => { readyResolve = r; });
|
|
85
|
+
child.stdout?.setEncoding('utf8');
|
|
86
|
+
child.stdout?.on('data', (d) => {
|
|
87
|
+
buf += d;
|
|
88
|
+
let i;
|
|
89
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
90
|
+
const line = buf.slice(0, i);
|
|
91
|
+
buf = buf.slice(i + 1);
|
|
92
|
+
if (!line.trim())
|
|
93
|
+
continue;
|
|
94
|
+
let ev;
|
|
95
|
+
try {
|
|
96
|
+
ev = JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ev.type === 'partial' && typeof ev.text === 'string') {
|
|
102
|
+
partial = ev.text;
|
|
103
|
+
if (firstPartialMs === null && ev.text)
|
|
104
|
+
firstPartialMs = Date.now() - t0;
|
|
105
|
+
}
|
|
106
|
+
else if (ev.type === 'final') {
|
|
107
|
+
finalText = String(ev.text ?? partial);
|
|
108
|
+
if (typeof ev.confidence === 'number')
|
|
109
|
+
finalConfidence = ev.confidence;
|
|
110
|
+
}
|
|
111
|
+
else if (ev.type === 'ready') {
|
|
112
|
+
readySeen = true;
|
|
113
|
+
readyResolve?.();
|
|
114
|
+
}
|
|
115
|
+
else if (ev.type === 'error') {
|
|
116
|
+
err = String(ev.message ?? 'speech error');
|
|
117
|
+
readyResolve?.();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
child.stderr?.on('data', (d) => { err += d.toString(); });
|
|
122
|
+
let closed = false;
|
|
123
|
+
let spawnError = null;
|
|
124
|
+
const settled = new Promise((resolve) => {
|
|
125
|
+
child.on('close', () => { closed = true; readyResolve?.(); resolve(); });
|
|
126
|
+
// A failed spawn (EACCES / ENOENT / EAGAIN) must never become an uncaughtException that takes the
|
|
127
|
+
// whole terminal down (task-1339, re-verify N5): record it so stop() rejects cleanly instead.
|
|
128
|
+
child.on('error', (e) => { spawnError = e; closed = true; if (!err)
|
|
129
|
+
err = `mic helper failed to start: ${e.message}`; readyResolve?.(); resolve(); });
|
|
130
|
+
});
|
|
131
|
+
// Spawn failures surface on the next tick: give them one turn so listen() rejects (the controller
|
|
132
|
+
// then says "mic/STT could not start") rather than reporting a mic that was never open.
|
|
133
|
+
await new Promise((r) => setImmediate(r));
|
|
134
|
+
if (spawnError)
|
|
135
|
+
throw new Error(err || `mic helper failed to start: ${spawnError.message}`);
|
|
136
|
+
// Force-terminate the helper so the mic process can never outlive the session (task-1282 rework D5).
|
|
137
|
+
const kill = () => {
|
|
138
|
+
if (closed)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
child.kill('SIGINT');
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
// If SIGINT did not land promptly, escalate so the OS mic indicator can never be left on.
|
|
145
|
+
setTimeout(() => { if (!closed) {
|
|
146
|
+
try {
|
|
147
|
+
child.kill('SIGKILL');
|
|
148
|
+
}
|
|
149
|
+
catch { /* ignore */ }
|
|
150
|
+
} }, 500).unref?.();
|
|
151
|
+
};
|
|
152
|
+
// The helper authorizes, loads the on-device model and opens the audio engine before it emits
|
|
153
|
+
// `ready`. Reporting "listening" earlier let the user talk (and press stop) into a mic that was not
|
|
154
|
+
// open yet: an empty transcript and a silent no-op. Wait for it (bounded; first run after a
|
|
155
|
+
// permission prompt can take a few seconds).
|
|
156
|
+
await Promise.race([ready, new Promise((r) => setTimeout(r, 10_000))]);
|
|
157
|
+
if (closed || !readySeen) {
|
|
158
|
+
kill();
|
|
159
|
+
throw new Error(err || (closed ? 'mic helper exited before it was ready' : 'mic helper did not become ready in 10 s (check System Settings > Privacy > Microphone / Speech Recognition for your terminal)'));
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
stop: async () => {
|
|
163
|
+
try {
|
|
164
|
+
child.stdin?.write('stop\n');
|
|
165
|
+
child.stdin?.end();
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
await Promise.race([settled, new Promise((r) => setTimeout(r, 5000))]);
|
|
169
|
+
if (!closed)
|
|
170
|
+
kill(); // the helper ignored `stop` within 3 s: kill it rather than orphan the mic
|
|
171
|
+
const text = (finalText ?? partial).trim();
|
|
172
|
+
if (!text && err)
|
|
173
|
+
throw new Error(err);
|
|
174
|
+
return { text, confidence: text ? finalConfidence : 0, firstPartialMs, speakerCount: 1 };
|
|
175
|
+
},
|
|
176
|
+
cancel: () => { kill(); },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// An STT engine that captures no audio — listen()/stop() replay a scripted transcript. It powers the
|
|
2
|
+
// headless tests and the latency harness, and the `--text`/`--script` demo, so the whole pipeline is
|
|
3
|
+
// exercisable with no mic and no model.
|
|
4
|
+
export class FakeStt {
|
|
5
|
+
name = 'fake';
|
|
6
|
+
label = 'fake (scripted, for tests)';
|
|
7
|
+
queue;
|
|
8
|
+
deflt;
|
|
9
|
+
/** `scripts` are consumed one per listen(); once exhausted, `fallback` is reused. */
|
|
10
|
+
constructor(scripts = [], fallback = { text: '' }) {
|
|
11
|
+
this.queue = scripts.slice();
|
|
12
|
+
this.deflt = fallback;
|
|
13
|
+
}
|
|
14
|
+
async available() { return true; }
|
|
15
|
+
async listen() {
|
|
16
|
+
const s = this.queue.shift() ?? this.deflt;
|
|
17
|
+
let settled = null;
|
|
18
|
+
const resolveResult = () => (settled ??= {
|
|
19
|
+
text: s.text,
|
|
20
|
+
confidence: s.confidence ?? 1,
|
|
21
|
+
lowConfTokens: s.lowConfTokens ?? [],
|
|
22
|
+
firstPartialMs: s.firstPartialMs ?? null,
|
|
23
|
+
speakerCount: s.speakerCount ?? 1,
|
|
24
|
+
echoFlagged: s.echoFlagged ?? false,
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
stop: () => new Promise((resolve) => {
|
|
28
|
+
const delay = Math.max(0, s.transcribeMs ?? 0);
|
|
29
|
+
setTimeout(() => resolve(resolveResult()), delay);
|
|
30
|
+
}),
|
|
31
|
+
cancel: () => { settled = resolveResult(); },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// whisper.cpp STT engine (task-1282). Local, no key, no network once the model is downloaded. This is
|
|
2
|
+
// the portable fallback when Apple Speech is unavailable. The spike measured `base.en` at ~3.5–4.8 s
|
|
3
|
+
// batch on a CPU-only Intel Mac with good accuracy; `large-v3-turbo` was unusable (55–93 s) — so this
|
|
4
|
+
// engine defaults to `base.en` and the doctor steers the user to it, never to a turbo model on CPU.
|
|
5
|
+
//
|
|
6
|
+
// Capture: `sox`/`rec` or `ffmpeg -f avfoundation` writes a 16 kHz mono WAV while the key is held;
|
|
7
|
+
// stop() finalises the WAV and runs whisper-cli on it. The WAV lives in the OS temp dir and is
|
|
8
|
+
// DELETED immediately after transcription unless DEXTERM_VOICE_RECORD=1 — nothing is kept by default.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { existsSync, rmSync, mkdtempSync } from 'node:fs';
|
|
11
|
+
import { homedir, tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
export const WHISPER_BIN = process.env.DEXTERM_WHISPER_BIN || 'whisper-cli';
|
|
14
|
+
/** Default model — base.en, the spike's accurate+usable choice. Never large-v3-turbo on CPU. */
|
|
15
|
+
export const WHISPER_MODEL = process.env.DEXTERM_WHISPER_MODEL || join(homedir(), '.dexterm', 'models', 'ggml-base.en.bin');
|
|
16
|
+
/** Where the user is told to put the model (doctor fix line). */
|
|
17
|
+
export const WHISPER_MODEL_DIR = join(homedir(), '.dexterm', 'models');
|
|
18
|
+
/** Pick a mic-capture tool: sox (`rec`) first, then ffmpeg's avfoundation input. */
|
|
19
|
+
function pickRecorder() {
|
|
20
|
+
if (which('rec'))
|
|
21
|
+
return { cmd: 'rec', args: (wav) => ['-q', '-c', '1', '-r', '16000', '-b', '16', wav], stopSignal: 'SIGINT' };
|
|
22
|
+
if (which('sox'))
|
|
23
|
+
return { cmd: 'sox', args: (wav) => ['-q', '-d', '-c', '1', '-r', '16000', '-b', '16', wav], stopSignal: 'SIGINT' };
|
|
24
|
+
if (which('ffmpeg')) {
|
|
25
|
+
const dev = process.env.DEXTERM_FFMPEG_MIC || ':default';
|
|
26
|
+
return { cmd: 'ffmpeg', args: (wav) => ['-hide_banner', '-loglevel', 'error', '-f', 'avfoundation', '-i', dev, '-ac', '1', '-ar', '16000', '-y', wav], stopSignal: 'SIGINT' };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** Synchronous PATH check without spawning (cheap; used by available() and pickRecorder). */
|
|
31
|
+
function which(bin) {
|
|
32
|
+
const paths = (process.env.PATH ?? '').split(':');
|
|
33
|
+
for (const p of paths) {
|
|
34
|
+
if (p && existsSync(join(p, bin)))
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export class WhisperStt {
|
|
40
|
+
name = 'whisper';
|
|
41
|
+
label = 'whisper.cpp base.en (local, no key)';
|
|
42
|
+
model;
|
|
43
|
+
recordToDisk = process.env.DEXTERM_VOICE_RECORD === '1';
|
|
44
|
+
constructor(model = WHISPER_MODEL) { this.model = model; }
|
|
45
|
+
async available() {
|
|
46
|
+
return (which(WHISPER_BIN) || existsSync(WHISPER_BIN)) && existsSync(this.model) && pickRecorder() !== null;
|
|
47
|
+
}
|
|
48
|
+
async listen() {
|
|
49
|
+
const rec = pickRecorder();
|
|
50
|
+
if (!rec)
|
|
51
|
+
throw new Error('no mic capture tool (install sox or ffmpeg)');
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), 'dexterm-voice-'));
|
|
53
|
+
const wav = join(dir, 'capture.wav');
|
|
54
|
+
const t0 = Date.now();
|
|
55
|
+
const recorder = spawn(rec.cmd, rec.args(wav), { stdio: 'ignore' });
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
if (this.recordToDisk)
|
|
59
|
+
return; // explicit opt-in to keep the audio
|
|
60
|
+
try {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
catch { /* ignore */ }
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
stop: async () => {
|
|
67
|
+
// Ask the recorder to finalise the WAV header, then wait for it to exit.
|
|
68
|
+
try {
|
|
69
|
+
recorder.kill(rec.stopSignal);
|
|
70
|
+
}
|
|
71
|
+
catch { /* already gone */ }
|
|
72
|
+
await new Promise((r) => { recorder.on('close', () => r()); setTimeout(r, 2000); });
|
|
73
|
+
if (cancelled || !existsSync(wav)) {
|
|
74
|
+
cleanup();
|
|
75
|
+
return { text: '', confidence: 0 };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const text = await transcribe(wav, this.model);
|
|
79
|
+
return { text, confidence: text ? 0.9 : 0, firstPartialMs: null, speakerCount: 1 };
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
cleanup();
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
cancel: () => { cancelled = true; try {
|
|
86
|
+
recorder.kill('SIGKILL');
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ } cleanup(); },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Run whisper-cli on a finished WAV and return the transcript text. Rejects on a non-zero exit. */
|
|
93
|
+
function transcribe(wav, model) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const p = spawn(WHISPER_BIN, ['-m', model, '-f', wav, '-nt', '-l', 'en', '-t', '4'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
96
|
+
let out = '', err = '';
|
|
97
|
+
p.stdout.on('data', (d) => (out += d));
|
|
98
|
+
p.stderr.on('data', (d) => (err += d));
|
|
99
|
+
p.on('error', reject);
|
|
100
|
+
p.on('close', (code) => code === 0 ? resolve(out.replace(/\s+/g, ' ').trim()) : reject(new Error(`whisper exit ${code}: ${err.slice(-200)}`)));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Turn an analyst answer into its spoken form. It used to read only the FIRST ~2 sentences; since 2026-10-12
|
|
2
|
+
// the whole answer is read, so the default is now every sentence, capped for sanity (a long-form dossier). The analyst's answer arrives either as a verdict + summary lines (structured
|
|
3
|
+
// skills) or as free prose (chat follow-ups); this flattens both and trims to a speakable length.
|
|
4
|
+
//
|
|
5
|
+
// Pure and dependency-free so it is unit-tested headless.
|
|
6
|
+
const MAX_CHARS = 1800;
|
|
7
|
+
/** Collapse whitespace and strip markup/markdown that would be read aloud as noise. */
|
|
8
|
+
function tidy(s) {
|
|
9
|
+
return s
|
|
10
|
+
.replace(/[*_`#>]/g, ' ') // markdown emphasis / headings / code / quotes
|
|
11
|
+
.replace(/\[[^\]]*\]\([^)]*\)/g, (m) => m.replace(/\]\([^)]*\)/, ']').replace(/[[\]]/g, '')) // [label](url) -> label
|
|
12
|
+
.replace(/https?:\/\/\S+/g, 'a link')
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
/** Split into sentences on ., !, ? boundaries, keeping it simple and robust. */
|
|
17
|
+
export function splitSentences(text) {
|
|
18
|
+
const t = tidy(text);
|
|
19
|
+
if (!t)
|
|
20
|
+
return [];
|
|
21
|
+
const parts = t.match(/[^.!?]+[.!?]+|[^.!?]+$/g);
|
|
22
|
+
return (parts ?? [t]).map((s) => s.trim()).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the spoken summary. A verdict (e.g. "AVOID", "LARP") is spoken first as its own short clause,
|
|
26
|
+
* then the whole answer (every summary/prose sentence unless `maxSentences` limits it). Capped at MAX_CHARS.
|
|
27
|
+
*/
|
|
28
|
+
export function spokenSummary(src, maxSentences = Infinity) {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
const verdict = (src.verdict ?? '').trim();
|
|
31
|
+
if (verdict)
|
|
32
|
+
chunks.push(`Verdict: ${verdict}.`);
|
|
33
|
+
const body = (src.summary && src.summary.length ? src.summary.join('. ') : (src.text ?? '')).trim();
|
|
34
|
+
const sentences = splitSentences(body).slice(0, maxSentences);
|
|
35
|
+
chunks.push(...sentences);
|
|
36
|
+
let out = tidy(chunks.join(' '));
|
|
37
|
+
if (out.length > MAX_CHARS) {
|
|
38
|
+
// Cut at the last sentence boundary that fits, else hard-trim.
|
|
39
|
+
const cut = out.slice(0, MAX_CHARS);
|
|
40
|
+
const lastStop = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '));
|
|
41
|
+
out = (lastStop > 80 ? cut.slice(0, lastStop + 1) : cut.trimEnd() + '…');
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A TTS engine that produces no audio — it records what would have been spoken and resolves at once.
|
|
2
|
+
// Used by the headless tests and the latency harness so speech-out is deterministic and silent.
|
|
3
|
+
export class FakeTts {
|
|
4
|
+
name = 'fake';
|
|
5
|
+
label = 'fake (silent, for tests)';
|
|
6
|
+
/** Every text passed to speak(), in order, for assertions. */
|
|
7
|
+
spoken = [];
|
|
8
|
+
/** How many times stop() interrupted a live utterance. */
|
|
9
|
+
stops = 0;
|
|
10
|
+
voices;
|
|
11
|
+
constructor(voices = ['Samantha', 'Alex', 'Daniel']) { this.voices = voices; }
|
|
12
|
+
async available() { return true; }
|
|
13
|
+
async listVoices() { return this.voices.slice(); }
|
|
14
|
+
speak(text, opts = {}) {
|
|
15
|
+
this.spoken.push(text);
|
|
16
|
+
let stopped = false;
|
|
17
|
+
let resolve;
|
|
18
|
+
const done = new Promise((r) => { resolve = r; });
|
|
19
|
+
// Fire first-audio on the next tick so latency timing sees a realistic (tiny) gap, then finish.
|
|
20
|
+
const t = setTimeout(() => { if (!stopped) {
|
|
21
|
+
opts.onFirstAudio?.();
|
|
22
|
+
resolve();
|
|
23
|
+
} }, 0);
|
|
24
|
+
return {
|
|
25
|
+
done,
|
|
26
|
+
stop: () => { if (stopped)
|
|
27
|
+
return; stopped = true; clearTimeout(t); this.stops++; resolve(); },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// macOS `say` TTS engine (task-1282). `say` ships with macOS, needs no key and no network, so it is
|
|
2
|
+
// the free/portable default (SPEC: macOS say first). It streams to the audio device as it synthesises,
|
|
3
|
+
// so time-to-first-audio is close to process spawn. speak() is interruptible via stop() (SIGTERM to
|
|
4
|
+
// the child), which is how a key or the user starting to talk cuts a reply short.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
export const SAY_BIN = process.env.DEXTERM_SAY_BIN || 'say';
|
|
8
|
+
export class SayTts {
|
|
9
|
+
name = 'say';
|
|
10
|
+
label = 'macOS say (built-in, no key)';
|
|
11
|
+
defaultVoice;
|
|
12
|
+
constructor(defaultVoice) { this.defaultVoice = defaultVoice; }
|
|
13
|
+
async available() {
|
|
14
|
+
if (process.platform !== 'darwin')
|
|
15
|
+
return false;
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
try {
|
|
18
|
+
const child = execFile(SAY_BIN, ['-v', '?'], { timeout: 4000 }, (err) => resolve(!err || err.code !== 'ENOENT'));
|
|
19
|
+
child.on('error', () => resolve(false));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
resolve(false);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Parse `say -v '?'`: each line is "Name en_US # sample text". */
|
|
27
|
+
async listVoices() {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
try {
|
|
30
|
+
execFile(SAY_BIN, ['-v', '?'], { timeout: 5000, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
31
|
+
if (err || !stdout) {
|
|
32
|
+
resolve([]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const names = [];
|
|
36
|
+
for (const line of stdout.split('\n')) {
|
|
37
|
+
const m = line.match(/^(.+?)\s{2,}[a-z]{2}(?:[_-][A-Za-z]+)?\s/);
|
|
38
|
+
if (m)
|
|
39
|
+
names.push(m[1].trim());
|
|
40
|
+
}
|
|
41
|
+
resolve(names);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
resolve([]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
speak(text, opts = {}) {
|
|
50
|
+
const voice = opts.voice ?? this.defaultVoice;
|
|
51
|
+
const rate = opts.rate ?? 190;
|
|
52
|
+
const args = [];
|
|
53
|
+
if (voice)
|
|
54
|
+
args.push('-v', voice);
|
|
55
|
+
args.push('-r', String(rate), text);
|
|
56
|
+
let child = null;
|
|
57
|
+
let stopped = false;
|
|
58
|
+
let resolve;
|
|
59
|
+
const done = new Promise((r) => { resolve = r; });
|
|
60
|
+
try {
|
|
61
|
+
child = spawn(SAY_BIN, args, { stdio: 'ignore' });
|
|
62
|
+
// `say` has no first-sample hook; the spawn event is the closest honest lower bound and is what
|
|
63
|
+
// the latency table reports as "first spoken word" for this engine.
|
|
64
|
+
child.on('spawn', () => { if (!stopped)
|
|
65
|
+
opts.onFirstAudio?.(); });
|
|
66
|
+
child.on('error', () => { if (!stopped) {
|
|
67
|
+
stopped = true;
|
|
68
|
+
resolve();
|
|
69
|
+
} });
|
|
70
|
+
child.on('close', () => { if (!stopped) {
|
|
71
|
+
stopped = true;
|
|
72
|
+
resolve();
|
|
73
|
+
} });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// No `say` on this machine: resolve immediately so the caller degrades to text.
|
|
77
|
+
queueMicrotask(() => { if (!stopped) {
|
|
78
|
+
stopped = true;
|
|
79
|
+
resolve();
|
|
80
|
+
} });
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
done,
|
|
84
|
+
stop: () => {
|
|
85
|
+
if (stopped)
|
|
86
|
+
return;
|
|
87
|
+
stopped = true;
|
|
88
|
+
try {
|
|
89
|
+
child?.kill('SIGTERM');
|
|
90
|
+
}
|
|
91
|
+
catch { /* already gone */ }
|
|
92
|
+
resolve();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dexterm-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
|
|
5
|
+
"author": "The dexterm authors",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dexterm": "./dist/packages/cli/bin/dexterm.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/packages/cli/src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node scripts/build-dist.mjs",
|
|
20
|
+
"prepack": "node scripts/build-dist.mjs"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.18"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@dexterm/agents": "0.0.1",
|
|
27
|
+
"@dexterm/custom": "0.0.1",
|
|
28
|
+
"@dexterm/data": "0.0.1",
|
|
29
|
+
"@dexterm/mcp": "0.0.1",
|
|
30
|
+
"@dexterm/packs": "0.0.1",
|
|
31
|
+
"@dexterm/popup": "0.0.1",
|
|
32
|
+
"@dexterm/recorder": "0.0.1",
|
|
33
|
+
"@dexterm/tui": "0.0.1",
|
|
34
|
+
"@dexterm/voice": "0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/dexterm-xyz/dexterm.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://dexterm.xyz",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/dexterm-xyz/dexterm/issues"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/README.md
DELETED