@bybrawe/opencode-loop 0.5.28 → 0.5.30
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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +4 -4
- package/scripts/command-lifecycle-test.mjs +47 -0
- package/scripts/command-router-test.mjs +175 -0
- package/scripts/compaction-runtime-test.mjs +128 -0
- package/scripts/comprehensive-test.mjs +11 -3
- package/scripts/executor-runtime-test.mjs +310 -0
- package/scripts/fixtures/opencode2-real-adapter-probe.js +70 -0
- package/scripts/goal-command-handlers-test.mjs +236 -0
- package/scripts/goal-policy-test.mjs +143 -0
- package/scripts/goal-runtime-test.mjs +203 -0
- package/scripts/goal-steering-test.mjs +153 -0
- package/scripts/host-goal-steering-canary.mjs +443 -0
- package/scripts/host-loop-canary.mjs +54 -12
- package/scripts/job-workspace-test.mjs +164 -0
- package/scripts/loop-command-handlers-test.mjs +279 -0
- package/scripts/loop-registration-test.mjs +220 -0
- package/scripts/opencode2-loop-canary.mjs +433 -0
- package/scripts/publish-workflow-test.mjs +25 -0
- package/scripts/scheduler-runtime-test.mjs +147 -0
- package/scripts/session-activity-test.mjs +95 -0
- package/scripts/session-status-test.mjs +220 -0
- package/scripts/v2-command-adapter-test.mjs +113 -0
- package/scripts/v2-command-runtime-test.mjs +98 -0
- package/scripts/v2-diagnostics-test.mjs +74 -0
- package/scripts/v2-host-contract-test.mjs +27 -0
- package/scripts/v2-plugin-sentinel-test.mjs +60 -8
- package/scripts/v2-prompt-interval-test.mjs +171 -0
- package/scripts/v2-prompt-runtime-test.mjs +267 -0
- package/scripts/v2-runtime-adapter-test.mjs +326 -0
- package/src/index.js +2751 -1594
- package/src/source/legacy-v1.js +168 -1860
- package/src/source/opencode/command-router.js +103 -0
- package/src/source/opencode/commands.js +83 -0
- package/src/source/opencode/goal-commands.js +107 -0
- package/src/source/opencode/loop-commands.js +195 -0
- package/src/source/opencode/loop-registration.js +100 -0
- package/src/source/opencode/messages.js +55 -0
- package/src/source/opencode2/capabilities.js +3 -1
- package/src/source/opencode2/commands.js +72 -0
- package/src/source/opencode2/diagnostics.js +80 -0
- package/src/source/opencode2/event-bridge.js +19 -1
- package/src/source/opencode2/events.js +64 -0
- package/src/source/opencode2/experimental.js +41 -1
- package/src/source/opencode2/host-contract.js +26 -0
- package/src/source/opencode2/prompt-runtime.js +381 -0
- package/src/source/opencode2/runtime-adapter.js +56 -0
- package/src/source/opencode2/status.js +40 -0
- package/src/source/runtime/compaction.js +122 -0
- package/src/source/runtime/executor.js +558 -0
- package/src/source/runtime/goal-policy.js +77 -0
- package/src/source/runtime/goal-runtime.js +231 -0
- package/src/source/runtime/goal-steering.js +198 -0
- package/src/source/runtime/job-workspace.js +148 -0
- package/src/source/runtime/scheduler.js +212 -0
- package/src/source/runtime/session-activity.js +112 -0
- package/src/source/runtime/session-status.js +197 -0
- package/scripts/host-bridge-parity-test.mjs +0 -22
|
@@ -18,7 +18,7 @@ export const OPENCODE_LOOP_V2_RUNTIME_REQUIREMENTS = Object.freeze([
|
|
|
18
18
|
"runtime.adapter",
|
|
19
19
|
])
|
|
20
20
|
|
|
21
|
-
export const OPENCODE_LOOP_V2_COMMAND_SOURCE = "
|
|
21
|
+
export const OPENCODE_LOOP_V2_COMMAND_SOURCE = "plugin-transform"
|
|
22
22
|
|
|
23
23
|
export function inspectOpenCode2Context(ctx) {
|
|
24
24
|
const command = ctx?.command
|
|
@@ -31,6 +31,8 @@ export function inspectOpenCode2Context(ctx) {
|
|
|
31
31
|
eventSubscribe: hasFunction(event, "subscribe"),
|
|
32
32
|
sessionHook: hasFunction(session, "hook"),
|
|
33
33
|
sessionPrompt: hasFunction(session, "prompt"),
|
|
34
|
+
sessionCommand: hasFunction(session, "command"),
|
|
35
|
+
sessionShell: hasFunction(session, "shell"),
|
|
34
36
|
toolTransform: hasFunction(tool, "transform"),
|
|
35
37
|
toolHook: hasFunction(tool, "hook"),
|
|
36
38
|
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const OPENCODE_LOOP_V2_COMMANDS = Object.freeze({
|
|
2
|
+
loop: Object.freeze({
|
|
3
|
+
description: "Start an OpenCode auto-continue loop. Usage: /loop 5m <task>",
|
|
4
|
+
template: "OpenCode Loop local command handled. Reply exactly: OK.",
|
|
5
|
+
}),
|
|
6
|
+
"loop-pause": Object.freeze({
|
|
7
|
+
description: "Pause OpenCode Loop jobs.",
|
|
8
|
+
template: "OpenCode Loop pause command handled locally. Reply exactly: OK.",
|
|
9
|
+
}),
|
|
10
|
+
"loop-resume": Object.freeze({
|
|
11
|
+
description: "Resume OpenCode Loop jobs.",
|
|
12
|
+
template: "OpenCode Loop resume command handled locally. Reply exactly: OK.",
|
|
13
|
+
}),
|
|
14
|
+
"loop-stop": Object.freeze({
|
|
15
|
+
description: "Stop OpenCode Loop jobs.",
|
|
16
|
+
template: "OpenCode Loop stop command handled locally. Reply exactly: OK.",
|
|
17
|
+
}),
|
|
18
|
+
"loop-remove": Object.freeze({
|
|
19
|
+
description: "Remove OpenCode Loop jobs.",
|
|
20
|
+
template: "OpenCode Loop remove command handled locally. Reply exactly: OK.",
|
|
21
|
+
}),
|
|
22
|
+
"loop-clear": Object.freeze({
|
|
23
|
+
description: "Clear all OpenCode Loop jobs.",
|
|
24
|
+
template: "OpenCode Loop clear command handled locally. Reply exactly: OK.",
|
|
25
|
+
}),
|
|
26
|
+
"loop-status": Object.freeze({
|
|
27
|
+
description: "Show OpenCode Loop status for the current session.",
|
|
28
|
+
template: "OpenCode Loop status command handled locally. Reply exactly: OK.",
|
|
29
|
+
}),
|
|
30
|
+
"loop-now": Object.freeze({
|
|
31
|
+
description: "Run matching OpenCode Loop jobs on the next idle boundary.",
|
|
32
|
+
template: "OpenCode Loop run-now command handled locally. Reply exactly: OK.",
|
|
33
|
+
}),
|
|
34
|
+
"loop-export": Object.freeze({
|
|
35
|
+
description: "Export OpenCode Loop state for the current session.",
|
|
36
|
+
template: "OpenCode Loop export command handled locally. Reply exactly: OK.",
|
|
37
|
+
}),
|
|
38
|
+
"loop-help": Object.freeze({
|
|
39
|
+
description: "Show the experimental OpenCode 2 Loop command help.",
|
|
40
|
+
template: "OpenCode Loop help command handled locally. Reply exactly: OK.",
|
|
41
|
+
}),
|
|
42
|
+
"loop-doctor": Object.freeze({
|
|
43
|
+
description: "Show experimental OpenCode 2 Loop diagnostics.",
|
|
44
|
+
template: "OpenCode Loop doctor command handled locally. Reply exactly: OK.",
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export function registerOpenCode2LoopCommands(draft) {
|
|
49
|
+
if (!draft || typeof draft.update !== "function") {
|
|
50
|
+
throw new Error("OpenCode 2 command draft.update capability is unavailable")
|
|
51
|
+
}
|
|
52
|
+
for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
53
|
+
draft.update(name, (command) => {
|
|
54
|
+
command.template = definition.template
|
|
55
|
+
command.description = definition.description
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseOpenCode2LoopCommandText(value) {
|
|
61
|
+
if (typeof value !== "string") return undefined
|
|
62
|
+
for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
63
|
+
const template = definition.template
|
|
64
|
+
if (value === template) return Object.freeze({ name, arguments: "" })
|
|
65
|
+
if (!value.startsWith(`${template}\n\n`)) continue
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
name,
|
|
68
|
+
arguments: value.slice(template.length + 2).trim(),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readState as defaultReadState, stateDir } from "../core/state.js"
|
|
2
|
+
|
|
3
|
+
export const OPENCODE_LOOP_V2_HELP_TEXT = [
|
|
4
|
+
"OpenCode Loop V2 experimental help:",
|
|
5
|
+
"/loop 0s --max-runs 2 <prompt> autonomous prompt loop",
|
|
6
|
+
"/loop 5m --no-now --name later --multi <prompt> delayed named prompt loop",
|
|
7
|
+
"/loop 0s --command /review slash-command loop when the host exposes session.command",
|
|
8
|
+
"/loop-status show current Loop jobs",
|
|
9
|
+
"/loop-now [target] run matching jobs on the next idle boundary",
|
|
10
|
+
"/loop-pause [target] | /loop-resume [target] pause or resume jobs",
|
|
11
|
+
"/loop-stop [target] | /loop-remove [target] remove matching jobs",
|
|
12
|
+
"/loop-clear clear all jobs",
|
|
13
|
+
"/loop-export export current session Loop state as JSON",
|
|
14
|
+
"/loop-help show this experimental V2 help",
|
|
15
|
+
"/loop-doctor show local V2 diagnostics",
|
|
16
|
+
"Experimental V2 does not yet claim full stable-plugin parity; unsupported options fail closed.",
|
|
17
|
+
].join("\n")
|
|
18
|
+
|
|
19
|
+
function scopeFrom(event) {
|
|
20
|
+
const directory = typeof event?.directory === "string" ? event.directory.trim() : ""
|
|
21
|
+
const sessionID = String(event?.sessionID || "").trim()
|
|
22
|
+
if (!directory || !sessionID) return undefined
|
|
23
|
+
return { directory, sessionID }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createOpenCode2DiagnosticsRuntime(options = {}) {
|
|
27
|
+
if (typeof options.prompt !== "function") throw new TypeError("V2 diagnostics runtime requires prompt()")
|
|
28
|
+
const readState = typeof options.readState === "function" ? options.readState : defaultReadState
|
|
29
|
+
const runtimeVersion = options.runtimeVersion || process.version
|
|
30
|
+
const runtimePlatform = options.runtimePlatform || process.platform
|
|
31
|
+
|
|
32
|
+
async function exportState(event) {
|
|
33
|
+
const scope = scopeFrom(event)
|
|
34
|
+
if (!scope) return { handled: false, reason: "missing-scope" }
|
|
35
|
+
const state = await readState(scope.directory, scope.sessionID)
|
|
36
|
+
const text = `OpenCode loop state export:\n\`\`\`json\n${JSON.stringify(state, null, 2)}\n\`\`\``
|
|
37
|
+
const request = { sessionID: scope.sessionID, text, noReply: true }
|
|
38
|
+
await options.prompt(request)
|
|
39
|
+
return { handled: true, accepted: true, state, request }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function help(event) {
|
|
43
|
+
const scope = scopeFrom(event)
|
|
44
|
+
if (!scope) return { handled: false, reason: "missing-scope" }
|
|
45
|
+
const request = { sessionID: scope.sessionID, text: OPENCODE_LOOP_V2_HELP_TEXT, noReply: true }
|
|
46
|
+
await options.prompt(request)
|
|
47
|
+
return { handled: true, accepted: true, request }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function doctor(event) {
|
|
51
|
+
const scope = scopeFrom(event)
|
|
52
|
+
if (!scope) return { handled: false, reason: "missing-scope" }
|
|
53
|
+
const state = await readState(scope.directory, scope.sessionID)
|
|
54
|
+
const text = [
|
|
55
|
+
"OpenCode Loop V2 doctor:",
|
|
56
|
+
"- plugin: bybrawe.opencode-loop.v2.experimental",
|
|
57
|
+
`- project directory: ${scope.directory}`,
|
|
58
|
+
`- state directory: ${stateDir(scope.directory)}`,
|
|
59
|
+
`- active jobs: ${(state.jobs || []).length}`,
|
|
60
|
+
`- node: ${runtimeVersion}`,
|
|
61
|
+
`- platform: ${runtimePlatform}`,
|
|
62
|
+
"- full stable parity: not claimed",
|
|
63
|
+
"- smoke test: /loop 0s --max-runs 1 continue the current task",
|
|
64
|
+
].join("\n")
|
|
65
|
+
const request = { sessionID: scope.sessionID, text, noReply: true }
|
|
66
|
+
await options.prompt(request)
|
|
67
|
+
return { handled: true, accepted: true, state, request }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function onEvent(event) {
|
|
71
|
+
if (event?.kind === "command" && event?.action === "executed") {
|
|
72
|
+
if (event?.name === "loop-export") return await exportState(event)
|
|
73
|
+
if (event?.name === "loop-help") return await help(event)
|
|
74
|
+
if (event?.name === "loop-doctor") return await doctor(event)
|
|
75
|
+
}
|
|
76
|
+
return { handled: false }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Object.freeze({ onEvent, exportState, help, doctor })
|
|
80
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeOpenCodeEvent } from "../runtime/events.js"
|
|
2
2
|
import { createSessionRuntimeManager } from "../runtime/session-manager.js"
|
|
3
|
+
import { normalizeOpenCode2NativeEvent } from "./events.js"
|
|
3
4
|
|
|
4
5
|
function cleanupRegistration(registration) {
|
|
5
6
|
if (typeof registration === "function") return registration
|
|
@@ -41,6 +42,7 @@ export function createOpenCode2EventBridge({
|
|
|
41
42
|
let disposed = false
|
|
42
43
|
let managerDisposed = false
|
|
43
44
|
let queue = Promise.resolve()
|
|
45
|
+
const sessionDirectories = new Map()
|
|
44
46
|
|
|
45
47
|
function report(error) {
|
|
46
48
|
try { onError(error) } catch {}
|
|
@@ -61,20 +63,35 @@ export function createOpenCode2EventBridge({
|
|
|
61
63
|
return Boolean(current)
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
function normalize(raw) {
|
|
67
|
+
const current = normalizeOpenCode2NativeEvent(raw) || normalizeOpenCodeEvent(raw)
|
|
68
|
+
if (!current) return undefined
|
|
69
|
+
const sessionID = current.sessionID
|
|
70
|
+
const rememberedDirectory = sessionID ? sessionDirectories.get(sessionID) : undefined
|
|
71
|
+
const eventDirectory = current.directory || rememberedDirectory || directory
|
|
72
|
+
const event = eventDirectory === current.directory
|
|
73
|
+
? current
|
|
74
|
+
: Object.freeze({ ...current, directory: eventDirectory })
|
|
75
|
+
if (sessionID && event.directory) sessionDirectories.set(sessionID, event.directory)
|
|
76
|
+
return event
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
async function process(raw) {
|
|
65
80
|
if (stopped) return undefined
|
|
66
|
-
const event =
|
|
81
|
+
const event = normalize(raw)
|
|
67
82
|
if (!event || !sameDirectory(directory, event.directory)) return undefined
|
|
68
83
|
|
|
69
84
|
const runtime = event.sessionID ? runtimeManager.observeExternal(event.sessionID) : undefined
|
|
70
85
|
await onEvent(event, runtime)
|
|
71
86
|
|
|
72
87
|
if (event.kind === "session" && event.action === "deleted") {
|
|
88
|
+
sessionDirectories.delete(event.sessionID)
|
|
73
89
|
runtimeManager.remove(event.sessionID, { expectedRuntime: runtime, reason: "session-deleted" })
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
if (event.kind === "server" && event.action === "disposed") {
|
|
77
93
|
stopped = true
|
|
94
|
+
sessionDirectories.clear()
|
|
78
95
|
disposeManager("server-disposed")
|
|
79
96
|
}
|
|
80
97
|
|
|
@@ -141,6 +158,7 @@ export function createOpenCode2EventBridge({
|
|
|
141
158
|
const cleanup = cleanupRegistration(registration)
|
|
142
159
|
registration = undefined
|
|
143
160
|
if (cleanup) await cleanup()
|
|
161
|
+
sessionDirectories.clear()
|
|
144
162
|
disposeManager(reason)
|
|
145
163
|
return true
|
|
146
164
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseOpenCode2LoopCommandText } from "./commands.js"
|
|
2
|
+
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value && typeof value === "object" ? value : undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function text(value) {
|
|
8
|
+
return typeof value === "string" && value.trim() ? value : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function directoryFrom(raw) {
|
|
12
|
+
return text(record(raw?.location)?.directory)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sessionIDFrom(raw) {
|
|
16
|
+
const data = record(raw?.data)
|
|
17
|
+
return text(data?.sessionID)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeOpenCode2NativeEvent(raw) {
|
|
21
|
+
const type = text(raw?.type)
|
|
22
|
+
if (!type) return undefined
|
|
23
|
+
|
|
24
|
+
const data = record(raw?.data) || {}
|
|
25
|
+
const directory = directoryFrom(raw)
|
|
26
|
+
const sessionID = sessionIDFrom(raw)
|
|
27
|
+
|
|
28
|
+
if (type === "session.inbox.enqueued") {
|
|
29
|
+
const item = record(data.item)
|
|
30
|
+
const payload = record(item?.payload)
|
|
31
|
+
const parsed = item?.type === "user" ? parseOpenCode2LoopCommandText(payload?.text) : undefined
|
|
32
|
+
if (!sessionID || !parsed) return undefined
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
kind: "command",
|
|
35
|
+
action: "executed",
|
|
36
|
+
sessionID,
|
|
37
|
+
directory,
|
|
38
|
+
name: parsed.name,
|
|
39
|
+
arguments: parsed.arguments,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (type === "session.execution.started") {
|
|
44
|
+
if (!sessionID) return undefined
|
|
45
|
+
return Object.freeze({ kind: "session", action: "status", sessionID, directory, status: "busy" })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (type === "session.execution.succeeded") {
|
|
49
|
+
if (!sessionID) return undefined
|
|
50
|
+
return Object.freeze({ kind: "session", action: "idle", sessionID, directory })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "session.created") {
|
|
54
|
+
if (!sessionID) return undefined
|
|
55
|
+
return Object.freeze({ kind: "session", action: "created", sessionID, directory: directory || text(record(data.location)?.directory) })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (type === "session.deleted") {
|
|
59
|
+
if (!sessionID) return undefined
|
|
60
|
+
return Object.freeze({ kind: "session", action: "deleted", sessionID, directory })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { inspectOpenCode2Context } from "./capabilities.js"
|
|
2
|
+
import { OPENCODE_LOOP_V2_COMMANDS, registerOpenCode2LoopCommands } from "./commands.js"
|
|
3
|
+
import { createOpenCode2DiagnosticsRuntime } from "./diagnostics.js"
|
|
4
|
+
import { createOpenCode2PromptRuntime } from "./prompt-runtime.js"
|
|
5
|
+
import { createOpenCode2RuntimeAdapter } from "./runtime-adapter.js"
|
|
2
6
|
|
|
3
7
|
export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental"
|
|
8
|
+
export { OPENCODE_LOOP_V2_COMMANDS }
|
|
4
9
|
|
|
5
10
|
export const OpenCodeLoopV2ExperimentalPlugin = {
|
|
6
11
|
id: OPENCODE_LOOP_V2_PLUGIN_ID,
|
|
@@ -9,7 +14,42 @@ export const OpenCodeLoopV2ExperimentalPlugin = {
|
|
|
9
14
|
if (!capabilities.commandTransform) {
|
|
10
15
|
throw new Error("OpenCode 2 command.transform capability is unavailable")
|
|
11
16
|
}
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
const commandRegistration = await ctx.command.transform(registerOpenCode2LoopCommands)
|
|
19
|
+
if (!capabilities.eventSubscribe || !capabilities.sessionPrompt) {
|
|
20
|
+
await commandRegistration?.dispose?.()
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let promptRuntime
|
|
25
|
+
let diagnosticsRuntime
|
|
26
|
+
const onRuntimeEvent = async (event) => {
|
|
27
|
+
const promptResult = await promptRuntime?.onEvent(event)
|
|
28
|
+
if (promptResult?.handled) return promptResult
|
|
29
|
+
return await diagnosticsRuntime?.onEvent(event) ?? promptResult
|
|
30
|
+
}
|
|
31
|
+
const runtime = createOpenCode2RuntimeAdapter(ctx, { onEvent: onRuntimeEvent })
|
|
32
|
+
promptRuntime = createOpenCode2PromptRuntime({
|
|
33
|
+
prompt: (request) => runtime.prompt(request),
|
|
34
|
+
command: capabilities.sessionCommand ? (request) => runtime.command(request) : undefined,
|
|
35
|
+
})
|
|
36
|
+
diagnosticsRuntime = createOpenCode2DiagnosticsRuntime({
|
|
37
|
+
prompt: (request) => runtime.prompt(request),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await runtime.start()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await promptRuntime?.dispose?.().catch(() => undefined)
|
|
44
|
+
await commandRegistration?.dispose?.().catch(() => undefined)
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return async () => {
|
|
49
|
+
await promptRuntime?.dispose?.()
|
|
50
|
+
await runtime.dispose("plugin-cleanup")
|
|
51
|
+
await commandRegistration?.dispose?.()
|
|
52
|
+
}
|
|
13
53
|
},
|
|
14
54
|
}
|
|
15
55
|
|
|
@@ -16,6 +16,22 @@ function normalizePrompt(input) {
|
|
|
16
16
|
})
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizeCommand(input) {
|
|
20
|
+
const sessionID = String(input?.sessionID || "").trim()
|
|
21
|
+
const command = String(input?.command || "").trim().replace(/^\/+/, "")
|
|
22
|
+
if (!sessionID) throw new TypeError("command requires a session ID")
|
|
23
|
+
if (!command) throw new TypeError("command requires a command name")
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
sessionID,
|
|
26
|
+
command,
|
|
27
|
+
arguments: input?.arguments === undefined ? undefined : String(input.arguments),
|
|
28
|
+
agent: input?.agent,
|
|
29
|
+
model: input?.model,
|
|
30
|
+
delivery: input?.delivery,
|
|
31
|
+
resume: input?.resume,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export function createOpenCode2HostContract(options = {}) {
|
|
20
36
|
const onEvent = typeof options.onEvent === "function" ? options.onEvent : async () => {}
|
|
21
37
|
let started = false
|
|
@@ -49,6 +65,15 @@ export function createOpenCode2HostContract(options = {}) {
|
|
|
49
65
|
return await options.sendPrompt(Object.freeze({ ...request, runtime }))
|
|
50
66
|
}
|
|
51
67
|
|
|
68
|
+
async function command(input) {
|
|
69
|
+
if (disposed || hostDisposed) throw new Error("OpenCode 2 host contract is unavailable")
|
|
70
|
+
if (!started) throw new Error("OpenCode 2 host contract is not started")
|
|
71
|
+
if (typeof options.sendCommand !== "function") throw new Error("OpenCode 2 session.command capability is unavailable")
|
|
72
|
+
const request = normalizeCommand(input)
|
|
73
|
+
const runtime = bridge.runtimeManager.observeExternal(request.sessionID)
|
|
74
|
+
return await options.sendCommand(Object.freeze({ ...request, runtime }))
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
async function dispose(reason = "host-contract-disposed") {
|
|
53
78
|
if (disposed) return false
|
|
54
79
|
disposed = true
|
|
@@ -59,6 +84,7 @@ export function createOpenCode2HostContract(options = {}) {
|
|
|
59
84
|
return Object.freeze({
|
|
60
85
|
start,
|
|
61
86
|
prompt,
|
|
87
|
+
command,
|
|
62
88
|
dispose,
|
|
63
89
|
runtimeManager: bridge.runtimeManager,
|
|
64
90
|
isStarted: () => started,
|