@gmickel/gno 0.29.1 → 0.30.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.
@@ -144,45 +144,48 @@ function AppContent({
144
144
  <div className="flex-1">
145
145
  <Page key={location} navigate={navigate} />
146
146
  </div>
147
- <footer className="border-t border-border/50 bg-background/80 py-4 text-center text-muted-foreground text-sm">
148
- <div className="flex items-center justify-center gap-4">
147
+ <footer className="border-t border-border/30 bg-background/60 py-6 text-center text-sm backdrop-blur-sm">
148
+ <div className="ornament mx-auto mb-4 max-w-[8rem] text-muted-foreground/20">
149
+ <span className="text-[10px]">◆</span>
150
+ </div>
151
+ <div className="flex items-center justify-center gap-5 text-muted-foreground/60">
149
152
  <button
150
- className="transition-colors hover:text-foreground"
153
+ className="transition-colors duration-300 hover:text-primary"
151
154
  onClick={() => navigate("/collections")}
152
155
  type="button"
153
156
  >
154
157
  Collections
155
158
  </button>
156
- <span className="text-border">·</span>
159
+ <span className="text-border/30">—</span>
157
160
  <a
158
- className="transition-colors hover:text-foreground"
161
+ className="transition-colors duration-300 hover:text-primary"
159
162
  href="https://github.com/gmickel/gno"
160
163
  rel="noopener noreferrer"
161
164
  target="_blank"
162
165
  >
163
166
  GitHub
164
167
  </a>
165
- <span className="text-border">·</span>
168
+ <span className="text-border/30">—</span>
166
169
  <a
167
- className="transition-colors hover:text-foreground"
170
+ className="transition-colors duration-300 hover:text-primary"
168
171
  href="https://discord.gg/nHEmyJB5tg"
169
172
  rel="noopener noreferrer"
170
173
  target="_blank"
171
174
  >
172
175
  Discord
173
176
  </a>
174
- <span className="text-border">·</span>
177
+ <span className="text-border/30">—</span>
175
178
  <a
176
- className="transition-colors hover:text-foreground"
179
+ className="transition-colors duration-300 hover:text-primary"
177
180
  href="https://gno.sh"
178
181
  rel="noopener noreferrer"
179
182
  target="_blank"
180
183
  >
181
184
  gno.sh
182
185
  </a>
183
- <span className="text-border">·</span>
186
+ <span className="text-border/30">—</span>
184
187
  <a
185
- className="transition-colors hover:text-foreground"
188
+ className="transition-colors duration-300 hover:text-primary"
186
189
  href="https://twitter.com/gmickel"
187
190
  rel="noopener noreferrer"
188
191
  target="_blank"
@@ -20,16 +20,16 @@ export function WorkspaceTabs({
20
20
  onNewTab,
21
21
  }: WorkspaceTabsProps) {
22
22
  return (
23
- <div className="border-border/50 border-b bg-background/90">
24
- <div className="mx-auto flex max-w-7xl items-center gap-2 overflow-x-auto px-4 py-2">
23
+ <div className="border-border/40 border-b bg-background/95 backdrop-blur-sm">
24
+ <div className="mx-auto flex max-w-7xl items-center gap-1.5 overflow-x-auto px-4 py-2">
25
25
  {tabs.map((tab) => {
26
26
  const active = tab.id === activeTabId;
27
27
  return (
28
28
  <div
29
- className={`flex items-center rounded-lg border px-2 py-1 ${
29
+ className={`group flex items-center rounded-lg border px-2 py-1 transition-all duration-200 ${
30
30
  active
31
- ? "border-primary/40 bg-primary/10 text-primary"
32
- : "border-border/60 bg-card/70"
31
+ ? "border-primary/40 bg-primary/10 text-primary shadow-[0_0_12px_-4px_hsl(var(--primary)/0.25)]"
32
+ : "border-border/30 bg-card/40 hover:border-border/60 hover:bg-card/70"
33
33
  }`}
34
34
  key={tab.id}
35
35
  >
@@ -41,6 +41,7 @@ export function WorkspaceTabs({
41
41
  {tab.label}
42
42
  </button>
43
43
  <Button
44
+ className="opacity-50 transition-opacity group-hover:opacity-100"
44
45
  onClick={() => onClose(tab.id)}
45
46
  size="icon-sm"
46
47
  variant="ghost"
@@ -50,8 +51,13 @@ export function WorkspaceTabs({
50
51
  </div>
51
52
  );
52
53
  })}
53
- <Button onClick={onNewTab} size="sm" variant="outline">
54
- <PlusIcon className="mr-2 size-4" />
54
+ <Button
55
+ className="border-dashed"
56
+ onClick={onNewTab}
57
+ size="sm"
58
+ variant="outline"
59
+ >
60
+ <PlusIcon className="mr-1.5 size-3.5" />
55
61
  New Tab
56
62
  </Button>
57
63
  </div>
@@ -98,16 +98,25 @@ body {
98
98
  margin: 0;
99
99
  min-height: 100vh;
100
100
 
101
- /* Subtle grid pattern */
101
+ /* Layered background: teal radial glow + refined grid */
102
102
  background-image:
103
- linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
104
- linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
105
- background-size: 40px 40px;
103
+ radial-gradient(
104
+ ellipse at 50% 0%,
105
+ hsl(var(--primary) / 0.05) 0%,
106
+ transparent 50%
107
+ ),
108
+ linear-gradient(hsl(var(--foreground) / 0.018) 1px, transparent 1px),
109
+ linear-gradient(90deg, hsl(var(--foreground) / 0.018) 1px, transparent 1px);
110
+ background-size:
111
+ 100% 100%,
112
+ 32px 32px,
113
+ 32px 32px;
114
+ background-attachment: fixed, scroll, scroll;
106
115
  }
107
116
 
108
117
  ::selection {
109
- background-color: hsl(var(--primary) / 0.15);
110
- color: hsl(var(--primary));
118
+ background-color: hsl(var(--primary) / 0.25);
119
+ color: hsl(var(--foreground));
111
120
  }
112
121
 
113
122
  /* Typography */
@@ -117,11 +126,14 @@ h3,
117
126
  h4,
118
127
  h5,
119
128
  h6 {
120
- /* System serif for local-first/offline operation */
121
- font-family: Georgia, "Times New Roman", serif;
122
- line-height: 1.25;
129
+ /* Distinctive serif stack - offline-safe system fonts */
130
+ font-family:
131
+ "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia,
132
+ serif;
133
+ line-height: 1.2;
123
134
  font-weight: 600;
124
- letter-spacing: -0.01em;
135
+ letter-spacing: -0.02em;
136
+ text-wrap: balance;
125
137
  }
126
138
 
127
139
  code,
@@ -208,6 +220,44 @@ mark {
208
220
  .stagger-4 {
209
221
  animation-delay: 0.4s;
210
222
  }
223
+ .stagger-5 {
224
+ animation-delay: 0.5s;
225
+ }
226
+ .stagger-6 {
227
+ animation-delay: 0.6s;
228
+ }
229
+
230
+ /* Slide-up reveal with spring-like ease */
231
+ @keyframes slide-up {
232
+ from {
233
+ opacity: 0;
234
+ transform: translateY(16px);
235
+ }
236
+ to {
237
+ opacity: 1;
238
+ transform: translateY(0);
239
+ }
240
+ }
241
+
242
+ .animate-slide-up {
243
+ animation: slide-up 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
244
+ }
245
+
246
+ /* Scale entrance */
247
+ @keyframes scale-in {
248
+ from {
249
+ opacity: 0;
250
+ transform: scale(0.96);
251
+ }
252
+ to {
253
+ opacity: 1;
254
+ transform: scale(1);
255
+ }
256
+ }
257
+
258
+ .animate-scale-in {
259
+ animation: scale-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
260
+ }
211
261
 
212
262
  /* Collapsible animations */
213
263
  @keyframes collapse-down {
@@ -283,6 +333,59 @@ mark {
283
333
  box-shadow: 0 8px 30px -10px hsl(var(--primary) / 0.2);
284
334
  }
285
335
 
336
+ /* Text glow for hero / display elements */
337
+ .glow-text {
338
+ text-shadow:
339
+ 0 0 40px hsl(var(--primary) / 0.3),
340
+ 0 0 80px hsl(var(--primary) / 0.15);
341
+ }
342
+
343
+ [data-theme="light"] .glow-text {
344
+ text-shadow: none;
345
+ }
346
+
347
+ /* Decorative ornamental divider */
348
+ .ornament {
349
+ display: flex;
350
+ align-items: center;
351
+ gap: 0.75rem;
352
+ }
353
+
354
+ .ornament::before,
355
+ .ornament::after {
356
+ content: "";
357
+ flex: 1;
358
+ height: 1px;
359
+ background: linear-gradient(
360
+ 90deg,
361
+ transparent,
362
+ hsl(var(--border)),
363
+ transparent
364
+ );
365
+ }
366
+
367
+ /* Warm glass variant with secondary/gold accent. Reserved for future cards. */
368
+ .glass-warm {
369
+ background: hsl(var(--card) / 0.8);
370
+ backdrop-filter: blur(16px) saturate(150%);
371
+ -webkit-backdrop-filter: blur(16px) saturate(150%);
372
+ border: 1px solid hsl(var(--secondary) / 0.12);
373
+ box-shadow: 0 0 30px -10px hsl(var(--secondary) / 0.08);
374
+ }
375
+
376
+ /* Vignette overlay for atmospheric depth. Requires position: relative on host. */
377
+ .vignette::after {
378
+ content: "";
379
+ position: absolute;
380
+ inset: 0;
381
+ pointer-events: none;
382
+ background: radial-gradient(
383
+ ellipse at center,
384
+ transparent 50%,
385
+ hsl(var(--background) / 0.4) 100%
386
+ );
387
+ }
388
+
286
389
  /* Focus ring with glow */
287
390
  .focus-glow:focus-visible {
288
391
  outline: 2px solid hsl(var(--primary));
@@ -783,21 +783,30 @@ export default function Ask({ navigate }: PageProps) {
783
783
  )}
784
784
 
785
785
  {conversation.length === 0 && (
786
- <div className="py-10 text-center md:py-14">
787
- <Sparkles className="mx-auto mb-4 size-12 text-primary/60" />
788
- <h2 className="mb-2 font-medium text-lg">Ask anything</h2>
789
- <p className="text-muted-foreground">
786
+ <div className="animate-scale-in py-14 text-center opacity-0 md:py-20">
787
+ <div className="relative mx-auto mb-6 size-16">
788
+ <div className="absolute inset-0 animate-pulse-glow rounded-full bg-primary/10" />
789
+ <Sparkles className="absolute inset-0 m-auto size-8 text-primary/70" />
790
+ </div>
791
+ <h2 className="mb-2 text-xl">Ask anything</h2>
792
+ <p className="mx-auto max-w-sm text-muted-foreground">
790
793
  {answerAvailable
791
794
  ? "Get AI-powered answers with citations from your documents"
792
795
  : "AI answers not available. Install a generation model to enable."}
793
796
  </p>
797
+ <div
798
+ aria-hidden="true"
799
+ className="ornament mx-auto mt-6 max-w-[8rem] text-muted-foreground/20"
800
+ >
801
+ <span className="text-[10px]">◆</span>
802
+ </div>
794
803
  </div>
795
804
  )}
796
805
 
797
806
  {conversation.map((entry) => (
798
807
  <div className="space-y-4" key={entry.id}>
799
808
  <div className="flex justify-end">
800
- <div className="max-w-[80%] rounded-lg bg-secondary px-4 py-3">
809
+ <div className="max-w-[80%] rounded-xl bg-secondary px-5 py-3 shadow-[0_0_20px_-8px_hsl(var(--secondary)/0.3)]">
801
810
  <p className="text-foreground">{entry.query}</p>
802
811
  </div>
803
812
  </div>
@@ -823,7 +832,7 @@ export default function Ask({ navigate }: PageProps) {
823
832
  {entry.response && (
824
833
  <>
825
834
  {entry.response.answer && (
826
- <div className="prose prose-sm prose-invert max-w-none rounded-lg bg-card/50 p-4">
835
+ <div className="prose prose-sm prose-invert max-w-none rounded-lg border border-border/30 bg-card/60 p-5 shadow-[0_0_30px_-10px_hsl(var(--primary)/0.08)]">
827
836
  <p className="whitespace-pre-wrap leading-relaxed">
828
837
  {renderAnswer(
829
838
  entry.response.answer,
@@ -129,7 +129,7 @@ function CollectionCard({
129
129
 
130
130
  return (
131
131
  <Card
132
- className="group relative cursor-pointer overflow-hidden transition-all hover:border-primary/30 hover:bg-card/90"
132
+ className="group relative cursor-pointer overflow-hidden transition-all duration-300 hover:border-primary/30 hover:bg-card/90 hover:shadow-[0_0_30px_-12px_hsl(var(--primary)/0.15)]"
133
133
  onClick={onBrowse}
134
134
  onKeyDown={(event) => {
135
135
  if (event.key === "Enter" || event.key === " ") {
@@ -373,20 +373,26 @@ export default function Dashboard({ navigate }: PageProps) {
373
373
  </div>
374
374
  )}
375
375
 
376
- <header className="relative border-border/50 border-b bg-card/50 backdrop-blur-sm">
377
- <div className="aurora-glow absolute inset-0 opacity-30" />
378
- <div className="relative px-8 py-12">
379
- <div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-start">
376
+ <header className="relative overflow-hidden border-border/50 border-b bg-card/50 backdrop-blur-sm">
377
+ <div className="aurora-glow absolute inset-0 opacity-40" />
378
+ <div className="relative px-8 py-14 md:py-16">
379
+ <div className="grid gap-8 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
380
380
  <div className="min-w-0">
381
- <div className="flex items-center gap-3">
382
- <GnoLogo className="size-8 shrink-0 text-primary" />
383
- <h1 className="font-bold text-4xl text-primary tracking-tight">
381
+ <div className="flex items-center gap-4">
382
+ <GnoLogo className="size-10 shrink-0 text-primary" />
383
+ <h1 className="glow-text font-bold text-5xl text-primary tracking-tighter">
384
384
  GNO
385
385
  </h1>
386
386
  </div>
387
- <p className="mt-4 text-lg text-muted-foreground">
387
+ <p className="mt-3 text-lg tracking-wide text-muted-foreground">
388
388
  Your Local Knowledge Index
389
389
  </p>
390
+ <div
391
+ aria-hidden="true"
392
+ className="ornament mt-5 max-w-[12rem] text-muted-foreground/30"
393
+ >
394
+ <span className="text-xs">✦</span>
395
+ </div>
390
396
  </div>
391
397
 
392
398
  <div className="flex flex-wrap items-center gap-3 md:justify-end">
@@ -441,7 +447,7 @@ export default function Dashboard({ navigate }: PageProps) {
441
447
 
442
448
  <nav className="mb-10 flex flex-wrap gap-4">
443
449
  <Button
444
- className="gap-2"
450
+ className="gap-2 shadow-[0_0_24px_-6px_hsl(var(--primary)/0.4)]"
445
451
  onClick={() => navigate("/search")}
446
452
  size="lg"
447
453
  >
@@ -449,7 +455,7 @@ export default function Dashboard({ navigate }: PageProps) {
449
455
  Search
450
456
  </Button>
451
457
  <Button
452
- className="gap-2"
458
+ className="gap-2 shadow-[0_0_24px_-6px_hsl(var(--secondary)/0.4)]"
453
459
  onClick={() => navigate("/ask")}
454
460
  size="lg"
455
461
  variant="secondary"
@@ -606,9 +612,9 @@ export default function Dashboard({ navigate }: PageProps) {
606
612
  )}
607
613
 
608
614
  {status && (
609
- <div className="mb-10 grid animate-fade-in gap-6 opacity-0 md:grid-cols-4">
610
- <Card className="group relative overflow-hidden border-primary/30 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent transition-all duration-300 hover:border-primary/50 hover:shadow-[0_0_30px_-10px_hsl(var(--primary)/0.3)]">
611
- <div className="pointer-events-none absolute -top-12 -right-12 size-32 rounded-full bg-primary/10 blur-2xl" />
615
+ <div className="mb-10 grid animate-slide-up gap-6 opacity-0 md:grid-cols-4">
616
+ <Card className="group relative overflow-hidden border-primary/30 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent transition-all duration-300 hover:border-primary/50 hover:shadow-[0_0_40px_-10px_hsl(var(--primary)/0.35)]">
617
+ <div className="pointer-events-none absolute -top-12 -right-12 size-36 rounded-full bg-primary/10 blur-3xl transition-all duration-500 group-hover:bg-primary/15" />
612
618
  <CardHeader className="relative pb-2">
613
619
  <CardDescription className="flex items-center gap-2 text-primary/80">
614
620
  <Database className="size-4" />
@@ -616,7 +622,7 @@ export default function Dashboard({ navigate }: PageProps) {
616
622
  </CardDescription>
617
623
  </CardHeader>
618
624
  <CardContent className="relative">
619
- <div className="font-bold text-5xl tracking-tight text-primary">
625
+ <div className="glow-text font-bold text-5xl tracking-tight text-primary">
620
626
  {status.totalDocuments.toLocaleString()}
621
627
  </div>
622
628
  <p className="mt-1 text-muted-foreground text-sm">
@@ -625,7 +631,7 @@ export default function Dashboard({ navigate }: PageProps) {
625
631
  </CardContent>
626
632
  </Card>
627
633
 
628
- <Card className="group stagger-1 animate-fade-in opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg">
634
+ <Card className="group stagger-1 animate-slide-up opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg">
629
635
  <CardHeader className="pb-2">
630
636
  <CardDescription className="flex items-center gap-2">
631
637
  <Layers className="size-4" />
@@ -640,7 +646,7 @@ export default function Dashboard({ navigate }: PageProps) {
640
646
  </Card>
641
647
 
642
648
  <Card
643
- className="group stagger-2 animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg"
649
+ className="group stagger-2 animate-slide-up cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg"
644
650
  onClick={openCollections}
645
651
  onKeyDown={(event) => {
646
652
  if (event.key === "Enter" || event.key === " ") {
@@ -673,7 +679,7 @@ export default function Dashboard({ navigate }: PageProps) {
673
679
  </Card>
674
680
 
675
681
  <Card
676
- className="group stagger-3 animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-secondary/50 hover:bg-secondary/5 hover:shadow-lg"
682
+ className="group stagger-3 animate-slide-up cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-secondary/50 hover:bg-secondary/5 hover:shadow-[0_0_30px_-10px_hsl(var(--secondary)/0.25)]"
677
683
  onClick={() => openCapture()}
678
684
  >
679
685
  <CardHeader className="pb-2">
@@ -697,9 +703,11 @@ export default function Dashboard({ navigate }: PageProps) {
697
703
  )}
698
704
 
699
705
  {status && status.collections.length > 0 && (
700
- <section className="stagger-3 animate-fade-in opacity-0">
706
+ <section className="stagger-4 animate-slide-up opacity-0">
701
707
  <div className="mb-6 flex items-center justify-between gap-4 border-border/50 border-b pb-3">
702
- <h2 className="font-semibold text-2xl">Collections</h2>
708
+ <h2 className="font-semibold text-2xl tracking-tight">
709
+ Collections
710
+ </h2>
703
711
  <Button onClick={openCollections} size="sm" variant="outline">
704
712
  Manage Collections
705
713
  </Button>
@@ -1057,7 +1057,7 @@ export default function Search({ navigate }: PageProps) {
1057
1057
  </div>
1058
1058
  {results.map((result, i) => (
1059
1059
  <Card
1060
- className="group animate-fade-in cursor-pointer opacity-0 transition-all hover:border-primary/50 hover:bg-card/80"
1060
+ className="group animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:border-primary/50 hover:bg-card/80 hover:shadow-[0_0_24px_-10px_hsl(var(--primary)/0.12)]"
1061
1061
  key={`${result.docid}-${i}`}
1062
1062
  onClick={() =>
1063
1063
  navigate(
@@ -8,13 +8,8 @@
8
8
 
9
9
  import type { ContextHolder } from "./routes/api";
10
10
 
11
- import { getIndexDbPath } from "../app/constants";
12
- import { getConfigPaths, isInitialized, loadConfig } from "../config";
13
- import { getActivePreset } from "../llm/registry";
14
- import { SqliteAdapter } from "../store/sqlite/adapter";
15
- import { createServerContext, disposeServerContext } from "./context";
11
+ import { startBackgroundRuntime } from "./background-runtime";
16
12
  import { DocumentEventBus } from "./doc-events";
17
- import { createEmbedScheduler } from "./embed-scheduler";
18
13
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
19
14
  import homepage from "./public/index.html";
20
15
  import {
@@ -57,7 +52,6 @@ import {
57
52
  handleDocSimilar,
58
53
  } from "./routes/links";
59
54
  import { forbiddenResponse, isRequestAllowed } from "./security";
60
- import { CollectionWatchService } from "./watch-service";
61
55
 
62
56
  export interface ServeOptions {
63
57
  /** Port to listen on (default: 3000) */
@@ -131,78 +125,18 @@ export async function startServer(
131
125
  ): Promise<ServeResult> {
132
126
  const port = options.port ?? 3000;
133
127
  const isDev = process.env.NODE_ENV !== "production";
134
-
135
- // Check initialization
136
- const initialized = await isInitialized(options.configPath);
137
- if (!initialized) {
138
- return { success: false, error: "GNO not initialized. Run: gno init" };
139
- }
140
-
141
- // Load config
142
- const configResult = await loadConfig(options.configPath);
143
- if (!configResult.ok) {
144
- return { success: false, error: configResult.error.message };
145
- }
146
- const config = configResult.value;
147
-
148
- // Open database once for server lifetime
149
- const store = new SqliteAdapter();
150
- const dbPath = getIndexDbPath(options.index);
151
- // Use actual config path (from options or default) for consistency
152
- const paths = getConfigPaths();
153
- const actualConfigPath = options.configPath ?? paths.configFile;
154
- store.setConfigPath(actualConfigPath);
155
-
156
- const openResult = await store.open(dbPath, config.ftsTokenizer);
157
- if (!openResult.ok) {
158
- return { success: false, error: openResult.error.message };
159
- }
160
-
161
- // Sync collections and contexts from config to DB (same as CLI initStore)
162
- const syncCollResult = await store.syncCollections(config.collections);
163
- if (!syncCollResult.ok) {
164
- await store.close();
165
- return { success: false, error: syncCollResult.error.message };
166
- }
167
- const syncCtxResult = await store.syncContexts(config.contexts ?? []);
168
- if (!syncCtxResult.ok) {
169
- await store.close();
170
- return { success: false, error: syncCtxResult.error.message };
171
- }
172
-
173
- // Create server context with LLM ports for hybrid search and AI answers
174
- // Use holder pattern to allow hot-reloading presets
175
- const ctx = await createServerContext(store, config);
176
-
177
- const ctxHolder: ContextHolder = {
178
- current: ctx,
179
- config, // Keep original config for reloading
180
- scheduler: null, // Will be set below
181
- eventBus: null,
182
- watchService: null,
183
- };
184
-
185
- // Create embed scheduler with getters (survives context/preset reloads)
186
- const scheduler = createEmbedScheduler({
187
- db: store.getRawDb(),
188
- getEmbedPort: () => ctxHolder.current.embedPort,
189
- getVectorIndex: () => ctxHolder.current.vectorIndex,
190
- getModelUri: () => getActivePreset(ctxHolder.config).embed,
128
+ const runtimeResult = await startBackgroundRuntime({
129
+ configPath: options.configPath,
130
+ index: options.index,
131
+ requireCollections: false,
132
+ eventBus: new DocumentEventBus(),
191
133
  });
192
- ctxHolder.scheduler = scheduler;
193
- ctxHolder.current.scheduler = scheduler;
194
- const eventBus = new DocumentEventBus();
195
- ctxHolder.eventBus = eventBus;
196
- ctxHolder.current.eventBus = eventBus;
197
- const watchService = new CollectionWatchService({
198
- collections: config.collections,
199
- store,
200
- scheduler,
201
- eventBus,
202
- });
203
- watchService.start();
204
- ctxHolder.watchService = watchService;
205
- ctxHolder.current.watchService = watchService;
134
+ if (!runtimeResult.success) {
135
+ return { success: false, error: runtimeResult.error };
136
+ }
137
+ const runtime = runtimeResult.runtime;
138
+ const store = runtime.store;
139
+ const ctxHolder: ContextHolder = runtime.ctxHolder;
206
140
 
207
141
  // Shutdown controller for clean lifecycle
208
142
  const shutdownController = new AbortController();
@@ -210,11 +144,7 @@ export async function startServer(
210
144
  // Graceful shutdown handler
211
145
  const shutdown = async () => {
212
146
  console.log("\nShutting down...");
213
- watchService.dispose();
214
- eventBus.close();
215
- scheduler.dispose();
216
- await disposeServerContext(ctxHolder.current);
217
- await store.close();
147
+ await runtime.dispose();
218
148
  shutdownController.abort();
219
149
  };
220
150
 
@@ -420,7 +350,12 @@ export async function startServer(
420
350
  },
421
351
  },
422
352
  "/api/events": {
423
- GET: () => withSecurityHeaders(eventBus.createResponse(), isDev),
353
+ GET: () =>
354
+ withSecurityHeaders(
355
+ runtime.eventBus?.createResponse() ??
356
+ new Response("event stream unavailable", { status: 503 }),
357
+ isDev
358
+ ),
424
359
  },
425
360
  "/api/tags": {
426
361
  GET: async (req: Request) => {
@@ -568,7 +503,7 @@ export async function startServer(
568
503
  },
569
504
  });
570
505
  } catch (e) {
571
- await store.close();
506
+ await runtime.dispose();
572
507
  return {
573
508
  success: false,
574
509
  error: e instanceof Error ? e.message : String(e),