@chime-io/plugin-opencode 1.0.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,300 @@
1
+ import fs from 'node:fs/promises';
2
+ import { createOpenCodeEventFormatter, extractErrorMessage, } from './format.js';
3
+ const QUESTION_TOOLS = new Set(['question', 'ask_user_question', 'askuserquestion']);
4
+ function getLifecycleLogFile() {
5
+ return process.env.TELME_LOG_FILE || '/tmp/telme.log';
6
+ }
7
+ async function appendLifecycleLog(stage, payload) {
8
+ try {
9
+ const line = JSON.stringify({
10
+ ts: new Date().toISOString(),
11
+ stage,
12
+ ...payload,
13
+ });
14
+ await fs.appendFile(getLifecycleLogFile(), `${line}\n`, 'utf8');
15
+ }
16
+ catch {
17
+ // Never break plugin lifecycle when file logging fails.
18
+ }
19
+ }
20
+ function isBusyStatus(status) {
21
+ return status === 'busy' || status?.type === 'busy';
22
+ }
23
+ function getQuestionText(args) {
24
+ const questions = args?.questions;
25
+ if (!Array.isArray(questions) || questions.length === 0)
26
+ return '';
27
+ const questionText = questions[0]?.question;
28
+ return typeof questionText === 'string' ? questionText : '';
29
+ }
30
+ export function createOpenCodeNotifierPlugin({ client, notifier, logger, }) {
31
+ const sessionCache = new Map();
32
+ const rootActivity = new Map();
33
+ const notifyingRoots = new Set();
34
+ const rootErrors = new Map();
35
+ const notifiedTaskErrors = new Set();
36
+ const questionNotifications = new Map();
37
+ const permissionNotifications = new Map();
38
+ const formatter = createOpenCodeEventFormatter({
39
+ listMessages: async (sessionId) => {
40
+ if (!client.session?.messages)
41
+ return [];
42
+ const attempts = [
43
+ () => client.session.messages({ sessionID: sessionId, limit: 10 }),
44
+ () => client.session.messages({
45
+ path: { id: sessionId },
46
+ query: { limit: 10 },
47
+ }),
48
+ ];
49
+ for (const attempt of attempts) {
50
+ try {
51
+ const result = await attempt();
52
+ if (Array.isArray(result))
53
+ return result;
54
+ if (Array.isArray(result.data))
55
+ return result.data;
56
+ }
57
+ catch (error) {
58
+ await logger.warn('Failed to load session messages for notification', {
59
+ sessionId,
60
+ error: error instanceof Error ? error.message : String(error),
61
+ });
62
+ }
63
+ }
64
+ return [];
65
+ },
66
+ });
67
+ async function getSessionInfo(sessionId) {
68
+ const cached = sessionCache.get(sessionId);
69
+ if (cached)
70
+ return cached;
71
+ if (!client.session) {
72
+ throw new Error('OpenCode session client is required');
73
+ }
74
+ const result = await client.session.get({ sessionID: sessionId });
75
+ if (result.error || !result.data) {
76
+ throw new Error(result.error ? `Failed to load session ${sessionId}` : `Session ${sessionId} not found`);
77
+ }
78
+ sessionCache.set(result.data.id, result.data);
79
+ return result.data;
80
+ }
81
+ async function getRootSessionInfo(sessionId) {
82
+ const visited = new Set();
83
+ let current = await getSessionInfo(sessionId);
84
+ while (current.parentID) {
85
+ if (visited.has(current.id)) {
86
+ throw new Error(`Detected session parent cycle at ${current.id}`);
87
+ }
88
+ visited.add(current.id);
89
+ current = await getSessionInfo(current.parentID);
90
+ }
91
+ return current;
92
+ }
93
+ async function notifyRootSession(sessionId) {
94
+ let notifiedRootId = null;
95
+ try {
96
+ const rootSession = await getRootSessionInfo(sessionId);
97
+ if (rootSession.id !== sessionId)
98
+ return;
99
+ if (!rootActivity.get(rootSession.id))
100
+ return;
101
+ if (notifyingRoots.has(rootSession.id))
102
+ return;
103
+ notifyingRoots.add(rootSession.id);
104
+ notifiedRootId = rootSession.id;
105
+ if (rootErrors.has(rootSession.id)) {
106
+ await notifier.notify(await formatter.formatSessionError(rootSession, rootErrors.get(rootSession.id)));
107
+ }
108
+ else {
109
+ await notifier.notify(await formatter.formatSessionCompleted(rootSession));
110
+ }
111
+ rootActivity.set(rootSession.id, false);
112
+ rootErrors.delete(rootSession.id);
113
+ }
114
+ catch (error) {
115
+ await logger.warn('Failed to process notification event', {
116
+ sessionId,
117
+ error: error instanceof Error ? error.message : String(error),
118
+ });
119
+ }
120
+ finally {
121
+ if (notifiedRootId) {
122
+ notifyingRoots.delete(notifiedRootId);
123
+ }
124
+ }
125
+ }
126
+ async function notifyTaskSessionError(sessionId, errorMessage) {
127
+ if (notifiedTaskErrors.has(sessionId))
128
+ return;
129
+ try {
130
+ const session = await getSessionInfo(sessionId);
131
+ if (!session.parentID)
132
+ return;
133
+ notifiedTaskErrors.add(sessionId);
134
+ await notifier.notify(await formatter.formatSessionError(session, errorMessage));
135
+ }
136
+ catch (error) {
137
+ await logger.warn('Failed to send task error notification', {
138
+ sessionId,
139
+ error: error instanceof Error ? error.message : String(error),
140
+ });
141
+ }
142
+ }
143
+ async function notifyQuestion(sessionId, callId, questionText) {
144
+ if (!callId || questionNotifications.has(callId))
145
+ return;
146
+ try {
147
+ const session = await getSessionInfo(sessionId);
148
+ questionNotifications.set(callId, sessionId);
149
+ await notifier.notify(formatter.formatQuestion(session, questionText));
150
+ }
151
+ catch (error) {
152
+ await logger.warn('Failed to send question notification', {
153
+ sessionId,
154
+ callId,
155
+ error: error instanceof Error ? error.message : String(error),
156
+ });
157
+ }
158
+ }
159
+ async function notifyPermission(sessionId, permissionId, title) {
160
+ if (!permissionId || permissionNotifications.has(permissionId))
161
+ return;
162
+ try {
163
+ const session = await getSessionInfo(sessionId);
164
+ permissionNotifications.set(permissionId, sessionId);
165
+ await notifier.notify(formatter.formatPermission(session, title));
166
+ }
167
+ catch (error) {
168
+ await logger.warn('Failed to send permission notification', {
169
+ sessionId,
170
+ permissionId,
171
+ error: error instanceof Error ? error.message : String(error),
172
+ });
173
+ }
174
+ }
175
+ function clearSessionNotificationState(sessionId) {
176
+ notifiedTaskErrors.delete(sessionId);
177
+ for (const [callId, trackedSessionId] of questionNotifications.entries()) {
178
+ if (trackedSessionId === sessionId) {
179
+ questionNotifications.delete(callId);
180
+ }
181
+ }
182
+ for (const [permissionId, trackedSessionId] of permissionNotifications.entries()) {
183
+ if (trackedSessionId === sessionId) {
184
+ permissionNotifications.delete(permissionId);
185
+ }
186
+ }
187
+ }
188
+ return {
189
+ 'tool.execute.before': async (input, output) => {
190
+ await appendLifecycleLog('tool.execute.before', {
191
+ tool: input.tool,
192
+ sessionId: input.sessionID,
193
+ callId: input.callID,
194
+ });
195
+ if (!input.tool || !QUESTION_TOOLS.has(input.tool))
196
+ return;
197
+ if (!input.sessionID || !input.callID)
198
+ return;
199
+ await notifyQuestion(input.sessionID, input.callID, getQuestionText(output.args));
200
+ },
201
+ 'tool.execute.after': async (input) => {
202
+ await appendLifecycleLog('tool.execute.after', {
203
+ tool: input.tool,
204
+ sessionId: input.sessionID,
205
+ callId: input.callID,
206
+ });
207
+ if (!input.tool || !QUESTION_TOOLS.has(input.tool) || !input.callID)
208
+ return;
209
+ questionNotifications.delete(input.callID);
210
+ },
211
+ event: async ({ event }) => {
212
+ await appendLifecycleLog('event.received', {
213
+ eventType: event.type,
214
+ sessionId: event.properties?.sessionID,
215
+ });
216
+ if (event.type === 'session.created' || event.type === 'session.updated') {
217
+ const info = event.properties?.info;
218
+ if (info?.id) {
219
+ sessionCache.set(info.id, info);
220
+ }
221
+ return;
222
+ }
223
+ if (event.type === 'session.deleted') {
224
+ const sessionId = event.properties?.info?.id;
225
+ if (sessionId) {
226
+ clearSessionNotificationState(sessionId);
227
+ }
228
+ return;
229
+ }
230
+ if (event.type === 'permission.updated' || event.type === 'permission.asked') {
231
+ const permissionId = event.properties?.id;
232
+ const sessionId = event.properties?.sessionID;
233
+ if (!permissionId || !sessionId)
234
+ return;
235
+ await notifyPermission(sessionId, permissionId, event.properties?.title ?? '');
236
+ return;
237
+ }
238
+ if (event.type === 'permission.replied') {
239
+ const permissionId = event.properties?.permissionID;
240
+ if (permissionId) {
241
+ permissionNotifications.delete(permissionId);
242
+ }
243
+ return;
244
+ }
245
+ if (event.type === 'session.status') {
246
+ const sessionId = event.properties?.sessionID;
247
+ const status = event.properties?.status;
248
+ if (!sessionId)
249
+ return;
250
+ if (typeof status === 'object' && status?.type === 'idle') {
251
+ await notifyRootSession(sessionId);
252
+ return;
253
+ }
254
+ if (!isBusyStatus(status))
255
+ return;
256
+ try {
257
+ const rootSession = await getRootSessionInfo(sessionId);
258
+ rootActivity.set(rootSession.id, true);
259
+ }
260
+ catch (error) {
261
+ await logger.warn('Failed to resolve root session for status event', {
262
+ sessionId,
263
+ error: error instanceof Error ? error.message : String(error),
264
+ });
265
+ }
266
+ return;
267
+ }
268
+ if (event.type === 'session.error') {
269
+ const sessionId = event.properties?.sessionID;
270
+ if (!sessionId)
271
+ return;
272
+ try {
273
+ const session = await getSessionInfo(sessionId);
274
+ const errorMessage = extractErrorMessage(event.properties?.error);
275
+ if (session.parentID) {
276
+ await notifyTaskSessionError(sessionId, errorMessage);
277
+ return;
278
+ }
279
+ const rootSession = await getRootSessionInfo(sessionId);
280
+ rootActivity.set(rootSession.id, true);
281
+ rootErrors.set(rootSession.id, errorMessage);
282
+ }
283
+ catch (error) {
284
+ await logger.warn('Failed to resolve root session for error event', {
285
+ sessionId,
286
+ error: error instanceof Error ? error.message : String(error),
287
+ });
288
+ }
289
+ return;
290
+ }
291
+ if (event.type !== 'session.idle')
292
+ return;
293
+ const sessionId = event.properties?.sessionID;
294
+ if (!sessionId)
295
+ return;
296
+ await notifyRootSession(sessionId);
297
+ },
298
+ };
299
+ }
300
+ //# sourceMappingURL=notifier-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifier-plugin.js","sourceRoot":"","sources":["../src/notifier-plugin.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@chime-io/plugin-opencode",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "dependencies": {
8
+ "@chime-io/core": "workspace:*",
9
+ "@chime-io/channel-telegram": "workspace:*"
10
+ },
11
+ "publishConfig": { "access": "public" },
12
+ "scripts": {
13
+ "build": "tsc -b",
14
+ "build:watch": "tsc -b --watch --preserveWatchOutput",
15
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
16
+ "typecheck": "tsc -b --pretty false"
17
+ }
18
+ }
package/src/format.ts ADDED
@@ -0,0 +1,247 @@
1
+ const MAX_RESULT_LENGTH = 160;
2
+
3
+ export interface SessionSummary {
4
+ additions?: number;
5
+ deletions?: number;
6
+ files?: number;
7
+ }
8
+
9
+ export interface OpenCodeSession {
10
+ id: string;
11
+ title?: string;
12
+ slug?: string;
13
+ parentID?: string | null;
14
+ summary?: SessionSummary;
15
+ }
16
+
17
+ export interface OpenCodeErrorLike {
18
+ message?: string;
19
+ name?: string;
20
+ data?: {
21
+ message?: string;
22
+ };
23
+ }
24
+
25
+ export interface OpenCodeAssistantInfo {
26
+ role?: string;
27
+ error?: OpenCodeErrorLike | string;
28
+ }
29
+
30
+ export interface OpenCodeTextPart {
31
+ type: 'text';
32
+ text?: string;
33
+ }
34
+
35
+ export interface OpenCodeToolPart {
36
+ type: 'tool';
37
+ state?: {
38
+ status?: string;
39
+ title?: string;
40
+ error?: OpenCodeErrorLike | string;
41
+ };
42
+ }
43
+
44
+ export interface OpenCodePatchPart {
45
+ type: 'patch';
46
+ files?: string[];
47
+ }
48
+
49
+ export type OpenCodeMessagePart =
50
+ | OpenCodeTextPart
51
+ | OpenCodeToolPart
52
+ | OpenCodePatchPart;
53
+
54
+ export interface OpenCodeConversationMessage {
55
+ info?: OpenCodeAssistantInfo;
56
+ parts?: OpenCodeMessagePart[];
57
+ }
58
+
59
+ export interface OpenCodeNotification {
60
+ agent: 'opencode';
61
+ kind: 'session.completed' | 'session.error' | 'interaction.question' | 'interaction.permission';
62
+ title: string;
63
+ lines: string[];
64
+ metadata: { sessionId: string };
65
+ }
66
+
67
+ export interface OpenCodeEventFormatter {
68
+ formatSessionCompleted(session: OpenCodeSession): Promise<OpenCodeNotification>;
69
+ formatSessionError(session: OpenCodeSession, errorMessage?: string): Promise<OpenCodeNotification>;
70
+ formatQuestion(session: OpenCodeSession, questionText?: string): OpenCodeNotification;
71
+ formatPermission(session: OpenCodeSession, title?: string): OpenCodeNotification;
72
+ }
73
+
74
+ export interface CreateOpenCodeEventFormatterOptions {
75
+ listMessages(sessionId: string): Promise<OpenCodeConversationMessage[]>;
76
+ }
77
+
78
+ function normalizeSummaryText(value: unknown): string {
79
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
80
+ }
81
+
82
+ export function truncateText(value: unknown, maxLength = MAX_RESULT_LENGTH): string {
83
+ const normalized = normalizeSummaryText(value);
84
+ if (!normalized) return '';
85
+ if (normalized.length <= maxLength) return normalized;
86
+ return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
87
+ }
88
+
89
+ export function formatChangeSummary(session: Pick<OpenCodeSession, 'summary'>): string {
90
+ if (!session.summary) return '';
91
+
92
+ const { additions = 0, deletions = 0, files = 0 } = session.summary;
93
+ const parts: string[] = [];
94
+
95
+ if (additions > 0) parts.push(`+${additions}`);
96
+ if (deletions > 0) parts.push(`-${deletions}`);
97
+ if (files > 0) parts.push(`${files} file${files === 1 ? '' : 's'}`);
98
+
99
+ return parts.join(' · ');
100
+ }
101
+
102
+ export function extractErrorMessage(error: OpenCodeErrorLike | string | null | undefined): string {
103
+ if (!error) return '';
104
+ if (typeof error === 'string') return truncateText(error);
105
+
106
+ if (typeof error.data?.message === 'string' && error.data.message.trim()) {
107
+ return truncateText(error.data.message);
108
+ }
109
+
110
+ if (typeof error.message === 'string' && error.message.trim()) {
111
+ return truncateText(error.message);
112
+ }
113
+
114
+ if (typeof error.name === 'string' && error.name.trim()) {
115
+ return truncateText(error.name);
116
+ }
117
+
118
+ return truncateText(String(error));
119
+ }
120
+
121
+ export function extractLastResultFromMessages(messages: OpenCodeConversationMessage[]): string {
122
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
123
+ const message = messages[messageIndex];
124
+ if (message?.info?.role !== 'assistant') continue;
125
+
126
+ const parts = Array.isArray(message.parts) ? message.parts : [];
127
+ for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
128
+ const part = parts[partIndex];
129
+
130
+ if (part?.type === 'text' && part.text) {
131
+ const text = truncateText(part.text);
132
+ if (text) return text;
133
+ }
134
+
135
+ if (
136
+ part?.type === 'tool' &&
137
+ part.state?.status === 'completed' &&
138
+ part.state.title
139
+ ) {
140
+ return truncateText(`工具:${part.state.title}`);
141
+ }
142
+
143
+ if (
144
+ part?.type === 'patch' &&
145
+ Array.isArray(part.files) &&
146
+ part.files.length > 0
147
+ ) {
148
+ const listedFiles = part.files.slice(0, 2).join(', ');
149
+ const remainder = part.files.length > 2 ? ` 等${part.files.length}个文件` : '';
150
+ return truncateText(`修改:${listedFiles}${remainder}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ return '';
156
+ }
157
+
158
+ export function extractLastErrorFromMessages(messages: OpenCodeConversationMessage[]): string {
159
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
160
+ const message = messages[messageIndex];
161
+ if (message?.info?.role !== 'assistant') continue;
162
+
163
+ const assistantError = extractErrorMessage(message.info.error);
164
+ if (assistantError) return assistantError;
165
+
166
+ const parts = Array.isArray(message.parts) ? message.parts : [];
167
+ for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
168
+ const part = parts[partIndex];
169
+
170
+ if (part?.type === 'tool' && part.state?.status === 'error') {
171
+ const toolError = extractErrorMessage(part.state.error);
172
+ if (toolError) return toolError;
173
+ if (part.state.title) return truncateText(`工具失败:${part.state.title}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ return '';
179
+ }
180
+
181
+ function getShortSessionId(sessionId: string): string {
182
+ return String(sessionId).slice(0, 8);
183
+ }
184
+
185
+ export function createOpenCodeEventFormatter({
186
+ listMessages,
187
+ }: CreateOpenCodeEventFormatterOptions): OpenCodeEventFormatter {
188
+ return {
189
+ async formatSessionCompleted(session) {
190
+ const messages = await listMessages(session.id);
191
+ const changeSummary = formatChangeSummary(session);
192
+ const lastResult = extractLastResultFromMessages(messages);
193
+
194
+ return {
195
+ agent: 'opencode',
196
+ kind: 'session.completed',
197
+ title: `OpenCode · ${session.title ?? session.slug ?? getShortSessionId(session.id)}`,
198
+ lines: [
199
+ changeSummary || `主会话已完成 · session ${getShortSessionId(session.id)}`,
200
+ lastResult,
201
+ ].filter((line): line is string => Boolean(line)),
202
+ metadata: { sessionId: session.id },
203
+ };
204
+ },
205
+
206
+ async formatSessionError(session, errorMessage) {
207
+ const messages = await listMessages(session.id);
208
+ const resolvedError = errorMessage || extractLastErrorFromMessages(messages);
209
+
210
+ return {
211
+ agent: 'opencode',
212
+ kind: 'session.error',
213
+ title: `OpenCode · ${session.title ?? session.slug ?? getShortSessionId(session.id)}`,
214
+ lines: [`出错啦:${resolvedError || 'Unknown error'}`],
215
+ metadata: { sessionId: session.id },
216
+ };
217
+ },
218
+
219
+ formatQuestion(session, questionText) {
220
+ return {
221
+ agent: 'opencode',
222
+ kind: 'interaction.question',
223
+ title: `OpenCode · ${session.title ?? session.slug ?? getShortSessionId(session.id)}`,
224
+ lines: [
225
+ questionText
226
+ ? `Agent 正在等你回答:${truncateText(questionText)}`
227
+ : 'Agent 正在等你回答',
228
+ ],
229
+ metadata: { sessionId: session.id },
230
+ };
231
+ },
232
+
233
+ formatPermission(session, title) {
234
+ return {
235
+ agent: 'opencode',
236
+ kind: 'interaction.permission',
237
+ title: `OpenCode · ${session.title ?? session.slug ?? getShortSessionId(session.id)}`,
238
+ lines: [
239
+ title
240
+ ? `Agent 需要你的确认:${truncateText(title)}`
241
+ : 'Agent 需要你的确认',
242
+ ],
243
+ metadata: { sessionId: session.id },
244
+ };
245
+ },
246
+ };
247
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { createNotifier } from "@telnotify/core";
2
+ import { createTelegramChannel } from "@telnotify/telegram";
3
+
4
+ import {
5
+ createOpenCodeNotifierPlugin,
6
+ type Logger,
7
+ type OpenCodeClient,
8
+ } from "./notifier-plugin.js";
9
+
10
+ function createLogger(client: OpenCodeClient): Logger {
11
+ return {
12
+ async warn(message, extra) {
13
+ if (!client.app?.log) return;
14
+ await client.app.log({
15
+ body: {
16
+ service: "telnotify-opencode-plugin",
17
+ level: "warn",
18
+ message,
19
+ ...(extra ? { extra } : {}),
20
+ },
21
+ });
22
+ },
23
+ };
24
+ }
25
+
26
+ export async function OpenCodeTelegramPlugin({
27
+ client,
28
+ }: {
29
+ client: OpenCodeClient;
30
+ }): Promise<ReturnType<typeof createOpenCodeNotifierPlugin>> {
31
+ const token = process.env.TELEGRAM_BOT_TOKEN;
32
+ const userId = process.env.TELEGRAM_USER_ID;
33
+ const parseMode =
34
+ process.env.TELEGRAM_PARSE_MODE === "MarkdownV2"
35
+ ? "MarkdownV2"
36
+ : process.env.TELEGRAM_PARSE_MODE === "Markdown"
37
+ ? "Markdown"
38
+ : "HTML";
39
+ const silent = process.env.TELEGRAM_SILENT === "1";
40
+
41
+ const notifier = createNotifier({
42
+ channels: [
43
+ createTelegramChannel({
44
+ token: token ?? "",
45
+ userId: userId ?? "",
46
+ parseMode,
47
+ silent,
48
+ }),
49
+ ],
50
+ });
51
+
52
+ return createOpenCodeNotifierPlugin({
53
+ client,
54
+ notifier,
55
+ logger: createLogger(client),
56
+ });
57
+ }
58
+
59
+ export default OpenCodeTelegramPlugin;