Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 37 additions & 29 deletions src/claude-code-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,9 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
content: [{ type: "text", text }] as any,
finishReason: "stop",
usage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},
inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 0, text: undefined, reasoning: undefined },
} as unknown as LanguageModelV2Usage,
Comment on lines +147 to +149

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage shape is being changed and then forced through with as unknown as LanguageModelV2Usage, which bypasses type safety and may break downstream consumers expecting the previous runtime shape. Prefer constructing a LanguageModelV2Usage value that type-checks without double-casting (or keep the prior shape) and ensure the change is intentional/documented since it’s outside the PR’s stated scope.

Suggested change
inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 0, text: undefined, reasoning: undefined },
} as unknown as LanguageModelV2Usage,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},

Copilot uses AI. Check for mistakes.
request: { body: { text: "" } },
response: {
id: generateId(),
Expand Down Expand Up @@ -185,6 +184,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
skipPermissions: this.config.skipPermissions !== false,
includeSessionId: false,
model: this.modelId,
mcpConfigPath: this.config.mcpConfigPath,
})

log.info("doGenerate starting", {
Expand All @@ -200,6 +200,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
const proc = spawn(this.config.cliPath, cliArgs, {
cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
env: { ...process.env, TERM: "xterm-256color" },
})

Comment on lines 200 to 206

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as the session manager: shell: process.platform === "win32" can change quoting semantics and allow shell interpretation of arguments. Prefer a .cmd-specific execution path (or an escaping helper) so only the Windows shim case uses a shell and arguments remain safely escaped.

Suggested change
const proc = spawn(this.config.cliPath, cliArgs, {
cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
env: { ...process.env, TERM: "xterm-256color" },
})
const isWindowsCmdShim =
process.platform === "win32" &&
/\.(cmd|bat)$/i.test(this.config.cliPath)
const escapeForCmdExe = (value: string): string => {
const escapedForQuotes = value.replace(/"/g, '""')
const escapedMetaChars = escapedForQuotes.replace(
/([()%!^"<>&|])/g,
"^$1",
)
return `"${escapedMetaChars}"`
}
const proc = isWindowsCmdShim
? spawn(process.env.ComSpec ?? "cmd.exe", [
"/d",
"/s",
"/c",
`${escapeForCmdExe(this.config.cliPath)} ${cliArgs
.map((arg) => escapeForCmdExe(arg))
.join(" ")}`,
], {
cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: false,
env: { ...process.env, TERM: "xterm-256color" },
})
: spawn(this.config.cliPath, cliArgs, {
cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: false,
env: { ...process.env, TERM: "xterm-256color" },
})

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -396,14 +397,19 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
} as any)
}

const usage: LanguageModelV2Usage = {
inputTokens: result.usage?.input_tokens,
outputTokens: result.usage?.output_tokens,
totalTokens:
result.usage?.input_tokens && result.usage?.output_tokens
? result.usage.input_tokens + result.usage.output_tokens
: undefined,
}
const usage = {
inputTokens: {
total: result.usage?.input_tokens ?? 0,
noCache: undefined,
cacheRead: (result.usage as any)?.cache_read_input_tokens ?? undefined,
cacheWrite: undefined,
},
outputTokens: {
total: result.usage?.output_tokens ?? 0,
text: undefined,
reasoning: undefined,
},
} as unknown as LanguageModelV2Usage

return {
content,
Expand Down Expand Up @@ -455,10 +461,9 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
type: "finish",
finishReason: "stop",
usage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},
inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 0, text: undefined, reasoning: undefined },
} as unknown as LanguageModelV2Usage,
providerMetadata: {
"claude-code": {
synthetic: true,
Expand Down Expand Up @@ -505,6 +510,7 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
sessionKey: sk,
skipPermissions,
model: this.modelId,
mcpConfigPath: this.config.mcpConfigPath,
})

const stream = new ReadableStream<LanguageModelV2StreamPart>({
Expand Down Expand Up @@ -998,15 +1004,18 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
finishReason:
toolCallMap.size > 0 ? "tool-calls" : "stop",
usage: {
inputTokens: msg.usage?.input_tokens,
outputTokens: msg.usage?.output_tokens,
totalTokens:
msg.usage?.input_tokens &&
msg.usage?.output_tokens
? msg.usage.input_tokens +
msg.usage.output_tokens
: undefined,
},
inputTokens: {
total: msg.usage?.input_tokens ?? 0,
noCache: undefined,
cacheRead: (msg.usage as any)?.cache_read_input_tokens ?? undefined,
cacheWrite: undefined,
},
outputTokens: {
total: msg.usage?.output_tokens ?? 0,
text: undefined,
reasoning: undefined,
},
} as unknown as LanguageModelV2Usage,
providerMetadata: {
"claude-code": resultMeta,
},
Expand Down Expand Up @@ -1041,10 +1050,9 @@ export class ClaudeCodeLanguageModel implements LanguageModelV2 {
type: "finish",
finishReason: "stop",
usage: {
inputTokens: undefined,
outputTokens: undefined,
totalTokens: undefined,
},
inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: undefined, text: undefined, reasoning: undefined },
} as unknown as LanguageModelV2Usage,
providerMetadata: {
"claude-code": resultMeta,
},
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function createClaudeCode(
cliPath,
cwd,
skipPermissions: settings.skipPermissions ?? true,
mcpConfigPath: settings.mcpConfigPath,
})
}

Expand Down
8 changes: 5 additions & 3 deletions src/message-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ Now continuing with the current message:
for (const msg of messages) {
if (msg.role === "user") {
if (typeof msg.content === "string") {
content.push({ type: "text", text: msg.content })
if (msg.content.trim()) {
content.push({ type: "text", text: msg.content })
}
} else if (Array.isArray(msg.content)) {
for (const part of msg.content as any[]) {
if (part.type === "text") {
if (!part.text || !part.text.trim()) continue
content.push({ type: "text", text: part.text })
} else if (part.type === "tool-result") {
const p = part as any
Expand Down Expand Up @@ -136,11 +139,10 @@ Now continuing with the current message:
type: "user",
message: {
role: "user",
content: [{ type: "text", text: "" }],
content: [{ type: "text", text: "Continue." }],
},
})
}

return JSON.stringify({
type: "user",
message: {
Expand Down
8 changes: 7 additions & 1 deletion src/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function spawnClaudeProcess(
const proc = spawn(cliPath, cliArgs, {
cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
env: { ...process.env, TERM: "xterm-256color" },
Comment on lines 53 to 57

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, enabling shell: true changes argument handling and can introduce command-injection/quoting issues if any CLI args (e.g., --model, paths) contain shell metacharacters or spaces. Consider avoiding shell unless the target is actually a .cmd shim (e.g., detect/resolve the real executable), or use a Windows-safe spawn helper that properly escapes arguments when invoking cmd.exe.

Copilot uses AI. Check for mistakes.
})

Expand Down Expand Up @@ -107,8 +108,9 @@ export function buildCliArgs(opts: {
skipPermissions: boolean
includeSessionId?: boolean
model?: string
mcpConfigPath?: string
}): string[] {
const { sessionKey, skipPermissions, includeSessionId = true, model } = opts
const { sessionKey, skipPermissions, includeSessionId = true, model, mcpConfigPath } = opts
const args = [
"--output-format",
"stream-json",
Expand All @@ -132,6 +134,10 @@ export function buildCliArgs(opts: {
args.push("--dangerously-skip-permissions")
}

if (mcpConfigPath) {
args.push("--mcp-config", mcpConfigPath)
}

return args
}

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ export interface ClaudeCodeConfig {
cliPath: string
cwd?: string
skipPermissions?: boolean
mcpConfigPath?: string
}

export interface ClaudeCodeProviderSettings {
cliPath?: string
cwd?: string
name?: string
skipPermissions?: boolean
mcpConfigPath?: string
}

/**
Expand Down