@gmickel/gno 1.12.3 → 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.
- package/README.md +57 -30
- package/assets/skill/SKILL.md +6 -1
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/context-resolver.ts +285 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/index.ts +3 -3
- package/src/pipeline/answer-prompt.ts +80 -0
- package/src/pipeline/answer.ts +12 -26
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/result-context.ts +51 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/vsearch.ts +2 -0
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +320 -53
- package/src/store/types.ts +124 -0
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { formatQueryForEmbedding } from "./contextual";
|
|
|
18
18
|
import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
|
|
19
19
|
import { selectBestChunkForSteering } from "./intent";
|
|
20
20
|
import { detectQueryLanguage } from "./query-language";
|
|
21
|
+
import { attachSearchResultContexts } from "./result-context";
|
|
21
22
|
import {
|
|
22
23
|
resolveRecencyTimestamp,
|
|
23
24
|
isWithinTemporalRange,
|
|
@@ -333,6 +334,7 @@ export async function searchVectorWithEmbedding(
|
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
const finalResults = results.slice(0, limit);
|
|
337
|
+
await attachSearchResultContexts(store, finalResults);
|
|
336
338
|
|
|
337
339
|
return ok({
|
|
338
340
|
results: finalResults,
|
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);
|
package/src/serve/connectors.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
157
|
-
summary:
|
|
158
|
-
? `${definition.appName} skill is
|
|
159
|
-
:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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?:
|
|
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({
|
|
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
|
-
...
|
|
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-
|
|
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
|
);
|
|
@@ -204,10 +204,8 @@ export function FirstRunWizard({
|
|
|
204
204
|
onboarding,
|
|
205
205
|
onAddCollection,
|
|
206
206
|
onDownloadModels,
|
|
207
|
-
onEmbed,
|
|
208
207
|
onSync,
|
|
209
208
|
onSyncComplete,
|
|
210
|
-
embedding = false,
|
|
211
209
|
syncJobId = null,
|
|
212
210
|
syncing = false,
|
|
213
211
|
}: FirstRunWizardProps) {
|
|
@@ -238,27 +236,19 @@ export function FirstRunWizard({
|
|
|
238
236
|
: null;
|
|
239
237
|
const progressValue = getStepProgress(onboarding);
|
|
240
238
|
const isShowingRecommended = activeStep?.id === recommendedStepId;
|
|
241
|
-
const indexingNeedsEmbeddings =
|
|
242
|
-
onboarding.stage === "indexing" &&
|
|
243
|
-
onboarding.detail.toLowerCase().includes("embedding");
|
|
244
|
-
|
|
245
239
|
const runRecommendedAction = () => {
|
|
246
240
|
if (!activeStep) {
|
|
247
241
|
return;
|
|
248
242
|
}
|
|
249
|
-
if (activeStep.
|
|
243
|
+
if (activeStep.action === "add-collection") {
|
|
250
244
|
onAddCollection();
|
|
251
245
|
return;
|
|
252
246
|
}
|
|
253
|
-
if (activeStep.
|
|
247
|
+
if (activeStep.action === "download-models") {
|
|
254
248
|
onDownloadModels();
|
|
255
249
|
return;
|
|
256
250
|
}
|
|
257
|
-
if (activeStep.
|
|
258
|
-
onEmbed();
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (activeStep.id === "indexing" && !syncing) {
|
|
251
|
+
if (activeStep.action === "sync" && !syncing) {
|
|
262
252
|
onSync();
|
|
263
253
|
}
|
|
264
254
|
};
|
|
@@ -403,7 +393,7 @@ export function FirstRunWizard({
|
|
|
403
393
|
<WizardPanel>
|
|
404
394
|
<WizardPanelHeader
|
|
405
395
|
badge={<Badge variant="outline">Safe to rerun</Badge>}
|
|
406
|
-
title="
|
|
396
|
+
title="Prove lexical retrieval"
|
|
407
397
|
>
|
|
408
398
|
<p className="max-w-2xl text-muted-foreground text-sm leading-6">
|
|
409
399
|
{activeStep.detail}
|
|
@@ -419,31 +409,13 @@ export function FirstRunWizard({
|
|
|
419
409
|
|
|
420
410
|
<WizardActionPanel
|
|
421
411
|
action={
|
|
422
|
-
<Button
|
|
423
|
-
disabled={indexingNeedsEmbeddings ? embedding : syncing}
|
|
424
|
-
onClick={indexingNeedsEmbeddings ? onEmbed : onSync}
|
|
425
|
-
size="lg"
|
|
426
|
-
>
|
|
412
|
+
<Button disabled={syncing} onClick={onSync} size="lg">
|
|
427
413
|
<RefreshCwIcon className="mr-2 size-4" />
|
|
428
|
-
{
|
|
429
|
-
? embedding
|
|
430
|
-
? "Embedding..."
|
|
431
|
-
: "Finish embeddings"
|
|
432
|
-
: syncing
|
|
433
|
-
? "Syncing..."
|
|
434
|
-
: "Run first sync"}
|
|
414
|
+
{syncing ? "Syncing..." : "Run first sync"}
|
|
435
415
|
</Button>
|
|
436
416
|
}
|
|
437
|
-
body=
|
|
438
|
-
|
|
439
|
-
? "Your files are indexed. One more embedding pass will unlock semantic search and local answers."
|
|
440
|
-
: "Good last step before you move into normal use."
|
|
441
|
-
}
|
|
442
|
-
title={
|
|
443
|
-
indexingNeedsEmbeddings
|
|
444
|
-
? "Finish semantic indexing"
|
|
445
|
-
: "Index the current workspace"
|
|
446
|
-
}
|
|
417
|
+
body="Builds the local lexical index and proves that this exact folder can return corpus-derived evidence."
|
|
418
|
+
title="Index and verify the current workspace"
|
|
447
419
|
/>
|
|
448
420
|
|
|
449
421
|
{syncJobId && (
|
|
@@ -451,7 +423,7 @@ export function FirstRunWizard({
|
|
|
451
423
|
<WizardPanelHeader title="Sync progress">
|
|
452
424
|
<p className="max-w-2xl text-muted-foreground text-sm leading-6">
|
|
453
425
|
Your first indexing run is in progress. The wizard will advance
|
|
454
|
-
once the job finishes and
|
|
426
|
+
once the job finishes and lexical retrieval is proven.
|
|
455
427
|
</p>
|
|
456
428
|
</WizardPanelHeader>
|
|
457
429
|
<div style={{ padding: "24px 28px" }}>
|
|
@@ -568,24 +540,14 @@ export function FirstRunWizard({
|
|
|
568
540
|
</div>
|
|
569
541
|
{activeStep && activeStep.id !== "preset" && (
|
|
570
542
|
<Button
|
|
571
|
-
disabled={
|
|
572
|
-
activeStep.id === "indexing"
|
|
573
|
-
? indexingNeedsEmbeddings
|
|
574
|
-
? embedding
|
|
575
|
-
: syncing
|
|
576
|
-
: false
|
|
577
|
-
}
|
|
543
|
+
disabled={activeStep.action === "sync" ? syncing : false}
|
|
578
544
|
onClick={runRecommendedAction}
|
|
579
545
|
size="sm"
|
|
580
546
|
variant={isShowingRecommended ? "default" : "outline"}
|
|
581
547
|
>
|
|
582
|
-
{activeStep.
|
|
583
|
-
?
|
|
584
|
-
|
|
585
|
-
: "Finish embeddings"
|
|
586
|
-
: activeStep.id === "indexing" && syncing
|
|
587
|
-
? "Syncing..."
|
|
588
|
-
: getStepActionLabel(activeStep.id)}
|
|
548
|
+
{activeStep.action === "sync" && syncing
|
|
549
|
+
? "Syncing..."
|
|
550
|
+
: getStepActionLabel(activeStep.id)}
|
|
589
551
|
</Button>
|
|
590
552
|
)}
|
|
591
553
|
</div>
|