@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/dist/cli/config/domains/context-cli.js +86 -0
  4. package/dist/cli/config/domains/domain-cli.js +468 -0
  5. package/dist/cli/config/general.js +174 -0
  6. package/dist/cli/config/inspect.js +29 -26
  7. package/dist/cli/config/migrate.js +6 -22
  8. package/dist/cli/config/scope.js +35 -0
  9. package/dist/cli/connect/relay.js +284 -0
  10. package/dist/cli/core/command-tree.js +68 -0
  11. package/dist/cli/core/serve-options.js +71 -0
  12. package/dist/cli/init/setup-config.js +26 -0
  13. package/dist/cli/init.js +37 -8
  14. package/dist/cli/maintenance-prune.js +1 -1
  15. package/dist/cli/maintenance.js +6 -6
  16. package/dist/cli/mcp/external-mcp.js +29 -17
  17. package/dist/cli/mcp/status.js +2 -2
  18. package/dist/cli/system/status.js +35 -0
  19. package/dist/cli.js +132 -272
  20. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +2 -2
  21. package/dist/mcp/server/core/schemas.js +2 -10
  22. package/dist/mcp/server/operations/runtime/operation-runtime.js +11 -7
  23. package/dist/runtime/config/config.js +30 -29
  24. package/dist/runtime/config/definition/general-config.js +18 -3
  25. package/dist/runtime/config/resolution/resolver.js +7 -4
  26. package/dist/runtime/config/user-config.js +6 -6
  27. package/dist/runtime/config/validation/paths.js +12 -0
  28. package/dist/subagents/profiles.js +37 -0
  29. package/dist/workspaces/bootstrap.js +31 -14
  30. package/dist/workspaces/context.js +159 -7
  31. package/dist/workspaces/relay/auth/cli-test-support.js +22 -0
  32. package/dist/workspaces/resources/context-sources.js +29 -0
  33. package/dist/workspaces/resources/resource-monitor.js +29 -6
  34. package/dist/workspaces/resources/skills.js +15 -10
  35. package/dist/workspaces/sessions.js +4 -2
  36. package/dist/workspaces/state/project-context.js +34 -8
  37. package/dist/workspaces.js +5 -2
  38. package/docs/chatgpt-coding-workflow.md +23 -20
  39. package/docs/configuration.md +40 -27
  40. package/docs/gotchas.md +8 -6
  41. package/docs/roadmap.md +1 -1
  42. package/package.json +2 -2
  43. package/schemas/v1/config.project-local.schema.json +74 -0
  44. package/schemas/v1/config.project.schema.json +74 -0
  45. package/schemas/v1/config.user.schema.json +57 -2
  46. package/scripts/ci/config-v2-product-acceptance.mjs +17 -12
  47. package/scripts/debug/runtime.mjs +19 -3
  48. package/scripts/debug/runtime.test.mjs +3 -0
  49. 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 { expandHomePath } from "../mcp/filesystem/roots.js";
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
- .split(",")
32
- .map((root) => resolve(expandHomePath(root.trim())))
33
- .filter(Boolean);
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 maintenance prune");
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"}`);
@@ -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 maintenance ${subcommand} option: ${unknown[0]}`);
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 maintenance inspection`);
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 maintenance",
598
+ "ForgeRelay system",
599
599
  "",
600
600
  "Usage:",
601
- " forgerelay maintenance inspect [--json]",
602
- " forgerelay maintenance prune [--json]",
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, formatExternalMcpList, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./status.js";
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 runExternalMcpList(parseMcpScopeArgs("list", rest), dependencies);
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 runExternalMcpList(options, dependencies) {
77
- if (options.rest.length > 0) {
78
- throw new Error("Usage: forgerelay mcp list [--project <path>|--global]");
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
- console.log(formatExternalMcpList(status));
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 list [--project <path>|--global]",
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
  }
@@ -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 formatExternalMcpList(status) {
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
+ }