@akira-tl/forgerelay 0.8.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +13 -10
- 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/schemas.js +23 -0
- 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/git/worktree-recovery.js +189 -0
- package/dist/workspaces/inventory.js +4 -1
- package/dist/workspaces/relay/result-support.js +36 -0
- package/dist/workspaces/relay/transport/remote-transport.js +30 -3
- package/dist/workspaces/relay/workspace-relay.js +4 -1
- 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 +33 -21
- package/docs/roadmap.md +45 -35
- 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 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { markCapabilityGuideActivated, resolveCapabilityGuideReadPath, } from "../mcp/server/core/capabilities.js";
|
|
5
5
|
import { assertAllowedPath, isPathInsideRoot, resolveAllowedPath, } from "../mcp/filesystem/roots.js";
|
|
6
6
|
import { loadWorkspaceSkills, markSkillActivated, resolveSkillReadPath, } from "./resources/skills.js";
|
|
7
|
+
import { WorkspaceResourceMonitor, } from "./resources/resource-monitor.js";
|
|
7
8
|
const INITIAL_INSTRUCTION_DISCOVERY_DEPTH = 1;
|
|
8
9
|
const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
|
9
10
|
const SKIPPED_CONTEXT_DIRS = new Set([
|
|
@@ -25,6 +26,7 @@ const SKIPPED_CONTEXT_DIRS = new Set([
|
|
|
25
26
|
*/
|
|
26
27
|
export class WorkspaceContextService {
|
|
27
28
|
config;
|
|
29
|
+
resourceMonitor = new WorkspaceResourceMonitor();
|
|
28
30
|
constructor(config) {
|
|
29
31
|
this.config = config;
|
|
30
32
|
}
|
|
@@ -87,6 +89,7 @@ export class WorkspaceContextService {
|
|
|
87
89
|
markReadPathLoaded(workspace, readPath) {
|
|
88
90
|
if (readPath.skillRead?.isSkillFile) {
|
|
89
91
|
markSkillActivated(workspace.activatedSkillDirs, readPath.skillRead.skill);
|
|
92
|
+
this.resourceMonitor.markSkillActivated(workspace.id, readPath.skillRead.skill.filePath);
|
|
90
93
|
}
|
|
91
94
|
if (readPath.capabilityGuideRead?.isGuideFile) {
|
|
92
95
|
markCapabilityGuideActivated(workspace.activatedCapabilityGuideDirs, readPath.capabilityGuideRead.guide);
|
|
@@ -198,8 +201,40 @@ export class WorkspaceContextService {
|
|
|
198
201
|
await this.discoverInstructionTree(workspace, directory, 0);
|
|
199
202
|
loaded.push(...await this.loadKnownInstructionsInDirectory(workspace, directory));
|
|
200
203
|
}
|
|
204
|
+
for (const file of loaded) {
|
|
205
|
+
this.resourceMonitor.trackLoadedInstruction(workspace.id, workspace.root, file.path);
|
|
206
|
+
}
|
|
201
207
|
return loaded;
|
|
202
208
|
}
|
|
209
|
+
trackWorkspaceResources(workspace, agentsFiles, availableAgentsFiles) {
|
|
210
|
+
this.resourceMonitor.trackWorkspace({
|
|
211
|
+
workspaceId: workspace.id,
|
|
212
|
+
root: workspace.root,
|
|
213
|
+
loadedInstructions: agentsFiles.map((file) => file.path),
|
|
214
|
+
availableInstructions: availableAgentsFiles.map((file) => file.path),
|
|
215
|
+
skills: workspace.skills.map((skill) => ({
|
|
216
|
+
name: skill.name,
|
|
217
|
+
filePath: skill.filePath,
|
|
218
|
+
baseDir: skill.baseDir,
|
|
219
|
+
activated: workspace.activatedSkillDirs.has(resolve(skill.baseDir)),
|
|
220
|
+
})),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
claimResourceUpdates(workspaceId, conversationScopeId) {
|
|
224
|
+
return this.resourceMonitor.claim(workspaceId, conversationScopeId);
|
|
225
|
+
}
|
|
226
|
+
acknowledgeResourceUpdates(workspaceId, conversationScopeId) {
|
|
227
|
+
this.resourceMonitor.acknowledge(workspaceId, conversationScopeId);
|
|
228
|
+
}
|
|
229
|
+
resourceUpdatesCurrent(workspaceId, conversationScopeId) {
|
|
230
|
+
return this.resourceMonitor.isCurrentForScope(workspaceId, conversationScopeId);
|
|
231
|
+
}
|
|
232
|
+
forgetWorkspaceResources(workspaceId) {
|
|
233
|
+
this.resourceMonitor.forgetWorkspace(workspaceId);
|
|
234
|
+
}
|
|
235
|
+
pruneWorkspaceResources(activeWorkspaceIds) {
|
|
236
|
+
this.resourceMonitor.pruneWorkspaces(activeWorkspaceIds);
|
|
237
|
+
}
|
|
203
238
|
async discoverInstructionTree(workspace, directory, remainingDepth) {
|
|
204
239
|
const resolvedDirectory = resolve(directory);
|
|
205
240
|
if (workspace.scannedInstructionDirs.has(resolvedDirectory))
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { assertAllowedPath } from "../../mcp/filesystem/roots.js";
|
|
5
|
+
import { git } from "./git.js";
|
|
6
|
+
export async function inspectManagedWorktreeRecovery(session, config) {
|
|
7
|
+
if (session.status !== "active" ||
|
|
8
|
+
session.mode !== "worktree" ||
|
|
9
|
+
!session.managed)
|
|
10
|
+
return undefined;
|
|
11
|
+
const backing = await directoryState(session.root, [config.worktreeRoot]);
|
|
12
|
+
const source = await sourceState(session.sourceRoot, config.allowedRoots);
|
|
13
|
+
const conditions = [];
|
|
14
|
+
if (source === "missing")
|
|
15
|
+
conditions.push("source-missing");
|
|
16
|
+
else if (source === "unavailable")
|
|
17
|
+
conditions.push("source-unavailable");
|
|
18
|
+
if (backing === "missing")
|
|
19
|
+
conditions.push("backing-missing");
|
|
20
|
+
if (source !== "available" || !session.sourceRoot) {
|
|
21
|
+
return {
|
|
22
|
+
classification: "manual-intervention",
|
|
23
|
+
conditions,
|
|
24
|
+
backing,
|
|
25
|
+
source,
|
|
26
|
+
gitRegistration: "unavailable",
|
|
27
|
+
managedBranch: "unknown",
|
|
28
|
+
targetBranch: "unknown",
|
|
29
|
+
backingBranch: "unavailable",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const sourceRoot = assertAllowedPath(session.sourceRoot, config.allowedRoots);
|
|
33
|
+
const [managedBranch, targetBranch, registration] = await Promise.all([
|
|
34
|
+
branchState(sourceRoot, session.branch),
|
|
35
|
+
branchState(sourceRoot, session.targetBranch),
|
|
36
|
+
registrationState(sourceRoot, session.root),
|
|
37
|
+
]);
|
|
38
|
+
if (managedBranch === "missing")
|
|
39
|
+
conditions.push("managed-branch-missing");
|
|
40
|
+
if (targetBranch === "missing")
|
|
41
|
+
conditions.push("target-branch-missing");
|
|
42
|
+
if (registration === "stale")
|
|
43
|
+
conditions.push("git-registration-stale");
|
|
44
|
+
else if (registration === "missing")
|
|
45
|
+
conditions.push("git-registration-missing");
|
|
46
|
+
else if (registration === "unavailable")
|
|
47
|
+
conditions.push("git-registration-unavailable");
|
|
48
|
+
const backingBranch = backing === "present"
|
|
49
|
+
? await backingBranchState(session.root, session.branch)
|
|
50
|
+
: "unavailable";
|
|
51
|
+
if (backingBranch === "mismatched")
|
|
52
|
+
conditions.push("branch-mismatch");
|
|
53
|
+
const recoverable = backing === "missing" &&
|
|
54
|
+
managedBranch === "present" &&
|
|
55
|
+
targetBranch === "present" &&
|
|
56
|
+
(registration === "stale" || registration === "missing") &&
|
|
57
|
+
conditions.every((condition) => condition === "backing-missing" ||
|
|
58
|
+
condition === "git-registration-stale" ||
|
|
59
|
+
condition === "git-registration-missing");
|
|
60
|
+
return {
|
|
61
|
+
classification: conditions.length === 0
|
|
62
|
+
? "healthy"
|
|
63
|
+
: recoverable
|
|
64
|
+
? "recoverable"
|
|
65
|
+
: "manual-intervention",
|
|
66
|
+
conditions,
|
|
67
|
+
backing,
|
|
68
|
+
source,
|
|
69
|
+
gitRegistration: registration,
|
|
70
|
+
managedBranch,
|
|
71
|
+
targetBranch,
|
|
72
|
+
backingBranch,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function directoryState(path, allowedRoots) {
|
|
76
|
+
const allowedPath = assertAllowedPath(path, allowedRoots);
|
|
77
|
+
try {
|
|
78
|
+
return (await stat(allowedPath)).isDirectory() ? "present" : "missing";
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (isMissingPath(error))
|
|
82
|
+
return "missing";
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function sourceState(sourceRoot, allowedRoots) {
|
|
87
|
+
if (!sourceRoot)
|
|
88
|
+
return "unavailable";
|
|
89
|
+
let sourcePath;
|
|
90
|
+
try {
|
|
91
|
+
sourcePath = assertAllowedPath(sourceRoot, allowedRoots);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "unavailable";
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (!(await stat(sourcePath)).isDirectory())
|
|
98
|
+
return "missing";
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return isMissingPath(error) ? "missing" : "unavailable";
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const gitRoot = (await git(sourcePath, ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
105
|
+
const [canonicalGitRoot, canonicalSource] = await Promise.all([realpath(gitRoot), realpath(sourcePath)]);
|
|
106
|
+
return pathKey(canonicalGitRoot) === pathKey(canonicalSource) ? "available" : "unavailable";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return "unavailable";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function branchState(sourceRoot, branch) {
|
|
113
|
+
if (!branch)
|
|
114
|
+
return "missing";
|
|
115
|
+
try {
|
|
116
|
+
await git(sourceRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
117
|
+
return "present";
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return "missing";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function registrationState(sourceRoot, worktreePath) {
|
|
124
|
+
let registrations;
|
|
125
|
+
try {
|
|
126
|
+
registrations = parseWorktreeRegistrations((await git(sourceRoot, ["worktree", "list", "--porcelain"])).stdout);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return "unavailable";
|
|
130
|
+
}
|
|
131
|
+
const worktreeKey = await registrationPathKey(worktreePath);
|
|
132
|
+
for (const registration of registrations) {
|
|
133
|
+
if (await registrationPathKey(registration.path) !== worktreeKey)
|
|
134
|
+
continue;
|
|
135
|
+
return registration.prunable ? "stale" : "registered";
|
|
136
|
+
}
|
|
137
|
+
return "missing";
|
|
138
|
+
}
|
|
139
|
+
async function backingBranchState(worktreeRoot, expectedBranch) {
|
|
140
|
+
if (!expectedBranch)
|
|
141
|
+
return "mismatched";
|
|
142
|
+
try {
|
|
143
|
+
const actual = (await git(worktreeRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"])).stdout.trim();
|
|
144
|
+
return actual === expectedBranch ? "matching" : "mismatched";
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return "mismatched";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function parseWorktreeRegistrations(output) {
|
|
151
|
+
return output
|
|
152
|
+
.trim()
|
|
153
|
+
.split(/\n\n+/)
|
|
154
|
+
.map((block) => {
|
|
155
|
+
const lines = block.split("\n");
|
|
156
|
+
const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
|
|
157
|
+
if (!path)
|
|
158
|
+
return undefined;
|
|
159
|
+
return {
|
|
160
|
+
path,
|
|
161
|
+
prunable: lines.some((line) => line.startsWith("prunable ")),
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
.filter((entry) => entry !== undefined);
|
|
165
|
+
}
|
|
166
|
+
async function registrationPathKey(path) {
|
|
167
|
+
const resolved = resolve(path);
|
|
168
|
+
try {
|
|
169
|
+
return pathKey(await realpath(resolved));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (!isMissingPath(error))
|
|
173
|
+
return pathKey(resolved);
|
|
174
|
+
try {
|
|
175
|
+
return pathKey(join(await realpath(dirname(resolved)), basename(resolved)));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return pathKey(resolved);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function pathKey(path) {
|
|
183
|
+
const resolved = resolve(path);
|
|
184
|
+
return platform() === "win32" ? resolved.toLowerCase() : resolved;
|
|
185
|
+
}
|
|
186
|
+
function isMissingPath(error) {
|
|
187
|
+
return error instanceof Error && "code" in error &&
|
|
188
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
189
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, resolve } from "node:path";
|
|
2
2
|
import { assertAllowedPath } from "../mcp/filesystem/roots.js";
|
|
3
3
|
import { canonicalPath } from "./paths.js";
|
|
4
|
+
import { inspectManagedWorktreeRecovery } from "./git/worktree-recovery.js";
|
|
4
5
|
const WORKSPACE_STALE_REMINDER_MS = 2 * 24 * 60 * 60 * 1_000;
|
|
5
6
|
/** Build bounded read-only projections over persistent Workspace sessions. */
|
|
6
7
|
export class WorkspaceInventoryService {
|
|
@@ -84,11 +85,12 @@ export class WorkspaceInventoryService {
|
|
|
84
85
|
}
|
|
85
86
|
async inventoryEntryForSession(session, now, current) {
|
|
86
87
|
const rootValid = await this.sessions.validSessionRoot(session) !== undefined;
|
|
88
|
+
const recovery = await inspectManagedWorktreeRecovery(session, this.config);
|
|
87
89
|
const lastUsedAt = Date.parse(session.lastUsedAt);
|
|
88
90
|
const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
|
|
89
91
|
const state = session.status !== "active"
|
|
90
92
|
? "closed"
|
|
91
|
-
: !rootValid
|
|
93
|
+
: !rootValid || (recovery !== undefined && recovery.classification !== "healthy")
|
|
92
94
|
? "invalid"
|
|
93
95
|
: idleMs >= WORKSPACE_STALE_REMINDER_MS
|
|
94
96
|
? "stale"
|
|
@@ -109,6 +111,7 @@ export class WorkspaceInventoryService {
|
|
|
109
111
|
lastUsedAt: session.lastUsedAt,
|
|
110
112
|
idleMs,
|
|
111
113
|
rootValid,
|
|
114
|
+
...(recovery ? { recovery } : {}),
|
|
112
115
|
current,
|
|
113
116
|
};
|
|
114
117
|
}
|
|
@@ -25,6 +25,42 @@ export function copyNumberField(source, target, field) {
|
|
|
25
25
|
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
26
26
|
target[field] = value;
|
|
27
27
|
}
|
|
28
|
+
export function safeManagedWorktreeRecovery(value) {
|
|
29
|
+
if (!value || typeof value !== "object")
|
|
30
|
+
return undefined;
|
|
31
|
+
const recovery = value;
|
|
32
|
+
const classifications = new Set(["healthy", "recoverable", "manual-intervention"]);
|
|
33
|
+
const conditions = new Set([
|
|
34
|
+
"backing-missing",
|
|
35
|
+
"managed-branch-missing",
|
|
36
|
+
"git-registration-stale",
|
|
37
|
+
"git-registration-missing",
|
|
38
|
+
"git-registration-unavailable",
|
|
39
|
+
"branch-mismatch",
|
|
40
|
+
"source-missing",
|
|
41
|
+
"source-unavailable",
|
|
42
|
+
"target-branch-missing",
|
|
43
|
+
]);
|
|
44
|
+
if (typeof recovery.classification !== "string" || !classifications.has(recovery.classification) ||
|
|
45
|
+
!Array.isArray(recovery.conditions) || recovery.conditions.some((condition) => typeof condition !== "string" || !conditions.has(condition)) ||
|
|
46
|
+
(recovery.backing !== "present" && recovery.backing !== "missing") ||
|
|
47
|
+
(recovery.source !== "available" && recovery.source !== "missing" && recovery.source !== "unavailable") ||
|
|
48
|
+
(recovery.gitRegistration !== "registered" && recovery.gitRegistration !== "stale" && recovery.gitRegistration !== "missing" && recovery.gitRegistration !== "unavailable") ||
|
|
49
|
+
(recovery.managedBranch !== "present" && recovery.managedBranch !== "missing" && recovery.managedBranch !== "unknown") ||
|
|
50
|
+
(recovery.targetBranch !== "present" && recovery.targetBranch !== "missing" && recovery.targetBranch !== "unknown") ||
|
|
51
|
+
(recovery.backingBranch !== "matching" && recovery.backingBranch !== "mismatched" && recovery.backingBranch !== "unavailable"))
|
|
52
|
+
return undefined;
|
|
53
|
+
return {
|
|
54
|
+
classification: recovery.classification,
|
|
55
|
+
conditions: [...recovery.conditions],
|
|
56
|
+
backing: recovery.backing,
|
|
57
|
+
source: recovery.source,
|
|
58
|
+
gitRegistration: recovery.gitRegistration,
|
|
59
|
+
managedBranch: recovery.managedBranch,
|
|
60
|
+
targetBranch: recovery.targetBranch,
|
|
61
|
+
backingBranch: recovery.backingBranch,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
28
64
|
export function safeTaskSummary(value) {
|
|
29
65
|
if (!value || typeof value !== "object")
|
|
30
66
|
return undefined;
|
|
@@ -27,15 +27,42 @@ export async function readRemoteOwnerToken(sshRoute) {
|
|
|
27
27
|
const result = await runSshCommand([
|
|
28
28
|
...prefix,
|
|
29
29
|
target,
|
|
30
|
-
|
|
31
|
-
"auth",
|
|
32
|
-
"__owner-token",
|
|
30
|
+
remoteOwnerTokenCommand(),
|
|
33
31
|
]);
|
|
34
32
|
const token = result.stdout.trim();
|
|
35
33
|
if (!token)
|
|
36
34
|
throw new Error("SSH owner-token command returned an empty token.");
|
|
37
35
|
return token;
|
|
38
36
|
}
|
|
37
|
+
function remoteOwnerTokenCommand() {
|
|
38
|
+
const script = [
|
|
39
|
+
"set -u",
|
|
40
|
+
"try_forgerelay() {",
|
|
41
|
+
" candidate=\"$1\"",
|
|
42
|
+
" [ -x \"$candidate\" ] || return 1",
|
|
43
|
+
" token=$(\"$candidate\" auth __owner-token 2>/dev/null) || token=",
|
|
44
|
+
" if [ -z \"$token\" ]; then",
|
|
45
|
+
" sibling_node=$(dirname \"$candidate\")/node",
|
|
46
|
+
" [ -x \"$sibling_node\" ] || return 1",
|
|
47
|
+
" token=$(\"$sibling_node\" \"$candidate\" auth __owner-token 2>/dev/null) || return 1",
|
|
48
|
+
" fi",
|
|
49
|
+
" [ -n \"$token\" ] || return 1",
|
|
50
|
+
" printf '%s\\n' \"$token\"",
|
|
51
|
+
" exit 0",
|
|
52
|
+
"}",
|
|
53
|
+
"path_candidate=$(command -v forgerelay 2>/dev/null || true)",
|
|
54
|
+
"[ -n \"$path_candidate\" ] && try_forgerelay \"$path_candidate\"",
|
|
55
|
+
"for candidate in \"$HOME/.local/bin/forgerelay\" \"$HOME/.npm-global/bin/forgerelay\" \"$HOME/.volta/bin/forgerelay\" \"$HOME/.local/share/pnpm/forgerelay\" \"$HOME/.bun/bin/forgerelay\" \"$HOME\"/.nvm/versions/node/*/bin/forgerelay; do",
|
|
56
|
+
" try_forgerelay \"$candidate\"",
|
|
57
|
+
"done",
|
|
58
|
+
"printf '%s\\n' 'ForgeRelay CLI was not found in the non-interactive SSH environment or supported user-local install paths.' >&2",
|
|
59
|
+
"exit 127",
|
|
60
|
+
].join("\n");
|
|
61
|
+
return `sh -c ${shellQuote(script)}`;
|
|
62
|
+
}
|
|
63
|
+
function shellQuote(value) {
|
|
64
|
+
return `'${value.replaceAll("'", `'\"'\"'`)}'`;
|
|
65
|
+
}
|
|
39
66
|
export async function openRemoteServiceEndpoint(target, sshRoute) {
|
|
40
67
|
if (!sshRoute) {
|
|
41
68
|
return { endpoint: target, close: async () => undefined };
|
|
@@ -7,7 +7,7 @@ import { RemoteMcpConnectionPool } from "./transport/remote-mcp-connection-pool.
|
|
|
7
7
|
import { withFileLock } from "../../runtime/state/lock/file-lock.js";
|
|
8
8
|
import { withRemoteServiceEndpoint } from "./transport/remote-transport.js";
|
|
9
9
|
import { loadForgeRelayFiles, writeForgeRelayRemote, } from "../../runtime/config/user-config.js";
|
|
10
|
-
import { assertRemoteToolSucceeded,
|
|
10
|
+
import { assertRemoteToolSucceeded, copyBooleanField, copyNumberField, copyStringField, errorMessage, remapToolResultWorkspaceId, replaceExactWorkspaceId, safeManagedWorktreeRecovery, safeTaskSummary, sanitizedRemoteError, stringField, toolResultText, } from "./result-support.js";
|
|
11
11
|
export class RemoteWorkspaceRelay {
|
|
12
12
|
routes = new Map();
|
|
13
13
|
turnRoutes = new Map();
|
|
@@ -77,6 +77,9 @@ export class RemoteWorkspaceRelay {
|
|
|
77
77
|
copyStringField(remoteInspection, projection, "lastUsedAt");
|
|
78
78
|
copyNumberField(remoteInspection, projection, "idleMs");
|
|
79
79
|
copyBooleanField(remoteInspection, projection, "rootValid");
|
|
80
|
+
const recovery = safeManagedWorktreeRecovery(remoteInspection.recovery);
|
|
81
|
+
if (recovery)
|
|
82
|
+
projection.recovery = recovery;
|
|
80
83
|
const taskSummary = safeTaskSummary(remoteInspection.taskSummary);
|
|
81
84
|
if (taskSummary)
|
|
82
85
|
projection.taskSummary = taskSummary;
|