docks-kit 0.4.0 → 0.6.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/AGENTS.md +6 -2
- package/cli/docs/flags.md +6 -1
- package/cli/docs/install.md +24 -0
- package/cli/docs/models.md +8 -8
- package/cli/docs/modifiers.md +20 -3
- package/cli/docs/sync-layers.md +12 -9
- package/cli/src/commands/status.ts +9 -2
- package/cli/src/commands/sync.ts +34 -3
- package/cli/src/efforts.ts +91 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +101 -0
- package/cli/src/engine-native/claudeSync.ts +9 -2
- package/cli/src/engine-native/codexSync.ts +7 -1
- package/cli/src/engine-native/codexToml.ts +41 -8
- package/cli/src/engine-native/index.ts +18 -2
- package/cli/src/engine-native/modes.ts +1 -1
- package/cli/src/engine-native/parseArgs.ts +114 -7
- package/cli/src/engine-native/sessionRelayReadiness.ts +94 -0
- package/cli/src/generated/sotPayload.ts +9 -7
- package/cli/src/main.ts +19 -9
- package/package.json +1 -1
- package/cli/src/engine-native/claudeModel.ts +0 -54
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { p } from "./exec"
|
|
8
8
|
import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
9
9
|
|
|
10
|
+
import { resolveEffort } from "../efforts"
|
|
10
11
|
import type { Ctx } from "./index"
|
|
11
12
|
|
|
12
13
|
export function replaceTopLevelSetting(content: string, key: string, replacement: string): string {
|
|
@@ -48,27 +49,59 @@ export function replaceTopLevelSettingInFile(file: string, key: string, replacem
|
|
|
48
49
|
return true
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
interface CodexSettingEdit {
|
|
53
|
+
readonly tag: string
|
|
54
|
+
readonly key: "model" | "model_reasoning_effort"
|
|
55
|
+
readonly value: string
|
|
56
|
+
readonly changed: string
|
|
57
|
+
readonly unchanged: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function syncCodexSetting(ctx: Ctx, edit: CodexSettingEdit): void {
|
|
52
61
|
const { change, echo, verbose, warn } = ctx.services.logger
|
|
53
62
|
const userCodexSettings = p(ctx.home, ".codex", "config.toml")
|
|
54
63
|
|
|
55
|
-
if (model === "") return
|
|
56
|
-
|
|
57
64
|
if (ctx.dryRun) {
|
|
58
|
-
echo(`[dry-run] (
|
|
65
|
+
echo(`[dry-run] (${edit.tag}) set ${edit.key} = "${edit.value}" in ${userCodexSettings}`)
|
|
59
66
|
return
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
try {
|
|
63
70
|
readFileSync(userCodexSettings)
|
|
64
71
|
} catch {
|
|
65
|
-
warn(`(
|
|
72
|
+
warn(`(${edit.tag}) ${userCodexSettings} missing — skipped`)
|
|
66
73
|
return
|
|
67
74
|
}
|
|
68
|
-
if (replaceTopLevelSettingInFile(userCodexSettings,
|
|
69
|
-
change(
|
|
75
|
+
if (replaceTopLevelSettingInFile(userCodexSettings, edit.key, `${edit.key} = "${edit.value}"`)) {
|
|
76
|
+
change(edit.changed)
|
|
70
77
|
ctx.nextStepTriggers.codexRestart = true
|
|
71
78
|
} else {
|
|
72
|
-
verbose(
|
|
79
|
+
verbose(edit.unchanged)
|
|
73
80
|
}
|
|
74
81
|
}
|
|
82
|
+
|
|
83
|
+
export function syncCodexModel(ctx: Ctx, model: string): void {
|
|
84
|
+
if (model === "") return
|
|
85
|
+
syncCodexSetting(ctx, {
|
|
86
|
+
tag: "--codex-model",
|
|
87
|
+
key: "model",
|
|
88
|
+
value: model,
|
|
89
|
+
changed: `Model: deployed Codex model set to ${model} (SoT unchanged; flag-less sync reverts)`,
|
|
90
|
+
unchanged: `Model: deployed Codex model already ${model}`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function syncCodexEffort(ctx: Ctx, effort: string): void {
|
|
95
|
+
if (effort === "") return
|
|
96
|
+
const resolved = resolveEffort("codex", effort)
|
|
97
|
+
const useDefault = effort === "default"
|
|
98
|
+
syncCodexSetting(ctx, {
|
|
99
|
+
tag: "--codex-effort",
|
|
100
|
+
key: "model_reasoning_effort",
|
|
101
|
+
value: resolved,
|
|
102
|
+
changed: useDefault
|
|
103
|
+
? `Effort: deployed Codex model_reasoning_effort set to ${resolved} (SoT default)`
|
|
104
|
+
: `Effort: deployed Codex model_reasoning_effort set to ${resolved} (SoT unchanged; flag-less sync reverts)`,
|
|
105
|
+
unchanged: `Effort: deployed Codex model_reasoning_effort already ${resolved}`
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -15,7 +15,14 @@ import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
|
|
|
15
15
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
16
16
|
import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
|
|
17
17
|
import { modeModel, modeToolchain } from "./modes"
|
|
18
|
-
import { ExitError, parseArgs,
|
|
18
|
+
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
|
|
19
|
+
|
|
20
|
+
export type ModifierFlag =
|
|
21
|
+
| "--claude-model"
|
|
22
|
+
| "--claude-effort"
|
|
23
|
+
| "--claude-advisor"
|
|
24
|
+
| "--codex-model"
|
|
25
|
+
| "--codex-effort"
|
|
19
26
|
|
|
20
27
|
export interface Ctx {
|
|
21
28
|
readonly repoDir: string
|
|
@@ -31,7 +38,12 @@ export interface Ctx {
|
|
|
31
38
|
claudePermissive: boolean
|
|
32
39
|
claudePlugins: Array<string>
|
|
33
40
|
claudeModel: string
|
|
41
|
+
claudeEffort: string
|
|
42
|
+
claudeAdvisor: string
|
|
34
43
|
codexModel: string
|
|
44
|
+
codexEffort: string
|
|
45
|
+
/** Distinguishes an explicitly empty modifier from an option that was not supplied. */
|
|
46
|
+
modifierFlags?: Set<ModifierFlag>
|
|
35
47
|
/** Injected capability seam (logger/deps/platform) — see services.ts. */
|
|
36
48
|
readonly services: EngineServices
|
|
37
49
|
bunRuntime?: BunRuntimeState
|
|
@@ -66,7 +78,11 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
66
78
|
claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
|
|
67
79
|
claudePlugins: (env["CLAUDE_PLUGINS"] ?? "").split(" ").filter((s) => s !== ""),
|
|
68
80
|
claudeModel: env["CLAUDE_MODEL"] ?? "",
|
|
81
|
+
claudeEffort: "",
|
|
82
|
+
claudeAdvisor: "",
|
|
69
83
|
codexModel: env["CODEX_MODEL"] ?? "",
|
|
84
|
+
codexEffort: "",
|
|
85
|
+
modifierFlags: new Set(),
|
|
70
86
|
services,
|
|
71
87
|
targetFilterSet: false,
|
|
72
88
|
syncClaude: false,
|
|
@@ -79,7 +95,7 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
79
95
|
function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
80
96
|
const { echo } = ctx.services.logger
|
|
81
97
|
parseArgs(ctx, args)
|
|
82
|
-
|
|
98
|
+
validateModifierFlags(ctx)
|
|
83
99
|
|
|
84
100
|
const claudeRan = ctx.syncClaude
|
|
85
101
|
const claudeRuntime = claudeRan ? claudeSync(ctx) : undefined
|
|
@@ -6,7 +6,7 @@ import { p } from "./exec"
|
|
|
6
6
|
import { readFileSync } from "node:fs"
|
|
7
7
|
import { payloadText } from "../payload"
|
|
8
8
|
|
|
9
|
-
import { syncClaudeModel } from "./
|
|
9
|
+
import { syncClaudeModel } from "./claudeSettingsModifiers"
|
|
10
10
|
import { syncCodexModel } from "./codexToml"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { isObject, parseJson, type Json } from "./jq"
|
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
* early parser exit and is caught once in runEngineNative.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Ctx } from "./index"
|
|
7
|
+
import type { Ctx, ModifierFlag } from "./index"
|
|
8
|
+
import {
|
|
9
|
+
CLAUDE_ADVISOR_STATES,
|
|
10
|
+
advisorCatalog,
|
|
11
|
+
advisorFlagGrammar,
|
|
12
|
+
advisorValueGrammar,
|
|
13
|
+
effortCatalog,
|
|
14
|
+
effortFlagGrammar,
|
|
15
|
+
effortValueGrammar,
|
|
16
|
+
isEffortModifierValue
|
|
17
|
+
} from "../efforts"
|
|
8
18
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
9
19
|
|
|
10
20
|
export class ExitError extends Error {
|
|
@@ -14,6 +24,13 @@ export class ExitError extends Error {
|
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
27
|
+
const MODIFIER_FLAGS = new Set<ModifierFlag>([
|
|
28
|
+
"--claude-model",
|
|
29
|
+
"--claude-effort",
|
|
30
|
+
"--claude-advisor",
|
|
31
|
+
"--codex-model",
|
|
32
|
+
"--codex-effort"
|
|
33
|
+
])
|
|
17
34
|
|
|
18
35
|
function usage(ctx: Ctx): void {
|
|
19
36
|
const { echo } = ctx.services.logger
|
|
@@ -41,6 +58,8 @@ function usage(ctx: Ctx): void {
|
|
|
41
58
|
echo(
|
|
42
59
|
" --claude-model=<m> set deployed ~/.claude/settings.json model (aliases: best|opus|fable|sonnet|haiku, full claude-* IDs, or 'default' to unset)"
|
|
43
60
|
)
|
|
61
|
+
echo(` ${effortFlagGrammar("claude").padEnd(39)} set deployed effortLevel`)
|
|
62
|
+
echo(` ${advisorFlagGrammar().padEnd(39)} set deployed advisor state`)
|
|
44
63
|
echo(
|
|
45
64
|
" --claude-compact-window=<n> set deployed autocompact window in tokens (e.g. 680000 or 680k) for disposable sessions"
|
|
46
65
|
)
|
|
@@ -48,6 +67,7 @@ function usage(ctx: Ctx): void {
|
|
|
48
67
|
" --claude-permissive empty permissions.ask/deny in deployed settings (sandboxes/containers; unattended commits + pushes)"
|
|
49
68
|
)
|
|
50
69
|
echo(" --codex-model=<m> set deployed ~/.codex/config.toml model (e.g. gpt-5.5)")
|
|
70
|
+
echo(` ${effortFlagGrammar("codex").padEnd(39)} set deployed model_reasoning_effort`)
|
|
51
71
|
echo("")
|
|
52
72
|
echo("Sticky opt-ins (installed + enabled until --prune)")
|
|
53
73
|
echo(
|
|
@@ -81,9 +101,42 @@ function selectTarget(ctx: Ctx, target: string): void {
|
|
|
81
101
|
ctx.targetFilterSet = true
|
|
82
102
|
}
|
|
83
103
|
|
|
104
|
+
function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
|
|
105
|
+
switch (flag) {
|
|
106
|
+
case "--claude-model":
|
|
107
|
+
ctx.claudeModel = value
|
|
108
|
+
break
|
|
109
|
+
case "--claude-effort":
|
|
110
|
+
ctx.claudeEffort = value
|
|
111
|
+
break
|
|
112
|
+
case "--claude-advisor":
|
|
113
|
+
ctx.claudeAdvisor = value
|
|
114
|
+
break
|
|
115
|
+
case "--codex-model":
|
|
116
|
+
ctx.codexModel = value
|
|
117
|
+
break
|
|
118
|
+
case "--codex-effort":
|
|
119
|
+
ctx.codexEffort = value
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
|
|
123
|
+
flags.add(flag)
|
|
124
|
+
ctx.modifierFlags = flags
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isModifierFlag(value: string): value is ModifierFlag {
|
|
128
|
+
return MODIFIER_FLAGS.has(value as ModifierFlag)
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
85
132
|
const { err } = ctx.services.logger
|
|
86
|
-
for (
|
|
133
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
134
|
+
const arg = args[index] ?? ""
|
|
135
|
+
if (isModifierFlag(arg) && args[index + 1] === "") {
|
|
136
|
+
setModifier(ctx, arg, "")
|
|
137
|
+
index += 1
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
87
140
|
switch (arg) {
|
|
88
141
|
case "claude":
|
|
89
142
|
case "codex":
|
|
@@ -116,6 +169,18 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
116
169
|
printModels(ctx, "codex")
|
|
117
170
|
err("--codex-model requires a value: --codex-model=<model>")
|
|
118
171
|
throw new ExitError(2)
|
|
172
|
+
case "--claude-effort":
|
|
173
|
+
printCatalog(ctx, effortCatalog("claude"))
|
|
174
|
+
err(`--claude-effort requires a value: ${effortFlagGrammar("claude")}`)
|
|
175
|
+
throw new ExitError(2)
|
|
176
|
+
case "--codex-effort":
|
|
177
|
+
printCatalog(ctx, effortCatalog("codex"))
|
|
178
|
+
err(`--codex-effort requires a value: ${effortFlagGrammar("codex")}`)
|
|
179
|
+
throw new ExitError(2)
|
|
180
|
+
case "--claude-advisor":
|
|
181
|
+
printCatalog(ctx, advisorCatalog())
|
|
182
|
+
err(`--claude-advisor requires a value: ${advisorFlagGrammar()}`)
|
|
183
|
+
throw new ExitError(2)
|
|
119
184
|
case "--claude-compact-window":
|
|
120
185
|
err("--claude-compact-window requires a value: --claude-compact-window=<tokens> (e.g. 680k)")
|
|
121
186
|
throw new ExitError(2)
|
|
@@ -159,9 +224,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
159
224
|
break
|
|
160
225
|
}
|
|
161
226
|
if (arg.startsWith("--claude-model=")) {
|
|
162
|
-
ctx
|
|
227
|
+
setModifier(ctx, "--claude-model", arg.slice("--claude-model=".length))
|
|
163
228
|
} else if (arg.startsWith("--codex-model=")) {
|
|
164
|
-
ctx
|
|
229
|
+
setModifier(ctx, "--codex-model", arg.slice("--codex-model=".length))
|
|
230
|
+
} else if (arg.startsWith("--claude-effort=")) {
|
|
231
|
+
setModifier(ctx, "--claude-effort", arg.slice("--claude-effort=".length))
|
|
232
|
+
} else if (arg.startsWith("--codex-effort=")) {
|
|
233
|
+
setModifier(ctx, "--codex-effort", arg.slice("--codex-effort=".length))
|
|
234
|
+
} else if (arg.startsWith("--claude-advisor=")) {
|
|
235
|
+
setModifier(ctx, "--claude-advisor", arg.slice("--claude-advisor=".length))
|
|
165
236
|
} else if (arg.startsWith("--claude-compact-window=")) {
|
|
166
237
|
const parsed = parseCompactWindow(arg.slice("--claude-compact-window=".length))
|
|
167
238
|
if (parsed === undefined) {
|
|
@@ -184,9 +255,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
184
255
|
}
|
|
185
256
|
}
|
|
186
257
|
|
|
187
|
-
|
|
258
|
+
function printCatalog(ctx: Ctx, catalog: string): void {
|
|
259
|
+
for (const line of catalog.split("\n")) ctx.services.logger.echo(line)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function validateModifierFlags(ctx: Ctx): void {
|
|
188
263
|
const { err, warn } = ctx.services.logger
|
|
189
|
-
|
|
264
|
+
const supplied = (flag: ModifierFlag, value: string): boolean =>
|
|
265
|
+
value !== "" || ctx.modifierFlags?.has(flag) === true
|
|
266
|
+
if (supplied("--claude-model", ctx.claudeModel)) {
|
|
190
267
|
if (!ctx.syncClaude) {
|
|
191
268
|
warn("--claude-model ignored: claude target not selected")
|
|
192
269
|
ctx.claudeModel = ""
|
|
@@ -196,7 +273,27 @@ export function validateModelFlags(ctx: Ctx): void {
|
|
|
196
273
|
throw new ExitError(2)
|
|
197
274
|
}
|
|
198
275
|
}
|
|
199
|
-
if (ctx.
|
|
276
|
+
if (supplied("--claude-effort", ctx.claudeEffort)) {
|
|
277
|
+
if (!ctx.syncClaude) {
|
|
278
|
+
warn("--claude-effort ignored: claude target not selected")
|
|
279
|
+
ctx.claudeEffort = ""
|
|
280
|
+
} else if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
|
|
281
|
+
printCatalog(ctx, effortCatalog("claude"))
|
|
282
|
+
err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`)
|
|
283
|
+
throw new ExitError(2)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (supplied("--claude-advisor", ctx.claudeAdvisor)) {
|
|
287
|
+
if (!ctx.syncClaude) {
|
|
288
|
+
warn("--claude-advisor ignored: claude target not selected")
|
|
289
|
+
ctx.claudeAdvisor = ""
|
|
290
|
+
} else if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
|
|
291
|
+
printCatalog(ctx, advisorCatalog())
|
|
292
|
+
err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`)
|
|
293
|
+
throw new ExitError(2)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (supplied("--codex-model", ctx.codexModel)) {
|
|
200
297
|
if (!ctx.syncCodex) {
|
|
201
298
|
warn("--codex-model ignored: codex target not selected")
|
|
202
299
|
ctx.codexModel = ""
|
|
@@ -206,4 +303,14 @@ export function validateModelFlags(ctx: Ctx): void {
|
|
|
206
303
|
throw new ExitError(2)
|
|
207
304
|
}
|
|
208
305
|
}
|
|
306
|
+
if (supplied("--codex-effort", ctx.codexEffort)) {
|
|
307
|
+
if (!ctx.syncCodex) {
|
|
308
|
+
warn("--codex-effort ignored: codex target not selected")
|
|
309
|
+
ctx.codexEffort = ""
|
|
310
|
+
} else if (!isEffortModifierValue("codex", ctx.codexEffort)) {
|
|
311
|
+
printCatalog(ctx, effortCatalog("codex"))
|
|
312
|
+
err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`)
|
|
313
|
+
throw new ExitError(2)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
209
316
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
import { isObject, parseJson } from "./jq"
|
|
4
|
+
|
|
5
|
+
const SESSION_RELAY_PLUGIN_ID = "session-relay@docks"
|
|
6
|
+
|
|
7
|
+
export type SessionRelayReadinessReason =
|
|
8
|
+
| "codex_cli_unavailable"
|
|
9
|
+
| "plugin_list_failed"
|
|
10
|
+
| "invalid_plugin_list"
|
|
11
|
+
| "plugin_missing"
|
|
12
|
+
| "plugin_ambiguous"
|
|
13
|
+
| "plugin_not_installed"
|
|
14
|
+
| "plugin_disabled"
|
|
15
|
+
|
|
16
|
+
export interface SessionRelayReadiness {
|
|
17
|
+
readonly schema: 1
|
|
18
|
+
readonly state: "ready" | "unavailable"
|
|
19
|
+
readonly reason: SessionRelayReadinessReason | null
|
|
20
|
+
readonly version: string | null
|
|
21
|
+
readonly installed: boolean
|
|
22
|
+
readonly enabled: boolean
|
|
23
|
+
readonly scope: "new_sessions"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CodexPluginListProbe {
|
|
27
|
+
readonly status: number | null
|
|
28
|
+
readonly stdout: string
|
|
29
|
+
readonly errorCode: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unavailable(
|
|
33
|
+
reason: SessionRelayReadinessReason,
|
|
34
|
+
fields: { version?: string | null; installed?: boolean; enabled?: boolean } = {}
|
|
35
|
+
): SessionRelayReadiness {
|
|
36
|
+
return {
|
|
37
|
+
schema: 1,
|
|
38
|
+
state: "unavailable",
|
|
39
|
+
reason,
|
|
40
|
+
version: fields.version ?? null,
|
|
41
|
+
installed: fields.installed ?? false,
|
|
42
|
+
enabled: fields.enabled ?? false,
|
|
43
|
+
scope: "new_sessions"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function classifySessionRelayReadiness(probe: CodexPluginListProbe): SessionRelayReadiness {
|
|
48
|
+
if (probe.errorCode === "ENOENT") return unavailable("codex_cli_unavailable")
|
|
49
|
+
if (probe.errorCode !== null || probe.status !== 0) return unavailable("plugin_list_failed")
|
|
50
|
+
|
|
51
|
+
const value = parseJson(probe.stdout)
|
|
52
|
+
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) {
|
|
53
|
+
return unavailable("invalid_plugin_list")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const matches = value["installed"].filter(
|
|
57
|
+
(entry) => isObject(entry) && entry["pluginId"] === SESSION_RELAY_PLUGIN_ID
|
|
58
|
+
)
|
|
59
|
+
if (matches.length === 0) return unavailable("plugin_missing")
|
|
60
|
+
if (matches.length !== 1) return unavailable("plugin_ambiguous")
|
|
61
|
+
|
|
62
|
+
const row = matches[0]
|
|
63
|
+
if (row === undefined || !isObject(row)) return unavailable("invalid_plugin_list")
|
|
64
|
+
const version = row["version"]
|
|
65
|
+
const installed = row["installed"]
|
|
66
|
+
const enabled = row["enabled"]
|
|
67
|
+
if (typeof version !== "string" || version.length === 0 || typeof installed !== "boolean" || typeof enabled !== "boolean") {
|
|
68
|
+
return unavailable("invalid_plugin_list")
|
|
69
|
+
}
|
|
70
|
+
if (!installed) return unavailable("plugin_not_installed", { version, enabled })
|
|
71
|
+
if (!enabled) return unavailable("plugin_disabled", { version, installed })
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
schema: 1,
|
|
75
|
+
state: "ready",
|
|
76
|
+
reason: null,
|
|
77
|
+
version,
|
|
78
|
+
installed,
|
|
79
|
+
enabled,
|
|
80
|
+
scope: "new_sessions"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function sessionRelayReadiness(): SessionRelayReadiness {
|
|
85
|
+
const result = spawnSync("codex", ["plugin", "list", "--json"], {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
88
|
+
})
|
|
89
|
+
return classifySessionRelayReadiness({
|
|
90
|
+
status: result.status,
|
|
91
|
+
stdout: result.stdout ?? "",
|
|
92
|
+
errorCode: (result.error as NodeJS.ErrnoException | undefined)?.code ?? null
|
|
93
|
+
})
|
|
94
|
+
}
|