@oxygen-agent/cli 1.177.1 → 1.209.6
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/http-client.js +6 -4
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1144 -24
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +28 -6
- package/node_modules/@oxygen/shared/dist/billing.js +41 -0
- package/node_modules/@oxygen/shared/dist/budget-scopes.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/budget-scopes.js +9 -0
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +26 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/networks.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/networks.js +25 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +9 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +91 -1
- package/node_modules/@oxygen/shared/dist/sequences.js +288 -15
- package/node_modules/@oxygen/shared/dist/signup-lead-webhook.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-webhook.js +78 -0
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/sql-error.js +15 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/version.js +4 -2
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +2 -5
- package/node_modules/@oxygen/workflows/dist/index.d.ts +23 -0
- package/node_modules/@oxygen/workflows/dist/index.js +199 -24
- package/oxygen.js +2 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// skipcq: JS-0271 — bin entry source; build chmod+x on dist/index.js
|
|
3
1
|
import { execFileSync } from "node:child_process";
|
|
4
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
3
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -9,7 +7,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
10
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
9
|
import { Command, Option } from "commander";
|
|
12
|
-
import { formatCellForDisplay, OXYGEN_VERSION, OxygenError, success, toFailure } from "@oxygen/shared";
|
|
10
|
+
import { formatCellForDisplay, formatPublicBudgetScopes, OXYGEN_VERSION, OxygenError, success, toFailure, } from "@oxygen/shared";
|
|
13
11
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
14
12
|
import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
|
|
15
13
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
@@ -374,6 +372,43 @@ function resolveCrmSetupMode(options) {
|
|
|
374
372
|
}
|
|
375
373
|
return options.live === true ? "live" : "dry_run";
|
|
376
374
|
}
|
|
375
|
+
function buildCrmObjectCreateBody(options) {
|
|
376
|
+
const slug = readOption(options.slug);
|
|
377
|
+
if (!slug) {
|
|
378
|
+
throw new OxygenError("missing_slug", "--slug is required.", { exitCode: 1 });
|
|
379
|
+
}
|
|
380
|
+
const displayName = readOption(options.displayName);
|
|
381
|
+
if (!displayName) {
|
|
382
|
+
throw new OxygenError("missing_display_name", "--display-name is required.", { exitCode: 1 });
|
|
383
|
+
}
|
|
384
|
+
const columns = options.columnsJson ? parseJsonArray(options.columnsJson) : [];
|
|
385
|
+
const identities = options.identitiesJson ? parseJsonArray(options.identitiesJson) : [];
|
|
386
|
+
return {
|
|
387
|
+
slug,
|
|
388
|
+
display_name: displayName,
|
|
389
|
+
columns,
|
|
390
|
+
identities,
|
|
391
|
+
mode: resolveCrmSetupMode(options),
|
|
392
|
+
...(readOption(options.singularName) ? { singular_name: readOption(options.singularName) } : {}),
|
|
393
|
+
...(readOption(options.pluralName) ? { plural_name: readOption(options.pluralName) } : {}),
|
|
394
|
+
...(readOption(options.labelColumn) ? { label_column: readOption(options.labelColumn) } : {}),
|
|
395
|
+
...(options.relationshipsJson ? { relationships: parseJsonArray(options.relationshipsJson) } : {}),
|
|
396
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function buildCrmObjectAddAttrBody(options) {
|
|
400
|
+
const column = readOption(options.columnJson)
|
|
401
|
+
? parseJsonObject(options.columnJson)
|
|
402
|
+
: null;
|
|
403
|
+
if (!column) {
|
|
404
|
+
throw new OxygenError("missing_column", "--column-json is required.", { exitCode: 1 });
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
column,
|
|
408
|
+
mode: resolveCrmSetupMode(options),
|
|
409
|
+
...(options.asIdentityJson ? { identity: parseJsonObject(options.asIdentityJson) } : {}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
377
412
|
function buildCrmAssertBody(object, options) {
|
|
378
413
|
return {
|
|
379
414
|
object,
|
|
@@ -382,6 +417,81 @@ function buildCrmAssertBody(object, options) {
|
|
|
382
417
|
mode: resolveCrmSetupMode(options),
|
|
383
418
|
};
|
|
384
419
|
}
|
|
420
|
+
function buildCrmSyncImportBody(provider, options) {
|
|
421
|
+
return {
|
|
422
|
+
provider,
|
|
423
|
+
object: options.object,
|
|
424
|
+
mode: resolveCrmSetupMode(options),
|
|
425
|
+
...(options.into ? { into: options.into } : {}),
|
|
426
|
+
...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
|
|
427
|
+
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
428
|
+
...(options.approved ? { approved: true } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function buildCrmSyncStatusPath(runId) {
|
|
432
|
+
const params = new URLSearchParams();
|
|
433
|
+
params.set("run_id", runId);
|
|
434
|
+
return `/api/cli/crm/sync/status?${params.toString()}`;
|
|
435
|
+
}
|
|
436
|
+
function buildCrmSyncLinksPath(object, rowId) {
|
|
437
|
+
const params = new URLSearchParams();
|
|
438
|
+
params.set("object", object);
|
|
439
|
+
params.set("row_id", rowId);
|
|
440
|
+
return `/api/cli/crm/sync/links?${params.toString()}`;
|
|
441
|
+
}
|
|
442
|
+
function buildCrmSearchBody(query, options) {
|
|
443
|
+
const objects = readCsvOption(options.objects);
|
|
444
|
+
const limit = readPositiveInt(options.limit);
|
|
445
|
+
return {
|
|
446
|
+
query,
|
|
447
|
+
...(objects.length > 0 ? { objects } : {}),
|
|
448
|
+
...(limit !== undefined ? { limit } : {}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function buildCrmMergeBody(object, survivorRowId, loserRowId, options) {
|
|
452
|
+
const mode = resolveCrmSetupMode(options);
|
|
453
|
+
if (mode === "live" && options.confirm !== true) {
|
|
454
|
+
throw new OxygenError("confirm_required", "crm merge --live requires --confirm after inspecting the dry-run field diff.", { exitCode: 1 });
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
object,
|
|
458
|
+
survivor_row_id: survivorRowId,
|
|
459
|
+
loser_row_id: loserRowId,
|
|
460
|
+
mode,
|
|
461
|
+
...(options.confirm === true ? { confirm: true } : {}),
|
|
462
|
+
...(options.fieldOverridesJson ? { field_overrides: parseJsonObject(options.fieldOverridesJson) } : {}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function buildNotetakerSetupBody(options) {
|
|
466
|
+
return {
|
|
467
|
+
mode: resolveNotetakerMode(options),
|
|
468
|
+
enabled: options.disabled === true ? false : true,
|
|
469
|
+
capture_mode: "invited_bot",
|
|
470
|
+
auto_record_scope: "manual",
|
|
471
|
+
...(readOption(options.botName) ? { bot_name: readOption(options.botName) } : {}),
|
|
472
|
+
...(readPositiveInt(options.defaultMaxMinutes) ? { default_max_minutes: readPositiveInt(options.defaultMaxMinutes) } : {}),
|
|
473
|
+
...(readPositiveNumber(options.defaultMaxCredits) ? { default_max_credits: readPositiveNumber(options.defaultMaxCredits) } : {}),
|
|
474
|
+
...(readPositiveInt(options.retentionDays) ? { retention_days: readPositiveInt(options.retentionDays) } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function buildNotetakerScheduleBody(meetingUrl, options) {
|
|
478
|
+
return {
|
|
479
|
+
mode: resolveNotetakerMode(options),
|
|
480
|
+
meeting_url: meetingUrl,
|
|
481
|
+
approved: options.approved === true,
|
|
482
|
+
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
483
|
+
...(readOption(options.joinAt) ? { join_at: readOption(options.joinAt) } : {}),
|
|
484
|
+
...(readPositiveInt(options.maxMinutes) ? { max_minutes: readPositiveInt(options.maxMinutes) } : {}),
|
|
485
|
+
...(readPositiveNumber(options.maxCredits) ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
486
|
+
...(readOption(options.crmLinksJson) ? { crm_links: parseJsonArray(readOption(options.crmLinksJson) ?? "") } : {}),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function resolveNotetakerMode(options) {
|
|
490
|
+
if (options.live === true && options.dryRun === true) {
|
|
491
|
+
throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
|
|
492
|
+
}
|
|
493
|
+
return options.live === true ? "live" : "dry_run";
|
|
494
|
+
}
|
|
385
495
|
function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
386
496
|
return {
|
|
387
497
|
object,
|
|
@@ -394,6 +504,122 @@ function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
|
394
504
|
mode: resolveCrmSetupMode(options),
|
|
395
505
|
};
|
|
396
506
|
}
|
|
507
|
+
function buildCrmActivityBody(object, rowId, options) {
|
|
508
|
+
const body = {
|
|
509
|
+
mode: resolveCrmSetupMode(options),
|
|
510
|
+
activity_type: options.type,
|
|
511
|
+
links: [{ object, row_id: rowId, role: options.role ?? "actor" }],
|
|
512
|
+
};
|
|
513
|
+
if (options.summary)
|
|
514
|
+
body.summary = options.summary;
|
|
515
|
+
if (options.body)
|
|
516
|
+
body.body = options.body;
|
|
517
|
+
if (options.channel)
|
|
518
|
+
body.channel = options.channel;
|
|
519
|
+
if (options.direction)
|
|
520
|
+
body.direction = options.direction;
|
|
521
|
+
if (options.occurredAt)
|
|
522
|
+
body.occurred_at = options.occurredAt;
|
|
523
|
+
if (options.sourceProvider)
|
|
524
|
+
body.source_provider = options.sourceProvider;
|
|
525
|
+
if (options.providerEventId)
|
|
526
|
+
body.provider_event_id = options.providerEventId;
|
|
527
|
+
if (options.metadataJson)
|
|
528
|
+
body.metadata = parseJsonObject(options.metadataJson);
|
|
529
|
+
return body;
|
|
530
|
+
}
|
|
531
|
+
function buildCrmActivityNoteBody(object, rowId, text, options) {
|
|
532
|
+
return {
|
|
533
|
+
mode: resolveCrmSetupMode(options),
|
|
534
|
+
activity_type: "note",
|
|
535
|
+
summary: text,
|
|
536
|
+
links: [{ object, row_id: rowId, role: "actor" }],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function buildCrmTimelinePath(object, rowId, options) {
|
|
540
|
+
const params = new URLSearchParams();
|
|
541
|
+
if (options.limit)
|
|
542
|
+
params.set("limit", options.limit);
|
|
543
|
+
if (options.cursor)
|
|
544
|
+
params.set("cursor", options.cursor);
|
|
545
|
+
const query = params.toString();
|
|
546
|
+
const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/timeline`;
|
|
547
|
+
return query ? `${base}?${query}` : base;
|
|
548
|
+
}
|
|
549
|
+
function buildCrmPipelinePath(options) {
|
|
550
|
+
const params = new URLSearchParams();
|
|
551
|
+
if (readOption(options.pipeline))
|
|
552
|
+
params.set("pipeline", readOption(options.pipeline));
|
|
553
|
+
if (options.limit)
|
|
554
|
+
params.set("limit", options.limit);
|
|
555
|
+
const query = params.toString();
|
|
556
|
+
return query ? `/api/cli/crm/pipeline?${query}` : "/api/cli/crm/pipeline";
|
|
557
|
+
}
|
|
558
|
+
function buildCrmDuplicatesPath(object, options) {
|
|
559
|
+
const params = new URLSearchParams();
|
|
560
|
+
if (options.limit)
|
|
561
|
+
params.set("limit", options.limit);
|
|
562
|
+
const query = params.toString();
|
|
563
|
+
const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/duplicates`;
|
|
564
|
+
return query ? `${base}?${query}` : base;
|
|
565
|
+
}
|
|
566
|
+
function buildCrmAutomationSetBody(templateId, options) {
|
|
567
|
+
if (options.armed === true && options.disarmed === true) {
|
|
568
|
+
throw new OxygenError("conflicting_flags", "Pass either --armed or --disarmed, not both.", { exitCode: 1 });
|
|
569
|
+
}
|
|
570
|
+
if (options.armed !== true && options.disarmed !== true) {
|
|
571
|
+
throw new OxygenError("missing_flag", "Pass --armed or --disarmed.", { exitCode: 1 });
|
|
572
|
+
}
|
|
573
|
+
const body = {
|
|
574
|
+
template_id: templateId,
|
|
575
|
+
armed: options.armed === true,
|
|
576
|
+
mode: resolveCrmSetupMode(options),
|
|
577
|
+
};
|
|
578
|
+
if (options.configJson)
|
|
579
|
+
body.config = parseJsonObject(options.configJson);
|
|
580
|
+
return body;
|
|
581
|
+
}
|
|
582
|
+
function buildCrmAutomationAuditPath(options) {
|
|
583
|
+
const params = new URLSearchParams();
|
|
584
|
+
if (readOption(options.templateId))
|
|
585
|
+
params.set("template_id", readOption(options.templateId));
|
|
586
|
+
if (options.limit)
|
|
587
|
+
params.set("limit", options.limit);
|
|
588
|
+
const query = params.toString();
|
|
589
|
+
return query ? `/api/cli/crm/automation/audit?${query}` : "/api/cli/crm/automation/audit";
|
|
590
|
+
}
|
|
591
|
+
function buildCrmListCreateBody(slug, options) {
|
|
592
|
+
const body = {
|
|
593
|
+
slug,
|
|
594
|
+
base_object: options.base,
|
|
595
|
+
kind: options.kind,
|
|
596
|
+
mode: resolveCrmSetupMode(options),
|
|
597
|
+
};
|
|
598
|
+
if (options.displayName)
|
|
599
|
+
body.display_name = options.displayName;
|
|
600
|
+
if (options.project)
|
|
601
|
+
body.project = options.project;
|
|
602
|
+
if (options.filterJson)
|
|
603
|
+
body.filter = parseJsonObject(options.filterJson);
|
|
604
|
+
return body;
|
|
605
|
+
}
|
|
606
|
+
function buildCrmListEntryBody(object, rowId, options) {
|
|
607
|
+
return {
|
|
608
|
+
object,
|
|
609
|
+
row_id: rowId,
|
|
610
|
+
mode: resolveCrmSetupMode(options),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function buildCrmListMembersPath(list, options) {
|
|
614
|
+
const params = new URLSearchParams();
|
|
615
|
+
if (options.limit)
|
|
616
|
+
params.set("limit", options.limit);
|
|
617
|
+
if (options.cursor)
|
|
618
|
+
params.set("cursor", options.cursor);
|
|
619
|
+
const query = params.toString();
|
|
620
|
+
const base = `/api/cli/crm/lists/${encodeURIComponent(list)}/members`;
|
|
621
|
+
return query ? `${base}?${query}` : base;
|
|
622
|
+
}
|
|
397
623
|
function parseCrmIdentityOption(value) {
|
|
398
624
|
const separator = value.indexOf("=");
|
|
399
625
|
if (separator <= 0 || separator === value.length - 1) {
|
|
@@ -416,6 +642,10 @@ function readSpecFileBody(path) {
|
|
|
416
642
|
throw error;
|
|
417
643
|
}
|
|
418
644
|
}
|
|
645
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
646
|
+
function isUuid(value) {
|
|
647
|
+
return UUID_PATTERN.test(value);
|
|
648
|
+
}
|
|
419
649
|
// Builds the `prompts` command tree (list/get/upsert/archive). The
|
|
420
650
|
// deprecated `templates` alias is the identical tree — same subcommands,
|
|
421
651
|
// options, request bodies, and routes — differing only in the parent command
|
|
@@ -450,7 +680,7 @@ function buildPromptTemplatesCommand(surface, description) {
|
|
|
450
680
|
.action(async (idOrSlug, options) => {
|
|
451
681
|
await handleAsyncAction(`${surface} get`, options, () => requestOxygen("/api/cli/templates/get", {
|
|
452
682
|
method: "POST",
|
|
453
|
-
body: idOrSlug
|
|
683
|
+
body: isUuid(idOrSlug)
|
|
454
684
|
? { id: idOrSlug }
|
|
455
685
|
: { slug: idOrSlug },
|
|
456
686
|
}));
|
|
@@ -809,6 +1039,24 @@ export function createProgram() {
|
|
|
809
1039
|
.option("--json", "Print a JSON envelope.")
|
|
810
1040
|
.action(async (options) => {
|
|
811
1041
|
await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
|
|
1042
|
+
}))
|
|
1043
|
+
.addCommand(new Command("get")
|
|
1044
|
+
.description("Show one support ticket with its message thread (staff only).")
|
|
1045
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
1046
|
+
.option("--json", "Print a JSON envelope.")
|
|
1047
|
+
.action(async (ticketId, options) => {
|
|
1048
|
+
await handleAsyncAction("support admin get", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`));
|
|
1049
|
+
}))
|
|
1050
|
+
.addCommand(new Command("reply")
|
|
1051
|
+
.description("Add a staff message to a support ticket without resolving it.")
|
|
1052
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
1053
|
+
.requiredOption("--body <body>", "Reply body.")
|
|
1054
|
+
.option("--json", "Print a JSON envelope.")
|
|
1055
|
+
.action(async (ticketId, options) => {
|
|
1056
|
+
await handleAsyncAction("support admin reply", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
body: { body: readOption(options.body) },
|
|
1059
|
+
}));
|
|
812
1060
|
}))
|
|
813
1061
|
.addCommand(new Command("resolve")
|
|
814
1062
|
.description("Resolve a support ticket and notify the opener (staff only).")
|
|
@@ -1004,6 +1252,50 @@ export function createProgram() {
|
|
|
1004
1252
|
body: { name },
|
|
1005
1253
|
}));
|
|
1006
1254
|
}));
|
|
1255
|
+
program
|
|
1256
|
+
.command("notetaker")
|
|
1257
|
+
.description("AI meeting notetaker setup, scheduling, and session inspection.")
|
|
1258
|
+
.addCommand(new Command("setup")
|
|
1259
|
+
.description("Configure the Recall-backed Oxygen Notetaker. Defaults to dry-run.")
|
|
1260
|
+
.option("--bot-name <name>", "Meeting bot display name. Defaults to Oxygen Notetaker.")
|
|
1261
|
+
.option("--disabled", "Save the notetaker as disabled when used with --live.")
|
|
1262
|
+
.option("--default-max-minutes <n>", "Default maximum meeting minutes to reserve. Defaults to 60.")
|
|
1263
|
+
.option("--default-max-credits <n>", "Default spend cap for live schedules.")
|
|
1264
|
+
.option("--retention-days <n>", "Transcript/artifact retention window. Defaults to 30.")
|
|
1265
|
+
.option("--dry-run", "Preview setup without writing settings.")
|
|
1266
|
+
.option("--live", "Apply notetaker settings.")
|
|
1267
|
+
.option("--json", "Print a JSON envelope.")
|
|
1268
|
+
.action(async (options) => {
|
|
1269
|
+
await handleAsyncAction("notetaker setup", options, () => requestOxygen("/api/cli/notetaker/setup", {
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
body: buildNotetakerSetupBody(options),
|
|
1272
|
+
}));
|
|
1273
|
+
}))
|
|
1274
|
+
.addCommand(new Command("schedule")
|
|
1275
|
+
.description("Schedule an invited meeting bot. Defaults to dry-run.")
|
|
1276
|
+
.argument("<meeting_url>", "Zoom, Google Meet, or Teams meeting URL.")
|
|
1277
|
+
.option("--title <title>", "Meeting title for the CRM timeline.")
|
|
1278
|
+
.option("--join-at <iso>", "ISO date-time for scheduled bot join.")
|
|
1279
|
+
.option("--max-minutes <n>", "Maximum meeting minutes to reserve.")
|
|
1280
|
+
.option("--max-credits <n>", "Required spend cap for live schedules.")
|
|
1281
|
+
.option("--crm-links-json <json>", "JSON array of CRM links: [{\"object\":\"companies\",\"row_id\":\"...\",\"role\":\"account\"}].")
|
|
1282
|
+
.option("--approved", "Approve the live paid Recall bot schedule.")
|
|
1283
|
+
.option("--dry-run", "Preview schedule and credit estimate without creating a bot.")
|
|
1284
|
+
.option("--live", "Create the Recall bot after approval and credit reservation.")
|
|
1285
|
+
.option("--json", "Print a JSON envelope.")
|
|
1286
|
+
.action(async (meetingUrl, options) => {
|
|
1287
|
+
await handleAsyncAction("notetaker schedule", options, () => requestOxygen("/api/cli/notetaker/schedule", {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
body: buildNotetakerScheduleBody(meetingUrl, options),
|
|
1290
|
+
}));
|
|
1291
|
+
}))
|
|
1292
|
+
.addCommand(new Command("status")
|
|
1293
|
+
.description("Get one notetaker session with artifacts and credit state.")
|
|
1294
|
+
.argument("<session_id>", "Notetaker session id.")
|
|
1295
|
+
.option("--json", "Print a JSON envelope.")
|
|
1296
|
+
.action(async (sessionId, options) => {
|
|
1297
|
+
await handleAsyncAction("notetaker status", options, () => requestOxygen(`/api/cli/notetaker/sessions/${encodeURIComponent(sessionId)}`));
|
|
1298
|
+
}));
|
|
1007
1299
|
program
|
|
1008
1300
|
.command("crm")
|
|
1009
1301
|
.description("Agent-native CRM object setup and metadata commands.")
|
|
@@ -1020,11 +1312,67 @@ export function createProgram() {
|
|
|
1020
1312
|
body: buildCrmSetupBody(options),
|
|
1021
1313
|
}));
|
|
1022
1314
|
}))
|
|
1023
|
-
.addCommand(
|
|
1024
|
-
|
|
1315
|
+
.addCommand(
|
|
1316
|
+
// `crm objects` lists (parent action runs when no subcommand matches);
|
|
1317
|
+
// `crm objects create` / `crm objects add-attr` create custom objects.
|
|
1318
|
+
new Command("objects")
|
|
1319
|
+
.description("List configured CRM objects, or create/extend custom objects.")
|
|
1025
1320
|
.option("--json", "Print a JSON envelope.")
|
|
1026
1321
|
.action(async (options) => {
|
|
1027
1322
|
await handleAsyncAction("crm objects", options, () => requestOxygen("/api/cli/crm/objects"));
|
|
1323
|
+
})
|
|
1324
|
+
.addCommand(new Command("create")
|
|
1325
|
+
.description("Create or repair a custom CRM object. Defaults to dry-run.")
|
|
1326
|
+
.requiredOption("--slug <slug>", "Object slug (snake_case), such as projects or tasks.")
|
|
1327
|
+
.requiredOption("--display-name <name>", "Human-readable object name, such as Projects.")
|
|
1328
|
+
.requiredOption("--columns-json <json>", "JSON array of column definitions {key,label,dataType,semanticType,...}.")
|
|
1329
|
+
.option("--identities-json <json>", "JSON array of identity definitions {columnKey,normalization,isPrimary?}.")
|
|
1330
|
+
.option("--relationships-json <json>", "JSON array of relationship definitions (not yet supported).")
|
|
1331
|
+
.option("--label-column <key>", "Column key to use as the record label. Defaults to the isRecordLabel column or the first column.")
|
|
1332
|
+
.option("--singular-name <name>", "Singular display name, such as Project.")
|
|
1333
|
+
.option("--plural-name <name>", "Plural display name, such as Projects.")
|
|
1334
|
+
.option("--project <project>", "Project id or slug for the created CRM table.")
|
|
1335
|
+
.option("--dry-run", "Preview the object plan without creating tables.")
|
|
1336
|
+
.option("--live", "Apply the object creation. Default is dry-run.")
|
|
1337
|
+
.option("--json", "Print a JSON envelope.")
|
|
1338
|
+
.action(async (options, command) => {
|
|
1339
|
+
// The `crm objects` parent declares its own --json + a default
|
|
1340
|
+
// action, so commander binds `objects create --json` to the parent
|
|
1341
|
+
// and options.json is undefined here. optsWithGlobals() reads --json
|
|
1342
|
+
// wherever it landed, so the envelope is emitted like every other
|
|
1343
|
+
// crm_* command instead of printing the bare result.
|
|
1344
|
+
await handleAsyncAction("crm object create", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen("/api/cli/crm/objects", {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
body: buildCrmObjectCreateBody(options),
|
|
1347
|
+
}));
|
|
1348
|
+
}))
|
|
1349
|
+
.addCommand(new Command("add-attr")
|
|
1350
|
+
.description("Add one attribute to an existing custom CRM object, optionally as an identity. Defaults to dry-run.")
|
|
1351
|
+
.argument("<object>", "Custom CRM object slug, such as projects or tasks. Standard objects are managed by `crm setup`.")
|
|
1352
|
+
.requiredOption("--column-json <json>", "JSON column definition {key,label,dataType,semanticType,...}.")
|
|
1353
|
+
.option("--as-identity-json <json>", "JSON identity definition {columnKey,normalization,isPrimary?} promoting the new column.")
|
|
1354
|
+
.option("--dry-run", "Preview the attribute plan without altering the table.")
|
|
1355
|
+
.option("--live", "Apply the attribute. Default is dry-run.")
|
|
1356
|
+
.option("--json", "Print a JSON envelope.")
|
|
1357
|
+
.action(async (object, options, command) => {
|
|
1358
|
+
// See `objects create` above: optsWithGlobals() recovers --json,
|
|
1359
|
+
// which the parent command otherwise swallows.
|
|
1360
|
+
await handleAsyncAction("crm object add-attr", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/attributes`, {
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
body: buildCrmObjectAddAttrBody(options),
|
|
1363
|
+
}));
|
|
1364
|
+
})))
|
|
1365
|
+
.addCommand(new Command("search")
|
|
1366
|
+
.description("Search CRM records by identity or record label.")
|
|
1367
|
+
.argument("<query>", "Domain, email, LinkedIn URL, or record name to search for.")
|
|
1368
|
+
.option("--objects <objects>", "Comma-separated CRM object slugs to search. Defaults to all configured objects.")
|
|
1369
|
+
.option("--limit <limit>", "Maximum records to return.")
|
|
1370
|
+
.option("--json", "Print a JSON envelope.")
|
|
1371
|
+
.action(async (query, options) => {
|
|
1372
|
+
await handleAsyncAction("crm search", options, () => requestOxygen("/api/cli/crm/records/search", {
|
|
1373
|
+
method: "POST",
|
|
1374
|
+
body: buildCrmSearchBody(query, options),
|
|
1375
|
+
}));
|
|
1028
1376
|
}))
|
|
1029
1377
|
.addCommand(new Command("assert")
|
|
1030
1378
|
.description("Create or update one CRM record by object identity. Defaults to dry-run.")
|
|
@@ -1065,6 +1413,55 @@ export function createProgram() {
|
|
|
1065
1413
|
method: "POST",
|
|
1066
1414
|
body: buildCrmRelationshipUpsertBody(object, rowId, options),
|
|
1067
1415
|
}));
|
|
1416
|
+
})))
|
|
1417
|
+
.addCommand(new Command("activity")
|
|
1418
|
+
.description("Log and read CRM record timeline activities.")
|
|
1419
|
+
.addCommand(new Command("log")
|
|
1420
|
+
.description("Log a timeline activity on a CRM record. Defaults to dry-run.")
|
|
1421
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1422
|
+
.argument("<row_id>", "CRM record row id.")
|
|
1423
|
+
.requiredOption("--type <type>", "Activity type, such as note, call, meeting_booked, stage_changed.")
|
|
1424
|
+
.option("--summary <summary>", "Short one-line summary of the activity.")
|
|
1425
|
+
.option("--body <body>", "Longer activity body or details.")
|
|
1426
|
+
.option("--channel <channel>", "Channel, such as email, linkedin, or phone.")
|
|
1427
|
+
.option("--direction <direction>", "inbound, outbound, internal, or system.")
|
|
1428
|
+
.option("--occurred-at <iso>", "ISO-8601 timestamp the activity occurred. Defaults to now.")
|
|
1429
|
+
.option("--role <role>", "Link role for the record. Defaults to actor.")
|
|
1430
|
+
.option("--source-provider <provider>", "Provider that produced the activity (for idempotency).")
|
|
1431
|
+
.option("--provider-event-id <id>", "Provider event id (dedupes redelivered events).")
|
|
1432
|
+
.option("--metadata-json <json>", "JSON object of extra activity metadata.")
|
|
1433
|
+
.option("--dry-run", "Preview the activity without writing it.")
|
|
1434
|
+
.option("--live", "Write the activity. Default is dry-run.")
|
|
1435
|
+
.option("--json", "Print a JSON envelope.")
|
|
1436
|
+
.action(async (object, rowId, options) => {
|
|
1437
|
+
await handleAsyncAction("crm activity log", options, () => requestOxygen("/api/cli/crm/activities", {
|
|
1438
|
+
method: "POST",
|
|
1439
|
+
body: buildCrmActivityBody(object, rowId, options),
|
|
1440
|
+
}));
|
|
1441
|
+
}))
|
|
1442
|
+
.addCommand(new Command("note")
|
|
1443
|
+
.description("Log a free-text note on a CRM record. Defaults to dry-run.")
|
|
1444
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1445
|
+
.argument("<row_id>", "CRM record row id.")
|
|
1446
|
+
.argument("<text>", "Note text.")
|
|
1447
|
+
.option("--dry-run", "Preview the note without writing it.")
|
|
1448
|
+
.option("--live", "Write the note. Default is dry-run.")
|
|
1449
|
+
.option("--json", "Print a JSON envelope.")
|
|
1450
|
+
.action(async (object, rowId, text, options) => {
|
|
1451
|
+
await handleAsyncAction("crm activity note", options, () => requestOxygen("/api/cli/crm/activities", {
|
|
1452
|
+
method: "POST",
|
|
1453
|
+
body: buildCrmActivityNoteBody(object, rowId, text, options),
|
|
1454
|
+
}));
|
|
1455
|
+
}))
|
|
1456
|
+
.addCommand(new Command("timeline")
|
|
1457
|
+
.description("Show a CRM record's activity timeline, newest first.")
|
|
1458
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1459
|
+
.argument("<row_id>", "CRM record row id.")
|
|
1460
|
+
.option("--limit <limit>", "Maximum activities to return.")
|
|
1461
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
1462
|
+
.option("--json", "Print a JSON envelope.")
|
|
1463
|
+
.action(async (object, rowId, options) => {
|
|
1464
|
+
await handleAsyncAction("crm activity timeline", options, () => requestOxygen(buildCrmTimelinePath(object, rowId, options)));
|
|
1068
1465
|
})))
|
|
1069
1466
|
.addCommand(new Command("describe")
|
|
1070
1467
|
.description("Describe one configured CRM object.")
|
|
@@ -1072,7 +1469,166 @@ export function createProgram() {
|
|
|
1072
1469
|
.option("--json", "Print a JSON envelope.")
|
|
1073
1470
|
.action(async (object, options) => {
|
|
1074
1471
|
await handleAsyncAction("crm describe", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}`));
|
|
1075
|
-
}))
|
|
1472
|
+
}))
|
|
1473
|
+
.addCommand(new Command("duplicates")
|
|
1474
|
+
.description("List ranked duplicate-candidate record pairs for one CRM object (identity-overlap + fuzzy-label). Read-only.")
|
|
1475
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1476
|
+
.option("--limit <limit>", "Maximum candidate pairs to return. Defaults to 25, max 100.")
|
|
1477
|
+
.option("--json", "Print a JSON envelope.")
|
|
1478
|
+
.action(async (object, options) => {
|
|
1479
|
+
await handleAsyncAction("crm duplicates", options, () => requestOxygen(buildCrmDuplicatesPath(object, options)));
|
|
1480
|
+
}))
|
|
1481
|
+
.addCommand(new Command("merge")
|
|
1482
|
+
.description("Merge two CRM records: survivor-wins-with-loser-fill field write + relink/tombstone. Defaults to dry-run; --live requires --confirm.")
|
|
1483
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
1484
|
+
.argument("<survivor_row_id>", "Row id of the record to KEEP (older record by default).")
|
|
1485
|
+
.argument("<loser_row_id>", "Row id of the record to MERGE AWAY and tombstone.")
|
|
1486
|
+
.option("--field-overrides-json <json>", "JSON object of resolved field values keyed by column key, overriding survivor-wins-with-loser-fill.")
|
|
1487
|
+
.option("--dry-run", "Preview the field-level diff and relink plan without writing. Default.")
|
|
1488
|
+
.option("--live", "Apply the merge. Requires --confirm.")
|
|
1489
|
+
.option("--confirm", "Confirm the live merge after inspecting the dry-run diff.")
|
|
1490
|
+
.option("--json", "Print a JSON envelope.")
|
|
1491
|
+
.action(async (object, survivorRowId, loserRowId, options) => {
|
|
1492
|
+
await handleAsyncAction("crm merge", options, () => requestOxygen("/api/cli/crm/merge", {
|
|
1493
|
+
method: "POST",
|
|
1494
|
+
body: buildCrmMergeBody(object, survivorRowId, loserRowId, options),
|
|
1495
|
+
}));
|
|
1496
|
+
}))
|
|
1497
|
+
.addCommand(new Command("pipeline")
|
|
1498
|
+
.description("Show the deals pipeline grouped by stage, with per-stage counts and amount totals.")
|
|
1499
|
+
.option("--pipeline <name>", "Filter to one pipeline, such as sales, expansion, or renewal.")
|
|
1500
|
+
.option("--limit <limit>", "Maximum deals to return per stage.")
|
|
1501
|
+
.option("--json", "Print a JSON envelope.")
|
|
1502
|
+
.action(async (options) => {
|
|
1503
|
+
await handleAsyncAction("crm pipeline", options, () => requestOxygen(buildCrmPipelinePath(options)));
|
|
1504
|
+
}))
|
|
1505
|
+
.addCommand(new Command("lists")
|
|
1506
|
+
.description("Static and dynamic CRM lists over companies, people, deals, and custom objects.")
|
|
1507
|
+
.addCommand(new Command("create")
|
|
1508
|
+
.description("Create a static or dynamic CRM list. Defaults to dry-run.")
|
|
1509
|
+
.argument("<slug>", "List slug, such as vip_accounts.")
|
|
1510
|
+
.requiredOption("--base <object>", "Base CRM object slug the list draws from, such as people.")
|
|
1511
|
+
.requiredOption("--kind <kind>", "List kind: static (materialized) or dynamic (filter-computed).")
|
|
1512
|
+
.option("--filter-json <json>", "Dynamic-list filter as a JSON FilterTree object.")
|
|
1513
|
+
.option("--display-name <name>", "Human-friendly list name.")
|
|
1514
|
+
.option("--project <project>", "Project id or slug for the backing list table.")
|
|
1515
|
+
.option("--dry-run", "Preview the list without creating it.")
|
|
1516
|
+
.option("--live", "Create the list. Default is dry-run.")
|
|
1517
|
+
.option("--json", "Print a JSON envelope.")
|
|
1518
|
+
.action(async (slug, options) => {
|
|
1519
|
+
await handleAsyncAction("crm lists create", options, () => requestOxygen("/api/cli/crm/lists", {
|
|
1520
|
+
method: "POST",
|
|
1521
|
+
body: buildCrmListCreateBody(slug, options),
|
|
1522
|
+
}));
|
|
1523
|
+
}))
|
|
1524
|
+
.addCommand(new Command("add")
|
|
1525
|
+
.description("Add a record to a static CRM list. Defaults to dry-run.")
|
|
1526
|
+
.argument("<list>", "List slug.")
|
|
1527
|
+
.argument("<object>", "Base CRM object slug, such as people.")
|
|
1528
|
+
.argument("<row_id>", "Base record row id.")
|
|
1529
|
+
.option("--dry-run", "Preview the add without writing it.")
|
|
1530
|
+
.option("--live", "Write the membership. Default is dry-run.")
|
|
1531
|
+
.option("--json", "Print a JSON envelope.")
|
|
1532
|
+
.action(async (list, object, rowId, options) => {
|
|
1533
|
+
await handleAsyncAction("crm lists add", options, () => requestOxygen(`/api/cli/crm/lists/${encodeURIComponent(list)}/entries`, {
|
|
1534
|
+
method: "POST",
|
|
1535
|
+
body: buildCrmListEntryBody(object, rowId, options),
|
|
1536
|
+
}));
|
|
1537
|
+
}))
|
|
1538
|
+
.addCommand(new Command("remove")
|
|
1539
|
+
.description("Remove a record from a static CRM list. Defaults to dry-run.")
|
|
1540
|
+
.argument("<list>", "List slug.")
|
|
1541
|
+
.argument("<object>", "Base CRM object slug, such as people.")
|
|
1542
|
+
.argument("<row_id>", "Base record row id.")
|
|
1543
|
+
.option("--dry-run", "Preview the removal without writing it.")
|
|
1544
|
+
.option("--live", "Tombstone the membership. Default is dry-run.")
|
|
1545
|
+
.option("--json", "Print a JSON envelope.")
|
|
1546
|
+
.action(async (list, object, rowId, options) => {
|
|
1547
|
+
await handleAsyncAction("crm lists remove", options, () => requestOxygen(`/api/cli/crm/lists/${encodeURIComponent(list)}/entries`, {
|
|
1548
|
+
method: "DELETE",
|
|
1549
|
+
body: buildCrmListEntryBody(object, rowId, options),
|
|
1550
|
+
}));
|
|
1551
|
+
}))
|
|
1552
|
+
.addCommand(new Command("members")
|
|
1553
|
+
.description("List a CRM list's members (the audience for sequence enrollment).")
|
|
1554
|
+
.argument("<list>", "List slug.")
|
|
1555
|
+
.option("--limit <limit>", "Maximum members to return.")
|
|
1556
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
1557
|
+
.option("--json", "Print a JSON envelope.")
|
|
1558
|
+
.action(async (list, options) => {
|
|
1559
|
+
await handleAsyncAction("crm lists members", options, () => requestOxygen(buildCrmListMembersPath(list, options)));
|
|
1560
|
+
}))
|
|
1561
|
+
.addCommand(new Command("ls")
|
|
1562
|
+
.description("List every configured CRM list.")
|
|
1563
|
+
.option("--json", "Print a JSON envelope.")
|
|
1564
|
+
.action(async (options) => {
|
|
1565
|
+
await handleAsyncAction("crm lists ls", options, () => requestOxygen("/api/cli/crm/lists"));
|
|
1566
|
+
})))
|
|
1567
|
+
.addCommand(new Command("automation")
|
|
1568
|
+
.description("Arm / disarm standing CRM automation templates and read the audit trail.")
|
|
1569
|
+
.addCommand(new Command("rules")
|
|
1570
|
+
.description("List the standing CRM automation templates and their armed state.")
|
|
1571
|
+
.option("--json", "Print a JSON envelope.")
|
|
1572
|
+
.action(async (options) => {
|
|
1573
|
+
await handleAsyncAction("crm automation rules", options, () => requestOxygen("/api/cli/crm/automation"));
|
|
1574
|
+
}))
|
|
1575
|
+
.addCommand(new Command("set")
|
|
1576
|
+
.description("Arm or disarm a standing CRM automation template. Defaults to dry-run.")
|
|
1577
|
+
.argument("<template-id>", "Template id, such as crm-lead-stage-router.")
|
|
1578
|
+
.option("--armed", "Arm the template (let bridged signals advance stages).")
|
|
1579
|
+
.option("--disarmed", "Disarm the template.")
|
|
1580
|
+
.option("--config-json <json>", "JSON object of per-template config.")
|
|
1581
|
+
.option("--dry-run", "Preview the change without writing it.")
|
|
1582
|
+
.option("--live", "Write the change. Default is dry-run.")
|
|
1583
|
+
.option("--json", "Print a JSON envelope.")
|
|
1584
|
+
.action(async (templateId, options) => {
|
|
1585
|
+
await handleAsyncAction("crm automation set", options, () => requestOxygen("/api/cli/crm/automation", {
|
|
1586
|
+
method: "POST",
|
|
1587
|
+
body: buildCrmAutomationSetBody(templateId, options),
|
|
1588
|
+
}));
|
|
1589
|
+
}))
|
|
1590
|
+
.addCommand(new Command("audit")
|
|
1591
|
+
.description("Show recent CRM automation rule changes, newest first.")
|
|
1592
|
+
.option("--template-id <id>", "Filter to one template id.")
|
|
1593
|
+
.option("--limit <limit>", "Maximum audit entries to return.")
|
|
1594
|
+
.option("--json", "Print a JSON envelope.")
|
|
1595
|
+
.action(async (options) => {
|
|
1596
|
+
await handleAsyncAction("crm automation audit", options, () => requestOxygen(buildCrmAutomationAuditPath(options)));
|
|
1597
|
+
})))
|
|
1598
|
+
.addCommand(new Command("sync")
|
|
1599
|
+
.description("Import records from a connected CRM (HubSpot/Attio) into Oxygen CRM objects, and inspect import runs and provider links.")
|
|
1600
|
+
.addCommand(new Command("import")
|
|
1601
|
+
.description("Import provider records into an Oxygen CRM object as a durable workflow run. Defaults to dry-run.")
|
|
1602
|
+
.argument("<provider>", "Connected CRM to import from: hubspot or attio.")
|
|
1603
|
+
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
1604
|
+
.option("--into <object>", "Oxygen CRM object slug to import into. Defaults to the natural mapping.")
|
|
1605
|
+
.option("--limit <n>", "Max records to pull this run.")
|
|
1606
|
+
.option("--max-credits <n>", "Volume + external-quota cap (BYOK reads cost 0 Oxygen credits but use your CRM API quota).")
|
|
1607
|
+
.option("--approved", "Confirm a live import after inspecting a dry run.")
|
|
1608
|
+
.option("--dry-run", "Preview the import without pulling. Default.")
|
|
1609
|
+
.option("--live", "Run the live import. Requires --approved and --max-credits.")
|
|
1610
|
+
.option("--json", "Print a JSON envelope.")
|
|
1611
|
+
.action(async (provider, options) => {
|
|
1612
|
+
await handleAsyncAction("crm sync import", options, () => requestOxygen("/api/cli/crm/sync/import", {
|
|
1613
|
+
method: "POST",
|
|
1614
|
+
body: buildCrmSyncImportBody(provider, options),
|
|
1615
|
+
}));
|
|
1616
|
+
}))
|
|
1617
|
+
.addCommand(new Command("status")
|
|
1618
|
+
.description("Show the status of a CRM import workflow run.")
|
|
1619
|
+
.argument("<run_id>", "Import workflow run id.")
|
|
1620
|
+
.option("--json", "Print a JSON envelope.")
|
|
1621
|
+
.action(async (runId, options) => {
|
|
1622
|
+
await handleAsyncAction("crm sync status", options, () => requestOxygen(buildCrmSyncStatusPath(runId)));
|
|
1623
|
+
}))
|
|
1624
|
+
.addCommand(new Command("links")
|
|
1625
|
+
.description("Show provider record links (HubSpot/Attio ids) mapped to one Oxygen CRM record.")
|
|
1626
|
+
.argument("<object>", "Oxygen CRM object slug, such as companies or people.")
|
|
1627
|
+
.argument("<row_id>", "Oxygen CRM record row id.")
|
|
1628
|
+
.option("--json", "Print a JSON envelope.")
|
|
1629
|
+
.action(async (object, rowId, options) => {
|
|
1630
|
+
await handleAsyncAction("crm sync links", options, () => requestOxygen(buildCrmSyncLinksPath(object, rowId)));
|
|
1631
|
+
})));
|
|
1076
1632
|
const tablesCommand = program
|
|
1077
1633
|
.command("tables")
|
|
1078
1634
|
.description("Tenant workspace table commands.")
|
|
@@ -1600,7 +2156,7 @@ export function createProgram() {
|
|
|
1600
2156
|
.description("Playbooks, strategies, campaigns, personas, and research notes.")
|
|
1601
2157
|
.addCommand(new Command("list")
|
|
1602
2158
|
.description("List workspace context assets.")
|
|
1603
|
-
.option("--type <type>", "Filter by playbook, strategy, campaign, positioning, persona, competitor, research_note, or other.")
|
|
2159
|
+
.option("--type <type>", "Filter by playbook, strategy, campaign, positioning, brand, voice, persona, competitor, research_note, message_playbook, or other.")
|
|
1604
2160
|
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
1605
2161
|
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
1606
2162
|
.option("--include-archived", "Include archived assets when no status filter is set.")
|
|
@@ -1621,7 +2177,7 @@ export function createProgram() {
|
|
|
1621
2177
|
.addCommand(new Command("upsert")
|
|
1622
2178
|
.description("Create or update a context asset.")
|
|
1623
2179
|
.option("--id <asset_id>", "Existing asset UUID to update. Omit to create.")
|
|
1624
|
-
.option("--type <type>", "Asset type. Defaults to other on create.")
|
|
2180
|
+
.option("--type <type>", "Asset type (positioning, brand, voice, persona, competitor, message_playbook, playbook, strategy, campaign, research_note, other). Defaults to other on create.")
|
|
1625
2181
|
.option("--title <title>", "Asset title. Required on create.")
|
|
1626
2182
|
.option("--status <status>", "draft, active, or archived.")
|
|
1627
2183
|
.option("--tags <csv>", "Comma-separated asset tags.")
|
|
@@ -1629,12 +2185,24 @@ export function createProgram() {
|
|
|
1629
2185
|
.option("--body <text>", "Asset body text.")
|
|
1630
2186
|
.option("--data-json <json>", "Flexible JSON object for structured asset data.")
|
|
1631
2187
|
.option("--asset-json <json>", "Full asset JSON object. CLI flags override matching fields.")
|
|
2188
|
+
.option("--default", "Pin as the canonical default for its type (auto-applied to copy generation). brand/voice/positioning defaults are injected into AI message columns.")
|
|
1632
2189
|
.option("--json", "Print a JSON envelope.")
|
|
1633
2190
|
.action(async (options) => {
|
|
1634
2191
|
await handleAsyncAction("context asset upsert", options, () => requestOxygen("/api/cli/context/assets/upsert", {
|
|
1635
2192
|
method: "POST",
|
|
1636
2193
|
body: buildContextAssetUpsertBody(options),
|
|
1637
2194
|
}));
|
|
2195
|
+
}))
|
|
2196
|
+
.addCommand(new Command("set-default")
|
|
2197
|
+
.description("Pin a context asset as the canonical default for its type. The canonical brand/voice/positioning docs are auto-applied to AI copy generation.")
|
|
2198
|
+
.argument("<asset_id>", "Context asset UUID.")
|
|
2199
|
+
.option("--off", "Unpin instead of pin (clears the canonical default for this type).")
|
|
2200
|
+
.option("--json", "Print a JSON envelope.")
|
|
2201
|
+
.action(async (assetId, options) => {
|
|
2202
|
+
await handleAsyncAction("context asset set-default", options, () => requestOxygen("/api/cli/context/assets/set-default", {
|
|
2203
|
+
method: "POST",
|
|
2204
|
+
body: { id: assetId, is_default: options.off ? false : true },
|
|
2205
|
+
}));
|
|
1638
2206
|
}))
|
|
1639
2207
|
.addCommand(new Command("archive")
|
|
1640
2208
|
.description("Archive a context asset.")
|
|
@@ -1790,10 +2358,15 @@ export function createProgram() {
|
|
|
1790
2358
|
}))
|
|
1791
2359
|
.addCommand(new Command("archive")
|
|
1792
2360
|
.description("Archive a saved blueprint (seed blueprints cannot be archived).")
|
|
1793
|
-
.argument("<
|
|
2361
|
+
.argument("<id_or_slug>", "Blueprint UUID or slug.")
|
|
1794
2362
|
.option("--json", "Print a JSON envelope.")
|
|
1795
|
-
.action(async (
|
|
1796
|
-
await handleAsyncAction("blueprints archive", options, () => requestOxygen("/api/cli/blueprints/archive", {
|
|
2363
|
+
.action(async (idOrSlug, options) => {
|
|
2364
|
+
await handleAsyncAction("blueprints archive", options, () => requestOxygen("/api/cli/blueprints/archive", {
|
|
2365
|
+
method: "POST",
|
|
2366
|
+
body: isUuid(idOrSlug)
|
|
2367
|
+
? { id: idOrSlug }
|
|
2368
|
+
: { slug: idOrSlug },
|
|
2369
|
+
}));
|
|
1797
2370
|
}))
|
|
1798
2371
|
.addCommand(new Command("share")
|
|
1799
2372
|
.description("Create a public share URL for a saved blueprint (oxygen-agent.com/b/<code>).")
|
|
@@ -2017,6 +2590,7 @@ export function createProgram() {
|
|
|
2017
2590
|
.option("--force", "Run even when the target cell already has a value.")
|
|
2018
2591
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
2019
2592
|
.option("--background", "Create a durable background table action run instead of executing synchronously.")
|
|
2593
|
+
.option("--approved", "Confirm the paid background run after inspecting a dry run or preview.")
|
|
2020
2594
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for a background run.")
|
|
2021
2595
|
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run. Defaults to 250 for AI columns and 50 otherwise.")
|
|
2022
2596
|
.option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
|
|
@@ -2075,6 +2649,7 @@ export function createProgram() {
|
|
|
2075
2649
|
...(options.force ? { force: true } : {}),
|
|
2076
2650
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
2077
2651
|
...(options.background ? { background: true } : {}),
|
|
2652
|
+
...(options.approved ? { approved: true } : {}),
|
|
2078
2653
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
2079
2654
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
2080
2655
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
@@ -2262,6 +2837,7 @@ export function createProgram() {
|
|
|
2262
2837
|
.option("--filter-json <json>", "Filter object or array for server-side row selection.")
|
|
2263
2838
|
.option("--force", "Run even when the target cell already has a value.")
|
|
2264
2839
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
2840
|
+
.option("--approved", "Confirm the paid table action run after inspecting a dry run or preview.")
|
|
2265
2841
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
2266
2842
|
.option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
|
|
2267
2843
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
@@ -2279,6 +2855,7 @@ export function createProgram() {
|
|
|
2279
2855
|
selection: readTableRunSelection(options),
|
|
2280
2856
|
...(options.force ? { force: true } : {}),
|
|
2281
2857
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
2858
|
+
...(options.approved ? { approved: true } : {}),
|
|
2282
2859
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
2283
2860
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
2284
2861
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
@@ -2904,7 +3481,8 @@ export function createProgram() {
|
|
|
2904
3481
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
2905
3482
|
}))
|
|
2906
3483
|
.addCommand(new Command("usage")
|
|
2907
|
-
.description("Show credit ledger events.")
|
|
3484
|
+
.description("Show credit ledger events or automation action usage.")
|
|
3485
|
+
.option("--meter <meter>", "credits or automation_actions. Defaults to credits.")
|
|
2908
3486
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
2909
3487
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
2910
3488
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -2969,13 +3547,46 @@ export function createProgram() {
|
|
|
2969
3547
|
...(readOption(options.description) ? { description: readOption(options.description) } : {}),
|
|
2970
3548
|
},
|
|
2971
3549
|
}));
|
|
3550
|
+
}))
|
|
3551
|
+
.addCommand(new Command("set-plan")
|
|
3552
|
+
.description("Create or update an off-Stripe custom plan for an organization (staff only).")
|
|
3553
|
+
.requiredOption("--organization-id <id>", "Organization id to put on the custom plan.")
|
|
3554
|
+
.requiredOption("--credits <n>", "Monthly managed credits included in the plan.")
|
|
3555
|
+
.option("--org-id <id>", "Alias for --organization-id.")
|
|
3556
|
+
.option("--name <text>", "Display name for the plan, e.g. \"Custom Contract - $250/mo\".")
|
|
3557
|
+
.option("--price <usd>", "Monthly contract price in USD (recorded for display; not charged off-Stripe).")
|
|
3558
|
+
.option("--weekly-credits <n>", "Weekly spend throttle. Defaults to the monthly credits (no throttle).")
|
|
3559
|
+
.option("--rollover <n>", "Max balance credits roll over to. Defaults to the monthly credits.")
|
|
3560
|
+
.option("--base-tier <tier>", "Base pricing tier to inherit feature flags from. Defaults to growth_250.")
|
|
3561
|
+
.option("--byok", "Enable bring-your-own-key integrations for the plan.")
|
|
3562
|
+
.option("--note <text>", "Internal note stored on the contract.")
|
|
3563
|
+
.option("--json", "Print a JSON envelope.")
|
|
3564
|
+
.action(async (options) => {
|
|
3565
|
+
await handleAsyncAction("billing set-plan", options, () => requestOxygen("/api/cli/billing/set-plan", {
|
|
3566
|
+
method: "POST",
|
|
3567
|
+
body: {
|
|
3568
|
+
organization_id: readOption(options.organizationId) ?? readOption(options.orgId),
|
|
3569
|
+
monthly_credits: readPositiveNumber(options.credits),
|
|
3570
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
3571
|
+
...(readOption(options.price) ? { price: readPositiveNumber(options.price) } : {}),
|
|
3572
|
+
...(readOption(options.weeklyCredits)
|
|
3573
|
+
? { weekly_credits_limit: readPositiveNumber(options.weeklyCredits) }
|
|
3574
|
+
: {}),
|
|
3575
|
+
...(readOption(options.rollover)
|
|
3576
|
+
? { rollover_cap: readPositiveNumber(options.rollover) }
|
|
3577
|
+
: {}),
|
|
3578
|
+
...(readOption(options.baseTier) ? { base_tier: readOption(options.baseTier) } : {}),
|
|
3579
|
+
...(options.byok ? { byok: true } : {}),
|
|
3580
|
+
...(readOption(options.note) ? { note: readOption(options.note) } : {}),
|
|
3581
|
+
},
|
|
3582
|
+
}));
|
|
2972
3583
|
}));
|
|
2973
3584
|
program
|
|
2974
3585
|
.command("budget")
|
|
2975
3586
|
.description("Standing credit caps (org/table daily/monthly hard-blocks) beyond per-run max_credits.")
|
|
2976
3587
|
.addCommand(new Command("list")
|
|
2977
3588
|
.description("List the organization's standing budget policies.")
|
|
2978
|
-
.option("--scope <scope>",
|
|
3589
|
+
.option("--scope <scope>", `Filter by scope: ${formatPublicBudgetScopes()}.`)
|
|
2979
3590
|
.option("--status <status>", "Filter by status. Defaults to all.")
|
|
2980
3591
|
.option("--json", "Print a JSON envelope.")
|
|
2981
3592
|
.action(async (options) => {
|
|
@@ -2991,7 +3602,7 @@ export function createProgram() {
|
|
|
2991
3602
|
}))
|
|
2992
3603
|
.addCommand(new Command("set")
|
|
2993
3604
|
.description("Set, raise, or lower a standing credit cap (idempotent per scope+window).")
|
|
2994
|
-
.requiredOption("--scope <scope>",
|
|
3605
|
+
.requiredOption("--scope <scope>", formatPublicBudgetScopes())
|
|
2995
3606
|
.requiredOption("--window <window>", "per_run, daily, or monthly.")
|
|
2996
3607
|
.requiredOption("--max-credits <credits>", "Cap in Oxygen credits.")
|
|
2997
3608
|
.option("--scope-id <id>", "Table id/slug for --scope table (or other scope id). Omit for a scope-wide cap.")
|
|
@@ -3732,6 +4343,8 @@ export function createProgram() {
|
|
|
3732
4343
|
.option("--live", "Execute the action live. Default is dry-run.")
|
|
3733
4344
|
.option("--dry-run", "Force dry-run (no provider call).")
|
|
3734
4345
|
.option("--mode <mode>", "'live' or 'dry_run'. Overridden by --live/--dry-run if provided.")
|
|
4346
|
+
.option("--approved", "Required for live actions after inspecting dry-run output.")
|
|
4347
|
+
.option("--max-credits <n>", "Required positive credit cap for live actions.")
|
|
3735
4348
|
.option("--json", "Print a JSON envelope.")
|
|
3736
4349
|
.action(async (integrationId, actionSlug, options) => {
|
|
3737
4350
|
await handleAsyncAction("integrations run", options, () => {
|
|
@@ -3739,6 +4352,7 @@ export function createProgram() {
|
|
|
3739
4352
|
? parseJsonObject(readOption(options.input))
|
|
3740
4353
|
: {};
|
|
3741
4354
|
const mode = resolveComposioRunMode(options);
|
|
4355
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3742
4356
|
return requestOxygen("/api/cli/integrations/composio/run", {
|
|
3743
4357
|
method: "POST",
|
|
3744
4358
|
body: {
|
|
@@ -3746,6 +4360,8 @@ export function createProgram() {
|
|
|
3746
4360
|
action_slug: actionSlug,
|
|
3747
4361
|
arguments: args,
|
|
3748
4362
|
mode,
|
|
4363
|
+
...(options.approved ? { approved: true } : {}),
|
|
4364
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
3749
4365
|
},
|
|
3750
4366
|
});
|
|
3751
4367
|
});
|
|
@@ -3857,10 +4473,19 @@ export function createProgram() {
|
|
|
3857
4473
|
program.addCommand(new Command("inbox")
|
|
3858
4474
|
.description("Unified inbox (unibox): LinkedIn conversations and (--channel email) the fleet's email conversations synced from Zapmail Zapbox. Scan, read threads, and reply.")
|
|
3859
4475
|
.addCommand(new Command("list")
|
|
3860
|
-
.description("List conversations across all connected accounts, newest first. --channel email lists the Zapbox-synced email inbox.")
|
|
4476
|
+
.description("List conversations across all connected accounts, newest first. --channel email lists the Zapbox-synced email inbox; filter it by status, campaign, mailbox provider/domain, and Primary/Others bucket.")
|
|
3861
4477
|
.option("--channel <channel>", "Inbox channel: linkedin (default) or email.")
|
|
3862
4478
|
.option("--account <id>", "LinkedIn only: filter to one sender account (sender id, connection id, or Unipile account id).")
|
|
3863
4479
|
.option("--unread", "Only show conversations with unread messages.")
|
|
4480
|
+
.option("--responses-only", "Email only: only conversations with an inbound reply (never sent-only threads).")
|
|
4481
|
+
.option("--bucket <bucket>", "Email only: primary or others (superseded by --segment).")
|
|
4482
|
+
.option("--segment <segment>", "Email only: top-tab folder — primary, others, sent, warmup, or dmarc.")
|
|
4483
|
+
.option("--status <keys>", "Email only: comma-separated status keys (e.g. interested,meeting_booked).")
|
|
4484
|
+
.option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) ids.")
|
|
4485
|
+
.option("--provider <providers>", "Email only: comma-separated providers (google,microsoft).")
|
|
4486
|
+
.option("--domain <domains>", "Email only: comma-separated counterpart domains to include.")
|
|
4487
|
+
.option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
|
|
4488
|
+
.option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
|
|
3864
4489
|
.option("--search <text>", "Filter by attendee name or last-message text.")
|
|
3865
4490
|
.option("--include-archived", "Include archived conversations.")
|
|
3866
4491
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
@@ -3876,6 +4501,22 @@ export function createProgram() {
|
|
|
3876
4501
|
params.set("account", account);
|
|
3877
4502
|
if (options.unread)
|
|
3878
4503
|
params.set("unread", "true");
|
|
4504
|
+
if (options.responsesOnly)
|
|
4505
|
+
params.set("responses_only", "true");
|
|
4506
|
+
for (const [flag, key] of [
|
|
4507
|
+
["bucket", "bucket"],
|
|
4508
|
+
["segment", "segment"],
|
|
4509
|
+
["status", "status"],
|
|
4510
|
+
["sequenceId", "sequence_id"],
|
|
4511
|
+
["provider", "provider"],
|
|
4512
|
+
["domain", "domain"],
|
|
4513
|
+
["excludeDomain", "exclude_domain"],
|
|
4514
|
+
["mailboxId", "mailbox_id"],
|
|
4515
|
+
]) {
|
|
4516
|
+
const value = readOption(options[flag]);
|
|
4517
|
+
if (value)
|
|
4518
|
+
params.set(key, value);
|
|
4519
|
+
}
|
|
3879
4520
|
const search = readOption(options.search);
|
|
3880
4521
|
if (search)
|
|
3881
4522
|
params.set("search", search);
|
|
@@ -3913,28 +4554,36 @@ export function createProgram() {
|
|
|
3913
4554
|
.requiredOption("--text <message>", "Reply text to send.")
|
|
3914
4555
|
.option("--channel <channel>", "Inbox channel: linkedin (default) or email.")
|
|
3915
4556
|
.option("--approved", "Approve and send the message. Without this flag, returns a preview only.")
|
|
4557
|
+
.option("--draft-id <id>", "Email only: when approving an AI reply-agent draft, its id (marks it sent on success).")
|
|
3916
4558
|
.option("--json", "Print a JSON envelope.")
|
|
3917
4559
|
.action(async (conversation, options) => {
|
|
3918
4560
|
await handleAsyncAction("inbox send", options, () => {
|
|
3919
4561
|
const channel = readOption(options.channel);
|
|
4562
|
+
const draftId = readOption(options.draftId);
|
|
3920
4563
|
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/send`, {
|
|
3921
4564
|
method: "POST",
|
|
3922
4565
|
body: {
|
|
3923
4566
|
text: readOption(options.text),
|
|
3924
4567
|
...(channel ? { channel } : {}),
|
|
3925
4568
|
...(options.approved ? { approved: true } : {}),
|
|
4569
|
+
...(draftId ? { draft_id: draftId } : {}),
|
|
3926
4570
|
},
|
|
3927
4571
|
});
|
|
3928
4572
|
});
|
|
3929
4573
|
}))
|
|
3930
4574
|
.addCommand(new Command("mark-read")
|
|
3931
4575
|
.description("Mark a conversation and all its messages as read.")
|
|
3932
|
-
.argument("<conversation>", "Conversation id
|
|
4576
|
+
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
4577
|
+
.option("--channel <channel>", "Inbox channel: linkedin (default) or email.")
|
|
3933
4578
|
.option("--json", "Print a JSON envelope.")
|
|
3934
4579
|
.action(async (conversation, options) => {
|
|
3935
|
-
await handleAsyncAction("inbox mark-read", options, () =>
|
|
3936
|
-
|
|
3937
|
-
|
|
4580
|
+
await handleAsyncAction("inbox mark-read", options, () => {
|
|
4581
|
+
const channel = readOption(options.channel);
|
|
4582
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/read`, {
|
|
4583
|
+
method: "POST",
|
|
4584
|
+
body: channel ? { channel } : {},
|
|
4585
|
+
});
|
|
4586
|
+
});
|
|
3938
4587
|
}))
|
|
3939
4588
|
.addCommand(new Command("analyze")
|
|
3940
4589
|
.description("Run the default analysis on an email conversation now: sentiment + interest category + a drafted reply (Oxygen's default model). The worker also does this automatically on inbound mail.")
|
|
@@ -3977,7 +4626,167 @@ export function createProgram() {
|
|
|
3977
4626
|
body.message_limit = Number(messageLimit);
|
|
3978
4627
|
return requestOxygen("/api/cli/inbox/sync", { method: "POST", body });
|
|
3979
4628
|
});
|
|
3980
|
-
}))
|
|
4629
|
+
}))
|
|
4630
|
+
.addCommand(new Command("status")
|
|
4631
|
+
.description("Set an email conversation's status (the Instantly-style tier). A manual override that locks out AI re-classification.")
|
|
4632
|
+
.argument("<conversation>", "Conversation id or Zapbox thread id.")
|
|
4633
|
+
.argument("<status>", "A status label key (e.g. interested, meeting_booked, won, not_interested).")
|
|
4634
|
+
.option("--json", "Print a JSON envelope.")
|
|
4635
|
+
.action(async (conversation, status, options) => {
|
|
4636
|
+
await handleAsyncAction("inbox status", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/status`, {
|
|
4637
|
+
method: "POST",
|
|
4638
|
+
body: { status },
|
|
4639
|
+
}));
|
|
4640
|
+
}))
|
|
4641
|
+
.addCommand(new Command("labels")
|
|
4642
|
+
.description("Manage inbox status labels (the Instantly-style system set + custom labels).")
|
|
4643
|
+
.addCommand(new Command("list")
|
|
4644
|
+
.description("List status labels.")
|
|
4645
|
+
.option("--include-archived", "Include archived custom labels.")
|
|
4646
|
+
.option("--json", "Print a JSON envelope.")
|
|
4647
|
+
.action(async (options) => {
|
|
4648
|
+
await handleAsyncAction("inbox labels list", options, () => requestOxygen(`/api/cli/inbox/labels${options.includeArchived ? "?include_archived=true" : ""}`));
|
|
4649
|
+
}))
|
|
4650
|
+
.addCommand(new Command("create")
|
|
4651
|
+
.description("Create a custom status label.")
|
|
4652
|
+
.argument("<name>", "Display name.")
|
|
4653
|
+
.option("--color <hex>", "Hex color (e.g. #22c55e).")
|
|
4654
|
+
.option("--bucket <bucket>", "primary (default) or others.")
|
|
4655
|
+
.option("--json", "Print a JSON envelope.")
|
|
4656
|
+
.action(async (name, options) => {
|
|
4657
|
+
await handleAsyncAction("inbox label create", options, () => {
|
|
4658
|
+
const color = readOption(options.color);
|
|
4659
|
+
const bucket = readOption(options.bucket);
|
|
4660
|
+
return requestOxygen("/api/cli/inbox/labels", {
|
|
4661
|
+
method: "POST",
|
|
4662
|
+
body: { name, ...(color ? { color } : {}), ...(bucket ? { bucket } : {}) },
|
|
4663
|
+
});
|
|
4664
|
+
});
|
|
4665
|
+
}))
|
|
4666
|
+
.addCommand(new Command("update")
|
|
4667
|
+
.description("Update a status label's name, color, or bucket.")
|
|
4668
|
+
.argument("<key>", "The label key.")
|
|
4669
|
+
.option("--name <name>", "New display name.")
|
|
4670
|
+
.option("--color <hex>", "New hex color.")
|
|
4671
|
+
.option("--bucket <bucket>", "New bucket (primary or others).")
|
|
4672
|
+
.option("--json", "Print a JSON envelope.")
|
|
4673
|
+
.action(async (key, options) => {
|
|
4674
|
+
await handleAsyncAction("inbox label update", options, () => {
|
|
4675
|
+
const name = readOption(options.name);
|
|
4676
|
+
const color = readOption(options.color);
|
|
4677
|
+
const bucket = readOption(options.bucket);
|
|
4678
|
+
return requestOxygen(`/api/cli/inbox/labels/${encodeURIComponent(key)}`, {
|
|
4679
|
+
method: "PATCH",
|
|
4680
|
+
body: { ...(name ? { name } : {}), ...(color ? { color } : {}), ...(bucket ? { bucket } : {}) },
|
|
4681
|
+
});
|
|
4682
|
+
});
|
|
4683
|
+
}))
|
|
4684
|
+
.addCommand(new Command("delete")
|
|
4685
|
+
.description("Archive a custom status label (system labels and in-use labels are rejected).")
|
|
4686
|
+
.argument("<key>", "The custom label key.")
|
|
4687
|
+
.option("--json", "Print a JSON envelope.")
|
|
4688
|
+
.action(async (key, options) => {
|
|
4689
|
+
await handleAsyncAction("inbox label archive", options, () => requestOxygen(`/api/cli/inbox/labels/${encodeURIComponent(key)}`, { method: "DELETE" }));
|
|
4690
|
+
})))
|
|
4691
|
+
.addCommand(new Command("drafts")
|
|
4692
|
+
.description("The AI reply-agent draft queue (the approve-before-send review queue).")
|
|
4693
|
+
.addCommand(new Command("list")
|
|
4694
|
+
.description("List drafts awaiting review (queued + edited by default).")
|
|
4695
|
+
.option("--status <keys>", "Comma-separated draft statuses (queued,edited,sent,rejected).")
|
|
4696
|
+
.option("--sequence-id <id>", "Filter to one campaign (sequence) id.")
|
|
4697
|
+
.option("--limit <n>", "Maximum drafts to return.")
|
|
4698
|
+
.option("--json", "Print a JSON envelope.")
|
|
4699
|
+
.action(async (options) => {
|
|
4700
|
+
await handleAsyncAction("inbox drafts list", options, () => {
|
|
4701
|
+
const params = new URLSearchParams();
|
|
4702
|
+
const status = readOption(options.status);
|
|
4703
|
+
if (status)
|
|
4704
|
+
params.set("status", status);
|
|
4705
|
+
const sequenceId = readOption(options.sequenceId);
|
|
4706
|
+
if (sequenceId)
|
|
4707
|
+
params.set("sequence_id", sequenceId);
|
|
4708
|
+
const limit = readOption(options.limit);
|
|
4709
|
+
if (limit)
|
|
4710
|
+
params.set("limit", limit);
|
|
4711
|
+
const suffix = params.toString();
|
|
4712
|
+
return requestOxygen(`/api/cli/inbox/drafts${suffix ? `?${suffix}` : ""}`);
|
|
4713
|
+
});
|
|
4714
|
+
}))
|
|
4715
|
+
.addCommand(new Command("edit")
|
|
4716
|
+
.description("Edit an open draft's body before approval. Does not send.")
|
|
4717
|
+
.argument("<draft>", "The draft id.")
|
|
4718
|
+
.requiredOption("--text <message>", "The new draft body.")
|
|
4719
|
+
.option("--json", "Print a JSON envelope.")
|
|
4720
|
+
.action(async (draft, options) => {
|
|
4721
|
+
await handleAsyncAction("inbox draft update", options, () => requestOxygen(`/api/cli/inbox/drafts/${encodeURIComponent(draft)}`, {
|
|
4722
|
+
method: "PATCH",
|
|
4723
|
+
body: { text: readOption(options.text) },
|
|
4724
|
+
}));
|
|
4725
|
+
}))
|
|
4726
|
+
.addCommand(new Command("reject")
|
|
4727
|
+
.description("Reject an open draft so it leaves the queue. Does not send.")
|
|
4728
|
+
.argument("<draft>", "The draft id.")
|
|
4729
|
+
.option("--reason <text>", "Optional reason.")
|
|
4730
|
+
.option("--json", "Print a JSON envelope.")
|
|
4731
|
+
.action(async (draft, options) => {
|
|
4732
|
+
await handleAsyncAction("inbox draft reject", options, () => {
|
|
4733
|
+
const reason = readOption(options.reason);
|
|
4734
|
+
return requestOxygen(`/api/cli/inbox/drafts/${encodeURIComponent(draft)}/reject`, {
|
|
4735
|
+
method: "POST",
|
|
4736
|
+
body: reason ? { reason } : {},
|
|
4737
|
+
});
|
|
4738
|
+
});
|
|
4739
|
+
})))
|
|
4740
|
+
.addCommand(new Command("reply-agent")
|
|
4741
|
+
.description("The draft-only AI reply agent (the 'AI Sales Agent'): auto-classifies + auto-drafts replies into the review queue. It NEVER auto-sends.")
|
|
4742
|
+
.addCommand(new Command("get")
|
|
4743
|
+
.description("Show the reply-agent config.")
|
|
4744
|
+
.option("--json", "Print a JSON envelope.")
|
|
4745
|
+
.action(async (options) => {
|
|
4746
|
+
await handleAsyncAction("inbox reply-agent get", options, () => requestOxygen("/api/cli/inbox/reply-agent"));
|
|
4747
|
+
}))
|
|
4748
|
+
.addCommand(new Command("set")
|
|
4749
|
+
.description("Configure the reply agent (enable, persona/tone, targets). It drafts into the queue; a human approves+sends.")
|
|
4750
|
+
.option("--enabled", "Enable the agent.")
|
|
4751
|
+
.option("--disabled", "Disable the agent.")
|
|
4752
|
+
.option("--persona <text>", "Who the agent is.")
|
|
4753
|
+
.option("--tone <text>", "Reply tone.")
|
|
4754
|
+
.option("--instructions <text>", "Extra drafting instructions.")
|
|
4755
|
+
.option("--signature <text>", "Signature appended to drafts.")
|
|
4756
|
+
.option("--target-statuses <keys>", "Comma-separated statuses to draft for (empty = all draftable).")
|
|
4757
|
+
.option("--target-sequence-ids <ids>", "Comma-separated campaign ids to draft for (empty = all).")
|
|
4758
|
+
.option("--max-drafts-per-day <n>", "Per-day draft cap.")
|
|
4759
|
+
.option("--json", "Print a JSON envelope.")
|
|
4760
|
+
.action(async (options) => {
|
|
4761
|
+
await handleAsyncAction("inbox reply-agent set", options, () => {
|
|
4762
|
+
const body = {};
|
|
4763
|
+
if (options.enabled)
|
|
4764
|
+
body.enabled = true;
|
|
4765
|
+
if (options.disabled)
|
|
4766
|
+
body.enabled = false;
|
|
4767
|
+
// Presence (flag passed) — not readOption — decides inclusion, so an
|
|
4768
|
+
// absent flag leaves the field untouched rather than clearing it.
|
|
4769
|
+
const opts = options;
|
|
4770
|
+
for (const [flag, key] of [
|
|
4771
|
+
["persona", "persona"],
|
|
4772
|
+
["tone", "tone"],
|
|
4773
|
+
["instructions", "instructions"],
|
|
4774
|
+
["signature", "signature"],
|
|
4775
|
+
]) {
|
|
4776
|
+
if (opts[flag] !== undefined)
|
|
4777
|
+
body[key] = opts[flag];
|
|
4778
|
+
}
|
|
4779
|
+
if (options.targetStatuses !== undefined) {
|
|
4780
|
+
body.target_statuses = options.targetStatuses.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4781
|
+
}
|
|
4782
|
+
if (options.targetSequenceIds !== undefined) {
|
|
4783
|
+
body.target_sequence_ids = options.targetSequenceIds.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4784
|
+
}
|
|
4785
|
+
if (options.maxDraftsPerDay !== undefined)
|
|
4786
|
+
body.max_drafts_per_day = Number(options.maxDraftsPerDay);
|
|
4787
|
+
return requestOxygen("/api/cli/inbox/reply-agent", { method: "POST", body });
|
|
4788
|
+
});
|
|
4789
|
+
}))));
|
|
3981
4790
|
program.addCommand(new Command("sequences")
|
|
3982
4791
|
.description("Multichannel outreach sequences: one enrollment per lead spans LinkedIn + email over a journey. LinkedIn steps dispatch natively (rate-limited, credit-capped); email steps send natively or place/move/stop the lead in a bound Instantly campaign (BYOK). Cross-channel reply-stop is intrinsic. A LinkedIn-only sequence behaves exactly like the original sequencer.")
|
|
3983
4792
|
.addCommand(new Command("list")
|
|
@@ -4033,6 +4842,9 @@ export function createProgram() {
|
|
|
4033
4842
|
.option("--email-connection <id>", "Instantly connection id for the email track. Defaults to the org's active Instantly connection.")
|
|
4034
4843
|
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences) compiled to an Instantly campaign on start.")
|
|
4035
4844
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
|
|
4845
|
+
.option("--max-live-sends <n>", "Send ceiling for the email track (positive integer). Required to start an email sequence live.")
|
|
4846
|
+
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
|
|
4847
|
+
.option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
|
|
4036
4848
|
.option("--json", "Print a JSON envelope.")
|
|
4037
4849
|
.action(async (options) => {
|
|
4038
4850
|
await handleAsyncAction("sequences create", options, () => {
|
|
@@ -4044,6 +4856,8 @@ export function createProgram() {
|
|
|
4044
4856
|
const senders = readCsvOption(options.senders);
|
|
4045
4857
|
const email = readCampaignEmailBinding(options);
|
|
4046
4858
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
4859
|
+
const maxLiveSends = readPositiveInteger(options.maxLiveSends);
|
|
4860
|
+
const settings = readSequenceSettings(options);
|
|
4047
4861
|
return requestOxygen("/api/cli/sequences", {
|
|
4048
4862
|
method: "POST",
|
|
4049
4863
|
body: {
|
|
@@ -4056,6 +4870,8 @@ export function createProgram() {
|
|
|
4056
4870
|
...(readOption(options.urlColumn) ? { linkedin_url_column_key: readOption(options.urlColumn) } : {}),
|
|
4057
4871
|
...(email ? { email } : {}),
|
|
4058
4872
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
4873
|
+
...(maxLiveSends !== undefined ? { max_live_sends: maxLiveSends } : {}),
|
|
4874
|
+
...(settings ? { settings } : {}),
|
|
4059
4875
|
},
|
|
4060
4876
|
});
|
|
4061
4877
|
});
|
|
@@ -4072,6 +4888,9 @@ export function createProgram() {
|
|
|
4072
4888
|
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences).")
|
|
4073
4889
|
.option("--clear-email", "Remove the email binding from the sequence (draft only).")
|
|
4074
4890
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
|
|
4891
|
+
.option("--max-live-sends <n>", "Send ceiling for the email track (positive integer). Required to start an email sequence live.")
|
|
4892
|
+
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
|
|
4893
|
+
.option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
|
|
4075
4894
|
.option("--json", "Print a JSON envelope.")
|
|
4076
4895
|
.action(async (sequence, options) => {
|
|
4077
4896
|
await handleAsyncAction("sequences update", options, () => {
|
|
@@ -4092,6 +4911,12 @@ export function createProgram() {
|
|
|
4092
4911
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
4093
4912
|
if (maxCredits !== undefined)
|
|
4094
4913
|
body.max_credits = maxCredits;
|
|
4914
|
+
const maxLiveSends = readPositiveInteger(options.maxLiveSends);
|
|
4915
|
+
if (maxLiveSends !== undefined)
|
|
4916
|
+
body.max_live_sends = maxLiveSends;
|
|
4917
|
+
const settings = readSequenceSettings(options);
|
|
4918
|
+
if (settings)
|
|
4919
|
+
body.settings = settings;
|
|
4095
4920
|
if (options.clearEmail) {
|
|
4096
4921
|
body.email = null;
|
|
4097
4922
|
}
|
|
@@ -4101,7 +4926,7 @@ export function createProgram() {
|
|
|
4101
4926
|
body.email = email;
|
|
4102
4927
|
}
|
|
4103
4928
|
if (Object.keys(body).length === 0) {
|
|
4104
|
-
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email,
|
|
4929
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, or --send-window-file).");
|
|
4105
4930
|
}
|
|
4106
4931
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
4107
4932
|
method: "PATCH",
|
|
@@ -4138,20 +4963,33 @@ export function createProgram() {
|
|
|
4138
4963
|
.argument("<sequence>", "Sequence id or slug.")
|
|
4139
4964
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only.")
|
|
4140
4965
|
.option("--max-credits <n>", "Credit cap (required with --approved).")
|
|
4966
|
+
.option("--max-live-sends <n>", "Email send ceiling (positive integer). Required to start a sequence with email steps live.")
|
|
4141
4967
|
.option("--dry-run", "Activate in dry-run mode: advance every step with simulated sends, no LinkedIn actions, no credits.")
|
|
4142
4968
|
.option("--json", "Print a JSON envelope.")
|
|
4143
4969
|
.action(async (sequence, options) => {
|
|
4144
4970
|
await handleAsyncAction("sequences start", options, () => {
|
|
4145
4971
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
4972
|
+
const maxLiveSends = readPositiveInteger(options.maxLiveSends);
|
|
4146
4973
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
4147
4974
|
method: "POST",
|
|
4148
4975
|
body: {
|
|
4149
4976
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
4150
4977
|
...(options.approved ? { approved: true } : {}),
|
|
4151
4978
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
4979
|
+
...(maxLiveSends !== undefined ? { max_live_sends: maxLiveSends } : {}),
|
|
4152
4980
|
},
|
|
4153
4981
|
});
|
|
4154
4982
|
});
|
|
4983
|
+
}))
|
|
4984
|
+
.addCommand(new Command("signal")
|
|
4985
|
+
.description("Record an external GTM signal (e.g. linkedin_connected, email_replied, company_raised_funds, job_change, web_visit, intent) onto a running sequence's enrollment(s) to drive signal-triggered branch/wait control. Target an enrollment by --enrollment or --lead (at least one is required); the server validates the signal name.")
|
|
4986
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
4987
|
+
.requiredOption("--signal <name>", "Signal to record: linkedin_connected, linkedin_replied, email_sent, email_opened, email_clicked, email_replied, email_bounced, company_hiring, company_raised_funds, job_change, new_hire, web_visit, intent.")
|
|
4988
|
+
.option("--enrollment <id>", "Target enrollment id.")
|
|
4989
|
+
.option("--lead <provider_id>", "Target enrollment by its lead provider id.")
|
|
4990
|
+
.option("--json", "Print a JSON envelope.")
|
|
4991
|
+
.action(async (sequence, options) => {
|
|
4992
|
+
await handleSequenceSignalAction(sequence, options);
|
|
4155
4993
|
}))
|
|
4156
4994
|
.addCommand(new Command("pause")
|
|
4157
4995
|
.description("Pause an active sequence (stops new dispatches; enrollments resume on un-pause).")
|
|
@@ -4205,6 +5043,13 @@ export function createProgram() {
|
|
|
4205
5043
|
.option("--json", "Print a JSON envelope.")
|
|
4206
5044
|
.action(async (sequence, options) => {
|
|
4207
5045
|
await handleAsyncAction("sequences stats", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats`));
|
|
5046
|
+
}))
|
|
5047
|
+
.addCommand(new Command("variants")
|
|
5048
|
+
.description("Break the sequence's email performance down by A/B variant (per step) and by sending mailbox: sent, replied, reply rate, bounced/failed, and credits used.")
|
|
5049
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
5050
|
+
.option("--json", "Print a JSON envelope.")
|
|
5051
|
+
.action(async (sequence, options) => {
|
|
5052
|
+
await handleSequenceVariantsAction(sequence, options);
|
|
4208
5053
|
})));
|
|
4209
5054
|
program.addCommand(new Command("email")
|
|
4210
5055
|
.description("Ad-hoc email from the org's sending fleet: one-off sends with no sequence, enrollment, or contact sync. Zapbox-connected mailboxes send through Zapmail's API (no Google/Microsoft consent); BYOK — 0 Oxygen credits.")
|
|
@@ -4459,6 +5304,16 @@ export function createProgram() {
|
|
|
4459
5304
|
.option("--json", "Print a JSON envelope.")
|
|
4460
5305
|
.action(async (domains, options) => {
|
|
4461
5306
|
await handleAsyncAction("domains buy", options, () => runDomainsBuy(domains, options));
|
|
5307
|
+
}))
|
|
5308
|
+
.addCommand(new Command("adopt")
|
|
5309
|
+
.description("STAFF: Import domains that already exist in the shared Oxygen-managed Cloudflare account into a workspace, then mirror them into the domain cache. FREE — no Oxygen credits and no Cloudflare purchase (the domains are already registered); this differs from `domains buy`. Idempotent. Without --yes, prints a preview of what would be adopted/skipped.")
|
|
5310
|
+
.argument("[domains...]", "Specific domains to adopt. Omit and pass --all to adopt every zone in the managed account.")
|
|
5311
|
+
.option("--all", "Adopt every zone discovered in the managed Cloudflare account.")
|
|
5312
|
+
.option("--organization-id <id>", "STAFF override: adopt into this workspace instead of the calling org.")
|
|
5313
|
+
.option("--yes", "Execute the adoption. Without this flag, prints a preview only.")
|
|
5314
|
+
.option("--json", "Print a JSON envelope.")
|
|
5315
|
+
.action(async (domains, options) => {
|
|
5316
|
+
await handleAsyncAction("domains adopt", options, () => runDomainsAdopt(domains, options));
|
|
4462
5317
|
}))
|
|
4463
5318
|
.addCommand(new Command("registrations")
|
|
4464
5319
|
.description("List the org's domain purchase ledger: every registration attempt with status and price snapshot.")
|
|
@@ -4666,7 +5521,7 @@ export function createProgram() {
|
|
|
4666
5521
|
.description("Workflow event trigger utilities.")
|
|
4667
5522
|
.addCommand(new Command("emit")
|
|
4668
5523
|
.description("Emit a normalized workflow event and enqueue matching event-triggered workflows.")
|
|
4669
|
-
.requiredOption("--source <source>", "Event source, such as instantly, calendar, or
|
|
5524
|
+
.requiredOption("--source <source>", "Event source, such as instantly, calendar, hubspot, or oxygen.notetaker.")
|
|
4670
5525
|
.requiredOption("--event <event>", "Event type, such as email.reply_received.")
|
|
4671
5526
|
.requiredOption("--payload-json <json>", "Normalized event payload object.")
|
|
4672
5527
|
.option("--raw-payload-json <json>", "Optional raw provider payload object.")
|
|
@@ -4765,6 +5620,44 @@ export function createProgram() {
|
|
|
4765
5620
|
method: "POST",
|
|
4766
5621
|
body: { run_id: runId },
|
|
4767
5622
|
}));
|
|
5623
|
+
}))
|
|
5624
|
+
.addCommand(new Command("approvals")
|
|
5625
|
+
.description("List workflow runs awaiting a human approval decision (the mid-run approval inbox).")
|
|
5626
|
+
.option("--status <status>", "Filter: pending (default), approved, rejected, expired, or all.")
|
|
5627
|
+
.option("--run-id <run_id>", "Only approvals for this workflow run.")
|
|
5628
|
+
.option("--limit <n>", "Maximum approvals to return. Defaults to 50.")
|
|
5629
|
+
.option("--json", "Print a JSON envelope.")
|
|
5630
|
+
.action(async (options) => {
|
|
5631
|
+
await handleAsyncAction("workflows approvals", options, () => {
|
|
5632
|
+
const query = new URLSearchParams();
|
|
5633
|
+
if (readOption(options.status))
|
|
5634
|
+
query.set("status", readOption(options.status) ?? "");
|
|
5635
|
+
if (readOption(options.runId))
|
|
5636
|
+
query.set("run_id", readOption(options.runId) ?? "");
|
|
5637
|
+
const limit = readPositiveInt(options.limit);
|
|
5638
|
+
if (limit)
|
|
5639
|
+
query.set("limit", String(limit));
|
|
5640
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
5641
|
+
return requestOxygen(`/api/cli/workflows/approvals${suffix}`);
|
|
5642
|
+
});
|
|
5643
|
+
}))
|
|
5644
|
+
.addCommand(new Command("resume")
|
|
5645
|
+
.description("Approve or reject a paused mid-run workflow approval, resuming the run.")
|
|
5646
|
+
.argument("<run_id>", "Workflow run UUID.")
|
|
5647
|
+
.requiredOption("--decision <decision>", "approve or reject.")
|
|
5648
|
+
.option("--step-id <step_id>", "Approval step id, when a run has more than one pending gate.")
|
|
5649
|
+
.option("--plan-hash <plan_hash>", "Reviewed plan hash; the decision is refused if the plan changed since review.")
|
|
5650
|
+
.option("--json", "Print a JSON envelope.")
|
|
5651
|
+
.action(async (runId, options) => {
|
|
5652
|
+
await handleAsyncAction("workflows resume", options, () => requestOxygen("/api/cli/workflows/resume", {
|
|
5653
|
+
method: "POST",
|
|
5654
|
+
body: {
|
|
5655
|
+
run_id: runId,
|
|
5656
|
+
decision: readOption(options.decision),
|
|
5657
|
+
...(readOption(options.stepId) ? { step_id: readOption(options.stepId) } : {}),
|
|
5658
|
+
...(readOption(options.planHash) ? { plan_hash: readOption(options.planHash) } : {}),
|
|
5659
|
+
},
|
|
5660
|
+
}));
|
|
4768
5661
|
}))
|
|
4769
5662
|
.addCommand(new Command("enable")
|
|
4770
5663
|
.description("Enable a workflow automation and its current trigger.")
|
|
@@ -4817,7 +5710,46 @@ export function createProgram() {
|
|
|
4817
5710
|
}));
|
|
4818
5711
|
return program;
|
|
4819
5712
|
}
|
|
5713
|
+
/**
|
|
5714
|
+
* Map a workflow compile/validation throw to a typed `OxygenError`. The
|
|
5715
|
+
* compiler and linter (`compileWorkflowDefinition`, `assertWorkflowManifest`,
|
|
5716
|
+
* `serializeWorkflowFunction`, cron parsing) throw ordinary `Error`s shaped
|
|
5717
|
+
* `"<code>: <message>"`, which `@oxygen/shared`'s `toFailure` would otherwise
|
|
5718
|
+
* flatten to a machine-hostile `unexpected_error` — making `workflows lint`
|
|
5719
|
+
* (whose whole job is to report authoring mistakes) read like an internal
|
|
5720
|
+
* crash. Preserve the embedded lint code so the failure mirrors what a manifest
|
|
5721
|
+
* lint would return (`duplicate_step_id`, `invalid_cron`, `invalid_max_credits`,
|
|
5722
|
+
* …). Same precedent as `parseJsonValue`.
|
|
5723
|
+
*/
|
|
5724
|
+
function asWorkflowCompileError(error, filePath) {
|
|
5725
|
+
if (error instanceof OxygenError)
|
|
5726
|
+
return error;
|
|
5727
|
+
const fsCode = error?.code;
|
|
5728
|
+
if (typeof fsCode === "string" && /^E[A-Z]+$/.test(fsCode)) {
|
|
5729
|
+
return new OxygenError("workflow_file_unreadable", `Could not read workflow file: ${error instanceof Error ? error.message : fsCode}`, { details: { file: filePath }, exitCode: 1 });
|
|
5730
|
+
}
|
|
5731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5732
|
+
const match = /^([a-z][a-z0-9_]*): ([\s\S]+)$/.exec(message);
|
|
5733
|
+
if (match?.[1] && match[2]) {
|
|
5734
|
+
return new OxygenError(match[1], match[2], {
|
|
5735
|
+
details: { file: filePath, phase: "compile" },
|
|
5736
|
+
exitCode: 1,
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
5739
|
+
return new OxygenError("invalid_workflow", message, {
|
|
5740
|
+
details: { file: filePath, phase: "compile" },
|
|
5741
|
+
exitCode: 1,
|
|
5742
|
+
});
|
|
5743
|
+
}
|
|
4820
5744
|
async function compileWorkflowFile(filePath) {
|
|
5745
|
+
try {
|
|
5746
|
+
return await loadWorkflowManifestFromFile(filePath);
|
|
5747
|
+
}
|
|
5748
|
+
catch (error) {
|
|
5749
|
+
throw asWorkflowCompileError(error, filePath);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
async function loadWorkflowManifestFromFile(filePath) {
|
|
4821
5753
|
const absolutePath = resolve(filePath);
|
|
4822
5754
|
const source = readFileSync(absolutePath, "utf8");
|
|
4823
5755
|
const extension = extname(absolutePath).toLowerCase();
|
|
@@ -5214,7 +6146,10 @@ function normalizeWorkflowRunErrors(value) {
|
|
|
5214
6146
|
return output;
|
|
5215
6147
|
}
|
|
5216
6148
|
function isTerminalWorkflowRunStatus(status) {
|
|
5217
|
-
|
|
6149
|
+
// 'awaiting_approval' pauses the run indefinitely for a human decision (lease
|
|
6150
|
+
// cleared, excluded from claim + lease sweep), so it must stop the tail and
|
|
6151
|
+
// surface the approval rather than poll forever.
|
|
6152
|
+
return status === "completed" || status === "failed" || status === "canceled" || status === "awaiting_approval";
|
|
5218
6153
|
}
|
|
5219
6154
|
function tableWebhookListPath(options) {
|
|
5220
6155
|
const params = new URLSearchParams();
|
|
@@ -6496,6 +7431,38 @@ function domainsBuyRerunCommand(domains, quoteId, options, data) {
|
|
|
6496
7431
|
];
|
|
6497
7432
|
return `${resolveCliBinaryName()} domains buy ${domains.join(" ")} ${flags.join(" ")}`;
|
|
6498
7433
|
}
|
|
7434
|
+
// Staff import of pre-existing managed-account domains. Exactly one of an explicit
|
|
7435
|
+
// list or --all; without --yes the server returns a preview and the CLI appends a
|
|
7436
|
+
// copy-paste rerun command to execute it.
|
|
7437
|
+
async function runDomainsAdopt(domains, options) {
|
|
7438
|
+
const hasExplicit = domains.length > 0;
|
|
7439
|
+
if (hasExplicit === Boolean(options.all)) {
|
|
7440
|
+
throw new Error("Pass exactly one of <domains...> or --all. Use --all to adopt every domain in the managed account, or list specific domains.");
|
|
7441
|
+
}
|
|
7442
|
+
const organizationId = readOption(options.organizationId);
|
|
7443
|
+
const data = await requestOxygen("/api/cli/domains/adopt", {
|
|
7444
|
+
method: "POST",
|
|
7445
|
+
body: {
|
|
7446
|
+
...(options.all ? { all: true } : {}),
|
|
7447
|
+
...(hasExplicit ? { domains } : {}),
|
|
7448
|
+
...(organizationId ? { organization_id: organizationId } : {}),
|
|
7449
|
+
...(options.yes ? { approved: true } : {}),
|
|
7450
|
+
},
|
|
7451
|
+
});
|
|
7452
|
+
if (options.yes)
|
|
7453
|
+
return data;
|
|
7454
|
+
return { ...data, rerun_command: domainsAdoptRerunCommand(domains, options) };
|
|
7455
|
+
}
|
|
7456
|
+
function domainsAdoptRerunCommand(domains, options) {
|
|
7457
|
+
const organizationId = readOption(options.organizationId);
|
|
7458
|
+
const flags = [
|
|
7459
|
+
...(options.all ? ["--all"] : []),
|
|
7460
|
+
...(organizationId ? [`--organization-id ${organizationId}`] : []),
|
|
7461
|
+
"--yes",
|
|
7462
|
+
];
|
|
7463
|
+
const args = domains.length > 0 ? ` ${domains.join(" ")}` : "";
|
|
7464
|
+
return `${resolveCliBinaryName()} domains adopt${args} ${flags.join(" ")}`;
|
|
7465
|
+
}
|
|
6499
7466
|
// succeeded/failed are terminal; action_required stops the wait too because
|
|
6500
7467
|
// only the user (in the Cloudflare dashboard) can move it forward.
|
|
6501
7468
|
function isSettledDomainRegistrationStatus(status) {
|
|
@@ -8108,6 +9075,125 @@ function formatProfileUseSuccess(profile, options) {
|
|
|
8108
9075
|
}
|
|
8109
9076
|
return lines.join("\n");
|
|
8110
9077
|
}
|
|
9078
|
+
// `sequences signal` records an external GTM signal onto a running sequence's
|
|
9079
|
+
// enrollment(s). The signal *name* is validated server-side against the typed
|
|
9080
|
+
// enum; the CLI only enforces that a signal was supplied and that at least one
|
|
9081
|
+
// enrollment target (--enrollment / --lead) is present, so an unscoped call
|
|
9082
|
+
// fails before spending a request.
|
|
9083
|
+
async function handleSequenceSignalAction(sequence, options) {
|
|
9084
|
+
try {
|
|
9085
|
+
const signal = readOption(options.signal);
|
|
9086
|
+
if (!signal)
|
|
9087
|
+
throw new Error("--signal is required.");
|
|
9088
|
+
const enrollmentId = readOption(options.enrollment);
|
|
9089
|
+
const leadProviderId = readOption(options.lead);
|
|
9090
|
+
if (!enrollmentId && !leadProviderId) {
|
|
9091
|
+
throw new Error("Provide a target enrollment with --enrollment <id> and/or --lead <provider_id>.");
|
|
9092
|
+
}
|
|
9093
|
+
const data = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/signal`, {
|
|
9094
|
+
method: "POST",
|
|
9095
|
+
body: {
|
|
9096
|
+
signal,
|
|
9097
|
+
...(enrollmentId ? { enrollment_id: enrollmentId } : {}),
|
|
9098
|
+
...(leadProviderId ? { lead_provider_id: leadProviderId } : {}),
|
|
9099
|
+
},
|
|
9100
|
+
});
|
|
9101
|
+
if (options.json) {
|
|
9102
|
+
writeJson(success("sequences signal", data));
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
const updated = typeof data.enrollments_updated === "number" ? data.enrollments_updated : 0;
|
|
9106
|
+
const recordedSignal = data.signal ?? signal;
|
|
9107
|
+
const link = data.deepLink ?? data.web_url;
|
|
9108
|
+
process.stdout.write(`Recorded ${recordedSignal} on ${updated} enrollment(s).${link ? ` ${link}` : ""}\n`);
|
|
9109
|
+
}
|
|
9110
|
+
catch (error) {
|
|
9111
|
+
const failure = toFailure("sequences signal", error);
|
|
9112
|
+
writeJson(failure);
|
|
9113
|
+
writeMaxCreditsHint(error);
|
|
9114
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
async function handleSequenceVariantsAction(sequence, options) {
|
|
9118
|
+
try {
|
|
9119
|
+
const data = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/variants`);
|
|
9120
|
+
if (options.json) {
|
|
9121
|
+
writeJson(success("sequences variants", data));
|
|
9122
|
+
return;
|
|
9123
|
+
}
|
|
9124
|
+
process.stdout.write(formatSequenceVariants(data));
|
|
9125
|
+
}
|
|
9126
|
+
catch (error) {
|
|
9127
|
+
const failure = toFailure("sequences variants", error);
|
|
9128
|
+
writeJson(failure);
|
|
9129
|
+
writeMaxCreditsHint(error);
|
|
9130
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
function formatVariantCell(value) {
|
|
9134
|
+
if (value === null || value === undefined)
|
|
9135
|
+
return "—";
|
|
9136
|
+
return String(value);
|
|
9137
|
+
}
|
|
9138
|
+
function formatRatePercent(value) {
|
|
9139
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
9140
|
+
return "—";
|
|
9141
|
+
// Rates arrive as a 0–1 fraction; render as a one-decimal percentage.
|
|
9142
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
9143
|
+
}
|
|
9144
|
+
function renderVariantTable(headers, rows) {
|
|
9145
|
+
const widths = headers.map((header, columnIndex) => {
|
|
9146
|
+
let max = header.length;
|
|
9147
|
+
for (const row of rows) {
|
|
9148
|
+
const cell = row[columnIndex] ?? "";
|
|
9149
|
+
if (cell.length > max)
|
|
9150
|
+
max = cell.length;
|
|
9151
|
+
}
|
|
9152
|
+
return max;
|
|
9153
|
+
});
|
|
9154
|
+
const renderRow = (cells) => ` ${cells.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ")}`.replace(/\s+$/, "");
|
|
9155
|
+
const separator = ` ${widths.map((width) => "-".repeat(width)).join(" ")}`;
|
|
9156
|
+
return [renderRow(headers), separator, ...rows.map(renderRow)];
|
|
9157
|
+
}
|
|
9158
|
+
function formatSequenceVariants(data) {
|
|
9159
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
9160
|
+
const byVariant = Array.isArray(data.by_variant) ? data.by_variant : [];
|
|
9161
|
+
const byMailbox = Array.isArray(data.by_mailbox) ? data.by_mailbox : [];
|
|
9162
|
+
const lines = ["", styles.bold("By variant")];
|
|
9163
|
+
if (byVariant.length === 0) {
|
|
9164
|
+
lines.push(` ${styles.dim("No variant analytics yet.")}`);
|
|
9165
|
+
}
|
|
9166
|
+
else {
|
|
9167
|
+
const headers = ["STEP", "VARIANT", "SENT", "REPLIED", "REPLY RATE", "BOUNCED", "CREDITS"];
|
|
9168
|
+
const rows = byVariant.map((row) => [
|
|
9169
|
+
formatVariantCell(row.step_index),
|
|
9170
|
+
formatVariantCell(row.variant_id),
|
|
9171
|
+
formatVariantCell(row.sent),
|
|
9172
|
+
formatVariantCell(row.replied),
|
|
9173
|
+
formatRatePercent(row.reply_rate),
|
|
9174
|
+
formatVariantCell(row.bounced),
|
|
9175
|
+
formatVariantCell(row.credits_used),
|
|
9176
|
+
]);
|
|
9177
|
+
lines.push(...renderVariantTable(headers, rows));
|
|
9178
|
+
}
|
|
9179
|
+
lines.push("", styles.bold("By mailbox"));
|
|
9180
|
+
if (byMailbox.length === 0) {
|
|
9181
|
+
lines.push(` ${styles.dim("No mailbox analytics yet.")}`);
|
|
9182
|
+
}
|
|
9183
|
+
else {
|
|
9184
|
+
const headers = ["MAILBOX", "SENT", "REPLIED", "REPLY RATE", "FAILED"];
|
|
9185
|
+
const rows = byMailbox.map((row) => [
|
|
9186
|
+
formatVariantCell(row.email_address),
|
|
9187
|
+
formatVariantCell(row.sent),
|
|
9188
|
+
formatVariantCell(row.replied),
|
|
9189
|
+
formatRatePercent(row.reply_rate),
|
|
9190
|
+
formatVariantCell(row.failed),
|
|
9191
|
+
]);
|
|
9192
|
+
lines.push(...renderVariantTable(headers, rows));
|
|
9193
|
+
}
|
|
9194
|
+
lines.push("");
|
|
9195
|
+
return lines.join("\n");
|
|
9196
|
+
}
|
|
8111
9197
|
function formatWhoami(identity, context) {
|
|
8112
9198
|
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
8113
9199
|
const email = identity.user.email ?? identity.user.id;
|
|
@@ -8431,6 +9517,8 @@ function buildBillingLedgerParams(options) {
|
|
|
8431
9517
|
params.set("days", String(days));
|
|
8432
9518
|
if (limit)
|
|
8433
9519
|
params.set("limit", String(limit));
|
|
9520
|
+
if (readOption(options.meter))
|
|
9521
|
+
params.set("meter", readOption(options.meter));
|
|
8434
9522
|
if (readOption(options.from))
|
|
8435
9523
|
params.set("from", readOption(options.from));
|
|
8436
9524
|
if (readOption(options.to))
|
|
@@ -8479,6 +9567,7 @@ function buildContextAssetUpsertBody(options) {
|
|
|
8479
9567
|
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
8480
9568
|
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
8481
9569
|
...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
|
|
9570
|
+
...(options.default ? { is_default: true } : {}),
|
|
8482
9571
|
};
|
|
8483
9572
|
}
|
|
8484
9573
|
function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enrichment aliases, selection, provider order, and safety caps.
|
|
@@ -8615,6 +9704,37 @@ function readNonNegativeInt(value) {
|
|
|
8615
9704
|
}
|
|
8616
9705
|
return parsed;
|
|
8617
9706
|
}
|
|
9707
|
+
function readPositiveInteger(value) {
|
|
9708
|
+
const trimmed = value?.trim();
|
|
9709
|
+
if (!trimmed)
|
|
9710
|
+
return undefined;
|
|
9711
|
+
const parsed = Number(trimmed);
|
|
9712
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
9713
|
+
throw new OxygenError("invalid_number", "Expected a positive integer.", {
|
|
9714
|
+
details: { value },
|
|
9715
|
+
exitCode: 1,
|
|
9716
|
+
});
|
|
9717
|
+
}
|
|
9718
|
+
return parsed;
|
|
9719
|
+
}
|
|
9720
|
+
// Folds the sequence-level email send controls (--max-emails-per-mailbox-per-day,
|
|
9721
|
+
// --send-window-file) into a partial `settings` object. Returns undefined when
|
|
9722
|
+
// neither flag is set so the field is omitted from the request body entirely.
|
|
9723
|
+
// The server shallow-merges this fragment over the sequence's current settings
|
|
9724
|
+
// (apps/web .../sequences/[sequenceId]/route.ts), so a throttle-only PATCH
|
|
9725
|
+
// preserves other settings the server already holds, e.g. dispatch_mode/schedule.
|
|
9726
|
+
function readSequenceSettings(options) {
|
|
9727
|
+
const settings = {};
|
|
9728
|
+
const maxPerMailbox = readPositiveInteger(options.maxEmailsPerMailboxPerDay);
|
|
9729
|
+
if (maxPerMailbox !== undefined) {
|
|
9730
|
+
settings.max_emails_per_mailbox_per_day = maxPerMailbox;
|
|
9731
|
+
}
|
|
9732
|
+
const windowPath = readOption(options.sendWindowFile);
|
|
9733
|
+
if (windowPath) {
|
|
9734
|
+
settings.email_send_window = readJsonFileValue(resolve(windowPath), "--send-window-file");
|
|
9735
|
+
}
|
|
9736
|
+
return Object.keys(settings).length > 0 ? settings : undefined;
|
|
9737
|
+
}
|
|
8618
9738
|
function muteTokenEcho() {
|
|
8619
9739
|
if (!input.isTTY || process.platform === "win32")
|
|
8620
9740
|
return () => undefined;
|