@oxygen-agent/cli 1.64.5 → 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.64.5
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,18 +1,21 @@
1
1
  export type StoredProfileIdentity = {
2
- organization: {
3
- id: string;
4
- slug: string | null;
5
- name: string;
6
- };
2
+ organization: StoredOrganization | null;
7
3
  user: {
8
4
  id: string;
9
5
  email: string | null;
10
6
  };
11
7
  capturedAt: string;
12
8
  };
9
+ export type StoredOrganization = {
10
+ id: string;
11
+ slug: string | null;
12
+ name: string;
13
+ };
13
14
  export type StoredCredentials = {
14
15
  token: string;
15
16
  apiUrl: string;
17
+ authKind?: "org_api_key" | "user_session";
18
+ activeOrganization?: StoredOrganization | null;
16
19
  identity?: StoredProfileIdentity;
17
20
  };
18
21
  export type CredentialProfile = StoredCredentials & {
@@ -48,5 +51,10 @@ export declare function pickProfileNameForIdentity(organizationId: string, candi
48
51
  name: string;
49
52
  renamed: boolean;
50
53
  }>;
54
+ export declare function pickProfileNameForUserSession(userId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
55
+ name: string;
56
+ renamed: boolean;
57
+ }>;
51
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>;
52
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,6 +47,11 @@ 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
+ }
46
55
  if (credentials.identity)
47
56
  normalized.identity = credentials.identity;
48
57
  const existing = await readSystemCredentialDocument(env);
@@ -96,12 +105,18 @@ export async function clearCredentials(env = process.env, options = {}) {
96
105
  export async function listCredentialProfiles(env = process.env) {
97
106
  const envToken = readEnvToken(env);
98
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;
99
115
  return {
100
116
  activeProfile: readEnvProfile(env) ?? "env",
101
117
  profiles: [{
102
118
  name: readEnvProfile(env) ?? "env",
103
- token: envToken,
104
- apiUrl: defaultApiUrl(env),
119
+ ...credentials,
105
120
  }],
106
121
  };
107
122
  }
@@ -144,7 +159,9 @@ export async function findProfileNameByOrganizationId(organizationId, env = proc
144
159
  return null;
145
160
  const profiles = readProfiles(document);
146
161
  for (const [name, credentials] of Object.entries(profiles)) {
147
- if (credentials.identity?.organization.id === organizationId)
162
+ if (credentials.identity?.organization?.id === organizationId)
163
+ return name;
164
+ if (credentials.activeOrganization?.id === organizationId)
148
165
  return name;
149
166
  }
150
167
  return null;
@@ -152,7 +169,8 @@ export async function findProfileNameByOrganizationId(organizationId, env = proc
152
169
  export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
153
170
  const document = await readSystemCredentialDocument(env);
154
171
  const profiles = document ? readProfiles(document) : {};
155
- const existingForOrg = Object.entries(profiles).find(([, credentials]) => credentials.identity?.organization.id === organizationId);
172
+ const existingForOrg = Object.entries(profiles).find(([, credentials]) => credentials.identity?.organization?.id === organizationId ||
173
+ credentials.activeOrganization?.id === organizationId);
156
174
  if (existingForOrg)
157
175
  return { name: existingForOrg[0], renamed: false };
158
176
  const base = normalizeProfileName(candidateSeed);
@@ -168,6 +186,26 @@ export async function pickProfileNameForIdentity(organizationId, candidateSeed,
168
186
  exitCode: 1,
169
187
  });
170
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
+ }
171
209
  export async function switchCredentialProfile(profile, env = process.env) {
172
210
  const document = await readSystemCredentialDocument(env);
173
211
  if (!document) {
@@ -187,6 +225,35 @@ export async function switchCredentialProfile(profile, env = process.env) {
187
225
  await writeCredentialDocument({ profiles, activeProfile: profileName }, env);
188
226
  return { name: profileName, ...credentials };
189
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
+ }
190
257
  export function normalizeApiUrl(value) {
191
258
  const trimmed = value.trim();
192
259
  if (!trimmed)
@@ -477,7 +544,7 @@ function credentialStoreError(operation, store, error) {
477
544
  return new OxygenError("credential_store_failed", `Unable to ${operation} Oxygen CLI credentials in ${store}.`, { details, exitCode: 1 });
478
545
  }
479
546
  function sanitizeCredentialErrorText(value) {
480
- 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]");
481
548
  }
482
549
  function macOSSecurityCommand(env) {
483
550
  return env.OXYGEN_SECURITY_COMMAND?.trim() || "security";
@@ -574,6 +641,18 @@ function parseStoredCredentialsObject(value) {
574
641
  token: token.trim(),
575
642
  apiUrl: normalizeApiUrl(apiUrl),
576
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;
577
656
  const identity = parseStoredProfileIdentity(value.identity);
578
657
  if (identity)
579
658
  credentials.identity = identity;
@@ -585,19 +664,10 @@ function parseStoredProfileIdentity(value) {
585
664
  const record = value;
586
665
  const org = record.organization;
587
666
  const user = record.user;
588
- if (!org || typeof org !== "object" || Array.isArray(org))
589
- return null;
590
667
  if (!user || typeof user !== "object" || Array.isArray(user))
591
668
  return null;
592
- const orgRecord = org;
593
669
  const userRecord = user;
594
- const orgId = typeof orgRecord.id === "string" ? orgRecord.id.trim() : "";
595
- const orgName = typeof orgRecord.name === "string" ? orgRecord.name : "";
596
- if (!orgId || !orgName)
597
- return null;
598
- const orgSlug = typeof orgRecord.slug === "string" && orgRecord.slug.trim()
599
- ? orgRecord.slug.trim()
600
- : null;
670
+ const organization = parseStoredOrganization(org);
601
671
  const userId = typeof userRecord.id === "string" ? userRecord.id.trim() : "";
602
672
  if (!userId)
603
673
  return null;
@@ -608,11 +678,24 @@ function parseStoredProfileIdentity(value) {
608
678
  ? record.capturedAt.trim()
609
679
  : new Date(0).toISOString();
610
680
  return {
611
- organization: { id: orgId, slug: orgSlug, name: orgName },
681
+ organization,
612
682
  user: { id: userId, email: userEmail },
613
683
  capturedAt,
614
684
  };
615
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 };
698
+ }
616
699
  function readProfiles(document) {
617
700
  const profiles = {};
618
701
  const rawProfiles = document.profiles ?? {};
@@ -621,6 +704,11 @@ function readProfiles(document) {
621
704
  token: credentials.token,
622
705
  apiUrl: normalizeApiUrl(credentials.apiUrl),
623
706
  };
707
+ if (credentials.authKind)
708
+ normalized.authKind = credentials.authKind;
709
+ if (credentials.activeOrganization !== undefined) {
710
+ normalized.activeOrganization = credentials.activeOrganization;
711
+ }
624
712
  if (credentials.identity)
625
713
  normalized.identity = credentials.identity;
626
714
  profiles[normalizeProfileName(name)] = normalized;
@@ -636,3 +724,10 @@ function readEnvToken(env) {
636
724
  const token = env.OXYGEN_API_KEY ?? env.OXYGEN_TOKEN;
637
725
  return token?.trim() || null;
638
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))