opencode-claw 0.1.0
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/LICENSE +21 -0
- package/README.md +316 -0
- package/dist/channels/router.d.ts +18 -0
- package/dist/channels/router.js +188 -0
- package/dist/channels/slack.d.ts +4 -0
- package/dist/channels/slack.js +87 -0
- package/dist/channels/telegram.d.ts +4 -0
- package/dist/channels/telegram.js +79 -0
- package/dist/channels/types.d.ts +27 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp.d.ts +4 -0
- package/dist/channels/whatsapp.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/compat.d.ts +12 -0
- package/dist/compat.js +63 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +57 -0
- package/dist/config/schema.d.ts +482 -0
- package/dist/config/schema.js +108 -0
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.js +1 -0
- package/dist/cron/scheduler.d.ts +16 -0
- package/dist/cron/scheduler.js +113 -0
- package/dist/exports.d.ts +9 -0
- package/dist/exports.js +4 -0
- package/dist/health/server.d.ts +17 -0
- package/dist/health/server.js +68 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +127 -0
- package/dist/memory/factory.d.ts +3 -0
- package/dist/memory/factory.js +14 -0
- package/dist/memory/openviking.d.ts +5 -0
- package/dist/memory/openviking.js +138 -0
- package/dist/memory/plugin-entry.d.ts +3 -0
- package/dist/memory/plugin-entry.js +11 -0
- package/dist/memory/plugin.d.ts +5 -0
- package/dist/memory/plugin.js +63 -0
- package/dist/memory/txt.d.ts +2 -0
- package/dist/memory/txt.js +137 -0
- package/dist/memory/types.d.ts +36 -0
- package/dist/memory/types.js +1 -0
- package/dist/outbox/drainer.d.ts +8 -0
- package/dist/outbox/drainer.js +140 -0
- package/dist/outbox/writer.d.ts +15 -0
- package/dist/outbox/writer.js +29 -0
- package/dist/sessions/manager.d.ts +20 -0
- package/dist/sessions/manager.js +68 -0
- package/dist/sessions/persistence.d.ts +4 -0
- package/dist/sessions/persistence.js +24 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/reconnect.d.ts +16 -0
- package/dist/utils/reconnect.js +54 -0
- package/dist/utils/shutdown.d.ts +5 -0
- package/dist/utils/shutdown.js +27 -0
- package/opencode-claw.example.json +71 -0
- package/package.json +77 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Bot } from "grammy";
|
|
2
|
+
import { createReconnector } from "../utils/reconnect.js";
|
|
3
|
+
export function createTelegramAdapter(config, logger) {
|
|
4
|
+
const bot = new Bot(config.botToken);
|
|
5
|
+
let state = "disconnected";
|
|
6
|
+
let handler;
|
|
7
|
+
const reconnector = createReconnector({
|
|
8
|
+
name: "telegram",
|
|
9
|
+
logger,
|
|
10
|
+
connect: async () => {
|
|
11
|
+
state = "connecting";
|
|
12
|
+
bot.start({
|
|
13
|
+
onStart: () => {
|
|
14
|
+
state = "connected";
|
|
15
|
+
reconnector.reset();
|
|
16
|
+
logger.info("telegram: bot connected");
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
bot.on("message:text", async (ctx) => {
|
|
22
|
+
if (!handler)
|
|
23
|
+
return;
|
|
24
|
+
if (!ctx.from)
|
|
25
|
+
return;
|
|
26
|
+
const peerId = String(ctx.from.id);
|
|
27
|
+
if (config.allowlist.length > 0 && !config.allowlist.includes(peerId)) {
|
|
28
|
+
if (config.rejectionBehavior === "reject") {
|
|
29
|
+
await ctx.reply("This assistant is private.");
|
|
30
|
+
}
|
|
31
|
+
logger.debug("telegram: message dropped (not in allowlist)", { peerId });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const msg = {
|
|
35
|
+
channel: "telegram",
|
|
36
|
+
peerId,
|
|
37
|
+
peerName: ctx.from.first_name,
|
|
38
|
+
groupId: ctx.chat.type !== "private" ? String(ctx.chat.id) : undefined,
|
|
39
|
+
threadId: ctx.message.message_thread_id ? String(ctx.message.message_thread_id) : undefined,
|
|
40
|
+
text: ctx.message.text ?? "",
|
|
41
|
+
raw: ctx.message,
|
|
42
|
+
};
|
|
43
|
+
await handler(msg);
|
|
44
|
+
});
|
|
45
|
+
bot.catch((err) => {
|
|
46
|
+
logger.error("telegram: bot error", { error: err.message });
|
|
47
|
+
state = "error";
|
|
48
|
+
reconnector.attempt();
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
id: "telegram",
|
|
52
|
+
name: "Telegram",
|
|
53
|
+
async start(h) {
|
|
54
|
+
handler = h;
|
|
55
|
+
state = "connecting";
|
|
56
|
+
logger.info("telegram: starting bot (polling mode)");
|
|
57
|
+
bot.start({
|
|
58
|
+
onStart: () => {
|
|
59
|
+
state = "connected";
|
|
60
|
+
logger.info("telegram: bot connected");
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
async stop() {
|
|
65
|
+
reconnector.stop();
|
|
66
|
+
state = "disconnected";
|
|
67
|
+
await bot.stop();
|
|
68
|
+
logger.info("telegram: bot stopped");
|
|
69
|
+
},
|
|
70
|
+
async send(peerId, message) {
|
|
71
|
+
await bot.api.sendMessage(Number(peerId), message.text, {
|
|
72
|
+
reply_parameters: message.replyToId ? { message_id: Number(message.replyToId) } : undefined,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
status() {
|
|
76
|
+
return state;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ChannelId = "slack" | "telegram" | "whatsapp";
|
|
2
|
+
export type ChannelStatus = "connected" | "disconnected" | "connecting" | "error";
|
|
3
|
+
export type InboundMessage = {
|
|
4
|
+
channel: ChannelId;
|
|
5
|
+
peerId: string;
|
|
6
|
+
peerName?: string;
|
|
7
|
+
groupId?: string;
|
|
8
|
+
threadId?: string;
|
|
9
|
+
text: string;
|
|
10
|
+
mediaUrl?: string;
|
|
11
|
+
replyToId?: string;
|
|
12
|
+
raw: unknown;
|
|
13
|
+
};
|
|
14
|
+
export type OutboundMessage = {
|
|
15
|
+
text: string;
|
|
16
|
+
threadId?: string;
|
|
17
|
+
replyToId?: string;
|
|
18
|
+
};
|
|
19
|
+
export type InboundMessageHandler = (msg: InboundMessage) => Promise<void>;
|
|
20
|
+
export type ChannelAdapter = {
|
|
21
|
+
readonly id: ChannelId;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
start(handler: InboundMessageHandler): Promise<void>;
|
|
24
|
+
stop(): Promise<void>;
|
|
25
|
+
send(peerId: string, message: OutboundMessage): Promise<void>;
|
|
26
|
+
status(): ChannelStatus;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { DisconnectReason, makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys";
|
|
2
|
+
export function createWhatsAppAdapter(config, logger) {
|
|
3
|
+
let state = "disconnected";
|
|
4
|
+
let handler;
|
|
5
|
+
let sock;
|
|
6
|
+
// Debounce map: peerId -> pending messages
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
function extractPeerId(jid) {
|
|
9
|
+
if (!jid)
|
|
10
|
+
return "";
|
|
11
|
+
return jid.split("@")[0] ?? "";
|
|
12
|
+
}
|
|
13
|
+
function flush(peerId) {
|
|
14
|
+
const item = pending.get(peerId);
|
|
15
|
+
if (!item || !handler)
|
|
16
|
+
return;
|
|
17
|
+
pending.delete(peerId);
|
|
18
|
+
const combined = item.texts.join("\n");
|
|
19
|
+
if (!combined.trim())
|
|
20
|
+
return;
|
|
21
|
+
const msg = {
|
|
22
|
+
channel: "whatsapp",
|
|
23
|
+
peerId: item.peerId,
|
|
24
|
+
text: combined,
|
|
25
|
+
raw: {},
|
|
26
|
+
};
|
|
27
|
+
handler(msg).catch((err) => {
|
|
28
|
+
logger.error("whatsapp: handler error", {
|
|
29
|
+
peerId,
|
|
30
|
+
error: err instanceof Error ? err.message : String(err),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function debounce(peerId, text) {
|
|
35
|
+
const existing = pending.get(peerId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
clearTimeout(existing.timer);
|
|
38
|
+
existing.texts.push(text);
|
|
39
|
+
existing.timer = setTimeout(() => flush(peerId), config.debounceMs);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const timer = setTimeout(() => flush(peerId), config.debounceMs);
|
|
43
|
+
pending.set(peerId, { peerId, texts: [text], timer });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function connect() {
|
|
47
|
+
const { state: authState, saveCreds } = await useMultiFileAuthState(config.authDir);
|
|
48
|
+
sock = makeWASocket({
|
|
49
|
+
auth: authState,
|
|
50
|
+
printQRInTerminal: true,
|
|
51
|
+
});
|
|
52
|
+
sock.ev.on("creds.update", saveCreds);
|
|
53
|
+
sock.ev.on("connection.update", (update) => {
|
|
54
|
+
const { connection, lastDisconnect } = update;
|
|
55
|
+
if (connection === "close") {
|
|
56
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
57
|
+
if (code !== DisconnectReason.loggedOut) {
|
|
58
|
+
logger.warn("whatsapp: connection closed, reconnecting", {
|
|
59
|
+
code,
|
|
60
|
+
});
|
|
61
|
+
state = "connecting";
|
|
62
|
+
connect();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
logger.error("whatsapp: logged out, not reconnecting");
|
|
66
|
+
state = "disconnected";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (connection === "open") {
|
|
70
|
+
state = "connected";
|
|
71
|
+
logger.info("whatsapp: connected");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
sock.ev.on("messages.upsert", (upsert) => {
|
|
75
|
+
if (upsert.type !== "notify")
|
|
76
|
+
return;
|
|
77
|
+
for (const msg of upsert.messages) {
|
|
78
|
+
if (msg.key.fromMe)
|
|
79
|
+
continue;
|
|
80
|
+
const jid = msg.key.remoteJid;
|
|
81
|
+
const peerId = extractPeerId(jid);
|
|
82
|
+
if (!peerId)
|
|
83
|
+
continue;
|
|
84
|
+
// Allowlist check
|
|
85
|
+
if (config.allowlist.length > 0 && !config.allowlist.includes(peerId)) {
|
|
86
|
+
if (config.rejectionBehavior === "reject" && sock && jid) {
|
|
87
|
+
sock
|
|
88
|
+
.sendMessage(jid, {
|
|
89
|
+
text: "This assistant is private.",
|
|
90
|
+
})
|
|
91
|
+
.catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
logger.debug("whatsapp: message dropped (not in allowlist)", { peerId });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const text = msg.message?.conversation ?? msg.message?.extendedTextMessage?.text ?? "";
|
|
97
|
+
if (!text)
|
|
98
|
+
continue;
|
|
99
|
+
// Debounce rapid messages from same peer
|
|
100
|
+
debounce(peerId, text);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
id: "whatsapp",
|
|
106
|
+
name: "WhatsApp",
|
|
107
|
+
async start(h) {
|
|
108
|
+
handler = h;
|
|
109
|
+
state = "connecting";
|
|
110
|
+
logger.info("whatsapp: connecting...");
|
|
111
|
+
await connect();
|
|
112
|
+
},
|
|
113
|
+
async stop() {
|
|
114
|
+
// Flush all pending debounced messages
|
|
115
|
+
for (const [peerId] of pending) {
|
|
116
|
+
flush(peerId);
|
|
117
|
+
}
|
|
118
|
+
pending.clear();
|
|
119
|
+
if (sock) {
|
|
120
|
+
sock.end(undefined);
|
|
121
|
+
sock = undefined;
|
|
122
|
+
}
|
|
123
|
+
state = "disconnected";
|
|
124
|
+
logger.info("whatsapp: disconnected");
|
|
125
|
+
},
|
|
126
|
+
async send(peerId, message) {
|
|
127
|
+
if (!sock)
|
|
128
|
+
throw new Error("WhatsApp socket not connected");
|
|
129
|
+
const jid = `${peerId}@s.whatsapp.net`;
|
|
130
|
+
await sock.sendMessage(jid, { text: message.text });
|
|
131
|
+
},
|
|
132
|
+
status() {
|
|
133
|
+
return state;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
package/dist/compat.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function readTextFile(path: string): Promise<string>;
|
|
2
|
+
export declare function fileExists(path: string): Promise<boolean>;
|
|
3
|
+
export declare function readJsonFile<T>(path: string): Promise<T>;
|
|
4
|
+
export declare function writeTextFile(path: string, content: string): Promise<void>;
|
|
5
|
+
export declare function createFileWriter(path: string): {
|
|
6
|
+
write(data: string): void;
|
|
7
|
+
flush(): void;
|
|
8
|
+
};
|
|
9
|
+
export declare function createHttpServer(port: number, handler: (req: Request) => Promise<Response>): {
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
};
|
package/dist/compat.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
export async function readTextFile(path) {
|
|
5
|
+
return readFile(path, "utf-8");
|
|
6
|
+
}
|
|
7
|
+
export async function fileExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await stat(path);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function readJsonFile(path) {
|
|
17
|
+
const text = await readFile(path, "utf-8");
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
export async function writeTextFile(path, content) {
|
|
21
|
+
await writeFile(path, content, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
export function createFileWriter(path) {
|
|
24
|
+
const stream = createWriteStream(path, { flags: "a", encoding: "utf-8" });
|
|
25
|
+
return {
|
|
26
|
+
write(data) {
|
|
27
|
+
stream.write(data);
|
|
28
|
+
},
|
|
29
|
+
flush() { },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createHttpServer(port, handler) {
|
|
33
|
+
let srv = null;
|
|
34
|
+
return {
|
|
35
|
+
start() {
|
|
36
|
+
srv = createServer((nodeReq, nodeRes) => {
|
|
37
|
+
const host = nodeReq.headers.host ?? "localhost";
|
|
38
|
+
const url = `http://${host}${nodeReq.url ?? "/"}`;
|
|
39
|
+
const request = new Request(url, {
|
|
40
|
+
method: nodeReq.method ?? "GET",
|
|
41
|
+
headers: nodeReq.headers,
|
|
42
|
+
});
|
|
43
|
+
handler(request)
|
|
44
|
+
.then(async (response) => {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
nodeRes.writeHead(response.status, Object.fromEntries(response.headers));
|
|
47
|
+
nodeRes.end(body);
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
nodeRes.writeHead(500);
|
|
51
|
+
nodeRes.end("Internal Server Error");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
srv.listen(port);
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
if (srv) {
|
|
58
|
+
srv.close();
|
|
59
|
+
srv = null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fileExists, readJsonFile } from "../compat.js";
|
|
2
|
+
import { configSchema } from "./schema.js";
|
|
3
|
+
function expand(value) {
|
|
4
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
5
|
+
const env = process.env[name];
|
|
6
|
+
if (env === undefined)
|
|
7
|
+
throw new Error(`Environment variable "${name}" is not set`);
|
|
8
|
+
return env;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function expandDeep(obj) {
|
|
12
|
+
if (typeof obj === "string")
|
|
13
|
+
return expand(obj);
|
|
14
|
+
if (Array.isArray(obj))
|
|
15
|
+
return obj.map(expandDeep);
|
|
16
|
+
if (obj !== null && typeof obj === "object") {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
19
|
+
result[key] = expandDeep(val);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
const searchPaths = [
|
|
26
|
+
process.env.OPENCODE_CLAW_CONFIG,
|
|
27
|
+
"./opencode-claw.json",
|
|
28
|
+
`${process.env.HOME}/.config/opencode-claw/config.json`,
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
export async function loadConfig() {
|
|
31
|
+
let raw = null;
|
|
32
|
+
let found = "";
|
|
33
|
+
for (const p of searchPaths) {
|
|
34
|
+
if (await fileExists(p)) {
|
|
35
|
+
raw = await readJsonFile(p);
|
|
36
|
+
found = p;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!raw) {
|
|
41
|
+
throw new Error([
|
|
42
|
+
"No config file found. Searched:",
|
|
43
|
+
...searchPaths.map((p) => ` - ${p}`),
|
|
44
|
+
"",
|
|
45
|
+
"Copy opencode-claw.example.json to opencode-claw.json and fill in your values.",
|
|
46
|
+
].join("\n"));
|
|
47
|
+
}
|
|
48
|
+
const expanded = expandDeep(raw);
|
|
49
|
+
const result = configSchema.safeParse(expanded);
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
const errors = result.error.issues
|
|
52
|
+
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
throw new Error(`Config validation failed (${found}):\n${errors}`);
|
|
55
|
+
}
|
|
56
|
+
return result.data;
|
|
57
|
+
}
|