opencode-discord 1.8.1 → 1.8.3
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/index.ts +112 -35
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -5,14 +5,16 @@ const CLIENT_ID = process.env.DISCORD_CLIENT_ID ?? "1486144419940929676"
|
|
|
5
5
|
const RECONNECT_DELAY = 5_000
|
|
6
6
|
const MAX_RECONNECT_ATTEMPTS = 5
|
|
7
7
|
|
|
8
|
-
type Activity = "idle" | "thinking" | "editing" | "running" | "reading"
|
|
9
|
-
type LogLevel = "debug" | "info" | "error" | "warn"
|
|
8
|
+
type Activity = "idle" | "thinking" | "editing" | "running" | "reading" | "approving" | "commanding"
|
|
10
9
|
|
|
11
10
|
function basename(filePath?: string): string | undefined {
|
|
12
11
|
if (!filePath) return undefined
|
|
13
12
|
const normalized = filePath.replace(/\\/g, "/")
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
return normalized.split("/").pop() || undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function props(event: { properties?: Record<string, unknown> }) {
|
|
17
|
+
return (event.properties ?? {}) as Record<string, any>
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
@@ -24,26 +26,39 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
24
26
|
let lastEditedFile: string | undefined
|
|
25
27
|
let activity: Activity = "idle"
|
|
26
28
|
let sessionCount = 0
|
|
29
|
+
let filesChanged = 0
|
|
30
|
+
let lastCommand: string | undefined
|
|
31
|
+
let modelName: string | undefined
|
|
32
|
+
let sessionTokens = { input: 0, output: 0 }
|
|
27
33
|
let reconnectAttempts = 0
|
|
28
34
|
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
|
|
29
35
|
let pendingPresence = false
|
|
36
|
+
let presenceDirty = false
|
|
30
37
|
let destroyed = false
|
|
31
38
|
|
|
32
|
-
function log(level:
|
|
33
|
-
sdk.app.log({
|
|
34
|
-
body: {
|
|
35
|
-
service: "discord-status",
|
|
36
|
-
level,
|
|
37
|
-
message,
|
|
38
|
-
extra,
|
|
39
|
-
},
|
|
40
|
-
}).catch(() => {})
|
|
39
|
+
function log(level: string, message: string, extra?: Record<string, unknown>) {
|
|
40
|
+
sdk.app.log({ body: { service: "discord-status", level, message, extra } }).catch(() => {})
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function formatTokens(n: number): string {
|
|
44
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
45
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
|
46
|
+
return String(n)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resetSession() {
|
|
50
|
+
lastEditedFile = undefined
|
|
51
|
+
lastCommand = undefined
|
|
52
|
+
filesChanged = 0
|
|
53
|
+
sessionTokens = { input: 0, output: 0 }
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
function updatePresence() {
|
|
46
|
-
if (!connected
|
|
57
|
+
if (!connected) return
|
|
58
|
+
if (pendingPresence) {
|
|
59
|
+
presenceDirty = true
|
|
60
|
+
return
|
|
61
|
+
}
|
|
47
62
|
pendingPresence = true
|
|
48
63
|
|
|
49
64
|
let details: string
|
|
@@ -70,13 +85,24 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
70
85
|
case "reading":
|
|
71
86
|
details = lastEditedFile ? `Reading ${lastEditedFile}` : "Reading files"
|
|
72
87
|
break
|
|
88
|
+
case "approving":
|
|
89
|
+
details = "Waiting for approval..."
|
|
90
|
+
break
|
|
91
|
+
case "commanding":
|
|
92
|
+
details = lastCommand ? `Running /${lastCommand}` : "Running command"
|
|
93
|
+
break
|
|
73
94
|
default:
|
|
74
95
|
details = "Session active"
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
const stateParts = [currentProject]
|
|
99
|
+
if (filesChanged > 0) stateParts.push(`${filesChanged} file${filesChanged !== 1 ? "s" : ""} changed`)
|
|
100
|
+
const totalTokens = sessionTokens.input + sessionTokens.output
|
|
101
|
+
if (totalTokens > 0) stateParts.push(`${formatTokens(totalTokens)} tokens`)
|
|
102
|
+
state = stateParts.join(" · ")
|
|
103
|
+
|
|
104
|
+
smallImageKey = activity === "thinking" ? "thinking" : activity === "approving" ? "idle" : "active"
|
|
105
|
+
smallImageText = activity === "thinking" ? "Generating" : activity === "approving" ? "Needs approval" : "Active"
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
rpc.user?.setActivity({
|
|
@@ -84,13 +110,21 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
84
110
|
state,
|
|
85
111
|
startTimestamp,
|
|
86
112
|
largeImageKey: "opencode",
|
|
87
|
-
largeImageText:
|
|
113
|
+
largeImageText: modelName
|
|
114
|
+
? `OpenCode · ${modelName}`
|
|
115
|
+
: `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
|
|
88
116
|
smallImageKey,
|
|
89
117
|
smallImageText,
|
|
90
118
|
})
|
|
91
119
|
.then(() => log("debug", "Presence updated", { details, state }))
|
|
92
120
|
.catch((e) => log("error", "Failed to set presence", { error: String(e) }))
|
|
93
|
-
.finally(() => {
|
|
121
|
+
.finally(() => {
|
|
122
|
+
pendingPresence = false
|
|
123
|
+
if (presenceDirty) {
|
|
124
|
+
presenceDirty = false
|
|
125
|
+
updatePresence()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
function scheduleReconnect() {
|
|
@@ -135,8 +169,8 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
135
169
|
rpc.destroy().catch(() => {})
|
|
136
170
|
})
|
|
137
171
|
|
|
172
|
+
log("info", "Logging in to Discord RPC...", { clientId: CLIENT_ID, project: currentProject })
|
|
138
173
|
try {
|
|
139
|
-
log("info", "Logging in to Discord RPC...")
|
|
140
174
|
await rpc.login()
|
|
141
175
|
reconnectAttempts = 0
|
|
142
176
|
log("info", "Discord RPC login successful")
|
|
@@ -153,9 +187,23 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
153
187
|
sessionActive = true
|
|
154
188
|
sessionCount += 1
|
|
155
189
|
startTimestamp = new Date()
|
|
156
|
-
|
|
190
|
+
resetSession()
|
|
157
191
|
activity = "thinking"
|
|
158
192
|
break
|
|
193
|
+
case "session.updated":
|
|
194
|
+
sessionActive = true
|
|
195
|
+
if (activity === "idle") activity = "thinking"
|
|
196
|
+
break
|
|
197
|
+
case "session.status": {
|
|
198
|
+
const status = props(event).status
|
|
199
|
+
if (status === "busy" || status === "retry") {
|
|
200
|
+
sessionActive = true
|
|
201
|
+
activity = "thinking"
|
|
202
|
+
} else if (status === "idle") {
|
|
203
|
+
activity = "idle"
|
|
204
|
+
}
|
|
205
|
+
break
|
|
206
|
+
}
|
|
159
207
|
case "session.idle":
|
|
160
208
|
activity = "idle"
|
|
161
209
|
break
|
|
@@ -165,26 +213,59 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
165
213
|
break
|
|
166
214
|
case "session.deleted":
|
|
167
215
|
sessionActive = false
|
|
168
|
-
|
|
216
|
+
resetSession()
|
|
169
217
|
activity = "idle"
|
|
170
|
-
if (connected) {
|
|
171
|
-
rpc.user?.clearActivity().catch(() => {})
|
|
172
|
-
}
|
|
218
|
+
if (connected) rpc.user?.clearActivity().catch(() => {})
|
|
173
219
|
return
|
|
174
220
|
case "session.compacted":
|
|
175
221
|
activity = "thinking"
|
|
176
222
|
break
|
|
177
|
-
case "file.edited":
|
|
223
|
+
case "file.edited":
|
|
178
224
|
activity = "editing"
|
|
179
|
-
|
|
180
|
-
|
|
225
|
+
lastEditedFile = basename(props(event).file) ?? lastEditedFile
|
|
226
|
+
break
|
|
227
|
+
case "session.diff": {
|
|
228
|
+
const diffs = props(event).diff
|
|
229
|
+
if (Array.isArray(diffs)) filesChanged = diffs.length
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
case "message.part.updated": {
|
|
233
|
+
const part = props(event).part
|
|
234
|
+
if (part?.type === "step-finish" && part.tokens) {
|
|
235
|
+
sessionTokens.input += part.tokens.input ?? 0
|
|
236
|
+
sessionTokens.output += part.tokens.output ?? 0
|
|
237
|
+
}
|
|
238
|
+
if (sessionActive && activity !== "editing" && activity !== "running") {
|
|
239
|
+
activity = "thinking"
|
|
240
|
+
}
|
|
181
241
|
break
|
|
182
242
|
}
|
|
243
|
+
case "pty.created":
|
|
244
|
+
activity = "running"
|
|
245
|
+
break
|
|
246
|
+
case "pty.exited":
|
|
247
|
+
activity = sessionActive ? "thinking" : "idle"
|
|
248
|
+
break
|
|
249
|
+
case "permission.updated":
|
|
250
|
+
activity = "approving"
|
|
251
|
+
break
|
|
252
|
+
case "permission.replied":
|
|
253
|
+
activity = sessionActive ? "thinking" : "idle"
|
|
254
|
+
break
|
|
255
|
+
case "command.executed":
|
|
256
|
+
lastCommand = props(event).name
|
|
257
|
+
activity = "commanding"
|
|
258
|
+
break
|
|
183
259
|
}
|
|
184
260
|
|
|
185
261
|
updatePresence()
|
|
186
262
|
},
|
|
187
263
|
|
|
264
|
+
"chat.params": async (input) => {
|
|
265
|
+
const name = input.model?.name ?? input.model?.id
|
|
266
|
+
if (name) modelName = name
|
|
267
|
+
},
|
|
268
|
+
|
|
188
269
|
"tool.execute.before": async (input, output) => {
|
|
189
270
|
const args = output.args as { filePath?: string } | undefined
|
|
190
271
|
|
|
@@ -202,15 +283,11 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
|
|
|
202
283
|
},
|
|
203
284
|
|
|
204
285
|
"tool.execute.after": async (input) => {
|
|
205
|
-
if (input.tool === "
|
|
206
|
-
activity = "editing"
|
|
207
|
-
const filePath = input.args?.filePath as string | undefined
|
|
208
|
-
lastEditedFile = basename(filePath) ?? lastEditedFile
|
|
209
|
-
} else if (input.tool === "bash") {
|
|
286
|
+
if (input.tool === "bash") {
|
|
210
287
|
activity = sessionActive ? "thinking" : "idle"
|
|
211
288
|
}
|
|
212
289
|
|
|
213
290
|
updatePresence()
|
|
214
291
|
},
|
|
215
292
|
}
|
|
216
|
-
}
|
|
293
|
+
}
|