@oxygen-agent/cli 1.1013.61 → 1.1013.93
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/index.js +178 -40
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +5 -4
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +44 -27
- package/node_modules/@oxygen/shared/dist/staff.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/staff.js +12 -0
- package/node_modules/@oxygen/shared/dist/table-field-types.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/table-field-types.js +126 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -76,6 +76,7 @@ import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/pla
|
|
|
76
76
|
import { AI_COLUMN_CREDITS, AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS } from "@oxygen/shared/pricing-sheet";
|
|
77
77
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
78
78
|
import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
|
|
79
|
+
import { lookupColumnDataType } from "@oxygen/shared/table-field-types";
|
|
79
80
|
import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
|
|
80
81
|
import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
|
|
81
82
|
import { Command, CommanderError, Option } from "commander";
|
|
@@ -7016,7 +7017,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7016
7017
|
.addCommand(new Command("create")
|
|
7017
7018
|
.description("Create a table private to the active workspace. Free — 0 Oxygen credits. To create a table and import a file in one step, use `tables import --create <name>`.")
|
|
7018
7019
|
.argument("<name>", "Display name for the table.")
|
|
7019
|
-
.requiredOption("--columns-json <json>", 'Required nonempty JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
7020
|
+
.requiredOption("--columns-json <json>", 'Required nonempty JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. A column may name a field type instead, as `columns add --type` does: {"label":"Stage","type":"select","options":["Lead","Won"]} (text, number, checkbox, date, json, markdown, image, select, multi_select, status). Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
7020
7021
|
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
7021
7022
|
.option("--json", "Print a JSON envelope.")
|
|
7022
7023
|
.action(async (name, options) => {
|
|
@@ -7593,33 +7594,44 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7593
7594
|
});
|
|
7594
7595
|
});
|
|
7595
7596
|
tablesCommand.addCommand(new Command("relate")
|
|
7596
|
-
.description("
|
|
7597
|
+
.description("Link two tables (the web's \"Link to another table\"): creates an empty linked-record column on BOTH tables, so links show and can be removed from either side; matching values are not required. `oxygen tables relate contacts --to accounts --live` names them from the tables (account on contacts, contacts on accounts) and re-running it finds the same link. Then use `oxygen tables link` to populate row-to-row edges. Works on any workspace table; plain tables stay plain and are never registered as CRM objects. Defaults to dry-run.")
|
|
7597
7598
|
.argument("<table>", "Source table id or slug.")
|
|
7598
|
-
.argument("
|
|
7599
|
-
.
|
|
7600
|
-
.option("--
|
|
7599
|
+
.argument("[slug]", "Relation slug — also the relation column key on the source, such as client. Omit it to name the relation from the target table (or reuse the one relation that already links them).")
|
|
7600
|
+
.option("--to <table>", "Table to link to: id or slug. May also be a CRM object table (companies, people, deals).")
|
|
7601
|
+
.option("--target-table <table>", "Same as --to.")
|
|
7602
|
+
.option("--display-name <name>", "Display name for the relation column. Defaults to the target table's name, or the slug in Title Case when you name the slug.")
|
|
7601
7603
|
.option("--cardinality <cardinality>", "one_to_one, one_to_many, many_to_one, or many_to_many (source:target). Default many_to_many. one_to_many means each source row links many target rows and each target row links back to one source.")
|
|
7602
|
-
.option("--inverse-slug <slug>", "
|
|
7604
|
+
.option("--inverse-slug <slug>", "Key of the linked-record column on the target table. Defaults to a name from the source table; a standard CRM object target (companies, people, deals) gets no column there.")
|
|
7603
7605
|
.option("--inverse-display-name <name>", "Display name for the inverse side.")
|
|
7604
7606
|
.option("--dry-run", "Preview the relation plan without writing.")
|
|
7605
7607
|
.option("--live", "Apply the relation. Default is dry-run.")
|
|
7606
7608
|
.option("--json", "Print a JSON envelope.")
|
|
7607
7609
|
.action(async (table, slug, options) => {
|
|
7608
|
-
await handleAsyncAction("tables relate", options, () =>
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7610
|
+
await handleAsyncAction("tables relate", options, () => {
|
|
7611
|
+
const to = readOption(options.to);
|
|
7612
|
+
const targetTable = readOption(options.targetTable);
|
|
7613
|
+
if (to && targetTable && to !== targetTable) {
|
|
7614
|
+
throw new OxygenError("invalid_request", "--to and --target-table name different tables; pass one.", { exitCode: 1 });
|
|
7615
|
+
}
|
|
7616
|
+
if (!to && !targetTable) {
|
|
7617
|
+
throw new OxygenError("invalid_request", "Name the table to link to with --to <table>.", { exitCode: 1 });
|
|
7618
|
+
}
|
|
7619
|
+
return requestOxygen("/api/cli/tables/relations", {
|
|
7620
|
+
method: "POST",
|
|
7621
|
+
body: {
|
|
7622
|
+
table,
|
|
7623
|
+
relationship: {
|
|
7624
|
+
...(readOption(slug) ? { slug: readOption(slug) } : {}),
|
|
7625
|
+
target_table: to ?? targetTable,
|
|
7626
|
+
...(options.displayName ? { display_name: options.displayName } : {}),
|
|
7627
|
+
...(options.cardinality ? { cardinality: options.cardinality } : {}),
|
|
7628
|
+
...(options.inverseSlug ? { inverse_slug: options.inverseSlug } : {}),
|
|
7629
|
+
...(options.inverseDisplayName ? { inverse_display_name: options.inverseDisplayName } : {}),
|
|
7630
|
+
},
|
|
7631
|
+
mode: options.live ? "live" : "dry_run",
|
|
7619
7632
|
},
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
}));
|
|
7633
|
+
});
|
|
7634
|
+
});
|
|
7623
7635
|
}))
|
|
7624
7636
|
.addCommand(new Command("relations")
|
|
7625
7637
|
.description("Inspect and manage native Tables-owned relation definitions.")
|
|
@@ -9803,7 +9815,7 @@ Examples:
|
|
|
9803
9815
|
.command("columns")
|
|
9804
9816
|
.description("Workspace table column commands. Price first: `tools search --for-table <table>` lists every enrichment this table's columns support with its credit cost per row (0 credits). Ready-made templates (pricing pages, job posts, event pages, 10-Ks, website facts, market questions, text extraction, plus free 0-credit cleanups — company names, job titles, email type, text to list; person questions over an enriched LinkedIn profile and person appearances; company research questions — founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ and footprint, LinkedIn/Crunchbase/careers page lookups): `columns catalog`, then `columns add <table> --prompt-key <key> --input <name>=<column>`. Column choice guide: the oxygen-tables skill (`oxygen skills get oxygen-tables --json`); enrichment presets and waterfalls: the oxygen-enrichment skill. With --json parse stdout only: the envelope is stdout, and the spend and cost notes (`estimated N credits for a live run`) are stderr, so merging the streams breaks the JSON.")
|
|
9805
9817
|
.addCommand(new Command("add")
|
|
9806
|
-
.description("Add a nullable column, a catalog template (--prompt-key) or a multi-column enrichment bundle (--preset) to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately.")
|
|
9818
|
+
.description("Add a nullable column, a catalog template (--prompt-key) or a multi-column enrichment bundle (--preset) to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately. A plain field is --type: text, number, checkbox, date, json, markdown, image, or a choice column — select, multi_select or status, with --options \"Lead,Contacted,Won\". To link another table row to row, use `oxygen tables relate <table> --to <other-table> --live`; to read a value out of one, --lookup-table.")
|
|
9807
9819
|
.argument("<table>", "Table id or slug.")
|
|
9808
9820
|
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, the LinkedIn Scraper only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs, followers only on rows the LinkedIn Scraper answers because Blitz publishes none; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, the LinkedIn Scraper only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — or `domain_check` (1 credit/row for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
9809
9821
|
.option("--input <slot=column...>", "Bind a preset or template input to an exact column, e.g. --input url=pricing_url with --prompt-key, or --input domain=website --input linkedin_url=company_li with --preset. Repeatable. For a preset it is only needed when the automatic match is wrong or missing; for a template it names the column each declared input reads.", collectRepeatable, [])
|
|
@@ -9829,7 +9841,11 @@ Examples:
|
|
|
9829
9841
|
.option("--research-mode <mode>", "Research columns: how tightly the answer is bound to the sources. Default: summarize and combine what they say. strict: answer only from what a source states verbatim (for figures and identifiers). estimate: reason to a figure from the evidence (for revenue or headcount).")
|
|
9830
9842
|
.option("--research-results <n>", "Research columns: how many search results to ground each row on (1-25, default 5). More evidence costs no extra search, but a larger prompt.")
|
|
9831
9843
|
.option("--research-engine <engine>", "Research columns: pin the search provider (serper or exa), or with --research-url the page-fetch provider (firecrawl or exa). Defaults to serper for search and firecrawl for fetch, falling back automatically. Most users should not set this.")
|
|
9832
|
-
.option("--
|
|
9844
|
+
.option("--type <type>", "Field type, the web picker's names: text, number, checkbox, date, json, markdown, image, select, multi_select or status. Sets kind manual with the matching data and semantic type (multi_select stores jsonb); text/number/checkbox/date/json also work as a data type beside --kind.")
|
|
9845
|
+
.option("--options <choices>", "Choices for a select, multi_select or status column, comma-separated, in display order, e.g. \"Lead,Contacted,Replied,Won\". Each value is what a cell stores; colors are assigned. Use --options-json for labels or colors.")
|
|
9846
|
+
.option("--options-json <json>", "Choices as a JSON array of strings or {value,label?,color?} objects (colors: gray, red, orange, amber, yellow, green, teal, blue, indigo, purple, pink). value is what a cell stores; label is what the grid shows.")
|
|
9847
|
+
.option("--description <text>", "A note on the column, shown in its header and the column editor.")
|
|
9848
|
+
.option("--semantic-type <type>", "Optional semantic type: a display hint such as company.domain or person.email, or a choice type (select, multi_select, status) — --type sets those for you.")
|
|
9833
9849
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata. Required for --kind formula as a flat object: {\"expression\":\"trim(company_name)\"}. Check the expression with `formulas validate` first.")
|
|
9834
9850
|
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai; pair it with --kind research to make it a Web Research Agent that searches the web per row. If the prompt names its output sections — a line reading 'Return the following sections:' followed by 'Score: ...', 'Reasoning: ...' — the column answers in exactly that shape and each section becomes a referenceable sub-column; otherwise it answers in plain text. Use --no-structured-output to keep it plain text either way. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
|
|
9835
9851
|
.option("--prompt-key <key>", "Add a column template from `oxygen columns catalog` (e.g. pricing_plan_summary_v1 reads a pricing-page URL column and returns plan count, price range and enterprise plan; company_founders_v1 reads a domain column and returns the founders with sources; person_skill_set_v1 reads the person_enrich payload column with --input profile=person_enrich). Materializes the template's prompt, output schema and grounding and sets its kind (ai, research, formula, or search — a managed web search whose cell is a page URL). Bind each declared input with --input <name>=<column_key>.")
|
|
@@ -9894,8 +9910,8 @@ Examples:
|
|
|
9894
9910
|
// A preset names its own columns, so --label does not apply to it.
|
|
9895
9911
|
const preset = readOption(options.preset);
|
|
9896
9912
|
if (preset) {
|
|
9897
|
-
if (options.label || options.promptKey || options.definitionJson) {
|
|
9898
|
-
throw new OxygenError("invalid_request", "--preset builds its own column set. Drop --label/--prompt-key/--definition-json, or add the column by hand without --preset.", { exitCode: 1 });
|
|
9913
|
+
if (options.label || options.promptKey || options.definitionJson || options.type || options.options || options.optionsJson) {
|
|
9914
|
+
throw new OxygenError("invalid_request", "--preset builds its own column set. Drop --label/--prompt-key/--definition-json/--type/--options, or add the column by hand without --preset.", { exitCode: 1 });
|
|
9899
9915
|
}
|
|
9900
9916
|
// Named only when passed: the classification flags are hidden from help.
|
|
9901
9917
|
const classificationFlag = options.classify ? "--classify" : options.yesNo ? "--yes-no" : options.score ? "--score" : null;
|
|
@@ -9966,8 +9982,22 @@ Examples:
|
|
|
9966
9982
|
column.kind = options.kind;
|
|
9967
9983
|
if (options.semanticType)
|
|
9968
9984
|
column.semantic_type = options.semanticType;
|
|
9969
|
-
|
|
9970
|
-
|
|
9985
|
+
// --description joins --definition-json in one assignment: a second
|
|
9986
|
+
// self-spread of column.definition multiplies the parity scanner's work.
|
|
9987
|
+
const description = readOption(options.description);
|
|
9988
|
+
if (options.definitionJson || description) {
|
|
9989
|
+
column.definition = {
|
|
9990
|
+
...(options.definitionJson ? parseJsonObject(options.definitionJson) : {}),
|
|
9991
|
+
...(description ? { description } : {}),
|
|
9992
|
+
};
|
|
9993
|
+
}
|
|
9994
|
+
// A field type and its choices travel as themselves; the server
|
|
9995
|
+
// resolves them exactly as the web picker's rows do.
|
|
9996
|
+
if (readOption(options.type))
|
|
9997
|
+
column.type = readOption(options.type);
|
|
9998
|
+
const choices = readColumnOptionsFlags(options);
|
|
9999
|
+
if (choices)
|
|
10000
|
+
column.options = choices;
|
|
9971
10001
|
const requestedKind = readOption(options.kind)?.toLowerCase() ?? null;
|
|
9972
10002
|
const isResearch = requestedKind === "research";
|
|
9973
10003
|
if (capability)
|
|
@@ -10058,15 +10088,11 @@ Examples:
|
|
|
10058
10088
|
const lookupDefinition = applyLookupColumnConfig(definition, options);
|
|
10059
10089
|
column.definition = lookupDefinition;
|
|
10060
10090
|
// Derive the output data type from the mode so callers don't have to know
|
|
10061
|
-
// it (the bind lesson): count/aggregate ->
|
|
10062
|
-
//
|
|
10091
|
+
// it (the bind lesson), with the web editor's rule: count/aggregate ->
|
|
10092
|
+
// numeric, exists -> boolean, first_match -> text for one return column,
|
|
10093
|
+
// jsonb for several. Sent explicitly so an older server agrees too.
|
|
10063
10094
|
if (!options.dataType) {
|
|
10064
|
-
|
|
10065
|
-
column.data_type = mode === "count" || mode === "aggregate"
|
|
10066
|
-
? "numeric"
|
|
10067
|
-
: mode === "exists"
|
|
10068
|
-
? "boolean"
|
|
10069
|
-
: "jsonb";
|
|
10095
|
+
column.data_type = lookupColumnDataType(lookupDefinition.mode ?? "first_match", lookupDefinition.returnColumns);
|
|
10070
10096
|
}
|
|
10071
10097
|
}
|
|
10072
10098
|
const body = { table, column };
|
|
@@ -10268,12 +10294,26 @@ Examples:
|
|
|
10268
10294
|
});
|
|
10269
10295
|
}))
|
|
10270
10296
|
.addCommand(new Command("update")
|
|
10271
|
-
.description("Update a workspace column: label and visibility, and for AI/research columns the prompt, named inputs, model, reasoning level, run condition, output schema, and web-research settings.")
|
|
10297
|
+
.description("Update a workspace column: label, note, position and visibility; a choice column's options; a lookup or bind column's source, join and mode; and for AI/research columns the prompt, named inputs, model, reasoning level, run condition, output schema, and web-research settings.")
|
|
10272
10298
|
.argument("<table>", "Table id or slug.")
|
|
10273
10299
|
.argument("<column>", "Column id or key.")
|
|
10274
10300
|
.option("--label <label>", "New display label.")
|
|
10275
|
-
.option("--semantic-type <type>", "New semantic type.")
|
|
10301
|
+
.option("--semantic-type <type>", "New semantic type: a display hint such as company.domain, or a choice type (select, multi_select, status) to give a column options.")
|
|
10276
10302
|
.option("--clear-semantic-type", "Clear the column's semantic type.")
|
|
10303
|
+
.option("--options <choices>", "Replace a select, multi_select or status column's choices, comma-separated in display order. Cells keep the values they hold; keep a value in the list to keep it selectable.")
|
|
10304
|
+
.option("--options-json <json>", "Replacement choices as a JSON array of strings or {value,label?,color?}. Rename a choice by giving the same value a new label.")
|
|
10305
|
+
.option("--description <text>", "Replace the column's note.")
|
|
10306
|
+
.option("--lookup-table <table>", "Lookup columns: read from this source table instead; pass --lookup-match with it, since the join column belongs to the source.")
|
|
10307
|
+
.option("--lookup-match <pair>", "Lookup columns: new join as localColumn=sourceColumn.")
|
|
10308
|
+
.option("--lookup-normalize <mode>", "Lookup columns: key normalization exact, lower-trim, email, domain, or linkedin.")
|
|
10309
|
+
.option("--lookup-mode <mode>", "Lookup columns: first-match, count, exists, or aggregate. The column's data type follows the mode.")
|
|
10310
|
+
.option("--lookup-return <csv>", "Lookup columns: the source columns first-match returns.")
|
|
10311
|
+
.option("--lookup-order <column:dir>", "Lookup columns: first-match tie-break, e.g. created_at:desc.")
|
|
10312
|
+
.option("--lookup-aggregate <fn:column>", "Lookup columns: aggregate reducer as fn:column (sum, avg, min or max).")
|
|
10313
|
+
.option("--bind-object <slug>", "Bind columns: resolve rows to this CRM object instead (companies, people, deals, ...).")
|
|
10314
|
+
.option("--bind-map <pairs>", "Bind columns: identity=column pairs to add or change, e.g. domain=website.")
|
|
10315
|
+
.option("--bind-create", "Bind columns: create a CRM record for unmatched rows (a truth write; runs need --approved).")
|
|
10316
|
+
.option("--no-bind-create", "Bind columns: leave unmatched rows unbound.")
|
|
10277
10317
|
.option("--position <n>", "Move the column to this zero-based position.")
|
|
10278
10318
|
.option("--definition-json <json>", "Definition metadata to shallow-merge into the column; arrays are replaced wholesale and missing keys leave existing fields unchanged.")
|
|
10279
10319
|
.option("--definition-unset <keys>", "Comma-separated definition keys to REMOVE after the merge (e.g. outputSchema,model) — a merge can only add or overwrite. Lifecycle and provenance keys are rejected.")
|
|
@@ -10341,10 +10381,33 @@ Examples:
|
|
|
10341
10381
|
// the server SHALLOW-merges `definition`, so a nested inputMapping
|
|
10342
10382
|
// sent through --definition-json adds keys and can never remove one.
|
|
10343
10383
|
// This replaces the object wholesale.
|
|
10384
|
+
// Every flag below adds whole keys, merged into `definition` ONCE. The
|
|
10385
|
+
// surface-parity scanner re-reads each assignment that spreads a
|
|
10386
|
+
// variable into itself, so stacking `definition = { ...definition }`
|
|
10387
|
+
// assignments multiplied its work 13x and timed the gate out (#5086).
|
|
10388
|
+
const additions = {};
|
|
10344
10389
|
const inputMapping = readOption(options.inputMapping);
|
|
10345
|
-
if (inputMapping)
|
|
10346
|
-
|
|
10390
|
+
if (inputMapping)
|
|
10391
|
+
additions.inputMapping = parseJsonObject(inputMapping);
|
|
10392
|
+
const description = readOption(options.description);
|
|
10393
|
+
if (description)
|
|
10394
|
+
additions.description = description;
|
|
10395
|
+
// Lookup and bind flags edit the STORED configuration: the server merges
|
|
10396
|
+
// `definition` one level deep, so a partial `match` would replace the
|
|
10397
|
+
// whole join. Read the column, apply the flags over it, send the result.
|
|
10398
|
+
const editsLookup = hasLookupColumnFlags(options);
|
|
10399
|
+
const editsBind = Boolean(readOption(options.bindObject) || readOption(options.bindMap))
|
|
10400
|
+
|| typeof options.bindCreate === "boolean";
|
|
10401
|
+
if (editsLookup || editsBind) {
|
|
10402
|
+
const stored = await readStoredColumnForUpdate(table, column);
|
|
10403
|
+
if (editsLookup)
|
|
10404
|
+
Object.assign(additions, lookupDefinitionEdit(stored, options));
|
|
10405
|
+
if (editsBind)
|
|
10406
|
+
Object.assign(additions, bindDefinitionEdit(stored, options));
|
|
10347
10407
|
}
|
|
10408
|
+
if (Object.keys(additions).length > 0)
|
|
10409
|
+
definition = { ...(definition ?? {}), ...additions };
|
|
10410
|
+
const choices = readColumnOptionsFlags(options);
|
|
10348
10411
|
await assertColumnDefinitionReferences(table, definition);
|
|
10349
10412
|
// Sent beside `definition`, never inside it: the server merges the
|
|
10350
10413
|
// depth into the column's STORED search block, so it cannot drop a
|
|
@@ -10355,6 +10418,7 @@ Examples:
|
|
|
10355
10418
|
body: {
|
|
10356
10419
|
table,
|
|
10357
10420
|
column,
|
|
10421
|
+
...(choices ? { options: choices } : {}),
|
|
10358
10422
|
...(researchDepth ? { research_depth: researchDepth } : {}),
|
|
10359
10423
|
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
10360
10424
|
...(options.clearSemanticType ? { semantic_type: null } : readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
|
|
@@ -10379,7 +10443,7 @@ Examples:
|
|
|
10379
10443
|
.argument("<table>", "Table id or slug.")
|
|
10380
10444
|
.argument("<column>", "Column id or key.")
|
|
10381
10445
|
.requiredOption("--to <target>", "agent (Web Research Agent), ai (AI column, no web access) or keyword (Keyword search).")
|
|
10382
|
-
.option("--prompt <text>", "Keyword search to agent only: the question the agent answers for each row.")
|
|
10446
|
+
.option("--prompt <text>", "Keyword search to agent only: the question the agent answers for each row. Defaults to the column's saved search, as the web's mode switch does.")
|
|
10383
10447
|
.option("--yes", "Convert, clearing the column's current results.")
|
|
10384
10448
|
.option("--json", "Print a JSON envelope.")
|
|
10385
10449
|
.action(async (table, column, options) => {
|
|
@@ -10409,7 +10473,7 @@ Examples:
|
|
|
10409
10473
|
.description("Convert a manual text column to a stronger data type, migrating stored values.")
|
|
10410
10474
|
.argument("<table>", "Table id or slug.")
|
|
10411
10475
|
.argument("<column>", "Column id or key.")
|
|
10412
|
-
.requiredOption("--data-type <type>", "Target data type: text, numeric, boolean, or timestamptz.")
|
|
10476
|
+
.requiredOption("--data-type <type>", "Target data type: text, numeric, boolean, jsonb, or timestamptz (the web's Data type choices).")
|
|
10413
10477
|
.option("--dry-run", "Preview the conversion (row counts and samples) without writing.")
|
|
10414
10478
|
.option("--json", "Print a JSON envelope.")
|
|
10415
10479
|
.action(async (table, column, options) => {
|
|
@@ -21092,10 +21156,20 @@ async function uploadCopilotAttachment(sessionId, filePath) {
|
|
|
21092
21156
|
}
|
|
21093
21157
|
const controller = new AbortController();
|
|
21094
21158
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
21159
|
+
// Content-Type is bound into the storage signature. Older tickets omit the
|
|
21160
|
+
// SDK's binary default; newer tickets supply every required upload header.
|
|
21161
|
+
const uploadHeaders = { "content-type": "application/octet-stream" };
|
|
21162
|
+
if (isRecord(upload.headers)) {
|
|
21163
|
+
for (const [name, value] of Object.entries(upload.headers)) {
|
|
21164
|
+
if (typeof value === "string")
|
|
21165
|
+
uploadHeaders[name] = value;
|
|
21166
|
+
}
|
|
21167
|
+
}
|
|
21095
21168
|
let putResponse;
|
|
21096
21169
|
try {
|
|
21097
21170
|
putResponse = await fetch(uploadUrl, {
|
|
21098
21171
|
method: "PUT",
|
|
21172
|
+
headers: uploadHeaders,
|
|
21099
21173
|
body: new Uint8Array(fileBuffer),
|
|
21100
21174
|
signal: controller.signal,
|
|
21101
21175
|
});
|
|
@@ -22110,6 +22184,70 @@ function applyLookupColumnConfig(definition, options) {
|
|
|
22110
22184
|
definition.aggregate = aggregate;
|
|
22111
22185
|
return definition;
|
|
22112
22186
|
}
|
|
22187
|
+
// `--options a,b,c` or `--options-json '[...]'`: a choice column's option list in
|
|
22188
|
+
// the shape the server's `options` field takes (strings or {value,label,color}).
|
|
22189
|
+
function readColumnOptionsFlags(options) {
|
|
22190
|
+
const csv = readOption(options.options);
|
|
22191
|
+
const json = readOption(options.optionsJson);
|
|
22192
|
+
if (csv && json) {
|
|
22193
|
+
throw new OxygenError("invalid_request", "Pass --options or --options-json, not both.", { exitCode: 1 });
|
|
22194
|
+
}
|
|
22195
|
+
if (json)
|
|
22196
|
+
return parseJsonArray(json);
|
|
22197
|
+
if (!csv)
|
|
22198
|
+
return null;
|
|
22199
|
+
return csv.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
22200
|
+
}
|
|
22201
|
+
function hasLookupColumnFlags(options) {
|
|
22202
|
+
return Boolean(readOption(options.lookupTable)
|
|
22203
|
+
|| readOption(options.lookupMatch)
|
|
22204
|
+
|| readOption(options.lookupNormalize)
|
|
22205
|
+
|| readOption(options.lookupMode)
|
|
22206
|
+
|| readOption(options.lookupReturn)
|
|
22207
|
+
|| readOption(options.lookupOrder)
|
|
22208
|
+
|| readOption(options.lookupAggregate));
|
|
22209
|
+
}
|
|
22210
|
+
async function readStoredColumnForUpdate(table, column) {
|
|
22211
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table, include_archived: true } });
|
|
22212
|
+
const stored = (describe.columns ?? []).find((entry) => entry.key === column || entry.id === column);
|
|
22213
|
+
if (!stored) {
|
|
22214
|
+
throw new OxygenError("column_not_found", `Column ${column} was not found on ${table}.`, {
|
|
22215
|
+
details: { table, column },
|
|
22216
|
+
exitCode: 1,
|
|
22217
|
+
});
|
|
22218
|
+
}
|
|
22219
|
+
return stored;
|
|
22220
|
+
}
|
|
22221
|
+
function storedDefinitionCopy(stored) {
|
|
22222
|
+
return isRecord(stored.definition) ? structuredClone(stored.definition) : {};
|
|
22223
|
+
}
|
|
22224
|
+
function pickDefinitionKeys(definition, keys) {
|
|
22225
|
+
return Object.fromEntries(keys.filter((key) => definition[key] !== undefined).map((key) => [key, definition[key]]));
|
|
22226
|
+
}
|
|
22227
|
+
// `columns update --lookup-*`: the same flags as `columns add`, applied over the
|
|
22228
|
+
// stored lookup so a partial edit (only the normalization, only the mode) keeps
|
|
22229
|
+
// the rest of the join. A new source table needs its own join column.
|
|
22230
|
+
function lookupDefinitionEdit(stored, options) {
|
|
22231
|
+
if (stored.kind !== "lookup") {
|
|
22232
|
+
throw new OxygenError("invalid_request", `--lookup-* flags edit a lookup column, and ${stored.key} is a ${stored.kind ?? "plain"} column.`, { details: { column: stored.key, kind: stored.kind ?? null }, exitCode: 1 });
|
|
22233
|
+
}
|
|
22234
|
+
if (readOption(options.lookupTable) && !readOption(options.lookupMatch)) {
|
|
22235
|
+
throw new OxygenError("invalid_request", "--lookup-table changes the source table, so pass --lookup-match localColumn=sourceColumn with it: the join column belongs to the source table.", { exitCode: 1 });
|
|
22236
|
+
}
|
|
22237
|
+
const next = applyLookupColumnConfig(storedDefinitionCopy(stored), options);
|
|
22238
|
+
return pickDefinitionKeys(next, ["version", "sourceTable", "match", "inputMapping", "mode", "returnColumns", "orderBy", "aggregate"]);
|
|
22239
|
+
}
|
|
22240
|
+
// `columns update --bind-*`: applied over the stored binding; --bind-map adds or
|
|
22241
|
+
// changes identities and keeps the others (`--input-mapping` replaces them all).
|
|
22242
|
+
function bindDefinitionEdit(stored, options) {
|
|
22243
|
+
if (stored.kind !== "bind") {
|
|
22244
|
+
throw new OxygenError("invalid_request", `--bind-* flags edit a bind column, and ${stored.key} is a ${stored.kind ?? "plain"} column.`, { details: { column: stored.key, kind: stored.kind ?? null }, exitCode: 1 });
|
|
22245
|
+
}
|
|
22246
|
+
const next = applyBindColumnConfig(storedDefinitionCopy(stored), options);
|
|
22247
|
+
if (options.bindCreate === false)
|
|
22248
|
+
next.onNoMatch = "none";
|
|
22249
|
+
return pickDefinitionKeys(next, ["version", "object", "inputMapping", "onNoMatch"]);
|
|
22250
|
+
}
|
|
22113
22251
|
// Parse a `tables promote --map` value into the wire mappings [{column, attribute}].
|
|
22114
22252
|
// Accepts the CSV pair form `column=attribute,column2=attribute2` (LHS is the working
|
|
22115
22253
|
// column, RHS the CRM attribute — the reverse of --bind-map's identity=column), a
|
|
@@ -1082,6 +1082,9 @@ function routeCardFor(card, query) {
|
|
|
1082
1082
|
const maintenance = card.primitive === "tables" ? tableMaintenanceRecommendation(query) : null;
|
|
1083
1083
|
if (maintenance)
|
|
1084
1084
|
card = { ...card, execution: maintenance.execution };
|
|
1085
|
+
const structure = card.primitive === "tables" && !maintenance ? tableStructureRecommendation(query) : null;
|
|
1086
|
+
if (structure)
|
|
1087
|
+
card = { ...card, posture: "workspace_write", execution: structure.execution };
|
|
1085
1088
|
const read = card.primitive === "tables" ? tableReadRecommendation(query) : null;
|
|
1086
1089
|
if (read)
|
|
1087
1090
|
card = { ...card, execution: read.execution };
|
|
@@ -1639,6 +1642,49 @@ function tableMaintenanceRecommendation(query) {
|
|
|
1639
1642
|
}
|
|
1640
1643
|
return null;
|
|
1641
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Column structure the web's column editor and "Link to another table" own:
|
|
1647
|
+
* linking two tables, plain and choice field types with their options, and a
|
|
1648
|
+
* lookup or bind column's configuration. All of it was reachable from the CLI
|
|
1649
|
+
* and MCP, but generic table discovery ranked `tables create` first (2026-10-10:
|
|
1650
|
+
* "link two tables" put `tables relate` tenth, and the Copilot, asked for a
|
|
1651
|
+
* select column, decided the API had none). Sort, rank and order wording stays
|
|
1652
|
+
* with the value-order read above.
|
|
1653
|
+
*/
|
|
1654
|
+
function tableStructureRecommendation(query) {
|
|
1655
|
+
if (isFunctionIntent(query) || isSecondPartyDecisionIntent(query)
|
|
1656
|
+
|| /\b(workflows?|agents?|campaigns?|sequences?|crm|views?|kanban|sort\w*|order\w*|rank\w*|runs?|credits?)\b/.test(query))
|
|
1657
|
+
return null;
|
|
1658
|
+
const edits = /\b(change|edit|update|rename|recolou?r|replace|remove|delete|existing)\b/.test(query);
|
|
1659
|
+
const pair = (add, update) => {
|
|
1660
|
+
const [first, second] = edits ? [update, add] : [add, update];
|
|
1661
|
+
return { tools: [first[0], second[0]], commands: [first[1], second[1]] };
|
|
1662
|
+
};
|
|
1663
|
+
if (/\b(link\w*|connect\w*|relat\w*|join\w*)\b/.test(query)
|
|
1664
|
+
&& /\btables\b|\btable\b.{0,40}\btable\b|\blinked.records?\b|\blinked.record.columns?\b/.test(query))
|
|
1665
|
+
return {
|
|
1666
|
+
tools: ["oxygen_tables_relate", "oxygen_tables_link", "oxygen_tables_link_bulk", "oxygen_tables_relations"],
|
|
1667
|
+
commands: ["tables relate", "tables link", "tables relations list"],
|
|
1668
|
+
execution: "Link two Tables like the web's Link to another table: tables relate <table> --to <other> (oxygen_tables_relate) creates an empty linked-record column on BOTH tables; no matching values are needed and plain tables never become CRM objects. Then link rows one pair at a time with tables link <table> <row_id> --relation <slug> --target-row-id <id> (oxygen_tables_link), or by matching values with tables link <table> --to <other> (oxygen_tables_link_bulk: free preview, then approved). tables relations list|update|archive manages it. 0 credits.",
|
|
1669
|
+
};
|
|
1670
|
+
if (/\b(lookups?|look up|vlookup)\b/.test(query) && /\b(columns?|tables?)\b/.test(query))
|
|
1671
|
+
return {
|
|
1672
|
+
...pair(["oxygen_columns_add", "columns add"], ["oxygen_columns_update", "columns update"]),
|
|
1673
|
+
execution: "A lookup column reads another Table by a key: columns add <table> --lookup-table <source> --lookup-match <local>=<source_column> with --lookup-mode first-match|count|exists|aggregate, --lookup-return, --lookup-normalize, --lookup-order or --lookup-aggregate (MCP oxygen_columns_add kind lookup). Edit one with the same flags on columns update; they apply over the stored join, and the data type follows the mode. Free; reads resolve live. To link rows instead, tables relate.",
|
|
1674
|
+
};
|
|
1675
|
+
if (/\b(bind|binding)\b/.test(query) && /\bcolumns?\b/.test(query))
|
|
1676
|
+
return {
|
|
1677
|
+
...pair(["oxygen_columns_add", "columns add"], ["oxygen_columns_update", "columns update"]),
|
|
1678
|
+
execution: "A bind column resolves each row to a CRM record: columns add <table> --bind-object companies --bind-map domain=<column> (MCP kind bind, definition.object and inputMapping). columns update --bind-object/--bind-map/--bind-create edits it over the stored binding. Creating records on no match is a truth write that needs an approved run.",
|
|
1679
|
+
};
|
|
1680
|
+
if (/\b(columns?|fields?)\b/.test(query)
|
|
1681
|
+
&& /\b(select|single.select|multi.?select|dropdown|picklist|status|choices?|options?|checkbox|date|number|markdown)\b/.test(query))
|
|
1682
|
+
return {
|
|
1683
|
+
...pair(["oxygen_columns_add", "columns add"], ["oxygen_columns_update", "columns update"]),
|
|
1684
|
+
execution: "Add a plain field by its type, the web picker's names: columns add <table> --label <label> --type select|multi_select|status|text|number|checkbox|date|json|markdown|image, with --options \"A,B,C\" for a choice type (MCP oxygen_columns_add column.type, choices in definition.options). Replace a choice column's options with columns update --options (MCP options); cells store each value, so a rename is a new label on the same value. Setting a cell is a row write. 0 credits.",
|
|
1685
|
+
};
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1642
1688
|
/** Stored conversation drafts are Messages data, rather than worker queue health. */
|
|
1643
1689
|
function storedReplyDraftReadRecommendation(query) {
|
|
1644
1690
|
if (!/\bdrafts?\b/.test(query)
|
|
@@ -2447,6 +2493,9 @@ function recommendationsFor(card, query) {
|
|
|
2447
2493
|
const maintenance = tableMaintenanceRecommendation(query);
|
|
2448
2494
|
if (maintenance)
|
|
2449
2495
|
return maintenance;
|
|
2496
|
+
const structure = tableStructureRecommendation(query);
|
|
2497
|
+
if (structure)
|
|
2498
|
+
return structure;
|
|
2450
2499
|
if (isTableCapacityReadIntent(query))
|
|
2451
2500
|
return {
|
|
2452
2501
|
tools: ["oxygen_limits_show", "oxygen_tables_query", "oxygen_tables_describe"],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Browser resource operations, using the same scoped handlers on every surface. */
|
|
2
|
-
import {
|
|
2
|
+
import { TABLE_FIELD_TYPES } from "../table-field-types.js";
|
|
3
|
+
import { arr, bool, enumOf, int, jsonValue, nullable, num, obj, str } from "./schema.js";
|
|
3
4
|
import { TABLE_ROW_READ_WINDOW_FIELDS } from "./table-row-read.js";
|
|
4
5
|
import { TABLE_ROW_REMOVAL_FIELDS } from "./table-row-removal.js";
|
|
5
6
|
const text = (description) => str(description ? { description } : {});
|
|
@@ -68,7 +69,7 @@ export const BROWSER_OPERATIONS = [
|
|
|
68
69
|
operation("tables.resource.column_stats", `${table}/column-stats`, "GET", "tables column-stats", "oxygen_table_column_stats", "tables", "Read per-column completion and provider statistics; large Tables use labeled samples."),
|
|
69
70
|
operation("tables.resource.fill_progress", `${table}/fill-progress`, "GET", "tables fill-progress", "oxygen_table_fill_progress", "tables", "Read hosted Table fill progress and any actionable ingestion failure."),
|
|
70
71
|
operation("tables.resource.kind_estimates", "/api/cli/tables/column-kind-estimates", "GET", "columns kind-estimates", "oxygen_column_kind_estimates", "tables", "Read managed credit estimates for available Table column kinds."),
|
|
71
|
-
operation("tables.resource.columns.create", `${table}/columns`, "POST", "columns create", "oxygen_table_column_create", "tables", "Create a Table column at an optional zero-based position.", { column: obj({ label: text(), key: text(), kind: text(), data_type: dataType(), semantic_type: text(), definition: json }, { required: ["label"] }), position: int({ minimum: 0 }) }, ["column"]),
|
|
72
|
+
operation("tables.resource.columns.create", `${table}/columns`, "POST", "columns create", "oxygen_table_column_create", "tables", "Create a Table column at an optional zero-based position; type is a field type, and a choice type takes options (cells store each value).", { column: obj({ label: text(), key: text(), kind: text(), type: enumOf(TABLE_FIELD_TYPES), options: arr(jsonValue()), data_type: dataType(), semantic_type: text(), definition: json }, { required: ["label"] }), position: int({ minimum: 0 }) }, ["column"]),
|
|
72
73
|
operation("tables.resource.columns.edit", column, "PATCH", "columns edit", "oxygen_table_column_edit", "tables", "Save a column's label, type, order or definition. Returns downstream reference warnings rather than silently discarding them.", { label: text(), semantic_type: nullable(text()), definition: json, definition_unset: strings(), data_type: dataType(), position: int({ minimum: 0 }) }),
|
|
73
74
|
operation("tables.resource.columns.archive", `${column}/archive`, "POST", "columns archive", "oxygen_columns_archive", "tables", "Archive a Table column indefinitely while preserving its stored values.", {}, [], nativeCli),
|
|
74
75
|
operation("tables.resource.columns.duplicate", `${column}/duplicate`, "POST", "columns duplicate", "oxygen_table_column_duplicate", "tables", "Duplicate a Table column and its definition with an optional new label.", { label: text() }),
|
|
@@ -84,8 +85,8 @@ export const BROWSER_OPERATIONS = [
|
|
|
84
85
|
operation("tables.resource.rows.delete", `${table}/rows/{row}`, "DELETE", "tables rows remove", "oxygen_table_row_remove", "tables", "Remove one row from a workspace Table."),
|
|
85
86
|
operation("tables.resource.rows.bulk_delete", `${table}/rows/delete`, "POST", "tables rows remove-selection", "oxygen_table_rows_remove_selection", "tables", "Permanently delete Table rows by a readable filter tree, exact ids or a matching selection. Unsupported scope fields are refused.", TABLE_ROW_REMOVAL_FIELDS, [], {}, { destructiveHint: true }),
|
|
86
87
|
operation("tables.resource.rows.ids", `${table}/rows/ids`, "GET", "tables rows range-ids", "oxygen_table_rows_range_ids", "tables", "Resolve a bounded row range into exact ids, preserving the current filter, search, sort and CRM scope.", { offset: int({ minimum: 0 }), limit: int({ minimum: 1 }), filter: text(), search: text(), sort: text(), crm_object: text() }, ["offset", "limit"]),
|
|
87
|
-
operation("tables.resource.relationships.get", `${table}/records/{row}/relationships`, "GET", "tables rows relationships", "oxygen_table_row_relationships", "crm", "Read
|
|
88
|
-
operation("tables.resource.relationships.set", `${table}/records/{row}/relationships`, "POST", "tables rows relationship-set", "oxygen_table_row_relationship_set", "crm", "
|
|
88
|
+
operation("tables.resource.relationships.get", `${table}/records/{row}/relationships`, "GET", "tables rows relationships", "oxygen_table_row_relationships", "crm", "Read one Table row's linked records, across Table relations and CRM relationships, and the relations it can link through."),
|
|
89
|
+
operation("tables.resource.relationships.set", `${table}/records/{row}/relationships`, "POST", "tables rows relationship-set", "oxygen_table_row_relationship_set", "crm", "Link one Table row to a target row through a relation (Table or CRM) and return the link.", { relationship: text("Relation slug: the linked-record column key."), target_row_id: text("Row id in the related table.") }, ["relationship", "target_row_id"]),
|
|
89
90
|
operation("tables.resource.views.list", `${table}/views`, "GET", "tables views list", "oxygen_tables_views_list", "tables", "List saved Table views and their complete display configuration.", {}, [], nativeCli),
|
|
90
91
|
operation("tables.resource.views.create", `${table}/views`, "POST", "tables views create", "oxygen_tables_views_create", "tables", "Create a saved Table view; group_by configures the same kanban grouping as the browser.", viewFields, ["name"], nativeCli),
|
|
91
92
|
operation("tables.resource.views.edit", `${table}/views/{view}`, "PATCH", "tables views update", "oxygen_tables_views_update", "tables", "Edit a saved view's name, type, configuration, grouping, order or default status.", viewFields, [], nativeCli),
|
|
@@ -90,7 +90,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
90
90
|
operation("tables.import.preview", "/api/cli/tables/import-preview", "POST", "tables import-preview", "oxygen_tables_import_preview", "tables", "Preview a staged file's headers, rows and existing-table mapping; writes no imported rows.", { storage_key: text(), file_name: text(), format: enumOf(["csv", "json", "xlsx"]), byte_length: int({ minimum: 1 }), table: text(), upsert_key: text() }, ["storage_key", "byte_length"], { mutates: false }),
|
|
91
91
|
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"]),
|
|
92
92
|
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" } }),
|
|
93
|
-
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
|
|
93
|
+
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 --to (MCP oxygen_tables_link_bulk).", TABLE_LINK_SUGGEST_CONTRACT.properties, ["table", "to"], { mutates: false }),
|
|
94
94
|
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 } }),
|
|
95
95
|
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),
|
|
96
96
|
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),
|
|
@@ -46,9 +46,7 @@ export type HostedAiModelSpec = {
|
|
|
46
46
|
/** Ordered fallback model ids tried when `model` is unavailable. */
|
|
47
47
|
fallbackModels: readonly string[];
|
|
48
48
|
/**
|
|
49
|
-
* OpenRouter reasoning effort to request; omit
|
|
50
|
-
* support their max mode (Kimi K3) — the request builder sends no reasoning
|
|
51
|
-
* param for those.
|
|
49
|
+
* OpenRouter reasoning effort to request; omit to use the provider default.
|
|
52
50
|
*/
|
|
53
51
|
reasoningEffort?: "low" | "medium" | "high";
|
|
54
52
|
/**
|
|
@@ -224,8 +222,10 @@ export declare function redactManagedModelIds<T>(value: T, options?: {
|
|
|
224
222
|
*/
|
|
225
223
|
export declare function hostedAiDefaultModels(useCase: HostedAiUseCase): Record<HostedAiLevel, string>;
|
|
226
224
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
225
|
+
* Staff dev experiment: only the authoritative selfhost-dev deployment and an
|
|
226
|
+
* explicit enable flag can run Copilot. NODE_ENV and request hosts cannot open
|
|
227
|
+
* it; production and shadow remain closed even with an enabled flag.
|
|
230
228
|
*/
|
|
231
229
|
export declare function isCopilotEnabled(env?: Record<string, string | undefined>): boolean;
|
|
230
|
+
/** Historical cost reconciliation may settle receipts without running Copilot. */
|
|
231
|
+
export declare function isCopilotAccountingEnabled(env?: Record<string, string | undefined>): boolean;
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export const HOSTED_AI_MODEL_REGISTRY = {
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Historical calibration of the September AI-column tiers, re-picked
|
|
26
|
+
* 2026-09-26 on measured evidence (Balanced and Max refreshed below)
|
|
26
27
|
* (Philipp, 2026-09-26: Clay-level quality, faster, 30-50% cheaper than Clay,
|
|
27
28
|
* and at least 5x markup on AI and research columns -- with one explicit
|
|
28
29
|
* founder exception for Max, below).
|
|
@@ -46,7 +47,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
46
47
|
* Balanced deepseek-v4.1-flash, no reasoning 4.23 0.0079 (0.60x v4-pro @ $0.0132) 4 ~5.1x
|
|
47
48
|
* Max gpt-6-sol 4.13 (5th; fastest, 1-3 s) 0.098 25 ~2.5x
|
|
48
49
|
*
|
|
49
|
-
* Balanced
|
|
50
|
+
* September Balanced ran with reasoning OFF (`disableReasoning`): on the prod-shaped
|
|
50
51
|
* set it cost $0.00141/row against reasoning-on v4-pro's $0.00235 with
|
|
51
52
|
* classify, extract and research accuracy unchanged (100%; research citations
|
|
52
53
|
* 90%). With reasoning on it would not clear 5x at 4 credits.
|
|
@@ -74,8 +75,11 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
74
75
|
* models answered $49, including Luna, V4.1 Flash and Sol; Gemini 3.8 Flash
|
|
75
76
|
* (Max's fallback) was one of the four that did not.
|
|
76
77
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
78
|
+
* Refresh approved 2026-10-10: Balanced now uses Haiku 5.5 medium and Max
|
|
79
|
+
* uses Sol 6.1 medium; Fast and all customer credit prices stay unchanged.
|
|
80
|
+
* The measurements above describe the September models, not a bakeoff of
|
|
81
|
+
* these replacements. Selection/pricing evidence: oxygen-knowledge/wiki/ops/
|
|
82
|
+
* Background AI Model Selection 2026-10.md. No new inference spend was used.
|
|
79
83
|
* Retired ids live in LEGACY_MANAGED_AI_COLUMN_MODELS.
|
|
80
84
|
*/
|
|
81
85
|
ai_column: {
|
|
@@ -88,18 +92,19 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
88
92
|
maxOutputTokens: 4096,
|
|
89
93
|
},
|
|
90
94
|
medium: {
|
|
91
|
-
model: "
|
|
95
|
+
model: "anthropic/claude-haiku-5.5",
|
|
92
96
|
displayName: "Oxygen Balanced",
|
|
93
|
-
fallbackModels: ["
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
fallbackModels: ["deepseek/deepseek-v4.1-flash"],
|
|
98
|
+
reasoningEffort: "medium",
|
|
99
|
+
estPromptUsdPerM: 0.1,
|
|
100
|
+
estCompletionUsdPerM: 0.5,
|
|
96
101
|
maxOutputTokens: 4096,
|
|
97
|
-
disableReasoning: true,
|
|
98
102
|
},
|
|
99
103
|
high: {
|
|
100
|
-
model: "openai/gpt-6-sol",
|
|
104
|
+
model: "openai/gpt-6.1-sol",
|
|
101
105
|
displayName: "Oxygen Max",
|
|
102
106
|
fallbackModels: ["google/gemini-3.8-flash"],
|
|
107
|
+
reasoningEffort: "medium",
|
|
103
108
|
estPromptUsdPerM: 2,
|
|
104
109
|
estCompletionUsdPerM: 10,
|
|
105
110
|
maxOutputTokens: 4096,
|
|
@@ -114,8 +119,9 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
114
119
|
* cache WRITE on a fresh model, ten times a turn. Models only change below
|
|
115
120
|
* this thread, where the context is fresh -- see COPILOT_ERRAND_LEVEL.
|
|
116
121
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
122
|
+
* Before the 2026-10-10 Sol refresh, Auto ran Kimi K3. Its
|
|
123
|
+
* `reasoningEffort: "medium"` made that tier worth shipping.
|
|
124
|
+
* Until 2026-09-21 Kimi was sent with NO `reasoning` field at
|
|
119
125
|
* all, on a registry comment asserting K3 was max-reasoning-only. Measured
|
|
120
126
|
* against the live API on the same prompt and tool schema, that claim was
|
|
121
127
|
* false and the omission was the expensive case:
|
|
@@ -129,16 +135,14 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
129
135
|
* surface where output is ~61% of the bill.
|
|
130
136
|
*/
|
|
131
137
|
auto: {
|
|
132
|
-
model: "
|
|
138
|
+
model: "openai/gpt-6.1-sol",
|
|
133
139
|
displayName: "Oxygen Auto",
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
// quarters of the window. GLM-5.3 keeps both and caches automatically.
|
|
138
|
-
fallbackModels: ["z-ai/glm-5.3"],
|
|
140
|
+
// Approved 2026-10-10 for NEW sessions. Existing sessions retain their
|
|
141
|
+
// persisted resolved_model; Kimi's priced spec survives under high.
|
|
142
|
+
fallbackModels: ["anthropic/claude-sonnet-5.5"],
|
|
139
143
|
reasoningEffort: "medium",
|
|
140
|
-
estPromptUsdPerM:
|
|
141
|
-
estCompletionUsdPerM:
|
|
144
|
+
estPromptUsdPerM: 2,
|
|
145
|
+
estCompletionUsdPerM: 10,
|
|
142
146
|
maxOutputTokens: 8192,
|
|
143
147
|
},
|
|
144
148
|
low: {
|
|
@@ -183,10 +187,10 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
183
187
|
},
|
|
184
188
|
/**
|
|
185
189
|
* Retained only for sessions created before "Oxygen Auto" shipped, whose
|
|
186
|
-
* `reasoning_level` is already 'high' on the row
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
+
* `reasoning_level` is already 'high' on the row, and for Auto sessions
|
|
191
|
+
* pinned to Kimi before the 2026-10-10 refresh. Keep its pricing, effort
|
|
192
|
+
* and GLM fallback so changing the new-session default never switches an
|
|
193
|
+
* existing conversation's model or loses its prompt cache.
|
|
190
194
|
*/
|
|
191
195
|
high: {
|
|
192
196
|
model: "moonshotai/kimi-k3",
|
|
@@ -239,6 +243,8 @@ export const LEGACY_MANAGED_AI_COLUMN_MODELS = {
|
|
|
239
243
|
"deepseek/deepseek-v4-flash": "low",
|
|
240
244
|
"deepseek/deepseek-v4-pro": "medium",
|
|
241
245
|
"moonshotai/kimi-k2.6": "high",
|
|
246
|
+
"deepseek/deepseek-v4.1-flash": "medium",
|
|
247
|
+
"openai/gpt-6-sol": "high",
|
|
242
248
|
};
|
|
243
249
|
/**
|
|
244
250
|
* Managed model ids OXYGEN ran that never sat in the registry above, mapped to
|
|
@@ -445,6 +451,11 @@ function managedModelLabels(preferredUseCase, env = typeof process === "undefine
|
|
|
445
451
|
labels.set(model, level === "AUTO" ? COPILOT_TIER_LABELS.auto : MANAGED_AI_TIER_LABELS[level.toLowerCase()]);
|
|
446
452
|
}
|
|
447
453
|
}
|
|
454
|
+
// Historical Copilot events carry a model id without their session's level.
|
|
455
|
+
// Keep Kimi's former Auto label when reading those events after the refresh;
|
|
456
|
+
// session payloads use their persisted reasoning_level instead.
|
|
457
|
+
if (!labels.has("moonshotai/kimi-k3"))
|
|
458
|
+
labels.set("moonshotai/kimi-k3", COPILOT_TIER_LABELS.auto);
|
|
448
459
|
const useCases = Object.keys(HOSTED_AI_MODEL_REGISTRY);
|
|
449
460
|
const ordered = preferredUseCase
|
|
450
461
|
? [preferredUseCase, ...useCases.filter((useCase) => useCase !== preferredUseCase)]
|
|
@@ -579,11 +590,17 @@ export function hostedAiDefaultModels(useCase) {
|
|
|
579
590
|
};
|
|
580
591
|
}
|
|
581
592
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
593
|
+
* Staff dev experiment: only the authoritative selfhost-dev deployment and an
|
|
594
|
+
* explicit enable flag can run Copilot. NODE_ENV and request hosts cannot open
|
|
595
|
+
* it; production and shadow remain closed even with an enabled flag.
|
|
585
596
|
*/
|
|
586
597
|
export function isCopilotEnabled(env = process.env) {
|
|
598
|
+
if (env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase() !== "selfhost-dev")
|
|
599
|
+
return false;
|
|
600
|
+
return isCopilotAccountingEnabled(env);
|
|
601
|
+
}
|
|
602
|
+
/** Historical cost reconciliation may settle receipts without running Copilot. */
|
|
603
|
+
export function isCopilotAccountingEnabled(env = process.env) {
|
|
587
604
|
const raw = env.OXYGEN_COPILOT_ENABLED;
|
|
588
605
|
if (typeof raw !== "string")
|
|
589
606
|
return false;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Staff authorization uses configured email domains, never a workspace role. */
|
|
2
|
+
export function isStaffEmail(email, env = process.env) {
|
|
3
|
+
if (!email)
|
|
4
|
+
return false;
|
|
5
|
+
const at = email.lastIndexOf("@");
|
|
6
|
+
if (at <= 0)
|
|
7
|
+
return false;
|
|
8
|
+
const domain = email.slice(at + 1).toLowerCase();
|
|
9
|
+
return (env.OXYGEN_STAFF_EMAIL_DOMAINS ?? "")
|
|
10
|
+
.split(",")
|
|
11
|
+
.some((entry) => entry.trim().toLowerCase() === domain && domain.length > 0);
|
|
12
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type SelectOption } from "./select-options.js";
|
|
2
|
+
export declare const TABLE_FIELD_TYPES: readonly ["text", "number", "checkbox", "date", "json", "markdown", "image", "select", "multi_select", "status"];
|
|
3
|
+
export type TableFieldType = (typeof TABLE_FIELD_TYPES)[number];
|
|
4
|
+
export type TableFieldDataType = "text" | "numeric" | "boolean" | "timestamptz" | "jsonb";
|
|
5
|
+
/** The canonical field type for a spelling, or null when it is not one. */
|
|
6
|
+
export declare function resolveTableFieldType(raw: unknown): TableFieldType | null;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a field type only describes how the value is STORED. Those compose
|
|
9
|
+
* with every column kind (`type: "text"` beside `kind: "ai"` keeps meaning the
|
|
10
|
+
* data type, as it always did on `tables create`); a type with a semantic is a
|
|
11
|
+
* manual field of its own.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isDataOnlyTableFieldType(type: TableFieldType): boolean;
|
|
14
|
+
export declare function tableFieldTypeTakesOptions(type: TableFieldType): boolean;
|
|
15
|
+
export declare function tableFieldTypeDataType(type: TableFieldType): TableFieldDataType;
|
|
16
|
+
export declare function tableFieldTypeSemantic(type: TableFieldType): string | null;
|
|
17
|
+
export type TableFieldTypeColumn = {
|
|
18
|
+
kind: "manual";
|
|
19
|
+
dataType: TableFieldDataType;
|
|
20
|
+
semanticType: string | null;
|
|
21
|
+
definition: {
|
|
22
|
+
options?: SelectOption[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The stored column a field type creates. A choice type always carries an
|
|
27
|
+
* `options` array (empty when none were given), exactly as the picker seeds
|
|
28
|
+
* it, so the editor opens on an option list rather than a missing one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function tableFieldTypeColumn(type: TableFieldType, options?: unknown): TableFieldTypeColumn;
|
|
31
|
+
export type LookupResultDataType = "text" | "numeric" | "boolean" | "jsonb";
|
|
32
|
+
/**
|
|
33
|
+
* The data type a lookup column gets for its mode when nobody chose one: the
|
|
34
|
+
* web editor's rule, shared so `columns add` and `columns update` on the CLI
|
|
35
|
+
* and MCP derive the same type instead of leaving text in place and failing at
|
|
36
|
+
* run time. count/aggregate are numbers, exists is a boolean, a first_match
|
|
37
|
+
* returning one column is that value as text, and several (or none chosen yet)
|
|
38
|
+
* are an object.
|
|
39
|
+
*/
|
|
40
|
+
export declare function lookupColumnDataType(mode: unknown, returnColumns: unknown): LookupResultDataType;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a stored data type can hold a lookup's result: the run validator's
|
|
43
|
+
* rule (`validateTableActionLookupColumn` in @oxygen/tenant-db). A single-return
|
|
44
|
+
* first_match fits any type, so editing one never forces a retype.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lookupDataTypeFits(mode: unknown, returnColumns: unknown, dataType: string): boolean;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// One vocabulary for a Table column's FIELD TYPE: the plain data and choice
|
|
2
|
+
// columns the web "+ Add column" picker offers (Text, Number, Checkbox, Date,
|
|
3
|
+
// JSON, Markdown, Select, Multi-select, Status) plus the editor's "Display as
|
|
4
|
+
// image". Every authoring surface resolves a `type` through here, so a Select
|
|
5
|
+
// column made by the CLI, MCP, the Copilot or the picker is the same stored
|
|
6
|
+
// column: kind manual, data type text, semantic type select, options in its
|
|
7
|
+
// definition.
|
|
8
|
+
//
|
|
9
|
+
// Why it exists (2026-10-10): the Workspace Copilot, asked for a relationship-
|
|
10
|
+
// stage select column, concluded "plain select isn't supported by the API" and
|
|
11
|
+
// made a text column. The API could always store one, but only as a
|
|
12
|
+
// kind + data_type + semantic_type + definition.options combination that no
|
|
13
|
+
// schema named, and multi-select additionally needs jsonb. A type name is the
|
|
14
|
+
// thing a person or an agent actually asks for.
|
|
15
|
+
//
|
|
16
|
+
// CLIENT-SAFE LEAF: the web picker imports it; keep runtime imports to
|
|
17
|
+
// ./select-options.
|
|
18
|
+
import { normalizeSelectOptions } from "./select-options.js";
|
|
19
|
+
export const TABLE_FIELD_TYPES = [
|
|
20
|
+
"text",
|
|
21
|
+
"number",
|
|
22
|
+
"checkbox",
|
|
23
|
+
"date",
|
|
24
|
+
"json",
|
|
25
|
+
"markdown",
|
|
26
|
+
"image",
|
|
27
|
+
"select",
|
|
28
|
+
"multi_select",
|
|
29
|
+
"status",
|
|
30
|
+
];
|
|
31
|
+
const SHAPES = {
|
|
32
|
+
text: { dataType: "text", semanticType: null, takesOptions: false },
|
|
33
|
+
number: { dataType: "numeric", semanticType: null, takesOptions: false },
|
|
34
|
+
checkbox: { dataType: "boolean", semanticType: null, takesOptions: false },
|
|
35
|
+
date: { dataType: "timestamptz", semanticType: null, takesOptions: false },
|
|
36
|
+
json: { dataType: "jsonb", semanticType: null, takesOptions: false },
|
|
37
|
+
markdown: { dataType: "text", semanticType: "markdown", takesOptions: false },
|
|
38
|
+
image: { dataType: "text", semanticType: "image", takesOptions: false },
|
|
39
|
+
select: { dataType: "text", semanticType: "select", takesOptions: true },
|
|
40
|
+
// A multi-select cell is a list, so it is stored as jsonb; the picker has
|
|
41
|
+
// always seeded it that way, and a text multi-select cannot hold its values.
|
|
42
|
+
multi_select: { dataType: "jsonb", semanticType: "multi_select", takesOptions: true },
|
|
43
|
+
status: { dataType: "text", semanticType: "status", takesOptions: true },
|
|
44
|
+
};
|
|
45
|
+
// Accepted spellings. A column `type` on `tables create` has always meant its
|
|
46
|
+
// data type, so every data type and data-type alias the tenant normalizer
|
|
47
|
+
// accepts resolves to the field type that stores it (`url`/`email` are text
|
|
48
|
+
// there too). The rest are the picker's ids and the obvious variants.
|
|
49
|
+
const ALIASES = {
|
|
50
|
+
numeric: "number",
|
|
51
|
+
boolean: "checkbox",
|
|
52
|
+
timestamptz: "date",
|
|
53
|
+
jsonb: "json",
|
|
54
|
+
url: "text",
|
|
55
|
+
email: "text",
|
|
56
|
+
single_select: "select",
|
|
57
|
+
multiselect: "multi_select",
|
|
58
|
+
};
|
|
59
|
+
/** The canonical field type for a spelling, or null when it is not one. */
|
|
60
|
+
export function resolveTableFieldType(raw) {
|
|
61
|
+
if (typeof raw !== "string")
|
|
62
|
+
return null;
|
|
63
|
+
const normalized = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
64
|
+
if (TABLE_FIELD_TYPES.includes(normalized))
|
|
65
|
+
return normalized;
|
|
66
|
+
return ALIASES[normalized] ?? null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a field type only describes how the value is STORED. Those compose
|
|
70
|
+
* with every column kind (`type: "text"` beside `kind: "ai"` keeps meaning the
|
|
71
|
+
* data type, as it always did on `tables create`); a type with a semantic is a
|
|
72
|
+
* manual field of its own.
|
|
73
|
+
*/
|
|
74
|
+
export function isDataOnlyTableFieldType(type) {
|
|
75
|
+
return SHAPES[type].semanticType === null;
|
|
76
|
+
}
|
|
77
|
+
export function tableFieldTypeTakesOptions(type) {
|
|
78
|
+
return SHAPES[type].takesOptions;
|
|
79
|
+
}
|
|
80
|
+
export function tableFieldTypeDataType(type) {
|
|
81
|
+
return SHAPES[type].dataType;
|
|
82
|
+
}
|
|
83
|
+
export function tableFieldTypeSemantic(type) {
|
|
84
|
+
return SHAPES[type].semanticType;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The stored column a field type creates. A choice type always carries an
|
|
88
|
+
* `options` array (empty when none were given), exactly as the picker seeds
|
|
89
|
+
* it, so the editor opens on an option list rather than a missing one.
|
|
90
|
+
*/
|
|
91
|
+
export function tableFieldTypeColumn(type, options) {
|
|
92
|
+
const shape = SHAPES[type];
|
|
93
|
+
return {
|
|
94
|
+
kind: "manual",
|
|
95
|
+
dataType: shape.dataType,
|
|
96
|
+
semanticType: shape.semanticType,
|
|
97
|
+
definition: shape.takesOptions ? { options: normalizeSelectOptions(options ?? []) } : {},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The data type a lookup column gets for its mode when nobody chose one: the
|
|
102
|
+
* web editor's rule, shared so `columns add` and `columns update` on the CLI
|
|
103
|
+
* and MCP derive the same type instead of leaving text in place and failing at
|
|
104
|
+
* run time. count/aggregate are numbers, exists is a boolean, a first_match
|
|
105
|
+
* returning one column is that value as text, and several (or none chosen yet)
|
|
106
|
+
* are an object.
|
|
107
|
+
*/
|
|
108
|
+
export function lookupColumnDataType(mode, returnColumns) {
|
|
109
|
+
if (mode === "exists")
|
|
110
|
+
return "boolean";
|
|
111
|
+
if (mode === "count" || mode === "aggregate")
|
|
112
|
+
return "numeric";
|
|
113
|
+
return Array.isArray(returnColumns) && returnColumns.length === 1 ? "text" : "jsonb";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether a stored data type can hold a lookup's result: the run validator's
|
|
117
|
+
* rule (`validateTableActionLookupColumn` in @oxygen/tenant-db). A single-return
|
|
118
|
+
* first_match fits any type, so editing one never forces a retype.
|
|
119
|
+
*/
|
|
120
|
+
export function lookupDataTypeFits(mode, returnColumns, dataType) {
|
|
121
|
+
if (mode === "count" || mode === "aggregate")
|
|
122
|
+
return dataType === "numeric";
|
|
123
|
+
if (mode === "exists")
|
|
124
|
+
return dataType === "boolean";
|
|
125
|
+
return !(Array.isArray(returnColumns) && returnColumns.length > 1) || dataType === "jsonb";
|
|
126
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1013.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1013.93";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
|
|
2
|
-
export const OXYGEN_BUILD_VERSION = "1.1013.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1013.93";
|