@akira-tl/forgerelay 1.2.5 → 1.3.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/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/dist/cli/config/domains/context-cli.js +86 -0
- package/dist/cli/config/domains/domain-cli.js +468 -0
- package/dist/cli/config/general.js +174 -0
- package/dist/cli/config/inspect.js +29 -26
- package/dist/cli/config/migrate.js +6 -22
- package/dist/cli/config/scope.js +35 -0
- package/dist/cli/connect/relay.js +284 -0
- package/dist/cli/core/command-tree.js +68 -0
- package/dist/cli/core/serve-options.js +71 -0
- package/dist/cli/init/setup-config.js +26 -0
- package/dist/cli/init.js +37 -8
- package/dist/cli/maintenance-prune.js +1 -1
- package/dist/cli/maintenance.js +6 -6
- package/dist/cli/mcp/external-mcp.js +29 -17
- package/dist/cli/mcp/status.js +2 -2
- package/dist/cli/system/status.js +35 -0
- package/dist/cli.js +132 -272
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +2 -2
- package/dist/mcp/server/core/schemas.js +2 -10
- package/dist/mcp/server/operations/runtime/operation-runtime.js +11 -7
- package/dist/runtime/config/config.js +30 -29
- package/dist/runtime/config/definition/general-config.js +18 -3
- package/dist/runtime/config/resolution/resolver.js +7 -4
- package/dist/runtime/config/user-config.js +6 -6
- package/dist/runtime/config/validation/paths.js +12 -0
- package/dist/subagents/profiles.js +37 -0
- package/dist/workspaces/bootstrap.js +31 -14
- package/dist/workspaces/context.js +159 -7
- package/dist/workspaces/relay/auth/cli-test-support.js +22 -0
- package/dist/workspaces/resources/context-sources.js +29 -0
- package/dist/workspaces/resources/resource-monitor.js +29 -6
- package/dist/workspaces/resources/skills.js +15 -10
- package/dist/workspaces/sessions.js +4 -2
- package/dist/workspaces/state/project-context.js +34 -8
- package/dist/workspaces.js +5 -2
- package/docs/chatgpt-coding-workflow.md +23 -20
- package/docs/configuration.md +40 -27
- package/docs/gotchas.md +8 -6
- package/docs/roadmap.md +1 -1
- package/package.json +2 -2
- package/schemas/v1/config.project-local.schema.json +74 -0
- package/schemas/v1/config.project.schema.json +74 -0
- package/schemas/v1/config.user.schema.json +57 -2
- package/scripts/ci/config-v2-product-acceptance.mjs +17 -12
- package/scripts/debug/runtime.mjs +19 -3
- package/scripts/debug/runtime.test.mjs +3 -0
- package/scripts/debug/serve.mjs +2 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const CLI_ROOT_ROUTES = [
|
|
2
|
+
{ command: "serve", handler: "serve", publicSummary: "Start the ForgeRelay runtime" },
|
|
3
|
+
{ command: "init", handler: "init", publicSummary: "Run first-time or setup-owned configuration" },
|
|
4
|
+
{ command: "config", handler: "config", publicSummary: "Inspect and mutate declarative configuration" },
|
|
5
|
+
{ command: "connect", handler: "connect", publicSummary: "Manage remote ForgeRelay and External MCP relationships" },
|
|
6
|
+
{ command: "system", handler: "system", publicSummary: "Diagnose and maintain ForgeRelay" },
|
|
7
|
+
{ command: "help", handler: "help", publicSummary: "Show this help" },
|
|
8
|
+
{ command: "version", handler: "version", publicSummary: "Print the installed version" },
|
|
9
|
+
// Compatibility-only routes. These remain dispatchable during the supported
|
|
10
|
+
// compatibility window but never appear in generated public root help.
|
|
11
|
+
{ command: "start", handler: "serve" },
|
|
12
|
+
{ command: "doctor", handler: "system", argsPrefix: ["doctor"] },
|
|
13
|
+
{ command: "hooks", handler: "config", argsPrefix: ["hooks", "--compat"] },
|
|
14
|
+
{ command: "auth", handler: "connect", argsPrefix: ["relay"] },
|
|
15
|
+
{ command: "mcp", handler: "connect", argsPrefix: ["mcp"] },
|
|
16
|
+
{ command: "maintenance", handler: "system" },
|
|
17
|
+
{ command: "agents", compatibilityHandler: "agents" },
|
|
18
|
+
{ command: "--help", handler: "help" },
|
|
19
|
+
{ command: "-h", handler: "help" },
|
|
20
|
+
{ command: "--version", handler: "version" },
|
|
21
|
+
{ command: "-v", handler: "version" },
|
|
22
|
+
];
|
|
23
|
+
export function resolveCliRootRoute(command) {
|
|
24
|
+
if (command === undefined)
|
|
25
|
+
return { command: "help", handler: "help" };
|
|
26
|
+
const route = CLI_ROOT_ROUTES.find((candidate) => candidate.command === command);
|
|
27
|
+
if (!route)
|
|
28
|
+
throw new Error(`Unknown command: ${command}`);
|
|
29
|
+
return route;
|
|
30
|
+
}
|
|
31
|
+
export function routeArguments(route, args) {
|
|
32
|
+
return [...(route.argsPrefix ?? []), ...args];
|
|
33
|
+
}
|
|
34
|
+
const SERVE_OPTION_HELP_LINES = [
|
|
35
|
+
"--host <host> Override the bind host for this invocation",
|
|
36
|
+
"--port <port> Override the listen port for this invocation",
|
|
37
|
+
"--root <path> Override allowed roots; repeat for multiple roots",
|
|
38
|
+
"--public-url <url> Override client-facing base URLs; repeat for multiple URLs",
|
|
39
|
+
"--allow-elevated Explicitly allow this invocation to run with elevated/unknown OS privilege",
|
|
40
|
+
];
|
|
41
|
+
export function renderCliRootHelp() {
|
|
42
|
+
const publicRoutes = CLI_ROOT_ROUTES.filter((route) => route.publicSummary !== undefined);
|
|
43
|
+
const commandWidth = Math.max(...publicRoutes.map((route) => route.command.length));
|
|
44
|
+
return [
|
|
45
|
+
"ForgeRelay",
|
|
46
|
+
"",
|
|
47
|
+
"Usage:",
|
|
48
|
+
" forgerelay Show help",
|
|
49
|
+
" forgerelay <command> [options] Run a command",
|
|
50
|
+
"",
|
|
51
|
+
"Commands:",
|
|
52
|
+
...publicRoutes.map((route) => ` forgerelay ${route.command.padEnd(commandWidth)} ${route.publicSummary}`),
|
|
53
|
+
"",
|
|
54
|
+
"Serve options:",
|
|
55
|
+
...SERVE_OPTION_HELP_LINES.map((line) => ` forgerelay serve ${line}`),
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
export function renderServeHelp() {
|
|
59
|
+
return [
|
|
60
|
+
"ForgeRelay serve",
|
|
61
|
+
"",
|
|
62
|
+
"Usage:",
|
|
63
|
+
" forgerelay serve [options]",
|
|
64
|
+
"",
|
|
65
|
+
"Options:",
|
|
66
|
+
...SERVE_OPTION_HELP_LINES.map((line) => ` ${line}`),
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { normalizeAllowedRootPath } from "../../runtime/config/validation/paths.js";
|
|
2
|
+
import { normalizePublicBaseUrlsInput, validateBindAddress, validateClientFacingBaseUrls, validatePort, } from "../setup-support.js";
|
|
3
|
+
export function parseServeCommandArgs(args) {
|
|
4
|
+
let allowElevated = false;
|
|
5
|
+
let host;
|
|
6
|
+
let port;
|
|
7
|
+
const roots = [];
|
|
8
|
+
const publicBaseUrls = [];
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === "--allow-elevated") {
|
|
12
|
+
if (allowElevated)
|
|
13
|
+
throw new Error("--allow-elevated may only be supplied once.");
|
|
14
|
+
allowElevated = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (arg === "--host") {
|
|
18
|
+
if (host !== undefined)
|
|
19
|
+
throw new Error("--host may only be supplied once.");
|
|
20
|
+
const value = args[++index];
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
throw new Error("Missing value for --host.");
|
|
23
|
+
const validation = validateBindAddress(value);
|
|
24
|
+
if (validation)
|
|
25
|
+
throw new Error(`Invalid --host: ${validation}`);
|
|
26
|
+
host = value.trim();
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--port") {
|
|
30
|
+
if (port !== undefined)
|
|
31
|
+
throw new Error("--port may only be supplied once.");
|
|
32
|
+
const value = args[++index];
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
throw new Error("Missing value for --port.");
|
|
35
|
+
const validation = validatePort(value);
|
|
36
|
+
if (validation)
|
|
37
|
+
throw new Error(`Invalid --port: ${validation}`);
|
|
38
|
+
port = Number(value);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--root") {
|
|
42
|
+
const value = args[++index];
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
throw new Error("Missing value for --root.");
|
|
45
|
+
roots.push(normalizeAllowedRootPath(value));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === "--public-url") {
|
|
49
|
+
const value = args[++index];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
throw new Error("Missing value for --public-url.");
|
|
52
|
+
const validation = validateClientFacingBaseUrls(value);
|
|
53
|
+
if (validation)
|
|
54
|
+
throw new Error(`Invalid --public-url: ${validation}`);
|
|
55
|
+
publicBaseUrls.push(...normalizePublicBaseUrlsInput(value));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Unknown serve option: ${arg}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
allowElevated,
|
|
62
|
+
runtimeOverrides: {
|
|
63
|
+
...(host === undefined ? {} : { host }),
|
|
64
|
+
...(port === undefined ? {} : { port }),
|
|
65
|
+
...(roots.length === 0 ? {} : { allowedRoots: roots }),
|
|
66
|
+
...(publicBaseUrls.length === 0
|
|
67
|
+
? {}
|
|
68
|
+
: { publicBaseUrl: Array.from(new Set(publicBaseUrls)) }),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_INSTRUCTION_NAMES, DEFAULT_SKILL_PATHS, DEFAULT_SYSTEM_INSTRUCTIONS_PATH, } from "../../runtime/config/definition/general-config.js";
|
|
1
2
|
export function applySetupConfig(current, selection) {
|
|
2
3
|
const next = {
|
|
3
4
|
...current,
|
|
@@ -5,6 +6,8 @@ export function applySetupConfig(current, selection) {
|
|
|
5
6
|
allowedRoots: [...selection.allowedRoots],
|
|
6
7
|
};
|
|
7
8
|
applyNetworkSelection(next, selection.network);
|
|
9
|
+
if (selection.context)
|
|
10
|
+
applyAgentContextSelection(next, selection.context);
|
|
8
11
|
if (selection.advanced)
|
|
9
12
|
applyAdvancedSelection(next, selection.advanced);
|
|
10
13
|
return next;
|
|
@@ -22,6 +25,29 @@ function applyNetworkSelection(config, selection) {
|
|
|
22
25
|
config.publicBaseUrl = selection.publicBaseUrl;
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
function applyAgentContextSelection(config, selection) {
|
|
29
|
+
if (selection.systemInstructionsPath === DEFAULT_SYSTEM_INSTRUCTIONS_PATH) {
|
|
30
|
+
delete config.systemInstructionsPath;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
config.systemInstructionsPath = selection.systemInstructionsPath;
|
|
34
|
+
}
|
|
35
|
+
if (sameStringList(selection.instructionNames, DEFAULT_INSTRUCTION_NAMES)) {
|
|
36
|
+
delete config.instructionNames;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
config.instructionNames = [...selection.instructionNames];
|
|
40
|
+
}
|
|
41
|
+
if (sameStringList(selection.skillPaths, DEFAULT_SKILL_PATHS)) {
|
|
42
|
+
delete config.skillPaths;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
config.skillPaths = [...selection.skillPaths];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function sameStringList(left, right) {
|
|
49
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
50
|
+
}
|
|
25
51
|
function applyAdvancedSelection(config, selection) {
|
|
26
52
|
if (selection.port === 7676)
|
|
27
53
|
delete config.port;
|
package/dist/cli/init.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import * as prompts from "@clack/prompts";
|
|
3
2
|
import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
|
|
4
|
-
import {
|
|
3
|
+
import { normalizeAllowedRootPaths } from "../runtime/config/validation/paths.js";
|
|
5
4
|
import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
|
|
6
5
|
import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
|
|
7
6
|
import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
|
|
8
7
|
import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
|
|
9
|
-
import { generalConfigDefinition } from "../runtime/config/definition/general-config.js";
|
|
8
|
+
import { DEFAULT_INSTRUCTION_NAMES, DEFAULT_SKILL_PATHS, DEFAULT_SYSTEM_INSTRUCTIONS_PATH, generalConfigDefinition, } from "../runtime/config/definition/general-config.js";
|
|
10
9
|
import { configSchemaId } from "../runtime/config/definition/schema.js";
|
|
11
10
|
import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
|
|
12
11
|
import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
|
|
13
|
-
import { applySetupConfig } from "./init/setup-config.js";
|
|
12
|
+
import { applySetupConfig, } from "./init/setup-config.js";
|
|
13
|
+
function parseSetupList(value) {
|
|
14
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
function validateSetupList(value) {
|
|
17
|
+
return value && parseSetupList(value).length > 0 ? undefined : "Enter at least one comma-separated value.";
|
|
18
|
+
}
|
|
14
19
|
export async function runInit({ force, advanced, version }) {
|
|
15
20
|
const files = loadForgeRelayFiles();
|
|
16
21
|
if (!force && !advanced && files.configExists && files.authExists) {
|
|
@@ -27,10 +32,33 @@ export async function runInit({ force, advanced, version }) {
|
|
|
27
32
|
defaultValue: defaultRoots,
|
|
28
33
|
validate: (value) => value?.trim() ? undefined : "Enter at least one project root.",
|
|
29
34
|
});
|
|
30
|
-
const allowedRoots = rootsAnswer
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const allowedRoots = normalizeAllowedRootPaths(rootsAnswer.split(","));
|
|
36
|
+
const defaultSystemInstructionsPath = files.config.systemInstructionsPath ?? DEFAULT_SYSTEM_INSTRUCTIONS_PATH;
|
|
37
|
+
const systemInstructionsPath = (await textPrompt({
|
|
38
|
+
message: `Which system instruction file should ForgeRelay load? Press Enter to use ${defaultSystemInstructionsPath}`,
|
|
39
|
+
placeholder: defaultSystemInstructionsPath,
|
|
40
|
+
defaultValue: defaultSystemInstructionsPath,
|
|
41
|
+
validate: (value) => value?.trim() ? undefined : "Enter an instruction file path.",
|
|
42
|
+
})).trim();
|
|
43
|
+
const defaultInstructionNames = (files.config.instructionNames ?? DEFAULT_INSTRUCTION_NAMES).join(", ");
|
|
44
|
+
const instructionNames = parseSetupList(await textPrompt({
|
|
45
|
+
message: `Which project instruction filenames should ForgeRelay discover? Press Enter to use ${defaultInstructionNames}`,
|
|
46
|
+
placeholder: defaultInstructionNames,
|
|
47
|
+
defaultValue: defaultInstructionNames,
|
|
48
|
+
validate: validateSetupList,
|
|
49
|
+
}));
|
|
50
|
+
const defaultSkillPaths = (files.config.skillPaths ?? DEFAULT_SKILL_PATHS).join(", ");
|
|
51
|
+
const skillPaths = parseSetupList(await textPrompt({
|
|
52
|
+
message: `Which Skill directories should ForgeRelay scan? Press Enter to use ${defaultSkillPaths}`,
|
|
53
|
+
placeholder: defaultSkillPaths,
|
|
54
|
+
defaultValue: defaultSkillPaths,
|
|
55
|
+
validate: validateSetupList,
|
|
56
|
+
}));
|
|
57
|
+
const contextSelection = {
|
|
58
|
+
systemInstructionsPath,
|
|
59
|
+
instructionNames,
|
|
60
|
+
skillPaths,
|
|
61
|
+
};
|
|
34
62
|
const defaultPort = String(files.config.port ?? 7676);
|
|
35
63
|
const port = advanced
|
|
36
64
|
? Number(await textPrompt({
|
|
@@ -231,6 +259,7 @@ export async function runInit({ force, advanced, version }) {
|
|
|
231
259
|
mode: networkMode,
|
|
232
260
|
...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
|
|
233
261
|
},
|
|
262
|
+
context: contextSelection,
|
|
234
263
|
...(advancedSelection ? { advanced: advancedSelection } : {}),
|
|
235
264
|
});
|
|
236
265
|
const auth = {
|
|
@@ -91,7 +91,7 @@ export function pruneMaintenanceState(stateDir, policy, now = new Date()) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
export function printMaintenancePruneReport(report) {
|
|
94
|
-
console.log("ForgeRelay
|
|
94
|
+
console.log("ForgeRelay system prune");
|
|
95
95
|
console.log(`State directory: ${report.stateDir}`);
|
|
96
96
|
console.log(`Historical retention: ${report.historicalAuthorized ? `authorized before ${report.cutoff}` : "not authorized (unlimited)"}`);
|
|
97
97
|
console.log(`Orphan administrative cleanup: ${report.administrativeAuthorized ? "authorized" : "not authorized"}`);
|
package/dist/cli/maintenance.js
CHANGED
|
@@ -23,12 +23,12 @@ export function runMaintenanceCommand(args, env = process.env) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (subcommand !== "inspect" && subcommand !== "prune") {
|
|
26
|
-
throw new Error(`Unknown maintenance command: ${subcommand}`);
|
|
26
|
+
throw new Error(`Unknown system maintenance command: ${subcommand}`);
|
|
27
27
|
}
|
|
28
28
|
const json = rest.includes("--json");
|
|
29
29
|
const unknown = rest.filter((value) => value !== "--json");
|
|
30
30
|
if (unknown.length > 0)
|
|
31
|
-
throw new Error(`Unknown
|
|
31
|
+
throw new Error(`Unknown system ${subcommand} option: ${unknown[0]}`);
|
|
32
32
|
const inspection = inspectMaintenanceState(env);
|
|
33
33
|
if (subcommand === "inspect") {
|
|
34
34
|
if (json) {
|
|
@@ -567,7 +567,7 @@ function errorMessage(error) {
|
|
|
567
567
|
return error instanceof Error ? error.message : String(error);
|
|
568
568
|
}
|
|
569
569
|
function printMaintenanceReport(report) {
|
|
570
|
-
console.log(`ForgeRelay
|
|
570
|
+
console.log(`ForgeRelay system inspection`);
|
|
571
571
|
console.log(`State directory: ${report.stateDir}`);
|
|
572
572
|
console.log(`Database: ${report.database}`);
|
|
573
573
|
console.log(`Retention: durable history ${report.policy.durableHistory}; orphaned administrative cleanup ${report.policy.orphanedAdministrativeState ? "enabled" : "disabled"}`);
|
|
@@ -595,11 +595,11 @@ function formatBytes(bytes) {
|
|
|
595
595
|
}
|
|
596
596
|
function printMaintenanceHelp() {
|
|
597
597
|
console.log([
|
|
598
|
-
"ForgeRelay
|
|
598
|
+
"ForgeRelay system",
|
|
599
599
|
"",
|
|
600
600
|
"Usage:",
|
|
601
|
-
" forgerelay
|
|
602
|
-
" forgerelay
|
|
601
|
+
" forgerelay system inspect [--json]",
|
|
602
|
+
" forgerelay system prune [--json]",
|
|
603
603
|
"",
|
|
604
604
|
"Inspection is read-only. Durable history is retained without an age limit unless retention.historyDays is explicitly configured.",
|
|
605
605
|
"Prune is manual owner maintenance and removes only categories authorized by the configured retention policy.",
|
|
@@ -6,14 +6,17 @@ import { computeScopeUnion, selectClientAuthMethod, } from "@modelcontextprotoco
|
|
|
6
6
|
import { externalMcpCredentialIdentity, } from "../../runtime/config/external-mcp-auth-store.js";
|
|
7
7
|
import { ExternalMcpInteractiveOAuthProvider, beginExternalMcpInteractiveOAuth, finishExternalMcpInteractiveOAuth, } from "../../mcp/operations/external-mcp/external-mcp-oauth.js";
|
|
8
8
|
import { ExternalMcpError, ExternalMcpGateway, } from "../../mcp/operations/external-mcp/external-mcp.js";
|
|
9
|
-
import { findExternalMcpServerStatus, formatAuth,
|
|
9
|
+
import { findExternalMcpServerStatus, formatAuth, formatExternalMcpStatus, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./status.js";
|
|
10
10
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
11
11
|
export async function runExternalMcpCommand(args, dependencies = {}) {
|
|
12
12
|
const [subcommand, ...rest] = args;
|
|
13
13
|
switch (subcommand) {
|
|
14
|
+
case "status":
|
|
15
|
+
await runExternalMcpStatus(parseMcpScopeArgs("status", rest), dependencies);
|
|
16
|
+
return;
|
|
14
17
|
case "list":
|
|
15
18
|
case "ls":
|
|
16
|
-
await
|
|
19
|
+
await runExternalMcpStatus(parseMcpScopeArgs("list", rest), dependencies);
|
|
17
20
|
return;
|
|
18
21
|
case "test":
|
|
19
22
|
await runExternalMcpTest(parseMcpTargetArgs("test", rest), dependencies);
|
|
@@ -31,7 +34,7 @@ export async function runExternalMcpCommand(args, dependencies = {}) {
|
|
|
31
34
|
printMcpHelp();
|
|
32
35
|
return;
|
|
33
36
|
default:
|
|
34
|
-
throw new Error(`Unknown mcp command: ${subcommand}`);
|
|
37
|
+
throw new Error(`Unknown connect mcp command: ${subcommand}`);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
function parseMcpScopeArgs(command, args) {
|
|
@@ -51,7 +54,7 @@ function parseMcpScopeArgs(command, args) {
|
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
53
56
|
if (arg.startsWith("-"))
|
|
54
|
-
throw new Error(`Unknown mcp ${command} option: ${arg}`);
|
|
57
|
+
throw new Error(`Unknown connect mcp ${command} option: ${arg}`);
|
|
55
58
|
rest.push(arg);
|
|
56
59
|
}
|
|
57
60
|
if (global && projectRoot)
|
|
@@ -65,7 +68,7 @@ function parseMcpScopeArgs(command, args) {
|
|
|
65
68
|
function parseMcpTargetArgs(command, args) {
|
|
66
69
|
const parsed = parseMcpScopeArgs(command, args);
|
|
67
70
|
if (parsed.rest.length !== 1) {
|
|
68
|
-
throw new Error(`Usage: forgerelay mcp ${command} <server> [--project <path>|--global]`);
|
|
71
|
+
throw new Error(`Usage: forgerelay connect mcp ${command} <server> [--project <path>|--global]`);
|
|
69
72
|
}
|
|
70
73
|
return {
|
|
71
74
|
server: parsed.rest[0],
|
|
@@ -73,13 +76,22 @@ function parseMcpTargetArgs(command, args) {
|
|
|
73
76
|
...(parsed.global ? { global: true } : {}),
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
|
-
async function
|
|
77
|
-
if (options.rest.length >
|
|
78
|
-
throw new Error("Usage: forgerelay mcp
|
|
79
|
+
async function runExternalMcpStatus(options, dependencies) {
|
|
80
|
+
if (options.rest.length > 1) {
|
|
81
|
+
throw new Error("Usage: forgerelay connect mcp status [server] [--project <path>|--global]");
|
|
79
82
|
}
|
|
80
83
|
const scope = await resolveExternalMcpScope(options, dependencies);
|
|
81
84
|
const status = inspectExternalMcpStatus(scope);
|
|
82
|
-
|
|
85
|
+
const serverName = options.rest[0];
|
|
86
|
+
if (serverName) {
|
|
87
|
+
const server = findExternalMcpServerStatus(status, serverName);
|
|
88
|
+
if (!server)
|
|
89
|
+
throw new Error(`Unknown configured External MCP server: ${serverName}.`);
|
|
90
|
+
console.log(formatExternalMcpStatus({ ...status, servers: [server] }));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(formatExternalMcpStatus(status));
|
|
94
|
+
}
|
|
83
95
|
if (status.configIssues > 0) {
|
|
84
96
|
throw new Error("External MCP configuration or credential status contains issues.");
|
|
85
97
|
}
|
|
@@ -144,8 +156,8 @@ function printTestTarget(scope, server) {
|
|
|
144
156
|
}
|
|
145
157
|
function externalMcpAuthCommand(scope, server) {
|
|
146
158
|
return scope.mode === "global"
|
|
147
|
-
? `forgerelay mcp auth ${cliArgument(server)} --global`
|
|
148
|
-
: `forgerelay mcp auth ${cliArgument(server)} --project ${cliArgument(scope.projectRoot)}`;
|
|
159
|
+
? `forgerelay connect mcp auth ${cliArgument(server)} --global`
|
|
160
|
+
: `forgerelay connect mcp auth ${cliArgument(server)} --project ${cliArgument(scope.projectRoot)}`;
|
|
149
161
|
}
|
|
150
162
|
function cliArgument(value) {
|
|
151
163
|
return /^[A-Za-z0-9_./:\\-]+$/.test(value) ? value : JSON.stringify(value);
|
|
@@ -340,7 +352,7 @@ function hasStaticAuthorizationHeader(headers) {
|
|
|
340
352
|
function assertInteractive(dependencies) {
|
|
341
353
|
const interactive = dependencies.isInteractive ?? (Boolean(input.isTTY) && Boolean(output.isTTY));
|
|
342
354
|
if (!interactive) {
|
|
343
|
-
throw new Error("forgerelay mcp auth requires an interactive terminal for browser/callback authorization.");
|
|
355
|
+
throw new Error("forgerelay connect mcp auth requires an interactive terminal for browser/callback authorization.");
|
|
344
356
|
}
|
|
345
357
|
}
|
|
346
358
|
async function promptMaskedCallback(dependencies) {
|
|
@@ -474,12 +486,12 @@ function isHeadlessEnvironment(env) {
|
|
|
474
486
|
}
|
|
475
487
|
function printMcpHelp() {
|
|
476
488
|
console.log([
|
|
477
|
-
"ForgeRelay mcp",
|
|
489
|
+
"ForgeRelay connect mcp",
|
|
478
490
|
"",
|
|
479
491
|
"Usage:",
|
|
480
|
-
" forgerelay mcp
|
|
481
|
-
" forgerelay mcp test <server> [--project <path>|--global]",
|
|
482
|
-
" forgerelay mcp auth <server> [--project <path>|--global]",
|
|
483
|
-
" forgerelay mcp logout <server> [--project <path>|--global]",
|
|
492
|
+
" forgerelay connect mcp status [server] [--project <path>|--global]",
|
|
493
|
+
" forgerelay connect mcp test <server> [--project <path>|--global]",
|
|
494
|
+
" forgerelay connect mcp auth <server> [--project <path>|--global]",
|
|
495
|
+
" forgerelay connect mcp logout <server> [--project <path>|--global]",
|
|
484
496
|
].join("\n"));
|
|
485
497
|
}
|
package/dist/cli/mcp/status.js
CHANGED
|
@@ -72,7 +72,7 @@ export function inspectExternalMcpStatus(scope) {
|
|
|
72
72
|
export function findExternalMcpServerStatus(status, name) {
|
|
73
73
|
return status.servers.find((server) => server.name === name);
|
|
74
74
|
}
|
|
75
|
-
export function
|
|
75
|
+
export function formatExternalMcpStatus(status) {
|
|
76
76
|
const lines = ["External MCP", ""];
|
|
77
77
|
if (status.scope.mode === "global") {
|
|
78
78
|
lines.push("Scope: global");
|
|
@@ -147,7 +147,7 @@ export function formatExternalMcpDoctor(status) {
|
|
|
147
147
|
` Credential store: ${status.credentialStore}`,
|
|
148
148
|
` Config issues: ${status.configIssues}`,
|
|
149
149
|
" Hot reload: active",
|
|
150
|
-
" Active checks: not run (use `forgerelay mcp test <server>`)",
|
|
150
|
+
" Active checks: not run (use `forgerelay connect mcp test <server>`)",
|
|
151
151
|
].join("\n");
|
|
152
152
|
}
|
|
153
153
|
export function formatAuth(status) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveRuntimeConfigFacts } from "../../runtime/config/config.js";
|
|
2
|
+
import { inspectRuntimeLease } from "../../runtime/state/runtime-lease.js";
|
|
3
|
+
export function inspectSystemStatus(env = process.env) {
|
|
4
|
+
const config = resolveRuntimeConfigFacts(env);
|
|
5
|
+
return {
|
|
6
|
+
...(config.instanceId ? { instanceId: config.instanceId } : {}),
|
|
7
|
+
stateDir: config.stateDir,
|
|
8
|
+
host: config.host,
|
|
9
|
+
port: config.port,
|
|
10
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
11
|
+
lease: inspectRuntimeLease(config.stateDir),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function runSystemStatus(env = process.env) {
|
|
15
|
+
const status = inspectSystemStatus(env);
|
|
16
|
+
console.log([
|
|
17
|
+
"ForgeRelay system status",
|
|
18
|
+
"",
|
|
19
|
+
`Instance: ${status.instanceId ?? "not initialized"}`,
|
|
20
|
+
`Runtime: ${formatRuntimeState(status.lease)}`,
|
|
21
|
+
...(status.lease.pid === undefined ? [] : [`PID: ${status.lease.pid}`]),
|
|
22
|
+
`State dir: ${status.stateDir}`,
|
|
23
|
+
`Configured bind: ${status.host}:${status.port}`,
|
|
24
|
+
`Configured public URL: ${status.publicBaseUrl}`,
|
|
25
|
+
].join("\n"));
|
|
26
|
+
}
|
|
27
|
+
function formatRuntimeState(lease) {
|
|
28
|
+
if (lease.malformed)
|
|
29
|
+
return "unknown (malformed runtime lease)";
|
|
30
|
+
if (lease.active)
|
|
31
|
+
return "running";
|
|
32
|
+
if (lease.stale)
|
|
33
|
+
return "not running (stale runtime lease present)";
|
|
34
|
+
return "not running";
|
|
35
|
+
}
|