opencode-claw 0.2.5 → 0.3.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/README.md CHANGED
@@ -47,6 +47,8 @@ npm install -g opencode-claw
47
47
  opencode-claw
48
48
  ```
49
49
 
50
+ No git clone needed. The package is published to npm and runs anywhere Node.js is available.
51
+
50
52
  ## Quick Start
51
53
 
52
54
  ```bash
@@ -60,13 +62,65 @@ npx opencode-claw --init
60
62
  npx opencode-claw
61
63
  ```
62
64
 
63
- Or create `opencode-claw.json` manually see the [example config](https://github.com/jinkoso/opencode-claw/blob/main/opencode-claw.example.json) for all available options.
65
+ `--init` launches an interactive wizard that asks which channels to enable, prompts for bot tokens and allowlists, and writes an `opencode-claw.json` in the current directory. You can also copy the bundled example config directly:
66
+
67
+ ```bash
68
+ # After global install
69
+ cp $(npm root -g)/opencode-claw/opencode-claw.example.json ./opencode-claw.json
70
+
71
+ # After npx run (example config also on GitHub)
72
+ # https://github.com/jinkoso/opencode-claw/blob/main/opencode-claw.example.json
73
+ ```
74
+
75
+ ## How It Works
64
76
 
65
- The service starts an OpenCode server, connects your configured channels, initializes the memory system, and begins listening for messages.
77
+ When you run `opencode-claw`, it:
78
+
79
+ 1. Reads `opencode-claw.json` from the current directory (or the path in `OPENCODE_CLAW_CONFIG`)
80
+ 2. Starts an OpenCode server, **automatically registering the memory plugin** — no changes to `opencode.json` or any OpenCode config required
81
+ 3. Connects your configured channel adapters (Telegram, Slack, WhatsApp)
82
+ 4. Routes inbound messages to OpenCode sessions and streams responses back
83
+
84
+ ### Automatic Memory Plugin Registration
85
+
86
+ The memory plugin is wired automatically. You do **not** need to modify OpenCode's own config files. Internally, opencode-claw resolves the plugin path relative to its own installed location and passes it to the OpenCode SDK:
87
+
88
+ ```
89
+ opencode-claw starts
90
+ → resolves plugin path from its own dist/ directory
91
+ → passes plugin: ["file:///...path.../dist/memory/plugin-entry.js"] to createOpencode()
92
+ → OpenCode server starts with the plugin loaded
93
+ → memory_search, memory_store, memory_delete tools available in every session
94
+ ```
95
+
96
+ This path resolution uses `import.meta.url` and works correctly whether you installed via `npm install -g`, `npx`, or as a local dependency — no hardcoded paths, no manual setup.
97
+
98
+ ### Session Persistence
99
+
100
+ opencode-claw maps each channel peer (Telegram username, Slack user ID, phone number) to an OpenCode session ID, persisted in `./data/sessions.json` (relative to the config file). When you restart the service, existing sessions resume automatically.
101
+
102
+ ### Config File Location
103
+
104
+ The config file is discovered in this order:
105
+
106
+ 1. `OPENCODE_CLAW_CONFIG` environment variable (absolute path to the config file)
107
+ 2. `./opencode-claw.json` in the current working directory
108
+ 3. `../opencode-claw.json` in the parent directory
109
+
110
+ All relative paths inside `opencode-claw.json` (memory directory, session file, outbox, WhatsApp auth) are resolved relative to the **config file's directory**, not the current working directory. This means the service creates consistent data paths regardless of where you invoke it from.
111
+
112
+ **Data files created by default (relative to config file):**
113
+
114
+ | Path | Purpose |
115
+ |------|---------|
116
+ | `./data/memory/` | Memory files (txt backend) |
117
+ | `./data/sessions.json` | Session ID persistence |
118
+ | `./data/outbox/` | Async delivery queue for cron results |
119
+ | `./data/whatsapp/auth/` | WhatsApp multi-device credentials |
66
120
 
67
121
  ## Configuration
68
122
 
69
- All configuration lives in `opencode-claw.json` in the current working directory. Environment variables can be referenced with `${VAR_NAME}` syntax and will be expanded at load time.
123
+ All configuration lives in `opencode-claw.json`. Environment variables can be referenced with `${VAR_NAME}` syntax and are expanded at load time.
70
124
 
71
125
  ### OpenCode
72
126
 
@@ -114,7 +168,7 @@ All configuration lives in `opencode-claw.json` in the current working directory
114
168
  }
115
169
  ```
116
170
 
117
- Memory is injected into OpenCode sessions via a plugin. The agent can call `memory_search`, `memory_store`, and `memory_delete` tools during any conversation.
171
+ Memory is injected into OpenCode sessions automatically via the memory plugin. The agent can call `memory_search`, `memory_store`, and `memory_delete` tools during any conversation.
118
172
 
119
173
  ### Channels
120
174
 
@@ -202,7 +256,12 @@ Jobs use standard [cron expressions](https://crontab.guru/). Each job creates a
202
256
  ```json
203
257
  {
204
258
  "router": {
205
- "timeoutMs": 300000
259
+ "timeoutMs": 300000,
260
+ "progress": {
261
+ "enabled": true,
262
+ "toolThrottleMs": 5000,
263
+ "heartbeatMs": 30000
264
+ }
206
265
  }
207
266
  }
208
267
  ```
@@ -210,6 +269,11 @@ Jobs use standard [cron expressions](https://crontab.guru/). Each job creates a
210
269
  | Field | Type | Default | Description |
211
270
  |-------|------|---------|-------------|
212
271
  | `timeoutMs` | number | `300000` (5 min) | Max time to wait for an agent response before timing out |
272
+ | `progress.enabled` | boolean | `true` | Forward tool-use notifications and heartbeats to the channel while the agent is working |
273
+ | `progress.toolThrottleMs` | number | `5000` | Minimum ms between tool-use progress messages (prevents flooding) |
274
+ | `progress.heartbeatMs` | number | `30000` | Interval for "still working…" heartbeat messages during long-running tasks |
275
+
276
+ When `progress.enabled` is true, the router sends intermediate updates to the channel while the agent is processing — tool call notifications (e.g. "Running: read_file"), todo list updates when the agent calls `TodoWrite`, and periodic heartbeats so you know it hasn't stalled.
213
277
 
214
278
  ### Health Server
215
279
 
@@ -263,11 +327,8 @@ Any non-command message is routed to the active OpenCode session as a prompt.
263
327
  ## Programmatic API
264
328
 
265
329
  Use opencode-claw as a library in your own Node.js application:
266
-
267
330
  ```typescript
268
- import { main, createMemoryBackend } from "opencode-claw"
269
-
270
- // Run the full service programmatically
331
+ import { main, createMemoryBackend } from "opencode-claw/lib"
271
332
  await main()
272
333
  ```
273
334
 
@@ -294,18 +355,49 @@ import type {
294
355
  ChannelId,
295
356
  InboundMessage,
296
357
  OutboundMessage,
297
- } from "opencode-claw"
358
+ } from "opencode-claw/lib"
298
359
  ```
299
360
 
300
361
  ### Standalone Memory Plugin
301
362
 
302
- Use the memory plugin with a vanilla OpenCode installation (without the rest of opencode-claw):
363
+ The memory plugin can be used with a vanilla OpenCode installation (without the rest of opencode-claw). Add the package name to the `plugin` array in your `opencode.json`:
364
+
365
+ ```json
366
+ {
367
+ "plugin": ["opencode-claw"]
368
+ }
369
+ ```
370
+
371
+ OpenCode will install the package automatically on next startup and load the memory plugin.
372
+
373
+ Or wire it programmatically via the SDK:
303
374
 
304
375
  ```typescript
305
- import { memoryPlugin } from "opencode-claw/plugin"
376
+ import { createOpencode } from "@opencode-ai/sdk"
377
+ import { resolve } from "node:path"
378
+ import { fileURLToPath } from "node:url"
379
+ import { dirname } from "node:path"
380
+ const dir = dirname(fileURLToPath(import.meta.url))
381
+ const pluginPath = `file://${resolve(dir, "node_modules/opencode-claw/dist/memory/plugin-entry.js")}`
382
+ const { client, server } = await createOpencode({
383
+ config: { plugin: [pluginPath] },
384
+ })
385
+ ```
386
+
387
+ The plugin registers `memory_search`, `memory_store`, and `memory_delete` tools, and injects relevant memories into the system prompt via a chat transform hook. It reads its config from `opencode-claw.json` in the working directory — only the `memory` section is required:
388
+
389
+ ```json
390
+ {
391
+ "memory": {
392
+ "backend": "txt",
393
+ "txt": {
394
+ "directory": "./data/memory"
395
+ }
396
+ }
397
+ }
306
398
  ```
307
399
 
308
- This registers `memory_search`, `memory_store`, and `memory_delete` tools, and injects relevant memories into the system prompt via a chat transform hook. Configure it by placing an `opencode-claw.json` with a `memory` section in your OpenCode project directory.
400
+ > **Note**: You do not need the standalone plugin wiring when using `opencode-claw` directly it is registered automatically on startup.
309
401
 
310
402
  ## Inspiration
311
403
 
@@ -131,6 +131,21 @@ function humanizeToolName(raw) {
131
131
  return raw;
132
132
  return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
133
133
  }
134
+ function formatTodoList(todos) {
135
+ if (todos.length === 0)
136
+ return "📋 Todo list cleared.";
137
+ const icons = {
138
+ completed: "✅",
139
+ in_progress: "🔄",
140
+ pending: "⬜",
141
+ cancelled: "❌",
142
+ };
143
+ const lines = todos.map((t) => {
144
+ const icon = icons[t.status] ?? "•";
145
+ return `${icon} [${t.priority}] ${t.content}`;
146
+ });
147
+ return `📋 **Todos**\n${lines.join("\n")}`;
148
+ }
134
149
  async function routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions) {
135
150
  const adapter = deps.adapters.get(msg.channel);
136
151
  if (!adapter) {
@@ -224,6 +239,10 @@ async function routeMessage(msg, deps, activeStreams, activeStreamsMeta, pending
224
239
  },
225
240
  toolThrottleMs: deps.config.router.progress.toolThrottleMs,
226
241
  heartbeatMs: deps.config.router.progress.heartbeatMs,
242
+ onTodoUpdated: async (todos) => {
243
+ const text = formatTodoList(todos);
244
+ await adapter.send(msg.peerId, { text });
245
+ },
227
246
  }
228
247
  : undefined;
229
248
  let reply;
@@ -1,3 +1,4 @@
1
+ import { dirname, isAbsolute, resolve } from "node:path";
1
2
  import { fileExists, readJsonFile } from "../compat.js";
2
3
  import { configSchema } from "./schema.js";
3
4
  function expand(value) {
@@ -22,12 +23,12 @@ function expandDeep(obj) {
22
23
  }
23
24
  return obj;
24
25
  }
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
26
  export async function loadConfig() {
27
+ const searchPaths = [
28
+ process.env.OPENCODE_CLAW_CONFIG,
29
+ "./opencode-claw.json",
30
+ `${process.env.HOME}/.config/opencode-claw/config.json`,
31
+ ].filter(Boolean);
31
32
  let raw = null;
32
33
  let found = "";
33
34
  for (const p of searchPaths) {
@@ -53,5 +54,16 @@ export async function loadConfig() {
53
54
  .join("\n");
54
55
  throw new Error(`Config validation failed (${found}):\n${errors}`);
55
56
  }
56
- return result.data;
57
+ const data = result.data;
58
+ const base = dirname(resolve(found));
59
+ function resolvePath(p) {
60
+ return isAbsolute(p) ? p : resolve(base, p);
61
+ }
62
+ data.memory.txt.directory = resolvePath(data.memory.txt.directory);
63
+ data.sessions.persistPath = resolvePath(data.sessions.persistPath);
64
+ data.outbox.directory = resolvePath(data.outbox.directory);
65
+ if (data.channels.whatsapp) {
66
+ data.channels.whatsapp.authDir = resolvePath(data.channels.whatsapp.authDir);
67
+ }
68
+ return data;
57
69
  }
@@ -57,7 +57,12 @@ function createMemoryPlugin(backend) {
57
57
  const block = memories.map((m) => `- [${m.category}] ${m.content}`).join("\n");
58
58
  output.system.push(`\n\n## Relevant Context from Memory\n${block}`);
59
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
+ output.system.push("\n\n## Memory — Mandatory Protocol", "\nYou have two persistent memory tools: **memory_search** and **memory_store**.", "\n\n**MANDATORY you MUST follow this on every turn:**", "\n1. **BEFORE replying**: call `memory_search` with the user's topic. Always. No exceptions.", "\n2. **AFTER completing any task**: call `memory_store` for EVERY new fact you learned.", "\n\n**What to store (one fact per call):**", "\n- Project: absolute path, repo URL, language/stack, key entry points", "\n- Workflows: build, run, lint, test commands", "\n- Architecture: patterns, design decisions, known pitfalls", "\n- Preferences: user choices, style guidelines", "\n- Relationships: which projects depend on each other", "\n\n**Categories**: project | experience | preference | entity | event | knowledge", "\nStore one clear, self-contained statement per call. This is NOT optional.");
61
+ },
62
+ "experimental.session.compacting": async (_input, output) => {
63
+ output.context.push("MANDATORY: Before this session is compacted, call memory_store to preserve every important fact " +
64
+ "from this session. Store: project paths, build/run/test commands, architecture decisions, " +
65
+ "key file locations, lessons learned, user preferences. One fact per call. Do this first.");
61
66
  },
62
67
  });
63
68
  }
@@ -0,0 +1 @@
1
+ export { memoryPlugin } from "./memory/plugin-entry.js";
@@ -0,0 +1 @@
1
+ export { memoryPlugin } from "./memory/plugin-entry.js";
@@ -1,12 +1,14 @@
1
- import type { OpencodeClient, QuestionRequest } from "@opencode-ai/sdk/v2";
1
+ import type { OpencodeClient, QuestionRequest, Todo } from "@opencode-ai/sdk/v2";
2
2
  import type { Logger } from "../utils/logger.js";
3
3
  export type ToolProgressCallback = (tool: string, title: string) => Promise<void>;
4
4
  export type HeartbeatCallback = () => Promise<void>;
5
5
  export type QuestionCallback = (question: QuestionRequest) => Promise<Array<Array<string>>>;
6
+ export type TodoUpdatedCallback = (todos: Todo[]) => Promise<void>;
6
7
  export type ProgressOptions = {
7
8
  onToolRunning?: ToolProgressCallback;
8
9
  onHeartbeat?: HeartbeatCallback;
9
10
  onQuestion?: QuestionCallback;
11
+ onTodoUpdated?: TodoUpdatedCallback;
10
12
  toolThrottleMs?: number;
11
13
  heartbeatMs?: number;
12
14
  };
@@ -81,6 +81,16 @@ export async function promptStreaming(client, sessionId, promptText, timeoutMs,
81
81
  }
82
82
  continue;
83
83
  }
84
+ if (event.type === "todo.updated") {
85
+ const { sessionID, todos } = event.properties;
86
+ if (sessionID !== sessionId)
87
+ continue;
88
+ if (progress?.onTodoUpdated) {
89
+ await progress.onTodoUpdated(todos).catch(() => { });
90
+ touchActivity();
91
+ }
92
+ continue;
93
+ }
84
94
  if (event.type === "session.error") {
85
95
  const { sessionID, error } = event.properties;
86
96
  if (sessionID && sessionID !== sessionId)
@@ -59,7 +59,12 @@
59
59
  "maxAttempts": 3
60
60
  },
61
61
  "router": {
62
- "timeoutMs": 300000
62
+ "timeoutMs": 300000,
63
+ "progress": {
64
+ "enabled": true,
65
+ "toolThrottleMs": 5000,
66
+ "heartbeatMs": 30000
67
+ }
63
68
  },
64
69
  "health": {
65
70
  "enabled": false,
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "opencode-claw",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Wrap OpenCode with persistent memory, messaging channels, and cron jobs",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "main": "./dist/exports.js",
7
+ "main": "./dist/plugin-root.js",
8
8
  "types": "./dist/exports.d.ts",
9
9
  "bin": {
10
10
  "opencode-claw": "./dist/cli.js"
11
11
  },
12
12
  "exports": {
13
13
  ".": {
14
+ "import": "./dist/plugin-root.js",
15
+ "types": "./dist/plugin-root.d.ts"
16
+ },
17
+ "./lib": {
14
18
  "import": "./dist/exports.js",
15
19
  "types": "./dist/exports.d.ts"
16
20
  },