@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.
Files changed (69) hide show
  1. package/README.md +57 -30
  2. package/assets/skill/SKILL.md +6 -1
  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/context-resolver.ts +285 -0
  37. package/src/core/indexed-reference.ts +33 -8
  38. package/src/core/runtime-entrypoint.ts +24 -0
  39. package/src/mcp/activation-verification-mode.ts +4 -0
  40. package/src/mcp/server.ts +9 -2
  41. package/src/mcp/tools/index.ts +3 -3
  42. package/src/pipeline/answer-prompt.ts +80 -0
  43. package/src/pipeline/answer.ts +12 -26
  44. package/src/pipeline/hybrid.ts +2 -0
  45. package/src/pipeline/result-context.ts +51 -0
  46. package/src/pipeline/search.ts +5 -1
  47. package/src/pipeline/vsearch.ts +2 -0
  48. package/src/sdk/client.ts +7 -0
  49. package/src/sdk/types.ts +1 -0
  50. package/src/serve/activation-health.ts +91 -0
  51. package/src/serve/background-runtime.ts +11 -1
  52. package/src/serve/connectors.ts +164 -19
  53. package/src/serve/public/components/BootstrapStatus.tsx +94 -1
  54. package/src/serve/public/components/FirstRunWizard.tsx +13 -51
  55. package/src/serve/public/components/HealthCenter.tsx +8 -2
  56. package/src/serve/public/globals.built.css +1 -1
  57. package/src/serve/public/pages/Connectors.tsx +216 -55
  58. package/src/serve/public/pages/Dashboard.tsx +1 -0
  59. package/src/serve/routes/api.ts +152 -8
  60. package/src/serve/server.ts +44 -9
  61. package/src/serve/status-model.ts +4 -0
  62. package/src/serve/status.ts +79 -35
  63. package/src/store/activation-receipts.ts +390 -0
  64. package/src/store/index.ts +8 -0
  65. package/src/store/migrations/012-activation-receipts.ts +38 -0
  66. package/src/store/migrations/013-fts-sync-marker.ts +39 -0
  67. package/src/store/migrations/index.ts +4 -0
  68. package/src/store/sqlite/adapter.ts +320 -53
  69. package/src/store/types.ts +124 -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
- async function checkNodeLlamaCpp(config: Config): Promise<DoctorCheck> {
316
- const llm = new LlmAdapter(config);
332
+ function checkNodeLlamaCpp(): DoctorCheck {
317
333
  try {
318
- await llm.getManager().getLlama();
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 loaded successfully",
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(await checkNodeLlamaCpp(config));
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.some((c) => c.status === "error");
544
+ const hasErrors = hasCriticalDoctorErrors(checks);
524
545
 
525
546
  return {
526
- healthy: !hasErrors,
547
+ healthy:
548
+ !hasErrors &&
549
+ activation.healthy &&
550
+ isConnectorActivationComplete(activation),
527
551
  checks,
552
+ activation,
528
553
  };
529
554
  }
530
555
 
@@ -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
+ }