@clearlysh/beehaven 0.8.16
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 +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clearlysh/beehaven",
|
|
3
|
+
"version": "0.8.16",
|
|
4
|
+
"description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"beehaven": "dist/cli.js",
|
|
9
|
+
"clearly": "dist/clearly.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/cli.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"python",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"scripts/",
|
|
18
|
+
"skills"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsx watch src/cli.ts",
|
|
29
|
+
"build": "tsc && node scripts/stamp-build-channel.mjs && node scripts/sync-installed.mjs",
|
|
30
|
+
"start": "node dist/cli.js",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:json-capable": "vitest run src/__tests__/json-capable.test.ts",
|
|
33
|
+
"test:limit-capable": "vitest run src/__tests__/limit-capable.test.ts",
|
|
34
|
+
"test:out-capable": "vitest run src/__tests__/out-capable.test.ts",
|
|
35
|
+
"test:script-citations": "vitest run src/__tests__/script-citations.test.ts",
|
|
36
|
+
"prepublishOnly": "BEEHAVEN_PUBLIC_BUILD=1 BEEHAVEN_NO_SYNC=1 npm run build",
|
|
37
|
+
"sync:skills": "node scripts/sync-skills.mjs",
|
|
38
|
+
"prepack": "node scripts/sync-skills.mjs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.141",
|
|
42
|
+
"bplist-parser": "^0.3.2",
|
|
43
|
+
"chokidar": "4.0.0",
|
|
44
|
+
"fflate": "^0.8.3",
|
|
45
|
+
"ws": "8.18.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@clearly-sh/beehaven-core": ">=0.3.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@clearly-sh/beehaven-core": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"optionalDependencies": {
|
|
56
|
+
"better-sqlite3": "11.0.0",
|
|
57
|
+
"node-pty": "1.0.0",
|
|
58
|
+
"open": "10.1.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"@types/ws": "^8.5.7",
|
|
63
|
+
"tsx": "^4.19.0",
|
|
64
|
+
"typescript": "^5.6.0",
|
|
65
|
+
"vitest": "^4.0.0"
|
|
66
|
+
},
|
|
67
|
+
"keywords": [
|
|
68
|
+
"beehaven",
|
|
69
|
+
"clearly",
|
|
70
|
+
"cli",
|
|
71
|
+
"ai-agents",
|
|
72
|
+
"agent-workspace",
|
|
73
|
+
"claude-code",
|
|
74
|
+
"codex",
|
|
75
|
+
"agent-tools",
|
|
76
|
+
"design-canvas",
|
|
77
|
+
"mcp"
|
|
78
|
+
],
|
|
79
|
+
"license": "MIT",
|
|
80
|
+
"homepage": "https://www.clearly.sh/docs/cli",
|
|
81
|
+
"bugs": {
|
|
82
|
+
"url": "https://github.com/clearly-sh/clearly/issues"
|
|
83
|
+
},
|
|
84
|
+
"repository": {
|
|
85
|
+
"type": "git",
|
|
86
|
+
"url": "https://github.com/clearly-sh/clearly.git"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
qwen3_tts.py — the MLX side of the `qwen3-mlx` TTS provider for `beehaven speak`.
|
|
4
|
+
|
|
5
|
+
WHY THIS EXISTS. Every other voice path `speak` can take is a network call
|
|
6
|
+
(cloud Kokoro, ElevenLabs) or a canned system voice (macOS `say`). None of them
|
|
7
|
+
can speak in *your* voice, and the cloud ones need a relay + a live token. This
|
|
8
|
+
script is the on-device path: Alibaba's Qwen3-TTS running under Apple's MLX,
|
|
9
|
+
which does 3-second voice cloning locally on Apple Silicon. No relay, no API
|
|
10
|
+
key, no audio leaves the machine.
|
|
11
|
+
|
|
12
|
+
CONTRACT WITH THE NODE SIDE (`src/tts-qwen3.ts`).
|
|
13
|
+
- One JSON request object on **stdin**, one JSON response object on **stdout**.
|
|
14
|
+
- stdout carries NOTHING ELSE. mlx-audio prints progress, tokens/sec, and
|
|
15
|
+
"✅ Audio successfully generated" to stdout unconditionally, so every call
|
|
16
|
+
into it is wrapped in `redirect_stdout(sys.stderr)`. The Node side parses
|
|
17
|
+
stdout strictly, so a single stray print would corrupt the response.
|
|
18
|
+
- Failures come back as `{"ok": false, "error": "..."}` with exit code 1 —
|
|
19
|
+
the caller gets a message, never a Python traceback in the user's terminal.
|
|
20
|
+
|
|
21
|
+
COMMANDS
|
|
22
|
+
probe {} -> versions + whether Metal is usable
|
|
23
|
+
transcribe {audio} -> {text} (whisper, clone-time only)
|
|
24
|
+
speak {text, out, ...} -> {path, ...} (synthesis)
|
|
25
|
+
|
|
26
|
+
WHY TRANSCRIBE IS A SEPARATE COMMAND. Qwen3-TTS clones via in-context learning:
|
|
27
|
+
it needs the reference audio AND a transcript of it. mlx-audio will happily
|
|
28
|
+
auto-transcribe at synthesis time, but that loads whisper-large-v3-turbo
|
|
29
|
+
(~1.6 GB) on *every* utterance. We transcribe ONCE when the voice is cloned,
|
|
30
|
+
persist the transcript in the voice profile, and pass `ref_text` on every
|
|
31
|
+
subsequent `speak` — so whisper never loads again and the profile stays a
|
|
32
|
+
self-contained, portable description of the voice.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
40
|
+
import wave
|
|
41
|
+
from contextlib import redirect_stdout
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
# Model families. Voice CLONING requires a `-Base` checkpoint: CustomVoice is
|
|
45
|
+
# fixed predefined speakers and VoiceDesign builds a voice from a text
|
|
46
|
+
# description — neither accepts a reference recording.
|
|
47
|
+
DEFAULT_MODEL = "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-4bit"
|
|
48
|
+
DEFAULT_STT_MODEL = "mlx-community/whisper-large-v3-turbo-asr-fp16"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _fail(msg: str) -> None:
|
|
52
|
+
"""Emit the error envelope on stdout and leave with a non-zero status."""
|
|
53
|
+
json.dump({"ok": False, "error": str(msg)}, sys.stdout)
|
|
54
|
+
sys.stdout.write("\n")
|
|
55
|
+
sys.stdout.flush()
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ok(payload: dict) -> None:
|
|
60
|
+
json.dump({"ok": True, **payload}, sys.stdout)
|
|
61
|
+
sys.stdout.write("\n")
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_probe(_req: dict) -> dict:
|
|
66
|
+
import importlib.metadata as md
|
|
67
|
+
|
|
68
|
+
import mlx.core as mx
|
|
69
|
+
|
|
70
|
+
def _ver(pkg: str) -> str:
|
|
71
|
+
try:
|
|
72
|
+
return md.version(pkg)
|
|
73
|
+
except Exception:
|
|
74
|
+
return "?"
|
|
75
|
+
|
|
76
|
+
# Touching the default device is the cheapest proof Metal actually
|
|
77
|
+
# initializes — an x86 Python under Rosetta imports mlx fine and then dies
|
|
78
|
+
# at the first kernel launch.
|
|
79
|
+
device = str(mx.default_device())
|
|
80
|
+
return {
|
|
81
|
+
"python": sys.version.split()[0],
|
|
82
|
+
"mlxAudio": _ver("mlx-audio"),
|
|
83
|
+
"mlx": _ver("mlx"),
|
|
84
|
+
"device": device,
|
|
85
|
+
"defaultModel": DEFAULT_MODEL,
|
|
86
|
+
"sttModel": DEFAULT_STT_MODEL,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_transcribe(req: dict) -> dict:
|
|
91
|
+
audio = req.get("audio")
|
|
92
|
+
if not audio:
|
|
93
|
+
raise ValueError("transcribe requires `audio`")
|
|
94
|
+
audio_path = Path(audio).expanduser()
|
|
95
|
+
if not audio_path.exists():
|
|
96
|
+
raise FileNotFoundError(f"reference audio not found: {audio_path}")
|
|
97
|
+
|
|
98
|
+
model_id = req.get("sttModel") or DEFAULT_STT_MODEL
|
|
99
|
+
started = time.time()
|
|
100
|
+
with redirect_stdout(sys.stderr):
|
|
101
|
+
from mlx_audio.stt import load as load_stt
|
|
102
|
+
|
|
103
|
+
stt = load_stt(model_id)
|
|
104
|
+
# whisper takes the path directly and handles decode + resample, so we
|
|
105
|
+
# don't have to guess the TTS model's sample rate here.
|
|
106
|
+
result = stt.generate(str(audio_path), language=req.get("language"))
|
|
107
|
+
text = (getattr(result, "text", "") or "").strip()
|
|
108
|
+
if not text:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
"transcription came back empty — is the reference audio silent or music-only?"
|
|
111
|
+
)
|
|
112
|
+
return {"text": text, "model": model_id, "elapsedSec": round(time.time() - started, 2)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cmd_speak(req: dict) -> dict:
|
|
116
|
+
text = (req.get("text") or "").strip()
|
|
117
|
+
if not text:
|
|
118
|
+
raise ValueError("speak requires `text`")
|
|
119
|
+
out = req.get("out")
|
|
120
|
+
if not out:
|
|
121
|
+
raise ValueError("speak requires `out` (destination .wav path)")
|
|
122
|
+
|
|
123
|
+
out_path = Path(out).expanduser()
|
|
124
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
ref_audio = req.get("refAudio")
|
|
127
|
+
ref_text = req.get("refText")
|
|
128
|
+
if ref_audio:
|
|
129
|
+
ref_audio = str(Path(ref_audio).expanduser())
|
|
130
|
+
if not Path(ref_audio).exists():
|
|
131
|
+
raise FileNotFoundError(f"reference audio not found: {ref_audio}")
|
|
132
|
+
|
|
133
|
+
kwargs = {
|
|
134
|
+
"text": text,
|
|
135
|
+
"model": req.get("model") or DEFAULT_MODEL,
|
|
136
|
+
# `join_audio` makes mlx-audio write exactly ONE file at
|
|
137
|
+
# "<file_prefix>.<audio_format>" instead of one file per sentence
|
|
138
|
+
# segment — that predictable name is how we find the result.
|
|
139
|
+
"join_audio": True,
|
|
140
|
+
"output_path": str(out_path.parent),
|
|
141
|
+
"file_prefix": out_path.stem,
|
|
142
|
+
"audio_format": out_path.suffix.lstrip(".") or "wav",
|
|
143
|
+
"verbose": False,
|
|
144
|
+
"play": False,
|
|
145
|
+
"temperature": float(req.get("temperature", 0.7)),
|
|
146
|
+
"speed": float(req.get("speed", 1.0)),
|
|
147
|
+
"lang_code": req.get("langCode") or "en",
|
|
148
|
+
}
|
|
149
|
+
if ref_audio:
|
|
150
|
+
kwargs["ref_audio"] = ref_audio
|
|
151
|
+
# A Base checkpoint in ICL mode takes its identity entirely from the
|
|
152
|
+
# reference, so blank the speaker — otherwise mlx-audio's `af_heart`
|
|
153
|
+
# default (a Kokoro speaker name) rides along and muddies the logs.
|
|
154
|
+
kwargs["voice"] = None
|
|
155
|
+
# Passing ref_text keeps mlx-audio from loading whisper. When a caller
|
|
156
|
+
# skips it (bare `--ref-audio` with no cached profile) mlx-audio falls
|
|
157
|
+
# back to transcribing, which is slow but correct.
|
|
158
|
+
if ref_text:
|
|
159
|
+
kwargs["ref_text"] = ref_text
|
|
160
|
+
else:
|
|
161
|
+
kwargs["stt_model"] = req.get("sttModel") or DEFAULT_STT_MODEL
|
|
162
|
+
elif req.get("voice"):
|
|
163
|
+
# No reference recording: a predefined speaker (CustomVoice checkpoints).
|
|
164
|
+
kwargs["voice"] = req["voice"]
|
|
165
|
+
if req.get("instruct"):
|
|
166
|
+
kwargs["instruct"] = req["instruct"]
|
|
167
|
+
if req.get("maxTokens"):
|
|
168
|
+
kwargs["max_tokens"] = int(req["maxTokens"])
|
|
169
|
+
|
|
170
|
+
started = time.time()
|
|
171
|
+
with redirect_stdout(sys.stderr):
|
|
172
|
+
from mlx_audio.tts.generate import generate_audio
|
|
173
|
+
|
|
174
|
+
generate_audio(**kwargs)
|
|
175
|
+
|
|
176
|
+
if not out_path.exists():
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"synthesis reported success but produced no file at {out_path}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
duration = None
|
|
182
|
+
try:
|
|
183
|
+
with wave.open(str(out_path), "rb") as wf:
|
|
184
|
+
duration = round(wf.getnframes() / float(wf.getframerate()), 2)
|
|
185
|
+
except Exception:
|
|
186
|
+
# Non-wav container (flac/mp3) — duration is a nicety, not a contract.
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
elapsed = round(time.time() - started, 2)
|
|
190
|
+
return {
|
|
191
|
+
"path": str(out_path),
|
|
192
|
+
"bytes": out_path.stat().st_size,
|
|
193
|
+
"durationSec": duration,
|
|
194
|
+
"elapsedSec": elapsed,
|
|
195
|
+
# <1.0 means we generated faster than real time — the number that
|
|
196
|
+
# decides whether this provider is usable interactively.
|
|
197
|
+
"realTimeFactor": round(elapsed / duration, 2) if duration else None,
|
|
198
|
+
"model": kwargs["model"],
|
|
199
|
+
"cloned": bool(ref_audio),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
COMMANDS = {"probe": cmd_probe, "transcribe": cmd_transcribe, "speak": cmd_speak}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main() -> None:
|
|
207
|
+
command = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
208
|
+
handler = COMMANDS.get(command)
|
|
209
|
+
if handler is None:
|
|
210
|
+
_fail(f"unknown command {command!r} — expected one of {', '.join(COMMANDS)}")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
raw = sys.stdin.read().strip()
|
|
214
|
+
try:
|
|
215
|
+
req = json.loads(raw) if raw else {}
|
|
216
|
+
except json.JSONDecodeError as exc:
|
|
217
|
+
_fail(f"invalid JSON request: {exc}")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
_ok(handler(req))
|
|
222
|
+
except Exception as exc: # noqa: BLE001 — the envelope IS the error channel
|
|
223
|
+
import traceback
|
|
224
|
+
|
|
225
|
+
traceback.print_exc(file=sys.stderr)
|
|
226
|
+
_fail(f"{type(exc).__name__}: {exc}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postbuild — declare, in the emitted bundle, whether this is a PUBLIC build.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ WHAT THIS DECIDES. `PUBLIC_BUILD` is what makes `HIVE_DEV_MODE=1` inert. Developer scope
|
|
6
|
+
* mints a second IPC token that unlocks `get-token` (the user's Firebase ID token), `shell`, `sql`,
|
|
7
|
+
* `marketplace:install`, the `admin:*` proxies, and — on the relay path — `bash` /
|
|
8
|
+
* `workspace-exec`, i.e. arbitrary local command execution. None of that belongs in a build handed
|
|
9
|
+
* to `curl … | sh` users.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ IT REWRITES `dist/`, NEVER `src/`. Stamping the source would leave the working tree dirty after
|
|
12
|
+
* a publish and, far worse, could be committed — shipping a checkout that silently cannot enable
|
|
13
|
+
* developer mode, with no error to explain it.
|
|
14
|
+
*
|
|
15
|
+
* ⚠ THE DEFAULT IS NOT PUBLIC. An ordinary `npm run build` writes `false`, so a checkout behaves as
|
|
16
|
+
* it always has. `scripts/publish-beehaven-cli.sh` sets `BEEHAVEN_PUBLIC_BUILD=1`. Defaulting the
|
|
17
|
+
* other way would mean one forgotten flag ships a developer build to everyone, silently.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ IT FAILS THE BUILD RATHER THAN WARNING. A stamp that quietly did nothing is exactly the shape
|
|
20
|
+
* of bug this exists to prevent: the tarball would look fine and carry the capability.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
/**
|
|
29
|
+
* ⚠ `BEEHAVEN_STAMP_FILE` IS A TEST SEAM, and it exists because the refusal below is the script's
|
|
30
|
+
* whole safety property — an unstamped build looks perfect and carries the wrong answer. The only
|
|
31
|
+
* way to prove the refusal fires is to run this script against a deliberately-broken file, and
|
|
32
|
+
* doing that to the real `dist/` would break every other test that execs the built CLI. Unset in
|
|
33
|
+
* every real build, so the default path is unchanged.
|
|
34
|
+
*/
|
|
35
|
+
const FILE = process.env.BEEHAVEN_STAMP_FILE || join(ROOT, 'dist', 'build-channel.js');
|
|
36
|
+
|
|
37
|
+
const truthy = (v) => ['1', 'true', 'yes', 'on'].includes(String(v ?? '').trim().toLowerCase());
|
|
38
|
+
const isPublic = truthy(process.env.BEEHAVEN_PUBLIC_BUILD);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WHICH BUILD — CBH2-145. A commit and a build time, so `--version` can tell two builds of 0.8.2
|
|
42
|
+
* apart. Before this, the Mac app's bundled `cli.js` and the repo build both said `0.8.2`, differed
|
|
43
|
+
* by 9 KB, and disagreed about which verbs exist.
|
|
44
|
+
*
|
|
45
|
+
* ⚠⚠ GIT IS OPTIONAL, AND THAT IS NOT LAZINESS. A user who unpacks the published tarball and runs
|
|
46
|
+
* `npm run build` has no `.git`, and failing their build to protect a diagnostic field would be a
|
|
47
|
+
* strictly worse outcome than an unstamped sha. The TIMESTAMP has no such dependency, so the
|
|
48
|
+
* "which build" question is answered either way — which is why there are two fields and not one.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ `-dirty` IS SCOPED TO THIS PACKAGE (`git status --porcelain -- <pkgRoot>`). Against the whole
|
|
51
|
+
* monorepo the marker would be on in essentially every dev build and would therefore carry no
|
|
52
|
+
* information. Scoped, it means precisely "the sha does not identify these bytes".
|
|
53
|
+
*
|
|
54
|
+
* ⚠ ITS BOUND, STATED: the CLI also links `@clearly-sh/beehaven-core`, whose sources live outside
|
|
55
|
+
* this path. A core-only change leaves the marker clean. The sha still identifies the CLI sources,
|
|
56
|
+
* which is what this file is emitted from.
|
|
57
|
+
*/
|
|
58
|
+
function gitSha() {
|
|
59
|
+
const git = (args) =>
|
|
60
|
+
execFileSync('git', args, { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
61
|
+
try {
|
|
62
|
+
const sha = git(['rev-parse', '--short=12', 'HEAD']);
|
|
63
|
+
if (!sha) return '';
|
|
64
|
+
const dirty = git(['status', '--porcelain', '--', ROOT]).length > 0;
|
|
65
|
+
return dirty ? `${sha}-dirty` : sha;
|
|
66
|
+
} catch {
|
|
67
|
+
return ''; // no git, no repo, shallow checkout — all legitimate
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sha = gitSha();
|
|
72
|
+
// ⚠ Minute precision, UTC. Seconds add noise to a field a human reads; a timezone adds ambiguity.
|
|
73
|
+
const builtAt = new Date().toISOString().replace(/:\d\d\.\d+Z$/, 'Z');
|
|
74
|
+
|
|
75
|
+
if (!existsSync(FILE)) {
|
|
76
|
+
console.error(`stamp-build-channel: ${FILE} does not exist — did tsc emit? Refusing to continue.`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const src = readFileSync(FILE, 'utf8');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ⚠ EVERY STAMP IS ANCHORED ON THE DECLARATION TSC EMITS, so a rename fails loudly instead of
|
|
84
|
+
* stamping nothing. That refusal is the whole safety property of this script — an unstamped build
|
|
85
|
+
* looks perfect and carries the wrong answer — so a NEW field gets the same treatment rather than a
|
|
86
|
+
* best-effort `replace`.
|
|
87
|
+
*/
|
|
88
|
+
const STAMPS = [
|
|
89
|
+
{ name: 'PUBLIC_BUILD', decl: /export const PUBLIC_BUILD = (?:true|false);/, next: `export const PUBLIC_BUILD = ${isPublic};` },
|
|
90
|
+
{ name: 'BUILD_SHA', decl: /export const BUILD_SHA = '[^']*';/, next: `export const BUILD_SHA = '${sha}';` },
|
|
91
|
+
{ name: 'BUILD_TIME', decl: /export const BUILD_TIME = '[^']*';/, next: `export const BUILD_TIME = '${builtAt}';` },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
let out = src;
|
|
95
|
+
for (const s of STAMPS) {
|
|
96
|
+
if (!s.decl.test(out)) {
|
|
97
|
+
console.error(`stamp-build-channel: could not find the ${s.name} declaration in the emitted `
|
|
98
|
+
+ 'build-channel.js. The export was renamed or the emit shape changed — fix this script rather '
|
|
99
|
+
+ 'than shipping an unstamped build.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
out = out.replace(s.decl, s.next);
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(FILE, out);
|
|
105
|
+
|
|
106
|
+
// ⚠ Read it BACK. The whole point is that the shipped bytes say what we think they say.
|
|
107
|
+
const after = readFileSync(FILE, 'utf8');
|
|
108
|
+
for (const s of STAMPS) {
|
|
109
|
+
if (!after.includes(s.next)) {
|
|
110
|
+
console.error(`stamp-build-channel: the ${s.name} stamp did not take. Refusing to continue.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (isPublic) console.log(' build channel: PUBLIC — Developer Mode is compiled out');
|
|
115
|
+
console.log(` build id: ${sha || '(no git)'} ${builtAt}`);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postbuild — keep the INSTALLED cli in step with the one you just built.
|
|
4
|
+
*
|
|
5
|
+
* ── THE TRAP THIS CLOSES ─────────────────────────────────────────────────────────────
|
|
6
|
+
* `curl … install.sh` writes `~/.local/bin/beehaven`, a wrapper that execs
|
|
7
|
+
* `~/.beehaven/cli/dist/cli.js`. `~/.local/bin` precedes the npm prefix on PATH, so after
|
|
8
|
+
* `npm run build` in this package the binary named `beehaven` is STILL the installed copy —
|
|
9
|
+
* and both report the same version, because the number comes from a package.json neither
|
|
10
|
+
* build bumps on a rebuild.
|
|
11
|
+
*
|
|
12
|
+
* The failure mode is not a broken CLI, it is a LYING one: you fix a bug, rebuild, re-run,
|
|
13
|
+
* observe the old behaviour, and conclude the fix didn't work. That happened during this
|
|
14
|
+
* feature's own development and cost a verified-correct fix an extra debugging cycle.
|
|
15
|
+
*
|
|
16
|
+
* ── WHAT THIS DOES ───────────────────────────────────────────────────────────────────
|
|
17
|
+
* If (and only if) `~/.beehaven/cli` already exists — i.e. this machine has an installed
|
|
18
|
+
* copy — mirror the freshly-built `dist/`, `skills/`, and `package.json` into it. A machine with no
|
|
19
|
+
* install is untouched, so this is a no-op in CI and on a fresh clone.
|
|
20
|
+
*
|
|
21
|
+
* It deliberately does NOT create the install: writing a new CLI into a user's home on the
|
|
22
|
+
* strength of a `npm run build` would be a side effect nobody asked for. It only keeps an
|
|
23
|
+
* EXISTING one honest.
|
|
24
|
+
*
|
|
25
|
+
* Opt out with BEEHAVEN_NO_SYNC=1 (e.g. when you are deliberately testing an older install).
|
|
26
|
+
*/
|
|
27
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join, dirname } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const pkgRoot = join(here, '..');
|
|
35
|
+
const installed = join(homedir(), '.beehaven', 'cli');
|
|
36
|
+
|
|
37
|
+
if (process.env.BEEHAVEN_NO_SYNC === '1') {
|
|
38
|
+
console.log(' postbuild: sync skipped (BEEHAVEN_NO_SYNC=1)');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* ⚠⚠ A THIRD COPY THIS SCRIPT DOES NOT OWN: THE MAC APP'S BUNDLE.
|
|
43
|
+
*
|
|
44
|
+
* The header above describes closing the `~/.local/bin` trap by syncing `~/.beehaven/cli`. That is
|
|
45
|
+
* still right and still incomplete — on a machine with the Mac app installed, the wrapper at
|
|
46
|
+
* `~/.local/bin/beehaven` says `managed-by: clearly-mac-app` and `overwritten on every app launch`,
|
|
47
|
+
* and it execs the app's OWN bundled `cli.js`. So the binary named `beehaven` is neither this build
|
|
48
|
+
* nor the one we just synced.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ IT IS THE EXACT FAILURE THE HEADER NAMES — "you fix a bug, rebuild, re-run, observe the old
|
|
51
|
+
* behaviour, and conclude the fix didn't work" — and it cost a cycle again on 2026-09-01, on a
|
|
52
|
+
* daemon-startup fix that was correct and simply was not the code being run.
|
|
53
|
+
*
|
|
54
|
+
* ⚠ WE WARN RATHER THAN WRITE. Copying into `/Applications` on every build is invasive, needs
|
|
55
|
+
* permissions this script should not want, and would be undone by the next app launch anyway. What
|
|
56
|
+
* the reader needs is to know which binary they are about to test.
|
|
57
|
+
*/
|
|
58
|
+
const wrapper = join(homedir(), '.local', 'bin', 'beehaven');
|
|
59
|
+
try {
|
|
60
|
+
if (existsSync(wrapper)) {
|
|
61
|
+
const body = readFileSync(wrapper, 'utf8');
|
|
62
|
+
if (/managed-by:\s*clearly-mac-app/.test(body)) {
|
|
63
|
+
console.warn(
|
|
64
|
+
' postbuild: ⚠ `beehaven` on your PATH is the MAC APP\'s bundled CLI, which this build does\n' +
|
|
65
|
+
' not update. Test this build with `node dist/cli.js …`, or relaunch the app\n' +
|
|
66
|
+
' after copying, or the old behaviour will look like your fix failing.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch { /* a warning must never fail a build */ }
|
|
71
|
+
|
|
72
|
+
if (!existsSync(installed)) {
|
|
73
|
+
// No install on this machine — nothing to keep in step. Silent: this is the normal state
|
|
74
|
+
// in CI, and a warning nobody can act on is noise on every build.
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const distSrc = join(pkgRoot, 'dist');
|
|
80
|
+
if (!existsSync(distSrc)) {
|
|
81
|
+
console.error(' postbuild: dist/ missing — did tsc fail? Installed copy left alone.');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
const distDst = join(installed, 'dist');
|
|
85
|
+
rmSync(distDst, { recursive: true, force: true });
|
|
86
|
+
mkdirSync(distDst, { recursive: true });
|
|
87
|
+
cpSync(distSrc, distDst, { recursive: true });
|
|
88
|
+
cpSync(join(pkgRoot, 'package.json'), join(installed, 'package.json'));
|
|
89
|
+
/**
|
|
90
|
+
* THE SKILLS ARE PART OF THE CLI RELEASE, NOT OPTIONAL CACHE.
|
|
91
|
+
*
|
|
92
|
+
* `agent-skills.ts` resolves its source relative to the running installed `dist/`, so syncing
|
|
93
|
+
* only code left `~/.beehaven/cli/skills` on the previous release. `skills install` then compared
|
|
94
|
+
* every agent home against that stale manifest, confidently reported "current", and never
|
|
95
|
+
* delivered documentation fixes made in the same build. Mirror the directory atomically enough
|
|
96
|
+
* for this local postbuild path: remove the old bundle, copy the just-synced hashed bundle, then
|
|
97
|
+
* let the normal installer preserve locally edited destination skills.
|
|
98
|
+
*/
|
|
99
|
+
const skillsSrc = join(pkgRoot, 'skills');
|
|
100
|
+
const skillsDst = join(installed, 'skills');
|
|
101
|
+
if (existsSync(skillsSrc)) {
|
|
102
|
+
rmSync(skillsDst, { recursive: true, force: true });
|
|
103
|
+
cpSync(skillsSrc, skillsDst, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
// `python/` carries the local-TTS bridge; without it a synced install reports an
|
|
106
|
+
// incomplete qwen3-mlx runtime rather than working. Copy it when the source has it.
|
|
107
|
+
const py = join(pkgRoot, 'python');
|
|
108
|
+
if (existsSync(py)) cpSync(py, join(installed, 'python'), { recursive: true });
|
|
109
|
+
/**
|
|
110
|
+
* LINK THE OPTIONAL PEER DEP.
|
|
111
|
+
*
|
|
112
|
+
* `agent-identity-daemon` does a bare `await import('@clearly-sh/beehaven-core/server')`
|
|
113
|
+
* with no fallback. The monorepo resolves that through a workspace symlink; the standalone
|
|
114
|
+
* install does NOT have the package at all — so local agent minting fails there with
|
|
115
|
+
* "Cannot find package", which surfaces to a user as "Could not mint identity. Is the
|
|
116
|
+
* daemon running?" — a message about the wrong thing entirely.
|
|
117
|
+
*
|
|
118
|
+
* Copying dist without this leaves an install whose daemon cannot mint. So the sync links
|
|
119
|
+
* it, from the monorepo that just built it. Safe by construction: this whole script only
|
|
120
|
+
* runs on a machine that has BOTH an install and this source tree.
|
|
121
|
+
*/
|
|
122
|
+
const coreSrc = join(pkgRoot, '..', 'core');
|
|
123
|
+
const scopeDir = join(installed, 'node_modules', '@clearly-sh');
|
|
124
|
+
if (existsSync(coreSrc)) {
|
|
125
|
+
try {
|
|
126
|
+
mkdirSync(scopeDir, { recursive: true });
|
|
127
|
+
const link = join(scopeDir, 'beehaven-core');
|
|
128
|
+
rmSync(link, { recursive: true, force: true });
|
|
129
|
+
symlinkSync(coreSrc, link, 'dir');
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error(` postbuild: could not link @clearly-sh/beehaven-core (${e?.message ?? e})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// VERIFY BEFORE CLAIMING. A sync that leaves a CLI which cannot start is worse than a stale
|
|
136
|
+
// one, because the staleness at least still works. Run the synced entry point; if it can't
|
|
137
|
+
// even print its version, say so loudly rather than reporting success.
|
|
138
|
+
try {
|
|
139
|
+
execFileSync(process.execPath, [join(distDst, 'cli.js'), '--version'], { stdio: 'pipe', timeout: 20_000 });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(' postbuild: SYNCED BUILD DOES NOT RUN — the installed CLI may be broken.');
|
|
142
|
+
console.error(` ${String(e?.stderr ?? e?.message ?? e).slice(0, 300)}`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A marker the CLI can read to say WHERE the installed bytes came from, so
|
|
147
|
+
// `beehaven --version` on the installed copy can name its origin instead of implying it is
|
|
148
|
+
// a release.
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(installed, '.synced-from'),
|
|
151
|
+
JSON.stringify({ from: pkgRoot, at: new Date().toISOString() }, null, 2),
|
|
152
|
+
);
|
|
153
|
+
console.log(` postbuild: synced → ${installed}`);
|
|
154
|
+
console.log(` Verify selection with \`beehaven --version\`; an equal/newer Mac bundle may still win.`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Never fail the build over this. A stale install is a nuisance; a red build over a copy
|
|
157
|
+
// is worse, and the runtime staleness warning still catches the case.
|
|
158
|
+
console.error(` postbuild: sync failed (${err?.message ?? err}) — installed copy may be stale.`);
|
|
159
|
+
}
|