@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,113 @@
1
+ /**
2
+ * Runs inside the evaluation subprocess (`bun --no-addons --no-install
3
+ * --smol eval-child.ts <project>`): imports the project's `song.ts`, fills
4
+ * sample hashes, and prints one JSON line. Never imported by dawg itself.
5
+ */
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { realpathSync } from "node:fs";
9
+ import { readFile, stat } from "node:fs/promises";
10
+ import { isAbsolute, join, relative, resolve } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+
13
+ const MAX_SAMPLE_FILE_BYTES = 50 * 1024 * 1024;
14
+
15
+ type Diagnostic = {
16
+ message: string;
17
+ file?: string;
18
+ line?: number;
19
+ col?: number;
20
+ };
21
+
22
+ async function main(): Promise<void> {
23
+ const project = realpathSync(resolve(process.argv[2] ?? "."));
24
+ const entry = join(project, "song.ts");
25
+ let output: unknown;
26
+ try {
27
+ const module = (await import(pathToFileURL(entry).href)) as {
28
+ default?: unknown;
29
+ };
30
+ const song = module.default;
31
+ if (typeof song !== "object" || song === null)
32
+ throw new Error("song.ts must `export default song({...})`");
33
+ output = { ok: true, score: await withSampleHashes(song, project) };
34
+ } catch (error) {
35
+ output = { ok: false, error: diagnostic(error, project) };
36
+ }
37
+ process.stdout.write(`${JSON.stringify(output)}\n`);
38
+ }
39
+
40
+ function diagnostic(error: unknown, project: string): Diagnostic {
41
+ if (typeof error !== "object" || error === null)
42
+ return { message: String(error) };
43
+ let record = error as Record<string, unknown>;
44
+ // Bun wraps build errors in an AggregateError; the first one has the position.
45
+ if (Array.isArray(record.errors) && record.errors.length > 0) {
46
+ const first: unknown = record.errors[0];
47
+ if (typeof first === "object" && first !== null)
48
+ record = first as Record<string, unknown>;
49
+ }
50
+ const message =
51
+ typeof record.message === "string" ? record.message : String(error);
52
+ const out: Diagnostic = { message };
53
+ // Bun's BuildMessage/ResolveMessage carry a position.
54
+ const position = record.position as Record<string, unknown> | undefined;
55
+ if (position && typeof position.file === "string") {
56
+ out.file = relative(project, position.file) || position.file;
57
+ if (typeof position.line === "number") out.line = position.line;
58
+ if (typeof position.column === "number") out.col = position.column;
59
+ return out;
60
+ }
61
+ const stack = typeof record.stack === "string" ? record.stack : "";
62
+ for (const line of stack.split("\n")) {
63
+ const match = line.match(
64
+ /\(?((?:\/|file:\/\/)[^\s()]+?):(\d+):(\d+)\)?\s*$/,
65
+ );
66
+ if (!match) continue;
67
+ const file = match[1]!.replace(/^file:\/\//, "");
68
+ const rel = relative(project, file);
69
+ if (rel.startsWith("..") || isAbsolute(rel) || rel.startsWith(".dawg"))
70
+ continue;
71
+ out.file = rel;
72
+ out.line = Number(match[2]);
73
+ out.col = Number(match[3]);
74
+ break;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** Adds `sha256` to sampler voices whose file exists and is within the size limit. */
80
+ async function withSampleHashes(
81
+ song: object,
82
+ project: string,
83
+ ): Promise<unknown> {
84
+ const plain = JSON.parse(JSON.stringify(song)) as Record<string, unknown>;
85
+ const tracks = Array.isArray(plain.tracks) ? plain.tracks : [];
86
+ for (const track of tracks) {
87
+ if (typeof track !== "object" || track === null) continue;
88
+ const sampler = (track as Record<string, unknown>).sampler;
89
+ if (typeof sampler !== "object" || sampler === null) continue;
90
+ const voices = (sampler as Record<string, unknown>).voices;
91
+ if (typeof voices !== "object" || voices === null) continue;
92
+ for (const voice of Object.values(voices as Record<string, unknown>)) {
93
+ if (typeof voice !== "object" || voice === null) continue;
94
+ const ref = voice as Record<string, unknown>;
95
+ if (typeof ref.src !== "string" || typeof ref.sha256 === "string")
96
+ continue;
97
+ const path = resolve(project, ref.src);
98
+ if (relative(project, path).startsWith("..")) continue;
99
+ try {
100
+ const info = await stat(path);
101
+ if (!info.isFile() || info.size > MAX_SAMPLE_FILE_BYTES) continue;
102
+ ref.sha256 = createHash("sha256")
103
+ .update(await readFile(path))
104
+ .digest("hex");
105
+ } catch {
106
+ // Missing sample: left for `dawg check` to report.
107
+ }
108
+ }
109
+ }
110
+ return plain;
111
+ }
112
+
113
+ await main();
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Evaluates a project's `song.ts` in a sandboxed Bun subprocess and parses
3
+ * the result from `unknown` into a validated `TrackScore`.
4
+ *
5
+ * The child runs `eval-child.ts` with a scrubbed environment, the project as
6
+ * its working directory, no native addons or auto-install, a 10 second
7
+ * budget and a 1 MiB output bound. Anything it prints that is not the
8
+ * expected JSON line is a diagnostic.
9
+ */
10
+
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { decodeLoopDocument } from "../loop.ts";
14
+ import { ScoreValidationError, TrackScore } from "../score.ts";
15
+
16
+ export const EVAL_TIMEOUT_MS = 10_000;
17
+ export const EVAL_MAX_OUTPUT_BYTES = 1024 * 1024;
18
+
19
+ /** A problem in a project file; `file` is project-relative when known. */
20
+ export type Diagnostic = Readonly<{
21
+ message: string;
22
+ file?: string;
23
+ line?: number;
24
+ col?: number;
25
+ }>;
26
+
27
+ export type EvalResult =
28
+ | Readonly<{ ok: true; score: TrackScore; ms: number }>
29
+ | Readonly<{ ok: false; diagnostics: readonly Diagnostic[]; ms: number }>;
30
+
31
+ /** How the evaluator starts its child; tests inject a scripted one. */
32
+ export type EvalSpawn = (
33
+ args: readonly string[],
34
+ options: Readonly<{
35
+ cwd: string;
36
+ env: Readonly<Record<string, string>>;
37
+ timeoutMs: number;
38
+ maxOutputBytes: number;
39
+ }>,
40
+ ) => Promise<
41
+ Readonly<{ code: number; stdout: string; stderr: string; killed: boolean }>
42
+ >;
43
+
44
+ export type EvalOptions = Readonly<{
45
+ spawn?: EvalSpawn;
46
+ timeoutMs?: number;
47
+ }>;
48
+
49
+ const CHILD = join(dirname(fileURLToPath(import.meta.url)), "eval-child.ts");
50
+
51
+ /** Evaluates `<project>/song.ts`; never throws for project errors. */
52
+ export async function evaluateProject(
53
+ project: string,
54
+ options: EvalOptions = {},
55
+ ): Promise<EvalResult> {
56
+ const started = performance.now();
57
+ const spawn = options.spawn ?? bunSpawn;
58
+ const env: Record<string, string> = {};
59
+ for (const key of ["PATH", "HOME", "TMPDIR"]) {
60
+ const value = process.env[key];
61
+ if (value !== undefined) env[key] = value;
62
+ }
63
+ env.NO_COLOR = "1";
64
+ const done = (result: EvalResult): EvalResult => result;
65
+ let run: Awaited<ReturnType<EvalSpawn>>;
66
+ try {
67
+ run = await spawn(
68
+ [
69
+ process.execPath,
70
+ "--no-addons",
71
+ "--no-install",
72
+ "--smol",
73
+ CHILD,
74
+ project,
75
+ ],
76
+ {
77
+ cwd: project,
78
+ env,
79
+ timeoutMs: options.timeoutMs ?? EVAL_TIMEOUT_MS,
80
+ maxOutputBytes: EVAL_MAX_OUTPUT_BYTES,
81
+ },
82
+ );
83
+ } catch (error) {
84
+ return done(
85
+ failure(started, [{ message: `could not start bun: ${text(error)}` }]),
86
+ );
87
+ }
88
+ if (run.killed)
89
+ return done(
90
+ failure(started, [
91
+ {
92
+ file: "song.ts",
93
+ message: "evaluation timed out (10 s); song.ts must be pure",
94
+ },
95
+ ]),
96
+ );
97
+ const line = lastJsonLine(run.stdout);
98
+ if (line === undefined) {
99
+ const detail = (run.stderr || run.stdout)
100
+ .trim()
101
+ .split("\n")
102
+ .slice(0, 8)
103
+ .join("\n");
104
+ return done(
105
+ failure(started, [
106
+ {
107
+ file: "song.ts",
108
+ message: detail
109
+ ? `evaluation exited with code ${run.code}: ${detail}`
110
+ : `evaluation exited with code ${run.code}`,
111
+ },
112
+ ]),
113
+ );
114
+ }
115
+ return done(parseChildOutput(line, started));
116
+ }
117
+
118
+ /** Parses the child's JSON line from `unknown`; exported for tests. */
119
+ export function parseChildOutput(
120
+ line: string,
121
+ started = performance.now(),
122
+ ): EvalResult {
123
+ let value: unknown;
124
+ try {
125
+ value = JSON.parse(line);
126
+ } catch {
127
+ return failure(started, [
128
+ { file: "song.ts", message: "evaluation printed invalid JSON" },
129
+ ]);
130
+ }
131
+ if (typeof value !== "object" || value === null)
132
+ return failure(started, [
133
+ { file: "song.ts", message: "evaluation printed no result" },
134
+ ]);
135
+ const record = value as Record<string, unknown>;
136
+ if (record.ok !== true) {
137
+ const error =
138
+ typeof record.error === "object" && record.error !== null
139
+ ? (record.error as Record<string, unknown>)
140
+ : {};
141
+ const diagnostic: {
142
+ message: string;
143
+ file?: string;
144
+ line?: number;
145
+ col?: number;
146
+ } = {
147
+ message:
148
+ typeof error.message === "string" && error.message.length > 0
149
+ ? error.message.slice(0, 2000)
150
+ : "song.ts threw",
151
+ };
152
+ if (typeof error.file === "string")
153
+ diagnostic.file = error.file.slice(0, 512);
154
+ if (typeof error.line === "number" && Number.isInteger(error.line))
155
+ diagnostic.line = error.line;
156
+ if (typeof error.col === "number" && Number.isInteger(error.col))
157
+ diagnostic.col = error.col;
158
+ return failure(started, [diagnostic]);
159
+ }
160
+ try {
161
+ const score = decodeLoopDocument(record.score);
162
+ return { ok: true, score, ms: elapsed(started) };
163
+ } catch (error) {
164
+ return failure(started, [
165
+ {
166
+ file: "song.ts",
167
+ message:
168
+ error instanceof ScoreValidationError
169
+ ? `invalid score: ${error.message}`
170
+ : `invalid score: ${text(error)}`,
171
+ },
172
+ ]);
173
+ }
174
+ }
175
+
176
+ /** `file:line:col message` for humans; file-less diagnostics print the message alone. */
177
+ export function formatDiagnostic(diagnostic: Diagnostic): string {
178
+ if (!diagnostic.file) return diagnostic.message;
179
+ const where =
180
+ diagnostic.line === undefined
181
+ ? diagnostic.file
182
+ : `${diagnostic.file}:${diagnostic.line}:${diagnostic.col ?? 1}`;
183
+ return `${where} ${diagnostic.message}`;
184
+ }
185
+
186
+ function failure(
187
+ started: number,
188
+ diagnostics: readonly Diagnostic[],
189
+ ): EvalResult {
190
+ return {
191
+ ok: false,
192
+ diagnostics: Object.freeze(diagnostics.map((d) => Object.freeze({ ...d }))),
193
+ ms: elapsed(started),
194
+ };
195
+ }
196
+
197
+ function elapsed(started: number): number {
198
+ return Math.round(performance.now() - started);
199
+ }
200
+
201
+ function text(error: unknown): string {
202
+ return error instanceof Error ? error.message : String(error);
203
+ }
204
+
205
+ function lastJsonLine(stdout: string): string | undefined {
206
+ const lines = stdout.split("\n").map((line) => line.trim());
207
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
208
+ const line = lines[index]!;
209
+ if (line.startsWith('{"ok":')) return line;
210
+ }
211
+ return undefined;
212
+ }
213
+
214
+ /** Default spawner: `Bun.spawn` with the given env only (no inheritance). */
215
+ export const bunSpawn: EvalSpawn = async (args, options) => {
216
+ const child = Bun.spawn([...args], {
217
+ cwd: options.cwd,
218
+ env: options.env,
219
+ stdin: "ignore",
220
+ stdout: "pipe",
221
+ stderr: "pipe",
222
+ });
223
+ let killed = false;
224
+ const timer = setTimeout(() => {
225
+ killed = true;
226
+ child.kill();
227
+ }, options.timeoutMs);
228
+ const read = async (stream: ReadableStream<Uint8Array> | null) => {
229
+ if (!stream) return "";
230
+ const chunks: Uint8Array[] = [];
231
+ let total = 0;
232
+ const reader = stream.getReader();
233
+ for (;;) {
234
+ const { value, done } = await reader.read();
235
+ if (done || !value) break;
236
+ if (total + value.byteLength > options.maxOutputBytes) {
237
+ killed = true;
238
+ child.kill();
239
+ break;
240
+ }
241
+ chunks.push(value);
242
+ total += value.byteLength;
243
+ }
244
+ reader.releaseLock();
245
+ return Buffer.concat(chunks).toString("utf8");
246
+ };
247
+ try {
248
+ const [stdout, stderr, code] = await Promise.all([
249
+ read(child.stdout),
250
+ read(child.stderr),
251
+ child.exited,
252
+ ]);
253
+ return { code, stdout, stderr, killed };
254
+ } finally {
255
+ clearTimeout(timer);
256
+ }
257
+ };