@aryaminus/controlkeel-opencode 0.2.23 → 0.2.25
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/.opencode/plugins/controlkeel-governance.ts +103 -23
- package/index.js +103 -23
- package/package.json +1 -1
|
@@ -11,19 +11,41 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
11
11
|
* - routes plan submission and wait decisions through the CK CLI
|
|
12
12
|
*/
|
|
13
13
|
export const ControlKeelGovernance: Plugin = async ({ project, client, $, directory }) => {
|
|
14
|
-
const
|
|
14
|
+
const extractJsonCandidates = (output: string) => {
|
|
15
15
|
const trimmed = output.trim()
|
|
16
16
|
if (!trimmed) {
|
|
17
|
-
return
|
|
17
|
+
return []
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const lines = trimmed
|
|
21
21
|
.split(/\r?\n/)
|
|
22
|
-
.map((line) => line.
|
|
23
|
-
.filter((line) => line.length > 0)
|
|
22
|
+
.map((line) => line.trimEnd())
|
|
23
|
+
.filter((line) => line.trim().length > 0)
|
|
24
|
+
|
|
25
|
+
const candidates: string[] = []
|
|
26
|
+
const seen = new Set<string>()
|
|
27
|
+
|
|
28
|
+
const pushCandidate = (candidate: string) => {
|
|
29
|
+
const normalized = candidate.trim()
|
|
30
|
+
if (!normalized || seen.has(normalized)) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
seen.add(normalized)
|
|
35
|
+
candidates.push(normalized)
|
|
36
|
+
}
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
pushCandidate(trimmed)
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
41
|
+
const line = lines[i].trimStart()
|
|
42
|
+
if (line.startsWith("{") || line.startsWith("[")) {
|
|
43
|
+
pushCandidate(line)
|
|
44
|
+
pushCandidate(lines.slice(i).join("\n"))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return candidates
|
|
27
49
|
}
|
|
28
50
|
|
|
29
51
|
const parseJson = (output: string) => {
|
|
@@ -35,17 +57,14 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
35
57
|
try {
|
|
36
58
|
return JSON.parse(trimmed)
|
|
37
59
|
} catch (_error) {
|
|
38
|
-
const candidate
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
for (const candidate of extractJsonCandidates(trimmed)) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(candidate)
|
|
63
|
+
} catch (_fallbackError) {
|
|
64
|
+
}
|
|
42
65
|
}
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
return JSON.parse(candidate)
|
|
46
|
-
} catch (_fallbackError) {
|
|
47
|
-
throw new Error(`ControlKeel returned invalid JSON: ${output}`)
|
|
48
|
-
}
|
|
67
|
+
throw new Error(`ControlKeel returned invalid JSON: ${output}`)
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -161,6 +180,52 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
161
180
|
}
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
const resolveReviewScope = async () => {
|
|
184
|
+
const envTaskId = process.env.CONTROLKEEL_TASK_ID
|
|
185
|
+
const envSessionId = process.env.CONTROLKEEL_SESSION_ID
|
|
186
|
+
|
|
187
|
+
if (envTaskId || envSessionId) {
|
|
188
|
+
return {
|
|
189
|
+
taskId: envTaskId ?? null,
|
|
190
|
+
sessionId: envSessionId ?? null,
|
|
191
|
+
source: "env",
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const contextEnv = process.env.LOGGER_LEVEL
|
|
196
|
+
? process.env
|
|
197
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
198
|
+
|
|
199
|
+
const contextProc = Bun.spawn(["controlkeel", "context", "--json", "--project-root", directory], {
|
|
200
|
+
stdout: "pipe",
|
|
201
|
+
stderr: "pipe",
|
|
202
|
+
env: contextEnv,
|
|
203
|
+
})
|
|
204
|
+
const contextOut = await new Response(contextProc.stdout).text()
|
|
205
|
+
const contextErr = await new Response(contextProc.stderr).text()
|
|
206
|
+
const contextExit = await contextProc.exited
|
|
207
|
+
|
|
208
|
+
if (contextExit !== 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`controlkeel context --json failed with exit code ${contextExit}${contextErr.trim() ? `: ${contextErr.trim()}` : ""}`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const contextPayload = parseJson([contextOut, contextErr].filter(Boolean).join("\n"))
|
|
215
|
+
const contextTaskId = contextPayload?.current_task?.id
|
|
216
|
+
const contextSessionId = contextPayload?.session_id
|
|
217
|
+
|
|
218
|
+
if (!contextTaskId && !contextSessionId) {
|
|
219
|
+
throw new Error("ControlKeel context did not include a session_id or current_task.id")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
taskId: contextTaskId != null ? String(contextTaskId) : null,
|
|
224
|
+
sessionId: contextSessionId != null ? String(contextSessionId) : null,
|
|
225
|
+
source: "context",
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
164
229
|
const submitPlan = async (
|
|
165
230
|
body: string,
|
|
166
231
|
submittedBy: string,
|
|
@@ -169,8 +234,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
169
234
|
) => {
|
|
170
235
|
await ensurePlanSubmitSupport()
|
|
171
236
|
|
|
172
|
-
const
|
|
173
|
-
const envSessionId = process.env.CONTROLKEEL_SESSION_ID
|
|
237
|
+
const reviewScope = await resolveReviewScope()
|
|
174
238
|
const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
|
|
175
239
|
const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
|
|
176
240
|
|
|
@@ -181,10 +245,18 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
181
245
|
try {
|
|
182
246
|
const submitArgs = ["controlkeel", "review", "plan", "submit", "--body-file", tmpFile, "--submitted-by", submittedBy, "--json"]
|
|
183
247
|
if (title) submitArgs.push("--title", title)
|
|
184
|
-
if (
|
|
185
|
-
else if (
|
|
248
|
+
if (reviewScope.taskId) submitArgs.push("--task-id", reviewScope.taskId)
|
|
249
|
+
else if (reviewScope.sessionId) submitArgs.push("--session-id", reviewScope.sessionId)
|
|
186
250
|
|
|
187
|
-
const
|
|
251
|
+
const submitEnv = process.env.LOGGER_LEVEL
|
|
252
|
+
? process.env
|
|
253
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
254
|
+
|
|
255
|
+
const submitProc = Bun.spawn(submitArgs, {
|
|
256
|
+
stdout: "pipe",
|
|
257
|
+
stderr: "pipe",
|
|
258
|
+
env: submitEnv,
|
|
259
|
+
})
|
|
188
260
|
const submitOut = await new Response(submitProc.stdout).text()
|
|
189
261
|
const submitErr = await new Response(submitProc.stderr).text()
|
|
190
262
|
const submitExit = await submitProc.exited
|
|
@@ -195,7 +267,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
195
267
|
)
|
|
196
268
|
}
|
|
197
269
|
|
|
198
|
-
const submitPayload = parseJson(submitOut
|
|
270
|
+
const submitPayload = parseJson([submitOut, submitErr].filter(Boolean).join("\n"))
|
|
199
271
|
|
|
200
272
|
if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
|
|
201
273
|
throw new Error(
|
|
@@ -208,7 +280,15 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
208
280
|
throw new Error("ControlKeel did not return a review id")
|
|
209
281
|
}
|
|
210
282
|
|
|
211
|
-
const
|
|
283
|
+
const waitEnv = process.env.LOGGER_LEVEL
|
|
284
|
+
? process.env
|
|
285
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
286
|
+
|
|
287
|
+
const waitProc = Bun.spawn(["controlkeel", "review", "plan", "wait", "--id", String(reviewId), "--timeout", String(waitTimeoutSecondsSafe), "--json"], {
|
|
288
|
+
stdout: "pipe",
|
|
289
|
+
stderr: "pipe",
|
|
290
|
+
env: waitEnv,
|
|
291
|
+
})
|
|
212
292
|
const waitOut = await new Response(waitProc.stdout).text()
|
|
213
293
|
const waitErr = await new Response(waitProc.stderr).text()
|
|
214
294
|
const waitExit = await waitProc.exited
|
|
@@ -219,7 +299,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
219
299
|
)
|
|
220
300
|
}
|
|
221
301
|
|
|
222
|
-
const waitPayload = parseJson(waitOut
|
|
302
|
+
const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
|
|
223
303
|
return {
|
|
224
304
|
reviewId,
|
|
225
305
|
submitPayload,
|
package/index.js
CHANGED
|
@@ -7,19 +7,41 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
7
7
|
* but ships as plain JavaScript for npm-based installs.
|
|
8
8
|
*/
|
|
9
9
|
export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
10
|
-
const
|
|
10
|
+
const extractJsonCandidates = (output) => {
|
|
11
11
|
const trimmed = output.trim()
|
|
12
12
|
if (!trimmed) {
|
|
13
|
-
return
|
|
13
|
+
return []
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const lines = trimmed
|
|
17
17
|
.split(/\r?\n/)
|
|
18
|
-
.map((line) => line.
|
|
19
|
-
.filter((line) => line.length > 0)
|
|
18
|
+
.map((line) => line.trimEnd())
|
|
19
|
+
.filter((line) => line.trim().length > 0)
|
|
20
|
+
|
|
21
|
+
const candidates = []
|
|
22
|
+
const seen = new Set()
|
|
23
|
+
|
|
24
|
+
const pushCandidate = (candidate) => {
|
|
25
|
+
const normalized = candidate.trim()
|
|
26
|
+
if (!normalized || seen.has(normalized)) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
seen.add(normalized)
|
|
31
|
+
candidates.push(normalized)
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
pushCandidate(trimmed)
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
37
|
+
const line = lines[i].trimStart()
|
|
38
|
+
if (line.startsWith("{") || line.startsWith("[")) {
|
|
39
|
+
pushCandidate(line)
|
|
40
|
+
pushCandidate(lines.slice(i).join("\n"))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return candidates
|
|
23
45
|
}
|
|
24
46
|
|
|
25
47
|
const parseJson = (output) => {
|
|
@@ -31,17 +53,14 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
31
53
|
try {
|
|
32
54
|
return JSON.parse(trimmed)
|
|
33
55
|
} catch (_error) {
|
|
34
|
-
const candidate
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
for (const candidate of extractJsonCandidates(trimmed)) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(candidate)
|
|
59
|
+
} catch (_fallbackError) {
|
|
60
|
+
}
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
return JSON.parse(candidate)
|
|
42
|
-
} catch (_fallbackError) {
|
|
43
|
-
throw new Error(`ControlKeel returned invalid JSON: ${output}`)
|
|
44
|
-
}
|
|
63
|
+
throw new Error(`ControlKeel returned invalid JSON: ${output}`)
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
|
|
@@ -154,11 +173,56 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
|
|
176
|
+
const resolveReviewScope = async () => {
|
|
177
|
+
const envTaskId = process.env.CONTROLKEEL_TASK_ID
|
|
178
|
+
const envSessionId = process.env.CONTROLKEEL_SESSION_ID
|
|
179
|
+
|
|
180
|
+
if (envTaskId || envSessionId) {
|
|
181
|
+
return {
|
|
182
|
+
taskId: envTaskId ?? null,
|
|
183
|
+
sessionId: envSessionId ?? null,
|
|
184
|
+
source: "env",
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const contextEnv = process.env.LOGGER_LEVEL
|
|
189
|
+
? process.env
|
|
190
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
191
|
+
|
|
192
|
+
const contextProc = Bun.spawn(["controlkeel", "context", "--json", "--project-root", directory], {
|
|
193
|
+
stdout: "pipe",
|
|
194
|
+
stderr: "pipe",
|
|
195
|
+
env: contextEnv,
|
|
196
|
+
})
|
|
197
|
+
const contextOut = await new Response(contextProc.stdout).text()
|
|
198
|
+
const contextErr = await new Response(contextProc.stderr).text()
|
|
199
|
+
const contextExit = await contextProc.exited
|
|
200
|
+
|
|
201
|
+
if (contextExit !== 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`controlkeel context --json failed with exit code ${contextExit}${contextErr.trim() ? `: ${contextErr.trim()}` : ""}`
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const contextPayload = parseJson([contextOut, contextErr].filter(Boolean).join("\n"))
|
|
208
|
+
const contextTaskId = contextPayload?.current_task?.id
|
|
209
|
+
const contextSessionId = contextPayload?.session_id
|
|
210
|
+
|
|
211
|
+
if (!contextTaskId && !contextSessionId) {
|
|
212
|
+
throw new Error("ControlKeel context did not include a session_id or current_task.id")
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
taskId: contextTaskId != null ? String(contextTaskId) : null,
|
|
217
|
+
sessionId: contextSessionId != null ? String(contextSessionId) : null,
|
|
218
|
+
source: "context",
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
157
222
|
const submitPlan = async (body, submittedBy, title, waitTimeoutSeconds) => {
|
|
158
223
|
await ensurePlanSubmitSupport()
|
|
159
224
|
|
|
160
|
-
const
|
|
161
|
-
const envSessionId = process.env.CONTROLKEEL_SESSION_ID
|
|
225
|
+
const reviewScope = await resolveReviewScope()
|
|
162
226
|
const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
|
|
163
227
|
const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
|
|
164
228
|
|
|
@@ -169,10 +233,18 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
169
233
|
try {
|
|
170
234
|
const submitArgs = ["controlkeel", "review", "plan", "submit", "--body-file", tmpFile, "--submitted-by", submittedBy, "--json"]
|
|
171
235
|
if (title) submitArgs.push("--title", title)
|
|
172
|
-
if (
|
|
173
|
-
else if (
|
|
236
|
+
if (reviewScope.taskId) submitArgs.push("--task-id", reviewScope.taskId)
|
|
237
|
+
else if (reviewScope.sessionId) submitArgs.push("--session-id", reviewScope.sessionId)
|
|
174
238
|
|
|
175
|
-
const
|
|
239
|
+
const submitEnv = process.env.LOGGER_LEVEL
|
|
240
|
+
? process.env
|
|
241
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
242
|
+
|
|
243
|
+
const submitProc = Bun.spawn(submitArgs, {
|
|
244
|
+
stdout: "pipe",
|
|
245
|
+
stderr: "pipe",
|
|
246
|
+
env: submitEnv,
|
|
247
|
+
})
|
|
176
248
|
const submitOut = await new Response(submitProc.stdout).text()
|
|
177
249
|
const submitErr = await new Response(submitProc.stderr).text()
|
|
178
250
|
const submitExit = await submitProc.exited
|
|
@@ -183,7 +255,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
183
255
|
)
|
|
184
256
|
}
|
|
185
257
|
|
|
186
|
-
const submitPayload = parseJson(submitOut
|
|
258
|
+
const submitPayload = parseJson([submitOut, submitErr].filter(Boolean).join("\n"))
|
|
187
259
|
|
|
188
260
|
if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
|
|
189
261
|
throw new Error(
|
|
@@ -196,7 +268,15 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
196
268
|
throw new Error("ControlKeel did not return a review id")
|
|
197
269
|
}
|
|
198
270
|
|
|
199
|
-
const
|
|
271
|
+
const waitEnv = process.env.LOGGER_LEVEL
|
|
272
|
+
? process.env
|
|
273
|
+
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
274
|
+
|
|
275
|
+
const waitProc = Bun.spawn(["controlkeel", "review", "plan", "wait", "--id", String(reviewId), "--timeout", String(waitTimeoutSecondsSafe), "--json"], {
|
|
276
|
+
stdout: "pipe",
|
|
277
|
+
stderr: "pipe",
|
|
278
|
+
env: waitEnv,
|
|
279
|
+
})
|
|
200
280
|
const waitOut = await new Response(waitProc.stdout).text()
|
|
201
281
|
const waitErr = await new Response(waitProc.stderr).text()
|
|
202
282
|
const waitExit = await waitProc.exited
|
|
@@ -207,7 +287,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
207
287
|
)
|
|
208
288
|
}
|
|
209
289
|
|
|
210
|
-
const waitPayload = parseJson(waitOut
|
|
290
|
+
const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
|
|
211
291
|
return {
|
|
212
292
|
reviewId,
|
|
213
293
|
submitPayload,
|
package/package.json
CHANGED