@oxygen-agent/cli 1.224.5 → 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.224.5
37
+ Version: 1.226.15
package/dist/index.js CHANGED
@@ -2189,6 +2189,7 @@ export function createProgram() {
2189
2189
  .option("--status <status>", "Filter by draft, active, or archived.")
2190
2190
  .option("--tags <csv>", "Comma-separated tags that must be present.")
2191
2191
  .option("--include-archived", "Include archived assets when no status filter is set.")
2192
+ .option("--limit <n>", "Maximum assets to return. Defaults to 100; hard cap is 500.")
2192
2193
  .option("--json", "Print a JSON envelope.")
2193
2194
  .action(async (options) => {
2194
2195
  await handleAsyncAction("context assets list", options, () => requestOxygen(`/api/cli/context/assets${contextAssetsQuery(options)}`));
@@ -4551,9 +4552,12 @@ export function createProgram() {
4551
4552
  program.addCommand(new Command("engagement")
4552
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.")
4553
4554
  .addCommand(new Command("harvest")
4554
- .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.")
4555
- .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.")
4556
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.")
4557
4561
  .option("--recurring", "Keep re-polling the newest pages for new engagers (default: one walk).")
4558
4562
  .option("--account <ref>", "Sender account that reads (sender id, connection id, or Unipile account id). Omit for the org default.")
4559
4563
  .option("--no-reactions", "Skip reactors.")
@@ -4564,11 +4568,17 @@ export function createProgram() {
4564
4568
  const post = readOption(options.post);
4565
4569
  const list = readOption(options.list);
4566
4570
  const account = readOption(options.account);
4571
+ const source = readOption(options.source);
4572
+ const postUrl = readOption(options.postUrl);
4573
+ const maxCredits = readPositiveNumber(options.maxCredits);
4567
4574
  return requestOxygen("/api/cli/linkedin/engagement/harvest", {
4568
4575
  method: "POST",
4569
4576
  body: {
4570
4577
  ...(post ? { post } : {}),
4571
4578
  ...(list ? { list } : {}),
4579
+ ...(source ? { source } : {}),
4580
+ ...(postUrl ? { post_url: postUrl } : {}),
4581
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
4572
4582
  ...(options.recurring ? { recurring: true } : {}),
4573
4583
  ...(account ? { account } : {}),
4574
4584
  // commander sets reactions/comments=false for --no-* flags.
@@ -4742,13 +4752,15 @@ export function createProgram() {
4742
4752
  .option("--responses-only", "Email only: only conversations with an inbound reply (never sent-only threads).")
4743
4753
  .option("--bucket <bucket>", "Email only: primary or others (superseded by --segment).")
4744
4754
  .option("--segment <segment>", "Email only: top-tab folder — primary, others, sent, warmup, or dmarc.")
4745
- .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.")
4746
4758
  .option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) ids.")
4747
4759
  .option("--provider <providers>", "Email only: comma-separated providers (google,microsoft).")
4748
4760
  .option("--domain <domains>", "Email only: comma-separated counterpart domains to include.")
4749
4761
  .option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
4750
4762
  .option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
4751
- .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.")
4752
4764
  .option("--include-archived", "Include archived conversations.")
4753
4765
  .option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
4754
4766
  .option("--json", "Print a JSON envelope.")
@@ -4770,6 +4782,8 @@ export function createProgram() {
4770
4782
  ["segment", "segment"],
4771
4783
  ["channels", "channels"],
4772
4784
  ["status", "status"],
4785
+ ["since", "since"],
4786
+ ["until", "until"],
4773
4787
  ["sequenceId", "sequence_id"],
4774
4788
  ["provider", "provider"],
4775
4789
  ["domain", "domain"],
@@ -4863,15 +4877,19 @@ export function createProgram() {
4863
4877
  });
4864
4878
  }))
4865
4879
  .addCommand(new Command("archive")
4866
- .description("Archive an email conversation (triage it out of the inbox). --unarchive restores it.")
4867
- .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.")
4868
4883
  .option("--unarchive", "Restore the conversation instead of archiving it.")
4869
4884
  .option("--json", "Print a JSON envelope.")
4870
4885
  .action(async (conversation, options) => {
4871
- await handleAsyncAction("inbox archive", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/archive`, {
4872
- method: "POST",
4873
- body: { channel: "email", archived: !options.unarchive },
4874
- }));
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
+ });
4875
4893
  }))
4876
4894
  .addCommand(new Command("sync")
4877
4895
  .description("Force a backstop inbox sync from Unipile for all active sender accounts (pulls recent chats + messages into the unibox).")
@@ -4940,6 +4958,36 @@ export function createProgram() {
4940
4958
  body: { channel, confirm: Boolean(options.yes) },
4941
4959
  });
4942
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
+ });
4943
4991
  }))
4944
4992
  .addCommand(new Command("labels")
4945
4993
  .description("Manage inbox status labels (the Instantly-style system set + custom labels).")
@@ -9893,6 +9941,9 @@ function contextAssetsQuery(options) {
9893
9941
  query.append("tag", tag);
9894
9942
  if (options.includeArchived)
9895
9943
  query.set("include_archived", "true");
9944
+ const limit = readPositiveInt(options.limit);
9945
+ if (limit !== undefined)
9946
+ query.set("limit", String(limit));
9896
9947
  const value = query.toString();
9897
9948
  return value ? `?${value}` : "";
9898
9949
  }
@@ -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.224.5";
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.224.5";
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.224.5",
3
+ "version": "1.226.15",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",