@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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- 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 +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- 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 +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- 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 +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- 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 +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { estimateRequestCost, formatUsd, type Price } from "./models.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spend accounting for the line under the prompt
|
|
8
|
+
* (`$0.12 session · $0.48 today · opus-5.5 · gateway`).
|
|
9
|
+
*
|
|
10
|
+
* Each response's reported usage is priced once: the provider's own `cost`
|
|
11
|
+
* when it sends one (OpenRouter, the AI Gateway), otherwise tokens × the
|
|
12
|
+
* models.dev price. Session spend lives in memory; today's spend comes from
|
|
13
|
+
* `~/.config/dawg/usage.json`, a small per-day ledger shared by every window.
|
|
14
|
+
* Writes take an exclusive lock file and replace the ledger atomically.
|
|
15
|
+
*/
|
|
16
|
+
export type UsageReport = Readonly<{
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
cachedInputTokens?: number;
|
|
20
|
+
costUsd?: number;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
/** USD for one response; undefined when neither cost nor price is known. */
|
|
24
|
+
export function priceUsage(
|
|
25
|
+
usage: UsageReport,
|
|
26
|
+
price: Price | undefined,
|
|
27
|
+
): number | undefined {
|
|
28
|
+
if (usage.costUsd !== undefined) return usage.costUsd;
|
|
29
|
+
if (!price) return undefined;
|
|
30
|
+
return estimateRequestCost(price, usage);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DayTotal = Readonly<{
|
|
34
|
+
usd: number;
|
|
35
|
+
requests: number;
|
|
36
|
+
inputTokens: number;
|
|
37
|
+
outputTokens: number;
|
|
38
|
+
}>;
|
|
39
|
+
export type Ledger = Readonly<{
|
|
40
|
+
version: 1;
|
|
41
|
+
days: Readonly<Record<string, DayTotal>>;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
/** Days kept in the ledger; older ones are pruned on write. */
|
|
45
|
+
export const LEDGER_DAYS = 31;
|
|
46
|
+
const MAX_LEDGER_BYTES = 64 * 1024;
|
|
47
|
+
const LOCK_STALE_MS = 10_000;
|
|
48
|
+
|
|
49
|
+
/** Local calendar day, `2026-10-06`. */
|
|
50
|
+
export function dayKey(at: Date = new Date()): string {
|
|
51
|
+
const month = String(at.getMonth() + 1).padStart(2, "0");
|
|
52
|
+
const day = String(at.getDate()).padStart(2, "0");
|
|
53
|
+
return `${at.getFullYear()}-${month}-${day}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function finite(value: unknown, max: number): number {
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
58
|
+
? Math.min(value, max)
|
|
59
|
+
: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseLedger(value: unknown): Ledger {
|
|
63
|
+
const days: Record<string, DayTotal> = {};
|
|
64
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
65
|
+
const raw = (value as { days?: unknown }).days;
|
|
66
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw))
|
|
67
|
+
for (const [key, total] of Object.entries(raw)) {
|
|
68
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) continue;
|
|
69
|
+
if (typeof total !== "object" || total === null) continue;
|
|
70
|
+
const row = total as Record<string, unknown>;
|
|
71
|
+
days[key] = {
|
|
72
|
+
usd: finite(row.usd, 1e6),
|
|
73
|
+
requests: Math.floor(finite(row.requests, 1e9)),
|
|
74
|
+
inputTokens: Math.floor(finite(row.inputTokens, 1e13)),
|
|
75
|
+
outputTokens: Math.floor(finite(row.outputTokens, 1e13)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { version: 1, days: prune(days) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function prune(days: Record<string, DayTotal>): Record<string, DayTotal> {
|
|
83
|
+
const keys = Object.keys(days).sort().slice(-LEDGER_DAYS);
|
|
84
|
+
return Object.fromEntries(keys.map((key) => [key, days[key]!]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function addToLedger(
|
|
88
|
+
ledger: Ledger,
|
|
89
|
+
day: string,
|
|
90
|
+
usd: number,
|
|
91
|
+
usage: UsageReport,
|
|
92
|
+
): Ledger {
|
|
93
|
+
const current = ledger.days[day] ?? {
|
|
94
|
+
usd: 0,
|
|
95
|
+
requests: 0,
|
|
96
|
+
inputTokens: 0,
|
|
97
|
+
outputTokens: 0,
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
version: 1,
|
|
101
|
+
days: prune({
|
|
102
|
+
...ledger.days,
|
|
103
|
+
[day]: {
|
|
104
|
+
usd: current.usd + usd,
|
|
105
|
+
requests: current.requests + 1,
|
|
106
|
+
inputTokens: current.inputTokens + usage.inputTokens,
|
|
107
|
+
outputTokens: current.outputTokens + usage.outputTokens,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function readLedger(dir: string): Promise<Ledger> {
|
|
114
|
+
const path = join(dir, "usage.json");
|
|
115
|
+
try {
|
|
116
|
+
const info = await stat(path);
|
|
117
|
+
if (!info.isFile() || info.size > MAX_LEDGER_BYTES) return parseLedger({});
|
|
118
|
+
return parseLedger(JSON.parse(await readFile(path, "utf8")));
|
|
119
|
+
} catch {
|
|
120
|
+
return parseLedger({});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function withLock<T>(dir: string, run: () => Promise<T>): Promise<T> {
|
|
125
|
+
const lock = join(dir, "usage.json.lock");
|
|
126
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
127
|
+
try {
|
|
128
|
+
const handle = await open(lock, "wx", 0o600);
|
|
129
|
+
await handle.close();
|
|
130
|
+
break;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
133
|
+
const info = await stat(lock).catch(() => undefined);
|
|
134
|
+
if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS)
|
|
135
|
+
await rm(lock, { force: true });
|
|
136
|
+
else if (attempt > 50) throw new Error("usage ledger is locked");
|
|
137
|
+
else await Bun.sleep(20);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return await run();
|
|
142
|
+
} finally {
|
|
143
|
+
await rm(lock, { force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Add one priced response to today's total; returns the updated ledger. */
|
|
148
|
+
export async function recordUsage(
|
|
149
|
+
dir: string,
|
|
150
|
+
usd: number,
|
|
151
|
+
usage: UsageReport,
|
|
152
|
+
at: Date = new Date(),
|
|
153
|
+
): Promise<Ledger> {
|
|
154
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
155
|
+
return withLock(dir, async () => {
|
|
156
|
+
const next = addToLedger(await readLedger(dir), dayKey(at), usd, usage);
|
|
157
|
+
const path = join(dir, "usage.json");
|
|
158
|
+
const temp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
159
|
+
const handle = await open(temp, "wx", 0o600);
|
|
160
|
+
try {
|
|
161
|
+
await handle.writeFile(`${JSON.stringify(next)}\n`, "utf8");
|
|
162
|
+
} finally {
|
|
163
|
+
await handle.close();
|
|
164
|
+
}
|
|
165
|
+
await rename(temp, path);
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** In-memory session totals plus the shared daily ledger. */
|
|
171
|
+
export class SpendMeter {
|
|
172
|
+
sessionUsd = 0;
|
|
173
|
+
todayUsd = 0;
|
|
174
|
+
/** True once any response could not be priced. */
|
|
175
|
+
unpriced = false;
|
|
176
|
+
private chain: Promise<unknown> = Promise.resolve();
|
|
177
|
+
|
|
178
|
+
constructor(
|
|
179
|
+
private readonly dir: string,
|
|
180
|
+
private readonly now: () => Date = () => new Date(),
|
|
181
|
+
) {}
|
|
182
|
+
|
|
183
|
+
/** Load today's total from the ledger (other windows count too). */
|
|
184
|
+
async load(): Promise<void> {
|
|
185
|
+
const ledger = await readLedger(this.dir);
|
|
186
|
+
this.todayUsd = ledger.days[dayKey(this.now())]?.usd ?? 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Account one response. Ledger writes are serialized and best-effort. */
|
|
190
|
+
add(usage: UsageReport, price: Price | undefined): number | undefined {
|
|
191
|
+
const usd = priceUsage(usage, price);
|
|
192
|
+
if (usd === undefined) {
|
|
193
|
+
this.unpriced = true;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
this.sessionUsd += usd;
|
|
197
|
+
this.todayUsd += usd;
|
|
198
|
+
this.chain = this.chain
|
|
199
|
+
.then(() => recordUsage(this.dir, usd, usage, this.now()))
|
|
200
|
+
.then((ledger) => {
|
|
201
|
+
const today = ledger.days[dayKey(this.now())]?.usd;
|
|
202
|
+
if (today !== undefined) this.todayUsd = today;
|
|
203
|
+
})
|
|
204
|
+
.catch(() => undefined);
|
|
205
|
+
return usd;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Account a dollar amount with no tokens (billed web searches). */
|
|
209
|
+
addUsd(usd: number): void {
|
|
210
|
+
if (!Number.isFinite(usd) || usd <= 0) return;
|
|
211
|
+
this.sessionUsd += usd;
|
|
212
|
+
this.todayUsd += usd;
|
|
213
|
+
const usage: UsageReport = {
|
|
214
|
+
inputTokens: 0,
|
|
215
|
+
outputTokens: 0,
|
|
216
|
+
costUsd: usd,
|
|
217
|
+
};
|
|
218
|
+
this.chain = this.chain
|
|
219
|
+
.then(() => recordUsage(this.dir, usd, usage, this.now()))
|
|
220
|
+
.then((ledger) => {
|
|
221
|
+
const today = ledger.days[dayKey(this.now())]?.usd;
|
|
222
|
+
if (today !== undefined) this.todayUsd = today;
|
|
223
|
+
})
|
|
224
|
+
.catch(() => undefined);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Wait for pending ledger writes (tests, exit). */
|
|
228
|
+
flush(): Promise<unknown> {
|
|
229
|
+
return this.chain;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The spend line: `$0.12 session · $0.48 today · opus-5.5 · gateway`;
|
|
235
|
+
* `subscription · sonnet · claude`; `no model · dawg login`. Narrow widths
|
|
236
|
+
* drop the today total, then the session total, then the provider.
|
|
237
|
+
*/
|
|
238
|
+
export function spendLine(
|
|
239
|
+
options: {
|
|
240
|
+
kind: "api" | "subscription" | "offline";
|
|
241
|
+
model?: string;
|
|
242
|
+
provider?: string;
|
|
243
|
+
sessionUsd?: number;
|
|
244
|
+
todayUsd?: number;
|
|
245
|
+
},
|
|
246
|
+
width = 120,
|
|
247
|
+
): string {
|
|
248
|
+
if (options.kind === "offline") return "no model · dawg login";
|
|
249
|
+
const tail = [options.model, options.provider].filter(Boolean) as string[];
|
|
250
|
+
const money =
|
|
251
|
+
options.kind === "subscription"
|
|
252
|
+
? ["subscription"]
|
|
253
|
+
: [
|
|
254
|
+
`${formatUsd(options.sessionUsd ?? 0)} session`,
|
|
255
|
+
`${formatUsd(options.todayUsd ?? 0)} today`,
|
|
256
|
+
];
|
|
257
|
+
const candidates = [
|
|
258
|
+
[...money, ...tail],
|
|
259
|
+
[money[0]!, ...tail],
|
|
260
|
+
[money[0]!, ...tail.slice(0, 1)],
|
|
261
|
+
tail.slice(0, 1),
|
|
262
|
+
];
|
|
263
|
+
for (const parts of candidates) {
|
|
264
|
+
const line = parts.join(" · ");
|
|
265
|
+
if (line.length <= width) return line;
|
|
266
|
+
}
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The agent's web options for a provider selection: the turn's own gateway
|
|
272
|
+
* or OpenRouter key powers paid search, and billed searches land in the
|
|
273
|
+
* same spend meter (and daily ledger) as model responses.
|
|
274
|
+
*/
|
|
275
|
+
export function webHostFor(
|
|
276
|
+
selection: Readonly<{ kind: string; apiKey?: string }>,
|
|
277
|
+
meter: Pick<SpendMeter, "addUsd">,
|
|
278
|
+
onChange: () => void = () => {},
|
|
279
|
+
): {
|
|
280
|
+
gatewayApiKey?: string;
|
|
281
|
+
openRouterApiKey?: string;
|
|
282
|
+
onSpend: (spend: Readonly<{ usd: number }>) => void;
|
|
283
|
+
} {
|
|
284
|
+
return {
|
|
285
|
+
...(selection.kind === "gateway" && selection.apiKey
|
|
286
|
+
? { gatewayApiKey: selection.apiKey }
|
|
287
|
+
: {}),
|
|
288
|
+
...(selection.kind === "openrouter" && selection.apiKey
|
|
289
|
+
? { openRouterApiKey: selection.apiKey }
|
|
290
|
+
: {}),
|
|
291
|
+
onSpend: (spend) => {
|
|
292
|
+
meter.addUsd(spend.usd);
|
|
293
|
+
onChange();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|