@akira-tl/forgerelay 1.1.2 → 1.2.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 +38 -0
- package/README.md +14 -0
- package/dist/activity/runtime/mcp-query-tools.js +15 -5
- package/dist/cli/config/inspect.js +331 -0
- package/dist/cli/config/migrate.js +351 -0
- package/dist/cli/init/setup-config.js +41 -0
- package/dist/cli/init.js +128 -130
- package/dist/cli/maintenance.js +15 -5
- package/dist/cli/mcp/external-mcp.js +14 -10
- package/dist/cli/mcp/status.js +11 -6
- package/dist/cli.js +40 -7
- package/dist/lsp/language-server-config.js +66 -144
- package/dist/lsp/runtime/manager.js +35 -4
- package/dist/mcp/hooks/config.js +141 -0
- package/dist/mcp/hooks/external-mcp-transform.js +22 -13
- package/dist/mcp/hooks/hook-cli.js +62 -52
- package/dist/mcp/hooks/hooks.js +111 -85
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
- package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
- package/dist/mcp/server/core/capability-support.js +7 -0
- package/dist/runtime/config/config.js +142 -64
- package/dist/runtime/config/definition/catalog.js +12 -0
- package/dist/runtime/config/definition/definition.js +115 -0
- package/dist/runtime/config/definition/external-mcp.js +58 -0
- package/dist/runtime/config/definition/general-config.js +258 -0
- package/dist/runtime/config/definition/language-servers.js +116 -0
- package/dist/runtime/config/definition/schema.js +113 -0
- package/dist/runtime/config/definition/types.js +2 -0
- package/dist/runtime/config/external-mcp-auth-store.js +105 -29
- package/dist/runtime/config/external-mcp-config.js +39 -0
- package/dist/runtime/config/external-mcp-registry.js +284 -106
- package/dist/runtime/config/inspection/live.js +77 -0
- package/dist/runtime/config/resolution/general.js +93 -0
- package/dist/runtime/config/resolution/hooks.js +315 -0
- package/dist/runtime/config/resolution/language-servers.js +237 -0
- package/dist/runtime/config/resolution/project-sources.js +159 -0
- package/dist/runtime/config/resolution/resolver.js +485 -0
- package/dist/runtime/config/resolution/types.js +1 -0
- package/dist/runtime/config/runtime/config-runtime.js +74 -0
- package/dist/runtime/config/runtime/source-refresh.js +263 -0
- package/dist/runtime/config/user-config.js +38 -31
- package/dist/runtime/security/project-execution-trust.js +34 -0
- package/dist/runtime/testing/server-fixture.js +9 -8
- package/dist/server.js +10 -6
- package/dist/subagents/profiles.js +336 -53
- package/dist/subagents/sessions/execution.js +6 -35
- package/dist/subagents/sessions/manager.js +14 -2
- package/dist/ui/.vite/manifest.json +275 -275
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
- package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
- package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
- package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
- package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
- package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
- package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
- package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
- package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
- package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
- package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
- package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
- package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
- package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
- package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
- package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
- package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
- package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
- package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
- package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
- package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
- package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
- package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
- package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
- package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
- package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
- package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
- package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
- package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
- package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
- package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
- package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
- package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
- package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
- package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
- package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
- package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
- package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
- package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
- package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
- package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
- package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
- package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
- package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
- package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
- package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
- package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
- package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
- package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
- package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
- package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
- package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
- package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
- package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
- package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
- package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
- package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
- package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
- package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
- package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
- package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
- package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
- package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
- package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
- package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
- package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
- package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
- package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
- package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
- package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
- package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
- package/dist/workspaces/conversation-test-support.js +3 -11
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces/state/project-context.js +221 -0
- package/dist/workspaces.js +3 -1
- package/docs/chatgpt-coding-workflow.md +8 -3
- package/docs/configuration.md +99 -41
- package/docs/gotchas.md +24 -5
- package/docs/security.md +14 -3
- package/docs/setup.md +12 -7
- package/package.json +12 -3
- package/schemas/v1/config.project-local.schema.json +14 -0
- package/schemas/v1/config.project.schema.json +14 -0
- package/schemas/v1/config.user.schema.json +538 -0
- package/schemas/v1/hooks.project-local.schema.json +118 -0
- package/schemas/v1/hooks.project.schema.json +118 -0
- package/schemas/v1/hooks.user.schema.json +118 -0
- package/schemas/v1/language-servers.project-local.schema.json +230 -0
- package/schemas/v1/language-servers.project.schema.json +230 -0
- package/schemas/v1/language-servers.user.schema.json +230 -0
- package/schemas/v1/mcp.project-local.schema.json +143 -0
- package/schemas/v1/mcp.project.schema.json +143 -0
- package/schemas/v1/mcp.user.schema.json +143 -0
- package/scripts/ci/architecture.mjs +54 -4
- package/scripts/ci/cmd-acceptance.mjs +9 -9
- package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
- package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
- package/scripts/ci/gates/core-contract.mjs +27 -0
- package/scripts/ci/gates/platform-tests.mjs +38 -0
- package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
- package/scripts/ci/gates/test-shard.mjs +71 -0
- package/scripts/ci/powershell51-acceptance.mjs +9 -9
- package/scripts/ci/pwsh-acceptance.mjs +9 -9
- package/scripts/ci/verify.mjs +2 -0
- package/scripts/ci/windows-product-acceptance.mjs +9 -10
- package/scripts/config/schema-text.mjs +4 -0
- package/scripts/config/schema-text.test.mjs +18 -0
- package/scripts/config/schema.mjs +74 -0
- package/scripts/lsp/interop.mjs +4 -1
- package/scripts/release/pack.mjs +64 -4
- package/scripts/release/release-gate.test.mjs +93 -27
- package/scripts/release-parity.mjs +19 -2
- package/templates/instructions/cmd.md +12 -0
- package/templates/instructions/fish.md +11 -0
- package/templates/instructions/powershell.md +14 -0
- package/templates/instructions/pwsh.md +14 -0
- package/templates/instructions/zsh.md +11 -0
- package/dist/runtime/instructions/powershell-skill.js +0 -65
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { defineConfigDomain } from "./definition.js";
|
|
3
|
+
const USER_RUNTIME_SCOPES = ["runtime", "user", "built-in"];
|
|
4
|
+
const USER_SCOPES = ["user", "built-in"];
|
|
5
|
+
const USER_ONLY_SCOPES = ["user"];
|
|
6
|
+
const LEGACY_REMOVAL_VERSION = "1.4.0";
|
|
7
|
+
const commandShellSchema = z.object({
|
|
8
|
+
mode: z.enum(["follow-launcher", "pinned"]),
|
|
9
|
+
family: z.enum(["bash", "zsh", "fish", "sh", "pwsh", "powershell", "cmd"]),
|
|
10
|
+
executable: z.string().min(1),
|
|
11
|
+
}).strict();
|
|
12
|
+
const retentionSchema = z.object({
|
|
13
|
+
historyDays: z.number().int().min(1).max(36_500).optional(),
|
|
14
|
+
orphanedAdministrativeState: z.boolean().optional(),
|
|
15
|
+
}).strict();
|
|
16
|
+
export const generalConfigDefinition = defineConfigDomain({
|
|
17
|
+
domain: "config",
|
|
18
|
+
title: "ForgeRelay general configuration",
|
|
19
|
+
description: "Canonical ForgeRelay general configuration. Domain-specific MCP, Language Server, Hook, Subagent, and credential settings use separate sources.",
|
|
20
|
+
fields: {
|
|
21
|
+
host: field(z.string().min(1), "Local bind host.", {
|
|
22
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
23
|
+
builtIn: literal("127.0.0.1"),
|
|
24
|
+
reload: "restart-required",
|
|
25
|
+
runtimeOverride: runtimeEnv("HOST", (env) => env.HOST),
|
|
26
|
+
}),
|
|
27
|
+
port: field(z.number().int().min(1).max(65535), "Local listening port.", {
|
|
28
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
29
|
+
builtIn: literal(7676),
|
|
30
|
+
reload: "restart-required",
|
|
31
|
+
runtimeOverride: runtimeEnv("PORT", (env) => readIntegerEnv(env, "PORT", 1, 65535)),
|
|
32
|
+
}),
|
|
33
|
+
allowedRoots: field(z.array(z.string()), "Filesystem roots that Workspaces may open.", {
|
|
34
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
35
|
+
builtIn: computed("The process working directory when no roots are configured."),
|
|
36
|
+
reload: "restart-required",
|
|
37
|
+
runtimeOverride: runtimeEnv("FORGERELAY_ALLOWED_ROOTS", (env) => readListEnv(env, "FORGERELAY_ALLOWED_ROOTS")),
|
|
38
|
+
}),
|
|
39
|
+
publicBaseUrl: field(z.union([z.string().min(1), z.array(z.string().min(1)).min(1), z.null()]), "Canonical public base URL or ordered public base URL list.", {
|
|
40
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
41
|
+
builtIn: computed("A local HTTP URL derived from the effective bind host and port."),
|
|
42
|
+
reload: "restart-required",
|
|
43
|
+
runtimeOverride: runtimeEnv("FORGERELAY_PUBLIC_BASE_URL", (env) => readRequiredTextEnv(env, "FORGERELAY_PUBLIC_BASE_URL")),
|
|
44
|
+
}),
|
|
45
|
+
allowedHosts: field(z.array(z.string()), "Explicit Host-header allowlist.", {
|
|
46
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
47
|
+
builtIn: computed("Derived from loopback, bind host, and configured public base URL hostnames."),
|
|
48
|
+
reload: "restart-required",
|
|
49
|
+
runtimeOverride: runtimeEnv("FORGERELAY_ALLOWED_HOSTS", (env) => readListEnv(env, "FORGERELAY_ALLOWED_HOSTS")),
|
|
50
|
+
}),
|
|
51
|
+
trustedProxies: field(z.array(z.string()), "Trusted proxy IP addresses, CIDRs, or the internal loopback alias.", {
|
|
52
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
53
|
+
builtIn: computed("No trusted proxy unless the deployment shape safely derives loopback trust."),
|
|
54
|
+
reload: "restart-required",
|
|
55
|
+
runtimeOverride: runtimeEnv("FORGERELAY_TRUSTED_PROXIES", (env) => readListEnv(env, "FORGERELAY_TRUSTED_PROXIES")),
|
|
56
|
+
}),
|
|
57
|
+
stateDir: field(z.string().min(1), "ForgeRelay durable state directory.", {
|
|
58
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
59
|
+
builtIn: computed("~/.local/share/forgerelay"),
|
|
60
|
+
reload: "restart-required",
|
|
61
|
+
runtimeOverride: runtimeEnv("FORGERELAY_STATE_DIR", (env) => env.FORGERELAY_STATE_DIR),
|
|
62
|
+
}),
|
|
63
|
+
worktreeRoot: field(z.string().min(1), "Managed worktree directory.", {
|
|
64
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
65
|
+
builtIn: computed("~/.forgerelay/worktrees"),
|
|
66
|
+
reload: "restart-required",
|
|
67
|
+
runtimeOverride: runtimeEnv("FORGERELAY_WORKTREE_ROOT", (env) => env.FORGERELAY_WORKTREE_ROOT),
|
|
68
|
+
}),
|
|
69
|
+
artifactsEnabled: field(z.boolean(), "Enable ForgeRelay Artifact capability support.", {
|
|
70
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
71
|
+
builtIn: literal(false),
|
|
72
|
+
runtimeOverride: runtimeEnv("FORGERELAY_ARTIFACTS", (env) => readBooleanEnv(env, "FORGERELAY_ARTIFACTS")),
|
|
73
|
+
}),
|
|
74
|
+
artifactMaxFileBytes: field(z.number().int().min(1), "Maximum bytes accepted for one downloaded Artifact file.", {
|
|
75
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
76
|
+
builtIn: literal(100 * 1024 * 1024),
|
|
77
|
+
runtimeOverride: runtimeEnv("FORGERELAY_ARTIFACT_MAX_FILE_BYTES", (env) => readIntegerEnv(env, "FORGERELAY_ARTIFACT_MAX_FILE_BYTES", 1)),
|
|
78
|
+
}),
|
|
79
|
+
mediaMaxBytes: field(z.number().int().min(1), "Maximum aggregate decoded inline image bytes in one Host-facing result.", {
|
|
80
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
81
|
+
builtIn: literal(20 * 1024 * 1024),
|
|
82
|
+
runtimeOverride: runtimeEnv("FORGERELAY_MEDIA_MAX_BYTES", (env) => readIntegerEnv(env, "FORGERELAY_MEDIA_MAX_BYTES", 1)),
|
|
83
|
+
}),
|
|
84
|
+
taskReminderInterval: field(z.number().int().min(0), "Semantic-work interval for Workspace Task reminders; zero disables reminders.", {
|
|
85
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
86
|
+
builtIn: literal(30),
|
|
87
|
+
runtimeOverride: runtimeEnv("FORGERELAY_TASK_REMINDER_INTERVAL", (env) => readIntegerEnv(env, "FORGERELAY_TASK_REMINDER_INTERVAL", 0)),
|
|
88
|
+
}),
|
|
89
|
+
retention: field(retentionSchema, "Owner-authorized durable history and orphaned administrative-state retention policy.", {
|
|
90
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
91
|
+
builtIn: computed("Unlimited durable history and no orphaned administrative cleanup authorization."),
|
|
92
|
+
runtimeOverride: runtimeEnv(["FORGERELAY_RETENTION_HISTORY_DAYS", "FORGERELAY_RETENTION_ORPHANED_ADMIN"], readRetentionEnv),
|
|
93
|
+
}),
|
|
94
|
+
activityPanelExpanded: field(z.boolean(), "Whether the Activity Panel opens expanded by default.", {
|
|
95
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
96
|
+
builtIn: literal(false),
|
|
97
|
+
runtimeOverride: runtimeEnv("FORGERELAY_ACTIVITY_PANEL_EXPANDED", (env) => readBooleanEnv(env, "FORGERELAY_ACTIVITY_PANEL_EXPANDED")),
|
|
98
|
+
}),
|
|
99
|
+
workflowInstructions: field(z.union([z.string(), z.literal(false)]), "Replace ForgeRelay built-in Agent workflow instructions while retaining the capability contract.", {
|
|
100
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
101
|
+
builtIn: computed("ForgeRelay built-in workflow instructions."),
|
|
102
|
+
runtimeOverride: runtimeEnv("FORGERELAY_WORKFLOW_INSTRUCTIONS", (env) => env.FORGERELAY_WORKFLOW_INSTRUCTIONS),
|
|
103
|
+
}),
|
|
104
|
+
appendInstructions: field(z.string(), "Append operator workflow instructions after the selected ForgeRelay workflow policy.", {
|
|
105
|
+
scopes: ["runtime", ...USER_ONLY_SCOPES],
|
|
106
|
+
builtIn: none(),
|
|
107
|
+
runtimeOverride: runtimeEnv("FORGERELAY_APPEND_INSTRUCTIONS", (env) => env.FORGERELAY_APPEND_INSTRUCTIONS),
|
|
108
|
+
}),
|
|
109
|
+
agentDir: field(z.string().min(1), "Agent runtime directory used by supported integrations.", {
|
|
110
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
111
|
+
builtIn: computed("~/.codex"),
|
|
112
|
+
reload: "restart-required",
|
|
113
|
+
runtimeOverride: runtimeEnv("FORGERELAY_AGENT_DIR", (env) => env.FORGERELAY_AGENT_DIR),
|
|
114
|
+
}),
|
|
115
|
+
systemInstructionsPath: field(z.string().min(1), "Global Agent instruction file consumed by ForgeRelay.", {
|
|
116
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
117
|
+
builtIn: computed("~/.agents/AGENTS.md"),
|
|
118
|
+
runtimeOverride: runtimeEnv("FORGERELAY_SYSTEM_INSTRUCTIONS_PATH", (env) => readNonEmptyPathEnv(env, "FORGERELAY_SYSTEM_INSTRUCTIONS_PATH")),
|
|
119
|
+
}),
|
|
120
|
+
commandShell: field(commandShellSchema, "Recorded command-shell preference for ForgeRelay command and Hook execution.", {
|
|
121
|
+
scopes: USER_SCOPES,
|
|
122
|
+
builtIn: computed("Detected launcher shell with the recorded compatibility fallback used when needed."),
|
|
123
|
+
reload: "restart-required",
|
|
124
|
+
}),
|
|
125
|
+
shellInstructions: field(z.boolean(), "Enable ForgeRelay-owned runtime shell instructions.", {
|
|
126
|
+
scopes: USER_SCOPES,
|
|
127
|
+
builtIn: literal(false),
|
|
128
|
+
}),
|
|
129
|
+
subagents: field(z.boolean(), "Enable Subagent Session support.", {
|
|
130
|
+
scopes: USER_RUNTIME_SCOPES,
|
|
131
|
+
builtIn: literal(false),
|
|
132
|
+
runtimeOverride: runtimeEnv("FORGERELAY_SUBAGENTS", (env) => readBooleanEnv(env, "FORGERELAY_SUBAGENTS")),
|
|
133
|
+
}),
|
|
134
|
+
allowAgentLanguageServerInstall: field(z.boolean(), "Allow Agents to install ForgeRelay-managed Language Servers.", {
|
|
135
|
+
scopes: USER_SCOPES,
|
|
136
|
+
builtIn: literal(false),
|
|
137
|
+
}),
|
|
138
|
+
languageServers: field(z.unknown(), "Deprecated inline Language Server configuration retained for v1.2 compatibility.", {
|
|
139
|
+
scopes: USER_ONLY_SCOPES,
|
|
140
|
+
builtIn: none(),
|
|
141
|
+
deprecation: legacyDeprecation("language-servers.json"),
|
|
142
|
+
}),
|
|
143
|
+
mcpServers: field(z.unknown(), "Deprecated inline External MCP configuration retained for v1.2 compatibility.", {
|
|
144
|
+
scopes: USER_ONLY_SCOPES,
|
|
145
|
+
builtIn: none(),
|
|
146
|
+
deprecation: legacyDeprecation("mcp.json"),
|
|
147
|
+
}),
|
|
148
|
+
hooks: field(z.unknown(), "Deprecated inline Hook configuration retained for v1.2 compatibility.", {
|
|
149
|
+
scopes: USER_ONLY_SCOPES,
|
|
150
|
+
builtIn: none(),
|
|
151
|
+
deprecation: legacyDeprecation("hooks/"),
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
function field(schema, description, options) {
|
|
156
|
+
return {
|
|
157
|
+
schema,
|
|
158
|
+
description,
|
|
159
|
+
legalScopes: options.scopes,
|
|
160
|
+
merge: "replace",
|
|
161
|
+
reload: options.reload ?? "hot",
|
|
162
|
+
sensitivity: options.sensitivity ?? "public",
|
|
163
|
+
interpolation: options.interpolation ?? "none",
|
|
164
|
+
executionEffect: options.executionEffect ?? "none",
|
|
165
|
+
builtIn: options.builtIn,
|
|
166
|
+
...(options.runtimeOverride ? { runtimeOverride: options.runtimeOverride } : {}),
|
|
167
|
+
...(options.deprecation ? { deprecation: options.deprecation } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function literal(value) {
|
|
171
|
+
return { kind: "literal", value };
|
|
172
|
+
}
|
|
173
|
+
function computed(description) {
|
|
174
|
+
return { kind: "computed", description };
|
|
175
|
+
}
|
|
176
|
+
function none() {
|
|
177
|
+
return { kind: "none" };
|
|
178
|
+
}
|
|
179
|
+
function runtimeEnv(env, readEnv) {
|
|
180
|
+
return { env, readEnv };
|
|
181
|
+
}
|
|
182
|
+
function readBooleanEnv(env, name) {
|
|
183
|
+
const value = env[name];
|
|
184
|
+
if (value === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
function readIntegerEnv(env, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
189
|
+
const value = env[name];
|
|
190
|
+
if (value === undefined || value === "")
|
|
191
|
+
return undefined;
|
|
192
|
+
const parsed = Number(value);
|
|
193
|
+
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
194
|
+
const range = maximum === Number.MAX_SAFE_INTEGER
|
|
195
|
+
? `an integer greater than or equal to ${minimum}`
|
|
196
|
+
: `an integer from ${minimum} to ${maximum}`;
|
|
197
|
+
throw new Error(`Invalid ${name}: expected ${range}.`);
|
|
198
|
+
}
|
|
199
|
+
return parsed;
|
|
200
|
+
}
|
|
201
|
+
function readListEnv(env, name) {
|
|
202
|
+
const value = env[name];
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
return undefined;
|
|
205
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
function readRequiredTextEnv(env, name) {
|
|
208
|
+
const value = env[name];
|
|
209
|
+
if (value === undefined)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (!value.split(",").some((entry) => entry.trim().length > 0)) {
|
|
212
|
+
throw new Error(`${name} must contain at least one public base URL.`);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
function readNonEmptyPathEnv(env, name) {
|
|
217
|
+
const value = env[name];
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
if (!value.trim())
|
|
221
|
+
throw new Error(`${name} must be one non-empty path`);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function readRetentionEnv(env) {
|
|
225
|
+
const rawHistoryDays = env.FORGERELAY_RETENTION_HISTORY_DAYS;
|
|
226
|
+
let historyDays;
|
|
227
|
+
if (rawHistoryDays !== undefined && rawHistoryDays !== "") {
|
|
228
|
+
const parsed = Number(rawHistoryDays);
|
|
229
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 36_500) {
|
|
230
|
+
throw new Error("Retention historyDays must be an integer between 1 and 36500.");
|
|
231
|
+
}
|
|
232
|
+
historyDays = parsed;
|
|
233
|
+
}
|
|
234
|
+
const rawOrphaned = env.FORGERELAY_RETENTION_ORPHANED_ADMIN;
|
|
235
|
+
let orphanedAdministrativeState;
|
|
236
|
+
if (rawOrphaned !== undefined && rawOrphaned !== "") {
|
|
237
|
+
const normalized = rawOrphaned.trim().toLowerCase();
|
|
238
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
239
|
+
orphanedAdministrativeState = true;
|
|
240
|
+
else if (["0", "false", "no", "off"].includes(normalized))
|
|
241
|
+
orphanedAdministrativeState = false;
|
|
242
|
+
else
|
|
243
|
+
throw new Error("Retention orphanedAdministrativeState must be a boolean.");
|
|
244
|
+
}
|
|
245
|
+
if (historyDays === undefined && orphanedAdministrativeState === undefined)
|
|
246
|
+
return undefined;
|
|
247
|
+
return {
|
|
248
|
+
...(historyDays === undefined ? {} : { historyDays }),
|
|
249
|
+
...(orphanedAdministrativeState === undefined ? {} : { orphanedAdministrativeState }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function legacyDeprecation(replacement) {
|
|
253
|
+
return {
|
|
254
|
+
since: "1.2.0",
|
|
255
|
+
removeIn: LEGACY_REMOVAL_VERSION,
|
|
256
|
+
replacement,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { defineConfigDomain } from "./definition.js";
|
|
3
|
+
const LANGUAGE_SERVER_SCOPES = ["project-local", "project", "user", "built-in"];
|
|
4
|
+
const ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5
|
+
export const languageServerDefinitionSchema = z.object({
|
|
6
|
+
command: z.string().min(1).optional(),
|
|
7
|
+
args: z.array(z.string()).optional(),
|
|
8
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
9
|
+
languages: z.array(z.string().min(1)).min(1).optional(),
|
|
10
|
+
extensions: z.array(z.string().regex(/^\./)).min(1).optional(),
|
|
11
|
+
languageIdByExtension: z.record(z.string().regex(/^\./), z.string().min(1)).optional(),
|
|
12
|
+
projectMarkers: z.array(z.string().min(1)).optional(),
|
|
13
|
+
disabled: z.literal(false).optional(),
|
|
14
|
+
}).strict();
|
|
15
|
+
export const languageServerDisabledSchema = z.object({ disabled: z.literal(true) }).strict();
|
|
16
|
+
export const languageServerServersSchema = z.record(z.string().min(1), z.union([languageServerDisabledSchema, languageServerDefinitionSchema]));
|
|
17
|
+
export const BUILTIN_LANGUAGE_SERVER_DEFINITIONS = {
|
|
18
|
+
typescript: {
|
|
19
|
+
args: ["--stdio"],
|
|
20
|
+
languages: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
|
21
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
22
|
+
languageIdByExtension: {
|
|
23
|
+
".ts": "typescript",
|
|
24
|
+
".tsx": "typescriptreact",
|
|
25
|
+
".js": "javascript",
|
|
26
|
+
".jsx": "javascriptreact",
|
|
27
|
+
".mjs": "javascript",
|
|
28
|
+
".cjs": "javascript",
|
|
29
|
+
},
|
|
30
|
+
projectMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
|
|
31
|
+
},
|
|
32
|
+
pyright: {
|
|
33
|
+
args: ["--stdio"],
|
|
34
|
+
languages: ["python"],
|
|
35
|
+
extensions: [".py", ".pyi"],
|
|
36
|
+
languageIdByExtension: { ".py": "python", ".pyi": "python" },
|
|
37
|
+
projectMarkers: ["pyrightconfig.json", "pyproject.toml", "setup.cfg", "setup.py"],
|
|
38
|
+
},
|
|
39
|
+
"rust-analyzer": {
|
|
40
|
+
languages: ["rust"],
|
|
41
|
+
extensions: [".rs"],
|
|
42
|
+
languageIdByExtension: { ".rs": "rust" },
|
|
43
|
+
projectMarkers: ["Cargo.toml"],
|
|
44
|
+
},
|
|
45
|
+
gopls: {
|
|
46
|
+
languages: ["go"],
|
|
47
|
+
extensions: [".go"],
|
|
48
|
+
languageIdByExtension: { ".go": "go" },
|
|
49
|
+
projectMarkers: ["go.work", "go.mod"],
|
|
50
|
+
},
|
|
51
|
+
clangd: {
|
|
52
|
+
languages: ["c", "cpp", "objective-c", "objective-cpp"],
|
|
53
|
+
extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".m", ".mm"],
|
|
54
|
+
languageIdByExtension: {
|
|
55
|
+
".c": "c",
|
|
56
|
+
".cc": "cpp",
|
|
57
|
+
".cpp": "cpp",
|
|
58
|
+
".cxx": "cpp",
|
|
59
|
+
".h": "cpp",
|
|
60
|
+
".hh": "cpp",
|
|
61
|
+
".hpp": "cpp",
|
|
62
|
+
".hxx": "cpp",
|
|
63
|
+
".m": "objective-c",
|
|
64
|
+
".mm": "objective-cpp",
|
|
65
|
+
},
|
|
66
|
+
projectMarkers: ["compile_commands.json", "compile_flags.txt", ".clangd"],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const languageServersConfigDefinition = defineConfigDomain({
|
|
70
|
+
domain: "language-servers",
|
|
71
|
+
title: "ForgeRelay Language Server configuration",
|
|
72
|
+
description: "Language Server definitions used by Code Intelligence.",
|
|
73
|
+
fileShape: { kind: "keyed-root", field: "servers" },
|
|
74
|
+
fields: {
|
|
75
|
+
servers: {
|
|
76
|
+
schema: languageServerServersSchema,
|
|
77
|
+
description: "Language Server definitions keyed by stable server name.",
|
|
78
|
+
required: true,
|
|
79
|
+
legalScopes: LANGUAGE_SERVER_SCOPES,
|
|
80
|
+
merge: "keyed",
|
|
81
|
+
reload: "hot",
|
|
82
|
+
sensitivity: "sensitive",
|
|
83
|
+
interpolation: "env",
|
|
84
|
+
interpolateValue: interpolateLanguageServerEnvironment,
|
|
85
|
+
builtIn: { kind: "literal", value: BUILTIN_LANGUAGE_SERVER_DEFINITIONS },
|
|
86
|
+
executionEffect: (value) => isRecord(value) && value.disabled === true ? "none" : "process",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
function interpolateLanguageServerEnvironment(value, environment) {
|
|
91
|
+
return Object.fromEntries(Object.entries(value).map(([name, entry]) => {
|
|
92
|
+
if (!("env" in entry) || !isRecord(entry.env))
|
|
93
|
+
return [name, entry];
|
|
94
|
+
return [name, {
|
|
95
|
+
...entry,
|
|
96
|
+
env: Object.fromEntries(Object.entries(entry.env).map(([key, raw]) => [
|
|
97
|
+
key,
|
|
98
|
+
typeof raw === "string" ? interpolateString(raw, environment) : raw,
|
|
99
|
+
])),
|
|
100
|
+
}];
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
function interpolateString(value, environment) {
|
|
104
|
+
return value.replace(ENV_REFERENCE, (_match, name) => {
|
|
105
|
+
const resolved = environment[name];
|
|
106
|
+
if (resolved !== undefined)
|
|
107
|
+
return resolved;
|
|
108
|
+
const error = new Error(`Required environment variable ${name} is not available.`);
|
|
109
|
+
error.code = "missing_environment";
|
|
110
|
+
error.variable = name;
|
|
111
|
+
throw error;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function isRecord(value) {
|
|
115
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { configSourceSchema } from "./definition.js";
|
|
3
|
+
import { CONFIG_FILE_SCOPES, CONFIG_SCHEMA_CONTRACT_MAJOR, } from "./types.js";
|
|
4
|
+
const DRAFT_7_SCHEMA = "http://json-schema.org/draft-07/schema#";
|
|
5
|
+
const SCHEMA_RAW_BASE = "https://raw.githubusercontent.com/Akira-TL/forgerelay/main";
|
|
6
|
+
export function configSchemaRelativePath(definition, scope) {
|
|
7
|
+
return `schemas/v${CONFIG_SCHEMA_CONTRACT_MAJOR}/${definition.domain}.${scope}.schema.json`;
|
|
8
|
+
}
|
|
9
|
+
export function configSchemaId(definition, scope) {
|
|
10
|
+
return `${SCHEMA_RAW_BASE}/${configSchemaRelativePath(definition, scope)}`;
|
|
11
|
+
}
|
|
12
|
+
export function generateConfigJsonSchema(definition, scope) {
|
|
13
|
+
if (definition.schemaOutput === "none") {
|
|
14
|
+
throw new Error(`Config domain ${definition.domain} does not expose a JSON file schema.`);
|
|
15
|
+
}
|
|
16
|
+
if (definition.fileShape?.kind === "keyed-root") {
|
|
17
|
+
return generateKeyedRootConfigJsonSchema(definition, scope);
|
|
18
|
+
}
|
|
19
|
+
if (definition.fileShape?.kind === "keyed-entry") {
|
|
20
|
+
return generateKeyedEntryConfigJsonSchema(definition, scope);
|
|
21
|
+
}
|
|
22
|
+
const schema = z.toJSONSchema(configSourceSchema(definition, scope), {
|
|
23
|
+
target: "draft-7",
|
|
24
|
+
});
|
|
25
|
+
applySchemaIdentity(schema, definition, scope);
|
|
26
|
+
const properties = asObject(schema.properties);
|
|
27
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
28
|
+
if (!field.legalScopes.includes(scope))
|
|
29
|
+
continue;
|
|
30
|
+
const property = asObject(properties[name]);
|
|
31
|
+
applyFieldMetadata(property, field);
|
|
32
|
+
properties[name] = property;
|
|
33
|
+
}
|
|
34
|
+
schema.properties = properties;
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
|
37
|
+
function generateKeyedRootConfigJsonSchema(definition, scope) {
|
|
38
|
+
const fileShape = definition.fileShape;
|
|
39
|
+
if (!fileShape || fileShape.kind !== "keyed-root") {
|
|
40
|
+
throw new Error(`Config domain ${definition.domain} is not keyed-root.`);
|
|
41
|
+
}
|
|
42
|
+
const field = definition.fields[fileShape.field];
|
|
43
|
+
if (!field || !field.legalScopes.includes(scope)) {
|
|
44
|
+
throw new Error(`Config domain ${definition.domain} does not expose keyed-root field ${fileShape.field} at ${scope} scope.`);
|
|
45
|
+
}
|
|
46
|
+
const schema = z.toJSONSchema(field.schema, { target: "draft-7" });
|
|
47
|
+
applySchemaIdentity(schema, definition, scope);
|
|
48
|
+
const properties = asObject(schema.properties);
|
|
49
|
+
properties.$schema = {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Editor-only JSON Schema URL. Ignored by ForgeRelay resolution.",
|
|
52
|
+
};
|
|
53
|
+
schema.properties = properties;
|
|
54
|
+
applyFieldMetadata(schema, field);
|
|
55
|
+
return schema;
|
|
56
|
+
}
|
|
57
|
+
function generateKeyedEntryConfigJsonSchema(definition, scope) {
|
|
58
|
+
const fileShape = definition.fileShape;
|
|
59
|
+
if (!fileShape || fileShape.kind !== "keyed-entry") {
|
|
60
|
+
throw new Error(`Config domain ${definition.domain} is not keyed-entry.`);
|
|
61
|
+
}
|
|
62
|
+
const field = definition.fields[fileShape.field];
|
|
63
|
+
if (!field || !field.legalScopes.includes(scope)) {
|
|
64
|
+
throw new Error(`Config domain ${definition.domain} does not expose keyed-entry field ${fileShape.field} at ${scope} scope.`);
|
|
65
|
+
}
|
|
66
|
+
const schema = z.toJSONSchema(fileShape.fileSchema, { target: "draft-7" });
|
|
67
|
+
applySchemaIdentity(schema, definition, scope);
|
|
68
|
+
applyFieldMetadata(schema, field);
|
|
69
|
+
return schema;
|
|
70
|
+
}
|
|
71
|
+
function applySchemaIdentity(schema, definition, scope) {
|
|
72
|
+
schema.$schema = DRAFT_7_SCHEMA;
|
|
73
|
+
schema.$id = configSchemaId(definition, scope);
|
|
74
|
+
schema.title = `${definition.title} (${scope})`;
|
|
75
|
+
schema.description = definition.description;
|
|
76
|
+
}
|
|
77
|
+
function applyFieldMetadata(target, field) {
|
|
78
|
+
if (field.builtIn.kind === "literal")
|
|
79
|
+
target.default = field.builtIn.value;
|
|
80
|
+
target["x-forgerelay-scopes"] = [...field.legalScopes];
|
|
81
|
+
target["x-forgerelay-merge"] = field.merge;
|
|
82
|
+
target["x-forgerelay-reload"] = field.reload;
|
|
83
|
+
target["x-forgerelay-sensitivity"] = field.sensitivity;
|
|
84
|
+
target["x-forgerelay-interpolation"] = field.interpolation;
|
|
85
|
+
target["x-forgerelay-execution-effect"] =
|
|
86
|
+
typeof field.executionEffect === "function" ? "dynamic" : field.executionEffect;
|
|
87
|
+
if (field.runtimeOverride) {
|
|
88
|
+
target["x-forgerelay-runtime-override"] = {
|
|
89
|
+
...(field.runtimeOverride.cli ? { cli: field.runtimeOverride.cli } : {}),
|
|
90
|
+
...(field.runtimeOverride.env ? { env: field.runtimeOverride.env } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (field.builtIn.kind === "computed") {
|
|
94
|
+
target["x-forgerelay-computed-default"] = field.builtIn.description;
|
|
95
|
+
}
|
|
96
|
+
if (field.deprecation) {
|
|
97
|
+
target.deprecated = true;
|
|
98
|
+
target["x-forgerelay-deprecation"] = field.deprecation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function generateConfigSchemaFiles(definitions) {
|
|
102
|
+
return definitions.flatMap((definition) => definition.schemaOutput === "none"
|
|
103
|
+
? []
|
|
104
|
+
: CONFIG_FILE_SCOPES.map((scope) => ({
|
|
105
|
+
relativePath: configSchemaRelativePath(definition, scope),
|
|
106
|
+
schema: generateConfigJsonSchema(definition, scope),
|
|
107
|
+
})));
|
|
108
|
+
}
|
|
109
|
+
function asObject(value) {
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
111
|
+
? value
|
|
112
|
+
: {};
|
|
113
|
+
}
|