@hraness/dawg 0.2.0 → 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 (94) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/DAWG.md +196 -11
  3. package/README.md +79 -30
  4. package/core/diff.ts +249 -0
  5. package/core/key.ts +43 -0
  6. package/core/pitch.ts +60 -0
  7. package/core/score.ts +324 -1
  8. package/core/sdk/eval-child.ts +113 -0
  9. package/core/sdk/eval.ts +257 -0
  10. package/core/sdk/print.ts +393 -0
  11. package/core/sdk/v1.ts +954 -0
  12. package/core/slug.ts +19 -0
  13. package/package.json +8 -5
  14. package/src/agent/agent.ts +280 -14
  15. package/src/agent/brief.ts +23 -3
  16. package/src/agent/gateway.ts +215 -41
  17. package/src/agent/models.ts +633 -0
  18. package/src/agent/ops.ts +3 -18
  19. package/src/agent/planner.ts +38 -0
  20. package/src/agent/provider.ts +234 -106
  21. package/src/agent/sse.ts +31 -7
  22. package/src/agent/tools.ts +398 -1
  23. package/src/agent/usage.ts +296 -0
  24. package/src/agent/workspace.ts +683 -0
  25. package/src/agent/xcb-agent.ts +27 -12
  26. package/src/agent/xcb.ts +233 -21
  27. package/src/audio/click.ts +125 -0
  28. package/src/audio/engine.ts +407 -33
  29. package/src/audio/live.ts +152 -0
  30. package/src/audio/player.ts +19 -4
  31. package/src/audio/render-worker.ts +68 -0
  32. package/src/audio/renderer.ts +174 -0
  33. package/src/audio/sampler.ts +292 -0
  34. package/src/audio/samples.ts +683 -0
  35. package/src/audio/wav.ts +290 -76
  36. package/src/auth/cli.ts +146 -27
  37. package/src/auth/credentials.ts +167 -41
  38. package/src/auth/discover.ts +481 -0
  39. package/src/auth/login.ts +885 -128
  40. package/src/auth/openrouter.ts +206 -0
  41. package/src/auth/picker.ts +282 -0
  42. package/src/auth/runner.ts +25 -2
  43. package/src/auth/tui.ts +60 -43
  44. package/src/commands/edit.ts +170 -0
  45. package/src/commands/help.ts +247 -0
  46. package/src/commands/history.ts +32 -19
  47. package/src/commands/music.ts +24 -7
  48. package/src/commands/sample.ts +302 -0
  49. package/src/main.ts +1043 -166
  50. package/src/media/analyze.ts +364 -0
  51. package/src/media/backend.ts +253 -0
  52. package/src/media/cli.ts +173 -0
  53. package/src/media/download.ts +281 -0
  54. package/src/media/dsp.ts +281 -0
  55. package/src/media/import.ts +130 -0
  56. package/src/media/lyrics.ts +201 -0
  57. package/src/media/notes.ts +363 -0
  58. package/src/media/paths.ts +168 -0
  59. package/src/media/process.ts +226 -0
  60. package/src/media/registry.ts +9 -0
  61. package/src/media/sidecar.ts +72 -0
  62. package/src/media/stemdeck.ts +254 -0
  63. package/src/media/stems.ts +173 -0
  64. package/src/media/tools.ts +292 -0
  65. package/src/media/types.ts +92 -0
  66. package/src/media/vendor/basic-pitch.ts +261 -0
  67. package/src/media/vendor/drums.ts +817 -0
  68. package/src/media/vendor/grid.ts +203 -0
  69. package/src/media/vendor/util.ts +139 -0
  70. package/src/media/vendor/wav.ts +233 -0
  71. package/src/project/check.ts +80 -0
  72. package/src/project/init.ts +253 -0
  73. package/src/project/sync.ts +432 -0
  74. package/src/project/typecheck.ts +149 -0
  75. package/src/render.ts +28 -6
  76. package/src/session/attach.ts +3 -4
  77. package/src/session/daemon.ts +25 -8
  78. package/src/session/delta.ts +249 -0
  79. package/src/session/naming.ts +3 -37
  80. package/src/session/port.ts +26 -6
  81. package/src/session/rebase.ts +38 -8
  82. package/src/session/store.ts +116 -21
  83. package/src/tui/menu.ts +1083 -0
  84. package/src/tui/play-mode.ts +442 -0
  85. package/src/tui/play-session.ts +636 -0
  86. package/src/web/fetch.ts +340 -0
  87. package/src/web/http.ts +137 -0
  88. package/src/web/search.ts +681 -0
  89. package/tui/activity.ts +42 -3
  90. package/tui/app.ts +261 -18
  91. package/tui/drums.ts +44 -0
  92. package/tui/highway.ts +18 -2
  93. package/tui/layers.ts +14 -2
  94. package/tui/play-strip.ts +143 -0
@@ -0,0 +1,481 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import {
4
+ keyEnvName,
5
+ maskKey,
6
+ resolveGatewayKey,
7
+ resolveOpenRouterKey,
8
+ type AuthEnv,
9
+ type ResolvedKey,
10
+ } from "./credentials.ts";
11
+ import {
12
+ readAccountRows,
13
+ readCapabilities,
14
+ readXcbVersion,
15
+ refreshAccounts,
16
+ resolveXcbBin,
17
+ versionAtLeast,
18
+ XCB_MIN_VERSION,
19
+ type XcbAccount,
20
+ type XcbAccountRow,
21
+ type XcbModel,
22
+ } from "../agent/xcb.ts";
23
+
24
+ /**
25
+ * Read-only detection of every sign-in option, in parallel, each probe under
26
+ * its own deadline. Nothing here writes, creates keys, or signs in; the only
27
+ * subprocesses are `vercel whoami` and xcb's capability and account listings.
28
+ */
29
+ export const LOGIN_OPTIONS = Object.freeze([
30
+ "gateway",
31
+ "openrouter",
32
+ "codex",
33
+ "claude",
34
+ ] as const);
35
+ export type LoginOption = (typeof LOGIN_OPTIONS)[number];
36
+ export type SubscriptionFamily = "codex" | "claude";
37
+
38
+ export const OPTION_TITLES: Readonly<Record<LoginOption, string>> =
39
+ Object.freeze({
40
+ gateway: "Vercel AI Gateway",
41
+ openrouter: "OpenRouter",
42
+ codex: "ChatGPT / Codex subscription",
43
+ claude: "Claude subscription",
44
+ });
45
+
46
+ export type ProbeState = "ok" | "missing" | "timeout" | "error";
47
+
48
+ export type VercelProbe = Readonly<{
49
+ state: ProbeState;
50
+ bin?: string;
51
+ /** `username (team slug)` when logged in. */
52
+ user?: string;
53
+ loggedIn: boolean;
54
+ }>;
55
+
56
+ export type SubscriptionReadiness =
57
+ "ready" | "pending" | "unavailable" | "signed-out";
58
+
59
+ export type SubscriptionAccount = Readonly<{
60
+ id: string;
61
+ label: string;
62
+ provider: string;
63
+ readiness: SubscriptionReadiness;
64
+ reason: string | null;
65
+ models: readonly XcbModel[];
66
+ account?: XcbAccount;
67
+ }>;
68
+
69
+ export type XcbProbe = Readonly<{
70
+ state: ProbeState;
71
+ bin?: string;
72
+ /** `xcb --version`, when it answered. */
73
+ version?: string;
74
+ /** Installed xcb predates automatic admission (0.20.0). */
75
+ outdated?: boolean;
76
+ accounts: readonly SubscriptionAccount[];
77
+ }>;
78
+
79
+ export type OptionStatus = Readonly<{
80
+ id: LoginOption;
81
+ title: string;
82
+ /** Usable right now with no further step. */
83
+ ready: boolean;
84
+ /** Something was found: ready, or one step away (e.g. Vercel CLI logged in). */
85
+ detected: boolean;
86
+ /** One line for the picker: "✓ found AI_GATEWAY_API_KEY". */
87
+ summary: string;
88
+ }>;
89
+
90
+ export type Discovery = Readonly<{
91
+ options: readonly OptionStatus[];
92
+ gatewayKey?: ResolvedKey;
93
+ openrouterKey?: ResolvedKey;
94
+ vercel: VercelProbe;
95
+ /** `.vercel/project.json` in the working directory. */
96
+ linkedProject: boolean;
97
+ xcb: XcbProbe;
98
+ }>;
99
+
100
+ export const DISCOVERY_TIMEOUT_MS = 4_000;
101
+
102
+ /** Resolve to `undefined` when `promise` misses the deadline. */
103
+ export async function withDeadline<T>(
104
+ promise: Promise<T>,
105
+ ms: number,
106
+ ): Promise<{ value: T } | undefined> {
107
+ let timer: ReturnType<typeof setTimeout> | undefined;
108
+ const timeout = new Promise<undefined>((resolve) => {
109
+ timer = setTimeout(() => resolve(undefined), ms);
110
+ });
111
+ try {
112
+ return await Promise.race([promise.then((value) => ({ value })), timeout]);
113
+ } finally {
114
+ if (timer) clearTimeout(timer);
115
+ }
116
+ }
117
+
118
+ export async function discover(
119
+ auth: AuthEnv,
120
+ options: DiscoverOptions = {},
121
+ ): Promise<Discovery> {
122
+ const timeoutMs = options.timeoutMs ?? DISCOVERY_TIMEOUT_MS;
123
+ const controller = new AbortController();
124
+ try {
125
+ const [gatewayKey, openrouterKey, vercel, linkedProject, xcb] =
126
+ await Promise.all([
127
+ withDeadline(
128
+ resolveGatewayKey(auth).catch(() => undefined),
129
+ timeoutMs,
130
+ ).then((result) => result?.value),
131
+ withDeadline(
132
+ resolveOpenRouterKey(auth).catch(() => undefined),
133
+ timeoutMs,
134
+ ).then((result) => result?.value),
135
+ probeVercel(auth, timeoutMs, controller.signal),
136
+ isFile(join(options.cwd ?? process.cwd(), ".vercel", "project.json")),
137
+ probeXcb(auth, timeoutMs, controller.signal, options),
138
+ ]);
139
+ const partial = { vercel, linkedProject, xcb };
140
+ return {
141
+ ...(gatewayKey ? { gatewayKey } : {}),
142
+ ...(openrouterKey ? { openrouterKey } : {}),
143
+ ...partial,
144
+ options: [
145
+ gatewayStatus(gatewayKey, vercel, linkedProject),
146
+ openrouterStatus(openrouterKey),
147
+ subscriptionStatus("codex", xcb),
148
+ subscriptionStatus("claude", xcb),
149
+ ],
150
+ };
151
+ } finally {
152
+ // Stop any probe still running past its deadline.
153
+ controller.abort();
154
+ }
155
+ }
156
+
157
+ async function isFile(path: string): Promise<boolean> {
158
+ try {
159
+ return (await stat(path)).isFile();
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ async function probeVercel(
166
+ auth: AuthEnv,
167
+ timeoutMs: number,
168
+ signal: AbortSignal,
169
+ ): Promise<VercelProbe> {
170
+ const bin = auth.runner.which("vercel");
171
+ if (!bin) return { state: "missing", loggedIn: false };
172
+ const result = await withDeadline(
173
+ vercelWhoami(auth, bin, { timeoutMs, signal }),
174
+ timeoutMs,
175
+ );
176
+ if (!result) return { state: "timeout", bin, loggedIn: false };
177
+ return result.value
178
+ ? { state: "ok", bin, user: result.value, loggedIn: true }
179
+ : { state: "ok", bin, loggedIn: false };
180
+ }
181
+
182
+ /** `vercel whoami --format json --non-interactive` → `user (team slug)`. */
183
+ export async function vercelWhoami(
184
+ auth: Pick<AuthEnv, "runner">,
185
+ vercel: string,
186
+ options: { timeoutMs?: number; signal?: AbortSignal } = {},
187
+ ): Promise<string | undefined> {
188
+ const result = await auth.runner
189
+ .run(vercel, ["whoami", "--format", "json", "--non-interactive"], {
190
+ timeoutMs: options.timeoutMs ?? 20_000,
191
+ maxOutputBytes: 64 * 1024,
192
+ ...(options.signal ? { signal: options.signal } : {}),
193
+ })
194
+ .catch(() => undefined);
195
+ if (!result || result.code !== 0) return undefined;
196
+ try {
197
+ const parsed: unknown = JSON.parse(result.stdout);
198
+ if (typeof parsed !== "object" || parsed === null) return undefined;
199
+ const record = parsed as Record<string, unknown>;
200
+ const username =
201
+ typeof record.username === "string" ? record.username : undefined;
202
+ if (!username || !/^[\w.@+-]{1,64}$/.test(username)) return undefined;
203
+ const team = record.team as Record<string, unknown> | undefined;
204
+ const slug =
205
+ team && typeof team.slug === "string" && /^[\w.-]{1,64}$/.test(team.slug)
206
+ ? team.slug
207
+ : undefined;
208
+ return slug ? `${username} (team ${slug})` : username;
209
+ } catch {
210
+ return undefined;
211
+ }
212
+ }
213
+
214
+ export type DiscoverOptions = {
215
+ timeoutMs?: number;
216
+ cwd?: string;
217
+ /**
218
+ * Run `xcb accounts refresh` for connected accounts that list no models
219
+ * (sign-in and `dawg auth status` only; startup skips it).
220
+ */
221
+ refreshXcb?: boolean;
222
+ /** Progress lines such as `refreshing 16 codex accounts…`. */
223
+ onProgress?: (line: string) => void;
224
+ };
225
+
226
+ /** Per-account refresh cap; the whole refresh shares one extra deadline. */
227
+ export const XCB_REFRESH_TIMEOUT_MS = 10_000;
228
+
229
+ async function probeXcb(
230
+ auth: AuthEnv,
231
+ timeoutMs: number,
232
+ signal: AbortSignal,
233
+ options: DiscoverOptions = {},
234
+ ): Promise<XcbProbe> {
235
+ const bin = resolveXcbBin(auth.env, auth.runner);
236
+ if (!bin) return { state: "missing", accounts: [] };
237
+ const result = await withDeadline(
238
+ Promise.all([
239
+ readCapabilities(bin, auth.runner, signal),
240
+ readAccountRows(bin, auth.runner, signal),
241
+ readXcbVersion(bin, auth.runner, signal),
242
+ ]),
243
+ timeoutMs,
244
+ ).catch(() => "error" as const);
245
+ if (result === "error") return { state: "error", bin, accounts: [] };
246
+ if (!result) return { state: "timeout", bin, accounts: [] };
247
+ let [capabilities, rows, version] = result.value;
248
+ const stale = capabilities.accounts.filter(
249
+ (account) =>
250
+ account.connected &&
251
+ account.reason === "models_unavailable" &&
252
+ (account.provider === "codex" || account.provider === "claude"),
253
+ );
254
+ if (options.refreshXcb && stale.length > 0) {
255
+ const families = [...new Set(stale.map((account) => account.provider))];
256
+ options.onProgress?.(
257
+ `refreshing ${stale.length} ${families.join("/")} account${stale.length === 1 ? "" : "s"}…`,
258
+ );
259
+ const refreshed = await withDeadline(
260
+ refreshAccounts(
261
+ bin,
262
+ auth.runner,
263
+ stale.map((account) => account.id),
264
+ { timeoutMs: XCB_REFRESH_TIMEOUT_MS, signal },
265
+ ),
266
+ XCB_REFRESH_TIMEOUT_MS + 2_000,
267
+ );
268
+ if (refreshed && refreshed.value.length > 0) {
269
+ const again = await withDeadline(
270
+ readCapabilities(bin, auth.runner, signal),
271
+ timeoutMs,
272
+ ).catch(() => undefined);
273
+ if (again) capabilities = again.value;
274
+ }
275
+ }
276
+ return {
277
+ state: "ok",
278
+ bin,
279
+ ...(version
280
+ ? { version, outdated: !versionAtLeast(version, XCB_MIN_VERSION) }
281
+ : {}),
282
+ accounts: classifyAccounts(capabilities.accounts, rows),
283
+ };
284
+ }
285
+
286
+ /** Merge xcb's capability rows with its sign-in rows into one readiness each. */
287
+ export function classifyAccounts(
288
+ accounts: readonly XcbAccount[],
289
+ rows: readonly XcbAccountRow[],
290
+ ): SubscriptionAccount[] {
291
+ const byId = new Map(rows.map((row) => [row.id, row]));
292
+ const result: SubscriptionAccount[] = [];
293
+ for (const account of accounts) {
294
+ const row = byId.get(account.id);
295
+ if (row && !row.enabled) continue;
296
+ const readiness: SubscriptionReadiness = account.available
297
+ ? account.admission === "pending"
298
+ ? "pending"
299
+ : "ready"
300
+ : row?.authenticationRequired ||
301
+ !account.connected ||
302
+ account.reason === "authentication_required" ||
303
+ account.reason === "not_connected"
304
+ ? "signed-out"
305
+ : "unavailable";
306
+ result.push({
307
+ id: account.id,
308
+ label: account.label,
309
+ provider: account.provider,
310
+ readiness,
311
+ reason: account.reason,
312
+ models: account.models,
313
+ account,
314
+ });
315
+ }
316
+ // Accounts xcb lists but cannot generate with yet (no capability row).
317
+ for (const row of rows) {
318
+ if (!row.enabled || accounts.some((account) => account.id === row.id))
319
+ continue;
320
+ result.push({
321
+ id: row.id,
322
+ label: row.label,
323
+ provider: row.provider,
324
+ readiness: row.authenticationRequired ? "signed-out" : "unavailable",
325
+ reason: null,
326
+ models: [],
327
+ });
328
+ }
329
+ return result;
330
+ }
331
+
332
+ export function usable(account: SubscriptionAccount): boolean {
333
+ return account.readiness === "ready" || account.readiness === "pending";
334
+ }
335
+
336
+ function gatewayStatus(
337
+ key: ResolvedKey | undefined,
338
+ vercel: VercelProbe,
339
+ linkedProject: boolean,
340
+ ): OptionStatus {
341
+ const base = { id: "gateway" as const, title: OPTION_TITLES.gateway };
342
+ if (key)
343
+ return {
344
+ ...base,
345
+ ready: true,
346
+ detected: true,
347
+ summary:
348
+ key.source === "env"
349
+ ? `✓ found ${keyEnvName("gateway")}`
350
+ : key.source === "oidc"
351
+ ? "✓ found VERCEL_OIDC_TOKEN"
352
+ : `✓ saved key ${maskKey(key.key)}`,
353
+ };
354
+ if (vercel.loggedIn)
355
+ return {
356
+ ...base,
357
+ ready: false,
358
+ detected: true,
359
+ summary: `✓ vercel CLI logged in as ${vercel.user}; creates a key`,
360
+ };
361
+ return {
362
+ ...base,
363
+ ready: false,
364
+ detected: false,
365
+ summary:
366
+ vercel.state === "missing"
367
+ ? "not set up · paste a key or install the vercel CLI"
368
+ : vercel.state === "timeout"
369
+ ? "vercel CLI did not answer in time · signs in with `vercel login`"
370
+ : linkedProject
371
+ ? "not signed in · project linked; signs in with `vercel login`"
372
+ : "vercel CLI not logged in · signs in with `vercel login`",
373
+ };
374
+ }
375
+
376
+ function openrouterStatus(key: ResolvedKey | undefined): OptionStatus {
377
+ const base = { id: "openrouter" as const, title: OPTION_TITLES.openrouter };
378
+ if (key)
379
+ return {
380
+ ...base,
381
+ ready: true,
382
+ detected: true,
383
+ summary:
384
+ key.source === "env"
385
+ ? `✓ found ${keyEnvName("openrouter")}`
386
+ : `✓ saved key ${maskKey(key.key)}`,
387
+ };
388
+ return {
389
+ ...base,
390
+ ready: false,
391
+ detected: false,
392
+ summary: "not set up · signs in through your browser",
393
+ };
394
+ }
395
+
396
+ /** Counts per readiness for one subscription family. */
397
+ export function familyCounts(
398
+ xcb: XcbProbe,
399
+ family: string,
400
+ ): Record<SubscriptionReadiness, number> {
401
+ const counts = { ready: 0, pending: 0, unavailable: 0, "signed-out": 0 };
402
+ for (const account of xcb.accounts)
403
+ if (account.provider === family) counts[account.readiness] += 1;
404
+ return counts;
405
+ }
406
+
407
+ function plural(count: number, word: string): string {
408
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
409
+ }
410
+
411
+ function subscriptionStatus(
412
+ family: SubscriptionFamily,
413
+ xcb: XcbProbe,
414
+ ): OptionStatus {
415
+ const base = { id: family, title: OPTION_TITLES[family] };
416
+ const name = family === "codex" ? "Codex" : "Claude";
417
+ if (xcb.state === "missing")
418
+ return {
419
+ ...base,
420
+ ready: false,
421
+ detected: false,
422
+ summary: "not set up · needs xcb",
423
+ };
424
+ if (xcb.state !== "ok")
425
+ return {
426
+ ...base,
427
+ ready: false,
428
+ detected: false,
429
+ summary:
430
+ xcb.state === "timeout"
431
+ ? "xcb did not answer in time"
432
+ : "xcb could not list accounts",
433
+ };
434
+ const counts = familyCounts(xcb, family);
435
+ const parts: string[] = [];
436
+ if (counts.ready > 0)
437
+ parts.push(`${plural(counts.ready, `${name} account`)} ready`);
438
+ if (counts.pending > 0)
439
+ parts.push(
440
+ `${counts.ready > 0 ? counts.pending : plural(counts.pending, `${name} account`)} admission pending`,
441
+ );
442
+ const usableCount = counts.ready + counts.pending;
443
+ if (counts.unavailable > 0) parts.push(`${counts.unavailable} unavailable`);
444
+ if (counts["signed-out"] > 0)
445
+ parts.push(`${counts["signed-out"]} not signed in`);
446
+ if (usableCount === 0 && xcb.outdated)
447
+ parts.push(`upgrade xcb to ${XCB_MIN_VERSION}+`);
448
+ if (parts.length === 0)
449
+ return {
450
+ ...base,
451
+ ready: false,
452
+ detected: false,
453
+ summary: `not signed in · \`xcb setup ${family}\``,
454
+ };
455
+ return {
456
+ ...base,
457
+ ready: usableCount > 0,
458
+ detected: usableCount > 0,
459
+ summary: `${usableCount > 0 ? "✓ " : ""}${parts.join(" · ")}`,
460
+ };
461
+ }
462
+
463
+ /** Usable (account, model) pairs for one family, ready accounts first. */
464
+ export function subscriptionChoices(
465
+ xcb: XcbProbe,
466
+ family: string | undefined,
467
+ ): { account: SubscriptionAccount; model: XcbModel }[] {
468
+ const accounts = xcb.accounts
469
+ .filter(
470
+ (account) =>
471
+ usable(account) &&
472
+ (family === undefined || account.provider === family),
473
+ )
474
+ .sort(
475
+ (a, b) =>
476
+ Number(b.readiness === "ready") - Number(a.readiness === "ready"),
477
+ );
478
+ return accounts.flatMap((account) =>
479
+ account.models.map((model) => ({ account, model })),
480
+ );
481
+ }