opencode-overclock 0.3.0 → 0.5.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.
Files changed (82) hide show
  1. package/README.md +252 -111
  2. package/package.json +6 -4
  3. package/skills/codebase-design/DEEPENING.md +35 -0
  4. package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
  5. package/skills/codebase-design/SKILL.md +93 -0
  6. package/skills/diagnosing-bugs/SKILL.md +123 -0
  7. package/skills/domain-modeling/ADR-FORMAT.md +55 -0
  8. package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
  9. package/skills/domain-modeling/SKILL.md +102 -0
  10. package/skills/doubt/SKILL.md +80 -0
  11. package/skills/grilling/SKILL.md +96 -0
  12. package/skills/source-discipline/SKILL.md +78 -0
  13. package/skills/tdd/SKILL.md +87 -0
  14. package/skills/to-spec/SKILL.md +69 -0
  15. package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
  16. package/skills/to-tickets/SKILL.md +74 -0
  17. package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
  18. package/src/bridge.ts +1 -0
  19. package/src/buddy/companion.ts +104 -5
  20. package/src/buddy/sprites.ts +4 -4
  21. package/src/buddy/tui.ts +175 -65
  22. package/src/core/bridge.ts +34 -0
  23. package/src/core/lifecycle.ts +67 -0
  24. package/src/core/policy.ts +128 -0
  25. package/src/core/summary.ts +33 -0
  26. package/src/core/types.ts +193 -0
  27. package/src/features/buddy.ts +1 -2
  28. package/src/features/guard.ts +421 -37
  29. package/src/features/index.ts +18 -4
  30. package/src/features/recovery.ts +153 -0
  31. package/src/features/safety.ts +147 -0
  32. package/src/features/sched.ts +183 -89
  33. package/src/features/tasks.ts +134 -33
  34. package/src/features/truncator.ts +116 -0
  35. package/src/features/usage.ts +46 -65
  36. package/src/features/workflow.ts +256 -0
  37. package/src/index.ts +96 -67
  38. package/src/lib/busy.ts +1 -25
  39. package/src/lib/exec.ts +13 -0
  40. package/src/lib/inject.ts +10 -56
  41. package/src/lib/mirror.ts +13 -0
  42. package/src/lib/probe.ts +1 -15
  43. package/src/lib/state.ts +10 -39
  44. package/src/lib/tmux.ts +1 -0
  45. package/src/lib/ui.ts +208 -0
  46. package/src/merge.ts +2 -66
  47. package/src/platform/probe.ts +25 -0
  48. package/src/platform/process/exec.ts +317 -0
  49. package/src/platform/process/tmux.ts +60 -0
  50. package/src/platform/session/busy.ts +33 -0
  51. package/src/platform/session/inject.ts +89 -0
  52. package/src/platform/session/notify.ts +20 -0
  53. package/src/platform/storage/state.ts +99 -0
  54. package/src/platform/storage/store.ts +61 -0
  55. package/src/summary.ts +1 -0
  56. package/src/tools.ts +8 -244
  57. package/src/tui.ts +57 -186
  58. package/src/types.ts +1 -73
  59. package/src/v2/context.ts +470 -0
  60. package/src/v2/host.ts +120 -0
  61. package/src/v2/loader.ts +150 -0
  62. package/src/workflow/agents/codebase-researcher.ts +27 -0
  63. package/src/workflow/agents/design-explorer.ts +33 -0
  64. package/src/workflow/agents/doubt-reviewer.ts +26 -0
  65. package/src/workflow/agents/engineering-coach.ts +23 -0
  66. package/src/workflow/agents/performance-auditor.ts +29 -0
  67. package/src/workflow/agents/security-auditor.ts +23 -0
  68. package/src/workflow/agents/spec-reviewer.ts +15 -0
  69. package/src/workflow/agents/standards-reviewer.ts +24 -0
  70. package/src/workflow/agents/test-engineer.ts +28 -0
  71. package/src/workflow/catalog.ts +210 -0
  72. package/src/workflow/templates/build.ts +47 -0
  73. package/src/workflow/templates/define.ts +45 -0
  74. package/src/workflow/templates/diagnose.ts +58 -0
  75. package/src/workflow/templates/plan.ts +52 -0
  76. package/src/workflow/templates/ship.ts +64 -0
  77. package/src/buddy/reactions.ts +0 -41
  78. package/src/buddy/types.ts +0 -30
  79. package/src/config.ts +0 -19
  80. package/src/features/checkpoints.ts +0 -128
  81. package/src/features/sandbox.ts +0 -104
  82. package/src/validate.ts +0 -197
@@ -0,0 +1,256 @@
1
+ import { existsSync } from "node:fs"
2
+ import { resolve, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import type { FeatureModule, WorkflowOptions } from "../core/types.ts"
5
+ import { DEFINE_TEMPLATE } from "../workflow/templates/define.ts"
6
+ import { PLAN_TEMPLATE } from "../workflow/templates/plan.ts"
7
+ import { BUILD_TEMPLATE } from "../workflow/templates/build.ts"
8
+ import { DIAGNOSE_TEMPLATE } from "../workflow/templates/diagnose.ts"
9
+ import { SHIP_TEMPLATE } from "../workflow/templates/ship.ts"
10
+ import { STANDARDS_REVIEWER_PROMPT } from "../workflow/agents/standards-reviewer.ts"
11
+ import { SPEC_REVIEWER_PROMPT } from "../workflow/agents/spec-reviewer.ts"
12
+ import { SECURITY_AUDITOR_PROMPT } from "../workflow/agents/security-auditor.ts"
13
+ import { TEST_ENGINEER_PROMPT } from "../workflow/agents/test-engineer.ts"
14
+ import { PERFORMANCE_AUDITOR_PROMPT } from "../workflow/agents/performance-auditor.ts"
15
+ import { DOUBT_REVIEWER_PROMPT } from "../workflow/agents/doubt-reviewer.ts"
16
+ import { CODEBASE_RESEARCHER_PROMPT } from "../workflow/agents/codebase-researcher.ts"
17
+ import { DESIGN_EXPLORER_PROMPT } from "../workflow/agents/design-explorer.ts"
18
+ import { ENGINEERING_COACH_PROMPT } from "../workflow/agents/engineering-coach.ts"
19
+
20
+ function getBundledSkillsDir(customPath?: string): string {
21
+ if (customPath) return customPath
22
+ const currentDir =
23
+ typeof import.meta.dir === "string" ? import.meta.dir : dirname(fileURLToPath(import.meta.url))
24
+ return resolve(currentDir, "../../skills")
25
+ }
26
+
27
+ export const WORKFLOW_COMMANDS = {
28
+ define: {
29
+ description: "Interrogate requirements and draft SPEC.md with recommended defaults",
30
+ template: DEFINE_TEMPLATE,
31
+ },
32
+ plan: {
33
+ description: "Decompose spec into vertical tracer-bullet tasks in tasks/plan.md",
34
+ template: PLAN_TEMPLATE,
35
+ },
36
+ build: {
37
+ description: "Autonomous TDD implementation with tripwires and atomic commits",
38
+ template: BUILD_TEMPLATE,
39
+ },
40
+ diagnose: {
41
+ description: "Disciplined bug reproduction and isolation loop ([DEBUG-xxxx] tags)",
42
+ template: DIAGNOSE_TEMPLATE,
43
+ },
44
+ ship: {
45
+ description: "3-way parallel review (Standards, Spec, Security) with GO/NO-GO verdict",
46
+ template: SHIP_TEMPLATE,
47
+ },
48
+ }
49
+
50
+ export const WORKFLOW_AGENTS = {
51
+ "standards-reviewer": {
52
+ mode: "subagent" as const,
53
+ description: "Senior Staff Engineer auditing diffs for repo conventions and code smells",
54
+ prompt: STANDARDS_REVIEWER_PROMPT,
55
+ tools: {
56
+ write: false,
57
+ edit: false,
58
+ },
59
+ permission: {
60
+ edit: "deny" as const,
61
+ },
62
+ },
63
+ "spec-reviewer": {
64
+ mode: "subagent" as const,
65
+ description: "Product Engineer auditing diffs strictly against originating specifications",
66
+ prompt: SPEC_REVIEWER_PROMPT,
67
+ tools: {
68
+ write: false,
69
+ edit: false,
70
+ },
71
+ permission: {
72
+ edit: "deny" as const,
73
+ },
74
+ },
75
+ "security-auditor": {
76
+ mode: "subagent" as const,
77
+ description: "Adversarial Security Engineer auditing diffs for OWASP vulnerabilities and secrets",
78
+ prompt: SECURITY_AUDITOR_PROMPT,
79
+ tools: {
80
+ write: false,
81
+ edit: false,
82
+ },
83
+ permission: {
84
+ edit: "deny" as const,
85
+ },
86
+ },
87
+ "test-engineer": {
88
+ mode: "subagent" as const,
89
+ description: "QA Engineer auditing test strategy, coverage gaps, and Prove-It verification",
90
+ prompt: TEST_ENGINEER_PROMPT,
91
+ tools: {
92
+ write: false,
93
+ edit: false,
94
+ },
95
+ permission: {
96
+ edit: "deny" as const,
97
+ },
98
+ },
99
+ "performance-auditor": {
100
+ mode: "subagent" as const,
101
+ description: "Senior Performance Engineer auditing latency, N+1 queries, and resource leaks",
102
+ prompt: PERFORMANCE_AUDITOR_PROMPT,
103
+ tools: {
104
+ write: false,
105
+ edit: false,
106
+ },
107
+ permission: {
108
+ edit: "deny" as const,
109
+ },
110
+ },
111
+ "doubt-reviewer": {
112
+ mode: "subagent" as const,
113
+ description: "Adversarial Verification Engineer evaluating artifacts without author bias",
114
+ prompt: DOUBT_REVIEWER_PROMPT,
115
+ tools: {
116
+ write: false,
117
+ edit: false,
118
+ },
119
+ permission: {
120
+ edit: "deny" as const,
121
+ },
122
+ },
123
+ "codebase-researcher": {
124
+ mode: "subagent" as const,
125
+ description: "Scout Agent tracing seams, dependencies, and call graphs without polluting context",
126
+ prompt: CODEBASE_RESEARCHER_PROMPT,
127
+ tools: {
128
+ write: false,
129
+ edit: false,
130
+ },
131
+ permission: {
132
+ edit: "deny" as const,
133
+ },
134
+ },
135
+ "design-explorer": {
136
+ mode: "subagent" as const,
137
+ description: "Principal Architect producing contrasting 'Design It Twice' interface proposals",
138
+ prompt: DESIGN_EXPLORER_PROMPT,
139
+ tools: {
140
+ write: false,
141
+ edit: false,
142
+ },
143
+ permission: {
144
+ edit: "deny" as const,
145
+ },
146
+ },
147
+ "engineering-coach": {
148
+ mode: "subagent" as const,
149
+ description: "Elite Staff Mentor providing Socratic debugging guidance and design critique",
150
+ prompt: ENGINEERING_COACH_PROMPT,
151
+ tools: {
152
+ write: false,
153
+ edit: false,
154
+ },
155
+ permission: {
156
+ edit: "deny" as const,
157
+ },
158
+ },
159
+ }
160
+
161
+ export const workflow: FeatureModule = {
162
+ name: "workflow",
163
+ defaultEnabled: true,
164
+ tools: [],
165
+ async init(_ctx, options) {
166
+ const opts = (options ?? {}) as WorkflowOptions
167
+ if (opts.enabled === false) {
168
+ return {}
169
+ }
170
+
171
+ const skillsPath = getBundledSkillsDir(opts.skillsPath)
172
+
173
+ return {
174
+ config: async (cfg: any) => {
175
+ if (opts.commands !== false) {
176
+ cfg.command = {
177
+ ...WORKFLOW_COMMANDS,
178
+ ...(cfg.command ?? {}),
179
+ }
180
+ }
181
+
182
+ if (opts.subagents !== false) {
183
+ cfg.agent = {
184
+ ...WORKFLOW_AGENTS,
185
+ ...(cfg.agent ?? {}),
186
+ }
187
+ }
188
+
189
+ if (existsSync(skillsPath)) {
190
+ cfg.skills = typeof cfg.skills === "object" && cfg.skills !== null ? cfg.skills : {}
191
+ if (!Array.isArray(cfg.skills.paths)) {
192
+ cfg.skills.paths = []
193
+ }
194
+ if (!cfg.skills.paths.includes(skillsPath)) {
195
+ cfg.skills.paths.push(skillsPath)
196
+ }
197
+ }
198
+ },
199
+ }
200
+ },
201
+
202
+ setup: async (v2Context, options) => {
203
+ const opts = (options ?? {}) as WorkflowOptions
204
+ if (opts.enabled === false) return
205
+
206
+ if (opts.commands !== false && v2Context.command?.transform) {
207
+ await v2Context.command.transform(async (draft) => {
208
+ for (const [name, cmd] of Object.entries(WORKFLOW_COMMANDS)) {
209
+ draft.update(name, (current) => {
210
+ current.name = current.name ?? name
211
+ current.description = current.description ?? cmd.description
212
+ current.template = current.template ?? cmd.template
213
+ })
214
+ }
215
+ })
216
+ }
217
+
218
+ if (opts.subagents !== false && v2Context.agent?.transform) {
219
+ await v2Context.agent.transform(async (draft) => {
220
+ for (const [id, ag] of Object.entries(WORKFLOW_AGENTS)) {
221
+ draft.update(id, (current) => {
222
+ current.mode = current.mode ?? ag.mode
223
+ current.description = current.description ?? ag.description
224
+ current.system = current.system ?? ag.prompt
225
+ if (ag.permission?.edit === "deny") {
226
+ const perms = (current.permissions as any[]) ?? []
227
+ const hasDenyEdit = perms.some((p: any) => p.action === "edit" && p.effect === "deny")
228
+ if (!hasDenyEdit) {
229
+ perms.push({
230
+ action: "edit",
231
+ resource: "*",
232
+ effect: "deny",
233
+ })
234
+ current.permissions = perms as any
235
+ }
236
+ }
237
+ })
238
+ }
239
+ })
240
+ }
241
+
242
+ const skillsPath = getBundledSkillsDir(opts.skillsPath)
243
+ if (existsSync(skillsPath) && v2Context.skill?.transform) {
244
+ await v2Context.skill.transform(async (draft) => {
245
+ const existing = draft.list?.() ?? []
246
+ const alreadyAdded = existing.some((s: any) => s.type === "directory" && s.path === skillsPath)
247
+ if (!alreadyAdded) {
248
+ draft.source({
249
+ type: "directory",
250
+ path: skillsPath,
251
+ } as any)
252
+ }
253
+ })
254
+ }
255
+ },
256
+ }
package/src/index.ts CHANGED
@@ -1,85 +1,114 @@
1
- import type { Hooks, Plugin } from "@opencode-ai/plugin"
1
+ import type { Hooks } from "@opencode-ai/plugin"
2
2
  import { features } from "./features/index.ts"
3
- import { loadConfig } from "./config.ts"
4
- import { mergeHooks } from "./merge.ts"
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 { EMPTY_POLICY, resolveToolPolicy, type ToolPolicy } from "./tools.ts"
11
- import type { FeatureModule, SharedDeps } from "./types.ts"
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. Load config -> probe surfaces -> init enabled modules -> merge hooks.
15
- * Module crash or missing SDK surface (upstream drift) -> skip module, plugin survive.
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: Plugin = async (ctx) => {
18
- const config = await loadConfig(ctx.directory)
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
- // Collected now, reported once the tool policy is known so a single pass covers both.
21
- const issues = validateConfig(config, features)
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
- // Resolved after the init loop, against the modules that actually loaded -- warning about a
24
- // tool belonging to a disabled feature would be noise. Modules only call `toolName` at
25
- // runtime (a hook or timer, long after init), so reading it through this binding is safe.
26
- let policy: ToolPolicy = EMPTY_POLICY
27
- const shared: SharedDeps = {
28
- busy: createBusyTracker(),
29
- toolName: (declared) => policy.rename[declared] ?? declared,
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
- for (const feature of features) {
37
- const setting = config.features?.[feature.name] ?? feature.defaultEnabled
38
- if (setting === false) continue
39
- const missing = missingSurfaces(ctx.client, feature.requires ?? [])
40
- if (missing.length) {
41
- console.warn(
42
- `[overclock] ${feature.name} disabled: client lacks ${missing.join(", ")} (upstream drift?)`,
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
- const options = typeof setting === "object" ? setting : {}
48
- try {
49
- parts.push(await feature.init(ctx, options, shared))
50
- enabled.push(feature)
51
- } catch (e) {
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
- const resolved = resolveToolPolicy(config, enabled)
57
- policy = resolved.policy
58
- issues.push(...resolved.issues)
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
- // A mistyped key is otherwise a silent no-op -- the feature runs with defaults and the
61
- // user believes their setting took effect. Warn, never throw: bad config degrades to
62
- // defaults rather than taking the plugin down.
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
- // Say what was added. This plugin grants the agent background shell execution and
75
- // recurring scheduling; that should not be discovered by accident. Log every start
76
- // (stderr, invisible unless you look), toast only on a project's first run.
77
- console.warn(`[overclock] ${summarise(enabled, skipped, policy)}`)
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
- if (skipped.length)
83
- void toast(ctx.client, `overclock: ${skipped.join(", ")} disabled (SDK drift)`, "warning")
84
- return mergeHooks(parts, policy)
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"
@@ -0,0 +1,13 @@
1
+ export {
2
+ execBash,
3
+ killProcessTree,
4
+ shellQuote,
5
+ sanitizeEnv,
6
+ redactSensitiveOutput,
7
+ NON_INTERACTIVE_ENV,
8
+ DEFAULT_PRESERVED_ENV,
9
+ SENSITIVE_ENV_PATTERN,
10
+ SENSITIVE_OUTPUT_PATTERNS,
11
+ type ExecBashOptions,
12
+ type ExecBashResult,
13
+ } from "../platform/process/exec.ts"
package/src/lib/inject.ts CHANGED
@@ -1,56 +1,10 @@
1
- import type { PluginInput } from "@opencode-ai/plugin"
2
-
3
- type Client = PluginInput["client"]
4
- type ModelRef = { providerID: string; modelID: string }
5
-
6
- /**
7
- * Session's active model = model of last assistant message.
8
- * Without this, promptAsync falls back to config default model -> injected turns
9
- * run on the wrong model (and pile up QUEUED behind a hung default).
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
- import { mkdir } from "node:fs/promises"
2
-
3
- /** State root: <project>/.opencode/overclock/[sub]. Creates if missing. */
4
- export async function ensureStateDir(directory: string, sub?: string): Promise<string> {
5
- const dir = `${directory}/.opencode/overclock${sub ? `/${sub}` : ""}`
6
- await mkdir(dir, { recursive: true })
7
- return dir
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"
@@ -0,0 +1 @@
1
+ export { isInsideTmux, spawnTaskPane, type TmuxPane } from "../platform/process/tmux.ts"