opencode-claw 0.2.0 → 0.2.2

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.
@@ -18,7 +18,7 @@ function rejection(config, channel) {
18
18
  }
19
19
  function checkAllowlist(config, msg) {
20
20
  const list = allowlist(config, msg.channel);
21
- if (!list)
21
+ if (!list || list.length === 0)
22
22
  return true;
23
23
  return list.includes(msg.peerId);
24
24
  }
@@ -105,6 +105,12 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
105
105
  }
106
106
  }
107
107
  }
108
+ /** Turn raw MCP tool names like `websearch_web_search_exa` into `Web Search Exa`. */
109
+ function humanizeToolName(raw) {
110
+ if (raw.includes(" "))
111
+ return raw;
112
+ return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
113
+ }
108
114
  async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
109
115
  const adapter = deps.adapters.get(msg.channel);
110
116
  if (!adapter) {
@@ -175,7 +181,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
175
181
  const progress = progressEnabled
176
182
  ? {
177
183
  onToolRunning: (_tool, title) => adapter.send(msg.peerId, {
178
- text: `🔧 ${title}...`,
184
+ text: `🔧 ${humanizeToolName(title)}...`,
179
185
  replyToId: msg.replyToId,
180
186
  }),
181
187
  onHeartbeat: async () => {
@@ -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
- await app.client.chat.postMessage({
78
- channel: peerId,
79
- text: message.text,
80
- thread_ts: message.threadId,
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
- await bot.api.sendMessage(Number(peerId), message.text, {
72
- reply_parameters: message.replyToId ? { message_id: Number(message.replyToId) } : undefined,
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
- await sock.sendMessage(jid, { text: message.text });
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)
@@ -53,10 +53,11 @@ function createMemoryPlugin(backend) {
53
53
  limit: 5,
54
54
  minRelevance: 0.05,
55
55
  });
56
- if (memories.length === 0)
57
- return;
58
- const block = memories.map((m) => `- [${m.category}] ${m.content}`).join("\n");
59
- output.system.push(`\n\n## Relevant Context from Memory\n${block}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claw",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Wrap OpenCode with persistent memory, messaging channels, and cron jobs",
5
5
  "license": "MIT",
6
6
  "type": "module",