owpenwork 0.1.13 → 0.1.15

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,6 +19,11 @@ const TOOL_LABELS = {
19
19
  task: "agent",
20
20
  webfetch: "webfetch",
21
21
  };
22
+ const CHANNEL_LABELS = {
23
+ whatsapp: "WhatsApp",
24
+ telegram: "Telegram",
25
+ };
26
+ const TYPING_INTERVAL_MS = 6000;
22
27
  export async function startBridge(config, logger, reporter) {
23
28
  const reportStatus = reporter?.onStatus;
24
29
  const client = createClient(config);
@@ -43,6 +48,67 @@ export async function startBridge(config, logger, reporter) {
43
48
  }
44
49
  const sessionQueue = new Map();
45
50
  const activeRuns = new Map();
51
+ const sessionModels = new Map();
52
+ const typingLoops = new Map();
53
+ const formatPeer = (channel, peerId) => channel === "whatsapp" ? normalizeWhatsAppId(peerId) : peerId;
54
+ const formatModelLabel = (model) => model ? `${model.providerID}/${model.modelID}` : null;
55
+ const extractModelRef = (info) => {
56
+ if (!info || typeof info !== "object")
57
+ return null;
58
+ const record = info;
59
+ if (record.role !== "user")
60
+ return null;
61
+ if (!record.model || typeof record.model !== "object")
62
+ return null;
63
+ const model = record.model;
64
+ if (typeof model.providerID !== "string" || typeof model.modelID !== "string")
65
+ return null;
66
+ return { providerID: model.providerID, modelID: model.modelID };
67
+ };
68
+ const reportThinking = (run) => {
69
+ if (!reportStatus)
70
+ return;
71
+ const modelLabel = formatModelLabel(sessionModels.get(run.sessionID));
72
+ const nextLabel = modelLabel ? `Thinking (${modelLabel})` : "Thinking...";
73
+ if (run.thinkingLabel === nextLabel && run.thinkingActive)
74
+ return;
75
+ run.thinkingLabel = nextLabel;
76
+ run.thinkingActive = true;
77
+ reportStatus(`[${CHANNEL_LABELS[run.channel]}] ${formatPeer(run.channel, run.peerId)} ${nextLabel}`);
78
+ };
79
+ const reportDone = (run) => {
80
+ if (!reportStatus || !run.thinkingActive)
81
+ return;
82
+ const modelLabel = formatModelLabel(sessionModels.get(run.sessionID));
83
+ const suffix = modelLabel ? ` (${modelLabel})` : "";
84
+ reportStatus(`[${CHANNEL_LABELS[run.channel]}] ${formatPeer(run.channel, run.peerId)} Done${suffix}`);
85
+ run.thinkingActive = false;
86
+ };
87
+ const startTyping = (run) => {
88
+ const adapter = adapters.get(run.channel);
89
+ if (!adapter?.sendTyping)
90
+ return;
91
+ if (typingLoops.has(run.sessionID))
92
+ return;
93
+ const sendTyping = async () => {
94
+ try {
95
+ await adapter.sendTyping?.(run.peerId);
96
+ }
97
+ catch (error) {
98
+ logger.warn({ error, channel: run.channel }, "typing update failed");
99
+ }
100
+ };
101
+ void sendTyping();
102
+ const timer = setInterval(sendTyping, TYPING_INTERVAL_MS);
103
+ typingLoops.set(run.sessionID, timer);
104
+ };
105
+ const stopTyping = (sessionID) => {
106
+ const timer = typingLoops.get(sessionID);
107
+ if (!timer)
108
+ return;
109
+ clearInterval(timer);
110
+ typingLoops.delete(sessionID);
111
+ };
46
112
  let opencodeHealthy = false;
47
113
  let opencodeVersion;
48
114
  async function refreshHealth() {
@@ -80,6 +146,46 @@ export async function startBridge(config, logger, reporter) {
80
146
  const event = normalizeEvent(raw);
81
147
  if (!event)
82
148
  continue;
149
+ if (event.type === "message.updated") {
150
+ if (event.properties && typeof event.properties === "object") {
151
+ const record = event.properties;
152
+ const info = record.info;
153
+ const sessionID = typeof info?.sessionID === "string" ? info.sessionID : null;
154
+ const model = extractModelRef(info);
155
+ if (sessionID && model) {
156
+ sessionModels.set(sessionID, model);
157
+ const run = activeRuns.get(sessionID);
158
+ if (run)
159
+ reportThinking(run);
160
+ }
161
+ }
162
+ }
163
+ if (event.type === "session.status") {
164
+ if (event.properties && typeof event.properties === "object") {
165
+ const record = event.properties;
166
+ const sessionID = typeof record.sessionID === "string" ? record.sessionID : null;
167
+ const status = record.status;
168
+ if (sessionID && (status?.type === "busy" || status?.type === "retry")) {
169
+ const run = activeRuns.get(sessionID);
170
+ if (run) {
171
+ reportThinking(run);
172
+ startTyping(run);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ if (event.type === "session.idle") {
178
+ if (event.properties && typeof event.properties === "object") {
179
+ const record = event.properties;
180
+ const sessionID = typeof record.sessionID === "string" ? record.sessionID : null;
181
+ if (sessionID) {
182
+ stopTyping(sessionID);
183
+ const run = activeRuns.get(sessionID);
184
+ if (run)
185
+ reportDone(run);
186
+ }
187
+ }
188
+ }
83
189
  if (event.type === "message.part.updated") {
84
190
  const part = event.properties?.part;
85
191
  if (!part?.sessionID)
@@ -201,6 +307,8 @@ export async function startBridge(config, logger, reporter) {
201
307
  seenToolStates: new Map(),
202
308
  };
203
309
  activeRuns.set(sessionID, runState);
310
+ reportThinking(runState);
311
+ startTyping(runState);
204
312
  try {
205
313
  const response = await client.session.prompt({
206
314
  sessionID,
@@ -228,6 +336,8 @@ export async function startBridge(config, logger, reporter) {
228
336
  });
229
337
  }
230
338
  finally {
339
+ stopTyping(sessionID);
340
+ reportDone(runState);
231
341
  activeRuns.delete(sessionID);
232
342
  }
233
343
  });
@@ -243,6 +353,8 @@ export async function startBridge(config, logger, reporter) {
243
353
  throw new Error("Failed to create session");
244
354
  store.upsertSession(message.channel, message.peerId, sessionID);
245
355
  logger.info({ sessionID, channel: message.channel, peerId: message.peerId }, "session created");
356
+ reportStatus?.(`${CHANNEL_LABELS[message.channel]} session created for ${formatPeer(message.channel, message.peerId)} (ID: ${sessionID}).`);
357
+ await sendText(message.channel, message.peerId, "🧭 Session started.", { kind: "system" });
246
358
  return sessionID;
247
359
  }
248
360
  function enqueue(sessionID, task) {
@@ -271,6 +383,10 @@ export async function startBridge(config, logger, reporter) {
271
383
  clearInterval(healthTimer);
272
384
  if (stopHealthServer)
273
385
  stopHealthServer();
386
+ for (const timer of typingLoops.values()) {
387
+ clearInterval(timer);
388
+ }
389
+ typingLoops.clear();
274
390
  for (const adapter of adapters.values()) {
275
391
  await adapter.stop();
276
392
  }
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { createLogger } from "./logger.js";
10
10
  import { createClient } from "./opencode.js";
11
11
  import { truncateText } from "./text.js";
12
12
  import { loginWhatsApp, unpairWhatsApp } from "./whatsapp.js";
13
- const VERSION = "0.1.13";
13
+ const VERSION = "0.1.15";
14
14
  const STEP_OPTIONS = [
15
15
  {
16
16
  value: "config",
@@ -182,6 +182,19 @@ async function runSetupWizard(config, options) {
182
182
  allowFrom = allowFrom.length ? allowFrom : ["*"];
183
183
  }
184
184
  }
185
+ const currentWorkspace = config.configFile.opencodeDirectory ?? config.opencodeDirectory;
186
+ const keepDefault = unwrap(await confirm({
187
+ message: `Use this OpenCode workspace? (${currentWorkspace})`,
188
+ initialValue: true,
189
+ }));
190
+ if (!keepDefault) {
191
+ const workspace = unwrap(await text({
192
+ message: "OpenCode workspace path",
193
+ placeholder: "/path/to/workspace",
194
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Path required"),
195
+ }));
196
+ next.opencodeDirectory = workspace.trim();
197
+ }
185
198
  next.channels = next.channels ?? {};
186
199
  next.channels.whatsapp = {
187
200
  dmPolicy,
package/dist/config.js CHANGED
@@ -118,16 +118,16 @@ function parseAllowlist(env) {
118
118
  }
119
119
  export function loadConfig(env = process.env, options = {}) {
120
120
  const requireOpencode = options.requireOpencode ?? false;
121
- const opencodeDirectory = env.OPENCODE_DIRECTORY?.trim() ?? "";
122
- if (!opencodeDirectory && requireOpencode) {
123
- throw new Error("OPENCODE_DIRECTORY is required");
124
- }
125
- const resolvedDirectory = opencodeDirectory || process.cwd();
126
121
  const dataDir = expandHome(env.OWPENBOT_DATA_DIR ?? "~/.owpenbot");
127
122
  const dbPath = expandHome(env.OWPENBOT_DB_PATH ?? path.join(dataDir, "owpenbot.db"));
128
123
  const logFile = expandHome(env.OWPENBOT_LOG_FILE ?? path.join(dataDir, "logs", "owpenbot.log"));
129
124
  const configPath = resolveConfigPath(dataDir, env);
130
125
  const { config: configFile } = readConfigFile(configPath);
126
+ const opencodeDirectory = env.OPENCODE_DIRECTORY?.trim() || configFile.opencodeDirectory || "";
127
+ if (!opencodeDirectory && requireOpencode) {
128
+ throw new Error("OPENCODE_DIRECTORY is required");
129
+ }
130
+ const resolvedDirectory = opencodeDirectory || process.cwd();
131
131
  const whatsappFile = configFile.channels?.whatsapp ?? {};
132
132
  const whatsappAccountId = env.WHATSAPP_ACCOUNT_ID?.trim() || "default";
133
133
  const accountAuthDir = whatsappFile.accounts?.[whatsappAccountId]?.authDir;
@@ -146,7 +146,7 @@ export function loadConfig(env = process.env, options = {}) {
146
146
  return {
147
147
  configPath,
148
148
  configFile,
149
- opencodeUrl: env.OPENCODE_URL?.trim() ?? "http://127.0.0.1:4096",
149
+ opencodeUrl: env.OPENCODE_URL?.trim() || configFile.opencodeUrl || "http://127.0.0.1:4096",
150
150
  opencodeDirectory: resolvedDirectory,
151
151
  opencodeUsername: env.OPENCODE_SERVER_USERNAME?.trim() || undefined,
152
152
  opencodePassword: env.OPENCODE_SERVER_PASSWORD?.trim() || undefined,
package/dist/whatsapp.js CHANGED
@@ -171,6 +171,16 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
171
171
  const sent = await socket.sendMessage(peerId, { text });
172
172
  recordSentMessage(sent?.key?.id);
173
173
  },
174
+ async sendTyping(peerId) {
175
+ if (!socket)
176
+ return;
177
+ try {
178
+ await socket.sendPresenceUpdate("composing", peerId);
179
+ }
180
+ catch (error) {
181
+ log.warn({ error, peerId }, "whatsapp typing update failed");
182
+ }
183
+ },
174
184
  };
175
185
  }
176
186
  export async function loginWhatsApp(config, logger, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owpenwork",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "WhatsApp bridge for a running OpenCode server",
5
5
  "private": false,
6
6
  "type": "module",