@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,263 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
const MISSING_FINGERPRINT = "missing";
|
|
5
|
+
export class ConfigSourceRuntime {
|
|
6
|
+
units = new Map();
|
|
7
|
+
directories = new Map();
|
|
8
|
+
refreshFile(input) {
|
|
9
|
+
return this.refreshUnit({
|
|
10
|
+
...input,
|
|
11
|
+
path: resolve(input.path),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
refreshDirectory(input) {
|
|
15
|
+
const directory = resolve(input.directory);
|
|
16
|
+
const previousDirectory = this.directories.get(input.key);
|
|
17
|
+
let names;
|
|
18
|
+
try {
|
|
19
|
+
names = readdirSync(directory, { withFileTypes: true })
|
|
20
|
+
.filter((entry) => entry.isFile() && input.include(entry.name))
|
|
21
|
+
.map((entry) => entry.name)
|
|
22
|
+
.sort((left, right) => left.localeCompare(right));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isMissingDirectory(error)) {
|
|
26
|
+
this.clearDeletedDirectoryUnits(input.key, previousDirectory, directory, new Set());
|
|
27
|
+
this.directories.set(input.key, {
|
|
28
|
+
names: new Set(),
|
|
29
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
directory,
|
|
33
|
+
state: "missing",
|
|
34
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
35
|
+
effectiveFingerprint: fingerprintSourceUnits([]),
|
|
36
|
+
units: [],
|
|
37
|
+
diagnosticChanged: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const issue = {
|
|
41
|
+
code: "invalid_source",
|
|
42
|
+
message: "Configuration directory could not be read.",
|
|
43
|
+
};
|
|
44
|
+
const issueFingerprint = `directory-read-error:${errorCode(error)}`;
|
|
45
|
+
const diagnosticChanged = previousDirectory?.issueFingerprint !== issueFingerprint;
|
|
46
|
+
const units = this.previousDirectoryUnits(input.key, previousDirectory, directory, issue, issueFingerprint);
|
|
47
|
+
const observedFingerprint = fingerprintSourceUnits(units.map((unit) => ({ name: unit.name, fingerprint: unit.status.observedFingerprint })));
|
|
48
|
+
const effectiveFingerprint = fingerprintSourceUnits(units.flatMap((unit) => unit.status.effectiveFingerprint
|
|
49
|
+
? [{ name: unit.name, fingerprint: unit.status.effectiveFingerprint }]
|
|
50
|
+
: []));
|
|
51
|
+
this.directories.set(input.key, {
|
|
52
|
+
names: new Set(units.map((unit) => unit.name)),
|
|
53
|
+
observedFingerprint,
|
|
54
|
+
issueFingerprint,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
directory,
|
|
58
|
+
state: "invalid",
|
|
59
|
+
observedFingerprint,
|
|
60
|
+
effectiveFingerprint,
|
|
61
|
+
units,
|
|
62
|
+
issue,
|
|
63
|
+
diagnosticChanged,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const currentNames = new Set(names);
|
|
67
|
+
this.clearDeletedDirectoryUnits(input.key, previousDirectory, directory, currentNames);
|
|
68
|
+
const units = names.map((name) => {
|
|
69
|
+
const path = join(directory, name);
|
|
70
|
+
const result = this.refreshUnit({
|
|
71
|
+
key: directoryUnitKey(input.key, name),
|
|
72
|
+
path,
|
|
73
|
+
parse: (content) => input.parse(name, content, path),
|
|
74
|
+
validate: input.validate ? (value) => input.validate(value, name, path) : undefined,
|
|
75
|
+
parseIssue: input.parseIssue
|
|
76
|
+
? (error) => resolveDirectoryIssue(input.parseIssue, error, name, path, "Configuration source is invalid.")
|
|
77
|
+
: undefined,
|
|
78
|
+
readIssue: input.readIssue
|
|
79
|
+
? (error) => resolveDirectoryIssue(input.readIssue, error, name, path, "Configuration source could not be read.")
|
|
80
|
+
: undefined,
|
|
81
|
+
});
|
|
82
|
+
return { name, ...result };
|
|
83
|
+
});
|
|
84
|
+
const observedFingerprint = fingerprintSourceUnits(units.map((unit) => ({ name: unit.name, fingerprint: unit.status.observedFingerprint })));
|
|
85
|
+
const effectiveFingerprint = fingerprintSourceUnits(units.flatMap((unit) => unit.status.effectiveFingerprint
|
|
86
|
+
? [{ name: unit.name, fingerprint: unit.status.effectiveFingerprint }]
|
|
87
|
+
: []));
|
|
88
|
+
this.directories.set(input.key, {
|
|
89
|
+
names: currentNames,
|
|
90
|
+
observedFingerprint,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
directory,
|
|
94
|
+
state: units.some((unit) => unit.status.state === "invalid") ? "invalid" : "valid",
|
|
95
|
+
observedFingerprint,
|
|
96
|
+
effectiveFingerprint,
|
|
97
|
+
units,
|
|
98
|
+
diagnosticChanged: units.some((unit) => unit.status.diagnosticChanged),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
refreshUnit(input) {
|
|
102
|
+
const path = resolve(input.path);
|
|
103
|
+
const previous = this.units.get(input.key);
|
|
104
|
+
let content;
|
|
105
|
+
try {
|
|
106
|
+
content = readFileSync(path, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (isMissingFile(error)) {
|
|
110
|
+
const changed = previous?.observedFingerprint !== MISSING_FINGERPRINT || previous?.state !== "missing";
|
|
111
|
+
const missing = {
|
|
112
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
113
|
+
state: "missing",
|
|
114
|
+
};
|
|
115
|
+
this.units.set(input.key, missing);
|
|
116
|
+
return {
|
|
117
|
+
status: {
|
|
118
|
+
key: input.key,
|
|
119
|
+
path,
|
|
120
|
+
state: "missing",
|
|
121
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
122
|
+
usingLastKnownGood: false,
|
|
123
|
+
changed,
|
|
124
|
+
diagnosticChanged: false,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const fingerprint = `read-error:${errorCode(error)}`;
|
|
129
|
+
const issue = resolveIssue(input.readIssue, error, "Configuration source could not be read.");
|
|
130
|
+
return this.invalidUnit(input.key, path, fingerprint, previous, issue);
|
|
131
|
+
}
|
|
132
|
+
const fingerprint = contentFingerprint(content);
|
|
133
|
+
if (previous?.observedFingerprint === fingerprint) {
|
|
134
|
+
return storedResult(input.key, path, previous, false, false);
|
|
135
|
+
}
|
|
136
|
+
let value;
|
|
137
|
+
try {
|
|
138
|
+
value = input.parse(content);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const issue = resolveIssue(input.parseIssue, error, "Configuration source is invalid.");
|
|
142
|
+
return this.invalidUnit(input.key, path, fingerprint, previous, issue);
|
|
143
|
+
}
|
|
144
|
+
const validationIssue = input.validate?.(value);
|
|
145
|
+
if (validationIssue) {
|
|
146
|
+
return this.invalidUnit(input.key, path, fingerprint, previous, validationIssue);
|
|
147
|
+
}
|
|
148
|
+
const valid = {
|
|
149
|
+
observedFingerprint: fingerprint,
|
|
150
|
+
effectiveFingerprint: fingerprint,
|
|
151
|
+
state: "valid",
|
|
152
|
+
value,
|
|
153
|
+
};
|
|
154
|
+
this.units.set(input.key, valid);
|
|
155
|
+
return storedResult(input.key, path, valid, true, false);
|
|
156
|
+
}
|
|
157
|
+
invalidUnit(key, path, observedFingerprint, previous, issue) {
|
|
158
|
+
if (previous?.observedFingerprint === observedFingerprint && previous.state === "invalid") {
|
|
159
|
+
return storedResult(key, path, previous, false, false);
|
|
160
|
+
}
|
|
161
|
+
const keepLastKnownGood = previous?.effectiveFingerprint !== undefined && previous.value !== undefined;
|
|
162
|
+
const invalid = {
|
|
163
|
+
observedFingerprint,
|
|
164
|
+
...(keepLastKnownGood ? {
|
|
165
|
+
effectiveFingerprint: previous.effectiveFingerprint,
|
|
166
|
+
value: previous.value,
|
|
167
|
+
} : {}),
|
|
168
|
+
state: "invalid",
|
|
169
|
+
issue,
|
|
170
|
+
};
|
|
171
|
+
this.units.set(key, invalid);
|
|
172
|
+
return storedResult(key, path, invalid, true, true);
|
|
173
|
+
}
|
|
174
|
+
clearDeletedDirectoryUnits(directoryKey, previous, directory, currentNames) {
|
|
175
|
+
if (!previous)
|
|
176
|
+
return;
|
|
177
|
+
for (const name of previous.names) {
|
|
178
|
+
if (currentNames.has(name))
|
|
179
|
+
continue;
|
|
180
|
+
const key = directoryUnitKey(directoryKey, name);
|
|
181
|
+
this.units.set(key, {
|
|
182
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
183
|
+
state: "missing",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
previousDirectoryUnits(directoryKey, previous, directory, issue, observedFingerprint) {
|
|
188
|
+
if (!previous)
|
|
189
|
+
return [];
|
|
190
|
+
return [...previous.names].sort((left, right) => left.localeCompare(right)).map((name) => {
|
|
191
|
+
const key = directoryUnitKey(directoryKey, name);
|
|
192
|
+
const prior = this.units.get(key);
|
|
193
|
+
const invalid = {
|
|
194
|
+
observedFingerprint,
|
|
195
|
+
...(prior?.effectiveFingerprint && prior.value !== undefined ? {
|
|
196
|
+
effectiveFingerprint: prior.effectiveFingerprint,
|
|
197
|
+
value: prior.value,
|
|
198
|
+
} : {}),
|
|
199
|
+
state: "invalid",
|
|
200
|
+
issue,
|
|
201
|
+
};
|
|
202
|
+
const diagnosticChanged = prior?.observedFingerprint !== observedFingerprint || prior.state !== "invalid";
|
|
203
|
+
this.units.set(key, invalid);
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
...storedResult(key, join(directory, name), invalid, diagnosticChanged, diagnosticChanged),
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export function fingerprintSourceUnits(units) {
|
|
212
|
+
const sorted = [...units]
|
|
213
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
214
|
+
.map(({ name, fingerprint }) => [name, fingerprint]);
|
|
215
|
+
return createHash("sha256").update(JSON.stringify(sorted)).digest("base64url");
|
|
216
|
+
}
|
|
217
|
+
function contentFingerprint(content) {
|
|
218
|
+
return createHash("sha256").update(content).digest("base64url");
|
|
219
|
+
}
|
|
220
|
+
function directoryUnitKey(directoryKey, name) {
|
|
221
|
+
return `${directoryKey}\0${name}`;
|
|
222
|
+
}
|
|
223
|
+
function storedResult(key, path, stored, changed, diagnosticChanged) {
|
|
224
|
+
return {
|
|
225
|
+
...(stored.value !== undefined ? { value: stored.value } : {}),
|
|
226
|
+
status: {
|
|
227
|
+
key,
|
|
228
|
+
path,
|
|
229
|
+
state: stored.state,
|
|
230
|
+
observedFingerprint: stored.observedFingerprint,
|
|
231
|
+
...(stored.effectiveFingerprint ? { effectiveFingerprint: stored.effectiveFingerprint } : {}),
|
|
232
|
+
usingLastKnownGood: stored.state === "invalid" && stored.effectiveFingerprint !== undefined && stored.value !== undefined,
|
|
233
|
+
changed,
|
|
234
|
+
diagnosticChanged,
|
|
235
|
+
},
|
|
236
|
+
...(stored.issue ? { issue: stored.issue } : {}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function resolveIssue(configured, error, fallbackMessage) {
|
|
240
|
+
if (typeof configured === "function")
|
|
241
|
+
return configured(error);
|
|
242
|
+
return configured ?? { code: "invalid_source", message: fallbackMessage };
|
|
243
|
+
}
|
|
244
|
+
function resolveDirectoryIssue(configured, error, name, path, fallbackMessage) {
|
|
245
|
+
if (typeof configured === "function")
|
|
246
|
+
return configured(error, name, path);
|
|
247
|
+
return configured ?? { code: "invalid_source", message: fallbackMessage };
|
|
248
|
+
}
|
|
249
|
+
function isMissingFile(error) {
|
|
250
|
+
return errorCode(error) === "ENOENT";
|
|
251
|
+
}
|
|
252
|
+
function isMissingDirectory(error) {
|
|
253
|
+
const code = errorCode(error);
|
|
254
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
255
|
+
}
|
|
256
|
+
function errorCode(error) {
|
|
257
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
258
|
+
const code = error.code;
|
|
259
|
+
if (typeof code === "string" && code)
|
|
260
|
+
return code;
|
|
261
|
+
}
|
|
262
|
+
return "UNKNOWN";
|
|
263
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { rename as renameAsync, rm as rmAsync, writeFile as writeFileAsync, } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join, resolve } from "node:path";
|
|
5
6
|
import { withFileLock } from "../state/lock/file-lock.js";
|
|
6
7
|
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
7
|
-
import { mergeHookConfigs, parseHookFile, } from "../../mcp/hooks/hooks.js";
|
|
8
8
|
export function forgerelayConfigDir(env = process.env) {
|
|
9
9
|
const explicit = env.FORGERELAY_CONFIG_DIR;
|
|
10
10
|
if (explicit)
|
|
@@ -26,10 +26,7 @@ export function forgerelayHooksDir(env = process.env) {
|
|
|
26
26
|
export function forgerelaySkillsDir(env = process.env) {
|
|
27
27
|
return join(forgerelayConfigDir(env), "skills");
|
|
28
28
|
}
|
|
29
|
-
export function
|
|
30
|
-
return join(forgerelayConfigDir(env), "agents");
|
|
31
|
-
}
|
|
32
|
-
export function loadForgeRelayFiles(env = process.env) {
|
|
29
|
+
export function loadForgeRelayFiles(env = process.env, options = {}) {
|
|
33
30
|
const dir = forgerelayConfigDir(env);
|
|
34
31
|
const configPath = join(dir, "config.json");
|
|
35
32
|
const authPath = join(dir, "auth.json");
|
|
@@ -47,8 +44,7 @@ export function loadForgeRelayFiles(env = process.env) {
|
|
|
47
44
|
hooksExists,
|
|
48
45
|
config: configExists ? readJsonFile(configPath) : {},
|
|
49
46
|
auth: authExists ? readJsonFile(authPath) : {},
|
|
50
|
-
hooks: hooksExists ? readJsonFile(hooksPath) : {},
|
|
51
|
-
hookFiles: readHookFiles(join(dir, "hooks")),
|
|
47
|
+
hooks: options.readLegacyHooks && hooksExists ? readJsonFile(hooksPath) : {},
|
|
52
48
|
};
|
|
53
49
|
}
|
|
54
50
|
export function writeForgeRelayConfig(config, env = process.env) {
|
|
@@ -60,8 +56,8 @@ export function writeForgeRelayConfig(config, env = process.env) {
|
|
|
60
56
|
export async function writeForgeRelayAuth(auth, env = process.env) {
|
|
61
57
|
const filePath = forgerelayAuthPath(env);
|
|
62
58
|
mkdirSync(forgerelayConfigDir(env), { recursive: true });
|
|
63
|
-
return withFileLock(`${filePath}.lock`, () => {
|
|
64
|
-
|
|
59
|
+
return withFileLock(`${filePath}.lock`, async () => {
|
|
60
|
+
await writeAuthJsonFile(filePath, auth, 0o600);
|
|
65
61
|
return filePath;
|
|
66
62
|
});
|
|
67
63
|
}
|
|
@@ -141,25 +137,6 @@ export function resolveSubagentsFlag(config, env = process.env) {
|
|
|
141
137
|
return config.subagents;
|
|
142
138
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
143
139
|
}
|
|
144
|
-
function readHookFiles(directory) {
|
|
145
|
-
if (!existsSync(directory))
|
|
146
|
-
return {};
|
|
147
|
-
let hooks = {};
|
|
148
|
-
const entries = readdirSync(directory, { withFileTypes: true })
|
|
149
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
150
|
-
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
151
|
-
for (const entry of entries) {
|
|
152
|
-
const filePath = join(directory, entry.name);
|
|
153
|
-
try {
|
|
154
|
-
hooks = mergeHookConfigs(hooks, parseHookFile(readJsonFile(filePath), entry.name.slice(0, -5)));
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
158
|
-
throw new Error(`Unable to load hook file ${filePath}: ${reason}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return hooks;
|
|
162
|
-
}
|
|
163
140
|
function readJsonFile(filePath) {
|
|
164
141
|
try {
|
|
165
142
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
@@ -172,12 +149,42 @@ function readJsonFile(filePath) {
|
|
|
172
149
|
async function updateForgeRelayAuth(update, env) {
|
|
173
150
|
const filePath = forgerelayAuthPath(env);
|
|
174
151
|
mkdirSync(forgerelayConfigDir(env), { recursive: true });
|
|
175
|
-
return withFileLock(`${filePath}.lock`, () => {
|
|
152
|
+
return withFileLock(`${filePath}.lock`, async () => {
|
|
176
153
|
const auth = existsSync(filePath) ? readJsonFile(filePath) : {};
|
|
177
|
-
|
|
154
|
+
await writeAuthJsonFile(filePath, update(auth), 0o600);
|
|
178
155
|
return filePath;
|
|
179
156
|
});
|
|
180
157
|
}
|
|
158
|
+
async function writeAuthJsonFile(filePath, value, mode) {
|
|
159
|
+
const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
160
|
+
try {
|
|
161
|
+
await writeFileAsync(tempPath, JSON.stringify(value, null, 2) + "\n", { mode });
|
|
162
|
+
await replaceAuthFile(tempPath, filePath);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await rmAsync(tempPath, { force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function replaceAuthFile(tempPath, filePath) {
|
|
169
|
+
const maxAttempts = process.platform === "win32" ? 40 : 1;
|
|
170
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
171
|
+
try {
|
|
172
|
+
await renameAsync(tempPath, filePath);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const code = error.code;
|
|
177
|
+
const retryable = process.platform === "win32"
|
|
178
|
+
&& ["EACCES", "EBUSY", "EEXIST", "EPERM"].includes(code ?? "");
|
|
179
|
+
if (!retryable || attempt === maxAttempts)
|
|
180
|
+
throw error;
|
|
181
|
+
await delay(25);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function delay(ms) {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
}
|
|
181
188
|
function writeJsonFile(filePath, value, mode) {
|
|
182
189
|
const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
183
190
|
try {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const compatibilityAllowProjectExecutionTrustPolicy = {
|
|
2
|
+
async authorize(requirement) {
|
|
3
|
+
return {
|
|
4
|
+
policy: "compatibility-allow",
|
|
5
|
+
decision: "allow",
|
|
6
|
+
projectId: requirement.projectId,
|
|
7
|
+
executableConfigFingerprint: requirement.executableConfigFingerprint,
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export function projectExecutionRequirement(input) {
|
|
12
|
+
const entry = input.resolution.entries[input.entryKey];
|
|
13
|
+
if (!entry
|
|
14
|
+
|| entry.effective.source.scope !== "project"
|
|
15
|
+
|| entry.effective.executionEffect !== "process"
|
|
16
|
+
|| entry.tombstone === true) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const executionFingerprint = entry.effective.executionFingerprint;
|
|
20
|
+
if (!executionFingerprint) {
|
|
21
|
+
throw new Error(`Project execution metadata is missing a fingerprint for ${entry.logicalPath}.`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
projectId: input.projectId,
|
|
25
|
+
domain: input.resolution.domain,
|
|
26
|
+
logicalPath: entry.logicalPath,
|
|
27
|
+
source: {
|
|
28
|
+
id: entry.effective.source.id,
|
|
29
|
+
...(entry.effective.source.location ? { location: entry.effective.source.location } : {}),
|
|
30
|
+
},
|
|
31
|
+
display: { ...input.display },
|
|
32
|
+
executableConfigFingerprint: executionFingerprint,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -11,7 +11,6 @@ import { HostTurnStore } from "../../activity/history/host-turn-store.js";
|
|
|
11
11
|
import { ActivityLifecycle } from "../../activity/runtime/lifecycle.js";
|
|
12
12
|
import { ActivityQueryService } from "../../activity/history/query-service.js";
|
|
13
13
|
import { loadConfig } from "../config/config.js";
|
|
14
|
-
import { parseHookConfig } from "../../mcp/hooks/hooks.js";
|
|
15
14
|
import { CodeIntelligenceManager } from "../../lsp/runtime/manager.js";
|
|
16
15
|
import { ProcessManager } from "../../mcp/process/process-sessions.js";
|
|
17
16
|
import { createReviewCheckpointManager } from "../../workspaces/review/review-checkpoints.js";
|
|
@@ -30,15 +29,19 @@ export async function fixture(t, options = {}) {
|
|
|
30
29
|
const agentDir = join(root, "agent");
|
|
31
30
|
const stateDir = join(root, ".state");
|
|
32
31
|
const configDir = join(root, ".config");
|
|
33
|
-
await mkdir(join(project, ".forgerelay", "
|
|
32
|
+
await mkdir(join(project, ".forgerelay", "subagents"), { recursive: true });
|
|
34
33
|
await mkdir(configDir, { recursive: true });
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const userConfig = {
|
|
35
|
+
...(options.userConfig ?? {}),
|
|
36
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
37
|
+
};
|
|
38
|
+
if (Object.keys(userConfig).length > 0) {
|
|
39
|
+
await writeFile(join(configDir, "config.json"), JSON.stringify(userConfig, null, 2) + "\n");
|
|
37
40
|
}
|
|
38
41
|
await mkdir(agentDir, { recursive: true });
|
|
39
42
|
await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
|
|
40
43
|
await writeFile(join(project, "AGENTS.md"), "project instructions\n");
|
|
41
|
-
await writeFile(join(project, ".forgerelay", "
|
|
44
|
+
await writeFile(join(project, ".forgerelay", "subagents", "reviewer.md"), [
|
|
42
45
|
"---",
|
|
43
46
|
"name: reviewer",
|
|
44
47
|
"description: Reviews project changes.",
|
|
@@ -66,9 +69,7 @@ export async function fixture(t, options = {}) {
|
|
|
66
69
|
PORT: "1",
|
|
67
70
|
...options.env,
|
|
68
71
|
});
|
|
69
|
-
const config =
|
|
70
|
-
? { ...loadedConfig, hooks: parseHookConfig(options.hooks) }
|
|
71
|
-
: loadedConfig;
|
|
72
|
+
const config = loadedConfig;
|
|
72
73
|
const store = new SqliteWorkspaceStore(stateDir);
|
|
73
74
|
const workspaces = new WorkspaceRegistry(config, store);
|
|
74
75
|
const auditStore = new ActivityAuditStore(stateDir);
|
package/dist/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CapabilityError, createCapabilityRegistry } from "./mcp/server/core/cap
|
|
|
9
9
|
import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform } from "./mcp/artifacts/artifact-tools.js";
|
|
10
10
|
import { ArtifactError } from "./mcp/artifacts/artifact-error.js";
|
|
11
11
|
import { loadConfig } from "./runtime/config/config.js";
|
|
12
|
+
import { liveWorkspaceConfigPanelState } from "./runtime/config/inspection/live.js";
|
|
12
13
|
import { CodeIntelligenceError } from "./lsp/code-intelligence.js";
|
|
13
14
|
import { installManagedLanguageServers, installedManagedLanguageServers, supportedManagedLanguageServers, } from "./lsp/runtime/managed-language-servers.js";
|
|
14
15
|
import { HookRunner } from "./mcp/hooks/hooks.js";
|
|
@@ -130,7 +131,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
130
131
|
};
|
|
131
132
|
};
|
|
132
133
|
const toolDescriptions = buildToolDescriptions(config);
|
|
133
|
-
const hooks = new HookRunner(
|
|
134
|
+
const hooks = new HookRunner({}, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)), config.commandShellRuntime, config.configDir, config.configRuntime.sources);
|
|
134
135
|
const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
135
136
|
const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
|
|
136
137
|
const reviewChangesAvailable = config.widgets === "changes";
|
|
@@ -138,7 +139,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
138
139
|
let batchExecutor;
|
|
139
140
|
const batchExecuteAvailable = config.toolMode !== "codex";
|
|
140
141
|
const capabilityRegistry = createCapabilityRegistry({
|
|
141
|
-
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.
|
|
142
|
+
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, {}, config.configDir, config.configRuntime.sources),
|
|
142
143
|
...subagentMcp.registryDependencies,
|
|
143
144
|
externalMcp: externalMcpCapability,
|
|
144
145
|
workspaceRecovery: {
|
|
@@ -215,7 +216,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
215
216
|
}
|
|
216
217
|
if (input.operation === "managed.install") {
|
|
217
218
|
if (!config.allowAgentLanguageServerInstall) {
|
|
218
|
-
throw new CapabilityError("code.managed_install_disabled", "Agent-managed Language Server installation is disabled. Enable it explicitly with forgerelay init --
|
|
219
|
+
throw new CapabilityError("code.managed_install_disabled", "Agent-managed Language Server installation is disabled. Enable it explicitly with forgerelay init --advanced.");
|
|
219
220
|
}
|
|
220
221
|
const install = options.managedLanguageServerInstaller ?? installManagedLanguageServers;
|
|
221
222
|
const installed = await install(input.servers, config.configDir);
|
|
@@ -439,14 +440,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
439
440
|
},
|
|
440
441
|
});
|
|
441
442
|
};
|
|
442
|
-
const workspacePanelState = (workspaceId) => {
|
|
443
|
+
const workspacePanelState = async (workspaceId) => {
|
|
443
444
|
const remembered = workspacePanelStates.get(workspaceId);
|
|
444
445
|
if (remoteWorkspaces.has(workspaceId) || compositeWorkspaces.has(workspaceId)) {
|
|
445
446
|
return remembered;
|
|
446
447
|
}
|
|
447
448
|
try {
|
|
448
|
-
const
|
|
449
|
-
|
|
449
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
450
|
+
const live = liveWorkspacePanelState(workspace);
|
|
451
|
+
const presentation = remembered ? { ...live, ...remembered } : live;
|
|
452
|
+
const configuration = await liveWorkspaceConfigPanelState(config, workspace.root);
|
|
453
|
+
return configuration ? { ...presentation, configuration } : presentation;
|
|
450
454
|
}
|
|
451
455
|
catch {
|
|
452
456
|
return undefined;
|