@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
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const AUTH_LOCK_TIMEOUT_MS = 5_000;
|
|
9
|
-
const AUTH_LOCK_STALE_MS = 30_000;
|
|
10
|
-
const AUTH_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
5
|
+
import { withFileLock } from "../state/lock/file-lock.js";
|
|
6
|
+
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
7
|
+
import { mergeHookConfigs, parseHookFile, } from "../../mcp/hooks/hooks.js";
|
|
11
8
|
export function forgerelayConfigDir(env = process.env) {
|
|
12
9
|
const explicit = env.FORGERELAY_CONFIG_DIR;
|
|
13
10
|
if (explicit)
|
|
@@ -60,10 +57,10 @@ export function writeForgeRelayConfig(config, env = process.env) {
|
|
|
60
57
|
writeJsonFile(filePath, config, 0o600);
|
|
61
58
|
return filePath;
|
|
62
59
|
}
|
|
63
|
-
export function writeForgeRelayAuth(auth, env = process.env) {
|
|
60
|
+
export async function writeForgeRelayAuth(auth, env = process.env) {
|
|
64
61
|
const filePath = forgerelayAuthPath(env);
|
|
65
62
|
mkdirSync(forgerelayConfigDir(env), { recursive: true });
|
|
66
|
-
return
|
|
63
|
+
return withFileLock(`${filePath}.lock`, () => {
|
|
67
64
|
writeJsonFile(filePath, auth, 0o600);
|
|
68
65
|
return filePath;
|
|
69
66
|
});
|
|
@@ -74,12 +71,12 @@ export function generateOwnerToken() {
|
|
|
74
71
|
export function generateInstanceId() {
|
|
75
72
|
return `forge-${randomUUID()}`;
|
|
76
73
|
}
|
|
77
|
-
export function ensureForgeRelayInstanceId(env = process.env) {
|
|
74
|
+
export async function ensureForgeRelayInstanceId(env = process.env) {
|
|
78
75
|
const existing = loadForgeRelayFiles(env).auth.instanceId?.trim();
|
|
79
76
|
if (existing)
|
|
80
77
|
return existing;
|
|
81
78
|
let resolved = "";
|
|
82
|
-
updateForgeRelayAuth((auth) => {
|
|
79
|
+
await updateForgeRelayAuth((auth) => {
|
|
83
80
|
const current = auth.instanceId?.trim();
|
|
84
81
|
if (current) {
|
|
85
82
|
resolved = current;
|
|
@@ -97,7 +94,7 @@ function normalizeRemoteAlias(alias) {
|
|
|
97
94
|
}
|
|
98
95
|
return normalized;
|
|
99
96
|
}
|
|
100
|
-
export function writeForgeRelayRemote(alias, remote, env = process.env) {
|
|
97
|
+
export async function writeForgeRelayRemote(alias, remote, env = process.env) {
|
|
101
98
|
const normalizedAlias = normalizeRemoteAlias(alias);
|
|
102
99
|
return updateForgeRelayAuth((auth) => {
|
|
103
100
|
const remotes = { ...(auth.remotes ?? {}) };
|
|
@@ -113,7 +110,7 @@ export function writeForgeRelayRemote(alias, remote, env = process.env) {
|
|
|
113
110
|
return { ...auth, remotes };
|
|
114
111
|
}, env);
|
|
115
112
|
}
|
|
116
|
-
export function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
|
|
113
|
+
export async function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
|
|
117
114
|
const from = normalizeRemoteAlias(fromAlias);
|
|
118
115
|
const to = normalizeRemoteAlias(toAlias);
|
|
119
116
|
return updateForgeRelayAuth((auth) => {
|
|
@@ -128,7 +125,7 @@ export function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
|
|
|
128
125
|
return { ...auth, remotes };
|
|
129
126
|
}, env);
|
|
130
127
|
}
|
|
131
|
-
export function removeForgeRelayRemote(alias, env = process.env) {
|
|
128
|
+
export async function removeForgeRelayRemote(alias, env = process.env) {
|
|
132
129
|
const normalizedAlias = normalizeRemoteAlias(alias);
|
|
133
130
|
return updateForgeRelayAuth((auth) => {
|
|
134
131
|
const remotes = { ...(auth.remotes ?? {}) };
|
|
@@ -142,7 +139,7 @@ export function ensureForgeRelayDefaultSkills(env = process.env) {
|
|
|
142
139
|
const targetPath = join(forgerelaySkillsDir(env), "subagent-delegation", "SKILL.md");
|
|
143
140
|
if (existsSync(targetPath))
|
|
144
141
|
return [];
|
|
145
|
-
const sourcePath = new URL("
|
|
142
|
+
const sourcePath = new URL("../../../skills/subagent-delegation/SKILL.md", import.meta.url);
|
|
146
143
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
147
144
|
writeFileSync(targetPath, readFileSync(sourcePath, "utf8"), { mode: 0o644 });
|
|
148
145
|
return [targetPath];
|
|
@@ -181,52 +178,15 @@ function readJsonFile(filePath) {
|
|
|
181
178
|
throw new Error(`Unable to read ${filePath}: ${reason}`);
|
|
182
179
|
}
|
|
183
180
|
}
|
|
184
|
-
function updateForgeRelayAuth(update, env) {
|
|
181
|
+
async function updateForgeRelayAuth(update, env) {
|
|
185
182
|
const filePath = forgerelayAuthPath(env);
|
|
186
183
|
mkdirSync(forgerelayConfigDir(env), { recursive: true });
|
|
187
|
-
return
|
|
184
|
+
return withFileLock(`${filePath}.lock`, () => {
|
|
188
185
|
const auth = existsSync(filePath) ? readJsonFile(filePath) : {};
|
|
189
186
|
writeJsonFile(filePath, update(auth), 0o600);
|
|
190
187
|
return filePath;
|
|
191
188
|
});
|
|
192
189
|
}
|
|
193
|
-
function withAuthFileLock(filePath, operation) {
|
|
194
|
-
const lockPath = `${filePath}.lock`;
|
|
195
|
-
const deadline = Date.now() + AUTH_LOCK_TIMEOUT_MS;
|
|
196
|
-
for (;;) {
|
|
197
|
-
try {
|
|
198
|
-
const fd = openSync(lockPath, "wx", 0o600);
|
|
199
|
-
closeSync(fd);
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
const code = error.code;
|
|
204
|
-
if (code !== "EEXIST")
|
|
205
|
-
throw error;
|
|
206
|
-
try {
|
|
207
|
-
if (Date.now() - statSync(lockPath).mtimeMs > AUTH_LOCK_STALE_MS) {
|
|
208
|
-
rmSync(lockPath, { force: true });
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (statError) {
|
|
213
|
-
if (statError.code === "ENOENT")
|
|
214
|
-
continue;
|
|
215
|
-
throw statError;
|
|
216
|
-
}
|
|
217
|
-
if (Date.now() >= deadline) {
|
|
218
|
-
throw new Error(`Timed out waiting for ForgeRelay auth lock: ${lockPath}`);
|
|
219
|
-
}
|
|
220
|
-
Atomics.wait(AUTH_LOCK_SLEEP, 0, 0, AUTH_LOCK_RETRY_MS);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
return operation();
|
|
225
|
-
}
|
|
226
|
-
finally {
|
|
227
|
-
rmSync(lockPath, { force: true });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
190
|
function writeJsonFile(filePath, value, mode) {
|
|
231
191
|
const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
232
192
|
try {
|
|
@@ -5,7 +5,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
|
5
5
|
import * as schema from "./schema.js";
|
|
6
6
|
import { migrateDatabase } from "./migrations.js";
|
|
7
7
|
export function databasePath(stateDir) {
|
|
8
|
-
return join(stateDir, "
|
|
8
|
+
return join(stateDir, "forgerelay.sqlite");
|
|
9
9
|
}
|
|
10
10
|
export function openDatabase(stateDir) {
|
|
11
11
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
@@ -84,18 +84,28 @@ const migrations = [
|
|
|
84
84
|
name: "workspace-context-components",
|
|
85
85
|
up: migrateWorkspaceContextComponents,
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
version: 18,
|
|
89
|
+
name: "file-backed-activity-audit",
|
|
90
|
+
up: migrateFileBackedActivityAudit,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
version: 19,
|
|
94
|
+
name: "file-backed-bash-metadata",
|
|
95
|
+
up: migrateFileBackedBashMetadata,
|
|
96
|
+
},
|
|
87
97
|
];
|
|
88
98
|
export function migrateDatabase(sqlite) {
|
|
89
99
|
const migrate = sqlite.transaction(() => {
|
|
90
100
|
sqlite.exec(`
|
|
91
|
-
create table if not exists
|
|
101
|
+
create table if not exists forgerelay_schema_migrations (
|
|
92
102
|
version integer primary key,
|
|
93
103
|
name text not null,
|
|
94
104
|
applied_at text not null
|
|
95
105
|
);
|
|
96
106
|
`);
|
|
97
|
-
const applied = new Set(sqlite.prepare("select version from
|
|
98
|
-
const recordMigration = sqlite.prepare("insert into
|
|
107
|
+
const applied = new Set(sqlite.prepare("select version from forgerelay_schema_migrations").all().map((row) => row.version));
|
|
108
|
+
const recordMigration = sqlite.prepare("insert into forgerelay_schema_migrations (version, name, applied_at) values (?, ?, ?)");
|
|
99
109
|
for (const migration of migrations) {
|
|
100
110
|
if (applied.has(migration.version))
|
|
101
111
|
continue;
|
|
@@ -418,6 +428,27 @@ function migrateActivityHostTurnWorkspace(sqlite) {
|
|
|
418
428
|
on activity_host_turns(conversation_scope_id, workspace_id, created_at desc);
|
|
419
429
|
`);
|
|
420
430
|
}
|
|
431
|
+
function migrateFileBackedActivityAudit(sqlite) {
|
|
432
|
+
if (tableExists(sqlite, "activity_audit_events")) {
|
|
433
|
+
addColumnIfMissing(sqlite, "activity_audit_events", "payload_file", "text");
|
|
434
|
+
addColumnIfMissing(sqlite, "activity_audit_events", "payload_offset", "integer");
|
|
435
|
+
addColumnIfMissing(sqlite, "activity_audit_events", "payload_length", "integer");
|
|
436
|
+
}
|
|
437
|
+
if (tableExists(sqlite, "bash_output_streams")) {
|
|
438
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "log_file", "text");
|
|
439
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "output_bytes", "integer not null default 0");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function migrateFileBackedBashMetadata(sqlite) {
|
|
443
|
+
if (!tableExists(sqlite, "bash_output_streams"))
|
|
444
|
+
return;
|
|
445
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "command_file", "text");
|
|
446
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "command_offset", "integer");
|
|
447
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "command_length", "integer");
|
|
448
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "error_file", "text");
|
|
449
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "error_offset", "integer");
|
|
450
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "error_length", "integer");
|
|
451
|
+
}
|
|
421
452
|
function tableExists(sqlite, table) {
|
|
422
453
|
return Boolean(sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(table));
|
|
423
454
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
const DEFAULT_RETRY_MS = 10;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
5
|
+
const DEFAULT_STALE_MS = 30_000;
|
|
6
|
+
/**
|
|
7
|
+
* Cross-process lock acquisition must yield the event loop: these locks guard
|
|
8
|
+
* tiny JSON state updates, but a contended lock can otherwise freeze every MCP
|
|
9
|
+
* request handled by the same Node process. The owner token also prevents a
|
|
10
|
+
* process that lost an abandoned lock from deleting a later owner's lock when
|
|
11
|
+
* it eventually reaches cleanup.
|
|
12
|
+
*/
|
|
13
|
+
export async function withFileLock(lockPath, operation, options = {}) {
|
|
14
|
+
const retryMs = positiveInteger(options.retryMs, DEFAULT_RETRY_MS, "retryMs");
|
|
15
|
+
const timeoutMs = positiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs");
|
|
16
|
+
const staleMs = positiveInteger(options.staleMs, DEFAULT_STALE_MS, "staleMs");
|
|
17
|
+
const mode = options.mode ?? 0o600;
|
|
18
|
+
const owner = {
|
|
19
|
+
pid: process.pid,
|
|
20
|
+
token: randomBytes(16).toString("hex"),
|
|
21
|
+
};
|
|
22
|
+
const serializedOwner = `${JSON.stringify(owner)}\n`;
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
const handle = await open(lockPath, "wx", mode);
|
|
27
|
+
try {
|
|
28
|
+
await handle.writeFile(serializedOwner, "utf8");
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await handle.close();
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code !== "EEXIST")
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
if (await reclaimAbandonedLock(lockPath, staleMs))
|
|
40
|
+
continue;
|
|
41
|
+
if (Date.now() >= deadline) {
|
|
42
|
+
throw new Error(`Timed out waiting for file lock: ${lockPath}`);
|
|
43
|
+
}
|
|
44
|
+
await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return await operation();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await releaseOwnedLock(lockPath, owner);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function reclaimAbandonedLock(lockPath, staleMs) {
|
|
54
|
+
let owner;
|
|
55
|
+
let ageMs;
|
|
56
|
+
try {
|
|
57
|
+
const [contents, info] = await Promise.all([
|
|
58
|
+
readFile(lockPath, "utf8"),
|
|
59
|
+
stat(lockPath),
|
|
60
|
+
]);
|
|
61
|
+
owner = parseOwner(contents);
|
|
62
|
+
ageMs = Date.now() - info.mtimeMs;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code === "ENOENT")
|
|
66
|
+
return true;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
// A valid live PID is stronger evidence than mtime. This avoids declaring a
|
|
70
|
+
// long but healthy operation stale merely because the event loop was paused.
|
|
71
|
+
if (owner && processIsAlive(owner.pid))
|
|
72
|
+
return false;
|
|
73
|
+
if (!owner && ageMs <= staleMs)
|
|
74
|
+
return false;
|
|
75
|
+
const stalePath = `${lockPath}.${process.pid}.${randomBytes(8).toString("hex")}.stale`;
|
|
76
|
+
try {
|
|
77
|
+
await rename(lockPath, stalePath);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code === "ENOENT")
|
|
81
|
+
return true;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
await rm(stalePath, { force: true });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
async function releaseOwnedLock(lockPath, owner) {
|
|
88
|
+
let current;
|
|
89
|
+
try {
|
|
90
|
+
current = parseOwner(await readFile(lockPath, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === "ENOENT")
|
|
94
|
+
return;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
if (!current || current.pid !== owner.pid || current.token !== owner.token)
|
|
98
|
+
return;
|
|
99
|
+
await rm(lockPath, { force: true });
|
|
100
|
+
}
|
|
101
|
+
function parseOwner(contents) {
|
|
102
|
+
try {
|
|
103
|
+
const value = JSON.parse(contents);
|
|
104
|
+
return Number.isSafeInteger(value.pid) && (value.pid ?? 0) > 0 && typeof value.token === "string" && value.token
|
|
105
|
+
? { pid: value.pid, token: value.token }
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function processIsAlive(pid) {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(pid, 0);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return error.code !== "ESRCH";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function positiveInteger(value, fallback, name) {
|
|
122
|
+
const resolved = value ?? fallback;
|
|
123
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
124
|
+
throw new Error(`File lock ${name} must be a positive integer.`);
|
|
125
|
+
}
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
function delay(ms) {
|
|
129
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.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 "../config/config.js";
|
|
15
|
+
import { parseHookConfig } from "../../mcp/hooks/hooks.js";
|
|
16
|
+
import { CodeIntelligenceManager } from "../../lsp/runtime/manager.js";
|
|
17
|
+
import { ProcessManager } from "../../mcp/process/process-sessions.js";
|
|
18
|
+
import { createReviewCheckpointManager } from "../../workspaces/review/review-checkpoints.js";
|
|
19
|
+
import { createMcpServer } from "../../server.js";
|
|
20
|
+
import { SqliteWorkspaceStore } from "../../workspaces/state/workspace-store.js";
|
|
21
|
+
import { WorkspaceRegistry } from "../../workspaces.js";
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
/**
|
|
24
|
+
* Build the real in-memory MCP seam used by server integration tests. The
|
|
25
|
+
* fixture owns all durable stores and runtime managers so tests exercise the
|
|
26
|
+
* same composition path while keeping filesystem state isolated per test.
|
|
27
|
+
*/
|
|
28
|
+
export async function fixture(t, options = {}) {
|
|
29
|
+
const root = await mkdtemp(join(tmpdir(), "forgerelay-server-test-"));
|
|
30
|
+
const project = join(root, "project");
|
|
31
|
+
const agentDir = join(root, "agent");
|
|
32
|
+
const stateDir = join(root, ".state");
|
|
33
|
+
await mkdir(join(project, ".forgerelay", "agents"), { recursive: true });
|
|
34
|
+
await mkdir(agentDir, { recursive: true });
|
|
35
|
+
await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
|
|
36
|
+
await writeFile(join(project, "AGENTS.md"), "project instructions\n");
|
|
37
|
+
await writeFile(join(project, ".forgerelay", "agents", "reviewer.md"), [
|
|
38
|
+
"---",
|
|
39
|
+
"name: reviewer",
|
|
40
|
+
"description: Reviews project changes.",
|
|
41
|
+
"provider: codex",
|
|
42
|
+
"---",
|
|
43
|
+
"Review changes.",
|
|
44
|
+
].join("\n"));
|
|
45
|
+
if (options.git) {
|
|
46
|
+
await writeFile(join(project, "README.md"), "hello\n");
|
|
47
|
+
await git(project, ["init"]);
|
|
48
|
+
await git(project, ["config", "user.email", "forgerelay@example.com"]);
|
|
49
|
+
await git(project, ["config", "user.name", "ForgeRelay Test"]);
|
|
50
|
+
await git(project, ["add", "."]);
|
|
51
|
+
await git(project, ["commit", "-m", "Initial commit"]);
|
|
52
|
+
}
|
|
53
|
+
const loadedConfig = loadConfig({
|
|
54
|
+
FORGERELAY_CONFIG_DIR: join(root, ".config"),
|
|
55
|
+
FORGERELAY_STATE_DIR: stateDir,
|
|
56
|
+
FORGERELAY_ALLOWED_ROOTS: root,
|
|
57
|
+
FORGERELAY_WORKTREE_ROOT: join(root, ".worktrees"),
|
|
58
|
+
FORGERELAY_AGENT_DIR: agentDir,
|
|
59
|
+
FORGERELAY_WIDGETS: "full",
|
|
60
|
+
FORGERELAY_TOOL_MODE: "full",
|
|
61
|
+
FORGERELAY_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
|
|
62
|
+
PORT: "1",
|
|
63
|
+
...options.env,
|
|
64
|
+
});
|
|
65
|
+
const config = options.hooks
|
|
66
|
+
? { ...loadedConfig, hooks: parseHookConfig(options.hooks) }
|
|
67
|
+
: loadedConfig;
|
|
68
|
+
const store = new SqliteWorkspaceStore(stateDir);
|
|
69
|
+
const workspaces = new WorkspaceRegistry(config, store);
|
|
70
|
+
const auditStore = new ActivityAuditStore(stateDir);
|
|
71
|
+
const bashOutputStore = new BashOutputStore(stateDir);
|
|
72
|
+
let hostTurnSequence = 0;
|
|
73
|
+
const hostTurnStore = new HostTurnStore(stateDir, {
|
|
74
|
+
turnId: () => `turn_host_test_${++hostTurnSequence}`,
|
|
75
|
+
});
|
|
76
|
+
const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
|
|
77
|
+
const processSessions = options.processSessions ?? new ProcessManager({ outputAudit: bashOutputStore });
|
|
78
|
+
let activitySequence = 0;
|
|
79
|
+
let turnSequence = 0;
|
|
80
|
+
const activityLifecycle = new ActivityLifecycle(auditStore, {
|
|
81
|
+
activityId: () => `act_test_${++activitySequence}`,
|
|
82
|
+
turnId: () => `turn_test_${++turnSequence}`,
|
|
83
|
+
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
84
|
+
});
|
|
85
|
+
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
86
|
+
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], options.incomingArtifactAdapters ?? [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
|
|
87
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
88
|
+
const client = new Client({ name: "forgerelay-test-client", version: "1.0.0" });
|
|
89
|
+
await Promise.all([
|
|
90
|
+
client.connect(clientTransport),
|
|
91
|
+
server.connect(serverTransport),
|
|
92
|
+
]);
|
|
93
|
+
let closed = false;
|
|
94
|
+
const close = async () => {
|
|
95
|
+
if (closed)
|
|
96
|
+
return;
|
|
97
|
+
closed = true;
|
|
98
|
+
await client.close();
|
|
99
|
+
await server.close();
|
|
100
|
+
await codeIntelligence.shutdown();
|
|
101
|
+
processSessions.shutdown();
|
|
102
|
+
hostTurnStore.close();
|
|
103
|
+
bashOutputStore.close();
|
|
104
|
+
auditStore.close();
|
|
105
|
+
store.close();
|
|
106
|
+
};
|
|
107
|
+
t.after(async () => {
|
|
108
|
+
await close();
|
|
109
|
+
await rm(root, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
client,
|
|
113
|
+
project,
|
|
114
|
+
config,
|
|
115
|
+
stateDir,
|
|
116
|
+
store,
|
|
117
|
+
workspaces,
|
|
118
|
+
processSessions,
|
|
119
|
+
activityLifecycle,
|
|
120
|
+
codeIntelligence,
|
|
121
|
+
auditStore,
|
|
122
|
+
bashOutputStore,
|
|
123
|
+
hostTurnStore,
|
|
124
|
+
activityQueries,
|
|
125
|
+
close,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function git(cwd, args) {
|
|
129
|
+
await execFileAsync("git", args, { cwd });
|
|
130
|
+
}
|
|
131
|
+
export async function callOpen(client, path, conversationScopeId, mode, newWorktree) {
|
|
132
|
+
const params = {
|
|
133
|
+
name: "open_workspace",
|
|
134
|
+
arguments: {
|
|
135
|
+
path,
|
|
136
|
+
...(mode ? { mode } : {}),
|
|
137
|
+
...(newWorktree ? { newWorktree: true } : {}),
|
|
138
|
+
},
|
|
139
|
+
...(conversationScopeId
|
|
140
|
+
? { _meta: { "openai/session": conversationScopeId } }
|
|
141
|
+
: {}),
|
|
142
|
+
};
|
|
143
|
+
return client.callTool(params);
|
|
144
|
+
}
|
|
145
|
+
export function structuredContent(result) {
|
|
146
|
+
assert.ok(result.structuredContent);
|
|
147
|
+
return result.structuredContent;
|
|
148
|
+
}
|
|
149
|
+
export function responseText(result) {
|
|
150
|
+
const content = result.content;
|
|
151
|
+
assert.ok(Array.isArray(content));
|
|
152
|
+
const first = content[0];
|
|
153
|
+
assert.equal(first?.type, "text");
|
|
154
|
+
assert.equal(typeof first?.text, "string");
|
|
155
|
+
return first?.text;
|
|
156
|
+
}
|
|
157
|
+
export function allResponseText(result) {
|
|
158
|
+
const content = result.content;
|
|
159
|
+
assert.ok(Array.isArray(content));
|
|
160
|
+
return content
|
|
161
|
+
.filter((entry) => typeof entry === "object" &&
|
|
162
|
+
entry !== null &&
|
|
163
|
+
entry.type === "text" &&
|
|
164
|
+
typeof entry.text === "string")
|
|
165
|
+
.map((entry) => entry.text)
|
|
166
|
+
.join("\n");
|
|
167
|
+
}
|
|
168
|
+
export async function waitForCompletedProcess(processSessions) {
|
|
169
|
+
const deadline = performance.now() + 5_000;
|
|
170
|
+
while (processSessions.stats().completed === 0 && performance.now() < deadline) {
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
172
|
+
}
|
|
173
|
+
assert.equal(processSessions.stats().completed, 1);
|
|
174
|
+
}
|
|
175
|
+
export async function waitForToolText(client, params, expected, timeoutMs = 5_000) {
|
|
176
|
+
const deadline = performance.now() + timeoutMs;
|
|
177
|
+
let result = await client.callTool(params);
|
|
178
|
+
while (!expected.test(allResponseText(result)) && performance.now() < deadline) {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
180
|
+
result = await client.callTool(params);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
export function responseCard(result) {
|
|
185
|
+
const metadata = result._meta;
|
|
186
|
+
assert.ok(metadata && typeof metadata === "object");
|
|
187
|
+
const card = metadata.card;
|
|
188
|
+
assert.ok(card && typeof card === "object");
|
|
189
|
+
return card;
|
|
190
|
+
}
|