@hraness/dawg 0.0.0-stage → 0.3.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 (113) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +327 -0
  3. package/LICENSE +21 -0
  4. package/README.md +213 -2
  5. package/core/diff.ts +249 -0
  6. package/core/drums.ts +102 -0
  7. package/core/key.ts +43 -0
  8. package/core/loop.ts +78 -0
  9. package/core/pitch.ts +60 -0
  10. package/core/score.ts +1388 -0
  11. package/core/sdk/eval-child.ts +113 -0
  12. package/core/sdk/eval.ts +257 -0
  13. package/core/sdk/print.ts +393 -0
  14. package/core/sdk/v1.ts +954 -0
  15. package/core/slug.ts +19 -0
  16. package/package.json +45 -4
  17. package/src/agent/agent.ts +853 -0
  18. package/src/agent/brief.ts +160 -0
  19. package/src/agent/gateway.ts +441 -0
  20. package/src/agent/models.ts +633 -0
  21. package/src/agent/ops.ts +157 -0
  22. package/src/agent/planner.ts +259 -0
  23. package/src/agent/provider.ts +454 -0
  24. package/src/agent/sse.ts +114 -0
  25. package/src/agent/tools.ts +1373 -0
  26. package/src/agent/usage.ts +296 -0
  27. package/src/agent/workspace.ts +683 -0
  28. package/src/agent/xcb-agent.ts +262 -0
  29. package/src/agent/xcb.ts +579 -0
  30. package/src/audio/click.ts +125 -0
  31. package/src/audio/clock.ts +68 -0
  32. package/src/audio/engine.ts +841 -0
  33. package/src/audio/live.ts +152 -0
  34. package/src/audio/lock.ts +57 -0
  35. package/src/audio/player.ts +134 -0
  36. package/src/audio/render-worker.ts +68 -0
  37. package/src/audio/renderer.ts +174 -0
  38. package/src/audio/sampler.ts +292 -0
  39. package/src/audio/samples.ts +683 -0
  40. package/src/audio/wav.ts +861 -0
  41. package/src/auth/cli.ts +231 -0
  42. package/src/auth/credentials.ts +411 -0
  43. package/src/auth/discover.ts +481 -0
  44. package/src/auth/login.ts +1191 -0
  45. package/src/auth/openrouter.ts +206 -0
  46. package/src/auth/picker.ts +282 -0
  47. package/src/auth/runner.ts +207 -0
  48. package/src/auth/tui.ts +107 -0
  49. package/src/commands/edit.ts +170 -0
  50. package/src/commands/help.ts +247 -0
  51. package/src/commands/history.ts +69 -0
  52. package/src/commands/music.ts +461 -0
  53. package/src/commands/sample.ts +302 -0
  54. package/src/daemon.ts +31 -0
  55. package/src/main.ts +2209 -0
  56. package/src/media/analyze.ts +364 -0
  57. package/src/media/backend.ts +253 -0
  58. package/src/media/cli.ts +173 -0
  59. package/src/media/download.ts +281 -0
  60. package/src/media/dsp.ts +281 -0
  61. package/src/media/import.ts +130 -0
  62. package/src/media/lyrics.ts +201 -0
  63. package/src/media/notes.ts +363 -0
  64. package/src/media/paths.ts +168 -0
  65. package/src/media/process.ts +226 -0
  66. package/src/media/registry.ts +9 -0
  67. package/src/media/sidecar.ts +72 -0
  68. package/src/media/stemdeck.ts +254 -0
  69. package/src/media/stems.ts +173 -0
  70. package/src/media/tools.ts +292 -0
  71. package/src/media/types.ts +92 -0
  72. package/src/media/vendor/basic-pitch.ts +261 -0
  73. package/src/media/vendor/drums.ts +817 -0
  74. package/src/media/vendor/grid.ts +203 -0
  75. package/src/media/vendor/util.ts +139 -0
  76. package/src/media/vendor/wav.ts +233 -0
  77. package/src/project/check.ts +80 -0
  78. package/src/project/init.ts +253 -0
  79. package/src/project/sync.ts +432 -0
  80. package/src/project/typecheck.ts +149 -0
  81. package/src/render.ts +121 -0
  82. package/src/session/attach.ts +181 -0
  83. package/src/session/client.ts +498 -0
  84. package/src/session/daemon.ts +740 -0
  85. package/src/session/delta.ts +249 -0
  86. package/src/session/list.ts +180 -0
  87. package/src/session/lock.ts +92 -0
  88. package/src/session/meta.ts +253 -0
  89. package/src/session/naming.ts +430 -0
  90. package/src/session/port.ts +481 -0
  91. package/src/session/presence.ts +159 -0
  92. package/src/session/protocol.ts +618 -0
  93. package/src/session/rebase.ts +168 -0
  94. package/src/session/store.ts +581 -0
  95. package/src/tui/menu.ts +1083 -0
  96. package/src/tui/play-mode.ts +442 -0
  97. package/src/tui/play-session.ts +636 -0
  98. package/src/web/fetch.ts +340 -0
  99. package/src/web/http.ts +137 -0
  100. package/src/web/search.ts +681 -0
  101. package/tui/activity.ts +364 -0
  102. package/tui/app.ts +1372 -0
  103. package/tui/drums.ts +65 -0
  104. package/tui/highway.ts +921 -0
  105. package/tui/input.ts +63 -0
  106. package/tui/keys.ts +102 -0
  107. package/tui/layers.ts +80 -0
  108. package/tui/play-strip.ts +143 -0
  109. package/tui/prompt.ts +609 -0
  110. package/tui/render.ts +124 -0
  111. package/tui/screen.ts +247 -0
  112. package/tui/text.ts +72 -0
  113. package/tui/theme.ts +451 -0
@@ -0,0 +1,157 @@
1
+ import { pitchToMidi } from "../../core/pitch.ts";
2
+ import { SCORE_LIMITS } from "../../core/score.ts";
3
+
4
+ export { pitchToMidi };
5
+
6
+ export type AgentOperation =
7
+ | { type: "add-track"; trackId: string }
8
+ | {
9
+ type: "add-note";
10
+ pitch: number;
11
+ start: number;
12
+ duration: number;
13
+ velocity: number;
14
+ }
15
+ | { type: "remove-note"; noteId: string }
16
+ | {
17
+ type: "update-note";
18
+ noteId: string;
19
+ patch: {
20
+ start?: number;
21
+ duration?: number;
22
+ pitch?: number;
23
+ velocity?: number;
24
+ };
25
+ }
26
+ | { type: "set-tempo"; tempoBpm: number }
27
+ | { type: "set-bars"; bars: number }
28
+ | { type: "extend-bars"; bars: number }
29
+ | {
30
+ type: "track";
31
+ patch: {
32
+ instrument?: string;
33
+ muted?: boolean;
34
+ volume?: number;
35
+ pan?: number;
36
+ name?: string;
37
+ };
38
+ }
39
+ | {
40
+ type: "automation";
41
+ parameter: "volume" | "pan";
42
+ points: readonly { beat: number; value: number }[];
43
+ }
44
+ | { type: "clear-track" }
45
+ | { type: "transport"; action: "play" | "pause" | "toggle" };
46
+
47
+ export function parsePrompt(prompt: string): AgentOperation | undefined {
48
+ const text = prompt.trim().toLowerCase();
49
+ if (/^(play|start)\b/.test(text))
50
+ return { type: "transport", action: "play" };
51
+ if (/^(pause|stop)\b/.test(text))
52
+ return { type: "transport", action: "pause" };
53
+ if (/^toggle\b/.test(text)) return { type: "transport", action: "toggle" };
54
+ const track = text.match(/^(?:add\s+)?track\s+([a-z0-9._-]{1,64})$/);
55
+ if (track) return { type: "add-track", trackId: track[1]! };
56
+ const bars = text.match(/^bars\s+(\d+)$/);
57
+ const extension = text.match(/^extend\s+(\d+)\s+bars?$/);
58
+ if (bars || extension) {
59
+ const count = Number((bars ?? extension)![1]);
60
+ if (Number.isInteger(count) && count >= 1 && count <= SCORE_LIMITS.maxBars)
61
+ return { type: bars ? "set-bars" : "extend-bars", bars: count };
62
+ }
63
+ const tempo = text.match(/^(?:tempo|bpm)\s+(\d+(?:\.\d+)?)\s*$/);
64
+ if (tempo) {
65
+ const tempoBpm = Number(tempo[1]);
66
+ if (Number.isFinite(tempoBpm)) return { type: "set-tempo", tempoBpm };
67
+ }
68
+ if (/^(?:clear|clear\s+track|remove\s+all)$/.test(text))
69
+ return { type: "clear-track" };
70
+ const instrument = text.match(
71
+ /^(?:instrument|sound|voice)\s+([a-z0-9._ -]{1,64})$/,
72
+ );
73
+ if (instrument)
74
+ return { type: "track", patch: { instrument: instrument[1]!.trim() } };
75
+ if (/^(?:mute|silence)\b/.test(text))
76
+ return { type: "track", patch: { muted: true } };
77
+ if (/^(?:unmute|unsilence)\b/.test(text))
78
+ return { type: "track", patch: { muted: false } };
79
+ const volume = text.match(/^(?:volume|vol)\s+(0(?:\.\d+)?|1(?:\.0+)?)$/);
80
+ if (volume) return { type: "track", patch: { volume: Number(volume[1]) } };
81
+ const clearAutomation = text.match(
82
+ /^(?:clear|reset)\s+(?:(volume|pan)\s+)?automation$/,
83
+ );
84
+ if (clearAutomation)
85
+ return {
86
+ type: "automation",
87
+ parameter:
88
+ (clearAutomation[1] as "volume" | "pan" | undefined) ?? "volume",
89
+ points: [],
90
+ };
91
+ const automation = text.match(
92
+ /^(?:automate|automation)\s+(volume|pan)\s+at\s+(\d+(?:\.\d+)?)\s+(-?(?:0(?:\.\d+)?|1(?:\.0+)?))$/,
93
+ );
94
+ if (automation) {
95
+ const parameter = automation[1] as "volume" | "pan";
96
+ const beat = Number(automation[2]);
97
+ const value = Number(automation[3]);
98
+ if (
99
+ Number.isFinite(beat) &&
100
+ Number.isFinite(value) &&
101
+ (parameter === "pan" ? value >= -1 : value >= 0)
102
+ )
103
+ return {
104
+ type: "automation",
105
+ parameter,
106
+ points: [{ beat, value }],
107
+ };
108
+ }
109
+ const pan = text.match(/^(?:pan)\s+(-?1(?:\.0+)?|-?0(?:\.\d+)?)$/);
110
+ if (pan) return { type: "track", patch: { pan: Number(pan[1]) } };
111
+ const remove = text.match(
112
+ /^(?:remove|delete)(?:\s+note)?\s+([a-z0-9._-]{1,64})$/,
113
+ );
114
+ if (remove) return { type: "remove-note", noteId: remove[1]! };
115
+ const move = text.match(
116
+ /^(?:move|shift)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(?:to|at)\s+(\d+(?:\.\d+)?)$/,
117
+ );
118
+ if (move)
119
+ return {
120
+ type: "update-note",
121
+ noteId: move[1]!,
122
+ patch: { start: Number(move[2]) },
123
+ };
124
+ const length = text.match(
125
+ /^(?:length|duration)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(\d+(?:\.\d+)?)$/,
126
+ );
127
+ if (length)
128
+ return {
129
+ type: "update-note",
130
+ noteId: length[1]!,
131
+ patch: { duration: Number(length[2]) },
132
+ };
133
+ const velocity = text.match(
134
+ /^(?:velocity|vel)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(0(?:\.\d+)?|1(?:\.0+)?)$/,
135
+ );
136
+ if (velocity)
137
+ return {
138
+ type: "update-note",
139
+ noteId: velocity[1]!,
140
+ patch: { velocity: Number(velocity[2]) },
141
+ };
142
+ const match = text.match(
143
+ /^(?:add|put)\s+(?:note\s+)?([a-g](?:#|b)?-?\d+)\s+(?:at\s+)?(\d+(?:\.\d+)?)\s*(?:for|dur|duration)?\s*(\d+(?:\.\d+)?)?$/,
144
+ );
145
+ if (!match) return undefined;
146
+ const pitch = pitchToMidi(match[1] ?? "c4");
147
+ const start = Number(match[2] ?? 0);
148
+ const duration = Number(match[3] ?? 1);
149
+ if (
150
+ !Number.isFinite(pitch) ||
151
+ !Number.isFinite(start) ||
152
+ !Number.isFinite(duration) ||
153
+ duration <= 0
154
+ )
155
+ return undefined;
156
+ return { type: "add-note", pitch, start, duration, velocity: 0.8 };
157
+ }
@@ -0,0 +1,259 @@
1
+ import {
2
+ SCORE_LIMITS,
3
+ AUTOMATION_LANES,
4
+ isAutomationParameter,
5
+ normalizeDelay,
6
+ normalizeFilter,
7
+ normalizeReverb,
8
+ normalizeSampler,
9
+ scoreFromJSON,
10
+ type ScoreOperation,
11
+ } from "../../core/score.ts";
12
+ import { DRUM_VOICES } from "../../core/drums.ts";
13
+
14
+ const MAX_RESPONSE_BYTES = 64 * 1024;
15
+ const MAX_OPERATIONS = 32;
16
+
17
+ export type CompositionPlan = Readonly<{
18
+ operations: readonly ScoreOperation[];
19
+ explanation?: string;
20
+ }>;
21
+
22
+ /** Parse the only model output shape that the local reducer is allowed to apply. */
23
+ export function parseCompositionPlan(text: string): CompositionPlan {
24
+ if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES)
25
+ throw new Error("agent response is too large");
26
+ const candidate = text.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1] ?? text;
27
+ let value: unknown;
28
+ try {
29
+ value = JSON.parse(candidate.trim());
30
+ } catch {
31
+ throw new Error("agent response was not valid JSON");
32
+ }
33
+ if (
34
+ !isRecord(value) ||
35
+ !Array.isArray(value.operations) ||
36
+ value.operations.length > MAX_OPERATIONS
37
+ ) {
38
+ throw new Error("agent response must contain a bounded operations array");
39
+ }
40
+ const operations = value.operations.map(parseOperation);
41
+ const explanation =
42
+ typeof value.explanation === "string"
43
+ ? value.explanation.slice(0, 2_000)
44
+ : undefined;
45
+ return explanation === undefined
46
+ ? { operations }
47
+ : { operations, explanation };
48
+ }
49
+
50
+ /**
51
+ * Validate one foreign operation against the bounded planner contract. Every
52
+ * tool call and legacy JSON plan passes through here before the reducer sees it.
53
+ */
54
+ export function validateAgentOperation(value: unknown): ScoreOperation {
55
+ return parseOperation(value);
56
+ }
57
+
58
+ function parseOperation(value: unknown): ScoreOperation {
59
+ if (!isRecord(value) || typeof value.type !== "string")
60
+ throw new Error("agent operation is malformed");
61
+ if (value.type === "addTrack") {
62
+ const track = scoreFromJSON({ tracks: [value.track] }).tracks[0]!;
63
+ return { type: "addTrack", track };
64
+ }
65
+ if (
66
+ value.type === "setBars" &&
67
+ typeof value.bars === "number" &&
68
+ Number.isInteger(value.bars) &&
69
+ value.bars >= 1 &&
70
+ value.bars <= SCORE_LIMITS.maxBars
71
+ ) {
72
+ return { type: "setBars", bars: value.bars };
73
+ }
74
+ if (
75
+ value.type === "removeNote" &&
76
+ typeof value.noteId === "string" &&
77
+ value.noteId.length <= 64
78
+ ) {
79
+ return { type: "removeNote", noteId: value.noteId };
80
+ }
81
+ if (
82
+ value.type === "setTempo" &&
83
+ typeof value.tempoBpm === "number" &&
84
+ Number.isFinite(value.tempoBpm)
85
+ ) {
86
+ return { type: "setTempo", tempoBpm: value.tempoBpm };
87
+ }
88
+ if (
89
+ value.type === "clearTrack" &&
90
+ typeof value.trackId === "string" &&
91
+ value.trackId.length <= 64
92
+ ) {
93
+ return { type: "clearTrack", trackId: value.trackId };
94
+ }
95
+ if (
96
+ value.type === "removeTrack" &&
97
+ typeof value.trackId === "string" &&
98
+ value.trackId.length <= 64
99
+ ) {
100
+ return { type: "removeTrack", trackId: value.trackId };
101
+ }
102
+ if (
103
+ value.type === "moveTrack" &&
104
+ typeof value.trackId === "string" &&
105
+ value.trackId.length <= 64 &&
106
+ typeof value.index === "number" &&
107
+ Number.isInteger(value.index) &&
108
+ value.index >= 0 &&
109
+ value.index < SCORE_LIMITS.maxTracks
110
+ ) {
111
+ return { type: "moveTrack", trackId: value.trackId, index: value.index };
112
+ }
113
+ if (
114
+ value.type === "setKey" &&
115
+ (value.key === null ||
116
+ (typeof value.key === "string" &&
117
+ value.key.length <= SCORE_LIMITS.maxNameLength))
118
+ ) {
119
+ return { type: "setKey", key: value.key };
120
+ }
121
+ if (
122
+ value.type === "setMeter" &&
123
+ typeof value.beatsPerBar === "number" &&
124
+ Number.isInteger(value.beatsPerBar) &&
125
+ value.beatsPerBar >= 1 &&
126
+ value.beatsPerBar <= SCORE_LIMITS.maxBeatsPerBar
127
+ ) {
128
+ return { type: "setMeter", beatsPerBar: value.beatsPerBar };
129
+ }
130
+ if (
131
+ value.type === "updateTrack" &&
132
+ typeof value.trackId === "string" &&
133
+ value.trackId.length <= 64 &&
134
+ isRecord(value.patch)
135
+ ) {
136
+ const patch = value.patch;
137
+ const safe: Record<string, unknown> = {};
138
+ if (typeof patch.name === "string") safe.name = patch.name.slice(0, 96);
139
+ if (typeof patch.instrument === "string")
140
+ safe.instrument = patch.instrument.slice(0, 64);
141
+ if (typeof patch.muted === "boolean") safe.muted = patch.muted;
142
+ if (typeof patch.volume === "number" && Number.isFinite(patch.volume))
143
+ safe.volume = patch.volume;
144
+ if (typeof patch.pan === "number" && Number.isFinite(patch.pan))
145
+ safe.pan = patch.pan;
146
+ if (typeof patch.solo === "boolean") safe.solo = patch.solo;
147
+ // Effects reuse the score's bounded validators; null removes an effect.
148
+ if (patch.filter !== undefined)
149
+ safe.filter = normalizeFilter(patch.filter) ?? null;
150
+ if (patch.delay !== undefined)
151
+ safe.delay = normalizeDelay(patch.delay) ?? null;
152
+ if (patch.reverb !== undefined)
153
+ safe.reverb = normalizeReverb(patch.reverb) ?? null;
154
+ if (patch.sampler !== undefined)
155
+ safe.sampler = normalizeSampler(patch.sampler) ?? null;
156
+ return { type: "updateTrack", trackId: value.trackId, patch: safe };
157
+ }
158
+ if (
159
+ value.type === "setAutomation" &&
160
+ typeof value.trackId === "string" &&
161
+ value.trackId.length <= SCORE_LIMITS.maxIdLength &&
162
+ isAutomationParameter(value.parameter) &&
163
+ Array.isArray(value.points) &&
164
+ value.points.length <= SCORE_LIMITS.maxAutomationPoints
165
+ ) {
166
+ const { min: minValue, max: maxValue } = AUTOMATION_LANES[value.parameter];
167
+ const points = value.points.map((candidate) => {
168
+ if (!isRecord(candidate))
169
+ throw new Error("agent automation point is malformed");
170
+ if (
171
+ typeof candidate.tick !== "number" ||
172
+ !Number.isInteger(candidate.tick) ||
173
+ candidate.tick < 0 ||
174
+ candidate.tick > SCORE_LIMITS.maxTick ||
175
+ typeof candidate.value !== "number" ||
176
+ !Number.isFinite(candidate.value) ||
177
+ candidate.value < minValue ||
178
+ candidate.value > maxValue
179
+ ) {
180
+ throw new Error("agent automation point is invalid");
181
+ }
182
+ return { tick: candidate.tick, value: candidate.value };
183
+ });
184
+ return {
185
+ type: "setAutomation",
186
+ trackId: value.trackId,
187
+ parameter: value.parameter,
188
+ points,
189
+ };
190
+ }
191
+ if (
192
+ value.type === "updateNote" &&
193
+ typeof value.noteId === "string" &&
194
+ value.noteId.length <= 64 &&
195
+ isRecord(value.patch)
196
+ ) {
197
+ const patch = value.patch;
198
+ const safe: Record<string, number> = {};
199
+ for (const key of [
200
+ "startTick",
201
+ "durationTicks",
202
+ "pitch",
203
+ "velocity",
204
+ ] as const) {
205
+ if (typeof patch[key] === "number" && Number.isFinite(patch[key]))
206
+ safe[key] = patch[key];
207
+ }
208
+ return { type: "updateNote", noteId: value.noteId, patch: safe };
209
+ }
210
+ if (value.type === "addNote" && isRecord(value.note)) {
211
+ const note = value.note;
212
+ if (
213
+ typeof note.id !== "string" ||
214
+ typeof note.trackId !== "string" ||
215
+ typeof note.pitch !== "number" ||
216
+ typeof note.velocity !== "number"
217
+ ) {
218
+ throw new Error("agent note is malformed");
219
+ }
220
+ const ticks =
221
+ typeof note.startTick === "number" &&
222
+ typeof note.durationTicks === "number";
223
+ const start = ticks ? note.startTick : note.start;
224
+ const duration = ticks ? note.durationTicks : note.duration;
225
+ if (typeof start !== "number" || typeof duration !== "number")
226
+ throw new Error("agent note timing is malformed");
227
+ if (
228
+ !Number.isFinite(start) ||
229
+ !Number.isFinite(duration) ||
230
+ duration <= 0 ||
231
+ !Number.isFinite(note.pitch) ||
232
+ !Number.isFinite(note.velocity)
233
+ ) {
234
+ throw new Error("agent note timing is invalid");
235
+ }
236
+ return {
237
+ type: "addNote",
238
+ note: {
239
+ id: note.id.slice(0, 64),
240
+ trackId: note.trackId.slice(0, 64),
241
+ ...(ticks
242
+ ? { startTick: start, durationTicks: duration }
243
+ : { start, duration }),
244
+ pitch: note.pitch,
245
+ velocity: note.velocity,
246
+ },
247
+ };
248
+ }
249
+ throw new Error("unsupported agent operation");
250
+ }
251
+
252
+ function isRecord(value: unknown): value is Record<string, unknown> {
253
+ return typeof value === "object" && value !== null && !Array.isArray(value);
254
+ }
255
+
256
+ /** "kick=36, snare=38, ..." for the model's drum vocabulary. */
257
+ export function drumVoiceGuide(): string {
258
+ return DRUM_VOICES.map((info) => `${info.voice}=${info.pitch}`).join(", ");
259
+ }