@oxygen-agent/cli 1.50.37 → 1.98.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation and support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.50.37
37
+ Version: 1.98.7
@@ -44,7 +44,7 @@ export async function createBrowserLoginSession(apiUrl) {
44
44
  cli_state: state,
45
45
  source: "oxygen_cli",
46
46
  });
47
- const redirectPath = `/settings/api-keys?${redirectParams.toString()}`;
47
+ const redirectPath = `/settings/cli?${redirectParams.toString()}`;
48
48
  const verificationUrl = new URL("/auth/sign-in", apiUrl);
49
49
  verificationUrl.searchParams.set("redirect_url", redirectPath);
50
50
  verificationUrl.searchParams.set("source", "oxygen_cli");
@@ -1,6 +1,22 @@
1
+ export type StoredProfileIdentity = {
2
+ organization: StoredOrganization | null;
3
+ user: {
4
+ id: string;
5
+ email: string | null;
6
+ };
7
+ capturedAt: string;
8
+ };
9
+ export type StoredOrganization = {
10
+ id: string;
11
+ slug: string | null;
12
+ name: string;
13
+ };
1
14
  export type StoredCredentials = {
2
15
  token: string;
3
16
  apiUrl: string;
17
+ authKind?: "org_api_key" | "user_session";
18
+ activeOrganization?: StoredOrganization | null;
19
+ identity?: StoredProfileIdentity;
4
20
  };
5
21
  export type CredentialProfile = StoredCredentials & {
6
22
  name: string;
@@ -23,5 +39,22 @@ export declare function clearCredentials(env?: NodeJS.ProcessEnv, options?: {
23
39
  remainingProfiles: number;
24
40
  }>;
25
41
  export declare function listCredentialProfiles(env?: NodeJS.ProcessEnv): Promise<CredentialProfilesState>;
42
+ export type ProfileResolution = {
43
+ name: string;
44
+ source: "env" | "file" | "default";
45
+ credentials: StoredCredentials | null;
46
+ exists: boolean;
47
+ };
48
+ export declare function resolveActiveProfile(env?: NodeJS.ProcessEnv): Promise<ProfileResolution>;
49
+ export declare function findProfileNameByOrganizationId(organizationId: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
50
+ export declare function pickProfileNameForIdentity(organizationId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
51
+ name: string;
52
+ renamed: boolean;
53
+ }>;
54
+ export declare function pickProfileNameForUserSession(userId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
55
+ name: string;
56
+ renamed: boolean;
57
+ }>;
26
58
  export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
59
+ export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
27
60
  export declare function normalizeApiUrl(value: string): string;
@@ -16,10 +16,14 @@ export function defaultApiUrl(env = process.env) {
16
16
  export async function loadCredentials(env = process.env) {
17
17
  const envToken = readEnvToken(env);
18
18
  if (envToken) {
19
- return {
19
+ const credentials = {
20
20
  token: envToken,
21
21
  apiUrl: defaultApiUrl(env),
22
22
  };
23
+ const authKind = inferAuthKind(envToken);
24
+ if (authKind)
25
+ credentials.authKind = authKind;
26
+ return credentials;
23
27
  }
24
28
  const document = await readSystemCredentialDocument(env);
25
29
  if (!document)
@@ -43,9 +47,22 @@ export async function saveCredentials(credentials, env = process.env, options =
43
47
  token: credentials.token,
44
48
  apiUrl: normalizeApiUrl(credentials.apiUrl),
45
49
  };
50
+ if (credentials.authKind)
51
+ normalized.authKind = credentials.authKind;
52
+ if (credentials.activeOrganization !== undefined) {
53
+ normalized.activeOrganization = credentials.activeOrganization;
54
+ }
55
+ if (credentials.identity)
56
+ normalized.identity = credentials.identity;
46
57
  const existing = await readSystemCredentialDocument(env);
47
58
  const profile = normalizeProfileName(options.profile ?? readEnvProfile(env) ?? existing?.activeProfile ?? DEFAULT_PROFILE_NAME);
48
59
  const profiles = existing ? readProfiles(existing) : {};
60
+ if (!normalized.identity) {
61
+ const prior = profiles[profile];
62
+ if (prior?.identity && prior.token === normalized.token) {
63
+ normalized.identity = prior.identity;
64
+ }
65
+ }
49
66
  profiles[profile] = normalized;
50
67
  const activeProfile = options.activate === false
51
68
  ? readProfileName(existing?.activeProfile) ?? profile
@@ -88,12 +105,18 @@ export async function clearCredentials(env = process.env, options = {}) {
88
105
  export async function listCredentialProfiles(env = process.env) {
89
106
  const envToken = readEnvToken(env);
90
107
  if (envToken) {
108
+ const credentials = {
109
+ token: envToken,
110
+ apiUrl: defaultApiUrl(env),
111
+ };
112
+ const authKind = inferAuthKind(envToken);
113
+ if (authKind)
114
+ credentials.authKind = authKind;
91
115
  return {
92
116
  activeProfile: readEnvProfile(env) ?? "env",
93
117
  profiles: [{
94
118
  name: readEnvProfile(env) ?? "env",
95
- token: envToken,
96
- apiUrl: defaultApiUrl(env),
119
+ ...credentials,
97
120
  }],
98
121
  };
99
122
  }
@@ -116,6 +139,73 @@ export async function listCredentialProfiles(env = process.env) {
116
139
  }),
117
140
  };
118
141
  }
142
+ export async function resolveActiveProfile(env = process.env) {
143
+ const envProfile = readEnvProfile(env);
144
+ const document = await readSystemCredentialDocument(env);
145
+ const profiles = document ? readProfiles(document) : {};
146
+ const fileActive = readProfileName(document?.activeProfile);
147
+ const candidate = envProfile ?? fileActive ?? DEFAULT_PROFILE_NAME;
148
+ const credentials = profiles[candidate] ?? null;
149
+ const source = envProfile
150
+ ? "env"
151
+ : fileActive
152
+ ? "file"
153
+ : "default";
154
+ return { name: candidate, source, credentials, exists: Boolean(credentials) };
155
+ }
156
+ export async function findProfileNameByOrganizationId(organizationId, env = process.env) {
157
+ const document = await readSystemCredentialDocument(env);
158
+ if (!document)
159
+ return null;
160
+ const profiles = readProfiles(document);
161
+ for (const [name, credentials] of Object.entries(profiles)) {
162
+ if (credentials.identity?.organization?.id === organizationId)
163
+ return name;
164
+ if (credentials.activeOrganization?.id === organizationId)
165
+ return name;
166
+ }
167
+ return null;
168
+ }
169
+ export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
170
+ const document = await readSystemCredentialDocument(env);
171
+ const profiles = document ? readProfiles(document) : {};
172
+ const existingForOrg = Object.entries(profiles).find(([, credentials]) => credentials.identity?.organization?.id === organizationId ||
173
+ credentials.activeOrganization?.id === organizationId);
174
+ if (existingForOrg)
175
+ return { name: existingForOrg[0], renamed: false };
176
+ const base = normalizeProfileName(candidateSeed);
177
+ if (!profiles[base])
178
+ return { name: base, renamed: false };
179
+ for (let suffix = 2; suffix < 1000; suffix++) {
180
+ const next = `${base}-${suffix}`;
181
+ if (!profiles[next])
182
+ return { name: next, renamed: true };
183
+ }
184
+ throw new OxygenError("profile_collision", "Unable to derive a unique CLI profile name.", {
185
+ details: { base },
186
+ exitCode: 1,
187
+ });
188
+ }
189
+ export async function pickProfileNameForUserSession(userId, candidateSeed, env = process.env) {
190
+ const document = await readSystemCredentialDocument(env);
191
+ const profiles = document ? readProfiles(document) : {};
192
+ const existingForUser = Object.entries(profiles).find(([, credentials]) => credentials.authKind === "user_session" &&
193
+ credentials.identity?.user.id === userId);
194
+ if (existingForUser)
195
+ return { name: existingForUser[0], renamed: false };
196
+ const base = normalizeProfileName(candidateSeed);
197
+ if (!profiles[base])
198
+ return { name: base, renamed: false };
199
+ for (let suffix = 2; suffix < 1000; suffix++) {
200
+ const next = `${base}-${suffix}`;
201
+ if (!profiles[next])
202
+ return { name: next, renamed: true };
203
+ }
204
+ throw new OxygenError("profile_collision", "Unable to derive a unique CLI profile name.", {
205
+ details: { base },
206
+ exitCode: 1,
207
+ });
208
+ }
119
209
  export async function switchCredentialProfile(profile, env = process.env) {
120
210
  const document = await readSystemCredentialDocument(env);
121
211
  if (!document) {
@@ -135,6 +225,35 @@ export async function switchCredentialProfile(profile, env = process.env) {
135
225
  await writeCredentialDocument({ profiles, activeProfile: profileName }, env);
136
226
  return { name: profileName, ...credentials };
137
227
  }
228
+ export async function updateActiveOrganizationForProfile(profile, organization, env = process.env) {
229
+ const document = await readSystemCredentialDocument(env);
230
+ if (!document) {
231
+ throw new OxygenError("not_logged_in", "Run `oxygen login` before selecting an organization.", {
232
+ exitCode: 1,
233
+ });
234
+ }
235
+ const profileName = normalizeProfileName(profile);
236
+ const profiles = readProfiles(document);
237
+ const credentials = profiles[profileName];
238
+ if (!credentials) {
239
+ throw new OxygenError("profile_not_found", `Oxygen CLI profile "${profileName}" is not stored.`, {
240
+ details: { profile: profileName },
241
+ exitCode: 1,
242
+ });
243
+ }
244
+ const updated = {
245
+ ...credentials,
246
+ activeOrganization: organization,
247
+ };
248
+ if (credentials.identity)
249
+ updated.identity = { ...credentials.identity, organization };
250
+ profiles[profileName] = updated;
251
+ await writeCredentialDocument({
252
+ profiles,
253
+ activeProfile: readProfileName(document.activeProfile) ?? profileName,
254
+ }, env);
255
+ return { name: profileName, ...updated };
256
+ }
138
257
  export function normalizeApiUrl(value) {
139
258
  const trimmed = value.trim();
140
259
  if (!trimmed)
@@ -231,6 +350,8 @@ async function writeCredentialDocument(input, env) {
231
350
  activeProfile: input.activeProfile,
232
351
  profiles: input.profiles,
233
352
  };
353
+ if (activeCredentials.identity)
354
+ document.identity = activeCredentials.identity;
234
355
  await writeSystemCredentialPayload(serializeCredentialDocument(document), env);
235
356
  }
236
357
  function credentialStore(env) {
@@ -392,7 +513,7 @@ async function deleteWindowsCredentialManager(env) {
392
513
  [void][OxygenCredMan]::CredDelete("${SERVICE_NAME}", 1, 0)
393
514
  `, env).catch(() => undefined);
394
515
  }
395
- async function runPowerShell(script, env) {
516
+ function runPowerShell(script, env) {
396
517
  const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
397
518
  return execFileAsync(windowsPowerShellCommand(env), [
398
519
  "-NoProfile",
@@ -423,7 +544,7 @@ function credentialStoreError(operation, store, error) {
423
544
  return new OxygenError("credential_store_failed", `Unable to ${operation} Oxygen CLI credentials in ${store}.`, { details, exitCode: 1 });
424
545
  }
425
546
  function sanitizeCredentialErrorText(value) {
426
- return value.replace(/\boxy_live_[A-Za-z0-9_-]+\b/g, "[redacted_token]");
547
+ return value.replace(/\boxy_(?:live|user)_[A-Za-z0-9_-]+\b/g, "[redacted_token]");
427
548
  }
428
549
  function macOSSecurityCommand(env) {
429
550
  return env.OXYGEN_SECURITY_COMMAND?.trim() || "security";
@@ -516,16 +637,81 @@ function parseStoredCredentialsObject(value) {
516
637
  return null;
517
638
  if (typeof apiUrl !== "string" || !apiUrl.trim())
518
639
  return null;
519
- return { token: token.trim(), apiUrl: normalizeApiUrl(apiUrl) };
640
+ const credentials = {
641
+ token: token.trim(),
642
+ apiUrl: normalizeApiUrl(apiUrl),
643
+ };
644
+ const authKind = value.authKind;
645
+ if (authKind === "org_api_key" || authKind === "user_session") {
646
+ credentials.authKind = authKind;
647
+ }
648
+ else {
649
+ const inferredAuthKind = inferAuthKind(credentials.token);
650
+ if (inferredAuthKind)
651
+ credentials.authKind = inferredAuthKind;
652
+ }
653
+ const activeOrganization = parseStoredOrganization(value.activeOrganization);
654
+ if (activeOrganization)
655
+ credentials.activeOrganization = activeOrganization;
656
+ const identity = parseStoredProfileIdentity(value.identity);
657
+ if (identity)
658
+ credentials.identity = identity;
659
+ return credentials;
660
+ }
661
+ function parseStoredProfileIdentity(value) {
662
+ if (!value || typeof value !== "object" || Array.isArray(value))
663
+ return null;
664
+ const record = value;
665
+ const org = record.organization;
666
+ const user = record.user;
667
+ if (!user || typeof user !== "object" || Array.isArray(user))
668
+ return null;
669
+ const userRecord = user;
670
+ const organization = parseStoredOrganization(org);
671
+ const userId = typeof userRecord.id === "string" ? userRecord.id.trim() : "";
672
+ if (!userId)
673
+ return null;
674
+ const userEmail = typeof userRecord.email === "string" && userRecord.email.trim()
675
+ ? userRecord.email.trim()
676
+ : null;
677
+ const capturedAt = typeof record.capturedAt === "string" && record.capturedAt.trim()
678
+ ? record.capturedAt.trim()
679
+ : new Date(0).toISOString();
680
+ return {
681
+ organization,
682
+ user: { id: userId, email: userEmail },
683
+ capturedAt,
684
+ };
685
+ }
686
+ function parseStoredOrganization(value) {
687
+ if (!value || typeof value !== "object" || Array.isArray(value))
688
+ return null;
689
+ const record = value;
690
+ const id = typeof record.id === "string" ? record.id.trim() : "";
691
+ const name = typeof record.name === "string" ? record.name : "";
692
+ if (!id || !name)
693
+ return null;
694
+ const slug = typeof record.slug === "string" && record.slug.trim()
695
+ ? record.slug.trim()
696
+ : null;
697
+ return { id, slug, name };
520
698
  }
521
699
  function readProfiles(document) {
522
700
  const profiles = {};
523
701
  const rawProfiles = document.profiles ?? {};
524
702
  for (const [name, credentials] of Object.entries(rawProfiles)) {
525
- profiles[normalizeProfileName(name)] = {
703
+ const normalized = {
526
704
  token: credentials.token,
527
705
  apiUrl: normalizeApiUrl(credentials.apiUrl),
528
706
  };
707
+ if (credentials.authKind)
708
+ normalized.authKind = credentials.authKind;
709
+ if (credentials.activeOrganization !== undefined) {
710
+ normalized.activeOrganization = credentials.activeOrganization;
711
+ }
712
+ if (credentials.identity)
713
+ normalized.identity = credentials.identity;
714
+ profiles[normalizeProfileName(name)] = normalized;
529
715
  }
530
716
  if (Object.keys(profiles).length === 0) {
531
717
  const defaultCredentials = parseStoredCredentialsObject(document);
@@ -538,3 +724,10 @@ function readEnvToken(env) {
538
724
  const token = env.OXYGEN_API_KEY ?? env.OXYGEN_TOKEN;
539
725
  return token?.trim() || null;
540
726
  }
727
+ function inferAuthKind(token) {
728
+ if (token.startsWith("oxy_user_"))
729
+ return "user_session";
730
+ if (token.startsWith("oxy_live_"))
731
+ return "org_api_key";
732
+ return undefined;
733
+ }
@@ -2,10 +2,14 @@ import { type StoredCredentials } from "./credentials.js";
2
2
  type RequestOptions = {
3
3
  method?: "GET" | "POST" | "DELETE";
4
4
  body?: Record<string, unknown>;
5
+ formData?: FormData;
5
6
  credentials?: StoredCredentials;
6
7
  requireAuth?: boolean;
7
8
  timeoutMs?: number;
9
+ traceId?: string;
8
10
  fetch?: typeof fetch;
11
+ selectedOrganization?: string;
9
12
  };
10
- export declare function requestOxygen<T>(path: string, options?: RequestOptions): Promise<T>;
13
+ export declare function requestOxygen<T>(// skipcq: JS-R1005
14
+ path: string, options?: RequestOptions): Promise<T>;
11
15
  export {};
@@ -2,7 +2,8 @@ import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { defaultApiUrl, loadCredentials } from "./credentials.js";
4
4
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
5
- export async function requestOxygen(path, options = {}) {
5
+ export async function requestOxygen(// skipcq: JS-R1005
6
+ path, options = {}) {
6
7
  const credentials = options.credentials
7
8
  ?? (options.requireAuth === false ? null : await loadCredentials());
8
9
  if (!credentials && options.requireAuth !== false) {
@@ -11,7 +12,7 @@ export async function requestOxygen(path, options = {}) {
11
12
  });
12
13
  }
13
14
  const apiUrl = credentials?.apiUrl ?? defaultApiUrl();
14
- const traceId = randomUUID();
15
+ const traceId = options.traceId ?? randomUUID();
15
16
  const headers = {
16
17
  Accept: "application/json",
17
18
  "X-Oxygen-Trace-Id": traceId,
@@ -20,6 +21,13 @@ export async function requestOxygen(path, options = {}) {
20
21
  if (credentials?.token) {
21
22
  headers.Authorization = `Bearer ${credentials.token}`;
22
23
  }
24
+ const selectedOrganization = resolveSelectedOrganization(credentials, options.selectedOrganization);
25
+ if (selectedOrganization) {
26
+ headers["X-Oxygen-Organization"] = selectedOrganization;
27
+ }
28
+ if (options.body && options.formData) {
29
+ throw new OxygenError("invalid_request", "Pass body or formData, not both.", { exitCode: 1 });
30
+ }
23
31
  if (options.body) {
24
32
  headers["Content-Type"] = "application/json";
25
33
  }
@@ -31,6 +39,7 @@ export async function requestOxygen(path, options = {}) {
31
39
  method: options.method ?? "GET",
32
40
  headers,
33
41
  ...(options.body ? { body: JSON.stringify(options.body) } : {}),
42
+ ...(options.formData ? { body: options.formData } : {}),
34
43
  };
35
44
  if (timeout.signal)
36
45
  requestInit.signal = timeout.signal;
@@ -44,6 +53,7 @@ export async function requestOxygen(path, options = {}) {
44
53
  api_url: apiUrl,
45
54
  path,
46
55
  timeout_ms: timeoutMs,
56
+ trace_id: traceId,
47
57
  },
48
58
  exitCode: 1,
49
59
  });
@@ -72,6 +82,17 @@ export async function requestOxygen(path, options = {}) {
72
82
  }
73
83
  return envelope.data;
74
84
  }
85
+ function resolveSelectedOrganization(credentials, explicit) {
86
+ const fromOption = explicit?.trim();
87
+ if (fromOption)
88
+ return fromOption;
89
+ const fromEnv = process.env.OXYGEN_ORG?.trim();
90
+ if (fromEnv)
91
+ return fromEnv;
92
+ return credentials?.activeOrganization?.slug
93
+ ?? credentials?.activeOrganization?.id
94
+ ?? null;
95
+ }
75
96
  const staleCliWarningVersions = new Set();
76
97
  function warnIfCliIsOlderThanApi(serverVersion) {
77
98
  if (!serverVersion || staleCliWarningVersions.has(serverVersion))