@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.
- package/CHANGELOG.md +79 -0
- package/DAWG.md +196 -11
- package/README.md +79 -30
- package/core/diff.ts +249 -0
- package/core/key.ts +43 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +324 -1
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +280 -14
- package/src/agent/brief.ts +23 -3
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/planner.ts +38 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +398 -1
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +27 -12
- package/src/agent/xcb.ts +233 -21
- package/src/audio/click.ts +125 -0
- package/src/audio/engine.ts +407 -33
- package/src/audio/live.ts +152 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +290 -76
- 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/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +24 -7
- package/src/commands/sample.ts +302 -0
- package/src/main.ts +1043 -166
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -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 +292 -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/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 +28 -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 +3 -37
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +261 -18
- package/tui/drums.ts +44 -0
- package/tui/highway.ts +18 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +143 -0
package/src/auth/credentials.ts
CHANGED
|
@@ -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`
|
|
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?:
|
|
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
|
-
|
|
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
|
|
144
|
+
export async function resolveKey(
|
|
93
145
|
auth: AuthEnv,
|
|
146
|
+
kind: KeyKind,
|
|
94
147
|
): Promise<ResolvedKey | undefined> {
|
|
95
|
-
const
|
|
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,
|
|
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?.
|
|
166
|
+
const key = file?.[spec.field];
|
|
103
167
|
if (isValidKey(key)) return { key, source: "file" };
|
|
104
168
|
return undefined;
|
|
105
169
|
}
|
|
106
170
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
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(
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
226
|
+
[spec.field]: key,
|
|
138
227
|
});
|
|
139
228
|
return "file";
|
|
140
229
|
}
|
|
141
230
|
|
|
142
|
-
|
|
231
|
+
export function storeGatewayKey(
|
|
143
232
|
auth: AuthEnv,
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
163
|
-
export async function
|
|
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
|
-
|
|
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
|
-
|
|
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 || !(
|
|
189
|
-
const {
|
|
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?:
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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)) {
|