@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.
Files changed (65) hide show
  1. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  2. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  3. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  4. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  5. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  6. package/dist/cc-orchestrator/external-agent-session-store.js +144 -0
  7. package/dist/context/manager.d.ts +3 -1
  8. package/dist/context/manager.js +24 -15
  9. package/dist/credentials/types.d.ts +2 -2
  10. package/dist/engine/engine.d.ts +5 -1
  11. package/dist/engine/engine.js +92 -51
  12. package/dist/engine/turn-loop.js +1 -1
  13. package/dist/git/worktree.d.ts +49 -6
  14. package/dist/git/worktree.js +265 -31
  15. package/dist/index.d.ts +4 -3
  16. package/dist/index.js +3 -2
  17. package/dist/logging/logger.js +6 -6
  18. package/dist/plugins/installer/installFromSource.js +9 -1
  19. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  20. package/dist/plugins/installer/sourcePath.js +50 -0
  21. package/dist/plugins/pluginInstaller.js +24 -22
  22. package/dist/protocol/chat-session-manager.d.ts +1 -0
  23. package/dist/protocol/chat-session-manager.js +13 -0
  24. package/dist/protocol/server.d.ts +8 -0
  25. package/dist/protocol/server.js +45 -8
  26. package/dist/protocol/types.d.ts +4 -0
  27. package/dist/protocol/types.js +4 -0
  28. package/dist/run/FileRunStore.js +10 -1
  29. package/dist/run/Heartbeat.js +12 -0
  30. package/dist/run/RunApprovalBackend.d.ts +3 -0
  31. package/dist/run/RunApprovalBackend.js +41 -6
  32. package/dist/run/RunLock.js +2 -0
  33. package/dist/run/RunManager.d.ts +2 -0
  34. package/dist/run/RunManager.js +64 -24
  35. package/dist/run/ids.d.ts +2 -0
  36. package/dist/run/ids.js +23 -0
  37. package/dist/session/session-manager.d.ts +35 -1
  38. package/dist/session/session-manager.js +189 -2
  39. package/dist/settings/manager.d.ts +1 -0
  40. package/dist/settings/manager.js +45 -26
  41. package/dist/settings/schema-export.d.ts +2 -3
  42. package/dist/settings/schema-export.js +2 -3
  43. package/dist/tool-system/builtin/background-jobs.d.ts +8 -1
  44. package/dist/tool-system/builtin/background-jobs.js +8 -1
  45. package/dist/tool-system/builtin/config.d.ts +2 -1
  46. package/dist/tool-system/builtin/config.js +16 -11
  47. package/dist/tool-system/builtin/drive-claude-code.d.ts +15 -2
  48. package/dist/tool-system/builtin/drive-claude-code.js +174 -39
  49. package/dist/tool-system/builtin/edit.js +5 -2
  50. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  51. package/dist/tool-system/builtin/generate-video.js +13 -4
  52. package/dist/tool-system/builtin/index.js +5 -1
  53. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  54. package/dist/tool-system/builtin/lsp.js +6 -3
  55. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  56. package/dist/tool-system/builtin/read.js +5 -2
  57. package/dist/tool-system/builtin/worktree.d.ts +2 -4
  58. package/dist/tool-system/builtin/worktree.js +250 -75
  59. package/dist/tool-system/builtin/write.js +5 -3
  60. package/dist/tool-system/context.d.ts +12 -0
  61. package/dist/tool-system/mcp-manager.d.ts +8 -0
  62. package/dist/tool-system/mcp-manager.js +32 -11
  63. package/dist/types.d.ts +12 -0
  64. package/dist/utils/toolDisplay.js +1 -1
  65. 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
- mkdirSync(sessionDir, { recursive: true });
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`
@@ -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
- const parts = key.split(".");
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
- const parts = key.split(".");
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.split(".");
375
+ const parts = parseDottedSettingKey(key);
357
376
  let target = current;
358
377
  for (let i = 0; i < parts.length - 1; i++) {
359
- const next = target?.[parts[i]];
360
- if (!next || typeof next !== "object" || Array.isArray(next))
378
+ const seg = parts[i];
379
+ if (!target || !isOwnPlainObject(target, seg))
361
380
  return;
362
- target = next;
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 boot. A host that
11
- * wants the file on disk should call `writeSettingsSchemaFile()` explicitly
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 boot. A host that
11
- * wants the file on disk should call `writeSettingsSchemaFile()` explicitly
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>): Promise<string>;
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 = args.__cwd ?? process.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
- // Set nested key
53
- const parts = key.split(".");
54
- let obj = settings;
55
- for (let i = 0; i < parts.length - 1; i++) {
56
- if (typeof obj[parts[i]] !== "object" || obj[parts[i]] === null) {
57
- obj[parts[i]] = {};
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 {};