@oxygen-agent/cli 1.284.3 → 1.285.53

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.284.3
37
+ Version: 1.285.53
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
4
  import { OxygenError } from "@oxygen/shared";
5
+ import { readErrorMessage } from "./util.js";
5
6
  const CALLBACK_HOST = "127.0.0.1";
6
7
  const CALLBACK_PATH = "/cli/login/callback";
7
8
  const MAX_CALLBACK_BODY_BYTES = 32_000;
@@ -175,6 +176,3 @@ function closeServer(server) {
175
176
  });
176
177
  });
177
178
  }
178
- function readErrorMessage(error) {
179
- return error instanceof Error ? error.message : "unknown error";
180
- }
@@ -21,7 +21,7 @@ export type StoredCredentials = {
21
21
  export type CredentialProfile = StoredCredentials & {
22
22
  name: string;
23
23
  };
24
- export type CredentialProfilesState = {
24
+ type CredentialProfilesState = {
25
25
  activeProfile: string | null;
26
26
  profiles: CredentialProfile[];
27
27
  };
@@ -46,7 +46,6 @@ export type ProfileResolution = {
46
46
  exists: boolean;
47
47
  };
48
48
  export declare function resolveActiveProfile(env?: NodeJS.ProcessEnv): Promise<ProfileResolution>;
49
- export declare function findProfileNameByOrganizationId(organizationId: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
50
49
  export declare function pickProfileNameForIdentity(organizationId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
51
50
  name: string;
52
51
  renamed: boolean;
@@ -58,3 +57,4 @@ export declare function pickProfileNameForUserSession(userId: string, candidateS
58
57
  export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
59
58
  export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
60
59
  export declare function normalizeApiUrl(value: string): string;
60
+ export {};
@@ -43,17 +43,7 @@ export async function loadCredentials(env = process.env) {
43
43
  return parseStoredCredentialsObject(document);
44
44
  }
45
45
  export async function saveCredentials(credentials, env = process.env, options = {}) {
46
- const normalized = {
47
- token: credentials.token,
48
- apiUrl: normalizeApiUrl(credentials.apiUrl),
49
- };
50
- if (credentials.authKind)
51
- normalized.authKind = credentials.authKind;
52
- if (credentials.activeOrganization !== undefined) {
53
- normalized.activeOrganization = credentials.activeOrganization;
54
- }
55
- if (credentials.identity)
56
- normalized.identity = credentials.identity;
46
+ const normalized = normalizeStoredCredentialsRecord(credentials);
57
47
  const existing = await readSystemCredentialDocument(env);
58
48
  const profile = normalizeProfileName(options.profile ?? readEnvProfile(env) ?? existing?.activeProfile ?? DEFAULT_PROFILE_NAME);
59
49
  const profiles = existing ? readProfiles(existing) : {};
@@ -153,26 +143,12 @@ export async function resolveActiveProfile(env = process.env) {
153
143
  : "default";
154
144
  return { name: candidate, source, credentials, exists: Boolean(credentials) };
155
145
  }
156
- export async function findProfileNameByOrganizationId(organizationId, env = process.env) {
157
- const document = await readSystemCredentialDocument(env);
158
- if (!document)
159
- return null;
160
- const profiles = readProfiles(document);
161
- for (const [name, credentials] of Object.entries(profiles)) {
162
- if (credentials.identity?.organization?.id === organizationId)
163
- return name;
164
- if (credentials.activeOrganization?.id === organizationId)
165
- return name;
166
- }
167
- return null;
168
- }
169
- export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
146
+ async function pickProfileName(matches, candidateSeed, env) {
170
147
  const document = await readSystemCredentialDocument(env);
171
148
  const profiles = document ? readProfiles(document) : {};
172
- const existingForOrg = Object.entries(profiles).find(([, credentials]) => credentials.identity?.organization?.id === organizationId ||
173
- credentials.activeOrganization?.id === organizationId);
174
- if (existingForOrg)
175
- return { name: existingForOrg[0], renamed: false };
149
+ const existing = Object.entries(profiles).find(([, credentials]) => matches(credentials));
150
+ if (existing)
151
+ return { name: existing[0], renamed: false };
176
152
  const base = normalizeProfileName(candidateSeed);
177
153
  if (!profiles[base])
178
154
  return { name: base, renamed: false };
@@ -186,25 +162,13 @@ export async function pickProfileNameForIdentity(organizationId, candidateSeed,
186
162
  exitCode: 1,
187
163
  });
188
164
  }
165
+ export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
166
+ return pickProfileName((credentials) => credentials.identity?.organization?.id === organizationId ||
167
+ credentials.activeOrganization?.id === organizationId, candidateSeed, env);
168
+ }
189
169
  export async function pickProfileNameForUserSession(userId, candidateSeed, env = process.env) {
190
- const document = await readSystemCredentialDocument(env);
191
- const profiles = document ? readProfiles(document) : {};
192
- const existingForUser = Object.entries(profiles).find(([, credentials]) => credentials.authKind === "user_session" &&
193
- credentials.identity?.user.id === userId);
194
- if (existingForUser)
195
- return { name: existingForUser[0], renamed: false };
196
- const base = normalizeProfileName(candidateSeed);
197
- if (!profiles[base])
198
- return { name: base, renamed: false };
199
- for (let suffix = 2; suffix < 1000; suffix++) {
200
- const next = `${base}-${suffix}`;
201
- if (!profiles[next])
202
- return { name: next, renamed: true };
203
- }
204
- throw new OxygenError("profile_collision", "Unable to derive a unique CLI profile name.", {
205
- details: { base },
206
- exitCode: 1,
207
- });
170
+ return pickProfileName((credentials) => credentials.authKind === "user_session" &&
171
+ credentials.identity?.user.id === userId, candidateSeed, env);
208
172
  }
209
173
  export async function switchCredentialProfile(profile, env = process.env) {
210
174
  const document = await readSystemCredentialDocument(env);
@@ -696,22 +660,25 @@ function parseStoredOrganization(value) {
696
660
  : null;
697
661
  return { id, slug, name };
698
662
  }
663
+ function normalizeStoredCredentialsRecord(credentials) {
664
+ const normalized = {
665
+ token: credentials.token,
666
+ apiUrl: normalizeApiUrl(credentials.apiUrl),
667
+ };
668
+ if (credentials.authKind)
669
+ normalized.authKind = credentials.authKind;
670
+ if (credentials.activeOrganization !== undefined) {
671
+ normalized.activeOrganization = credentials.activeOrganization;
672
+ }
673
+ if (credentials.identity)
674
+ normalized.identity = credentials.identity;
675
+ return normalized;
676
+ }
699
677
  function readProfiles(document) {
700
678
  const profiles = {};
701
679
  const rawProfiles = document.profiles ?? {};
702
680
  for (const [name, credentials] of Object.entries(rawProfiles)) {
703
- const normalized = {
704
- token: credentials.token,
705
- apiUrl: normalizeApiUrl(credentials.apiUrl),
706
- };
707
- if (credentials.authKind)
708
- normalized.authKind = credentials.authKind;
709
- if (credentials.activeOrganization !== undefined) {
710
- normalized.activeOrganization = credentials.activeOrganization;
711
- }
712
- if (credentials.identity)
713
- normalized.identity = credentials.identity;
714
- profiles[normalizeProfileName(name)] = normalized;
681
+ profiles[normalizeProfileName(name)] = normalizeStoredCredentialsRecord(credentials);
715
682
  }
716
683
  if (Object.keys(profiles).length === 0) {
717
684
  const defaultCredentials = parseStoredCredentialsObject(document);
@@ -1,10 +1,9 @@
1
1
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { defaultApiUrl, loadCredentials } from "./credentials.js";
4
- import { resolveCliUpdateGuidance } from "./runtime.js";
4
+ import { isProdApiUrl, resolveCliUpdateGuidance } from "./runtime.js";
5
5
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
6
6
  const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
7
- const PROD_API_HOSTNAME = "oxygen-agent.com";
8
7
  const cliCompatibilityCheckKeys = new Set();
9
8
  export async function requestOxygen(// skipcq: JS-R1005
10
9
  path, options = {}) {
@@ -175,14 +174,6 @@ function warnIfCliIsOlderThanApi(serverVersion, apiUrl) {
175
174
  process.stderr.write(`[${guidance.binaryName}] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
176
175
  + `${guidance.warningInstruction}\n`);
177
176
  }
178
- function isProdApiUrl(apiUrl) {
179
- try {
180
- return new URL(apiUrl).hostname === PROD_API_HOSTNAME;
181
- }
182
- catch {
183
- return false;
184
- }
185
- }
186
177
  function assertCliMeetsMinimumApiVersion(compatibility, options, apiUrl) {
187
178
  if (options.enforceMinimumCliVersion === false)
188
179
  return;
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, upda
24
24
  import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
25
25
  import { resolveCliBinaryName } from "./runtime.js";
26
26
  import { updateCli } from "./update.js";
27
+ import { isRecord, readErrorMessage, readOption } from "./util.js";
27
28
  function buildFindBody(capability, options) {
28
29
  const body = { capability };
29
30
  const set = (key, value) => {
@@ -795,13 +796,24 @@ function readPublishingPostText(options, required) {
795
796
  if (inlineText && textFile) {
796
797
  throw new OxygenError("conflicting_flags", "Pass either --text or --text-file, not both.", { exitCode: 1 });
797
798
  }
798
- const text = textFile ? readFileSync(resolve(textFile), "utf8").trimEnd() : inlineText;
799
+ const text = textFile ? readPublishingTextFile(textFile) : inlineText;
799
800
  if (text?.trim())
800
801
  return text;
801
802
  if (!required)
802
803
  return null;
803
804
  throw new OxygenError("invalid_request", "Pass --text or --text-file.", { exitCode: 1 });
804
805
  }
806
+ // Read --text-file, sanitizing the OS error so a missing/unreadable file reports
807
+ // only the basename the user passed — never the resolved absolute filesystem path
808
+ // Node's ENOENT would otherwise leak into the CLI error output.
809
+ function readPublishingTextFile(textFile) {
810
+ try {
811
+ return readFileSync(resolve(textFile), "utf8").trimEnd();
812
+ }
813
+ catch {
814
+ throw new OxygenError("text_file_unreadable", `Couldn't read --text-file '${basename(textFile)}'. Check the path exists and is readable.`, { exitCode: 1 });
815
+ }
816
+ }
805
817
  function buildCrmRelationshipUpsertBody(object, rowId, options) {
806
818
  return {
807
819
  object,
@@ -5727,6 +5739,23 @@ export function createProgram() {
5727
5739
  .option("--json", "Print a JSON envelope.")
5728
5740
  .action(async (id, options) => {
5729
5741
  await handleAsyncAction("senders health", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/health`));
5742
+ }))
5743
+ .addCommand(new Command("inmail-balance")
5744
+ .description("Show remaining LinkedIn InMail credits for a sender (Premium / Sales Navigator / Recruiter each hold a separate pool). Run before enrolling an `inmail` sequencer step so a campaign does not stall on an account with zero credits. Read-only — not billed. <id> defaults to the org's primary sender.")
5745
+ .argument("[id]", "Sender account id, connection id, or Unipile account id. Defaults to the org's primary sender.")
5746
+ .option("--service <service>", "Narrow to one credit pool: classic | sales_navigator | recruiter. Defaults to every subscribed service.")
5747
+ .option("--json", "Print a JSON envelope.")
5748
+ .action(async (id, options) => {
5749
+ await handleAsyncAction("senders inmail-balance", options, () => {
5750
+ const query = new URLSearchParams();
5751
+ if (id)
5752
+ query.set("account", id);
5753
+ const service = readOption(options.service);
5754
+ if (service)
5755
+ query.set("service", service);
5756
+ const suffix = query.toString();
5757
+ return requestOxygen(`/api/cli/linkedin/inmail-balance${suffix ? `?${suffix}` : ""}`);
5758
+ });
5730
5759
  }))
5731
5760
  .addCommand(new Command("sync")
5732
5761
  .description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
@@ -6267,7 +6296,7 @@ export function createProgram() {
6267
6296
  .action(async (options) => {
6268
6297
  await handleAsyncAction("linkedin invitations withdraw", options, () => {
6269
6298
  const ids = (options.ids ?? "").split(",").map((id) => id.trim()).filter(Boolean);
6270
- const olderThan = readPositiveInteger(options.olderThan);
6299
+ const olderThan = readPositiveInt(options.olderThan);
6271
6300
  const account = readOption(options.account);
6272
6301
  return requestOxygen("/api/cli/linkedin/connections/invite/withdraw", {
6273
6302
  method: "POST",
@@ -6288,7 +6317,7 @@ export function createProgram() {
6288
6317
  .action(async (options) => {
6289
6318
  await handleAsyncAction("linkedin invitations list", options, () => {
6290
6319
  const account = readOption(options.account);
6291
- const limit = readPositiveInteger(options.limit);
6320
+ const limit = readPositiveInt(options.limit);
6292
6321
  const params = new URLSearchParams();
6293
6322
  if (account)
6294
6323
  params.set("account", account);
@@ -6990,7 +7019,7 @@ export function createProgram() {
6990
7019
  const senders = readCsvOption(options.senders);
6991
7020
  const email = readCampaignEmailBinding(options);
6992
7021
  const maxCredits = readPositiveNumber(options.maxCredits);
6993
- const maxLiveSends = readPositiveInteger(options.maxLiveSends);
7022
+ const maxLiveSends = readPositiveInt(options.maxLiveSends);
6994
7023
  const settings = readSequenceSettings(options);
6995
7024
  return requestOxygen("/api/cli/sequences", {
6996
7025
  method: "POST",
@@ -7057,7 +7086,7 @@ export function createProgram() {
7057
7086
  const maxCredits = readPositiveNumber(options.maxCredits);
7058
7087
  if (maxCredits !== undefined)
7059
7088
  body.max_credits = maxCredits;
7060
- const maxLiveSends = readPositiveInteger(options.maxLiveSends);
7089
+ const maxLiveSends = readPositiveInt(options.maxLiveSends);
7061
7090
  if (maxLiveSends !== undefined)
7062
7091
  body.max_live_sends = maxLiveSends;
7063
7092
  const settings = readSequenceSettings(options);
@@ -7130,7 +7159,7 @@ export function createProgram() {
7130
7159
  .action(async (sequence, options) => {
7131
7160
  await handleAsyncAction("sequences start", options, () => {
7132
7161
  const maxCredits = readPositiveNumber(options.maxCredits);
7133
- const maxLiveSends = readPositiveInteger(options.maxLiveSends);
7162
+ const maxLiveSends = readPositiveInt(options.maxLiveSends);
7134
7163
  return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
7135
7164
  method: "POST",
7136
7165
  body: {
@@ -7561,10 +7590,10 @@ export function createProgram() {
7561
7590
  body: { ramp_config: null },
7562
7591
  });
7563
7592
  }
7564
- const startPerDay = readPositiveInteger(options.startPerDay);
7565
- const increaseEveryDays = readPositiveInteger(options.increaseEveryDays);
7566
- const step = readPositiveInteger(options.step);
7567
- const cap = readPositiveInteger(options.cap);
7593
+ const startPerDay = readPositiveInt(options.startPerDay);
7594
+ const increaseEveryDays = readPositiveInt(options.increaseEveryDays);
7595
+ const step = readPositiveInt(options.step);
7596
+ const cap = readPositiveInt(options.cap);
7568
7597
  if (startPerDay === undefined || increaseEveryDays === undefined || step === undefined || cap === undefined) {
7569
7598
  throw new Error("Provide --start-per-day, --increase-every-days, --step, and --cap (or --clear to remove the override).");
7570
7599
  }
@@ -9245,7 +9274,7 @@ function readCompaniesSearchPlanBody(options) {
9245
9274
  }
9246
9275
  function readCompaniesSearchRunBody(options) {
9247
9276
  const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
9248
- const plan = options.planJson ? readCompanySearchPlanJson(options.planJson) : null;
9277
+ const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
9249
9278
  if (!prompt && !plan) {
9250
9279
  throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
9251
9280
  }
@@ -9396,7 +9425,7 @@ function assertNotPeopleSearchKind(kind) {
9396
9425
  });
9397
9426
  }
9398
9427
  }
9399
- function readCompanySearchPlanJson(value) {
9428
+ function readSearchPlanJson(value) {
9400
9429
  const parsed = parseJsonObject(readFileIfPresent(value));
9401
9430
  const data = parsed.data;
9402
9431
  return data && typeof data === "object" && !Array.isArray(data)
@@ -9418,7 +9447,7 @@ function readPeopleSearchPlanBody(options) {
9418
9447
  }
9419
9448
  function readPeopleSearchRunBody(options) {
9420
9449
  const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
9421
- const plan = options.planJson ? readPeopleSearchPlanJson(options.planJson) : null;
9450
+ const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
9422
9451
  if (!prompt && !plan) {
9423
9452
  throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
9424
9453
  }
@@ -9504,13 +9533,6 @@ function readPeopleSearchFilters(options) {
9504
9533
  }
9505
9534
  return Object.keys(filters).length > 0 ? filters : null;
9506
9535
  }
9507
- function readPeopleSearchPlanJson(value) {
9508
- const parsed = parseJsonObject(readFileIfPresent(value));
9509
- const data = parsed.data;
9510
- return data && typeof data === "object" && !Array.isArray(data)
9511
- ? data
9512
- : parsed;
9513
- }
9514
9536
  function readCompaniesEnrichBody(table, options) {
9515
9537
  const body = { table };
9516
9538
  const fields = readCsvOption(options.missingFields);
@@ -10907,9 +10929,6 @@ function readRecord(value, key) {
10907
10929
  const entry = value[key];
10908
10930
  return isRecord(entry) ? entry : null;
10909
10931
  }
10910
- function isRecord(value) {
10911
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10912
- }
10913
10932
  function tableWebUrl(tableIdOrSlug) {
10914
10933
  return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
10915
10934
  }
@@ -11706,9 +11725,6 @@ function createCliLoginUrl(apiUrl) {
11706
11725
  url.searchParams.set("source", "oxygen_cli");
11707
11726
  return url.toString();
11708
11727
  }
11709
- function readErrorMessage(error) {
11710
- return error instanceof Error ? error.message : "unknown error";
11711
- }
11712
11728
  function startOxygenSpinner(message) {
11713
11729
  if (!output.isTTY) {
11714
11730
  output.write(`${message}...\n`);
@@ -12378,9 +12394,6 @@ function ansi(enabled) {
12378
12394
  yellow: wrap(33, 39),
12379
12395
  };
12380
12396
  }
12381
- function readOption(value) {
12382
- return value?.trim() ? value.trim() : null;
12383
- }
12384
12397
  // Commander collector for a repeatable option (e.g. --attach <url> --attach <url>):
12385
12398
  // accumulates each value into an array.
12386
12399
  function collectRepeatable(value, previous) {
@@ -13470,19 +13483,6 @@ function readNonNegativeInt(value) {
13470
13483
  }
13471
13484
  return parsed;
13472
13485
  }
13473
- function readPositiveInteger(value) {
13474
- const trimmed = value?.trim();
13475
- if (!trimmed)
13476
- return undefined;
13477
- const parsed = Number(trimmed);
13478
- if (!Number.isInteger(parsed) || parsed <= 0) {
13479
- throw new OxygenError("invalid_number", "Expected a positive integer.", {
13480
- details: { value },
13481
- exitCode: 1,
13482
- });
13483
- }
13484
- return parsed;
13485
- }
13486
13486
  // Folds the sequence-level email send controls (--max-emails-per-mailbox-per-day,
13487
13487
  // --send-window-file) into a partial `settings` object. Returns undefined when
13488
13488
  // neither flag is set so the field is omitted from the request body entirely.
@@ -13491,7 +13491,7 @@ function readPositiveInteger(value) {
13491
13491
  // preserves other settings the server already holds, e.g. dispatch_mode/schedule.
13492
13492
  function readSequenceSettings(options) {
13493
13493
  const settings = {};
13494
- const maxPerMailbox = readPositiveInteger(options.maxEmailsPerMailboxPerDay);
13494
+ const maxPerMailbox = readPositiveInt(options.maxEmailsPerMailboxPerDay);
13495
13495
  if (maxPerMailbox !== undefined) {
13496
13496
  settings.max_emails_per_mailbox_per_day = maxPerMailbox;
13497
13497
  }
@@ -13512,10 +13512,10 @@ function readSequenceSettings(options) {
13512
13512
  if (options.stopOnBounce === false) {
13513
13513
  settings.stop_on_bounce = false;
13514
13514
  }
13515
- const maxEmailsPerDay = readPositiveInteger(options.maxEmailsPerDay);
13515
+ const maxEmailsPerDay = readPositiveInt(options.maxEmailsPerDay);
13516
13516
  if (maxEmailsPerDay !== undefined)
13517
13517
  settings.max_emails_per_day = maxEmailsPerDay;
13518
- const maxNewEnrollmentsPerDay = readPositiveInteger(options.maxNewEnrollmentsPerDay);
13518
+ const maxNewEnrollmentsPerDay = readPositiveInt(options.maxNewEnrollmentsPerDay);
13519
13519
  if (maxNewEnrollmentsPerDay !== undefined)
13520
13520
  settings.max_new_enrollments_per_day = maxNewEnrollmentsPerDay;
13521
13521
  const prioritization = readOption(options.sequencePrioritization);
@@ -1,5 +1,3 @@
1
- export declare const MIRROR_LOCK_STALE_MS: number;
2
- export declare const MIRROR_SLUG_PATTERN: RegExp;
3
1
  export declare const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync \u2014 local edits are overwritten -->";
4
2
  export type MirrorPageRecord = {
5
3
  id: string;
@@ -16,7 +14,7 @@ export type MirrorState = {
16
14
  last_sync_at: string | null;
17
15
  pages: Record<string, MirrorPageRecord>;
18
16
  };
19
- export type MirrorIndexPage = {
17
+ type MirrorIndexPage = {
20
18
  slug: string;
21
19
  title?: string | null;
22
20
  type?: string | null;
@@ -26,7 +24,7 @@ export type MirrorIndexPage = {
26
24
  canonical?: boolean;
27
25
  degree?: number;
28
26
  };
29
- export type MirrorIndexSnapshot = {
27
+ type MirrorIndexSnapshot = {
30
28
  pages: MirrorIndexPage[];
31
29
  counts?: {
32
30
  total?: number;
@@ -34,7 +32,7 @@ export type MirrorIndexSnapshot = {
34
32
  byStatus?: Record<string, number>;
35
33
  };
36
34
  };
37
- export type MirrorLogEntry = {
35
+ type MirrorLogEntry = {
38
36
  seq?: string;
39
37
  event?: string;
40
38
  slug?: string | null;
@@ -49,14 +47,12 @@ export declare function resolveMirrorDir(input: {
49
47
  apiHost: string;
50
48
  orgId: string;
51
49
  }): string;
52
- export declare function mirrorSidecarDir(dir: string): string;
53
50
  export declare function mirrorManifestPath(dir: string): string;
54
51
  export declare function mirrorLockPath(dir: string): string;
55
52
  export declare function mirrorConflictsDir(dir: string): string;
56
53
  export declare function pageFilePath(dir: string, slug: string): string;
57
54
  /** Cheap "does a mirror exist here" check for post-write staleness hooks. */
58
55
  export declare function mirrorExists(dir: string): boolean;
59
- export declare function assertSafeMirrorSlug(slug: string): void;
60
56
  export declare function emptyMirrorState(input: {
61
57
  apiHost: string;
62
58
  orgId: string;
@@ -126,3 +122,4 @@ export declare function purgeMirror(dir: string): void;
126
122
  export declare function resetMirrorForFullResync(dir: string): {
127
123
  quarantined: number;
128
124
  };
125
+ export {};
@@ -22,10 +22,10 @@ const MIRROR_MANIFEST_FILENAME = "manifest.json";
22
22
  const MIRROR_LOCK_FILENAME = "lock";
23
23
  const MIRROR_CONFLICTS_DIRNAME = "conflicts";
24
24
  const MIRROR_SLUG_MAX_LENGTH = 200;
25
- export const MIRROR_LOCK_STALE_MS = 10 * 60 * 1000;
25
+ const MIRROR_LOCK_STALE_MS = 10 * 60 * 1000;
26
26
  // Server page slugs are lowercase alphanumerics/dashes; anything else (path
27
27
  // separators, dots, uppercase) is rejected before it can touch the filesystem.
28
- export const MIRROR_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
28
+ const MIRROR_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
29
29
  export const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync — local edits are overwritten -->";
30
30
  // ── Paths ───────────────────────────────────────────────────────────────────────
31
31
  /** Same resolution as the credential/session stores: OXYGEN_CONFIG_DIR wins. */
@@ -38,7 +38,7 @@ export function resolveDefaultConfigDir(env = process.env) {
38
38
  export function resolveMirrorDir(input) {
39
39
  return join(input.configDir, "knowledge", safePathSegment(input.apiHost, "API host"), safePathSegment(input.orgId, "organization id"));
40
40
  }
41
- export function mirrorSidecarDir(dir) {
41
+ function mirrorSidecarDir(dir) {
42
42
  return join(dir, MIRROR_SIDECAR_DIRNAME);
43
43
  }
44
44
  export function mirrorManifestPath(dir) {
@@ -68,7 +68,7 @@ function safePathSegment(value, label) {
68
68
  }
69
69
  return cleaned;
70
70
  }
71
- export function assertSafeMirrorSlug(slug) {
71
+ function assertSafeMirrorSlug(slug) {
72
72
  if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug)) {
73
73
  throw new OxygenError("invalid_slug", "Mirror page slugs must be lowercase alphanumerics and dashes.", {
74
74
  details: { slug },
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { OxygenError } from "@oxygen/shared";
3
3
  import { CustomHttpUrlSafetyError, assertCustomHttpPublicUrlSyntax, assertCustomHttpResolvedHostAllowed, } from "@oxygen/shared/custom-http-safety";
4
4
  import { requestOxygen } from "./http-client.js";
5
+ import { isRecord } from "./util.js";
5
6
  const LOCAL_CUSTOM_HTTP_TIMEOUT_MS = 10_000;
6
7
  const LOCAL_CUSTOM_HTTP_MAX_RESPONSE_BYTES = 1_000_000;
7
8
  export async function runLocalCustomHttpColumn(table, columnKeyOrId, options) {
@@ -769,6 +770,3 @@ function isNonEmptyLocalValue(value) {
769
770
  return Object.keys(value).length > 0;
770
771
  return true;
771
772
  }
772
- function isRecord(value) {
773
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
774
- }
package/dist/runtime.d.ts CHANGED
@@ -12,4 +12,5 @@ export type CliUpdateGuidance = {
12
12
  };
13
13
  export declare function resolveCliBinaryName(env?: NodeJS.ProcessEnv, argv?: readonly string[]): typeof DEV_CLI_BINARY | typeof PROD_CLI_BINARY;
14
14
  export declare function resolveCliUpdateGuidance(apiUrl: string | undefined, env?: NodeJS.ProcessEnv, argv?: readonly string[]): CliUpdateGuidance;
15
+ export declare function isProdApiUrl(apiUrl: string): boolean;
15
16
  export {};
package/dist/runtime.js CHANGED
@@ -46,7 +46,7 @@ function normalizeBinaryName(value) {
46
46
  return PROD_CLI_BINARY;
47
47
  return null;
48
48
  }
49
- function isProdApiUrl(apiUrl) {
49
+ export function isProdApiUrl(apiUrl) {
50
50
  try {
51
51
  return new URL(apiUrl).hostname === PROD_API_HOSTNAME;
52
52
  }
package/dist/skills.js CHANGED
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
5
5
  import { OxygenError, toFailure } from "@oxygen/shared";
6
6
  import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
7
7
  import { requestOxygen } from "./http-client.js";
8
+ import { isRecord, readOption } from "./util.js";
8
9
  import { resolveCmdShimInvocation } from "./windows-shim.js";
9
10
  const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
10
11
  const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
@@ -444,9 +445,3 @@ function readWords(value) {
444
445
  .map((entry) => entry.trim())
445
446
  .filter(Boolean);
446
447
  }
447
- function readOption(value) {
448
- return value?.trim() ? value.trim() : null;
449
- }
450
- function isRecord(value) {
451
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
452
- }
package/dist/update.d.ts CHANGED
@@ -13,7 +13,7 @@ export type UpdateOptions = {
13
13
  package?: string;
14
14
  dryRun?: boolean;
15
15
  };
16
- export type UpdateResult = {
16
+ type UpdateResult = {
17
17
  current_version: string;
18
18
  package: string;
19
19
  command: string;
@@ -28,7 +28,6 @@ export declare function updateCli(options: UpdateOptions, runtime?: {
28
28
  spawnSync?: SpawnSyncLike;
29
29
  execFileSync?: ExecFileSyncLike;
30
30
  } & SkillsSourceRuntime): Promise<UpdateResult>;
31
- export declare function detectCliInstallPrefix(modulePath?: string): string | null;
32
31
  export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
33
32
  export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
34
33
  export {};
package/dist/update.js CHANGED
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
4
4
  import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
5
+ import { readOption } from "./util.js";
5
6
  import { quoteWindowsCmdArg } from "./windows-shim.js";
6
7
  const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
7
8
  // skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
@@ -76,7 +77,7 @@ export async function updateCli(options, runtime = {}) {
76
77
  exitCode: 1,
77
78
  });
78
79
  }
79
- export function detectCliInstallPrefix(modulePath = fileURLToPath(import.meta.url)) {
80
+ function detectCliInstallPrefix(modulePath = fileURLToPath(import.meta.url)) {
80
81
  return detectCliInstallPrefixFromPath(modulePath);
81
82
  }
82
83
  export function detectCliInstallPrefixFromPath(modulePath) {
@@ -99,9 +100,6 @@ export function detectCliInstallPrefixFromPath(modulePath) {
99
100
  export function resolveNpmExecutables(platform = process.platform) {
100
101
  return platform === "win32" ? ["cmd.exe"] : ["npm"];
101
102
  }
102
- function readOption(value) {
103
- return typeof value === "string" && value.trim() ? value.trim() : null;
104
- }
105
103
  function isMissingExecutableError(error) {
106
104
  return error.code === "ENOENT";
107
105
  }
package/dist/util.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function readOption(value: string | undefined): string | null;
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ export declare function readErrorMessage(error: unknown): string;
package/dist/util.js ADDED
@@ -0,0 +1,9 @@
1
+ export function readOption(value) {
2
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3
+ }
4
+ export function isRecord(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ export function readErrorMessage(error) {
8
+ return error instanceof Error ? error.message : "unknown error";
9
+ }
@@ -1,3 +1,4 @@
1
+ import { type EnvLike } from "./signed-token.js";
1
2
  /**
2
3
  * Signed token for native open/click tracking on cold email. The dispatcher MINTS
3
4
  * the token at send time (one per tracked action) and bakes it into a tracking
@@ -37,7 +38,6 @@ export type EmailTrackingTokenPayload = {
37
38
  /** Issued-at (epoch ms), recorded as provenance. */
38
39
  iat: number;
39
40
  };
40
- type EnvLike = Record<string, string | undefined>;
41
41
  /** SHA-256 hex of a destination URL — the value bound into a click token's `uh`. */
42
42
  export declare function hashTrackingUrl(url: string): string;
43
43
  /**
@@ -79,4 +79,3 @@ export declare function buildOpenPixelUrl(token: string, baseUrl: string): strin
79
79
  * in the `u` query param (the route recomputes its hash against the token's `uh`).
80
80
  */
81
81
  export declare function buildClickTrackingUrl(token: string, destinationUrl: string, baseUrl: string): string;
82
- export {};
@@ -1,7 +1,5 @@
1
- import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
- function base64urlEncode(value) {
3
- return Buffer.from(value, "utf8").toString("base64url");
4
- }
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { resolveAppBaseUrl, signPayload, splitAndVerifyToken } from "./signed-token.js";
5
3
  /** SHA-256 hex of a destination URL — the value bound into a click token's `uh`. */
6
4
  export function hashTrackingUrl(url) {
7
5
  return createHash("sha256").update(url, "utf8").digest("hex");
@@ -11,21 +9,7 @@ export function hashTrackingUrl(url) {
11
9
  * computed over the encoded body, so any tamper invalidates the signature.
12
10
  */
13
11
  export function signEmailTrackingToken(payload, secret) {
14
- const body = base64urlEncode(JSON.stringify(payload));
15
- const signature = createHmac("sha256", secret).update(body).digest("base64url");
16
- return `${body}.${signature}`;
17
- }
18
- /** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
19
- function verifySignature(body, providedSig, secret) {
20
- const expected = createHmac("sha256", secret).update(body).digest();
21
- let provided;
22
- try {
23
- provided = Buffer.from(providedSig, "base64url");
24
- }
25
- catch {
26
- return false;
27
- }
28
- return provided.length === expected.length && timingSafeEqual(provided, expected);
12
+ return signPayload(payload, secret);
29
13
  }
30
14
  /** Decode + validate the base64url body; typed payload or null on any problem. */
31
15
  function parseTokenPayload(body) {
@@ -71,14 +55,8 @@ function parseTokenPayload(body) {
71
55
  * null -> benign gif / plain redirect with no recording.
72
56
  */
73
57
  export function verifyEmailTrackingToken(token, secret) {
74
- if (typeof token !== "string" || typeof secret !== "string" || !secret)
75
- return null;
76
- const dot = token.indexOf(".");
77
- if (dot <= 0 || dot === token.length - 1)
78
- return null;
79
- const body = token.slice(0, dot);
80
- const providedSig = token.slice(dot + 1);
81
- if (!verifySignature(body, providedSig, secret))
58
+ const body = splitAndVerifyToken(token, secret);
59
+ if (body === null)
82
60
  return null;
83
61
  return parseTokenPayload(body);
84
62
  }
@@ -114,8 +92,7 @@ export function emailTrackingSigningSecret(env = process.env) {
114
92
  * fallback the token routes are always reachable at.)
115
93
  */
116
94
  export function emailTrackingAppBaseUrl(env = process.env) {
117
- const raw = (env.NEXT_PUBLIC_APP_URL || env.OXYGEN_APP_URL || "https://oxygen-agent.com").trim();
118
- return raw.replace(/\/+$/, "");
95
+ return resolveAppBaseUrl(env);
119
96
  }
120
97
  /** The open-pixel URL for a signed token: `<base>/api/t/o/<token>`. */
121
98
  export function buildOpenPixelUrl(token, baseUrl) {
@@ -1,3 +1,4 @@
1
+ import { type EnvLike } from "./signed-token.js";
1
2
  /**
2
3
  * Signed token for the RFC 8058 one-click List-Unsubscribe link on native cold
3
4
  * email. The worker MINTS the token at send time and bakes the URL into the
@@ -28,7 +29,6 @@ export type UnsubscribeTokenPayload = {
28
29
  /** Issued-at (epoch ms), recorded as provenance. */
29
30
  iat: number;
30
31
  };
31
- type EnvLike = Record<string, string | undefined>;
32
32
  /**
33
33
  * Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
34
34
  * computed over the encoded body, so any tamper to the payload invalidates the
@@ -59,4 +59,3 @@ export declare function oxygenAppBaseUrl(env?: EnvLike): string;
59
59
  * re-checks it against the token), token in the `token` query param.
60
60
  */
61
61
  export declare function buildUnsubscribeUrl(orgId: string, token: string, baseUrl: string): string;
62
- export {};
@@ -1,7 +1,4 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
- function base64urlEncode(value) {
3
- return Buffer.from(value, "utf8").toString("base64url");
4
- }
1
+ import { resolveAppBaseUrl, signPayload, splitAndVerifyToken } from "./signed-token.js";
5
2
  /**
6
3
  * Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
7
4
  * computed over the encoded body, so any tamper to the payload invalidates the
@@ -10,22 +7,7 @@ function base64urlEncode(value) {
10
7
  */
11
8
  export function signUnsubscribeToken(payload, secret) {
12
9
  const normalized = { ...payload, email: payload.email.trim().toLowerCase() };
13
- const body = base64urlEncode(JSON.stringify(normalized));
14
- const signature = createHmac("sha256", secret).update(body).digest("base64url");
15
- return `${body}.${signature}`;
16
- }
17
- /** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
18
- function verifySignature(body, providedSig, secret) {
19
- const expected = createHmac("sha256", secret).update(body).digest();
20
- let provided;
21
- try {
22
- provided = Buffer.from(providedSig, "base64url");
23
- }
24
- catch {
25
- return false;
26
- }
27
- // Equal-length guard before timingSafeEqual (it throws on length mismatch).
28
- return provided.length === expected.length && timingSafeEqual(provided, expected);
10
+ return signPayload(normalized, secret);
29
11
  }
30
12
  /** Decode and validate the base64url body; returns typed payload or null on any problem. */
31
13
  function parseTokenPayload(body) {
@@ -62,14 +44,8 @@ function parseTokenPayload(body) {
62
44
  * secret, tampered body, replayed garbage). Pure: the route maps null -> 401.
63
45
  */
64
46
  export function verifyUnsubscribeToken(token, secret) {
65
- if (typeof token !== "string" || typeof secret !== "string" || !secret)
66
- return null;
67
- const dot = token.indexOf(".");
68
- if (dot <= 0 || dot === token.length - 1)
69
- return null;
70
- const body = token.slice(0, dot);
71
- const providedSig = token.slice(dot + 1);
72
- if (!verifySignature(body, providedSig, secret))
47
+ const body = splitAndVerifyToken(token, secret);
48
+ if (body === null)
73
49
  return null;
74
50
  return parseTokenPayload(body);
75
51
  }
@@ -87,8 +63,7 @@ export function unsubscribeSigningSecret(env = process.env) {
87
63
  * stripped. Prefers NEXT_PUBLIC_APP_URL, then OXYGEN_APP_URL, then the prod host.
88
64
  */
89
65
  export function oxygenAppBaseUrl(env = process.env) {
90
- const raw = (env.NEXT_PUBLIC_APP_URL || env.OXYGEN_APP_URL || "https://oxygen-agent.com").trim();
91
- return raw.replace(/\/+$/, "");
66
+ return resolveAppBaseUrl(env);
92
67
  }
93
68
  /**
94
69
  * The canonical one-click unsubscribe URL: org id in the path (the route
@@ -8,6 +8,12 @@ import { OxygenError } from "./cli-result.js";
8
8
  // it. Configured against Hetzner Object Storage (S3-compatible) via the
9
9
  // OXYGEN_IMPORT_S3_* env vars; works with any S3-compatible endpoint.
10
10
  const PRESIGN_EXPIRY_SECONDS = 900;
11
+ // The Fly worker publishes tenants serially, so a hung S3 GET on one tenant's
12
+ // publishing media would stall every other tenant's publishing tick. Bound the
13
+ // media download the same way the native-provider fetches are bounded
14
+ // (AbortSignal.timeout); 120s matches the worker's other media-transfer budgets
15
+ // and is generous for the bounded image/short-video assets publishing uploads.
16
+ const PUBLISHING_MEDIA_DOWNLOAD_TIMEOUT_MS = 120_000;
11
17
  function readImportStorageConfig() {
12
18
  const endpoint = process.env.OXYGEN_IMPORT_S3_ENDPOINT?.trim();
13
19
  const bucket = process.env.OXYGEN_IMPORT_S3_BUCKET?.trim();
@@ -138,7 +144,7 @@ export async function openImportObjectStream(input) {
138
144
  }
139
145
  export async function openPublishingMediaObjectStream(input) {
140
146
  const { client, config } = resolveClient();
141
- const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
147
+ const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }), { abortSignal: AbortSignal.timeout(PUBLISHING_MEDIA_DOWNLOAD_TIMEOUT_MS) });
142
148
  const body = result.Body;
143
149
  if (!body) {
144
150
  throw new OxygenError("publishing_media_object_missing", "Publishing media object had no body.", {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * HMAC-signed-token core shared by the email tracking + unsubscribe tokens:
3
+ * `<base64url(json)>.<base64url(hmac-sha256)>` minting/verification plus the
4
+ * public app base-URL resolver. Internal to @oxygen/shared (not exported from
5
+ * the package index); each token module owns its payload shape + parsing.
6
+ */
7
+ export type EnvLike = Record<string, string | undefined>;
8
+ /**
9
+ * Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
10
+ * computed over the encoded body, so any tamper invalidates the signature.
11
+ */
12
+ export declare function signPayload(payload: unknown, secret: string): string;
13
+ /**
14
+ * Split a `<body>.<signature>` token at the first dot and verify the signature
15
+ * (constant-time). Returns the still-encoded body on a valid signature; null on
16
+ * ANY problem (bad shape, blank/wrong secret, tampered body).
17
+ */
18
+ export declare function splitAndVerifyToken(token: string, secret: string): string | null;
19
+ /**
20
+ * The public app base URL, trailing slash stripped. Prefers NEXT_PUBLIC_APP_URL,
21
+ * then OXYGEN_APP_URL, then the prod host.
22
+ */
23
+ export declare function resolveAppBaseUrl(env?: EnvLike): string;
@@ -0,0 +1,51 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ function base64urlEncode(value) {
3
+ return Buffer.from(value, "utf8").toString("base64url");
4
+ }
5
+ /**
6
+ * Sign a payload into `<base64url(json)>.<base64url(hmac-sha256)>`. The HMAC is
7
+ * computed over the encoded body, so any tamper invalidates the signature.
8
+ */
9
+ export function signPayload(payload, secret) {
10
+ const body = base64urlEncode(JSON.stringify(payload));
11
+ const signature = createHmac("sha256", secret).update(body).digest("base64url");
12
+ return `${body}.${signature}`;
13
+ }
14
+ /** Constant-time HMAC-SHA256 check; false on any error (bad base64, length mismatch). */
15
+ function verifySignature(body, providedSig, secret) {
16
+ const expected = createHmac("sha256", secret).update(body).digest();
17
+ let provided;
18
+ try {
19
+ provided = Buffer.from(providedSig, "base64url");
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ // Equal-length guard before timingSafeEqual (it throws on length mismatch).
25
+ return provided.length === expected.length && timingSafeEqual(provided, expected);
26
+ }
27
+ /**
28
+ * Split a `<body>.<signature>` token at the first dot and verify the signature
29
+ * (constant-time). Returns the still-encoded body on a valid signature; null on
30
+ * ANY problem (bad shape, blank/wrong secret, tampered body).
31
+ */
32
+ export function splitAndVerifyToken(token, secret) {
33
+ if (typeof token !== "string" || typeof secret !== "string" || !secret)
34
+ return null;
35
+ const dot = token.indexOf(".");
36
+ if (dot <= 0 || dot === token.length - 1)
37
+ return null;
38
+ const body = token.slice(0, dot);
39
+ const providedSig = token.slice(dot + 1);
40
+ if (!verifySignature(body, providedSig, secret))
41
+ return null;
42
+ return body;
43
+ }
44
+ /**
45
+ * The public app base URL, trailing slash stripped. Prefers NEXT_PUBLIC_APP_URL,
46
+ * then OXYGEN_APP_URL, then the prod host.
47
+ */
48
+ export function resolveAppBaseUrl(env = process.env) {
49
+ const raw = (env.NEXT_PUBLIC_APP_URL || env.OXYGEN_APP_URL || "https://oxygen-agent.com").trim();
50
+ return raw.replace(/\/+$/, "");
51
+ }
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.284.3";
1
+ export declare const OXYGEN_VERSION = "1.285.53";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.284.3";
1
+ export const OXYGEN_VERSION = "1.285.53";
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.284.3",
3
+ "version": "1.285.53",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",