Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8ab6fb5
fix: restore approved monkey pet
oyeong011 May 3, 2026
8ba7ed8
feat: add expanded animal sprite set
oyeong011 May 4, 2026
14dbca9
fix: restore template menu bar icon tint
oyeong011 May 4, 2026
0933f00
feat: add evening review reschedule recommendations
oyeong011 May 4, 2026
b14a6c8
release: v0.4.67 appcast entry
github-actions[bot] May 4, 2026
66c5ac0
release: v0.4.68
oyeong011 May 5, 2026
07fe449
release: v0.4.68 appcast entry
github-actions[bot] May 5, 2026
c8435e9
fix: auto-recover Google Calendar delete on transient failures
oyeong011 May 23, 2026
97fccbf
feat: evening review notification flow + auto-apply opt-in toggle
oyeong011 May 23, 2026
15cd1dc
chore: add CLI re-auth helper script + OAuth troubleshooting doc
oyeong011 May 23, 2026
2658b88
chore: ignore tool cache dirs (.lazyweb, .playwright-mcp)
oyeong011 May 27, 2026
45b53ff
fix(ios): keep tokens on transient OAuth refresh errors
oyeong011 May 27, 2026
f7e3232
refactor(macos): lazy CLI resolution + PATH allowlist for AIService
oyeong011 May 27, 2026
157270c
feat: calendar UX overhaul (shared completion model + iOS interaction)
oyeong011 May 27, 2026
6fd4a58
docs: refresh landing page (hero mockup + copy reorganization)
oyeong011 May 27, 2026
77f227c
fix(build): re-sign Sparkle when ad-hoc signing main bundle
oyeong011 May 27, 2026
3a2207e
perf(macos): cache date formatters in GoogleCalendarService
oyeong011 May 30, 2026
ca795ca
fix(macos): include todos in AI chat context
oyeong011 May 30, 2026
46c851c
chore: 로컬 작업분 백업 커밋 (iOS/공유 모듈 변경, 테스트, 랜딩 자료)
oyeong011 Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 0 additions & 1 deletion .claude/scheduled_tasks.lock

This file was deleted.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ Planit/Services/BundledCredentials.local.swift
CaleniOS/CaleniOS.xcodeproj/
# Claude Code worktree 임시 디렉터리 — submodule로 인식돼 Pages 빌드 깨뜨림
.claude/worktrees/

# 개발 보조 도구 캐시/세션 디렉터리
.lazyweb/
.playwright-mcp/
139 changes: 96 additions & 43 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,96 @@
# Repository Guidelines

## Project Structure & Module Organization

This is a Swift Package for a macOS menu bar app. The package target is `Calen`, while application sources live under `Planit/`.

- `Planit/PlanitApp.swift` contains the app entry point and menu bar popover setup.
- `Planit/Views/`, `Planit/ViewModels/`, `Planit/Models/`, and `Planit/Services/` hold SwiftUI UI, state, domain models, and integrations.
- `Planit/Resources/` stores localized `Localizable.strings`, app icon, and privacy manifest.
- `Tests/` contains Swift Testing unit tests.
- `scripts/build-app.sh` builds release artifacts.

## Build, Test, and Development Commands

- `swift build` builds the debug Swift package.
- `swift run Calen` launches the app from SwiftPM for local development.
- `swift test` runs the `CalenTests` test target.
- `swift build -c release` builds an optimized binary.
- `scripts/build-app.sh 1.0.0` creates a universal `.app`, `.zip`, and `.dmg` under `.build/release`.

Use Xcode only when you need signing, entitlements, or UI debugging. Generated outputs in `.build/`, `.swiftpm/`, `DerivedData/`, and `xcuserdata/` should stay uncommitted.

## Coding Style & Naming Conventions

Use Swift 5.9 conventions with 4-space indentation. Keep types in `UpperCamelCase`, functions and properties in `lowerCamelCase`, and file names aligned with the primary type when practical. Prefer `struct` for values and `final class` for observable reference types. Keep UI in `Views`, long-lived state in `ViewModels`, and external integrations in `Services`.

There is no dedicated formatter configuration in this repo; preserve the existing Xcode/SwiftPM style and organize larger files with `// MARK:` sections.

## Testing Guidelines

Tests use the Swift Testing framework (`import Testing`) with `@Test` functions and `#expect` assertions. Add tests in `Tests/` with behavior-focused names such as `cliDetection_rejectedCommands`. When app modules are hard to import, keep tests around extracted pure logic or small mirrored helpers.

Run `swift test` before submitting changes. Add regression tests for bug fixes and security-sensitive logic.

## Commit & Pull Request Guidelines

Recent commits use concise, imperative summaries such as `Add Google Calendar OAuth integration...` or version-prefixed release notes like `v1.0.0: ...`. Keep commit subjects specific and mention the main feature or fix.

Pull requests should include a short description, verification commands run, linked issues when available, and screenshots or screen recordings for UI changes.

## Security & Configuration Tips

Do not commit credentials. `google_credentials.json`, `*credentials*.json`, `.omx/`, and build artifacts are ignored. Keep OAuth, Keychain, entitlements, and notification changes tightly scoped and document any required local setup.
# PROJECT KNOWLEDGE BASE

**Generated:** 2026-07-01T02:53:56Z
**Commit:** `ca795ca`
**Branch:** `feature/screen-time-integration`

## OVERVIEW

Calen is a Swift 5.9 calendar app repo with three live surfaces: macOS menu bar app (`Calen`), platform-neutral shared library (`CalenShared`), and iOS companion app/widget (`CaleniOS`). The repo is a hybrid SwiftPM plus XcodeGen workspace; do not assume a single canonical `Sources/` layout.

## STRUCTURE

```text
./
|-- Package.swift # SwiftPM target graph and resource bundling
|-- Planit/ # macOS executable target path for `Calen`
|-- Shared/ # non-target wrapper; child guidance lives here
|-- CaleniOS/ # iOS app/widget plus XcodeGen project spec
|-- Tests/ # Swift Testing target `CalenTests`
|-- scripts/ # build, release, signing, localization checks
|-- docs/ # public docs, Sparkle appcasts, OAuth evidence
|-- marketing/ # non-build assets/copy
`-- submission/ # non-build presentation/submission artifacts
```

Do not add guidance files inside SwiftPM target paths such as `Planit/` or `Shared/Sources/CalenShared/`; stray markdown can become package input noise.

## WHERE TO LOOK

| Task | Location | Notes |
| --- | --- | --- |
| macOS boot/menu bar | `Planit/PlanitApp.swift` | `@main`, `NSStatusItem`, popover, updater hooks |
| macOS app state | `Planit/ViewModels/CalendarViewModel.swift` | central state/sync owner, largest source hotspot |
| macOS UI composition | `Planit/Views/MainView.swift` | service wiring plus nested calendar/detail views |
| macOS Google auth | `Planit/Services/GoogleAuthManager.swift` | loopback OAuth, PKCE, Keychain tokens |
| macOS calendar API | `Planit/Services/GoogleCalendarService.swift` | concrete Google Calendar integration |
| shared planning | `Shared/Sources/CalenShared/Planning/PlanningOrchestrator.swift` | pure parse/validate/generate path |
| shared REST client | `Shared/Sources/CalenShared/Networking/GoogleCalendarClient.swift` | used by iOS repository and tests |
| iOS app shell | `CaleniOS/Sources/CaleniOSApp.swift` | SwiftData stack, env services, placeholder main |
| iOS project generation | `CaleniOS/project.yml` | XcodeGen source of truth for app and widget |
| widget contract | `CaleniOS/Sources/Services/WidgetDataPublisher.swift`, `CaleniOS/Widget/` | mirrored App Group payload schema |
| packaging | `scripts/build-app.sh`, `.github/workflows/release.yml` | universal binary, Sparkle, signing, notarization |
| localization/privacy checks | `Tests/LocalizationQualityTests.swift`, `Tests/MacPrivacyEntitlementTests.swift` | packaging invariants are tested |

## CODE MAP

| Symbol | Type | Location | Refs | Role |
| --- | --- | --- | --- | --- |
| `PlanitApp` | `App` | `Planit/PlanitApp.swift:9` | entry | macOS status-bar app boot |
| `AppDelegate` | class | `Planit/PlanitApp.swift:24` | entry | popover/menu/updater lifecycle |
| `MainCalendarView` | `View` | `Planit/Views/MainView.swift:31` | high | macOS service graph and root UI |
| `CalendarViewModel` | class | `Planit/ViewModels/CalendarViewModel.swift:20` | high | app state, sync, CRUD hub |
| `GoogleAuthManager` | class | `Planit/Services/GoogleAuthManager.swift:49` | medium | OAuth credentials and token lifecycle |
| `PlanningOrchestrator` | class | `Shared/Sources/CalenShared/Planning/PlanningOrchestrator.swift:159` | medium | shared AI planning validation |
| `GoogleCalendarClient` | class | `Shared/Sources/CalenShared/Networking/GoogleCalendarClient.swift:28` | medium | shared Google REST boundary |
| `CaleniOSApp` | `App` | `CaleniOS/Sources/CaleniOSApp.swift:23` | entry | iOS lifecycle and SwiftData setup |
| `RootView` | `View` | `CaleniOS/Sources/App/RootView.swift:10` | entry | routes directly to `MainTabView` |
| `WidgetDataProvider` | enum | `CaleniOS/Widget/WidgetDataProvider.swift:14` | widget | reads App Group event snapshots |

## CONVENTIONS

- `Package.swift` uses nonstandard target paths: macOS app in `Planit/`, shared library in `Shared/Sources/CalenShared`, iOS executable in `CaleniOS/Sources`.
- Keep platform-only frameworks out of `CalenShared`: no AppKit, UIKit, EventKit, Sparkle, or WidgetKit there.
- Keep long Swift files organized with `// MARK:`. Current hotspots include `CalendarViewModel.swift`, `MainView.swift`, `ReviewView.swift`, `AIService.swift`, and iOS `SettingsView.swift`.
- Tests use Swift Testing (`import Testing`, `@Test`, `#expect`). Favor pure logic, parser, layout, request/response, localization, and packaging invariant tests.
- Localization base is `Planit/Resources/ko.lproj/Localizable.strings`; `scripts/verify-localizations.sh` and `LocalizationQualityTests` both enforce resource quality.
- Use Xcode only for signing, entitlements, UI debugging, or generated iOS project workflows.

## ANTI-PATTERNS (THIS PROJECT)

- Do not commit credentials: `google_credentials.json`, `*credentials*.json`, and `Planit/Services/BundledCredentials.local.swift` stay local/secret-generated.
- Do not commit generated outputs: `.build/`, `.swiftpm/`, `DerivedData/`, `xcuserdata/`, `CaleniOS/CaleniOS.xcodeproj/`, `.omx/`, `.lazyweb/`, `.playwright-mcp/`.
- Do not bundle entitlement plist files as app resources; packaging scripts explicitly remove them.
- Do not hand-edit generated iOS project files; change `CaleniOS/project.yml` and regenerate.
- Do not break the widget payload mirror between app-side publisher and widget-side snapshot/provider.

## COMMANDS

```bash
swift build
swift run Calen
swift test
swift build -c release
bash scripts/build-app.sh 1.0.0
bash scripts/build-ios-app.sh 0.1.0
bash scripts/verify-localizations.sh
make dev
make ci-ios
```

## NOTES

- `scripts/build-app.sh` intentionally builds `arm64` and `x86_64` separately, merges with `lipo`, embeds Sparkle, adds `@executable_path/../Frameworks` rpath, and signs Sparkle inside-out.
- Release signing/notarization needs Developer ID and notarization env vars; CI injects bundled OAuth credentials from GitHub secrets.
- Existing generated/vendor noise under `CaleniOS/build/DerivedData/SourcePackages/checkouts/Sparkle/` is not repo source.
- The current test suite is broad but heterogeneous; `Tests/CalenTests.swift` is a maintenance hotspot, while newer suites are more focused.
66 changes: 66 additions & 0 deletions CaleniOS/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CALENIOS KNOWLEDGE BASE

**Scope:** `/Users/oy/Projects/Planit/CaleniOS`
**Generated:** 2026-07-01T02:53:56Z

## OVERVIEW

`CaleniOS/` is the iOS companion app plus widget surface. It depends on `CalenShared`, uses XcodeGen for app/widget project generation, and has iOS-only SwiftData, CloudKit, App Group, OAuth, theme, language, and widget code.

## STRUCTURE

```text
CaleniOS/
|-- project.yml # XcodeGen source of truth
|-- BUILD.md # local IPA/archive guide
|-- Sources/
| |-- CaleniOSApp.swift # iOS app entry, SwiftData container
| |-- App/ # RootView, MainTabView, AppState
| |-- Models/ # iOS wrappers/re-exports
| |-- Services/ # iOS auth, memory, widget publisher
| |-- Views/ # iOS SwiftUI screens
| `-- Resources/ # iOS entitlements/localizations
`-- Widget/ # WidgetKit extension and App Group reader
```

## WHERE TO LOOK

| Task | Location | Notes |
| --- | --- | --- |
| generated project config | `project.yml` | app, widget, entitlements, package reference |
| iOS build flow | `BUILD.md`, `../scripts/build-ios-app.sh` | `xcodegen`, archive, export IPA |
| app entry | `Sources/CaleniOSApp.swift` | SwiftData setup and non-iOS placeholder main |
| navigation shell | `Sources/App/MainTabView.swift`, `Sources/App/AppState.swift` | tab routing and transient nav state |
| Google auth | `Sources/Services/iOSGoogleAuthManager.swift` | `ASWebAuthenticationSession`, Keychain, callback URL |
| Google data | `Sources/Services/GoogleCalendarRepository.swift`, `Sources/Views/HomeViewModel.swift` | iOS repository/view-model bridge |
| shared memory | `Sources/Services/iOSMemoryFetcher.swift` | implements `CalenShared.MemoryFetching` |
| widget write path | `Sources/Services/WidgetDataPublisher.swift` | app-side App Group payload |
| widget read path | `Widget/EventSnapshot.swift`, `Widget/WidgetDataProvider.swift` | mirrored schema; keep compatible |

## CONVENTIONS

- Change `project.yml` for project, plist, entitlement, widget, bundle ID, App Group, and package-reference behavior; generated `.xcodeproj` is not source.
- Preserve `createIntermediateGroups: false` and `schemePathPrefix: ../`; they avoid XcodeGen/local-package collisions.
- Keep `Sources/Info.plist` and entitlements aligned with `project.yml`; check comments before editing generated-owner fields.
- Keep iOS-only app code under `Sources/` and widget-only code under `Widget/`.
- Use `#if os(iOS)` carefully. The placeholder `@main` exists so SwiftPM can compile the executable target off iOS.
- Widget payload schema is duplicated by design; update publisher, `EventSnapshot`, provider, and tests together.

## ANTI-PATTERNS

- Do not manually edit or commit `CaleniOS/CaleniOS.xcodeproj`; regenerate it with XcodeGen.
- Do not route widget data through app-only types unless the widget target can compile them.
- Do not replace custom CKRecord read-only memory sync with SwiftData automatic CloudKit sync without a full schema/release plan.
- Do not remove App Group or CloudKit entitlements casually; app, widget, Keychain, and memory sync depend on them.
- Do not put macOS-only services, AppKit, Sparkle, or menu bar assumptions in this subtree.

## COMMANDS

```bash
brew install xcodegen
(cd CaleniOS && xcodegen generate)
xcodebuild -project CaleniOS/CaleniOS.xcodeproj -scheme CaleniOS -destination 'generic/platform=iOS Simulator' build
bash scripts/build-ios-app.sh 0.1.0
```

Run the script from repo root. It creates `.build/ios-archive/CaleniOS-<VERSION>.xcarchive` and `.build/ios-ipa/CaleniOS.ipa`.
7 changes: 7 additions & 0 deletions CaleniOS/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ iOS Calen 앱(`com.oy.planit.ios`)의 로컬 .ipa 빌드 파이프라인.
scripts/build-ios-app.sh 0.1.0
```

```bash
bash scripts/verify-ios-release-readiness.sh
```

위 명령은 릴리즈 전 `xcodegen`/번들 ID/앱그룹/iCloud entitlements/`DEVELOPMENT_TEAM` 가용성을 점검하고,
TestFlight 업로드 갭(현재 수동 업로드)까지 함께 점검한다.

스크립트가 수행하는 단계:
1. `xcodegen generate` — `CaleniOS/CaleniOS.xcodeproj` 를 `CaleniOS/project.yml` 로부터 동적 생성.
2. `xcodebuild archive` — Release 구성으로 generic iOS 아카이브 생성.
Expand Down
32 changes: 14 additions & 18 deletions CaleniOS/Sources/App/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,24 @@ struct MainTabView: View {
@EnvironmentObject private var theme: iOSThemeService

var body: some View {
ZStack(alignment: .bottom) {
// MARK: Content Area
Group {
switch appState.selectedTab {
case .today:
HomeView()
case .chat:
ChatTabView()
case .review:
ReviewTabView()
case .profile:
SettingsView()
}
Group {
switch appState.selectedTab {
case .today:
HomeView()
case .chat:
ChatTabView()
case .review:
ReviewTabView()
case .profile:
SettingsView()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.bottom, 90)

// MARK: Custom Tab Bar
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.safeAreaInset(edge: .bottom, spacing: 0) {
CustomTabBar(selectedTab: $appState.selectedTab)
.background(Color.calenCream.ignoresSafeArea(edges: .bottom))
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(.bottom)
}
}

Expand Down
116 changes: 116 additions & 0 deletions CaleniOS/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"images": [
{
"filename": "appicon-40.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
},
{
"filename": "appicon-60.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
},
{
"filename": "appicon-58.png",
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
},
{
"filename": "appicon-87.png",
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
},
{
"filename": "appicon-80.png",
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
},
{
"filename": "appicon-120.png",
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
},
{
"filename": "appicon-120.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
},
{
"filename": "appicon-180.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
},
{
"filename": "appicon-20.png",
"idiom": "ipad",
"scale": "1x",
"size": "20x20"
},
{
"filename": "appicon-40.png",
"idiom": "ipad",
"scale": "2x",
"size": "20x20"
},
{
"filename": "appicon-29.png",
"idiom": "ipad",
"scale": "1x",
"size": "29x29"
},
{
"filename": "appicon-58.png",
"idiom": "ipad",
"scale": "2x",
"size": "29x29"
},
{
"filename": "appicon-40.png",
"idiom": "ipad",
"scale": "1x",
"size": "40x40"
},
{
"filename": "appicon-80.png",
"idiom": "ipad",
"scale": "2x",
"size": "40x40"
},
{
"filename": "appicon-76.png",
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
},
{
"filename": "appicon-152.png",
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
},
{
"filename": "appicon-167.png",
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
},
{
"filename": "appicon-1024.png",
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions CaleniOS/Sources/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
Loading