@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
@@ -15,30 +15,81 @@ import type { CommandRunner } from "./runner.ts";
15
15
  /**
16
16
  * Local credentials and provider choice. Secrets live in the macOS Keychain
17
17
  * or `~/.config/dawg/credentials.json` (0600 under a 0700 directory), never in
18
- * the project's `.dawg/` directory. `AI_GATEWAY_API_KEY` always wins.
18
+ * the project's `.dawg/` directory. `AI_GATEWAY_API_KEY` and
19
+ * `OPENROUTER_API_KEY` always win over stored keys.
19
20
  */
20
21
  export const KEYCHAIN_SERVICE = "dawg";
21
22
  export const CONFIG_DIR_NAME = "dawg";
22
23
  export const KEYCHAIN_ACCOUNT = "ai-gateway";
24
+
25
+ /** The two key-based providers. Subscriptions keep their secrets in xcb. */
26
+ export type KeyKind = "gateway" | "openrouter";
27
+ type KeySpec = Readonly<{
28
+ account: string;
29
+ field: string;
30
+ env: string;
31
+ label: string;
32
+ }>;
33
+ const KEY_SPECS: Readonly<Record<KeyKind, KeySpec>> = Object.freeze({
34
+ gateway: {
35
+ account: KEYCHAIN_ACCOUNT,
36
+ field: "aiGateway",
37
+ env: "AI_GATEWAY_API_KEY",
38
+ label: "dawg AI Gateway key",
39
+ },
40
+ openrouter: {
41
+ account: "openrouter",
42
+ field: "openrouter",
43
+ env: "OPENROUTER_API_KEY",
44
+ label: "dawg OpenRouter key",
45
+ },
46
+ });
47
+ /** A Vercel OIDC token is a JWT; bound it and keep the alphabet quote-safe. */
48
+ const OIDC_PATTERN = /^[A-Za-z0-9._-]{32,8192}$/;
23
49
  const MAX_CONFIG_BYTES = 16 * 1024;
24
50
  /** Gateway keys are opaque tokens; restricting the alphabet keeps them quote-safe. */
25
51
  const KEY_PATTERN = /^[A-Za-z0-9._-]{16,256}$/;
26
52
  const XCB_ACCOUNT_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
27
53
  const XCB_MODEL_PATTERN = /^[A-Za-z0-9._:/-]{1,200}$/;
28
54
 
55
+ /**
56
+ * `codex` and `claude` are subscriptions through xcb; `xcb` is any xcb
57
+ * account (Devin included), kept for `dawg login --xcb`.
58
+ */
29
59
  export const PROVIDER_CHOICES = Object.freeze([
30
60
  "gateway",
61
+ "openrouter",
62
+ "codex",
63
+ "claude",
31
64
  "xcb",
32
65
  "auto",
33
66
  ] as const);
34
67
  export type ProviderChoice = (typeof PROVIDER_CHOICES)[number];
68
+ export const SUBSCRIPTION_SLOTS = Object.freeze([
69
+ "codex",
70
+ "claude",
71
+ "xcb",
72
+ ] as const);
73
+ export type SubscriptionSlot = (typeof SUBSCRIPTION_SLOTS)[number];
74
+ export type XcbPick = Readonly<{ account: string; model: string }>;
35
75
 
36
76
  export type DawgConfig = Readonly<{
37
77
  provider?: ProviderChoice;
38
- xcb?: Readonly<{ account: string; model: string }>;
78
+ xcb?: XcbPick;
79
+ codex?: XcbPick;
80
+ claude?: XcbPick;
81
+ /** Last model chosen per key-based provider: a `provider/model` ID. */
82
+ gatewayModel?: string;
83
+ openrouterModel?: string;
84
+ /** Set when the first-run picker was dismissed, so it does not nag. */
85
+ setup?: "skipped";
39
86
  }>;
40
87
 
41
- export type CredentialSource = "env" | "keychain" | "file";
88
+ /** `vendor/model` IDs as the AI Gateway and OpenRouter spell them. */
89
+ export const PROVIDER_MODEL_ID_PATTERN =
90
+ /^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}$/i;
91
+
92
+ export type CredentialSource = "env" | "keychain" | "file" | "oidc";
42
93
  export type ResolvedKey = Readonly<{ key: string; source: CredentialSource }>;
43
94
 
44
95
  export type AuthEnv = Readonly<{
@@ -74,6 +125,7 @@ export function isValidKey(value: unknown): value is string {
74
125
  /** `vck_…abcd`: never more than a short prefix and the last four characters. */
75
126
  export function maskKey(key: string): string {
76
127
  if (key.length < 12) return "…";
128
+ if (key.startsWith("sk-or-")) return `sk-or-…${key.slice(-4)}`;
77
129
  const underscore = key.indexOf("_");
78
130
  const prefix =
79
131
  underscore > 0 && underscore <= 4 ? key.slice(0, underscore + 1) : "";
@@ -89,42 +141,79 @@ function useKeychain(auth: AuthEnv): boolean {
89
141
  }
90
142
 
91
143
  /** Env, then Keychain, then the credentials file. */
92
- export async function resolveGatewayKey(
144
+ export async function resolveKey(
93
145
  auth: AuthEnv,
146
+ kind: KeyKind,
94
147
  ): Promise<ResolvedKey | undefined> {
95
- const fromEnv = auth.env.AI_GATEWAY_API_KEY?.trim();
148
+ const spec = KEY_SPECS[kind];
149
+ const fromEnv = auth.env[spec.env]?.trim();
96
150
  if (fromEnv) return { key: fromEnv, source: "env" };
151
+ const stored = await resolveStoredKey(auth, kind);
152
+ return stored;
153
+ }
154
+
155
+ /** The Keychain or the credentials file, ignoring the environment. */
156
+ export async function resolveStoredKey(
157
+ auth: AuthEnv,
158
+ kind: KeyKind,
159
+ ): Promise<ResolvedKey | undefined> {
160
+ const spec = KEY_SPECS[kind];
97
161
  if (useKeychain(auth)) {
98
- const key = (await readKeychain(auth, KEYCHAIN_SERVICE)) ?? "";
162
+ const key = (await readKeychain(auth, spec.account)) ?? "";
99
163
  if (isValidKey(key)) return { key, source: "keychain" };
100
164
  }
101
165
  const file = await readJsonObject(join(auth.dir, "credentials.json"));
102
- const key = file?.aiGateway;
166
+ const key = file?.[spec.field];
103
167
  if (isValidKey(key)) return { key, source: "file" };
104
168
  return undefined;
105
169
  }
106
170
 
107
- /** Store the key. Returns where it went. Throws on an invalid key. */
108
- export async function storeGatewayKey(
171
+ /**
172
+ * The gateway credential: `AI_GATEWAY_API_KEY`, a stored key, then a
173
+ * `VERCEL_OIDC_TOKEN` (what `vercel env pull` writes for a linked project;
174
+ * AI Gateway accepts it as a bearer token until it expires).
175
+ */
176
+ export async function resolveGatewayKey(
177
+ auth: AuthEnv,
178
+ ): Promise<ResolvedKey | undefined> {
179
+ const key = await resolveKey(auth, "gateway");
180
+ if (key) return key;
181
+ const oidc = auth.env.VERCEL_OIDC_TOKEN?.trim();
182
+ if (oidc && OIDC_PATTERN.test(oidc)) return { key: oidc, source: "oidc" };
183
+ return undefined;
184
+ }
185
+
186
+ export function resolveOpenRouterKey(
109
187
  auth: AuthEnv,
188
+ ): Promise<ResolvedKey | undefined> {
189
+ return resolveKey(auth, "openrouter");
190
+ }
191
+
192
+ /** Store a key. Returns where it went. Throws on an invalid key. */
193
+ export async function storeKey(
194
+ auth: AuthEnv,
195
+ kind: KeyKind,
110
196
  key: string,
111
197
  ): Promise<"keychain" | "file"> {
198
+ const spec = KEY_SPECS[kind];
112
199
  if (!isValidKey(key))
113
- throw new Error("that does not look like an AI Gateway key");
200
+ throw new Error(
201
+ `that does not look like an ${kind === "gateway" ? "AI Gateway" : "OpenRouter"} key`,
202
+ );
114
203
  if (useKeychain(auth)) {
115
204
  // `security -i` reads the command from stdin, so the secret never appears
116
205
  // in any process's argv. KEY_PATTERN guarantees it needs no escaping.
117
206
  const result = await auth.runner
118
207
  .run("security", ["-i"], {
119
- stdin: `add-generic-password -U -s ${KEYCHAIN_SERVICE} -a ${KEYCHAIN_ACCOUNT} -l "dawg AI Gateway key" -w "${key}"\n`,
208
+ stdin: `add-generic-password -U -s ${KEYCHAIN_SERVICE} -a ${spec.account} -l "${spec.label}" -w "${key}"\n`,
120
209
  timeoutMs: 15_000,
121
210
  maxOutputBytes: 4096,
122
211
  })
123
212
  .catch(() => undefined);
124
213
  if (result?.code === 0) {
125
- const check = await resolveFromKeychainOnly(auth);
214
+ const check = await readKeychain(auth, spec.account);
126
215
  if (check === key) {
127
- await removeFileKey(auth);
216
+ await removeFileKey(auth, kind);
128
217
  return "keychain";
129
218
  }
130
219
  }
@@ -134,33 +223,37 @@ export async function storeGatewayKey(
134
223
  await writePrivateJson(auth.dir, path, {
135
224
  ...current,
136
225
  version: 1,
137
- aiGateway: key,
226
+ [spec.field]: key,
138
227
  });
139
228
  return "file";
140
229
  }
141
230
 
142
- async function resolveFromKeychainOnly(
231
+ export function storeGatewayKey(
143
232
  auth: AuthEnv,
144
- ): Promise<string | undefined> {
145
- return readKeychain(auth, KEYCHAIN_SERVICE);
233
+ key: string,
234
+ ): Promise<"keychain" | "file"> {
235
+ return storeKey(auth, "gateway", key);
146
236
  }
147
237
 
148
238
  async function readKeychain(
149
239
  auth: AuthEnv,
150
- service: string,
240
+ account: string,
151
241
  ): Promise<string | undefined> {
152
242
  const result = await auth.runner
153
243
  .run(
154
244
  "security",
155
- ["find-generic-password", "-s", service, "-a", KEYCHAIN_ACCOUNT, "-w"],
245
+ ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"],
156
246
  { timeoutMs: 10_000, maxOutputBytes: 4096 },
157
247
  )
158
248
  .catch(() => undefined);
159
249
  return result?.code === 0 ? result.stdout.trim() : undefined;
160
250
  }
161
251
 
162
- /** Remove stored keys from both backends. Env keys are left to the user. */
163
- export async function clearGatewayKey(auth: AuthEnv): Promise<string[]> {
252
+ /** Remove a stored key from both backends. Env keys are left to the user. */
253
+ export async function clearKey(
254
+ auth: AuthEnv,
255
+ kind: KeyKind,
256
+ ): Promise<string[]> {
164
257
  const removed: string[] = [];
165
258
  if (useKeychain(auth)) {
166
259
  const result = await auth.runner
@@ -171,22 +264,32 @@ export async function clearGatewayKey(auth: AuthEnv): Promise<string[]> {
171
264
  "-s",
172
265
  KEYCHAIN_SERVICE,
173
266
  "-a",
174
- KEYCHAIN_ACCOUNT,
267
+ KEY_SPECS[kind].account,
175
268
  ],
176
269
  { timeoutMs: 10_000, maxOutputBytes: 4096 },
177
270
  )
178
271
  .catch(() => undefined);
179
272
  if (result?.code === 0) removed.push("keychain");
180
273
  }
181
- if (await removeFileKey(auth)) removed.push("file");
274
+ if (await removeFileKey(auth, kind)) removed.push("file");
182
275
  return removed;
183
276
  }
184
277
 
185
- async function removeFileKey(auth: AuthEnv): Promise<boolean> {
278
+ export function clearGatewayKey(auth: AuthEnv): Promise<string[]> {
279
+ return clearKey(auth, "gateway");
280
+ }
281
+
282
+ /** The environment variable that overrides a stored key. */
283
+ export function keyEnvName(kind: KeyKind): string {
284
+ return KEY_SPECS[kind].env;
285
+ }
286
+
287
+ async function removeFileKey(auth: AuthEnv, kind: KeyKind): Promise<boolean> {
288
+ const field = KEY_SPECS[kind].field;
186
289
  const path = join(auth.dir, "credentials.json");
187
290
  const current = await readJsonObject(path);
188
- if (!current || !("aiGateway" in current)) return false;
189
- const { aiGateway: _removed, ...rest } = current;
291
+ if (!current || !(field in current)) return false;
292
+ const { [field]: _removed, ...rest } = current;
190
293
  if (Object.keys(rest).filter((key) => key !== "version").length === 0)
191
294
  await rm(path, { force: true });
192
295
  else await writePrivateJson(auth.dir, path, rest);
@@ -205,30 +308,53 @@ export function parseConfig(value: unknown): DawgConfig {
205
308
  const record = value as Record<string, unknown>;
206
309
  const config: {
207
310
  provider?: ProviderChoice;
208
- xcb?: { account: string; model: string };
311
+ xcb?: XcbPick;
312
+ codex?: XcbPick;
313
+ claude?: XcbPick;
314
+ gatewayModel?: string;
315
+ openrouterModel?: string;
316
+ setup?: "skipped";
209
317
  } = {};
210
318
  if (PROVIDER_CHOICES.includes(record.provider as ProviderChoice))
211
319
  config.provider = record.provider as ProviderChoice;
212
- const xcb = record.xcb;
213
- if (typeof xcb === "object" && xcb !== null && !Array.isArray(xcb)) {
214
- const { account, model } = xcb as Record<string, unknown>;
215
- if (
216
- typeof account === "string" &&
217
- XCB_ACCOUNT_PATTERN.test(account) &&
218
- typeof model === "string" &&
219
- XCB_MODEL_PATTERN.test(model)
220
- )
221
- config.xcb = { account, model };
320
+ for (const slot of SUBSCRIPTION_SLOTS) {
321
+ const pick = parsePick(record[slot]);
322
+ if (pick) config[slot] = pick;
222
323
  }
324
+ for (const field of ["gatewayModel", "openrouterModel"] as const) {
325
+ const id = record[field];
326
+ if (typeof id === "string" && PROVIDER_MODEL_ID_PATTERN.test(id))
327
+ config[field] = id;
328
+ }
329
+ if (record.setup === "skipped") config.setup = "skipped";
223
330
  return config;
224
331
  }
225
332
 
333
+ function parsePick(value: unknown): XcbPick | undefined {
334
+ if (typeof value !== "object" || value === null || Array.isArray(value))
335
+ return undefined;
336
+ const { account, model } = value as Record<string, unknown>;
337
+ return typeof account === "string" &&
338
+ XCB_ACCOUNT_PATTERN.test(account) &&
339
+ typeof model === "string" &&
340
+ XCB_MODEL_PATTERN.test(model)
341
+ ? { account, model }
342
+ : undefined;
343
+ }
344
+
345
+ export type ConfigPatch = Partial<{
346
+ provider: ProviderChoice | undefined;
347
+ xcb: XcbPick | undefined;
348
+ codex: XcbPick | undefined;
349
+ claude: XcbPick | undefined;
350
+ gatewayModel: string | undefined;
351
+ openrouterModel: string | undefined;
352
+ setup: "skipped" | undefined;
353
+ }>;
354
+
226
355
  export async function writeConfig(
227
356
  auth: Pick<AuthEnv, "dir">,
228
- patch: Partial<{
229
- provider: ProviderChoice | undefined;
230
- xcb: DawgConfig["xcb"] | undefined;
231
- }>,
357
+ patch: ConfigPatch,
232
358
  ): Promise<DawgConfig> {
233
359
  const next: Record<string, unknown> = { ...(await readConfig(auth)) };
234
360
  for (const [key, value] of Object.entries(patch)) {