@oxygen-agent/cli 1.225.28 → 1.226.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.225.28
37
+ Version: 1.226.15
package/dist/index.js CHANGED
@@ -4552,9 +4552,12 @@ export function createProgram() {
4552
4552
  program.addCommand(new Command("engagement")
4553
4553
  .description("Harvest a LinkedIn post's engagers (reactors + commenters) into an enrollable CRM static list. The harvest runs as a slow, durable background drip under a dedicated conservative read budget — it never bursts and never starves the sequencer's own reads.")
4554
4554
  .addCommand(new Command("harvest")
4555
- .description("Start (or re-arm) a harvest of a post's engagers into a CRM static list you can enroll into a sequence. Engagers drip into the list over many ticks; poll `engagement status` to watch it fill. No messages are sent and no Oxygen credits are charged.")
4556
- .requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
4555
+ .description("Start (or re-arm) a harvest of a post's engagers into a CRM static list you can enroll into a sequence. Engagers drip into the list over many ticks; poll `engagement status` to watch it fill. No messages are sent. Cookieless harvests spend Oxygen credits per scraper page and require --max-credits.")
4556
+ .requiredOption("--post <social_id_or_url>", "Composite post social_id from `oxygen posts get` (NOT the activity URN), or the public LinkedIn post URL for cookieless.")
4557
4557
  .requiredOption("--list <slug>", "CRM static list slug to fill (created or reused over the people object).")
4558
+ .option("--source <source>", "cookieless|unipile. Defaults to cookieless when a public post URL is supplied.")
4559
+ .option("--post-url <url>", "Public LinkedIn post URL for cookieless harvesting when --post is a social id.")
4560
+ .option("--max-credits <credits>", "Required for cookieless harvesting; caps managed scraper spend for this armed run.")
4558
4561
  .option("--recurring", "Keep re-polling the newest pages for new engagers (default: one walk).")
4559
4562
  .option("--account <ref>", "Sender account that reads (sender id, connection id, or Unipile account id). Omit for the org default.")
4560
4563
  .option("--no-reactions", "Skip reactors.")
@@ -4565,11 +4568,17 @@ export function createProgram() {
4565
4568
  const post = readOption(options.post);
4566
4569
  const list = readOption(options.list);
4567
4570
  const account = readOption(options.account);
4571
+ const source = readOption(options.source);
4572
+ const postUrl = readOption(options.postUrl);
4573
+ const maxCredits = readPositiveNumber(options.maxCredits);
4568
4574
  return requestOxygen("/api/cli/linkedin/engagement/harvest", {
4569
4575
  method: "POST",
4570
4576
  body: {
4571
4577
  ...(post ? { post } : {}),
4572
4578
  ...(list ? { list } : {}),
4579
+ ...(source ? { source } : {}),
4580
+ ...(postUrl ? { post_url: postUrl } : {}),
4581
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
4573
4582
  ...(options.recurring ? { recurring: true } : {}),
4574
4583
  ...(account ? { account } : {}),
4575
4584
  // commander sets reactions/comments=false for --no-* flags.
@@ -4743,13 +4752,15 @@ export function createProgram() {
4743
4752
  .option("--responses-only", "Email only: only conversations with an inbound reply (never sent-only threads).")
4744
4753
  .option("--bucket <bucket>", "Email only: primary or others (superseded by --segment).")
4745
4754
  .option("--segment <segment>", "Email only: top-tab folder — primary, others, sent, warmup, or dmarc.")
4746
- .option("--status <keys>", "Email only: comma-separated status keys (e.g. interested,meeting_booked).")
4755
+ .option("--status <keys>", "Comma-separated status keys (e.g. interested,meeting_booked). Cross-channel — filters email + LinkedIn + WhatsApp by the shared taxonomy.")
4756
+ .option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Cross-channel.")
4757
+ .option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Cross-channel.")
4747
4758
  .option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) ids.")
4748
4759
  .option("--provider <providers>", "Email only: comma-separated providers (google,microsoft).")
4749
4760
  .option("--domain <domains>", "Email only: comma-separated counterpart domains to include.")
4750
4761
  .option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
4751
4762
  .option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
4752
- .option("--search <text>", "Filter by attendee name or last-message text.")
4763
+ .option("--search <text>", "Filter by attendee name or last-message text. Cross-channel.")
4753
4764
  .option("--include-archived", "Include archived conversations.")
4754
4765
  .option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
4755
4766
  .option("--json", "Print a JSON envelope.")
@@ -4771,6 +4782,8 @@ export function createProgram() {
4771
4782
  ["segment", "segment"],
4772
4783
  ["channels", "channels"],
4773
4784
  ["status", "status"],
4785
+ ["since", "since"],
4786
+ ["until", "until"],
4774
4787
  ["sequenceId", "sequence_id"],
4775
4788
  ["provider", "provider"],
4776
4789
  ["domain", "domain"],
@@ -4864,15 +4877,19 @@ export function createProgram() {
4864
4877
  });
4865
4878
  }))
4866
4879
  .addCommand(new Command("archive")
4867
- .description("Archive an email conversation (triage it out of the inbox). --unarchive restores it.")
4868
- .argument("<conversation>", "Conversation id or Zapbox thread id.")
4880
+ .description("Archive a conversation (triage it out of the inbox). --unarchive restores it. --channel linkedin/whatsapp archives a DM.")
4881
+ .argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
4882
+ .option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
4869
4883
  .option("--unarchive", "Restore the conversation instead of archiving it.")
4870
4884
  .option("--json", "Print a JSON envelope.")
4871
4885
  .action(async (conversation, options) => {
4872
- await handleAsyncAction("inbox archive", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/archive`, {
4873
- method: "POST",
4874
- body: { channel: "email", archived: !options.unarchive },
4875
- }));
4886
+ await handleAsyncAction("inbox archive", options, () => {
4887
+ const channel = readOption(options.channel) ?? "email";
4888
+ return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/archive`, {
4889
+ method: "POST",
4890
+ body: { channel, archived: !options.unarchive },
4891
+ });
4892
+ });
4876
4893
  }))
4877
4894
  .addCommand(new Command("sync")
4878
4895
  .description("Force a backstop inbox sync from Unipile for all active sender accounts (pulls recent chats + messages into the unibox).")
@@ -4941,6 +4958,36 @@ export function createProgram() {
4941
4958
  body: { channel, confirm: Boolean(options.yes) },
4942
4959
  });
4943
4960
  });
4961
+ }))
4962
+ .addCommand(new Command("bulk")
4963
+ .description("Bulk-apply one triage action — set-status / archive / unarchive — across up to 100 hand-picked conversations (the ids `inbox list` returns) in one call. Without --yes, previews the matched/would-change counts (no mutation). With --yes, applies it. --channel scopes the ids (email default; run once per channel).")
4964
+ .requiredOption("--action <action>", "set-status, archive, or unarchive.")
4965
+ .requiredOption("--ids <ids>", "Comma-separated conversation ids (the uuids `inbox list` returns). Max 100.")
4966
+ .option("--status <key>", "Required for --action set-status: the status label key to set (e.g. interested).")
4967
+ .option("--channel <channel>", "Inbox channel the ids belong to: email (default), linkedin, or whatsapp.")
4968
+ .option("--yes", "Apply the action. Without this flag, returns a preview of the counts only.")
4969
+ .option("--json", "Print a JSON envelope.")
4970
+ .action(async (options) => {
4971
+ await handleAsyncAction("inbox bulk", options, () => {
4972
+ // CLI uses hyphenated `set-status` for ergonomics; the API wants `set_status`.
4973
+ const action = (readOption(options.action) ?? "").replace(/-/g, "_");
4974
+ const channel = readOption(options.channel) ?? "email";
4975
+ const status = readOption(options.status);
4976
+ const ids = (readOption(options.ids) ?? "")
4977
+ .split(",")
4978
+ .map((id) => id.trim())
4979
+ .filter(Boolean);
4980
+ return requestOxygen("/api/cli/inbox/bulk", {
4981
+ method: "POST",
4982
+ body: {
4983
+ action,
4984
+ channel,
4985
+ conversation_ids: ids,
4986
+ ...(status ? { status } : {}),
4987
+ ...(options.yes ? { approved: true } : {}),
4988
+ },
4989
+ });
4990
+ });
4944
4991
  }))
4945
4992
  .addCommand(new Command("labels")
4946
4993
  .description("Manage inbox status labels (the Instantly-style system set + custom labels).")
@@ -7,6 +7,7 @@ export * from "./cli-envelope.js";
7
7
  export * from "./cli-result.js";
8
8
  export * from "./column-types.js";
9
9
  export * from "./credit-guidance.js";
10
+ export * from "./linkedin-post-url.js";
10
11
  export * from "./linkedin-sequences.js";
11
12
  export * from "./networks.js";
12
13
  export * from "./sequences.js";
@@ -7,6 +7,7 @@ export * from "./cli-envelope.js";
7
7
  export * from "./cli-result.js";
8
8
  export * from "./column-types.js";
9
9
  export * from "./credit-guidance.js";
10
+ export * from "./linkedin-post-url.js";
10
11
  export * from "./linkedin-sequences.js";
11
12
  export * from "./networks.js";
12
13
  export * from "./sequences.js";
@@ -0,0 +1,2 @@
1
+ export declare function isLinkedInPostUrlLike(value: string): boolean;
2
+ export declare function normalizeLinkedInPostUrl(value: string): string | null;
@@ -0,0 +1,51 @@
1
+ const LINKEDIN_HOST = "linkedin.com";
2
+ const URL_LIKE_PATTERN = /^[a-z][a-z\d+.-]*:\/\//i;
3
+ function normalizeHostname(hostname) {
4
+ return hostname.toLowerCase().replace(/\.+$/, "");
5
+ }
6
+ function isLinkedInHostname(hostname) {
7
+ const normalized = normalizeHostname(hostname);
8
+ return normalized === LINKEDIN_HOST || normalized.endsWith(`.${LINKEDIN_HOST}`);
9
+ }
10
+ function safelyDecodePath(pathname) {
11
+ try {
12
+ return decodeURIComponent(pathname);
13
+ }
14
+ catch {
15
+ return pathname;
16
+ }
17
+ }
18
+ function isLinkedInPostPath(pathname) {
19
+ const path = safelyDecodePath(pathname).toLowerCase();
20
+ return /^\/posts\/[^/]+\/?$/.test(path) || /^\/feed\/update\/urn:li:activity:[^/]+\/?$/.test(path);
21
+ }
22
+ export function isLinkedInPostUrlLike(value) {
23
+ const trimmed = value.trim();
24
+ return URL_LIKE_PATTERN.test(trimmed) || trimmed.toLowerCase().includes("linkedin.com");
25
+ }
26
+ export function normalizeLinkedInPostUrl(value) {
27
+ const trimmed = value.trim();
28
+ if (!trimmed)
29
+ return null;
30
+ let url;
31
+ try {
32
+ url = new URL(trimmed);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ if (url.protocol !== "http:" && url.protocol !== "https:")
38
+ return null;
39
+ const hostname = normalizeHostname(url.hostname);
40
+ if (!isLinkedInHostname(hostname))
41
+ return null;
42
+ if (!isLinkedInPostPath(url.pathname))
43
+ return null;
44
+ url.protocol = "https:";
45
+ url.hostname = hostname;
46
+ url.username = "";
47
+ url.password = "";
48
+ url.search = "";
49
+ url.hash = "";
50
+ return url.toString();
51
+ }
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.225.28";
1
+ export declare const OXYGEN_VERSION = "1.226.15";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.225.28";
1
+ export const OXYGEN_VERSION = "1.226.15";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  // 1.181.0: paid table action runs and background columns run require
4
4
  // approved=true in addition to max_credits; older CLIs cannot send the flag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.225.28",
3
+ "version": "1.226.15",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",