opencode-claw 0.2.3 → 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 +62 -13
- package/dist/cli.js +0 -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
|
@@ -31,11 +31,13 @@ function parseCommand(text) {
|
|
|
31
31
|
return { name: trimmed.slice(1).toLowerCase(), args: "" };
|
|
32
32
|
return { name: trimmed.slice(1, space).toLowerCase(), args: trimmed.slice(space + 1).trim() };
|
|
33
33
|
}
|
|
34
|
+
const PAGE_SIZE = 10;
|
|
34
35
|
const HELP_TEXT = `Available commands:
|
|
35
36
|
/new [title] — Create a new session
|
|
36
37
|
/switch <id> — Switch to an existing session
|
|
37
|
-
/sessions — List your sessions
|
|
38
|
+
/sessions [page] — List your sessions (paginated)
|
|
38
39
|
/current — Show current session
|
|
40
|
+
/status — Show current agent run status
|
|
39
41
|
/fork — Fork current session into a new one
|
|
40
42
|
/cancel — Abort the currently running agent
|
|
41
43
|
/help — Show this help`;
|
|
@@ -43,7 +45,7 @@ const HELP_TEXT = `Available commands:
|
|
|
43
45
|
function peerKey(channel, peerId) {
|
|
44
46
|
return `${channel}:${peerId}`;
|
|
45
47
|
}
|
|
46
|
-
async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
48
|
+
async function handleCommand(cmd, msg, deps, activeStreams, activeStreamsMeta) {
|
|
47
49
|
const key = buildSessionKey(msg.channel, msg.peerId, msg.threadId);
|
|
48
50
|
switch (cmd.name) {
|
|
49
51
|
case "new": {
|
|
@@ -60,12 +62,18 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
|
60
62
|
const list = await deps.sessions.listSessions(key);
|
|
61
63
|
if (list.length === 0)
|
|
62
64
|
return "No sessions found.";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const page = Math.max(1, Number.parseInt(cmd.args) || 1);
|
|
66
|
+
const totalPages = Math.ceil(list.length / PAGE_SIZE);
|
|
67
|
+
const clamped = Math.min(page, totalPages);
|
|
68
|
+
const slice = list.slice((clamped - 1) * PAGE_SIZE, clamped * PAGE_SIZE);
|
|
69
|
+
const lines = slice.map((s) => {
|
|
65
70
|
const marker = s.active ? " (active)" : "";
|
|
66
71
|
return `• ${s.id} — ${s.title}${marker}`;
|
|
67
|
-
})
|
|
68
|
-
|
|
72
|
+
});
|
|
73
|
+
if (totalPages > 1) {
|
|
74
|
+
lines.push(`\nPage ${clamped}/${totalPages}${clamped < totalPages ? ` — use /sessions ${clamped + 1} for next` : ""}`);
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
69
77
|
}
|
|
70
78
|
case "current": {
|
|
71
79
|
const id = deps.sessions.currentSession(key);
|
|
@@ -96,6 +104,19 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
|
96
104
|
deps.logger.info("router: session aborted by user", { sessionId, aborted });
|
|
97
105
|
return aborted ? "Agent aborted." : "Abort request sent (agent may already be done).";
|
|
98
106
|
}
|
|
107
|
+
case "status": {
|
|
108
|
+
const pk = peerKey(msg.channel, msg.peerId);
|
|
109
|
+
const sessionId = activeStreams.get(pk);
|
|
110
|
+
if (!sessionId)
|
|
111
|
+
return "No agent is currently running.";
|
|
112
|
+
const meta = activeStreamsMeta.get(pk);
|
|
113
|
+
const elapsedSec = meta ? Math.floor((Date.now() - meta.startedAt) / 1000) : 0;
|
|
114
|
+
const mins = Math.floor(elapsedSec / 60);
|
|
115
|
+
const secs = elapsedSec % 60;
|
|
116
|
+
const elapsed = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
117
|
+
const tool = meta?.lastTool ? ` — last tool: ${humanizeToolName(meta.lastTool)}` : "";
|
|
118
|
+
return `⏳ Agent is running (${elapsed} elapsed${tool})`;
|
|
119
|
+
}
|
|
99
120
|
case "help": {
|
|
100
121
|
return HELP_TEXT;
|
|
101
122
|
}
|
|
@@ -110,7 +131,22 @@ function humanizeToolName(raw) {
|
|
|
110
131
|
return raw;
|
|
111
132
|
return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
112
133
|
}
|
|
113
|
-
|
|
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
|
+
}
|
|
149
|
+
async function routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions) {
|
|
114
150
|
const adapter = deps.adapters.get(msg.channel);
|
|
115
151
|
if (!adapter) {
|
|
116
152
|
deps.logger.warn("router: no adapter for channel", { channel: msg.channel });
|
|
@@ -131,7 +167,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
131
167
|
// Command interception
|
|
132
168
|
const cmd = parseCommand(msg.text);
|
|
133
169
|
if (cmd) {
|
|
134
|
-
const reply = await handleCommand(cmd, msg, deps, activeStreams);
|
|
170
|
+
const reply = await handleCommand(cmd, msg, deps, activeStreams, activeStreamsMeta);
|
|
135
171
|
await adapter.send(msg.peerId, { text: reply, replyToId: msg.replyToId });
|
|
136
172
|
return;
|
|
137
173
|
}
|
|
@@ -141,6 +177,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
141
177
|
deps.logger.debug("router: prompting session", { sessionId, channel: msg.channel });
|
|
142
178
|
const pk = peerKey(msg.channel, msg.peerId);
|
|
143
179
|
activeStreams.set(pk, sessionId);
|
|
180
|
+
activeStreamsMeta.set(pk, { startedAt: Date.now(), lastTool: undefined });
|
|
144
181
|
// Start typing indicator
|
|
145
182
|
if (adapter.sendTyping) {
|
|
146
183
|
await adapter.sendTyping(msg.peerId).catch(() => { });
|
|
@@ -179,10 +216,15 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
179
216
|
}
|
|
180
217
|
const progress = progressEnabled
|
|
181
218
|
? {
|
|
182
|
-
onToolRunning: (_tool, title) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
219
|
+
onToolRunning: (_tool, title) => {
|
|
220
|
+
const meta = activeStreamsMeta.get(pk);
|
|
221
|
+
if (meta)
|
|
222
|
+
meta.lastTool = title;
|
|
223
|
+
return adapter.send(msg.peerId, {
|
|
224
|
+
text: `🔧 ${humanizeToolName(title)}...`,
|
|
225
|
+
replyToId: msg.replyToId,
|
|
226
|
+
});
|
|
227
|
+
},
|
|
186
228
|
onHeartbeat: async () => {
|
|
187
229
|
if (adapter.sendTyping) {
|
|
188
230
|
await adapter.sendTyping(msg.peerId).catch(() => { });
|
|
@@ -197,6 +239,10 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
197
239
|
},
|
|
198
240
|
toolThrottleMs: deps.config.router.progress.toolThrottleMs,
|
|
199
241
|
heartbeatMs: deps.config.router.progress.heartbeatMs,
|
|
242
|
+
onTodoUpdated: async (todos) => {
|
|
243
|
+
const text = formatTodoList(todos);
|
|
244
|
+
await adapter.send(msg.peerId, { text });
|
|
245
|
+
},
|
|
200
246
|
}
|
|
201
247
|
: undefined;
|
|
202
248
|
let reply;
|
|
@@ -219,6 +265,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
219
265
|
}
|
|
220
266
|
finally {
|
|
221
267
|
activeStreams.delete(pk);
|
|
268
|
+
activeStreamsMeta.delete(pk);
|
|
222
269
|
pendingQuestions.delete(pk);
|
|
223
270
|
if (adapter.stopTyping) {
|
|
224
271
|
await adapter.stopTyping(msg.peerId).catch(() => { });
|
|
@@ -234,6 +281,8 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
|
234
281
|
export function createRouter(deps) {
|
|
235
282
|
// Tracks which sessionId is currently streaming for each channel:peerId pair
|
|
236
283
|
const activeStreams = new Map();
|
|
284
|
+
// Tracks timing + last tool for each active stream
|
|
285
|
+
const activeStreamsMeta = new Map();
|
|
237
286
|
// Tracks pending question resolvers — when agent asks a question, user's next message resolves it
|
|
238
287
|
const pendingQuestions = new Map();
|
|
239
288
|
async function handler(msg) {
|
|
@@ -247,7 +296,7 @@ export function createRouter(deps) {
|
|
|
247
296
|
pending.resolve(msg.text);
|
|
248
297
|
return;
|
|
249
298
|
}
|
|
250
|
-
await routeMessage(msg, deps, activeStreams, pendingQuestions);
|
|
299
|
+
await routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions);
|
|
251
300
|
}
|
|
252
301
|
catch (err) {
|
|
253
302
|
deps.logger.error("router: unhandled error", {
|
package/dist/cli.js
CHANGED
|
File without changes
|
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
|
},
|