npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sincenety

v0.8.9

Published

Claude Code 작업 갈무리 도구 — 작업 이력 자동 분석 및 구조화된 기록 생성

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.

한국어 문서 (Korean) | Sample Report | CLI Report (Workers AI)

$ 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-07

Features

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:

  1. 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)
    • --json outputs per-date JSON
  2. sincenety circle — LLM-powered summaries

    • Internally runs air first
    • --json: outputs session data for AI summary (SKILL.md integration)
    • --save: saves stdin JSON to daily_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 projectName are individually summarized, then consolidated into a single merged summary per project — eliminates duplicate entries and improves report coherence
  3. sincenety out — Smart email delivery

    • out: 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
  • circle auto-summarizes when ai_provider is cloudflare: per-session topic/outcome/flow/significance + daily overview
  • circle --json --summarize: Workers AI summaries included in JSON output (requires ai_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_reports wrapUp 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:

  1. D1 Cloud Sync — Cloudflare API token (enables Workers AI + cloud sync)
  2. 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/apppasswords

All 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 registrationconfig --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; out skips vacation days automatically

Config Setup Wizard

Run sincenety config --setup for an interactive 3-choice wizard:

  1. Gmail SMTP (with app password URL guidance)
  2. Resend API
  3. 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 sync pushes local data to a central D1 database (push / pull-config / status / init)
  • Auto-sync after out completes (non-fatal — network errors don't block email delivery)
  • Shared config: SMTP settings set once, sync --pull-config on new machines to pull shared config
  • Machine ID: hardware-based auto-detection (see below), config --machine-name override for custom identification
  • Zero new dependencies: uses native fetch for 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|monthly from the /sincenety skill. 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 / outm error contract: if the target row is missing or has an empty sessions array, runOut emits 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): runCircle forces a re-summary of the current week (Mon–today) dailies every run, even when gather_reports.data_hash is 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 != null still protects daily/weekly/monthly rows from overwrite, even under the forced-refresh rule above.
  • Removed config/flags: pipeline_mode config key and --mode / --pipeline-mode CLI flags are gone. The --pipeline-mode flag 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: out always sends email regardless of whether another device already sent — no skip, no dedup
  • Session merge by topic: sessions with identical projectName + title are 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

  1. Go to dash.cloudflare.com/profile/api-tokens
  2. "Create Token""Custom token" (click "Get started" at the bottom)
  3. 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-token setup cannot find your account.

  1. Account Resources → Include → select your account
  2. "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 complete

Auto 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 (machines table)

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:

  1. Cloudflare D1 API Tokendash.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 |

  2. Resend API Keyresend.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 sincenety

Option B: Global install (recommended for daily use)

npm install -g sincenety@latest

The 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 SMTP

After setup, just run:

sincenety

Note: 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 link

Verify 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 instructions

air — 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 --json

circle — 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.json

config — 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-10

Generate 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 8

sync — 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 status

Claude Code Skill (/sincenety)

Use /sincenety directly inside Claude Code sessions for AI-powered daily reports.

Installation

  1. Install the CLI (provides the data collection engine):
npm install -g sincenety@latest
  1. Install the skill (registers /sincenety command in Claude Code):
mkdir -p ~/.claude/skills/sincenety
cp node_modules/sincenety/src/skill/SKILL.md ~/.claude/skills/sincenety/SKILL.md

Or if installed globally:

mkdir -p ~/.claude/skills/sincenety
cp "$(npm root -g)/sincenety/src/skill/SKILL.md" ~/.claude/skills/sincenety/SKILL.md
  1. Update to latest version:

Inside Claude Code, run:

! npm install -g sincenety@latest

How it works

When you type /sincenety inside Claude Code:

  1. Data collectionair collects all sessions with checkpoint-based backfill
  2. JSON outputcircle --json outputs session data with conversation turns
  3. AI summary — Claude Code itself analyzes and generates topic/outcome/flow/significance
  4. Save to DBcircle --save writes summary to daily_reports
  5. 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 password

Generate 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.json

Install 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 skipped

Encryption

  • 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:

  1. Idempotency boundarysincenety is 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) on sessions and the UNIQUE(report_date, report_type) on daily_reports make 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".

  2. Send-state authoritydaily_reports.emailed_at is the single source of truth for "was this report already delivered?" Throughout autoSummarize and circleSave (circle.ts), rows with emailedAt != null are 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_logs is the append-only audit trail: every successful and failed send lands there with subject, recipient, provider, and error message.

  3. Cross-device merge pivotsync push (pre-send) uploads this machine's daily_reports rows to Cloudflare D1; sync pull downloads 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 attribution

Encryption 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 ∥ salt by 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, rerun sincenety --since "2026-04-01" to rebuild from source.
  • Exception: daily_reports.summary_json (AI summaries) and email_logs are 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 for daily_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 execution

Changelog

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), not forcedWeekMonth. The function-level comment and the variable name both reflect the narrower scope.
  • circleJson (skill --json path): 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: datesInCurrentMonthUpToToday helper deleted from src/core/circle.ts (no remaining call sites).
  • Emailed-row protection unchanged: the emailedAt != null guard inside autoSummarize is 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.tsrunCircle and circleJson scope reduction; remove datesInCurrentMonthUpToToday; log message + comment edits
  • src/cli.ts — version bump to 0.8.9
  • package.json0.8.80.8.9
  • README.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-DD remains 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-overview topics.join(", ") fallback) is deleted. Weekly/monthly reports are now generated only via the skill path circle --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, renderDailyEmail looked up getGatherReportByDate(date) even for weekly/monthly — but date is 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. Now gatherReport is consulted only when reportType === "daily".
  • Atomic DB write. SqlJsAdapter.save() previously used writeFile(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 to dbPath.tmp.<pid> and renames — atomic on the same filesystem.
  • First-run backfill: 90d → 7d. determineRange defaulted 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/outm used to silently "skip" when renderDailyEmail returned 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, runCircle now adds this week (Monday–today) and this month (day 1–today) to the dates it force-summarizes, bypassing the freshness-skip logic (but still protecting emailedAt != null rows). circle --json similarly 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 to SessionSummary).
  • src/core/out.ts: PIPELINE_MODES, PipelineMode, PIPELINE_MODE_CONFIG_KEY, isPipelineMode, resolvePipelineMode. OutOptions.mode dropped.
  • src/cli.ts: parseModeFlag, --mode flag on out/outd/outw/outm, --pipeline-mode on config (now a deprecation warning that does nothing).
  • Config key pipeline_mode is 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: computes forcedWeekMonth = (this week ∪ this month) ∩ (gather exists) and passes it both to allChanged (so new dates get summarized) and to autoSummarize's new forceDates parameter (so existing-but-fresh dates still get re-summarized — previously they'd be skipped by the stale-only check). emailedAt != null dates 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 overview no longer falls back to "<date> 작업: topic1, topic2, ..." when Cloudflare generateOverview returns null. If the AI path fails, overview stays 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/outm failure modes, including the exact error strings and remediation.
  • Noted that circle --json always 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 range
  • src/core/out.ts — drop PipelineMode machinery, weekly/monthly empty-row error handling
  • src/core/air.ts — first-run backfill 90d → 7d
  • src/cli.ts — remove --mode / --pipeline-mode flags, drop parseModeFlag, version bump
  • src/email/renderer.ts — gather lookup gated on reportType === "daily"
  • src/storage/sqljs-adapter.ts — atomic write via tmp+rename
  • src/skill/SKILL.md — rewrote pipeline/weekly/monthly sections
  • package.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

  • autoSummarize skipped re-summarization when daily_reports already had summaryJson. If sincenety ran at 10am (summarizing sessions A, B) and ran again at 3pm (air detected new session C via data hash change), the second run's autoSummarize saw the existing summaryJson and continue'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:

  1. Emailed report (emailedAt != null) → always skip (protect sent reports)
  2. Fresh report (gather's updatedAt ≤ daily's createdAt) → skip (no new data)
  3. 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 simple summaryJson existence check with freshness-aware logic
  • package.json: version bump 0.8.6 → 0.8.7
  • src/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 summarizeHeuristic function in src/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 of runCircle (and again at autoSummarize). If ai_provider is not cloudflare or anthropic with valid credentials, the entire sincenety process aborts immediately with a clear remediation message and process.exit(1) (cron-detectable). No email is sent, no false summary is written.
  • ai_provider=claude-code is now CLI-forbidden. This provider value is only valid inside the /sincenety slash command (where Claude Code itself produces the summary externally and saves it via circle --save). Running sincenety from 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: AiUnavailableError exception class for callers to catch and abort.
    • summarizeSession() now returns Promise<SessionSummary> (non-nullable) and throws on any failure.
    • Removed silent catch {} in summarizeWithClaude — Anthropic SDK errors now propagate (auth, rate limit, network). Same for empty-response and JSON-parse failures.
    • claude-code provider 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.
  • src/core/ai-provider.ts:

    • New: assertAiReadyForCliPipeline(storage) — single source of truth for the pipeline-entry check. Cleanly rejects claude-code (CLI context), heuristic (no provider), and cloudflare/anthropic without credentials.
  • 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 whole autoSummarize call. Partial summaries are never written to daily_reports.
    • Cloudflare cfSummarize returning null is now treated as failure → throw (was: silent skip).
    • The cross-device D1 pull try/catch is preserved (network flakiness is expected and orthogonal to summary correctness), but its silent catch blocks now log warnings with the actual error message.
  • src/core/circle.ts:runCircle:

    • Calls assertAiReadyForCliPipeline(storage) immediately before invoking autoSummarize (only on the CLI path — --json/--save/--skipAutoSummarize paths bypass since they're for the slash-command flow).
  • src/core/gatherer.ts:

    • Removed the summarizeSessions call. gather_reports rows now contain only raw session metadata (title from s.title ?? s.summary with stripped XML tags). The wrapUp field on the gather report is removed since it was filled by the heuristic. AI-generated wrapUp lives only in daily_reports.summary_json going forward.
  • 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 so circle.autoSummarize can abort the pipeline.

Test changes

  • tests/cf-ai.test.ts: "should handle API errors gracefully" test renamed and rewritten — now asserts rejects.toThrow(/Workers AI HTTP 401/) instead of expect(result).toBeNull(). Reflects the new contract: silent null returns are forbidden.
  • tests/circle.test.ts: 4 runCircle — summaryErrors propagation tests now seed ai_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

  1. Reproduction of the user's symptom (forensic): inspecting daily_reports for 2026-04-14 showed topic = "오늘 cli 환경에서 실행한거 skip 되었던데, 내 홈 위치 실행했어(~/), 왜 sk…" and flow = "오늘 cli 환경에서 실행한거 skip 되었던데, 내… → cli 에서 어제 날짜기준 정리하려면 어떻게 명령어 …" — verbatim user prompt text echoed by the heuristic. Confirmed root cause.
  2. Cloudflare AI smoke test (isolated): direct summarizeSession call with the exact "echoed" user input now produces topic: "skill 자동 호출 문제 해결" and outcome describing the actual code changes (file paths, version bump, install logic) in 3rd person. Zero user-prompt characters appear in the output.
  3. Guard test (isolated): summarizeSession with ai_provider=claude-code throws AiUnavailableError with the slash-command hint as expected.
  4. End-to-end CLI test: node dist/cli.js out with ai_provider=claude-code (the bad config) now prints the clear Korean error message, sends 0 emails, exits with code 1 (verified directly).
  5. 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-code who run sincenety from cron/CLI (not via the slash command): the cron will start failing with exit 1 and a clear message. Switch via sincenety config --ai-provider cloudflare (uses existing D1 token) or sincenety config --ai-provider anthropic + set ANTHROPIC_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_reports rows generated by the heuristic are not auto-corrected (their emailedAt != null guard still protects them from overwrite). To regenerate manually: clear summaryJson and emailedAt on the affected row, then re-run circle or call autoSummarize directly.

v0.8.5 (2026-04-15) — Auto-install Claude Code skill on npm install -g

Highlights

  • Fixes "/sincenety not listed on other machines": Before v0.8.5, npm install -g sincenety only installed the CLI binary — the Claude Code skill at ~/.claude/skills/sincenety/SKILL.md was 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