@plosson/agentio 0.4.4 → 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.
@@ -9,6 +9,7 @@ import { GCalClient } from '../services/gcal/client';
9
9
  import { printGCalCalendarList, printGCalEventList, printGCalEvent, printGCalEventCreated, printGCalEventDeleted, printGCalFreeBusy } from '../utils/output';
10
10
  import { CliError, handleError } from '../utils/errors';
11
11
  import { readStdin } from '../utils/stdin';
12
+ import { enforceWriteAccess } from '../utils/read-only';
12
13
 
13
14
  async function getGCalClient(profileName?: string): Promise<{ client: GCalClient; profile: string }> {
14
15
  const { tokens, profile } = await getValidTokens('gcal', profileName);
@@ -155,7 +156,8 @@ export function registerGCalCommands(program: Command): void {
155
156
  return { method: method as 'email' | 'popup', minutes: parseInt(minutes, 10) };
156
157
  });
157
158
 
158
- const { client } = await getGCalClient(options.profile);
159
+ const { client, profile } = await getGCalClient(options.profile);
160
+ await enforceWriteAccess('gcal', profile, 'create event');
159
161
  const event = await client.createEvent({
160
162
  calendarId: calendarId || 'primary',
161
163
  summary: options.summary,
@@ -210,7 +212,8 @@ export function registerGCalCommands(program: Command): void {
210
212
  throw new CliError('INVALID_PARAMS', 'Cannot use both --attendee and --add-attendee');
211
213
  }
212
214
 
213
- const { client } = await getGCalClient(options.profile);
215
+ const { client, profile } = await getGCalClient(options.profile);
216
+ await enforceWriteAccess('gcal', profile, 'update event');
214
217
  const event = await client.updateEvent({
215
218
  calendarId,
216
219
  eventId,
@@ -243,7 +246,8 @@ export function registerGCalCommands(program: Command): void {
243
246
  .option('--send-updates <mode>', 'Send notifications: all, externalOnly, none', 'all')
244
247
  .action(async (calendarId: string, eventId: string, options) => {
245
248
  try {
246
- const { client } = await getGCalClient(options.profile);
249
+ const { client, profile } = await getGCalClient(options.profile);
250
+ await enforceWriteAccess('gcal', profile, 'delete event');
247
251
  await client.deleteEvent(calendarId, eventId, options.sendUpdates);
248
252
  printGCalEventDeleted(calendarId, eventId);
249
253
  } catch (error) {
@@ -300,7 +304,8 @@ export function registerGCalCommands(program: Command): void {
300
304
  throw new CliError('INVALID_PARAMS', `Invalid status: ${options.status}`, 'Use: accepted, declined, or tentative');
301
305
  }
302
306
 
303
- const { client } = await getGCalClient(options.profile);
307
+ const { client, profile } = await getGCalClient(options.profile);
308
+ await enforceWriteAccess('gcal', profile, 'respond to event');
304
309
  const event = await client.respond({
305
310
  calendarId,
306
311
  eventId,
@@ -353,6 +358,7 @@ export function registerGCalCommands(program: Command): void {
353
358
  .command('add')
354
359
  .description('Add a new Google Calendar profile')
355
360
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
361
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
356
362
  .action(async (options) => {
357
363
  try {
358
364
  console.error('Starting OAuth flow for Google Calendar...\n');
@@ -371,11 +377,14 @@ export function registerGCalCommands(program: Command): void {
371
377
 
372
378
  const profileName = options.profile || email;
373
379
 
374
- await setProfile('gcal', profileName);
380
+ await setProfile('gcal', profileName, { readOnly: options.readOnly });
375
381
  await setCredentials('gcal', profileName, { ...tokens, email });
376
382
 
377
383
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
378
384
  console.log(` Email: ${email}`);
385
+ if (options.readOnly) {
386
+ console.log(` Access: read-only`);
387
+ }
379
388
  } catch (error) {
380
389
  handleError(error);
381
390
  }
@@ -12,6 +12,7 @@ import { CliError, handleError } from '../utils/errors';
12
12
  import { readStdin, prompt } from '../utils/stdin';
13
13
  import { interactiveSelect } from '../utils/interactive';
14
14
  import { printGChatSendResult, printGChatMessageList, printGChatMessage, printGChatSpaceList } from '../utils/output';
15
+ import { enforceWriteAccess } from '../utils/read-only';
15
16
  import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
16
17
 
17
18
  const getGChatClient = createClientGetter<GChatCredentials, GChatClient>({
@@ -95,7 +96,8 @@ export function registerGChatCommands(program: Command): void {
95
96
  }
96
97
  }
97
98
 
98
- const { client } = await getGChatClient(options.profile);
99
+ const { client, profile } = await getGChatClient(options.profile);
100
+ await enforceWriteAccess('gchat', profile, 'send message');
99
101
  const result = await client.send({
100
102
  text,
101
103
  payload,
@@ -185,6 +187,7 @@ export function registerGChatCommands(program: Command): void {
185
187
  .command('add')
186
188
  .description('Add a new Google Chat profile (webhook or OAuth)')
187
189
  .option('--profile <name>', 'Profile name (required for webhook, auto-detected for OAuth)')
190
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
188
191
  .action(async (options) => {
189
192
  try {
190
193
  console.error('\nGoogle Chat Setup\n');
@@ -205,9 +208,9 @@ export function registerGChatCommands(program: Command): void {
205
208
  'Run: agentio gchat profile add --profile <name>'
206
209
  );
207
210
  }
208
- await setupWebhookProfile(options.profile);
211
+ await setupWebhookProfile(options.profile, options.readOnly);
209
212
  } else {
210
- await setupOAuthProfile(options.profile);
213
+ await setupOAuthProfile(options.profile, options.readOnly);
211
214
  }
212
215
  } catch (error) {
213
216
  handleError(error);
@@ -215,13 +218,16 @@ export function registerGChatCommands(program: Command): void {
215
218
  });
216
219
  }
217
220
 
218
- function printProfileSetupSuccess(profileName: string, authType: 'webhook' | 'oauth'): void {
221
+ function printProfileSetupSuccess(profileName: string, authType: 'webhook' | 'oauth', readOnly?: boolean): void {
219
222
  const typeLabel = authType.charAt(0).toUpperCase() + authType.slice(1);
220
223
  console.log(`\nSuccess! ${typeLabel} profile "${profileName}" configured.`);
224
+ if (readOnly) {
225
+ console.log(` Access: read-only`);
226
+ }
221
227
  console.log(` Test with: agentio gchat send --profile ${profileName} "Hello from agentio"`);
222
228
  }
223
229
 
224
- async function setupWebhookProfile(profileName: string): Promise<void> {
230
+ async function setupWebhookProfile(profileName: string, readOnly?: boolean): Promise<void> {
225
231
  console.error('Webhook Setup\n');
226
232
  console.error('1. In Google Chat, find or create a space');
227
233
  console.error('2. Go to Space Settings → Webhooks');
@@ -264,13 +270,13 @@ async function setupWebhookProfile(profileName: string): Promise<void> {
264
270
  webhookUrl: webhookUrl,
265
271
  };
266
272
 
267
- await setProfile('gchat', profileName);
273
+ await setProfile('gchat', profileName, { readOnly });
268
274
  await setCredentials('gchat', profileName, credentials);
269
275
 
270
- printProfileSetupSuccess(profileName, 'webhook');
276
+ printProfileSetupSuccess(profileName, 'webhook', readOnly);
271
277
  }
272
278
 
273
- async function setupOAuthProfile(profileNameOverride?: string): Promise<void> {
279
+ async function setupOAuthProfile(profileNameOverride?: string, readOnly?: boolean): Promise<void> {
274
280
  console.error('OAuth Setup\n');
275
281
  console.error('Starting OAuth flow for Google Chat profile...\n');
276
282
 
@@ -320,8 +326,8 @@ async function setupOAuthProfile(profileNameOverride?: string): Promise<void> {
320
326
  email: userEmail,
321
327
  };
322
328
 
323
- await setProfile('gchat', profileName);
329
+ await setProfile('gchat', profileName, { readOnly });
324
330
  await setCredentials('gchat', profileName, credentials);
325
331
 
326
- printProfileSetupSuccess(profileName, 'oauth');
332
+ printProfileSetupSuccess(profileName, 'oauth', readOnly);
327
333
  }
@@ -11,6 +11,7 @@ import { GDocsClient } from '../services/gdocs/client';
11
11
  import { printGDocsList, printGDocCreated, raw } from '../utils/output';
12
12
  import { CliError, handleError } from '../utils/errors';
13
13
  import { readStdin } from '../utils/stdin';
14
+ import { enforceWriteAccess } from '../utils/read-only';
14
15
  import type { GDocsCredentials } from '../types/gdocs';
15
16
 
16
17
  const getGDocsClient = createClientGetter<GDocsCredentials, GDocsClient>({
@@ -77,7 +78,8 @@ export function registerGDocsCommands(program: Command): void {
77
78
  throw new CliError('INVALID_PARAMS', 'No content provided', 'Provide --content or pipe markdown via stdin');
78
79
  }
79
80
 
80
- const { client } = await getGDocsClient(options.profile);
81
+ const { client, profile } = await getGDocsClient(options.profile);
82
+ await enforceWriteAccess('gdocs', profile, 'create document');
81
83
  const result = await client.create(options.title, content, options.folder);
82
84
 
83
85
  printGDocCreated(result);
@@ -138,6 +140,7 @@ Query Syntax Examples:
138
140
  .command('add')
139
141
  .description('Add a new Google Docs profile')
140
142
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
143
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
141
144
  .action(async (options) => {
142
145
  try {
143
146
  console.error('Starting OAuth flow for Google Docs...\n');
@@ -174,11 +177,14 @@ Query Syntax Examples:
174
177
  email: userEmail,
175
178
  };
176
179
 
177
- await setProfile('gdocs', profileName);
180
+ await setProfile('gdocs', profileName, { readOnly: options.readOnly });
178
181
  await setCredentials('gdocs', profileName, credentials);
179
182
 
180
183
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
181
184
  console.log(` Email: ${userEmail}`);
185
+ if (options.readOnly) {
186
+ console.log(` Access: read-only`);
187
+ }
182
188
  console.log(` Test with: agentio gdocs list --profile ${profileName}`);
183
189
  } catch (error) {
184
190
  handleError(error);
@@ -10,6 +10,7 @@ import { GDriveClient } from '../services/gdrive/client';
10
10
  import { printGDriveFileList, printGDriveFile, printGDriveDownloaded, printGDriveUploaded } from '../utils/output';
11
11
  import { CliError, handleError } from '../utils/errors';
12
12
  import { prompt } from '../utils/stdin';
13
+ import { enforceWriteAccess } from '../utils/read-only';
13
14
  import type { GDriveCredentials, GDriveAccessLevel } from '../types/gdrive';
14
15
 
15
16
  const getGDriveClient = createClientGetter<GDriveCredentials, GDriveClient>({
@@ -189,7 +190,8 @@ Examples:
189
190
  `)
190
191
  .action(async (filePath: string, options) => {
191
192
  try {
192
- const { client } = await getGDriveClient(options.profile);
193
+ const { client, profile } = await getGDriveClient(options.profile);
194
+ await enforceWriteAccess('gdrive', profile, 'upload file');
193
195
  const result = await client.upload({
194
196
  filePath,
195
197
  name: options.name,
@@ -220,6 +222,7 @@ Examples:
220
222
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
221
223
  .option('--readonly', 'Create a read-only profile (skip access level prompt)')
222
224
  .option('--full', 'Create a full access profile (skip access level prompt)')
225
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
223
226
  .action(async (options) => {
224
227
  try {
225
228
  console.error('Google Drive Setup\n');
@@ -274,12 +277,15 @@ Examples:
274
277
  accessLevel,
275
278
  };
276
279
 
277
- await setProfile('gdrive', profileName);
280
+ await setProfile('gdrive', profileName, { readOnly: options.readOnly });
278
281
  await setCredentials('gdrive', profileName, credentials);
279
282
 
280
283
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
281
284
  console.log(` Email: ${userEmail}`);
282
- console.log(` Access: ${accessLevel === 'full' ? 'Full (read & write)' : 'Read-only'}`);
285
+ console.log(` API Access: ${accessLevel === 'full' ? 'Full (read & write)' : 'Read-only'}`);
286
+ if (options.readOnly) {
287
+ console.log(` Profile Access: read-only`);
288
+ }
283
289
  console.log(` Test with: agentio gdrive list --profile ${profileName}`);
284
290
  } catch (error) {
285
291
  handleError(error);
@@ -7,6 +7,7 @@ import { GitHubClient } from '../services/github/client';
7
7
  import { performGitHubOAuthFlow } from '../auth/github-oauth';
8
8
  import { generateExportData } from './config';
9
9
  import { CliError, handleError } from '../utils/errors';
10
+ import { enforceWriteAccess } from '../utils/read-only';
10
11
  import type { GitHubCredentials } from '../types/github';
11
12
 
12
13
  const getGitHubClient = createClientGetter<GitHubCredentials, GitHubClient>({
@@ -42,6 +43,7 @@ export function registerGitHubCommands(program: Command): void {
42
43
  parseRepo(repo);
43
44
 
44
45
  const { client, profile } = await getGitHubClient(options.profile);
46
+ await enforceWriteAccess('github', profile, 'install secrets');
45
47
 
46
48
  console.error(`Using GitHub profile: ${profile}`);
47
49
  console.error(`Installing secrets to: ${repo}`);
@@ -77,6 +79,7 @@ export function registerGitHubCommands(program: Command): void {
77
79
  parseRepo(repo);
78
80
 
79
81
  const { client, profile } = await getGitHubClient(options.profile);
82
+ await enforceWriteAccess('github', profile, 'uninstall secrets');
80
83
 
81
84
  console.error(`Using GitHub profile: ${profile}`);
82
85
  console.error(`Removing secrets from: ${repo}`);
@@ -105,6 +108,7 @@ export function registerGitHubCommands(program: Command): void {
105
108
  .command('add')
106
109
  .description('Add a new GitHub profile')
107
110
  .option('--profile <name>', 'Profile name (auto-detected from username if not provided)')
111
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
108
112
  .action(async (options) => {
109
113
  try {
110
114
  console.error('\nGitHub Setup\n');
@@ -133,10 +137,13 @@ export function registerGitHubCommands(program: Command): void {
133
137
  console.error(`\nAuthenticated as: ${user.login}${user.email ? ` (${user.email})` : ''}`);
134
138
 
135
139
  // Save credentials
136
- await setProfile('github', profileName);
140
+ await setProfile('github', profileName, { readOnly: options.readOnly });
137
141
  await setCredentials('github', profileName, credentials);
138
142
 
139
143
  console.log(`\nProfile "${profileName}" configured!`);
144
+ if (options.readOnly) {
145
+ console.log(` Access: read-only`);
146
+ }
140
147
  console.log(` Install secrets: agentio github install owner/repo --profile ${profileName}`);
141
148
  } catch (error) {
142
149
  handleError(error);
@@ -11,6 +11,7 @@ import { GmailClient } from '../services/gmail/client';
11
11
  import { printMessageList, printMessage, printSendResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
12
12
  import { CliError, handleError } from '../utils/errors';
13
13
  import { readStdin } from '../utils/stdin';
14
+ import { enforceWriteAccess } from '../utils/read-only';
14
15
  import type { GmailAttachment } from '../types/gmail';
15
16
 
16
17
  function escapeHtml(text: string): string {
@@ -228,7 +229,8 @@ Query Syntax Examples:
228
229
  ? [...regularAttachments, ...inlineAttachments]
229
230
  : undefined;
230
231
 
231
- const { client } = await getGmailClient(options.profile);
232
+ const { client, profile } = await getGmailClient(options.profile);
233
+ await enforceWriteAccess('gmail', profile, 'send email');
232
234
  const result = await client.send({
233
235
  to: options.to,
234
236
  cc: options.cc.length ? options.cc : undefined,
@@ -263,7 +265,8 @@ Query Syntax Examples:
263
265
  throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
264
266
  }
265
267
 
266
- const { client } = await getGmailClient(options.profile);
268
+ const { client, profile } = await getGmailClient(options.profile);
269
+ await enforceWriteAccess('gmail', profile, 'reply to email');
267
270
  const result = await client.reply({
268
271
  threadId: options.threadId,
269
272
  body,
@@ -282,7 +285,8 @@ Query Syntax Examples:
282
285
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
283
286
  .action(async (messageIds: string[], options) => {
284
287
  try {
285
- const { client } = await getGmailClient(options.profile);
288
+ const { client, profile } = await getGmailClient(options.profile);
289
+ await enforceWriteAccess('gmail', profile, 'archive email');
286
290
  for (const messageId of messageIds) {
287
291
  await client.archive(messageId);
288
292
  printArchived(messageId);
@@ -308,7 +312,8 @@ Query Syntax Examples:
308
312
  throw new CliError('INVALID_PARAMS', 'Cannot specify both --read and --unread');
309
313
  }
310
314
 
311
- const { client } = await getGmailClient(options.profile);
315
+ const { client, profile } = await getGmailClient(options.profile);
316
+ await enforceWriteAccess('gmail', profile, 'mark email');
312
317
  for (const messageId of messageIds) {
313
318
  await client.mark(messageId, options.read);
314
319
  printMarked(messageId, options.read);
@@ -458,6 +463,7 @@ ${emailHeader}
458
463
  .command('add')
459
464
  .description('Add a new Gmail profile')
460
465
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
466
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
461
467
  .action(async (options) => {
462
468
  try {
463
469
  console.error('Starting OAuth flow for Gmail...\n');
@@ -476,11 +482,14 @@ ${emailHeader}
476
482
 
477
483
  const profileName = options.profile || email;
478
484
 
479
- await setProfile('gmail', profileName);
485
+ await setProfile('gmail', profileName, { readOnly: options.readOnly });
480
486
  await setCredentials('gmail', profileName, { ...tokens, email });
481
487
 
482
488
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
483
489
  console.log(` Email: ${email}`);
490
+ if (options.readOnly) {
491
+ console.log(` Access: read-only`);
492
+ }
484
493
  } catch (error) {
485
494
  handleError(error);
486
495
  }
@@ -18,6 +18,7 @@ import {
18
18
  printGSheetsCreated,
19
19
  } from '../utils/output';
20
20
  import { CliError, handleError } from '../utils/errors';
21
+ import { enforceWriteAccess } from '../utils/read-only';
21
22
  import type { GSheetsCredentials } from '../types/gsheets';
22
23
 
23
24
  const getGSheetsClient = createClientGetter<GSheetsCredentials, GSheetsClient>({
@@ -152,7 +153,8 @@ Input Options:
152
153
  .action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
153
154
  try {
154
155
  const values = parseValues(valueArgs, options.valuesJson);
155
- const { client } = await getGSheetsClient(options.profile);
156
+ const { client, profile } = await getGSheetsClient(options.profile);
157
+ await enforceWriteAccess('gsheets', profile, 'update values');
156
158
  const result = await client.update(spreadsheetId, range, values, {
157
159
  valueInputOption: options.input,
158
160
  });
@@ -192,7 +194,8 @@ Insert Options:
192
194
  .action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
193
195
  try {
194
196
  const values = parseValues(valueArgs, options.valuesJson);
195
- const { client } = await getGSheetsClient(options.profile);
197
+ const { client, profile } = await getGSheetsClient(options.profile);
198
+ await enforceWriteAccess('gsheets', profile, 'append values');
196
199
  const result = await client.append(spreadsheetId, range, values, {
197
200
  valueInputOption: options.input,
198
201
  insertDataOption: options.insert,
@@ -211,7 +214,8 @@ Insert Options:
211
214
  .option('--profile <name>', 'Profile name')
212
215
  .action(async (spreadsheetId: string, range: string, options) => {
213
216
  try {
214
- const { client } = await getGSheetsClient(options.profile);
217
+ const { client, profile } = await getGSheetsClient(options.profile);
218
+ await enforceWriteAccess('gsheets', profile, 'clear values');
215
219
  const result = await client.clear(spreadsheetId, range);
216
220
  printGSheetsClearResult(result);
217
221
  } catch (error) {
@@ -242,7 +246,8 @@ Insert Options:
242
246
  .option('--sheets <names>', 'Comma-separated sheet names to create')
243
247
  .action(async (title: string, options) => {
244
248
  try {
245
- const { client } = await getGSheetsClient(options.profile);
249
+ const { client, profile } = await getGSheetsClient(options.profile);
250
+ await enforceWriteAccess('gsheets', profile, 'create spreadsheet');
246
251
  const sheetNames = options.sheets ? options.sheets.split(',').map((n: string) => n.trim()) : undefined;
247
252
  const result = await client.create(title, sheetNames);
248
253
  printGSheetsCreated(result);
@@ -260,7 +265,8 @@ Insert Options:
260
265
  .option('--parent <folder-id>', 'Destination folder ID')
261
266
  .action(async (spreadsheetId: string, title: string, options) => {
262
267
  try {
263
- const { client } = await getGSheetsClient(options.profile);
268
+ const { client, profile } = await getGSheetsClient(options.profile);
269
+ await enforceWriteAccess('gsheets', profile, 'copy spreadsheet');
264
270
  const result = await client.copy(spreadsheetId, title, options.parent);
265
271
  printGSheetsCreated(result);
266
272
  } catch (error) {
@@ -320,6 +326,7 @@ Examples:
320
326
  .command('add')
321
327
  .description('Add a new Google Sheets profile')
322
328
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
329
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
323
330
  .action(async (options) => {
324
331
  try {
325
332
  console.error('Starting OAuth flow for Google Sheets...\n');
@@ -352,11 +359,14 @@ Examples:
352
359
  email: userEmail,
353
360
  };
354
361
 
355
- await setProfile('gsheets', profileName);
362
+ await setProfile('gsheets', profileName, { readOnly: options.readOnly });
356
363
  await setCredentials('gsheets', profileName, credentials);
357
364
 
358
365
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
359
366
  console.log(` Email: ${userEmail}`);
367
+ if (options.readOnly) {
368
+ console.log(` Access: read-only`);
369
+ }
360
370
  console.log(` Test with: agentio gsheets list --profile ${profileName}`);
361
371
  } catch (error) {
362
372
  handleError(error);
@@ -19,6 +19,7 @@ import {
19
19
  } from '../utils/output';
20
20
  import { CliError, handleError } from '../utils/errors';
21
21
  import { readStdin } from '../utils/stdin';
22
+ import { enforceWriteAccess } from '../utils/read-only';
22
23
 
23
24
  async function getGTasksClient(profileName?: string): Promise<{ client: GTasksClient; profile: string }> {
24
25
  const { tokens, profile } = await getValidTokens('gtasks', profileName);
@@ -60,7 +61,8 @@ export function registerGTasksCommands(program: Command): void {
60
61
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
61
62
  .action(async (title: string, options) => {
62
63
  try {
63
- const { client } = await getGTasksClient(options.profile);
64
+ const { client, profile } = await getGTasksClient(options.profile);
65
+ await enforceWriteAccess('gtasks', profile, 'create task list');
64
66
  const taskList = await client.createTaskList(title);
65
67
  printGTaskListCreated(taskList);
66
68
  } catch (error) {
@@ -75,7 +77,8 @@ export function registerGTasksCommands(program: Command): void {
75
77
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
76
78
  .action(async (tasklistId: string, options) => {
77
79
  try {
78
- const { client } = await getGTasksClient(options.profile);
80
+ const { client, profile } = await getGTasksClient(options.profile);
81
+ await enforceWriteAccess('gtasks', profile, 'delete task list');
79
82
  await client.deleteTaskList(tasklistId);
80
83
  printGTaskListDeleted(tasklistId);
81
84
  } catch (error) {
@@ -146,7 +149,8 @@ export function registerGTasksCommands(program: Command): void {
146
149
  if (stdin) notes = stdin;
147
150
  }
148
151
 
149
- const { client } = await getGTasksClient(options.profile);
152
+ const { client, profile } = await getGTasksClient(options.profile);
153
+ await enforceWriteAccess('gtasks', profile, 'create task');
150
154
  const task = await client.createTask({
151
155
  tasklistId,
152
156
  title: options.title,
@@ -182,7 +186,8 @@ export function registerGTasksCommands(program: Command): void {
182
186
  throw new CliError('INVALID_PARAMS', `Invalid status: ${options.status}`, 'Use: needsAction or completed');
183
187
  }
184
188
 
185
- const { client } = await getGTasksClient(options.profile);
189
+ const { client, profile } = await getGTasksClient(options.profile);
190
+ await enforceWriteAccess('gtasks', profile, 'update task');
186
191
  const task = await client.updateTask({
187
192
  tasklistId,
188
193
  taskId,
@@ -205,7 +210,8 @@ export function registerGTasksCommands(program: Command): void {
205
210
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
206
211
  .action(async (tasklistId: string, taskId: string, options) => {
207
212
  try {
208
- const { client } = await getGTasksClient(options.profile);
213
+ const { client, profile } = await getGTasksClient(options.profile);
214
+ await enforceWriteAccess('gtasks', profile, 'complete task');
209
215
  const task = await client.completeTask(tasklistId, taskId);
210
216
  console.log(`Task completed: ${task.title}`);
211
217
  console.log(`ID: ${task.id}`);
@@ -223,7 +229,8 @@ export function registerGTasksCommands(program: Command): void {
223
229
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
224
230
  .action(async (tasklistId: string, taskId: string, options) => {
225
231
  try {
226
- const { client } = await getGTasksClient(options.profile);
232
+ const { client, profile } = await getGTasksClient(options.profile);
233
+ await enforceWriteAccess('gtasks', profile, 'uncomplete task');
227
234
  const task = await client.uncompleteTask(tasklistId, taskId);
228
235
  console.log(`Task uncompleted: ${task.title}`);
229
236
  console.log(`ID: ${task.id}`);
@@ -240,7 +247,8 @@ export function registerGTasksCommands(program: Command): void {
240
247
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
241
248
  .action(async (tasklistId: string, taskId: string, options) => {
242
249
  try {
243
- const { client } = await getGTasksClient(options.profile);
250
+ const { client, profile } = await getGTasksClient(options.profile);
251
+ await enforceWriteAccess('gtasks', profile, 'delete task');
244
252
  await client.deleteTask(tasklistId, taskId);
245
253
  printGTaskDeleted(tasklistId, taskId);
246
254
  } catch (error) {
@@ -255,7 +263,8 @@ export function registerGTasksCommands(program: Command): void {
255
263
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
256
264
  .action(async (tasklistId: string, options) => {
257
265
  try {
258
- const { client } = await getGTasksClient(options.profile);
266
+ const { client, profile } = await getGTasksClient(options.profile);
267
+ await enforceWriteAccess('gtasks', profile, 'clear tasks');
259
268
  await client.clearCompleted(tasklistId);
260
269
  printGTasksCleared(tasklistId);
261
270
  } catch (error) {
@@ -275,7 +284,8 @@ export function registerGTasksCommands(program: Command): void {
275
284
  if (!options.parent && !options.previous) {
276
285
  throw new CliError('INVALID_PARAMS', 'At least one of --parent or --previous is required');
277
286
  }
278
- const { client } = await getGTasksClient(options.profile);
287
+ const { client, profile } = await getGTasksClient(options.profile);
288
+ await enforceWriteAccess('gtasks', profile, 'move task');
279
289
  const task = await client.moveTask(tasklistId, taskId, options.parent, options.previous);
280
290
  console.log(`Task moved: ${task.title}`);
281
291
  console.log(`ID: ${task.id}`);
@@ -296,6 +306,7 @@ export function registerGTasksCommands(program: Command): void {
296
306
  .command('add')
297
307
  .description('Add a new Google Tasks profile')
298
308
  .option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
309
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
299
310
  .action(async (options) => {
300
311
  try {
301
312
  console.error('Starting OAuth flow for Google Tasks...\n');
@@ -314,11 +325,14 @@ export function registerGTasksCommands(program: Command): void {
314
325
 
315
326
  const profileName = options.profile || email;
316
327
 
317
- await setProfile('gtasks', profileName);
328
+ await setProfile('gtasks', profileName, { readOnly: options.readOnly });
318
329
  await setCredentials('gtasks', profileName, { ...tokens, email });
319
330
 
320
331
  console.log(`\nSuccess! Profile "${profileName}" configured.`);
321
332
  console.log(` Email: ${email}`);
333
+ if (options.readOnly) {
334
+ console.log(` Access: read-only`);
335
+ }
322
336
  } catch (error) {
323
337
  handleError(error);
324
338
  }
@@ -7,6 +7,7 @@ import { JiraClient } from '../services/jira/client';
7
7
  import { CliError, handleError } from '../utils/errors';
8
8
  import { readStdin } from '../utils/stdin';
9
9
  import { interactiveSelect } from '../utils/interactive';
10
+ import { enforceWriteAccess } from '../utils/read-only';
10
11
  import {
11
12
  printJiraProjectList,
12
13
  printJiraIssueList,
@@ -163,7 +164,8 @@ export function registerJiraCommands(program: Command): void {
163
164
  throw new CliError('INVALID_PARAMS', 'Comment body is required. Provide as argument or pipe via stdin.');
164
165
  }
165
166
 
166
- const { client } = await getJiraClient(options.profile);
167
+ const { client, profile } = await getJiraClient(options.profile);
168
+ await enforceWriteAccess('jira', profile, 'add comment');
167
169
  const result = await client.addComment(issueKey, text);
168
170
  printJiraCommentResult(result);
169
171
  } catch (error) {
@@ -196,7 +198,8 @@ export function registerJiraCommands(program: Command): void {
196
198
  .option('--profile <name>', 'Profile name (optional if only one profile exists)')
197
199
  .action(async (issueKey: string, transitionId: string, options) => {
198
200
  try {
199
- const { client } = await getJiraClient(options.profile);
201
+ const { client, profile } = await getJiraClient(options.profile);
202
+ await enforceWriteAccess('jira', profile, 'transition issue');
200
203
  const result = await client.transitionIssue(issueKey, transitionId);
201
204
  printJiraTransitionResult(result);
202
205
  } catch (error) {
@@ -215,6 +218,7 @@ export function registerJiraCommands(program: Command): void {
215
218
  .command('add')
216
219
  .description('Add a new JIRA profile with OAuth authentication')
217
220
  .option('--profile <name>', 'Profile name (auto-detected from site URL if not provided)')
221
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
218
222
  .action(async (options) => {
219
223
  try {
220
224
  console.error('\nJIRA OAuth Setup\n');
@@ -248,10 +252,13 @@ export function registerJiraCommands(program: Command): void {
248
252
  siteUrl: result.siteUrl,
249
253
  };
250
254
 
251
- await setProfile('jira', profileName);
255
+ await setProfile('jira', profileName, { readOnly: options.readOnly });
252
256
  await setCredentials('jira', profileName, credentials);
253
257
 
254
258
  console.log(`\nProfile "${profileName}" configured!`);
259
+ if (options.readOnly) {
260
+ console.log(` Access: read-only`);
261
+ }
255
262
  console.log(` Test with: agentio jira projects --profile ${profileName}`);
256
263
  } catch (error) {
257
264
  handleError(error);
@@ -8,6 +8,7 @@ import { SlackClient } from '../services/slack/client';
8
8
  import { CliError, handleError } from '../utils/errors';
9
9
  import { readStdin, prompt } from '../utils/stdin';
10
10
  import { printSlackSendResult } from '../utils/output';
11
+ import { enforceWriteAccess } from '../utils/read-only';
11
12
  import type { SlackCredentials, SlackWebhookCredentials } from '../types/slack';
12
13
 
13
14
  const getSlackClient = createClientGetter<SlackCredentials, SlackClient>({
@@ -89,7 +90,8 @@ export function registerSlackCommands(program: Command): void {
89
90
  }
90
91
  }
91
92
 
92
- const { client } = await getSlackClient(options.profile);
93
+ const { client, profile } = await getSlackClient(options.profile);
94
+ await enforceWriteAccess('slack', profile, 'send message');
93
95
  const result = await client.send({
94
96
  text,
95
97
  payload,
@@ -112,16 +114,17 @@ export function registerSlackCommands(program: Command): void {
112
114
  .command('add')
113
115
  .description('Add a new Slack profile (webhook)')
114
116
  .requiredOption('--profile <name>', 'Profile name (required)')
117
+ .option('--read-only', 'Create as read-only profile (blocks write operations)')
115
118
  .action(async (options) => {
116
119
  try {
117
- await setupWebhookProfile(options.profile);
120
+ await setupWebhookProfile(options.profile, options.readOnly);
118
121
  } catch (error) {
119
122
  handleError(error);
120
123
  }
121
124
  });
122
125
  }
123
126
 
124
- async function setupWebhookProfile(profileName: string): Promise<void> {
127
+ async function setupWebhookProfile(profileName: string, readOnly?: boolean): Promise<void> {
125
128
  console.error('\nSlack Webhook Setup\n');
126
129
  console.error('1. Go to https://api.slack.com/apps and create a new app (or use existing)');
127
130
  console.error('2. Enable "Incoming Webhooks" in Features');
@@ -177,9 +180,12 @@ async function setupWebhookProfile(profileName: string): Promise<void> {
177
180
  channelName: channelName || undefined,
178
181
  };
179
182
 
180
- await setProfile('slack', profileName);
183
+ await setProfile('slack', profileName, { readOnly });
181
184
  await setCredentials('slack', profileName, credentials);
182
185
 
183
186
  console.log(`\nSuccess! Webhook profile "${profileName}" configured.`);
187
+ if (readOnly) {
188
+ console.log(` Access: read-only`);
189
+ }
184
190
  console.log(` Test with: agentio slack send --profile ${profileName} "Hello from agentio"`);
185
191
  }