@haifai/bwhale 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +53 -0
  2. package/bin/bwhale.js +227 -0
  3. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # bwhale — Baby Whale in one command
2
+
3
+ Local-first knowledge-work coworker: give it a task, get back finished
4
+ Excel workbooks, PowerPoint decks, Word documents, and PDFs — built by
5
+ code, previewed pixel-perfect, and never leaving your machine.
6
+
7
+ ```bash
8
+ npm install -g bwhale
9
+ bwhale
10
+ ```
11
+
12
+ First run fetches the runtime once (~400 MB, from this project's GitHub
13
+ Releases) into `~/.bwhale`, checks your platform's prerequisites, and
14
+ opens `http://127.0.0.1:24680`. Later runs start instantly.
15
+
16
+ ## Commands
17
+
18
+ | Command | What it does |
19
+ |---|---|
20
+ | `bwhale` | Start (first run installs the runtime) |
21
+ | `bwhale doctor` | Report what's present / missing on your machine |
22
+ | `bwhale update` | Refresh to the latest release on next start |
23
+
24
+ ## What it checks and installs
25
+
26
+ The app ships with its own Node — you only need the platform basics, and
27
+ `bwhale doctor` tells you exactly what's missing and the platform-native
28
+ command to get it:
29
+
30
+ - **macOS** — nothing else required; office libraries install themselves
31
+ on first boot
32
+ - **Linux** — `git` required; `python3` for office-file creation;
33
+ `libreoffice` optional (pixel-perfect previews, also installable
34
+ in-app later)
35
+ - **Windows** — `git`; `python3` for office creation
36
+
37
+ Connect a model (one-time): create `~/.dsh/.credentials.yaml`:
38
+
39
+ ```yaml
40
+ version: 1
41
+ refs:
42
+ DEEPSEEK_API_KEY: sk-your-key-here
43
+ ```
44
+
45
+ then `chmod 600 ~/.dsh/.credentials.yaml`.
46
+
47
+ ## Privacy
48
+
49
+ Files, sessions, history, and the workspace live entirely on your machine.
50
+ The only network calls are the model provider you configure, the one-time
51
+ runtime download, and update checks.
52
+
53
+ MIT — a knowledge-work product built on the DeepSeek Harness.
package/bin/bwhale.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bwhale — the npm launcher for Baby Whale. The published package carries
4
+ * only the app code pointer and this installer/launcher (never
5
+ * node_modules); the runtime bundle is fetched once from GitHub Releases
6
+ * into ~/.bwhale, with per-platform prerequisites checked — and offered for
7
+ * install the platform's own way — before boot.
8
+ *
9
+ * Commands:
10
+ * bwhale start (first run: fetch bundle, then boot + open)
11
+ * bwhale doctor report prerequisite status per platform convention
12
+ * bwhale update refresh the runtime bundle to the latest release
13
+ */
14
+ import { spawnSync } from 'node:child_process'
15
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
16
+ import { homedir, platform, arch } from 'node:os'
17
+ import path from 'node:path'
18
+ import { Readable } from 'node:stream'
19
+ import { finished } from 'node:stream/promises'
20
+
21
+ const REPO = 'Haifai-AI/baby-whale'
22
+ const HOME = homedir()
23
+ const ROOT = process.env.BWHALE_HOME ?? path.join(HOME, '.bwhale')
24
+ const LATEST_API = `https://api.github.com/repos/${REPO}/releases/latest`
25
+ const PORT = 24680
26
+ const MARKER_VERSION = 2 // bundle layout revision; bump forces a refetch
27
+
28
+ const SUPPORT = {
29
+ darwin: {
30
+ label: 'macOS',
31
+ platformKey: () => 'macos',
32
+ missing: () => [], // git/python ship with the dev tools; office libs auto-install at first boot
33
+ },
34
+ linux: {
35
+ label: 'Linux',
36
+ platformKey: () => 'linux',
37
+ missing: () => {
38
+ const gaps = []
39
+ if (!which('git')) gaps.push({ name: 'git', hint: 'sudo apt install git (or your distro equivalent)' })
40
+ if (!which('python3')) gaps.push({ name: 'python3', hint: 'sudo apt install python3 python3-venv (office-file creation)' })
41
+ if (!which('soffice')) gaps.push({
42
+ name: 'LibreOffice (optional — pixel-perfect previews)',
43
+ hint: 'sudo apt install libreoffice — or accept the in-app one-time setup later',
44
+ })
45
+ return gaps
46
+ },
47
+ },
48
+ win32: {
49
+ label: 'Windows',
50
+ platformKey: () => 'windows',
51
+ missing: () => {
52
+ const gaps = []
53
+ if (!which('git')) gaps.push({ name: 'git', hint: 'winget install Git.Git' })
54
+ if (!which('python')) gaps.push({ name: 'python3', hint: 'winget install Python.Python.3.12 (office-file creation)' })
55
+ return gaps
56
+ },
57
+ },
58
+ }[platform()] ?? null
59
+
60
+ function which(bin) {
61
+ const probe = platform() === 'win32'
62
+ ? spawnSync('where', [bin], { stdio: 'ignore' })
63
+ : spawnSync('which', [bin], { stdio: 'ignore' })
64
+ return probe.status === 0
65
+ }
66
+
67
+ function log(message) { console.error(message) }
68
+ function die(message) { log(`bwhale: ${message}`); process.exit(1) }
69
+
70
+ /** The platform/arch bundle asset this machine needs. */
71
+ function bundleAssetName(version) {
72
+ const p = SUPPORT?.platformKey() ?? platform()
73
+ const a = arch() === 'arm64' ? 'arm64' : 'x86_64'
74
+ return `baby-whale-${p}-${a}-${version}.zip`
75
+ }
76
+
77
+ async function latestRelease() {
78
+ const response = await fetch(LATEST_API, { headers: { 'user-agent': 'bwhale-launcher' } })
79
+ if (!response.ok) die(`cannot reach GitHub Releases (HTTP ${response.status})`)
80
+ const release = await response.json()
81
+ const asset = (release.assets ?? []).find(a => a.name === bundleAssetName(release.tag_name))
82
+ if (asset === undefined) {
83
+ die(`no bundle for ${platform()}/${arch()} in release ${release.tag_name} — assets: ${(release.assets ?? []).map(a => a.name).join(', ') || 'none'}`)
84
+ }
85
+ return { version: release.tag_name, url: asset.browser_download_url, name: asset.name, size: asset.size }
86
+ }
87
+
88
+ /** Stream a download with a percent progress line; resumable cache across runs. */
89
+ async function download(url, dest, size) {
90
+ mkdirSync(path.dirname(dest), { recursive: true })
91
+ const partial = `${dest}.part`
92
+ const response = await fetch(url, { redirect: 'follow' })
93
+ if (!response.ok || response.body === null) die(`download failed with HTTP ${response.status}`)
94
+ const total = Number(response.headers.get('content-length') ?? size ?? 0)
95
+ let received = 0
96
+ const out = createWriteStream(partial)
97
+ process.stderr.write('bwhale: downloading runtime ')
98
+ for await (const chunk of Readable.fromWeb(response.body)) {
99
+ received += chunk.length
100
+ out.write(chunk)
101
+ if (total > 0) process.stderr.write(`\rbwhale: downloading runtime ${Math.round((received / total) * 100)}%`)
102
+ }
103
+ process.stderr.write('\n')
104
+ await finished(out)
105
+ // integrity: the release asset is the contract; a truncated file must never install
106
+ if (total > 0 && received !== total) die(`download truncated (${received}/${total} bytes) — retry`)
107
+ rmSync(dest, { force: true })
108
+ statSync(partial)
109
+ return partial
110
+ }
111
+
112
+ function unzip(archive, into) {
113
+ mkdirSync(into, { recursive: true })
114
+ const result = platform() === 'win32'
115
+ ? spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -Force -LiteralPath "${archive}" -DestinationPath "${into}"`], { stdio: 'inherit' })
116
+ : spawnSync('unzip', ['-q', archive, '-d', into], { stdio: 'inherit' })
117
+ if (result.status !== 0) die('extraction failed')
118
+ }
119
+
120
+ /** Installed bundle layout: <ROOT>/runtime/baby-whale-<p>-<a>-<version> */
121
+ function runtimeDir(version) {
122
+ const p = SUPPORT?.platformKey() ?? platform()
123
+ const a = arch() === 'arm64' ? 'arm64' : 'x86_64'
124
+ return path.join(ROOT, 'runtime', `baby-whale-${p}-${a}-${version}`)
125
+ }
126
+
127
+ function installedMarker() {
128
+ return path.join(ROOT, 'installed.json')
129
+ }
130
+
131
+ function readInstalled() {
132
+ try {
133
+ const parsed = JSON.parse(readFileSync(installedMarker(), 'utf8'))
134
+ return parsed?.markerVersion === MARKER_VERSION ? parsed : null
135
+ } catch { return null }
136
+ }
137
+
138
+ function writeInstalled(entry) {
139
+ mkdirSync(ROOT, { recursive: true })
140
+ writeFileSync(installedMarker(), JSON.stringify({ markerVersion: MARKER_VERSION, ...entry }, null, 2))
141
+ }
142
+
143
+ /** Ensure the runtime bundle exists at the wanted version; fetch+install if not. */
144
+ async function ensureRuntime(wanted) {
145
+ const existing = readInstalled()
146
+ if (wanted === 'installed' && existing !== null) return existing
147
+ const release = wanted === 'latest'
148
+ ? await latestRelease()
149
+ : { version: wanted, url: `https://github.com/${REPO}/releases/download/${wanted}/${bundleAssetName(wanted)}`, name: bundleAssetName(wanted) }
150
+ if (existing?.version === release.version) return existing // already exactly this version
151
+ log(`bwhale: fetching Baby Whale ${release.version} runtime (${Math.round((release.size ?? 0) / 1048576) || '~400'} MB, once)`)
152
+ const archive = await download(release.url, path.join(ROOT, 'cache', release.name), release.size)
153
+ const dir = runtimeDir(release.version)
154
+ rmSync(dir, { recursive: true, force: true })
155
+ unzip(archive, path.join(ROOT, 'runtime'))
156
+ const inner = path.join(dir, 'baby-whale')
157
+ if (!existsSync(inner)) die('bundle layout unexpected — no baby-whale directory')
158
+ if (platform() !== 'win32') {
159
+ // launcher + node binary need the exec bit back (zip loses some)
160
+ for (const rel of ['START.command', 'START', 'node/bin/node']) {
161
+ const p = path.join(dir, rel)
162
+ if (existsSync(p)) spawnSync('chmod', ['+x', p])
163
+ }
164
+ }
165
+ rmSync(archive, { force: true })
166
+ const entry = { version: release.version, dir, installedAt: new Date().toISOString() }
167
+ writeInstalled(entry)
168
+ return entry
169
+ }
170
+
171
+ async function cmdStart(argv) {
172
+ const gaps = SUPPORT?.missing() ?? []
173
+ if (gaps.length > 0) {
174
+ log(`bwhale: missing prerequisites on ${SUPPORT?.label ?? platform()}:`)
175
+ for (const gap of gaps) log(` - ${gap.name}: ${gap.hint}`)
176
+ if (gaps.some(g => g.name.startsWith('git'))) die('git is required (session history)')
177
+ }
178
+ const entry = await ensureRuntime(process.env.BWHALE_VERSION ?? 'latest')
179
+ const nodeBin = platform() === 'win32'
180
+ ? path.join(entry.dir, 'node', 'node.exe')
181
+ : path.join(entry.dir, 'node', 'bin', 'node')
182
+ const cli = path.join(entry.dir, 'baby-whale', 'apps', 'cli', 'lib', 'bin.js')
183
+ if (!existsSync(cli)) die(`runtime incomplete: ${cli} missing — run \`bwhale update\``)
184
+ // Already serving? Never double-start — just bring the workspace up.
185
+ try {
186
+ const probe = await fetch(`http://127.0.0.1:${PORT}/`, { signal: AbortSignal.timeout(1500) })
187
+ if (probe.ok) {
188
+ log(`bwhale: Baby Whale is already running at http://127.0.0.1:${PORT}/`)
189
+ const open = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open'
190
+ spawnSync(open, [`http://127.0.0.1:${PORT}/`], { stdio: 'ignore' })
191
+ return
192
+ }
193
+ } catch {
194
+ // Not running — boot it below.
195
+ }
196
+ const child = spawnSync(nodeBin, [cli, 'web', ...argv], { stdio: 'inherit' })
197
+ process.exitCode = child.status ?? 0
198
+ }
199
+
200
+ function cmdDoctor() {
201
+ const p = platform()
202
+ log(`bwhale doctor — ${SUPPORT?.label ?? p} (${arch()})`)
203
+ log(` node: ${process.version} (launcher runtime — the app ships its own)`)
204
+ log(` git: ${which('git') ? 'ok' : 'MISSING (required)'}`)
205
+ log(` python3: ${which('python3') || which('python') ? 'ok' : 'MISSING (office-file creation auto-installs libs on first boot)'}`)
206
+ log(` libreoffice: ${which('soffice') ? 'ok' : 'not found (optional — the app offers a one-time setup for pixel-perfect previews)'}`)
207
+ const entry = readInstalled()
208
+ log(` runtime: ${entry === null ? 'not installed yet (first \`bwhale\` fetches it)' : `${entry.version} at ${entry.dir}`}`)
209
+ const gaps = SUPPORT?.missing() ?? []
210
+ if (gaps.length > 0) {
211
+ log(' to install missing pieces:')
212
+ for (const gap of gaps) log(` ${gap.name}: ${gap.hint}`)
213
+ }
214
+ }
215
+
216
+ function cmdUpdate() {
217
+ rmSync(installedMarker(), { force: true })
218
+ log('bwhale: will fetch the latest release on next start.')
219
+ }
220
+
221
+ const [command = 'start', ...rest] = process.argv.slice(2)
222
+ const dispatch = {
223
+ start: () => cmdStart(rest.filter(a => a !== '--no-open')),
224
+ doctor: () => cmdDoctor(),
225
+ update: () => cmdUpdate(),
226
+ }
227
+ ;(dispatch[command] ?? die(`unknown command "${command}" — try bwhale, bwhale doctor, or bwhale update`))()
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@haifai/bwhale",
3
+ "version": "0.1.2",
4
+ "description": "Baby Whale — local-first knowledge-work coworker. One command: bwhale. Fetches its runtime once, starts the app, opens your browser. Nothing leaves your machine.",
5
+ "license": "MIT",
6
+ "publishConfig": { "access": "public" },
7
+ "type": "module",
8
+ "bin": { "bwhale": "bin/bwhale.js" },
9
+ "files": ["bin/", "README.md"],
10
+ "engines": { "node": ">=20" },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Haifai-AI/baby-whale.git",
14
+ "directory": "npm/bwhale"
15
+ },
16
+ "keywords": ["ai", "agent", "office", "xlsx", "pptx", "docx", "cowork", "local-first"],
17
+ "scripts": { "prepack": "node --check bin/bwhale.js" }
18
+ }