Agentero
에이전트를 연결해 로컬 Vault에 연구 컨텍스트를 보관·Zotero에서 무결점 이동·읽고 이해·재활용
구글 계정에 탑재된 개인 AI 에이전트로, 중요한 메일에 별표 표시, 스팸 삭제, 구독 해지 수행
A personal AI agent that lives inside your own Google account. Stars important emails, deletes spam, and unsubscribes you from forgotten newsletters.
생산성TypeScript · CSSGitHub @johntperales10-inboxai온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Build me a clean, modern 3-stage web app called "InboxAI" — a Gmail AI Agent setup wizard. The app lives on one page and walks users through 3 stages. Only one stage is visible at a time.
=== BRAND & STYLE ===
=== STAGE 1 — LANDING / HERO === Full screen hero section with:
=== STAGE 2 — SETTINGS FORM === Header: "Tell us about your inbox" with subtext "Takes 2 minutes. We'll build your custom script with your settings already filled in."
Back button at top left that goes back to Stage 1.
Form fields:
Big button at bottom: "Build my custom script →" — clicking it generates the script and goes to Stage 3.
=== STAGE 3 — GENERATED SCRIPT + INSTALL STEPS === Back button at top left that goes back to Stage 2.
Header: "Your script is ready" Subtext: "Follow the 4 steps below. Takes about 5 minutes. You'll never need to touch code again."
Green success banner: "✓ Your settings have been baked into the script below."
SCRIPT OUTPUT BOX:
INSTALL STEPS — numbered list of cards: Step 1: "Go to script.google.com and sign in with the same Google account as your Gmail. Click New project." Step 2: "Delete everything in the editor. Paste your script above using Ctrl+V (Cmd+V on Mac). Save with Ctrl+S." Step 3: "Click the dropdown at the top of the editor — it says myFunction by default. Change it to testGemini. Press ▶ Run. Accept any permissions Google asks for." Step 4: "Once that works, change the dropdown to setupTrigger and press ▶ Run. Your agent is now live and runs every hour automatically." Step 5: "To stop the agent any time, run stopAgent from the same dropdown. To fully remove access go to myaccount.google.com/permissions."
=== THE SCRIPT TEMPLATE === When the user clicks "Build my custom script", generate this script with their values filled in. Replace all placeholder values with what the user entered in the form:
// ============================================================ // GMAIL AI AGENT — your custom build // Paste into script.google.com → New project → Run setupTrigger // ============================================================
const SETTINGS = { geminiApiKey: "PASTE_YOUR_GEMINI_API_KEY_HERE", trustedSenders: [ [LIST OF EMAILS FROM FORM, each on its own line in quotes] ], deleteSpam: [TRUE OR FALSE FROM TOGGLE], unsubscribeAfterDays: [NUMBER FROM SLIDER], notificationEmail: Session.getActiveUser().getEmail(), useTelegram: [TRUE OR FALSE FROM TOGGLE], telegramBotToken: "[TOKEN FROM FORM OR PASTE_YOUR_BOT_TOKEN_HERE]", telegramChatId: "[CHAT ID FROM FORM OR PASTE_YOUR_CHAT_ID_HERE]", batchSize: 20, lookbackDays: 3 };
const Memory = {
load() { const raw = PropertiesService.getScriptProperties().getProperty("agentMemory"); return raw ? JSON.parse(raw) : {}; },
save(memory) { PropertiesService.getScriptProperties().setProperty("agentMemory", JSON.stringify(memory)); },
remember(email, action, reason) { const m = this.load(); m[email.toLowerCase()] = { action, reason, learnedOn: new Date().toLocaleDateString() }; this.save(m); },
recall(email) { return this.load()[email.toLowerCase()] || null; },
summarize() { const m = this.load(); const e = Object.entries(m); if (!e.length) return "No senders learned yet."; return e.map(([em, d]) => ${em}: ${d.action} — "${d.reason}").join("\n"); },
forget(email) { const m = this.load(); delete m[email.toLowerCase()]; this.save(m); },
reset() { PropertiesService.getScriptProperties().deleteProperty("agentMemory"); }
};
function askGemini(senderEmail, senderName, subject, snippet) {
const prompt = You are a Gmail assistant. Analyze this email and decide what to do.\nFrom: ${senderName} <${senderEmail}>\nSubject: ${subject}\nPreview: ${snippet}\nRules: STAR=personal/important, TRASH=junk/scam, UNSUB=newsletter/marketing, FLAG=unsure\nReply ONLY:\nDECISION: [STAR|TRASH|UNSUB|FLAG]\nREASON: [one sentence];
const url = https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${SETTINGS.geminiApiKey};
try {
const res = UrlFetchApp.fetch(url, { method: "post", contentType: "application/json", payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.1, maxOutputTokens: 60 } }), muteHttpExceptions: true });
const text = JSON.parse(res.getContentText())?.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
if (!text) return { action: "FLAG", reason: "No response from Gemini" };
const action = (text.match(/DECISION:\s*(STAR|TRASH|UNSUB|FLAG)/i) || [])[1]?.toUpperCase() || "FLAG";
const reason = (text.match(/REASON:\s*(.+)/i) || [])[1]?.trim() || "No reason";
return { action, reason };
} catch (e) { return { action: "FLAG", reason: "API error: " + e.message }; }
}
function setupTrigger() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); ScriptApp.newTrigger("runAgent").timeBased().everyHours(1).create(); sendNotification("✅ Gmail Agent active!", "Your Gmail AI Agent is running.", "✅ Gmail AI Agent is live!"); Logger.log("Agent activated."); }
function runAgent() {
const flagged = [], actioned = [];
processTrustedSenders(actioned);
if (SETTINGS.deleteSpam) clearSpam(actioned);
const threads = GmailApp.search(is:unread newer_than:${SETTINGS.lookbackDays}d, 0, SETTINGS.batchSize);
for (const thread of threads) {
const msgs = thread.getMessages();
if (!msgs.length) continue;
const msg = msgs[msgs.length - 1];
const senderFull = msg.getFrom();
const senderEmail = extractEmail(senderFull);
if (isTrustedSender(senderEmail)) continue;
let decision = Memory.recall(senderEmail);
if (!decision) { decision = askGemini(senderEmail, extractName(senderFull), msg.getSubject() || "(no subject)", msg.getPlainBody().substring(0, 300)); Memory.remember(senderEmail, decision.action, decision.reason); }
switch (decision.action) {
case "STAR": msg.star(); actioned.push(⭐ Starred: "${msg.getSubject()}"); break;
case "TRASH": thread.moveToTrash(); actioned.push(🗑️ Trashed: "${msg.getSubject()}"); break;
case "UNSUB":
const url = getUnsubscribeUrl(msg);
if (url && attemptUnsubscribe(url)) { thread.moveToTrash(); actioned.push(📧 Unsubscribed: ${senderEmail}); }
else flagged.push({ from: senderFull, subject: msg.getSubject(), reason: "Unsubscribe failed — manual action needed" });
break;
default: flagged.push({ from: senderFull, subject: msg.getSubject(), reason: decision.reason });
}
}
if (flagged.length) sendFlagNotification(flagged, actioned);
}
function processTrustedSenders(actioned) {
GmailApp.search(is:unread newer_than:${SETTINGS.lookbackDays}d, 0, SETTINGS.batchSize).forEach(thread => {
thread.getMessages().forEach(msg => { if (msg.isUnread() && isTrustedSender(extractEmail(msg.getFrom()))) { msg.star(); actioned.push(⭐ Starred (trusted): "${msg.getSubject()}"); } });
});
}
function isTrustedSender(email) { return SETTINGS.trustedSenders.some(t => email.toLowerCase().includes(t.toLowerCase())); }
function clearSpam(actioned) { const s = GmailApp.search("in:spam", 0, SETTINGS.batchSize); if (s.length) { GmailApp.moveThreadsToTrash(s); actioned.push(🗑️ Trashed ${s.length} spam threads); } }
function sendFlagNotification(flagged, actioned) {
const lines = flagged.map((e, i) => ${i + 1}. From: ${e.from}\n "${e.subject}"\n Reason: ${e.reason}).join("\n\n");
sendNotification(🚩 ${flagged.length} email(s) need your attention, ${flagged.length} email(s) were flagged:\n\n${lines}\n\nThese were NOT touched., 🚩 ${flagged.length} email(s) flagged:\n${lines});
}
function sendNotification(subject, emailBody, telegramText) {
try { GmailApp.sendEmail(SETTINGS.notificationEmail, subject, emailBody); } catch (e) {}
if (SETTINGS.useTelegram && SETTINGS.telegramBotToken !== "PASTE_YOUR_BOT_TOKEN_HERE") {
try { UrlFetchApp.fetch(https://api.telegram.org/bot${SETTINGS.telegramBotToken}/sendMessage, { method: "post", contentType: "application/json", payload: JSON.stringify({ chat_id: SETTINGS.telegramChatId, text: telegramText || subject }), muteHttpExceptions: true }); } catch (e) {}
}
}
function extractEmail(f) { const m = f.match(/<(.+?)>/); return m ? m[1].toLowerCase() : f.toLowerCase().trim(); } function extractName(f) { const m = f.match(/^([^<]+)</); return m ? m[1].trim() : f; } function getUnsubscribeUrl(msg) { try { const m = msg.getRawContent().match(/List-Unsubscribe:\s*<(https?://[^>]+)>/i); return m ? m[1] : null; } catch (e) { return null; } } function attemptUnsubscribe(url) { try { const r = UrlFetchApp.fetch(url, { method: "get", followRedirects: true, muteHttpExceptions: true }); return r.getResponseCode() < 400; } catch (e) { return false; } }
function testGemini() { Logger.log(JSON.stringify(askGemini("[email protected]", "Example", "Weekly digest", "Top stories this week..."))); } function testTelegram() { SETTINGS.useTelegram = true; sendNotification("🤖 Test", "Test email.", "🤖 Telegram connected!"); } function showMemory() { Logger.log(Memory.summarize()); } function forgetSender() { Memory.forget("[email protected]"); } function resetMemory() { Memory.reset(); } function stopAgent() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); Logger.log("Agent stopped."); }
=== IMPORTANT NOTES FOR THE BUILDER ===
This project was built with Lovable.
Live app: https://inboxaidfw.lovable.app
Continue developing this project in the Lovable editor.
main on GitHub and your changes sync back into Lovable, ready for your next prompt.Prefer working locally? You need Node.js and npm — install with nvm.
git clone <this-repository-url>
cd <repository-name>
npm i
npm run dev