@gmickel/gno 2.9.2 → 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.
- package/README.md +2 -2
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.9.2.zip → gno-browser-clipper-v2.10.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.10.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/spec/cli.md +70 -2
- package/src/app/bun-runtime.ts +41 -0
- package/src/cli/commands/daemon.ts +3 -0
- package/src/cli/commands/doctor.ts +61 -0
- package/src/cli/commands/mcp/config-editors.ts +56 -8
- package/src/cli/commands/mcp/install.ts +159 -24
- package/src/cli/commands/mcp/launch-path.ts +87 -0
- package/src/cli/commands/mcp/paths.ts +30 -6
- package/src/cli/commands/mcp/repair-entry.ts +160 -0
- package/src/cli/commands/mcp/status.ts +55 -15
- package/src/cli/commands/mcp.ts +4 -0
- package/src/cli/commands/serve.ts +3 -0
- package/src/cli/commands/status.ts +24 -0
- package/src/cli/detach.ts +10 -0
- package/src/cli/program.ts +8 -3
- package/src/cli/runtime-health.ts +328 -0
- package/src/core/network-boundary-inventory.ts +16 -0
- package/src/ingestion/file-processor.ts +118 -17
- package/src/ingestion/prepare-worker.ts +20 -2
- package/src/serve/public/components/BootstrapStatus.tsx +3 -3
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/status.ts +2 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.9.2.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.
|
|
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.
|
|
335
|
+
Requires [Bun](https://bun.sh/) >=1.4.1.
|
|
336
336
|
|
|
337
337
|
<!-- /public-truth -->
|
|
338
338
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
56174f38783259cef05d81ae53adceebc8467799b735c2f3026b928f7070ae76 gno-browser-clipper-v2.10.0.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "2.
|
|
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",
|
|
@@ -268,7 +268,7 @@
|
|
|
268
268
|
"file-type": "22.0.2"
|
|
269
269
|
},
|
|
270
270
|
"engines": {
|
|
271
|
-
"bun": ">=1.
|
|
271
|
+
"bun": ">=1.4.1"
|
|
272
272
|
},
|
|
273
273
|
"trustedDependencies": [
|
|
274
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
|
|
@@ -1507,7 +1514,12 @@ executable, which cannot start the worker, runs the phases in a child process
|
|
|
1507
1514
|
of itself (internal environment flag) under the same limits; the child's
|
|
1508
1515
|
resident memory counts toward `conversion.maxMemoryMb` (Linux `/proc`, `ps`
|
|
1509
1516
|
elsewhere; time only on Windows). A processor that cannot start fails the file
|
|
1510
|
-
with `ISOLATION_UNAVAILABLE`; nothing is prepared without the budget.
|
|
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
|
|
1511
1523
|
child is SIGKILLed on process exit, SIGTERM, SIGINT (via the CLI's exit
|
|
1512
1524
|
path) and resident shutdown; on Linux it also carries a parent-death signal,
|
|
1513
1525
|
so it dies with a SIGKILLed parent, and elsewhere it exits at its next step
|
|
@@ -3019,6 +3031,30 @@ gno doctor [--json|--md]
|
|
|
3019
3031
|
}
|
|
3020
3032
|
```
|
|
3021
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
|
+
|
|
3022
3058
|
The `findings-pass` check reports the daemon's scheduled findings pass from
|
|
3023
3059
|
its persisted run state: `ok` when disabled or the last run succeeded, `warn`
|
|
3024
3060
|
on `skipped_lease` / `overdue` / no recorded run, `error` on `failed` or a
|
|
@@ -3093,7 +3129,7 @@ gno mcp install [--target <target>] [--scope <scope>] [--force] [--dry-run] [--e
|
|
|
3093
3129
|
| ---------------- | ------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3094
3130
|
| `--target` | string | claude-desktop | Target client (see table below) |
|
|
3095
3131
|
| `--scope` | string | target default | Scope: `user` or `project`; LibreChat defaults to project |
|
|
3096
|
-
| `--force` | boolean | false |
|
|
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 |
|
|
3097
3133
|
| `--dry-run` | boolean | false | Show what would be done without changes |
|
|
3098
3134
|
| `--enable-write` | boolean | false | Start the registered server with write tools enabled |
|
|
3099
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 |
|
|
@@ -3132,6 +3168,25 @@ gno mcp install [--target <target>] [--scope <scope>] [--force] [--dry-run] [--e
|
|
|
3132
3168
|
| lmstudio | user | `~/.lmstudio/mcp.json` | `~/.lmstudio/mcp.json` | `~/.lmstudio/mcp.json` |
|
|
3133
3169
|
| librechat | project | `./librechat.yaml` | `./librechat.yaml` | `./librechat.yaml` |
|
|
3134
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
|
+
|
|
3135
3190
|
**Config Formats:**
|
|
3136
3191
|
|
|
3137
3192
|
- JSONC-compatible (`mcpServers` key): Claude Desktop, Claude Code, Cursor, Windsurf, LM Studio
|
|
@@ -3352,6 +3407,19 @@ MCP Server Status
|
|
|
3352
3407
|
}
|
|
3353
3408
|
```
|
|
3354
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
|
+
|
|
3355
3423
|
**Exit Codes:**
|
|
3356
3424
|
|
|
3357
3425
|
- 0: Success
|
|
@@ -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,6 +5,7 @@ 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,
|
|
@@ -203,6 +204,8 @@ export async function daemon(
|
|
|
203
204
|
},
|
|
204
205
|
};
|
|
205
206
|
|
|
207
|
+
warnIfBunBelowFloor("daemon", logger.error);
|
|
208
|
+
|
|
206
209
|
const runtimeResult: BackgroundRuntimeResult = await (
|
|
207
210
|
deps.startBackgroundRuntime ?? startBackgroundRuntime
|
|
208
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -29,14 +29,22 @@ import {
|
|
|
29
29
|
} from "./config.js";
|
|
30
30
|
import {
|
|
31
31
|
buildMcpServerEntry,
|
|
32
|
+
resolveLaunchPaths,
|
|
32
33
|
getDefaultTargetScope,
|
|
33
34
|
getTargetScopes,
|
|
34
35
|
getTargetDisplayName,
|
|
36
|
+
type McpConfigFormat,
|
|
35
37
|
type McpScope,
|
|
36
38
|
type McpServerEntry,
|
|
37
39
|
type McpTarget,
|
|
38
40
|
resolveMcpConfigPath,
|
|
39
41
|
} from "./paths.js";
|
|
42
|
+
import {
|
|
43
|
+
type ExplicitInstallValues,
|
|
44
|
+
readExistingRegistration,
|
|
45
|
+
type RepairedRegistration,
|
|
46
|
+
repairRegistration,
|
|
47
|
+
} from "./repair-entry.js";
|
|
40
48
|
import { normalizeMcpServerEntryForInstall } from "./server-entry.js";
|
|
41
49
|
|
|
42
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -71,6 +79,8 @@ export interface McpInstallResult {
|
|
|
71
79
|
configPath: string;
|
|
72
80
|
action: "created" | "updated" | "dry_run_create" | "dry_run_update";
|
|
73
81
|
serverEntry: McpServerEntry;
|
|
82
|
+
/** `--force` kept the existing entry's settings and repaired its launch path. */
|
|
83
|
+
repaired?: boolean;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -89,6 +99,12 @@ export async function installMcpToTarget(
|
|
|
89
99
|
dryRun?: boolean;
|
|
90
100
|
cwd?: string;
|
|
91
101
|
homeDir?: string;
|
|
102
|
+
/**
|
|
103
|
+
* With `force` over an existing entry: keep its settings and apply only
|
|
104
|
+
* these explicit options (repair-entry.ts). Without it, the entry is
|
|
105
|
+
* replaced.
|
|
106
|
+
*/
|
|
107
|
+
preserveExisting?: ExplicitInstallValues;
|
|
92
108
|
}
|
|
93
109
|
): Promise<McpInstallResult> {
|
|
94
110
|
const { force = false, dryRun = false, cwd, homeDir } = options;
|
|
@@ -120,23 +136,45 @@ export async function installMcpToTarget(
|
|
|
120
136
|
);
|
|
121
137
|
}
|
|
122
138
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const existing =
|
|
140
|
+
alreadyExists && options.preserveExisting
|
|
141
|
+
? readExistingRegistration(parsed.entry, configFormat)
|
|
142
|
+
: null;
|
|
143
|
+
const repaired =
|
|
144
|
+
existing && options.preserveExisting
|
|
145
|
+
? repairRegistration(existing, normalizedEntry, options.preserveExisting)
|
|
146
|
+
: null;
|
|
147
|
+
if (repaired) {
|
|
148
|
+
// Same structural rules as a fresh entry for what this command writes.
|
|
149
|
+
normalizeMcpServerEntryForInstall({
|
|
150
|
+
command: repaired.command,
|
|
151
|
+
args: repaired.args,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const updated = repaired
|
|
155
|
+
? writeRepairedEntry(
|
|
156
|
+
content,
|
|
157
|
+
configPath,
|
|
158
|
+
configFormat,
|
|
159
|
+
serversKey,
|
|
160
|
+
repaired
|
|
161
|
+
)
|
|
162
|
+
: writeFreshEntry(
|
|
163
|
+
content,
|
|
164
|
+
configPath,
|
|
165
|
+
configFormat,
|
|
166
|
+
serversKey,
|
|
167
|
+
normalizedEntry
|
|
168
|
+
);
|
|
169
|
+
const writtenEntry: McpServerEntry = repaired
|
|
170
|
+
? {
|
|
171
|
+
command: repaired.command,
|
|
172
|
+
args: repaired.args,
|
|
173
|
+
...(Object.keys(repaired.env).length > 0
|
|
174
|
+
? { env: repaired.env as McpServerEntry["env"] }
|
|
175
|
+
: {}),
|
|
176
|
+
}
|
|
177
|
+
: normalizedEntry;
|
|
140
178
|
|
|
141
179
|
const wouldCreate = !alreadyExists;
|
|
142
180
|
if (dryRun) {
|
|
@@ -145,7 +183,8 @@ export async function installMcpToTarget(
|
|
|
145
183
|
scope,
|
|
146
184
|
configPath,
|
|
147
185
|
action: wouldCreate ? "dry_run_create" : "dry_run_update",
|
|
148
|
-
serverEntry:
|
|
186
|
+
serverEntry: writtenEntry,
|
|
187
|
+
...(repaired ? { repaired: true } : {}),
|
|
149
188
|
};
|
|
150
189
|
}
|
|
151
190
|
|
|
@@ -157,8 +196,73 @@ export async function installMcpToTarget(
|
|
|
157
196
|
scope,
|
|
158
197
|
configPath,
|
|
159
198
|
action,
|
|
160
|
-
serverEntry:
|
|
199
|
+
serverEntry: writtenEntry,
|
|
200
|
+
...(repaired ? { repaired: true } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writeFreshEntry(
|
|
205
|
+
content: string,
|
|
206
|
+
configPath: string,
|
|
207
|
+
configFormat: McpConfigFormat,
|
|
208
|
+
serversKey: string,
|
|
209
|
+
normalizedEntry: McpServerEntry
|
|
210
|
+
): string {
|
|
211
|
+
const standardEntry = {
|
|
212
|
+
command: normalizedEntry.command,
|
|
213
|
+
args: normalizedEntry.args,
|
|
214
|
+
...(normalizedEntry.env ? { env: normalizedEntry.env } : {}),
|
|
215
|
+
};
|
|
216
|
+
if (configFormat === "codex_toml") {
|
|
217
|
+
return setTomlServerEntry(content, configPath, standardEntry);
|
|
218
|
+
}
|
|
219
|
+
if (configFormat === "yaml_standard") {
|
|
220
|
+
return setYamlServerEntry(content, configPath, serversKey, standardEntry);
|
|
221
|
+
}
|
|
222
|
+
const entry = buildEntry(
|
|
223
|
+
normalizedEntry.command,
|
|
224
|
+
normalizedEntry.args,
|
|
225
|
+
configFormat,
|
|
226
|
+
normalizedEntry.env
|
|
227
|
+
);
|
|
228
|
+
return setJsoncServerEntry(content, configPath, serversKey, entry);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Write a repaired entry, keeping the keys GNO does not manage. */
|
|
232
|
+
function writeRepairedEntry(
|
|
233
|
+
content: string,
|
|
234
|
+
configPath: string,
|
|
235
|
+
configFormat: McpConfigFormat,
|
|
236
|
+
serversKey: string,
|
|
237
|
+
repaired: RepairedRegistration
|
|
238
|
+
): string {
|
|
239
|
+
const hasEnv = Object.keys(repaired.env).length > 0;
|
|
240
|
+
if (configFormat === "codex_toml") {
|
|
241
|
+
return setTomlServerEntry(
|
|
242
|
+
content,
|
|
243
|
+
configPath,
|
|
244
|
+
{ command: repaired.command, args: repaired.args },
|
|
245
|
+
{ env: repaired.env, extra: repaired.extra }
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (configFormat === "mcp") {
|
|
249
|
+
return setJsoncServerEntry(content, configPath, serversKey, {
|
|
250
|
+
type: "local",
|
|
251
|
+
enabled: true,
|
|
252
|
+
...repaired.extra,
|
|
253
|
+
command: [repaired.command, ...repaired.args],
|
|
254
|
+
...(hasEnv ? { environment: repaired.env } : {}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const entry = {
|
|
258
|
+
...repaired.extra,
|
|
259
|
+
command: repaired.command,
|
|
260
|
+
args: repaired.args,
|
|
261
|
+
...(hasEnv ? { env: repaired.env } : {}),
|
|
161
262
|
};
|
|
263
|
+
return configFormat === "yaml_standard"
|
|
264
|
+
? setYamlServerEntry(content, configPath, serversKey, entry)
|
|
265
|
+
: setJsoncServerEntry(content, configPath, serversKey, entry);
|
|
162
266
|
}
|
|
163
267
|
|
|
164
268
|
/**
|
|
@@ -200,6 +304,14 @@ export async function installMcp(opts: InstallOptions = {}): Promise<void> {
|
|
|
200
304
|
opts.configPath ?? paths.configFile,
|
|
201
305
|
opts.cwd
|
|
202
306
|
);
|
|
307
|
+
// Only what the user passed overrides an existing entry under --force;
|
|
308
|
+
// the defaults above never do.
|
|
309
|
+
const explicit: ExplicitInstallValues = {
|
|
310
|
+
...(opts.indexName !== undefined ? { indexName } : {}),
|
|
311
|
+
...(opts.configPath !== undefined ? { configPath } : {}),
|
|
312
|
+
...(toolProfile !== undefined ? { toolProfile } : {}),
|
|
313
|
+
...(opts.enableWrite ? { enableWrite: true } : {}),
|
|
314
|
+
};
|
|
203
315
|
const globals = safeGetGlobals();
|
|
204
316
|
const json = opts.json ?? globals.json;
|
|
205
317
|
const quiet = opts.quiet ?? globals.quiet;
|
|
@@ -228,11 +340,18 @@ export async function installMcp(opts: InstallOptions = {}): Promise<void> {
|
|
|
228
340
|
dryRun,
|
|
229
341
|
cwd: opts.cwd,
|
|
230
342
|
homeDir: opts.homeDir,
|
|
343
|
+
preserveExisting: explicit,
|
|
231
344
|
});
|
|
345
|
+
const launch = resolveLaunchPaths();
|
|
346
|
+
const pinnedPaths = [launch.bun, launch.entrypoint]
|
|
347
|
+
.filter((path) => path.pinned)
|
|
348
|
+
.map((path) => path.path);
|
|
232
349
|
|
|
233
350
|
// Output
|
|
234
351
|
if (json) {
|
|
235
|
-
process.stdout.write(
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
`${JSON.stringify({ installed: { ...result, ...(pinnedPaths.length > 0 ? { pinnedPaths } : {}) } }, null, 2)}\n`
|
|
354
|
+
);
|
|
236
355
|
return;
|
|
237
356
|
}
|
|
238
357
|
|
|
@@ -247,8 +366,9 @@ export async function installMcp(opts: InstallOptions = {}): Promise<void> {
|
|
|
247
366
|
`Would ${dryRunVerb} gno in ${getTargetDisplayName(target)}:\n`
|
|
248
367
|
);
|
|
249
368
|
process.stdout.write(` Config: ${result.configPath}\n`);
|
|
250
|
-
process.stdout.write(` Command: ${serverEntry.command}\n`);
|
|
251
|
-
process.stdout.write(` Args: ${serverEntry.args.join(" ")}\n`);
|
|
369
|
+
process.stdout.write(` Command: ${result.serverEntry.command}\n`);
|
|
370
|
+
process.stdout.write(` Args: ${result.serverEntry.args.join(" ")}\n`);
|
|
371
|
+
writePinNote(pinnedPaths);
|
|
252
372
|
return;
|
|
253
373
|
}
|
|
254
374
|
|
|
@@ -256,8 +376,23 @@ export async function installMcp(opts: InstallOptions = {}): Promise<void> {
|
|
|
256
376
|
process.stdout.write(
|
|
257
377
|
`${verb} gno MCP server in ${getTargetDisplayName(target)}.\n`
|
|
258
378
|
);
|
|
259
|
-
process.stdout.write(` Config: ${result.configPath}\n
|
|
379
|
+
process.stdout.write(` Config: ${result.configPath}\n`);
|
|
380
|
+
if (result.repaired) {
|
|
381
|
+
process.stdout.write(
|
|
382
|
+
" Kept the existing settings; updated the Bun and GNO paths.\n"
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
writePinNote(pinnedPaths);
|
|
260
386
|
process.stdout.write(
|
|
261
|
-
|
|
387
|
+
`\nRestart ${getTargetDisplayName(target)} to load the server.\n`
|
|
262
388
|
);
|
|
263
389
|
}
|
|
390
|
+
|
|
391
|
+
/** Say when a launch path stays on one installed version. */
|
|
392
|
+
function writePinNote(pinnedPaths: string[]): void {
|
|
393
|
+
for (const path of pinnedPaths) {
|
|
394
|
+
process.stdout.write(
|
|
395
|
+
` Note: ${path} is inside a versioned install directory with no version-independent link to it; this registration keeps using that version after an upgrade. Run gno mcp install --force again after upgrading (gno doctor reports it).\n`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|