@oxygen-agent/cli 1.922.14 → 1.936.1

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +18 -0
  3. package/dist/admin-primary-providers-render.js +371 -0
  4. package/dist/command-manifest.js +6 -0
  5. package/dist/functions-commands.d.ts +6 -0
  6. package/dist/functions-commands.js +56 -0
  7. package/dist/http-client.d.ts +4 -0
  8. package/dist/http-client.js +49 -2
  9. package/dist/index.js +175 -40
  10. package/dist/ugc-commands.d.ts +6 -0
  11. package/dist/ugc-commands.js +748 -0
  12. package/dist/visual-commands.d.ts +6 -0
  13. package/dist/visual-commands.js +57 -0
  14. package/dist/visual-render-wait.d.ts +3 -0
  15. package/dist/visual-render-wait.js +56 -0
  16. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +43 -0
  17. package/node_modules/@oxygen/shared/dist/byok-connect.js +84 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +26 -10
  19. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
  20. package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
  21. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
  22. package/node_modules/@oxygen/shared/dist/index.js +4 -0
  23. package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
  24. package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
  25. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
  26. package/node_modules/@oxygen/shared/dist/langfuse.js +17 -0
  27. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
  28. package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
  30. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
  31. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
  32. package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
  33. package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
  34. package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
  35. package/node_modules/@oxygen/shared/dist/ugc.d.ts +113 -0
  36. package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
  37. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
  38. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -0
  39. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +30 -0
  42. package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
  44. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
  45. package/node_modules/@oxygen/shared/package.json +10 -0
  46. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ type JsonOptions = {
3
+ json?: boolean;
4
+ };
5
+ export declare function registerVisualCommands(program: Command, handle: (command: string, options: JsonOptions, action: () => Promise<unknown>) => Promise<void>): void;
6
+ export {};
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { OxygenError } from "@oxygen/shared";
5
+ import { requestOxygen } from "./http-client.js";
6
+ import { waitForVisualRender } from "./visual-render-wait.js";
7
+ export function registerVisualCommands(program, handle) {
8
+ const visuals = program.command("visuals").description("Author, render, inspect and download GTM infographics using tenant workspace files and governed render jobs.");
9
+ visuals.command("source-create").description("Save an immutable HTML/CSS/SVG source revision; no code executes.")
10
+ .requiredOption("--name <name>", "Workspace filename.")
11
+ .requiredOption("--file <path>", "Local HTML source file (max 1.5 MB).")
12
+ .option("--parent-file-id <id>", "Previous source revision UUID.").option("--json", "Print a JSON envelope.")
13
+ .action((options) => handle("visuals source create", options, async () => {
14
+ if (statSync(options.file).size > 1_500_000)
15
+ throw new OxygenError("visual_source_limit", "HTML source must fit within 1.5 MB.");
16
+ const bytes = readFileSync(options.file);
17
+ if (bytes.length > 1_500_000)
18
+ throw new OxygenError("visual_source_limit", "HTML source must fit within 1.5 MB.");
19
+ return requestOxygen("/api/cli/visuals/sources", { method: "POST", body: { name: options.name, html: bytes.toString("utf8"), ...(options.parentFileId ? { parent_file_id: options.parentFileId } : {}) } });
20
+ }));
21
+ visuals.command("file-get <file-id>").description("Read source, file metadata, provenance and web/download links.").option("--json", "Print a JSON envelope.")
22
+ .action((id, options) => handle("visuals file get", options, () => requestOxygen(`/api/cli/visuals/files/${encodeURIComponent(id)}`)));
23
+ visuals.command("download <file-id>").description("Download a file with SHA-256 verification; refuses to overwrite existing local files.")
24
+ .requiredOption("--out <path>", "New local destination filename.").option("--json", "Print a JSON envelope.")
25
+ .action((id, options) => handle("visuals download", options, async () => {
26
+ const path = `/api/cli/visuals/files/${encodeURIComponent(id)}`;
27
+ const data = await requestOxygen(path);
28
+ const bytes = await requestOxygen(`${path}/download`, { binaryMaxBytes: Math.max(1, data.file.size_bytes) });
29
+ if (bytes.length !== data.file.size_bytes || createHash("sha256").update(bytes).digest("hex") !== data.file.sha256)
30
+ throw new OxygenError("file_integrity", "Downloaded bytes do not match the workspace file hash.");
31
+ writeFileSync(options.out, bytes, { flag: "wx", mode: 0o600 });
32
+ return { path: resolve(options.out), ...data.file };
33
+ }));
34
+ visuals.command("render-preview").description("Preview rendering without starting paid compute.")
35
+ .requiredOption("--source-file-ids <ids>", "Comma-separated HTML file UUIDs in page order.")
36
+ .option("--width <pixels>", "CSS pixel width.", "1080").option("--height <pixels>", "CSS pixel height.", "1350")
37
+ .option("--scale <1|2>", "PNG scale.", "1").option("--formats <png,pdf>", "Output formats.", "png")
38
+ .option("--json", "Print a JSON envelope.")
39
+ .action((options) => handle("visuals render preview", options, () => requestOxygen("/api/cli/visuals/renders", { method: "POST", body: { source_file_ids: options.sourceFileIds.split(",").map((s) => s.trim()), width: Number(options.width), height: Number(options.height), scale: Number(options.scale), formats: options.formats.split(",") } })));
40
+ visuals.command("render-start <render-id>").description("Approve an exact preview and enqueue one idempotent render job.")
41
+ .requiredOption("--scope-hash <hash>", "Exact preview scope hash.").requiredOption("--max-credits <credits>", "Approved ceiling from preview.")
42
+ .requiredOption("--approved", "Authorize this exact render.").option("--json", "Print a JSON envelope.")
43
+ .action((id, options) => handle("visuals render start", options, () => requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, { method: "POST", body: { scope_hash: options.scopeHash, max_credits: Number(options.maxCredits), approved: options.approved } })));
44
+ visuals.command("render-get <render-id>").description("Read render state and output downloads; optionally wait for a terminal state.")
45
+ .option("--wait", "Poll every 2 seconds until completed, failed, cancelled or effect_unknown; starts no render.")
46
+ .option("--timeout-seconds <n>", "Maximum wait with --wait, from 1 to 600 seconds. Defaults to 180.")
47
+ .option("--json", "Print a JSON envelope.")
48
+ .action((id, options) => handle("visuals render get", options, async () => {
49
+ if (options.wait)
50
+ return waitForVisualRender(id, options.timeoutSeconds);
51
+ if (options.timeoutSeconds !== undefined)
52
+ throw new OxygenError("invalid_request", "Pass --wait with --timeout-seconds.");
53
+ return requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`);
54
+ }));
55
+ visuals.command("render-cancel <render-id>").description("Request cancellation; read state to verify it stopped.")
56
+ .option("--json", "Print a JSON envelope.").action((id, options) => handle("visuals render cancel", options, () => requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, { method: "DELETE" })));
57
+ }
@@ -0,0 +1,3 @@
1
+ /** Poll only the existing job. Neither terminal failure nor timeout authorizes
2
+ * a start, retry, cancellation, or a new credit reservation. */
3
+ export declare function waitForVisualRender(id: string, requestedTimeoutSeconds?: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,56 @@
1
+ import { OxygenError } from "@oxygen/shared";
2
+ import { readPositiveInt, readRecordString } from "./cli-values.js";
3
+ import { requestOxygen } from "./http-client.js";
4
+ import { waitForCliRun } from "./run-wait.js";
5
+ const TERMINAL = new Set(["completed", "failed", "cancelled", "effect_unknown"]);
6
+ const DEFAULT_TIMEOUT_SECONDS = 180;
7
+ const MAX_TIMEOUT_SECONDS = 600;
8
+ /** Poll only the existing job. Neither terminal failure nor timeout authorizes
9
+ * a start, retry, cancellation, or a new credit reservation. */
10
+ export async function waitForVisualRender(id, requestedTimeoutSeconds) {
11
+ const timeoutSeconds = readPositiveInt(requestedTimeoutSeconds) ?? DEFAULT_TIMEOUT_SECONDS;
12
+ if (timeoutSeconds > MAX_TIMEOUT_SECONDS)
13
+ throw new OxygenError("invalid_number", "Visual render wait timeout must be between 1 and 600 seconds.");
14
+ const deadline = Date.now() + timeoutSeconds * 1000;
15
+ const controller = new AbortController();
16
+ let polls = 0;
17
+ let latestStatus = null;
18
+ const timedOut = () => new OxygenError("visual_render_wait_timeout", "Timed out waiting for the visual render. Read the same render ID again to continue checking.", {
19
+ details: { render_id: id, status: latestStatus, timeout_seconds: timeoutSeconds, polls }, exitCode: 1,
20
+ });
21
+ let timer;
22
+ const timeout = new Promise((_, reject) => {
23
+ timer = setTimeout(() => { controller.abort(); reject(timedOut()); }, timeoutSeconds * 1000);
24
+ });
25
+ try {
26
+ return await Promise.race([waitForCliRun({
27
+ runId: id, requestedTimeoutSeconds: String(timeoutSeconds),
28
+ defaultTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS, defaultIntervalSeconds: 2,
29
+ fetchRun: async () => {
30
+ const remainingMs = deadline - Date.now();
31
+ if (controller.signal.aborted || remainingMs <= 0)
32
+ throw timedOut();
33
+ polls += 1;
34
+ const data = await requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, {
35
+ method: "GET", timeoutMs: remainingMs, signal: controller.signal,
36
+ });
37
+ latestStatus = readRecordString(data.render, "status");
38
+ if (latestStatus === "preview")
39
+ throw new OxygenError("visual_render_not_started", "This render is still a preview. Start the approved render before waiting.", {
40
+ details: { render_id: id, status: latestStatus },
41
+ });
42
+ if (!latestStatus || !(TERMINAL.has(latestStatus) || latestStatus === "queued" || latestStatus === "running")) {
43
+ throw new OxygenError("invalid_response", "The render returned an unrecognized status.", { details: { render_id: id, status: latestStatus } });
44
+ }
45
+ return { ...data, status: latestStatus };
46
+ },
47
+ isTerminal: (status) => status !== null && TERMINAL.has(status),
48
+ shapeTerminal: (data, status, count, elapsedMs) => ({ ...data, status, terminal: true, polls: count, elapsedMs }),
49
+ timeoutCode: "visual_render_wait_timeout", timeoutMessage: "Timed out waiting for the visual render.", timeoutDetailIdKey: "render_id",
50
+ }), timeout]);
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ controller.abort();
55
+ }
56
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Which providers `oxygen integrations connect <id> --api-key <key>` ACTUALLY
3
+ * connects for native execution.
4
+ *
5
+ * WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
6
+ * a customer's own key. It does not say the customer has any way to give us one.
7
+ * Those are two different facts, and 10 of the 42 byok-capable native providers
8
+ * have the first without the second:
9
+ *
10
+ * apify builtwith dropleads lusha openwebninja parallel peopledatalabs
11
+ * rocketreach theirstack wiza
12
+ *
13
+ * For those, `oxygen integrations connect <id> --api-key` does not reach
14
+ * `connectViaApiKey` at all. With no native catalog definition,
15
+ * `shouldConnectIntegrationViaComposio` is true, so the key is stored WITH COMPOSIO
16
+ * and no `integration_secrets` row of kind `api_key` is written. On the run path the
17
+ * tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
18
+ * `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
19
+ * MANAGED key — the very key that was out of credits. The command appears to work,
20
+ * changes nothing, and the customer is back where they started with no error.
21
+ *
22
+ * That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
23
+ * of the refused tools, and a refusal message that told Mentcape to connect their own
24
+ * Parallel key would have sent them down a path that silently does nothing.
25
+ *
26
+ * So a message may only offer BYOK when BOTH halves hold, and this list is the second
27
+ * half. It lives in @oxygen/shared because the two sides that need it cannot see each
28
+ * other: `packages/integrations` writes the message, the catalog it would have to
29
+ * consult lives in `apps/web/src/lib/integrations/catalog.ts`, and packages never
30
+ * import from apps. `catalog.byok-connect.test.ts` in apps/web binds the two together
31
+ * so this list cannot drift from the connect forms it claims exist.
32
+ *
33
+ * ALLOWLIST, NOT DENYLIST, on purpose. A new provider that arrives without a connect
34
+ * form gets the alternatives-only message until someone adds it here — quieter than
35
+ * correct. A denylist would fail the other way: silently promising a connect form
36
+ * nobody built, which is the bug this fixes.
37
+ */
38
+ export declare const NATIVE_API_KEY_CONNECT_INTEGRATION_IDS: ReadonlySet<string>;
39
+ /**
40
+ * True when a customer can hand Oxygen their own key for this provider AND the
41
+ * native runner will use it. Never infer this from `supports_byok` alone.
42
+ */
43
+ export declare function hasNativeApiKeyConnect(integrationId: string | null | undefined): boolean;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Which providers `oxygen integrations connect <id> --api-key <key>` ACTUALLY
3
+ * connects for native execution.
4
+ *
5
+ * WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
6
+ * a customer's own key. It does not say the customer has any way to give us one.
7
+ * Those are two different facts, and 10 of the 42 byok-capable native providers
8
+ * have the first without the second:
9
+ *
10
+ * apify builtwith dropleads lusha openwebninja parallel peopledatalabs
11
+ * rocketreach theirstack wiza
12
+ *
13
+ * For those, `oxygen integrations connect <id> --api-key` does not reach
14
+ * `connectViaApiKey` at all. With no native catalog definition,
15
+ * `shouldConnectIntegrationViaComposio` is true, so the key is stored WITH COMPOSIO
16
+ * and no `integration_secrets` row of kind `api_key` is written. On the run path the
17
+ * tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
18
+ * `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
19
+ * MANAGED key — the very key that was out of credits. The command appears to work,
20
+ * changes nothing, and the customer is back where they started with no error.
21
+ *
22
+ * That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
23
+ * of the refused tools, and a refusal message that told Mentcape to connect their own
24
+ * Parallel key would have sent them down a path that silently does nothing.
25
+ *
26
+ * So a message may only offer BYOK when BOTH halves hold, and this list is the second
27
+ * half. It lives in @oxygen/shared because the two sides that need it cannot see each
28
+ * other: `packages/integrations` writes the message, the catalog it would have to
29
+ * consult lives in `apps/web/src/lib/integrations/catalog.ts`, and packages never
30
+ * import from apps. `catalog.byok-connect.test.ts` in apps/web binds the two together
31
+ * so this list cannot drift from the connect forms it claims exist.
32
+ *
33
+ * ALLOWLIST, NOT DENYLIST, on purpose. A new provider that arrives without a connect
34
+ * form gets the alternatives-only message until someone adds it here — quieter than
35
+ * correct. A denylist would fail the other way: silently promising a connect form
36
+ * nobody built, which is the bug this fixes.
37
+ */
38
+ export const NATIVE_API_KEY_CONNECT_INTEGRATION_IDS = new Set([
39
+ "adyntel",
40
+ "ai_ark",
41
+ "apollo",
42
+ "bettercontact",
43
+ "blitzapi",
44
+ "bounceban",
45
+ "breakcold",
46
+ "brightdata",
47
+ "clearoutphone",
48
+ "contactout",
49
+ "crustdata",
50
+ "discolike",
51
+ "exa",
52
+ "findymail",
53
+ "firecrawl",
54
+ "forager",
55
+ "fullenrich",
56
+ "getsales",
57
+ "hunter",
58
+ "icypeas",
59
+ "leadmagic",
60
+ "linkup",
61
+ "millionverifier",
62
+ // Connects through saveMoltsetsConnection rather than the generic save, but that
63
+ // service calls saveApiKeyIntegrationConnection with secretKind "api_key" like the
64
+ // rest — so the native runner does find the key.
65
+ "moltsets",
66
+ "ocean",
67
+ "predictleads",
68
+ "prospeo",
69
+ "scalelist",
70
+ // scraper: dev v1.920.2 added the HarvestAPI workspace-key connect (native api-key form).
71
+ "scraper",
72
+ "serper",
73
+ "signalbase",
74
+ "zapmail",
75
+ "zerobounce",
76
+ ]);
77
+ /**
78
+ * True when a customer can hand Oxygen their own key for this provider AND the
79
+ * native runner will use it. Never infer this from `supports_byok` alone.
80
+ */
81
+ export function hasNativeApiKeyConnect(integrationId) {
82
+ return typeof integrationId === "string"
83
+ && NATIVE_API_KEY_CONNECT_INTEGRATION_IDS.has(integrationId);
84
+ }
@@ -3,6 +3,20 @@ import { RECIPE_PRIMITIVES } from "./recipes.js";
3
3
  // It is navigation, never execution: live schemas, prices, readiness, and
4
4
  // approval enforcement remain on the exact command/tool selected from here.
5
5
  export const OXYGEN_CAPABILITY_ROUTES = [
6
+ {
7
+ id: "visual-rendering",
8
+ layer: "Action",
9
+ primitive: null,
10
+ owns: "Agent-authored GTM infographics and carousel pages: editable HTML/CSS/SVG sources, isolated PNG/PDF rendering, actual visual inspection, revisions and downloads.",
11
+ notFor: "A separate product primitive, arbitrary package execution, image generation providers, motion/video, or publishing.",
12
+ execution: "Save source in workspace files; preview exact rendering scope and price; approve, poll the durable job, inspect its PNG pixels, revise and export. Renderer availability is checked by preview.",
13
+ posture: "mixed",
14
+ gatewayTools: ["oxygen_visual_source_create", "oxygen_visual_render_preview", "oxygen_visual_render_get", "oxygen_visual_image_view"],
15
+ gatewayCommands: ["visuals source-create", "visuals render-preview", "visuals render-get", "visuals download"],
16
+ skills: ["oxygen-visual-design", "oxygen-knowledge"],
17
+ endpointSections: ["visuals"],
18
+ intentTerms: ["infographic", "gtm flow image", "render html", "visual design", "carousel pages", "svg", "png", "pdf export", "graphic designer"],
19
+ },
6
20
  {
7
21
  id: "workspace-access",
8
22
  layer: "Control",
@@ -194,20 +208,20 @@ export const OXYGEN_CAPABILITY_ROUTES = [
194
208
  id: "tables",
195
209
  layer: "Data",
196
210
  primitive: "tables",
197
- owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, cell state, projects, and run provenance.",
211
+ owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, reusable Functions with isolated drafts and published versions, cell state, projects, and run provenance.",
198
212
  notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
199
213
  execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs.",
200
214
  posture: "mixed",
201
- gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk"],
202
- gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link"],
215
+ gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
216
+ gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link", "functions list", "functions draft"],
203
217
  skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration"],
204
- endpointSections: ["action-columns", "callables", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
218
+ endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
205
219
  // "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
206
220
  // blind user eval asked for exactly "link two tables" and was routed to
207
221
  // `tables create` / `columns add` / `enrich-column preview` — none of which
208
222
  // do it. The agent only found the right command by grepping the raw 25k-line
209
223
  // command manifest, which is not a discovery path a customer has.
210
- intentTerms: ["table", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship"],
224
+ intentTerms: ["table", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable"],
211
225
  },
212
226
  {
213
227
  id: "messages",
@@ -320,11 +334,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
320
334
  notFor: "One-to-one messaging, outreach campaigns, or general automation.",
321
335
  execution: "Create and schedule on OXYGEN; explicit approval releases the hosted publisher.",
322
336
  posture: "external_write",
323
- gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve"],
324
- gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve"],
325
- skills: ["oxygen-linkedin-marketing"],
326
- endpointSections: ["publishing"],
327
- intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing"],
337
+ gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve", "oxygen_ugc_get"],
338
+ gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list"],
339
+ skills: ["oxygen-linkedin-marketing", "oxygen-ugc"],
340
+ endpointSections: ["publishing", "ugc"],
341
+ intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator"],
328
342
  },
329
343
  {
330
344
  id: "workflows",
@@ -542,6 +556,8 @@ function normalizeIntent(query) {
542
556
  return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
543
557
  }
544
558
  function explicitCapabilityIntent(query) {
559
+ if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
560
+ return ROUTE_BY_ID.get("visual-rendering") ?? null;
545
561
  // A unified sender profile is an owned Sequence identity, even when the ask
546
562
  // names every attached channel (LinkedIn + WhatsApp + email). Resolve this
547
563
  // before public LinkedIn research, whose generic "profile" wording would
@@ -61,6 +61,8 @@ export type FeatureGateResolution = {
61
61
  * - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
62
62
  * to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
63
63
  * data.ts`). Wrong "on" = public posts from customer accounts.
64
+ * - OXYGEN_UGC_ENABLED — (b). Cross-workspace public posting and sponsored
65
+ * paid jobs use a local fail-closed switch independent of flag services.
64
66
  * - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
65
67
  * production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
66
68
  * value takes the fleet down while work is queued, so it must never depend on a
@@ -52,6 +52,8 @@
52
52
  * - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
53
53
  * to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
54
54
  * data.ts`). Wrong "on" = public posts from customer accounts.
55
+ * - OXYGEN_UGC_ENABLED — (b). Cross-workspace public posting and sponsored
56
+ * paid jobs use a local fail-closed switch independent of flag services.
55
57
  * - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
56
58
  * production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
57
59
  * value takes the fleet down while work is queued, so it must never depend on a
@@ -67,6 +69,7 @@
67
69
  export const NEVER_FLAGGABLE = [
68
70
  "OXYGEN_WORKER_AUTOSCALE",
69
71
  "OXYGEN_PUBLISHING_ENABLED",
72
+ "OXYGEN_UGC_ENABLED",
70
73
  "OXYGEN_AGENTS_ENABLED",
71
74
  "OXYGEN_TELEMETRY_ENABLED",
72
75
  "OXYGEN_LOG_SHIPPING_ENABLED",
@@ -2,8 +2,10 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
2
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
3
  export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, type WorkflowStatusChange, type WorkflowStatusChangeActor, type WorkflowStatusChangeSource, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
4
4
  export * from "./billing.js";
5
+ export * from "./visual-render.js";
5
6
  export * from "./billing-anchors.js";
6
7
  export * from "./budget-scopes.js";
8
+ export * from "./byok-connect.js";
7
9
  export * from "./capability-discovery.js";
8
10
  export * from "./user-capability-routing.js";
9
11
  export * from "./plan-capabilities.js";
@@ -11,6 +13,7 @@ export * from "./plan-limits.js";
11
13
  export * from "./sending-seats.js";
12
14
  export * from "./sending-seat-capacity.js";
13
15
  export * from "./plain-support-events.js";
16
+ export * from "./provider-balance-signal.js";
14
17
  export * from "./provider-funding-errors.js";
15
18
  export * from "./publishing-limits.js";
16
19
  export * from "./spend-safety.js";
@@ -109,3 +112,4 @@ export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
109
112
  export declare function isVersionGreater(a: string, b: string): boolean;
110
113
  /** True when `a` is a strictly lesser semantic version than `b`. */
111
114
  export declare function isVersionLess(a: string, b: string): boolean;
115
+ export * from "./ugc.js";
@@ -2,8 +2,10 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
2
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
3
  export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
4
4
  export * from "./billing.js";
5
+ export * from "./visual-render.js";
5
6
  export * from "./billing-anchors.js";
6
7
  export * from "./budget-scopes.js";
8
+ export * from "./byok-connect.js";
7
9
  export * from "./capability-discovery.js";
8
10
  export * from "./user-capability-routing.js";
9
11
  export * from "./plan-capabilities.js";
@@ -11,6 +13,7 @@ export * from "./plan-limits.js";
11
13
  export * from "./sending-seats.js";
12
14
  export * from "./sending-seat-capacity.js";
13
15
  export * from "./plain-support-events.js";
16
+ export * from "./provider-balance-signal.js";
14
17
  export * from "./provider-funding-errors.js";
15
18
  export * from "./publishing-limits.js";
16
19
  export * from "./spend-safety.js";
@@ -145,3 +148,4 @@ export function isVersionGreater(a, b) {
145
148
  export function isVersionLess(a, b) {
146
149
  return compareSemver(a, b) < 0;
147
150
  }
151
+ export * from "./ugc.js";
@@ -3,7 +3,7 @@ export type KnowledgePageType = typeof KNOWLEDGE_PAGE_TYPES[number];
3
3
  export declare const KNOWLEDGE_META_PAGE_TYPES: readonly ["schema", "source_summary", "report"];
4
4
  export declare const KNOWLEDGE_PAGE_STATUSES: readonly ["draft", "active", "archived"];
5
5
  export type KnowledgePageStatus = typeof KNOWLEDGE_PAGE_STATUSES[number];
6
- export declare const RESERVED_KNOWLEDGE_SLUGS: readonly ["index", "log", "schema", "company-profile", "readme"];
6
+ export declare const RESERVED_KNOWLEDGE_SLUGS: readonly ["index", "log", "schema", "company-profile", "readme", "new"];
7
7
  export declare const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
8
8
  export declare const KNOWLEDGE_SLUG_PATTERN: RegExp;
9
9
  export declare function isValidKnowledgeSlug(value: string): boolean;
@@ -32,17 +32,18 @@ export const KNOWLEDGE_PAGE_TYPES = [
32
32
  export const KNOWLEDGE_META_PAGE_TYPES = ["schema", "source_summary", "report"];
33
33
  export const KNOWLEDGE_PAGE_STATUSES = ["draft", "active", "archived"];
34
34
  // Slugs the write path refuses (the scaffold seeder may create `schema`): they are
35
- // computed projections or virtual pages, never ordinary rows.
35
+ // computed projections, virtual pages, or static UI routes, never ordinary rows.
36
36
  export const RESERVED_KNOWLEDGE_SLUGS = [
37
37
  "index",
38
38
  "log",
39
39
  "schema",
40
40
  "company-profile",
41
41
  "readme",
42
+ "new",
42
43
  ];
43
44
  export const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
44
45
  // Flat kebab grammar: lowercase alphanumeric + dashes, no leading/trailing dash,
45
- // no slashes (pages are a flat namespace; the graph, tags, and types organize).
46
+ // no slashes (slugs are a flat namespace; folders, the graph, tags, and types organize).
46
47
  export const KNOWLEDGE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
47
48
  export function isValidKnowledgeSlug(value) {
48
49
  return KNOWLEDGE_SLUG_PATTERN.test(value);
@@ -84,7 +84,7 @@ Index-first → open only relevant pages → answer citing slugs → file durabl
84
84
  | \`schema\` | describes the wiki itself (this page) |
85
85
 
86
86
  ## Slugs
87
- Flat kebab: lowercase alphanumerics + dashes, no slashes, ≤120 chars. **Immutable once created** — choose the durable name (\`competitor-clay\`, not \`clay-notes-july\`). No folders; types, tags, and wikilinks organize. Reserved (never create): \`index\`, \`log\`, \`schema\`, \`company-profile\`, \`readme\`.
87
+ Flat kebab: lowercase alphanumerics + dashes, no slashes, ≤120 chars. **Immutable once created** — choose the durable name (\`competitor-clay\`, not \`clay-notes-july\`). Nested folders organize pages in the explorer without changing slugs or wikilinks; types and tags provide additional organization. Reserved (never create): \`index\`, \`log\`, \`schema\`, \`company-profile\`, \`readme\`, \`new\`.
88
88
 
89
89
  ## Wikilinks
90
90
  Wrap a slug in double square brackets to link it. Three forms all resolve to the same page — bare (\`slug\`), aliased (\`slug|display text\`), and heading-anchored (\`slug#heading\`) — because the alias and the heading are stripped before lookup. **Linking to a page that doesn't exist yet is good** — the dashed node is a visible to-do. Every page should link out (avoid dead-ends) and be linked to (avoid orphans).
@@ -38,6 +38,13 @@ import { createHash } from "node:crypto";
38
38
  import { log } from "./log.js";
39
39
  const FLUSH_TIMEOUT_MS = 5_000;
40
40
  const WARN_THROTTLE_MS = 30_000;
41
+ /**
42
+ * The OTLP exporter's own deadline, in SECONDS (the @langfuse/otel option's
43
+ * unit). Deliberately ABOVE FLUSH_TIMEOUT_MS: our bound protects the caller,
44
+ * this one protects the batch, and making them equal — the SDK's 5s default —
45
+ * meant every flush that ran long lost its spans instead of finishing late.
46
+ */
47
+ const LANGFUSE_EXPORT_TIMEOUT_SECONDS = 10;
41
48
  // Defensive per-field bound, well under Langfuse's ~1 MB event cap. Copilot
42
49
  // transcripts max out around 150 KB; anything larger is truncated with an
43
50
  // explicit marker rather than risking a rejected ingestion batch.
@@ -179,6 +186,16 @@ function createOtelEmitter(env, warn) {
179
186
  secretKey: env.LANGFUSE_SECRET_KEY,
180
187
  ...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
181
188
  environment: resolveLlmTracingEnvironment(env),
189
+ // SECONDS, and the OTLP POST's own deadline. The SDK default is 5,
190
+ // which is exactly FLUSH_TIMEOUT_MS — zero headroom, so a batch that
191
+ // needed 6s was guaranteed to die on the transport and be dropped:
192
+ // 37 `llm_tracing.ingest_failed` warns over 30 days to 2026-09-09,
193
+ // every one stage='flush' / 'Request timed out', against 275,970
194
+ // traces Langfuse accepted in the same window. 10s gives the POST
195
+ // room without touching FLUSH_TIMEOUT_MS — boundedNever still
196
+ // releases the caller at 5s, so a slow Langfuse can never hold a
197
+ // worker tick.
198
+ timeout: LANGFUSE_EXPORT_TIMEOUT_SECONDS,
182
199
  });
183
200
  // PRIVATE provider. Deliberately NOT .register()ed — see the file
184
201
  // header: the global provider carries the Axiom OTLP exporters, and a
@@ -1,4 +1,10 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
1
2
  export declare function isObjectStorageConfigured(): boolean;
3
+ /** Server-only storage adapter seam. Callers own tenant and object-policy checks. */
4
+ export declare function resolveObjectStorageClient(): {
5
+ client: S3Client;
6
+ bucket: string;
7
+ };
2
8
  export declare function buildImportObjectKey(input: {
3
9
  organizationId: string;
4
10
  fileName?: string | null;
@@ -77,6 +77,11 @@ function resolveClient() {
77
77
  }
78
78
  // Keys are namespaced by org so a tenant can only ever be handed (and the
79
79
  // enqueue route only accepts) keys under its own prefix.
80
+ /** Server-only storage adapter seam. Callers own tenant and object-policy checks. */
81
+ export function resolveObjectStorageClient() {
82
+ const { client, config } = resolveClient();
83
+ return { client, bucket: config.bucket };
84
+ }
80
85
  export function buildImportObjectKey(input) {
81
86
  const safeName = sanitizeFileName(input.fileName) || "import";
82
87
  return `imports/${input.organizationId}/${randomUUID()}/${safeName}`;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The managed-provider BALANCE signal — one contract shared by the two producers
3
+ * that can observe it and the one Axiom monitor that alerts on it.
4
+ *
5
+ * WHY THIS EXISTS. Until now the only way OXYGEN learned that one of its POOLED
6
+ * provider accounts had run out of money was a customer's call being refused.
7
+ * `provider.managed_credits_exhausted` fires at that moment — after the failure,
8
+ * never before it. Plain T-104 (Mentcape, 2026-08-24 and again 2026-08-31) is what
9
+ * that costs: `serper.places`, `serper.maps` and `parallel.search` refused live
10
+ * calls for a workspace holding 75,347 Oxygen credits, because OUR balance with
11
+ * those vendors was empty. Prod Axiom over the 16 days to 2026-09-05 shows the same
12
+ * refusal reaching >=6 orgs on serper, >=3 on parallel and >=5 on exa.
13
+ *
14
+ * The balance snapshot cron already read most of those balances every two hours and
15
+ * wrote them to `provider_balance_snapshots`. It emitted no per-provider log line at
16
+ * all, so a balance sliding toward zero was visible only to a human who opened
17
+ * /admin/costs. This module is the missing signal: one structured line per managed
18
+ * provider per snapshot, carrying the number AND the verdict.
19
+ *
20
+ * TWO PRODUCERS, ONE VOCABULARY.
21
+ *
22
+ * - `provider_balance.snapshot` — the cron, for every provider whose balance we can
23
+ * actually read. Proactive: it fires while there is still money left.
24
+ * - `provider_balance.exhausted` — the tool runner, when a managed account actually
25
+ * refuses a paid call. That is the ONLY balance evidence available for a provider
26
+ * with no usable balance API, which is exactly the T-104 three (see
27
+ * PROVIDERS_WITHOUT_BALANCE_API in @oxygen/providers). Without it the new monitor
28
+ * would be structurally blind to the providers that caused the incident.
29
+ *
30
+ * Both carry the same `status` vocabulary so ONE monitor query covers both, and both
31
+ * put `status` and `provider` in flat fields because those are the two dimensions the
32
+ * monitor filters and groups on. Every other field rides in the `worker_fields` map
33
+ * at zero column cost — see AXIOM_STABLE_FIELDS in ./axiom-field-budget.ts, which is
34
+ * load-bearing in both directions.
35
+ */
36
+ /** The proactive line, emitted per managed provider by the balance-snapshot cron. */
37
+ export declare const PROVIDER_BALANCE_SNAPSHOT_MSG = "provider_balance.snapshot";
38
+ /**
39
+ * The reactive line, emitted by the tool runner when a managed account refuses a paid
40
+ * call. Deliberately a SEPARATE msg from `provider.managed_credits_exhausted`: that
41
+ * one is the refusal event (a customer call failed), this one is the balance fact (our
42
+ * account is empty). The refusal monitor counts the former; conflating them would make
43
+ * one query mean two different things.
44
+ */
45
+ export declare const PROVIDER_BALANCE_EXHAUSTED_MSG = "provider_balance.exhausted";
46
+ /**
47
+ * The verdict on one managed balance.
48
+ *
49
+ * `unknown` is not a synonym for `ok`, and the distinction is the whole point: the
50
+ * pre-existing snapshot recorded status `ok` with `balance_remaining = null` for
51
+ * blitzapi and contactout on all 353 rows of the last 30 days, and would have
52
+ * recorded `ok` at a balance of 0 too. "We asked and got no number" must never read
53
+ * as "there is money".
54
+ */
55
+ export type ProviderBalanceStatus = "ok" | "low" | "exhausted" | "unknown" | "not_monitored" | "auth_error" | "rate_limited" | "error";
56
+ /** The statuses the P1 balance monitor alerts on. Anything else is informational. */
57
+ export declare const ALERTING_PROVIDER_BALANCE_STATUSES: readonly ProviderBalanceStatus[];
58
+ /**
59
+ * Default low-balance floor, in the provider's OWN unit (almost always provider
60
+ * credits). 1,000 is not a round number chosen for looking tidy — it is roughly
61
+ * three to eight days of measured burn for the credit providers OXYGEN funds,
62
+ * taken from `provider_balance_snapshots` in the prod control DB on 2026-09-07
63
+ * over the preceding 30 days:
64
+ *
65
+ * bettercontact 10,130 -> 364 (~325/day) ~3 days of head-room at 1,000
66
+ * leadmagic 7,702 -> 903 (~227/day) ~4 days
67
+ * millionverifier 41,357 -> 37,695 (~122/day) ~8 days
68
+ * ai_ark 10,000 -> 9,813 (~6/day) months
69
+ *
70
+ * A floor is a claim about how the account behaved when it was measured, nothing
71
+ * more. Re-measure it rather than nudging it when it turns out to be noisy — the
72
+ * monitor changelog exists for exactly that conversation.
73
+ */
74
+ export declare const DEFAULT_MANAGED_BALANCE_FLOOR = 1000;
75
+ /**
76
+ * Per-provider floors, and the providers that are deliberately NOT balance-monitored
77
+ * (`null`). Only entries that the default gets wrong are listed; everything else
78
+ * inherits DEFAULT_MANAGED_BALANCE_FLOOR.
79
+ */
80
+ export declare const MANAGED_BALANCE_FLOORS: Readonly<Record<string, number | null>>;
81
+ /** `OXYGEN_MANAGED_BALANCE_FLOOR_FIRECRAWL`, `..._AI_ARK`, ... */
82
+ export declare function managedBalanceFloorEnvVar(provider: string): string;
83
+ /**
84
+ * The floor in force for one provider. `null` means "do not judge this balance".
85
+ *
86
+ * The env override is the operator's escape hatch between deploys: a top-up that
87
+ * changes the plan size, or a provider that turns out to be noisy at the default,
88
+ * is one Doppler value away from being right. `off`/`none`/`disabled` switches the
89
+ * provider off the monitor entirely; an unparseable or negative value is IGNORED
90
+ * rather than obeyed, because a typo must not silently disarm an alert.
91
+ */
92
+ export declare function managedBalanceFloor(provider: string, env?: Record<string, string | undefined>): number | null;
93
+ export type ProviderBalanceVerdict = {
94
+ status: ProviderBalanceStatus;
95
+ /** The single boolean the monitor's description tells a responder to read. */
96
+ balanceLow: boolean;
97
+ /** The floor actually applied, after the env override. `null` = not monitored. */
98
+ floor: number | null;
99
+ level: "info" | "warn" | "error";
100
+ };
101
+ /**
102
+ * Turn one balance reading into the verdict both producers log.
103
+ *
104
+ * Pure on purpose: the thresholds are the alerting policy, so they are unit-testable
105
+ * without a provider, a cron, or a network.
106
+ */
107
+ export declare function classifyManagedBalance(input: {
108
+ provider: string;
109
+ /** The balance fetcher's own status. Anything but "ok" means we are blind. */
110
+ fetchStatus: "ok" | "auth_error" | "rate_limited" | "error";
111
+ balanceRemaining: number | null;
112
+ env?: Record<string, string | undefined>;
113
+ }): ProviderBalanceVerdict;