@akira-tl/forgerelay 0.8.9 → 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 +20 -0
- package/capabilities/code-intelligence/GUIDE.md +7 -3
- package/capabilities/subagents/GUIDE.md +9 -0
- package/capabilities/workspace/workspace-tasks/GUIDE.md +6 -0
- package/dist/cli/init.js +180 -0
- package/dist/cli/setup-support.js +65 -0
- package/dist/cli.js +9 -94
- package/dist/lsp/code-intelligence.js +17 -3
- 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 +5 -2
- package/dist/mcp/process/process-platform.js +21 -9
- package/dist/mcp/process/process-sessions.js +3 -3
- package/dist/mcp/process/tools.js +6 -6
- package/dist/mcp/server/core/capability-registry.js +7 -2
- package/dist/mcp/server/core/tool-support.js +8 -5
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +74 -19
- package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +8 -7
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +28 -8
- package/dist/mcp/server/workspace/runtime/workspace-open.js +4 -1
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +3 -3
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/runtime/config/config.js +1 -0
- package/dist/runtime/config/user-config.js +1 -10
- package/dist/server.js +36 -6
- package/dist/workspaces/context.js +35 -0
- package/dist/workspaces/relay/transport/remote-transport.js +30 -3
- package/dist/workspaces/resources/resource-monitor.js +377 -0
- package/dist/workspaces/resources/skills.js +13 -15
- package/dist/workspaces/sessions.js +3 -0
- package/dist/workspaces.js +11 -0
- package/docs/chatgpt-coding-workflow.md +4 -3
- package/docs/configuration.md +14 -9
- package/package.json +2 -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/skills/subagent-delegation/SKILL.md +0 -132
|
@@ -4,6 +4,7 @@ export class DiagnosticSnapshotStore {
|
|
|
4
4
|
maxDiagnosticsPerDocument;
|
|
5
5
|
pushSnapshots = new Map();
|
|
6
6
|
pullSnapshots = new Map();
|
|
7
|
+
pushWaiters = new Map();
|
|
7
8
|
pushObserved = false;
|
|
8
9
|
constructor(maxDocuments, maxDiagnosticsPerDocument) {
|
|
9
10
|
this.maxDocuments = maxDocuments;
|
|
@@ -11,10 +12,13 @@ export class DiagnosticSnapshotStore {
|
|
|
11
12
|
}
|
|
12
13
|
capturePush(params, document, encoding) {
|
|
13
14
|
this.pushObserved = true;
|
|
14
|
-
if (!document)
|
|
15
|
+
if (!document) {
|
|
16
|
+
this.notifyPushWaiters(params.uri, true);
|
|
15
17
|
return;
|
|
18
|
+
}
|
|
16
19
|
const snapshot = this.normalizeSnapshot(params.diagnostics, document, encoding, params.version === undefined ? {} : { publishedVersion: params.version });
|
|
17
20
|
this.setBounded(this.pushSnapshots, params.uri, snapshot);
|
|
21
|
+
this.notifyPushWaiters(params.uri, true);
|
|
18
22
|
}
|
|
19
23
|
capturePull(diagnostics, document, encoding, resultId) {
|
|
20
24
|
const snapshot = this.normalizeSnapshot(diagnostics, document, encoding, resultId === undefined ? {} : { resultId });
|
|
@@ -41,12 +45,52 @@ export class DiagnosticSnapshotStore {
|
|
|
41
45
|
readPull(document, limit) {
|
|
42
46
|
return this.read(this.pullSnapshots, document, limit, false);
|
|
43
47
|
}
|
|
48
|
+
async waitForFreshPush(document, limit, timeoutMs, signal) {
|
|
49
|
+
let snapshot = this.readPush(document, limit);
|
|
50
|
+
const deadline = Date.now() + timeoutMs;
|
|
51
|
+
while (snapshot.freshness.state === "missing" || snapshot.freshness.state === "stale") {
|
|
52
|
+
if (signal?.aborted)
|
|
53
|
+
break;
|
|
54
|
+
const remaining = deadline - Date.now();
|
|
55
|
+
if (remaining <= 0 || !await this.waitForPush(document.uri, remaining, signal))
|
|
56
|
+
break;
|
|
57
|
+
snapshot = this.readPush(document, limit);
|
|
58
|
+
}
|
|
59
|
+
return snapshot;
|
|
60
|
+
}
|
|
44
61
|
hasObservedPushDiagnostics() {
|
|
45
62
|
return this.pushObserved;
|
|
46
63
|
}
|
|
64
|
+
waitForPush(uri, timeoutMs, signal) {
|
|
65
|
+
if (signal?.aborted)
|
|
66
|
+
return Promise.resolve(false);
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
const waiters = this.pushWaiters.get(uri) ?? new Set();
|
|
70
|
+
const finish = (observed) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
signal?.removeEventListener("abort", onAbort);
|
|
76
|
+
waiters.delete(finish);
|
|
77
|
+
if (waiters.size === 0)
|
|
78
|
+
this.pushWaiters.delete(uri);
|
|
79
|
+
resolve(observed);
|
|
80
|
+
};
|
|
81
|
+
const onAbort = () => finish(false);
|
|
82
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
83
|
+
timer.unref();
|
|
84
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
85
|
+
waiters.add(finish);
|
|
86
|
+
this.pushWaiters.set(uri, waiters);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
47
89
|
clear() {
|
|
48
90
|
this.pushSnapshots.clear();
|
|
49
91
|
this.pullSnapshots.clear();
|
|
92
|
+
for (const uri of [...this.pushWaiters.keys()])
|
|
93
|
+
this.notifyPushWaiters(uri, false);
|
|
50
94
|
this.pushObserved = false;
|
|
51
95
|
}
|
|
52
96
|
get size() {
|
|
@@ -70,6 +114,13 @@ export class DiagnosticSnapshotStore {
|
|
|
70
114
|
...metadata,
|
|
71
115
|
};
|
|
72
116
|
}
|
|
117
|
+
notifyPushWaiters(uri, observed) {
|
|
118
|
+
const waiters = this.pushWaiters.get(uri);
|
|
119
|
+
if (!waiters)
|
|
120
|
+
return;
|
|
121
|
+
for (const waiter of [...waiters])
|
|
122
|
+
waiter(observed);
|
|
123
|
+
}
|
|
73
124
|
setBounded(snapshots, uri, snapshot) {
|
|
74
125
|
snapshots.delete(uri);
|
|
75
126
|
snapshots.set(uri, snapshot);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { delimiter, join, resolve } from "node:path";
|
|
4
|
+
const MANAGED_LANGUAGE_SERVERS = {
|
|
5
|
+
typescript: {
|
|
6
|
+
id: "typescript",
|
|
7
|
+
label: "TypeScript / JavaScript",
|
|
8
|
+
executable: "typescript-language-server",
|
|
9
|
+
packages: ["typescript-language-server@6", "typescript@6"],
|
|
10
|
+
},
|
|
11
|
+
pyright: {
|
|
12
|
+
id: "pyright",
|
|
13
|
+
label: "Python (Pyright)",
|
|
14
|
+
executable: "pyright-langserver",
|
|
15
|
+
packages: ["pyright@1"],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export function managedLanguageServerRoot(configDir) {
|
|
19
|
+
return join(configDir, "language-servers");
|
|
20
|
+
}
|
|
21
|
+
export function managedLanguageServerBinDir(configDir) {
|
|
22
|
+
return join(managedLanguageServerRoot(configDir), "node_modules", ".bin");
|
|
23
|
+
}
|
|
24
|
+
export function managedLanguageServerExecutablePath(configDir, id, platform = process.platform) {
|
|
25
|
+
const bin = managedLanguageServerBinDir(configDir);
|
|
26
|
+
const executable = MANAGED_LANGUAGE_SERVERS[id].executable;
|
|
27
|
+
const extensions = platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
28
|
+
return extensions
|
|
29
|
+
.map((extension) => join(bin, `${executable}${extension}`))
|
|
30
|
+
.find((path) => existsSync(path));
|
|
31
|
+
}
|
|
32
|
+
export function managedLanguageServerIdForCommand(configDir, command, platform = process.platform) {
|
|
33
|
+
return supportedManagedLanguageServers().find((id) => {
|
|
34
|
+
const executable = managedLanguageServerExecutablePath(configDir, id, platform);
|
|
35
|
+
if (executable === undefined)
|
|
36
|
+
return false;
|
|
37
|
+
const executablePath = resolve(executable);
|
|
38
|
+
const commandPath = resolve(command);
|
|
39
|
+
return platform === "win32"
|
|
40
|
+
? executablePath.toLowerCase() === commandPath.toLowerCase()
|
|
41
|
+
: executablePath === commandPath;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function managedTypeScriptTsserverPath(configDir) {
|
|
45
|
+
const path = join(managedLanguageServerRoot(configDir), "node_modules", "typescript", "lib", "tsserver.js");
|
|
46
|
+
return existsSync(path) ? path : undefined;
|
|
47
|
+
}
|
|
48
|
+
export function managedLanguageServerRuntimeIdentity(configDir, id) {
|
|
49
|
+
const packageNames = id === "typescript"
|
|
50
|
+
? ["typescript-language-server", "typescript"]
|
|
51
|
+
: ["pyright"];
|
|
52
|
+
try {
|
|
53
|
+
const versions = packageNames.map((name) => {
|
|
54
|
+
const packageJson = JSON.parse(readFileSync(join(managedLanguageServerRoot(configDir), "node_modules", name, "package.json"), "utf8"));
|
|
55
|
+
if (!packageJson.version)
|
|
56
|
+
throw new Error(`Missing version for ${name}`);
|
|
57
|
+
return [name, packageJson.version];
|
|
58
|
+
});
|
|
59
|
+
return JSON.stringify(versions);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function withManagedLanguageServerPath(env, configDir) {
|
|
66
|
+
if (!configDir)
|
|
67
|
+
return { ...env };
|
|
68
|
+
const bin = managedLanguageServerBinDir(configDir);
|
|
69
|
+
return {
|
|
70
|
+
...env,
|
|
71
|
+
PATH: [bin, env.PATH].filter((value) => Boolean(value)).join(delimiter),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function managedLanguageServerOptions() {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
value: "typescript",
|
|
78
|
+
label: MANAGED_LANGUAGE_SERVERS.typescript.label,
|
|
79
|
+
hint: "Installs typescript-language-server and TypeScript into ForgeRelay's private config directory.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
value: "pyright",
|
|
83
|
+
label: MANAGED_LANGUAGE_SERVERS.pyright.label,
|
|
84
|
+
hint: "Installs Pyright into ForgeRelay's private config directory.",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
export function supportedManagedLanguageServers() {
|
|
89
|
+
return Object.keys(MANAGED_LANGUAGE_SERVERS);
|
|
90
|
+
}
|
|
91
|
+
export function installedManagedLanguageServers(configDir) {
|
|
92
|
+
return supportedManagedLanguageServers()
|
|
93
|
+
.filter((id) => managedExecutableExists(configDir, MANAGED_LANGUAGE_SERVERS[id].executable));
|
|
94
|
+
}
|
|
95
|
+
export function managedLanguageServerPackages(ids) {
|
|
96
|
+
const packages = new Set();
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
for (const packageSpec of MANAGED_LANGUAGE_SERVERS[id].packages)
|
|
99
|
+
packages.add(packageSpec);
|
|
100
|
+
}
|
|
101
|
+
return [...packages];
|
|
102
|
+
}
|
|
103
|
+
export async function installManagedLanguageServers(ids, configDir, runNpm = defaultNpmRunner) {
|
|
104
|
+
const selected = Array.from(new Set(ids));
|
|
105
|
+
const root = managedLanguageServerRoot(configDir);
|
|
106
|
+
const packages = managedLanguageServerPackages(selected);
|
|
107
|
+
if (packages.length === 0)
|
|
108
|
+
return { installed: [], packages: [], root };
|
|
109
|
+
mkdirSync(root, { recursive: true });
|
|
110
|
+
const packageJsonPath = join(root, "package.json");
|
|
111
|
+
if (!existsSync(packageJsonPath)) {
|
|
112
|
+
writeFileSync(packageJsonPath, JSON.stringify({
|
|
113
|
+
name: "forgerelay-managed-language-servers",
|
|
114
|
+
private: true,
|
|
115
|
+
description: "ForgeRelay-managed optional Language Servers. Do not publish.",
|
|
116
|
+
}, null, 2) + "\n");
|
|
117
|
+
}
|
|
118
|
+
await runNpm([
|
|
119
|
+
"install",
|
|
120
|
+
"--prefix",
|
|
121
|
+
root,
|
|
122
|
+
"--save-exact",
|
|
123
|
+
"--no-audit",
|
|
124
|
+
"--no-fund",
|
|
125
|
+
...packages,
|
|
126
|
+
]);
|
|
127
|
+
for (const id of selected)
|
|
128
|
+
assertManagedLanguageServerInstall(configDir, id);
|
|
129
|
+
return { installed: selected, packages, root };
|
|
130
|
+
}
|
|
131
|
+
function assertManagedLanguageServerInstall(configDir, id) {
|
|
132
|
+
if (!managedLanguageServerExecutablePath(configDir, id)) {
|
|
133
|
+
throw new Error(`Managed Language Server ${id} installed without its expected executable.`);
|
|
134
|
+
}
|
|
135
|
+
if (id === "typescript" && !managedTypeScriptTsserverPath(configDir)) {
|
|
136
|
+
throw new Error("Managed TypeScript Language Server installed without a compatible TypeScript tsserver.js. " +
|
|
137
|
+
"ForgeRelay requires a tsserver-based TypeScript package for typescript-language-server.");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function managedExecutableExists(configDir, executable) {
|
|
141
|
+
const bin = managedLanguageServerBinDir(configDir);
|
|
142
|
+
if (process.platform === "win32") {
|
|
143
|
+
return [".cmd", ".exe", ".bat", ""].some((extension) => existsSync(join(bin, `${executable}${extension}`)));
|
|
144
|
+
}
|
|
145
|
+
return existsSync(join(bin, executable));
|
|
146
|
+
}
|
|
147
|
+
function defaultNpmRunner(args) {
|
|
148
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const child = spawn(command, args, {
|
|
151
|
+
env: process.env,
|
|
152
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
shell: process.platform === "win32",
|
|
155
|
+
});
|
|
156
|
+
let stdout = "";
|
|
157
|
+
let stderr = "";
|
|
158
|
+
child.stdout?.setEncoding("utf8");
|
|
159
|
+
child.stderr?.setEncoding("utf8");
|
|
160
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
161
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
162
|
+
child.once("error", reject);
|
|
163
|
+
child.once("close", (code) => {
|
|
164
|
+
if (code === 0) {
|
|
165
|
+
resolve();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const detail = (stderr || stdout).trim();
|
|
169
|
+
reject(new Error(`npm install for managed Language Servers failed with exit ${code ?? "unknown"}${detail ? `: ${detail}` : ""}`));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -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);
|
|
@@ -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({
|
|
@@ -11,23 +11,35 @@ const defaultProcessTreeRuntime = {
|
|
|
11
11
|
return !result.error && result.status === 0;
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
|
-
const
|
|
15
|
-
const POSIX_SHELLS = new Set(["ash", "dash", "sh"]);
|
|
14
|
+
const POSIX_SHELLS = new Set(["ash", "dash", "ksh", "sh"]);
|
|
16
15
|
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
17
16
|
if (platform === "win32") {
|
|
18
17
|
return {
|
|
19
18
|
executable: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
|
|
20
|
-
|
|
19
|
+
// Match Node's native `spawn(command, { shell: cmd.exe })` quoting.
|
|
20
|
+
// cmd.exe /S applies special quote stripping, so the whole command must
|
|
21
|
+
// be wrapped even when the executable inside it is already quoted.
|
|
22
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
21
23
|
windowsVerbatimArguments: true,
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Agent and Hook commands must not source the user's interactive/login shell
|
|
27
|
+
// configuration. ForgeRelay already inherits PATH and other environment from
|
|
28
|
+
// the process that launched the server; re-running zsh/bash as a login shell
|
|
29
|
+
// can inject prompts, banners, aliases, plugins, or other user-only behavior.
|
|
30
|
+
const configuredShell = environment.FORGERELAY_COMMAND_SHELL?.trim();
|
|
31
|
+
const executable = configuredShell || (platform === "linux" || platform === "darwin"
|
|
32
|
+
? "/bin/bash"
|
|
33
|
+
: "/bin/sh");
|
|
34
|
+
const shellName = basename(executable);
|
|
35
|
+
if (shellName === "bash") {
|
|
36
|
+
return { executable, args: ["--noprofile", "--norc", "-c", command] };
|
|
28
37
|
}
|
|
29
|
-
if (
|
|
30
|
-
return { executable
|
|
38
|
+
if (shellName === "zsh") {
|
|
39
|
+
return { executable, args: ["-f", "-c", command] };
|
|
40
|
+
}
|
|
41
|
+
if (POSIX_SHELLS.has(shellName)) {
|
|
42
|
+
return { executable, args: ["-c", command] };
|
|
31
43
|
}
|
|
32
44
|
return { executable: "/bin/sh", args: ["-c", command] };
|
|
33
45
|
}
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { resolveShellCommand, terminateProcessTree } from "./process-platform.js";
|
|
3
3
|
const DEFAULT_EXEC_YIELD_MS = 10_000;
|
|
4
4
|
const DEFAULT_INTERACTIVE_YIELD_MS = 250;
|
|
5
|
-
const DEFAULT_POLL_YIELD_MS =
|
|
5
|
+
export const DEFAULT_POLL_YIELD_MS = 60_000;
|
|
6
6
|
const MAX_START_YIELD_MS = 300_000;
|
|
7
7
|
const MAX_COMMAND_YIELD_MS = 300_000;
|
|
8
8
|
const MAX_POLL_YIELD_MS = 300_000;
|
|
@@ -437,7 +437,7 @@ export class ProcessManager {
|
|
|
437
437
|
startPipe(processEntry, input) {
|
|
438
438
|
const shell = resolveShellCommand(input.command);
|
|
439
439
|
const detached = process.platform !== "win32";
|
|
440
|
-
const child = spawn(
|
|
440
|
+
const child = spawn(shell.executable, shell.args, {
|
|
441
441
|
cwd: input.cwd,
|
|
442
442
|
env: processEnvironment({
|
|
443
443
|
workspaceId: input.workspaceId,
|
|
@@ -446,8 +446,8 @@ export class ProcessManager {
|
|
|
446
446
|
}),
|
|
447
447
|
stdio: "pipe",
|
|
448
448
|
windowsHide: true,
|
|
449
|
+
windowsVerbatimArguments: shell.windowsVerbatimArguments,
|
|
449
450
|
detached,
|
|
450
|
-
shell: shell.executable,
|
|
451
451
|
});
|
|
452
452
|
processEntry.process = {
|
|
453
453
|
write: (data) => child.stdin.write(data),
|
|
@@ -40,7 +40,7 @@ function registerBashTool(options) {
|
|
|
40
40
|
columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width for action=run, or resize width for action=process."),
|
|
41
41
|
rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height for action=run, or resize height for action=process."),
|
|
42
42
|
workingDirectory: z.string().optional().describe("For action=run, working directory relative to the workspace root. Defaults to the workspace root."),
|
|
43
|
-
yieldTimeMs: z.number().int().min(0).max(300_000).optional().describe("Maximum feedback wait, not a minimum delay: if the process finishes sooner, the call returns immediately. For long-running commands or wait-only action=process calls, set a long window near the Host request deadline (60000ms when supported) instead of repeated short polling. For action=run, use 0 for immediate background handoff; otherwise defaults to 10000ms. For action=process, wait-only calls default to
|
|
43
|
+
yieldTimeMs: z.number().int().min(0).max(300_000).optional().describe("Maximum feedback wait, not a minimum delay: if the process finishes sooner, the call returns immediately. For long-running commands or wait-only action=process calls, set a long window near the Host request deadline (60000ms when supported) instead of repeated short polling. For action=run, use 0 for immediate background handoff; otherwise defaults to 10000ms. For action=process, wait-only calls default to 60000ms and interaction to 250ms."),
|
|
44
44
|
timeoutMs: z.number().int().min(1).max(86_400_000).optional().describe("For action=run, total execution timeout from process start. On expiry ForgeRelay terminates the process. Omit for no ForgeRelay execution deadline."),
|
|
45
45
|
maxOutputTokens: z.number().int().positive().max(100_000).optional().describe("Approximate output token budget. Defaults to 10000."),
|
|
46
46
|
},
|
|
@@ -70,7 +70,7 @@ function registerBashTool(options) {
|
|
|
70
70
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
71
71
|
}, routing.hostScopeIdFor(extra._meta, extra.sessionId));
|
|
72
72
|
return action === "run"
|
|
73
|
-
? routing.presentSemantic(response, target)
|
|
73
|
+
? routing.presentSemantic(response, target, routing.hostScopeIdFor(extra._meta, extra.sessionId))
|
|
74
74
|
: routing.present(response, target);
|
|
75
75
|
}
|
|
76
76
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
@@ -91,7 +91,7 @@ function registerBashTool(options) {
|
|
|
91
91
|
yieldTimeMs,
|
|
92
92
|
timeoutMs,
|
|
93
93
|
maxOutputTokens,
|
|
94
|
-
}, executionContext), target);
|
|
94
|
+
}, executionContext), target, routing.hostScopeIdFor(extra._meta, extra.sessionId));
|
|
95
95
|
}
|
|
96
96
|
if (action === "output") {
|
|
97
97
|
if (!outputId)
|
|
@@ -202,7 +202,7 @@ function registerCodexProcessTools(options) {
|
|
|
202
202
|
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
203
203
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
204
204
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
205
|
-
}, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
205
|
+
}, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target, routing.hostScopeIdFor(extra._meta, extra.sessionId));
|
|
206
206
|
}
|
|
207
207
|
return routing.presentSemantic(await shellRun({
|
|
208
208
|
workspaceId: target.executionWorkspaceId,
|
|
@@ -215,7 +215,7 @@ function registerCodexProcessTools(options) {
|
|
|
215
215
|
yieldTimeMs,
|
|
216
216
|
timeoutMs,
|
|
217
217
|
maxOutputTokens,
|
|
218
|
-
}, context), target);
|
|
218
|
+
}, context), target, routing.hostScopeIdFor(extra._meta, extra.sessionId));
|
|
219
219
|
});
|
|
220
220
|
registerAppTool(server, "write_stdin", {
|
|
221
221
|
title: "Write to process",
|
|
@@ -229,7 +229,7 @@ function registerCodexProcessTools(options) {
|
|
|
229
229
|
chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
|
|
230
230
|
columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
|
|
231
231
|
rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
|
|
232
|
-
yieldTimeMs: z.number().int().min(0).max(300_000).optional().describe("Milliseconds to keep waiting before returning again, max 300000. Polling defaults to
|
|
232
|
+
yieldTimeMs: z.number().int().min(0).max(300_000).optional().describe("Milliseconds to keep waiting before returning again, max 300000. Polling defaults to 60000; interaction defaults to 250."),
|
|
233
233
|
maxOutputTokens: z.number().int().positive().max(100_000).optional().describe("Approximate output token budget. Defaults to 10000."),
|
|
234
234
|
},
|
|
235
235
|
outputSchema: processOutputSchema(),
|
|
@@ -241,6 +241,11 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
241
241
|
path: z.string().min(1),
|
|
242
242
|
limit: z.number().int().min(1).max(MAX_CODE_INTELLIGENCE_RESULT_LIMIT).optional(),
|
|
243
243
|
}).strict(),
|
|
244
|
+
z.object({ operation: z.literal("managed.status") }).strict(),
|
|
245
|
+
z.object({
|
|
246
|
+
operation: z.literal("managed.install"),
|
|
247
|
+
servers: z.array(z.enum(["typescript", "pyright"])).min(1),
|
|
248
|
+
}).strict(),
|
|
244
249
|
]);
|
|
245
250
|
return new CapabilityRegistry([
|
|
246
251
|
{
|
|
@@ -273,7 +278,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
273
278
|
...(dependencies.codeIntelligence
|
|
274
279
|
? [{
|
|
275
280
|
name: "code.intelligence",
|
|
276
|
-
description: "Read semantic code information
|
|
281
|
+
description: "Read semantic code information and, when explicitly enabled by the user, manage ForgeRelay-owned Language Servers for this instance.",
|
|
277
282
|
guideName: "code-intelligence",
|
|
278
283
|
readGuideBeforeFirstUse: true,
|
|
279
284
|
batchPolicy: "parallel",
|
|
@@ -300,7 +305,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
300
305
|
...(dependencies.subagentSession
|
|
301
306
|
? [{
|
|
302
307
|
name: "subagent.session",
|
|
303
|
-
description: "
|
|
308
|
+
description: "Delegate explicit work to provider-backed Subagent Sessions in the current Execution Workspace; disclose delegation and verify returned results before presenting them as final.",
|
|
304
309
|
guideName: "subagents",
|
|
305
310
|
readGuideBeforeFirstUse: true,
|
|
306
311
|
batchPolicy: "unsupported",
|
|
@@ -64,15 +64,18 @@ export function textBlock(text) {
|
|
|
64
64
|
return { type: "text", text };
|
|
65
65
|
}
|
|
66
66
|
export function attachWorkspaceTaskReminder(result, reminder) {
|
|
67
|
-
|
|
67
|
+
return attachWorkspaceNotice(result, reminder);
|
|
68
|
+
}
|
|
69
|
+
export function attachWorkspaceContextUpdate(result, update) {
|
|
70
|
+
return attachWorkspaceNotice(result, update);
|
|
71
|
+
}
|
|
72
|
+
function attachWorkspaceNotice(result, notice) {
|
|
73
|
+
if (!notice || toolResultIsError(result) || typeof result !== "object" || result === null)
|
|
68
74
|
return result;
|
|
69
75
|
const content = result.content;
|
|
70
76
|
if (!Array.isArray(content))
|
|
71
77
|
return result;
|
|
72
|
-
return {
|
|
73
|
-
...result,
|
|
74
|
-
content: [...content, textBlock(reminder)],
|
|
75
|
-
};
|
|
78
|
+
return { ...result, content: [...content, textBlock(notice)] };
|
|
76
79
|
}
|
|
77
80
|
export function textSummary(content) {
|
|
78
81
|
const text = contentText(content);
|