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
+95
View File
@@ -0,0 +1,95 @@
/**
* Minimal shared argv parser for gmail CLI commands.
* Supports --flag value, --flag=value, and boolean --flags.
*/
export function parseArgv(argv, { booleans = [], strings = [], multi = [] } = {}) {
const out = {
_: [],
help: false,
};
for (const key of booleans) out[key] = false;
for (const key of strings) out[key] = null;
for (const key of multi) out[key] = [];
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--help" || a === "-h") {
out.help = true;
continue;
}
if (!a.startsWith("--")) {
out._.push(a);
continue;
}
const eq = a.indexOf("=");
let name;
let rawValue;
if (eq >= 0) {
name = a.slice(2, eq);
rawValue = a.slice(eq + 1);
} else {
name = a.slice(2);
rawValue = undefined;
}
const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (booleans.includes(camel) || booleans.includes(name)) {
const key = booleans.includes(camel) ? camel : name;
out[key] = true;
continue;
}
if (multi.includes(camel) || multi.includes(name)) {
const key = multi.includes(camel) ? camel : name;
const value = rawValue !== undefined ? rawValue : argv[++i];
if (value == null) continue;
out[key].push(
...String(value)
.split(",")
.map((s) => s.trim())
.filter(Boolean),
);
continue;
}
if (strings.includes(camel) || strings.includes(name)) {
const key = strings.includes(camel) ? camel : name;
out[key] = rawValue !== undefined ? rawValue : argv[++i];
continue;
}
// Unknown flag: treat as string if next token looks like a value
if (rawValue !== undefined) {
out[camel] = rawValue;
} else if (argv[i + 1] && !argv[i + 1].startsWith("--")) {
out[camel] = argv[++i];
} else {
out[camel] = true;
}
}
return out;
}
export function requireReason(args, action) {
const reason = (args.reason || "").trim();
if (!reason) {
return {
ok: false,
error: `--reason is required for ${action}`,
};
}
return { ok: true, reason };
}
export function parseIds(args) {
const fromFlag = Array.isArray(args.ids) ? args.ids : [];
const fromPositional = args._ || [];
const joined = [...fromFlag, ...fromPositional]
.flatMap((s) => String(s).split(","))
.map((s) => s.trim())
.filter(Boolean);
return [...new Set(joined)];
}
+74
View File
@@ -0,0 +1,74 @@
import { GMAIL_DEFAULT_USER } from "../config.js";
import {
getDefaultUser,
getUserByEmail,
getUserById,
listUsersWithTokens,
} from "../db.js";
/**
* Resolve which connected mailbox a CLI command should use.
*
* Order: --user → GMAIL_DEFAULT_USER env → users.is_default → sole connected user.
*
* @param {string|null|undefined} userFlag
* @param {object} [deps] injectable for tests
* @returns {{ ok: true, user: object } | { ok: false, error: string }}
*/
export function resolveUser(userFlag, deps = {}) {
const listUsers = deps.listUsers || listUsersWithTokens;
const getByEmail = deps.getByEmail || getUserByEmail;
const getById = deps.getById || getUserById;
const getDefault = deps.getDefault || getDefaultUser;
const envDefault =
deps.envDefault !== undefined ? deps.envDefault : GMAIL_DEFAULT_USER;
const users = listUsers();
if (!users.length) {
return {
ok: false,
error:
"No signed-in users with tokens. Sign in via the web UI first.",
};
}
const hasTokens = (user) => user && users.some((u) => u.id === user.id);
if (userFlag) {
const byEmail = getByEmail(userFlag);
const byId =
users.find((u) => String(u.id) === String(userFlag)) ||
getById(userFlag);
const user = byEmail || byId || null;
if (!user || !hasTokens(user)) {
return { ok: false, error: `User not found: ${userFlag}` };
}
return { ok: true, user };
}
if (envDefault) {
const fromEnv = getByEmail(envDefault);
if (!fromEnv || !hasTokens(fromEnv)) {
return {
ok: false,
error: `GMAIL_DEFAULT_USER not found or has no tokens: ${envDefault}`,
};
}
return { ok: true, user: fromEnv };
}
const flagged = getDefault();
if (flagged && hasTokens(flagged)) {
return { ok: true, user: flagged };
}
if (users.length === 1) {
return { ok: true, user: users[0] };
}
return {
ok: false,
error:
'Multiple accounts connected; set a default: npm run gmail -- accounts --default you@gmail.com',
};
}