@akira-tl/forgerelay 0.8.8 → 0.8.10
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 +10 -18
- package/capabilities/code-intelligence/GUIDE.md +7 -3
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/capabilities/subagents/GUIDE.md +9 -0
- package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
- package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
- package/dist/activity/history/bash-output-store.js +250 -0
- package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
- package/dist/activity/{query-service.js → history/query-service.js} +13 -7
- package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
- package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
- package/dist/activity/storage/history.js +12 -0
- package/dist/activity/storage/paths.js +56 -0
- package/dist/activity/storage/segmented-log.js +149 -0
- package/dist/cli/init.js +180 -0
- package/dist/cli/setup-support.js +181 -0
- package/dist/cli.js +23 -216
- package/dist/lsp/code-intelligence.js +18 -4
- package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
- package/dist/lsp/runtime/managed-language-servers.js +172 -0
- package/dist/lsp/runtime/manager.js +37 -2
- package/dist/lsp/runtime/process-launch.js +3 -0
- package/dist/lsp/test-support/server-fixture.js +14 -11
- package/dist/mcp/filesystem/filesystem-tools.js +245 -0
- package/dist/mcp/hooks/command-runner.js +79 -0
- package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
- package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
- package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
- package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
- package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
- package/dist/mcp/panel/app.js +143 -0
- package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
- package/dist/{process-platform.js → mcp/process/process-platform.js} +21 -9
- package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
- package/dist/mcp/process/runtime.js +257 -0
- package/dist/mcp/process/tools.js +346 -0
- package/dist/mcp/server/core/activity-support.js +70 -0
- package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
- package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +9 -4
- package/dist/mcp/server/core/capability-support.js +90 -0
- package/dist/mcp/server/core/schemas.js +216 -0
- package/dist/mcp/server/core/tool-support.js +210 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
- package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -0
- package/dist/mcp/server/transport/http-server.js +301 -0
- package/dist/mcp/server/transport/http-support.js +43 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +242 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/{config.js → runtime/config/config.js} +4 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
- package/dist/{db → runtime/state/db}/client.js +1 -1
- package/dist/{db → runtime/state/db}/migrations.js +34 -3
- package/dist/runtime/state/lock/file-lock.js +130 -0
- package/dist/runtime/testing/server-fixture.js +190 -0
- package/dist/server.js +133 -4189
- package/dist/subagents/providers/adapters/claude.js +86 -52
- package/dist/subagents/providers/adapters/codex.js +103 -39
- package/dist/subagents/providers/adapters/opencode.js +114 -84
- package/dist/subagents/providers/adapters/pi.js +59 -8
- package/dist/subagents/providers/availability.js +15 -30
- package/dist/subagents/providers/runtime/external-command.js +56 -0
- package/dist/subagents/sessions/capability.js +1 -1
- package/dist/subagents/sessions/execution.js +1 -1
- package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
- package/dist/subagents/sessions/store.js +1 -1
- package/dist/ui/.vite/manifest.json +342 -410
- package/dist/ui/activity-panel-app.html +2 -4
- package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
- package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
- package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
- package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
- package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
- package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
- package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
- package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
- package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
- package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
- package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
- package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
- package/dist/ui/assets/css-C1tjBtVN.js +1 -0
- package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
- package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
- package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
- package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
- package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
- package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
- package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
- package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
- package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
- package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
- package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
- package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
- package/dist/ui/assets/html-By4dNnM4.js +1 -0
- package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
- package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
- package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
- package/dist/ui/assets/java-DYRe2lWG.js +1 -0
- package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
- package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
- package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
- package/dist/ui/assets/json-dbltgiiA.js +1 -0
- package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
- package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
- package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
- package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
- package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
- package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
- package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
- package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
- package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
- package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
- package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
- package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
- package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
- package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
- package/dist/ui/assets/r-CcezTbLq.js +1 -0
- package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
- package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
- package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
- package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
- package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
- package/dist/ui/assets/scss-BQb-cp01.js +1 -0
- package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
- package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
- package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
- package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
- package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
- package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
- package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
- package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
- package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
- package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
- package/dist/ui/assets/tsx-CajzROyy.js +1 -0
- package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
- package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
- package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
- package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
- package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
- package/dist/ui/assets/xml-BI86-WbH.js +1 -0
- package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
- package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
- package/dist/workspaces/bootstrap.js +72 -0
- package/dist/workspaces/context.js +337 -0
- package/dist/workspaces/conversation-test-support.js +86 -0
- package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
- package/dist/workspaces/inventory.js +131 -0
- package/dist/workspaces/paths.js +55 -0
- package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
- package/dist/workspaces/relay/result-support.js +111 -0
- package/dist/workspaces/relay/tests/test-support.js +203 -0
- package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
- package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
- package/dist/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/resource-monitor.js +377 -0
- package/dist/workspaces/resources/skills.js +283 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +460 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +77 -993
- package/docs/chatgpt-coding-workflow.md +7 -14
- package/docs/configuration.md +26 -34
- package/docs/gotchas.md +0 -30
- package/docs/roadmap.md +6 -10
- package/docs/security.md +1 -5
- package/docs/setup.md +0 -11
- package/package.json +6 -9
- package/scripts/ci/architecture.mjs +109 -0
- package/scripts/ci/verify.mjs +5 -0
- package/scripts/debug/accept/bootstrap.mjs +326 -0
- package/scripts/debug/accept/harness.mjs +76 -0
- package/scripts/debug/accept/support.mjs +432 -0
- package/scripts/debug/accept.mjs +43 -806
- package/scripts/debug/relay-accept/support.mjs +135 -0
- package/scripts/debug/relay-accept.mjs +26 -115
- package/scripts/debug/traffic/support.mjs +167 -0
- package/scripts/debug/traffic/traffic-audit.mjs +4 -161
- package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
- package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
- package/scripts/release/publish.mjs +12 -9
- package/scripts/release/release-gate.test.mjs +3 -1
- package/scripts/release/release-version.test.mjs +13 -1
- package/scripts/release-parity.mjs +1 -1
- package/scripts/release-proof.mjs +15 -0
- package/scripts/release-proof.test.mjs +27 -1
- package/scripts/release-version.mjs +16 -1
- package/scripts/wiki/sync.mjs +21 -10
- package/dist/activity/bash-output-store.js +0 -127
- package/dist/pi-tools.js +0 -110
- package/dist/skills.js +0 -127
- package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
- package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
- package/dist/ui/assets/c-IBzBpxAO.js +0 -1
- package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
- package/dist/ui/assets/css-DEfus9J5.js +0 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
- package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
- package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
- package/dist/ui/assets/html-D-C6YZk4.js +0 -1
- package/dist/ui/assets/java-ppHvycj5.js +0 -1
- package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
- package/dist/ui/assets/json-Cw91r8h1.js +0 -1
- package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
- package/dist/ui/assets/r-DfFga4si.js +0 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
- package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
- package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
- package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
- package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
- package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
- package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
- package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
- package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
- package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
- package/dist/ui/workspace-app.html +0 -18
- package/dist/ui/workspace-lifecycle-app.html +0 -18
- package/docs/agent-profile-schema.md +0 -114
- package/docs/artifact-exchange.md +0 -65
- package/docs/versioning.md +0 -205
- package/skills/subagent-delegation/SKILL.md +0 -132
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
- /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
- /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
- /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
- /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
- /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
- /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
- /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
- /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
- /package/dist/{oauth → mcp/oauth}/router.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
- /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
- /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
- /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
- /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
- /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
- /package/dist/{db → runtime/state/db}/schema.js +0 -0
- /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
- /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
- /package/dist/{git.js → workspaces/git/git.js} +0 -0
- /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
- /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
- /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
- /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { realpath } from "node:fs/promises";
|
|
2
3
|
import { resolve, sep } from "node:path";
|
|
3
4
|
import { CodeIntelligenceError, LanguageService, languageServiceKey, } from "../code-intelligence.js";
|
|
4
5
|
import { LanguageServerConfigurationError, resolveLanguageProject, } from "../language-server-config.js";
|
|
6
|
+
import { managedLanguageServerIdForCommand, managedLanguageServerRuntimeIdentity, managedTypeScriptTsserverPath, withManagedLanguageServerPath, } from "./managed-language-servers.js";
|
|
5
7
|
const LANGUAGE_SERVICE_IDLE_MS = 10 * 60 * 1_000;
|
|
6
8
|
const LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS = 60 * 1_000;
|
|
7
9
|
const MAX_LANGUAGE_SERVICES = 16;
|
|
@@ -50,11 +52,12 @@ export class CodeIntelligenceManager {
|
|
|
50
52
|
try {
|
|
51
53
|
canonicalWorkspaceRoot = await realpath(resolve(workspaceRoot));
|
|
52
54
|
this.assertWorkspaceRootAvailable(canonicalWorkspaceRoot);
|
|
53
|
-
project = await resolveLanguageProject({
|
|
55
|
+
project = withManagedLanguageServerRuntime(await resolveLanguageProject({
|
|
54
56
|
workspaceRoot: canonicalWorkspaceRoot,
|
|
55
57
|
sourcePath: input.path,
|
|
56
58
|
globalConfig: this.config.languageServers,
|
|
57
|
-
|
|
59
|
+
env: withManagedLanguageServerPath(process.env, this.config.configDir),
|
|
60
|
+
}), this.config.configDir);
|
|
58
61
|
}
|
|
59
62
|
catch (error) {
|
|
60
63
|
if (error instanceof LanguageServerConfigurationError) {
|
|
@@ -319,6 +322,38 @@ export class CodeIntelligenceManager {
|
|
|
319
322
|
await candidate[1].shutdown();
|
|
320
323
|
}
|
|
321
324
|
}
|
|
325
|
+
function withManagedLanguageServerRuntime(project, configDir) {
|
|
326
|
+
if (!configDir)
|
|
327
|
+
return project;
|
|
328
|
+
const managedId = managedLanguageServerIdForCommand(configDir, project.definition.command);
|
|
329
|
+
if (!managedId)
|
|
330
|
+
return project;
|
|
331
|
+
const runtimeIdentity = managedLanguageServerRuntimeIdentity(configDir, managedId);
|
|
332
|
+
if (!runtimeIdentity)
|
|
333
|
+
return project;
|
|
334
|
+
let initializationOptions = project.definition.initializationOptions;
|
|
335
|
+
if (managedId === "typescript") {
|
|
336
|
+
const tsserverPath = managedTypeScriptTsserverPath(configDir);
|
|
337
|
+
if (!tsserverPath)
|
|
338
|
+
return project;
|
|
339
|
+
initializationOptions = { tsserver: { path: tsserverPath } };
|
|
340
|
+
}
|
|
341
|
+
const fingerprint = createHash("sha256")
|
|
342
|
+
.update(project.definition.fingerprint)
|
|
343
|
+
.update("\0")
|
|
344
|
+
.update(runtimeIdentity)
|
|
345
|
+
.update("\0")
|
|
346
|
+
.update(JSON.stringify(initializationOptions ?? null))
|
|
347
|
+
.digest("hex");
|
|
348
|
+
return {
|
|
349
|
+
...project,
|
|
350
|
+
definition: {
|
|
351
|
+
...project.definition,
|
|
352
|
+
...(initializationOptions === undefined ? {} : { initializationOptions }),
|
|
353
|
+
fingerprint,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
322
357
|
function identityBelongsToWorkspaceRoot(identity, workspaceRoot) {
|
|
323
358
|
try {
|
|
324
359
|
const parsed = JSON.parse(identity);
|
|
@@ -4,16 +4,16 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
-
import { ActivityAuditStore } from "../../activity/audit-store.js";
|
|
8
|
-
import { BashOutputStore } from "../../activity/bash-output-store.js";
|
|
9
|
-
import { HostTurnStore } from "../../activity/host-turn-store.js";
|
|
10
|
-
import { ActivityLifecycle } from "../../activity/lifecycle.js";
|
|
11
|
-
import { ActivityQueryService } from "../../activity/query-service.js";
|
|
12
|
-
import { loadConfig } from "../../config.js";
|
|
13
|
-
import { createReviewCheckpointManager } from "../../review-checkpoints.js";
|
|
14
|
-
import { ProcessManager } from "../../process-sessions.js";
|
|
7
|
+
import { ActivityAuditStore } from "../../activity/history/audit-store.js";
|
|
8
|
+
import { BashOutputStore } from "../../activity/history/bash-output-store.js";
|
|
9
|
+
import { HostTurnStore } from "../../activity/history/host-turn-store.js";
|
|
10
|
+
import { ActivityLifecycle } from "../../activity/runtime/lifecycle.js";
|
|
11
|
+
import { ActivityQueryService } from "../../activity/history/query-service.js";
|
|
12
|
+
import { loadConfig } from "../../runtime/config/config.js";
|
|
13
|
+
import { createReviewCheckpointManager } from "../../workspaces/review/review-checkpoints.js";
|
|
14
|
+
import { ProcessManager } from "../../mcp/process/process-sessions.js";
|
|
15
15
|
import { createMcpServer } from "../../server.js";
|
|
16
|
-
import { SqliteWorkspaceStore } from "../../workspace-store.js";
|
|
16
|
+
import { SqliteWorkspaceStore } from "../../workspaces/state/workspace-store.js";
|
|
17
17
|
import { WorkspaceRegistry } from "../../workspaces.js";
|
|
18
18
|
import { CodeIntelligenceManager } from "../runtime/manager.js";
|
|
19
19
|
export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
@@ -35,6 +35,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
35
35
|
FORGERELAY_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
|
|
36
36
|
PORT: "1",
|
|
37
37
|
});
|
|
38
|
+
config.allowAgentLanguageServerInstall = options.allowAgentLanguageServerInstall === true;
|
|
38
39
|
const store = new SqliteWorkspaceStore(stateDir);
|
|
39
40
|
const workspaces = new WorkspaceRegistry(config, store);
|
|
40
41
|
const auditStore = new ActivityAuditStore(stateDir);
|
|
@@ -46,7 +47,9 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
46
47
|
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
47
48
|
});
|
|
48
49
|
const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
|
|
49
|
-
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries
|
|
50
|
+
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options.managedLanguageServerInstaller
|
|
51
|
+
? { managedLanguageServerInstaller: options.managedLanguageServerInstaller }
|
|
52
|
+
: {});
|
|
50
53
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
51
54
|
const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
|
|
52
55
|
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
|
@@ -73,7 +76,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
73
76
|
retryDelay: 100,
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
|
-
return { client, project, codeIntelligence, close };
|
|
79
|
+
return { client, project, config, codeIntelligence, close };
|
|
77
80
|
}
|
|
78
81
|
export async function callOpen(client, path, conversationScopeId) {
|
|
79
82
|
return client.callTool({
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, realpath, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { createPatch } from "diff";
|
|
5
|
+
import { resolveCanonicalAllowedPath } from "./roots.js";
|
|
6
|
+
const DEFAULT_MAX_LINES = 2_000;
|
|
7
|
+
const DEFAULT_MAX_BYTES = 50 * 1_024;
|
|
8
|
+
const mutationQueues = new Map();
|
|
9
|
+
function textContent(text) {
|
|
10
|
+
return [{ type: "text", text }];
|
|
11
|
+
}
|
|
12
|
+
function failed(error) {
|
|
13
|
+
return {
|
|
14
|
+
content: textContent(error instanceof Error ? error.message : String(error)),
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function readFileTool(input, context) {
|
|
19
|
+
try {
|
|
20
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.readRoots ?? [context.root]);
|
|
21
|
+
await access(path, constants.R_OK);
|
|
22
|
+
const buffer = await readFile(path);
|
|
23
|
+
const mimeType = supportedImageMimeType(buffer);
|
|
24
|
+
if (mimeType) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
28
|
+
{ type: "image", data: buffer.toString("base64"), mimeType },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { content: textContent(readText(buffer.toString("utf8"), input)) };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return failed(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function writeFileTool(input, context) {
|
|
39
|
+
try {
|
|
40
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
41
|
+
await withMutationQueue(path, async () => {
|
|
42
|
+
await mkdir(dirname(path), { recursive: true });
|
|
43
|
+
await writeFile(path, input.content, "utf8");
|
|
44
|
+
});
|
|
45
|
+
return { content: textContent(`Successfully wrote ${input.content.length} bytes to ${input.path}`) };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return failed(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function editFileTool(input, context, signal) {
|
|
52
|
+
try {
|
|
53
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
54
|
+
const details = await withMutationQueue(path, async () => {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
await access(path, constants.R_OK | constants.W_OK);
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const raw = await readFile(path, "utf8");
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
const applied = applyExactEdits(raw, input.edits, input.path);
|
|
61
|
+
await writeFile(path, applied.output, "utf8");
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
return applied.details;
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
content: textContent(`Successfully replaced ${input.edits.length} block(s) in ${input.path}.`),
|
|
67
|
+
details,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return failed(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function preflightEditFiles(paths, edits, context, signal) {
|
|
75
|
+
const seen = new Map();
|
|
76
|
+
for (const path of paths) {
|
|
77
|
+
signal?.throwIfAborted();
|
|
78
|
+
const absolute = await resolveCanonicalAllowedPath(path, context.cwd, context.fileRoots ?? [context.root]);
|
|
79
|
+
const canonical = await realpath(absolute);
|
|
80
|
+
const previous = seen.get(canonical);
|
|
81
|
+
if (previous) {
|
|
82
|
+
throw new Error(`Bulk Edit targets overlap: ${previous} and ${path} resolve to the same file.`);
|
|
83
|
+
}
|
|
84
|
+
seen.set(canonical, path);
|
|
85
|
+
await access(absolute, constants.R_OK | constants.W_OK);
|
|
86
|
+
const content = await readFile(absolute, "utf8");
|
|
87
|
+
applyExactEdits(content, edits, path);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function readText(content, input) {
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
const startIndex = Math.max(0, (input.offset ?? 1) - 1);
|
|
93
|
+
if (startIndex >= lines.length) {
|
|
94
|
+
throw new Error(`Offset ${input.offset} is beyond end of file (${lines.length} lines total)`);
|
|
95
|
+
}
|
|
96
|
+
const explicitEnd = input.limit === undefined
|
|
97
|
+
? lines.length
|
|
98
|
+
: Math.min(lines.length, startIndex + input.limit);
|
|
99
|
+
const selected = lines.slice(startIndex, explicitEnd);
|
|
100
|
+
const limited = truncateLinesAndBytes(selected, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES);
|
|
101
|
+
const startLine = startIndex + 1;
|
|
102
|
+
const endLine = startLine + limited.lines.length - 1;
|
|
103
|
+
let output = limited.lines.join("\n");
|
|
104
|
+
if (limited.truncated) {
|
|
105
|
+
const nextOffset = endLine + 1;
|
|
106
|
+
output += `\n\n[Showing lines ${startLine}-${endLine} of ${lines.length}. Use offset=${nextOffset} to continue.]`;
|
|
107
|
+
}
|
|
108
|
+
else if (explicitEnd < lines.length) {
|
|
109
|
+
const remaining = lines.length - explicitEnd;
|
|
110
|
+
output += `\n\n[${remaining} more lines in file. Use offset=${explicitEnd + 1} to continue.]`;
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
function truncateLinesAndBytes(lines, maxLines, maxBytes) {
|
|
115
|
+
const output = [];
|
|
116
|
+
let bytes = 0;
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (output.length >= maxLines)
|
|
119
|
+
return { lines: output, truncated: true };
|
|
120
|
+
const prefix = output.length === 0 ? "" : "\n";
|
|
121
|
+
const lineBytes = Buffer.byteLength(prefix + line, "utf8");
|
|
122
|
+
if (bytes + lineBytes > maxBytes) {
|
|
123
|
+
if (output.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
lines: [`[Line exceeds ${maxBytes} byte read limit. Use bash for byte-oriented inspection.]`],
|
|
126
|
+
truncated: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { lines: output, truncated: true };
|
|
130
|
+
}
|
|
131
|
+
output.push(line);
|
|
132
|
+
bytes += lineBytes;
|
|
133
|
+
}
|
|
134
|
+
return { lines: output, truncated: false };
|
|
135
|
+
}
|
|
136
|
+
function applyExactEdits(rawContent, edits, displayPath) {
|
|
137
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
138
|
+
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
|
|
139
|
+
}
|
|
140
|
+
const { bom, text } = stripBom(rawContent);
|
|
141
|
+
const lineEnding = detectLineEnding(text);
|
|
142
|
+
const original = normalizeLineEndings(text);
|
|
143
|
+
const prepared = edits.map((edit, index) => {
|
|
144
|
+
if (typeof edit.oldText !== "string" || typeof edit.newText !== "string" || edit.oldText.length === 0) {
|
|
145
|
+
throw new Error(`Edit ${index + 1} must contain non-empty oldText and string newText.`);
|
|
146
|
+
}
|
|
147
|
+
const oldText = normalizeLineEndings(edit.oldText);
|
|
148
|
+
const newText = normalizeLineEndings(edit.newText);
|
|
149
|
+
const start = original.indexOf(oldText);
|
|
150
|
+
if (start < 0) {
|
|
151
|
+
throw new Error(`Could not edit ${displayPath}: oldText for replacement ${index + 1} did not match.`);
|
|
152
|
+
}
|
|
153
|
+
if (original.indexOf(oldText, start + oldText.length) >= 0) {
|
|
154
|
+
throw new Error(`Could not edit ${displayPath}: oldText for replacement ${index + 1} must be unique but matched multiple locations.`);
|
|
155
|
+
}
|
|
156
|
+
return { start, end: start + oldText.length, newText };
|
|
157
|
+
}).sort((left, right) => left.start - right.start);
|
|
158
|
+
for (let index = 1; index < prepared.length; index += 1) {
|
|
159
|
+
if (prepared[index].start < prepared[index - 1].end) {
|
|
160
|
+
throw new Error(`Could not edit ${displayPath}: replacements overlap in the original file.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let cursor = 0;
|
|
164
|
+
let updated = "";
|
|
165
|
+
for (const edit of prepared) {
|
|
166
|
+
updated += original.slice(cursor, edit.start);
|
|
167
|
+
updated += edit.newText;
|
|
168
|
+
cursor = edit.end;
|
|
169
|
+
}
|
|
170
|
+
updated += original.slice(cursor);
|
|
171
|
+
const firstChangedLine = prepared.length === 0
|
|
172
|
+
? undefined
|
|
173
|
+
: original.slice(0, prepared[0].start).split("\n").length;
|
|
174
|
+
const patch = createPatch(displayPath, original, updated, "", "", { context: 3 });
|
|
175
|
+
const diff = patch;
|
|
176
|
+
const output = bom + restoreLineEndings(updated, lineEnding);
|
|
177
|
+
return { output, details: { diff, patch, firstChangedLine } };
|
|
178
|
+
}
|
|
179
|
+
function stripBom(content) {
|
|
180
|
+
return content.charCodeAt(0) === 0xfeff
|
|
181
|
+
? { bom: "\ufeff", text: content.slice(1) }
|
|
182
|
+
: { bom: "", text: content };
|
|
183
|
+
}
|
|
184
|
+
function detectLineEnding(content) {
|
|
185
|
+
if (content.includes("\r\n"))
|
|
186
|
+
return "\r\n";
|
|
187
|
+
if (content.includes("\r"))
|
|
188
|
+
return "\r";
|
|
189
|
+
return "\n";
|
|
190
|
+
}
|
|
191
|
+
function normalizeLineEndings(content) {
|
|
192
|
+
return content.replace(/\r\n?/g, "\n");
|
|
193
|
+
}
|
|
194
|
+
function restoreLineEndings(content, lineEnding) {
|
|
195
|
+
return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
|
|
196
|
+
}
|
|
197
|
+
function supportedImageMimeType(buffer) {
|
|
198
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
199
|
+
return "image/png";
|
|
200
|
+
}
|
|
201
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
202
|
+
return "image/jpeg";
|
|
203
|
+
}
|
|
204
|
+
const header6 = buffer.subarray(0, 6).toString("ascii");
|
|
205
|
+
if (header6 === "GIF87a" || header6 === "GIF89a")
|
|
206
|
+
return "image/gif";
|
|
207
|
+
if (buffer.length >= 12 &&
|
|
208
|
+
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
209
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP")
|
|
210
|
+
return "image/webp";
|
|
211
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM")
|
|
212
|
+
return "image/bmp";
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
async function withMutationQueue(path, operation) {
|
|
216
|
+
const canonical = await canonicalMutationPath(path);
|
|
217
|
+
const previous = mutationQueues.get(canonical) ?? Promise.resolve();
|
|
218
|
+
let release;
|
|
219
|
+
const current = new Promise((resolve) => {
|
|
220
|
+
release = resolve;
|
|
221
|
+
});
|
|
222
|
+
mutationQueues.set(canonical, previous.then(() => current));
|
|
223
|
+
await previous;
|
|
224
|
+
try {
|
|
225
|
+
return await operation();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
release();
|
|
229
|
+
if (mutationQueues.get(canonical) === current)
|
|
230
|
+
mutationQueues.delete(canonical);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function canonicalMutationPath(path) {
|
|
234
|
+
try {
|
|
235
|
+
return await realpath(path);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
try {
|
|
239
|
+
return `${await realpath(dirname(path))}/${path.slice(dirname(path).length + 1)}`;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { terminateProcessTree } from "../process/process-platform.js";
|
|
3
|
+
const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
4
|
+
export function executeHookCommand(input) {
|
|
5
|
+
input.signal?.throwIfAborted();
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(input.executable, input.args, {
|
|
8
|
+
cwd: input.cwd,
|
|
9
|
+
env: input.env,
|
|
10
|
+
detached: input.detached,
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
windowsVerbatimArguments: input.windowsVerbatimArguments,
|
|
13
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
+
});
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
let timedOut = false;
|
|
18
|
+
let forceKillTimer;
|
|
19
|
+
let aborted = false;
|
|
20
|
+
const abort = () => {
|
|
21
|
+
if (aborted)
|
|
22
|
+
return;
|
|
23
|
+
aborted = true;
|
|
24
|
+
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
25
|
+
forceKillTimer = setTimeout(() => {
|
|
26
|
+
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
27
|
+
}, 500);
|
|
28
|
+
forceKillTimer.unref();
|
|
29
|
+
};
|
|
30
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
31
|
+
child.stdout?.on("data", (chunk) => {
|
|
32
|
+
stdout = appendCaptured(stdout, chunk);
|
|
33
|
+
});
|
|
34
|
+
child.stderr?.on("data", (chunk) => {
|
|
35
|
+
stderr = appendCaptured(stderr, chunk);
|
|
36
|
+
});
|
|
37
|
+
const timeout = setTimeout(() => {
|
|
38
|
+
timedOut = true;
|
|
39
|
+
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
40
|
+
forceKillTimer = setTimeout(() => {
|
|
41
|
+
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
42
|
+
}, 500);
|
|
43
|
+
forceKillTimer.unref();
|
|
44
|
+
}, input.timeoutMs);
|
|
45
|
+
timeout.unref();
|
|
46
|
+
child.once("error", (error) => {
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
if (forceKillTimer)
|
|
49
|
+
clearTimeout(forceKillTimer);
|
|
50
|
+
input.signal?.removeEventListener("abort", abort);
|
|
51
|
+
reject(error);
|
|
52
|
+
});
|
|
53
|
+
child.once("close", (exitCode, signal) => {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
if (forceKillTimer)
|
|
56
|
+
clearTimeout(forceKillTimer);
|
|
57
|
+
input.signal?.removeEventListener("abort", abort);
|
|
58
|
+
if (aborted) {
|
|
59
|
+
reject(input.signal?.reason instanceof Error
|
|
60
|
+
? input.signal.reason
|
|
61
|
+
: Object.assign(new Error("Hook execution cancelled by Host."), { name: "AbortError" }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve({ exitCode, signal, stdout, stderr, timedOut });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function appendCaptured(current, chunk) {
|
|
69
|
+
if (Buffer.byteLength(current) >= MAX_CAPTURE_BYTES)
|
|
70
|
+
return current;
|
|
71
|
+
const next = current + chunk.toString();
|
|
72
|
+
if (Buffer.byteLength(next) <= MAX_CAPTURE_BYTES)
|
|
73
|
+
return next;
|
|
74
|
+
return Buffer.from(next).subarray(0, MAX_CAPTURE_BYTES).toString("utf8");
|
|
75
|
+
}
|
|
76
|
+
export function hookFailureOutput(stdout, stderr) {
|
|
77
|
+
const output = (stderr.trim() || stdout.trim()).replace(/\s+/g, " ");
|
|
78
|
+
return output.length > 1_000 ? `${output.slice(0, 997)}...` : output;
|
|
79
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { HOOK_EVENTS, loadProjectHookConfig, mergeHookConfigs, parseHookConfig, } from "./hooks.js";
|
|
3
|
-
import { loadForgeRelayFiles } from "
|
|
3
|
+
import { loadForgeRelayFiles } from "../../runtime/config/user-config.js";
|
|
4
4
|
export async function checkHookConfiguration(projectRoot, globalHooks = loadGlobalHooks()) {
|
|
5
5
|
const globalEntries = flattenHooks(globalHooks, "global");
|
|
6
6
|
const project = await loadProjectHookConfig(projectRoot);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { performance } from "node:perf_hooks";
|
|
5
|
-
import { commandPreview, logEvent, workspaceLogLabel } from "
|
|
6
|
-
import { resolveShellCommand
|
|
4
|
+
import { commandPreview, logEvent, workspaceLogLabel } from "../../runtime/logging/logger.js";
|
|
5
|
+
import { resolveShellCommand } from "../process/process-platform.js";
|
|
6
|
+
import { executeHookCommand, hookFailureOutput } from "./command-runner.js";
|
|
7
7
|
export const HOOK_EVENTS = [
|
|
8
8
|
"WorkspaceOpen",
|
|
9
9
|
"BeforeTool",
|
|
@@ -19,7 +19,6 @@ const DEFAULT_HOOK_TIMEOUT_SECONDS = 30;
|
|
|
19
19
|
const MAX_HOOK_TIMEOUT_SECONDS = 300;
|
|
20
20
|
const PROJECT_HOOKS_PATH = join(".forgerelay", "hooks.json");
|
|
21
21
|
const PROJECT_HOOKS_DIR = join(".forgerelay", "hooks");
|
|
22
|
-
const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
23
22
|
const BLOCKING_EVENTS = new Set(["BeforeTool", "BeforeWorktreeClose"]);
|
|
24
23
|
const EVENT_SET = new Set(HOOK_EVENTS);
|
|
25
24
|
export class HookExecutionError extends Error {
|
|
@@ -529,82 +528,6 @@ function hookEnvironment(baseEnv, event, invocation) {
|
|
|
529
528
|
FORGERELAY_TOOL_NAME: typeof invocation.payload?.tool === "string" ? invocation.payload.tool : undefined,
|
|
530
529
|
};
|
|
531
530
|
}
|
|
532
|
-
function executeHookCommand(input) {
|
|
533
|
-
input.signal?.throwIfAborted();
|
|
534
|
-
return new Promise((resolve, reject) => {
|
|
535
|
-
const child = spawn(input.executable, input.args, {
|
|
536
|
-
cwd: input.cwd,
|
|
537
|
-
env: input.env,
|
|
538
|
-
detached: input.detached,
|
|
539
|
-
windowsHide: true,
|
|
540
|
-
windowsVerbatimArguments: input.windowsVerbatimArguments,
|
|
541
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
542
|
-
});
|
|
543
|
-
let stdout = "";
|
|
544
|
-
let stderr = "";
|
|
545
|
-
let timedOut = false;
|
|
546
|
-
let forceKillTimer;
|
|
547
|
-
let aborted = false;
|
|
548
|
-
const abort = () => {
|
|
549
|
-
if (aborted)
|
|
550
|
-
return;
|
|
551
|
-
aborted = true;
|
|
552
|
-
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
553
|
-
forceKillTimer = setTimeout(() => {
|
|
554
|
-
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
555
|
-
}, 500);
|
|
556
|
-
forceKillTimer.unref();
|
|
557
|
-
};
|
|
558
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
559
|
-
child.stdout?.on("data", (chunk) => {
|
|
560
|
-
stdout = appendCaptured(stdout, chunk);
|
|
561
|
-
});
|
|
562
|
-
child.stderr?.on("data", (chunk) => {
|
|
563
|
-
stderr = appendCaptured(stderr, chunk);
|
|
564
|
-
});
|
|
565
|
-
const timeout = setTimeout(() => {
|
|
566
|
-
timedOut = true;
|
|
567
|
-
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
568
|
-
forceKillTimer = setTimeout(() => {
|
|
569
|
-
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
570
|
-
}, 500);
|
|
571
|
-
forceKillTimer.unref();
|
|
572
|
-
}, input.timeoutMs);
|
|
573
|
-
timeout.unref();
|
|
574
|
-
child.once("error", (error) => {
|
|
575
|
-
clearTimeout(timeout);
|
|
576
|
-
if (forceKillTimer)
|
|
577
|
-
clearTimeout(forceKillTimer);
|
|
578
|
-
input.signal?.removeEventListener("abort", abort);
|
|
579
|
-
reject(error);
|
|
580
|
-
});
|
|
581
|
-
child.once("close", (exitCode, signal) => {
|
|
582
|
-
clearTimeout(timeout);
|
|
583
|
-
if (forceKillTimer)
|
|
584
|
-
clearTimeout(forceKillTimer);
|
|
585
|
-
input.signal?.removeEventListener("abort", abort);
|
|
586
|
-
if (aborted) {
|
|
587
|
-
reject(input.signal?.reason instanceof Error
|
|
588
|
-
? input.signal.reason
|
|
589
|
-
: Object.assign(new Error("Hook execution cancelled by Host."), { name: "AbortError" }));
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
resolve({ exitCode, signal, stdout, stderr, timedOut });
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
function appendCaptured(current, chunk) {
|
|
597
|
-
if (Buffer.byteLength(current) >= MAX_CAPTURE_BYTES)
|
|
598
|
-
return current;
|
|
599
|
-
const next = current + chunk.toString();
|
|
600
|
-
if (Buffer.byteLength(next) <= MAX_CAPTURE_BYTES)
|
|
601
|
-
return next;
|
|
602
|
-
return Buffer.from(next).subarray(0, MAX_CAPTURE_BYTES).toString("utf8");
|
|
603
|
-
}
|
|
604
|
-
function hookFailureOutput(stdout, stderr) {
|
|
605
|
-
const output = (stderr.trim() || stdout.trim()).replace(/\s+/g, " ");
|
|
606
|
-
return output.length > 1_000 ? `${output.slice(0, 997)}...` : output;
|
|
607
|
-
}
|
|
608
531
|
function errorMessage(error) {
|
|
609
532
|
return error instanceof Error ? error.message : String(error);
|
|
610
533
|
}
|
|
@@ -23,7 +23,7 @@ function htmlEscape(value) {
|
|
|
23
23
|
.replaceAll("'", "'");
|
|
24
24
|
}
|
|
25
25
|
function formHtml(params) {
|
|
26
|
-
const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "
|
|
26
|
+
const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "forgerelay";
|
|
27
27
|
const resourceText = params.resource?.href ?? "ForgeRelay MCP endpoint";
|
|
28
28
|
const error = params.error
|
|
29
29
|
? `<p class="error">${htmlEscape(params.error)}</p>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
|
-
import { openDatabase } from "
|
|
3
|
+
import { openDatabase } from "../../runtime/state/db/client.js";
|
|
4
4
|
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
5
5
|
let parsed;
|
|
6
6
|
try {
|
|
@@ -9,9 +9,10 @@ function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
|
9
9
|
catch {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
13
|
+
if (loopback)
|
|
14
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
15
|
+
return parsed.protocol === "https:" && allowedHosts.includes(parsed.hostname);
|
|
15
16
|
}
|
|
16
17
|
export class SqliteOAuthStore {
|
|
17
18
|
database;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { preflightDeletePaths } from "../file-mutations.js";
|
|
2
|
-
import { preflightEditFiles } from "../
|
|
1
|
+
import { preflightDeletePaths } from "../filesystem/file-mutations.js";
|
|
2
|
+
import { preflightEditFiles } from "../filesystem/filesystem-tools.js";
|
|
3
3
|
import { openAiConversationScopeId } from "../request-meta.js";
|
|
4
4
|
import { executeSequentialBulkMutation } from "./bulk-mutation.js";
|
|
5
5
|
export class NativeBulkMutationExecutor {
|