moshi-opencode-hooks 1.0.1 → 1.0.9

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 +92 -16
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -4,20 +4,19 @@ import type { Plugin } from "@opencode-ai/plugin"
4
4
 
5
5
  const TOKEN_PATH = `${homedir()}/.config/moshi/token`
6
6
  const API_URL = "https://api.getmoshi.app/api/v1/agent-events"
7
- const INTERESTING_TOOLS = new Set(["bash", "edit", "write", "read", "glob", "grep", "task"])
7
+ const INTERESTING_TOOLS = new Set(["bash", "edit", "write", "read", "glob", "grep", "task", "question", "apply_patch"])
8
8
 
9
9
  interface HookState {
10
10
  model?: string
11
11
  lastToolName?: string
12
12
  lastStopTime?: number
13
- sessionStartTime?: number
14
13
  }
15
14
 
16
15
  interface AgentEvent {
17
16
  source: "opencode"
18
- eventType: "user_prompt" | "pre_tool" | "post_tool" | "notification" | "stop" | "agent_turn_complete"
17
+ eventType: "pre_tool" | "post_tool" | "notification" | "stop"
19
18
  sessionId: string
20
- category: "approval_required" | "task_complete" | "tool_running" | "tool_finished" | "info" | "error"
19
+ category: "approval_required" | "task_complete" | "tool_running" | "tool_finished"
21
20
  title: string
22
21
  message: string
23
22
  eventId: string
@@ -55,7 +54,27 @@ async function loadToken(): Promise<string | null> {
55
54
  }
56
55
  }
57
56
 
58
- async function sendEvent(token: string, event: AgentEvent): Promise<void> {
57
+ async function logPluginEvent(
58
+ client: Parameters<Plugin>[0]["client"],
59
+ level: "warn" | "error",
60
+ message: string,
61
+ extra?: Record<string, unknown>,
62
+ ): Promise<void> {
63
+ try {
64
+ await client.app.log({
65
+ body: {
66
+ service: "moshi-hooks",
67
+ level,
68
+ message,
69
+ extra,
70
+ },
71
+ })
72
+ } catch (err) {
73
+ console.error(`[moshi-hooks] ${message}`, err)
74
+ }
75
+ }
76
+
77
+ async function sendEvent(token: string, event: AgentEvent): Promise<Response> {
59
78
  const body = JSON.stringify(event)
60
79
  const headers = {
61
80
  "Content-Type": "application/json",
@@ -69,11 +88,32 @@ async function sendEvent(token: string, event: AgentEvent): Promise<void> {
69
88
  body,
70
89
  signal: AbortSignal.timeout(5000),
71
90
  })
91
+ return res
92
+ } catch (err) {
93
+ throw err
94
+ }
95
+ }
96
+
97
+ async function sendAgentEvent(
98
+ client: Parameters<Plugin>[0]["client"],
99
+ token: string,
100
+ event: AgentEvent,
101
+ ): Promise<void> {
102
+ try {
103
+ const res = await sendEvent(token, event)
72
104
  if (!res.ok && res.status >= 500) {
73
- console.error(`[moshi-hooks] API error: ${res.status}`)
105
+ await logPluginEvent(client, "error", "Moshi API returned server error", {
106
+ status: res.status,
107
+ eventType: event.eventType,
108
+ sessionId: event.sessionId,
109
+ })
74
110
  }
75
111
  } catch (err) {
76
- console.error(`[moshi-hooks] Failed to send event:`, err)
112
+ await logPluginEvent(client, "error", "Failed to send Moshi event", {
113
+ error: err instanceof Error ? err.message : String(err),
114
+ eventType: event.eventType,
115
+ sessionId: event.sessionId,
116
+ })
77
117
  }
78
118
  }
79
119
 
@@ -87,7 +127,7 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
87
127
  const events = await client.event.subscribe()
88
128
  for await (const event of events.stream) {
89
129
  const token = await loadToken()
90
- if (!token) break
130
+ if (!token) continue
91
131
 
92
132
  const sessionId = (event as any).sessionId ?? "unknown"
93
133
  const projectName = directory ? basename(directory) : undefined
@@ -95,7 +135,6 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
95
135
 
96
136
  if (event.type === "session.created") {
97
137
  await writeState(sessionId, {
98
- sessionStartTime: Date.now() / 1000,
99
138
  model: (event as any).properties?.model,
100
139
  })
101
140
  continue
@@ -119,29 +158,63 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
119
158
  modelName: state.model,
120
159
  toolName: state.lastToolName,
121
160
  }
122
- await sendEvent(token, evt)
161
+ await sendAgentEvent(client, token, evt)
123
162
  }
124
163
  }
125
164
  } catch (err) {
126
- console.error(`[moshi-hooks] Event subscription error:`, err)
165
+ await logPluginEvent(client, "error", "Event subscription error", {
166
+ error: err instanceof Error ? err.message : String(err),
167
+ })
127
168
  }
128
169
  }
129
170
 
130
171
  setupEventSubscription()
131
172
 
173
+ const isSubagentSession = async (sessionID: string): Promise<boolean> => {
174
+ try {
175
+ const res = await (client.session.get as any)({ sessionID })
176
+ return !!(res.data?.parentID)
177
+ } catch {
178
+ return false
179
+ }
180
+ }
181
+
132
182
  return {
133
- "tool.execute.before": async (input, _output) => {
183
+ "tool.execute.before": async (input, output) => {
134
184
  const token = await loadToken()
135
185
  if (!token) return
136
186
 
137
187
  const { tool, sessionID } = input
138
188
  if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
189
+ if (await isSubagentSession(sessionID)) return
139
190
 
140
191
  await writeState(sessionID, { lastToolName: tool })
141
192
 
142
193
  const state = await readState(sessionID)
143
194
  const projectName = directory ? basename(directory) : undefined
144
195
 
196
+ if (tool === "question") {
197
+ const questions: any[] = output.args?.questions ?? []
198
+ const lines = questions.map((q) => {
199
+ const opts = q.options?.map((o: any) => ` - ${o.label}`).join("\n") ?? ""
200
+ return `${q.header}: ${q.question}\n${opts}`
201
+ })
202
+ const evt: AgentEvent = {
203
+ source: "opencode",
204
+ eventType: "notification",
205
+ sessionId: sessionID,
206
+ category: "approval_required",
207
+ title: "Question",
208
+ message: lines.join("\n---\n").slice(0, 512),
209
+ eventId: crypto.randomUUID(),
210
+ projectName,
211
+ modelName: state.model,
212
+ toolName: tool,
213
+ }
214
+ await sendAgentEvent(client, token, evt)
215
+ return
216
+ }
217
+
145
218
  const evt: AgentEvent = {
146
219
  source: "opencode",
147
220
  eventType: "pre_tool",
@@ -154,7 +227,7 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
154
227
  modelName: state.model,
155
228
  toolName: tool,
156
229
  }
157
- await sendEvent(token, evt)
230
+ await sendAgentEvent(client, token, evt)
158
231
  },
159
232
 
160
233
  "tool.execute.after": async (input, output) => {
@@ -163,6 +236,9 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
163
236
 
164
237
  const { tool, sessionID } = input
165
238
  if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
239
+ if (await isSubagentSession(sessionID)) return
240
+
241
+ if (tool === "question") return
166
242
 
167
243
  const state = await readState(sessionID)
168
244
  const projectName = directory ? basename(directory) : undefined
@@ -179,10 +255,10 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
179
255
  modelName: state.model,
180
256
  toolName: tool,
181
257
  }
182
- await sendEvent(token, evt)
258
+ await sendAgentEvent(client, token, evt)
183
259
  },
184
260
 
185
- "permission.ask": async (input, output) => {
261
+ "permission.asked": async (input: any, _output: any) => {
186
262
  const token = await loadToken()
187
263
  if (!token) return
188
264
 
@@ -203,7 +279,7 @@ export const MoshiHooks: Plugin = async ({ client, directory }) => {
203
279
  projectName,
204
280
  modelName: state.model,
205
281
  }
206
- await sendEvent(token, evt)
282
+ await sendAgentEvent(client, token, evt)
207
283
  },
208
284
  }
209
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshi-opencode-hooks",
3
- "version": "1.0.1",
3
+ "version": "1.0.9",
4
4
  "description": "OpenCode plugin for Moshi live activity integration",
5
5
  "repository": {
6
6
  "type": "git",