@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.
@@ -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 name of profiles) {
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 profilePad = s.profile.padEnd(profileWidth);
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;
@@ -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',
@@ -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 name of profiles) {
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
- console.log(` ${name}${phone}${displayName} [${status}]`);
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
- if (!serviceProfiles.includes(profileName)) {
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
- if (!serviceProfiles.includes(profileName)) {
95
+ const found = serviceProfiles.find((p) => getProfileName(p) === profileName);
96
+ if (!found) {
80
97
  return { profile: null };
81
98
  }
82
- return { profile: profileName };
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
- return { profile: serviceProfiles[0] };
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
- if (!config.profiles[service]!.includes(profileName)) {
109
- config.profiles[service]!.push(profileName);
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 || !serviceProfiles.includes(profileName)) {
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((p) => p !== profileName);
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: string[];
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 };
@@ -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 apiSecret: string = '';
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 authorization header
84
+ * Verify X-API-Key header
85
85
  */
86
86
  function verifyAuth(request: Request): boolean {
87
- if (!apiSecret) return true; // No auth configured
87
+ if (!apiKey) return true; // No auth configured
88
88
 
89
- const authHeader = request.headers.get('Authorization');
90
- if (!authHeader) return false;
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['api'], serviceAdapters: Map<ServiceName, ServiceAdapter>): Server<unknown> {
760
- const port = config?.port ?? DEFAULT_GATEWAY_CONFIG.api.port;
761
- const host = config?.host ?? DEFAULT_GATEWAY_CONFIG.api.host;
762
- apiSecret = config?.secret ?? '';
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
 
@@ -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; secret: string } | null = null;
52
+ let cachedConfig: { url: string; apiKey: string } | null = null;
53
53
 
54
54
  /**
55
- * Get gateway URL and secret from config or environment
55
+ * Get gateway URL and API key from config or environment
56
56
  */
57
- async function getGatewayConnection(): Promise<{ url: string; secret: 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 envSecret = process.env.AGENTIO_GATEWAY_SECRET || await getEnv('AGENTIO_GATEWAY_SECRET');
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, secret: envSecret || '' };
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
- if (!gatewayConfig?.api) {
74
- throw new CliError('CONFIG_ERROR', 'Gateway not configured', 'Set AGENTIO_GATEWAY_URL or configure gateway in config');
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
- const host = gatewayConfig.api.host ?? '127.0.0.1';
78
- const port = gatewayConfig.api.port ?? 7890;
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 secret = gatewayConfig.api.secret ?? '';
83
+ const apiKey = gatewayConfig?.apiKey ?? '';
81
84
 
82
- cachedConfig = { url, secret };
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, secret } = await getGatewayConnection();
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 (secret) {
97
- headers['Authorization'] = `Bearer ${secret}`;
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 AGENTIO_GATEWAY_SECRET');
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 };