@oml/cli 0.14.1 → 0.14.3

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/src/auth/auth.ts CHANGED
@@ -5,11 +5,13 @@ import chalk from 'chalk';
5
5
  import * as fs from 'node:fs/promises';
6
6
  import * as os from 'node:os';
7
7
  import * as path from 'node:path';
8
+ import keytar from 'keytar';
8
9
  import {
9
10
  DEFAULT_API_BASE_URL,
10
11
  DEFAULT_SUPABASE_ANON_KEY,
11
12
  DEFAULT_SUPABASE_URL
12
13
  } from './constants.js';
14
+
13
15
  const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code';
14
16
  const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
15
17
  const GITHUB_USER_URL = 'https://api.github.com/user';
@@ -17,20 +19,32 @@ const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liQkHYczdOAvHp5P';
17
19
  const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
18
20
  const SUPABASE_URL_ENV = 'OML_SUPABASE_URL';
19
21
  const SUPABASE_ANON_KEY_ENV = 'OML_SUPABASE_ANON_KEY';
22
+ const API_KEY_ENV = 'OML_PLATFORM_API_KEY';
23
+ const KEYCHAIN_SERVICE = 'oml-code';
24
+ const ACCESS_TOKEN_KEY = 'oml.cli.access_token';
25
+ const REFRESH_TOKEN_KEY = 'oml.cli.refresh_token';
26
+ const EXPIRES_AT_KEY = 'oml.cli.expires_at';
27
+ const PROFILE_PATH = path.join(os.homedir(), '.oml', 'cli-profile.json');
28
+ const LOCK_PATH = path.join(os.homedir(), '.oml', 'credentials.lock');
29
+ const SESSION_EXPIRATION_LEEWAY_MS = 20_000;
30
+ const LOCK_TIMEOUT_MS = 5_000;
31
+ const LOCK_POLL_INTERVAL_MS = 200;
20
32
 
21
33
  type Provider = 'github';
22
34
 
23
- type StoredSession = {
35
+ type StoredProfile = {
24
36
  provider: Provider;
25
37
  userId: string;
26
38
  userLabel?: string;
27
39
  email: string | null;
28
40
  tier?: string;
41
+ signedInAt: string;
42
+ };
43
+
44
+ type StoredCredential = {
29
45
  accessToken: string;
30
46
  refreshToken: string;
31
- tokenType: string;
32
- expiresIn: number;
33
- signedInAt: string;
47
+ expiresAtMs: number;
34
48
  };
35
49
 
36
50
  type LoginOptions = {
@@ -43,134 +57,219 @@ export type OmlCliServerAuthSnapshot = {
43
57
  };
44
58
 
45
59
  export class OmlCliAuthService {
60
+ async login(_options: LoginOptions): Promise<void> {
61
+ const apiKey = process.env[API_KEY_ENV]?.trim();
62
+ if (apiKey) {
63
+ console.error(chalk.yellow(
64
+ 'OML_PLATFORM_API_KEY is set and will take precedence over stored OAuth credentials. ' +
65
+ 'Unset it to use interactive CLI login.'
66
+ ));
67
+ console.error('Run `oml whoami` to inspect the currently effective authentication mode.');
68
+ return;
69
+ }
46
70
 
47
- async login(options: LoginOptions): Promise<void> {
48
- const session = await this.authenticate();
49
- await writeSession(session, true);
71
+ const existing = await this.tryGetValidSnapshot();
72
+ if (existing) {
73
+ const profile = await readProfile();
74
+ console.error(chalk.green(
75
+ `Already signed in as ${profile?.userLabel ?? profile?.email ?? profile?.userId ?? 'current user'}.`
76
+ ));
77
+ return;
78
+ }
79
+
80
+ const session = await authenticateWithGitHub();
81
+ await writeCredential({
82
+ accessToken: session.accessToken,
83
+ refreshToken: session.refreshToken,
84
+ expiresAtMs: session.expiresAtMs,
85
+ });
86
+ await writeProfile({
87
+ provider: session.provider,
88
+ userId: session.userId,
89
+ userLabel: session.userLabel,
90
+ email: session.email,
91
+ tier: session.tier,
92
+ signedInAt: new Date().toISOString(),
93
+ });
50
94
  const summary = session.userLabel ?? session.email ?? 'signed-in user';
51
95
  console.error(chalk.green(`Signed in as ${summary} via ${session.provider}.`));
52
96
  }
53
97
 
54
98
  async logout(): Promise<void> {
55
- await deleteSession();
56
- console.error(chalk.green('Signed out.'));
99
+ const activeServers = await listActiveServers();
100
+ await deleteCredential();
101
+ await deleteProfile();
102
+ if (activeServers.length > 0) {
103
+ console.error(chalk.yellow('Stored OAuth credentials were cleared. Running servers were not stopped:'));
104
+ for (const server of activeServers) {
105
+ console.error(`- ${server.workspaceRoot ?? '(unknown workspace)'} on port ${server.port} (pid ${server.pid})`);
106
+ }
107
+ if (process.env[API_KEY_ENV]?.trim()) {
108
+ console.error(chalk.yellow('OML_PLATFORM_API_KEY is still set, so future starts may use API-key auth.'));
109
+ }
110
+ return;
111
+ }
112
+ console.error(chalk.green('Stored OAuth credentials were cleared.'));
113
+ if (process.env[API_KEY_ENV]?.trim()) {
114
+ console.error(chalk.yellow('OML_PLATFORM_API_KEY is still set, so future starts may use API-key auth.'));
115
+ }
57
116
  }
58
117
 
59
118
  async whoami(): Promise<void> {
60
- const session = await readSession();
61
- if (!session) {
119
+ const apiKey = process.env[API_KEY_ENV]?.trim();
120
+ if (apiKey) {
121
+ console.error('Auth mode: api_key');
122
+ console.error('Account: resolved by OML Platform from OML_PLATFORM_API_KEY');
123
+ const profile = await readProfile();
124
+ if (profile) {
125
+ console.error(`Stored OAuth user: ${profile.userLabel ?? profile.email ?? profile.userId}`);
126
+ }
127
+ return;
128
+ }
129
+
130
+ const credential = await readCredential();
131
+ const profile = await readProfile();
132
+ if (!credential || !profile) {
62
133
  console.error(chalk.yellow('Not signed in.'));
63
134
  return;
64
135
  }
65
- console.error(`Provider: ${session.provider}`);
66
- console.error(`User ID: ${session.userId}`);
67
- console.error(`User label: ${session.userLabel ?? '(not set)'}`);
68
- console.error(`Email: ${session.email ?? '(not set)'}`);
69
- console.error(`Tier: ${session.tier ?? '(not set)'}`);
70
- console.error(`Signed in at: ${session.signedInAt}`);
136
+ console.error('Auth mode: oauth');
137
+ console.error(`Provider: ${profile.provider}`);
138
+ console.error(`User ID: ${profile.userId}`);
139
+ console.error(`User label: ${profile.userLabel ?? '(not set)'}`);
140
+ console.error(`Email: ${profile.email ?? '(not set)'}`);
141
+ console.error(`Tier: ${profile.tier ?? '(not set)'}`);
142
+ console.error(`Signed in at: ${profile.signedInAt}`);
143
+ console.error(`Access token expires at: ${new Date(credential.expiresAtMs).toISOString()}`);
71
144
  }
72
145
 
73
146
  async ensureAuthenticated(operationName: string): Promise<void> {
74
- const session = await readSession();
147
+ if (process.env[API_KEY_ENV]?.trim()) {
148
+ return;
149
+ }
150
+ const session = await readCredential();
75
151
  if (!session) {
76
152
  throw new Error(`${operationName} requires authentication. Run 'oml login' first.`);
77
153
  }
78
154
  }
79
155
 
80
156
  async getAccessToken(): Promise<string> {
81
- const session = await readSession();
82
- if (!session?.accessToken) {
83
- throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
84
- }
157
+ const session = await this.getServerAuthSnapshot();
85
158
  return session.accessToken;
86
159
  }
87
160
 
88
161
  async refreshAccessToken(): Promise<string> {
89
- const session = await readSession();
90
- if (!session?.refreshToken) {
91
- throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
92
- }
93
-
94
- let refreshed;
95
- try {
96
- refreshed = await refreshSupabaseAccessToken(
97
- resolveSupabaseUrl(),
98
- resolveSupabaseAnonKey(),
99
- session.refreshToken
100
- );
101
- } catch {
102
- throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
103
- }
104
-
105
- const updatedSession: StoredSession = {
106
- ...session,
107
- accessToken: refreshed.access_token,
108
- refreshToken: refreshed.refresh_token,
109
- tokenType: refreshed.token_type,
110
- expiresIn: refreshed.expires_in,
111
- email: refreshed.email ?? session.email,
112
- signedInAt: new Date().toISOString(),
113
- };
114
-
115
- await writeSession(updatedSession);
116
- return updatedSession.accessToken;
162
+ const refreshed = await this.refreshCredential();
163
+ return refreshed.accessToken;
117
164
  }
118
165
 
119
166
  async getServerAuthSnapshot(): Promise<OmlCliServerAuthSnapshot> {
120
- const session = await readSession();
167
+ const session = await readCredential();
121
168
  if (!session?.accessToken) {
122
169
  throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
123
170
  }
124
- const signedInAtMs = Date.parse(session.signedInAt);
125
- const expiresAtMs = Number.isFinite(signedInAtMs)
126
- ? signedInAtMs + (session.expiresIn * 1000)
127
- : undefined;
128
- if (expiresAtMs !== undefined && Date.now() + 20_000 >= expiresAtMs) {
129
- await this.refreshAccessToken();
130
- return await this.getServerAuthSnapshot();
171
+ if (Date.now() + SESSION_EXPIRATION_LEEWAY_MS >= session.expiresAtMs) {
172
+ const refreshed = await this.refreshCredential();
173
+ return {
174
+ accessToken: refreshed.accessToken,
175
+ refreshToken: refreshed.refreshToken,
176
+ expiresAtMs: refreshed.expiresAtMs,
177
+ };
131
178
  }
132
179
  return {
133
180
  accessToken: session.accessToken,
134
181
  refreshToken: session.refreshToken,
135
- expiresAtMs,
182
+ expiresAtMs: session.expiresAtMs,
136
183
  };
137
184
  }
138
185
 
139
- async storeRefreshedTokens(accessToken: string, refreshToken: string, expiresAtMs: number): Promise<void> {
140
- const session = await readSession();
141
- if (!session) {
142
- return;
186
+ private async tryGetValidSnapshot(): Promise<OmlCliServerAuthSnapshot | null> {
187
+ try {
188
+ return await this.getServerAuthSnapshot();
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ private async refreshCredential(): Promise<StoredCredential> {
195
+ const session = await readCredential();
196
+ if (!session?.refreshToken) {
197
+ throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
198
+ }
199
+
200
+ const lock = await acquireCredentialLock();
201
+ try {
202
+ const latest = await readCredential();
203
+ if (latest && Date.now() + SESSION_EXPIRATION_LEEWAY_MS < latest.expiresAtMs) {
204
+ return latest;
205
+ }
206
+ const source = latest ?? session;
207
+ const refreshed = await refreshSupabaseAccessToken(
208
+ resolveSupabaseUrl(),
209
+ resolveSupabaseAnonKey(),
210
+ source.refreshToken
211
+ );
212
+ const updatedSession: StoredCredential = {
213
+ accessToken: refreshed.access_token,
214
+ refreshToken: refreshed.refresh_token,
215
+ expiresAtMs: Date.now() + (refreshed.expires_in * 1000),
216
+ };
217
+ await writeCredential(updatedSession);
218
+ const profile = await readProfile();
219
+ if (profile) {
220
+ await writeProfile({
221
+ ...profile,
222
+ email: refreshed.email ?? profile.email,
223
+ });
224
+ }
225
+ return updatedSession;
226
+ } catch (error) {
227
+ if (isUnauthorizedError(error)) {
228
+ await deleteCredential();
229
+ await deleteProfile();
230
+ throw new Error('Authentication refresh failed because the stored credential was revoked. Run \'oml login\' again.');
231
+ }
232
+ throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
233
+ } finally {
234
+ await lock.release();
143
235
  }
144
- const expiresIn = Math.max(0, Math.round((expiresAtMs - Date.now()) / 1000));
145
- const updatedSession: StoredSession = {
146
- ...session,
147
- accessToken,
148
- refreshToken,
149
- expiresIn,
150
- signedInAt: new Date().toISOString(),
151
- };
152
- await writeSession(updatedSession);
153
236
  }
237
+ }
154
238
 
155
- private async authenticate(): Promise<StoredSession> {
156
- return authenticateWithGitHub();
239
+ async function readCredential(): Promise<StoredCredential | undefined> {
240
+ const [accessToken, refreshToken, expiresAtRaw] = await Promise.all([
241
+ keytar.getPassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY),
242
+ keytar.getPassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY),
243
+ keytar.getPassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY),
244
+ ]);
245
+ const expiresAtMs = Number(expiresAtRaw ?? NaN);
246
+ if (!accessToken || !refreshToken || !Number.isFinite(expiresAtMs)) {
247
+ return undefined;
157
248
  }
249
+ return { accessToken, refreshToken, expiresAtMs };
250
+ }
251
+
252
+ async function writeCredential(credential: StoredCredential): Promise<void> {
253
+ await Promise.all([
254
+ keytar.setPassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY, credential.accessToken),
255
+ keytar.setPassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY, credential.refreshToken),
256
+ keytar.setPassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY, String(credential.expiresAtMs)),
257
+ ]);
258
+ }
259
+
260
+ async function deleteCredential(): Promise<void> {
261
+ await Promise.all([
262
+ keytar.deletePassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY),
263
+ keytar.deletePassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY),
264
+ keytar.deletePassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY),
265
+ ]);
158
266
  }
159
267
 
160
- async function readSession(): Promise<StoredSession | undefined> {
268
+ async function readProfile(): Promise<StoredProfile | undefined> {
161
269
  try {
162
- const content = await fs.readFile(getSessionPath(), 'utf-8');
163
- const data = JSON.parse(content) as Partial<StoredSession>;
164
- if (!data.userId || !data.provider || !data.signedInAt) {
165
- return undefined;
166
- }
167
- if (!data.accessToken) {
168
- return undefined;
169
- }
170
- if (!data.refreshToken || !data.tokenType || typeof data.expiresIn !== 'number') {
171
- return undefined;
172
- }
173
- if (data.provider !== 'github') {
270
+ const content = await fs.readFile(PROFILE_PATH, 'utf-8');
271
+ const data = JSON.parse(content) as Partial<StoredProfile>;
272
+ if (!data.provider || !data.userId || !data.signedInAt) {
174
273
  return undefined;
175
274
  }
176
275
  return {
@@ -179,34 +278,21 @@ async function readSession(): Promise<StoredSession | undefined> {
179
278
  userLabel: data.userLabel,
180
279
  email: data.email ?? null,
181
280
  tier: data.tier,
182
- accessToken: data.accessToken,
183
- refreshToken: data.refreshToken,
184
- tokenType: data.tokenType,
185
- expiresIn: data.expiresIn,
186
- signedInAt: data.signedInAt
281
+ signedInAt: data.signedInAt,
187
282
  };
188
283
  } catch {
189
284
  return undefined;
190
285
  }
191
286
  }
192
287
 
193
- async function writeSession(session: StoredSession, warnAboutPlaintext = false): Promise<void> {
194
- const sessionPath = getSessionPath();
195
- await fs.mkdir(path.dirname(sessionPath), { recursive: true });
196
- await fs.writeFile(sessionPath, `${JSON.stringify(session, null, 2)}\n`, 'utf-8');
197
- if (warnAboutPlaintext) {
198
- process.stderr.write(
199
- chalk.yellow(
200
- `[oml] Warning: credentials stored in plaintext at ${sessionPath}. ` +
201
- `A system keychain is not available in this environment.\n`
202
- )
203
- );
204
- }
288
+ async function writeProfile(profile: StoredProfile): Promise<void> {
289
+ await fs.mkdir(path.dirname(PROFILE_PATH), { recursive: true });
290
+ await fs.writeFile(PROFILE_PATH, `${JSON.stringify(profile, null, 2)}\n`, 'utf-8');
205
291
  }
206
292
 
207
- async function deleteSession(): Promise<void> {
293
+ async function deleteProfile(): Promise<void> {
208
294
  try {
209
- await fs.unlink(getSessionPath());
295
+ await fs.unlink(PROFILE_PATH);
210
296
  } catch (error) {
211
297
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
212
298
  throw error;
@@ -214,11 +300,16 @@ async function deleteSession(): Promise<void> {
214
300
  }
215
301
  }
216
302
 
217
- function getSessionPath(): string {
218
- return path.join(os.homedir(), '.oml', 'auth.json');
219
- }
220
-
221
- async function authenticateWithGitHub(): Promise<StoredSession> {
303
+ async function authenticateWithGitHub(): Promise<{
304
+ provider: Provider;
305
+ userId: string;
306
+ userLabel?: string;
307
+ email: string | null;
308
+ tier?: string;
309
+ accessToken: string;
310
+ refreshToken: string;
311
+ expiresAtMs: number;
312
+ }> {
222
313
  const clientId = resolveClientId();
223
314
  const params = new URLSearchParams({
224
315
  client_id: clientId,
@@ -266,9 +357,7 @@ async function authenticateWithGitHub(): Promise<StoredSession> {
266
357
  tier: platformSession.tier,
267
358
  accessToken: platformSession.access_token,
268
359
  refreshToken: platformSession.refresh_token,
269
- tokenType: platformSession.token_type,
270
- expiresIn: platformSession.expires_in,
271
- signedInAt: new Date().toISOString()
360
+ expiresAtMs: Date.now() + platformSession.expires_in * 1000,
272
361
  };
273
362
  }
274
363
 
@@ -323,6 +412,59 @@ async function pollForGitHubAccessToken(clientId: string, device: GitHubDeviceCo
323
412
  }
324
413
  }
325
414
 
415
+ async function acquireCredentialLock(): Promise<{ release: () => Promise<void> }> {
416
+ await fs.mkdir(path.dirname(LOCK_PATH), { recursive: true });
417
+ const startedAt = Date.now();
418
+ while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
419
+ try {
420
+ const handle = await fs.open(LOCK_PATH, 'wx');
421
+ return {
422
+ release: async () => {
423
+ await handle.close();
424
+ await fs.rm(LOCK_PATH, { force: true });
425
+ },
426
+ };
427
+ } catch (error) {
428
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
429
+ throw error;
430
+ }
431
+ await delay(LOCK_POLL_INTERVAL_MS);
432
+ }
433
+ }
434
+ throw new Error('Timed out waiting for the CLI credential lock.');
435
+ }
436
+
437
+ async function listActiveServers(): Promise<Array<{ pid: number; port: number; workspaceRoot?: string }>> {
438
+ const baseDir = path.join(os.homedir(), '.oml', 'workspaces');
439
+ try {
440
+ const workspaceDirs = await fs.readdir(baseDir);
441
+ const active: Array<{ pid: number; port: number; workspaceRoot?: string }> = [];
442
+ for (const workspaceDir of workspaceDirs) {
443
+ const lockFile = path.join(baseDir, workspaceDir, 'server.lock');
444
+ try {
445
+ const raw = await fs.readFile(lockFile, 'utf-8');
446
+ const parsed = JSON.parse(raw) as { pid?: unknown; port?: unknown; workspaceRoot?: unknown };
447
+ const pid = Number(parsed.pid);
448
+ const port = Number(parsed.port);
449
+ if (!Number.isFinite(pid) || !Number.isFinite(port)) {
450
+ continue;
451
+ }
452
+ process.kill(Math.trunc(pid), 0);
453
+ active.push({
454
+ pid: Math.trunc(pid),
455
+ port: Math.trunc(port),
456
+ workspaceRoot: typeof parsed.workspaceRoot === 'string' ? parsed.workspaceRoot : undefined,
457
+ });
458
+ } catch {
459
+ // ignore malformed or stale lock entries
460
+ }
461
+ }
462
+ return active;
463
+ } catch {
464
+ return [];
465
+ }
466
+ }
467
+
326
468
  function resolveClientId(): string {
327
469
  const configured = process.env.OML_AUTH_GITHUB_CLIENT_ID?.trim() || DEFAULT_GITHUB_CLIENT_ID;
328
470
  if (configured) {
@@ -349,6 +491,11 @@ function delay(ms: number): Promise<void> {
349
491
  return new Promise((resolve) => setTimeout(resolve, ms));
350
492
  }
351
493
 
494
+ function isUnauthorizedError(error: unknown): boolean {
495
+ const message = error instanceof Error ? error.message : String(error);
496
+ return /\b401\b/.test(message);
497
+ }
498
+
352
499
  type GitHubDeviceCodeResponse = {
353
500
  device_code?: string;
354
501
  user_code?: string;
@@ -363,6 +510,5 @@ type GitHubTokenResponse = {
363
510
  };
364
511
 
365
512
  type GitHubUserResponse = {
366
- id?: number;
367
513
  login?: string;
368
514
  };
@@ -15,6 +15,7 @@ import chalk from 'chalk';
15
15
  import { DEFAULT_API_BASE_URL } from './constants.js';
16
16
  import { OmlCliAuthService } from './auth.js';
17
17
  const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
18
+ const API_KEY_ENV = 'OML_PLATFORM_API_KEY';
18
19
 
19
20
  let client: OmlClient | null = null;
20
21
  let shutdownHandle: NodeShutdownHandle | null = null;
@@ -34,22 +35,22 @@ export async function initializePlatform(
34
35
 
35
36
  const resolvedApiBaseUrl = process.env[API_BASE_URL_ENV]?.trim() || apiBaseUrl;
36
37
 
38
+ const apiKey = process.env[API_KEY_ENV]?.trim();
37
39
  const config: OmlClientConfig = {
38
40
  apiBaseUrl: resolvedApiBaseUrl,
39
41
  tool: 'oml-cli',
40
42
  storage: new FileStorageAdapter(),
41
- auth: {
42
- method: 'oauth',
43
- getToken: () => authService.getAccessToken(),
44
- refreshToken: () => authService.refreshAccessToken(),
45
- },
46
- onConcurrencyLimit: (info) => {
47
- console.error(chalk.yellow(
48
- `OML Platform: concurrent session limit reached `
49
- + `(${info.active_sessions}/${info.max_sessions}). `
50
- + `Close another instance or upgrade your plan.`
51
- ));
52
- },
43
+ auth: apiKey
44
+ ? {
45
+ method: 'oauth',
46
+ getToken: async () => apiKey,
47
+ refreshToken: async () => apiKey,
48
+ }
49
+ : {
50
+ method: 'oauth',
51
+ getToken: () => authService.getAccessToken(),
52
+ refreshToken: () => authService.refreshAccessToken(),
53
+ },
53
54
  onAuthError: (error) => {
54
55
  console.error(chalk.red(
55
56
  `OML Platform: authentication error — ${toGenericPlatformErrorMessage(error)}`
@@ -70,7 +71,7 @@ export async function initializePlatform(
70
71
 
71
72
  /**
72
73
  * Dispose the platform client. Flushes any buffered telemetry
73
- * and ends the session. Call at the end of CLI execution.
74
+ * and flushes buffered telemetry. Call at the end of CLI execution.
74
75
  */
75
76
  export async function disposePlatform(): Promise<void> {
76
77
  if (shutdownHandle) {