This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Two-layer project: a Chrome MV3 extension (React + TypeScript + Vite + Tailwind + shadcn/ui) and a Google Apps Script web app backend. The extension lets users configure and monitor syncing of Google Meet notes into a master Google Doc for use with any AI tool.
- Extension source:
src/— built withnpm run build, output todist/ - Backend:
apps-script/Code.gs(~1080 lines) — deployed manually via Apps Script editor
npm run dev # Vite dev server at localhost:5173
npm run build # Build extension → dist/
npm test # Run Vitest test suite
npm run test:watch # Watch mode
npm run package # Build + zip → meet-gemini-notebooklm.zip
npx vitest run src/lib/api.test.ts # Run a single test file
npx vitest run -t "test name" # Run tests matching a name patternLoad dist/ as unpacked extension in Chrome (chrome://extensions → Developer mode → Load unpacked).
- Edit
apps-script/Code.gs. - Copy full contents into the Apps Script editor bound to a Google Doc.
- Deploy as web app: Execute as: Me, Who has access: Anyone.
- Copy the deployment URL — user enters it in the extension's setup wizard.
npm run build(ornpm run packagefor a zip).- Load
dist/unpacked in Chrome, or attach zip to a GitHub Release via the release workflow. - OAuth client ID must be set in
public/manifest.jsonbefore building.
handleRequest(e)is the single entry point for GET/POST.- Auth: token passed as
?token=<accessToken>query param — Apps Script stripsAuthorizationheaders, sofetchApiinapi.tsappends the token to the URL. validateCaller_(accessToken)calls Google tokeninfo endpoint, compares email toSession.getActiveUser().getEmail(), caches 5 min viaCacheService.CONFIG_OVERRIDESloaded fromPropertiesServiceon startup via IIFE;updateSettingspersists changes there.SETTINGS_KEY_MAP_maps camelCase frontend keys ↔ SCREAMING_SNAKE_CASECONFIGkeys.- POST detection uses
e.postData(note.method === 'POST'— that field doesn't exist in Apps Script). getHistory()returns{id, timestamp, filesProcessed, status, message, syncedNames, updatedNames, duration}matchingSyncEventtype.getFiles()returns{id, name, lastSynced, size}matchingSyncFiletype; fetches name fromDrive.Files.get.
- Path alias:
@maps tosrc/— used throughout the codebase (import { api } from '@/lib/api'). - Auth:
chrome.identity.getAuthTokenwith scopes frommanifest.json(openid,email,script.projects,script.deployments— the latter two for backend auto-deploy). The extension token is used solely to verify identity invalidateCaller_; Apps Script usesScriptApp.getOAuthToken()for Drive/Docs. - Config:
deploymentUrlis the source of truth inchrome.storage.sync. It is mirrored into Zustand byApp.tsxon mount but is not persisted by the store'spartialize—chrome.storage.syncis always the authoritative copy.api.tsreads it directly from storage viagetDeploymentUrl(). - Auto-sync is driven by
src/background.ts(MV3 service worker) viachrome.alarms. It readsautoSyncEnabledandautoSyncIntervalMinutesfromchrome.storage.syncdirectly (no Zustand access from the service worker). Settings changes triggerchrome.storage.onChangedto reconfigure the alarm. - State layer:
useSettingsStore(Zustand +persist) holds runtime UI state.useApihook wrapsapi.tscalls and writes results into the store.useAuthmanages thechrome.identitytoken lifecycle. - First run:
App.tsxreadschrome.storage.syncon mount; renders<SetupWizard />if URL not set. - SetupWizard ordering:
setDeploymentUrl(url)must be called AFTERawait signIn()resolves — calling it before causesApp.tsxto unmount the wizard mid-flow. - OAuth client ID: set in
public/manifest.jsonunderoauth2.client_id. Format:<id>.apps.googleusercontent.com.
- GitHub Releases: tag
v*triggers.github/workflows/release.yml→ builds, zips, and attaches to a release PRIVACY.md: covers data handling — no third-party data transmission- Version: shown in popup and dashboard headers via
chrome.runtime.getManifest().version
| Function | Purpose |
|---|---|
appendMeetNotesToMaster() |
Main sync: discovers, filters, cleans, and batch-inserts meeting notes |
checkAndArchive_(docId, tz, force) |
Triggers monthly or at ~800k chars; force=true skips threshold check |
cleanGeminiText_() |
Strips Gemini metadata, markdown headers/bold, and excess whitespace |
CONFIG (top of file) |
Controls MAX_FILES_PER_RUN, ARCHIVE_THRESHOLD_CHARS, ENABLE_MONTHLY_ARCHIVE, MAX_AGE_DAYS |
Chrome extensions can't be navigated to directly (chrome-extension:// URLs are blocked by tools). Instead, use the Vite dev server to iterate visually without rebuilding or reloading the extension:
npm run dev # starts Vite dev server at http://localhost:5173Four dev entry points are available:
| URL | Entry point | What it shows |
|---|---|---|
/dashboard.html |
src/dashboard/main.tsx (conditional mock) |
Full auth + dashboard flow |
/dashboard-dev.html |
src/dashboard/dev-main.tsx |
<Dashboard /> directly, mocked data |
/popup-dev.html |
src/popup/popup-dev-main.tsx |
<Popup /> directly, mocked data |
/wizard-dev.html |
src/wizard-dev-main.tsx |
<SetupWizard /> with scenario picker (success / HTML error / network error / auth error) |
How dev mode works:
src/dashboard/main.tsxconditionally importssrc/dev-mocks.tswhenimport.meta.env.DEVis true.src/dev-mocks.tsstubs outwindow.chrome(storage, identity, tabs, runtime) and seeds the Zustand store with realistic fake data so all tabs render with content.src/dashboard/dev-main.tsxrenders<Dashboard />directly, bypassing the auth/setup flow.src/wizard-dev-main.tsxinlines its own chrome mock and exposes a scenario picker to test different API response states.- Dev mocks are tree-shaken out of production builds — they never appear in
dist/.
Verification workflow for UI changes:
- Start dev server:
npm run dev - Open
http://localhost:5173/dashboard-dev.htmlin a browser - Navigate tabs and inspect visually or via browser DevTools
npm test # runs vitest (jsdom, globals: true)- Test files:
src/**/*.test.{ts,tsx},apps-script/**/*.test.ts tsconfig.jsonexcludes test files from tsc build — required to avoid "Cannot find name 'vi'" errors.src/test/setup.tsmockschrome.storage.sync,chrome.identity,chrome.runtime,chrome.tabs.vimust be imported explicitly in setup.ts (import { vi } from 'vitest') even withglobals: true.
dist/is gitignored — build artifacts are not committed.Drive.Files.getreturnssize: "0"for Google Docs (not binary files) —getFiles()treats this as0.Session.getActiveUser().getEmail()returns empty for some personal accounts.validateCaller_falls back to a storedOWNER_EMAILscript property: it is seeded on first successful auth fromtokeninfo.email. To reset (e.g. after binding to a new user), delete theOWNER_EMAILproperty in the Apps Script editor → Project Settings → Script Properties.- Email notifications via
MailAppsilently fail when quota is exceeded or on personal accounts. - Archive email failure is caught and logged but does not abort the archive.
- shadcn/ui CSS variables ARE defined in
src/index.css(--primary,--input,--background,--ring, etc.). They can be used directly. background.jsmust land at the dist root, notdist/assets/. Vite routes it there viaoutput.entryFileNamescallback invite.config.ts— don't remove that logic.- Auto-sync settings (
autoSyncEnabled,autoSyncIntervalMinutes) live inchrome.storage.syncdirectly, not in Zustand — the background service worker reads them at alarm time without access to the store.