@cjhyy/code-shell-capability-coding 0.9.44 → 0.9.46

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 (49) hide show
  1. package/dist/capability-runtime.js +4 -1
  2. package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
  3. package/dist/cc-orchestrator/codex-session-history.js +11 -4
  4. package/dist/cc-orchestrator/external-agent-changes.js +7 -65
  5. package/dist/cc-orchestrator/external-agent-driver.js +61 -3
  6. package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
  7. package/dist/cc-orchestrator/session-discovery.js +63 -6
  8. package/dist/cc-orchestrator/session-history.js +5 -0
  9. package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
  10. package/dist/external-runtimes/claude-code/event-translator.js +58 -9
  11. package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
  12. package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
  13. package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
  14. package/dist/external-runtimes/claude-code/permissions.js +120 -0
  15. package/dist/external-runtimes/claude-code/runtime.d.ts +40 -4
  16. package/dist/external-runtimes/claude-code/runtime.js +130 -39
  17. package/dist/external-runtimes/codex/app-server-client.d.ts +12 -0
  18. package/dist/external-runtimes/codex/app-server-client.js +16 -0
  19. package/dist/external-runtimes/codex/runtime.d.ts +11 -0
  20. package/dist/external-runtimes/codex/runtime.js +143 -16
  21. package/dist/external-runtimes/index.d.ts +2 -0
  22. package/dist/external-runtimes/index.js +1 -0
  23. package/dist/external-runtimes/session-factory.d.ts +14 -0
  24. package/dist/external-runtimes/session-factory.js +40 -1
  25. package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
  26. package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
  27. package/dist/git/parse-log.d.ts +6 -2
  28. package/dist/git/parse-log.js +7 -3
  29. package/dist/git/utils.d.ts +4 -0
  30. package/dist/git/utils.js +29 -10
  31. package/dist/git/worktree/crud.js +28 -0
  32. package/dist/git/worktree/diff.js +57 -29
  33. package/dist/index.capability.js +14 -1
  34. package/dist/index.git.d.ts +1 -0
  35. package/dist/index.git.js +1 -0
  36. package/dist/quota/index.d.ts +2 -1
  37. package/dist/quota/index.js +27 -7
  38. package/dist/quota/types.d.ts +5 -2
  39. package/dist/review/review-prompt.d.ts +2 -0
  40. package/dist/review/review-prompt.js +70 -29
  41. package/dist/review/review-target.d.ts +29 -0
  42. package/dist/review/review-target.js +353 -0
  43. package/dist/tools/apply-patch/applier.js +64 -39
  44. package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
  45. package/dist/tools/apply-patch/backup-targets.js +24 -0
  46. package/dist/tools/apply-patch/parser.js +4 -2
  47. package/dist/tools/check-quota.js +3 -1
  48. package/dist/tools/lsp.js +36 -5
  49. 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
- parts.push(`Status:\n${status.map((entry) => `${entry.status} ${entry.path}`).join("\n")}`);
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 && meta.id === threadId && meta.cwd === cwd)
199
- return file;
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 undefined;
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, readdirSync, statSync, openSync, readSync, closeSync, } from "node:fs";
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>`. Match those directly off the raw text.
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
- const file = findRolloutFile(codexHome, cwd, threadId);
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()).then(() => {
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(reject);
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 all session files for `cwd`, cheaply (stat only). Lets the UI know
98
- * whether a bounded `discoverSessions` left older sessions unshown. */
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.type !== "user")
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
- const selected = selectRecentStats(listSessionStats(dir), opts);
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 selected) {
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 all session files for `cwd`, cheaply (stat only). Lets the UI know
289
- * whether a bounded `discoverSessions` left older sessions unshown. */
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
- return listSessionStats(dir).length;
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 number = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
203
- const promptTokens = number(usage?.input_tokens);
204
- if (promptTokens !== undefined) {
205
- const completionTokens = number(usage?.output_tokens);
206
- const cacheReadTokens = number(usage?.cache_read_input_tokens);
207
- const cacheCreationTokens = number(usage?.cache_creation_input_tokens);
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 ? { cacheReadTokens } : {}),
215
- ...(cacheCreationTokens !== undefined ? { cacheCreationTokens } : {}),
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
- const dir = mkdtempSync(join(tmpdir(), "codeshell-mcp-"));
62
- const path = join(dir, "mcp-config.json");
63
- writeFileSync(path, buildClaudeMcpConfig(options), { mode: 0o600 });
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;