@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 +1 -1
- package/src/auth/oauth.ts +1 -0
- package/src/mcp/server.ts +19 -2
- package/src/services/gchat/client.ts +131 -23
- package/src/types/gchat.ts +1 -0
- package/src/utils/output.ts +8 -2
package/package.json
CHANGED
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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>;
|
package/src/types/gchat.ts
CHANGED
package/src/utils/output.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|