@pedrohnas/opencode-telegram 0.1.0 → 1.2.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.
Files changed (43) hide show
  1. package/.env.example +17 -0
  2. package/bunfig.toml +2 -0
  3. package/docs/PROGRESS.md +327 -0
  4. package/docs/mapping.md +326 -0
  5. package/docs/plans/phase-1.md +176 -0
  6. package/docs/plans/phase-2.md +235 -0
  7. package/docs/plans/phase-3.md +485 -0
  8. package/docs/plans/phase-4.md +566 -0
  9. package/docs/spec.md +2055 -0
  10. package/e2e/client.ts +24 -0
  11. package/e2e/helpers.ts +119 -0
  12. package/e2e/phase-0.test.ts +30 -0
  13. package/e2e/phase-1.test.ts +48 -0
  14. package/e2e/phase-2.test.ts +54 -0
  15. package/e2e/phase-3.test.ts +142 -0
  16. package/e2e/phase-4.test.ts +96 -0
  17. package/e2e/runner.ts +145 -0
  18. package/package.json +14 -12
  19. package/scripts/gen-session.ts +49 -0
  20. package/src/bot.test.ts +301 -0
  21. package/src/bot.ts +91 -0
  22. package/src/config.test.ts +130 -0
  23. package/src/config.ts +15 -0
  24. package/src/event-bus.test.ts +175 -0
  25. package/src/handlers/allowlist.test.ts +60 -0
  26. package/src/handlers/allowlist.ts +33 -0
  27. package/src/handlers/cancel.test.ts +105 -0
  28. package/src/handlers/permissions.test.ts +72 -0
  29. package/src/handlers/questions.test.ts +107 -0
  30. package/src/handlers/sessions.test.ts +479 -0
  31. package/src/handlers/sessions.ts +202 -0
  32. package/src/handlers/typing.test.ts +60 -0
  33. package/src/index.ts +26 -0
  34. package/src/pending-requests.test.ts +64 -0
  35. package/src/send/chunker.test.ts +74 -0
  36. package/src/send/draft-stream.test.ts +229 -0
  37. package/src/send/format.test.ts +143 -0
  38. package/src/send/tool-progress.test.ts +70 -0
  39. package/src/session-manager.test.ts +198 -0
  40. package/src/session-manager.ts +23 -0
  41. package/src/turn-manager.test.ts +155 -0
  42. package/src/turn-manager.ts +5 -0
  43. package/tsconfig.json +9 -0
@@ -0,0 +1,479 @@
1
+ import { describe, test, expect, mock, beforeEach } from "bun:test"
2
+ import {
3
+ formatSessionList,
4
+ formatSessionInfo,
5
+ formatHistory,
6
+ parseSessionCallback,
7
+ handleList,
8
+ handleRename,
9
+ handleDelete,
10
+ handleInfo,
11
+ handleHistory,
12
+ handleSummarize,
13
+ } from "./sessions"
14
+ import { SessionManager } from "../session-manager"
15
+
16
+ // --- Mock SDK factory ---
17
+
18
+ function createMockSdk(overrides: {
19
+ list?: any[]
20
+ get?: any
21
+ messages?: any[]
22
+ update?: any
23
+ delete?: any
24
+ summarize?: any
25
+ } = {}) {
26
+ return {
27
+ session: {
28
+ list: mock(async () => ({ data: overrides.list ?? [] })),
29
+ get: mock(async () => ({ data: overrides.get ?? {} })),
30
+ messages: mock(async () => ({ data: overrides.messages ?? [] })),
31
+ update: mock(async () => ({ data: overrides.update ?? {} })),
32
+ delete: mock(async () => ({ data: overrides.delete ?? true })),
33
+ summarize: mock(async () => ({ data: overrides.summarize ?? true })),
34
+ create: mock(async () => ({ data: { id: "new", title: "", directory: "/tmp" } })),
35
+ },
36
+ }
37
+ }
38
+
39
+ // --- Helper: make session objects ---
40
+
41
+ function makeSession(overrides: Partial<{
42
+ id: string
43
+ title: string
44
+ directory: string
45
+ time: { created: number; updated: number; compacting?: number }
46
+ archived: boolean
47
+ }> = {}) {
48
+ return {
49
+ id: overrides.id ?? "sess-1",
50
+ title: overrides.title ?? "Test Session",
51
+ directory: overrides.directory ?? "/tmp",
52
+ projectID: "proj-1",
53
+ version: "1",
54
+ time: overrides.time ?? {
55
+ created: 1700000000000,
56
+ updated: 1700001000000,
57
+ },
58
+ ...(overrides.archived ? { time: { ...overrides.time, created: 1700000000000, updated: 1700001000000, archived: 9999 } } : {}),
59
+ }
60
+ }
61
+
62
+ function makeArchivedSession(id: string, title: string) {
63
+ return {
64
+ id,
65
+ title,
66
+ directory: "/tmp",
67
+ projectID: "proj-1",
68
+ version: "1",
69
+ time: {
70
+ created: 1700000000000,
71
+ updated: 1700001000000,
72
+ archived: 9999,
73
+ },
74
+ }
75
+ }
76
+
77
+ // ============================================================
78
+ // formatSessionList
79
+ // ============================================================
80
+
81
+ describe("formatSessionList", () => {
82
+ test("formats sessions as inline keyboard rows (title + date)", () => {
83
+ const sessions = [
84
+ makeSession({ id: "s1", title: "My Session", time: { created: 1700000000000, updated: 1700001000000 } }),
85
+ ]
86
+ const result = formatSessionList(sessions)
87
+ expect(result.text).toContain("Select a session")
88
+ expect(result.reply_markup.inline_keyboard).toHaveLength(1)
89
+ const btn = result.reply_markup.inline_keyboard[0][0]
90
+ expect(btn.text).toContain("My Session")
91
+ expect(btn.callback_data).toBe("sess:s1")
92
+ })
93
+
94
+ test("filters out archived sessions", () => {
95
+ const sessions = [
96
+ makeSession({ id: "s1", title: "Active" }),
97
+ makeArchivedSession("s2", "Archived"),
98
+ ]
99
+ const result = formatSessionList(sessions)
100
+ expect(result.reply_markup.inline_keyboard).toHaveLength(1)
101
+ expect(result.reply_markup.inline_keyboard[0][0].callback_data).toBe("sess:s1")
102
+ })
103
+
104
+ test("sorts by updated desc", () => {
105
+ const sessions = [
106
+ makeSession({ id: "old", title: "Old", time: { created: 1000, updated: 1000 } }),
107
+ makeSession({ id: "new", title: "New", time: { created: 2000, updated: 3000 } }),
108
+ ]
109
+ const result = formatSessionList(sessions)
110
+ // "New" should be first (updated: 3000 > 1000)
111
+ expect(result.reply_markup.inline_keyboard[0][0].callback_data).toBe("sess:new")
112
+ expect(result.reply_markup.inline_keyboard[1][0].callback_data).toBe("sess:old")
113
+ })
114
+
115
+ test("limits to 10 sessions", () => {
116
+ const sessions = Array.from({ length: 15 }, (_, i) =>
117
+ makeSession({ id: `s${i}`, title: `Session ${i}`, time: { created: 1000, updated: 2000 + i } }),
118
+ )
119
+ const result = formatSessionList(sessions)
120
+ expect(result.reply_markup.inline_keyboard).toHaveLength(10)
121
+ })
122
+
123
+ test("empty list returns 'No sessions found.' text", () => {
124
+ const result = formatSessionList([])
125
+ expect(result.text).toBe("No sessions found.")
126
+ expect(result.reply_markup.inline_keyboard).toHaveLength(0)
127
+ })
128
+ })
129
+
130
+ // ============================================================
131
+ // parseSessionCallback
132
+ // ============================================================
133
+
134
+ describe("parseSessionCallback", () => {
135
+ test("parses 'sess:abc123' correctly", () => {
136
+ const result = parseSessionCallback("sess:abc123")
137
+ expect(result).toEqual({ sessionPrefix: "abc123" })
138
+ })
139
+
140
+ test("returns null for non-sess prefix", () => {
141
+ expect(parseSessionCallback("invalid")).toBeNull()
142
+ expect(parseSessionCallback("perm:once:123")).toBeNull()
143
+ expect(parseSessionCallback("q:123:0")).toBeNull()
144
+ })
145
+ })
146
+
147
+ // ============================================================
148
+ // handleList
149
+ // ============================================================
150
+
151
+ describe("handleList", () => {
152
+ test("returns formatted session list from SDK", async () => {
153
+ const sdk = createMockSdk({
154
+ list: [
155
+ makeSession({ id: "s1", title: "Session One" }),
156
+ makeSession({ id: "s2", title: "Session Two" }),
157
+ ],
158
+ })
159
+ const result = await handleList({ sdk: sdk as any })
160
+ expect(result.text).toContain("Select a session")
161
+ expect(result.reply_markup.inline_keyboard).toHaveLength(2)
162
+ })
163
+
164
+ test("returns 'No sessions found.' when list is empty", async () => {
165
+ const sdk = createMockSdk({ list: [] })
166
+ const result = await handleList({ sdk: sdk as any })
167
+ expect(result.text).toBe("No sessions found.")
168
+ })
169
+ })
170
+
171
+ // ============================================================
172
+ // handleRename
173
+ // ============================================================
174
+
175
+ describe("handleRename", () => {
176
+ let sm: SessionManager
177
+
178
+ beforeEach(() => {
179
+ sm = new SessionManager({ maxEntries: 10, ttlMs: 60000 })
180
+ })
181
+
182
+ test("returns 'No active session.' when no session mapped", async () => {
183
+ const sdk = createMockSdk()
184
+ const result = await handleRename({
185
+ chatKey: "123",
186
+ title: "New Title",
187
+ sdk: sdk as any,
188
+ sessionManager: sm,
189
+ })
190
+ expect(result).toBe("No active session.")
191
+ })
192
+
193
+ test("returns 'Session renamed to: {title}' on success", async () => {
194
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
195
+ const sdk = createMockSdk()
196
+ const result = await handleRename({
197
+ chatKey: "123",
198
+ title: "My New Title",
199
+ sdk: sdk as any,
200
+ sessionManager: sm,
201
+ })
202
+ expect(result).toBe("Session renamed to: My New Title")
203
+ })
204
+
205
+ test("calls sdk.session.update with correct params", async () => {
206
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
207
+ const sdk = createMockSdk()
208
+ await handleRename({
209
+ chatKey: "123",
210
+ title: "Updated Title",
211
+ sdk: sdk as any,
212
+ sessionManager: sm,
213
+ })
214
+ expect(sdk.session.update).toHaveBeenCalledTimes(1)
215
+ const call = (sdk.session.update as any).mock.calls[0][0]
216
+ expect(call.sessionID).toBe("s1")
217
+ expect(call.title).toBe("Updated Title")
218
+ })
219
+
220
+ test("returns usage message when title is empty", async () => {
221
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
222
+ const sdk = createMockSdk()
223
+ const result = await handleRename({
224
+ chatKey: "123",
225
+ title: "",
226
+ sdk: sdk as any,
227
+ sessionManager: sm,
228
+ })
229
+ expect(result).toBe("Usage: /rename <title>")
230
+ })
231
+ })
232
+
233
+ // ============================================================
234
+ // handleDelete
235
+ // ============================================================
236
+
237
+ describe("handleDelete", () => {
238
+ let sm: SessionManager
239
+
240
+ beforeEach(() => {
241
+ sm = new SessionManager({ maxEntries: 10, ttlMs: 60000 })
242
+ })
243
+
244
+ test("returns 'No active session.' when no session mapped", async () => {
245
+ const sdk = createMockSdk()
246
+ const result = await handleDelete({
247
+ chatKey: "123",
248
+ sdk: sdk as any,
249
+ sessionManager: sm,
250
+ })
251
+ expect(result).toBe("No active session.")
252
+ })
253
+
254
+ test("returns 'Session deleted.' on success", async () => {
255
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
256
+ const sdk = createMockSdk()
257
+ const result = await handleDelete({
258
+ chatKey: "123",
259
+ sdk: sdk as any,
260
+ sessionManager: sm,
261
+ })
262
+ expect(result).toBe("Session deleted.")
263
+ })
264
+
265
+ test("calls sdk.session.delete then sessionManager.remove", async () => {
266
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
267
+ const sdk = createMockSdk()
268
+ await handleDelete({
269
+ chatKey: "123",
270
+ sdk: sdk as any,
271
+ sessionManager: sm,
272
+ })
273
+ expect(sdk.session.delete).toHaveBeenCalledTimes(1)
274
+ const call = (sdk.session.delete as any).mock.calls[0][0]
275
+ expect(call.sessionID).toBe("s1")
276
+ // SessionManager mapping should be removed
277
+ expect(sm.get("123")).toBeUndefined()
278
+ })
279
+ })
280
+
281
+ // ============================================================
282
+ // handleInfo
283
+ // ============================================================
284
+
285
+ describe("handleInfo", () => {
286
+ let sm: SessionManager
287
+
288
+ beforeEach(() => {
289
+ sm = new SessionManager({ maxEntries: 10, ttlMs: 60000 })
290
+ })
291
+
292
+ test("returns 'No active session.' when no session mapped", async () => {
293
+ const sdk = createMockSdk()
294
+ const result = await handleInfo({
295
+ chatKey: "123",
296
+ sdk: sdk as any,
297
+ sessionManager: sm,
298
+ })
299
+ expect(result).toBe("No active session.")
300
+ })
301
+
302
+ test("returns formatted session info on success", async () => {
303
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
304
+ const sdk = createMockSdk({
305
+ get: {
306
+ id: "s1",
307
+ title: "My Session",
308
+ directory: "/home/project",
309
+ time: { created: 1700000000000, updated: 1700001000000 },
310
+ },
311
+ })
312
+ const result = await handleInfo({
313
+ chatKey: "123",
314
+ sdk: sdk as any,
315
+ sessionManager: sm,
316
+ })
317
+ expect(result).toContain("My Session")
318
+ })
319
+
320
+ test("includes title, created date, updated date", async () => {
321
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
322
+ const sdk = createMockSdk({
323
+ get: {
324
+ id: "s1",
325
+ title: "Test",
326
+ directory: "/home",
327
+ time: { created: 1700000000000, updated: 1700001000000 },
328
+ },
329
+ })
330
+ const result = await handleInfo({
331
+ chatKey: "123",
332
+ sdk: sdk as any,
333
+ sessionManager: sm,
334
+ })
335
+ expect(result).toContain("Test")
336
+ // Should contain some date representation
337
+ expect(result).toMatch(/\d{4}/) // year somewhere
338
+ })
339
+ })
340
+
341
+ // ============================================================
342
+ // formatSessionInfo
343
+ // ============================================================
344
+
345
+ describe("formatSessionInfo", () => {
346
+ test("formats session with title and dates", () => {
347
+ const session = {
348
+ id: "s1",
349
+ title: "My Session",
350
+ directory: "/home/project",
351
+ time: { created: 1700000000000, updated: 1700001000000 },
352
+ }
353
+ const result = formatSessionInfo(session as any)
354
+ expect(result).toContain("My Session")
355
+ expect(result).toContain("/home/project")
356
+ })
357
+ })
358
+
359
+ // ============================================================
360
+ // handleHistory
361
+ // ============================================================
362
+
363
+ describe("handleHistory", () => {
364
+ let sm: SessionManager
365
+
366
+ beforeEach(() => {
367
+ sm = new SessionManager({ maxEntries: 10, ttlMs: 60000 })
368
+ })
369
+
370
+ test("returns 'No active session.' when no session mapped", async () => {
371
+ const sdk = createMockSdk()
372
+ const result = await handleHistory({
373
+ chatKey: "123",
374
+ sdk: sdk as any,
375
+ sessionManager: sm,
376
+ })
377
+ expect(result).toBe("No active session.")
378
+ })
379
+
380
+ test("returns formatted message history", async () => {
381
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
382
+ const sdk = createMockSdk({
383
+ messages: [
384
+ {
385
+ info: { id: "m1", role: "user", time: { created: 1000 } },
386
+ parts: [{ type: "text", text: "Hello" }],
387
+ },
388
+ {
389
+ info: { id: "m2", role: "assistant", time: { created: 2000 } },
390
+ parts: [{ type: "text", text: "Hi there!" }],
391
+ },
392
+ ],
393
+ })
394
+ const result = await handleHistory({
395
+ chatKey: "123",
396
+ sdk: sdk as any,
397
+ sessionManager: sm,
398
+ })
399
+ expect(result).toContain("Hello")
400
+ expect(result).toContain("Hi there!")
401
+ })
402
+
403
+ test("truncates long messages", async () => {
404
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
405
+ const longText = "A".repeat(1000)
406
+ const sdk = createMockSdk({
407
+ messages: [
408
+ {
409
+ info: { id: "m1", role: "user", time: { created: 1000 } },
410
+ parts: [{ type: "text", text: longText }],
411
+ },
412
+ ],
413
+ })
414
+ const result = await handleHistory({
415
+ chatKey: "123",
416
+ sdk: sdk as any,
417
+ sessionManager: sm,
418
+ })
419
+ // Should be truncated — result shorter than original text
420
+ expect(result.length).toBeLessThan(longText.length)
421
+ })
422
+
423
+ test("returns 'No messages yet.' when history is empty", async () => {
424
+ sm.set("123", { sessionId: "s1", directory: "/tmp" })
425
+ const sdk = createMockSdk({ messages: [] })
426
+ const result = await handleHistory({
427
+ chatKey: "123",
428
+ sdk: sdk as any,
429
+ sessionManager: sm,
430
+ })
431
+ expect(result).toBe("No messages yet.")
432
+ })
433
+ })
434
+
435
+ // ============================================================
436
+ // formatHistory
437
+ // ============================================================
438
+
439
+ describe("formatHistory", () => {
440
+ test("formats messages as role: text", () => {
441
+ const messages = [
442
+ {
443
+ info: { id: "m1", role: "user" as const, time: { created: 1000 } },
444
+ parts: [{ type: "text" as const, text: "Hello" }],
445
+ },
446
+ {
447
+ info: { id: "m2", role: "assistant" as const, time: { created: 2000 } },
448
+ parts: [{ type: "text" as const, text: "World" }],
449
+ },
450
+ ]
451
+ const result = formatHistory(messages)
452
+ expect(result).toContain("user")
453
+ expect(result).toContain("Hello")
454
+ expect(result).toContain("assistant")
455
+ expect(result).toContain("World")
456
+ })
457
+ })
458
+
459
+ // ============================================================
460
+ // handleSummarize
461
+ // ============================================================
462
+
463
+ describe("handleSummarize", () => {
464
+ let sm: SessionManager
465
+
466
+ beforeEach(() => {
467
+ sm = new SessionManager({ maxEntries: 10, ttlMs: 60000 })
468
+ })
469
+
470
+ test("returns 'No active session.' when no session mapped", async () => {
471
+ const sdk = createMockSdk()
472
+ const result = await handleSummarize({
473
+ chatKey: "123",
474
+ sdk: sdk as any,
475
+ sessionManager: sm,
476
+ })
477
+ expect(result).toBe("No active session.")
478
+ })
479
+ })
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Session command handlers — pure functions + thin async handlers.
3
+ *
4
+ * Exports:
5
+ * - formatSessionList(sessions) — format sessions as inline keyboard
6
+ * - formatSessionInfo(session) — format session info text
7
+ * - formatHistory(messages) — format message history
8
+ * - parseSessionCallback(data) — parse "sess:" callback data
9
+ * - handleList(params) — fetch + format session list
10
+ * - handleRename(params) — rename current session
11
+ * - handleDelete(params) — delete current session
12
+ * - handleInfo(params) — show session info
13
+ * - handleHistory(params) — show recent messages
14
+ * - handleSummarize(params) — summarize current session
15
+ */
16
+
17
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2"
18
+ import type { SessionManager } from "../session-manager"
19
+
20
+ // --- Pure formatting functions ---
21
+
22
+ export function formatSessionList(sessions: any[]): {
23
+ text: string
24
+ reply_markup: { inline_keyboard: any[][] }
25
+ } {
26
+ // Filter out archived sessions
27
+ const active = sessions.filter((s) => !s.time?.archived)
28
+
29
+ if (active.length === 0) {
30
+ return { text: "No sessions found.", reply_markup: { inline_keyboard: [] } }
31
+ }
32
+
33
+ // Sort by updated desc
34
+ const sorted = [...active].sort(
35
+ (a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0),
36
+ )
37
+
38
+ // Limit to 10
39
+ const limited = sorted.slice(0, 10)
40
+
41
+ const rows = limited.map((s) => {
42
+ const date = new Date(s.time?.updated ?? 0).toLocaleDateString()
43
+ const title = s.title || s.id.slice(0, 8)
44
+ return [
45
+ {
46
+ text: `${title} (${date})`,
47
+ callback_data: `sess:${s.id.slice(0, 20)}`,
48
+ },
49
+ ]
50
+ })
51
+
52
+ return {
53
+ text: "Select a session:",
54
+ reply_markup: { inline_keyboard: rows },
55
+ }
56
+ }
57
+
58
+ export function formatSessionInfo(session: any): string {
59
+ const title = session.title || session.id
60
+ const dir = session.directory || "—"
61
+ const created = session.time?.created
62
+ ? new Date(session.time.created).toLocaleString()
63
+ : "—"
64
+ const updated = session.time?.updated
65
+ ? new Date(session.time.updated).toLocaleString()
66
+ : "—"
67
+
68
+ return [
69
+ `Session: ${title}`,
70
+ `Directory: ${dir}`,
71
+ `Created: ${created}`,
72
+ `Updated: ${updated}`,
73
+ ].join("\n")
74
+ }
75
+
76
+ export function formatHistory(
77
+ messages: Array<{ info: any; parts: any[] }>,
78
+ ): string {
79
+ if (messages.length === 0) return "No messages yet."
80
+
81
+ // Take last 10 messages
82
+ const recent = messages.slice(-10)
83
+
84
+ return recent
85
+ .map((m) => {
86
+ const role = m.info.role ?? "unknown"
87
+ const textParts = m.parts.filter((p: any) => p.type === "text")
88
+ const text = textParts.map((p: any) => p.text).join("") || "(no text)"
89
+ // Truncate long messages
90
+ const truncated = text.length > 200 ? text.slice(0, 200) + "..." : text
91
+ return `${role}: ${truncated}`
92
+ })
93
+ .join("\n\n")
94
+ }
95
+
96
+ export function parseSessionCallback(
97
+ data: string,
98
+ ): { sessionPrefix: string } | null {
99
+ if (!data.startsWith("sess:")) return null
100
+ const prefix = data.slice(5)
101
+ if (!prefix) return null
102
+ return { sessionPrefix: prefix }
103
+ }
104
+
105
+ // --- Async handlers ---
106
+
107
+ export async function handleList(params: {
108
+ sdk: OpencodeClient
109
+ }): Promise<{ text: string; reply_markup: { inline_keyboard: any[][] } }> {
110
+ const result = await params.sdk.session.list()
111
+ const sessions = (result as any).data ?? []
112
+ return formatSessionList(sessions)
113
+ }
114
+
115
+ export async function handleRename(params: {
116
+ chatKey: string
117
+ title: string
118
+ sdk: OpencodeClient
119
+ sessionManager: SessionManager
120
+ }): Promise<string> {
121
+ const { chatKey, title, sdk, sessionManager } = params
122
+
123
+ if (!title.trim()) {
124
+ return "Usage: /rename <title>"
125
+ }
126
+
127
+ const entry = sessionManager.get(chatKey)
128
+ if (!entry) return "No active session."
129
+
130
+ await sdk.session.update({
131
+ sessionID: entry.sessionId,
132
+ title,
133
+ })
134
+
135
+ return `Session renamed to: ${title}`
136
+ }
137
+
138
+ export async function handleDelete(params: {
139
+ chatKey: string
140
+ sdk: OpencodeClient
141
+ sessionManager: SessionManager
142
+ }): Promise<string> {
143
+ const { chatKey, sdk, sessionManager } = params
144
+ const entry = sessionManager.get(chatKey)
145
+ if (!entry) return "No active session."
146
+
147
+ await sdk.session.delete({
148
+ sessionID: entry.sessionId,
149
+ })
150
+ sessionManager.remove(chatKey)
151
+
152
+ return "Session deleted."
153
+ }
154
+
155
+ export async function handleInfo(params: {
156
+ chatKey: string
157
+ sdk: OpencodeClient
158
+ sessionManager: SessionManager
159
+ }): Promise<string> {
160
+ const { chatKey, sdk, sessionManager } = params
161
+ const entry = sessionManager.get(chatKey)
162
+ if (!entry) return "No active session."
163
+
164
+ const result = await sdk.session.get({
165
+ sessionID: entry.sessionId,
166
+ })
167
+ const session = (result as any).data
168
+ if (!session) return "Session not found."
169
+
170
+ return formatSessionInfo(session)
171
+ }
172
+
173
+ export async function handleHistory(params: {
174
+ chatKey: string
175
+ sdk: OpencodeClient
176
+ sessionManager: SessionManager
177
+ }): Promise<string> {
178
+ const { chatKey, sdk, sessionManager } = params
179
+ const entry = sessionManager.get(chatKey)
180
+ if (!entry) return "No active session."
181
+
182
+ const result = await sdk.session.messages({
183
+ sessionID: entry.sessionId,
184
+ })
185
+ const messages = (result as any).data ?? []
186
+
187
+ if (messages.length === 0) return "No messages yet."
188
+
189
+ return formatHistory(messages)
190
+ }
191
+
192
+ export async function handleSummarize(params: {
193
+ chatKey: string
194
+ sdk: OpencodeClient
195
+ sessionManager: SessionManager
196
+ }): Promise<string> {
197
+ const { chatKey, sessionManager } = params
198
+ const entry = sessionManager.get(chatKey)
199
+ if (!entry) return "No active session."
200
+
201
+ return "Use /history to see recent messages, or ask the AI to summarize the conversation."
202
+ }