@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
|
@@ -11,12 +11,13 @@ import { stat } from "node:fs/promises";
|
|
|
11
11
|
import { arch, platform } from "node:os";
|
|
12
12
|
|
|
13
13
|
import type { Config } from "../../config/types";
|
|
14
|
+
import type { ActivationStatus } from "../../core/activation-status";
|
|
14
15
|
|
|
15
16
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
16
17
|
import { getConfigPaths, isInitialized, loadConfig } from "../../config";
|
|
18
|
+
import { isConnectorActivationComplete } from "../../core/activation-connector-health";
|
|
17
19
|
import { getCodeChunkingStatus } from "../../ingestion/chunker";
|
|
18
20
|
import { ModelCache } from "../../llm/cache";
|
|
19
|
-
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
20
21
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
21
22
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
22
23
|
import { loadFts5Snowball } from "../../store/sqlite/fts5-snowball";
|
|
@@ -26,6 +27,11 @@ import {
|
|
|
26
27
|
getLoadAttempts,
|
|
27
28
|
} from "../../store/sqlite/setup";
|
|
28
29
|
import { getStoredEmbeddingFingerprint } from "../../store/vector/freshness";
|
|
30
|
+
import {
|
|
31
|
+
buildDoctorActivation,
|
|
32
|
+
checkConnectorActivation,
|
|
33
|
+
checkRetrievalActivation,
|
|
34
|
+
} from "./doctor-activation";
|
|
29
35
|
|
|
30
36
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
37
|
// Types
|
|
@@ -63,6 +69,8 @@ export interface EmbeddingFingerprintHealth {
|
|
|
63
69
|
export interface DoctorOptions {
|
|
64
70
|
/** Override config path */
|
|
65
71
|
configPath?: string;
|
|
72
|
+
/** Index name */
|
|
73
|
+
indexName?: string;
|
|
66
74
|
/** Output as JSON */
|
|
67
75
|
json?: boolean;
|
|
68
76
|
/** Output as Markdown */
|
|
@@ -72,6 +80,14 @@ export interface DoctorOptions {
|
|
|
72
80
|
export interface DoctorResult {
|
|
73
81
|
healthy: boolean;
|
|
74
82
|
checks: DoctorCheck[];
|
|
83
|
+
activation: ActivationStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether doctor found a process-failing check (warnings remain exit-safe). */
|
|
87
|
+
export function hasCriticalDoctorErrors(
|
|
88
|
+
checks: readonly DoctorCheck[]
|
|
89
|
+
): boolean {
|
|
90
|
+
return checks.some(({ status }) => status === "error");
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -105,8 +121,8 @@ async function checkConfig(configPath?: string): Promise<DoctorCheck> {
|
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
async function checkDatabase(): Promise<DoctorCheck> {
|
|
109
|
-
const dbPath = getIndexDbPath();
|
|
124
|
+
async function checkDatabase(indexName?: string): Promise<DoctorCheck> {
|
|
125
|
+
const dbPath = getIndexDbPath(indexName);
|
|
110
126
|
|
|
111
127
|
try {
|
|
112
128
|
await stat(dbPath);
|
|
@@ -175,9 +191,10 @@ function describeFingerprintGroup(group: EmbeddingFingerprintGroup): string {
|
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
async function checkEmbeddingFingerprints(
|
|
178
|
-
config: Config
|
|
194
|
+
config: Config,
|
|
195
|
+
indexName?: string
|
|
179
196
|
): Promise<DoctorCheck> {
|
|
180
|
-
const dbPath = getIndexDbPath();
|
|
197
|
+
const dbPath = getIndexDbPath(indexName);
|
|
181
198
|
try {
|
|
182
199
|
await stat(dbPath);
|
|
183
200
|
} catch {
|
|
@@ -312,14 +329,13 @@ async function checkEmbeddingFingerprints(
|
|
|
312
329
|
}
|
|
313
330
|
}
|
|
314
331
|
|
|
315
|
-
|
|
316
|
-
const llm = new LlmAdapter(config);
|
|
332
|
+
function checkNodeLlamaCpp(): DoctorCheck {
|
|
317
333
|
try {
|
|
318
|
-
|
|
334
|
+
import.meta.resolve("node-llama-cpp");
|
|
319
335
|
return {
|
|
320
336
|
name: "node-llama-cpp",
|
|
321
337
|
status: "ok",
|
|
322
|
-
message: "node-llama-cpp
|
|
338
|
+
message: "node-llama-cpp package available (runtime not initialized)",
|
|
323
339
|
};
|
|
324
340
|
} catch (e) {
|
|
325
341
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -328,8 +344,6 @@ async function checkNodeLlamaCpp(config: Config): Promise<DoctorCheck> {
|
|
|
328
344
|
status: "error",
|
|
329
345
|
message: `node-llama-cpp failed: ${message}`,
|
|
330
346
|
};
|
|
331
|
-
} finally {
|
|
332
|
-
await llm.dispose();
|
|
333
347
|
}
|
|
334
348
|
}
|
|
335
349
|
|
|
@@ -495,7 +509,7 @@ export async function doctor(
|
|
|
495
509
|
checks.push(await checkConfig(options.configPath));
|
|
496
510
|
|
|
497
511
|
// Database check
|
|
498
|
-
checks.push(await checkDatabase());
|
|
512
|
+
checks.push(await checkDatabase(options.indexName));
|
|
499
513
|
|
|
500
514
|
// Load config for model checks (if available)
|
|
501
515
|
const { createDefaultConfig } = await import("../../config");
|
|
@@ -507,7 +521,7 @@ export async function doctor(
|
|
|
507
521
|
checks.push(...modelChecks);
|
|
508
522
|
|
|
509
523
|
// node-llama-cpp check
|
|
510
|
-
checks.push(
|
|
524
|
+
checks.push(checkNodeLlamaCpp());
|
|
511
525
|
|
|
512
526
|
// SQLite extension checks
|
|
513
527
|
const sqliteChecks = await checkSqliteExtensions();
|
|
@@ -517,14 +531,25 @@ export async function doctor(
|
|
|
517
531
|
checks.push(checkCodeChunking());
|
|
518
532
|
|
|
519
533
|
// Embedding fingerprint freshness
|
|
520
|
-
checks.push(await checkEmbeddingFingerprints(config));
|
|
534
|
+
checks.push(await checkEmbeddingFingerprints(config, options.indexName));
|
|
535
|
+
|
|
536
|
+
const activation = await buildDoctorActivation(config, options);
|
|
537
|
+
checks.push(checkRetrievalActivation(activation));
|
|
538
|
+
const connectorActivation = checkConnectorActivation(activation);
|
|
539
|
+
if (connectorActivation) {
|
|
540
|
+
checks.push(connectorActivation);
|
|
541
|
+
}
|
|
521
542
|
|
|
522
543
|
// Determine overall health
|
|
523
|
-
const hasErrors = checks
|
|
544
|
+
const hasErrors = hasCriticalDoctorErrors(checks);
|
|
524
545
|
|
|
525
546
|
return {
|
|
526
|
-
healthy:
|
|
547
|
+
healthy:
|
|
548
|
+
!hasErrors &&
|
|
549
|
+
activation.healthy &&
|
|
550
|
+
isConnectorActivationComplete(activation),
|
|
527
551
|
checks,
|
|
552
|
+
activation,
|
|
528
553
|
};
|
|
529
554
|
}
|
|
530
555
|
|
package/src/cli/commands/get.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type { DocumentRow, StorePort, StoreResult } from "../../store/types";
|
|
|
9
9
|
import type { ParsedRef } from "./ref-parser";
|
|
10
10
|
|
|
11
11
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
12
|
+
import {
|
|
13
|
+
INDEX_NAME_REQUIREMENTS,
|
|
14
|
+
isValidIndexName,
|
|
15
|
+
} from "../../app/index-name";
|
|
12
16
|
import {
|
|
13
17
|
getDocumentCapabilities,
|
|
14
18
|
type DocumentCapabilities,
|
|
@@ -114,6 +118,13 @@ export async function get(
|
|
|
114
118
|
}
|
|
115
119
|
const explicitIndexName =
|
|
116
120
|
parsed.type === "uri" ? parseUri(parsed.value)?.indexName : undefined;
|
|
121
|
+
if (explicitIndexName !== undefined && !isValidIndexName(explicitIndexName)) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
|
|
125
|
+
isValidation: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
117
128
|
const indexName = explicitIndexName ?? options.indexName;
|
|
118
129
|
|
|
119
130
|
const initResult = await initStore({
|
|
@@ -134,6 +145,13 @@ export async function get(
|
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
function validateOptions(options: GetCommandOptions): GetResult | null {
|
|
148
|
+
if (options.indexName !== undefined && !isValidIndexName(options.indexName)) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
|
|
152
|
+
isValidation: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
137
155
|
if (options.from !== undefined && options.from <= 0) {
|
|
138
156
|
return {
|
|
139
157
|
success: false,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink-safe atomic writes for MCP configuration files.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/mcp/atomic-config-write
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises supplies structural and metadata operations that Bun does
|
|
8
|
+
// not expose: symlink inspection/resolution, atomic rename, mode changes, and
|
|
9
|
+
// best-effort temporary-file cleanup.
|
|
10
|
+
import {
|
|
11
|
+
chmod,
|
|
12
|
+
lstat,
|
|
13
|
+
mkdir,
|
|
14
|
+
realpath,
|
|
15
|
+
rename,
|
|
16
|
+
stat,
|
|
17
|
+
unlink,
|
|
18
|
+
} from "node:fs/promises";
|
|
19
|
+
// node:path supplies path manipulation; Bun has no equivalent.
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { CliError } from "../../errors.js";
|
|
23
|
+
|
|
24
|
+
interface ResolvedWriteTarget {
|
|
25
|
+
mode?: number;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isMissingPathError(error: unknown): boolean {
|
|
30
|
+
return (
|
|
31
|
+
error instanceof Error &&
|
|
32
|
+
"code" in error &&
|
|
33
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configPathError(configPath: string, detail: string): CliError {
|
|
38
|
+
return new CliError(
|
|
39
|
+
"RUNTIME",
|
|
40
|
+
`Cannot write MCP config ${configPath}: ${detail}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveWriteTarget(
|
|
45
|
+
configPath: string
|
|
46
|
+
): Promise<ResolvedWriteTarget> {
|
|
47
|
+
let pathStats: Awaited<ReturnType<typeof lstat>>;
|
|
48
|
+
try {
|
|
49
|
+
pathStats = await lstat(configPath);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isMissingPathError(error)) {
|
|
52
|
+
return { path: configPath };
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!pathStats.isSymbolicLink()) {
|
|
58
|
+
if (!pathStats.isFile()) {
|
|
59
|
+
throw configPathError(configPath, "path exists but is not a file");
|
|
60
|
+
}
|
|
61
|
+
return { mode: pathStats.mode & 0o7777, path: configPath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let resolvedPath: string;
|
|
65
|
+
try {
|
|
66
|
+
resolvedPath = await realpath(configPath);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (isMissingPathError(error)) {
|
|
69
|
+
throw configPathError(configPath, "symbolic link target does not exist");
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const targetStats = await stat(resolvedPath);
|
|
75
|
+
if (!targetStats.isFile()) {
|
|
76
|
+
throw configPathError(configPath, "symbolic link target is not a file");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { mode: targetStats.mode & 0o7777, path: resolvedPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Atomically replace an MCP config file with serialized JSON, YAML, or TOML.
|
|
84
|
+
*
|
|
85
|
+
* Existing file permissions survive replacement. A config-path symlink remains
|
|
86
|
+
* in place: its resolved file is replaced by a temporary sibling. Dangling
|
|
87
|
+
* symlinks fail closed instead of being replaced with regular files.
|
|
88
|
+
*/
|
|
89
|
+
export async function writeMcpConfigTextAtomically(
|
|
90
|
+
configPath: string,
|
|
91
|
+
content: string
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
94
|
+
|
|
95
|
+
const target = await resolveWriteTarget(configPath);
|
|
96
|
+
const targetDirectory = dirname(target.path);
|
|
97
|
+
const tempPath = join(
|
|
98
|
+
targetDirectory,
|
|
99
|
+
`.${basename(target.path)}.gno-tmp-${process.pid}-${crypto.randomUUID()}`
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await Bun.write(tempPath, content, {
|
|
104
|
+
createPath: false,
|
|
105
|
+
...(target.mode === undefined ? {} : { mode: target.mode & 0o777 }),
|
|
106
|
+
});
|
|
107
|
+
if (target.mode !== undefined) {
|
|
108
|
+
// Bun 1.3 does not consistently apply Bun.write's mode option on macOS.
|
|
109
|
+
await chmod(tempPath, target.mode);
|
|
110
|
+
}
|
|
111
|
+
await rename(tempPath, target.path);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await unlink(tempPath).catch(() => {
|
|
114
|
+
// The write may have failed before creating the temporary file.
|
|
115
|
+
});
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Resolve one unambiguous MCP client config filename. */
|
|
2
|
+
|
|
3
|
+
// node:fs/promises has no Bun equivalent for lstat, including dangling links.
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
import type { McpConfigPaths } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
import { CliError } from "../../errors.js";
|
|
9
|
+
|
|
10
|
+
export async function configPathEntryExists(path: string): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await lstat(path);
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function resolveMcpConfigLocation(
|
|
23
|
+
paths: McpConfigPaths
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const candidates = [
|
|
26
|
+
paths.configPath,
|
|
27
|
+
...(paths.alternativeConfigPaths ?? []),
|
|
28
|
+
];
|
|
29
|
+
const present: string[] = [];
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
if (await configPathEntryExists(candidate)) {
|
|
32
|
+
present.push(candidate);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (present.length > 1) {
|
|
36
|
+
throw new CliError(
|
|
37
|
+
"RUNTIME",
|
|
38
|
+
`Ambiguous MCP config files: ${present.join(", ")}. Keep exactly one.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return present[0] ?? paths.configPath;
|
|
42
|
+
}
|