@sagmans/dsh-tui 0.5.2 → 0.7.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 (285) 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 +121 -0
  6. package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +162 -0
  7. package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +178 -0
  8. package/README.md +131 -17
  9. package/lib/agent/host.d.ts +11 -0
  10. package/lib/agent/host.d.ts.map +1 -1
  11. package/lib/agent/host.js +16 -2
  12. package/lib/agent/host.js.map +1 -1
  13. package/lib/agent/present.d.ts.map +1 -1
  14. package/lib/agent/present.js +2 -1
  15. package/lib/agent/present.js.map +1 -1
  16. package/lib/agent/undo.d.ts +52 -0
  17. package/lib/agent/undo.d.ts.map +1 -0
  18. package/lib/agent/undo.js +106 -0
  19. package/lib/agent/undo.js.map +1 -0
  20. package/lib/cards/composition.d.ts +68 -0
  21. package/lib/cards/composition.d.ts.map +1 -0
  22. package/lib/cards/composition.js +117 -0
  23. package/lib/cards/composition.js.map +1 -0
  24. package/lib/cards/presenter.d.ts +23 -0
  25. package/lib/cards/presenter.d.ts.map +1 -0
  26. package/lib/cards/presenter.js +394 -0
  27. package/lib/cards/presenter.js.map +1 -0
  28. package/lib/cards/preview.d.ts +42 -0
  29. package/lib/cards/preview.d.ts.map +1 -0
  30. package/lib/cards/preview.js +51 -0
  31. package/lib/cards/preview.js.map +1 -0
  32. package/lib/cards.d.ts +42 -95
  33. package/lib/cards.d.ts.map +1 -1
  34. package/lib/cards.js +4 -526
  35. package/lib/cards.js.map +1 -1
  36. package/lib/compat/section.d.ts +29 -0
  37. package/lib/compat/section.d.ts.map +1 -0
  38. package/lib/compat/section.js +82 -0
  39. package/lib/compat/section.js.map +1 -0
  40. package/lib/config.d.ts +27 -2
  41. package/lib/config.d.ts.map +1 -1
  42. package/lib/config.js +61 -1
  43. package/lib/config.js.map +1 -1
  44. package/lib/contracts.d.ts +28 -0
  45. package/lib/contracts.d.ts.map +1 -1
  46. package/lib/fold-cursor.d.ts +8 -0
  47. package/lib/fold-cursor.d.ts.map +1 -1
  48. package/lib/fold-cursor.js +18 -0
  49. package/lib/fold-cursor.js.map +1 -1
  50. package/lib/gates/question-card.d.ts +77 -0
  51. package/lib/gates/question-card.d.ts.map +1 -0
  52. package/lib/gates/question-card.js +113 -0
  53. package/lib/gates/question-card.js.map +1 -0
  54. package/lib/gates/questions.d.ts +127 -0
  55. package/lib/gates/questions.d.ts.map +1 -0
  56. package/lib/gates/questions.js +418 -0
  57. package/lib/gates/questions.js.map +1 -0
  58. package/lib/gates.d.ts +1 -113
  59. package/lib/gates.d.ts.map +1 -1
  60. package/lib/gates.js +1 -484
  61. package/lib/gates.js.map +1 -1
  62. package/lib/herdr/reporter.d.ts +9 -4
  63. package/lib/herdr/reporter.d.ts.map +1 -1
  64. package/lib/herdr/reporter.js +5 -9
  65. package/lib/herdr/reporter.js.map +1 -1
  66. package/lib/herdr/state.d.ts +35 -4
  67. package/lib/herdr/state.d.ts.map +1 -1
  68. package/lib/herdr/state.js +37 -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 +343 -2267
  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 -621
  81. package/lib/input/actions.js.map +1 -1
  82. package/lib/input/completion.d.ts +11 -1
  83. package/lib/input/completion.d.ts.map +1 -1
  84. package/lib/input/completion.js +17 -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 +5 -1
  106. package/lib/input/keymap.js.map +1 -1
  107. package/lib/input/submission.d.ts +11 -1
  108. package/lib/input/submission.d.ts.map +1 -1
  109. package/lib/input/submission.js +10 -1
  110. package/lib/input/submission.js.map +1 -1
  111. package/lib/input/workspace-files.d.ts +10 -0
  112. package/lib/input/workspace-files.d.ts.map +1 -0
  113. package/lib/input/workspace-files.js +359 -0
  114. package/lib/input/workspace-files.js.map +1 -0
  115. package/lib/install-skills.d.ts +20 -0
  116. package/lib/install-skills.d.ts.map +1 -0
  117. package/lib/install-skills.js +198 -0
  118. package/lib/install-skills.js.map +1 -0
  119. package/lib/keys-command.d.ts +2 -1
  120. package/lib/keys-command.d.ts.map +1 -1
  121. package/lib/keys-command.js +2 -1
  122. package/lib/keys-command.js.map +1 -1
  123. package/lib/model-list.d.ts +37 -0
  124. package/lib/model-list.d.ts.map +1 -0
  125. package/lib/model-list.js +60 -0
  126. package/lib/model-list.js.map +1 -0
  127. package/lib/startup.d.ts +8 -0
  128. package/lib/startup.d.ts.map +1 -1
  129. package/lib/startup.js +120 -24
  130. package/lib/startup.js.map +1 -1
  131. package/lib/subagents.d.ts +7 -3
  132. package/lib/subagents.d.ts.map +1 -1
  133. package/lib/subagents.js +28 -4
  134. package/lib/subagents.js.map +1 -1
  135. package/lib/surface/appearance-preferences.d.ts +26 -0
  136. package/lib/surface/appearance-preferences.d.ts.map +1 -0
  137. package/lib/surface/appearance-preferences.js +139 -0
  138. package/lib/surface/appearance-preferences.js.map +1 -0
  139. package/lib/surface/appearance.d.ts +87 -0
  140. package/lib/surface/appearance.d.ts.map +1 -0
  141. package/lib/surface/appearance.js +353 -0
  142. package/lib/surface/appearance.js.map +1 -0
  143. package/lib/surface/background-work.d.ts +49 -0
  144. package/lib/surface/background-work.d.ts.map +1 -0
  145. package/lib/surface/background-work.js +146 -0
  146. package/lib/surface/background-work.js.map +1 -0
  147. package/lib/surface/commands.d.ts +80 -0
  148. package/lib/surface/commands.d.ts.map +1 -0
  149. package/lib/surface/commands.js +364 -0
  150. package/lib/surface/commands.js.map +1 -0
  151. package/lib/surface/modal-input.d.ts +58 -0
  152. package/lib/surface/modal-input.d.ts.map +1 -0
  153. package/lib/surface/modal-input.js +237 -0
  154. package/lib/surface/modal-input.js.map +1 -0
  155. package/lib/surface/model-choice.d.ts +54 -0
  156. package/lib/surface/model-choice.d.ts.map +1 -0
  157. package/lib/surface/model-choice.js +325 -0
  158. package/lib/surface/model-choice.js.map +1 -0
  159. package/lib/surface/preset-choice.d.ts +78 -0
  160. package/lib/surface/preset-choice.d.ts.map +1 -0
  161. package/lib/surface/preset-choice.js +163 -0
  162. package/lib/surface/preset-choice.js.map +1 -0
  163. package/lib/surface/prompt-input.d.ts +97 -0
  164. package/lib/surface/prompt-input.d.ts.map +1 -0
  165. package/lib/surface/prompt-input.js +215 -0
  166. package/lib/surface/prompt-input.js.map +1 -0
  167. package/lib/surface/prompt-memory.d.ts +76 -0
  168. package/lib/surface/prompt-memory.d.ts.map +1 -0
  169. package/lib/surface/prompt-memory.js +167 -0
  170. package/lib/surface/prompt-memory.js.map +1 -0
  171. package/lib/surface/session-lifecycle.d.ts +120 -0
  172. package/lib/surface/session-lifecycle.d.ts.map +1 -0
  173. package/lib/surface/session-lifecycle.js +321 -0
  174. package/lib/surface/session-lifecycle.js.map +1 -0
  175. package/lib/surface/session-picker.d.ts +39 -0
  176. package/lib/surface/session-picker.d.ts.map +1 -0
  177. package/lib/surface/session-picker.js +107 -0
  178. package/lib/surface/session-picker.js.map +1 -0
  179. package/lib/surface/session-view.d.ts +86 -0
  180. package/lib/surface/session-view.d.ts.map +1 -0
  181. package/lib/surface/session-view.js +215 -0
  182. package/lib/surface/session-view.js.map +1 -0
  183. package/lib/surface/staged-turns.d.ts +81 -0
  184. package/lib/surface/staged-turns.d.ts.map +1 -0
  185. package/lib/surface/staged-turns.js +206 -0
  186. package/lib/surface/staged-turns.js.map +1 -0
  187. package/lib/surface/terminal-lifecycle.d.ts +76 -0
  188. package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
  189. package/lib/surface/terminal-lifecycle.js +222 -0
  190. package/lib/surface/terminal-lifecycle.js.map +1 -0
  191. package/lib/terminal/warning-screen.d.ts +8 -1
  192. package/lib/terminal/warning-screen.d.ts.map +1 -1
  193. package/lib/terminal/warning-screen.js +9 -0
  194. package/lib/terminal/warning-screen.js.map +1 -1
  195. package/lib/terminal-text/scan.d.ts +44 -0
  196. package/lib/terminal-text/scan.d.ts.map +1 -0
  197. package/lib/terminal-text/scan.js +195 -0
  198. package/lib/terminal-text/scan.js.map +1 -0
  199. package/lib/terminal-text/sgr.d.ts +32 -0
  200. package/lib/terminal-text/sgr.d.ts.map +1 -0
  201. package/lib/terminal-text/sgr.js +203 -0
  202. package/lib/terminal-text/sgr.js.map +1 -0
  203. package/lib/terminal-text.d.ts +1 -1
  204. package/lib/terminal-text.d.ts.map +1 -1
  205. package/lib/terminal-text.js +2 -379
  206. package/lib/terminal-text.js.map +1 -1
  207. package/lib/theme-command.js +2 -1
  208. package/lib/theme-command.js.map +1 -1
  209. package/lib/theme-defaults.d.ts +64 -0
  210. package/lib/theme-defaults.d.ts.map +1 -0
  211. package/lib/theme-defaults.js +248 -0
  212. package/lib/theme-defaults.js.map +1 -0
  213. package/lib/theme-resolver.d.ts +36 -0
  214. package/lib/theme-resolver.d.ts.map +1 -0
  215. package/lib/theme-resolver.js +112 -0
  216. package/lib/theme-resolver.js.map +1 -0
  217. package/lib/theme-schema.d.ts +6 -6
  218. package/lib/theme-schema.d.ts.map +1 -1
  219. package/lib/theme-schema.js +3 -2
  220. package/lib/theme-schema.js.map +1 -1
  221. package/lib/theme-settings.d.ts +13 -15
  222. package/lib/theme-settings.d.ts.map +1 -1
  223. package/lib/theme-settings.js +27 -19
  224. package/lib/theme-settings.js.map +1 -1
  225. package/lib/theme-tokens.d.ts +1 -90
  226. package/lib/theme-tokens.d.ts.map +1 -1
  227. package/lib/theme-tokens.js +11 -341
  228. package/lib/theme-tokens.js.map +1 -1
  229. package/lib/theme.d.ts +1 -1
  230. package/lib/theme.d.ts.map +1 -1
  231. package/lib/theme.js +2 -1
  232. package/lib/theme.js.map +1 -1
  233. package/lib/tool-display.d.ts.map +1 -1
  234. package/lib/tool-display.js +2 -1
  235. package/lib/tool-display.js.map +1 -1
  236. package/lib/transcript/message-content.d.ts +21 -0
  237. package/lib/transcript/message-content.d.ts.map +1 -0
  238. package/lib/transcript/message-content.js +61 -0
  239. package/lib/transcript/message-content.js.map +1 -0
  240. package/lib/transcript/tool-calls.d.ts +106 -0
  241. package/lib/transcript/tool-calls.d.ts.map +1 -0
  242. package/lib/transcript/tool-calls.js +254 -0
  243. package/lib/transcript/tool-calls.js.map +1 -0
  244. package/lib/transcript.d.ts +38 -43
  245. package/lib/transcript.d.ts.map +1 -1
  246. package/lib/transcript.js +58 -245
  247. package/lib/transcript.js.map +1 -1
  248. package/lib/ui/copy.d.ts +17 -0
  249. package/lib/ui/copy.d.ts.map +1 -0
  250. package/lib/ui/copy.js +104 -0
  251. package/lib/ui/copy.js.map +1 -0
  252. package/lib/ui/dock.d.ts +35 -3
  253. package/lib/ui/dock.d.ts.map +1 -1
  254. package/lib/ui/dock.js +124 -23
  255. package/lib/ui/dock.js.map +1 -1
  256. package/lib/ui/editor.d.ts.map +1 -1
  257. package/lib/ui/editor.js +2 -1
  258. package/lib/ui/editor.js.map +1 -1
  259. package/lib/ui/frame.d.ts +31 -0
  260. package/lib/ui/frame.d.ts.map +1 -1
  261. package/lib/ui/frame.js +27 -4
  262. package/lib/ui/frame.js.map +1 -1
  263. package/lib/ui/keymap-picker.d.ts +2 -1
  264. package/lib/ui/keymap-picker.d.ts.map +1 -1
  265. package/lib/ui/keymap-picker.js +1 -0
  266. package/lib/ui/keymap-picker.js.map +1 -1
  267. package/lib/ui/view/gate-card.d.ts +21 -0
  268. package/lib/ui/view/gate-card.d.ts.map +1 -0
  269. package/lib/ui/view/gate-card.js +104 -0
  270. package/lib/ui/view/gate-card.js.map +1 -0
  271. package/lib/ui/view/tool-card.d.ts +138 -0
  272. package/lib/ui/view/tool-card.d.ts.map +1 -0
  273. package/lib/ui/view/tool-card.js +413 -0
  274. package/lib/ui/view/tool-card.js.map +1 -0
  275. package/lib/ui/view/transcript-message.d.ts +62 -0
  276. package/lib/ui/view/transcript-message.d.ts.map +1 -0
  277. package/lib/ui/view/transcript-message.js +158 -0
  278. package/lib/ui/view/transcript-message.js.map +1 -0
  279. package/lib/ui/view.d.ts +87 -156
  280. package/lib/ui/view.d.ts.map +1 -1
  281. package/lib/ui/view.js +122 -556
  282. package/lib/ui/view.js.map +1 -1
  283. package/package.json +3 -1
  284. package/themes/deepseek-blue.yaml +9 -0
  285. package/themes/violet-orbit.yaml +7 -0
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env bash
2
+ # Clone the selected dsh home so plugin runs never write into the real profile.
3
+ set -euo pipefail
4
+
5
+ name="$(basename "${BASH_SOURCE[0]}")"
6
+ # The link policy is shared with the validator, so both read one rule set.
7
+ helper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
8
+ default_repo="${DSH_DOGFOOD_DEFAULT_REPO:-$PWD}"
9
+ default_repo="$(cd "$default_repo" && pwd -P)"
10
+
11
+ die() { printf '%s: error: %s\n' "$name" "$*" >&2; exit 1; }
12
+ note() { printf '%s: %s\n' "$name" "$*" >&2; }
13
+ step() { printf '\n==> %s\n' "$*" >&2; }
14
+
15
+ usage() {
16
+ cat <<'EOF'
17
+ Run a dsh plugin from a worktree, in a clone of the real home.
18
+
19
+ Usage:
20
+ run-plugin-from-worktree.sh [options] [<worktree-name-or-path>] [-- <dsh args>]
21
+
22
+ The target is the checkout to run. A path is used as given; a name is matched
23
+ against this checkout's worktrees by directory name or branch tail (feat/copy
24
+ matches .../feat/copy). No target means the current checkout.
25
+
26
+ Options:
27
+ --home DIR scratch home to run in (default: <tmp>/dsh-dogfood/<name>-<id>)
28
+ --source-home DIR home to clone (default: ~/.dsh)
29
+ --profile NAME profile inside the scratch home (default: tui)
30
+ --dsh PATH launcher to run (default: dsh on PATH, or $DSH_BIN)
31
+ --fresh seed the scratch home with credentials and settings only
32
+ --with-sessions copy stored sessions too, so --resume reaches real history
33
+ --reseed discard an existing scratch home and clone again
34
+ --no-build do not run pnpm run build in the target first
35
+ --no-launch set the run up and print the command instead of starting
36
+ --status print what the scratch home is pointed at and exit
37
+ --dry-run print the plan without touching anything
38
+ --clean remove the scratch home and exit
39
+ --list list this repository's worktrees and exit
40
+ -h, --help this text
41
+
42
+ Examples:
43
+ run-plugin-from-worktree.sh feature-x -- --resume
44
+ run-plugin-from-worktree.sh --fresh --profile tui-copy-fix ../dsh-tui-copy
45
+ run-plugin-from-worktree.sh --with-sessions --no-launch
46
+
47
+ The scratch home holds copied credentials and is kept private. --clean requires
48
+ a marker bound to this home, source, and checkout; no unmarked home is deleted.
49
+ EOF
50
+ }
51
+
52
+ # --- arguments ---------------------------------------------------------------
53
+
54
+ target=""
55
+ home=""
56
+ source_home="$HOME/.dsh"
57
+ profile="tui"
58
+ dsh_bin="${DSH_BIN:-}"
59
+ fresh=0
60
+ with_sessions=0
61
+ reseed=0
62
+ build=1
63
+ launch=1
64
+ dry_run=0
65
+ action=""
66
+
67
+ while [[ $# -gt 0 ]]; do
68
+ case "$1" in
69
+ --home) home="${2:?--home needs a directory}"; shift 2 ;;
70
+ --source-home) source_home="${2:?--source-home needs a directory}"; shift 2 ;;
71
+ --profile) profile="${2:?--profile needs a name}"; shift 2 ;;
72
+ --dsh) dsh_bin="${2:?--dsh needs a path}"; shift 2 ;;
73
+ --fresh) fresh=1; shift ;;
74
+ --with-sessions) with_sessions=1; shift ;;
75
+ --reseed) reseed=1; shift ;;
76
+ --no-build) build=0; shift ;;
77
+ --no-launch) launch=0; shift ;;
78
+ --status) action="status"; shift ;;
79
+ --dry-run) dry_run=1; launch=0; shift ;;
80
+ --clean) action="clean"; shift ;;
81
+ --list) action="list"; shift ;;
82
+ -h|--help) usage; exit 0 ;;
83
+ --) shift; break ;;
84
+ -*) die "unknown option $1 (try --help)" ;;
85
+ *) [[ -z "$target" ]] || die "only one target is accepted (got $target and $1)"
86
+ target="$1"; shift ;;
87
+ esac
88
+ done
89
+ passthrough=("$@")
90
+
91
+ if [[ -n "$source_home" ]]; then source_home="${source_home/#\~/$HOME}"; fi
92
+
93
+ # --- the repository's worktrees ---------------------------------------------
94
+
95
+ worktree_lines() {
96
+ git -C "$default_repo" worktree list --porcelain 2>/dev/null || true
97
+ }
98
+
99
+ list_worktrees() {
100
+ local line path="" branch=""
101
+ while IFS= read -r line; do
102
+ case "$line" in
103
+ worktree\ *) path="${line#worktree }" ;;
104
+ branch\ *) branch="${line#branch refs/heads/}" ;;
105
+ "")
106
+ [[ -n "$path" ]] || continue
107
+ if [[ -n "$branch" ]]; then printf '%-44s %s\n' "$path" "($branch)"
108
+ else printf '%-44s %s\n' "$path" "(detached)"; fi
109
+ path=""; branch="" ;;
110
+ esac
111
+ done < <(worktree_lines)
112
+ if [[ -n "$path" ]]; then printf '%-44s %s\n' "$path" "(${branch:-detached})"; fi
113
+ }
114
+
115
+ # A name matches a worktree's directory name or the tail of its branch, so both
116
+ # feat/copy-columns and copy-columns reach the same checkout.
117
+ resolve_by_name() {
118
+ local want="$1" line path="" branch="" found=""
119
+ while IFS= read -r line; do
120
+ case "$line" in
121
+ worktree\ *) path="${line#worktree }" ;;
122
+ branch\ *) branch="${line#branch refs/heads/}" ;;
123
+ "")
124
+ [[ -n "$path" ]] || continue
125
+ if [[ "$(basename "$path")" == "$want" || "$branch" == "$want" || "${branch##*/}" == "$want" ]]; then
126
+ [[ -z "$found" ]] || die "'$want' matches more than one worktree; pass a path"
127
+ found="$path"
128
+ fi
129
+ path=""; branch="" ;;
130
+ esac
131
+ done < <(worktree_lines)
132
+ if [[ -n "$path" ]] && [[ "$(basename "$path")" == "$want" || "$branch" == "$want" || "${branch##*/}" == "$want" ]]; then
133
+ [[ -z "$found" ]] || die "'$want' matches more than one worktree; pass a path"
134
+ found="$path"
135
+ fi
136
+ printf '%s' "$found"
137
+ }
138
+
139
+ resolve_target() {
140
+ [[ -n "$target" ]] || { printf '%s' "$default_repo"; return; }
141
+ if [[ -d "$target" ]]; then (cd "$target" && pwd -P); return; fi
142
+ local hit sibling
143
+ hit="$(resolve_by_name "$target")"
144
+ if [[ -z "$hit" ]]; then
145
+ sibling="$(cd "$default_repo/.." 2>/dev/null && pwd -P)/$target"
146
+ if [[ -d "$sibling" ]]; then hit="$sibling"; fi
147
+ fi
148
+ [[ -n "$hit" ]] || die "no worktree or directory named '$target' (try --list)"
149
+ (cd "$hit" && pwd -P)
150
+ }
151
+
152
+ if [[ "$action" == "list" ]]; then list_worktrees; exit 0; fi
153
+
154
+ [[ "$profile" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die "invalid profile name: $profile"
155
+ # A removed checkout cannot supply package.json. Only explicit scratch-home cleanup
156
+ # may use its private marker to recover the original, now absent target identity.
157
+ orphan_clean=0
158
+ if [[ "$action" == clean && -n "$home" && -z "$target" ]]; then
159
+ candidate_home="$(node -e "console.log(require('node:path').resolve(process.argv[1]))" "$home")"
160
+ # Orphan cleanup trusts only a private clone marker owned by this user.
161
+ if [[ -f "$candidate_home/.dsh-dogfood" ]] && node -e '
162
+ const fs = require("node:fs")
163
+ const [dir, marker] = process.argv.slice(1).map((file) => fs.lstatSync(file))
164
+ process.exit(dir.isDirectory() && dir.uid === process.getuid() && (dir.mode & 0o077) === 0 && marker.isFile() && marker.nlink === 1 && marker.uid === process.getuid() && (marker.mode & 0o077) === 0 ? 0 : 1)
165
+ ' "$candidate_home" "$candidate_home/.dsh-dogfood"; then
166
+ candidate_target="$(sed -n 's/^target=//p' "$candidate_home/.dsh-dogfood" | head -1)"
167
+ if [[ -n "$candidate_target" && ! -e "$candidate_target/package.json" ]]; then
168
+ node -e "const p=require('node:path'),s=process.argv[1];process.exit(p.isAbsolute(s) && s !== p.parse(s).root && s === p.normalize(s) ? 0 : 1)" "$candidate_target" || die "scratch marker has invalid target path"
169
+ target_path="$candidate_target"
170
+ orphan_clean=1
171
+ fi
172
+ fi
173
+ fi
174
+ if [[ "$orphan_clean" == 0 ]]; then
175
+ target_path="$(resolve_target)"
176
+ [[ -f "$target_path/package.json" ]] || die "$target_path has no package.json"
177
+ package_name="$(node -e "const p = require(process.argv[1]); if (typeof p.name !== 'string') process.exit(1); console.log(p.name)" "$target_path/package.json")" || die "$target_path has an invalid package.json"
178
+ [[ "$package_name" =~ ^(@[a-zA-Z0-9][a-zA-Z0-9._-]*/)?[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die "invalid plugin package name: $package_name"
179
+ if [[ "$build" == 1 ]] && ! node -e "process.exit(typeof require(process.argv[1]).scripts?.build === 'string' && require(process.argv[1]).scripts.build.trim() ? 0 : 1)" "$target_path/package.json"; then
180
+ build=0
181
+ fi
182
+ fi
183
+
184
+ # The hash prevents same-named checkouts in different repositories sharing state.
185
+ target_name="$(basename "$target_path")"
186
+ target_id="$(node -e "console.log(require('node:crypto').createHash('sha256').update(process.argv[1]).digest('hex').slice(0, 12))" "$target_path")"
187
+ tmp_root="${TMPDIR:-/tmp}"
188
+ : "${home:=${tmp_root%/}/dsh-dogfood/${target_name}-${target_id}}"
189
+ if [[ "$action" == clean && ! -d "$source_home" ]]; then
190
+ [[ -n "$home" && ! -e "$source_home" && ! -L "$source_home" ]] || die "missing source home requires --clean --home and an absent source path"
191
+ # Cleanup still needs the original physical identity to match its private marker.
192
+ source_home="$(node -e "const fs=require('node:fs'),p=require('node:path');let x=p.resolve(process.argv[1]),b=x;while(!fs.existsSync(b))b=p.dirname(b);console.log(p.join(fs.realpathSync(b),p.relative(b,x)))" "$source_home")"
193
+ else
194
+ [[ -d "$source_home" ]] || die "no home to clone at $source_home; pass --source-home"
195
+ source_home="$(cd "$source_home" && pwd -P)"
196
+ fi
197
+ [[ ! -L "$home" ]] || die "scratch home cannot be a symlink: $home"
198
+ home="$(node -e "const fs=require('node:fs'),p=require('node:path');let x=p.resolve(process.argv[1]),b=x;while(!fs.existsSync(b))b=p.dirname(b);console.log(p.join(fs.realpathSync(b),p.relative(b,x)))" "$home")"
199
+ [[ ! -L "$home" ]] || die "scratch home cannot be a symlink: $home"
200
+ [[ "$home" != "$source_home" && "$home/" != "$source_home/"* && "$source_home/" != "$home/"* ]] || die "scratch home and source home must not overlap"
201
+ user_home="$(cd "$HOME" && pwd -P)"
202
+ [[ "$home/" != "$user_home/"* && "$user_home/" != "$home/"* ]] || die "scratch home cannot overlap the user home"
203
+ [[ "$home/" != "$target_path/"* && "$target_path/" != "$home/"* ]] || die "scratch home cannot overlap the target checkout"
204
+ [[ "$home" != / ]] || die "scratch home cannot be filesystem root"
205
+ for path in "$home" "$source_home" "$target_path"; do [[ "$path" != *$'\n'* ]] || die "paths cannot contain newlines"; done
206
+ marker="$home/.dsh-dogfood"
207
+ profile_json="$home/profiles/$profile/package.json"
208
+
209
+ check_credentials() {
210
+ [[ ! -L "$home/.credentials.yaml" ]] || die "cloned credentials cannot be a symlink"
211
+ [[ -e "$home/.credentials.yaml" ]] || return 0
212
+ node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$home/.credentials.yaml" || die "cloned credentials cannot be hardlinked or non-regular"
213
+ }
214
+
215
+ check_clone_links() {
216
+ node "$helper_dir/clone-links.mjs" check "$home" "$source_home" || die "cloned home contains unsafe symlink or hardlink"
217
+ }
218
+
219
+ materialize_cloned_links() {
220
+ node "$helper_dir/clone-links.mjs" materialize "$home" "$source_home" || die "cloned home links could not be made self-contained"
221
+ }
222
+
223
+ validate_marker() {
224
+ [[ -f "$marker" && ! -L "$marker" ]] || die "$home has no valid .dsh-dogfood marker; refusing to modify it"
225
+ node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$marker" || die "$home marker cannot be hardlinked or non-regular"
226
+ [[ "$(marker_value home)" == "$home" && "$(marker_value source)" == "$source_home" && "$(marker_value target)" == "$target_path" && "$(marker_value profile)" == "$profile" ]] || die "$home marker identity does not match home, source, target, and profile"
227
+ }
228
+
229
+ [[ -n "$dsh_bin" ]] || dsh_bin="$(command -v dsh || true)"
230
+
231
+ describe_bundles() {
232
+ [[ -f "$profile_json" ]] || { printf 'none\n'; return; }
233
+ node -e "console.log(require(process.argv[1]).dsh.profile.bundles.join(', '))" "$profile_json" 2>/dev/null || printf 'unreadable\n'
234
+ }
235
+
236
+ marker_value() {
237
+ [[ -f "$marker" ]] || { printf 'unknown'; return; }
238
+ sed -n "s/^$1=//p" "$marker" | head -1
239
+ }
240
+
241
+ if [[ "$action" == "status" ]]; then
242
+ [[ -d "$home" ]] || die "no scratch home at $home"
243
+ printf 'home: %s\n' "$home"
244
+ printf 'source: %s\n' "$(marker_value source)"
245
+ printf 'target: %s\n' "$(marker_value target)"
246
+ printf 'profile: %s\n' "$profile"
247
+ printf 'bundles: %s\n' "$(describe_bundles)"
248
+ printf 'link: %s\n' "$(node -e "console.log(require(process.argv[1]).dependencies?.[process.argv[2]] ?? 'absent')" "$profile_json" "$package_name" 2>/dev/null || printf 'absent')"
249
+ exit 0
250
+ fi
251
+
252
+ if [[ "$action" == "clean" ]]; then
253
+ [[ -d "$home" ]] || { note "nothing at $home"; exit 0; }
254
+ validate_marker
255
+ rm -rf "$home"
256
+ note "removed $home"
257
+ exit 0
258
+ fi
259
+
260
+ if [[ "$dry_run" == 1 ]]; then
261
+ printf 'target: %s\n' "$target_path"
262
+ printf 'home: %s%s\n' "$home" "$([[ -d "$home" ]] && printf ' (exists: reuse)' || printf ' (missing: seed)')"
263
+ printf 'source: %s%s\n' "$source_home" "$([[ "$fresh" == 1 ]] && printf ' (fresh: credentials and settings only)' || printf '')"
264
+ printf 'profile: %s\n' "$profile"
265
+ printf 'build: %s\n' "$([[ "$build" == 1 ]] && printf 'pnpm run build' || printf 'skipped')"
266
+ printf 'launch: DSH_HOME=%s %s --profile %s %s\n' "$home" "${dsh_bin:-dsh}" "$profile" "${passthrough[*]:-}"
267
+ exit 0
268
+ fi
269
+
270
+ [[ -n "$dsh_bin" ]] || die "no dsh launcher found; put dsh on PATH or pass --dsh"
271
+
272
+ # --- seed the scratch home ---------------------------------------------------
273
+
274
+ if [[ -d "$home" && "$action" != "clean" && "$dry_run" != 1 ]]; then
275
+ validate_marker
276
+ check_credentials
277
+ chmod 700 "$home"
278
+ if [[ -f "$home/.credentials.yaml" ]]; then chmod 600 "$home/.credentials.yaml"; fi
279
+ fi
280
+
281
+ if [[ "$reseed" == 1 && -d "$home" ]]; then
282
+ validate_marker
283
+ rm -rf "$home"
284
+ fi
285
+
286
+ if [[ "$fresh" == 1 && -d "$home" ]]; then
287
+ note "$home already exists: --fresh applies when a home is seeded (--reseed to re-seed it)"
288
+ fi
289
+
290
+ if [[ ! -d "$home" ]]; then
291
+ [[ -d "$source_home" ]] || die "no home to clone at $source_home; pass --source-home"
292
+ [[ "$with_sessions" != 1 || ! -L "$source_home/sessions" ]] || die "source sessions cannot be a symlink"
293
+ step "seeding $home from $source_home"
294
+ mkdir -p "$home"
295
+ chmod 700 "$home"
296
+ if [[ "$fresh" == 1 ]]; then
297
+ for seed in .credentials.yaml settings.yaml; do
298
+ if [[ -f "$source_home/$seed" ]]; then cp -p "$source_home/$seed" "$home/$seed"; fi
299
+ done
300
+ elif command -v rsync >/dev/null 2>&1; then
301
+ # Preserving the source root's mode can expose copied credentials before final chmod.
302
+ rsync -a --no-perms --exclude '/sessions/' --exclude '/.dsh-dogfood' "$source_home/" "$home/"
303
+ if [[ "$with_sessions" == 1 && -d "$source_home/sessions" ]]; then rsync -a --no-perms "$source_home/sessions/" "$home/sessions/"; fi
304
+ else
305
+ # tar carries the tree without the bulk of the sessions; BSD tar takes the
306
+ # exclude in the same form as GNU tar.
307
+ mkdir -p "$home/sessions"
308
+ (cd "$source_home" && tar -c --exclude './sessions' --exclude './.dsh-dogfood' .) | (cd "$home" && tar -x)
309
+ if [[ "$with_sessions" == 1 && -d "$source_home/sessions" ]]; then (cd "$source_home" && tar -c sessions) | (cd "$home" && tar -x); fi
310
+ fi
311
+ chmod 700 "$home"
312
+ check_credentials
313
+ chmod 600 "$home/.credentials.yaml" 2>/dev/null || true
314
+ if [[ "$with_sessions" != 1 ]]; then
315
+ note "sessions were not copied; --resume starts empty (pass --with-sessions for your history)"
316
+ fi
317
+ fi
318
+
319
+ printf 'home=%s\nsource=%s\ntarget=%s\nprofile=%s\n' "$home" "$source_home" "$target_path" "$profile" > "$marker"
320
+ chmod 600 "$marker"
321
+ materialize_cloned_links
322
+ check_clone_links
323
+
324
+ # --- build, link, run --------------------------------------------------------
325
+
326
+ if [[ "$build" == 1 && ! -d "$target_path/node_modules" ]]; then
327
+ die "$target_path has no node_modules; run pnpm install there first (or pass --no-build)"
328
+ fi
329
+
330
+ if [[ "$build" == 1 ]]; then
331
+ step "building $target_path"
332
+ (cd "$target_path" && pnpm run build) || die "pnpm run build failed in $target_path"
333
+ fi
334
+
335
+ # A profile's local bundles are installed as relative symlinks under its own
336
+ # node_modules, and those paths only resolve at the depth of the home they were
337
+ # installed in. A clone at a different depth (a /tmp scratch home) leaves every
338
+ # one dangling, so dsh refuses to mount the profile. Rebuild each link from the
339
+ # absolute 'link:' spec its package.json already carries. The profile is created
340
+ # from the shipped template when it is missing, then rewritten, because
341
+ # 'dsh plugin add' re-materialises the same relative links and drops the other
342
+ # bundles the profile had.
343
+ step "pointing profile '$profile' at $target_path"
344
+ profile_dir="$home/profiles/$profile"
345
+ [[ ! -L "$home/profiles" && ! -L "$profile_dir" && ! -L "$profile_json" ]] || die "cloned profile cannot contain a symlink in its manifest path"
346
+ if [[ ! -f "$profile_json" ]]; then
347
+ DSH_HOME="$home" "$dsh_bin" --profile "$profile" --from-default-profile "$profile" --help >/dev/null 2>&1 || true
348
+ [[ ! -L "$home/profiles" && ! -L "$profile_dir" && ! -L "$profile_json" ]] || die "cloned profile cannot contain a symlink in its manifest path"
349
+ if [[ ! -f "$profile_json" ]]; then
350
+ DSH_HOME="$home" "$dsh_bin" plugin --profile "$profile" add "$target_path" >&2 || die "could not create profile '$profile' in $home"
351
+ fi
352
+ fi
353
+ [[ -f "$profile_json" ]] || die "profile '$profile' has no package.json at $profile_json"
354
+ node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$profile_json" || die "cloned profile manifest cannot be hardlinked or non-regular"
355
+
356
+ # Legacy callers require a prelisted bundle; generic callers can add one in the clone.
357
+ if [[ "${DSH_DOGFOOD_REQUIRE_LISTED:-0}" == 1 ]] && ! node -e "process.exit(require(process.argv[1]).dsh?.profile?.bundles?.includes(process.argv[2]) ? 0 : 1)" "$profile_json" "$package_name"; then
358
+ die "profile '$profile' does not list $package_name (bundles: $(describe_bundles)); add it to the profile first"
359
+ fi
360
+ node -e '
361
+ const fs = require("node:fs")
362
+ const path = require("node:path")
363
+ const [manifestPath, directory, originalDirectory, packageName, targetPath] = process.argv.slice(1)
364
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
365
+ if (!Array.isArray(manifest.dsh?.profile?.bundles)) throw Error("invalid profile bundle list")
366
+ if (!manifest.dsh.profile.bundles.includes(packageName)) manifest.dsh.profile.bundles.push(packageName)
367
+ manifest.dependencies ||= {}
368
+ manifest.dependencies[packageName] = "link:" + targetPath
369
+ const moduleDir = path.join(directory, "node_modules")
370
+ const entries = Object.entries(manifest.dependencies).filter(([, spec]) => typeof spec === "string" && spec.startsWith("link:"))
371
+ const packagePattern = /^(@[a-zA-Z0-9][a-zA-Z0-9._-]*\/)?[a-zA-Z0-9][a-zA-Z0-9._-]*$/
372
+ const state = (file) => fs.lstatSync(file, { throwIfNoEntry: false })
373
+ for (const part of [directory, path.dirname(directory), moduleDir]) {
374
+ if (state(part)?.isSymbolicLink()) throw Error("unsafe symlink in profile path: " + part)
375
+ }
376
+ for (const [name, spec] of entries) {
377
+ if (!packagePattern.test(name)) throw Error("invalid link dependency name: " + name)
378
+ if (name.includes("\n") || spec.includes("\n") || spec.includes("\0")) throw Error("unsafe link dependency: " + name)
379
+ const destination = path.join(moduleDir, name)
380
+ if (state(path.dirname(destination))?.isSymbolicLink()) throw Error("unsafe symlink in node_modules: " + name)
381
+ const existing = state(destination)
382
+ if (existing && !existing.isSymbolicLink()) throw Error("unsafe existing module at " + destination)
383
+ }
384
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n")
385
+ for (const [name, spec] of entries) {
386
+ const destination = path.join(moduleDir, name)
387
+ const target = path.resolve(originalDirectory, spec.slice("link:".length))
388
+ fs.mkdirSync(path.dirname(destination), { recursive: true })
389
+ if (state(destination)) fs.unlinkSync(destination)
390
+ fs.symlinkSync(target, destination)
391
+ }
392
+ ' "$profile_json" "$profile_dir" "$source_home/profiles/$profile" "$package_name" "$target_path" || die "could not safely relink profile '$profile'"
393
+ # Newly materialized dependency links must obey the same source-home boundary.
394
+ check_clone_links
395
+
396
+ bundles="$(describe_bundles)"
397
+ if ! node -e "process.exit(require(process.argv[1]).dsh.profile.bundles.includes(process.argv[2]) ? 0 : 1)" "$profile_json" "$package_name"; then
398
+ die "profile '$profile' does not list $package_name after relink (bundles: $bundles)"
399
+ fi
400
+
401
+ printf '\n' >&2
402
+ printf 'home: %s\n' "$home" >&2
403
+ printf 'target: %s\n' "$target_path" >&2
404
+ printf 'profile: %s (%s)\n' "$profile" "$bundles" >&2
405
+
406
+ run_command=(env "DSH_HOME=$home" "$dsh_bin" --profile "$profile")
407
+ if [[ "${#passthrough[@]}" -gt 0 ]]; then
408
+ run_command+=("${passthrough[@]}")
409
+ fi
410
+
411
+ if [[ "$launch" != 1 ]]; then
412
+ printf '\nrun it with:\n %s\n' "${run_command[*]}" >&2
413
+ exit 0
414
+ fi
415
+
416
+ step "starting the surface"
417
+ exec "${run_command[@]}"
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: dsh-tui-update-models
3
+ description: "Use when providers, models, prices, context windows, or reasoning effort levels must be added, changed, subset, or removed in a dsh profile: the llm-pi-ai provider config, the dsh-provider-extra owned catalog, settings.yaml, and a profile's cordis.patch.yml."
4
+ ---
5
+ # Update dsh providers and models
6
+
7
+ Every model the surface can pick comes from one of two owners, resolved at
8
+ composition time and read back by `/model`, `dsh --profile tui list-models`, and
9
+ the ctx gauge. Find the owner first; then edit exactly one place.
10
+
11
+ ## 1. Find the owner
12
+
13
+ | Where the wiring is | What it owns |
14
+ | --- | --- |
15
+ | `~/.dsh/profiles/<profile>/cordis.patch.yml` | The patch layer: applied after every bundle layer, last entry wins. Holds the catalog or `llm-pi-ai` config a developer actually edits. |
16
+ | `~/.dsh/settings.yaml` | Host settings. Plugin sections keyed by plugin id; used when a host writes config through the settings service instead of a patch. |
17
+ | `~/.dsh/profiles/<profile>/package.json` | Bundles and their dependency specs (`link:` for a checkout, a range for a release). |
18
+ | `node_modules/<bundle>/cordis.patch.yml` | The bundle's own defaults: disabled base rows, inserted rows, default config. |
19
+ | `node_modules/@earendil-works/pi-ai/dist/providers/data/<source>.json` | Installed per-provider catalog: ids, and the cost/context/effort data a template clone inherits. |
20
+
21
+ Entry shapes in the patch layer:
22
+
23
+ ```yaml
24
+ - id: llm-pi-ai # id-targeted config override
25
+ config: { ... } # replaces that row's config wholesale - restate every field
26
+ - { "id": "llm-deepseek", "disabled": true } # retire a row a bundle ships
27
+ - insert: # add a row the profile does not have
28
+ - id: web-search-exa
29
+ name: '@deepseek-ai/dsh-web-search-exa'
30
+ ```
31
+
32
+ A config in a patch **replaces** the base config; it never merges field by field.
33
+
34
+ ## 2. Pick the approach
35
+
36
+ | Intent | Approach |
37
+ | --- | --- |
38
+ | Add or re-declare providers additively | `llm-pi-ai` config: `providers.<route>.models[]` |
39
+ | One new id that behaves like a catalog sibling | `extraModels: [{ id, template, name? }]` (clone the sibling's wire behavior) |
40
+ | Serve only some of a route's catalog | a `models:` whitelist of ids, in the order the picker should show |
41
+ | Own the whole model directory (gateways, subscriptions, logins) | `dsh-provider-extra` `catalog` v1, and disable the base rows |
42
+ | Retire a model or a provider | drop it from the list, or disable the row that offers it |
43
+
44
+ `dsh-provider-extra` catalog skeleton, with the two override layers a correct
45
+ entry needs as soon as a vendor number differs from the inherited one:
46
+
47
+ ```yaml
48
+ - id: dsh-provider-extra
49
+ config:
50
+ catalog:
51
+ version: 1
52
+ default: { provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
53
+ providers:
54
+ - id: openai
55
+ name: OpenAI API
56
+ source: openai # pi-ai provider id; its data is the base
57
+ auth: { apiKeyRef: OPENAI_API_KEY }
58
+ models:
59
+ - id: gpt-6-sol
60
+ name: GPT-6 Sol
61
+ template: gpt-5.6-sol # inherits wire behavior AND stale metadata
62
+ metadata: # override what the vendor documented
63
+ contextWindow: 1050000
64
+ cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }
65
+ ```
66
+
67
+ Rules the resolver enforces: `version` is exactly `1`; duplicate provider ids
68
+ are refused; an empty catalog requires `default: null`; `default` must resolve to
69
+ a declared provider/model; a `reasoningEffort` the model does not support fails
70
+ composition.
71
+
72
+ ## 3. Reasoning efforts
73
+
74
+ pi-ai levels are `off, minimal, low, medium, high, xhigh, max`. In
75
+ `metadata.thinkingLevelMap`:
76
+
77
+ | Value | Meaning |
78
+ | --- | --- |
79
+ | `null` | level is not offered |
80
+ | absent | offered, sent to the vendor under this same name |
81
+ | a string | offered, sent as that vendor value (OpenAI's `none`, codex's `low` for `minimal`) |
82
+
83
+ `xhigh` and `max` are offered **only** when explicitly mapped. `off` means "send no
84
+ reasoning parameter" on the OpenAI and codex adapters. Vendors name the same
85
+ concept differently - map it, never rename the vendor value.
86
+
87
+ ## 4. Costs
88
+
89
+ Per 1M tokens: `{ input, output, cacheRead, cacheWrite }`. `0` is right for a plan
90
+ that bills credits, wrong for a per-token route. A vendor that charges more above
91
+ an input threshold needs the tier row, otherwise long prompts are understated.
92
+
93
+ ## 5. Verify before and after the edit
94
+
95
+ ```sh
96
+ dsh --profile tui list-models # every reachable route, id<TAB>name
97
+ ~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs # resolved cost/ctx/efforts
98
+ ```
99
+
100
+ The dump marks each value `installed`, `template:<id>`, or `metadata`, so an
101
+ inherited number is visible instead of trusted. Then check the vendor's own page
102
+ for price, context window, max output, and accepted effort values - the installed
103
+ catalog lags vendors, and a template clone carries a sibling's numbers.
104
+
105
+ ## 6. Pitfalls
106
+
107
+ | Symptom | Cause |
108
+ | --- | --- |
109
+ | A new model shows the sibling's price or ctx | it was cloned from a `template`; declare `metadata` |
110
+ | A window that is really a pricing threshold | vendors publish e.g. "above 272K input priced 2x" beside the real window |
111
+ | `off` missing on a reasoning model | `thinkingLevelMap.off` is `null` or absent where the vendor takes `none` |
112
+ | A stealth model with no vendor page | its numbers exist only in the routing catalog (pi.dev model page, models.dev) |
113
+ | Config ignored after an edit | a linked bundle loads `lib/`; run `pnpm run build` in that checkout |
114
+ | ctx gauge and auto-compact trigger early/late | the declared `contextWindow` is wrong, not the gauge |
115
+
116
+ Never put a key in a profile or a patch: name it (`apiKeyEnv`, `apiKeyRef`,
117
+ `credentialProvider`) and keep the value in the environment or `$DSH_HOME/.env`.
118
+ Test catalog edits in a cloned home - see the `dsh-tui-dogfood` skill.
119
+
120
+ Full field lists, credentials, and the vendor-verification checklist:
121
+ [references/model-wiring.md](references/model-wiring.md).