@oxygen-agent/cli 1.1011.974 → 1.1012.2
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/README.md +1 -1
- package/dist/command-manifest.d.ts +3 -1
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +326 -111
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
- package/node_modules/@oxygen/shared/dist/geo.js +314 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +88 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +15 -5
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
|
@@ -43,7 +43,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
43
43
|
operation("copilot.attachments.delete", "/api/cli/copilot/sessions/{session_id}/attachments/{attachment_id}", "DELETE", "copilot attachments delete", "oxygen_copilot_attachment_delete", "agents", "Detach an unsent Copilot attachment; attachments already quoted by a turn cannot be removed."),
|
|
44
44
|
operation("copilot.feedback", "/api/cli/copilot/sessions/{session_id}/turns/{turn_id}/feedback", "POST", "copilot feedback", "oxygen_copilot_feedback", "agents", "Rate one attended Copilot turn and optionally explain the rating.", { rating: enumOf(["up", "down"]), comment: text() }, ["rating"]),
|
|
45
45
|
operation("copilot.interrupt", "/api/cli/copilot/sessions/{session_id}/turns/{turn_id}/interrupt", "POST", "copilot interrupt", "oxygen_copilot_interrupt", "agents", "Request cancellation of one Copilot turn. Read the session again to confirm it stopped.", {}, [], mcpOnly),
|
|
46
|
-
operation("context.canonical.get", "/api/cli/context/canonical/get", "POST", "context canonical get", "oxygen_context_canonical_get", "knowledge", "Read the pinned
|
|
46
|
+
operation("context.canonical.get", "/api/cli/context/canonical/get", "POST", "context canonical get", "oxygen_context_canonical_get", "knowledge", "Read the current canonical (pinned) voice, brand or positioning that AI copy uses, or any type's pinned asset. present=false means none is pinned; grounding_when_absent says what AI copy uses instead.", { type: text("Type to read: voice, brand or positioning (the canonical copy AI uses), or another context type such as playbook or persona.") }, ["type"], { mutates: false }),
|
|
47
47
|
operation("crm.notes.update", "/api/cli/crm/notes/{note_id}", "PATCH", "crm notes update", "oxygen_crm_note_update", "crm", "Edit a CRM note's body, title or pinned state; an empty title clears it.", { body: text(), title: nullable(text()), pinned: bool() }, [], mcpOnly),
|
|
48
48
|
operation("crm.notes.delete", "/api/cli/crm/notes/{note_id}", "DELETE", "crm notes delete", "oxygen_crm_note_delete", "crm", "Delete one CRM note in this workspace.", {}, [], mcpOnly),
|
|
49
49
|
operation("crm.files.attach_inline", "/api/cli/crm/objects/{object}/records/{row_id}/files", "POST", "crm files attach-inline", "oxygen_crm_file_attach", "crm", CRM_FILE_ATTACH_DESCRIPTION, CRM_FILE_ATTACH_CONTRACT.properties, [...(CRM_FILE_ATTACH_CONTRACT.required ?? [])]),
|
|
@@ -62,7 +62,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
62
62
|
operation("directory.apply", "/api/cli/directory/apply", "POST", "directory apply", "oxygen_directory_apply", "crm", "Apply for an agency-directory listing for this workspace; reuses an existing open application.", { message: text(), website: text() }, [], mcpOnly),
|
|
63
63
|
operation("directory.logo", "/api/cli/directory/listing/logo", "POST", "directory logo upload", "oxygen_directory_logo_upload", "crm", "Upload a public agency-directory logo and read back the updated listing; PNG, JPEG, WebP or SVG, at most 2 MB.", { content_base64: text(), file_name: text(), content_type: enumOf(["image/png", "image/jpeg", "image/webp", "image/svg+xml"]) }, ["content_base64", "file_name", "content_type"], { multipart: { contentField: "content_base64", nameField: "file_name", typeField: "content_type", fileField: "file" } }),
|
|
64
64
|
operation("domains.delete", "/api/cli/domains/delete", "POST", "domains delete", "oxygen_domains_delete", "deliverability", "Preview domain removal and renewal cancellation; execute only with the exact plan hash and confirmation.", { domains: list(), approved: bool(), plan_hash: text(), confirmation: text() }, ["domains"], mcpOnly),
|
|
65
|
-
operation("knowledge.pages.archive", "/api/cli/knowledge/pages/archive", "POST", "knowledge page archive", "oxygen_knowledge_page_archive", "knowledge", "Archive a knowledge page
|
|
65
|
+
operation("knowledge.pages.archive", "/api/cli/knowledge/pages/archive", "POST", "knowledge page archive", "oxygen_knowledge_page_archive", "knowledge", "Archive a knowledge page; content/history remain, links become unresolved. Pass expected_revision for the version you reviewed.", { slug: text(), id: text(), expected_revision: int({ minimum: 1 }) }, [], mcpOnly),
|
|
66
66
|
operation("knowledge.sync.manifest", "/api/cli/knowledge/sync/manifest", "POST", "knowledge sync-manifest", "oxygen_knowledge_sync_manifest", "knowledge", "Read the wiki revision and content-hash inventory without downloading instruction bodies.", {}, [], { mutates: false }),
|
|
67
67
|
operation("knowledge.sync.delta", "/api/cli/knowledge/sync/delta", "POST", "knowledge sync", "oxygen_knowledge_sync_delta", "knowledge", "Read one resumable batch of wiki pages and tombstones, bounded by the server's byte budget.", { cursor: text(), limit: int({ minimum: 1, maximum: 200 }) }, [], { ...mcpOnly, mutates: false }),
|
|
68
68
|
operation("limits.provider.get", "/api/cli/limits/provider", "GET", "limits provider list", "oxygen_provider_limits_list", "billing", "Read provider request limits and BYOK warning defaults. MCP defaults to bounded summary pages; follow paging.next_offset with the same filter. HTTP/CLI omit pagination for the full legacy list. 0 credits.", {}, [], { contract: providerLimitListContract, generated: { cli: false, mcp: false } }),
|
|
@@ -81,7 +81,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
81
81
|
operation("settings.beta.set", "/api/cli/settings/beta-features", "POST", "settings beta-features set", "oxygen_settings_beta_features_set", "workspace", "Enable or disable beta navigation for every workspace member; admin or owner only. Approve the preference change. This does not disable routes. 0 credits.", {}, [], { contract: betaFeaturesUpdateContract }),
|
|
82
82
|
operation("spintax.generate", "/api/cli/spintax", "POST", "spintax generate", "oxygen_spintax_generate", "sequences", "Generate wording or copy variations as spintax: live (default) costs 3 credits and requires approval and a credit ceiling; explicit dry_run is a free preview. Preserve merge variables; send nothing. Live requires approved:true and max_credits>=3 after preview; failed generation refunds the reservation.", {}, [], { contract: spintaxContract, defaults: { mode: "live" }, spends: true }),
|
|
83
83
|
operation("table_action_items.resolve", "/api/cli/table-action-items/{item_id}/resolve", "POST", "table-actions resolve", "oxygen_table_action_item_resolve", "tables", "Record a human-verified uncertain Table action outcome without repeating it. Use cells resolve for row/column coordinates.", TABLE_ITEM_RESOLVE_CONTRACT.properties, ["outcome"]),
|
|
84
|
-
operation("table_ingestions.append", "/api/cli/table-ingestion-runs/{ingestion_run_id}/items", "POST", "table-ingestions append", "oxygen_table_ingestions_append_items", "runs", "Append
|
|
84
|
+
operation("table_ingestions.append", "/api/cli/table-ingestion-runs/{ingestion_run_id}/items", "POST", "table-ingestions append", "oxygen_table_ingestions_append_items", "runs", "Append generated row chunks with items [{position:0,payload:{rows:[{column:value}]}}]. For capped provider paging, create a new approved tool_page run from the last completed item.nextCursor; preserve the Table, filters/mapping/upsert key and use a new credit cap.", { items: rows(), max_attempts: int({ minimum: 1 }) }, ["items"], cliOnly),
|
|
85
85
|
operation("tables.favorites.get", "/api/cli/tables/favorite", "GET", "tables favorites", "oxygen_tables_favorites", "tables", "Read starred Tables/folders (per-member preference; free (0 credits)); next_cursor. Catalog stars differ.", TABLE_FAVORITES_LIST_CONTRACT.properties, [], { generated: { cli: true, mcp: false } }),
|
|
86
86
|
operation("tables.favorite.set", "/api/cli/tables/favorite", "POST", "tables favorite", "oxygen_tables_favorite", "tables", "Star/unstar exactly one table or folder (saved per-member preference). Free (0 credits). Catalog stars differ.", TABLE_FAVORITE_CONTRACT.properties, ["favorite"]),
|
|
87
87
|
operation("tables.import.csv", "/api/cli/tables/import-csv", "POST", "tables import-csv", "oxygen_tables_import_csv", "tables", "Import inline CSV into a hosted Table through its durable ingestion run. Use staged upload for large files.", { csv_text: text(), csv_base64: text(), table: text(), table_name: text(), project: text(), filename: text(), upsert_key: text(), batch_size: int({ minimum: 1, maximum: 5000 }) }, [], cliOnly),
|
|
@@ -90,7 +90,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
90
90
|
operation("tables.import.staged", "/api/cli/tables/import-staged", "POST", "tables import-staged", "oxygen_tables_import_staged", "tables", "Start a durable hosted import from this workspace's staged file; choose an existing table or create_name.", { table: text(), create_name: text(), project: text(), storage_key: text(), storage_bucket: text(), storage_provider: enumOf(["s3"]), file_name: text(), format: enumOf(["csv", "json", "xlsx"]), sha256: text(), upsert_key: text(), source_key_map: json, byte_length: int({ minimum: 1 }), row_count: int({ minimum: 1 }), max_concurrency: int({ minimum: 1, maximum: 1000 }), source: enumOf(["cli", "mcp", "web"]), batch_size: int({ minimum: 1, maximum: 5000 }) }, ["storage_key", "byte_length"]),
|
|
91
91
|
operation("tables.import.file", "/api/cli/tables/import-file", "POST", "tables import-file", "oxygen_tables_import_file", "tables", "Import small base64 file bytes through the shared multipart import API; use staged upload for large files.", { content_base64: text(), file_name: text(), content_type: text(), table: text(), table_name: text(), project: text(), format: enumOf(["csv", "json", "xlsx"]), sheet: text(), upsert_key: text(), source_key_map: json, source_hash: text(), batch_size: int({ minimum: 1, maximum: 5000 }), max_concurrency: int({ minimum: 1 }) }, ["content_base64", "file_name"], { multipart: { contentField: "content_base64", nameField: "file_name", typeField: "content_type", fileField: "file" } }),
|
|
92
92
|
operation("tables.link.suggest", "/api/cli/tables/link/suggest", "POST", "tables link-suggest", "oxygen_tables_link_suggest", "tables", "Plan column links and matching row counts between Tables; creates no relation. Apply with tables link bulk.", TABLE_LINK_SUGGEST_CONTRACT.properties, ["table", "to"], { mutates: false }),
|
|
93
|
-
operation("tables.recover_pending", "/api/cli/tables/recover-pending", "POST", "tables recover-pending", "oxygen_tables_recover_pending", "tables", "
|
|
93
|
+
operation("tables.recover_pending", "/api/cli/tables/recover-pending", "POST", "tables recover-pending", "oxygen_tables_recover_pending", "tables", "Recover BetterContact email/phone requests and read verification blockers; no new orders or imports.", TABLE_RECOVER_PENDING_CONTRACT.properties, ["table"], { ...mcpOnly, annotations: { openWorldHint: true } }),
|
|
94
94
|
operation("tables.webhooks.create", "/api/cli/tables/webhooks", "POST", "tables webhook create", "oxygen_table_webhook_create", "tables", "Create an inbound Table webhook or platform LinkedIn feed. Optional auto_run is standing permission with explicit or plan-default delivery ceilings.", { table: text(), name: text(), mode: enumOf(["insert", "upsert"]), upsert_key: text(), items_path: text(), event_id_path: text(), event_type_path: text(), occurred_at_path: text(), auth_mode: enumOf(["none", "secret"]), feed_kind: enumOf(["generic", "unipile"]), unipile_account_id: text(), event_types: list(), mapping: text(), auto_run: json }, ["table"], mcpOnly),
|
|
95
95
|
operation("tables.webhooks.update", "/api/cli/tables/webhooks", "PATCH", "tables webhook update", "oxygen_table_webhook_update", "tables", "Edit a Table webhook's name, state or standing auto_run; auth_mode=secret rotates its secret and returns it once.", { endpoint_id: text(), name: text(), status: enumOf(["active", "disabled"]), auth_mode: enumOf(["none", "secret"]), auto_run: json, clear_auto_run: bool() }, ["endpoint_id"], mcpOnly),
|
|
96
96
|
operation("whatsapp.draft", "/api/cli/whatsapp/draft", "POST", "whatsapp draft", "oxygen_whatsapp_draft", "inbox", "Generate a WhatsApp reply draft in the workspace voice, or approve a reviewed draft for an existing chat. Draft generation uses managed AI credits.", { conversation_id: text(), goal: text(), row_id: text(), object: text(), approved: bool(), draft_id: text(), text: text() }, ["conversation_id"], { ...cliOnly, spends: true }),
|
|
@@ -102,7 +102,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
102
102
|
operation("db.provision", "/api/cli/db/provision", "POST", "db provision", "oxygen_workspace_database_provision", "workspace", "Provision or retry this workspace database immediately; already-ready workspaces retain it. No preview or confirmation field; native workspace write access applies. May outlast the connector deadline: read oxygen_workspace_database_status before retrying. Hosting is plan-included; 0 credits.", {}, [], mcpOnly),
|
|
103
103
|
operation("sessions.list", "/api/cli/sessions", "GET", "sessions list", "oxygen_sessions_list", "workspace", "List your personal CLI sessions across workspaces; OAuth or personal user session required, organization API keys are refused."),
|
|
104
104
|
operation("sessions.revoke", "/api/cli/sessions/{session_id}", "DELETE", "sessions revoke", "oxygen_sessions_revoke", "workspace", "Revoke one of your personal CLI sessions; OAuth or personal user session required, organization API keys are refused."),
|
|
105
|
-
operation("billing.allowance", "/api/cli/billing/allowance", "GET", "billing allowance", "oxygen_billing_allowance", "billing", "
|
|
105
|
+
operation("billing.allowance", "/api/cli/billing/allowance", "GET", "billing allowance", "oxygen_billing_allowance", "billing", "Inspect this workspace's recurring credit allowance and period; due plan grants or credit expiry may apply. 0 credits; reserves nothing.", {}, [], { ...mcpOnly, mutates: true }),
|
|
106
106
|
operation("billing.invoices", "/api/cli/billing/invoices", "GET", "billing invoices", "oxygen_billing_invoices", "billing", "Read this workspace's billing invoices and their hosted payment links.", {}, [], mcpOnly),
|
|
107
107
|
operation("billing.seats.set", "/api/cli/billing/seats", "POST", "billing seats set", "oxygen_billing_seats_set", "billing", "Set purchased legacy sending-seat quantity. Retired seats cannot increase; reducing existing seats lowers the next renewal charge.", { seat_key: enumOf(["linkedin", "whatsapp", "phone_number", "email_sender"]), quantity: int({ minimum: 0 }) }, ["seat_key", "quantity"], mcpOnly),
|
|
108
108
|
operation("collaboration.gates.list", "/api/cli/collab/gates", "GET", "approvals gate list", "oxygen_approval_gates_list", "collaboration", "Read this workspace's approval gates, optionally for one exact subject.", { subject: text() }, [], mcpOnly),
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { arr, bool, enumOf, int, obj, str, } from "./schema.js";
|
|
2
|
+
import { GEO_ENGINES } from "../geo.js";
|
|
3
|
+
const text = (maxLength = 1000) => str({ maxLength });
|
|
4
|
+
const strings = () => arr(text(), { maxItems: 100 });
|
|
5
|
+
const engine = enumOf(GEO_ENGINES);
|
|
6
|
+
const market = obj({
|
|
7
|
+
country: str({ minLength: 2, maxLength: 2 }),
|
|
8
|
+
language: str({ minLength: 2, maxLength: 10 }),
|
|
9
|
+
}, { required: ["country", "language"] });
|
|
10
|
+
const project = {
|
|
11
|
+
name: str({ minLength: 1, maxLength: 120 }),
|
|
12
|
+
aliases: strings(),
|
|
13
|
+
domains: arr(text(), { minItems: 1, maxItems: 100 }),
|
|
14
|
+
icp: text(5000),
|
|
15
|
+
engines: arr(engine, { minItems: 1, maxItems: 7 }),
|
|
16
|
+
markets: arr(market, { minItems: 1, maxItems: 10 }),
|
|
17
|
+
repetitions: int({ minimum: 1, maximum: 5 }),
|
|
18
|
+
archived: bool(),
|
|
19
|
+
};
|
|
20
|
+
const prompt = {
|
|
21
|
+
text: str({ minLength: 1, maxLength: 500 }),
|
|
22
|
+
topic: text(120),
|
|
23
|
+
persona: text(120),
|
|
24
|
+
stage: text(120),
|
|
25
|
+
source: enumOf(["hypothesis", "research", "customer"]),
|
|
26
|
+
evidence_id: text(),
|
|
27
|
+
enabled: bool(),
|
|
28
|
+
};
|
|
29
|
+
const filters = {
|
|
30
|
+
engine,
|
|
31
|
+
country: text(2),
|
|
32
|
+
language: text(10),
|
|
33
|
+
prompt_id: text(),
|
|
34
|
+
topic: text(120),
|
|
35
|
+
from: text(40),
|
|
36
|
+
to: text(40),
|
|
37
|
+
limit: int({ minimum: 1, maximum: 200 }),
|
|
38
|
+
cursor: text(100),
|
|
39
|
+
};
|
|
40
|
+
function operation(id, suffix, method, command, summary, fields = {}, required = [], spends = false) {
|
|
41
|
+
const route = `/api/cli/geo${suffix}`;
|
|
42
|
+
return {
|
|
43
|
+
id: `geo.${id}`,
|
|
44
|
+
route,
|
|
45
|
+
method,
|
|
46
|
+
parameters: [...route.matchAll(/\{(\w+)\}/g)].map((m) => m[1]),
|
|
47
|
+
contract: obj(fields, { required }),
|
|
48
|
+
cli: {
|
|
49
|
+
path: ["geo", ...command.split(" ")],
|
|
50
|
+
summary: `Staff-only dev pilot: ${summary}`,
|
|
51
|
+
},
|
|
52
|
+
mcp: {
|
|
53
|
+
name: `oxygen_geo_${id.replaceAll(".", "_")}`,
|
|
54
|
+
title: `Oxygen GEO: ${command}`,
|
|
55
|
+
description: `${summary} Experimental: staff in dev workspaces only.`,
|
|
56
|
+
pack: "geo",
|
|
57
|
+
},
|
|
58
|
+
mutates: method !== "GET",
|
|
59
|
+
spends,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export const GEO_OPERATIONS = [
|
|
63
|
+
operation("projects.list", "/projects", "GET", "projects list", "List GEO / AEO AI-search tracking projects."),
|
|
64
|
+
operation("projects.create", "/projects", "POST", "projects create", "Create a GEO / AEO brand tracking project.", project, ["name", "domains"]),
|
|
65
|
+
operation("projects.get", "/projects/{project_id}", "GET", "projects get", "Read a GEO / AEO project and its tracking settings."),
|
|
66
|
+
operation("projects.update", "/projects/{project_id}", "PATCH", "projects update", "Change brand aliases, ICP, markets or tracking settings.", project),
|
|
67
|
+
operation("context.get", "/context", "GET", "context get", "Read editable brand, ICP and offering context from workspace Knowledge."),
|
|
68
|
+
operation("engines.list", "/engines", "GET", "engines list", "Inspect consumer answer-engine readiness, supported markets and collection cost."),
|
|
69
|
+
operation("prompts.list", "/projects/{project_id}/prompts", "GET", "prompts list", "List tracked buyer prompts and research provenance.", { limit: filters.limit, cursor: filters.cursor }),
|
|
70
|
+
operation("prompts.create", "/projects/{project_id}/prompts", "POST", "prompts add", "Add a reviewed buyer question to GEO / AEO tracking.", prompt, ["text"]),
|
|
71
|
+
operation("prompts.update", "/projects/{project_id}/prompts/{document_id}", "PATCH", "prompts update", "Revise or pause one tracked buyer question.", prompt),
|
|
72
|
+
operation("prompts.import", "/projects/{project_id}/prompts/import", "POST", "prompts import", "Import reviewed buyer prompts in one bounded batch.", {
|
|
73
|
+
prompts: arr(obj(prompt, { required: ["text"] }), {
|
|
74
|
+
minItems: 1,
|
|
75
|
+
maxItems: 100,
|
|
76
|
+
}),
|
|
77
|
+
}, ["prompts"]),
|
|
78
|
+
operation("competitors.list", "/projects/{project_id}/competitors", "GET", "competitors list", "List brands benchmarked against this project.", { limit: filters.limit, cursor: filters.cursor }),
|
|
79
|
+
operation("competitors.create", "/projects/{project_id}/competitors", "POST", "competitors add", "Add a competitive brand and its aliases and domains.", { name: project.name, aliases: strings(), domains: strings() }, ["name"]),
|
|
80
|
+
operation("competitors.update", "/projects/{project_id}/competitors/{document_id}", "PATCH", "competitors update", "Update competitive brand aliases and owned domains.", { name: project.name, aliases: strings(), domains: strings() }),
|
|
81
|
+
operation("runs.preview", "/projects/{project_id}/runs/preview", "POST", "runs preview", "Preview consumer collection coverage and cost without spending.", { prompt_ids: strings() }),
|
|
82
|
+
operation("runs.start", "/projects/{project_id}/runs", "POST", "runs start", "Run reviewed buyer prompts against real consumer answer engines within the pilot budget.", { preview_token: text(128) }, ["preview_token"], true),
|
|
83
|
+
operation("runs.list", "/projects/{project_id}/runs", "GET", "runs list", "List tracking runs with completion and costs."),
|
|
84
|
+
operation("runs.get", "/projects/{project_id}/runs/{run_id}", "GET", "runs get", "Inspect a tracking run, provider tasks, costs, failures and retries."),
|
|
85
|
+
operation("runs.retry", "/projects/{project_id}/runs/{run_id}/retry", "POST", "runs retry", "Resume failed reads of saved provider tasks without a new paid submission."),
|
|
86
|
+
operation("runs.cancel", "/projects/{project_id}/runs/{run_id}/cancel", "POST", "runs cancel", "Stop unsubmitted samples; submitted collection retains its spend reservation."),
|
|
87
|
+
operation("schedules.set", "/projects/{project_id}/schedule", "PATCH", "schedule set", "Pause or configure daily/weekly GEO tracking within a reviewed cost ceiling.", {
|
|
88
|
+
cadence: enumOf(["paused", "daily", "weekly"]),
|
|
89
|
+
preview_token: text(128),
|
|
90
|
+
}, ["cadence"], true),
|
|
91
|
+
operation("answers.list", "/projects/{project_id}/answers", "GET", "answers list", "Inspect collected answers and imported samples with citations and mention evidence.", filters),
|
|
92
|
+
operation("answers.get", "/projects/{project_id}/answers/{document_id}", "GET", "answers get", "Read the immutable original answer and full provider payload."),
|
|
93
|
+
operation("answers.import", "/projects/{project_id}/answers", "POST", "answers import", "Import a manually observed consumer answer, separate from controlled tracker metrics.", {
|
|
94
|
+
engine,
|
|
95
|
+
prompt: text(500),
|
|
96
|
+
text: text(100000),
|
|
97
|
+
urls: strings(),
|
|
98
|
+
captured_at: text(40),
|
|
99
|
+
}, ["engine", "prompt", "text"]),
|
|
100
|
+
operation("analytics.get", "/projects/{project_id}/analytics", "GET", "analytics get", "Read AI visibility, citations, share of voice and trends with sample denominators.", filters),
|
|
101
|
+
operation("sources.list", "/projects/{project_id}/sources", "GET", "sources list", "Find domains and pages cited by AI, including competitive citation gaps.", filters),
|
|
102
|
+
operation("evidence.preview", "/projects/{project_id}/evidence/preview", "POST", "evidence preview", "Preview and redact ChatGPT/Claude conversation exports before saving.", { transcript: text(1000000), redact: strings() }, ["transcript"]),
|
|
103
|
+
operation("evidence.list", "/projects/{project_id}/evidence", "GET", "evidence list", "List customer conversation evidence and original buyer language.", { limit: filters.limit, cursor: filters.cursor }),
|
|
104
|
+
operation("evidence.create", "/projects/{project_id}/evidence", "POST", "evidence save", "Save a reviewed redacted conversation with optional CRM attribution.", {
|
|
105
|
+
transcript: text(1000000),
|
|
106
|
+
redact: strings(),
|
|
107
|
+
record_id: text(),
|
|
108
|
+
record_object: text(120),
|
|
109
|
+
engine,
|
|
110
|
+
title: text(120),
|
|
111
|
+
}, ["transcript", "engine"]),
|
|
112
|
+
operation("audits.create", "/projects/{project_id}/audits", "POST", "audits run", "Audit up to ten owned public pages for crawlability and citation readiness.", { urls: arr(text(2000), { minItems: 1, maxItems: 10 }) }, ["urls"]),
|
|
113
|
+
operation("audits.list", "/projects/{project_id}/audits", "GET", "audits list", "Read site audit findings and proposed fixes.", { limit: filters.limit, cursor: filters.cursor }),
|
|
114
|
+
operation("opportunities.list", "/projects/{project_id}/opportunities", "GET", "opportunities list", "Read prioritized GEO content, source and messaging opportunities.", { limit: filters.limit, cursor: filters.cursor }),
|
|
115
|
+
operation("opportunities.refresh", "/projects/{project_id}/opportunities/refresh", "POST", "opportunities refresh", "Derive actionable content briefs and citation gaps from stored tracking evidence."),
|
|
116
|
+
operation("opportunities.draft", "/projects/{project_id}/opportunities/{document_id}/draft", "POST", "opportunities draft", "Save a reviewed content brief as a Knowledge page or an unapproved social draft.", {
|
|
117
|
+
target: enumOf(["knowledge", "post"]),
|
|
118
|
+
content: text(50000),
|
|
119
|
+
channel: enumOf(["linkedin", "x"]),
|
|
120
|
+
}, ["target"]),
|
|
121
|
+
operation("experiments.list", "/projects/{project_id}/experiments", "GET", "experiments list", "List optimization experiments and observed before/after changes.", { limit: filters.limit, cursor: filters.cursor }),
|
|
122
|
+
operation("experiments.create", "/projects/{project_id}/experiments", "POST", "experiments create", "Record an optimization hypothesis, baseline and target buyer questions.", {
|
|
123
|
+
title: text(120),
|
|
124
|
+
hypothesis: text(5000),
|
|
125
|
+
prompt_ids: strings(),
|
|
126
|
+
opportunity_id: text(),
|
|
127
|
+
proposed_change: text(10000),
|
|
128
|
+
}, ["title", "hypothesis"]),
|
|
129
|
+
operation("experiments.update", "/projects/{project_id}/experiments/{document_id}", "PATCH", "experiments update", "Record a reviewed change or evaluate an experiment against subsequent samples.", {
|
|
130
|
+
status: enumOf(["planned", "changed", "evaluated", "cancelled"]),
|
|
131
|
+
change_url: text(2000),
|
|
132
|
+
notes: text(10000),
|
|
133
|
+
}),
|
|
134
|
+
{
|
|
135
|
+
...operation("exports.get", "/projects/{project_id}/export", "GET", "export", "Download GEO evidence, metrics or an internal PDF report.", { format: enumOf(["json", "csv", "pdf"]), ...filters }),
|
|
136
|
+
binary: { maxBytes: 20_000_000 },
|
|
137
|
+
},
|
|
138
|
+
];
|
|
@@ -61,6 +61,12 @@ export type OperationSpec<C extends ContractObject = ContractObject> = {
|
|
|
61
61
|
};
|
|
62
62
|
mutates: boolean;
|
|
63
63
|
spends: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* MCP only: what a trimmed result should advise instead of the generic
|
|
66
|
+
* "smaller limit / a cursor", for a read whose single row can exceed the
|
|
67
|
+
* result ceiling (OXP-1.11.67).
|
|
68
|
+
*/
|
|
69
|
+
truncationHint?: string;
|
|
64
70
|
};
|
|
65
71
|
/** The route with its `{param}` segments filled in, each URL-encoded. */
|
|
66
72
|
export declare function operationPath(spec: Pick<OperationSpec, "route">, params: Readonly<Record<string, string>>): string;
|
|
@@ -10,6 +10,7 @@ import { SENDER_LIMITS_SET_OPERATION } from "./sender-limits.js";
|
|
|
10
10
|
import { WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION } from "./workspace-pages.js";
|
|
11
11
|
import { BROWSER_OPERATIONS } from "./browser-operations.js";
|
|
12
12
|
import { TABLE_FEED_UPDATE_OPERATION } from "./table-feed-update.js";
|
|
13
|
+
import { GEO_OPERATIONS } from "./geo.js";
|
|
13
14
|
import { CUSTOMER_OPERATIONS } from "./customer-operations.js";
|
|
14
15
|
/** The route with its `{param}` segments filled in, each URL-encoded. */
|
|
15
16
|
export function operationPath(spec, params) {
|
|
@@ -80,6 +81,6 @@ export function operationFormData(spec, body) {
|
|
|
80
81
|
}
|
|
81
82
|
return form;
|
|
82
83
|
}
|
|
83
|
-
export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...BROWSER_OPERATIONS];
|
|
84
|
-
export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...BROWSER_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
|
|
85
|
-
export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...BROWSER_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
|
|
84
|
+
export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS];
|
|
85
|
+
export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
|
|
86
|
+
export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
|
|
@@ -12,6 +12,7 @@ export const PUBLISHING_AMPLIFICATION_SCOPE_KINDS = ["all", "tag", "post", "ugc_
|
|
|
12
12
|
* closed. Coarse tool annotations remain mixed; native identity/schema checks
|
|
13
13
|
* still run after this call-level classification. */
|
|
14
14
|
export const PUBLISHING_STORED_READ_SELECTORS = {
|
|
15
|
+
oxygen_publishing_comments: { argument: "action", values: ["list", "get"] },
|
|
15
16
|
oxygen_publishing_notes: { argument: "action", values: ["list"] },
|
|
16
17
|
oxygen_publishing_revisions: { argument: "action", values: ["list"] },
|
|
17
18
|
oxygen_publishing_analytics: { argument: "scope", values: ["summary", "timeseries", "post", "account"] },
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const TABLE_ROW_REMOVAL_FIELDS: {
|
|
2
|
+
row_ids: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
3
|
+
filter_tree: {
|
|
4
|
+
type?: never;
|
|
5
|
+
description?: string;
|
|
6
|
+
};
|
|
7
|
+
selection: import("./schema.js").OneOfProperty;
|
|
8
|
+
};
|
|
9
|
+
export declare const TABLE_ROW_REMOVAL_CONTRACT: import("./schema.js").ContractObject<{
|
|
10
|
+
row_ids: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
11
|
+
filter_tree: {
|
|
12
|
+
type?: never;
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
selection: import("./schema.js").OneOfProperty;
|
|
16
|
+
}, never>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { arr, enumOf, int, jsonValue, obj, str } from "./schema.js";
|
|
2
|
+
const rowIds = () => arr(str({ minLength: 1 }), { minItems: 1 });
|
|
3
|
+
// Removal uses the grid's scope, not the broader execution selection. In
|
|
4
|
+
// particular, execution's `filters` array is not a serialized browser filter.
|
|
5
|
+
const selection = {
|
|
6
|
+
description: '{"mode":"row_ids","row_ids":["id"]} or {"mode":"all_matching","filter_tree":{...}}, with optional search, sort, range {offset,limit}, excluded_row_ids, crm_object. Unreadable filter nodes and filters/limit are refused. Unqualified all_matching removes every row.',
|
|
7
|
+
oneOf: [
|
|
8
|
+
obj({ mode: enumOf(["row_ids"]), row_ids: rowIds() }, { required: ["mode", "row_ids"] }),
|
|
9
|
+
obj({
|
|
10
|
+
mode: enumOf(["all_matching"]),
|
|
11
|
+
filter_tree: jsonValue({ description: "Readable filter tree, as tables query takes. Not with filter." }),
|
|
12
|
+
filter: str({ description: "Browser URL filter (f). Not with filter_tree." }),
|
|
13
|
+
search: str(),
|
|
14
|
+
sort: str(),
|
|
15
|
+
range: obj({ offset: int({ minimum: 0 }), limit: int({ minimum: 1 }) }, { required: ["offset", "limit"] }),
|
|
16
|
+
excluded_row_ids: arr(str({ minLength: 1 })),
|
|
17
|
+
crm_object: str(),
|
|
18
|
+
}, { required: ["mode"] }),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
// The readable filtered scope (OXP-1.1.12): removes exactly the rows this
|
|
22
|
+
// filter tree matches. Shorthand for an all_matching selection.
|
|
23
|
+
const filterTree = jsonValue({
|
|
24
|
+
description: 'Remove the rows this filter tree matches (as tables query takes), e.g. {"type":"group","children":[{"type":"leaf","columnKey":"status","operator":"is","value":"churned"}]} (text: is/is_not/contains; numbers: eq/gt/lt). Unreadable nodes are refused. Not with row_ids/selection.',
|
|
25
|
+
});
|
|
26
|
+
export const TABLE_ROW_REMOVAL_FIELDS = { row_ids: rowIds(), filter_tree: filterTree, selection };
|
|
27
|
+
export const TABLE_ROW_REMOVAL_CONTRACT = obj(TABLE_ROW_REMOVAL_FIELDS);
|
|
@@ -82,6 +82,7 @@ export type CostEstimatePriceSheet = {
|
|
|
82
82
|
topupStepCredits: number;
|
|
83
83
|
sending: {
|
|
84
84
|
emailPerMailboxPerDay: number;
|
|
85
|
+
/** Inclusive completed sending days, starting at 0, as in email-campaign-ramp. */
|
|
85
86
|
emailRamp: readonly {
|
|
86
87
|
throughDay: number;
|
|
87
88
|
perDay: number;
|
|
@@ -133,6 +134,11 @@ export type CostEstimateLimit = {
|
|
|
133
134
|
capacity_per_month: number;
|
|
134
135
|
/** What a newly connected sender manages in its first month (email ramp). */
|
|
135
136
|
first_month_capacity: number | null;
|
|
137
|
+
/** Sender need when all entered mailboxes start at the beginning of the ramp. */
|
|
138
|
+
first_month_senders_needed: number | null;
|
|
139
|
+
/** null when this channel has no modeled first-month capacity. */
|
|
140
|
+
first_month_hit: boolean | null;
|
|
141
|
+
/** Steady-state sender need and hit; the first-month ramp does not change these. */
|
|
136
142
|
senders_needed: number;
|
|
137
143
|
hit: boolean;
|
|
138
144
|
note: string;
|
|
@@ -115,8 +115,8 @@ function cheapestRung(credits, usdSeats, sheet) {
|
|
|
115
115
|
function rampFirstMonthCapacity(sheet) {
|
|
116
116
|
const { emailRamp, emailPerMailboxPerDay, sendingDaysPerMonth } = sheet.sending;
|
|
117
117
|
let total = 0;
|
|
118
|
-
for (let
|
|
119
|
-
const tier = emailRamp.find((step) =>
|
|
118
|
+
for (let completedDays = 0; completedDays < sendingDaysPerMonth; completedDays += 1) {
|
|
119
|
+
const tier = emailRamp.find((step) => completedDays <= step.throughDay);
|
|
120
120
|
total += tier ? Math.min(tier.perDay, emailPerMailboxPerDay) : emailPerMailboxPerDay;
|
|
121
121
|
}
|
|
122
122
|
return total;
|
|
@@ -125,11 +125,19 @@ function limitRow(input) {
|
|
|
125
125
|
const capacity = input.senders * input.perSender;
|
|
126
126
|
const sendersNeeded = input.needed === 0 || input.perSender <= 0 ? 0 : Math.ceil(input.needed / input.perSender);
|
|
127
127
|
const hit = input.needed > capacity;
|
|
128
|
-
const
|
|
128
|
+
const firstMonthCapacity = input.firstMonthPerSender === null ? null : input.senders * input.firstMonthPerSender;
|
|
129
|
+
const firstMonthSendersNeeded = input.firstMonthPerSender === null || input.firstMonthPerSender <= 0
|
|
130
|
+
? null
|
|
131
|
+
: Math.ceil(input.needed / input.firstMonthPerSender);
|
|
132
|
+
const firstMonthHit = firstMonthCapacity === null ? null : input.needed > firstMonthCapacity;
|
|
133
|
+
const steadyNote = input.needed === 0
|
|
129
134
|
? "Not used by this scenario."
|
|
130
135
|
: hit
|
|
131
136
|
? `${formatInt(input.needed)} ${input.unit} a month needs ${formatInt(sendersNeeded)}; you entered ${formatInt(input.senders)}. Each sends ${formatInt(input.perSender)} a month (${input.bindingLimit}). ${input.addWith}`
|
|
132
137
|
: `${formatInt(input.needed)} of ${formatInt(capacity)} ${input.unit} a month (${input.bindingLimit} each).`;
|
|
138
|
+
const rampNote = firstMonthHit && input.firstMonthPerSender !== null && input.firstMonthPerSender < input.perSender
|
|
139
|
+
? ` If these mailboxes are newly connected, their first-month capacity is ${formatInt(firstMonthCapacity)} ${input.unit}; ${formatInt(input.needed)} needs ${firstMonthSendersNeeded === null ? "more mailboxes" : `${formatInt(firstMonthSendersNeeded)} mailboxes`}. This quote prices the ${formatInt(input.senders)} entered mailboxes; request a new estimate after adding mailboxes.`
|
|
140
|
+
: "";
|
|
133
141
|
return {
|
|
134
142
|
key: input.key,
|
|
135
143
|
label: input.label,
|
|
@@ -138,10 +146,12 @@ function limitRow(input) {
|
|
|
138
146
|
per_sender_per_month: input.perSender,
|
|
139
147
|
binding_limit: input.bindingLimit,
|
|
140
148
|
capacity_per_month: capacity,
|
|
141
|
-
first_month_capacity:
|
|
149
|
+
first_month_capacity: firstMonthCapacity,
|
|
150
|
+
first_month_senders_needed: firstMonthSendersNeeded,
|
|
151
|
+
first_month_hit: firstMonthHit,
|
|
142
152
|
senders_needed: sendersNeeded,
|
|
143
153
|
hit,
|
|
144
|
-
note,
|
|
154
|
+
note: steadyNote + rampNote,
|
|
145
155
|
};
|
|
146
156
|
}
|
|
147
157
|
/** Clamp a scenario into the accepted bounds: whole, non-negative numbers. */
|
|
@@ -316,6 +326,8 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
316
326
|
}),
|
|
317
327
|
].filter((row) => row.needed_per_month > 0);
|
|
318
328
|
const limitsHit = limits.filter((row) => row.hit).map((row) => row.key);
|
|
329
|
+
const firstMonthRampHit = limits.find((row) => row.key === "email_per_mailbox"
|
|
330
|
+
&& row.first_month_hit && row.first_month_capacity !== null && row.first_month_capacity < row.capacity_per_month);
|
|
319
331
|
const assumptions = [
|
|
320
332
|
"Every lead is already in a table and goes through each enrichment and every sequence step. Finding new leads is priced separately.",
|
|
321
333
|
`Sending limits use ${sending.sendingDaysPerMonth} sending days a month and the defaults for a newly connected account (a sender you already run may be set lower): ${sending.emailPerMailboxPerDay} emails a mailbox a day, ${sending.linkedinInvitesPerDay} LinkedIn invites a day and ${sending.linkedinInvitesPerWeek} a week, ${sending.linkedinMessagesPerDay} LinkedIn messages a day.`,
|
|
@@ -323,9 +335,9 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
323
335
|
"While senders are paid as sending seats (sender_billing: sending_seats) they reserve no credits; once seats are retired each connected sender reserves its monthly price in credits instead, never both.",
|
|
324
336
|
`Credits are shown at face value, ${formatInt(sheet.creditsPerUsd)} credits to $1; top-ups cost ${formatUsd(sheet.topupUsdCentsPer100 / 100)} per 100 credits.`,
|
|
325
337
|
];
|
|
326
|
-
if (totalMailboxes > 0 && emails > 0) {
|
|
338
|
+
if (totalMailboxes > 0 && emails > 0 && sending.emailRamp.length > 0) {
|
|
327
339
|
const ramp = sending.emailRamp.map((step) => step.perDay).join(", ");
|
|
328
|
-
assumptions.push(`A newly connected mailbox ramps up (${ramp} a day) before it sends at its full daily limit; first_month_capacity
|
|
340
|
+
assumptions.push(`A newly connected mailbox ramps up (${ramp} a day) before it sends at its full daily limit; first_month_capacity, first_month_senders_needed and first_month_hit show that first ${sending.sendingDaysPerMonth}-sending-day window. The other capacity and hit fields describe steady state.`);
|
|
329
341
|
}
|
|
330
342
|
const planText = recommended
|
|
331
343
|
? `cheapest all in: the ${recommended.expected.label} plan${recommended.expected.topup_credits > 0 ? " with top-ups" : ""}${usdSeats > 0 ? " and seats" : ""}, ${formatUsd(recommended.expected.monthly_usd ?? 0)} a month`
|
|
@@ -337,7 +349,10 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
337
349
|
`About ${formatInt(credits)} credits a month expected (up to ${formatInt(worstCredits)} worst case)${usdSeats > 0 ? ` plus ${formatUsd(usdSeats)} a month in sending seats` : ""}; ${planText}.`,
|
|
338
350
|
limitsHit.length > 0
|
|
339
351
|
? `Limits hit: ${limits.filter((row) => row.hit).map((row) => `${row.label.toLowerCase()} (needs ${formatInt(row.senders_needed)})`).join(", ")}.`
|
|
340
|
-
: limits.length > 0 ? "No sending limit is hit." : "",
|
|
352
|
+
: limits.length > 0 ? firstMonthRampHit ? "No steady-state sending limit is hit." : "No sending limit is hit." : "",
|
|
353
|
+
firstMonthRampHit
|
|
354
|
+
? `New-mailbox ramp shortfall: ${formatInt(firstMonthRampHit.first_month_capacity)} of ${formatInt(firstMonthRampHit.needed_per_month)} emails in the first ${sending.sendingDaysPerMonth} sending days${firstMonthRampHit.first_month_senders_needed === null ? "" : ` (needs ${formatInt(firstMonthRampHit.first_month_senders_needed)} mailboxes)`}. Prices cover the entered mailboxes; request a new estimate after adding mailboxes.`
|
|
355
|
+
: "",
|
|
341
356
|
].filter(Boolean).join(" ");
|
|
342
357
|
return {
|
|
343
358
|
lines,
|
|
@@ -56,7 +56,8 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
56
56
|
f("job_start_date", "Job start date", null),
|
|
57
57
|
f("person_location", "Location", "person.location"),
|
|
58
58
|
f("person_bio", "Bio", "person.bio"),
|
|
59
|
-
|
|
59
|
+
// Blitz publishes no follower count; only the fallback lane fills it (OXP-1.27.6).
|
|
60
|
+
f("person_linkedin_followers", "LinkedIn followers (only when the fallback lane answers)", "person.linkedin_followers", "numeric"),
|
|
60
61
|
f("education", "Education", null, "jsonb"),
|
|
61
62
|
f("school_name", "School", null),
|
|
62
63
|
],
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type DeployTier } from "./deploy-env.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which background EXTERNAL EFFECTS each deployment is armed to dispatch, declared
|
|
4
|
+
* here per environment rather than implied by whichever credentials a box holds.
|
|
5
|
+
*
|
|
6
|
+
* OXP-1.6.190 (classify-A P-05). Three background families write outside OXYGEN with
|
|
7
|
+
* no human in the loop: the off-session Stripe writes of `inbox_billing_reconcile`,
|
|
8
|
+
* the Zapmail writes of `mailbox_connection_repair`, and the dedicated-egress vendor
|
|
9
|
+
* release (IPRoyal `toggle-auto-extend`) of the egress order reconcile and lifecycle.
|
|
10
|
+
* Before this table their arming was implicit: present credentials meant armed.
|
|
11
|
+
*
|
|
12
|
+
* Chosen design (coordinator, 2026-10-07, "option 2"): every CURRENT default is kept, so
|
|
13
|
+
* dev billing reconcile, Zapmail repair and egress releases keep working for dev flows,
|
|
14
|
+
* but the state is now explicit, reviewable and read back, and two mismatches fail
|
|
15
|
+
* closed outside production-data environments: a LIVE Stripe key, and a vendor release
|
|
16
|
+
* whose family is not armed here. Production stays armed; nothing here changes a
|
|
17
|
+
* production default or touches a secret. A default-off posture outside production
|
|
18
|
+
* was considered and not chosen, because other dev workflows depend on these effects.
|
|
19
|
+
*
|
|
20
|
+
* Changing an entry is a reviewed code change. `OXYGEN_WORKER_MAILBOX_CONNECTION_REPAIR`
|
|
21
|
+
* remains the existing runtime kill switch for the repair family (`false` disarms it).
|
|
22
|
+
*/
|
|
23
|
+
export declare const EXTERNAL_EFFECT_FAMILIES: readonly ["inbox_billing_stripe", "mailbox_connection_repair", "egress_vendor_release"];
|
|
24
|
+
export type ExternalEffectFamily = typeof EXTERNAL_EFFECT_FAMILIES[number];
|
|
25
|
+
export type ExternalEffectArmingState = "armed" | "disarmed";
|
|
26
|
+
/** The checked-in declaration, per deploy tier (see `resolveDeployEnv`). */
|
|
27
|
+
export declare const EXTERNAL_EFFECT_ARMING: Readonly<Record<ExternalEffectFamily, Readonly<Record<DeployTier, ExternalEffectArmingState>>>>;
|
|
28
|
+
export type ExternalEffectArming = {
|
|
29
|
+
family: ExternalEffectFamily;
|
|
30
|
+
armed: boolean;
|
|
31
|
+
declared: ExternalEffectArmingState;
|
|
32
|
+
/** `declaration` = the table above; `env` = the family's kill switch disarmed it. */
|
|
33
|
+
source: "declaration" | "env";
|
|
34
|
+
tier: DeployTier;
|
|
35
|
+
deploy_env: string;
|
|
36
|
+
production_data: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function resolveExternalEffectArming(family: ExternalEffectFamily, env?: NodeJS.ProcessEnv): ExternalEffectArming;
|
|
39
|
+
/** The readback: every family's effective gate for this process. */
|
|
40
|
+
export declare function describeExternalEffectArming(env?: NodeJS.ProcessEnv): ExternalEffectArming[];
|
|
41
|
+
/**
|
|
42
|
+
* Throw before an effect is dispatched when this process is not armed for it.
|
|
43
|
+
* Production-data environments keep their declared state; the refusal names the
|
|
44
|
+
* family and environment so a skipped effect is never silent.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertExternalEffectArmed(family: ExternalEffectFamily, env?: NodeJS.ProcessEnv): ExternalEffectArming;
|
|
47
|
+
/** Live-mode Stripe secret or restricted key. Test keys (`sk_test_`/`rk_test_`) never match. */
|
|
48
|
+
export declare function isLiveStripeKey(key: string | null | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Fail closed when a process that does NOT hold production data is about to write to
|
|
51
|
+
* Stripe with a LIVE key: that is a dev or local box pointed at real money. The key is
|
|
52
|
+
* never echoed; only its mode is named.
|
|
53
|
+
*/
|
|
54
|
+
export declare function assertStripeKeyMatchesEnvironment(key: string | null | undefined, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
import { isProductionData, resolveDeployEnv } from "./deploy-env.js";
|
|
3
|
+
/**
|
|
4
|
+
* Which background EXTERNAL EFFECTS each deployment is armed to dispatch, declared
|
|
5
|
+
* here per environment rather than implied by whichever credentials a box holds.
|
|
6
|
+
*
|
|
7
|
+
* OXP-1.6.190 (classify-A P-05). Three background families write outside OXYGEN with
|
|
8
|
+
* no human in the loop: the off-session Stripe writes of `inbox_billing_reconcile`,
|
|
9
|
+
* the Zapmail writes of `mailbox_connection_repair`, and the dedicated-egress vendor
|
|
10
|
+
* release (IPRoyal `toggle-auto-extend`) of the egress order reconcile and lifecycle.
|
|
11
|
+
* Before this table their arming was implicit: present credentials meant armed.
|
|
12
|
+
*
|
|
13
|
+
* Chosen design (coordinator, 2026-10-07, "option 2"): every CURRENT default is kept, so
|
|
14
|
+
* dev billing reconcile, Zapmail repair and egress releases keep working for dev flows,
|
|
15
|
+
* but the state is now explicit, reviewable and read back, and two mismatches fail
|
|
16
|
+
* closed outside production-data environments: a LIVE Stripe key, and a vendor release
|
|
17
|
+
* whose family is not armed here. Production stays armed; nothing here changes a
|
|
18
|
+
* production default or touches a secret. A default-off posture outside production
|
|
19
|
+
* was considered and not chosen, because other dev workflows depend on these effects.
|
|
20
|
+
*
|
|
21
|
+
* Changing an entry is a reviewed code change. `OXYGEN_WORKER_MAILBOX_CONNECTION_REPAIR`
|
|
22
|
+
* remains the existing runtime kill switch for the repair family (`false` disarms it).
|
|
23
|
+
*/
|
|
24
|
+
export const EXTERNAL_EFFECT_FAMILIES = [
|
|
25
|
+
"inbox_billing_stripe",
|
|
26
|
+
"mailbox_connection_repair",
|
|
27
|
+
"egress_vendor_release",
|
|
28
|
+
];
|
|
29
|
+
/** The checked-in declaration, per deploy tier (see `resolveDeployEnv`). */
|
|
30
|
+
export const EXTERNAL_EFFECT_ARMING = {
|
|
31
|
+
inbox_billing_stripe: { prod: "armed", shadow: "armed", dev: "armed", local: "armed" },
|
|
32
|
+
mailbox_connection_repair: { prod: "armed", shadow: "armed", dev: "armed", local: "armed" },
|
|
33
|
+
egress_vendor_release: { prod: "armed", shadow: "armed", dev: "armed", local: "armed" },
|
|
34
|
+
};
|
|
35
|
+
/** Per-family runtime kill switch: an explicit false disarms, nothing arms beyond the declaration. */
|
|
36
|
+
const KILL_SWITCH_ENV = {
|
|
37
|
+
mailbox_connection_repair: "OXYGEN_WORKER_MAILBOX_CONNECTION_REPAIR",
|
|
38
|
+
};
|
|
39
|
+
function killSwitchDisarms(family, env) {
|
|
40
|
+
const name = KILL_SWITCH_ENV[family];
|
|
41
|
+
const raw = name ? env[name]?.trim().toLowerCase() : undefined;
|
|
42
|
+
// Exactly the worker's readBooleanEnv falsy set, so the readback and the gate agree.
|
|
43
|
+
return raw === "false" || raw === "0" || raw === "no";
|
|
44
|
+
}
|
|
45
|
+
export function resolveExternalEffectArming(family, env = process.env) {
|
|
46
|
+
const deploy = resolveDeployEnv(env);
|
|
47
|
+
const declared = EXTERNAL_EFFECT_ARMING[family][deploy.tier];
|
|
48
|
+
const disarmedByEnv = declared === "armed" && killSwitchDisarms(family, env);
|
|
49
|
+
return {
|
|
50
|
+
family,
|
|
51
|
+
armed: declared === "armed" && !disarmedByEnv,
|
|
52
|
+
declared,
|
|
53
|
+
source: disarmedByEnv ? "env" : "declaration",
|
|
54
|
+
tier: deploy.tier,
|
|
55
|
+
deploy_env: deploy.label,
|
|
56
|
+
production_data: isProductionData(env),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The readback: every family's effective gate for this process. */
|
|
60
|
+
export function describeExternalEffectArming(env = process.env) {
|
|
61
|
+
return EXTERNAL_EFFECT_FAMILIES.map((family) => resolveExternalEffectArming(family, env));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Throw before an effect is dispatched when this process is not armed for it.
|
|
65
|
+
* Production-data environments keep their declared state; the refusal names the
|
|
66
|
+
* family and environment so a skipped effect is never silent.
|
|
67
|
+
*/
|
|
68
|
+
export function assertExternalEffectArmed(family, env = process.env) {
|
|
69
|
+
const arming = resolveExternalEffectArming(family, env);
|
|
70
|
+
if (!arming.armed) {
|
|
71
|
+
throw new OxygenError("external_effect_not_armed", `${family} is not armed in ${arming.deploy_env} (${arming.source === "env" ? "disarmed by its kill switch" : "declared disarmed"}), so this external effect was not dispatched.`, { details: { family, deploy_env: arming.deploy_env, source: arming.source }, exitCode: 1 });
|
|
72
|
+
}
|
|
73
|
+
return arming;
|
|
74
|
+
}
|
|
75
|
+
/** Live-mode Stripe secret or restricted key. Test keys (`sk_test_`/`rk_test_`) never match. */
|
|
76
|
+
export function isLiveStripeKey(key) {
|
|
77
|
+
const value = key?.trim() ?? "";
|
|
78
|
+
return value.startsWith("sk_live_") || value.startsWith("rk_live_");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fail closed when a process that does NOT hold production data is about to write to
|
|
82
|
+
* Stripe with a LIVE key: that is a dev or local box pointed at real money. The key is
|
|
83
|
+
* never echoed; only its mode is named.
|
|
84
|
+
*/
|
|
85
|
+
export function assertStripeKeyMatchesEnvironment(key, env = process.env) {
|
|
86
|
+
if (!isLiveStripeKey(key) || isProductionData(env))
|
|
87
|
+
return;
|
|
88
|
+
const deploy = resolveDeployEnv(env);
|
|
89
|
+
throw new OxygenError("external_effect_environment_mismatch", `A live-mode Stripe key is configured in ${deploy.label}, which does not hold production data; refusing the Stripe write. Use a test-mode key outside production.`, { details: { family: "inbox_billing_stripe", deploy_env: deploy.label, key_mode: "live" }, exitCode: 1 });
|
|
90
|
+
}
|