opencode-telegram-mirror 0.4.3 → 0.5.1
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/package.json +1 -1
- package/src/main.ts +125 -2
- package/test/test-title-gen.ts +126 -0
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -83,6 +83,73 @@ async function updateStatusMessage(
|
|
|
83
83
|
log("debug", "Status message update", { messageId, state, success })
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Generate a session title using OpenCode with a lightweight model.
|
|
88
|
+
* Returns { type: "title", value: string } if successful,
|
|
89
|
+
* or { type: "unknown", value: string } if more context is needed.
|
|
90
|
+
*/
|
|
91
|
+
type TitleResult =
|
|
92
|
+
| { type: "unknown"; value: string }
|
|
93
|
+
| { type: "title"; value: string }
|
|
94
|
+
|
|
95
|
+
async function generateSessionTitle(
|
|
96
|
+
server: OpenCodeServer,
|
|
97
|
+
userMessage: string
|
|
98
|
+
): Promise<TitleResult> {
|
|
99
|
+
const tempSession = await server.client.session.create({ title: "title-gen" })
|
|
100
|
+
|
|
101
|
+
if (!tempSession.data) {
|
|
102
|
+
return { type: "unknown", value: "failed to create temp session" }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const response = await server.client.session.prompt({
|
|
107
|
+
sessionID: tempSession.data.id,
|
|
108
|
+
model: { providerID: "opencode", modelID: "glm-4.7-free" },
|
|
109
|
+
system: `You generate short titles for coding sessions based on user messages.
|
|
110
|
+
|
|
111
|
+
If the message provides enough context to understand the task, respond with:
|
|
112
|
+
{"type":"title","value":"<title here>"}
|
|
113
|
+
|
|
114
|
+
If the message is just a branch name, file path, or lacks context to understand what the user wants to do, respond with:
|
|
115
|
+
{"type":"unknown","value":"<brief reason>"}
|
|
116
|
+
|
|
117
|
+
Title rules (when generating):
|
|
118
|
+
- max 50 characters
|
|
119
|
+
- summarize the user's intent
|
|
120
|
+
- one line, no quotes or colons
|
|
121
|
+
- if a Linear ticket ID exists in the message (e.g. APP-550, ENG-123), always prefix the title with it
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
- "feat/add-login" -> {"type":"unknown","value":"branch name only, need task description"}
|
|
125
|
+
- "fix the auth bug in login.ts" -> {"type":"title","value":"Fix auth bug in login"}
|
|
126
|
+
- "src/components/Button.tsx" -> {"type":"unknown","value":"file path only, need task description"}
|
|
127
|
+
- "add dark mode toggle to settings" -> {"type":"title","value":"Add dark mode toggle to settings"}
|
|
128
|
+
- "APP-550: fix auth bug" -> {"type":"title","value":"APP-550: Fix auth bug"}
|
|
129
|
+
- "feat/APP-123-add-user-profile" -> {"type":"unknown","value":"branch name only, need task description"}
|
|
130
|
+
- "working on APP-123 to add user profiles" -> {"type":"title","value":"APP-123: Add user profiles"}
|
|
131
|
+
- "https://linear.app/team/issue/ENG-456/fix-button" -> {"type":"title","value":"ENG-456: Fix button"}
|
|
132
|
+
|
|
133
|
+
Respond with only valid JSON, nothing else.`,
|
|
134
|
+
parts: [{ type: "text", text: userMessage }],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const textPart = response.data?.parts?.find(
|
|
138
|
+
(p: { type: string }) => p.type === "text"
|
|
139
|
+
) as { type: "text"; text: string } | undefined
|
|
140
|
+
const text = textPart?.text?.trim() || ""
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(text) as TitleResult
|
|
144
|
+
} catch {
|
|
145
|
+
// If LLM didn't return valid JSON, treat response as title
|
|
146
|
+
return { type: "title", value: text.slice(0, 50) }
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await server.client.session.delete({ sessionID: tempSession.data.id })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
86
153
|
interface BotState {
|
|
87
154
|
server: OpenCodeServer;
|
|
88
155
|
telegram: TelegramClient;
|
|
@@ -94,6 +161,7 @@ interface BotState {
|
|
|
94
161
|
updatesUrl: string | null;
|
|
95
162
|
botUserId: number | null;
|
|
96
163
|
sessionId: string | null;
|
|
164
|
+
needsTitle: boolean;
|
|
97
165
|
|
|
98
166
|
assistantMessageIds: Set<string>;
|
|
99
167
|
pendingParts: Map<string, Part[]>;
|
|
@@ -260,6 +328,7 @@ async function main() {
|
|
|
260
328
|
updatesUrl: config.updatesUrl || null,
|
|
261
329
|
botUserId: botInfo.id,
|
|
262
330
|
sessionId,
|
|
331
|
+
needsTitle: !initialThreadTitle,
|
|
263
332
|
assistantMessageIds: new Set(),
|
|
264
333
|
pendingParts: new Map(),
|
|
265
334
|
sentPartIds: new Set(),
|
|
@@ -384,6 +453,7 @@ Do not start implementing until you have clarity on what needs to be done.`
|
|
|
384
453
|
|
|
385
454
|
if (sessionResult.data?.id) {
|
|
386
455
|
state.sessionId = sessionResult.data.id
|
|
456
|
+
state.needsTitle = true
|
|
387
457
|
setSessionId(sessionResult.data.id, log)
|
|
388
458
|
log("info", "Created OpenCode session", { sessionId: state.sessionId })
|
|
389
459
|
|
|
@@ -426,6 +496,16 @@ interface TelegramUpdate {
|
|
|
426
496
|
mime_type?: string
|
|
427
497
|
file_size?: number
|
|
428
498
|
}
|
|
499
|
+
video?: {
|
|
500
|
+
file_id: string
|
|
501
|
+
file_unique_id: string
|
|
502
|
+
duration: number
|
|
503
|
+
}
|
|
504
|
+
video_note?: {
|
|
505
|
+
file_id: string
|
|
506
|
+
file_unique_id: string
|
|
507
|
+
duration: number
|
|
508
|
+
}
|
|
429
509
|
from?: { id: number; username?: string }
|
|
430
510
|
chat: { id: number }
|
|
431
511
|
}
|
|
@@ -624,7 +704,7 @@ async function handleTelegramMessage(
|
|
|
624
704
|
msg: NonNullable<TelegramUpdate["message"]>,
|
|
625
705
|
) {
|
|
626
706
|
const messageText = msg.text || msg.caption
|
|
627
|
-
if (!messageText && !msg.photo && !msg.voice) return
|
|
707
|
+
if (!messageText && !msg.photo && !msg.voice && !msg.video && !msg.video_note) return
|
|
628
708
|
|
|
629
709
|
// Ignore all bot messages - context is sent directly via OpenCode API
|
|
630
710
|
if (msg.from?.id === state.botUserId) {
|
|
@@ -748,6 +828,7 @@ async function handleTelegramMessage(
|
|
|
748
828
|
})
|
|
749
829
|
if (result.data) {
|
|
750
830
|
state.sessionId = result.data.id
|
|
831
|
+
state.needsTitle = true
|
|
751
832
|
setSessionId(result.data.id, log)
|
|
752
833
|
log("info", "Created session for command", { sessionId: result.data.id })
|
|
753
834
|
} else {
|
|
@@ -821,6 +902,7 @@ async function handleTelegramMessage(
|
|
|
821
902
|
|
|
822
903
|
if (result.data) {
|
|
823
904
|
state.sessionId = result.data.id
|
|
905
|
+
state.needsTitle = true
|
|
824
906
|
setSessionId(result.data.id, log)
|
|
825
907
|
log("info", "Created session", { sessionId: result.data.id })
|
|
826
908
|
} else {
|
|
@@ -829,6 +911,14 @@ async function handleTelegramMessage(
|
|
|
829
911
|
}
|
|
830
912
|
}
|
|
831
913
|
|
|
914
|
+
if (msg.video || msg.video_note) {
|
|
915
|
+
log("info", "Rejecting video message - not supported")
|
|
916
|
+
await state.telegram.sendMessage(
|
|
917
|
+
"Video files are not supported. Please send screenshots or audio files instead."
|
|
918
|
+
)
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
|
|
832
922
|
// Build prompt parts
|
|
833
923
|
const parts: Array<
|
|
834
924
|
| { type: "text"; text: string }
|
|
@@ -905,7 +995,8 @@ async function handleTelegramMessage(
|
|
|
905
995
|
|
|
906
996
|
const transcribedText = transcriptionResult.value
|
|
907
997
|
log("info", "Voice transcribed", { preview: transcribedText.slice(0, 50) })
|
|
908
|
-
|
|
998
|
+
const voiceContext = `[Voice message transcript - may contain transcription inaccuracies]\n\n${transcribedText}`
|
|
999
|
+
parts.push({ type: "text", text: voiceContext })
|
|
909
1000
|
}
|
|
910
1001
|
|
|
911
1002
|
if (messageText) {
|
|
@@ -926,6 +1017,38 @@ async function handleTelegramMessage(
|
|
|
926
1017
|
})
|
|
927
1018
|
|
|
928
1019
|
log("info", "Prompt sent", { sessionId: state.sessionId })
|
|
1020
|
+
|
|
1021
|
+
if (state.needsTitle && state.sessionId) {
|
|
1022
|
+
const textContent = parts
|
|
1023
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
1024
|
+
.map((p) => p.text)
|
|
1025
|
+
.join("\n")
|
|
1026
|
+
|
|
1027
|
+
if (textContent) {
|
|
1028
|
+
generateSessionTitle(state.server, textContent)
|
|
1029
|
+
.then(async (result) => {
|
|
1030
|
+
if (result.type === "title" && state.sessionId) {
|
|
1031
|
+
log("info", "Generated session title", { title: result.value })
|
|
1032
|
+
const updateResult = await state.server.client.session.update({
|
|
1033
|
+
sessionID: state.sessionId,
|
|
1034
|
+
title: result.value,
|
|
1035
|
+
})
|
|
1036
|
+
if (updateResult.data) {
|
|
1037
|
+
state.threadTitle = result.value
|
|
1038
|
+
state.needsTitle = false
|
|
1039
|
+
if (state.threadId) {
|
|
1040
|
+
await state.telegram.editForumTopic(state.threadId, result.value)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
log("debug", "Title generation deferred", { reason: result.value })
|
|
1045
|
+
}
|
|
1046
|
+
})
|
|
1047
|
+
.catch((err) => {
|
|
1048
|
+
log("error", "Title generation failed", { error: String(err) })
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
929
1052
|
}
|
|
930
1053
|
|
|
931
1054
|
async function handleTelegramCallback(
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Quick test for session title generation
|
|
4
|
+
* Usage: bun run test/test-title-gen.ts
|
|
5
|
+
*
|
|
6
|
+
* Requires OPENCODE_URL env var or will start its own server
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { startServer, connectToServer, stopServer, type OpenCodeServer } from "../src/opencode"
|
|
10
|
+
|
|
11
|
+
type TitleResult =
|
|
12
|
+
| { type: "unknown"; value: string }
|
|
13
|
+
| { type: "title"; value: string }
|
|
14
|
+
|
|
15
|
+
async function generateSessionTitle(
|
|
16
|
+
server: OpenCodeServer,
|
|
17
|
+
userMessage: string
|
|
18
|
+
): Promise<TitleResult> {
|
|
19
|
+
const tempSession = await server.client.session.create({ title: "title-gen" })
|
|
20
|
+
|
|
21
|
+
if (!tempSession.data) {
|
|
22
|
+
return { type: "unknown", value: "failed to create temp session" }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await server.client.session.prompt({
|
|
27
|
+
sessionID: tempSession.data.id,
|
|
28
|
+
model: { providerID: "opencode", modelID: "glm-4.7-free" },
|
|
29
|
+
system: `You generate short titles for coding sessions based on user messages.
|
|
30
|
+
|
|
31
|
+
If the message provides enough context to understand the task, respond with:
|
|
32
|
+
{"type":"title","value":"<title here>"}
|
|
33
|
+
|
|
34
|
+
If the message is just a branch name, file path, or lacks context to understand what the user wants to do, respond with:
|
|
35
|
+
{"type":"unknown","value":"<brief reason>"}
|
|
36
|
+
|
|
37
|
+
Title rules (when generating):
|
|
38
|
+
- max 50 characters
|
|
39
|
+
- summarize the user's intent
|
|
40
|
+
- one line, no quotes or colons
|
|
41
|
+
- if a Linear ticket ID exists in the message (e.g. APP-550, ENG-123), always prefix the title with it
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
- "feat/add-login" -> {"type":"unknown","value":"branch name only, need task description"}
|
|
45
|
+
- "fix the auth bug in login.ts" -> {"type":"title","value":"Fix auth bug in login"}
|
|
46
|
+
- "src/components/Button.tsx" -> {"type":"unknown","value":"file path only, need task description"}
|
|
47
|
+
- "add dark mode toggle to settings" -> {"type":"title","value":"Add dark mode toggle to settings"}
|
|
48
|
+
- "APP-550: fix auth bug" -> {"type":"title","value":"APP-550: Fix auth bug"}
|
|
49
|
+
- "feat/APP-123-add-user-profile" -> {"type":"unknown","value":"branch name only, need task description"}
|
|
50
|
+
- "working on APP-123 to add user profiles" -> {"type":"title","value":"APP-123: Add user profiles"}
|
|
51
|
+
- "https://linear.app/team/issue/ENG-456/fix-button" -> {"type":"title","value":"ENG-456: Fix button"}
|
|
52
|
+
|
|
53
|
+
Respond with only valid JSON, nothing else.`,
|
|
54
|
+
parts: [{ type: "text", text: userMessage }],
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const textPart = response.data?.parts?.find(
|
|
58
|
+
(p: { type: string }) => p.type === "text"
|
|
59
|
+
) as { type: "text"; text: string } | undefined
|
|
60
|
+
const text = textPart?.text?.trim() || ""
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(text) as TitleResult
|
|
64
|
+
} catch {
|
|
65
|
+
return { type: "title", value: text.slice(0, 50) }
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
await server.client.session.delete({ sessionID: tempSession.data.id })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const testCases = [
|
|
73
|
+
"feat/add-login",
|
|
74
|
+
"fix the auth bug in login.ts",
|
|
75
|
+
"src/components/Button.tsx",
|
|
76
|
+
"add dark mode toggle to settings",
|
|
77
|
+
"APP-550: fix auth bug",
|
|
78
|
+
"feat/APP-123-add-user-profile",
|
|
79
|
+
"working on APP-123 to add user profiles",
|
|
80
|
+
"https://linear.app/team/issue/ENG-456/fix-button",
|
|
81
|
+
"andrew/test-branch",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
async function main() {
|
|
85
|
+
const openCodeUrl = process.env.OPENCODE_URL
|
|
86
|
+
let server: OpenCodeServer
|
|
87
|
+
let startedServer = false
|
|
88
|
+
|
|
89
|
+
if (openCodeUrl) {
|
|
90
|
+
console.log(`Connecting to OpenCode at ${openCodeUrl}...`)
|
|
91
|
+
const result = await connectToServer(openCodeUrl, process.cwd())
|
|
92
|
+
if (result.status === "error") {
|
|
93
|
+
console.error("Failed to connect:", result.error.message)
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
server = result.value
|
|
97
|
+
} else {
|
|
98
|
+
console.log("Starting OpenCode server...")
|
|
99
|
+
const result = await startServer(process.cwd())
|
|
100
|
+
if (result.status === "error") {
|
|
101
|
+
console.error("Failed to start:", result.error.message)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
server = result.value
|
|
105
|
+
startedServer = true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("\n=== Testing Title Generation ===\n")
|
|
109
|
+
|
|
110
|
+
for (const testCase of testCases) {
|
|
111
|
+
console.log(`Input: "${testCase}"`)
|
|
112
|
+
try {
|
|
113
|
+
const result = await generateSessionTitle(server, testCase)
|
|
114
|
+
console.log(`Result: ${JSON.stringify(result)}`)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.log(`Error: ${err}`)
|
|
117
|
+
}
|
|
118
|
+
console.log()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (startedServer) {
|
|
122
|
+
await stopServer()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch(console.error)
|