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:
2026-09-22 10:09:25 +07:00
co-authored by Cursor
parent 770ed8121a
commit f270f531ea
13 changed files with 1643 additions and 145 deletions
+16 -127
View File
@@ -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));