@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version-independent launch paths for client registrations (fn-213).
|
|
3
|
+
*
|
|
4
|
+
* `process.execPath` and the GNO entrypoint are absolute, and under a
|
|
5
|
+
* version manager they sit in a versioned install directory
|
|
6
|
+
* (`installs/bun/1.3.14/...`, `Cellar/bun/1.4.2/...`). A registration that
|
|
7
|
+
* names that directory keeps starting the old runtime after an upgrade, and
|
|
8
|
+
* fails once the version is removed. Each manager also keeps a link that
|
|
9
|
+
* follows its upgrades; a registration names that link instead, but only
|
|
10
|
+
* when it resolves to the very file in use now. Shims that pick a version
|
|
11
|
+
* from the shell environment (asdf, proto, mise shims) are never used: GUI
|
|
12
|
+
* clients start servers without that environment.
|
|
13
|
+
*
|
|
14
|
+
* @module src/cli/commands/mcp/launch-path
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// node:fs realpathSync: synchronous link resolution; Bun has no realpath API.
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
/** mise, asdf and proto: `<root>/{installs,tools}/bun/<version>/...`. */
|
|
21
|
+
const MANAGER_VERSION_DIR = /([\\/])(installs|tools)\1bun\1([^\\/]+)(?=[\\/])/;
|
|
22
|
+
/** Homebrew: `<prefix>/Cellar/bun/<version>/...`. */
|
|
23
|
+
const HOMEBREW_VERSION_DIR = /([\\/])Cellar\1bun\1[^\\/]+(?=[\\/])/;
|
|
24
|
+
/** Version-independent directory names a manager keeps itself. */
|
|
25
|
+
const ALIAS_VERSIONS = new Set(["latest", "current", "system"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The version-independent links a manager keeps for `path`, or null when
|
|
29
|
+
* `path` is not inside a versioned install directory.
|
|
30
|
+
*/
|
|
31
|
+
export function versionIndependentCandidates(path: string): string[] | null {
|
|
32
|
+
const cellar = HOMEBREW_VERSION_DIR.exec(path);
|
|
33
|
+
if (cellar) {
|
|
34
|
+
const separator = cellar[1] ?? "/";
|
|
35
|
+
return [
|
|
36
|
+
path.replace(HOMEBREW_VERSION_DIR, `${separator}opt${separator}bun`),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const manager = MANAGER_VERSION_DIR.exec(path);
|
|
40
|
+
if (!manager || ALIAS_VERSIONS.has(manager[3] ?? "")) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const separator = manager[1] ?? "/";
|
|
44
|
+
return [
|
|
45
|
+
path.replace(
|
|
46
|
+
MANAGER_VERSION_DIR,
|
|
47
|
+
`${separator}${manager[2]}${separator}bun${separator}latest`
|
|
48
|
+
),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const realpathOrNull = (path: string): string | null => {
|
|
53
|
+
try {
|
|
54
|
+
return realpathSync(path);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface LaunchPath {
|
|
61
|
+
/** The path to write. */
|
|
62
|
+
path: string;
|
|
63
|
+
/**
|
|
64
|
+
* True when `path` is inside a versioned install directory and no
|
|
65
|
+
* version-independent link resolves to it: the registration stays on this
|
|
66
|
+
* version until it is reinstalled.
|
|
67
|
+
*/
|
|
68
|
+
pinned: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Prefer a version-independent link that resolves to the same file. */
|
|
72
|
+
export function stableLaunchPath(
|
|
73
|
+
path: string,
|
|
74
|
+
realpath: (path: string) => string | null = realpathOrNull
|
|
75
|
+
): LaunchPath {
|
|
76
|
+
const candidates = versionIndependentCandidates(path);
|
|
77
|
+
if (!candidates) {
|
|
78
|
+
return { path, pinned: false };
|
|
79
|
+
}
|
|
80
|
+
const target = realpath(path);
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
if (target !== null && realpath(candidate) === target) {
|
|
83
|
+
return { path: candidate, pinned: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { path, pinned: true };
|
|
87
|
+
}
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type McpToolProfile,
|
|
19
19
|
isMcpToolProfile,
|
|
20
20
|
} from "../../../mcp/tool-profile.js";
|
|
21
|
+
import { isBunfsPath } from "../../../serve/spa-production-build";
|
|
22
|
+
import { type LaunchPath, stableLaunchPath } from "./launch-path.js";
|
|
21
23
|
import { getTargetDisplayName } from "./target-display.js";
|
|
22
24
|
|
|
23
25
|
export { getTargetDisplayName } from "./target-display.js";
|
|
@@ -447,14 +449,35 @@ export function resolveAllMcpPaths(
|
|
|
447
449
|
// Bun & GNO Path Discovery
|
|
448
450
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
449
451
|
|
|
452
|
+
/**
|
|
453
|
+
* The Bun and GNO paths a registration launches, preferring links that
|
|
454
|
+
* follow version-manager upgrades (see launch-path.ts). A standalone
|
|
455
|
+
* compiled executable is left alone: its execPath is GNO itself.
|
|
456
|
+
*/
|
|
457
|
+
export function resolveLaunchPaths(): {
|
|
458
|
+
bun: LaunchPath;
|
|
459
|
+
entrypoint: LaunchPath;
|
|
460
|
+
} {
|
|
461
|
+
const entrypoint = getCurrentGnoEntrypoint();
|
|
462
|
+
if (isBunfsPath(import.meta.path)) {
|
|
463
|
+
return {
|
|
464
|
+
bun: { path: process.execPath, pinned: false },
|
|
465
|
+
entrypoint: { path: entrypoint, pinned: false },
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
bun: stableLaunchPath(process.execPath),
|
|
470
|
+
entrypoint: stableLaunchPath(entrypoint),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
450
474
|
/**
|
|
451
475
|
* Find absolute path to bun executable.
|
|
452
|
-
* Cross-platform:
|
|
476
|
+
* Cross-platform: starts from process.execPath since we're already running
|
|
477
|
+
* under Bun, then prefers a version-independent link to the same binary.
|
|
453
478
|
*/
|
|
454
479
|
export function findBunPath(): string {
|
|
455
|
-
|
|
456
|
-
// This is cross-platform and doesn't require shelling out to `which`.
|
|
457
|
-
return process.execPath;
|
|
480
|
+
return resolveLaunchPaths().bun.path;
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
/**
|
|
@@ -467,8 +490,9 @@ export function buildMcpServerEntry(
|
|
|
467
490
|
if (options.indexName !== undefined) {
|
|
468
491
|
assertValidIndexName(options.indexName);
|
|
469
492
|
}
|
|
470
|
-
const
|
|
471
|
-
const
|
|
493
|
+
const launch = resolveLaunchPaths();
|
|
494
|
+
const bunPath = launch.bun.path;
|
|
495
|
+
const args = ["run", launch.entrypoint.path];
|
|
472
496
|
appendMcpArguments(args, options);
|
|
473
497
|
const dirs = resolveDirs();
|
|
474
498
|
return {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gno mcp install --force` over an existing registration (fn-213).
|
|
3
|
+
*
|
|
4
|
+
* The launch path (the interpreter and the `run <entrypoint>` prefix) is
|
|
5
|
+
* always rewritten. Every other argument, environment variable and entry key
|
|
6
|
+
* is kept, except the options the user passed explicitly in this invocation,
|
|
7
|
+
* which override their values. Defaults the installer fills in for omitted
|
|
8
|
+
* options never override an existing value.
|
|
9
|
+
*
|
|
10
|
+
* @module src/cli/commands/mcp/repair-entry
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// node:path basename: no Bun path utilities.
|
|
14
|
+
import { basename } from "node:path";
|
|
15
|
+
|
|
16
|
+
import type { McpToolProfile } from "../../../mcp/tool-profile.js";
|
|
17
|
+
import type { McpConfigFormat, McpServerEntry } from "./paths.js";
|
|
18
|
+
|
|
19
|
+
import { isPlainRecord } from "./config-editors.js";
|
|
20
|
+
|
|
21
|
+
/** Install options the user passed explicitly (undefined = not passed). */
|
|
22
|
+
export interface ExplicitInstallValues {
|
|
23
|
+
indexName?: string;
|
|
24
|
+
configPath?: string;
|
|
25
|
+
toolProfile?: McpToolProfile;
|
|
26
|
+
enableWrite?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An existing registration split into what GNO writes and everything else. */
|
|
30
|
+
export interface ExistingRegistration {
|
|
31
|
+
command: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
env: Record<string, string>;
|
|
34
|
+
/** Entry keys GNO does not manage, kept verbatim. */
|
|
35
|
+
extra: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isStringArray = (value: unknown): value is string[] =>
|
|
39
|
+
Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
40
|
+
|
|
41
|
+
const stringRecord = (value: unknown): Record<string, string> | null => {
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
if (!isPlainRecord(value)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const entries = Object.entries(value);
|
|
49
|
+
return entries.every(([, item]) => typeof item === "string")
|
|
50
|
+
? (Object.fromEntries(entries) as Record<string, string>)
|
|
51
|
+
: null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The entry `--force` writes when it repairs an existing registration. */
|
|
55
|
+
export interface RepairedRegistration {
|
|
56
|
+
command: string;
|
|
57
|
+
args: string[];
|
|
58
|
+
env: Record<string, string>;
|
|
59
|
+
extra: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read an existing entry; null when it is not a command registration. */
|
|
63
|
+
export function readExistingRegistration(
|
|
64
|
+
entry: unknown,
|
|
65
|
+
format: McpConfigFormat
|
|
66
|
+
): ExistingRegistration | null {
|
|
67
|
+
if (!isPlainRecord(entry)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (format === "mcp") {
|
|
71
|
+
const { command, environment, ...extra } = entry;
|
|
72
|
+
const env = stringRecord(environment);
|
|
73
|
+
if (!isStringArray(command) || command.length === 0 || env === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const [executable = "", ...args] = command;
|
|
77
|
+
return { command: executable, args, env, extra };
|
|
78
|
+
}
|
|
79
|
+
const { command, args, env: rawEnv, ...extra } = entry;
|
|
80
|
+
const env = stringRecord(rawEnv);
|
|
81
|
+
if (typeof command !== "string" || !isStringArray(args) || env === null) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return { command, args, env, extra };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isGnoExecutable = (command: string): boolean =>
|
|
88
|
+
["gno", "gno.exe", "gno.cmd"].includes(basename(command).toLowerCase());
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The arguments after the launch prefix, or null when the registration is
|
|
92
|
+
* not a recognizable GNO MCP launch (it is then replaced as before).
|
|
93
|
+
*/
|
|
94
|
+
function argumentsAfterLaunch(
|
|
95
|
+
registration: ExistingRegistration
|
|
96
|
+
): string[] | null {
|
|
97
|
+
const { command, args } = registration;
|
|
98
|
+
let rest: string[] | null = null;
|
|
99
|
+
if (args[0] === "run" && args.length >= 2) {
|
|
100
|
+
rest = args.slice(2);
|
|
101
|
+
} else if (isGnoExecutable(command)) {
|
|
102
|
+
rest = args;
|
|
103
|
+
}
|
|
104
|
+
return rest?.includes("mcp") ? rest : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Set `flag value` in `list`, replacing `flag v` or `flag=v` in place. */
|
|
108
|
+
function setFlag(list: string[], flag: string, value: string): string[] {
|
|
109
|
+
const position = list.findIndex(
|
|
110
|
+
(item) => item === flag || item.startsWith(`${flag}=`)
|
|
111
|
+
);
|
|
112
|
+
if (position === -1) {
|
|
113
|
+
return [...list, flag, value];
|
|
114
|
+
}
|
|
115
|
+
const next = [...list];
|
|
116
|
+
if (next[position] === flag) {
|
|
117
|
+
next.splice(position, 2, flag, value);
|
|
118
|
+
} else {
|
|
119
|
+
next.splice(position, 1, flag, value);
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The entry `--force` writes over `existing`: `fresh`'s launch path, the
|
|
126
|
+
* existing arguments and environment with explicit options applied, and the
|
|
127
|
+
* existing keys GNO does not manage. Null when `existing` is not a GNO MCP
|
|
128
|
+
* launch, so the caller writes `fresh` as before.
|
|
129
|
+
*/
|
|
130
|
+
export function repairRegistration(
|
|
131
|
+
existing: ExistingRegistration,
|
|
132
|
+
fresh: McpServerEntry,
|
|
133
|
+
explicit: ExplicitInstallValues
|
|
134
|
+
): RepairedRegistration | null {
|
|
135
|
+
const rest = argumentsAfterLaunch(existing);
|
|
136
|
+
if (!rest) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const mcpPosition = rest.indexOf("mcp");
|
|
140
|
+
let globals = rest.slice(0, mcpPosition);
|
|
141
|
+
let subcommand = rest.slice(mcpPosition + 1);
|
|
142
|
+
if (explicit.indexName !== undefined) {
|
|
143
|
+
globals = setFlag(globals, "--index", explicit.indexName);
|
|
144
|
+
}
|
|
145
|
+
if (explicit.configPath !== undefined) {
|
|
146
|
+
globals = setFlag(globals, "--config", explicit.configPath);
|
|
147
|
+
}
|
|
148
|
+
if (explicit.toolProfile !== undefined) {
|
|
149
|
+
subcommand = setFlag(subcommand, "--tool-profile", explicit.toolProfile);
|
|
150
|
+
}
|
|
151
|
+
if (explicit.enableWrite && !subcommand.includes("--enable-write")) {
|
|
152
|
+
subcommand = [...subcommand, "--enable-write"];
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
command: fresh.command,
|
|
156
|
+
args: [...fresh.args.slice(0, 2), ...globals, "mcp", ...subcommand],
|
|
157
|
+
env: { ...fresh.env, ...existing.env },
|
|
158
|
+
extra: existing.extra,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -101,6 +101,54 @@ interface StatusResult {
|
|
|
101
101
|
// Config Reading
|
|
102
102
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
103
|
|
|
104
|
+
const isStringArray = (value: unknown): boolean =>
|
|
105
|
+
Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Client settings that never change how the command runs: timeouts, labels
|
|
109
|
+
* and approval prompts. Anything else (cwd, url, headers, another transport,
|
|
110
|
+
* unknown keys) keeps an entry fail-closed, because the connector verifier
|
|
111
|
+
* starts the registered command.
|
|
112
|
+
*/
|
|
113
|
+
const INERT_ENTRY_KEYS: Record<string, (value: unknown) => boolean> = {
|
|
114
|
+
type: (value) => value === "stdio",
|
|
115
|
+
timeout: (value) => typeof value === "number",
|
|
116
|
+
startup_timeout_sec: (value) => typeof value === "number",
|
|
117
|
+
tool_timeout_sec: (value) => typeof value === "number",
|
|
118
|
+
description: (value) => typeof value === "string",
|
|
119
|
+
autoApprove: isStringArray,
|
|
120
|
+
alwaysAllow: isStringArray,
|
|
121
|
+
enabled: (value) => typeof value === "boolean",
|
|
122
|
+
disabled: (value) => typeof value === "boolean",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Keys other than the launch keys are all known and inert. */
|
|
126
|
+
function hasOnlyInertExtras(
|
|
127
|
+
record: Record<string, unknown>,
|
|
128
|
+
launchKeys: readonly string[],
|
|
129
|
+
allowed: Record<string, (value: unknown) => boolean>
|
|
130
|
+
): boolean {
|
|
131
|
+
return Object.entries(record).every(
|
|
132
|
+
([key, value]) =>
|
|
133
|
+
launchKeys.includes(key) || (allowed[key]?.(value) ?? false)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** OpenCode writes `type: "local"` and `enabled` itself. */
|
|
138
|
+
const OPENCODE_INERT_KEYS: Record<string, (value: unknown) => boolean> = {
|
|
139
|
+
timeout: (value) => typeof value === "number",
|
|
140
|
+
enabled: (value) => typeof value === "boolean",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** A registration the client has switched off. */
|
|
144
|
+
function isDisabledEntry(entry: unknown): boolean {
|
|
145
|
+
if (!entry || typeof entry !== "object") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const record = entry as Record<string, unknown>;
|
|
149
|
+
return record.enabled === false || record.disabled === true;
|
|
150
|
+
}
|
|
151
|
+
|
|
104
152
|
/**
|
|
105
153
|
* Normalize entry to standard format for display.
|
|
106
154
|
*/
|
|
@@ -120,12 +168,10 @@ function normalizeEntry(
|
|
|
120
168
|
!Array.isArray(record.command) ||
|
|
121
169
|
record.command.length === 0 ||
|
|
122
170
|
!record.command.every((part) => typeof part === "string") ||
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
key !== "enabled" &&
|
|
128
|
-
key !== "environment"
|
|
171
|
+
!hasOnlyInertExtras(
|
|
172
|
+
record,
|
|
173
|
+
["type", "command", "environment"],
|
|
174
|
+
OPENCODE_INERT_KEYS
|
|
129
175
|
)
|
|
130
176
|
) {
|
|
131
177
|
return null;
|
|
@@ -149,9 +195,7 @@ function normalizeEntry(
|
|
|
149
195
|
record.command.length === 0 ||
|
|
150
196
|
!Array.isArray(record.args) ||
|
|
151
197
|
!record.args.every((argument) => typeof argument === "string") ||
|
|
152
|
-
|
|
153
|
-
(key) => key !== "command" && key !== "args" && key !== "env"
|
|
154
|
-
)
|
|
198
|
+
!hasOnlyInertExtras(record, ["command", "args", "env"], INERT_ENTRY_KEYS)
|
|
155
199
|
) {
|
|
156
200
|
return null;
|
|
157
201
|
}
|
|
@@ -211,12 +255,8 @@ export async function checkMcpTargetStatus(
|
|
|
211
255
|
return { target, scope, configPath, configured: false };
|
|
212
256
|
}
|
|
213
257
|
const entry = parsed.entry;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
entry &&
|
|
217
|
-
typeof entry === "object" &&
|
|
218
|
-
(entry as { enabled?: unknown }).enabled === false
|
|
219
|
-
) {
|
|
258
|
+
// A registration the client has switched off is not configured.
|
|
259
|
+
if (isDisabledEntry(entry)) {
|
|
220
260
|
return { target, scope, configPath, configured: false };
|
|
221
261
|
}
|
|
222
262
|
const configIdentity = entryIdentity(entry);
|
package/src/cli/commands/mcp.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { McpToolProfile } from "../../mcp/tool-profile";
|
|
8
8
|
import type { GlobalOptions } from "../context";
|
|
9
9
|
|
|
10
|
+
import { warnIfBunBelowFloor } from "../../app/bun-runtime";
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Start the MCP server.
|
|
12
14
|
* Reads global options for --index and --config flags.
|
|
@@ -15,6 +17,8 @@ export async function mcpCommand(
|
|
|
15
17
|
options: GlobalOptions,
|
|
16
18
|
commandOptions: { enableWrite?: boolean; toolProfile?: McpToolProfile } = {}
|
|
17
19
|
): Promise<void> {
|
|
20
|
+
// stderr only: stdout carries the MCP protocol.
|
|
21
|
+
warnIfBunBelowFloor("mcp");
|
|
18
22
|
const { startMcpServer } = await import("../../mcp/server.js");
|
|
19
23
|
await startMcpServer({
|
|
20
24
|
indexName: options.index,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @module src/cli/commands/serve
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { warnIfBunBelowFloor } from "../../app/bun-runtime";
|
|
9
|
+
|
|
8
10
|
export type { ServeOptions, ServeResult } from "../../serve";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -14,6 +16,7 @@ export type { ServeOptions, ServeResult } from "../../serve";
|
|
|
14
16
|
export async function serve(
|
|
15
17
|
options: import("../../serve").ServeOptions = {}
|
|
16
18
|
): Promise<import("../../serve").ServeResult> {
|
|
19
|
+
warnIfBunBelowFloor("serve");
|
|
17
20
|
const { startServer } = await import("../../serve");
|
|
18
21
|
return startServer(options);
|
|
19
22
|
}
|
|
@@ -44,6 +44,12 @@ import {
|
|
|
44
44
|
resolveProcessPaths,
|
|
45
45
|
statusProcess,
|
|
46
46
|
} from "../detach";
|
|
47
|
+
import {
|
|
48
|
+
checkRegistrationRuntimes,
|
|
49
|
+
checkResidentRuntimes,
|
|
50
|
+
formatRuntimeIssue,
|
|
51
|
+
type RuntimeIssue,
|
|
52
|
+
} from "../runtime-health";
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
* Options for status command.
|
|
@@ -70,6 +76,8 @@ export type StatusResult =
|
|
|
70
76
|
contentTypeBoost: ContentTypeBoostStatus;
|
|
71
77
|
memory: MemoryStatus;
|
|
72
78
|
backgroundIssues: ResidentBackgroundIssue[];
|
|
79
|
+
/** Registrations and residents on a missing, old or other Bun (fn-213). */
|
|
80
|
+
runtimeIssues?: RuntimeIssue[];
|
|
73
81
|
/** Files whose latest conversion stopped at the per-file budget. */
|
|
74
82
|
budgetStops: BudgetStop[];
|
|
75
83
|
}
|
|
@@ -433,6 +441,10 @@ export async function status(
|
|
|
433
441
|
contentTypeBoost: buildContentTypeBoostStatus(config.contentTypes ?? []),
|
|
434
442
|
memory: await buildMemoryStatus(store, config.collections),
|
|
435
443
|
backgroundIssues: await collectResidentIssues(),
|
|
444
|
+
runtimeIssues: [
|
|
445
|
+
...(await checkRegistrationRuntimes()),
|
|
446
|
+
...(await checkResidentRuntimes()),
|
|
447
|
+
],
|
|
436
448
|
budgetStops: await collectBudgetStops(store),
|
|
437
449
|
};
|
|
438
450
|
} finally {
|
|
@@ -486,6 +498,9 @@ export function formatStatus(
|
|
|
486
498
|
...(result.backgroundIssues.length
|
|
487
499
|
? { backgroundIssues: result.backgroundIssues }
|
|
488
500
|
: {}),
|
|
501
|
+
...(result.runtimeIssues?.length
|
|
502
|
+
? { runtimeIssues: result.runtimeIssues }
|
|
503
|
+
: {}),
|
|
489
504
|
},
|
|
490
505
|
null,
|
|
491
506
|
2
|
|
@@ -524,5 +539,14 @@ export function formatStatus(
|
|
|
524
539
|
]
|
|
525
540
|
: []),
|
|
526
541
|
...(issueLines.length ? ["", "Background issues:", ...issueLines] : []),
|
|
542
|
+
...(result.runtimeIssues?.length
|
|
543
|
+
? [
|
|
544
|
+
"",
|
|
545
|
+
"Bun runtime:",
|
|
546
|
+
...result.runtimeIssues.map(
|
|
547
|
+
(issue) => ` [${issue.severity}] ${formatRuntimeIssue(issue)}`
|
|
548
|
+
),
|
|
549
|
+
]
|
|
550
|
+
: []),
|
|
527
551
|
].join("\n");
|
|
528
552
|
}
|
package/src/cli/detach.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface PidFilePayload {
|
|
|
56
56
|
version: string;
|
|
57
57
|
started_at: string;
|
|
58
58
|
port?: number | null;
|
|
59
|
+
/** Bun the resident runs on (fn-213); absent in older pid-files. */
|
|
60
|
+
bun_version?: string;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/** Result of a `--status` call (matches process-status@1.0). */
|
|
@@ -246,6 +248,9 @@ export async function readPidFile(
|
|
|
246
248
|
version: record.version,
|
|
247
249
|
started_at: record.started_at,
|
|
248
250
|
port,
|
|
251
|
+
...(typeof record.bun_version === "string" && record.bun_version
|
|
252
|
+
? { bun_version: record.bun_version }
|
|
253
|
+
: {}),
|
|
249
254
|
};
|
|
250
255
|
}
|
|
251
256
|
|
|
@@ -610,6 +615,11 @@ export async function spawnDetached(
|
|
|
610
615
|
version: VERSION,
|
|
611
616
|
started_at: new Date().toISOString(),
|
|
612
617
|
port: options.port ?? null,
|
|
618
|
+
// The child runs on this process's Bun unless a caller replaced the
|
|
619
|
+
// command.
|
|
620
|
+
...(options.cmdPrefix || options.execPath
|
|
621
|
+
? {}
|
|
622
|
+
: { bun_version: Bun.version }),
|
|
613
623
|
};
|
|
614
624
|
|
|
615
625
|
// If the pid-file write fails (disk full, permission race, bad path on
|
package/src/cli/program.ts
CHANGED
|
@@ -2644,7 +2644,7 @@ function wireMcpCommand(program: Command): void {
|
|
|
2644
2644
|
"advertised tool set the registration starts with: core (7 read tools + capture/remember with --enable-write) or full (default)"
|
|
2645
2645
|
)
|
|
2646
2646
|
.option("--json", "JSON output")
|
|
2647
|
-
.action(async (cmdOpts: Record<string, unknown
|
|
2647
|
+
.action(async (cmdOpts: Record<string, unknown>, command: Command) => {
|
|
2648
2648
|
const target = cmdOpts.target as string;
|
|
2649
2649
|
const toolProfile = parseToolProfileOption(cmdOpts.toolProfile);
|
|
2650
2650
|
const requestedScope = cmdOpts.scope;
|
|
@@ -2687,9 +2687,14 @@ function wireMcpCommand(program: Command): void {
|
|
|
2687
2687
|
scope: scope as "user" | "project",
|
|
2688
2688
|
force: Boolean(cmdOpts.force),
|
|
2689
2689
|
dryRun: Boolean(cmdOpts.dryRun),
|
|
2690
|
-
|
|
2690
|
+
// Only options the user passed: --force keeps the rest of an
|
|
2691
|
+
// existing registration (--index has a default, so check its source).
|
|
2692
|
+
enableWrite: cmdOpts.enableWrite === true ? true : undefined,
|
|
2691
2693
|
toolProfile,
|
|
2692
|
-
indexName:
|
|
2694
|
+
indexName:
|
|
2695
|
+
command.getOptionValueSourceWithGlobals("index") === "cli"
|
|
2696
|
+
? globals.index
|
|
2697
|
+
: undefined,
|
|
2693
2698
|
configPath: globals.config,
|
|
2694
2699
|
// Pass undefined if not set, so global --json can take effect
|
|
2695
2700
|
json: cmdOpts.json === true ? true : undefined,
|