@plosson/agentio 0.4.4 → 0.5.2
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/commands/config.ts +13 -7
- package/src/commands/discourse.ts +5 -1
- package/src/commands/gateway.ts +384 -87
- package/src/commands/gcal.ts +14 -5
- package/src/commands/gchat.ts +16 -10
- package/src/commands/gdocs.ts +8 -2
- package/src/commands/gdrive.ts +9 -3
- package/src/commands/github.ts +8 -1
- package/src/commands/gmail.ts +14 -5
- package/src/commands/gsheets.ts +16 -6
- package/src/commands/gtasks.ts +24 -10
- package/src/commands/jira.ts +10 -3
- package/src/commands/slack.ts +10 -4
- package/src/commands/sql.ts +18 -2
- package/src/commands/status.ts +13 -7
- package/src/commands/telegram.ts +14 -2
- package/src/commands/whatsapp.ts +24 -4
- package/src/config/config-manager.ts +104 -14
- package/src/gateway/api.ts +9 -12
- package/src/gateway/client.ts +18 -15
- package/src/gateway/daemon.ts +35 -203
- package/src/gateway/types.ts +4 -4
- package/src/services/gdrive/client.ts +19 -9
- package/src/types/config.ts +30 -21
- package/src/utils/output.ts +8 -15
- package/src/utils/profile-commands.ts +36 -5
- package/src/utils/read-only.ts +22 -0
package/src/gateway/daemon.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import type { GatewayConfig, DaemonState, DEFAULT_GATEWAY_CONFIG } from './types';
|
|
7
|
-
import { CONFIG_DIR, loadConfig } from '../config/config-manager';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import type { ServiceName, Config } from '../types/config';
|
|
4
|
+
import type { GatewayConfig } from './types';
|
|
5
|
+
import { CONFIG_DIR, loadConfig, saveConfig } from '../config/config-manager';
|
|
8
6
|
import { getCredentials } from '../auth/token-store';
|
|
9
7
|
import { initDatabase, closeDatabase, insertInboxMessage, inboxMessageExists, getPendingOutboxMessages, updateOutboxStatus, cleanupInbox, cleanupOutbox } from './store';
|
|
10
8
|
import { startApiServer, stopApiServer } from './api';
|
|
@@ -15,10 +13,8 @@ import { WhatsAppAdapter } from './adapters/whatsapp';
|
|
|
15
13
|
import type { TelegramCredentials } from '../types/telegram';
|
|
16
14
|
import type { WhatsAppCredentials } from '../types/whatsapp';
|
|
17
15
|
|
|
18
|
-
const PID_FILE = join(CONFIG_DIR, 'gateway.pid');
|
|
19
16
|
const LOG_FILE = join(CONFIG_DIR, 'gateway.log');
|
|
20
17
|
|
|
21
|
-
let isRunning = false;
|
|
22
18
|
let shutdownRequested = false;
|
|
23
19
|
let adapters: Map<ServiceName, ServiceAdapter> = new Map();
|
|
24
20
|
let outboxInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -32,46 +28,6 @@ export async function getGatewayConfig(): Promise<GatewayConfig> {
|
|
|
32
28
|
return (config as unknown as { gateway?: GatewayConfig }).gateway ?? {};
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
/**
|
|
36
|
-
* Check if daemon is running
|
|
37
|
-
*/
|
|
38
|
-
export async function isDaemonRunning(): Promise<{ running: boolean; pid?: number }> {
|
|
39
|
-
if (!existsSync(PID_FILE)) {
|
|
40
|
-
return { running: false };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const pidStr = await readFile(PID_FILE, 'utf-8');
|
|
45
|
-
const pid = parseInt(pidStr.trim(), 10);
|
|
46
|
-
|
|
47
|
-
// Check if process is still running
|
|
48
|
-
try {
|
|
49
|
-
process.kill(pid, 0); // Doesn't kill, just checks
|
|
50
|
-
return { running: true, pid };
|
|
51
|
-
} catch {
|
|
52
|
-
// Process not running, clean up stale PID file
|
|
53
|
-
await unlink(PID_FILE).catch(() => {});
|
|
54
|
-
return { running: false };
|
|
55
|
-
}
|
|
56
|
-
} catch {
|
|
57
|
-
return { running: false };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Write PID file
|
|
63
|
-
*/
|
|
64
|
-
async function writePidFile(): Promise<void> {
|
|
65
|
-
await writeFile(PID_FILE, process.pid.toString(), { mode: 0o600 });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Remove PID file
|
|
70
|
-
*/
|
|
71
|
-
async function removePidFile(): Promise<void> {
|
|
72
|
-
await unlink(PID_FILE).catch(() => {});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
31
|
/**
|
|
76
32
|
* Handle inbound message from adapter
|
|
77
33
|
*/
|
|
@@ -191,16 +147,17 @@ async function initializeAdapters(): Promise<void> {
|
|
|
191
147
|
handleInboundMessage('telegram', profile, message);
|
|
192
148
|
};
|
|
193
149
|
|
|
194
|
-
for (const
|
|
150
|
+
for (const entry of telegramProfiles) {
|
|
151
|
+
const profileName = typeof entry === 'string' ? entry : entry.name;
|
|
195
152
|
try {
|
|
196
|
-
const credentials = await getCredentials<TelegramCredentials>('telegram',
|
|
153
|
+
const credentials = await getCredentials<TelegramCredentials>('telegram', profileName);
|
|
197
154
|
if (credentials) {
|
|
198
|
-
await telegramAdapter.connect(
|
|
155
|
+
await telegramAdapter.connect(profileName, credentials);
|
|
199
156
|
} else {
|
|
200
|
-
console.error(`[telegram] No credentials for profile: ${
|
|
157
|
+
console.error(`[telegram] No credentials for profile: ${profileName}`);
|
|
201
158
|
}
|
|
202
159
|
} catch (error) {
|
|
203
|
-
console.error(`[telegram] Failed to connect ${
|
|
160
|
+
console.error(`[telegram] Failed to connect ${profileName}:`, error instanceof Error ? error.message : error);
|
|
204
161
|
}
|
|
205
162
|
}
|
|
206
163
|
|
|
@@ -215,16 +172,17 @@ async function initializeAdapters(): Promise<void> {
|
|
|
215
172
|
handleInboundMessage('whatsapp', profile, message);
|
|
216
173
|
};
|
|
217
174
|
|
|
218
|
-
for (const
|
|
175
|
+
for (const entry of whatsappProfiles) {
|
|
176
|
+
const profileName = typeof entry === 'string' ? entry : entry.name;
|
|
219
177
|
try {
|
|
220
|
-
const credentials = await getCredentials<WhatsAppCredentials>('whatsapp',
|
|
178
|
+
const credentials = await getCredentials<WhatsAppCredentials>('whatsapp', profileName);
|
|
221
179
|
if (credentials) {
|
|
222
|
-
await whatsappAdapter.connect(
|
|
180
|
+
await whatsappAdapter.connect(profileName, credentials);
|
|
223
181
|
} else {
|
|
224
|
-
console.error(`[whatsapp] No credentials for profile: ${
|
|
182
|
+
console.error(`[whatsapp] No credentials for profile: ${profileName}`);
|
|
225
183
|
}
|
|
226
184
|
} catch (error) {
|
|
227
|
-
console.error(`[whatsapp] Failed to connect ${
|
|
185
|
+
console.error(`[whatsapp] Failed to connect ${profileName}:`, error instanceof Error ? error.message : error);
|
|
228
186
|
}
|
|
229
187
|
}
|
|
230
188
|
|
|
@@ -248,52 +206,10 @@ async function shutdownAdapters(): Promise<void> {
|
|
|
248
206
|
}
|
|
249
207
|
|
|
250
208
|
/**
|
|
251
|
-
* Start the gateway
|
|
209
|
+
* Start the gateway server (runs in foreground, managed by systemd)
|
|
252
210
|
*/
|
|
253
|
-
export async function
|
|
254
|
-
|
|
255
|
-
const status = await isDaemonRunning();
|
|
256
|
-
if (status.running) {
|
|
257
|
-
throw new Error(`Gateway already running (PID ${status.pid})`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (!options.foreground) {
|
|
261
|
-
// Fork to background
|
|
262
|
-
// Find the script path from argv or use import.meta to get current file location
|
|
263
|
-
let scriptPath: string;
|
|
264
|
-
|
|
265
|
-
// import.meta.path gives us the current file path, navigate to index.ts
|
|
266
|
-
const currentFile = import.meta.path || import.meta.url.replace('file://', '');
|
|
267
|
-
const srcDir = join(currentFile, '..', '..');
|
|
268
|
-
scriptPath = join(srcDir, 'index.ts');
|
|
269
|
-
|
|
270
|
-
// Verify the path exists, fallback to cwd-based path
|
|
271
|
-
if (!existsSync(scriptPath)) {
|
|
272
|
-
scriptPath = join(process.cwd(), 'src', 'index.ts');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Open log file for appending - child writes directly to file
|
|
276
|
-
const logFd = openSync(LOG_FILE, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND, 0o644);
|
|
277
|
-
|
|
278
|
-
const child = spawn(process.execPath, [scriptPath, 'gateway', 'start', '--foreground'], {
|
|
279
|
-
detached: true,
|
|
280
|
-
stdio: ['ignore', logFd, logFd],
|
|
281
|
-
env: process.env,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
child.unref();
|
|
285
|
-
|
|
286
|
-
// Close the fd in parent - child has its own copy
|
|
287
|
-
closeSync(logFd);
|
|
288
|
-
|
|
289
|
-
console.log(`Gateway started in background (PID ${child.pid})`);
|
|
290
|
-
console.log(`Logs: ${LOG_FILE}`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Running in foreground
|
|
295
|
-
isRunning = true;
|
|
296
|
-
console.log(`Gateway starting (PID ${process.pid})`);
|
|
211
|
+
export async function startGateway(): Promise<void> {
|
|
212
|
+
console.log(`agentio-gateway starting (PID ${process.pid})`);
|
|
297
213
|
|
|
298
214
|
// Handle shutdown signals
|
|
299
215
|
const shutdown = async (signal: string) => {
|
|
@@ -319,9 +235,6 @@ export async function startDaemon(options: { foreground?: boolean } = {}): Promi
|
|
|
319
235
|
// Close database
|
|
320
236
|
closeDatabase();
|
|
321
237
|
|
|
322
|
-
// Remove PID file
|
|
323
|
-
await removePidFile();
|
|
324
|
-
|
|
325
238
|
console.log('Gateway stopped');
|
|
326
239
|
process.exit(0);
|
|
327
240
|
};
|
|
@@ -330,11 +243,20 @@ export async function startDaemon(options: { foreground?: boolean } = {}): Promi
|
|
|
330
243
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
331
244
|
|
|
332
245
|
try {
|
|
333
|
-
//
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
246
|
+
// Load config and auto-generate API key on first run
|
|
247
|
+
const config = await loadConfig() as Config;
|
|
248
|
+
let gatewayConfig = config.gateway ?? {};
|
|
249
|
+
|
|
250
|
+
if (!gatewayConfig.apiKey) {
|
|
251
|
+
const generatedKey = `gw_${randomBytes(24).toString('base64url')}`;
|
|
252
|
+
gatewayConfig = {
|
|
253
|
+
...gatewayConfig,
|
|
254
|
+
apiKey: generatedKey,
|
|
255
|
+
};
|
|
256
|
+
config.gateway = gatewayConfig;
|
|
257
|
+
await saveConfig(config);
|
|
258
|
+
console.log(`First run - generated API key: ${generatedKey}`);
|
|
259
|
+
}
|
|
338
260
|
|
|
339
261
|
// Initialize database
|
|
340
262
|
await initDatabase();
|
|
@@ -350,7 +272,7 @@ export async function startDaemon(options: { foreground?: boolean } = {}): Promi
|
|
|
350
272
|
await initializeAdapters();
|
|
351
273
|
|
|
352
274
|
// Start API server
|
|
353
|
-
startApiServer(gatewayConfig
|
|
275
|
+
startApiServer(gatewayConfig, adapters);
|
|
354
276
|
|
|
355
277
|
// Start outbox processor (every 2 seconds)
|
|
356
278
|
outboxInterval = setInterval(processOutbox, 2000);
|
|
@@ -364,98 +286,8 @@ export async function startDaemon(options: { foreground?: boolean } = {}): Promi
|
|
|
364
286
|
await new Promise(() => {}); // Wait forever
|
|
365
287
|
} catch (error) {
|
|
366
288
|
console.error('Gateway error:', error instanceof Error ? error.message : error);
|
|
367
|
-
await removePidFile();
|
|
368
289
|
process.exit(1);
|
|
369
290
|
}
|
|
370
291
|
}
|
|
371
292
|
|
|
372
|
-
|
|
373
|
-
* Stop the gateway daemon
|
|
374
|
-
*/
|
|
375
|
-
export async function stopDaemon(): Promise<void> {
|
|
376
|
-
const status = await isDaemonRunning();
|
|
377
|
-
if (!status.running || !status.pid) {
|
|
378
|
-
console.log('Gateway is not running');
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
process.kill(status.pid, 'SIGTERM');
|
|
384
|
-
console.log(`Sent SIGTERM to gateway (PID ${status.pid})`);
|
|
385
|
-
|
|
386
|
-
// Wait for process to stop
|
|
387
|
-
for (let i = 0; i < 30; i++) {
|
|
388
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
389
|
-
try {
|
|
390
|
-
process.kill(status.pid, 0);
|
|
391
|
-
} catch {
|
|
392
|
-
console.log('Gateway stopped');
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Force kill if still running
|
|
398
|
-
try {
|
|
399
|
-
process.kill(status.pid, 'SIGKILL');
|
|
400
|
-
console.log('Gateway force killed');
|
|
401
|
-
} catch {
|
|
402
|
-
console.log('Gateway stopped');
|
|
403
|
-
}
|
|
404
|
-
} catch (error) {
|
|
405
|
-
console.error('Failed to stop gateway:', error instanceof Error ? error.message : error);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Get daemon status
|
|
411
|
-
*/
|
|
412
|
-
export async function getDaemonStatus(): Promise<{
|
|
413
|
-
running: boolean;
|
|
414
|
-
pid?: number;
|
|
415
|
-
adapters?: { service: string; profile: string; connected: boolean }[];
|
|
416
|
-
}> {
|
|
417
|
-
const status = await isDaemonRunning();
|
|
418
|
-
if (!status.running) {
|
|
419
|
-
return { running: false };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Try to get status from API
|
|
423
|
-
const gatewayConfig = await getGatewayConfig();
|
|
424
|
-
const port = gatewayConfig.api?.port ?? 7890;
|
|
425
|
-
const host = gatewayConfig.api?.host ?? '127.0.0.1';
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
const response = await fetch(`http://${host}:${port}/status`, {
|
|
429
|
-
headers: gatewayConfig.api?.secret ? { Authorization: `Bearer ${gatewayConfig.api.secret}` } : {},
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
if (response.ok) {
|
|
433
|
-
const data = await response.json() as { adapters: { service: string; profile: string; connected: boolean }[] };
|
|
434
|
-
return {
|
|
435
|
-
running: true,
|
|
436
|
-
pid: status.pid,
|
|
437
|
-
adapters: data.adapters,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
} catch {
|
|
441
|
-
// API not responding, but process exists
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return { running: true, pid: status.pid };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Reload daemon configuration
|
|
449
|
-
*/
|
|
450
|
-
export async function reloadDaemon(): Promise<void> {
|
|
451
|
-
const status = await isDaemonRunning();
|
|
452
|
-
if (!status.running || !status.pid) {
|
|
453
|
-
throw new Error('Gateway is not running');
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Send SIGHUP to trigger reload
|
|
457
|
-
process.kill(status.pid, 'SIGHUP');
|
|
458
|
-
console.log(`Sent reload signal to gateway (PID ${status.pid})`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export { PID_FILE, LOG_FILE };
|
|
293
|
+
export { LOG_FILE };
|
package/src/gateway/types.ts
CHANGED
|
@@ -59,11 +59,11 @@ export type OutboxStatus = 'pending' | 'sending' | 'sent' | 'failed';
|
|
|
59
59
|
|
|
60
60
|
export const DEFAULT_GATEWAY_CONFIG: Required<GatewayConfig> = {
|
|
61
61
|
name: '',
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
apiUrl: '',
|
|
63
|
+
apiKey: '',
|
|
64
|
+
server: {
|
|
64
65
|
port: 7890,
|
|
65
|
-
host: '
|
|
66
|
-
secret: '',
|
|
66
|
+
host: '0.0.0.0', // Bind to all interfaces by default for server
|
|
67
67
|
},
|
|
68
68
|
webhook: {
|
|
69
69
|
url: '',
|
|
@@ -132,15 +132,25 @@ export class GDriveClient implements ServiceClient {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const q = queryParts.join(' and ') || undefined;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
const allFiles: GDriveFile[] = [];
|
|
136
|
+
let pageToken: string | undefined;
|
|
137
|
+
|
|
138
|
+
// Paginate through results until we have enough or no more pages
|
|
139
|
+
do {
|
|
140
|
+
const response = await this.drive.files.list({
|
|
141
|
+
pageSize: Math.min(limit - allFiles.length, 100),
|
|
142
|
+
pageToken,
|
|
143
|
+
q,
|
|
144
|
+
fields: 'nextPageToken,files(id,name,mimeType,size,createdTime,modifiedTime,owners,parents,webViewLink,webContentLink,starred,trashed,shared,description)',
|
|
145
|
+
orderBy,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const files = (response.data.files || []).map(this.parseFile);
|
|
149
|
+
allFiles.push(...files);
|
|
150
|
+
pageToken = response.data.nextPageToken || undefined;
|
|
151
|
+
} while (pageToken && allFiles.length < limit);
|
|
152
|
+
|
|
153
|
+
return allFiles.slice(0, limit);
|
|
144
154
|
} catch (err) {
|
|
145
155
|
this.throwApiError(err, 'list files');
|
|
146
156
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export interface GatewayServerConfig {
|
|
2
|
+
// Server binding (for running a gateway daemon)
|
|
3
|
+
port?: number; // Port to bind (default: 7890)
|
|
4
|
+
host?: string; // Host to bind (default: 0.0.0.0 for server)
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface GatewayWebhookConfig {
|
|
@@ -21,30 +21,39 @@ export interface GatewayRetentionConfig {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface GatewayConfig {
|
|
24
|
-
name?: string; // Gateway identity name
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
name?: string; // Gateway identity name (for local server identification)
|
|
25
|
+
apiUrl?: string; // Gateway URL (e.g., "https://gateway.example.com:7890")
|
|
26
|
+
apiKey?: string; // API key for authentication
|
|
27
|
+
server?: GatewayServerConfig; // Server binding settings (for running daemon)
|
|
27
28
|
webhook?: GatewayWebhookConfig;
|
|
28
29
|
media?: GatewayMediaConfig;
|
|
29
30
|
retention?: GatewayRetentionConfig;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
export interface ProfileEntry {
|
|
34
|
+
name: string;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper type for backward compatibility during migration
|
|
39
|
+
export type ProfileValue = string | ProfileEntry;
|
|
40
|
+
|
|
32
41
|
export interface Config {
|
|
33
42
|
profiles: {
|
|
34
|
-
gdocs?:
|
|
35
|
-
gdrive?:
|
|
36
|
-
gmail?:
|
|
37
|
-
gcal?:
|
|
38
|
-
gtasks?:
|
|
39
|
-
gchat?:
|
|
40
|
-
gsheets?:
|
|
41
|
-
github?:
|
|
42
|
-
jira?:
|
|
43
|
-
slack?:
|
|
44
|
-
telegram?:
|
|
45
|
-
whatsapp?:
|
|
46
|
-
discourse?:
|
|
47
|
-
sql?:
|
|
43
|
+
gdocs?: ProfileValue[];
|
|
44
|
+
gdrive?: ProfileValue[];
|
|
45
|
+
gmail?: ProfileValue[];
|
|
46
|
+
gcal?: ProfileValue[];
|
|
47
|
+
gtasks?: ProfileValue[];
|
|
48
|
+
gchat?: ProfileValue[];
|
|
49
|
+
gsheets?: ProfileValue[];
|
|
50
|
+
github?: ProfileValue[];
|
|
51
|
+
jira?: ProfileValue[];
|
|
52
|
+
slack?: ProfileValue[];
|
|
53
|
+
telegram?: ProfileValue[];
|
|
54
|
+
whatsapp?: ProfileValue[];
|
|
55
|
+
discourse?: ProfileValue[];
|
|
56
|
+
sql?: ProfileValue[];
|
|
48
57
|
};
|
|
49
58
|
env?: Record<string, string>;
|
|
50
59
|
gateway?: GatewayConfig;
|
package/src/utils/output.ts
CHANGED
|
@@ -515,22 +515,15 @@ export function printGDriveFileList(files: GDriveFile[], title: string = 'Files'
|
|
|
515
515
|
|
|
516
516
|
console.log(`${title} (${files.length})\n`);
|
|
517
517
|
|
|
518
|
-
for (
|
|
519
|
-
const file = files[i];
|
|
518
|
+
for (const file of files) {
|
|
520
519
|
const isFolder = file.mimeType === 'application/vnd.google-apps.folder';
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
console.log(`
|
|
528
|
-
console.log(` ID: ${file.id}`);
|
|
529
|
-
if (file.size) console.log(` Size: ${formatBytes(file.size)}`);
|
|
530
|
-
if (file.owners?.length) console.log(` Owner: ${file.owners[0]}`);
|
|
531
|
-
if (file.modifiedTime) console.log(` Modified: ${file.modifiedTime}`);
|
|
532
|
-
if (file.webViewLink) console.log(` Link: ${file.webViewLink}`);
|
|
533
|
-
console.log('');
|
|
520
|
+
const type = getShortMimeType(file.mimeType).padEnd(7);
|
|
521
|
+
const size = file.size ? formatBytes(file.size).padStart(8) : ' -';
|
|
522
|
+
const date = file.modifiedTime ? file.modifiedTime.slice(0, 10) : ' ';
|
|
523
|
+
const flags = (file.starred ? '*' : '') + (file.shared ? '⇄' : '');
|
|
524
|
+
const name = isFolder ? `${file.name}/` : file.name;
|
|
525
|
+
console.log(`${type} ${size} ${date} ${flags.padEnd(2)} ${name}`);
|
|
526
|
+
console.log(` ${file.id}`);
|
|
534
527
|
}
|
|
535
528
|
}
|
|
536
529
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { listProfiles, removeProfile } from '../config/config-manager';
|
|
2
|
+
import { listProfiles, removeProfile, setProfileReadOnly } from '../config/config-manager';
|
|
3
3
|
import { removeCredentials, getCredentials } from '../auth/token-store';
|
|
4
|
-
import { handleError } from './errors';
|
|
4
|
+
import { handleError, CliError } from './errors';
|
|
5
5
|
import type { ServiceName } from '../types/config';
|
|
6
6
|
|
|
7
7
|
export interface ProfileCommandsOptions<T> {
|
|
@@ -31,10 +31,11 @@ export function createProfileCommands<T>(
|
|
|
31
31
|
if (profiles.length === 0) {
|
|
32
32
|
console.log('No profiles configured');
|
|
33
33
|
} else {
|
|
34
|
-
for (const
|
|
35
|
-
const credentials = await getCredentials<T>(service, name);
|
|
34
|
+
for (const entry of profiles) {
|
|
35
|
+
const credentials = await getCredentials<T>(service, entry.name);
|
|
36
36
|
const extraInfo = getExtraInfo ? getExtraInfo(credentials) : '';
|
|
37
|
-
|
|
37
|
+
const readOnlyIndicator = entry.readOnly ? ' [read-only]' : '';
|
|
38
|
+
console.log(`${entry.name}${readOnlyIndicator}${extraInfo}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
} catch (error) {
|
|
@@ -42,6 +43,36 @@ export function createProfileCommands<T>(
|
|
|
42
43
|
}
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
profile
|
|
47
|
+
.command('update')
|
|
48
|
+
.description(`Update a ${displayName} profile`)
|
|
49
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
50
|
+
.option('--read-only', 'Set profile as read-only')
|
|
51
|
+
.option('--no-read-only', 'Remove read-only restriction')
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
try {
|
|
54
|
+
const profileName = opts.profile;
|
|
55
|
+
|
|
56
|
+
// Check if read-only flag is explicitly set or unset
|
|
57
|
+
if (opts.readOnly === undefined) {
|
|
58
|
+
throw new CliError('INVALID_PARAMS', 'No update specified', 'Use --read-only or --no-read-only');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const updated = await setProfileReadOnly(service, profileName, opts.readOnly);
|
|
62
|
+
if (!updated) {
|
|
63
|
+
throw new CliError('PROFILE_NOT_FOUND', `Profile "${profileName}" not found`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opts.readOnly) {
|
|
67
|
+
console.log(`Profile "${profileName}" is now read-only`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`Profile "${profileName}" read-only restriction removed`);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
handleError(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
45
76
|
profile
|
|
46
77
|
.command('remove')
|
|
47
78
|
.description(`Remove a ${displayName} profile`)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CliError } from './errors';
|
|
2
|
+
import { isProfileReadOnly } from '../config/config-manager';
|
|
3
|
+
import type { ServiceName } from '../types/config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enforce that a profile has write access for a given operation.
|
|
7
|
+
* Throws a CliError if the profile is read-only.
|
|
8
|
+
*/
|
|
9
|
+
export async function enforceWriteAccess(
|
|
10
|
+
service: ServiceName,
|
|
11
|
+
profile: string,
|
|
12
|
+
operation: string
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const readOnly = await isProfileReadOnly(service, profile);
|
|
15
|
+
if (readOnly) {
|
|
16
|
+
throw new CliError(
|
|
17
|
+
'PERMISSION_DENIED',
|
|
18
|
+
`Cannot ${operation}: profile "${profile}" is read-only`,
|
|
19
|
+
`To modify this profile's access: agentio ${service} profile update --profile ${profile} --no-read-only`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|