Skip to content
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"qrcode-terminal": "^0.12.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/node": "^22.19.15",

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

@types/node was bumped to v22 while engines.node is still ">=20.0.0". Using a newer Node type set than the minimum supported runtime can let TypeScript accept APIs that don't exist in Node 20, leading to runtime failures. Consider pinning @types/node to the minimum supported major (20) or raising the engines.node requirement accordingly.

Suggested change
"@types/node": "^22.19.15",
"@types/node": "^20.17.0",

Copilot uses AI. Check for mistakes.
"typescript": "^5.5.0"
},
"engines": {
Expand Down
348 changes: 177 additions & 171 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,171 +1,177 @@
/**
* Configuration types and defaults for wechat-acp.
*/

import path from "node:path";
import os from "node:os";

export interface AgentCommandConfig {
command: string;
args: string[];
env?: Record<string, string>;
}

export interface AgentPreset extends AgentCommandConfig {
label: string;
description?: string;
}

export interface ResolvedAgentConfig extends AgentCommandConfig {
id?: string;
label?: string;
source: "preset" | "raw";
}

export const BUILT_IN_AGENTS: Record<string, AgentPreset> = {
copilot: {
label: "GitHub Copilot",
command: "npx",
args: ["@github/copilot", "--acp", "--yolo"],
description: "GitHub Copilot",
},
claude: {
label: "Claude Code",
command: "npx",
args: ["@zed-industries/claude-code-acp"],
description: "Claude Code ACP",
},
gemini: {
label: "Gemini CLI",
command: "npx",
args: ["@google/gemini-cli", "--experimental-acp"],
description: "Gemini CLI",
},
qwen: {
label: "Qwen Code",
command: "npx",
args: ["@qwen-code/qwen-code", "--acp", "--experimental-skills"],
description: "Qwen Code",
},
codex: {
label: "Codex CLI",
command: "npx",
args: ["@zed-industries/codex-acp"],
description: "Codex ACP",
},
opencode: {
label: "OpenCode",
command: "npx",
args: ["opencode-ai", "acp"],
description: "OpenCode",
},
};

export interface WeChatAcpConfig {
wechat: {
baseUrl: string;
cdnBaseUrl: string;
botType: string;
};
agent: {
preset?: string;
command: string;
args: string[];
cwd: string;
env?: Record<string, string>;
};
agents: Record<string, AgentPreset>;
session: {
idleTimeoutMs: number;
maxConcurrentUsers: number;
};
daemon: {
enabled: boolean;
logFile: string;
pidFile: string;
};
storage: {
dir: string;
};
}

export function defaultStorageDir(): string {
return path.join(os.homedir(), ".wechat-acp");
}

export function defaultConfig(): WeChatAcpConfig {
const storageDir = defaultStorageDir();
return {
wechat: {
baseUrl: "https://ilinkai.weixin.qq.com",
cdnBaseUrl: "https://novac2c.cdn.weixin.qq.com/c2c",
botType: "3",
},
agent: {
preset: undefined,
command: "",
args: [],
cwd: process.cwd(),
},
agents: { ...BUILT_IN_AGENTS },
session: {
idleTimeoutMs: 1440 * 60_000, // 24 hours
maxConcurrentUsers: 10,
},
daemon: {
enabled: false,
logFile: path.join(storageDir, "wechat-acp.log"),
pidFile: path.join(storageDir, "daemon.pid"),
},
storage: {
dir: storageDir,
},
};
}

/**
* Parse agent string like "claude code" or "npx tsx ./agent.ts"
* into { command, args }.
*/
export function parseAgentCommand(agentStr: string): { command: string; args: string[] } {
const parts = agentStr.trim().split(/\s+/);
if (parts.length === 0 || !parts[0]) {
throw new Error("Agent command cannot be empty");
}
return {
command: parts[0],
args: parts.slice(1),
};
}

export function resolveAgentSelection(
agentSelection: string,
registry: Record<string, AgentPreset> = BUILT_IN_AGENTS,
): ResolvedAgentConfig {
const preset = registry[agentSelection];
if (preset) {
return {
id: agentSelection,
label: preset.label,
command: preset.command,
args: [...preset.args],
env: preset.env ? { ...preset.env } : undefined,
source: "preset",
};
}

const parsed = parseAgentCommand(agentSelection);
return {
command: parsed.command,
args: parsed.args,
source: "raw",
};
}

export function listBuiltInAgents(
registry: Record<string, AgentPreset> = BUILT_IN_AGENTS,
): Array<{ id: string; preset: AgentPreset }> {
return Object.entries(registry)
.sort(([left], [right]) => left.localeCompare(right))
.map(([id, preset]) => ({ id, preset }));
}
/**
* Configuration types and defaults for wechat-acp.
*/

import path from "node:path";
import os from "node:os";

export interface AgentCommandConfig {
command: string;
args: string[];
env?: Record<string, string>;
}

export interface AgentPreset extends AgentCommandConfig {
label: string;
description?: string;
}

export interface ResolvedAgentConfig extends AgentCommandConfig {
id?: string;
label?: string;
source: "preset" | "raw";
}

export const BUILT_IN_AGENTS: Record<string, AgentPreset> = {
hermes: {
label: "Hermes Agent",
command: "hermes",
args: ["acp"],
description: "Hermes Agent ACP mode",
},
Comment on lines +25 to +31

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

Adding the new built-in hermes preset makes the user-facing preset list in README out of date (README.md currently lists copilot/claude/gemini/qwen/codex/opencode only). Please update the documentation so npx wechat-acp agents output matches the documented preset list.

Copilot uses AI. Check for mistakes.
copilot: {
label: "GitHub Copilot",
command: "npx",
args: ["@github/copilot", "--acp", "--yolo"],
description: "GitHub Copilot",
},
claude: {
label: "Claude Code",
command: "npx",
args: ["@zed-industries/claude-code-acp"],
description: "Claude Code ACP",
},
gemini: {
label: "Gemini CLI",
command: "npx",
args: ["@google/gemini-cli", "--experimental-acp"],
description: "Gemini CLI",
},
qwen: {
label: "Qwen Code",
command: "npx",
args: ["@qwen-code/qwen-code", "--acp", "--experimental-skills"],
description: "Qwen Code",
},
codex: {
label: "Codex CLI",
command: "npx",
args: ["@zed-industries/codex-acp"],
description: "Codex ACP",
},
opencode: {
label: "OpenCode",
command: "npx",
args: ["opencode-ai", "acp"],
description: "OpenCode",
},
};

export interface WeChatAcpConfig {
wechat: {
baseUrl: string;
cdnBaseUrl: string;
botType: string;
};
agent: {
preset?: string;
command: string;
args: string[];
cwd: string;
env?: Record<string, string>;
};
agents: Record<string, AgentPreset>;
session: {
idleTimeoutMs: number;
maxConcurrentUsers: number;
};
daemon: {
enabled: boolean;
logFile: string;
pidFile: string;
};
storage: {
dir: string;
};
}

export function defaultStorageDir(): string {
return path.join(os.homedir(), ".wechat-acp");
}

export function defaultConfig(): WeChatAcpConfig {
const storageDir = defaultStorageDir();
return {
wechat: {
baseUrl: "https://ilinkai.weixin.qq.com",
cdnBaseUrl: "https://novac2c.cdn.weixin.qq.com/c2c",
botType: "3",
},
agent: {
preset: undefined,
command: "",
args: [],
cwd: process.cwd(),
},
agents: { ...BUILT_IN_AGENTS },
session: {
idleTimeoutMs: 1440 * 60_000, // 24 hours
maxConcurrentUsers: 10,
},
daemon: {
enabled: false,
logFile: path.join(storageDir, "wechat-acp.log"),
pidFile: path.join(storageDir, "daemon.pid"),
},
storage: {
dir: storageDir,
},
};
}

/**
* Parse agent string like "claude code" or "npx tsx ./agent.ts"
* into { command, args }.
*/
export function parseAgentCommand(agentStr: string): { command: string; args: string[] } {
const parts = agentStr.trim().split(/\s+/);
if (parts.length === 0 || !parts[0]) {
throw new Error("Agent command cannot be empty");
}
return {
command: parts[0],
args: parts.slice(1),
};
}

export function resolveAgentSelection(
agentSelection: string,
registry: Record<string, AgentPreset> = BUILT_IN_AGENTS,
): ResolvedAgentConfig {
const preset = registry[agentSelection];
if (preset) {
return {
id: agentSelection,
label: preset.label,
command: preset.command,
args: [...preset.args],
env: preset.env ? { ...preset.env } : undefined,
source: "preset",
};
}

const parsed = parseAgentCommand(agentSelection);
return {
command: parsed.command,
args: parsed.args,
source: "raw",
};
}

export function listBuiltInAgents(
registry: Record<string, AgentPreset> = BUILT_IN_AGENTS,
): Array<{ id: string; preset: AgentPreset }> {
return Object.entries(registry)
.sort(([left], [right]) => left.localeCompare(right))
.map(([id, preset]) => ({ id, preset }));
}
Loading