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 +105 -13
- package/dist/channels/router.js +19 -0
- package/dist/config/loader.js +18 -6
- package/dist/memory/plugin.js +6 -1
- package/dist/plugin-root.d.ts +1 -0
- package/dist/plugin-root.js +1 -0
- package/dist/sessions/prompt.d.ts +3 -1
- package/dist/sessions/prompt.js +10 -0
- package/opencode-claw.example.json +6 -1
- package/package.json +6 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
package/dist/channels/router.js
CHANGED
|
@@ -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;
|
package/dist/config/loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/memory/plugin.js
CHANGED
|
@@ -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(
|
|
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
|
};
|
package/dist/sessions/prompt.js
CHANGED
|
@@ -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)
|
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claw",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
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
|
},
|