@akira-tl/forgerelay 0.8.7 → 0.8.9
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 +18 -0
- package/README.md +10 -18
- package/capabilities/host-integration/GUIDE.md +2 -2
- 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/setup-support.js +116 -0
- package/dist/cli.js +18 -126
- package/dist/lsp/code-intelligence.js +1 -1
- package/dist/lsp/test-support/server-fixture.js +9 -9
- 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/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} +2 -2
- 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 +207 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -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 +222 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/{config.js → runtime/config/config.js} +3 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
- 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 +139 -4192
- 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 -409
- 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 +302 -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/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/skills.js +285 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +457 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +69 -991
- package/docs/chatgpt-coding-workflow.md +3 -11
- package/docs/configuration.md +12 -25
- 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 -8
- 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/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-CUAN6zyW.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-CgzrutLm.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-BrLbezbq.js +0 -1
- package/dist/ui/assets/scrollbar-CbhpdW05.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-Bhj96tsR.js +0 -1
- package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
- package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.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/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/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/{process-platform.js → mcp/process/process-platform.js} +0 -0
- /package/dist/{process-sessions.js → mcp/process/process-sessions.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/{remote-transport.js → workspaces/relay/transport/remote-transport.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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { assertAllowedPath } from "../mcp/filesystem/roots.js";
|
|
3
|
+
import { canonicalPath } from "./paths.js";
|
|
4
|
+
const WORKSPACE_STALE_REMINDER_MS = 2 * 24 * 60 * 60 * 1_000;
|
|
5
|
+
/** Build bounded read-only projections over persistent Workspace sessions. */
|
|
6
|
+
export class WorkspaceInventoryService {
|
|
7
|
+
config;
|
|
8
|
+
store;
|
|
9
|
+
workspaces;
|
|
10
|
+
sessions;
|
|
11
|
+
constructor(config, store, workspaces, sessions) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.store = store;
|
|
14
|
+
this.workspaces = workspaces;
|
|
15
|
+
this.sessions = sessions;
|
|
16
|
+
}
|
|
17
|
+
async inspectWorkspace(workspaceId) {
|
|
18
|
+
let session = this.store?.getSession(workspaceId);
|
|
19
|
+
if (!session) {
|
|
20
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
21
|
+
if (workspace)
|
|
22
|
+
session = workspaceSessionSnapshot(workspace);
|
|
23
|
+
}
|
|
24
|
+
if (!session)
|
|
25
|
+
throw new Error(`Unknown workspaceId: ${workspaceId}.`);
|
|
26
|
+
const entry = await this.inventoryEntryForSession(session, Date.now(), false);
|
|
27
|
+
const { current: _current, ...inspection } = entry;
|
|
28
|
+
return { kind: "workspace", location: "local", ...inspection };
|
|
29
|
+
}
|
|
30
|
+
async listWorkspaces(input = {}, openOptions = {}) {
|
|
31
|
+
this.sessions.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const sessions = this.store
|
|
34
|
+
? this.store.listSessions()
|
|
35
|
+
: [...this.workspaces.values()].map(workspaceSessionSnapshot);
|
|
36
|
+
const currentWorkspaceIds = new Set(openOptions.conversationScopeId && this.store
|
|
37
|
+
? this.store
|
|
38
|
+
.listConversationBindings()
|
|
39
|
+
.filter((binding) => binding.conversationScopeId === openOptions.conversationScopeId)
|
|
40
|
+
.map((binding) => binding.workspaceSessionId)
|
|
41
|
+
: []);
|
|
42
|
+
const workspaceIdFilter = input.workspaceId
|
|
43
|
+
? this.store?.getSession(input.workspaceId)?.id ?? input.workspaceId
|
|
44
|
+
: undefined;
|
|
45
|
+
const rootKey = input.root
|
|
46
|
+
? await canonicalPath(assertAllowedPath(input.root, [...this.config.allowedRoots, this.config.worktreeRoot]))
|
|
47
|
+
: undefined;
|
|
48
|
+
const entries = await Promise.all(sessions.map((session) => this.inventoryEntryForSession(session, now, currentWorkspaceIds.has(session.id))));
|
|
49
|
+
const filtered = [];
|
|
50
|
+
for (let index = 0; index < sessions.length; index += 1) {
|
|
51
|
+
const session = sessions[index];
|
|
52
|
+
const entry = entries[index];
|
|
53
|
+
if (!session || !entry)
|
|
54
|
+
continue;
|
|
55
|
+
if (workspaceIdFilter && entry.workspaceId !== workspaceIdFilter)
|
|
56
|
+
continue;
|
|
57
|
+
if (input.status && entry.status !== input.status)
|
|
58
|
+
continue;
|
|
59
|
+
if (input.state && entry.state !== input.state)
|
|
60
|
+
continue;
|
|
61
|
+
if (input.mode && entry.mode !== input.mode)
|
|
62
|
+
continue;
|
|
63
|
+
if (input.staleOnly && entry.state !== "stale")
|
|
64
|
+
continue;
|
|
65
|
+
if (rootKey) {
|
|
66
|
+
const sessionRootKey = await canonicalPath(session.root);
|
|
67
|
+
const sourceRootKey = session.sourceRoot ? await canonicalPath(session.sourceRoot) : undefined;
|
|
68
|
+
if (sessionRootKey !== rootKey && sourceRootKey !== rootKey)
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
filtered.push(entry);
|
|
72
|
+
}
|
|
73
|
+
const summary = filtered.reduce((counts, entry) => {
|
|
74
|
+
counts[entry.state] += 1;
|
|
75
|
+
return counts;
|
|
76
|
+
}, { active: 0, stale: 0, invalid: 0, closed: 0 });
|
|
77
|
+
const offset = Math.max(0, input.offset ?? 0);
|
|
78
|
+
const limit = Math.min(100, Math.max(1, input.limit ?? 50));
|
|
79
|
+
return {
|
|
80
|
+
workspaces: filtered.slice(offset, offset + limit),
|
|
81
|
+
summary: { total: entries.length, matching: filtered.length, ...summary },
|
|
82
|
+
page: { offset, limit, hasMore: offset + limit < filtered.length },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async inventoryEntryForSession(session, now, current) {
|
|
86
|
+
const rootValid = await this.sessions.validSessionRoot(session) !== undefined;
|
|
87
|
+
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
88
|
+
const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
|
|
89
|
+
const state = session.status !== "active"
|
|
90
|
+
? "closed"
|
|
91
|
+
: !rootValid
|
|
92
|
+
? "invalid"
|
|
93
|
+
: idleMs >= WORKSPACE_STALE_REMINDER_MS
|
|
94
|
+
? "stale"
|
|
95
|
+
: "active";
|
|
96
|
+
const projectRoot = session.sourceRoot ?? session.root;
|
|
97
|
+
return {
|
|
98
|
+
label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
|
|
99
|
+
workspaceId: session.id,
|
|
100
|
+
root: session.root,
|
|
101
|
+
status: session.status,
|
|
102
|
+
state,
|
|
103
|
+
mode: session.mode,
|
|
104
|
+
sourceRoot: session.sourceRoot,
|
|
105
|
+
branch: session.branch,
|
|
106
|
+
targetBranch: session.targetBranch,
|
|
107
|
+
managed: session.managed,
|
|
108
|
+
createdAt: session.createdAt,
|
|
109
|
+
lastUsedAt: session.lastUsedAt,
|
|
110
|
+
idleMs,
|
|
111
|
+
rootValid,
|
|
112
|
+
current,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function workspaceSessionSnapshot(workspace) {
|
|
117
|
+
return {
|
|
118
|
+
id: workspace.id,
|
|
119
|
+
root: workspace.root,
|
|
120
|
+
status: "active",
|
|
121
|
+
mode: workspace.mode,
|
|
122
|
+
sourceRoot: workspace.sourceRoot,
|
|
123
|
+
baseRef: workspace.worktree?.baseRef,
|
|
124
|
+
baseSha: workspace.worktree?.baseSha,
|
|
125
|
+
branch: workspace.worktree?.branch,
|
|
126
|
+
targetBranch: workspace.worktree?.targetBranch,
|
|
127
|
+
managed: workspace.worktree?.managed ?? false,
|
|
128
|
+
createdAt: "",
|
|
129
|
+
lastUsedAt: "",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { mkdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
export function canonicalPersistedWorkspacePath(path) {
|
|
5
|
+
const missingSegments = [];
|
|
6
|
+
let candidate = path;
|
|
7
|
+
while (true) {
|
|
8
|
+
try {
|
|
9
|
+
return resolve(realpathSync(candidate), ...missingSegments.slice().reverse());
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
13
|
+
return resolve(path);
|
|
14
|
+
}
|
|
15
|
+
const parent = dirname(candidate);
|
|
16
|
+
if (parent === candidate)
|
|
17
|
+
return resolve(path);
|
|
18
|
+
missingSegments.push(basename(candidate));
|
|
19
|
+
candidate = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function canonicalPath(path) {
|
|
24
|
+
const missingSegments = [];
|
|
25
|
+
let candidate = path;
|
|
26
|
+
while (true) {
|
|
27
|
+
try {
|
|
28
|
+
return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const parent = dirname(candidate);
|
|
35
|
+
if (parent === candidate)
|
|
36
|
+
return path;
|
|
37
|
+
missingSegments.push(basename(candidate));
|
|
38
|
+
candidate = parent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function ensureCheckoutWorkspaceRoot(path, ops = { stat, mkdir }) {
|
|
43
|
+
try {
|
|
44
|
+
return await ops.stat(path);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!isErrnoException(error) || error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
await ops.mkdir(path, { recursive: true });
|
|
51
|
+
return await ops.stat(path);
|
|
52
|
+
}
|
|
53
|
+
function isErrnoException(error) {
|
|
54
|
+
return error instanceof Error && "code" in error;
|
|
55
|
+
}
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
3
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
4
|
import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
-
import { publicEndpointUrl } from "
|
|
5
|
+
import { publicEndpointUrl } from "../../../mcp/oauth/public-url.js";
|
|
6
6
|
const REMOTE_AUTH_TIMEOUT_MS = 15_000;
|
|
7
7
|
const packageVersion = readForgeRelayVersion();
|
|
8
8
|
export function normalizeRemoteServiceTarget(value) {
|
|
@@ -112,7 +112,7 @@ async function exchangeCliCredential(target, credential) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
function readForgeRelayVersion() {
|
|
115
|
-
const packageJson = JSON.parse(readFileSync(new URL("
|
|
115
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../../../package.json", import.meta.url), "utf8"));
|
|
116
116
|
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
117
117
|
throw new Error("Unable to read ForgeRelay package version.");
|
|
118
118
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function assertRemoteToolSucceeded(alias, tool, result) {
|
|
2
|
+
if (result.isError !== true)
|
|
3
|
+
return;
|
|
4
|
+
throw new Error(`Remote ForgeRelay ${alias} ${tool} failed: ${toolResultText(result)}`);
|
|
5
|
+
}
|
|
6
|
+
export function stringField(structured, field, label) {
|
|
7
|
+
const value = structured?.[field];
|
|
8
|
+
if (typeof value !== "string" || !value) {
|
|
9
|
+
throw new Error(`${label} did not include ${field}.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function copyStringField(source, target, field) {
|
|
14
|
+
const value = source[field];
|
|
15
|
+
if (typeof value === "string")
|
|
16
|
+
target[field] = value;
|
|
17
|
+
}
|
|
18
|
+
export function copyBooleanField(source, target, field) {
|
|
19
|
+
const value = source[field];
|
|
20
|
+
if (typeof value === "boolean")
|
|
21
|
+
target[field] = value;
|
|
22
|
+
}
|
|
23
|
+
export function copyNumberField(source, target, field) {
|
|
24
|
+
const value = source[field];
|
|
25
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
26
|
+
target[field] = value;
|
|
27
|
+
}
|
|
28
|
+
export function safeTaskSummary(value) {
|
|
29
|
+
if (!value || typeof value !== "object")
|
|
30
|
+
return undefined;
|
|
31
|
+
const summary = value;
|
|
32
|
+
if (summary.level !== "summary" ||
|
|
33
|
+
summary.version !== 1 ||
|
|
34
|
+
typeof summary.revision !== "number" ||
|
|
35
|
+
!Number.isInteger(summary.revision) ||
|
|
36
|
+
summary.revision < 0 ||
|
|
37
|
+
!Array.isArray(summary.lists)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const lists = [];
|
|
41
|
+
for (const value of summary.lists) {
|
|
42
|
+
if (!value || typeof value !== "object")
|
|
43
|
+
return undefined;
|
|
44
|
+
const list = value;
|
|
45
|
+
if (typeof list.id !== "string" ||
|
|
46
|
+
typeof list.name !== "string" ||
|
|
47
|
+
(list.state !== "active" && list.state !== "archived") ||
|
|
48
|
+
typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
|
|
49
|
+
typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
|
|
50
|
+
typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
lists.push({
|
|
54
|
+
id: list.id,
|
|
55
|
+
name: list.name,
|
|
56
|
+
state: list.state,
|
|
57
|
+
revision: list.revision,
|
|
58
|
+
taskCount: list.taskCount,
|
|
59
|
+
unfinishedTaskCount: list.unfinishedTaskCount,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
level: "summary",
|
|
64
|
+
version: 1,
|
|
65
|
+
revision: summary.revision,
|
|
66
|
+
lists,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function toolResultText(result) {
|
|
70
|
+
return (result.content ?? [])
|
|
71
|
+
.filter((entry) => entry.type === "text")
|
|
72
|
+
.map((entry) => entry.text)
|
|
73
|
+
.join("\n") || "remote tool returned an error";
|
|
74
|
+
}
|
|
75
|
+
export function remapToolResultWorkspaceId(result, remoteWorkspaceId, gatewayWorkspaceId) {
|
|
76
|
+
return {
|
|
77
|
+
...result,
|
|
78
|
+
content: (result.content ?? []).map((entry) => entry.type === "text"
|
|
79
|
+
? { ...entry, text: entry.text.split(remoteWorkspaceId).join(gatewayWorkspaceId) }
|
|
80
|
+
: entry),
|
|
81
|
+
...(result._meta
|
|
82
|
+
? { _meta: replaceExactWorkspaceId(result._meta, remoteWorkspaceId, gatewayWorkspaceId) }
|
|
83
|
+
: {}),
|
|
84
|
+
...(result.structuredContent
|
|
85
|
+
? {
|
|
86
|
+
structuredContent: replaceExactWorkspaceId(result.structuredContent, remoteWorkspaceId, gatewayWorkspaceId),
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function replaceExactWorkspaceId(value, from, to) {
|
|
92
|
+
if (typeof value === "string")
|
|
93
|
+
return value.split(from).join(to);
|
|
94
|
+
if (Array.isArray(value))
|
|
95
|
+
return value.map((entry) => replaceExactWorkspaceId(entry, from, to));
|
|
96
|
+
if (!value || typeof value !== "object")
|
|
97
|
+
return value;
|
|
98
|
+
return Object.fromEntries(Object.entries(value)
|
|
99
|
+
.map(([key, entry]) => [key, replaceExactWorkspaceId(entry, from, to)]));
|
|
100
|
+
}
|
|
101
|
+
export function sanitizedRemoteError(error, remoteWorkspaceId, gatewayWorkspaceId) {
|
|
102
|
+
let message = errorMessage(error);
|
|
103
|
+
if (remoteWorkspaceId && gatewayWorkspaceId) {
|
|
104
|
+
message = message.split(remoteWorkspaceId).join(gatewayWorkspaceId);
|
|
105
|
+
}
|
|
106
|
+
message = message.replace(/(^|[^A-Za-z0-9_])ws_[0-9a-f]{10}(?=$|[^A-Za-z0-9_])/g, "$1[remote-workspace]");
|
|
107
|
+
return new Error(message, { cause: error });
|
|
108
|
+
}
|
|
109
|
+
export function errorMessage(error) {
|
|
110
|
+
return error instanceof Error ? error.message : String(error);
|
|
111
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
8
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { ActivityAuditStore } from "../../../activity/history/audit-store.js";
|
|
10
|
+
import { BashOutputStore } from "../../../activity/history/bash-output-store.js";
|
|
11
|
+
import { HostTurnStore } from "../../../activity/history/host-turn-store.js";
|
|
12
|
+
import { ActivityLifecycle } from "../../../activity/runtime/lifecycle.js";
|
|
13
|
+
import { ActivityQueryService } from "../../../activity/history/query-service.js";
|
|
14
|
+
import { loadConfig } from "../../../runtime/config/config.js";
|
|
15
|
+
import { CodeIntelligenceManager } from "../../../lsp/runtime/manager.js";
|
|
16
|
+
import { createReviewCheckpointManager } from "../../review/review-checkpoints.js";
|
|
17
|
+
import { ProcessManager } from "../../../mcp/process/process-sessions.js";
|
|
18
|
+
import { createMcpServer, createServer } from "../../../server.js";
|
|
19
|
+
import { SqliteWorkspaceStore } from "../../state/workspace-store.js";
|
|
20
|
+
import { WorkspaceRegistry } from "../../../workspaces.js";
|
|
21
|
+
const childProcessModule = createRequire(import.meta.url)("node:child_process");
|
|
22
|
+
const cleanProductEnv = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("FORGERELAY_")));
|
|
23
|
+
export async function setupGitRepository(root) {
|
|
24
|
+
await mkdir(root, { recursive: true });
|
|
25
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "ignore" });
|
|
26
|
+
execFileSync("git", ["config", "user.name", "ForgeRelay Test"], { cwd: root });
|
|
27
|
+
execFileSync("git", ["config", "user.email", "forgerelay-test@example.invalid"], { cwd: root });
|
|
28
|
+
await writeFile(join(root, "README.md"), "relay worktree fixture\n");
|
|
29
|
+
execFileSync("git", ["add", "README.md"], { cwd: root });
|
|
30
|
+
execFileSync("git", ["commit", "-m", "TEST: (relay) initialize worktree fixture"], { cwd: root, stdio: "ignore" });
|
|
31
|
+
}
|
|
32
|
+
export async function startForge(t, options) {
|
|
33
|
+
const configDir = options.existingConfigDir ?? join(options.root, "config");
|
|
34
|
+
const stateDir = join(options.root, "state");
|
|
35
|
+
await mkdir(configDir, { recursive: true });
|
|
36
|
+
await mkdir(stateDir, { recursive: true });
|
|
37
|
+
if (!options.existingConfigDir) {
|
|
38
|
+
await writeFile(join(configDir, "auth.json"), JSON.stringify({
|
|
39
|
+
ownerToken: options.ownerToken,
|
|
40
|
+
instanceId: options.instanceId,
|
|
41
|
+
}, null, 2), { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
await writeFile(join(configDir, "config.json"), JSON.stringify({
|
|
44
|
+
host: "127.0.0.1",
|
|
45
|
+
port: 7676,
|
|
46
|
+
allowedRoots: [options.allowedRoot],
|
|
47
|
+
publicBaseUrl: "http://127.0.0.1:7676",
|
|
48
|
+
stateDir,
|
|
49
|
+
worktreeRoot: join(options.root, "worktrees"),
|
|
50
|
+
...(options.taskReminderInterval !== undefined ? { taskReminderInterval: options.taskReminderInterval } : {}),
|
|
51
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
52
|
+
}, null, 2));
|
|
53
|
+
const env = {
|
|
54
|
+
...cleanProductEnv,
|
|
55
|
+
FORGERELAY_CONFIG_DIR: configDir,
|
|
56
|
+
FORGERELAY_TOOL_MODE: options.toolMode ?? "minimal",
|
|
57
|
+
FORGERELAY_WIDGETS: "off",
|
|
58
|
+
FORGERELAY_SKILLS: "0",
|
|
59
|
+
};
|
|
60
|
+
const running = createServer(loadConfig(env));
|
|
61
|
+
const httpServer = running.app.listen(0, "127.0.0.1");
|
|
62
|
+
await new Promise((resolve) => httpServer.once("listening", resolve));
|
|
63
|
+
const port = httpServer.address().port;
|
|
64
|
+
t.after(async () => {
|
|
65
|
+
await running.close();
|
|
66
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
70
|
+
ownerToken: options.ownerToken,
|
|
71
|
+
openAdditionalEndpoint: async () => {
|
|
72
|
+
const additionalServer = running.app.listen(0, "127.0.0.1");
|
|
73
|
+
await new Promise((resolve) => additionalServer.once("listening", resolve));
|
|
74
|
+
const additionalPort = additionalServer.address().port;
|
|
75
|
+
t.after(() => new Promise((resolve) => additionalServer.close(() => resolve())));
|
|
76
|
+
return `http://127.0.0.1:${additionalPort}`;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function startGatewayClient(t, options) {
|
|
81
|
+
const stateDir = options.stateDir ?? join(options.root, "state");
|
|
82
|
+
await mkdir(stateDir, { recursive: true });
|
|
83
|
+
await writeFile(join(options.configDir, "config.json"), JSON.stringify({
|
|
84
|
+
allowedRoots: [options.allowedRoot],
|
|
85
|
+
stateDir,
|
|
86
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
87
|
+
}, null, 2));
|
|
88
|
+
const config = loadConfig({
|
|
89
|
+
...cleanProductEnv,
|
|
90
|
+
FORGERELAY_CONFIG_DIR: options.configDir,
|
|
91
|
+
FORGERELAY_TOOL_MODE: options.toolMode ?? "minimal",
|
|
92
|
+
FORGERELAY_WIDGETS: "off",
|
|
93
|
+
FORGERELAY_SKILLS: "0",
|
|
94
|
+
});
|
|
95
|
+
const workspaceStore = new SqliteWorkspaceStore(stateDir);
|
|
96
|
+
const workspaces = new WorkspaceRegistry(config, workspaceStore);
|
|
97
|
+
const auditStore = new ActivityAuditStore(stateDir);
|
|
98
|
+
const bashOutputStore = new BashOutputStore(stateDir);
|
|
99
|
+
const hostTurnStore = new HostTurnStore(stateDir);
|
|
100
|
+
const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
|
|
101
|
+
const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
|
|
102
|
+
const activityLifecycle = new ActivityLifecycle(auditStore, {
|
|
103
|
+
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
104
|
+
});
|
|
105
|
+
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
106
|
+
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
|
|
107
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
108
|
+
const client = new Client({ name: "forgerelay-workspace-relay-test", version: "1.0.0" });
|
|
109
|
+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
|
110
|
+
t.after(async () => {
|
|
111
|
+
await client.close();
|
|
112
|
+
await server.close();
|
|
113
|
+
await codeIntelligence.shutdown();
|
|
114
|
+
processSessions.shutdown();
|
|
115
|
+
hostTurnStore.close();
|
|
116
|
+
bashOutputStore.close();
|
|
117
|
+
auditStore.close();
|
|
118
|
+
workspaceStore.close();
|
|
119
|
+
});
|
|
120
|
+
return client;
|
|
121
|
+
}
|
|
122
|
+
export async function installFakeSsh(t, root, sshLog) {
|
|
123
|
+
const fakeSshPath = join(root, "fake-ssh.cjs");
|
|
124
|
+
await writeFile(fakeSshPath, fakeSshRelaySource());
|
|
125
|
+
const originalSpawn = childProcessModule.spawn;
|
|
126
|
+
childProcessModule.spawn = ((command, args = [], options = {}) => {
|
|
127
|
+
if (command !== "ssh")
|
|
128
|
+
return originalSpawn(command, args, options);
|
|
129
|
+
return originalSpawn(process.execPath, [fakeSshPath, ...args], {
|
|
130
|
+
...options,
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
...(options.env ?? {}),
|
|
134
|
+
TEST_SSH_LOG: sshLog,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
syncBuiltinESMExports();
|
|
139
|
+
t.after(() => {
|
|
140
|
+
childProcessModule.spawn = originalSpawn;
|
|
141
|
+
syncBuiltinESMExports();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function fakeSshRelaySource() {
|
|
145
|
+
return `#!/usr/bin/env node
|
|
146
|
+
const fs = require("node:fs");
|
|
147
|
+
const net = require("node:net");
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
fs.appendFileSync(process.env.TEST_SSH_LOG, JSON.stringify(args) + "\\n");
|
|
150
|
+
const forwardIndex = args.indexOf("-L");
|
|
151
|
+
const forward = forwardIndex >= 0 ? args[forwardIndex + 1] : undefined;
|
|
152
|
+
if (!forward) {
|
|
153
|
+
process.stderr.write("fake ssh: missing -L\\n");
|
|
154
|
+
process.exit(40);
|
|
155
|
+
}
|
|
156
|
+
const match = /^127\\.0\\.0\\.1:(\\d+):([^:]+):(\\d+)$/.exec(forward);
|
|
157
|
+
if (!match) {
|
|
158
|
+
process.stderr.write("fake ssh: invalid forward " + forward + "\\n");
|
|
159
|
+
process.exit(41);
|
|
160
|
+
}
|
|
161
|
+
const localPort = Number(match[1]);
|
|
162
|
+
const remoteHost = match[2];
|
|
163
|
+
const remotePort = Number(match[3]);
|
|
164
|
+
const sockets = new Set();
|
|
165
|
+
const server = net.createServer((client) => {
|
|
166
|
+
const upstream = net.connect(remotePort, remoteHost);
|
|
167
|
+
sockets.add(client);
|
|
168
|
+
sockets.add(upstream);
|
|
169
|
+
client.on("close", () => sockets.delete(client));
|
|
170
|
+
upstream.on("close", () => sockets.delete(upstream));
|
|
171
|
+
client.pipe(upstream);
|
|
172
|
+
upstream.pipe(client);
|
|
173
|
+
});
|
|
174
|
+
server.on("error", (error) => {
|
|
175
|
+
process.stderr.write(String(error) + "\\n");
|
|
176
|
+
process.exit(42);
|
|
177
|
+
});
|
|
178
|
+
server.listen(localPort, "127.0.0.1", () => {
|
|
179
|
+
process.stderr.write("debug1: Local forwarding listening on 127.0.0.1 port " + localPort + ".\\n");
|
|
180
|
+
});
|
|
181
|
+
const stop = () => {
|
|
182
|
+
for (const socket of sockets) socket.destroy();
|
|
183
|
+
server.close(() => process.exit(0));
|
|
184
|
+
};
|
|
185
|
+
process.on("SIGTERM", stop);
|
|
186
|
+
process.on("SIGINT", stop);
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
export function structuredContent(result) {
|
|
190
|
+
const parsed = parseToolResult(result);
|
|
191
|
+
assert.ok(parsed.structuredContent);
|
|
192
|
+
return parsed.structuredContent;
|
|
193
|
+
}
|
|
194
|
+
export function resultText(result) {
|
|
195
|
+
const parsed = parseToolResult(result);
|
|
196
|
+
return (parsed.content ?? [])
|
|
197
|
+
.filter((entry) => entry.type === "text")
|
|
198
|
+
.map((entry) => entry.text)
|
|
199
|
+
.join("\n");
|
|
200
|
+
}
|
|
201
|
+
function parseToolResult(result) {
|
|
202
|
+
return CallToolResultSchema.parse(result);
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|