@oxygen-agent/cli 1.50.37 → 1.64.5

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.
@@ -0,0 +1,41 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export type ExecFileSyncLike = typeof execFileSync;
3
+ export type SkillsInstallOptions = {
4
+ json?: boolean;
5
+ apiUrl?: string;
6
+ agents?: string | string[];
7
+ skill?: string;
8
+ project?: boolean;
9
+ copy?: boolean;
10
+ };
11
+ export type SkillsListOptions = {
12
+ json?: boolean;
13
+ apiUrl?: string;
14
+ };
15
+ export type SkillsDoctorOptions = {
16
+ json?: boolean;
17
+ apiUrl?: string;
18
+ };
19
+ export type AutomaticSkillsInstallResult = {
20
+ attempted: boolean;
21
+ ok: boolean;
22
+ command: string;
23
+ reason?: string;
24
+ error?: {
25
+ code: string;
26
+ message: string;
27
+ details?: unknown;
28
+ };
29
+ };
30
+ export declare function listAgentSkills(options: SkillsListOptions): Promise<Record<string, unknown>>;
31
+ export declare function doctorAgentSkills(options: SkillsDoctorOptions): Promise<Record<string, unknown>>;
32
+ export declare function installAgentSkills(options: SkillsInstallOptions, runtime?: {
33
+ env?: NodeJS.ProcessEnv;
34
+ execFileSync?: ExecFileSyncLike;
35
+ }): Record<string, unknown>;
36
+ export declare function runAutomaticSkillsInstall(options?: {
37
+ apiUrl?: string;
38
+ env?: NodeJS.ProcessEnv;
39
+ execFileSync?: ExecFileSyncLike;
40
+ }): AutomaticSkillsInstallResult;
41
+ export declare function skippedAutomaticSkillsInstall(reason: string, apiUrl?: string): AutomaticSkillsInstallResult;
package/dist/skills.js ADDED
@@ -0,0 +1,325 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { OxygenError, toFailure } from "@oxygen/shared";
6
+ import { defaultApiUrl, normalizeApiUrl } from "./credentials.js";
7
+ const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
8
+ const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
9
+ export async function listAgentSkills(options) {
10
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
11
+ const indexUrl = skillIndexUrl(apiUrl);
12
+ const index = await inspectSkillIndex(indexUrl);
13
+ const installed = detectInstalledAgentSkills();
14
+ return {
15
+ index_url: indexUrl,
16
+ available: {
17
+ detected: index.reachable,
18
+ names: index.skill_names,
19
+ skills: index.skills,
20
+ ...(index.error ? { error: index.error } : {}),
21
+ },
22
+ installed,
23
+ recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
24
+ install_command_args: recommendedSkillsInstallArgs(apiUrl),
25
+ };
26
+ }
27
+ export async function doctorAgentSkills(options) {
28
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
29
+ const indexUrl = skillIndexUrl(apiUrl);
30
+ const index = await inspectSkillIndex(indexUrl);
31
+ const installer = inspectSkillsInstaller();
32
+ const installed = detectInstalledAgentSkills();
33
+ const data = {
34
+ index_url: indexUrl,
35
+ index,
36
+ installer,
37
+ installed,
38
+ recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
39
+ install_command_args: recommendedSkillsInstallArgs(apiUrl),
40
+ };
41
+ if (!index.reachable) {
42
+ throw new OxygenError("skills_index_unreachable", "Oxygen skill index is not reachable.", {
43
+ details: data,
44
+ exitCode: 1,
45
+ });
46
+ }
47
+ if (!installer.available) {
48
+ throw new OxygenError("skills_installer_unavailable", "The local npx installer command is not available.", {
49
+ details: data,
50
+ exitCode: 1,
51
+ });
52
+ }
53
+ return data;
54
+ }
55
+ export function installAgentSkills(options, runtime = {}) {
56
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
57
+ const indexUrl = skillIndexUrl(apiUrl);
58
+ const agents = readWords(options.agents ?? DEFAULT_SKILL_AGENTS);
59
+ const skill = readOption(options.skill) ?? "*";
60
+ const args = installerAddArgs(indexUrl, agents, skill);
61
+ if (!options.project)
62
+ args.push("--global");
63
+ if (options.copy)
64
+ args.push("--copy");
65
+ let output = "";
66
+ try {
67
+ const exec = runtime.execFileSync ?? execFileSync;
68
+ output = String(exec("npx", args, {
69
+ encoding: "utf8",
70
+ env: runtime.env ?? process.env,
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ }));
73
+ }
74
+ catch (error) {
75
+ const failure = error;
76
+ throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
77
+ details: {
78
+ index_url: indexUrl,
79
+ stdout: failure.stdout?.slice(0, 2000) ?? "",
80
+ stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
81
+ },
82
+ exitCode: 1,
83
+ });
84
+ }
85
+ return {
86
+ indexUrl,
87
+ index_url: indexUrl,
88
+ agents,
89
+ skill,
90
+ scope: options.project ? "project" : "global",
91
+ output,
92
+ };
93
+ }
94
+ export function runAutomaticSkillsInstall(options = {}) {
95
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
96
+ const env = options.env ?? process.env;
97
+ if (env.OXYGEN_SKIP_SKILLS === "1") {
98
+ return skippedAutomaticSkillsInstall("OXYGEN_SKIP_SKILLS=1", apiUrl);
99
+ }
100
+ const command = recommendedSkillsInstallCommand(apiUrl);
101
+ try {
102
+ installAgentSkills({
103
+ apiUrl,
104
+ agents: DEFAULT_SKILL_AGENTS,
105
+ skill: "*",
106
+ json: true,
107
+ }, {
108
+ env,
109
+ ...(options.execFileSync ? { execFileSync: options.execFileSync } : {}),
110
+ });
111
+ return {
112
+ attempted: true,
113
+ ok: true,
114
+ command,
115
+ };
116
+ }
117
+ catch (error) {
118
+ const failure = toFailure("skills install", error);
119
+ return {
120
+ attempted: true,
121
+ ok: false,
122
+ command,
123
+ error: failure.error,
124
+ };
125
+ }
126
+ }
127
+ export function skippedAutomaticSkillsInstall(reason, apiUrl = normalizeApiUrl(defaultApiUrl())) {
128
+ return {
129
+ attempted: false,
130
+ ok: true,
131
+ command: recommendedSkillsInstallCommand(apiUrl),
132
+ reason,
133
+ };
134
+ }
135
+ async function inspectSkillIndex(indexUrl) {
136
+ const controller = new AbortController();
137
+ const timeout = setTimeout(() => controller.abort(), 10_000);
138
+ timeout.unref?.();
139
+ try {
140
+ const response = await fetch(indexUrl, {
141
+ headers: { Accept: "application/json" },
142
+ signal: controller.signal,
143
+ });
144
+ const text = await response.text();
145
+ if (!response.ok) {
146
+ return emptySkillIndexInspection({
147
+ status: response.status,
148
+ error: { code: "http_error", message: `HTTP ${response.status}` },
149
+ });
150
+ }
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(text);
154
+ }
155
+ catch (error) {
156
+ return emptySkillIndexInspection({
157
+ status: response.status,
158
+ error: {
159
+ code: "invalid_json",
160
+ message: error instanceof Error ? error.message : "Invalid JSON.",
161
+ },
162
+ });
163
+ }
164
+ let index;
165
+ try {
166
+ index = parseSkillIndex(parsed);
167
+ }
168
+ catch (error) {
169
+ return emptySkillIndexInspection({
170
+ status: response.status,
171
+ error: {
172
+ code: error instanceof OxygenError ? error.code : "invalid_skill_index",
173
+ message: error instanceof Error ? error.message : "Skill index shape is invalid.",
174
+ },
175
+ });
176
+ }
177
+ return {
178
+ reachable: true,
179
+ status: response.status,
180
+ version: index.version,
181
+ base_url: index.base_url,
182
+ skill_names: index.skills.map((skill) => skill.name),
183
+ skills: index.skills,
184
+ };
185
+ }
186
+ catch (error) {
187
+ const aborted = error instanceof Error && error.name === "AbortError";
188
+ return emptySkillIndexInspection({
189
+ status: null,
190
+ error: {
191
+ code: aborted ? "network_timeout" : "network_error",
192
+ message: error instanceof Error ? error.message : "Unable to reach skill index.",
193
+ },
194
+ });
195
+ }
196
+ finally {
197
+ clearTimeout(timeout);
198
+ }
199
+ }
200
+ function emptySkillIndexInspection(input) {
201
+ return {
202
+ reachable: false,
203
+ status: input.status,
204
+ version: null,
205
+ base_url: null,
206
+ skill_names: [],
207
+ skills: [],
208
+ error: input.error,
209
+ };
210
+ }
211
+ function parseSkillIndex(value) {
212
+ if (!isRecord(value) || !Array.isArray(value.skills)) {
213
+ throw new OxygenError("invalid_skill_index", "Skill index must include a skills array.", {
214
+ exitCode: 1,
215
+ });
216
+ }
217
+ const skills = value.skills
218
+ .map((entry) => {
219
+ if (!isRecord(entry) || typeof entry.name !== "string" || !entry.name.trim())
220
+ return null;
221
+ return {
222
+ name: entry.name.trim(),
223
+ title: typeof entry.title === "string" ? entry.title : null,
224
+ description: typeof entry.description === "string" ? entry.description : null,
225
+ entrypoint: typeof entry.entrypoint === "string" ? entry.entrypoint : null,
226
+ };
227
+ })
228
+ .filter((entry) => entry !== null);
229
+ return {
230
+ version: typeof value.version === "string" ? value.version : null,
231
+ base_url: typeof value.base_url === "string" ? value.base_url : null,
232
+ skills,
233
+ };
234
+ }
235
+ function inspectSkillsInstaller() {
236
+ const result = spawnSync("npx", ["--version"], {
237
+ encoding: "utf8",
238
+ stdio: ["ignore", "pipe", "pipe"],
239
+ timeout: 5000,
240
+ });
241
+ const error = result.error instanceof Error ? result.error.message : null;
242
+ const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
243
+ const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
244
+ return {
245
+ command_path: "npx",
246
+ check_args: ["--version"],
247
+ available: result.status === 0 && !error,
248
+ version: stdout || null,
249
+ ...(error ? { error } : {}),
250
+ ...(stderr ? { stderr: stderr.slice(0, 1000) } : {}),
251
+ };
252
+ }
253
+ function detectInstalledAgentSkills() {
254
+ const roots = [
255
+ { agent: "codex", path: join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "skills") },
256
+ { agent: "claude-code", path: join(process.env.CLAUDE_HOME ?? join(homedir(), ".claude"), "skills") },
257
+ { agent: "cursor", path: join(process.env.CURSOR_HOME ?? join(homedir(), ".cursor"), "skills") },
258
+ { agent: "project", path: resolve(".agents", "skills") },
259
+ ];
260
+ const names = new Set();
261
+ const locations = [];
262
+ for (const root of roots) {
263
+ if (!existsSync(root.path))
264
+ continue;
265
+ try {
266
+ const rootNames = readdirSync(root.path, { withFileTypes: true })
267
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("oxygen-"))
268
+ .map((entry) => entry.name)
269
+ .sort();
270
+ for (const name of rootNames)
271
+ names.add(name);
272
+ locations.push({ ...root, names: rootNames });
273
+ }
274
+ catch {
275
+ locations.push({ ...root, names: [] });
276
+ }
277
+ }
278
+ return {
279
+ detected: locations.length > 0,
280
+ names: [...names].sort(),
281
+ locations,
282
+ };
283
+ }
284
+ function skillIndexUrl(apiUrl) {
285
+ return `${apiUrl}${SKILL_INDEX_PATH}`;
286
+ }
287
+ function recommendedSkillsInstallCommand(apiUrl) {
288
+ return recommendedSkillsInstallArgs(apiUrl)
289
+ .map((entry) => entry === "*" ? "'*'" : entry)
290
+ .join(" ");
291
+ }
292
+ function recommendedSkillsInstallArgs(apiUrl) {
293
+ const args = ["oxygen", "skills", "install"];
294
+ if (apiUrl !== normalizeApiUrl(defaultApiUrl())) {
295
+ args.push("--api-url", apiUrl);
296
+ }
297
+ args.push("--agents", ...DEFAULT_SKILL_AGENTS, "--skill", "*", "--json");
298
+ return args;
299
+ }
300
+ function installerAddArgs(indexUrl, agents, skill) {
301
+ return [
302
+ "skills",
303
+ "add",
304
+ indexUrl,
305
+ "--agents",
306
+ ...agents,
307
+ "--yes",
308
+ "--skill",
309
+ skill,
310
+ "--full-depth",
311
+ ];
312
+ }
313
+ function readWords(value) {
314
+ const text = Array.isArray(value) ? value.join(" ") : value;
315
+ return text
316
+ .split(/[,\s]+/)
317
+ .map((entry) => entry.trim())
318
+ .filter(Boolean);
319
+ }
320
+ function readOption(value) {
321
+ return value?.trim() ? value.trim() : null;
322
+ }
323
+ function isRecord(value) {
324
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
325
+ }
package/dist/update.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { type AutomaticSkillsInstallResult, type ExecFileSyncLike } from "./skills.js";
2
3
  type SpawnResult = ReturnType<typeof spawnSync>;
3
4
  type SpawnSyncLike = (command: string, args: string[], options: {
4
5
  encoding: BufferEncoding;
@@ -18,12 +19,14 @@ export type UpdateResult = {
18
19
  command: string;
19
20
  dry_run: boolean;
20
21
  updated: boolean;
22
+ skills_install: AutomaticSkillsInstallResult;
21
23
  };
22
24
  export declare function updateCli(options: UpdateOptions, runtime?: {
23
25
  env?: NodeJS.ProcessEnv;
24
26
  platform?: NodeJS.Platform;
25
27
  modulePath?: string;
26
28
  spawnSync?: SpawnSyncLike;
29
+ execFileSync?: ExecFileSyncLike;
27
30
  }): UpdateResult;
28
31
  export declare function detectCliInstallPrefix(modulePath?: string): string | null;
29
32
  export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
package/dist/update.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
4
+ import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
4
5
  const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
6
+ // skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
5
7
  export function updateCli(options, runtime = {}) {
6
8
  const env = runtime.env ?? process.env;
7
9
  const platform = runtime.platform ?? process.platform;
@@ -22,6 +24,7 @@ export function updateCli(options, runtime = {}) {
22
24
  command,
23
25
  dry_run: true,
24
26
  updated: false,
27
+ skills_install: skippedAutomaticSkillsInstall("dry_run"),
25
28
  };
26
29
  }
27
30
  const attemptedExecutables = [];
@@ -48,6 +51,10 @@ export function updateCli(options, runtime = {}) {
48
51
  command,
49
52
  dry_run: false,
50
53
  updated: true,
54
+ skills_install: runAutomaticSkillsInstall({
55
+ env,
56
+ ...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
57
+ }),
51
58
  };
52
59
  }
53
60
  break;
@@ -52,6 +52,7 @@ const IDENTIFIER_COLUMN_RE = /(?:^|[^a-z0-9])(phone|mobile|tel(?:ephone)?|fax|zi
52
52
  * a value as the absence of a string. Surface-specific concerns (HTML
53
53
  * escaping, ellipsis budgets) belong outside this function.
54
54
  */
55
+ // skipcq: JS-R1005 — dispatches across cell types + surfaces; branches are intrinsic
55
56
  export function formatCellForDisplay(value, column, options) {
56
57
  if (value === null || value === undefined || value === "")
57
58
  return "";
@@ -198,9 +199,7 @@ function shouldRescueColumn(column) {
198
199
  const dataType = readDataType(column);
199
200
  if (dataType !== "text" && dataType !== "numeric")
200
201
  return false;
201
- if (columnLooksLikeIdentifier(column))
202
- return false;
203
- return true;
202
+ return !columnLooksLikeIdentifier(column);
204
203
  }
205
204
  /**
206
205
  * Decide whether the same value-level + column-level guards used inside
@@ -4,6 +4,7 @@ export * from "./cell-format.js";
4
4
  export * from "./column-types.js";
5
5
  export * from "./credit-guidance.js";
6
6
  export * from "./log.js";
7
+ export * from "./provider-request-outcomes.js";
7
8
  export * from "./telemetry.js";
8
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
9
10
  [key: string]: JsonValue;
@@ -5,6 +5,7 @@ export * from "./cell-format.js";
5
5
  export * from "./column-types.js";
6
6
  export * from "./credit-guidance.js";
7
7
  export * from "./log.js";
8
+ export * from "./provider-request-outcomes.js";
8
9
  export * from "./telemetry.js";
9
10
  export class OxygenError extends Error {
10
11
  code;
@@ -0,0 +1,3 @@
1
+ export declare const PROVIDER_REQUEST_OUTCOMES: readonly ["success", "error", "blocked"];
2
+ export type ProviderRequestOutcome = (typeof PROVIDER_REQUEST_OUTCOMES)[number];
3
+ export declare function isProviderRequestOutcome(value: unknown): value is ProviderRequestOutcome;
@@ -0,0 +1,5 @@
1
+ export const PROVIDER_REQUEST_OUTCOMES = ["success", "error", "blocked"];
2
+ const PROVIDER_REQUEST_OUTCOME_SET = new Set(PROVIDER_REQUEST_OUTCOMES);
3
+ export function isProviderRequestOutcome(value) {
4
+ return typeof value === "string" && PROVIDER_REQUEST_OUTCOME_SET.has(value);
5
+ }
@@ -1 +1 @@
1
- export declare const OXYGEN_VERSION = "1.50.37";
1
+ export declare const OXYGEN_VERSION = "1.64.5";
@@ -1 +1 @@
1
- export const OXYGEN_VERSION = "1.50.37";
1
+ export const OXYGEN_VERSION = "1.64.5";
@@ -411,11 +411,44 @@ export async function runPureWorkflowFunction(input) {
411
411
  const first = issues[0];
412
412
  throw new Error(first ? `${first.code}: ${first.message}` : "Invalid workflow function source.");
413
413
  }
414
+ // Cross the host/sandbox boundary as a JSON string, then parse INSIDE the
415
+ // vm context. If we passed the host object directly its prototype chain
416
+ // would point at the host's Object/Function, and the regex token blacklist
417
+ // is trivially bypassed (e.g. "con"+"structor") to reach
418
+ // __oxygen_context.constructor.constructor === host Function — which is
419
+ // not affected by the new context's codeGeneration setting and yields
420
+ // worker-process RCE. Parsing inside the context rebinds the prototype to
421
+ // the sandboxed Object, so the same walk reaches the sandboxed Function,
422
+ // which then trips contextCodeGeneration.strings=false below.
414
423
  const context = toJsonValue(input.context, "context");
424
+ const contextJson = JSON.stringify(context);
415
425
  const sandbox = Object.create(null);
416
- sandbox.__oxygen_context = context;
417
- const script = new vm.Script(`"use strict";\nconst __oxygen_fn = (${input.source});\n__oxygen_fn(__oxygen_context);`);
418
- const result = script.runInNewContext(sandbox, { timeout: timeoutMs });
426
+ sandbox.__oxygen_context_json = contextJson;
427
+ // Shadow dangerous globals on the sandbox surface. Defense-in-depth: the
428
+ // load-bearing block is contextCodeGeneration; this just removes the
429
+ // obvious top-level handles.
430
+ sandbox.Function = undefined;
431
+ sandbox.eval = undefined;
432
+ sandbox.setTimeout = undefined;
433
+ sandbox.setInterval = undefined;
434
+ sandbox.setImmediate = undefined;
435
+ sandbox.queueMicrotask = undefined;
436
+ sandbox.WebAssembly = undefined;
437
+ const script = new vm.Script(`"use strict";\n`
438
+ + `const __oxygen_context = JSON.parse(__oxygen_context_json);\n`
439
+ + `const __oxygen_fn = (${input.source});\n`
440
+ + `__oxygen_fn(__oxygen_context);`);
441
+ // Disable dynamic code generation in the sandboxed context. Combined with
442
+ // the JSON re-parse above, this means every prototype-walk path to
443
+ // Function — direct, via context.constructor.constructor, or any other
444
+ // reachable Function instance — throws EvalError when called with source.
445
+ const result = script.runInNewContext(sandbox, {
446
+ timeout: timeoutMs,
447
+ contextCodeGeneration: {
448
+ strings: false,
449
+ wasm: false,
450
+ },
451
+ });
419
452
  const resolved = isPromiseLike(result)
420
453
  ? await withTimeout(result, timeoutMs)
421
454
  : result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.50.37",
3
+ "version": "1.64.5",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",