@oxygen-agent/cli 1.286.13 → 1.309.0
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/cli-values.d.ts +18 -0
- package/dist/cli-values.js +66 -0
- package/dist/credentials.d.ts +22 -2
- package/dist/credentials.js +80 -17
- package/dist/help.js +1 -1
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +68 -30
- package/dist/index.js +789 -243
- package/dist/knowledge-mirror.d.ts +10 -0
- package/dist/knowledge-mirror.js +18 -0
- package/dist/run-wait.js +2 -26
- package/dist/runtime.d.ts +63 -4
- package/dist/runtime.js +113 -3
- package/node_modules/@oxygen/shared/dist/deprecation-registry.js +2 -18
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +223 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +9 -27
- package/node_modules/@oxygen/shared/dist/identifiers.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/identifiers.js +48 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/index.js +7 -1
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +301 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +105 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/log.js +65 -6
- package/node_modules/@oxygen/shared/dist/redaction.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +15 -3
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +11 -3
- package/node_modules/@oxygen/shared/dist/sequences.js +11 -2
- package/node_modules/@oxygen/shared/dist/timing.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/timing.js +12 -0
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/version.js +33 -2
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -1
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +41 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +203 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,15 +9,16 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
9
9
|
import { Command, Option } from "commander";
|
|
10
10
|
import { applyOxygenHelp } from "./help.js";
|
|
11
11
|
import { buildCommandManifest } from "./command-manifest.js";
|
|
12
|
-
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, isVersionGreater, isVersionLess, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, success, toFailure, } from "@oxygen/shared";
|
|
12
|
+
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, isVersionGreater, isVersionLess, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, sleep, success, toFailure, } from "@oxygen/shared";
|
|
13
13
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
14
14
|
import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
|
|
15
15
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
16
16
|
import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
|
|
17
17
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
18
18
|
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
19
|
-
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
19
|
+
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, listLocalMirrors, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
20
20
|
import { waitForCliRun } from "./run-wait.js";
|
|
21
|
+
import { assertModeFlagsExclusive, parseJsonObject, readJsonObjectOption, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
|
|
21
22
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
22
23
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
23
24
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
@@ -125,12 +126,18 @@ async function handleAsyncAction(command, options, action) {
|
|
|
125
126
|
writeCreditsReceipt(data);
|
|
126
127
|
}
|
|
127
128
|
catch (error) {
|
|
128
|
-
|
|
129
|
-
writeJson(failure);
|
|
130
|
-
writeMaxCreditsHint(error);
|
|
131
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
129
|
+
emitCliFailure(command, error);
|
|
132
130
|
}
|
|
133
131
|
}
|
|
132
|
+
// Single-source the CLI failure-emit contract: write the machine-readable
|
|
133
|
+
// failure envelope to stdout, surface any spend-gate hint on stderr, and set the
|
|
134
|
+
// process exit code from the error. Command handlers that don't route through
|
|
135
|
+
// handleAsyncAction catch straight into this so the shape can't drift.
|
|
136
|
+
function emitCliFailure(command, error) {
|
|
137
|
+
writeJson(toFailure(command, error));
|
|
138
|
+
writeMaxCreditsHint(error);
|
|
139
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
140
|
+
}
|
|
134
141
|
// Paid envelopes (push 3 legibility) carry a `credits` block: quote on
|
|
135
142
|
// dry_run, receipt on live, remaining balance on both. Mirror it as one
|
|
136
143
|
// stderr line so spend stays visible in a terminal without polluting the
|
|
@@ -153,6 +160,53 @@ function writeCreditsReceipt(data) {
|
|
|
153
160
|
process.stderr.write(`estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
|
|
154
161
|
}
|
|
155
162
|
}
|
|
163
|
+
// Arming a cron commits recurring spend, so `workflows enable` mirrors its
|
|
164
|
+
// `automation` block as stderr lines: what the schedule burns per day, what
|
|
165
|
+
// share of the monthly allowance that is, and when it runs out. Observed burn
|
|
166
|
+
// (this workflow's own billed runs) is the honest number and is preferred; the
|
|
167
|
+
// manifest floor is a MINIMUM and is labelled as one, because it excludes the
|
|
168
|
+
// per-row billing that is ~93% of the meter in practice.
|
|
169
|
+
function writeAutomationProjection(data) {
|
|
170
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
171
|
+
return;
|
|
172
|
+
const automation = data.automation;
|
|
173
|
+
if (!automation || typeof automation !== "object" || Array.isArray(automation))
|
|
174
|
+
return;
|
|
175
|
+
const block = automation;
|
|
176
|
+
const estimate = block.estimate;
|
|
177
|
+
const observed = block.observed;
|
|
178
|
+
const allowance = block.allowance;
|
|
179
|
+
const runsPer30Days = typeof estimate?.scheduledRunsPer30Days === "number"
|
|
180
|
+
? estimate.scheduledRunsPer30Days
|
|
181
|
+
: null;
|
|
182
|
+
if (runsPer30Days === null)
|
|
183
|
+
return; // On-demand: no schedule, no burn to project.
|
|
184
|
+
const projected = typeof observed?.projectedActionsPer30Days === "number"
|
|
185
|
+
? observed.projectedActionsPer30Days
|
|
186
|
+
: typeof estimate?.scheduledActionsPer30DaysFloor === "number"
|
|
187
|
+
? estimate.scheduledActionsPer30DaysFloor
|
|
188
|
+
: null;
|
|
189
|
+
if (projected === null)
|
|
190
|
+
return;
|
|
191
|
+
const fromHistory = typeof observed?.projectedActionsPer30Days === "number";
|
|
192
|
+
const perDay = Math.round(projected / 30);
|
|
193
|
+
const included = typeof allowance?.includedActions === "number" ? allowance.includedActions : null;
|
|
194
|
+
// A cheap schedule on a big plan rounds to "0% of the allowance", which reads
|
|
195
|
+
// as a bug rather than as reassurance. Floor the display at "<1%".
|
|
196
|
+
const sharePct = included !== null && included > 0 ? (projected / included) * 100 : null;
|
|
197
|
+
const share = sharePct === null
|
|
198
|
+
? ""
|
|
199
|
+
: ` (${sharePct < 1 ? "<1" : Math.round(sharePct)}% of the ${included.toLocaleString("en-US")}/mo allowance)`;
|
|
200
|
+
const basis = fromHistory
|
|
201
|
+
? `based on ${typeof observed?.runSample === "number" ? observed.runSample : 0} past billed runs`
|
|
202
|
+
: "MINIMUM — excludes per-row billing, real cost will be higher";
|
|
203
|
+
process.stderr.write(`schedule: ${runsPer30Days.toLocaleString("en-US")} runs/30d, ~${perDay.toLocaleString("en-US")} automation actions/day${share}\n`);
|
|
204
|
+
process.stderr.write(` ${basis}\n`);
|
|
205
|
+
const exhaustion = observed?.projectedExhaustionAt;
|
|
206
|
+
if (typeof exhaustion === "string") {
|
|
207
|
+
process.stderr.write(` allowance runs out ${exhaustion} at this pace — the schedule pauses until the window resets\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
156
210
|
// Paid live runs are refused server-side with typed spend-gate errors
|
|
157
211
|
// (max_credits_required, approval_required, spend_cap_required,
|
|
158
212
|
// spend_cap_too_low). Surface each as a one-line stderr hint with the concrete
|
|
@@ -196,24 +250,6 @@ function readDetailsNumber(details, key) {
|
|
|
196
250
|
const value = details[key];
|
|
197
251
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
198
252
|
}
|
|
199
|
-
function parseJsonObject(value) {
|
|
200
|
-
let parsed;
|
|
201
|
-
try {
|
|
202
|
-
parsed = JSON.parse(value);
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
throw new OxygenError("invalid_json", "Input must be valid JSON.", {
|
|
206
|
-
details: { reason: error instanceof Error ? error.message : "unknown" },
|
|
207
|
-
exitCode: 1,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
211
|
-
throw new OxygenError("invalid_json", "Input JSON must be an object.", {
|
|
212
|
-
exitCode: 1,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return parsed;
|
|
216
|
-
}
|
|
217
253
|
function parseJsonArray(value) {
|
|
218
254
|
let parsed;
|
|
219
255
|
try {
|
|
@@ -452,9 +488,7 @@ function parseSuppressListOption(value) {
|
|
|
452
488
|
];
|
|
453
489
|
}
|
|
454
490
|
function resolveComposioRunMode(options) {
|
|
455
|
-
|
|
456
|
-
throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
|
|
457
|
-
}
|
|
491
|
+
assertModeFlagsExclusive(options);
|
|
458
492
|
if (options.live === true)
|
|
459
493
|
return "live";
|
|
460
494
|
if (options.dryRun === true)
|
|
@@ -472,7 +506,7 @@ const DEFAULT_CRM_SETUP_OBJECTS = ["companies", "people"];
|
|
|
472
506
|
function buildCrmSetupBody(options) {
|
|
473
507
|
return {
|
|
474
508
|
objects: readCrmSetupObjects(options.objects),
|
|
475
|
-
mode:
|
|
509
|
+
mode: resolveLiveDryRunMode(options),
|
|
476
510
|
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
477
511
|
};
|
|
478
512
|
}
|
|
@@ -480,12 +514,6 @@ function readCrmSetupObjects(value) {
|
|
|
480
514
|
const objects = readCsvOption(value);
|
|
481
515
|
return objects.length > 0 ? [...new Set(objects)] : DEFAULT_CRM_SETUP_OBJECTS;
|
|
482
516
|
}
|
|
483
|
-
function resolveCrmSetupMode(options) {
|
|
484
|
-
if (options.live === true && options.dryRun === true) {
|
|
485
|
-
throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
|
|
486
|
-
}
|
|
487
|
-
return options.live === true ? "live" : "dry_run";
|
|
488
|
-
}
|
|
489
517
|
function buildCrmObjectCreateBody(options) {
|
|
490
518
|
const slug = readOption(options.slug);
|
|
491
519
|
if (!slug) {
|
|
@@ -502,7 +530,7 @@ function buildCrmObjectCreateBody(options) {
|
|
|
502
530
|
display_name: displayName,
|
|
503
531
|
columns,
|
|
504
532
|
identities,
|
|
505
|
-
mode:
|
|
533
|
+
mode: resolveLiveDryRunMode(options),
|
|
506
534
|
...(readOption(options.singularName) ? { singular_name: readOption(options.singularName) } : {}),
|
|
507
535
|
...(readOption(options.pluralName) ? { plural_name: readOption(options.pluralName) } : {}),
|
|
508
536
|
...(readOption(options.labelColumn) ? { label_column: readOption(options.labelColumn) } : {}),
|
|
@@ -519,7 +547,7 @@ function buildCrmObjectAddAttrBody(options) {
|
|
|
519
547
|
}
|
|
520
548
|
return {
|
|
521
549
|
column,
|
|
522
|
-
mode:
|
|
550
|
+
mode: resolveLiveDryRunMode(options),
|
|
523
551
|
...(options.asIdentityJson ? { identity: parseJsonObject(options.asIdentityJson) } : {}),
|
|
524
552
|
};
|
|
525
553
|
}
|
|
@@ -528,14 +556,14 @@ function buildCrmAssertBody(object, options) {
|
|
|
528
556
|
object,
|
|
529
557
|
identity: parseCrmIdentityOption(options.identity),
|
|
530
558
|
values: options.valuesJson ? parseJsonObject(options.valuesJson) : {},
|
|
531
|
-
mode:
|
|
559
|
+
mode: resolveLiveDryRunMode(options),
|
|
532
560
|
};
|
|
533
561
|
}
|
|
534
562
|
function buildCrmSyncImportBody(provider, options) {
|
|
535
563
|
return {
|
|
536
564
|
provider,
|
|
537
565
|
object: options.object,
|
|
538
|
-
mode:
|
|
566
|
+
mode: resolveLiveDryRunMode(options),
|
|
539
567
|
...(options.into ? { into: options.into } : {}),
|
|
540
568
|
...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
|
|
541
569
|
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
@@ -546,7 +574,7 @@ function buildCrmSyncConfigureBody(options) {
|
|
|
546
574
|
return {
|
|
547
575
|
provider: options.provider,
|
|
548
576
|
object: options.object,
|
|
549
|
-
mode:
|
|
577
|
+
mode: resolveLiveDryRunMode(options),
|
|
550
578
|
...(readOption(options.direction) ? { direction: readOption(options.direction) } : {}),
|
|
551
579
|
...(readOption(options.cron) ? { cron: readOption(options.cron) } : {}),
|
|
552
580
|
...(readOption(options.timezone) ? { timezone: readOption(options.timezone) } : {}),
|
|
@@ -559,7 +587,7 @@ function buildCrmSyncRunBody(options) {
|
|
|
559
587
|
return {
|
|
560
588
|
provider: options.provider,
|
|
561
589
|
object: options.object,
|
|
562
|
-
mode:
|
|
590
|
+
mode: resolveLiveDryRunMode(options),
|
|
563
591
|
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
564
592
|
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
565
593
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -620,7 +648,7 @@ function buildCrmSearchBody(query, options) {
|
|
|
620
648
|
};
|
|
621
649
|
}
|
|
622
650
|
function buildCrmMergeBody(object, survivorRowId, loserRowId, options) {
|
|
623
|
-
const mode =
|
|
651
|
+
const mode = resolveLiveDryRunMode(options);
|
|
624
652
|
if (mode === "live" && options.confirm !== true) {
|
|
625
653
|
throw new OxygenError("confirm_required", "crm merge --live requires --confirm after inspecting the dry-run field diff.", { exitCode: 1 });
|
|
626
654
|
}
|
|
@@ -635,7 +663,7 @@ function buildCrmMergeBody(object, survivorRowId, loserRowId, options) {
|
|
|
635
663
|
}
|
|
636
664
|
function buildNotetakerSetupBody(options) {
|
|
637
665
|
return {
|
|
638
|
-
mode:
|
|
666
|
+
mode: resolveLiveDryRunMode(options),
|
|
639
667
|
enabled: options.disabled === true ? false : true,
|
|
640
668
|
capture_mode: "invited_bot",
|
|
641
669
|
auto_record_scope: "manual",
|
|
@@ -647,7 +675,7 @@ function buildNotetakerSetupBody(options) {
|
|
|
647
675
|
}
|
|
648
676
|
function buildNotetakerScheduleBody(meetingUrl, options) {
|
|
649
677
|
return {
|
|
650
|
-
mode:
|
|
678
|
+
mode: resolveLiveDryRunMode(options),
|
|
651
679
|
meeting_url: meetingUrl,
|
|
652
680
|
approved: options.approved === true,
|
|
653
681
|
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
@@ -657,12 +685,6 @@ function buildNotetakerScheduleBody(meetingUrl, options) {
|
|
|
657
685
|
...(readOption(options.crmLinksJson) ? { crm_links: parseJsonArray(readOption(options.crmLinksJson) ?? "") } : {}),
|
|
658
686
|
};
|
|
659
687
|
}
|
|
660
|
-
function resolveNotetakerMode(options) {
|
|
661
|
-
if (options.live === true && options.dryRun === true) {
|
|
662
|
-
throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
|
|
663
|
-
}
|
|
664
|
-
return options.live === true ? "live" : "dry_run";
|
|
665
|
-
}
|
|
666
688
|
function buildPublishingPostsListPath(options) {
|
|
667
689
|
const query = new URLSearchParams();
|
|
668
690
|
const status = readOption(options.status);
|
|
@@ -803,16 +825,93 @@ function readPublishingPostText(options, required) {
|
|
|
803
825
|
return null;
|
|
804
826
|
throw new OxygenError("invalid_request", "Pass --text or --text-file.", { exitCode: 1 });
|
|
805
827
|
}
|
|
806
|
-
// Read
|
|
807
|
-
// only the basename the user passed — never the resolved absolute filesystem
|
|
808
|
-
// Node's ENOENT would otherwise leak into the CLI error output.
|
|
809
|
-
|
|
828
|
+
// Read a file-backed flag, sanitizing the OS error so a missing/unreadable file
|
|
829
|
+
// reports only the basename the user passed — never the resolved absolute filesystem
|
|
830
|
+
// path Node's ENOENT would otherwise leak into the CLI error output. `flag` names the
|
|
831
|
+
// option in the error so --source-file doesn't report itself as --text-file.
|
|
832
|
+
function readPublishingTextFile(textFile, flag = "--text-file") {
|
|
810
833
|
try {
|
|
811
834
|
return readFileSync(resolve(textFile), "utf8").trimEnd();
|
|
812
835
|
}
|
|
813
836
|
catch {
|
|
814
|
-
throw new OxygenError("text_file_unreadable", `Couldn't read
|
|
837
|
+
throw new OxygenError("text_file_unreadable", `Couldn't read ${flag} '${basename(textFile)}'. Check the path exists and is readable.`, { exitCode: 1 });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// --source-text / --source-file for the AI draft templates that rewrite a source
|
|
841
|
+
// (summarize, repurpose, podcast_clip, hashtags). Mirrors readPublishingPostText.
|
|
842
|
+
function readPublishingSourceText(options) {
|
|
843
|
+
const inlineText = readOption(options.sourceText);
|
|
844
|
+
const sourceFile = readOption(options.sourceFile);
|
|
845
|
+
if (inlineText && sourceFile) {
|
|
846
|
+
throw new OxygenError("conflicting_flags", "Pass either --source-text or --source-file, not both.", {
|
|
847
|
+
exitCode: 1,
|
|
848
|
+
});
|
|
815
849
|
}
|
|
850
|
+
if (sourceFile)
|
|
851
|
+
return readPublishingTextFile(sourceFile, "--source-file");
|
|
852
|
+
return inlineText?.trim() ? inlineText : null;
|
|
853
|
+
}
|
|
854
|
+
function buildPublishingPostsDraftBody(options) {
|
|
855
|
+
const body = {
|
|
856
|
+
template: options.template,
|
|
857
|
+
max_credits: readPositiveNumber(options.maxCredits),
|
|
858
|
+
};
|
|
859
|
+
const topic = readOption(options.topic);
|
|
860
|
+
const channels = readOption(options.channels);
|
|
861
|
+
const variants = readPositiveInt(options.variants);
|
|
862
|
+
const sourceText = readPublishingSourceText(options);
|
|
863
|
+
const sourceUrl = readOption(options.sourceUrl);
|
|
864
|
+
if (topic)
|
|
865
|
+
body.topic = topic;
|
|
866
|
+
if (channels) {
|
|
867
|
+
body.channels = channels.split(",").map((channel) => channel.trim()).filter(Boolean);
|
|
868
|
+
}
|
|
869
|
+
if (variants !== undefined)
|
|
870
|
+
body.variants = variants;
|
|
871
|
+
// Commander's --no-ground sets ground=false; the default (true) is the API's too,
|
|
872
|
+
// so only the explicit opt-out is worth sending.
|
|
873
|
+
if (options.ground === false)
|
|
874
|
+
body.ground = false;
|
|
875
|
+
if (sourceText)
|
|
876
|
+
body.source_text = sourceText;
|
|
877
|
+
if (sourceUrl)
|
|
878
|
+
body.source_url = sourceUrl;
|
|
879
|
+
return body;
|
|
880
|
+
}
|
|
881
|
+
function buildPublishingDraftsListPath(options) {
|
|
882
|
+
const query = new URLSearchParams();
|
|
883
|
+
const kind = readOption(options.kind);
|
|
884
|
+
const status = readOption(options.status);
|
|
885
|
+
const limit = readOption(options.limit);
|
|
886
|
+
if (kind)
|
|
887
|
+
query.set("kind", kind);
|
|
888
|
+
if (status)
|
|
889
|
+
query.set("status", status);
|
|
890
|
+
if (limit)
|
|
891
|
+
query.set("limit", limit);
|
|
892
|
+
const suffix = query.toString();
|
|
893
|
+
return suffix ? `/api/cli/publishing/drafts?${suffix}` : "/api/cli/publishing/drafts";
|
|
894
|
+
}
|
|
895
|
+
function buildPublishingDraftsAcceptBody(options) {
|
|
896
|
+
const body = {
|
|
897
|
+
variant_index: readNonNegativeInt(options.variant) ?? 0,
|
|
898
|
+
};
|
|
899
|
+
const publishAt = readOption(options.publishAt);
|
|
900
|
+
const channel = readOption(options.channel);
|
|
901
|
+
const sender = readOption(options.sender);
|
|
902
|
+
const providerConnection = readOption(options.providerConnection);
|
|
903
|
+
const timezone = readOption(options.timezone);
|
|
904
|
+
if (publishAt)
|
|
905
|
+
body.publish_at = publishAt;
|
|
906
|
+
if (channel)
|
|
907
|
+
body.channel = channel;
|
|
908
|
+
if (sender)
|
|
909
|
+
body.sender_account_id = sender;
|
|
910
|
+
if (providerConnection)
|
|
911
|
+
body.provider_connection_id = providerConnection;
|
|
912
|
+
if (timezone)
|
|
913
|
+
body.timezone = timezone;
|
|
914
|
+
return body;
|
|
816
915
|
}
|
|
817
916
|
function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
818
917
|
return {
|
|
@@ -824,12 +923,12 @@ function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
|
824
923
|
row_id: options.targetRowId,
|
|
825
924
|
},
|
|
826
925
|
...(readOption(options.role) ? { role: readOption(options.role) } : {}),
|
|
827
|
-
mode:
|
|
926
|
+
mode: resolveLiveDryRunMode(options),
|
|
828
927
|
};
|
|
829
928
|
}
|
|
830
929
|
function buildCrmActivityBody(object, rowId, options) {
|
|
831
930
|
const body = {
|
|
832
|
-
mode:
|
|
931
|
+
mode: resolveLiveDryRunMode(options),
|
|
833
932
|
activity_type: options.type,
|
|
834
933
|
links: [{ object, row_id: rowId, role: options.role ?? "actor" }],
|
|
835
934
|
};
|
|
@@ -853,7 +952,7 @@ function buildCrmActivityBody(object, rowId, options) {
|
|
|
853
952
|
}
|
|
854
953
|
function buildCrmActivityNoteBody(object, rowId, text, options) {
|
|
855
954
|
return {
|
|
856
|
-
mode:
|
|
955
|
+
mode: resolveLiveDryRunMode(options),
|
|
857
956
|
activity_type: "note",
|
|
858
957
|
summary: text,
|
|
859
958
|
links: [{ object, row_id: rowId, role: "actor" }],
|
|
@@ -936,6 +1035,17 @@ function buildCrmLeadsTodayPath(options) {
|
|
|
936
1035
|
const query = params.toString();
|
|
937
1036
|
return query ? `/api/cli/crm/signals/leads-today?${query}` : "/api/cli/crm/signals/leads-today";
|
|
938
1037
|
}
|
|
1038
|
+
function buildSignalsListPath(options) {
|
|
1039
|
+
const params = new URLSearchParams();
|
|
1040
|
+
if (options.types)
|
|
1041
|
+
params.set("types", options.types);
|
|
1042
|
+
if (options.since)
|
|
1043
|
+
params.set("since", options.since);
|
|
1044
|
+
if (options.limit)
|
|
1045
|
+
params.set("limit", options.limit);
|
|
1046
|
+
const query = params.toString();
|
|
1047
|
+
return query ? `/api/cli/signals?${query}` : "/api/cli/signals";
|
|
1048
|
+
}
|
|
939
1049
|
function buildCrmAutomationSetBody(templateId, options) {
|
|
940
1050
|
if (options.armed === true && options.disarmed === true) {
|
|
941
1051
|
throw new OxygenError("conflicting_flags", "Pass either --armed or --disarmed, not both.", { exitCode: 1 });
|
|
@@ -946,7 +1056,7 @@ function buildCrmAutomationSetBody(templateId, options) {
|
|
|
946
1056
|
const body = {
|
|
947
1057
|
template_id: templateId,
|
|
948
1058
|
armed: options.armed === true,
|
|
949
|
-
mode:
|
|
1059
|
+
mode: resolveLiveDryRunMode(options),
|
|
950
1060
|
};
|
|
951
1061
|
if (options.configJson)
|
|
952
1062
|
body.config = parseJsonObject(options.configJson);
|
|
@@ -966,7 +1076,7 @@ function buildCrmListCreateBody(slug, options) {
|
|
|
966
1076
|
slug,
|
|
967
1077
|
base_object: options.base,
|
|
968
1078
|
kind: options.kind,
|
|
969
|
-
mode:
|
|
1079
|
+
mode: resolveLiveDryRunMode(options),
|
|
970
1080
|
};
|
|
971
1081
|
if (options.displayName)
|
|
972
1082
|
body.display_name = options.displayName;
|
|
@@ -980,7 +1090,7 @@ function buildCrmListEntryBody(object, rowId, options) {
|
|
|
980
1090
|
return {
|
|
981
1091
|
object,
|
|
982
1092
|
row_id: rowId,
|
|
983
|
-
mode:
|
|
1093
|
+
mode: resolveLiveDryRunMode(options),
|
|
984
1094
|
};
|
|
985
1095
|
}
|
|
986
1096
|
function buildCrmListMembersPath(list, options) {
|
|
@@ -1126,6 +1236,7 @@ export function createProgram() {
|
|
|
1126
1236
|
.option("--token <token>", "CLI API token created in the Oxygen dashboard.")
|
|
1127
1237
|
.option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
1128
1238
|
.option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
|
|
1239
|
+
.option("--force", "Repoint an existing profile at a different API host. Refused without this flag.")
|
|
1129
1240
|
.option("--no-browser", "Skip the browser handoff and paste the token manually.")
|
|
1130
1241
|
.option("--json", "Print a JSON envelope.")
|
|
1131
1242
|
.action(async (options) => {
|
|
@@ -1139,6 +1250,7 @@ export function createProgram() {
|
|
|
1139
1250
|
.requiredOption("--token <token>", "CLI API token.")
|
|
1140
1251
|
.option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
1141
1252
|
.option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
|
|
1253
|
+
.option("--force", "Repoint an existing profile at a different API host. Refused without this flag.")
|
|
1142
1254
|
.option("--json", "Print a JSON envelope.")
|
|
1143
1255
|
.action(async (options) => {
|
|
1144
1256
|
await handleAuthUseTokenAction(options);
|
|
@@ -1834,6 +1946,98 @@ export function createProgram() {
|
|
|
1834
1946
|
await handleAsyncAction("publishing posts retry", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/retry`, {
|
|
1835
1947
|
method: "POST",
|
|
1836
1948
|
}));
|
|
1949
|
+
}))
|
|
1950
|
+
.addCommand(new Command("delete")
|
|
1951
|
+
.description("Delete a never-attempted post from the queue (draft, scheduled, or queued with no publish attempts). A post that was ever attempted — published, publishing, failed, or re-armed after a provider error — is refused: cancel it instead, so its attempt history is kept. To remove a LinkedIn post that already went live, use `oxygen posts delete`.")
|
|
1952
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1953
|
+
.option("--json", "Print a JSON envelope.")
|
|
1954
|
+
.action(async (postId, options) => {
|
|
1955
|
+
await handleAsyncAction("publishing posts delete", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}`, {
|
|
1956
|
+
method: "DELETE",
|
|
1957
|
+
}));
|
|
1958
|
+
}))
|
|
1959
|
+
.addCommand(new Command("draft")
|
|
1960
|
+
.description("Draft posts with AI, grounded in the workspace Knowledge Graph. PAID (one AI call) — requires --max-credits. Writes drafts only: nothing is scheduled or published until you accept a draft and approve the post.")
|
|
1961
|
+
.requiredOption("--template <key>", "Draft template: summarize, repurpose, changelog, feature_launch, case_study, personal_story, startup_advice, company_milestone, hiring, fundraise, hook_generator, popular_questions, tech_stack, podcast_clip, or hashtags.")
|
|
1962
|
+
.requiredOption("--max-credits <n>", "Credit cap for the AI call (required — this is a paid action).")
|
|
1963
|
+
.option("--topic <topic>", "What the post is about. Required by most templates.")
|
|
1964
|
+
.option("--channels <list>", "Comma-separated target channels: linkedin, x, instagram, tiktok, facebook, youtube.")
|
|
1965
|
+
.option("--variants <n>", "How many angles to draft (1-5). Defaults to 3.")
|
|
1966
|
+
.option("--no-ground", "Skip Knowledge-Graph page retrieval. The canonical brand voice still applies.")
|
|
1967
|
+
.option("--source-text <text>", "Source text for summarize, repurpose, podcast_clip, and hashtags.")
|
|
1968
|
+
.option("--source-file <path>", "Read the source text from a local file.")
|
|
1969
|
+
.option("--source-url <url>", "Source URL to reference.")
|
|
1970
|
+
.option("--json", "Print a JSON envelope.")
|
|
1971
|
+
.action(async (options) => {
|
|
1972
|
+
await handleAsyncAction("publishing posts draft", options, () => requestOxygen("/api/cli/publishing/posts/draft", {
|
|
1973
|
+
method: "POST",
|
|
1974
|
+
body: buildPublishingPostsDraftBody(options),
|
|
1975
|
+
}));
|
|
1976
|
+
}))
|
|
1977
|
+
.addCommand(new Command("review")
|
|
1978
|
+
.description("Pre-publish review of a post: deterministic channel lint (free, and blocking at approve) plus an AI voice/claims check against the Knowledge Graph. PAID (one AI call) — requires --max-credits. AI findings never block approval.")
|
|
1979
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1980
|
+
.requiredOption("--max-credits <n>", "Credit cap for the AI review call (required — this is a paid action).")
|
|
1981
|
+
.option("--json", "Print a JSON envelope.")
|
|
1982
|
+
.action(async (postId, options) => {
|
|
1983
|
+
await handleAsyncAction("publishing posts review", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/review`, {
|
|
1984
|
+
method: "POST",
|
|
1985
|
+
body: { max_credits: readPositiveNumber(options.maxCredits) },
|
|
1986
|
+
}));
|
|
1987
|
+
})))
|
|
1988
|
+
.addCommand(new Command("drafts")
|
|
1989
|
+
.description("Review the AI post-draft queue before anything reaches the publish queue.")
|
|
1990
|
+
.addCommand(new Command("list")
|
|
1991
|
+
.description("List post drafts. Defaults to the open queue (queued + edited).")
|
|
1992
|
+
.option("--kind <kind>", "Filter by ai or idea.")
|
|
1993
|
+
.option("--status <list>", "Comma-separated statuses: queued, edited, accepted, rejected.")
|
|
1994
|
+
.option("--limit <n>", "Maximum drafts to return.")
|
|
1995
|
+
.option("--json", "Print a JSON envelope.")
|
|
1996
|
+
.action(async (options) => {
|
|
1997
|
+
await handleAsyncAction("publishing drafts list", options, () => requestOxygen(buildPublishingDraftsListPath(options)));
|
|
1998
|
+
}))
|
|
1999
|
+
.addCommand(new Command("accept")
|
|
2000
|
+
.description("Accept a draft variant into the publish queue. The post is created needs-approval — approve it separately before it can publish.")
|
|
2001
|
+
.argument("<draft_id>", "Post draft id.")
|
|
2002
|
+
.option("--variant <i>", "Which variant to accept (0-based). Defaults to 0.")
|
|
2003
|
+
.option("--publish-at <iso>", "ISO date-time to schedule the post for. Defaults to now (still needs approval).")
|
|
2004
|
+
.option("--channel <channel>", "Channel override. Defaults to the draft's first target channel.")
|
|
2005
|
+
.option("--sender <sender_account_id>", "LinkedIn sender account id.")
|
|
2006
|
+
.option("--provider-connection <connection_id>", "Oxygen integration connection id for Composio-backed providers.")
|
|
2007
|
+
.option("--timezone <tz>", "Display timezone for the scheduled date.")
|
|
2008
|
+
.option("--json", "Print a JSON envelope.")
|
|
2009
|
+
.action(async (draftId, options) => {
|
|
2010
|
+
await handleAsyncAction("publishing drafts accept", options, () => requestOxygen(`/api/cli/publishing/drafts/${encodeURIComponent(draftId)}/accept`, {
|
|
2011
|
+
method: "POST",
|
|
2012
|
+
body: buildPublishingDraftsAcceptBody(options),
|
|
2013
|
+
}));
|
|
2014
|
+
}))
|
|
2015
|
+
.addCommand(new Command("reject")
|
|
2016
|
+
.description("Reject a draft. The reason is filed to the Knowledge Graph so the next drafts learn from it.")
|
|
2017
|
+
.argument("<draft_id>", "Post draft id.")
|
|
2018
|
+
.option("--reason <reason>", "Why this draft was rejected.")
|
|
2019
|
+
.option("--json", "Print a JSON envelope.")
|
|
2020
|
+
.action(async (draftId, options) => {
|
|
2021
|
+
await handleAsyncAction("publishing drafts reject", options, () => requestOxygen(`/api/cli/publishing/drafts/${encodeURIComponent(draftId)}/reject`, {
|
|
2022
|
+
method: "POST",
|
|
2023
|
+
body: readOption(options.reason) ? { reason: readOption(options.reason) } : {},
|
|
2024
|
+
}));
|
|
2025
|
+
}))
|
|
2026
|
+
.addCommand(new Command("edit")
|
|
2027
|
+
.description("Rewrite one variant's text. The first edit snapshots the AI's original copy, so accepting it teaches the Knowledge Graph what you changed.")
|
|
2028
|
+
.argument("<draft_id>", "Post draft id.")
|
|
2029
|
+
.option("--variant <i>", "Which variant to edit (0-based). Defaults to 0.")
|
|
2030
|
+
.option("--text <text>", "The replacement post text.")
|
|
2031
|
+
.option("--text-file <path>", "Read the replacement text from a local file.")
|
|
2032
|
+
.option("--json", "Print a JSON envelope.")
|
|
2033
|
+
.action(async (draftId, options) => {
|
|
2034
|
+
await handleAsyncAction("publishing drafts edit", options, () => requestOxygen(`/api/cli/publishing/drafts/${encodeURIComponent(draftId)}`, {
|
|
2035
|
+
method: "PATCH",
|
|
2036
|
+
body: {
|
|
2037
|
+
variant_index: readNonNegativeInt(options.variant) ?? 0,
|
|
2038
|
+
text: readPublishingPostText(options, true),
|
|
2039
|
+
},
|
|
2040
|
+
}));
|
|
1837
2041
|
})))
|
|
1838
2042
|
.addCommand(new Command("media")
|
|
1839
2043
|
.description("Manage Publishing media uploads.")
|
|
@@ -2326,6 +2530,52 @@ export function createProgram() {
|
|
|
2326
2530
|
.action(async (object, rowId, options) => {
|
|
2327
2531
|
await handleAsyncAction("crm sync links", options, () => requestOxygen(buildCrmSyncLinksPath(object, rowId)));
|
|
2328
2532
|
})));
|
|
2533
|
+
program
|
|
2534
|
+
.command("signals")
|
|
2535
|
+
.description("The Signals primitive: the typed GTM intent-event stream — record signals, list the raw event feed, inspect the type registry, and read the ranked leads-to-contact-today queue.")
|
|
2536
|
+
.addCommand(new Command("list")
|
|
2537
|
+
.description("List the raw GTM signal-event feed newest-first (website_visit / profile_view / post_reaction / new_follower), each resolved onto the person it attached to. Read-only.")
|
|
2538
|
+
.option("--types <types>", "Comma-separated signal types to include. Defaults to all four.")
|
|
2539
|
+
.option("--since <days>", "Look-back window in days. Defaults to 7, max 30.")
|
|
2540
|
+
.option("--limit <limit>", "Maximum events to return. Defaults to 25, max 100.")
|
|
2541
|
+
.option("--json", "Print a JSON envelope.")
|
|
2542
|
+
.action(async (options) => {
|
|
2543
|
+
await handleAsyncAction("signals list", options, () => requestOxygen(buildSignalsListPath(options)));
|
|
2544
|
+
}))
|
|
2545
|
+
.addCommand(new Command("registry")
|
|
2546
|
+
.description("Show the signal-type registry: each registered type with its intent weight and capture status (all four registered types are live end-to-end — website_visit via the RB2B webhook, the engagement family via the engagement→signal bridge). Read-only.")
|
|
2547
|
+
.option("--json", "Print a JSON envelope.")
|
|
2548
|
+
.action(async (options) => {
|
|
2549
|
+
await handleAsyncAction("signals registry", options, () => requestOxygen("/api/cli/signals/registry"));
|
|
2550
|
+
}))
|
|
2551
|
+
.addCommand(new Command("record")
|
|
2552
|
+
.description("Record one inbound GTM signal (website_visit, profile_view, post_engager, new_follower). Internal write — free, no approval. Idempotent on --external-event-id.")
|
|
2553
|
+
.requiredOption("--event <event>", "Signal event: website_visit | profile_view | post_engager | new_follower.")
|
|
2554
|
+
.option("--external-event-id <id>", "Stable provider event id (redelivery/idempotency guard). Derived from --session-id when omitted.")
|
|
2555
|
+
.option("--email <email>", "Person email (identity).")
|
|
2556
|
+
.option("--linkedin-url <url>", "Person LinkedIn URL (identity when no email).")
|
|
2557
|
+
.option("--company-domain <domain>", "Company domain for the company assert.")
|
|
2558
|
+
.option("--company-name <name>", "Company name.")
|
|
2559
|
+
.option("--first-name <name>", "Person first name.")
|
|
2560
|
+
.option("--last-name <name>", "Person last name.")
|
|
2561
|
+
.option("--page-url <url>", "Page the signal fired on (website visit).")
|
|
2562
|
+
.option("--session-id <id>", "Provider session id (provenance + idempotency).")
|
|
2563
|
+
.option("--payload-json <json>", "JSON object merged into the signal payload (wins over the flags above).")
|
|
2564
|
+
.option("--json", "Print a JSON envelope.")
|
|
2565
|
+
.action(async (options) => {
|
|
2566
|
+
await handleAsyncAction("signals record", options, () => requestOxygen("/api/cli/signals", {
|
|
2567
|
+
method: "POST",
|
|
2568
|
+
body: buildCrmSignalRecordBody(options),
|
|
2569
|
+
}));
|
|
2570
|
+
}))
|
|
2571
|
+
.addCommand(new Command("leads-today")
|
|
2572
|
+
.description("List people to contact today, ranked by their most recent GTM intent signal. Do-not-contact filtering is best-effort (email-only leads surface unchecked).")
|
|
2573
|
+
.option("--limit <limit>", "Maximum leads to return. Defaults to 25, max 100.")
|
|
2574
|
+
.option("--within-days <days>", "Signal look-back window in days. Defaults to 7, max 30.")
|
|
2575
|
+
.option("--json", "Print a JSON envelope.")
|
|
2576
|
+
.action(async (options) => {
|
|
2577
|
+
await handleAsyncAction("signals leads-today", options, () => requestOxygen(buildCrmLeadsTodayPath(options)));
|
|
2578
|
+
}));
|
|
2329
2579
|
const tablesCommand = program
|
|
2330
2580
|
.command("tables")
|
|
2331
2581
|
.description("Tenant workspace table commands.")
|
|
@@ -2518,7 +2768,7 @@ export function createProgram() {
|
|
|
2518
2768
|
await handleAsyncAction("tables query", options, () => {
|
|
2519
2769
|
const limit = readPositiveInt(options.limit);
|
|
2520
2770
|
const filters = readFilterJsonOption(options.filterJson);
|
|
2521
|
-
const filterTree =
|
|
2771
|
+
const filterTree = readJsonObjectOption(options.filterTreeJson);
|
|
2522
2772
|
const sorts = readSortJsonOption(options.sortJson);
|
|
2523
2773
|
if (filters && (filterTree || sorts)) {
|
|
2524
2774
|
throw new OxygenError("invalid_filter", "Pass either --filter-json (legacy) or --filter-tree-json/--sort-json, not both.", { exitCode: 1 });
|
|
@@ -2953,7 +3203,7 @@ export function createProgram() {
|
|
|
2953
3203
|
.option("--body <text>", "Page body (Markdown, supports [[wikilinks]]).")
|
|
2954
3204
|
.option("--data-json <json>", "Flexible JSON object for structured page data.")
|
|
2955
3205
|
.option("--expected-revision <n>", "Fail the write if the current page revision differs (optimistic concurrency guard).")
|
|
2956
|
-
.option("--approved", "
|
|
3206
|
+
.option("--approved", "Accepted for compatibility but never honored for canonical live copy: a gated edit always files a proposal - approve it with `oxygen knowledge proposals approve <id>`.")
|
|
2957
3207
|
.option("--json", "Print a JSON envelope.")
|
|
2958
3208
|
.action(async (options) => {
|
|
2959
3209
|
await handleAsyncAction("knowledge page upsert", options, async () => {
|
|
@@ -3007,7 +3257,7 @@ export function createProgram() {
|
|
|
3007
3257
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
3008
3258
|
.option("--canonical", "Pin as the canonical default for its type (default).")
|
|
3009
3259
|
.option("--no-canonical", "Unpin instead of pin (clears the canonical default for this type).")
|
|
3010
|
-
.option("--approved", "
|
|
3260
|
+
.option("--approved", "Accepted for compatibility but never honored: pinning a sensitive type (voice, brand, positioning, playbooks) always files a proposal - approve it with `oxygen knowledge proposals approve <id>`.")
|
|
3011
3261
|
.option("--json", "Print a JSON envelope.")
|
|
3012
3262
|
.action(async (slugOrId, options) => {
|
|
3013
3263
|
await handleAsyncAction("knowledge page pin", options, async () => {
|
|
@@ -3083,8 +3333,10 @@ export function createProgram() {
|
|
|
3083
3333
|
await handleAsyncAction("knowledge index", options, () => requestOxygen("/api/cli/knowledge/index"));
|
|
3084
3334
|
}))
|
|
3085
3335
|
.addCommand(new Command("graph")
|
|
3086
|
-
.description("Knowledge graph of pages and their [[wikilink]] edges.")
|
|
3336
|
+
.description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph.")
|
|
3087
3337
|
.option("--max-nodes <n>", "Maximum nodes to include in the graph.")
|
|
3338
|
+
.option("--center <slug_or_id>", "Center the graph on one page and show only its neighborhood (local mode).")
|
|
3339
|
+
.option("--depth <n>", "Local-graph hop depth (1-3, default 1). Only applies with --center.")
|
|
3088
3340
|
.option("--json", "Print a JSON envelope.")
|
|
3089
3341
|
.action(async (options) => {
|
|
3090
3342
|
await handleAsyncAction("knowledge graph", options, () => requestOxygen("/api/cli/knowledge/graph", {
|
|
@@ -3093,7 +3345,7 @@ export function createProgram() {
|
|
|
3093
3345
|
}));
|
|
3094
3346
|
}))
|
|
3095
3347
|
.addCommand(new Command("lint")
|
|
3096
|
-
.description("Structural wiki health report: orphans, unresolved links, missing canonicals, stale/oversized pages, aged proposals.")
|
|
3348
|
+
.description("Structural wiki health report: orphans, dead-ends, unresolved links, missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs, decisions missing a reversal condition, stale/oversized pages, and aged proposals.")
|
|
3097
3349
|
.option("--json", "Print a JSON envelope.")
|
|
3098
3350
|
.action(async (options) => {
|
|
3099
3351
|
await handleAsyncAction("knowledge lint", options, () => requestOxygen("/api/cli/knowledge/lint"));
|
|
@@ -3210,7 +3462,7 @@ export function createProgram() {
|
|
|
3210
3462
|
}));
|
|
3211
3463
|
})))
|
|
3212
3464
|
.addCommand(new Command("seed")
|
|
3213
|
-
.description("Seed the knowledge wiki
|
|
3465
|
+
.description("Seed or upgrade the knowledge wiki scaffold: on first run creates the starter page set — a start-here guide, a conventions schema page, and stub hubs (icp, positioning, offers, competitors, playbooks, campaign-learnings, decisions), all [[wikilinked]]. Idempotent; never overwrites your edits.")
|
|
3214
3466
|
.option("--json", "Print a JSON envelope.")
|
|
3215
3467
|
.action(async (options) => {
|
|
3216
3468
|
await handleAsyncAction("knowledge seed", options, () => requestOxygen("/api/cli/knowledge/seed", {
|
|
@@ -3240,14 +3492,45 @@ export function createProgram() {
|
|
|
3240
3492
|
},
|
|
3241
3493
|
}));
|
|
3242
3494
|
}))
|
|
3495
|
+
.addCommand(new Command("agent")
|
|
3496
|
+
.description("The scheduled knowledge synthesis agent: unattended distillation of auto-filed sources into learnings pages (default off, spend-capped). Canonical changes still go through the proposal queue.")
|
|
3497
|
+
.addCommand(new Command("get")
|
|
3498
|
+
.description("Show the synthesis-agent config (enabled, cadence, per-day credit cap).")
|
|
3499
|
+
.option("--json", "Print a JSON envelope.")
|
|
3500
|
+
.action(async (options) => {
|
|
3501
|
+
await handleAsyncAction("knowledge agent get", options, () => requestOxygen("/api/cli/knowledge/agent"));
|
|
3502
|
+
}))
|
|
3503
|
+
.addCommand(new Command("set")
|
|
3504
|
+
.description("Configure the synthesis agent. Enabling lets the background worker distill new sources on the cadence; it only writes working learnings pages.")
|
|
3505
|
+
.option("--enabled", "Enable unattended synthesis.")
|
|
3506
|
+
.option("--disabled", "Disable unattended synthesis.")
|
|
3507
|
+
.option("--cadence-hours <n>", "Hours between synthesis cycles (1-168, default 24).")
|
|
3508
|
+
.option("--max-credits-per-day <n>", "Managed-credit cap per UTC day (0-500, default 5).")
|
|
3509
|
+
.option("--json", "Print a JSON envelope.")
|
|
3510
|
+
.action(async (options) => {
|
|
3511
|
+
await handleAsyncAction("knowledge agent set", options, () => {
|
|
3512
|
+
const body = {};
|
|
3513
|
+
if (options.enabled)
|
|
3514
|
+
body.enabled = true;
|
|
3515
|
+
if (options.disabled)
|
|
3516
|
+
body.enabled = false;
|
|
3517
|
+
// Presence decides inclusion so an absent flag keeps the stored value.
|
|
3518
|
+
if (options.cadenceHours !== undefined)
|
|
3519
|
+
body.cadence_hours = Number(options.cadenceHours);
|
|
3520
|
+
if (options.maxCreditsPerDay !== undefined)
|
|
3521
|
+
body.max_credits_per_day = Number(options.maxCreditsPerDay);
|
|
3522
|
+
return requestOxygen("/api/cli/knowledge/agent", { method: "POST", body });
|
|
3523
|
+
});
|
|
3524
|
+
})))
|
|
3243
3525
|
.addCommand(new Command("sync")
|
|
3244
|
-
.description("Clone or refresh the local knowledge mirror: server-rendered page markdown under the CLI config dir.")
|
|
3526
|
+
.description("Clone or refresh the local knowledge mirror: server-rendered page markdown under the CLI config dir. Pass --all to mirror every workspace the credential can access.")
|
|
3527
|
+
.option("--all", "Sync every organization the credential can access, each into its own mirror; one org's failure never aborts the rest.")
|
|
3245
3528
|
.option("--full", "Purge the mirror (including its cursor) and re-clone every page.")
|
|
3246
3529
|
.option("--if-stale", "Skip when the mirror completed a sync within the TTL (or one is already running).")
|
|
3247
3530
|
.option("--ttl <minutes>", "Freshness window for --if-stale, in minutes. Defaults to 15.")
|
|
3248
3531
|
.option("--json", "Print a JSON envelope.")
|
|
3249
3532
|
.action(async (options) => {
|
|
3250
|
-
await handleAsyncAction("knowledge sync", options, () => runKnowledgeMirrorSync(options));
|
|
3533
|
+
await handleAsyncAction("knowledge sync", options, () => options.all ? runKnowledgeMirrorSyncAll(options) : runKnowledgeMirrorSync(options));
|
|
3251
3534
|
}))
|
|
3252
3535
|
.addCommand(new Command("push")
|
|
3253
3536
|
.description("Push locally edited or newly created mirror pages back to the workspace wiki. Edits to canonical voice/brand/positioning pages become approval proposals instead of writing live.")
|
|
@@ -3257,7 +3540,8 @@ export function createProgram() {
|
|
|
3257
3540
|
await handleAsyncAction("knowledge push", options, () => runKnowledgeMirrorPush(options));
|
|
3258
3541
|
}))
|
|
3259
3542
|
.addCommand(new Command("status")
|
|
3260
|
-
.description("Report the local knowledge mirror: path, page count, last sync, quarantined conflicts.")
|
|
3543
|
+
.description("Report the local knowledge mirror: path, page count, last sync, quarantined conflicts. Pass --all to report every locally mirrored workspace for the active API host.")
|
|
3544
|
+
.option("--all", "Report every locally mirrored workspace for the active API host, with a per-mirror freshness flag.")
|
|
3261
3545
|
.option("--print-path", "Print only the mirror directory path.")
|
|
3262
3546
|
.option("--clear-conflicts", "Delete quarantined conflict copies from the mirror sidecar.")
|
|
3263
3547
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -3266,7 +3550,7 @@ export function createProgram() {
|
|
|
3266
3550
|
await handleKnowledgeMirrorPrintPathAction();
|
|
3267
3551
|
return;
|
|
3268
3552
|
}
|
|
3269
|
-
await handleAsyncAction("knowledge status", options, () => runKnowledgeMirrorStatus(options));
|
|
3553
|
+
await handleAsyncAction("knowledge status", options, () => options.all ? runKnowledgeMirrorStatusAll() : runKnowledgeMirrorStatus(options));
|
|
3270
3554
|
}))
|
|
3271
3555
|
.addCommand(new Command("purge")
|
|
3272
3556
|
.description("Delete the whole local knowledge mirror for the active API host and organization.")
|
|
@@ -3511,7 +3795,8 @@ export function createProgram() {
|
|
|
3511
3795
|
});
|
|
3512
3796
|
}));
|
|
3513
3797
|
program.addCommand(buildPromptTemplatesCommand("prompts", "Reusable prompt templates layered into AI columns at run time."));
|
|
3514
|
-
|
|
3798
|
+
// The deprecated `templates` alias tree was removed at its registry sunset
|
|
3799
|
+
// (v1.290.0) — `oxygen prompts` has been canonical since v1.80.0.
|
|
3515
3800
|
program
|
|
3516
3801
|
.command("reviews")
|
|
3517
3802
|
.description("Human-in-the-loop reviews for AI-generated outreach messages.")
|
|
@@ -3869,6 +4154,20 @@ export function createProgram() {
|
|
|
3869
4154
|
},
|
|
3870
4155
|
});
|
|
3871
4156
|
});
|
|
4157
|
+
}))
|
|
4158
|
+
.addCommand(new Command("deps")
|
|
4159
|
+
.description("Show the column dependency graph: which columns feed which (formulas, input mappings, run conditions, waterfall targets), transitive up/downstream, and cycles. Read-only.")
|
|
4160
|
+
.argument("<table>", "Table id or slug.")
|
|
4161
|
+
.option("--column <key>", "Focus on one column: its upstream inputs, downstream dependents, and edges.")
|
|
4162
|
+
.option("--json", "Print a JSON envelope.")
|
|
4163
|
+
.action(async (table, options) => {
|
|
4164
|
+
await handleAsyncAction("columns deps", options, () => {
|
|
4165
|
+
const params = new URLSearchParams({ table });
|
|
4166
|
+
const column = readOption(options.column);
|
|
4167
|
+
if (column)
|
|
4168
|
+
params.set("column", column);
|
|
4169
|
+
return requestOxygen(`/api/cli/tables/columns/deps?${params.toString()}`, { method: "GET" });
|
|
4170
|
+
});
|
|
3872
4171
|
}));
|
|
3873
4172
|
program
|
|
3874
4173
|
.command("action-column")
|
|
@@ -3906,6 +4205,38 @@ export function createProgram() {
|
|
|
3906
4205
|
});
|
|
3907
4206
|
});
|
|
3908
4207
|
}));
|
|
4208
|
+
program
|
|
4209
|
+
.command("formulas")
|
|
4210
|
+
.description("Formula language for formula columns and per-column run conditions (only-run-if).")
|
|
4211
|
+
.addCommand(new Command("functions")
|
|
4212
|
+
.description("List every formula function (name, signature, examples) and the operator grammar. The same language powers formula columns and --run-condition gates.")
|
|
4213
|
+
.option("--category <category>", "Filter by category: logic, string, number, date, url_email, array, json, null_handling, cross_row.")
|
|
4214
|
+
.option("--json", "Print a JSON envelope.")
|
|
4215
|
+
.action(async (options) => {
|
|
4216
|
+
await handleAsyncAction("formulas functions", options, () => {
|
|
4217
|
+
const category = readOption(options.category);
|
|
4218
|
+
const query = category ? `?category=${encodeURIComponent(category)}` : "";
|
|
4219
|
+
return requestOxygen(`/api/cli/tables/formulas/functions${query}`, { method: "GET" });
|
|
4220
|
+
});
|
|
4221
|
+
}))
|
|
4222
|
+
.addCommand(new Command("validate")
|
|
4223
|
+
.description("Validate a formula against a table and preview its output on sample rows — free, no writes. Draft → validate → attach with `oxygen columns add --kind formula`.")
|
|
4224
|
+
.argument("<table>", "Table id or slug.")
|
|
4225
|
+
.requiredOption("--expression <expr>", "The formula expression to validate, e.g. \"if(score > 80, 'hot', 'warm')\".")
|
|
4226
|
+
.option("--rows <n>", "How many sample rows to evaluate (0-25, default 5).")
|
|
4227
|
+
.option("--row-id <id...>", "Evaluate specific row ids instead of the first rows.")
|
|
4228
|
+
.option("--json", "Print a JSON envelope.")
|
|
4229
|
+
.action(async (table, options) => {
|
|
4230
|
+
await handleAsyncAction("formulas validate", options, () => requestOxygen("/api/cli/tables/formulas/validate", {
|
|
4231
|
+
method: "POST",
|
|
4232
|
+
body: {
|
|
4233
|
+
table,
|
|
4234
|
+
expression: options.expression,
|
|
4235
|
+
...(readOption(options.rows) ? { sample_limit: Number(readOption(options.rows)) } : {}),
|
|
4236
|
+
...(options.rowId && options.rowId.length > 0 ? { row_ids: options.rowId } : {}),
|
|
4237
|
+
},
|
|
4238
|
+
}));
|
|
4239
|
+
}));
|
|
3909
4240
|
program
|
|
3910
4241
|
.command("table-runs")
|
|
3911
4242
|
.description("Durable background table action run commands.")
|
|
@@ -4665,6 +4996,24 @@ export function createProgram() {
|
|
|
4665
4996
|
...(readOption(options.note) ? { note: readOption(options.note) } : {}),
|
|
4666
4997
|
},
|
|
4667
4998
|
}));
|
|
4999
|
+
}))
|
|
5000
|
+
.addCommand(new Command("cancel")
|
|
5001
|
+
.description("Schedule the plan subscription to cancel at period end. During a trial the card is never charged; undo any time before then with billing resume.")
|
|
5002
|
+
.option("--json", "Print a JSON envelope.")
|
|
5003
|
+
.action(async (options) => {
|
|
5004
|
+
await handleAsyncAction("billing cancel", options, () => requestOxygen("/api/cli/billing/cancel", {
|
|
5005
|
+
method: "POST",
|
|
5006
|
+
body: { action: "cancel" },
|
|
5007
|
+
}));
|
|
5008
|
+
}))
|
|
5009
|
+
.addCommand(new Command("resume")
|
|
5010
|
+
.description("Remove a scheduled cancellation so the subscription renews (or the trial converts) normally.")
|
|
5011
|
+
.option("--json", "Print a JSON envelope.")
|
|
5012
|
+
.action(async (options) => {
|
|
5013
|
+
await handleAsyncAction("billing resume", options, () => requestOxygen("/api/cli/billing/cancel", {
|
|
5014
|
+
method: "POST",
|
|
5015
|
+
body: { action: "resume" },
|
|
5016
|
+
}));
|
|
4668
5017
|
}));
|
|
4669
5018
|
program
|
|
4670
5019
|
.command("budget")
|
|
@@ -5414,6 +5763,7 @@ export function createProgram() {
|
|
|
5414
5763
|
.option("--force", "Re-run rows with an existing target enrichment value.")
|
|
5415
5764
|
.option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run. Defaults to 20.")
|
|
5416
5765
|
.option("--json", "Print a JSON envelope.")
|
|
5766
|
+
.option("--approved", "Approve THIS run after inspecting `enrich-column preview` (free). Paid enrichment runs fail with approval_required (exit 7) without it.")
|
|
5417
5767
|
.action(async (table, options) => {
|
|
5418
5768
|
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
5419
5769
|
await handleAsyncAction("enrich-column run", options, () => requestOxygen("/api/cli/enrich-column/run", {
|
|
@@ -5421,6 +5771,7 @@ export function createProgram() {
|
|
|
5421
5771
|
body: {
|
|
5422
5772
|
...buildEnrichColumnBody(table, options),
|
|
5423
5773
|
max_credits: readPositiveNumber(options.maxCredits),
|
|
5774
|
+
...(options.approved ? { approved: true } : {}),
|
|
5424
5775
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
5425
5776
|
...(options.force ? { force: true } : {}),
|
|
5426
5777
|
},
|
|
@@ -6029,6 +6380,36 @@ export function createProgram() {
|
|
|
6029
6380
|
},
|
|
6030
6381
|
});
|
|
6031
6382
|
});
|
|
6383
|
+
}))
|
|
6384
|
+
.addCommand(new Command("delete")
|
|
6385
|
+
.description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove a post that is still only SCHEDULED in Oxygen, use `oxygen publishing posts delete` instead.")
|
|
6386
|
+
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (the id returned when the post was created).")
|
|
6387
|
+
.option("--account <ref>", "Sender account that authored the post (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
6388
|
+
.option("--approved", "Actually delete the post. Without it the command refuses and deletes nothing.")
|
|
6389
|
+
.option("--json", "Print a JSON envelope.")
|
|
6390
|
+
.action(async (options) => {
|
|
6391
|
+
await handleAsyncAction("posts delete", options, () => {
|
|
6392
|
+
const post = readOption(options.post);
|
|
6393
|
+
if (!post)
|
|
6394
|
+
throw new Error("--post is required (the composite social_id from `oxygen posts get`).");
|
|
6395
|
+
const account = readOption(options.account);
|
|
6396
|
+
// The DELETE route has no preview mode — it removes the live post on
|
|
6397
|
+
// the first call — so the approval gate lives here. Refuse rather than
|
|
6398
|
+
// silently deleting a real, public, irreversible artifact.
|
|
6399
|
+
if (!options.approved) {
|
|
6400
|
+
throw new OxygenError("approval_required", "Deleting a LinkedIn post is a real, irreversible public write. Re-run with --approved to delete it.", {
|
|
6401
|
+
details: { post, ...(account ? { account } : {}) },
|
|
6402
|
+
exitCode: 7,
|
|
6403
|
+
});
|
|
6404
|
+
}
|
|
6405
|
+
const params = new URLSearchParams();
|
|
6406
|
+
params.set("post", post);
|
|
6407
|
+
if (account)
|
|
6408
|
+
params.set("account", account);
|
|
6409
|
+
return requestOxygen(`/api/cli/linkedin/posts?${params.toString()}`, {
|
|
6410
|
+
method: "DELETE",
|
|
6411
|
+
});
|
|
6412
|
+
});
|
|
6032
6413
|
})));
|
|
6033
6414
|
program.addCommand(new Command("engagement")
|
|
6034
6415
|
.description("Harvest a LinkedIn post's engagers (reactors + commenters) into an enrollable CRM static list. The harvest runs as a slow, durable background drip under a dedicated conservative read budget — it never bursts and never starves the sequencer's own reads.")
|
|
@@ -7007,6 +7388,8 @@ export function createProgram() {
|
|
|
7007
7388
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
7008
7389
|
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
7009
7390
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'off' (default) rotates mailboxes freely; 'prefer' biases toward a mailbox on the recipient's own provider; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
7391
|
+
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
7392
|
+
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
|
|
7010
7393
|
.option("--no-stop-on-bounce", "Keep a lead's enrollment running after a hard bounce (default: stop it). The bounce is still recorded as an email_bounced signal either way.")
|
|
7011
7394
|
.option("--json", "Print a JSON envelope.")
|
|
7012
7395
|
.action(async (options) => {
|
|
@@ -7061,6 +7444,8 @@ export function createProgram() {
|
|
|
7061
7444
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
7062
7445
|
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
7063
7446
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'off' (default) rotates mailboxes freely; 'prefer' biases toward a mailbox on the recipient's own provider; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
7447
|
+
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
7448
|
+
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
|
|
7064
7449
|
.option("--no-stop-on-bounce", "Keep a lead's enrollment running after a hard bounce (default: stop it). The bounce is still recorded as an email_bounced signal either way.")
|
|
7065
7450
|
.option("--tracking-opens", "Turn open-pixel tracking ON for this sequence's native email sends (the default; injection still needs a verified tracking domain — see `oxygen domains tracking`).")
|
|
7066
7451
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
@@ -7123,9 +7508,10 @@ export function createProgram() {
|
|
|
7123
7508
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`));
|
|
7124
7509
|
}))
|
|
7125
7510
|
.addCommand(new Command("enroll")
|
|
7126
|
-
.description("Enroll leads into a sequence from a
|
|
7511
|
+
.description("Enroll leads into a sequence. Enrolling spends no credits and sends nothing — dispatch only happens at `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile URL is read from the bound table's URL column, no pre-resolved provider ids needed. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn lead needs a lead_provider_id, a lead_profile_url, or a table_row_id whose row carries a LinkedIn URL. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings.")
|
|
7127
7512
|
.argument("<sequence>", "Sequence id or slug.")
|
|
7128
|
-
.
|
|
7513
|
+
.option("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ row_values: { email }, lead_name }] } for cold email, or { lead_provider_id, lead_name, table_row_id, row_values } for LinkedIn / existing rows. Exactly one of --leads-file or --from-table.")
|
|
7514
|
+
.option("--from-table", "Enroll every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; re-run to continue). Exactly one of --leads-file or --from-table.")
|
|
7129
7515
|
.option("--exclude-contacted", "Also skip leads any OTHER active sequence is already contacting (cross-campaign exclusion). Off by default.")
|
|
7130
7516
|
.option("--suppress-list <ids>", "Per-call do-not-enroll lead provider ids dropped for this enroll only: a comma-separated list, or @<path> to a file of ids (comma/whitespace separated).")
|
|
7131
7517
|
.option("--ignore-sender-bindings", "Enroll leads even when they are already owned by a sender account outside this sequence's pool (overrides the one-person-one-sender guarantee). Off by default.")
|
|
@@ -7133,17 +7519,30 @@ export function createProgram() {
|
|
|
7133
7519
|
.action(async (sequence, options) => {
|
|
7134
7520
|
await handleAsyncAction("sequences enroll", options, () => {
|
|
7135
7521
|
const leadsPath = readOption(options.leadsFile);
|
|
7136
|
-
if (
|
|
7137
|
-
throw new Error("--leads-file
|
|
7138
|
-
|
|
7522
|
+
if (options.fromTable && leadsPath) {
|
|
7523
|
+
throw new Error("Provide exactly one of --leads-file or --from-table.");
|
|
7524
|
+
}
|
|
7525
|
+
if (!options.fromTable && !leadsPath) {
|
|
7526
|
+
throw new Error("Provide exactly one of --leads-file or --from-table.");
|
|
7527
|
+
}
|
|
7139
7528
|
const suppressList = parseSuppressListOption(options.suppressList);
|
|
7529
|
+
const sharedFlags = {
|
|
7530
|
+
...(options.excludeContacted ? { exclude_contacted: true } : {}),
|
|
7531
|
+
...(suppressList.length > 0 ? { suppress_list: suppressList } : {}),
|
|
7532
|
+
...(options.ignoreSenderBindings ? { ignore_sender_bindings: true } : {}),
|
|
7533
|
+
};
|
|
7534
|
+
if (options.fromTable) {
|
|
7535
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enroll`, {
|
|
7536
|
+
method: "POST",
|
|
7537
|
+
body: { from_table: true, ...sharedFlags },
|
|
7538
|
+
});
|
|
7539
|
+
}
|
|
7540
|
+
const parsed = readJsonFileValue(resolve(leadsPath), "--leads-file");
|
|
7140
7541
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enroll`, {
|
|
7141
7542
|
method: "POST",
|
|
7142
7543
|
body: {
|
|
7143
7544
|
leads: parsed.leads ?? [],
|
|
7144
|
-
...
|
|
7145
|
-
...(suppressList.length > 0 ? { suppress_list: suppressList } : {}),
|
|
7146
|
-
...(options.ignoreSenderBindings ? { ignore_sender_bindings: true } : {}),
|
|
7545
|
+
...sharedFlags,
|
|
7147
7546
|
},
|
|
7148
7547
|
});
|
|
7149
7548
|
});
|
|
@@ -7174,7 +7573,7 @@ export function createProgram() {
|
|
|
7174
7573
|
.addCommand(new Command("signal")
|
|
7175
7574
|
.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.")
|
|
7176
7575
|
.argument("<sequence>", "Sequence id or slug.")
|
|
7177
|
-
.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.")
|
|
7576
|
+
.requiredOption("--signal <name>", "Signal to record: linkedin_connected, linkedin_replied, whatsapp_replied, email_sent, email_opened, email_clicked, email_replied, email_bounced, company_hiring, company_raised_funds, job_change, new_hire, web_visit, intent.")
|
|
7178
7577
|
.option("--enrollment <id>", "Target enrollment id.")
|
|
7179
7578
|
.option("--lead <provider_id>", "Target enrollment by its lead provider id.")
|
|
7180
7579
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7386,16 +7785,19 @@ export function createProgram() {
|
|
|
7386
7785
|
});
|
|
7387
7786
|
})));
|
|
7388
7787
|
program.addCommand(new Command("managed-inboxes")
|
|
7389
|
-
.description("
|
|
7788
|
+
.description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, list/get your subscriptions, and cancel. Subscribe/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
|
|
7390
7789
|
.addCommand(new Command("subscribe")
|
|
7391
|
-
.description("Subscribe a domain + mailboxes as a managed monthly
|
|
7790
|
+
.description("Subscribe a domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured.")
|
|
7392
7791
|
.requiredOption("--domain <domain>", "Sending domain to register + host the mailboxes (e.g. send.acme.com).")
|
|
7393
|
-
.requiredOption("--provider <provider>", "Mailbox
|
|
7394
|
-
.option("--
|
|
7792
|
+
.requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
|
|
7793
|
+
.option("--vendor <vendor>", "Pin the vendor: inboxkit or cmr. Omit to let OXYGEN choose. A named vendor with no credential FAILS rather than falling back to another.")
|
|
7794
|
+
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\"}].")
|
|
7395
7795
|
.option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
|
|
7396
|
-
.option("--years <n>", "Years to register the domain for (
|
|
7397
|
-
.option("--
|
|
7398
|
-
.option("--
|
|
7796
|
+
.option("--years <n>", "Years to register the domain for (1-10). Defaults to 1.")
|
|
7797
|
+
.option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to https://<domain>.")
|
|
7798
|
+
.option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required — a registrar needs a real registrant.")
|
|
7799
|
+
.option("--warmup", "Add the vendor's warmup pool for every mailbox (a recurring monthly per-mailbox add-on).")
|
|
7800
|
+
.option("--infraguard", "Add deliverability monitoring for the domain — blacklist, DNS-drift, and bounce checks (a recurring monthly per-domain add-on).")
|
|
7399
7801
|
.option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
|
|
7400
7802
|
.option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
|
|
7401
7803
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7406,7 +7808,7 @@ export function createProgram() {
|
|
|
7406
7808
|
if (!domain)
|
|
7407
7809
|
throw new Error("--domain is required.");
|
|
7408
7810
|
if (!provider)
|
|
7409
|
-
throw new Error("--provider is required (google or
|
|
7811
|
+
throw new Error("--provider is required (google, microsoft, or azure).");
|
|
7410
7812
|
let mailboxes;
|
|
7411
7813
|
const mailboxesJson = readOption(options.mailboxes);
|
|
7412
7814
|
const filePath = readOption(options.file);
|
|
@@ -7421,6 +7823,8 @@ export function createProgram() {
|
|
|
7421
7823
|
throw new Error("Provide --mailboxes <json> or --file <path>.");
|
|
7422
7824
|
}
|
|
7423
7825
|
const years = readOption(options.years);
|
|
7826
|
+
const vendor = readOption(options.vendor);
|
|
7827
|
+
const redirectUrl = readOption(options.redirectUrl);
|
|
7424
7828
|
const billingPath = readOption(options.billing);
|
|
7425
7829
|
const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
|
|
7426
7830
|
const quote = readOption(options.quote);
|
|
@@ -7430,9 +7834,12 @@ export function createProgram() {
|
|
|
7430
7834
|
domain,
|
|
7431
7835
|
provider,
|
|
7432
7836
|
mailboxes,
|
|
7837
|
+
...(vendor ? { vendor } : {}),
|
|
7433
7838
|
...(years ? { years: Number(years) } : {}),
|
|
7839
|
+
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
7434
7840
|
...(billing ? { billing } : {}),
|
|
7435
|
-
...(options.
|
|
7841
|
+
...(options.warmup ? { warmup: true } : {}),
|
|
7842
|
+
...(options.infraguard ? { infraguard: true } : {}),
|
|
7436
7843
|
...(options.approved ? { approved: true } : {}),
|
|
7437
7844
|
...(quote ? { quote_id: quote } : {}),
|
|
7438
7845
|
},
|
|
@@ -7440,7 +7847,7 @@ export function createProgram() {
|
|
|
7440
7847
|
});
|
|
7441
7848
|
}))
|
|
7442
7849
|
.addCommand(new Command("list")
|
|
7443
|
-
.description("List the org's managed inbox subscriptions with
|
|
7850
|
+
.description("List the org's managed inbox subscriptions with vendor, platform, mailbox count, monthly USD price, lifecycle status, and internal billing posture. Read-only, 0 Oxygen credits.")
|
|
7444
7851
|
.option("--status <status>", "Filter by status: pending, active, renewing, past_due, expired, cancelled, or failed.")
|
|
7445
7852
|
.option("--json", "Print a JSON envelope.")
|
|
7446
7853
|
.action(async (options) => {
|
|
@@ -7454,14 +7861,36 @@ export function createProgram() {
|
|
|
7454
7861
|
});
|
|
7455
7862
|
}))
|
|
7456
7863
|
.addCommand(new Command("get")
|
|
7457
|
-
.description("Get one managed inbox subscription by domain (
|
|
7864
|
+
.description("Get one managed inbox subscription by domain (vendor, platform, mailbox count, monthly USD price, vendor order id, lifecycle status, auto-renew, period end, internal billing status). Read-only, 0 Oxygen credits.")
|
|
7458
7865
|
.argument("<domain>", "The managed inbox domain (e.g. send.acme.com).")
|
|
7459
7866
|
.option("--json", "Print a JSON envelope.")
|
|
7460
7867
|
.action(async (domain, options) => {
|
|
7461
7868
|
await handleAsyncAction("managed-inboxes get", options, () => requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}`));
|
|
7869
|
+
}))
|
|
7870
|
+
.addCommand(new Command("warmup")
|
|
7871
|
+
.description("Turn the warmup pool on or off for a managed domain's inboxes, after purchase. Warmup runs on the vendor's pool against mailboxes it provisioned, so it is only available on inboxes BOUGHT through Oxygen — an imported inbox you already owned must arrive already warmed. WITHOUT --approved this prints a priced preview; re-run with --approved to apply.")
|
|
7872
|
+
.argument("<domain>", "The managed inbox domain (e.g. send.acme.com).")
|
|
7873
|
+
.option("--enable", "Turn warmup on (a recurring monthly per-mailbox add-on).")
|
|
7874
|
+
.option("--disable", "Turn warmup off and stop billing for it.")
|
|
7875
|
+
.option("--approved", "Apply the change (otherwise a priced preview is returned).")
|
|
7876
|
+
.option("--json", "Print a JSON envelope.")
|
|
7877
|
+
.action(async (domain, options) => {
|
|
7878
|
+
await handleAsyncAction("managed-inboxes warmup", options, () => {
|
|
7879
|
+
if (options.enable === options.disable) {
|
|
7880
|
+
throw new Error("Pass exactly one of --enable or --disable.");
|
|
7881
|
+
}
|
|
7882
|
+
return requestOxygen("/api/cli/managed-inboxes/warmup", {
|
|
7883
|
+
method: "POST",
|
|
7884
|
+
body: {
|
|
7885
|
+
domain,
|
|
7886
|
+
enabled: options.enable === true,
|
|
7887
|
+
...(options.approved ? { approved: true } : {}),
|
|
7888
|
+
},
|
|
7889
|
+
});
|
|
7890
|
+
});
|
|
7462
7891
|
}))
|
|
7463
7892
|
.addCommand(new Command("cancel")
|
|
7464
|
-
.description("Cancel a managed inbox subscription by domain (stops
|
|
7893
|
+
.description("Cancel a managed inbox subscription by domain (stops the recurring monthly charge and frees the domain at period end). WITHOUT --approved prints a preview; re-run with --approved to cancel. Cancelling is free.")
|
|
7465
7894
|
.argument("<domain>", "The managed inbox domain to cancel.")
|
|
7466
7895
|
.option("--approved", "Actually cancel (otherwise a preview is returned).")
|
|
7467
7896
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7744,32 +8173,19 @@ export function createProgram() {
|
|
|
7744
8173
|
});
|
|
7745
8174
|
})))
|
|
7746
8175
|
.addCommand(new Command("order")
|
|
7747
|
-
.description("
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
.
|
|
7752
|
-
.
|
|
7753
|
-
.
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
const quote = readOption(options.quote);
|
|
7761
|
-
return requestOxygen("/api/cli/mailboxes/order", {
|
|
7762
|
-
method: "POST",
|
|
7763
|
-
body: {
|
|
7764
|
-
provider: readOption(options.provider),
|
|
7765
|
-
domains,
|
|
7766
|
-
...(perDomain ? { mailboxes_per_domain: Number(perDomain) } : {}),
|
|
7767
|
-
...(workspace ? { workspace_id: workspace } : {}),
|
|
7768
|
-
...(options.approved ? { approved: true } : {}),
|
|
7769
|
-
...(quote ? { quote_id: quote } : {}),
|
|
7770
|
-
},
|
|
7771
|
-
});
|
|
7772
|
-
});
|
|
8176
|
+
.description("RETIRED — managed mailboxes are now purchased as monthly subscriptions via `oxygen managed-inboxes subscribe` (InboxKit-backed). This legacy Zapmail order path was never enabled anywhere and now always fails with code `gone`.")
|
|
8177
|
+
// Swallow the old flags (--provider/--domains/--approved/...) so a
|
|
8178
|
+
// stale invocation reaches the tombstone envelope instead of dying
|
|
8179
|
+
// on commander's unknown-option parse error.
|
|
8180
|
+
.allowUnknownOption(true)
|
|
8181
|
+
.allowExcessArguments(true)
|
|
8182
|
+
.action(() => {
|
|
8183
|
+
emitCliFailure("mailboxes order", new OxygenError("gone", "`oxygen mailboxes order` is retired — managed mailboxes are now purchased as monthly subscriptions via `oxygen managed-inboxes subscribe` (InboxKit-backed). Preview a quote, then re-run with --approved --quote <id>. Nothing was ordered or charged.", {
|
|
8184
|
+
details: {
|
|
8185
|
+
replacement: "oxygen managed-inboxes subscribe",
|
|
8186
|
+
deep_link: "https://oxygen-agent.com/billing",
|
|
8187
|
+
},
|
|
8188
|
+
}));
|
|
7773
8189
|
}))
|
|
7774
8190
|
.addCommand(new Command("orders")
|
|
7775
8191
|
.description("List the org's managed mailbox provisioning orders (newest first).")
|
|
@@ -7953,14 +8369,14 @@ export function createProgram() {
|
|
|
7953
8369
|
await handleAsyncAction("domains postmaster status", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/postmaster`));
|
|
7954
8370
|
})))
|
|
7955
8371
|
.addCommand(new Command("add")
|
|
7956
|
-
.description("
|
|
8372
|
+
.description("BYOK CLOUDFLARE: onboard a domain registered elsewhere by creating a zone in your connected Cloudflare account. For managed domain + inbox provisioning use `managed-inboxes subscribe`.")
|
|
7957
8373
|
.argument("<domain>", "Apex domain to add, such as acme.com.")
|
|
7958
8374
|
.option("--json", "Print a JSON envelope.")
|
|
7959
8375
|
.action(async (domain, options) => {
|
|
7960
8376
|
await handleAsyncAction("domains add", options, () => requestOxygen("/api/cli/domains/add", { method: "POST", body: { domain } }));
|
|
7961
8377
|
}))
|
|
7962
8378
|
.addCommand(new Command("search")
|
|
7963
|
-
.description("
|
|
8379
|
+
.description("BYOK CLOUDFLARE: search Registrar for available domains with registration/renewal pricing. Free — nothing is purchased.")
|
|
7964
8380
|
.argument("<query>", "Search text, such as a brand or keyword.")
|
|
7965
8381
|
.option("--json", "Print a JSON envelope.")
|
|
7966
8382
|
.action(async (query, options) => {
|
|
@@ -7970,7 +8386,7 @@ export function createProgram() {
|
|
|
7970
8386
|
});
|
|
7971
8387
|
}))
|
|
7972
8388
|
.addCommand(new Command("check")
|
|
7973
|
-
.description("
|
|
8389
|
+
.description("BYOK CLOUDFLARE: check availability and pricing for up to 20 exact domains. Free — nothing is purchased.")
|
|
7974
8390
|
.argument("<domains...>", "Domain names to check (max 20).")
|
|
7975
8391
|
.option("--json", "Print a JSON envelope.")
|
|
7976
8392
|
.action(async (domains, options) => {
|
|
@@ -7985,7 +8401,7 @@ export function createProgram() {
|
|
|
7985
8401
|
});
|
|
7986
8402
|
}))
|
|
7987
8403
|
.addCommand(new Command("buy")
|
|
7988
|
-
.description("
|
|
8404
|
+
.description("BYOK CLOUDFLARE ONLY. REAL MONEY, NON-REFUNDABLE: approved purchases bill your connected Cloudflare account and use 0 Oxygen credits. Without --approved, returns a priced preview plus quote_id. For a managed domain + inbox bundle use `managed-inboxes subscribe` (InboxKit).")
|
|
7989
8405
|
.argument("<domains...>", "Domain names to buy.")
|
|
7990
8406
|
.option("--approved", "Execute the purchase. Without this flag, returns a preview only.")
|
|
7991
8407
|
.option("--quote <id>", "Quote id from the preview (required with --approved).")
|
|
@@ -7998,7 +8414,7 @@ export function createProgram() {
|
|
|
7998
8414
|
await handleAsyncAction("domains buy", options, () => runDomainsBuy(domains, options));
|
|
7999
8415
|
}))
|
|
8000
8416
|
.addCommand(new Command("adopt")
|
|
8001
|
-
.description("STAFF:
|
|
8417
|
+
.description("STAFF LEGACY RECOVERY: attach domains that already exist in the shared Oxygen-managed Cloudflare account to their workspace. FREE; no purchase. New managed domain + inbox bundles use InboxKit. Without --yes, previews only.")
|
|
8002
8418
|
.argument("[domains...]", "Specific domains to adopt. Omit and pass --all to adopt every zone in the managed account.")
|
|
8003
8419
|
.option("--all", "Adopt every zone discovered in the managed Cloudflare account.")
|
|
8004
8420
|
.option("--organization-id <id>", "STAFF override: adopt into this workspace instead of the calling org.")
|
|
@@ -8362,14 +8778,18 @@ export function createProgram() {
|
|
|
8362
8778
|
}));
|
|
8363
8779
|
}))
|
|
8364
8780
|
.addCommand(new Command("enable")
|
|
8365
|
-
.description("Enable a workflow automation and its current trigger.")
|
|
8781
|
+
.description("Enable a workflow automation and its current trigger. Prints the schedule's projected automation-action burn; refuses a cron cadence the plan's monthly allowance cannot sustain.")
|
|
8366
8782
|
.argument("<workflow>", "Workflow id, slug, or name.")
|
|
8367
8783
|
.option("--json", "Print a JSON envelope.")
|
|
8368
8784
|
.action(async (workflow, options) => {
|
|
8369
|
-
await handleAsyncAction("workflows enable", options, () =>
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8785
|
+
await handleAsyncAction("workflows enable", options, async () => {
|
|
8786
|
+
const data = await requestOxygen("/api/cli/workflows/enable", {
|
|
8787
|
+
method: "POST",
|
|
8788
|
+
body: { workflow },
|
|
8789
|
+
});
|
|
8790
|
+
writeAutomationProjection(data);
|
|
8791
|
+
return data;
|
|
8792
|
+
});
|
|
8373
8793
|
}))
|
|
8374
8794
|
.addCommand(new Command("disable")
|
|
8375
8795
|
.description("Disable a workflow automation and its current trigger.")
|
|
@@ -8380,7 +8800,56 @@ export function createProgram() {
|
|
|
8380
8800
|
method: "POST",
|
|
8381
8801
|
body: { workflow },
|
|
8382
8802
|
}));
|
|
8383
|
-
}))
|
|
8803
|
+
}))
|
|
8804
|
+
.addCommand(new Command("mcp")
|
|
8805
|
+
.description("Publish workflows as dynamic MCP tools (oxygen_workflow_<slug>) — the 'Clay Functions' pattern.")
|
|
8806
|
+
.addCommand(new Command("enable")
|
|
8807
|
+
.description("Publish an active workflow as a callable MCP tool so any MCP client can run it.")
|
|
8808
|
+
.argument("<workflow>", "Workflow id, slug, or name.")
|
|
8809
|
+
.option("--display-name <name>", "Display name for the published MCP tool.")
|
|
8810
|
+
.option("--description <text>", "Description for the published MCP tool.")
|
|
8811
|
+
.option("--json", "Print a JSON envelope.")
|
|
8812
|
+
.action(async (workflow, options) => {
|
|
8813
|
+
await handleAsyncAction("workflows mcp enable", options, async () => {
|
|
8814
|
+
const data = await requestOxygen("/api/cli/workflows/mcp", {
|
|
8815
|
+
method: "POST",
|
|
8816
|
+
body: {
|
|
8817
|
+
workflow,
|
|
8818
|
+
enabled: true,
|
|
8819
|
+
...(readOption(options.displayName) ? { display_name: readOption(options.displayName) } : {}),
|
|
8820
|
+
...(readOption(options.description) ? { description: readOption(options.description) } : {}),
|
|
8821
|
+
},
|
|
8822
|
+
});
|
|
8823
|
+
// MCP tool names are capped at 64 chars. Warn (non-fatal) when the
|
|
8824
|
+
// resolved slug pushes oxygen_workflow_<slug> over the limit: the
|
|
8825
|
+
// flag is set but the tool is skipped in tools/list until the slug
|
|
8826
|
+
// is shortened. Uses the server-resolved slug, not the raw ref.
|
|
8827
|
+
const slug = data?.workflow?.slug;
|
|
8828
|
+
if (typeof slug === "string") {
|
|
8829
|
+
const toolName = `oxygen_workflow_${slug}`;
|
|
8830
|
+
if (toolName.length > 64) {
|
|
8831
|
+
process.stderr.write(`warning: "${toolName}" is ${toolName.length} chars (limit 64) — the workflow is enabled for MCP but will not appear in tools/list until its slug is shortened.\n`);
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
return data;
|
|
8835
|
+
});
|
|
8836
|
+
}))
|
|
8837
|
+
.addCommand(new Command("disable")
|
|
8838
|
+
.description("Unpublish a workflow's MCP tool. The workflow stays active and callable via `oxygen workflows call`.")
|
|
8839
|
+
.argument("<workflow>", "Workflow id, slug, or name.")
|
|
8840
|
+
.option("--json", "Print a JSON envelope.")
|
|
8841
|
+
.action(async (workflow, options) => {
|
|
8842
|
+
await handleAsyncAction("workflows mcp disable", options, () => requestOxygen("/api/cli/workflows/mcp", {
|
|
8843
|
+
method: "POST",
|
|
8844
|
+
body: { workflow, enabled: false },
|
|
8845
|
+
}));
|
|
8846
|
+
}))
|
|
8847
|
+
.addCommand(new Command("list")
|
|
8848
|
+
.description("List workflows currently published as MCP tools.")
|
|
8849
|
+
.option("--json", "Print a JSON envelope.")
|
|
8850
|
+
.action(async (options) => {
|
|
8851
|
+
await handleAsyncAction("workflows mcp list", options, () => requestOxygen("/api/cli/workflows/mcp-callable"));
|
|
8852
|
+
})));
|
|
8384
8853
|
program
|
|
8385
8854
|
.command("skills")
|
|
8386
8855
|
.description("Agent skill discovery and installation commands.")
|
|
@@ -9555,7 +10024,7 @@ function readCompaniesEnrichBody(table, options) {
|
|
|
9555
10024
|
return body;
|
|
9556
10025
|
}
|
|
9557
10026
|
function readCompaniesEnrichSelection(options) {
|
|
9558
|
-
const explicitSelection =
|
|
10027
|
+
const explicitSelection = readJsonObjectOption(options.selectionJson);
|
|
9559
10028
|
const hasAll = Boolean(options.all);
|
|
9560
10029
|
const limit = readPositiveInt(options.limit);
|
|
9561
10030
|
const rowIds = readCsvOption(options.rowIds);
|
|
@@ -9586,12 +10055,6 @@ function readFilterSelectionOption(value) {
|
|
|
9586
10055
|
const filters = readFilterJsonOption(value);
|
|
9587
10056
|
return filters ? { mode: "filter", filters } : undefined;
|
|
9588
10057
|
}
|
|
9589
|
-
function readSelectionJsonOption(value) {
|
|
9590
|
-
const raw = readOption(value);
|
|
9591
|
-
if (!raw)
|
|
9592
|
-
return undefined;
|
|
9593
|
-
return parseJsonObject(raw);
|
|
9594
|
-
}
|
|
9595
10058
|
function readFilterJsonOption(value) {
|
|
9596
10059
|
const raw = readOption(value);
|
|
9597
10060
|
if (!raw)
|
|
@@ -9615,12 +10078,6 @@ function readFilterJsonOption(value) {
|
|
|
9615
10078
|
}
|
|
9616
10079
|
return filters;
|
|
9617
10080
|
}
|
|
9618
|
-
function readFilterTreeJsonOption(value) {
|
|
9619
|
-
const raw = readOption(value);
|
|
9620
|
-
if (!raw)
|
|
9621
|
-
return undefined;
|
|
9622
|
-
return parseJsonObject(raw);
|
|
9623
|
-
}
|
|
9624
10081
|
function readSortJsonOption(value) {
|
|
9625
10082
|
const raw = readOption(value);
|
|
9626
10083
|
if (!raw)
|
|
@@ -10917,12 +11374,6 @@ function chunk(values, size) {
|
|
|
10917
11374
|
function readCount(value) {
|
|
10918
11375
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
10919
11376
|
}
|
|
10920
|
-
function readRecordString(value, key) {
|
|
10921
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10922
|
-
return null;
|
|
10923
|
-
const entry = value[key];
|
|
10924
|
-
return typeof entry === "string" ? entry : null;
|
|
10925
|
-
}
|
|
10926
11377
|
function readRecord(value, key) {
|
|
10927
11378
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10928
11379
|
return null;
|
|
@@ -10957,9 +11408,6 @@ function isTerminalTableActionRunStatus(status) {
|
|
|
10957
11408
|
|| status === "failed"
|
|
10958
11409
|
|| status === "canceled";
|
|
10959
11410
|
}
|
|
10960
|
-
function sleep(ms) {
|
|
10961
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10962
|
-
}
|
|
10963
11411
|
async function resolveActiveProfileWithSource() {
|
|
10964
11412
|
const resolution = await resolveActiveProfile();
|
|
10965
11413
|
let source;
|
|
@@ -11031,9 +11479,7 @@ async function handleWhoamiAction(options) {
|
|
|
11031
11479
|
process.stdout.write(formatWhoami(identity, context));
|
|
11032
11480
|
}
|
|
11033
11481
|
catch (error) {
|
|
11034
|
-
|
|
11035
|
-
writeJson(failure);
|
|
11036
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11482
|
+
emitCliFailure("whoami", error);
|
|
11037
11483
|
}
|
|
11038
11484
|
}
|
|
11039
11485
|
async function handleOnboardingStartAction(options) {
|
|
@@ -11046,9 +11492,7 @@ async function handleOnboardingStartAction(options) {
|
|
|
11046
11492
|
process.stdout.write(formatOnboardingStart(data));
|
|
11047
11493
|
}
|
|
11048
11494
|
catch (error) {
|
|
11049
|
-
|
|
11050
|
-
writeJson(failure);
|
|
11051
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11495
|
+
emitCliFailure("onboarding start", error);
|
|
11052
11496
|
}
|
|
11053
11497
|
}
|
|
11054
11498
|
async function handleOnboardingResetAction(options) {
|
|
@@ -11070,9 +11514,7 @@ async function handleOnboardingResetAction(options) {
|
|
|
11070
11514
|
process.stdout.write(formatOnboardingReset(data));
|
|
11071
11515
|
}
|
|
11072
11516
|
catch (error) {
|
|
11073
|
-
|
|
11074
|
-
writeJson(failure);
|
|
11075
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11517
|
+
emitCliFailure("onboarding reset", error);
|
|
11076
11518
|
}
|
|
11077
11519
|
}
|
|
11078
11520
|
function formatOnboardingReset(data) {
|
|
@@ -11128,9 +11570,7 @@ async function handleLoginAction(options) {
|
|
|
11128
11570
|
}
|
|
11129
11571
|
}
|
|
11130
11572
|
catch (error) {
|
|
11131
|
-
|
|
11132
|
-
writeJson(failure);
|
|
11133
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11573
|
+
emitCliFailure("login", error);
|
|
11134
11574
|
}
|
|
11135
11575
|
}
|
|
11136
11576
|
async function handleAuthUseTokenAction(options) {
|
|
@@ -11173,9 +11613,7 @@ async function handleAuthDoctorAction(options) {
|
|
|
11173
11613
|
process.stdout.write(formatAuthDoctor(data));
|
|
11174
11614
|
}
|
|
11175
11615
|
catch (error) {
|
|
11176
|
-
|
|
11177
|
-
writeJson(failure);
|
|
11178
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11616
|
+
emitCliFailure("auth doctor", error);
|
|
11179
11617
|
}
|
|
11180
11618
|
}
|
|
11181
11619
|
async function runAuthDoctor() {
|
|
@@ -11264,9 +11702,7 @@ async function handleOrgUseAction(organization, options, command) {
|
|
|
11264
11702
|
writeJson(result);
|
|
11265
11703
|
}
|
|
11266
11704
|
catch (error) {
|
|
11267
|
-
|
|
11268
|
-
writeJson(failure);
|
|
11269
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11705
|
+
emitCliFailure(command, error);
|
|
11270
11706
|
}
|
|
11271
11707
|
}
|
|
11272
11708
|
async function handleProfilesListAction(options) {
|
|
@@ -11283,9 +11719,7 @@ async function handleProfilesListAction(options) {
|
|
|
11283
11719
|
process.stdout.write(formatProfilesList(data));
|
|
11284
11720
|
}
|
|
11285
11721
|
catch (error) {
|
|
11286
|
-
|
|
11287
|
-
writeJson(failure);
|
|
11288
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11722
|
+
emitCliFailure("profiles list", error);
|
|
11289
11723
|
}
|
|
11290
11724
|
}
|
|
11291
11725
|
async function handleProfilesUseAction(profile, options) {
|
|
@@ -11304,9 +11738,7 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
11304
11738
|
process.stdout.write(formatProfileUseSuccess(data.profile, { totalProfiles }));
|
|
11305
11739
|
}
|
|
11306
11740
|
catch (error) {
|
|
11307
|
-
|
|
11308
|
-
writeJson(failure);
|
|
11309
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11741
|
+
emitCliFailure("profiles use", error);
|
|
11310
11742
|
}
|
|
11311
11743
|
}
|
|
11312
11744
|
async function handleProfilesEnvAction(profile, options) {
|
|
@@ -11343,9 +11775,7 @@ async function handleProfilesEnvAction(profile, options) {
|
|
|
11343
11775
|
process.stdout.write(`export OXYGEN_API_URL=${shellQuote(match.apiUrl)}\n`);
|
|
11344
11776
|
}
|
|
11345
11777
|
catch (error) {
|
|
11346
|
-
|
|
11347
|
-
writeJson(failure);
|
|
11348
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11778
|
+
emitCliFailure("profiles env", error);
|
|
11349
11779
|
}
|
|
11350
11780
|
}
|
|
11351
11781
|
async function handleProfilesCurrentAction(options) {
|
|
@@ -11367,9 +11797,7 @@ async function handleProfilesCurrentAction(options) {
|
|
|
11367
11797
|
process.stdout.write(formatProfilesCurrent(context));
|
|
11368
11798
|
}
|
|
11369
11799
|
catch (error) {
|
|
11370
|
-
|
|
11371
|
-
writeJson(failure);
|
|
11372
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11800
|
+
emitCliFailure("profiles current", error);
|
|
11373
11801
|
}
|
|
11374
11802
|
}
|
|
11375
11803
|
function shellQuote(value) {
|
|
@@ -11396,9 +11824,7 @@ async function handleLogoutAction(options) {
|
|
|
11396
11824
|
process.stdout.write(formatLogoutSuccess(result));
|
|
11397
11825
|
}
|
|
11398
11826
|
catch (error) {
|
|
11399
|
-
|
|
11400
|
-
writeJson(failure);
|
|
11401
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11827
|
+
emitCliFailure("logout", error);
|
|
11402
11828
|
}
|
|
11403
11829
|
}
|
|
11404
11830
|
async function handleUpdateAction(options) {
|
|
@@ -11411,9 +11837,7 @@ async function handleUpdateAction(options) {
|
|
|
11411
11837
|
process.stdout.write(formatUpdateSuccess(result));
|
|
11412
11838
|
}
|
|
11413
11839
|
catch (error) {
|
|
11414
|
-
|
|
11415
|
-
writeJson(failure);
|
|
11416
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11840
|
+
emitCliFailure("update", error);
|
|
11417
11841
|
}
|
|
11418
11842
|
}
|
|
11419
11843
|
function buildApiKeyCreateBody(options) {
|
|
@@ -11477,8 +11901,16 @@ async function login(options) {
|
|
|
11477
11901
|
chosenProfile = picked.name;
|
|
11478
11902
|
renamed = picked.renamed;
|
|
11479
11903
|
}
|
|
11480
|
-
const profile = await saveCredentials(credentials, process.env, {
|
|
11904
|
+
const profile = await saveCredentials(credentials, process.env, {
|
|
11905
|
+
profile: chosenProfile,
|
|
11906
|
+
allowHostChange: options.force === true,
|
|
11907
|
+
});
|
|
11481
11908
|
const skillsInstall = await runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl, credentials });
|
|
11909
|
+
// Opt-in eager mirror of every reachable workspace, off by default so login
|
|
11910
|
+
// stays fast and non-surprising. Best-effort: a sync failure never fails login.
|
|
11911
|
+
if (process.env.OXYGEN_KNOWLEDGE_AUTOSYNC === "1") {
|
|
11912
|
+
await runKnowledgeMirrorSyncAll({ ifStale: true }).catch(() => undefined);
|
|
11913
|
+
}
|
|
11482
11914
|
if (!options.json) {
|
|
11483
11915
|
process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
|
|
11484
11916
|
const hint = await buildPostLoginHint(profile);
|
|
@@ -11521,7 +11953,10 @@ async function applyAuthToken(options) {
|
|
|
11521
11953
|
chosenProfile = picked.name;
|
|
11522
11954
|
renamed = picked.renamed;
|
|
11523
11955
|
}
|
|
11524
|
-
const profile = await saveCredentials(loginIdentity.credentials, process.env, {
|
|
11956
|
+
const profile = await saveCredentials(loginIdentity.credentials, process.env, {
|
|
11957
|
+
profile: chosenProfile,
|
|
11958
|
+
allowHostChange: options.force === true,
|
|
11959
|
+
});
|
|
11525
11960
|
return {
|
|
11526
11961
|
identity: loginIdentity.whoami,
|
|
11527
11962
|
loginIdentity,
|
|
@@ -11576,18 +12011,19 @@ async function resolveLoginIdentity(token, apiUrl) {
|
|
|
11576
12011
|
};
|
|
11577
12012
|
}
|
|
11578
12013
|
function pickProfileForLogin(loginIdentity) {
|
|
12014
|
+
const apiUrl = loginIdentity.credentials.apiUrl;
|
|
11579
12015
|
if (loginIdentity.credentials.authKind === "user_session") {
|
|
11580
12016
|
const seed = loginIdentity.activeOrganization?.slug
|
|
11581
12017
|
?? loginIdentity.user.email
|
|
11582
12018
|
?? loginIdentity.user.id;
|
|
11583
|
-
return pickProfileNameForUserSession(loginIdentity.user.id, seed);
|
|
12019
|
+
return pickProfileNameForUserSession(loginIdentity.user.id, seed, apiUrl);
|
|
11584
12020
|
}
|
|
11585
12021
|
const organization = loginIdentity.activeOrganization;
|
|
11586
12022
|
if (!organization) {
|
|
11587
|
-
return pickProfileNameForUserSession(loginIdentity.user.id, loginIdentity.user.email ?? loginIdentity.user.id);
|
|
12023
|
+
return pickProfileNameForUserSession(loginIdentity.user.id, loginIdentity.user.email ?? loginIdentity.user.id, apiUrl);
|
|
11588
12024
|
}
|
|
11589
12025
|
const seed = organization.slug?.trim() || organization.id;
|
|
11590
|
-
return pickProfileNameForIdentity(organization.id, seed);
|
|
12026
|
+
return pickProfileNameForIdentity(organization.id, seed, apiUrl);
|
|
11591
12027
|
}
|
|
11592
12028
|
function readEnvProfileName() {
|
|
11593
12029
|
const value = process.env.OXYGEN_PROFILE?.trim();
|
|
@@ -11624,17 +12060,20 @@ function isUserSessionToken(token) {
|
|
|
11624
12060
|
return token.startsWith("oxy_user_");
|
|
11625
12061
|
}
|
|
11626
12062
|
async function buildPostLoginHint(activeProfile) {
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
return `\n Pin this terminal: eval "$(oxygen profiles env ${activeProfile})"\n\n`;
|
|
11632
|
-
}
|
|
12063
|
+
const plain = output.isTTY !== true || Boolean(process.env.NO_COLOR);
|
|
12064
|
+
const styles = ansi(!plain);
|
|
12065
|
+
const label = (text) => (plain ? text : styles.dim(text));
|
|
12066
|
+
const command = (text) => (plain ? text : styles.bold(text));
|
|
11633
12067
|
const state = await listCredentialProfiles().catch(() => null);
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
12068
|
+
const lines = [];
|
|
12069
|
+
// The pin hint only makes sense with more than one stored profile.
|
|
12070
|
+
if (state && state.profiles.length >= 2) {
|
|
12071
|
+
lines.push(` ${label("Pin this terminal:")} ${command(`eval "$(oxygen profiles env ${activeProfile})"`)}`);
|
|
12072
|
+
}
|
|
12073
|
+
// Mirror every reachable workspace into local markdown so agents can read/grep
|
|
12074
|
+
// the wiki offline; --if-stale keeps a large multi-workspace loop cheap.
|
|
12075
|
+
lines.push(` ${label("Mirror your wikis:")} ${command("oxygen knowledge sync --all --if-stale")}`);
|
|
12076
|
+
return `\n${lines.join("\n")}\n\n`;
|
|
11638
12077
|
}
|
|
11639
12078
|
async function promptForToken(options) {
|
|
11640
12079
|
const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
|
|
@@ -12018,10 +12457,7 @@ async function handleSequenceSignalAction(sequence, options) {
|
|
|
12018
12457
|
process.stdout.write(`Recorded ${recordedSignal} on ${updated} enrollment(s).${link ? ` ${link}` : ""}\n`);
|
|
12019
12458
|
}
|
|
12020
12459
|
catch (error) {
|
|
12021
|
-
|
|
12022
|
-
writeJson(failure);
|
|
12023
|
-
writeMaxCreditsHint(error);
|
|
12024
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12460
|
+
emitCliFailure("sequences signal", error);
|
|
12025
12461
|
}
|
|
12026
12462
|
}
|
|
12027
12463
|
async function handleSequenceVariantsAction(sequence, options) {
|
|
@@ -12055,10 +12491,7 @@ async function handleSequenceVariantsAction(sequence, options) {
|
|
|
12055
12491
|
process.stdout.write(formatSequenceVariants(data));
|
|
12056
12492
|
}
|
|
12057
12493
|
catch (error) {
|
|
12058
|
-
|
|
12059
|
-
writeJson(failure);
|
|
12060
|
-
writeMaxCreditsHint(error);
|
|
12061
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12494
|
+
emitCliFailure("sequences variants", error);
|
|
12062
12495
|
}
|
|
12063
12496
|
}
|
|
12064
12497
|
function formatVariantCell(value) {
|
|
@@ -12186,10 +12619,7 @@ async function handleTablesTidySuggestAction(table, options) {
|
|
|
12186
12619
|
process.stdout.write(formatTidySuggestions(data));
|
|
12187
12620
|
}
|
|
12188
12621
|
catch (error) {
|
|
12189
|
-
|
|
12190
|
-
writeJson(failure);
|
|
12191
|
-
writeMaxCreditsHint(error);
|
|
12192
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12622
|
+
emitCliFailure("tables tidy-suggest", error);
|
|
12193
12623
|
}
|
|
12194
12624
|
}
|
|
12195
12625
|
function formatTidyCount(value) {
|
|
@@ -12567,9 +12997,7 @@ async function handleSupportAdminWorkflowAction(ticketId, options) {
|
|
|
12567
12997
|
process.stdout.write(formatSupportAdminWorkflowTicket(data));
|
|
12568
12998
|
}
|
|
12569
12999
|
catch (error) {
|
|
12570
|
-
|
|
12571
|
-
writeJson(failure);
|
|
12572
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
13000
|
+
emitCliFailure("support admin workflow", error);
|
|
12573
13001
|
}
|
|
12574
13002
|
}
|
|
12575
13003
|
async function handleSupportAdminUpdateAction(ticketId, options) {
|
|
@@ -12583,9 +13011,7 @@ async function handleSupportAdminUpdateAction(ticketId, options) {
|
|
|
12583
13011
|
process.stdout.write(formatSupportAdminUpdateTicket(data));
|
|
12584
13012
|
}
|
|
12585
13013
|
catch (error) {
|
|
12586
|
-
|
|
12587
|
-
writeJson(failure);
|
|
12588
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
13014
|
+
emitCliFailure("support admin update", error);
|
|
12589
13015
|
}
|
|
12590
13016
|
}
|
|
12591
13017
|
function formatSupportAdminWorkflowStep(styles, label, status, artifactLabel, artifact) {
|
|
@@ -12883,8 +13309,12 @@ function buildKnowledgePageListBody(options) {
|
|
|
12883
13309
|
}
|
|
12884
13310
|
function buildKnowledgeGraphBody(options) {
|
|
12885
13311
|
const maxNodes = readPositiveInt(options.maxNodes);
|
|
13312
|
+
const center = readOption(options.center);
|
|
13313
|
+
const depth = readPositiveInt(options.depth);
|
|
12886
13314
|
return {
|
|
12887
13315
|
...(maxNodes !== undefined ? { max_nodes: maxNodes } : {}),
|
|
13316
|
+
...(center ? { center } : {}),
|
|
13317
|
+
...(depth !== undefined ? { depth } : {}),
|
|
12888
13318
|
};
|
|
12889
13319
|
}
|
|
12890
13320
|
function buildKnowledgeLogListBody(options) {
|
|
@@ -12976,10 +13406,27 @@ async function resolveKnowledgeMirrorTarget() {
|
|
|
12976
13406
|
async function markKnowledgeMirrorStaleAfterWrite() {
|
|
12977
13407
|
try {
|
|
12978
13408
|
const credentials = await loadCredentials().catch(() => null);
|
|
12979
|
-
|
|
12980
|
-
if (!target || !mirrorExists(target.dir))
|
|
13409
|
+
if (!credentials)
|
|
12981
13410
|
return;
|
|
12982
|
-
|
|
13411
|
+
const cached = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
|
|
13412
|
+
const ref = process.env.OXYGEN_ORG?.trim() || cached?.id || cached?.slug;
|
|
13413
|
+
if (!ref)
|
|
13414
|
+
return;
|
|
13415
|
+
// Resolve the ref to an org id offline — the hook stays network-free so a page
|
|
13416
|
+
// write is never slowed by a whoami round-trip. A non-cached slug override
|
|
13417
|
+
// (`--org <slug>` for an org we haven't cached) can't be mapped without a
|
|
13418
|
+
// network call, so that mirror isn't stale-marked here; the 15-min --if-stale
|
|
13419
|
+
// TTL and `knowledge sync --all` are the safety net.
|
|
13420
|
+
let orgId = null;
|
|
13421
|
+
if (cached && (ref === cached.id || ref === cached.slug))
|
|
13422
|
+
orgId = cached.id;
|
|
13423
|
+
else if (isUuid(ref))
|
|
13424
|
+
orgId = ref;
|
|
13425
|
+
if (!orgId)
|
|
13426
|
+
return;
|
|
13427
|
+
const target = knowledgeMirrorTargetFor(credentials.apiUrl, orgId);
|
|
13428
|
+
if (mirrorExists(target.dir))
|
|
13429
|
+
markMirrorStale(target.dir);
|
|
12983
13430
|
}
|
|
12984
13431
|
catch {
|
|
12985
13432
|
// A broken local mirror must never fail the page write itself.
|
|
@@ -13035,8 +13482,24 @@ function applyKnowledgeSyncPage(dir, state, page, counters) {
|
|
|
13035
13482
|
};
|
|
13036
13483
|
counters.synced += 1;
|
|
13037
13484
|
}
|
|
13038
|
-
|
|
13039
|
-
|
|
13485
|
+
// `orgOverride` targets a specific org (used by `sync --all`): it names the mirror
|
|
13486
|
+
// directory explicitly and pins X-Oxygen-Organization on every request in the
|
|
13487
|
+
// loop, so each workspace clones into its own mirror regardless of the cached
|
|
13488
|
+
// active org. Absent (the default single-org path), behavior is unchanged.
|
|
13489
|
+
async function runKnowledgeMirrorSync(// skipcq: JS-R1005 -- one linear sync pipeline: staleness gate, lock, delta loop, generated files.
|
|
13490
|
+
options, orgOverride) {
|
|
13491
|
+
const requestOrg = orgOverride ? { selectedOrganization: orgOverride.selector } : {};
|
|
13492
|
+
let target;
|
|
13493
|
+
if (orgOverride) {
|
|
13494
|
+
const credentials = await loadCredentials();
|
|
13495
|
+
if (!credentials) {
|
|
13496
|
+
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", { exitCode: 1 });
|
|
13497
|
+
}
|
|
13498
|
+
target = knowledgeMirrorTargetFor(credentials.apiUrl, orgOverride.orgId);
|
|
13499
|
+
}
|
|
13500
|
+
else {
|
|
13501
|
+
target = await resolveKnowledgeMirrorTarget();
|
|
13502
|
+
}
|
|
13040
13503
|
const ttlMinutes = readPositiveInt(options.ttl) ?? KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES;
|
|
13041
13504
|
if (options.ifStale && !options.full) {
|
|
13042
13505
|
const state = readMirrorState(target.dir);
|
|
@@ -13073,6 +13536,7 @@ async function runKnowledgeMirrorSync(options) {
|
|
|
13073
13536
|
const delta = await requestOxygen("/api/cli/knowledge/sync/delta", {
|
|
13074
13537
|
method: "POST",
|
|
13075
13538
|
body: cursor ? { cursor } : {},
|
|
13539
|
+
...requestOrg,
|
|
13076
13540
|
});
|
|
13077
13541
|
if (delta.web_url)
|
|
13078
13542
|
webUrl = delta.web_url;
|
|
@@ -13109,10 +13573,11 @@ async function runKnowledgeMirrorSync(options) {
|
|
|
13109
13573
|
}
|
|
13110
13574
|
// Generated summaries come from the server's own projections, not a client-side
|
|
13111
13575
|
// recomputation. A real wiki page may own the `index`/`log` slug — page bytes win.
|
|
13112
|
-
const index = await requestOxygen("/api/cli/knowledge/index");
|
|
13576
|
+
const index = await requestOxygen("/api/cli/knowledge/index", requestOrg);
|
|
13113
13577
|
const logPage = await requestOxygen("/api/cli/knowledge/log", {
|
|
13114
13578
|
method: "POST",
|
|
13115
13579
|
body: {},
|
|
13580
|
+
...requestOrg,
|
|
13116
13581
|
});
|
|
13117
13582
|
if (!state.pages.index)
|
|
13118
13583
|
writeGeneratedIndexFile(target.dir, index.index);
|
|
@@ -13136,6 +13601,50 @@ async function runKnowledgeMirrorSync(options) {
|
|
|
13136
13601
|
releaseMirrorLock(target.dir);
|
|
13137
13602
|
}
|
|
13138
13603
|
}
|
|
13604
|
+
// `knowledge sync --all` — mirror every workspace the credential can reach, each
|
|
13605
|
+
// into its own org-id-keyed mirror directory. Loops the same `/api/cli/orgs` list
|
|
13606
|
+
// the `orgs` command reads and syncs each org via `runKnowledgeMirrorSync` with an
|
|
13607
|
+
// explicit org override. One org's failure (including a revoked membership, which
|
|
13608
|
+
// purges just that org's mirror) is recorded and never aborts the rest. Pair with
|
|
13609
|
+
// `--if-stale` so a large fleet loop stays cheap (each org honors its own TTL).
|
|
13610
|
+
async function runKnowledgeMirrorSyncAll(options) {
|
|
13611
|
+
const credentials = await loadCredentials();
|
|
13612
|
+
if (!credentials) {
|
|
13613
|
+
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", { exitCode: 1 });
|
|
13614
|
+
}
|
|
13615
|
+
const orgs = await requestOxygen("/api/cli/orgs", { credentials });
|
|
13616
|
+
const results = [];
|
|
13617
|
+
for (const org of orgs.organizations) {
|
|
13618
|
+
const selector = org.slug ?? org.id;
|
|
13619
|
+
try {
|
|
13620
|
+
const summary = await runKnowledgeMirrorSync({ ...options, all: false }, { orgId: org.id, selector });
|
|
13621
|
+
results.push({
|
|
13622
|
+
org_id: org.id,
|
|
13623
|
+
slug: org.slug ?? null,
|
|
13624
|
+
status: summary.skipped ? "skipped" : "synced",
|
|
13625
|
+
...summary,
|
|
13626
|
+
});
|
|
13627
|
+
}
|
|
13628
|
+
catch (error) {
|
|
13629
|
+
results.push({
|
|
13630
|
+
org_id: org.id,
|
|
13631
|
+
slug: org.slug ?? null,
|
|
13632
|
+
status: "failed",
|
|
13633
|
+
error: error instanceof OxygenError
|
|
13634
|
+
? { code: error.code, message: error.message }
|
|
13635
|
+
: { code: "unknown", message: error instanceof Error ? error.message : "Unknown error." },
|
|
13636
|
+
});
|
|
13637
|
+
}
|
|
13638
|
+
}
|
|
13639
|
+
return {
|
|
13640
|
+
orgs: results,
|
|
13641
|
+
count: results.length,
|
|
13642
|
+
synced: results.filter((result) => result.status === "synced").length,
|
|
13643
|
+
skipped: results.filter((result) => result.status === "skipped").length,
|
|
13644
|
+
failed: results.filter((result) => result.status === "failed").length,
|
|
13645
|
+
web_url: `${credentials.apiUrl.replace(/\/$/, "")}/knowledge`,
|
|
13646
|
+
};
|
|
13647
|
+
}
|
|
13139
13648
|
// One page's push: parse the local file, guard identity, upsert with the manifest
|
|
13140
13649
|
// revision as the optimistic-concurrency base, and translate the two expected
|
|
13141
13650
|
// gate outcomes (approval proposal, revision conflict) into statuses.
|
|
@@ -13299,6 +13808,45 @@ async function runKnowledgeMirrorStatus(options) {
|
|
|
13299
13808
|
web_url: target.webUrl,
|
|
13300
13809
|
};
|
|
13301
13810
|
}
|
|
13811
|
+
// `knowledge status --all` — freshness report across every locally mirrored
|
|
13812
|
+
// workspace for the active API host. Pure filesystem read (no network): enumerate
|
|
13813
|
+
// the org-id mirror dirs, read each `MirrorState`, and flag any whose last sync is
|
|
13814
|
+
// older than the default TTL. Slugs are cross-referenced from the cached active
|
|
13815
|
+
// org only (offline); other orgs report `org_id` alone.
|
|
13816
|
+
async function runKnowledgeMirrorStatusAll() {
|
|
13817
|
+
const credentials = await loadCredentials();
|
|
13818
|
+
if (!credentials) {
|
|
13819
|
+
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", { exitCode: 1 });
|
|
13820
|
+
}
|
|
13821
|
+
const apiHost = new URL(credentials.apiUrl).host;
|
|
13822
|
+
const orgIds = listLocalMirrors({ configDir: resolveDefaultConfigDir(), apiHost });
|
|
13823
|
+
const cached = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
|
|
13824
|
+
const ttlMs = KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES * 60_000;
|
|
13825
|
+
const mirrors = orgIds.map((orgId) => {
|
|
13826
|
+
const target = knowledgeMirrorTargetFor(credentials.apiUrl, orgId);
|
|
13827
|
+
const state = readMirrorState(target.dir);
|
|
13828
|
+
const conflicts = listConflictFiles(target.dir);
|
|
13829
|
+
const lastSyncMs = state?.last_sync_at ? Date.parse(state.last_sync_at) : Number.NaN;
|
|
13830
|
+
const stale = !Number.isFinite(lastSyncMs) || Date.now() - lastSyncMs >= ttlMs;
|
|
13831
|
+
return {
|
|
13832
|
+
org_id: orgId,
|
|
13833
|
+
...(cached && cached.id === orgId ? { slug: cached.slug ?? null } : {}),
|
|
13834
|
+
mirror_path: target.dir,
|
|
13835
|
+
exists: mirrorExists(target.dir),
|
|
13836
|
+
pages_total: state ? Object.keys(state.pages).length : 0,
|
|
13837
|
+
last_sync_at: state?.last_sync_at ?? null,
|
|
13838
|
+
stale,
|
|
13839
|
+
conflict_count: conflicts.length,
|
|
13840
|
+
web_url: target.webUrl,
|
|
13841
|
+
};
|
|
13842
|
+
});
|
|
13843
|
+
return {
|
|
13844
|
+
api_host: apiHost,
|
|
13845
|
+
count: mirrors.length,
|
|
13846
|
+
mirrors,
|
|
13847
|
+
web_url: `${credentials.apiUrl.replace(/\/$/, "")}/knowledge`,
|
|
13848
|
+
};
|
|
13849
|
+
}
|
|
13302
13850
|
async function runKnowledgeMirrorPurge() {
|
|
13303
13851
|
const target = await resolveKnowledgeMirrorTarget();
|
|
13304
13852
|
const existed = existsSync(target.dir);
|
|
@@ -13313,9 +13861,7 @@ async function handleKnowledgeMirrorPrintPathAction() {
|
|
|
13313
13861
|
process.stdout.write(`${target.dir}\n`);
|
|
13314
13862
|
}
|
|
13315
13863
|
catch (error) {
|
|
13316
|
-
|
|
13317
|
-
writeJson(failure);
|
|
13318
|
-
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
13864
|
+
emitCliFailure("knowledge status", error);
|
|
13319
13865
|
}
|
|
13320
13866
|
}
|
|
13321
13867
|
// Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
|
|
@@ -13330,7 +13876,7 @@ function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enri
|
|
|
13330
13876
|
table, options) {
|
|
13331
13877
|
const limit = readPositiveInt(options.limit);
|
|
13332
13878
|
const filterSelection = readFilterSelectionOption(options.filterJson);
|
|
13333
|
-
const explicitSelection =
|
|
13879
|
+
const explicitSelection = readJsonObjectOption(options.selectionJson);
|
|
13334
13880
|
const selectedModes = [
|
|
13335
13881
|
Boolean(options.all),
|
|
13336
13882
|
limit !== undefined,
|
|
@@ -13444,19 +13990,6 @@ options) {
|
|
|
13444
13990
|
...(hasRandomize ? { randomize_daily_caps: options.randomizeCaps } : {}),
|
|
13445
13991
|
};
|
|
13446
13992
|
}
|
|
13447
|
-
function readPositiveInt(value) {
|
|
13448
|
-
const trimmed = value?.trim();
|
|
13449
|
-
if (!trimmed)
|
|
13450
|
-
return undefined;
|
|
13451
|
-
const parsed = Number(trimmed);
|
|
13452
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
13453
|
-
throw new OxygenError("invalid_number", "Expected a positive integer.", {
|
|
13454
|
-
details: { value },
|
|
13455
|
-
exitCode: 1,
|
|
13456
|
-
});
|
|
13457
|
-
}
|
|
13458
|
-
return parsed;
|
|
13459
|
-
}
|
|
13460
13993
|
function readPositiveNumber(value) {
|
|
13461
13994
|
const trimmed = value?.trim();
|
|
13462
13995
|
if (!trimmed)
|
|
@@ -13527,6 +14060,19 @@ function readSequenceSettings(options) {
|
|
|
13527
14060
|
const espMatching = readOption(options.espMatching);
|
|
13528
14061
|
if (espMatching)
|
|
13529
14062
|
settings.esp_matching = espMatching;
|
|
14063
|
+
const senderFailover = readOption(options.senderFailover);
|
|
14064
|
+
if (senderFailover)
|
|
14065
|
+
settings.sender_failover = senderFailover;
|
|
14066
|
+
// email_min_gap_minutes accepts 0 ("no extra humanization gap"), which
|
|
14067
|
+
// readPositiveInt rejects (it requires >= 1); treat a literal 0 as valid and
|
|
14068
|
+
// defer any other value to readPositiveInt's positive-integer validation. The
|
|
14069
|
+
// server enforces the 0-720 range (validateSequenceSettings).
|
|
14070
|
+
const emailMinGapRaw = readOption(options.emailMinGapMinutes);
|
|
14071
|
+
if (emailMinGapRaw !== null) {
|
|
14072
|
+
const emailMinGap = Number(emailMinGapRaw) === 0 ? 0 : readPositiveInt(emailMinGapRaw);
|
|
14073
|
+
if (emailMinGap !== undefined)
|
|
14074
|
+
settings.email_min_gap_minutes = emailMinGap;
|
|
14075
|
+
}
|
|
13530
14076
|
return Object.keys(settings).length > 0 ? settings : undefined;
|
|
13531
14077
|
}
|
|
13532
14078
|
function muteTokenEcho() {
|