@schlessera/brain-ui-server 0.13.0 → 0.14.0

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 (182) hide show
  1. package/README.md +51 -12
  2. package/dist/agent/backend.d.ts +163 -37
  3. package/dist/agent/backend.d.ts.map +1 -1
  4. package/dist/agent/backend.js +281 -286
  5. package/dist/agent/backend.js.map +1 -1
  6. package/dist/app.d.ts +46 -5
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +94 -35
  9. package/dist/app.js.map +1 -1
  10. package/dist/brain/client.d.ts +41 -30
  11. package/dist/brain/client.d.ts.map +1 -1
  12. package/dist/brain/client.js +138 -132
  13. package/dist/brain/client.js.map +1 -1
  14. package/dist/config/env.d.ts +104 -0
  15. package/dist/config/env.d.ts.map +1 -0
  16. package/dist/config/env.js +296 -0
  17. package/dist/config/env.js.map +1 -0
  18. package/dist/cron/scheduler.d.ts +16 -8
  19. package/dist/cron/scheduler.d.ts.map +1 -1
  20. package/dist/cron/scheduler.js +54 -53
  21. package/dist/cron/scheduler.js.map +1 -1
  22. package/dist/db/brain-db.d.ts +50 -0
  23. package/dist/db/brain-db.d.ts.map +1 -0
  24. package/dist/db/brain-db.js +75 -0
  25. package/dist/db/brain-db.js.map +1 -0
  26. package/dist/db/client.d.ts +8 -6
  27. package/dist/db/client.d.ts.map +1 -1
  28. package/dist/db/client.js +12 -25
  29. package/dist/db/client.js.map +1 -1
  30. package/dist/db/settings.d.ts +5 -4
  31. package/dist/db/settings.d.ts.map +1 -1
  32. package/dist/db/settings.js +9 -12
  33. package/dist/db/settings.js.map +1 -1
  34. package/dist/files/walker.d.ts +6 -7
  35. package/dist/files/walker.d.ts.map +1 -1
  36. package/dist/files/walker.js +6 -9
  37. package/dist/files/walker.js.map +1 -1
  38. package/dist/graph/reader.d.ts +6 -6
  39. package/dist/graph/reader.d.ts.map +1 -1
  40. package/dist/graph/reader.js +22 -12
  41. package/dist/graph/reader.js.map +1 -1
  42. package/dist/index.d.ts +15 -9
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +20 -10
  45. package/dist/index.js.map +1 -1
  46. package/dist/middleware/auth.d.ts +17 -6
  47. package/dist/middleware/auth.d.ts.map +1 -1
  48. package/dist/middleware/auth.js +34 -43
  49. package/dist/middleware/auth.js.map +1 -1
  50. package/dist/middleware/origin.d.ts +1 -2
  51. package/dist/middleware/origin.d.ts.map +1 -1
  52. package/dist/middleware/origin.js +1 -7
  53. package/dist/middleware/origin.js.map +1 -1
  54. package/dist/middleware/passkeys.d.ts +29 -5
  55. package/dist/middleware/passkeys.d.ts.map +1 -1
  56. package/dist/middleware/passkeys.js +47 -76
  57. package/dist/middleware/passkeys.js.map +1 -1
  58. package/dist/middleware/tailscale.d.ts +14 -11
  59. package/dist/middleware/tailscale.d.ts.map +1 -1
  60. package/dist/middleware/tailscale.js +11 -10
  61. package/dist/middleware/tailscale.js.map +1 -1
  62. package/dist/routes/brain.d.ts +9 -154
  63. package/dist/routes/brain.d.ts.map +1 -1
  64. package/dist/routes/brain.js +187 -188
  65. package/dist/routes/brain.js.map +1 -1
  66. package/dist/routes/files.d.ts +4 -88
  67. package/dist/routes/files.d.ts.map +1 -1
  68. package/dist/routes/files.js +74 -71
  69. package/dist/routes/files.js.map +1 -1
  70. package/dist/routes/graph.d.ts +4 -255
  71. package/dist/routes/graph.d.ts.map +1 -1
  72. package/dist/routes/graph.js +67 -64
  73. package/dist/routes/graph.js.map +1 -1
  74. package/dist/routes/health.d.ts +8 -22
  75. package/dist/routes/health.d.ts.map +1 -1
  76. package/dist/routes/health.js +10 -10
  77. package/dist/routes/health.js.map +1 -1
  78. package/dist/routes/models.d.ts +7 -90
  79. package/dist/routes/models.d.ts.map +1 -1
  80. package/dist/routes/models.js +53 -51
  81. package/dist/routes/models.js.map +1 -1
  82. package/dist/routes/providers.d.ts +5 -34
  83. package/dist/routes/providers.d.ts.map +1 -1
  84. package/dist/routes/providers.js +13 -11
  85. package/dist/routes/providers.js.map +1 -1
  86. package/dist/routes/sessions.d.ts +7 -78
  87. package/dist/routes/sessions.d.ts.map +1 -1
  88. package/dist/routes/sessions.js +35 -34
  89. package/dist/routes/sessions.js.map +1 -1
  90. package/dist/routes/share.d.ts +7 -60
  91. package/dist/routes/share.d.ts.map +1 -1
  92. package/dist/routes/share.js +80 -77
  93. package/dist/routes/share.js.map +1 -1
  94. package/dist/routes/voice.d.ts +8 -91
  95. package/dist/routes/voice.d.ts.map +1 -1
  96. package/dist/routes/voice.js +62 -59
  97. package/dist/routes/voice.js.map +1 -1
  98. package/dist/share/staging.d.ts +4 -4
  99. package/dist/share/staging.d.ts.map +1 -1
  100. package/dist/share/staging.js +7 -8
  101. package/dist/share/staging.js.map +1 -1
  102. package/dist/voice/deepgram-token.d.ts +1 -1
  103. package/dist/voice/deepgram-token.d.ts.map +1 -1
  104. package/dist/voice/deepgram-token.js +1 -2
  105. package/dist/voice/deepgram-token.js.map +1 -1
  106. package/dist/voice/keyterm-builder.d.ts +23 -8
  107. package/dist/voice/keyterm-builder.d.ts.map +1 -1
  108. package/dist/voice/keyterm-builder.js +67 -43
  109. package/dist/voice/keyterm-builder.js.map +1 -1
  110. package/dist/voice/speech-providers.d.ts +6 -4
  111. package/dist/voice/speech-providers.d.ts.map +1 -1
  112. package/dist/voice/speech-providers.js +28 -25
  113. package/dist/voice/speech-providers.js.map +1 -1
  114. package/dist/ws/bridge.d.ts.map +1 -1
  115. package/dist/ws/bridge.js +2 -3
  116. package/dist/ws/bridge.js.map +1 -1
  117. package/dist/ws/clients.d.ts +18 -10
  118. package/dist/ws/clients.d.ts.map +1 -1
  119. package/dist/ws/clients.js +35 -37
  120. package/dist/ws/clients.js.map +1 -1
  121. package/dist/ws/connection.d.ts.map +1 -1
  122. package/dist/ws/connection.js +5 -7
  123. package/dist/ws/connection.js.map +1 -1
  124. package/dist/ws/dispatch.d.ts.map +1 -1
  125. package/dist/ws/dispatch.js +1 -2
  126. package/dist/ws/dispatch.js.map +1 -1
  127. package/dist/ws/host.d.ts +16 -12
  128. package/dist/ws/host.d.ts.map +1 -1
  129. package/dist/ws/host.js +14 -23
  130. package/dist/ws/host.js.map +1 -1
  131. package/dist/ws/routing.d.ts +2 -1
  132. package/dist/ws/routing.d.ts.map +1 -1
  133. package/dist/ws/routing.js +5 -6
  134. package/dist/ws/routing.js.map +1 -1
  135. package/dist/ws/run-session.js +1 -1
  136. package/dist/ws/run-session.js.map +1 -1
  137. package/dist/ws/session-catalog.d.ts +2 -2
  138. package/dist/ws/session-catalog.d.ts.map +1 -1
  139. package/dist/ws/session-catalog.js +2 -3
  140. package/dist/ws/session-catalog.js.map +1 -1
  141. package/package.json +8 -5
  142. package/src/agent/backend.ts +509 -340
  143. package/src/app.ts +157 -36
  144. package/src/brain/client.ts +166 -143
  145. package/src/config/env.ts +401 -0
  146. package/src/cron/scheduler.ts +77 -59
  147. package/src/db/brain-db.ts +104 -0
  148. package/src/db/client.ts +12 -31
  149. package/src/db/settings.ts +12 -14
  150. package/src/files/walker.ts +6 -10
  151. package/src/graph/reader.ts +26 -15
  152. package/src/index.ts +56 -17
  153. package/src/middleware/auth.ts +51 -45
  154. package/src/middleware/origin.ts +1 -8
  155. package/src/middleware/passkeys.ts +72 -68
  156. package/src/middleware/tailscale.ts +22 -17
  157. package/src/routes/brain.ts +31 -30
  158. package/src/routes/files.ts +10 -7
  159. package/src/routes/graph.ts +9 -6
  160. package/src/routes/health.ts +17 -10
  161. package/src/routes/models.ts +19 -15
  162. package/src/routes/providers.ts +14 -15
  163. package/src/routes/sessions.ts +18 -11
  164. package/src/routes/share.ts +35 -24
  165. package/src/routes/voice.ts +78 -68
  166. package/src/share/staging.ts +10 -8
  167. package/src/voice/deepgram-token.ts +4 -2
  168. package/src/voice/keyterm-builder.ts +90 -56
  169. package/src/voice/speech-providers.ts +28 -24
  170. package/src/ws/bridge.ts +2 -3
  171. package/src/ws/clients.ts +38 -41
  172. package/src/ws/connection.ts +5 -7
  173. package/src/ws/dispatch.ts +1 -2
  174. package/src/ws/host.ts +24 -27
  175. package/src/ws/routing.ts +6 -10
  176. package/src/ws/run-session.ts +1 -1
  177. package/src/ws/session-catalog.ts +2 -3
  178. package/dist/ws/handler.d.ts +0 -34
  179. package/dist/ws/handler.d.ts.map +0 -1
  180. package/dist/ws/handler.js +0 -33
  181. package/dist/ws/handler.js.map +0 -1
  182. package/src/ws/handler.ts +0 -50
@@ -49,8 +49,10 @@ function invalid(param: string) {
49
49
  return { error: "invalid_param", param } as const;
50
50
  }
51
51
 
52
- export const graphRoutes = new Hono()
53
- .get("/graph/meta", (c) => c.json(getGraphMeta()))
52
+ export function createGraphRoutes(deps: { brainRoot: string }): Hono {
53
+ const { brainRoot } = deps;
54
+ return new Hono()
55
+ .get("/graph/meta", (c) => c.json(getGraphMeta({ brainPath: brainRoot })))
54
56
 
55
57
  .get("/graph/clusters", (c) => {
56
58
  const rawCommunity = c.req.query("community");
@@ -61,7 +63,7 @@ export const graphRoutes = new Hono()
61
63
  }
62
64
 
63
65
  try {
64
- return c.json(getClusters({ community, includeIsolates: c.req.query("isolates") === "1" }));
66
+ return c.json(getClusters({ brainPath: brainRoot, community, includeIsolates: c.req.query("isolates") === "1" }));
65
67
  } catch (err) {
66
68
  const { body, status } = errorResponse(err);
67
69
  return c.json(body, status);
@@ -77,7 +79,7 @@ export const graphRoutes = new Hono()
77
79
  if (direction === null) return c.json(invalid("direction"), 400);
78
80
 
79
81
  try {
80
- return c.json(getNeighborhood({ center, depth, direction }));
82
+ return c.json(getNeighborhood({ brainPath: brainRoot, center, depth, direction }));
81
83
  } catch (err) {
82
84
  const { body, status } = errorResponse(err);
83
85
  return c.json(body, status);
@@ -92,7 +94,7 @@ export const graphRoutes = new Hono()
92
94
  if (maxDepth === null) return c.json(invalid("maxDepth"), 400);
93
95
 
94
96
  try {
95
- return c.json(getDiscovery({ root, direction, maxDepth }));
97
+ return c.json(getDiscovery({ brainPath: brainRoot, root, direction, maxDepth }));
96
98
  } catch (err) {
97
99
  const { body, status } = errorResponse(err);
98
100
  return c.json(body, status);
@@ -104,9 +106,10 @@ export const graphRoutes = new Hono()
104
106
  if (staleDays === null) return c.json(invalid("staleDays"), 400);
105
107
 
106
108
  try {
107
- return c.json(getMaintenance({ staleDays }));
109
+ return c.json(getMaintenance({ brainPath: brainRoot, staleDays }));
108
110
  } catch (err) {
109
111
  const { body, status } = errorResponse(err);
110
112
  return c.json(body, status);
111
113
  }
112
114
  });
115
+ }
@@ -1,6 +1,4 @@
1
1
  import { Hono } from "hono";
2
- import { getCronStatus } from "../cron/scheduler.js";
3
- import { isTurnActive } from "../ws/handler.js";
4
2
 
5
3
  const startTime = Date.now();
6
4
 
@@ -15,15 +13,24 @@ export const healthRoutes = new Hono().get("/health", (c) => {
15
13
  });
16
14
  });
17
15
 
16
+ export interface StatusDeps {
17
+ /** Git SHA baked at build time (SOURCE_COMMIT), "dev" when unset. */
18
+ sourceCommit: string;
19
+ getCronStatus(): unknown;
20
+ isTurnActive(): boolean;
21
+ }
22
+
18
23
  // Operational status. Registered BEHIND the auth guard: it exposes the git SHA,
19
24
  // cron job errors (raw stderr with filesystem paths), and whether a turn is
20
25
  // active — none of which should be readable unauthenticated.
21
- export const statusRoutes = new Hono().get("/status", (c) => {
22
- return c.json({
23
- healthy: true,
24
- uptime: Date.now() - startTime,
25
- version: process.env.SOURCE_COMMIT ?? "dev",
26
- cronJobs: getCronStatus(),
27
- activeSession: isTurnActive(),
26
+ export function createStatusRoutes(deps: StatusDeps): Hono {
27
+ return new Hono().get("/status", (c) => {
28
+ return c.json({
29
+ healthy: true,
30
+ uptime: Date.now() - startTime,
31
+ version: deps.sourceCommit,
32
+ cronJobs: deps.getCronStatus(),
33
+ activeSession: deps.isTurnActive(),
34
+ });
28
35
  });
29
- });
36
+ }
@@ -6,22 +6,25 @@
6
6
  // was last refreshed and why it might be stale.
7
7
 
8
8
  import { Hono } from "hono";
9
+ import type { Database } from "bun:sqlite";
9
10
  import type {
10
11
  ModelCatalogEntry,
11
12
  ModelCatalogResponse,
12
13
  } from "@schlessera/brain-ui-sdk";
13
- import {
14
- getModelSource,
15
- invalidateProfiles,
16
- listAllProviders,
17
- } from "../agent/backend.js";
14
+ import type { BackendRegistry } from "../agent/backend.js";
18
15
  import { getHiddenModelIds, setHiddenModelIds } from "../db/settings.js";
19
16
 
20
- async function buildCatalog(): Promise<ModelCatalogResponse> {
21
- const source = await getModelSource();
22
- const hidden = new Set(getHiddenModelIds());
17
+ export function createModelRoutes(deps: {
18
+ registry: BackendRegistry;
19
+ db: Database;
20
+ }): Hono {
21
+ const { registry, db } = deps;
22
+
23
+ async function buildCatalog(): Promise<ModelCatalogResponse> {
24
+ const source = await registry.getModelSource();
25
+ const hidden = new Set(getHiddenModelIds(db));
23
26
  const models: ModelCatalogEntry[] = (
24
- await listAllProviders({ includeHidden: true })
27
+ await registry.listAllProviders({ includeHidden: true })
25
28
  ).map((profile) => ({ ...profile, hidden: hidden.has(profile.id) }));
26
29
 
27
30
  const state = source?.state();
@@ -36,11 +39,11 @@ async function buildCatalog(): Promise<ModelCatalogResponse> {
36
39
  };
37
40
  }
38
41
 
39
- export const modelRoutes = new Hono()
42
+ return new Hono()
40
43
  .get("/models", async (c) => {
41
44
  // Serves the cached roster immediately and refreshes behind the response
42
45
  // when stale; only a cold start (nothing cached) waits on the network.
43
- const source = await getModelSource();
46
+ const source = await registry.getModelSource();
44
47
  await source?.ensureFresh();
45
48
  return c.json(await buildCatalog());
46
49
  })
@@ -55,14 +58,14 @@ export const modelRoutes = new Hono()
55
58
  return c.json({ error: "hidden must be an array of profile ids" }, 400);
56
59
  }
57
60
 
58
- setHiddenModelIds(hidden as string[]);
61
+ setHiddenModelIds(db, hidden as string[]);
59
62
  // The picker reads through a memo — drop it so the change is immediate.
60
- invalidateProfiles();
63
+ registry.invalidateProfiles();
61
64
  return c.json(await buildCatalog());
62
65
  })
63
66
 
64
67
  .post("/models/refresh", async (c) => {
65
- const source = await getModelSource();
68
+ const source = await registry.getModelSource();
66
69
  if (!source) {
67
70
  return c.json(
68
71
  { error: "Model discovery is not available on this backend" },
@@ -80,6 +83,7 @@ export const modelRoutes = new Hono()
80
83
  }`
81
84
  );
82
85
  }
83
- invalidateProfiles();
86
+ registry.invalidateProfiles();
84
87
  return c.json(await buildCatalog());
85
88
  });
89
+ }
@@ -1,19 +1,18 @@
1
1
  import { Hono } from "hono";
2
- import {
3
- getBackendsInfo,
4
- getModelSource,
5
- listAllProviders,
6
- } from "../agent/backend.js";
2
+ import type { BackendRegistry } from "../agent/backend.js";
7
3
 
8
- export const providerRoutes = new Hono().get("/providers", async (c) => {
9
- // Keeps the roster current without a restart: serves the cached list and
10
- // refreshes behind the response when it has gone stale. Only a cold start
11
- // (nothing cached yet) waits on the Models API.
12
- const source = await getModelSource();
13
- await source?.ensureFresh();
4
+ export function createProviderRoutes(deps: { registry: BackendRegistry }): Hono {
5
+ const { registry } = deps;
6
+ return new Hono().get("/providers", async (c) => {
7
+ // Keeps the roster current without a restart: serves the cached list and
8
+ // refreshes behind the response when it has gone stale. Only a cold start
9
+ // (nothing cached yet) waits on the Models API.
10
+ const source = await registry.getModelSource();
11
+ await source?.ensureFresh();
14
12
 
15
- return c.json({
16
- providers: await listAllProviders(),
17
- backends: await getBackendsInfo(),
13
+ return c.json({
14
+ providers: await registry.listAllProviders(),
15
+ backends: await registry.getBackendsInfo(),
16
+ });
18
17
  });
19
- });
18
+ }
@@ -1,18 +1,24 @@
1
1
  import { Hono } from "hono";
2
- import { getBackendForSession, getBackends } from "../agent/backend.js";
3
- import { getDb } from "../db/client.js";
2
+ import type { Database } from "bun:sqlite";
3
+ import type { BackendRegistry } from "../agent/backend.js";
4
4
 
5
- function getStoredBackendId(sessionId: string): string | null {
6
- const row = getDb()
7
- .query("SELECT backend_id AS backendId FROM sessions WHERE id = ?")
8
- .get(sessionId) as { backendId: string | null } | null;
9
- return row?.backendId ?? null;
10
- }
5
+ export function createSessionRoutes(deps: {
6
+ registry: BackendRegistry;
7
+ db: Database;
8
+ }): Hono {
9
+ const { registry, db } = deps;
10
+
11
+ function getStoredBackendId(sessionId: string): string | null {
12
+ const row = db
13
+ .query("SELECT backend_id AS backendId FROM sessions WHERE id = ?")
14
+ .get(sessionId) as { backendId: string | null } | null;
15
+ return row?.backendId ?? null;
16
+ }
11
17
 
12
- export const sessionRoutes = new Hono()
18
+ return new Hono()
13
19
  .get("/sessions", async (c) => {
14
20
  try {
15
- const backends = await getBackends();
21
+ const backends = await registry.getBackends();
16
22
  const sessions = (
17
23
  await Promise.all(
18
24
  backends.map(async (backend) =>
@@ -37,7 +43,7 @@ export const sessionRoutes = new Hono()
37
43
  .get("/sessions/:id", async (c) => {
38
44
  const id = c.req.param("id");
39
45
  try {
40
- const backend = await getBackendForSession(getStoredBackendId(id));
46
+ const backend = await registry.getBackendForSession(getStoredBackendId(id));
41
47
  const messages = await backend.getHistory(id);
42
48
  return c.json({ id, messages });
43
49
  } catch (err) {
@@ -47,3 +53,4 @@ export const sessionRoutes = new Hono()
47
53
  );
48
54
  }
49
55
  });
56
+ }
@@ -82,39 +82,49 @@ export const shareTargetFallbackRoutes = new Hono().post("/share-target", (c) =>
82
82
  c.redirect("/?share_error=no_worker", 303)
83
83
  );
84
84
 
85
- /**
86
- * In-flight intakes. Each one holds its whole payload in memory while the
87
- * multipart parser runs, so without a bound the per-share cap multiplies by
88
- * however many clients ask at once — on a box that also runs headless Chrome
89
- * for the renderer.
90
- */
91
- let inFlight = 0;
92
-
93
- /** At most one sweep per interval: the intake path should not stat the inbox on every upload. */
94
- let lastPrune = 0;
95
85
  const PRUNE_INTERVAL_MS = 10 * 60 * 1000;
96
86
 
97
- function maybePrune(): void {
98
- const now = Date.now();
99
- if (now - lastPrune < PRUNE_INTERVAL_MS) return;
100
- lastPrune = now;
101
- // Deliberately not awaited: pruning is housekeeping, and the client is
102
- // waiting on the staging result, not on it.
103
- void pruneShareStaging().catch((err) => {
104
- console.error("[share] prune failed:", err);
105
- });
106
- }
107
-
108
87
  function firstString(form: FormData, name: string): string | undefined {
109
88
  const value = form.get(name);
110
89
  return typeof value === "string" && value.trim() ? value : undefined;
111
90
  }
112
91
 
113
- export const shareRoutes = new Hono().post("/share", async (c) => {
92
+ export interface ShareRoutesDeps {
93
+ brainRoot: string;
94
+ /** ALLOWED_ORIGINS — the same-origin check's split-topology allowlist. */
95
+ allowedOrigins: string[];
96
+ }
97
+
98
+ export function createShareRoutes(deps: ShareRoutesDeps): Hono {
99
+ const { brainRoot, allowedOrigins } = deps;
100
+
101
+ /**
102
+ * In-flight intakes. Each one holds its whole payload in memory while the
103
+ * multipart parser runs, so without a bound the per-share cap multiplies by
104
+ * however many clients ask at once — on a box that also runs headless Chrome
105
+ * for the renderer.
106
+ */
107
+ let inFlight = 0;
108
+
109
+ /** At most one sweep per interval: the intake path should not stat the inbox on every upload. */
110
+ let lastPrune = 0;
111
+
112
+ function maybePrune(): void {
113
+ const now = Date.now();
114
+ if (now - lastPrune < PRUNE_INTERVAL_MS) return;
115
+ lastPrune = now;
116
+ // Deliberately not awaited: pruning is housekeeping, and the client is
117
+ // waiting on the staging result, not on it.
118
+ void pruneShareStaging(brainRoot).catch((err) => {
119
+ console.error("[share] prune failed:", err);
120
+ });
121
+ }
122
+
123
+ return new Hono().post("/share", async (c) => {
114
124
  // See middleware/origin.ts: a multipart POST is a CORS-simple request, so it
115
125
  // reaches this route with no preflight, and in tailscale mode the credential
116
126
  // is the source IP. This route is only ever called by the app itself.
117
- if (!isSameOriginRequest(c)) {
127
+ if (!isSameOriginRequest(c, allowedOrigins)) {
118
128
  return c.json({ error: "cross_origin_rejected" }, 403);
119
129
  }
120
130
 
@@ -154,7 +164,7 @@ export const shareRoutes = new Hono().post("/share", async (c) => {
154
164
 
155
165
  inFlight += 1;
156
166
  try {
157
- const result = await stageShare({
167
+ const result = await stageShare(brainRoot, {
158
168
  title: firstString(form, "title"),
159
169
  text: firstString(form, "text"),
160
170
  url: firstString(form, "url"),
@@ -176,3 +186,4 @@ export const shareRoutes = new Hono().post("/share", async (c) => {
176
186
  inFlight -= 1;
177
187
  }
178
188
  });
189
+ }
@@ -4,77 +4,87 @@ import type {
4
4
  VoiceTokenResponse,
5
5
  VoiceSessionResponse,
6
6
  } from "@schlessera/brain-ui-sdk/protocol";
7
+ import type { VoiceConfig } from "../config/env.js";
7
8
  import { mintDeepgramToken } from "../voice/deepgram-token.js";
8
- import { getKeyterms } from "../voice/keyterm-builder.js";
9
+ import { getKeyterms, type KeytermSettings } from "../voice/keyterm-builder.js";
9
10
  import { pickSpeechProvider } from "../voice/speech-providers.js";
10
11
 
11
- export const voiceRoutes = new Hono()
12
- .post("/voice/session", async (c) => {
13
- try {
14
- const provider = pickSpeechProvider();
15
- // Only fetch domain keyterms when the provider can use them.
16
- const keyterms = provider.capabilities.keyterms
17
- ? getKeyterms(false).keyterms
18
- : [];
19
- const session = await provider.createSession({ keyterms });
20
- const body: VoiceSessionResponse = {
21
- providerId: provider.id,
22
- url: session.url,
23
- ...(session.token !== undefined ? { token: session.token } : {}),
24
- ...(session.params !== undefined ? { params: session.params } : {}),
25
- expiresAt: session.expiresAt,
26
- capabilities: provider.capabilities,
27
- };
28
- return c.json(body);
29
- } catch (err) {
30
- return c.json(
31
- { error: err instanceof Error ? err.message : "Voice session failed" },
32
- 500
33
- );
34
- }
35
- })
12
+ export interface VoiceRoutesDeps {
13
+ voice: VoiceConfig;
14
+ keyterms: KeytermSettings;
15
+ }
36
16
 
37
- // Deprecated alias, kept for client transition. Returns the old token shape
38
- // by minting a Deepgram token directly; new clients use /voice/session.
39
- .post("/voice/token", async (c) => {
40
- try {
41
- const { token, expiresAt } = await mintDeepgramToken(60);
42
- const body: VoiceTokenResponse = { token, expiresAt };
43
- return c.json(body);
44
- } catch (err) {
45
- return c.json(
46
- { error: err instanceof Error ? err.message : "Token mint failed" },
47
- 500
48
- );
49
- }
50
- })
17
+ export function createVoiceRoutes(deps: VoiceRoutesDeps): Hono {
18
+ const { voice, keyterms } = deps;
51
19
 
52
- .get("/voice/keyterms", async (c) => {
53
- const force = c.req.query("rebuild") === "1";
54
- try {
55
- const cache = getKeyterms(force);
56
- const body: VoiceKeytermsResponse = {
57
- keyterms: cache.keyterms,
58
- generatedAt: cache.generatedAt,
59
- count: cache.count,
60
- };
61
- return c.json(body);
62
- } catch (err) {
63
- return c.json(
64
- { error: err instanceof Error ? err.message : "Keyterms failed" },
65
- 500
66
- );
67
- }
68
- })
20
+ return new Hono()
21
+ .post("/voice/session", async (c) => {
22
+ try {
23
+ const provider = pickSpeechProvider(voice);
24
+ // Only fetch domain keyterms when the provider can use them.
25
+ const terms = provider.capabilities.keyterms
26
+ ? getKeyterms(keyterms, false).keyterms
27
+ : [];
28
+ const session = await provider.createSession({ keyterms: terms });
29
+ const body: VoiceSessionResponse = {
30
+ providerId: provider.id,
31
+ url: session.url,
32
+ ...(session.token !== undefined ? { token: session.token } : {}),
33
+ ...(session.params !== undefined ? { params: session.params } : {}),
34
+ expiresAt: session.expiresAt,
35
+ capabilities: provider.capabilities,
36
+ };
37
+ return c.json(body);
38
+ } catch (err) {
39
+ return c.json(
40
+ { error: err instanceof Error ? err.message : "Voice session failed" },
41
+ 500
42
+ );
43
+ }
44
+ })
69
45
 
70
- .get("/voice/overrides", async (c) => {
71
- try {
72
- const cache = getKeyterms(false);
73
- return c.json({ overrides: cache.overrides });
74
- } catch (err) {
75
- return c.json(
76
- { error: err instanceof Error ? err.message : "Overrides failed" },
77
- 500
78
- );
79
- }
80
- });
46
+ // Deprecated alias, kept for client transition. Returns the old token shape
47
+ // by minting a Deepgram token directly; new clients use /voice/session.
48
+ .post("/voice/token", async (c) => {
49
+ try {
50
+ const { token, expiresAt } = await mintDeepgramToken(voice.deepgramApiKey, 60);
51
+ const body: VoiceTokenResponse = { token, expiresAt };
52
+ return c.json(body);
53
+ } catch (err) {
54
+ return c.json(
55
+ { error: err instanceof Error ? err.message : "Token mint failed" },
56
+ 500
57
+ );
58
+ }
59
+ })
60
+
61
+ .get("/voice/keyterms", async (c) => {
62
+ const force = c.req.query("rebuild") === "1";
63
+ try {
64
+ const cache = getKeyterms(keyterms, force);
65
+ const body: VoiceKeytermsResponse = {
66
+ keyterms: cache.keyterms,
67
+ generatedAt: cache.generatedAt,
68
+ count: cache.count,
69
+ };
70
+ return c.json(body);
71
+ } catch (err) {
72
+ return c.json(
73
+ { error: err instanceof Error ? err.message : "Keyterms failed" },
74
+ 500
75
+ );
76
+ }
77
+ })
78
+
79
+ .get("/voice/overrides", async (c) => {
80
+ try {
81
+ const cache = getKeyterms(keyterms, false);
82
+ return c.json({ overrides: cache.overrides });
83
+ } catch (err) {
84
+ return c.json(
85
+ { error: err instanceof Error ? err.message : "Overrides failed" },
86
+ 500
87
+ );
88
+ }
89
+ });
90
+ }
@@ -12,7 +12,6 @@ import {
12
12
  type ShareIntakeResult,
13
13
  type ShareStagingManifest,
14
14
  } from "@schlessera/brain-ui-sdk/protocol";
15
- import { getBrainRoot } from "../files/walker.js";
16
15
 
17
16
  /**
18
17
  * Staging for an incoming system share.
@@ -123,9 +122,9 @@ export interface ShareInput {
123
122
  files: File[];
124
123
  }
125
124
 
126
- /** Absolute staging root. Read per call, so a test can move BRAIN_PATH. */
127
- export function shareStagingRoot(): string {
128
- return join(getBrainRoot(), SHARE_STAGING_DIR);
125
+ /** Absolute staging root inside the given brain repo. */
126
+ export function shareStagingRoot(brainPath: string): string {
127
+ return join(brainPath, SHARE_STAGING_DIR);
129
128
  }
130
129
 
131
130
  /**
@@ -270,7 +269,10 @@ async function countStaged(root: string): Promise<number> {
270
269
  * observe a share that is missing files or a manifest — not even if the process
271
270
  * is killed mid-write, which `try/catch` cleanup cannot cover.
272
271
  */
273
- export async function stageShare(input: ShareInput): Promise<ShareIntakeResult> {
272
+ export async function stageShare(
273
+ brainPath: string,
274
+ input: ShareInput
275
+ ): Promise<ShareIntakeResult> {
274
276
  const title = cleanTextField(input.title);
275
277
  const rawUrl = cleanTextField(input.url);
276
278
  const { url, leftover } = splitUrl(rawUrl);
@@ -297,7 +299,7 @@ export async function stageShare(input: ShareInput): Promise<ShareIntakeResult>
297
299
  throw new ShareTooLargeError("share_too_large", SHARE_MAX_TOTAL_BYTES);
298
300
  }
299
301
 
300
- const root = shareStagingRoot();
302
+ const root = shareStagingRoot(brainPath);
301
303
  if ((await countStaged(root)) >= SHARE_MAX_STAGED) {
302
304
  throw new ShareTooLargeError("inbox_full", SHARE_MAX_STAGED);
303
305
  }
@@ -392,8 +394,8 @@ export async function stageShare(input: ShareInput): Promise<ShareIntakeResult>
392
394
  * deployment can sweep at boot too — scheduling belongs to the container
393
395
  * crontab, and this sweep is cheap and bounded.
394
396
  */
395
- export async function pruneShareStaging(now = Date.now()): Promise<number> {
396
- const root = shareStagingRoot();
397
+ export async function pruneShareStaging(brainPath: string, now = Date.now()): Promise<number> {
398
+ const root = shareStagingRoot(brainPath);
397
399
  let entries;
398
400
  try {
399
401
  entries = await readdir(root, { withFileTypes: true });
@@ -32,11 +32,13 @@ function requestGrant(apiKey: string, ttlSeconds: number): Promise<Response> {
32
32
  });
33
33
  }
34
34
 
35
- export async function mintDeepgramToken(ttlSeconds = 60): Promise<{
35
+ export async function mintDeepgramToken(
36
+ apiKey: string | null,
37
+ ttlSeconds = 60
38
+ ): Promise<{
36
39
  token: string;
37
40
  expiresAt: number;
38
41
  }> {
39
- const apiKey = process.env.DEEPGRAM_API_KEY;
40
42
  if (!apiKey) {
41
43
  throw new Error("DEEPGRAM_API_KEY is not set");
42
44
  }