@akira-tl/forgerelay 1.2.4 → 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 (52) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -11
  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 +11 -24
  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 -2
  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/setup-support.js +3 -2
  19. package/dist/cli/system/status.js +35 -0
  20. package/dist/cli.js +132 -272
  21. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +2 -2
  22. package/dist/mcp/server/core/schemas.js +2 -10
  23. package/dist/mcp/server/operations/runtime/operation-runtime.js +11 -7
  24. package/dist/runtime/config/config.js +30 -29
  25. package/dist/runtime/config/definition/general-config.js +22 -6
  26. package/dist/runtime/config/external-mcp-config.js +4 -3
  27. package/dist/runtime/config/resolution/resolver.js +7 -4
  28. package/dist/runtime/config/user-config.js +9 -6
  29. package/dist/runtime/config/validation/paths.js +12 -0
  30. package/dist/runtime/config/validation/ports.js +9 -0
  31. package/dist/subagents/profiles.js +37 -0
  32. package/dist/workspaces/bootstrap.js +31 -14
  33. package/dist/workspaces/context.js +159 -7
  34. package/dist/workspaces/relay/auth/cli-test-support.js +22 -0
  35. package/dist/workspaces/resources/context-sources.js +29 -0
  36. package/dist/workspaces/resources/resource-monitor.js +29 -6
  37. package/dist/workspaces/resources/skills.js +15 -10
  38. package/dist/workspaces/sessions.js +4 -2
  39. package/dist/workspaces/state/project-context.js +34 -8
  40. package/dist/workspaces.js +5 -2
  41. package/docs/chatgpt-coding-workflow.md +23 -20
  42. package/docs/configuration.md +40 -27
  43. package/docs/gotchas.md +8 -6
  44. package/docs/roadmap.md +1 -1
  45. package/package.json +2 -2
  46. package/schemas/v1/config.project-local.schema.json +74 -0
  47. package/schemas/v1/config.project.schema.json +74 -0
  48. package/schemas/v1/config.user.schema.json +59 -4
  49. package/scripts/ci/config-v2-product-acceptance.mjs +17 -12
  50. package/scripts/debug/runtime.mjs +19 -3
  51. package/scripts/debug/runtime.test.mjs +3 -0
  52. package/scripts/debug/serve.mjs +2 -2
package/dist/cli.js CHANGED
@@ -7,12 +7,13 @@ import { readFile, rm } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import * as prompts from "@clack/prompts";
11
10
  import { loadConfig } from "./runtime/config/config.js";
12
11
  import { acquireRuntimeLease } from "./runtime/state/runtime-lease.js";
13
12
  import { runInit } from "./cli/init.js";
14
13
  import { runConfigMigration } from "./cli/config/migrate.js";
15
14
  import { runConfigInspection } from "./cli/config/inspect.js";
15
+ import { renderGeneralConfigHelp, runGeneralConfigGet, runGeneralConfigSet, runGeneralConfigUnset } from "./cli/config/general.js";
16
+ import { runConfigDomainCommand } from "./cli/config/domains/domain-cli.js";
16
17
  import { runMaintenanceCommand } from "./cli/maintenance.js";
17
18
  import { runExternalMcpCommand } from "./cli/mcp/external-mcp.js";
18
19
  import { formatExternalMcpDoctor, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./cli/mcp/status.js";
@@ -22,58 +23,42 @@ import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.j
22
23
  import { SubagentSessionManager } from "./subagents/sessions/manager.js";
23
24
  import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
24
25
  import { parseSubagentRunArgs } from "./subagents/cli-target.js";
25
- import { ensureForgeRelayInstanceId, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, writeForgeRelayConfig, writeForgeRelayRemote, } from "./runtime/config/user-config.js";
26
+ import { ensureForgeRelayInstanceId, loadForgeRelayFiles, } from "./runtime/config/user-config.js";
26
27
  import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
27
28
  import { publicEndpointUrl } from "./mcp/oauth/public-url.js";
28
29
  import { assertRuntimePrivilegeAllowed, detectRuntimePrivilege, elevatedRuntimeWarning, formatRuntimePrivilege, } from "./runtime/security/runtime-privilege.js";
29
30
  import { formatCommandShellRuntime } from "./runtime/shell/command-shell-runtime.js";
30
31
  import { commandShellCompatibilityWarning } from "./cli/shell/setup.js";
31
- import { authenticateRemote, defaultRemoteAlias, isRemoteMcpUnauthorized, normalizeRemoteServiceTarget, refreshRemoteAuthentication, verifyRemoteMcp, } from "./workspaces/relay/auth/remote-auth.js";
32
- import { defaultSshRouteAlias, parseSshRoute, readRemoteOwnerToken, withRemoteServiceEndpoint, } from "./workspaces/relay/transport/remote-transport.js";
33
- import { assertSupportedNode, checkGitAvailable, checkSqliteNative, nodeVersionStatus, normalizeOptionalPublicBaseUrl, } from "./cli/setup-support.js";
32
+ import { renderCliRootHelp, renderServeHelp, resolveCliRootRoute, routeArguments, } from "./cli/core/command-tree.js";
33
+ import { parseServeCommandArgs } from "./cli/core/serve-options.js";
34
+ import { runRelayCommand } from "./cli/connect/relay.js";
35
+ import { runSystemStatus } from "./cli/system/status.js";
36
+ import { assertSupportedNode, checkGitAvailable, checkSqliteNative, nodeVersionStatus, } from "./cli/setup-support.js";
34
37
  const require = createRequire(import.meta.url);
35
38
  async function main(argv) {
36
39
  assertSupportedNode();
37
40
  const [rawCommand, ...rest] = argv;
38
- const defaultServeFlag = rawCommand === "--allow-elevated";
39
- const command = defaultServeFlag ? "serve" : normalizeCommand(rawCommand);
40
- const args = defaultServeFlag ? argv : rest;
41
- switch (command) {
42
- case "serve": {
43
- const serveOptions = parseServeCommandArgs(args);
44
- const runtimePrivilege = detectRuntimePrivilege();
45
- assertRuntimePrivilegeAllowed(runtimePrivilege, serveOptions.allowElevated);
46
- if (serveOptions.allowElevated)
47
- console.warn(elevatedRuntimeWarning(runtimePrivilege));
48
- await ensureConfigured();
49
- await serve(runtimePrivilege);
41
+ const route = resolveCliRootRoute(rawCommand);
42
+ const args = routeArguments(route, rest);
43
+ if (route.compatibilityHandler) {
44
+ await runCompatibilityRootCommand(route.compatibilityHandler, args);
45
+ return;
46
+ }
47
+ switch (route.handler) {
48
+ case "serve":
49
+ await runServeCommand(args);
50
50
  return;
51
- }
52
- case "init": {
53
- const initOptions = parseInitCommandArgs(args);
54
- await runInit({ ...initOptions, version: installedForgeRelayVersion() });
55
- return;
56
- }
57
- case "doctor":
58
- await runDoctor();
51
+ case "init":
52
+ await runInitCommand(args);
59
53
  return;
60
54
  case "config":
61
- await runConfigCommand(args);
55
+ await runConfigRootCommand(args);
62
56
  return;
63
- case "hooks":
64
- await runHooksCommand(args);
57
+ case "connect":
58
+ await runConnectCommand(args);
65
59
  return;
66
- case "agents":
67
- await runAgentsCommand(args);
68
- return;
69
- case "auth":
70
- await runAuthCommand(args);
71
- return;
72
- case "mcp":
73
- await runExternalMcpCommand(args);
74
- return;
75
- case "maintenance":
76
- runMaintenanceCommand(args);
60
+ case "system":
61
+ await runSystemCommand(args);
77
62
  return;
78
63
  case "help":
79
64
  printHelp();
@@ -83,16 +68,83 @@ async function main(argv) {
83
68
  return;
84
69
  }
85
70
  }
86
- function normalizeCommand(command) {
87
- if (!command || command === "serve" || command === "start")
88
- return "serve";
89
- if (command === "init" || command === "doctor" || command === "config" || command === "hooks" || command === "agents" || command === "auth" || command === "mcp" || command === "maintenance")
90
- return command;
91
- if (command === "help" || command === "--help" || command === "-h")
92
- return "help";
93
- if (command === "version" || command === "--version" || command === "-v")
94
- return "version";
95
- throw new Error(`Unknown command: ${command}`);
71
+ async function runCompatibilityRootCommand(handler, args) {
72
+ switch (handler) {
73
+ case "agents":
74
+ await runAgentsCommand(args);
75
+ return;
76
+ }
77
+ }
78
+ async function runServeCommand(args) {
79
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
80
+ console.log(renderServeHelp());
81
+ return;
82
+ }
83
+ const serveOptions = parseServeCommandArgs(args);
84
+ const runtimePrivilege = detectRuntimePrivilege();
85
+ assertRuntimePrivilegeAllowed(runtimePrivilege, serveOptions.allowElevated);
86
+ if (serveOptions.allowElevated)
87
+ console.warn(elevatedRuntimeWarning(runtimePrivilege));
88
+ await ensureConfigured();
89
+ await serve(runtimePrivilege, serveOptions.runtimeOverrides);
90
+ }
91
+ async function runInitCommand(args) {
92
+ const initOptions = parseInitCommandArgs(args);
93
+ await runInit({ ...initOptions, version: installedForgeRelayVersion() });
94
+ }
95
+ async function runConfigRootCommand(args) {
96
+ const [domain, ...rest] = args;
97
+ if (domain === "hooks" && rest[0] === "--compat") {
98
+ await runHooksCommand(rest.slice(1));
99
+ return;
100
+ }
101
+ if (domain && await runConfigDomainCommand(domain, rest))
102
+ return;
103
+ if (domain === "hooks") {
104
+ await runHooksCommand(rest);
105
+ return;
106
+ }
107
+ await runConfigCommand(args);
108
+ }
109
+ async function runConnectCommand(args) {
110
+ const [domain, ...rest] = args;
111
+ if (domain === "relay") {
112
+ await runRelayCommand(rest);
113
+ return;
114
+ }
115
+ if (domain === "mcp") {
116
+ await runExternalMcpCommand(rest);
117
+ return;
118
+ }
119
+ if (!domain || domain === "help" || domain === "--help" || domain === "-h") {
120
+ printConnectHelp();
121
+ return;
122
+ }
123
+ throw new Error(`Unknown connect command: ${domain}`);
124
+ }
125
+ async function runSystemCommand(args) {
126
+ const [subcommand, ...rest] = args;
127
+ if (subcommand === "doctor") {
128
+ if (rest.length > 0)
129
+ throw new Error("forgerelay system doctor does not accept additional arguments.");
130
+ await runDoctor();
131
+ return;
132
+ }
133
+ if (subcommand === "status") {
134
+ if (rest.length > 0)
135
+ throw new Error("forgerelay system status does not accept additional arguments.");
136
+ runSystemStatus();
137
+ return;
138
+ }
139
+ if (subcommand === "inspect" || subcommand === "prune") {
140
+ runMaintenanceCommand([subcommand, ...rest]);
141
+ return;
142
+ }
143
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
144
+ printSystemHelp();
145
+ return;
146
+ }
147
+ throw new Error(`Unknown system command: ${subcommand}`);
96
148
  }
97
149
  function parseInitCommandArgs(args) {
98
150
  let force = false;
@@ -109,19 +161,6 @@ function parseInitCommandArgs(args) {
109
161
  }
110
162
  return { force, advanced };
111
163
  }
112
- function parseServeCommandArgs(args) {
113
- let allowElevated = false;
114
- for (const arg of args) {
115
- if (arg === "--allow-elevated") {
116
- if (allowElevated)
117
- throw new Error("--allow-elevated may only be supplied once.");
118
- allowElevated = true;
119
- continue;
120
- }
121
- throw new Error(`Unknown serve option: ${arg}`);
122
- }
123
- return { allowElevated };
124
- }
125
164
  async function ensureConfigured() {
126
165
  const files = loadForgeRelayFiles();
127
166
  if (files.configExists && files.authExists) {
@@ -144,7 +183,7 @@ async function ensureConfigured() {
144
183
  }
145
184
  await runInit({ force: false, advanced: false, version: installedForgeRelayVersion() });
146
185
  }
147
- async function serve(runtimePrivilege) {
186
+ async function serve(runtimePrivilege, runtimeOverrides = {}) {
148
187
  const sqliteStatus = checkSqliteNative();
149
188
  if (sqliteStatus !== "ok") {
150
189
  throw new Error([
@@ -156,7 +195,7 @@ async function serve(runtimePrivilege) {
156
195
  ].join("\n"));
157
196
  }
158
197
  const { createServer } = await import("./server.js");
159
- const config = loadConfig();
198
+ const config = loadConfig(process.env, { runtimeOverrides });
160
199
  config.runtimePrivilege = runtimePrivilege;
161
200
  const runtimeLease = acquireRuntimeLease(config.stateDir);
162
201
  let server;
@@ -209,165 +248,6 @@ async function serve(runtimePrivilege) {
209
248
  process.once("SIGINT", handleShutdown);
210
249
  process.once("SIGTERM", handleShutdown);
211
250
  }
212
- function parseAuthCommandArgs(args) {
213
- let target;
214
- let alias;
215
- let ownerToken;
216
- let sshRoute;
217
- let sshAuth = false;
218
- for (let index = 0; index < args.length; index += 1) {
219
- const arg = args[index];
220
- if (arg === "--alias") {
221
- alias = args[++index];
222
- if (!alias)
223
- throw new Error("Missing value for --alias.");
224
- continue;
225
- }
226
- if (arg === "--token") {
227
- ownerToken = args[++index];
228
- if (!ownerToken)
229
- throw new Error("Missing value for --token.");
230
- continue;
231
- }
232
- if (arg === "-J") {
233
- const route = args[++index];
234
- if (!route)
235
- throw new Error("Missing value for -J.");
236
- sshRoute = parseSshRoute(route);
237
- continue;
238
- }
239
- if (arg === "--ssh-auth") {
240
- sshAuth = true;
241
- continue;
242
- }
243
- if (arg.startsWith("-"))
244
- throw new Error(`Unknown auth option: ${arg}`);
245
- if (target)
246
- throw new Error(`Unexpected auth argument: ${arg}`);
247
- target = arg;
248
- }
249
- if (!target)
250
- throw new Error("Missing remote service target.");
251
- if (sshAuth && !sshRoute)
252
- throw new Error("--ssh-auth requires -J <ssh-route>.");
253
- if (sshAuth && ownerToken)
254
- throw new Error("--ssh-auth and --token cannot be used together.");
255
- return { target, alias, ownerToken, sshRoute, sshAuth };
256
- }
257
- async function resolveAuthOwnerToken(ownerToken) {
258
- if (ownerToken)
259
- return ownerToken;
260
- if (!input.isTTY || !output.isTTY) {
261
- throw new Error("Missing owner token. Pass --token, use --ssh-auth with -J, or run in an interactive terminal.");
262
- }
263
- const result = await prompts.password({
264
- message: "Remote ForgeRelay owner token",
265
- validate: (value) => value?.trim() ? undefined : "Enter the remote owner token.",
266
- });
267
- if (prompts.isCancel(result))
268
- throw new Error("Remote authentication cancelled.");
269
- return String(result);
270
- }
271
- function localOwnerToken() {
272
- const token = process.env.FORGERELAY_OAUTH_OWNER_TOKEN
273
- ?? loadForgeRelayFiles().auth.ownerToken;
274
- if (!token)
275
- throw new Error("ForgeRelay owner token is not configured on this machine.");
276
- return token;
277
- }
278
- async function runAuthCommand(args) {
279
- const [subcommand, ...rest] = args;
280
- if (subcommand === "__owner-token") {
281
- if (rest.length > 0)
282
- throw new Error("Internal owner-token command does not accept arguments.");
283
- process.stdout.write(`${localOwnerToken()}\n`);
284
- return;
285
- }
286
- if (subcommand === "list") {
287
- if (rest.length > 0)
288
- throw new Error("forgerelay auth list does not accept additional arguments.");
289
- const remotes = loadForgeRelayFiles().auth.remotes ?? {};
290
- if (Object.keys(remotes).length === 0) {
291
- console.log("No remote ForgeRelay instances registered.");
292
- return;
293
- }
294
- for (const [alias, remote] of Object.entries(remotes).sort(([left], [right]) => left.localeCompare(right))) {
295
- console.log(`${alias}\t${remote.target}\t${remote.instanceId}`);
296
- }
297
- return;
298
- }
299
- if (subcommand === "rename") {
300
- const [fromAlias, toAlias, ...extra] = rest;
301
- if (!fromAlias || !toAlias || extra.length > 0) {
302
- throw new Error("Usage: forgerelay auth rename <old-alias> <new-alias>");
303
- }
304
- await renameForgeRelayRemote(fromAlias, toAlias);
305
- console.log(`Renamed remote ${fromAlias} to ${toAlias}.`);
306
- return;
307
- }
308
- if (subcommand === "remove") {
309
- const [alias, ...extra] = rest;
310
- if (!alias || extra.length > 0)
311
- throw new Error("Usage: forgerelay auth remove <alias>");
312
- await removeForgeRelayRemote(alias);
313
- console.log(`Removed remote ${alias}.`);
314
- return;
315
- }
316
- if (subcommand === "test") {
317
- const [alias, ...extra] = rest;
318
- if (!alias || extra.length > 0)
319
- throw new Error("Usage: forgerelay auth test <alias>");
320
- const files = loadForgeRelayFiles();
321
- const storedRemote = files.auth.remotes?.[alias];
322
- if (!storedRemote)
323
- throw new Error(`Unknown remote alias: ${alias}`);
324
- let remote = storedRemote;
325
- await withRemoteServiceEndpoint(remote.target, remote.sshRoute, async (endpoint) => {
326
- let refreshed = false;
327
- if (remote.accessTokenExpiresAt <= Math.floor(Date.now() / 1000)) {
328
- remote = await refreshRemoteAuthentication(remote, endpoint);
329
- await writeForgeRelayRemote(alias, remote);
330
- refreshed = true;
331
- }
332
- try {
333
- await verifyRemoteMcp(remote, endpoint);
334
- }
335
- catch (error) {
336
- if (refreshed || !isRemoteMcpUnauthorized(error))
337
- throw error;
338
- remote = await refreshRemoteAuthentication(remote, endpoint);
339
- await writeForgeRelayRemote(alias, remote);
340
- await verifyRemoteMcp(remote, endpoint);
341
- }
342
- });
343
- console.log(`${alias}\tok\t${remote.instanceId}`);
344
- return;
345
- }
346
- const parsed = parseAuthCommandArgs(args);
347
- const target = normalizeRemoteServiceTarget(parsed.target);
348
- const authenticated = await withRemoteServiceEndpoint(target, parsed.sshRoute, async (endpoint) => {
349
- const ownerToken = parsed.sshAuth
350
- ? await readRemoteOwnerToken(parsed.sshRoute ?? [])
351
- : await resolveAuthOwnerToken(parsed.ownerToken);
352
- return authenticateRemote(endpoint, ownerToken);
353
- });
354
- const remote = {
355
- ...authenticated,
356
- target,
357
- ...(parsed.sshRoute ? { sshRoute: parsed.sshRoute } : {}),
358
- };
359
- const files = loadForgeRelayFiles();
360
- const existingAlias = Object.entries(files.auth.remotes ?? {}).find(([, record]) => record.instanceId === remote.instanceId)?.[0];
361
- const defaultAlias = parsed.sshRoute
362
- ? defaultSshRouteAlias(parsed.sshRoute)
363
- : defaultRemoteAlias(remote.target);
364
- const alias = parsed.alias?.trim() || existingAlias || defaultAlias;
365
- if (!files.auth.instanceId) {
366
- await ensureForgeRelayInstanceId();
367
- }
368
- await writeForgeRelayRemote(alias, remote);
369
- console.log(`Authenticated remote ${alias} (${remote.instanceId}).`);
370
- }
371
251
  async function runDoctor() {
372
252
  const files = loadForgeRelayFiles();
373
253
  console.log(`Config dir: ${files.dir}`);
@@ -413,7 +293,11 @@ async function runDoctor() {
413
293
  }
414
294
  }
415
295
  async function runConfigCommand(args) {
416
- const [subcommand, key, ...rest] = args;
296
+ const [subcommand] = args;
297
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
298
+ console.log(renderGeneralConfigHelp());
299
+ return;
300
+ }
417
301
  if (subcommand === "migrate") {
418
302
  await runConfigMigration(args.slice(1));
419
303
  return;
@@ -422,65 +306,41 @@ async function runConfigCommand(args) {
422
306
  process.exitCode = await runConfigInspection(args);
423
307
  return;
424
308
  }
425
- const files = loadForgeRelayFiles();
426
309
  if (!subcommand || subcommand === "get") {
427
- console.log(JSON.stringify(files.config, null, 2));
310
+ await runGeneralConfigGet(subcommand ? args.slice(1) : []);
428
311
  return;
429
312
  }
430
- if (subcommand !== "set") {
431
- throw new Error(`Unknown config command: ${subcommand}`);
313
+ if (subcommand === "set") {
314
+ await runGeneralConfigSet(args.slice(1));
315
+ return;
432
316
  }
433
- if (key !== "publicBaseUrl") {
434
- throw new Error("Only `forgerelay config set publicBaseUrl <url[,url...]|null>` is supported right now.");
317
+ if (subcommand === "unset") {
318
+ await runGeneralConfigUnset(args.slice(1));
319
+ return;
435
320
  }
436
- const value = rest.join(" ").trim();
437
- if (!value)
438
- throw new Error("Missing publicBaseUrl value.");
439
- writeForgeRelayConfig({
440
- ...files.config,
441
- publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
442
- });
443
- console.log(`Updated ${files.configPath}`);
321
+ throw new Error(`Unknown config command: ${subcommand}`);
444
322
  }
445
323
  function printHelp() {
324
+ console.log(renderCliRootHelp());
325
+ }
326
+ function printConnectHelp() {
446
327
  console.log([
447
- "ForgeRelay",
328
+ "ForgeRelay connect",
448
329
  "",
449
330
  "Usage:",
450
- " forgerelay Run first-time setup if needed, then start the server",
451
- " forgerelay serve Start the server",
452
- " forgerelay serve --allow-elevated",
453
- " Explicitly allow this invocation to run with elevated/unknown OS privilege",
454
- " forgerelay init Run minimal first-time setup",
455
- " forgerelay init --advanced Configure common advanced runtime options",
456
- " forgerelay init --force Update setup-owned fields without migrating legacy config",
457
- " forgerelay doctor Show config, runtime, and native dependency status",
458
- " forgerelay config get Print persisted config",
459
- " forgerelay config set publicBaseUrl <url[,url...]|null>",
460
- " forgerelay config check [--global|--project <path>] [--json]",
461
- " forgerelay config sources [--global|--project <path>] [--json]",
462
- " forgerelay config explain <logical-path> [--global|--project <path>] [--json]",
463
- " forgerelay config migrate [--dry-run] [--global|--project <path>]",
464
- " forgerelay hooks list [--project <path>]",
465
- " forgerelay hooks check [--project <path>]",
466
- " forgerelay agents ls List subagent sessions",
467
- " forgerelay agents run <profile-or-provider-or-id> [--model <model>] <prompt>",
468
- " forgerelay agents show <id>",
469
- " forgerelay auth <target> [--alias <name>] [--token <owner-token>]",
470
- " forgerelay auth -J <ssh-route> <target> [--alias <name>] [--token <owner-token>|--ssh-auth]",
471
- " forgerelay auth list",
472
- " forgerelay auth test <alias>",
473
- " forgerelay auth rename <old-alias> <new-alias>",
474
- " forgerelay auth remove <alias>",
475
- " forgerelay mcp list [--project <path>|--global]",
476
- " forgerelay mcp test <server> [--project <path>|--global]",
477
- " forgerelay mcp auth <server> [--project <path>|--global]",
478
- " forgerelay mcp logout <server> [--project <path>|--global]",
479
- " forgerelay maintenance inspect [--json]",
480
- " forgerelay -v, --version Print the installed version",
331
+ " forgerelay connect relay ...",
332
+ " forgerelay connect mcp ...",
333
+ ].join("\n"));
334
+ }
335
+ function printSystemHelp() {
336
+ console.log([
337
+ "ForgeRelay system",
481
338
  "",
482
- "For temporary tunnels:",
483
- " FORGERELAY_PUBLIC_BASE_URL=https://example.trycloudflare.com/forgerelay/debug forgerelay serve",
339
+ "Usage:",
340
+ " forgerelay system doctor",
341
+ " forgerelay system status",
342
+ " forgerelay system inspect [--json]",
343
+ " forgerelay system prune [--json]",
484
344
  ].join("\n"));
485
345
  }
486
346
  async function runAgentsCommand(args) {
@@ -361,10 +361,10 @@ function isCredentialReauthorizationError(error) {
361
361
  ].includes(error.code);
362
362
  }
363
363
  function authRequired(server) {
364
- return new ExternalMcpOAuthError("auth_required", `External MCP ${server} requires OAuth authorization. Run: forgerelay mcp auth ${server}`);
364
+ return new ExternalMcpOAuthError("auth_required", `External MCP ${server} requires OAuth authorization. Run: forgerelay connect mcp auth ${server}`);
365
365
  }
366
366
  function reauthorizationRequired(server, reason) {
367
- return new ExternalMcpOAuthError("reauthorization_required", `${reason} Run: forgerelay mcp auth ${server}`);
367
+ return new ExternalMcpOAuthError("reauthorization_required", `${reason} Run: forgerelay connect mcp auth ${server}`);
368
368
  }
369
369
  function sameServerUrl(left, right) {
370
370
  try {
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod/v4";
2
+ import { redactSkillDiagnosticMessage } from "../../../workspaces/resources/skills.js";
2
3
  export function resultOutputSchema(extra = {}) {
3
4
  return {
4
5
  result: z
@@ -21,18 +22,9 @@ export const workspaceSkillDiagnosticOutputSchema = z.object({
21
22
  });
22
23
  export function redactSkillDiagnosticPaths(diagnostics) {
23
24
  return diagnostics.map((diagnostic) => {
24
- let message = diagnostic.message;
25
- const hiddenPaths = [
26
- diagnostic.path,
27
- diagnostic.collision?.winnerPath,
28
- diagnostic.collision?.loserPath,
29
- ].filter((path) => Boolean(path));
30
- for (const path of hiddenPaths) {
31
- message = message.split(path).join("<skill-path>");
32
- }
33
25
  return {
34
26
  type: diagnostic.type,
35
- message,
27
+ message: redactSkillDiagnosticMessage(diagnostic),
36
28
  ...(diagnostic.collision
37
29
  ? {
38
30
  collision: {
@@ -16,10 +16,14 @@ import { assertWorkspaceInstructionsLoadedBeforeSideEffect, auditToolResultWitho
16
16
  import { appendAutomaticMutationDiagnostics } from "./mutation-diagnostics.js";
17
17
  export function createOperationRuntime(options) {
18
18
  const { config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore, capabilityRegistry, codeIntelligence, hostScopeIdFor, } = options;
19
+ const refreshedWorkspace = async (workspaceId) => {
20
+ await workspaces.refreshContextSources(workspaceId);
21
+ return workspaces.getWorkspace(workspaceId);
22
+ };
19
23
  const coreOperations = createCoreOperationExecutor({
20
24
  read: async (input, context) => {
21
25
  const { workspaceId, ...readInput } = input;
22
- const workspace = workspaces.getWorkspace(workspaceId);
26
+ const workspace = await refreshedWorkspace(workspaceId);
23
27
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context), activityRequestFor(input, context), {
24
28
  signal: context.signal,
25
29
  tool: toolNames.read,
@@ -95,7 +99,7 @@ export function createOperationRuntime(options) {
95
99
  },
96
100
  write: async (input, context) => {
97
101
  const { workspaceId, ...writeInput } = input;
98
- const workspace = workspaces.getWorkspace(workspaceId);
102
+ const workspace = await refreshedWorkspace(workspaceId);
99
103
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context), activityRequestFor(input, context), {
100
104
  signal: context.signal,
101
105
  tool: toolNames.write,
@@ -156,7 +160,7 @@ export function createOperationRuntime(options) {
156
160
  },
157
161
  edit: async (input, context) => {
158
162
  const { workspaceId, ...editInput } = input;
159
- const workspace = workspaces.getWorkspace(workspaceId);
163
+ const workspace = await refreshedWorkspace(workspaceId);
160
164
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context), activityRequestFor(input, context), {
161
165
  signal: context.signal,
162
166
  tool: toolNames.edit,
@@ -218,7 +222,7 @@ export function createOperationRuntime(options) {
218
222
  },
219
223
  rename: async (input, context) => {
220
224
  const { workspaceId, path, newPath } = input;
221
- const workspace = workspaces.getWorkspace(workspaceId);
225
+ const workspace = await refreshedWorkspace(workspaceId);
222
226
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context), activityRequestFor(input, context), {
223
227
  signal: context.signal,
224
228
  tool: toolNames.rename,
@@ -277,7 +281,7 @@ export function createOperationRuntime(options) {
277
281
  },
278
282
  delete: async (input, context) => {
279
283
  const { workspaceId, path, recursive } = input;
280
- const workspace = workspaces.getWorkspace(workspaceId);
284
+ const workspace = await refreshedWorkspace(workspaceId);
281
285
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context), activityRequestFor(input, context), {
282
286
  signal: context.signal,
283
287
  tool: toolNames.delete,
@@ -336,7 +340,7 @@ export function createOperationRuntime(options) {
336
340
  },
337
341
  shellRun: async (input, context) => {
338
342
  const { workspaceId, command, surface, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, } = input;
339
- const workspace = workspaces.getWorkspace(workspaceId);
343
+ const workspace = await refreshedWorkspace(workspaceId);
340
344
  const activityRequest = surface === "exec_command"
341
345
  ? {
342
346
  workspaceId,
@@ -486,7 +490,7 @@ export function createOperationRuntime(options) {
486
490
  }
487
491
  }
488
492
  }
489
- const workspace = workspaces.getWorkspace(workspaceId);
493
+ const workspace = await refreshedWorkspace(workspaceId);
490
494
  let changedPaths = [];
491
495
  return runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context), toolNames.capability, activityRequestFor(capabilityActivityAuditRequest(input), context), (activityContext) => runToolWithHooks(hooks, {
492
496
  signal: context.signal,