@plosson/agentio 0.5.6 → 0.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,8 +55,9 @@
55
55
  "@inquirer/prompts": "^8.2.0",
56
56
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
57
57
  "commander": "^14.0.2",
58
- "google-auth-library": "^9.0.0",
58
+ "google-auth-library": "^10.0.0",
59
59
  "libsodium-wrappers": "^0.8.1",
60
+ "protobufjs": "^8.0.0",
60
61
  "qrcode-terminal": "^0.12.0",
61
62
  "rss-parser": "^3.13.0"
62
63
  }
package/src/auth/oauth.ts CHANGED
@@ -7,6 +7,7 @@ const GMAIL_SCOPES = [
7
7
  'https://www.googleapis.com/auth/gmail.readonly', // search & read emails
8
8
  'https://www.googleapis.com/auth/gmail.send', // send emails
9
9
  'https://www.googleapis.com/auth/gmail.compose', // create/update drafts
10
+ 'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
10
11
  ];
11
12
 
12
13
  const GCHAT_SCOPES = [
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { calendar } from '@googleapis/calendar';
3
3
  import { getValidTokens, createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
4
4
  import { setCredentials } from '../auth/token-store';
5
- import { setProfile } from '../config/config-manager';
5
+ import { setProfile, getProfile } from '../config/config-manager';
6
6
  import { createProfileCommands } from '../utils/profile-commands';
7
7
  import { performOAuthFlow } from '../auth/oauth';
8
8
  import { GCalClient } from '../services/gcal/client';
@@ -373,7 +373,16 @@ export function registerGCalCommands(program: Command): void {
373
373
  throw new CliError('AUTH_FAILED', 'Could not fetch email from Calendar', 'Try again or specify --profile manually');
374
374
  }
375
375
 
376
- const profileName = options.profile || email;
376
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
377
+ let profileName: string;
378
+ if (options.profile) {
379
+ profileName = options.profile;
380
+ } else if (options.readOnly && await getProfile('gcal', email)) {
381
+ // Profile with email already exists, use -readonly suffix
382
+ profileName = `${email}-readonly`;
383
+ } else {
384
+ profileName = email;
385
+ }
377
386
 
378
387
  await setProfile('gcal', profileName, { readOnly: options.readOnly });
379
388
  await setCredentials('gcal', profileName, { ...tokens, email });
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { chat as gchat } from '@googleapis/chat';
3
3
  import { readFile } from 'fs/promises';
4
4
  import { setCredentials } from '../auth/token-store';
5
- import { setProfile } from '../config/config-manager';
5
+ import { setProfile, getProfile } from '../config/config-manager';
6
6
  import { createProfileCommands } from '../utils/profile-commands';
7
7
  import { createClientGetter } from '../utils/client-factory';
8
8
  import { performOAuthFlow } from '../auth/oauth';
@@ -309,7 +309,16 @@ async function setupOAuthProfile(profileNameOverride?: string, readOnly?: boolea
309
309
  );
310
310
  }
311
311
 
312
- const profileName = profileNameOverride || userEmail;
312
+ // Determine profile name: use explicit override, or email, or email-readonly if conflict
313
+ let profileName: string;
314
+ if (profileNameOverride) {
315
+ profileName = profileNameOverride;
316
+ } else if (readOnly && await getProfile('gchat', userEmail)) {
317
+ // Profile with email already exists, use -readonly suffix
318
+ profileName = `${userEmail}-readonly`;
319
+ } else {
320
+ profileName = userEmail;
321
+ }
313
322
 
314
323
  const credentials: GChatOAuthCredentials = {
315
324
  type: 'oauth',
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { writeFile } from 'fs/promises';
3
3
  import { createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
4
4
  import { setCredentials } from '../auth/token-store';
5
- import { setProfile } from '../config/config-manager';
5
+ import { setProfile, getProfile } from '../config/config-manager';
6
6
  import { createProfileCommands } from '../utils/profile-commands';
7
7
  import { createClientGetter } from '../utils/client-factory';
8
8
  import { performOAuthFlow } from '../auth/oauth';
@@ -159,7 +159,16 @@ Query Syntax Examples:
159
159
  );
160
160
  }
161
161
 
162
- const profileName = options.profile || userEmail;
162
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
163
+ let profileName: string;
164
+ if (options.profile) {
165
+ profileName = options.profile;
166
+ } else if (options.readOnly && await getProfile('gdocs', userEmail)) {
167
+ // Profile with email already exists, use -readonly suffix
168
+ profileName = `${userEmail}-readonly`;
169
+ } else {
170
+ profileName = userEmail;
171
+ }
163
172
 
164
173
  const credentials: GDocsCredentials = {
165
174
  accessToken: tokens.access_token,
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
3
3
  import { setCredentials } from '../auth/token-store';
4
- import { setProfile } from '../config/config-manager';
4
+ import { setProfile, getProfile } from '../config/config-manager';
5
5
  import { createProfileCommands } from '../utils/profile-commands';
6
6
  import { createClientGetter } from '../utils/client-factory';
7
7
  import { performOAuthFlow } from '../auth/oauth';
@@ -258,7 +258,16 @@ Examples:
258
258
  );
259
259
  }
260
260
 
261
- const profileName = options.profile || userEmail;
261
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
262
+ let profileName: string;
263
+ if (options.profile) {
264
+ profileName = options.profile;
265
+ } else if (options.readOnly && await getProfile('gdrive', userEmail)) {
266
+ // Profile with email already exists, use -readonly suffix
267
+ profileName = `${userEmail}-readonly`;
268
+ } else {
269
+ profileName = userEmail;
270
+ }
262
271
 
263
272
  const credentials: GDriveCredentials = {
264
273
  accessToken: tokens.access_token,
@@ -3,7 +3,7 @@ import { basename, join } from 'path';
3
3
  import { tmpdir } from 'os';
4
4
  import { getValidTokens, createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
5
5
  import { setCredentials } from '../auth/token-store';
6
- import { setProfile } from '../config/config-manager';
6
+ import { setProfile, getProfile } from '../config/config-manager';
7
7
  import { createProfileCommands } from '../utils/profile-commands';
8
8
  import { performOAuthFlow } from '../auth/oauth';
9
9
  import { GmailClient } from '../services/gmail/client';
@@ -477,7 +477,16 @@ ${emailHeader}
477
477
  throw new CliError('AUTH_FAILED', 'Could not fetch email from Gmail', 'Try again or specify --profile manually');
478
478
  }
479
479
 
480
- const profileName = options.profile || email;
480
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
481
+ let profileName: string;
482
+ if (options.profile) {
483
+ profileName = options.profile;
484
+ } else if (options.readOnly && await getProfile('gmail', email)) {
485
+ // Profile with email already exists, use -readonly suffix
486
+ profileName = `${email}-readonly`;
487
+ } else {
488
+ profileName = email;
489
+ }
481
490
 
482
491
  await setProfile('gmail', profileName, { readOnly: options.readOnly });
483
492
  await setCredentials('gmail', profileName, { ...tokens, email });
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { writeFile } from 'fs/promises';
3
3
  import { createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
4
4
  import { setCredentials } from '../auth/token-store';
5
- import { setProfile } from '../config/config-manager';
5
+ import { setProfile, getProfile } from '../config/config-manager';
6
6
  import { createProfileCommands } from '../utils/profile-commands';
7
7
  import { createClientGetter } from '../utils/client-factory';
8
8
  import { performOAuthFlow } from '../auth/oauth';
@@ -341,7 +341,16 @@ Examples:
341
341
  throw new CliError('AUTH_FAILED', `Failed to fetch user email: ${errorMessage}`, 'Ensure the account has an email address');
342
342
  }
343
343
 
344
- const profileName = options.profile || userEmail;
344
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
345
+ let profileName: string;
346
+ if (options.profile) {
347
+ profileName = options.profile;
348
+ } else if (options.readOnly && await getProfile('gsheets', userEmail)) {
349
+ // Profile with email already exists, use -readonly suffix
350
+ profileName = `${userEmail}-readonly`;
351
+ } else {
352
+ profileName = userEmail;
353
+ }
345
354
 
346
355
  const credentials: GSheetsCredentials = {
347
356
  accessToken: tokens.access_token,
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { getValidTokens, createGoogleAuth, fetchGoogleUserEmail } from '../auth/token-manager';
3
3
  import { setCredentials } from '../auth/token-store';
4
- import { setProfile } from '../config/config-manager';
4
+ import { setProfile, getProfile } from '../config/config-manager';
5
5
  import { createProfileCommands } from '../utils/profile-commands';
6
6
  import { performOAuthFlow } from '../auth/oauth';
7
7
  import { GTasksClient } from '../services/gtasks/client';
@@ -320,7 +320,16 @@ export function registerGTasksCommands(program: Command): void {
320
320
  throw new CliError('AUTH_FAILED', 'Could not fetch email', 'Try again or specify --profile manually');
321
321
  }
322
322
 
323
- const profileName = options.profile || email;
323
+ // Determine profile name: use explicit --profile, or email, or email-readonly if conflict
324
+ let profileName: string;
325
+ if (options.profile) {
326
+ profileName = options.profile;
327
+ } else if (options.readOnly && await getProfile('gtasks', email)) {
328
+ // Profile with email already exists, use -readonly suffix
329
+ profileName = `${email}-readonly`;
330
+ } else {
331
+ profileName = email;
332
+ }
324
333
 
325
334
  await setProfile('gtasks', profileName, { readOnly: options.readOnly });
326
335
  await setCredentials('gtasks', profileName, { ...tokens, email });
@@ -0,0 +1,278 @@
1
+ import { Command } from 'commander';
2
+ import { getProfileStatuses, type ProfileStatus } from './status';
3
+ import { getCredentials, setCredentials } from '../auth/token-store';
4
+ import { performOAuthFlow, type OAuthService } from '../auth/oauth';
5
+ import { performGitHubOAuthFlow } from '../auth/github-oauth';
6
+ import { performJiraOAuthFlow, type AtlassianSite } from '../auth/jira-oauth';
7
+ import { fetchGoogleUserEmail } from '../auth/token-manager';
8
+ import { GitHubClient } from '../services/github/client';
9
+ import { interactiveCheckbox, interactiveSelect } from '../utils/interactive';
10
+ import { handleError } from '../utils/errors';
11
+ import type { ServiceName } from '../types/config';
12
+ import type { GDocsCredentials } from '../types/gdocs';
13
+ import type { GDriveCredentials } from '../types/gdrive';
14
+ import type { GChatCredentials } from '../types/gchat';
15
+ import type { GSheetsCredentials } from '../types/gsheets';
16
+ import type { GitHubCredentials } from '../types/github';
17
+ import type { JiraCredentials } from '../types/jira';
18
+ import type { OAuthTokens } from '../types/tokens';
19
+
20
+ type GmailCredentials = OAuthTokens & { email?: string };
21
+ type GCalCredentials = OAuthTokens & { email?: string };
22
+ type GTasksCredentials = OAuthTokens & { email?: string };
23
+
24
+ // Services that use Google OAuth and store { ...tokens, email }
25
+ const GOOGLE_SIMPLE_SERVICES: ServiceName[] = ['gmail', 'gcal', 'gtasks'];
26
+
27
+ // Services that use Google OAuth with custom credential objects
28
+ const GOOGLE_CUSTOM_SERVICES: ServiceName[] = ['gdocs', 'gsheets'];
29
+
30
+ // Services that require manual credential setup
31
+ const MANUAL_SERVICES: ServiceName[] = ['telegram', 'slack', 'discourse', 'sql'];
32
+
33
+ async function reauthGoogleSimple(
34
+ service: ServiceName,
35
+ profileName: string
36
+ ): Promise<void> {
37
+ const oauthService = service as OAuthService;
38
+ console.error(`\nRe-authenticating ${service} / ${profileName}...`);
39
+
40
+ const tokens = await performOAuthFlow(oauthService);
41
+ const email = await fetchGoogleUserEmail(tokens.access_token);
42
+
43
+ // Preserve existing credential fields, update tokens and email
44
+ const existing = await getCredentials<GmailCredentials | GCalCredentials | GTasksCredentials>(service, profileName);
45
+ await setCredentials(service, profileName, { ...existing, ...tokens, email });
46
+
47
+ console.error(` Done (${email})`);
48
+ }
49
+
50
+ async function reauthGoogleCustom(
51
+ service: ServiceName,
52
+ profileName: string
53
+ ): Promise<void> {
54
+ const oauthService = service as OAuthService;
55
+ console.error(`\nRe-authenticating ${service} / ${profileName}...`);
56
+
57
+ const tokens = await performOAuthFlow(oauthService);
58
+ const email = await fetchGoogleUserEmail(tokens.access_token);
59
+
60
+ const existing = await getCredentials<GDocsCredentials | GSheetsCredentials>(service, profileName);
61
+ const credentials = {
62
+ ...existing,
63
+ accessToken: tokens.access_token,
64
+ refreshToken: tokens.refresh_token,
65
+ expiryDate: tokens.expiry_date,
66
+ tokenType: tokens.token_type,
67
+ scope: tokens.scope,
68
+ email,
69
+ };
70
+
71
+ await setCredentials(service, profileName, credentials);
72
+ console.error(` Done (${email})`);
73
+ }
74
+
75
+ async function reauthGDrive(profileName: string): Promise<void> {
76
+ console.error(`\nRe-authenticating gdrive / ${profileName}...`);
77
+
78
+ // Read existing credentials to preserve accessLevel
79
+ const existing = await getCredentials<GDriveCredentials>('gdrive', profileName);
80
+ const accessLevel = existing?.accessLevel || 'readonly';
81
+ const oauthService: OAuthService = accessLevel === 'full' ? 'gdrive-full' : 'gdrive-readonly';
82
+
83
+ const tokens = await performOAuthFlow(oauthService);
84
+ const email = await fetchGoogleUserEmail(tokens.access_token);
85
+
86
+ const credentials: GDriveCredentials = {
87
+ ...existing,
88
+ accessToken: tokens.access_token,
89
+ refreshToken: tokens.refresh_token,
90
+ expiryDate: tokens.expiry_date,
91
+ tokenType: tokens.token_type,
92
+ scope: tokens.scope,
93
+ email,
94
+ accessLevel,
95
+ };
96
+
97
+ await setCredentials('gdrive', profileName, credentials);
98
+ console.error(` Done (${email}, ${accessLevel})`);
99
+ }
100
+
101
+ async function reauthGChat(profileName: string): Promise<void> {
102
+ const existing = await getCredentials<GChatCredentials>('gchat', profileName);
103
+
104
+ if (existing?.type === 'webhook') {
105
+ console.error(`\nSkipping gchat / ${profileName}: webhook profiles don't expire. Run 'agentio gchat profile add' to update.`);
106
+ return;
107
+ }
108
+
109
+ console.error(`\nRe-authenticating gchat / ${profileName}...`);
110
+
111
+ const tokens = await performOAuthFlow('gchat');
112
+ const email = await fetchGoogleUserEmail(tokens.access_token);
113
+
114
+ const credentials = {
115
+ ...existing,
116
+ type: 'oauth' as const,
117
+ accessToken: tokens.access_token,
118
+ refreshToken: tokens.refresh_token,
119
+ expiryDate: tokens.expiry_date,
120
+ tokenType: tokens.token_type,
121
+ scope: tokens.scope,
122
+ email,
123
+ };
124
+
125
+ await setCredentials('gchat', profileName, credentials);
126
+ console.error(` Done (${email})`);
127
+ }
128
+
129
+ async function reauthGitHub(profileName: string): Promise<void> {
130
+ console.error(`\nRe-authenticating github / ${profileName}...`);
131
+
132
+ const oauthResult = await performGitHubOAuthFlow();
133
+
134
+ // Fetch updated user info
135
+ const tempCreds: GitHubCredentials = {
136
+ accessToken: oauthResult.accessToken,
137
+ username: '',
138
+ email: null,
139
+ };
140
+ const client = new GitHubClient(tempCreds);
141
+ const user = await client.getUser();
142
+
143
+ // Preserve existing fields, update token and user info
144
+ const existing = await getCredentials<GitHubCredentials>('github', profileName);
145
+ const credentials: GitHubCredentials = {
146
+ ...existing,
147
+ accessToken: oauthResult.accessToken,
148
+ username: user.login,
149
+ email: user.email,
150
+ };
151
+
152
+ await setCredentials('github', profileName, credentials);
153
+ console.error(` Done (${user.login})`);
154
+ }
155
+
156
+ async function reauthJira(profileName: string): Promise<void> {
157
+ console.error(`\nRe-authenticating jira / ${profileName}...`);
158
+
159
+ const selectSite = async (sites: AtlassianSite[]): Promise<AtlassianSite> => {
160
+ return interactiveSelect({
161
+ message: 'Select a JIRA site:',
162
+ choices: sites.map((site) => ({
163
+ name: site.name,
164
+ value: site,
165
+ description: site.url,
166
+ })),
167
+ });
168
+ };
169
+
170
+ const result = await performJiraOAuthFlow(selectSite);
171
+
172
+ const existing = await getCredentials<JiraCredentials>('jira', profileName);
173
+ const credentials: JiraCredentials = {
174
+ ...existing,
175
+ accessToken: result.accessToken,
176
+ refreshToken: result.refreshToken,
177
+ expiryDate: result.expiryDate,
178
+ cloudId: result.cloudId,
179
+ siteUrl: result.siteUrl,
180
+ };
181
+
182
+ await setCredentials('jira', profileName, credentials);
183
+ console.error(` Done (${result.siteUrl})`);
184
+ }
185
+
186
+ async function reauthProfile(service: ServiceName, profileName: string): Promise<void> {
187
+ if (GOOGLE_SIMPLE_SERVICES.includes(service)) {
188
+ await reauthGoogleSimple(service, profileName);
189
+ return;
190
+ }
191
+
192
+ if (GOOGLE_CUSTOM_SERVICES.includes(service)) {
193
+ await reauthGoogleCustom(service, profileName);
194
+ return;
195
+ }
196
+
197
+ switch (service) {
198
+ case 'gdrive':
199
+ await reauthGDrive(profileName);
200
+ break;
201
+
202
+ case 'gchat':
203
+ await reauthGChat(profileName);
204
+ break;
205
+
206
+ case 'github':
207
+ await reauthGitHub(profileName);
208
+ break;
209
+
210
+ case 'jira':
211
+ await reauthJira(profileName);
212
+ break;
213
+
214
+ case 'whatsapp':
215
+ console.error(`\nSkipping whatsapp / ${profileName}: use 'agentio whatsapp profile add' to re-pair.`);
216
+ break;
217
+
218
+ default:
219
+ if (MANUAL_SERVICES.includes(service)) {
220
+ console.error(`\nSkipping ${service} / ${profileName}: uses manual credentials. Run 'agentio ${service} profile add' to update.`);
221
+ }
222
+ break;
223
+ }
224
+ }
225
+
226
+ export function registerReauthCommand(program: Command): void {
227
+ program
228
+ .command('reauth')
229
+ .description('Re-authenticate expired or invalid profiles')
230
+ .option('--all', 'Re-authenticate all invalid profiles without prompting')
231
+ .action(async (options) => {
232
+ try {
233
+ console.error('Checking profile credentials...\n');
234
+
235
+ const statuses = await getProfileStatuses();
236
+ const invalid = statuses.filter(
237
+ (s) => s.status === 'invalid' || s.status === 'no-creds'
238
+ );
239
+
240
+ if (invalid.length === 0) {
241
+ console.log('All profiles are valid.');
242
+ return;
243
+ }
244
+
245
+ let selected: ProfileStatus[];
246
+
247
+ if (options.all) {
248
+ selected = invalid;
249
+ } else {
250
+ const choices = invalid.map((s) => ({
251
+ name: `${s.service} / ${s.profile} (${s.error || 'no credentials'})`,
252
+ value: s,
253
+ checked: true,
254
+ }));
255
+
256
+ selected = await interactiveCheckbox({
257
+ message: 'Select profiles to re-authenticate:',
258
+ choices,
259
+ required: true,
260
+ });
261
+ }
262
+
263
+ for (const s of selected) {
264
+ try {
265
+ await reauthProfile(s.service, s.profile);
266
+ } catch (error) {
267
+ console.error(
268
+ `\n Failed to reauth ${s.service} / ${s.profile}: ${error instanceof Error ? error.message : String(error)}`
269
+ );
270
+ }
271
+ }
272
+
273
+ console.log('\nDone.');
274
+ } catch (error) {
275
+ handleError(error);
276
+ }
277
+ });
278
+ }
@@ -14,6 +14,7 @@ import { JiraClient } from '../services/jira/client';
14
14
  import { GChatClient } from '../services/gchat/client';
15
15
  import { SlackClient } from '../services/slack/client';
16
16
  import { DiscourseClient } from '../services/discourse/client';
17
+ import { GSheetsClient } from '../services/gsheets/client';
17
18
  import { SqlClient } from '../services/sql/client';
18
19
  import type { ServiceClient, ValidationResult } from '../types/service';
19
20
  import type { ServiceName } from '../types/config';
@@ -26,6 +27,7 @@ import type { GDriveCredentials } from '../types/gdrive';
26
27
  import type { GCalCredentials } from '../types/gcal';
27
28
  import type { GTasksCredentials } from '../types/gtasks';
28
29
  import type { GChatCredentials } from '../types/gchat';
30
+ import type { GSheetsCredentials } from '../types/gsheets';
29
31
  import type { SlackCredentials } from '../types/slack';
30
32
  import type { DiscourseCredentials } from '../types/discourse';
31
33
  import type { SqlCredentials } from '../types/sql';
@@ -66,6 +68,11 @@ async function createServiceClient(
66
68
  return new GDriveClient(creds);
67
69
  }
68
70
 
71
+ case 'gsheets': {
72
+ const creds = credentials as GSheetsCredentials;
73
+ return new GSheetsClient(creds);
74
+ }
75
+
69
76
  case 'gcal': {
70
77
  const creds = credentials as GCalCredentials;
71
78
  const auth = createGoogleAuth({
@@ -240,8 +247,8 @@ async function createServiceClient(
240
247
  }
241
248
  }
242
249
 
243
- interface ProfileStatus {
244
- service: string;
250
+ export interface ProfileStatus {
251
+ service: ServiceName;
245
252
  profile: string;
246
253
  readOnly?: boolean;
247
254
  status: 'ok' | 'invalid' | 'no-creds' | 'skipped';
@@ -249,6 +256,58 @@ interface ProfileStatus {
249
256
  error?: string;
250
257
  }
251
258
 
259
+ export async function getProfileStatuses(options?: { test?: boolean }): Promise<ProfileStatus[]> {
260
+ const shouldTest = options?.test !== false;
261
+ const allProfiles = await listProfiles();
262
+ const statuses: ProfileStatus[] = [];
263
+
264
+ for (const { service, profiles } of allProfiles) {
265
+ for (const entry of profiles) {
266
+ const credentials = await getCredentials(service, entry.name);
267
+
268
+ if (!credentials) {
269
+ statuses.push({
270
+ service,
271
+ profile: entry.name,
272
+ readOnly: entry.readOnly,
273
+ status: 'no-creds',
274
+ });
275
+ continue;
276
+ }
277
+
278
+ if (!shouldTest) {
279
+ statuses.push({
280
+ service,
281
+ profile: entry.name,
282
+ readOnly: entry.readOnly,
283
+ status: 'skipped',
284
+ });
285
+ continue;
286
+ }
287
+
288
+ const client = await createServiceClient(service, credentials, entry.name);
289
+ let result: ValidationResult;
290
+
291
+ if (client) {
292
+ result = await client.validate();
293
+ } else {
294
+ result = { valid: true, info: 'unknown service' };
295
+ }
296
+
297
+ statuses.push({
298
+ service,
299
+ profile: entry.name,
300
+ readOnly: entry.readOnly,
301
+ status: result.valid ? 'ok' : 'invalid',
302
+ info: result.info,
303
+ error: result.error,
304
+ });
305
+ }
306
+ }
307
+
308
+ return statuses;
309
+ }
310
+
252
311
  export function registerStatusCommand(program: Command): void {
253
312
  program
254
313
  .command('status')
@@ -257,57 +316,11 @@ export function registerStatusCommand(program: Command): void {
257
316
  .option('--json', 'Output in JSON format')
258
317
  .action(async (options) => {
259
318
  try {
260
- const allProfiles = await listProfiles();
261
319
  const version = program.version();
262
320
  const envVars = await listEnv();
263
321
  const envKeys = Object.keys(envVars).sort();
264
322
 
265
- // Collect all profile statuses
266
- const statuses: ProfileStatus[] = [];
267
-
268
- for (const { service, profiles } of allProfiles) {
269
- for (const entry of profiles) {
270
- const credentials = await getCredentials(service, entry.name);
271
-
272
- if (!credentials) {
273
- statuses.push({
274
- service,
275
- profile: entry.name,
276
- readOnly: entry.readOnly,
277
- status: 'no-creds',
278
- });
279
- continue;
280
- }
281
-
282
- if (options.test === false) {
283
- statuses.push({
284
- service,
285
- profile: entry.name,
286
- readOnly: entry.readOnly,
287
- status: 'skipped',
288
- });
289
- continue;
290
- }
291
-
292
- const client = await createServiceClient(service, credentials, entry.name);
293
- let result: ValidationResult;
294
-
295
- if (client) {
296
- result = await client.validate();
297
- } else {
298
- result = { valid: true, info: 'unknown service' };
299
- }
300
-
301
- statuses.push({
302
- service,
303
- profile: entry.name,
304
- readOnly: entry.readOnly,
305
- status: result.valid ? 'ok' : 'invalid',
306
- info: result.info,
307
- error: result.error,
308
- });
309
- }
310
- }
323
+ const statuses = await getProfileStatuses({ test: options.test });
311
324
 
312
325
  // JSON output mode
313
326
  if (options.json) {
@@ -255,9 +255,11 @@ export async function startGateway(): Promise<void> {
255
255
  };
256
256
  config.gateway = gatewayConfig;
257
257
  await saveConfig(config);
258
- console.log(`First run - generated API key: ${generatedKey}`);
259
258
  }
260
259
 
260
+ // Always display API key for easy access (e.g., Docker logs)
261
+ console.log(`API Key: ${gatewayConfig.apiKey}`);
262
+
261
263
  // Initialize database
262
264
  await initDatabase();
263
265
  console.log('Database initialized');
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ import { registerClaudeCommands } from './commands/claude';
24
24
  import { registerConfigCommands } from './commands/config';
25
25
  import { registerDocsCommand } from './commands/docs';
26
26
  import { registerGatewayCommands } from './commands/gateway';
27
+ import { registerReauthCommand } from './commands/reauth';
27
28
  import { registerStatusCommand } from './commands/status';
28
29
  import { registerUpdateCommand } from './commands/update';
29
30
 
@@ -66,6 +67,7 @@ registerClaudeCommands(program);
66
67
  registerConfigCommands(program);
67
68
  registerDocsCommand(program);
68
69
  registerGatewayCommands(program);
70
+ registerReauthCommand(program);
69
71
  registerStatusCommand(program);
70
72
  registerUpdateCommand(program);
71
73
 
@@ -33,7 +33,7 @@ export class GChatClient implements ServiceClient {
33
33
  const auth = this.createOAuthClient(oauthCreds);
34
34
  const chat = gchat({ version: 'v1', auth: auth as any });
35
35
  await chat.spaces.list({ pageSize: 1 });
36
- return { valid: true, info: 'oauth' };
36
+ return { valid: true, info: oauthCreds.email || 'oauth' };
37
37
  } catch (error) {
38
38
  const message = error instanceof Error ? error.message : 'Unknown error';
39
39
  if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {