@openparachute/vault 0.6.4 → 0.6.5-rc.10
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/core/src/__fixtures__/golden-vault.ts +125 -0
- package/core/src/__fixtures__/portable-export-golden.json +10 -0
- package/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/mcp.ts +306 -314
- package/core/src/notes.ts +54 -62
- package/core/src/onboarding.ts +14 -296
- package/core/src/portable-md-batching.test.ts +67 -0
- package/core/src/portable-md-golden.test.ts +55 -0
- package/core/src/portable-md.ts +579 -435
- package/core/src/schema.ts +21 -43
- package/core/src/seed-packs.test.ts +191 -0
- package/core/src/seed-packs.ts +559 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/store.ts +14 -9
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/txn.test.ts +229 -0
- package/core/src/txn.ts +105 -0
- package/core/src/types.ts +9 -0
- 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/cli.ts +778 -7
- package/src/onboarding-seed.test.ts +126 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +25 -7
- package/src/server.ts +108 -31
- package/src/transcription/build.test.ts +224 -0
- package/src/transcription/build.ts +252 -0
- package/src/transcription/capability.test.ts +118 -0
- package/src/transcription/capability.ts +96 -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 +259 -0
- package/src/transcription/select.ts +334 -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 +38 -10
- package/src/vault.test.ts +48 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the
|
|
2
|
+
* Unit tests for the default-pack seeding (originally demo-prep Workstream A —
|
|
3
|
+
* A1/A2/A3; reshaped for named seed packs).
|
|
3
4
|
*
|
|
4
5
|
* Store-direct (no CLI subprocess) so they're fast. Covers:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
6
|
+
* - default seed = `welcome` (3-note web + the capture tag) + `getting-started`
|
|
7
|
+
* — and NOT `surface-starter` (out of the default seed, ratified 2026-07-02).
|
|
8
|
+
* - A1: the Getting Started doctrine markers.
|
|
9
|
+
* - A3 (reshaped): the welcome web's wikilinks resolve; Getting Started
|
|
10
|
+
* points at the surface-starter PACK without a dangling wikilink.
|
|
7
11
|
* - idempotency: a re-run never clobbers an edited note.
|
|
12
|
+
* - applySeedPack(surface-starter): the add-pack path works + is idempotent,
|
|
13
|
+
* and the guide carries the surface doctrine (GAP 1/2 markers).
|
|
8
14
|
* - A2: the projection / vault-info pointer steers the AI at Getting Started.
|
|
9
15
|
*/
|
|
10
16
|
|
|
@@ -16,14 +22,28 @@ import { tmpdir } from "os";
|
|
|
16
22
|
import { BunStore } from "./vault-store.ts";
|
|
17
23
|
import { seedOnboardingNotes } from "./onboarding-seed.ts";
|
|
18
24
|
import {
|
|
25
|
+
applySeedPack,
|
|
26
|
+
CONNECT_AI_PATH,
|
|
19
27
|
GETTING_STARTED_PATH,
|
|
28
|
+
NOTES_REQUIRED_TAGS,
|
|
29
|
+
SURFACE_STARTER_PACK,
|
|
20
30
|
SURFACE_STARTER_PATH,
|
|
21
|
-
|
|
31
|
+
TRY_LINKING_PATH,
|
|
32
|
+
WELCOME_PATH,
|
|
33
|
+
} from "../core/src/seed-packs.ts";
|
|
22
34
|
import {
|
|
23
35
|
buildVaultProjection,
|
|
24
36
|
projectionToMarkdown,
|
|
25
37
|
} from "../core/src/vault-projection.ts";
|
|
26
38
|
|
|
39
|
+
/** Every note path the default seed writes on a blank vault. */
|
|
40
|
+
const DEFAULT_SEED_PATHS = [
|
|
41
|
+
WELCOME_PATH,
|
|
42
|
+
TRY_LINKING_PATH,
|
|
43
|
+
CONNECT_AI_PATH,
|
|
44
|
+
GETTING_STARTED_PATH,
|
|
45
|
+
];
|
|
46
|
+
|
|
27
47
|
let db: Database;
|
|
28
48
|
let store: BunStore;
|
|
29
49
|
let tmpDir: string;
|
|
@@ -40,14 +60,15 @@ afterEach(() => {
|
|
|
40
60
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
41
61
|
});
|
|
42
62
|
|
|
43
|
-
describe("seedOnboardingNotes (
|
|
44
|
-
test("seeds
|
|
63
|
+
describe("seedOnboardingNotes — default packs (welcome + getting-started)", () => {
|
|
64
|
+
test("seeds the welcome web + Getting Started on a blank vault — and NOT Surface Starter", async () => {
|
|
45
65
|
const result = await seedOnboardingNotes(store);
|
|
46
|
-
expect(result.seeded.sort()).toEqual(
|
|
47
|
-
[GETTING_STARTED_PATH, SURFACE_STARTER_PATH].sort(),
|
|
48
|
-
);
|
|
66
|
+
expect(result.seeded.sort()).toEqual([...DEFAULT_SEED_PATHS].sort());
|
|
49
67
|
expect(result.skipped).toEqual([]);
|
|
50
68
|
|
|
69
|
+
// Surface Starter is OUT of the default seed (button/CLI only).
|
|
70
|
+
expect(await store.getNoteByPath(SURFACE_STARTER_PATH)).toBeNull();
|
|
71
|
+
|
|
51
72
|
const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
|
|
52
73
|
expect(gs).not.toBeNull();
|
|
53
74
|
expect(gs!.content).toContain("# Getting Started");
|
|
@@ -59,26 +80,28 @@ describe("seedOnboardingNotes (A1/A3)", () => {
|
|
|
59
80
|
expect(gs!.content).toContain("parachute-vault import");
|
|
60
81
|
expect(gs!.content).toContain("Adapt this note");
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(ss!.content).toContain("@openparachute/surface-render");
|
|
68
|
-
expect(ss!.content).toContain("createVaultSurface");
|
|
69
|
-
expect(ss!.content).toContain("NoteRenderer");
|
|
83
|
+
// The welcome web: three person-voiced notes.
|
|
84
|
+
for (const path of [WELCOME_PATH, TRY_LINKING_PATH, CONNECT_AI_PATH]) {
|
|
85
|
+
expect(await store.getNoteByPath(path)).not.toBeNull();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expect(
|
|
89
|
+
test("seeds the ONE capture tag Notes requires (byte-equal semantics)", async () => {
|
|
90
|
+
const result = await seedOnboardingNotes(store);
|
|
91
|
+
expect(result.tags).toEqual(["capture"]);
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
for (const decl of NOTES_REQUIRED_TAGS) {
|
|
94
|
+
const record = await store.getTagRecord(decl.name);
|
|
95
|
+
expect(record).not.toBeNull();
|
|
96
|
+
expect(record!.description).toBe(decl.description);
|
|
97
|
+
expect(record!.parent_names ?? []).toEqual(decl.parent_names ?? []);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The retired subtype tags are NOT seeded — entry method is note
|
|
101
|
+
// metadata.source (text|voice), not taxonomy (2026-07-03). Existing
|
|
102
|
+
// vaults carrying these tags stay valid; new vaults don't get them.
|
|
103
|
+
expect(await store.getTagRecord("capture/text")).toBeNull();
|
|
104
|
+
expect(await store.getTagRecord("capture/voice")).toBeNull();
|
|
82
105
|
});
|
|
83
106
|
|
|
84
107
|
test("GAP 4: Getting Started shows a concrete update-tag fields example + write gotchas", async () => {
|
|
@@ -98,29 +121,39 @@ describe("seedOnboardingNotes (A1/A3)", () => {
|
|
|
98
121
|
expect(gs!.content).toContain("first listed value");
|
|
99
122
|
});
|
|
100
123
|
|
|
101
|
-
test("A3: Getting Started
|
|
124
|
+
test("A3 (reshaped): the welcome web's wikilinks resolve; Getting Started has no dangling Surface Starter link", async () => {
|
|
102
125
|
await seedOnboardingNotes(store);
|
|
103
|
-
const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
|
|
104
|
-
expect(gs!.content).toContain("[[Surface Starter]]");
|
|
105
126
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
127
|
+
// welcome → try-linking, try-linking → welcome, connect-AI → welcome.
|
|
128
|
+
const welcome = await store.getNoteByPath(WELCOME_PATH);
|
|
129
|
+
const tryLinking = await store.getNoteByPath(TRY_LINKING_PATH);
|
|
130
|
+
const connectAi = await store.getNoteByPath(CONNECT_AI_PATH);
|
|
131
|
+
|
|
132
|
+
const welcomeOut = await store.getLinks(welcome!.id, { direction: "outbound" });
|
|
133
|
+
expect(welcomeOut.some((l) => l.targetId === tryLinking!.id)).toBe(true);
|
|
134
|
+
const tryOut = await store.getLinks(tryLinking!.id, { direction: "outbound" });
|
|
135
|
+
expect(tryOut.some((l) => l.targetId === welcome!.id)).toBe(true);
|
|
136
|
+
const connectOut = await store.getLinks(connectAi!.id, { direction: "outbound" });
|
|
137
|
+
expect(connectOut.some((l) => l.targetId === welcome!.id)).toBe(true);
|
|
138
|
+
|
|
139
|
+
// Getting Started mentions the surface-starter PACK — not a wikilink to a
|
|
140
|
+
// note that the default seed no longer creates.
|
|
141
|
+
const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
|
|
142
|
+
expect(gs!.content).not.toContain("[[Surface Starter]]");
|
|
143
|
+
expect(gs!.content).toContain("add-pack surface-starter");
|
|
110
144
|
});
|
|
111
145
|
|
|
112
|
-
test("idempotent: a second seed run skips
|
|
146
|
+
test("idempotent: a second seed run skips all notes (does not duplicate)", async () => {
|
|
113
147
|
await seedOnboardingNotes(store);
|
|
114
148
|
const second = await seedOnboardingNotes(store);
|
|
115
149
|
expect(second.seeded).toEqual([]);
|
|
116
|
-
expect(second.skipped.sort()).toEqual(
|
|
117
|
-
[GETTING_STARTED_PATH, SURFACE_STARTER_PATH].sort(),
|
|
118
|
-
);
|
|
150
|
+
expect(second.skipped.sort()).toEqual([...DEFAULT_SEED_PATHS].sort());
|
|
119
151
|
|
|
120
152
|
// Exactly one note per path — no duplicates.
|
|
121
153
|
const all = await store.queryNotes({});
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
for (const path of DEFAULT_SEED_PATHS) {
|
|
155
|
+
expect(all.filter((n) => n.path === path)).toHaveLength(1);
|
|
156
|
+
}
|
|
124
157
|
});
|
|
125
158
|
|
|
126
159
|
test("idempotent: never clobbers an operator-edited Getting Started", async () => {
|
|
@@ -139,6 +172,59 @@ describe("seedOnboardingNotes (A1/A3)", () => {
|
|
|
139
172
|
});
|
|
140
173
|
});
|
|
141
174
|
|
|
175
|
+
describe("applySeedPack — surface-starter via add-pack", () => {
|
|
176
|
+
test("applies the Surface Starter guide onto a default-seeded vault", async () => {
|
|
177
|
+
await seedOnboardingNotes(store);
|
|
178
|
+
const result = await applySeedPack(store, SURFACE_STARTER_PACK);
|
|
179
|
+
expect(result.pack).toBe("surface-starter");
|
|
180
|
+
expect(result.seededNotes).toEqual([SURFACE_STARTER_PATH]);
|
|
181
|
+
expect(result.skippedNotes).toEqual([]);
|
|
182
|
+
expect(result.tags).toEqual([]);
|
|
183
|
+
|
|
184
|
+
const ss = await store.getNoteByPath(SURFACE_STARTER_PATH);
|
|
185
|
+
expect(ss).not.toBeNull();
|
|
186
|
+
expect(ss!.content).toContain("# Surface Starter");
|
|
187
|
+
// A3: the two surface packages by name.
|
|
188
|
+
expect(ss!.content).toContain("@openparachute/surface-client");
|
|
189
|
+
expect(ss!.content).toContain("@openparachute/surface-render");
|
|
190
|
+
expect(ss!.content).toContain("createVaultSurface");
|
|
191
|
+
expect(ss!.content).toContain("NoteRenderer");
|
|
192
|
+
|
|
193
|
+
// GAP 2: warn that a surface can't be run/developed from this MCP session.
|
|
194
|
+
expect(ss!.content).toContain("not from this");
|
|
195
|
+
expect(ss!.content.toLowerCase()).toContain("browser");
|
|
196
|
+
|
|
197
|
+
// GAP 1: a real, copy-paste end-to-end snippet — every load-bearing symbol
|
|
198
|
+
// present, verified against the surface-client / surface-render source.
|
|
199
|
+
expect(ss!.content).toContain("clientName"); // the one required createVaultSurface option
|
|
200
|
+
expect(ss!.content).toContain("getClient");
|
|
201
|
+
expect(ss!.content).toContain("queryNotes");
|
|
202
|
+
expect(ss!.content).toContain("handleCallback");
|
|
203
|
+
expect(ss!.content).toContain("resolve="); // NoteRenderer wikilink resolver prop
|
|
204
|
+
|
|
205
|
+
// Its [[Getting Started]] wikilink resolves against the default seed.
|
|
206
|
+
const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
|
|
207
|
+
const outbound = await store.getLinks(ss!.id, { direction: "outbound" });
|
|
208
|
+
expect(outbound.some((l) => l.targetId === gs!.id)).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("idempotent: a re-apply skips and never clobbers an edited note", async () => {
|
|
212
|
+
await applySeedPack(store, SURFACE_STARTER_PACK);
|
|
213
|
+
const ss = await store.getNoteByPath(SURFACE_STARTER_PATH);
|
|
214
|
+
const edited = "# Surface Starter\n\nWe built ours with Solid.";
|
|
215
|
+
await store.updateNote(ss!.id, { content: edited });
|
|
216
|
+
|
|
217
|
+
const rerun = await applySeedPack(store, SURFACE_STARTER_PACK);
|
|
218
|
+
expect(rerun.seededNotes).toEqual([]);
|
|
219
|
+
expect(rerun.skippedNotes).toEqual([SURFACE_STARTER_PATH]);
|
|
220
|
+
|
|
221
|
+
const after = await store.getNoteByPath(SURFACE_STARTER_PATH);
|
|
222
|
+
expect(after!.content).toBe(edited);
|
|
223
|
+
const all = await store.queryNotes({});
|
|
224
|
+
expect(all.filter((n) => n.path === SURFACE_STARTER_PATH)).toHaveLength(1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
142
228
|
describe("vault-info / projection pointer (A2)", () => {
|
|
143
229
|
test("projection carries getting_started when the note exists", async () => {
|
|
144
230
|
// Absent before seeding → no pointer.
|
package/src/onboarding-seed.ts
CHANGED
|
@@ -1,74 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seed the
|
|
2
|
+
* Seed the default packs on vault creation.
|
|
3
3
|
*
|
|
4
|
-
* A freshly-created vault gets
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A freshly-created vault gets the `welcome` pack (the person-voiced
|
|
5
|
+
* three-note welcome web + the `capture` tag the Notes surface expects) and
|
|
6
|
+
* the `getting-started` pack (the AI-legible start-here doctrine) — so the
|
|
7
|
+
* first minute shows a small living graph, and a connected AI can read the
|
|
8
|
+
* guide and help the operator set the vault up.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* a sweep over existing vaults.
|
|
10
|
+
* The `surface-starter` pack is deliberately NOT part of the default seed
|
|
11
|
+
* (ratified 2026-07-02): it's added on demand via
|
|
12
|
+
* `parachute-vault add-pack surface-starter` or a console affordance.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* IDEMPOTENT + create-time only: each note is written ONLY when absent (tag
|
|
15
|
+
* upserts converge on the same rows). A re-init, restart, or re-run never
|
|
16
|
+
* clobbers a note the operator/AI has since edited. Existing vaults are
|
|
17
|
+
* unaffected — this runs on the create path, not as a sweep over existing
|
|
18
|
+
* vaults.
|
|
15
19
|
*
|
|
16
|
-
*
|
|
20
|
+
* Best-effort + non-fatal: seeding must NEVER fail a vault create. Any error
|
|
21
|
+
* is caught and logged; the vault is still usable without the starter content.
|
|
22
|
+
*
|
|
23
|
+
* Pack contents live in core/src/seed-packs.ts (shared with the cloud vault).
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "../core/src/onboarding.ts";
|
|
27
|
+
applySeedPack,
|
|
28
|
+
GETTING_STARTED_PACK,
|
|
29
|
+
welcomePack,
|
|
30
|
+
} from "../core/src/seed-packs.ts";
|
|
25
31
|
import type { Store } from "../core/src/types.ts";
|
|
26
32
|
|
|
27
33
|
interface SeedResult {
|
|
28
|
-
/**
|
|
34
|
+
/** Note paths actually written this run (absent ones). Empty when all pre-existed. */
|
|
29
35
|
seeded: string[];
|
|
30
|
-
/**
|
|
36
|
+
/** Note paths skipped because the note already existed (idempotency). */
|
|
31
37
|
skipped: string[];
|
|
38
|
+
/** Tag names upserted (idempotent by nature). */
|
|
39
|
+
tags: string[];
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
store: Store,
|
|
40
|
-
path: string,
|
|
41
|
-
content: string,
|
|
42
|
-
): Promise<boolean> {
|
|
43
|
-
const existing = await store.getNoteByPath(path);
|
|
44
|
-
if (existing) return false;
|
|
45
|
-
await store.createNote(content, { path });
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Seed the onboarding notes into `store`, idempotently. Surface Starter is
|
|
51
|
-
* created first so the `[[Surface Starter]]` wikilink in Getting Started
|
|
52
|
-
* resolves immediately (the create path also auto-resolves later, but this
|
|
53
|
-
* keeps the link live from the first write).
|
|
43
|
+
* Seed the default packs (`welcome` + `getting-started`) into `store`,
|
|
44
|
+
* idempotently. Welcome first so the welcome web exists before the guide
|
|
45
|
+
* lands (wikilinks auto-resolve either way; this just keeps the seed's
|
|
46
|
+
* unresolved-link window minimal).
|
|
54
47
|
*/
|
|
55
48
|
export async function seedOnboardingNotes(store: Store): Promise<SeedResult> {
|
|
56
49
|
const seeded: string[] = [];
|
|
57
50
|
const skipped: string[] = [];
|
|
51
|
+
const tags: string[] = [];
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
// No consoleOrigin: create-time runs are often pre-expose, and a baked-in
|
|
54
|
+
// loopback origin would go stale — the generic line is the safe default.
|
|
55
|
+
for (const pack of [welcomePack(), GETTING_STARTED_PACK]) {
|
|
56
|
+
const result = await applySeedPack(store, pack);
|
|
57
|
+
seeded.push(...result.seededNotes);
|
|
58
|
+
skipped.push(...result.skippedNotes);
|
|
59
|
+
tags.push(...result.tags);
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
return { seeded, skipped };
|
|
62
|
+
return { seeded, skipped, tags };
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
/**
|
|
71
|
-
* Best-effort wrapper for the create path: seed the
|
|
66
|
+
* Best-effort wrapper for the create path: seed the default packs, swallow
|
|
72
67
|
* and log any error so a seeding failure never fails the vault create.
|
|
73
68
|
*/
|
|
74
69
|
export async function seedOnboardingNotesBestEffort(store: Store): Promise<void> {
|
|
@@ -76,7 +71,7 @@ export async function seedOnboardingNotesBestEffort(store: Store): Promise<void>
|
|
|
76
71
|
await seedOnboardingNotes(store);
|
|
77
72
|
} catch (err) {
|
|
78
73
|
console.error(
|
|
79
|
-
`Note:
|
|
74
|
+
`Note: starter content not seeded (${(err as Error)?.message ?? err}). ` +
|
|
80
75
|
`The vault was still created; you can connect an AI and ask it to set things up.`,
|
|
81
76
|
);
|
|
82
77
|
}
|
package/src/routes.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Store, Note, QueryOpts } from "../core/src/types.ts";
|
|
15
15
|
import { TAG_EXPAND_MODES, stripTagHash, type TagExpandMode } from "../core/src/tag-hierarchy.ts";
|
|
16
16
|
import { listUnresolvedWikilinks } from "../core/src/wikilinks.ts";
|
|
17
|
+
import { transactionAsync } from "../core/src/txn.ts";
|
|
17
18
|
import { getNote, getNotes, getNoteTags, toNoteIndex, filterMetadata, mergeMetadata, MAX_BATCH_SIZE, validateExtension, ExtensionValidationError } from "../core/src/notes.ts";
|
|
18
19
|
import {
|
|
19
20
|
parseContentRange,
|
|
@@ -28,6 +29,10 @@ import * as linkOps from "../core/src/links.ts";
|
|
|
28
29
|
import * as tagSchemaOps from "../core/src/tag-schemas.ts";
|
|
29
30
|
import { IndexedFieldError } from "../core/src/indexed-fields.ts";
|
|
30
31
|
import { buildVaultProjection, resolveTagInheritance } from "../core/src/vault-projection.ts";
|
|
32
|
+
import {
|
|
33
|
+
resolveTranscriptionCapability,
|
|
34
|
+
type TranscriptionCapability,
|
|
35
|
+
} from "./transcription/capability.ts";
|
|
31
36
|
import { loadSchemaConfig } from "../core/src/schema-defaults.ts";
|
|
32
37
|
import {
|
|
33
38
|
buildExpandVisibility,
|
|
@@ -1143,17 +1148,16 @@ async function handleNotesInner(
|
|
|
1143
1148
|
// don't collide with concurrent single-item callers on the shared
|
|
1144
1149
|
// bun:sqlite connection.
|
|
1145
1150
|
const batched = items.length > 1;
|
|
1146
|
-
|
|
1147
|
-
try {
|
|
1151
|
+
const runBatch = async (): Promise<void> => {
|
|
1148
1152
|
for (const item of items) {
|
|
1149
1153
|
// Validate extension before reaching the Store (vault#328).
|
|
1150
|
-
// Thrown inside the
|
|
1151
|
-
//
|
|
1154
|
+
// Thrown inside the batch transaction — rolls the batch back,
|
|
1155
|
+
// same shape as the path-conflict path.
|
|
1152
1156
|
const extension = item.extension !== undefined
|
|
1153
1157
|
? validateExtension(item.extension)
|
|
1154
1158
|
: undefined;
|
|
1155
1159
|
// Strict-schema gate (vault#299) — reject before any write so a
|
|
1156
|
-
// mid-batch violation rolls back
|
|
1160
|
+
// mid-batch violation rolls back the batch transaction.
|
|
1157
1161
|
gateStrictWrite(store, writeCtx, {
|
|
1158
1162
|
path: item.path,
|
|
1159
1163
|
tags: item.tags,
|
|
@@ -1181,9 +1185,10 @@ async function handleNotesInner(
|
|
|
1181
1185
|
|
|
1182
1186
|
created.push((await store.getNote(note.id)) ?? note);
|
|
1183
1187
|
}
|
|
1184
|
-
|
|
1188
|
+
};
|
|
1189
|
+
try {
|
|
1190
|
+
await (batched ? transactionAsync(db, runBatch) : runBatch());
|
|
1185
1191
|
} catch (e: any) {
|
|
1186
|
-
if (batched) db.exec("ROLLBACK");
|
|
1187
1192
|
// Duck-type for module-boundary robustness (matches the PATCH branch).
|
|
1188
1193
|
if (e && e.code === "PATH_CONFLICT") {
|
|
1189
1194
|
return json(
|
|
@@ -2327,11 +2332,24 @@ export async function handleVault(
|
|
|
2327
2332
|
store: Store,
|
|
2328
2333
|
vaultConfig: VaultConfigLike,
|
|
2329
2334
|
persist?: () => void,
|
|
2335
|
+
/**
|
|
2336
|
+
* Injection seam (scribe-fold Phase 1) for the transcription capability
|
|
2337
|
+
* probe on the GET landing. Production omits it and resolves the live
|
|
2338
|
+
* default (scribe-http) provider's availability; tests inject a deterministic
|
|
2339
|
+
* resolver. Kept separate from the `auto_transcribe` POLICY toggle: this
|
|
2340
|
+
* reports whether transcription is actually POSSIBLE (a provider is
|
|
2341
|
+
* configured AND available), which is what a surface gates its mic on.
|
|
2342
|
+
*/
|
|
2343
|
+
resolveCapability: () => Promise<TranscriptionCapability> = resolveTranscriptionCapability,
|
|
2330
2344
|
): Promise<Response> {
|
|
2331
2345
|
const url = new URL(req.url);
|
|
2332
2346
|
|
|
2333
2347
|
if (req.method === "GET") {
|
|
2334
2348
|
const result: Record<string, unknown> = vaultResponse(vaultConfig);
|
|
2349
|
+
// Transcription capability flag — `enabled` iff a provider is configured
|
|
2350
|
+
// and available. `minutes_remaining` is omitted (cloud/plan concern;
|
|
2351
|
+
// self-host is unmetered). This is the field Notes gates the mic on.
|
|
2352
|
+
result.transcription = await resolveCapability();
|
|
2335
2353
|
if (parseBool(parseQuery(url, "include_stats"), false)) {
|
|
2336
2354
|
result.stats = await store.getVaultStats();
|
|
2337
2355
|
}
|
package/src/server.ts
CHANGED
|
@@ -30,6 +30,20 @@ import { setTranscriptionWorker } from "./transcription-registry.ts";
|
|
|
30
30
|
import { assetsDir } from "./routes.ts";
|
|
31
31
|
import { resolveScribeAuthToken, ensureScribeBearer } from "./scribe-env.ts";
|
|
32
32
|
import { getCachedScribeUrl } from "./scribe-discovery.ts";
|
|
33
|
+
import { TranscribeCppProvider } from "./transcription/providers/transcribe-cpp.ts";
|
|
34
|
+
import { ParakeetMlxProvider } from "./transcription/providers/parakeet-mlx.ts";
|
|
35
|
+
import { OnnxAsrProvider } from "./transcription/providers/onnx-asr.ts";
|
|
36
|
+
import {
|
|
37
|
+
resolveTranscriptionProviderName,
|
|
38
|
+
resolveTranscribeCppPaths,
|
|
39
|
+
transcribeCppInstalled,
|
|
40
|
+
resolveParakeetMlxBin,
|
|
41
|
+
resolveParakeetMlxModel,
|
|
42
|
+
resolveOnnxAsrBin,
|
|
43
|
+
resolveOnnxAsrModel,
|
|
44
|
+
parakeetMlxInstalled,
|
|
45
|
+
onnxAsrInstalled,
|
|
46
|
+
} from "./transcription/select.ts";
|
|
33
47
|
import { readEnvFile, setEnvVar } from "./config.ts";
|
|
34
48
|
import { resolveBindHostname } from "./bind.ts";
|
|
35
49
|
import { MirrorManager } from "./mirror-manager.ts";
|
|
@@ -107,40 +121,102 @@ registerConfiguredTriggers();
|
|
|
107
121
|
* Authorization header on a 401 and transcription fails with a friendly
|
|
108
122
|
* error captured on the transcript note.
|
|
109
123
|
*/
|
|
110
|
-
const scribeUrl = getCachedScribeUrl();
|
|
111
124
|
let transcriptionWorker: TranscriptionWorker | null = null;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// pick up the just-written value without restart.
|
|
117
|
-
const { created, token } = ensureScribeBearer(readEnvFile, setEnvVar);
|
|
118
|
-
if (created) {
|
|
119
|
-
process.env.SCRIBE_AUTH_TOKEN = token;
|
|
120
|
-
console.log("[transcribe] generated SCRIBE_AUTH_TOKEN (32 bytes, base64url) — mirror this value into scribe's config");
|
|
121
|
-
}
|
|
122
|
-
transcriptionWorker = startTranscriptionWorker({
|
|
123
|
-
vaultList: () => listVaults(),
|
|
124
|
-
getStore: (name) => getVaultStore(name),
|
|
125
|
-
scribeUrl,
|
|
126
|
-
scribeToken: resolveScribeAuthToken(),
|
|
127
|
-
resolveAssetsDir: (vault) => assetsDir(vault),
|
|
128
|
-
getAudioRetention: (vault) => readVaultConfig(vault)?.audio_retention ?? "keep",
|
|
129
|
-
getContextPredicates: (vault) => readVaultConfig(vault)?.transcription?.context,
|
|
130
|
-
});
|
|
131
|
-
// Event-driven hot path — the `attachment:created` hook fires the worker
|
|
132
|
-
// in a microtask instead of waiting for the 30s sweep.
|
|
125
|
+
|
|
126
|
+
// Shared worker wiring: the event-driven `attachment:created` hot path + the
|
|
127
|
+
// REST-retry registry. Same for whichever provider the worker was built with.
|
|
128
|
+
function wireTranscriptionWorker(worker: TranscriptionWorker): void {
|
|
133
129
|
registerTranscriptionHook(
|
|
134
130
|
defaultHookRegistry,
|
|
135
|
-
|
|
131
|
+
worker,
|
|
136
132
|
(store) => getVaultNameForStore(store as never),
|
|
137
133
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
setTranscriptionWorker(worker);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fields the worker needs regardless of provider (queue/retention/context).
|
|
138
|
+
const commonWorkerOpts = {
|
|
139
|
+
vaultList: () => listVaults(),
|
|
140
|
+
getStore: (name: string) => getVaultStore(name),
|
|
141
|
+
resolveAssetsDir: (vault: string) => assetsDir(vault),
|
|
142
|
+
getAudioRetention: (vault: string) => readVaultConfig(vault)?.audio_retention ?? "keep",
|
|
143
|
+
getContextPredicates: (vault: string) => readVaultConfig(vault)?.transcription?.context,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Provider selection (scribe-fold Phase 2a). Default is `scribe-http` — unset
|
|
147
|
+
// TRANSCRIPTION_PROVIDER means the existing scribe-http path runs unchanged.
|
|
148
|
+
const providerName = resolveTranscriptionProviderName();
|
|
149
|
+
if (providerName === "transcribe-cpp") {
|
|
150
|
+
// Local, no-Python provider: subprocess a transcribe-cli. Only start the
|
|
151
|
+
// worker when a runnable CLI + model are actually present — otherwise every
|
|
152
|
+
// pending item would terminal-fail with `missing_provider`. (v0.1.1 ships a
|
|
153
|
+
// library, not a CLI, so this gate stays closed until TRANSCRIBE_CPP_BIN or a
|
|
154
|
+
// build-from-source CLI exists.) `transcribeCppInstalled` is a cheap
|
|
155
|
+
// existsSync check (no spawn), matching `available()`.
|
|
156
|
+
const tcPaths = resolveTranscribeCppPaths();
|
|
157
|
+
if (transcribeCppInstalled(tcPaths)) {
|
|
158
|
+
transcriptionWorker = startTranscriptionWorker({
|
|
159
|
+
...commonWorkerOpts,
|
|
160
|
+
provider: new TranscribeCppProvider({ binPath: tcPaths.binPath, modelPath: tcPaths.modelPath }),
|
|
161
|
+
});
|
|
162
|
+
wireTranscriptionWorker(transcriptionWorker);
|
|
163
|
+
console.log(`[transcribe] worker started → transcribe-cpp (${tcPaths.modelPath})`);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(
|
|
166
|
+
"[transcribe] TRANSCRIPTION_PROVIDER=transcribe-cpp but no runnable transcribe-cli + model installed — see `parachute-vault transcription status` (v0.1.1 ships a library, not a CLI; set TRANSCRIBE_CPP_BIN or build one)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
} else if (providerName === "parakeet-mlx" || providerName === "onnx-asr") {
|
|
170
|
+
// Python-based local providers (scribe-fold Phase 2b): subprocess the
|
|
171
|
+
// parakeet-mlx / onnx-asr CLI. Only start the worker when a runnable binary
|
|
172
|
+
// resolves (env override → managed venv → PATH) — otherwise every pending
|
|
173
|
+
// item would terminal-fail with `missing_provider`. Cheap existsSync check
|
|
174
|
+
// (no spawn), matching the providers' `available()`.
|
|
175
|
+
const installed = providerName === "parakeet-mlx" ? parakeetMlxInstalled() : onnxAsrInstalled();
|
|
176
|
+
if (installed) {
|
|
177
|
+
const provider =
|
|
178
|
+
providerName === "parakeet-mlx"
|
|
179
|
+
? new ParakeetMlxProvider({
|
|
180
|
+
binPath: resolveParakeetMlxBin(),
|
|
181
|
+
model: resolveParakeetMlxModel(),
|
|
182
|
+
})
|
|
183
|
+
: new OnnxAsrProvider({ binPath: resolveOnnxAsrBin(), model: resolveOnnxAsrModel() });
|
|
184
|
+
transcriptionWorker = startTranscriptionWorker({ ...commonWorkerOpts, provider });
|
|
185
|
+
wireTranscriptionWorker(transcriptionWorker);
|
|
186
|
+
console.log(`[transcribe] worker started → ${providerName}`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(
|
|
189
|
+
`[transcribe] TRANSCRIPTION_PROVIDER=${providerName} but no runnable ${providerName} binary found — run \`parachute-vault transcription install\` (see \`transcription status\`)`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
142
192
|
} else {
|
|
143
|
-
|
|
193
|
+
// Default scribe-http path — behavior-preserving (Phase 1). Start the worker
|
|
194
|
+
// if scribe is discoverable; otherwise transcription is a no-op.
|
|
195
|
+
//
|
|
196
|
+
// Scribe URL resolution order (per `scribe-discovery.ts`): `SCRIBE_URL` env
|
|
197
|
+
// var, then `~/.parachute/services.json` `parachute-scribe` entry, then none.
|
|
198
|
+
//
|
|
199
|
+
// Bearer generation (vault#353): if neither `SCRIBE_AUTH_TOKEN` nor the
|
|
200
|
+
// legacy `SCRIBE_TOKEN` is set, generate a fresh 32-byte base64url bearer
|
|
201
|
+
// and persist it to vault's `.env` so subsequent restarts use the same
|
|
202
|
+
// value. The operator (or hub install) mirrors this bearer to scribe.
|
|
203
|
+
const scribeUrl = getCachedScribeUrl();
|
|
204
|
+
if (scribeUrl) {
|
|
205
|
+
const { created, token } = ensureScribeBearer(readEnvFile, setEnvVar);
|
|
206
|
+
if (created) {
|
|
207
|
+
process.env.SCRIBE_AUTH_TOKEN = token;
|
|
208
|
+
console.log("[transcribe] generated SCRIBE_AUTH_TOKEN (32 bytes, base64url) — mirror this value into scribe's config");
|
|
209
|
+
}
|
|
210
|
+
transcriptionWorker = startTranscriptionWorker({
|
|
211
|
+
...commonWorkerOpts,
|
|
212
|
+
scribeUrl,
|
|
213
|
+
scribeToken: resolveScribeAuthToken(),
|
|
214
|
+
});
|
|
215
|
+
wireTranscriptionWorker(transcriptionWorker);
|
|
216
|
+
console.log(`[transcribe] worker started → ${scribeUrl}`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log("[transcribe] worker disabled (no scribe in services.json and SCRIBE_URL unset)");
|
|
219
|
+
}
|
|
144
220
|
}
|
|
145
221
|
|
|
146
222
|
if (process.env.VAULT_AUTH_TOKEN?.trim()) {
|
|
@@ -206,9 +282,10 @@ if (listVaults().length === 0) {
|
|
|
206
282
|
}
|
|
207
283
|
writeGlobalConfig(globalConfig);
|
|
208
284
|
console.log(`Auto-created vault "${vaultName}" (API key: ${fullKey})`);
|
|
209
|
-
// Seed the
|
|
210
|
-
//
|
|
211
|
-
//
|
|
285
|
+
// Seed the default packs (welcome web + capture tag, Getting Started
|
|
286
|
+
// guide) so a connected AI can self-orient and help set the vault up —
|
|
287
|
+
// same as the `create`/`init` CLI path. Idempotent + best-effort (never
|
|
288
|
+
// fails first boot). Mirrors createVault() in cli.ts.
|
|
212
289
|
await seedOnboardingNotesBestEffort(getVaultStore(vaultName));
|
|
213
290
|
} else if (firstBoot.source === "env-invalid") {
|
|
214
291
|
// PARACHUTE_VAULT_NAME was set but failed validation — do NOT silently
|