moshi-opencode-hooks 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,25 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: oven-sh/setup-bun@v2
17
+ with:
18
+ bun-version: latest
19
+ - run: bun install
20
+ - run: bun run build
21
+ - run: |
22
+ echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
23
+ npm publish --provenance --access public
24
+ env:
25
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CLAUDE.md ADDED
@@ -0,0 +1,17 @@
1
+ # moshi-opencode-hooks
2
+
3
+ OpenCode plugin that sends events to Moshi for live activity integration.
4
+
5
+ ## Building
6
+
7
+ ```bash
8
+ bun run index.ts # Test run
9
+ bun tsc --noEmit # Type check
10
+ ```
11
+
12
+ ## Publishing to npm
13
+
14
+ ```bash
15
+ bun run build
16
+ npm publish
17
+ ```
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # moshi-opencode-hooks
2
+
3
+ OpenCode plugin for Moshi live activity integration. Provides real-time progress updates in the Moshi iOS app when using OpenCode.
4
+
5
+ ## Setup
6
+
7
+ 1. **Install the token** (if not already done with Claude Code):
8
+ ```bash
9
+ bunx moshi-hooks token Xjqyek7li0vXgBnIlvXEn3VJgdhWf8qW
10
+ ```
11
+
12
+ 2. **Add as npm plugin** in `~/.config/opencode/opencode.json`:
13
+ ```json
14
+ {
15
+ "$schema": "https://opencode.ai/config.json",
16
+ "plugin": ["moshi-opencode-hooks"]
17
+ }
18
+ ```
19
+
20
+ Or use a local path for development:
21
+ ```json
22
+ {
23
+ "$schema": "https://opencode.ai/config.json",
24
+ "plugin": ["/path/to/moshi-opencode-hooks"]
25
+ }
26
+ ```
27
+
28
+ ## Events Sent to Moshi
29
+
30
+ - `tool.execute.before/after` - Tool execution (Bash, Edit, Write, Read, Glob, Grep, Task)
31
+ - `permission.ask` - Permission prompts
32
+ - `session.created` - Session start
33
+ - `session.idle` - Task completion
34
+
35
+ ## Requirements
36
+
37
+ - OpenCode
38
+ - Moshi iOS app with Cloud Code hook token
package/bun.lock ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "moshi-opencode-hooks",
7
+ "dependencies": {
8
+ "@opencode-ai/plugin": "^1.2.27",
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "latest",
12
+ },
13
+ "peerDependencies": {
14
+ "typescript": "^5",
15
+ },
16
+ },
17
+ },
18
+ "packages": {
19
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.27", "", { "dependencies": { "@opencode-ai/sdk": "1.2.27", "zod": "4.1.8" } }, "sha512-h+8Bw9v9nghMg7T+SUCTzxlIhOrsTqXW7U0HVLGQST5DjbN7uyCUM51roZWZ8LRjGxzbzFhvPnY1bj8i+ioZyw=="],
20
+
21
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.27", "", {}, "sha512-Wk0o/I+Fo+wE3zgvlJDs8Fb67KlKqX0PrV8dK5adSDkANq6r4Z25zXJg2iOir+a8ntg3rAcpel1OY4FV/TwRUA=="],
22
+
23
+ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
24
+
25
+ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
26
+
27
+ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
28
+
29
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
30
+
31
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
32
+
33
+ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
34
+ }
35
+ }
package/index.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { homedir } from "os"
2
+ import { basename } from "path"
3
+ import type { Plugin } from "@opencode-ai/plugin"
4
+
5
+ const TOKEN_PATH = `${homedir()}/.config/moshi/token`
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"])
8
+
9
+ interface HookState {
10
+ model?: string
11
+ lastToolName?: string
12
+ lastStopTime?: number
13
+ sessionStartTime?: number
14
+ }
15
+
16
+ interface AgentEvent {
17
+ source: "opencode"
18
+ eventType: "user_prompt" | "pre_tool" | "post_tool" | "notification" | "stop" | "agent_turn_complete"
19
+ sessionId: string
20
+ category: "approval_required" | "task_complete" | "tool_running" | "tool_finished" | "info" | "error"
21
+ title: string
22
+ message: string
23
+ eventId: string
24
+ projectName?: string
25
+ modelName?: string
26
+ toolName?: string
27
+ contextPercent?: number
28
+ }
29
+
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
+ async function loadToken(): Promise<string | null> {
50
+ try {
51
+ const text = await Bun.file(TOKEN_PATH).text()
52
+ return text.trim() || null
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ async function sendEvent(token: string, event: AgentEvent): Promise<void> {
59
+ const body = JSON.stringify(event)
60
+ const headers = {
61
+ "Content-Type": "application/json",
62
+ "Authorization": `Bearer ${token}`,
63
+ }
64
+
65
+ try {
66
+ const res = await fetch(API_URL, {
67
+ method: "POST",
68
+ headers,
69
+ body,
70
+ signal: AbortSignal.timeout(5000),
71
+ })
72
+ if (!res.ok && res.status >= 500) {
73
+ console.error(`[moshi-hooks] API error: ${res.status}`)
74
+ }
75
+ } catch (err) {
76
+ console.error(`[moshi-hooks] Failed to send event:`, err)
77
+ }
78
+ }
79
+
80
+ function formatToolName(toolName: string): string {
81
+ return toolName.charAt(0).toUpperCase() + toolName.slice(1)
82
+ }
83
+
84
+ export const MoshiHooks: Plugin = async ({ client, directory }) => {
85
+ const setupEventSubscription = async () => {
86
+ try {
87
+ const events = await client.event.subscribe()
88
+ for await (const event of events.stream) {
89
+ const token = await loadToken()
90
+ if (!token) break
91
+
92
+ const sessionId = (event as any).sessionId ?? "unknown"
93
+ const projectName = directory ? basename(directory) : undefined
94
+ const state = await readState(sessionId)
95
+
96
+ if (event.type === "session.created") {
97
+ await writeState(sessionId, {
98
+ sessionStartTime: Date.now() / 1000,
99
+ model: (event as any).properties?.model,
100
+ })
101
+ continue
102
+ }
103
+
104
+ if (event.type === "session.idle") {
105
+ const now = Date.now() / 1000
106
+ if (state.lastStopTime && now - state.lastStopTime < 5) continue
107
+
108
+ await writeState(sessionId, { lastStopTime: now })
109
+
110
+ const evt: AgentEvent = {
111
+ source: "opencode",
112
+ eventType: "stop",
113
+ sessionId,
114
+ category: "task_complete",
115
+ title: "Task Complete",
116
+ message: "",
117
+ eventId: crypto.randomUUID(),
118
+ projectName,
119
+ modelName: state.model,
120
+ toolName: state.lastToolName,
121
+ }
122
+ await sendEvent(token, evt)
123
+ }
124
+ }
125
+ } catch (err) {
126
+ console.error(`[moshi-hooks] Event subscription error:`, err)
127
+ }
128
+ }
129
+
130
+ setupEventSubscription()
131
+
132
+ return {
133
+ "tool.execute.before": async (input, _output) => {
134
+ const token = await loadToken()
135
+ if (!token) return
136
+
137
+ const { tool, sessionID } = input
138
+ if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
139
+
140
+ await writeState(sessionID, { lastToolName: tool })
141
+
142
+ const state = await readState(sessionID)
143
+ const projectName = directory ? basename(directory) : undefined
144
+
145
+ const evt: AgentEvent = {
146
+ source: "opencode",
147
+ eventType: "pre_tool",
148
+ sessionId: sessionID,
149
+ category: "tool_running",
150
+ title: `Running ${formatToolName(tool)}`,
151
+ message: "",
152
+ eventId: crypto.randomUUID(),
153
+ projectName,
154
+ modelName: state.model,
155
+ toolName: tool,
156
+ }
157
+ await sendEvent(token, evt)
158
+ },
159
+
160
+ "tool.execute.after": async (input, output) => {
161
+ const token = await loadToken()
162
+ if (!token) return
163
+
164
+ const { tool, sessionID } = input
165
+ if (!tool || !INTERESTING_TOOLS.has(tool.toLowerCase())) return
166
+
167
+ const state = await readState(sessionID)
168
+ const projectName = directory ? basename(directory) : undefined
169
+
170
+ const evt: AgentEvent = {
171
+ source: "opencode",
172
+ eventType: "post_tool",
173
+ sessionId: sessionID,
174
+ category: "tool_finished",
175
+ title: `Finished ${formatToolName(tool)}`,
176
+ message: "",
177
+ eventId: crypto.randomUUID(),
178
+ projectName,
179
+ modelName: state.model,
180
+ toolName: tool,
181
+ }
182
+ await sendEvent(token, evt)
183
+ },
184
+
185
+ "permission.ask": async (input, output) => {
186
+ const token = await loadToken()
187
+ if (!token) return
188
+
189
+ const sessionID = (input as any).sessionID ?? "unknown"
190
+ const state = await readState(sessionID)
191
+ const projectName = directory ? basename(directory) : undefined
192
+
193
+ const prompt = (input as any).prompt ?? ""
194
+
195
+ const evt: AgentEvent = {
196
+ source: "opencode",
197
+ eventType: "notification",
198
+ sessionId: sessionID,
199
+ category: "approval_required",
200
+ title: "Permission Required",
201
+ message: prompt.slice(0, 256),
202
+ eventId: crypto.randomUUID(),
203
+ projectName,
204
+ modelName: state.model,
205
+ }
206
+ await sendEvent(token, evt)
207
+ },
208
+ }
209
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "moshi-opencode-hooks",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode plugin for Moshi live activity integration",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/goniz/moshi-opencode-hooks"
8
+ },
9
+ "module": "index.ts",
10
+ "type": "module",
11
+ "main": "index.ts",
12
+ "bin": {
13
+ "moshi-opencode-hooks": "./src/cli.ts"
14
+ },
15
+ "exports": {
16
+ ".": "./index.ts"
17
+ },
18
+ "scripts": {
19
+ "build": "bun build index.ts --outdir=dist --target=bun --format=esm",
20
+ "typecheck": "bun tsc --noEmit"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bun": "latest"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5"
27
+ },
28
+ "dependencies": {
29
+ "@opencode-ai/plugin": "^1.2.27"
30
+ }
31
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { homedir } from "os"
4
+ import { dirname } from "path"
5
+
6
+ const TOKEN_PATH = `${homedir()}/.config/moshi/token`
7
+ const OPENCODE_CONFIG_PATH = `${homedir()}/.config/opencode/opencode.json`
8
+ const PLUGIN_SOURCE = "moshi-opencode-hooks"
9
+ const HOOK_IDENTIFIER = "moshi-opencode-hooks"
10
+
11
+ async function loadConfig(): Promise<Record<string, unknown>> {
12
+ try {
13
+ const file = Bun.file(OPENCODE_CONFIG_PATH)
14
+ if (!file.size) return {}
15
+ return await file.json()
16
+ } catch {
17
+ return {}
18
+ }
19
+ }
20
+
21
+ async function saveConfig(config: Record<string, unknown>): Promise<void> {
22
+ const { mkdir } = await import("fs/promises")
23
+ await mkdir(dirname(OPENCODE_CONFIG_PATH), { recursive: true })
24
+ await Bun.write(OPENCODE_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n")
25
+ }
26
+
27
+ function isMoshiHook(entry: string): boolean {
28
+ return entry.includes(HOOK_IDENTIFIER) || entry.includes("moshi-opencode-hooks")
29
+ }
30
+
31
+ async function setup(): Promise<void> {
32
+ const config = await loadConfig()
33
+ const plugins: string[] = Array.isArray(config.plugin) ? config.plugin : []
34
+
35
+ const filtered = plugins.filter((p) => !isMoshiHook(p))
36
+ filtered.push(PLUGIN_SOURCE)
37
+
38
+ config.plugin = filtered
39
+ await saveConfig(config)
40
+ console.log(`moshi-opencode-hooks: registered in ${OPENCODE_CONFIG_PATH}`)
41
+ }
42
+
43
+ async function uninstall(): Promise<void> {
44
+ const config = await loadConfig()
45
+ const plugins: string[] = Array.isArray(config.plugin) ? config.plugin : []
46
+
47
+ const filtered = plugins.filter((p) => !isMoshiHook(p))
48
+ config.plugin = filtered
49
+
50
+ await saveConfig(config)
51
+ console.log(`moshi-opencode-hooks: removed from ${OPENCODE_CONFIG_PATH}`)
52
+ }
53
+
54
+ async function setToken(value: string): Promise<void> {
55
+ const { mkdir } = await import("fs/promises")
56
+ await mkdir(dirname(TOKEN_PATH), { recursive: true })
57
+ await Bun.write(TOKEN_PATH, value + "\n")
58
+ console.log(`moshi-opencode-hooks: token saved to ${TOKEN_PATH}`)
59
+ }
60
+
61
+ async function getToken(): Promise<void> {
62
+ try {
63
+ const text = await Bun.file(TOKEN_PATH).text()
64
+ console.log(text.trim() || `no token found (expected at ${TOKEN_PATH})`)
65
+ } catch {
66
+ console.log(`no token found (expected at ${TOKEN_PATH})`)
67
+ }
68
+ }
69
+
70
+ async function main() {
71
+ const subcommand = process.argv[2]
72
+
73
+ if (subcommand === "setup") {
74
+ await setup()
75
+ return
76
+ }
77
+
78
+ if (subcommand === "uninstall") {
79
+ await uninstall()
80
+ return
81
+ }
82
+
83
+ if (subcommand === "token") {
84
+ const value = process.argv[3]
85
+ if (value) {
86
+ await setToken(value)
87
+ } else {
88
+ await getToken()
89
+ }
90
+ return
91
+ }
92
+
93
+ console.log("Usage:")
94
+ console.log(" moshi-opencode-hooks setup Register plugin")
95
+ console.log(" moshi-opencode-hooks uninstall Remove plugin")
96
+ console.log(" moshi-opencode-hooks token [value] Show or set API token")
97
+ process.exit(0)
98
+ }
99
+
100
+ main().catch((err) => {
101
+ console.error(`moshi-opencode-hooks: error:`, err)
102
+ process.exit(1)
103
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }