@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
package/dist/git/worktree.js
CHANGED
|
@@ -30,6 +30,8 @@ export function selectPlatformScript(scripts, platform = process.platform) {
|
|
|
30
30
|
* Validate worktree slug to prevent path traversal attacks.
|
|
31
31
|
*/
|
|
32
32
|
export function validateWorktreeSlug(slug) {
|
|
33
|
+
if (slug.trim().length === 0)
|
|
34
|
+
throw new Error("Worktree slug cannot be empty");
|
|
33
35
|
if (slug.length > 64)
|
|
34
36
|
throw new Error("Worktree slug too long (max 64 chars)");
|
|
35
37
|
if (/[^a-zA-Z0-9._-]/.test(slug))
|
|
@@ -41,7 +43,72 @@ export function validateWorktreeSlug(slug) {
|
|
|
41
43
|
* Find the canonical git root (resolves worktree → main repo).
|
|
42
44
|
*/
|
|
43
45
|
export function findGitRoot(cwd) {
|
|
44
|
-
return
|
|
46
|
+
return findMainWorktreeRoot(cwd);
|
|
47
|
+
}
|
|
48
|
+
export function findMainWorktreeRoot(cwd) {
|
|
49
|
+
const commonDir = execFileSync(GIT_BIN, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
}).trim();
|
|
54
|
+
return dirname(commonDir);
|
|
55
|
+
}
|
|
56
|
+
export function findWorktreeForBranch(cwd, branch) {
|
|
57
|
+
const normalized = normalizeBranchName(branch);
|
|
58
|
+
return listWorktrees(cwd).find((entry) => entry.branch === normalized)?.path;
|
|
59
|
+
}
|
|
60
|
+
export function assertBranchNotCheckedOut(cwd, branch) {
|
|
61
|
+
const existingPath = findWorktreeForBranch(cwd, branch);
|
|
62
|
+
if (existingPath) {
|
|
63
|
+
throw new Error(`branch ${normalizeBranchName(branch)} already checked out at ${existingPath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function branchExists(cwd, branch) {
|
|
67
|
+
try {
|
|
68
|
+
return (execFileSync(GIT_BIN, ["branch", "--list", normalizeBranchName(branch)], {
|
|
69
|
+
cwd,
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
}).trim().length > 0);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function isGitWorktreeRoot(cwd) {
|
|
79
|
+
if (!existsSync(join(cwd, ".git")))
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
const inside = execFileSync(GIT_BIN, ["rev-parse", "--is-inside-work-tree"], {
|
|
83
|
+
cwd,
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
timeout: 5000,
|
|
86
|
+
}).trim();
|
|
87
|
+
if (inside !== "true")
|
|
88
|
+
return false;
|
|
89
|
+
const topLevel = execFileSync(GIT_BIN, ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
90
|
+
cwd,
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
timeout: 5000,
|
|
93
|
+
}).trim();
|
|
94
|
+
return resolve(topLevel) === resolve(cwd);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function currentBranch(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const branch = execFileSync(GIT_BIN, ["branch", "--show-current"], {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
timeout: 5000,
|
|
106
|
+
}).trim();
|
|
107
|
+
return branch || undefined;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
45
112
|
}
|
|
46
113
|
/**
|
|
47
114
|
* Create an isolated git worktree for an agent session.
|
|
@@ -51,6 +118,7 @@ export function createWorktree(cwd, slug, sessionId) {
|
|
|
51
118
|
const gitRoot = findGitRoot(cwd);
|
|
52
119
|
const branchName = `worktree/${slug}-${sessionId.slice(0, 8)}`;
|
|
53
120
|
const worktreePath = resolve(gitRoot, "..", `.worktrees/${slug}-${sessionId.slice(0, 8)}`);
|
|
121
|
+
assertBranchNotCheckedOut(gitRoot, branchName);
|
|
54
122
|
// Get current branch for later reference
|
|
55
123
|
let originalBranch;
|
|
56
124
|
try {
|
|
@@ -134,50 +202,87 @@ export async function runWorktreeSetup(worktreePath, script, opts = {}) {
|
|
|
134
202
|
* Remove a worktree and optionally its branch.
|
|
135
203
|
*/
|
|
136
204
|
export function removeWorktree(worktreePath, removeBranch = false) {
|
|
205
|
+
// The MAIN repo root, not the worktree's own toplevel. `git rev-parse
|
|
206
|
+
// --show-toplevel` from inside a worktree returns the worktree path, which
|
|
207
|
+
// is about to be deleted; the branch-delete must run from the main repo,
|
|
208
|
+
// which outlives the worktree. Derive it from the common git dir.
|
|
209
|
+
let mainRoot;
|
|
137
210
|
try {
|
|
138
|
-
// The MAIN repo root, not the worktree's own toplevel. `git rev-parse
|
|
139
|
-
// --show-toplevel` from inside a worktree returns the worktree path, which
|
|
140
|
-
// is about to be deleted; the branch-delete must run from the main repo,
|
|
141
|
-
// which outlives the worktree. Derive it from the common git dir.
|
|
142
211
|
const commonDir = execFileSync(GIT_BIN, ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: worktreePath, encoding: "utf-8", timeout: 5000 }).trim();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
212
|
+
mainRoot = dirname(commonDir); // <main>/.git → <main>
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
throw new Error(`failed to inspect worktree ${worktreePath}: ${gitErrorMessage(err)}`);
|
|
216
|
+
}
|
|
217
|
+
// Capture the worktree's branch BEFORE removing the worktree — afterwards
|
|
218
|
+
// the directory is gone and `git branch --show-current` in it would fail.
|
|
219
|
+
let branch = "";
|
|
220
|
+
if (removeBranch) {
|
|
221
|
+
try {
|
|
222
|
+
branch = execFileSync(GIT_BIN, ["branch", "--show-current"], {
|
|
223
|
+
cwd: worktreePath,
|
|
224
|
+
encoding: "utf-8",
|
|
225
|
+
timeout: 5000,
|
|
226
|
+
}).trim();
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw new Error(`failed to determine branch for worktree ${worktreePath}: ${gitErrorMessage(err)}`);
|
|
230
|
+
}
|
|
231
|
+
if (!branch) {
|
|
232
|
+
throw new Error(`failed to determine branch for worktree ${worktreePath}`);
|
|
233
|
+
}
|
|
234
|
+
if (!branch.startsWith("worktree/")) {
|
|
235
|
+
throw new Error(`refusing to delete non-CodeShell worktree branch ${branch}`);
|
|
159
236
|
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
160
239
|
execFileSync(GIT_BIN, ["worktree", "remove", worktreePath, "--force"], {
|
|
161
240
|
cwd: mainRoot,
|
|
162
241
|
timeout: 30000,
|
|
163
242
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`);
|
|
246
|
+
}
|
|
247
|
+
if (removeBranch) {
|
|
248
|
+
try {
|
|
249
|
+
execFileSync(GIT_BIN, ["branch", "-D", branch], { cwd: mainRoot, timeout: 10000 });
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
return {
|
|
253
|
+
dirRemoved: true,
|
|
254
|
+
branch,
|
|
255
|
+
branchDeleted: false,
|
|
256
|
+
branchError: gitErrorMessage(err),
|
|
257
|
+
};
|
|
171
258
|
}
|
|
172
259
|
}
|
|
260
|
+
return removeBranch ? { dirRemoved: true, branch, branchDeleted: true } : { dirRemoved: true };
|
|
261
|
+
}
|
|
262
|
+
export function worktreeHasUncommittedChanges(worktreePath) {
|
|
263
|
+
try {
|
|
264
|
+
return (execFileSync(GIT_BIN, ["status", "--porcelain"], {
|
|
265
|
+
cwd: worktreePath,
|
|
266
|
+
encoding: "utf-8",
|
|
267
|
+
timeout: 10000,
|
|
268
|
+
}).trim().length > 0);
|
|
269
|
+
}
|
|
173
270
|
catch {
|
|
174
|
-
|
|
271
|
+
return false;
|
|
175
272
|
}
|
|
176
273
|
}
|
|
274
|
+
export function worktreeHasUncommittedOrAheadChanges(worktreePath, baseRef) {
|
|
275
|
+
if (worktreeHasUncommittedChanges(worktreePath))
|
|
276
|
+
return true;
|
|
277
|
+
const comparisonBase = baseRef && commitRefExists(worktreePath, baseRef)
|
|
278
|
+
? baseRef
|
|
279
|
+
: findComparisonBaseRef(worktreePath);
|
|
280
|
+
return comparisonBase ? aheadCommitCount(worktreePath, comparisonBase) > 0 : false;
|
|
281
|
+
}
|
|
177
282
|
/**
|
|
178
283
|
* List active worktrees.
|
|
179
284
|
*/
|
|
180
|
-
export function listWorktrees(cwd) {
|
|
285
|
+
export function listWorktrees(cwd, opts = {}) {
|
|
181
286
|
const raw = execFileSync(GIT_BIN, ["worktree", "list", "--porcelain"], {
|
|
182
287
|
cwd,
|
|
183
288
|
encoding: "utf-8",
|
|
@@ -202,7 +307,136 @@ export function listWorktrees(cwd) {
|
|
|
202
307
|
}
|
|
203
308
|
if (current.path)
|
|
204
309
|
entries.push(current);
|
|
205
|
-
|
|
310
|
+
if (!opts.includeDiffSummary && !opts.workspaceOwners?.length)
|
|
311
|
+
return entries;
|
|
312
|
+
let mainRoot = "";
|
|
313
|
+
try {
|
|
314
|
+
mainRoot = findMainWorktreeRoot(cwd);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
mainRoot = entries[0]?.path ?? "";
|
|
318
|
+
}
|
|
319
|
+
const baseRef = opts.includeDiffSummary ? findComparisonBaseRef(mainRoot || cwd) : undefined;
|
|
320
|
+
return entries.map((entry) => {
|
|
321
|
+
const owners = ownersForWorktree(entry.path, opts.workspaceOwners ?? []);
|
|
322
|
+
return {
|
|
323
|
+
...entry,
|
|
324
|
+
...(mainRoot ? { isMain: resolve(entry.path) === resolve(mainRoot) } : {}),
|
|
325
|
+
...(opts.includeDiffSummary ? { diff: diffSummary(entry.path, baseRef) } : {}),
|
|
326
|
+
...(owners.length > 0
|
|
327
|
+
? {
|
|
328
|
+
occupiedBySessionIds: owners,
|
|
329
|
+
occupiedByOtherSession: owners.some((id) => id !== opts.currentSessionId),
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function ownersForWorktree(path, owners) {
|
|
336
|
+
const target = resolve(path);
|
|
337
|
+
return owners
|
|
338
|
+
.filter((owner) => {
|
|
339
|
+
const workspace = owner.workspace;
|
|
340
|
+
if (!workspace)
|
|
341
|
+
return false;
|
|
342
|
+
return resolve(workspace.root) === target;
|
|
343
|
+
})
|
|
344
|
+
.map((owner) => owner.sessionId);
|
|
345
|
+
}
|
|
346
|
+
function findComparisonBaseRef(cwd) {
|
|
347
|
+
for (const ref of ["main", "master", "origin/main", "origin/master"]) {
|
|
348
|
+
if (commitRefExists(cwd, ref))
|
|
349
|
+
return ref;
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
function commitRefExists(cwd, ref) {
|
|
354
|
+
try {
|
|
355
|
+
execFileSync(GIT_BIN, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
356
|
+
cwd,
|
|
357
|
+
encoding: "utf-8",
|
|
358
|
+
timeout: 5000,
|
|
359
|
+
});
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function diffSummary(cwd, baseRef) {
|
|
367
|
+
const dirtyFiles = statusFileSet(cwd);
|
|
368
|
+
const committedFiles = baseRef ? changedFilesSinceBase(cwd, baseRef) : new Set();
|
|
369
|
+
const changedFiles = new Set([...dirtyFiles, ...committedFiles]);
|
|
370
|
+
return {
|
|
371
|
+
...(baseRef ? { baseRef } : {}),
|
|
372
|
+
changedFiles: changedFiles.size,
|
|
373
|
+
aheadCommits: baseRef ? aheadCommitCount(cwd, baseRef) : 0,
|
|
374
|
+
hasUncommittedChanges: dirtyFiles.size > 0,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function changedFilesSinceBase(cwd, baseRef) {
|
|
378
|
+
const out = gitOutput(cwd, ["diff", "--name-only", `${baseRef}...HEAD`]);
|
|
379
|
+
const raw = out ?? gitOutput(cwd, ["diff", "--name-only", `${baseRef}..HEAD`]) ?? "";
|
|
380
|
+
return new Set(raw
|
|
381
|
+
.split("\n")
|
|
382
|
+
.map((line) => line.trim())
|
|
383
|
+
.filter(Boolean));
|
|
384
|
+
}
|
|
385
|
+
function aheadCommitCount(cwd, baseRef) {
|
|
386
|
+
const out = gitOutput(cwd, ["rev-list", "--count", `${baseRef}..HEAD`]);
|
|
387
|
+
const n = Number.parseInt(out?.trim() ?? "0", 10);
|
|
388
|
+
return Number.isFinite(n) ? n : 0;
|
|
389
|
+
}
|
|
390
|
+
function statusFileSet(cwd) {
|
|
391
|
+
const raw = gitOutput(cwd, ["status", "--porcelain=v1"]) ?? "";
|
|
392
|
+
const files = new Set();
|
|
393
|
+
for (const line of raw.split("\n")) {
|
|
394
|
+
if (!line.trim())
|
|
395
|
+
continue;
|
|
396
|
+
const path = line.slice(3).trim();
|
|
397
|
+
if (!path)
|
|
398
|
+
continue;
|
|
399
|
+
const renameTarget = path.includes(" -> ") ? path.split(" -> ").at(-1) : path;
|
|
400
|
+
if (renameTarget)
|
|
401
|
+
files.add(unquoteGitPath(renameTarget));
|
|
402
|
+
}
|
|
403
|
+
return files;
|
|
404
|
+
}
|
|
405
|
+
function unquoteGitPath(path) {
|
|
406
|
+
if (!path.startsWith('"') || !path.endsWith('"'))
|
|
407
|
+
return path;
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(path);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return path.slice(1, -1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function gitOutput(cwd, args) {
|
|
416
|
+
try {
|
|
417
|
+
return execFileSync(GIT_BIN, args, {
|
|
418
|
+
cwd,
|
|
419
|
+
encoding: "utf-8",
|
|
420
|
+
timeout: 10000,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function gitErrorMessage(err) {
|
|
428
|
+
const stderr = err.stderr;
|
|
429
|
+
if (Buffer.isBuffer(stderr)) {
|
|
430
|
+
const msg = stderr.toString("utf-8").trim();
|
|
431
|
+
if (msg)
|
|
432
|
+
return msg;
|
|
433
|
+
}
|
|
434
|
+
if (typeof stderr === "string" && stderr.trim())
|
|
435
|
+
return stderr.trim();
|
|
436
|
+
return err instanceof Error ? err.message : String(err);
|
|
437
|
+
}
|
|
438
|
+
function normalizeBranchName(branch) {
|
|
439
|
+
return branch.replace(/^refs\/heads\//, "");
|
|
206
440
|
}
|
|
207
441
|
/**
|
|
208
442
|
* Symlink large directories (node_modules, .venv, etc.) from main repo to worktree.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.6.0-rc.
|
|
7
|
-
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
6
|
+
export declare const VERSION = "0.6.0-rc.13";
|
|
7
|
+
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionWorkspace, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
export { Engine, loadAgentDefinitionsForCwd } from "./engine/engine.js";
|
|
10
10
|
export type { EngineConfig, EngineHookConfig, EngineResult } from "./engine/types.js";
|
|
@@ -45,6 +45,7 @@ export { Methods, ErrorCodes, type RpcMessage, type RunResult, } from "./protoco
|
|
|
45
45
|
export { Transcript } from "./session/transcript.js";
|
|
46
46
|
export { SessionManager } from "./session/session-manager.js";
|
|
47
47
|
export { FileHistory } from "./session/file-history.js";
|
|
48
|
+
export { createWorktree, currentBranch, findMainWorktreeRoot, listWorktrees, removeWorktree, validateWorktreeSlug, worktreeHasUncommittedOrAheadChanges, worktreeHasUncommittedChanges, type ListWorktreesOptions, type RemoveWorktreeResult, type WorktreeDiffSummary, type WorktreeInfo, type WorktreeWorkspaceOwner, } from "./git/worktree.js";
|
|
48
49
|
export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
|
|
49
50
|
export { diffLines, renderDiffPreview, type DiffLine } from "./session/simple-diff.js";
|
|
50
51
|
export { MemoryManager } from "./session/memory.js";
|
|
@@ -103,7 +104,7 @@ export type { IterateConfig, IterateResult, IterateSubject, IterateFormat, Itera
|
|
|
103
104
|
export { RunManager, type RunManagerConfig, type RunStore, FileRunStore, RunQueue, EngineRunner, AUTOMATION_PROMPT_NOTE, AUTOMATION_RUN_SOURCE, type EngineRunnerConfig, type RunExecutionHandle, type RunExecutor, type CustomToolEntry, RunApprovalBackend, createRunAskUserFn, type RunLifecycleHooks, CheckpointWriter, ArtifactTracker, RunLock, type RunLockConfig, type RunLockAcquireResult, Heartbeat, NoopEvaluator, CompositeEvaluator, type Evaluator, type EvaluatorResult, type EvaluatorContext, type RunStatus, type RunSnapshot, type RunEvent, type RunCheckpoint, type RunApproval, type RunArtifactRef, type SubmitRunInput, type ResumeRunInput, type ListRunsQuery, type RunStreamEvent, type RunStreamCallback, type DetachFn, VALID_TRANSITIONS, createRunManager, type CreateRunManagerOptions, } from "./run/index.js";
|
|
104
105
|
export { defineProduct, type ProductDefinition, type ProductPreset, type ProductAdapter, type ProductContract, type CustomTool, type ProductRuntimeOptions, type ProductInstance, } from "./product/index.js";
|
|
105
106
|
export { logger } from "./logging/logger.js";
|
|
106
|
-
export { SettingsManager, type SettingsScope } from "./settings/manager.js";
|
|
107
|
+
export { SettingsManager, userHome, type SettingsScope } from "./settings/manager.js";
|
|
107
108
|
export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, type MigrationStep, } from "./settings/migrate-config.js";
|
|
108
109
|
export { SettingsSchema, validateSettings } from "./settings/schema.js";
|
|
109
110
|
export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.6.0-rc.
|
|
6
|
+
export const VERSION = "0.6.0-rc.13";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|
|
@@ -44,6 +44,7 @@ export { Methods, ErrorCodes, } from "./protocol/types.js";
|
|
|
44
44
|
export { Transcript } from "./session/transcript.js";
|
|
45
45
|
export { SessionManager } from "./session/session-manager.js";
|
|
46
46
|
export { FileHistory } from "./session/file-history.js";
|
|
47
|
+
export { createWorktree, currentBranch, findMainWorktreeRoot, listWorktrees, removeWorktree, validateWorktreeSlug, worktreeHasUncommittedOrAheadChanges, worktreeHasUncommittedChanges, } from "./git/worktree.js";
|
|
47
48
|
export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
|
|
48
49
|
export { diffLines, renderDiffPreview } from "./session/simple-diff.js";
|
|
49
50
|
export { MemoryManager } from "./session/memory.js";
|
|
@@ -121,7 +122,7 @@ export { defineProduct, } from "./product/index.js";
|
|
|
121
122
|
// ─── Logging ─────────────────────────────────────────────────────
|
|
122
123
|
export { logger } from "./logging/logger.js";
|
|
123
124
|
// ─── Settings ────────────────────────────────────────────────────
|
|
124
|
-
export { SettingsManager } from "./settings/manager.js";
|
|
125
|
+
export { SettingsManager, userHome } from "./settings/manager.js";
|
|
125
126
|
export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, } from "./settings/migrate-config.js";
|
|
126
127
|
export { SettingsSchema, validateSettings } from "./settings/schema.js";
|
|
127
128
|
export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
|
package/dist/logging/logger.js
CHANGED
|
@@ -121,12 +121,12 @@ function resolveCategoryFilter() {
|
|
|
121
121
|
//
|
|
122
122
|
// Two mechanisms, layered:
|
|
123
123
|
//
|
|
124
|
-
// 1. `runWithSid(sid, fn)` — preferred. Engine.run
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// global would.
|
|
124
|
+
// 1. `runWithSid(sid, fn)` — preferred. Once Engine.run has resolved the
|
|
125
|
+
// authoritative sid, its session-bound execution body runs in this
|
|
126
|
+
// AsyncLocalStorage scope, so every log line emitted inside the (possibly
|
|
127
|
+
// deeply async) call tree picks up the right sid. Concurrent parent + child
|
|
128
|
+
// Engines coexist because each `await` boundary preserves the ALS context —
|
|
129
|
+
// they don't trample each other like a single module global would.
|
|
130
130
|
//
|
|
131
131
|
// 2. `_currentSidFallback` — module-level mutable, written by `setCurrentSid`.
|
|
132
132
|
// Used only as a fallback when a code path runs outside any ALS scope
|
|
@@ -5,6 +5,7 @@ import { gitClone, gitRevParseHead } from "../gitOps.js";
|
|
|
5
5
|
import { installPluginFromPath } from "./install.js";
|
|
6
6
|
import { pluginMetaPath } from "./paths.js";
|
|
7
7
|
import { PluginInstallError } from "./types.js";
|
|
8
|
+
import { resolveContainedPluginSubpath } from "./sourcePath.js";
|
|
8
9
|
/**
|
|
9
10
|
* Remote install orchestrator: a thin bridge over the existing pieces. Clone
|
|
10
11
|
* the git source to a private temp dir, hand the (sub)directory to the local
|
|
@@ -26,7 +27,14 @@ export async function installPluginFromSource(parsed, name, installedAt) {
|
|
|
26
27
|
if (!clone.ok) {
|
|
27
28
|
throw new PluginInstallError(`clone failed: ${clone.error}`);
|
|
28
29
|
}
|
|
29
|
-
const realSrc = parsed.subdir
|
|
30
|
+
const realSrc = parsed.subdir
|
|
31
|
+
? (() => {
|
|
32
|
+
const resolved = resolveContainedPluginSubpath(tmp, parsed.subdir, "subdir");
|
|
33
|
+
if (!resolved.ok)
|
|
34
|
+
throw new PluginInstallError(resolved.error);
|
|
35
|
+
return resolved.path;
|
|
36
|
+
})()
|
|
37
|
+
: tmp;
|
|
30
38
|
if (!existsSync(realSrc) || !statSync(realSrc).isDirectory()) {
|
|
31
39
|
throw new PluginInstallError(`subdir not found in repo: ${parsed.subdir}`);
|
|
32
40
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ContainedPluginSubpathResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
path: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function validateRelativePluginSubpath(subpath: string, label: string): string | null;
|
|
9
|
+
export declare function resolveContainedPluginSubpath(root: string, subpath: string, label: string): ContainedPluginSubpathResult;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
3
|
+
function normPath(path) {
|
|
4
|
+
return process.platform === "win32" ? path.toLowerCase() : path;
|
|
5
|
+
}
|
|
6
|
+
function isContainedOrRoot(child, parent) {
|
|
7
|
+
const c = normPath(child);
|
|
8
|
+
const parentPath = normPath(parent);
|
|
9
|
+
const parentPrefix = normPath(parent.endsWith(sep) ? parent : parent + sep);
|
|
10
|
+
return c === parentPath || c.startsWith(parentPrefix);
|
|
11
|
+
}
|
|
12
|
+
export function validateRelativePluginSubpath(subpath, label) {
|
|
13
|
+
if (typeof subpath !== "string" || subpath.length === 0) {
|
|
14
|
+
return `${label} must be a non-empty relative path`;
|
|
15
|
+
}
|
|
16
|
+
if (subpath.includes("\0")) {
|
|
17
|
+
return `${label} must not contain NUL bytes: ${subpath}`;
|
|
18
|
+
}
|
|
19
|
+
if (isAbsolute(subpath)) {
|
|
20
|
+
return `${label} must be relative and inside the source tree: ${subpath}`;
|
|
21
|
+
}
|
|
22
|
+
if (subpath.split(/[\\/]+/).includes("..")) {
|
|
23
|
+
return `${label} must not contain parent-directory segments: ${subpath}`;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function resolveContainedPluginSubpath(root, subpath, label) {
|
|
28
|
+
const invalid = validateRelativePluginSubpath(subpath, label);
|
|
29
|
+
if (invalid)
|
|
30
|
+
return { ok: false, error: invalid };
|
|
31
|
+
let realRoot;
|
|
32
|
+
try {
|
|
33
|
+
realRoot = realpathSync(root);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return { ok: false, error: `${label} root could not be resolved: ${err.message}` };
|
|
37
|
+
}
|
|
38
|
+
const candidate = resolve(root, subpath);
|
|
39
|
+
let realCandidate;
|
|
40
|
+
try {
|
|
41
|
+
realCandidate = realpathSync(candidate);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { ok: false, error: `${label} not found in source tree: ${subpath}` };
|
|
45
|
+
}
|
|
46
|
+
if (!isContainedOrRoot(realCandidate, realRoot)) {
|
|
47
|
+
return { ok: false, error: `${label} escapes the source tree: ${subpath}` };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, path: realCandidate };
|
|
50
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, realpathSync, rmSync, rmdirSync } from "node:fs";
|
|
8
8
|
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
9
|
-
import { dirname,
|
|
9
|
+
import { dirname, join, sep } from "node:path";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { gitClone, gitRevParseHead, gitSparseCheckoutAdd, githubRepoToCloneUrl, } from "./gitOps.js";
|
|
12
12
|
import { loadMarketplace } from "./marketplaceManager.js";
|
|
@@ -15,6 +15,7 @@ import { appendInstallEntry, pluginInstallKey, readInstalledPlugins, removeInsta
|
|
|
15
15
|
import { rewritePluginVars } from "./varRewrite.js";
|
|
16
16
|
import { assertSafePluginName } from "./installer/paths.js";
|
|
17
17
|
import { pruneDisabledSettingsForPlugin } from "./installer/pruneDisabled.js";
|
|
18
|
+
import { resolveContainedPluginSubpath, validateRelativePluginSubpath, } from "./installer/sourcePath.js";
|
|
18
19
|
function userHome() {
|
|
19
20
|
return process.env.HOME ?? homedir();
|
|
20
21
|
}
|
|
@@ -81,12 +82,11 @@ export function resolveSafePluginPath(installPath, cacheRoot) {
|
|
|
81
82
|
return resolvedTarget;
|
|
82
83
|
}
|
|
83
84
|
async function materializePath(marketplaceInstallLocation, relativePath, cacheTarget) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
:
|
|
87
|
-
if (!existsSync(src)) {
|
|
88
|
-
return { ok: false, error: `plugin source path "${relativePath}" not found in marketplace` };
|
|
85
|
+
const contained = resolveContainedPluginSubpath(marketplaceInstallLocation, relativePath, "plugin source path");
|
|
86
|
+
if (!contained.ok) {
|
|
87
|
+
return { ok: false, error: contained.error };
|
|
89
88
|
}
|
|
89
|
+
const src = contained.path;
|
|
90
90
|
await mkdir(dirname(cacheTarget), { recursive: true });
|
|
91
91
|
if (existsSync(cacheTarget))
|
|
92
92
|
await rm(cacheTarget, { recursive: true, force: true });
|
|
@@ -106,22 +106,23 @@ async function materializeGit(url, ref, cacheTarget) {
|
|
|
106
106
|
return { ok: true, sha: head.stdout };
|
|
107
107
|
}
|
|
108
108
|
async function materializeGitSubdir(url, subPath, ref, cacheTarget) {
|
|
109
|
+
const invalid = validateRelativePluginSubpath(subPath, "git-subdir path");
|
|
110
|
+
if (invalid)
|
|
111
|
+
return { ok: false, error: invalid };
|
|
109
112
|
// Clone to a tempdir, then copy the subdir over.
|
|
110
113
|
const tmp = await mkdtemp(join(tmpdir(), "plugin-clone-"));
|
|
111
114
|
try {
|
|
112
|
-
const
|
|
115
|
+
const repoDir = join(tmp, "repo");
|
|
116
|
+
const clone = await gitClone(url, repoDir, { full: true, ...(ref ? { ref } : {}) });
|
|
113
117
|
if (!clone.ok)
|
|
114
118
|
return { ok: false, error: clone.error };
|
|
115
|
-
const head = await gitRevParseHead(
|
|
119
|
+
const head = await gitRevParseHead(repoDir);
|
|
116
120
|
if (!head.ok)
|
|
117
121
|
return { ok: false, error: head.error };
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
error: `git-subdir path "${subPath}" not found in cloned repository`,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
122
|
+
const contained = resolveContainedPluginSubpath(repoDir, subPath, "git-subdir path");
|
|
123
|
+
if (!contained.ok)
|
|
124
|
+
return { ok: false, error: contained.error };
|
|
125
|
+
const src = contained.path;
|
|
125
126
|
await mkdir(dirname(cacheTarget), { recursive: true });
|
|
126
127
|
if (existsSync(cacheTarget))
|
|
127
128
|
await rm(cacheTarget, { recursive: true, force: true });
|
|
@@ -162,19 +163,20 @@ async function materialize(source, marketplaceInstallLocation, marketplace, plug
|
|
|
162
163
|
// once we know it. For path sources there's no SHA — use "local".
|
|
163
164
|
const placeholder = pluginCacheDir(marketplace, plugin, "_pending_");
|
|
164
165
|
if (typeof source === "string") {
|
|
166
|
+
const invalid = validateRelativePluginSubpath(source, "plugin source path");
|
|
167
|
+
if (invalid)
|
|
168
|
+
return { ok: false, error: invalid };
|
|
165
169
|
// The marketplace was cloned sparse (only the manifest dirs are on disk),
|
|
166
170
|
// so a local-path plugin's tree may not be checked out yet. Expand the
|
|
167
171
|
// sparse-checkout to include it before reading. Best-effort: on a
|
|
168
172
|
// non-sparse (full) clone this errors harmlessly and the files are already
|
|
169
173
|
// present, so we ignore the result and let materializePath report a real
|
|
170
174
|
// missing-path error if the subdir truly isn't there.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await gitSparseCheckoutAdd(marketplaceInstallLocation, sparseRel);
|
|
177
|
-
}
|
|
175
|
+
// Strip a leading "./" — in non-cone sparse mode the literal "./" prefix
|
|
176
|
+
// doesn't match repo paths (which are stored without it), so the
|
|
177
|
+
// expand would silently no-op and the tree would stay un-materialized.
|
|
178
|
+
const sparseRel = source.replace(/^\.\//, "");
|
|
179
|
+
await gitSparseCheckoutAdd(marketplaceInstallLocation, sparseRel);
|
|
178
180
|
const r = await materializePath(marketplaceInstallLocation, source, placeholder);
|
|
179
181
|
if (!r.ok)
|
|
180
182
|
return r;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChatSession } from "./chat-session.js";
|
|
2
2
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
3
3
|
import { clearAgentOutputFiles } from "../tool-system/builtin/agent-output-file.js";
|
|
4
|
+
import { logger } from "../logging/logger.js";
|
|
4
5
|
export class ChatSessionManager {
|
|
5
6
|
sessions = new Map();
|
|
6
7
|
runtime;
|
|
@@ -59,6 +60,7 @@ export class ChatSessionManager {
|
|
|
59
60
|
if (!s)
|
|
60
61
|
return;
|
|
61
62
|
s.cancel();
|
|
63
|
+
this.unregisterMcpOwner(s);
|
|
62
64
|
this.sessions.delete(sessionId);
|
|
63
65
|
}
|
|
64
66
|
closeAll() {
|
|
@@ -110,4 +112,15 @@ export class ChatSessionManager {
|
|
|
110
112
|
clearInterval(this.sweeper);
|
|
111
113
|
this.sweeper = null;
|
|
112
114
|
}
|
|
115
|
+
unregisterMcpOwner(session) {
|
|
116
|
+
const mcpPool = this.runtime.mcpPool;
|
|
117
|
+
if (typeof mcpPool?.unregisterOwner !== "function")
|
|
118
|
+
return;
|
|
119
|
+
void mcpPool.unregisterOwner(session.engine).catch((err) => {
|
|
120
|
+
logger.warn("chat_session.mcp_owner_unregister_failed", {
|
|
121
|
+
sessionId: session.id,
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
113
126
|
}
|
|
@@ -84,6 +84,14 @@ export declare class AgentServer {
|
|
|
84
84
|
private approvalTimers;
|
|
85
85
|
/** Default approval timeout: 5 minutes */
|
|
86
86
|
private static readonly APPROVAL_TIMEOUT_MS;
|
|
87
|
+
/**
|
|
88
|
+
* Wall-clock timeout for AskUserQuestion during a GOAL run (10 minutes).
|
|
89
|
+
* Plain interactive asks stay untimed; only a self-driving goal run arms this
|
|
90
|
+
* so a mid-goal question can't suspend the loop forever with nobody watching.
|
|
91
|
+
* Longer than the tool-approval timeout — a user who IS present deserves more
|
|
92
|
+
* time to compose a real answer before we assume-and-continue.
|
|
93
|
+
*/
|
|
94
|
+
private static readonly GOAL_ASKUSER_TIMEOUT_MS;
|
|
87
95
|
/**
|
|
88
96
|
* Unsubscribe handle for the process-local `agentNotificationBus`
|
|
89
97
|
* subscription set up in the constructor. Called from `close()` so a
|