@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
@@ -34,7 +34,7 @@ import { PLAN_MODE_ALLOWED_TOOLS } from "../tool-system/plan-mode-allowlist.js";
34
34
  import { PromptComposer } from "../prompt/composer.js";
35
35
  import { SessionManager } from "../session/session-manager.js";
36
36
  import { ModelFacade } from "./model-facade.js";
37
- import { logger, setCurrentSid, runWithSid, getCurrentSid } from "../logging/logger.js";
37
+ import { logger, runWithSid, getCurrentSid } from "../logging/logger.js";
38
38
  import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
39
39
  import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
40
40
  import { TurnLoop } from "./turn-loop.js";
@@ -129,7 +129,7 @@ export function resolveChildLlm(modelKey, pool, parentLlm) {
129
129
  * Names in `disabledAgents` are filtered out so the LLM never sees them.
130
130
  */
131
131
  /**
132
- * Resolve the working directory for a run. Precedence:
132
+ * Resolve the working directory for a run. Precedence for legacy sessions:
133
133
  * options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
134
134
  *
135
135
  * The session-cwd tier is what stops a project-bound session from being
@@ -735,20 +735,45 @@ export class Engine {
735
735
  * Run a task from start to finish.
736
736
  */
737
737
  async run(task, options) {
738
- // When the caller omits cwd but is resuming an existing session, recover
739
- // that session's bound cwd from disk so a project-bound session keeps
740
- // loading its own agents/settings/memory even if the host's UI repo
741
- // selection has drifted to null. Only probe on omission — an explicit cwd
742
- // always wins, and a fresh session has nothing to recover.
743
- const sessionCwd = options?.cwd === undefined && options?.sessionId
744
- ? this.sessionManager.readCwd(options.sessionId)
738
+ const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
739
+ ? this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
745
740
  : undefined;
746
- const cwd = resolveRunCwd({
747
- optionCwd: options?.cwd,
748
- sessionCwd,
749
- configCwd: this.config.cwd,
750
- processCwd: process.cwd(),
751
- });
741
+ if (workspaceResume && !workspaceResume.ok) {
742
+ return {
743
+ text: `ERROR: ${workspaceResume.message}`,
744
+ reason: "completed",
745
+ sessionId: options.sessionId,
746
+ turnCount: 0,
747
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
748
+ };
749
+ }
750
+ if (workspaceResume?.ok &&
751
+ workspaceResume.reason === "worktree_missing_branch_gone" &&
752
+ workspaceResume.message) {
753
+ return {
754
+ text: workspaceResume.message,
755
+ reason: "completed",
756
+ sessionId: options.sessionId,
757
+ turnCount: 0,
758
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
759
+ };
760
+ }
761
+ // Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
762
+ // passes a stale cwd. Legacy sessions without workspace keep the historical
763
+ // explicit-cwd precedence for backward compatibility.
764
+ const workspaceCwd = workspaceResume?.ok && workspaceResume.reason !== "legacy" ? workspaceResume.cwd : undefined;
765
+ const sessionCwd = workspaceCwd === undefined && options?.cwd === undefined && options?.sessionId
766
+ ? workspaceResume?.ok
767
+ ? workspaceResume.cwd
768
+ : this.sessionManager.readCwd(options.sessionId)
769
+ : undefined;
770
+ const cwd = workspaceCwd ??
771
+ resolveRunCwd({
772
+ optionCwd: options?.cwd,
773
+ sessionCwd,
774
+ configCwd: this.config.cwd,
775
+ processCwd: process.cwd(),
776
+ });
752
777
  // Wrap the caller's onStream so we can intercept `task_update`
753
778
  // events emitted by TodoWrite and keep an in-engine snapshot.
754
779
  // TaskGuard reads this snapshot at turn end to decide whether to
@@ -1013,23 +1038,7 @@ export class Engine {
1013
1038
  },
1014
1039
  sessionExists: (sessionId) => this.sessionManager.exists(sessionId),
1015
1040
  };
1016
- // Priority: config.sandbox → project settings.sandbox → global → per-run
1017
- // default. Read UNMERGED per-scope (getForScope) so a project that wrote no
1018
- // sandbox genuinely follows global, rather than inheriting global's mode and
1019
- // looking like it set one. Fixes "项目级配了不生效" + the scope model.
1020
- let projectSandbox;
1021
- let globalSandbox;
1022
- try {
1023
- const sm = this.getSettingsManager();
1024
- if (this.config.isSubAgent !== true) {
1025
- projectSandbox = sm.getForScope("project", cwd).sandbox;
1026
- }
1027
- globalSandbox = sm.getForScope("user").sandbox;
1028
- }
1029
- catch {
1030
- // settings unavailable → fall through to per-run default
1031
- }
1032
- const sandboxConfig = resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
1041
+ const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
1033
1042
  // A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
1034
1043
  // per standard §S4. resolveSandboxBackend throws when an explicit
1035
1044
  // mode is unavailable on this host; we let it propagate. The
@@ -1070,12 +1079,16 @@ export class Engine {
1070
1079
  ? sandboxBackend
1071
1080
  : { ...sandboxBackend, network: sandboxConfig.network },
1072
1081
  cwd,
1082
+ shellEnv: this.readShellEnv(cwd),
1073
1083
  // TodoWrite reads this to push task_update events independently
1074
1084
  // of its return value, so the UI's pinned task panel refreshes
1075
1085
  // immediately rather than after the LLM next surfaces the
1076
1086
  // snapshot. wrappedOnStream snoops the same channel to keep
1077
1087
  // latestTodos current for TaskGuard.
1078
1088
  streamCallback: options?.onStream,
1089
+ setCwd(nextCwd) {
1090
+ toolCtx.cwd = nextCwd;
1091
+ },
1079
1092
  };
1080
1093
  logger.info("engine.run", {
1081
1094
  task: taskText.slice(0, 200),
@@ -1134,6 +1147,7 @@ export class Engine {
1134
1147
  let session;
1135
1148
  let messages;
1136
1149
  let freshImageMessage;
1150
+ let resumedFromDisk = false;
1137
1151
  const claimedClientMessageIds = new Set();
1138
1152
  const claimClientMessageId = (bundle, clientMessageId, source) => {
1139
1153
  if (!clientMessageId)
@@ -1151,6 +1165,7 @@ export class Engine {
1151
1165
  return true;
1152
1166
  };
1153
1167
  if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
1168
+ resumedFromDisk = true;
1154
1169
  session = this.sessionManager.resume(options.sessionId);
1155
1170
  const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
1156
1171
  messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
@@ -1233,21 +1248,6 @@ export class Engine {
1233
1248
  // with this value so `/undo` reverts exactly this turn's file changes.
1234
1249
  // (Both resume and cold-start paths converge here.)
1235
1250
  session.state.turnSeq = (session.state.turnSeq ?? 0) + 1;
1236
- // Stamp the resolved session id for downstream logging.
1237
- //
1238
- // `setCurrentSid` updates the module-level fallback so any code path
1239
- // running outside an ALS scope (bootstrap, /sid before a run starts)
1240
- // still sees the latest sid.
1241
- //
1242
- // The real isolation comes from wrapping the rest of `run` in
1243
- // `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
1244
- // call inside that closure — including those inside `await`ed child
1245
- // Engine.run() calls — reads sid from this scope's
1246
- // AsyncLocalStorage binding, not the module global. Sibling Engines
1247
- // running concurrently each get their own scope; an `enterSid` inside
1248
- // a child mutates that child's scope only and doesn't leak back to
1249
- // the parent's chain after `await child.run(...)` returns.
1250
- setCurrentSid(session.state.sessionId);
1251
1251
  // B2 / Gate 1: stamp the resolved sid onto the tool context so
1252
1252
  // session-scoped side effects (background-agent completion
1253
1253
  // notifications) attribute to the right session. toolCtx is created
@@ -1255,6 +1255,9 @@ export class Engine {
1255
1255
  // the first point we can set it. After this assignment treat the
1256
1256
  // field as immutable for the rest of the run.
1257
1257
  toolCtx.sessionId = session.state.sessionId;
1258
+ toolCtx.setSessionWorkspace = (workspace) => {
1259
+ session.state.workspace = workspace;
1260
+ };
1258
1261
  return runWithSid(session.state.sessionId, async () => {
1259
1262
  recordSessionStart(session.state.sessionId, {
1260
1263
  // Strip <codeshell-image> base64 payloads before they reach
@@ -1265,7 +1268,7 @@ export class Engine {
1265
1268
  model: this.config.llm.model,
1266
1269
  provider: this.config.llm.provider,
1267
1270
  permissionMode: this.config.permissionMode ?? "acceptEdits",
1268
- resumed: !!options?.sessionId,
1271
+ resumed: resumedFromDisk,
1269
1272
  });
1270
1273
  // Session-level hook: fired once per Engine.run() entry, regardless of
1271
1274
  // cold-start vs resume. Handlers can return `messages` to inject a
@@ -1275,7 +1278,8 @@ export class Engine {
1275
1278
  const sessionStartHook = await this.emitHook("on_session_start", {
1276
1279
  sessionId: session.state.sessionId,
1277
1280
  cwd,
1278
- resumed: !!options?.sessionId,
1281
+ resumed: resumedFromDisk,
1282
+ source: resumedFromDisk ? "resume" : "startup",
1279
1283
  });
1280
1284
  // Per-turn hook: fired every time a new user prompt enters the loop.
1281
1285
  // Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
@@ -1289,7 +1293,7 @@ export class Engine {
1289
1293
  // and silently leaking attachment bytes through hooks is the kind of
1290
1294
  // exfiltration risk a curious user-installed shell hook shouldn't carry.
1291
1295
  prompt: taskText,
1292
- resumed: !!options?.sessionId,
1296
+ resumed: resumedFromDisk,
1293
1297
  });
1294
1298
  // updatedPrompt: handler rewrote the user's prompt text. Replace the
1295
1299
  // last user message we just pushed (cold-start: line ~511; resume:
@@ -2837,6 +2841,25 @@ export class Engine {
2837
2841
  }
2838
2842
  return cached;
2839
2843
  }
2844
+ resolveSandboxConfigForCwd(cwd) {
2845
+ // Priority: config.sandbox → project settings.sandbox → global → per-run
2846
+ // default. Read UNMERGED per-scope (getForScope) so a project that wrote no
2847
+ // sandbox genuinely follows global, rather than inheriting global's mode and
2848
+ // looking like it set one. Fixes "项目级配了不生效" + the scope model.
2849
+ let projectSandbox;
2850
+ let globalSandbox;
2851
+ try {
2852
+ const sm = this.getSettingsManager();
2853
+ if (this.config.isSubAgent !== true) {
2854
+ projectSandbox = sm.getForScope("project", cwd).sandbox;
2855
+ }
2856
+ globalSandbox = sm.getForScope("user").sandbox;
2857
+ }
2858
+ catch {
2859
+ // settings unavailable → fall through to per-run default
2860
+ }
2861
+ return resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
2862
+ }
2840
2863
  /**
2841
2864
  * Build the shell env layered onto the Bash tool / background shells (see
2842
2865
  * mergeShellEnv). Three user-configured sources, merged lowest → highest:
@@ -2931,9 +2954,23 @@ export class Engine {
2931
2954
  return undefined;
2932
2955
  }
2933
2956
  }
2957
+ async resolveWorktreeSetupSandbox(cwd) {
2958
+ if (!cwd)
2959
+ return undefined;
2960
+ const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
2961
+ const sandboxBackend = this.runtime
2962
+ ? await this.runtime.resolveSandbox(sandboxConfig, cwd)
2963
+ : await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
2964
+ return sandboxBackend.name === "off"
2965
+ ? sandboxBackend
2966
+ : { ...sandboxBackend, network: sandboxConfig.network };
2967
+ }
2968
+ readWorktreeSetupShellEnv(cwd) {
2969
+ return this.readShellEnv(cwd);
2970
+ }
2934
2971
  buildToolContext() {
2935
2972
  const { disabledSkills, disabledPlugins } = this.readDisabledLists();
2936
- return {
2973
+ const ctx = {
2937
2974
  shellEnv: this.readShellEnv(this.config.cwd),
2938
2975
  cwd: this.config.cwd ?? process.cwd(),
2939
2976
  llmConfig: this.config.llm,
@@ -2961,6 +2998,10 @@ export class Engine {
2961
2998
  // opts out via config. Otherwise allowed.
2962
2999
  allowBackgroundShells: this.config.isSubAgent === true ? false : this.config.allowBackgroundShells !== false,
2963
3000
  };
3001
+ ctx.setCwd = (cwd) => {
3002
+ ctx.cwd = cwd;
3003
+ };
3004
+ return ctx;
2964
3005
  }
2965
3006
  /**
2966
3007
  * Read settings.disabledSkills + settings.disabledPlugins in a single
@@ -553,7 +553,7 @@ export class TurnLoop {
553
553
  // compaction decisions use hybrid (actual + delta) estimation rather than
554
554
  // pure heuristics. Without this the manager falls back to char/4 estimates.
555
555
  if (response.usage?.promptTokens !== undefined) {
556
- this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
556
+ this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
557
557
  }
558
558
  messages = this.markPendingImagesConsumed(messages);
559
559
  // Truncation that cut off a TOOL CALL: the model overflowed
@@ -2,6 +2,7 @@
2
2
  * Git worktree management — create/remove isolated worktrees for agents.
3
3
  */
4
4
  import type { SandboxBackend } from "../tool-system/sandbox/index.js";
5
+ import type { SessionWorkspace } from "../types.js";
5
6
  export interface WorktreeSession {
6
7
  originalCwd: string;
7
8
  worktreePath: string;
@@ -34,6 +35,12 @@ export declare function validateWorktreeSlug(slug: string): void;
34
35
  * Find the canonical git root (resolves worktree → main repo).
35
36
  */
36
37
  export declare function findGitRoot(cwd: string): string;
38
+ export declare function findMainWorktreeRoot(cwd: string): string;
39
+ export declare function findWorktreeForBranch(cwd: string, branch: string): string | undefined;
40
+ export declare function assertBranchNotCheckedOut(cwd: string, branch: string): void;
41
+ export declare function branchExists(cwd: string, branch: string): boolean;
42
+ export declare function isGitWorktreeRoot(cwd: string): boolean;
43
+ export declare function currentBranch(cwd: string): string | undefined;
37
44
  /**
38
45
  * Create an isolated git worktree for an agent session.
39
46
  */
@@ -70,15 +77,51 @@ export declare function runWorktreeSetup(worktreePath: string, script: string |
70
77
  timeoutMs?: number;
71
78
  signal?: AbortSignal;
72
79
  }): Promise<WorktreeSetupResult>;
80
+ export interface RemoveWorktreeResult {
81
+ /** True once `git worktree remove` completed and the directory is gone. */
82
+ dirRemoved: boolean;
83
+ /** The branch targeted for deletion when removeBranch=true. */
84
+ branch?: string;
85
+ /** True when removeBranch=true and branch deletion completed. */
86
+ branchDeleted?: boolean;
87
+ /** Non-empty when the worktree directory is gone but branch deletion failed. */
88
+ branchError?: string;
89
+ }
73
90
  /**
74
91
  * Remove a worktree and optionally its branch.
75
92
  */
76
- export declare function removeWorktree(worktreePath: string, removeBranch?: boolean): void;
77
- /**
78
- * List active worktrees.
79
- */
80
- export declare function listWorktrees(cwd: string): Array<{
93
+ export declare function removeWorktree(worktreePath: string, removeBranch?: boolean): RemoveWorktreeResult;
94
+ export declare function worktreeHasUncommittedChanges(worktreePath: string): boolean;
95
+ export declare function worktreeHasUncommittedOrAheadChanges(worktreePath: string, baseRef?: string): boolean;
96
+ export interface WorktreeDiffSummary {
97
+ /** Best-effort base used for the branch comparison, usually main/master. */
98
+ baseRef?: string;
99
+ /** Unique files changed either by commits ahead of base or by uncommitted changes. */
100
+ changedFiles: number;
101
+ /** Commits reachable from HEAD but not from baseRef. */
102
+ aheadCommits: number;
103
+ /** True when `git status --porcelain` reports local changes. */
104
+ hasUncommittedChanges: boolean;
105
+ }
106
+ export interface WorktreeWorkspaceOwner {
107
+ sessionId: string;
108
+ workspace?: SessionWorkspace;
109
+ }
110
+ export interface ListWorktreesOptions {
111
+ includeDiffSummary?: boolean;
112
+ currentSessionId?: string;
113
+ workspaceOwners?: WorktreeWorkspaceOwner[];
114
+ }
115
+ export interface WorktreeInfo {
81
116
  path: string;
82
117
  branch: string;
83
118
  head: string;
84
- }>;
119
+ isMain?: boolean;
120
+ diff?: WorktreeDiffSummary;
121
+ occupiedBySessionIds?: string[];
122
+ occupiedByOtherSession?: boolean;
123
+ }
124
+ /**
125
+ * List active worktrees.
126
+ */
127
+ export declare function listWorktrees(cwd: string, opts?: ListWorktreesOptions): WorktreeInfo[];