@oxygen-agent/cli 1.1010.721 → 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.
- package/README.md +1 -1
- package/dist/auto-update.d.ts +129 -0
- package/dist/auto-update.js +392 -0
- package/dist/command-manifest.js +14 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +6 -3
- package/dist/functions-commands.js +1 -1
- package/dist/http-client.js +28 -4
- package/dist/index.js +583 -145
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/update.d.ts +29 -0
- package/dist/update.js +62 -16
- package/dist/workflow-plan-limit-notices.d.ts +8 -0
- package/dist/workflow-plan-limit-notices.js +28 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
- package/node_modules/@oxygen/shared/dist/billing.js +333 -42
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
- package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
- package/node_modules/@oxygen/shared/dist/index.js +2 -42
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
- package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
- package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
- package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
- package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/semver.js +41 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
- package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
- package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
- package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +14 -27
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
- package/node_modules/@oxygen/workflows/dist/index.js +152 -2
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
- package/package.json +1 -1
package/dist/run-wait.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export interface CliRunWaitConfig {
|
|
|
8
8
|
defaultTimeoutSeconds: number;
|
|
9
9
|
defaultIntervalSeconds: number;
|
|
10
10
|
/** Fetches the latest run snapshot to inspect this poll. */
|
|
11
|
-
fetchRun: () => Promise<Record<string, unknown>>;
|
|
11
|
+
fetchRun: (signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
12
|
+
/** Opt in only for read-only polls; retries share the original deadline. */
|
|
13
|
+
retryTransientNetworkErrors?: boolean;
|
|
12
14
|
/** True when the run status is terminal for this resource. */
|
|
13
15
|
isTerminal: (status: string | null) => boolean;
|
|
14
16
|
/** Builds the response once a terminal status is observed. */
|
package/dist/run-wait.js
CHANGED
|
@@ -8,12 +8,26 @@ export async function waitForCliRun(config) {
|
|
|
8
8
|
const startedAt = Date.now();
|
|
9
9
|
const deadline = startedAt + timeoutSeconds * 1000;
|
|
10
10
|
let polls = 0;
|
|
11
|
+
let latestRun = null;
|
|
12
|
+
let status = null;
|
|
11
13
|
while (true) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (Date.now() < deadline) {
|
|
15
|
+
polls += 1;
|
|
16
|
+
const signal = config.retryTransientNetworkErrors
|
|
17
|
+
? AbortSignal.timeout(Math.max(1, deadline - Date.now())) : undefined;
|
|
18
|
+
try {
|
|
19
|
+
latestRun = await config.fetchRun(signal);
|
|
20
|
+
status = readRecordString(latestRun, "status");
|
|
21
|
+
if (config.isTerminal(status)) {
|
|
22
|
+
return config.shapeTerminal(latestRun, status, polls, Date.now() - startedAt);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const transient = error instanceof OxygenError
|
|
27
|
+
&& (error.code === "network_error" || error.code === "network_timeout");
|
|
28
|
+
if (!config.retryTransientNetworkErrors || (!transient && !signal?.aborted))
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
17
31
|
}
|
|
18
32
|
const remainingMs = deadline - Date.now();
|
|
19
33
|
if (remainingMs <= 0) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type RowsFileFormat } from "@oxygen/shared/file-import";
|
|
2
|
+
/**
|
|
3
|
+
* Large-file imports (decision F.8, 2026-09-27: up to 5 GiB on the largest plans).
|
|
4
|
+
*
|
|
5
|
+
* The ordinary `tables import --file` path reads the whole file into memory and
|
|
6
|
+
* parses every row into an object before it uploads anything. That is fine at a
|
|
7
|
+
* few MiB and impossible at a few GiB: a V8 string tops out near 1 GiB and the
|
|
8
|
+
* parsed rows weigh many times the file. Above this threshold a CSV or JSONL
|
|
9
|
+
* file therefore never enters memory whole. One streaming pass counts its rows,
|
|
10
|
+
* hashes it and collects its columns; the file is then PUT to object storage
|
|
11
|
+
* straight from disk, and the worker streams it again from there.
|
|
12
|
+
*
|
|
13
|
+
* JSON and XLSX cannot stream (a JSON array has no record boundary, an XLSX is a
|
|
14
|
+
* zip), so they never take this path: they are capped at
|
|
15
|
+
* MAX_BUFFERED_IMPORT_PARSE_BYTES before anything is read.
|
|
16
|
+
*/
|
|
17
|
+
export declare const STREAMED_FILE_IMPORT_MIN_BYTES: number;
|
|
18
|
+
/** Rows kept from the head of the file to infer a new table's column types from. */
|
|
19
|
+
export declare const STREAMED_IMPORT_SAMPLE_ROWS = 2000;
|
|
20
|
+
/** An upload that moves no bytes for this long is abandoned, whatever its size. */
|
|
21
|
+
export declare const STREAMED_UPLOAD_STALL_TIMEOUT_MS = 120000;
|
|
22
|
+
export declare function shouldStreamFileImport(format: RowsFileFormat, fileBytes: number): boolean;
|
|
23
|
+
export type StreamedImportScan = {
|
|
24
|
+
rowCount: number;
|
|
25
|
+
sha256: string;
|
|
26
|
+
/** Every column the file names, in first-seen order (the whole file, not the sample). */
|
|
27
|
+
labels: string[];
|
|
28
|
+
sampleRows: Record<string, unknown>[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read the file once, as a stream: count its rows with the same parser the
|
|
32
|
+
* worker will use (so a malformed file fails here, before any upload), hash it,
|
|
33
|
+
* and collect its column labels. JSONL rows may each name different keys, so the
|
|
34
|
+
* labels come from every row; only the type-inference sample is bounded.
|
|
35
|
+
*/
|
|
36
|
+
export declare function scanStreamedImportFile(path: string, format: RowsFileFormat, options?: {
|
|
37
|
+
sampleRows?: number;
|
|
38
|
+
}): Promise<StreamedImportScan>;
|
|
39
|
+
/**
|
|
40
|
+
* The rows a target is prepared from: the sample, plus one empty row naming any
|
|
41
|
+
* column that first appears after it, so a created table gets every column the
|
|
42
|
+
* file has and an existing table's mapping sees every header.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rowsForStreamedImportTarget(scan: StreamedImportScan): Record<string, unknown>[];
|
|
45
|
+
/**
|
|
46
|
+
* PUT a file to a presigned URL straight from disk. Content-Length is sent
|
|
47
|
+
* explicitly (S3 refuses a chunked PUT, and the presigner signed the length),
|
|
48
|
+
* and the request is abandoned only when it stops moving, never for being big:
|
|
49
|
+
* a 5 GiB upload on a slow link is legitimate, a silent socket is not.
|
|
50
|
+
*/
|
|
51
|
+
export declare function putImportFileStream(input: {
|
|
52
|
+
uploadUrl: string;
|
|
53
|
+
path: string;
|
|
54
|
+
byteLength: number;
|
|
55
|
+
contentType: string;
|
|
56
|
+
stallTimeoutMs?: number;
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { iterateRowsFileStreamBatches, } from "@oxygen/shared/file-import";
|
|
5
|
+
/**
|
|
6
|
+
* Large-file imports (decision F.8, 2026-09-27: up to 5 GiB on the largest plans).
|
|
7
|
+
*
|
|
8
|
+
* The ordinary `tables import --file` path reads the whole file into memory and
|
|
9
|
+
* parses every row into an object before it uploads anything. That is fine at a
|
|
10
|
+
* few MiB and impossible at a few GiB: a V8 string tops out near 1 GiB and the
|
|
11
|
+
* parsed rows weigh many times the file. Above this threshold a CSV or JSONL
|
|
12
|
+
* file therefore never enters memory whole. One streaming pass counts its rows,
|
|
13
|
+
* hashes it and collects its columns; the file is then PUT to object storage
|
|
14
|
+
* straight from disk, and the worker streams it again from there.
|
|
15
|
+
*
|
|
16
|
+
* JSON and XLSX cannot stream (a JSON array has no record boundary, an XLSX is a
|
|
17
|
+
* zip), so they never take this path: they are capped at
|
|
18
|
+
* MAX_BUFFERED_IMPORT_PARSE_BYTES before anything is read.
|
|
19
|
+
*/
|
|
20
|
+
export const STREAMED_FILE_IMPORT_MIN_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
/** Rows kept from the head of the file to infer a new table's column types from. */
|
|
22
|
+
export const STREAMED_IMPORT_SAMPLE_ROWS = 2_000;
|
|
23
|
+
/** An upload that moves no bytes for this long is abandoned, whatever its size. */
|
|
24
|
+
export const STREAMED_UPLOAD_STALL_TIMEOUT_MS = 120_000;
|
|
25
|
+
export function shouldStreamFileImport(format, fileBytes) {
|
|
26
|
+
return (format === "csv" || format === "jsonl") && fileBytes > STREAMED_FILE_IMPORT_MIN_BYTES;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the file once, as a stream: count its rows with the same parser the
|
|
30
|
+
* worker will use (so a malformed file fails here, before any upload), hash it,
|
|
31
|
+
* and collect its column labels. JSONL rows may each name different keys, so the
|
|
32
|
+
* labels come from every row; only the type-inference sample is bounded.
|
|
33
|
+
*/
|
|
34
|
+
export async function scanStreamedImportFile(path, format, options = {}) {
|
|
35
|
+
const sampleLimit = options.sampleRows ?? STREAMED_IMPORT_SAMPLE_ROWS;
|
|
36
|
+
const hash = createHash("sha256");
|
|
37
|
+
async function* hashedChunks() {
|
|
38
|
+
for await (const chunk of createReadStream(path)) {
|
|
39
|
+
const bytes = chunk;
|
|
40
|
+
hash.update(bytes);
|
|
41
|
+
yield bytes;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let rowCount = 0;
|
|
45
|
+
const labels = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const sampleRows = [];
|
|
48
|
+
for await (const batch of iterateRowsFileStreamBatches(hashedChunks(), format, { batchSize: 1_000 })) {
|
|
49
|
+
rowCount += batch.length;
|
|
50
|
+
for (const row of batch) {
|
|
51
|
+
if (sampleRows.length < sampleLimit)
|
|
52
|
+
sampleRows.push(row);
|
|
53
|
+
for (const key of Object.keys(row)) {
|
|
54
|
+
if (key.startsWith("_") || seen.has(key))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
labels.push(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { rowCount, sha256: hash.digest("hex"), labels, sampleRows };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The rows a target is prepared from: the sample, plus one empty row naming any
|
|
65
|
+
* column that first appears after it, so a created table gets every column the
|
|
66
|
+
* file has and an existing table's mapping sees every header.
|
|
67
|
+
*/
|
|
68
|
+
export function rowsForStreamedImportTarget(scan) {
|
|
69
|
+
const sampled = new Set(scan.sampleRows.flatMap((row) => Object.keys(row)));
|
|
70
|
+
const late = scan.labels.filter((label) => !sampled.has(label));
|
|
71
|
+
if (late.length === 0)
|
|
72
|
+
return scan.sampleRows;
|
|
73
|
+
return [...scan.sampleRows, Object.fromEntries(late.map((label) => [label, null]))];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* PUT a file to a presigned URL straight from disk. Content-Length is sent
|
|
77
|
+
* explicitly (S3 refuses a chunked PUT, and the presigner signed the length),
|
|
78
|
+
* and the request is abandoned only when it stops moving, never for being big:
|
|
79
|
+
* a 5 GiB upload on a slow link is legitimate, a silent socket is not.
|
|
80
|
+
*/
|
|
81
|
+
export async function putImportFileStream(input) {
|
|
82
|
+
const stallTimeoutMs = input.stallTimeoutMs ?? STREAMED_UPLOAD_STALL_TIMEOUT_MS;
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
let timer = null;
|
|
85
|
+
const arm = () => {
|
|
86
|
+
if (timer)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = setTimeout(() => controller.abort(new Error("The upload stopped making progress.")), stallTimeoutMs);
|
|
89
|
+
};
|
|
90
|
+
async function* progressing() {
|
|
91
|
+
for await (const chunk of createReadStream(input.path)) {
|
|
92
|
+
arm();
|
|
93
|
+
yield chunk;
|
|
94
|
+
}
|
|
95
|
+
// The last byte is out; the store now has one stall window to answer.
|
|
96
|
+
arm();
|
|
97
|
+
}
|
|
98
|
+
arm();
|
|
99
|
+
try {
|
|
100
|
+
return await (input.fetchImpl ?? fetch)(input.uploadUrl, {
|
|
101
|
+
method: "PUT",
|
|
102
|
+
headers: {
|
|
103
|
+
"content-type": input.contentType,
|
|
104
|
+
"content-length": String(input.byteLength),
|
|
105
|
+
},
|
|
106
|
+
body: Readable.toWeb(Readable.from(progressing())),
|
|
107
|
+
duplex: "half",
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (timer)
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/update.d.ts
CHANGED
|
@@ -12,9 +12,17 @@ export type UpdateOptions = {
|
|
|
12
12
|
json?: boolean;
|
|
13
13
|
package?: string;
|
|
14
14
|
dryRun?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Leave the skills refresh to the newly installed binary. An automatic update
|
|
17
|
+
* runs inside the OLD process, which the API may be refusing outright — its
|
|
18
|
+
* own skills request would be rejected for the very version being replaced.
|
|
19
|
+
*/
|
|
20
|
+
skipSkillsInstall?: boolean;
|
|
15
21
|
};
|
|
16
22
|
type UpdateResult = {
|
|
17
23
|
current_version: string;
|
|
24
|
+
/** Read back from the installed package after npm finished; null when unknown. */
|
|
25
|
+
installed_version?: string | null;
|
|
18
26
|
package: string;
|
|
19
27
|
command: string;
|
|
20
28
|
dry_run: boolean;
|
|
@@ -24,10 +32,31 @@ type UpdateResult = {
|
|
|
24
32
|
export declare function updateCli(options: UpdateOptions, runtime?: {
|
|
25
33
|
env?: NodeJS.ProcessEnv;
|
|
26
34
|
platform?: NodeJS.Platform;
|
|
35
|
+
argv?: readonly string[];
|
|
27
36
|
modulePath?: string;
|
|
28
37
|
spawnSync?: SpawnSyncLike;
|
|
29
38
|
execFileSync?: ExecFileSyncLike;
|
|
30
39
|
} & SkillsSourceRuntime): Promise<UpdateResult>;
|
|
40
|
+
export type CliInstall =
|
|
41
|
+
/** `npm install -g`: npm can replace it in place with `--prefix`. */
|
|
42
|
+
{
|
|
43
|
+
kind: "global";
|
|
44
|
+
prefix: string;
|
|
45
|
+
packageDir: string;
|
|
46
|
+
}
|
|
47
|
+
/** A project's own node_modules (npx, `npm install` in a directory, a pinned capsule). */
|
|
48
|
+
| {
|
|
49
|
+
kind: "local";
|
|
50
|
+
projectDir: string;
|
|
51
|
+
packageDir: string;
|
|
52
|
+
}
|
|
53
|
+
/** Running from a source checkout. */
|
|
54
|
+
| {
|
|
55
|
+
kind: "source";
|
|
56
|
+
};
|
|
57
|
+
export declare function detectCliInstall(modulePath: string): CliInstall;
|
|
31
58
|
export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
|
|
59
|
+
/** The version npm actually left on disk for the running package. */
|
|
60
|
+
export declare function readInstalledCliVersion(packageDir: string): string | null;
|
|
32
61
|
export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
|
|
33
62
|
export {};
|
package/dist/update.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
|
|
4
6
|
import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
|
|
7
|
+
import { resolveCliBinaryName } from "./runtime.js";
|
|
5
8
|
import { readOption } from "./util.js";
|
|
6
9
|
import { quoteWindowsCmdArg } from "./windows-shim.js";
|
|
7
10
|
const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
|
|
@@ -11,7 +14,9 @@ export async function updateCli(options, runtime = {}) {
|
|
|
11
14
|
const platform = runtime.platform ?? process.platform;
|
|
12
15
|
const spawn = runtime.spawnSync ?? spawnSync;
|
|
13
16
|
const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
|
|
14
|
-
const
|
|
17
|
+
const modulePath = runtime.modulePath ?? fileURLToPath(import.meta.url);
|
|
18
|
+
const install = detectCliInstall(modulePath);
|
|
19
|
+
const prefix = install.kind === "global" ? install.prefix : null;
|
|
15
20
|
const args = prefix
|
|
16
21
|
? ["install", "-g", "--prefix", prefix, packageSpec]
|
|
17
22
|
: ["install", "-g", packageSpec];
|
|
@@ -19,6 +24,8 @@ export async function updateCli(options, runtime = {}) {
|
|
|
19
24
|
// output). Quote args that contain shell-significant characters so a
|
|
20
25
|
// copy-paste of the line works on install prefixes with spaces.
|
|
21
26
|
const command = ["npm", ...args].map(quoteForDisplay).join(" ");
|
|
27
|
+
if (!options.dryRun)
|
|
28
|
+
assertUpdatableInstall(install, env, runtime.argv ?? process.argv);
|
|
22
29
|
if (options.dryRun) {
|
|
23
30
|
return {
|
|
24
31
|
current_version: OXYGEN_VERSION,
|
|
@@ -49,17 +56,20 @@ export async function updateCli(options, runtime = {}) {
|
|
|
49
56
|
if (!result.error && result.status === 0) {
|
|
50
57
|
return {
|
|
51
58
|
current_version: OXYGEN_VERSION,
|
|
59
|
+
installed_version: install.kind === "global" ? readInstalledCliVersion(install.packageDir) : null,
|
|
52
60
|
package: packageSpec,
|
|
53
61
|
command,
|
|
54
62
|
dry_run: false,
|
|
55
63
|
updated: true,
|
|
56
|
-
skills_install:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
skills_install: options.skipSkillsInstall
|
|
65
|
+
? skippedAutomaticSkillsInstall("deferred_to_updated_cli")
|
|
66
|
+
: await runAutomaticSkillsInstall({
|
|
67
|
+
env,
|
|
68
|
+
platform,
|
|
69
|
+
...(runtime.credentials !== undefined ? { credentials: runtime.credentials } : {}),
|
|
70
|
+
...(runtime.fetch ? { fetch: runtime.fetch } : {}),
|
|
71
|
+
...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
|
|
72
|
+
}),
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
break;
|
|
@@ -77,10 +87,7 @@ export async function updateCli(options, runtime = {}) {
|
|
|
77
87
|
exitCode: 1,
|
|
78
88
|
});
|
|
79
89
|
}
|
|
80
|
-
function
|
|
81
|
-
return detectCliInstallPrefixFromPath(modulePath);
|
|
82
|
-
}
|
|
83
|
-
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
90
|
+
export function detectCliInstall(modulePath) {
|
|
84
91
|
const normalizedPath = modulePath.replace(/\\/g, "/");
|
|
85
92
|
const markers = [
|
|
86
93
|
"/node_modules/@oxygen-agent/cli/dist/",
|
|
@@ -91,11 +98,50 @@ export function detectCliInstallPrefixFromPath(modulePath) {
|
|
|
91
98
|
if (markerIndex === -1)
|
|
92
99
|
continue;
|
|
93
100
|
const packageParent = normalizedPath.slice(0, markerIndex);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
const packageDir = normalizedPath.slice(0, markerIndex + marker.length - "/dist/".length);
|
|
102
|
+
if (packageParent.endsWith("/lib")) {
|
|
103
|
+
return { kind: "global", prefix: packageParent.slice(0, -"/lib".length), packageDir };
|
|
104
|
+
}
|
|
105
|
+
// npm's Windows global layout has no lib/: <prefix>\node_modules.
|
|
106
|
+
if (/^[A-Za-z]:\//.test(normalizedPath))
|
|
107
|
+
return { kind: "global", prefix: packageParent, packageDir };
|
|
108
|
+
// Anywhere else, `npm install -g --prefix <dir>` writes <dir>/lib/node_modules — a
|
|
109
|
+
// different tree from the one running — and reports success while nothing changed.
|
|
110
|
+
return { kind: "local", projectDir: packageParent, packageDir };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "source" };
|
|
113
|
+
}
|
|
114
|
+
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
115
|
+
const install = detectCliInstall(modulePath);
|
|
116
|
+
return install.kind === "global" ? install.prefix : null;
|
|
117
|
+
}
|
|
118
|
+
/** The version npm actually left on disk for the running package. */
|
|
119
|
+
export function readInstalledCliVersion(packageDir) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
122
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function assertUpdatableInstall(install, env, argv) {
|
|
129
|
+
if (resolveCliBinaryName(env, argv) === "oxygen-dev") {
|
|
130
|
+
throw new OxygenError("cli_update_unsupported_install", "oxygen-dev is built from the dev branch, not installed from npm. "
|
|
131
|
+
+ "Rebuild it with `npm run install-dev-cli` from the latest dev checkout.", { details: { install: "dev_build", update_command: "npm run install-dev-cli" }, exitCode: 1 });
|
|
132
|
+
}
|
|
133
|
+
if (install.kind === "local") {
|
|
134
|
+
throw new OxygenError("cli_update_unsupported_install", `This Oxygen CLI runs from a project-local install in ${install.projectDir}, which \`oxygen update\` cannot replace. `
|
|
135
|
+
+ "Run `npm install @oxygen-agent/cli@latest` in that directory, or install globally with "
|
|
136
|
+
+ "`npm install -g @oxygen-agent/cli`.", {
|
|
137
|
+
details: {
|
|
138
|
+
install: "local",
|
|
139
|
+
install_dir: install.projectDir,
|
|
140
|
+
update_command: "npm install @oxygen-agent/cli@latest",
|
|
141
|
+
},
|
|
142
|
+
exitCode: 1,
|
|
143
|
+
});
|
|
97
144
|
}
|
|
98
|
-
return null;
|
|
99
145
|
}
|
|
100
146
|
export function resolveNpmExecutables(platform = process.platform) {
|
|
101
147
|
return platform === "win32" ? ["cmd.exe"] : ["npm"];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the plan does to one workflow's trigger, as stderr lines beside the
|
|
3
|
+
* `workflows get` / `workflows apply` envelope: a cron the scheduler fires at
|
|
4
|
+
* the plan's minimum interval instead of as written (repricing L4.2, P-47), and
|
|
5
|
+
* the deliveries a minute a webhook trigger's URL accepts (L4.4). The envelope
|
|
6
|
+
* carries both as data (`workflow.trigger.slowedToPlanMinimum`, `rate_limit`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatWorkflowPlanLimitNotices(data: unknown): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the plan does to one workflow's trigger, as stderr lines beside the
|
|
3
|
+
* `workflows get` / `workflows apply` envelope: a cron the scheduler fires at
|
|
4
|
+
* the plan's minimum interval instead of as written (repricing L4.2, P-47), and
|
|
5
|
+
* the deliveries a minute a webhook trigger's URL accepts (L4.4). The envelope
|
|
6
|
+
* carries both as data (`workflow.trigger.slowedToPlanMinimum`, `rate_limit`).
|
|
7
|
+
*/
|
|
8
|
+
export function formatWorkflowPlanLimitNotices(data) {
|
|
9
|
+
const block = asRecord(data);
|
|
10
|
+
const workflow = asRecord(block?.workflow);
|
|
11
|
+
const trigger = asRecord(workflow?.trigger);
|
|
12
|
+
if (!block || !trigger)
|
|
13
|
+
return [];
|
|
14
|
+
const slug = typeof workflow?.slug === "string" ? workflow.slug : "workflow";
|
|
15
|
+
const notices = [];
|
|
16
|
+
const minutes = asRecord(trigger.slowedToPlanMinimum)?.planMinimumIntervalMinutes;
|
|
17
|
+
if (typeof minutes === "number") {
|
|
18
|
+
notices.push(`${slug}: schedule slowed — your plan runs a schedule at most every ${minutes} minutes, so this one fires every ${minutes} minutes instead of as written. Upgrade (\`oxygen billing change\`) or change the schedule.`);
|
|
19
|
+
}
|
|
20
|
+
const perWindow = asRecord(block.rate_limit)?.requests_per_window;
|
|
21
|
+
if (trigger.type === "webhook" && typeof perWindow === "number") {
|
|
22
|
+
notices.push(`${slug}: webhook accepts up to ${perWindow.toLocaleString("en-US")} deliveries a minute on your plan; more get HTTP 429 with Retry-After (\`oxygen limits show\`).`);
|
|
23
|
+
}
|
|
24
|
+
return notices;
|
|
25
|
+
}
|
|
26
|
+
function asRecord(value) {
|
|
27
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
28
|
+
}
|
|
@@ -1023,7 +1023,7 @@ export function registerUgcCommands(program, { request: requestOxygen, handle })
|
|
|
1023
1023
|
{
|
|
1024
1024
|
action: "sponsorship_cancel",
|
|
1025
1025
|
command: "sponsorship cancel",
|
|
1026
|
-
description: "Preview stopping renewal for one program sponsorship. Confirm with --approved and a signed-in master admin.
|
|
1026
|
+
description: "Preview stopping renewal for one program sponsorship. Confirm with --approved and a signed-in master admin. The paid period stays; a credit sponsorship stops without a card provider call.",
|
|
1027
1027
|
fields: [
|
|
1028
1028
|
{
|
|
1029
1029
|
name: "program_id",
|
|
@@ -1048,7 +1048,7 @@ export function registerUgcCommands(program, { request: requestOxygen, handle })
|
|
|
1048
1048
|
{
|
|
1049
1049
|
action: "sponsorship_preview",
|
|
1050
1050
|
command: "sponsorship preview",
|
|
1051
|
-
description: "Preview
|
|
1051
|
+
description: "Preview sponsoring creators as a signed-in master admin: funding is card (seat plus 10,000-credit grant) or credits (one LinkedIn reservation per creator). Org API keys cannot quote.",
|
|
1052
1052
|
fields: [
|
|
1053
1053
|
{
|
|
1054
1054
|
name: "program_id",
|
|
@@ -1067,7 +1067,7 @@ export function registerUgcCommands(program, { request: requestOxygen, handle })
|
|
|
1067
1067
|
{
|
|
1068
1068
|
action: "sponsorship_confirm",
|
|
1069
1069
|
command: "sponsorship confirm",
|
|
1070
|
-
description: "Confirm the exact sponsorship quote as the same signed-in master
|
|
1070
|
+
description: "Confirm the exact sponsorship quote as the same signed-in master admin. Card quotes return checkouts; credit quotes start the reservations or fail with insufficient_credits.",
|
|
1071
1071
|
fields: [
|
|
1072
1072
|
{
|
|
1073
1073
|
name: "program_id",
|
|
@@ -89,3 +89,20 @@ export declare function subscriptionGrantSlices(periodStart: Date, periodEnd: Da
|
|
|
89
89
|
export declare function dueSubscriptionGrantSlices(periodStart: Date, periodEnd: Date, now: Date): SubscriptionGrantSlice[];
|
|
90
90
|
/** The slice `now` falls in, or null outside the period. */
|
|
91
91
|
export declare function currentSubscriptionGrantSlice(periodStart: Date, periodEnd: Date, now: Date): SubscriptionGrantSlice | null;
|
|
92
|
+
/**
|
|
93
|
+
* When the next monthly credit grant of a paid plan arrives (repricing 2026-09,
|
|
94
|
+
* S13: "know when my credits arrive").
|
|
95
|
+
*
|
|
96
|
+
* A monthly plan grants with its renewal, at the period end. A yearly plan
|
|
97
|
+
* grants each slice at its start, so the next grant is the end of the slice
|
|
98
|
+
* `now` falls in; the last slice's end is the renewal. A grant that depends on
|
|
99
|
+
* the renewal is null when the renewal will not happen (`renews: false`: a
|
|
100
|
+
* scheduled cancellation, a lapsed or unpaid plan), because no grant follows.
|
|
101
|
+
*/
|
|
102
|
+
export declare function nextSubscriptionCreditGrantAt(input: {
|
|
103
|
+
periodStart: Date | null | undefined;
|
|
104
|
+
periodEnd: Date | null | undefined;
|
|
105
|
+
billingInterval: "month" | "year" | null | undefined;
|
|
106
|
+
renews: boolean;
|
|
107
|
+
now?: Date;
|
|
108
|
+
}): Date | null;
|
|
@@ -198,3 +198,30 @@ export function dueSubscriptionGrantSlices(periodStart, periodEnd, now) {
|
|
|
198
198
|
export function currentSubscriptionGrantSlice(periodStart, periodEnd, now) {
|
|
199
199
|
return (subscriptionGrantSlices(periodStart, periodEnd).find((slice) => slice.start.getTime() <= now.getTime() && now.getTime() < slice.end.getTime()) ?? null);
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* When the next monthly credit grant of a paid plan arrives (repricing 2026-09,
|
|
203
|
+
* S13: "know when my credits arrive").
|
|
204
|
+
*
|
|
205
|
+
* A monthly plan grants with its renewal, at the period end. A yearly plan
|
|
206
|
+
* grants each slice at its start, so the next grant is the end of the slice
|
|
207
|
+
* `now` falls in; the last slice's end is the renewal. A grant that depends on
|
|
208
|
+
* the renewal is null when the renewal will not happen (`renews: false`: a
|
|
209
|
+
* scheduled cancellation, a lapsed or unpaid plan), because no grant follows.
|
|
210
|
+
*/
|
|
211
|
+
export function nextSubscriptionCreditGrantAt(input) {
|
|
212
|
+
const now = input.now ?? new Date();
|
|
213
|
+
const end = input.periodEnd;
|
|
214
|
+
if (!(end instanceof Date) || Number.isNaN(end.getTime()))
|
|
215
|
+
return null;
|
|
216
|
+
if (end.getTime() <= now.getTime())
|
|
217
|
+
return null;
|
|
218
|
+
const start = input.periodStart;
|
|
219
|
+
if (input.billingInterval === "year"
|
|
220
|
+
&& start instanceof Date
|
|
221
|
+
&& !Number.isNaN(start.getTime())) {
|
|
222
|
+
const slice = currentSubscriptionGrantSlice(start, end, now);
|
|
223
|
+
if (slice && slice.end.getTime() < end.getTime())
|
|
224
|
+
return slice.end;
|
|
225
|
+
}
|
|
226
|
+
return input.renews ? end : null;
|
|
227
|
+
}
|