@openparachute/vault 0.6.5-rc.2 → 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.
- package/.parachute/module.json +1 -0
- package/core/src/core.test.ts +7 -7
- package/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/mcp.ts +1 -1
- package/core/src/notes.ts +33 -15
- package/core/src/onboarding.ts +14 -296
- package/core/src/schema.ts +5 -5
- package/core/src/seed-packs.test.ts +357 -0
- package/core/src/seed-packs.ts +823 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/tag-hierarchy.ts +1 -1
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/types.ts +1 -1
- package/core/src/vault-projection.ts +1 -1
- package/core/src/wikilinks.ts +10 -4
- package/package.json +1 -1
- package/src/add-pack.test.ts +142 -0
- package/src/admin-spa.ts +2 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +806 -7
- package/src/export-watch.ts +1 -1
- package/src/live-frame-parity.test.ts +201 -0
- package/src/module-manifest.ts +8 -0
- package/src/onboarding-seed.test.ts +188 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +20 -3
- package/src/routing.test.ts +2 -2
- package/src/routing.ts +18 -6
- package/src/self-register.test.ts +19 -0
- package/src/self-register.ts +6 -1
- package/src/server.ts +133 -31
- package/src/services-manifest.ts +8 -0
- package/src/subscriptions.ts +100 -42
- package/src/tag-scope.ts +3 -3
- package/src/test-support/live-frame-corpus.ts +72 -0
- package/src/token-store.ts +2 -2
- package/src/transcription/build.test.ts +305 -0
- package/src/transcription/build.ts +332 -0
- package/src/transcription/capability.test.ts +118 -0
- package/src/transcription/capability.ts +96 -0
- package/src/transcription/download.test.ts +101 -0
- package/src/transcription/download.ts +71 -0
- package/src/transcription/install-python.test.ts +366 -0
- package/src/transcription/install-python.ts +471 -0
- package/src/transcription/install.test.ts +167 -0
- package/src/transcription/install.ts +296 -0
- package/src/transcription/providers/onnx-asr.test.ts +229 -0
- package/src/transcription/providers/onnx-asr.ts +239 -0
- package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
- package/src/transcription/providers/parakeet-mlx.ts +242 -0
- package/src/transcription/providers/scribe-http.test.ts +195 -0
- package/src/transcription/providers/scribe-http.ts +144 -0
- package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
- package/src/transcription/providers/transcribe-cpp.ts +293 -0
- package/src/transcription/select.test.ts +299 -0
- package/src/transcription/select.ts +397 -0
- package/src/transcription/tiers.test.ts +197 -0
- package/src/transcription/tiers.ts +184 -0
- package/src/transcription-worker.test.ts +44 -0
- package/src/transcription-worker.ts +57 -122
- package/src/vault-create.test.ts +43 -10
- package/src/vault.test.ts +49 -1
- package/src/ws-server.ts +408 -0
- package/src/ws-subscribe.test.ts +474 -0
- package/src/ws-subscribe.ts +242 -0
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -248
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,48 @@ 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
|
+
probeTranscribeCliRunnable,
|
|
156
|
+
readManifest,
|
|
157
|
+
transcriptionHomeDir,
|
|
158
|
+
pythonVenvDir,
|
|
159
|
+
pythonManifestPath,
|
|
160
|
+
readPythonManifest,
|
|
161
|
+
resolveParakeetMlxBin,
|
|
162
|
+
resolveParakeetMlxModel,
|
|
163
|
+
resolveOnnxAsrBin,
|
|
164
|
+
resolveOnnxAsrModel,
|
|
165
|
+
parakeetMlxInstalled,
|
|
166
|
+
onnxAsrInstalled,
|
|
167
|
+
type TranscribeCppManifest,
|
|
168
|
+
type PythonInstallManifest,
|
|
169
|
+
} from "./transcription/select.ts";
|
|
170
|
+
import {
|
|
171
|
+
buildTranscribeCli,
|
|
172
|
+
cliSourceUrl,
|
|
173
|
+
compilerInstallHint,
|
|
174
|
+
TRANSCRIBE_CPP_SOURCE_REF,
|
|
175
|
+
type CliBuildResult,
|
|
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";
|
|
143
185
|
|
|
144
186
|
// ---------------------------------------------------------------------------
|
|
145
187
|
// Argument parsing
|
|
@@ -181,6 +223,9 @@ switch (command) {
|
|
|
181
223
|
case "create":
|
|
182
224
|
await cmdCreate(cmdArgs);
|
|
183
225
|
break;
|
|
226
|
+
case "add-pack":
|
|
227
|
+
await cmdAddPack(cmdArgs);
|
|
228
|
+
break;
|
|
184
229
|
case "list":
|
|
185
230
|
case "ls":
|
|
186
231
|
cmdList();
|
|
@@ -246,6 +291,9 @@ switch (command) {
|
|
|
246
291
|
case "schema":
|
|
247
292
|
await cmdSchema(cmdArgs);
|
|
248
293
|
break;
|
|
294
|
+
case "transcription":
|
|
295
|
+
await cmdTranscription(cmdArgs);
|
|
296
|
+
break;
|
|
249
297
|
case "help":
|
|
250
298
|
case "--help":
|
|
251
299
|
case "-h":
|
|
@@ -3496,6 +3544,629 @@ function printHistoryUsage(): void {
|
|
|
3496
3544
|
console.error(" --json Emit the history as JSON instead of the human-readable list");
|
|
3497
3545
|
}
|
|
3498
3546
|
|
|
3547
|
+
// ---------------------------------------------------------------------------
|
|
3548
|
+
// Transcription — `parachute-vault transcription <subcommand>` (scribe-fold 2a)
|
|
3549
|
+
// ---------------------------------------------------------------------------
|
|
3550
|
+
|
|
3551
|
+
/**
|
|
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.
|
|
3566
|
+
*
|
|
3567
|
+
* `parachute-vault transcription status` — show the configured provider +
|
|
3568
|
+
* per-provider install state.
|
|
3569
|
+
*/
|
|
3570
|
+
async function cmdTranscription(args: string[]) {
|
|
3571
|
+
const sub = args[0];
|
|
3572
|
+
if (sub === "install") {
|
|
3573
|
+
await cmdTranscriptionInstall(args.slice(1));
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
if (sub === "status" || sub === undefined) {
|
|
3577
|
+
await cmdTranscriptionStatus();
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
console.error(`Unknown transcription subcommand: ${sub}`);
|
|
3581
|
+
console.error(
|
|
3582
|
+
"Usage: parachute-vault transcription install [--dry-run] [--model <id>] [--provider <transcribe-cpp|parakeet-mlx|onnx-asr|scribe-http>]",
|
|
3583
|
+
);
|
|
3584
|
+
console.error(" parachute-vault transcription status");
|
|
3585
|
+
process.exit(1);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
/** Render an install plan for the human (shared by --dry-run + the real run). */
|
|
3589
|
+
function printTranscriptionPlan(plan: InstallPlan): void {
|
|
3590
|
+
console.log(`Host: ${plan.platform}/${plan.arch}, ${plan.totalRamGb}GB RAM`);
|
|
3591
|
+
if (!plan.supported) {
|
|
3592
|
+
console.log(`\n ${plan.refused ? "Refused" : "Unsupported"}: ${plan.reason}`);
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
const m = plan.model!;
|
|
3596
|
+
console.log(`Binary: ${plan.asset!.asset}`);
|
|
3597
|
+
console.log(`Model: ${m.name} (${m.file})`);
|
|
3598
|
+
console.log(` ${m.note}`);
|
|
3599
|
+
console.log(` ~${m.approxSizeMb}MB download, ~${m.approxRuntimeGb}GB peak RAM while transcribing`);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
async function cmdTranscriptionInstall(args: string[]) {
|
|
3603
|
+
const dryRun = args.includes("--dry-run") || args.includes("--plan");
|
|
3604
|
+
const force = args.includes("--force");
|
|
3605
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
3606
|
+
const overrideModel = takeArgValue(args, "--model").value;
|
|
3607
|
+
const providerArg = takeArgValue(args, "--provider").value;
|
|
3608
|
+
|
|
3609
|
+
if (providerArg === "scribe-http") {
|
|
3610
|
+
// Just flip config back to the remote provider — no download.
|
|
3611
|
+
if (dryRun) {
|
|
3612
|
+
console.log("Would set TRANSCRIPTION_PROVIDER=scribe-http in the vault .env (remote provider; no download).");
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
setEnvVar("TRANSCRIPTION_PROVIDER", "scribe-http");
|
|
3616
|
+
console.log("Set TRANSCRIPTION_PROVIDER=scribe-http. Restart vault to apply. (Remote provider — no local binary/model needed.)");
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
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
|
+
);
|
|
3740
|
+
process.exit(1);
|
|
3741
|
+
}
|
|
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;
|
|
3775
|
+
const plan = planInstall({
|
|
3776
|
+
platform: process.platform,
|
|
3777
|
+
arch: process.arch,
|
|
3778
|
+
totalRamBytes: detectTotalRamBytes(),
|
|
3779
|
+
overrideModel,
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
console.log("Transcription install plan (transcribe-cpp — local, no Python):\n");
|
|
3783
|
+
printTranscriptionPlan(plan);
|
|
3784
|
+
|
|
3785
|
+
if (!plan.supported) {
|
|
3786
|
+
// Refused / unsupported is not a hard error — it's a steer to remote.
|
|
3787
|
+
console.log("\nNo local install performed. See the note above.");
|
|
3788
|
+
process.exit(dryRun ? 0 : 1);
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
const paths = resolveTranscribeCppPaths();
|
|
3792
|
+
console.log(`\nInstall dir: ${paths.dir}`);
|
|
3793
|
+
|
|
3794
|
+
if (dryRun) {
|
|
3795
|
+
console.log("\n(--dry-run) No files downloaded.");
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
if (!yes) {
|
|
3800
|
+
const ok = await confirm(`Download ~${plan.footprintMb}MB and install to ${paths.dir}?`, true);
|
|
3801
|
+
if (!ok) {
|
|
3802
|
+
console.log("Aborted.");
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// ffmpeg hint (non-fatal — the transcode path checks at runtime).
|
|
3808
|
+
if (!Bun.which("ffmpeg")) {
|
|
3809
|
+
console.warn(
|
|
3810
|
+
"\n⚠ ffmpeg not found on PATH. transcribe-cli needs 16kHz mono WAV; non-WAV capture audio is transcoded via ffmpeg.\n" +
|
|
3811
|
+
" Install it: apt install ffmpeg (Debian/Ubuntu) | brew install ffmpeg (macOS)",
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
try {
|
|
3816
|
+
mkdirSync(paths.binDir, { recursive: true });
|
|
3817
|
+
mkdirSync(paths.libsDir, { recursive: true });
|
|
3818
|
+
mkdirSync(paths.modelsDir, { recursive: true });
|
|
3819
|
+
|
|
3820
|
+
// 1) Release library bundle → extract the runtime dylibs/sos into libs/.
|
|
3821
|
+
// transcribe.cpp v0.1.1 ships a LIBRARY, not a prebuilt transcribe-cli,
|
|
3822
|
+
// so a CLI is usually NOT placed here — `installTranscribeLibs` still
|
|
3823
|
+
// opportunistically places one if a (future) asset includes it.
|
|
3824
|
+
const existingLibs = (readdirSync(paths.libsDir) as string[]).filter(isSharedLibFile);
|
|
3825
|
+
let libFiles = existingLibs;
|
|
3826
|
+
if (existingLibs.length && !force) {
|
|
3827
|
+
console.log(`\n✓ runtime libraries already present (${paths.libsDir}) — skipping (use --force to re-download).`);
|
|
3828
|
+
} else {
|
|
3829
|
+
console.log(`\nDownloading ${plan.asset!.asset} …`);
|
|
3830
|
+
libFiles = await installTranscribeLibs(plan, paths);
|
|
3831
|
+
console.log(
|
|
3832
|
+
libFiles.length
|
|
3833
|
+
? `✓ Extracted ${libFiles.length} runtime library file(s) → ${paths.libsDir}`
|
|
3834
|
+
: `⚠ no shared libraries found in ${plan.asset!.asset} (the asset layout may have changed).`,
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// 2) Model GGUF.
|
|
3839
|
+
const modelDest = join(paths.modelsDir, plan.model!.file);
|
|
3840
|
+
if (existsSync(modelDest) && !force) {
|
|
3841
|
+
console.log(`✓ model already present (${modelDest}) — skipping (use --force to re-download).`);
|
|
3842
|
+
} else {
|
|
3843
|
+
console.log(`Downloading model ${plan.model!.file} (~${plan.model!.approxSizeMb}MB) …`);
|
|
3844
|
+
await downloadTo(plan.model!.url, modelDest);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// 3) Build transcribe-cli from source against the extracted dylibs.
|
|
3848
|
+
// transcribe.cpp v0.1.1 ships a LIBRARY, not a prebuilt CLI, so we
|
|
3849
|
+
// compile examples/cli/main.cpp + examples/common/wav.cpp against
|
|
3850
|
+
// libtranscribe (a tiny ~127KB compile — no ggml rebuild). Non-fatal:
|
|
3851
|
+
// any failure keeps the current provider + prints guidance.
|
|
3852
|
+
const buildResult = await maybeBuildTranscribeCli(paths, libFiles, force);
|
|
3853
|
+
|
|
3854
|
+
// 4) Manifest.
|
|
3855
|
+
const manifest: TranscribeCppManifest = {
|
|
3856
|
+
version: plan.asset!.asset,
|
|
3857
|
+
asset: plan.asset!.asset,
|
|
3858
|
+
binFile: "transcribe-cli",
|
|
3859
|
+
model: plan.model!.name,
|
|
3860
|
+
modelFile: plan.model!.file,
|
|
3861
|
+
libFiles,
|
|
3862
|
+
binBuiltFrom: buildResult?.ok ? TRANSCRIBE_CPP_SOURCE_REF : undefined,
|
|
3863
|
+
os: plan.platform,
|
|
3864
|
+
arch: plan.arch,
|
|
3865
|
+
ram_gb: plan.totalRamGb,
|
|
3866
|
+
installedAt: new Date().toISOString(),
|
|
3867
|
+
};
|
|
3868
|
+
writeFileSync(paths.manifestPath, JSON.stringify(manifest, null, 2));
|
|
3869
|
+
|
|
3870
|
+
// 5) CLI resolution + provider switch — honest, no silent success. `binPath`
|
|
3871
|
+
// resolves to TRANSCRIBE_CPP_BIN or the built binary; we activate
|
|
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.
|
|
3877
|
+
if (existsSync(paths.binPath)) {
|
|
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
|
+
}
|
|
3891
|
+
} else {
|
|
3892
|
+
console.log(`\n✓ Libraries + model installed to ${paths.dir} — transcription is NOT yet activated.`);
|
|
3893
|
+
console.log(noRunnableCliGuidance(paths, buildResult));
|
|
3894
|
+
}
|
|
3895
|
+
} catch (err) {
|
|
3896
|
+
console.error(`\nInstall failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3897
|
+
console.error("The verb is idempotent — fix the issue and re-run. Or use the remote provider: parachute-vault transcription install --provider scribe-http");
|
|
3898
|
+
process.exit(1);
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
/**
|
|
3903
|
+
* Download the release tarball, extract it, and place its runtime shared
|
|
3904
|
+
* libraries (libtranscribe + libggml `.dylib`/`.so`) into `paths.libsDir`,
|
|
3905
|
+
* returning the extracted lib filenames. transcribe.cpp v0.1.1 ships a LIBRARY,
|
|
3906
|
+
* not a prebuilt `transcribe-cli` — so we also look for a CLI binary (a future
|
|
3907
|
+
* asset may include one) and, if present, place it at `paths.binPath` + chmod +x
|
|
3908
|
+
* (the caller decides activation from whether a CLI exists). Uses system `tar`
|
|
3909
|
+
* (present on Linux + macOS). Kept out of the plan/dry-run path so tests never
|
|
3910
|
+
* hit the network.
|
|
3911
|
+
*/
|
|
3912
|
+
async function installTranscribeLibs(
|
|
3913
|
+
plan: InstallPlan,
|
|
3914
|
+
paths: ReturnType<typeof resolveTranscribeCppPaths>,
|
|
3915
|
+
): Promise<string[]> {
|
|
3916
|
+
const tmpRoot = join(paths.dir, ".dl");
|
|
3917
|
+
mkdirSync(tmpRoot, { recursive: true });
|
|
3918
|
+
const tarball = join(tmpRoot, plan.asset!.asset);
|
|
3919
|
+
await downloadTo(plan.asset!.url, tarball);
|
|
3920
|
+
|
|
3921
|
+
const extractDir = join(tmpRoot, "x");
|
|
3922
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
3923
|
+
mkdirSync(extractDir, { recursive: true });
|
|
3924
|
+
const tarProc = Bun.spawnSync(["tar", "-xzf", tarball, "-C", extractDir]);
|
|
3925
|
+
if (tarProc.exitCode !== 0) {
|
|
3926
|
+
throw new Error(`tar extract failed: ${new TextDecoder().decode(tarProc.stderr)}`);
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// Walk the extracted tree: keep the shared libs, opportunistically place a CLI.
|
|
3930
|
+
mkdirSync(paths.libsDir, { recursive: true });
|
|
3931
|
+
const libFiles: string[] = [];
|
|
3932
|
+
for (const rel of readdirSync(extractDir, { recursive: true }) as string[]) {
|
|
3933
|
+
const abs = join(extractDir, rel);
|
|
3934
|
+
if (!statSync(abs).isFile()) continue;
|
|
3935
|
+
const base = rel.split("/").pop()!;
|
|
3936
|
+
if (isSharedLibFile(base)) {
|
|
3937
|
+
copyFileSync(abs, join(paths.libsDir, base));
|
|
3938
|
+
libFiles.push(base);
|
|
3939
|
+
} else if (base === "transcribe-cli") {
|
|
3940
|
+
copyFileSync(abs, paths.binPath);
|
|
3941
|
+
chmodSync(paths.binPath, 0o755);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
3945
|
+
return libFiles;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
/**
|
|
3949
|
+
* Build `transcribe-cli` from source against the extracted dylibs, honoring
|
|
3950
|
+
* idempotency + the `TRANSCRIBE_CPP_BIN` operator override. Returns the build
|
|
3951
|
+
* result, or `null` when the build was intentionally skipped (already built /
|
|
3952
|
+
* operator-provided binary / no libs to link). Non-fatal — logs progress and
|
|
3953
|
+
* warnings; failures come back as a typed `{ ok:false }` the caller reports.
|
|
3954
|
+
*/
|
|
3955
|
+
async function maybeBuildTranscribeCli(
|
|
3956
|
+
paths: ReturnType<typeof resolveTranscribeCppPaths>,
|
|
3957
|
+
libFiles: string[],
|
|
3958
|
+
force: boolean,
|
|
3959
|
+
): Promise<CliBuildResult | null> {
|
|
3960
|
+
if (process.env.TRANSCRIBE_CPP_BIN?.trim()) {
|
|
3961
|
+
console.log(
|
|
3962
|
+
existsSync(paths.binPath)
|
|
3963
|
+
? `\n✓ Using operator-provided TRANSCRIBE_CPP_BIN=${paths.binPath} — skipping source build.`
|
|
3964
|
+
: `\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.`,
|
|
3965
|
+
);
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
if (existsSync(paths.binPath) && !force) {
|
|
3969
|
+
console.log(`\n✓ transcribe-cli already built (${paths.binPath}) — skipping build (use --force to rebuild).`);
|
|
3970
|
+
return null;
|
|
3971
|
+
}
|
|
3972
|
+
if (libFiles.length === 0) {
|
|
3973
|
+
console.warn(`\n⚠ no runtime libraries were extracted — cannot build transcribe-cli. Skipping build.`);
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
console.log(`\nBuilding transcribe-cli from source (transcribe.cpp @ ${TRANSCRIBE_CPP_SOURCE_REF.slice(0, 12)}) …`);
|
|
3978
|
+
mkdirSync(paths.binDir, { recursive: true });
|
|
3979
|
+
const srcDir = join(paths.dir, ".src");
|
|
3980
|
+
const result = await buildTranscribeCli(
|
|
3981
|
+
{ srcDir, libsDir: paths.libsDir, binPath: paths.binPath },
|
|
3982
|
+
{
|
|
3983
|
+
platform: process.platform,
|
|
3984
|
+
which: (c) => Bun.which(c),
|
|
3985
|
+
fetchSource: async (files, dir) => {
|
|
3986
|
+
rmSync(dir, { recursive: true, force: true });
|
|
3987
|
+
for (const rel of files) {
|
|
3988
|
+
const dest = join(dir, rel);
|
|
3989
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
3990
|
+
await downloadTo(cliSourceUrl(rel), dest);
|
|
3991
|
+
}
|
|
3992
|
+
},
|
|
3993
|
+
readFile: (p) => readFileSync(p, "utf8"),
|
|
3994
|
+
writeFile: (p, content) => writeFileSync(p, content),
|
|
3995
|
+
compile: (cmd) => {
|
|
3996
|
+
const p = Bun.spawnSync(cmd);
|
|
3997
|
+
return { exitCode: p.exitCode ?? 1, stderr: new TextDecoder().decode(p.stderr) };
|
|
3998
|
+
},
|
|
3999
|
+
exists: existsSync,
|
|
4000
|
+
removeBin: (p) => rmSync(p, { force: true }),
|
|
4001
|
+
rename: (from, to) => renameSync(from, to),
|
|
4002
|
+
},
|
|
4003
|
+
);
|
|
4004
|
+
|
|
4005
|
+
if (result.ok) {
|
|
4006
|
+
chmodSync(paths.binPath, 0o755); // belt-and-suspenders; the compiler emits 0755
|
|
4007
|
+
rmSync(srcDir, { recursive: true, force: true }); // tidy — source is re-fetchable
|
|
4008
|
+
console.log(`✓ Built transcribe-cli → ${result.binPath}`);
|
|
4009
|
+
} else {
|
|
4010
|
+
// Keep srcDir on failure so the printed compile command can be re-run by hand.
|
|
4011
|
+
console.warn(`⚠ transcribe-cli build failed (${result.stage}): ${result.message}`);
|
|
4012
|
+
if (result.command) console.warn(` compile command tried:\n ${result.command}`);
|
|
4013
|
+
}
|
|
4014
|
+
return result;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
/**
|
|
4018
|
+
* The "no runnable CLI" guidance printed when activation can't proceed —
|
|
4019
|
+
* tailored to WHY (build failed at toolchain vs compile, or was skipped).
|
|
4020
|
+
*/
|
|
4021
|
+
function noRunnableCliGuidance(
|
|
4022
|
+
paths: ReturnType<typeof resolveTranscribeCppPaths>,
|
|
4023
|
+
build: CliBuildResult | null,
|
|
4024
|
+
): string {
|
|
4025
|
+
const lines: string[] = [];
|
|
4026
|
+
if (build && !build.ok && build.stage === "toolchain") {
|
|
4027
|
+
lines.push(
|
|
4028
|
+
`\n⚠ Could not build transcribe-cli — ${build.message}`,
|
|
4029
|
+
" To finish, either:",
|
|
4030
|
+
` • install a C++ compiler and re-run this verb (${compilerInstallHint(process.platform)}); OR`,
|
|
4031
|
+
" • build transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
|
|
4032
|
+
" • use the remote provider: parachute-vault transcription install --provider scribe-http",
|
|
4033
|
+
);
|
|
4034
|
+
} else if (build && !build.ok && build.stage === "fetch") {
|
|
4035
|
+
lines.push(
|
|
4036
|
+
`\n⚠ Could not fetch the transcribe-cli source — ${build.message}`,
|
|
4037
|
+
" The libraries + model are installed; only the source download failed (check network/proxy).",
|
|
4038
|
+
" To finish, either:",
|
|
4039
|
+
" • re-run this verb once connectivity is restored; OR",
|
|
4040
|
+
" • build transcribe-cli yourself, point TRANSCRIBE_CPP_BIN at it, and re-run; OR",
|
|
4041
|
+
" • use the remote provider: parachute-vault transcription install --provider scribe-http",
|
|
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
|
+
);
|
|
4051
|
+
} else if (build && !build.ok) {
|
|
4052
|
+
lines.push(
|
|
4053
|
+
`\n⚠ transcribe-cli build failed (${build.stage}). The libraries + model are installed;`,
|
|
4054
|
+
" the CLI is not. To finish, either:",
|
|
4055
|
+
" • fix the toolchain and re-run this verb; OR",
|
|
4056
|
+
" • build transcribe-cli yourself (the compile command tried is printed above),",
|
|
4057
|
+
" point TRANSCRIBE_CPP_BIN at the result, and re-run; OR",
|
|
4058
|
+
" • use the remote provider: parachute-vault transcription install --provider scribe-http",
|
|
4059
|
+
);
|
|
4060
|
+
} else {
|
|
4061
|
+
// Build skipped (no libs, or an operator-provided binary that isn't present).
|
|
4062
|
+
lines.push(
|
|
4063
|
+
"\n⚠ No runnable transcribe-cli is available yet. To finish, either:",
|
|
4064
|
+
` • re-run this verb so it builds a transcribe-cli against ${paths.libsDir} (needs a C++ compiler); OR`,
|
|
4065
|
+
" • point TRANSCRIBE_CPP_BIN at a transcribe-cli you built, and re-run; OR",
|
|
4066
|
+
" • use the remote provider: parachute-vault transcription install --provider scribe-http",
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
lines.push(" Your current provider is unchanged — check with `parachute-vault transcription status`.");
|
|
4070
|
+
return lines.join("\n");
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
/** `parachute-vault transcription status` — provider + per-provider install state. */
|
|
4074
|
+
async function cmdTranscriptionStatus(): Promise<void> {
|
|
4075
|
+
const active = resolveTranscriptionProviderName();
|
|
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).
|
|
4092
|
+
const paths = resolveTranscribeCppPaths();
|
|
4093
|
+
const manifest = readManifest(paths.manifestPath);
|
|
4094
|
+
const cliPresent = existsSync(paths.binPath);
|
|
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})`}`);
|
|
4106
|
+
if (manifest) {
|
|
4107
|
+
console.log(` model: ${manifest.model} (${manifest.modelFile})`);
|
|
4108
|
+
console.log(
|
|
4109
|
+
` libs: ${paths.libsDir}${manifest.libFiles?.length ? ` (${manifest.libFiles.length} file(s))` : ""}`,
|
|
4110
|
+
);
|
|
4111
|
+
console.log(
|
|
4112
|
+
cliPresent
|
|
4113
|
+
? ` cli: ${paths.binPath}${manifest.binBuiltFrom ? ` (built from source @ ${manifest.binBuiltFrom.slice(0, 12)})` : ""}`
|
|
4114
|
+
: ` cli: NOT FOUND — re-run \`transcription install\` to build one (needs a C++ compiler), or set TRANSCRIBE_CPP_BIN`,
|
|
4115
|
+
);
|
|
4116
|
+
console.log(` ram: ${manifest.ram_gb}GB at install (${manifest.os}/${manifest.arch})`);
|
|
4117
|
+
} else {
|
|
4118
|
+
console.log(` (not installed — run: parachute-vault transcription install --provider transcribe-cpp)`);
|
|
4119
|
+
}
|
|
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 {
|
|
4130
|
+
console.log(
|
|
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)",
|
|
4134
|
+
);
|
|
4135
|
+
}
|
|
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
|
+
);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
3499
4170
|
// ---------------------------------------------------------------------------
|
|
3500
4171
|
// Schema maintenance — `parachute-vault schema <subcommand>`
|
|
3501
4172
|
// ---------------------------------------------------------------------------
|
|
@@ -3962,11 +4633,14 @@ async function createVault(
|
|
|
3962
4633
|
// row is written — vault is a pure hub resource-server post-0.5.0.
|
|
3963
4634
|
const seedStore = getVaultStore(name);
|
|
3964
4635
|
|
|
3965
|
-
// Seed the
|
|
3966
|
-
//
|
|
3967
|
-
//
|
|
3968
|
-
//
|
|
3969
|
-
//
|
|
4636
|
+
// Seed the default packs (welcome web + capture tag, Getting Started guide)
|
|
4637
|
+
// so the first minute shows a small living graph and a connected AI can read
|
|
4638
|
+
// the guide and help the operator set the vault up. Surface Starter is NOT
|
|
4639
|
+
// default-seeded — `parachute-vault add-pack surface-starter` adds it later.
|
|
4640
|
+
// Idempotent + best-effort: only writes notes that are absent, and never
|
|
4641
|
+
// fails the create. Runs BEFORE the mirror bootstrap so the seeded content
|
|
4642
|
+
// lands in the initial mirror commit. See src/onboarding-seed.ts +
|
|
4643
|
+
// core/src/seed-packs.ts.
|
|
3970
4644
|
await seedOnboardingNotesBestEffort(seedStore);
|
|
3971
4645
|
|
|
3972
4646
|
// Default new vaults to an internal live mirror (local git backup of the
|
|
@@ -4088,6 +4762,99 @@ function installMcpConfig(opts: InstallMcpConfigOpts): void {
|
|
|
4088
4762
|
writeFileSync(targetPath, JSON.stringify(config, null, 2) + "\n");
|
|
4089
4763
|
}
|
|
4090
4764
|
|
|
4765
|
+
/**
|
|
4766
|
+
* `parachute-vault add-pack <pack> [--vault <name>]` — apply a named seed
|
|
4767
|
+
* pack (starter notes + tags) to a vault.
|
|
4768
|
+
*
|
|
4769
|
+
* New vaults default-seed the `welcome` + `getting-started` packs at create;
|
|
4770
|
+
* this verb adds the opt-in ones (today: `surface-starter`) — or re-applies a
|
|
4771
|
+
* default pack to an older vault that predates it. Idempotent per item: a
|
|
4772
|
+
* note is created only when no note lives at its path (a re-run never
|
|
4773
|
+
* clobbers an edited note); tag upserts converge on the same rows.
|
|
4774
|
+
*
|
|
4775
|
+
* Unlike `import` (bulk stream writes, vault#323 daemon-busy guard), this is
|
|
4776
|
+
* a handful of tiny writes against a WAL database, so it runs fine next to a
|
|
4777
|
+
* live daemon; a write-lock collision (SQLITE_BUSY) is caught with a
|
|
4778
|
+
* retry-is-safe message instead of pre-emptively demanding a daemon stop.
|
|
4779
|
+
*/
|
|
4780
|
+
async function cmdAddPack(args: string[]) {
|
|
4781
|
+
let vaultName = "default";
|
|
4782
|
+
const positional: string[] = [];
|
|
4783
|
+
for (let i = 0; i < args.length; i++) {
|
|
4784
|
+
const arg = args[i]!;
|
|
4785
|
+
if (arg === "--vault") {
|
|
4786
|
+
const v = args[++i];
|
|
4787
|
+
if (!v) {
|
|
4788
|
+
console.error("--vault requires a value.");
|
|
4789
|
+
process.exit(1);
|
|
4790
|
+
}
|
|
4791
|
+
vaultName = v;
|
|
4792
|
+
} else {
|
|
4793
|
+
positional.push(arg);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
const packName = positional[0];
|
|
4797
|
+
|
|
4798
|
+
const { listSeedPacks, getSeedPack, applySeedPack } = await import(
|
|
4799
|
+
"../core/src/seed-packs.ts"
|
|
4800
|
+
);
|
|
4801
|
+
|
|
4802
|
+
const printPacks = () => {
|
|
4803
|
+
console.error("\nAvailable packs:");
|
|
4804
|
+
for (const p of listSeedPacks()) {
|
|
4805
|
+
console.error(` ${p.name.padEnd(17)} ${p.description}`);
|
|
4806
|
+
}
|
|
4807
|
+
};
|
|
4808
|
+
|
|
4809
|
+
if (!packName) {
|
|
4810
|
+
console.error("Usage: parachute-vault add-pack <pack> [--vault <name>]");
|
|
4811
|
+
printPacks();
|
|
4812
|
+
process.exit(1);
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
const pack = getSeedPack(packName);
|
|
4816
|
+
if (!pack) {
|
|
4817
|
+
console.error(`Unknown pack: "${packName}"`);
|
|
4818
|
+
printPacks();
|
|
4819
|
+
process.exit(1);
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
const config = readVaultConfig(vaultName);
|
|
4823
|
+
if (!config) {
|
|
4824
|
+
console.error(`Vault "${vaultName}" not found. Run: parachute-vault create ${vaultName}`);
|
|
4825
|
+
process.exit(1);
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
const store = getVaultStore(vaultName);
|
|
4829
|
+
try {
|
|
4830
|
+
const result = await applySeedPack(store, pack);
|
|
4831
|
+
console.log(`Pack "${pack.name}" applied to vault "${vaultName}":`);
|
|
4832
|
+
for (const path of result.seededNotes) {
|
|
4833
|
+
console.log(` + note ${path}`);
|
|
4834
|
+
}
|
|
4835
|
+
for (const path of result.skippedNotes) {
|
|
4836
|
+
console.log(` = note ${path} (already exists — left untouched)`);
|
|
4837
|
+
}
|
|
4838
|
+
for (const tag of result.tags) {
|
|
4839
|
+
console.log(` ~ tag ${tag} (upserted)`);
|
|
4840
|
+
}
|
|
4841
|
+
if (result.seededNotes.length === 0 && result.tags.length === 0) {
|
|
4842
|
+
console.log(" Nothing to add — everything was already in place.");
|
|
4843
|
+
}
|
|
4844
|
+
} catch (err) {
|
|
4845
|
+
const message = (err as Error)?.message ?? String(err);
|
|
4846
|
+
if (message.includes("SQLITE_BUSY") || (err as { code?: string })?.code === "SQLITE_BUSY") {
|
|
4847
|
+
console.error(
|
|
4848
|
+
`error: the vault database is busy (a running daemon holds the write lock). ` +
|
|
4849
|
+
`add-pack is idempotent — re-run it, or stop the daemon first with: parachute stop vault`,
|
|
4850
|
+
);
|
|
4851
|
+
} else {
|
|
4852
|
+
console.error(`error: failed to apply pack "${pack.name}": ${message}`);
|
|
4853
|
+
}
|
|
4854
|
+
process.exit(1);
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4091
4858
|
function usage() {
|
|
4092
4859
|
console.log(`
|
|
4093
4860
|
Parachute Vault — self-hosted knowledge graph
|
|
@@ -4153,6 +4920,14 @@ Vaults:
|
|
|
4153
4920
|
opt-in upgrade). --no-mirror creates a bare vault with no mirror config.
|
|
4154
4921
|
Operators can flip the server-wide default with 'default_mirror: off' in
|
|
4155
4922
|
config.yaml (recommended for cloud / disk-constrained boxes).
|
|
4923
|
+
parachute-vault add-pack <pack> [--vault <name>]
|
|
4924
|
+
Add a named seed pack (starter notes + tags) to a vault
|
|
4925
|
+
(default vault: "default"). Run with no arguments to list
|
|
4926
|
+
the available packs. New vaults seed the "welcome" +
|
|
4927
|
+
"getting-started" packs automatically; "surface-starter"
|
|
4928
|
+
is opt-in via this command. Idempotent: notes are only
|
|
4929
|
+
created when absent — a re-run never clobbers an edited
|
|
4930
|
+
note.
|
|
4156
4931
|
parachute-vault list List all vaults
|
|
4157
4932
|
parachute-vault remove <name> [--yes] Remove a vault
|
|
4158
4933
|
parachute-vault mcp-install [--mint|--token <t>]
|
|
@@ -4298,6 +5073,30 @@ Schema maintenance:
|
|
|
4298
5073
|
Writes through strict enforcement (migrate
|
|
4299
5074
|
bypass). See \`schema migrate-field --help\`.
|
|
4300
5075
|
|
|
5076
|
+
Transcription (scribe-fold):
|
|
5077
|
+
parachute-vault transcription install [--dry-run] [--model <id>] [--force] [--yes]
|
|
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.
|
|
5099
|
+
|
|
4301
5100
|
── Advanced / standalone ──────────────────────────────────────────────
|
|
4302
5101
|
|
|
4303
5102
|
Direct daemon controls. For normal use, prefer the Parachute Hub wrappers
|