@cjhyy/code-shell-capability-coding 0.9.3 → 0.9.5

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.
@@ -8,6 +8,10 @@ export interface BuildArgsOpts {
8
8
  permissionMode: PermissionMode;
9
9
  cwd: string;
10
10
  imagePaths?: string[];
11
+ /** Explicit extra directories the external agent may inspect. Claude Code
12
+ * maps these to --add-dir; callers keep them read-only by pairing them with
13
+ * permissionMode:"default". */
14
+ additionalReadDirs?: string[];
11
15
  codexImageInputSupported?: boolean;
12
16
  }
13
17
  export interface ParsedResult {
@@ -18,6 +18,9 @@ export const claudeAdapter = {
18
18
  args.push("--model", opts.model);
19
19
  if (opts.resumeSessionId)
20
20
  args.push("--resume", opts.resumeSessionId);
21
+ if (opts.additionalReadDirs?.length) {
22
+ args.push("--add-dir", ...opts.additionalReadDirs);
23
+ }
21
24
  // Hard-disallow Workflow: driving CC unattended (esp. bypassPermissions),
22
25
  // CC's Workflow tool fans out a FLEET of agents — the real token-burn culprit
23
26
  // (user: "自动了 2 个 workflow token 就烧没了"). A single Task (one sub-agent)
@@ -216,6 +216,7 @@ export function runAgentOnce(adapter, opts, signal) {
216
216
  permissionMode: opts.permissionMode ?? "default",
217
217
  cwd: opts.cwd,
218
218
  imagePaths: opts.imagePaths,
219
+ additionalReadDirs: opts.additionalReadDirs,
219
220
  codexImageInputSupported,
220
221
  });
221
222
  throwIfAborted(signal);
@@ -39,6 +39,13 @@ export declare class CodexEventTranslator {
39
39
  private activeTurnId;
40
40
  /** Turns that reached a terminal state. Late events for these are dropped. */
41
41
  private readonly finishedTurns;
42
+ /**
43
+ * Serialized arguments each tool item was OPENED with, keyed by item id.
44
+ * Codex allocates an item before its arguments are known (a webSearch opens
45
+ * with `query: ""`), so on completion we compare and emit the settled values
46
+ * when they differ — otherwise the transcript keeps the empty snapshot.
47
+ */
48
+ private readonly openedToolArgs;
42
49
  constructor(options: CodexEventTranslatorOptions);
43
50
  /**
44
51
  * Translate one notification. Returns zero or more events — zero is a normal,
@@ -22,6 +22,32 @@ function asRecord(value) {
22
22
  function str(value) {
23
23
  return typeof value === "string" && value ? value : undefined;
24
24
  }
25
+ /**
26
+ * A thread item's fields minus its identity, i.e. the tool's arguments. Output
27
+ * fields are excluded so a completed item's arguments can be compared against
28
+ * the ones seen when it opened.
29
+ */
30
+ const ITEM_OUTPUT_KEYS = new Set([
31
+ "id",
32
+ "type",
33
+ "aggregatedOutput",
34
+ "output",
35
+ "text",
36
+ "result",
37
+ "changes",
38
+ "error",
39
+ "status",
40
+ "startedAtMs",
41
+ "completedAtMs",
42
+ ]);
43
+ function toolArgsOf(item) {
44
+ const args = {};
45
+ for (const [key, value] of Object.entries(item)) {
46
+ if (!ITEM_OUTPUT_KEYS.has(key))
47
+ args[key] = value;
48
+ }
49
+ return args;
50
+ }
25
51
  /**
26
52
  * Map a Codex turn status onto a CodeShell `TerminalReason`.
27
53
  *
@@ -46,6 +72,13 @@ export class CodexEventTranslator {
46
72
  activeTurnId;
47
73
  /** Turns that reached a terminal state. Late events for these are dropped. */
48
74
  finishedTurns = new Set();
75
+ /**
76
+ * Serialized arguments each tool item was OPENED with, keyed by item id.
77
+ * Codex allocates an item before its arguments are known (a webSearch opens
78
+ * with `query: ""`), so on completion we compare and emit the settled values
79
+ * when they differ — otherwise the transcript keeps the empty snapshot.
80
+ */
81
+ openedToolArgs = new Map();
49
82
  constructor(options) {
50
83
  this.threadId = options.threadId;
51
84
  this.codeshellServer = options.codeshellServerName ?? CODESHELL_MCP_SERVER;
@@ -112,6 +145,9 @@ export class CodexEventTranslator {
112
145
  if (oldest !== undefined)
113
146
  this.finishedTurns.delete(oldest);
114
147
  }
148
+ // A tool item opened but never completed (interrupt, crash) would otherwise
149
+ // keep its entry forever on this long-lived translator.
150
+ this.openedToolArgs.clear();
115
151
  }
116
152
  onTurnStarted(params) {
117
153
  const turn = asRecord(params.turn);
@@ -241,7 +277,8 @@ export class CodexEventTranslator {
241
277
  return [];
242
278
  if (this.isCodeshellHostTool(item))
243
279
  return [];
244
- const { id: _id, type: _type, ...args } = item;
280
+ const args = toolArgsOf(item);
281
+ this.openedToolArgs.set(id, JSON.stringify(args));
245
282
  return [{ type: "tool_use_start", toolCall: { id, toolName: type, args } }];
246
283
  }
247
284
  onItemCompleted(params) {
@@ -270,16 +307,26 @@ export class CodexEventTranslator {
270
307
  }
271
308
  const error = asRecord(item.error);
272
309
  const errorMessage = str(error?.message) ?? str(item.error);
273
- return [
274
- {
275
- type: "tool_result",
276
- result: {
277
- id,
278
- toolName: type,
279
- ...(output !== undefined ? { result: output } : {}),
280
- ...(errorMessage ? { error: errorMessage, isError: true } : {}),
281
- },
310
+ const events = [];
311
+ // Correct the arguments first when they only materialized now, so the
312
+ // recorded call shows what actually ran before its result.
313
+ const openedArgs = this.openedToolArgs.get(id);
314
+ this.openedToolArgs.delete(id);
315
+ if (openedArgs !== undefined) {
316
+ const settledArgs = toolArgsOf(item);
317
+ if (Object.keys(settledArgs).length > 0 && JSON.stringify(settledArgs) !== openedArgs) {
318
+ events.push({ type: "tool_use_args_delta", toolCallId: id, args: settledArgs });
319
+ }
320
+ }
321
+ events.push({
322
+ type: "tool_result",
323
+ result: {
324
+ id,
325
+ toolName: type,
326
+ ...(output !== undefined ? { result: output } : {}),
327
+ ...(errorMessage ? { error: errorMessage, isError: true } : {}),
282
328
  },
283
- ];
329
+ });
330
+ return events;
284
331
  }
285
332
  }
@@ -7,6 +7,8 @@ export interface ExternalRuntimeAttachment {
7
7
  }
8
8
  export interface ExternalRuntimeTurnInput {
9
9
  text: string;
10
+ /** Optional shorter text persisted/rendered for this turn; `text` still reaches the runtime. */
11
+ displayText?: string;
10
12
  clientMessageId?: string;
11
13
  attachments?: readonly ExternalRuntimeAttachment[];
12
14
  /** Host-injected continuation (for example, a background-job completion). */
@@ -26,7 +26,10 @@ export const CODING_TOOLS = [
26
26
  isReadOnly: false,
27
27
  isConcurrencySafe: false,
28
28
  timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
29
- pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
29
+ pathPolicy: [
30
+ { kind: "arg", arg: "attachmentPaths", operation: "read" },
31
+ { kind: "arg", arg: "additionalReadDirs", operation: "read" },
32
+ ],
30
33
  }, driveAgentTool, { presetTags: ["general", "terminal-coding"], availability: unavailableInQuickChat }),
31
34
  defineTool({
32
35
  ...driveAgentJobsToolDef,
@@ -42,7 +45,10 @@ export const CODING_TOOLS = [
42
45
  isReadOnly: false,
43
46
  isConcurrencySafe: false,
44
47
  timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
45
- pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
48
+ pathPolicy: [
49
+ { kind: "arg", arg: "attachmentPaths", operation: "read" },
50
+ { kind: "arg", arg: "additionalReadDirs", operation: "read" },
51
+ ],
46
52
  }, driveClaudeCodeTool, { presetTags: ["general", "terminal-coding"], availability: unavailableInQuickChat }),
47
53
  defineTool({
48
54
  ...checkQuotaToolDef,
@@ -18,6 +18,7 @@ type Runner = (opts: {
18
18
  permissionMode?: PermMode;
19
19
  signal?: AbortSignal;
20
20
  imagePaths?: string[];
21
+ additionalReadDirs?: string[];
21
22
  onSessionId?: (sessionId: string) => void;
22
23
  }) => Promise<AgentRunResult>;
23
24
  type SessionStore = {
@@ -48,6 +49,8 @@ type LegacyRunner = (opts: {
48
49
  permissionMode?: PermMode;
49
50
  signal?: AbortSignal;
50
51
  onSessionId?: (sessionId: string) => void;
52
+ additionalReadDirs?: string[];
53
+ imagePaths?: string[];
51
54
  }) => Promise<AgentRunResult>;
52
55
  export declare function makeDriveClaudeCodeTool(runner?: LegacyRunner, options?: DriveAgentToolOptions): (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
53
56
  export declare const driveClaudeCodeTool: (args: Record<string, unknown>, ctx?: ToolContext) => Promise<string>;
@@ -28,6 +28,8 @@ export const driveAgentToolDef = {
28
28
  "If the external agent is explicitly expected to write a workspace other than its launch cwd, " +
29
29
  "set effectiveWorkspaceCwd so cross-session conflict checks use that declared workspace; omit " +
30
30
  "it when the run writes in cwd. " +
31
+ "For read-only diagnosis that needs logs or transcripts outside cwd, explicitly pass their " +
32
+ "directories in additionalReadDirs together with permissionMode:'default'. " +
31
33
  "For a quick task where you want the answer inline, pass background:false. " +
32
34
  "It has NO time concept of its own: for 'in N minutes' / 'every N' / looping, use CronCreate " +
33
35
  "instead (never sleep). A scheduled CronCreate job runs one codeshell turn whose prompt can " +
@@ -94,6 +96,11 @@ export const driveAgentToolDef = {
94
96
  items: { type: "string" },
95
97
  description: "Optional local file paths to hand to the driven agent. Paths must resolve inside cwd. Images are also passed to Codex with -i when the installed Codex CLI supports it; otherwise all paths are listed in the prompt.",
96
98
  },
99
+ additionalReadDirs: {
100
+ type: "array",
101
+ items: { type: "string" },
102
+ description: "Optional existing directories outside cwd that a read-only diagnostic run may inspect (for example, a transcript directory). Requires permissionMode:'default'; writable modes are rejected so this cannot silently widen their write scope. Claude Code receives these via --add-dir; all CLIs also receive the canonical paths in the prompt.",
103
+ },
97
104
  permissionMode: {
98
105
  type: "string",
99
106
  enum: ["default", "acceptEdits", "bypassPermissions"],
@@ -124,6 +131,7 @@ const defaultRunner = (opts) => {
124
131
  cwd: opts.cwd,
125
132
  permissionMode: opts.permissionMode ?? "default",
126
133
  imagePaths: opts.imagePaths,
134
+ additionalReadDirs: opts.additionalReadDirs,
127
135
  onSessionId: opts.onSessionId,
128
136
  }, opts.signal);
129
137
  };
@@ -421,6 +429,51 @@ function resolveAttachmentPaths(raw, cwd) {
421
429
  }
422
430
  return { paths };
423
431
  }
432
+ function isPathInside(root, candidate) {
433
+ const rel = relative(root, candidate);
434
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`));
435
+ }
436
+ function resolveAdditionalReadDirs(raw, cwd, permissionMode) {
437
+ if (raw === undefined)
438
+ return { directories: [] };
439
+ if (!Array.isArray(raw)) {
440
+ return { directories: [], error: "additionalReadDirs must be an array of strings" };
441
+ }
442
+ // Resolution and containment share one base — the caller's workspace, same
443
+ // as resolveAttachmentPaths. A worktree-isolated run rebinds its execution
444
+ // cwd, but a directory inside the caller's own workspace is still an
445
+ // authorized read, not a widening.
446
+ const containmentRoot = realpathSync(cwd);
447
+ const seen = new Set();
448
+ const directories = [];
449
+ for (const item of raw) {
450
+ if (typeof item !== "string" || !item.trim()) {
451
+ return {
452
+ directories: [],
453
+ error: "additionalReadDirs must contain only non-empty strings",
454
+ };
455
+ }
456
+ const candidate = isAbsolute(item) ? item : resolve(cwd, item);
457
+ if (!existsSync(candidate)) {
458
+ return { directories: [], error: `additional read directory not found: ${item}` };
459
+ }
460
+ const real = realpathSync(candidate);
461
+ if (!statSync(real).isDirectory()) {
462
+ return { directories: [], error: `additional read path is not a directory: ${item}` };
463
+ }
464
+ if (!isPathInside(containmentRoot, real) && permissionMode !== "default") {
465
+ return {
466
+ directories: [],
467
+ error: "additionalReadDirs outside cwd require permissionMode:'default'; refusing to widen a writable DriveAgent run",
468
+ };
469
+ }
470
+ if (!seen.has(real)) {
471
+ seen.add(real);
472
+ directories.push(real);
473
+ }
474
+ }
475
+ return { directories };
476
+ }
424
477
  const DRIVE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
425
478
  function appendAttachmentPrompt(prompt, paths) {
426
479
  if (paths.length === 0)
@@ -432,6 +485,13 @@ function appendAttachmentPrompt(prompt, paths) {
432
485
  }
433
486
  return `${prompt}\n${lines.join("\n")}`;
434
487
  }
488
+ function appendAdditionalReadDirsPrompt(prompt, directories) {
489
+ if (directories.length === 0)
490
+ return prompt;
491
+ return `${prompt}\n\nAdditional read-only directories explicitly authorized for this diagnostic run:\n${directories
492
+ .map((path) => `- ${path}`)
493
+ .join("\n")}`;
494
+ }
435
495
  async function recordSuccessfulSession(store, cli, cwd, result, metadata, includeErroredSession = false) {
436
496
  if ((!includeErroredSession && result.isError) || !result.sessionId)
437
497
  return;
@@ -893,7 +953,13 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
893
953
  return `Error: ${resolvedAttachmentPaths.error}`;
894
954
  }
895
955
  const attachmentPaths = resolvedAttachmentPaths.paths;
896
- const promptWithAttachments = appendAttachmentPrompt(prompt, attachmentPaths);
956
+ const resolvedAdditionalReadDirs = resolveAdditionalReadDirs(args.additionalReadDirs, attachmentSourceCwd, permissionMode);
957
+ if (resolvedAdditionalReadDirs.error) {
958
+ safeFinalizeDriveWorktree(managedWorktree);
959
+ return `Error: ${resolvedAdditionalReadDirs.error}`;
960
+ }
961
+ const additionalReadDirs = resolvedAdditionalReadDirs.directories;
962
+ const promptWithAttachments = appendAdditionalReadDirsPrompt(appendAttachmentPrompt(prompt, attachmentPaths), additionalReadDirs);
897
963
  const imagePaths = cli === "codex"
898
964
  ? attachmentPaths.filter((path) => DRIVE_IMAGE_EXTS.has(extname(path).toLowerCase()))
899
965
  : [];
@@ -907,6 +973,7 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
907
973
  cwd,
908
974
  permissionMode,
909
975
  imagePaths,
976
+ additionalReadDirs,
910
977
  };
911
978
  const foregroundHandoffMs = externalRuntime
912
979
  ? Number.POSITIVE_INFINITY
@@ -1350,6 +1417,7 @@ export const driveClaudeCodeToolDef = {
1350
1417
  effectiveWorkspaceCwd: driveAgentToolDef.inputSchema.properties
1351
1418
  .effectiveWorkspaceCwd,
1352
1419
  attachmentPaths: driveAgentToolDef.inputSchema.properties.attachmentPaths,
1420
+ additionalReadDirs: driveAgentToolDef.inputSchema.properties.additionalReadDirs,
1353
1421
  permissionMode: driveAgentToolDef.inputSchema.properties.permissionMode,
1354
1422
  background: driveAgentToolDef.inputSchema.properties.background,
1355
1423
  },
@@ -1357,9 +1425,10 @@ export const driveClaudeCodeToolDef = {
1357
1425
  },
1358
1426
  };
1359
1427
  export function makeDriveClaudeCodeTool(runner, options) {
1360
- const generic = runner
1361
- ? ({ prompt, resumeSessionId, model, cwd, permissionMode, signal, onSessionId }) => runner({ prompt, resumeSessionId, model, cwd, permissionMode, signal, onSessionId })
1362
- : undefined;
1428
+ // Forward the full option set (minus `cli`, which is pinned to claude) so
1429
+ // validated inputs like additionalReadDirs actually reach the runner instead
1430
+ // of being silently dropped while the prompt claims they were authorized.
1431
+ const generic = runner ? ({ cli: _cli, ...opts }) => runner(opts) : undefined;
1363
1432
  return makeDriveAgentTool(generic ?? defaultRunner, "claude", options);
1364
1433
  }
1365
1434
  export const driveClaudeCodeTool = makeDriveClaudeCodeTool();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-capability-coding",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Coding capability pack for the generic code-shell agent core.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
40
40
  },
41
41
  "dependencies": {
42
- "@cjhyy/code-shell-core": "0.9.3"
42
+ "@cjhyy/code-shell-core": "0.9.5"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=20.10"