@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.
- package/README.md +57 -30
- package/assets/skill/SKILL.md +5 -0
- 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/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/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 +313 -53
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: ${[
|
|
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
|
+
}
|
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
|
-
|
|
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);
|
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
|
);
|