@cjhyy/code-shell-capability-coding 0.9.44 → 0.9.47
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/capability-runtime.js +4 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
- package/dist/cc-orchestrator/codex-session-history.js +11 -4
- package/dist/cc-orchestrator/external-agent-changes.js +7 -65
- package/dist/cc-orchestrator/external-agent-driver.js +61 -3
- package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
- package/dist/cc-orchestrator/session-discovery.js +63 -6
- package/dist/cc-orchestrator/session-history.js +5 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
- package/dist/external-runtimes/claude-code/event-translator.js +58 -9
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
- package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
- package/dist/external-runtimes/claude-code/permissions.js +120 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +43 -4
- package/dist/external-runtimes/claude-code/runtime.js +155 -39
- package/dist/external-runtimes/claude-code/text-inference.d.ts +22 -0
- package/dist/external-runtimes/claude-code/text-inference.js +240 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +15 -0
- package/dist/external-runtimes/codex/app-server-client.js +69 -6
- package/dist/external-runtimes/codex/event-translator.js +16 -2
- package/dist/external-runtimes/codex/runtime.d.ts +14 -0
- package/dist/external-runtimes/codex/runtime.js +152 -16
- package/dist/external-runtimes/index.d.ts +4 -0
- package/dist/external-runtimes/index.js +2 -0
- package/dist/external-runtimes/session-factory.d.ts +16 -0
- package/dist/external-runtimes/session-factory.js +43 -1
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
- package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
- package/dist/git/parse-log.d.ts +6 -2
- package/dist/git/parse-log.js +7 -3
- package/dist/git/utils.d.ts +4 -0
- package/dist/git/utils.js +29 -10
- package/dist/git/worktree/crud.d.ts +2 -0
- package/dist/git/worktree/crud.js +29 -1
- package/dist/git/worktree/diff.js +57 -29
- package/dist/index.capability.js +14 -1
- package/dist/index.git.d.ts +1 -0
- package/dist/index.git.js +1 -0
- package/dist/lsp/client.d.ts +3 -0
- package/dist/lsp/client.js +96 -25
- package/dist/lsp/manager.d.ts +2 -0
- package/dist/lsp/manager.js +56 -15
- package/dist/prompt/coding.md +1 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +27 -7
- package/dist/quota/types.d.ts +5 -2
- package/dist/review/review-prompt.d.ts +2 -0
- package/dist/review/review-prompt.js +70 -29
- package/dist/review/review-target.d.ts +29 -0
- package/dist/review/review-target.js +353 -0
- package/dist/tools/apply-patch/applier.js +64 -39
- package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
- package/dist/tools/apply-patch/backup-targets.js +24 -0
- package/dist/tools/apply-patch/parser.js +4 -2
- package/dist/tools/check-quota.js +3 -1
- package/dist/tools/lsp.js +36 -5
- package/package.json +2 -2
|
@@ -56,7 +56,10 @@ export const gitDynamicContextProvider = ({ cwd, preset }) => {
|
|
|
56
56
|
const log = getGitLog(cwd, 5);
|
|
57
57
|
const parts = [`gitStatus: Current branch: ${branch}`];
|
|
58
58
|
if (status.length > 0) {
|
|
59
|
-
|
|
59
|
+
const lines = status.map((entry) => entry.origPath
|
|
60
|
+
? `${entry.status} ${entry.origPath} -> ${entry.path}`
|
|
61
|
+
: `${entry.status} ${entry.path}`);
|
|
62
|
+
parts.push(`Status:\n${lines.join("\n")}`);
|
|
60
63
|
}
|
|
61
64
|
if (log.length > 0) {
|
|
62
65
|
parts.push(`Recent commits:\n${log.map((entry) => `${entry.hash} ${entry.message}`).join("\n")}`);
|
|
@@ -35,5 +35,7 @@ export declare function parseCodexRecentHistory(raw: string, limit: number): {
|
|
|
35
35
|
* authoritative rendered stream; parallel `event_msg.agent_message` records
|
|
36
36
|
* are deliberately ignored to avoid duplicate assistant bubbles. */
|
|
37
37
|
export declare function parseCodexTranscriptLine(line: string): SessionTailEvent[];
|
|
38
|
-
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`.
|
|
38
|
+
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`.
|
|
39
|
+
* A resumed thread can own several rollouts; pick the newest by mtime, the
|
|
40
|
+
* same file the room list dedupes to, so list title and history agree. */
|
|
39
41
|
export declare function findCodexRolloutFile(codexHome: string, cwd: string, threadId: string): string | undefined;
|
|
@@ -182,23 +182,30 @@ function argsOf(payload) {
|
|
|
182
182
|
return { input: payload.input };
|
|
183
183
|
return undefined;
|
|
184
184
|
}
|
|
185
|
-
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`.
|
|
185
|
+
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`.
|
|
186
|
+
* A resumed thread can own several rollouts; pick the newest by mtime, the
|
|
187
|
+
* same file the room list dedupes to, so list title and history agree. */
|
|
186
188
|
export function findCodexRolloutFile(codexHome, cwd, threadId) {
|
|
187
189
|
const root = join(codexHome, "sessions");
|
|
188
190
|
if (!existsSync(root))
|
|
189
191
|
return undefined;
|
|
192
|
+
let best;
|
|
190
193
|
for (const file of walkRollouts(root)) {
|
|
191
194
|
let meta;
|
|
195
|
+
let mtimeMs;
|
|
192
196
|
try {
|
|
193
197
|
meta = readSessionMeta(file);
|
|
198
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
194
199
|
}
|
|
195
200
|
catch {
|
|
196
201
|
continue;
|
|
197
202
|
}
|
|
198
|
-
if (meta
|
|
199
|
-
|
|
203
|
+
if (!meta || meta.id !== threadId || meta.cwd !== cwd)
|
|
204
|
+
continue;
|
|
205
|
+
if (!best || mtimeMs > best.mtimeMs)
|
|
206
|
+
best = { file, mtimeMs };
|
|
200
207
|
}
|
|
201
|
-
return
|
|
208
|
+
return best?.file;
|
|
202
209
|
}
|
|
203
210
|
/** Recursively yield every `rollout-*.jsonl` file under `root`. */
|
|
204
211
|
function* walkRollouts(root) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync, existsSync
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { encodeCwd } from "./session-discovery.js";
|
|
5
|
+
import { findCodexRolloutFile } from "./codex-session-history.js";
|
|
5
6
|
import { logger } from "@cjhyy/code-shell-core/extension";
|
|
6
7
|
/**
|
|
7
8
|
* #6 — attribute file changes made by a background external agent (DriveAgent
|
|
@@ -107,9 +108,10 @@ function filesFromCodexArgs(name, raw) {
|
|
|
107
108
|
return [];
|
|
108
109
|
if (name === "apply_patch" || name === "ApplyPatch") {
|
|
109
110
|
// apply_patch payload wraps a patch; headers look like `*** Update File: <p>`
|
|
110
|
-
// or `*** Add File: <p
|
|
111
|
+
// or `*** Add File: <p>`, and a rename follows Update with `*** Move to: <p>`.
|
|
112
|
+
// Match those directly off the raw text so a rename keeps both paths.
|
|
111
113
|
const out = [];
|
|
112
|
-
const re = /\*\*\*\s+(?:Update|Add|Delete) File:\s+(.+)/g;
|
|
114
|
+
const re = /\*\*\*\s+(?:(?:Update|Add|Delete) File|Move to):\s+(.+)/g;
|
|
113
115
|
let m;
|
|
114
116
|
while ((m = re.exec(raw)) !== null) {
|
|
115
117
|
const f = m[1]?.trim();
|
|
@@ -129,7 +131,8 @@ function filesFromCodexArgs(name, raw) {
|
|
|
129
131
|
}
|
|
130
132
|
/** Locate a codex rollout by threadId+cwd under `<codexHome>/sessions` and extract changes. */
|
|
131
133
|
export function readCodexChangedFiles(cwd, threadId, codexHome = join(homedir(), ".codex")) {
|
|
132
|
-
|
|
134
|
+
// Same locator as history/tails: a resumed thread uses its newest rollout.
|
|
135
|
+
const file = findCodexRolloutFile(codexHome, cwd, threadId);
|
|
133
136
|
if (!file)
|
|
134
137
|
return [];
|
|
135
138
|
let text;
|
|
@@ -168,64 +171,3 @@ export function readExternalChangedFiles(cli, cwd, sessionId) {
|
|
|
168
171
|
return [];
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
|
-
// ── codex rollout location (mirrors codex-session-history.ts) ──────────────
|
|
172
|
-
function findRolloutFile(codexHome, cwd, threadId) {
|
|
173
|
-
const root = join(codexHome, "sessions");
|
|
174
|
-
if (!existsSync(root))
|
|
175
|
-
return undefined;
|
|
176
|
-
for (const file of walkRollouts(root)) {
|
|
177
|
-
let meta;
|
|
178
|
-
try {
|
|
179
|
-
meta = readSessionMeta(file);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (meta && meta.id === threadId && meta.cwd === cwd)
|
|
185
|
-
return file;
|
|
186
|
-
}
|
|
187
|
-
return undefined;
|
|
188
|
-
}
|
|
189
|
-
function* walkRollouts(root) {
|
|
190
|
-
let entries;
|
|
191
|
-
try {
|
|
192
|
-
entries = readdirSync(root);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
for (const name of entries) {
|
|
198
|
-
const full = join(root, name);
|
|
199
|
-
let st;
|
|
200
|
-
try {
|
|
201
|
-
st = statSync(full);
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (st.isDirectory())
|
|
207
|
-
yield* walkRollouts(full);
|
|
208
|
-
else if (name.startsWith("rollout-") && name.endsWith(".jsonl"))
|
|
209
|
-
yield full;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function readSessionMeta(file, maxBytes = 1 << 16) {
|
|
213
|
-
const fd = openSync(file, "r");
|
|
214
|
-
let text;
|
|
215
|
-
try {
|
|
216
|
-
const buf = Buffer.alloc(maxBytes);
|
|
217
|
-
const n = readSync(fd, buf, 0, maxBytes, 0);
|
|
218
|
-
text = buf.toString("utf-8", 0, n);
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
closeSync(fd);
|
|
222
|
-
}
|
|
223
|
-
const nl = text.indexOf("\n");
|
|
224
|
-
const first = (nl === -1 ? text : text.slice(0, nl)).trim();
|
|
225
|
-
if (!first)
|
|
226
|
-
return undefined;
|
|
227
|
-
const d = JSON.parse(first);
|
|
228
|
-
if (d.type !== "session_meta" || !d.payload)
|
|
229
|
-
return undefined;
|
|
230
|
-
return { id: d.payload.id, cwd: d.payload.cwd };
|
|
231
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { pathWithCommonBins } from "./cc-capability.js";
|
|
4
|
-
import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
|
|
4
|
+
import { killProcessGroup, recordExternalModelCall } from "@cjhyy/code-shell-core/extension";
|
|
5
5
|
/** Pure: reduce collected output lines + exit code to a result. Unit-testable. */
|
|
6
6
|
export function runWithLines(adapter, lines, exitCode) {
|
|
7
7
|
const parsed = adapter.parseResult(lines);
|
|
@@ -199,6 +199,7 @@ export function detectCodexImageInput(command, cwd, signal) {
|
|
|
199
199
|
* No time concept — a single turn. Honors AbortSignal (kills the child). */
|
|
200
200
|
export function runAgentOnce(adapter, opts, signal) {
|
|
201
201
|
return new Promise((resolve, reject) => {
|
|
202
|
+
let finishTrace = () => { };
|
|
202
203
|
void (async () => {
|
|
203
204
|
throwIfAborted(signal);
|
|
204
205
|
const codexImageInputSupported = adapter.kind === "codex" && (opts.imagePaths?.length ?? 0) > 0
|
|
@@ -234,6 +235,53 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
234
235
|
detached: false,
|
|
235
236
|
stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
236
237
|
});
|
|
238
|
+
const common = {
|
|
239
|
+
provider: adapter.kind,
|
|
240
|
+
model: opts.model ?? "CLI default",
|
|
241
|
+
source: `drive-agent-${adapter.kind}-cli`,
|
|
242
|
+
purpose: "external_delegation",
|
|
243
|
+
startedAt: Date.now(),
|
|
244
|
+
};
|
|
245
|
+
const initialTrace = recordExternalModelCall({
|
|
246
|
+
...common,
|
|
247
|
+
status: "running",
|
|
248
|
+
input: {
|
|
249
|
+
messages: [{ role: "user", content: opts.prompt }],
|
|
250
|
+
parameters: {
|
|
251
|
+
cliArguments: args,
|
|
252
|
+
promptViaStdin: viaStdin,
|
|
253
|
+
launchCwd: opts.cwd,
|
|
254
|
+
resumeExternalSessionId: opts.resumeSessionId,
|
|
255
|
+
imagePaths: opts.imagePaths,
|
|
256
|
+
additionalReadDirs: opts.additionalReadDirs,
|
|
257
|
+
limitation: "CLI entry input and visible final result only; private model requests, retries, and provider usage are opaque.",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
let traceFinished = false;
|
|
262
|
+
finishTrace = (result, error) => {
|
|
263
|
+
if (!initialTrace || traceFinished)
|
|
264
|
+
return;
|
|
265
|
+
traceFinished = true;
|
|
266
|
+
const status = signal?.aborted
|
|
267
|
+
? "cancelled"
|
|
268
|
+
: error || result?.isError || result?.exitCode !== 0
|
|
269
|
+
? "failed"
|
|
270
|
+
: "completed";
|
|
271
|
+
const captured = initialTrace.inputState !== "not-recorded";
|
|
272
|
+
recordExternalModelCall({
|
|
273
|
+
...common,
|
|
274
|
+
id: initialTrace.id,
|
|
275
|
+
status,
|
|
276
|
+
endedAt: Date.now(),
|
|
277
|
+
output: captured && result ? { text: result.finalText, stopReason: status } : undefined,
|
|
278
|
+
error: error
|
|
279
|
+
? captured
|
|
280
|
+
? String(error instanceof Error ? error.message : error)
|
|
281
|
+
: "External CLI request failed"
|
|
282
|
+
: undefined,
|
|
283
|
+
});
|
|
284
|
+
};
|
|
237
285
|
let reportedSessionId = "";
|
|
238
286
|
const reportSessionId = (candidate) => {
|
|
239
287
|
if (reportedSessionId || typeof candidate !== "string" || !candidate.trim())
|
|
@@ -287,6 +335,7 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
287
335
|
return;
|
|
288
336
|
settled = true;
|
|
289
337
|
cleanup();
|
|
338
|
+
finishTrace(undefined, err);
|
|
290
339
|
// A missing binary is the most common failure (user hasn't installed the
|
|
291
340
|
// CLI, or GUI-launched Electron's PATH misses it). Turn the cryptic
|
|
292
341
|
// "spawn codex ENOENT" into something actionable that names the command.
|
|
@@ -301,12 +350,21 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
301
350
|
return;
|
|
302
351
|
settled = true;
|
|
303
352
|
cleanup();
|
|
304
|
-
void (termination ?? Promise.resolve())
|
|
353
|
+
void (termination ?? Promise.resolve())
|
|
354
|
+
.then(() => {
|
|
305
355
|
const result = runWithLines(adapter, lines, code);
|
|
356
|
+
finishTrace(result);
|
|
306
357
|
reportSessionId(result.sessionId);
|
|
307
358
|
resolve(result);
|
|
359
|
+
})
|
|
360
|
+
.catch((error) => {
|
|
361
|
+
finishTrace(undefined, error);
|
|
362
|
+
reject(error);
|
|
308
363
|
});
|
|
309
364
|
});
|
|
310
|
-
})().catch(
|
|
365
|
+
})().catch((error) => {
|
|
366
|
+
finishTrace(undefined, error);
|
|
367
|
+
reject(error);
|
|
368
|
+
});
|
|
311
369
|
});
|
|
312
370
|
}
|
|
@@ -94,6 +94,8 @@ export interface ClaudeSessionDiscoveryDiagnostics {
|
|
|
94
94
|
* the larger bounded content read used to find the card title.
|
|
95
95
|
*/
|
|
96
96
|
export declare function discoverRecentClaudeSessions(opts?: DiscoverOptions, claudeHome?: string, scope?: ExternalSessionDiscoveryScope, diagnostics?: ClaudeSessionDiscoveryDiagnostics): RecentExternalSession[];
|
|
97
|
-
/** Count of
|
|
98
|
-
* whether a bounded `discoverSessions` left older sessions
|
|
97
|
+
/** Count of session files for `cwd`, cheaply (stat + small identity probe).
|
|
98
|
+
* Lets the UI know whether a bounded `discoverSessions` left older sessions
|
|
99
|
+
* unshown. An identity the probe cannot read is counted, so this may only
|
|
100
|
+
* overestimate the authoritative discovered length. */
|
|
99
101
|
export declare function countSessions(cwd: string, claudeHome?: string): number;
|
|
@@ -75,7 +75,7 @@ function firstUserMessage(lines) {
|
|
|
75
75
|
catch {
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
78
|
-
if (d
|
|
78
|
+
if (d?.type !== "user")
|
|
79
79
|
continue;
|
|
80
80
|
const c = d.message?.content;
|
|
81
81
|
const text = typeof c === "string"
|
|
@@ -92,6 +92,39 @@ function firstUserMessage(lines) {
|
|
|
92
92
|
}
|
|
93
93
|
return "";
|
|
94
94
|
}
|
|
95
|
+
/** First top-level `cwd` recorded in a transcript: the launch cwd Claude used
|
|
96
|
+
* to name the project dir (later records may follow in-session `cd`s). */
|
|
97
|
+
function firstRecordedCwd(lines) {
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (!line.trim())
|
|
100
|
+
continue;
|
|
101
|
+
let d;
|
|
102
|
+
try {
|
|
103
|
+
d = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!d || typeof d !== "object")
|
|
109
|
+
continue;
|
|
110
|
+
const recorded = d.cwd;
|
|
111
|
+
if (typeof recorded === "string")
|
|
112
|
+
return recorded;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True when a transcript's recorded cwd is a different path that merely shares
|
|
118
|
+
* `cwd`'s lossy project-dir encoding (`/foo-bar` vs `/foo/bar`). A missing
|
|
119
|
+
* cwd cannot disprove ownership, so legacy transcripts stay in their bucket.
|
|
120
|
+
*/
|
|
121
|
+
function isCollidingClaudeCwd(recordedCwd, cwd) {
|
|
122
|
+
if (recordedCwd === undefined)
|
|
123
|
+
return false;
|
|
124
|
+
const recorded = resolve(recordedCwd);
|
|
125
|
+
const expected = resolve(cwd);
|
|
126
|
+
return recorded !== expected && encodeCwd(recorded) === encodeCwd(expected);
|
|
127
|
+
}
|
|
95
128
|
function countUserMessages(lines) {
|
|
96
129
|
let n = 0;
|
|
97
130
|
for (const line of lines) {
|
|
@@ -149,9 +182,17 @@ export function discoverSessions(cwd, claudeHome = join(homedir(), ".claude"), o
|
|
|
149
182
|
const dir = join(claudeProjectsDir(claudeHome), encodeCwd(cwd));
|
|
150
183
|
if (!existsSync(dir))
|
|
151
184
|
return [];
|
|
152
|
-
|
|
185
|
+
// Window first, then apply the limit while skipping collided identities so a
|
|
186
|
+
// foreign session in the shared bucket never displaces one of this cwd's.
|
|
187
|
+
const windowed = selectRecentStats(listSessionStats(dir), {
|
|
188
|
+
sinceMs: opts.sinceMs,
|
|
189
|
+
now: opts.now,
|
|
190
|
+
});
|
|
191
|
+
const limit = opts.limit && opts.limit > 0 ? opts.limit : Infinity;
|
|
153
192
|
const out = [];
|
|
154
|
-
for (const s of
|
|
193
|
+
for (const s of windowed) {
|
|
194
|
+
if (out.length >= limit)
|
|
195
|
+
break;
|
|
155
196
|
let lines;
|
|
156
197
|
try {
|
|
157
198
|
lines = readFileSync(s.file, "utf-8").split("\n");
|
|
@@ -159,6 +200,8 @@ export function discoverSessions(cwd, claudeHome = join(homedir(), ".claude"), o
|
|
|
159
200
|
catch {
|
|
160
201
|
continue;
|
|
161
202
|
}
|
|
203
|
+
if (isCollidingClaudeCwd(firstRecordedCwd(lines), cwd))
|
|
204
|
+
continue;
|
|
162
205
|
out.push({
|
|
163
206
|
sessionId: s.sessionId,
|
|
164
207
|
cwd,
|
|
@@ -285,11 +328,25 @@ export function discoverRecentClaudeSessions(opts = {}, claudeHome = join(homedi
|
|
|
285
328
|
}
|
|
286
329
|
return out;
|
|
287
330
|
}
|
|
288
|
-
/** Count of
|
|
289
|
-
* whether a bounded `discoverSessions` left older sessions
|
|
331
|
+
/** Count of session files for `cwd`, cheaply (stat + small identity probe).
|
|
332
|
+
* Lets the UI know whether a bounded `discoverSessions` left older sessions
|
|
333
|
+
* unshown. An identity the probe cannot read is counted, so this may only
|
|
334
|
+
* overestimate the authoritative discovered length. */
|
|
290
335
|
export function countSessions(cwd, claudeHome = join(homedir(), ".claude")) {
|
|
291
336
|
const dir = join(claudeProjectsDir(claudeHome), encodeCwd(cwd));
|
|
292
337
|
if (!existsSync(dir))
|
|
293
338
|
return 0;
|
|
294
|
-
|
|
339
|
+
let count = 0;
|
|
340
|
+
for (const s of listSessionStats(dir)) {
|
|
341
|
+
let recorded;
|
|
342
|
+
try {
|
|
343
|
+
recorded = readSessionIdentityCwd(s.file);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!isCollidingClaudeCwd(recorded, cwd))
|
|
349
|
+
count++;
|
|
350
|
+
}
|
|
351
|
+
return count;
|
|
295
352
|
}
|
|
@@ -52,6 +52,9 @@ export function parseRecentHistory(raw, limit) {
|
|
|
52
52
|
catch {
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
|
+
// A valid JSON scalar (e.g. `null`) is not a record; skip it like noise.
|
|
56
|
+
if (!d || typeof d !== "object")
|
|
57
|
+
continue;
|
|
55
58
|
if (d.type === "user") {
|
|
56
59
|
const t = textOf(d.message?.content).trim();
|
|
57
60
|
if (!t || NOISE.some((n) => t.startsWith(n)))
|
|
@@ -80,6 +83,8 @@ export function parseClaudeTranscriptLine(line) {
|
|
|
80
83
|
catch {
|
|
81
84
|
return [];
|
|
82
85
|
}
|
|
86
|
+
if (!d || typeof d !== "object")
|
|
87
|
+
return [];
|
|
83
88
|
const out = [];
|
|
84
89
|
if (d.type === "user") {
|
|
85
90
|
const content = d.message?.content;
|
|
@@ -33,6 +33,13 @@ export declare class ClaudeEventTranslator {
|
|
|
33
33
|
private terminal;
|
|
34
34
|
/** Accumulates `input_json_delta` per tool block so args land as one object. */
|
|
35
35
|
private readonly toolInput;
|
|
36
|
+
/**
|
|
37
|
+
* Prompt usage of the latest top-level model request in this turn. The
|
|
38
|
+
* terminal `result.usage` sums every request of the process, which is right
|
|
39
|
+
* for accounting but overstates the context N-fold (each request re-reads the
|
|
40
|
+
* cache), so the context anchor comes from here.
|
|
41
|
+
*/
|
|
42
|
+
private lastRequestUsage?;
|
|
36
43
|
constructor(options: ClaudeEventTranslatorOptions);
|
|
37
44
|
/** Reset turn-scoped state while preserving the durable Claude session id. */
|
|
38
45
|
beginTurn(): void;
|
|
@@ -6,6 +6,21 @@ function asRecord(value) {
|
|
|
6
6
|
function str(value) {
|
|
7
7
|
return typeof value === "string" && value ? value : undefined;
|
|
8
8
|
}
|
|
9
|
+
function count(value) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
function normaliseUsage(usage) {
|
|
13
|
+
const input = count(usage?.input_tokens);
|
|
14
|
+
if (input === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const cacheReadTokens = count(usage?.cache_read_input_tokens);
|
|
17
|
+
const cacheCreationTokens = count(usage?.cache_creation_input_tokens);
|
|
18
|
+
return {
|
|
19
|
+
promptTokens: input + (cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0),
|
|
20
|
+
...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
|
|
21
|
+
...(cacheCreationTokens !== undefined ? { cacheCreationTokens } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
9
24
|
/**
|
|
10
25
|
* Map Claude's terminal reason onto CodeShell's.
|
|
11
26
|
*
|
|
@@ -27,6 +42,13 @@ export class ClaudeEventTranslator {
|
|
|
27
42
|
terminal = false;
|
|
28
43
|
/** Accumulates `input_json_delta` per tool block so args land as one object. */
|
|
29
44
|
toolInput = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Prompt usage of the latest top-level model request in this turn. The
|
|
47
|
+
* terminal `result.usage` sums every request of the process, which is right
|
|
48
|
+
* for accounting but overstates the context N-fold (each request re-reads the
|
|
49
|
+
* cache), so the context anchor comes from here.
|
|
50
|
+
*/
|
|
51
|
+
lastRequestUsage;
|
|
30
52
|
constructor(options) {
|
|
31
53
|
this.options = options;
|
|
32
54
|
this.codeshellServer = options.codeshellServerName ?? "codeshell_tools";
|
|
@@ -35,6 +57,7 @@ export class ClaudeEventTranslator {
|
|
|
35
57
|
beginTurn() {
|
|
36
58
|
this.terminal = false;
|
|
37
59
|
this.toolInput.clear();
|
|
60
|
+
this.lastRequestUsage = undefined;
|
|
38
61
|
}
|
|
39
62
|
/** Claude session id, learned from the `system/init` line. */
|
|
40
63
|
runtimeSessionId;
|
|
@@ -86,6 +109,13 @@ export class ClaudeEventTranslator {
|
|
|
86
109
|
if (!event || !type)
|
|
87
110
|
return [];
|
|
88
111
|
if (type === "message_start") {
|
|
112
|
+
// A sub-agent's request (Task tool) carries its parent tool id; its prompt
|
|
113
|
+
// is not this conversation's context.
|
|
114
|
+
if (message.parent_tool_use_id == null) {
|
|
115
|
+
const usage = normaliseUsage(asRecord(asRecord(event.message)?.usage));
|
|
116
|
+
if (usage)
|
|
117
|
+
this.lastRequestUsage = usage;
|
|
118
|
+
}
|
|
89
119
|
this.turnNumber += 1;
|
|
90
120
|
return [{ type: "stream_request_start", turnNumber: this.turnNumber }];
|
|
91
121
|
}
|
|
@@ -111,6 +141,12 @@ export class ClaudeEventTranslator {
|
|
|
111
141
|
const text = str(delta?.text);
|
|
112
142
|
return text ? [{ type: "text_delta", text }] : [];
|
|
113
143
|
}
|
|
144
|
+
if (deltaType === "thinking_delta") {
|
|
145
|
+
// Same event the native engine emits for extended thinking. The
|
|
146
|
+
// accompanying `signature_delta` is an opaque integrity token, not text.
|
|
147
|
+
const text = str(delta?.thinking);
|
|
148
|
+
return text ? [{ type: "thinking_delta", text }] : [];
|
|
149
|
+
}
|
|
114
150
|
if (deltaType === "input_json_delta") {
|
|
115
151
|
// Arguments stream as JSON fragments; buffer and emit once complete.
|
|
116
152
|
const partial = typeof delta?.partial_json === "string" ? delta.partial_json : "";
|
|
@@ -199,20 +235,33 @@ export class ClaudeEventTranslator {
|
|
|
199
235
|
this.terminal = true;
|
|
200
236
|
const events = [];
|
|
201
237
|
const usage = asRecord(message.usage);
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
const total = normaliseUsage(usage);
|
|
239
|
+
if (total) {
|
|
240
|
+
// `promptTokens`/`cache*` describe the latest request (the context, as in
|
|
241
|
+
// the native stream); `singleTurn*` carry this submission's aggregate, which
|
|
242
|
+
// is what accounting adds up. Without a per-request reading the aggregate is
|
|
243
|
+
// the best available context estimate.
|
|
244
|
+
const latest = this.lastRequestUsage ?? total;
|
|
245
|
+
const completionTokens = count(usage?.output_tokens);
|
|
208
246
|
events.push({
|
|
209
247
|
type: "usage_update",
|
|
210
|
-
promptTokens,
|
|
248
|
+
promptTokens: latest.promptTokens,
|
|
249
|
+
singleTurnPromptTokens: total.promptTokens,
|
|
211
250
|
promptTokensSource: "provider_usage",
|
|
212
251
|
promptTokensConfidence: "high",
|
|
213
252
|
...(completionTokens !== undefined ? { completionTokens } : {}),
|
|
214
|
-
...(cacheReadTokens !== undefined
|
|
215
|
-
|
|
253
|
+
...(latest.cacheReadTokens !== undefined
|
|
254
|
+
? { cacheReadTokens: latest.cacheReadTokens }
|
|
255
|
+
: {}),
|
|
256
|
+
...(total.cacheReadTokens !== undefined
|
|
257
|
+
? { singleTurnCacheReadTokens: total.cacheReadTokens }
|
|
258
|
+
: {}),
|
|
259
|
+
...(latest.cacheCreationTokens !== undefined
|
|
260
|
+
? { cacheCreationTokens: latest.cacheCreationTokens }
|
|
261
|
+
: {}),
|
|
262
|
+
...(total.cacheCreationTokens !== undefined
|
|
263
|
+
? { singleTurnCacheCreationTokens: total.cacheCreationTokens }
|
|
264
|
+
: {}),
|
|
216
265
|
});
|
|
217
266
|
}
|
|
218
267
|
const reason = terminalReasonFor(str(message.subtype), message.is_error === true);
|
|
@@ -34,6 +34,14 @@ export interface ClaudeMcpConfigFile {
|
|
|
34
34
|
* files", it was "avoid mutating persistent user config".
|
|
35
35
|
*/
|
|
36
36
|
export declare function writeClaudeMcpConfigFile(options: ClaudeMcpConfigOptions): ClaudeMcpConfigFile;
|
|
37
|
+
/**
|
|
38
|
+
* Write per-invocation host guidance for `--append-system-prompt-file`.
|
|
39
|
+
*
|
|
40
|
+
* A file rather than `--append-system-prompt <text>`: the guidance includes the
|
|
41
|
+
* renderer's session brief and related-session list, and argv is readable by
|
|
42
|
+
* every local user (see {@link buildClaudeMcpConfig}).
|
|
43
|
+
*/
|
|
44
|
+
export declare function writeClaudeSystemPromptFile(text: string): ClaudeMcpConfigFile;
|
|
37
45
|
/**
|
|
38
46
|
* Fully-qualified tool names for `--allowed-tools`.
|
|
39
47
|
*
|
|
@@ -58,9 +58,22 @@ export function buildClaudeMcpConfig(options) {
|
|
|
58
58
|
* files", it was "avoid mutating persistent user config".
|
|
59
59
|
*/
|
|
60
60
|
export function writeClaudeMcpConfigFile(options) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
return writePrivateTempFile("codeshell-mcp-", "mcp-config.json", buildClaudeMcpConfig(options));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write per-invocation host guidance for `--append-system-prompt-file`.
|
|
65
|
+
*
|
|
66
|
+
* A file rather than `--append-system-prompt <text>`: the guidance includes the
|
|
67
|
+
* renderer's session brief and related-session list, and argv is readable by
|
|
68
|
+
* every local user (see {@link buildClaudeMcpConfig}).
|
|
69
|
+
*/
|
|
70
|
+
export function writeClaudeSystemPromptFile(text) {
|
|
71
|
+
return writePrivateTempFile("codeshell-claude-prompt-", "append-system-prompt.md", text);
|
|
72
|
+
}
|
|
73
|
+
function writePrivateTempFile(prefix, name, content) {
|
|
74
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
75
|
+
const path = join(dir, name);
|
|
76
|
+
writeFileSync(path, content, { mode: 0o600 });
|
|
64
77
|
// Set explicitly as well: writeFileSync's mode is subject to umask.
|
|
65
78
|
chmodSync(path, 0o600);
|
|
66
79
|
return {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's NATIVE tool permissions under a CodeShell host.
|
|
3
|
+
*
|
|
4
|
+
* CodeShell Host Tools (`mcp__codeshell_tools__*`) are authorized by
|
|
5
|
+
* `ToolExecutor` whatever Claude decides. Claude's own tools (Bash, Edit, Write,
|
|
6
|
+
* WebFetch, …) are not: they obey only the CLI's permission mode and whoever
|
|
7
|
+
* answers its permission prompts. Two things therefore have to be explicit:
|
|
8
|
+
*
|
|
9
|
+
* 1. `--permission-mode`, derived from the host's selection. Leaving it out is
|
|
10
|
+
* not neutral: the CLI then falls back to the user's own
|
|
11
|
+
* `~/.claude/settings.json` `defaultMode`, which may be far wider than what
|
|
12
|
+
* the user picked in CodeShell.
|
|
13
|
+
* 2. `--permission-prompt-tool`, so an "ask" reaches the host's approval UI. In
|
|
14
|
+
* `-p` mode without one, every ask is denied outright.
|
|
15
|
+
*
|
|
16
|
+
* The prompt tool is an MCP tool on the session's own bridge. Claude calls it
|
|
17
|
+
* with `{ tool_name, input, tool_use_id }` and expects a JSON text result,
|
|
18
|
+
* `{ behavior: "allow", updatedInput }` or `{ behavior: "deny", message }`
|
|
19
|
+
* (shape read from the CLI's own result schema; not run against a real account).
|
|
20
|
+
*/
|
|
21
|
+
import type { PermissionMode } from "@cjhyy/code-shell-core/extension";
|
|
22
|
+
import type { BridgeToolHost } from "../shared/mcp-bridge.js";
|
|
23
|
+
/** Values passed to `claude --permission-mode`. */
|
|
24
|
+
export declare const CLAUDE_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "dontAsk", "bypassPermissions"];
|
|
25
|
+
export type ClaudePermissionMode = (typeof CLAUDE_PERMISSION_MODES)[number];
|
|
26
|
+
/**
|
|
27
|
+
* Map a CodeShell permission selection onto Claude's.
|
|
28
|
+
*
|
|
29
|
+
* Never escalates: Plan wins over everything; full access maps to
|
|
30
|
+
* `bypassPermissions` only when CodeShell itself is in `bypassPermissions`; and
|
|
31
|
+
* anything without an exact Claude equivalent — notably CodeShell's legacy
|
|
32
|
+
* `auto`, which in Claude would mean a model-classifier auto-approval — becomes
|
|
33
|
+
* `default`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function claudePermissionModeFor(selection: {
|
|
36
|
+
permissionMode: PermissionMode | undefined;
|
|
37
|
+
planMode: boolean;
|
|
38
|
+
}): ClaudePermissionMode;
|
|
39
|
+
/**
|
|
40
|
+
* Whether native asks should be routed to the host in this mode. Plan stays
|
|
41
|
+
* read-only (an approved ExitPlanMode would otherwise leave it); `dontAsk` means
|
|
42
|
+
* "do not ask"; `bypassPermissions` never asks.
|
|
43
|
+
*/
|
|
44
|
+
export declare function claudeModeUsesPermissionPrompt(mode: ClaudePermissionMode): boolean;
|
|
45
|
+
/** Plain CodeShell-side name; the bridge qualifies it as `mcp__<server>__…`. */
|
|
46
|
+
export declare const CLAUDE_PERMISSION_PROMPT_TOOL = "CodeShellPermissionPrompt";
|
|
47
|
+
export interface ClaudePermissionPromptRequest {
|
|
48
|
+
/** Claude's tool name, e.g. `Bash`, `Edit`, `mcp__other__tool`. */
|
|
49
|
+
toolName: string;
|
|
50
|
+
input: Record<string, unknown>;
|
|
51
|
+
toolUseId?: string;
|
|
52
|
+
}
|
|
53
|
+
export type ClaudePermissionPromptDecider = (request: ClaudePermissionPromptRequest, signal?: AbortSignal) => Promise<{
|
|
54
|
+
approved: boolean;
|
|
55
|
+
reason?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Add the permission-prompt tool to a session's bridge surface.
|
|
59
|
+
*
|
|
60
|
+
* Answers, in order:
|
|
61
|
+
* - the prompt tool itself → deny. It is visible to the model like any MCP
|
|
62
|
+
* tool; a direct call must not raise a host approval dialog.
|
|
63
|
+
* - this server's own tools → allow without asking. `ToolExecutor` authorizes
|
|
64
|
+
* them; asking here too would approve one operation twice (the same rule the
|
|
65
|
+
* Codex adapter applies to its MCP elicitations).
|
|
66
|
+
* - `ExitPlanMode` → deny. The mode is CodeShell's selection, not the model's.
|
|
67
|
+
* - everything else → `decide`, failing closed on any error or abort.
|
|
68
|
+
*/
|
|
69
|
+
export declare function withClaudePermissionPrompt(host: BridgeToolHost, decide: ClaudePermissionPromptDecider, serverName: string): BridgeToolHost;
|