opencode-claw 0.2.3 → 0.2.5

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.
@@ -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,7 @@ 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
+ async function routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions) {
114
135
  const adapter = deps.adapters.get(msg.channel);
115
136
  if (!adapter) {
116
137
  deps.logger.warn("router: no adapter for channel", { channel: msg.channel });
@@ -131,7 +152,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
131
152
  // Command interception
132
153
  const cmd = parseCommand(msg.text);
133
154
  if (cmd) {
134
- const reply = await handleCommand(cmd, msg, deps, activeStreams);
155
+ const reply = await handleCommand(cmd, msg, deps, activeStreams, activeStreamsMeta);
135
156
  await adapter.send(msg.peerId, { text: reply, replyToId: msg.replyToId });
136
157
  return;
137
158
  }
@@ -141,6 +162,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
141
162
  deps.logger.debug("router: prompting session", { sessionId, channel: msg.channel });
142
163
  const pk = peerKey(msg.channel, msg.peerId);
143
164
  activeStreams.set(pk, sessionId);
165
+ activeStreamsMeta.set(pk, { startedAt: Date.now(), lastTool: undefined });
144
166
  // Start typing indicator
145
167
  if (adapter.sendTyping) {
146
168
  await adapter.sendTyping(msg.peerId).catch(() => { });
@@ -179,10 +201,15 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
179
201
  }
180
202
  const progress = progressEnabled
181
203
  ? {
182
- onToolRunning: (_tool, title) => adapter.send(msg.peerId, {
183
- text: `🔧 ${humanizeToolName(title)}...`,
184
- replyToId: msg.replyToId,
185
- }),
204
+ onToolRunning: (_tool, title) => {
205
+ const meta = activeStreamsMeta.get(pk);
206
+ if (meta)
207
+ meta.lastTool = title;
208
+ return adapter.send(msg.peerId, {
209
+ text: `🔧 ${humanizeToolName(title)}...`,
210
+ replyToId: msg.replyToId,
211
+ });
212
+ },
186
213
  onHeartbeat: async () => {
187
214
  if (adapter.sendTyping) {
188
215
  await adapter.sendTyping(msg.peerId).catch(() => { });
@@ -219,6 +246,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
219
246
  }
220
247
  finally {
221
248
  activeStreams.delete(pk);
249
+ activeStreamsMeta.delete(pk);
222
250
  pendingQuestions.delete(pk);
223
251
  if (adapter.stopTyping) {
224
252
  await adapter.stopTyping(msg.peerId).catch(() => { });
@@ -234,6 +262,8 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
234
262
  export function createRouter(deps) {
235
263
  // Tracks which sessionId is currently streaming for each channel:peerId pair
236
264
  const activeStreams = new Map();
265
+ // Tracks timing + last tool for each active stream
266
+ const activeStreamsMeta = new Map();
237
267
  // Tracks pending question resolvers — when agent asks a question, user's next message resolves it
238
268
  const pendingQuestions = new Map();
239
269
  async function handler(msg) {
@@ -247,7 +277,7 @@ export function createRouter(deps) {
247
277
  pending.resolve(msg.text);
248
278
  return;
249
279
  }
250
- await routeMessage(msg, deps, activeStreams, pendingQuestions);
280
+ await routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions);
251
281
  }
252
282
  catch (err) {
253
283
  deps.logger.error("router: unhandled error", {
package/dist/cli.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claw",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Wrap OpenCode with persistent memory, messaging channels, and cron jobs",
5
5
  "license": "MIT",
6
6
  "type": "module",