opencode-claw 0.2.2 → 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,9 +45,8 @@ 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
- const prefix = `${msg.channel}:${msg.peerId}`;
49
50
  switch (cmd.name) {
50
51
  case "new": {
51
52
  const id = await deps.sessions.newSession(key, cmd.args || undefined);
@@ -58,15 +59,21 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
58
59
  return `Switched to session: ${cmd.args}`;
59
60
  }
60
61
  case "sessions": {
61
- const list = await deps.sessions.listSessions(prefix);
62
+ const list = await deps.sessions.listSessions(key);
62
63
  if (list.length === 0)
63
64
  return "No sessions found.";
64
- return list
65
- .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) => {
66
70
  const marker = s.active ? " (active)" : "";
67
71
  return `• ${s.id} — ${s.title}${marker}`;
68
- })
69
- .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");
70
77
  }
71
78
  case "current": {
72
79
  const id = deps.sessions.currentSession(key);
@@ -97,6 +104,19 @@ async function handleCommand(cmd, msg, deps, activeStreams) {
97
104
  deps.logger.info("router: session aborted by user", { sessionId, aborted });
98
105
  return aborted ? "Agent aborted." : "Abort request sent (agent may already be done).";
99
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
+ }
100
120
  case "help": {
101
121
  return HELP_TEXT;
102
122
  }
@@ -111,7 +131,7 @@ function humanizeToolName(raw) {
111
131
  return raw;
112
132
  return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
113
133
  }
114
- async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
134
+ async function routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions) {
115
135
  const adapter = deps.adapters.get(msg.channel);
116
136
  if (!adapter) {
117
137
  deps.logger.warn("router: no adapter for channel", { channel: msg.channel });
@@ -132,7 +152,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
132
152
  // Command interception
133
153
  const cmd = parseCommand(msg.text);
134
154
  if (cmd) {
135
- const reply = await handleCommand(cmd, msg, deps, activeStreams);
155
+ const reply = await handleCommand(cmd, msg, deps, activeStreams, activeStreamsMeta);
136
156
  await adapter.send(msg.peerId, { text: reply, replyToId: msg.replyToId });
137
157
  return;
138
158
  }
@@ -142,6 +162,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
142
162
  deps.logger.debug("router: prompting session", { sessionId, channel: msg.channel });
143
163
  const pk = peerKey(msg.channel, msg.peerId);
144
164
  activeStreams.set(pk, sessionId);
165
+ activeStreamsMeta.set(pk, { startedAt: Date.now(), lastTool: undefined });
145
166
  // Start typing indicator
146
167
  if (adapter.sendTyping) {
147
168
  await adapter.sendTyping(msg.peerId).catch(() => { });
@@ -180,10 +201,15 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
180
201
  }
181
202
  const progress = progressEnabled
182
203
  ? {
183
- onToolRunning: (_tool, title) => adapter.send(msg.peerId, {
184
- text: `🔧 ${humanizeToolName(title)}...`,
185
- replyToId: msg.replyToId,
186
- }),
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
+ },
187
213
  onHeartbeat: async () => {
188
214
  if (adapter.sendTyping) {
189
215
  await adapter.sendTyping(msg.peerId).catch(() => { });
@@ -220,6 +246,7 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
220
246
  }
221
247
  finally {
222
248
  activeStreams.delete(pk);
249
+ activeStreamsMeta.delete(pk);
223
250
  pendingQuestions.delete(pk);
224
251
  if (adapter.stopTyping) {
225
252
  await adapter.stopTyping(msg.peerId).catch(() => { });
@@ -235,6 +262,8 @@ async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
235
262
  export function createRouter(deps) {
236
263
  // Tracks which sessionId is currently streaming for each channel:peerId pair
237
264
  const activeStreams = new Map();
265
+ // Tracks timing + last tool for each active stream
266
+ const activeStreamsMeta = new Map();
238
267
  // Tracks pending question resolvers — when agent asks a question, user's next message resolves it
239
268
  const pendingQuestions = new Map();
240
269
  async function handler(msg) {
@@ -248,7 +277,7 @@ export function createRouter(deps) {
248
277
  pending.resolve(msg.text);
249
278
  return;
250
279
  }
251
- await routeMessage(msg, deps, activeStreams, pendingQuestions);
280
+ await routeMessage(msg, deps, activeStreams, activeStreamsMeta, pendingQuestions);
252
281
  }
253
282
  catch (err) {
254
283
  deps.logger.error("router: unhandled error", {
package/dist/cli.js CHANGED
File without changes
@@ -13,7 +13,7 @@ export declare function createSessionManager(client: OpencodeClient, config: Ses
13
13
  resolveSession: (key: string, title?: string) => Promise<string>;
14
14
  switchSession: (key: string, targetId: string) => Promise<void>;
15
15
  newSession: (key: string, title?: string) => Promise<string>;
16
- listSessions: (channelPeerPrefix: string) => Promise<SessionInfo[]>;
16
+ listSessions: (key: string) => Promise<SessionInfo[]>;
17
17
  currentSession: (key: string) => string | undefined;
18
18
  persist: () => Promise<void>;
19
19
  };
@@ -39,20 +39,17 @@ export function createSessionManager(client, config, map, logger) {
39
39
  logger.info("sessions: created and switched to new session", { key, id: session.data.id });
40
40
  return session.data.id;
41
41
  }
42
- async function listSessions(channelPeerPrefix) {
42
+ async function listSessions(key) {
43
43
  const all = await client.session.list();
44
44
  const sessions = all.data ?? [];
45
- const entries = [...map.entries()].filter(([key]) => key.includes(channelPeerPrefix));
46
- return entries.map(([key, id]) => {
47
- const session = sessions.find((s) => s.id === id);
48
- return {
49
- id,
50
- key,
51
- title: session?.title ?? "(deleted)",
52
- active: map.get(key) === id,
53
- createdAt: session?.time.created,
54
- };
55
- });
45
+ const activeId = map.get(key);
46
+ return sessions.map((s) => ({
47
+ id: s.id,
48
+ key: [...map.entries()].find(([, id]) => id === s.id)?.[0] ?? "(external)",
49
+ title: s.title ?? s.id,
50
+ active: s.id === activeId,
51
+ createdAt: s.time.created,
52
+ }));
56
53
  }
57
54
  function currentSession(key) {
58
55
  return map.get(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claw",
3
- "version": "0.2.2",
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",