opencode-telegram-mirror 0.3.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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Simple state persistence for Telegram bot
3
+ */
4
+
5
+ import { Database } from "bun:sqlite"
6
+ import type { LogFn } from "./log"
7
+
8
+ const DB_PATH = process.env.TELEGRAM_DB_PATH || "/tmp/telegram-opencode.db"
9
+
10
+ let db: Database | null = null
11
+
12
+ export function getDb(log: LogFn): Database {
13
+ if (!db) {
14
+ log("debug", "Opening SQLite database", { path: DB_PATH })
15
+ db = new Database(DB_PATH, { create: true })
16
+
17
+ db.run(
18
+ "CREATE TABLE IF NOT EXISTS state (" +
19
+ "key TEXT PRIMARY KEY, " +
20
+ "value TEXT NOT NULL)"
21
+ )
22
+
23
+ log("info", "SQLite database initialized", { path: DB_PATH })
24
+ }
25
+ return db
26
+ }
27
+
28
+ /**
29
+ * Get the stored session ID
30
+ */
31
+ export function getSessionId(log: LogFn): string | null {
32
+ const database = getDb(log)
33
+ type Row = { value: string }
34
+ const row = database
35
+ .query<Row, [string]>("SELECT value FROM state WHERE key = ?")
36
+ .get("session_id")
37
+ return row?.value ?? null
38
+ }
39
+
40
+ /**
41
+ * Store the session ID
42
+ */
43
+ export function setSessionId(sessionId: string, log: LogFn): void {
44
+ const database = getDb(log)
45
+ database.run(
46
+ "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
47
+ ["session_id", sessionId]
48
+ )
49
+ log("info", "Stored session ID", { sessionId })
50
+ }
51
+
52
+ /**
53
+ * Get the last processed Telegram update_id
54
+ */
55
+ export function getLastUpdateId(log: LogFn): number {
56
+ const database = getDb(log)
57
+ type Row = { value: string }
58
+ const row = database
59
+ .query<Row, [string]>("SELECT value FROM state WHERE key = ?")
60
+ .get("last_update_id")
61
+ return row ? Number.parseInt(row.value, 10) : 0
62
+ }
63
+
64
+ /**
65
+ * Store the last processed Telegram update_id
66
+ */
67
+ export function setLastUpdateId(updateId: number, log: LogFn): void {
68
+ const database = getDb(log)
69
+ database.run(
70
+ "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
71
+ ["last_update_id", String(updateId)]
72
+ )
73
+ }
74
+
75
+ /**
76
+ * Get the stored control message ID
77
+ */
78
+ export function getControlMessageId(log: LogFn): number | null {
79
+ const database = getDb(log)
80
+ type Row = { value: string }
81
+ const row = database
82
+ .query<Row, [string]>("SELECT value FROM state WHERE key = ?")
83
+ .get("control_message_id")
84
+ return row ? Number.parseInt(row.value, 10) : null
85
+ }
86
+
87
+ /**
88
+ * Store the control message ID
89
+ */
90
+ export function setControlMessageId(messageId: number, log: LogFn): void {
91
+ const database = getDb(log)
92
+ database.run(
93
+ "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
94
+ ["control_message_id", String(messageId)]
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Get the stored session variant
100
+ */
101
+ export function getSessionVariant(log: LogFn): string | null {
102
+ const database = getDb(log)
103
+ type Row = { value: string }
104
+ const row = database
105
+ .query<Row, [string]>("SELECT value FROM state WHERE key = ?")
106
+ .get("session_variant")
107
+ return row?.value ?? null
108
+ }
109
+
110
+ /**
111
+ * Store the session variant
112
+ */
113
+ export function setSessionVariant(variant: string, log: LogFn): void {
114
+ const database = getDb(log)
115
+ database.run(
116
+ "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
117
+ ["session_variant", variant]
118
+ )
119
+ log("info", "Stored session variant", { variant })
120
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Diff service for uploading diffs to the diff viewer
3
+ */
4
+
5
+ import { Result, TaggedError } from "better-result"
6
+ import type { LogFn } from "./log"
7
+
8
+ // Configure via environment variable - optional, if not set diff uploads are disabled
9
+ const DIFF_VIEWER_URL = process.env.DIFF_VIEWER_URL || null
10
+
11
+ export interface DiffFile {
12
+ path: string
13
+ oldContent: string
14
+ newContent: string
15
+ additions: number
16
+ deletions: number
17
+ }
18
+
19
+ export interface DiffUploadResult {
20
+ id: string
21
+ url: string
22
+ viewerUrl: string // URL for the mini-app viewer
23
+ }
24
+
25
+ export class DiffUploadError extends TaggedError("DiffUploadError")<{
26
+ message: string
27
+ cause: unknown
28
+ }>() {
29
+ constructor(args: { cause: unknown }) {
30
+ const causeMessage = args.cause instanceof Error ? args.cause.message : String(args.cause)
31
+ super({ ...args, message: `Diff upload failed: ${causeMessage}` })
32
+ }
33
+ }
34
+
35
+ export type DiffUploadResultValue = DiffUploadResult
36
+ export type DiffUploadResultError = DiffUploadError
37
+ export type DiffUploadResultReturn = Result<DiffUploadResultValue | null, DiffUploadResultError>
38
+
39
+ /**
40
+ * Count additions and deletions between two strings
41
+ */
42
+ function countChanges(oldContent: string, newContent: string): { additions: number; deletions: number } {
43
+ const oldLines = oldContent.split("\n")
44
+ const newLines = newContent.split("\n")
45
+
46
+ // Simple line-based diff counting
47
+ const oldSet = new Set(oldLines)
48
+ const newSet = new Set(newLines)
49
+
50
+ let additions = 0
51
+ let deletions = 0
52
+
53
+ for (const line of newLines) {
54
+ if (!oldSet.has(line)) additions++
55
+ }
56
+
57
+ for (const line of oldLines) {
58
+ if (!newSet.has(line)) deletions++
59
+ }
60
+
61
+ return { additions, deletions }
62
+ }
63
+
64
+ /**
65
+ * Upload a diff to the diff viewer service
66
+ */
67
+ export async function uploadDiff(
68
+ files: DiffFile[],
69
+ options?: { title?: string; log?: LogFn }
70
+ ): Promise<DiffUploadResultReturn> {
71
+ const log = options?.log ?? (() => {})
72
+
73
+ // If DIFF_VIEWER_URL is not configured, skip diff upload
74
+ if (!DIFF_VIEWER_URL) {
75
+ log("debug", "Diff upload skipped - DIFF_VIEWER_URL not configured")
76
+ return Result.ok(null)
77
+ }
78
+
79
+ const uploadResult = await Result.tryPromise({
80
+ try: async () => {
81
+ log("debug", "Uploading diff", { fileCount: files.length, url: DIFF_VIEWER_URL })
82
+
83
+ const response = await fetch(`${DIFF_VIEWER_URL}/api/diff`, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({
87
+ title: options?.title,
88
+ files,
89
+ }),
90
+ })
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`Diff upload failed: ${response.status}`)
94
+ }
95
+
96
+ const data = (await response.json()) as { id: string; url: string }
97
+
98
+ log("info", "Diff uploaded", { id: data.id, url: data.url })
99
+
100
+ return {
101
+ id: data.id,
102
+ url: data.url,
103
+ viewerUrl: `${DIFF_VIEWER_URL}/diff/${data.id}`,
104
+ }
105
+ },
106
+ catch: (error) => new DiffUploadError({ cause: error }),
107
+ })
108
+
109
+ if (uploadResult.status === "error") {
110
+ log("error", "Error uploading diff", { error: uploadResult.error.message })
111
+ }
112
+
113
+ return uploadResult
114
+ }
115
+
116
+ /**
117
+ * Create a diff file from edit tool input
118
+ */
119
+ export function createDiffFromEdit(input: {
120
+ filePath: string
121
+ oldString: string
122
+ newString: string
123
+ }): DiffFile {
124
+ const { additions, deletions } = countChanges(input.oldString, input.newString)
125
+
126
+ return {
127
+ path: input.filePath,
128
+ oldContent: input.oldString,
129
+ newContent: input.newString,
130
+ additions,
131
+ deletions,
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Generate inline diff preview (truncated for Telegram message)
137
+ */
138
+ export function generateInlineDiffPreview(
139
+ oldContent: string,
140
+ newContent: string,
141
+ maxLines = 10
142
+ ): string {
143
+ const oldLines = oldContent.split("\n")
144
+ const newLines = newContent.split("\n")
145
+
146
+ const diffLines: string[] = []
147
+ let lineCount = 0
148
+
149
+ // Find removed lines
150
+ for (const line of oldLines) {
151
+ if (!newLines.includes(line) && line.trim()) {
152
+ diffLines.push(`-${line}`)
153
+ lineCount++
154
+ if (lineCount >= maxLines) break
155
+ }
156
+ }
157
+
158
+ // Find added lines
159
+ if (lineCount < maxLines) {
160
+ for (const line of newLines) {
161
+ if (!oldLines.includes(line) && line.trim()) {
162
+ diffLines.push(`+${line}`)
163
+ lineCount++
164
+ if (lineCount >= maxLines) break
165
+ }
166
+ }
167
+ }
168
+
169
+ if (diffLines.length === 0) {
170
+ return ""
171
+ }
172
+
173
+ return `\`\`\`diff\n${diffLines.join("\n")}\n\`\`\``
174
+ }
175
+
176
+ export { DIFF_VIEWER_URL }
package/src/log.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Stdout logging for the Telegram plugin
3
+ */
4
+
5
+ export type LogFn = (
6
+ level: "debug" | "info" | "warn" | "error",
7
+ message: string,
8
+ extra?: Record<string, unknown>
9
+ ) => void
10
+
11
+ export function createLogger(): LogFn {
12
+ return (level, message, extra) => {
13
+ const timestamp = new Date().toISOString()
14
+ const extraStr = extra ? ` ${JSON.stringify(extra)}` : ""
15
+ const line = `${timestamp} [${level}] ${message}${extraStr}`
16
+
17
+ if (level === "error" || level === "warn") {
18
+ console.error(line)
19
+ } else {
20
+ console.log(line)
21
+ }
22
+ }
23
+ }