@ericdisero/aurora-shared 0.1.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 (44) hide show
  1. package/README.md +9 -0
  2. package/dist/audio/ffmpeg.d.ts +21 -0
  3. package/dist/audio/ffmpeg.js +112 -0
  4. package/dist/audio/wav.d.ts +15 -0
  5. package/dist/audio/wav.js +159 -0
  6. package/dist/config.d.ts +14 -0
  7. package/dist/config.js +50 -0
  8. package/dist/db.d.ts +3 -0
  9. package/dist/db.js +121 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.js +8 -0
  12. package/dist/jobs.d.ts +45 -0
  13. package/dist/jobs.js +220 -0
  14. package/dist/operations/index.d.ts +12 -0
  15. package/dist/operations/index.js +848 -0
  16. package/dist/paths.d.ts +17 -0
  17. package/dist/paths.js +79 -0
  18. package/dist/providers/mvsep.d.ts +27 -0
  19. package/dist/providers/mvsep.js +112 -0
  20. package/dist/providers/suno.d.ts +89 -0
  21. package/dist/providers/suno.js +309 -0
  22. package/dist/sidecars.d.ts +20 -0
  23. package/dist/sidecars.js +109 -0
  24. package/dist/skills/content.d.ts +1 -0
  25. package/dist/skills/content.js +9 -0
  26. package/dist/split.d.ts +24 -0
  27. package/dist/split.js +162 -0
  28. package/dist/stack.d.ts +19 -0
  29. package/dist/stack.js +139 -0
  30. package/dist/storage/assets.d.ts +30 -0
  31. package/dist/storage/assets.js +103 -0
  32. package/dist/storage/projects.d.ts +12 -0
  33. package/dist/storage/projects.js +85 -0
  34. package/dist/storage/references.d.ts +10 -0
  35. package/dist/storage/references.js +54 -0
  36. package/dist/storage/stems.d.ts +13 -0
  37. package/dist/storage/stems.js +41 -0
  38. package/dist/types.d.ts +72 -0
  39. package/dist/types.js +5 -0
  40. package/package.json +51 -0
  41. package/skills/aurora-cost-discipline.md +31 -0
  42. package/skills/aurora-music-production.md +43 -0
  43. package/skills/aurora-split-and-stems.md +33 -0
  44. package/skills/aurora-suno-prompting.md +35 -0
package/dist/jobs.js ADDED
@@ -0,0 +1,220 @@
1
+ // Background-job model. Long ops (generate / sounds / cover / split) can submit
2
+ // and return immediately; the job's provider handles (taskId / MVSEP hashes)
3
+ // persist to userData/agent-jobs/<jobId>.json, so status survives process
4
+ // restarts — aurora_get_job_status re-polls the PROVIDER, not in-process state,
5
+ // and finishes downloads/DB-landing the moment results are ready (per-stem
6
+ // progressive for splits). Mirrors the bridge's job.json manifest discipline.
7
+ import { join, extname } from 'node:path';
8
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
9
+ import { existsSync } from 'node:fs';
10
+ import { getJobsDir } from './paths.js';
11
+ import { downloadTo, fetchGenerationRecord, isGenerationFailure } from './providers/suno.js';
12
+ import { fetchSeparationStatus, resolveSeparationStatus } from './providers/mvsep.js';
13
+ import { ensureKindDir, getAsset, insertAsset, uniqueDestPath } from './storage/assets.js';
14
+ import { landSplitJob, finalizeSplit } from './split.js';
15
+ function jobPath(jobId) {
16
+ return join(getJobsDir(), `${jobId}.json`);
17
+ }
18
+ export async function saveJob(m) {
19
+ m.updatedAt = new Date().toISOString();
20
+ await mkdir(getJobsDir(), { recursive: true });
21
+ await writeFile(jobPath(m.jobId), JSON.stringify(m, null, 2));
22
+ }
23
+ export async function loadJob(jobId) {
24
+ const p = jobPath(jobId);
25
+ if (!existsSync(p))
26
+ return null;
27
+ try {
28
+ return JSON.parse(await readFile(p, 'utf-8'));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export async function listJobs() {
35
+ const dir = getJobsDir();
36
+ if (!existsSync(dir))
37
+ return [];
38
+ const files = (await readdir(dir)).filter((f) => f.endsWith('.json'));
39
+ const jobs = [];
40
+ for (const f of files) {
41
+ try {
42
+ jobs.push(JSON.parse(await readFile(join(dir, f), 'utf-8')));
43
+ }
44
+ catch {
45
+ // skip unreadable manifests
46
+ }
47
+ }
48
+ return jobs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
49
+ }
50
+ export function newJobManifest(kind, jobId, projectId, baseName, params, provider) {
51
+ const now = new Date().toISOString();
52
+ return {
53
+ jobId,
54
+ kind,
55
+ status: 'running',
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ projectId,
59
+ baseName,
60
+ params,
61
+ provider,
62
+ landed: {},
63
+ assetIds: [],
64
+ stems: [],
65
+ stage: 'submitted'
66
+ };
67
+ }
68
+ function sanitizeFileName(name) {
69
+ return name.replace(/[\\/:*?"<>|]/g, '_').trim() || 'track';
70
+ }
71
+ /** Land finished generation/sounds/cover variations as project assets —
72
+ * mirrors the app's generation:generate landing (MP3 + audioId in origin;
73
+ * the app's or MCP's fetch-WAV upgrades on demand). */
74
+ async function landGenerationAssets(m, variations) {
75
+ const kind = m.kind === 'cover' ? 'cover' : 'generation';
76
+ const outputDir = await ensureKindDir(m.projectId, kind);
77
+ for (let i = 0; i < variations.length; i++) {
78
+ const v = variations[i];
79
+ if (!v.audioUrl)
80
+ continue;
81
+ const variantName = variations.length > 1 ? `${m.baseName} v${i + 1}` : m.baseName;
82
+ const ext = extname(new URL(v.audioUrl).pathname) || '.mp3';
83
+ const dest = uniqueDestPath(outputDir, `${sanitizeFileName(variantName)}${ext}`);
84
+ await downloadTo(v.audioUrl, dest);
85
+ const asset = insertAsset({
86
+ projectId: m.projectId,
87
+ kind,
88
+ name: sanitizeFileName(variantName),
89
+ path: dest,
90
+ origin: {
91
+ provider: 'sunoapi',
92
+ ...m.params,
93
+ taskId: m.provider.taskId,
94
+ audioId: v.id ?? null
95
+ },
96
+ sourceAssetId: m.provider.sourceAssetId ?? null
97
+ });
98
+ m.assetIds.push(asset.id);
99
+ }
100
+ }
101
+ // Per-job serialization: two concurrent status polls advancing the same job
102
+ // would double-land assets (both read landed=false, both download + insert).
103
+ // Within this process, the second caller awaits the first and gets its result.
104
+ // (Cross-process races — CLI + MCP simultaneously — are narrowed by the fresh
105
+ // manifest re-read below, not fully eliminated; acceptable for v1 dogfood.)
106
+ const advanceLocks = new Map();
107
+ /** Advance a running job by ONE provider poll, landing whatever is ready.
108
+ * Idempotent and serialized per jobId — the status op can hit it repeatedly. */
109
+ export async function advanceJob(m) {
110
+ if (m.status !== 'running')
111
+ return m;
112
+ const inFlight = advanceLocks.get(m.jobId);
113
+ if (inFlight)
114
+ return inFlight;
115
+ const run = (async () => {
116
+ // Re-read disk state so a concurrent process's landing isn't repeated.
117
+ const fresh = (await loadJob(m.jobId)) ?? m;
118
+ if (fresh.status !== 'running')
119
+ return fresh;
120
+ try {
121
+ if (fresh.kind === 'split') {
122
+ await advanceSplit(fresh);
123
+ }
124
+ else {
125
+ await advanceGeneration(fresh);
126
+ }
127
+ }
128
+ catch (err) {
129
+ fresh.status = 'error';
130
+ fresh.error = err instanceof Error ? err.message : String(err);
131
+ fresh.stage = 'error';
132
+ }
133
+ await saveJob(fresh);
134
+ return fresh;
135
+ })();
136
+ advanceLocks.set(m.jobId, run);
137
+ try {
138
+ return await run;
139
+ }
140
+ finally {
141
+ advanceLocks.delete(m.jobId);
142
+ }
143
+ }
144
+ async function advanceGeneration(m) {
145
+ const taskId = m.provider.taskId;
146
+ if (!taskId)
147
+ throw new Error('Job manifest has no provider taskId');
148
+ const record = await fetchGenerationRecord(taskId);
149
+ m.lastStatus = record.status;
150
+ // Monotonic grow only — providers can drop streamAudioUrl from later
151
+ // responses; never shrink a previously-seen preview list.
152
+ const streams = record.variations
153
+ .map((v) => v.streamAudioUrl)
154
+ .filter((u) => Boolean(u));
155
+ if (streams.length > (m.streamUrls?.length ?? 0))
156
+ m.streamUrls = streams;
157
+ if (record.status === 'SUCCESS' || record.status === 'CALLBACK_EXCEPTION') {
158
+ const ready = record.variations.filter((v) => v.audioUrl);
159
+ if (ready.length === 0) {
160
+ if (record.status === 'SUCCESS') {
161
+ throw new Error('Provider reported SUCCESS but returned no audio URLs');
162
+ }
163
+ m.stage = 'finishing (callback grace window)';
164
+ return;
165
+ }
166
+ if (!m.landed.assets) {
167
+ m.stage = 'downloading variations';
168
+ await landGenerationAssets(m, ready);
169
+ m.landed.assets = true;
170
+ }
171
+ m.status = 'done';
172
+ m.stage = 'complete';
173
+ return;
174
+ }
175
+ if (isGenerationFailure(record.status)) {
176
+ throw new Error(`Generation failed with status: ${record.status}`);
177
+ }
178
+ m.stage =
179
+ (m.streamUrls?.length ?? 0) > 0
180
+ ? `generating (${record.status}) — stream preview available`
181
+ : `generating (${record.status})`;
182
+ }
183
+ async function advanceSplit(m) {
184
+ const { hashes, assetId, stemsDir } = m.provider;
185
+ if (!hashes || !assetId || !stemsDir)
186
+ throw new Error('Split job manifest is incomplete');
187
+ const asset = getAsset(assetId);
188
+ if (!asset)
189
+ throw new Error(`Split source asset no longer exists: ${assetId}`);
190
+ const jobNames = ['vocals', 'drumsep', 'bass'];
191
+ const pendingStates = [];
192
+ // Poll the pending hashes in parallel — one status round-trip per poll, not three.
193
+ const pending = jobNames.filter((j) => !m.landed[j]);
194
+ const statuses = await Promise.all(pending.map((j) => fetchSeparationStatus(hashes[j])));
195
+ for (let i = 0; i < pending.length; i++) {
196
+ const job = pending[i];
197
+ const files = resolveSeparationStatus(hashes[job], statuses[i]);
198
+ if (files) {
199
+ const rows = await landSplitJob(job, files, asset, stemsDir);
200
+ m.stems.push(...rows.map((r) => ({ stemType: r.stemType, path: r.path })));
201
+ m.landed[job] = true;
202
+ }
203
+ else {
204
+ pendingStates.push(`${job}: ${statuses[i].status}`);
205
+ }
206
+ }
207
+ if (jobNames.every((j) => m.landed[j])) {
208
+ if (!m.landed.ee) {
209
+ const row = await finalizeSplit(asset, stemsDir);
210
+ m.stems.push({ stemType: row.stemType, path: row.path });
211
+ m.landed.ee = true;
212
+ }
213
+ m.status = 'done';
214
+ m.stage = 'complete — 7 stems landed';
215
+ return;
216
+ }
217
+ const landedCount = jobNames.filter((j) => m.landed[j]).length;
218
+ m.stage = `separating (${landedCount}/3 jobs landed; ${pendingStates.join(', ')})`;
219
+ }
220
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ export interface OperationResult {
3
+ text: string;
4
+ data?: unknown;
5
+ }
6
+ export interface Operation<I> {
7
+ id: string;
8
+ description: string;
9
+ input: z.ZodType<I>;
10
+ run: (input: I) => Promise<OperationResult>;
11
+ }
12
+ export declare const ALL_OPERATIONS: ReadonlyArray<Operation<unknown>>;