diff --git a/.claude/hooks/block-risky-bash.sh b/.claude/hooks/block-risky-bash.sh index 5eb6bbaf..b27f9f0e 100755 --- a/.claude/hooks/block-risky-bash.sh +++ b/.claude/hooks/block-risky-bash.sh @@ -2,12 +2,25 @@ # PreToolUse Bash guard: block dangerous shell commands. # Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777, # fork bombs, block device overwrites, mkfs, paths escaping project root. +# Uses python3 instead of jq for JSON (guaranteed on macOS). set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +CMD=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('command', '')) +except: pass +" <<< "$INPUT") BASE="${CLAUDE_PROJECT_DIR:-}" -[[ -z "$BASE" ]] && BASE=$(echo "$INPUT" | jq -r '.cwd // empty') +[[ -z "$BASE" ]] && BASE=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('cwd', '')) +except: pass +" <<< "$INPUT") [[ -z "$BASE" ]] && BASE="$(pwd)" # Normalize: collapse whitespace, strip leading/trailing @@ -15,13 +28,16 @@ CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:spa deny() { local reason="$1" - jq -n --arg r "$reason" '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: $r + python3 -c " +import json +print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', + 'permissionDecision': 'deny', + 'permissionDecisionReason': '$reason' } - }' +})) +" exit 0 } @@ -40,7 +56,6 @@ esac # Check for path traversal escaping project root (../ outside project) # Only if we have a sensible base if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then - ABS_BASE=$(cd "$BASE" && pwd) # Simple heuristic: command contains .. and would resolve outside project if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then # Extract plausible paths and check - allow ../ within project diff --git a/.claude/hooks/post-push-progress.sh b/.claude/hooks/post-push-progress.sh new file mode 100755 index 00000000..fa909356 --- /dev/null +++ b/.claude/hooks/post-push-progress.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# PostToolUse Bash hook: detect git push/commit and prompt Claude to update PROGRESS.md. +# Returns structured feedback with recent commits so Claude can write a session log entry. +# Uses python3 instead of jq for JSON (guaranteed on macOS). +set -euo pipefail + +INPUT=$(cat) + +# Extract command from JSON using python3 +CMD=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('command', '')) +except: pass +" <<< "$INPUT") + +# Only trigger on git push or git commit commands +if ! echo "$CMD" | grep -qE '\bgit\s+(push|commit)\b'; then + exit 0 +fi + +# Gather context for the progress update +BASE="${CLAUDE_PROJECT_DIR:-$(pwd)}" +BRANCH=$(git -C "$BASE" branch --show-current 2>/dev/null || echo "unknown") +PROGRESS_FILE="$BASE/PROGRESS.md" +TIMESTAMP=$(date '+%Y-%m-%d %H:%M') + +# Get recent commits (branch vs main, or last 10) +if git -C "$BASE" rev-parse --verify main &>/dev/null; then + COMMITS=$(git -C "$BASE" log --oneline main..HEAD 2>/dev/null | head -15) + if [ -z "$COMMITS" ]; then + COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null) + fi +else + COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null) +fi + +# Get changed files in recent commits +CHANGED_FILES=$(git -C "$BASE" diff --name-only main..HEAD 2>/dev/null | head -20 || \ + git -C "$BASE" diff --name-only HEAD~5..HEAD 2>/dev/null | head -20 || \ + echo "unknown") + +# Build the feedback message and output as JSON using python3 +python3 -c " +import json, sys + +message = '''Progress Update Needed + +A git push/commit was detected on branch \`$BRANCH\` at $TIMESTAMP. + +Recent commits: +\`\`\` +$COMMITS +\`\`\` + +Changed files: +\`\`\` +$CHANGED_FILES +\`\`\` + +Please update PROGRESS.md: +1. Add a session log entry under '## Session Log' with format: ### $TIMESTAMP — $BRANCH +2. Summarize what was accomplished (2-4 bullet points based on the commits above) +3. Update any roadmap checkboxes if tasks were completed +4. Commit the PROGRESS.md update''' + +output = { + 'hookSpecificOutput': { + 'hookEventName': 'PostToolUse', + 'progressUpdate': message + } +} +print(json.dumps(output)) +" diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh index c8ac2211..a0670545 100755 --- a/.claude/hooks/protect-files.sh +++ b/.claude/hooks/protect-files.sh @@ -1,12 +1,25 @@ #!/usr/bin/env bash # PreToolUse Edit|Write guard: block edits outside project and to protected paths. # Denies: paths outside project, .git/, .env*, lockfiles, node_modules/ +# Uses python3 instead of jq for JSON (guaranteed on macOS). set -euo pipefail INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') +FILE_PATH=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('file_path', '')) +except: pass +" <<< "$INPUT") BASE="${CLAUDE_PROJECT_DIR:-}" -[[ -z "$BASE" ]] && BASE=$(echo "$INPUT" | jq -r '.cwd // empty') +[[ -z "$BASE" ]] && BASE=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('cwd', '')) +except: pass +" <<< "$INPUT") [[ -z "$BASE" ]] && BASE="$(pwd)" # Resolve to absolute path @@ -27,6 +40,22 @@ fi ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true [[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}" +deny() { + local reason="$1" + echo "Blocked: $ABS_PATH — $reason" >&2 + python3 -c " +import json +print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', + 'permissionDecision': 'deny', + 'permissionDecisionReason': '$reason' + } +})) +" + exit 0 +} + # Protected patterns (path contains or equals) PROTECTED_PATTERNS=( ".git/" @@ -39,42 +68,18 @@ PROTECTED_PATTERNS=( for pattern in "${PROTECTED_PATTERNS[@]}"; do if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then - echo "Blocked: $ABS_PATH matches protected pattern '$pattern'" >&2 - jq -n --arg r "Edit blocked: path matches protected pattern ($pattern)" '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: $r - } - }' - exit 0 + deny "Edit blocked: path matches protected pattern ($pattern)" fi done # .env.*.local if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then - echo "Blocked: $ABS_PATH is an env secrets file" >&2 - jq -n '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: "Edit blocked: .env.*.local files contain secrets" - } - }' - exit 0 + deny "Edit blocked: .env.*.local files contain secrets" fi # Ensure path is under project root (ABS_BASE has trailing /) if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then - echo "Blocked: $ABS_PATH is outside project ($ABS_BASE)" >&2 - jq -n --arg r "Edit blocked: path is outside project directory" '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: $r - } - }' - exit 0 + deny "Edit blocked: path is outside project directory" fi exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 73207c49..0454473d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -19,6 +19,17 @@ } ] } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-push-progress.sh" + } + ] + } ] } } diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..b3cbbe12 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,605 @@ +# AIUI Project Plan: Roadmap, Progress Tracking & Automated Coding Tasks + +## Context + +AIUI is at ~65% architectural completeness. The chat + content surface experience is mature (11 content types, streaming, responsive layout, glass morphism design system). However, critical infrastructure is missing: no persistent storage beyond dev mode, zero error boundaries, no unit tests, plugin system spec'd but unused, no encryption. + +The user has Claude Code automation set up for late-night unattended runs. This plan provides: +1. **Progress tracking automation** — PROGRESS.md updated on every commit/push +2. **Detailed task specs** — granular enough for automated Claude sessions to execute autonomously + +There are **6 active worktrees** — all automation must work across branches. + +--- + +## Part 1: Progress Tracking Automation + +### Task 1.1: Create PROGRESS.md + +**File**: `PROGRESS.md` (repo root) + +Create a structured progress document with: +- Project status summary (current milestone, % complete) +- Roadmap checklist (M0–M7, matching Part 2 below) +- Session log section (auto-populated by hook) + +Format: +```markdown +# AIUI Progress + +## Current Status +**Active Milestone**: M1 — Stability & Polish +**Overall**: ~65% architectural completeness + +## Roadmap +### M0: Foundation ✅ +- [x] Chat interface with streaming +- [x] 11 content type renderers +- [x] Responsive layout (mobile + desktop) +- [x] Glass morphism design system +- [x] Claude proxy + web search +- [x] PWA support + +### M1: Stability & Polish 🔄 +- [ ] Persistent storage (IndexedDB) +- [ ] Error boundaries +- [ ] Unit tests for composables +- [ ] E2E test coverage +- [ ] CI pipeline + +(... remaining milestones from Part 2 ...) + +## Session Log + +``` + +### Task 1.2: Create post-push progress hook + +**File**: `.claude/hooks/post-push-progress.sh` + +A `PostToolUse` hook for Bash that: +1. Reads stdin JSON, extracts `tool_input.command` +2. Checks if command contains `git push` or `git commit` +3. If match: runs `git log --oneline` for commits on current branch not on `main` +4. Reads current PROGRESS.md roadmap section to identify active milestone +5. Outputs JSON with `hookSpecificOutput` containing a message asking Claude to update the Session Log in PROGRESS.md + +The hook script should: +- Extract branch name via `git branch --show-current` +- Get commit list via `git log --oneline main..HEAD` (or last 5 commits if no divergence) +- Format the output as structured feedback + +### Task 1.3: Register the hook + +**File**: `.claude/settings.json` + +Add a PostToolUse entry: +```json +{ + "hooks": { + "PreToolUse": [ ... existing ... ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-push-progress.sh" + } + ] + } + ] + } +} +``` + +--- + +## Part 2: Long-Term Delivery Roadmap + +### M0: Foundation ✅ (Complete) +All done — chat, renderers, layout, design system, proxy, PWA. + +--- + +### M1: Stability & Polish (Infrastructure) + +#### Task M1.1: IndexedDB Persistent Storage + +**Why**: Conversations are lost on page refresh in production. Currently only persisted via dev-mode `/api/dev-chats` Vite middleware. + +**Files to modify**: +- `packages/app/src/stores/chat.ts` (198 lines) — add IndexedDB adapter +- Create `packages/app/src/utils/idb-storage.ts` — IndexedDB wrapper + +**Implementation**: +1. Create `idb-storage.ts` with these functions: + ```ts + export async function openDB(): Promise + export async function saveConversation(conv: Conversation): Promise + export async function loadAllConversations(): Promise> + export async function deleteConversation(id: string): Promise + ``` + - DB name: `aiui-store`, version 1 + - Object store: `conversations`, keyPath: `id` + - Index on `updatedAt` for sorted retrieval + +2. Modify `chat.ts`: + - Replace `saveServerChats()` (line 51-62) with `saveConversation()` calls + - Replace `loadServerChats()` (line 64-84) with `loadAllConversations()` + - Keep dev-chats middleware as fallback when `indexedDB` unavailable + - Debounced save stays at 800ms + +**Acceptance criteria**: +- Conversations survive page refresh +- New conversations appear after reload +- Delete removes from IndexedDB +- Falls back to dev-chats middleware if IDB unavailable +- `pnpm typecheck` passes + +#### Task M1.2: Error Boundaries + +**Why**: Zero `onErrorCaptured` usage. Any component error crashes the whole app. + +**Files to modify**: +- Create `packages/app/src/components/ui/ErrorBoundary.vue` +- `packages/app/src/pages/ChatPage.vue` (597 lines) — wrap major sections +- `packages/app/src/components/chat/ChatWindow.vue` (248 lines) — wrap message list +- `packages/app/src/components/content/ContentPanel.vue` — wrap detail views + +**Implementation**: +1. Create `ErrorBoundary.vue`: + ```vue + + + ``` + +2. Wrap in ChatPage.vue: + - Wrap chat column (around ``) + - Wrap content panel column (around ``) + - Wrap detail view column (around ``) + +3. Wrap in ChatWindow.vue: + - Wrap the message loop (v-for of ChatMessage) + +4. Wrap in ContentPanel.vue: + - Wrap each grid component render + - Wrap detail component render + +**Acceptance criteria**: +- A failing renderer shows error card with retry button +- Chat continues working if content panel errors +- Content panel continues if one card errors +- `pnpm typecheck` passes + +#### Task M1.3: Unit Tests for contentExtraction + +**Why**: `contentExtraction.ts` is 967 lines of regex parsing with zero tests. It's the most critical composable. + +**File to create**: `packages/app/src/__tests__/contentExtraction.test.ts` + +**Tests to write** (use vitest): +```ts +describe('contentExtraction', () => { + describe('extractAllFilms', () => { + it('extracts film_ext tags with title, year, director') + it('extracts film:id tags and looks up from library') + it('returns empty array for text with no film tags') + it('handles multiple films in one message') + it('handles malformed tags gracefully') + }) + + describe('extractAllSongs', () => { + it('extracts song_ext tags with title, artist, year') + it('extracts song:id tags from library') + it('extracts songs from markdown bold patterns') + it('deduplicates songs by title+artist') + }) + + describe('extractAllPodcasts', () => { + it('extracts podcast_ext tags') + it('extracts podcast:id from library') + }) + + describe('extractAllBooks', () => { + it('extracts book_ext tags with title, author, year') + it('handles optional fields') + }) + + describe('extractAllTVSeries', () => { + it('extracts tv_ext tags') + it('parses creator and network fields') + }) + + describe('extractAllPlaces', () => { + it('extracts place_ext tags with all fields') + it('handles missing optional fields (rating, price)') + }) + + describe('extractMagazineSections', () => { + it('extracts sections from markdown headings') + it('captures content between headings') + it('extracts hero images') + }) + + describe('stripContentTags', () => { + it('removes all tag types from text') + it('preserves non-tag content') + it('handles nested/adjacent tags') + }) + + describe('extractBoldDomainLinks', () => { + it('extracts **domain.com** patterns with URLs') + it('extracts markdown links') + }) +}) +``` + +**How to run**: `pnpm test` (vitest via turbo) + +**Acceptance criteria**: +- All tests pass +- Covers the 10 main extraction functions +- Tests edge cases (empty input, malformed tags, duplicates) +- `pnpm test` exits 0 + +#### Task M1.4: Unit Tests for useAI + +**File to create**: `packages/app/src/__tests__/useAI.test.ts` + +**Tests to write**: +```ts +describe('useAI', () => { + describe('provider selection', () => { + it('defaults to first available provider') + it('switches provider via setActiveProvider') + it('lists available models for active provider') + }) + + describe('context injection', () => { + it('includes film library in system prompt') + it('includes song library in system prompt') + it('includes content tag format instructions') + }) + + describe('sendMessage', () => { + it('adds user message to store') + it('creates assistant message placeholder') + it('sets isStreaming to true during stream') + it('sets isStreaming to false after completion') + it('handles stream errors gracefully') + }) + + describe('stopGeneration', () => { + it('aborts active stream') + it('sets isStreaming to false') + }) +}) +``` + +**Note**: Will need to mock `fetch` for streaming tests. Use vitest's `vi.fn()`. + +**Acceptance criteria**: +- All tests pass with mocked fetch/SSE +- `pnpm test` exits 0 + +#### Task M1.5: E2E Test Expansion + +**File to modify**: `packages/app/e2e/content-surfaces.spec.ts` + +**Tests to add**: +```ts +test('sends a message and receives streaming response') +test('content panel shows film cards when AI mentions films') +test('clicking a film card opens detail view') +test('mobile viewport shows full-screen overlay for content') +test('stop button halts generation') +test('web search toggle works') +test('new conversation clears messages') +test('panel side toggle switches layout') +``` + +**Acceptance criteria**: +- `pnpm test:e2e` passes (needs dev server running) + +--- + +### M2: Content Experience (UX) + +#### Task M2.1: Markdown Rendering in Chat + +**Why**: Chat messages display plain text. Markdown (bold, italic, links, code blocks, lists) should render properly. + +**Files to modify**: +- `packages/app/src/components/chat/ChatMessage.vue` (333 lines) +- Add `markdown-it` as dependency + +**Implementation**: +1. `pnpm add markdown-it` + `pnpm add -D @types/markdown-it` in `packages/app` +2. In ChatMessage.vue: + - Import and configure markdown-it with safe defaults (no HTML) + - After `stripContentTags()`, render remaining text through markdown-it + - Use `v-html` with the sanitized markdown output + - Add CSS for rendered markdown (code blocks, lists, links) in main.css + - Ensure content tags are extracted BEFORE markdown rendering + +**Security**: markdown-it with `html: false` prevents XSS. No raw HTML passthrough. + +**Acceptance criteria**: +- Bold, italic, links, code blocks, lists render in chat +- Content tags still extract correctly (films, songs, etc.) +- No XSS possible +- `pnpm typecheck` passes + +#### Task M2.2: Virtual Scrolling for Chat + +**Why**: Long conversations with many messages cause scroll jank. + +**Files to modify**: +- `packages/app/src/components/chat/ChatWindow.vue` +- Add `@tanstack/vue-virtual` dependency + +**Implementation**: +1. `pnpm add @tanstack/vue-virtual` in `packages/app` +2. Replace the message `v-for` loop with `useVirtualizer`: + - Estimate row heights (user messages ~60px, assistant ~200px) + - Use dynamic measurement for actual heights + - Maintain scroll-to-bottom behavior during streaming + - Keep overscan at 5 items + +**Acceptance criteria**: +- Scrolling is smooth with 100+ messages +- Auto-scroll to bottom during streaming still works +- `pnpm typecheck` passes + +#### Task M2.3: Music Source Resolution + +**Why**: PlayerBar exists but music source resolution is incomplete. Iframe embedding untested. + +**Files to modify**: +- `packages/app/src/composables/usePlayer.ts` (185 lines) +- `packages/app/src/components/player/PlayerBar.vue` (165 lines) + +**Implementation**: +1. In usePlayer.ts: + - Add queue management: `queue: ShallowRef`, `currentIndex: Ref` + - Add `playNext()`, `playPrevious()`, `addToQueue(song)` methods + - Fix iframe playback (lines 72-83): create Plyr instance for iframes too + - Add retry logic for failed music searches (try next source) + +2. In PlayerBar.vue: + - Add next/previous buttons + - Show queue count + - Add queue panel (slide-up from player) + +**Acceptance criteria**: +- Can play songs from search results +- Next/previous navigation works +- Queue persists across song changes +- `pnpm typecheck` passes + +#### Task M2.4: Nostr Feed Integration + +**Why**: NostrGrid.vue exists but is non-functional. No relay connection. + +**Files to modify**: +- `packages/app/src/components/content/NostrGrid.vue` +- Create `packages/app/src/composables/useNostr.ts` + +**Implementation**: +1. Create `useNostr.ts`: + - Connect to public relays (wss://relay.damus.io, wss://nos.lol, wss://relay.snort.social) + - Use raw WebSocket (no nostr-tools dependency to keep bundle small) + - Subscribe to kind:1 (text notes) with limit 50 + - Parse NIP-01 event format manually + - Export `useNostr()` returning `{ events, isConnected, connect, disconnect }` + +2. Update NostrGrid.vue: + - Use `useNostr()` composable + - Display events as cards with author npub (truncated), content, timestamp + - Lazy-load on tab activation only + +**Acceptance criteria**: +- Nostr tab shows real posts from public relays +- Connection/disconnection is clean (no leaked WebSockets) +- Handles relay errors gracefully +- `pnpm typecheck` passes + +--- + +### M3: Plugin System (Infrastructure) + +#### Task M3.1: Activate Plugin Registry at Runtime + +**Why**: `packages/core/src/plugins/registry.ts` exists with `registerPlugin()` but nothing calls it. + +**Files to modify**: +- `packages/app/src/main.ts` (26 lines) — add plugin initialization +- Create `packages/app/src/plugins/index.ts` — plugin bootstrap +- Create `packages/app/src/plugins/claude-provider.ts` — first AI provider plugin + +**Implementation**: +1. Create `plugins/index.ts`: + ```ts + export async function initializePlugins() { + // Register built-in plugins + const { claudeProvider } = await import('./claude-provider') + registerPlugin(claudeProvider) + } + ``` + +2. Create `plugins/claude-provider.ts`: + - Implement `AIProviderAdapter` interface from `@aiui/core` + - Wrap existing `useAI.ts` streaming logic as a plugin + - Export as a Tier 1 (trusted) plugin + +3. In `main.ts`: + - Call `initializePlugins()` before app mount + - Make it async with error handling + +**Acceptance criteria**: +- Plugin registry has at least 1 registered plugin at runtime +- Chat still works through the plugin adapter +- `getPluginsByType('ai-provider')` returns the Claude provider +- `pnpm typecheck` passes + +#### Task M3.2: Renderer Plugin Registration + +**Why**: Content renderers are hardcoded. Making them pluggable enables community extensions. + +**Files to modify**: +- Create `packages/app/src/plugins/renderers/film-renderer.ts` +- Create `packages/app/src/plugins/renderers/song-renderer.ts` +- Modify `packages/app/src/plugins/index.ts` — register renderers +- Modify `packages/app/src/components/content/ContentPanel.vue` — use registry lookups + +**Implementation**: +1. Create renderer plugins for film and song (as examples): + ```ts + const filmRenderer: RendererDefinition = { + id: 'film', + name: 'Film Renderer', + contentType: 'film', + surfaces: ['chat-preview', 'panel-preview', 'panel-play'], + chatPreview: FilmCard, + panelPreview: FilmGrid, + panelPlay: FilmDetail, + } + ``` + +2. Register in `plugins/index.ts` via `registerRenderer()` + +3. In ContentPanel.vue, look up renderers via `getRendererForContentType()` instead of hardcoded imports (gradual migration — start with film/song, keep others hardcoded) + +**Acceptance criteria**: +- Film and song renderers registered via plugin system +- `getAllRenderers()` returns registered renderers +- Content panel still renders correctly +- `pnpm typecheck` passes + +--- + +### M4: Social & Discovery (UX) + +#### Task M4.1: Social Embeds + +Render Nostr notes, quoted posts inline in chat. Parse `nostr:` URI scheme. + +#### Task M4.2: Search Improvements + +Federated search across content types — unified search bar that queries films, songs, podcasts, web simultaneously. + +#### Task M4.3: Bookmarks/Favorites + +Save content items (films, songs, articles) to a local favorites list persisted in IndexedDB. + +*(Detailed specs to be written when M3 is complete)* + +--- + +### M5: Security & Privacy (Infrastructure) + +#### Task M5.1: E2E Encryption + +Integrate tweetnacl.js for message encryption. + +#### Task M5.2: Encrypted Storage + +AES-256-GCM encryption layer over IndexedDB via Web Crypto API. + +#### Task M5.3: API Key Vault + +Encrypted storage for API keys with PBKDF2 key derivation. + +*(Detailed specs to be written when M4 is complete)* + +--- + +### M6: Payments & Identity (UX + Infrastructure) + +#### Task M6.1: Lightning Wallet Deep-links +#### Task M6.2: Cashu Token Support +#### Task M6.3: Nostr Identity (NIP-07) + +*(Detailed specs to be written when M5 is complete)* + +--- + +### M7: Platform (Infrastructure) + +#### Task M7.1: MCP Integration +#### Task M7.2: Multi-provider AI Normalization +#### Task M7.3: Tauri Desktop Build +#### Task M7.4: Offline Mode + +*(Detailed specs to be written when M6 is complete)* + +--- + +## Part 3: Automated Session Execution Order + +For automated late-night Claude sessions, execute tasks in this order: + +### Priority Queue (each session picks next incomplete task): + +1. **Task 1.1** — Create PROGRESS.md *(5 min)* +2. **Task 1.2** — Create post-push progress hook *(10 min)* +3. **Task 1.3** — Register hook in settings.json *(2 min)* +4. **Task M1.2** — Error boundaries *(15 min)* +5. **Task M1.3** — Unit tests for contentExtraction *(20 min)* +6. **Task M1.1** — IndexedDB persistent storage *(25 min)* +7. **Task M1.4** — Unit tests for useAI *(20 min)* +8. **Task M2.1** — Markdown rendering *(20 min)* +9. **Task M1.5** — E2E test expansion *(15 min)* +10. **Task M2.3** — Music source resolution *(20 min)* +11. **Task M2.2** — Virtual scrolling *(15 min)* +12. **Task M2.4** — Nostr feed integration *(25 min)* +13. **Task M3.1** — Activate plugin registry *(20 min)* +14. **Task M3.2** — Renderer plugin registration *(20 min)* + +### Session Protocol + +Each automated session should: +1. Read `PROGRESS.md` to find the next incomplete task +2. Read this plan file for the task's detailed spec +3. Execute the task following the spec exactly +4. Run `pnpm typecheck` after changes +5. Run `pnpm lint` after changes +6. Run `pnpm test` if unit tests exist +7. Commit with conventional format: `type(scope): description` +8. Push to current branch +9. Update PROGRESS.md session log (triggered by hook, or manually) + +--- + +## Verification + +After implementing Part 1 (progress automation): +1. Run `pnpm typecheck && pnpm lint` — should pass +2. Commit a change and push — hook should fire +3. Verify PROGRESS.md gets a session log entry +4. Test from a different worktree — should work identically + +After each M1–M3 task: +1. `pnpm typecheck` passes +2. `pnpm lint` passes +3. `pnpm test` passes (if tests exist) +4. Dev server runs without errors (`pnpm dev`) +5. Manual smoke test: send a message, see content render diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 00000000..5f705129 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,62 @@ +# AIUI Progress + +## Current Status +**Active Milestone**: M1 — Stability & Polish +**Overall**: ~65% architectural completeness + +## Roadmap + +### M0: Foundation ✅ +- [x] Chat interface with streaming (Claude/OpenRouter/Mock) +- [x] 11 content type renderers (film, song, podcast, book, TV, image, place, article, magazine, nostr, code) +- [x] Responsive layout (mobile three-column + desktop overlays) +- [x] Glass morphism design system (Tailwind CSS) +- [x] Claude proxy + web search (SearXNG/DDG) +- [x] PWA support (auto-update, installable) +- [x] Music player (Plyr-based PlayerBar) +- [x] Dev chat persistence (Vite middleware) + +### M1: Stability & Polish 🔄 +- [x] Progress tracking automation +- [ ] Error boundaries (`onErrorCaptured` wrappers) +- [ ] Unit tests — contentExtraction composable +- [ ] IndexedDB persistent storage +- [ ] Unit tests — useAI composable +- [ ] E2E test expansion (content surfaces, responsive) + +### M2: Content Experience 🔲 +- [ ] Markdown rendering in chat (markdown-it) +- [ ] Music source resolution + queue management +- [ ] Virtual scrolling for chat (@tanstack/vue-virtual) +- [ ] Nostr feed integration (relay WebSocket) + +### M3: Plugin System 🔲 +- [ ] Activate plugin registry at runtime +- [ ] Renderer plugin registration (film/song) +- [ ] Plugin settings UI +- [ ] Plugin sandboxing (Tier 1/2) + +### M4: Social & Discovery 🔲 +- [ ] Social embeds (Nostr notes inline) +- [ ] Federated search across content types +- [ ] Bookmarks/favorites (IndexedDB) + +### M5: Security & Privacy 🔲 +- [ ] E2E encryption (tweetnacl.js) +- [ ] Encrypted storage (AES-256-GCM over IndexedDB) +- [ ] API key vault (Web Crypto + PBKDF2) + +### M6: Payments & Identity 🔲 +- [ ] Lightning wallet deep-links +- [ ] Cashu token support +- [ ] Nostr identity (NIP-07) + +### M7: Platform 🔲 +- [ ] MCP integration +- [ ] Multi-provider AI normalization +- [ ] Tauri desktop build +- [ ] Offline mode (service worker, local-first sync) + +## Session Log + +