@oxygen-agent/cli 1.263.0 → 1.274.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1009 -50
- package/dist/knowledge-mirror.d.ts +106 -0
- package/dist/knowledge-mirror.js +375 -0
- package/dist/skills.d.ts +33 -4
- package/dist/skills.js +96 -19
- package/dist/update.d.ts +2 -2
- package/dist/update.js +4 -2
- package/node_modules/@oxygen/shared/dist/billing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/billing.js +5 -5
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +38 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +137 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.js +38 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.js +57 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +25 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +56 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
|
14
14
|
import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
|
|
15
15
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
16
16
|
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
17
|
+
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, markMirrorStale, mirrorExists, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
17
18
|
import { waitForCliRun } from "./run-wait.js";
|
|
18
19
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
19
20
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
@@ -636,6 +637,146 @@ function resolveNotetakerMode(options) {
|
|
|
636
637
|
}
|
|
637
638
|
return options.live === true ? "live" : "dry_run";
|
|
638
639
|
}
|
|
640
|
+
function buildPublishingPostsListPath(options) {
|
|
641
|
+
const query = new URLSearchParams();
|
|
642
|
+
const status = readOption(options.status);
|
|
643
|
+
const limit = readOption(options.limit);
|
|
644
|
+
if (status)
|
|
645
|
+
query.set("status", status);
|
|
646
|
+
if (limit)
|
|
647
|
+
query.set("limit", limit);
|
|
648
|
+
const suffix = query.toString();
|
|
649
|
+
return suffix ? `/api/cli/publishing/posts?${suffix}` : "/api/cli/publishing/posts";
|
|
650
|
+
}
|
|
651
|
+
function buildPublishingPostCreateBody(options) {
|
|
652
|
+
const status = resolvePublishingCreateStatus(options);
|
|
653
|
+
const body = {
|
|
654
|
+
content_text: readPublishingPostText(options, true),
|
|
655
|
+
publish_at: options.publishAt,
|
|
656
|
+
status,
|
|
657
|
+
approved: options.approved === true,
|
|
658
|
+
};
|
|
659
|
+
const sender = readOption(options.sender);
|
|
660
|
+
const provider = readOption(options.provider);
|
|
661
|
+
const channel = readOption(options.channel);
|
|
662
|
+
const providerConnection = readOption(options.providerConnection);
|
|
663
|
+
const title = readOption(options.title);
|
|
664
|
+
const timezone = readOption(options.timezone);
|
|
665
|
+
const content = buildPublishingContent(options);
|
|
666
|
+
if (provider)
|
|
667
|
+
body.provider = provider;
|
|
668
|
+
if (channel)
|
|
669
|
+
body.channel = channel;
|
|
670
|
+
if (sender)
|
|
671
|
+
body.sender_account_id = sender;
|
|
672
|
+
if (providerConnection)
|
|
673
|
+
body.provider_connection_id = providerConnection;
|
|
674
|
+
if (title)
|
|
675
|
+
body.title = title;
|
|
676
|
+
if (timezone)
|
|
677
|
+
body.timezone = timezone;
|
|
678
|
+
if (content)
|
|
679
|
+
body.content = content;
|
|
680
|
+
return body;
|
|
681
|
+
}
|
|
682
|
+
function buildPublishingPostUpdateBody(options) {
|
|
683
|
+
const body = {};
|
|
684
|
+
const sender = readOption(options.sender);
|
|
685
|
+
const provider = readOption(options.provider);
|
|
686
|
+
const channel = readOption(options.channel);
|
|
687
|
+
const providerConnection = readOption(options.providerConnection);
|
|
688
|
+
const title = readOption(options.title);
|
|
689
|
+
const text = readPublishingPostText(options, false);
|
|
690
|
+
const publishAt = readOption(options.publishAt);
|
|
691
|
+
const timezone = readOption(options.timezone);
|
|
692
|
+
const status = readOption(options.status);
|
|
693
|
+
const content = buildPublishingContent(options);
|
|
694
|
+
if (provider)
|
|
695
|
+
body.provider = provider;
|
|
696
|
+
if (channel)
|
|
697
|
+
body.channel = channel;
|
|
698
|
+
if (sender)
|
|
699
|
+
body.sender_account_id = sender;
|
|
700
|
+
if (providerConnection)
|
|
701
|
+
body.provider_connection_id = providerConnection;
|
|
702
|
+
if (title)
|
|
703
|
+
body.title = title;
|
|
704
|
+
if (text !== null)
|
|
705
|
+
body.content_text = text;
|
|
706
|
+
if (publishAt)
|
|
707
|
+
body.publish_at = publishAt;
|
|
708
|
+
if (timezone)
|
|
709
|
+
body.timezone = timezone;
|
|
710
|
+
if (status)
|
|
711
|
+
body.status = status;
|
|
712
|
+
if (content)
|
|
713
|
+
body.content = content;
|
|
714
|
+
return body;
|
|
715
|
+
}
|
|
716
|
+
function buildPublishingMediaUploadUrlBody(options) {
|
|
717
|
+
const fileName = readOption(options.fileName);
|
|
718
|
+
const contentType = readOption(options.contentType);
|
|
719
|
+
const byteLength = readPositiveInt(options.byteLength);
|
|
720
|
+
if (!fileName) {
|
|
721
|
+
throw new OxygenError("invalid_request", "--file-name is required.", { exitCode: 1 });
|
|
722
|
+
}
|
|
723
|
+
if (!contentType) {
|
|
724
|
+
throw new OxygenError("invalid_request", "--content-type is required.", { exitCode: 1 });
|
|
725
|
+
}
|
|
726
|
+
if (!byteLength) {
|
|
727
|
+
throw new OxygenError("invalid_request", "--byte-length must be a positive integer.", { exitCode: 1 });
|
|
728
|
+
}
|
|
729
|
+
const body = {
|
|
730
|
+
file_name: fileName,
|
|
731
|
+
content_type: contentType,
|
|
732
|
+
byte_length: byteLength,
|
|
733
|
+
};
|
|
734
|
+
const scheduledPost = readOption(options.scheduledPost);
|
|
735
|
+
const metadataJson = readOption(options.metadataJson);
|
|
736
|
+
if (scheduledPost)
|
|
737
|
+
body.scheduled_post_id = scheduledPost;
|
|
738
|
+
if (metadataJson)
|
|
739
|
+
body.metadata = parseJsonObject(metadataJson);
|
|
740
|
+
return body;
|
|
741
|
+
}
|
|
742
|
+
function buildPublishingMediaUploadedBody(options) {
|
|
743
|
+
const sha256 = readOption(options.sha256);
|
|
744
|
+
return sha256 ? { sha256 } : {};
|
|
745
|
+
}
|
|
746
|
+
function buildPublishingContent(options) {
|
|
747
|
+
const contentJson = readOption(options.contentJson);
|
|
748
|
+
const composioAction = readOption(options.composioAction);
|
|
749
|
+
if (!contentJson && !composioAction)
|
|
750
|
+
return null;
|
|
751
|
+
const content = contentJson ? parseJsonObject(contentJson) : {};
|
|
752
|
+
if (composioAction) {
|
|
753
|
+
const existing = content.composio && typeof content.composio === "object" && !Array.isArray(content.composio)
|
|
754
|
+
? content.composio
|
|
755
|
+
: {};
|
|
756
|
+
content.composio = { ...existing, action_slug: composioAction };
|
|
757
|
+
}
|
|
758
|
+
return content;
|
|
759
|
+
}
|
|
760
|
+
function resolvePublishingCreateStatus(options) {
|
|
761
|
+
const status = readOption(options.status);
|
|
762
|
+
if (options.draft === true && status && status !== "draft") {
|
|
763
|
+
throw new OxygenError("conflicting_flags", "Pass either --draft or --status, not both.", { exitCode: 1 });
|
|
764
|
+
}
|
|
765
|
+
return options.draft === true ? "draft" : status ?? "scheduled";
|
|
766
|
+
}
|
|
767
|
+
function readPublishingPostText(options, required) {
|
|
768
|
+
const inlineText = readOption(options.text);
|
|
769
|
+
const textFile = readOption(options.textFile);
|
|
770
|
+
if (inlineText && textFile) {
|
|
771
|
+
throw new OxygenError("conflicting_flags", "Pass either --text or --text-file, not both.", { exitCode: 1 });
|
|
772
|
+
}
|
|
773
|
+
const text = textFile ? readFileSync(resolve(textFile), "utf8").trimEnd() : inlineText;
|
|
774
|
+
if (text?.trim())
|
|
775
|
+
return text;
|
|
776
|
+
if (!required)
|
|
777
|
+
return null;
|
|
778
|
+
throw new OxygenError("invalid_request", "Pass --text or --text-file.", { exitCode: 1 });
|
|
779
|
+
}
|
|
639
780
|
function buildCrmRelationshipUpsertBody(object, rowId, options) {
|
|
640
781
|
return {
|
|
641
782
|
object,
|
|
@@ -1023,8 +1164,8 @@ export function createProgram() {
|
|
|
1023
1164
|
.option("--package <npm_spec>", "Override the npm package spec.")
|
|
1024
1165
|
.option("--dry-run", "Print the update command without running it.")
|
|
1025
1166
|
.option("--json", "Print a JSON envelope.")
|
|
1026
|
-
.action((options) => {
|
|
1027
|
-
handleUpdateAction(options);
|
|
1167
|
+
.action(async (options) => {
|
|
1168
|
+
await handleUpdateAction(options);
|
|
1028
1169
|
});
|
|
1029
1170
|
program
|
|
1030
1171
|
.command("api-keys")
|
|
@@ -1517,6 +1658,125 @@ export function createProgram() {
|
|
|
1517
1658
|
.action(async (sessionId, options) => {
|
|
1518
1659
|
await handleAsyncAction("notetaker status", options, () => requestOxygen(`/api/cli/notetaker/sessions/${encodeURIComponent(sessionId)}`));
|
|
1519
1660
|
}));
|
|
1661
|
+
program
|
|
1662
|
+
.command("publishing")
|
|
1663
|
+
.description("Deterministic social post scheduling commands.")
|
|
1664
|
+
.addCommand(new Command("posts")
|
|
1665
|
+
.description("Manage scheduled Publishing posts.")
|
|
1666
|
+
.addCommand(new Command("list")
|
|
1667
|
+
.description("List scheduled posts.")
|
|
1668
|
+
.option("--status <status>", "Filter by draft, scheduled, queued, publishing, published, failed, or canceled.")
|
|
1669
|
+
.option("--limit <n>", "Maximum posts to return.")
|
|
1670
|
+
.option("--json", "Print a JSON envelope.")
|
|
1671
|
+
.action(async (options) => {
|
|
1672
|
+
await handleAsyncAction("publishing posts list", options, () => requestOxygen(buildPublishingPostsListPath(options)));
|
|
1673
|
+
}))
|
|
1674
|
+
.addCommand(new Command("create")
|
|
1675
|
+
.description("Create a scheduled post. Publishing requires approval before the worker can send.")
|
|
1676
|
+
.requiredOption("--publish-at <iso>", "ISO date-time when the post should publish.")
|
|
1677
|
+
.option("--provider <provider>", "Publishing provider: linkedin, x, instagram, tiktok, facebook, or youtube. Defaults to linkedin.")
|
|
1678
|
+
.option("--channel <channel>", "Publishing channel override. Defaults to provider.")
|
|
1679
|
+
.option("--sender <sender_account_id>", "LinkedIn sender account id. Required for LinkedIn before the worker can publish.")
|
|
1680
|
+
.option("--provider-connection <connection_id>", "Oxygen integration connection id for Composio-backed providers.")
|
|
1681
|
+
.option("--title <title>", "Internal title for the queue.")
|
|
1682
|
+
.option("--text <text>", "Post text.")
|
|
1683
|
+
.option("--text-file <path>", "Read post text from a local file.")
|
|
1684
|
+
.option("--content-json <json>", "Optional structured provider content. For Composio providers, pass provider_arguments or composio.arguments.")
|
|
1685
|
+
.option("--composio-action <slug>", "Override the Composio action slug for this scheduled post.")
|
|
1686
|
+
.option("--timezone <tz>", "Display timezone for the scheduled date. Defaults to UTC.")
|
|
1687
|
+
.option("--status <status>", "draft or scheduled. Defaults to scheduled.")
|
|
1688
|
+
.option("--draft", "Create as a draft instead of scheduled.")
|
|
1689
|
+
.option("--approved", "Mark the post approved for the scheduler.")
|
|
1690
|
+
.option("--json", "Print a JSON envelope.")
|
|
1691
|
+
.action(async (options) => {
|
|
1692
|
+
await handleAsyncAction("publishing posts create", options, () => requestOxygen("/api/cli/publishing/posts", {
|
|
1693
|
+
method: "POST",
|
|
1694
|
+
body: buildPublishingPostCreateBody(options),
|
|
1695
|
+
}));
|
|
1696
|
+
}))
|
|
1697
|
+
.addCommand(new Command("get")
|
|
1698
|
+
.description("Get one scheduled post with attempt history.")
|
|
1699
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1700
|
+
.option("--json", "Print a JSON envelope.")
|
|
1701
|
+
.action(async (postId, options) => {
|
|
1702
|
+
await handleAsyncAction("publishing posts get", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}`));
|
|
1703
|
+
}))
|
|
1704
|
+
.addCommand(new Command("update")
|
|
1705
|
+
.description("Update an editable draft, scheduled, or failed post.")
|
|
1706
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1707
|
+
.option("--provider <provider>", "Publishing provider: linkedin, x, instagram, tiktok, facebook, or youtube.")
|
|
1708
|
+
.option("--channel <channel>", "Publishing channel override.")
|
|
1709
|
+
.option("--sender <sender_account_id>", "LinkedIn sender account id.")
|
|
1710
|
+
.option("--provider-connection <connection_id>", "Oxygen integration connection id for Composio-backed providers.")
|
|
1711
|
+
.option("--title <title>", "Internal title for the queue.")
|
|
1712
|
+
.option("--text <text>", "Post text.")
|
|
1713
|
+
.option("--text-file <path>", "Read post text from a local file.")
|
|
1714
|
+
.option("--content-json <json>", "Optional structured provider content. For Composio providers, pass provider_arguments or composio.arguments.")
|
|
1715
|
+
.option("--composio-action <slug>", "Override the Composio action slug for this scheduled post.")
|
|
1716
|
+
.option("--publish-at <iso>", "ISO date-time when the post should publish.")
|
|
1717
|
+
.option("--timezone <tz>", "Display timezone for the scheduled date.")
|
|
1718
|
+
.option("--status <status>", "draft or scheduled.")
|
|
1719
|
+
.option("--json", "Print a JSON envelope.")
|
|
1720
|
+
.action(async (postId, options) => {
|
|
1721
|
+
await handleAsyncAction("publishing posts update", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}`, {
|
|
1722
|
+
method: "PATCH",
|
|
1723
|
+
body: buildPublishingPostUpdateBody(options),
|
|
1724
|
+
}));
|
|
1725
|
+
}))
|
|
1726
|
+
.addCommand(new Command("approve")
|
|
1727
|
+
.description("Approve a scheduled post so the worker can publish it when due.")
|
|
1728
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1729
|
+
.option("--json", "Print a JSON envelope.")
|
|
1730
|
+
.action(async (postId, options) => {
|
|
1731
|
+
await handleAsyncAction("publishing posts approve", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/approve`, {
|
|
1732
|
+
method: "POST",
|
|
1733
|
+
}));
|
|
1734
|
+
}))
|
|
1735
|
+
.addCommand(new Command("cancel")
|
|
1736
|
+
.description("Cancel a draft, scheduled, queued, or failed post.")
|
|
1737
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1738
|
+
.option("--json", "Print a JSON envelope.")
|
|
1739
|
+
.action(async (postId, options) => {
|
|
1740
|
+
await handleAsyncAction("publishing posts cancel", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/cancel`, {
|
|
1741
|
+
method: "POST",
|
|
1742
|
+
}));
|
|
1743
|
+
}))
|
|
1744
|
+
.addCommand(new Command("retry")
|
|
1745
|
+
.description("Move a failed post back to scheduled for another worker attempt.")
|
|
1746
|
+
.argument("<post_id>", "Scheduled post id.")
|
|
1747
|
+
.option("--json", "Print a JSON envelope.")
|
|
1748
|
+
.action(async (postId, options) => {
|
|
1749
|
+
await handleAsyncAction("publishing posts retry", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/retry`, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
}));
|
|
1752
|
+
})))
|
|
1753
|
+
.addCommand(new Command("media")
|
|
1754
|
+
.description("Manage Publishing media uploads.")
|
|
1755
|
+
.addCommand(new Command("upload-url")
|
|
1756
|
+
.description("Create a Publishing media asset and presigned object-storage upload URL.")
|
|
1757
|
+
.requiredOption("--file-name <name>", "Original media filename.")
|
|
1758
|
+
.requiredOption("--content-type <type>", "Media MIME type, such as image/png or video/mp4.")
|
|
1759
|
+
.requiredOption("--byte-length <bytes>", "Exact media byte length.")
|
|
1760
|
+
.option("--scheduled-post <post_id>", "Optional scheduled post id to associate with the media.")
|
|
1761
|
+
.option("--metadata-json <json>", "Optional metadata object to store with the media asset.")
|
|
1762
|
+
.option("--json", "Print a JSON envelope.")
|
|
1763
|
+
.action(async (options) => {
|
|
1764
|
+
await handleAsyncAction("publishing media upload-url", options, () => requestOxygen("/api/cli/publishing/media/upload-url", {
|
|
1765
|
+
method: "POST",
|
|
1766
|
+
body: buildPublishingMediaUploadUrlBody(options),
|
|
1767
|
+
}));
|
|
1768
|
+
}))
|
|
1769
|
+
.addCommand(new Command("uploaded")
|
|
1770
|
+
.description("Verify an uploaded Publishing media object and mark the media asset uploaded.")
|
|
1771
|
+
.argument("<media_id>", "Publishing media asset id.")
|
|
1772
|
+
.option("--sha256 <hex>", "Optional SHA-256 digest of the uploaded bytes.")
|
|
1773
|
+
.option("--json", "Print a JSON envelope.")
|
|
1774
|
+
.action(async (mediaId, options) => {
|
|
1775
|
+
await handleAsyncAction("publishing media uploaded", options, () => requestOxygen(`/api/cli/publishing/media/${encodeURIComponent(mediaId)}/uploaded`, {
|
|
1776
|
+
method: "POST",
|
|
1777
|
+
body: buildPublishingMediaUploadedBody(options),
|
|
1778
|
+
}));
|
|
1779
|
+
})));
|
|
1520
1780
|
program
|
|
1521
1781
|
.command("dashboards")
|
|
1522
1782
|
.description("Default GTM dashboards: the stitched Command Center funnel across the sequencer, unibox, and CRM.")
|
|
@@ -2591,6 +2851,315 @@ export function createProgram() {
|
|
|
2591
2851
|
body: { id: assetId },
|
|
2592
2852
|
}));
|
|
2593
2853
|
})));
|
|
2854
|
+
program
|
|
2855
|
+
.command("knowledge")
|
|
2856
|
+
.description("Company knowledge wiki: slug-addressed pages, a [[wikilink]] graph, and a decision log.")
|
|
2857
|
+
.addCommand(new Command("page")
|
|
2858
|
+
.description("Knowledge wiki pages (slug-addressed, revision-guarded).")
|
|
2859
|
+
.addCommand(new Command("upsert")
|
|
2860
|
+
.description("Create or update a knowledge wiki page.")
|
|
2861
|
+
.option("--slug <slug>", "Stable page slug to create or address. Omit to create by title.")
|
|
2862
|
+
.option("--id <page_id>", "Existing page UUID to update. Omit to create.")
|
|
2863
|
+
.option("--type <type>", "Page type (positioning, competitor, research_note, playbook, strategy, other). Defaults to other on create.")
|
|
2864
|
+
.option("--title <title>", "Page title. Required on create.")
|
|
2865
|
+
.option("--status <status>", "draft, active, or archived.")
|
|
2866
|
+
.option("--tags <csv>", "Comma-separated page tags.")
|
|
2867
|
+
.option("--summary <text>", "Short page summary.")
|
|
2868
|
+
.option("--body <text>", "Page body (Markdown, supports [[wikilinks]]).")
|
|
2869
|
+
.option("--data-json <json>", "Flexible JSON object for structured page data.")
|
|
2870
|
+
.option("--expected-revision <n>", "Fail the write if the current page revision differs (optimistic concurrency guard).")
|
|
2871
|
+
.option("--approved", "Mark the write as human-approved.")
|
|
2872
|
+
.option("--json", "Print a JSON envelope.")
|
|
2873
|
+
.action(async (options) => {
|
|
2874
|
+
await handleAsyncAction("knowledge page upsert", options, async () => {
|
|
2875
|
+
const data = await requestOxygen("/api/cli/knowledge/pages/upsert", {
|
|
2876
|
+
method: "POST",
|
|
2877
|
+
body: buildKnowledgePageUpsertBody(options),
|
|
2878
|
+
});
|
|
2879
|
+
await markKnowledgeMirrorStaleAfterWrite();
|
|
2880
|
+
return data;
|
|
2881
|
+
});
|
|
2882
|
+
}))
|
|
2883
|
+
.addCommand(new Command("get")
|
|
2884
|
+
.description("Read one knowledge wiki page by slug or UUID.")
|
|
2885
|
+
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
2886
|
+
.option("--json", "Print a JSON envelope.")
|
|
2887
|
+
.action(async (slugOrId, options) => {
|
|
2888
|
+
await handleAsyncAction("knowledge page get", options, () => requestOxygen("/api/cli/knowledge/pages/get", {
|
|
2889
|
+
method: "POST",
|
|
2890
|
+
body: { slug: slugOrId },
|
|
2891
|
+
}));
|
|
2892
|
+
}))
|
|
2893
|
+
.addCommand(new Command("list")
|
|
2894
|
+
.description("List knowledge wiki pages.")
|
|
2895
|
+
.option("--type <type>", "Filter by page type.")
|
|
2896
|
+
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
2897
|
+
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
2898
|
+
.option("--limit <n>", "Maximum pages to return.")
|
|
2899
|
+
.option("--json", "Print a JSON envelope.")
|
|
2900
|
+
.action(async (options) => {
|
|
2901
|
+
await handleAsyncAction("knowledge pages", options, () => requestOxygen("/api/cli/knowledge/pages", {
|
|
2902
|
+
method: "POST",
|
|
2903
|
+
body: buildKnowledgePageListBody(options),
|
|
2904
|
+
}));
|
|
2905
|
+
}))
|
|
2906
|
+
.addCommand(new Command("archive")
|
|
2907
|
+
.description("Archive a knowledge wiki page.")
|
|
2908
|
+
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
2909
|
+
.option("--json", "Print a JSON envelope.")
|
|
2910
|
+
.action(async (slugOrId, options) => {
|
|
2911
|
+
await handleAsyncAction("knowledge page archive", options, async () => {
|
|
2912
|
+
const data = await requestOxygen("/api/cli/knowledge/pages/archive", {
|
|
2913
|
+
method: "POST",
|
|
2914
|
+
body: knowledgePageRefBody(slugOrId),
|
|
2915
|
+
});
|
|
2916
|
+
await markKnowledgeMirrorStaleAfterWrite();
|
|
2917
|
+
return data;
|
|
2918
|
+
});
|
|
2919
|
+
}))
|
|
2920
|
+
.addCommand(new Command("pin")
|
|
2921
|
+
.description("Pin a knowledge wiki page as the canonical default for its type. Canonical voice/brand/positioning pages are auto-applied to AI copy generation.")
|
|
2922
|
+
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
2923
|
+
.option("--canonical", "Pin as the canonical default for its type (default).")
|
|
2924
|
+
.option("--no-canonical", "Unpin instead of pin (clears the canonical default for this type).")
|
|
2925
|
+
.option("--approved", "Mark the pin as human-approved (required for sensitive types: voice, brand, positioning).")
|
|
2926
|
+
.option("--json", "Print a JSON envelope.")
|
|
2927
|
+
.action(async (slugOrId, options) => {
|
|
2928
|
+
await handleAsyncAction("knowledge page pin", options, async () => {
|
|
2929
|
+
const data = await requestOxygen("/api/cli/knowledge/pages/pin", {
|
|
2930
|
+
method: "POST",
|
|
2931
|
+
body: {
|
|
2932
|
+
...knowledgePageRefBody(slugOrId),
|
|
2933
|
+
canonical: options.canonical !== false,
|
|
2934
|
+
...(options.approved ? { approved: true } : {}),
|
|
2935
|
+
},
|
|
2936
|
+
});
|
|
2937
|
+
await markKnowledgeMirrorStaleAfterWrite();
|
|
2938
|
+
return data;
|
|
2939
|
+
});
|
|
2940
|
+
}))
|
|
2941
|
+
.addCommand(new Command("revisions")
|
|
2942
|
+
.description("List the revision history of a knowledge wiki page, newest first.")
|
|
2943
|
+
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
2944
|
+
.option("--limit <n>", "Maximum revisions to return.")
|
|
2945
|
+
.option("--json", "Print a JSON envelope.")
|
|
2946
|
+
.action(async (slugOrId, options) => {
|
|
2947
|
+
await handleAsyncAction("knowledge page revisions", options, () => {
|
|
2948
|
+
const limit = readPositiveInt(options.limit);
|
|
2949
|
+
return requestOxygen("/api/cli/knowledge/pages/revisions", {
|
|
2950
|
+
method: "POST",
|
|
2951
|
+
body: {
|
|
2952
|
+
...knowledgePageRefBody(slugOrId),
|
|
2953
|
+
...(limit !== undefined ? { limit } : {}),
|
|
2954
|
+
},
|
|
2955
|
+
});
|
|
2956
|
+
});
|
|
2957
|
+
}))
|
|
2958
|
+
.addCommand(new Command("revision")
|
|
2959
|
+
.description("Read one full revision snapshot of a knowledge wiki page.")
|
|
2960
|
+
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
2961
|
+
.argument("<revision>", "Revision number.")
|
|
2962
|
+
.option("--json", "Print a JSON envelope.")
|
|
2963
|
+
.action(async (slugOrId, revision, options) => {
|
|
2964
|
+
await handleAsyncAction("knowledge page revision", options, () => {
|
|
2965
|
+
const revisionNumber = readPositiveInt(revision);
|
|
2966
|
+
if (revisionNumber === undefined) {
|
|
2967
|
+
throw new OxygenError("invalid_request", "Revision must be a positive integer.", {
|
|
2968
|
+
details: { revision },
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
return requestOxygen("/api/cli/knowledge/pages/revisions/get", {
|
|
2972
|
+
method: "POST",
|
|
2973
|
+
body: {
|
|
2974
|
+
...knowledgePageRefBody(slugOrId),
|
|
2975
|
+
revision: revisionNumber,
|
|
2976
|
+
},
|
|
2977
|
+
});
|
|
2978
|
+
});
|
|
2979
|
+
})))
|
|
2980
|
+
.addCommand(new Command("search")
|
|
2981
|
+
.description("Full-text search across knowledge wiki pages, ranked by relevance.")
|
|
2982
|
+
.argument("<query>", "Search text. Supports quoted phrases, OR, and -exclude.")
|
|
2983
|
+
.option("--type <type>", "Filter by page type.")
|
|
2984
|
+
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
2985
|
+
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
2986
|
+
.option("--limit <n>", "Maximum pages to return.")
|
|
2987
|
+
.option("--json", "Print a JSON envelope.")
|
|
2988
|
+
.action(async (query, options) => {
|
|
2989
|
+
await handleAsyncAction("knowledge search", options, () => requestOxygen("/api/cli/knowledge/search", {
|
|
2990
|
+
method: "POST",
|
|
2991
|
+
body: buildKnowledgeSearchBody(query, options),
|
|
2992
|
+
}));
|
|
2993
|
+
}))
|
|
2994
|
+
.addCommand(new Command("index")
|
|
2995
|
+
.description("Compact wiki index: every page's slug, one-liner, tags, link degree, plus type/status counts.")
|
|
2996
|
+
.option("--json", "Print a JSON envelope.")
|
|
2997
|
+
.action(async (options) => {
|
|
2998
|
+
await handleAsyncAction("knowledge index", options, () => requestOxygen("/api/cli/knowledge/index"));
|
|
2999
|
+
}))
|
|
3000
|
+
.addCommand(new Command("graph")
|
|
3001
|
+
.description("Knowledge graph of pages and their [[wikilink]] edges.")
|
|
3002
|
+
.option("--max-nodes <n>", "Maximum nodes to include in the graph.")
|
|
3003
|
+
.option("--json", "Print a JSON envelope.")
|
|
3004
|
+
.action(async (options) => {
|
|
3005
|
+
await handleAsyncAction("knowledge graph", options, () => requestOxygen("/api/cli/knowledge/graph", {
|
|
3006
|
+
method: "POST",
|
|
3007
|
+
body: buildKnowledgeGraphBody(options),
|
|
3008
|
+
}));
|
|
3009
|
+
}))
|
|
3010
|
+
.addCommand(new Command("lint")
|
|
3011
|
+
.description("Structural wiki health report: orphans, unresolved links, missing canonicals, stale/oversized pages, aged proposals.")
|
|
3012
|
+
.option("--json", "Print a JSON envelope.")
|
|
3013
|
+
.action(async (options) => {
|
|
3014
|
+
await handleAsyncAction("knowledge lint", options, () => requestOxygen("/api/cli/knowledge/lint"));
|
|
3015
|
+
})
|
|
3016
|
+
.addCommand(new Command("relink")
|
|
3017
|
+
.description("Recompute [[wikilink]] edges (and gap-fill unlinked mentions) across wiki pages.")
|
|
3018
|
+
.option("--slugs <csv>", "Comma-separated page slugs to relink. Omit to relink every page.")
|
|
3019
|
+
.option("--json", "Print a JSON envelope.")
|
|
3020
|
+
.action(async (options) => {
|
|
3021
|
+
await handleAsyncAction("knowledge lint relink", options, () => {
|
|
3022
|
+
const slugs = readCsvOption(options.slugs);
|
|
3023
|
+
return requestOxygen("/api/cli/knowledge/lint/relink", {
|
|
3024
|
+
method: "POST",
|
|
3025
|
+
body: slugs.length > 0 ? { slugs } : {},
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
})))
|
|
3029
|
+
.addCommand(new Command("log")
|
|
3030
|
+
.description("Knowledge decision and activity log.")
|
|
3031
|
+
.addCommand(new Command("list")
|
|
3032
|
+
.description("List knowledge log entries, newest first.")
|
|
3033
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
3034
|
+
.option("--limit <n>", "Maximum log entries to return.")
|
|
3035
|
+
.option("--json", "Print a JSON envelope.")
|
|
3036
|
+
.action(async (options) => {
|
|
3037
|
+
await handleAsyncAction("knowledge log", options, () => requestOxygen("/api/cli/knowledge/log", {
|
|
3038
|
+
method: "POST",
|
|
3039
|
+
body: buildKnowledgeLogListBody(options),
|
|
3040
|
+
}));
|
|
3041
|
+
}))
|
|
3042
|
+
.addCommand(new Command("append")
|
|
3043
|
+
.description("Append an entry to the knowledge log.")
|
|
3044
|
+
.requiredOption("--event <event>", "note, query_filed, or decision.")
|
|
3045
|
+
.option("--slug <slug>", "Related page slug.")
|
|
3046
|
+
.option("--summary <text>", "Short summary of the log entry.")
|
|
3047
|
+
.option("--data-json <json>", "Flexible JSON object for structured log data.")
|
|
3048
|
+
.option("--json", "Print a JSON envelope.")
|
|
3049
|
+
.action(async (options) => {
|
|
3050
|
+
await handleAsyncAction("knowledge log append", options, () => requestOxygen("/api/cli/knowledge/log/append", {
|
|
3051
|
+
method: "POST",
|
|
3052
|
+
body: buildKnowledgeLogAppendBody(options),
|
|
3053
|
+
}));
|
|
3054
|
+
})))
|
|
3055
|
+
.addCommand(new Command("proposals")
|
|
3056
|
+
.description("Draft change proposals against wiki pages, awaiting human review.")
|
|
3057
|
+
.option("--status <status>", "open, decided, or all. Defaults to open.")
|
|
3058
|
+
.option("--json", "Print a JSON envelope.")
|
|
3059
|
+
.action(async (options) => {
|
|
3060
|
+
await handleAsyncAction("knowledge proposals", options, () => requestOxygen("/api/cli/knowledge/proposals", {
|
|
3061
|
+
method: "POST",
|
|
3062
|
+
body: {
|
|
3063
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
3064
|
+
},
|
|
3065
|
+
}));
|
|
3066
|
+
})
|
|
3067
|
+
.addCommand(new Command("approve")
|
|
3068
|
+
.description("Approve a knowledge proposal and apply it to the target page.")
|
|
3069
|
+
.argument("<id_or_slug>", "Proposal UUID or proposal page slug.")
|
|
3070
|
+
.option("--force", "Apply even if the target page changed since the proposal was drafted.")
|
|
3071
|
+
.option("--json", "Print a JSON envelope.")
|
|
3072
|
+
.action(async (idOrSlug, options) => {
|
|
3073
|
+
await handleAsyncAction("knowledge proposals approve", options, () => requestOxygen("/api/cli/knowledge/proposals/approve", {
|
|
3074
|
+
method: "POST",
|
|
3075
|
+
body: {
|
|
3076
|
+
...knowledgeProposalRefBody(idOrSlug),
|
|
3077
|
+
...(options.force ? { force: true } : {}),
|
|
3078
|
+
},
|
|
3079
|
+
}));
|
|
3080
|
+
}))
|
|
3081
|
+
.addCommand(new Command("reject")
|
|
3082
|
+
.description("Reject a knowledge proposal without changing the target page.")
|
|
3083
|
+
.argument("<id_or_slug>", "Proposal UUID or proposal page slug.")
|
|
3084
|
+
.option("--json", "Print a JSON envelope.")
|
|
3085
|
+
.action(async (idOrSlug, options) => {
|
|
3086
|
+
await handleAsyncAction("knowledge proposals reject", options, () => requestOxygen("/api/cli/knowledge/proposals/reject", {
|
|
3087
|
+
method: "POST",
|
|
3088
|
+
body: knowledgeProposalRefBody(idOrSlug),
|
|
3089
|
+
}));
|
|
3090
|
+
})))
|
|
3091
|
+
.addCommand(new Command("resolve")
|
|
3092
|
+
.description("Resolve task-scoped workspace GTM context with readiness and revision provenance.")
|
|
3093
|
+
.option("--purpose <purpose>", "general, lead_sourcing, qualification, outbound_copy, or workflow_design.")
|
|
3094
|
+
.option("--asset-type <csv>", "Comma-separated context asset types to include.")
|
|
3095
|
+
.option("--asset-status <status>", "draft, active, archived, or all. Defaults to active.")
|
|
3096
|
+
.option("--tags <csv>", "Comma-separated asset tags that must be present.")
|
|
3097
|
+
.option("--include-archived", "Include archived assets when no asset status is set.")
|
|
3098
|
+
.option("--max-assets <n>", "Maximum assets to include. Defaults to 10, max 50.")
|
|
3099
|
+
.option("--require-ready", "Exit with a conflict error when required context sections are missing.")
|
|
3100
|
+
.option("--json", "Print a JSON envelope.")
|
|
3101
|
+
.action(async (options) => {
|
|
3102
|
+
await handleAsyncAction("knowledge resolve", options, () => requestOxygen("/api/cli/knowledge/resolve", {
|
|
3103
|
+
method: "POST",
|
|
3104
|
+
body: buildContextResolveBody(options),
|
|
3105
|
+
}));
|
|
3106
|
+
}))
|
|
3107
|
+
.addCommand(new Command("profile")
|
|
3108
|
+
.description("Company profile and ICP memory.")
|
|
3109
|
+
.option("--json", "Print a JSON envelope.")
|
|
3110
|
+
.action(async (options) => {
|
|
3111
|
+
await handleAsyncAction("knowledge profile", options, () => requestOxygen("/api/cli/knowledge/profile"));
|
|
3112
|
+
})
|
|
3113
|
+
.addCommand(new Command("update")
|
|
3114
|
+
.description("Merge one or more profile sections into workspace GTM memory.")
|
|
3115
|
+
.requiredOption("--data-json <json>", "JSON object with company, offering, icp, market, gtm_stack, or custom sections.")
|
|
3116
|
+
.option("--summary <text>", "Optional concise summary for the profile.")
|
|
3117
|
+
.option("--json", "Print a JSON envelope.")
|
|
3118
|
+
.action(async (options) => {
|
|
3119
|
+
await handleAsyncAction("knowledge profile update", options, () => requestOxygen("/api/cli/knowledge/profile/update", {
|
|
3120
|
+
method: "POST",
|
|
3121
|
+
body: {
|
|
3122
|
+
data: parseJsonObject(options.dataJson ?? "{}"),
|
|
3123
|
+
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
3124
|
+
},
|
|
3125
|
+
}));
|
|
3126
|
+
})))
|
|
3127
|
+
.addCommand(new Command("seed")
|
|
3128
|
+
.description("Seed the knowledge wiki with starter pages.")
|
|
3129
|
+
.option("--json", "Print a JSON envelope.")
|
|
3130
|
+
.action(async (options) => {
|
|
3131
|
+
await handleAsyncAction("knowledge seed", options, () => requestOxygen("/api/cli/knowledge/seed", {
|
|
3132
|
+
method: "POST",
|
|
3133
|
+
body: {},
|
|
3134
|
+
}));
|
|
3135
|
+
}))
|
|
3136
|
+
.addCommand(new Command("sync")
|
|
3137
|
+
.description("Clone or refresh the local knowledge mirror: server-rendered page markdown under the CLI config dir.")
|
|
3138
|
+
.option("--full", "Purge the mirror (including its cursor) and re-clone every page.")
|
|
3139
|
+
.option("--if-stale", "Skip when the mirror completed a sync within the TTL (or one is already running).")
|
|
3140
|
+
.option("--ttl <minutes>", "Freshness window for --if-stale, in minutes. Defaults to 15.")
|
|
3141
|
+
.option("--json", "Print a JSON envelope.")
|
|
3142
|
+
.action(async (options) => {
|
|
3143
|
+
await handleAsyncAction("knowledge sync", options, () => runKnowledgeMirrorSync(options));
|
|
3144
|
+
}))
|
|
3145
|
+
.addCommand(new Command("status")
|
|
3146
|
+
.description("Report the local knowledge mirror: path, page count, last sync, quarantined conflicts.")
|
|
3147
|
+
.option("--print-path", "Print only the mirror directory path.")
|
|
3148
|
+
.option("--clear-conflicts", "Delete quarantined conflict copies from the mirror sidecar.")
|
|
3149
|
+
.option("--json", "Print a JSON envelope.")
|
|
3150
|
+
.action(async (options) => {
|
|
3151
|
+
if (options.printPath) {
|
|
3152
|
+
await handleKnowledgeMirrorPrintPathAction();
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
await handleAsyncAction("knowledge status", options, () => runKnowledgeMirrorStatus(options));
|
|
3156
|
+
}))
|
|
3157
|
+
.addCommand(new Command("purge")
|
|
3158
|
+
.description("Delete the whole local knowledge mirror for the active API host and organization.")
|
|
3159
|
+
.option("--json", "Print a JSON envelope.")
|
|
3160
|
+
.action(async (options) => {
|
|
3161
|
+
await handleAsyncAction("knowledge purge", options, () => runKnowledgeMirrorPurge());
|
|
3162
|
+
}));
|
|
2594
3163
|
program
|
|
2595
3164
|
.command("blueprints")
|
|
2596
3165
|
.description("Portable Oxygen blueprints: bundle a workflow + tables + columns + prompts as shareable JSON.")
|
|
@@ -4074,6 +4643,52 @@ export function createProgram() {
|
|
|
4074
4643
|
...(options.approve ? { approve: true } : {}),
|
|
4075
4644
|
},
|
|
4076
4645
|
}));
|
|
4646
|
+
}))
|
|
4647
|
+
.addCommand(new Command("register")
|
|
4648
|
+
.description("STAFF: register a manually-purchased egress IP (e.g. IPRoyal ISP-dedicated) into this environment's inventory. Secrets never pass here — --credential-ref names the Doppler family EGRESS_CRED_<REF>_USERNAME/_PASSWORD, set separately in Doppler. Register, then `egress assign`, then enable EGRESS_POOL_ENABLED (pool mode fails closed for unassigned mailboxes).")
|
|
4649
|
+
.requiredOption("--host <host>", "Proxy hostname or IP — bare, no scheme/port/credentials.")
|
|
4650
|
+
.requiredOption("--port <port>", "Proxy port (1-65535).")
|
|
4651
|
+
.requiredOption("--credential-ref <ref>", "Doppler credential-family name (A-Z, 0-9, _), e.g. IPROYAL.")
|
|
4652
|
+
.option("--vendor <vendor>", "Vendor slug for inventory provenance.", "iproyal")
|
|
4653
|
+
.option("--protocol <protocol>", "Proxy protocol: http (default), https, or socks5.")
|
|
4654
|
+
.option("--tier <tier>", "shared (default) or dedicated (requires --dedicated-org).")
|
|
4655
|
+
.option("--dedicated-org <orgId>", "Organization id a dedicated-tier IP is reserved for.")
|
|
4656
|
+
.option("--session-label <label>", "Optional vendor session/order label for provenance.")
|
|
4657
|
+
.option("--json", "Print a JSON envelope.")
|
|
4658
|
+
.action(async (options) => {
|
|
4659
|
+
await handleAsyncAction("egress register", options, () => requestOxygen("/api/cli/egress/register", {
|
|
4660
|
+
method: "POST",
|
|
4661
|
+
body: {
|
|
4662
|
+
host: readOption(options.host),
|
|
4663
|
+
port: Number(readOption(options.port)),
|
|
4664
|
+
credential_ref: readOption(options.credentialRef),
|
|
4665
|
+
vendor: readOption(options.vendor),
|
|
4666
|
+
...(readOption(options.protocol) ? { protocol: readOption(options.protocol) } : {}),
|
|
4667
|
+
...(readOption(options.tier) ? { tier: readOption(options.tier) } : {}),
|
|
4668
|
+
...(readOption(options.dedicatedOrg) ? { dedicated_organization_id: readOption(options.dedicatedOrg) } : {}),
|
|
4669
|
+
...(readOption(options.sessionLabel) ? { session_label: readOption(options.sessionLabel) } : {}),
|
|
4670
|
+
},
|
|
4671
|
+
}));
|
|
4672
|
+
}))
|
|
4673
|
+
.addCommand(new Command("assign")
|
|
4674
|
+
.description("Assign sending mailboxes to inventoried egress IPs — least-loaded across the active pool, or pinned with --ip. Existing open assignments are kept (stable for life; use rotate to re-home a burned IP). Admin/owner role; no spend, 0 credits.")
|
|
4675
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to assign.")
|
|
4676
|
+
.option("--all", "Assign every sending mailbox in the pool that lacks an open assignment.")
|
|
4677
|
+
.option("--ip <egressIpId>", "Pin the targeted mailboxes to this egress IP id (must be active and org-visible).")
|
|
4678
|
+
.option("--json", "Print a JSON envelope.")
|
|
4679
|
+
.action(async (options) => {
|
|
4680
|
+
await handleAsyncAction("egress assign", options, () => {
|
|
4681
|
+
const list = readOption(options.mailboxes);
|
|
4682
|
+
const mailboxes = list ? list.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
|
|
4683
|
+
return requestOxygen("/api/cli/egress/assign", {
|
|
4684
|
+
method: "POST",
|
|
4685
|
+
body: {
|
|
4686
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
4687
|
+
...(options.all ? { all: true } : {}),
|
|
4688
|
+
...(readOption(options.ip) ? { egress_ip_id: readOption(options.ip) } : {}),
|
|
4689
|
+
},
|
|
4690
|
+
});
|
|
4691
|
+
});
|
|
4077
4692
|
})));
|
|
4078
4693
|
program
|
|
4079
4694
|
.command("directory")
|
|
@@ -6293,7 +6908,7 @@ export function createProgram() {
|
|
|
6293
6908
|
});
|
|
6294
6909
|
}))
|
|
6295
6910
|
.addCommand(new Command("update")
|
|
6296
|
-
.description("Update a DRAFT sequence's name, journey, channels, senders, email binding, or LinkedIn credit cap. The journey is re-validated; the email binding stays editable only while the sequence is a draft. --max-credits is locked once the sequence has been started (re-run `sequences start`). Pass only the fields you want to change.")
|
|
6911
|
+
.description("Update a DRAFT sequence's name, journey, channels, senders, email binding, or LinkedIn credit cap, plus the always-editable open/click tracking toggles (--[no-]tracking-opens / --[no-]tracking-clicks, default on). The journey is re-validated; the email binding stays editable only while the sequence is a draft. --max-credits is locked once the sequence has been started (re-run `sequences start`). Pass only the fields you want to change.")
|
|
6297
6912
|
.argument("<sequence>", "Sequence id or slug.")
|
|
6298
6913
|
.option("--name <name>", "New human-readable sequence name.")
|
|
6299
6914
|
.option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey.")
|
|
@@ -6315,6 +6930,10 @@ export function createProgram() {
|
|
|
6315
6930
|
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
6316
6931
|
.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.")
|
|
6317
6932
|
.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.")
|
|
6933
|
+
.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`).")
|
|
6934
|
+
.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).")
|
|
6935
|
+
.option("--tracking-clicks", "Turn click-link tracking ON for this sequence's native email sends (the default; same verified-tracking-domain gate as opens).")
|
|
6936
|
+
.option("--no-tracking-clicks", "Turn OFF click-link rewriting for this sequence's native email sends (links go out untouched).")
|
|
6318
6937
|
.option("--json", "Print a JSON envelope.")
|
|
6319
6938
|
.action(async (sequence, options) => {
|
|
6320
6939
|
await handleAsyncAction("sequences update", options, () => {
|
|
@@ -6341,6 +6960,12 @@ export function createProgram() {
|
|
|
6341
6960
|
const settings = readSequenceSettings(options);
|
|
6342
6961
|
if (settings)
|
|
6343
6962
|
body.settings = settings;
|
|
6963
|
+
// Tri-state tracking toggles: only send the key when a flag was
|
|
6964
|
+
// explicitly passed, so an unrelated update never flips tracking.
|
|
6965
|
+
if (options.trackingOpens !== undefined)
|
|
6966
|
+
body.tracking_opens = options.trackingOpens;
|
|
6967
|
+
if (options.trackingClicks !== undefined)
|
|
6968
|
+
body.tracking_clicks = options.trackingClicks;
|
|
6344
6969
|
if (options.clearEmail) {
|
|
6345
6970
|
body.email = null;
|
|
6346
6971
|
}
|
|
@@ -6350,7 +6975,7 @@ export function createProgram() {
|
|
|
6350
6975
|
body.email = email;
|
|
6351
6976
|
}
|
|
6352
6977
|
if (Object.keys(body).length === 0) {
|
|
6353
|
-
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file,
|
|
6978
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file, --no-stop-on-bounce, or --[no-]tracking-opens / --[no-]tracking-clicks).");
|
|
6354
6979
|
}
|
|
6355
6980
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
6356
6981
|
method: "PATCH",
|
|
@@ -6618,7 +7243,7 @@ export function createProgram() {
|
|
|
6618
7243
|
to: readOption(options.to),
|
|
6619
7244
|
subject: readOption(options.subject),
|
|
6620
7245
|
body: bodyText,
|
|
6621
|
-
...(htmlBody
|
|
7246
|
+
...(htmlBody?.trim() ? { html: htmlBody } : {}),
|
|
6622
7247
|
...(readOption(options.displayName) ? { display_name: readOption(options.displayName) } : {}),
|
|
6623
7248
|
...(cc.length > 0 ? { cc } : {}),
|
|
6624
7249
|
...(bcc.length > 0 ? { bcc } : {}),
|
|
@@ -6826,31 +7451,35 @@ export function createProgram() {
|
|
|
6826
7451
|
await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
|
|
6827
7452
|
}))
|
|
6828
7453
|
.addCommand(new Command("warmup")
|
|
6829
|
-
.description("Mailbox warmup via Instantly
|
|
7454
|
+
.description("Mailbox warmup via Instantly — BYOK over your connected Instantly account, or --billing-mode managed via the OXYGEN-managed Instantly workspace (both 0 Oxygen credits).")
|
|
6830
7455
|
.addCommand(new Command("enable")
|
|
6831
|
-
.description("Enable Instantly warmup for sending mailboxes and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
7456
|
+
.description("Enable Instantly warmup for sending mailboxes and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given. Omit --billing-mode for automatic: BYOK when your Instantly is connected, else managed when enabled on the deployment.")
|
|
6832
7457
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
6833
|
-
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
7458
|
+
.option("--connection <id>", "Instantly connection id (forces BYOK). Defaults to the org's active Instantly connection.")
|
|
7459
|
+
.option("--billing-mode <mode>", "byok = your Instantly connection; managed = the OXYGEN-managed Instantly workspace.")
|
|
6834
7460
|
.option("--dry-run", "Simulate without calling Instantly (mailboxes marked pending).")
|
|
6835
7461
|
.option("--json", "Print a JSON envelope.")
|
|
6836
7462
|
.action(async (options) => {
|
|
6837
7463
|
await handleAsyncAction("mailboxes warmup enable", options, () => {
|
|
6838
7464
|
const mailboxes = readCsvOption(options.mailboxes);
|
|
6839
7465
|
const connection = readOption(options.connection);
|
|
7466
|
+
const billingMode = readOption(options.billingMode);
|
|
6840
7467
|
return requestOxygen("/api/cli/mailboxes/warmup", {
|
|
6841
7468
|
method: "POST",
|
|
6842
7469
|
body: {
|
|
6843
7470
|
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
6844
7471
|
...(connection ? { connection_id: connection } : {}),
|
|
7472
|
+
...(billingMode ? { billing_mode: billingMode } : {}),
|
|
6845
7473
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
6846
7474
|
},
|
|
6847
7475
|
});
|
|
6848
7476
|
});
|
|
6849
7477
|
}))
|
|
6850
7478
|
.addCommand(new Command("status")
|
|
6851
|
-
.description("Sync Instantly's warmup analytics into the pool, updating each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
7479
|
+
.description("Sync Instantly's warmup analytics into the pool, updating each mailbox's warmup state. Targets the whole pool unless --mailboxes is given. Omit --billing-mode for automatic: BYOK when connected, else managed when enabled.")
|
|
6852
7480
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
6853
|
-
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
7481
|
+
.option("--connection <id>", "Instantly connection id (forces BYOK). Defaults to the org's active Instantly connection.")
|
|
7482
|
+
.option("--billing-mode <mode>", "byok = your Instantly connection; managed = the OXYGEN-managed Instantly workspace.")
|
|
6854
7483
|
.option("--dry-run", "Skip the Instantly call (mailboxes marked pending).")
|
|
6855
7484
|
.option("--json", "Print a JSON envelope.")
|
|
6856
7485
|
.action(async (options) => {
|
|
@@ -6862,6 +7491,9 @@ export function createProgram() {
|
|
|
6862
7491
|
const connection = readOption(options.connection);
|
|
6863
7492
|
if (connection)
|
|
6864
7493
|
params.set("connection_id", connection);
|
|
7494
|
+
const billingMode = readOption(options.billingMode);
|
|
7495
|
+
if (billingMode)
|
|
7496
|
+
params.set("billing_mode", billingMode);
|
|
6865
7497
|
if (options.dryRun)
|
|
6866
7498
|
params.set("dry_run", "true");
|
|
6867
7499
|
const suffix = params.toString();
|
|
@@ -6918,14 +7550,15 @@ export function createProgram() {
|
|
|
6918
7550
|
await handleAsyncAction("mailboxes order-status", options, () => requestOxygen(`/api/cli/mailboxes/orders/${encodeURIComponent(order)}`));
|
|
6919
7551
|
})));
|
|
6920
7552
|
program.addCommand(new Command("deliverability")
|
|
6921
|
-
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests. Placement tests are approval-gated paid runs (managed
|
|
7553
|
+
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard or Zapmail. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test); fail closed with 409 when no health provider is connected.")
|
|
6922
7554
|
.addCommand(new Command("placement-test")
|
|
6923
7555
|
.description("Directional inbox-placement (spam) tests: run, list, and get. Results estimate inbox-vs-spam landing from seed inboxes.")
|
|
6924
7556
|
.addCommand(new Command("run")
|
|
6925
|
-
.description("Start a placement test for one sending mailbox. Without --approved this returns a PREVIEW (billing mode + credits_required). Re-run with --approved and --max-credits >= credits_required to send.")
|
|
7557
|
+
.description("Start a placement test for one sending mailbox. Without --approved this returns a PREVIEW (billing mode + credits_required). Re-run with --approved and --max-credits >= credits_required to send. Zapmail runs test Zapmail-hosted mailboxes ONLY, compose their own probe (subject/body ignored), and complete async (2-24h).")
|
|
6926
7558
|
.argument("<mailbox>", "Sending mailbox address to test (e.g. ada@send.acme.com).")
|
|
6927
|
-
.option("--
|
|
6928
|
-
.option("--
|
|
7559
|
+
.option("--provider <provider>", "Health provider: emailguard or zapmail. Omit to auto-resolve (Zapmail-hosted mailboxes route to zapmail, else emailguard).")
|
|
7560
|
+
.option("--subject <subject>", "Optional subject for the seed message (zapmail ignores it beyond labeling the test).")
|
|
7561
|
+
.option("--body <text>", "Optional plain-text body for the seed message (ignored by zapmail).")
|
|
6929
7562
|
.option("--approved", "Actually run the test (otherwise a preview is returned).")
|
|
6930
7563
|
.option("--max-credits <n>", "Credit cap the caller accepts (must be >= credits_required for managed runs).")
|
|
6931
7564
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6934,6 +7567,7 @@ export function createProgram() {
|
|
|
6934
7567
|
method: "POST",
|
|
6935
7568
|
body: {
|
|
6936
7569
|
mailbox,
|
|
7570
|
+
...(readOption(options.provider) ? { provider: readOption(options.provider) } : {}),
|
|
6937
7571
|
...(readOption(options.subject) ? { subject: readOption(options.subject) } : {}),
|
|
6938
7572
|
...(readOption(options.body) ? { body_text: readOption(options.body) } : {}),
|
|
6939
7573
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -6942,7 +7576,7 @@ export function createProgram() {
|
|
|
6942
7576
|
}));
|
|
6943
7577
|
}))
|
|
6944
7578
|
.addCommand(new Command("list")
|
|
6945
|
-
.description("List recent directional inbox-placement tests with status, billing mode, seed set, score, and credits used.")
|
|
7579
|
+
.description("List recent directional inbox-placement tests with status, health provider, billing mode, seed set, score, and credits used.")
|
|
6946
7580
|
.option("--limit <n>", "Max rows (default 50, cap 200).")
|
|
6947
7581
|
.option("--json", "Print a JSON envelope.")
|
|
6948
7582
|
.action(async (options) => {
|
|
@@ -6952,7 +7586,7 @@ export function createProgram() {
|
|
|
6952
7586
|
});
|
|
6953
7587
|
}))
|
|
6954
7588
|
.addCommand(new Command("get")
|
|
6955
|
-
.description("Get one directional inbox-placement test by id — status, seed inboxes, per-provider results, and score.")
|
|
7589
|
+
.description("Get one directional inbox-placement test by id — status, health provider, seed inboxes, per-provider results, and score. Zapmail results land async (2-24h); keep polling while status is awaiting_results.")
|
|
6956
7590
|
.argument("<id>", "Placement test id.")
|
|
6957
7591
|
.option("--json", "Print a JSON envelope.")
|
|
6958
7592
|
.action(async (id, options) => {
|
|
@@ -7530,8 +8164,7 @@ export function createProgram() {
|
|
|
7530
8164
|
.option("--copy", "Copy skill files instead of symlinking when supported by npx skills. Default on Windows, where symlinks need Developer Mode or admin.")
|
|
7531
8165
|
.option("--json", "Print a JSON envelope.")
|
|
7532
8166
|
.action(async (options) => {
|
|
7533
|
-
|
|
7534
|
-
await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
|
|
8167
|
+
await handleAsyncAction("skills install", options, () => installAgentSkills(options));
|
|
7535
8168
|
}));
|
|
7536
8169
|
return program;
|
|
7537
8170
|
}
|
|
@@ -7592,36 +8225,36 @@ function readBundledSdkDts(pkg) {
|
|
|
7592
8225
|
}
|
|
7593
8226
|
function renderStarterRecipe(id, name) {
|
|
7594
8227
|
return [
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
8228
|
+
'import { defineRecipe, type RecipeContext } from "@oxygen/recipe-sdk";',
|
|
8229
|
+
"",
|
|
8230
|
+
"// A durable recipe is the default export of defineRecipe(...); it compiles into an",
|
|
8231
|
+
"// Oxygen workflow. Recipes reach external systems ONLY through catalog tools —",
|
|
8232
|
+
"// raw fetch/network is disabled in the recipe sandbox.",
|
|
8233
|
+
"//",
|
|
7601
8234
|
`// Deploy: oxygen workflows apply --file ./${id}.ts`,
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
8235
|
+
"// Dry-run: oxygen workflows call <workflow-id> --mode dry_run --json",
|
|
8236
|
+
"// Docs: https://oxygen-agent.com/docs/authoring/recipes",
|
|
8237
|
+
"export default defineRecipe({",
|
|
7605
8238
|
` id: ${JSON.stringify(id)},`,
|
|
7606
8239
|
` name: ${JSON.stringify(name)},`,
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
8240
|
+
" // Allowlist of every tool id this recipe may call — including native table",
|
|
8241
|
+
" // ops, which dispatch through oxygen.* tools (e.g. oxygen.rows_upsert for",
|
|
8242
|
+
" // ctx.rows.upsert). Discover provider tools with: oxygen tools search <query>",
|
|
8243
|
+
' tools: ["firecrawl.scrape", "oxygen.rows_upsert"],',
|
|
8244
|
+
' trigger: { type: "api" },',
|
|
8245
|
+
" inputSchema: {",
|
|
8246
|
+
' type: "object",',
|
|
8247
|
+
" properties: {},",
|
|
8248
|
+
" },",
|
|
8249
|
+
" async run(ctx: RecipeContext) {",
|
|
8250
|
+
' ctx.log("info", "recipe started", { mode: ctx.mode });',
|
|
8251
|
+
" // Example: pull data through a tool, then persist it to a table.",
|
|
8252
|
+
' // const page = await ctx.tools.run("firecrawl.scrape", { url }, { key: "scrape" });',
|
|
8253
|
+
' // await ctx.rows.upsert("my_table", rows, { key: "save", upsertKey: "id" });',
|
|
8254
|
+
" return { ok: true, at: await ctx.now() };",
|
|
8255
|
+
" },",
|
|
8256
|
+
"});",
|
|
8257
|
+
"",
|
|
7625
8258
|
].join("\n");
|
|
7626
8259
|
}
|
|
7627
8260
|
function renderRecipeTsconfig() {
|
|
@@ -7687,7 +8320,7 @@ function scaffoldRecipeProject(options) {
|
|
|
7687
8320
|
next_steps: [
|
|
7688
8321
|
`Edit ${recipeFileName}, then find tools with: oxygen tools search <query>`,
|
|
7689
8322
|
`Deploy it: oxygen workflows apply --file ./${recipeFileName}`,
|
|
7690
|
-
|
|
8323
|
+
"Dry-run it: oxygen workflows call <workflow-id> --mode dry_run --json",
|
|
7691
8324
|
],
|
|
7692
8325
|
};
|
|
7693
8326
|
}
|
|
@@ -9416,7 +10049,7 @@ async function runDomainsDnsPlan(domain, options) {
|
|
|
9416
10049
|
}
|
|
9417
10050
|
// Apply an approved DNS plan (writes records). Requires --plan <hash> with
|
|
9418
10051
|
// --approved, mirroring `domains buy --quote`.
|
|
9419
|
-
|
|
10052
|
+
function runDomainsDnsApply(domain, options) {
|
|
9420
10053
|
const planHash = readOption(options.plan);
|
|
9421
10054
|
if (options.approved && !planHash) {
|
|
9422
10055
|
throw new Error("--plan <hash> is required with --approved. Run `domains dns plan` first to get the plan and its hash.");
|
|
@@ -10513,9 +11146,9 @@ async function handleLogoutAction(options) {
|
|
|
10513
11146
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
10514
11147
|
}
|
|
10515
11148
|
}
|
|
10516
|
-
function handleUpdateAction(options) {
|
|
11149
|
+
async function handleUpdateAction(options) {
|
|
10517
11150
|
try {
|
|
10518
|
-
const result = updateCli(options);
|
|
11151
|
+
const result = await updateCli(options);
|
|
10519
11152
|
if (options.json) {
|
|
10520
11153
|
writeJson(success("update", result));
|
|
10521
11154
|
return;
|
|
@@ -10590,7 +11223,7 @@ async function login(options) {
|
|
|
10590
11223
|
renamed = picked.renamed;
|
|
10591
11224
|
}
|
|
10592
11225
|
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
10593
|
-
const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
|
|
11226
|
+
const skillsInstall = await runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl, credentials });
|
|
10594
11227
|
if (!options.json) {
|
|
10595
11228
|
process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
|
|
10596
11229
|
const hint = await buildPostLoginHint(profile);
|
|
@@ -11968,6 +12601,332 @@ function buildContextAssetUpsertBody(options) {
|
|
|
11968
12601
|
...(options.default ? { is_default: true } : {}),
|
|
11969
12602
|
};
|
|
11970
12603
|
}
|
|
12604
|
+
function buildKnowledgePageUpsertBody(options) {
|
|
12605
|
+
const tags = readCsvOption(options.tags);
|
|
12606
|
+
const expectedRevision = readPositiveInt(options.expectedRevision);
|
|
12607
|
+
return {
|
|
12608
|
+
...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
|
|
12609
|
+
...(readOption(options.id) ? { id: readOption(options.id) } : {}),
|
|
12610
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12611
|
+
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
12612
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12613
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12614
|
+
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
12615
|
+
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
12616
|
+
...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
|
|
12617
|
+
...(expectedRevision !== undefined ? { expected_revision: expectedRevision } : {}),
|
|
12618
|
+
...(options.approved ? { approved: true } : {}),
|
|
12619
|
+
};
|
|
12620
|
+
}
|
|
12621
|
+
function buildKnowledgePageListBody(options) {
|
|
12622
|
+
const tags = readCsvOption(options.tags);
|
|
12623
|
+
const limit = readPositiveInt(options.limit);
|
|
12624
|
+
return {
|
|
12625
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12626
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12627
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12628
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12629
|
+
};
|
|
12630
|
+
}
|
|
12631
|
+
function buildKnowledgeGraphBody(options) {
|
|
12632
|
+
const maxNodes = readPositiveInt(options.maxNodes);
|
|
12633
|
+
return {
|
|
12634
|
+
...(maxNodes !== undefined ? { max_nodes: maxNodes } : {}),
|
|
12635
|
+
};
|
|
12636
|
+
}
|
|
12637
|
+
function buildKnowledgeLogListBody(options) {
|
|
12638
|
+
const limit = readPositiveInt(options.limit);
|
|
12639
|
+
return {
|
|
12640
|
+
...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
|
|
12641
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12642
|
+
};
|
|
12643
|
+
}
|
|
12644
|
+
function buildKnowledgeLogAppendBody(options) {
|
|
12645
|
+
return {
|
|
12646
|
+
...(readOption(options.event) ? { event: readOption(options.event) } : {}),
|
|
12647
|
+
...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
|
|
12648
|
+
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
12649
|
+
...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
|
|
12650
|
+
};
|
|
12651
|
+
}
|
|
12652
|
+
function buildKnowledgeSearchBody(query, options) {
|
|
12653
|
+
const tags = readCsvOption(options.tags);
|
|
12654
|
+
const limit = readPositiveInt(options.limit);
|
|
12655
|
+
return {
|
|
12656
|
+
query: query.trim(),
|
|
12657
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12658
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12659
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12660
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12661
|
+
};
|
|
12662
|
+
}
|
|
12663
|
+
// ── Knowledge mirror (local wiki clone) ─────────────────────────────────────────
|
|
12664
|
+
// `oxygen knowledge sync` maintains <configDir>/knowledge/<api-host>/<org-id>/ with
|
|
12665
|
+
// SERVER-rendered page markdown written verbatim — the server hashes those exact
|
|
12666
|
+
// bytes (content_sha256) and the client never re-renders, so renderer drift can't
|
|
12667
|
+
// mass-quarantine mirrors. Pure directory mechanics live in knowledge-mirror.ts.
|
|
12668
|
+
const KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES = 15;
|
|
12669
|
+
// 500 batches × the server's 100-page default is far above any v1 wiki; the cap
|
|
12670
|
+
// only exists so a misbehaving server can never wedge the CLI in a loop.
|
|
12671
|
+
const KNOWLEDGE_SYNC_MAX_BATCHES = 500;
|
|
12672
|
+
// Envelope error codes meaning this org can no longer be read with the stored
|
|
12673
|
+
// token (revoked membership, expired key, org mismatch). The mirror is a local
|
|
12674
|
+
// cache of tenant data and must not outlive access to it.
|
|
12675
|
+
const KNOWLEDGE_SYNC_ACCESS_REVOKED_CODES = new Set([
|
|
12676
|
+
"unauthorized",
|
|
12677
|
+
"forbidden",
|
|
12678
|
+
"forbidden_origin",
|
|
12679
|
+
"organization_not_found",
|
|
12680
|
+
"organization_mismatch",
|
|
12681
|
+
]);
|
|
12682
|
+
function knowledgeMirrorTargetFor(apiUrl, orgId) {
|
|
12683
|
+
const apiHost = new URL(apiUrl).host;
|
|
12684
|
+
return {
|
|
12685
|
+
dir: resolveMirrorDir({ configDir: resolveDefaultConfigDir(), apiHost, orgId }),
|
|
12686
|
+
apiHost,
|
|
12687
|
+
orgId,
|
|
12688
|
+
webUrl: `${apiUrl.replace(/\/$/, "")}/knowledge`,
|
|
12689
|
+
};
|
|
12690
|
+
}
|
|
12691
|
+
// The org id every other command resolves implicitly (X-Oxygen-Organization from
|
|
12692
|
+
// OXYGEN_ORG / the stored profile) must be pinned explicitly here because it names
|
|
12693
|
+
// the mirror directory. Null when the cached identity can't answer for the
|
|
12694
|
+
// selected org — e.g. env-token logins or an OXYGEN_ORG/--org override.
|
|
12695
|
+
function knowledgeMirrorTargetFromCredentials(credentials) {
|
|
12696
|
+
const cached = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
|
|
12697
|
+
if (!cached)
|
|
12698
|
+
return null;
|
|
12699
|
+
const selected = process.env.OXYGEN_ORG?.trim();
|
|
12700
|
+
if (selected && selected !== cached.id && selected !== cached.slug)
|
|
12701
|
+
return null;
|
|
12702
|
+
return knowledgeMirrorTargetFor(credentials.apiUrl, cached.id);
|
|
12703
|
+
}
|
|
12704
|
+
async function resolveKnowledgeMirrorTarget() {
|
|
12705
|
+
const credentials = await loadCredentials();
|
|
12706
|
+
if (!credentials) {
|
|
12707
|
+
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", {
|
|
12708
|
+
exitCode: 1,
|
|
12709
|
+
});
|
|
12710
|
+
}
|
|
12711
|
+
const local = knowledgeMirrorTargetFromCredentials(credentials);
|
|
12712
|
+
if (local)
|
|
12713
|
+
return local;
|
|
12714
|
+
const identity = await requestOxygen("/api/cli/whoami", {
|
|
12715
|
+
enforceMinimumCliVersion: false,
|
|
12716
|
+
});
|
|
12717
|
+
return knowledgeMirrorTargetFor(credentials.apiUrl, identity.organization.id);
|
|
12718
|
+
}
|
|
12719
|
+
// Post-write staleness hook for `knowledge page upsert|pin|archive`: clearing
|
|
12720
|
+
// last_sync_at makes the next `knowledge sync --if-stale` run for real. A byte-
|
|
12721
|
+
// perfect single-page refresh needs a dedicated sync endpoint (deferred); the
|
|
12722
|
+
// hook is deliberately local-only — no whoami round-trip — and best-effort.
|
|
12723
|
+
async function markKnowledgeMirrorStaleAfterWrite() {
|
|
12724
|
+
try {
|
|
12725
|
+
const credentials = await loadCredentials().catch(() => null);
|
|
12726
|
+
const target = credentials ? knowledgeMirrorTargetFromCredentials(credentials) : null;
|
|
12727
|
+
if (!target || !mirrorExists(target.dir))
|
|
12728
|
+
return;
|
|
12729
|
+
markMirrorStale(target.dir);
|
|
12730
|
+
}
|
|
12731
|
+
catch {
|
|
12732
|
+
// A broken local mirror must never fail the page write itself.
|
|
12733
|
+
}
|
|
12734
|
+
}
|
|
12735
|
+
function knowledgeSyncSummary(target, state, extra) {
|
|
12736
|
+
return {
|
|
12737
|
+
synced: 0,
|
|
12738
|
+
deleted: 0,
|
|
12739
|
+
quarantined: 0,
|
|
12740
|
+
pages_total: state ? Object.keys(state.pages).length : 0,
|
|
12741
|
+
cursor: state?.cursor ?? null,
|
|
12742
|
+
last_sync_at: state?.last_sync_at ?? null,
|
|
12743
|
+
mirror_path: target.dir,
|
|
12744
|
+
web_url: target.webUrl,
|
|
12745
|
+
...extra,
|
|
12746
|
+
};
|
|
12747
|
+
}
|
|
12748
|
+
function applyKnowledgeSyncPage(dir, state, page, counters) {
|
|
12749
|
+
if (page.rendered_markdown === undefined || page.content_sha256 === undefined || !page.slug) {
|
|
12750
|
+
// Tombstone (archived or slug-less): delete the file this page owns locally,
|
|
12751
|
+
// matching by slug first and falling back to the page id (slug re-use).
|
|
12752
|
+
const localSlug = page.slug && state.pages[page.slug]?.id === page.id
|
|
12753
|
+
? page.slug
|
|
12754
|
+
: findMirrorSlugByPageId(state, page.id);
|
|
12755
|
+
if (localSlug) {
|
|
12756
|
+
deletePageFile(dir, localSlug);
|
|
12757
|
+
delete state.pages[localSlug];
|
|
12758
|
+
counters.deleted += 1;
|
|
12759
|
+
}
|
|
12760
|
+
return;
|
|
12761
|
+
}
|
|
12762
|
+
// Slug renames: drop the file this page previously owned under its old slug.
|
|
12763
|
+
const previousSlug = findMirrorSlugByPageId(state, page.id);
|
|
12764
|
+
if (previousSlug && previousSlug !== page.slug) {
|
|
12765
|
+
deletePageFile(dir, previousSlug);
|
|
12766
|
+
delete state.pages[previousSlug];
|
|
12767
|
+
counters.deleted += 1;
|
|
12768
|
+
}
|
|
12769
|
+
// Quarantine only bytes WE didn't write (differ from the manifest's recorded
|
|
12770
|
+
// sha) that this write would destroy (differ from the incoming server bytes).
|
|
12771
|
+
const recordedSha = state.pages[page.slug]?.content_sha256;
|
|
12772
|
+
if (isFileDirty(dir, page.slug, recordedSha ?? page.content_sha256)
|
|
12773
|
+
&& isFileDirty(dir, page.slug, page.content_sha256)) {
|
|
12774
|
+
quarantineDirtyFile(dir, page.slug);
|
|
12775
|
+
counters.quarantined += 1;
|
|
12776
|
+
}
|
|
12777
|
+
writePageFile(dir, page.slug, page.rendered_markdown);
|
|
12778
|
+
state.pages[page.slug] = {
|
|
12779
|
+
id: page.id,
|
|
12780
|
+
revision: page.revision,
|
|
12781
|
+
content_sha256: page.content_sha256,
|
|
12782
|
+
};
|
|
12783
|
+
counters.synced += 1;
|
|
12784
|
+
}
|
|
12785
|
+
async function runKnowledgeMirrorSync(options) {
|
|
12786
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12787
|
+
const ttlMinutes = readPositiveInt(options.ttl) ?? KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES;
|
|
12788
|
+
if (options.ifStale && !options.full) {
|
|
12789
|
+
const state = readMirrorState(target.dir);
|
|
12790
|
+
const lastSyncMs = state?.last_sync_at ? Date.parse(state.last_sync_at) : Number.NaN;
|
|
12791
|
+
if (Number.isFinite(lastSyncMs) && Date.now() - lastSyncMs < ttlMinutes * 60_000) {
|
|
12792
|
+
return knowledgeSyncSummary(target, state, { skipped: true, reason: "fresh" });
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12795
|
+
if (!acquireMirrorLock(target.dir)) {
|
|
12796
|
+
if (options.ifStale) {
|
|
12797
|
+
return knowledgeSyncSummary(target, readMirrorState(target.dir), { skipped: true, reason: "locked" });
|
|
12798
|
+
}
|
|
12799
|
+
throw new OxygenError("knowledge_sync_locked", "Another knowledge sync is already running for this mirror. Retry shortly; a crashed sync's lock self-clears after 10 minutes.", {
|
|
12800
|
+
details: { mirror_path: target.dir },
|
|
12801
|
+
exitCode: 1,
|
|
12802
|
+
});
|
|
12803
|
+
}
|
|
12804
|
+
try {
|
|
12805
|
+
// --full re-clones from scratch but keeps the never-silently-clobber promise:
|
|
12806
|
+
// dirty local files are quarantined into .oxygen/conflicts/ (which survives)
|
|
12807
|
+
// first. Done AFTER acquiring the mirror lock above so a concurrent --full
|
|
12808
|
+
// can't delete files out from under another running sync before locking.
|
|
12809
|
+
const fullResetQuarantined = options.full
|
|
12810
|
+
? resetMirrorForFullResync(target.dir).quarantined
|
|
12811
|
+
: 0;
|
|
12812
|
+
const state = readMirrorState(target.dir)
|
|
12813
|
+
?? emptyMirrorState({ apiHost: target.apiHost, orgId: target.orgId });
|
|
12814
|
+
const counters = { synced: 0, deleted: 0, quarantined: fullResetQuarantined };
|
|
12815
|
+
let webUrl = target.webUrl;
|
|
12816
|
+
let cursor = state.cursor;
|
|
12817
|
+
let lastSeen = null;
|
|
12818
|
+
let completed = false;
|
|
12819
|
+
for (let batch = 0; batch < KNOWLEDGE_SYNC_MAX_BATCHES; batch += 1) {
|
|
12820
|
+
const delta = await requestOxygen("/api/cli/knowledge/sync/delta", {
|
|
12821
|
+
method: "POST",
|
|
12822
|
+
body: cursor ? { cursor } : {},
|
|
12823
|
+
});
|
|
12824
|
+
if (delta.web_url)
|
|
12825
|
+
webUrl = delta.web_url;
|
|
12826
|
+
for (const page of delta.pages) {
|
|
12827
|
+
applyKnowledgeSyncPage(target.dir, state, page, counters);
|
|
12828
|
+
lastSeen = { updated_at: page.updated_at, id: page.id };
|
|
12829
|
+
}
|
|
12830
|
+
if (!delta.has_more) {
|
|
12831
|
+
// Completed. Persist the watermark AFTER the last seen page — the delta
|
|
12832
|
+
// keyset is (updated_at, id) ascending and updated_at is the server's own
|
|
12833
|
+
// ISO rendering, so `<updated_at>|<id>` is exactly the server cursor and
|
|
12834
|
+
// the next sync fetches only pages updated since.
|
|
12835
|
+
if (lastSeen)
|
|
12836
|
+
state.cursor = `${lastSeen.updated_at}|${lastSeen.id}`;
|
|
12837
|
+
completed = true;
|
|
12838
|
+
break;
|
|
12839
|
+
}
|
|
12840
|
+
if (!delta.next_cursor || delta.next_cursor === cursor) {
|
|
12841
|
+
throw new OxygenError("knowledge_sync_stalled", "The sync cursor did not advance between batches; aborting to avoid a loop.", {
|
|
12842
|
+
details: { cursor, mirror_path: target.dir },
|
|
12843
|
+
exitCode: 1,
|
|
12844
|
+
});
|
|
12845
|
+
}
|
|
12846
|
+
cursor = delta.next_cursor;
|
|
12847
|
+
// Persist per batch so an interrupted clone resumes exactly where it stopped.
|
|
12848
|
+
state.cursor = cursor;
|
|
12849
|
+
writeMirrorState(target.dir, state);
|
|
12850
|
+
}
|
|
12851
|
+
if (!completed) {
|
|
12852
|
+
throw new OxygenError("knowledge_sync_incomplete", "The sync batch cap was reached before the wiki finished; re-run `knowledge sync` to resume from the stored cursor.", {
|
|
12853
|
+
details: { cursor: state.cursor, mirror_path: target.dir, batch_cap: KNOWLEDGE_SYNC_MAX_BATCHES },
|
|
12854
|
+
exitCode: 1,
|
|
12855
|
+
});
|
|
12856
|
+
}
|
|
12857
|
+
// Generated summaries come from the server's own projections, not a client-side
|
|
12858
|
+
// recomputation. A real wiki page may own the `index`/`log` slug — page bytes win.
|
|
12859
|
+
const index = await requestOxygen("/api/cli/knowledge/index");
|
|
12860
|
+
const logPage = await requestOxygen("/api/cli/knowledge/log", {
|
|
12861
|
+
method: "POST",
|
|
12862
|
+
body: {},
|
|
12863
|
+
});
|
|
12864
|
+
if (!state.pages.index)
|
|
12865
|
+
writeGeneratedIndexFile(target.dir, index.index);
|
|
12866
|
+
if (!state.pages.log)
|
|
12867
|
+
writeGeneratedLogFile(target.dir, logPage.entries);
|
|
12868
|
+
state.last_sync_at = new Date().toISOString();
|
|
12869
|
+
writeMirrorState(target.dir, state);
|
|
12870
|
+
return knowledgeSyncSummary(target, state, { ...counters, ...(options.full ? { full: true } : {}), web_url: webUrl });
|
|
12871
|
+
}
|
|
12872
|
+
catch (error) {
|
|
12873
|
+
if (error instanceof OxygenError && KNOWLEDGE_SYNC_ACCESS_REVOKED_CODES.has(error.code)) {
|
|
12874
|
+
purgeMirror(target.dir);
|
|
12875
|
+
throw new OxygenError("knowledge_mirror_access_revoked", "This organization can no longer be read with the stored credentials (membership revoked or token expired). The local knowledge mirror was purged.", {
|
|
12876
|
+
details: { org_id: target.orgId, mirror_path: target.dir, cause: error.code },
|
|
12877
|
+
exitCode: 1,
|
|
12878
|
+
});
|
|
12879
|
+
}
|
|
12880
|
+
throw error;
|
|
12881
|
+
}
|
|
12882
|
+
finally {
|
|
12883
|
+
releaseMirrorLock(target.dir);
|
|
12884
|
+
}
|
|
12885
|
+
}
|
|
12886
|
+
async function runKnowledgeMirrorStatus(options) {
|
|
12887
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12888
|
+
const clearedConflicts = options.clearConflicts ? clearConflictFiles(target.dir) : undefined;
|
|
12889
|
+
const state = readMirrorState(target.dir);
|
|
12890
|
+
const conflicts = listConflictFiles(target.dir);
|
|
12891
|
+
return {
|
|
12892
|
+
mirror_path: target.dir,
|
|
12893
|
+
exists: mirrorExists(target.dir),
|
|
12894
|
+
pages_total: state ? Object.keys(state.pages).length : 0,
|
|
12895
|
+
last_sync_at: state?.last_sync_at ?? null,
|
|
12896
|
+
cursor: state?.cursor ?? null,
|
|
12897
|
+
conflicts,
|
|
12898
|
+
conflict_count: conflicts.length,
|
|
12899
|
+
...(clearedConflicts !== undefined ? { cleared_conflicts: clearedConflicts } : {}),
|
|
12900
|
+
web_url: target.webUrl,
|
|
12901
|
+
};
|
|
12902
|
+
}
|
|
12903
|
+
async function runKnowledgeMirrorPurge() {
|
|
12904
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12905
|
+
const existed = existsSync(target.dir);
|
|
12906
|
+
purgeMirror(target.dir);
|
|
12907
|
+
return { purged: existed, mirror_path: target.dir, web_url: target.webUrl };
|
|
12908
|
+
}
|
|
12909
|
+
// `--print-path` emits the bare directory path (for shell substitution and
|
|
12910
|
+
// editor tooling), not a JSON envelope — errors still use the failure envelope.
|
|
12911
|
+
async function handleKnowledgeMirrorPrintPathAction() {
|
|
12912
|
+
try {
|
|
12913
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12914
|
+
process.stdout.write(`${target.dir}\n`);
|
|
12915
|
+
}
|
|
12916
|
+
catch (error) {
|
|
12917
|
+
const failure = toFailure("knowledge status", error);
|
|
12918
|
+
writeJson(failure);
|
|
12919
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
12920
|
+
}
|
|
12921
|
+
}
|
|
12922
|
+
// Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
|
|
12923
|
+
// the single CLI argument the same way the templates commands do.
|
|
12924
|
+
function knowledgePageRefBody(slugOrId) {
|
|
12925
|
+
return isUuid(slugOrId) ? { id: slugOrId } : { slug: slugOrId };
|
|
12926
|
+
}
|
|
12927
|
+
function knowledgeProposalRefBody(idOrSlug) {
|
|
12928
|
+
return isUuid(idOrSlug) ? { proposal_id: idOrSlug } : { slug: idOrSlug };
|
|
12929
|
+
}
|
|
11971
12930
|
function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enrichment aliases, selection, provider order, and safety caps.
|
|
11972
12931
|
table, options) {
|
|
11973
12932
|
const limit = readPositiveInt(options.limit);
|