owpenwork 0.1.1

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/.env.example ADDED
@@ -0,0 +1,27 @@
1
+ OPENCODE_URL=http://127.0.0.1:4096
2
+ OPENCODE_DIRECTORY=
3
+ OPENCODE_SERVER_USERNAME=
4
+ OPENCODE_SERVER_PASSWORD=
5
+
6
+ TELEGRAM_BOT_TOKEN=
7
+ TELEGRAM_ENABLED=true
8
+
9
+ OWPENBOT_DATA_DIR=~/.owpenbot
10
+ OWPENBOT_DB_PATH=~/.owpenbot/owpenbot.db
11
+ OWPENBOT_CONFIG_PATH=~/.owpenbot/owpenbot.json
12
+ OWPENBOT_HEALTH_PORT=3005
13
+
14
+ WHATSAPP_ACCOUNT_ID=default
15
+ WHATSAPP_AUTH_DIR=
16
+ WHATSAPP_ENABLED=true
17
+ WHATSAPP_DM_POLICY=
18
+ WHATSAPP_SELF_CHAT=false
19
+
20
+ ALLOW_FROM=
21
+ ALLOW_FROM_TELEGRAM=
22
+ ALLOW_FROM_WHATSAPP=
23
+ TOOL_UPDATES_ENABLED=false
24
+ GROUPS_ENABLED=false
25
+ TOOL_OUTPUT_LIMIT=1200
26
+ PERMISSION_MODE=allow
27
+ LOG_LEVEL=info
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # Owpenbot
2
+
3
+ Simple WhatsApp bridge for a running OpenCode server. Telegram support exists but is not yet E2E tested.
4
+
5
+ ## Install + Run (WhatsApp)
6
+
7
+ One-command install (recommended):
8
+
9
+ ```bash
10
+ curl -fsSL https://raw.githubusercontent.com/different-ai/openwork/dev/packages/owpenbot/install.sh | bash
11
+ ```
12
+
13
+ Then follow the printed next steps (run `owpenbot setup`, link WhatsApp, start the bridge).
14
+
15
+ 1) One-command setup (installs deps, builds, creates `.env` if missing):
16
+
17
+ ```bash
18
+ pnpm -C packages/owpenbot setup
19
+ ```
20
+
21
+ 2) (Optional) Fill in `packages/owpenbot/.env` (see `.env.example`).
22
+
23
+ Required:
24
+ - `OPENCODE_URL`
25
+ - `OPENCODE_DIRECTORY`
26
+ - `WHATSAPP_AUTH_DIR`
27
+
28
+ Recommended:
29
+ - `OPENCODE_SERVER_USERNAME`
30
+ - `OPENCODE_SERVER_PASSWORD`
31
+
32
+ 3) Run setup (writes `~/.owpenbot/owpenbot.json`):
33
+
34
+ ```bash
35
+ owpenbot setup
36
+ ```
37
+
38
+ 4) Link WhatsApp (QR):
39
+
40
+ ```bash
41
+ owpenbot whatsapp login
42
+ ```
43
+
44
+ 5) Start the bridge:
45
+
46
+ ```bash
47
+ owpenbot start
48
+ ```
49
+
50
+ Owpenbot keeps the WhatsApp session alive once connected.
51
+
52
+ 6) Pair a user with the bot (only if DM policy is pairing):
53
+
54
+ - Run `owpenbot pairing list` to view pending codes.
55
+ - Approve a code: `owpenbot pairing approve <code>`.
56
+ - The user can then message again to receive OpenCode replies.
57
+
58
+ ## Usage Flows
59
+
60
+ ### One-person flow (personal testing)
61
+
62
+ Use your own WhatsApp account as the bot and test from a second number you control.
63
+
64
+ 1) Run `owpenbot setup` and choose “personal number.”
65
+ 2) Run `owpenbot whatsapp login` to scan the QR.
66
+ 3) Message yourself or from a second number; your number is already allowlisted.
67
+
68
+ Note: WhatsApp’s “message yourself” thread is not reliable for bot testing.
69
+
70
+ ### Two-person flow (dedicated bot)
71
+
72
+ Use a separate WhatsApp number as the bot account so it stays independent from your personal chat history.
73
+
74
+ 1) Create a new WhatsApp account for the dedicated number.
75
+ 2) Run `owpenbot setup` and choose “dedicated number.”
76
+ 3) Run `owpenbot whatsapp login` to scan the QR.
77
+ 4) If DM policy is pairing, approve codes with `owpenbot pairing approve <code>`.
78
+
79
+ ## Telegram (Untested)
80
+
81
+ Telegram support is wired but not E2E tested yet. To try it:
82
+ - Set `TELEGRAM_BOT_TOKEN`.
83
+ - Optionally set `TELEGRAM_ENABLED=true`.
84
+
85
+ ## Commands
86
+
87
+ ```bash
88
+ owpenbot setup
89
+ owpenbot whatsapp login
90
+ owpenbot start
91
+ owpenbot pairing list
92
+ owpenbot pairing approve <code>
93
+ owpenbot status
94
+ ```
95
+
96
+ ## Defaults
97
+
98
+ - SQLite at `~/.owpenbot/owpenbot.db` unless overridden.
99
+ - Config stored at `~/.owpenbot/owpenbot.json` (created by `owpenbot setup`).
100
+ - DM policy defaults to `pairing` unless changed in setup.
101
+ - Group chats are disabled unless `GROUPS_ENABLED=true`.
102
+
103
+ ## Tests
104
+
105
+ ```bash
106
+ pnpm -C packages/owpenbot test:unit
107
+ pnpm -C packages/owpenbot test:smoke
108
+ ```
package/dist/bridge.js ADDED
@@ -0,0 +1,260 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { normalizeWhatsAppId } from "./config.js";
3
+ import { BridgeStore } from "./db.js";
4
+ import { normalizeEvent } from "./events.js";
5
+ import { startHealthServer } from "./health.js";
6
+ import { buildPermissionRules, createClient } from "./opencode.js";
7
+ import { chunkText, formatInputSummary, truncateText } from "./text.js";
8
+ import { createTelegramAdapter } from "./telegram.js";
9
+ import { createWhatsAppAdapter } from "./whatsapp.js";
10
+ const TOOL_LABELS = {
11
+ bash: "bash",
12
+ read: "read",
13
+ write: "write",
14
+ edit: "edit",
15
+ patch: "patch",
16
+ multiedit: "edit",
17
+ grep: "grep",
18
+ glob: "glob",
19
+ task: "agent",
20
+ webfetch: "webfetch",
21
+ };
22
+ export async function startBridge(config, logger) {
23
+ const client = createClient(config);
24
+ const store = new BridgeStore(config.dbPath);
25
+ store.seedAllowlist("telegram", config.allowlist.telegram);
26
+ store.seedAllowlist("whatsapp", [...config.whatsappAllowFrom].filter((entry) => entry !== "*"));
27
+ store.prunePairingRequests();
28
+ const adapters = new Map();
29
+ if (config.telegramEnabled && config.telegramToken) {
30
+ adapters.set("telegram", createTelegramAdapter(config, logger, handleInbound));
31
+ }
32
+ else {
33
+ logger.info("telegram adapter disabled");
34
+ }
35
+ if (config.whatsappEnabled) {
36
+ adapters.set("whatsapp", createWhatsAppAdapter(config, logger, handleInbound, { printQr: true }));
37
+ }
38
+ else {
39
+ logger.info("whatsapp adapter disabled");
40
+ }
41
+ const sessionQueue = new Map();
42
+ const activeRuns = new Map();
43
+ let opencodeHealthy = false;
44
+ let opencodeVersion;
45
+ async function refreshHealth() {
46
+ try {
47
+ const health = await client.global.health();
48
+ opencodeHealthy = Boolean(health.healthy);
49
+ opencodeVersion = health.version;
50
+ }
51
+ catch (error) {
52
+ logger.warn({ error }, "failed to reach opencode health");
53
+ opencodeHealthy = false;
54
+ }
55
+ }
56
+ await refreshHealth();
57
+ const healthTimer = setInterval(refreshHealth, 30_000);
58
+ let stopHealthServer = null;
59
+ if (config.healthPort) {
60
+ stopHealthServer = startHealthServer(config.healthPort, () => ({
61
+ ok: opencodeHealthy,
62
+ opencode: {
63
+ url: config.opencodeUrl,
64
+ healthy: opencodeHealthy,
65
+ version: opencodeVersion,
66
+ },
67
+ channels: {
68
+ telegram: adapters.has("telegram"),
69
+ whatsapp: adapters.has("whatsapp"),
70
+ },
71
+ }), logger);
72
+ }
73
+ const eventAbort = new AbortController();
74
+ void (async () => {
75
+ const subscription = await client.event.subscribe(undefined, { signal: eventAbort.signal });
76
+ for await (const raw of subscription.stream) {
77
+ const event = normalizeEvent(raw);
78
+ if (!event)
79
+ continue;
80
+ if (event.type === "message.part.updated") {
81
+ const part = event.properties?.part;
82
+ if (!part?.sessionID)
83
+ continue;
84
+ const run = activeRuns.get(part.sessionID);
85
+ if (!run || !run.toolUpdatesEnabled)
86
+ continue;
87
+ if (part.type !== "tool")
88
+ continue;
89
+ const callId = part.callID;
90
+ if (!callId)
91
+ continue;
92
+ const state = part.state;
93
+ const status = state?.status ?? "unknown";
94
+ if (run.seenToolStates.get(callId) === status)
95
+ continue;
96
+ run.seenToolStates.set(callId, status);
97
+ const label = TOOL_LABELS[part.tool] ?? part.tool;
98
+ const title = state.title || truncateText(formatInputSummary(state.input ?? {}), 120) || "running";
99
+ let message = `[tool] ${label} ${status}: ${title}`;
100
+ if (status === "completed" && state.output) {
101
+ const output = truncateText(state.output.trim(), config.toolOutputLimit);
102
+ if (output)
103
+ message += `\n${output}`;
104
+ }
105
+ await sendText(run.channel, run.peerId, message);
106
+ }
107
+ if (event.type === "permission.asked") {
108
+ const permission = event.properties;
109
+ if (!permission?.id || !permission.sessionID)
110
+ continue;
111
+ const response = config.permissionMode === "deny" ? "reject" : "always";
112
+ await client.permission.respond({
113
+ sessionID: permission.sessionID,
114
+ permissionID: permission.id,
115
+ response,
116
+ });
117
+ if (response === "reject") {
118
+ const run = activeRuns.get(permission.sessionID);
119
+ if (run) {
120
+ await sendText(run.channel, run.peerId, "Permission denied. Update configuration to allow tools.");
121
+ }
122
+ }
123
+ }
124
+ }
125
+ })().catch((error) => {
126
+ logger.error({ error }, "event stream closed");
127
+ });
128
+ async function sendText(channel, peerId, text) {
129
+ const adapter = adapters.get(channel);
130
+ if (!adapter)
131
+ return;
132
+ const chunks = chunkText(text, adapter.maxTextLength);
133
+ for (const chunk of chunks) {
134
+ logger.info({ channel, peerId, length: chunk.length }, "sending message");
135
+ await adapter.sendText(peerId, chunk);
136
+ }
137
+ }
138
+ async function handleInbound(message) {
139
+ const adapter = adapters.get(message.channel);
140
+ if (!adapter)
141
+ return;
142
+ let inbound = message;
143
+ logger.info({ channel: inbound.channel, peerId: inbound.peerId, length: inbound.text.length }, "received message");
144
+ const peerKey = inbound.channel === "whatsapp" ? normalizeWhatsAppId(inbound.peerId) : inbound.peerId;
145
+ if (inbound.channel === "whatsapp") {
146
+ if (config.whatsappDmPolicy === "disabled") {
147
+ return;
148
+ }
149
+ const allowAll = config.whatsappDmPolicy === "open" || config.whatsappAllowFrom.has("*");
150
+ const isSelf = Boolean(inbound.fromMe && config.whatsappSelfChatMode);
151
+ const allowed = allowAll || isSelf || store.isAllowed("whatsapp", peerKey);
152
+ if (!allowed) {
153
+ if (config.whatsappDmPolicy === "allowlist") {
154
+ await sendText(inbound.channel, inbound.peerId, "Access denied. Ask the owner to allowlist your number.");
155
+ return;
156
+ }
157
+ store.prunePairingRequests();
158
+ const active = store.getPairingRequest("whatsapp", peerKey);
159
+ const pending = store.listPairingRequests("whatsapp");
160
+ if (!active && pending.length >= 3) {
161
+ await sendText(inbound.channel, inbound.peerId, "Pairing queue full. Ask the owner to approve pending requests.");
162
+ return;
163
+ }
164
+ const code = active?.code ?? String(Math.floor(100000 + Math.random() * 900000));
165
+ if (!active) {
166
+ store.createPairingRequest("whatsapp", peerKey, code, 60 * 60_000);
167
+ }
168
+ await sendText(inbound.channel, inbound.peerId, `Pairing required. Ask the owner to approve code: ${code}`);
169
+ return;
170
+ }
171
+ }
172
+ else if (config.allowlist[inbound.channel].size > 0) {
173
+ if (!store.isAllowed(inbound.channel, peerKey)) {
174
+ await sendText(inbound.channel, inbound.peerId, "Access denied.");
175
+ return;
176
+ }
177
+ }
178
+ const session = store.getSession(inbound.channel, peerKey);
179
+ const sessionID = session?.session_id ?? (await createSession({ ...inbound, peerId: peerKey }));
180
+ enqueue(sessionID, async () => {
181
+ const runState = {
182
+ sessionID,
183
+ channel: inbound.channel,
184
+ peerId: inbound.peerId,
185
+ toolUpdatesEnabled: config.toolUpdatesEnabled,
186
+ seenToolStates: new Map(),
187
+ };
188
+ activeRuns.set(sessionID, runState);
189
+ try {
190
+ const response = await client.session.prompt({
191
+ sessionID,
192
+ parts: [{ type: "text", text: inbound.text }],
193
+ });
194
+ const parts = response.parts ?? [];
195
+ const reply = parts
196
+ .filter((part) => part.type === "text" && !part.ignored)
197
+ .map((part) => part.text ?? "")
198
+ .join("\n")
199
+ .trim();
200
+ if (reply) {
201
+ await sendText(inbound.channel, inbound.peerId, reply);
202
+ }
203
+ else {
204
+ await sendText(inbound.channel, inbound.peerId, "No response generated. Try again.");
205
+ }
206
+ }
207
+ catch (error) {
208
+ logger.error({ error }, "prompt failed");
209
+ await sendText(inbound.channel, inbound.peerId, "Error: failed to reach OpenCode.");
210
+ }
211
+ finally {
212
+ activeRuns.delete(sessionID);
213
+ }
214
+ });
215
+ }
216
+ async function createSession(message) {
217
+ const title = `owpenbot ${message.channel} ${message.peerId}`;
218
+ const session = await client.session.create({
219
+ title,
220
+ permission: buildPermissionRules(config.permissionMode),
221
+ });
222
+ const sessionID = session.id;
223
+ if (!sessionID)
224
+ throw new Error("Failed to create session");
225
+ store.upsertSession(message.channel, message.peerId, sessionID);
226
+ logger.info({ sessionID, channel: message.channel, peerId: message.peerId }, "session created");
227
+ return sessionID;
228
+ }
229
+ function enqueue(sessionID, task) {
230
+ const previous = sessionQueue.get(sessionID) ?? Promise.resolve();
231
+ const next = previous
232
+ .then(task)
233
+ .catch((error) => {
234
+ logger.error({ error }, "session task failed");
235
+ })
236
+ .finally(() => {
237
+ if (sessionQueue.get(sessionID) === next) {
238
+ sessionQueue.delete(sessionID);
239
+ }
240
+ });
241
+ sessionQueue.set(sessionID, next);
242
+ }
243
+ for (const adapter of adapters.values()) {
244
+ await adapter.start();
245
+ }
246
+ logger.info({ channels: Array.from(adapters.keys()) }, "bridge started");
247
+ return {
248
+ async stop() {
249
+ eventAbort.abort();
250
+ clearInterval(healthTimer);
251
+ if (stopHealthServer)
252
+ stopHealthServer();
253
+ for (const adapter of adapters.values()) {
254
+ await adapter.stop();
255
+ }
256
+ store.close();
257
+ await delay(50);
258
+ },
259
+ };
260
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,252 @@
1
+ import fs from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { Command } from "commander";
4
+ import { startBridge } from "./bridge.js";
5
+ import { loadConfig, normalizeWhatsAppId, readConfigFile, writeConfigFile, } from "./config.js";
6
+ import { BridgeStore } from "./db.js";
7
+ import { createLogger } from "./logger.js";
8
+ import { loginWhatsApp, unpairWhatsApp } from "./whatsapp.js";
9
+ const program = new Command();
10
+ program
11
+ .name("owpenbot")
12
+ .description("OpenCode WhatsApp + Telegram bridge")
13
+ .argument("[path]");
14
+ const runStart = async (pathOverride) => {
15
+ if (pathOverride?.trim()) {
16
+ process.env.OPENCODE_DIRECTORY = pathOverride.trim();
17
+ }
18
+ const config = loadConfig();
19
+ const logger = createLogger(config.logLevel);
20
+ if (!process.env.OPENCODE_DIRECTORY) {
21
+ process.env.OPENCODE_DIRECTORY = config.opencodeDirectory;
22
+ }
23
+ const bridge = await startBridge(config, logger);
24
+ logger.info("Commands: owpenbot whatsapp login, owpenbot pairing list, owpenbot status");
25
+ const shutdown = async () => {
26
+ logger.info("shutting down");
27
+ await bridge.stop();
28
+ process.exit(0);
29
+ };
30
+ process.on("SIGINT", shutdown);
31
+ process.on("SIGTERM", shutdown);
32
+ };
33
+ program
34
+ .command("start")
35
+ .description("Start the bridge")
36
+ .action(() => runStart());
37
+ program.action((pathArg) => {
38
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
39
+ program.outputHelp();
40
+ return;
41
+ }
42
+ return runStart(pathArg);
43
+ });
44
+ program
45
+ .command("setup")
46
+ .description("Create or update owpenbot.json for WhatsApp")
47
+ .option("--non-interactive", "Write defaults without prompts", false)
48
+ .action(async (opts) => {
49
+ const config = loadConfig(process.env, { requireOpencode: false });
50
+ const { config: existing } = readConfigFile(config.configPath);
51
+ const next = existing ?? { version: 1 };
52
+ if (opts.nonInteractive) {
53
+ next.version = 1;
54
+ next.channels = next.channels ?? {};
55
+ next.channels.whatsapp = {
56
+ dmPolicy: "pairing",
57
+ allowFrom: [],
58
+ selfChatMode: false,
59
+ accounts: {
60
+ [config.whatsappAccountId]: {
61
+ authDir: config.whatsappAuthDir,
62
+ },
63
+ },
64
+ };
65
+ writeConfigFile(config.configPath, next);
66
+ console.log(`Wrote ${config.configPath}`);
67
+ return;
68
+ }
69
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
70
+ const phoneMode = await rl.question("WhatsApp setup: (1) Personal number (2) Dedicated number [1]: ");
71
+ const mode = phoneMode.trim() === "2" ? "dedicated" : "personal";
72
+ let dmPolicy = "pairing";
73
+ let allowFrom = [];
74
+ let selfChatMode = false;
75
+ if (mode === "personal") {
76
+ let normalized = "";
77
+ while (!normalized) {
78
+ const number = await rl.question("Your WhatsApp number (E.164, e.g. +15551234567): ");
79
+ const candidate = normalizeWhatsAppId(number);
80
+ if (!/^\+\d+$/.test(candidate)) {
81
+ console.log("Invalid number. Try again.");
82
+ continue;
83
+ }
84
+ normalized = candidate;
85
+ }
86
+ allowFrom = [normalized];
87
+ dmPolicy = "allowlist";
88
+ selfChatMode = true;
89
+ }
90
+ else {
91
+ const policyInput = await rl.question("DM policy: (1) Pairing (2) Allowlist (3) Open (4) Disabled [1]: ");
92
+ const policyChoice = policyInput.trim();
93
+ if (policyChoice === "2")
94
+ dmPolicy = "allowlist";
95
+ else if (policyChoice === "3")
96
+ dmPolicy = "open";
97
+ else if (policyChoice === "4")
98
+ dmPolicy = "disabled";
99
+ else
100
+ dmPolicy = "pairing";
101
+ const listInput = await rl.question("Allowlist numbers (comma-separated, optional): ");
102
+ if (listInput.trim()) {
103
+ allowFrom = listInput
104
+ .split(",")
105
+ .map((item) => normalizeWhatsAppId(item))
106
+ .filter(Boolean);
107
+ }
108
+ if (dmPolicy === "open") {
109
+ allowFrom = allowFrom.length ? allowFrom : ["*"];
110
+ }
111
+ }
112
+ rl.close();
113
+ next.version = 1;
114
+ next.channels = next.channels ?? {};
115
+ next.channels.whatsapp = {
116
+ dmPolicy,
117
+ allowFrom,
118
+ selfChatMode,
119
+ accounts: {
120
+ [config.whatsappAccountId]: {
121
+ authDir: config.whatsappAuthDir,
122
+ },
123
+ },
124
+ };
125
+ writeConfigFile(config.configPath, next);
126
+ console.log(`Wrote ${config.configPath}`);
127
+ });
128
+ program
129
+ .command("pairing-code")
130
+ .description("List pending pairing codes")
131
+ .action(() => {
132
+ const config = loadConfig(process.env, { requireOpencode: false });
133
+ const store = new BridgeStore(config.dbPath);
134
+ store.prunePairingRequests();
135
+ const requests = store.listPairingRequests("whatsapp");
136
+ if (!requests.length) {
137
+ console.log("No pending pairing requests.");
138
+ }
139
+ else {
140
+ for (const request of requests) {
141
+ console.log(`${request.code} ${request.peer_id}`);
142
+ }
143
+ }
144
+ store.close();
145
+ });
146
+ const whatsapp = program.command("whatsapp").description("WhatsApp helpers");
147
+ whatsapp
148
+ .command("login")
149
+ .description("Login to WhatsApp via QR code")
150
+ .action(async () => {
151
+ const config = loadConfig(process.env, { requireOpencode: false });
152
+ const logger = createLogger(config.logLevel);
153
+ await loginWhatsApp(config, logger);
154
+ });
155
+ whatsapp
156
+ .command("logout")
157
+ .description("Logout of WhatsApp and clear auth state")
158
+ .action(() => {
159
+ const config = loadConfig(process.env, { requireOpencode: false });
160
+ const logger = createLogger(config.logLevel);
161
+ unpairWhatsApp(config, logger);
162
+ });
163
+ program
164
+ .command("qr")
165
+ .description("Print a WhatsApp QR code to pair")
166
+ .action(async () => {
167
+ const config = loadConfig(process.env, { requireOpencode: false });
168
+ const logger = createLogger(config.logLevel);
169
+ await loginWhatsApp(config, logger);
170
+ });
171
+ program
172
+ .command("unpair")
173
+ .description("Clear WhatsApp pairing data")
174
+ .action(() => {
175
+ const config = loadConfig(process.env, { requireOpencode: false });
176
+ const logger = createLogger(config.logLevel);
177
+ unpairWhatsApp(config, logger);
178
+ });
179
+ const pairing = program.command("pairing").description("Pairing requests");
180
+ pairing
181
+ .command("list")
182
+ .description("List pending pairing requests")
183
+ .action(() => {
184
+ const config = loadConfig(process.env, { requireOpencode: false });
185
+ const store = new BridgeStore(config.dbPath);
186
+ store.prunePairingRequests();
187
+ const requests = store.listPairingRequests("whatsapp");
188
+ if (!requests.length) {
189
+ console.log("No pending pairing requests.");
190
+ }
191
+ else {
192
+ for (const request of requests) {
193
+ console.log(`${request.code} ${request.peer_id}`);
194
+ }
195
+ }
196
+ store.close();
197
+ });
198
+ pairing
199
+ .command("approve")
200
+ .argument("<code>")
201
+ .description("Approve a pairing request")
202
+ .action((code) => {
203
+ const config = loadConfig(process.env, { requireOpencode: false });
204
+ const store = new BridgeStore(config.dbPath);
205
+ const request = store.approvePairingRequest("whatsapp", code.trim());
206
+ if (!request) {
207
+ console.log("Pairing code not found or expired.");
208
+ store.close();
209
+ return;
210
+ }
211
+ store.allowPeer("whatsapp", request.peer_id);
212
+ store.close();
213
+ console.log(`Approved ${request.peer_id}`);
214
+ });
215
+ pairing
216
+ .command("deny")
217
+ .argument("<code>")
218
+ .description("Deny a pairing request")
219
+ .action((code) => {
220
+ const config = loadConfig(process.env, { requireOpencode: false });
221
+ const store = new BridgeStore(config.dbPath);
222
+ const ok = store.denyPairingRequest("whatsapp", code.trim());
223
+ store.close();
224
+ console.log(ok ? "Removed pairing request." : "Pairing code not found.");
225
+ });
226
+ program
227
+ .command("status")
228
+ .description("Show WhatsApp and OpenCode status")
229
+ .action(async () => {
230
+ const config = loadConfig(process.env, { requireOpencode: false });
231
+ const authPath = `${config.whatsappAuthDir}/creds.json`;
232
+ const linked = fs.existsSync(authPath);
233
+ console.log(`Config: ${config.configPath}`);
234
+ console.log(`WhatsApp linked: ${linked ? "yes" : "no"}`);
235
+ console.log(`Auth dir: ${config.whatsappAuthDir}`);
236
+ console.log(`OpenCode URL: ${config.opencodeUrl}`);
237
+ });
238
+ program
239
+ .command("doctor")
240
+ .description("Diagnose common issues")
241
+ .action(async () => {
242
+ const config = loadConfig(process.env, { requireOpencode: false });
243
+ const authPath = `${config.whatsappAuthDir}/creds.json`;
244
+ if (!fs.existsSync(authPath)) {
245
+ console.log("WhatsApp not linked. Run: owpenbot whatsapp login");
246
+ }
247
+ else {
248
+ console.log("WhatsApp linked.");
249
+ }
250
+ console.log("If replies fail, ensure OpenCode server is running at OPENCODE_URL.");
251
+ });
252
+ await program.parseAsync(process.argv);