@cjhyy/code-shell-capability-coding 0.9.3 → 0.9.4
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/agent-adapter.d.ts +4 -0
- package/dist/cc-orchestrator/agent-adapter.js +3 -0
- package/dist/cc-orchestrator/external-agent-driver.js +1 -0
- package/dist/external-runtimes/codex/event-translator.d.ts +7 -0
- package/dist/external-runtimes/codex/event-translator.js +58 -11
- package/dist/external-runtimes/turn-input.d.ts +2 -0
- package/dist/index.capability.js +8 -2
- package/dist/tools/drive-agent.d.ts +3 -0
- package/dist/tools/drive-agent.js +73 -4
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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). */
|
package/dist/index.capability.js
CHANGED
|
@@ -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: [
|
|
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: [
|
|
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
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
"version": "0.9.4",
|
|
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.
|
|
42
|
+
"@cjhyy/code-shell-core": "0.9.4"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=20.10"
|