@oxygen-agent/cli 1.353.2 → 1.365.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 +321 -145
- package/node_modules/@oxygen/shared/dist/billing.d.ts +32 -10
- package/node_modules/@oxygen/shared/dist/billing.js +79 -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 +89 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +182 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/linkedin-mentions.d.ts +37 -0
- package/node_modules/@oxygen/shared/dist/linkedin-mentions.js +157 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +1 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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,7 +636,7 @@ 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),
|
|
@@ -819,8 +823,7 @@ function buildPublishingPostsListPath(options) {
|
|
|
819
823
|
const filters = {
|
|
820
824
|
status: options.status,
|
|
821
825
|
approval_status: options.approvalStatus,
|
|
822
|
-
|
|
823
|
-
campaign_id: options.campaign,
|
|
826
|
+
tag: options.tag,
|
|
824
827
|
provider: options.provider,
|
|
825
828
|
limit: options.limit,
|
|
826
829
|
};
|
|
@@ -844,8 +847,18 @@ function buildPublishingMentionsResolveBody(options) {
|
|
|
844
847
|
if (!text && identifiers.length === 0) {
|
|
845
848
|
throw new OxygenError("invalid_request", "Pass --text/--text-file or at least one --identifier.", { exitCode: 1 });
|
|
846
849
|
}
|
|
850
|
+
const source = readOption(options.source);
|
|
851
|
+
if (source && source !== "sender" && source !== "scraper") {
|
|
852
|
+
throw new OxygenError("invalid_request", "--source must be sender or scraper.", { exitCode: 1 });
|
|
853
|
+
}
|
|
854
|
+
if ((source ?? "sender") === "sender" && !readOption(options.account)) {
|
|
855
|
+
throw new OxygenError("invalid_request", "--account is required when resolving through a sender.", { exitCode: 1 });
|
|
856
|
+
}
|
|
847
857
|
return {
|
|
848
|
-
account: options.account,
|
|
858
|
+
...(options.account ? { account: options.account } : {}),
|
|
859
|
+
...(source ? { source } : {}),
|
|
860
|
+
...(options.approved ? { approved: true } : {}),
|
|
861
|
+
...(options.maxCredits ? { max_credits: Number(options.maxCredits) } : {}),
|
|
849
862
|
...(text ? { text } : {}),
|
|
850
863
|
...(identifiers.length > 0 ? {
|
|
851
864
|
identifiers: identifiers.map((value) => ({
|
|
@@ -1155,22 +1168,7 @@ function buildPublishingDraftsAcceptBody(options) {
|
|
|
1155
1168
|
body.timezone = timezone;
|
|
1156
1169
|
return body;
|
|
1157
1170
|
}
|
|
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) {
|
|
1171
|
+
function buildPublishingTagsBody(options) {
|
|
1174
1172
|
const add = splitCommaList(options.add);
|
|
1175
1173
|
const remove = splitCommaList(options.remove);
|
|
1176
1174
|
if (add.length === 0 && remove.length === 0) {
|
|
@@ -1209,14 +1207,12 @@ function buildPublishingImportBody(options) {
|
|
|
1209
1207
|
throw new OxygenError("conflicting_flags", "Pass either --dry-run or --approved, not both.", { exitCode: 1 });
|
|
1210
1208
|
}
|
|
1211
1209
|
const contents = readPublishingTextFile(file, "--file");
|
|
1212
|
-
const
|
|
1213
|
-
const labels = splitCommaList(options.label);
|
|
1210
|
+
const tags = splitCommaList(options.tags);
|
|
1214
1211
|
const isJson = file.trim().toLowerCase().endsWith(".json");
|
|
1215
1212
|
return {
|
|
1216
1213
|
...(isJson ? { rows: readPublishingImportRows(contents) } : { csv: contents }),
|
|
1217
1214
|
dry_run: options.approved !== true,
|
|
1218
|
-
...(
|
|
1219
|
-
...(labels.length > 0 ? { labels } : {}),
|
|
1215
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
1220
1216
|
};
|
|
1221
1217
|
}
|
|
1222
1218
|
function readPublishingImportRows(contents) {
|
|
@@ -1269,12 +1265,10 @@ function buildPublishingAmplificationCreateBody(options) {
|
|
|
1269
1265
|
};
|
|
1270
1266
|
}
|
|
1271
1267
|
function publishingAmplificationScope(options) {
|
|
1272
|
-
const
|
|
1273
|
-
const label = readOption(options.label);
|
|
1268
|
+
const tag = readOption(options.tag);
|
|
1274
1269
|
const post = readOption(options.post);
|
|
1275
1270
|
return {
|
|
1276
|
-
...(
|
|
1277
|
-
...(label ? { scope_label: label } : {}),
|
|
1271
|
+
...(tag ? { scope_tag: tag } : {}),
|
|
1278
1272
|
...(post ? { scope_post_id: post } : {}),
|
|
1279
1273
|
};
|
|
1280
1274
|
}
|
|
@@ -2314,8 +2308,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2314
2308
|
.addCommand(new Command("mentions")
|
|
2315
2309
|
.description("Resolve LinkedIn identities for a publish-faithful post preview.")
|
|
2316
2310
|
.addCommand(new Command("resolve")
|
|
2317
|
-
.description("Resolve @handles, LinkedIn profile/company links, provider ids, and URNs through a connected sender (notify=false).")
|
|
2318
|
-
.
|
|
2311
|
+
.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).")
|
|
2312
|
+
.option("--account <account>", "LinkedIn sender id, connection id, or Unipile account id. Required unless --source scraper.")
|
|
2313
|
+
.option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup, ~10 credits per identity, needs --approved --max-credits).")
|
|
2314
|
+
.option("--approved", "Approve the paid scraper lookups (required with --source scraper).")
|
|
2315
|
+
.option("--max-credits <n>", "Credit ceiling for scraper lookups (required with --source scraper).")
|
|
2319
2316
|
.option("--text <text>", "Post text containing @identifiers or LinkedIn URLs.")
|
|
2320
2317
|
.option("--text-file <path>", "Read post text from a local file.")
|
|
2321
2318
|
.option("--identifier <value...>", "Explicit provider id, public identifier, LinkedIn URL, or URN (repeat values after the flag).")
|
|
@@ -2334,8 +2331,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2334
2331
|
.description("List scheduled posts.")
|
|
2335
2332
|
.option("--status <status>", "Filter by draft, scheduled, queued, publishing, published, failed, or canceled.")
|
|
2336
2333
|
.option("--approval-status <status>", "Filter by draft, needs_approval, approved, or rejected.")
|
|
2337
|
-
.option("--
|
|
2338
|
-
.option("--campaign <campaign_id>", "Only posts in this campaign.")
|
|
2334
|
+
.option("--tag <tag>", "Only posts carrying this workspace tag.")
|
|
2339
2335
|
.option("--provider <provider>", "Filter by provider: linkedin, x, instagram, tiktok, facebook, or youtube.")
|
|
2340
2336
|
.option("--limit <n>", "Maximum posts to return.")
|
|
2341
2337
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2612,69 +2608,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2612
2608
|
body: buildPublishingMediaUploadedBody(options),
|
|
2613
2609
|
}));
|
|
2614
2610
|
})))
|
|
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.")
|
|
2611
|
+
.addCommand(new Command("tags")
|
|
2612
|
+
.description("Apply unified workspace tags to scheduled posts; tags are never published.")
|
|
2668
2613
|
.addCommand(new Command("set")
|
|
2669
|
-
.description("Add and/or remove
|
|
2614
|
+
.description("Add and/or remove workspace tags on a post.")
|
|
2670
2615
|
.argument("<post_id>", "Scheduled post id.")
|
|
2671
|
-
.option("--add <
|
|
2672
|
-
.option("--remove <
|
|
2616
|
+
.option("--add <tags>", "Comma-separated workspace tags to add.")
|
|
2617
|
+
.option("--remove <tags>", "Comma-separated workspace tags to remove.")
|
|
2673
2618
|
.option("--json", "Print a JSON envelope.")
|
|
2674
2619
|
.action(async (postId, options) => {
|
|
2675
|
-
await handleAsyncAction("publishing
|
|
2620
|
+
await handleAsyncAction("publishing tags set", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/tags`, {
|
|
2676
2621
|
method: "POST",
|
|
2677
|
-
body:
|
|
2622
|
+
body: buildPublishingTagsBody(options),
|
|
2678
2623
|
}));
|
|
2679
2624
|
})))
|
|
2680
2625
|
.addCommand(new Command("analytics")
|
|
@@ -2713,8 +2658,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2713
2658
|
.requiredOption("--file <path>", "Path to a .csv (header row) or .json file ([rows] or { rows: [...] }).")
|
|
2714
2659
|
.option("--dry-run", "Parse, lint, and report without writing. Default.")
|
|
2715
2660
|
.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.")
|
|
2661
|
+
.option("--tags <tags>", "Comma-separated workspace tags for every imported post.")
|
|
2718
2662
|
.option("--json", "Print a JSON envelope.")
|
|
2719
2663
|
.action(async (options) => {
|
|
2720
2664
|
await handleAsyncAction("publishing import", options, () => requestOxygen("/api/cli/publishing/import", {
|
|
@@ -2733,9 +2677,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2733
2677
|
.addCommand(new Command("create")
|
|
2734
2678
|
.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
2679
|
.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).")
|
|
2680
|
+
.requiredOption("--scope <kind>", "What it amplifies: all, tag, or post.")
|
|
2681
|
+
.option("--tag <tag>", "Workspace tag to scope to (with --scope tag).")
|
|
2739
2682
|
.option("--post <post_id>", "Post to scope to (with --scope post).")
|
|
2740
2683
|
.requiredOption("--senders <ids>", "Comma-separated connected sender account ids that will engage.")
|
|
2741
2684
|
.requiredOption("--actions <list>", "Comma-separated actions: reaction, comment.")
|
|
@@ -2878,7 +2821,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2878
2821
|
.description("Agent-native CRM object setup and metadata commands.")
|
|
2879
2822
|
.addCommand(new Command("setup")
|
|
2880
2823
|
.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.")
|
|
2824
|
+
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
2882
2825
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
2883
2826
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
2884
2827
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
@@ -4560,7 +4503,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4560
4503
|
}));
|
|
4561
4504
|
program
|
|
4562
4505
|
.command("tags")
|
|
4563
|
-
.description("Workspace tags: one
|
|
4506
|
+
.description("Workspace tags: one vocabulary across publishing posts, knowledge pages, sequences, tables, workflows, and recipes. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters). Write tags on the owning surface, then `tags get <tag>` returns every carrier with deep-links.")
|
|
4564
4507
|
.addCommand(new Command("list")
|
|
4565
4508
|
.description("List every workspace tag with per-primitive counts, most-used first.")
|
|
4566
4509
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4568,7 +4511,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4568
4511
|
await handleAsyncAction("tags list", options, () => requestOxygen("/api/cli/tags"));
|
|
4569
4512
|
}))
|
|
4570
4513
|
.addCommand(new Command("get")
|
|
4571
|
-
.description("Everything carrying one tag — knowledge pages, sequences, tables, workflows, recipes — each with its deep-link.")
|
|
4514
|
+
.description("Everything carrying one tag — publishing posts, knowledge pages, sequences, tables, workflows, and recipes — each with its deep-link.")
|
|
4572
4515
|
.argument("<tag>", "The tag (case-insensitive).")
|
|
4573
4516
|
.option("--json", "Print a JSON envelope.")
|
|
4574
4517
|
.action(async (tag, options) => {
|
|
@@ -6007,6 +5950,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6007
5950
|
program
|
|
6008
5951
|
.command("billing")
|
|
6009
5952
|
.description("Plan and managed credit commands.")
|
|
5953
|
+
.addCommand(new Command("change")
|
|
5954
|
+
.description("Preview an upgrade or downgrade and return a Stripe confirmation link. Nothing changes until confirmed in Stripe.")
|
|
5955
|
+
.requiredOption("--to <tier>", "Target plan: starter, pro, or team.")
|
|
5956
|
+
.option("--json", "Print a JSON envelope.")
|
|
5957
|
+
.action(async (options) => {
|
|
5958
|
+
await handleAsyncAction("billing change", options, () => requestOxygen("/api/cli/billing/change", {
|
|
5959
|
+
method: "POST",
|
|
5960
|
+
body: { tier: options.to },
|
|
5961
|
+
}));
|
|
5962
|
+
}))
|
|
6010
5963
|
.addCommand(new Command("balance")
|
|
6011
5964
|
.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
5965
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6133,14 +6086,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6133
6086
|
}));
|
|
6134
6087
|
}))
|
|
6135
6088
|
.addCommand(new Command("topup")
|
|
6136
|
-
.description("Buy on-demand credits at $1.25 per 1,000
|
|
6137
|
-
.argument("[pack]", "
|
|
6089
|
+
.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.")
|
|
6090
|
+
.argument("[pack]", "Legacy pack alias in USD: 10, 25, 100, or 250.")
|
|
6091
|
+
.option("--credits <n>", "Credits to buy (8,000-200,000 in 1,000-credit increments).")
|
|
6138
6092
|
.option("--json", "Print a JSON envelope.")
|
|
6139
6093
|
.action(async (pack, options) => {
|
|
6140
|
-
|
|
6094
|
+
const credits = readOption(options.credits);
|
|
6095
|
+
await handleAsyncAction("billing topup", options, () => pack || credits
|
|
6141
6096
|
? requestOxygen("/api/cli/billing/topup", {
|
|
6142
6097
|
method: "POST",
|
|
6143
|
-
body: {
|
|
6098
|
+
body: {
|
|
6099
|
+
...(pack ? { pack } : {}),
|
|
6100
|
+
...(credits ? { credits: readPositiveNumber(credits) } : {}),
|
|
6101
|
+
},
|
|
6144
6102
|
})
|
|
6145
6103
|
: requestOxygen("/api/cli/billing/topup"));
|
|
6146
6104
|
}));
|
|
@@ -6625,6 +6583,116 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6625
6583
|
return requestOxygen(`/api/cli/agent/runs${suffix}`);
|
|
6626
6584
|
});
|
|
6627
6585
|
}));
|
|
6586
|
+
program
|
|
6587
|
+
.command("copilot")
|
|
6588
|
+
.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.")
|
|
6589
|
+
.addCommand(new Command("start")
|
|
6590
|
+
.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.")
|
|
6591
|
+
.requiredOption("--budget-credits <number>", "Required: the session inference budget in credits — your explicit approval of the maximum inference spend for the whole session.")
|
|
6592
|
+
.option("--tier <low|medium|high>", "Reasoning effort tier for the session's model.")
|
|
6593
|
+
.option("--model <id>", "Pin a specific model id instead of the journey/tier default.")
|
|
6594
|
+
.option("--per-turn-ceiling <number>", "Optional per-turn credit ceiling that caps any single turn's inference spend.")
|
|
6595
|
+
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
6596
|
+
.option("--title <text>", "Optional human title for the session.")
|
|
6597
|
+
.option("--json", "Print a JSON envelope.")
|
|
6598
|
+
.action(async (options) => {
|
|
6599
|
+
await handleAsyncAction("copilot start", options, () => {
|
|
6600
|
+
const budgetCredits = readPositiveNumber(options.budgetCredits);
|
|
6601
|
+
if (budgetCredits === undefined) {
|
|
6602
|
+
throw new OxygenError("invalid_budget", "Pass --budget-credits as a positive number of credits to approve the session's inference spend.", { exitCode: 1 });
|
|
6603
|
+
}
|
|
6604
|
+
const perTurnCeiling = readPositiveNumber(options.perTurnCeiling);
|
|
6605
|
+
const tier = readOption(options.tier);
|
|
6606
|
+
const model = readOption(options.model);
|
|
6607
|
+
const journey = readOption(options.journey);
|
|
6608
|
+
const title = readOption(options.title);
|
|
6609
|
+
const body = { budget_credits: budgetCredits };
|
|
6610
|
+
if (tier)
|
|
6611
|
+
body.tier = tier;
|
|
6612
|
+
if (model)
|
|
6613
|
+
body.model = model;
|
|
6614
|
+
if (perTurnCeiling !== undefined)
|
|
6615
|
+
body.per_turn_credit_ceiling = perTurnCeiling;
|
|
6616
|
+
if (journey)
|
|
6617
|
+
body.journey = journey;
|
|
6618
|
+
if (title)
|
|
6619
|
+
body.title = title;
|
|
6620
|
+
return requestOxygen("/api/cli/copilot/sessions", { method: "POST", body });
|
|
6621
|
+
});
|
|
6622
|
+
}))
|
|
6623
|
+
.addCommand(new Command("list")
|
|
6624
|
+
.description("List your Workspace Copilot sessions with their status, budget, and credits spent.")
|
|
6625
|
+
.option("--json", "Print a JSON envelope.")
|
|
6626
|
+
.action(async (options) => {
|
|
6627
|
+
await handleAsyncAction("copilot list", options, () => requestOxygen("/api/cli/copilot/sessions"));
|
|
6628
|
+
}))
|
|
6629
|
+
.addCommand(new Command("get")
|
|
6630
|
+
.description("Show one Workspace Copilot session: its state, turns, and event log. Use --after-seq to page through the event stream.")
|
|
6631
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6632
|
+
.option("--after-seq <n>", "Only return events after this sequence number.")
|
|
6633
|
+
.option("--events <n>", "Maximum events to return.")
|
|
6634
|
+
.option("--json", "Print a JSON envelope.")
|
|
6635
|
+
.action(async (sessionId, options) => {
|
|
6636
|
+
await handleAsyncAction("copilot get", options, () => {
|
|
6637
|
+
const query = new URLSearchParams();
|
|
6638
|
+
const afterSeq = readNonNegativeInt(options.afterSeq);
|
|
6639
|
+
const eventLimit = readPositiveInt(options.events);
|
|
6640
|
+
if (afterSeq !== undefined)
|
|
6641
|
+
query.set("after_seq", String(afterSeq));
|
|
6642
|
+
if (eventLimit !== undefined)
|
|
6643
|
+
query.set("event_limit", String(eventLimit));
|
|
6644
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
6645
|
+
return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}${suffix}`);
|
|
6646
|
+
});
|
|
6647
|
+
}))
|
|
6648
|
+
.addCommand(new Command("send")
|
|
6649
|
+
.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.")
|
|
6650
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6651
|
+
.argument("<message...>", "Message to send (all words joined with spaces).")
|
|
6652
|
+
.option("--no-wait", "Post the turn and return immediately without streaming the reply.")
|
|
6653
|
+
.option("--context <json>", "Optional JSON object of surface context to attach to the turn.")
|
|
6654
|
+
.option("--timeout-seconds <n>", "Maximum time to wait for the turn to finish. Defaults to 300.")
|
|
6655
|
+
.option("--interval-seconds <n>", "Polling interval while streaming. Defaults to 2.")
|
|
6656
|
+
.option("--json", "Print a JSON envelope.")
|
|
6657
|
+
.action(async (sessionId, messageParts, options) => {
|
|
6658
|
+
await handleAsyncAction("copilot send", options, async () => {
|
|
6659
|
+
const message = messageParts.join(" ");
|
|
6660
|
+
const context = readJsonObjectOption(options.context);
|
|
6661
|
+
const posted = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/turns`, { method: "POST", body: { message, ...(context ? { context } : {}) } });
|
|
6662
|
+
// Commander maps --no-wait to `wait: false` (default true).
|
|
6663
|
+
if (options.wait === false)
|
|
6664
|
+
return posted;
|
|
6665
|
+
const turn = isRecord(posted.turn) ? posted.turn : {};
|
|
6666
|
+
const turnId = typeof turn.id === "string" ? turn.id : "";
|
|
6667
|
+
const latestSeq = posted.latest_seq;
|
|
6668
|
+
const initialSeq = typeof latestSeq === "number" && Number.isFinite(latestSeq) ? latestSeq : 0;
|
|
6669
|
+
const finalData = await waitForCopilotTurn({
|
|
6670
|
+
sessionId,
|
|
6671
|
+
turnId,
|
|
6672
|
+
initialSeq,
|
|
6673
|
+
requestedTimeoutSeconds: options.timeoutSeconds,
|
|
6674
|
+
requestedIntervalSeconds: options.intervalSeconds,
|
|
6675
|
+
});
|
|
6676
|
+
writeCopilotTurnCreditsSummary(finalData, turnId);
|
|
6677
|
+
return finalData;
|
|
6678
|
+
});
|
|
6679
|
+
}))
|
|
6680
|
+
.addCommand(new Command("approve")
|
|
6681
|
+
.description("Approve (or, with --reject, reject) an action a Workspace Copilot session is waiting on; approving re-queues the paused turn.")
|
|
6682
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6683
|
+
.argument("<approvalId>", "Approval id from the approval_requested event.")
|
|
6684
|
+
.option("--reject", "Reject the action instead of approving it.")
|
|
6685
|
+
.option("--json", "Print a JSON envelope.")
|
|
6686
|
+
.action(async (sessionId, approvalId, options) => {
|
|
6687
|
+
await handleAsyncAction("copilot approve", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.reject ? "reject" : "approve" } }));
|
|
6688
|
+
}))
|
|
6689
|
+
.addCommand(new Command("cancel")
|
|
6690
|
+
.description("Cancel a Workspace Copilot session, stopping any in-flight turn.")
|
|
6691
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
6692
|
+
.option("--json", "Print a JSON envelope.")
|
|
6693
|
+
.action(async (sessionId, options) => {
|
|
6694
|
+
await handleAsyncAction("copilot cancel", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" }));
|
|
6695
|
+
}));
|
|
6628
6696
|
program
|
|
6629
6697
|
.command("runs")
|
|
6630
6698
|
.description("Tenant run provenance commands.")
|
|
@@ -8618,14 +8686,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8618
8686
|
});
|
|
8619
8687
|
}))
|
|
8620
8688
|
.addCommand(new Command("create")
|
|
8621
|
-
.description("Create a draft multichannel sequence from a steps JSON file. Assign LinkedIn senders with --senders (
|
|
8689
|
+
.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
8690
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
8623
8691
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
8624
8692
|
.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
8693
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp. Defaults to the channels the journey touches.")
|
|
8626
8694
|
.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
8695
|
.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).
|
|
8696
|
+
.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
8697
|
.option("--table <id>", "Source table id whose rows supply {{column}} template values.")
|
|
8630
8698
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
8631
8699
|
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
@@ -8638,7 +8706,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8638
8706
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
8639
8707
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
8640
8708
|
.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
8709
|
.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
8710
|
.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
8711
|
.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 +8714,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8647
8714
|
.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
8715
|
.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
8716
|
.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
|
|
8717
|
+
.option("--tags <csv>", "Comma-separated workspace tags linking this sequence to tables, workflows, and knowledge.")
|
|
8651
8718
|
.option("--json", "Print a JSON envelope.")
|
|
8652
8719
|
.action(async (options) => {
|
|
8653
8720
|
await handleAsyncAction("sequences create", options, () => {
|
|
@@ -8728,7 +8795,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8728
8795
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
8729
8796
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
8730
8797
|
.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
8798
|
.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
8799
|
.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
8800
|
.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 +9046,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8980
9046
|
.action(async (sequence, options) => {
|
|
8981
9047
|
await handleSequenceVariantsAction(sequence, options);
|
|
8982
9048
|
})));
|
|
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
9049
|
program.addCommand(new Command("suppressions")
|
|
9017
9050
|
.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
9051
|
.addCommand(new Command("list")
|
|
@@ -11034,6 +11067,133 @@ function waitForWorkflowRun(runId, options) {
|
|
|
11034
11067
|
},
|
|
11035
11068
|
});
|
|
11036
11069
|
}
|
|
11070
|
+
function isTerminalCopilotTurnStatus(status) {
|
|
11071
|
+
// Terminal here means "stop streaming this turn": completed/failed/cancelled
|
|
11072
|
+
// are final, and awaiting_input parks the turn on a human decision (an
|
|
11073
|
+
// approval) that the caller surfaces rather than polling through.
|
|
11074
|
+
return status === "completed" || status === "failed"
|
|
11075
|
+
|| status === "cancelled" || status === "awaiting_input";
|
|
11076
|
+
}
|
|
11077
|
+
function readCopilotTurnStatus(data, turnId) {
|
|
11078
|
+
const turns = Array.isArray(data.turns) ? data.turns : [];
|
|
11079
|
+
for (const turn of turns) {
|
|
11080
|
+
if (isRecord(turn) && turn.id === turnId) {
|
|
11081
|
+
return typeof turn.status === "string" ? turn.status : null;
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
return null;
|
|
11085
|
+
}
|
|
11086
|
+
function readCopilotTurnCreditsCaptured(data, turnId) {
|
|
11087
|
+
const turns = Array.isArray(data.turns) ? data.turns : [];
|
|
11088
|
+
for (const turn of turns) {
|
|
11089
|
+
if (isRecord(turn) && turn.id === turnId && typeof turn.credits_captured === "number") {
|
|
11090
|
+
return turn.credits_captured;
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
return null;
|
|
11094
|
+
}
|
|
11095
|
+
// The session-detail payload carries no top-level `credits` block, so the shared
|
|
11096
|
+
// writeCreditsReceipt (which handleAsyncAction runs on the returned data) is a
|
|
11097
|
+
// no-op for `copilot send`. Mirror the sent turn's own captured spend as one
|
|
11098
|
+
// stderr line so a terminal still shows what the turn cost.
|
|
11099
|
+
function writeCopilotTurnCreditsSummary(data, turnId) {
|
|
11100
|
+
const captured = readCopilotTurnCreditsCaptured(data, turnId);
|
|
11101
|
+
if (captured === null)
|
|
11102
|
+
return;
|
|
11103
|
+
process.stderr.write(`credits used this turn: ${captured.toLocaleString("en-US")}\n`);
|
|
11104
|
+
}
|
|
11105
|
+
// Print one appended copilot event to stderr as it streams in. assistant_delta
|
|
11106
|
+
// text is written verbatim — no prefix, no trailing newline — so the model's
|
|
11107
|
+
// output flows as one continuous block; every other kind is a one-line human
|
|
11108
|
+
// notice. billing_settled is intentionally silent (its spend lands in the
|
|
11109
|
+
// end-of-turn credits summary); tool_call_* surface as brief progress so a turn
|
|
11110
|
+
// that pauses on a slow capability does not look frozen.
|
|
11111
|
+
function printCopilotEvent(event, sessionId) {
|
|
11112
|
+
if (!isRecord(event))
|
|
11113
|
+
return;
|
|
11114
|
+
const kind = typeof event.kind === "string" ? event.kind : "";
|
|
11115
|
+
const payload = isRecord(event.payload) ? event.payload : {};
|
|
11116
|
+
switch (kind) {
|
|
11117
|
+
case "assistant_delta": {
|
|
11118
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
11119
|
+
if (text)
|
|
11120
|
+
process.stderr.write(text);
|
|
11121
|
+
return;
|
|
11122
|
+
}
|
|
11123
|
+
case "tool_call_started": {
|
|
11124
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "tool";
|
|
11125
|
+
process.stderr.write(`\n[tool] ${capability}…\n`);
|
|
11126
|
+
return;
|
|
11127
|
+
}
|
|
11128
|
+
case "tool_call_finished": {
|
|
11129
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "tool";
|
|
11130
|
+
const summary = typeof payload.summary === "string" ? payload.summary : "";
|
|
11131
|
+
process.stderr.write(`[tool] ${capability} ${payload.ok === true ? "ok" : "failed"}${summary ? ` — ${summary}` : ""}\n`);
|
|
11132
|
+
return;
|
|
11133
|
+
}
|
|
11134
|
+
case "approval_requested": {
|
|
11135
|
+
const capability = typeof payload.capability === "string" ? payload.capability : "action";
|
|
11136
|
+
const preview = typeof payload.preview === "string" ? payload.preview : "";
|
|
11137
|
+
const estimated = typeof payload.estimated_credits === "number" ? payload.estimated_credits : null;
|
|
11138
|
+
const approvalId = typeof payload.approval_id === "string" ? payload.approval_id : "";
|
|
11139
|
+
process.stderr.write(`\n[approval required] ${capability}${preview ? ` — ${preview}` : ""}`
|
|
11140
|
+
+ `${estimated !== null ? ` (~${estimated.toLocaleString("en-US")} credits)` : ""}. `
|
|
11141
|
+
+ `Approve with: ${resolveCliBinaryName()} copilot approve ${sessionId} ${approvalId}\n`);
|
|
11142
|
+
return;
|
|
11143
|
+
}
|
|
11144
|
+
case "fallback_engaged": {
|
|
11145
|
+
const from = typeof payload.from === "string" ? payload.from : "?";
|
|
11146
|
+
const to = typeof payload.to === "string" ? payload.to : "?";
|
|
11147
|
+
const reason = typeof payload.reason === "string" ? payload.reason : "";
|
|
11148
|
+
process.stderr.write(`[model fallback] ${from} → ${to}${reason ? ` (${reason})` : ""}\n`);
|
|
11149
|
+
return;
|
|
11150
|
+
}
|
|
11151
|
+
case "error": {
|
|
11152
|
+
const code = typeof payload.code === "string" ? payload.code : "error";
|
|
11153
|
+
const message = typeof payload.message === "string" ? payload.message : "";
|
|
11154
|
+
process.stderr.write(`[error] ${code}${message ? `: ${message}` : ""}\n`);
|
|
11155
|
+
return;
|
|
11156
|
+
}
|
|
11157
|
+
default:
|
|
11158
|
+
return;
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
// `copilot send` is the CLI's one streaming wait surface. It reuses the shared
|
|
11162
|
+
// waitForCliRun poll loop (timeout/interval parsing, deadline, bounded sleep,
|
|
11163
|
+
// typed timeout throw) but injects a fetch that GETs the session detail after a
|
|
11164
|
+
// moving `after_seq` cursor, prints each newly appended event to stderr, and
|
|
11165
|
+
// advances the cursor to latest_seq. The SENT TURN's status — not the session's
|
|
11166
|
+
// — is the terminal signal, so fetchRun returns a record whose `status` mirrors
|
|
11167
|
+
// that turn while shapeTerminal returns the last full GET payload for scripts.
|
|
11168
|
+
function waitForCopilotTurn(options) {
|
|
11169
|
+
let cursor = options.initialSeq;
|
|
11170
|
+
let latestData = {};
|
|
11171
|
+
return waitForCliRun({
|
|
11172
|
+
runId: options.turnId,
|
|
11173
|
+
requestedTimeoutSeconds: options.requestedTimeoutSeconds,
|
|
11174
|
+
requestedIntervalSeconds: options.requestedIntervalSeconds,
|
|
11175
|
+
defaultTimeoutSeconds: COPILOT_SEND_DEFAULT_TIMEOUT_SECONDS,
|
|
11176
|
+
defaultIntervalSeconds: COPILOT_SEND_DEFAULT_INTERVAL_SECONDS,
|
|
11177
|
+
fetchRun: async () => {
|
|
11178
|
+
const query = new URLSearchParams({ after_seq: String(cursor) });
|
|
11179
|
+
latestData = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(options.sessionId)}?${query.toString()}`);
|
|
11180
|
+
const events = Array.isArray(latestData.events) ? latestData.events : [];
|
|
11181
|
+
for (const event of events)
|
|
11182
|
+
printCopilotEvent(event, options.sessionId);
|
|
11183
|
+
const latestSeq = latestData.latest_seq;
|
|
11184
|
+
if (typeof latestSeq === "number" && Number.isFinite(latestSeq))
|
|
11185
|
+
cursor = latestSeq;
|
|
11186
|
+
// waitForCliRun reads terminality off `status`; mirror the sent turn's
|
|
11187
|
+
// status there while leaving latestData itself untouched for shapeTerminal.
|
|
11188
|
+
return { ...latestData, status: readCopilotTurnStatus(latestData, options.turnId) };
|
|
11189
|
+
},
|
|
11190
|
+
isTerminal: isTerminalCopilotTurnStatus,
|
|
11191
|
+
shapeTerminal: () => latestData,
|
|
11192
|
+
timeoutCode: "copilot_send_timeout",
|
|
11193
|
+
timeoutMessage: "Timed out waiting for the copilot turn to finish.",
|
|
11194
|
+
timeoutDetailIdKey: "turn_id",
|
|
11195
|
+
});
|
|
11196
|
+
}
|
|
11037
11197
|
function workflowTemplateActionBody(templateId, options) {
|
|
11038
11198
|
const inputJson = readOption(options.inputJson);
|
|
11039
11199
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
@@ -16344,9 +16504,6 @@ function readSequenceSettings(options) {
|
|
|
16344
16504
|
const prioritization = readOption(options.sequencePrioritization);
|
|
16345
16505
|
if (prioritization)
|
|
16346
16506
|
settings.sequence_prioritization = prioritization;
|
|
16347
|
-
const scheduleTemplate = readOption(options.scheduleTemplate);
|
|
16348
|
-
if (scheduleTemplate)
|
|
16349
|
-
settings.schedule_template = scheduleTemplate;
|
|
16350
16507
|
const espMatching = readOption(options.espMatching);
|
|
16351
16508
|
if (espMatching)
|
|
16352
16509
|
settings.esp_matching = espMatching;
|
|
@@ -16408,4 +16565,23 @@ process.stdout.on("error", (error) => {
|
|
|
16408
16565
|
process.stderr.write(`stdout error: ${error.message}\n`);
|
|
16409
16566
|
process.exit(1);
|
|
16410
16567
|
});
|
|
16411
|
-
|
|
16568
|
+
const program = createProgram();
|
|
16569
|
+
program.exitOverride();
|
|
16570
|
+
try {
|
|
16571
|
+
await program.parseAsync(process.argv);
|
|
16572
|
+
}
|
|
16573
|
+
catch (error) {
|
|
16574
|
+
if (error instanceof CommanderError) {
|
|
16575
|
+
process.exitCode = error.exitCode;
|
|
16576
|
+
}
|
|
16577
|
+
else {
|
|
16578
|
+
throw error;
|
|
16579
|
+
}
|
|
16580
|
+
}
|
|
16581
|
+
// Commander can emit >16 KiB of grouped help. When stdout is a child-process
|
|
16582
|
+
// pipe, wait for the queued write callback before Node exits or the tail of
|
|
16583
|
+
// `oxygen --help` can be truncated at the pipe high-water mark.
|
|
16584
|
+
await Promise.all([flushWritable(process.stdout), flushWritable(process.stderr)]);
|
|
16585
|
+
function flushWritable(stream) {
|
|
16586
|
+
return new Promise((resolve) => stream.write("", () => resolve()));
|
|
16587
|
+
}
|