@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.
- package/CHANGELOG.md +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/src/session/store.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, rm
|
|
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
|
|
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"> & {
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|