@plosson/agentio 0.4.3 → 0.5.1
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 +10 -2
- package/src/commands/config.ts +13 -7
- package/src/commands/discourse.ts +5 -1
- package/src/commands/gateway.ts +381 -88
- 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 +375 -0
- 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/index.ts +2 -0
- package/src/services/gdrive/client.ts +19 -9
- package/src/services/gsheets/client.ts +362 -0
- package/src/types/config.ts +31 -21
- package/src/types/gsheets.ts +81 -0
- package/src/utils/output.ts +89 -15
- package/src/utils/profile-commands.ts +36 -5
- package/src/utils/read-only.ts +22 -0
package/src/commands/status.ts
CHANGED
|
@@ -243,6 +243,7 @@ async function createServiceClient(
|
|
|
243
243
|
interface ProfileStatus {
|
|
244
244
|
service: string;
|
|
245
245
|
profile: string;
|
|
246
|
+
readOnly?: boolean;
|
|
246
247
|
status: 'ok' | 'invalid' | 'no-creds' | 'skipped';
|
|
247
248
|
info?: string;
|
|
248
249
|
error?: string;
|
|
@@ -265,13 +266,14 @@ export function registerStatusCommand(program: Command): void {
|
|
|
265
266
|
const statuses: ProfileStatus[] = [];
|
|
266
267
|
|
|
267
268
|
for (const { service, profiles } of allProfiles) {
|
|
268
|
-
for (const
|
|
269
|
-
const credentials = await getCredentials(service, name);
|
|
269
|
+
for (const entry of profiles) {
|
|
270
|
+
const credentials = await getCredentials(service, entry.name);
|
|
270
271
|
|
|
271
272
|
if (!credentials) {
|
|
272
273
|
statuses.push({
|
|
273
274
|
service,
|
|
274
|
-
profile: name,
|
|
275
|
+
profile: entry.name,
|
|
276
|
+
readOnly: entry.readOnly,
|
|
275
277
|
status: 'no-creds',
|
|
276
278
|
});
|
|
277
279
|
continue;
|
|
@@ -280,13 +282,14 @@ export function registerStatusCommand(program: Command): void {
|
|
|
280
282
|
if (options.test === false) {
|
|
281
283
|
statuses.push({
|
|
282
284
|
service,
|
|
283
|
-
profile: name,
|
|
285
|
+
profile: entry.name,
|
|
286
|
+
readOnly: entry.readOnly,
|
|
284
287
|
status: 'skipped',
|
|
285
288
|
});
|
|
286
289
|
continue;
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
const client = await createServiceClient(service, credentials, name);
|
|
292
|
+
const client = await createServiceClient(service, credentials, entry.name);
|
|
290
293
|
let result: ValidationResult;
|
|
291
294
|
|
|
292
295
|
if (client) {
|
|
@@ -297,7 +300,8 @@ export function registerStatusCommand(program: Command): void {
|
|
|
297
300
|
|
|
298
301
|
statuses.push({
|
|
299
302
|
service,
|
|
300
|
-
profile: name,
|
|
303
|
+
profile: entry.name,
|
|
304
|
+
readOnly: entry.readOnly,
|
|
301
305
|
status: result.valid ? 'ok' : 'invalid',
|
|
302
306
|
info: result.info,
|
|
303
307
|
error: result.error,
|
|
@@ -344,7 +348,9 @@ export function registerStatusCommand(program: Command): void {
|
|
|
344
348
|
// Print each profile on one line
|
|
345
349
|
for (const s of statuses) {
|
|
346
350
|
const servicePad = s.service.padEnd(serviceWidth);
|
|
347
|
-
const
|
|
351
|
+
const readOnlyIndicator = s.readOnly ? ' [RO]' : '';
|
|
352
|
+
const profileWithRo = s.profile + readOnlyIndicator;
|
|
353
|
+
const profilePad = profileWithRo.padEnd(profileWidth + 5); // +5 for [RO]
|
|
348
354
|
|
|
349
355
|
let statusStr: string;
|
|
350
356
|
let details: string;
|
package/src/commands/telegram.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { TelegramClient } from '../services/telegram/client';
|
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
8
|
import { readStdin, prompt } from '../utils/stdin';
|
|
9
9
|
import { getGatewayClient, isGatewayAvailable } from '../gateway/client';
|
|
10
|
+
import { enforceWriteAccess } from '../utils/read-only';
|
|
10
11
|
import {
|
|
11
12
|
printInboxMessageList,
|
|
12
13
|
printInboxMessage,
|
|
@@ -59,7 +60,8 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
59
60
|
sendOptions.disable_notification = true;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const { client } = await getTelegramClient(options.profile);
|
|
63
|
+
const { client, profile } = await getTelegramClient(options.profile);
|
|
64
|
+
await enforceWriteAccess('telegram', profile, 'send message');
|
|
63
65
|
const result = await client.sendMessage(text, sendOptions);
|
|
64
66
|
|
|
65
67
|
console.log('Message sent');
|
|
@@ -81,6 +83,7 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
81
83
|
.command('add')
|
|
82
84
|
.description('Add a new Telegram bot profile')
|
|
83
85
|
.option('--profile <name>', 'Profile name (auto-detected from bot username if not provided)')
|
|
86
|
+
.option('--read-only', 'Create as read-only profile (blocks write operations)')
|
|
84
87
|
.action(async (options) => {
|
|
85
88
|
try {
|
|
86
89
|
console.error('\nTelegram Bot Setup\n');
|
|
@@ -171,10 +174,13 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
171
174
|
channelName: channelName,
|
|
172
175
|
};
|
|
173
176
|
|
|
174
|
-
await setProfile('telegram', profileName);
|
|
177
|
+
await setProfile('telegram', profileName, { readOnly: options.readOnly });
|
|
175
178
|
await setCredentials('telegram', profileName, credentials);
|
|
176
179
|
|
|
177
180
|
console.log(`\nProfile "${profileName}" configured!`);
|
|
181
|
+
if (options.readOnly) {
|
|
182
|
+
console.log(` Access: read-only`);
|
|
183
|
+
}
|
|
178
184
|
console.log(` Test with: agentio telegram send --profile ${profileName} "Hello world"`);
|
|
179
185
|
} catch (error) {
|
|
180
186
|
handleError(error);
|
|
@@ -260,6 +266,11 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
const client = await getGatewayClient();
|
|
269
|
+
// Get the inbox message to determine the profile for read-only check
|
|
270
|
+
const inboxMessage = await client.inboxGet(id);
|
|
271
|
+
if (inboxMessage) {
|
|
272
|
+
await enforceWriteAccess('telegram', inboxMessage.profile, 'reply to message');
|
|
273
|
+
}
|
|
263
274
|
const result = await client.inboxReply(id, text);
|
|
264
275
|
printInboxReplyResult(result);
|
|
265
276
|
} catch (error) {
|
|
@@ -325,6 +336,7 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
325
336
|
else throw new CliError('INVALID_PARAMS', 'parse-mode must be "html" or "markdown"');
|
|
326
337
|
}
|
|
327
338
|
|
|
339
|
+
await enforceWriteAccess('telegram', profileResult.profile, 'send message');
|
|
328
340
|
const client = await getGatewayClient();
|
|
329
341
|
const result = await client.outboxSend({
|
|
330
342
|
service: 'telegram',
|
package/src/commands/whatsapp.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { setProfile, resolveProfile, removeProfile } from '../config/config-mana
|
|
|
4
4
|
import { CliError, handleError } from '../utils/errors';
|
|
5
5
|
import { readStdin, prompt, confirm } from '../utils/stdin';
|
|
6
6
|
import { getGatewayClient, isGatewayAvailable } from '../gateway/client';
|
|
7
|
+
import { enforceWriteAccess } from '../utils/read-only';
|
|
7
8
|
import {
|
|
8
9
|
printInboxMessageList,
|
|
9
10
|
printInboxMessage,
|
|
@@ -86,6 +87,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
86
87
|
.command('add')
|
|
87
88
|
.description('Add a new WhatsApp profile and pair via QR code')
|
|
88
89
|
.option('--profile <name>', 'Profile name')
|
|
90
|
+
.option('--read-only', 'Create as read-only profile (blocks write operations)')
|
|
89
91
|
.action(async (options) => {
|
|
90
92
|
try {
|
|
91
93
|
console.error('\nWhatsApp Profile Setup\n');
|
|
@@ -111,10 +113,13 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
111
113
|
paired: false,
|
|
112
114
|
};
|
|
113
115
|
|
|
114
|
-
await setProfile('whatsapp', profileName);
|
|
116
|
+
await setProfile('whatsapp', profileName, { readOnly: options.readOnly });
|
|
115
117
|
await setCredentials('whatsapp', profileName, credentials);
|
|
116
118
|
|
|
117
119
|
console.log(`Profile "${profileName}" created.`);
|
|
120
|
+
if (options.readOnly) {
|
|
121
|
+
console.log(` Access: read-only`);
|
|
122
|
+
}
|
|
118
123
|
|
|
119
124
|
// Check if gateway is running
|
|
120
125
|
const gatewayRunning = await isGatewayAvailable();
|
|
@@ -151,12 +156,13 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
151
156
|
|
|
152
157
|
console.log(`WhatsApp profiles (${profiles.length}):\n`);
|
|
153
158
|
|
|
154
|
-
for (const
|
|
155
|
-
const creds = await getCredentials<WhatsAppCredentials>('whatsapp', name);
|
|
159
|
+
for (const entry of profiles) {
|
|
160
|
+
const creds = await getCredentials<WhatsAppCredentials>('whatsapp', entry.name);
|
|
156
161
|
const status = creds?.paired ? 'paired' : 'not paired';
|
|
157
162
|
const phone = creds?.phoneNumber ? ` (${creds.phoneNumber})` : '';
|
|
158
163
|
const displayName = creds?.displayName ? ` - ${creds.displayName}` : '';
|
|
159
|
-
|
|
164
|
+
const readOnlyIndicator = entry.readOnly ? ' [read-only]' : '';
|
|
165
|
+
console.log(` ${entry.name}${readOnlyIndicator}${phone}${displayName} [${status}]`);
|
|
160
166
|
}
|
|
161
167
|
} catch (error) {
|
|
162
168
|
handleError(error);
|
|
@@ -288,6 +294,11 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
288
294
|
}
|
|
289
295
|
|
|
290
296
|
const client = await getGatewayClient();
|
|
297
|
+
// Get the inbox message to determine the profile for read-only check
|
|
298
|
+
const inboxMessage = await client.inboxGet(id);
|
|
299
|
+
if (inboxMessage) {
|
|
300
|
+
await enforceWriteAccess('whatsapp', inboxMessage.profile, 'reply to message');
|
|
301
|
+
}
|
|
291
302
|
const result = await client.inboxReply(id, text);
|
|
292
303
|
printInboxReplyResult(result);
|
|
293
304
|
} catch (error) {
|
|
@@ -368,6 +379,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
368
379
|
}
|
|
369
380
|
}
|
|
370
381
|
|
|
382
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'send message');
|
|
371
383
|
const client = await getGatewayClient();
|
|
372
384
|
|
|
373
385
|
// Resolve destination
|
|
@@ -519,6 +531,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
519
531
|
throw new CliError('INVALID_PARAMS', 'At least one participant is required. Use --participants <phone...>');
|
|
520
532
|
}
|
|
521
533
|
|
|
534
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'create group');
|
|
522
535
|
const client = await getGatewayClient();
|
|
523
536
|
const group = await client.whatsappGroupCreate(
|
|
524
537
|
profileResult.profile,
|
|
@@ -554,6 +567,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
554
567
|
throw new CliError('INVALID_PARAMS', 'Provide --name, --description, or --picture to update.');
|
|
555
568
|
}
|
|
556
569
|
|
|
570
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'update group');
|
|
557
571
|
const client = await getGatewayClient();
|
|
558
572
|
|
|
559
573
|
// Resolve name to ID if needed
|
|
@@ -593,6 +607,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
593
607
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
594
608
|
}
|
|
595
609
|
|
|
610
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'add participants');
|
|
596
611
|
const client = await getGatewayClient();
|
|
597
612
|
|
|
598
613
|
// Resolve name to ID if needed
|
|
@@ -637,6 +652,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
637
652
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
638
653
|
}
|
|
639
654
|
|
|
655
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'remove participants');
|
|
640
656
|
const client = await getGatewayClient();
|
|
641
657
|
|
|
642
658
|
// Resolve name to ID if needed
|
|
@@ -681,6 +697,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
681
697
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
682
698
|
}
|
|
683
699
|
|
|
700
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'promote participants');
|
|
684
701
|
const client = await getGatewayClient();
|
|
685
702
|
|
|
686
703
|
// Resolve name to ID if needed
|
|
@@ -725,6 +742,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
725
742
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
726
743
|
}
|
|
727
744
|
|
|
745
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'demote participants');
|
|
728
746
|
const client = await getGatewayClient();
|
|
729
747
|
|
|
730
748
|
// Resolve name to ID if needed
|
|
@@ -768,6 +786,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
768
786
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
769
787
|
}
|
|
770
788
|
|
|
789
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'leave group');
|
|
771
790
|
const client = await getGatewayClient();
|
|
772
791
|
|
|
773
792
|
// Resolve name to ID if needed
|
|
@@ -843,6 +862,7 @@ export function registerWhatsAppCommands(program: Command): void {
|
|
|
843
862
|
throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
|
|
844
863
|
}
|
|
845
864
|
|
|
865
|
+
await enforceWriteAccess('whatsapp', profileResult.profile, 'join group');
|
|
846
866
|
const client = await getGatewayClient();
|
|
847
867
|
const groupId = await client.whatsappGroupJoin(profileResult.profile, code);
|
|
848
868
|
printWhatsAppGroupJoined(groupId);
|
|
@@ -2,12 +2,26 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
|
-
import type { Config, ServiceName } from '../types/config';
|
|
5
|
+
import type { Config, ServiceName, ProfileEntry, ProfileValue } from '../types/config';
|
|
6
6
|
|
|
7
7
|
const CONFIG_DIR = join(homedir(), '.config', 'agentio');
|
|
8
8
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
9
9
|
|
|
10
|
-
const ALL_SERVICES: ServiceName[] = ['gdocs', 'gdrive', 'gmail', 'gchat', 'github', 'jira', 'slack', 'telegram', 'whatsapp', 'discourse', 'sql'];
|
|
10
|
+
const ALL_SERVICES: ServiceName[] = ['gdocs', 'gdrive', 'gmail', 'gcal', 'gtasks', 'gchat', 'gsheets', 'github', 'jira', 'slack', 'telegram', 'whatsapp', 'discourse', 'sql'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a profile value to ProfileEntry format
|
|
14
|
+
*/
|
|
15
|
+
function normalizeProfile(entry: ProfileValue): ProfileEntry {
|
|
16
|
+
return typeof entry === 'string' ? { name: entry } : entry;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the profile name from a ProfileValue
|
|
21
|
+
*/
|
|
22
|
+
function getProfileName(entry: ProfileValue): string {
|
|
23
|
+
return typeof entry === 'string' ? entry : entry.name;
|
|
24
|
+
}
|
|
11
25
|
|
|
12
26
|
const DEFAULT_CONFIG: Config = {
|
|
13
27
|
profiles: {},
|
|
@@ -54,7 +68,8 @@ export async function getProfile(
|
|
|
54
68
|
const config = await loadConfig();
|
|
55
69
|
|
|
56
70
|
const serviceProfiles = config.profiles[service] || [];
|
|
57
|
-
|
|
71
|
+
const found = serviceProfiles.find((p) => getProfileName(p) === profileName);
|
|
72
|
+
if (!found) {
|
|
58
73
|
return null;
|
|
59
74
|
}
|
|
60
75
|
|
|
@@ -66,20 +81,23 @@ export async function getProfile(
|
|
|
66
81
|
* - If profileName is provided, validates it exists
|
|
67
82
|
* - If not provided and exactly 1 profile exists, returns that profile
|
|
68
83
|
* - Returns null if no profiles exist or if multiple profiles exist without explicit selection
|
|
84
|
+
* - Also returns the readOnly status of the resolved profile
|
|
69
85
|
*/
|
|
70
86
|
export async function resolveProfile(
|
|
71
87
|
service: ServiceName,
|
|
72
88
|
profileName?: string
|
|
73
|
-
): Promise<{ profile: string | null; error?: 'none' | 'multiple' }> {
|
|
89
|
+
): Promise<{ profile: string | null; readOnly?: boolean; error?: 'none' | 'multiple' }> {
|
|
74
90
|
const config = await loadConfig();
|
|
75
91
|
const serviceProfiles = config.profiles[service] || [];
|
|
76
92
|
|
|
77
93
|
if (profileName) {
|
|
78
94
|
// Explicit profile requested - validate it exists
|
|
79
|
-
|
|
95
|
+
const found = serviceProfiles.find((p) => getProfileName(p) === profileName);
|
|
96
|
+
if (!found) {
|
|
80
97
|
return { profile: null };
|
|
81
98
|
}
|
|
82
|
-
|
|
99
|
+
const entry = normalizeProfile(found);
|
|
100
|
+
return { profile: entry.name, readOnly: entry.readOnly };
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
// No profile specified - check if we can auto-select
|
|
@@ -88,16 +106,22 @@ export async function resolveProfile(
|
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
if (serviceProfiles.length === 1) {
|
|
91
|
-
|
|
109
|
+
const entry = normalizeProfile(serviceProfiles[0]);
|
|
110
|
+
return { profile: entry.name, readOnly: entry.readOnly };
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
// Multiple profiles exist - user must specify
|
|
95
114
|
return { profile: null, error: 'multiple' };
|
|
96
115
|
}
|
|
97
116
|
|
|
117
|
+
export interface SetProfileOptions {
|
|
118
|
+
readOnly?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
export async function setProfile(
|
|
99
122
|
service: ServiceName,
|
|
100
|
-
profileName: string
|
|
123
|
+
profileName: string,
|
|
124
|
+
options?: SetProfileOptions
|
|
101
125
|
): Promise<void> {
|
|
102
126
|
const config = await loadConfig();
|
|
103
127
|
|
|
@@ -105,8 +129,20 @@ export async function setProfile(
|
|
|
105
129
|
config.profiles[service] = [];
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
const existingIndex = config.profiles[service]!.findIndex(
|
|
133
|
+
(p) => getProfileName(p) === profileName
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const entry: ProfileEntry = {
|
|
137
|
+
name: profileName,
|
|
138
|
+
...(options?.readOnly ? { readOnly: true } : {}),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (existingIndex === -1) {
|
|
142
|
+
config.profiles[service]!.push(entry);
|
|
143
|
+
} else {
|
|
144
|
+
// Update existing profile
|
|
145
|
+
config.profiles[service]![existingIndex] = entry;
|
|
110
146
|
}
|
|
111
147
|
|
|
112
148
|
await saveConfig(config);
|
|
@@ -119,11 +155,18 @@ export async function removeProfile(
|
|
|
119
155
|
const config = await loadConfig();
|
|
120
156
|
|
|
121
157
|
const serviceProfiles = config.profiles[service];
|
|
122
|
-
if (!serviceProfiles
|
|
158
|
+
if (!serviceProfiles) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const found = serviceProfiles.find((p) => getProfileName(p) === profileName);
|
|
163
|
+
if (!found) {
|
|
123
164
|
return false;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
config.profiles[service] = serviceProfiles.filter(
|
|
167
|
+
config.profiles[service] = serviceProfiles.filter(
|
|
168
|
+
(p) => getProfileName(p) !== profileName
|
|
169
|
+
);
|
|
127
170
|
|
|
128
171
|
await saveConfig(config);
|
|
129
172
|
return true;
|
|
@@ -131,14 +174,14 @@ export async function removeProfile(
|
|
|
131
174
|
|
|
132
175
|
export async function listProfiles(service?: ServiceName): Promise<{
|
|
133
176
|
service: ServiceName;
|
|
134
|
-
profiles:
|
|
177
|
+
profiles: ProfileEntry[];
|
|
135
178
|
}[]> {
|
|
136
179
|
const config = await loadConfig();
|
|
137
180
|
const services = service ? [service] : ALL_SERVICES;
|
|
138
181
|
|
|
139
182
|
return services.map((svc) => ({
|
|
140
183
|
service: svc,
|
|
141
|
-
profiles: config.profiles[svc] || [],
|
|
184
|
+
profiles: (config.profiles[svc] || []).map(normalizeProfile),
|
|
142
185
|
}));
|
|
143
186
|
}
|
|
144
187
|
|
|
@@ -171,4 +214,51 @@ export async function listEnv(): Promise<Record<string, string>> {
|
|
|
171
214
|
return config.env || {};
|
|
172
215
|
}
|
|
173
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Check if a profile is read-only
|
|
219
|
+
*/
|
|
220
|
+
export async function isProfileReadOnly(
|
|
221
|
+
service: ServiceName,
|
|
222
|
+
profileName: string
|
|
223
|
+
): Promise<boolean> {
|
|
224
|
+
const config = await loadConfig();
|
|
225
|
+
const serviceProfiles = config.profiles[service] || [];
|
|
226
|
+
const found = serviceProfiles.find((p) => getProfileName(p) === profileName);
|
|
227
|
+
if (!found) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return normalizeProfile(found).readOnly === true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set the read-only status of a profile
|
|
235
|
+
*/
|
|
236
|
+
export async function setProfileReadOnly(
|
|
237
|
+
service: ServiceName,
|
|
238
|
+
profileName: string,
|
|
239
|
+
readOnly: boolean
|
|
240
|
+
): Promise<boolean> {
|
|
241
|
+
const config = await loadConfig();
|
|
242
|
+
const serviceProfiles = config.profiles[service];
|
|
243
|
+
if (!serviceProfiles) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const index = serviceProfiles.findIndex((p) => getProfileName(p) === profileName);
|
|
248
|
+
if (index === -1) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const entry = normalizeProfile(serviceProfiles[index]);
|
|
253
|
+
if (readOnly) {
|
|
254
|
+
entry.readOnly = true;
|
|
255
|
+
} else {
|
|
256
|
+
delete entry.readOnly;
|
|
257
|
+
}
|
|
258
|
+
serviceProfiles[index] = entry;
|
|
259
|
+
|
|
260
|
+
await saveConfig(config);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
174
264
|
export { CONFIG_DIR, CONFIG_FILE };
|
package/src/gateway/api.ts
CHANGED
|
@@ -56,7 +56,7 @@ import type { ServiceAdapter } from './adapters/types';
|
|
|
56
56
|
import type { WhatsAppAdapter } from './adapters/whatsapp';
|
|
57
57
|
|
|
58
58
|
let server: Server<unknown> | null = null;
|
|
59
|
-
let
|
|
59
|
+
let apiKey: string = '';
|
|
60
60
|
let startTime: number = 0;
|
|
61
61
|
let adapters: Map<ServiceName, ServiceAdapter> = new Map();
|
|
62
62
|
|
|
@@ -81,16 +81,13 @@ function jsonResponse<T>(data: T, status: number = 200): Response {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* Verify
|
|
84
|
+
* Verify X-API-Key header
|
|
85
85
|
*/
|
|
86
86
|
function verifyAuth(request: Request): boolean {
|
|
87
|
-
if (!
|
|
87
|
+
if (!apiKey) return true; // No auth configured
|
|
88
88
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const [type, token] = authHeader.split(' ');
|
|
93
|
-
return type === 'Bearer' && token === apiSecret;
|
|
89
|
+
const key = request.headers.get('X-API-Key');
|
|
90
|
+
return key === apiKey;
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
/**
|
|
@@ -756,10 +753,10 @@ async function handleRequest(request: Request): Promise<Response> {
|
|
|
756
753
|
/**
|
|
757
754
|
* Start the API server
|
|
758
755
|
*/
|
|
759
|
-
export function startApiServer(config: GatewayConfig
|
|
760
|
-
const port = config?.port ?? DEFAULT_GATEWAY_CONFIG.
|
|
761
|
-
const host = config?.host ?? DEFAULT_GATEWAY_CONFIG.
|
|
762
|
-
|
|
756
|
+
export function startApiServer(config: GatewayConfig, serviceAdapters: Map<ServiceName, ServiceAdapter>): Server<unknown> {
|
|
757
|
+
const port = config?.server?.port ?? DEFAULT_GATEWAY_CONFIG.server.port;
|
|
758
|
+
const host = config?.server?.host ?? DEFAULT_GATEWAY_CONFIG.server.host;
|
|
759
|
+
apiKey = config?.apiKey ?? '';
|
|
763
760
|
startTime = Date.now();
|
|
764
761
|
adapters = serviceAdapters;
|
|
765
762
|
|
package/src/gateway/client.ts
CHANGED
|
@@ -49,20 +49,20 @@ import type {
|
|
|
49
49
|
} from './types';
|
|
50
50
|
import type { WhatsAppGroup, WhatsAppParticipantAction } from '../types/whatsapp';
|
|
51
51
|
|
|
52
|
-
let cachedConfig: { url: string;
|
|
52
|
+
let cachedConfig: { url: string; apiKey: string } | null = null;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Get gateway URL and
|
|
55
|
+
* Get gateway URL and API key from config or environment
|
|
56
56
|
*/
|
|
57
|
-
async function getGatewayConnection(): Promise<{ url: string;
|
|
57
|
+
async function getGatewayConnection(): Promise<{ url: string; apiKey: string }> {
|
|
58
58
|
if (cachedConfig) return cachedConfig;
|
|
59
59
|
|
|
60
60
|
// Check environment variables first
|
|
61
61
|
const envUrl = process.env.AGENTIO_GATEWAY_URL || await getEnv('AGENTIO_GATEWAY_URL');
|
|
62
|
-
const
|
|
62
|
+
const envApiKey = process.env.AGENTIO_GATEWAY_API_KEY || await getEnv('AGENTIO_GATEWAY_API_KEY');
|
|
63
63
|
|
|
64
64
|
if (envUrl) {
|
|
65
|
-
cachedConfig = { url: envUrl,
|
|
65
|
+
cachedConfig = { url: envUrl, apiKey: envApiKey || '' };
|
|
66
66
|
return cachedConfig;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -70,16 +70,19 @@ async function getGatewayConnection(): Promise<{ url: string; secret: string }>
|
|
|
70
70
|
const config = await loadConfig() as unknown as { gateway?: GatewayConfig };
|
|
71
71
|
const gatewayConfig = config.gateway;
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Priority: apiUrl > construct from server host:port
|
|
74
|
+
if (gatewayConfig?.apiUrl) {
|
|
75
|
+
cachedConfig = { url: gatewayConfig.apiUrl, apiKey: gatewayConfig.apiKey ?? '' };
|
|
76
|
+
return cachedConfig;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
+
// Fallback for local gateway (construct URL from server host:port)
|
|
80
|
+
const host = gatewayConfig?.server?.host ?? '127.0.0.1';
|
|
81
|
+
const port = gatewayConfig?.server?.port ?? 7890;
|
|
79
82
|
const url = `http://${host}:${port}`;
|
|
80
|
-
const
|
|
83
|
+
const apiKey = gatewayConfig?.apiKey ?? '';
|
|
81
84
|
|
|
82
|
-
cachedConfig = { url,
|
|
85
|
+
cachedConfig = { url, apiKey };
|
|
83
86
|
return cachedConfig;
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -87,14 +90,14 @@ async function getGatewayConnection(): Promise<{ url: string; secret: string }>
|
|
|
87
90
|
* Make a request to the gateway API
|
|
88
91
|
*/
|
|
89
92
|
async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
|
|
90
|
-
const { url,
|
|
93
|
+
const { url, apiKey } = await getGatewayConnection();
|
|
91
94
|
|
|
92
95
|
const headers: Record<string, string> = {
|
|
93
96
|
'Content-Type': 'application/json',
|
|
94
97
|
};
|
|
95
98
|
|
|
96
|
-
if (
|
|
97
|
-
headers['
|
|
99
|
+
if (apiKey) {
|
|
100
|
+
headers['X-API-Key'] = apiKey;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
try {
|
|
@@ -106,7 +109,7 @@ async function request<T>(method: string, endpoint: string, body?: unknown): Pro
|
|
|
106
109
|
|
|
107
110
|
if (!response.ok) {
|
|
108
111
|
if (response.status === 401) {
|
|
109
|
-
throw new CliError('AUTH_FAILED', 'Gateway authentication failed', 'Check
|
|
112
|
+
throw new CliError('AUTH_FAILED', 'Gateway authentication failed', 'Check AGENTIO_GATEWAY_API_KEY');
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
const errorData = await response.json().catch(() => ({})) as { error?: string };
|