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.
@@ -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
- export function syncCodexModel(ctx: Ctx, model: string): void {
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] (--codex-model) set model = "${model}" in ${userCodexSettings}`)
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(`(--codex-model) ${userCodexSettings} missing — skipped`)
72
+ warn(`(${edit.tag}) ${userCodexSettings} missing — skipped`)
66
73
  return
67
74
  }
68
- if (replaceTopLevelSettingInFile(userCodexSettings, "model", `model = "${model}"`)) {
69
- change(`Model: deployed Codex model set to ${model} (SoT unchanged; flag-less sync reverts)`)
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(`Model: deployed Codex model already ${model}`)
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, validateModelFlags } from "./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
- validateModelFlags(ctx)
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 "./claudeModel"
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 (const arg of args) {
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.claudeModel = arg.slice("--claude-model=".length)
227
+ setModifier(ctx, "--claude-model", arg.slice("--claude-model=".length))
163
228
  } else if (arg.startsWith("--codex-model=")) {
164
- ctx.codexModel = arg.slice("--codex-model=".length)
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
- export function validateModelFlags(ctx: Ctx): void {
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
- if (ctx.claudeModel !== "") {
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.codexModel !== "") {
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
+ }