videobook 0.2.4 → 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 (74) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +6 -2
  3. package/dist/archive-b2.js +1 -1
  4. package/dist/cli/args.js +42 -3
  5. package/dist/cli/config-command.js +17 -1
  6. package/dist/cli/doctor-command.js +15 -0
  7. package/dist/cli/har-command.js +86 -0
  8. package/dist/cli/main.js +13 -1
  9. package/dist/command-reference.js +5 -1
  10. package/dist/content-store.js +2 -1
  11. package/dist/diagnostics.js +1 -1
  12. package/dist/env-secrets.js +1 -0
  13. package/dist/generation-har.js +106 -0
  14. package/dist/har-capture.js +142 -0
  15. package/dist/har-export.js +238 -0
  16. package/dist/har-index.js +98 -0
  17. package/dist/har-retention.js +73 -0
  18. package/dist/har-settings.js +27 -0
  19. package/dist/jobs/_alibaba-upload.js +2 -4
  20. package/dist/jobs/_fal-upload.js +2 -4
  21. package/dist/jobs/_image-edit-adapter.js +1 -1
  22. package/dist/jobs/_kie-upload.js +2 -4
  23. package/dist/jobs/complete-provider-task.js +5 -2
  24. package/dist/jobs/generate-media.js +9 -1
  25. package/dist/jobs/image-subjects.js +1 -1
  26. package/dist/jobs/kie-paygate.js +10 -3
  27. package/dist/jobs/run-notebook.js +4 -1
  28. package/dist/jobs/xai-paygate.js +5 -2
  29. package/dist/media/adapters/kie.js +2 -1
  30. package/dist/media/catalog.js +20 -0
  31. package/dist/media/cost.js +1 -1
  32. package/dist/package-version.js +18 -0
  33. package/dist/posts/dropbox-auth.js +1 -1
  34. package/dist/posts/post-metrics.js +2 -1
  35. package/dist/provider-usage.js +1 -18
  36. package/dist/queue.js +12 -1
  37. package/dist/serve.js +2 -0
  38. package/dist/tools/api-status.js +1 -1
  39. package/dist/tools/generate-media.js +3 -0
  40. package/dist/tools/generation-payload.js +17 -2
  41. package/dist/tools/get-generation-har.js +43 -0
  42. package/dist/tools/index.js +4 -0
  43. package/dist/tools/list-cell-generations.js +19 -0
  44. package/dist/tools/reconcile-project-work.js +18 -12
  45. package/dist/tools/set-har-capture.js +30 -0
  46. package/dist/upload.js +2 -1
  47. package/dist/usage-providers.js +48 -10
  48. package/dist/workspace-action-groups.js +2 -2
  49. package/dist/x/account.js +1 -1
  50. package/dist/x/authenticated-handle.js +1 -1
  51. package/dist/x/oauth-flow.js +1 -1
  52. package/dist/x/post-to-x.js +2 -1
  53. package/dist/x/x-tokens.js +2 -1
  54. package/package.json +15 -14
  55. package/ui/assets/{alive-captions-Bp6dxosy.js → alive-captions-Cj8C1tCN.js} +1 -1
  56. package/ui/assets/{confirm-bodies-Ds1HtaGc.js → confirm-bodies-BxWXwDf0.js} +1 -1
  57. package/ui/assets/{fullscreen-BCeVHjvY.js → fullscreen-DyEncSdm.js} +1 -1
  58. package/ui/assets/{goal-ui-DbAGwgMN.js → goal-ui-BdFeoSWh.js} +2 -2
  59. package/ui/assets/{index-D1d-F1y9.js → index-QZ8lbemN.js} +9 -9
  60. package/ui/assets/inspector-BC6xWkJU.js +1 -0
  61. package/ui/assets/{jobs-ui-C1cbw-xG.js → jobs-ui-BudI4w0r.js} +1 -1
  62. package/ui/assets/{music-video-CWtxPlxq.js → music-video-CT4C55ok.js} +1 -1
  63. package/ui/assets/ui-mq0uIS5P.css +1 -0
  64. package/ui/assets/ui-z5r-p0K9.js +2 -0
  65. package/ui/assets/{vendor-BFupLz37.js → vendor-D8XcMav4.js} +2 -2
  66. package/ui/assets/{vendor-flow-CKiuzaeM.js → vendor-flow-nqTFQmTh.js} +1 -1
  67. package/ui/assets/vendor-mcp-D_C3EZPv.js +60 -0
  68. package/ui/assets/videobook-CsW453sb.js +21 -0
  69. package/ui/index.html +12 -12
  70. package/ui/assets/inspector-BJTC0luH.js +0 -1
  71. package/ui/assets/ui-CXDi5Omq.js +0 -2
  72. package/ui/assets/ui-Oirr9VOf.css +0 -1
  73. package/ui/assets/vendor-mcp-DQEeVZH0.js +0 -67
  74. package/ui/assets/videobook-J-j9bSjM.js +0 -20
@@ -0,0 +1,238 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createWriteStream } from "node:fs";
3
+ import { mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { createGzip } from "node:zlib";
9
+ import { scrubHar } from "@apicity/har";
10
+ import { captureFilePath, readCaptureFile, readHarIndex } from "./har-index.js";
11
+ import { installedPackageVersion } from "./package-version.js";
12
+ const BLOCK = 512;
13
+ const OWN_VERSION = createRequire(import.meta.url)("../package.json").version;
14
+ function isHarArchivePath(path) {
15
+ return /\.(?:tar\.gz|tgz)$/iu.test(path);
16
+ }
17
+ export function selectHarRows(rows, selection) {
18
+ const seen = new Set();
19
+ return rows.filter((row) => {
20
+ if (seen.has(row.file))
21
+ return false;
22
+ seen.add(row.file);
23
+ return (selection.since == null || Date.parse(row.ts) >= selection.since)
24
+ && (!selection.failures || row.outcome !== "ok")
25
+ && (!selection.providers?.length || selection.providers.includes(row.provider));
26
+ });
27
+ }
28
+ function emptyOutcomes() {
29
+ return { ok: 0, provider_error: 0, local_error: 0, network_error: 0 };
30
+ }
31
+ function sha256(bytes) {
32
+ return createHash("sha256").update(bytes).digest("hex");
33
+ }
34
+ function verifiedMeta(row, har) {
35
+ const meta = har.log._apicity;
36
+ return {
37
+ ts: meta?.startedAt ?? row.ts,
38
+ callId: meta?.callId ?? row.callId,
39
+ provider: meta?.provider ?? row.provider,
40
+ endpoint: meta?.endpoint ?? row.endpoint,
41
+ outcome: meta?.outcome ?? row.outcome,
42
+ status: meta?.status ?? row.status ?? null,
43
+ context: meta?.context ?? row.context ?? null,
44
+ };
45
+ }
46
+ function manifestFor(captures, options) {
47
+ const outcomes = emptyOutcomes();
48
+ const providers = new Map();
49
+ const endpoints = new Map();
50
+ for (const { meta } of captures) {
51
+ outcomes[meta.outcome] += 1;
52
+ const provider = providers.get(meta.provider) ?? { provider: meta.provider, calls: 0, outcomes: emptyOutcomes() };
53
+ provider.calls += 1;
54
+ provider.outcomes[meta.outcome] += 1;
55
+ providers.set(meta.provider, provider);
56
+ const key = JSON.stringify([meta.provider, meta.endpoint]);
57
+ const endpoint = endpoints.get(key) ?? { provider: meta.provider, endpoint: meta.endpoint, calls: 0, outcomes: emptyOutcomes() };
58
+ endpoint.calls += 1;
59
+ endpoint.outcomes[meta.outcome] += 1;
60
+ endpoints.set(key, endpoint);
61
+ }
62
+ const times = captures.map(({ meta }) => meta.ts).sort();
63
+ return {
64
+ format: "videobook-har-export",
65
+ version: 1,
66
+ createdAt: (options.now ?? new Date()).toISOString(),
67
+ videobook: OWN_VERSION,
68
+ apicity: {
69
+ har: installedPackageVersion("@apicity/har"),
70
+ captured: [...new Set(captures.map((capture) => capture.apicityVersion))].sort(),
71
+ },
72
+ filters: {
73
+ since: options.since == null ? null : new Date(options.since).toISOString(),
74
+ failures: Boolean(options.failures),
75
+ providers: options.providers?.length ? [...options.providers] : null,
76
+ },
77
+ files: captures.length,
78
+ dateRange: { first: times[0] ?? null, last: times.at(-1) ?? null },
79
+ outcomes,
80
+ providers: [...providers.values()].sort((a, b) => a.provider.localeCompare(b.provider)),
81
+ endpoints: [...endpoints.values()].sort((a, b) => a.provider.localeCompare(b.provider) || a.endpoint.localeCompare(b.endpoint)),
82
+ };
83
+ }
84
+ function indexLines(captures) {
85
+ return captures.map(({ file, meta }) => `${JSON.stringify({
86
+ ts: meta.ts,
87
+ callId: meta.callId,
88
+ provider: meta.provider,
89
+ endpoint: meta.endpoint,
90
+ outcome: meta.outcome,
91
+ ...(meta.status === null ? {} : { status: meta.status }),
92
+ file,
93
+ ...(meta.context ? { context: meta.context } : {}),
94
+ })}\n`).join("");
95
+ }
96
+ async function* exportEntries(captures, manifest) {
97
+ yield { path: "manifest.json", bytes: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`) };
98
+ yield { path: "index.jsonl", bytes: Buffer.from(indexLines(captures)) };
99
+ for (const capture of captures) {
100
+ const bytes = await readFile(capture.path);
101
+ if (sha256(bytes) !== capture.sha256)
102
+ throw new Error(`${capture.path} changed during the export; run it again`);
103
+ yield { path: capture.file, bytes };
104
+ }
105
+ }
106
+ function octal(value, width) {
107
+ return `${value.toString(8).padStart(width - 1, "0")}\0`;
108
+ }
109
+ function tarHeader(name, size, type, mtime) {
110
+ const header = Buffer.alloc(BLOCK);
111
+ header.write(name, 0, 100, "utf8");
112
+ header.write(octal(0o600, 8), 100, "ascii");
113
+ header.write(octal(0, 8), 108, "ascii");
114
+ header.write(octal(0, 8), 116, "ascii");
115
+ header.write(octal(size, 12), 124, "ascii");
116
+ header.write(octal(Math.floor(mtime / 1000), 12), 136, "ascii");
117
+ header.write(" ".repeat(8), 148, "ascii");
118
+ header.write(type, 156, "ascii");
119
+ header.write("ustar\0", 257, "ascii");
120
+ header.write("00", 263, "ascii");
121
+ const checksum = header.reduce((sum, byte) => sum + byte, 0);
122
+ header.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
123
+ return header;
124
+ }
125
+ function paxPath(path) {
126
+ const body = ` path=${path}\n`;
127
+ const bytes = Buffer.byteLength(body);
128
+ let length = bytes + String(bytes).length;
129
+ while (length !== bytes + String(length).length)
130
+ length = bytes + String(length).length;
131
+ return Buffer.from(`${length}${body}`);
132
+ }
133
+ function padding(size) {
134
+ return Buffer.alloc((BLOCK - (size % BLOCK)) % BLOCK);
135
+ }
136
+ async function* tarBlocks(entries, root, mtime) {
137
+ for await (const entry of entries) {
138
+ const name = `${root}/${entry.path}`;
139
+ if (Buffer.byteLength(name) > 100) {
140
+ const pax = paxPath(name);
141
+ yield tarHeader("PaxHeader", pax.length, "x", mtime);
142
+ yield pax;
143
+ yield padding(pax.length);
144
+ }
145
+ yield tarHeader(name, entry.bytes.length, "0", mtime);
146
+ yield entry.bytes;
147
+ yield padding(entry.bytes.length);
148
+ }
149
+ yield Buffer.alloc(BLOCK * 2);
150
+ }
151
+ async function writeArchive(out, entries, mtime) {
152
+ const root = basename(out).replace(/\.(?:tar\.gz|tgz)$/iu, "") || "videobook-har";
153
+ const partial = join(dirname(out), `.${basename(out)}.${randomUUID()}.partial`);
154
+ try {
155
+ await pipeline(Readable.from(tarBlocks(entries, root, mtime)), createGzip(), createWriteStream(partial, { flags: "wx", mode: 0o600 }));
156
+ await rename(partial, out);
157
+ }
158
+ catch (error) {
159
+ await rm(partial, { force: true });
160
+ throw error;
161
+ }
162
+ }
163
+ async function writeDirectory(out, entries) {
164
+ const partial = join(dirname(out), `.${basename(out)}.${randomUUID()}.partial`);
165
+ try {
166
+ await mkdir(partial, { mode: 0o700 });
167
+ for await (const entry of entries) {
168
+ const target = join(partial, ...entry.path.split("/"));
169
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
170
+ await writeFile(target, entry.bytes, { flag: "wx", mode: 0o600 });
171
+ }
172
+ await rmdir(out).catch((error) => {
173
+ if (error.code !== "ENOENT")
174
+ throw error;
175
+ });
176
+ await rename(partial, out);
177
+ }
178
+ catch (error) {
179
+ await rm(partial, { recursive: true, force: true });
180
+ throw error;
181
+ }
182
+ }
183
+ async function outputTaken(out, archive) {
184
+ try {
185
+ const info = await stat(out);
186
+ return archive || !info.isDirectory() || (await readdir(out)).length > 0;
187
+ }
188
+ catch (error) {
189
+ if (error.code === "ENOENT")
190
+ return false;
191
+ throw error;
192
+ }
193
+ }
194
+ function inside(parent, child) {
195
+ const location = relative(resolve(parent), resolve(child));
196
+ return location === "" || (location !== ".." && !location.startsWith(`..${sep}`) && !isAbsolute(location));
197
+ }
198
+ export async function exportHarCaptures(options) {
199
+ const archive = isHarArchivePath(options.out);
200
+ if (inside(options.dir, options.out))
201
+ return { status: "out-inside-captures", out: options.out };
202
+ if (await outputTaken(options.out, archive))
203
+ return { status: "out-exists", out: options.out };
204
+ const verified = [];
205
+ const skipped = [];
206
+ const needsScrub = [];
207
+ for (const row of selectHarRows(await readHarIndex(options.dir), options)) {
208
+ const capture = await readCaptureFile(options.dir, row.file);
209
+ if (!capture) {
210
+ skipped.push(captureFilePath(options.dir, row.file) ?? row.file);
211
+ continue;
212
+ }
213
+ const before = JSON.stringify(capture.har);
214
+ if (JSON.stringify(scrubHar(structuredClone(capture.har))) !== before) {
215
+ needsScrub.push(capture.path);
216
+ continue;
217
+ }
218
+ verified.push({
219
+ path: capture.path,
220
+ file: row.file.split(/[\\/]/u).join("/"),
221
+ sha256: sha256(capture.bytes),
222
+ meta: verifiedMeta(row, capture.har),
223
+ apicityVersion: capture.har.log.creator?.version ?? "unknown",
224
+ });
225
+ }
226
+ if (needsScrub.length > 0)
227
+ return { status: "needs-scrub", files: needsScrub };
228
+ if (verified.length === 0)
229
+ return { status: "empty", skipped };
230
+ const manifest = manifestFor(verified, options);
231
+ const entries = exportEntries(verified, manifest);
232
+ await mkdir(dirname(options.out), { recursive: true });
233
+ if (archive)
234
+ await writeArchive(options.out, entries, (options.now ?? new Date()).getTime());
235
+ else
236
+ await writeDirectory(options.out, entries);
237
+ return { status: "exported", out: options.out, archive, manifest, skipped };
238
+ }
@@ -0,0 +1,98 @@
1
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ import { harDirectory } from "./har-settings.js";
4
+ const OUTCOMES = new Set(["ok", "provider_error", "local_error", "network_error"]);
5
+ let indexCache = null;
6
+ function record(value) {
7
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
8
+ }
9
+ function indexRow(line) {
10
+ let value;
11
+ try {
12
+ value = JSON.parse(line);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ const row = record(value);
18
+ if (!row || typeof row.ts !== "string" || typeof row.callId !== "string" || typeof row.provider !== "string"
19
+ || typeof row.endpoint !== "string" || typeof row.outcome !== "string" || !OUTCOMES.has(row.outcome)
20
+ || typeof row.file !== "string")
21
+ return null;
22
+ const context = record(row.context);
23
+ return {
24
+ ts: row.ts,
25
+ callId: row.callId,
26
+ provider: row.provider,
27
+ endpoint: row.endpoint,
28
+ outcome: row.outcome,
29
+ ...(typeof row.status === "number" ? { status: row.status } : {}),
30
+ file: row.file,
31
+ ...(context ? { context } : {}),
32
+ };
33
+ }
34
+ export async function readHarIndex(dir = harDirectory()) {
35
+ const path = join(dir, "index.jsonl");
36
+ let key;
37
+ try {
38
+ const info = await stat(path);
39
+ key = `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
40
+ }
41
+ catch (error) {
42
+ if (error.code === "ENOENT")
43
+ return [];
44
+ throw error;
45
+ }
46
+ if (indexCache?.path === path && indexCache.key === key)
47
+ return indexCache.rows;
48
+ const text = await readFile(path, "utf8");
49
+ const rows = text.split("\n").flatMap((line) => {
50
+ const row = line.trim() ? indexRow(line) : null;
51
+ return row ? [row] : [];
52
+ });
53
+ indexCache = { path, key, rows };
54
+ return rows;
55
+ }
56
+ export function captureFilePath(dir, file) {
57
+ if (!file.endsWith(".har") || isAbsolute(file) || file.includes("\0"))
58
+ return null;
59
+ const parts = file.split(/[\\/]/u);
60
+ if (parts.some((part) => part === "" || part === "." || part === ".."))
61
+ return null;
62
+ return join(dir, ...parts);
63
+ }
64
+ function isHar(value) {
65
+ const log = record(record(value)?.log);
66
+ return Boolean(log && Array.isArray(log.entries));
67
+ }
68
+ export async function readCaptureFile(dir, file) {
69
+ const path = captureFilePath(dir, file);
70
+ if (!path)
71
+ return null;
72
+ try {
73
+ if (!(await lstat(path)).isFile())
74
+ return null;
75
+ const location = relative(await realpath(dir), await realpath(path));
76
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location))
77
+ return null;
78
+ const bytes = await readFile(path);
79
+ const har = JSON.parse(bytes.toString("utf8"));
80
+ return isHar(har) ? { path, bytes, har } : null;
81
+ }
82
+ catch (error) {
83
+ if (error instanceof SyntaxError || error.code === "ENOENT")
84
+ return null;
85
+ throw error;
86
+ }
87
+ }
88
+ export async function captureFileExists(dir, file) {
89
+ const path = captureFilePath(dir, file);
90
+ if (!path)
91
+ return false;
92
+ try {
93
+ return (await lstat(path)).isFile();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
@@ -0,0 +1,73 @@
1
+ import { readFile, readdir, stat, unlink, writeFile, rename } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { harDirectory, readHarSettings } from "./har-settings.js";
5
+ import { maintainHarCaptures } from "./har-capture.js";
6
+ import { logger } from "./logger.js";
7
+ let nextSweep = 0;
8
+ async function captureFiles(dir, root = dir) {
9
+ const result = [];
10
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
11
+ const path = join(dir, entry.name);
12
+ if (entry.isDirectory())
13
+ result.push(...await captureFiles(path, root));
14
+ else if (entry.isFile() && entry.name.endsWith(".har")) {
15
+ const info = await stat(path);
16
+ result.push({ file: relative(root, path), bytes: info.size, time: info.mtimeMs });
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ export async function pruneHarCaptures(dir, settings, now = Date.now()) {
22
+ if (settings.mode === "off")
23
+ return;
24
+ let files;
25
+ try {
26
+ files = await captureFiles(dir);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return;
31
+ throw error;
32
+ }
33
+ files.sort((a, b) => a.time - b.time || a.file.localeCompare(b.file));
34
+ let index = "";
35
+ try {
36
+ index = await readFile(join(dir, "index.jsonl"), "utf8");
37
+ }
38
+ catch (error) {
39
+ if (error.code !== "ENOENT")
40
+ throw error;
41
+ }
42
+ const rows = index.split("\n").filter(Boolean).map((line) => ({ line, value: JSON.parse(line) }));
43
+ let bytes = files.reduce((sum, file) => sum + file.bytes, 0) + Buffer.byteLength(index);
44
+ const keep = new Set(files.map((file) => file.file));
45
+ for (const file of files) {
46
+ if (file.time >= now - settings.maxAgeDays * 86_400_000 && bytes <= settings.maxTotalMB * 1024 * 1024)
47
+ continue;
48
+ await unlink(join(dir, file.file));
49
+ keep.delete(file.file);
50
+ bytes -= file.bytes + rows.filter((row) => row.value.file === file.file).reduce((sum, row) => sum + Buffer.byteLength(row.line) + 1, 0);
51
+ }
52
+ const retained = rows.filter((row) => keep.has(row.value.file)).map((row) => row.line + "\n").join("");
53
+ const temporary = join(dir, `.index-${randomUUID()}.tmp`);
54
+ await writeFile(temporary, retained, { mode: 0o600 });
55
+ await rename(temporary, join(dir, "index.jsonl"));
56
+ }
57
+ export async function sweepHarCaptures(force = false) {
58
+ if (!force && Date.now() < nextSweep)
59
+ return;
60
+ nextSweep = Date.now() + 86_400_000;
61
+ try {
62
+ const settings = await readHarSettings();
63
+ if (settings.mode === "off") {
64
+ nextSweep = 0;
65
+ return;
66
+ }
67
+ if (!await maintainHarCaptures(() => pruneHarCaptures(harDirectory(), settings)))
68
+ nextSweep = Date.now() + 60_000;
69
+ }
70
+ catch (error) {
71
+ logger.warn("har-capture-failed", { error });
72
+ }
73
+ }
@@ -0,0 +1,27 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { appState } from "./app-state.js";
4
+ import { dataLayout } from "./data-home.js";
5
+ export const harSettingsSchema = z.object({
6
+ mode: z.enum(["off", "failures", "all"]).default("off"),
7
+ maxAgeDays: z.number().positive().finite().default(30),
8
+ maxTotalMB: z.number().positive().finite().default(512),
9
+ });
10
+ export function harDirectory() {
11
+ const layout = dataLayout();
12
+ return join(layout.root, ...(layout.kind === "checkout" ? ["runtime"] : []), "har");
13
+ }
14
+ export async function readHarSettings(store = appState, env = process.env) {
15
+ const stored = harSettingsSchema.safeParse(await store.get("harCapture") ?? {});
16
+ const settings = stored.success ? stored.data : harSettingsSchema.parse({});
17
+ const override = env.VIDEOBOOK_HAR_CAPTURE?.trim();
18
+ if (!override)
19
+ return settings;
20
+ const mode = harSettingsSchema.shape.mode.safeParse(override);
21
+ return { ...settings, mode: mode.success ? mode.data : "off" };
22
+ }
23
+ export async function writeHarSettings(value, store = appState) {
24
+ const settings = harSettingsSchema.parse(value);
25
+ await store.set("harCapture", settings);
26
+ return readHarSettings(store);
27
+ }
@@ -1,5 +1,4 @@
1
- import { createAlibaba } from "../usage-providers.js";
2
- import { uploadFile } from "@apicity/alibaba";
1
+ import { uploadAlibabaFile } from "../usage-providers.js";
3
2
  import { prepareAssetForUpload, uploadBytesToFreeHost } from "../upload.js";
4
3
  import { getApiKey } from "../api-key.js";
5
4
  import { logger } from "../logger.js";
@@ -11,8 +10,7 @@ export async function uploadAssetToAlibaba(engine, assetId, filename, model) {
11
10
  const apiKey = getApiKey("DASHSCOPE_API_KEY");
12
11
  if (apiKey) {
13
12
  try {
14
- const provider = createAlibaba({ apiKey, timeout: 60_000 });
15
- return await uploadFile(provider, {
13
+ return await uploadAlibabaFile({ apiKey, timeout: 60_000 }, {
16
14
  model,
17
15
  data,
18
16
  filename: hashedFilename,
@@ -1,5 +1,4 @@
1
- import { createFal } from "../usage-providers.js";
2
- import { uploadFile } from "@apicity/fal";
1
+ import { uploadFalFile } from "../usage-providers.js";
3
2
  import { prepareAssetForUpload, uploadBytesToFreeHost, } from "../upload.js";
4
3
  import { getApiKey } from "../api-key.js";
5
4
  import { logger } from "../logger.js";
@@ -10,8 +9,7 @@ async function uploadPreparedToFal(data, hashedFilename, contentType) {
10
9
  const apiKey = getApiKey("FAL_API_KEY");
11
10
  if (apiKey) {
12
11
  try {
13
- const provider = createFal({ apiKey, timeout: 60_000 });
14
- return await uploadFile(provider, {
12
+ return await uploadFalFile({ apiKey, timeout: 60_000 }, {
15
13
  data,
16
14
  filename: hashedFilename,
17
15
  contentType,
@@ -1,5 +1,5 @@
1
1
  import { createFal } from "../usage-providers.js";
2
- import { submitMediaJob } from "@apicity/kie";
2
+ import { submitMediaJob } from "../usage-providers.js";
3
3
  import { FalError } from "@apicity/fal";
4
4
  import { SeedreamProImageToImageRequestSchema } from "@apicity/kie/zod";
5
5
  import { setTimeout as delay } from "node:timers/promises";
@@ -1,5 +1,4 @@
1
- import { createKie } from "../usage-providers.js";
2
- import { uploadFile } from "@apicity/kie";
1
+ import { uploadKieFile } from "../usage-providers.js";
3
2
  import { prepareAssetForUpload, uploadBytesToFreeHost, } from "../upload.js";
4
3
  import { getApiKey } from "../api-key.js";
5
4
  import { logger } from "../logger.js";
@@ -12,9 +11,8 @@ async function uploadPreparedToKie(data, hashedFilename, contentType) {
12
11
  const apiKey = getApiKey("KIE_API_KEY");
13
12
  if (apiKey) {
14
13
  try {
15
- const provider = createKie({ apiKey, timeout: 120_000 });
16
14
  const blob = new Blob([new Uint8Array(data)], { type: contentType });
17
- return await uploadFile(provider, blob, hashedFilename);
15
+ return await uploadKieFile({ apiKey, timeout: 120_000 }, blob, hashedFilename);
18
16
  }
19
17
  catch (err) {
20
18
  logger.warn("kie-upload-fallback", { error: err });
@@ -1,3 +1,4 @@
1
+ import { withHarCapture } from "../har-capture.js";
1
2
  import { join } from "node:path";
2
3
  import { tmpdir } from "node:os";
3
4
  import { writeFile, unlink, readFile as readFsFile } from "node:fs/promises";
@@ -47,7 +48,9 @@ async function reclaimFailedCompletion(engine, task, ctx) {
47
48
  return lease.ownerId;
48
49
  }
49
50
  export function registerCompleteProviderTaskJob(engine) {
50
- registerJobHandler("complete_kie_task", async (payload, ctx) => {
51
+ registerJobHandler("complete_kie_task", async (payload, ctx) => withHarCapture({
52
+ provider: String(payload.taskType).split("_")[0] ?? "unknown", endpoint: "generation.complete", input: payload,
53
+ }, async () => {
51
54
  const { taskId, taskType, assetId, bookId, meta, resultUrls, resultObject, } = payload;
52
55
  // Pattern C: inherit the provider lease via getOwner, abort if the row
53
56
  // already terminated, and register the lease so the wrapper's catch path
@@ -125,7 +128,7 @@ export function registerCompleteProviderTaskJob(engine) {
125
128
  clearInterval(heartbeat);
126
129
  await engine.jobs.locks.release(assetDir);
127
130
  }
128
- });
131
+ }, globalThis.fetch, { providerTaskId: payload.taskId, model: payload.meta.model, runId: payload.meta.runId, attemptId: payload.meta.attemptId }));
129
132
  }
130
133
  async function mediaOutputs(taskType, taskId, urls) {
131
134
  if (taskType === MEDIA_TASK_TYPES.machgen)
@@ -1,3 +1,5 @@
1
+ import { withHarCapture } from "../har-capture.js";
2
+ import { withBookContext } from "../book-context.js";
1
3
  import { isSeedanceEdit } from "../media/seedance-edit.js";
2
4
  import { registerJobHandler } from "../queue.js";
3
5
  import { assertProviderPayload } from "../provider-prompt-transform.js";
@@ -41,6 +43,8 @@ export async function generationSourceCostHints(engine, refs, job) {
41
43
  function generationMeta(job, model, refs) {
42
44
  return {
43
45
  model: model.id,
46
+ runId: job.runId,
47
+ attemptId: job.generationId ?? job.assetId,
44
48
  media: model.media,
45
49
  prompt: job.prompt ?? "",
46
50
  sourceImageIds: refs.sourceImageIds,
@@ -48,7 +52,7 @@ function generationMeta(job, model, refs) {
48
52
  sourceAudioIds: refs.sourceAudioIds,
49
53
  };
50
54
  }
51
- export async function runGenerateMedia(engine, job, ctx) {
55
+ async function generateMedia(engine, job, ctx) {
52
56
  const model = resolveModel(job.model);
53
57
  const adapter = adapterFor(model);
54
58
  requireApiKey(adapter.envKey);
@@ -81,6 +85,10 @@ export async function runGenerateMedia(engine, job, ctx) {
81
85
  }
82
86
  return { success: true, model: model.id, taskId: result.taskId, status: result.kind };
83
87
  }
88
+ export async function runGenerateMedia(engine, job, ctx) {
89
+ return withBookContext({ bookId: job.bookId, engine, operation: "generate_media", artifactId: job.assetId,
90
+ attemptId: job.generationId ?? job.assetId, runId: job.runId, model: job.model }, () => withHarCapture({ provider: resolveModel(job.model).provider, endpoint: "generation.submit", input: job }, () => generateMedia(engine, job, ctx)));
91
+ }
84
92
  export function registerGenerateMediaJob(engine) {
85
93
  registerJobHandler("generate_media", (payload, ctx) => runGenerateMedia(engine, payload, ctx));
86
94
  }
@@ -1,4 +1,4 @@
1
- import { submitMediaJob, } from "@apicity/kie";
1
+ import { submitMediaJob } from "../usage-providers.js";
2
2
  import { GrokImagineImage2ImageEditRequestSchema, GrokImagineImage2SegmentMapRequestSchema, SeedreamProLayerDecompositionRequestSchema, } from "@apicity/kie/zod";
3
3
  import sharp from "sharp";
4
4
  import { KIE_RESULT_TIMEOUT_MS, kieProvider, parseResultUrls, waitForKieResult, } from "./_kie-poll.js";
@@ -1,5 +1,6 @@
1
+ import { withHarCapture } from "../har-capture.js";
1
2
  import { createKie } from "../usage-providers.js";
2
- import { submitMediaJob } from "@apicity/kie";
3
+ import { submitMediaJob } from "../usage-providers.js";
3
4
  import { mintOtp } from "@apicity/cost";
4
5
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { randomBytes } from "node:crypto";
@@ -81,7 +82,7 @@ function createKieProvider(apiKey) {
81
82
  paygate: { secret: ensurePaygateSecret() },
82
83
  });
83
84
  }
84
- export async function submitKieTask(engine, ctx, args) {
85
+ async function submitKieTaskInner(engine, ctx, args) {
85
86
  const request = args.request;
86
87
  assertProviderPayload(request, {
87
88
  toolName: args.taskType,
@@ -129,7 +130,7 @@ export async function submitKieTask(engine, ctx, args) {
129
130
  ctx.setLease({ kind: "task", taskId });
130
131
  return taskId;
131
132
  }
132
- export async function submitQueuedKieTask(engine, task) {
133
+ async function submitQueuedKieTaskInner(engine, task) {
133
134
  if (task.taskId !== QUEUED_TASK_ID)
134
135
  throw new Error(`KIE task ${task.artifactId} is not locally queued`);
135
136
  const ownerId = await engine.jobs.pending.getOwner(task.artifactId, QUEUED_TASK_ID);
@@ -175,3 +176,9 @@ export async function submitQueuedKieTask(engine, task) {
175
176
  await engine.metadata.artifacts.write(task.artifactId, "kie-task-id", { taskId });
176
177
  return writeResult.value.task;
177
178
  }
179
+ export async function submitKieTask(engine, ctx, args) {
180
+ return withHarCapture({ provider: "kie", endpoint: "generation.paygate", input: args.request }, () => submitKieTaskInner(engine, ctx, args));
181
+ }
182
+ export async function submitQueuedKieTask(engine, task) {
183
+ return withHarCapture({ provider: "kie", endpoint: "generation.queued", input: task }, () => submitQueuedKieTaskInner(engine, task));
184
+ }
@@ -1,3 +1,4 @@
1
+ import { withBookContext } from "../book-context.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { setTimeout as delay } from "node:timers/promises";
3
4
  import { LIGHTNING_VIDEO_MODEL, payloadPrompt, submittedGenerationPrompt, } from "../lightning-defaults.js";
@@ -1006,6 +1007,7 @@ async function generationTool(engine, document, cell, bookId, options = {}) {
1006
1007
  await queue.enqueue(jobId, {
1007
1008
  ...prepared.imageEdit.jobInput,
1008
1009
  type: "generate_image_subject_edit", imageAssetId, bookId,
1010
+ runId: options.runId, model: prepared.model,
1009
1011
  notebookId: document.id, producerCellId: cell.id, outputCellId, jobId,
1010
1012
  managedByNotebook: true,
1011
1013
  ...(generationId ? { generationId } : {}),
@@ -1013,7 +1015,8 @@ async function generationTool(engine, document, cell, bookId, options = {}) {
1013
1015
  response = { content: [{ type: "text", text: JSON.stringify({ image_asset_id: imageAssetId }) }] };
1014
1016
  }
1015
1017
  else {
1016
- response = await definition.handler(prepared.input);
1018
+ response = await withBookContext({ bookId, engine, operation: prepared.tool, runId: options.runId,
1019
+ attemptId: generationId ?? undefined, model: prepared.model }, () => definition.handler(prepared.input));
1017
1020
  }
1018
1021
  }
1019
1022
  catch (cause) {
@@ -1,3 +1,4 @@
1
+ import { withHarCapture } from "../har-capture.js";
1
2
  import { createXai } from "../usage-providers.js";
2
3
  import { mintOtp } from "@apicity/cost";
3
4
  import { ensurePaygateSecret } from "./kie-paygate.js";
@@ -21,6 +22,8 @@ export function mintXaiOtp(dotPath, request, ttlSeconds = 10 * 60) {
21
22
  });
22
23
  }
23
24
  export async function callXaiPaid(method, dotPath, request) {
24
- assertProviderPayload(request, { toolName: dotPath, wire: true });
25
- return method(request, { otp: mintXaiOtp(dotPath, request) });
25
+ return withHarCapture({ provider: "xai", endpoint: dotPath, input: request }, () => {
26
+ assertProviderPayload(request, { toolName: dotPath, wire: true });
27
+ return method(request, { otp: mintXaiOtp(dotPath, request) });
28
+ });
26
29
  }
@@ -1,5 +1,6 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
- import { KieError, submitMediaJob } from "@apicity/kie";
2
+ import { submitMediaJob } from "../../usage-providers.js";
3
+ import { KieError } from "@apicity/kie";
3
4
  import { createKie } from "../../usage-providers.js";
4
5
  import { requireApiKey } from "../../api-key.js";
5
6
  import { authorizeKiePaygate, ensurePaygateSecret, mintKieOtp } from "../../jobs/kie-paygate.js";