@openparachute/vault 0.6.5-rc.2 → 0.6.5-rc.9

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 (34) hide show
  1. package/core/src/do-param-cap.test.ts +161 -0
  2. package/core/src/links.ts +8 -13
  3. package/core/src/notes.ts +33 -15
  4. package/core/src/onboarding.ts +14 -296
  5. package/core/src/seed-packs.test.ts +191 -0
  6. package/core/src/seed-packs.ts +559 -0
  7. package/core/src/sql-in.test.ts +58 -0
  8. package/core/src/sql-in.ts +76 -0
  9. package/core/src/transcription/provider.ts +141 -0
  10. package/core/src/vault-projection.ts +1 -1
  11. package/core/src/wikilinks.ts +10 -4
  12. package/package.json +1 -1
  13. package/src/add-pack.test.ts +142 -0
  14. package/src/cli.ts +542 -7
  15. package/src/onboarding-seed.test.ts +126 -40
  16. package/src/onboarding-seed.ts +41 -46
  17. package/src/routes.ts +17 -0
  18. package/src/server.ts +77 -31
  19. package/src/transcription/build.test.ts +224 -0
  20. package/src/transcription/build.ts +252 -0
  21. package/src/transcription/capability.test.ts +93 -0
  22. package/src/transcription/capability.ts +71 -0
  23. package/src/transcription/install.test.ts +167 -0
  24. package/src/transcription/install.ts +296 -0
  25. package/src/transcription/providers/scribe-http.test.ts +195 -0
  26. package/src/transcription/providers/scribe-http.ts +144 -0
  27. package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
  28. package/src/transcription/providers/transcribe-cpp.ts +293 -0
  29. package/src/transcription/select.test.ts +134 -0
  30. package/src/transcription/select.ts +164 -0
  31. package/src/transcription-worker.test.ts +44 -0
  32. package/src/transcription-worker.ts +57 -122
  33. package/src/vault-create.test.ts +38 -10
  34. package/src/vault.test.ts +48 -0
package/src/cli.ts CHANGED
@@ -17,9 +17,9 @@
17
17
  * parachute-vault status — show full status
18
18
  */
19
19
 
20
- import { resolve } from "path";
20
+ import { resolve, join, dirname } from "path";
21
21
  import { homedir } from "os";
22
- import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "fs";
22
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, copyFileSync, chmodSync, readdirSync, statSync, renameSync } from "fs";
23
23
  // JSON import — resolved at module load, works for both dev runs
24
24
  // (`bun src/cli.ts …`) and the published package (`bunx @openparachute/vault`)
25
25
  // because package.json ships at the root next to src/.
@@ -140,6 +140,28 @@ import {
140
140
  verifyAndConsumeBackupCode,
141
141
  getTotpSecret,
142
142
  } from "./two-factor.ts";
143
+ import {
144
+ planInstall,
145
+ detectTotalRamBytes,
146
+ isSharedLibFile,
147
+ MODELS,
148
+ type InstallPlan,
149
+ type ModelChoice,
150
+ } from "./transcription/install.ts";
151
+ import {
152
+ resolveTranscribeCppPaths,
153
+ resolveTranscriptionProviderName,
154
+ transcribeCppInstalled,
155
+ readManifest,
156
+ type TranscribeCppManifest,
157
+ } from "./transcription/select.ts";
158
+ import {
159
+ buildTranscribeCli,
160
+ cliSourceUrl,
161
+ compilerInstallHint,
162
+ TRANSCRIBE_CPP_SOURCE_REF,
163
+ type CliBuildResult,
164
+ } from "./transcription/build.ts";
143
165
 
144
166
  // ---------------------------------------------------------------------------
145
167
  // Argument parsing
@@ -181,6 +203,9 @@ switch (command) {
181
203
  case "create":
182
204
  await cmdCreate(cmdArgs);
183
205
  break;
206
+ case "add-pack":
207
+ await cmdAddPack(cmdArgs);
208
+ break;
184
209
  case "list":
185
210
  case "ls":
186
211
  cmdList();
@@ -246,6 +271,9 @@ switch (command) {
246
271
  case "schema":
247
272
  await cmdSchema(cmdArgs);
248
273
  break;
274
+ case "transcription":
275
+ await cmdTranscription(cmdArgs);
276
+ break;
249
277
  case "help":
250
278
  case "--help":
251
279
  case "-h":
@@ -3496,6 +3524,390 @@ function printHistoryUsage(): void {
3496
3524
  console.error(" --json Emit the history as JSON instead of the human-readable list");
3497
3525
  }
3498
3526
 
3527
+ // ---------------------------------------------------------------------------
3528
+ // Transcription — `parachute-vault transcription <subcommand>` (scribe-fold 2a)
3529
+ // ---------------------------------------------------------------------------
3530
+
3531
+ /**
3532
+ * `parachute-vault transcription install` — set up the local `transcribe-cpp`
3533
+ * provider: detect OS/arch + RAM, pick a tier-appropriate GGUF model
3534
+ * (scribe#82 fix — Parakeet floored to ≥4GB), download the prebuilt
3535
+ * transcribe-cli release asset + the model into
3536
+ * `~/.parachute/transcription/`, and switch `TRANSCRIPTION_PROVIDER` in the
3537
+ * vault `.env`. Idempotent (skips already-present files unless `--force`),
3538
+ * non-fatal. `--dry-run`/`--plan` prints the selection without downloading —
3539
+ * the path tests exercise (no network).
3540
+ *
3541
+ * `parachute-vault transcription status` — show the configured provider +
3542
+ * install state.
3543
+ */
3544
+ async function cmdTranscription(args: string[]) {
3545
+ const sub = args[0];
3546
+ if (sub === "install") {
3547
+ await cmdTranscriptionInstall(args.slice(1));
3548
+ return;
3549
+ }
3550
+ if (sub === "status" || sub === undefined) {
3551
+ cmdTranscriptionStatus();
3552
+ return;
3553
+ }
3554
+ console.error(`Unknown transcription subcommand: ${sub}`);
3555
+ console.error("Usage: parachute-vault transcription install [--dry-run] [--model <id>] [--provider <name>]");
3556
+ console.error(" parachute-vault transcription status");
3557
+ process.exit(1);
3558
+ }
3559
+
3560
+ /** Render an install plan for the human (shared by --dry-run + the real run). */
3561
+ function printTranscriptionPlan(plan: InstallPlan): void {
3562
+ console.log(`Host: ${plan.platform}/${plan.arch}, ${plan.totalRamGb}GB RAM`);
3563
+ if (!plan.supported) {
3564
+ console.log(`\n ${plan.refused ? "Refused" : "Unsupported"}: ${plan.reason}`);
3565
+ return;
3566
+ }
3567
+ const m = plan.model!;
3568
+ console.log(`Binary: ${plan.asset!.asset}`);
3569
+ console.log(`Model: ${m.name} (${m.file})`);
3570
+ console.log(` ${m.note}`);
3571
+ console.log(` ~${m.approxSizeMb}MB download, ~${m.approxRuntimeGb}GB peak RAM while transcribing`);
3572
+ }
3573
+
3574
+ async function cmdTranscriptionInstall(args: string[]) {
3575
+ const dryRun = args.includes("--dry-run") || args.includes("--plan");
3576
+ const force = args.includes("--force");
3577
+ const yes = args.includes("--yes") || args.includes("-y");
3578
+ const overrideModel = takeArgValue(args, "--model").value;
3579
+ const provider = takeArgValue(args, "--provider").value ?? "transcribe-cpp";
3580
+
3581
+ if (provider === "scribe-http") {
3582
+ // Just flip config back to the remote provider — no download.
3583
+ if (dryRun) {
3584
+ console.log("Would set TRANSCRIPTION_PROVIDER=scribe-http in the vault .env (remote provider; no download).");
3585
+ return;
3586
+ }
3587
+ setEnvVar("TRANSCRIPTION_PROVIDER", "scribe-http");
3588
+ console.log("Set TRANSCRIPTION_PROVIDER=scribe-http. Restart vault to apply. (Remote provider — no local binary/model needed.)");
3589
+ return;
3590
+ }
3591
+ if (provider !== "transcribe-cpp") {
3592
+ console.error(`Unknown --provider "${provider}". Valid: transcribe-cpp, scribe-http.`);
3593
+ process.exit(1);
3594
+ }
3595
+
3596
+ const plan = planInstall({
3597
+ platform: process.platform,
3598
+ arch: process.arch,
3599
+ totalRamBytes: detectTotalRamBytes(),
3600
+ overrideModel,
3601
+ });
3602
+
3603
+ console.log("Transcription install plan (transcribe-cpp — local, no Python):\n");
3604
+ printTranscriptionPlan(plan);
3605
+
3606
+ if (!plan.supported) {
3607
+ // Refused / unsupported is not a hard error — it's a steer to remote.
3608
+ console.log("\nNo local install performed. See the note above.");
3609
+ process.exit(dryRun ? 0 : 1);
3610
+ }
3611
+
3612
+ const paths = resolveTranscribeCppPaths();
3613
+ console.log(`\nInstall dir: ${paths.dir}`);
3614
+
3615
+ if (dryRun) {
3616
+ console.log("\n(--dry-run) No files downloaded.");
3617
+ return;
3618
+ }
3619
+
3620
+ if (!yes) {
3621
+ const ok = await confirm(`Download ~${plan.footprintMb}MB and install to ${paths.dir}?`, true);
3622
+ if (!ok) {
3623
+ console.log("Aborted.");
3624
+ return;
3625
+ }
3626
+ }
3627
+
3628
+ // ffmpeg hint (non-fatal — the transcode path checks at runtime).
3629
+ if (!Bun.which("ffmpeg")) {
3630
+ console.warn(
3631
+ "\n⚠ ffmpeg not found on PATH. transcribe-cli needs 16kHz mono WAV; non-WAV capture audio is transcoded via ffmpeg.\n" +
3632
+ " Install it: apt install ffmpeg (Debian/Ubuntu) | brew install ffmpeg (macOS)",
3633
+ );
3634
+ }
3635
+
3636
+ try {
3637
+ mkdirSync(paths.binDir, { recursive: true });
3638
+ mkdirSync(paths.libsDir, { recursive: true });
3639
+ mkdirSync(paths.modelsDir, { recursive: true });
3640
+
3641
+ // 1) Release library bundle → extract the runtime dylibs/sos into libs/.
3642
+ // transcribe.cpp v0.1.1 ships a LIBRARY, not a prebuilt transcribe-cli,
3643
+ // so a CLI is usually NOT placed here — `installTranscribeLibs` still
3644
+ // opportunistically places one if a (future) asset includes it.
3645
+ const existingLibs = (readdirSync(paths.libsDir) as string[]).filter(isSharedLibFile);
3646
+ let libFiles = existingLibs;
3647
+ if (existingLibs.length && !force) {
3648
+ console.log(`\n✓ runtime libraries already present (${paths.libsDir}) — skipping (use --force to re-download).`);
3649
+ } else {
3650
+ console.log(`\nDownloading ${plan.asset!.asset} …`);
3651
+ libFiles = await installTranscribeLibs(plan, paths);
3652
+ console.log(
3653
+ libFiles.length
3654
+ ? `✓ Extracted ${libFiles.length} runtime library file(s) → ${paths.libsDir}`
3655
+ : `⚠ no shared libraries found in ${plan.asset!.asset} (the asset layout may have changed).`,
3656
+ );
3657
+ }
3658
+
3659
+ // 2) Model GGUF.
3660
+ const modelDest = join(paths.modelsDir, plan.model!.file);
3661
+ if (existsSync(modelDest) && !force) {
3662
+ console.log(`✓ model already present (${modelDest}) — skipping (use --force to re-download).`);
3663
+ } else {
3664
+ console.log(`Downloading model ${plan.model!.file} (~${plan.model!.approxSizeMb}MB) …`);
3665
+ await downloadTo(plan.model!.url, modelDest);
3666
+ }
3667
+
3668
+ // 3) Build transcribe-cli from source against the extracted dylibs.
3669
+ // transcribe.cpp v0.1.1 ships a LIBRARY, not a prebuilt CLI, so we
3670
+ // compile examples/cli/main.cpp + examples/common/wav.cpp against
3671
+ // libtranscribe (a tiny ~127KB compile — no ggml rebuild). Non-fatal:
3672
+ // any failure keeps the current provider + prints guidance.
3673
+ const buildResult = await maybeBuildTranscribeCli(paths, libFiles, force);
3674
+
3675
+ // 4) Manifest.
3676
+ const manifest: TranscribeCppManifest = {
3677
+ version: plan.asset!.asset,
3678
+ asset: plan.asset!.asset,
3679
+ binFile: "transcribe-cli",
3680
+ model: plan.model!.name,
3681
+ modelFile: plan.model!.file,
3682
+ libFiles,
3683
+ binBuiltFrom: buildResult?.ok ? TRANSCRIBE_CPP_SOURCE_REF : undefined,
3684
+ os: plan.platform,
3685
+ arch: plan.arch,
3686
+ ram_gb: plan.totalRamGb,
3687
+ installedAt: new Date().toISOString(),
3688
+ };
3689
+ writeFileSync(paths.manifestPath, JSON.stringify(manifest, null, 2));
3690
+
3691
+ // 5) CLI resolution + provider switch — honest, no silent success. `binPath`
3692
+ // resolves to TRANSCRIBE_CPP_BIN or the built binary; we activate
3693
+ // transcribe-cpp only when one actually exists, else we keep the current
3694
+ // provider and report the acquisition gap rather than taking
3695
+ // transcription offline behind a provider that can't run.
3696
+ if (existsSync(paths.binPath)) {
3697
+ setEnvVar("TRANSCRIPTION_PROVIDER", "transcribe-cpp");
3698
+ console.log(`\n✓ Installed and activated → TRANSCRIPTION_PROVIDER=transcribe-cpp set in the vault .env.`);
3699
+ console.log(` CLI: ${paths.binPath}`);
3700
+ console.log(` Restart vault to activate: parachute restart vault`);
3701
+ } else {
3702
+ console.log(`\n✓ Libraries + model installed to ${paths.dir} — transcription is NOT yet activated.`);
3703
+ console.log(noRunnableCliGuidance(paths, buildResult));
3704
+ }
3705
+ } catch (err) {
3706
+ console.error(`\nInstall failed: ${err instanceof Error ? err.message : String(err)}`);
3707
+ console.error("The verb is idempotent — fix the issue and re-run. Or use the remote provider: parachute-vault transcription install --provider scribe-http");
3708
+ process.exit(1);
3709
+ }
3710
+ }
3711
+
3712
+ /** Download a URL to a destination file (streamed via fetch → Bun.write). */
3713
+ async function downloadTo(url: string, dest: string): Promise<void> {
3714
+ const resp = await fetch(url, { redirect: "follow" });
3715
+ if (!resp.ok) throw new Error(`download failed (${resp.status}) for ${url}`);
3716
+ await Bun.write(dest, resp);
3717
+ }
3718
+
3719
+ /**
3720
+ * Download the release tarball, extract it, and place its runtime shared
3721
+ * libraries (libtranscribe + libggml `.dylib`/`.so`) into `paths.libsDir`,
3722
+ * returning the extracted lib filenames. transcribe.cpp v0.1.1 ships a LIBRARY,
3723
+ * not a prebuilt `transcribe-cli` — so we also look for a CLI binary (a future
3724
+ * asset may include one) and, if present, place it at `paths.binPath` + chmod +x
3725
+ * (the caller decides activation from whether a CLI exists). Uses system `tar`
3726
+ * (present on Linux + macOS). Kept out of the plan/dry-run path so tests never
3727
+ * hit the network.
3728
+ */
3729
+ async function installTranscribeLibs(
3730
+ plan: InstallPlan,
3731
+ paths: ReturnType<typeof resolveTranscribeCppPaths>,
3732
+ ): Promise<string[]> {
3733
+ const tmpRoot = join(paths.dir, ".dl");
3734
+ mkdirSync(tmpRoot, { recursive: true });
3735
+ const tarball = join(tmpRoot, plan.asset!.asset);
3736
+ await downloadTo(plan.asset!.url, tarball);
3737
+
3738
+ const extractDir = join(tmpRoot, "x");
3739
+ rmSync(extractDir, { recursive: true, force: true });
3740
+ mkdirSync(extractDir, { recursive: true });
3741
+ const tarProc = Bun.spawnSync(["tar", "-xzf", tarball, "-C", extractDir]);
3742
+ if (tarProc.exitCode !== 0) {
3743
+ throw new Error(`tar extract failed: ${new TextDecoder().decode(tarProc.stderr)}`);
3744
+ }
3745
+
3746
+ // Walk the extracted tree: keep the shared libs, opportunistically place a CLI.
3747
+ mkdirSync(paths.libsDir, { recursive: true });
3748
+ const libFiles: string[] = [];
3749
+ for (const rel of readdirSync(extractDir, { recursive: true }) as string[]) {
3750
+ const abs = join(extractDir, rel);
3751
+ if (!statSync(abs).isFile()) continue;
3752
+ const base = rel.split("/").pop()!;
3753
+ if (isSharedLibFile(base)) {
3754
+ copyFileSync(abs, join(paths.libsDir, base));
3755
+ libFiles.push(base);
3756
+ } else if (base === "transcribe-cli") {
3757
+ copyFileSync(abs, paths.binPath);
3758
+ chmodSync(paths.binPath, 0o755);
3759
+ }
3760
+ }
3761
+ rmSync(tmpRoot, { recursive: true, force: true });
3762
+ return libFiles;
3763
+ }
3764
+
3765
+ /**
3766
+ * Build `transcribe-cli` from source against the extracted dylibs, honoring
3767
+ * idempotency + the `TRANSCRIBE_CPP_BIN` operator override. Returns the build
3768
+ * result, or `null` when the build was intentionally skipped (already built /
3769
+ * operator-provided binary / no libs to link). Non-fatal — logs progress and
3770
+ * warnings; failures come back as a typed `{ ok:false }` the caller reports.
3771
+ */
3772
+ async function maybeBuildTranscribeCli(
3773
+ paths: ReturnType<typeof resolveTranscribeCppPaths>,
3774
+ libFiles: string[],
3775
+ force: boolean,
3776
+ ): Promise<CliBuildResult | null> {
3777
+ if (process.env.TRANSCRIBE_CPP_BIN?.trim()) {
3778
+ console.log(
3779
+ existsSync(paths.binPath)
3780
+ ? `\n✓ Using operator-provided TRANSCRIBE_CPP_BIN=${paths.binPath} — skipping source build.`
3781
+ : `\n⚠ TRANSCRIBE_CPP_BIN=${paths.binPath} is set but no file exists there — skipping source build; nothing will be activated until it points at a real binary.`,
3782
+ );
3783
+ return null;
3784
+ }
3785
+ if (existsSync(paths.binPath) && !force) {
3786
+ console.log(`\n✓ transcribe-cli already built (${paths.binPath}) — skipping build (use --force to rebuild).`);
3787
+ return null;
3788
+ }
3789
+ if (libFiles.length === 0) {
3790
+ console.warn(`\n⚠ no runtime libraries were extracted — cannot build transcribe-cli. Skipping build.`);
3791
+ return null;
3792
+ }
3793
+
3794
+ console.log(`\nBuilding transcribe-cli from source (transcribe.cpp @ ${TRANSCRIBE_CPP_SOURCE_REF.slice(0, 12)}) …`);
3795
+ mkdirSync(paths.binDir, { recursive: true });
3796
+ const srcDir = join(paths.dir, ".src");
3797
+ const result = await buildTranscribeCli(
3798
+ { srcDir, libsDir: paths.libsDir, binPath: paths.binPath },
3799
+ {
3800
+ platform: process.platform,
3801
+ which: (c) => Bun.which(c),
3802
+ fetchSource: async (files, dir) => {
3803
+ rmSync(dir, { recursive: true, force: true });
3804
+ for (const rel of files) {
3805
+ const dest = join(dir, rel);
3806
+ mkdirSync(dirname(dest), { recursive: true });
3807
+ await downloadTo(cliSourceUrl(rel), dest);
3808
+ }
3809
+ },
3810
+ compile: (cmd) => {
3811
+ const p = Bun.spawnSync(cmd);
3812
+ return { exitCode: p.exitCode ?? 1, stderr: new TextDecoder().decode(p.stderr) };
3813
+ },
3814
+ exists: existsSync,
3815
+ removeBin: (p) => rmSync(p, { force: true }),
3816
+ rename: (from, to) => renameSync(from, to),
3817
+ },
3818
+ );
3819
+
3820
+ if (result.ok) {
3821
+ chmodSync(paths.binPath, 0o755); // belt-and-suspenders; the compiler emits 0755
3822
+ rmSync(srcDir, { recursive: true, force: true }); // tidy — source is re-fetchable
3823
+ console.log(`✓ Built transcribe-cli → ${result.binPath}`);
3824
+ } else {
3825
+ // Keep srcDir on failure so the printed compile command can be re-run by hand.
3826
+ console.warn(`⚠ transcribe-cli build failed (${result.stage}): ${result.message}`);
3827
+ if (result.command) console.warn(` compile command tried:\n ${result.command}`);
3828
+ }
3829
+ return result;
3830
+ }
3831
+
3832
+ /**
3833
+ * The "no runnable CLI" guidance printed when activation can't proceed —
3834
+ * tailored to WHY (build failed at toolchain vs compile, or was skipped).
3835
+ */
3836
+ function noRunnableCliGuidance(
3837
+ paths: ReturnType<typeof resolveTranscribeCppPaths>,
3838
+ build: CliBuildResult | null,
3839
+ ): string {
3840
+ const lines: string[] = [];
3841
+ if (build && !build.ok && build.stage === "toolchain") {
3842
+ lines.push(
3843
+ `\n⚠ Could not build transcribe-cli — ${build.message}`,
3844
+ " To finish, either:",
3845
+ ` • install a C++ compiler and re-run this verb (${compilerInstallHint(process.platform)}); OR`,
3846
+ " • build transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
3847
+ " • use the remote provider: parachute-vault transcription install --provider scribe-http",
3848
+ );
3849
+ } else if (build && !build.ok && build.stage === "fetch") {
3850
+ lines.push(
3851
+ `\n⚠ Could not fetch the transcribe-cli source — ${build.message}`,
3852
+ " The libraries + model are installed; only the source download failed (check network/proxy).",
3853
+ " To finish, either:",
3854
+ " • re-run this verb once connectivity is restored; OR",
3855
+ " • build transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
3856
+ " • use the remote provider: parachute-vault transcription install --provider scribe-http",
3857
+ );
3858
+ } else if (build && !build.ok) {
3859
+ lines.push(
3860
+ `\n⚠ transcribe-cli build failed (${build.stage}). The libraries + model are installed;`,
3861
+ " the CLI is not. To finish, either:",
3862
+ " • fix the toolchain and re-run this verb; OR",
3863
+ " • build transcribe-cli yourself (the compile command tried is printed above),",
3864
+ " point TRANSCRIBE_CPP_BIN at the result, and re-run; OR",
3865
+ " • use the remote provider: parachute-vault transcription install --provider scribe-http",
3866
+ );
3867
+ } else {
3868
+ // Build skipped (no libs, or an operator-provided binary that isn't present).
3869
+ lines.push(
3870
+ "\n⚠ No runnable transcribe-cli is available yet. To finish, either:",
3871
+ ` • re-run this verb so it builds a transcribe-cli against ${paths.libsDir} (needs a C++ compiler); OR`,
3872
+ " • point TRANSCRIBE_CPP_BIN at a transcribe-cli you built, and re-run; OR",
3873
+ " • use the remote provider: parachute-vault transcription install --provider scribe-http",
3874
+ );
3875
+ }
3876
+ lines.push(" Your current provider is unchanged — check with `parachute-vault transcription status`.");
3877
+ return lines.join("\n");
3878
+ }
3879
+
3880
+ /** `parachute-vault transcription status` — provider + install state. */
3881
+ function cmdTranscriptionStatus(): void {
3882
+ const active = resolveTranscriptionProviderName();
3883
+ console.log(`Configured provider: ${active}`);
3884
+ const paths = resolveTranscribeCppPaths();
3885
+ const manifest = readManifest(paths.manifestPath);
3886
+ const cliPresent = existsSync(paths.binPath);
3887
+ const installed = transcribeCppInstalled(paths); // runnable CLI + model both present
3888
+ console.log(`transcribe-cpp runnable: ${installed ? "yes" : "no"}`);
3889
+ if (manifest) {
3890
+ console.log(` model: ${manifest.model} (${manifest.modelFile})`);
3891
+ console.log(
3892
+ ` libs: ${paths.libsDir}${manifest.libFiles?.length ? ` (${manifest.libFiles.length} file(s))` : ""}`,
3893
+ );
3894
+ console.log(
3895
+ cliPresent
3896
+ ? ` cli: ${paths.binPath}${manifest.binBuiltFrom ? ` (built from source @ ${manifest.binBuiltFrom.slice(0, 12)})` : ""}`
3897
+ : ` cli: NOT FOUND — re-run \`transcription install\` to build one (needs a C++ compiler), or set TRANSCRIBE_CPP_BIN`,
3898
+ );
3899
+ console.log(` ram: ${manifest.ram_gb}GB at install (${manifest.os}/${manifest.arch})`);
3900
+ } else {
3901
+ console.log(` (nothing installed — run: parachute-vault transcription install)`);
3902
+ }
3903
+ if (active === "transcribe-cpp" && !installed) {
3904
+ console.log(
3905
+ "\n⚠ provider is transcribe-cpp but no runnable transcribe-cli is installed — transcription is offline until a CLI is available (TRANSCRIBE_CPP_BIN, or a build from source).",
3906
+ );
3907
+ }
3908
+ console.log(`\nAvailable models (override with --model): ${Object.values(MODELS).map((m: ModelChoice) => m.name).join(", ")}`);
3909
+ }
3910
+
3499
3911
  // ---------------------------------------------------------------------------
3500
3912
  // Schema maintenance — `parachute-vault schema <subcommand>`
3501
3913
  // ---------------------------------------------------------------------------
@@ -3962,11 +4374,14 @@ async function createVault(
3962
4374
  // row is written — vault is a pure hub resource-server post-0.5.0.
3963
4375
  const seedStore = getVaultStore(name);
3964
4376
 
3965
- // Seed the in-vault onboarding guide (Getting Started + Surface Starter) so a
3966
- // connected AI can read it and help the operator set the vault up. Idempotent
3967
- // + best-effort: only writes notes that are absent, and never fails the
3968
- // create. Runs BEFORE the mirror bootstrap so the seeded notes land in the
3969
- // initial mirror commit. See src/onboarding-seed.ts + core/src/onboarding.ts.
4377
+ // Seed the default packs (welcome web + capture tag, Getting Started guide)
4378
+ // so the first minute shows a small living graph and a connected AI can read
4379
+ // the guide and help the operator set the vault up. Surface Starter is NOT
4380
+ // default-seeded `parachute-vault add-pack surface-starter` adds it later.
4381
+ // Idempotent + best-effort: only writes notes that are absent, and never
4382
+ // fails the create. Runs BEFORE the mirror bootstrap so the seeded content
4383
+ // lands in the initial mirror commit. See src/onboarding-seed.ts +
4384
+ // core/src/seed-packs.ts.
3970
4385
  await seedOnboardingNotesBestEffort(seedStore);
3971
4386
 
3972
4387
  // Default new vaults to an internal live mirror (local git backup of the
@@ -4088,6 +4503,99 @@ function installMcpConfig(opts: InstallMcpConfigOpts): void {
4088
4503
  writeFileSync(targetPath, JSON.stringify(config, null, 2) + "\n");
4089
4504
  }
4090
4505
 
4506
+ /**
4507
+ * `parachute-vault add-pack <pack> [--vault <name>]` — apply a named seed
4508
+ * pack (starter notes + tags) to a vault.
4509
+ *
4510
+ * New vaults default-seed the `welcome` + `getting-started` packs at create;
4511
+ * this verb adds the opt-in ones (today: `surface-starter`) — or re-applies a
4512
+ * default pack to an older vault that predates it. Idempotent per item: a
4513
+ * note is created only when no note lives at its path (a re-run never
4514
+ * clobbers an edited note); tag upserts converge on the same rows.
4515
+ *
4516
+ * Unlike `import` (bulk stream writes, vault#323 daemon-busy guard), this is
4517
+ * a handful of tiny writes against a WAL database, so it runs fine next to a
4518
+ * live daemon; a write-lock collision (SQLITE_BUSY) is caught with a
4519
+ * retry-is-safe message instead of pre-emptively demanding a daemon stop.
4520
+ */
4521
+ async function cmdAddPack(args: string[]) {
4522
+ let vaultName = "default";
4523
+ const positional: string[] = [];
4524
+ for (let i = 0; i < args.length; i++) {
4525
+ const arg = args[i]!;
4526
+ if (arg === "--vault") {
4527
+ const v = args[++i];
4528
+ if (!v) {
4529
+ console.error("--vault requires a value.");
4530
+ process.exit(1);
4531
+ }
4532
+ vaultName = v;
4533
+ } else {
4534
+ positional.push(arg);
4535
+ }
4536
+ }
4537
+ const packName = positional[0];
4538
+
4539
+ const { listSeedPacks, getSeedPack, applySeedPack } = await import(
4540
+ "../core/src/seed-packs.ts"
4541
+ );
4542
+
4543
+ const printPacks = () => {
4544
+ console.error("\nAvailable packs:");
4545
+ for (const p of listSeedPacks()) {
4546
+ console.error(` ${p.name.padEnd(17)} ${p.description}`);
4547
+ }
4548
+ };
4549
+
4550
+ if (!packName) {
4551
+ console.error("Usage: parachute-vault add-pack <pack> [--vault <name>]");
4552
+ printPacks();
4553
+ process.exit(1);
4554
+ }
4555
+
4556
+ const pack = getSeedPack(packName);
4557
+ if (!pack) {
4558
+ console.error(`Unknown pack: "${packName}"`);
4559
+ printPacks();
4560
+ process.exit(1);
4561
+ }
4562
+
4563
+ const config = readVaultConfig(vaultName);
4564
+ if (!config) {
4565
+ console.error(`Vault "${vaultName}" not found. Run: parachute-vault create ${vaultName}`);
4566
+ process.exit(1);
4567
+ }
4568
+
4569
+ const store = getVaultStore(vaultName);
4570
+ try {
4571
+ const result = await applySeedPack(store, pack);
4572
+ console.log(`Pack "${pack.name}" applied to vault "${vaultName}":`);
4573
+ for (const path of result.seededNotes) {
4574
+ console.log(` + note ${path}`);
4575
+ }
4576
+ for (const path of result.skippedNotes) {
4577
+ console.log(` = note ${path} (already exists — left untouched)`);
4578
+ }
4579
+ for (const tag of result.tags) {
4580
+ console.log(` ~ tag ${tag} (upserted)`);
4581
+ }
4582
+ if (result.seededNotes.length === 0 && result.tags.length === 0) {
4583
+ console.log(" Nothing to add — everything was already in place.");
4584
+ }
4585
+ } catch (err) {
4586
+ const message = (err as Error)?.message ?? String(err);
4587
+ if (message.includes("SQLITE_BUSY") || (err as { code?: string })?.code === "SQLITE_BUSY") {
4588
+ console.error(
4589
+ `error: the vault database is busy (a running daemon holds the write lock). ` +
4590
+ `add-pack is idempotent — re-run it, or stop the daemon first with: parachute stop vault`,
4591
+ );
4592
+ } else {
4593
+ console.error(`error: failed to apply pack "${pack.name}": ${message}`);
4594
+ }
4595
+ process.exit(1);
4596
+ }
4597
+ }
4598
+
4091
4599
  function usage() {
4092
4600
  console.log(`
4093
4601
  Parachute Vault — self-hosted knowledge graph
@@ -4153,6 +4661,14 @@ Vaults:
4153
4661
  opt-in upgrade). --no-mirror creates a bare vault with no mirror config.
4154
4662
  Operators can flip the server-wide default with 'default_mirror: off' in
4155
4663
  config.yaml (recommended for cloud / disk-constrained boxes).
4664
+ parachute-vault add-pack <pack> [--vault <name>]
4665
+ Add a named seed pack (starter notes + tags) to a vault
4666
+ (default vault: "default"). Run with no arguments to list
4667
+ the available packs. New vaults seed the "welcome" +
4668
+ "getting-started" packs automatically; "surface-starter"
4669
+ is opt-in via this command. Idempotent: notes are only
4670
+ created when absent — a re-run never clobbers an edited
4671
+ note.
4156
4672
  parachute-vault list List all vaults
4157
4673
  parachute-vault remove <name> [--yes] Remove a vault
4158
4674
  parachute-vault mcp-install [--mint|--token <t>]
@@ -4298,6 +4814,25 @@ Schema maintenance:
4298
4814
  Writes through strict enforcement (migrate
4299
4815
  bypass). See \`schema migrate-field --help\`.
4300
4816
 
4817
+ Transcription (scribe-fold):
4818
+ parachute-vault transcription install [--dry-run] [--model <id>] [--force] [--yes]
4819
+ Install the local transcribe-cpp provider — detects
4820
+ OS/arch + RAM, downloads the transcribe.cpp runtime
4821
+ libraries + a RAM-tier-appropriate GGUF model into
4822
+ ~/.parachute/transcription/. NOTE: v0.1.1 ships a library,
4823
+ not a CLI — it activates (sets
4824
+ TRANSCRIPTION_PROVIDER=transcribe-cpp) only when a runnable
4825
+ transcribe-cli is available (TRANSCRIBE_CPP_BIN or a build
4826
+ from source), else it reports the gap. --dry-run prints the
4827
+ pick + footprint without downloading. --model overrides the
4828
+ RAM-tier choice (whisper-tiny.en | whisper-small.en |
4829
+ parakeet-tdt-0.6b-v3). --force re-downloads; --yes skips the
4830
+ confirm. Needs ffmpeg on PATH to transcode audio to 16kHz
4831
+ mono WAV (hinted if missing).
4832
+ parachute-vault transcription install --provider scribe-http
4833
+ Switch back to the remote scribe-http provider (no download).
4834
+ parachute-vault transcription status Show the configured provider + local install state.
4835
+
4301
4836
  ── Advanced / standalone ──────────────────────────────────────────────
4302
4837
 
4303
4838
  Direct daemon controls. For normal use, prefer the Parachute Hub wrappers