cli-wechat-bridge 1.0.5
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/LICENSE.txt +21 -0
- package/README.md +637 -0
- package/bin/_run-entry.mjs +35 -0
- package/bin/wechat-bridge-claude.mjs +5 -0
- package/bin/wechat-bridge-codex.mjs +5 -0
- package/bin/wechat-bridge-opencode.mjs +5 -0
- package/bin/wechat-bridge-shell.mjs +5 -0
- package/bin/wechat-bridge.mjs +5 -0
- package/bin/wechat-check-update.mjs +5 -0
- package/bin/wechat-claude-start.mjs +5 -0
- package/bin/wechat-claude.mjs +5 -0
- package/bin/wechat-codex-start.mjs +5 -0
- package/bin/wechat-codex.mjs +5 -0
- package/bin/wechat-daemon.mjs +5 -0
- package/bin/wechat-opencode-start.mjs +5 -0
- package/bin/wechat-opencode.mjs +5 -0
- package/bin/wechat-setup.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +95 -0
- package/dist/bridge/bridge-adapters.claude.js +829 -0
- package/dist/bridge/bridge-adapters.codex.js +2228 -0
- package/dist/bridge/bridge-adapters.core.js +717 -0
- package/dist/bridge/bridge-adapters.js +26 -0
- package/dist/bridge/bridge-adapters.opencode.js +2129 -0
- package/dist/bridge/bridge-adapters.shared.js +1005 -0
- package/dist/bridge/bridge-adapters.shell.js +363 -0
- package/dist/bridge/bridge-controller.js +48 -0
- package/dist/bridge/bridge-final-reply.js +46 -0
- package/dist/bridge/bridge-process-reaper.js +348 -0
- package/dist/bridge/bridge-state.js +362 -0
- package/dist/bridge/bridge-types.js +1 -0
- package/dist/bridge/bridge-utils.js +1240 -0
- package/dist/bridge/claude-hook.js +82 -0
- package/dist/bridge/claude-hooks.js +267 -0
- package/dist/bridge/wechat-bridge.js +1026 -0
- package/dist/commands/check-update.js +30 -0
- package/dist/companion/codex-panel-link.js +72 -0
- package/dist/companion/codex-panel.js +179 -0
- package/dist/companion/codex-remote-client.js +124 -0
- package/dist/companion/local-companion-link.js +240 -0
- package/dist/companion/local-companion-start.js +420 -0
- package/dist/companion/local-companion.js +424 -0
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/media/media-types.js +1 -0
- package/dist/runtime/create-runtime-host.js +12 -0
- package/dist/runtime/legacy-adapter-runtime.js +46 -0
- package/dist/runtime/runtime-types.js +5 -0
- package/dist/utils/version-checker.js +161 -0
- package/dist/wechat/channel-config.js +196 -0
- package/dist/wechat/setup.js +283 -0
- package/dist/wechat/standalone-bot.js +355 -0
- package/dist/wechat/wechat-channel.js +492 -0
- package/dist/wechat/wechat-transport.js +1213 -0
- package/package.json +101 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
async function readStdin() {
|
|
5
|
+
return await new Promise((resolve) => {
|
|
6
|
+
const chunks = [];
|
|
7
|
+
process.stdin.on("data", (chunk) => {
|
|
8
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
9
|
+
});
|
|
10
|
+
process.stdin.on("end", () => {
|
|
11
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
12
|
+
});
|
|
13
|
+
process.stdin.resume();
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const portText = process.env.CLAUDE_WECHAT_HOOK_PORT;
|
|
18
|
+
const token = process.env.CLAUDE_WECHAT_HOOK_TOKEN;
|
|
19
|
+
const port = portText ? Number.parseInt(portText, 10) : Number.NaN;
|
|
20
|
+
if (!token || !Number.isInteger(port) || port <= 0) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const payload = await readStdin();
|
|
24
|
+
if (!payload.trim()) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const requestId = crypto.randomUUID();
|
|
28
|
+
let stdout = "";
|
|
29
|
+
await new Promise((resolve) => {
|
|
30
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
31
|
+
let buffer = "";
|
|
32
|
+
const finish = () => {
|
|
33
|
+
try {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Best effort cleanup.
|
|
38
|
+
}
|
|
39
|
+
resolve();
|
|
40
|
+
};
|
|
41
|
+
socket.once("connect", () => {
|
|
42
|
+
try {
|
|
43
|
+
socket.write(`${JSON.stringify({ token, requestId, payload })}\n`);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
finish();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
socket.setEncoding("utf8");
|
|
50
|
+
socket.on("data", (chunk) => {
|
|
51
|
+
buffer += chunk;
|
|
52
|
+
while (true) {
|
|
53
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
54
|
+
if (newlineIndex < 0) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
58
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
59
|
+
if (!line) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const response = JSON.parse(line);
|
|
64
|
+
if (response.requestId === requestId && typeof response.stdout === "string") {
|
|
65
|
+
stdout = response.stdout;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore malformed hook responses.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
socket.once("close", finish);
|
|
74
|
+
socket.once("error", finish);
|
|
75
|
+
});
|
|
76
|
+
if (stdout) {
|
|
77
|
+
process.stdout.write(stdout);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
main().catch(() => {
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { normalizeOutput, truncatePreview } from "./bridge-utils.js";
|
|
2
|
+
function quoteWindowsCommandArg(value) {
|
|
3
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
4
|
+
}
|
|
5
|
+
function quotePosixCommandArg(value) {
|
|
6
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
export function parseClaudeHookPayload(raw) {
|
|
9
|
+
const trimmed = raw.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(trimmed);
|
|
15
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function extractClaudeResumeConversationId(transcriptPath) {
|
|
22
|
+
if (typeof transcriptPath !== "string") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = transcriptPath.trim();
|
|
26
|
+
if (!trimmed) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const segments = trimmed.split(/[\\/]+/);
|
|
30
|
+
const fileName = segments[segments.length - 1] ?? "";
|
|
31
|
+
if (!fileName.toLowerCase().endsWith(".jsonl")) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const conversationId = fileName.slice(0, -".jsonl".length).trim();
|
|
35
|
+
return conversationId || null;
|
|
36
|
+
}
|
|
37
|
+
export function buildClaudeHookSettings(command) {
|
|
38
|
+
const hook = {
|
|
39
|
+
hooks: [
|
|
40
|
+
{
|
|
41
|
+
type: "command",
|
|
42
|
+
command,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
hooks: {
|
|
48
|
+
SessionStart: [hook],
|
|
49
|
+
UserPromptSubmit: [hook],
|
|
50
|
+
PermissionRequest: [hook],
|
|
51
|
+
Notification: [
|
|
52
|
+
{
|
|
53
|
+
matcher: "permission_prompt",
|
|
54
|
+
hooks: hook.hooks,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
Stop: [hook],
|
|
58
|
+
StopFailure: [hook],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function buildClaudeHookScript(params) {
|
|
63
|
+
const runtimeArgs = ["--no-warnings"];
|
|
64
|
+
if (params.hookEntryPath.endsWith(".ts")) {
|
|
65
|
+
runtimeArgs.push("--experimental-strip-types");
|
|
66
|
+
}
|
|
67
|
+
runtimeArgs.push(params.hookEntryPath);
|
|
68
|
+
if (params.platform === "win32") {
|
|
69
|
+
const command = [
|
|
70
|
+
params.runtimeExecPath,
|
|
71
|
+
...runtimeArgs,
|
|
72
|
+
].map(quoteWindowsCommandArg).join(" ");
|
|
73
|
+
return [
|
|
74
|
+
"@echo off",
|
|
75
|
+
"setlocal",
|
|
76
|
+
`set "CLAUDE_WECHAT_HOOK_PORT=${params.hookPort}"`,
|
|
77
|
+
`set "CLAUDE_WECHAT_HOOK_TOKEN=${params.hookToken}"`,
|
|
78
|
+
// Claude reads hook decisions from stdout, so only stderr can be discarded here.
|
|
79
|
+
`${command} 2>nul`,
|
|
80
|
+
"exit /b 0",
|
|
81
|
+
].join("\r\n");
|
|
82
|
+
}
|
|
83
|
+
const command = [
|
|
84
|
+
params.runtimeExecPath,
|
|
85
|
+
...runtimeArgs,
|
|
86
|
+
].map(quotePosixCommandArg).join(" ");
|
|
87
|
+
return [
|
|
88
|
+
"#!/bin/sh",
|
|
89
|
+
`export CLAUDE_WECHAT_HOOK_PORT=${quotePosixCommandArg(String(params.hookPort))}`,
|
|
90
|
+
`export CLAUDE_WECHAT_HOOK_TOKEN=${quotePosixCommandArg(params.hookToken)}`,
|
|
91
|
+
// Claude reads hook decisions from stdout, so only stderr can be discarded here.
|
|
92
|
+
`${command} 2>/dev/null || true`,
|
|
93
|
+
"exit 0",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
function summarizeClaudePlan(plan) {
|
|
97
|
+
const lines = normalizeOutput(plan)
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((line) => line.trim())
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
if (lines.length === 0) {
|
|
102
|
+
return "(empty plan)";
|
|
103
|
+
}
|
|
104
|
+
const heading = lines
|
|
105
|
+
.find((line) => /^#+\s+/.test(line))
|
|
106
|
+
?.replace(/^#+\s+/, "")
|
|
107
|
+
.trim();
|
|
108
|
+
const description = lines.find((line) => !/^#+\s+/.test(line) &&
|
|
109
|
+
!/^[-*]\s+/.test(line) &&
|
|
110
|
+
!/^\d+\.\s+/.test(line));
|
|
111
|
+
return truncatePreview([heading, description].filter(Boolean).join(" - ") || lines[0] || "(empty plan)", 180);
|
|
112
|
+
}
|
|
113
|
+
function summarizeClaudeToolInput(toolName, toolInput) {
|
|
114
|
+
if (!toolInput) {
|
|
115
|
+
return {
|
|
116
|
+
detailLabel: "details",
|
|
117
|
+
detailPreview: "(no input)",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (toolName === "ExitPlanMode" && typeof toolInput.plan === "string") {
|
|
121
|
+
return {
|
|
122
|
+
detailLabel: "plan",
|
|
123
|
+
detailPreview: summarizeClaudePlan(toolInput.plan),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (typeof toolInput.command === "string" && toolInput.command.trim()) {
|
|
127
|
+
return {
|
|
128
|
+
detailLabel: "command",
|
|
129
|
+
detailPreview: toolInput.command.trim(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (typeof toolInput.file_path === "string" && toolInput.file_path.trim()) {
|
|
133
|
+
return {
|
|
134
|
+
detailLabel: "path",
|
|
135
|
+
detailPreview: toolInput.file_path.trim(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (typeof toolInput.pattern === "string" && toolInput.pattern.trim()) {
|
|
139
|
+
return {
|
|
140
|
+
detailLabel: "pattern",
|
|
141
|
+
detailPreview: toolInput.pattern.trim(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (typeof toolInput.url === "string" && toolInput.url.trim()) {
|
|
145
|
+
return {
|
|
146
|
+
detailLabel: "url",
|
|
147
|
+
detailPreview: toolInput.url.trim(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
detailLabel: "details",
|
|
152
|
+
detailPreview: truncatePreview(JSON.stringify(toolInput), 180),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function buildClaudePermissionApprovalRequest(payload) {
|
|
156
|
+
const toolName = typeof payload.tool_name === "string" && payload.tool_name.trim()
|
|
157
|
+
? payload.tool_name.trim()
|
|
158
|
+
: "Tool";
|
|
159
|
+
const { detailLabel, detailPreview } = summarizeClaudeToolInput(toolName, payload.tool_input);
|
|
160
|
+
return {
|
|
161
|
+
source: "cli",
|
|
162
|
+
summary: `Claude permission is required for ${toolName}.`,
|
|
163
|
+
commandPreview: `${toolName}: ${detailPreview}`,
|
|
164
|
+
toolName,
|
|
165
|
+
detailLabel,
|
|
166
|
+
detailPreview,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function buildClaudePermissionDecisionHookOutput(action) {
|
|
170
|
+
const decision = action === "confirm"
|
|
171
|
+
? {
|
|
172
|
+
behavior: "allow",
|
|
173
|
+
}
|
|
174
|
+
: {
|
|
175
|
+
behavior: "deny",
|
|
176
|
+
message: "Permission denied from WeChat bridge.",
|
|
177
|
+
interrupt: false,
|
|
178
|
+
};
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
hookSpecificOutput: {
|
|
181
|
+
hookEventName: "PermissionRequest",
|
|
182
|
+
decision,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function extractClaudeAssistantMessageText(payload) {
|
|
187
|
+
return typeof payload.last_assistant_message === "string"
|
|
188
|
+
? normalizeOutput(payload.last_assistant_message).trim()
|
|
189
|
+
: "";
|
|
190
|
+
}
|
|
191
|
+
function extractClaudeAssistantContentText(content) {
|
|
192
|
+
if (!Array.isArray(content)) {
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
const parts = content
|
|
196
|
+
.flatMap((item) => {
|
|
197
|
+
if (!item || typeof item !== "object") {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const candidate = item;
|
|
201
|
+
if (candidate.type !== "text" || typeof candidate.text !== "string") {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const text = normalizeOutput(candidate.text).trim();
|
|
205
|
+
return text ? [text] : [];
|
|
206
|
+
});
|
|
207
|
+
return parts.join("\n\n").trim();
|
|
208
|
+
}
|
|
209
|
+
export function extractClaudeTranscriptFinalReply(rawTranscript) {
|
|
210
|
+
const lines = rawTranscript.split(/\r?\n/);
|
|
211
|
+
let fallbackText = null;
|
|
212
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
213
|
+
const line = lines[index]?.trim();
|
|
214
|
+
if (!line) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(line);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!parsed || typeof parsed !== "object") {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (parsed.type !== "assistant" || !parsed.message || parsed.message.role !== "assistant") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const text = extractClaudeAssistantContentText(parsed.message.content);
|
|
231
|
+
if (!text) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (parsed.message.stop_reason === "end_turn") {
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
fallbackText ??= text;
|
|
238
|
+
}
|
|
239
|
+
return fallbackText;
|
|
240
|
+
}
|
|
241
|
+
export function normalizeClaudeAssistantMessage(payload) {
|
|
242
|
+
return extractClaudeAssistantMessageText(payload) || "(no final reply)";
|
|
243
|
+
}
|
|
244
|
+
export function buildClaudeFailureMessage(payload) {
|
|
245
|
+
const details = [
|
|
246
|
+
typeof payload.last_assistant_message === "string"
|
|
247
|
+
? normalizeOutput(payload.last_assistant_message).trim()
|
|
248
|
+
: "",
|
|
249
|
+
typeof payload.error_details === "string"
|
|
250
|
+
? normalizeOutput(payload.error_details).trim()
|
|
251
|
+
: "",
|
|
252
|
+
typeof payload.error === "string" ? payload.error.trim() : "",
|
|
253
|
+
].filter(Boolean);
|
|
254
|
+
return truncatePreview(details.join(" | ") || "Claude reported an unknown error.", 500);
|
|
255
|
+
}
|
|
256
|
+
export function findInjectedClaudePromptIndex(prompt, pendingInputs, nowMs = Date.now(), maxAgeMs = 15_000) {
|
|
257
|
+
const normalizedPrompt = normalizeOutput(prompt).trim();
|
|
258
|
+
if (!normalizedPrompt) {
|
|
259
|
+
return -1;
|
|
260
|
+
}
|
|
261
|
+
return pendingInputs.findIndex((candidate) => {
|
|
262
|
+
if (nowMs - candidate.createdAtMs > maxAgeMs) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return candidate.normalizedText === normalizedPrompt;
|
|
266
|
+
});
|
|
267
|
+
}
|