@unlinearity/cli-wechat-bridge 1.2.0 → 1.2.2

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 (50) hide show
  1. package/README.md +25 -10
  2. package/bin/wechat-check-update.mjs +0 -0
  3. package/bin/wechat-claude.mjs +0 -0
  4. package/bin/wechat-codex.mjs +0 -0
  5. package/bin/wechat-daemon.mjs +0 -0
  6. package/bin/wechat-opencode.mjs +0 -0
  7. package/bin/wechat-pi.mjs +0 -0
  8. package/bin/wechat-setup.mjs +0 -0
  9. package/bin/wecom-check-update.mjs +0 -0
  10. package/bin/wecom-claude.mjs +0 -0
  11. package/bin/wecom-codex.mjs +0 -0
  12. package/bin/wecom-daemon.mjs +0 -0
  13. package/bin/wecom-opencode.mjs +0 -0
  14. package/bin/wecom-pi.mjs +0 -0
  15. package/bin/wecom-setup.mjs +0 -0
  16. package/dist/bridge/adapter-control.js +19 -12
  17. package/dist/bridge/bridge-adapters.claude.js +219 -115
  18. package/dist/bridge/bridge-adapters.codex.js +295 -90
  19. package/dist/bridge/bridge-adapters.core.js +84 -7
  20. package/dist/bridge/bridge-adapters.opencode.js +344 -74
  21. package/dist/bridge/bridge-adapters.pi.js +23 -18
  22. package/dist/bridge/bridge-adapters.shared.js +9 -9
  23. package/dist/bridge/bridge-session-resume.js +13 -6
  24. package/dist/bridge/bridge-utils.js +94 -144
  25. package/dist/bridge/claude-hooks.js +60 -3
  26. package/dist/bridge/claude-native-controls.js +32 -13
  27. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  28. package/dist/bridge/language-control.js +33 -0
  29. package/dist/bridge/native-terminal-control.js +38 -10
  30. package/dist/bridge/opencode-v2-compat.js +458 -0
  31. package/dist/bridge/wechat-bridge.js +57 -42
  32. package/dist/channels/channel-driver-factory.js +44 -0
  33. package/dist/channels/local/local-channel-port.js +40 -0
  34. package/dist/channels/local/local-driver.js +217 -0
  35. package/dist/channels/wechat/wechat-driver.js +11 -2
  36. package/dist/channels/wecom/wecom-driver.js +8 -1
  37. package/dist/companion/local-companion.js +27 -7
  38. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  39. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  40. package/dist/core/bridge-control.js +3 -0
  41. package/dist/core/bridge-defer.js +3 -2
  42. package/dist/core/bridge-event-forwarder.js +3 -0
  43. package/dist/core/channel-types.js +5 -1
  44. package/dist/daemon/wechat-daemon.js +202 -124
  45. package/dist/i18n/index.js +25 -11
  46. package/dist/i18n/messages-en.js +244 -1
  47. package/dist/i18n/messages-zh.js +244 -1
  48. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  49. package/dist/wechat/channel-config.js +3 -2
  50. package/package.json +4 -2
@@ -0,0 +1,458 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ function isRecord(value) {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function ok(data) {
11
+ return { data };
12
+ }
13
+ async function result(operation) {
14
+ try {
15
+ return ok(await operation());
16
+ }
17
+ catch (error) {
18
+ return { error };
19
+ }
20
+ }
21
+ function location(directory) {
22
+ return typeof directory === "string" && directory
23
+ ? { directory }
24
+ : undefined;
25
+ }
26
+ export function normalizeOpenCodeV2Session(value) {
27
+ const session = isRecord(value) ? value : {};
28
+ const sessionLocation = isRecord(session.location) ? session.location : {};
29
+ const time = isRecord(session.time) ? session.time : {};
30
+ const model = isRecord(session.model) ? session.model : undefined;
31
+ return {
32
+ ...session,
33
+ id: typeof session.id === "string" ? session.id : "",
34
+ directory: typeof sessionLocation.directory === "string"
35
+ ? sessionLocation.directory
36
+ : "",
37
+ workspaceID: typeof session.projectID === "string" ? session.projectID : undefined,
38
+ parentID: typeof session.parentID === "string" ? session.parentID : undefined,
39
+ time: {
40
+ ...time,
41
+ created: typeof time.created === "number" ? time.created : Date.now(),
42
+ updated: typeof time.updated === "number" ? time.updated : Date.now(),
43
+ },
44
+ model: model
45
+ ? {
46
+ ...model,
47
+ id: typeof model.id === "string" ? model.id : "",
48
+ providerID: typeof model.providerID === "string" ? model.providerID : "",
49
+ variant: typeof model.variant === "string" ? model.variant : "default",
50
+ }
51
+ : undefined,
52
+ };
53
+ }
54
+ function sessionFromCreatedEvent(data) {
55
+ const eventLocation = isRecord(data.location) ? data.location : {};
56
+ return normalizeOpenCodeV2Session({
57
+ id: data.sessionID,
58
+ projectID: data.projectID,
59
+ parentID: data.parentID,
60
+ title: data.title,
61
+ agent: data.agent,
62
+ model: data.model,
63
+ location: eventLocation,
64
+ time: { created: Date.now(), updated: Date.now() },
65
+ });
66
+ }
67
+ function textPart(data, text) {
68
+ const ordinal = typeof data.ordinal === "number" ? data.ordinal : 0;
69
+ return {
70
+ id: `${String(data.assistantMessageID ?? "message")}:text:${ordinal}`,
71
+ sessionID: data.sessionID,
72
+ messageID: data.assistantMessageID,
73
+ type: "text",
74
+ ...(text === undefined ? {} : { text }),
75
+ };
76
+ }
77
+ function normalizeFormQuestions(form) {
78
+ const fields = Array.isArray(form.fields) ? form.fields : [];
79
+ return fields.flatMap((field) => {
80
+ if (!isRecord(field) || typeof field.key !== "string")
81
+ return [];
82
+ const configuredOptions = Array.isArray(field.options)
83
+ ? field.options.filter(isRecord).map((option) => ({
84
+ label: typeof option.label === "string" ? option.label : String(option.value ?? ""),
85
+ description: typeof option.description === "string" ? option.description : "",
86
+ }))
87
+ : [];
88
+ const options = field.type === "boolean" && configuredOptions.length === 0
89
+ ? [
90
+ { label: "Yes", description: "Answer true." },
91
+ { label: "No", description: "Answer false." },
92
+ ]
93
+ : configuredOptions;
94
+ return [{
95
+ id: field.key,
96
+ header: typeof field.title === "string"
97
+ ? field.title
98
+ : typeof form.title === "string"
99
+ ? form.title
100
+ : "OpenCode question",
101
+ question: typeof field.description === "string"
102
+ ? field.description
103
+ : typeof field.title === "string"
104
+ ? field.title
105
+ : field.key,
106
+ options,
107
+ multiple: field.type === "multiselect",
108
+ custom: field.custom === true || options.length === 0,
109
+ }];
110
+ });
111
+ }
112
+ function normalizeFormAnswer(field, values) {
113
+ const options = Array.isArray(field.options) ? field.options.filter(isRecord) : [];
114
+ const normalizedValues = values.map((value) => {
115
+ const option = options.find((option) => option.label === value);
116
+ return typeof option?.value === "string" ? option.value : value;
117
+ });
118
+ if (field.type === "multiselect")
119
+ return normalizedValues;
120
+ const value = normalizedValues[0] ?? "";
121
+ if (field.type === "boolean") {
122
+ return /^(1|true|yes|y)$/i.test(String(value));
123
+ }
124
+ if (field.type === "number" || field.type === "integer") {
125
+ const number = Number(value);
126
+ return Number.isFinite(number) ? number : value;
127
+ }
128
+ return value;
129
+ }
130
+ /** Convert OpenCode 2 events to the stable event vocabulary consumed by the adapter. */
131
+ export function normalizeOpenCodeV2Event(value) {
132
+ if (!isRecord(value) || typeof value.type !== "string")
133
+ return [];
134
+ const data = isRecord(value.data) ? value.data : {};
135
+ const eventLocation = isRecord(value.location) ? value.location : {};
136
+ const directory = typeof eventLocation.directory === "string"
137
+ ? eventLocation.directory
138
+ : undefined;
139
+ switch (value.type) {
140
+ case "session.created":
141
+ return [{ type: "session.created", directory, properties: { info: sessionFromCreatedEvent(data) } }];
142
+ case "session.deleted":
143
+ return [{ type: "session.deleted", directory, properties: { sessionID: data.sessionID } }];
144
+ case "session.status":
145
+ return [{ type: "session.status", directory, properties: data }];
146
+ case "session.execution.started":
147
+ return [{ type: "session.status", directory, properties: { sessionID: data.sessionID, status: { type: "busy" } } }];
148
+ case "session.execution.succeeded":
149
+ case "session.execution.interrupted":
150
+ return [{ type: "session.idle", directory, properties: { sessionID: data.sessionID } }];
151
+ case "session.execution.failed":
152
+ return [
153
+ { type: "session.error", directory, properties: { sessionID: data.sessionID, error: data.error } },
154
+ { type: "session.idle", directory, properties: { sessionID: data.sessionID } },
155
+ ];
156
+ case "session.text.started":
157
+ return [{
158
+ type: "message.part.updated",
159
+ directory,
160
+ properties: { part: textPart(data, "") },
161
+ }];
162
+ case "session.text.delta":
163
+ return [{
164
+ type: "message.part.delta",
165
+ directory,
166
+ properties: {
167
+ sessionID: data.sessionID,
168
+ messageID: data.assistantMessageID,
169
+ partID: textPart(data).id,
170
+ field: "text",
171
+ delta: data.delta,
172
+ },
173
+ }];
174
+ case "session.text.ended":
175
+ return [{
176
+ type: "message.part.updated",
177
+ directory,
178
+ properties: { part: textPart(data, typeof data.text === "string" ? data.text : "") },
179
+ }];
180
+ case "session.inbox.enqueued": {
181
+ const item = isRecord(data.item) ? data.item : {};
182
+ const payload = isRecord(item.payload) ? item.payload : {};
183
+ if (item.type !== "user" || typeof payload.text !== "string")
184
+ return [];
185
+ const messageID = typeof data.inboxID === "string" ? data.inboxID : String(value.id ?? "user");
186
+ return [
187
+ {
188
+ type: "message.updated",
189
+ directory,
190
+ properties: {
191
+ sessionID: data.sessionID,
192
+ info: { id: messageID, sessionID: data.sessionID, role: "user" },
193
+ },
194
+ },
195
+ {
196
+ type: "message.part.updated",
197
+ directory,
198
+ properties: {
199
+ sessionID: data.sessionID,
200
+ part: {
201
+ id: `${messageID}:text:0`,
202
+ sessionID: data.sessionID,
203
+ messageID,
204
+ type: "text",
205
+ text: payload.text,
206
+ },
207
+ },
208
+ },
209
+ ];
210
+ }
211
+ case "permission.asked":
212
+ return [{
213
+ type: "permission.asked",
214
+ directory,
215
+ properties: {
216
+ ...data,
217
+ requestID: data.id,
218
+ permission: data.action,
219
+ patterns: data.resources,
220
+ },
221
+ }];
222
+ case "permission.replied":
223
+ return [{ type: "permission.replied", directory, properties: data }];
224
+ case "form.created": {
225
+ const form = isRecord(data.form) ? data.form : {};
226
+ return [{
227
+ type: "question.asked",
228
+ directory,
229
+ properties: {
230
+ sessionID: form.sessionID,
231
+ id: form.id,
232
+ requestID: form.id,
233
+ questions: normalizeFormQuestions(form),
234
+ },
235
+ }];
236
+ }
237
+ case "form.replied": {
238
+ const form = isRecord(data.form) ? data.form : data;
239
+ return [{
240
+ type: "question.replied",
241
+ directory,
242
+ properties: { sessionID: form.sessionID, requestID: form.id ?? data.formID },
243
+ }];
244
+ }
245
+ case "form.cancelled": {
246
+ const form = isRecord(data.form) ? data.form : data;
247
+ return [{
248
+ type: "question.rejected",
249
+ directory,
250
+ properties: { sessionID: form.sessionID, requestID: form.id ?? data.formID },
251
+ }];
252
+ }
253
+ case "server.connected":
254
+ case "session.idle":
255
+ case "tui.prompt.append":
256
+ case "tui.command.execute":
257
+ case "tui.session.select":
258
+ case "tui.toast.show":
259
+ return [{ type: value.type, directory, properties: data }];
260
+ default:
261
+ return [];
262
+ }
263
+ }
264
+ function mapProviders(providersValue, modelsValue) {
265
+ const providers = isRecord(providersValue) && Array.isArray(providersValue.data)
266
+ ? providersValue.data.filter(isRecord)
267
+ : [];
268
+ const models = isRecord(modelsValue) && Array.isArray(modelsValue.data)
269
+ ? modelsValue.data.filter(isRecord)
270
+ : [];
271
+ const providerById = new Map();
272
+ for (const provider of providers) {
273
+ if (typeof provider.id === "string")
274
+ providerById.set(provider.id, provider);
275
+ }
276
+ for (const model of models) {
277
+ if (typeof model.providerID !== "string" || providerById.has(model.providerID))
278
+ continue;
279
+ providerById.set(model.providerID, {
280
+ id: model.providerID,
281
+ name: model.providerID,
282
+ activation: "enabled",
283
+ });
284
+ }
285
+ const all = Array.from(providerById.values()).map((provider) => {
286
+ const providerID = typeof provider.id === "string" ? provider.id : "";
287
+ const providerModels = Object.fromEntries(models
288
+ .filter((model) => model.providerID === providerID && typeof model.id === "string")
289
+ .map((model) => {
290
+ const variants = Array.isArray(model.variants)
291
+ ? Object.fromEntries(model.variants.filter(isRecord).flatMap((variant) => typeof variant.id === "string" ? [[variant.id, variant]] : []))
292
+ : isRecord(model.variants)
293
+ ? model.variants
294
+ : {};
295
+ return [model.id, {
296
+ ...model,
297
+ name: typeof model.name === "string" ? model.name : model.id,
298
+ variants,
299
+ }];
300
+ }));
301
+ return { ...provider, models: providerModels };
302
+ });
303
+ return {
304
+ all,
305
+ connected: all
306
+ .filter((provider) => provider.activation !== "disabled" && typeof provider.id === "string")
307
+ .map((provider) => provider.id),
308
+ };
309
+ }
310
+ export async function createOpenCodeV2CompatClient(options) {
311
+ const { OpenCode } = await import("@opencode/client");
312
+ const client = OpenCode.make({ baseUrl: options.baseUrl, headers: options.headers });
313
+ const formFields = new Map();
314
+ const formSessions = new Map();
315
+ const permissionSessions = new Map();
316
+ const sessionStatuses = new Map();
317
+ const waitForPopulatedList = async (load) => {
318
+ let response;
319
+ for (let attempt = 0; attempt < 20; attempt += 1) {
320
+ response = await load();
321
+ if (isRecord(response) && Array.isArray(response.data) && response.data.length > 0) {
322
+ return response;
323
+ }
324
+ await new Promise((resolve) => setTimeout(resolve, 250));
325
+ }
326
+ return response;
327
+ };
328
+ const listAvailableModels = (requestOptions) => waitForPopulatedList(() => client.model.list({ location: directoryLocation }, requestOptions));
329
+ async function* subscribe(input) {
330
+ for await (const event of client.event.subscribe(input)) {
331
+ if (isRecord(event) && event.type === "form.created" && isRecord(event.data) && isRecord(event.data.form)) {
332
+ const form = event.data.form;
333
+ if (typeof form.id === "string" && Array.isArray(form.fields)) {
334
+ formFields.set(form.id, form.fields.filter(isRecord).filter((field) => typeof field.key === "string"));
335
+ if (typeof form.sessionID === "string") {
336
+ formSessions.set(form.id, form.sessionID);
337
+ }
338
+ }
339
+ }
340
+ if (isRecord(event) && event.type === "permission.asked" && isRecord(event.data)) {
341
+ if (typeof event.data.id === "string" && typeof event.data.sessionID === "string") {
342
+ permissionSessions.set(event.data.id, event.data.sessionID);
343
+ }
344
+ }
345
+ if (isRecord(event) && isRecord(event.data) && typeof event.data.sessionID === "string") {
346
+ if (event.type === "session.execution.started") {
347
+ sessionStatuses.set(event.data.sessionID, "busy");
348
+ }
349
+ else if (event.type === "session.execution.succeeded" ||
350
+ event.type === "session.execution.failed" ||
351
+ event.type === "session.execution.interrupted" ||
352
+ event.type === "session.idle") {
353
+ sessionStatuses.set(event.data.sessionID, "idle");
354
+ }
355
+ }
356
+ for (const normalized of normalizeOpenCodeV2Event(event)) {
357
+ yield normalized;
358
+ }
359
+ }
360
+ }
361
+ const directoryLocation = location(options.directory);
362
+ return {
363
+ server: client.server,
364
+ app: {
365
+ agents: (_input, requestOptions) => result(async () => {
366
+ const response = await waitForPopulatedList(() => client.agent.list({ location: directoryLocation }, requestOptions));
367
+ return isRecord(response) && Array.isArray(response.data)
368
+ ? response.data.filter(isRecord).map((agent) => ({
369
+ ...agent,
370
+ name: typeof agent.id === "string" ? agent.id : agent.name,
371
+ }))
372
+ : [];
373
+ }),
374
+ },
375
+ provider: {
376
+ list: (_input, requestOptions) => result(async () => mapProviders(await client.provider.list({ location: directoryLocation }, requestOptions), await listAvailableModels(requestOptions))),
377
+ },
378
+ session: {
379
+ create: (_input, requestOptions) => result(async () => normalizeOpenCodeV2Session(await client.session.create({ location: directoryLocation }, requestOptions))),
380
+ list: (_input, requestOptions) => result(async () => {
381
+ const response = await client.session.list({ directory: options.directory, order: "desc" }, requestOptions);
382
+ return isRecord(response) && Array.isArray(response.data)
383
+ ? response.data.map(normalizeOpenCodeV2Session)
384
+ : [];
385
+ }),
386
+ get: (input, requestOptions) => result(async () => normalizeOpenCodeV2Session(await client.session.get({ sessionID: input.sessionID }, requestOptions))),
387
+ status: (_input, requestOptions) => result(async () => {
388
+ const response = await client.session.list({ directory: options.directory, order: "desc" }, requestOptions);
389
+ const sessions = isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
390
+ return Object.fromEntries(sessions.flatMap((session) => {
391
+ if (typeof session.id !== "string")
392
+ return [];
393
+ return [[session.id, { type: sessionStatuses.get(session.id) ?? "idle" }]];
394
+ }));
395
+ }),
396
+ promptAsync: (input, requestOptions) => result(async () => {
397
+ const parts = Array.isArray(input.parts) ? input.parts.filter(isRecord) : [];
398
+ const text = parts.flatMap((part) => part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
399
+ await client.session.prompt({ sessionID: input.sessionID, text }, requestOptions);
400
+ return {};
401
+ }),
402
+ abort: (input, requestOptions) => result(async () => await client.session.interrupt({ sessionID: input.sessionID }, requestOptions)),
403
+ },
404
+ v2: {
405
+ session: {
406
+ switchAgent: (input, requestOptions) => result(async () => await client.session.switchAgent(input, requestOptions)),
407
+ switchModel: (input, requestOptions) => result(async () => await client.session.switchModel(input, requestOptions)),
408
+ },
409
+ },
410
+ permission: {
411
+ reply: (input, requestOptions) => result(async () => await client.permission.reply({
412
+ sessionID: input.sessionID ?? permissionSessions.get(String(input.requestID)),
413
+ requestID: input.requestID,
414
+ decision: input.reply === "reject" ? "reject" : input.reply,
415
+ }, requestOptions).then((response) => {
416
+ permissionSessions.delete(String(input.requestID));
417
+ return response;
418
+ })),
419
+ },
420
+ question: {
421
+ reply: (input, requestOptions) => result(async () => {
422
+ const answers = Array.isArray(input.answers) ? input.answers : [];
423
+ const fields = formFields.get(String(input.requestID)) ?? [];
424
+ const answer = Object.fromEntries(fields.flatMap((field, index) => {
425
+ if (typeof field.key !== "string")
426
+ return [];
427
+ const values = Array.isArray(answers[index]) ? answers[index] : [];
428
+ return [[field.key, normalizeFormAnswer(field, values)]];
429
+ }));
430
+ await client.session.form.reply({
431
+ sessionID: input.sessionID ?? formSessions.get(String(input.requestID)),
432
+ formID: input.requestID,
433
+ answer,
434
+ }, requestOptions);
435
+ formFields.delete(String(input.requestID));
436
+ formSessions.delete(String(input.requestID));
437
+ return {};
438
+ }),
439
+ reject: (input, requestOptions) => result(async () => {
440
+ await client.session.form.cancel({
441
+ sessionID: input.sessionID ?? formSessions.get(String(input.requestID)),
442
+ formID: input.requestID,
443
+ }, requestOptions);
444
+ formFields.delete(String(input.requestID));
445
+ formSessions.delete(String(input.requestID));
446
+ return {};
447
+ }),
448
+ },
449
+ event: {
450
+ subscribe: async (_input, requestOptions) => ({
451
+ stream: subscribe(requestOptions),
452
+ }),
453
+ },
454
+ tui: {
455
+ selectSession: async () => ok({}),
456
+ },
457
+ };
458
+ }