owpenwork 0.1.11 → 0.1.12

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/dist/bridge.js CHANGED
@@ -19,7 +19,8 @@ const TOOL_LABELS = {
19
19
  task: "agent",
20
20
  webfetch: "webfetch",
21
21
  };
22
- export async function startBridge(config, logger) {
22
+ export async function startBridge(config, logger, reporter) {
23
+ const reportStatus = reporter?.onStatus;
23
24
  const client = createClient(config);
24
25
  const store = new BridgeStore(config.dbPath);
25
26
  store.seedAllowlist("telegram", config.allowlist.telegram);
@@ -31,12 +32,14 @@ export async function startBridge(config, logger) {
31
32
  }
32
33
  else {
33
34
  logger.info("telegram adapter disabled");
35
+ reportStatus?.("Telegram adapter disabled.");
34
36
  }
35
37
  if (config.whatsappEnabled) {
36
- adapters.set("whatsapp", createWhatsAppAdapter(config, logger, handleInbound, { printQr: true }));
38
+ adapters.set("whatsapp", createWhatsAppAdapter(config, logger, handleInbound, { printQr: true, onStatus: reportStatus }));
37
39
  }
38
40
  else {
39
41
  logger.info("whatsapp adapter disabled");
42
+ reportStatus?.("WhatsApp adapter disabled.");
40
43
  }
41
44
  const sessionQueue = new Map();
42
45
  const activeRuns = new Map();
@@ -102,7 +105,7 @@ export async function startBridge(config, logger) {
102
105
  if (output)
103
106
  message += `\n${output}`;
104
107
  }
105
- await sendText(run.channel, run.peerId, message);
108
+ await sendText(run.channel, run.peerId, message, { kind: "tool" });
106
109
  }
107
110
  if (event.type === "permission.asked") {
108
111
  const permission = event.properties;
@@ -117,7 +120,9 @@ export async function startBridge(config, logger) {
117
120
  if (response === "reject") {
118
121
  const run = activeRuns.get(permission.sessionID);
119
122
  if (run) {
120
- await sendText(run.channel, run.peerId, "Permission denied. Update configuration to allow tools.");
123
+ await sendText(run.channel, run.peerId, "Permission denied. Update configuration to allow tools.", {
124
+ kind: "system",
125
+ });
121
126
  }
122
127
  }
123
128
  }
@@ -125,10 +130,14 @@ export async function startBridge(config, logger) {
125
130
  })().catch((error) => {
126
131
  logger.error({ error }, "event stream closed");
127
132
  });
128
- async function sendText(channel, peerId, text) {
133
+ async function sendText(channel, peerId, text, options = {}) {
129
134
  const adapter = adapters.get(channel);
130
135
  if (!adapter)
131
136
  return;
137
+ const kind = options.kind ?? "system";
138
+ if (options.display !== false) {
139
+ reporter?.onOutbound?.({ channel, peerId, text, kind });
140
+ }
132
141
  const chunks = chunkText(text, adapter.maxTextLength);
133
142
  for (const chunk of chunks) {
134
143
  logger.info({ channel, peerId, length: chunk.length }, "sending message");
@@ -151,30 +160,36 @@ export async function startBridge(config, logger) {
151
160
  const allowed = allowAll || isSelf || store.isAllowed("whatsapp", peerKey);
152
161
  if (!allowed) {
153
162
  if (config.whatsappDmPolicy === "allowlist") {
154
- await sendText(inbound.channel, inbound.peerId, "Access denied. Ask the owner to allowlist your number.");
163
+ await sendText(inbound.channel, inbound.peerId, "Access denied. Ask the owner to allowlist your number.", { kind: "system" });
155
164
  return;
156
165
  }
157
166
  store.prunePairingRequests();
158
167
  const active = store.getPairingRequest("whatsapp", peerKey);
159
168
  const pending = store.listPairingRequests("whatsapp");
160
169
  if (!active && pending.length >= 3) {
161
- await sendText(inbound.channel, inbound.peerId, "Pairing queue full. Ask the owner to approve pending requests.");
170
+ await sendText(inbound.channel, inbound.peerId, "Pairing queue full. Ask the owner to approve pending requests.", { kind: "system" });
162
171
  return;
163
172
  }
164
173
  const code = active?.code ?? String(Math.floor(100000 + Math.random() * 900000));
165
174
  if (!active) {
166
175
  store.createPairingRequest("whatsapp", peerKey, code, 60 * 60_000);
167
176
  }
168
- await sendText(inbound.channel, inbound.peerId, `Pairing required. Ask the owner to approve code: ${code}`);
177
+ await sendText(inbound.channel, inbound.peerId, `Pairing required. Ask the owner to approve code: ${code}`, { kind: "system" });
169
178
  return;
170
179
  }
171
180
  }
172
181
  else if (config.allowlist[inbound.channel].size > 0) {
173
182
  if (!store.isAllowed(inbound.channel, peerKey)) {
174
- await sendText(inbound.channel, inbound.peerId, "Access denied.");
183
+ await sendText(inbound.channel, inbound.peerId, "Access denied.", { kind: "system" });
175
184
  return;
176
185
  }
177
186
  }
187
+ reporter?.onInbound?.({
188
+ channel: inbound.channel,
189
+ peerId: inbound.peerId,
190
+ text: inbound.text,
191
+ fromMe: inbound.fromMe,
192
+ });
178
193
  const session = store.getSession(inbound.channel, peerKey);
179
194
  const sessionID = session?.session_id ?? (await createSession({ ...inbound, peerId: peerKey }));
180
195
  enqueue(sessionID, async () => {
@@ -198,15 +213,19 @@ export async function startBridge(config, logger) {
198
213
  .join("\n")
199
214
  .trim();
200
215
  if (reply) {
201
- await sendText(inbound.channel, inbound.peerId, reply);
216
+ await sendText(inbound.channel, inbound.peerId, reply, { kind: "reply" });
202
217
  }
203
218
  else {
204
- await sendText(inbound.channel, inbound.peerId, "No response generated. Try again.");
219
+ await sendText(inbound.channel, inbound.peerId, "No response generated. Try again.", {
220
+ kind: "system",
221
+ });
205
222
  }
206
223
  }
207
224
  catch (error) {
208
225
  logger.error({ error }, "prompt failed");
209
- await sendText(inbound.channel, inbound.peerId, "Error: failed to reach OpenCode.");
226
+ await sendText(inbound.channel, inbound.peerId, "Error: failed to reach OpenCode.", {
227
+ kind: "system",
228
+ });
210
229
  }
211
230
  finally {
212
231
  activeRuns.delete(sessionID);
@@ -242,8 +261,10 @@ export async function startBridge(config, logger) {
242
261
  }
243
262
  for (const adapter of adapters.values()) {
244
263
  await adapter.start();
264
+ reportStatus?.(`${adapter.name === "whatsapp" ? "WhatsApp" : "Telegram"} adapter started.`);
245
265
  }
246
266
  logger.info({ channels: Array.from(adapters.keys()) }, "bridge started");
267
+ reportStatus?.(`Bridge running. Logs: ${config.logFile}`);
247
268
  return {
248
269
  async stop() {
249
270
  eventAbort.abort();
package/dist/cli.js CHANGED
@@ -8,8 +8,9 @@ import { loadConfig, normalizeWhatsAppId, readConfigFile, writeConfigFile, } fro
8
8
  import { BridgeStore } from "./db.js";
9
9
  import { createLogger } from "./logger.js";
10
10
  import { createClient } from "./opencode.js";
11
+ import { truncateText } from "./text.js";
11
12
  import { loginWhatsApp, unpairWhatsApp } from "./whatsapp.js";
12
- const VERSION = "0.1.11";
13
+ const VERSION = "0.1.12";
13
14
  const STEP_OPTIONS = [
14
15
  {
15
16
  value: "config",
@@ -62,6 +63,38 @@ function defaultSelections(configExists, whatsappLinked) {
62
63
  selections.push("start");
63
64
  return selections;
64
65
  }
66
+ function createAppLogger(config) {
67
+ return createLogger(config.logLevel, { logFile: config.logFile });
68
+ }
69
+ function createConsoleReporter() {
70
+ const formatChannel = (channel) => (channel === "whatsapp" ? "WhatsApp" : "Telegram");
71
+ const formatPeer = (channel, peerId, fromMe) => {
72
+ const base = channel === "whatsapp" ? normalizeWhatsAppId(peerId) : peerId;
73
+ return fromMe ? `${base} (me)` : base;
74
+ };
75
+ const printBlock = (prefix, text) => {
76
+ const lines = text.split(/\r?\n/).map((line) => truncateText(line.trim(), 240));
77
+ const [first, ...rest] = lines.length ? lines : ["(empty)"];
78
+ console.log(`${prefix} ${first}`);
79
+ for (const line of rest) {
80
+ console.log(`${" ".repeat(prefix.length)} ${line}`);
81
+ }
82
+ };
83
+ return {
84
+ onStatus(message) {
85
+ console.log(message);
86
+ },
87
+ onInbound({ channel, peerId, text, fromMe }) {
88
+ const prefix = `[${formatChannel(channel)}] ${formatPeer(channel, peerId, fromMe)} >`;
89
+ printBlock(prefix, text);
90
+ },
91
+ onOutbound({ channel, peerId, text, kind }) {
92
+ const marker = kind === "reply" ? "<" : kind === "tool" ? "*" : "!";
93
+ const prefix = `[${formatChannel(channel)}] ${formatPeer(channel, peerId)} ${marker}`;
94
+ printBlock(prefix, text);
95
+ },
96
+ };
97
+ }
65
98
  function updateConfig(configPath, updater) {
66
99
  const { config } = readConfigFile(configPath);
67
100
  const base = config ?? { version: 1 };
@@ -195,12 +228,13 @@ async function runStart(pathOverride) {
195
228
  process.env.OPENCODE_DIRECTORY = pathOverride.trim();
196
229
  }
197
230
  const config = loadConfig();
198
- const logger = createLogger(config.logLevel);
231
+ const logger = createAppLogger(config);
232
+ const reporter = createConsoleReporter();
199
233
  if (!process.env.OPENCODE_DIRECTORY) {
200
234
  process.env.OPENCODE_DIRECTORY = config.opencodeDirectory;
201
235
  }
202
- const bridge = await startBridge(config, logger);
203
- logger.info("Commands: owpenwork whatsapp login, owpenwork pairing list, owpenwork status");
236
+ const bridge = await startBridge(config, logger, reporter);
237
+ reporter.onStatus?.("Commands: owpenwork whatsapp login, owpenwork pairing list, owpenwork status");
204
238
  const shutdown = async () => {
205
239
  logger.info("shutting down");
206
240
  await bridge.stop();
@@ -277,7 +311,7 @@ async function runGuidedFlow(pathArg, opts) {
277
311
  if (!relink)
278
312
  return;
279
313
  }
280
- await loginWhatsApp(config, createLogger(config.logLevel));
314
+ await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
281
315
  });
282
316
  continue;
283
317
  }
@@ -338,7 +372,7 @@ login
338
372
  .description("Login to WhatsApp via QR code")
339
373
  .action(async () => {
340
374
  const config = loadConfig(process.env, { requireOpencode: false });
341
- await loginWhatsApp(config, createLogger(config.logLevel));
375
+ await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
342
376
  });
343
377
  login
344
378
  .command("telegram")
@@ -372,28 +406,28 @@ whatsapp
372
406
  .description("Login to WhatsApp via QR code")
373
407
  .action(async () => {
374
408
  const config = loadConfig(process.env, { requireOpencode: false });
375
- await loginWhatsApp(config, createLogger(config.logLevel));
409
+ await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
376
410
  });
377
411
  whatsapp
378
412
  .command("logout")
379
413
  .description("Logout of WhatsApp and clear auth state")
380
414
  .action(() => {
381
415
  const config = loadConfig(process.env, { requireOpencode: false });
382
- unpairWhatsApp(config, createLogger(config.logLevel));
416
+ unpairWhatsApp(config, createAppLogger(config));
383
417
  });
384
418
  program
385
419
  .command("qr")
386
420
  .description("Print a WhatsApp QR code to pair")
387
421
  .action(async () => {
388
422
  const config = loadConfig(process.env, { requireOpencode: false });
389
- await loginWhatsApp(config, createLogger(config.logLevel));
423
+ await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
390
424
  });
391
425
  program
392
426
  .command("unpair")
393
427
  .description("Clear WhatsApp pairing data")
394
428
  .action(() => {
395
429
  const config = loadConfig(process.env, { requireOpencode: false });
396
- unpairWhatsApp(config, createLogger(config.logLevel));
430
+ unpairWhatsApp(config, createAppLogger(config));
397
431
  });
398
432
  const pairing = program.command("pairing").description("Pairing requests");
399
433
  pairing
package/dist/config.js CHANGED
@@ -125,6 +125,7 @@ export function loadConfig(env = process.env, options = {}) {
125
125
  const resolvedDirectory = opencodeDirectory || process.cwd();
126
126
  const dataDir = expandHome(env.OWPENBOT_DATA_DIR ?? "~/.owpenbot");
127
127
  const dbPath = expandHome(env.OWPENBOT_DB_PATH ?? path.join(dataDir, "owpenbot.db"));
128
+ const logFile = expandHome(env.OWPENBOT_LOG_FILE ?? path.join(dataDir, "logs", "owpenbot.log"));
128
129
  const configPath = resolveConfigPath(dataDir, env);
129
130
  const { config: configFile } = readConfigFile(configPath);
130
131
  const whatsappFile = configFile.channels?.whatsapp ?? {};
@@ -159,6 +160,7 @@ export function loadConfig(env = process.env, options = {}) {
159
160
  whatsappEnabled: parseBoolean(env.WHATSAPP_ENABLED, true),
160
161
  dataDir,
161
162
  dbPath,
163
+ logFile,
162
164
  allowlist: envAllowlist,
163
165
  toolUpdatesEnabled: parseBoolean(env.TOOL_UPDATES_ENABLED, false),
164
166
  groupsEnabled: parseBoolean(env.GROUPS_ENABLED, false),
package/dist/logger.js CHANGED
@@ -1,5 +1,12 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import pino from "pino";
2
- export function createLogger(level) {
4
+ export function createLogger(level, options) {
5
+ if (options?.logFile) {
6
+ fs.mkdirSync(path.dirname(options.logFile), { recursive: true });
7
+ const destination = pino.destination({ dest: options.logFile, sync: false });
8
+ return pino({ level, base: undefined }, destination);
9
+ }
3
10
  return pino({
4
11
  level,
5
12
  base: undefined,
package/dist/whatsapp.js CHANGED
@@ -43,15 +43,18 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
43
43
  if (update.qr && opts.printQr) {
44
44
  qrcode.generate(update.qr, { small: true });
45
45
  log.info("scan the QR code to connect WhatsApp");
46
+ opts.onStatus?.("Scan the QR code to connect WhatsApp.");
46
47
  }
47
48
  if (update.connection === "open") {
48
49
  log.info("whatsapp connected");
50
+ opts.onStatus?.("WhatsApp connected.");
49
51
  }
50
52
  if (update.connection === "close") {
51
53
  const lastDisconnect = update.lastDisconnect;
52
54
  const statusCode = lastDisconnect?.error?.output?.statusCode;
53
55
  if (statusCode === 515 && !stopped) {
54
56
  log.warn("whatsapp stream error; restarting connection");
57
+ opts.onStatus?.("WhatsApp stream error; reconnecting.");
55
58
  setTimeout(() => {
56
59
  void connect();
57
60
  }, 1000);
@@ -60,10 +63,12 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
60
63
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
61
64
  if (shouldReconnect && !stopped) {
62
65
  log.warn("whatsapp connection closed, reconnecting");
66
+ opts.onStatus?.("WhatsApp connection closed; reconnecting.");
63
67
  void connect();
64
68
  }
65
69
  else if (!shouldReconnect) {
66
70
  log.warn("whatsapp logged out, run 'owpenbot whatsapp login'");
71
+ opts.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
67
72
  }
68
73
  }
69
74
  });
@@ -114,7 +119,7 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
114
119
  },
115
120
  };
116
121
  }
117
- export async function loginWhatsApp(config, logger) {
122
+ export async function loginWhatsApp(config, logger, options = {}) {
118
123
  const authDir = path.resolve(config.whatsappAuthDir);
119
124
  ensureDir(authDir);
120
125
  const log = logger.child({ channel: "whatsapp" });
@@ -155,8 +160,10 @@ export async function loginWhatsApp(config, logger) {
155
160
  if (update.qr) {
156
161
  qrcode.generate(update.qr, { small: true });
157
162
  log.info("scan the QR code to connect WhatsApp");
163
+ options.onStatus?.("Scan the QR code to connect WhatsApp.");
158
164
  }
159
165
  if (update.connection === "open") {
166
+ options.onStatus?.("WhatsApp linked.");
160
167
  finish("connection.open", "linked");
161
168
  }
162
169
  if (update.connection === "close") {
@@ -165,15 +172,18 @@ export async function loginWhatsApp(config, logger) {
165
172
  if (statusCode === 515) {
166
173
  if (state.creds?.registered || fs.existsSync(credsPath)) {
167
174
  log.info("whatsapp login requires reconnect; completing login");
175
+ options.onStatus?.("WhatsApp login requires reconnect; completing login.");
168
176
  finish("connection.restart.required", "linked");
169
177
  }
170
178
  else {
171
179
  log.warn("whatsapp restart requested before creds registered");
180
+ options.onStatus?.("WhatsApp login needs another scan.");
172
181
  finish("connection.restart.required", "restart");
173
182
  }
174
183
  return;
175
184
  }
176
185
  if (state.creds?.registered) {
186
+ options.onStatus?.("WhatsApp linked.");
177
187
  finish("connection.close.registered", "linked");
178
188
  }
179
189
  }
@@ -184,6 +194,7 @@ export async function loginWhatsApp(config, logger) {
184
194
  }
185
195
  if (attempt < maxAttempts) {
186
196
  log.warn("retrying whatsapp login after restart");
197
+ options.onStatus?.("Retrying WhatsApp login...");
187
198
  await new Promise((resolve) => setTimeout(resolve, 1500));
188
199
  }
189
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owpenwork",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "WhatsApp bridge for a running OpenCode server",
5
5
  "private": false,
6
6
  "type": "module",