docks-kit 0.16.16 → 0.17.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 +5 -1
- package/cli/docs/flags.md +21 -0
- package/cli/docs/install.md +1 -0
- package/cli/docs/omp-models.md +59 -0
- package/cli/docs/overview.md +1 -1
- package/cli/docs/sync-layers.md +1 -1
- package/cli/docs/toolchain.md +16 -3
- package/cli/src/argv.ts +63 -4
- package/cli/src/commands/omp.ts +189 -0
- package/cli/src/commands/sync.ts +1 -1
- package/cli/src/commands/update.ts +8 -52
- package/cli/src/engine-native/claudePlugins.ts +78 -35
- package/cli/src/engine-native/deps.ts +9 -0
- package/cli/src/engine-native/exec.ts +54 -2
- package/cli/src/engine-native/harnesses.ts +93 -12
- package/cli/src/engine-native/ompOverlay.ts +174 -0
- package/cli/src/engine-native/toolchain.ts +3 -0
- package/cli/src/generated/sotPayload.ts +4 -4
- package/cli/src/main.ts +4 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -31,6 +31,8 @@ launcher can fall back to Bun source.
|
|
|
31
31
|
| `docks-kit` / `docks-kit.ps1` | POSIX and Windows CLI launchers. On supported hosts, each runs the matching binary in `cli/dist/` only when its `--version` matches `package.json`. Otherwise it runs Bun-from-source and auto-installs Bun plus `node_modules`. Hosts outside the support matrix fail before source fallback. The standalone platform release binary provides no-Bun recovery. |
|
|
32
32
|
| `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
|
|
33
33
|
| `cli/src/engine-native/ompSync.ts` | omp file deployment, marketplace registration, and plugin synchronization |
|
|
34
|
+
| `cli/src/commands/omp.ts` | `docks-kit omp` session launcher: renders the free-model run overlay and forwards args to omp |
|
|
35
|
+
| `cli/src/engine-native/ompOverlay.ts` | Free-model overlay render plus catalog parse and thinking-ceiling helpers |
|
|
34
36
|
| `cli/` | Effect 4 RC CLI + bundled docs topics |
|
|
35
37
|
| `SoT/models.json` | Kit-verified Claude and Codex model catalog |
|
|
36
38
|
| `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
|
|
@@ -72,6 +74,8 @@ omp SoT notes:
|
|
|
72
74
|
- Sync registers the `docks` marketplace. It installs or upgrades `docks@docks` and `plan-lifecycle@docks` at user scope.
|
|
73
75
|
- Sync installs `pi-intercom` at the verified version from `SoT/toolchain.json`.
|
|
74
76
|
- The omp CLI is upstream-owned and self-updating through `omp update`. Sync never installs or upgrades the CLI.
|
|
77
|
+
- `docks-kit omp [--model <selector>|--pick] [args...]` starts one interactive omp session on a single free model. It renders a run overlay to `~/.cache/docks-kit/omp-free-<model>-<digest>.yml` (mode 0600) and passes it through omp's repeatable `--config` flag. `ompOverlay.ts overlayFileName` gives each model its own file, because omp can re-read the overlay during a live session and a second launcher on another model must not rewrite it. Deployed `~/.omp/agent/` files stay untouched, so the next plain `omp` run uses the paid configuration again. Remaining arguments forward verbatim to omp.
|
|
78
|
+
- The session model persists per machine in `~/.docks-kit/state.json` under `ompSession`, next to `harnesses`. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both the session level and the advisor level from the ladder of the chosen model, never from a fixed list: free ladders are not uniform, three free models publish no `medium`, and two publish no ladder, which the overlay renders as bare selectors. The picker lists only zero-cost catalog models.
|
|
75
79
|
|
|
76
80
|
For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the matching SoT directory.
|
|
77
81
|
|
|
@@ -83,7 +87,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the m
|
|
|
83
87
|
- **Effect 4 CLI stack.** The CLI pins `effect@4.0.0-rc.115` (including `effect/unstable/cli`), `@effect/platform-bun@4.0.0-rc.115` (`BunServices.layer`, `BunRuntime.runMain`), `@effect/vitest@4.0.0-rc.115`, and `vitest@5.0.0` (inside the `@effect/vitest` peer range `>=5.0.0 <6.0.0`; 5.0.0 is past the 5.x patch point for GHSA-82fw-gwwq-j7x9, fixed in 5.0.0-rc.2). `@effect/cli` and `@effect/platform` are removed and must not be reintroduced.
|
|
84
88
|
- **Effect skill routing.** Effect work in this checkout must verify migration and API call shapes against the installed declarations under `node_modules/effect/dist/unstable/cli/`, never from memory or a mutable dist-tag. The `effect-ts-setup`, `effect-ts-port`, and `effect-ts-specialist` skills target Effect 3.x and do not apply.
|
|
85
89
|
- **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, `agents`, and `omp`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync omp` for omp-only config edits); targets can be combined with `--dry-run`, `--skip-bubblewrap` (skip optional bubblewrap bootstrap for the Codex Linux sandbox), `--skip-plugin-refresh` (install missing plugins without refreshing existing caches), `--reconcile`, `--prune`, and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
|
|
86
|
-
- **Per-machine harness selection.** `~/.docks-kit/state.json` drives a flag-less sync. A missing file selects `claude`, `codex`, and `agents`; it never selects `omp` implicitly. `sync` never prompts and never writes the selection file. `docks-kit harnesses` is the only command that writes the selection.
|
|
90
|
+
- **Per-machine harness selection.** `~/.docks-kit/state.json` drives a flag-less sync. A missing file selects `claude`, `codex`, and `agents`; it never selects `omp` implicitly. `sync` never prompts and never writes the selection file. `docks-kit harnesses` is the only command that writes the `harnesses` selection. The same state file also carries the independent `ompSession` key, which only `docks-kit omp` writes; every writer merges over the stored record, so neither key can drop the other.
|
|
87
91
|
- **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claudeSync.ts REMOVED_MANIFEST, baseline removal inventory`), a curated list of unambiguous kit-owned artifacts that `claudeSync.ts syncRemovals, baseline artifact prune` force-prunes on every sync, including the home-relative `~/.local/bin/session-relay` artifact installed outside `~/.claude`; see `CLAUDE.md` § Pruning stale artifacts.
|
|
88
92
|
- **`--reconcile` / `--prune` are the kit-owned reconcile flags.** Orthogonal — `--reconcile` reconciles the settings layer (SoT-declared keys/tables/arrays win; user-only keys and nested objects are preserved; permissions arrays are replaced wholesale by SoT). `--prune` uninstalls kit-managed installations not in the SoT (plugins, marketplaces, and `~/.agents/skills/*` entries tracked in `~/.agents/.kit-managed-skills`). Combine for a full reset to SoT's kit-managed scope. User-only additions outside the kit's scope (custom env vars, mcpServers, manually-installed skills, third-party plugins not declared in SoT) are always preserved. Each tool's per-tool file documents the specific paths and diff recipes.
|
|
89
93
|
- **SOLID-aligned modules.** `cli/src/engine-native/parseArgs.ts` owns flag parsing and validation. `toolchain.ts` owns verified-version floor reporting over `SoT/toolchain.json`; `bun.ts` owns the shared, memoized Bun bootstrap; `claudeRuntime.ts` owns Claude settings materialization. `claudeSync.ts`, `codexSync.ts`, and `skillsSync.ts` own their tool-specific sync logic. `ompSync.ts` owns omp file and plugin sync. `ompYaml.ts` owns the omp YAML merge. `harnesses.ts` owns per-machine harness selection. `index.ts` is the thin orchestrator. The public CLI seam is `cli/src/engine.ts`.
|
package/cli/docs/flags.md
CHANGED
|
@@ -52,6 +52,27 @@ Bare model, effort, or advisor modifiers print the relevant valid-value catalog
|
|
|
52
52
|
and exit 2. A modifier for a target not selected by the positional arguments is
|
|
53
53
|
ignored with a warning; Claude modifiers never touch Codex config and vice versa.
|
|
54
54
|
|
|
55
|
+
## `omp` session flags
|
|
56
|
+
|
|
57
|
+
| Flag | Effect |
|
|
58
|
+
|------|--------|
|
|
59
|
+
| `--model <selector>` | Session model for this run and later runs; rejected unless the live catalog reports zero input and output cost; recorded in `~/.docks-kit/state.json` under `ompSession` |
|
|
60
|
+
| `--pick` | Interactive picker over free catalog models; records the choice the same way |
|
|
61
|
+
|
|
62
|
+
Remaining arguments forward verbatim to omp after the launcher flags.
|
|
63
|
+
`docks-kit omp -p "..."` runs one prompt. `docks-kit omp --continue` resumes
|
|
64
|
+
the previous session. A bare `docks-kit omp` opens an interactive session.
|
|
65
|
+
The launcher changes no deployed file and never invokes `sync`.
|
|
66
|
+
The boundary opens at the first token that is neither a declared launcher
|
|
67
|
+
flag nor a declared global flag. Declared flags stay with docks-kit and
|
|
68
|
+
never reach omp:
|
|
69
|
+
|
|
70
|
+
- `--model`, `--pick`
|
|
71
|
+
- `--help`, `--version`, `--log-level`, `--wizard`, `--completions`
|
|
72
|
+
|
|
73
|
+
The same names on omp stay reachable behind an explicit delimiter. Use
|
|
74
|
+
`docks-kit omp -- --help` to ask omp for help.
|
|
75
|
+
|
|
55
76
|
## Renamed legacy flags (pre-CLI sync.sh)
|
|
56
77
|
|
|
57
78
|
Old flags exit with a rename hint — there is no compat behavior.
|
package/cli/docs/install.md
CHANGED
|
@@ -153,6 +153,7 @@ sync/config reads.
|
|
|
153
153
|
|
|
154
154
|
- Bun for source/global installs; release binaries embed the runtime
|
|
155
155
|
- Node/npm for npm-global LSP servers
|
|
156
|
+
- rustup is optional and needed only for the Rust language server
|
|
156
157
|
- jq is optional doctor/test tooling; sync has no jq runtime dependency
|
|
157
158
|
- curl is required only when a source launcher must download Bun. The POSIX
|
|
158
159
|
launchers run `install.sh`; Windows runs `install.ps1` through PowerShell.
|
package/cli/docs/omp-models.md
CHANGED
|
@@ -211,6 +211,65 @@ omp's 272k context window is the `/extended-context off` window for Astra.
|
|
|
211
211
|
`/extended-context on` uses the 922k input window, 1.05M total, which matches
|
|
212
212
|
AA's 1M.
|
|
213
213
|
|
|
214
|
+
## Free session launcher (`docks-kit omp`)
|
|
215
|
+
|
|
216
|
+
`docks-kit omp [--model <selector>|--pick] [args...]` starts one interactive
|
|
217
|
+
omp session on a single free model. All 12 model roles resolve to that model.
|
|
218
|
+
All 9 retry fallback chains are empty, so a retry cannot reach a paid model.
|
|
219
|
+
The overlay also sets `defaultThinkingLevel` and `task.maxEffort`, except for
|
|
220
|
+
a model that publishes no thinking ladder, where both keys are omitted and the
|
|
221
|
+
deployed values apply.
|
|
222
|
+
|
|
223
|
+
The launcher renders a run overlay to
|
|
224
|
+
`~/.cache/docks-kit/omp-free-<model>-<digest>.yml` at mode 0600 and passes it
|
|
225
|
+
through omp's repeatable `--config` flag. Each model gets its own file,
|
|
226
|
+
because omp can re-read the overlay during a live session, and a second
|
|
227
|
+
launcher on another model must not rewrite that file. The name sanitizes the
|
|
228
|
+
selector for the file system and appends a digest of the exact selector, so
|
|
229
|
+
two selectors that differ only in separator characters stay apart. The
|
|
230
|
+
launcher never reads or writes `~/.omp/agent/config.yml` or `models.yml`. It
|
|
231
|
+
never touches the SoT. The next plain `omp` run uses the paid configuration
|
|
232
|
+
again.
|
|
233
|
+
|
|
234
|
+
Every argument after the launcher flags forwards verbatim to omp. `docks-kit
|
|
235
|
+
omp -p "..."` runs one prompt. `docks-kit omp --continue` resumes the
|
|
236
|
+
previous session. A bare `docks-kit omp` opens an interactive session.
|
|
237
|
+
|
|
238
|
+
The default model is `opencode-zen/muse-spark-1.3-contributor-free` ("Muse
|
|
239
|
+
Spark 1.3 Free", 1,048,576-token context) at `xhigh`. `xhigh` is the ceiling
|
|
240
|
+
of that model ladder: `minimal, low, medium, high, xhigh`. The paid
|
|
241
|
+
`muse-spark-1.3` sibling also offers `max`. The free variant does not.
|
|
242
|
+
|
|
243
|
+
Both levels come from the ladder of the chosen model, never from a fixed
|
|
244
|
+
list. The session level is the ceiling of that ladder. The advisor level is
|
|
245
|
+
the highest level at or below `medium`, because the advisor performs quick
|
|
246
|
+
review passes. When every level of a ladder is above `medium`, the advisor
|
|
247
|
+
takes the lowest level, so it never becomes the most expensive role.
|
|
248
|
+
|
|
249
|
+
Free ladders are not uniform. On 2026-09-11 the 26 free models published six
|
|
250
|
+
distinct ladders: 21 include `medium`, 3 stop at `low, high, max` or
|
|
251
|
+
`high, max`, and 2 publish no ladder at all. A model without a ladder gets
|
|
252
|
+
bare selectors with no `:level` suffix, because an invented level makes omp
|
|
253
|
+
fail when the session starts.
|
|
254
|
+
|
|
255
|
+
The choice persists per machine in `~/.docks-kit/state.json` under
|
|
256
|
+
`ompSession`, next to `harnesses`. It survives across sessions. It is never
|
|
257
|
+
committed. `--model <selector>` records one selector. `--pick` opens an
|
|
258
|
+
interactive picker.
|
|
259
|
+
|
|
260
|
+
The picker lists only models the live `omp models --json` catalog reports at
|
|
261
|
+
zero input and output cost (26 entries on 2026-09-11). This path cannot start
|
|
262
|
+
a paid session. The catalog can advertise a free model that the account
|
|
263
|
+
cannot call; omp reports that provider error unchanged.
|
|
264
|
+
|
|
265
|
+
An omp login is required. The kit owns no login flow. It surfaces omp's own
|
|
266
|
+
authentication error unchanged.
|
|
267
|
+
|
|
268
|
+
When the catalog renames the free variant, change only the default selector
|
|
269
|
+
constant. A ladder change needs no code change, because both levels come from
|
|
270
|
+
the catalog row of the chosen model. Refresh the recorded default and the
|
|
271
|
+
ladder counts in these docs in the same commit.
|
|
272
|
+
|
|
214
273
|
## Maintenance
|
|
215
274
|
|
|
216
275
|
- Refresh the snapshot from the AA release page of each family
|
package/cli/docs/overview.md
CHANGED
|
@@ -16,7 +16,7 @@ AI-assisted dev environment on every machine.
|
|
|
16
16
|
| `SoT/models.json` | Kit-verified model catalog (see `docks-kit docs models`) |
|
|
17
17
|
| `SoT/toolchain.json` | Verified-version floors for external tools (see `docks-kit docs toolchain`) |
|
|
18
18
|
| `cli/src/generated/sotPayload.ts` | Deterministic generated payload embedded in standalone/npm execution |
|
|
19
|
-
| `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain |
|
|
19
|
+
| `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain and the `docks-kit omp` session overlay |
|
|
20
20
|
| `cli/` | This CLI (Effect 4 RC on Bun) plus bundled docs |
|
|
21
21
|
| `docks-kit` / `docks-kit.ps1` | POSIX and Windows launchers: version-matching compiled binary → Bun-from-source, with Bun auto-install |
|
|
22
22
|
|
package/cli/docs/sync-layers.md
CHANGED
|
@@ -30,7 +30,7 @@ Order matters — runtime readiness and settings form one transaction:
|
|
|
30
30
|
7. **Plugins** — seven idempotent passes via the `claude plugin` CLI
|
|
31
31
|
(marketplaces → install → update → [--prune: uninstall/remove] → re-assert
|
|
32
32
|
SoT enabled-state). Optional opt-ins via `--claude-plugin=<name>`.
|
|
33
|
-
8. LSP server binaries (npm globals).
|
|
33
|
+
8. LSP server binaries (npm globals plus the rust-analyzer rustup component).
|
|
34
34
|
|
|
35
35
|
The statusline reads Claude's native `rate_limits`. There is no OAuth request,
|
|
36
36
|
usage cache, jq/curl runtime dependency, or Stop fetch hook.
|
package/cli/docs/toolchain.md
CHANGED
|
@@ -39,12 +39,25 @@ floor: it skips the `typescript-language-server` install on such a host, warns
|
|
|
39
39
|
with the host Node version, and installs the other language servers. Nothing
|
|
40
40
|
else consults the floor, so a host below it still runs every kit operation.
|
|
41
41
|
|
|
42
|
+
## rust-analyzer: no floor, no pin
|
|
43
|
+
|
|
44
|
+
`rust-analyzer` is a `check` row with no floor and no `verified` pin. The kit
|
|
45
|
+
installs the component with `rustup component add rust-analyzer` for the
|
|
46
|
+
`rust-analyzer-lsp` plugin, so the version follows the host Rust toolchain. A
|
|
47
|
+
pin would claim control the kit does not have, the stance `bubblewrap` already
|
|
48
|
+
takes for a tool the kit does not publish. `docks-kit toolchain check` reports
|
|
49
|
+
the installed version and judges nothing.
|
|
50
|
+
|
|
42
51
|
## Language-server upgrades
|
|
43
52
|
|
|
44
53
|
`claudeSync syncLspServers` installs `intelephense`, `typescript-language-server`,
|
|
45
|
-
and `
|
|
46
|
-
that is already present. A `verified` bump therefore reaches
|
|
47
|
-
immediately and leaves an existing install alone.
|
|
54
|
+
`typescript`, and `rust-analyzer` only when the binary is missing. It never
|
|
55
|
+
upgrades a server that is already present. A `verified` bump therefore reaches
|
|
56
|
+
a fresh host immediately and leaves an existing install alone.
|
|
57
|
+
|
|
58
|
+
The rustup channel follows the same rule. Sync adds the component only when the
|
|
59
|
+
`rust-analyzer` binary is missing, and `rustup update` is the user's remedy for
|
|
60
|
+
an old component.
|
|
48
61
|
|
|
49
62
|
A lagging server shows as `below-floor` in `docks-kit toolchain check`. To move
|
|
50
63
|
it, upgrade Node to 22.22.2 or newer, then install the pinned version by hand:
|
package/cli/src/argv.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { statusCommand } from "./commands/status"
|
|
|
8
8
|
import { syncCommand } from "./commands/sync"
|
|
9
9
|
import { toolchainCommand } from "./commands/toolchain"
|
|
10
10
|
import { updateCommand } from "./commands/update"
|
|
11
|
+
import { ompCommand } from "./commands/omp"
|
|
11
12
|
import {
|
|
12
13
|
advisorCatalog,
|
|
13
14
|
advisorFlagGrammar,
|
|
@@ -161,7 +162,8 @@ const COMMANDS: ReadonlyArray<CommandValue> = [
|
|
|
161
162
|
statusCommand,
|
|
162
163
|
pluginsCommand,
|
|
163
164
|
skillsCommand,
|
|
164
|
-
docsCommand
|
|
165
|
+
docsCommand,
|
|
166
|
+
ompCommand
|
|
165
167
|
]
|
|
166
168
|
|
|
167
169
|
// A Map, not a plain object: an object literal answers `toString` and friends from
|
|
@@ -373,11 +375,68 @@ const missingModifierValue = (flag: string): string | undefined => {
|
|
|
373
375
|
}
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
// Without the injected `--`, Effect 4 rejects a forwarded flag such as `-p` or
|
|
379
|
+
// `--mode` as unrecognized. The omp launcher therefore owns a passthrough
|
|
380
|
+
// boundary: the first token after the `omp` word that is neither a flag
|
|
381
|
+
// declared on the omp or global surface nor a value consumed by such a flag
|
|
382
|
+
// starts the verbatim tail forwarded to omp. An undeclared long flag joins the
|
|
383
|
+
// tail too, because most omp flags are long (`--mode`, `--continue`,
|
|
384
|
+
// `--models`), and omp itself reports an unrecognized one accurately. Use
|
|
385
|
+
// `docks-kit omp -- --model x` to reach omp's own same-named flag.
|
|
386
|
+
const spliceOmpBoundary = (args: ReadonlyArray<string>): ReadonlyArray<string> => {
|
|
387
|
+
const surface = COMMAND_SURFACES.get("omp")
|
|
388
|
+
let ompIndex = -1
|
|
389
|
+
for (let index = 0; index < args.length; index++) {
|
|
390
|
+
const token = args[index] as string
|
|
391
|
+
if (token === "--") return args
|
|
392
|
+
const name = flagNameOf(token)
|
|
393
|
+
if (token === name && takesValueInAnySurface(name)) {
|
|
394
|
+
const next = args[index + 1]
|
|
395
|
+
if (next === "--") return args
|
|
396
|
+
if (next !== undefined && !(next.startsWith("-") && declaredInAnySurface(flagNameOf(next)))) {
|
|
397
|
+
index++
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!token.startsWith("-")) {
|
|
402
|
+
ompIndex = index
|
|
403
|
+
break
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (ompIndex === -1 || args[ompIndex] !== "omp") return args
|
|
407
|
+
for (let index = ompIndex + 1; index < args.length; index++) {
|
|
408
|
+
const token = args[index] as string
|
|
409
|
+
// An explicit delimiter already marks the tail; a second one would be
|
|
410
|
+
// forwarded to omp as a literal argument.
|
|
411
|
+
if (token === "--") return args
|
|
412
|
+
if (!token.startsWith("-")) {
|
|
413
|
+
return [...args.slice(0, index), "--", ...args.slice(index)]
|
|
414
|
+
}
|
|
415
|
+
const name = flagNameOf(token)
|
|
416
|
+
const canonical = declaredFlagName(name, surface)
|
|
417
|
+
if (canonical === undefined) return [...args.slice(0, index), "--", ...args.slice(index)]
|
|
418
|
+
const takesValue =
|
|
419
|
+
surface?.valueFlags.includes(canonical) === true ||
|
|
420
|
+
GLOBAL_SURFACE.valueFlags.includes(canonical)
|
|
421
|
+
if (takesValue && !token.includes("=")) {
|
|
422
|
+
const next = args[index + 1]
|
|
423
|
+
if (
|
|
424
|
+
next !== undefined &&
|
|
425
|
+
!(next.startsWith("-") && declaredFlagName(flagNameOf(next), surface) !== undefined)
|
|
426
|
+
) {
|
|
427
|
+
index++
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return args
|
|
432
|
+
}
|
|
433
|
+
|
|
376
434
|
/** Validate the argument list, then hand back the arguments Effect 4 should parse. */
|
|
377
435
|
export const prepareArgv = (args: ReadonlyArray<string>): ArgvOutcome => {
|
|
378
|
-
const
|
|
436
|
+
const boundaryArgs = subcommandName(args) === "omp" ? spliceOmpBoundary(args) : args
|
|
437
|
+
const subcommand = subcommandName(boundaryArgs)
|
|
379
438
|
const commandSurface = subcommand === undefined ? undefined : COMMAND_SURFACES.get(subcommand)
|
|
380
|
-
const { flags, normalizations } = scanArgv(
|
|
439
|
+
const { flags, normalizations } = scanArgv(boundaryArgs, commandSurface)
|
|
381
440
|
const unknownCommandWouldMisdiagnoseFlag =
|
|
382
441
|
subcommand !== undefined &&
|
|
383
442
|
commandSurface === undefined &&
|
|
@@ -430,5 +489,5 @@ export const prepareArgv = (args: ReadonlyArray<string>): ArgvOutcome => {
|
|
|
430
489
|
}
|
|
431
490
|
}
|
|
432
491
|
|
|
433
|
-
return { kind: "accept", args: normalizeArgv(
|
|
492
|
+
return { kind: "accept", args: normalizeArgv(boundaryArgs, normalizations) }
|
|
434
493
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli"
|
|
2
|
+
import { Console, Effect, Option } from "effect"
|
|
3
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { bail } from "../engine"
|
|
5
|
+
import { capture, p, spawnHost, which } from "../engine-native/exec"
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_OMP_SESSION_MODEL,
|
|
8
|
+
engineHome,
|
|
9
|
+
readOmpSessionModel,
|
|
10
|
+
writeOmpSessionModel,
|
|
11
|
+
type OmpSessionModel
|
|
12
|
+
} from "../engine-native/harnesses"
|
|
13
|
+
import {
|
|
14
|
+
advisorLevelFor,
|
|
15
|
+
buildOmpArgs,
|
|
16
|
+
ladderCeiling,
|
|
17
|
+
overlayFileName,
|
|
18
|
+
parseFreeModels,
|
|
19
|
+
renderFreeOverlay,
|
|
20
|
+
type CatalogModel
|
|
21
|
+
} from "../engine-native/ompOverlay"
|
|
22
|
+
|
|
23
|
+
const model = Flag.String("model").pipe(
|
|
24
|
+
Flag.withDescription("Free model selector to use and remember for omp sessions"),
|
|
25
|
+
Flag.optional
|
|
26
|
+
)
|
|
27
|
+
const pick = Flag.Boolean("pick").pipe(
|
|
28
|
+
Flag.withDescription("Choose the free model interactively"),
|
|
29
|
+
Flag.withDefault(false)
|
|
30
|
+
)
|
|
31
|
+
const args: Argument.Argument<ReadonlyArray<string>> = Argument.String("args").pipe(
|
|
32
|
+
Argument.withDescription("Arguments forwarded verbatim to omp"),
|
|
33
|
+
Argument.variadic()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export type FreeSelectorResolution =
|
|
37
|
+
| { readonly ok: true; readonly model: CatalogModel }
|
|
38
|
+
| { readonly ok: false; readonly message: string }
|
|
39
|
+
|
|
40
|
+
// Pure selector matching so the picker rule stays testable without spawning omp.
|
|
41
|
+
// An exact selector always wins; otherwise one case-insensitive substring hit
|
|
42
|
+
// across the free selectors resolves, because a longer exact selector is easy
|
|
43
|
+
// to mistype and the catalog is small enough to disambiguate safely.
|
|
44
|
+
export function resolveFreeSelector(
|
|
45
|
+
input: string,
|
|
46
|
+
free: ReadonlyArray<CatalogModel>
|
|
47
|
+
): FreeSelectorResolution {
|
|
48
|
+
const trimmed = input.trim()
|
|
49
|
+
if (trimmed === "") {
|
|
50
|
+
return { ok: false, message: "Model selector must not be empty or blank" }
|
|
51
|
+
}
|
|
52
|
+
const exact = free.find((candidate) => candidate.selector === trimmed)
|
|
53
|
+
if (exact !== undefined) return { ok: true, model: exact }
|
|
54
|
+
const lowered = trimmed.toLowerCase()
|
|
55
|
+
const matches = free.filter((candidate) => candidate.selector.toLowerCase().includes(lowered))
|
|
56
|
+
const list = free.map((candidate) => candidate.selector).join(", ")
|
|
57
|
+
if (matches.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: `Model '${trimmed}' is not in the free catalog. Free models: ${list}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const only = matches[0]
|
|
64
|
+
if (matches.length === 1 && only !== undefined) return { ok: true, model: only }
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
message: `Model '${trimmed}' is ambiguous; matches: ${matches.map((candidate) => candidate.selector).join(", ")}. Pass the exact selector.`
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const loadFreeCatalog = (omp: string): Effect.Effect<ReadonlyArray<CatalogModel>> =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const raw = yield* Effect.promise(() => capture(omp, ["models", "--json"]))
|
|
74
|
+
if (raw === "") {
|
|
75
|
+
return yield* bail(
|
|
76
|
+
"'omp models --json' returned no output; verify omp runs on this host, then retry"
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
let parsed: unknown
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(raw) as unknown
|
|
82
|
+
} catch {
|
|
83
|
+
return yield* bail(
|
|
84
|
+
"'omp models --json' returned output that is not JSON; verify the omp version, then retry"
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
const free = parseFreeModels(parsed)
|
|
88
|
+
if (free.length === 0) {
|
|
89
|
+
return yield* bail("'omp models --json' listed no free models; verify the omp login, then retry")
|
|
90
|
+
}
|
|
91
|
+
return free
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const ompCommand = Command.make("omp", { model, pick, args }, (config) =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
if (Option.isSome(config.model) && config.pick) {
|
|
97
|
+
return yield* bail("Pass either --model <selector> or --pick, not both")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const omp = yield* Effect.sync(() => which("omp"))
|
|
101
|
+
if (omp === "") {
|
|
102
|
+
return yield* bail("omp not found on PATH; install omp, then retry", 1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const home = engineHome(process.env)
|
|
106
|
+
if (Option.isSome(config.model)) {
|
|
107
|
+
const free = yield* loadFreeCatalog(omp)
|
|
108
|
+
const resolved = resolveFreeSelector(config.model.value, free)
|
|
109
|
+
if (!resolved.ok) return yield* bail(resolved.message)
|
|
110
|
+
// Compute both levels once from the catalog row so a plain launch
|
|
111
|
+
// never needs the catalog again.
|
|
112
|
+
const thinking = ladderCeiling(resolved.model.thinking)
|
|
113
|
+
const advisorThinking = advisorLevelFor(resolved.model.thinking)
|
|
114
|
+
yield* Effect.sync(() =>
|
|
115
|
+
writeOmpSessionModel(home, { selector: resolved.model.selector, thinking, advisorThinking })
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (config.pick) {
|
|
120
|
+
// Check the terminal before the catalog spawn, so a non-interactive run
|
|
121
|
+
// fails with the actionable message instead of an omp subprocess first.
|
|
122
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
123
|
+
return yield* bail("Picking needs a terminal; pass --model <selector> instead")
|
|
124
|
+
}
|
|
125
|
+
const free = yield* loadFreeCatalog(omp)
|
|
126
|
+
const chosen = yield* Prompt.Select({
|
|
127
|
+
message: "Choose the free model for this omp session",
|
|
128
|
+
choices: free.map((candidate) => {
|
|
129
|
+
const ceiling = ladderCeiling(candidate.thinking)
|
|
130
|
+
return {
|
|
131
|
+
title: `${candidate.name} — ${candidate.selector}`,
|
|
132
|
+
value: candidate.selector,
|
|
133
|
+
description:
|
|
134
|
+
ceiling === undefined
|
|
135
|
+
? `model default thinking; ${candidate.contextWindow} context`
|
|
136
|
+
: `thinking to ${ceiling}; ${candidate.contextWindow} context`
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
const match = free.find((candidate) => candidate.selector === chosen)
|
|
141
|
+
if (match === undefined) return yield* bail(`Model '${chosen}' is not in the free catalog`)
|
|
142
|
+
const thinking = ladderCeiling(match.thinking)
|
|
143
|
+
const advisorThinking = advisorLevelFor(match.thinking)
|
|
144
|
+
yield* Effect.sync(() => writeOmpSessionModel(home, { selector: match.selector, thinking, advisorThinking }))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const session: OmpSessionModel = (yield* Effect.sync(() => readOmpSessionModel(home))) ??
|
|
148
|
+
DEFAULT_OMP_SESSION_MODEL
|
|
149
|
+
|
|
150
|
+
// A stable cache path, not a temp file deleted at exit, because omp may
|
|
151
|
+
// re-read the overlay during a live session. The name carries the model,
|
|
152
|
+
// so a second launcher on another model cannot rewrite the file a running
|
|
153
|
+
// session still reads.
|
|
154
|
+
const directory = p(home, ".cache", "docks-kit")
|
|
155
|
+
const overlayPath = p(directory, overlayFileName(session.selector))
|
|
156
|
+
yield* Effect.sync(() => {
|
|
157
|
+
// `mode` applies only when mkdir creates the path, so an existing
|
|
158
|
+
// permissive cache directory keeps its mode until the chmod below.
|
|
159
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
160
|
+
chmodSync(directory, 0o700)
|
|
161
|
+
writeFileSync(overlayPath, renderFreeOverlay(session), { mode: 0o600 })
|
|
162
|
+
chmodSync(overlayPath, 0o600)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// A level-free model states the model default so the line never shows
|
|
166
|
+
// an undefined level.
|
|
167
|
+
const hasLevel = typeof session.thinking === "string" && session.thinking.trim() !== ""
|
|
168
|
+
yield* Console.error(
|
|
169
|
+
hasLevel
|
|
170
|
+
? `Starting omp with ${session.selector} at ${session.thinking} thinking (session only; deployed config unchanged)`
|
|
171
|
+
: `Starting omp with ${session.selector} at the model default thinking level (session only; deployed config unchanged)`
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const child = yield* Effect.sync(() =>
|
|
175
|
+
spawnHost("omp", buildOmpArgs(overlayPath, config.args), { stdio: "inherit" })
|
|
176
|
+
)
|
|
177
|
+
if (child.error !== undefined) {
|
|
178
|
+
return yield* bail(
|
|
179
|
+
`Failed to start omp: ${child.error instanceof Error ? child.error.message : String(child.error)}`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
// A signalled child reports a null status, which must not look successful.
|
|
183
|
+
yield* Effect.sync(() => process.exit(child.status ?? 1))
|
|
184
|
+
})
|
|
185
|
+
).pipe(
|
|
186
|
+
Command.withDescription(
|
|
187
|
+
"Start omp with every model role overridden to the remembered free model for this run only (no deployed configuration is changed; --model or --pick remembers a new free default)."
|
|
188
|
+
)
|
|
189
|
+
)
|
package/cli/src/commands/sync.ts
CHANGED
|
@@ -31,7 +31,7 @@ const updateNudge = (logger: Logger): void => {
|
|
|
31
31
|
|
|
32
32
|
const VALID_TARGETS = ["claude", "codex", "agents", "omp"]
|
|
33
33
|
|
|
34
|
-
const targets = Argument.String("target").pipe(
|
|
34
|
+
const targets: Argument.Argument<ReadonlyArray<string>> = Argument.String("target").pipe(
|
|
35
35
|
Argument.withDescription("Sync targets: claude, codex, agents, omp (default: selected harnesses)"),
|
|
36
36
|
Argument.variadic()
|
|
37
37
|
)
|
|
@@ -1,63 +1,19 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Console, Effect } from "effect"
|
|
3
|
-
import { spawnSync, type SpawnSyncOptions, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process"
|
|
4
3
|
import { existsSync, readFileSync } from "node:fs"
|
|
5
4
|
import { bail, compiled } from "../engine"
|
|
6
5
|
import { kitHome } from "../kitHome"
|
|
7
|
-
import { p, which } from "../engine-native/exec"
|
|
8
|
-
import { hostOs, type HostOs
|
|
6
|
+
import { p, spawnHost, which } from "../engine-native/exec"
|
|
7
|
+
import { hostOs, type HostOs } from "../engine-native/os"
|
|
9
8
|
|
|
10
9
|
const noSync = Flag.Boolean("no-sync").pipe(
|
|
11
10
|
Flag.withDescription("Update the kit only; skip the chained flag-less sync"),
|
|
12
11
|
Flag.withDefault(false)
|
|
13
12
|
)
|
|
14
13
|
|
|
15
|
-
/** A tool this host cannot resolve, shaped like the failed spawn it replaces. */
|
|
16
|
-
const notFound = (command: string): SpawnSyncReturns<string> => ({
|
|
17
|
-
pid: 0,
|
|
18
|
-
output: [],
|
|
19
|
-
stdout: "",
|
|
20
|
-
stderr: "",
|
|
21
|
-
status: null,
|
|
22
|
-
signal: null,
|
|
23
|
-
error: new Error(`command not found on PATH: ${command}`)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Every child in this command starts here, because two host facts must never be
|
|
28
|
-
* separated from the argv they describe: a Windows shim invocation is only
|
|
29
|
-
* correct with the verbatim-arguments flag, and a pathless name would let
|
|
30
|
-
* CreateProcess search the parent's current directory before the system one.
|
|
31
|
-
*/
|
|
32
|
-
export const spawnUpdate = (
|
|
33
|
-
command: string,
|
|
34
|
-
args: ReadonlyArray<string>,
|
|
35
|
-
overrides: SpawnSyncOptions = {},
|
|
36
|
-
host: HostOs = hostOs()
|
|
37
|
-
): SpawnSyncReturns<string> => {
|
|
38
|
-
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "")
|
|
39
|
-
const executablePath = resolvesSuffixes ? which(command, host.executableSuffixes) : command
|
|
40
|
-
if (executablePath === "") return notFound(command)
|
|
41
|
-
let invocation: Invocation
|
|
42
|
-
try {
|
|
43
|
-
invocation = host.invoke(executablePath, args)
|
|
44
|
-
} catch (cause) {
|
|
45
|
-
// A value this host cannot put on a command line at all. Print the encoder's
|
|
46
|
-
// reason and exit, matching how this command reports a failed child.
|
|
47
|
-
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`)
|
|
48
|
-
return process.exit(2)
|
|
49
|
-
}
|
|
50
|
-
const options: SpawnSyncOptionsWithStringEncoding = {
|
|
51
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
-
...overrides,
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
55
|
-
}
|
|
56
|
-
return spawnSync(invocation.command, [...invocation.args], options)
|
|
57
|
-
}
|
|
58
14
|
|
|
59
15
|
const git = (home: string, args: Array<string>): { ok: boolean; out: string } => {
|
|
60
|
-
const res =
|
|
16
|
+
const res = spawnHost("git", ["-C", home, ...args])
|
|
61
17
|
return { ok: res.error === undefined && res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() }
|
|
62
18
|
}
|
|
63
19
|
|
|
@@ -65,7 +21,7 @@ const git = (home: string, args: Array<string>): { ok: boolean; out: string } =>
|
|
|
65
21
|
* version loaded, so the chained sync must be a new process. */
|
|
66
22
|
const chainSync = (argv0: string, args: Array<string>): Effect.Effect<void> =>
|
|
67
23
|
Effect.sync(() => {
|
|
68
|
-
const res =
|
|
24
|
+
const res = spawnHost(argv0, args, { stdio: "inherit" })
|
|
69
25
|
if (res.error !== undefined || res.status !== 0) process.exit(res.status ?? 1)
|
|
70
26
|
})
|
|
71
27
|
|
|
@@ -95,7 +51,7 @@ type CapturePackageRoot = (
|
|
|
95
51
|
) => PackageRootCapture
|
|
96
52
|
|
|
97
53
|
const capturePackageRoot: CapturePackageRoot = (command, args) => {
|
|
98
|
-
const res =
|
|
54
|
+
const res = spawnHost(command, args)
|
|
99
55
|
return {
|
|
100
56
|
status: res.status,
|
|
101
57
|
stdout: res.stdout ?? "",
|
|
@@ -191,7 +147,7 @@ export const packageUpdateResult = (
|
|
|
191
147
|
|
|
192
148
|
const updateCheckout = (home: string, skipSync: boolean) =>
|
|
193
149
|
Effect.gen(function* () {
|
|
194
|
-
if (
|
|
150
|
+
if (spawnHost("git", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
195
151
|
return yield* bail("git not found - cannot update the kit checkout")
|
|
196
152
|
}
|
|
197
153
|
const dirty = git(home, ["status", "--porcelain"])
|
|
@@ -217,7 +173,7 @@ const updateCheckout = (home: string, skipSync: boolean) =>
|
|
|
217
173
|
|
|
218
174
|
const touched = git(home, ["diff", "--name-only", before, after]).out.split("\n")
|
|
219
175
|
if (touched.includes("bun.lock") || touched.includes("package.json")) {
|
|
220
|
-
const res =
|
|
176
|
+
const res = spawnHost("bun", ["install", "--frozen-lockfile"], { cwd: home, stdio: "inherit" })
|
|
221
177
|
if (res.error !== undefined || res.status !== 0) {
|
|
222
178
|
return yield* bail("dependencies changed but 'bun install --frozen-lockfile' failed - fix that, then run docks-kit sync", 1)
|
|
223
179
|
}
|
|
@@ -245,7 +201,7 @@ const updatePackage = (home: string, skipSync: boolean) =>
|
|
|
245
201
|
const updateArgs = manager === "bun"
|
|
246
202
|
? ["add", "-g", "docks-kit@latest"]
|
|
247
203
|
: ["install", "-g", "docks-kit@latest"]
|
|
248
|
-
const res =
|
|
204
|
+
const res = spawnHost(manager, updateArgs, { stdio: "inherit" })
|
|
249
205
|
if (res.error !== undefined || res.status !== 0) {
|
|
250
206
|
return yield* bail(
|
|
251
207
|
`global package update failed (${manager === "bun" ? "bun add -g" : "npm install -g"} docks-kit@latest)`,
|