@gmickel/gno 1.12.4 → 1.13.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 (61) hide show
  1. package/README.md +57 -30
  2. package/assets/skill/SKILL.md +5 -0
  3. package/assets/skill/cli-reference.md +16 -6
  4. package/assets/skill/mcp-reference.md +22 -3
  5. package/package.json +2 -1
  6. package/src/app/constants.ts +43 -10
  7. package/src/app/index-name.ts +127 -0
  8. package/src/cli/commands/doctor-activation.ts +151 -0
  9. package/src/cli/commands/doctor.ts +41 -16
  10. package/src/cli/commands/get.ts +18 -0
  11. package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
  12. package/src/cli/commands/mcp/config-discovery.ts +42 -0
  13. package/src/cli/commands/mcp/config-editors.ts +432 -0
  14. package/src/cli/commands/mcp/config.ts +63 -160
  15. package/src/cli/commands/mcp/install.ts +75 -37
  16. package/src/cli/commands/mcp/paths.ts +141 -136
  17. package/src/cli/commands/mcp/server-entry.ts +66 -0
  18. package/src/cli/commands/mcp/status.ts +189 -57
  19. package/src/cli/commands/mcp/target-display.ts +30 -0
  20. package/src/cli/commands/mcp/uninstall.ts +29 -31
  21. package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
  22. package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
  23. package/src/cli/commands/multi-get.ts +31 -6
  24. package/src/cli/commands/status.ts +107 -11
  25. package/src/cli/program.ts +66 -20
  26. package/src/core/activation-connector-health.ts +19 -0
  27. package/src/core/activation-probe-plan.ts +321 -0
  28. package/src/core/activation-probe.ts +138 -0
  29. package/src/core/activation-receipt-store.ts +39 -0
  30. package/src/core/activation-status.ts +513 -0
  31. package/src/core/activation-verifier.ts +416 -0
  32. package/src/core/connector-environment.ts +68 -0
  33. package/src/core/connector-policy.ts +233 -0
  34. package/src/core/connector-verification-target.ts +150 -0
  35. package/src/core/connector-verifier.ts +497 -0
  36. package/src/core/indexed-reference.ts +33 -8
  37. package/src/core/runtime-entrypoint.ts +24 -0
  38. package/src/mcp/activation-verification-mode.ts +4 -0
  39. package/src/mcp/server.ts +9 -2
  40. package/src/sdk/client.ts +7 -0
  41. package/src/sdk/types.ts +1 -0
  42. package/src/serve/activation-health.ts +91 -0
  43. package/src/serve/background-runtime.ts +11 -1
  44. package/src/serve/connectors.ts +164 -19
  45. package/src/serve/public/components/BootstrapStatus.tsx +94 -1
  46. package/src/serve/public/components/FirstRunWizard.tsx +13 -51
  47. package/src/serve/public/components/HealthCenter.tsx +8 -2
  48. package/src/serve/public/globals.built.css +1 -1
  49. package/src/serve/public/pages/Connectors.tsx +216 -55
  50. package/src/serve/public/pages/Dashboard.tsx +1 -0
  51. package/src/serve/routes/api.ts +152 -8
  52. package/src/serve/server.ts +44 -9
  53. package/src/serve/status-model.ts +4 -0
  54. package/src/serve/status.ts +79 -35
  55. package/src/store/activation-receipts.ts +390 -0
  56. package/src/store/index.ts +8 -0
  57. package/src/store/migrations/012-activation-receipts.ts +38 -0
  58. package/src/store/migrations/013-fts-sync-marker.ts +39 -0
  59. package/src/store/migrations/index.ts +4 -0
  60. package/src/store/sqlite/adapter.ts +313 -53
  61. package/src/store/types.ts +118 -0
@@ -1,9 +1,11 @@
1
1
  import {
2
+ AlertCircleIcon,
2
3
  BotIcon,
3
4
  CheckCircle2Icon,
4
5
  CpuIcon,
5
6
  Loader2Icon,
6
7
  PlugZapIcon,
8
+ SearchCheckIcon,
7
9
  } from "lucide-react";
8
10
  import { useCallback, useEffect, useState } from "react";
9
11
 
@@ -40,12 +42,35 @@ interface ConnectorStatus {
40
42
 
41
43
  interface ConnectorsResponse {
42
44
  connectors: ConnectorStatus[];
45
+ collections: string[];
46
+ }
47
+
48
+ interface ConnectorVerificationResponse {
49
+ verification: {
50
+ collection: string;
51
+ lexicalReady: boolean;
52
+ connectorReady: boolean;
53
+ generatedAt: string;
54
+ stages: {
55
+ connector: {
56
+ status: "passed" | "pending" | "failed" | "skipped";
57
+ code?: string;
58
+ };
59
+ };
60
+ };
61
+ remediation: string | null;
43
62
  }
44
63
 
45
64
  export default function Connectors({ navigate }: PageProps) {
46
65
  const [connectors, setConnectors] = useState<ConnectorStatus[]>([]);
66
+ const [collections, setCollections] = useState<string[]>([]);
67
+ const [selectedCollection, setSelectedCollection] = useState("");
47
68
  const [error, setError] = useState<string | null>(null);
48
69
  const [installingId, setInstallingId] = useState<string | null>(null);
70
+ const [verifyingId, setVerifyingId] = useState<string | null>(null);
71
+ const [verificationById, setVerificationById] = useState<
72
+ Record<string, ConnectorVerificationResponse>
73
+ >({});
49
74
 
50
75
  const loadConnectors = useCallback(async () => {
51
76
  const { data, error: err } =
@@ -56,6 +81,11 @@ export default function Connectors({ navigate }: PageProps) {
56
81
  }
57
82
 
58
83
  setConnectors(data?.connectors ?? []);
84
+ const nextCollections = data?.collections ?? [];
85
+ setCollections(nextCollections);
86
+ setSelectedCollection((current) =>
87
+ nextCollections.includes(current) ? current : (nextCollections[0] ?? "")
88
+ );
59
89
  setError(null);
60
90
  }, []);
61
91
 
@@ -83,6 +113,37 @@ export default function Connectors({ navigate }: PageProps) {
83
113
  await loadConnectors();
84
114
  };
85
115
 
116
+ const handleVerify = async (connectorId: string) => {
117
+ if (!selectedCollection) {
118
+ setError("Add and index a collection before verifying retrieval.");
119
+ return;
120
+ }
121
+
122
+ setVerifyingId(connectorId);
123
+ setError(null);
124
+ const { data, error: err } = await apiFetch<ConnectorVerificationResponse>(
125
+ "/api/connectors/verify",
126
+ {
127
+ method: "POST",
128
+ body: JSON.stringify({
129
+ connectorId,
130
+ collection: selectedCollection,
131
+ }),
132
+ }
133
+ );
134
+ setVerifyingId(null);
135
+
136
+ if (err || !data) {
137
+ setError(err ?? "Connector verification could not be completed.");
138
+ return;
139
+ }
140
+
141
+ setVerificationById((current) => ({
142
+ ...current,
143
+ [connectorId]: data,
144
+ }));
145
+ };
146
+
86
147
  return (
87
148
  <div className="min-h-screen">
88
149
  <main className="mx-auto max-w-6xl p-8">
@@ -103,9 +164,29 @@ export default function Connectors({ navigate }: PageProps) {
103
164
  opt-in instead of something the app quietly enables for you.
104
165
  </p>
105
166
  </div>
106
- <Button onClick={() => navigate("/")} variant="outline">
107
- Back to Dashboard
108
- </Button>
167
+ <div className="flex flex-col items-end gap-3">
168
+ <Button onClick={() => navigate("/")} variant="outline">
169
+ Back to Dashboard
170
+ </Button>
171
+ {collections.length > 0 && (
172
+ <label className="flex items-center gap-2 font-mono text-muted-foreground text-xs">
173
+ Proof collection
174
+ <select
175
+ className="cursor-pointer rounded border border-border/60 bg-background px-2 py-1.5 text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
176
+ onChange={(event) =>
177
+ setSelectedCollection(event.currentTarget.value)
178
+ }
179
+ value={selectedCollection}
180
+ >
181
+ {collections.map((collection) => (
182
+ <option key={collection} value={collection}>
183
+ {collection}
184
+ </option>
185
+ ))}
186
+ </select>
187
+ </label>
188
+ )}
189
+ </div>
109
190
  </div>
110
191
 
111
192
  {error && (
@@ -115,62 +196,142 @@ export default function Connectors({ navigate }: PageProps) {
115
196
  )}
116
197
 
117
198
  <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
118
- {connectors.map((connector) => (
119
- <Card className="border-border/60 bg-card/70" key={connector.id}>
120
- <CardHeader className="space-y-3 pb-3">
121
- <div className="flex items-center justify-between gap-3">
122
- <div className="flex items-center gap-2">
123
- <BotIcon className="size-4 text-primary" />
124
- <CardTitle className="text-base">
125
- {connector.appName}
126
- </CardTitle>
199
+ {connectors.map((connector) => {
200
+ const verification = verificationById[connector.id];
201
+ return (
202
+ <Card className="border-border/60 bg-card/70" key={connector.id}>
203
+ <CardHeader className="space-y-3 pb-3">
204
+ <div className="flex items-center justify-between gap-3">
205
+ <div className="flex items-center gap-2">
206
+ <BotIcon className="size-4 text-primary" />
207
+ <CardTitle className="text-base">
208
+ {connector.appName}
209
+ </CardTitle>
210
+ </div>
211
+ {connector.installed ? (
212
+ <CheckCircle2Icon className="size-4 text-green-500" />
213
+ ) : (
214
+ <CpuIcon className="size-4 text-muted-foreground" />
215
+ )}
127
216
  </div>
128
- {connector.installed ? (
129
- <CheckCircle2Icon className="size-4 text-green-500" />
130
- ) : (
131
- <CpuIcon className="size-4 text-muted-foreground" />
217
+ <CardDescription>{connector.summary}</CardDescription>
218
+ </CardHeader>
219
+ <CardContent className="space-y-4 text-sm">
220
+ <div className="rounded-lg border border-border/50 bg-background/70 p-3">
221
+ <div className="mb-1 font-medium">
222
+ {connector.mode.label}
223
+ </div>
224
+ <p className="text-muted-foreground">
225
+ {connector.mode.detail}
226
+ </p>
227
+ </div>
228
+ <div className="space-y-1">
229
+ <div>
230
+ <span className="font-medium">Install type:</span>{" "}
231
+ {connector.installKind.toUpperCase()}
232
+ </div>
233
+ <div>
234
+ <span className="font-medium">Scope:</span>{" "}
235
+ {connector.scope}
236
+ </div>
237
+ <div className="font-mono text-muted-foreground text-xs">
238
+ {connector.path}
239
+ </div>
240
+ </div>
241
+ {connector.error && (
242
+ <p className="text-destructive text-sm">
243
+ {connector.error}
244
+ </p>
132
245
  )}
133
- </div>
134
- <CardDescription>{connector.summary}</CardDescription>
135
- </CardHeader>
136
- <CardContent className="space-y-4 text-sm">
137
- <div className="rounded-lg border border-border/50 bg-background/70 p-3">
138
- <div className="mb-1 font-medium">{connector.mode.label}</div>
139
246
  <p className="text-muted-foreground">
140
- {connector.mode.detail}
247
+ {connector.nextAction}
141
248
  </p>
142
- </div>
143
- <div className="space-y-1">
144
- <div>
145
- <span className="font-medium">Install type:</span>{" "}
146
- {connector.installKind.toUpperCase()}
147
- </div>
148
- <div>
149
- <span className="font-medium">Scope:</span>{" "}
150
- {connector.scope}
151
- </div>
152
- <div className="font-mono text-muted-foreground text-xs">
153
- {connector.path}
154
- </div>
155
- </div>
156
- {connector.error && (
157
- <p className="text-destructive text-sm">{connector.error}</p>
158
- )}
159
- <p className="text-muted-foreground">{connector.nextAction}</p>
160
- <Button
161
- className="w-full"
162
- onClick={() => void handleInstall(connector.id)}
163
- size="sm"
164
- variant={connector.installed ? "outline" : "default"}
165
- >
166
- {installingId === connector.id ? (
167
- <Loader2Icon className="mr-2 size-4 animate-spin" />
168
- ) : null}
169
- {connector.installed ? "Reinstall" : "Install"}
170
- </Button>
171
- </CardContent>
172
- </Card>
173
- ))}
249
+ {connector.installed && connector.installKind === "skill" && (
250
+ <div className="rounded border border-secondary/30 bg-secondary/10 p-3">
251
+ <div className="flex items-center gap-2 font-medium text-secondary text-xs">
252
+ <AlertCircleIcon className="size-3.5" />
253
+ Runtime verification unavailable
254
+ </div>
255
+ <p className="mt-1 text-muted-foreground text-xs">
256
+ target_runtime_unverifiable this client exposes no
257
+ safe read-only runtime hook. The skill file is
258
+ installed, but GNO cannot claim the agent used it.
259
+ </p>
260
+ </div>
261
+ )}
262
+ {connector.installed && connector.installKind === "mcp" && (
263
+ <div className="space-y-3 border-border/50 border-t pt-4">
264
+ <div className="flex items-start gap-2">
265
+ <SearchCheckIcon className="mt-0.5 size-4 text-primary" />
266
+ <div>
267
+ <p className="font-medium text-sm">Retrieval proof</p>
268
+ <p className="text-muted-foreground text-xs">
269
+ Starts this configured MCP once and checks a real,
270
+ collection-scoped result. Runs only when requested.
271
+ </p>
272
+ </div>
273
+ </div>
274
+ <Button
275
+ className="w-full"
276
+ disabled={!selectedCollection || verifyingId !== null}
277
+ onClick={() => void handleVerify(connector.id)}
278
+ size="sm"
279
+ variant="secondary"
280
+ >
281
+ {verifyingId === connector.id ? (
282
+ <Loader2Icon className="mr-2 size-4 animate-spin" />
283
+ ) : (
284
+ <SearchCheckIcon className="mr-2 size-4" />
285
+ )}
286
+ Verify retrieval
287
+ </Button>
288
+ {collections.length === 0 && (
289
+ <p className="text-muted-foreground text-xs">
290
+ Add and index a collection before verifying retrieval.
291
+ </p>
292
+ )}
293
+ {verification && (
294
+ <div
295
+ aria-live="polite"
296
+ className={
297
+ verification.verification.stages.connector
298
+ .status === "passed"
299
+ ? "rounded border border-primary/30 bg-primary/10 p-3 text-xs"
300
+ : "rounded border border-secondary/30 bg-secondary/10 p-3 text-xs"
301
+ }
302
+ >
303
+ <p className="font-mono text-foreground uppercase tracking-wide">
304
+ {verification.verification.stages.connector.status}
305
+ {verification.verification.stages.connector.code
306
+ ? ` / ${verification.verification.stages.connector.code}`
307
+ : ""}
308
+ </p>
309
+ <p className="mt-1 text-muted-foreground">
310
+ Collection: {verification.verification.collection}
311
+ </p>
312
+ <p className="mt-2 text-foreground">
313
+ {verification.remediation ??
314
+ "Retrieval returned the expected indexed source. No action needed."}
315
+ </p>
316
+ </div>
317
+ )}
318
+ </div>
319
+ )}
320
+ <Button
321
+ className="w-full"
322
+ onClick={() => void handleInstall(connector.id)}
323
+ size="sm"
324
+ variant={connector.installed ? "outline" : "default"}
325
+ >
326
+ {installingId === connector.id ? (
327
+ <Loader2Icon className="mr-2 size-4 animate-spin" />
328
+ ) : null}
329
+ {connector.installed ? "Reinstall" : "Install"}
330
+ </Button>
331
+ </CardContent>
332
+ </Card>
333
+ );
334
+ })}
174
335
  </div>
175
336
  </main>
176
337
  </div>
@@ -520,6 +520,7 @@ export default function Dashboard({ navigate }: PageProps) {
520
520
  {status && (
521
521
  <div className="mb-10">
522
522
  <BootstrapStatus
523
+ activation={status.activation}
523
524
  bootstrap={status.bootstrap}
524
525
  onDownloadModels={() => void handleDownloadModels()}
525
526
  />
@@ -23,7 +23,12 @@ import type {
23
23
  SearchOptions,
24
24
  } from "../../pipeline/types";
25
25
  import type { SqliteAdapter } from "../../store/sqlite/adapter";
26
- import type { DocumentRow } from "../../store/types";
26
+ import type {
27
+ ActivationVerificationReceipt,
28
+ DocumentRow,
29
+ StorePort,
30
+ StoreResult,
31
+ } from "../../store/types";
27
32
  import type { DocumentEventBus } from "../doc-events";
28
33
  import type { EmbedScheduler } from "../embed-scheduler";
29
34
  import type { StartJobError } from "../jobs";
@@ -47,6 +52,10 @@ import {
47
52
  type PublicCaptureInput,
48
53
  } from "../../core/capture";
49
54
  import { writeCapturePlanFile } from "../../core/capture-write";
55
+ import {
56
+ type ConnectorVerificationCode,
57
+ getConnectorVerificationRemediation,
58
+ } from "../../core/connector-verifier";
50
59
  import {
51
60
  buildEditableCopyContent,
52
61
  deriveEditableCopyRelPath,
@@ -116,7 +125,11 @@ import {
116
125
  import { exportPublishArtifact } from "../../publish/export-service";
117
126
  import { buildBrowseTree, normalizeBrowsePath } from "../browse-tree";
118
127
  import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
119
- import { getConnectorStatuses, installConnector } from "../connectors";
128
+ import {
129
+ getConnectorStatuses,
130
+ installConnector,
131
+ verifyInstalledConnector,
132
+ } from "../connectors";
120
133
  import {
121
134
  downloadState,
122
135
  reloadServerContext,
@@ -153,6 +166,27 @@ export interface InstallConnectorRequestBody {
153
166
  reinstall?: boolean;
154
167
  }
155
168
 
169
+ export interface VerifyConnectorRequestBody {
170
+ connectorId: string;
171
+ collection: string;
172
+ }
173
+
174
+ interface ConnectorRouteDeps {
175
+ getStatuses: typeof getConnectorStatuses;
176
+ verify: (
177
+ id: string,
178
+ store: StorePort,
179
+ collection: string,
180
+ options?: { force?: boolean },
181
+ overrides?: { cwd?: string; homeDir?: string }
182
+ ) => Promise<StoreResult<ActivationVerificationReceipt>>;
183
+ }
184
+
185
+ const DEFAULT_CONNECTOR_ROUTE_DEPS: ConnectorRouteDeps = {
186
+ getStatuses: getConnectorStatuses,
187
+ verify: verifyInstalledConnector,
188
+ };
189
+
156
190
  export interface SearchRequestBody {
157
191
  query: string;
158
192
  // Only BM25 supported in web UI (vector/hybrid require LLM deps)
@@ -4272,18 +4306,120 @@ export function handleEmbedStatus(scheduler: EmbedScheduler | null): Response {
4272
4306
  });
4273
4307
  }
4274
4308
 
4275
- export async function handleConnectors(overrides?: {
4276
- cwd?: string;
4277
- homeDir?: string;
4278
- }): Promise<Response> {
4309
+ export async function handleConnectors(
4310
+ config: Config,
4311
+ overrides?: { cwd?: string; homeDir?: string },
4312
+ deps: ConnectorRouteDeps = DEFAULT_CONNECTOR_ROUTE_DEPS
4313
+ ): Promise<Response> {
4279
4314
  return jsonResponse({
4280
- connectors: await getConnectorStatuses(overrides),
4315
+ connectors: await deps.getStatuses(overrides),
4316
+ collections: config.collections
4317
+ .map(({ name }) => name)
4318
+ .sort((left, right) => left.localeCompare(right)),
4281
4319
  });
4282
4320
  }
4283
4321
 
4322
+ /**
4323
+ * Explicitly run one collection-scoped connector retrieval proof.
4324
+ * Passive connector/status routes never call this handler.
4325
+ */
4326
+ export async function handleVerifyConnector(
4327
+ config: Config,
4328
+ store: StorePort,
4329
+ req: Request,
4330
+ overrides?: { cwd?: string; homeDir?: string },
4331
+ deps: ConnectorRouteDeps = DEFAULT_CONNECTOR_ROUTE_DEPS
4332
+ ): Promise<Response> {
4333
+ let body: unknown;
4334
+ try {
4335
+ body = await req.json();
4336
+ } catch {
4337
+ return errorResponse("VALIDATION", "Invalid JSON body");
4338
+ }
4339
+
4340
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4341
+ return errorResponse("VALIDATION", "Request body must be an object");
4342
+ }
4343
+ const fields = Object.keys(body);
4344
+ const allowedFields = new Set(["connectorId", "collection"]);
4345
+ if (fields.some((field) => !allowedFields.has(field))) {
4346
+ return errorResponse("VALIDATION", "Request body has unknown fields");
4347
+ }
4348
+
4349
+ const { connectorId, collection } = body as Record<string, unknown>;
4350
+ if (
4351
+ typeof connectorId !== "string" ||
4352
+ connectorId.length === 0 ||
4353
+ connectorId.length > 128
4354
+ ) {
4355
+ return errorResponse("VALIDATION", "Missing or invalid connectorId");
4356
+ }
4357
+ if (
4358
+ typeof collection !== "string" ||
4359
+ collection.length === 0 ||
4360
+ collection.length > 64
4361
+ ) {
4362
+ return errorResponse("VALIDATION", "Missing or invalid collection");
4363
+ }
4364
+ if (!config.collections.some(({ name }) => name === collection)) {
4365
+ return errorResponse("VALIDATION", "Unknown collection");
4366
+ }
4367
+
4368
+ try {
4369
+ const statuses = await deps.getStatuses(overrides);
4370
+ const connector = statuses.find(({ id }) => id === connectorId);
4371
+ if (!connector) {
4372
+ return errorResponse("VALIDATION", "Unknown connectorId");
4373
+ }
4374
+
4375
+ const result = await deps.verify(
4376
+ connectorId,
4377
+ store,
4378
+ collection,
4379
+ { force: true },
4380
+ overrides
4381
+ );
4382
+ if (!result.ok) {
4383
+ return errorResponse(
4384
+ "CONNECTOR_VERIFICATION_FAILED",
4385
+ "Connector verification could not be completed",
4386
+ 500
4387
+ );
4388
+ }
4389
+
4390
+ const code = result.value.stages.connector.code;
4391
+ return jsonResponse({
4392
+ verification: {
4393
+ collection: result.value.collection,
4394
+ lexicalReady: result.value.ready,
4395
+ connectorReady: result.value.stages.connector.status === "passed",
4396
+ generatedAt: result.value.generatedAt,
4397
+ stages: { connector: result.value.stages.connector },
4398
+ },
4399
+ remediation: code
4400
+ ? getConnectorVerificationRemediation(
4401
+ code as ConnectorVerificationCode,
4402
+ connector.appName
4403
+ )
4404
+ : null,
4405
+ });
4406
+ } catch {
4407
+ return errorResponse(
4408
+ "CONNECTOR_VERIFICATION_FAILED",
4409
+ "Connector verification could not be completed",
4410
+ 500
4411
+ );
4412
+ }
4413
+ }
4414
+
4284
4415
  export async function handleInstallConnector(
4285
4416
  req: Request,
4286
- overrides?: { cwd?: string; homeDir?: string }
4417
+ overrides?: {
4418
+ cwd?: string;
4419
+ homeDir?: string;
4420
+ indexName?: string;
4421
+ configPath?: string;
4422
+ }
4287
4423
  ): Promise<Response> {
4288
4424
  let body: InstallConnectorRequestBody;
4289
4425
  try {
@@ -4391,6 +4527,14 @@ export async function routeApi(
4391
4527
  return handleCollections(config);
4392
4528
  }
4393
4529
 
4530
+ if (path === "/api/connectors" && req.method === "GET") {
4531
+ return handleConnectors(config);
4532
+ }
4533
+
4534
+ if (path === "/api/connectors/verify" && req.method === "POST") {
4535
+ return handleVerifyConnector(config, store, req);
4536
+ }
4537
+
4394
4538
  if (path === "/api/publish/export" && req.method === "POST") {
4395
4539
  return handlePublishExport(config, store, req);
4396
4540
  }
@@ -58,6 +58,7 @@ import {
58
58
  handleTrashDoc,
59
59
  handleUpdateCollection,
60
60
  handleUpdateDoc,
61
+ handleVerifyConnector,
61
62
  } from "./routes/api";
62
63
  import { handleGraph, handleGraphQuery } from "./routes/graph";
63
64
  import {
@@ -81,6 +82,13 @@ export interface ServeResult {
81
82
  error?: string;
82
83
  }
83
84
 
85
+ interface StartServerDependencies {
86
+ startBackgroundRuntime?: typeof startBackgroundRuntime;
87
+ serve?: typeof Bun.serve;
88
+ handleInstallConnector?: typeof handleInstallConnector;
89
+ waitForShutdown?: (signal: AbortSignal) => Promise<void>;
90
+ }
91
+
84
92
  // Hostname parsing helpers - preserved for future fetch handler use
85
93
  // function parseHostname(host: string): string { ... }
86
94
  // function isLoopback(hostname: string): boolean { ... }
@@ -135,11 +143,14 @@ function withSecurityHeaders(response: Response, isDev: boolean): Response {
135
143
  * Opens DB once, closes on SIGINT/SIGTERM.
136
144
  */
137
145
  export async function startServer(
138
- options: ServeOptions = {}
146
+ options: ServeOptions = {},
147
+ dependencies: StartServerDependencies = {}
139
148
  ): Promise<ServeResult> {
140
149
  const port = options.port ?? 3000;
141
150
  const isDev = process.env.NODE_ENV !== "production";
142
- const runtimeResult = await startBackgroundRuntime({
151
+ const runtimeResult = await (
152
+ dependencies.startBackgroundRuntime ?? startBackgroundRuntime
153
+ )({
143
154
  configPath: options.configPath,
144
155
  index: options.index,
145
156
  requireCollections: false,
@@ -171,7 +182,7 @@ export async function startServer(
171
182
  // Start server with try/catch for port-in-use etc.
172
183
  let server: ReturnType<typeof Bun.serve>;
173
184
  try {
174
- server = Bun.serve({
185
+ server = (dependencies.serve ?? Bun.serve)({
175
186
  port,
176
187
  hostname: "127.0.0.1", // Loopback only - no LAN exposure
177
188
 
@@ -216,7 +227,11 @@ export async function startServer(
216
227
  },
217
228
  },
218
229
  "/api/connectors": {
219
- GET: async () => withSecurityHeaders(await handleConnectors(), isDev),
230
+ GET: async () =>
231
+ withSecurityHeaders(
232
+ await handleConnectors(ctxHolder.config),
233
+ isDev
234
+ ),
220
235
  },
221
236
  "/api/connectors/install": {
222
237
  POST: async (req: Request) => {
@@ -224,7 +239,23 @@ export async function startServer(
224
239
  return withSecurityHeaders(forbiddenResponse(), isDev);
225
240
  }
226
241
  return withSecurityHeaders(
227
- await handleInstallConnector(req),
242
+ await (
243
+ dependencies.handleInstallConnector ?? handleInstallConnector
244
+ )(req, {
245
+ indexName: options.index,
246
+ configPath: runtime.actualConfigPath,
247
+ }),
248
+ isDev
249
+ );
250
+ },
251
+ },
252
+ "/api/connectors/verify": {
253
+ POST: async (req: Request) => {
254
+ if (!isRequestAllowed(req, port)) {
255
+ return withSecurityHeaders(forbiddenResponse(), isDev);
256
+ }
257
+ return withSecurityHeaders(
258
+ await handleVerifyConnector(ctxHolder.config, store, req),
228
259
  isDev
229
260
  );
230
261
  },
@@ -695,11 +726,15 @@ export async function startServer(
695
726
  console.log("Press Ctrl+C to stop");
696
727
 
697
728
  // Block until shutdown signal
698
- await new Promise<void>((resolve) => {
699
- shutdownController.signal.addEventListener("abort", () => resolve(), {
700
- once: true,
729
+ if (dependencies.waitForShutdown) {
730
+ await dependencies.waitForShutdown(shutdownController.signal);
731
+ } else {
732
+ await new Promise<void>((resolve) => {
733
+ shutdownController.signal.addEventListener("abort", () => resolve(), {
734
+ once: true,
735
+ });
701
736
  });
702
- });
737
+ }
703
738
 
704
739
  removeShutdownHandlers();
705
740
  try {
@@ -1,3 +1,5 @@
1
+ import type { ActivationStatus } from "../core/activation-status";
2
+
1
3
  export type HealthCheckStatus = "ok" | "warn" | "error";
2
4
 
3
5
  export type HealthActionKind =
@@ -29,6 +31,7 @@ export interface OnboardingStep {
29
31
  title: string;
30
32
  status: OnboardingStepStatus;
31
33
  detail: string;
34
+ action?: "add-collection" | "download-models" | "sync" | "embed";
32
35
  }
33
36
 
34
37
  export interface OnboardingState {
@@ -142,6 +145,7 @@ export interface AppStatusResponse {
142
145
  hybrid: boolean;
143
146
  answer: boolean;
144
147
  };
148
+ activation: ActivationStatus;
145
149
  onboarding: OnboardingState;
146
150
  health: HealthCenterState;
147
151
  background: BackgroundServiceState;