-
Notifications
You must be signed in to change notification settings - Fork 308
Expand file tree
/
Copy pathentire.ts
More file actions
177 lines (165 loc) · 6.43 KB
/
entire.ts
File metadata and controls
177 lines (165 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Entire CLI plugin for OpenCode
// Auto-generated by `entire enable --agent opencode`
// Do not edit manually — changes will be overwritten on next install.
// Requires Bun runtime (used by OpenCode's plugin system for loading ESM plugins).
import type { Plugin } from "@opencode-ai/plugin"
export const EntirePlugin: Plugin = async ({ directory }) => {
const ENTIRE_CMD = 'go run "$(git rev-parse --show-toplevel)"/cmd/entire/main.go'
// Track seen user messages to fire turn-start only once per message
const seenUserMessages = new Set<string>()
// Track current session ID for message events (which don't include sessionID)
let currentSessionID: string | null = null
// Track the model used by the most recent assistant message
let currentModel: string | null = null
// In-memory store for message metadata (role, tokens, etc.)
const messageStore = new Map<string, any>()
/**
* Build the shell command for a hook invocation.
* Uses sh -c so that shell command substitution in ENTIRE_CMD
* (e.g., $(git rev-parse --show-toplevel) for local-dev) is interpreted.
*/
function hookCmd(hookName: string): string[] {
return ["sh", "-c", `${ENTIRE_CMD} hooks opencode ${hookName}`]
}
/**
* Pipe JSON payload to an entire hooks command (async).
* Errors are logged but never thrown — plugin failures must not crash OpenCode.
*/
async function callHook(hookName: string, payload: Record<string, unknown>) {
try {
const json = JSON.stringify(payload)
const proc = Bun.spawn(hookCmd(hookName), {
cwd: directory,
stdin: new Blob([json + "\n"]),
stdout: "ignore",
stderr: "ignore",
})
await proc.exited
} catch {
// Silently ignore — plugin failures must not crash OpenCode
}
}
/**
* Synchronous variant for hooks that fire near process exit (turn-end, session-end).
* `opencode run` breaks its event loop on the same session.status idle event that
* triggers turn-end. The async callHook would be killed before completing.
* Bun.spawnSync blocks the event loop, preventing exit until the hook finishes.
*/
function callHookSync(hookName: string, payload: Record<string, unknown>) {
try {
const json = JSON.stringify(payload)
Bun.spawnSync(hookCmd(hookName), {
cwd: directory,
stdin: new TextEncoder().encode(json + "\n"),
stdout: "ignore",
stderr: "ignore",
})
} catch {
// Silently ignore — plugin failures must not crash OpenCode
}
}
return {
event: async ({ event }) => {
try {
switch (event.type) {
case "session.created": {
const session = (event as any).properties?.info
if (!session?.id) break
// Reset per-session tracking state when switching sessions.
if (currentSessionID !== session.id) {
seenUserMessages.clear()
messageStore.clear()
currentModel = null
}
currentSessionID = session.id
await callHook("session-start", {
session_id: session.id,
})
break
}
case "message.updated": {
const msg = (event as any).properties?.info
if (!msg) break
// Store message metadata (role, time, tokens, etc.)
messageStore.set(msg.id, msg)
// Track model from assistant messages
if (msg.role === "assistant" && msg.modelID) {
currentModel = msg.modelID
}
break
}
case "message.part.updated": {
const part = (event as any).properties?.part
if (!part?.messageID) break
// Fire turn-start on the first text part of a new user message
const msg = messageStore.get(part.messageID)
if (msg?.role === "user" && part.type === "text" && !seenUserMessages.has(msg.id)) {
seenUserMessages.add(msg.id)
const sessionID = msg.sessionID ?? currentSessionID
if (sessionID) {
await callHook("turn-start", {
session_id: sessionID,
prompt: part.text ?? "",
model: currentModel ?? "",
})
}
}
break
}
case "session.status": {
// session.status fires in both TUI and non-interactive (run) mode.
// session.idle is deprecated and not reliably emitted in run mode.
const props = (event as any).properties
if (props?.status?.type !== "idle") break
const sessionID = props?.sessionID ?? currentSessionID
if (!sessionID) break
// Use sync variant: `opencode run` exits on the same idle event,
// so an async hook would be killed before completing.
callHookSync("turn-end", {
session_id: sessionID,
model: currentModel ?? "",
})
break
}
case "session.compacted": {
const sessionID = (event as any).properties?.sessionID
if (!sessionID) break
await callHook("compaction", {
session_id: sessionID,
})
break
}
case "session.deleted": {
const session = (event as any).properties?.info
if (!session?.id) break
seenUserMessages.clear()
messageStore.clear()
currentSessionID = null
// Use sync variant: session-end may fire during shutdown.
callHookSync("session-end", {
session_id: session.id,
})
break
}
case "server.instance.disposed": {
// Fires when OpenCode shuts down (TUI close or `opencode run` exit).
// session.deleted only fires on explicit user deletion, not on quit,
// so this is the only reliable way to end sessions on exit.
if (!currentSessionID) break
const sessionID = currentSessionID
seenUserMessages.clear()
messageStore.clear()
currentSessionID = null
// Use sync variant: this is the last event before process exit.
callHookSync("session-end", {
session_id: sessionID,
})
break
}
}
} catch {
// Silently ignore — plugin failures must not crash OpenCode
}
},
}
}