@oxygen-agent/cli 1.1014.618 → 1.1014.619
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/commands/crm.js +5 -2
- package/dist/commands/tables.js +15 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -2
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +16 -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/commands/crm.js
CHANGED
|
@@ -391,6 +391,8 @@ function buildCrmDuplicatesPath(object, options) {
|
|
|
391
391
|
const params = new URLSearchParams();
|
|
392
392
|
if (options.limit)
|
|
393
393
|
params.set("limit", options.limit);
|
|
394
|
+
if (options.cursor)
|
|
395
|
+
params.set("cursor", options.cursor);
|
|
394
396
|
const query = params.toString();
|
|
395
397
|
const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/duplicates`;
|
|
396
398
|
return query ? `${base}?${query}` : base;
|
|
@@ -1393,9 +1395,10 @@ export function registerCrmFamily(program, handleAsyncAction) {
|
|
|
1393
1395
|
await handleAsyncAction("crm describe", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}${field ? `?attribute=${encodeURIComponent(field)}` : ""}`));
|
|
1394
1396
|
}))
|
|
1395
1397
|
.addCommand(new Command("duplicates")
|
|
1396
|
-
.description("List ranked duplicate-candidate record pairs for one CRM object (identity-overlap + fuzzy-label). Read-only.")
|
|
1398
|
+
.description("List ranked duplicate-candidate record pairs for one CRM object (identity-overlap + fuzzy-label), across every record. Read-only.")
|
|
1397
1399
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1398
|
-
.option("--limit <limit>", "Maximum candidate pairs
|
|
1400
|
+
.option("--limit <limit>", "Maximum candidate pairs per page. Defaults to 25, max 100.")
|
|
1401
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page (its nextCursor).")
|
|
1399
1402
|
.option("--json", "Print a JSON envelope.")
|
|
1400
1403
|
.action(async (object, options) => {
|
|
1401
1404
|
await handleAsyncAction("crm duplicates", options, () => requestOxygen(buildCrmDuplicatesPath(object, options)));
|
package/dist/commands/tables.js
CHANGED
|
@@ -14,7 +14,7 @@ import { PIPEDRIVE_CATALOG_QUERY_CONTRACT, pipedriveCatalogQueryParams } from "@
|
|
|
14
14
|
import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/contracts/table-list";
|
|
15
15
|
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
16
16
|
import { TABLE_QUERY_CLI_DEFAULT_LIMIT, TABLE_QUERY_CLI_DEFAULT_MAX_CHARS, tableQueryRawResultFits, tableQueryRawRecovery } from "@oxygen/shared/contracts/table-query-raw";
|
|
17
|
-
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer } from "@oxygen/shared/file-import";
|
|
17
|
+
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, dropBlankImportCells, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer } from "@oxygen/shared/file-import";
|
|
18
18
|
import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
|
|
19
19
|
import { MAX_CLI_JSON_BODY_BYTES, PLAN_LIMITS } from "@oxygen/shared/plan-limits";
|
|
20
20
|
import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
|
|
@@ -608,6 +608,9 @@ async function importRowsFromUrl(table, options) {
|
|
|
608
608
|
return withImportWaitNextStep({ ...result, background: true, autoBackground: true });
|
|
609
609
|
}
|
|
610
610
|
async function importRows(table, options) {
|
|
611
|
+
if (options.url && options.skipBlankCells) {
|
|
612
|
+
throw new OxygenError("invalid_request", "--skip-blank-cells works on --file imports. Download the link and import the file.", { exitCode: 1 });
|
|
613
|
+
}
|
|
611
614
|
if (options.url)
|
|
612
615
|
return importRowsFromUrl(table, { ...options, url: options.url });
|
|
613
616
|
if (!options.file) {
|
|
@@ -661,7 +664,11 @@ async function importRowsFromFile(table, options) {
|
|
|
661
664
|
sourceHash,
|
|
662
665
|
}, preparedBackgroundTarget));
|
|
663
666
|
}
|
|
664
|
-
const
|
|
667
|
+
const prepared = await prepareImportTarget(table, options, parsedRows);
|
|
668
|
+
// The foreground writes rows itself: a row without a key leaves that value alone.
|
|
669
|
+
const target = options.skipBlankCells
|
|
670
|
+
? { ...prepared, rows: prepared.rows.map((row) => dropBlankImportCells(row, prepared.upsertKey)) }
|
|
671
|
+
: prepared;
|
|
665
672
|
let rowCount = 0;
|
|
666
673
|
let insertedCount = 0;
|
|
667
674
|
let updatedCount = 0;
|
|
@@ -1022,6 +1029,7 @@ table, options, format, file, context, preparedTarget = null) {
|
|
|
1022
1029
|
: {}),
|
|
1023
1030
|
...(target.upsertKey ? { upsert_key: target.upsertKey } : {}),
|
|
1024
1031
|
...(target.sourceKeyMap ? { source_key_map: target.sourceKeyMap } : {}),
|
|
1032
|
+
...(options.skipBlankCells ? { skip_blank_cells: true } : {}),
|
|
1025
1033
|
},
|
|
1026
1034
|
});
|
|
1027
1035
|
emitQueueWaitStderrNote(readRecord(result, "queue_wait"));
|
|
@@ -1108,6 +1116,10 @@ async function enqueueImportFile(table, options, format, batchSize, context, pre
|
|
|
1108
1116
|
exitCode: 1,
|
|
1109
1117
|
});
|
|
1110
1118
|
}
|
|
1119
|
+
if (options.skipBlankCells) {
|
|
1120
|
+
// The multipart import cannot leave blank cells out; failing beats clearing them.
|
|
1121
|
+
throw new OxygenError("skip_blank_cells_unavailable", "--skip-blank-cells needs the staged file upload, which this OXYGEN server has not configured. Add --sync to write the rows from this machine instead.", { exitCode: 1 });
|
|
1122
|
+
}
|
|
1111
1123
|
const fileBuffer = readFileSync(options.file);
|
|
1112
1124
|
const filename = basename(options.file);
|
|
1113
1125
|
const form = new FormData();
|
|
@@ -2441,6 +2453,7 @@ export function registerTablesFamily(program, handleAsyncAction, binaryName) {
|
|
|
2441
2453
|
.option("--create <name>", "Create a new table with columns inferred from the file before importing.")
|
|
2442
2454
|
.option("--project <project>", "Project id or slug for --create. Defaults to General.")
|
|
2443
2455
|
.option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
|
|
2456
|
+
.option("--skip-blank-cells", "Leave a row's existing value where the file's cell is blank instead of clearing it, so a partial update needs one file.")
|
|
2444
2457
|
.option("--batch-size <n>", `Requested rows per import chunk, not a limit on the file. Defaults to 500; values up to 5000 are accepted, but Oxygen splits writes to at most 500 rows (and smaller for wide rows, about ${formatJsonBodyLimit()} of JSON per request) so they fit request and worker lease budgets.`)
|
|
2445
2458
|
.option("--background", "Enqueue durable import chunks for the background worker.")
|
|
2446
2459
|
.option("--sync", `Force foreground import even for files above ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows. That threshold picks foreground vs background; it does not cap how many rows a file may hold.`)
|
|
@@ -13,6 +13,7 @@ const text = (description) => str(description === undefined ? {} : { description
|
|
|
13
13
|
const list = () => arr(str());
|
|
14
14
|
const json = { type: "object", additionalProperties: true };
|
|
15
15
|
const rows = () => arr(json);
|
|
16
|
+
const skipBlankCells = () => bool({ description: "Keep existing values where a cell is blank." });
|
|
16
17
|
function operation(id, route, method, cli, mcp, pack, summary, fields = {}, required = [], options = {}) {
|
|
17
18
|
const { annotations, ...behavior } = options;
|
|
18
19
|
const parameters = [...route.matchAll(/\{(\w+)\}/g)].map((match) => match[1]);
|
|
@@ -87,10 +88,10 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
87
88
|
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),
|
|
88
89
|
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 } }),
|
|
89
90
|
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"]),
|
|
90
|
-
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),
|
|
91
|
+
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(), skip_blank_cells: skipBlankCells(), batch_size: int({ minimum: 1, maximum: 5000 }) }, [], cliOnly),
|
|
91
92
|
operation("tables.import.prepare", "/api/cli/tables/import-url", "POST", "tables import-upload", "oxygen_tables_import_upload", "tables", "Prepare a scoped storage upload for a hosted Table import; uploads and imports are separate, inspectable steps.", { file_name: text(), content_type: text(), byte_length: int({ minimum: 1 }), trace_id: text(), format: enumOf(["csv", "json", "xlsx"]) }, ["file_name", "byte_length"]),
|
|
92
93
|
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 }),
|
|
93
|
-
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"]),
|
|
94
|
+
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(), skip_blank_cells: skipBlankCells(), 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"]),
|
|
94
95
|
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" } }),
|
|
95
96
|
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 }),
|
|
96
97
|
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 } }),
|
|
@@ -67,6 +67,14 @@ export declare function parseRowsSampleText(text: string, format: Exclude<RowsFi
|
|
|
67
67
|
}): RowsSample;
|
|
68
68
|
export declare function inferImportColumnLabels(rows: Record<string, unknown>[]): string[];
|
|
69
69
|
export declare function normalizeRowsForNewTable(rows: Record<string, unknown>[]): NewTableImportRows;
|
|
70
|
+
/**
|
|
71
|
+
* The row without its blank cells, for an import that must leave a record's
|
|
72
|
+
* existing value alone where the file has nothing ("skip blank cells"): an
|
|
73
|
+
* upsert writes only the keys a row carries. Blank is null, undefined or text
|
|
74
|
+
* that is only whitespace. The upsert key always stays, so a row without one is
|
|
75
|
+
* still reported as missing its key instead of quietly dropping out.
|
|
76
|
+
*/
|
|
77
|
+
export declare function dropBlankImportCells(row: Record<string, unknown>, keepKey?: string | null): Record<string, unknown>;
|
|
70
78
|
export declare function normalizeImportColumnKey(value: string): string;
|
|
71
79
|
/**
|
|
72
80
|
* Refuse a JSON or XLSX file over the buffered-parse ceiling before anything is
|
|
@@ -217,6 +217,22 @@ export function normalizeRowsForNewTable(rows) {
|
|
|
217
217
|
// it MATCHES imported file headers back to the column key MINTED by tenant-db's
|
|
218
218
|
// toSnakeIdentifier, and has call sites across web/CLI/API. The two must never
|
|
219
219
|
// diverge \u2014 see packages/shared/src/identifiers.ts + identifiers.test.ts.
|
|
220
|
+
/**
|
|
221
|
+
* The row without its blank cells, for an import that must leave a record's
|
|
222
|
+
* existing value alone where the file has nothing ("skip blank cells"): an
|
|
223
|
+
* upsert writes only the keys a row carries. Blank is null, undefined or text
|
|
224
|
+
* that is only whitespace. The upsert key always stays, so a row without one is
|
|
225
|
+
* still reported as missing its key instead of quietly dropping out.
|
|
226
|
+
*/
|
|
227
|
+
export function dropBlankImportCells(row, keepKey) {
|
|
228
|
+
const kept = {};
|
|
229
|
+
for (const [key, value] of Object.entries(row)) {
|
|
230
|
+
const blank = value === null || value === undefined || (typeof value === "string" && value.trim() === "");
|
|
231
|
+
if (!blank || key === keepKey)
|
|
232
|
+
kept[key] = value;
|
|
233
|
+
}
|
|
234
|
+
return kept;
|
|
235
|
+
}
|
|
220
236
|
export function normalizeImportColumnKey(value) {
|
|
221
237
|
return toSnakeIdentifier(value, "column");
|
|
222
238
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1014.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1014.619";
|
|
@@ -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.1014.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1014.619";
|