@openparachute/vault 0.6.5-rc.9 → 0.6.5

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 (52) hide show
  1. package/.parachute/module.json +1 -0
  2. package/core/src/core.test.ts +7 -7
  3. package/core/src/mcp.ts +1 -1
  4. package/core/src/schema.ts +5 -5
  5. package/core/src/seed-packs.test.ts +222 -56
  6. package/core/src/seed-packs.ts +334 -70
  7. package/core/src/tag-hierarchy.ts +1 -1
  8. package/core/src/tag-schemas.ts +2 -2
  9. package/core/src/types.ts +1 -1
  10. package/package.json +1 -1
  11. package/src/admin-spa.ts +2 -1
  12. package/src/auth.ts +1 -1
  13. package/src/cli.ts +317 -53
  14. package/src/export-watch.ts +1 -1
  15. package/src/live-frame-parity.test.ts +201 -0
  16. package/src/module-manifest.ts +8 -0
  17. package/src/onboarding-seed.test.ts +82 -20
  18. package/src/onboarding-seed.ts +2 -2
  19. package/src/routes.ts +3 -3
  20. package/src/routing.test.ts +2 -2
  21. package/src/routing.ts +18 -6
  22. package/src/self-register.test.ts +19 -0
  23. package/src/self-register.ts +6 -1
  24. package/src/server.ts +56 -0
  25. package/src/services-manifest.ts +8 -0
  26. package/src/subscriptions.ts +100 -42
  27. package/src/tag-scope.ts +3 -3
  28. package/src/test-support/live-frame-corpus.ts +72 -0
  29. package/src/token-store.ts +2 -2
  30. package/src/transcription/build.test.ts +86 -5
  31. package/src/transcription/build.ts +87 -7
  32. package/src/transcription/capability.test.ts +25 -0
  33. package/src/transcription/capability.ts +27 -2
  34. package/src/transcription/download.test.ts +101 -0
  35. package/src/transcription/download.ts +71 -0
  36. package/src/transcription/install-python.test.ts +366 -0
  37. package/src/transcription/install-python.ts +471 -0
  38. package/src/transcription/providers/onnx-asr.test.ts +229 -0
  39. package/src/transcription/providers/onnx-asr.ts +239 -0
  40. package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
  41. package/src/transcription/providers/parakeet-mlx.ts +242 -0
  42. package/src/transcription/select.test.ts +166 -1
  43. package/src/transcription/select.ts +234 -1
  44. package/src/transcription/tiers.test.ts +197 -0
  45. package/src/transcription/tiers.ts +184 -0
  46. package/src/vault-create.test.ts +19 -14
  47. package/src/vault.test.ts +1 -1
  48. package/src/ws-server.ts +408 -0
  49. package/src/ws-subscribe.test.ts +474 -0
  50. package/src/ws-subscribe.ts +242 -0
  51. package/src/subscribe.test.ts +0 -609
  52. package/src/subscribe.ts +0 -248
package/src/cli.ts CHANGED
@@ -152,8 +152,20 @@ import {
152
152
  resolveTranscribeCppPaths,
153
153
  resolveTranscriptionProviderName,
154
154
  transcribeCppInstalled,
155
+ probeTranscribeCliRunnable,
155
156
  readManifest,
157
+ transcriptionHomeDir,
158
+ pythonVenvDir,
159
+ pythonManifestPath,
160
+ readPythonManifest,
161
+ resolveParakeetMlxBin,
162
+ resolveParakeetMlxModel,
163
+ resolveOnnxAsrBin,
164
+ resolveOnnxAsrModel,
165
+ parakeetMlxInstalled,
166
+ onnxAsrInstalled,
156
167
  type TranscribeCppManifest,
168
+ type PythonInstallManifest,
157
169
  } from "./transcription/select.ts";
158
170
  import {
159
171
  buildTranscribeCli,
@@ -162,6 +174,14 @@ import {
162
174
  TRANSCRIBE_CPP_SOURCE_REF,
163
175
  type CliBuildResult,
164
176
  } from "./transcription/build.ts";
177
+ import { downloadTo } from "./transcription/download.ts";
178
+ import { selectDefaultProvider, NOMINAL_SLACK_GB, type TierPlan } from "./transcription/tiers.ts";
179
+ import {
180
+ PYTHON_PROVIDERS,
181
+ buildDefaultPythonDeps,
182
+ installPythonBackend,
183
+ type PythonProviderName,
184
+ } from "./transcription/install-python.ts";
165
185
 
166
186
  // ---------------------------------------------------------------------------
167
187
  // Argument parsing
@@ -3529,17 +3549,23 @@ function printHistoryUsage(): void {
3529
3549
  // ---------------------------------------------------------------------------
3530
3550
 
3531
3551
  /**
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).
3552
+ * `parachute-vault transcription install` — set up a LOCAL transcription
3553
+ * provider. With no `--provider`, the ratified RAM/arch/OS tier table
3554
+ * (scribe-fold Phase 2b, `tiers.ts`) picks the host's default:
3555
+ *
3556
+ * - macOS Apple Silicon 8GB+ parakeet-mlx (Python venv, MLX)
3557
+ * - Linux 4GB+ → onnx-asr (Python venv, int8 ONNX)
3558
+ * - Linux ~2GB → transcribe-cpp + small whisper GGUF
3559
+ * - ~1GB / below floor → remote steer (scribe-http), never forced
3560
+ *
3561
+ * The pick + footprint is printed and the operator confirms (or overrides
3562
+ * with `--provider`/`--model`). Every path is idempotent + non-fatal, and
3563
+ * activation is HONEST: `TRANSCRIPTION_PROVIDER` flips only when a runnable
3564
+ * binary actually exists (PRs #532/#533 precedent). `--dry-run`/`--plan`
3565
+ * prints the selection without downloading.
3540
3566
  *
3541
3567
  * `parachute-vault transcription status` — show the configured provider +
3542
- * install state.
3568
+ * per-provider install state.
3543
3569
  */
3544
3570
  async function cmdTranscription(args: string[]) {
3545
3571
  const sub = args[0];
@@ -3548,11 +3574,13 @@ async function cmdTranscription(args: string[]) {
3548
3574
  return;
3549
3575
  }
3550
3576
  if (sub === "status" || sub === undefined) {
3551
- cmdTranscriptionStatus();
3577
+ await cmdTranscriptionStatus();
3552
3578
  return;
3553
3579
  }
3554
3580
  console.error(`Unknown transcription subcommand: ${sub}`);
3555
- console.error("Usage: parachute-vault transcription install [--dry-run] [--model <id>] [--provider <name>]");
3581
+ console.error(
3582
+ "Usage: parachute-vault transcription install [--dry-run] [--model <id>] [--provider <transcribe-cpp|parakeet-mlx|onnx-asr|scribe-http>]",
3583
+ );
3556
3584
  console.error(" parachute-vault transcription status");
3557
3585
  process.exit(1);
3558
3586
  }
@@ -3576,9 +3604,9 @@ async function cmdTranscriptionInstall(args: string[]) {
3576
3604
  const force = args.includes("--force");
3577
3605
  const yes = args.includes("--yes") || args.includes("-y");
3578
3606
  const overrideModel = takeArgValue(args, "--model").value;
3579
- const provider = takeArgValue(args, "--provider").value ?? "transcribe-cpp";
3607
+ const providerArg = takeArgValue(args, "--provider").value;
3580
3608
 
3581
- if (provider === "scribe-http") {
3609
+ if (providerArg === "scribe-http") {
3582
3610
  // Just flip config back to the remote provider — no download.
3583
3611
  if (dryRun) {
3584
3612
  console.log("Would set TRANSCRIPTION_PROVIDER=scribe-http in the vault .env (remote provider; no download).");
@@ -3588,11 +3616,162 @@ async function cmdTranscriptionInstall(args: string[]) {
3588
3616
  console.log("Set TRANSCRIPTION_PROVIDER=scribe-http. Restart vault to apply. (Remote provider — no local binary/model needed.)");
3589
3617
  return;
3590
3618
  }
3591
- if (provider !== "transcribe-cpp") {
3592
- console.error(`Unknown --provider "${provider}". Valid: transcribe-cpp, scribe-http.`);
3619
+
3620
+ let provider: string;
3621
+ let tierModel: string | undefined;
3622
+ if (providerArg) {
3623
+ if (!["transcribe-cpp", "parakeet-mlx", "onnx-asr"].includes(providerArg)) {
3624
+ console.error(
3625
+ `Unknown --provider "${providerArg}". Valid: transcribe-cpp, parakeet-mlx, onnx-asr, scribe-http.`,
3626
+ );
3627
+ process.exit(1);
3628
+ }
3629
+ provider = providerArg;
3630
+ } else {
3631
+ // Auto-default: the ratified RAM/arch/OS tier table (tiers.ts). Probe the
3632
+ // host, print the pick + footprint; the operator confirms below (or
3633
+ // re-runs with --provider/--model to override).
3634
+ const tier = selectDefaultProvider({
3635
+ platform: process.platform,
3636
+ arch: process.arch,
3637
+ totalRamBytes: detectTotalRamBytes(),
3638
+ });
3639
+ printTierPick(tier);
3640
+ if (tier.provider === "scribe-http") {
3641
+ // Remote steer — never force a local install on a below-tier box.
3642
+ console.log("\nNo local install performed. To point at a remote provider now:");
3643
+ console.log(" parachute-vault transcription install --provider scribe-http");
3644
+ if (tier.localOptIn) {
3645
+ console.log(`\nLocal opt-in (explicit only — not auto-installed): ${tier.localOptIn.note}`);
3646
+ }
3647
+ process.exit(dryRun ? 0 : 1);
3648
+ }
3649
+ provider = tier.provider;
3650
+ tierModel = tier.model;
3651
+ console.log("");
3652
+ }
3653
+
3654
+ if (provider === "transcribe-cpp") {
3655
+ // The tier's model pick (e.g. the Linux ~2GB whisper-small tier) seeds the
3656
+ // transcribe-cpp flow; an explicit --model always wins.
3657
+ await runTranscribeCppInstall({ dryRun, force, yes, overrideModel: overrideModel ?? tierModel });
3658
+ return;
3659
+ }
3660
+ await runPythonProviderInstall(provider as PythonProviderName, {
3661
+ dryRun,
3662
+ force,
3663
+ yes,
3664
+ overrideModel,
3665
+ });
3666
+ }
3667
+
3668
+ /** Print the tier-table pick for this host (the install auto-default). */
3669
+ function printTierPick(tier: TierPlan): void {
3670
+ console.log(`Host: ${tier.platform}/${tier.arch}, ${tier.totalRamGb}GB RAM`);
3671
+ console.log(`Tier default: ${tier.provider}${tier.model ? ` (${tier.model})` : ""}`);
3672
+ console.log(` ${tier.reason}`);
3673
+ if (tier.approxDiskMb) {
3674
+ console.log(` ~${tier.approxDiskMb}MB download${tier.peakRamNote ? `, ${tier.peakRamNote}` : ""}`);
3675
+ }
3676
+ console.log(" (override with --provider <transcribe-cpp|parakeet-mlx|onnx-asr|scribe-http>)");
3677
+ }
3678
+
3679
+ /**
3680
+ * Install a Python-based local provider (parakeet-mlx / onnx-asr) into the
3681
+ * managed venv under `~/.parachute/transcription/venv/` and — honestly —
3682
+ * activate it only when a runnable binary verified (`install-python.ts` owns
3683
+ * the apt/venv/warm-pull steps; activation + manifest stay here, mirroring
3684
+ * the transcribe-cpp flow).
3685
+ */
3686
+ async function runPythonProviderInstall(
3687
+ provider: PythonProviderName,
3688
+ opts: { dryRun: boolean; force: boolean; yes: boolean; overrideModel?: string },
3689
+ ): Promise<void> {
3690
+ const spec = PYTHON_PROVIDERS[provider];
3691
+ const venv = pythonVenvDir();
3692
+ // `--model` wins; else env/manifest/ratified default. The pick lands in the
3693
+ // install manifest, which the runtime model resolution reads back.
3694
+ const model =
3695
+ opts.overrideModel ??
3696
+ (provider === "parakeet-mlx" ? resolveParakeetMlxModel() : resolveOnnxAsrModel());
3697
+ const totalRamGb = Math.round((detectTotalRamBytes() / 2 ** 30) * 10) / 10;
3698
+
3699
+ console.log(`Transcription install plan (${provider} — local, Python venv):\n`);
3700
+ console.log(`Host: ${process.platform}/${process.arch}, ${totalRamGb}GB RAM`);
3701
+ console.log(`Package: ${spec.pipTarget} → ${venv}`);
3702
+ console.log(`Model: ${model}`);
3703
+ console.log(` ~${spec.approxDiskMb}MB model download, ${spec.peakRamNote}`);
3704
+
3705
+ if (opts.dryRun) {
3706
+ // Preview the guards the real run would hit — pure, no side effects.
3707
+ if (spec.platform && spec.platform !== process.platform) {
3708
+ console.log(`\n Refused: ${provider} only runs on ${spec.platform}; this host is ${process.platform}.`);
3709
+ } else if (spec.arch && spec.arch !== process.arch) {
3710
+ console.log(`\n Refused: ${provider} needs ${spec.arch} (Apple Silicon); this host is ${process.arch}.`);
3711
+ } else if (totalRamGb < spec.minRamGb - NOMINAL_SLACK_GB && !opts.force) {
3712
+ console.log(`\n Refused: ${totalRamGb}GB RAM < the ~${spec.minRamGb}GB ${provider} floor. ${spec.ramFloorRationale}`);
3713
+ }
3714
+ console.log("\n(--dry-run) Nothing installed.");
3715
+ return;
3716
+ }
3717
+
3718
+ if (!opts.yes) {
3719
+ const ok = await confirm(
3720
+ `Install ${spec.pipTarget} into ${venv} (model ~${spec.approxDiskMb}MB ${provider === "parakeet-mlx" ? "downloads on first use" : "warm-pulled now"})?`,
3721
+ true,
3722
+ );
3723
+ if (!ok) {
3724
+ console.log("Aborted.");
3725
+ return;
3726
+ }
3727
+ }
3728
+
3729
+ const outcome = await installPythonBackend(buildDefaultPythonDeps(), {
3730
+ provider,
3731
+ force: opts.force,
3732
+ model,
3733
+ });
3734
+
3735
+ if (!outcome.ok || !outcome.binPath) {
3736
+ console.error(`\n✗ ${outcome.summary}`);
3737
+ console.error(
3738
+ "Your current provider is unchanged — the verb is idempotent; fix the issue and re-run. Or use the remote provider: parachute-vault transcription install --provider scribe-http",
3739
+ );
3593
3740
  process.exit(1);
3594
3741
  }
3595
3742
 
3743
+ // Manifest + honest activation (a runnable binary exists — verified above).
3744
+ const manifest: PythonInstallManifest = {
3745
+ provider,
3746
+ pipTarget: spec.pipTarget,
3747
+ bin: outcome.binPath,
3748
+ venv: outcome.venv ?? "",
3749
+ model,
3750
+ os: process.platform,
3751
+ arch: process.arch,
3752
+ ram_gb: totalRamGb,
3753
+ installedAt: new Date().toISOString(),
3754
+ };
3755
+ mkdirSync(transcriptionHomeDir(), { recursive: true });
3756
+ writeFileSync(pythonManifestPath(), JSON.stringify(manifest, null, 2));
3757
+
3758
+ setEnvVar("TRANSCRIPTION_PROVIDER", provider);
3759
+ console.log(`\n✓ Installed and activated → TRANSCRIPTION_PROVIDER=${provider} set in the vault .env.`);
3760
+ console.log(` Binary: ${outcome.binPath}`);
3761
+ console.log(` Model: ${model}`);
3762
+ if (provider === "parakeet-mlx") {
3763
+ console.log(` NOTE: the model (~${spec.approxDiskMb}MB) downloads into the HuggingFace cache on the first transcription.`);
3764
+ }
3765
+ console.log(` Restart vault to activate: parachute restart vault`);
3766
+ }
3767
+
3768
+ async function runTranscribeCppInstall(opts: {
3769
+ dryRun: boolean;
3770
+ force: boolean;
3771
+ yes: boolean;
3772
+ overrideModel?: string;
3773
+ }) {
3774
+ const { dryRun, force, yes, overrideModel } = opts;
3596
3775
  const plan = planInstall({
3597
3776
  platform: process.platform,
3598
3777
  arch: process.arch,
@@ -3690,14 +3869,25 @@ async function cmdTranscriptionInstall(args: string[]) {
3690
3869
 
3691
3870
  // 5) CLI resolution + provider switch — honest, no silent success. `binPath`
3692
3871
  // 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.
3872
+ // transcribe-cpp only when one actually EXECUTES (`--help` exits 0
3873
+ // vault#534: an existsSync-only check activated Linux installs whose CLI
3874
+ // exited 1 on every run). Otherwise we keep the current provider and
3875
+ // report the gap rather than taking transcription offline behind a
3876
+ // provider that can't run.
3696
3877
  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`);
3878
+ const probe = await probeTranscribeCliRunnable(paths.binPath);
3879
+ if (probe.ok) {
3880
+ setEnvVar("TRANSCRIPTION_PROVIDER", "transcribe-cpp");
3881
+ console.log(`\n✓ Installed and activated TRANSCRIPTION_PROVIDER=transcribe-cpp set in the vault .env.`);
3882
+ console.log(` CLI: ${paths.binPath} (verified runnable)`);
3883
+ console.log(` Restart vault to activate: parachute restart vault`);
3884
+ } else {
3885
+ console.log(
3886
+ `\n⚠ transcribe-cli exists at ${paths.binPath} but is NOT runnable (${probe.reason}).`,
3887
+ );
3888
+ console.log(" NOT activating transcribe-cpp — your current provider is unchanged.");
3889
+ console.log(noRunnableCliGuidance(paths, buildResult));
3890
+ }
3701
3891
  } else {
3702
3892
  console.log(`\n✓ Libraries + model installed to ${paths.dir} — transcription is NOT yet activated.`);
3703
3893
  console.log(noRunnableCliGuidance(paths, buildResult));
@@ -3709,13 +3899,6 @@ async function cmdTranscriptionInstall(args: string[]) {
3709
3899
  }
3710
3900
  }
3711
3901
 
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
3902
  /**
3720
3903
  * Download the release tarball, extract it, and place its runtime shared
3721
3904
  * libraries (libtranscribe + libggml `.dylib`/`.so`) into `paths.libsDir`,
@@ -3807,6 +3990,8 @@ async function maybeBuildTranscribeCli(
3807
3990
  await downloadTo(cliSourceUrl(rel), dest);
3808
3991
  }
3809
3992
  },
3993
+ readFile: (p) => readFileSync(p, "utf8"),
3994
+ writeFile: (p, content) => writeFileSync(p, content),
3810
3995
  compile: (cmd) => {
3811
3996
  const p = Bun.spawnSync(cmd);
3812
3997
  return { exitCode: p.exitCode ?? 1, stderr: new TextDecoder().decode(p.stderr) };
@@ -3855,6 +4040,14 @@ function noRunnableCliGuidance(
3855
4040
  " • build transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
3856
4041
  " • use the remote provider: parachute-vault transcription install --provider scribe-http",
3857
4042
  );
4043
+ } else if (build && !build.ok && build.stage === "patch") {
4044
+ lines.push(
4045
+ `\n⚠ Could not apply the vault#534 backend-init source patch — ${build.message}`,
4046
+ " The pinned upstream source no longer matches the patch anchor. This needs a code fix",
4047
+ " (re-anchor or drop the patch), not a toolchain fix. Until then, either:",
4048
+ " • build a transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
4049
+ " • use the remote provider: parachute-vault transcription install --provider scribe-http",
4050
+ );
3858
4051
  } else if (build && !build.ok) {
3859
4052
  lines.push(
3860
4053
  `\n⚠ transcribe-cli build failed (${build.stage}). The libraries + model are installed;`,
@@ -3877,15 +4070,39 @@ function noRunnableCliGuidance(
3877
4070
  return lines.join("\n");
3878
4071
  }
3879
4072
 
3880
- /** `parachute-vault transcription status` — provider + install state. */
3881
- function cmdTranscriptionStatus(): void {
4073
+ /** `parachute-vault transcription status` — provider + per-provider install state. */
4074
+ async function cmdTranscriptionStatus(): Promise<void> {
3882
4075
  const active = resolveTranscriptionProviderName();
3883
4076
  console.log(`Configured provider: ${active}`);
4077
+
4078
+ // What the ratified tier table would pick for this host (install default).
4079
+ const tier = selectDefaultProvider({
4080
+ platform: process.platform,
4081
+ arch: process.arch,
4082
+ totalRamBytes: detectTotalRamBytes(),
4083
+ });
4084
+ console.log(
4085
+ `Tier default for this host (${tier.platform}/${tier.arch}, ${tier.totalRamGb}GB): ${tier.provider}${tier.model ? ` (${tier.model})` : ""}`,
4086
+ );
4087
+
4088
+ // transcribe-cpp — prebuilt libs + GGUF model + built CLI. "runnable" is an
4089
+ // EXECUTED verdict, not a stat: `--help` must actually exit 0 (vault#534 —
4090
+ // an existsSync-only check said "yes" on Linux while the CLI exited 1 on
4091
+ // every real transcription because its dlopen'd CPU backends never loaded).
3884
4092
  const paths = resolveTranscribeCppPaths();
3885
4093
  const manifest = readManifest(paths.manifestPath);
3886
4094
  const cliPresent = existsSync(paths.binPath);
3887
- const installed = transcribeCppInstalled(paths); // runnable CLI + model both present
3888
- console.log(`transcribe-cpp runnable: ${installed ? "yes" : "no"}`);
4095
+ const filesPresent = transcribeCppInstalled(paths); // CLI + model both on disk
4096
+ let installed = false;
4097
+ let notRunnableReason: string | undefined;
4098
+ if (!filesPresent) {
4099
+ notRunnableReason = !cliPresent ? "no transcribe-cli binary" : "no GGUF model on disk";
4100
+ } else {
4101
+ const probe = await probeTranscribeCliRunnable(paths.binPath);
4102
+ installed = probe.ok;
4103
+ notRunnableReason = probe.reason;
4104
+ }
4105
+ console.log(`\ntranscribe-cpp runnable: ${installed ? "yes" : `no (${notRunnableReason})`}`);
3889
4106
  if (manifest) {
3890
4107
  console.log(` model: ${manifest.model} (${manifest.modelFile})`);
3891
4108
  console.log(
@@ -3898,14 +4115,56 @@ function cmdTranscriptionStatus(): void {
3898
4115
  );
3899
4116
  console.log(` ram: ${manifest.ram_gb}GB at install (${manifest.os}/${manifest.arch})`);
3900
4117
  } else {
3901
- console.log(` (nothing installed — run: parachute-vault transcription install)`);
4118
+ console.log(` (not installed — run: parachute-vault transcription install --provider transcribe-cpp)`);
3902
4119
  }
3903
- if (active === "transcribe-cpp" && !installed) {
4120
+
4121
+ // parakeet-mlx / onnx-asr — Python venv providers (scribe-fold Phase 2b).
4122
+ const pyManifest = readPythonManifest(pythonManifestPath());
4123
+ const pkBin = resolveParakeetMlxBin();
4124
+ const pkRunnable = parakeetMlxInstalled();
4125
+ console.log(`\nparakeet-mlx runnable: ${pkRunnable ? "yes" : "no"}`);
4126
+ if (pkRunnable) {
4127
+ console.log(` bin: ${pkBin}`);
4128
+ console.log(` model: ${resolveParakeetMlxModel()} (HF cache; downloads on first use)`);
4129
+ } else {
3904
4130
  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).",
4131
+ process.platform === "darwin" && process.arch === "arm64"
4132
+ ? ` (not installed — run: parachute-vault transcription install${tier.provider === "parakeet-mlx" ? "" : " --provider parakeet-mlx"})`
4133
+ : " (macOS Apple Silicon only)",
3906
4134
  );
3907
4135
  }
3908
- console.log(`\nAvailable models (override with --model): ${Object.values(MODELS).map((m: ModelChoice) => m.name).join(", ")}`);
4136
+
4137
+ const oxBin = resolveOnnxAsrBin();
4138
+ const oxRunnable = onnxAsrInstalled();
4139
+ console.log(`onnx-asr runnable: ${oxRunnable ? "yes" : "no"}`);
4140
+ if (oxRunnable) {
4141
+ console.log(` bin: ${oxBin}`);
4142
+ console.log(` model: ${resolveOnnxAsrModel()} (HF cache)`);
4143
+ } else {
4144
+ console.log(
4145
+ ` (not installed — run: parachute-vault transcription install${tier.provider === "onnx-asr" ? "" : " --provider onnx-asr"})`,
4146
+ );
4147
+ }
4148
+ if (pyManifest) {
4149
+ console.log(
4150
+ ` python install: ${pyManifest.provider} (${pyManifest.pipTarget}) @ ${pyManifest.installedAt}${pyManifest.venv ? ` — venv ${pyManifest.venv}` : ""}`,
4151
+ );
4152
+ }
4153
+
4154
+ // Offline warning when the ACTIVE provider isn't runnable.
4155
+ const activeRunnable =
4156
+ active === "scribe-http" ||
4157
+ (active === "transcribe-cpp" && installed) ||
4158
+ (active === "parakeet-mlx" && pkRunnable) ||
4159
+ (active === "onnx-asr" && oxRunnable);
4160
+ if (!activeRunnable) {
4161
+ console.log(
4162
+ `\n⚠ provider is ${active} but no runnable ${active} install was found — transcription is offline until one is available (\`parachute-vault transcription install\`).`,
4163
+ );
4164
+ }
4165
+ console.log(
4166
+ `\nAvailable transcribe-cpp models (override with --model): ${Object.values(MODELS).map((m: ModelChoice) => m.name).join(", ")}`,
4167
+ );
3909
4168
  }
3910
4169
 
3911
4170
  // ---------------------------------------------------------------------------
@@ -4816,22 +5075,27 @@ Schema maintenance:
4816
5075
 
4817
5076
  Transcription (scribe-fold):
4818
5077
  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.
5078
+ Install this host's tier-default LOCAL provider (probes
5079
+ OS/arch + RAM, prints the pick + footprint, confirms):
5080
+ macOS Apple Silicon 8GB+ parakeet-mlx (Python venv, MLX)
5081
+ Linux 4GB+ → onnx-asr (Python venv, int8 ONNX)
5082
+ Linux ~2GB → transcribe-cpp + small whisper GGUF
5083
+ ~1GB / below floor → remote steer (never forced)
5084
+ Activation is honest: TRANSCRIPTION_PROVIDER flips only when
5085
+ a runnable binary exists. Idempotent; --dry-run prints the
5086
+ pick without downloading; --force re-downloads / bypasses the
5087
+ RAM floor on an explicit --provider; --yes skips the confirm.
5088
+ parachute-vault transcription install --provider <name>
5089
+ Override the tier default. transcribe-cpp downloads the
5090
+ runtime libs + a GGUF (--model overrides the RAM-tier pick:
5091
+ whisper-tiny.en | whisper-small.en | parakeet-tdt-0.6b-v3;
5092
+ builds transcribe-cli from source needs a C++ compiler).
5093
+ parakeet-mlx / onnx-asr install a Python venv under
5094
+ ~/.parachute/transcription/venv (needs python3; apt-installs
5095
+ python3/ffmpeg on Linux). scribe-http switches back to the
5096
+ remote provider (no download).
5097
+ parachute-vault transcription status Show the configured provider, this host's tier default, and
5098
+ per-provider install state.
4835
5099
 
4836
5100
  ── Advanced / standalone ──────────────────────────────────────────────
4837
5101
 
@@ -10,7 +10,7 @@
10
10
  * cursor captured at the *start* of the previous cycle. Vault writes are
11
11
  * HTTP-mediated and don't surface to filesystem watchers (the bun:sqlite DB
12
12
  * is opaque), so polling on `updated_at >= cursor` is the simplest robust
13
- * detection. See `parachute-patterns/cookbook/vault-portable-export.md`.
13
+ * detection. See `docs/contracts/vault-portable-export.md`.
14
14
  */
15
15
 
16
16
  import { ensureGitAvailable } from "./git-preflight.ts";