docks-kit 0.18.1 → 0.19.1

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 CHANGED
@@ -35,12 +35,14 @@ launcher can fall back to Bun source.
35
35
  | `cli/src/commands/omp.ts` | `docks-kit omp` session launcher: renders the free-model run overlay and forwards args to omp |
36
36
  | `cli/src/engine-native/ompOverlay.ts` | Free-model overlay render plus catalog parse and thinking-ceiling helpers |
37
37
  | `cli/src/engine-native/<axis>.ts` | One change axis per file: `codexConfig`/`codexHooks`/`codexPlugins`/`codexStatus`, `claudeSettings`/`claudeRemovals`/`claudePluginPasses`/`claudeOptionalPlugins`/`claudeLsp`, `parseModifiers`/`parseHelp`, `ompFileDeploy`/`ompMarketplace`/`ompPlugins`, `skillsManifest`/`skillsLinks`/`skillsInstall`/`skillsPrune`, `engineCtx`/`syncConcurrency`/`syncDispatch`; orchestrators keep prior exports |
38
- | `cli/src/engine-native/sharedTypes.d.ts` | Single definition per shared shape (manifest records, settings edits, omp session model, scalar modifier flags) |
38
+ | `cli/src/engine-native/sharedTypes.d.ts` | Single definition per shared shape (manifest records, settings edits, omp session model, scalar modifier flags, resolved model catalog) |
39
+ | `cli/src/engine-native/kitDb.ts` | Only opener of the SQLite store `~/.docks-kit/kit.db`: append-only migrations, one-time `state.json` import, TTL cache |
40
+ | `cli/src/engine-native/liveModels.ts` | Live model list per enabled harness (Anthropic API via Claude Code login, Codex model cache, `omp models --json`) merged over `SoT/models.json` |
39
41
  | `.oxlintrc.json` / `.oxfmtrc.json` | Mechanical lint (correctness, suspicious, eqeqeq, no-var, prefer-const, no-unused) and code format scope |
40
42
  | `cli/test/fixtures/` | Golden fixtures: `home-fresh`, `home-drift`, `home-invalid-json`, `codex-toml`, `statusline` |
41
43
  | `cli/` | Effect 4 RC CLI + bundled docs topics |
42
- | `SoT/models.json` | Kit-verified Claude and Codex model catalog |
43
- | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
44
+ | `SoT/models.json` | Curated Claude and Codex model overlay: aliases and notes always apply; id rows are the fallback when a harness live list is unavailable |
45
+ | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative; optional `upstream` source read by `docks-kit toolchain outdated`) |
44
46
  | `SoT/.claude/bin/` | Dependency-free Bun runtime programs for Claude's statusline, SessionStart, and Notification |
45
47
  | `SoT/.omp/` | Kit-owned omp SoT: `AGENTS.md`, `config.yml`, `models.yml`, `mcp.json`, and `intercom.json` |
46
48
  | `install.sh` / `install.ps1` | POSIX and Windows global installers |
@@ -84,7 +86,7 @@ omp SoT notes:
84
86
  - Sync installs `pi-intercom` at the verified version from `SoT/toolchain.json`.
85
87
  - The omp CLI is upstream-owned and self-updating through `omp update`. Sync never installs or upgrades the CLI.
86
88
  - `docks-kit omp [--model <selector>|--pick] [args...]` starts one interactive omp session with every model role set to one free model and every retry chain empty. Higher-precedence model selection can replace those overlay values. It renders the 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.
87
- - 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 levels from the ladder of the chosen model for the `--model` path, and `ompOverlay.ts planEffortChoice` drives the `--pick` wizard, which asks whether every role shares one level and otherwise takes one level for the main roles and one for the advisor from that model's own ladder. Levels never come 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.
89
+ - The session model persists per machine in `~/.docks-kit/kit.db` (table `omp_session`), next to the harness selection. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both levels from the ladder of the chosen model for the `--model` path, and `ompOverlay.ts planEffortChoice` drives the `--pick` wizard, which asks whether every role shares one level and otherwise takes one level for the main roles and one for the advisor from that model's own ladder. Levels never come 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.
88
90
 
89
91
  For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the matching SoT directory.
90
92
 
@@ -96,7 +98,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the m
96
98
  - **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.
97
99
  - **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.
98
100
  - **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).
99
- - **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.
101
+ - **Per-machine harness selection.** The SQLite store `~/.docks-kit/kit.db` drives a flag-less sync. A missing selection selects `claude`, `codex`, and `agents`; it never selects `omp` implicitly. `sync` never prompts and never writes the selection. `docks-kit harnesses` is the only command that writes the `harness_selection` table. The same store holds the independent `omp_session` table, which only `docks-kit omp` writes, and a TTL cache (`cache_entry`) for the live Anthropic model list and `toolchain outdated` lookups. `kitDb.ts withKitDb` is the only opener: it runs the append-only `MIGRATIONS`, imports a legacy `state.json` once and renames it to `state.json.migrated`, and refuses a store from a newer schema. Cache rows carry the kit version as fingerprint, so a new release refetches them.
100
102
  - **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.
101
103
  - **`--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.
102
104
  - **SOLID-aligned modules.** Axis modules own one concern each (see layout); orchestrators (`claudeSync.ts`, `codexSync.ts`, `skillsSync.ts`, `ompSync.ts`, `index.ts`, `argv.ts`, `parseArgs.ts`) keep prior exports. New shared shapes go in `sharedTypes.d.ts`, never a second local copy. The public CLI seam is `cli/src/engine.ts`.
package/README.md CHANGED
@@ -47,13 +47,13 @@ docks-kit sync [claude] [codex] [agents] [omp] deploy explicit targets or the m
47
47
  docks-kit harnesses view or change this machine's selection
48
48
  docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
49
49
  docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
50
- docks-kit models [claude|codex] model catalogs (`--json`)
50
+ docks-kit models [claude|codex|omp] [--refresh] live catalogs for enabled harnesses (`--json`)
51
51
  docks-kit omp [--model <m>|--pick] [args...] one omp session on a free model, nothing deployed changes
52
- docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
52
+ docks-kit toolchain [check|ensure <tool>|outdated] verified-version floors for external tools
53
53
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
54
54
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
55
55
  docks-kit skills list [--json] universal skills vs manifest
56
- docks-kit docs [topic] self-documentation (10 topics)
56
+ docks-kit docs [topic] self-documentation (11 topics)
57
57
  --help --version --wizard --completions built-in
58
58
  ```
59
59
 
@@ -91,16 +91,17 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
91
91
  - **Additive by default** — user-only settings keys, plugins, and skills
92
92
  survive a plain sync. Reconciliation toward the SoT is explicit
93
93
  (`--reconcile` / `--prune`).
94
- - **Per-machine selection** — `~/.docks-kit/state.json` drives a flag-less sync.
95
- A missing file selects Claude Code, Codex, and universal skills. It does not
94
+ - **Per-machine selection** — `~/.docks-kit/kit.db` drives a flag-less sync.
95
+ A missing selection selects Claude Code, Codex, and universal skills. It does not
96
96
  select omp. Use `docks-kit harnesses` to view or change the selection.
97
97
  - **Idempotent** — every step is safe to re-run; no-change syncs are no-ops.
98
98
  - **Toolchain floors** — `SoT/toolchain.json` records the kit-verified version
99
99
  floors for external tools (bun, bwrap, …). `docks-kit toolchain check` prints
100
100
  the full doctor table. Bun is the one managed install and is pinned to its
101
101
  verified version.
102
- - **Model catalog** — `SoT/models.json` is the research-verified source for
103
- model validation, listings, and pickers.
102
+ - **Model catalog** — enabled harnesses provide live Claude, Codex, or omp
103
+ model IDs; `SoT/models.json` supplies Claude and Codex aliases, notes, and the
104
+ offline fallback. omp has no curated fallback list.
104
105
  - **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
105
106
  programs for statusline, SessionStart, and Notification. Quota display uses
106
107
  Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
@@ -115,7 +116,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
115
116
  | `SoT/.codex/` | Codex SoT (config.toml, rules, AGENTS.md, marketplace) |
116
117
  | `SoT/.omp/` | omp SoT (AGENTS.md, config.yml, models.yml, mcp.json, intercom.json) |
117
118
  | `SoT/.agents/` | Universal-skill manifest |
118
- | `SoT/models.json` | Kit-verified Claude and Codex model catalog |
119
+ | `SoT/models.json` | Curated aliases, notes, and offline Claude/Codex fallback |
119
120
  | `SoT/toolchain.json` | Verified-version floors |
120
121
  | `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
121
122
  | `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
package/cli/docs/flags.md CHANGED
@@ -14,7 +14,7 @@ docks-kit sync omp # opt-in harness
14
14
 
15
15
  Positional targets are `claude`, `codex`, `agents`, and `omp`. A flag-less
16
16
  `sync` deploys the per-machine harness selection stored in
17
- `~/.docks-kit/state.json`. A missing or invalid state file selects `claude`,
17
+ `~/.docks-kit/kit.db`. A missing selection or an unreadable store selects `claude`,
18
18
  `codex`, and `agents`, and never selects `omp`. Choose the stored selection
19
19
  with `docks-kit harnesses`.
20
20
 
@@ -56,7 +56,7 @@ ignored with a warning; Claude modifiers never touch Codex config and vice versa
56
56
 
57
57
  | Flag | Effect |
58
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` |
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/kit.db` (table `omp_session`) |
60
60
  | `--pick` | Interactive picker over free catalog models; records the choice the same way |
61
61
 
62
62
  Remaining arguments forward verbatim to omp after the launcher flags.
@@ -1,9 +1,17 @@
1
1
  # Models
2
2
 
3
- `SoT/models.json` is the kit-verified model catalog — the single source for
4
- the engine validators, `docks-kit models`, the interactive picker, and the
5
- bare-flag helper output. Each tool section carries a `verified` date; update
6
- the entry and date when a model ships or retires.
3
+ `SoT/models.json` supplies aliases and notes for Claude and Codex. For each
4
+ enabled harness, `docks-kit models` and the picker list live model IDs when
5
+ available. Claude uses the Claude Code login to query the Anthropic models API
6
+ and caches the result for six hours in `~/.docks-kit/kit.db`; Codex reads
7
+ `~/.codex/models_cache.json`; omp runs `omp models --json`. The kit never needs
8
+ omp to list Claude models.
9
+
10
+ Live lists retain the curated aliases and notes, but omit curated IDs absent
11
+ from the live source. A disabled harness, missing login/cache, or unavailable
12
+ source falls back to the curated IDs. omp has no curated fallback models.
13
+ The `verified` field is the curated section date, not the live fetch date.
14
+ Bare model flags without a value print the curated list without a network call.
7
15
 
8
16
  ## Validation rules
9
17
 
@@ -33,9 +41,10 @@ the entry and date when a model ships or retires.
33
41
  ## Commands
34
42
 
35
43
  ```
36
- docks-kit models # both catalogs
37
- docks-kit models claude --json # machine-readable
38
- docks-kit model claude # current deployed + SoT + picker (TTY)
44
+ docks-kit models # enabled Claude, Codex, and omp catalogs
45
+ docks-kit models claude --json # source, fetch date, and fallback reason
46
+ docks-kit models claude --refresh # bypass the six-hour Anthropic cache
47
+ docks-kit model claude # current deployed + live list + picker (TTY)
39
48
  docks-kit model claude opus # per-machine override from the Opus SoT
40
49
  docks-kit sync claude --claude-model=opus # same, as part of a sync
41
50
  ```
@@ -418,8 +418,8 @@ distinct ladders: 21 include `medium`, 3 stop at `low, high, max` or
418
418
  bare selectors with no `:level` suffix, because an invented level makes omp
419
419
  fail when the session starts.
420
420
 
421
- The choice persists per machine in `~/.docks-kit/state.json` under
422
- `ompSession`, next to `harnesses`. It survives across sessions. It is never
421
+ The choice persists per machine in `~/.docks-kit/kit.db` (table
422
+ `omp_session`), next to the harness selection. It survives across sessions. It is never
423
423
  committed. `--model <selector>` records one selector and derives both levels
424
424
  from the catalog row. `--pick` opens an interactive wizard.
425
425
 
@@ -13,7 +13,7 @@ AI-assisted dev environment on every machine.
13
13
  | `SoT/.codex/` | Codex config (config.toml, rules, AGENTS.md, marketplace) |
14
14
  | `SoT/.agents/` | Universal agent skills manifest (agentskills.io standard) |
15
15
  | `SoT/.omp/` | omp config deployed to `~/.omp/agent/` (AGENTS.md, config.yml, models.yml, mcp.json) plus `intercom.json` for pi intercom |
16
- | `SoT/models.json` | Kit-verified model catalog (see `docks-kit docs models`) |
16
+ | `SoT/models.json` | Curated Claude and Codex aliases, notes, and offline fallback for the live model lists (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
19
  | `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain and the `docks-kit omp` session overlay |
@@ -2,7 +2,7 @@
2
2
 
3
3
  `docks-kit sync [claude] [codex] [agents] [omp]` — targets are positional
4
4
  words; no target means this machine's harness selection in
5
- `~/.docks-kit/state.json`. The default is `claude codex agents`, and
5
+ `~/.docks-kit/kit.db`. The default is `claude codex agents`, and
6
6
  `docks-kit harnesses` changes it.
7
7
 
8
8
  ## claude (→ ~/.claude, ~/.claude.json, shell rc)
@@ -85,4 +85,14 @@ manifest's `verified` version.
85
85
  ```text
86
86
  docks-kit toolchain check # doctor table (also inside docks-kit status)
87
87
  docks-kit toolchain ensure bun # ensure the only managed tool
88
+ docks-kit toolchain outdated [--refresh] # verified pins vs newest upstream release
88
89
  ```
90
+
91
+ `docks-kit toolchain outdated` reads each tool's optional `upstream` entry in
92
+ `SoT/toolchain.json`. An `npm` entry reads the registry `latest` tag. With a
93
+ `line`, it reads the newest release on that major line instead, so `tsc` stays
94
+ on 6.x. A `github` entry reads the latest release tag and strips `tagPrefix`.
95
+ Set `GITHUB_TOKEN` or `GH_TOKEN` to raise the GitHub rate limit. Successful
96
+ lookups cache for 24 hours in `~/.docks-kit/kit.db`; `--refresh` bypasses the
97
+ cache. The report never installs anything and never edits a pin. A failed
98
+ lookup prints `lookup failed: <reason>` and the command still exits 0.
@@ -24,8 +24,14 @@ const TOPICS: Record<string, { summary: string; body: string }> = {
24
24
  summary: "Deploy-time modifiers and the flag-less-sync-reverts contract",
25
25
  body: modifiers,
26
26
  },
27
- models: { summary: "Model catalog, validation rules, model get/set", body: models },
28
- toolchain: { summary: "Verified-version floors and the doctor table", body: toolchain },
27
+ models: {
28
+ summary: "Live model lists, the curated overlay, validation, model get/set",
29
+ body: models,
30
+ },
31
+ toolchain: {
32
+ summary: "Verified-version floors, the doctor table, and the upstream outdated report",
33
+ body: toolchain,
34
+ },
29
35
  plugins: { summary: "enabledPlugins tri-state + optional plugin opt-ins", body: plugins },
30
36
  install: {
31
37
  summary: "Install paths: repo checkout, bun add -g, POSIX/Windows installers",
@@ -4,12 +4,12 @@ import { bail } from "../engine";
4
4
  import {
5
5
  engineHome,
6
6
  HARNESSES,
7
- harnessStateFile,
8
7
  LEGACY_SELECTION,
9
8
  readHarnessSelection,
10
9
  writeHarnessSelection,
11
10
  type Harness,
12
11
  } from "../engine-native/harnesses";
12
+ import { kitDbFile } from "../engine-native/kitDb";
13
13
 
14
14
  const DESCRIPTIONS: Record<Harness, string> = {
15
15
  claude: "Claude Code user configuration",
@@ -50,7 +50,7 @@ export const harnessesCommand = Command.make("harnesses", {}, () =>
50
50
  }
51
51
 
52
52
  yield* Effect.sync(() => writeHarnessSelection(home, answer));
53
- yield* Console.log(`Saved harness selection: ${answer.join(", ")} (${harnessStateFile(home)})`);
53
+ yield* Console.log(`Saved harness selection: ${answer.join(", ")} (${kitDbFile(home)})`);
54
54
  }),
55
55
  ).pipe(
56
56
  Command.withDescription("Choose the harness selection that drives a flag-less docks-kit sync."),
@@ -1,7 +1,8 @@
1
1
  import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
3
  import { bail, engine } from "../engine";
4
- import { modelCatalog } from "../engine-native/models";
4
+ import { engineHome } from "../engine-native/harnesses";
5
+ import { defaultLiveInputs, resolveCatalog } from "../engine-native/liveModels";
5
6
  import type { Tool } from "../manifests";
6
7
 
7
8
  const tool = Argument.String("tool").pipe(Argument.withDescription("Which tool: claude | codex"));
@@ -42,7 +43,9 @@ export const modelCommand = Command.make("model", { tool, value, dryRun, verbose
42
43
  // … and offer an interactive picker when attached to a terminal.
43
44
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
44
45
 
45
- const catalog = modelCatalog(t);
46
+ const catalog = yield* Effect.promise(() =>
47
+ resolveCatalog(t, defaultLiveInputs(engineHome(process.env))),
48
+ );
46
49
  const chosen = yield* Prompt.Select({
47
50
  message: `Set the deployed ${t} model (deployed config only; a flag-less sync reverts to SoT)`,
48
51
  choices: [
@@ -1,48 +1,75 @@
1
1
  import { Argument, Command, Flag } from "effect/unstable/cli";
2
2
  import { Console, Effect, Option } from "effect";
3
3
  import { bail } from "../engine";
4
- import { modelCatalog } from "../engine-native/models";
5
- import type { Tool } from "../manifests";
4
+ import { engineHome, LEGACY_SELECTION, readHarnessSelection } from "../engine-native/harnesses";
5
+ import { defaultLiveInputs, resolveCatalog } from "../engine-native/liveModels";
6
+ import type { CatalogTool, ResolvedCatalog } from "../engine-native/sharedTypes";
6
7
 
7
8
  const tool = Argument.String("tool").pipe(
8
- Argument.withDescription("claude | codex (omit for both tool catalogs)"),
9
+ Argument.withDescription("claude | codex | omp (omit for enabled harnesses)"),
9
10
  Argument.optional,
10
11
  );
11
12
  const json = Flag.Boolean("json").pipe(
12
13
  Flag.withDescription("Machine-readable output"),
13
14
  Flag.withDefault(false),
14
15
  );
16
+ const refresh = Flag.Boolean("refresh").pipe(
17
+ Flag.withDescription("Bypass the kit cache (~/.docks-kit/kit.db) and fetch live lists again"),
18
+ Flag.withDefault(false),
19
+ );
15
20
 
16
- const renderTool = (t: Tool) =>
21
+ function isCatalogTool(value: string): value is CatalogTool {
22
+ return value === "claude" || value === "codex" || value === "omp";
23
+ }
24
+
25
+ const renderTool = (t: CatalogTool, catalog: ResolvedCatalog) =>
17
26
  Effect.gen(function* () {
18
- const catalog = modelCatalog(t);
19
- yield* Console.log(`${t} models (kit-verified ${catalog.verified}):`);
27
+ yield* Console.log(
28
+ catalog.source === "curated"
29
+ ? `${t} models (kit-verified ${catalog.verified}):`
30
+ : `${t} models (live — ${catalog.source}, fetched ${catalog.fetchedAt ?? "?"}):`,
31
+ );
32
+ if (catalog.fallbackReason !== undefined) {
33
+ yield* Console.log(` (live list unavailable: ${catalog.fallbackReason})`);
34
+ }
20
35
  for (const m of catalog.models) {
21
36
  yield* Console.log(` ${m.id.padEnd(28)} ${m.kind.padEnd(6)} ${m.note ?? ""}`);
22
37
  }
23
- yield* Console.log(
24
- t === "claude"
25
- ? " (full claude-* model IDs outside the catalog are accepted with a warning)"
26
- : " (well-formed IDs outside the catalog are accepted with a warning)",
27
- );
38
+ if (t === "claude") {
39
+ yield* Console.log(
40
+ " (full claude-* model IDs outside the catalog are accepted with a warning)",
41
+ );
42
+ } else if (t === "codex") {
43
+ yield* Console.log(" (well-formed IDs outside the catalog are accepted with a warning)");
44
+ }
28
45
  yield* Console.log("");
29
46
  });
30
47
 
31
- export const modelsCommand = Command.make("models", { tool, json }, (config) =>
48
+ export const modelsCommand = Command.make("models", { tool, json, refresh }, (config) =>
32
49
  Effect.gen(function* () {
33
50
  const requested = Option.getOrUndefined(config.tool);
34
- if (requested !== undefined && requested !== "claude" && requested !== "codex") {
35
- return yield* bail(`Unknown tool '${requested}' (valid: claude, codex)`);
36
- }
37
- const tools: Array<Tool> = requested !== undefined ? [requested as Tool] : ["claude", "codex"];
38
-
39
- if (config.json) {
40
- const out = Object.fromEntries(tools.map((t) => [t, modelCatalog(t)]));
41
- return yield* Console.log(JSON.stringify(out, null, 2));
51
+ if (requested !== undefined && !isCatalogTool(requested)) {
52
+ return yield* bail(`Unknown tool '${requested}' (valid: claude, codex, omp)`);
42
53
  }
43
-
54
+ const home = engineHome(process.env);
55
+ const tools: ReadonlyArray<CatalogTool> =
56
+ requested !== undefined
57
+ ? [requested]
58
+ : (readHarnessSelection(home) ?? LEGACY_SELECTION).filter(isCatalogTool);
59
+ const inputs = defaultLiveInputs(home, config.refresh);
60
+ const out: Partial<Record<CatalogTool, ResolvedCatalog>> = {};
44
61
  for (const t of tools) {
45
- yield* renderTool(t);
62
+ const catalog = yield* Effect.promise(() => resolveCatalog(t, inputs));
63
+ if (config.json) {
64
+ out[t] = catalog;
65
+ } else {
66
+ yield* renderTool(t, catalog);
67
+ }
46
68
  }
69
+ if (config.json) yield* Console.log(JSON.stringify(out, null, 2));
47
70
  }),
48
- ).pipe(Command.withDescription("List kit-verified Claude and Codex models (SoT/models.json)."));
71
+ ).pipe(
72
+ Command.withDescription(
73
+ "List models per enabled harness: live from each harness's login or cache, with SoT/models.json aliases and notes as overlay and offline fallback.",
74
+ ),
75
+ );
@@ -5,7 +5,7 @@ import { bail, engine } from "../engine";
5
5
  const MANAGED = ["bun"];
6
6
 
7
7
  const op = Argument.String("op").pipe(
8
- Argument.withDescription("check (default) | ensure <tool>"),
8
+ Argument.withDescription("check (default) | ensure <tool> | outdated"),
9
9
  Argument.optional,
10
10
  );
11
11
  const tool = Argument.String("tool").pipe(
@@ -17,28 +17,46 @@ const verbose = Flag.Boolean("verbose").pipe(
17
17
  Flag.withDescription("Also print no-op confirmations (present, up to date)"),
18
18
  Flag.withDefault(false),
19
19
  );
20
+ const refresh = Flag.Boolean("refresh").pipe(
21
+ Flag.withDescription("outdated: bypass the 24 h cache"),
22
+ Flag.withDefault(false),
23
+ );
20
24
 
21
- export const toolchainCommand = Command.make("toolchain", { op, tool, verbose }, (config) =>
22
- Effect.gen(function* () {
23
- const operation = Option.getOrElse(config.op, () => "check");
24
- const flags = config.verbose ? ["--verbose"] : [];
25
+ export const toolchainCommand = Command.make(
26
+ "toolchain",
27
+ { op, tool, verbose, refresh },
28
+ (config) =>
29
+ Effect.gen(function* () {
30
+ const operation = Option.getOrElse(config.op, () => "check");
31
+ const flags = config.verbose ? ["--verbose"] : [];
32
+ const operand = Option.getOrUndefined(config.tool);
33
+ const words = operand === undefined ? [] : [operand];
25
34
 
26
- switch (operation) {
27
- case "check":
28
- return yield* engine(["toolchain", "check", ...(config.verbose ? ["--verbose"] : [])]);
29
- case "ensure": {
30
- const t = Option.getOrUndefined(config.tool);
31
- if (t === undefined || !MANAGED.includes(t)) {
32
- return yield* bail(`toolchain ensure needs a managed tool: ${MANAGED.join(", ")}`);
35
+ switch (operation) {
36
+ case "check":
37
+ return yield* engine(["toolchain", "check", ...words, ...flags]);
38
+ case "ensure": {
39
+ const t = Option.getOrUndefined(config.tool);
40
+ if (t === undefined || !MANAGED.includes(t)) {
41
+ return yield* bail(`toolchain ensure needs a managed tool: ${MANAGED.join(", ")}`);
42
+ }
43
+ return yield* engine(["toolchain", "ensure", t, ...flags]);
33
44
  }
34
- return yield* engine(["toolchain", "ensure", t, ...flags]);
45
+ case "outdated":
46
+ return yield* engine([
47
+ "toolchain",
48
+ "outdated",
49
+ ...words,
50
+ ...(config.refresh ? ["--refresh"] : []),
51
+ ]);
52
+ default:
53
+ return yield* bail(
54
+ `Unknown toolchain op '${operation}' (valid: check, ensure, outdated)`,
55
+ );
35
56
  }
36
- default:
37
- return yield* bail(`Unknown toolchain op '${operation}' (valid: check, ensure)`);
38
- }
39
- }),
57
+ }),
40
58
  ).pipe(
41
59
  Command.withDescription(
42
- "Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs one managed tool when it is missing.",
60
+ "Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs one managed tool when it is missing; outdated compares verified pins with the newest upstream release (network, report only).",
43
61
  ),
44
62
  );
@@ -60,9 +60,8 @@ const capturePackageRoot: CapturePackageRoot = (command, args) => {
60
60
 
61
61
  /**
62
62
  * A Bun global home is `<root>/.bun/install/global/node_modules/<pkg>`. Windows
63
- * reports that path with backslashes, so containment is tested on a normalized
64
- * copy — but only on Windows, because a backslash is a legal POSIX filename
65
- * character and must never be read as a separator there.
63
+ * reports that path with backslashes and compares it without case sensitivity.
64
+ * On POSIX, a backslash remains a legal filename character, not a separator.
66
65
  */
67
66
  export const packageManagerForHome = (
68
67
  home: string,
@@ -70,13 +69,14 @@ export const packageManagerForHome = (
70
69
  host: HostOs = hostOs(),
71
70
  ): PackageManager => {
72
71
  const normalize = (value: string): string =>
73
- host.id === "windows" ? value.replaceAll("\\", "/") : value;
72
+ host.id === "windows" ? value.replaceAll("\\", "/").toLowerCase() : value;
74
73
  const normalizedHome = normalize(home);
75
74
  const underEnvironmentRoot = (name: "BUN_INSTALL_GLOBAL_DIR" | "BUN_INSTALL"): boolean => {
76
75
  const root = environment[name]?.trim();
77
76
  if (root === undefined || root === "") return false;
78
- const normalizedRoot = normalize(root);
79
- return normalizedHome === normalizedRoot || normalizedHome.startsWith(`${normalizedRoot}/`);
77
+ const normalizedRoot = normalize(root).replace(/\/+$/, "") || "/";
78
+ const prefix = normalizedRoot.endsWith("/") ? normalizedRoot : `${normalizedRoot}/`;
79
+ return normalizedHome === normalizedRoot || normalizedHome.startsWith(prefix);
80
80
  };
81
81
  return normalizedHome.includes("/.bun/") ||
82
82
  underEnvironmentRoot("BUN_INSTALL_GLOBAL_DIR") ||
@@ -19,6 +19,9 @@ import {
19
19
  import { payloadText } from "../payload";
20
20
  import type { JsonObject } from "./sharedTypes";
21
21
 
22
+ /** Claude Code's built-in marketplace: always present, never added or pruned by the kit. */
23
+ const OFFICIAL_MARKETPLACE = "claude-plugins-official";
24
+
22
25
  export async function cli(
23
26
  args: Array<string>,
24
27
  ): Promise<{ ok: boolean; out: string; detail: string }> {
@@ -113,20 +116,31 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
113
116
  ? repoObj["extraKnownMarketplaces"]
114
117
  : {};
115
118
  const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {};
116
-
117
- // Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
118
- let addedMp = 0;
119
- let f1 = 0;
120
- for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
119
+ const knownMarketplace = (mpName: string): boolean => {
121
120
  const known = readJsonFile(knownMarketplaces);
122
- if (
121
+ return (
123
122
  known !== undefined &&
124
123
  isObject(known) &&
125
124
  known[mpName] !== undefined &&
126
125
  known[mpName] !== null &&
127
126
  known[mpName] !== false
128
- )
129
- continue;
127
+ );
128
+ };
129
+ const addedMarketplaces = new Set<string>();
130
+ // The official marketplace is built in; a successful add is usable before its inventory is written.
131
+ // A missing inventory proves absence (fresh home); an unreadable one does not, so refresh as before.
132
+ const marketplaceReady = (mpName: string): boolean => {
133
+ if (mpName === OFFICIAL_MARKETPLACE || addedMarketplaces.has(mpName)) return true;
134
+ if (!existsSync(knownMarketplaces)) return false;
135
+ const known = readJsonFile(knownMarketplaces);
136
+ return known === undefined || !isObject(known) || knownMarketplace(mpName);
137
+ };
138
+
139
+ // Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
140
+ let addedMp = 0;
141
+ let f1 = 0;
142
+ for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
143
+ if (knownMarketplace(mpName)) continue;
130
144
  const repo =
131
145
  isObject(mpValue) && isObject(mpValue["source"])
132
146
  ? String((mpValue["source"] as JsonObject)["repo"] ?? "")
@@ -136,6 +150,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
136
150
  clearProgress();
137
151
  if (marketplaceResult.ok) {
138
152
  addedMp++;
153
+ addedMarketplaces.add(mpName);
139
154
  } else {
140
155
  recordFailure(
141
156
  ctx,
@@ -156,7 +171,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
156
171
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue;
157
172
  const separator = pluginId.lastIndexOf("@");
158
173
  const mpName = separator > 0 ? pluginId.slice(separator + 1) : "";
159
- if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
174
+ if (mpName !== "" && marketplaceReady(mpName) && !refreshedMarketplaces.has(mpName)) {
160
175
  progress(`Refreshing marketplace ${mpName}...`);
161
176
  const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
162
177
  clearProgress();
@@ -207,7 +222,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
207
222
  // would duplicate one failure in the ledger and in the failed-operation count.
208
223
  if (!ctx.skipPluginRefresh) {
209
224
  for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
210
- if (refreshedMarketplaces.has(mpName)) continue;
225
+ if (refreshedMarketplaces.has(mpName) || !marketplaceReady(mpName)) continue;
211
226
  progress(`Refreshing marketplace ${mpName}...`);
212
227
  const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
213
228
  clearProgress();
@@ -263,7 +278,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
263
278
  // Pass 6 — prune-gated marketplace removal.
264
279
  const known = readJsonFile(knownMarketplaces);
265
280
  for (const mpName of sortedKeys(known)) {
266
- if (mpName === "claude-plugins-official") continue;
281
+ if (mpName === OFFICIAL_MARKETPLACE) continue;
267
282
  if (nonUserMarketplaces.has(mpName)) continue;
268
283
  if (kitMarketplaces.has(mpName)) continue;
269
284
  progress(`Removing marketplace ${mpName}...`);
@@ -7,7 +7,7 @@
7
7
  import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
8
 
9
9
  import { payloadDisplayPath } from "../payload";
10
- import { mergeTableSettings, mergeTopLevelSettings } from "./codexToml";
10
+ import { isTomlHeaderLine, mergeTableSettings, mergeTopLevelSettings } from "./codexToml";
11
11
  import type { Ctx } from "./index";
12
12
 
13
13
  // --------------------------------------------------------------- config ----
@@ -68,14 +68,14 @@ function scrubDeprecatedFeaturesText(content: string): string {
68
68
  let changed = false;
69
69
  for (const line of lines) {
70
70
  if (inFeatures) {
71
- if (line.startsWith("[")) {
71
+ if (isTomlHeaderLine(line)) {
72
72
  inFeatures = false;
73
73
  if (keep) out += `${header}\n${body}`;
74
74
  else changed = true;
75
75
  out += `${line}\n`;
76
76
  continue;
77
77
  }
78
- if (/^use_legacy_landlock[ \t]*=/.test(line)) {
78
+ if (/^[ \t]*use_legacy_landlock[ \t]*=/.test(line)) {
79
79
  changed = true;
80
80
  continue;
81
81
  }
@@ -83,7 +83,7 @@ function scrubDeprecatedFeaturesText(content: string): string {
83
83
  if (/[^ \t\f\v\r]/.test(line)) keep = true;
84
84
  continue;
85
85
  }
86
- if (/^\[features\][ \t]*$/.test(line)) {
86
+ if (/^[ \t]*\[features\][ \t]*$/.test(line)) {
87
87
  inFeatures = true;
88
88
  header = line;
89
89
  body = "";
@@ -111,7 +111,7 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
111
111
  change("Codex: scrubbed deprecated [features].use_legacy_landlock");
112
112
  }
113
113
 
114
- export const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/;
114
+ export const PLUGIN_TABLE_HEADER = /^[ \t]*\[plugins\."([^"]+)"\][ \t]*$/;
115
115
  /** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
116
116
  const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
117
117
  "effect-kit@docks": true,
@@ -130,7 +130,7 @@ export function removeRetiredPluginTablesText(content: string): string {
130
130
  skipping = RETIRED_PLUGIN_IDS[header[1]!] === true;
131
131
  if (skipping) continue;
132
132
  } else if (skipping) {
133
- if (!line.startsWith("[")) continue;
133
+ if (!isTomlHeaderLine(line)) continue;
134
134
  skipping = false;
135
135
  }
136
136
  out += `${line}\n`;