moshi-opencode-hooks 1.0.15 → 1.0.17

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 +61 -60
  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,
@@ -124,13 +114,45 @@ function formatToolName(toolName: string): string {
124
114
 
125
115
  function formatModelName(model: string | undefined): string | undefined {
126
116
  if (!model) return undefined
127
- return model.replace(/^claude-/, "")
117
+ return model.split("/").pop()
118
+ }
119
+
120
+ async function getContextPercent(sessionId: string, client: Parameters<Plugin>[0]["client"]): Promise<number | undefined> {
121
+ try {
122
+ const sessionRes = await client.session.get({ path: { id: sessionId } })
123
+ const sessionData = (sessionRes as any).data
124
+ if (!sessionData) return undefined
125
+
126
+ const model = sessionData.model as { limit?: { context?: number } } | undefined
127
+ const contextLimit = model?.limit?.context
128
+ if (!contextLimit) return undefined
129
+
130
+ const messagesRes = await client.session.messages({ path: { id: sessionId }, query: { limit: 1 } })
131
+ const messagesData = (messagesRes as any).data
132
+ const messages = messagesData?.messages ?? messagesData
133
+ const lastMsg = Array.isArray(messages) ? messages[messages.length - 1] : messages
134
+ if (!lastMsg?.tokens?.input) return undefined
135
+
136
+ const totalInputTokens = lastMsg.tokens.input
137
+ return Math.min(100, Math.round((totalInputTokens / contextLimit) * 100))
138
+ } catch {
139
+ }
140
+ return undefined
128
141
  }
129
142
 
130
143
  const pkg = await import("./package.json", { assert: { type: "json" } })
131
144
  const VERSION = pkg.default.version
132
145
 
133
146
  export const MoshiHooks: Plugin = async ({ client, directory }) => {
147
+ setTimeout(() => {
148
+ client.tui.showToast({
149
+ body: {
150
+ message: `moshi-opencode-hooks v${VERSION} is active`,
151
+ variant: "info",
152
+ },
153
+ })
154
+ }, 5000)
155
+
134
156
  const setupEventSubscription = async () => {
135
157
  try {
136
158
  const events = await client.event.subscribe()
@@ -140,24 +162,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
140
162
 
141
163
  const sessionId = (event as any).sessionId ?? "unknown"
142
164
  const projectName = directory ? basename(directory) : undefined
143
- const state = await readState(sessionId)
144
-
145
- if (event.type === "session.created") {
146
- const isSubagent = await isSubagentSession(sessionId)
147
- await writeState(sessionId, {
148
- model: (event as any).properties?.model,
149
- isSubagent,
150
- })
151
- continue
152
- }
153
165
 
154
166
  if (event.type === "session.idle") {
155
- if (state.isSubagent) continue
156
-
157
- const now = Date.now() / 1000
158
- if (state.lastStopTime && now - state.lastStopTime < 5) continue
159
-
160
- await writeState(sessionId, { lastStopTime: now })
167
+ if (await isSubagentSession(sessionId)) continue
161
168
 
162
169
  const evt: AgentEvent = {
163
170
  source: "opencode",
@@ -168,8 +175,8 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
168
175
  message: "",
169
176
  eventId: crypto.randomUUID(),
170
177
  projectName,
171
- modelName: formatModelName(state.model),
172
- toolName: state.lastToolName,
178
+ modelName: formatModelName(await getOrLoadModel(client, sessionId)),
179
+ contextPercent: await getContextPercent(sessionId, client),
173
180
  }
174
181
  await sendAgentEvent(client, token, evt)
175
182
  }
@@ -193,11 +200,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
193
200
  }
194
201
 
195
202
  return {
196
- "tui.toast.show": async (_input: unknown, output: any) => {
197
- output.message = `moshi-opencode-hooks v${VERSION} is active`
198
- output.type = "info"
199
- },
200
-
201
203
  "tool.execute.before": async (input, output) => {
202
204
  const token = await loadToken()
203
205
  if (!token) return
@@ -206,9 +208,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
206
208
  if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
207
209
  if (await isSubagentSession(sessionID)) return
208
210
 
209
- await writeState(sessionID, { lastToolName: tool })
210
-
211
- const state = await readState(sessionID)
212
211
  const projectName = directory ? basename(directory) : undefined
213
212
 
214
213
  if (tool === "question") {
@@ -226,8 +225,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
226
225
  message: lines.join("\n---\n").slice(0, 512),
227
226
  eventId: crypto.randomUUID(),
228
227
  projectName,
229
- modelName: formatModelName(state.model),
228
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
230
229
  toolName: tool,
230
+ contextPercent: await getContextPercent(sessionID, client),
231
231
  }
232
232
  await sendAgentEvent(client, token, evt)
233
233
  return
@@ -242,8 +242,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
242
242
  message: "",
243
243
  eventId: crypto.randomUUID(),
244
244
  projectName,
245
- modelName: formatModelName(state.model),
245
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
246
246
  toolName: tool,
247
+ contextPercent: await getContextPercent(sessionID, client),
247
248
  }
248
249
  await sendAgentEvent(client, token, evt)
249
250
  },
@@ -258,7 +259,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
258
259
 
259
260
  if (tool === "question") return
260
261
 
261
- const state = await readState(sessionID)
262
262
  const projectName = directory ? basename(directory) : undefined
263
263
 
264
264
  const evt: AgentEvent = {
@@ -270,8 +270,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
270
270
  message: "",
271
271
  eventId: crypto.randomUUID(),
272
272
  projectName,
273
- modelName: formatModelName(state.model),
273
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
274
274
  toolName: tool,
275
+ contextPercent: await getContextPercent(sessionID, client),
275
276
  }
276
277
  await sendAgentEvent(client, token, evt)
277
278
  },
@@ -281,8 +282,7 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
281
282
  if (!token) return
282
283
 
283
284
  const sessionID = (input as any).sessionID ?? "unknown"
284
- const state = await readState(sessionID)
285
- if (state.isSubagent) return
285
+ if (await isSubagentSession(sessionID)) return
286
286
 
287
287
  const projectName = directory ? basename(directory) : undefined
288
288
 
@@ -297,7 +297,8 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
297
297
  message: prompt.slice(0, 256),
298
298
  eventId: crypto.randomUUID(),
299
299
  projectName,
300
- modelName: formatModelName(state.model),
300
+ modelName: formatModelName(await getOrLoadModel(client, sessionID)),
301
+ contextPercent: await getContextPercent(sessionID, client),
301
302
  }
302
303
  await sendAgentEvent(client, token, evt)
303
304
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshi-opencode-hooks",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "OpenCode plugin for Moshi live activity integration",
5
5
  "repository": {
6
6
  "type": "git",