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 +116 -0
- package/dist/cli.js +14 -1
- package/dist/config.js +6 -6
- package/dist/whatsapp.js +10 -0
- package/package.json +1 -1
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
|
+
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()
|
|
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 = {}) {
|