@oxygen-agent/cli 1.354.0 → 1.377.3
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/command-manifest.js +12 -1
- package/dist/help.js +6 -5
- package/dist/index.js +692 -160
- package/node_modules/@oxygen/shared/dist/billing.d.ts +34 -10
- package/node_modules/@oxygen/shared/dist/billing.js +86 -11
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.js +36 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +212 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/index.js +9 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/langfuse.js +231 -0
- package/node_modules/@oxygen/shared/dist/linkedin-mentions.d.ts +37 -0
- package/node_modules/@oxygen/shared/dist/linkedin-mentions.js +157 -0
- package/node_modules/@oxygen/shared/dist/log.js +15 -2
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +5 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-agents.d.ts +8 -7
- package/node_modules/@oxygen/shared/dist/workspace-agents.js +34 -7
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { Command, Option } from "commander";
|
|
9
|
+
import { Command, CommanderError, Option } from "commander";
|
|
10
10
|
import { applyOxygenHelp } from "./help.js";
|
|
11
11
|
import { buildCommandManifest } from "./command-manifest.js";
|
|
12
12
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, sleep, success, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
@@ -95,6 +95,10 @@ const TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
|
|
|
95
95
|
const TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
|
|
96
96
|
const WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS = 600;
|
|
97
97
|
const WORKFLOW_TAIL_DEFAULT_INTERVAL_SECONDS = 2;
|
|
98
|
+
// `copilot send` streams a single turn: a shorter default deadline than a batch
|
|
99
|
+
// workflow tail, polled every couple of seconds so assistant text feels live.
|
|
100
|
+
const COPILOT_SEND_DEFAULT_TIMEOUT_SECONDS = 300;
|
|
101
|
+
const COPILOT_SEND_DEFAULT_INTERVAL_SECONDS = 2;
|
|
98
102
|
// Cloudflare domain sync returns partial pages under the request budget; the
|
|
99
103
|
// CLI auto-continues up to this many follow-up POSTs before handing back a
|
|
100
104
|
// still-partial summary.
|
|
@@ -632,11 +636,12 @@ function resolveComposioRunMode(options) {
|
|
|
632
636
|
}
|
|
633
637
|
return "dry_run";
|
|
634
638
|
}
|
|
635
|
-
const DEFAULT_CRM_SETUP_OBJECTS = ["companies", "people"];
|
|
639
|
+
const DEFAULT_CRM_SETUP_OBJECTS = ["companies", "people", "deals"];
|
|
636
640
|
function buildCrmSetupBody(options) {
|
|
637
641
|
return {
|
|
638
642
|
objects: readCrmSetupObjects(options.objects),
|
|
639
643
|
mode: resolveLiveDryRunMode(options),
|
|
644
|
+
...(options.withEnrichment === true ? { with_enrichment: true } : {}),
|
|
640
645
|
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
641
646
|
};
|
|
642
647
|
}
|
|
@@ -819,8 +824,7 @@ function buildPublishingPostsListPath(options) {
|
|
|
819
824
|
const filters = {
|
|
820
825
|
status: options.status,
|
|
821
826
|
approval_status: options.approvalStatus,
|
|
822
|
-
|
|
823
|
-
campaign_id: options.campaign,
|
|
827
|
+
tag: options.tag,
|
|
824
828
|
provider: options.provider,
|
|
825
829
|
limit: options.limit,
|
|
826
830
|
};
|
|
@@ -844,8 +848,18 @@ function buildPublishingMentionsResolveBody(options) {
|
|
|
844
848
|
if (!text && identifiers.length === 0) {
|
|
845
849
|
throw new OxygenError("invalid_request", "Pass --text/--text-file or at least one --identifier.", { exitCode: 1 });
|
|
846
850
|
}
|
|
851
|
+
const source = readOption(options.source);
|
|
852
|
+
if (source && source !== "sender" && source !== "scraper") {
|
|
853
|
+
throw new OxygenError("invalid_request", "--source must be sender or scraper.", { exitCode: 1 });
|
|
854
|
+
}
|
|
855
|
+
if ((source ?? "sender") === "sender" && !readOption(options.account)) {
|
|
856
|
+
throw new OxygenError("invalid_request", "--account is required when resolving through a sender.", { exitCode: 1 });
|
|
857
|
+
}
|
|
847
858
|
return {
|
|
848
|
-
account: options.account,
|
|
859
|
+
...(options.account ? { account: options.account } : {}),
|
|
860
|
+
...(source ? { source } : {}),
|
|
861
|
+
...(options.approved ? { approved: true } : {}),
|
|
862
|
+
...(options.maxCredits ? { max_credits: Number(options.maxCredits) } : {}),
|
|
849
863
|
...(text ? { text } : {}),
|
|
850
864
|
...(identifiers.length > 0 ? {
|
|
851
865
|
identifiers: identifiers.map((value) => ({
|
|
@@ -1155,22 +1169,7 @@ function buildPublishingDraftsAcceptBody(options) {
|
|
|
1155
1169
|
body.timezone = timezone;
|
|
1156
1170
|
return body;
|
|
1157
1171
|
}
|
|
1158
|
-
function
|
|
1159
|
-
const name = readOption(options.name);
|
|
1160
|
-
if (requireName && !name) {
|
|
1161
|
-
throw new OxygenError("invalid_request", "Pass --name.", { exitCode: 1 });
|
|
1162
|
-
}
|
|
1163
|
-
const goal = readOption(options.goal);
|
|
1164
|
-
const startsAt = readOption(options.startsAt);
|
|
1165
|
-
const endsAt = readOption(options.endsAt);
|
|
1166
|
-
return {
|
|
1167
|
-
...(name ? { name } : {}),
|
|
1168
|
-
...(goal ? { goal } : {}),
|
|
1169
|
-
...(startsAt ? { starts_at: startsAt } : {}),
|
|
1170
|
-
...(endsAt ? { ends_at: endsAt } : {}),
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
function buildPublishingLabelsBody(options) {
|
|
1172
|
+
function buildPublishingTagsBody(options) {
|
|
1174
1173
|
const add = splitCommaList(options.add);
|
|
1175
1174
|
const remove = splitCommaList(options.remove);
|
|
1176
1175
|
if (add.length === 0 && remove.length === 0) {
|
|
@@ -1209,14 +1208,12 @@ function buildPublishingImportBody(options) {
|
|
|
1209
1208
|
throw new OxygenError("conflicting_flags", "Pass either --dry-run or --approved, not both.", { exitCode: 1 });
|
|
1210
1209
|
}
|
|
1211
1210
|
const contents = readPublishingTextFile(file, "--file");
|
|
1212
|
-
const
|
|
1213
|
-
const labels = splitCommaList(options.label);
|
|
1211
|
+
const tags = splitCommaList(options.tags);
|
|
1214
1212
|
const isJson = file.trim().toLowerCase().endsWith(".json");
|
|
1215
1213
|
return {
|
|
1216
1214
|
...(isJson ? { rows: readPublishingImportRows(contents) } : { csv: contents }),
|
|
1217
1215
|
dry_run: options.approved !== true,
|
|
1218
|
-
...(
|
|
1219
|
-
...(labels.length > 0 ? { labels } : {}),
|
|
1216
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
1220
1217
|
};
|
|
1221
1218
|
}
|
|
1222
1219
|
function readPublishingImportRows(contents) {
|
|
@@ -1269,12 +1266,10 @@ function buildPublishingAmplificationCreateBody(options) {
|
|
|
1269
1266
|
};
|
|
1270
1267
|
}
|
|
1271
1268
|
function publishingAmplificationScope(options) {
|
|
1272
|
-
const
|
|
1273
|
-
const label = readOption(options.label);
|
|
1269
|
+
const tag = readOption(options.tag);
|
|
1274
1270
|
const post = readOption(options.post);
|
|
1275
1271
|
return {
|
|
1276
|
-
...(
|
|
1277
|
-
...(label ? { scope_label: label } : {}),
|
|
1272
|
+
...(tag ? { scope_tag: tag } : {}),
|
|
1278
1273
|
...(post ? { scope_post_id: post } : {}),
|
|
1279
1274
|
};
|
|
1280
1275
|
}
|
|
@@ -2314,8 +2309,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2314
2309
|
.addCommand(new Command("mentions")
|
|
2315
2310
|
.description("Resolve LinkedIn identities for a publish-faithful post preview.")
|
|
2316
2311
|
.addCommand(new Command("resolve")
|
|
2317
|
-
.description("Resolve @handles, LinkedIn profile/company links, provider ids, and URNs through a connected sender (notify=false).")
|
|
2318
|
-
.
|
|
2312
|
+
.description("Resolve @handles, LinkedIn profile/company links, provider ids, and URNs — through a connected sender (notify=false, free) or the managed scraper (--source scraper, paid, no sender/window needed).")
|
|
2313
|
+
.option("--account <account>", "LinkedIn sender id, connection id, or Unipile account id. Required unless --source scraper.")
|
|
2314
|
+
.option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup, ~10 credits per identity, needs --approved --max-credits).")
|
|
2315
|
+
.option("--approved", "Approve the paid scraper lookups (required with --source scraper).")
|
|
2316
|
+
.option("--max-credits <n>", "Credit ceiling for scraper lookups (required with --source scraper).")
|
|
2319
2317
|
.option("--text <text>", "Post text containing @identifiers or LinkedIn URLs.")
|
|
2320
2318
|
.option("--text-file <path>", "Read post text from a local file.")
|
|
2321
2319
|
.option("--identifier <value...>", "Explicit provider id, public identifier, LinkedIn URL, or URN (repeat values after the flag).")
|
|
@@ -2334,8 +2332,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2334
2332
|
.description("List scheduled posts.")
|
|
2335
2333
|
.option("--status <status>", "Filter by draft, scheduled, queued, publishing, published, failed, or canceled.")
|
|
2336
2334
|
.option("--approval-status <status>", "Filter by draft, needs_approval, approved, or rejected.")
|
|
2337
|
-
.option("--
|
|
2338
|
-
.option("--campaign <campaign_id>", "Only posts in this campaign.")
|
|
2335
|
+
.option("--tag <tag>", "Only posts carrying this workspace tag.")
|
|
2339
2336
|
.option("--provider <provider>", "Filter by provider: linkedin, x, instagram, tiktok, facebook, or youtube.")
|
|
2340
2337
|
.option("--limit <n>", "Maximum posts to return.")
|
|
2341
2338
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2612,69 +2609,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2612
2609
|
body: buildPublishingMediaUploadedBody(options),
|
|
2613
2610
|
}));
|
|
2614
2611
|
})))
|
|
2615
|
-
.addCommand(new Command("
|
|
2616
|
-
.description("
|
|
2617
|
-
.addCommand(new Command("list")
|
|
2618
|
-
.description("List campaigns with their post counts.")
|
|
2619
|
-
.option("--json", "Print a JSON envelope.")
|
|
2620
|
-
.action(async (options) => {
|
|
2621
|
-
await handleAsyncAction("publishing campaigns list", options, () => requestOxygen("/api/cli/publishing/campaigns"));
|
|
2622
|
-
}))
|
|
2623
|
-
.addCommand(new Command("create")
|
|
2624
|
-
.description("Create a campaign.")
|
|
2625
|
-
.requiredOption("--name <name>", "Campaign name.")
|
|
2626
|
-
.option("--goal <goal>", "What this campaign is for.")
|
|
2627
|
-
.option("--starts-at <iso>", "ISO date-time the campaign starts.")
|
|
2628
|
-
.option("--ends-at <iso>", "ISO date-time the campaign ends.")
|
|
2629
|
-
.option("--json", "Print a JSON envelope.")
|
|
2630
|
-
.action(async (options) => {
|
|
2631
|
-
await handleAsyncAction("publishing campaigns create", options, () => requestOxygen("/api/cli/publishing/campaigns", {
|
|
2632
|
-
method: "POST",
|
|
2633
|
-
body: buildPublishingCampaignBody(options, true),
|
|
2634
|
-
}));
|
|
2635
|
-
}))
|
|
2636
|
-
.addCommand(new Command("get")
|
|
2637
|
-
.description("Get one campaign with its posts.")
|
|
2638
|
-
.argument("<campaign_id>", "Campaign id.")
|
|
2639
|
-
.option("--json", "Print a JSON envelope.")
|
|
2640
|
-
.action(async (campaignId, options) => {
|
|
2641
|
-
await handleAsyncAction("publishing campaigns get", options, () => requestOxygen(`/api/cli/publishing/campaigns/${encodeURIComponent(campaignId)}`));
|
|
2642
|
-
}))
|
|
2643
|
-
.addCommand(new Command("update")
|
|
2644
|
-
.description("Update a campaign's name, goal, or window.")
|
|
2645
|
-
.argument("<campaign_id>", "Campaign id.")
|
|
2646
|
-
.option("--name <name>", "Campaign name.")
|
|
2647
|
-
.option("--goal <goal>", "What this campaign is for.")
|
|
2648
|
-
.option("--starts-at <iso>", "ISO date-time the campaign starts.")
|
|
2649
|
-
.option("--ends-at <iso>", "ISO date-time the campaign ends.")
|
|
2650
|
-
.option("--json", "Print a JSON envelope.")
|
|
2651
|
-
.action(async (campaignId, options) => {
|
|
2652
|
-
await handleAsyncAction("publishing campaigns update", options, () => requestOxygen(`/api/cli/publishing/campaigns/${encodeURIComponent(campaignId)}`, {
|
|
2653
|
-
method: "PATCH",
|
|
2654
|
-
body: buildPublishingCampaignBody(options, false),
|
|
2655
|
-
}));
|
|
2656
|
-
}))
|
|
2657
|
-
.addCommand(new Command("delete")
|
|
2658
|
-
.description("Delete a campaign. Its posts survive — they are just unlinked from it.")
|
|
2659
|
-
.argument("<campaign_id>", "Campaign id.")
|
|
2660
|
-
.option("--json", "Print a JSON envelope.")
|
|
2661
|
-
.action(async (campaignId, options) => {
|
|
2662
|
-
await handleAsyncAction("publishing campaigns delete", options, () => requestOxygen(`/api/cli/publishing/campaigns/${encodeURIComponent(campaignId)}`, {
|
|
2663
|
-
method: "DELETE",
|
|
2664
|
-
}));
|
|
2665
|
-
})))
|
|
2666
|
-
.addCommand(new Command("labels")
|
|
2667
|
-
.description("Tag scheduled posts. Labels drive queue filters and label-scoped amplification; they are never published.")
|
|
2612
|
+
.addCommand(new Command("tags")
|
|
2613
|
+
.description("Apply unified workspace tags to scheduled posts; tags are never published.")
|
|
2668
2614
|
.addCommand(new Command("set")
|
|
2669
|
-
.description("Add and/or remove
|
|
2615
|
+
.description("Add and/or remove workspace tags on a post.")
|
|
2670
2616
|
.argument("<post_id>", "Scheduled post id.")
|
|
2671
|
-
.option("--add <
|
|
2672
|
-
.option("--remove <
|
|
2617
|
+
.option("--add <tags>", "Comma-separated workspace tags to add.")
|
|
2618
|
+
.option("--remove <tags>", "Comma-separated workspace tags to remove.")
|
|
2673
2619
|
.option("--json", "Print a JSON envelope.")
|
|
2674
2620
|
.action(async (postId, options) => {
|
|
2675
|
-
await handleAsyncAction("publishing
|
|
2621
|
+
await handleAsyncAction("publishing tags set", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/tags`, {
|
|
2676
2622
|
method: "POST",
|
|
2677
|
-
body:
|
|
2623
|
+
body: buildPublishingTagsBody(options),
|
|
2678
2624
|
}));
|
|
2679
2625
|
})))
|
|
2680
2626
|
.addCommand(new Command("analytics")
|
|
@@ -2713,8 +2659,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2713
2659
|
.requiredOption("--file <path>", "Path to a .csv (header row) or .json file ([rows] or { rows: [...] }).")
|
|
2714
2660
|
.option("--dry-run", "Parse, lint, and report without writing. Default.")
|
|
2715
2661
|
.option("--approved", "Write the parsed rows into the queue as needs-approval drafts.")
|
|
2716
|
-
.option("--
|
|
2717
|
-
.option("--label <labels>", "Comma-separated labels for every imported post.")
|
|
2662
|
+
.option("--tags <tags>", "Comma-separated workspace tags for every imported post.")
|
|
2718
2663
|
.option("--json", "Print a JSON envelope.")
|
|
2719
2664
|
.action(async (options) => {
|
|
2720
2665
|
await handleAsyncAction("publishing import", options, () => requestOxygen("/api/cli/publishing/import", {
|
|
@@ -2733,9 +2678,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2733
2678
|
.addCommand(new Command("create")
|
|
2734
2679
|
.description("Create an amplification policy. It arms REAL public engagement from other people's connected accounts and SPENDS CREDITS, so it is refused (exit 7) without --approved and --max-credits — that pair is the standing grant, recorded with who/when/what scope. The policy is created DISABLED: run `amplification enable` when you actually want it to run.")
|
|
2735
2680
|
.requiredOption("--name <name>", "Policy name.")
|
|
2736
|
-
.requiredOption("--scope <kind>", "What it amplifies: all,
|
|
2737
|
-
.option("--
|
|
2738
|
-
.option("--label <label>", "Label to scope to (with --scope label).")
|
|
2681
|
+
.requiredOption("--scope <kind>", "What it amplifies: all, tag, or post.")
|
|
2682
|
+
.option("--tag <tag>", "Workspace tag to scope to (with --scope tag).")
|
|
2739
2683
|
.option("--post <post_id>", "Post to scope to (with --scope post).")
|
|
2740
2684
|
.requiredOption("--senders <ids>", "Comma-separated connected sender account ids that will engage.")
|
|
2741
2685
|
.requiredOption("--actions <list>", "Comma-separated actions: reaction, comment.")
|
|
@@ -2878,8 +2822,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2878
2822
|
.description("Agent-native CRM object setup and metadata commands.")
|
|
2879
2823
|
.addCommand(new Command("setup")
|
|
2880
2824
|
.description("Create or repair standard CRM object-backed tables. Defaults to dry-run.")
|
|
2881
|
-
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people.")
|
|
2825
|
+
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
2882
2826
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
2827
|
+
.option("--with-enrichment", "Also arm the standing cheap-enrichment auto-run on objects that already existed (fresh-created tables arm it by default). New rows then auto-enrich within the per-batch credit cap.")
|
|
2883
2828
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
2884
2829
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
2885
2830
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2951,7 +2896,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2951
2896
|
}));
|
|
2952
2897
|
}))
|
|
2953
2898
|
.addCommand(new Command("assert")
|
|
2954
|
-
.description("Create or update one CRM record by object identity. Defaults to dry-run.")
|
|
2899
|
+
.description("Create or update one CRM record by object identity. Defaults to dry-run. Note: the table's standing auto-run (auto-enrichment) fires on `tables insert|upsert|import`, not on assert — after asserting new records, run the enrichment columns (`oxygen columns run <table> <column>`) or insert via `oxygen tables insert` when you want them enriched hands-free.")
|
|
2955
2900
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
2956
2901
|
.requiredOption("--identity <key=value>", "Identity key/value, for example domain=acme.com or email=ceo@acme.com.")
|
|
2957
2902
|
.option("--values-json <json>", "JSON object of CRM attribute values keyed by column key.")
|
|
@@ -2971,6 +2916,29 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2971
2916
|
.option("--json", "Print a JSON envelope.")
|
|
2972
2917
|
.action(async (object, rowId, options) => {
|
|
2973
2918
|
await handleAsyncAction("crm get", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}`));
|
|
2919
|
+
}))
|
|
2920
|
+
.addCommand(new Command("tag")
|
|
2921
|
+
.description("Add/remove workspace tags on one CRM record (Tags primitive — the same vocabulary as sequences, tables, conversations; see `oxygen tags list`). Delta semantics: --add unions, --remove subtracts, remove wins. Standard objects carry the tags attribute after `oxygen crm setup`.")
|
|
2922
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
2923
|
+
.argument("<row_id>", "CRM record row id.")
|
|
2924
|
+
.option("--add <tags>", "Comma-separated tags to add.")
|
|
2925
|
+
.option("--remove <tags>", "Comma-separated tags to remove.")
|
|
2926
|
+
.option("--json", "Print a JSON envelope.")
|
|
2927
|
+
.action(async (object, rowId, options) => {
|
|
2928
|
+
await handleAsyncAction("crm tag", options, () => {
|
|
2929
|
+
const add = splitCommaList(options.add);
|
|
2930
|
+
const remove = splitCommaList(options.remove);
|
|
2931
|
+
if (add.length === 0 && remove.length === 0) {
|
|
2932
|
+
throw new OxygenError("invalid_request", "Pass --add and/or --remove.", { exitCode: 1 });
|
|
2933
|
+
}
|
|
2934
|
+
return requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/tags`, {
|
|
2935
|
+
method: "POST",
|
|
2936
|
+
body: {
|
|
2937
|
+
...(add.length > 0 ? { add } : {}),
|
|
2938
|
+
...(remove.length > 0 ? { remove } : {}),
|
|
2939
|
+
},
|
|
2940
|
+
});
|
|
2941
|
+
});
|
|
2974
2942
|
}))
|
|
2975
2943
|
.addCommand(new Command("relationships")
|
|
2976
2944
|
.description("Define CRM relationships and manage record relationship edges.")
|
|
@@ -3490,11 +3458,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3490
3458
|
.addCommand(new Command("list")
|
|
3491
3459
|
.description("List workspace tables in the current tenant database.")
|
|
3492
3460
|
.option("--project <project>", "Project id or slug to filter by.")
|
|
3461
|
+
.option("--tag <tag>", "Only tables carrying this workspace tag (see `oxygen tags list`).")
|
|
3493
3462
|
.option("--json", "Print a JSON envelope.")
|
|
3494
3463
|
.action(async (options) => {
|
|
3495
|
-
await handleAsyncAction("tables list", options, () =>
|
|
3496
|
-
|
|
3497
|
-
|
|
3464
|
+
await handleAsyncAction("tables list", options, () => {
|
|
3465
|
+
const params = new URLSearchParams();
|
|
3466
|
+
const project = readOption(options.project);
|
|
3467
|
+
if (project)
|
|
3468
|
+
params.set("project", project);
|
|
3469
|
+
const tag = readOption(options.tag);
|
|
3470
|
+
if (tag)
|
|
3471
|
+
params.set("tag", tag);
|
|
3472
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
3473
|
+
return requestOxygen(`/api/cli/tables${qs}`);
|
|
3474
|
+
});
|
|
3498
3475
|
}))
|
|
3499
3476
|
.addCommand(new Command("query")
|
|
3500
3477
|
.description("Query a workspace table by id or slug.")
|
|
@@ -4560,7 +4537,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4560
4537
|
}));
|
|
4561
4538
|
program
|
|
4562
4539
|
.command("tags")
|
|
4563
|
-
.description("Workspace tags: one
|
|
4540
|
+
.description("Workspace tags: one vocabulary across publishing posts, knowledge pages, sequences, tables, workflows, recipes, and inbox conversations. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters). Write tags on the owning surface (e.g. `oxygen inbox tag`, `oxygen tables tag`), then `tags get <tag>` returns every carrier with deep-links.")
|
|
4564
4541
|
.addCommand(new Command("list")
|
|
4565
4542
|
.description("List every workspace tag with per-primitive counts, most-used first.")
|
|
4566
4543
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4568,11 +4545,23 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4568
4545
|
await handleAsyncAction("tags list", options, () => requestOxygen("/api/cli/tags"));
|
|
4569
4546
|
}))
|
|
4570
4547
|
.addCommand(new Command("get")
|
|
4571
|
-
.description("Everything carrying one tag — knowledge pages, sequences, tables, workflows, recipes — each with its deep-link.")
|
|
4548
|
+
.description("Everything carrying one tag — publishing posts, knowledge pages, sequences, tables, workflows, recipes, and inbox conversations — each with its deep-link.")
|
|
4572
4549
|
.argument("<tag>", "The tag (case-insensitive).")
|
|
4573
4550
|
.option("--json", "Print a JSON envelope.")
|
|
4574
4551
|
.action(async (tag, options) => {
|
|
4575
4552
|
await handleAsyncAction("tags get", options, () => requestOxygen(`/api/cli/tags/${encodeURIComponent(tag)}`));
|
|
4553
|
+
}))
|
|
4554
|
+
.addCommand(new Command("rename")
|
|
4555
|
+
.description("Rename one tag EVERYWHERE it appears (every primitive, archived rows included). Preview-first: without --apply, prints per-kind would-change counts and mutates nothing. Idempotent — safe to re-run after a partial failure.")
|
|
4556
|
+
.argument("<from>", "The current tag (case-insensitive).")
|
|
4557
|
+
.argument("<to>", "The new tag (normalized lowercase).")
|
|
4558
|
+
.option("--apply", "Execute the rename. Without this flag, returns a preview of the counts only.")
|
|
4559
|
+
.option("--json", "Print a JSON envelope.")
|
|
4560
|
+
.action(async (from, to, options) => {
|
|
4561
|
+
await handleAsyncAction("tags rename", options, () => requestOxygen("/api/cli/tags/rename", {
|
|
4562
|
+
method: "POST",
|
|
4563
|
+
body: { from, to, ...(options.apply ? { apply: true } : {}) },
|
|
4564
|
+
}));
|
|
4576
4565
|
}));
|
|
4577
4566
|
program
|
|
4578
4567
|
.command("blueprints")
|
|
@@ -4596,6 +4585,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4596
4585
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
4597
4586
|
return requestOxygen(`/api/cli/blueprints${qs}`);
|
|
4598
4587
|
});
|
|
4588
|
+
}))
|
|
4589
|
+
.addCommand(new Command("tag")
|
|
4590
|
+
.description("Replace a saved blueprint's workspace tags (whole set; `--tags \"\"` clears). Works on already-saved blueprints — no re-export needed. See `oxygen tags list` for the vocabulary.")
|
|
4591
|
+
.argument("<blueprint>", "Blueprint slug or id.")
|
|
4592
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
4593
|
+
.option("--json", "Print a JSON envelope.")
|
|
4594
|
+
.action(async (blueprint, options) => {
|
|
4595
|
+
await handleAsyncAction("blueprints tag", options, () => requestOxygen("/api/cli/blueprints/tags", {
|
|
4596
|
+
method: "POST",
|
|
4597
|
+
body: { blueprint, tags: splitCommaList(options.tags) },
|
|
4598
|
+
}));
|
|
4599
4599
|
}))
|
|
4600
4600
|
.addCommand(new Command("describe")
|
|
4601
4601
|
.description("Describe one blueprint (seed or saved) by slug.")
|
|
@@ -5046,7 +5046,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5046
5046
|
.argument("<table>", "Table id or slug.")
|
|
5047
5047
|
.argument("<column>", "Column id or key.")
|
|
5048
5048
|
.option("--row-id <row_id>", "Workspace row id to run.")
|
|
5049
|
-
.option("--limit <n>", "
|
|
5049
|
+
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline (non-background) hard cap is 25.")
|
|
5050
5050
|
.option("--all", "Run all rows. Requires --background.")
|
|
5051
5051
|
.option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
|
|
5052
5052
|
.option("--force", "Run even when the target cell already has a value.")
|
|
@@ -6007,6 +6007,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6007
6007
|
program
|
|
6008
6008
|
.command("billing")
|
|
6009
6009
|
.description("Plan and managed credit commands.")
|
|
6010
|
+
.addCommand(new Command("change")
|
|
6011
|
+
.description("Preview an upgrade or downgrade and return a Stripe confirmation link. Nothing changes until confirmed in Stripe.")
|
|
6012
|
+
.requiredOption("--to <tier>", "Target plan: starter, pro, or team.")
|
|
6013
|
+
.option("--json", "Print a JSON envelope.")
|
|
6014
|
+
.action(async (options) => {
|
|
6015
|
+
await handleAsyncAction("billing change", options, () => requestOxygen("/api/cli/billing/change", {
|
|
6016
|
+
method: "POST",
|
|
6017
|
+
body: { tier: options.to },
|
|
6018
|
+
}));
|
|
6019
|
+
}))
|
|
6010
6020
|
.addCommand(new Command("balance")
|
|
6011
6021
|
.description("Show the current plan and managed credit balance. Credits are Oxygen's native unit; the plan's price and $-per-credit are at https://oxygen-agent.com/billing.")
|
|
6012
6022
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6133,14 +6143,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6133
6143
|
}));
|
|
6134
6144
|
}))
|
|
6135
6145
|
.addCommand(new Command("topup")
|
|
6136
|
-
.description("Buy on-demand credits at $1.25 per 1,000
|
|
6137
|
-
.argument("[pack]", "
|
|
6146
|
+
.description("Buy a custom amount of on-demand credits at $1.25 per 1,000. Run without an amount to inspect the allowed range; checkout happens in Stripe and purchased credits never expire.")
|
|
6147
|
+
.argument("[pack]", "Legacy pack alias in USD: 10, 25, 100, or 250.")
|
|
6148
|
+
.option("--credits <n>", "Credits to buy (8,000-200,000 in 1,000-credit increments).")
|
|
6138
6149
|
.option("--json", "Print a JSON envelope.")
|
|
6139
6150
|
.action(async (pack, options) => {
|
|
6140
|
-
|
|
6151
|
+
const credits = readOption(options.credits);
|
|
6152
|
+
await handleAsyncAction("billing topup", options, () => pack || credits
|
|
6141
6153
|
? requestOxygen("/api/cli/billing/topup", {
|
|
6142
6154
|
method: "POST",
|
|
6143
|
-
body: {
|
|
6155
|
+
body: {
|
|
6156
|
+
...(pack ? { pack } : {}),
|
|
6157
|
+
...(credits ? { credits: readPositiveNumber(credits) } : {}),
|
|
6158
|
+
},
|
|
6144
6159
|
})
|
|
6145
6160
|
: requestOxygen("/api/cli/billing/topup"));
|
|
6146
6161
|
}));
|
|
@@ -6579,37 +6594,37 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6579
6594
|
// `oxygen knowledge agent`, which configures the AI Knowledge Agent itself.)
|
|
6580
6595
|
program
|
|
6581
6596
|
.command("agent")
|
|
6582
|
-
.description("Agents:
|
|
6597
|
+
.description("Agents: the programmable Workspace Agent plus built-in specialist agents. Publish versioned instructions and policies, start durable runs, trigger them by cron/event/webhook, inspect every event, and control approvals. Web: /agents.")
|
|
6583
6598
|
.addCommand(new Command("list")
|
|
6584
|
-
.description("List the built-in
|
|
6599
|
+
.description("List the Workspace Agent and built-in specialists with state and recent run history.")
|
|
6585
6600
|
.option("--json", "Print a JSON envelope.")
|
|
6586
6601
|
.action(async (options) => {
|
|
6587
6602
|
await handleAsyncAction("agent list", options, () => requestOxygen("/api/cli/agent"));
|
|
6588
6603
|
}))
|
|
6589
6604
|
.addCommand(new Command("get")
|
|
6590
|
-
.description("Show one
|
|
6591
|
-
.argument("<slug>", "
|
|
6605
|
+
.description("Show one agent, including the Workspace Agent's active version and recent runs.")
|
|
6606
|
+
.argument("<slug>", "Agent slug: workspace, inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6592
6607
|
.option("--json", "Print a JSON envelope.")
|
|
6593
6608
|
.action(async (slug, options) => {
|
|
6594
6609
|
await handleAsyncAction("agent get", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}`));
|
|
6595
6610
|
}))
|
|
6596
6611
|
.addCommand(new Command("enable")
|
|
6597
|
-
.description("Enable a specialist agent so it resumes its
|
|
6612
|
+
.description("Enable a workspace or specialist agent so it resumes its governed runs.")
|
|
6598
6613
|
.argument("<slug>", "Specialist slug: inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6599
6614
|
.option("--json", "Print a JSON envelope.")
|
|
6600
6615
|
.action(async (slug, options) => {
|
|
6601
6616
|
await handleAsyncAction("agent enable", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/enable`, { method: "POST" }));
|
|
6602
6617
|
}))
|
|
6603
6618
|
.addCommand(new Command("disable")
|
|
6604
|
-
.description("Disable a specialist agent so it stops
|
|
6619
|
+
.description("Disable a workspace or specialist agent so it stops new runs on the next worker tick.")
|
|
6605
6620
|
.argument("<slug>", "Specialist slug: inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6606
6621
|
.option("--json", "Print a JSON envelope.")
|
|
6607
6622
|
.action(async (slug, options) => {
|
|
6608
6623
|
await handleAsyncAction("agent disable", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/disable`, { method: "POST" }));
|
|
6609
6624
|
}))
|
|
6610
6625
|
.addCommand(new Command("runs")
|
|
6611
|
-
.description("List recent runs across
|
|
6612
|
-
.argument("[slug]", "Optional
|
|
6626
|
+
.description("List recent runs across all agents (or one via <slug>), newest first.")
|
|
6627
|
+
.argument("[slug]", "Optional agent slug to filter to one agent.")
|
|
6613
6628
|
.option("--limit <n>", "Maximum runs to return (1-100). Defaults to 25.")
|
|
6614
6629
|
.option("--json", "Print a JSON envelope.")
|
|
6615
6630
|
.action(async (slug, options) => {
|
|
@@ -6624,6 +6639,316 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6624
6639
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
6625
6640
|
return requestOxygen(`/api/cli/agent/runs${suffix}`);
|
|
6626
6641
|
});
|
|
6642
|
+
}))
|
|
6643
|
+
.addCommand(new Command("set")
|
|
6644
|
+
.description("Publish an immutable version (reconfigure) for a runtime agent: the Workspace Agent or a custom agent.")
|
|
6645
|
+
.argument("<slug>", "workspace, or a custom agent slug (from `oxygen agent create`).")
|
|
6646
|
+
.requiredOption("--instructions <text>", "Workspace Agent instructions for this version.")
|
|
6647
|
+
.option("--model-policy-json <json>", "Model policy JSON. Defaults to managed OpenRouter medium.")
|
|
6648
|
+
.option("--tool-policy-json <json>", "Tool policy JSON. Omit restrictions for full eligible catalog access.")
|
|
6649
|
+
.option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
|
|
6650
|
+
.option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
|
|
6651
|
+
.option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
|
|
6652
|
+
.option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
|
|
6653
|
+
.option("--confirm-full-access", "Acknowledge standing full catalog access when the policy mode is full.")
|
|
6654
|
+
.option("--json", "Print a JSON envelope.")
|
|
6655
|
+
.action(async (slug, options) => {
|
|
6656
|
+
await handleAsyncAction("agent set", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}`, {
|
|
6657
|
+
method: "PATCH",
|
|
6658
|
+
body: {
|
|
6659
|
+
instructions: options.instructions,
|
|
6660
|
+
...(options.modelPolicyJson ? { model_policy: parseJsonObject(options.modelPolicyJson) } : {}),
|
|
6661
|
+
...(options.toolPolicyJson ? { tool_policy: parseJsonObject(options.toolPolicyJson) } : {}),
|
|
6662
|
+
...(options.contextPolicyJson ? { context_policy: parseJsonObject(options.contextPolicyJson) } : {}),
|
|
6663
|
+
...(options.skills ? { skill_slugs: options.skills.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6664
|
+
...(options.sandboxPolicyJson ? { sandbox_policy: parseJsonObject(options.sandboxPolicyJson) } : {}),
|
|
6665
|
+
...(options.limitsJson ? { limits: parseJsonObject(options.limitsJson) } : {}),
|
|
6666
|
+
confirm_full_access: options.confirmFullAccess === true,
|
|
6667
|
+
},
|
|
6668
|
+
}));
|
|
6669
|
+
}))
|
|
6670
|
+
.addCommand(new Command("create")
|
|
6671
|
+
.description("Create a custom agent and publish its first immutable version. Schedule it with `oxygen agent trigger create`.")
|
|
6672
|
+
.argument("<slug>", "User-minted agent slug: a lowercase letter then letters, digits, or hyphens (2-49 chars).")
|
|
6673
|
+
.requiredOption("--name <name>", "Display name for the agent.")
|
|
6674
|
+
.requiredOption("--instructions <text>", "Durable standing instructions for the first immutable version.")
|
|
6675
|
+
.option("--description <text>", "Optional one-line description of the agent's job.")
|
|
6676
|
+
.option("--model-policy-json <json>", "Model policy JSON. Defaults to managed OpenRouter medium.")
|
|
6677
|
+
.option("--tool-policy-json <json>", "Tool policy JSON. Omit restrictions for full eligible catalog access.")
|
|
6678
|
+
.option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
|
|
6679
|
+
.option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
|
|
6680
|
+
.option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
|
|
6681
|
+
.option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
|
|
6682
|
+
.option("--confirm-full-access", "Acknowledge standing full catalog access when the policy mode is full.")
|
|
6683
|
+
.option("--json", "Print a JSON envelope.")
|
|
6684
|
+
.action(async (slug, options) => {
|
|
6685
|
+
await handleAsyncAction("agent create", options, () => requestOxygen("/api/cli/agent/create", {
|
|
6686
|
+
method: "POST",
|
|
6687
|
+
body: {
|
|
6688
|
+
slug,
|
|
6689
|
+
name: options.name,
|
|
6690
|
+
instructions: options.instructions,
|
|
6691
|
+
...(options.description ? { description: options.description } : {}),
|
|
6692
|
+
...(options.modelPolicyJson ? { model_policy: parseJsonObject(options.modelPolicyJson) } : {}),
|
|
6693
|
+
...(options.toolPolicyJson ? { tool_policy: parseJsonObject(options.toolPolicyJson) } : {}),
|
|
6694
|
+
...(options.contextPolicyJson ? { context_policy: parseJsonObject(options.contextPolicyJson) } : {}),
|
|
6695
|
+
...(options.skills ? { skill_slugs: options.skills.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6696
|
+
...(options.sandboxPolicyJson ? { sandbox_policy: parseJsonObject(options.sandboxPolicyJson) } : {}),
|
|
6697
|
+
...(options.limitsJson ? { limits: parseJsonObject(options.limitsJson) } : {}),
|
|
6698
|
+
confirm_full_access: options.confirmFullAccess === true,
|
|
6699
|
+
},
|
|
6700
|
+
}));
|
|
6701
|
+
}))
|
|
6702
|
+
.addCommand(new Command("custom")
|
|
6703
|
+
.description("List the org's user-defined custom agents (distinct from `agent list`, which shows the built-in roster).")
|
|
6704
|
+
.option("--json", "Print a JSON envelope.")
|
|
6705
|
+
.action(async (options) => {
|
|
6706
|
+
await handleAsyncAction("agent custom", options, () => requestOxygen("/api/cli/agent/custom"));
|
|
6707
|
+
}))
|
|
6708
|
+
.addCommand(new Command("run")
|
|
6709
|
+
.description("Start a durable Workspace Agent run on the Fly worker.")
|
|
6710
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6711
|
+
.option("--goal <text>", "Natural-language goal.")
|
|
6712
|
+
.option("--input-json <json>", "Structured run input instead of --goal.")
|
|
6713
|
+
.option("--idempotency-key <key>", "Deduplicate retries of the same source event.")
|
|
6714
|
+
.option("--thread-id <id>", "Continue an existing persistent Agent thread.")
|
|
6715
|
+
.option("--thread-mode <isolated|persistent>", "Create a persistent thread for this run. Defaults to isolated.")
|
|
6716
|
+
.option("--file-ids <csv>", "Comma-separated workspace file UUIDs attached to context and eligible for sandbox input.")
|
|
6717
|
+
.requiredOption("--max-credits <credits>", "Hard credit ceiling for this run.")
|
|
6718
|
+
.requiredOption("--approved", "Explicitly approve managed inference up to --max-credits.")
|
|
6719
|
+
.option("--json", "Print a JSON envelope.")
|
|
6720
|
+
.action(async (slug, options) => {
|
|
6721
|
+
await handleAsyncAction("agent run", options, () => {
|
|
6722
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
6723
|
+
if (maxCredits === undefined)
|
|
6724
|
+
throw new OxygenError("invalid_request", "--max-credits must be positive.", { exitCode: 2 });
|
|
6725
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs`, {
|
|
6726
|
+
method: "POST",
|
|
6727
|
+
body: {
|
|
6728
|
+
...(options.goal ? { goal: options.goal } : {}),
|
|
6729
|
+
...(options.inputJson ? { input: parseJsonObject(options.inputJson) } : {}),
|
|
6730
|
+
...(options.idempotencyKey ? { idempotency_key: options.idempotencyKey } : {}),
|
|
6731
|
+
...(options.threadId ? { thread_id: options.threadId } : {}),
|
|
6732
|
+
thread_mode: options.threadMode ?? "isolated",
|
|
6733
|
+
...(options.fileIds ? { file_ids: options.fileIds.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6734
|
+
max_credits: maxCredits,
|
|
6735
|
+
approved: options.approved === true,
|
|
6736
|
+
},
|
|
6737
|
+
});
|
|
6738
|
+
});
|
|
6739
|
+
}))
|
|
6740
|
+
.addCommand(new Command("run-get")
|
|
6741
|
+
.description("Inspect one Workspace Agent run and its append-only event ledger.")
|
|
6742
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6743
|
+
.argument("<run-id>", "Agent run id.")
|
|
6744
|
+
.option("--after-seq <n>", "Return events after this sequence number.")
|
|
6745
|
+
.option("--json", "Print a JSON envelope.")
|
|
6746
|
+
.action(async (slug, runId, options) => {
|
|
6747
|
+
await handleAsyncAction("agent run get", options, () => {
|
|
6748
|
+
const params = new URLSearchParams();
|
|
6749
|
+
if (options.afterSeq)
|
|
6750
|
+
params.set("after_seq", options.afterSeq);
|
|
6751
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
6752
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}${suffix}`);
|
|
6753
|
+
});
|
|
6754
|
+
}))
|
|
6755
|
+
.addCommand(new Command("cancel")
|
|
6756
|
+
.description("Request cancellation of an active Workspace Agent run.")
|
|
6757
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6758
|
+
.argument("<run-id>", "Agent run id.")
|
|
6759
|
+
.option("--json", "Print a JSON envelope.")
|
|
6760
|
+
.action(async (slug, runId, options) => {
|
|
6761
|
+
await handleAsyncAction("agent run cancel", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" }));
|
|
6762
|
+
}))
|
|
6763
|
+
.addCommand(new Command("approval-decide")
|
|
6764
|
+
.description("Approve or reject one pending Workspace Agent tool call, then resume its durable run.")
|
|
6765
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6766
|
+
.argument("<run-id>", "Agent run id.")
|
|
6767
|
+
.argument("<approval-id>", "Approval id from the run event ledger.")
|
|
6768
|
+
.requiredOption("--decision <approved|rejected>", "Decision.")
|
|
6769
|
+
.option("--json", "Print a JSON envelope.")
|
|
6770
|
+
.action(async (slug, runId, approvalId, options) => {
|
|
6771
|
+
await handleAsyncAction("agent approval decide", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.decision } }));
|
|
6772
|
+
}))
|
|
6773
|
+
.addCommand(new Command("trigger-create")
|
|
6774
|
+
.description("Create a cron, internal event, or signed-webhook trigger for the Workspace Agent.")
|
|
6775
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6776
|
+
.requiredOption("--type <cron|event|webhook>", "Trigger type.")
|
|
6777
|
+
.requiredOption("--instruction <text>", "Goal template run for each delivery.")
|
|
6778
|
+
.option("--cron <expression>", "Five-field cron expression for cron triggers.")
|
|
6779
|
+
.option("--timezone <iana>", "IANA timezone. Defaults to UTC.")
|
|
6780
|
+
.option("--event-source <source>", "Internal workflow-event source for event triggers.")
|
|
6781
|
+
.option("--event-type <event>", "Internal workflow-event type for event triggers.")
|
|
6782
|
+
.option("--thread-mode <isolated|persistent>", "Reuse a bounded thread across deliveries. Defaults to isolated.")
|
|
6783
|
+
.requiredOption("--max-credits <credits>", "Standing per-delivery credit ceiling.")
|
|
6784
|
+
.requiredOption("--approved", "Authorize each trigger delivery up to --max-credits.")
|
|
6785
|
+
.option("--json", "Print a JSON envelope.")
|
|
6786
|
+
.action(async (slug, options) => {
|
|
6787
|
+
await handleAsyncAction("agent trigger create", options, () => {
|
|
6788
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
6789
|
+
if (maxCredits === undefined)
|
|
6790
|
+
throw new OxygenError("invalid_request", "--max-credits must be positive.", { exitCode: 2 });
|
|
6791
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers`, { method: "POST", body: {
|
|
6792
|
+
type: options.type, instruction_template: options.instruction,
|
|
6793
|
+
...(options.cron ? { cron_expression: options.cron } : {}),
|
|
6794
|
+
...(options.timezone ? { timezone: options.timezone } : {}),
|
|
6795
|
+
...(options.eventSource ? { event_source: options.eventSource } : {}),
|
|
6796
|
+
...(options.eventType ? { event_type: options.eventType } : {}),
|
|
6797
|
+
thread_mode: options.threadMode ?? "isolated",
|
|
6798
|
+
max_credits: maxCredits,
|
|
6799
|
+
approved: options.approved === true,
|
|
6800
|
+
} });
|
|
6801
|
+
});
|
|
6802
|
+
}))
|
|
6803
|
+
.addCommand(new Command("triggers")
|
|
6804
|
+
.description("List Workspace Agent cron, internal event, and webhook triggers.")
|
|
6805
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6806
|
+
.option("--json", "Print a JSON envelope.")
|
|
6807
|
+
.action(async (slug, options) => {
|
|
6808
|
+
await handleAsyncAction("agent triggers", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers`));
|
|
6809
|
+
}))
|
|
6810
|
+
.addCommand(new Command("trigger-set")
|
|
6811
|
+
.description("Activate, pause, or archive one Workspace Agent trigger.")
|
|
6812
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6813
|
+
.argument("<trigger-id>", "Agent trigger id.")
|
|
6814
|
+
.requiredOption("--status <active|paused|archived>", "New trigger status.")
|
|
6815
|
+
.option("--json", "Print a JSON envelope.")
|
|
6816
|
+
.action(async (slug, triggerId, options) => {
|
|
6817
|
+
await handleAsyncAction("agent trigger set", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers/${encodeURIComponent(triggerId)}`, { method: "PATCH", body: { status: options.status } }));
|
|
6818
|
+
}))
|
|
6819
|
+
.addCommand(new Command("file-create")
|
|
6820
|
+
.description("Store a tenant-owned text file for Agent context and sandbox input.")
|
|
6821
|
+
.requiredOption("--name <name>", "Workspace filename.")
|
|
6822
|
+
.requiredOption("--text <text>", "Inline text content (max 1.5 MB).")
|
|
6823
|
+
.option("--mime-type <type>", "MIME type. Defaults to text/plain.")
|
|
6824
|
+
.option("--json", "Print a JSON envelope.")
|
|
6825
|
+
.action(async (options) => {
|
|
6826
|
+
await handleAsyncAction("agent file create", options, () => requestOxygen("/api/cli/agent/files", {
|
|
6827
|
+
method: "POST", body: { name: options.name, text: options.text, mime_type: options.mimeType ?? "text/plain" },
|
|
6828
|
+
}));
|
|
6829
|
+
}))
|
|
6830
|
+
.addCommand(new Command("files")
|
|
6831
|
+
.description("List tenant-owned Agent context files.")
|
|
6832
|
+
.option("--limit <n>", "Maximum files (1-500).")
|
|
6833
|
+
.option("--json", "Print a JSON envelope.")
|
|
6834
|
+
.action(async (options) => {
|
|
6835
|
+
await handleAsyncAction("agent files", options, () => {
|
|
6836
|
+
const limit = readPositiveInt(options.limit);
|
|
6837
|
+
return requestOxygen(`/api/cli/agent/files${limit ? `?limit=${limit}` : ""}`);
|
|
6838
|
+
});
|
|
6839
|
+
}));
|
|
6840
|
+
program
|
|
6841
|
+
.command("copilot")
|
|
6842
|
+
.description("Workspace Copilot: an interactive workspace agent you drive turn by turn from the terminal. Start a session with an explicit inference budget, send it messages and watch the reply stream, approve the actions it proposes, then cancel when done. Inference bills credits at 5x the actual model cost, always inside the session budget you approve up front. Web: /copilot.")
|
|
6843
|
+
.addCommand(new Command("start")
|
|
6844
|
+
.description("Start a Workspace Copilot session. --budget-credits is your explicit approval of the session's inference spend cap; inference bills at 5x the actual model cost within it.")
|
|
6845
|
+
.requiredOption("--budget-credits <number>", "Required: the session inference budget in credits — your explicit approval of the maximum inference spend for the whole session.")
|
|
6846
|
+
.option("--tier <low|medium|high>", "Reasoning effort tier for the session's model.")
|
|
6847
|
+
.option("--model <id>", "Pin a specific model id instead of the journey/tier default.")
|
|
6848
|
+
.option("--per-turn-ceiling <number>", "Optional per-turn credit ceiling that caps any single turn's inference spend.")
|
|
6849
|
+
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
6850
|
+
.option("--title <text>", "Optional human title for the session.")
|
|
6851
|
+
.option("--auto-approve", "Start with auto-approve ON for paid, workspace-internal actions (cards are still created and decided automatically within each action's credit cap). External sends, enrollments, publishes, DNS, and external CRM pushes always stay human-gated.")
|
|
6852
|
+
.option("--json", "Print a JSON envelope.")
|
|
6853
|
+
.action(async (options) => {
|
|
6854
|
+
await handleAsyncAction("copilot start", options, () => {
|
|
6855
|
+
const budgetCredits = readPositiveNumber(options.budgetCredits);
|
|
6856
|
+
if (budgetCredits === undefined) {
|
|
6857
|
+
throw new OxygenError("invalid_budget", "Pass --budget-credits as a positive number of credits to approve the session's inference spend.", { exitCode: 1 });
|
|
6858
|
+
}
|
|
6859
|
+
const perTurnCeiling = readPositiveNumber(options.perTurnCeiling);
|
|
6860
|
+
const tier = readOption(options.tier);
|
|
6861
|
+
const model = readOption(options.model);
|
|
6862
|
+
const journey = readOption(options.journey);
|
|
6863
|
+
const title = readOption(options.title);
|
|
6864
|
+
const body = { budget_credits: budgetCredits };
|
|
6865
|
+
if (tier)
|
|
6866
|
+
body.tier = tier;
|
|
6867
|
+
if (model)
|
|
6868
|
+
body.model = model;
|
|
6869
|
+
if (perTurnCeiling !== undefined)
|
|
6870
|
+
body.per_turn_credit_ceiling = perTurnCeiling;
|
|
6871
|
+
if (journey)
|
|
6872
|
+
body.journey = journey;
|
|
6873
|
+
if (title)
|
|
6874
|
+
body.title = title;
|
|
6875
|
+
if (options.autoApprove)
|
|
6876
|
+
body.auto_approve = true;
|
|
6877
|
+
return requestOxygen("/api/cli/copilot/sessions", { method: "POST", body });
|
|
6878
|
+
});
|
|
6879
|
+
}))
|
|
6880
|
+
.addCommand(new Command("list")
|
|
6881
|
+
.description("List your Workspace Copilot sessions with their status, budget, and credits spent.")
|
|
6882
|
+
.option("--json", "Print a JSON envelope.")
|
|
6883
|
+
.action(async (options) => {
|
|
6884
|
+
await handleAsyncAction("copilot list", options, () => requestOxygen("/api/cli/copilot/sessions"));
|
|
6885
|
+
}))
|
|
6886
|
+
.addCommand(new Command("get")
|
|
6887
|
+
.description("Show one Workspace Copilot session: its state, turns, and event log. Use --after-seq to page through the event stream.")
|
|
6888
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6889
|
+
.option("--after-seq <n>", "Only return events after this sequence number.")
|
|
6890
|
+
.option("--events <n>", "Maximum events to return.")
|
|
6891
|
+
.option("--json", "Print a JSON envelope.")
|
|
6892
|
+
.action(async (sessionId, options) => {
|
|
6893
|
+
await handleAsyncAction("copilot get", options, () => {
|
|
6894
|
+
const query = new URLSearchParams();
|
|
6895
|
+
const afterSeq = readNonNegativeInt(options.afterSeq);
|
|
6896
|
+
const eventLimit = readPositiveInt(options.events);
|
|
6897
|
+
if (afterSeq !== undefined)
|
|
6898
|
+
query.set("after_seq", String(afterSeq));
|
|
6899
|
+
if (eventLimit !== undefined)
|
|
6900
|
+
query.set("event_limit", String(eventLimit));
|
|
6901
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
6902
|
+
return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}${suffix}`);
|
|
6903
|
+
});
|
|
6904
|
+
}))
|
|
6905
|
+
.addCommand(new Command("send")
|
|
6906
|
+
.description("Send a message to a Workspace Copilot session and stream the reply to stderr. Waits for the turn to finish (or pause for an approval) unless --no-wait. Inference bills credits at 5x the actual model cost within the session budget.")
|
|
6907
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6908
|
+
.argument("<message...>", "Message to send (all words joined with spaces).")
|
|
6909
|
+
.option("--no-wait", "Post the turn and return immediately without streaming the reply.")
|
|
6910
|
+
.option("--context <json>", "Optional JSON object of surface context to attach to the turn.")
|
|
6911
|
+
.option("--timeout-seconds <n>", "Maximum time to wait for the turn to finish. Defaults to 300.")
|
|
6912
|
+
.option("--interval-seconds <n>", "Polling interval while streaming. Defaults to 2.")
|
|
6913
|
+
.option("--json", "Print a JSON envelope.")
|
|
6914
|
+
.action(async (sessionId, messageParts, options) => {
|
|
6915
|
+
await handleAsyncAction("copilot send", options, async () => {
|
|
6916
|
+
const message = messageParts.join(" ");
|
|
6917
|
+
const context = readJsonObjectOption(options.context);
|
|
6918
|
+
const posted = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/turns`, { method: "POST", body: { message, ...(context ? { context } : {}) } });
|
|
6919
|
+
// Commander maps --no-wait to `wait: false` (default true).
|
|
6920
|
+
if (options.wait === false)
|
|
6921
|
+
return posted;
|
|
6922
|
+
const turn = isRecord(posted.turn) ? posted.turn : {};
|
|
6923
|
+
const turnId = typeof turn.id === "string" ? turn.id : "";
|
|
6924
|
+
const latestSeq = posted.latest_seq;
|
|
6925
|
+
const initialSeq = typeof latestSeq === "number" && Number.isFinite(latestSeq) ? latestSeq : 0;
|
|
6926
|
+
const finalData = await waitForCopilotTurn({
|
|
6927
|
+
sessionId,
|
|
6928
|
+
turnId,
|
|
6929
|
+
initialSeq,
|
|
6930
|
+
requestedTimeoutSeconds: options.timeoutSeconds,
|
|
6931
|
+
requestedIntervalSeconds: options.intervalSeconds,
|
|
6932
|
+
});
|
|
6933
|
+
writeCopilotTurnCreditsSummary(finalData, turnId);
|
|
6934
|
+
return finalData;
|
|
6935
|
+
});
|
|
6936
|
+
}))
|
|
6937
|
+
.addCommand(new Command("approve")
|
|
6938
|
+
.description("Approve (or, with --reject, reject) an action a Workspace Copilot session is waiting on; approving re-queues the paused turn.")
|
|
6939
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6940
|
+
.argument("<approvalId>", "Approval id from the approval_requested event.")
|
|
6941
|
+
.option("--reject", "Reject the action instead of approving it.")
|
|
6942
|
+
.option("--json", "Print a JSON envelope.")
|
|
6943
|
+
.action(async (sessionId, approvalId, options) => {
|
|
6944
|
+
await handleAsyncAction("copilot approve", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.reject ? "reject" : "approve" } }));
|
|
6945
|
+
}))
|
|
6946
|
+
.addCommand(new Command("cancel")
|
|
6947
|
+
.description("Cancel a Workspace Copilot session, stopping any in-flight turn.")
|
|
6948
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6949
|
+
.option("--json", "Print a JSON envelope.")
|
|
6950
|
+
.action(async (sessionId, options) => {
|
|
6951
|
+
await handleAsyncAction("copilot cancel", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" }));
|
|
6627
6952
|
}));
|
|
6628
6953
|
program
|
|
6629
6954
|
.command("runs")
|
|
@@ -7309,7 +7634,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7309
7634
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
7310
7635
|
}))
|
|
7311
7636
|
.addCommand(new Command("connect")
|
|
7312
|
-
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Use --count to mint several links at once for bulk onboarding. Open each URL in a browser to complete authentication.")
|
|
7637
|
+
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Use --count to mint several links at once for bulk onboarding. Open each URL in a browser to complete authentication. Links are shareable: send one to the account owner (e.g. a client) — no Oxygen login is needed to complete it, and the account lands in this workspace's sender list. Each link is valid for 30 minutes and connects one account.")
|
|
7313
7638
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
7314
7639
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
7315
7640
|
.option("--count <n>", "Mint N hosted-auth links in one call for bulk onboarding (1-25, default 1). Each link connects a different account. Ignored when reconnecting.")
|
|
@@ -7334,6 +7659,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7334
7659
|
.option("--json", "Print a JSON envelope.")
|
|
7335
7660
|
.action(async (id, options) => {
|
|
7336
7661
|
await handleAsyncAction("senders get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}`));
|
|
7662
|
+
}))
|
|
7663
|
+
.addCommand(new Command("tag")
|
|
7664
|
+
.description("Replace a sender account's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the senders behind one campaign tag. See `oxygen tags list` for the vocabulary.")
|
|
7665
|
+
.argument("<id>", "Sender account id or Unipile account id.")
|
|
7666
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
7667
|
+
.option("--json", "Print a JSON envelope.")
|
|
7668
|
+
.action(async (id, options) => {
|
|
7669
|
+
await handleAsyncAction("senders tag", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/tags`, {
|
|
7670
|
+
method: "POST",
|
|
7671
|
+
body: { tags: splitCommaList(options.tags) },
|
|
7672
|
+
}));
|
|
7337
7673
|
}))
|
|
7338
7674
|
.addCommand(new Command("health")
|
|
7339
7675
|
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp, and when the daily quota resets. Read-only — no provider call, no credits. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
@@ -8079,6 +8415,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8079
8415
|
.option("--bucket <bucket>", "Email only: primary or others (superseded by --segment).")
|
|
8080
8416
|
.option("--segment <segment>", "Email only: top-tab folder — primary, others, sent, warmup, or dmarc.")
|
|
8081
8417
|
.option("--status <keys>", "Comma-separated status keys (e.g. interested,meeting_booked). Cross-channel — filters email + LinkedIn + WhatsApp by the shared taxonomy.")
|
|
8418
|
+
.option("--sentiment <values>", "Comma-separated AI sentiment: positive, neutral, negative. Cross-channel; not-yet-analyzed conversations never match.")
|
|
8419
|
+
.option("--tag <tags>", "Comma-separated campaign tags — matches email conversations whose campaign (sequence) carries any of these tags. DMs have no campaign link, so a tag filter shows email only.")
|
|
8082
8420
|
.option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Cross-channel.")
|
|
8083
8421
|
.option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Cross-channel.")
|
|
8084
8422
|
.option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) UUIDs (not slugs — get the id from `sequences get <slug>`).")
|
|
@@ -8089,6 +8427,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8089
8427
|
.option("--search <text>", "Filter by attendee name or last-message text. Cross-channel.")
|
|
8090
8428
|
.option("--include-archived", "Include archived conversations.")
|
|
8091
8429
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
8430
|
+
.option("--cursor <cursor>", "channel=all only: the previous page's next_cursor — resumes the merged stream after that row.")
|
|
8431
|
+
.option("--no-counts", "channel=all only: skip the sidebar facet counts for a faster paged read (total_conversations/unread_conversations/sidebar_counts omitted from the envelope). Other channels ignore it.")
|
|
8092
8432
|
.option("--json", "Print a JSON envelope.")
|
|
8093
8433
|
.action(async (options) => {
|
|
8094
8434
|
await handleAsyncAction("inbox list", options, () => {
|
|
@@ -8108,6 +8448,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8108
8448
|
["segment", "segment"],
|
|
8109
8449
|
["channels", "channels"],
|
|
8110
8450
|
["status", "status"],
|
|
8451
|
+
["sentiment", "sentiment"],
|
|
8452
|
+
["tag", "tag"],
|
|
8111
8453
|
["since", "since"],
|
|
8112
8454
|
["until", "until"],
|
|
8113
8455
|
["sequenceId", "sequence_id"],
|
|
@@ -8115,6 +8457,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8115
8457
|
["domain", "domain"],
|
|
8116
8458
|
["excludeDomain", "exclude_domain"],
|
|
8117
8459
|
["mailboxId", "mailbox_id"],
|
|
8460
|
+
["cursor", "cursor"],
|
|
8118
8461
|
]) {
|
|
8119
8462
|
const value = readOption(options[flag]);
|
|
8120
8463
|
if (value)
|
|
@@ -8128,6 +8471,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8128
8471
|
const limit = readOption(options.limit);
|
|
8129
8472
|
if (limit)
|
|
8130
8473
|
params.set("limit", limit);
|
|
8474
|
+
// Commander's --no-counts sets counts:false; absent leaves the
|
|
8475
|
+
// server default (counts included) untouched.
|
|
8476
|
+
if (options.counts === false)
|
|
8477
|
+
params.set("include_counts", "false");
|
|
8131
8478
|
const suffix = params.toString();
|
|
8132
8479
|
return requestOxygen(`/api/cli/inbox${suffix ? `?${suffix}` : ""}`);
|
|
8133
8480
|
});
|
|
@@ -8274,6 +8621,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8274
8621
|
body: { status, channel },
|
|
8275
8622
|
});
|
|
8276
8623
|
});
|
|
8624
|
+
}))
|
|
8625
|
+
.addCommand(new Command("tag")
|
|
8626
|
+
.description("Add/remove workspace tags on a conversation (Tags primitive — the same vocabulary as sequences, tables, posts; see `oxygen tags list`). Delta semantics: --add unions, --remove subtracts, remove wins on conflict. Internal metadata, never sent to the counterpart. --channel linkedin/whatsapp tags a DM conversation.")
|
|
8627
|
+
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
8628
|
+
.option("--add <tags>", "Comma-separated tags to add.")
|
|
8629
|
+
.option("--remove <tags>", "Comma-separated tags to remove.")
|
|
8630
|
+
.option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
|
|
8631
|
+
.option("--json", "Print a JSON envelope.")
|
|
8632
|
+
.action(async (conversation, options) => {
|
|
8633
|
+
await handleAsyncAction("inbox tag", options, () => {
|
|
8634
|
+
const add = splitCommaList(options.add);
|
|
8635
|
+
const remove = splitCommaList(options.remove);
|
|
8636
|
+
if (add.length === 0 && remove.length === 0) {
|
|
8637
|
+
throw new OxygenError("invalid_request", "Pass --add and/or --remove.", { exitCode: 1 });
|
|
8638
|
+
}
|
|
8639
|
+
const channel = readOption(options.channel) ?? "email";
|
|
8640
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/tags`, {
|
|
8641
|
+
method: "POST",
|
|
8642
|
+
body: {
|
|
8643
|
+
...(add.length > 0 ? { add } : {}),
|
|
8644
|
+
...(remove.length > 0 ? { remove } : {}),
|
|
8645
|
+
channel,
|
|
8646
|
+
},
|
|
8647
|
+
});
|
|
8648
|
+
});
|
|
8277
8649
|
}))
|
|
8278
8650
|
.addCommand(new Command("rescan")
|
|
8279
8651
|
.description("Re-queue conversations for AI re-classification (status + sentiment + drafted reply). Without --yes, previews the counts that would be re-flipped. With --yes, the worker re-classifies the pending rows (free for opt-outs, low-tier model otherwise, credit-capped).")
|
|
@@ -8494,6 +8866,37 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8494
8866
|
body.max_drafts_per_day = Number(options.maxDraftsPerDay);
|
|
8495
8867
|
return requestOxygen("/api/cli/inbox/reply-agent", { method: "POST", body });
|
|
8496
8868
|
});
|
|
8869
|
+
})))
|
|
8870
|
+
.addCommand(new Command("auto-tagger")
|
|
8871
|
+
.description("The auto-tagging agent (the 'AI Inbox Tagger'): classifies each new inbound conversation against your workspace tag allowlist and auto-applies matches — additive, never re-adds a tag you removed, zero extra model calls (rides the analysis pass). Roster + run history: `oxygen agent get inbox-auto-tagger`; web home: /agents/inbox-auto-tagger.")
|
|
8872
|
+
.addCommand(new Command("get")
|
|
8873
|
+
.description("Show the auto-tagger config (enabled, tag allowlist, channels).")
|
|
8874
|
+
.option("--json", "Print a JSON envelope.")
|
|
8875
|
+
.action(async (options) => {
|
|
8876
|
+
await handleAsyncAction("inbox auto-tagger get", options, () => requestOxygen("/api/cli/inbox/auto-tagger"));
|
|
8877
|
+
}))
|
|
8878
|
+
.addCommand(new Command("set")
|
|
8879
|
+
.description("Update the auto-tagger. --tags replaces the whole allowlist (max 50; `--tags \"\"` clears, which pauses tagging while keeping the agent enabled).")
|
|
8880
|
+
.option("--enabled", "Enable the agent.")
|
|
8881
|
+
.option("--disabled", "Disable the agent.")
|
|
8882
|
+
.option("--tags <tags>", "Comma-separated tag allowlist the classifier may apply (replaces the whole set).")
|
|
8883
|
+
.option("--channels <list>", "Comma-separated channels to tag: a non-empty subset of email, linkedin, whatsapp (default all three).")
|
|
8884
|
+
.option("--json", "Print a JSON envelope.")
|
|
8885
|
+
.action(async (options) => {
|
|
8886
|
+
await handleAsyncAction("inbox auto-tagger set", options, () => {
|
|
8887
|
+
const body = {};
|
|
8888
|
+
if (options.enabled)
|
|
8889
|
+
body.enabled = true;
|
|
8890
|
+
if (options.disabled)
|
|
8891
|
+
body.enabled = false;
|
|
8892
|
+
if (options.tags !== undefined) {
|
|
8893
|
+
body.tags = options.tags.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
8894
|
+
}
|
|
8895
|
+
if (options.channels !== undefined) {
|
|
8896
|
+
body.channels = options.channels.split(",").map((c) => c.trim()).filter(Boolean);
|
|
8897
|
+
}
|
|
8898
|
+
return requestOxygen("/api/cli/inbox/auto-tagger", { method: "POST", body });
|
|
8899
|
+
});
|
|
8497
8900
|
}))));
|
|
8498
8901
|
program.addCommand(new Command("messages")
|
|
8499
8902
|
.description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus reply-rate/campaign analytics. Message-level, unlike inbox (conversation-level triage).")
|
|
@@ -8508,6 +8911,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8508
8911
|
.option("--sequence-id <id>", "Email only: campaign (sequence) id (restricts the result to email).")
|
|
8509
8912
|
.option("--status <keys>", "Comma-separated conversation status keys (e.g. interested,meeting_booked).")
|
|
8510
8913
|
.option("--sentiment <sentiment>", "Filter by sentiment: positive, neutral, or negative.")
|
|
8914
|
+
.option("--tag <tag>", "Only messages whose conversation carries this workspace tag (see `oxygen tags list`).")
|
|
8511
8915
|
.option("--since <iso>", "Only messages sent at or after this ISO timestamp.")
|
|
8512
8916
|
.option("--until <iso>", "Only messages sent before this ISO timestamp.")
|
|
8513
8917
|
.option("--limit <n>", "Maximum messages to return (1-200). Defaults to 50.")
|
|
@@ -8528,6 +8932,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8528
8932
|
["sequenceId", "sequence_id"],
|
|
8529
8933
|
["status", "status"],
|
|
8530
8934
|
["sentiment", "sentiment"],
|
|
8935
|
+
["tag", "tag"],
|
|
8531
8936
|
["since", "since"],
|
|
8532
8937
|
["until", "until"],
|
|
8533
8938
|
["limit", "limit"],
|
|
@@ -8618,14 +9023,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8618
9023
|
});
|
|
8619
9024
|
}))
|
|
8620
9025
|
.addCommand(new Command("create")
|
|
8621
|
-
.description("Create a draft multichannel sequence from a steps JSON file. Assign LinkedIn senders with --senders (
|
|
9026
|
+
.description("Create a draft multichannel sequence from a steps JSON file. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys); bind an Instantly email track with --email-*.")
|
|
8622
9027
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
8623
9028
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
8624
9029
|
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_reply | email_enroll | email_move | email_stop), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row. Native email sends (email_send/email_reply) also expose three reserved sender variables from the sending mailbox: {{sender_name}} (mailbox display name), {{sender_first_name}} (its first word), and {{sender_email}} (the from address); a row column of the same name WINS on collision, and a missing display name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm.MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — subject_template + body_template are REQUIRED on email_send; A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked).")
|
|
8625
9030
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp. Defaults to the channels the journey touches.")
|
|
8626
9031
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live — WhatsApp via Unipile is unofficial WhatsApp Web, so cold-initiating is an explicit ban-risk opt-in.")
|
|
8627
9032
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
8628
|
-
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids).
|
|
9033
|
+
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids). Optional at create; enroll and start require at least one when the journey has LinkedIn steps (attach later with `sequences update --senders`).")
|
|
8629
9034
|
.option("--table <id>", "Source table id whose rows supply {{column}} template values.")
|
|
8630
9035
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
8631
9036
|
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
@@ -8638,7 +9043,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8638
9043
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
8639
9044
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
8640
9045
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
8641
|
-
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
8642
9046
|
.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.")
|
|
8643
9047
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
8644
9048
|
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
|
|
@@ -8647,7 +9051,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8647
9051
|
.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.")
|
|
8648
9052
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
8649
9053
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
8650
|
-
.option("--tags <csv>", "Comma-separated workspace tags
|
|
9054
|
+
.option("--tags <csv>", "Comma-separated workspace tags linking this sequence to tables, workflows, and knowledge.")
|
|
8651
9055
|
.option("--json", "Print a JSON envelope.")
|
|
8652
9056
|
.action(async (options) => {
|
|
8653
9057
|
await handleAsyncAction("sequences create", options, () => {
|
|
@@ -8728,7 +9132,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8728
9132
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
8729
9133
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
8730
9134
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
8731
|
-
.option("--schedule-template <name>", "Name of a saved schedule template (oxygen schedules) backing the sending window.")
|
|
8732
9135
|
.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.")
|
|
8733
9136
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
8734
9137
|
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
|
|
@@ -8980,39 +9383,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8980
9383
|
.action(async (sequence, options) => {
|
|
8981
9384
|
await handleSequenceVariantsAction(sequence, options);
|
|
8982
9385
|
})));
|
|
8983
|
-
program.addCommand(new Command("schedules")
|
|
8984
|
-
.description("Named, reusable sending-schedule templates (timezone + weekdays + from/to). Reference one from a sequence via --schedule-template so a single 'US business hours' window backs many sequences.")
|
|
8985
|
-
.addCommand(new Command("list")
|
|
8986
|
-
.description("List the org's saved schedule templates.")
|
|
8987
|
-
.option("--json", "Print a JSON envelope.")
|
|
8988
|
-
.action(async (options) => {
|
|
8989
|
-
await handleAsyncAction("schedules list", options, () => requestOxygen("/api/cli/schedules"));
|
|
8990
|
-
}))
|
|
8991
|
-
.addCommand(new Command("create")
|
|
8992
|
-
.description("Create (or update by name) a schedule template. Partial windows get sensible defaults.")
|
|
8993
|
-
.requiredOption("--name <name>", "Unique template name (e.g. 'US business hours').")
|
|
8994
|
-
.requiredOption("--timezone <tz>", "IANA timezone (e.g. America/New_York).")
|
|
8995
|
-
.option("--start <HH:MM>", "Inclusive daily start time.", "09:00")
|
|
8996
|
-
.option("--end <HH:MM>", "Exclusive daily end time.", "17:00")
|
|
8997
|
-
.option("--days <csv>", "Allowed ISO weekdays, 1=Mon..7=Sun (e.g. 1,2,3,4,5). Omit → weekdays default.")
|
|
8998
|
-
.option("--json", "Print a JSON envelope.")
|
|
8999
|
-
.action(async (options) => {
|
|
9000
|
-
await handleAsyncAction("schedules create", options, () => {
|
|
9001
|
-
const days = readCsvOption(options.days).map((d) => Number(d)).filter((d) => Number.isInteger(d));
|
|
9002
|
-
return requestOxygen("/api/cli/schedules", {
|
|
9003
|
-
method: "POST",
|
|
9004
|
-
body: {
|
|
9005
|
-
name: readOption(options.name),
|
|
9006
|
-
definition: {
|
|
9007
|
-
timezone: readOption(options.timezone),
|
|
9008
|
-
...(readOption(options.start) ? { start: readOption(options.start) } : {}),
|
|
9009
|
-
...(readOption(options.end) ? { end: readOption(options.end) } : {}),
|
|
9010
|
-
...(days.length > 0 ? { days } : {}),
|
|
9011
|
-
},
|
|
9012
|
-
},
|
|
9013
|
-
});
|
|
9014
|
-
});
|
|
9015
|
-
})));
|
|
9016
9386
|
program.addCommand(new Command("suppressions")
|
|
9017
9387
|
.description("Do-not-contact + email blocklist. People/multichannel: lead provider ids the sequencer enroller skips at plan time (list | add | remove); list rows carry a resolved identity (name / picture / profile URL) when the workspace knows the person. Email blocklist (Instantly parity): addresses + whole-domain blocks the native email dispatcher skips before sending (addresses | domains | remove-address | remove-domain). `import` bulk-loads a mixed file: emails, bare domains, LinkedIn profile URLs, and LinkedIn member ids. Consumes 0 credits.")
|
|
9018
9388
|
.addCommand(new Command("list")
|
|
@@ -9463,6 +9833,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9463
9833
|
body: { mailboxes: parsed.mailboxes ?? [] },
|
|
9464
9834
|
});
|
|
9465
9835
|
});
|
|
9836
|
+
}))
|
|
9837
|
+
.addCommand(new Command("tag")
|
|
9838
|
+
.description("Replace a sending mailbox's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the mailboxes behind one campaign tag. See `oxygen tags list` for the vocabulary.")
|
|
9839
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
9840
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
9841
|
+
.option("--json", "Print a JSON envelope.")
|
|
9842
|
+
.action(async (mailbox, options) => {
|
|
9843
|
+
await handleAsyncAction("mailboxes tag", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/tags`, {
|
|
9844
|
+
method: "POST",
|
|
9845
|
+
body: { tags: splitCommaList(options.tags) },
|
|
9846
|
+
}));
|
|
9466
9847
|
}))
|
|
9467
9848
|
.addCommand(new Command("status")
|
|
9468
9849
|
.description("Set a sending mailbox's status. Pausing/disabling takes the inbox out of the rotation pool without losing its warmup state.")
|
|
@@ -10084,10 +10465,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10084
10465
|
.addCommand(new Command("list")
|
|
10085
10466
|
.description("List workflow automations. Archived workflows are hidden unless you pass --include-archived.")
|
|
10086
10467
|
.option("--include-archived", "Also list archived workflows, which are hidden by default.")
|
|
10468
|
+
.option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
|
|
10087
10469
|
.option("--json", "Print a JSON envelope.")
|
|
10088
10470
|
.action(async (options) => {
|
|
10089
10471
|
await handleAsyncAction("workflows list", options, async () => {
|
|
10090
|
-
const
|
|
10472
|
+
const params = new URLSearchParams();
|
|
10473
|
+
if (options.includeArchived)
|
|
10474
|
+
params.set("include_archived", "true");
|
|
10475
|
+
const tag = readOption(options.tag);
|
|
10476
|
+
if (tag)
|
|
10477
|
+
params.set("tag", tag);
|
|
10478
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
10479
|
+
const data = await requestOxygen(`/api/cli/workflows${qs}`);
|
|
10091
10480
|
writeDisabledWorkflowNotices(data);
|
|
10092
10481
|
return data;
|
|
10093
10482
|
});
|
|
@@ -11034,6 +11423,133 @@ function waitForWorkflowRun(runId, options) {
|
|
|
11034
11423
|
},
|
|
11035
11424
|
});
|
|
11036
11425
|
}
|
|
11426
|
+
function isTerminalCopilotTurnStatus(status) {
|
|
11427
|
+
// Terminal here means "stop streaming this turn": completed/failed/cancelled
|
|
11428
|
+
// are final, and awaiting_input parks the turn on a human decision (an
|
|
11429
|
+
// approval) that the caller surfaces rather than polling through.
|
|
11430
|
+
return status === "completed" || status === "failed"
|
|
11431
|
+
|| status === "cancelled" || status === "awaiting_input";
|
|
11432
|
+
}
|
|
11433
|
+
function readCopilotTurnStatus(data, turnId) {
|
|
11434
|
+
const turns = Array.isArray(data.turns) ? data.turns : [];
|
|
11435
|
+
for (const turn of turns) {
|
|
11436
|
+
if (isRecord(turn) && turn.id === turnId) {
|
|
11437
|
+
return typeof turn.status === "string" ? turn.status : null;
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
return null;
|
|
11441
|
+
}
|
|
11442
|
+
function readCopilotTurnCreditsCaptured(data, turnId) {
|
|
11443
|
+
const turns = Array.isArray(data.turns) ? data.turns : [];
|
|
11444
|
+
for (const turn of turns) {
|
|
11445
|
+
if (isRecord(turn) && turn.id === turnId && typeof turn.credits_captured === "number") {
|
|
11446
|
+
return turn.credits_captured;
|
|
11447
|
+
}
|
|
11448
|
+
}
|
|
11449
|
+
return null;
|
|
11450
|
+
}
|
|
11451
|
+
// The session-detail payload carries no top-level `credits` block, so the shared
|
|
11452
|
+
// writeCreditsReceipt (which handleAsyncAction runs on the returned data) is a
|
|
11453
|
+
// no-op for `copilot send`. Mirror the sent turn's own captured spend as one
|
|
11454
|
+
// stderr line so a terminal still shows what the turn cost.
|
|
11455
|
+
function writeCopilotTurnCreditsSummary(data, turnId) {
|
|
11456
|
+
const captured = readCopilotTurnCreditsCaptured(data, turnId);
|
|
11457
|
+
if (captured === null)
|
|
11458
|
+
return;
|
|
11459
|
+
process.stderr.write(`credits used this turn: ${captured.toLocaleString("en-US")}\n`);
|
|
11460
|
+
}
|
|
11461
|
+
// Print one appended copilot event to stderr as it streams in. assistant_delta
|
|
11462
|
+
// text is written verbatim — no prefix, no trailing newline — so the model's
|
|
11463
|
+
// output flows as one continuous block; every other kind is a one-line human
|
|
11464
|
+
// notice. billing_settled is intentionally silent (its spend lands in the
|
|
11465
|
+
// end-of-turn credits summary); tool_call_* surface as brief progress so a turn
|
|
11466
|
+
// that pauses on a slow capability does not look frozen.
|
|
11467
|
+
function printCopilotEvent(event, sessionId) {
|
|
11468
|
+
if (!isRecord(event))
|
|
11469
|
+
return;
|
|
11470
|
+
const kind = typeof event.kind === "string" ? event.kind : "";
|
|
11471
|
+
const payload = isRecord(event.payload) ? event.payload : {};
|
|
11472
|
+
switch (kind) {
|
|
11473
|
+
case "assistant_delta": {
|
|
11474
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
11475
|
+
if (text)
|
|
11476
|
+
process.stderr.write(text);
|
|
11477
|
+
return;
|
|
11478
|
+
}
|
|
11479
|
+
case "tool_call_started": {
|
|
11480
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "tool";
|
|
11481
|
+
process.stderr.write(`\n[tool] ${capability}…\n`);
|
|
11482
|
+
return;
|
|
11483
|
+
}
|
|
11484
|
+
case "tool_call_finished": {
|
|
11485
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "tool";
|
|
11486
|
+
const summary = typeof payload.summary === "string" ? payload.summary : "";
|
|
11487
|
+
process.stderr.write(`[tool] ${capability} ${payload.ok === true ? "ok" : "failed"}${summary ? ` — ${summary}` : ""}\n`);
|
|
11488
|
+
return;
|
|
11489
|
+
}
|
|
11490
|
+
case "approval_requested": {
|
|
11491
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "action";
|
|
11492
|
+
const preview = typeof payload.preview === "string" ? payload.preview : "";
|
|
11493
|
+
const estimated = typeof payload.estimated_credits === "number" ? payload.estimated_credits : null;
|
|
11494
|
+
const approvalId = typeof payload.approval_id === "string" ? payload.approval_id : "";
|
|
11495
|
+
process.stderr.write(`\n[approval required] ${capability}${preview ? ` — ${preview}` : ""}`
|
|
11496
|
+
+ `${estimated !== null ? ` (~${estimated.toLocaleString("en-US")} credits)` : ""}. `
|
|
11497
|
+
+ `Approve with: ${resolveCliBinaryName()} copilot approve ${sessionId} ${approvalId}\n`);
|
|
11498
|
+
return;
|
|
11499
|
+
}
|
|
11500
|
+
case "fallback_engaged": {
|
|
11501
|
+
const from = typeof payload.from === "string" ? payload.from : "?";
|
|
11502
|
+
const to = typeof payload.to === "string" ? payload.to : "?";
|
|
11503
|
+
const reason = typeof payload.reason === "string" ? payload.reason : "";
|
|
11504
|
+
process.stderr.write(`[model fallback] ${from} → ${to}${reason ? ` (${reason})` : ""}\n`);
|
|
11505
|
+
return;
|
|
11506
|
+
}
|
|
11507
|
+
case "error": {
|
|
11508
|
+
const code = typeof payload.code === "string" ? payload.code : "error";
|
|
11509
|
+
const message = typeof payload.message === "string" ? payload.message : "";
|
|
11510
|
+
process.stderr.write(`[error] ${code}${message ? `: ${message}` : ""}\n`);
|
|
11511
|
+
return;
|
|
11512
|
+
}
|
|
11513
|
+
default:
|
|
11514
|
+
return;
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
// `copilot send` is the CLI's one streaming wait surface. It reuses the shared
|
|
11518
|
+
// waitForCliRun poll loop (timeout/interval parsing, deadline, bounded sleep,
|
|
11519
|
+
// typed timeout throw) but injects a fetch that GETs the session detail after a
|
|
11520
|
+
// moving `after_seq` cursor, prints each newly appended event to stderr, and
|
|
11521
|
+
// advances the cursor to latest_seq. The SENT TURN's status — not the session's
|
|
11522
|
+
// — is the terminal signal, so fetchRun returns a record whose `status` mirrors
|
|
11523
|
+
// that turn while shapeTerminal returns the last full GET payload for scripts.
|
|
11524
|
+
function waitForCopilotTurn(options) {
|
|
11525
|
+
let cursor = options.initialSeq;
|
|
11526
|
+
let latestData = {};
|
|
11527
|
+
return waitForCliRun({
|
|
11528
|
+
runId: options.turnId,
|
|
11529
|
+
requestedTimeoutSeconds: options.requestedTimeoutSeconds,
|
|
11530
|
+
requestedIntervalSeconds: options.requestedIntervalSeconds,
|
|
11531
|
+
defaultTimeoutSeconds: COPILOT_SEND_DEFAULT_TIMEOUT_SECONDS,
|
|
11532
|
+
defaultIntervalSeconds: COPILOT_SEND_DEFAULT_INTERVAL_SECONDS,
|
|
11533
|
+
fetchRun: async () => {
|
|
11534
|
+
const query = new URLSearchParams({ after_seq: String(cursor) });
|
|
11535
|
+
latestData = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(options.sessionId)}?${query.toString()}`);
|
|
11536
|
+
const events = Array.isArray(latestData.events) ? latestData.events : [];
|
|
11537
|
+
for (const event of events)
|
|
11538
|
+
printCopilotEvent(event, options.sessionId);
|
|
11539
|
+
const latestSeq = latestData.latest_seq;
|
|
11540
|
+
if (typeof latestSeq === "number" && Number.isFinite(latestSeq))
|
|
11541
|
+
cursor = latestSeq;
|
|
11542
|
+
// waitForCliRun reads terminality off `status`; mirror the sent turn's
|
|
11543
|
+
// status there while leaving latestData itself untouched for shapeTerminal.
|
|
11544
|
+
return { ...latestData, status: readCopilotTurnStatus(latestData, options.turnId) };
|
|
11545
|
+
},
|
|
11546
|
+
isTerminal: isTerminalCopilotTurnStatus,
|
|
11547
|
+
shapeTerminal: () => latestData,
|
|
11548
|
+
timeoutCode: "copilot_send_timeout",
|
|
11549
|
+
timeoutMessage: "Timed out waiting for the copilot turn to finish.",
|
|
11550
|
+
timeoutDetailIdKey: "turn_id",
|
|
11551
|
+
});
|
|
11552
|
+
}
|
|
11037
11553
|
function workflowTemplateActionBody(templateId, options) {
|
|
11038
11554
|
const inputJson = readOption(options.inputJson);
|
|
11039
11555
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
@@ -16344,9 +16860,6 @@ function readSequenceSettings(options) {
|
|
|
16344
16860
|
const prioritization = readOption(options.sequencePrioritization);
|
|
16345
16861
|
if (prioritization)
|
|
16346
16862
|
settings.sequence_prioritization = prioritization;
|
|
16347
|
-
const scheduleTemplate = readOption(options.scheduleTemplate);
|
|
16348
|
-
if (scheduleTemplate)
|
|
16349
|
-
settings.schedule_template = scheduleTemplate;
|
|
16350
16863
|
const espMatching = readOption(options.espMatching);
|
|
16351
16864
|
if (espMatching)
|
|
16352
16865
|
settings.esp_matching = espMatching;
|
|
@@ -16408,4 +16921,23 @@ process.stdout.on("error", (error) => {
|
|
|
16408
16921
|
process.stderr.write(`stdout error: ${error.message}\n`);
|
|
16409
16922
|
process.exit(1);
|
|
16410
16923
|
});
|
|
16411
|
-
|
|
16924
|
+
const program = createProgram();
|
|
16925
|
+
program.exitOverride();
|
|
16926
|
+
try {
|
|
16927
|
+
await program.parseAsync(process.argv);
|
|
16928
|
+
}
|
|
16929
|
+
catch (error) {
|
|
16930
|
+
if (error instanceof CommanderError) {
|
|
16931
|
+
process.exitCode = error.exitCode;
|
|
16932
|
+
}
|
|
16933
|
+
else {
|
|
16934
|
+
throw error;
|
|
16935
|
+
}
|
|
16936
|
+
}
|
|
16937
|
+
// Commander can emit >16 KiB of grouped help. When stdout is a child-process
|
|
16938
|
+
// pipe, wait for the queued write callback before Node exits or the tail of
|
|
16939
|
+
// `oxygen --help` can be truncated at the pipe high-water mark.
|
|
16940
|
+
await Promise.all([flushWritable(process.stdout), flushWritable(process.stderr)]);
|
|
16941
|
+
function flushWritable(stream) {
|
|
16942
|
+
return new Promise((resolve) => stream.write("", () => resolve()));
|
|
16943
|
+
}
|