@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,4 +1,10 @@
1
1
  import { DEFAULT_INDEX_NAME, parseUri } from "../app/constants";
2
+ import {
3
+ canonicalizeIndexName,
4
+ INDEX_NAME_REQUIREMENTS,
5
+ indexNamesMatch,
6
+ isValidIndexName,
7
+ } from "../app/index-name";
2
8
  import { parseRef } from "./ref-parser";
3
9
 
4
10
  export interface EffectiveIndexResolution {
@@ -6,12 +12,14 @@ export interface EffectiveIndexResolution {
6
12
  }
7
13
 
8
14
  function normalizeIndexName(indexName?: string): string {
9
- const normalized = indexName?.trim();
10
- return normalized || DEFAULT_INDEX_NAME;
15
+ return canonicalizeIndexName(indexName ?? DEFAULT_INDEX_NAME);
11
16
  }
12
17
 
13
18
  export function indexesMatch(left?: string, right?: string): boolean {
14
- return normalizeIndexName(left) === normalizeIndexName(right);
19
+ return indexNamesMatch(
20
+ left ?? DEFAULT_INDEX_NAME,
21
+ right ?? DEFAULT_INDEX_NAME
22
+ );
15
23
  }
16
24
 
17
25
  export function getExplicitRefIndex(ref: string): string | undefined {
@@ -28,13 +36,28 @@ export function resolveEffectiveIndex(
28
36
  ):
29
37
  | { ok: true; value: EffectiveIndexResolution }
30
38
  | { ok: false; error: string } {
31
- const explicitIndexes = new Set<string>();
39
+ if (activeIndexName !== undefined && !isValidIndexName(activeIndexName)) {
40
+ return {
41
+ ok: false,
42
+ error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
43
+ };
44
+ }
45
+ const explicitIndexes = new Map<string, string>();
32
46
  let hasUnindexedRef = false;
33
47
 
34
48
  for (const ref of refs) {
35
49
  const explicitIndex = getExplicitRefIndex(ref);
36
- if (explicitIndex) {
37
- explicitIndexes.add(explicitIndex);
50
+ if (explicitIndex !== undefined) {
51
+ if (!isValidIndexName(explicitIndex)) {
52
+ return {
53
+ ok: false,
54
+ error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
55
+ };
56
+ }
57
+ const identity = canonicalizeIndexName(explicitIndex);
58
+ if (!explicitIndexes.has(identity)) {
59
+ explicitIndexes.set(identity, explicitIndex);
60
+ }
38
61
  } else {
39
62
  hasUnindexedRef = true;
40
63
  }
@@ -43,13 +66,15 @@ export function resolveEffectiveIndex(
43
66
  if (explicitIndexes.size > 1) {
44
67
  return {
45
68
  ok: false,
46
- error: `References cannot mix explicit indexes: ${[...explicitIndexes]
69
+ error: `References cannot mix explicit indexes: ${[
70
+ ...explicitIndexes.values(),
71
+ ]
47
72
  .sort()
48
73
  .join(", ")}`,
49
74
  };
50
75
  }
51
76
 
52
- const explicitIndex = [...explicitIndexes][0];
77
+ const explicitIndex = [...explicitIndexes.values()][0];
53
78
  if (
54
79
  explicitIndex &&
55
80
  hasUnindexedRef &&
@@ -0,0 +1,24 @@
1
+ /** Canonical entrypoint for the GNO package executing this process. */
2
+
3
+ // node:path has no Bun equivalent for portable absolute path resolution.
4
+ import { posix, win32 } from "node:path";
5
+
6
+ /** Resolve the stable package entrypoint for a core-module directory. */
7
+ export function resolveGnoEntrypoint(
8
+ coreModuleDir: string,
9
+ platformName: NodeJS.Platform = process.platform
10
+ ): string {
11
+ const pathApi = platformName === "win32" ? win32 : posix;
12
+ return pathApi.resolve(coreModuleDir, "../index.ts");
13
+ }
14
+
15
+ /**
16
+ * Resolve the CLI entrypoint beside the currently loaded GNO runtime.
17
+ *
18
+ * This remains stable for source checkouts, globally installed npm packages,
19
+ * packed installs, and the staged desktop runtime because all ship `src/` with
20
+ * the same layout.
21
+ */
22
+ export function getCurrentGnoEntrypoint(): string {
23
+ return resolveGnoEntrypoint(import.meta.dir);
24
+ }
@@ -0,0 +1,4 @@
1
+ /** Internal child-process marker for read-only connector activation proofs. */
2
+
3
+ export const MCP_ACTIVATION_VERIFICATION_ENV =
4
+ "GNO_MCP_ACTIVATION_VERIFICATION";
package/src/mcp/server.ts CHANGED
@@ -16,6 +16,7 @@ import type { SqliteAdapter } from "../store/sqlite/adapter";
16
16
  import { MCP_SERVER_NAME, VERSION, getIndexDbPath } from "../app/constants";
17
17
  import { JobManager } from "../core/job-manager";
18
18
  import { envIsSet } from "../llm/policy";
19
+ import { MCP_ACTIVATION_VERIFICATION_ENV } from "./activation-verification-mode";
19
20
 
20
21
  // ─────────────────────────────────────────────────────────────────────────────
21
22
  // Simple Promise Mutex (avoids async-mutex dependency)
@@ -114,9 +115,14 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
114
115
  const { initStore } = await import("../cli/commands/shared.js");
115
116
 
116
117
  // Open DB once with index/config threading
118
+ const activationVerification = envIsSet(
119
+ process.env,
120
+ MCP_ACTIVATION_VERIFICATION_ENV
121
+ );
117
122
  const init = await initStore({
118
123
  indexName: options.indexName,
119
124
  configPath: options.configPath,
125
+ syncConfig: !activationVerification,
120
126
  });
121
127
 
122
128
  if (!init.ok) {
@@ -146,8 +152,9 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
146
152
  // Server instance ID (per-process)
147
153
  const serverInstanceId = crypto.randomUUID();
148
154
 
149
- const enableWrite =
150
- options.enableWrite ?? envIsSet(process.env, "GNO_MCP_ENABLE_WRITE");
155
+ const enableWrite = activationVerification
156
+ ? false
157
+ : (options.enableWrite ?? envIsSet(process.env, "GNO_MCP_ENABLE_WRITE"));
151
158
  const dbPath = getIndexDbPath(options.indexName);
152
159
  const writeLockPath = join(dirname(dbPath), ".mcp-write.lock");
153
160
  const jobManager = new JobManager({
package/src/sdk/client.ts CHANGED
@@ -40,6 +40,7 @@ import type {
40
40
  } from "./types";
41
41
 
42
42
  import { decorateUriForIndex, getIndexDbPath } from "../app/constants";
43
+ import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
43
44
  import {
44
45
  ConfigSchema,
45
46
  loadConfig,
@@ -1208,6 +1209,12 @@ class GnoClientImpl implements GnoClient {
1208
1209
  export async function createGnoClient(
1209
1210
  options: GnoClientInitOptions = {}
1210
1211
  ): Promise<GnoClient> {
1212
+ if (options.indexName !== undefined && !isValidIndexName(options.indexName)) {
1213
+ throw sdkError(
1214
+ "VALIDATION",
1215
+ `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`
1216
+ );
1217
+ }
1211
1218
  const state = await resolveClientState(options);
1212
1219
  return new GnoClientImpl(state);
1213
1220
  }
package/src/sdk/types.ts CHANGED
@@ -46,6 +46,7 @@ export interface GnoClientInitOptions {
46
46
  config?: Config;
47
47
  configPath?: string;
48
48
  dbPath?: string;
49
+ /** Filesystem-safe index name: 1-64 Unicode letters/marks/numbers plus ` ._-`. */
49
50
  indexName?: string;
50
51
  cacheDir?: string;
51
52
  downloadPolicy?: DownloadPolicy;
@@ -0,0 +1,91 @@
1
+ /** UI-neutral health checks derived from the shared activation contract. */
2
+
3
+ import type { ActivationStatus } from "../core/activation-status";
4
+ import type { HealthCheck } from "./status-model";
5
+
6
+ function countLabel(count: number, singular: string): string {
7
+ return `${count} ${count === 1 ? singular : `${singular}s`}`;
8
+ }
9
+
10
+ export function buildActivationCheck(
11
+ activation: ActivationStatus
12
+ ): HealthCheck {
13
+ if (activation.healthy) {
14
+ const semanticReasons = [
15
+ ...new Set(
16
+ activation.collections.map(
17
+ ({ semanticAvailability }) => semanticAvailability.code
18
+ )
19
+ ),
20
+ ];
21
+ return {
22
+ id: "retrieval-activation",
23
+ title: "Retrieval proof",
24
+ status: "ok",
25
+ summary: `${countLabel(activation.collections.length, "folder")} passed lexical retrieval`,
26
+ detail: `Lexical search is proven. Semantic availability is separate (${semanticReasons.join(", ")}).`,
27
+ actionLabel: "Run update",
28
+ actionKind: "sync",
29
+ };
30
+ }
31
+
32
+ const failed = activation.collections.filter(({ ready }) => !ready);
33
+ const first = failed[0];
34
+ const detail = first?.remediation
35
+ ? `${first.collection}: ${first.remediation.stage}/${first.remediation.code}. Run: ${first.remediation.command}`
36
+ : "Add and index a supported text collection, then check retrieval again.";
37
+ return {
38
+ id: "retrieval-activation",
39
+ title: "Retrieval proof",
40
+ status: activation.usable ? "warn" : "error",
41
+ summary: activation.usable
42
+ ? `${countLabel(failed.length, "folder")} failed lexical retrieval`
43
+ : "No folder passed lexical retrieval",
44
+ detail,
45
+ actionLabel: "Run update",
46
+ actionKind: "sync",
47
+ };
48
+ }
49
+
50
+ export function buildConnectorActivationCheck(
51
+ activation: ActivationStatus
52
+ ): HealthCheck | null {
53
+ const { projected, total, truncated } = activation.connectorProjection;
54
+ const omitted = total - projected;
55
+ const observed = activation.connectors.filter(
56
+ ({ code }) =>
57
+ code !== "connector_not_configured" &&
58
+ code !== "target_runtime_unverifiable"
59
+ );
60
+ if (observed.length === 0 && !truncated) {
61
+ return null;
62
+ }
63
+ const failed = observed.filter(({ status }) => status === "failed");
64
+ const incomplete = observed.filter(({ status }) => status !== "passed");
65
+ const first = failed[0] ?? incomplete[0] ?? observed[0];
66
+ const firstDetail = first
67
+ ? `${first.target} / ${first.collection}: ${first.status}${first.code ? `/${first.code}` : ""}${first.remediation ? `. ${first.remediation}` : ""}`
68
+ : null;
69
+ const projectionDetail = truncated
70
+ ? `${omitted} target/collection checks were omitted by the bounded status projection; no result is claimed for them.`
71
+ : null;
72
+ return {
73
+ id: "connector-activation",
74
+ title: "Connector proof",
75
+ status:
76
+ failed.length > 0
77
+ ? "error"
78
+ : incomplete.length > 0 || truncated
79
+ ? "warn"
80
+ : "ok",
81
+ summary:
82
+ failed.length > 0
83
+ ? `${countLabel(failed.length, "connector proof")} failed`
84
+ : incomplete.length > 0
85
+ ? `${countLabel(incomplete.length, "connector proof")} incomplete`
86
+ : truncated
87
+ ? `${projected} of ${total} connector target/collection checks projected`
88
+ : `${countLabel(observed.length, "connector proof")} passed`,
89
+ detail: [projectionDetail, firstDetail].filter(Boolean).join(" "),
90
+ };
91
+ }
@@ -1,3 +1,6 @@
1
+ // node:path resolve has no Bun equivalent for canonical process-relative paths.
2
+ import { resolve } from "node:path";
3
+
1
4
  import type { Config } from "../config/types";
2
5
  import type { SyncResult } from "../ingestion";
3
6
  import type { DocumentEventBus } from "./doc-events";
@@ -9,6 +12,7 @@ import type {
9
12
  } from "./watch-service";
10
13
 
11
14
  import { getIndexDbPath } from "../app/constants";
15
+ import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
12
16
  import {
13
17
  ensureDirectories,
14
18
  formatConfigWarnings,
@@ -89,6 +93,12 @@ export async function startBackgroundRuntime(
89
93
  options: BackgroundRuntimeOptions = {},
90
94
  deps: BackgroundRuntimeDeps = {}
91
95
  ): Promise<BackgroundRuntimeResult> {
96
+ if (options.index !== undefined && !isValidIndexName(options.index)) {
97
+ return {
98
+ success: false,
99
+ error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
100
+ };
101
+ }
92
102
  const syncAllService = deps.syncAllService
93
103
  ? (...args: Parameters<typeof defaultSyncService.syncAll>) =>
94
104
  deps.syncAllService!(...args)
@@ -123,7 +133,7 @@ export async function startBackgroundRuntime(
123
133
  const store = deps.storeFactory ? deps.storeFactory() : new SqliteAdapter();
124
134
  const dbPath = getIndexDbPath(options.index);
125
135
  const paths = (deps.getConfigPaths ?? getConfigPaths)();
126
- const actualConfigPath = options.configPath ?? paths.configFile;
136
+ const actualConfigPath = resolve(options.configPath ?? paths.configFile);
127
137
  store.setConfigPath(actualConfigPath);
128
138
 
129
139
  const openResult = await store.open(dbPath, config.ftsTokenizer);
@@ -1,14 +1,27 @@
1
1
  import type { McpScope, McpTarget } from "../cli/commands/mcp/paths";
2
2
  import type { SkillScope, SkillTarget } from "../cli/commands/skill/paths";
3
+ import type {
4
+ ConnectorVerifierOptions,
5
+ ConnectorVerificationTarget,
6
+ } from "../core/connector-verifier";
7
+ import type {
8
+ ActivationVerificationReceipt,
9
+ StorePort,
10
+ StoreResult,
11
+ } from "../store/types";
3
12
 
4
13
  import { installMcpToTarget } from "../cli/commands/mcp/install";
5
14
  import {
6
15
  buildMcpServerEntry,
7
16
  getTargetDisplayName,
8
17
  } from "../cli/commands/mcp/paths";
9
- import { checkMcpTargetStatus } from "../cli/commands/mcp/status";
18
+ import {
19
+ checkMcpTargetStatus,
20
+ toMcpConnectorVerificationTarget,
21
+ } from "../cli/commands/mcp/status";
10
22
  import { installSkillToTarget } from "../cli/commands/skill/install";
11
23
  import { resolveSkillPaths } from "../cli/commands/skill/paths";
24
+ import { verifyConnectorActivation } from "../core/connector-verifier";
12
25
 
13
26
  export interface ConnectorStatus {
14
27
  id: string;
@@ -47,6 +60,75 @@ interface McpConnectorDefinition {
47
60
 
48
61
  type ConnectorDefinition = SkillConnectorDefinition | McpConnectorDefinition;
49
62
 
63
+ interface SkillInspection {
64
+ installed: boolean;
65
+ path: string;
66
+ unavailable: boolean;
67
+ }
68
+
69
+ interface ConnectorInstallContext {
70
+ cwd?: string;
71
+ homeDir?: string;
72
+ indexName?: string;
73
+ configPath?: string;
74
+ }
75
+
76
+ const SKILL_PATH_UNAVAILABLE_ERROR =
77
+ "Skill path configuration is invalid or unavailable.";
78
+
79
+ function unresolvedSkillPath(target: SkillTarget): string {
80
+ return `unresolved-skill-path/${target}`;
81
+ }
82
+
83
+ async function inspectSkillConnector(
84
+ definition: SkillConnectorDefinition,
85
+ overrides?: { cwd?: string; homeDir?: string }
86
+ ): Promise<SkillInspection> {
87
+ try {
88
+ const paths = resolveSkillPaths({
89
+ scope: definition.scope,
90
+ target: definition.target,
91
+ ...overrides,
92
+ });
93
+ return {
94
+ installed: await Bun.file(`${paths.gnoDir}/SKILL.md`).exists(),
95
+ path: paths.gnoDir,
96
+ unavailable: false,
97
+ };
98
+ } catch {
99
+ return {
100
+ installed: false,
101
+ path: unresolvedSkillPath(definition.target),
102
+ unavailable: true,
103
+ };
104
+ }
105
+ }
106
+
107
+ function toSkillVerificationTarget(
108
+ definition: SkillConnectorDefinition,
109
+ inspection: SkillInspection
110
+ ): ConnectorVerificationTarget {
111
+ if (inspection.unavailable) {
112
+ return {
113
+ kind: "skill",
114
+ id: definition.id,
115
+ target: definition.target,
116
+ scope: definition.scope,
117
+ configPath: inspection.path,
118
+ installed: false,
119
+ configError: true,
120
+ };
121
+ }
122
+ return {
123
+ kind: "skill",
124
+ id: definition.id,
125
+ target: definition.target,
126
+ scope: definition.scope,
127
+ configPath: inspection.path,
128
+ installed: inspection.installed,
129
+ };
130
+ }
131
+
50
132
  const CONNECTOR_DEFINITIONS: ConnectorDefinition[] = [
51
133
  {
52
134
  id: "claude-code-skill",
@@ -139,28 +221,29 @@ export async function getConnectorStatuses(overrides?: {
139
221
  const statuses = await Promise.all(
140
222
  CONNECTOR_DEFINITIONS.map(async (definition) => {
141
223
  if (definition.installKind === "skill") {
142
- const paths = resolveSkillPaths({
143
- scope: definition.scope,
144
- target: definition.target,
145
- ...overrides,
146
- });
147
- const skillMdPath = `${paths.gnoDir}/SKILL.md`;
148
- const installed = await Bun.file(skillMdPath).exists();
224
+ const inspection = await inspectSkillConnector(definition, overrides);
149
225
  return {
150
226
  id: definition.id,
151
227
  appName: definition.appName,
152
228
  installKind: definition.installKind,
153
229
  target: definition.target,
154
230
  scope: definition.scope,
155
- installed,
156
- path: paths.gnoDir,
157
- summary: installed
158
- ? `${definition.appName} skill is installed.`
159
- : `${definition.appName} skill is not installed yet.`,
160
- nextAction: installed
161
- ? "Restart the agent to reload the skill."
162
- : "Install the skill from the app.",
231
+ installed: inspection.installed,
232
+ path: inspection.path,
233
+ summary: inspection.unavailable
234
+ ? `${definition.appName} skill path is unavailable.`
235
+ : inspection.installed
236
+ ? `${definition.appName} skill is installed.`
237
+ : `${definition.appName} skill is not installed yet.`,
238
+ nextAction: inspection.unavailable
239
+ ? "Fix the skill path configuration, then reload status."
240
+ : inspection.installed
241
+ ? "Restart the agent to reload the skill."
242
+ : "Install the skill from the app.",
163
243
  mode: definition.mode,
244
+ ...(inspection.unavailable
245
+ ? { error: SKILL_PATH_UNAVAILABLE_ERROR }
246
+ : {}),
164
247
  } satisfies ConnectorStatus;
165
248
  }
166
249
 
@@ -192,12 +275,34 @@ export async function getConnectorStatuses(overrides?: {
192
275
  return statuses;
193
276
  }
194
277
 
278
+ /** Inspect current connector configs without starting any connector runtime. */
279
+ export async function getConnectorVerificationTargets(overrides?: {
280
+ cwd?: string;
281
+ homeDir?: string;
282
+ }): Promise<ConnectorVerificationTarget[]> {
283
+ return Promise.all(
284
+ CONNECTOR_DEFINITIONS.map(async (definition) => {
285
+ if (definition.installKind === "skill") {
286
+ const inspection = await inspectSkillConnector(definition, overrides);
287
+ return toSkillVerificationTarget(definition, inspection);
288
+ }
289
+
290
+ const status = await checkMcpTargetStatus(
291
+ definition.target,
292
+ definition.scope,
293
+ overrides ?? {}
294
+ );
295
+ return toMcpConnectorVerificationTarget(definition.id, status);
296
+ })
297
+ );
298
+ }
299
+
195
300
  export async function installConnector(
196
301
  id: string,
197
302
  options?: {
198
303
  reinstall?: boolean;
199
304
  },
200
- overrides?: { cwd?: string; homeDir?: string }
305
+ overrides?: ConnectorInstallContext
201
306
  ): Promise<ConnectorStatus> {
202
307
  const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id);
203
308
  if (!definition) {
@@ -220,14 +325,21 @@ export async function installConnector(
220
325
  overrides
221
326
  );
222
327
  } else {
328
+ const targetOverrides = overrides
329
+ ? { cwd: overrides.cwd, homeDir: overrides.homeDir }
330
+ : undefined;
223
331
  await installMcpToTarget(
224
332
  definition.target,
225
333
  definition.scope,
226
- buildMcpServerEntry({ enableWrite: false }),
334
+ buildMcpServerEntry({
335
+ enableWrite: false,
336
+ indexName: overrides?.indexName,
337
+ configPath: overrides?.configPath,
338
+ }),
227
339
  {
228
340
  force: options?.reinstall ?? false,
229
341
  dryRun: false,
230
- ...overrides,
342
+ ...targetOverrides,
231
343
  }
232
344
  );
233
345
  }
@@ -253,3 +365,36 @@ export function getConnectorDisplayName(id: string): string {
253
365
 
254
366
  return definition.appName;
255
367
  }
368
+
369
+ /**
370
+ * Resolve and verify one connector without editing its client configuration.
371
+ * Kept separate from passive status listing because this starts a local MCP
372
+ * child and performs a real, collection-scoped retrieval smoke.
373
+ */
374
+ export async function verifyInstalledConnector(
375
+ id: string,
376
+ store: StorePort,
377
+ collection: string,
378
+ options?: ConnectorVerifierOptions,
379
+ overrides?: { cwd?: string; homeDir?: string }
380
+ ): Promise<StoreResult<ActivationVerificationReceipt>> {
381
+ const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id);
382
+ if (!definition) {
383
+ throw new Error(`Unknown connector: ${id}`);
384
+ }
385
+
386
+ let target: ConnectorVerificationTarget;
387
+ if (definition.installKind === "skill") {
388
+ const inspection = await inspectSkillConnector(definition, overrides);
389
+ target = toSkillVerificationTarget(definition, inspection);
390
+ } else {
391
+ const status = await checkMcpTargetStatus(
392
+ definition.target,
393
+ definition.scope,
394
+ overrides ?? {}
395
+ );
396
+ target = toMcpConnectorVerificationTarget(definition.id, status);
397
+ }
398
+
399
+ return verifyConnectorActivation(store, collection, target, options);
400
+ }
@@ -1,4 +1,6 @@
1
1
  import {
2
+ AlertCircleIcon,
3
+ CheckCircle2Icon,
2
4
  DownloadIcon,
3
5
  HardDriveIcon,
4
6
  PackageIcon,
@@ -7,6 +9,7 @@ import {
7
9
 
8
10
  import type { AppStatusResponse } from "../../status-model";
9
11
 
12
+ import { buildConnectorActivationCheck } from "../../activation-health";
10
13
  import { Button } from "./ui/button";
11
14
  import {
12
15
  Card,
@@ -18,6 +21,7 @@ import {
18
21
 
19
22
  interface BootstrapStatusProps {
20
23
  bootstrap: AppStatusResponse["bootstrap"];
24
+ activation: AppStatusResponse["activation"];
21
25
  onDownloadModels: () => void;
22
26
  }
23
27
 
@@ -37,11 +41,21 @@ function formatRole(
37
41
  }
38
42
 
39
43
  export function BootstrapStatus({
44
+ activation,
40
45
  bootstrap,
41
46
  onDownloadModels,
42
47
  }: BootstrapStatusProps) {
43
48
  const missingModels =
44
49
  bootstrap.models.totalCount - bootstrap.models.cachedCount;
50
+ const displayedConnectorCount = Math.min(activation.connectors.length, 8);
51
+ const hiddenProjectedConnectorCount =
52
+ activation.connectorProjection.projected - displayedConnectorCount;
53
+ const omittedConnectorCount =
54
+ activation.connectorProjection.total -
55
+ activation.connectorProjection.projected;
56
+ const connectorHealth = buildConnectorActivationCheck(activation);
57
+ const connectorsHealthy =
58
+ connectorHealth === null || connectorHealth.status === "ok";
45
59
 
46
60
  return (
47
61
  <section className="space-y-4">
@@ -64,7 +78,7 @@ export function BootstrapStatus({
64
78
  )}
65
79
  </div>
66
80
 
67
- <div className="grid gap-4 xl:grid-cols-3">
81
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
68
82
  <Card className="border-border/60 bg-card/70">
69
83
  <CardHeader className="pb-3">
70
84
  <div className="flex items-center gap-2">
@@ -127,6 +141,85 @@ export function BootstrapStatus({
127
141
  </div>
128
142
  </CardContent>
129
143
  </Card>
144
+
145
+ <Card className="border-border/60 bg-card/70">
146
+ <CardHeader className="pb-3">
147
+ <div className="flex items-center gap-2">
148
+ {activation.healthy && connectorsHealthy ? (
149
+ <CheckCircle2Icon className="size-4 text-emerald-500" />
150
+ ) : activation.usable ? (
151
+ <ServerCogIcon className="size-4 text-amber-500" />
152
+ ) : (
153
+ <AlertCircleIcon className="size-4 text-destructive" />
154
+ )}
155
+ <CardTitle className="text-base">Retrieval proof</CardTitle>
156
+ </div>
157
+ <CardDescription>
158
+ {activation.healthy
159
+ ? "Lexical retrieval proven"
160
+ : activation.usable
161
+ ? `Search usable in ${activation.collections.filter(({ ready }) => ready).length}/${activation.collections.length} folders`
162
+ : "Retrieval proof failed"}
163
+ </CardDescription>
164
+ </CardHeader>
165
+ <CardContent className="space-y-3 text-sm">
166
+ {activation.collections.length === 0 ? (
167
+ <p className="text-muted-foreground">
168
+ Add and index a text folder to prove retrieval.
169
+ </p>
170
+ ) : (
171
+ activation.collections.map((collection) => (
172
+ <div
173
+ className="rounded-lg border border-border/50 px-3 py-2"
174
+ key={collection.collection}
175
+ >
176
+ <p className="font-medium">{collection.collection}</p>
177
+ <p className="text-muted-foreground text-xs">
178
+ {collection.ready
179
+ ? `Lexical passed; semantic ${collection.semanticAvailability.code}`
180
+ : `${collection.remediation?.stage ?? "index"}/${collection.remediation?.code ?? "index_query_failed"}`}
181
+ </p>
182
+ {collection.remediation && (
183
+ <p className="mt-1 font-mono text-muted-foreground text-xs">
184
+ {collection.remediation.command}
185
+ </p>
186
+ )}
187
+ </div>
188
+ ))
189
+ )}
190
+ {(activation.connectors.length > 0 ||
191
+ activation.connectorProjection.truncated) && (
192
+ <div className="space-y-2 border-border/50 border-t pt-3">
193
+ <p className="font-medium text-xs uppercase tracking-wide">
194
+ Connector proof
195
+ </p>
196
+ {activation.connectors.slice(0, 8).map((connector) => (
197
+ <div
198
+ className="text-muted-foreground text-xs"
199
+ key={`${connector.collection}-${connector.target}`}
200
+ >
201
+ <span className="font-medium text-foreground">
202
+ {connector.target}
203
+ </span>{" "}
204
+ · {connector.collection} · {connector.status}
205
+ {connector.code ? `/${connector.code}` : ""}
206
+ </div>
207
+ ))}
208
+ {hiddenProjectedConnectorCount > 0 && (
209
+ <p className="text-muted-foreground text-xs">
210
+ +{hiddenProjectedConnectorCount} more projected checks
211
+ </p>
212
+ )}
213
+ {activation.connectorProjection.truncated && (
214
+ <p className="text-muted-foreground text-xs">
215
+ {omittedConnectorCount} additional target/collection checks
216
+ omitted from this bounded status view
217
+ </p>
218
+ )}
219
+ </div>
220
+ )}
221
+ </CardContent>
222
+ </Card>
130
223
  </div>
131
224
  </section>
132
225
  );