@plosson/agentio 0.5.15 → 0.7.0

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.15",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/auth/oauth.ts CHANGED
@@ -15,6 +15,7 @@ const GCHAT_SCOPES = [
15
15
  'https://www.googleapis.com/auth/chat.messages.readonly', // read messages (get operations)
16
16
  'https://www.googleapis.com/auth/chat.spaces.readonly', // read space info and list
17
17
  'https://www.googleapis.com/auth/chat.memberships.readonly', // read space members
18
+ 'https://www.googleapis.com/auth/directory.readonly', // resolve user IDs to names/emails via People API
18
19
  'https://www.googleapis.com/auth/userinfo.email', // get user email for profile naming
19
20
  ];
20
21
 
package/src/mcp/server.ts CHANGED
@@ -197,6 +197,9 @@ export async function startMcpServer(
197
197
  process.exit(1);
198
198
  }
199
199
 
200
+ // Track last-checked timestamps per service:space for automatic --since injection
201
+ const lastChecked = new Map<string, Date>();
202
+
200
203
  // Create MCP server
201
204
  const server = new Server(
202
205
  { name: 'agentio', version: '1.0.0' },
@@ -230,6 +233,8 @@ export async function startMcpServer(
230
233
  const service = tool.commandPath[0];
231
234
  const profile = profileMap.get(service);
232
235
 
236
+ const input = (args as Record<string, unknown>) || {};
237
+
233
238
  // Build a fresh program for each call to avoid state leaks
234
239
  const execProgram = buildProgram(services);
235
240
 
@@ -237,11 +242,23 @@ export async function startMcpServer(
237
242
  const output = await executeCommand(
238
243
  execProgram,
239
244
  tool,
240
- (args as Record<string, unknown>) || {},
245
+ input,
241
246
  profile
242
247
  );
248
+
249
+ // For list commands, append last-checked info and update timestamp
250
+ let result = output || '(no output)';
251
+ if (name === 'gchat_list' && input.space) {
252
+ const key = `gchat:${input.space}`;
253
+ const last = lastChecked.get(key);
254
+ if (last) {
255
+ result += `\n\nPreviously checked: ${last.toISOString()}`;
256
+ }
257
+ lastChecked.set(key, new Date());
258
+ }
259
+
243
260
  return {
244
- content: [{ type: 'text' as const, text: output || '(no output)' }],
261
+ content: [{ type: 'text' as const, text: result }],
245
262
  };
246
263
  } catch (error: unknown) {
247
264
  const message =
@@ -15,8 +15,15 @@ import type {
15
15
  GChatSpace,
16
16
  } from '../../types/gchat';
17
17
 
18
+ interface ResolvedUser {
19
+ displayName: string;
20
+ email?: string;
21
+ }
22
+
18
23
  export class GChatClient implements ServiceClient {
19
24
  private credentials: GChatCredentials;
25
+ private userCache = new Map<string, ResolvedUser>();
26
+ private spaceIdCache = new Map<string, string>();
20
27
 
21
28
  constructor(credentials: GChatCredentials) {
22
29
  this.credentials = credentials;
@@ -46,9 +53,11 @@ export class GChatClient implements ServiceClient {
46
53
  async send(options: GChatSendOptions & { spaceId?: string }): Promise<GChatSendResult> {
47
54
  if (this.credentials.type === 'webhook') {
48
55
  return this.sendViaWebhook(options);
49
- } else {
50
- return this.sendViaOAuth(options);
51
56
  }
57
+ if (options.spaceId) {
58
+ options.spaceId = await this.resolveSpaceId(options.spaceId);
59
+ }
60
+ return this.sendViaOAuth(options);
52
61
  }
53
62
 
54
63
  async list(options: GChatListOptions): Promise<GChatMessage[]> {
@@ -66,6 +75,7 @@ export class GChatClient implements ServiceClient {
66
75
  'Use an OAuth profile to read messages'
67
76
  );
68
77
  }
78
+ options.spaceId = await this.resolveSpaceId(options.spaceId);
69
79
  return this.listViaOAuth(options);
70
80
  }
71
81
 
@@ -84,6 +94,7 @@ export class GChatClient implements ServiceClient {
84
94
  'Use an OAuth profile to read messages'
85
95
  );
86
96
  }
97
+ options.spaceId = await this.resolveSpaceId(options.spaceId);
87
98
  return this.getViaOAuth(options);
88
99
  }
89
100
 
@@ -223,6 +234,11 @@ export class GChatClient implements ServiceClient {
223
234
  });
224
235
 
225
236
  const messages = response.data.messages || [];
237
+
238
+ // Resolve unique sender IDs to display names via People API
239
+ const senderIds = [...new Set(messages.map(m => m.sender?.name).filter(Boolean))] as string[];
240
+ await this.resolveUsers(senderIds, auth);
241
+
226
242
  return messages.map((msg: chat_v1.Schema$Message) => {
227
243
  const gchatMsg: GChatMessage = {
228
244
  name: msg.name || '',
@@ -231,12 +247,7 @@ export class GChatClient implements ServiceClient {
231
247
  updateTime: (msg as Record<string, unknown>).lastUpdateTime as string || new Date().toISOString(),
232
248
  };
233
249
  if (msg.text) gchatMsg.text = msg.text;
234
- if (msg.sender?.name) {
235
- gchatMsg.sender = {
236
- name: msg.sender.name,
237
- displayName: msg.sender.displayName || msg.sender.name,
238
- };
239
- }
250
+ gchatMsg.sender = this.enrichSender(msg);
240
251
  if (msg.thread?.name) {
241
252
  gchatMsg.thread = {
242
253
  name: msg.thread.name,
@@ -270,6 +281,12 @@ export class GChatClient implements ServiceClient {
270
281
  }
271
282
 
272
283
  const msg = response.data as chat_v1.Schema$Message;
284
+
285
+ // Resolve sender
286
+ if (msg.sender?.name) {
287
+ await this.resolveUsers([msg.sender.name], auth);
288
+ }
289
+
273
290
  const gchatMsg: GChatMessage = {
274
291
  name: msg.name || '',
275
292
  createTime: msg.createTime || new Date().toISOString(),
@@ -277,12 +294,7 @@ export class GChatClient implements ServiceClient {
277
294
  updateTime: (msg as Record<string, unknown>).lastUpdateTime as string || new Date().toISOString(),
278
295
  };
279
296
  if (msg.text) gchatMsg.text = msg.text;
280
- if (msg.sender?.name) {
281
- gchatMsg.sender = {
282
- name: msg.sender.name,
283
- displayName: msg.sender.displayName || msg.sender.name,
284
- };
285
- }
297
+ gchatMsg.sender = this.enrichSender(msg);
286
298
  if (msg.thread?.name) {
287
299
  gchatMsg.thread = {
288
300
  name: msg.thread.name,
@@ -306,15 +318,29 @@ export class GChatClient implements ServiceClient {
306
318
  const chat = gchat({ version: 'v1', auth: auth as any });
307
319
 
308
320
  try {
309
- const response = await chat.spaces.list({});
310
-
311
- const spaces = response.data.spaces || [];
312
- return spaces.map((space: chat_v1.Schema$Space) => ({
313
- name: space.name || '',
314
- displayName: space.displayName || 'Unnamed',
315
- type: (space.type as 'ROOM' | 'DM') || 'ROOM',
316
- description: space.spaceDetails?.description || undefined,
317
- }));
321
+ const allSpaces: GChatSpace[] = [];
322
+ let pageToken: string | undefined;
323
+
324
+ do {
325
+ const response = await chat.spaces.list({
326
+ pageSize: 100,
327
+ pageToken,
328
+ });
329
+
330
+ const spaces = response.data.spaces || [];
331
+ for (const space of spaces) {
332
+ allSpaces.push({
333
+ name: space.name || '',
334
+ displayName: space.displayName || 'Unnamed',
335
+ type: (space.type as 'ROOM' | 'DM') || 'ROOM',
336
+ description: space.spaceDetails?.description || undefined,
337
+ });
338
+ }
339
+
340
+ pageToken = response.data.nextPageToken || undefined;
341
+ } while (pageToken);
342
+
343
+ return allSpaces;
318
344
  } catch (err) {
319
345
  const code = this.getErrorCode(err);
320
346
  const message = this.getErrorMessage(err);
@@ -326,6 +352,88 @@ export class GChatClient implements ServiceClient {
326
352
  }
327
353
  }
328
354
 
355
+ /**
356
+ * Resolve a space identifier that may be an ID or a display name.
357
+ * Tries as ID first (no API call), falls back to name resolution via listSpaces.
358
+ * Results are cached for the lifetime of this client instance.
359
+ */
360
+ private async resolveSpaceId(spaceIdOrName: string): Promise<string> {
361
+ // Check cache first
362
+ const cached = this.spaceIdCache.get(spaceIdOrName);
363
+ if (cached) return cached;
364
+
365
+ const oauthCreds = this.credentials as GChatOAuthCredentials;
366
+ const auth = this.createOAuthClient(oauthCreds);
367
+ const chat = gchat({ version: 'v1', auth: auth as any });
368
+
369
+ // Try as ID first
370
+ try {
371
+ const resp = await chat.spaces.get({ name: `spaces/${spaceIdOrName}` });
372
+ if (resp.data.name) {
373
+ this.spaceIdCache.set(spaceIdOrName, spaceIdOrName);
374
+ return spaceIdOrName;
375
+ }
376
+ } catch {
377
+ // Not a valid ID, try as display name
378
+ }
379
+
380
+ // Resolve by display name
381
+ const spaces = await this.listSpacesViaOAuth();
382
+ const nameLower = spaceIdOrName.toLowerCase();
383
+ const match = spaces.find(s => s.displayName.toLowerCase() === nameLower);
384
+
385
+ if (!match) {
386
+ throw new CliError(
387
+ 'NOT_FOUND',
388
+ `Space not found: "${spaceIdOrName}"`,
389
+ 'Use "agentio gchat spaces" to list available spaces'
390
+ );
391
+ }
392
+
393
+ const id = match.name.replace('spaces/', '');
394
+ this.spaceIdCache.set(spaceIdOrName, id);
395
+ return id;
396
+ }
397
+
398
+ private async resolveUsers(userIds: string[], auth: OAuth2Client): Promise<void> {
399
+ const unknown = userIds.filter(id => !this.userCache.has(id));
400
+ if (unknown.length === 0) return;
401
+
402
+ const token = await auth.getAccessToken();
403
+ if (!token.token) return;
404
+
405
+ // Resolve users in parallel via People API
406
+ await Promise.all(unknown.map(async (userId) => {
407
+ try {
408
+ // userId is like "users/123456", extract the numeric part
409
+ const personId = userId.replace('users/', '');
410
+ const res = await fetch(
411
+ `https://people.googleapis.com/v1/people/${personId}?personFields=names,emailAddresses`,
412
+ { headers: { Authorization: `Bearer ${token.token}` } }
413
+ );
414
+ if (!res.ok) return;
415
+ const data = await res.json() as Record<string, any>;
416
+ const name = data.names?.[0]?.displayName;
417
+ const email = data.emailAddresses?.[0]?.value;
418
+ if (name) {
419
+ this.userCache.set(userId, { displayName: name, email });
420
+ }
421
+ } catch {
422
+ // Silently skip unresolvable users
423
+ }
424
+ }));
425
+ }
426
+
427
+ private enrichSender(msg: chat_v1.Schema$Message): GChatMessage['sender'] {
428
+ if (!msg.sender?.name) return undefined;
429
+ const cached = this.userCache.get(msg.sender.name);
430
+ return {
431
+ name: msg.sender.name,
432
+ displayName: cached?.displayName || msg.sender.displayName || msg.sender.name,
433
+ email: cached?.email,
434
+ };
435
+ }
436
+
329
437
  private getErrorCode(err: unknown): ErrorCode {
330
438
  if (err && typeof err === 'object') {
331
439
  const error = err as Record<string, unknown>;
@@ -1,6 +1,7 @@
1
1
  export interface GChatSender {
2
2
  name: string;
3
3
  displayName: string;
4
+ email?: string;
4
5
  avatarUrl?: string;
5
6
  }
6
7
 
@@ -132,7 +132,10 @@ export function printGChatMessageList(messages: GChatMessage[]): void {
132
132
  const msg = messages[i];
133
133
  console.log(`[${i + 1}] ${msg.name}`);
134
134
  if (msg.sender) {
135
- console.log(` From: ${msg.sender.displayName || 'Unknown'}`);
135
+ const from = msg.sender.email
136
+ ? `${msg.sender.displayName} <${msg.sender.email}>`
137
+ : msg.sender.displayName || 'Unknown';
138
+ console.log(` From: ${from}`);
136
139
  }
137
140
  if (msg.text) {
138
141
  const snippet = msg.text.length > 100 ? msg.text.substring(0, 100) + '...' : msg.text;
@@ -146,7 +149,10 @@ export function printGChatMessageList(messages: GChatMessage[]): void {
146
149
  export function printGChatMessage(msg: GChatMessage): void {
147
150
  console.log(`ID: ${msg.name}`);
148
151
  if (msg.sender) {
149
- console.log(`From: ${msg.sender.displayName || 'Unknown'}`);
152
+ const from = msg.sender.email
153
+ ? `${msg.sender.displayName} <${msg.sender.email}>`
154
+ : msg.sender.displayName || 'Unknown';
155
+ console.log(`From: ${from}`);
150
156
  }
151
157
  console.log(`Date: ${msg.createTime}`);
152
158
  if (msg.thread) {