@hraness/dawg 0.6.1 → 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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
@@ -1,13 +1,29 @@
1
1
  /**
2
2
  * The command reference, in one place: `/help` renders it in the overlay,
3
- * `dawg --help` prints it, and the docs mirror it. Music words are bare;
4
- * app commands take a slash (bare aliases keep working but are listed once,
5
- * in their canonical form).
3
+ * `dawg --help` prints it, and the docs mirror it. Groups are the ten topic
4
+ * ids (src/lang/glossary.ts), the same ids `/guide` and `/menu` take.
5
+ * A topic may have sub-groups (`sound · performance`) so no group runs
6
+ * past about fifteen rows. Commands are written bare, since the slash is
7
+ * optional; only the free-text window verbs (`/rename`, `/fork`,
8
+ * `/resume`), `/auth`, `/bpm` (a sample's tempo; bare `bpm` is song tempo)
9
+ * and `/play` (play mode; bare `play` is the transport) keep it.
6
10
  */
7
11
  import { GUITAR_TUNING_NAMES } from "../../core/chords.ts";
12
+ import { KEYS, type KeyScreen } from "../../tui/grammar.ts";
13
+ import { displayWidth, truncate } from "../../tui/text.ts";
14
+ import {
15
+ resolveTopic,
16
+ topicMiss,
17
+ TOPIC_SUMMARY,
18
+ TOPICS,
19
+ type TopicId,
20
+ } from "../lang/glossary.ts";
8
21
  import { EXPRESSION_USAGE } from "./expression.ts";
22
+ import { editDistance } from "./nearest.ts";
23
+ import { VOCAL_VERBS } from "./vocal.ts";
9
24
 
10
- export type HelpGroup = "music" | "session" | "window" | "keys";
25
+ /** A topic id, or a heading of the start page. */
26
+ export type HelpGroup = string;
11
27
 
12
28
  export type HelpEntry = Readonly<{
13
29
  /** Canonical form, e.g. `pan <-1..1>`. */
@@ -23,29 +39,16 @@ export type HelpSection = Readonly<{
23
39
 
24
40
  export const HELP_SECTIONS: readonly HelpSection[] = [
25
41
  {
26
- group: "music",
42
+ group: "sound",
27
43
  entries: [
28
- { command: "play", summary: "start the transport" },
29
- { command: "pause", summary: "stop the transport" },
30
- { command: "tempo <bpm>", summary: "20–300 BPM" },
31
- { command: "add <note> at <beat> [for <beats>]", summary: "add C4 at 0" },
32
- { command: "remove <id>", summary: "delete a note" },
33
- { command: "move <id> to <beat>", summary: "shift a note" },
34
- { command: "length <id> <beats>", summary: "resize a note" },
35
- { command: "velocity <id> <0..1>", summary: "note loudness" },
36
- { command: "bars <count>", summary: "loop length, 1–256" },
37
- { command: "extend <count> bars", summary: "lengthen the loop" },
38
44
  {
39
45
  command: "instrument <name>",
40
- summary: "sine piano pluck bass saw square triangle wavetable kit",
46
+ summary: "sine piano pluck bass sawtooth square triangle wavetable kit",
47
+ },
48
+ {
49
+ command: "synth <param> <value> | preset <name>",
50
+ summary: "the synth · synth lpf 1200 · synth alone lists every param",
41
51
  },
42
- { command: "volume <0..1>", summary: "track level" },
43
- { command: "pan <-1..1>", summary: "left … right" },
44
- { command: "mute", summary: "silence this track" },
45
- { command: "unmute", summary: "hear it again" },
46
- { command: "solo", summary: "only this track" },
47
- { command: "unsolo", summary: "every track again" },
48
- { command: "clear", summary: "remove this track's notes" },
49
52
  {
50
53
  command: "wt <table> | wt <0..1> | wt list",
51
54
  summary: "wavetable synth · wt basic · wt wt_digital:2 · wt 0.5",
@@ -56,92 +59,68 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
56
59
  },
57
60
  {
58
61
  command:
59
- "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off",
62
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V",
60
63
  summary:
61
64
  "granular cloud · grain cloud · grain scan 0.2 · track swarm-2 · grain presets",
62
65
  },
63
66
  {
64
- command: "fx <effect> <param> <value> | on | off | preset <name>",
65
- summary: "effects · fx delay mix 0.3 · fx reverb on · fx lists them",
66
- },
67
- {
68
- command: "synth <param> <value> | preset <name>",
69
- summary: "synth voice · synth lpf 1200 · synth lists every param",
70
- },
71
- {
72
- command: "string <preset> | <param> <value> | presets | reset | off",
73
- summary:
74
- "plucked strings · string sitar · string buzz 0.8 · string ring 6",
67
+ command: "piano [<preset>] | grand | upright | felt | honkytonk",
68
+ summary: "modeled piano · piano ballad · piano lofi · piano prepared",
75
69
  },
76
70
  {
77
- command: "bowed [<preset>] | <param> <value> | presets",
78
- summary:
79
- "bowed strings · bowed violin · bowed cellos · bowed pressure 0.7 · bowed sord 1",
71
+ command: "keys <param> <value> | preset <name> | reset",
72
+ summary: "piano params · keys hardness 0.3 decay 1.5 · keys lists them",
80
73
  },
81
74
  {
82
- command: "rig <preset> | reset",
75
+ command: "epiano|wurli|clav [preset <name>] | <param> <value>",
83
76
  summary:
84
- "guitar rig · rig crunch · rig metal · rig shoegaze · rig lists them",
77
+ "electric keys · suitcase dyno funkclav · epiano vibe 0.6 · clav pickup bridge",
85
78
  },
86
79
  {
87
80
  command:
88
- "guitar tune <name|notes> | capo | hand | ring | position | reset",
89
- summary: "guitar fretting · guitar tune dadgad · guitar capo 2",
90
- },
91
- {
92
- command: "strum [chords] [pattern] [strokes D-DU-UDU] [speed 22ms]",
81
+ "tonewheel [<drawbars>] | combo [<tabs>] [<register>] | pipe [<stops>]",
93
82
  summary:
94
- "strummed guitar chords · strum G D Em C folk · strum I V vi IV · strum alone strums the track's chords",
83
+ "organs · tonewheel 888800008 perc 3rd · gospel · combo 08880 flute · pipe plenum · keys perc 3rd",
95
84
  },
96
85
  {
97
- command: "fx wobble|bloom|swell|double [param value]",
86
+ command: "rotary slow|fast|stop [at <beat>]",
98
87
  summary:
99
- "shoegaze · fx wobble depth 30 · fx double · fx reverb ir builtin:reverse",
100
- },
101
- {
102
- command: "track jangle|punk|funk|ragged|gtr-lead|gtr-metal|bachata",
103
- summary: "new guitar track with that rig",
88
+ "organ rotary speaker speed · rotary fast at 16 writes the keys-rotary lane",
104
89
  },
105
90
  {
106
- command: "stomp|head|cab <type> | <param> <value>",
91
+ command: "string <preset> | <param> <value> | presets | reset | off",
107
92
  summary:
108
- "pedal, amp, cabinet · stomp rat · head gain 7 gate -55 · cab 4x12",
109
- },
110
- {
111
- command: "piano [<preset>] | grand | upright | felt | honkytonk",
112
- summary: "modelled piano · piano ballad · piano lofi · piano prepared",
113
- },
114
- {
115
- command: "keys <param> <value> | preset <name> | reset",
116
- summary: "piano params · keys hardness 0.3 decay 1.5 · keys lists them",
93
+ "plucked strings · string sitar · string buzz 0.8 · string ring 6",
117
94
  },
118
95
  {
119
- command: "epiano|wurli|clav [preset <name>] | <param> <value>",
96
+ command: "bowed [<preset>] | <param> <value> | presets",
120
97
  summary:
121
- "electric keys · suitcase dyno funkclav · epiano vibe 0.6 · clav pickup bridge",
98
+ "bowed strings · bowed violin · bowed cellos · bowed pressure 0.7 · bowed sord 1",
122
99
  },
123
100
  {
124
101
  command:
125
- "tonewheel [<drawbars>] | combo [<registers>] [<voice>] | pipe [<stops>] [<row> <value> …]",
126
- summary:
127
- "organs · tonewheel 888800008 perc 3rd · gospel · combo 08880 flute · pipe plenum · keys perc 3rd",
128
- },
129
- {
130
- command: "rotary slow|fast|stop [at <beat>]",
131
- summary:
132
- "organ rotary speaker speed · rotary fast at 16 writes the keys-rotary lane",
102
+ "guitar tune <name|notes> | capo | hand | ring | position | reset",
103
+ summary: "guitar fretting · guitar tune dadgad · guitar capo 2",
133
104
  },
134
105
  {
135
106
  command:
136
107
  "modal <preset> | <param> <value> | mallet <name> | reset | off",
137
- summary:
138
- "mallets and bells · modal vibes · modal hardness 0.8 · modal presets",
108
+ summary: "mallets · modal vibes · modal hardness 0.8 · modal presets",
139
109
  },
140
110
  {
141
111
  command: "wind <preset> | <param> <value> | mute <name> | reset | off",
142
112
  summary:
143
113
  "winds and brass · wind flute · wind sax · wind players 4 · wind presets",
144
114
  },
115
+ {
116
+ command: "/try <sound command>",
117
+ summary: "hear it on a loop first · a A/B · enter keep",
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ group: "sound · performance",
123
+ entries: [
145
124
  {
146
125
  command: "art <articulation>|off [target]",
147
126
  summary:
@@ -179,375 +158,511 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
179
158
  "seeded feel at render · humanize 10 8 5 · humanize 20 bars 2-3",
180
159
  },
181
160
  { command: "expression", summary: "this track's performance settings" },
182
- { command: "filter <hz> [res]", summary: "low-pass · filter off" },
183
- { command: "delay <beats> [fb] [mix]", summary: "ping-pong · delay off" },
184
- { command: "reverb <mix> [size]", summary: "room · reverb off" },
161
+ ],
162
+ },
163
+ {
164
+ group: "sound · samples",
165
+ entries: [
185
166
  {
186
- command: "automate <lane> at <beat> <value>",
187
- summary: "volume pan filter resonance delay-feedback delay-mix wt",
167
+ command: "/sample [<path> [as <sample>]]",
168
+ summary: "add a sample · alone lists the samples",
188
169
  },
189
170
  {
190
- command: "automate <lane> points <b:v>...",
191
- summary: "several points · automate pan points 0:-1 4:1",
171
+ command: "/bpm <n> [<sample>]",
172
+ summary: "a sample's own tempo (the slash matters: bare bpm is tempo)",
192
173
  },
193
174
  {
194
- command: "automate <lane> remove <beat>",
195
- summary: "drop one point",
175
+ command: "fitmode [repitch|beats|tones|auto] [<sample>]",
176
+ summary: "how it fits · alone suggests one from the sound",
196
177
  },
197
- { command: "clear [<lane>] automation", summary: "drop a lane's points" },
198
178
  {
199
- command: "master <unit> on|off | preset <name> | <param> <value>",
200
- summary:
201
- "song master · eq glue tape width limiter · master glue ratio 4",
179
+ command: "len <beats> [<sample>]",
180
+ summary: "the sample lasts n beats of the song",
202
181
  },
203
182
  {
204
- command: "master <target> | target <lufs> | measure | off",
183
+ command: "shift <semitones> [formant keep|follow|<n>] [<sample>]",
205
184
  summary:
206
- "loudness · master streaming · master target -9 · master measure",
185
+ "a sample's pitch without changing its length · shift 0 clears",
207
186
  },
208
- { command: "track name <text>", summary: "rename this track" },
209
- { command: "meter <1..16>", summary: "beats per bar" },
210
187
  {
211
- command: "tempo <bpm> at <beat>|bar <n> [ramp|exp]",
212
- summary: "tempo change · tempo 90 at bar 9 ramp · tempo clear",
188
+ command: "fade [in|out] <seconds> [<sample>]",
189
+ summary: "a sample's fade in and out (Strudel fadeInTime/fadeTime)",
213
190
  },
214
191
  {
215
- command: "rit|accel [<n> bars] [to <bpm>] [at bar <n>]",
216
- summary: "gradual · rit 4 bars to 80 · a tempo · tempo primo",
192
+ command:
193
+ "resample <track>|orbit <n>|master [section <name>|bars a-b] [grain]",
194
+ summary: "render to a pinned WAV on a new sampler (or granular) track",
217
195
  },
196
+ ],
197
+ },
198
+ {
199
+ group: "voice",
200
+ entries: [
218
201
  {
219
- command: "fermata [at <beat>|at bar <n>|at end] [<extra beats>]",
220
- summary: "hold a beat · fermata at 31 2 · fermata clear",
202
+ command: "sing [preset] [param value]",
203
+ summary:
204
+ "built-in singing voice: aah ooh choir chorale airy glass lament soprano basso, throat: drone khoomei sygyt kargyraa",
221
205
  },
222
206
  {
223
- command: "meter <n>/<d> [at bar <n>]",
224
- summary: "meter change · meter 7/8 at bar 5 · meter clear",
207
+ command: "sing vowels <v> …",
208
+ summary:
209
+ "vowels for the track's notes in order, cycled · sing vowels a e i o",
225
210
  },
226
211
  {
227
- command: "track rate|phase|cycle <n> | off",
228
- summary: "polytempo · track rate 3/2 · track cycle 3",
212
+ command: "note vowel <v|a>u> [target]",
213
+ summary: "sung vowel for selected notes · note vowel o bar 2",
229
214
  },
230
215
  {
231
- command: "track phasing <beats> [over <beats>|hold <n>]",
232
- summary: "Reich phasing · track phasing 3 hold 8 · track time off",
216
+ command: "lyrics [bar] sun-lit morn-ing",
217
+ summary: "syllables onto the notes (- splits, _ holds, ~ skips)",
233
218
  },
234
219
  {
235
- command: "key <tonic> <mode> | none",
236
- summary: "song key · key A minor · key F# dorian",
220
+ command: "autotune [preset] [field value …] | off | presets",
221
+ summary:
222
+ "pitch correction · autotune hard · autotune gentle · autotune to notes melody · autotune key D bayati",
237
223
  },
238
224
  {
239
- command: "scale [<tonic>] <name> | list",
240
- summary: "song scale · scale D hijaz · scale yaman · scale list",
225
+ command:
226
+ "/formant <-12..12> [mix] | deep | giant | bright | tiny | on | off",
227
+ summary:
228
+ "formant shift at constant pitch · /formant -4 deeper · /formant 3 smaller",
241
229
  },
242
230
  {
243
- command: "tuning <name> | edo <n> | scl <file> | off",
244
- summary: "song tuning · tuning 19-edo · tuning just · tuning list",
231
+ command:
232
+ "/vowel <v> [<to> [<morph>]] | ee | to <v>|off | morph <0..1> | off",
233
+ summary: "vowel filter · /vowel a · /vowel a o 0.5 morphs a toward o",
245
234
  },
246
235
  {
247
- command: "tuning ref <hz> | root <note> | map linear|nearest",
248
- summary: "A4 reference · degree-0 key · keys per step",
236
+ command:
237
+ "vocoder [preset] | src <track> | <param> <value|reset> | off | presets",
238
+ summary:
239
+ "vocode the focused voice onto a synth (classic robot talkbox choir glass whisper smear lofi) · help vocoder lists params",
249
240
  },
241
+ ],
242
+ },
243
+ {
244
+ group: "voice · clips",
245
+ entries: [
246
+ { command: "vocal", summary: "voice tools: lists every verb" },
250
247
  {
251
- command: "tuning track <…> | track off",
252
- summary: "this track's tuning · off follows the song",
248
+ command:
249
+ "clip [id] gain -3 | gain by -3 | fade .01 .2 | fade in .01 | split 7",
250
+ summary: "edit an audio clip (dB, seconds, 1-based bars)",
253
251
  },
254
252
  {
255
- command: "cents <id> <±c>",
256
- summary: "detune one note · cents n3 -14 · add E4-14c at 0",
253
+ command:
254
+ "clip [id] trim offset 1 dur 4|end | rev | repeat 2 [to 32] | mute | rm",
255
+ summary: "trim, reverse, repeat, mute or remove a clip",
257
256
  },
257
+ ...VOCAL_VERBS.map((verb) => ({
258
+ command: `vocal ${verb.usage}`,
259
+ summary: verb.summary,
260
+ })),
261
+ ],
262
+ },
263
+ {
264
+ group: "effects",
265
+ entries: [
258
266
  {
259
- command: "section <name> <a>-<b> | add | dup | move | rename | delete",
260
- summary: "song sections · section chorus 9-16 · section lists them",
267
+ command: "fx <effect> <param> <value> | on | off | preset <name>",
268
+ summary:
269
+ "effects · fx delay mix 0.3 · fx reverb on · fx alone lists them",
261
270
  },
262
271
  {
263
- command: "section loop | jump | mute | vary <name>",
264
- summary: "section loop chorus · section mute verse drums",
272
+ command: "filter <hz> [res] · delay <beats> [fb] [mix] · reverb <mix>",
273
+ summary:
274
+ "shortcuts for fx filter, delay and reverb · filter 1200 · delay 0.5 · reverb 0.3",
265
275
  },
266
276
  {
267
- command: "form <section…> | off | bake",
268
- summary: "song form · form intro verse chorus*2 outro",
277
+ command: "rig <preset> | reset",
278
+ summary:
279
+ "guitar amp and pedals · rig crunch · rig jangle · rig metal · rig alone lists them",
269
280
  },
270
281
  {
271
- command: "build | drop | fill [<section> | <a>-<b>]",
272
- summary: "riser, roll, sweep · pre-drop cut and impact · drum fill",
282
+ command: "stomp|head|cab <type> | <param> <value>",
283
+ summary:
284
+ "pedal, amp, cabinet · stomp rat · head gain 7 gate -55 · cab 4x12",
285
+ },
286
+ {
287
+ command: "fx wobble|bloom|swell|double [param value]",
288
+ summary:
289
+ "shoegaze · fx wobble depth 30 · fx double · fx reverb ir builtin:reverse",
273
290
  },
291
+ ],
292
+ },
293
+ {
294
+ group: "rhythm",
295
+ entries: [
274
296
  {
275
- command: "hit <voice> at <beat>",
276
- summary: "kit tracks · hit kick at 0",
297
+ command: "hit <drum> at <beat>",
298
+ summary: "one hit on a kit track · hit kick at 0",
277
299
  },
278
300
  {
279
- command: "pattern <voice> <beats...> | every <step>",
280
- summary: "pattern kick every 1",
301
+ command: "pattern <drum> <beats...> | every <step>",
302
+ summary: "a drum on beats · pattern kick every 1",
281
303
  },
282
- { command: "clear <voice>", summary: "remove one drum voice" },
304
+ { command: "clear <drum>", summary: "remove one drum's hits" },
283
305
  {
284
- command: "euclid <voice> <pulses> [<steps>] [rotate <n>]",
306
+ command: "euclid <drum> <pulses> [<steps>] [rotate <n>]",
285
307
  summary: "generated rhythm · euclid hat 7 16 rotate 2",
286
308
  },
287
309
  {
288
- command: "euclid <voice> <field> <value> | off | freeze",
310
+ command: "euclid <drum> <field> <value> | off | freeze",
289
311
  summary: "repeats pace accent prob swing …",
290
312
  },
291
- { command: "grid <voice> <x.X.>", summary: "explicit steps · X accent" },
313
+ { command: "grid <drum> <x.X.>", summary: "explicit steps · X accent" },
292
314
  {
293
- command: "shift <semitones> [formant keep|follow|<n>] [<voice>]",
294
- summary:
295
- "a sample's pitch without changing its length · shift 0 clears",
315
+ command: "pattern [name]",
316
+ summary: "groove picker · hear each groove move",
296
317
  },
297
318
  {
298
- command: "fade [in|out] <seconds> [<voice>]",
299
- summary: "a sample's fade in and out (Strudel fadeInTime/fadeTime)",
319
+ command: "/kit [name]",
320
+ summary: "kit picker · synth kits, then sample kits",
300
321
  },
301
322
  {
302
- command:
303
- "resample <track>|orbit <n>|master [section <name>|bars a-b] [grain]",
304
- summary: "render to a pinned WAV on a new sampler (or granular) track",
323
+ command: "pack list|info|use|add",
324
+ summary: "sample packs · pack use 909/bd",
325
+ },
326
+ {
327
+ command: "euclid [drum]",
328
+ summary: "euclid rhythm editor · Ctrl-K › Rhythm",
305
329
  },
306
- { command: "undo", summary: "step back · Ctrl-Z" },
307
- { command: "redo", summary: "step forward · Ctrl-Y" },
308
330
  ],
309
331
  },
310
332
  {
311
- group: "session",
333
+ group: "chords",
312
334
  entries: [
313
- { command: "/sessions", summary: "list sessions in this workspace" },
314
- { command: "/resume [<n>|<name>|<id>]", summary: "switch session" },
315
- { command: "/rename <name>|--auto", summary: "name this session" },
316
- { command: "/fork [<name>]", summary: "copy into a new session" },
317
- { command: "/status", summary: "name · revision · digest · storage" },
318
335
  {
319
- command: "/export <file>",
320
- summary: "write track.loop/v1 JSON, or MIDI for .mid",
336
+ command: "key <tonic> <mode> | none",
337
+ summary: "song key · key A minor · key F# dorian",
321
338
  },
322
- { command: "/import <file>", summary: "replace the score from a file" },
323
339
  {
324
- command: "/sample [<path> [as <voice>]]",
325
- summary: "add a sample voice · list voices",
340
+ command: "scale [<tonic>] <name> | list",
341
+ summary: "song scale · scale D hijaz · scale yaman · scale list",
342
+ },
343
+ {
344
+ command: "tuning <name> | edo <n> | scl <file> | off",
345
+ summary: "song tuning · tuning 19-edo · tuning just · tuning list",
346
+ },
347
+ {
348
+ command: "tuning ref <hz> | root <note> | map linear|nearest",
349
+ summary: "A4 reference · degree-0 key · keys per step",
326
350
  },
327
351
  {
328
- command: "/bpm <n> [<voice>]",
352
+ command: "tuning track <…> | track off",
353
+ summary: "this track's tuning · off follows the song",
354
+ },
355
+ {
356
+ command: "cents <id> <±c>",
357
+ summary: "detune one note · cents n3 -14 · add E4-14c at 0",
358
+ },
359
+ {
360
+ command: "progression <chords> [each 4] [at 0] [bass]",
329
361
  summary:
330
- "the sample's own tempo (slash needed: bare bpm is song tempo)",
362
+ "sustained voice-led chords · progression i7 IV7 each 8 · progression Am7 D9 bass",
331
363
  },
332
364
  {
333
- command: "/fitmode [repitch|beats|tones|auto] [<voice>]",
334
- summary: "how it fits · alone suggests one from the sound",
365
+ command: "strum [chords] [pattern] [strokes D-DU-UDU] [speed 22ms]",
366
+ summary:
367
+ "strummed guitar chords · strum G D Em C folk · strum I V vi IV · strum alone strums the track's chords",
335
368
  },
336
369
  {
337
- command: "/len <beats> [<voice>]",
338
- summary: "the sample lasts n beats of the song",
370
+ command: "/chords auto|manual|off",
371
+ summary: "chords in play mode · /chords alone shows settings",
372
+ },
373
+ {
374
+ command: "play degrees|in-key|chromatic",
375
+ summary:
376
+ "in play mode the home row plays the key's degrees (any tuning)",
339
377
  },
340
378
  ],
341
379
  },
342
380
  {
343
- group: "window",
381
+ group: "mix",
344
382
  entries: [
383
+ { command: "volume <0..1>", summary: "track level" },
384
+ { command: "pan <-1..1>", summary: "left … right" },
385
+ { command: "mute", summary: "silence this track" },
386
+ { command: "unmute", summary: "hear it again" },
387
+ { command: "solo", summary: "only this track" },
388
+ { command: "unsolo", summary: "every track again" },
345
389
  {
346
- command: "/track <name>",
347
- summary: "focus a track, creating it if new",
348
- },
349
- { command: "/tracks", summary: "list tracks" },
350
- { command: "/view focus|all", summary: "one track or every track" },
351
- { command: "/transcript", summary: "scrollable log · Ctrl-O" },
352
- { command: "/theme default|high-contrast|mono", summary: "colors" },
353
- { command: "/motion on|off", summary: "animation" },
354
- { command: "/model [alias]", summary: "pick a model · cost per prompt" },
355
- {
356
- command: "/login [gateway|openrouter|codex|claude]",
357
- summary: "sign in to a provider",
390
+ command: "automate <lane> at <beat> <value>",
391
+ summary: "volume pan filter resonance delay-feedback delay-mix wt",
358
392
  },
359
393
  {
360
- command: "/logout [provider]",
361
- summary: "forget keys and the saved choice",
394
+ command: "automate <lane> points <b:v>...",
395
+ summary: "several points · automate pan points 0:-1 4:1",
362
396
  },
363
- { command: "/auth [--check]", summary: "provider and audio status" },
364
- { command: "/play [on|off]", summary: "keyboard play mode · Ctrl-P" },
397
+ { command: "automate <lane> remove <beat>", summary: "drop one point" },
398
+ { command: "clear [<lane>] automation", summary: "drop a lane's points" },
365
399
  {
366
- command: "/play degrees|chromatic",
367
- summary: "home row plays the key's scale degrees (any tuning) · i",
400
+ command: "master <unit> on|off | preset <name> | <param> <value>",
401
+ summary:
402
+ "song master · eq glue tape width limiter · master glue ratio 4",
368
403
  },
369
404
  {
370
- command: "/pattern [name]",
371
- summary: "drum groove picker · moving previews",
405
+ command: "master <target> | target <lufs> | measure | off",
406
+ summary:
407
+ "loudness · master streaming · master target -9 · master measure",
372
408
  },
409
+ ],
410
+ },
411
+ {
412
+ group: "arrange",
413
+ entries: [
414
+ { command: "add <note> at <beat> [for <beats>]", summary: "add C4 at 0" },
415
+ { command: "remove <id>", summary: "delete a note" },
416
+ { command: "move <id> to <beat>", summary: "shift a note" },
417
+ { command: "length <id> <beats>", summary: "resize a note" },
418
+ { command: "velocity <id> <0..1>", summary: "note loudness" },
419
+ { command: "clear", summary: "remove this track's notes" },
420
+ { command: "track <name>", summary: "focus a track, creating it if new" },
421
+ { command: "tracks", summary: "list tracks" },
373
422
  {
374
- command: "/kit [name]",
375
- summary: "drum kit picker · synth kits, then samples",
423
+ command: "track rm|move <name> [<position>]",
424
+ summary:
425
+ "remove a track (Ctrl-Z undoes) or move it · rm and delete are aliases",
376
426
  },
427
+ { command: "track name <text>", summary: "rename this track" },
377
428
  {
378
- command: "/pack list|info|use|add",
379
- summary: "sample packs · /pack use 909/bd",
429
+ command: "track rate|phase|loop <n> | off",
430
+ summary: "polytempo · track rate 3/2 · track loop 3",
380
431
  },
381
432
  {
382
- command: "/euclid [voice]",
383
- summary: "T-1 style rhythm editor · Rhythm in /menu",
433
+ command: "track phasing <beats> [over <beats>|hold <n>]",
434
+ summary: "Reich phasing · track phasing 3 hold 8 · track time off",
384
435
  },
436
+ ],
437
+ },
438
+ {
439
+ group: "arrange · song",
440
+ entries: [
385
441
  {
386
- command: "/menu [section]",
387
- summary: "every setting by hand · Ctrl-K",
442
+ command: "section <name> <a>-<b> | add | dup | move | rename | remove",
443
+ summary:
444
+ "song sections · section chorus 9-16 · section alone lists them",
388
445
  },
389
446
  {
390
- command: "/try <sound command>",
391
- summary: "hear it on a loop first · a A/B · enter keep",
447
+ command: "section loop | jump | mute | vary <name>",
448
+ summary: "section loop chorus · section mute verse drums",
392
449
  },
393
450
  {
394
- command: "/click on|off|<volume>",
395
- summary: "metronome · /count-in 0-2 · /grid 1/16",
451
+ command: "form <section…> | off | bake",
452
+ summary: "song form · form intro verse chorus*2 outro",
396
453
  },
397
454
  {
398
- command: "/chords auto|manual|off",
399
- summary: "play-mode chords · /chords for settings",
455
+ command: "build | drop | fill [<section> | <a>-<b>]",
456
+ summary: "riser, roll, sweep · pre-drop cut and impact · drum fill",
400
457
  },
401
458
  {
402
- command: "/help [topic]",
403
- summary: "start here · /help all for everything",
459
+ command: "style <id> [bars] [seed]",
460
+ summary: "a whole song in a style · style bebop 16 3 · style again",
404
461
  },
405
462
  {
406
- command: "/guide [topic]",
407
- summary: "short how-to guides · F1",
463
+ command: "style list|search|info · style blend <a> <b> [w]",
464
+ summary: "the style tree · style search maqam",
408
465
  },
466
+ { command: "bars <count>", summary: "loop length, 1–256" },
467
+ { command: "extend <count> bars", summary: "lengthen the loop" },
409
468
  ],
410
469
  },
411
470
  {
412
- group: "keys",
471
+ group: "project",
413
472
  entries: [
414
- { command: "Enter", summary: "submit" },
415
- { command: "Shift-Enter", summary: "newline" },
416
- { command: "Alt-Enter", summary: "queue the request" },
417
- { command: "Ctrl-Q", summary: "toggle queue mode" },
418
- { command: "Ctrl-Z / Ctrl-Y", summary: "undo / redo" },
419
- { command: "Ctrl-O", summary: "transcript" },
420
- { command: "Esc", summary: "cancel the agent · back one level" },
421
- { command: "?", summary: "keys for the screen you are on" },
422
- { command: "Space", summary: "play/pause on an empty prompt" },
473
+ { command: "play", summary: "start the transport" },
474
+ { command: "pause", summary: "stop the transport" },
423
475
  {
424
- command: "Ctrl-P",
425
- summary: "play mode · Z/X octave · R record · Q chords",
476
+ command: "/play [on|off]",
477
+ summary: "play mode: the computer keyboard plays notes · ctrl-p",
426
478
  },
479
+ { command: "tempo <bpm>", summary: "20–300 BPM" },
427
480
  {
428
- command: "Ctrl-K",
429
- summary: "menu · ←→ adjust · / filter · x reset",
481
+ command: "tempo <bpm> at <beat>|bar <n> [ramp|exp]",
482
+ summary: "tempo change · tempo 90 at bar 9 ramp · tempo clear",
430
483
  },
431
- { command: "Ctrl-C", summary: "exit" },
432
- ],
433
- },
434
- ];
435
-
436
- /**
437
- * `/help` with no topic: a short, task-first guide. Each row is something to
438
- * type or press, then what it does. The full reference is `/help all` (or one
439
- * group: `/help music`).
440
- */
441
- export const HELP_GUIDE: readonly HelpSection[] = [
442
- {
443
- group: "start here" as HelpGroup,
444
- entries: [
445
- { command: "type a request", summary: "“add a walking bass in A minor”" },
446
- { command: "ctrl-p", summary: "play notes on the computer keyboard" },
447
484
  {
448
- command: "ctrl-k",
449
- summary: "menu: sound, effects, performance, mix, master, arrange …",
485
+ command: "rit|accel [<n> bars] [to <bpm>] [at bar <n>]",
486
+ summary: "gradual · rit 4 bars to 80 · a tempo · tempo primo",
450
487
  },
451
- { command: "? · ctrl-z · ctrl-y", summary: "keys here · undo · redo" },
452
- ],
453
- },
454
- {
455
- group: "play notes" as HelpGroup,
456
- entries: [
457
488
  {
458
- command: "ctrl-p, a s d f …",
459
- summary: "piano keys · z x octave · esc leave",
489
+ command: "fermata [at <beat>|at bar <n>|at end] [<extra beats>]",
490
+ summary: "hold a beat · fermata at 31 2 · fermata clear",
460
491
  },
492
+ { command: "meter <1..16>", summary: "beats per bar" },
461
493
  {
462
- command: "r, then space",
463
- summary: "record over the loop (one undo a bar)",
494
+ command: "meter <n>/<d> [at bar <n>]",
495
+ summary: "meter change · meter 7/8 at bar 5 · meter clear",
464
496
  },
465
- ],
466
- },
467
- {
468
- group: "make drums" as HelpGroup,
469
- entries: [
470
497
  {
471
- command: "/pattern · /kit",
472
- summary: "pick a groove · pick a drum kit",
498
+ command: "/click on|off|<volume>",
499
+ summary: "metronome · /count-in 0-2 · grid 1/16",
473
500
  },
474
- { command: "/euclid", summary: "rhythm editor: pulses, steps, rotation" },
475
- ],
476
- },
477
- {
478
- group: "shape the sound" as HelpGroup,
479
- entries: [
480
501
  {
481
- command: "ctrl-k › Sound",
482
- summary: "instrument, strings, envelope, filter, wavetable",
502
+ command: "export <file> [stems]",
503
+ summary:
504
+ "JSON, MIDI for .mid, WAV for .wav · export song.wav · export mix.wav stems",
483
505
  },
484
- { command: "ctrl-k › Effects", summary: "delay, reverb, distortion …" },
506
+ { command: "import <file>", summary: "replace the score from a file" },
485
507
  {
486
- command: "master streaming",
487
- summary: "finish: -14 LUFS, limiter · master measure",
508
+ command: "calibration [0|1|latest|off]",
509
+ summary: "sound fixes · 1 chokes hats, levels keys · 0 legacy",
488
510
  },
489
- { command: "/try fx reverb mix 0.6", summary: "hear it before keeping" },
511
+ { command: "undo", summary: "step back · Ctrl-Z" },
512
+ { command: "redo", summary: "step forward · Ctrl-Y" },
513
+ { command: "/status", summary: "name · revision · digest · storage" },
490
514
  ],
491
515
  },
492
516
  {
493
- group: "shape the performance" as HelpGroup,
517
+ group: "project · window",
494
518
  entries: [
519
+ { command: "/view focus|all", summary: "one track or every track" },
520
+ { command: "/transcript", summary: "scrollable log · Ctrl-O" },
521
+ { command: "/theme default|high-contrast|mono", summary: "colors" },
522
+ { command: "/motion on|off", summary: "animation" },
523
+ {
524
+ command: "/menu [section]",
525
+ summary: "every setting by hand · Ctrl-K · /menu <topic>",
526
+ },
495
527
  {
496
- command: "art staccato bar 2",
497
- summary: "humanize 8 5 · ctrl-k › Sound › performance",
528
+ command: "help [topic]",
529
+ summary: "start here · help <topic> · help all for everything",
498
530
  },
531
+ { command: "/guide [topic]", summary: "short how-to guides · F1" },
499
532
  ],
500
533
  },
501
534
  {
502
- group: "chords" as HelpGroup,
535
+ group: "project · session",
503
536
  entries: [
504
- { command: "key A minor", summary: "set the song key" },
505
- {
506
- command: "ctrl-p, then q",
507
- summary: "chord mode · 1–4 type · 5–8 extension · n next",
508
- },
537
+ { command: "/sessions", summary: "list sessions in this workspace" },
538
+ { command: "/resume [<n>|<name>|<id>]", summary: "switch session" },
539
+ { command: "/rename <name>|--auto", summary: "name this session" },
540
+ { command: "/fork [<name>]", summary: "copy into a new session" },
509
541
  ],
510
542
  },
511
543
  {
512
- group: "song structure" as HelpGroup,
544
+ group: "agent",
513
545
  entries: [
514
546
  {
515
- command: "section verse 1-8",
516
- summary: "name bars · form verse chorus*2 orders them",
547
+ command: "/model [alias]",
548
+ summary: "pick the agent's model · /model lists them · /model fast",
517
549
  },
518
550
  {
519
- command: "build · drop · fill",
520
- summary: "transitions · ctrl-k › Arrange · /help arrange",
551
+ command: "/model key [gateway|openrouter|codex|claude]",
552
+ summary: "add an agent key (optional) · finds existing setups first",
553
+ },
554
+ {
555
+ command: "/logout [provider]",
556
+ summary: "forget keys and the saved choice",
557
+ },
558
+ { command: "/auth [--check]", summary: "provider and audio status" },
559
+ {
560
+ command: "/showme on|quiet|off",
561
+ summary:
562
+ "the agent types its commands in your prompt bar as it streams",
521
563
  },
522
564
  ],
523
565
  },
566
+ ];
567
+
568
+ /**
569
+ * `/help` with no topic: start here, then the ten topics. Each row is
570
+ * something to type or press, then what it does. The full reference is
571
+ * `/help all`; one topic is `/help <topic>`.
572
+ */
573
+ export const HELP_GUIDE: readonly HelpSection[] = [
524
574
  {
525
- group: "more" as HelpGroup,
575
+ group: "start here",
526
576
  entries: [
527
577
  {
528
- command: "rit 4 bars to 80",
529
- summary: "tempo, fermatas, meter · tuning pelog · scale yaman",
578
+ command: "type a request",
579
+ summary: "“add a walking bass in A minor” (with an agent)",
530
580
  },
531
581
  {
532
- command: "/help all",
533
- summary: "every command and key · /guide feature guides · F1",
582
+ command: "style deep-house",
583
+ summary: "a whole song in a style · style list",
534
584
  },
585
+ {
586
+ command: "space · ctrl-p",
587
+ summary: "play · play mode: the keyboard plays notes",
588
+ },
589
+ { command: "ctrl-k", summary: "menu: every setting by hand" },
590
+ { command: "? · ctrl-z · ctrl-y", summary: "keys here · undo · redo" },
591
+ ],
592
+ },
593
+ {
594
+ group: "topics · help <topic>",
595
+ entries: [
596
+ ...TOPICS.map((id) => ({ command: id, summary: TOPIC_SUMMARY[id] })),
597
+ { command: "all", summary: "every command · /guide <topic> · F1" },
535
598
  ],
536
599
  },
537
600
  ];
538
601
 
539
- /** Topics `/help <topic>` takes, besides `all`. */
540
- export const HELP_TOPICS = [
541
- "music",
542
- "session",
543
- "window",
544
- "keys",
545
- "arrange",
546
- ] as const;
602
+ /** Topics `/help <topic>` takes, besides `all` (aliases resolve too). */
603
+ export const HELP_TOPICS = TOPICS;
604
+
605
+ /**
606
+ * `/help keys`: every screen's `?` panel, straight from tui/grammar.ts
607
+ * KEYS, so the reference and the panels never disagree.
608
+ */
609
+ export function keysLines(width = 80): string[] {
610
+ const seen = new Set<string>();
611
+ const sections = (Object.keys(KEYS) as KeyScreen[]).flatMap((screen) =>
612
+ KEYS[screen].map((section) => ({
613
+ title:
614
+ section.title === screen || screen === "prompt"
615
+ ? section.title
616
+ : `${screen} · ${section.title}`,
617
+ rows: section.rows,
618
+ })),
619
+ );
620
+ const column =
621
+ Math.min(
622
+ 24,
623
+ Math.max(
624
+ ...sections.flatMap((s) => s.rows.map(([keys]) => displayWidth(keys))),
625
+ ),
626
+ ) + 2;
627
+ const lines = ["── keys"];
628
+ for (const section of sections) {
629
+ const key = JSON.stringify(section.rows);
630
+ if (seen.has(key)) continue;
631
+ seen.add(key);
632
+ lines.push("", truncate(`── ${section.title}`, width));
633
+ for (const [keys, action] of section.rows)
634
+ lines.push(
635
+ truncate(
636
+ `${keys}${" ".repeat(Math.max(1, column - displayWidth(keys)))}${action}`,
637
+ width,
638
+ ),
639
+ );
640
+ }
641
+ return lines;
642
+ }
643
+
644
+ /** The last rows of a topic page: the same id in the other two doors. */
645
+ function doors(id: TopicId, width: number): string[] {
646
+ return ["", truncate(`guide ${id} · menu ${id} · help all`, width)];
647
+ }
648
+
649
+ /** A topic's groups: `sound`, then `sound · performance`, `sound · samples`. */
650
+ export function topicSections(id: TopicId): readonly HelpSection[] {
651
+ return HELP_SECTIONS.filter(
652
+ (section) => section.group === id || section.group.startsWith(`${id} · `),
653
+ );
654
+ }
655
+
656
+ /** One topic's page: its groups, then where else the topic opens. */
657
+ function topicLines(id: TopicId, width: number): string[] {
658
+ if (id === "keys") return [...keysLines(width), ...doors(id, width)];
659
+ return [...sectionLines(topicSections(id), width), ...doors(id, width)];
660
+ }
547
661
 
548
662
  /**
549
- * Rows for `/help [topic]`: the guide with no topic, the full reference for
550
- * `all`, one group for its name; undefined for an unknown topic.
663
+ * Rows for `/help [topic]`: the start page with no topic, the full
664
+ * reference for `all`, a topic for its id or an alias, a command's rows
665
+ * for its verb (`/help vocoder`); undefined for an unknown word.
551
666
  */
552
667
  export function helpTopicLines(
553
668
  topic: string | undefined,
@@ -557,34 +672,91 @@ export function helpTopicLines(
557
672
  if (!name) return sectionLines(HELP_GUIDE, width, 20);
558
673
  if (name === "all" || name === "commands" || name === "reference")
559
674
  return helpLines(width);
560
- if (name === "arrange" || name === "arrangement" || name === "sections")
561
- return sectionLines([arrangeSection()], width);
562
- const group = HELP_SECTIONS.find((section) => section.group === name);
563
- return group ? sectionLines([group], width) : undefined;
675
+ if ((TOPICS as readonly string[]).includes(name))
676
+ return topicLines(name as TopicId, width);
677
+ const spelled = HELP_COMMAND_ALIASES[name];
678
+ if (spelled) return commandTopic(spelled, width, name);
679
+ const alias = resolveTopic(name);
680
+ // A topic alias wins unless a reference row starts with the word:
681
+ // `/help sections` is the arrange topic, `/help scale` the command.
682
+ const command =
683
+ alias && !hasRow(name) ? undefined : commandTopic(name, width);
684
+ if (command)
685
+ return alias
686
+ ? [...command, "", truncate(`see also help ${alias}`, width)]
687
+ : command;
688
+ return alias ? topicLines(alias, width) : undefined;
564
689
  }
565
690
 
566
- /** `/help arrange`: every arranging command with its full usage. */
567
- function arrangeSection(): HelpSection {
568
- const verbs = ["section", "sections", "form", "build", "drop", "fill"];
569
- return {
570
- group: "arrange" as HelpGroup,
571
- entries: [
572
- // Usage strings run long: wrapped onto continuation rows.
573
- ...verbs.flatMap((verb) =>
574
- wrapWords(USAGE[verb] ?? "", 46).map((summary, index) => ({
575
- command: index === 0 ? verb : "",
576
- summary,
577
- })),
578
- ),
579
- { command: "/menu arrange", summary: "the Arrange menu (ctrl-k)" },
580
- {
581
- command: "dawg render --section <name>",
582
- summary: "export one section",
583
- },
584
- ],
585
- };
691
+ /** True when a reference row starts with `name` (`/scale`, `scale …`). */
692
+ function hasRow(name: string): boolean {
693
+ const verb = name.replace(/[^a-z0-9-]/g, "");
694
+ return HELP_SECTIONS.some((section) =>
695
+ section.entries.some((entry) =>
696
+ new RegExp(`^/?${verb}(\\s|$)`, "i").test(entry.command),
697
+ ),
698
+ );
699
+ }
700
+
701
+ /**
702
+ * `/help <command>` (`/help vocoder`, `/help clip`): the command's rows
703
+ * from every group, then its full usage wrapped; undefined when no command
704
+ * has that name.
705
+ */
706
+ function commandTopic(
707
+ name: string,
708
+ width: number,
709
+ usageKey = name,
710
+ ): string[] | undefined {
711
+ const pattern = name
712
+ .replace(/[^a-z0-9 -]/g, "")
713
+ .trim()
714
+ .replace(/ +/g, "\\s+");
715
+ const own = (command: string) =>
716
+ new RegExp(`^/?${pattern}(\\s|$)`, "i").test(command);
717
+ const rows = HELP_SECTIONS.flatMap((section) =>
718
+ section.entries.filter((entry) => own(entry.command)),
719
+ );
720
+ const usage = USAGE[usageKey] ?? USAGE[name];
721
+ if (rows.length === 0 && !usage) return undefined;
722
+ const wrap = Math.max(30, width - 2);
723
+ // A usage line that repeats a row keeps only what it adds (examples).
724
+ const extra = usage
725
+ ? rows.reduce((text, entry) => {
726
+ const bare = (value: string) =>
727
+ value.replace(/^\//, "").replace(/\s*\|\s*/g, "|");
728
+ return bare(text).startsWith(bare(entry.command))
729
+ ? bare(text)
730
+ .slice(bare(entry.command).length)
731
+ .replace(/^\s*·\s*/, "")
732
+ : text;
733
+ }, usage)
734
+ : "";
735
+ // Examples a row summary already shows are not repeated.
736
+ const shown = rows.map((entry) => entry.summary).join(" · ");
737
+ const added = extra
738
+ .split(" · ")
739
+ .filter((part) => part && !shown.split(" · ").includes(part))
740
+ .join(" · ");
741
+ return [
742
+ `── ${name}`,
743
+ ...rows.flatMap((entry) => [
744
+ ...wrapWords(entry.command, wrap),
745
+ ...wrapWords(entry.summary, wrap - 2).map((line) => ` ${line}`),
746
+ ]),
747
+ ...(added ? ["", ...wrapWords(added, wrap)] : []),
748
+ ];
586
749
  }
587
750
 
751
+ /**
752
+ * Typed aliases that /help shows under their one word (design §8.2): the
753
+ * alias opens the page and the title, heading and rows name the canonical
754
+ * command.
755
+ */
756
+ export const HELP_COMMAND_ALIASES: Readonly<Record<string, string>> = {
757
+ login: "model key",
758
+ };
759
+
588
760
  function wrapWords(text: string, width: number): string[] {
589
761
  const rows: string[] = [];
590
762
  let row = "";
@@ -614,18 +786,21 @@ function sectionLines(
614
786
  );
615
787
  for (const section of sections) {
616
788
  if (lines.length > 0) lines.push("");
617
- lines.push(`── ${section.group}`);
789
+ lines.push(truncate(`── ${section.group}`, width));
618
790
  for (const entry of section.entries) {
619
791
  const pad = Math.max(1, column - entry.command.length);
620
792
  lines.push(
621
- `${entry.command}${" ".repeat(pad)}${entry.summary}`.slice(0, width),
793
+ truncate(`${entry.command}${" ".repeat(pad)}${entry.summary}`, width),
622
794
  );
623
795
  }
624
796
  }
625
797
  return lines;
626
798
  }
627
799
 
628
- /** Overlay rows: a heading per group, then `command summary` lines. */
800
+ /**
801
+ * `/help all`: a heading per topic, then `command summary` lines, then
802
+ * the keys of every screen.
803
+ */
629
804
  export function helpLines(width = 80): string[] {
630
805
  const lines: string[] = [];
631
806
  const column = Math.min(
@@ -636,27 +811,25 @@ export function helpLines(width = 80): string[] {
636
811
  );
637
812
  for (const section of HELP_SECTIONS) {
638
813
  if (lines.length > 0) lines.push("");
639
- lines.push(`── ${section.group}`);
814
+ lines.push(truncate(`── ${section.group}`, width));
640
815
  for (const entry of section.entries) {
641
816
  const pad = Math.max(1, column - entry.command.length);
642
817
  lines.push(
643
- `${entry.command}${" ".repeat(pad)}${entry.summary}`.slice(0, width),
818
+ truncate(`${entry.command}${" ".repeat(pad)}${entry.summary}`, width),
644
819
  );
645
820
  }
646
821
  }
647
- return lines;
822
+ return [...lines, "", ...keysLines(width)];
648
823
  }
649
824
 
650
825
  /** The `Commands:` block of `dawg --help`. */
651
826
  export function helpText(): string {
652
- return HELP_SECTIONS.filter((section) => section.group !== "keys")
653
- .map(
654
- (section) =>
655
- `${section.group}:\n${section.entries
656
- .map((entry) => ` ${entry.command}`)
657
- .join("\n")}`,
658
- )
659
- .join("\n");
827
+ return HELP_SECTIONS.map(
828
+ (section) =>
829
+ `${section.group}:\n${section.entries
830
+ .map((entry) => ` ${entry.command}`)
831
+ .join("\n")}`,
832
+ ).join("\n");
660
833
  }
661
834
 
662
835
  /** Usage for a known verb, shown instead of sending a near-miss to the agent. */
@@ -669,12 +842,15 @@ export const USAGE: Readonly<Record<string, string>> = {
669
842
  scale: "scale [<tonic>] <name> | list · scale D hijaz",
670
843
  tuning:
671
844
  "tuning <name> | edo <n> | ratios … | cents … | scl <file> [kbm <file>] | ref <hz> | root <note> | map linear|nearest | track … | off · tuning 19-edo",
672
- tune: "tuning <name> | edo <n> | scl <file> | off · tuning list",
845
+ calibration: "calibration [0|1|latest|off] · calibration latest",
846
+ tune: "tuning <name> | edo <n> | scl <file> | off · tuning list · pitch correction is autotune",
847
+ autotune:
848
+ "autotune [hard|robot|warble|trap|pop|natural|gentle|guided|locked] | to scale|chromatic|chord|notes [track] | key <key> | speed hold glide <ms> | relax amount center drift <0..1> | flex <0..100> | vib <Hz> vibmod <st> | voice auto|bass|tenor|alto|soprano | reset | off | presets · autotune hard · autotune natural flex 40",
673
849
  cents: "cents <id> <±cents> · cents n3 -14",
674
- grid: "/grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
850
+ grid: "grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
675
851
  tempo:
676
852
  "tempo takes 20…300 · tempo 120 · tempo 90 at bar 9 [ramp|exp] · tempo remove bar 9 · tempo clear · tempo map",
677
- bpm: "tempo takes 20…300 · tempo 120 · a sample's own tempo: /bpm 174 [<voice>] · /bpm off",
853
+ bpm: "tempo takes 20…300 · tempo 120 · a sample's own tempo: /bpm 174 [<sample>] · /bpm off",
678
854
  rit: "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
679
855
  ritardando:
680
856
  "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
@@ -696,7 +872,7 @@ export const USAGE: Readonly<Record<string, string>> = {
696
872
  bars: "bars takes 1…256 · bars 8",
697
873
  extend: "extend <count> bars · extend 4 bars",
698
874
  instrument:
699
- "instrument <name> · sine piano pluck bass saw square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · electric: epiano suitcase dyno wurli clav funkclav · synth: sawtooth supersaw pulse white pink z_square…",
875
+ "instrument <name> · sine piano pluck bass sawtooth square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · electric: epiano suitcase dyno wurli clav funkclav · synth: supersaw pulse white pink z_square · voices: vocal aah ooh choir chorale khoomei sygyt kargyraa vocoder…",
700
876
  volume: "volume takes 0…1 · volume 0.8",
701
877
  vol: "volume takes 0…1 · volume 0.8",
702
878
  pan: "pan takes -1…1 · pan -0.5",
@@ -712,28 +888,28 @@ export const USAGE: Readonly<Record<string, string>> = {
712
888
  reverb: "reverb <mix> [size] · reverb 0.3 0.6 · reverb off",
713
889
  automate: "automate <lane> at <beat> <value> · automate volume at 0 0.5",
714
890
  automation: "automate <lane> at <beat> <value> · automate volume at 0 0.5",
715
- hit: "hit <voice> at <beat> · hit kick at 0",
716
- pattern: "pattern <voice> <beats...> | every <step> · pattern kick every 1",
717
- clear: "clear · clear <voice> · clear [<lane>] automation",
891
+ hit: "hit <drum> at <beat> · hit kick at 0",
892
+ pattern: "pattern <drum> <beats...> | every <step> · pattern kick every 1",
893
+ clear: "clear · clear <drum> · clear [<lane>] automation",
718
894
  track:
719
- "/track <name> · /track drums · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track cycle <beats> · track phasing <beats> [over <beats>]",
720
- tracks: "/tracks",
895
+ "track <name> · track drums · track rm <name> · track move <name> <position> · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track loop <beats> · track phasing <beats> [over <beats>]",
896
+ tracks: "tracks",
721
897
  sessions: "/sessions",
722
898
  resume: "/resume [<n>|<name>|<id>]",
723
899
  rename: "/rename <name> | --auto",
724
900
  fork: "/fork [<name>]",
725
901
  status: "/status",
726
- export: "/export <file> · /export loop.track.json",
727
- import: "/import <file> · /import loop.track.json",
902
+ export: "export <file> · export loop.track.json",
903
+ import: "import <file> · import loop.track.json",
728
904
  sample:
729
- "/sample <path> [as <voice>] · /sample set <voice> <control> <value>… · /sample set brk fit on clip 1 · /sample set soft vel 0-63 rr a",
730
- samples: "/sample · lists the focused track's voices",
905
+ "/sample <path> [as <sample>] · /sample set <sample> <control> <value>… · /sample set brk fit on clip 1 · /sample set soft vel 0-63 rr a",
906
+ samples: "/sample · lists the focused track's samples",
731
907
  fitmode:
732
- "/fitmode [repitch|beats|tones|auto|off] [<voice>] · /fitmode beats · /fitmode auto brk",
733
- len: "/len <beats> [<voice>] · /len 16 · /len off",
908
+ "fitmode [repitch|beats|tones|auto|off] [<sample>] · fitmode beats · fitmode auto brk",
909
+ len: "len <beats> [<sample>] · len 16 · len off",
734
910
  shift:
735
- "shift <semitones> [formant keep|follow|<n>] [<voice>] · shift 7 formant keep · shift 0",
736
- fade: "fade [in|out] <seconds> [<voice>] · fade out 0.5 · fade in 0.05 · fade off",
911
+ "shift <semitones> [formant keep|follow|<n>] [<sample>] · shift 7 formant keep · shift 0",
912
+ fade: "fade [in|out] <seconds> [<sample>] · fade out 0.5 · fade in 0.05 · fade off",
737
913
  resample:
738
914
  "resample <track>|orbit <n>|master [section <name>|bars a-b] [post] [grain] [as <id>] · resample lead · resample drums bars 1-2 grain · resample master section chorus",
739
915
  bounce: "resample <track>|orbit <n>|master [section <name>|bars a-b] [grain]",
@@ -742,12 +918,14 @@ export const USAGE: Readonly<Record<string, string>> = {
742
918
  log: "/transcript",
743
919
  theme: "/theme default | high-contrast | mono",
744
920
  motion: "/motion on | off",
745
- model: "/model [alias | vendor/model]",
746
- login: "/login [gateway | openrouter | codex | claude]",
921
+ model:
922
+ "/model [fast | alias | vendor/model] · /model key [gateway | openrouter | codex | claude]",
923
+ models: "/model [fast | alias | vendor/model] · /model key [provider]",
924
+ login: "/model key [gateway | openrouter | codex | claude]",
747
925
  logout: "/logout [provider]",
748
926
  auth: "/auth [--check]",
749
- help: "/help [topic] · /help all · /help music|session|window|keys",
750
- guide: "/guide [topic] · /guide chords · F1",
927
+ help: "help [topic] · help all · help sound|voice|effects|rhythm|chords|mix|arrange|project|keys|agent",
928
+ guide: "/guide [topic] · /guide voice · F1 · the same topics as help",
751
929
  fx: "fx <effect> <param> <value> | on | off | preset <name> · fx delay mix 0.3",
752
930
  synth: "synth <param> <value> | preset <name> · synth lpf 1200",
753
931
  string:
@@ -756,6 +934,8 @@ export const USAGE: Readonly<Record<string, string>> = {
756
934
  "bowed [violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem] | <param> <value> | presets · bowed violin · bowed pressure 0.7",
757
935
  rig: "rig clean|crunch|punk|ragged|lead|metal|fuzz|octave|funk|wah|bachata|spring|bassdrive|reese|jangle|alt|shoegaze|glide|dreampop|swell|ebow | reset",
758
936
  guitar: `guitar tune ${GUITAR_TUNING_NAMES.join("|")} | E A D G B E · capo 0..12 · hand 3..6 · ring 0..1 · position · reset`,
937
+ progression:
938
+ "progression i7 IV7 i7 IV7 [each 8] [at 0] [bass] · numerals in the song key or symbols (Am7 D9) · block chords that hold for each span",
759
939
  strum:
760
940
  "strum G D Em C [folk|pop|punk|…] [strokes D-DU-UDU] [speed 22ms] [each 4] [at 0] · strum alone strums the track's chords",
761
941
  stomp:
@@ -778,14 +958,25 @@ export const USAGE: Readonly<Record<string, string>> = {
778
958
  modal:
779
959
  "modal <preset> | <body> | <param> <value> | mallet <name> | pair <track> | gamelan | reset | off | presets · modal vibes · modal gangsa · modal ring 3",
780
960
  wind: "wind <preset> | <param> <value> | mute <name> | reset | off | presets · wind flute · wind trumpet mute harmon · wind players 4",
781
- pack: "/pack list | info <name> | use <pack>/<sound> | add <url>",
961
+ formant:
962
+ "/formant <-12..12> [mix] | deep|giant|bright|tiny | on | off · /formant -4 · /formant 3 0.5",
963
+ vowel:
964
+ "/vowel <v> [<to> [<morph 0..1>]] | ee | to <v>|off | morph <0..1> | mix <0..1> | off · /vowel a o 0.5",
965
+ lyrics:
966
+ 'lyrics [bar] <syllables> | clear [bar] · lyrics "sun-lit morn-ing" (- splits, _ holds, ~ skips)',
967
+ sing: "sing <preset> | <param> <value> | drone <D3> | vowels a e i … | reset | off | presets · sing choir · sing khoomei drone D3 · sing vowel o voices 6",
968
+ vocoder:
969
+ "vocoder [preset] | src <track> | <param> <value|reset> | reset | off | presets · vocoder talkbox · vocoder src vox · vocoder formant +3 · vocoder gate auto · params: tap mode carrier follow root spread bands lo hi width attack release formant unvoiced sens hiss gate enhance depth freeze mix gain seed",
970
+ pack: "pack list | info <name> | use <pack>/<sound> | add <url>",
782
971
  kit: "/kit [name] · /kit syn909",
783
- euclid: "/euclid [voice] · euclid hat 7 16",
784
- menu: "/menu [sound|effects|rhythm|chords|mix|master|project|tuning]",
972
+ euclid: "euclid [drum] · euclid hat 7 16",
973
+ menu: "/menu [sound|voice|effects|rhythm|chords|mix|arrange|project|keys|agent] or any row name · /menu tuning · Ctrl-K",
974
+ style:
975
+ "style [list [id]|search <words>|info <id>|<id> [bars] [seed]|blend <a> <b> [w] [bars] [seed]|again] · style deep-house 16 · style blend bebop bossa-nova 0.3",
785
976
  master:
786
977
  "master <unit> on|off|preset <name>|<param> <value> · master streaming|club|loud · master target -14 · master measure · master off",
787
978
  try: "/try <sound command> · /try fx reverb mix 0.6",
788
- play: "/play [on|off|degrees|chromatic] · Ctrl-P · i toggles degrees",
979
+ play: "/play [on|off|degrees|in-key|chromatic] · ctrl-p play mode · i toggles degrees",
789
980
  meter:
790
981
  "meter <1..16> · meter 3 · meter 7/8 [at bar <n>] · meter remove bar <n> · meter clear",
791
982
  art: EXPRESSION_USAGE.art,
@@ -810,37 +1001,32 @@ export const USAGE: Readonly<Record<string, string>> = {
810
1001
  redo: "redo · Ctrl-Y",
811
1002
  };
812
1003
 
813
- /** Verbs a typo can be matched against, slash or bare as they are typed. */
1004
+ /** Verbs a typo can be matched against, without their slash. */
814
1005
  const KNOWN_VERBS: readonly string[] = [
815
1006
  ...new Set(
816
1007
  [
817
1008
  ...HELP_SECTIONS.flatMap((section) =>
818
- section.group === "keys"
819
- ? []
820
- : section.entries.map((entry) => entry.command.split(/[\s|[]/)[0]!),
1009
+ section.entries.map((entry) => entry.command.split(/[\s|[]/)[0]!),
821
1010
  ),
822
1011
  ...Object.values(USAGE).map((usage) => usage.split(/[\s|[]/)[0]!),
823
- ].filter((verb) => /^\/?[a-z][\w-]*$/i.test(verb)),
1012
+ ]
1013
+ .map((verb) => verb.replace(/^\//, ""))
1014
+ .filter((verb) => /^[a-z][\w-]*$/i.test(verb)),
824
1015
  ),
825
1016
  ];
826
1017
 
827
- function editDistance(a: string, b: string): number {
828
- const row = Array.from({ length: b.length + 1 }, (_, index) => index);
829
- for (let i = 1; i <= a.length; i += 1) {
830
- let previous = row[0]!;
831
- row[0] = i;
832
- for (let j = 1; j <= b.length; j += 1) {
833
- const current = row[j]!;
834
- row[j] = Math.min(
835
- row[j]! + 1,
836
- row[j - 1]! + 1,
837
- previous + (a[i - 1] === b[j - 1] ? 0 : 1),
838
- );
839
- previous = current;
840
- }
841
- }
842
- return row[b.length]!;
843
- }
1018
+ /** Window verbs whose free text would otherwise read as a typo fix. */
1019
+ const SLASH_ONLY: ReadonlySet<string> = new Set([
1020
+ "rename",
1021
+ "fork",
1022
+ "resume",
1023
+ "login",
1024
+ "logout",
1025
+ "auth",
1026
+ "bpm",
1027
+ ]);
1028
+
1029
+ export { editDistance } from "./nearest.ts";
844
1030
 
845
1031
  /**
846
1032
  * The known command nearest to the first word of `command` (`/clik` →
@@ -848,19 +1034,25 @@ function editDistance(a: string, b: string): number {
848
1034
  */
849
1035
  export function nearestCommand(command: string): string | undefined {
850
1036
  const word = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
1037
+ const slash = word.startsWith("/");
851
1038
  const bare = word.replace(/^\//, "");
852
1039
  if (!bare) return undefined;
853
1040
  let best: { verb: string; distance: number; score: number } | undefined;
854
1041
  for (const verb of KNOWN_VERBS) {
855
- const distance = editDistance(bare, verb.replace(/^\//, ""));
856
- if (distance === 0 && verb === word) continue;
857
- // Ties go to the form typed: `/patern` → `/pattern`, not `pattern`.
858
- const sameForm = verb.startsWith("/") === word.startsWith("/");
859
- const score = distance * 2 + (sameForm ? 0 : 1);
1042
+ const distance = editDistance(bare, verb);
1043
+ if (distance === 0 && !slash) continue;
1044
+ // Ties go to a dropped letter (`/clik` → `/click`, not `/clip`), the
1045
+ // commonest slip, then a changed one, then an extra one.
1046
+ const shape =
1047
+ verb.length > bare.length ? 0 : verb.length === bare.length ? 1 : 2;
1048
+ const score = distance * 3 + shape;
860
1049
  if (!best || score < best.score) best = { verb, distance, score };
861
1050
  }
862
1051
  const limit = bare.length <= 4 ? 1 : 2;
863
- return best && best.distance <= limit ? best.verb : undefined;
1052
+ if (!best || best.distance > limit) return undefined;
1053
+ // Answer in the form typed (`/clik` → `/click`), except an exact word
1054
+ // that failed with a slash (`/fx delay on` → `fx`): its slash was wrong.
1055
+ return slash && best.distance > 0 ? `/${best.verb}` : best.verb;
864
1056
  }
865
1057
 
866
1058
  /**
@@ -877,7 +1069,7 @@ export function typoFix(
877
1069
  const [first, ...rest] = text.split(/\s+/);
878
1070
  const word = first!.toLowerCase();
879
1071
  for (const verb of KNOWN_VERBS) {
880
- if (verb.startsWith("/") || verb === word) continue;
1072
+ if (SLASH_ONLY.has(verb) || verb === word) continue;
881
1073
  if (editDistance(word, verb) !== 1) continue;
882
1074
  const candidate = [verb, ...rest].join(" ");
883
1075
  if (parses(candidate)) return candidate;
@@ -917,7 +1109,18 @@ export function usageHint(command: string): string | undefined {
917
1109
  !words.slice(1).some((word) => /\d/.test(word) || ARRANGE_WORDS.has(word))
918
1110
  )
919
1111
  return undefined;
920
- return USAGE[verb];
1112
+ const usage = USAGE[verb];
1113
+ return usage === undefined ? undefined : usageForm(usage);
1114
+ }
1115
+
1116
+ /**
1117
+ * Every usage line reads `usage · <cmd> <args>`; a range refusal keeps
1118
+ * its own template (`pan takes -1…1 · pan -0.5`).
1119
+ */
1120
+ export function usageForm(usage: string): string {
1121
+ return / takes /.test(usage) || usage.startsWith("usage · ")
1122
+ ? usage
1123
+ : `usage · ${usage}`;
921
1124
  }
922
1125
 
923
1126
  const ARRANGE_VERBS: ReadonlySet<string> = new Set([
@@ -944,3 +1147,30 @@ const ARRANGE_WORDS: ReadonlySet<string> = new Set([
944
1147
  "bake",
945
1148
  "off",
946
1149
  ]);
1150
+
1151
+ /** The overlay title for `/help [topic]`: an alias shows its topic. */
1152
+ export function helpTitle(topic: string | undefined): string {
1153
+ const name = topic?.trim().toLowerCase().replace(/^\//, "");
1154
+ if (!name) return "help";
1155
+ const spelled = HELP_COMMAND_ALIASES[name];
1156
+ if (spelled) return `help · ${spelled}`;
1157
+ const id = (TOPICS as readonly string[]).includes(name)
1158
+ ? name
1159
+ : USAGE[name] || commandHelpExists(name)
1160
+ ? name
1161
+ : (resolveTopic(name) ?? name);
1162
+ return `help · ${id}`;
1163
+ }
1164
+
1165
+ function commandHelpExists(name: string): boolean {
1166
+ return HELP_SECTIONS.some((section) =>
1167
+ section.entries.some((entry) =>
1168
+ entry.command.replace(/^\//, "").startsWith(`${name} `),
1169
+ ),
1170
+ );
1171
+ }
1172
+
1173
+ /** `no topic X · did you mean Y · /help`, for `/help X` with no page. */
1174
+ export function helpMiss(topic: string): string {
1175
+ return topicMiss(topic, ["all"]);
1176
+ }