opencode-telegram-mirror 0.4.3 → 0.5.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/package.json +1 -1
- package/src/main.ts +104 -0
- 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
|
|
|
@@ -748,6 +818,7 @@ async function handleTelegramMessage(
|
|
|
748
818
|
})
|
|
749
819
|
if (result.data) {
|
|
750
820
|
state.sessionId = result.data.id
|
|
821
|
+
state.needsTitle = true
|
|
751
822
|
setSessionId(result.data.id, log)
|
|
752
823
|
log("info", "Created session for command", { sessionId: result.data.id })
|
|
753
824
|
} else {
|
|
@@ -821,6 +892,7 @@ async function handleTelegramMessage(
|
|
|
821
892
|
|
|
822
893
|
if (result.data) {
|
|
823
894
|
state.sessionId = result.data.id
|
|
895
|
+
state.needsTitle = true
|
|
824
896
|
setSessionId(result.data.id, log)
|
|
825
897
|
log("info", "Created session", { sessionId: result.data.id })
|
|
826
898
|
} else {
|
|
@@ -926,6 +998,38 @@ async function handleTelegramMessage(
|
|
|
926
998
|
})
|
|
927
999
|
|
|
928
1000
|
log("info", "Prompt sent", { sessionId: state.sessionId })
|
|
1001
|
+
|
|
1002
|
+
if (state.needsTitle && state.sessionId) {
|
|
1003
|
+
const textContent = parts
|
|
1004
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
1005
|
+
.map((p) => p.text)
|
|
1006
|
+
.join("\n")
|
|
1007
|
+
|
|
1008
|
+
if (textContent) {
|
|
1009
|
+
generateSessionTitle(state.server, textContent)
|
|
1010
|
+
.then(async (result) => {
|
|
1011
|
+
if (result.type === "title" && state.sessionId) {
|
|
1012
|
+
log("info", "Generated session title", { title: result.value })
|
|
1013
|
+
const updateResult = await state.server.client.session.update({
|
|
1014
|
+
sessionID: state.sessionId,
|
|
1015
|
+
title: result.value,
|
|
1016
|
+
})
|
|
1017
|
+
if (updateResult.data) {
|
|
1018
|
+
state.threadTitle = result.value
|
|
1019
|
+
state.needsTitle = false
|
|
1020
|
+
if (state.threadId) {
|
|
1021
|
+
await state.telegram.editForumTopic(state.threadId, result.value)
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} else {
|
|
1025
|
+
log("debug", "Title generation deferred", { reason: result.value })
|
|
1026
|
+
}
|
|
1027
|
+
})
|
|
1028
|
+
.catch((err) => {
|
|
1029
|
+
log("error", "Title generation failed", { error: String(err) })
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
929
1033
|
}
|
|
930
1034
|
|
|
931
1035
|
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)
|