sincenety
v0.8.9
Published
Claude Code 작업 갈무리 도구 — 작업 이력 자동 분석 및 구조화된 기록 생성
Maintainers
Readme
sincenety
Automatic work session tracker for Claude Code — A 3-phase pipeline that retroactively collects, summarizes, and reports all Claude Code activity. No start/stop needed.
$ sincenety
☁️ D1 sync complete
☁️ D1 sync complete
✅ sincenety complete — 1 sent, 0 skipped
$ sincenety air
📋 air complete
Date range: 3 days (backfill 2 days)
Total sessions: 12
Changed dates: 2
Changed: 2026-04-06, 2026-04-07
$ sincenety circle
📋 circle complete
Date range: 3 days
Total sessions: 12
Changed dates: 2
Finalized: 2026-04-06
Needs summary: 2026-04-07Features
Default Command: Full Pipeline
v0.7.0 — Running sincenety with no arguments executes the entire pipeline automatically: air → circle → out. This is the recommended way to use sincenety — one command does everything.
If D1 or email is not configured, it shows help + setup instructions instead.
3-Phase Pipeline: air → circle → out
The pipeline can also be run in individual phases:
sincenety air— Collect and store work records by date- Date-based grouping (midnight boundary, startedAt-based)
- Automatic backfill: checkpoint-based, collects empty dates too
- Change detection: data hash skips unchanged dates
- Empty day records (no sessions = still recorded)
--jsonoutputs per-date JSON
sincenety circle— LLM-powered summaries- Internally runs
airfirst --json: outputs session data for AI summary (SKILL.md integration)--save: saves stdin JSON todaily_reports--type daily|weekly|monthly- Auto-finalization: midnight finalizes previous day, Monday finalizes previous week, 1st finalizes previous month
- Change detection: data hash comparison saves tokens
- Vacation days get a [vacation] label automatically
- Project-level session merge: all sessions within the same
projectNameare individually summarized, then consolidated into a single merged summary per project — eliminates duplicate entries and improves report coherence
- Internally runs
sincenety out— Smart email deliveryout: daily always, +weekly on Friday, +monthly on month-end- Unsent catchup: missed Friday → Monday auto-sends weekly
- 4 providers: Gmail MCP / Resend / Gmail SMTP / Custom SMTP
outd/outw/outm: force daily / weekly / monthly--preview,--render-only,--history
CLI Commands
| Command | Description |
|---------|-------------|
| sincenety | Full pipeline — air → circle → out in one command |
| sincenety air | Collect — date-grouped auto-backfill gathering |
| sincenety circle | Summarize — LLM summary (--json/--save/--type) |
| sincenety out | Smart dispatch (weekday + unsent catchup) |
| sincenety outd | Force send daily report |
| sincenety outw | Force send weekly report |
| sincenety outm | Force send monthly report |
| sincenety sync | D1 central cloud sync |
| sincenety config | Settings (--setup, --vacation, --d1-*) |
Retroactive Work Gathering
No need to remember to start/stop tracking. sincenety parses ~/.claude/ data at runtime and reconstructs everything:
- Session JSONL parsing — Extracts token usage, model names, millisecond-precision timestamps, and conversation turns from
~/.claude/projects/[project]/[sessionId].jsonl - Checkpoint-based backfill — Automatically fills gaps from last checkpoint; first run backfills 90 days
Rich Work Records
| Field | Description | |-------|-------------| | Title | Auto-extracted from first user message | | Description | Top 3-5 user messages joined | | Token usage | Per-message input/output/cache token aggregation | | Duration | First message → last message precise measurement | | Model | Extracted from assistant responses | | Category | Auto-classified from project path |
AI Summarization Engine
Unified AI provider system — ai_provider config is respected in all environments (CLI, cron, Claude Code):
| ai_provider | circle auto-summary | gatherer summary | Typical use case |
|----------------|----------------------|-------------------|-----------------|
| cloudflare | Workers AI (Qwen3-30B) → heuristic fallback | Workers AI | CLI / cron |
| anthropic | Skip (no auto-summary) | Claude API (Haiku) | API key available |
| claude-code | Skip (SKILL.md handles it) | Heuristic | Claude Code /sincenety |
| auto (default) | Auto-detect: cloudflare only | Auto-detect | First-time setup |
# AI provider configuration (controls behavior in ALL environments)
sincenety config --ai-provider cloudflare # Use Workers AI
sincenety config --ai-provider anthropic # Use Claude API
sincenety config --ai-provider claude-code # Claude Code direct summary (SKILL.md)
sincenety config --ai-provider auto # Auto-detect (default)
# Check current settings
sincenety config
# → AI summary: ai_provider = auto (auto → cloudflare)- Cloudflare Workers AI (Qwen3-30B) for Korean text summarization
- D1 token only needed — no separate API key required
circleauto-summarizes whenai_provideriscloudflare: per-session topic/outcome/flow/significance + daily overviewcircle --json --summarize: Workers AI summaries included in JSON output (requiresai_provider = cloudflare)- Free tier: 10,000 neurons/day (sufficient for personal use, ~300 summaries/day)
- Heuristic fallback: if Workers AI call fails for a session, falls back to heuristic summary (no data loss)
Email AI Summary Integration
Email reports include AI-generated summaries from daily_reports:
- Overview section at the top of each email with a full-day summary
- Per-session mapping:
daily_reportswrapUp data maps to each session's topic/outcome/flow/significance - Gmail 102KB clip prevention: actions capped at 5 per session, text length optimized to stay under Gmail's clipping threshold
Required Setup (Mandatory)
sincenety requires two configurations before any command can run:
- D1 Cloud Sync — Cloudflare API token (enables Workers AI + cloud sync)
- Email Delivery — SMTP or Resend (enables report email delivery)
# Step 1: D1 token (auto-detects account, creates DB, enables Workers AI)
sincenety config --d1-token <API_TOKEN>
# Step 2: Email setup (interactive wizard)
sincenety config --setup
# → Gmail app password: https://myaccount.google.com/apppasswordsAll commands (air, circle, out, sync, etc.) will refuse to run until both are configured. Only config is exempt.
Vacation Management
- Google Calendar auto-detection — SKILL.md instructs Claude Code to check Google Calendar for vacation events
- CLI manual registration —
config --vacation 2026-04-10 2026-04-11 - Vacation keywords (Korean + English): 휴가/vacation/연차/PTO/병가/sick/반차/half-day
- Vacation types: vacation / sick / holiday / half / other
- Report integration — vacation days get a [vacation] label in
circle;outskips vacation days automatically
Config Setup Wizard
Run sincenety config --setup for an interactive 3-choice wizard:
- Gmail SMTP (with app password URL guidance)
- Resend API
- Custom SMTP
Connection test runs automatically on setup completion.
Gmail MCP Integration
Zero-config email delivery inside Claude Code via gmail_create_draft MCP tool. No SMTP credentials needed — Claude Code drafts the email directly in Gmail. Use out --render-only to get HTML output for the MCP path.
Config Management
Run sincenety config with no arguments to see a formatted settings status table. Supports vacation registration, email provider selection (Gmail/Resend/custom SMTP), and more.
Scope Selection (Global / Project)
Choose whether to track all projects on this machine or a specific project only:
- Global mode — collects all Claude Code sessions across all projects
- Project mode — filters to sessions from a single project path
Scope is set during initial setup (npm install -g) or on first npx sincenety run. Stored at ~/.sincenety/scope.json.
Cloud Sync (Cloudflare D1)
Multi-machine data aggregation via Cloudflare D1:
- Local-first: encrypted local DB remains the source of truth
sincenety syncpushes local data to a central D1 database (push / pull-config / status / init)- Auto-sync after
outcompletes (non-fatal — network errors don't block email delivery) - Shared config: SMTP settings set once,
sync --pull-configon new machines to pull shared config - Machine ID: hardware-based auto-detection (see below),
config --machine-nameoverride for custom identification - Zero new dependencies: uses native
fetchfor D1 REST API — no extra packages added
Weekly / Monthly Reports (v0.8.8+)
v0.8.8 removes the heuristic weekly/monthly baseline. The previous auto-generation path (introduced in v0.8.4) concatenated daily outcomes/flows with "\n" and " → " respectively, producing low-quality summaries that were then silently re-emailed. It's gone entirely.
- Skill-only generation: weekly/monthly rows are created exclusively via
circle --save --type weekly|monthlyfrom the/sincenetyskill. Claude Code inside the skill writes the summary using the full set of daily summaries as context, then saves it. CLI no longer invents weekly/monthly content. outw/outmerror contract: if the target row is missing or has an emptysessionsarray,runOutemits a precise error ("weekly report row for <date> not found. Run /sincenety to generate...") instead of silently skipping. cron detects via exit code 1.- Every-run current-week refresh (v0.8.9):
runCircleforces a re-summary of the current week (Mon–today) dailies every run, even whengather_reports.data_hashis unchanged. v0.8.8 also re-ran the entire current month every time, but that produced very long output and high token cost on each invocation; the month scope was dropped in v0.8.9. Monthly rows are now refreshed only via the skill at month-end / on demand. - Emailed-row protection preserved:
emailedAt != nullstill protects daily/weekly/monthly rows from overwrite, even under the forced-refresh rule above. - Removed config/flags:
pipeline_modeconfig key and--mode/--pipeline-modeCLI flags are gone. The--pipeline-modeflag now emits a one-line deprecation warning and does nothing.
Cross-Device Consolidated Reports
v0.8.0 — When working on multiple machines (e.g., Mac + Linux), sessions from all devices are automatically merged into a single daily report:
- Push-before-pull: local data is pushed to D1 first, then other devices' sessions are pulled for consolidation
- Circle cross-device merge:
circle(AI summarization) pulls other devices' sessions from D1 and generates a unified summary covering all machines — not just local work - Always-send policy:
outalways sends email regardless of whether another device already sent — no skip, no dedup - Session merge by topic: sessions with identical
projectName + titleare automatically merged — stats aggregated, best wrapUp selected, flow narratives concatenated - Graceful fallback: if D1 is unreachable, falls back to single-device local-only behavior
- Title extraction improvement: sessions starting with slash commands (e.g.,
/sincenety) now get meaningful fallback titles instead of empty strings
Cloudflare API Token Setup
- Go to dash.cloudflare.com/profile/api-tokens
- "Create Token" → "Custom token" (click "Get started" at the bottom)
- Set permissions:
| Permission | Access | Purpose |
|-----------|--------|---------|
| Account / D1 | Edit | DB creation + read/write |
| Account / Workers AI | Read | AI summary model (Qwen3-30B) |
| Account / Account Settings | Read | Account auto-detection on --d1-token setup |
All 3 are required. Without Account Settings Read,
--d1-tokensetup cannot find your account.
- Account Resources → Include → select your account
- "Create Token" → copy the token (shown only once!)
This single token powers D1 (central DB) + Workers AI (summary engine) + sync.
Token-Only D1 Setup
A single token is all you need. Everything else is auto-detected:
sincenety config --d1-token cfp_xxxxxxxx
# ✅ Account auto-detected
# ✅ D1 database auto-created/connected
# ✅ machine_id auto-detected (hardware UUID-based)
# ✅ Workers AI auto-enabled (Qwen3-30B)
# ✅ Schema setup completeAuto Machine ID
Hardware-based machine identification — zero configuration needed:
| Platform | Source | Characteristics | |----------|--------|-----------------| | macOS | IOPlatformUUID | Hardware-unique, survives OS reinstall | | Linux | /etc/machine-id | OS-unique | | Windows | MachineGuid | Install-unique |
- Format:
mac_a1b2c3d4_username - Auto-detected with no user action required
- Same machine always produces the same ID
- Used for D1 sync machine registry (
machinestable)
Encrypted Storage
All data is AES-256-GCM encrypted at ~/.sincenety/sincenety.db. Machine-bound key (hostname + username + random salt) by default.
Installation & Setup
There are two ways to run sincenety: npx (no install) or global install.
Option A: npx (recommended for first-time / one-shot use)
All three flags are required on first run. Without them, sincenety will show setup instructions and exit.
Prerequisites — get your tokens first:
Cloudflare D1 API Token — dash.cloudflare.com/profile/api-tokens
- Create a Custom token with these permissions:
| Permission | Access | Purpose | |-----------|--------|---------| | Account / D1 | Edit | DB creation + read/write | | Account / Workers AI | Read | AI summarization (Qwen3-30B) | | Account / Account Settings | Read | Account auto-detection |
Resend API Key — resend.com/api-keys
- Free tier: 100 emails/day (more than enough for daily reports)
Run:
npx sincenety --token <D1_TOKEN> --key <RESEND_KEY> --email [email protected]This single command will:
- Save D1 token → auto-detect Cloudflare account → create DB → setup schema
- Save Resend API key + recipient email
- Run the full pipeline: air → circle → out
Subsequent runs — config persists in ~/.sincenety/, so you only need:
npx sincenetyOption B: Global install (recommended for daily use)
npm install -g sincenety@latestThe installer runs an interactive setup wizard:
┌──────────────────────────────────────────────┐
│ sincenety — Initial Setup │
└──────────────────────────────────────────────┘
── Step 1/3: Scope ─────────────────────────────
1) Global — track all Claude Code projects on this machine
2) Project — track only a specific project
── Step 2/3: D1 Cloud Sync ─────────────────────
Guided Cloudflare API token creation with required permissions:
Account | Workers AI | Read
Account | D1 | Edit
Account | Account Settings | Read
── Step 3/3: Email Delivery ────────────────────
1) Gmail SMTP (app password required)
2) Resend API (resend.com API key)
3) Custom SMTPAfter setup, just run:
sincenetyNote: The setup wizard only runs on first install. Subsequent updates preserve your configuration. In non-TTY environments (CI/Docker), the wizard is skipped — configure manually with
sincenety config --setup.
Build from source
git clone https://github.com/pathcosmos/sincenety.git
cd sincenety
npm install && npm run build
npm linkVerify setup
sincenety config
# Shows all settings with ✅/❌ status
# AI summary: ai_provider = auto (auto → cloudflare)Usage
Default — Full Pipeline
# Run the entire pipeline: air → circle → out
sincenety
# If D1 or email is not configured, shows help + setup instructionsair — Collect Work Records
# Collect all sessions (checkpoint-based backfill, first run = 90 days)
sincenety air
# Specify custom history.jsonl path
sincenety air --history /path/to/history.jsonl
# JSON output (per-date structured data)
sincenety air --jsoncircle — AI Summary Pipeline
# Run air + check finalization status
sincenety circle
# Output session data as JSON for AI summary (SKILL.md integration)
sincenety circle --json
# Output with Workers AI summaries included (for SKILL.md cloudflare mode)
sincenety circle --json --summarize
# Save AI-generated summary to DB (stdin JSON)
sincenety circle --save < summary.json
sincenety circle --save --type weekly < weekly_summary.json
sincenety circle --save --type monthly < monthly_summary.jsonconfig — Settings Management
# Interactive setup wizard (Gmail SMTP / Resend / Custom SMTP)
sincenety config --setup
# Show current settings (ANSI table)
sincenety config
# Email settings
sincenety config --email [email protected]
sincenety config --smtp-user [email protected]
sincenety config --smtp-pass # Prompted securely
sincenety config --provider resend
sincenety config --resend-key rk_...
# AI provider (controls Claude Code behavior)
sincenety config --ai-provider cloudflare # Workers AI
sincenety config --ai-provider anthropic # Claude API
sincenety config --ai-provider claude-code # Claude Code direct summary
sincenety config --ai-provider auto # Auto-detect (default)
# Vacation management
sincenety config --vacation 2026-04-10 2026-04-11
sincenety config --vacation-list
sincenety config --vacation-clear 2026-04-10Generate Gmail app password: https://myaccount.google.com/apppasswords
out — Smart Email Delivery
# Smart dispatch (daily always, +weekly on Friday, +monthly on month-end)
sincenety out
# Preview (no send)
sincenety out --preview
# HTML JSON output (for Gmail MCP)
sincenety out --render-only
# View send history
sincenety out --history
# Force send specific report type
sincenety outd # daily report
sincenety outw # weekly report
sincenety outm # monthly report
# Target specific date (yyyyMMdd)
sincenety outd --date 20260408 # daily report for Apr 8
sincenety outw --date 20260408 # weekly report for week of Apr 6-12
sincenety outm --date 20260408 # monthly report for April 2026
sincenety out --date 20260408 # smart dispatch as if today is Apr 8sync — Cloud Sync (Cloudflare D1)
# D1 configuration
sincenety config --d1-account ACCOUNT_ID --d1-database DB_ID --d1-token TOKEN
sincenety config --machine-name "office-mac"
# Sync operations
sincenety sync --init # Create D1 schema
sincenety sync # Push local → D1
sincenety sync --pull-config # Pull shared config from D1
sincenety sync --status # Check sync statusClaude Code Skill (/sincenety)
Use /sincenety directly inside Claude Code sessions for AI-powered daily reports.
Installation
- Install the CLI (provides the data collection engine):
npm install -g sincenety@latest- Install the skill (registers
/sincenetycommand in Claude Code):
mkdir -p ~/.claude/skills/sincenety
cp node_modules/sincenety/src/skill/SKILL.md ~/.claude/skills/sincenety/SKILL.mdOr if installed globally:
mkdir -p ~/.claude/skills/sincenety
cp "$(npm root -g)/sincenety/src/skill/SKILL.md" ~/.claude/skills/sincenety/SKILL.md- Update to latest version:
Inside Claude Code, run:
! npm install -g sincenety@latestHow it works
When you type /sincenety inside Claude Code:
- Data collection —
aircollects all sessions with checkpoint-based backfill - JSON output —
circle --jsonoutputs session data with conversation turns - AI summary — Claude Code itself analyzes and generates topic/outcome/flow/significance
- Save to DB —
circle --savewrites summary todaily_reports - Email — If configured, sends an HTML email with AI summary
The key insight: Claude Code is the AI — no external API key needed.
Email setup (optional)
sincenety config --email [email protected]
sincenety config --smtp-user [email protected]
sincenety config --smtp-pass # Prompts for Gmail app passwordGenerate Gmail app password: https://myaccount.google.com/apppasswords
Architecture
sincenety/
├── src/
│ ├── cli.ts # CLI entry (default + air/circle/out/outd/outw/outm/sync/config)
│ ├── postinstall.ts # postinstall setup wizard (scope → D1 → email)
│ ├── core/
│ │ ├── air.ts # Phase 1: date-based gathering (backfill + hash)
│ │ ├── circle.ts # Phase 2: LLM summary pipeline (finalization + save)
│ │ ├── out.ts # Phase 3: smart email dispatch (out/outd/outw/outm)
│ │ ├── gatherer.ts # Core gathering logic (parse → group → store)
│ │ ├── summarizer.ts # AI summarization router (Workers AI / Claude API / heuristic)
│ │ └── ai-provider.ts # AI provider detection & routing (cloudflare/anthropic/claude-code)
│ ├── parser/
│ │ ├── history.ts # ~/.claude/history.jsonl streaming parser
│ │ └── session-jsonl.ts # Session JSONL parser (tokens/model/timing/turns)
│ ├── grouper/session.ts # Session grouping by sessionId + project
│ ├── storage/
│ │ ├── adapter.ts # StorageAdapter interface
│ │ └── sqljs-adapter.ts # sql.js implementation (encrypted DB, v4 migration)
│ ├── encryption/
│ │ ├── key.ts # PBKDF2 key derivation (machine-bound + passphrase)
│ │ └── crypto.ts # AES-256-GCM encrypt/decrypt
│ ├── report/
│ │ ├── terminal.ts # Terminal output formatter
│ │ └── markdown.ts # Markdown report generator
│ ├── email/
│ │ ├── sender.ts # nodemailer email sender
│ │ ├── renderer.ts # HTML email renderer (report → HTML, cross-device merge)
│ │ ├── merge-sessions.ts # Session merge by project (dedup same-project sessions)
│ │ ├── resend.ts # Resend API email provider
│ │ ├── provider.ts # Email provider abstraction (Gmail MCP/Resend/SMTP)
│ │ └── template.ts # Bright color-coded HTML email template
│ ├── vacation/
│ │ ├── manager.ts # Vacation CRUD (register/list/clear/check)
│ │ └── detector.ts # Vacation keyword detection (KO+EN)
│ ├── config/
│ │ ├── setup-wizard.ts # Interactive 3-choice setup wizard
│ │ └── scope.ts # Scope config (global/project) read/write/prompt
│ ├── cloud/
│ │ ├── d1-client.ts # Cloudflare D1 REST API client
│ │ ├── d1-schema.ts # D1 schema definition & migration
│ │ ├── d1-auto-setup.ts # Token-only auto-setup (account/DB detection)
│ │ ├── cf-ai.ts # Cloudflare Workers AI client (Qwen3-30B)
│ │ └── sync.ts # Sync logic (push/pull/status/init)
│ ├── util/
│ │ └── machine-id.ts # Cross-platform hardware ID detection
│ ├── scheduler/install.ts # launchd/cron auto-installer (disabled)
│ └── skill/SKILL.md # Claude Code skill definition
├── tests/
│ ├── encryption.test.ts # Encryption tests (26 cases)
│ ├── migration-v4.test.ts # DB v3→v4 migration tests (7 cases)
│ ├── air.test.ts # air command tests (7 cases)
│ ├── circle.test.ts # circle command tests (39 cases)
│ ├── out.test.ts # out command tests (47 cases)
│ ├── vacation.test.ts # Vacation management tests (13 cases)
│ ├── d1-client.test.ts # D1 client tests
│ ├── sync.test.ts # Sync tests
│ ├── cf-ai.test.ts # Cloudflare Workers AI tests
│ └── machine-id.test.ts # Machine ID detection tests
├── package.json
└── tsconfig.jsonInstall Flow
npm install -g sincenety@latest
│
▼
┌─ postinstall.js ─────────────────────────────────┐
│ │
│ TTY check ───→ No TTY? → "Run config --setup" │
│ │ │
│ ▼ (TTY) │
│ Already configured? ──→ Yes → "Updated. OK" │
│ │ │
│ ▼ (No) │
│ │
│ Step 1: Scope │
│ ┌────────────────────────┐ │
│ │ 1) Global (all) │ │
│ │ 2) Project (path) │ │
│ └───────┬────────────────┘ │
│ │ → ~/.sincenety/scope.json │
│ ▼ │
│ Step 2: D1 Cloud Sync │
│ ┌────────────────────────┐ │
│ │ D1 API token input │ │
│ │ → autoSetupD1() │ │
│ │ → ensureD1Schema() │ │
│ └───────┬────────────────┘ │
│ │ → ~/.sincenety/sincenety.db │
│ ▼ │
│ Step 3: Email │
│ ┌────────────────────────┐ │
│ │ 1) Gmail SMTP │ │
│ │ 2) Resend API │ │
│ │ 3) Custom SMTP │ │
│ └───────┬────────────────┘ │
│ │ → ~/.sincenety/sincenety.db │
│ ▼ │
│ ✅ Ready │
└───────────────────────────────────────────────────┘Run Flow
$ sincenety [--token T --key K --email E]
│
▼
Scope check ───→ missing? → prompt (global/project)
│
▼
Param check ───→ missing D1/email? → show setup guide + exit
│
▼
┌─ runOut(scope) ──────────────────────────────────┐
│ │
│ ┌─ air ─────────────────────────────────────┐ │
│ │ ~/.claude/history.jsonl │ │
│ │ → session list (sessionId + project) │ │
│ │ ~/.claude/projects/[p]/[id].jsonl │ │
│ │ → tokens / model / timing / turns │ │
│ │ │ │
│ │ scope filter (project mode) │ │
│ │ date grouping (midnight boundary) │ │
│ │ checkpoint backfill + data hash │ │
│ │ → gather_reports DB │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ circle ──────────────────────────────────┐ │
│ │ auto-finalization │ │
│ │ (yesterday / last week / last month) │ │
│ │ D1 cross-device session pull + merge │ │
│ │ Workers AI summary (Qwen3-30B) │ │
│ │ → daily_reports DB (all devices) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ D1 pre-sync ────────────────────────────┐ │
│ │ push local → D1 (my data first) │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ out (smart dispatch) ────────────────────┐ │
│ │ daily — always │ │
│ │ weekly — Friday (or catchup) │ │
│ │ monthly — month-end (or catchup) │ │
│ │ --date yyyyMMdd — target specific date │ │
│ │ │ │
│ │ D1 cross-device session pull + merge │ │
│ │ Project-level session merge (×N) │ │
│ │ │ │
│ │ → Gmail MCP / Resend / │ │
│ │ Gmail SMTP / Custom SMTP │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ D1 post-sync ───────────────────────────┐ │
│ │ push email logs → D1 │ │
│ └──────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘
│
▼
✅ sincenety complete — N sent, N skippedEncryption
- Algorithm: AES-256-GCM (authenticated encryption)
- Key derivation: PBKDF2 (SHA-256, 100,000 iterations)
- Key source:
hostname + username + random salt(machine-bound) - Salt:
~/.sincenety/sincenety.salt(32-byte random, created once, mode 0600) - File format:
[4B magic "SNCT"][12B IV][ciphertext][16B auth tag]
Local DB — Full Specification
File: ~/.sincenety/sincenety.db (AES-256-GCM encrypted blob, file mode 0600, dir mode 0700)
Engine: sql.js — WASM-compiled SQLite, zero native dependencies. The entire DB file is decrypted into memory on open, mutated in-place, re-encrypted on close. There is no incremental INSERT to disk — every run rewrites the whole encrypted blob.
Sidecar: ~/.sincenety/sincenety.salt — 32-byte cryptographically random salt, generated once on first run, used in PBKDF2 key derivation. If this file is deleted, the DB becomes permanently unreadable.
Opening the DB: file ~/.sincenety/sincenety.db should report data (opaque). If it says SQLite 3.x database, encryption is broken and the DB has leaked plaintext.
Why we keep the local DB (design rationale)
The local DB is a derived artifact — the source of truth is always ~/.claude/history.jsonl + ~/.claude/projects/*.jsonl. In principle everything could be reconstructed from those on every run. We keep the local DB anyway because it serves three jobs that pure file reconstruction cannot do cleanly:
Idempotency boundary —
sincenetyis designed to be run multiple times per day (cron at 10:00, manual at 15:00, auto at end-of-day). The composite PK(session_id, project)onsessionsand theUNIQUE(report_date, report_type)ondaily_reportsmake every run safely re-runnable. Without the DB, either (a) each run produces a duplicate report row/email or (b) a bespoke dedupe index must be maintained on disk — which is just "a DB, worse".Send-state authority —
daily_reports.emailed_atis the single source of truth for "was this report already delivered?" ThroughoutautoSummarizeandcircleSave(circle.ts), rows withemailedAt != nullare explicitly protected from overwrite — even under v0.8.9's force-refresh-this-week rule (and v0.8.8's wider this-week-and-month rule before it).email_logsis the append-only audit trail: every successful and failed send lands there with subject, recipient, provider, and error message.Cross-device merge pivot —
sync push(pre-send) uploads this machine'sdaily_reportsrows to Cloudflare D1;sync pulldownloads rows authored by other machines. The merge in the email renderer joins local rows with pulled rows by(report_date, project_name)and dedupes sessions by(project_name, title_normalized). Without a local DB, there is no "this machine's view" to push, and no stable pivot to merge remote rows into.
Not kept in the DB (conscious choices): full conversation text, code content, tool call payloads. Only metadata (counts, timings, tokens, titles, descriptions, short summaries) is persisted, limiting blast radius if the key derivation ever leaks.
When the local DB is genuinely redundant: a single-machine user who never emails, never syncs, and only reads --json stdout to pipe into Claude Code directly. For that user the DB adds cost without benefit. For everyone else (multi-device, scheduled delivery, week/month rollups), removing the DB would require rebuilding the three jobs above from scratch.
Storage file layout
~/.sincenety/
├── sincenety.db # encrypted SQLite blob (this document)
├── sincenety.salt # 32-byte PBKDF2 salt (0600)
└── machine-id # stable machine identifier for D1 row attributionEncryption envelope
[4B magic "SNCT"] [12B IV] [ciphertext (variable)] [16B GCM auth tag]- Algorithm: AES-256-GCM (AEAD — ciphertext tampering is detected on decrypt)
- Key derivation: PBKDF2-SHA256, 100,000 iterations, 32-byte output
- Key material:
hostname ∥ username ∥ saltby default (machine-bound), or a user-supplied passphrase - IV: 12 random bytes per encrypt, never reused for the same key
- Auth tag: 16 bytes, verified on every decrypt — tampering throws, does not silently fallback to empty DB
Schema version — v4 (current)
Schema version is stored in config.value under key schema_version. On open, applySchema() reads the current version and runs forward-only migrations:
| From → To | Migration summary |
|-----------|-------------------|
| v1 → v2 | ALTER TABLE sessions ADD COLUMN × 14 (tokens, timing breakdown, title, description, category, tags, model). Adds gather_reports and config tables. |
| v2 → v3 | Creates daily_reports table (AI summaries with UNIQUE(report_date, report_type)). |
| v3 → v4 | gather_reports gains report_date, data_hash, updated_at; daily_reports gains status, progress_label, data_hash; creates vacations and email_logs tables; adds idx_gather_report_date unique index. |
Migrations use ALTER TABLE ADD COLUMN (never DROP) to keep downgrade-from-newer safe. Invalid or unknown schema_version values are treated as "fresh install" — the DB is rebuilt from v1 forward.
Tables — per-column specification
sessions (22 columns) — the core per-work-session record
Composite primary key (id, project). One row per Claude Code session (one sessionId on one project directory). Upserted every gather run.
| Column | Type | Role |
|--------|------|------|
| id | TEXT NOT NULL | Claude Code sessionId (UUID from ~/.claude/sessions/<id>.json) |
| project | TEXT NOT NULL | Absolute project path (the cwd at session start) |
| project_name | TEXT NOT NULL | basename(project) — for display and same-project merging |
| started_at | INTEGER NOT NULL | Unix epoch ms — first message timestamp in the session |
| ended_at | INTEGER NOT NULL | Unix epoch ms — last message timestamp |
| duration_minutes | REAL DEFAULT 0 | (ended_at - started_at) / 60000, precomputed for report queries |
| message_count | INTEGER NOT NULL DEFAULT 0 | Total message count (user + assistant + tool) |
| user_message_count | INTEGER DEFAULT 0 | User-authored messages only |
| assistant_message_count | INTEGER DEFAULT 0 | Assistant responses only |
| tool_call_count | INTEGER DEFAULT 0 | Number of tool invocations (Read, Edit, Bash, …) |
| input_tokens | INTEGER DEFAULT 0 | Sum across session |
| output_tokens | INTEGER DEFAULT 0 | Sum across session |
| cache_creation_tokens | INTEGER DEFAULT 0 | Prompt-cache writes |
| cache_read_tokens | INTEGER DEFAULT 0 | Prompt-cache hits |
| total_tokens | INTEGER DEFAULT 0 | Denormalized sum of the four above — used directly in report aggregation |
| title | TEXT | AI-generated or heuristic session title (≤80 chars) |
| summary | TEXT | Short session summary (1–2 sentences) |
| description | TEXT | Longer description of what happened in this session |
| category | TEXT | Optional classification (feat/fix/docs/refactor/chore) |
| tags | TEXT | Comma-separated keyword tags |
| model | TEXT | Dominant model used (e.g. claude-opus-4-6, claude-sonnet-4-6) |
| created_at | INTEGER NOT NULL | DB row creation ms — not session time |
Indexes: idx_sessions_started (started_at), idx_sessions_project (project), idx_sessions_category (category).
Write path: gatherer.ts → UPSERT per session via INSERT … ON CONFLICT(id, project) DO UPDATE. Token counters are overwritten (not summed) — the source JSONL is canonical.
gather_reports (raw run log)
Captures the raw markdown + JSON output of a sincenety gather run. Not strictly required for operation — kept as an audit trail and for --json reproducibility.
| Column | Type | Role |
|--------|------|------|
| id | INTEGER PK AUTOINCREMENT | Surrogate key |
| gathered_at | INTEGER NOT NULL | Run timestamp (ms) |
| from_timestamp | INTEGER NOT NULL | Start of gather window |
| to_timestamp | INTEGER NOT NULL | End of gather window |
| session_count | INTEGER DEFAULT 0 | Sessions in this run |
| total_messages | INTEGER DEFAULT 0 | Aggregate message count |
| total_input_tokens | INTEGER DEFAULT 0 | |
| total_output_tokens | INTEGER DEFAULT 0 | |
| report_markdown | TEXT | Rendered terminal/markdown report |
| report_json | TEXT | Structured JSON for downstream save-daily |
| emailed_at | INTEGER | Deprecated — superseded by daily_reports.emailed_at |
| email_to | TEXT | Deprecated |
| report_date | TEXT (v4) | YYYY-MM-DD of the gather window start — used by unique index |
| data_hash | TEXT (v4) | Content hash of report_json; unchanged input → same hash → no-op rewrite |
| updated_at | INTEGER (v4) | Last modification ms |
Unique index idx_gather_report_date on (report_date) (v4) — one raw gather report per calendar day; reruns update the same row.
daily_reports (AI-summarized reports — daily/weekly/monthly)
The authoritative source for what gets emailed and what cross-device sync exchanges. One row per (report_date, report_type).
| Column | Type | Role |
|--------|------|------|
| id | INTEGER PK AUTOINCREMENT | |
| report_date | TEXT NOT NULL | YYYY-MM-DD anchor (for weekly/monthly: Monday / 1st of month) |
| report_type | TEXT NOT NULL DEFAULT 'daily' | One of daily / weekly / monthly |
| period_from | INTEGER NOT NULL | Window start (ms) |
| period_to | INTEGER NOT NULL | Window end (ms) |
| session_count | INTEGER DEFAULT 0 | Aggregated session count in window |
| total_messages | INTEGER DEFAULT 0 | Aggregated |
| total_tokens | INTEGER DEFAULT 0 | Aggregated |
| summary_json | TEXT NOT NULL | Serialized array of per-session SummaryEntry objects (title, overview, actions, tokens, project_name, …). The email renderer reads this field. |
| overview | TEXT | Day-level / week-level / month-level meta-summary (2–4 sentences) |
| report_markdown | TEXT | Pre-rendered markdown for CLI report command |
| created_at | INTEGER NOT NULL | Row creation ms |
| emailed_at | INTEGER | Null-checked (!= null) to decide overwrite eligibility. A non-null value means this report has been delivered and must not be overwritten by auto-summary. |
| email_to | TEXT | Recipient email address for the delivered report |
| status | TEXT DEFAULT 'in_progress' (v4) | in_progress while the window is still open, finalized when the period is fully closed (previous day / previous week / previous month). finalizePreviousReports flips the state. |
| progress_label | TEXT (v4) | Human-readable state label (e.g. "5/7 days of week") |
| data_hash | TEXT (v4) | Content hash for change detection — D1 sync skips pushes whose hash matches the remote row |
Constraint: UNIQUE(report_date, report_type) — the core idempotency guarantee.
Indexes: idx_daily_date, idx_daily_type.
checkpoints
Records the last processed timestamp per gather run. In practice deprecated because gathering always goes from today 00:00 forward (not incremental from last checkpoint), but kept for historical compatibility and potential future "incremental since N" mode.
| Column | Type | Role |
|--------|------|------|
| id | INTEGER PK AUTOINCREMENT | |
| timestamp | INTEGER NOT NULL | Last processed ms |
| created_at | INTEGER NOT NULL | |
config (key-value store)
| Column | Type | Role |
|--------|------|------|
| key | TEXT PK | Setting name |
| value | TEXT NOT NULL | String value (JSON-encoded when needed) |
| updated_at | INTEGER NOT NULL | |
Known keys: schema_version, email_to, smtp_user, smtp_pass, smtp_host, smtp_port, resend_key, d1_api_token, d1_account_id, d1_database_id, cf_ai_token, provider, ai_provider (cloudflare | anthropic | claude-code | auto), scope (global | project). (The pipeline_mode key was deprecated in v0.8.8 and is no longer read; its --pipeline-mode flag now emits a deprecation warning.)
vacations
| Column | Type | Role |
|--------|------|------|
| id | INTEGER PK AUTOINCREMENT | |
| date | TEXT NOT NULL UNIQUE | YYYY-MM-DD |
| type | TEXT NOT NULL DEFAULT 'vacation' | vacation / holiday / sick |
| source | TEXT NOT NULL DEFAULT 'manual' | manual / auto (keyword-detected from session content) |
| label | TEXT | Display label (e.g. "설 연휴") |
| created_at | INTEGER NOT NULL | |
On vacation days, out short-circuits delivery (no email sent). The UNIQUE on date prevents double-marking.
email_logs
Append-only audit of every email delivery attempt. Never deleted; grows unbounded (manual truncation if needed).
| Column | Type | Role |
|--------|------|------|
| id | INTEGER PK AUTOINCREMENT | |
| sent_at | INTEGER NOT NULL | Attempt ms |
| report_type | TEXT NOT NULL | daily / weekly / monthly |
| report_date | TEXT NOT NULL | YYYY-MM-DD of the report |
| period_from | TEXT NOT NULL | Window start (ISO date) |
| period_to | TEXT NOT NULL | Window end (ISO date) |
| recipient | TEXT NOT NULL | Delivered-to address |
| subject | TEXT NOT NULL | Rendered subject line |
| body_html | TEXT | Rendered HTML (nullable for failed sends) |
| body_text | TEXT | Plain-text fallback body |
| provider | TEXT NOT NULL | gmail-smtp / resend / gmail-mcp |
| status | TEXT NOT NULL DEFAULT 'sent' | sent / failed |
| error_message | TEXT | Error detail when status = 'failed' |
Indexes: idx_email_logs_sent (sent_at), idx_email_logs_report (report_date, report_type).
Read path (what the DB is actually used for)
| Command | Tables read | Purpose |
|---------|-------------|---------|
| sincenety (default) | sessions, daily_reports, vacations, email_logs, config | Full pipeline — gather → summarize → render → send |
| air | sessions, gather_reports | Phase 1 only — collect & store |
| circle | sessions, daily_reports | Phase 2 only — AI summarize + finalize |
| out / outd / outw / outm | daily_reports, email_logs, vacations, config | Phase 3 only — smart email send |
| report --date / --week / --month | daily_reports | Render stored summary to terminal |
| sync push | daily_reports, config | Upload own rows to D1 |
| sync pull | daily_reports, config | Download other machines' rows, merge |
| config | config | Show/edit settings |
| vacation | vacations | CRUD vacation days |
What is not supported (known gaps): full-text search over sessions.title/description, project-level aggregation view, timeline/heatmap queries. These are eligible candidates for future work — the data is already persisted, only read paths are missing.
Backup & recovery
- Not a backup target — the DB is derived from
~/.claude/. If lost, rerunsincenety --since "2026-04-01"to rebuild from source. - Exception:
daily_reports.summary_json(AI summaries) andemail_logsare not reconstructible from~/.claude/alone — they require re-running the LLM summarization, which costs tokens. These two tables are the only meaningful backup targets. Cloud sync to Cloudflare D1 serves as remote backup fordaily_reports. - Disaster recovery: delete
sincenety.db+sincenety.salt, reinstall, rerun. Historical email_logs and pre-LLM summaries are lost; session metadata is rebuilt from~/.claude/.
Tech Stack
| Component | Technology | |-----------|------------| | Language | TypeScript (ESM, Node16 modules) | | Runtime | Node.js >= 18 | | CLI | commander | | DB | sql.js (WASM SQLite, zero native deps) | | Encryption | Node.js built-in crypto (AES-256-GCM) | | Email | nodemailer (Gmail SMTP), Resend API | | Cloud | Cloudflare D1 REST API (native fetch, zero extra deps) | | AI Summarization | Cloudflare Workers AI (Qwen3-30B), zero extra deps | | Tests | vitest (128 cases across 11 test files) |
Development
npm install # Install dependencies
npm run build # Compile TypeScript (dist/)
npm run dev # Run with tsx (dev mode)
npm test # Run vitest tests (171 cases)
node dist/cli.js # Direct executionChangelog
v0.8.9 (2026-04-17) — Force-refresh scope reduced from this-week-and-month to this-week-only
Why
v0.8.8 made runCircle re-summarize the current week (Mon–today) + the current month (1st–today) on every invocation. In practice that meant the /sincenety default run printed twenty-plus ♻️ <date> re-summarizing lines each time and burned Workers AI calls on dailies that hadn't actually changed. User feedback was direct: "이건 너무 지금 길잖아, 토큰 소비도 많을테구" — and was followed by a real terminal log showing all of 2026-04-02 → 2026-04-09 being re-summarized at 2026-04-17.
Change
runCircle: forced-refresh date set is now this week only (forcedThisWeek), notforcedWeekMonth. The function-level comment and the variable name both reflect the narrower scope.circleJson(skill--jsonpath): same narrowing — skill clients now see "this week ∪ changed dates" instead of "this week ∪ this month ∪ changed dates".- Log line:
♻️ <date> re-summarizing (current week/month — forced refresh)→... (current week — forced refresh). - Removed dead code:
datesInCurrentMonthUpToTodayhelper deleted fromsrc/core/circle.ts(no remaining call sites). - Emailed-row protection unchanged: the
emailedAt != nullguard insideautoSummarizeis untouched, so already-sent dailies remain immune to overwrite. Stale (non-forced) dailies from previous weeks still get re-summarized on the existing freshness path; only the unconditional daily re-summary loop was narrowed. - Monthly summaries: no longer kept hot every run. They refresh via the skill at month-end or on demand (
circle --rerun YYYY-MM-DD).
Files changed
src/core/circle.ts—runCircleandcircleJsonscope reduction; removedatesInCurrentMonthUpToToday; log message + comment editssrc/cli.ts— version bump to 0.8.9package.json—0.8.8→0.8.9README.md/README.ko.md— changelog + cross-reference updates in body sections
Compatibility
- No DB schema change (still v4).
- No CLI flag changes; default behavior is just narrower in scope.
- No behavior change for
outd/outw/outm; their forced types still trigger their own paths. circle --rerun YYYY-MM-DDremains the explicit escape hatch when you need to re-summarize a specific older daily.
v0.8.8 (2026-04-17) — Heuristic weekly/monthly baseline removed + atomic DB write + renderer fix
Highlights
- Heuristic weekly/monthly baseline completely removed. The text-concatenation path (
summarizeRangeInto,autoSummarizeWeekly,autoSummarizeMonthly,mergeSummariesByTitle, and the daily-overviewtopics.join(", ")fallback) is deleted. Weekly/monthly reports are now generated only via the skill pathcircle --save --type <weekly|monthly>. CLI no longer invents summaries by concatenating outcomes/flows. - Renderer bug fixed: weekly/monthly no longer show only Monday/1st-of-month content. Previously,
renderDailyEmaillooked upgetGatherReportByDate(date)even for weekly/monthly — butdateis the Monday/first-of-month, so only that single day's gather got rendered, wiping out the aggregated content actually stored in the weekly/monthly row. NowgatherReportis consulted only whenreportType === "daily". - Atomic DB write.
SqlJsAdapter.save()previously usedwriteFile(dbPath, encrypted)which truncates-then-writes; a crash mid-write left a 0-byte DB that failed decryption on next launch (we lost a whole working DB to this on 2026-04-17). Now writes todbPath.tmp.<pid>and renames — atomic on the same filesystem. - First-run backfill: 90d → 7d.
determineRangedefaulted to 90 days when no checkpoint existed, which meant a fresh install / post-recovery launch ran Workers AI on a ~3-month history. The user's recovery session highlighted this waste — reduced to 7 days. out*fails loudly when the report row is missing or empty.outw/outmused to silently "skip" whenrenderDailyEmailreturned null. It now emits a precise error per type:weekly report row for <date> not found. Run /sincenety in Claude Code to generate a high-quality summary first.— making the skill contract explicit.- Every run re-summarizes the current week + current month. Per user direction,
runCirclenow adds this week (Monday–today) and this month (day 1–today) to the dates it force-summarizes, bypassing the freshness-skip logic (but still protectingemailedAt != nullrows).circle --jsonsimilarly always includes these ranges, so the skill always has fresh daily data to build weekly/monthly summaries from.
Removed symbols / config
src/core/circle.ts:summarizeRangeInto,autoSummarizeWeekly,autoSummarizeMonthly,mergeSummariesByTitle,normalizeTitle(unused after merge removal),MergedSummary(renamed toSessionSummary).src/core/out.ts:PIPELINE_MODES,PipelineMode,PIPELINE_MODE_CONFIG_KEY,isPipelineMode,resolvePipelineMode.OutOptions.modedropped.src/cli.ts:parseModeFlag,--modeflag onout/outd/outw/outm,--pipeline-modeonconfig(now a deprecation warning that does nothing).- Config key
pipeline_modeis no longer read anywhere; new installs will not have this key at all.
out* error contract (v0.8.8+)
When the weekly/monthly row is missing or summaryJson is empty, runOut now records an error entry (instead of skipped) and bumps result.errors. The entry message tells the user to run /sincenety to generate the summary first. cron detects this via the process exit code (already 1 when result.errors > 0). Daily remains on the old skipped path — a no-activity day is not an error.
circle behavior change
runCircle: computesforcedWeekMonth = (this week ∪ this month) ∩ (gather exists)and passes it both toallChanged(so new dates get summarized) and toautoSummarize's newforceDatesparameter (so existing-but-fresh dates still get re-summarized — previously they'd be skipped by the stale-only check).emailedAt != nulldates are still protected.circleJson: same expansion — skill clients always see the current week/month as "dates to process", even if no gather changes happened.- Daily
overviewno longer falls back to"<date> 작업: topic1, topic2, ..."when CloudflaregenerateOverviewreturns null. If the AI path fails,overviewstays null — the renderer handles missing overview gracefully. Consistent with v0.8.6's "no heuristic summaries ever" rule.
renderer fix
src/email/renderer.ts:
- const gatherReport = await storage.getGatherReportByDate(date);
+ // weekly/monthly는 기간 rollup이므로 per-day gather를 쓰면 안 된다
+ // (date가 월요일/1일이어서 그 하루치 gather만 잡혀 세션이 과소 표시됨).
+ const gatherReport =
+ reportType === "daily"
+ ? await storage.getGatherReportByDate(date)
+ : null;This was the direct cause of weekly reports showing 1 sessions, 4msg, 0Ktok in the subject line on Friday despite the weekly row containing 8 merged project summaries — renderer was serving Monday's single-session gather instead.
Atomic DB write
src/storage/sqljs-adapter.ts:
private async save(): Promise<void> {
if (!this.db) return;
const data = this.db.export();
const encrypted = encrypt(Buffer.from(data), this.encryptionKey);
- await writeFile(this.dbPath, encrypted, { mode: 0o600 });
+ // Atomic write: tmp → rename. writeFile() truncates-then-writes, so a
+ // mid-write crash leaves a 0-byte DB. rename() on same filesystem is atomic.
+ const tmpPath = `${this.dbPath}.tmp.${process.pid}`;
+ await writeFile(tmpPath, encrypted, { mode: 0o600 });
+ await rename(tmpPath, this.dbPath);
}Regression trigger: during a weekly resend on 2026-04-17, the send-path save() got interrupted mid-write and left ~/.sincenety/sincenety.db at 0 bytes. Every subsequent command printed "DB decryption failed". Config (including SMTP app password and D1 API token) had to be re-entered by hand, and 7 days of gather/daily data had to be re-collected and re-summarized. The atomic-write fix closes the hole.
First-run backfill reduction
src/core/air.ts:determineRange — when no checkpoint row exists, the default range is now 7 days (was 90). The old 90-day default made a cold start blow through a full quarter of Workers AI calls unnecessarily.
SKILL.md
- Rewrote the "pipeline mode" section to state that full/smart is gone and the heuristic baseline has been removed.
- Added a section on
outw/outmfailure modes, including the exact error strings and remediation. - Noted that
circle --jsonalways includes the current week/month — skill can rely on this for weekly/monthly re-summarization.
Files changed
src/core/circle.ts— delete heuristic baseline functions, remove daily overview fallback, add forceDates, expand circleJson rangesrc/core/out.ts— drop PipelineMode machinery, weekly/monthly empty-row error handlingsrc/core/air.ts— first-run backfill 90d → 7dsrc/cli.ts— remove--mode/--pipeline-modeflags, drop parseModeFlag, version bumpsrc/email/renderer.ts— gather lookup gated onreportType === "daily"src/storage/sqljs-adapter.ts— atomic write via tmp+renamesrc/skill/SKILL.md— rewrote pipeline/weekly/monthly sectionspackage.json— 0.8.7 → 0.8.8
v0.8.7 (2026-04-16) — Fix: autoSummarize now re-summarizes when new sessions arrive mid-day
Bug
autoSummarizeskipped re-summarization whendaily_reportsalready hadsummaryJson. Ifsincenetyran at 10am (summarizing sessions A, B) and ran again at 3pm (air detected new session C via data hash change), the second run'sautoSummarizesaw the existingsummaryJsonandcontinue'd — session C was never included in the daily summary. The user saw only the morning's sessions in the email.
Root cause
circle.ts:autoSummarize line 662-665 had:
const existingReport = await storage.getDailyReport(date, "daily");
if (existingReport?.summaryJson) continue;This check assumed that any existing summaryJson meant "this date is fully summarized." But air correctly updated gather_reports with the new session data (and a new updatedAt timestamp), making the daily_reports row stale — the freshness infrastructure (getDailyReportFreshness) already detected this, but autoSummarize never consulted it.
Fix
The skip logic now checks freshness before skipping:
- Emailed report (
emailedAt != null) → always skip (protect sent reports) - Fresh report (gather's
updatedAt≤ daily'screatedAt) → skip (no new data) - Stale report (gather updated after daily was created) → re-summarize with log
♻️ re-summarizing
This reuses the existing storage.getDailyReportFreshness() method (introduced in v0.8.4, previously only used for warning logs in out.ts).
Files changed
src/core/circle.ts:autoSummarize— replaced simplesummaryJsonexistence check with freshness-aware logicpackage.json: version bump 0.8.6 → 0.8.7src/cli.ts: version string updated
v0.8.6 (2026-04-16) — Heuristic summary fallback removed + AI-required pipeline guard + prompt hardening
Highlights
- Heuristic summary fallback completely removed. The
summarizeHeuristicfunction insrc/core/summarizer.ts(the regex-based "extract sentences from input/output and concatenate them") is deleted. It was the root cause of the long-standing complaint that "AI summary doesn't run; the output looks like my input text rewritten." The heuristic was a silent fallback that ran whenever the AI provider was unavailable, producing summaries that mirrored the user's prompt text instead of actual work performed. - Pipeline-wide AI-required guard. A new
assertAiReadyForCliPipeline()guard runs at the entry ofrunCircle(and again atautoSummarize). Ifai_provideris notcloudflareoranthropicwith valid credentials, the entiresincenetyprocess aborts immediately with a clear remediation message andprocess.exit(1)(cron-detectable). No email is sent, no false summary is written. ai_provider=claude-codeis now CLI-forbidden. This provider value is only valid inside the/sincenetyslash command (where Claude Code itself produces the summary externally and saves it viacircle --save). Runningsincenetyfrom a CLI/cron context with this provider now throws with a message explaining the constraint.- Prompt hardening for both AI paths. Cloudflare Workers AI and Anthropic prompts now strictly forbid echoing user prompt text into the summary. The prompt structure separates "user intent (context only)" from "assistant action/artifact (summary target)", and instructs the model to write in 3rd-person observer voice focused on what was produced/changed/decided.
Root cause of "AI summary not running" symptom
Before v0.8.6, src/core/summarizer.ts:279 had a switch that handled cloudflare and anthropic providers but fell through to the heuristic branch for claude-code and unset providers. Combined with summarizer.ts:141's catch {} silently swallowing all Anthropic API errors, any failure mode (wrong provider config, network error, auth failure) would silently degrade to the regex-based input-text echo. From the user's perspective: "I configured AI but the email looks like my input was just reformatted." That was literally what happened — the regex just sliced first sentences out of userInput and joined them with arrows.
A second contamination path: src/core/gatherer.ts was also calling summarizeSessions on every gather to fill in session titles, even though the real summary happens later in autoSummarize. That call is now removed.
Core changes
src/core/summarizer.ts:- Deleted:
summarizeHeuristic()function (~90 lines of regex extraction logic). - New:
AiUnavailableErrorexception class for callers to catch and abort. summarizeSession()now returnsPromise<SessionSummary>(non-nullable) and throws on any failure.- Removed silent
catch {}insummarizeWithClaude— Anthropic SDK errors now propagate (auth, rate limit, network). Same for empty-response and JSON-parse failures. claude-codeprovider explicitly throws with message indicating slash-command-only usage.- Heuristic / unset provider explicitly throws with remediation hint.
- Prompt restructured: turn format is
[턴 N] 사용자 의도(맥락): ... / 어시스턴트 수행/산출물: ...— separating intent from action. Output rules forbid quoting user utterances and require 3rd-person voice.
- Deleted:
src/core/ai-provider.ts:- New:
assertAiReadyForCliPipeline(storage)— single source of truth for the pipeline-entry check. Cleanly rejectsclaude-code(CLI context),heuristic(no provider), andcloudflare/anthropicwithout credentials.
- New:
src/core/circle.ts:autoSummarize:- Calls the guard at the top — throws before any DB read if AI is not ready.
- Per-session AI failure is no longer swallowed by the per-date
try/catch. The catch block was removed; AI failures propagate up to the caller, aborting the wholeautoSummarizecall. Partial summaries are never written todaily_reports. - Cloudflare
cfSummarizereturning null is now treated as failure → throw (was: silent skip). - The cross-device D1 pull
try/catchis preserved (network flakiness is expected and orthogonal to summary correctness), but its silentcatchblocks now log warnings with the actual error message.
src/core/circle.ts:runCircle:- Calls
assertAiReadyForCliPipeline(storage)immediately before invokingautoSummarize(only on the CLI path —--json/--save/--skipAutoSummarizepaths bypass since they're for the slash-command flow).
- Calls
src/core/gatherer.ts:- Removed the
summarizeSessionscall.gather_reportsrows now contain only raw session metadata (title froms.title ?? s.summarywith stripped XML tags). ThewrapUpfield on the gather report is removed since it was filled by the heuristic. AI-generated wrapUp lives only indaily_reports.summary_jsongoing forward.
- Removed the
src/cloud/cf-ai.ts:- Prompt rewrite mirrors
summarizer.ts: turn format separates intent/action, system prompt enumerates 5 strict output rules (no user-quote, 3rd-person, integrate intent+artifact, ignore filler responses, JSON only). - Silent
catch { return null }removed — HTTP errors, missing content, and JSON parse failures all throw with descriptive messages socircle.autoSummarizecan abort the pipeline.
- Prompt rewrite mirrors
Test changes
tests/cf-ai.test.ts: "should handle API errors gracefully" test renamed and rewritten — now assertsrejects.toThrow(/Workers AI HTTP 401/)instead ofexpect(result).toBeNull(). Reflects the new contract: silent null returns are forbidden.tests/circle.test.ts: 4runCircle — summaryErrors propagationtests now seedai_provider=cloudflare+ dummy D1 credentials so they pass the guard. 2 new tests added:runCircle throws when AI provider is unconfigured (full pipeline halt)claude-code provider throws on CLI path with slash-command guidance
- 173/173 tests passing (was 171 → +2 guard tests, no removals).
Verification
- Reproduction of the user's symptom (forensic): inspecting
daily_reportsfor 2026-04-14 showedtopic = "오늘 cli 환경에서 실행한거 skip 되었던데, 내 홈 위치 실행했어(~/), 왜 sk…"andflow = "오늘 cli 환경에서 실행한거 skip 되었던데, 내… → cli 에서 어제 날짜기준 정리하려면 어떻게 명령어 …"— verbatim user prompt text echoed by the heuristic. Confirmed root cause. - Cloudflare AI smoke test (isolated): direct
summarizeSessioncall with the exact "echoed" user input now producestopic: "skill 자동 호출 문제 해결"andoutcomedescribing the actual code changes (file paths, version bump, install logic) in 3rd person. Zero user-prompt characters appear in the output. - Guard test (isolated):
summarizeSessionwithai_provider=claude-codethrowsAiUnavailableErrorwith the slash-command hint as expected. - End-to-end CLI test:
node dist/cli.js outwithai_provider=claude-code(the bad config) now prints the clear Korean error message, sends 0 emails, exits with code 1 (verified directly). - Forced regeneration of historical rows: 2026-04-14 (6 sessions, 5 after merge) and 2026-04-15 (1 session) regenerated through
autoSummarize. All session topics/flows are now action-noun phrases with no user-prompt content; overviews integrate the day's work coherently.
Migration impact
- Users with
ai_provider=claude-codewho runsincenetyfrom cron/CLI (not via the slash command): the cron will start failing with exit 1 and a clear message. Switch viasincenety config --ai-provider cloudflare(uses existing D1 token) orsincenety config --ai-provider anthropic+ setANTHROPIC_API_KEY. - Users with no AI configured: same — the pipeline now refuses to run rather than emailing useless heuristic output. Configure cloudflare or anthropic.
- Existing
daily_reportsrows generated by the heuristic are not auto-corrected (theiremailedAt != nullguard still protects them from overwrite). To regenerate manually: clearsummaryJsonandemailedAton the affected row, then re-runcircleor callautoSummarizedirectly.
v0.8.5 (2026-04-15) — Auto-install Claude Code skill on npm install -g
Highlights
- Fixes "
/sincenetynot listed on other machines": Before v0.8.5,npm install -g sincenetyonly installed the CLI binary — the Claude Code skill at~/.claude/skills/sincenety/SKILL.mdwas never created, so the slash command did not show up after install on a fresh machine. The skill only existed on the original development machine where it had been placed manually. - **Root cause (two bugs co
