feat(watcher): add agent Gmail CLI with audit and defaults
Provide list/read/summarize plus reason-gated archive, label, draft, and forward-as-draft, with SQLite audit logging and default-mailbox resolution. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,140 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* List Gmail messages: id, from, subject (metadata only).
|
||||
* Thin wrapper around: npm run gmail -- list …
|
||||
*
|
||||
* Usage:
|
||||
* npm run list-messages -w watcher
|
||||
* npm run list-messages -w watcher -- --query 'newer_than:14d has:attachment filename:eml'
|
||||
* npm run list-messages -w watcher -- --max 100 --json
|
||||
* npm run list-messages --
|
||||
* npm run list-messages -- --query 'newer_than:14d' --max 100
|
||||
* npm run list-messages -- --json
|
||||
*
|
||||
* Options:
|
||||
* --query <q> Gmail search (default: newer_than:7d)
|
||||
* --user <email> Mailbox (default: first user with tokens)
|
||||
* --user <email> Mailbox (default: resolveUser order)
|
||||
* --max <n> Max messages (default 50)
|
||||
* --json Print JSON lines instead of a table
|
||||
*/
|
||||
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from "../src/config.js";
|
||||
import { getUserByEmail, listUsersWithTokens } from "../src/db.js";
|
||||
import { gmailClientForUser } from "../src/google.js";
|
||||
import { header } from "../src/mime.js";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const DEFAULT_QUERY = "newer_than:7d";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {
|
||||
query: null,
|
||||
user: null,
|
||||
max: 50,
|
||||
json: false,
|
||||
help: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--help" || a === "-h") out.help = true;
|
||||
else if (a === "--json") out.json = true;
|
||||
else if (a === "--query") out.query = argv[++i];
|
||||
else if (a === "--user") out.user = argv[++i];
|
||||
else if (a === "--max") out.max = Math.max(1, Number(argv[++i]) || 50);
|
||||
else if (a.startsWith("--query=")) out.query = a.slice("--query=".length);
|
||||
else if (a.startsWith("--user=")) out.user = a.slice("--user=".length);
|
||||
else if (a.startsWith("--max=")) {
|
||||
out.max = Math.max(1, Number(a.slice("--max=".length)) || 50);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function listMessageIds(gmail, query, max) {
|
||||
const ids = [];
|
||||
let pageToken;
|
||||
while (ids.length < max) {
|
||||
const { data } = await gmail.users.messages.list({
|
||||
userId: "me",
|
||||
q: query,
|
||||
maxResults: Math.min(50, max - ids.length),
|
||||
pageToken,
|
||||
});
|
||||
for (const m of data.messages || []) {
|
||||
if (m.id) ids.push(m.id);
|
||||
if (ids.length >= max) break;
|
||||
}
|
||||
pageToken = data.nextPageToken;
|
||||
if (!pageToken) break;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function metaFromPayload(payload) {
|
||||
return {
|
||||
from: header(payload, "From") || "",
|
||||
subject: header(payload, "Subject") || "(no subject)",
|
||||
date: header(payload, "Date") || "",
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
console.log(`Usage: list-messages [--query q] [--user email] [--max n] [--json]
|
||||
Default query: ${DEFAULT_QUERY}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
||||
console.error("Missing GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const users = listUsersWithTokens();
|
||||
if (!users.length) {
|
||||
console.error("No signed-in users with tokens. Sign in via the web UI first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let user = users[0];
|
||||
if (args.user) {
|
||||
const byEmail = getUserByEmail(args.user);
|
||||
const byId = users.find((u) => String(u.id) === String(args.user));
|
||||
user = byEmail || byId || null;
|
||||
if (!user) {
|
||||
console.error(`User not found: ${args.user}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const gmail = await gmailClientForUser(user.id);
|
||||
const query = args.query || DEFAULT_QUERY;
|
||||
const ids = await listMessageIds(gmail, query, args.max);
|
||||
|
||||
if (!args.json) {
|
||||
console.error(`# mailbox=${user.email} query=${JSON.stringify(query)} count=${ids.length}`);
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
const { data } = await gmail.users.messages.get({
|
||||
userId: "me",
|
||||
id,
|
||||
format: "metadata",
|
||||
metadataHeaders: ["From", "Subject", "Date"],
|
||||
});
|
||||
const meta = metaFromPayload(data.payload || {});
|
||||
const row = {
|
||||
id,
|
||||
from: meta.from,
|
||||
subject: meta.subject,
|
||||
date: meta.date,
|
||||
};
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(row));
|
||||
} else {
|
||||
console.log(`${row.id}\t${row.from}\t${row.subject}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const gmailJs = resolve(__dirname, "gmail.js");
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[gmailJs, "list", ...process.argv.slice(2)],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
child.on("exit", (code) => process.exit(code ?? 1));
|
||||
|
||||
Reference in New Issue
Block a user