@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.15

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 (81) hide show
  1. package/dist/capability-control/service.d.ts +2 -0
  2. package/dist/capability-control/service.js +4 -2
  3. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  4. package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
  5. package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
  6. package/dist/cc-orchestrator/session-history.js +2 -2
  7. package/dist/cli/agent-server-stdio.js +7 -2
  8. package/dist/context/manager.d.ts +11 -2
  9. package/dist/context/manager.js +64 -3
  10. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  11. package/dist/credentials/inject-credential-tool.js +29 -10
  12. package/dist/credentials/use-credential-tool.d.ts +1 -0
  13. package/dist/credentials/use-credential-tool.js +3 -0
  14. package/dist/engine/engine.d.ts +9 -0
  15. package/dist/engine/engine.js +96 -17
  16. package/dist/engine/turn-loop.d.ts +3 -1
  17. package/dist/engine/turn-loop.js +3 -1
  18. package/dist/engine/types.d.ts +8 -0
  19. package/dist/git/worktree/crud.d.ts +69 -0
  20. package/dist/git/worktree/crud.js +206 -0
  21. package/dist/git/worktree/diff.d.ts +14 -0
  22. package/dist/git/worktree/diff.js +82 -0
  23. package/dist/git/worktree/git-exec.d.ts +7 -0
  24. package/dist/git/worktree/git-exec.js +51 -0
  25. package/dist/git/worktree/index.d.ts +5 -0
  26. package/dist/git/worktree/index.js +5 -0
  27. package/dist/git/worktree/query.d.ts +42 -0
  28. package/dist/git/worktree/query.js +121 -0
  29. package/dist/git/worktree/slug.d.ts +11 -0
  30. package/dist/git/worktree/slug.js +58 -0
  31. package/dist/git/worktree.d.ts +1 -127
  32. package/dist/git/worktree.js +5 -464
  33. package/dist/index.d.ts +28 -27
  34. package/dist/index.js +24 -24
  35. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  36. package/dist/plugins/installer/checkUpdate.js +4 -2
  37. package/dist/plugins/installer/install.js +2 -0
  38. package/dist/plugins/installer/parseSource.d.ts +4 -1
  39. package/dist/plugins/installer/parseSource.js +28 -10
  40. package/dist/plugins/installer/update.d.ts +4 -1
  41. package/dist/plugins/installer/update.js +5 -3
  42. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  43. package/dist/plugins/parseMarketplaceInput.js +4 -3
  44. package/dist/preset/index.d.ts +1 -0
  45. package/dist/preset/index.js +6 -0
  46. package/dist/prompt/sections/base.md +1 -1
  47. package/dist/protocol/chat-session-manager.js +7 -0
  48. package/dist/protocol/server.d.ts +10 -0
  49. package/dist/protocol/server.js +88 -4
  50. package/dist/protocol/types.d.ts +6 -0
  51. package/dist/protocol/types.js +2 -0
  52. package/dist/runtime/background-shell.js +2 -3
  53. package/dist/services/auto-dream.d.ts +15 -4
  54. package/dist/services/auto-dream.js +20 -20
  55. package/dist/services/dream-consolidation.d.ts +2 -3
  56. package/dist/services/dream-consolidation.js +65 -15
  57. package/dist/services/extract-memories.d.ts +14 -5
  58. package/dist/services/extract-memories.js +20 -3
  59. package/dist/services/global-dream-promotion.d.ts +23 -0
  60. package/dist/services/global-dream-promotion.js +112 -0
  61. package/dist/services/memory-orchestrator.js +347 -34
  62. package/dist/session/memory.d.ts +56 -17
  63. package/dist/session/memory.js +337 -78
  64. package/dist/session/session-manager.d.ts +1 -1
  65. package/dist/session/session-manager.js +6 -6
  66. package/dist/settings/manager.js +43 -12
  67. package/dist/settings/schema.d.ts +21 -0
  68. package/dist/settings/schema.js +12 -0
  69. package/dist/tool-system/builtin/index.js +18 -8
  70. package/dist/tool-system/builtin/memory.js +40 -8
  71. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  72. package/dist/tool-system/builtin/worktree.js +59 -11
  73. package/dist/tool-system/context.d.ts +11 -1
  74. package/dist/tool-system/mcp-manager.d.ts +8 -5
  75. package/dist/tool-system/mcp-manager.js +85 -55
  76. package/dist/tool-system/path-policy.d.ts +2 -0
  77. package/dist/tool-system/path-policy.js +28 -12
  78. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  79. package/dist/tool-system/workspace-bridge.js +1 -0
  80. package/dist/types.d.ts +14 -0
  81. package/package.json +1 -1
@@ -9,9 +9,10 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
9
9
  import { logger } from "../logging/logger.js";
10
10
  import { CredentialStore } from "../credentials/index.js";
11
11
  import { ENV_ALLOWLIST } from "../runtime/spawn-common.js";
12
- import { writeFile, mkdir } from "node:fs/promises";
12
+ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  import { diagnoseMcpStdioMissingCommand, previewPath } from "./mcp-stdio-diagnostics.js";
15
+ import { codeShellHome } from "../session/session-manager.js";
15
16
  /**
16
17
  * Read a required secret from `process.env` by NAME (Codex-style env-secret
17
18
  * handling — the value is never persisted in MCP config). A referenced env var
@@ -118,7 +119,29 @@ export function inferTransportType(config) {
118
119
  * spill itself is bounded by the byte budget below; this cap is just
119
120
  * to keep ls(~/.code-shell/mcp_images) tractable.
120
121
  */
122
+ const MAX_MCP_IMAGE_SPILLS_PER_TOOL = 50;
121
123
  const MAX_MCP_IMAGE_BYTES = 8 * 1024 * 1024;
124
+ function spillTimestamp(filename, prefix) {
125
+ const suffix = filename.slice(prefix.length);
126
+ const dot = suffix.lastIndexOf(".");
127
+ const raw = dot === -1 ? suffix : suffix.slice(0, dot);
128
+ const timestamp = Number(raw);
129
+ return Number.isFinite(timestamp) ? timestamp : 0;
130
+ }
131
+ async function pruneMcpImageSpills(baseDir, prefix) {
132
+ const entries = await readdir(baseDir, { withFileTypes: true });
133
+ const spills = entries
134
+ .filter((entry) => entry.isFile() && entry.name.startsWith(prefix))
135
+ .map((entry) => ({
136
+ name: entry.name,
137
+ timestamp: spillTimestamp(entry.name, prefix),
138
+ }))
139
+ .sort((a, b) => a.timestamp - b.timestamp || a.name.localeCompare(b.name));
140
+ const excess = spills.length - MAX_MCP_IMAGE_SPILLS_PER_TOOL;
141
+ if (excess <= 0)
142
+ return;
143
+ await Promise.all(spills.slice(0, excess).map((entry) => unlink(join(baseDir, entry.name)).catch(() => { })));
144
+ }
122
145
  /**
123
146
  * Persist an MCP-returned image to disk and return the textual
124
147
  * reference the LLM should see. Images larger than
@@ -130,9 +153,7 @@ const MAX_MCP_IMAGE_BYTES = 8 * 1024 * 1024;
130
153
  * [mcp-image] server=playwright tool=screenshot saved=/abs/path.png (123 KB)
131
154
  */
132
155
  export async function spillMcpImage(serverName, toolName, base64, mimeType, opts) {
133
- const home = process.env.CODE_SHELL_HOME ?? process.env.HOME ?? "";
134
- const baseDir = opts?.baseDir ??
135
- (home ? join(home, ".code-shell", "mcp_images") : join("/tmp", "code-shell-mcp-images"));
156
+ const baseDir = opts?.baseDir ?? join(codeShellHome(), "mcp_images");
136
157
  const now = opts?.now ?? Date.now;
137
158
  const decodedBytes = Math.floor((base64.length * 3) / 4);
138
159
  if (decodedBytes > MAX_MCP_IMAGE_BYTES) {
@@ -147,11 +168,13 @@ export async function spillMcpImage(serverName, toolName, base64, mimeType, opts
147
168
  : "png";
148
169
  const safeServer = serverName.replace(/[^\w.-]+/g, "_");
149
170
  const safeTool = toolName.replace(/[^\w.-]+/g, "_");
150
- const filename = `${safeServer}-${safeTool}-${now()}.${ext}`;
171
+ const prefix = `${safeServer}-${safeTool}-`;
172
+ const filename = `${prefix}${now()}.${ext}`;
151
173
  const filePath = join(baseDir, filename);
152
174
  try {
153
175
  await mkdir(baseDir, { recursive: true });
154
176
  await writeFile(filePath, Buffer.from(base64, "base64"));
177
+ await pruneMcpImageSpills(baseDir, prefix);
155
178
  }
156
179
  catch (err) {
157
180
  logger.warn("mcp.image_spill_failed", {
@@ -448,59 +471,65 @@ export class MCPManager {
448
471
  * Discover tools from an MCP server and register them.
449
472
  */
450
473
  async discoverTools(serverName, client) {
451
- const result = await client.listTools();
452
- for (const tool of result.tools) {
453
- const registered = buildRegisteredTool(serverName, tool);
454
- // Register with an executor that calls the MCP server
455
- this.toolRegistry.registerTool(registered, async (args) => {
456
- const callResult = await client.callTool({
457
- name: tool.name,
458
- arguments: stripInternalToolArgs(args),
459
- });
460
- // Extract text + image content from the result. Image blobs
461
- // are spilled to ~/.code-shell/mcp_images/ so they don't bloat
462
- // the LLM message tree same pattern as the GenerateImage
463
- // tool and the model sees a textual reference it can Read
464
- // on a later turn if it needs the pixels. This sidesteps the
465
- // "MCP returned a 5MB screenshot token budget exploded"
466
- // failure mode that bit Codex (issue #11845); we never put
467
- // raw base64 image data in the result text. See
468
- // TODO-week.md #9c + docs/research-cc-vs-codex-image-handling.md §B.
469
- const parts = [];
470
- if (Array.isArray(callResult.content)) {
471
- for (const item of callResult.content) {
472
- if (typeof item === "string") {
473
- parts.push(item);
474
- continue;
475
- }
476
- if (typeof item !== "object" || item === null)
477
- continue;
478
- if ("text" in item) {
479
- parts.push(String(item.text));
480
- continue;
481
- }
482
- if (item.type === "image") {
483
- const block = item;
484
- if (typeof block.data === "string" && block.data.length > 0) {
485
- const note = await spillMcpImage(serverName, tool.name, block.data, block.mimeType ?? "image/png");
486
- parts.push(note);
474
+ try {
475
+ const result = await client.listTools();
476
+ for (const tool of result.tools) {
477
+ const registered = buildRegisteredTool(serverName, tool);
478
+ // Register with an executor that calls the MCP server
479
+ this.toolRegistry.registerTool(registered, async (args, ctx) => {
480
+ const callResult = await client.callTool({
481
+ name: tool.name,
482
+ arguments: stripInternalToolArgs(args),
483
+ }, undefined, ctx?.signal ? { signal: ctx.signal } : undefined);
484
+ // Extract text + image content from the result. Image blobs
485
+ // are spilled to ~/.code-shell/mcp_images/ so they don't bloat
486
+ // the LLM message tree same pattern as the GenerateImage
487
+ // tool and the model sees a textual reference it can Read
488
+ // on a later turn if it needs the pixels. This sidesteps the
489
+ // "MCP returned a 5MB screenshot token budget exploded"
490
+ // failure mode that bit Codex (issue #11845); we never put
491
+ // raw base64 image data in the result text. See
492
+ // TODO-week.md #9c + docs/research-cc-vs-codex-image-handling.md §B.
493
+ const parts = [];
494
+ if (Array.isArray(callResult.content)) {
495
+ for (const item of callResult.content) {
496
+ if (typeof item === "string") {
497
+ parts.push(item);
498
+ continue;
499
+ }
500
+ if (typeof item !== "object" || item === null)
501
+ continue;
502
+ if ("text" in item) {
503
+ parts.push(String(item.text));
504
+ continue;
505
+ }
506
+ if (item.type === "image") {
507
+ const block = item;
508
+ if (typeof block.data === "string" && block.data.length > 0) {
509
+ const note = await spillMcpImage(serverName, tool.name, block.data, block.mimeType ?? "image/png");
510
+ parts.push(note);
511
+ }
487
512
  }
488
513
  }
489
514
  }
490
- }
491
- const body = parts.join("\n") || "(no output)";
492
- // Trust boundary: MCP output is external content. Wrap it so the
493
- // model sees an explicit reminder that the body comes from an
494
- // untrusted server and any instructions inside are content, not
495
- // commands. The marker is intentionally short so it doesn't bloat
496
- // every tool result, but distinct enough that prompt-injected
497
- // strings can't fake their way out.
498
- return wrapMcpOutput(serverName, tool.name, body);
499
- });
500
- const set = this.registeredToolsByServer.get(serverName) ?? new Set();
501
- set.add(registered.name);
502
- this.registeredToolsByServer.set(serverName, set);
503
- logger.info("mcp.tool_registered", { server: serverName, tool: registered.name });
515
+ const body = parts.join("\n") || "(no output)";
516
+ // Trust boundary: MCP output is external content. Wrap it so the
517
+ // model sees an explicit reminder that the body comes from an
518
+ // untrusted server and any instructions inside are content, not
519
+ // commands. The marker is intentionally short so it doesn't bloat
520
+ // every tool result, but distinct enough that prompt-injected
521
+ // strings can't fake their way out.
522
+ return wrapMcpOutput(serverName, tool.name, body);
523
+ });
524
+ const set = this.registeredToolsByServer.get(serverName) ?? new Set();
525
+ set.add(registered.name);
526
+ this.registeredToolsByServer.set(serverName, set);
527
+ logger.info("mcp.tool_registered", { server: serverName, tool: registered.name });
528
+ }
529
+ }
530
+ catch (err) {
531
+ await this.disconnect(serverName);
532
+ throw err;
504
533
  }
505
534
  }
506
535
  /**
@@ -583,6 +612,7 @@ export class MCPManager {
583
612
  uri: r.uri,
584
613
  name: r.name ?? r.uri,
585
614
  description: r.description,
615
+ serverName: name,
586
616
  });
587
617
  }
588
618
  }
@@ -52,6 +52,8 @@ export interface ClassifyOptions {
52
52
  export type PathApprovalScope = "once" | "session" | "project";
53
53
  /** Test seam: clear the in-memory session grants. */
54
54
  export declare function _resetSessionPathGrants(): void;
55
+ export declare function openSessionPathApprovals(sessionId: string): void;
56
+ export declare function clearSessionPathApprovals(sessionId: string): void;
55
57
  /**
56
58
  * Classify a file path against the workspace + sensitive-path policy.
57
59
  *
@@ -105,6 +105,10 @@ function expandTilde(p) {
105
105
  }
106
106
  /** sessionId → set of approved directory grants (prefix + operation). */
107
107
  const sessionPathGrants = new Map();
108
+ /** Session ids explicitly closed by ChatSessionManager; late approvals must not recreate grants. */
109
+ const closedPathApprovalSessions = new Set();
110
+ /** Per-session prompt chains for enforcePathPolicyWithApproval (see comment there). */
111
+ const askChains = new Map();
108
112
  /** True if a grant for `grantOp` authorizes an operation of `wantOp`. */
109
113
  function grantCoversOp(grantOp, wantOp) {
110
114
  // write ⊇ {read, write}; read ⊇ {read}.
@@ -198,6 +202,8 @@ function storedOp(g) {
198
202
  function recordPathApproval(scope, resolved, operation, cwd, sessionId) {
199
203
  if (scope === "once")
200
204
  return;
205
+ if (sessionId && closedPathApprovalSessions.has(sessionId))
206
+ return;
201
207
  const prefix = dirPrefix(dirname(resolved));
202
208
  if (scope === "session") {
203
209
  if (!sessionId)
@@ -271,6 +277,16 @@ function recordPathApproval(scope, resolved, operation, cwd, sessionId) {
271
277
  /** Test seam: clear the in-memory session grants. */
272
278
  export function _resetSessionPathGrants() {
273
279
  sessionPathGrants.clear();
280
+ closedPathApprovalSessions.clear();
281
+ askChains.clear();
282
+ }
283
+ export function openSessionPathApprovals(sessionId) {
284
+ closedPathApprovalSessions.delete(sessionId);
285
+ }
286
+ export function clearSessionPathApprovals(sessionId) {
287
+ sessionPathGrants.delete(sessionId);
288
+ closedPathApprovalSessions.add(sessionId);
289
+ askChains.delete(sessionId);
274
290
  }
275
291
  /**
276
292
  * Best-effort resolution. realpath fails when the path doesn't exist yet —
@@ -461,8 +477,8 @@ export function enforcePathPolicy(filePath, operation, workspaceRoot) {
461
477
  }
462
478
  // ask — MVP refuses with explanatory message until askUser plumbing
463
479
  // lands. The conservative bias matches the plan's leaning answer for Q1.
464
- return `Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
465
- `Set CODESHELL_PATH_POLICY=off to disable enforcement during a rollback.`;
480
+ return (`Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
481
+ `Set CODESHELL_PATH_POLICY=off to disable enforcement during a rollback.`);
466
482
  }
467
483
  export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
468
484
  if (ctx?.cwd === undefined)
@@ -481,8 +497,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
481
497
  return `Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}`;
482
498
  }
483
499
  if (operation === "write" && ctx.planMode) {
484
- return `Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}. ` +
485
- `Plan mode does not allow file writes.`;
500
+ return (`Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}. ` +
501
+ `Plan mode does not allow file writes.`);
486
502
  }
487
503
  // Already approved this directory for this OPERATION (this session or
488
504
  // persisted for the project)? Proceed without re-prompting — this is the fix
@@ -491,8 +507,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
491
507
  if (isPathPreApproved(c.resolvedPath, operation, ctx.cwd, ctx.sessionId))
492
508
  return null;
493
509
  if (!ctx.askUser) {
494
- return `Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
495
- `No interactive approval UI is available in this run.`;
510
+ return (`Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
511
+ `No interactive approval UI is available in this run.`);
496
512
  }
497
513
  // Serialize concurrent asks (per session) and RE-CHECK grants when our turn
498
514
  // comes. Parallel tools hitting the same not-yet-approved directory all pass
@@ -502,7 +518,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
502
518
  const chainKey = ctx.sessionId ?? "__global__";
503
519
  const prevTurn = askChains.get(chainKey) ?? Promise.resolve();
504
520
  let release;
505
- askChains.set(chainKey, new Promise((r) => (release = r)));
521
+ const currentTurn = new Promise((r) => (release = r));
522
+ askChains.set(chainKey, currentTurn);
506
523
  try {
507
524
  await prevTurn;
508
525
  if (isPathPreApproved(c.resolvedPath, operation, ctx.cwd, ctx.sessionId))
@@ -511,6 +528,9 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
511
528
  }
512
529
  finally {
513
530
  release();
531
+ if (askChains.get(chainKey) === currentTurn) {
532
+ askChains.delete(chainKey);
533
+ }
514
534
  }
515
535
  }
516
536
  /** The actual interactive ask — split out so the serialized section reads flat.
@@ -521,9 +541,7 @@ async function promptForPathApproval(c, operation, ctx) {
521
541
  // "工作区外路径" header was misleading for sensitive-path asks.
522
542
  const isSensitive = c.reason.startsWith("sensitive");
523
543
  const what = operation === "read" ? "读取" : "写入";
524
- const title = isSensitive
525
- ? `工具想${what}敏感文件`
526
- : `工具想${what}工作区外路径`;
544
+ const title = isSensitive ? `工具想${what}敏感文件` : `工具想${what}工作区外路径`;
527
545
  const header = isSensitive ? "敏感文件权限" : "路径权限";
528
546
  // Scope options carry remembered grants for the directory of this path, so
529
547
  // the same folder isn't re-prompted. Labels are matched by exact string
@@ -566,8 +584,6 @@ async function promptForPathApproval(c, operation, ctx) {
566
584
  }
567
585
  return `Error: path approval denied by user — ${c.reason}. Path: ${c.resolvedPath}`;
568
586
  }
569
- /** Per-session prompt chains for enforcePathPolicyWithApproval (see comment there). */
570
- const askChains = new Map();
571
587
  /**
572
588
  * Internal: reset the "disabled warning" latch. Tests flip the env var
573
589
  * between cases and need each one to be able to re-trigger the warning.
@@ -0,0 +1,11 @@
1
+ import type { SessionWorkspace } from "../types.js";
2
+ /**
3
+ * WorkspaceBridge — host-backed session workspace switching.
4
+ *
5
+ * Desktop implements this through Electron main so model-initiated switches use
6
+ * the same service path as the UI workspace switcher. Undefined in headless or
7
+ * non-desktop contexts.
8
+ */
9
+ export interface WorkspaceBridge {
10
+ switch(target: string): Promise<SessionWorkspace>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -149,6 +149,14 @@ export interface SessionWorkspace {
149
149
  createdBy: "codeshell";
150
150
  };
151
151
  }
152
+ export interface ContextUsageAnchor {
153
+ promptTokens: number;
154
+ messageCount: number;
155
+ estimateAtAnchor?: number;
156
+ recordedAt: number;
157
+ provider?: string;
158
+ model?: string;
159
+ }
152
160
  export interface SessionState {
153
161
  sessionId: string;
154
162
  cwd: string;
@@ -162,6 +170,12 @@ export interface SessionState {
162
170
  * (for example model switches), so it must not drive whole-session metrics.
163
171
  */
164
172
  tokenUsage: TokenUsage;
173
+ /**
174
+ * Last provider-reported prompt-token count for the current context window.
175
+ * Separate from cumulative tokenUsage; used only to seed context-size estimates
176
+ * before the next provider response.
177
+ */
178
+ contextUsageAnchor?: ContextUsageAnchor;
165
179
  /**
166
180
  * Monotonic prompt-cache counters for the whole session. These only increase
167
181
  * from session start and are separate from the resettable tokenUsage window.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.6.0-rc.14",
3
+ "version": "0.6.0-rc.15",
4
4
  "description": "Core engine for code-shell — agent orchestration, tool execution, hooks, protocol. UI-agnostic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",