opencode-overclock 0.2.2 → 0.4.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.
- package/README.md +285 -81
- package/package.json +3 -3
- package/src/bridge.ts +1 -0
- package/src/buddy/companion.ts +104 -5
- package/src/buddy/sprites.ts +4 -4
- package/src/buddy/tui.ts +175 -65
- package/src/core/bridge.ts +34 -0
- package/src/core/lifecycle.ts +53 -0
- package/src/core/policy.ts +128 -0
- package/src/core/summary.ts +33 -0
- package/src/core/types.ts +164 -0
- package/src/features/buddy.ts +1 -2
- package/src/features/guard.ts +168 -30
- package/src/features/index.ts +6 -4
- package/src/features/recovery.ts +143 -0
- package/src/features/sched.ts +147 -89
- package/src/features/tasks.ts +54 -20
- package/src/features/truncator.ts +99 -0
- package/src/features/usage.ts +26 -65
- package/src/index.ts +98 -55
- package/src/lib/busy.ts +1 -25
- package/src/lib/exec.ts +7 -0
- package/src/lib/inject.ts +10 -56
- package/src/lib/mirror.ts +13 -0
- package/src/lib/probe.ts +1 -15
- package/src/lib/state.ts +10 -39
- package/src/lib/tmux.ts +1 -0
- package/src/lib/ui.ts +208 -0
- package/src/merge.ts +2 -35
- package/src/platform/probe.ts +25 -0
- package/src/platform/process/exec.ts +76 -0
- package/src/platform/process/tmux.ts +60 -0
- package/src/platform/session/busy.ts +33 -0
- package/src/platform/session/inject.ts +82 -0
- package/src/platform/session/notify.ts +20 -0
- package/src/platform/storage/state.ts +77 -0
- package/src/platform/storage/store.ts +61 -0
- package/src/summary.ts +1 -0
- package/src/tools.ts +8 -0
- package/src/tui.ts +57 -186
- package/src/types.ts +1 -39
- package/src/v2/context.ts +470 -0
- package/src/v2/host.ts +117 -0
- package/src/v2/loader.ts +150 -0
- package/src/buddy/reactions.ts +0 -41
- package/src/buddy/types.ts +0 -30
- package/src/config.ts +0 -19
- package/src/features/checkpoints.ts +0 -128
- package/src/features/sandbox.ts +0 -104
- package/src/validate.ts +0 -143
package/src/tools.ts
ADDED
package/src/tui.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
2
|
import { registerBuddy } from "./buddy/tui.ts"
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { createUi } from "./lib/ui.ts"
|
|
4
|
+
import {
|
|
5
|
+
scheduleStore,
|
|
6
|
+
taskStore,
|
|
7
|
+
usageStore,
|
|
8
|
+
type ScheduleEntryView,
|
|
9
|
+
type TaskMirrorEntry,
|
|
10
|
+
type UsageStateView,
|
|
11
|
+
} from "./lib/mirror.ts"
|
|
5
12
|
|
|
6
13
|
export interface TuiOptions {
|
|
7
14
|
notifyIdle?: boolean
|
|
@@ -11,44 +18,16 @@ export interface TuiOptions {
|
|
|
11
18
|
buddy?: boolean
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
* config file governs both surfaces. `features.buddy: false` disables; missing
|
|
17
|
-
* file or unreadable config = default on.
|
|
18
|
-
*/
|
|
19
|
-
export async function buddyEnabledInConfig(directory: string): Promise<boolean> {
|
|
20
|
-
try {
|
|
21
|
-
const file = Bun.file(`${directory}/.opencode/overclock.json`)
|
|
22
|
-
if (!(await file.exists())) return true
|
|
23
|
-
const config = (await file.json()) as { features?: Record<string, unknown> }
|
|
24
|
-
return config.features?.["buddy"] !== false
|
|
25
|
-
} catch {
|
|
26
|
-
return true
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface TaskMirrorEntry {
|
|
31
|
-
id: string
|
|
32
|
-
description: string
|
|
33
|
-
status: "running" | "exited" | "killed"
|
|
34
|
-
exitCode: number | null
|
|
35
|
-
startedAt: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface UsageDayBucket {
|
|
39
|
-
cost: number
|
|
40
|
-
tokens: { input: number; output: number; reasoning: number; cacheRead: number; cacheWrite: number }
|
|
41
|
-
messages: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface UsageStateShape {
|
|
45
|
-
days: Record<string, UsageDayBucket>
|
|
46
|
-
}
|
|
21
|
+
/** Read views the summary formatters accept. Re-exported for tests and downstream typing. */
|
|
22
|
+
export type { TaskMirrorEntry, ScheduleEntryView, UsageStateView }
|
|
47
23
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
24
|
+
/** Check if buddy feature is enabled via plugin options. */
|
|
25
|
+
export function isBuddyEnabled(options?: TuiOptions): boolean {
|
|
26
|
+
if (!options) return true
|
|
27
|
+
if (options.buddy === false) return false
|
|
28
|
+
const features = (options as { features?: Record<string, unknown> }).features
|
|
29
|
+
if (features?.buddy === false) return false
|
|
30
|
+
return true
|
|
52
31
|
}
|
|
53
32
|
|
|
54
33
|
/** Local YYYY-MM-DD from an epoch-ms timestamp (mirrors usage.ts, kept local to stay decoupled). */
|
|
@@ -75,7 +54,7 @@ export function formatTasksSummary(entries: TaskMirrorEntry[] | undefined | null
|
|
|
75
54
|
|
|
76
55
|
/** Pure: format the /oc-usage toast summary from a parsed usage.json, for a given "now". */
|
|
77
56
|
export function formatUsageSummary(
|
|
78
|
-
state:
|
|
57
|
+
state: UsageStateView | undefined | null,
|
|
79
58
|
now: number = Date.now(),
|
|
80
59
|
): string {
|
|
81
60
|
if (!state) return "no data yet"
|
|
@@ -86,169 +65,61 @@ export function formatUsageSummary(
|
|
|
86
65
|
}
|
|
87
66
|
|
|
88
67
|
/** Pure: format the /oc-schedules toast summary from a parsed schedules.json. */
|
|
89
|
-
export function formatSchedulesSummary(schedules:
|
|
68
|
+
export function formatSchedulesSummary(schedules: ScheduleEntryView[] | undefined | null): string {
|
|
90
69
|
if (!schedules) return "no data yet"
|
|
91
70
|
if (!schedules.length) return "no schedules"
|
|
92
71
|
const list = schedules.map((s) => `${s.id} (${s.spec})`).join(", ")
|
|
93
72
|
return `${schedules.length} schedule${schedules.length === 1 ? "" : "s"}: ${list}`
|
|
94
73
|
}
|
|
95
74
|
|
|
96
|
-
async function readState<T>(directory: string, file: string): Promise<T | undefined> {
|
|
97
|
-
try {
|
|
98
|
-
const f = Bun.file(`${directory}/${STATE_SUBDIR}/${file}`)
|
|
99
|
-
if (!(await f.exists())) return undefined
|
|
100
|
-
return (await f.json()) as T
|
|
101
|
-
} catch {
|
|
102
|
-
return undefined
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
75
|
const tui: TuiPlugin = async (api, options) => {
|
|
107
76
|
const opts = (options ?? {}) as TuiOptions
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (notifyIdle) {
|
|
117
|
-
unsubs.push(
|
|
118
|
-
api.event.on("session.status", (event) => {
|
|
119
|
-
if (event.properties.status.type !== "idle") return
|
|
120
|
-
void api.attention.notify({
|
|
121
|
-
title: "opencode",
|
|
122
|
-
message: "turn complete",
|
|
123
|
-
sound: { name: "done", when: "blurred" },
|
|
124
|
-
notification: { when: "blurred" },
|
|
125
|
-
})
|
|
126
|
-
}),
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
console.warn(`[overclock-tui] session.status subscription failed: ${e}`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
if (notifyPermission) {
|
|
135
|
-
unsubs.push(
|
|
136
|
-
api.event.on("permission.asked", () => {
|
|
137
|
-
void api.attention.notify({
|
|
138
|
-
title: "opencode",
|
|
139
|
-
message: "needs permission",
|
|
140
|
-
sound: { name: "permission", when: "blurred" },
|
|
141
|
-
notification: { when: "blurred" },
|
|
142
|
-
})
|
|
143
|
-
}),
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
} catch (e) {
|
|
147
|
-
console.warn(`[overclock-tui] permission.asked subscription failed: ${e}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
if (notifyQuestion) {
|
|
152
|
-
unsubs.push(
|
|
153
|
-
api.event.on("question.asked", () => {
|
|
154
|
-
void api.attention.notify({
|
|
155
|
-
title: "opencode",
|
|
156
|
-
message: "asking a question",
|
|
157
|
-
sound: { name: "question", when: "blurred" },
|
|
158
|
-
notification: { when: "blurred" },
|
|
159
|
-
})
|
|
160
|
-
}),
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
} catch (e) {
|
|
164
|
-
console.warn(`[overclock-tui] question.asked subscription failed: ${e}`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
if (notifyError) {
|
|
169
|
-
unsubs.push(
|
|
170
|
-
api.event.on("session.error", () => {
|
|
171
|
-
void api.attention.notify({
|
|
172
|
-
title: "opencode",
|
|
173
|
-
message: "session error",
|
|
174
|
-
sound: { name: "error", when: "blurred" },
|
|
175
|
-
notification: { when: "blurred" },
|
|
176
|
-
})
|
|
177
|
-
}),
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
} catch (e) {
|
|
181
|
-
console.warn(`[overclock-tui] session.error subscription failed: ${e}`)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
for (const unsub of unsubs) api.lifecycle.onDispose(async () => unsub())
|
|
186
|
-
} catch (e) {
|
|
187
|
-
console.warn(`[overclock-tui] lifecycle registration failed: ${e}`)
|
|
77
|
+
const ui = createUi(api)
|
|
78
|
+
|
|
79
|
+
if (opts.notifyIdle !== false) {
|
|
80
|
+
ui.on("session.status", (event) => {
|
|
81
|
+
if (event.properties?.status?.type === "idle") {
|
|
82
|
+
ui.notify({ message: "turn complete", sound: "done" })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
188
85
|
}
|
|
189
86
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
{
|
|
193
|
-
title: "Overclock: Tasks",
|
|
194
|
-
value: "overclock.tasks",
|
|
195
|
-
slash: { name: "oc-tasks" },
|
|
196
|
-
onSelect: async () => {
|
|
197
|
-
const entries = await readState<TaskMirrorEntry[]>(api.state.path.directory, "tasks.json")
|
|
198
|
-
api.ui.toast({ message: formatTasksSummary(entries) })
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
])
|
|
202
|
-
if (unregister) api.lifecycle.onDispose(async () => unregister())
|
|
203
|
-
} catch (e) {
|
|
204
|
-
console.warn(`[overclock-tui] /oc-tasks command registration failed: ${e}`)
|
|
87
|
+
if (opts.notifyPermission !== false) {
|
|
88
|
+
ui.on("permission.asked", () => ui.notify({ message: "needs permission", sound: "permission" }))
|
|
205
89
|
}
|
|
206
90
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
title: "Overclock: Usage",
|
|
211
|
-
value: "overclock.usage",
|
|
212
|
-
slash: { name: "oc-usage" },
|
|
213
|
-
onSelect: async () => {
|
|
214
|
-
const state = await readState<UsageStateShape>(api.state.path.directory, "usage.json")
|
|
215
|
-
api.ui.toast({ message: formatUsageSummary(state) })
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
])
|
|
219
|
-
if (unregister) api.lifecycle.onDispose(async () => unregister())
|
|
220
|
-
} catch (e) {
|
|
221
|
-
console.warn(`[overclock-tui] /oc-usage command registration failed: ${e}`)
|
|
91
|
+
if (opts.notifyQuestion !== false) {
|
|
92
|
+
ui.on("question.asked", () => ui.notify({ message: "asking a question", sound: "question" }))
|
|
222
93
|
}
|
|
223
94
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
title: "Overclock: Schedules",
|
|
228
|
-
value: "overclock.schedules",
|
|
229
|
-
slash: { name: "oc-schedules" },
|
|
230
|
-
onSelect: async () => {
|
|
231
|
-
const schedules = await readState<ScheduleMirrorEntry[]>(
|
|
232
|
-
api.state.path.directory,
|
|
233
|
-
"schedules.json",
|
|
234
|
-
)
|
|
235
|
-
api.ui.toast({ message: formatSchedulesSummary(schedules) })
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
])
|
|
239
|
-
if (unregister) api.lifecycle.onDispose(async () => unregister())
|
|
240
|
-
} catch (e) {
|
|
241
|
-
console.warn(`[overclock-tui] /oc-schedules command registration failed: ${e}`)
|
|
95
|
+
if (opts.notifyError !== false) {
|
|
96
|
+
ui.on("session.error", () => ui.notify({ message: "session error", sound: "error" }))
|
|
242
97
|
}
|
|
243
98
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
99
|
+
ui.command({
|
|
100
|
+
title: "Overclock: Tasks",
|
|
101
|
+
slash: "oc-tasks",
|
|
102
|
+
run: async () => ui.toast(formatTasksSummary(await ui.readOptional(taskStore))),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
ui.command({
|
|
106
|
+
title: "Overclock: Usage",
|
|
107
|
+
slash: "oc-usage",
|
|
108
|
+
run: async () => ui.toast(formatUsageSummary(await ui.readOptional(usageStore))),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
ui.command({
|
|
112
|
+
title: "Overclock: Schedules",
|
|
113
|
+
slash: "oc-schedules",
|
|
114
|
+
run: async () => ui.toast(formatSchedulesSummary(await ui.readOptional(scheduleStore))),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (isBuddyEnabled(opts)) {
|
|
118
|
+
try {
|
|
119
|
+
await registerBuddy(ui)
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.warn(`[overclock-tui] buddy disabled: ${e}`)
|
|
249
122
|
}
|
|
250
|
-
} catch (e) {
|
|
251
|
-
console.warn(`[overclock-tui] buddy disabled: ${e}`)
|
|
252
123
|
}
|
|
253
124
|
}
|
|
254
125
|
|
package/src/types.ts
CHANGED
|
@@ -1,39 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { BusyTracker } from "./lib/busy.ts"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Singletons built once by the entry and handed to every module.
|
|
6
|
-
* Derived from the event bus, so they must have exactly one subscription -- a per-module
|
|
7
|
-
* copy would be N subscriptions maintaining N identical copies of the same state.
|
|
8
|
-
*/
|
|
9
|
-
export interface SharedDeps {
|
|
10
|
-
/** live per-session busy/idle state; the entry owns the subscription that feeds it */
|
|
11
|
-
busy: BusyTracker
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Per-feature config from .opencode/overclock.json. `false` = off, object = options. */
|
|
15
|
-
export type FeatureConfig = boolean | Record<string, unknown>
|
|
16
|
-
|
|
17
|
-
/** Option value kinds a module declares, so a typo in overclock.json can be caught. */
|
|
18
|
-
export type OptionType = "boolean" | "number" | "string" | "array" | "object"
|
|
19
|
-
|
|
20
|
-
export interface OverclockConfig {
|
|
21
|
-
features?: Record<string, FeatureConfig>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* One feature = one module.
|
|
26
|
-
* init returns partial Hooks. Same hook from many modules -> composed in registry order.
|
|
27
|
-
*/
|
|
28
|
-
export interface FeatureModule {
|
|
29
|
-
name: string
|
|
30
|
-
/** on by default? */
|
|
31
|
-
defaultEnabled: boolean
|
|
32
|
-
/** SDK client surfaces (dot-paths) the module needs. Missing -> module skipped + warn. */
|
|
33
|
-
requires?: string[]
|
|
34
|
-
/** Tool names registered. Declared, not derived -- feeds the first-run summary. */
|
|
35
|
-
tools?: string[]
|
|
36
|
-
/** Accepted option keys -> expected type. Anything else in config is a typo. */
|
|
37
|
-
options?: Record<string, OptionType>
|
|
38
|
-
init(ctx: PluginInput, options: Record<string, unknown>, shared: SharedDeps): Promise<Partial<Hooks>>
|
|
39
|
-
}
|
|
1
|
+
export * from "./core/types.ts"
|