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 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
 
@@ -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
- return list
64
- .map((s) => {
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
- .join("\n");
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
- async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
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) => adapter.send(msg.peerId, {
183
- text: `🔧 ${humanizeToolName(title)}...`,
184
- replyToId: msg.replyToId,
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
@@ -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.3",
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
  },