@oxygen-agent/cli 1.1010.650 → 1.1010.905

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.
Files changed (114) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +15 -1
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/inbox-needs-reply-notice.d.ts +12 -0
  10. package/dist/inbox-needs-reply-notice.js +51 -0
  11. package/dist/index.js +756 -177
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/skills.js +48 -22
  15. package/dist/streamed-file-import.d.ts +58 -0
  16. package/dist/streamed-file-import.js +115 -0
  17. package/dist/update.d.ts +29 -0
  18. package/dist/update.js +62 -16
  19. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  20. package/dist/workflow-plan-limit-notices.js +28 -0
  21. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
  25. package/node_modules/@oxygen/shared/dist/billing.js +418 -45
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  29. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  31. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  32. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  33. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  36. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  37. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
  38. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  39. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
  42. package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  44. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  45. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  46. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  47. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  48. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  49. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
  50. package/node_modules/@oxygen/shared/dist/index.js +4 -43
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  54. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  55. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  56. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  58. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
  59. package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
  60. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  61. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  62. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  63. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  64. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
  65. package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
  66. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
  67. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
  69. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
  70. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  71. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  72. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  74. package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
  75. package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
  76. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  77. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  78. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
  79. package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
  80. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  81. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  82. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  83. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  84. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  85. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
  86. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
  87. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
  88. package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  91. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  93. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  94. package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
  95. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  96. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  97. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  98. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  99. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  100. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  101. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  102. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  103. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  104. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  105. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  106. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  107. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  108. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  109. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  110. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  111. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  112. package/package.json +1 -1
  113. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  114. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
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.1010.650
37
+ Version: 1.1010.905
@@ -0,0 +1,129 @@
1
+ import { OxygenError } from "@oxygen/shared/cli-result";
2
+ import { type CliEndpoint } from "./runtime.js";
3
+ import { type UpdateOptions } from "./update.js";
4
+ export declare const AUTO_UPDATE_DISABLE_ENV = "OXYGEN_NO_AUTO_UPDATE";
5
+ /** Set on the re-run after a floor-driven update; stops a second attempt. */
6
+ export declare const AUTO_UPDATED_FROM_ENV = "OXYGEN_CLI_AUTO_UPDATED";
7
+ export declare const BACKGROUND_CHECK_INTERVAL_MS: number;
8
+ export type AutoUpdateResult = {
9
+ at: string;
10
+ from: string;
11
+ to: string | null;
12
+ ok: boolean;
13
+ trigger: "background" | "floor";
14
+ error?: string;
15
+ };
16
+ export type AutoUpdateState = {
17
+ auto_update?: "on" | "off";
18
+ last_check_at?: string;
19
+ latest_seen?: string;
20
+ last_check_error?: string;
21
+ last_result?: AutoUpdateResult;
22
+ notice_pending?: boolean;
23
+ };
24
+ export type AutoUpdateIneligibleReason = "ci" | "disabled_by_env" | "disabled_by_setting" | "dev_binary" | "not_global_npm_install" | "non_production_endpoint";
25
+ export type AutoUpdateEligibility = {
26
+ eligible: true;
27
+ } | {
28
+ eligible: false;
29
+ reason: AutoUpdateIneligibleReason;
30
+ };
31
+ type SpawnSyncFn = (command: string, args: string[], options: {
32
+ stdio: "inherit" | "ignore";
33
+ env: NodeJS.ProcessEnv;
34
+ windowsHide: boolean;
35
+ timeout?: number;
36
+ }) => {
37
+ status: number | null;
38
+ error?: Error;
39
+ };
40
+ type SpawnDetachedFn = (command: string, args: string[], env: NodeJS.ProcessEnv) => void;
41
+ export type AutoUpdateRuntime = {
42
+ env?: NodeJS.ProcessEnv;
43
+ argv?: readonly string[];
44
+ execPath?: string;
45
+ modulePath?: string;
46
+ now?: () => Date;
47
+ fetch?: typeof fetch;
48
+ update?: (options: UpdateOptions) => Promise<{
49
+ updated: boolean;
50
+ }>;
51
+ spawnSync?: SpawnSyncFn;
52
+ spawnDetached?: SpawnDetachedFn;
53
+ /** Reads the version npm left on disk; defaults to the running package's package.json. */
54
+ readInstalledVersion?: () => string | null;
55
+ stderr?: (text: string) => void;
56
+ exit?: (code: number) => never;
57
+ };
58
+ export declare function recordMutatingRequestAnswered(): void;
59
+ /** Call before reading piped stdin: a re-run could not read it a second time. */
60
+ export declare function markStdinConsumed(): void;
61
+ /** Test-only: reset the per-process guards. */
62
+ export declare function resetAutoUpdateProcessGuards(): void;
63
+ export declare function autoUpdateStatePath(env?: NodeJS.ProcessEnv): string;
64
+ export declare function readAutoUpdateState(env?: NodeJS.ProcessEnv): AutoUpdateState;
65
+ /** Best-effort: an unwritable config directory must never fail a command. */
66
+ export declare function writeAutoUpdateState(state: AutoUpdateState, env?: NodeJS.ProcessEnv): boolean;
67
+ export declare function autoUpdateEligibility(runtime?: AutoUpdateRuntime & {
68
+ endpoint?: CliEndpoint;
69
+ state?: AutoUpdateState;
70
+ }): AutoUpdateEligibility;
71
+ /**
72
+ * The reactive path. Returns the error to throw (annotated with why no update
73
+ * happened), or — after a successful update — re-runs the command in the new
74
+ * binary and exits with its status.
75
+ */
76
+ export declare function recoverFromCliFloorRejection(error: OxygenError, endpoint: CliEndpoint, runtime?: AutoUpdateRuntime): Promise<OxygenError>;
77
+ /**
78
+ * The proactive path, called once at CLI start. Never blocks: it reads a small
79
+ * state file, prints any pending notice to stderr, and — at most once a day —
80
+ * spawns a detached `update --background` worker.
81
+ */
82
+ export declare function maybeScheduleBackgroundUpdate(runtime?: AutoUpdateRuntime): void;
83
+ export declare function fetchLatestCliVersion(fetchImpl?: typeof fetch): Promise<string>;
84
+ export type BackgroundUpdateOutcome = {
85
+ action: "skipped";
86
+ reason: AutoUpdateIneligibleReason;
87
+ } | {
88
+ action: "check_failed";
89
+ error: string;
90
+ } | {
91
+ action: "up_to_date";
92
+ latest: string;
93
+ } | {
94
+ action: "updated";
95
+ latest: string;
96
+ } | {
97
+ action: "update_failed";
98
+ latest: string;
99
+ error: string;
100
+ };
101
+ /** The detached worker behind `oxygen update --background`. Prints nothing. */
102
+ export declare function runBackgroundUpdate(runtime?: AutoUpdateRuntime): Promise<BackgroundUpdateOutcome>;
103
+ export type AutoUpdateStatus = {
104
+ current_version: string;
105
+ latest_version: string | null;
106
+ latest_error?: string;
107
+ update_available: boolean | null;
108
+ /** Present for --enable-auto/--disable-auto: a dry run reports what would change and writes nothing. */
109
+ dry_run?: boolean;
110
+ setting_changed?: boolean;
111
+ auto_update: {
112
+ setting: "on" | "off";
113
+ active: boolean;
114
+ reason: AutoUpdateIneligibleReason | null;
115
+ reason_text: string | null;
116
+ /** The step that works here: how to turn updates on, or how to update by hand. Null while active. */
117
+ next_step: string | null;
118
+ disable: string;
119
+ last_check_at: string | null;
120
+ last_result: AutoUpdateResult | null;
121
+ };
122
+ state_path: string;
123
+ };
124
+ /** `oxygen update --status`, `--enable-auto`, `--disable-auto`. Checks npm live. */
125
+ export declare function readAutoUpdateStatus(runtime?: AutoUpdateRuntime & {
126
+ setting?: "on" | "off";
127
+ dryRun?: boolean;
128
+ }): Promise<AutoUpdateStatus>;
129
+ export {};
@@ -0,0 +1,392 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { OxygenError } from "@oxygen/shared/cli-result";
6
+ import { isVersionGreater, parseSemver } from "@oxygen/shared/semver";
7
+ import { OXYGEN_VERSION } from "@oxygen/shared/version";
8
+ import { resolveConfigDir } from "./credentials.js";
9
+ import { resolveCliBinaryName, resolveCliUpdateGuidance } from "./runtime.js";
10
+ import { detectCliInstall, readInstalledCliVersion, updateCli } from "./update.js";
11
+ // The npm `oxygen` CLI keeps itself current in two ways:
12
+ //
13
+ // 1. Floor rejection (reactive). When the API refuses this CLI with
14
+ // `cli_update_required`, the CLI installs the newest release and re-runs the
15
+ // exact command once. The API refuses before doing any work, so the re-run
16
+ // cannot repeat a side effect — and the client refuses to re-run once any
17
+ // mutating request of its own has been answered in this process.
18
+ // 2. Background (proactive). At most once a day a detached child checks npm and
19
+ // installs a newer release; the command that spawned it never waits, and the
20
+ // next run prints one stderr line saying what happened.
21
+ //
22
+ // Neither ever runs for `oxygen-dev`, under CI, for a CLI that is not a global
23
+ // npm install (npx, a source checkout), or once turned off with
24
+ // `oxygen update --disable-auto` / OXYGEN_NO_AUTO_UPDATE=1.
25
+ export const AUTO_UPDATE_DISABLE_ENV = "OXYGEN_NO_AUTO_UPDATE";
26
+ /** Set on the re-run after a floor-driven update; stops a second attempt. */
27
+ export const AUTO_UPDATED_FROM_ENV = "OXYGEN_CLI_AUTO_UPDATED";
28
+ export const BACKGROUND_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
29
+ const CLI_PACKAGE_NAME = "@oxygen-agent/cli";
30
+ const NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/@oxygen-agent%2Fcli/dist-tags";
31
+ const NPM_CHECK_TIMEOUT_MS = 10_000;
32
+ const SKILLS_REFRESH_TIMEOUT_MS = 120_000;
33
+ // Why automatic updates cannot run, and the one step that works instead. Both
34
+ // ship in the `--status` envelope: an agent reads JSON, so a remedy printed only
35
+ // in the terminal text never reaches it.
36
+ const AUTO_UPDATE_REASON_TEXT = {
37
+ ci: "off because CI is set",
38
+ disabled_by_env: "off because OXYGEN_NO_AUTO_UPDATE is set",
39
+ disabled_by_setting: "off (turned off with --disable-auto)",
40
+ dev_binary: "not available for oxygen-dev; rebuild it from the dev branch",
41
+ not_global_npm_install: "not available: this CLI is not a global npm install (npx or a project's node_modules)",
42
+ non_production_endpoint: "not available against a non-production API",
43
+ };
44
+ const AUTO_UPDATE_NEXT_STEP = {
45
+ ci: "Update deliberately in CI: oxygen update --package @oxygen-agent/cli@<version>",
46
+ disabled_by_env: "Unset OXYGEN_NO_AUTO_UPDATE to turn automatic updates back on",
47
+ disabled_by_setting: "oxygen update --enable-auto",
48
+ dev_binary: "npm run install-dev-cli (from the latest dev checkout)",
49
+ not_global_npm_install: "Install globally to get automatic updates: npm install -g @oxygen-agent/cli (or update in place: npm install @oxygen-agent/cli@latest)",
50
+ non_production_endpoint: "oxygen update",
51
+ };
52
+ // A command that already sent a mutating request must never be re-run whole:
53
+ // the per-route floors can refuse a LATER request after an earlier one wrote.
54
+ let mutatingRequestAnswered = false;
55
+ let stdinConsumed = false;
56
+ export function recordMutatingRequestAnswered() {
57
+ mutatingRequestAnswered = true;
58
+ }
59
+ /** Call before reading piped stdin: a re-run could not read it a second time. */
60
+ export function markStdinConsumed() {
61
+ stdinConsumed = true;
62
+ }
63
+ /** Test-only: reset the per-process guards. */
64
+ export function resetAutoUpdateProcessGuards() {
65
+ mutatingRequestAnswered = false;
66
+ stdinConsumed = false;
67
+ }
68
+ export function autoUpdateStatePath(env = process.env) {
69
+ return join(resolveConfigDir(env), "update-state.json");
70
+ }
71
+ export function readAutoUpdateState(env = process.env) {
72
+ try {
73
+ const parsed = JSON.parse(readFileSync(autoUpdateStatePath(env), "utf8"));
74
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
75
+ }
76
+ catch {
77
+ return {};
78
+ }
79
+ }
80
+ /** Best-effort: an unwritable config directory must never fail a command. */
81
+ export function writeAutoUpdateState(state, env = process.env) {
82
+ try {
83
+ const path = autoUpdateStatePath(env);
84
+ mkdirSync(resolveConfigDir(env), { recursive: true });
85
+ const temp = `${path}.${process.pid}.tmp`;
86
+ writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
87
+ renameSync(temp, path);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ function isTruthyEnv(value) {
95
+ const normalized = value?.trim().toLowerCase();
96
+ return Boolean(normalized && normalized !== "0" && normalized !== "false");
97
+ }
98
+ export function autoUpdateEligibility(runtime = {}) {
99
+ const env = runtime.env ?? process.env;
100
+ const argv = runtime.argv ?? process.argv;
101
+ if (isTruthyEnv(env[AUTO_UPDATE_DISABLE_ENV]))
102
+ return { eligible: false, reason: "disabled_by_env" };
103
+ if (isTruthyEnv(env.CI))
104
+ return { eligible: false, reason: "ci" };
105
+ const state = runtime.state ?? readAutoUpdateState(env);
106
+ if (state.auto_update === "off")
107
+ return { eligible: false, reason: "disabled_by_setting" };
108
+ if (resolveCliBinaryName(env, argv) === "oxygen-dev")
109
+ return { eligible: false, reason: "dev_binary" };
110
+ const modulePath = runtime.modulePath ?? fileURLToPath(import.meta.url);
111
+ if (detectCliInstall(modulePath).kind !== "global")
112
+ return { eligible: false, reason: "not_global_npm_install" };
113
+ // Pointed at a non-production API, npm can only reinstall the version that
114
+ // API's floor is refusing; the endpoint is the fault, not the binary.
115
+ if (runtime.endpoint && resolveCliUpdateGuidance(runtime.endpoint, env, argv).channel !== "npm") {
116
+ return { eligible: false, reason: "non_production_endpoint" };
117
+ }
118
+ return { eligible: true };
119
+ }
120
+ function withAutoUpdateDetails(error, autoUpdate) {
121
+ const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
122
+ ? { ...error.details, auto_update: autoUpdate }
123
+ : { auto_update: autoUpdate };
124
+ return new OxygenError(error.code, error.message, { details, exitCode: error.exitCode });
125
+ }
126
+ // npm can exit 0 having written somewhere the running binary never loads, so an
127
+ // automatic update trusts only the version it can read back from its own package.
128
+ function readInstalledVersion(runtime) {
129
+ if (runtime.readInstalledVersion)
130
+ return runtime.readInstalledVersion();
131
+ const install = detectCliInstall(runtime.modulePath ?? fileURLToPath(import.meta.url));
132
+ return install.kind === "global" ? readInstalledCliVersion(install.packageDir) : null;
133
+ }
134
+ function readMinimumCliVersion(error) {
135
+ const details = error.details;
136
+ if (!details || typeof details !== "object")
137
+ return null;
138
+ const value = details.minimum_cli_version;
139
+ return typeof value === "string" ? value : null;
140
+ }
141
+ /**
142
+ * The reactive path. Returns the error to throw (annotated with why no update
143
+ * happened), or — after a successful update — re-runs the command in the new
144
+ * binary and exits with its status.
145
+ */
146
+ export async function recoverFromCliFloorRejection(error, endpoint, runtime = {}) {
147
+ if (error.code !== "cli_update_required")
148
+ return error;
149
+ const env = runtime.env ?? process.env;
150
+ const argv = runtime.argv ?? process.argv;
151
+ const stderr = runtime.stderr ?? ((text) => { process.stderr.write(text); });
152
+ if (env[AUTO_UPDATED_FROM_ENV]) {
153
+ return withAutoUpdateDetails(error, { attempted: false, reason: "already_updated_once", updated_from: env[AUTO_UPDATED_FROM_ENV] });
154
+ }
155
+ const eligibility = autoUpdateEligibility({ ...runtime, endpoint });
156
+ if (!eligibility.eligible)
157
+ return withAutoUpdateDetails(error, { attempted: false, reason: eligibility.reason });
158
+ if (mutatingRequestAnswered)
159
+ return withAutoUpdateDetails(error, { attempted: false, reason: "command_already_wrote" });
160
+ if (stdinConsumed)
161
+ return withAutoUpdateDetails(error, { attempted: false, reason: "stdin_consumed" });
162
+ const minimum = readMinimumCliVersion(error);
163
+ stderr(`[oxygen] CLI ${OXYGEN_VERSION} is older than this API accepts${minimum ? ` (minimum ${minimum})` : ""}; updating automatically...\n`);
164
+ const now = runtime.now ?? (() => new Date());
165
+ const update = runtime.update ?? ((options) => updateCli(options, { env }));
166
+ const state = readAutoUpdateState(env);
167
+ const fail = (reason, extra = {}) => {
168
+ writeAutoUpdateState({
169
+ ...state,
170
+ last_result: { at: now().toISOString(), from: OXYGEN_VERSION, to: null, ok: false, trigger: "floor", error: reason },
171
+ }, env);
172
+ stderr("[oxygen] Automatic update failed. Run `oxygen update` to update manually.\n");
173
+ return withAutoUpdateDetails(error, { attempted: true, ok: false, reason, ...extra });
174
+ };
175
+ try {
176
+ await update({ json: true, skipSkillsInstall: true });
177
+ }
178
+ catch (updateError) {
179
+ const stderrText = updateError instanceof OxygenError && updateError.details && typeof updateError.details === "object"
180
+ ? updateError.details.stderr
181
+ : undefined;
182
+ return fail(updateError instanceof Error ? updateError.message : String(updateError), typeof stderrText === "string" ? { npm_stderr: stderrText.slice(0, 2000) } : {});
183
+ }
184
+ const installed = readInstalledVersion(runtime);
185
+ if (!installed || !isVersionGreater(installed, OXYGEN_VERSION)) {
186
+ return fail(`npm finished but this CLI's own install still reports ${installed ?? "no version"}`);
187
+ }
188
+ writeAutoUpdateState({
189
+ ...state,
190
+ last_result: { at: now().toISOString(), from: OXYGEN_VERSION, to: installed, ok: true, trigger: "floor" },
191
+ }, env);
192
+ const execPath = runtime.execPath ?? process.execPath;
193
+ const entry = argv[1];
194
+ if (!entry)
195
+ return withAutoUpdateDetails(error, { attempted: true, ok: true, reason: "rerun_unavailable" });
196
+ refreshSkillsInBackground({ ...runtime, env, argv, execPath });
197
+ stderr("[oxygen] Updated. Re-running your command with the new version...\n");
198
+ const run = runtime.spawnSync ?? defaultSpawnSync;
199
+ const child = run(execPath, [entry, ...argv.slice(2)], {
200
+ stdio: "inherit",
201
+ env: { ...env, [AUTO_UPDATED_FROM_ENV]: OXYGEN_VERSION },
202
+ windowsHide: true,
203
+ });
204
+ if (child.error) {
205
+ return withAutoUpdateDetails(error, { attempted: true, ok: true, reason: "rerun_failed", rerun_error: child.error.message });
206
+ }
207
+ const exit = runtime.exit ?? ((code) => process.exit(code));
208
+ return exit(child.status ?? 1);
209
+ }
210
+ /** Refresh the product skills from the NEW binary, detached and best-effort. */
211
+ function refreshSkillsInBackground(runtime) {
212
+ const entry = runtime.argv[1];
213
+ if (!entry)
214
+ return;
215
+ try {
216
+ (runtime.spawnDetached ?? defaultSpawnDetached)(runtime.execPath, [entry, "update", "--refresh-skills"], runtime.env);
217
+ }
218
+ catch {
219
+ // Skills refresh is a convenience; `oxygen skills install` remains the manual path.
220
+ }
221
+ }
222
+ function defaultSpawnSync(...args) {
223
+ const [command, commandArgs, options] = args;
224
+ const result = spawnSync(command, commandArgs, options);
225
+ return { status: result.status, ...(result.error ? { error: result.error } : {}) };
226
+ }
227
+ function defaultSpawnDetached(command, args, env) {
228
+ const child = spawn(command, args, { detached: true, stdio: "ignore", env, windowsHide: true });
229
+ child.on("error", () => undefined);
230
+ child.unref();
231
+ }
232
+ function formatNotice(result) {
233
+ if (result.ok) {
234
+ return `[oxygen] Updated automatically from ${result.from}${result.to ? ` to ${result.to}` : ""}. `
235
+ + "Turn this off with `oxygen update --disable-auto`.\n";
236
+ }
237
+ return `[oxygen] Automatic update${result.to ? ` to ${result.to}` : ""} failed (${result.error ?? "unknown error"}). `
238
+ + "Run `oxygen update` to update manually.\n";
239
+ }
240
+ function isUpdateCommand(args) {
241
+ const first = args.find((arg) => !arg.startsWith("-"));
242
+ return first === "update";
243
+ }
244
+ /**
245
+ * The proactive path, called once at CLI start. Never blocks: it reads a small
246
+ * state file, prints any pending notice to stderr, and — at most once a day —
247
+ * spawns a detached `update --background` worker.
248
+ */
249
+ export function maybeScheduleBackgroundUpdate(runtime = {}) {
250
+ try {
251
+ const env = runtime.env ?? process.env;
252
+ const argv = runtime.argv ?? process.argv;
253
+ const args = argv.slice(2);
254
+ if (isUpdateCommand(args) || env[AUTO_UPDATED_FROM_ENV])
255
+ return;
256
+ const now = runtime.now ?? (() => new Date());
257
+ let state = readAutoUpdateState(env);
258
+ if (state.notice_pending && state.last_result) {
259
+ (runtime.stderr ?? ((text) => { process.stderr.write(text); }))(formatNotice(state.last_result));
260
+ state = { ...state, notice_pending: false };
261
+ writeAutoUpdateState(state, env);
262
+ }
263
+ if (!autoUpdateEligibility({ ...runtime, env, argv, state }).eligible)
264
+ return;
265
+ const lastCheck = state.last_check_at ? Date.parse(state.last_check_at) : Number.NaN;
266
+ if (Number.isFinite(lastCheck) && now().getTime() - lastCheck < BACKGROUND_CHECK_INTERVAL_MS)
267
+ return;
268
+ // Claim the slot before spawning so concurrent commands don't stampede npm.
269
+ if (!writeAutoUpdateState({ ...state, last_check_at: now().toISOString() }, env))
270
+ return;
271
+ const entry = argv[1];
272
+ if (!entry)
273
+ return;
274
+ (runtime.spawnDetached ?? defaultSpawnDetached)(runtime.execPath ?? process.execPath, [entry, "update", "--background"], env);
275
+ }
276
+ catch {
277
+ // Keeping the CLI current must never break the command the user ran.
278
+ }
279
+ }
280
+ export async function fetchLatestCliVersion(fetchImpl = fetch) {
281
+ const response = await fetchImpl(NPM_DIST_TAGS_URL, {
282
+ headers: { Accept: "application/json" },
283
+ signal: AbortSignal.timeout(NPM_CHECK_TIMEOUT_MS),
284
+ });
285
+ if (!response.ok)
286
+ throw new Error(`npm registry answered HTTP ${response.status}`);
287
+ const body = await response.json();
288
+ const latest = typeof body.latest === "string" ? body.latest.trim() : "";
289
+ if (!parseSemver(latest))
290
+ throw new Error("npm registry returned no usable latest version");
291
+ return latest;
292
+ }
293
+ /** The detached worker behind `oxygen update --background`. Prints nothing. */
294
+ export async function runBackgroundUpdate(runtime = {}) {
295
+ const env = runtime.env ?? process.env;
296
+ const argv = runtime.argv ?? process.argv;
297
+ const now = runtime.now ?? (() => new Date());
298
+ const state = readAutoUpdateState(env);
299
+ const eligibility = autoUpdateEligibility({ ...runtime, env, argv, state });
300
+ if (!eligibility.eligible)
301
+ return { action: "skipped", reason: eligibility.reason };
302
+ let latest;
303
+ try {
304
+ latest = await fetchLatestCliVersion(runtime.fetch ?? fetch);
305
+ }
306
+ catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ writeAutoUpdateState({ ...readAutoUpdateState(env), last_check_error: message }, env);
309
+ return { action: "check_failed", error: message };
310
+ }
311
+ const checked = { ...readAutoUpdateState(env), latest_seen: latest };
312
+ delete checked.last_check_error;
313
+ if (!isVersionGreater(latest, OXYGEN_VERSION)) {
314
+ writeAutoUpdateState(checked, env);
315
+ return { action: "up_to_date", latest };
316
+ }
317
+ const update = runtime.update ?? ((options) => updateCli(options, { env }));
318
+ try {
319
+ await update({ json: true, skipSkillsInstall: true, package: `${CLI_PACKAGE_NAME}@${latest}` });
320
+ const installed = readInstalledVersion(runtime);
321
+ if (installed !== latest) {
322
+ throw new Error(`npm finished but this CLI's own install reports ${installed ?? "no version"}`);
323
+ }
324
+ }
325
+ catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ writeAutoUpdateState({
328
+ ...checked,
329
+ notice_pending: true,
330
+ last_result: { at: now().toISOString(), from: OXYGEN_VERSION, to: latest, ok: false, trigger: "background", error: message },
331
+ }, env);
332
+ return { action: "update_failed", latest, error: message };
333
+ }
334
+ writeAutoUpdateState({
335
+ ...checked,
336
+ notice_pending: true,
337
+ last_result: { at: now().toISOString(), from: OXYGEN_VERSION, to: latest, ok: true, trigger: "background" },
338
+ }, env);
339
+ const entry = argv[1];
340
+ if (entry) {
341
+ (runtime.spawnSync ?? defaultSpawnSync)(runtime.execPath ?? process.execPath, [entry, "update", "--refresh-skills"], {
342
+ stdio: "ignore",
343
+ env,
344
+ windowsHide: true,
345
+ timeout: SKILLS_REFRESH_TIMEOUT_MS,
346
+ });
347
+ }
348
+ return { action: "updated", latest };
349
+ }
350
+ /** `oxygen update --status`, `--enable-auto`, `--disable-auto`. Checks npm live. */
351
+ export async function readAutoUpdateStatus(runtime = {}) {
352
+ const env = runtime.env ?? process.env;
353
+ let state = readAutoUpdateState(env);
354
+ let settingChanged;
355
+ if (runtime.setting) {
356
+ settingChanged = (state.auto_update === "off" ? "off" : "on") !== runtime.setting;
357
+ state = { ...state, auto_update: runtime.setting };
358
+ if (!runtime.dryRun && !writeAutoUpdateState(state, env)) {
359
+ throw new OxygenError("config_write_failed", `Could not save the auto-update setting to ${autoUpdateStatePath(env)}.`, {
360
+ details: { state_path: autoUpdateStatePath(env) },
361
+ exitCode: 1,
362
+ });
363
+ }
364
+ }
365
+ const eligibility = autoUpdateEligibility({ ...runtime, env, state });
366
+ let latest = null;
367
+ let latestError;
368
+ try {
369
+ latest = await fetchLatestCliVersion(runtime.fetch ?? fetch);
370
+ }
371
+ catch (error) {
372
+ latestError = error instanceof Error ? error.message : String(error);
373
+ }
374
+ return {
375
+ current_version: OXYGEN_VERSION,
376
+ latest_version: latest,
377
+ ...(latestError ? { latest_error: latestError } : {}),
378
+ update_available: latest ? isVersionGreater(latest, OXYGEN_VERSION) : null,
379
+ ...(runtime.setting ? { dry_run: runtime.dryRun === true, setting_changed: settingChanged === true } : {}),
380
+ auto_update: {
381
+ setting: state.auto_update === "off" ? "off" : "on",
382
+ active: eligibility.eligible,
383
+ reason: eligibility.eligible ? null : eligibility.reason,
384
+ reason_text: eligibility.eligible ? null : AUTO_UPDATE_REASON_TEXT[eligibility.reason],
385
+ next_step: eligibility.eligible ? null : AUTO_UPDATE_NEXT_STEP[eligibility.reason],
386
+ disable: "oxygen update --disable-auto (or set OXYGEN_NO_AUTO_UPDATE=1)",
387
+ last_check_at: state.last_check_at ?? null,
388
+ last_result: state.last_result ?? null,
389
+ },
390
+ state_path: autoUpdateStatePath(env),
391
+ };
392
+ }
@@ -68,7 +68,7 @@ const MUTATING_VERBS = new Set([
68
68
  // `support chat` starts or continues a customer-authored Plain Thread. The
69
69
  // conversational noun is still a real external write.
70
70
  "buy", "call", "cancel", "chat", "chat-action", "claim", "clear",
71
- "comment", "configure", "connect", "create", "decide", "delete", "delete-message", "delist", "disable",
71
+ "comment", "configure", "connect", "convert", "create", "decide", "delete", "delete-message", "delist", "disable",
72
72
  "disconnect", "dispatch", "done", "draft", "duplicate", "edit", "emit", "enable", "enroll",
73
73
  // Exact leaf, not a `field` prefix: `support admin field-set` writes a Plain
74
74
  // Thread field, while a future `field-get` would be a read.
@@ -89,6 +89,9 @@ const MUTATING_VERBS = new Set([
89
89
  "unarchive", "unpublish", "unshare",
90
90
  "unbind", "unsubscribe", "update", "upload", "upsert", "use", "warm-send", "warmup",
91
91
  "withdraw", "write",
92
+ // `voice numbers release` returns a number to the carrier: irreversible,
93
+ // someone else can take it within minutes. It is the only `release` leaf.
94
+ "release",
92
95
  ]);
93
96
  // Exact commands that must not inherit a read-only discovery label. Warmup
94
97
  // status persists state and can pause the provider; reconnect replaces its
@@ -103,6 +106,8 @@ const MUTATING_COMMANDS = new Set([
103
106
  // whose own subcommands (list/set/enable/disable/backfill, catalog/entries)
104
107
  // already carry their own correctly-classified verbs.
105
108
  "viewers enrichment",
109
+ // A noun leaf that switches a bought domain's yearly paid renewal on or off.
110
+ "domains renewal",
106
111
  ]);
107
112
  // `--approved` usually identifies a paid or externally mutating execution
108
113
  // mode, but a small number of approved operations are explicitly
@@ -126,6 +131,10 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
126
131
  // flag neither authorizes nor caps later inference; only `copilot send` can
127
132
  // trigger the billed model loop.
128
133
  "copilot start",
134
+ // Releasing a phone number STOPS its monthly charge; it never buys anything.
135
+ "voice numbers release",
136
+ // Both previews without --approved; approval cancels or archives, never charges.
137
+ "domains delete",
129
138
  "mailboxes delete",
130
139
  // Previews without --approved; with it, removes a program no creator ever
131
140
  // joined. No provider call, no credits.
@@ -171,6 +180,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
171
180
  // an agent that previewing the play arms it, and the preview would go unrun.
172
181
  "linkedin intent autoenroll",
173
182
  "mailboxes emailguard auto-enroll",
183
+ "domains delete",
174
184
  "mailboxes delete",
175
185
  "sequences recover-capacity",
176
186
  "support admin done",
@@ -178,6 +188,10 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
178
188
  // A bare call reads the Plain workspace and prints the plan; --apply writes.
179
189
  "support admin setup",
180
190
  "verify email",
191
+ // Both fenced by --approved: a bare call prices (buy) or describes (release)
192
+ // what would change and writes nothing.
193
+ "voice numbers buy",
194
+ "voice numbers release",
181
195
  "workflows webhooks rotate",
182
196
  ]);
183
197
  export function buildCommandManifest(program, binaryName) {
@@ -77,4 +77,6 @@ export declare function pickProfileNameForUserSession(userId: string, candidateS
77
77
  export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
78
78
  export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
79
79
  export declare function normalizeApiUrl(value: string): string;
80
+ /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
81
+ export declare function resolveConfigDir(env?: NodeJS.ProcessEnv): string;
80
82
  export {};
@@ -411,12 +411,15 @@ function credentialStore(env) {
411
411
  return "credential-manager";
412
412
  return "file";
413
413
  }
414
- function credentialsPath(env) {
415
- const configDir = env.OXYGEN_CONFIG_DIR?.trim()
414
+ /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
415
+ export function resolveConfigDir(env = process.env) {
416
+ return env.OXYGEN_CONFIG_DIR?.trim()
416
417
  || (process.platform === "win32"
417
418
  ? join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
418
419
  : join(homedir(), ".config", "oxygen"));
419
- return join(configDir, "credentials.json");
420
+ }
421
+ function credentialsPath(env) {
422
+ return join(resolveConfigDir(env), "credentials.json");
420
423
  }
421
424
  function readFileCredentialPayload(env) {
422
425
  const path = credentialsPath(env);
@@ -47,7 +47,7 @@ export function registerFunctionsCommands(program, handle) {
47
47
  functions.command("rename <function> <name>").description("Rename a Function while preserving its identity, callers and published execution.")
48
48
  .option("--json", "Print a JSON envelope.")
49
49
  .action((ref, name, options) => handle("functions rename", options, () => requestOxygen("/api/cli/functions", { method: "POST", body: { operation: "rename", function: ref, display_name: name } })));
50
- functions.command("delete <function>").description("Preview removal from the Function library, then remove it with --yes; preserves backing tables, rows and run history. Connected callers or active runs block deletion.")
50
+ functions.command("delete <function>").description("Preview removal from the Function library, then remove it with --yes; preserves backing tables, rows and run history. The preview names every column that calls it: those stop running, keeping the values they already wrote. Only the Function's own active runs block deletion.")
51
51
  .addHelpText("after", "\nDeleting your own Function is final: there is no restore, unlike `tables delete`. To reuse the logic, `tables duplicate` the backing Table and draft a new Function from the copy.\n")
52
52
  .option("--yes", "Confirm removal of this Function after reviewing its callers.")
53
53
  .option("--json", "Print a JSON envelope.")