@oxygen-agent/cli 1.263.0 → 1.272.83
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 +943 -39
- 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/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.")
|
|
@@ -6293,7 +6862,7 @@ export function createProgram() {
|
|
|
6293
6862
|
});
|
|
6294
6863
|
}))
|
|
6295
6864
|
.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.")
|
|
6865
|
+
.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
6866
|
.argument("<sequence>", "Sequence id or slug.")
|
|
6298
6867
|
.option("--name <name>", "New human-readable sequence name.")
|
|
6299
6868
|
.option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey.")
|
|
@@ -6315,6 +6884,10 @@ export function createProgram() {
|
|
|
6315
6884
|
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
6316
6885
|
.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
6886
|
.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.")
|
|
6887
|
+
.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`).")
|
|
6888
|
+
.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).")
|
|
6889
|
+
.option("--tracking-clicks", "Turn click-link tracking ON for this sequence's native email sends (the default; same verified-tracking-domain gate as opens).")
|
|
6890
|
+
.option("--no-tracking-clicks", "Turn OFF click-link rewriting for this sequence's native email sends (links go out untouched).")
|
|
6318
6891
|
.option("--json", "Print a JSON envelope.")
|
|
6319
6892
|
.action(async (sequence, options) => {
|
|
6320
6893
|
await handleAsyncAction("sequences update", options, () => {
|
|
@@ -6341,6 +6914,12 @@ export function createProgram() {
|
|
|
6341
6914
|
const settings = readSequenceSettings(options);
|
|
6342
6915
|
if (settings)
|
|
6343
6916
|
body.settings = settings;
|
|
6917
|
+
// Tri-state tracking toggles: only send the key when a flag was
|
|
6918
|
+
// explicitly passed, so an unrelated update never flips tracking.
|
|
6919
|
+
if (options.trackingOpens !== undefined)
|
|
6920
|
+
body.tracking_opens = options.trackingOpens;
|
|
6921
|
+
if (options.trackingClicks !== undefined)
|
|
6922
|
+
body.tracking_clicks = options.trackingClicks;
|
|
6344
6923
|
if (options.clearEmail) {
|
|
6345
6924
|
body.email = null;
|
|
6346
6925
|
}
|
|
@@ -6350,7 +6929,7 @@ export function createProgram() {
|
|
|
6350
6929
|
body.email = email;
|
|
6351
6930
|
}
|
|
6352
6931
|
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,
|
|
6932
|
+
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
6933
|
}
|
|
6355
6934
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
6356
6935
|
method: "PATCH",
|
|
@@ -6618,7 +7197,7 @@ export function createProgram() {
|
|
|
6618
7197
|
to: readOption(options.to),
|
|
6619
7198
|
subject: readOption(options.subject),
|
|
6620
7199
|
body: bodyText,
|
|
6621
|
-
...(htmlBody
|
|
7200
|
+
...(htmlBody?.trim() ? { html: htmlBody } : {}),
|
|
6622
7201
|
...(readOption(options.displayName) ? { display_name: readOption(options.displayName) } : {}),
|
|
6623
7202
|
...(cc.length > 0 ? { cc } : {}),
|
|
6624
7203
|
...(bcc.length > 0 ? { bcc } : {}),
|
|
@@ -7530,8 +8109,7 @@ export function createProgram() {
|
|
|
7530
8109
|
.option("--copy", "Copy skill files instead of symlinking when supported by npx skills. Default on Windows, where symlinks need Developer Mode or admin.")
|
|
7531
8110
|
.option("--json", "Print a JSON envelope.")
|
|
7532
8111
|
.action(async (options) => {
|
|
7533
|
-
|
|
7534
|
-
await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
|
|
8112
|
+
await handleAsyncAction("skills install", options, () => installAgentSkills(options));
|
|
7535
8113
|
}));
|
|
7536
8114
|
return program;
|
|
7537
8115
|
}
|
|
@@ -7592,36 +8170,36 @@ function readBundledSdkDts(pkg) {
|
|
|
7592
8170
|
}
|
|
7593
8171
|
function renderStarterRecipe(id, name) {
|
|
7594
8172
|
return [
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
8173
|
+
'import { defineRecipe, type RecipeContext } from "@oxygen/recipe-sdk";',
|
|
8174
|
+
"",
|
|
8175
|
+
"// A durable recipe is the default export of defineRecipe(...); it compiles into an",
|
|
8176
|
+
"// Oxygen workflow. Recipes reach external systems ONLY through catalog tools —",
|
|
8177
|
+
"// raw fetch/network is disabled in the recipe sandbox.",
|
|
8178
|
+
"//",
|
|
7601
8179
|
`// Deploy: oxygen workflows apply --file ./${id}.ts`,
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
8180
|
+
"// Dry-run: oxygen workflows call <workflow-id> --mode dry_run --json",
|
|
8181
|
+
"// Docs: https://oxygen-agent.com/docs/authoring/recipes",
|
|
8182
|
+
"export default defineRecipe({",
|
|
7605
8183
|
` id: ${JSON.stringify(id)},`,
|
|
7606
8184
|
` name: ${JSON.stringify(name)},`,
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
8185
|
+
" // Allowlist of every tool id this recipe may call — including native table",
|
|
8186
|
+
" // ops, which dispatch through oxygen.* tools (e.g. oxygen.rows_upsert for",
|
|
8187
|
+
" // ctx.rows.upsert). Discover provider tools with: oxygen tools search <query>",
|
|
8188
|
+
' tools: ["firecrawl.scrape", "oxygen.rows_upsert"],',
|
|
8189
|
+
' trigger: { type: "api" },',
|
|
8190
|
+
" inputSchema: {",
|
|
8191
|
+
' type: "object",',
|
|
8192
|
+
" properties: {},",
|
|
8193
|
+
" },",
|
|
8194
|
+
" async run(ctx: RecipeContext) {",
|
|
8195
|
+
' ctx.log("info", "recipe started", { mode: ctx.mode });',
|
|
8196
|
+
" // Example: pull data through a tool, then persist it to a table.",
|
|
8197
|
+
' // const page = await ctx.tools.run("firecrawl.scrape", { url }, { key: "scrape" });',
|
|
8198
|
+
' // await ctx.rows.upsert("my_table", rows, { key: "save", upsertKey: "id" });',
|
|
8199
|
+
" return { ok: true, at: await ctx.now() };",
|
|
8200
|
+
" },",
|
|
8201
|
+
"});",
|
|
8202
|
+
"",
|
|
7625
8203
|
].join("\n");
|
|
7626
8204
|
}
|
|
7627
8205
|
function renderRecipeTsconfig() {
|
|
@@ -7687,7 +8265,7 @@ function scaffoldRecipeProject(options) {
|
|
|
7687
8265
|
next_steps: [
|
|
7688
8266
|
`Edit ${recipeFileName}, then find tools with: oxygen tools search <query>`,
|
|
7689
8267
|
`Deploy it: oxygen workflows apply --file ./${recipeFileName}`,
|
|
7690
|
-
|
|
8268
|
+
"Dry-run it: oxygen workflows call <workflow-id> --mode dry_run --json",
|
|
7691
8269
|
],
|
|
7692
8270
|
};
|
|
7693
8271
|
}
|
|
@@ -9416,7 +9994,7 @@ async function runDomainsDnsPlan(domain, options) {
|
|
|
9416
9994
|
}
|
|
9417
9995
|
// Apply an approved DNS plan (writes records). Requires --plan <hash> with
|
|
9418
9996
|
// --approved, mirroring `domains buy --quote`.
|
|
9419
|
-
|
|
9997
|
+
function runDomainsDnsApply(domain, options) {
|
|
9420
9998
|
const planHash = readOption(options.plan);
|
|
9421
9999
|
if (options.approved && !planHash) {
|
|
9422
10000
|
throw new Error("--plan <hash> is required with --approved. Run `domains dns plan` first to get the plan and its hash.");
|
|
@@ -10513,9 +11091,9 @@ async function handleLogoutAction(options) {
|
|
|
10513
11091
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
10514
11092
|
}
|
|
10515
11093
|
}
|
|
10516
|
-
function handleUpdateAction(options) {
|
|
11094
|
+
async function handleUpdateAction(options) {
|
|
10517
11095
|
try {
|
|
10518
|
-
const result = updateCli(options);
|
|
11096
|
+
const result = await updateCli(options);
|
|
10519
11097
|
if (options.json) {
|
|
10520
11098
|
writeJson(success("update", result));
|
|
10521
11099
|
return;
|
|
@@ -10590,7 +11168,7 @@ async function login(options) {
|
|
|
10590
11168
|
renamed = picked.renamed;
|
|
10591
11169
|
}
|
|
10592
11170
|
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
10593
|
-
const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
|
|
11171
|
+
const skillsInstall = await runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl, credentials });
|
|
10594
11172
|
if (!options.json) {
|
|
10595
11173
|
process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
|
|
10596
11174
|
const hint = await buildPostLoginHint(profile);
|
|
@@ -11968,6 +12546,332 @@ function buildContextAssetUpsertBody(options) {
|
|
|
11968
12546
|
...(options.default ? { is_default: true } : {}),
|
|
11969
12547
|
};
|
|
11970
12548
|
}
|
|
12549
|
+
function buildKnowledgePageUpsertBody(options) {
|
|
12550
|
+
const tags = readCsvOption(options.tags);
|
|
12551
|
+
const expectedRevision = readPositiveInt(options.expectedRevision);
|
|
12552
|
+
return {
|
|
12553
|
+
...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
|
|
12554
|
+
...(readOption(options.id) ? { id: readOption(options.id) } : {}),
|
|
12555
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12556
|
+
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
12557
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12558
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12559
|
+
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
12560
|
+
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
12561
|
+
...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
|
|
12562
|
+
...(expectedRevision !== undefined ? { expected_revision: expectedRevision } : {}),
|
|
12563
|
+
...(options.approved ? { approved: true } : {}),
|
|
12564
|
+
};
|
|
12565
|
+
}
|
|
12566
|
+
function buildKnowledgePageListBody(options) {
|
|
12567
|
+
const tags = readCsvOption(options.tags);
|
|
12568
|
+
const limit = readPositiveInt(options.limit);
|
|
12569
|
+
return {
|
|
12570
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12571
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12572
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12573
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12574
|
+
};
|
|
12575
|
+
}
|
|
12576
|
+
function buildKnowledgeGraphBody(options) {
|
|
12577
|
+
const maxNodes = readPositiveInt(options.maxNodes);
|
|
12578
|
+
return {
|
|
12579
|
+
...(maxNodes !== undefined ? { max_nodes: maxNodes } : {}),
|
|
12580
|
+
};
|
|
12581
|
+
}
|
|
12582
|
+
function buildKnowledgeLogListBody(options) {
|
|
12583
|
+
const limit = readPositiveInt(options.limit);
|
|
12584
|
+
return {
|
|
12585
|
+
...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
|
|
12586
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12587
|
+
};
|
|
12588
|
+
}
|
|
12589
|
+
function buildKnowledgeLogAppendBody(options) {
|
|
12590
|
+
return {
|
|
12591
|
+
...(readOption(options.event) ? { event: readOption(options.event) } : {}),
|
|
12592
|
+
...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
|
|
12593
|
+
...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
|
|
12594
|
+
...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
|
|
12595
|
+
};
|
|
12596
|
+
}
|
|
12597
|
+
function buildKnowledgeSearchBody(query, options) {
|
|
12598
|
+
const tags = readCsvOption(options.tags);
|
|
12599
|
+
const limit = readPositiveInt(options.limit);
|
|
12600
|
+
return {
|
|
12601
|
+
query: query.trim(),
|
|
12602
|
+
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
12603
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
12604
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
12605
|
+
...(limit !== undefined ? { limit } : {}),
|
|
12606
|
+
};
|
|
12607
|
+
}
|
|
12608
|
+
// ── Knowledge mirror (local wiki clone) ─────────────────────────────────────────
|
|
12609
|
+
// `oxygen knowledge sync` maintains <configDir>/knowledge/<api-host>/<org-id>/ with
|
|
12610
|
+
// SERVER-rendered page markdown written verbatim — the server hashes those exact
|
|
12611
|
+
// bytes (content_sha256) and the client never re-renders, so renderer drift can't
|
|
12612
|
+
// mass-quarantine mirrors. Pure directory mechanics live in knowledge-mirror.ts.
|
|
12613
|
+
const KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES = 15;
|
|
12614
|
+
// 500 batches × the server's 100-page default is far above any v1 wiki; the cap
|
|
12615
|
+
// only exists so a misbehaving server can never wedge the CLI in a loop.
|
|
12616
|
+
const KNOWLEDGE_SYNC_MAX_BATCHES = 500;
|
|
12617
|
+
// Envelope error codes meaning this org can no longer be read with the stored
|
|
12618
|
+
// token (revoked membership, expired key, org mismatch). The mirror is a local
|
|
12619
|
+
// cache of tenant data and must not outlive access to it.
|
|
12620
|
+
const KNOWLEDGE_SYNC_ACCESS_REVOKED_CODES = new Set([
|
|
12621
|
+
"unauthorized",
|
|
12622
|
+
"forbidden",
|
|
12623
|
+
"forbidden_origin",
|
|
12624
|
+
"organization_not_found",
|
|
12625
|
+
"organization_mismatch",
|
|
12626
|
+
]);
|
|
12627
|
+
function knowledgeMirrorTargetFor(apiUrl, orgId) {
|
|
12628
|
+
const apiHost = new URL(apiUrl).host;
|
|
12629
|
+
return {
|
|
12630
|
+
dir: resolveMirrorDir({ configDir: resolveDefaultConfigDir(), apiHost, orgId }),
|
|
12631
|
+
apiHost,
|
|
12632
|
+
orgId,
|
|
12633
|
+
webUrl: `${apiUrl.replace(/\/$/, "")}/knowledge`,
|
|
12634
|
+
};
|
|
12635
|
+
}
|
|
12636
|
+
// The org id every other command resolves implicitly (X-Oxygen-Organization from
|
|
12637
|
+
// OXYGEN_ORG / the stored profile) must be pinned explicitly here because it names
|
|
12638
|
+
// the mirror directory. Null when the cached identity can't answer for the
|
|
12639
|
+
// selected org — e.g. env-token logins or an OXYGEN_ORG/--org override.
|
|
12640
|
+
function knowledgeMirrorTargetFromCredentials(credentials) {
|
|
12641
|
+
const cached = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
|
|
12642
|
+
if (!cached)
|
|
12643
|
+
return null;
|
|
12644
|
+
const selected = process.env.OXYGEN_ORG?.trim();
|
|
12645
|
+
if (selected && selected !== cached.id && selected !== cached.slug)
|
|
12646
|
+
return null;
|
|
12647
|
+
return knowledgeMirrorTargetFor(credentials.apiUrl, cached.id);
|
|
12648
|
+
}
|
|
12649
|
+
async function resolveKnowledgeMirrorTarget() {
|
|
12650
|
+
const credentials = await loadCredentials();
|
|
12651
|
+
if (!credentials) {
|
|
12652
|
+
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", {
|
|
12653
|
+
exitCode: 1,
|
|
12654
|
+
});
|
|
12655
|
+
}
|
|
12656
|
+
const local = knowledgeMirrorTargetFromCredentials(credentials);
|
|
12657
|
+
if (local)
|
|
12658
|
+
return local;
|
|
12659
|
+
const identity = await requestOxygen("/api/cli/whoami", {
|
|
12660
|
+
enforceMinimumCliVersion: false,
|
|
12661
|
+
});
|
|
12662
|
+
return knowledgeMirrorTargetFor(credentials.apiUrl, identity.organization.id);
|
|
12663
|
+
}
|
|
12664
|
+
// Post-write staleness hook for `knowledge page upsert|pin|archive`: clearing
|
|
12665
|
+
// last_sync_at makes the next `knowledge sync --if-stale` run for real. A byte-
|
|
12666
|
+
// perfect single-page refresh needs a dedicated sync endpoint (deferred); the
|
|
12667
|
+
// hook is deliberately local-only — no whoami round-trip — and best-effort.
|
|
12668
|
+
async function markKnowledgeMirrorStaleAfterWrite() {
|
|
12669
|
+
try {
|
|
12670
|
+
const credentials = await loadCredentials().catch(() => null);
|
|
12671
|
+
const target = credentials ? knowledgeMirrorTargetFromCredentials(credentials) : null;
|
|
12672
|
+
if (!target || !mirrorExists(target.dir))
|
|
12673
|
+
return;
|
|
12674
|
+
markMirrorStale(target.dir);
|
|
12675
|
+
}
|
|
12676
|
+
catch {
|
|
12677
|
+
// A broken local mirror must never fail the page write itself.
|
|
12678
|
+
}
|
|
12679
|
+
}
|
|
12680
|
+
function knowledgeSyncSummary(target, state, extra) {
|
|
12681
|
+
return {
|
|
12682
|
+
synced: 0,
|
|
12683
|
+
deleted: 0,
|
|
12684
|
+
quarantined: 0,
|
|
12685
|
+
pages_total: state ? Object.keys(state.pages).length : 0,
|
|
12686
|
+
cursor: state?.cursor ?? null,
|
|
12687
|
+
last_sync_at: state?.last_sync_at ?? null,
|
|
12688
|
+
mirror_path: target.dir,
|
|
12689
|
+
web_url: target.webUrl,
|
|
12690
|
+
...extra,
|
|
12691
|
+
};
|
|
12692
|
+
}
|
|
12693
|
+
function applyKnowledgeSyncPage(dir, state, page, counters) {
|
|
12694
|
+
if (page.rendered_markdown === undefined || page.content_sha256 === undefined || !page.slug) {
|
|
12695
|
+
// Tombstone (archived or slug-less): delete the file this page owns locally,
|
|
12696
|
+
// matching by slug first and falling back to the page id (slug re-use).
|
|
12697
|
+
const localSlug = page.slug && state.pages[page.slug]?.id === page.id
|
|
12698
|
+
? page.slug
|
|
12699
|
+
: findMirrorSlugByPageId(state, page.id);
|
|
12700
|
+
if (localSlug) {
|
|
12701
|
+
deletePageFile(dir, localSlug);
|
|
12702
|
+
delete state.pages[localSlug];
|
|
12703
|
+
counters.deleted += 1;
|
|
12704
|
+
}
|
|
12705
|
+
return;
|
|
12706
|
+
}
|
|
12707
|
+
// Slug renames: drop the file this page previously owned under its old slug.
|
|
12708
|
+
const previousSlug = findMirrorSlugByPageId(state, page.id);
|
|
12709
|
+
if (previousSlug && previousSlug !== page.slug) {
|
|
12710
|
+
deletePageFile(dir, previousSlug);
|
|
12711
|
+
delete state.pages[previousSlug];
|
|
12712
|
+
counters.deleted += 1;
|
|
12713
|
+
}
|
|
12714
|
+
// Quarantine only bytes WE didn't write (differ from the manifest's recorded
|
|
12715
|
+
// sha) that this write would destroy (differ from the incoming server bytes).
|
|
12716
|
+
const recordedSha = state.pages[page.slug]?.content_sha256;
|
|
12717
|
+
if (isFileDirty(dir, page.slug, recordedSha ?? page.content_sha256)
|
|
12718
|
+
&& isFileDirty(dir, page.slug, page.content_sha256)) {
|
|
12719
|
+
quarantineDirtyFile(dir, page.slug);
|
|
12720
|
+
counters.quarantined += 1;
|
|
12721
|
+
}
|
|
12722
|
+
writePageFile(dir, page.slug, page.rendered_markdown);
|
|
12723
|
+
state.pages[page.slug] = {
|
|
12724
|
+
id: page.id,
|
|
12725
|
+
revision: page.revision,
|
|
12726
|
+
content_sha256: page.content_sha256,
|
|
12727
|
+
};
|
|
12728
|
+
counters.synced += 1;
|
|
12729
|
+
}
|
|
12730
|
+
async function runKnowledgeMirrorSync(options) {
|
|
12731
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12732
|
+
const ttlMinutes = readPositiveInt(options.ttl) ?? KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES;
|
|
12733
|
+
if (options.ifStale && !options.full) {
|
|
12734
|
+
const state = readMirrorState(target.dir);
|
|
12735
|
+
const lastSyncMs = state?.last_sync_at ? Date.parse(state.last_sync_at) : Number.NaN;
|
|
12736
|
+
if (Number.isFinite(lastSyncMs) && Date.now() - lastSyncMs < ttlMinutes * 60_000) {
|
|
12737
|
+
return knowledgeSyncSummary(target, state, { skipped: true, reason: "fresh" });
|
|
12738
|
+
}
|
|
12739
|
+
}
|
|
12740
|
+
if (!acquireMirrorLock(target.dir)) {
|
|
12741
|
+
if (options.ifStale) {
|
|
12742
|
+
return knowledgeSyncSummary(target, readMirrorState(target.dir), { skipped: true, reason: "locked" });
|
|
12743
|
+
}
|
|
12744
|
+
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.", {
|
|
12745
|
+
details: { mirror_path: target.dir },
|
|
12746
|
+
exitCode: 1,
|
|
12747
|
+
});
|
|
12748
|
+
}
|
|
12749
|
+
try {
|
|
12750
|
+
// --full re-clones from scratch but keeps the never-silently-clobber promise:
|
|
12751
|
+
// dirty local files are quarantined into .oxygen/conflicts/ (which survives)
|
|
12752
|
+
// first. Done AFTER acquiring the mirror lock above so a concurrent --full
|
|
12753
|
+
// can't delete files out from under another running sync before locking.
|
|
12754
|
+
const fullResetQuarantined = options.full
|
|
12755
|
+
? resetMirrorForFullResync(target.dir).quarantined
|
|
12756
|
+
: 0;
|
|
12757
|
+
const state = readMirrorState(target.dir)
|
|
12758
|
+
?? emptyMirrorState({ apiHost: target.apiHost, orgId: target.orgId });
|
|
12759
|
+
const counters = { synced: 0, deleted: 0, quarantined: fullResetQuarantined };
|
|
12760
|
+
let webUrl = target.webUrl;
|
|
12761
|
+
let cursor = state.cursor;
|
|
12762
|
+
let lastSeen = null;
|
|
12763
|
+
let completed = false;
|
|
12764
|
+
for (let batch = 0; batch < KNOWLEDGE_SYNC_MAX_BATCHES; batch += 1) {
|
|
12765
|
+
const delta = await requestOxygen("/api/cli/knowledge/sync/delta", {
|
|
12766
|
+
method: "POST",
|
|
12767
|
+
body: cursor ? { cursor } : {},
|
|
12768
|
+
});
|
|
12769
|
+
if (delta.web_url)
|
|
12770
|
+
webUrl = delta.web_url;
|
|
12771
|
+
for (const page of delta.pages) {
|
|
12772
|
+
applyKnowledgeSyncPage(target.dir, state, page, counters);
|
|
12773
|
+
lastSeen = { updated_at: page.updated_at, id: page.id };
|
|
12774
|
+
}
|
|
12775
|
+
if (!delta.has_more) {
|
|
12776
|
+
// Completed. Persist the watermark AFTER the last seen page — the delta
|
|
12777
|
+
// keyset is (updated_at, id) ascending and updated_at is the server's own
|
|
12778
|
+
// ISO rendering, so `<updated_at>|<id>` is exactly the server cursor and
|
|
12779
|
+
// the next sync fetches only pages updated since.
|
|
12780
|
+
if (lastSeen)
|
|
12781
|
+
state.cursor = `${lastSeen.updated_at}|${lastSeen.id}`;
|
|
12782
|
+
completed = true;
|
|
12783
|
+
break;
|
|
12784
|
+
}
|
|
12785
|
+
if (!delta.next_cursor || delta.next_cursor === cursor) {
|
|
12786
|
+
throw new OxygenError("knowledge_sync_stalled", "The sync cursor did not advance between batches; aborting to avoid a loop.", {
|
|
12787
|
+
details: { cursor, mirror_path: target.dir },
|
|
12788
|
+
exitCode: 1,
|
|
12789
|
+
});
|
|
12790
|
+
}
|
|
12791
|
+
cursor = delta.next_cursor;
|
|
12792
|
+
// Persist per batch so an interrupted clone resumes exactly where it stopped.
|
|
12793
|
+
state.cursor = cursor;
|
|
12794
|
+
writeMirrorState(target.dir, state);
|
|
12795
|
+
}
|
|
12796
|
+
if (!completed) {
|
|
12797
|
+
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.", {
|
|
12798
|
+
details: { cursor: state.cursor, mirror_path: target.dir, batch_cap: KNOWLEDGE_SYNC_MAX_BATCHES },
|
|
12799
|
+
exitCode: 1,
|
|
12800
|
+
});
|
|
12801
|
+
}
|
|
12802
|
+
// Generated summaries come from the server's own projections, not a client-side
|
|
12803
|
+
// recomputation. A real wiki page may own the `index`/`log` slug — page bytes win.
|
|
12804
|
+
const index = await requestOxygen("/api/cli/knowledge/index");
|
|
12805
|
+
const logPage = await requestOxygen("/api/cli/knowledge/log", {
|
|
12806
|
+
method: "POST",
|
|
12807
|
+
body: {},
|
|
12808
|
+
});
|
|
12809
|
+
if (!state.pages.index)
|
|
12810
|
+
writeGeneratedIndexFile(target.dir, index.index);
|
|
12811
|
+
if (!state.pages.log)
|
|
12812
|
+
writeGeneratedLogFile(target.dir, logPage.entries);
|
|
12813
|
+
state.last_sync_at = new Date().toISOString();
|
|
12814
|
+
writeMirrorState(target.dir, state);
|
|
12815
|
+
return knowledgeSyncSummary(target, state, { ...counters, ...(options.full ? { full: true } : {}), web_url: webUrl });
|
|
12816
|
+
}
|
|
12817
|
+
catch (error) {
|
|
12818
|
+
if (error instanceof OxygenError && KNOWLEDGE_SYNC_ACCESS_REVOKED_CODES.has(error.code)) {
|
|
12819
|
+
purgeMirror(target.dir);
|
|
12820
|
+
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.", {
|
|
12821
|
+
details: { org_id: target.orgId, mirror_path: target.dir, cause: error.code },
|
|
12822
|
+
exitCode: 1,
|
|
12823
|
+
});
|
|
12824
|
+
}
|
|
12825
|
+
throw error;
|
|
12826
|
+
}
|
|
12827
|
+
finally {
|
|
12828
|
+
releaseMirrorLock(target.dir);
|
|
12829
|
+
}
|
|
12830
|
+
}
|
|
12831
|
+
async function runKnowledgeMirrorStatus(options) {
|
|
12832
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12833
|
+
const clearedConflicts = options.clearConflicts ? clearConflictFiles(target.dir) : undefined;
|
|
12834
|
+
const state = readMirrorState(target.dir);
|
|
12835
|
+
const conflicts = listConflictFiles(target.dir);
|
|
12836
|
+
return {
|
|
12837
|
+
mirror_path: target.dir,
|
|
12838
|
+
exists: mirrorExists(target.dir),
|
|
12839
|
+
pages_total: state ? Object.keys(state.pages).length : 0,
|
|
12840
|
+
last_sync_at: state?.last_sync_at ?? null,
|
|
12841
|
+
cursor: state?.cursor ?? null,
|
|
12842
|
+
conflicts,
|
|
12843
|
+
conflict_count: conflicts.length,
|
|
12844
|
+
...(clearedConflicts !== undefined ? { cleared_conflicts: clearedConflicts } : {}),
|
|
12845
|
+
web_url: target.webUrl,
|
|
12846
|
+
};
|
|
12847
|
+
}
|
|
12848
|
+
async function runKnowledgeMirrorPurge() {
|
|
12849
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12850
|
+
const existed = existsSync(target.dir);
|
|
12851
|
+
purgeMirror(target.dir);
|
|
12852
|
+
return { purged: existed, mirror_path: target.dir, web_url: target.webUrl };
|
|
12853
|
+
}
|
|
12854
|
+
// `--print-path` emits the bare directory path (for shell substitution and
|
|
12855
|
+
// editor tooling), not a JSON envelope — errors still use the failure envelope.
|
|
12856
|
+
async function handleKnowledgeMirrorPrintPathAction() {
|
|
12857
|
+
try {
|
|
12858
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
12859
|
+
process.stdout.write(`${target.dir}\n`);
|
|
12860
|
+
}
|
|
12861
|
+
catch (error) {
|
|
12862
|
+
const failure = toFailure("knowledge status", error);
|
|
12863
|
+
writeJson(failure);
|
|
12864
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
12865
|
+
}
|
|
12866
|
+
}
|
|
12867
|
+
// Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
|
|
12868
|
+
// the single CLI argument the same way the templates commands do.
|
|
12869
|
+
function knowledgePageRefBody(slugOrId) {
|
|
12870
|
+
return isUuid(slugOrId) ? { id: slugOrId } : { slug: slugOrId };
|
|
12871
|
+
}
|
|
12872
|
+
function knowledgeProposalRefBody(idOrSlug) {
|
|
12873
|
+
return isUuid(idOrSlug) ? { proposal_id: idOrSlug } : { slug: idOrSlug };
|
|
12874
|
+
}
|
|
11971
12875
|
function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enrichment aliases, selection, provider order, and safety caps.
|
|
11972
12876
|
table, options) {
|
|
11973
12877
|
const limit = readPositiveInt(options.limit);
|