moshi-opencode-hooks 1.0.16 → 1.0.18

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.
Files changed (2) hide show
  1. package/index.ts +79 -56
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -6,13 +6,6 @@ const TOKEN_PATH = `${homedir()}/.config/moshi/token`
6
6
  const API_URL = "https://api.getmoshi.app/api/v1/agent-events"
7
7
  const INTERESTING_TOOLS = new Set(["bash", "edit", "write", "read", "glob", "grep", "task", "question", "apply_patch", "webfetch", "websearch"])
8
8
 
9
- interface HookState {
10
- model?: string
11
- lastToolName?: string
12
- lastStopTime?: number
13
- isSubagent?: boolean
14
- }
15
-
16
9
  interface AgentEvent {
17
10
  source: "opencode"
18
11
  eventType: "pre_tool" | "post_tool" | "notification" | "stop"
@@ -27,25 +20,6 @@ interface AgentEvent {
27
20
  contextPercent?: number
28
21
  }
29
22
 
30
- function statePath(sessionId: string): string {
31
- return `/tmp/moshi-opencode-hook-${sessionId}.json`
32
- }
33
-
34
- async function readState(sessionId: string): Promise<HookState> {
35
- try {
36
- const file = Bun.file(statePath(sessionId))
37
- if (!file.size) return {}
38
- return await file.json()
39
- } catch {
40
- return {}
41
- }
42
- }
43
-
44
- async function writeState(sessionId: string, patch: Partial<HookState>): Promise<void> {
45
- const existing = await readState(sessionId)
46
- await Bun.write(statePath(sessionId), JSON.stringify({ ...existing, ...patch }))
47
- }
48
-
49
23
  async function loadToken(): Promise<string | null> {
50
24
  try {
51
25
  const text = await Bun.file(TOKEN_PATH).text()
@@ -95,6 +69,22 @@ async function sendEvent(token: string, event: AgentEvent): Promise<Response> {
95
69
  }
96
70
  }
97
71
 
72
+ async function getModel(client: Parameters<Plugin>[0]["client"], sessionId: string): Promise<string | undefined> {
73
+ try {
74
+ const res = await (client.session.get as any)({ sessionID: sessionId })
75
+ return res.data?.model
76
+ } catch {
77
+ return undefined
78
+ }
79
+ }
80
+
81
+ async function getOrLoadModel(
82
+ client: Parameters<Plugin>[0]["client"],
83
+ sessionId: string,
84
+ ): Promise<string | undefined> {
85
+ return await getModel(client, sessionId)
86
+ }
87
+
98
88
  async function sendAgentEvent(
99
89
  client: Parameters<Plugin>[0]["client"],
100
90
  token: string,
@@ -122,9 +112,58 @@ function formatToolName(toolName: string): string {
122
112
  return toolName.charAt(0).toUpperCase() + toolName.slice(1)
123
113
  }
124
114
 
115
+ function formatToolDescription(tool: string, args: any): string {
116
+ switch (tool) {
117
+ case "bash":
118
+ return args?.description?.slice(0, 200) ?? ""
119
+ case "edit":
120
+ return args?.filePath ?? ""
121
+ case "write":
122
+ return args?.filePath ?? ""
123
+ case "read":
124
+ return args?.filePath ?? ""
125
+ case "glob":
126
+ return args?.pattern ?? ""
127
+ case "grep":
128
+ return args?.pattern ?? ""
129
+ case "task":
130
+ return args?.description?.slice(0, 100) ?? ""
131
+ case "apply_patch":
132
+ return args?.description?.slice(0, 200) ?? ""
133
+ case "webfetch":
134
+ case "websearch":
135
+ return args?.url ?? ""
136
+ default:
137
+ return ""
138
+ }
139
+ }
140
+
125
141
  function formatModelName(model: string | undefined): string | undefined {
126
142
  if (!model) return undefined
127
- return model.replace(/^claude-/, "")
143
+ return model.split("/").pop()
144
+ }
145
+
146
+ async function getContextPercent(sessionId: string, client: Parameters<Plugin>[0]["client"]): Promise<number | undefined> {
147
+ try {
148
+ const sessionRes = await (client.session.get as any)({ sessionID: sessionId })
149
+ const sessionData = sessionRes.data
150
+ if (!sessionData) return undefined
151
+
152
+ const model = sessionData.model as { limit?: { context?: number } } | undefined
153
+ const contextLimit = model?.limit?.context
154
+ if (!contextLimit) return undefined
155
+
156
+ const messagesRes = await (client.session.messages as any)({ sessionID: sessionId, limit: 1 })
157
+ const messagesData = messagesRes.data
158
+ const messages = messagesData?.messages ?? messagesData
159
+ const lastMsg = Array.isArray(messages) ? messages[messages.length - 1] : messages
160
+ if (!lastMsg?.tokens?.input) return undefined
161
+
162
+ const totalInputTokens = lastMsg.tokens.input
163
+ return Math.min(100, Math.round((totalInputTokens / contextLimit) * 100))
164
+ } catch {
165
+ return undefined
166
+ }
128
167
  }
129
168
 
130
169
  const pkg = await import("./package.json", { assert: { type: "json" } })
@@ -149,24 +188,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
149
188
 
150
189
  const sessionId = (event as any).sessionId ?? "unknown"
151
190
  const projectName = directory ? basename(directory) : undefined
152
- const state = await readState(sessionId)
153
-
154
- if (event.type === "session.created") {
155
- const isSubagent = await isSubagentSession(sessionId)
156
- await writeState(sessionId, {
157
- model: (event as any).properties?.model,
158
- isSubagent,
159
- })
160
- continue
161
- }
162
191
 
163
192
  if (event.type === "session.idle") {
164
- if (state.isSubagent) continue
165
-
166
- const now = Date.now() / 1000
167
- if (state.lastStopTime && now - state.lastStopTime < 5) continue
168
-
169
- await writeState(sessionId, { lastStopTime: now })
193
+ if (await isSubagentSession(sessionId)) continue
170
194
 
171
195
  const evt: AgentEvent = {
172
196
  source: "opencode",
@@ -177,8 +201,8 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
177
201
  message: "",
178
202
  eventId: crypto.randomUUID(),
179
203
  projectName,
180
- modelName: formatModelName(state.model),
181
- toolName: state.lastToolName,
204
+ modelName: formatModelName(await getOrLoadModel(client, sessionId)),
205
+ contextPercent: await getContextPercent(sessionId, client),
182
206
  }
183
207
  await sendAgentEvent(client, token, evt)
184
208
  }
@@ -210,9 +234,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
210
234
  if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
211
235
  if (await isSubagentSession(sessionID)) return
212
236
 
213
- await writeState(sessionID, { lastToolName: tool })
214
-
215
- const state = await readState(sessionID)
216
237
  const projectName = directory ? basename(directory) : undefined
217
238
 
218
239
  if (tool === "question") {
@@ -230,8 +251,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
230
251
  message: lines.join("\n---\n").slice(0, 512),
231
252
  eventId: crypto.randomUUID(),
232
253
  projectName,
233
- modelName: formatModelName(state.model),
254
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
234
255
  toolName: tool,
256
+ contextPercent: await getContextPercent(sessionID, client),
235
257
  }
236
258
  await sendAgentEvent(client, token, evt)
237
259
  return
@@ -243,11 +265,12 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
243
265
  sessionId: sessionID,
244
266
  category: "tool_running",
245
267
  title: `Running ${formatToolName(tool)}`,
246
- message: "",
268
+ message: formatToolDescription(tool, output.args),
247
269
  eventId: crypto.randomUUID(),
248
270
  projectName,
249
- modelName: formatModelName(state.model),
271
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
250
272
  toolName: tool,
273
+ contextPercent: await getContextPercent(sessionID, client),
251
274
  }
252
275
  await sendAgentEvent(client, token, evt)
253
276
  },
@@ -262,7 +285,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
262
285
 
263
286
  if (tool === "question") return
264
287
 
265
- const state = await readState(sessionID)
266
288
  const projectName = directory ? basename(directory) : undefined
267
289
 
268
290
  const evt: AgentEvent = {
@@ -274,8 +296,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
274
296
  message: "",
275
297
  eventId: crypto.randomUUID(),
276
298
  projectName,
277
- modelName: formatModelName(state.model),
299
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
278
300
  toolName: tool,
301
+ contextPercent: await getContextPercent(sessionID, client),
279
302
  }
280
303
  await sendAgentEvent(client, token, evt)
281
304
  },
@@ -285,8 +308,7 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
285
308
  if (!token) return
286
309
 
287
310
  const sessionID = (input as any).sessionID ?? "unknown"
288
- const state = await readState(sessionID)
289
- if (state.isSubagent) return
311
+ if (await isSubagentSession(sessionID)) return
290
312
 
291
313
  const projectName = directory ? basename(directory) : undefined
292
314
 
@@ -301,7 +323,8 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
301
323
  message: prompt.slice(0, 256),
302
324
  eventId: crypto.randomUUID(),
303
325
  projectName,
304
- modelName: formatModelName(state.model),
326
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
327
+ contextPercent: await getContextPercent(sessionID, client),
305
328
  }
306
329
  await sendAgentEvent(client, token, evt)
307
330
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshi-opencode-hooks",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "OpenCode plugin for Moshi live activity integration",
5
5
  "repository": {
6
6
  "type": "git",