@oxygen-agent/cli 1.334.3 → 1.336.4

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.334.3
37
+ Version: 1.336.4
@@ -3,5 +3,21 @@ export type BrowserLoginSession = {
3
3
  waitForToken: () => Promise<string>;
4
4
  close: () => Promise<void>;
5
5
  };
6
+ export type RelayLoginSession = BrowserLoginSession & {
7
+ confirmationCode: string;
8
+ };
9
+ /**
10
+ * Relay browser login (device-flow style): register a login request
11
+ * server-side, send the user to /settings/cli to approve it, and poll the
12
+ * claim endpoint until the session token is released. No loopback server is
13
+ * involved, so it works where the browser→localhost POST cannot: browsers
14
+ * enforcing the Local Network Access permission, SSH/WSL/remote terminals,
15
+ * containers. Any failure to CREATE the request (old server without the
16
+ * endpoint, network policy) is the caller's cue to fall back to the legacy
17
+ * loopback flow.
18
+ */
19
+ export declare function createRelayLoginSession(apiUrl: string, options?: {
20
+ fetch?: typeof fetch;
21
+ }): Promise<RelayLoginSession>;
6
22
  export declare function createBrowserLoginSession(apiUrl: string): Promise<BrowserLoginSession>;
7
23
  export declare function openBrowser(url: string): boolean;
@@ -1,11 +1,141 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
- import { OxygenError } from "@oxygen/shared";
4
+ import { hostname } from "node:os";
5
+ import { OXYGEN_VERSION, OxygenError, deriveCliLoginConfirmationCode } from "@oxygen/shared";
6
+ import { requestOxygen } from "./http-client.js";
5
7
  import { readErrorMessage } from "./util.js";
6
8
  const CALLBACK_HOST = "127.0.0.1";
7
9
  const CALLBACK_PATH = "/cli/login/callback";
8
10
  const MAX_CALLBACK_BODY_BYTES = 32_000;
11
+ const RELAY_CREATE_TIMEOUT_MS = 10_000;
12
+ const RELAY_CLAIM_TIMEOUT_MS = 15_000;
13
+ const DEFAULT_RELAY_POLL_INTERVAL_SECONDS = 2;
14
+ const MAX_RELAY_POLL_INTERVAL_SECONDS = 10;
15
+ const MAX_CONSECUTIVE_TRANSIENT_CLAIM_ERRORS = 5;
16
+ // Claim-poll errors that read as infrastructure hiccups rather than a verdict
17
+ // about the login request itself — tolerated a few times before giving up.
18
+ const TRANSIENT_CLAIM_ERROR_CODES = new Set([
19
+ "network_error",
20
+ "network_timeout",
21
+ "invalid_response",
22
+ "unexpected_error",
23
+ ]);
24
+ /**
25
+ * Relay browser login (device-flow style): register a login request
26
+ * server-side, send the user to /settings/cli to approve it, and poll the
27
+ * claim endpoint until the session token is released. No loopback server is
28
+ * involved, so it works where the browser→localhost POST cannot: browsers
29
+ * enforcing the Local Network Access permission, SSH/WSL/remote terminals,
30
+ * containers. Any failure to CREATE the request (old server without the
31
+ * endpoint, network policy) is the caller's cue to fall back to the legacy
32
+ * loopback flow.
33
+ */
34
+ export async function createRelayLoginSession(apiUrl, options = {}) {
35
+ const state = randomBytes(32).toString("base64url");
36
+ // Empty token: pins the host to --api-url/profile resolution while sending no
37
+ // Authorization header — these endpoints are pre-auth by design.
38
+ const credentials = { token: "", apiUrl };
39
+ const requestOptions = options.fetch ? { fetch: options.fetch } : {};
40
+ const created = await requestOxygen("/api/cli/login-requests", {
41
+ method: "POST",
42
+ credentials,
43
+ body: {
44
+ state,
45
+ client: {
46
+ os: process.platform,
47
+ hostname: safeHostname(),
48
+ cli_version: OXYGEN_VERSION,
49
+ },
50
+ },
51
+ timeoutMs: RELAY_CREATE_TIMEOUT_MS,
52
+ ...requestOptions,
53
+ });
54
+ const pollIntervalMs = normalizePollIntervalSeconds(created.interval_seconds) * 1000;
55
+ const redirectParams = new URLSearchParams({
56
+ cli_state: state,
57
+ cli_login_request: "1",
58
+ source: "oxygen_cli",
59
+ });
60
+ const verificationUrl = new URL("/auth/sign-in", apiUrl);
61
+ verificationUrl.searchParams.set("redirect_url", `/settings/cli?${redirectParams.toString()}`);
62
+ verificationUrl.searchParams.set("source", "oxygen_cli");
63
+ let closed = false;
64
+ async function waitForToken() {
65
+ let transientErrors = 0;
66
+ while (!closed) {
67
+ await sleep(pollIntervalMs);
68
+ if (closed)
69
+ break;
70
+ let claim;
71
+ try {
72
+ claim = await requestOxygen("/api/cli/login-requests/claim", {
73
+ method: "POST",
74
+ credentials,
75
+ body: { state },
76
+ timeoutMs: RELAY_CLAIM_TIMEOUT_MS,
77
+ ...requestOptions,
78
+ });
79
+ }
80
+ catch (error) {
81
+ if (error instanceof OxygenError && error.code === "rate_limit_exceeded") {
82
+ await sleep(Math.max(readRetryAfterMs(error), pollIntervalMs));
83
+ continue;
84
+ }
85
+ if (error instanceof OxygenError && TRANSIENT_CLAIM_ERROR_CODES.has(error.code)) {
86
+ transientErrors += 1;
87
+ if (transientErrors >= MAX_CONSECUTIVE_TRANSIENT_CLAIM_ERRORS)
88
+ throw error;
89
+ continue;
90
+ }
91
+ // Typed verdicts (login_request_expired / _not_found / _already_claimed)
92
+ // and anything else surface immediately.
93
+ throw error;
94
+ }
95
+ transientErrors = 0;
96
+ if (claim.status === "ready" && typeof claim.token === "string" && claim.token) {
97
+ return claim.token;
98
+ }
99
+ }
100
+ throw new OxygenError("browser_login_closed", "Browser login closed.", { exitCode: 1 });
101
+ }
102
+ return {
103
+ verificationUrl: verificationUrl.toString(),
104
+ confirmationCode: deriveCliLoginConfirmationCode(state),
105
+ waitForToken,
106
+ close: () => {
107
+ closed = true;
108
+ return Promise.resolve();
109
+ },
110
+ };
111
+ }
112
+ function normalizePollIntervalSeconds(value) {
113
+ if (typeof value !== "number" || !Number.isFinite(value)) {
114
+ return DEFAULT_RELAY_POLL_INTERVAL_SECONDS;
115
+ }
116
+ return Math.min(MAX_RELAY_POLL_INTERVAL_SECONDS, Math.max(1, Math.trunc(value)));
117
+ }
118
+ function readRetryAfterMs(error) {
119
+ const details = error.details;
120
+ if (details && typeof details === "object" && !Array.isArray(details)) {
121
+ const seconds = details.retry_after_seconds;
122
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
123
+ return Math.min(seconds, 60) * 1000;
124
+ }
125
+ }
126
+ return DEFAULT_RELAY_POLL_INTERVAL_SECONDS * 1000;
127
+ }
128
+ function safeHostname() {
129
+ try {
130
+ return hostname();
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ }
136
+ function sleep(ms) {
137
+ return new Promise((resolve) => setTimeout(resolve, ms));
138
+ }
9
139
  export async function createBrowserLoginSession(apiUrl) {
10
140
  const state = randomBytes(32).toString("base64url");
11
141
  let settled = false;
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowSt
13
13
  import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
14
14
  import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
15
15
  import { isRecipeDefinition } from "@oxygen/recipe-sdk";
16
- import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
16
+ import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
17
17
  import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
18
18
  import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
19
19
  import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, listLocalMirrors, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
@@ -63,6 +63,7 @@ function buildFindBody(capability, options) {
63
63
  return body;
64
64
  }
65
65
  const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
66
+ const BROWSER_LOGIN_FALLBACK_HINT_MS = 15 * 1000;
66
67
  const OXYGEN_SPINNER_INTERVAL_MS = 90;
67
68
  const INITIAL_OXYGEN_PROFILE_ENV = process.env.OXYGEN_PROFILE?.trim() || null;
68
69
  let globalProfileFlag = null;
@@ -1595,7 +1596,7 @@ export function createProgram() {
1595
1596
  });
1596
1597
  program
1597
1598
  .command("login")
1598
- .description("Connect this terminal to Oxygen.")
1599
+ .description("Connect this terminal to Oxygen. Opens the browser to approve this terminal (matching confirmation code shown in both); paste a token with --token instead if you prefer.")
1599
1600
  .option("--token <token>", "CLI API token created in the Oxygen dashboard.")
1600
1601
  .option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
1601
1602
  .option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
@@ -1607,7 +1608,7 @@ export function createProgram() {
1607
1608
  });
1608
1609
  program
1609
1610
  .command("auth")
1610
- .description("Authentication helpers.")
1611
+ .description("Authentication helpers for scripts and agents (non-interactive token use). Day to day, `oxygen login` and `oxygen whoami` are all you need.")
1611
1612
  .addCommand(new Command("use-token")
1612
1613
  .description("Store a CLI API token non-interactively.")
1613
1614
  .requiredOption("--token <token>", "CLI API token.")
@@ -1626,7 +1627,7 @@ export function createProgram() {
1626
1627
  }));
1627
1628
  program
1628
1629
  .command("profiles")
1629
- .description("Manage stored CLI credential profiles.")
1630
+ .description("Manage stored CLI credential profiles (switch between workspaces and hosts). `oxygen login` creates or updates the active profile.")
1630
1631
  .addCommand(new Command("list")
1631
1632
  .description("List stored CLI credential profiles.")
1632
1633
  .option("--json", "Print a JSON envelope.")
@@ -3620,7 +3621,57 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3620
3621
  });
3621
3622
  });
3622
3623
  }));
3623
- tablesCommand.addCommand(new Command("promote")
3624
+ tablesCommand.addCommand(new Command("relate")
3625
+ .description("Relate two tables: define a relation column on the source that links rows to the target table. Works on ANY table — plain tables are registered under the hood automatically. Defaults to dry-run.")
3626
+ .argument("<table>", "Source table id or slug.")
3627
+ .argument("<slug>", "Relation slug — also the relation column key on the source, such as client.")
3628
+ .requiredOption("--target-table <table>", "Target table id or slug. May also be a CRM object table (companies, people, deals).")
3629
+ .option("--display-name <name>", "Display name for the relation column. Defaults to the slug in Title Case.")
3630
+ .option("--cardinality <cardinality>", "one_to_one, one_to_many, many_to_one, or many_to_many (source:target). Default many_to_many. one_to_many means each source row links many target rows and each target row links back to one source.")
3631
+ .option("--inverse-slug <slug>", "Inverse relation slug on the target; also creates the inverse relation column there.")
3632
+ .option("--inverse-display-name <name>", "Display name for the inverse side.")
3633
+ .option("--dry-run", "Preview the relation plan without writing.")
3634
+ .option("--live", "Apply the relation. Default is dry-run.")
3635
+ .option("--json", "Print a JSON envelope.")
3636
+ .action(async (table, slug, options) => {
3637
+ await handleAsyncAction("tables relate", options, () => requestOxygen("/api/cli/tables/relations", {
3638
+ method: "POST",
3639
+ body: {
3640
+ table,
3641
+ relationship: {
3642
+ slug,
3643
+ target_table: options.targetTable,
3644
+ ...(options.displayName ? { display_name: options.displayName } : {}),
3645
+ ...(options.cardinality ? { cardinality: options.cardinality } : {}),
3646
+ ...(options.inverseSlug ? { inverse_slug: options.inverseSlug } : {}),
3647
+ ...(options.inverseDisplayName ? { inverse_display_name: options.inverseDisplayName } : {}),
3648
+ },
3649
+ mode: options.live ? "live" : "dry_run",
3650
+ },
3651
+ }));
3652
+ }))
3653
+ .addCommand(new Command("link")
3654
+ .description("Link one row to a row in the related table through a relation defined with `tables relate`. Defaults to dry-run.")
3655
+ .argument("<table>", "Source table id or slug.")
3656
+ .argument("<row_id>", "Source row id.")
3657
+ .requiredOption("--relation <slug>", "Relation slug defined on the source table, such as client.")
3658
+ .requiredOption("--target-row-id <row_id>", "Target row id in the related table.")
3659
+ .option("--dry-run", "Preview the link (and any cardinality replacements) without writing.")
3660
+ .option("--live", "Apply the link. Default is dry-run.")
3661
+ .option("--json", "Print a JSON envelope.")
3662
+ .action(async (table, rowId, options) => {
3663
+ await handleAsyncAction("tables link", options, () => requestOxygen("/api/cli/tables/relations/link", {
3664
+ method: "POST",
3665
+ body: {
3666
+ table,
3667
+ row_id: rowId,
3668
+ relation: options.relation,
3669
+ target_row_id: options.targetRowId,
3670
+ mode: options.live ? "live" : "dry_run",
3671
+ },
3672
+ }));
3673
+ }))
3674
+ .addCommand(new Command("promote")
3624
3675
  .description("Write bound rows' columns onto their matched CRM records (Records<->Tables). Free, but a truth write: previews by default, then re-run with --approved to write. Approved runs queue as a background run — watch with `oxygen table-runs wait <run_id>`.")
3625
3676
  .argument("<table>", "Table id or slug whose bound rows to promote.")
3626
3677
  .option("--object <slug>", "CRM object to promote onto (companies, people, deals, ...). Inferred when the table has exactly one bind column.")
@@ -13874,24 +13925,20 @@ async function buildPostLoginHint(activeProfile) {
13874
13925
  }
13875
13926
  async function promptForToken(options) {
13876
13927
  const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
13928
+ const settingsUrl = cliSettingsUrl(options.apiUrl);
13877
13929
  if (options.json) {
13878
- throw new OxygenError("missing_token", "Pass a CLI API token with --token when using --json.", {
13930
+ throw new OxygenError("missing_token", `Pass a CLI API token with --token when using --json. Create one at ${settingsUrl}.`, {
13879
13931
  details: { login_url: fallbackLoginUrl },
13880
13932
  exitCode: 1,
13881
13933
  });
13882
13934
  }
13883
13935
  if (!process.stdin.isTTY) {
13884
- throw new OxygenError("missing_token", "Pass a CLI API token with --token, or run `oxygen login` in an interactive terminal.", {
13936
+ throw new OxygenError("missing_token", `Pass a CLI API token with --token, or run \`oxygen login\` in an interactive terminal. Create a token at ${settingsUrl}.`, {
13885
13937
  details: { login_url: fallbackLoginUrl },
13886
13938
  exitCode: 1,
13887
13939
  });
13888
13940
  }
13889
- const browserSession = options.browser
13890
- ? await createBrowserLoginSession(options.apiUrl).catch((error) => {
13891
- output.write(`Browser login unavailable: ${readErrorMessage(error)}\n`);
13892
- return null;
13893
- })
13894
- : null;
13941
+ const browserSession = options.browser ? await createLoginSession(options.apiUrl) : null;
13895
13942
  if (browserSession) {
13896
13943
  const opened = openBrowser(browserSession.verificationUrl);
13897
13944
  if (opened) {
@@ -13901,7 +13948,19 @@ async function promptForToken(options) {
13901
13948
  output.write("Open this URL in your browser:\n");
13902
13949
  output.write(`${browserSession.verificationUrl}\n`);
13903
13950
  }
13904
- const spinner = startOxygenSpinner("Waiting for browser authorization");
13951
+ if ("confirmationCode" in browserSession) {
13952
+ output.write(`Confirmation code: ${browserSession.confirmationCode}\n`);
13953
+ output.write("Approve the login in your browser — it must show this exact code.\n");
13954
+ }
13955
+ let spinner = startOxygenSpinner("Waiting for browser authorization");
13956
+ // One-shot fallback hint: after 15s of silence, tell the user how to finish
13957
+ // manually (blocked browsers and remote terminals used to look like a hang).
13958
+ const hintTimer = setTimeout(() => {
13959
+ spinner.stop();
13960
+ output.write(`Still waiting? To finish manually: open ${settingsUrl}, create a session, and paste the \`oxygen login --token <token>\` command it shows.\n`);
13961
+ spinner = startOxygenSpinner("Waiting for browser authorization");
13962
+ }, BROWSER_LOGIN_FALLBACK_HINT_MS);
13963
+ hintTimer.unref?.();
13905
13964
  try {
13906
13965
  const token = (await waitForBrowserToken(browserSession)).trim();
13907
13966
  if (token) {
@@ -13911,15 +13970,38 @@ async function promptForToken(options) {
13911
13970
  }
13912
13971
  catch (error) {
13913
13972
  spinner.fail(`Browser login unavailable: ${readErrorMessage(error)}`);
13914
- output.write("Paste a CLI API token instead.\n");
13973
+ output.write(`Paste a CLI API token instead (create one at ${settingsUrl}).\n`);
13915
13974
  }
13916
13975
  finally {
13976
+ clearTimeout(hintTimer);
13917
13977
  spinner.stop();
13918
13978
  await browserSession.close();
13919
13979
  }
13920
13980
  }
13921
13981
  return promptForManualToken();
13922
13982
  }
13983
+ /**
13984
+ * Relay login first (server-side approve + claim — works in every browser and
13985
+ * for remote/SSH terminals); legacy loopback only when the relay request cannot
13986
+ * even be created (older server without the endpoint, network policy). Any
13987
+ * create failure falls back — an old server answers with an HTML 404 that
13988
+ * surfaces as invalid_response, not not_found.
13989
+ */
13990
+ async function createLoginSession(apiUrl) {
13991
+ try {
13992
+ return await createRelayLoginSession(apiUrl);
13993
+ }
13994
+ catch {
13995
+ // Fall through to the legacy loopback handoff.
13996
+ }
13997
+ try {
13998
+ return await createBrowserLoginSession(apiUrl);
13999
+ }
14000
+ catch (error) {
14001
+ output.write(`Browser login unavailable: ${readErrorMessage(error)}\n`);
14002
+ return null;
14003
+ }
14004
+ }
13923
14005
  async function promptForManualToken() {
13924
14006
  const restoreEcho = muteTokenEcho();
13925
14007
  const rl = createInterface({ input, output });
@@ -13961,6 +14043,9 @@ function createCliLoginUrl(apiUrl) {
13961
14043
  url.searchParams.set("source", "oxygen_cli");
13962
14044
  return url.toString();
13963
14045
  }
14046
+ function cliSettingsUrl(apiUrl) {
14047
+ return new URL("/settings/cli", apiUrl).toString();
14048
+ }
13964
14049
  function startOxygenSpinner(message) {
13965
14050
  if (!output.isTTY) {
13966
14051
  output.write(`${message}...\n`);
@@ -0,0 +1,3 @@
1
+ export declare function isValidCliLoginState(value: string): boolean;
2
+ /** Uppercase first 8 hex chars of sha256(domain + state), formatted XXXX-XXXX. */
3
+ export declare function deriveCliLoginConfirmationCode(state: string): string;
@@ -0,0 +1,30 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Confirmation-code derivation for the CLI relay browser login ("login request")
4
+ * flow. The CLI generates a 32-byte base64url `state`, registers its hash with
5
+ * POST /api/cli/login-requests, and polls the claim endpoint; the signed-in
6
+ * /settings/cli page approves the request after the user compares the code it
7
+ * shows with the code the CLI printed.
8
+ *
9
+ * The code is a human-comparable fingerprint of `state` — a guard against
10
+ * approving the wrong terminal (two logins racing, a stale tab), NOT a security
11
+ * boundary: approval authority is the signed-in click, and secrecy lives in the
12
+ * 256-bit state itself, which is stored hash-only server-side. Both consumers
13
+ * are Node (CLI prints it; the API derives it at create time and returns it to
14
+ * the panel via lookup), so this stays a plain node:crypto helper — the browser
15
+ * never computes it.
16
+ */
17
+ const CLI_LOGIN_CODE_DOMAIN = "oxygen_cli_login_code_v1:";
18
+ /** 32 random bytes, base64url — always 43 chars of [A-Za-z0-9_-]. */
19
+ const CLI_LOGIN_STATE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
20
+ export function isValidCliLoginState(value) {
21
+ return CLI_LOGIN_STATE_PATTERN.test(value);
22
+ }
23
+ /** Uppercase first 8 hex chars of sha256(domain + state), formatted XXXX-XXXX. */
24
+ export function deriveCliLoginConfirmationCode(state) {
25
+ const digest = createHash("sha256")
26
+ .update(`${CLI_LOGIN_CODE_DOMAIN}${state}`, "utf8")
27
+ .digest("hex");
28
+ const compact = digest.slice(0, 8).toUpperCase();
29
+ return `${compact.slice(0, 4)}-${compact.slice(4)}`;
30
+ }
@@ -5,6 +5,7 @@ export * from "./billing.js";
5
5
  export * from "./budget-scopes.js";
6
6
  export * from "./cell-format.js";
7
7
  export * from "./cli-envelope.js";
8
+ export * from "./cli-login-code.js";
8
9
  export * from "./cli-result.js";
9
10
  export * from "./column-types.js";
10
11
  export * from "./credit-guidance.js";
@@ -5,6 +5,7 @@ export * from "./billing.js";
5
5
  export * from "./budget-scopes.js";
6
6
  export * from "./cell-format.js";
7
7
  export * from "./cli-envelope.js";
8
+ export * from "./cli-login-code.js";
8
9
  export * from "./cli-result.js";
9
10
  export * from "./column-types.js";
10
11
  export * from "./credit-guidance.js";
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.334.3";
1
+ export declare const OXYGEN_VERSION = "1.336.4";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.334.3";
1
+ export const OXYGEN_VERSION = "1.336.4";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.334.3",
3
+ "version": "1.336.4",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",