@hraness/dawg 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  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 +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Tool parameter schemas as every provider accepts them.
3
+ *
4
+ * dawg writes unions as `type: ["array", "string"]` with `items` beside it.
5
+ * OpenAI and Anthropic accept that, but Gemini (through the AI Gateway)
6
+ * rewrites the union to `anyOf` and leaves `items` outside, then rejects the
7
+ * whole request, so no Gemini model could run a turn. Here each union
8
+ * becomes an `anyOf` whose branches carry only their own keywords, and an
9
+ * array branch always has `items`.
10
+ */
11
+
12
+ type Schema = Record<string, unknown>;
13
+
14
+ /** Keywords that belong to one JSON type's branch of a union. */
15
+ const BRANCH_KEYWORDS: Readonly<Record<string, readonly string[]>> = {
16
+ array: ["items", "minItems", "maxItems"],
17
+ object: ["properties", "required", "additionalProperties"],
18
+ string: ["enum", "minLength", "maxLength", "pattern"],
19
+ number: ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"],
20
+ integer: ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"],
21
+ };
22
+ const ALL_BRANCH_KEYWORDS = new Set(Object.values(BRANCH_KEYWORDS).flat());
23
+
24
+ /** Items for an array branch that declared none: any scalar. */
25
+ const ANY_SCALAR_ITEMS: Schema = {
26
+ anyOf: [{ type: "number" }, { type: "string" }, { type: "boolean" }],
27
+ };
28
+
29
+ function isSchema(value: unknown): value is Schema {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ function branch(type: string, schema: Schema): Schema {
34
+ const out: Schema = { type };
35
+ for (const key of BRANCH_KEYWORDS[type] ?? []) {
36
+ if (!(key in schema)) continue;
37
+ const value = schema[key];
38
+ // An enum only constrains the branch whose type its values have.
39
+ if (key === "enum" && Array.isArray(value)) {
40
+ const kept = value.filter((v) => typeof v === "string");
41
+ if (kept.length > 0) out.enum = kept;
42
+ continue;
43
+ }
44
+ out[key] = value;
45
+ }
46
+ if (type === "array" && out.items === undefined) out.items = ANY_SCALAR_ITEMS;
47
+ return portableSchema(out);
48
+ }
49
+
50
+ /** A copy of `schema` with every type union rewritten as `anyOf`. */
51
+ export function portableSchema(schema: unknown): Schema {
52
+ if (!isSchema(schema)) return schema as Schema;
53
+ const out: Schema = {};
54
+ for (const [key, value] of Object.entries(schema)) {
55
+ if (key === "properties" && isSchema(value)) {
56
+ out.properties = Object.fromEntries(
57
+ Object.entries(value).map(([name, sub]) => [name, portableSchema(sub)]),
58
+ );
59
+ } else if (
60
+ (key === "items" || key === "additionalProperties") &&
61
+ isSchema(value)
62
+ ) {
63
+ out[key] = portableSchema(value);
64
+ } else if ((key === "anyOf" || key === "oneOf") && Array.isArray(value)) {
65
+ out[key] = value.map(portableSchema);
66
+ } else {
67
+ out[key] = value;
68
+ }
69
+ }
70
+ if (Array.isArray(out.type)) {
71
+ const types = out.type as string[];
72
+ const rest: Schema = {};
73
+ for (const [key, value] of Object.entries(out))
74
+ if (key !== "type" && !ALL_BRANCH_KEYWORDS.has(key)) rest[key] = value;
75
+ return { ...rest, anyOf: types.map((type) => branch(type, out)) };
76
+ }
77
+ if (out.type === "array" && out.items === undefined)
78
+ out.items = ANY_SCALAR_ITEMS;
79
+ return out;
80
+ }
@@ -23,6 +23,7 @@ import {
23
23
  } from "../audio/preview.ts";
24
24
  import { renderScorePcm, type RenderedAudio } from "../audio/wav.ts";
25
25
  import type { ScoreMeasurement } from "../audio/measure.ts";
26
+ import { VOICE_PREVIEWABLE_TOOLS } from "./voice-tools.ts";
26
27
  import type {
27
28
  ActionContext,
28
29
  AgentTool,
@@ -48,6 +49,8 @@ export const PREVIEWABLE_TOOLS: readonly string[] = Object.freeze([
48
49
  "set_granular",
49
50
  "set_drum_kit",
50
51
  "use_sound",
52
+ // 0.7 Voice: flagged per lane in voice-tools.ts.
53
+ ...VOICE_PREVIEWABLE_TOOLS,
51
54
  ]);
52
55
 
53
56
  const MAX_CHANGES = 8;
@@ -160,7 +163,7 @@ export function previewSoundTool(
160
163
  return {
161
164
  name: "preview_sound",
162
165
  description:
163
- "Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample, set_effects, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
166
+ "Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
164
167
  parameters: {
165
168
  type: "object",
166
169
  properties: {
@@ -1,3 +1,4 @@
1
+ import { runCommandAgentTurn } from "./command-agent.ts";
1
2
  import { statSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import {
@@ -29,6 +30,7 @@ import {
29
30
  } from "./gateway.ts";
30
31
  import {
31
32
  defaultModelId,
33
+ fallbackModelId,
32
34
  MODEL_CATALOG,
33
35
  modelAlias,
34
36
  resolveModelChoice,
@@ -90,7 +92,7 @@ export function isApiSelection(
90
92
  return selection.kind === "gateway" || selection.kind === "openrouter";
91
93
  }
92
94
 
93
- export const LOGIN_HINT = "run `dawg login` to enable the agent";
95
+ export const LOGIN_HINT = "run `dawg model key` to enable the agent";
94
96
  /** The small, cheap model used for one-line helpers such as session names. */
95
97
  export const GATEWAY_SMALL_MODEL = "anthropic/claude-haiku-4.5";
96
98
  export const OPENROUTER_SMALL_MODEL = "anthropic/claude-haiku-4.5";
@@ -113,7 +115,7 @@ export function providerChoice(
113
115
 
114
116
  /**
115
117
  * The model for a key-based provider: `DAWG_MODEL`, then the model saved by
116
- * `/model` or `dawg login`, then Opus 5.5. Throws on an unknown `DAWG_MODEL`.
118
+ * `/model` or `dawg model key`, then Opus 5.5. Throws on an unknown `DAWG_MODEL`.
117
119
  */
118
120
  export function apiModelId(
119
121
  service: ApiProvider,
@@ -186,14 +188,14 @@ export async function selectProvider(
186
188
  }
187
189
  if (choice === service)
188
190
  return offline(
189
- `no ${service === "gateway" ? "AI Gateway" : "OpenRouter"} key any more; run \`dawg login ${service}\``,
191
+ `no ${service === "gateway" ? "AI Gateway" : "OpenRouter"} key any more; run \`dawg model key ${service}\``,
190
192
  );
191
193
  }
192
194
  const bin = resolveXcbBin(auth.env, auth.runner);
193
195
  if (!bin)
194
196
  return offline(
195
197
  explicit
196
- ? `xcb is not installed; run \`dawg login ${choice === "xcb" ? "--xcb" : choice}\``
198
+ ? `xcb is not installed; run \`dawg model key ${choice === "xcb" ? "--xcb" : choice}\``
197
199
  : `no model configured; ${LOGIN_HINT}`,
198
200
  );
199
201
  let capabilities: XcbCapabilities;
@@ -251,7 +253,7 @@ export async function selectProvider(
251
253
  "<account>",
252
254
  account.id,
253
255
  )
254
- }; run \`dawg login ${family === "xcb" ? "--xcb" : family}\``,
256
+ }; run \`dawg model key ${family === "xcb" ? "--xcb" : family}\``,
255
257
  );
256
258
  }
257
259
  // No saved pick: the first usable account, ready before admission pending.
@@ -274,7 +276,7 @@ export async function selectProvider(
274
276
  }
275
277
  return offline(
276
278
  explicit
277
- ? `no ${FAMILY_NAMES[choice] ?? choice} is ready; run \`dawg login ${choice === "xcb" ? "--xcb" : choice}\``
279
+ ? `no ${FAMILY_NAMES[choice] ?? choice} is ready; run \`dawg model key ${choice === "xcb" ? "--xcb" : choice}\``
278
280
  : `no model configured; ${LOGIN_HINT}`,
279
281
  );
280
282
  }
@@ -349,12 +351,24 @@ export async function runProviderTurn(
349
351
  ...(options.signal ? { signal: options.signal } : {}),
350
352
  ...(options.budget ? { budget: options.budget } : {}),
351
353
  };
352
- if (isApiSelection(selection))
354
+ if (isApiSelection(selection)) {
355
+ const model = options.model ?? selection.modelId;
356
+ const fallback = fallbackModelId(selection.kind, model);
357
+ if (options.host.commands && !options.model)
358
+ return runCommandAgentTurn({
359
+ ...common,
360
+ model,
361
+ ...(fallback ? { fallbackModel: fallback } : {}),
362
+ client: options.gatewayClient ?? apiClient(selection),
363
+ commands: options.host.commands,
364
+ });
353
365
  return runAgentTurn({
354
366
  ...common,
355
- model: options.model ?? selection.modelId,
367
+ model,
368
+ ...(fallback ? { fallbackModel: fallback } : {}),
356
369
  client: options.gatewayClient ?? apiClient(selection),
357
370
  });
371
+ }
358
372
  if (selection.kind === "xcb") {
359
373
  const runner = options.runner ?? systemRunner;
360
374
  return runTextAgentTurn({
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Agent tool for range edits (op1-ux §6.4): `edit_range` builds the same
3
+ * line the prompt runs (`loop 5-6`, `copy bass 5-6 to 7 x2`, `bars insert
4
+ * 2 at 3`, `section split chorus at 13`) and applies it through
5
+ * src/commands/range.ts or src/commands/arrange.ts, so the tool, the typed
6
+ * command and the menu agree. Its operations are the score diff.
7
+ *
8
+ * Bars count from 1, like the prompt and the bar ruler.
9
+ */
10
+ import { diffScores } from "../../core/diff.ts";
11
+ import { SCORE_LIMITS, type TrackScore } from "../../core/score.ts";
12
+ import {
13
+ applySectionCommand,
14
+ loopSpan,
15
+ parseSectionCommand,
16
+ } from "../commands/arrange.ts";
17
+ import { parseLoopCommand } from "../commands/grammar.ts";
18
+ import { applyRangeCommand, parseRangeCommand } from "../commands/range.ts";
19
+ import type { AgentTool, ToolContext, ToolPlan } from "./tools.ts";
20
+ import { ToolArgumentError } from "./tool-error.ts";
21
+
22
+ export const RANGE_ACTIONS = [
23
+ "loop",
24
+ "unloop",
25
+ "copy",
26
+ "move",
27
+ "clear",
28
+ "reverse",
29
+ "insert_bars",
30
+ "remove_bars",
31
+ "split",
32
+ "join",
33
+ ] as const;
34
+
35
+ type RangeAction = (typeof RANGE_ACTIONS)[number];
36
+
37
+ const bar = (description: string) =>
38
+ ({
39
+ type: "integer",
40
+ minimum: 1,
41
+ maximum: SCORE_LIMITS.maxBars,
42
+ description,
43
+ }) as const;
44
+
45
+ function whole(value: unknown, what: string, max: number): number {
46
+ if (
47
+ typeof value !== "number" ||
48
+ !Number.isInteger(value) ||
49
+ value < 1 ||
50
+ value > max
51
+ )
52
+ throw new ToolArgumentError(`${what} must be a whole number 1..${max}`);
53
+ return value;
54
+ }
55
+
56
+ function word(value: unknown, what: string): string {
57
+ if (typeof value !== "string" || !/^[\w .'-]{1,64}$/u.test(value.trim()))
58
+ throw new ToolArgumentError(`${what} must be a name`);
59
+ return value.trim();
60
+ }
61
+
62
+ /** `5-6`, `5`, or a section name: the range words of the typed command. */
63
+ function rangeWords(args: Record<string, unknown>): string {
64
+ if (args.section !== undefined) return word(args.section, "section");
65
+ if (args.fromBar === undefined)
66
+ throw new ToolArgumentError("give fromBar (and toBar) or section");
67
+ const from = whole(args.fromBar, "fromBar", SCORE_LIMITS.maxBars);
68
+ const to =
69
+ args.toBar === undefined
70
+ ? from
71
+ : whole(args.toBar, "toBar", SCORE_LIMITS.maxBars);
72
+ if (to < from) throw new ToolArgumentError("toBar comes at or after fromBar");
73
+ return from === to ? `${from}` : `${from}-${to}`;
74
+ }
75
+
76
+ function trackWord(
77
+ args: Record<string, unknown>,
78
+ context: ToolContext,
79
+ ): string {
80
+ if (args.allTracks === true) return "all";
81
+ const id =
82
+ args.trackId === undefined
83
+ ? context.focusedTrackId
84
+ : word(args.trackId, "trackId");
85
+ if (!context.score.tracks.some((track) => track.id === id))
86
+ throw new ToolArgumentError(`no track ${id}`);
87
+ return id;
88
+ }
89
+
90
+ /** The typed command a call stands for (what show-me would type). */
91
+ export function rangeToolCommand(
92
+ args: Record<string, unknown>,
93
+ context: ToolContext,
94
+ ): string {
95
+ const action = args.action as RangeAction;
96
+ if (!RANGE_ACTIONS.includes(action))
97
+ throw new ToolArgumentError(`action must be ${RANGE_ACTIONS.join(", ")}`);
98
+ switch (action) {
99
+ case "loop":
100
+ return `loop ${rangeWords(args)}`;
101
+ case "unloop":
102
+ return "loop off";
103
+ case "copy":
104
+ case "move": {
105
+ if (args.toBar === undefined && args.atBar === undefined)
106
+ throw new ToolArgumentError(`${action} needs atBar, where it lands`);
107
+ const at = whole(args.atBar, "atBar", SCORE_LIMITS.maxBars);
108
+ const times =
109
+ action === "copy" && args.times !== undefined
110
+ ? ` x${whole(args.times, "times", 64)}`
111
+ : "";
112
+ const mode =
113
+ args.insert === true
114
+ ? " insert"
115
+ : action === "copy" && args.merge === true
116
+ ? " merge"
117
+ : "";
118
+ return `${action} ${trackWord(args, context)} ${rangeWords(args)} to ${at}${times}${mode}`;
119
+ }
120
+ case "clear":
121
+ case "reverse":
122
+ return `${action} ${trackWord(args, context)} ${rangeWords(args)}`;
123
+ case "insert_bars":
124
+ return `bars insert ${whole(args.bars, "bars", SCORE_LIMITS.maxBars)} at ${whole(args.atBar, "atBar", SCORE_LIMITS.maxBars)}`;
125
+ case "remove_bars":
126
+ return `bars remove ${rangeWords(args)}`;
127
+ case "split":
128
+ return `section split ${word(args.section, "section")} at ${whole(args.atBar, "atBar", SCORE_LIMITS.maxBars)}`;
129
+ case "join":
130
+ return `section join ${word(args.section, "section")}`;
131
+ }
132
+ }
133
+
134
+ function run(
135
+ line: string,
136
+ context: ToolContext,
137
+ ): { next?: TrackScore; message: string } {
138
+ const score = context.score;
139
+ const loop = parseLoopCommand(line);
140
+ if (loop?.type === "loop-bars") {
141
+ const result = loopSpan(score, loop.from, loop.to);
142
+ if (!result.ok) throw new ToolArgumentError(result.message);
143
+ return { next: result.next, message: result.message };
144
+ }
145
+ if (loop?.type === "loop-off")
146
+ return score.loop
147
+ ? { next: score.withLoop(null), message: "loop · off · playing the song" }
148
+ : run("section loop off", context);
149
+ if (loop?.type === "loop-section")
150
+ return run(`section loop ${loop.name}`, context);
151
+ const range = parseRangeCommand(line, score);
152
+ if (range) {
153
+ const result = applyRangeCommand(
154
+ score,
155
+ { trackId: context.focusedTrackId, playheadBar: 0 },
156
+ range,
157
+ );
158
+ if (!result.ok) throw new ToolArgumentError(result.message);
159
+ return {
160
+ ...(result.next ? { next: result.next } : {}),
161
+ message: result.message,
162
+ };
163
+ }
164
+ const section = parseSectionCommand(line, score);
165
+ if (!section) throw new ToolArgumentError(`cannot run: ${line}`);
166
+ const result = applySectionCommand(score, context.focusedTrackId, section);
167
+ if (!result.ok) throw new ToolArgumentError(result.message);
168
+ return {
169
+ ...(result.next ? { next: result.next } : {}),
170
+ message: result.message,
171
+ };
172
+ }
173
+
174
+ export const RANGE_TOOLS: readonly AgentTool[] = Object.freeze([
175
+ {
176
+ name: "edit_range",
177
+ description:
178
+ "Edit bar ranges (bars count from 1; a range is fromBar..toBar or a section name). loop sets the loop range playback cycles (no section is made); unloop plays the song. copy copies a track's (or allTracks') notes, automation and clips in the range to atBar, overwriting there (merge keeps what is there; insert shifts later music right), times tiles it end to end. move is copy plus clearing the source. clear empties the range (the bars stay). reverse mirrors it in time. insert_bars adds `bars` empty bars at atBar and moves later sections, clips, automation and tempo points right; remove_bars cuts the range out. split cuts a section in two at atBar; join merges a section with the one after it. Each call is one undo step and runs the same command the prompt does.",
179
+ parameters: {
180
+ type: "object",
181
+ properties: {
182
+ action: { type: "string", enum: [...RANGE_ACTIONS] },
183
+ fromBar: bar("first bar of the range"),
184
+ toBar: bar("last bar of the range (default fromBar)"),
185
+ section: {
186
+ type: "string",
187
+ maxLength: 32,
188
+ description: "a section as the range (split/join: the section)",
189
+ },
190
+ atBar: bar("copy/move: where it lands; insert_bars/split: the bar"),
191
+ bars: bar("insert_bars: how many"),
192
+ times: { type: "integer", minimum: 1, maximum: 64 },
193
+ trackId: {
194
+ type: "string",
195
+ maxLength: SCORE_LIMITS.maxIdLength,
196
+ description: "the track (default: the focused track)",
197
+ },
198
+ allTracks: { type: "boolean" },
199
+ insert: { type: "boolean" },
200
+ merge: { type: "boolean" },
201
+ },
202
+ required: ["action"],
203
+ additionalProperties: false,
204
+ },
205
+ plan(args, context): ToolPlan {
206
+ const line = rangeToolCommand(args, context);
207
+ const result = run(line, context);
208
+ return {
209
+ kind: "score",
210
+ operations: result.next ? diffScores(context.score, result.next) : [],
211
+ trackId: context.focusedTrackId,
212
+ summary: result.message,
213
+ };
214
+ },
215
+ },
216
+ ]);
@@ -75,7 +75,7 @@ export const RHYTHM_TOOLS: readonly AgentTool[] = Object.freeze([
75
75
  {
76
76
  name: "set_rhythm",
77
77
  description:
78
- "Preferred for drums: one row per voice of a kit/oneshot track; its notes regenerate from the row. pulses (4) over steps (16..64) Euclidean, rotate later; division 1/32..1/1 (1/16); grid 'x.X.' explicit (X accent); repeats 0..16 after each pulse every time, pace -1..1, ramp -1..1; velocity (0.8); accent 0..1 on E(accents,pulses); gate; legato; probability+seed; swing/nudge ±0.5 step; cycles [{pulses,rotate,repeats,probability,velocity}] per pass. remove [voices] (freeze keeps notes).",
78
+ "Preferred for drums: one row per voice of a kit/oneshot track; its notes regenerate from the row. pulses (4) over steps (16..64) Euclidean, rotate later; division 1/32..1/1 (1/16) is one step, and the steps×division cycle repeats from beat 0 to fill the loop (E(4,16) at 1/16 = a hit every beat, four on the floor; a backbeat on counts 2 and 4 is grid '....x.......x...' or E(2,16) rotate 4; offbeat 'and's are grid '..x.' ); grid 'x.X.' explicit (X accent); repeats 0..16 after each pulse every time, pace -1..1, ramp -1..1; velocity (0.8); accent 0..1 on E(accents,pulses); gate; legato; probability+seed; swing/nudge ±0.5 step; cycles [{pulses,rotate,repeats,probability,velocity}] per pass. remove [voices] (freeze keeps notes).",
79
79
  parameters: {
80
80
  type: "object",
81
81
  properties: {
@@ -380,7 +380,7 @@ export const SECTION_TOOLS: readonly AgentTool[] = Object.freeze([
380
380
  },
381
381
  {
382
382
  name: "add_transition",
383
- description: `Generate a transition with the built-in voices (bars count from 1). build: noise riser, accelerating snare roll, filter sweep and uplifter (all on by default), landing on the bar after its range. The range is \`bars\` bars (default 4) just before section \`into\` (the usual build into a drop or chorus); or a section's own bars (its last \`bars\` bars when bars is given); or \`bars\` bars starting at atBar; default the song's last 4 bars. The sweep is a high-pass up to 1.2 kHz on unfiltered or high-pass pitched tracks and a low-pass opening from cutoff/10 on low-pass tracks, on an octave curve, snapping back on the downbeat. drop: a pre-drop cut (cutBeats of silence, default 1, up to two bars) and an impact hit on the section's first bar or atBar (default: the section named drop, else chorus). fill: a drum fill (${FILL_STYLES.join(", ")}) in the last beats (default 1, ${FILL_BEATS_MIN} up to two bars) leading into the section or into atBar, plus a crash on its downbeat. Without section or atBar, fills go at every section boundary. Out-of-range values are clamped and the result says so.`,
383
+ description: `Generate a transition with the built-in voices (bars count from 1). build: noise riser, accelerating snare roll, filter sweep and uplifter (all on by default), landing on the bar after its range. The range is \`bars\` bars (default 4) just before section \`into\` (the usual build into a drop or chorus); or a section's own bars (its last \`bars\` bars when bars is given); or \`bars\` bars starting at atBar; default the song's last 4 bars. The sweep is a high-pass rising to 1.2 kHz on pitched tracks (a low-pass opening from cutoff/10 on low-pass tracks), snapping back on the downbeat. drop: a pre-drop cut (cutBeats of silence, default 1, up to two bars) and an impact hit on the section's first bar or atBar (default: the section named drop, else chorus). fill: a drum fill (${FILL_STYLES.join(", ")}) in the last beats (default 1, ${FILL_BEATS_MIN} up to two bars) leading into the section or into atBar, plus a crash on its downbeat. Without section or atBar, fills go at every section boundary. Out-of-range values are clamped.`,
384
384
  parameters: {
385
385
  type: "object",
386
386
  properties: {