@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.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -11
- 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 +11 -24
- 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 -2
- 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/setup-support.js +3 -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 +22 -6
- package/dist/runtime/config/external-mcp-config.js +4 -3
- package/dist/runtime/config/resolution/resolver.js +7 -4
- package/dist/runtime/config/user-config.js +9 -6
- package/dist/runtime/config/validation/paths.js +12 -0
- package/dist/runtime/config/validation/ports.js +9 -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 +59 -4
- 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
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,
|
|
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 {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
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
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
55
|
+
await runConfigRootCommand(args);
|
|
62
56
|
return;
|
|
63
|
-
case "
|
|
64
|
-
await
|
|
57
|
+
case "connect":
|
|
58
|
+
await runConnectCommand(args);
|
|
65
59
|
return;
|
|
66
|
-
case "
|
|
67
|
-
await
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
310
|
+
await runGeneralConfigGet(subcommand ? args.slice(1) : []);
|
|
428
311
|
return;
|
|
429
312
|
}
|
|
430
|
-
if (subcommand
|
|
431
|
-
|
|
313
|
+
if (subcommand === "set") {
|
|
314
|
+
await runGeneralConfigSet(args.slice(1));
|
|
315
|
+
return;
|
|
432
316
|
}
|
|
433
|
-
if (
|
|
434
|
-
|
|
317
|
+
if (subcommand === "unset") {
|
|
318
|
+
await runGeneralConfigUnset(args.slice(1));
|
|
319
|
+
return;
|
|
435
320
|
}
|
|
436
|
-
|
|
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
|
|
451
|
-
" forgerelay
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"
|
|
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
|
-
"
|
|
483
|
-
"
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|