@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,6 +1,9 @@
1
1
  import { readSseData } from "./sse.ts";
2
2
 
3
- /** The only model labels dawg accepts. Provider IDs are local configuration. */
3
+ /**
4
+ * The two original model aliases. A model is either one of these or an exact
5
+ * `vendor/model` ID chosen from the model picker (see `models.ts`).
6
+ */
4
7
  export const GATEWAY_MODELS = Object.freeze(["opus-5.5", "sol-6.1"] as const);
5
8
  export type GatewayModel = (typeof GATEWAY_MODELS)[number];
6
9
 
@@ -26,15 +29,20 @@ export function isGatewayModel(value: unknown): value is GatewayModel {
26
29
  );
27
30
  }
28
31
 
29
- /** Resolve an allowlisted alias to its provider ID, rejecting anything else. */
32
+ /**
33
+ * Resolve an alias (`opus-5.5`) or an exact `vendor/model` ID to the ID sent
34
+ * to the provider, rejecting anything else.
35
+ */
30
36
  export function resolveModelId(
31
37
  alias: string,
32
38
  overrides: Partial<Record<GatewayModel, string | undefined>> = {},
33
39
  ): string {
34
- if (!isGatewayModel(alias))
40
+ if (!isGatewayModel(alias)) {
41
+ if (MODEL_ID_PATTERN.test(alias)) return alias;
35
42
  throw new Error(
36
- `unknown model "${alias.slice(0, 32)}"; use ${GATEWAY_MODELS.join(" or ")}`,
43
+ `unknown model "${alias.slice(0, 32)}"; use ${GATEWAY_MODELS.join(" or ")} or a vendor/model ID`,
37
44
  );
45
+ }
38
46
  const id = overrides[alias] ?? DEFAULT_MODEL_IDS[alias];
39
47
  if (!MODEL_ID_PATTERN.test(id))
40
48
  throw new Error(`model ID for ${alias} must look like provider/model`);
@@ -68,7 +76,8 @@ export type ChatTool = {
68
76
  };
69
77
 
70
78
  export type ChatStreamRequest = {
71
- model: GatewayModel;
79
+ /** An alias (`opus-5.5`) or an exact `vendor/model` ID. */
80
+ model: string;
72
81
  messages: readonly ChatMessage[];
73
82
  tools?: readonly ChatTool[];
74
83
  temperature?: number;
@@ -81,6 +90,8 @@ export type ChatStreamRequest = {
81
90
  /** Normalized stream events; provider chunk shapes never leave this module. */
82
91
  export type ChatStreamEvent =
83
92
  | { type: "text"; delta: string }
93
+ /** Progress worth a status line, such as a retry; never model output. */
94
+ | { type: "activity"; message: string }
84
95
  | {
85
96
  type: "tool-delta";
86
97
  index: number;
@@ -88,11 +99,25 @@ export type ChatStreamEvent =
88
99
  name?: string;
89
100
  arguments?: string;
90
101
  }
91
- | { type: "finish"; reason: string };
102
+ | { type: "finish"; reason: string }
103
+ /**
104
+ * Token usage for one request, from the final chunk when the request asked
105
+ * for `stream_options.include_usage`. `costUsd` is the provider's own
106
+ * charge when it reports one (OpenRouter's `usage.cost`).
107
+ */
108
+ | {
109
+ type: "usage";
110
+ inputTokens: number;
111
+ outputTokens: number;
112
+ cachedInputTokens?: number;
113
+ costUsd?: number;
114
+ };
92
115
 
93
116
  export type GatewayClient = {
117
+ /** Which service this client talks to. */
118
+ readonly provider?: ApiProvider;
94
119
  /** The provider model ID that a turn will use (for diagnostics only). */
95
- modelId(model: GatewayModel): string;
120
+ modelId(model: string): string;
96
121
  stream(
97
122
  request: ChatStreamRequest,
98
123
  signal?: AbortSignal,
@@ -114,21 +139,79 @@ type GatewayFetcher = (
114
139
  init?: RequestInit,
115
140
  ) => Promise<Response>;
116
141
 
142
+ /** Retries after a failed request that has not yet produced a byte. */
143
+ export const GATEWAY_RETRIES = 2;
144
+ /** Base backoff; each retry doubles it, with +-50% jitter. */
145
+ const RETRY_BASE_MS = 500;
146
+ /** Response headers must arrive within this long, per attempt. */
147
+ export const GATEWAY_HEADER_TIMEOUT_MS = 15_000;
148
+
149
+ export type GatewayRetryOptions = Readonly<{
150
+ retries?: number;
151
+ baseMs?: number;
152
+ sleep?: (ms: number) => Promise<void>;
153
+ random?: () => number;
154
+ }>;
155
+
156
+ function retryableStatus(status: number): boolean {
157
+ return status === 408 || status === 429 || status >= 500;
158
+ }
159
+
160
+ /** The two OpenAI-compatible, key-based services. */
161
+ export type ApiProvider = "gateway" | "openrouter";
162
+ export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
163
+ export const GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1";
164
+ /** OpenRouter's app attribution headers (https://openrouter.ai/docs/app-attribution). */
165
+ const OPENROUTER_HEADERS = Object.freeze({
166
+ "http-referer": "https://dawg.sh",
167
+ "x-title": "dawg",
168
+ });
169
+
170
+ export type ApiClientOptions = {
171
+ apiKey?: string;
172
+ baseUrl?: string;
173
+ modelIds?: Partial<Record<GatewayModel, string>>;
174
+ fetcher?: GatewayFetcher;
175
+ retry?: GatewayRetryOptions;
176
+ headerTimeoutMs?: number;
177
+ };
178
+
179
+ /** The streaming tool-calling client for OpenRouter's OpenAI-compatible API. */
180
+ export function createOpenRouterClient(
181
+ options: ApiClientOptions = {},
182
+ ): GatewayClient {
183
+ return createApiClient("openrouter", {
184
+ ...options,
185
+ baseUrl:
186
+ options.baseUrl ?? process.env.OPENROUTER_BASE_URL ?? OPENROUTER_BASE_URL,
187
+ apiKey: options.apiKey ?? process.env.OPENROUTER_API_KEY,
188
+ });
189
+ }
190
+
117
191
  export function createGatewayClient(
118
- options: {
119
- apiKey?: string;
120
- baseUrl?: string;
121
- modelIds?: Partial<Record<GatewayModel, string>>;
122
- fetcher?: GatewayFetcher;
123
- } = {},
192
+ options: ApiClientOptions = {},
193
+ ): GatewayClient {
194
+ return createApiClient("gateway", {
195
+ ...options,
196
+ baseUrl:
197
+ options.baseUrl ?? process.env.AI_GATEWAY_BASE_URL ?? GATEWAY_BASE_URL,
198
+ apiKey: options.apiKey ?? process.env.AI_GATEWAY_API_KEY,
199
+ });
200
+ }
201
+
202
+ function createApiClient(
203
+ provider: ApiProvider,
204
+ options: ApiClientOptions,
124
205
  ): GatewayClient {
125
206
  const fetcher = options.fetcher ?? fetch;
126
- const baseUrl = (
127
- options.baseUrl ??
128
- process.env.AI_GATEWAY_BASE_URL ??
129
- "https://ai-gateway.vercel.sh/v1"
130
- ).replace(/\/$/, "");
131
- const apiKey = options.apiKey ?? process.env.AI_GATEWAY_API_KEY;
207
+ const name = provider === "openrouter" ? "OpenRouter" : "AI Gateway";
208
+ const baseUrl = (options.baseUrl ?? GATEWAY_BASE_URL).replace(/\/$/, "");
209
+ const apiKey = options.apiKey;
210
+ const retries = options.retry?.retries ?? GATEWAY_RETRIES;
211
+ const retryBaseMs = options.retry?.baseMs ?? RETRY_BASE_MS;
212
+ const sleep = options.retry?.sleep ?? ((ms: number) => Bun.sleep(ms));
213
+ const random = options.retry?.random ?? Math.random;
214
+ const headerTimeoutMs = options.headerTimeoutMs ?? GATEWAY_HEADER_TIMEOUT_MS;
132
215
  const overrides: Partial<Record<GatewayModel, string | undefined>> = {
133
216
  "opus-5.5": process.env.DAWG_OPUS_MODEL || undefined,
134
217
  "sol-6.1": process.env.DAWG_SOL_MODEL || undefined,
@@ -138,6 +221,7 @@ export function createGatewayClient(
138
221
  apiKey && apiKey.length > 0 ? text.split(apiKey).join("[redacted]") : text;
139
222
 
140
223
  return {
224
+ provider,
141
225
  modelId: (model) => resolveModelId(model, overrides),
142
226
  async *stream(request, signal) {
143
227
  const model =
@@ -146,13 +230,18 @@ export function createGatewayClient(
146
230
  : resolveModelId(request.model, overrides);
147
231
  if (!apiKey)
148
232
  throw new GatewayError(
149
- "no AI Gateway key; run `dawg login` or set AI_GATEWAY_API_KEY",
233
+ provider === "openrouter"
234
+ ? "no OpenRouter key; run `dawg login openrouter` or set OPENROUTER_API_KEY"
235
+ : "no AI Gateway key; run `dawg login` or set AI_GATEWAY_API_KEY",
150
236
  );
151
237
  const body: Record<string, unknown> = {
152
238
  model,
153
239
  messages: request.messages,
154
240
  stream: true,
241
+ // The final chunk then carries token usage (and OpenRouter's cost).
242
+ stream_options: { include_usage: true },
155
243
  };
244
+ if (provider === "openrouter") body.usage = { include: true };
156
245
  if (request.tools && request.tools.length > 0) {
157
246
  body.tools = request.tools;
158
247
  body.tool_choice = "auto";
@@ -165,26 +254,61 @@ export function createGatewayClient(
165
254
  request.maxTokens > 0
166
255
  )
167
256
  body.max_tokens = Math.min(request.maxTokens, 4096);
168
- const init: RequestInit = {
169
- method: "POST",
170
- headers: {
171
- authorization: `Bearer ${apiKey}`,
172
- "content-type": "application/json",
173
- accept: "text/event-stream",
174
- },
175
- body: JSON.stringify(body),
176
- };
177
- if (signal !== undefined) init.signal = signal;
178
- const response = await fetcher(`${baseUrl}/chat/completions`, init);
179
- if (!response.ok) {
180
- const detail = redact(await boundedErrorDetail(response));
181
- throw new GatewayError(
182
- `AI Gateway request failed (${response.status})${detail ? `: ${detail}` : ""}`,
183
- response.status,
184
- );
257
+ const encoded = JSON.stringify(body);
258
+ // Retry only before any byte of a response has been consumed: a
259
+ // network failure, a header-phase timeout, or 408/429/5xx. Once the
260
+ // stream is flowing, a failure surfaces as-is (replaying could repeat
261
+ // tool calls the model already made).
262
+ let response: Response | undefined;
263
+ for (let attempt = 0; ; attempt += 1) {
264
+ if (signal?.aborted) throw abortError(signal);
265
+ const headerSignal = AbortSignal.any([
266
+ ...(signal === undefined ? [] : [signal]),
267
+ AbortSignal.timeout(headerTimeoutMs),
268
+ ]);
269
+ const init: RequestInit = {
270
+ method: "POST",
271
+ headers: {
272
+ authorization: `Bearer ${apiKey}`,
273
+ "content-type": "application/json",
274
+ accept: "text/event-stream",
275
+ ...(provider === "openrouter" ? OPENROUTER_HEADERS : {}),
276
+ },
277
+ body: encoded,
278
+ signal: headerSignal,
279
+ };
280
+ let reason: string;
281
+ try {
282
+ const candidate = await fetcher(`${baseUrl}/chat/completions`, init);
283
+ if (candidate.ok) {
284
+ response = candidate;
285
+ break;
286
+ }
287
+ const detail = redact(await boundedErrorDetail(candidate));
288
+ const error = new GatewayError(
289
+ `${name} request failed (${candidate.status})${detail ? `: ${detail}` : ""}`,
290
+ candidate.status,
291
+ );
292
+ if (!retryableStatus(candidate.status) || attempt >= retries)
293
+ throw error;
294
+ reason = String(candidate.status);
295
+ } catch (error) {
296
+ if (error instanceof GatewayError) throw error;
297
+ if (signal?.aborted) throw abortError(signal);
298
+ if (attempt >= retries)
299
+ throw new GatewayError(
300
+ headerSignal.aborted
301
+ ? `${name} did not respond within ${headerTimeoutMs} ms`
302
+ : `${name} request failed: ${redact(error instanceof Error ? error.message : String(error))}`,
303
+ );
304
+ reason = headerSignal.aborted ? "timeout" : "network";
305
+ }
306
+ yield { type: "activity", message: `retrying (${reason})…` };
307
+ const backoff = retryBaseMs * 2 ** attempt;
308
+ await sleep(Math.round(backoff * (0.5 + random())));
185
309
  }
186
310
  if (!response.body)
187
- throw new GatewayError("AI Gateway returned an empty stream");
311
+ throw new GatewayError(`${name} returned an empty stream`);
188
312
  const streamOptions: { maxBytes: number; signal?: AbortSignal } = {
189
313
  maxBytes: request.maxResponseBytes,
190
314
  };
@@ -194,9 +318,9 @@ export function createGatewayClient(
194
318
  try {
195
319
  chunk = JSON.parse(data);
196
320
  } catch {
197
- throw new GatewayError("AI Gateway sent a malformed stream chunk");
321
+ throw new GatewayError(`${name} sent a malformed stream chunk`);
198
322
  }
199
- yield* normalizeChunk(chunk, redact);
323
+ yield* normalizeChunk(chunk, redact, name);
200
324
  }
201
325
  },
202
326
  };
@@ -205,6 +329,7 @@ export function createGatewayClient(
205
329
  function* normalizeChunk(
206
330
  chunk: unknown,
207
331
  redact: (text: string) => string,
332
+ name = "AI Gateway",
208
333
  ): Generator<ChatStreamEvent> {
209
334
  if (!isRecord(chunk)) return;
210
335
  if (isRecord(chunk.error)) {
@@ -212,8 +337,10 @@ function* normalizeChunk(
212
337
  typeof chunk.error.message === "string"
213
338
  ? chunk.error.message.slice(0, 300)
214
339
  : "unknown provider error";
215
- throw new GatewayError(`AI Gateway stream error: ${redact(message)}`);
340
+ throw new GatewayError(`${name} stream error: ${redact(message)}`);
216
341
  }
342
+ const usage = parseUsage(chunk.usage);
343
+ if (usage) yield usage;
217
344
  const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
218
345
  if (!isRecord(choice)) return;
219
346
  const delta = isRecord(choice.delta) ? choice.delta : undefined;
@@ -242,6 +369,53 @@ function* normalizeChunk(
242
369
  yield { type: "finish", reason: choice.finish_reason };
243
370
  }
244
371
 
372
+ /** Bounded, non-negative token counts; anything else is ignored. */
373
+ function count(value: unknown): number | undefined {
374
+ return typeof value === "number" &&
375
+ Number.isFinite(value) &&
376
+ value >= 0 &&
377
+ value < 1e9
378
+ ? Math.floor(value)
379
+ : undefined;
380
+ }
381
+
382
+ export function parseUsage(
383
+ value: unknown,
384
+ ): Extract<ChatStreamEvent, { type: "usage" }> | undefined {
385
+ if (!isRecord(value)) return undefined;
386
+ const inputTokens = count(value.prompt_tokens);
387
+ const outputTokens = count(value.completion_tokens);
388
+ if (inputTokens === undefined || outputTokens === undefined) return undefined;
389
+ const event: Extract<ChatStreamEvent, { type: "usage" }> = {
390
+ type: "usage",
391
+ inputTokens,
392
+ outputTokens,
393
+ };
394
+ const details = isRecord(value.prompt_tokens_details)
395
+ ? value.prompt_tokens_details
396
+ : undefined;
397
+ const cached = count(details?.cached_tokens);
398
+ if (cached !== undefined) event.cachedInputTokens = cached;
399
+ const rawCost =
400
+ typeof value.cost === "string" ? Number(value.cost) : value.cost;
401
+ if (
402
+ typeof rawCost === "number" &&
403
+ Number.isFinite(rawCost) &&
404
+ rawCost >= 0 &&
405
+ rawCost < 1000
406
+ )
407
+ event.costUsd = rawCost;
408
+ return event;
409
+ }
410
+
411
+ function abortError(signal: AbortSignal): Error {
412
+ const reason: unknown = signal.reason;
413
+ if (reason instanceof Error) return reason;
414
+ const error = new Error("agent request was aborted");
415
+ error.name = "AbortError";
416
+ return error;
417
+ }
418
+
245
419
  async function boundedErrorDetail(response: Response): Promise<string> {
246
420
  try {
247
421
  const text = (await response.text()).slice(0, MAX_ERROR_BODY_BYTES);