@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
@@ -5,6 +5,80 @@
5
5
  "$schema": {
6
6
  "description": "Editor-only JSON Schema URL. Ignored by ForgeRelay resolution.",
7
7
  "type": "string"
8
+ },
9
+ "systemInstructionsPath": {
10
+ "description": "Selected system-level Agent instruction file consumed by ForgeRelay.",
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "default": "~/.agents/AGENTS.md",
14
+ "x-forgerelay-scopes": [
15
+ "runtime",
16
+ "project-local",
17
+ "project",
18
+ "user",
19
+ "built-in"
20
+ ],
21
+ "x-forgerelay-merge": "replace",
22
+ "x-forgerelay-reload": "hot",
23
+ "x-forgerelay-sensitivity": "public",
24
+ "x-forgerelay-interpolation": "none",
25
+ "x-forgerelay-execution-effect": "none",
26
+ "x-forgerelay-runtime-override": {
27
+ "env": "FORGERELAY_SYSTEM_INSTRUCTIONS_PATH"
28
+ }
29
+ },
30
+ "instructionNames": {
31
+ "description": "Project instruction basenames discovered hierarchically within a Workspace.",
32
+ "type": "array",
33
+ "items": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "default": [
38
+ "AGENTS.md"
39
+ ],
40
+ "x-forgerelay-scopes": [
41
+ "runtime",
42
+ "project-local",
43
+ "project",
44
+ "user",
45
+ "built-in"
46
+ ],
47
+ "x-forgerelay-merge": "replace",
48
+ "x-forgerelay-reload": "hot",
49
+ "x-forgerelay-sensitivity": "public",
50
+ "x-forgerelay-interpolation": "none",
51
+ "x-forgerelay-execution-effect": "none",
52
+ "x-forgerelay-runtime-override": {
53
+ "env": "FORGERELAY_INSTRUCTION_NAMES"
54
+ }
55
+ },
56
+ "skillPaths": {
57
+ "description": "Ordered Agent Skill source directories; explicit higher-precedence lists replace lower-precedence lists.",
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string",
61
+ "minLength": 1
62
+ },
63
+ "default": [
64
+ "~/.agents/skills",
65
+ "./.agents/skills"
66
+ ],
67
+ "x-forgerelay-scopes": [
68
+ "runtime",
69
+ "project-local",
70
+ "project",
71
+ "user",
72
+ "built-in"
73
+ ],
74
+ "x-forgerelay-merge": "replace",
75
+ "x-forgerelay-reload": "hot",
76
+ "x-forgerelay-sensitivity": "public",
77
+ "x-forgerelay-interpolation": "none",
78
+ "x-forgerelay-execution-effect": "none",
79
+ "x-forgerelay-runtime-override": {
80
+ "env": "FORGERELAY_SKILL_PATHS"
81
+ }
8
82
  }
9
83
  },
10
84
  "additionalProperties": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "port": {
29
- "description": "Local listening port.",
29
+ "description": "Local listening port (1-65535).",
30
30
  "type": "integer",
31
31
  "minimum": 1,
32
32
  "maximum": 65535,
@@ -370,11 +370,14 @@
370
370
  "x-forgerelay-computed-default": "~/.codex"
371
371
  },
372
372
  "systemInstructionsPath": {
373
- "description": "Global Agent instruction file consumed by ForgeRelay.",
373
+ "description": "Selected system-level Agent instruction file consumed by ForgeRelay.",
374
374
  "type": "string",
375
375
  "minLength": 1,
376
+ "default": "~/.agents/AGENTS.md",
376
377
  "x-forgerelay-scopes": [
377
378
  "runtime",
379
+ "project-local",
380
+ "project",
378
381
  "user",
379
382
  "built-in"
380
383
  ],
@@ -385,8 +388,60 @@
385
388
  "x-forgerelay-execution-effect": "none",
386
389
  "x-forgerelay-runtime-override": {
387
390
  "env": "FORGERELAY_SYSTEM_INSTRUCTIONS_PATH"
391
+ }
392
+ },
393
+ "instructionNames": {
394
+ "description": "Project instruction basenames discovered hierarchically within a Workspace.",
395
+ "type": "array",
396
+ "items": {
397
+ "type": "string",
398
+ "minLength": 1
399
+ },
400
+ "default": [
401
+ "AGENTS.md"
402
+ ],
403
+ "x-forgerelay-scopes": [
404
+ "runtime",
405
+ "project-local",
406
+ "project",
407
+ "user",
408
+ "built-in"
409
+ ],
410
+ "x-forgerelay-merge": "replace",
411
+ "x-forgerelay-reload": "hot",
412
+ "x-forgerelay-sensitivity": "public",
413
+ "x-forgerelay-interpolation": "none",
414
+ "x-forgerelay-execution-effect": "none",
415
+ "x-forgerelay-runtime-override": {
416
+ "env": "FORGERELAY_INSTRUCTION_NAMES"
417
+ }
418
+ },
419
+ "skillPaths": {
420
+ "description": "Ordered Agent Skill source directories; explicit higher-precedence lists replace lower-precedence lists.",
421
+ "type": "array",
422
+ "items": {
423
+ "type": "string",
424
+ "minLength": 1
388
425
  },
389
- "x-forgerelay-computed-default": "~/.agents/AGENTS.md"
426
+ "default": [
427
+ "~/.agents/skills",
428
+ "./.agents/skills"
429
+ ],
430
+ "x-forgerelay-scopes": [
431
+ "runtime",
432
+ "project-local",
433
+ "project",
434
+ "user",
435
+ "built-in"
436
+ ],
437
+ "x-forgerelay-merge": "replace",
438
+ "x-forgerelay-reload": "hot",
439
+ "x-forgerelay-sensitivity": "public",
440
+ "x-forgerelay-interpolation": "none",
441
+ "x-forgerelay-execution-effect": "none",
442
+ "x-forgerelay-runtime-override": {
443
+ "env": "FORGERELAY_SKILL_PATHS"
444
+ }
390
445
  },
391
446
  "commandShell": {
392
447
  "description": "Recorded command-shell preference for ForgeRelay command and Hook execution.",
@@ -431,7 +486,7 @@
431
486
  "x-forgerelay-sensitivity": "public",
432
487
  "x-forgerelay-interpolation": "none",
433
488
  "x-forgerelay-execution-effect": "none",
434
- "x-forgerelay-computed-default": "Detected launcher shell with the recorded compatibility fallback used when needed."
489
+ "x-forgerelay-computed-default": "Platform compatibility default unless an explicit shell preference is recorded."
435
490
  },
436
491
  "shellInstructions": {
437
492
  "description": "Enable ForgeRelay-owned runtime shell instructions.",
@@ -87,25 +87,30 @@ async function acceptFreshInit({ installedCli, root, home }) {
87
87
  });
88
88
 
89
89
  let output = "";
90
- let rootsAnswered = false;
91
- let modeAnswered = false;
90
+ const promptsToAccept = [
91
+ "Where are your projects located?",
92
+ "Which system instruction file should ForgeRelay load?",
93
+ "Which project instruction filenames should ForgeRelay discover?",
94
+ "Which Skill directories should ForgeRelay scan?",
95
+ "How should clients reach this ForgeRelay instance?",
96
+ ];
97
+ const answered = new Set();
92
98
  child.onData((data) => {
93
99
  output += data;
94
- if (!rootsAnswered && output.includes("Where are your projects located?")) {
95
- rootsAnswered = true;
96
- child.write("\r");
97
- return;
98
- }
99
- if (rootsAnswered && !modeAnswered && output.includes("How should clients reach this ForgeRelay instance?")) {
100
- modeAnswered = true;
101
- child.write("\r");
100
+ for (const prompt of promptsToAccept) {
101
+ if (!answered.has(prompt) && output.includes(prompt)) {
102
+ answered.add(prompt);
103
+ child.write("\r");
104
+ break;
105
+ }
102
106
  }
103
107
  });
104
108
 
105
109
  const exitCode = await waitForPtyExit(child, 20_000);
106
110
  assert.equal(exitCode, 0, scrubTerminal(output));
107
- assert.equal(rootsAnswered, true, "packaged init did not ask for project roots");
108
- assert.equal(modeAnswered, true, "packaged init did not ask for connection mode");
111
+ for (const prompt of promptsToAccept) {
112
+ assert.equal(answered.has(prompt), true, `packaged init did not ask: ${prompt}`);
113
+ }
109
114
  for (const unexpected of [
110
115
  "Which local port should ForgeRelay use?",
111
116
  "Which command shell should Agent commands and Hooks use?",
@@ -40,7 +40,23 @@ export function interactiveDebugUrls(configDir) {
40
40
  : 7677;
41
41
  const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
42
42
  const baseUrl = `http://${displayHost}:${port}`;
43
- return { baseUrl, mcpUrl: `${baseUrl}/mcp` };
43
+ const publicBaseUrl = Array.isArray(config.publicBaseUrl) ? config.publicBaseUrl[0] : config.publicBaseUrl;
44
+ const routeBasePath = publicBasePath(publicBaseUrl);
45
+ return {
46
+ baseUrl,
47
+ healthUrl: `${baseUrl}${routeBasePath}/healthz`,
48
+ mcpUrl: `${baseUrl}/mcp`,
49
+ };
50
+ }
51
+
52
+ function publicBasePath(value) {
53
+ if (typeof value !== "string" || !value.trim()) return "";
54
+ try {
55
+ const pathname = new URL(value).pathname.replace(/\/+$/, "");
56
+ return pathname === "/" ? "" : pathname;
57
+ } catch {
58
+ return "";
59
+ }
44
60
  }
45
61
 
46
62
  function interactiveDebugOwnerToken(configDir) {
@@ -66,7 +82,7 @@ export function createInteractiveDebugEnvironment({
66
82
  } = {}) {
67
83
  const configDir = interactiveDebugConfigDir({ env, home });
68
84
  const ownerToken = interactiveDebugOwnerToken(configDir);
69
- const { baseUrl, mcpUrl } = interactiveDebugUrls(configDir);
85
+ const { baseUrl, healthUrl, mcpUrl } = interactiveDebugUrls(configDir);
70
86
  const productConfigDir = env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR
71
87
  ? resolve(env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.startsWith("~/")
72
88
  ? join(home, env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.slice(2))
@@ -99,7 +115,7 @@ export function createInteractiveDebugEnvironment({
99
115
  debugEnv.FORGERELAY_SKILLS = productSkillsEnabled;
100
116
  }
101
117
 
102
- return { ownerToken, configDir, baseUrl, mcpUrl, env: debugEnv };
118
+ return { ownerToken, configDir, baseUrl, healthUrl, mcpUrl, env: debugEnv };
103
119
  }
104
120
 
105
121
  export function createDebugEnvironment({
@@ -52,9 +52,11 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
52
52
  assert.equal(result.configDir, configDir);
53
53
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, configDir);
54
54
  assert.equal(result.baseUrl, "http://127.0.0.1:7677");
55
+ assert.equal(result.healthUrl, "http://127.0.0.1:7677/forgerelay/debug/healthz");
55
56
  assert.equal(result.mcpUrl, "http://127.0.0.1:7677/mcp");
56
57
  assert.deepEqual(interactiveDebugUrls(configDir), {
57
58
  baseUrl: "http://127.0.0.1:7677",
59
+ healthUrl: "http://127.0.0.1:7677/forgerelay/debug/healthz",
58
60
  mcpUrl: "http://127.0.0.1:7677/mcp",
59
61
  });
60
62
  assert.equal(result.env.HOST, undefined);
@@ -100,6 +102,7 @@ test("interactive debug config directory may be explicitly relocated without fal
100
102
  assert.equal(result.ownerToken, "custom-debug-password-123456");
101
103
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, customDir);
102
104
  assert.equal(result.baseUrl, "http://127.0.0.1:6768");
105
+ assert.equal(result.healthUrl, "http://127.0.0.1:6768/healthz");
103
106
  assert.equal(result.mcpUrl, "http://127.0.0.1:6768/mcp");
104
107
  assert.equal(result.env.FORGERELAY_SKILL_PATHS, join(productConfigDir, "skills"));
105
108
  });
@@ -1,11 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInteractiveDebugEnvironment, repoRoot } from "./runtime.mjs";
3
3
 
4
- const { configDir, baseUrl, mcpUrl, env } = createInteractiveDebugEnvironment();
4
+ const { configDir, healthUrl, mcpUrl, env } = createInteractiveDebugEnvironment();
5
5
 
6
6
  console.error("[forgerelay:debug] local debug server");
7
7
  console.error(`[forgerelay:debug] debug config: ${configDir}/config.json`);
8
- console.error(`[forgerelay:debug] health: ${baseUrl}/healthz`);
8
+ console.error(`[forgerelay:debug] health: ${healthUrl}`);
9
9
  console.error(`[forgerelay:debug] MCP: ${mcpUrl}`);
10
10
  console.error(`[forgerelay:debug] Owner password loaded from ${configDir}/auth.json`);
11
11
  console.error("[forgerelay:debug] all runtime state is under .forgerelay-debug/");