@cjhyy/code-shell-core 0.6.0-rc.11 → 0.6.0-rc.13
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/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +144 -0
- package/dist/context/manager.d.ts +3 -1
- package/dist/context/manager.js +24 -15
- package/dist/credentials/types.d.ts +2 -2
- package/dist/engine/engine.d.ts +5 -1
- package/dist/engine/engine.js +92 -51
- package/dist/engine/turn-loop.js +1 -1
- package/dist/git/worktree.d.ts +49 -6
- package/dist/git/worktree.js +265 -31
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/logging/logger.js +6 -6
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +13 -0
- package/dist/protocol/server.d.ts +8 -0
- package/dist/protocol/server.js +45 -8
- package/dist/protocol/types.d.ts +4 -0
- package/dist/protocol/types.js +4 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +189 -2
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +45 -26
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -1
- package/dist/tool-system/builtin/background-jobs.js +8 -1
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +15 -2
- package/dist/tool-system/builtin/drive-claude-code.js +174 -39
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.js +5 -1
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/read.js +5 -2
- package/dist/tool-system/builtin/worktree.d.ts +2 -4
- package/dist/tool-system/builtin/worktree.js +250 -75
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/mcp-manager.d.ts +8 -0
- package/dist/tool-system/mcp-manager.js +32 -11
- package/dist/types.d.ts +12 -0
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +1 -1
|
@@ -1,122 +1,297 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EnterWorktree / ExitWorktree tools —
|
|
2
|
+
* EnterWorktree / ExitWorktree tools — switch a session between git workspaces.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export function getActiveWorktree() {
|
|
8
|
-
return _activeWorktree;
|
|
9
|
-
}
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
|
|
10
7
|
export const enterWorktreeToolDef = {
|
|
11
8
|
name: "EnterWorktree",
|
|
12
|
-
description: "
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"Use this when you need to make experimental changes or work in isolation.",
|
|
9
|
+
description: "Switch the current session workspace. Target can be a new worktree slug, " +
|
|
10
|
+
"an existing worktree path or branch, or 'main' to return to the main repository. " +
|
|
11
|
+
"Switching leaves the previous worktree on disk.",
|
|
16
12
|
inputSchema: {
|
|
17
13
|
type: "object",
|
|
18
14
|
properties: {
|
|
15
|
+
target: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Workspace target: 'main', a new slug (alphanumeric, dots, dashes, underscores), " +
|
|
18
|
+
"or an existing worktree path/branch to switch to.",
|
|
19
|
+
},
|
|
19
20
|
slug: {
|
|
20
21
|
type: "string",
|
|
21
|
-
description: "
|
|
22
|
-
"Example: 'fix-auth-bug', 'refactor-api'",
|
|
22
|
+
description: "Deprecated alias for target. Prefer target.",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
|
-
required: [
|
|
25
|
+
required: [],
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
export async function enterWorktreeTool(args, ctx) {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
return "Error:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
29
|
+
const target = stringArg(args.target) ?? stringArg(args.slug);
|
|
30
|
+
if (!target)
|
|
31
|
+
return "Error: target is required";
|
|
32
|
+
const resolved = sessionServices(ctx);
|
|
33
|
+
if (!resolved.ok)
|
|
34
|
+
return resolved.error;
|
|
35
|
+
const { sessionId, sessionManager } = resolved;
|
|
36
|
+
const mainRoot = sessionManager.readCwd(sessionId) ?? ctx?.cwd ?? process.cwd();
|
|
37
|
+
const fromWorkspace = sessionManager.getSessionWorkspace(sessionId) ?? {
|
|
38
|
+
root: mainRoot,
|
|
39
|
+
kind: "main",
|
|
40
|
+
};
|
|
41
|
+
const fromRoot = fromWorkspace.root;
|
|
42
|
+
const currentTurnRoot = ctx?.cwd ?? fromRoot;
|
|
41
43
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let setupNote = "";
|
|
51
|
-
const setupScripts = ctx?.engine?.readWorktreeSetupScripts(cwd);
|
|
52
|
-
const script = selectPlatformScript(setupScripts);
|
|
53
|
-
if (script) {
|
|
54
|
-
const setup = await runWorktreeSetup(_activeWorktree.worktreePath, script, {
|
|
55
|
-
sandbox: ctx?.sandbox,
|
|
56
|
-
shellEnv: ctx?.shellEnv,
|
|
57
|
-
signal: ctx?.signal,
|
|
58
|
-
});
|
|
59
|
-
if (setup.ok) {
|
|
60
|
-
setupNote = `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
setupNote =
|
|
64
|
-
`\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
|
|
65
|
-
`You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
66
|
-
}
|
|
44
|
+
if (target === "main") {
|
|
45
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
46
|
+
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
47
|
+
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
48
|
+
return (`Switched to main workspace:\n` +
|
|
49
|
+
` Path: ${mainRoot}\n` +
|
|
50
|
+
` From: ${fromRoot}\n\n` +
|
|
51
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
67
52
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
53
|
+
const selected = resolveWorktreeTarget({
|
|
54
|
+
target,
|
|
55
|
+
cwd: ctx?.cwd ?? mainRoot,
|
|
56
|
+
mainRoot,
|
|
57
|
+
sessionId,
|
|
58
|
+
currentWorkspace: fromWorkspace,
|
|
59
|
+
});
|
|
60
|
+
const workspace = toSessionWorkspace(selected, fromWorkspace);
|
|
61
|
+
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
62
|
+
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
63
|
+
const setupNote = selected.created
|
|
64
|
+
? await runSetupIfConfigured(selected.session.worktreePath, mainRoot, ctx)
|
|
65
|
+
: "";
|
|
66
|
+
const verb = selected.created ? "Worktree created and switched" : "Switched to worktree";
|
|
67
|
+
return (`${verb}:\n` +
|
|
68
|
+
` Path: ${workspace.worktree.path}\n` +
|
|
69
|
+
` Branch: ${workspace.worktree.branch}\n` +
|
|
70
|
+
` From: ${selected.from}\n` +
|
|
71
|
+
` Previous: ${fromRoot}\n\n` +
|
|
72
|
+
nextTurnNotice(workspace.root, currentTurnRoot) +
|
|
73
73
|
setupNote);
|
|
74
74
|
}
|
|
75
75
|
catch (err) {
|
|
76
|
-
return `Error
|
|
76
|
+
return `Error switching worktree: ${err.message}`;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
/** Keep setup output from bloating the tool result — head+tail-ish trim. */
|
|
80
80
|
function truncate(s, max = 2000) {
|
|
81
81
|
return s.length <= max ? s : `${s.slice(0, max)}\n…(${s.length - max} more chars)`;
|
|
82
82
|
}
|
|
83
|
+
function persistSessionWorkspace(sessionManager, sessionId, workspace, ctx) {
|
|
84
|
+
sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
85
|
+
ctx?.setSessionWorkspace?.(workspace);
|
|
86
|
+
}
|
|
87
|
+
function nextTurnNotice(nextRoot, currentTurnRoot) {
|
|
88
|
+
return (`Workspace switched to ${nextRoot}. This takes effect on the next turn - ` +
|
|
89
|
+
`file/shell/sandbox tools in the CURRENT turn still target ${currentTurnRoot}.`);
|
|
90
|
+
}
|
|
83
91
|
export const exitWorktreeToolDef = {
|
|
84
92
|
name: "ExitWorktree",
|
|
85
|
-
description: "
|
|
86
|
-
"Choose whether to keep
|
|
93
|
+
description: "Switch the current session back to the main working directory. " +
|
|
94
|
+
"Choose whether to keep the worktree, detach it while preserving the branch, " +
|
|
95
|
+
"or discard it entirely.",
|
|
87
96
|
inputSchema: {
|
|
88
97
|
type: "object",
|
|
89
98
|
properties: {
|
|
90
99
|
action: {
|
|
91
100
|
type: "string",
|
|
92
|
-
enum: ["keep", "discard"],
|
|
93
|
-
description: "'keep' preserves the worktree branch for later
|
|
94
|
-
"'
|
|
101
|
+
enum: ["keep", "detach", "discard"],
|
|
102
|
+
description: "'keep' preserves the worktree directory and branch for later. " +
|
|
103
|
+
"'detach' removes the directory but keeps the branch. " +
|
|
104
|
+
"'discard' removes the directory and its branch entirely. " +
|
|
105
|
+
"If omitted, a clean worktree is detached automatically; a dirty worktree requires keep/discard.",
|
|
95
106
|
},
|
|
96
107
|
},
|
|
97
|
-
required: [
|
|
108
|
+
required: [],
|
|
98
109
|
},
|
|
99
110
|
};
|
|
100
|
-
export async function exitWorktreeTool(args) {
|
|
101
|
-
|
|
111
|
+
export async function exitWorktreeTool(args, ctx) {
|
|
112
|
+
const resolved = sessionServices(ctx);
|
|
113
|
+
if (!resolved.ok)
|
|
114
|
+
return resolved.error;
|
|
115
|
+
const { sessionId, sessionManager } = resolved;
|
|
116
|
+
const workspace = sessionManager.getSessionWorkspace(sessionId);
|
|
117
|
+
if (!workspace || workspace.kind !== "worktree" || !workspace.worktree) {
|
|
102
118
|
return "Not currently in a worktree.";
|
|
103
119
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
const requested = args.action;
|
|
121
|
+
if (requested !== undefined &&
|
|
122
|
+
requested !== "keep" &&
|
|
123
|
+
requested !== "detach" &&
|
|
124
|
+
requested !== "discard") {
|
|
125
|
+
return `Error: unknown action "${requested}" (expected keep, detach, or discard).`;
|
|
126
|
+
}
|
|
127
|
+
const hasPendingChanges = existsSync(workspace.worktree.path) &&
|
|
128
|
+
worktreeHasUncommittedOrAheadChanges(workspace.worktree.path, workspace.worktree.baseRef);
|
|
129
|
+
const action = requested ?? (hasPendingChanges ? undefined : "detach");
|
|
130
|
+
if (!action) {
|
|
131
|
+
return (`Error: worktree has uncommitted changes or new commits. Choose action "keep" to preserve ` +
|
|
132
|
+
`the directory or "discard" to delete the worktree and branch.`);
|
|
133
|
+
}
|
|
134
|
+
if (action === "detach" && hasPendingChanges) {
|
|
135
|
+
return (`Error: detach would drop uncommitted changes or new commits. Choose action "keep" to preserve ` +
|
|
136
|
+
`the directory or "discard" to delete the worktree and branch.`);
|
|
137
|
+
}
|
|
138
|
+
const mainRoot = sessionManager.readCwd(sessionId) ?? workspace.root;
|
|
139
|
+
const currentTurnRoot = ctx?.cwd ?? workspace.root;
|
|
107
140
|
try {
|
|
141
|
+
let removal;
|
|
142
|
+
if (action === "discard" || action === "detach") {
|
|
143
|
+
const otherOwners = otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, workspace.worktree.path);
|
|
144
|
+
if (otherOwners.length > 0) {
|
|
145
|
+
const mainWorkspace = { root: mainRoot, kind: "main" };
|
|
146
|
+
persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
|
|
147
|
+
sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
|
|
148
|
+
return sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
108
151
|
if (action === "discard") {
|
|
109
|
-
removeWorktree(
|
|
110
|
-
return `Worktree removed and branch ${session.worktreeBranch} deleted. Back to ${session.originalCwd}.`;
|
|
152
|
+
removal = removeWorktree(workspace.worktree.path, true);
|
|
111
153
|
}
|
|
112
|
-
else {
|
|
113
|
-
removeWorktree(
|
|
114
|
-
return (`Worktree removed. Branch ${session.worktreeBranch} preserved.\n` +
|
|
115
|
-
`To merge: git merge ${session.worktreeBranch}\n` +
|
|
116
|
-
`Back to ${session.originalCwd}.`);
|
|
154
|
+
else if (action === "detach") {
|
|
155
|
+
removal = removeWorktree(workspace.worktree.path, false);
|
|
117
156
|
}
|
|
157
|
+
const mainWorkspace = { root: mainRoot, kind: "main" };
|
|
158
|
+
persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
|
|
159
|
+
sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
|
|
160
|
+
if (action === "keep") {
|
|
161
|
+
return (`Worktree preserved at ${workspace.worktree.path}. ` +
|
|
162
|
+
`Branch ${workspace.worktree.branch} preserved.\n` +
|
|
163
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
164
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
165
|
+
}
|
|
166
|
+
if (action === "discard") {
|
|
167
|
+
if (removal?.branchDeleted === false) {
|
|
168
|
+
const branch = removal.branch ?? workspace.worktree.branch;
|
|
169
|
+
return (`WARNING: worktree removed; branch ${branch} could not be deleted: ` +
|
|
170
|
+
`${removal.branchError ?? "unknown error"}\n` +
|
|
171
|
+
`Delete it manually with git branch -D ${branch}.\n` +
|
|
172
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
173
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
174
|
+
}
|
|
175
|
+
return (`Worktree removed and branch ${workspace.worktree.branch} deleted. ` +
|
|
176
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
177
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
178
|
+
}
|
|
179
|
+
return (`Worktree removed${requested ? "" : " (auto-detached clean worktree)"}. ` +
|
|
180
|
+
`Branch ${workspace.worktree.branch} preserved.\n` +
|
|
181
|
+
`To merge: git merge ${workspace.worktree.branch}\n` +
|
|
182
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
183
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
118
184
|
}
|
|
119
185
|
catch (err) {
|
|
120
186
|
return `Error exiting worktree: ${err.message}`;
|
|
121
187
|
}
|
|
122
188
|
}
|
|
189
|
+
function sessionServices(ctx) {
|
|
190
|
+
const sessionId = ctx?.sessionId;
|
|
191
|
+
if (!sessionId)
|
|
192
|
+
return { ok: false, error: "Error: worktree tools require a sessionId." };
|
|
193
|
+
const sessionManager = ctx?.engine?.getSessionManager?.();
|
|
194
|
+
if (!sessionManager) {
|
|
195
|
+
return { ok: false, error: "Error: worktree tools require a session manager." };
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, sessionId, sessionManager };
|
|
198
|
+
}
|
|
199
|
+
function otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, worktreePath) {
|
|
200
|
+
const workspaceOwners = sessionManager
|
|
201
|
+
.list(Number.MAX_SAFE_INTEGER)
|
|
202
|
+
.map((session) => ({ sessionId: session.sessionId, workspace: session.workspace }))
|
|
203
|
+
.filter((owner) => owner.workspace !== undefined);
|
|
204
|
+
const entry = listWorktrees(mainRoot, {
|
|
205
|
+
currentSessionId: sessionId,
|
|
206
|
+
workspaceOwners,
|
|
207
|
+
}).find((worktree) => resolve(worktree.path) === resolve(worktreePath));
|
|
208
|
+
return (entry?.occupiedBySessionIds ?? []).filter((owner) => owner !== sessionId);
|
|
209
|
+
}
|
|
210
|
+
function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot) {
|
|
211
|
+
return (`Error: this worktree is also in use by session(s) ${otherOwners.join(", ")}; ` +
|
|
212
|
+
`switching to main, but removal has been skipped.\n` +
|
|
213
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
214
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
215
|
+
}
|
|
216
|
+
function resolveWorktreeTarget(opts) {
|
|
217
|
+
const entries = listWorktrees(opts.mainRoot);
|
|
218
|
+
const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
|
|
219
|
+
const branchTarget = normalizeBranchName(opts.target);
|
|
220
|
+
const match = entries.find((entry) => {
|
|
221
|
+
if (pathTarget && resolve(entry.path) === pathTarget)
|
|
222
|
+
return true;
|
|
223
|
+
return entry.branch === branchTarget;
|
|
224
|
+
});
|
|
225
|
+
if (match) {
|
|
226
|
+
return {
|
|
227
|
+
created: false,
|
|
228
|
+
session: {
|
|
229
|
+
originalCwd: opts.mainRoot,
|
|
230
|
+
worktreePath: match.path,
|
|
231
|
+
worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
|
|
232
|
+
worktreeBranch: match.branch,
|
|
233
|
+
originalBranch: currentBranch(opts.mainRoot),
|
|
234
|
+
sessionId: opts.sessionId,
|
|
235
|
+
createdAt: Date.now(),
|
|
236
|
+
},
|
|
237
|
+
from: opts.currentWorkspace.root,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (pathTarget) {
|
|
241
|
+
throw new Error(`no existing worktree found at ${opts.target}`);
|
|
242
|
+
}
|
|
243
|
+
validateWorktreeSlug(opts.target);
|
|
244
|
+
const created = createWorktree(opts.mainRoot, opts.target, opts.sessionId);
|
|
245
|
+
return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
|
|
246
|
+
}
|
|
247
|
+
function toSessionWorkspace(selected, currentWorkspace) {
|
|
248
|
+
const previous = currentWorkspace.kind === "worktree" &&
|
|
249
|
+
currentWorkspace.worktree &&
|
|
250
|
+
resolve(currentWorkspace.worktree.path) === resolve(selected.session.worktreePath)
|
|
251
|
+
? currentWorkspace.worktree
|
|
252
|
+
: undefined;
|
|
253
|
+
return {
|
|
254
|
+
root: selected.session.worktreePath,
|
|
255
|
+
kind: "worktree",
|
|
256
|
+
worktree: {
|
|
257
|
+
path: selected.session.worktreePath,
|
|
258
|
+
branch: selected.session.worktreeBranch,
|
|
259
|
+
baseRef: previous?.baseRef ?? selected.session.originalBranch ?? "HEAD",
|
|
260
|
+
createdBy: "codeshell",
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function runSetupIfConfigured(worktreePath, mainRoot, ctx) {
|
|
265
|
+
const setupScripts = ctx?.engine?.readWorktreeSetupScripts(mainRoot);
|
|
266
|
+
const script = selectPlatformScript(setupScripts);
|
|
267
|
+
if (!script)
|
|
268
|
+
return "";
|
|
269
|
+
const setupSandbox = ctx?.engine?.resolveWorktreeSetupSandbox
|
|
270
|
+
? await ctx.engine.resolveWorktreeSetupSandbox(worktreePath)
|
|
271
|
+
: ctx?.sandbox;
|
|
272
|
+
const setupShellEnv = ctx?.engine?.readWorktreeSetupShellEnv
|
|
273
|
+
? ctx.engine.readWorktreeSetupShellEnv(worktreePath)
|
|
274
|
+
: ctx?.shellEnv;
|
|
275
|
+
const setup = await runWorktreeSetup(worktreePath, script, {
|
|
276
|
+
sandbox: setupSandbox,
|
|
277
|
+
shellEnv: setupShellEnv,
|
|
278
|
+
signal: ctx?.signal,
|
|
279
|
+
});
|
|
280
|
+
if (setup.ok) {
|
|
281
|
+
return `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
282
|
+
}
|
|
283
|
+
return (`\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
|
|
284
|
+
`You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`);
|
|
285
|
+
}
|
|
286
|
+
function stringArg(value) {
|
|
287
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
288
|
+
}
|
|
289
|
+
function pathLike(target) {
|
|
290
|
+
return (isAbsolute(target) || target.startsWith(".") || target.includes("/") || target.includes("\\"));
|
|
291
|
+
}
|
|
292
|
+
function resolvePathTarget(target, cwd) {
|
|
293
|
+
return resolve(cwd, target);
|
|
294
|
+
}
|
|
295
|
+
function normalizeBranchName(branch) {
|
|
296
|
+
return branch.replace(/^refs\/heads\//, "");
|
|
297
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Built-in Write file tool.
|
|
3
3
|
*/
|
|
4
4
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
5
|
-
import { dirname } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { fileCache } from "./file-cache.js";
|
|
7
7
|
export const writeToolDef = {
|
|
8
8
|
name: "Write",
|
|
@@ -18,12 +18,14 @@ export const writeToolDef = {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
export async function writeTool(args, ctx) {
|
|
21
|
-
const
|
|
21
|
+
const rawPath = args.file_path;
|
|
22
22
|
const content = args.content;
|
|
23
|
-
if (!
|
|
23
|
+
if (!rawPath)
|
|
24
24
|
return "Error: file_path is required";
|
|
25
25
|
if (content === undefined)
|
|
26
26
|
return "Error: content is required";
|
|
27
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
28
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
27
29
|
try {
|
|
28
30
|
await mkdir(dirname(filePath), { recursive: true });
|
|
29
31
|
await writeFile(filePath, content, "utf-8");
|
|
@@ -18,6 +18,8 @@ import type { ToolRegistry } from "./registry.js";
|
|
|
18
18
|
import type { AgentPresetName } from "../preset/index.js";
|
|
19
19
|
import type { SandboxBackend } from "./sandbox/index.js";
|
|
20
20
|
import type { HookRegistry } from "../hooks/registry.js";
|
|
21
|
+
import type { SessionManager } from "../session/session-manager.js";
|
|
22
|
+
import type { SessionWorkspace } from "../types.js";
|
|
21
23
|
/**
|
|
22
24
|
* Narrow view of the owning Engine that tools are allowed to call back into.
|
|
23
25
|
* Defined here (in the low-level tool-system) rather than importing the
|
|
@@ -38,6 +40,12 @@ export interface ToolRuntimeHost {
|
|
|
38
40
|
linux?: string;
|
|
39
41
|
windows?: string;
|
|
40
42
|
} | undefined;
|
|
43
|
+
/** Resolve a setup-only sandbox for a newly-created worktree root. */
|
|
44
|
+
resolveWorktreeSetupSandbox?(cwd: string): Promise<SandboxBackend | undefined>;
|
|
45
|
+
/** Resolve setup-only shell env for a newly-created worktree root. */
|
|
46
|
+
readWorktreeSetupShellEnv?(cwd?: string): Record<string, string> | undefined;
|
|
47
|
+
/** Session state store used by session-scoped tools such as worktree switching. */
|
|
48
|
+
getSessionManager?(): SessionManager;
|
|
41
49
|
}
|
|
42
50
|
/** One choice in a multiple-choice AskUserQuestion. */
|
|
43
51
|
export interface AskUserChoice {
|
|
@@ -176,6 +184,10 @@ export interface ToolVisibilityContext {
|
|
|
176
184
|
export interface ToolContext {
|
|
177
185
|
/** Active working directory for this Engine. */
|
|
178
186
|
cwd: string;
|
|
187
|
+
/** Mutate the owning live context cwd. Worktree switching intentionally does not use this. */
|
|
188
|
+
setCwd?(cwd: string): void;
|
|
189
|
+
/** Mutate the owning live session state after a session workspace switch. */
|
|
190
|
+
setSessionWorkspace?(workspace: SessionWorkspace): void;
|
|
179
191
|
/** LLM credentials/endpoint for tools that need to make their own calls. */
|
|
180
192
|
llmConfig: LLMConfig;
|
|
181
193
|
/** Active model pool (Arena reads this to pick participants). */
|
|
@@ -80,6 +80,14 @@ export declare class MCPManager {
|
|
|
80
80
|
*/
|
|
81
81
|
connectAll(servers: Record<string, MCPServerConfig>, owner?: unknown): Promise<void>;
|
|
82
82
|
reconcile(servers: Record<string, MCPServerConfig>, owner?: unknown): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Remove one owner from the shared desired-server pool and disconnect servers
|
|
85
|
+
* that no remaining owner wants. This is called when a session-owned Engine
|
|
86
|
+
* is closed; without it project-scoped MCP servers can stay connected until
|
|
87
|
+
* worker shutdown.
|
|
88
|
+
*/
|
|
89
|
+
unregisterOwner(owner: unknown): Promise<void>;
|
|
90
|
+
private enabledServerNames;
|
|
83
91
|
/** Union of every registered owner's desired set; null when none registered. */
|
|
84
92
|
private unionDesired;
|
|
85
93
|
/**
|
|
@@ -247,13 +247,16 @@ export class MCPManager {
|
|
|
247
247
|
* Connect to all configured MCP servers and register their tools.
|
|
248
248
|
*/
|
|
249
249
|
async connectAll(servers, owner) {
|
|
250
|
+
const enabledNames = this.enabledServerNames(servers);
|
|
250
251
|
// Register this owner's desired set up front (see reconcile's shared-pool
|
|
251
252
|
// note) so a later reconcile from ANOTHER session can't disconnect servers
|
|
252
253
|
// this session connected at run start.
|
|
253
254
|
if (owner !== undefined) {
|
|
254
|
-
this.desiredByOwner.set(owner,
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
this.desiredByOwner.set(owner, enabledNames);
|
|
256
|
+
this.desiredServerNames = this.unionDesired() ?? new Set();
|
|
257
|
+
}
|
|
258
|
+
else if (this.desiredByOwner.size === 0) {
|
|
259
|
+
this.desiredServerNames = enabledNames;
|
|
257
260
|
}
|
|
258
261
|
// Codex-style toggle: skip servers explicitly disabled in settings.
|
|
259
262
|
// Only the literal `false` disables — absent / true / any other value
|
|
@@ -279,26 +282,42 @@ export class MCPManager {
|
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
async reconcile(servers, owner) {
|
|
282
|
-
const enabledNames =
|
|
283
|
-
.filter(([, config]) => config.enabled !== false)
|
|
284
|
-
.map(([name]) => name));
|
|
285
|
-
this.desiredServerNames = enabledNames;
|
|
285
|
+
const enabledNames = this.enabledServerNames(servers);
|
|
286
286
|
// Shared-pool semantics: this ONE pool serves every session in the worker,
|
|
287
287
|
// and sessions in different projects legitimately want DIFFERENT server
|
|
288
288
|
// sets (per-project capabilityOverrides). Disconnect only servers that NO
|
|
289
289
|
// registered owner wants — otherwise the per-session hot-reload patches
|
|
290
290
|
// would thrash each other's connections (last reconcile wins, killing a
|
|
291
|
-
// server another project's session is using). Owners are engines;
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// correctness regardless.
|
|
291
|
+
// server another project's session is using). Owners are engines; closed
|
|
292
|
+
// sessions call unregisterOwner() so their desired sets stop retaining idle
|
|
293
|
+
// connections.
|
|
295
294
|
if (owner !== undefined)
|
|
296
295
|
this.desiredByOwner.set(owner, enabledNames);
|
|
297
296
|
const union = this.unionDesired() ?? enabledNames;
|
|
297
|
+
this.desiredServerNames = union;
|
|
298
298
|
const stale = this.listServers().filter((name) => !union.has(name));
|
|
299
299
|
await Promise.all(stale.map((name) => this.disconnect(name)));
|
|
300
300
|
await this.connectAll(servers, owner);
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Remove one owner from the shared desired-server pool and disconnect servers
|
|
304
|
+
* that no remaining owner wants. This is called when a session-owned Engine
|
|
305
|
+
* is closed; without it project-scoped MCP servers can stay connected until
|
|
306
|
+
* worker shutdown.
|
|
307
|
+
*/
|
|
308
|
+
async unregisterOwner(owner) {
|
|
309
|
+
if (!this.desiredByOwner.delete(owner))
|
|
310
|
+
return;
|
|
311
|
+
const desired = this.unionDesired() ?? new Set();
|
|
312
|
+
this.desiredServerNames = desired;
|
|
313
|
+
const stale = this.listServers().filter((name) => !desired.has(name));
|
|
314
|
+
await Promise.all(stale.map((name) => this.disconnect(name)));
|
|
315
|
+
}
|
|
316
|
+
enabledServerNames(servers) {
|
|
317
|
+
return new Set(Object.entries(servers)
|
|
318
|
+
.filter(([, config]) => config.enabled !== false)
|
|
319
|
+
.map(([name]) => name));
|
|
320
|
+
}
|
|
302
321
|
/** Union of every registered owner's desired set; null when none registered. */
|
|
303
322
|
unionDesired() {
|
|
304
323
|
if (this.desiredByOwner.size === 0)
|
|
@@ -489,6 +508,8 @@ export class MCPManager {
|
|
|
489
508
|
*/
|
|
490
509
|
async disconnectAll() {
|
|
491
510
|
await Promise.all([...this.connections.keys()].map((name) => this.disconnect(name)));
|
|
511
|
+
this.desiredByOwner.clear();
|
|
512
|
+
this.desiredServerNames = null;
|
|
492
513
|
}
|
|
493
514
|
async disconnect(name) {
|
|
494
515
|
const conn = this.connections.get(name);
|
package/dist/types.d.ts
CHANGED
|
@@ -139,9 +139,21 @@ export type SessionStatus = "active" | "paused" | TerminalReason;
|
|
|
139
139
|
/** Which host/context created a session — used by the desktop disk-rebuild to
|
|
140
140
|
* filter the sidebar (only `desktop` + `automation` are shown). */
|
|
141
141
|
export type SessionOrigin = "desktop" | "tui" | "automation" | "subagent";
|
|
142
|
+
export interface SessionWorkspace {
|
|
143
|
+
root: string;
|
|
144
|
+
kind: "main" | "worktree";
|
|
145
|
+
worktree?: {
|
|
146
|
+
path: string;
|
|
147
|
+
branch: string;
|
|
148
|
+
baseRef: string;
|
|
149
|
+
createdBy: "codeshell";
|
|
150
|
+
};
|
|
151
|
+
}
|
|
142
152
|
export interface SessionState {
|
|
143
153
|
sessionId: string;
|
|
144
154
|
cwd: string;
|
|
155
|
+
/** Current session workspace pointer. Absent only on legacy state.json files. */
|
|
156
|
+
workspace?: SessionWorkspace;
|
|
145
157
|
startedAt: number;
|
|
146
158
|
model: string;
|
|
147
159
|
provider: string;
|
|
@@ -57,7 +57,7 @@ const TOOL_ARG_KEYS = {
|
|
|
57
57
|
Agent: ["description"],
|
|
58
58
|
TodoWrite: ["todos"],
|
|
59
59
|
Sleep: ["seconds"],
|
|
60
|
-
EnterWorktree: ["slug"],
|
|
60
|
+
EnterWorktree: ["target", "slug"],
|
|
61
61
|
CronCreate: ["name", "schedule"],
|
|
62
62
|
LSP: ["action", "file_path"],
|
|
63
63
|
NotebookEdit: ["action", "file_path"],
|
package/package.json
CHANGED