@cjhyy/code-shell-core 0.6.0-rc.12 → 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,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session lifecycle manager.
|
|
3
3
|
*/
|
|
4
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { Transcript } from "./transcript.js";
|
|
9
9
|
import { SessionError } from "../exceptions.js";
|
|
10
10
|
import { normalizeCumulativeUsageCounters } from "../engine/session-usage.js";
|
|
11
|
+
import { branchExists, isGitWorktreeRoot } from "../git/worktree.js";
|
|
11
12
|
/**
|
|
12
13
|
* Validate a session ID before it is joined into a filesystem path.
|
|
13
14
|
*
|
|
@@ -55,6 +56,44 @@ export function assertSafeSessionId(sessionId) {
|
|
|
55
56
|
export function codeShellHome() {
|
|
56
57
|
return process.env.CODE_SHELL_HOME || join(homedir(), ".code-shell");
|
|
57
58
|
}
|
|
59
|
+
function isSessionWorkspace(value) {
|
|
60
|
+
if (!value || typeof value !== "object")
|
|
61
|
+
return false;
|
|
62
|
+
const ws = value;
|
|
63
|
+
if (typeof ws.root !== "string" || ws.root.length === 0)
|
|
64
|
+
return false;
|
|
65
|
+
if (ws.kind !== "main" && ws.kind !== "worktree")
|
|
66
|
+
return false;
|
|
67
|
+
if (ws.kind === "main")
|
|
68
|
+
return ws.worktree === undefined;
|
|
69
|
+
const wt = ws.worktree;
|
|
70
|
+
return (!!wt &&
|
|
71
|
+
typeof wt.path === "string" &&
|
|
72
|
+
wt.path.length > 0 &&
|
|
73
|
+
typeof wt.branch === "string" &&
|
|
74
|
+
wt.branch.length > 0 &&
|
|
75
|
+
typeof wt.baseRef === "string" &&
|
|
76
|
+
wt.baseRef.length > 0 &&
|
|
77
|
+
wt.createdBy === "codeshell");
|
|
78
|
+
}
|
|
79
|
+
function validateResumeWorktreeRoot(root) {
|
|
80
|
+
if (!existsSync(root))
|
|
81
|
+
return "no longer exists";
|
|
82
|
+
let stat;
|
|
83
|
+
try {
|
|
84
|
+
stat = lstatSync(root);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return "no longer exists";
|
|
88
|
+
}
|
|
89
|
+
if (stat.isSymbolicLink())
|
|
90
|
+
return "is not a valid git worktree (symbolic link)";
|
|
91
|
+
if (!stat.isDirectory())
|
|
92
|
+
return "is not a valid git worktree (not a directory)";
|
|
93
|
+
if (!isGitWorktreeRoot(root))
|
|
94
|
+
return "is not a valid git worktree";
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
58
97
|
export class SessionManager {
|
|
59
98
|
sessionsDir;
|
|
60
99
|
constructor(storageDir) {
|
|
@@ -76,10 +115,19 @@ export class SessionManager {
|
|
|
76
115
|
assertSafeSessionId(explicitSessionId);
|
|
77
116
|
const sessionId = explicitSessionId ?? nanoid(16);
|
|
78
117
|
const sessionDir = join(this.sessionsDir, sessionId);
|
|
79
|
-
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(sessionDir);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err.code === "EEXIST") {
|
|
123
|
+
throw new SessionError(`Session already exists: ${sessionId}`);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
80
127
|
const state = {
|
|
81
128
|
sessionId,
|
|
82
129
|
cwd,
|
|
130
|
+
workspace: { root: cwd, kind: "main" },
|
|
83
131
|
startedAt: Date.now(),
|
|
84
132
|
model,
|
|
85
133
|
provider,
|
|
@@ -158,6 +206,143 @@ export class SessionManager {
|
|
|
158
206
|
return undefined;
|
|
159
207
|
}
|
|
160
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Disk-only workspace pointer reader. Legacy sessions written before
|
|
211
|
+
* `workspace` existed are treated as main-workspace sessions rooted at
|
|
212
|
+
* `state.cwd`; the read is intentionally non-mutating.
|
|
213
|
+
*/
|
|
214
|
+
getSessionWorkspace(sessionId) {
|
|
215
|
+
try {
|
|
216
|
+
assertSafeSessionId(sessionId);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
222
|
+
if (!existsSync(stateFile))
|
|
223
|
+
return undefined;
|
|
224
|
+
try {
|
|
225
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
226
|
+
if (isSessionWorkspace(state.workspace))
|
|
227
|
+
return state.workspace;
|
|
228
|
+
return typeof state.cwd === "string" && state.cwd.length > 0
|
|
229
|
+
? { root: state.cwd, kind: "main" }
|
|
230
|
+
: undefined;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Persist the current session workspace pointer without changing legacy
|
|
238
|
+
* `cwd`. P1 will teach ToolContext to resolve cwd from this field; for P0 it
|
|
239
|
+
* is a safety pointer and resume breadcrumb.
|
|
240
|
+
*/
|
|
241
|
+
setSessionWorkspace(sessionId, workspace) {
|
|
242
|
+
assertSafeSessionId(sessionId);
|
|
243
|
+
if (!isSessionWorkspace(workspace)) {
|
|
244
|
+
throw new SessionError(`invalid workspace for session ${sessionId}`);
|
|
245
|
+
}
|
|
246
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
247
|
+
if (!existsSync(stateFile)) {
|
|
248
|
+
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
249
|
+
}
|
|
250
|
+
let state;
|
|
251
|
+
try {
|
|
252
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
state.workspace = workspace;
|
|
258
|
+
this.saveState(state);
|
|
259
|
+
}
|
|
260
|
+
recordWorkspaceHandoff(sessionId, from, to) {
|
|
261
|
+
assertSafeSessionId(sessionId);
|
|
262
|
+
const transcriptFile = join(this.sessionsDir, sessionId, "transcript.jsonl");
|
|
263
|
+
if (!existsSync(transcriptFile))
|
|
264
|
+
return;
|
|
265
|
+
try {
|
|
266
|
+
const transcript = new Transcript(transcriptFile);
|
|
267
|
+
transcript.append("session_meta", {
|
|
268
|
+
sessionId,
|
|
269
|
+
cwd: to.root,
|
|
270
|
+
workspace: to,
|
|
271
|
+
handoffFrom: from?.root,
|
|
272
|
+
handoffAt: Date.now(),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Transcript handoff metadata is best-effort; state.workspace is the
|
|
277
|
+
// authoritative switch pointer.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolve the cwd a resumed session must run in from its persisted workspace
|
|
282
|
+
* pointer. A missing worktree directory is never silently treated as the main
|
|
283
|
+
* repo: if the branch still exists callers get a blocking recreate message;
|
|
284
|
+
* if the branch is gone the workspace pointer is reset to main and a warning
|
|
285
|
+
* message is returned for the host to surface before continuing.
|
|
286
|
+
*/
|
|
287
|
+
resolveSessionWorkspaceForResume(sessionId) {
|
|
288
|
+
assertSafeSessionId(sessionId);
|
|
289
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
290
|
+
if (!existsSync(stateFile)) {
|
|
291
|
+
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
292
|
+
}
|
|
293
|
+
let state;
|
|
294
|
+
try {
|
|
295
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
299
|
+
}
|
|
300
|
+
const legacyMain = typeof state.cwd === "string" && state.cwd.length > 0
|
|
301
|
+
? { root: state.cwd, kind: "main" }
|
|
302
|
+
: undefined;
|
|
303
|
+
const workspace = isSessionWorkspace(state.workspace) ? state.workspace : legacyMain;
|
|
304
|
+
if (!workspace) {
|
|
305
|
+
throw new SessionError(`Session ${sessionId} has no recoverable cwd`);
|
|
306
|
+
}
|
|
307
|
+
if (workspace.kind === "main") {
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
cwd: workspace.root,
|
|
311
|
+
workspace,
|
|
312
|
+
reason: isSessionWorkspace(state.workspace) ? "main" : "legacy",
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const invalidWorktreeReason = validateResumeWorktreeRoot(workspace.root);
|
|
316
|
+
if (!invalidWorktreeReason) {
|
|
317
|
+
return { ok: true, cwd: workspace.root, workspace, reason: "worktree" };
|
|
318
|
+
}
|
|
319
|
+
const branch = workspace.worktree?.branch;
|
|
320
|
+
const mainRoot = typeof state.cwd === "string" && state.cwd.length > 0 ? state.cwd : workspace.root;
|
|
321
|
+
if (branch && existsSync(mainRoot) && branchExists(mainRoot, branch)) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
cwd: mainRoot,
|
|
325
|
+
workspace,
|
|
326
|
+
reason: "worktree_missing_branch_exists",
|
|
327
|
+
message: `Session ${sessionId} is bound to worktree ${workspace.root}, but that directory ` +
|
|
328
|
+
`${invalidWorktreeReason}. Branch ${branch} still exists; recreate the worktree at ` +
|
|
329
|
+
`${workspace.worktree?.path ?? workspace.root} before resuming, or switch this session ` +
|
|
330
|
+
`back to main explicitly.`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const fallback = { root: mainRoot, kind: "main" };
|
|
334
|
+
state.workspace = fallback;
|
|
335
|
+
this.saveState(state);
|
|
336
|
+
return {
|
|
337
|
+
ok: true,
|
|
338
|
+
cwd: mainRoot,
|
|
339
|
+
workspace: fallback,
|
|
340
|
+
reason: "worktree_missing_branch_gone",
|
|
341
|
+
message: `Session ${sessionId} was bound to worktree ${workspace.root}, but that directory ` +
|
|
342
|
+
`${invalidWorktreeReason}${branch ? ` and branch ${branch} is gone` : ""}; fell back to main ` +
|
|
343
|
+
`${mainRoot}. Re-run the request if you want to continue there.`,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
161
346
|
/**
|
|
162
347
|
* Cheap "does this session have a persisted goal?" probe — reads only
|
|
163
348
|
* state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY
|
|
@@ -436,6 +621,8 @@ function parseUserPreview(line) {
|
|
|
436
621
|
return undefined;
|
|
437
622
|
if (event.data?.role !== "user")
|
|
438
623
|
return undefined;
|
|
624
|
+
if (event.data.injected === true)
|
|
625
|
+
return undefined;
|
|
439
626
|
const content = event.data.content;
|
|
440
627
|
const text = typeof content === "string"
|
|
441
628
|
? content
|
|
@@ -18,6 +18,7 @@ export declare function userHome(): string;
|
|
|
18
18
|
* ("permissions.defaultMode", "env.FOO") are caught too.
|
|
19
19
|
*/
|
|
20
20
|
export declare function isProtectedSettingKey(key: string): boolean;
|
|
21
|
+
export declare function setDottedSetting(target: Record<string, unknown>, key: string, value: unknown): void;
|
|
21
22
|
/**
|
|
22
23
|
* The fixed cwd used for "no-repo" pure-chat conversations (a chat not bound to
|
|
23
24
|
* any code project). Same location as desktop's `resolveNoRepoCwd`
|
package/dist/settings/manager.js
CHANGED
|
@@ -50,6 +50,45 @@ export function isProtectedSettingKey(key) {
|
|
|
50
50
|
const root = key.split(".")[0] ?? "";
|
|
51
51
|
return PROTECTED_SETTING_ROOTS.has(root);
|
|
52
52
|
}
|
|
53
|
+
const FORBIDDEN_SETTING_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
54
|
+
function parseDottedSettingKey(key) {
|
|
55
|
+
const parts = key.split(".");
|
|
56
|
+
if (parts.length === 0 ||
|
|
57
|
+
parts.some((seg) => seg.length === 0 || FORBIDDEN_SETTING_KEY_SEGMENTS.has(seg))) {
|
|
58
|
+
throw new Error(`invalid setting key: ${key}`);
|
|
59
|
+
}
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
function isOwnPlainObject(parent, key) {
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(parent, key))
|
|
64
|
+
return false;
|
|
65
|
+
const value = parent[key];
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
67
|
+
return false;
|
|
68
|
+
const proto = Object.getPrototypeOf(value);
|
|
69
|
+
return proto === Object.prototype || proto === null;
|
|
70
|
+
}
|
|
71
|
+
function descendForSettingWrite(target, key, fullKey) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
73
|
+
const inherited = target[key];
|
|
74
|
+
if (inherited && typeof inherited === "object") {
|
|
75
|
+
throw new Error(`invalid setting key: ${fullKey} (refusing to descend through inherited object segment: ${key})`);
|
|
76
|
+
}
|
|
77
|
+
target[key] = {};
|
|
78
|
+
}
|
|
79
|
+
else if (!isOwnPlainObject(target, key)) {
|
|
80
|
+
target[key] = {};
|
|
81
|
+
}
|
|
82
|
+
return target[key];
|
|
83
|
+
}
|
|
84
|
+
export function setDottedSetting(target, key, value) {
|
|
85
|
+
const parts = parseDottedSettingKey(key);
|
|
86
|
+
let current = target;
|
|
87
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
88
|
+
current = descendForSettingWrite(current, parts[i], key);
|
|
89
|
+
}
|
|
90
|
+
current[parts[parts.length - 1]] = value;
|
|
91
|
+
}
|
|
53
92
|
/**
|
|
54
93
|
* The fixed cwd used for "no-repo" pure-chat conversations (a chat not bound to
|
|
55
94
|
* any code project). Same location as desktop's `resolveNoRepoCwd`
|
|
@@ -286,17 +325,7 @@ export class SettingsManager {
|
|
|
286
325
|
// Corrupt file — overwrite rather than crash.
|
|
287
326
|
}
|
|
288
327
|
}
|
|
289
|
-
|
|
290
|
-
let target = current;
|
|
291
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
292
|
-
const seg = parts[i];
|
|
293
|
-
const next = target[seg];
|
|
294
|
-
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
295
|
-
target[seg] = {};
|
|
296
|
-
}
|
|
297
|
-
target = target[seg];
|
|
298
|
-
}
|
|
299
|
-
target[parts[parts.length - 1]] = value;
|
|
328
|
+
setDottedSetting(current, key, value);
|
|
300
329
|
mkdirSync(dirname(path), { recursive: true });
|
|
301
330
|
// Atomic write: stage to .tmp, then rename, so a concurrent read can't
|
|
302
331
|
// catch a half-written file. mode 0o600 — settings.json can hold plaintext
|
|
@@ -323,17 +352,7 @@ export class SettingsManager {
|
|
|
323
352
|
if (!existsSync(cwd))
|
|
324
353
|
return;
|
|
325
354
|
const current = this.readJsonObject(path);
|
|
326
|
-
|
|
327
|
-
let target = current;
|
|
328
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
329
|
-
const seg = parts[i];
|
|
330
|
-
const next = target[seg];
|
|
331
|
-
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
332
|
-
target[seg] = {};
|
|
333
|
-
}
|
|
334
|
-
target = target[seg];
|
|
335
|
-
}
|
|
336
|
-
target[parts[parts.length - 1]] = value;
|
|
355
|
+
setDottedSetting(current, key, value);
|
|
337
356
|
this.atomicWriteJson(path, current);
|
|
338
357
|
this.invalidate();
|
|
339
358
|
}
|
|
@@ -353,13 +372,13 @@ export class SettingsManager {
|
|
|
353
372
|
if (!resolveConfigPath(path))
|
|
354
373
|
return;
|
|
355
374
|
const current = this.readJsonObject(path);
|
|
356
|
-
const parts = key
|
|
375
|
+
const parts = parseDottedSettingKey(key);
|
|
357
376
|
let target = current;
|
|
358
377
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
359
|
-
const
|
|
360
|
-
if (!
|
|
378
|
+
const seg = parts[i];
|
|
379
|
+
if (!target || !isOwnPlainObject(target, seg))
|
|
361
380
|
return;
|
|
362
|
-
target =
|
|
381
|
+
target = target[seg];
|
|
363
382
|
}
|
|
364
383
|
if (target)
|
|
365
384
|
delete target[parts[parts.length - 1]];
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This module is side-effect free: it only EXPOSES the generator and a writer.
|
|
9
9
|
* `manager.load()` deliberately does NOT call these — emitting a file during
|
|
10
|
-
* load would pollute test HOMEs and add disk I/O to every
|
|
11
|
-
*
|
|
12
|
-
* (wiring TBD — see the task return note).
|
|
10
|
+
* load would pollute test HOMEs and add disk I/O to every settings read. Hosts
|
|
11
|
+
* write it explicitly during startup as a best-effort editor aid.
|
|
13
12
|
*/
|
|
14
13
|
/**
|
|
15
14
|
* Generate the JSON Schema for the settings object. The `name` option makes
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This module is side-effect free: it only EXPOSES the generator and a writer.
|
|
9
9
|
* `manager.load()` deliberately does NOT call these — emitting a file during
|
|
10
|
-
* load would pollute test HOMEs and add disk I/O to every
|
|
11
|
-
*
|
|
12
|
-
* (wiring TBD — see the task return note).
|
|
10
|
+
* load would pollute test HOMEs and add disk I/O to every settings read. Hosts
|
|
11
|
+
* write it explicitly during startup as a best-effort editor aid.
|
|
13
12
|
*/
|
|
14
13
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
15
14
|
import { dirname, join } from "node:path";
|
|
@@ -40,6 +40,8 @@ export interface BackgroundJobEntry {
|
|
|
40
40
|
ccSessionId?: string;
|
|
41
41
|
/** Files the external agent changed (parsed from its transcript, #6). */
|
|
42
42
|
changedFiles?: string[];
|
|
43
|
+
/** Working directory for jobs that operate on the filesystem, e.g. DriveAgent. */
|
|
44
|
+
cwd?: string;
|
|
43
45
|
}
|
|
44
46
|
/** Outcome passed to finish() to record how a job ended. */
|
|
45
47
|
export interface BackgroundJobOutcome {
|
|
@@ -48,11 +50,14 @@ export interface BackgroundJobOutcome {
|
|
|
48
50
|
ccSessionId?: string;
|
|
49
51
|
changedFiles?: string[];
|
|
50
52
|
}
|
|
53
|
+
export interface BackgroundJobStartOptions {
|
|
54
|
+
cwd?: string;
|
|
55
|
+
}
|
|
51
56
|
declare class BackgroundJobRegistry {
|
|
52
57
|
private jobs;
|
|
53
58
|
private listeners;
|
|
54
59
|
/** Register a running job. Invalid sessionId is ignored (cannot be waited on). */
|
|
55
|
-
start(jobId: string, sessionId: string, description?: string): void;
|
|
60
|
+
start(jobId: string, sessionId: string, description?: string, options?: BackgroundJobStartOptions): void;
|
|
56
61
|
/** Mark a job terminal (retained, not deleted). Unknown id is a no-op (no
|
|
57
62
|
* notify) so a double-finish or a finish after reset stays quiet. */
|
|
58
63
|
finish(jobId: string, outcome?: BackgroundJobOutcome): void;
|
|
@@ -60,6 +65,8 @@ declare class BackgroundJobRegistry {
|
|
|
60
65
|
hasRunningForSession(sessionId: string): boolean;
|
|
61
66
|
/** Running jobs spawned by `sessionId`. Feeds the goal judge's task list. */
|
|
62
67
|
listRunningForSession(sessionId: string): BackgroundJobEntry[];
|
|
68
|
+
/** Running jobs, across all sessions, that are operating in the same cwd. */
|
|
69
|
+
listRunningByCwd(cwd: string): BackgroundJobEntry[];
|
|
63
70
|
/** All jobs (running + retained terminal) for `sessionId`. Feeds the panel. */
|
|
64
71
|
listForSession(sessionId: string): BackgroundJobEntry[];
|
|
65
72
|
/** Drop every job of a session — called when the session is deleted/closed. */
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* loops run in the main engine process, so start/finish and the engine's wait
|
|
21
21
|
* loop observe the same instance.
|
|
22
22
|
*/
|
|
23
|
+
import { normalizeCwdPath } from "../../cc-orchestrator/cwd-normalize.js";
|
|
23
24
|
function isValidSessionId(sid) {
|
|
24
25
|
return typeof sid === "string" && sid.length > 0;
|
|
25
26
|
}
|
|
@@ -30,7 +31,7 @@ class BackgroundJobRegistry {
|
|
|
30
31
|
jobs = new Map(); // jobId -> entry (insertion-ordered)
|
|
31
32
|
listeners = new Set();
|
|
32
33
|
/** Register a running job. Invalid sessionId is ignored (cannot be waited on). */
|
|
33
|
-
start(jobId, sessionId, description = "") {
|
|
34
|
+
start(jobId, sessionId, description = "", options) {
|
|
34
35
|
if (!isValidSessionId(sessionId))
|
|
35
36
|
return;
|
|
36
37
|
this.jobs.set(jobId, {
|
|
@@ -39,6 +40,7 @@ class BackgroundJobRegistry {
|
|
|
39
40
|
description,
|
|
40
41
|
status: "running",
|
|
41
42
|
startedAt: Date.now(),
|
|
43
|
+
...(options?.cwd ? { cwd: normalizeCwdPath(options.cwd) } : {}),
|
|
42
44
|
});
|
|
43
45
|
this.notify();
|
|
44
46
|
}
|
|
@@ -71,6 +73,11 @@ class BackgroundJobRegistry {
|
|
|
71
73
|
listRunningForSession(sessionId) {
|
|
72
74
|
return [...this.jobs.values()].filter((e) => e.sessionId === sessionId && e.status === "running");
|
|
73
75
|
}
|
|
76
|
+
/** Running jobs, across all sessions, that are operating in the same cwd. */
|
|
77
|
+
listRunningByCwd(cwd) {
|
|
78
|
+
const normalized = normalizeCwdPath(cwd);
|
|
79
|
+
return [...this.jobs.values()].filter((e) => e.status === "running" && e.cwd === normalized);
|
|
80
|
+
}
|
|
74
81
|
/** All jobs (running + retained terminal) for `sessionId`. Feeds the panel. */
|
|
75
82
|
listForSession(sessionId) {
|
|
76
83
|
return [...this.jobs.values()].filter((e) => e.sessionId === sessionId);
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* ConfigTool — read or update project settings.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
import type { ToolContext } from "../context.js";
|
|
5
6
|
export declare const configToolDef: ToolDefinition;
|
|
6
|
-
export declare function configTool(args: Record<string, unknown
|
|
7
|
+
export declare function configTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { setDottedSetting } from "../../settings/manager.js";
|
|
7
|
+
import { enforcePathPolicyWithApproval } from "../path-policy.js";
|
|
6
8
|
export const configToolDef = {
|
|
7
9
|
name: "Config",
|
|
8
10
|
description: "Read or update the project's .code-shell/settings.json configuration. " +
|
|
@@ -26,11 +28,14 @@ export const configToolDef = {
|
|
|
26
28
|
required: ["action"],
|
|
27
29
|
},
|
|
28
30
|
};
|
|
29
|
-
export async function configTool(args) {
|
|
31
|
+
export async function configTool(args, ctx) {
|
|
30
32
|
const action = args.action;
|
|
31
|
-
const cwd =
|
|
33
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
32
34
|
const configPath = join(cwd, ".code-shell", "settings.json");
|
|
33
35
|
if (action === "read") {
|
|
36
|
+
const blocked = await enforcePathPolicyWithApproval(configPath, "read", ctx);
|
|
37
|
+
if (blocked)
|
|
38
|
+
return blocked;
|
|
34
39
|
if (!existsSync(configPath)) {
|
|
35
40
|
return "No project settings found. Use /init to create one.";
|
|
36
41
|
}
|
|
@@ -38,6 +43,9 @@ export async function configTool(args) {
|
|
|
38
43
|
return content;
|
|
39
44
|
}
|
|
40
45
|
if (action === "write") {
|
|
46
|
+
const blocked = await enforcePathPolicyWithApproval(configPath, "write", ctx);
|
|
47
|
+
if (blocked)
|
|
48
|
+
return blocked;
|
|
41
49
|
const key = args.key;
|
|
42
50
|
const value = args.value;
|
|
43
51
|
if (!key)
|
|
@@ -49,16 +57,13 @@ export async function configTool(args) {
|
|
|
49
57
|
if (existsSync(configPath)) {
|
|
50
58
|
settings = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
obj = obj[parts[i]];
|
|
60
|
+
try {
|
|
61
|
+
setDottedSetting(settings, key, value);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
return `Error: ${message}`;
|
|
60
66
|
}
|
|
61
|
-
obj[parts[parts.length - 1]] = value;
|
|
62
67
|
// Never resurrect a deleted project root: a recursive mkdir of
|
|
63
68
|
// <cwd>/.code-shell would recreate `cwd` itself as an empty shell when the
|
|
64
69
|
// directory has been deleted (e.g. a stale session pointing at a removed
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../../types.js";
|
|
2
2
|
import type { AgentRunResult } from "../../cc-orchestrator/external-agent-driver.js";
|
|
3
3
|
import type { ToolContext } from "../context.js";
|
|
4
|
+
import { type ExternalAgentSessionBinding, type ExternalAgentSessionRecord } from "../../cc-orchestrator/external-agent-session-store.js";
|
|
4
5
|
export type DriveCli = "claude" | "codex";
|
|
6
|
+
export declare const DRIVE_AGENT_FOREGROUND_HANDOFF_MS = 110000;
|
|
7
|
+
export declare const DRIVE_AGENT_TOOL_TIMEOUT_MS = 1800000;
|
|
5
8
|
export declare const driveAgentToolDef: ToolDefinition;
|
|
6
9
|
type PermMode = "default" | "acceptEdits" | "bypassPermissions";
|
|
7
10
|
type Runner = (opts: {
|
|
@@ -10,10 +13,19 @@ type Runner = (opts: {
|
|
|
10
13
|
resumeSessionId?: string;
|
|
11
14
|
cwd: string;
|
|
12
15
|
permissionMode?: PermMode;
|
|
16
|
+
signal?: AbortSignal;
|
|
13
17
|
}) => Promise<AgentRunResult>;
|
|
18
|
+
type SessionStore = {
|
|
19
|
+
get(cli: DriveCli, sessionId: string): ExternalAgentSessionBinding | undefined;
|
|
20
|
+
record(binding: ExternalAgentSessionRecord): void;
|
|
21
|
+
};
|
|
22
|
+
export interface DriveAgentToolOptions {
|
|
23
|
+
foregroundHandoffMs?: number;
|
|
24
|
+
sessionStore?: SessionStore;
|
|
25
|
+
}
|
|
14
26
|
/** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
|
|
15
27
|
* cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
|
|
16
|
-
export declare function makeDriveAgentTool(runner?: Runner, fixedCli?: DriveCli): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
28
|
+
export declare function makeDriveAgentTool(runner?: Runner, fixedCli?: DriveCli, options?: DriveAgentToolOptions): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
17
29
|
export declare const driveAgentTool: (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
18
30
|
export declare const driveClaudeCodeToolDef: ToolDefinition;
|
|
19
31
|
/** Back-compat factory: a DriveAgent pinned to cli:"claude" with the `cli` arg
|
|
@@ -24,7 +36,8 @@ type LegacyRunner = (opts: {
|
|
|
24
36
|
resumeSessionId?: string;
|
|
25
37
|
cwd: string;
|
|
26
38
|
permissionMode?: PermMode;
|
|
39
|
+
signal?: AbortSignal;
|
|
27
40
|
}) => Promise<AgentRunResult>;
|
|
28
|
-
export declare function makeDriveClaudeCodeTool(runner?: LegacyRunner): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
41
|
+
export declare function makeDriveClaudeCodeTool(runner?: LegacyRunner, options?: DriveAgentToolOptions): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
29
42
|
export declare const driveClaudeCodeTool: (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
|
|
30
43
|
export {};
|