@hraness/dawg 0.2.0 → 0.4.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 (140) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/DAWG.md +762 -11
  3. package/README.md +79 -30
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +261 -0
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/key.ts +43 -0
  9. package/core/kits.ts +320 -0
  10. package/core/params.ts +111 -0
  11. package/core/pitch.ts +60 -0
  12. package/core/rhythm.ts +287 -0
  13. package/core/score.ts +1054 -14
  14. package/core/sdk/eval-child.ts +125 -0
  15. package/core/sdk/eval.ts +260 -0
  16. package/core/sdk/print.ts +675 -0
  17. package/core/sdk/sync-chords.ts +54 -0
  18. package/core/sdk/v1.ts +4159 -0
  19. package/core/slug.ts +19 -0
  20. package/core/synth.ts +1001 -0
  21. package/package.json +8 -5
  22. package/src/agent/agent.ts +317 -16
  23. package/src/agent/brief.ts +50 -5
  24. package/src/agent/chord-tools.ts +357 -0
  25. package/src/agent/drum-tools.ts +135 -0
  26. package/src/agent/gateway.ts +215 -41
  27. package/src/agent/models.ts +633 -0
  28. package/src/agent/ops.ts +3 -18
  29. package/src/agent/pack-tools.ts +369 -0
  30. package/src/agent/planner.ts +55 -2
  31. package/src/agent/preview-tool.ts +270 -0
  32. package/src/agent/provider.ts +234 -106
  33. package/src/agent/rhythm-tools.ts +145 -0
  34. package/src/agent/sse.ts +31 -7
  35. package/src/agent/tools.ts +705 -14
  36. package/src/agent/usage.ts +296 -0
  37. package/src/agent/workspace.ts +683 -0
  38. package/src/agent/xcb-agent.ts +35 -14
  39. package/src/agent/xcb.ts +233 -21
  40. package/src/audio/audition.ts +93 -0
  41. package/src/audio/cache.ts +160 -0
  42. package/src/audio/click.ts +125 -0
  43. package/src/audio/effects/bus.ts +147 -0
  44. package/src/audio/effects/chain.ts +99 -0
  45. package/src/audio/effects/common.ts +251 -0
  46. package/src/audio/effects/convolution.ts +301 -0
  47. package/src/audio/effects/drive.ts +142 -0
  48. package/src/audio/effects/duck.ts +122 -0
  49. package/src/audio/effects/dynamics.ts +121 -0
  50. package/src/audio/effects/filter.ts +319 -0
  51. package/src/audio/effects/modulation.ts +174 -0
  52. package/src/audio/effects/space.ts +337 -0
  53. package/src/audio/engine.ts +430 -33
  54. package/src/audio/kits.ts +200 -0
  55. package/src/audio/live.ts +152 -0
  56. package/src/audio/packs.ts +1787 -0
  57. package/src/audio/player.ts +19 -4
  58. package/src/audio/preview.ts +470 -0
  59. package/src/audio/random.ts +15 -0
  60. package/src/audio/render-worker.ts +68 -0
  61. package/src/audio/renderer.ts +174 -0
  62. package/src/audio/sampler.ts +420 -0
  63. package/src/audio/samples.ts +1025 -0
  64. package/src/audio/synth/oscillators.ts +268 -0
  65. package/src/audio/synth/voice.ts +555 -0
  66. package/src/audio/synth/zzfx.ts +137 -0
  67. package/src/audio/wav.ts +550 -334
  68. package/src/audio/wavetable-maker.ts +717 -0
  69. package/src/audio/wavetable.ts +624 -0
  70. package/src/auth/cli.ts +146 -27
  71. package/src/auth/credentials.ts +167 -41
  72. package/src/auth/discover.ts +481 -0
  73. package/src/auth/login.ts +885 -128
  74. package/src/auth/openrouter.ts +206 -0
  75. package/src/auth/picker.ts +282 -0
  76. package/src/auth/runner.ts +25 -2
  77. package/src/auth/tui.ts +60 -43
  78. package/src/commands/drums.ts +299 -0
  79. package/src/commands/edit.ts +192 -0
  80. package/src/commands/fx.ts +360 -0
  81. package/src/commands/help.ts +535 -0
  82. package/src/commands/history.ts +32 -19
  83. package/src/commands/music.ts +34 -15
  84. package/src/commands/pack.ts +423 -0
  85. package/src/commands/rhythm.ts +230 -0
  86. package/src/commands/sample.ts +462 -0
  87. package/src/commands/synth.ts +229 -0
  88. package/src/commands/wavetable.ts +370 -0
  89. package/src/main.ts +2136 -170
  90. package/src/media/analyze.ts +364 -0
  91. package/src/media/backend.ts +253 -0
  92. package/src/media/cli.ts +186 -0
  93. package/src/media/download.ts +281 -0
  94. package/src/media/dsp.ts +281 -0
  95. package/src/media/import.ts +130 -0
  96. package/src/media/lyrics.ts +201 -0
  97. package/src/media/notes.ts +363 -0
  98. package/src/media/paths.ts +168 -0
  99. package/src/media/process.ts +226 -0
  100. package/src/media/registry.ts +9 -0
  101. package/src/media/sidecar.ts +72 -0
  102. package/src/media/stemdeck.ts +254 -0
  103. package/src/media/stems.ts +173 -0
  104. package/src/media/tools.ts +397 -0
  105. package/src/media/types.ts +92 -0
  106. package/src/media/vendor/basic-pitch.ts +261 -0
  107. package/src/media/vendor/drums.ts +817 -0
  108. package/src/media/vendor/grid.ts +203 -0
  109. package/src/media/vendor/util.ts +139 -0
  110. package/src/media/vendor/wav.ts +233 -0
  111. package/src/media/wavetable.ts +202 -0
  112. package/src/project/check.ts +80 -0
  113. package/src/project/init.ts +253 -0
  114. package/src/project/sync.ts +432 -0
  115. package/src/project/typecheck.ts +149 -0
  116. package/src/render.ts +49 -6
  117. package/src/session/attach.ts +3 -4
  118. package/src/session/daemon.ts +25 -8
  119. package/src/session/delta.ts +249 -0
  120. package/src/session/naming.ts +6 -38
  121. package/src/session/port.ts +26 -6
  122. package/src/session/rebase.ts +38 -8
  123. package/src/session/store.ts +116 -21
  124. package/src/tui/audition.ts +501 -0
  125. package/src/tui/euclid.ts +472 -0
  126. package/src/tui/menu.ts +2125 -0
  127. package/src/tui/play-chords.ts +538 -0
  128. package/src/tui/play-mode.ts +442 -0
  129. package/src/tui/play-session.ts +984 -0
  130. package/src/tui/sketch.ts +108 -0
  131. package/src/web/fetch.ts +340 -0
  132. package/src/web/http.ts +137 -0
  133. package/src/web/search.ts +684 -0
  134. package/tui/activity.ts +42 -3
  135. package/tui/app.ts +398 -33
  136. package/tui/drums.ts +44 -0
  137. package/tui/grammar.ts +281 -0
  138. package/tui/highway.ts +24 -2
  139. package/tui/layers.ts +14 -2
  140. package/tui/play-strip.ts +188 -0
@@ -1,7 +1,8 @@
1
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { acquireSessionLock } from "./lock.ts";
5
+ import { diffRewind, parseRewind, type Rewind } from "./delta.ts";
5
6
  import {
6
7
  applyMetaPatch,
7
8
  defaultSessionMeta,
@@ -14,9 +15,14 @@ import {
14
15
  type SessionMeta,
15
16
  } from "./meta.ts";
16
17
 
18
+ /** Caller payloads; the store keeps history separately in each event's `rewind`. */
17
19
  const MAX_EVENT_BYTES = 64 * 1024;
20
+ /** A rewind larger than this (a wholesale import) is not kept; the edit still lands. */
21
+ const MAX_REWIND_BYTES = 1024 * 1024;
18
22
  const MAX_EVENTS = 2_000;
19
- const MAX_RECORD_BYTES = 4 * 1024 * 1024;
23
+ export const MAX_RECORD_BYTES = 4 * 1024 * 1024;
24
+ /** Oldest rewinds are dropped in batches of this size when the record fills up. */
25
+ const COMPACT_BATCH = 64;
20
26
  const MAX_SESSION_ID_LENGTH = 64;
21
27
  const MAX_EVENT_KIND_LENGTH = 128;
22
28
  const MAX_TIMESTAMP_LENGTH = 64;
@@ -34,6 +40,14 @@ export type SessionEvent = {
34
40
  kind: string;
35
41
  payload: unknown;
36
42
  at: string;
43
+ /**
44
+ * How to recover the composition this event replaced from the one it
45
+ * produced (see `delta.ts`). Absent once compacted away: the store drops
46
+ * the oldest rewinds first when the record would exceed its size cap, so
47
+ * an edit never fails because of history, and undo reaches as far back as
48
+ * the remaining rewinds form a contiguous suffix.
49
+ */
50
+ rewind?: Rewind;
37
51
  };
38
52
 
39
53
  export type SessionRecord<T> = {
@@ -149,7 +163,7 @@ export async function ensureSession<T>(
149
163
  ),
150
164
  };
151
165
  const validated = validateSessionRecord<T>(record);
152
- await writeAtomic(paths.record, JSON.stringify(validated, null, 2));
166
+ await writeAtomic(paths.record, JSON.stringify(validated));
153
167
  return { paths, record: validated };
154
168
  } catch (error) {
155
169
  if (!isCode(error, "ENOENT")) throw error;
@@ -167,13 +181,18 @@ export async function ensureSession<T>(
167
181
  export async function appendSessionEvent<T>(
168
182
  paths: SessionPaths,
169
183
  current: SessionRecord<T>,
170
- event: Omit<SessionEvent, "id" | "revision" | "at"> & { id?: string },
184
+ event: Omit<SessionEvent, "id" | "revision" | "at" | "rewind"> & {
185
+ id?: string;
186
+ },
171
187
  composition: T,
172
188
  ): Promise<SessionRecord<T>> {
173
189
  // dawgd passes the client's idempotency key as the durable event id so a
174
190
  // retried intent stays a no-op across daemon restarts.
175
191
  if (event.id !== undefined) assertSessionId(event.id);
176
- const payloadJson = stringifyJson(event.payload, "session event payload");
192
+ // History is the store's job: a `before` composition in the payload (the
193
+ // pre-rewind convention) is dropped rather than persisted in full.
194
+ const payload = stripBefore(event.payload);
195
+ const payloadJson = stringifyJson(payload, "session event payload");
177
196
  const payloadBytes = Buffer.byteLength(payloadJson, "utf8");
178
197
  if (payloadBytes > MAX_EVENT_BYTES)
179
198
  throw new Error(`session event exceeds ${MAX_EVENT_BYTES} bytes`);
@@ -183,6 +202,7 @@ export async function appendSessionEvent<T>(
183
202
  event.kind.length > MAX_EVENT_KIND_LENGTH
184
203
  )
185
204
  throw new SessionValidationError("session event kind is invalid");
205
+ stringifyJson(composition, "session composition");
186
206
  const release = await acquireSessionLock(paths.lock);
187
207
  try {
188
208
  const disk = await readRecord<T>(paths.record);
@@ -193,33 +213,79 @@ export async function appendSessionEvent<T>(
193
213
  throw new Error(`session event limit ${MAX_EVENTS} reached`);
194
214
  const revision = disk.revision + 1;
195
215
  const at = new Date().toISOString();
216
+ const appended: SessionEvent = {
217
+ kind: event.kind,
218
+ payload,
219
+ id: event.id ?? randomUUID(),
220
+ revision,
221
+ at,
222
+ };
223
+ const rewind = diffRewind(disk.composition, composition);
224
+ if (Buffer.byteLength(JSON.stringify(rewind), "utf8") <= MAX_REWIND_BYTES)
225
+ appended.rewind = rewind;
196
226
  const next: SessionRecord<T> = {
197
227
  sessionId: disk.sessionId,
198
228
  revision,
199
229
  updatedAt: at,
200
230
  composition,
201
- events: [
202
- ...disk.events,
203
- {
204
- kind: event.kind,
205
- payload: event.payload,
206
- id: event.id ?? randomUUID(),
207
- revision,
208
- at,
209
- },
210
- ],
231
+ events: [...disk.events, appended],
211
232
  // Metadata always comes from disk so a rename written by another window
212
233
  // between this caller's read and its write is never reverted.
213
234
  meta: disk.meta,
214
235
  };
215
236
  const validated = validateSessionRecord<T>(next);
216
- await writeAtomic(paths.record, JSON.stringify(validated, null, 2));
217
- return validated;
237
+ const { record, json } = compactRecord(validated);
238
+ await writeAtomic(paths.record, json);
239
+ return record;
218
240
  } finally {
219
241
  await release();
220
242
  }
221
243
  }
222
244
 
245
+ /**
246
+ * Keeps the record under its size cap by dropping the oldest events'
247
+ * rewinds, oldest first, so the rewinds that remain are always the newest
248
+ * contiguous run. Only the composition and the bounded payloads are
249
+ * irreducible; a record that still does not fit is rejected as before.
250
+ */
251
+ function compactRecord<T>(record: SessionRecord<T>): {
252
+ record: SessionRecord<T>;
253
+ json: string;
254
+ } {
255
+ let current = record;
256
+ let json = JSON.stringify(current);
257
+ while (Buffer.byteLength(json, "utf8") > MAX_RECORD_BYTES) {
258
+ const first = current.events.findIndex(
259
+ (event) => event.rewind !== undefined,
260
+ );
261
+ if (first < 0)
262
+ throw new SessionValidationError(
263
+ `session record exceeds ${MAX_RECORD_BYTES} bytes`,
264
+ );
265
+ const events = current.events.map((event, index) => {
266
+ if (index < first || index >= first + COMPACT_BATCH || !event.rewind)
267
+ return event;
268
+ const { rewind: _rewind, ...rest } = event;
269
+ return rest;
270
+ });
271
+ current = { ...current, events };
272
+ json = JSON.stringify(current);
273
+ }
274
+ return { record: current, json };
275
+ }
276
+
277
+ function stripBefore(payload: unknown): unknown {
278
+ if (
279
+ typeof payload !== "object" ||
280
+ payload === null ||
281
+ Array.isArray(payload) ||
282
+ !("before" in payload)
283
+ )
284
+ return payload;
285
+ const { before: _before, ...rest } = payload as Record<string, unknown>;
286
+ return rest;
287
+ }
288
+
223
289
  export type MetaUpdate<T> =
224
290
  | { status: "applied"; record: SessionRecord<T> }
225
291
  | { status: "stale"; record: SessionRecord<T> };
@@ -245,7 +311,7 @@ export async function updateSessionMeta<T>(
245
311
  meta: applyMetaPatch(disk.meta, patch, new Date().toISOString()),
246
312
  };
247
313
  const validated = validateSessionRecord<T>(next);
248
- await writeAtomic(paths.record, JSON.stringify(validated, null, 2));
314
+ await writeAtomic(paths.record, JSON.stringify(validated));
249
315
  return { status: "applied", record: validated };
250
316
  } finally {
251
317
  await release();
@@ -277,7 +343,7 @@ export async function createForkSession<T>(
277
343
  forkOf: { sessionId: source.sessionId, revision: source.revision },
278
344
  },
279
345
  });
280
- await writeAtomic(paths.record, JSON.stringify(record, null, 2));
346
+ await writeAtomic(paths.record, JSON.stringify(record));
281
347
  return record;
282
348
  }
283
349
 
@@ -348,7 +414,18 @@ async function writeAtomic(path: string, contents: string): Promise<void> {
348
414
  `session record exceeds ${MAX_RECORD_BYTES} bytes`,
349
415
  );
350
416
  const temporary = `${path}.${process.pid}.tmp`;
351
- await writeFile(temporary, `${contents}\n`, "utf8");
417
+ // Durable before it is visible: the data reaches the disk before the
418
+ // rename publishes it, so a crash never leaves a truncated record behind.
419
+ const handle = await open(temporary, "w", 0o644);
420
+ try {
421
+ await handle.writeFile(`${contents}\n`, "utf8");
422
+ await handle.sync();
423
+ } catch (error) {
424
+ await handle.close().catch(() => undefined);
425
+ await rm(temporary, { force: true }).catch(() => undefined);
426
+ throw error;
427
+ }
428
+ await handle.close();
352
429
  await rename(temporary, path);
353
430
  }
354
431
 
@@ -432,7 +509,25 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
432
509
  throw new SessionValidationError(
433
510
  `session event exceeds ${MAX_EVENT_BYTES} bytes`,
434
511
  );
435
- return candidate as unknown as SessionEvent;
512
+ let rewind: Rewind | undefined;
513
+ try {
514
+ rewind = parseRewind(candidate.rewind);
515
+ } catch (error) {
516
+ throw new SessionValidationError(
517
+ error instanceof Error
518
+ ? error.message
519
+ : "session event rewind is invalid",
520
+ );
521
+ }
522
+ const parsed: SessionEvent = {
523
+ id: candidate.id,
524
+ revision: candidate.revision,
525
+ kind: candidate.kind,
526
+ payload: candidate.payload,
527
+ at: candidate.at,
528
+ };
529
+ if (rewind !== undefined) parsed.rewind = rewind;
530
+ return parsed;
436
531
  });
437
532
  let meta: SessionMeta;
438
533
  try {