opencode-claw 0.2.1 → 0.2.3
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/channels/router.js +8 -3
- package/dist/channels/slack.js +9 -5
- package/dist/channels/split-message.d.ts +6 -0
- package/dist/channels/split-message.js +45 -0
- package/dist/channels/telegram.js +9 -3
- package/dist/channels/whatsapp.js +5 -1
- package/dist/memory/plugin.js +5 -4
- package/dist/sessions/manager.d.ts +1 -1
- package/dist/sessions/manager.js +9 -12
- package/package.json +1 -1
package/dist/channels/router.js
CHANGED
|
@@ -45,7 +45,6 @@ function peerKey(channel, peerId) {
|
|
|
45
45
|
}
|
|
46
46
|
async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
47
47
|
const key = buildSessionKey(msg.channel, msg.peerId, msg.threadId);
|
|
48
|
-
const prefix = `${msg.channel}:${msg.peerId}`;
|
|
49
48
|
switch (cmd.name) {
|
|
50
49
|
case "new": {
|
|
51
50
|
const id = await deps.sessions.newSession(key, cmd.args || undefined);
|
|
@@ -58,7 +57,7 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
|
58
57
|
return `Switched to session: ${cmd.args}`;
|
|
59
58
|
}
|
|
60
59
|
case "sessions": {
|
|
61
|
-
const list = await deps.sessions.listSessions(
|
|
60
|
+
const list = await deps.sessions.listSessions(key);
|
|
62
61
|
if (list.length === 0)
|
|
63
62
|
return "No sessions found.";
|
|
64
63
|
return list
|
|
@@ -105,6 +104,12 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
}
|
|
107
|
+
/** Turn raw MCP tool names like `websearch_web_search_exa` into `Web Search Exa`. */
|
|
108
|
+
function humanizeToolName(raw) {
|
|
109
|
+
if (raw.includes(" "))
|
|
110
|
+
return raw;
|
|
111
|
+
return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
112
|
+
}
|
|
108
113
|
async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
109
114
|
const adapter = deps.adapters.get(msg.channel);
|
|
110
115
|
if (!adapter) {
|
|
@@ -175,7 +180,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
175
180
|
const progress = progressEnabled
|
|
176
181
|
? {
|
|
177
182
|
onToolRunning: (_tool, title) => adapter.send(msg.peerId, {
|
|
178
|
-
text: `🔧 ${title}...`,
|
|
183
|
+
text: `🔧 ${humanizeToolName(title)}...`,
|
|
179
184
|
replyToId: msg.replyToId,
|
|
180
185
|
}),
|
|
181
186
|
onHeartbeat: async () => {
|
package/dist/channels/slack.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { App } from "@slack/bolt";
|
|
2
2
|
import { createReconnector } from "../utils/reconnect.js";
|
|
3
|
+
import { splitMessage } from "./split-message.js";
|
|
3
4
|
export function createSlackAdapter(config, logger) {
|
|
4
5
|
const app = new App({
|
|
5
6
|
token: config.botToken,
|
|
@@ -74,11 +75,14 @@ export function createSlackAdapter(config, logger) {
|
|
|
74
75
|
logger.info("slack: app stopped");
|
|
75
76
|
},
|
|
76
77
|
async send(peerId, message) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const chunks = splitMessage(message.text, 40000);
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
await app.client.chat.postMessage({
|
|
81
|
+
channel: peerId,
|
|
82
|
+
text: chunk,
|
|
83
|
+
thread_ts: message.threadId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
82
86
|
},
|
|
83
87
|
async sendTyping(_peerId) {
|
|
84
88
|
// Slack has no general bot typing indicator API
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a message into chunks that fit within the given max length.
|
|
3
|
+
* Prefers splitting at double newlines (paragraphs), then single newlines,
|
|
4
|
+
* then spaces, and finally hard-cuts as a last resort.
|
|
5
|
+
*/
|
|
6
|
+
export declare function splitMessage(text: string, maxLength: number): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a message into chunks that fit within the given max length.
|
|
3
|
+
* Prefers splitting at double newlines (paragraphs), then single newlines,
|
|
4
|
+
* then spaces, and finally hard-cuts as a last resort.
|
|
5
|
+
*/
|
|
6
|
+
export function splitMessage(text, maxLength) {
|
|
7
|
+
if (text.length <= maxLength)
|
|
8
|
+
return [text];
|
|
9
|
+
const chunks = [];
|
|
10
|
+
let remaining = text;
|
|
11
|
+
while (remaining.length > 0) {
|
|
12
|
+
if (remaining.length <= maxLength) {
|
|
13
|
+
chunks.push(remaining);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
const slice = remaining.slice(0, maxLength);
|
|
17
|
+
let splitAt = -1;
|
|
18
|
+
// Try splitting at last double newline (paragraph boundary)
|
|
19
|
+
const doubleNewline = slice.lastIndexOf("\n\n");
|
|
20
|
+
if (doubleNewline > maxLength * 0.3) {
|
|
21
|
+
splitAt = doubleNewline;
|
|
22
|
+
}
|
|
23
|
+
// Try splitting at last single newline
|
|
24
|
+
if (splitAt === -1) {
|
|
25
|
+
const singleNewline = slice.lastIndexOf("\n");
|
|
26
|
+
if (singleNewline > maxLength * 0.3) {
|
|
27
|
+
splitAt = singleNewline;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Try splitting at last space
|
|
31
|
+
if (splitAt === -1) {
|
|
32
|
+
const space = slice.lastIndexOf(" ");
|
|
33
|
+
if (space > maxLength * 0.3) {
|
|
34
|
+
splitAt = space;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Hard cut as last resort
|
|
38
|
+
if (splitAt === -1) {
|
|
39
|
+
splitAt = maxLength;
|
|
40
|
+
}
|
|
41
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
42
|
+
remaining = remaining.slice(splitAt).replace(/^[\s]+/, "");
|
|
43
|
+
}
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Bot } from "grammy";
|
|
2
2
|
import { createReconnector } from "../utils/reconnect.js";
|
|
3
|
+
import { splitMessage } from "./split-message.js";
|
|
3
4
|
export function createTelegramAdapter(config, logger) {
|
|
4
5
|
const bot = new Bot(config.botToken);
|
|
5
6
|
let state = "disconnected";
|
|
@@ -68,9 +69,14 @@ export function createTelegramAdapter(config, logger) {
|
|
|
68
69
|
logger.info("telegram: bot stopped");
|
|
69
70
|
},
|
|
70
71
|
async send(peerId, message) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const chunks = splitMessage(message.text, 4096);
|
|
73
|
+
for (const chunk of chunks) {
|
|
74
|
+
await bot.api.sendMessage(Number(peerId), chunk, {
|
|
75
|
+
reply_parameters: message.replyToId
|
|
76
|
+
? { message_id: Number(message.replyToId) }
|
|
77
|
+
: undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
74
80
|
},
|
|
75
81
|
async sendTyping(peerId) {
|
|
76
82
|
await bot.api.sendChatAction(Number(peerId), "typing");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DisconnectReason, makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys";
|
|
2
|
+
import { splitMessage } from "./split-message.js";
|
|
2
3
|
export function createWhatsAppAdapter(config, logger) {
|
|
3
4
|
let state = "disconnected";
|
|
4
5
|
let handler;
|
|
@@ -127,7 +128,10 @@ export function createWhatsAppAdapter(config, logger) {
|
|
|
127
128
|
if (!sock)
|
|
128
129
|
throw new Error("WhatsApp socket not connected");
|
|
129
130
|
const jid = `${peerId}@s.whatsapp.net`;
|
|
130
|
-
|
|
131
|
+
const chunks = splitMessage(message.text, 65536);
|
|
132
|
+
for (const chunk of chunks) {
|
|
133
|
+
await sock.sendMessage(jid, { text: chunk });
|
|
134
|
+
}
|
|
131
135
|
},
|
|
132
136
|
async sendTyping(peerId) {
|
|
133
137
|
if (!sock)
|
package/dist/memory/plugin.js
CHANGED
|
@@ -53,10 +53,11 @@ function createMemoryPlugin(backend) {
|
|
|
53
53
|
limit: 5,
|
|
54
54
|
minRelevance: 0.05,
|
|
55
55
|
});
|
|
56
|
-
if (memories.length
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
if (memories.length > 0) {
|
|
57
|
+
const block = memories.map((m) => `- [${m.category}] ${m.content}`).join("\n");
|
|
58
|
+
output.system.push(`\n\n## Relevant Context from Memory\n${block}`);
|
|
59
|
+
}
|
|
60
|
+
output.system.push(`\n\n## Memory Instructions\nYou have access to persistent long-term memory across sessions via two tools:\n- **memory_search**: Look up stored facts about projects, workflows, and past experiences.\n- **memory_store**: Save important information so it persists across sessions.\n\n**When to search memory:**\n- At the start of any task involving a project — search for its location, build commands, test steps, and relationships to other projects.\n- When you are unsure about a project's structure or conventions.\n\n**When to store memory (do this proactively):**\n- Project facts: absolute path on disk, repo URL, language/stack, key entry points.\n- Dev workflows: how to build, run, lint, and format the project.\n- Test procedures: how to run tests locally, what test framework is used, any setup required.\n- Dependencies and relationships: which projects depend on each other, shared libraries, APIs consumed.\n- Architecture decisions: patterns used, notable design choices, known pitfalls.\n- Use category \`project\` for project-specific facts, \`knowledge\` for workflows and procedures, \`experience\` for lessons learned.\n\nStore facts at the granularity of one clear, self-contained statement per call.`);
|
|
60
61
|
},
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -13,7 +13,7 @@ export declare function createSessionManager(client: OpencodeClient, config: Ses
|
|
|
13
13
|
resolveSession: (key: string, title?: string) => Promise<string>;
|
|
14
14
|
switchSession: (key: string, targetId: string) => Promise<void>;
|
|
15
15
|
newSession: (key: string, title?: string) => Promise<string>;
|
|
16
|
-
listSessions: (
|
|
16
|
+
listSessions: (key: string) => Promise<SessionInfo[]>;
|
|
17
17
|
currentSession: (key: string) => string | undefined;
|
|
18
18
|
persist: () => Promise<void>;
|
|
19
19
|
};
|
package/dist/sessions/manager.js
CHANGED
|
@@ -39,20 +39,17 @@ export function createSessionManager(client, config, map, logger) {
|
|
|
39
39
|
logger.info("sessions: created and switched to new session", { key, id: session.data.id });
|
|
40
40
|
return session.data.id;
|
|
41
41
|
}
|
|
42
|
-
async function listSessions(
|
|
42
|
+
async function listSessions(key) {
|
|
43
43
|
const all = await client.session.list();
|
|
44
44
|
const sessions = all.data ?? [];
|
|
45
|
-
const
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
createdAt: session?.time.created,
|
|
54
|
-
};
|
|
55
|
-
});
|
|
45
|
+
const activeId = map.get(key);
|
|
46
|
+
return sessions.map((s) => ({
|
|
47
|
+
id: s.id,
|
|
48
|
+
key: [...map.entries()].find(([, id]) => id === s.id)?.[0] ?? "(external)",
|
|
49
|
+
title: s.title ?? s.id,
|
|
50
|
+
active: s.id === activeId,
|
|
51
|
+
createdAt: s.time.created,
|
|
52
|
+
}));
|
|
56
53
|
}
|
|
57
54
|
function currentSession(key) {
|
|
58
55
|
return map.get(key);
|