@sagmans/dsh-tui 0.6.0 → 0.8.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.
Files changed (271) hide show
  1. package/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
  2. package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
  3. package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
  4. package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
  5. package/.agents/skills/dsh-tui-update-models/SKILL.md +199 -0
  6. package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +292 -0
  7. package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +314 -0
  8. package/README.md +154 -27
  9. package/lib/agent/present.d.ts.map +1 -1
  10. package/lib/agent/present.js +2 -1
  11. package/lib/agent/present.js.map +1 -1
  12. package/lib/cards/composition.d.ts +68 -0
  13. package/lib/cards/composition.d.ts.map +1 -0
  14. package/lib/cards/composition.js +117 -0
  15. package/lib/cards/composition.js.map +1 -0
  16. package/lib/cards/presenter.d.ts +23 -0
  17. package/lib/cards/presenter.d.ts.map +1 -0
  18. package/lib/cards/presenter.js +394 -0
  19. package/lib/cards/presenter.js.map +1 -0
  20. package/lib/cards/preview.d.ts +42 -0
  21. package/lib/cards/preview.d.ts.map +1 -0
  22. package/lib/cards/preview.js +51 -0
  23. package/lib/cards/preview.js.map +1 -0
  24. package/lib/cards.d.ts +12 -120
  25. package/lib/cards.d.ts.map +1 -1
  26. package/lib/cards.js +4 -556
  27. package/lib/cards.js.map +1 -1
  28. package/lib/compat/section.d.ts +29 -0
  29. package/lib/compat/section.d.ts.map +1 -0
  30. package/lib/compat/section.js +82 -0
  31. package/lib/compat/section.js.map +1 -0
  32. package/lib/config.d.ts +27 -2
  33. package/lib/config.d.ts.map +1 -1
  34. package/lib/config.js +61 -1
  35. package/lib/config.js.map +1 -1
  36. package/lib/contracts.d.ts +28 -0
  37. package/lib/contracts.d.ts.map +1 -1
  38. package/lib/fold-cursor.d.ts +8 -0
  39. package/lib/fold-cursor.d.ts.map +1 -1
  40. package/lib/fold-cursor.js +18 -0
  41. package/lib/fold-cursor.js.map +1 -1
  42. package/lib/gates/question-card.d.ts +77 -0
  43. package/lib/gates/question-card.d.ts.map +1 -0
  44. package/lib/gates/question-card.js +113 -0
  45. package/lib/gates/question-card.js.map +1 -0
  46. package/lib/gates/questions.d.ts +127 -0
  47. package/lib/gates/questions.d.ts.map +1 -0
  48. package/lib/gates/questions.js +418 -0
  49. package/lib/gates/questions.js.map +1 -0
  50. package/lib/gates.d.ts +1 -113
  51. package/lib/gates.d.ts.map +1 -1
  52. package/lib/gates.js +1 -484
  53. package/lib/gates.js.map +1 -1
  54. package/lib/herdr/client.d.ts +8 -0
  55. package/lib/herdr/client.d.ts.map +1 -1
  56. package/lib/herdr/client.js +13 -1
  57. package/lib/herdr/client.js.map +1 -1
  58. package/lib/herdr/constants.d.ts +18 -0
  59. package/lib/herdr/constants.d.ts.map +1 -1
  60. package/lib/herdr/constants.js +18 -0
  61. package/lib/herdr/constants.js.map +1 -1
  62. package/lib/herdr/reporter.d.ts +20 -4
  63. package/lib/herdr/reporter.d.ts.map +1 -1
  64. package/lib/herdr/reporter.js +70 -20
  65. package/lib/herdr/reporter.js.map +1 -1
  66. package/lib/herdr/state.d.ts +10 -0
  67. package/lib/herdr/state.d.ts.map +1 -1
  68. package/lib/herdr/state.js +21 -4
  69. package/lib/herdr/state.js.map +1 -1
  70. package/lib/index.d.ts +1 -0
  71. package/lib/index.d.ts.map +1 -1
  72. package/lib/index.js +348 -2602
  73. package/lib/index.js.map +1 -1
  74. package/lib/input/action-catalog.d.ts +74 -0
  75. package/lib/input/action-catalog.d.ts.map +1 -0
  76. package/lib/input/action-catalog.js +137 -0
  77. package/lib/input/action-catalog.js.map +1 -0
  78. package/lib/input/actions.d.ts +5 -81
  79. package/lib/input/actions.d.ts.map +1 -1
  80. package/lib/input/actions.js +4 -625
  81. package/lib/input/actions.js.map +1 -1
  82. package/lib/input/completion.d.ts +1 -1
  83. package/lib/input/completion.d.ts.map +1 -1
  84. package/lib/input/completion.js +2 -1
  85. package/lib/input/completion.js.map +1 -1
  86. package/lib/input/file-index.d.ts +40 -0
  87. package/lib/input/file-index.d.ts.map +1 -0
  88. package/lib/input/file-index.js +141 -0
  89. package/lib/input/file-index.js.map +1 -0
  90. package/lib/input/file-search.d.ts +1 -38
  91. package/lib/input/file-search.d.ts.map +1 -1
  92. package/lib/input/file-search.js +1 -487
  93. package/lib/input/file-search.js.map +1 -1
  94. package/lib/input/key-press.d.ts +57 -0
  95. package/lib/input/key-press.d.ts.map +1 -0
  96. package/lib/input/key-press.js +218 -0
  97. package/lib/input/key-press.js.map +1 -0
  98. package/lib/input/keymap-conflicts.d.ts +59 -0
  99. package/lib/input/keymap-conflicts.d.ts.map +1 -0
  100. package/lib/input/keymap-conflicts.js +292 -0
  101. package/lib/input/keymap-conflicts.js.map +1 -0
  102. package/lib/input/keymap-settings.js +1 -1
  103. package/lib/input/keymap-settings.js.map +1 -1
  104. package/lib/input/keymap.d.ts.map +1 -1
  105. package/lib/input/keymap.js +3 -1
  106. package/lib/input/keymap.js.map +1 -1
  107. package/lib/input/workspace-files.d.ts +10 -0
  108. package/lib/input/workspace-files.d.ts.map +1 -0
  109. package/lib/input/workspace-files.js +359 -0
  110. package/lib/input/workspace-files.js.map +1 -0
  111. package/lib/install-skills.d.ts +39 -0
  112. package/lib/install-skills.d.ts.map +1 -0
  113. package/lib/install-skills.js +273 -0
  114. package/lib/install-skills.js.map +1 -0
  115. package/lib/keys-command.d.ts +2 -1
  116. package/lib/keys-command.d.ts.map +1 -1
  117. package/lib/keys-command.js +2 -1
  118. package/lib/keys-command.js.map +1 -1
  119. package/lib/model-list.d.ts +37 -0
  120. package/lib/model-list.d.ts.map +1 -0
  121. package/lib/model-list.js +60 -0
  122. package/lib/model-list.js.map +1 -0
  123. package/lib/startup.d.ts +8 -0
  124. package/lib/startup.d.ts.map +1 -1
  125. package/lib/startup.js +120 -24
  126. package/lib/startup.js.map +1 -1
  127. package/lib/subagents.d.ts +7 -3
  128. package/lib/subagents.d.ts.map +1 -1
  129. package/lib/subagents.js +28 -4
  130. package/lib/subagents.js.map +1 -1
  131. package/lib/surface/appearance-preferences.d.ts +26 -0
  132. package/lib/surface/appearance-preferences.d.ts.map +1 -0
  133. package/lib/surface/appearance-preferences.js +139 -0
  134. package/lib/surface/appearance-preferences.js.map +1 -0
  135. package/lib/surface/appearance.d.ts +87 -0
  136. package/lib/surface/appearance.d.ts.map +1 -0
  137. package/lib/surface/appearance.js +353 -0
  138. package/lib/surface/appearance.js.map +1 -0
  139. package/lib/surface/background-work.d.ts +51 -0
  140. package/lib/surface/background-work.d.ts.map +1 -0
  141. package/lib/surface/background-work.js +172 -0
  142. package/lib/surface/background-work.js.map +1 -0
  143. package/lib/surface/commands.d.ts +82 -0
  144. package/lib/surface/commands.d.ts.map +1 -0
  145. package/lib/surface/commands.js +370 -0
  146. package/lib/surface/commands.js.map +1 -0
  147. package/lib/surface/modal-input.d.ts +59 -0
  148. package/lib/surface/modal-input.d.ts.map +1 -0
  149. package/lib/surface/modal-input.js +243 -0
  150. package/lib/surface/modal-input.js.map +1 -0
  151. package/lib/surface/model-choice.d.ts +54 -0
  152. package/lib/surface/model-choice.d.ts.map +1 -0
  153. package/lib/surface/model-choice.js +325 -0
  154. package/lib/surface/model-choice.js.map +1 -0
  155. package/lib/surface/preset-choice.d.ts +78 -0
  156. package/lib/surface/preset-choice.d.ts.map +1 -0
  157. package/lib/surface/preset-choice.js +163 -0
  158. package/lib/surface/preset-choice.js.map +1 -0
  159. package/lib/surface/prompt-input.d.ts +97 -0
  160. package/lib/surface/prompt-input.d.ts.map +1 -0
  161. package/lib/surface/prompt-input.js +215 -0
  162. package/lib/surface/prompt-input.js.map +1 -0
  163. package/lib/surface/prompt-memory.d.ts +76 -0
  164. package/lib/surface/prompt-memory.d.ts.map +1 -0
  165. package/lib/surface/prompt-memory.js +167 -0
  166. package/lib/surface/prompt-memory.js.map +1 -0
  167. package/lib/surface/session-lifecycle.d.ts +120 -0
  168. package/lib/surface/session-lifecycle.d.ts.map +1 -0
  169. package/lib/surface/session-lifecycle.js +324 -0
  170. package/lib/surface/session-lifecycle.js.map +1 -0
  171. package/lib/surface/session-picker.d.ts +39 -0
  172. package/lib/surface/session-picker.d.ts.map +1 -0
  173. package/lib/surface/session-picker.js +107 -0
  174. package/lib/surface/session-picker.js.map +1 -0
  175. package/lib/surface/session-view.d.ts +86 -0
  176. package/lib/surface/session-view.d.ts.map +1 -0
  177. package/lib/surface/session-view.js +215 -0
  178. package/lib/surface/session-view.js.map +1 -0
  179. package/lib/surface/staged-turns.d.ts +81 -0
  180. package/lib/surface/staged-turns.d.ts.map +1 -0
  181. package/lib/surface/staged-turns.js +206 -0
  182. package/lib/surface/staged-turns.js.map +1 -0
  183. package/lib/surface/terminal-lifecycle.d.ts +76 -0
  184. package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
  185. package/lib/surface/terminal-lifecycle.js +222 -0
  186. package/lib/surface/terminal-lifecycle.js.map +1 -0
  187. package/lib/terminal-text/scan.d.ts +44 -0
  188. package/lib/terminal-text/scan.d.ts.map +1 -0
  189. package/lib/terminal-text/scan.js +195 -0
  190. package/lib/terminal-text/scan.js.map +1 -0
  191. package/lib/terminal-text/sgr.d.ts +32 -0
  192. package/lib/terminal-text/sgr.d.ts.map +1 -0
  193. package/lib/terminal-text/sgr.js +203 -0
  194. package/lib/terminal-text/sgr.js.map +1 -0
  195. package/lib/terminal-text.d.ts +1 -1
  196. package/lib/terminal-text.d.ts.map +1 -1
  197. package/lib/terminal-text.js +2 -379
  198. package/lib/terminal-text.js.map +1 -1
  199. package/lib/theme-command.js +2 -1
  200. package/lib/theme-command.js.map +1 -1
  201. package/lib/theme-defaults.d.ts +64 -0
  202. package/lib/theme-defaults.d.ts.map +1 -0
  203. package/lib/theme-defaults.js +248 -0
  204. package/lib/theme-defaults.js.map +1 -0
  205. package/lib/theme-files.d.ts +1 -1
  206. package/lib/theme-files.d.ts.map +1 -1
  207. package/lib/theme-files.js +1 -1
  208. package/lib/theme-files.js.map +1 -1
  209. package/lib/theme-resolver.d.ts +36 -0
  210. package/lib/theme-resolver.d.ts.map +1 -0
  211. package/lib/theme-resolver.js +112 -0
  212. package/lib/theme-resolver.js.map +1 -0
  213. package/lib/theme-schema.d.ts.map +1 -1
  214. package/lib/theme-schema.js +3 -2
  215. package/lib/theme-schema.js.map +1 -1
  216. package/lib/theme-settings.d.ts +5 -7
  217. package/lib/theme-settings.d.ts.map +1 -1
  218. package/lib/theme-settings.js +37 -21
  219. package/lib/theme-settings.js.map +1 -1
  220. package/lib/theme-tokens.d.ts +0 -89
  221. package/lib/theme-tokens.d.ts.map +1 -1
  222. package/lib/theme-tokens.js +0 -356
  223. package/lib/theme-tokens.js.map +1 -1
  224. package/lib/theme.d.ts +1 -1
  225. package/lib/theme.d.ts.map +1 -1
  226. package/lib/theme.js +2 -1
  227. package/lib/theme.js.map +1 -1
  228. package/lib/tool-display.d.ts.map +1 -1
  229. package/lib/tool-display.js +2 -1
  230. package/lib/tool-display.js.map +1 -1
  231. package/lib/transcript/message-content.d.ts +21 -0
  232. package/lib/transcript/message-content.d.ts.map +1 -0
  233. package/lib/transcript/message-content.js +61 -0
  234. package/lib/transcript/message-content.js.map +1 -0
  235. package/lib/transcript/tool-calls.d.ts +106 -0
  236. package/lib/transcript/tool-calls.d.ts.map +1 -0
  237. package/lib/transcript/tool-calls.js +254 -0
  238. package/lib/transcript/tool-calls.js.map +1 -0
  239. package/lib/transcript.d.ts +14 -55
  240. package/lib/transcript.d.ts.map +1 -1
  241. package/lib/transcript.js +31 -253
  242. package/lib/transcript.js.map +1 -1
  243. package/lib/ui/dock.d.ts +14 -2
  244. package/lib/ui/dock.d.ts.map +1 -1
  245. package/lib/ui/dock.js +52 -10
  246. package/lib/ui/dock.js.map +1 -1
  247. package/lib/ui/editor.d.ts.map +1 -1
  248. package/lib/ui/editor.js +2 -1
  249. package/lib/ui/editor.js.map +1 -1
  250. package/lib/ui/keymap-picker.d.ts +2 -1
  251. package/lib/ui/keymap-picker.d.ts.map +1 -1
  252. package/lib/ui/keymap-picker.js +1 -0
  253. package/lib/ui/keymap-picker.js.map +1 -1
  254. package/lib/ui/view/gate-card.d.ts +21 -0
  255. package/lib/ui/view/gate-card.d.ts.map +1 -0
  256. package/lib/ui/view/gate-card.js +104 -0
  257. package/lib/ui/view/gate-card.js.map +1 -0
  258. package/lib/ui/view/tool-card.d.ts +149 -0
  259. package/lib/ui/view/tool-card.d.ts.map +1 -0
  260. package/lib/ui/view/tool-card.js +429 -0
  261. package/lib/ui/view/tool-card.js.map +1 -0
  262. package/lib/ui/view/transcript-message.d.ts +62 -0
  263. package/lib/ui/view/transcript-message.d.ts.map +1 -0
  264. package/lib/ui/view/transcript-message.js +158 -0
  265. package/lib/ui/view/transcript-message.js.map +1 -0
  266. package/lib/ui/view.d.ts +105 -223
  267. package/lib/ui/view.d.ts.map +1 -1
  268. package/lib/ui/view.js +132 -679
  269. package/lib/ui/view.js.map +1 -1
  270. package/package.json +3 -1
  271. package/themes/violet-orbit.yaml +7 -5
@@ -0,0 +1,292 @@
1
+ # Model wiring reference
2
+
3
+ Companion to [SKILL.md](../SKILL.md): exact fields, files, and checks.
4
+
5
+ ## Resolution order
6
+
7
+ 1. Bundle layers in `cordis.yml`, in order.
8
+ 2. The profile patch `~/.dsh/profiles/<profile>/cordis.patch.yml`, entry by entry:
9
+ `id` targeted config, `disabled`, `insert`.
10
+ 3. A patch `config` **replaces** that row's base config; fields it omits fall back
11
+ to the schema default, not to the bundle's value.
12
+ 4. The model directory a profile sees is owned by whichever row is enabled:
13
+ `llm-pi-ai` (additive provider config), `dsh-provider-extra` (owned catalog),
14
+ or both, if the profile is not using exclusive ownership.
15
+ 5. `$DSH_HOME/settings.yaml` sits over the adapter rows, not over a catalog. A
16
+ `llm-pi-ai:` or `llm-deepseek:` section reshapes that adapter per request; a
17
+ `dsh-provider-extra:` section reshapes only the legacy routes'
18
+ `extraModels`/`codexExtraModels`, and only where no `models`/`codexModels`
19
+ selection is declared. A managed catalog reads no settings overlay at all, so
20
+ a catalog edit lands in the profile patch or nowhere.
21
+
22
+ Exclusive ownership is a set of disable rows plus the catalog itself:
23
+
24
+ ```yaml
25
+ - { "id": "agent-default-model", "disabled": true }
26
+ - { "id": "llm-pi-ai", "disabled": true }
27
+ - { "id": "llm-deepseek", "disabled": true }
28
+ - id: dsh-provider-extra
29
+ config: { catalog: { version: 1, ... } }
30
+ ```
31
+
32
+ Disable every competing row in the **whole** profile, not only the rows that come
33
+ before provider-extra: on `dsh-base` those are `llm-pi-ai`, `llm-deepseek`, and
34
+ `agent-default-model`, and another bundle may add `llm-openai` or
35
+ `llm-anthropic`. Inspect the composed profile rather than trusting this list.
36
+ Preflight refuses to register managed routes when an adapter, a provider
37
+ directory, or a default owner already exists; a later registration is caught by
38
+ the host's `llm/adapters-updated` notification. Either way the plugin logs
39
+ `CATALOG_OWNER_COLLISION` once per transition and fails closed: listing,
40
+ selection resolution, request preparation, default reads and saves, new
41
+ dispatch, and starting a sign-in all refuse while the conflict exists. Requests
42
+ already past their credential gates finish. Removing the competing rows restores
43
+ service without a reload of the catalog itself.
44
+
45
+ ## `llm-pi-ai` provider config
46
+
47
+ ```yaml
48
+ - id: llm-pi-ai
49
+ config:
50
+ providers:
51
+ qwen-token-plan:
52
+ apiKeyEnv: QWEN_TOKEN_PLAN_API_KEY # env var holding the key
53
+ baseURL: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1
54
+ api: openai-completions # optional wire override
55
+ models:
56
+ - id: qwen3.8-max # served as-is
57
+ - id: deepseek-v4.1-flash
58
+ name: DeepSeek V4.1 Flash
59
+ contextWindow: 1000000
60
+ maxTokens: 384000
61
+ input: [text, image]
62
+ reasoningEfforts: { low: low, high: high, max: max }
63
+ compat:
64
+ supportsStore: false
65
+ supportsDeveloperRole: false
66
+ supportsReasoningEffort: true
67
+ supportsStrictMode: true
68
+ thinkingFormat: qwen
69
+ ```
70
+
71
+ An id that the installed catalog already describes needs no restatement. An id it
72
+ does not describe needs the metadata (or a `template` sibling to clone).
73
+
74
+ Route fields (`providers` is a dict keyed by the route id; the key is the route):
75
+ `apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides` (per-id
76
+ reshaping of an installed catalog, refused beside a `models` list), `compat`,
77
+ `defaultContextWindow` (262144), `defaultMaxTokens` (32768), `defaultInput`
78
+ (`[text]`), `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`,
79
+ `transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`,
80
+ `maxRequestImageBytes`, `requestImagePixelBudget`, `requestImageMaxBytes`,
81
+ `retryPolicy`. A route key that names an installed pi-ai provider inherits that
82
+ provider's endpoint, protocol, display name, and catalog; any other key is a
83
+ complete hand declaration and must name `api` and `baseURL`.
84
+
85
+ `reasoningEfforts` is this adapter's spelling of `thinkingLevelMap`: a dict of
86
+ selectable level to the wire value dispatch sends, `false` for a non-reasoning
87
+ model, and only `off` may carry an empty or null value. An empty dict, a level
88
+ mapping to an empty string, or a dict offering nothing beyond `off` is refused.
89
+
90
+ ## `dsh-provider-extra`
91
+
92
+ Two config generations exist; a profile normally uses one, and presence of
93
+ `catalog` is what opts a profile into managed ownership:
94
+
95
+ | Config | Use |
96
+ | --- | --- |
97
+ | Additive: `apiKeyEnv`, `routeId`, `displayName`, `baseURL`, `headers`, `fallbackSessionId`, `models`, `extraModels`, `codexEnabled`, `codexRouteId`, `codexDisplayName`, `codexModels`, `codexExtraModels`, `codexTransport`, `loginCommandEnabled`, `loginCommandName` | add a gateway or subscription route beside whatever else the profile serves |
98
+ | `catalog` v1 | own the entire directory: routes, ids, aliases, metadata, and the default selection |
99
+
100
+ The generations differ where a declaration cannot resolve. An additive
101
+ `extraModels` entry with a bad `template` lands in that route's model
102
+ diagnostics while the route keeps serving; an additive `models`/`codexModels`
103
+ whitelist naming an id nothing provides refuses the whole route. A **catalog**
104
+ rejects the whole candidate instead: there is no per-model diagnostic path.
105
+
106
+ Catalog route fields (an unknown field rejects the candidate):
107
+
108
+ ```yaml
109
+ catalog:
110
+ version: 1 # exactly 1
111
+ default: # null only when nothing is served
112
+ { provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
113
+ providers:
114
+ - id: opencode-go-session # the id every selection and alias check uses
115
+ name: OpenCode Go (session)
116
+ source: opencode-go # installed pi-ai provider id; its data is the base
117
+ api: openai-completions # own endpoint only: required without source, refused beside it
118
+ baseURL: https://api.x.ai/v1 # required without source; http(s), no user/pass/fragment
119
+ headers: { X-Route: tenant-a } # static headers; secrets stay in a credential reference
120
+ transport: sse # openai-codex only: sse|websocket|websocket-cached|auto
121
+ fallbackSessionId: dsh-provider-extra # opencode-go only: session header for sessionless calls
122
+ auth: { apiKeyRef: OPENCODE_GO_API_KEY } # or { credentialProvider: openai-codex }; absent = keyless route
123
+ models:
124
+ - id: deepseek-flash
125
+ name: DeepSeek V4.1 Flash
126
+ aliases: [deepseek-v4.1-flash] # extra selector inputs, unique in the route, never extra rows
127
+ template: deepseek-v4-flash # installed sibling of the SAME source to clone
128
+ defaultMaxTokens: 384000 # request default; must be <= resolved maxTokens
129
+ metadata:
130
+ api: openai-completions
131
+ reasoning: true
132
+ input: [text, image]
133
+ cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0 }
134
+ # a threshold-priced vendor adds its tier rows beside those four rates:
135
+ # cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0,
136
+ # tiers: [{ inputTokensAbove: 272000, input: 0.44, output: 1.32,
137
+ # cacheRead: 0.014, cacheWrite: 0 }] }
138
+ contextWindow: 1000000
139
+ maxTokens: 384000 # capacity, never a request cap
140
+ thinkingLevelMap: { off: null, minimal: null, low: low, medium: null, high: high, xhigh: null, max: max }
141
+ headers: { X-Model: flash }
142
+ compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, supportsStrictMode: true, thinkingFormat: qwen }
143
+ - id: zai-catalog
144
+ name: Z.AI Coding Plan
145
+ source: zai
146
+ auth: { apiKeyRef: ZAI_API_KEY }
147
+ filter: { include: ['glm-*'], exclude: ['glm-4*'] } # instead of models; '*' is the only wildcard
148
+ ```
149
+
150
+ `filter` needs an installed `source`: an absent `include` starts from every
151
+ installed model, matches keep installed order and every installed fact (id, name,
152
+ cost, capacity, protocol, endpoint), and the route serves them minus `exclude`.
153
+ A matched model that publishes no endpoint needs a route `baseURL`.
154
+
155
+ A model with no resolvable template must declare all of `api` (unless the route
156
+ declares `api`), `reasoning`, `input`, `cost`, `contextWindow`, and
157
+ `maxTokens`. `metadata.api` cannot change a template-backed protocol, must equal
158
+ a route's declared `api`, and must be an api the `source` describes.
159
+
160
+ `compat` accepts only the flags the model's protocol honors: completions takes the
161
+ `supports*`/`requires*` booleans plus `maxTokensField`, `thinkingFormat`,
162
+ `thinkingTokenBudgetField`, `cacheControlFormat`, `deferredToolsMode`, and
163
+ `sessionAffinityFormat`; responses takes its own flag set plus
164
+ `sessionAffinityFormat`; anthropic-messages takes its flag set and no enums.
165
+ Template-language and gateway-routing keys (`chatTemplateKwargs`,
166
+ `chatTemplateArgs`, `openRouterRouting`, `vercelGatewayRouting`,
167
+ `vllmPriority`, `allowedFallbackModels`) are refused. The plugin's own
168
+ `src/catalog-routes.ts` lists the accepted names per protocol.
169
+
170
+ Behaviour:
171
+
172
+ | Case | Result |
173
+ | --- | --- |
174
+ | `template` names a sibling the source ships | clone its wire behavior and metadata |
175
+ | `template` names nothing, or the route has no source | candidate rejected: `unknown template <id>` |
176
+ | unknown id, no template, incomplete metadata | candidate rejected: `unknown model requires complete metadata: missing <field>` |
177
+ | both `models` and `filter`, or neither | candidate rejected: `declare exactly one of models or filter` |
178
+ | duplicate provider id, model id, or alias | candidate rejected |
179
+ | `defaultMaxTokens` above the resolved `maxTokens` | candidate rejected: `exceeds model maxTokens` |
180
+ | `default` names a model outside the served selection | candidate rejected; an alias is canonicalized to its wire id |
181
+ | `reasoningEffort` unsupported by the model | candidate rejected |
182
+ | nothing served (empty providers, or `models: []`) | `default` must be `null`; `currentSelection()` throws `NO_DEFAULT_MODEL` |
183
+
184
+ ### Default persistence
185
+
186
+ `catalog.default` reaches a new session as provider and model only: the pinned
187
+ host ignores its `reasoningEffort`, so effort is chosen in the surface.
188
+ `saveSelection(next)` validates first, then writes through the host's
189
+ `configEditor.edit(entry, updater)` so only `catalog.default` changes and
190
+ membership is re-checked inside the editor's lock. A host with no addressable
191
+ entry or no such editor rejects with `CONFIG_PERSISTENCE_UNAVAILABLE` - the
192
+ released `0.1.5-rc.2` host does - and nothing falls back to legacy
193
+ `agent-default-model` settings. Edit the profile patch there.
194
+
195
+ `compileCatalog(config)` and `buildCatalogProfile(provider)` are public: the
196
+ first returns a detached frozen snapshot or `undefined` for absence, the second
197
+ validates one route declaration on its own. Both are what the dump script and a
198
+ candidate check should read against.
199
+
200
+ ## Where the base data comes from
201
+
202
+ ```sh
203
+ # the harness install owns the pi-ai catalog the profile resolves against
204
+ DSH=$(readlink -f "$(command -v dsh)")
205
+ PI_AI="${DSH%/lib/bin.js}/node_modules/@earendil-works/pi-ai"
206
+ ls "$PI_AI/dist/providers/data" # one JSON per provider source, keyed by api
207
+ ```
208
+
209
+ `source` must be an id the installed pi-ai build ships (`builtinProviders()`);
210
+ `ls` the data directory for the list this harness answers. A JSON entry is the
211
+ whole truth for a `source`: `id`, `name`, `cost`, `contextWindow`,
212
+ `maxTokens`, `input`, `reasoning`, `thinkingLevelMap`, `compat`, `baseUrl`.
213
+ A model declared without `template` and without `metadata` is served exactly as
214
+ this file describes it, so an id the vendor renamed or repriced shows stale
215
+ numbers until the catalog declares them. An entry that carries its own complete
216
+ `metadata` needs no installed entry at all, which is how a route the harness does
217
+ not describe is wired.
218
+
219
+ ## Credentials
220
+
221
+ | Field | Meaning |
222
+ | --- | --- |
223
+ | `apiKeyEnv` | additive config and `llm-pi-ai`: read the key from this environment variable |
224
+ | `apiKeyRef` | catalog: a host credential reference; without a credential service the route reads that environment variable. Refused on an `openai-codex` source, which needs OAuth |
225
+ | `credentialProvider` | catalog: use a login flow's stored grant. Needs an installed `source` and must equal that source id |
226
+ | no `auth` | catalog: the route carries no key of its own, as a local endpoint does |
227
+
228
+ Keys never belong in a patch, a settings file, or a repository. Put launch-time
229
+ values in `$DSH_HOME/.env` or the environment. A provider row whose credential is
230
+ missing fails its route only; other routes still serve.
231
+
232
+ ## Verify
233
+
234
+ ```sh
235
+ dsh --profile tui list-models # provider/model<TAB>display name, one per reachable route
236
+ ~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs \
237
+ [--home "$DSH_HOME"] [--profile tui] [--data <pi-ai data dir>] [--json]
238
+ ```
239
+
240
+ The dump reads the catalog row written as a single JSON flow map
241
+ (`- {"id":"dsh-provider-extra","config":{"catalog":…}}`), which is the shape
242
+ `pnpm catalog:setup` appends and the shape a hand-written patch keeps when one
243
+ line must stay machine-readable. A catalog spread over block YAML is invisible to
244
+ it; `list-models` still reads the composed result either way. `--data` points the
245
+ dump at a provider-data directory instead of the installed harness, which is how
246
+ a template clone or a `filter` route is read on a machine with no `dsh`. The dump
247
+ also reports what the resolver would refuse, and exits 1, because a catalog that
248
+ cannot compose serves nothing: incomplete metadata for an unknown model, a
249
+ `defaultMaxTokens` above capacity, both or neither of `models`/`filter`, a
250
+ default the served selection does not contain, an effort the selected model does
251
+ not offer, and a default that is null while models are served.
252
+
253
+ To validate a candidate on a disposable home, provider-extra's clone-only setup
254
+ composes it and disables the competing rows itself:
255
+
256
+ ```sh
257
+ cd <dsh-provider-extra checkout> && pnpm catalog:setup \
258
+ --helper <plugin checkout>/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh \
259
+ --source-home ~/.dsh --home /tmp/dsh-catalog-check --profile tui \
260
+ --catalog /tmp/catalog-config.json [--allow-row ROW_ID] [--dsh /abs/path/to/dsh/lib/bin.js]
261
+ ```
262
+
263
+ `--catalog` is a private JSON file holding only `{"catalog": {...}}`. It prints
264
+ `composition-verified` on success, which is composition only: not runtime
265
+ ownership, not authentication. `--allow-row` asserts a provider-looking row
266
+ registers no provider and no default owner, because setup cannot see row
267
+ provenance and refuses rather than guesses.
268
+
269
+ `list-models` exits zero only after printing a line, so a deployment that
270
+ advertises nothing cannot pass for one that was enumerated.
271
+
272
+ For every model you touched, confirm against the vendor's own page:
273
+
274
+ 1. price per 1M tokens for input, output, cached input, cache write;
275
+ 2. context window and maximum output tokens (a "272K" beside "above 272K input
276
+ is priced 2x" is a billing threshold, not the window);
277
+ 3. the accepted reasoning effort values, and how the vendor spells "no reasoning"
278
+ (OpenAI: `none`; codex: omit the parameter);
279
+ 4. whether the route bills per token at all - a metered API wants its published
280
+ rates, and a credit or token plan writes `0`, which reads as unknown pricing
281
+ rather than free inference;
282
+ 5. that an `xhigh` or `max` mapping exists when the vendor documents one;
283
+ 6. that the same model id served on two routes carries the same facts: each route
284
+ inherits its own `source` entry, so one declared `contextWindow` and one
285
+ inherited is two different ctx gauges for one model.
286
+
287
+ A model with no vendor page (stealth releases, previews) is documented only by the
288
+ routing catalog: its pi.dev model page and `models.dev/api.json`.
289
+
290
+ Then run the profile against a cloned home before the real one - see the
291
+ `dsh-tui-dogfood` skill - and `pnpm run build` any linked bundle first: a linked
292
+ profile loads `lib/`, not `src/`.
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Print the model directory a dsh profile resolves, with the provenance of
4
+ * every field.
5
+ *
6
+ * The surface reads prices, context windows, and reasoning levels from the
7
+ * composition, not from the vendor's page, so an inherited or stale value is
8
+ * invisible in the UI. This reads the same two inputs the resolver does - the
9
+ * owned catalog in the profile patch, and the installed pi-ai provider data -
10
+ * and names where each number came from: "installed" for the source's own
11
+ * entry, "template:<id>" for a clone, "route" for the protocol a sourceless
12
+ * route declares once for every model it serves, "metadata" for what the
13
+ * catalog declares.
14
+ *
15
+ * An entry the catalog spells out completely needs no installed entry at all,
16
+ * so a missing harness install must not hide it. A `filter` route is the
17
+ * exception: its membership is the installed catalog's, so it cannot be read
18
+ * without that install. A declaration the plugin's resolver would refuse is
19
+ * reported as a refusal here too, because a catalog that cannot compose serves
20
+ * nothing.
21
+ *
22
+ * Usage: dump-model-catalog.mjs [--home <dsh home>] [--profile <name>]
23
+ * [--data <pi-ai data dir>] [--json]
24
+ *
25
+ * `--data` names the installed provider-data directory to inherit from, which
26
+ * is how a reading is checked where no harness is installed - a `filter` route
27
+ * and every template clone need one.
28
+ */
29
+ import { execFileSync } from 'node:child_process'
30
+ import { readFileSync, realpathSync } from 'node:fs'
31
+ import { homedir } from 'node:os'
32
+ import { join } from 'node:path'
33
+ import { fileURLToPath } from 'node:url'
34
+
35
+ const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
36
+ /** Fields a catalog entry may inherit, and the layer each value came from. */
37
+ const INHERITED_FIELDS = ['contextWindow', 'maxTokens', 'cost', 'input', 'reasoning', 'thinkingLevelMap', 'api']
38
+ /** What a model with no installed sibling and no template must state itself. */
39
+ const REQUIRED_METADATA = ['api', 'reasoning', 'input', 'cost', 'contextWindow', 'maxTokens']
40
+ const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite']
41
+ const INSTALLED = 'installed'
42
+ const METADATA = 'metadata'
43
+ const ROUTE = 'route'
44
+ const ABSENT = 'none'
45
+ /** The only wildcard a `filter` pattern carries; every other character is itself. */
46
+ const WILDCARD = '*'
47
+
48
+ /** The levels pi-ai offers: a null mapping removes one, and xhigh/max need one. */
49
+ export function supportedLevels(levelMap) {
50
+ return LEVELS.filter((level) => {
51
+ const mapped = levelMap?.[level]
52
+ if (mapped === null) return false
53
+ if (level === 'xhigh' || level === 'max') return mapped !== undefined
54
+ return true
55
+ })
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const options = { home: process.env.DSH_HOME ?? join(homedir(), '.dsh'), profile: 'tui', data: undefined, json: false }
60
+ for (let index = 0; index < argv.length; index += 1) {
61
+ const flag = argv[index]
62
+ if (flag === '--json') options.json = true
63
+ else if (flag === '--home') options.home = argv[++index]
64
+ else if (flag === '--profile') options.profile = argv[++index]
65
+ else if (flag === '--data') options.data = argv[++index]
66
+ else throw new Error('unknown argument ' + flag)
67
+ }
68
+ return options
69
+ }
70
+
71
+ /** The pi-ai data directory of the harness install the launcher belongs to. */
72
+ function dataDirectory() {
73
+ try {
74
+ const bin = execFileSync('sh', ['-c', 'command -v dsh'], { encoding: 'utf8' }).trim()
75
+ const root = join(realpathSync(bin), '..', '..')
76
+ return join(root, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data')
77
+ } catch {
78
+ // No harness install is a supported reading: declared entries still resolve.
79
+ return undefined
80
+ }
81
+ }
82
+
83
+ /** Every JSON patch entry that configures the owned catalog, last one winning. */
84
+ export function catalogConfig(patch) {
85
+ const lines = readFileSync(patch, 'utf8').split('\n')
86
+ let catalog
87
+ for (const line of lines) {
88
+ if (!line.startsWith('- {') || !line.includes('"catalog"')) continue
89
+ const entry = JSON.parse(line.slice(2))
90
+ if (entry.config?.catalog !== undefined) catalog = entry.config.catalog
91
+ }
92
+ return catalog
93
+ }
94
+
95
+ export function providerData(directory, source) {
96
+ if (directory === undefined || source === undefined) return undefined
97
+ try {
98
+ return JSON.parse(readFileSync(join(directory, source + '.json'), 'utf8'))
99
+ } catch {
100
+ return undefined
101
+ }
102
+ }
103
+
104
+ /** Installed models in the order the source publishes them, one api map at a time. */
105
+ function installedModels(data) {
106
+ if (data === undefined) return []
107
+ return Object.entries(data).flatMap(([api, models]) => Object.entries(models)
108
+ .map(([id, model]) => ({ ...model, id, api: model.api ?? api })))
109
+ }
110
+
111
+ /** Mirror the resolver's pattern rule so a filter reads the same here and there. */
112
+ function patternToRegExp(pattern) {
113
+ const escaped = pattern.split(WILDCARD).map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
114
+ return new RegExp('^' + escaped.join('.*') + '$')
115
+ }
116
+
117
+ /** A filter keeps installed facts and order, then drops what exclude names. */
118
+ export function expandFilter(data, filter) {
119
+ const include = filter?.include === undefined ? undefined : filter.include.map(patternToRegExp)
120
+ const exclude = filter?.exclude === undefined ? [] : filter.exclude.map(patternToRegExp)
121
+ return installedModels(data)
122
+ .filter(model => (include === undefined || include.some(pattern => pattern.test(model.id)))
123
+ && !exclude.some(pattern => pattern.test(model.id)))
124
+ }
125
+
126
+ /** Look one id up across a provider's api maps; an api in the spec disambiguates. */
127
+ function lookup(data, id, api) {
128
+ if (data === undefined) return undefined
129
+ for (const [key, models] of Object.entries(data)) {
130
+ if ((api === undefined || key === api) && models[id] !== undefined) return { ...models[id], api: models[id].api ?? key }
131
+ }
132
+ return undefined
133
+ }
134
+
135
+ function costOf(cost) {
136
+ if (cost === undefined) return undefined
137
+ return '$' + COST_FIELDS.map(field => cost[field]).join('/') + ' per 1M'
138
+ }
139
+
140
+ /** Resolve one catalog entry against the installed data it may inherit from. */
141
+ export function resolve(spec, data, routeApi) {
142
+ const metadata = spec.metadata ?? {}
143
+ const declared = Object.keys(metadata).length > 0
144
+ const cloneOf = spec.template ?? spec.id
145
+ const base = lookup(data, cloneOf, metadata.api)
146
+ if (base === undefined && !declared) {
147
+ return { error: 'no source entry for ' + cloneOf + '; declare a template or metadata' }
148
+ }
149
+ const merged = { ...base, ...metadata, id: spec.id, name: spec.name ?? base?.name ?? spec.id }
150
+ // A route that names no source states its protocol once, for every model.
151
+ if (merged.api === undefined && routeApi !== undefined) merged.api = routeApi
152
+ // Capacity and the request default are different numbers, and the resolver
153
+ // caps one by the other: folding them would hide the mistake it refuses.
154
+ const capacity = metadata.maxTokens ?? base?.maxTokens
155
+ const missing = base === undefined
156
+ ? REQUIRED_METADATA.filter(field => field === 'api' ? merged.api === undefined : metadata[field] === undefined)
157
+ : []
158
+ if (missing.length > 0) return { error: 'unknown model requires complete metadata: missing ' + missing.join(', ') }
159
+ if (spec.defaultMaxTokens !== undefined && capacity !== undefined && spec.defaultMaxTokens > capacity) {
160
+ return { error: 'defaultMaxTokens ' + spec.defaultMaxTokens + ' exceeds model maxTokens ' + capacity }
161
+ }
162
+ const origins = {}
163
+ for (const field of INHERITED_FIELDS) {
164
+ if (metadata[field] !== undefined) origins[field] = METADATA
165
+ else if (field === 'api' && base === undefined && routeApi !== undefined) origins[field] = ROUTE
166
+ else if (base === undefined) origins[field] = ABSENT
167
+ else origins[field] = spec.template === undefined ? INSTALLED : 'template:' + spec.template
168
+ }
169
+ return {
170
+ model: { ...merged, maxTokens: capacity },
171
+ origins,
172
+ levels: supportedLevels(merged.thinkingLevelMap),
173
+ ...(spec.defaultMaxTokens === undefined ? {} : { requestMax: spec.defaultMaxTokens }),
174
+ ...(spec.aliases === undefined ? {} : { aliases: spec.aliases.join(',') }),
175
+ ...(metadata.cost?.tiers === undefined ? {} : {
176
+ tiers: metadata.cost.tiers.map(tier => 'above ' + tier.inputTokensAbove + ': ' + costOf(tier)).join('; '),
177
+ }),
178
+ }
179
+ }
180
+
181
+ function rowsOf(catalog, directory) {
182
+ const rows = []
183
+ for (const provider of catalog.providers ?? []) {
184
+ const data = providerData(directory, provider.source)
185
+ const hasModels = 'models' in provider
186
+ const hasFilter = 'filter' in provider
187
+ // The resolver refuses both and neither; saying so here beats serving half.
188
+ if (hasModels === hasFilter) {
189
+ rows.push({ route: provider.id, error: 'declare exactly one of models or filter' })
190
+ continue
191
+ }
192
+ if (hasFilter) {
193
+ if (data === undefined) {
194
+ rows.push({
195
+ route: provider.id,
196
+ error: 'filter expands the installed catalog of source "' + provider.source + '"; no harness install found',
197
+ })
198
+ continue
199
+ }
200
+ for (const model of expandFilter(data, provider.filter)) {
201
+ rows.push({
202
+ route: provider.id + '/' + model.id,
203
+ name: model.name ?? model.id,
204
+ api: model.api,
205
+ contextWindow: model.contextWindow,
206
+ maxTokens: model.maxTokens,
207
+ cost: costOf(model.cost),
208
+ levels: supportedLevels(model.thinkingLevelMap).join(','),
209
+ origins: 'filter:' + (provider.filter.include ?? [WILDCARD]).join(',') + ' all installed',
210
+ })
211
+ }
212
+ continue
213
+ }
214
+ for (const spec of provider.models ?? []) {
215
+ const result = resolve(spec, data, provider.api)
216
+ const route = provider.id + '/' + spec.id
217
+ if (result.error !== undefined) {
218
+ rows.push({ route, error: result.error })
219
+ continue
220
+ }
221
+ rows.push({
222
+ route,
223
+ name: result.model.name,
224
+ api: result.model.api,
225
+ contextWindow: result.model.contextWindow,
226
+ maxTokens: result.model.maxTokens,
227
+ cost: costOf(result.model.cost),
228
+ levels: result.levels.join(','),
229
+ ...(result.requestMax === undefined ? {} : { requestMax: result.requestMax }),
230
+ ...(result.aliases === undefined ? {} : { aliases: result.aliases }),
231
+ ...(result.tiers === undefined ? {} : { tiers: result.tiers }),
232
+ origins: INHERITED_FIELDS
233
+ .map(field => [field, result.origins[field]])
234
+ .filter(([, origin]) => origin !== INSTALLED)
235
+ .map(([field, origin]) => field + '=' + origin)
236
+ .join(' ') || 'all installed',
237
+ })
238
+ }
239
+ }
240
+ return rows
241
+ }
242
+
243
+ /**
244
+ * The default is part of the catalog, not a comment beside it: the resolver
245
+ * refuses a selection nothing serves, so a reading that skipped it would call a
246
+ * broken catalog readable.
247
+ */
248
+ export function checkDefault(catalog, rows) {
249
+ const served = rows.filter(row => row.error === undefined)
250
+ const selection = catalog.default ?? null
251
+ if (served.length === 0) {
252
+ return selection === null ? [] : [{ route: 'default', error: 'an empty catalog requires default null' }]
253
+ }
254
+ if (selection === null || typeof selection !== 'object') {
255
+ return [{ route: 'default', error: 'a served catalog requires a default selection' }]
256
+ }
257
+ const alias = new Map()
258
+ for (const provider of catalog.providers ?? []) {
259
+ for (const model of provider.models ?? []) {
260
+ for (const name of [model.id, ...(model.aliases ?? [])]) alias.set(provider.id + '/' + name, model.id)
261
+ }
262
+ }
263
+ const route = selection.provider + '/' + selection.model
264
+ const canonical = alias.get(route)
265
+ const row = served.find(candidate => candidate.route === selection.provider + '/' + (canonical ?? selection.model))
266
+ if (row === undefined) {
267
+ return [{ route: 'default', error: 'provider/model is outside the managed selection: ' + route }]
268
+ }
269
+ const effort = selection.reasoningEffort
270
+ if (effort !== undefined && !String(row.levels).split(',').includes(effort)) {
271
+ return [{ route: 'default', error: 'effort is not supported by the selected model: ' + effort }]
272
+ }
273
+ return []
274
+ }
275
+
276
+ function main() {
277
+ const options = parseArgs(process.argv.slice(2))
278
+ const patch = join(options.home, 'profiles', options.profile, 'cordis.patch.yml')
279
+ const catalog = catalogConfig(patch)
280
+ if (catalog === undefined) {
281
+ process.stderr.write('no owned catalog in ' + patch + '\n'
282
+ + 'this profile wires models another way; see references/model-wiring.md\n')
283
+ process.exit(1)
284
+ }
285
+ const directory = options.data ?? dataDirectory()
286
+ if (directory === undefined) {
287
+ process.stderr.write('no harness install found; entries that inherit from it cannot be checked\n')
288
+ }
289
+ const served = rowsOf(catalog, directory)
290
+ const rows = [...served, ...checkDefault(catalog, served)]
291
+ const unresolved = rows.filter(row => row.error !== undefined).length
292
+ if (options.json) {
293
+ process.stdout.write(JSON.stringify({ default: catalog.default ?? null, rows }, null, 2) + '\n')
294
+ } else {
295
+ for (const row of rows) {
296
+ if (row.error !== undefined) {
297
+ process.stdout.write(row.route + '\n UNRESOLVED: ' + row.error + '\n')
298
+ continue
299
+ }
300
+ process.stdout.write(row.route + '\n ' + row.name + ' | ' + row.api + ' | ctx ' + row.contextWindow
301
+ + ' | max ' + row.maxTokens
302
+ + (row.requestMax === undefined ? '' : ' | req ' + row.requestMax)
303
+ + ' | ' + row.cost
304
+ + (row.tiers === undefined ? '' : ' | tiers ' + row.tiers)
305
+ + ' | efforts ' + row.levels
306
+ + (row.aliases === undefined ? '' : ' | aliases ' + row.aliases) + '\n inherited: ' + row.origins + '\n')
307
+ }
308
+ process.stdout.write('default: ' + JSON.stringify(catalog.default ?? null) + '\n')
309
+ }
310
+ process.exit(unresolved === 0 ? 0 : 1)
311
+ }
312
+
313
+ // Importing the resolver must not run the CLI: the spec reads it that way.
314
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main()