opencode-overclock 0.3.0 → 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 +206 -111
- package/package.json +2 -2
- 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 +52 -18
- package/src/features/truncator.ts +99 -0
- package/src/features/usage.ts +26 -65
- package/src/index.ts +96 -67
- 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 -66
- 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 -244
- package/src/tui.ts +57 -186
- package/src/types.ts +1 -73
- 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 -197
package/src/features/usage.ts
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
1
|
import type { FeatureModule } from "../types.ts"
|
|
3
2
|
import { ensureStateDir, readJson, writeJson } from "../lib/state.ts"
|
|
3
|
+
import { usageStore, type DayBucket, type UsageState, type UsageTokens } from "../lib/mirror.ts"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export interface UsageTokens {
|
|
8
|
-
input: number
|
|
9
|
-
output: number
|
|
10
|
-
reasoning: number
|
|
11
|
-
cacheRead: number
|
|
12
|
-
cacheWrite: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface DayBucket {
|
|
16
|
-
cost: number
|
|
17
|
-
tokens: UsageTokens
|
|
18
|
-
messages: number
|
|
19
|
-
seen: string[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface UsageState {
|
|
23
|
-
days: Record<string, DayBucket>
|
|
24
|
-
}
|
|
5
|
+
// Declared in platform/storage/store.ts so the TUI can inspect usage data without importing this feature module.
|
|
6
|
+
export type { UsageTokens, DayBucket, UsageState } from "../lib/mirror.ts"
|
|
25
7
|
|
|
26
8
|
export interface SessionUsage {
|
|
27
9
|
sessionID: string
|
|
@@ -99,27 +81,23 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
99
81
|
pruneOldDays(state, Date.now())
|
|
100
82
|
}
|
|
101
83
|
|
|
84
|
+
function extractTokens(t: any): UsageTokens {
|
|
85
|
+
return {
|
|
86
|
+
input: t?.input ?? 0,
|
|
87
|
+
output: t?.output ?? 0,
|
|
88
|
+
reasoning: t?.reasoning ?? 0,
|
|
89
|
+
cacheRead: t?.cache?.read ?? 0,
|
|
90
|
+
cacheWrite: t?.cache?.write ?? 0,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
102
94
|
function onEvent(event: { type: string; properties?: unknown }): void {
|
|
103
95
|
try {
|
|
104
96
|
if (event.type !== "message.updated") return
|
|
105
|
-
const info = (event.properties as
|
|
106
|
-
|
|
107
|
-
id?: string
|
|
108
|
-
role?: string
|
|
109
|
-
sessionID?: string
|
|
110
|
-
time?: { created?: number; completed?: number }
|
|
111
|
-
cost?: number
|
|
112
|
-
tokens?: {
|
|
113
|
-
input?: number
|
|
114
|
-
output?: number
|
|
115
|
-
reasoning?: number
|
|
116
|
-
cache?: { read?: number; write?: number }
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
| undefined
|
|
120
|
-
if (!info || info.role !== "assistant") return
|
|
121
|
-
if (!info.time?.completed) return
|
|
97
|
+
const info = (event.properties as any)?.info
|
|
98
|
+
if (!info || info.role !== "assistant" || !info.time?.completed) return
|
|
122
99
|
if (typeof info.id !== "string" || typeof info.sessionID !== "string") return
|
|
100
|
+
|
|
123
101
|
const created = info.time.created ?? info.time.completed
|
|
124
102
|
const day = dayKey(created)
|
|
125
103
|
const bucket = state.days[day] ?? emptyBucket()
|
|
@@ -128,20 +106,15 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
128
106
|
bucket.seen.push(info.id)
|
|
129
107
|
|
|
130
108
|
const cost = info.cost ?? 0
|
|
131
|
-
const
|
|
132
|
-
const input = t.input ?? 0
|
|
133
|
-
const output = t.output ?? 0
|
|
134
|
-
const reasoning = t.reasoning ?? 0
|
|
135
|
-
const cacheRead = t.cache?.read ?? 0
|
|
136
|
-
const cacheWrite = t.cache?.write ?? 0
|
|
109
|
+
const tok = extractTokens(info.tokens)
|
|
137
110
|
|
|
138
111
|
bucket.cost += cost
|
|
139
112
|
bucket.messages += 1
|
|
140
|
-
bucket.tokens.input += input
|
|
141
|
-
bucket.tokens.output += output
|
|
142
|
-
bucket.tokens.reasoning += reasoning
|
|
143
|
-
bucket.tokens.cacheRead += cacheRead
|
|
144
|
-
bucket.tokens.cacheWrite += cacheWrite
|
|
113
|
+
bucket.tokens.input += tok.input
|
|
114
|
+
bucket.tokens.output += tok.output
|
|
115
|
+
bucket.tokens.reasoning += tok.reasoning
|
|
116
|
+
bucket.tokens.cacheRead += tok.cacheRead
|
|
117
|
+
bucket.tokens.cacheWrite += tok.cacheWrite
|
|
145
118
|
|
|
146
119
|
const sess = sessions.get(info.sessionID) ?? {
|
|
147
120
|
sessionID: info.sessionID,
|
|
@@ -149,8 +122,8 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
149
122
|
tokens: { input: 0, output: 0 },
|
|
150
123
|
}
|
|
151
124
|
sess.cost += cost
|
|
152
|
-
sess.tokens.input += input
|
|
153
|
-
sess.tokens.output += output
|
|
125
|
+
sess.tokens.input += tok.input
|
|
126
|
+
sess.tokens.output += tok.output
|
|
154
127
|
sessions.set(info.sessionID, sess)
|
|
155
128
|
|
|
156
129
|
scheduleFlush()
|
|
@@ -236,11 +209,11 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
236
209
|
*/
|
|
237
210
|
export const usage: FeatureModule = {
|
|
238
211
|
name: "usage",
|
|
239
|
-
tools: [
|
|
212
|
+
tools: [],
|
|
240
213
|
defaultEnabled: true,
|
|
241
214
|
async init(ctx) {
|
|
242
215
|
const dir = await ensureStateDir(ctx.directory)
|
|
243
|
-
const tracker = createUsageTracker({ statePath:
|
|
216
|
+
const tracker = createUsageTracker({ statePath: usageStore.path(ctx.directory) })
|
|
244
217
|
await tracker.load()
|
|
245
218
|
|
|
246
219
|
return {
|
|
@@ -254,18 +227,6 @@ export const usage: FeatureModule = {
|
|
|
254
227
|
console.warn(`[overclock] usage: event handling failed: ${e}`)
|
|
255
228
|
}
|
|
256
229
|
},
|
|
257
|
-
tool: {
|
|
258
|
-
usage_report: tool({
|
|
259
|
-
description:
|
|
260
|
-
"Report cost/token usage for the last N days (default 7) plus today's per-session breakdown.",
|
|
261
|
-
args: {
|
|
262
|
-
days: z.number().optional().describe("number of days to report, default 7"),
|
|
263
|
-
},
|
|
264
|
-
async execute(args) {
|
|
265
|
-
return tracker.report(args.days ?? 7)
|
|
266
|
-
},
|
|
267
|
-
}),
|
|
268
|
-
},
|
|
269
230
|
}
|
|
270
231
|
},
|
|
271
232
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,85 +1,114 @@
|
|
|
1
|
-
import type { Hooks
|
|
1
|
+
import type { Hooks } from "@opencode-ai/plugin"
|
|
2
2
|
import { features } from "./features/index.ts"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { mergeHooks } from "./core/lifecycle.ts"
|
|
4
|
+
import { resolveToolPolicy } from "./core/policy.ts"
|
|
5
|
+
import { summarise } from "./core/summary.ts"
|
|
6
|
+
import { createHybridPlugin, type HybridPlugin } from "./core/bridge.ts"
|
|
7
|
+
import type { FeatureModule, OverclockOptions, SharedDeps } from "./core/types.ts"
|
|
5
8
|
import { missingSurfaces } from "./lib/probe.ts"
|
|
6
9
|
import { toast } from "./lib/inject.ts"
|
|
7
10
|
import { firstRun } from "./lib/state.ts"
|
|
8
|
-
import { validateConfig, summarise } from "./validate.ts"
|
|
9
11
|
import { createBusyTracker } from "./lib/busy.ts"
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
+
import { createV2Host } from "./v2/host.ts"
|
|
13
|
+
|
|
14
|
+
function featureOptions(
|
|
15
|
+
options: OverclockOptions,
|
|
16
|
+
feature: FeatureModule,
|
|
17
|
+
): Record<string, unknown> | null {
|
|
18
|
+
const setting = options[feature.name] ?? options.features?.[feature.name] ?? feature.defaultEnabled
|
|
19
|
+
if (setting === false) return null
|
|
20
|
+
return typeof setting === "object" && setting !== null ? (setting as Record<string, unknown>) : {}
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* Entry.
|
|
15
|
-
*
|
|
24
|
+
* Entry. Read options -> probe surfaces -> init enabled modules -> merge hooks.
|
|
25
|
+
* Employs createHybridPlugin so the plugin is runnable on both V1 and V2 OpenCode harnesses.
|
|
16
26
|
*/
|
|
17
|
-
export const Overclock:
|
|
18
|
-
|
|
27
|
+
export const Overclock: HybridPlugin<OverclockOptions> = createHybridPlugin<OverclockOptions>({
|
|
28
|
+
id: "overclock",
|
|
29
|
+
|
|
30
|
+
/** V1 lifecycle: tools, execution interception, event bus hooks */
|
|
31
|
+
server: async (ctx, pluginOptions) => {
|
|
32
|
+
const options = (pluginOptions ?? {}) as OverclockOptions
|
|
33
|
+
const { policy, issues } = resolveToolPolicy(options, features)
|
|
34
|
+
|
|
35
|
+
const shared: SharedDeps = {
|
|
36
|
+
busy: createBusyTracker(),
|
|
37
|
+
toolName: (declared) => policy.rename[declared] ?? declared,
|
|
38
|
+
}
|
|
39
|
+
const parts: Partial<Hooks>[] = [{ event: async ({ event }) => shared.busy.onEvent(event) }]
|
|
40
|
+
const skipped: string[] = []
|
|
41
|
+
const enabled: FeatureModule[] = []
|
|
42
|
+
|
|
43
|
+
for (const feature of features) {
|
|
44
|
+
const opts = featureOptions(options, feature)
|
|
45
|
+
if (opts === null) continue
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
const missing = missingSurfaces(ctx.client, feature.requires ?? [])
|
|
48
|
+
if (missing.length) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`[overclock] ${feature.name} disabled: client lacks ${missing.join(", ")} (upstream drift?)`,
|
|
51
|
+
)
|
|
52
|
+
skipped.push(feature.name)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
22
55
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
// First part, so the tracker is current before any module's own event hook reads it.
|
|
32
|
-
const parts: Partial<Hooks>[] = [{ event: async ({ event }) => shared.busy.onEvent(event) }]
|
|
33
|
-
const skipped: string[] = []
|
|
34
|
-
const enabled: FeatureModule[] = []
|
|
56
|
+
try {
|
|
57
|
+
parts.push(await feature.init(ctx, opts, shared))
|
|
58
|
+
enabled.push(feature)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn(`[overclock] feature ${feature.name} failed init: ${e}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
if (Array.isArray(options.plugins) && options.plugins.length > 0) {
|
|
65
|
+
const v2Host = createV2Host(ctx, options)
|
|
66
|
+
const loadedV2 = await v2Host.loadPlugins(options.plugins)
|
|
67
|
+
if (loadedV2.length > 0) {
|
|
68
|
+
console.warn(`[overclock] loaded ${loadedV2.length} v2 plugin(s): ${loadedV2.join(", ")}`)
|
|
69
|
+
}
|
|
70
|
+
parts.push(v2Host.createHooks())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const issue of issues) {
|
|
74
|
+
console.warn(`[overclock] config: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`)
|
|
75
|
+
}
|
|
76
|
+
if (issues.length) {
|
|
77
|
+
void toast(
|
|
78
|
+
ctx.client,
|
|
79
|
+
`overclock: ${issues.length} config issue${issues.length > 1 ? "s" : ""} (see logs)`,
|
|
80
|
+
"warning",
|
|
43
81
|
)
|
|
44
|
-
skipped.push(feature.name)
|
|
45
|
-
continue
|
|
46
82
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.warn(`[overclock] feature ${feature.name} failed init: ${e}`)
|
|
83
|
+
|
|
84
|
+
const summary = summarise(enabled, skipped, policy)
|
|
85
|
+
console.warn(`[overclock] ${summary}`)
|
|
86
|
+
if (await firstRun(ctx.directory)) {
|
|
87
|
+
void toast(ctx.client, `overclock active: ${summary}`, "info")
|
|
53
88
|
}
|
|
54
|
-
}
|
|
55
89
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
if (skipped.length) {
|
|
91
|
+
void toast(ctx.client, `overclock: ${skipped.join(", ")} disabled (SDK drift)`, "warning")
|
|
92
|
+
}
|
|
93
|
+
return mergeHooks(parts, policy)
|
|
94
|
+
},
|
|
59
95
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const issue of issues) {
|
|
64
|
-
console.warn(`[overclock] config: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`)
|
|
65
|
-
}
|
|
66
|
-
if (issues.length) {
|
|
67
|
-
void toast(
|
|
68
|
-
ctx.client,
|
|
69
|
-
`overclock: ${issues.length} config issue${issues.length > 1 ? "s" : ""} (see logs)`,
|
|
70
|
-
"warning",
|
|
71
|
-
)
|
|
72
|
-
}
|
|
96
|
+
/** V2 lifecycle: domain transforms (agents, commands, catalog, aisdk) */
|
|
97
|
+
setup: async (v2Context, pluginOptions) => {
|
|
98
|
+
const options = (pluginOptions ?? {}) as OverclockOptions
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (await firstRun(ctx.directory)) {
|
|
79
|
-
void toast(ctx.client, `overclock active: ${summarise(enabled, skipped, policy)}`, "info")
|
|
80
|
-
}
|
|
100
|
+
for (const feature of features) {
|
|
101
|
+
if (!feature.setup) continue
|
|
102
|
+
const opts = featureOptions(options, feature)
|
|
103
|
+
if (opts === null) continue
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
105
|
+
try {
|
|
106
|
+
await feature.setup(v2Context, opts)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn(`[overclock] feature ${feature.name} failed v2 setup: ${e}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export default Overclock
|
package/src/lib/busy.ts
CHANGED
|
@@ -1,25 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Session busy tracking via `session.status` (doc-preferred; `session.idle` deprecated).
|
|
3
|
-
* Unknown/never-firing statuses degrade gracefully: empty set = nothing reported busy.
|
|
4
|
-
*/
|
|
5
|
-
export interface BusyTracker {
|
|
6
|
-
/** feed bus events */
|
|
7
|
-
onEvent(event: { type: string; properties?: unknown }): void
|
|
8
|
-
isBusy(sessionID: string): boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createBusyTracker(): BusyTracker {
|
|
12
|
-
const busy = new Set<string>()
|
|
13
|
-
return {
|
|
14
|
-
onEvent(event) {
|
|
15
|
-
const p = (event.properties ?? {}) as { sessionID?: string; status?: { type?: string } }
|
|
16
|
-
if (!p.sessionID) return
|
|
17
|
-
if (event.type === "session.status") {
|
|
18
|
-
p.status?.type === "idle" ? busy.delete(p.sessionID) : busy.add(p.sessionID)
|
|
19
|
-
} else if (event.type === "session.idle" || event.type === "session.deleted") {
|
|
20
|
-
busy.delete(p.sessionID)
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
isBusy: (id) => busy.has(id),
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
export { createBusyTracker, type BusyTracker } from "../platform/session/busy.ts"
|
package/src/lib/exec.ts
ADDED
package/src/lib/inject.ts
CHANGED
|
@@ -1,56 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export async function sessionModel(client: Client, sessionID: string): Promise<ModelRef | undefined> {
|
|
12
|
-
try {
|
|
13
|
-
const res = await client.session.messages({ path: { id: sessionID } })
|
|
14
|
-
const msgs = res.data ?? []
|
|
15
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
16
|
-
const info = msgs[i]?.info
|
|
17
|
-
if (info?.role === "assistant" && info.modelID) {
|
|
18
|
-
return { providerID: info.providerID, modelID: info.modelID }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.warn(`[overclock] sessionModel lookup failed (${sessionID}): ${e}`)
|
|
23
|
-
}
|
|
24
|
-
return undefined
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Re-entry: push text into session as user prompt, on the session's own model.
|
|
29
|
-
* promptAsync = fire-and-forget, server queues if busy. Failure -> warn, never throw.
|
|
30
|
-
*/
|
|
31
|
-
export async function inject(client: Client, sessionID: string, text: string): Promise<boolean> {
|
|
32
|
-
try {
|
|
33
|
-
const model = await sessionModel(client, sessionID)
|
|
34
|
-
await client.session.promptAsync({
|
|
35
|
-
path: { id: sessionID },
|
|
36
|
-
body: { parts: [{ type: "text", text }], ...(model ? { model } : {}) },
|
|
37
|
-
})
|
|
38
|
-
return true
|
|
39
|
-
} catch (e) {
|
|
40
|
-
console.warn(`[overclock] inject failed (session ${sessionID}): ${e}`)
|
|
41
|
-
return false
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** TUI toast, best-effort (headless server -> no TUI, swallow). */
|
|
46
|
-
export async function toast(
|
|
47
|
-
client: Client,
|
|
48
|
-
message: string,
|
|
49
|
-
variant: "info" | "success" | "warning" | "error" = "info",
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
try {
|
|
52
|
-
await client.tui.showToast({ body: { message, variant } })
|
|
53
|
-
} catch {
|
|
54
|
-
// no TUI attached
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
inject,
|
|
3
|
+
sessionContext,
|
|
4
|
+
sessionModel,
|
|
5
|
+
toast,
|
|
6
|
+
type InjectOptions,
|
|
7
|
+
type ModelRef,
|
|
8
|
+
type SessionContext,
|
|
9
|
+
type ToastVariant,
|
|
10
|
+
} from "../platform/session/inject.ts"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
scheduleStore,
|
|
3
|
+
taskStore,
|
|
4
|
+
usageStore,
|
|
5
|
+
type DayBucket,
|
|
6
|
+
type DayBucketView,
|
|
7
|
+
type ScheduleEntry,
|
|
8
|
+
type ScheduleEntryView,
|
|
9
|
+
type TaskMirrorEntry,
|
|
10
|
+
type UsageState,
|
|
11
|
+
type UsageStateView,
|
|
12
|
+
type UsageTokens,
|
|
13
|
+
} from "../platform/storage/store.ts"
|
package/src/lib/probe.ts
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Runtime surface probe (doc: "probe for the surfaces we depend on ... instead of
|
|
3
|
-
* failing silently when upstream moves"). Dot-paths resolved against the SDK client;
|
|
4
|
-
* leaf must be a function.
|
|
5
|
-
*/
|
|
6
|
-
export function missingSurfaces(client: unknown, paths: string[]): string[] {
|
|
7
|
-
return paths.filter((path) => {
|
|
8
|
-
let node: unknown = client
|
|
9
|
-
for (const key of path.split(".")) {
|
|
10
|
-
if (node == null || typeof node !== "object") return true
|
|
11
|
-
node = (node as Record<string, unknown>)[key]
|
|
12
|
-
}
|
|
13
|
-
return typeof node !== "function"
|
|
14
|
-
})
|
|
15
|
-
}
|
|
1
|
+
export { missingSurfaces } from "../platform/probe.ts"
|
package/src/lib/state.ts
CHANGED
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* True once per project, then never again. Marker lives beside the other state, so
|
|
12
|
-
* deleting .opencode/overclock/ re-arms the first-run notice.
|
|
13
|
-
*/
|
|
14
|
-
export async function firstRun(directory: string): Promise<boolean> {
|
|
15
|
-
const dir = await ensureStateDir(directory)
|
|
16
|
-
const marker = Bun.file(`${dir}/.installed`)
|
|
17
|
-
if (await marker.exists()) return false
|
|
18
|
-
await Bun.write(marker, new Date().toISOString())
|
|
19
|
-
return true
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function readJson<T>(path: string, fallback: T): Promise<T> {
|
|
23
|
-
const file = Bun.file(path)
|
|
24
|
-
if (!(await file.exists())) return fallback
|
|
25
|
-
try {
|
|
26
|
-
return (await file.json()) as T
|
|
27
|
-
} catch {
|
|
28
|
-
return fallback
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function writeJson(path: string, value: unknown): Promise<void> {
|
|
33
|
-
await Bun.write(path, JSON.stringify(value, null, 2))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** POSIX single-quote escape. */
|
|
37
|
-
export function shellQuote(s: string): string {
|
|
38
|
-
return `'${s.replace(/'/g, `'\\''`)}'`
|
|
39
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
defineStore,
|
|
3
|
+
ensureStateDir,
|
|
4
|
+
firstRun,
|
|
5
|
+
readJson,
|
|
6
|
+
shellQuote,
|
|
7
|
+
stateDir,
|
|
8
|
+
writeJson,
|
|
9
|
+
type Store,
|
|
10
|
+
} from "../platform/storage/state.ts"
|
package/src/lib/tmux.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isInsideTmux, spawnTaskPane, type TmuxPane } from "../platform/process/tmux.ts"
|