get-claudia 1.51.5 → 1.51.7
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/bin/index.js +56 -0
- package/cli/commands/google-auth.js +296 -0
- package/cli/index.js +35 -1
- package/package.json +1 -1
- package/template-v2/CLAUDE.md +2 -0
package/bin/index.js
CHANGED
|
@@ -341,11 +341,67 @@ async function restartOllama() {
|
|
|
341
341
|
return startOllama();
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
// ─── Self-update trampoline ──────────────────────────────────────────────
|
|
345
|
+
// npx aggressively caches packages. If the user runs `npx get-claudia .`
|
|
346
|
+
// and a newer version exists, we re-exec with the latest to avoid stale installs.
|
|
347
|
+
|
|
348
|
+
function isNewerVersion(latest, current) {
|
|
349
|
+
const a = latest.split('.').map(Number);
|
|
350
|
+
const b = current.split('.').map(Number);
|
|
351
|
+
for (let i = 0; i < 3; i++) {
|
|
352
|
+
if ((a[i] || 0) > (b[i] || 0)) return true;
|
|
353
|
+
if ((a[i] || 0) < (b[i] || 0)) return false;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function checkForNewerVersion(currentVersion) {
|
|
359
|
+
// Skip if already re-execing (prevent infinite recursion)
|
|
360
|
+
if (process.env.CLAUDIA_SKIP_UPDATE_CHECK) return null;
|
|
361
|
+
// Skip for --help / --version (no need to update-check)
|
|
362
|
+
if (process.argv.includes('--help') || process.argv.includes('-h') || process.argv.includes('--version')) return null;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const resp = await fetch('https://registry.npmjs.org/get-claudia/latest', {
|
|
366
|
+
signal: AbortSignal.timeout(5000),
|
|
367
|
+
});
|
|
368
|
+
if (!resp.ok) return null;
|
|
369
|
+
const data = await resp.json();
|
|
370
|
+
const latest = data.version;
|
|
371
|
+
if (latest && isNewerVersion(latest, currentVersion)) return latest;
|
|
372
|
+
} catch {
|
|
373
|
+
// Network error or timeout: proceed with current version
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
344
378
|
// ─── Main ───────────────────────────────────────────────────────────────
|
|
345
379
|
|
|
346
380
|
async function main() {
|
|
347
381
|
const version = getVersion();
|
|
348
382
|
|
|
383
|
+
// Self-update trampoline: re-exec with latest if we're stale
|
|
384
|
+
const newerVersion = await checkForNewerVersion(version);
|
|
385
|
+
if (newerVersion) {
|
|
386
|
+
process.stdout.write(`\n ${colors.yellow}→${colors.reset} v${newerVersion} available (running v${version}). Updating...\n\n`);
|
|
387
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
388
|
+
try {
|
|
389
|
+
const child = spawn(npxCmd, ['--yes', `get-claudia@${newerVersion}`, ...process.argv.slice(2)], {
|
|
390
|
+
stdio: 'inherit',
|
|
391
|
+
env: { ...process.env, CLAUDIA_SKIP_UPDATE_CHECK: '1' },
|
|
392
|
+
});
|
|
393
|
+
await new Promise((resolve, reject) => {
|
|
394
|
+
child.on('close', (code) => resolve(code));
|
|
395
|
+
child.on('error', reject);
|
|
396
|
+
}).then((code) => {
|
|
397
|
+
process.exit(code || 0);
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
// Re-exec failed, fall through to current version
|
|
401
|
+
process.stdout.write(` ${colors.dim}Update failed, continuing with v${version}${colors.reset}\n`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
349
405
|
// Print compact banner
|
|
350
406
|
process.stdout.write(getBanner(version));
|
|
351
407
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* claudia gmail status - Check Gmail connection status
|
|
10
10
|
* claudia gmail search - Search emails
|
|
11
11
|
* claudia gmail read - Read a specific email
|
|
12
|
+
* claudia gmail send - Send an email with optional attachments
|
|
13
|
+
* claudia gmail draft - Create a draft email with optional attachments
|
|
12
14
|
* claudia gmail logout - Sign out of Gmail
|
|
13
15
|
* claudia calendar login - Sign in with Google (Calendar only)
|
|
14
16
|
* claudia calendar status - Check Calendar connection status
|
|
@@ -18,9 +20,146 @@
|
|
|
18
20
|
* claudia calendar logout - Sign out of Calendar
|
|
19
21
|
*/
|
|
20
22
|
|
|
23
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
24
|
+
import { basename, extname } from 'node:path';
|
|
25
|
+
import { randomBytes } from 'node:crypto';
|
|
21
26
|
import { authenticate, getAccessToken, isAuthenticated, revokeTokens, authStatus } from '../core/google-oauth.js';
|
|
22
27
|
import { outputJson as output } from '../core/output.js';
|
|
23
28
|
|
|
29
|
+
// ── MIME Helpers (for gmail send & draft) ──
|
|
30
|
+
|
|
31
|
+
const MIME_TYPES = {
|
|
32
|
+
png: 'image/png',
|
|
33
|
+
jpg: 'image/jpeg',
|
|
34
|
+
jpeg: 'image/jpeg',
|
|
35
|
+
gif: 'image/gif',
|
|
36
|
+
webp: 'image/webp',
|
|
37
|
+
svg: 'image/svg+xml',
|
|
38
|
+
pdf: 'application/pdf',
|
|
39
|
+
txt: 'text/plain',
|
|
40
|
+
csv: 'text/csv',
|
|
41
|
+
json: 'application/json',
|
|
42
|
+
xml: 'application/xml',
|
|
43
|
+
html: 'text/html',
|
|
44
|
+
zip: 'application/zip',
|
|
45
|
+
gz: 'application/gzip',
|
|
46
|
+
doc: 'application/msword',
|
|
47
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
48
|
+
xls: 'application/vnd.ms-excel',
|
|
49
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
50
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
51
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
52
|
+
mp3: 'audio/mpeg',
|
|
53
|
+
mp4: 'video/mp4',
|
|
54
|
+
wav: 'audio/wav',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function getMimeType(filePath) {
|
|
58
|
+
const ext = extname(filePath).toLowerCase().replace('.', '');
|
|
59
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate and read attachment files from disk.
|
|
64
|
+
* @param {string[]} filePaths
|
|
65
|
+
* @returns {Array<{path: string, data: Buffer, mimeType: string, filename: string}>}
|
|
66
|
+
*/
|
|
67
|
+
function prepareAttachments(filePaths) {
|
|
68
|
+
const MAX_SIZE = 25 * 1024 * 1024; // 25 MB Gmail limit
|
|
69
|
+
const attachments = [];
|
|
70
|
+
|
|
71
|
+
for (const filePath of filePaths) {
|
|
72
|
+
if (!existsSync(filePath)) {
|
|
73
|
+
throw new Error(`Attachment not found: ${filePath}`);
|
|
74
|
+
}
|
|
75
|
+
const stat = statSync(filePath);
|
|
76
|
+
if (!stat.isFile()) {
|
|
77
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
78
|
+
}
|
|
79
|
+
if (stat.size > MAX_SIZE) {
|
|
80
|
+
throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 25 MB): ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
attachments.push({
|
|
83
|
+
path: filePath,
|
|
84
|
+
data: readFileSync(filePath),
|
|
85
|
+
mimeType: getMimeType(filePath),
|
|
86
|
+
filename: basename(filePath),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return attachments;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build an RFC 2822 MIME message string.
|
|
94
|
+
* @param {Object} opts
|
|
95
|
+
* @param {string[]} opts.to
|
|
96
|
+
* @param {string} opts.subject
|
|
97
|
+
* @param {string} opts.body
|
|
98
|
+
* @param {string[]} [opts.cc]
|
|
99
|
+
* @param {string[]} [opts.bcc]
|
|
100
|
+
* @param {boolean} [opts.html]
|
|
101
|
+
* @param {string} [opts.replyTo] - Message-ID for In-Reply-To/References
|
|
102
|
+
* @param {Array} [opts.attachments] - From prepareAttachments()
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
function buildMimeMessage(opts) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
|
|
108
|
+
// Headers
|
|
109
|
+
lines.push(`To: ${opts.to.join(', ')}`);
|
|
110
|
+
lines.push(`Subject: ${opts.subject}`);
|
|
111
|
+
lines.push('MIME-Version: 1.0');
|
|
112
|
+
if (opts.cc && opts.cc.length) lines.push(`Cc: ${opts.cc.join(', ')}`);
|
|
113
|
+
if (opts.bcc && opts.bcc.length) lines.push(`Bcc: ${opts.bcc.join(', ')}`);
|
|
114
|
+
|
|
115
|
+
if (opts.replyTo) {
|
|
116
|
+
const msgId = opts.replyTo.startsWith('<') ? opts.replyTo : `<${opts.replyTo}>`;
|
|
117
|
+
lines.push(`In-Reply-To: ${msgId}`);
|
|
118
|
+
lines.push(`References: ${msgId}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hasAttachments = opts.attachments && opts.attachments.length > 0;
|
|
122
|
+
const contentType = opts.html ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8';
|
|
123
|
+
|
|
124
|
+
if (!hasAttachments) {
|
|
125
|
+
// Simple single-part message
|
|
126
|
+
lines.push(`Content-Type: ${contentType}`);
|
|
127
|
+
lines.push('Content-Transfer-Encoding: base64');
|
|
128
|
+
lines.push(''); // blank line separating headers from body
|
|
129
|
+
lines.push(Buffer.from(opts.body, 'utf-8').toString('base64'));
|
|
130
|
+
} else {
|
|
131
|
+
// Multipart/mixed
|
|
132
|
+
const boundary = `claudia_${randomBytes(16).toString('hex')}`;
|
|
133
|
+
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
|
|
136
|
+
// Text part
|
|
137
|
+
lines.push(`--${boundary}`);
|
|
138
|
+
lines.push(`Content-Type: ${contentType}`);
|
|
139
|
+
lines.push('Content-Transfer-Encoding: base64');
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(Buffer.from(opts.body, 'utf-8').toString('base64'));
|
|
142
|
+
lines.push('');
|
|
143
|
+
|
|
144
|
+
// Attachment parts
|
|
145
|
+
for (const att of opts.attachments) {
|
|
146
|
+
lines.push(`--${boundary}`);
|
|
147
|
+
lines.push(`Content-Type: ${att.mimeType}; name="${att.filename}"`);
|
|
148
|
+
lines.push(`Content-Disposition: attachment; filename="${att.filename}"`);
|
|
149
|
+
lines.push('Content-Transfer-Encoding: base64');
|
|
150
|
+
lines.push('');
|
|
151
|
+
// Split base64 into 76-char lines per RFC 2045
|
|
152
|
+
const b64 = att.data.toString('base64');
|
|
153
|
+
lines.push((b64.match(/.{1,76}/g) || []).join('\r\n'));
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push(`--${boundary}--`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return lines.join('\r\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
24
163
|
// ── Gmail Commands ──
|
|
25
164
|
|
|
26
165
|
export async function gmailLoginCommand() {
|
|
@@ -146,6 +285,163 @@ export async function gmailReadCommand(messageId) {
|
|
|
146
285
|
});
|
|
147
286
|
}
|
|
148
287
|
|
|
288
|
+
export async function gmailSendCommand(opts) {
|
|
289
|
+
const token = await getAccessToken('gmail');
|
|
290
|
+
if (!token) {
|
|
291
|
+
console.error('Not authenticated. Run: claudia gmail login');
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Validate required fields (belt-and-suspenders; Commander's requiredOption catches most)
|
|
297
|
+
if (!opts.to || opts.to.length === 0) {
|
|
298
|
+
console.error('At least one --to recipient is required.');
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!opts.subject) {
|
|
303
|
+
console.error('--subject is required.');
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!opts.body) {
|
|
308
|
+
console.error('--body is required.');
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Prepare attachments
|
|
314
|
+
let attachments = [];
|
|
315
|
+
if (opts.attach && opts.attach.length > 0) {
|
|
316
|
+
try {
|
|
317
|
+
attachments = prepareAttachments(opts.attach);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(err.message);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Build MIME message and base64url-encode it
|
|
326
|
+
const rawMessage = buildMimeMessage({
|
|
327
|
+
to: opts.to,
|
|
328
|
+
subject: opts.subject,
|
|
329
|
+
body: opts.body,
|
|
330
|
+
cc: opts.cc,
|
|
331
|
+
bcc: opts.bcc,
|
|
332
|
+
html: opts.html,
|
|
333
|
+
replyTo: opts.replyTo,
|
|
334
|
+
attachments,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const encodedMessage = Buffer.from(rawMessage, 'utf-8')
|
|
338
|
+
.toString('base64')
|
|
339
|
+
.replace(/\+/g, '-')
|
|
340
|
+
.replace(/\//g, '_')
|
|
341
|
+
.replace(/=+$/, '');
|
|
342
|
+
|
|
343
|
+
const requestBody = { raw: encodedMessage };
|
|
344
|
+
if (opts.thread) {
|
|
345
|
+
requestBody.threadId = opts.thread;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const resp = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: {
|
|
351
|
+
Authorization: `Bearer ${token}`,
|
|
352
|
+
'Content-Type': 'application/json',
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify(requestBody),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (!resp.ok) {
|
|
358
|
+
const err = await resp.text();
|
|
359
|
+
console.error(`Gmail API error (${resp.status}): ${err}`);
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const data = await resp.json();
|
|
365
|
+
output({
|
|
366
|
+
id: data.id,
|
|
367
|
+
threadId: data.threadId,
|
|
368
|
+
labelIds: data.labelIds || [],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function gmailDraftCommand(opts) {
|
|
373
|
+
const token = await getAccessToken('gmail');
|
|
374
|
+
if (!token) {
|
|
375
|
+
console.error('Not authenticated. Run: claudia gmail login');
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Drafts are more lenient than send: subject and body can be empty
|
|
381
|
+
const to = opts.to || [];
|
|
382
|
+
const subject = opts.subject || '';
|
|
383
|
+
const body = opts.body || '';
|
|
384
|
+
|
|
385
|
+
// Prepare attachments
|
|
386
|
+
let attachments = [];
|
|
387
|
+
if (opts.attach && opts.attach.length > 0) {
|
|
388
|
+
try {
|
|
389
|
+
attachments = prepareAttachments(opts.attach);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(err.message);
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Build MIME message and base64url-encode it
|
|
398
|
+
const rawMessage = buildMimeMessage({
|
|
399
|
+
to,
|
|
400
|
+
subject,
|
|
401
|
+
body,
|
|
402
|
+
cc: opts.cc,
|
|
403
|
+
bcc: opts.bcc,
|
|
404
|
+
html: opts.html,
|
|
405
|
+
replyTo: opts.replyTo,
|
|
406
|
+
attachments,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const encodedMessage = Buffer.from(rawMessage, 'utf-8')
|
|
410
|
+
.toString('base64')
|
|
411
|
+
.replace(/\+/g, '-')
|
|
412
|
+
.replace(/\//g, '_')
|
|
413
|
+
.replace(/=+$/, '');
|
|
414
|
+
|
|
415
|
+
// Gmail drafts API: { message: { raw, threadId? } }
|
|
416
|
+
const message = { raw: encodedMessage };
|
|
417
|
+
if (opts.thread) {
|
|
418
|
+
message.threadId = opts.thread;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const resp = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/drafts', {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: {
|
|
424
|
+
Authorization: `Bearer ${token}`,
|
|
425
|
+
'Content-Type': 'application/json',
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify({ message }),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (!resp.ok) {
|
|
431
|
+
const err = await resp.text();
|
|
432
|
+
console.error(`Gmail API error (${resp.status}): ${err}`);
|
|
433
|
+
process.exitCode = 1;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const data = await resp.json();
|
|
438
|
+
output({
|
|
439
|
+
id: data.id,
|
|
440
|
+
messageId: data.message?.id,
|
|
441
|
+
threadId: data.message?.threadId,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
149
445
|
export async function gmailLogoutCommand() {
|
|
150
446
|
const removed = revokeTokens('gmail');
|
|
151
447
|
if (removed) {
|
package/cli/index.js
CHANGED
|
@@ -522,7 +522,7 @@ program
|
|
|
522
522
|
// ── Gmail subcommand group ──
|
|
523
523
|
const gmail = program
|
|
524
524
|
.command('gmail')
|
|
525
|
-
.description('Gmail integration (login, search, read)');
|
|
525
|
+
.description('Gmail integration (login, search, read, send, draft)');
|
|
526
526
|
|
|
527
527
|
gmail
|
|
528
528
|
.command('login')
|
|
@@ -559,6 +559,40 @@ gmail
|
|
|
559
559
|
await gmailReadCommand(messageId);
|
|
560
560
|
});
|
|
561
561
|
|
|
562
|
+
gmail
|
|
563
|
+
.command('send')
|
|
564
|
+
.description('Send an email (with optional attachments)')
|
|
565
|
+
.requiredOption('--to <email...>', 'Recipient email address(es)')
|
|
566
|
+
.requiredOption('--subject <text>', 'Email subject')
|
|
567
|
+
.requiredOption('--body <text>', 'Email body text')
|
|
568
|
+
.option('--cc <email...>', 'CC recipient(s)')
|
|
569
|
+
.option('--bcc <email...>', 'BCC recipient(s)')
|
|
570
|
+
.option('--attach <filepath...>', 'File(s) to attach')
|
|
571
|
+
.option('--html', 'Treat body as HTML', false)
|
|
572
|
+
.option('--thread <threadId>', 'Thread ID (for replies)')
|
|
573
|
+
.option('--reply-to <messageId>', 'Message-ID for In-Reply-To header')
|
|
574
|
+
.action(async (opts) => {
|
|
575
|
+
const { gmailSendCommand } = await import('./commands/google-auth.js');
|
|
576
|
+
await gmailSendCommand(opts);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
gmail
|
|
580
|
+
.command('draft')
|
|
581
|
+
.description('Create a draft email (with optional attachments)')
|
|
582
|
+
.option('--to <email...>', 'Recipient email address(es)')
|
|
583
|
+
.option('--subject <text>', 'Email subject')
|
|
584
|
+
.option('--body <text>', 'Email body text')
|
|
585
|
+
.option('--cc <email...>', 'CC recipient(s)')
|
|
586
|
+
.option('--bcc <email...>', 'BCC recipient(s)')
|
|
587
|
+
.option('--attach <filepath...>', 'File(s) to attach')
|
|
588
|
+
.option('--html', 'Treat body as HTML', false)
|
|
589
|
+
.option('--thread <threadId>', 'Thread ID (for replies)')
|
|
590
|
+
.option('--reply-to <messageId>', 'Message-ID for In-Reply-To header')
|
|
591
|
+
.action(async (opts) => {
|
|
592
|
+
const { gmailDraftCommand } = await import('./commands/google-auth.js');
|
|
593
|
+
await gmailDraftCommand(opts);
|
|
594
|
+
});
|
|
595
|
+
|
|
562
596
|
gmail
|
|
563
597
|
.command('logout')
|
|
564
598
|
.description('Sign out of Gmail (remove stored tokens)')
|
package/package.json
CHANGED
package/template-v2/CLAUDE.md
CHANGED
|
@@ -328,6 +328,8 @@ I adapt to whatever tools are available. When you ask me to do something that ne
|
|
|
328
328
|
| `claudia gmail status` | Check if Gmail is connected |
|
|
329
329
|
| `claudia gmail search "<query>"` | Search emails (Gmail search syntax) |
|
|
330
330
|
| `claudia gmail read <messageId>` | Read a specific email |
|
|
331
|
+
| `claudia gmail send --to <email> --subject <text> --body <text>` | Send email (supports --attach, --cc, --bcc, --html, --thread, --reply-to) |
|
|
332
|
+
| `claudia gmail draft --to <email> --subject <text> --body <text>` | Create draft in Gmail (same options as send, all optional, supports --attach for images/files) |
|
|
331
333
|
| `claudia gmail logout` | Disconnect Gmail, remove tokens |
|
|
332
334
|
| `claudia calendar login` | Opens browser for Calendar-only OAuth |
|
|
333
335
|
| `claudia calendar status` | Check if Calendar is connected |
|