@gmickel/gno 2.9.1 → 2.10.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 (49) hide show
  1. package/README.md +2 -2
  2. package/assets/spa-production.json.gz +0 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v2.9.1.zip → gno-browser-clipper-v2.10.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v2.10.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +3 -2
  7. package/spec/cli.md +73 -2
  8. package/spec/output-schemas/collection-list.schema.json +5 -0
  9. package/src/app/bun-runtime.ts +41 -0
  10. package/src/cli/commands/daemon.ts +12 -0
  11. package/src/cli/commands/doctor.ts +61 -0
  12. package/src/cli/commands/mcp/config-editors.ts +56 -8
  13. package/src/cli/commands/mcp/install.ts +159 -24
  14. package/src/cli/commands/mcp/launch-path.ts +87 -0
  15. package/src/cli/commands/mcp/paths.ts +30 -6
  16. package/src/cli/commands/mcp/repair-entry.ts +160 -0
  17. package/src/cli/commands/mcp/status.ts +55 -15
  18. package/src/cli/commands/mcp.ts +4 -0
  19. package/src/cli/commands/serve.ts +3 -0
  20. package/src/cli/commands/status.ts +24 -0
  21. package/src/cli/detach.ts +52 -5
  22. package/src/cli/program.ts +19 -5
  23. package/src/cli/runtime-health.ts +328 -0
  24. package/src/config/types.ts +3 -0
  25. package/src/core/network-boundary-inventory.ts +16 -0
  26. package/src/core/project-profile-apply-state.ts +3 -0
  27. package/src/core/resident-shutdown-handler.ts +23 -0
  28. package/src/core/sighup-disposition.ts +66 -0
  29. package/src/core/time-slice.ts +34 -0
  30. package/src/ingestion/file-processor.ts +118 -17
  31. package/src/ingestion/graph-reconciliation.ts +18 -4
  32. package/src/ingestion/prepare-worker.ts +20 -2
  33. package/src/ingestion/sync.ts +70 -4
  34. package/src/serve/parent-lifeline.ts +44 -0
  35. package/src/serve/public/components/BootstrapStatus.tsx +3 -3
  36. package/src/serve/public/globals.built.css +1 -1
  37. package/src/serve/resident-runtime.ts +24 -1
  38. package/src/serve/resident-shutdown.ts +4 -0
  39. package/src/serve/server.ts +22 -1
  40. package/src/serve/status.ts +2 -1
  41. package/src/serve/watch-reconciliation.ts +134 -1
  42. package/src/serve/watch-service-flush-generation.ts +77 -3
  43. package/src/serve/watch-service.ts +27 -3
  44. package/src/sessions/import-child.ts +79 -11
  45. package/src/store/migrations/035-vector-length-index.ts +30 -0
  46. package/src/store/migrations/index.ts +2 -0
  47. package/src/store/sqlite/adapter.ts +4 -1
  48. package/src/store/vector/freshness.ts +13 -6
  49. package/browser-extension/artifacts/gno-browser-clipper-v2.9.1.zip.sha256 +0 -1
package/README.md CHANGED
@@ -139,7 +139,7 @@ See the [guide](docs/COMPILED-CONTEXT.md).
139
139
 
140
140
  <!-- public-truth:current-version -->
141
141
 
142
- > Current source version: **v2.9.1**. See [CHANGELOG.md](./CHANGELOG.md).
142
+ > Current source version: **v2.10.0**. See [CHANGELOG.md](./CHANGELOG.md).
143
143
 
144
144
  <!-- /public-truth -->
145
145
 
@@ -332,7 +332,7 @@ Manage the detached process with `gno daemon --status` and `gno daemon --stop`.
332
332
 
333
333
  <!-- public-truth:runtime -->
334
334
 
335
- Requires [Bun](https://bun.sh/) >=1.3.0.
335
+ Requires [Bun](https://bun.sh/) >=1.4.1.
336
336
 
337
337
  <!-- /public-truth -->
338
338
 
Binary file
@@ -0,0 +1 @@
1
+ 56174f38783259cef05d81ae53adceebc8467799b735c2f3026b928f7070ae76 gno-browser-clipper-v2.10.0.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "2.9.1"
24
+ "version": "2.10.0"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -90,6 +90,7 @@
90
90
  "test:watcher:filesystem": "bun test test/serve/watch-service-filesystem.test.ts test/serve/resident-runtime.test.ts",
91
91
  "bench:watcher-reconciliation": "bun scripts/watcher-reconciliation-benchmark.ts",
92
92
  "bench:session-import": "bun scripts/session-import-benchmark.ts",
93
+ "soak": "bun scripts/soak/index.ts",
93
94
  "smoke:watcher-reconciliation": "bun scripts/watcher-reconciliation-smoke.ts",
94
95
  "bench:code-embeddings": "bun scripts/code-embedding-benchmark.ts",
95
96
  "bench:code-embeddings:write": "bun scripts/code-embedding-benchmark.ts --write",
@@ -267,7 +268,7 @@
267
268
  "file-type": "22.0.2"
268
269
  },
269
270
  "engines": {
270
- "bun": ">=1.3.0"
271
+ "bun": ">=1.4.1"
271
272
  },
272
273
  "trustedDependencies": [
273
274
  "bun",
package/spec/cli.md CHANGED
@@ -393,6 +393,13 @@ is alive but did not answer its status request within 500ms). Each resident is
393
393
  asked once with that 500ms budget, so `gno status` never waits on a hung
394
394
  resident. Terminal output lists the same issues under `Background issues:`.
395
395
 
396
+ When an MCP client registration or a running resident uses a missing, old or
397
+ other Bun, JSON output adds `runtimeIssues` (absent otherwise) and terminal
398
+ output lists them under `Bun runtime:`. Each item has `kind`
399
+ (`registration` or `resident`), `severity` (`error`, `warn` or `info`),
400
+ `subject`, `message` and `repair`, the same findings as the doctor checks
401
+ `mcp-runtime` and `resident-runtime` below.
402
+
396
403
  Local activation fingerprints use active-document identifiers and source/mirror
397
404
  hashes plus schema, tokenizer, and owned FTS synchronization metadata. Passive
398
405
  status never selects or compares stored markdown or FTS bodies. On a receipt
@@ -1079,6 +1086,9 @@ gno embed [collection] [--collection <name>] [--force] [--model <uri>] [--batch-
1079
1086
  ]
1080
1087
  ````
1081
1088
 
1089
+ `updateCmdTimeoutMs` (milliseconds) appears when the collection sets a limit
1090
+ for `updateCmd`; without it the command is stopped after 10 minutes.
1091
+
1082
1092
  **Exit Codes:**
1083
1093
 
1084
1094
  - 0: Success
@@ -1504,7 +1514,12 @@ executable, which cannot start the worker, runs the phases in a child process
1504
1514
  of itself (internal environment flag) under the same limits; the child's
1505
1515
  resident memory counts toward `conversion.maxMemoryMb` (Linux `/proc`, `ps`
1506
1516
  elsewhere; time only on Windows). A processor that cannot start fails the file
1507
- with `ISOLATION_UNAVAILABLE`; nothing is prepared without the budget. The
1517
+ with `ISOLATION_UNAVAILABLE`; nothing is prepared without the budget. A
1518
+ message to or from the processor that cannot be deserialized fails the file
1519
+ with `PROCESSOR_MESSAGE_FAILED` (details: `direction` `job`, `result`, or
1520
+ `unknown` for the child backend, whose channel Bun closes on both ends;
1521
+ `backend`; `phase`), replaces the processor, and writes one stderr line naming
1522
+ the direction, backend, `<collection>/<relPath>` and phase. The
1508
1523
  child is SIGKILLed on process exit, SIGTERM, SIGINT (via the CLI's exit
1509
1524
  path) and resident shutdown; on Linux it also carries a parent-death signal,
1510
1525
  so it dies with a SIGKILLed parent, and elsewhere it exits at its next step
@@ -3016,6 +3031,30 @@ gno doctor [--json|--md]
3016
3031
  }
3017
3032
  ```
3018
3033
 
3034
+ The `mcp-runtime` check reads every MCP client registration GNO can install
3035
+ (all targets and scopes) and reports one `error` when a registration's Bun or
3036
+ GNO entrypoint no longer exists or its Bun is below the supported minimum
3037
+ (`engines.bun`), and `info` when it resolves to a different Bun than the one
3038
+ running doctor. A Bun version comes from a version manager's install path when
3039
+ the interpreter resolves into one, else from `<interpreter> --version` under a
3040
+ 2-second limit; a probe that fails or times out reports the version as unknown.
3041
+ The registered MCP command itself is never started. A registration that runs
3042
+ the `gno` launcher finds Bun on `PATH` and is only checked for existence. Each
3043
+ detail names the client, scope and file, and the repair
3044
+ `gno mcp install --target <target> --scope <scope> --force`.
3045
+
3046
+ The `resident-runtime` check reads the `serve` and `daemon` pid-files. A live
3047
+ resident's Bun comes from the pid-file's `bun_version` (written since this
3048
+ release) or, when absent, from `bootstrap.runtime.currentVersion` of
3049
+ `GET /api/status` on its recorded port (2-second limit; trusted only when the
3050
+ response is a GNO status payload). It is an `error` below the supported
3051
+ minimum, a `warn` when older than the Bun running doctor, and `info` when a
3052
+ resident from another GNO version does not report its Bun. The check never
3053
+ signals a process. Its repair restarts the resident: `gno <kind> --stop` then
3054
+ start it again, or, for a resident started by another GNO version (which
3055
+ `--stop` refuses), `kill <pid>` (`taskkill /PID <pid>` on Windows) and start it
3056
+ again.
3057
+
3019
3058
  The `findings-pass` check reports the daemon's scheduled findings pass from
3020
3059
  its persisted run state: `ok` when disabled or the last run succeeded, `warn`
3021
3060
  on `skipped_lease` / `overdue` / no recorded run, `error` on `failed` or a
@@ -3090,7 +3129,7 @@ gno mcp install [--target <target>] [--scope <scope>] [--force] [--dry-run] [--e
3090
3129
  | ---------------- | ------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
3091
3130
  | `--target` | string | claude-desktop | Target client (see table below) |
3092
3131
  | `--scope` | string | target default | Scope: `user` or `project`; LibreChat defaults to project |
3093
- | `--force` | boolean | false | Overwrite existing gno configuration |
3132
+ | `--force` | boolean | false | Repair an existing gno entry: rewrite its Bun and GNO paths, keep its other arguments, environment and keys, and apply only the options passed explicitly |
3094
3133
  | `--dry-run` | boolean | false | Show what would be done without changes |
3095
3134
  | `--enable-write` | boolean | false | Start the registered server with write tools enabled |
3096
3135
  | `--tool-profile` | string | full | Advertised tool set written into the registration: `core` (7 read tools, plus capture and remember with `--enable-write`) or `full`; omitted writes no flag |
@@ -3129,6 +3168,25 @@ gno mcp install [--target <target>] [--scope <scope>] [--force] [--dry-run] [--e
3129
3168
  | lmstudio | user | `~/.lmstudio/mcp.json` | `~/.lmstudio/mcp.json` | `~/.lmstudio/mcp.json` |
3130
3169
  | librechat | project | `./librechat.yaml` | `./librechat.yaml` | `./librechat.yaml` |
3131
3170
 
3171
+ **Launch paths and repair:** the command is the running Bun, the first
3172
+ argument after `run` the running package's `src/index.ts`. When either sits in
3173
+ a version manager's versioned install directory (mise, asdf or proto
3174
+ `installs/bun/<version>/` or `tools/bun/<version>/`, Homebrew
3175
+ `Cellar/bun/<version>/`), the manager's version-independent link
3176
+ (`installs/bun/latest/...`, `opt/bun/...`) is written instead, but only if it
3177
+ resolves to the same file; otherwise the versioned path is kept, the text
3178
+ output adds a note, and `--json` adds `installed.pinnedPaths`. Shims that pick
3179
+ a version from the shell environment are never written. A standalone compiled
3180
+ executable is written as is. With `--force` over an existing entry that
3181
+ launches GNO (`run <entrypoint> ... mcp ...`, or a `gno` executable), only the
3182
+ launch prefix is rewritten; `--index`, `--config`, `--tool-profile` and
3183
+ `--enable-write` change only when passed in this invocation, existing
3184
+ environment values win over the installer's data and cache directories, and
3185
+ keys GNO does not write are kept in every format, Codex TOML included;
3186
+ `installed.repaired` is `true`. A Codex key that the TOML writer cannot express
3187
+ stops the command before anything is written. An entry that does not launch GNO
3188
+ is replaced as before. Without `--force` an existing entry is refused.
3189
+
3132
3190
  **Config Formats:**
3133
3191
 
3134
3192
  - JSONC-compatible (`mcpServers` key): Claude Desktop, Claude Code, Cursor, Windsurf, LM Studio
@@ -3349,6 +3407,19 @@ MCP Server Status
3349
3407
  }
3350
3408
  ```
3351
3409
 
3410
+ An entry is reported (and offered to the connector verifier) only when its
3411
+ keys are the launch keys (`command`, `args`, `env`; OpenCode `type: "local"`,
3412
+ `command`, `environment`) plus inert client settings: `type: "stdio"`, numeric
3413
+ `timeout` / `startup_timeout_sec` / `tool_timeout_sec`, a string
3414
+ `description`, string-array `autoApprove` / `alwaysAllow`, and boolean
3415
+ `enabled` / `disabled` (OpenCode: numeric `timeout` and `enabled`).
3416
+ `serverEntry` carries only the launch keys. An entry with `enabled: false` or
3417
+ `disabled: true` is `configured: false` without an error. Any other key or
3418
+ value, including `cwd`, another transport `type`, `url`, `headers`, and
3419
+ environment variables other than `GNO_DATA_DIR` / `GNO_CACHE_DIR`, makes the
3420
+ entry `configured: false` with `error: "Malformed MCP server entry"`, and the
3421
+ connector verifier reports it as `connector_unsupported_config`.
3422
+
3352
3423
  **Exit Codes:**
3353
3424
 
3354
3425
  - 0: Success
@@ -36,6 +36,11 @@
36
36
  "type": "string",
37
37
  "description": "Optional shell command to run before indexing"
38
38
  },
39
+ "updateCmdTimeoutMs": {
40
+ "type": "integer",
41
+ "minimum": 1,
42
+ "description": "Optional limit for updateCmd in milliseconds; gno stops the command (and anything it started) after this long. Absent means the 10 minute default."
43
+ },
39
44
  "languageHint": {
40
45
  "type": "string",
41
46
  "description": "BCP-47 language hint for the collection"
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Minimum supported Bun (fn-213).
3
+ *
4
+ * `engines.bun` in package.json is the single source of truth. Bun 1.4.0
5
+ * and 1.4.1 fixed structured-clone bugs in the path worker messages take,
6
+ * which can crash a long-running process; older runtimes still start, but
7
+ * `serve`, `daemon` and `mcp` say so once on stderr.
8
+ *
9
+ * @module src/app/bun-runtime
10
+ */
11
+
12
+ import pkg from "../../package.json";
13
+
14
+ /** Supported Bun range, e.g. `>=1.4.1`. */
15
+ export const BUN_ENGINE_RANGE: string = pkg.engines.bun;
16
+
17
+ /** The minimum version in that range, e.g. `1.4.1`. */
18
+ export const BUN_FLOOR: string = BUN_ENGINE_RANGE.replace(/^>=\s*/, "");
19
+
20
+ /** Whether a Bun version is within the supported range. */
21
+ export function bunMeetsFloor(version: string = Bun.version): boolean {
22
+ return Bun.semver.satisfies(version, BUN_ENGINE_RANGE);
23
+ }
24
+
25
+ /** One-line warning for a command started on an unsupported Bun, else null. */
26
+ export function bunFloorWarning(
27
+ command: string,
28
+ version: string = Bun.version
29
+ ): string | null {
30
+ if (bunMeetsFloor(version)) return null;
31
+ return `gno ${command}: running on Bun ${version}, older than the supported minimum ${BUN_FLOOR}; older runtimes can crash long-running processes. Upgrade Bun and restart.`;
32
+ }
33
+
34
+ /** Print the warning for `command` when the running Bun is unsupported. */
35
+ export function warnIfBunBelowFloor(
36
+ command: string,
37
+ write: (line: string) => void = console.warn
38
+ ): void {
39
+ const warning = bunFloorWarning(command);
40
+ if (warning) write(warning);
41
+ }
@@ -5,11 +5,14 @@ import type { FindingsPassResult } from "../../serve/findings-pass";
5
5
  import type { ResidentRuntime } from "../../serve/resident-runtime";
6
6
  import type { SessionAutomationRunResult } from "../../sessions/types";
7
7
 
8
+ import { warnIfBunBelowFloor } from "../../app/bun-runtime";
8
9
  import {
9
10
  enforceCollectionEgress,
10
11
  EGRESS_DENIED_MESSAGE,
11
12
  EgressDeniedError,
12
13
  } from "../../core/egress-enforcement";
14
+ import { markResidentShutdownHandlerInstalled } from "../../core/resident-shutdown-handler";
15
+ import { sighupHandlerAllowed } from "../../core/sighup-disposition";
13
16
  import {
14
17
  DEFAULT_HTTP_GATEWAY_PORT,
15
18
  isHttpGatewayLoopbackBind,
@@ -164,6 +167,7 @@ function createSignalPromise(
164
167
  signal?.removeEventListener("abort", onAbort);
165
168
  process.off("SIGINT", onSigint);
166
169
  process.off("SIGTERM", onSigterm);
170
+ process.off("SIGHUP", onSighup);
167
171
  if (message && !quiet) {
168
172
  logger.log(message);
169
173
  }
@@ -174,10 +178,16 @@ function createSignalPromise(
174
178
  const onSigint = (): void => complete("Received SIGINT. Shutting down...");
175
179
  const onSigterm = (): void =>
176
180
  complete("Received SIGTERM. Shutting down...");
181
+ // A closed terminal or logout: shut down cleanly, as on SIGTERM, unless
182
+ // SIGHUP was ignored at start (nohup), which must keep working.
183
+ const handleSighup = sighupHandlerAllowed();
184
+ const onSighup = (): void => complete("Received SIGHUP. Shutting down...");
177
185
 
178
186
  signal?.addEventListener("abort", onAbort, { once: true });
179
187
  process.once("SIGINT", onSigint);
180
188
  process.once("SIGTERM", onSigterm);
189
+ if (handleSighup) process.once("SIGHUP", onSighup);
190
+ markResidentShutdownHandlerInstalled();
181
191
  });
182
192
  }
183
193
 
@@ -194,6 +204,8 @@ export async function daemon(
194
204
  },
195
205
  };
196
206
 
207
+ warnIfBunBelowFloor("daemon", logger.error);
208
+
197
209
  const runtimeResult: BackgroundRuntimeResult = await (
198
210
  deps.startBackgroundRuntime ?? startBackgroundRuntime
199
211
  )({
@@ -45,6 +45,12 @@ import {
45
45
  import { getStoredEmbeddingFingerprint } from "../../store/vector/freshness";
46
46
  import { currentOwnerCount } from "../../store/vector/runtime-compat";
47
47
  import { storedVectorPartition } from "../../store/vector/status";
48
+ import {
49
+ checkRegistrationRuntimes,
50
+ checkResidentRuntimes,
51
+ formatRuntimeIssue,
52
+ type RuntimeIssue,
53
+ } from "../runtime-health";
48
54
  import {
49
55
  buildDoctorActivation,
50
56
  checkConnectorActivation,
@@ -622,6 +628,58 @@ async function checkSqliteExtensions(): Promise<DoctorCheck[]> {
622
628
  * Report the daemon's scheduled findings pass from its persisted state so a
623
629
  * misconfigured, starved, or failing scheduler is visible without the daemon.
624
630
  */
631
+ const SEVERITY_RANK: Record<RuntimeIssue["severity"], number> = {
632
+ info: 0,
633
+ warn: 1,
634
+ error: 2,
635
+ };
636
+
637
+ function runtimeCheck(
638
+ name: string,
639
+ issues: RuntimeIssue[],
640
+ okMessage: string
641
+ ): DoctorCheck {
642
+ if (issues.length === 0) {
643
+ return { name, status: "ok", message: okMessage };
644
+ }
645
+ const worst = issues.reduce((current, issue) =>
646
+ SEVERITY_RANK[issue.severity] > SEVERITY_RANK[current.severity]
647
+ ? issue
648
+ : current
649
+ );
650
+ return {
651
+ name,
652
+ status: worst.severity,
653
+ message:
654
+ issues.length === 1
655
+ ? `${worst.subject}: ${worst.message}`
656
+ : `${issues.length} need attention; first: ${worst.subject}: ${worst.message}`,
657
+ details: issues.map(formatRuntimeIssue),
658
+ };
659
+ }
660
+
661
+ /** Bun used by MCP registrations and by running residents. */
662
+ export async function checkRuntimes(
663
+ overrides: { cwd?: string; homeDir?: string } = {}
664
+ ): Promise<DoctorCheck[]> {
665
+ const [registrations, residents] = await Promise.all([
666
+ checkRegistrationRuntimes(overrides),
667
+ checkResidentRuntimes(),
668
+ ]);
669
+ return [
670
+ runtimeCheck(
671
+ "mcp-runtime",
672
+ registrations,
673
+ "MCP registrations launch an existing, supported Bun"
674
+ ),
675
+ runtimeCheck(
676
+ "resident-runtime",
677
+ residents,
678
+ "No running resident is on an older Bun"
679
+ ),
680
+ ];
681
+ }
682
+
625
683
  export async function checkFindingsPass(
626
684
  config: Config,
627
685
  indexName?: string
@@ -727,6 +785,9 @@ export async function doctor(
727
785
  // Scheduled findings pass (daemon-only, opt-in)
728
786
  checks.push(await checkFindingsPass(config, options.indexName));
729
787
 
788
+ // Which Bun client registrations and running residents use (fn-213)
789
+ checks.push(...(await checkRuntimes()));
790
+
730
791
  const activation = await buildDoctorActivation(config, options);
731
792
  checks.push(checkRetrievalActivation(activation));
732
793
  const connectorActivation = checkConnectorActivation(activation);
@@ -371,19 +371,66 @@ function tomlString(value: string): string {
371
371
  return JSON.stringify(value);
372
372
  }
373
373
 
374
- function serializeTomlEntry(entry: StandardMcpEntry, newline: string): string {
374
+ const TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
375
+
376
+ function tomlKey(key: string): string {
377
+ return TOML_BARE_KEY.test(key) ? key : tomlString(key);
378
+ }
379
+
380
+ /** A kept key's value as TOML; null for values this writer cannot express. */
381
+ function tomlValue(value: unknown): string | null {
382
+ if (typeof value === "string") {
383
+ return tomlString(value);
384
+ }
385
+ if (
386
+ typeof value === "boolean" ||
387
+ (typeof value === "number" && Number.isFinite(value))
388
+ ) {
389
+ return String(value);
390
+ }
391
+ if (Array.isArray(value)) {
392
+ const items = value.map(tomlValue);
393
+ return items.every((item) => item !== null)
394
+ ? `[${items.join(", ")}]`
395
+ : null;
396
+ }
397
+ return null;
398
+ }
399
+
400
+ /** Keys of an existing GNO entry kept by a repair (see repair-entry.ts). */
401
+ export interface TomlEntryExtras {
402
+ env?: Record<string, string>;
403
+ extra?: Record<string, unknown>;
404
+ }
405
+
406
+ function serializeTomlEntry(
407
+ entry: StandardMcpEntry,
408
+ newline: string,
409
+ configPath: string,
410
+ extras: TomlEntryExtras = {}
411
+ ): string {
375
412
  const args = entry.args.map(tomlString).join(", ");
376
413
  const lines = [
377
414
  "[mcp_servers.gno]",
378
415
  `command = ${tomlString(entry.command)}`,
379
416
  `args = [${args}]`,
380
417
  ];
381
- if (entry.env && Object.keys(entry.env).length > 0) {
418
+ for (const [key, value] of Object.entries(extras.extra ?? {})) {
419
+ const serialized = tomlValue(value);
420
+ if (serialized === null) {
421
+ throw new CliError(
422
+ "RUNTIME",
423
+ `Cannot keep "${key}" of the GNO MCP entry in ${configPath}; remove the entry with gno mcp uninstall, or edit the key, then install again.`
424
+ );
425
+ }
426
+ lines.push(`${tomlKey(key)} = ${serialized}`);
427
+ }
428
+ const env = { ...entry.env, ...extras.env };
429
+ if (Object.keys(env).length > 0) {
382
430
  lines.push("", "[mcp_servers.gno.env]");
383
- for (const key of ["GNO_DATA_DIR", "GNO_CACHE_DIR"] as const) {
384
- const value = entry.env[key];
431
+ for (const [key, value] of Object.entries(env)) {
385
432
  if (value) {
386
- lines.push(`${key} = ${tomlString(value)}`);
433
+ lines.push(`${tomlKey(key)} = ${tomlString(value)}`);
387
434
  }
388
435
  }
389
436
  }
@@ -393,7 +440,8 @@ function serializeTomlEntry(entry: StandardMcpEntry, newline: string): string {
393
440
  export function setTomlServerEntry(
394
441
  content: string,
395
442
  configPath: string,
396
- entry: StandardMcpEntry
443
+ entry: StandardMcpEntry,
444
+ extras?: TomlEntryExtras
397
445
  ): string {
398
446
  const parsed = getTomlServerEntry(content, configPath);
399
447
  const removed = removeTomlGnoSections(content, configPath);
@@ -405,8 +453,8 @@ export function setTomlServerEntry(
405
453
  }
406
454
  const prefix = removed.content.trimEnd();
407
455
  const updated = prefix
408
- ? `${prefix}${removed.newline}${removed.newline}${serializeTomlEntry(entry, removed.newline)}`
409
- : serializeTomlEntry(entry, removed.newline);
456
+ ? `${prefix}${removed.newline}${removed.newline}${serializeTomlEntry(entry, removed.newline, configPath, extras)}`
457
+ : serializeTomlEntry(entry, removed.newline, configPath, extras);
410
458
  parseTomlRoot(updated, configPath);
411
459
  return updated;
412
460
  }