Skip to content

Commit bbb12e1

Browse files
tomymaritanoclaude
andauthored
fix(web): release develop to main — cleanUrls auth/verify 404 fix (#167)
## Summary - **Critical fix:** `cleanUrls: true` in `vercel.json` to resolve `/auth/verify` 404 in production (magic link broken) - feat(share): public notes API with metadata for portfolio consumption - feat(ai): add tool use (function calling) to AI assistant - feat(release): automated release pipeline with semantic-release - feat(ai-core): provider-agnostic AI architecture with streaming - feat(sync): connect auth → payment → sync flow with license gating - Multiple other features and fixes accumulated in develop ## Root Cause `apps/web/next.config.mjs` uses `output: 'export'` (static HTML). Next.js generates `auth/verify/index.html`, but Vercel needs `cleanUrls: true` to map `/auth/verify` → `auth/verify/index.html`. Without it, the auth verify route returns 404. ## Test plan - [ ] After merge: visit `https://readied.app/auth/verify?token=test` — should show verification UI, not 404 - [ ] With real magic link token: should attempt to open `readied://auth/verify?token=xxx` via deep link - [ ] Verify Vercel auto-deploys from main successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c6495e commit bbb12e1

51 files changed

Lines changed: 7853 additions & 165 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,7 @@
7676
"Bash(npx vitest:*)",
7777
"Bash(do echo:*)",
7878
"Bash(do gh:*)",
79-
"Bash(npx tsc:*)",
80-
"Bash(git stash:*)",
81-
"Bash(git rm:*)",
82-
"Bash(turbo typecheck:*)",
83-
"Bash(npx turbo:*)"
79+
"Bash(git tag:*)"
8480
]
8581
}
8682
}

.github/workflows/build.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
tags:
66
- 'v*'
7-
workflow_dispatch:
87

98
permissions:
109
contents: write

CLAUDE.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
```
1616
apps/
1717
desktop/ # Electron app (main, preload, renderer)
18-
docs-site/ # VitePress documentation
18+
web/ # Next.js marketing site + docs
1919
packages/
20+
ai-core/ # Provider-agnostic AI: streaming, LLM providers, context builder
2021
core/ # Domain logic + markdown parsing
22+
command-registry/ # Command palette registry
23+
plugin-api/ # Plugin system interfaces
2124
storage-core/ # Storage interfaces (pure TS)
2225
storage-sqlite/ # SQLite adapter (peerDep for better-sqlite3)
2326
licensing/ # License validation
2427
product-config/ # Product configuration
28+
sync-core/ # Sync engine
2529
```
2630

2731
## Commands
@@ -67,6 +71,12 @@ Pattern for workspace packages with native deps:
6771
}
6872
```
6973

74+
## Type Version Alignment
75+
76+
`@types/react` is pinned to `18.3.27` via `pnpm.overrides` in root `package.json`. This prevents type mismatches when packages like `lucide-react` resolve a different `@types/react` version than the app uses.
77+
78+
**If you see `'X' cannot be used as a JSX component` errors:** Check that `pnpm.overrides` in root `package.json` still pins `@types/react` to match the React version used by `apps/desktop`.
79+
7080
## Testing
7181

7282
- `pnpm test` runs all tests **except** storage-sqlite (safe to run always)
@@ -138,6 +148,13 @@ git pull origin develop
138148
git checkout -b feature/my-feature
139149
```
140150

151+
**Keep branch in sync (do this daily or before pushing):**
152+
153+
```bash
154+
git fetch origin develop
155+
git rebase origin/develop
156+
```
157+
141158
**Creating PR:**
142159

143160
```bash
@@ -153,6 +170,16 @@ git pull origin develop
153170
git branch -d feature/my-feature
154171
```
155172

173+
### Branch Hygiene (Critical)
174+
175+
Long-lived branches cause painful merge conflicts. Follow these rules:
176+
177+
- **Rebase daily:** `git fetch origin develop && git rebase origin/develop` before starting work each day
178+
- **Small PRs:** Prefer 3 small PRs over 1 large one. Split by layer (types → logic → UI)
179+
- **Max branch lifetime:** 2-3 days. If work takes longer, split into incremental PRs
180+
- **Don't touch unrelated files:** Avoid changes to `package.json`, lockfiles, or `apps/web` unless that's the PR's purpose — these are high-conflict files
181+
- **Rebase before pushing:** Always rebase against latest develop before `git push` to catch conflicts early
182+
156183
### Commit Messages
157184

158185
Use conventional commits:
@@ -251,6 +278,41 @@ case 'tag':
251278
- [ ] Sidebar uses `useNavigation()` hook
252279
- [ ] No implicit flags (`!== null`)
253280

281+
## AI Architecture
282+
283+
The AI system lives in `packages/ai-core` with a provider-agnostic, streaming-first design.
284+
285+
```
286+
Renderer (AiPanel) → IPC → Main (ipc-ai.ts) → AIService → ProviderRegistry → Provider → SSE stream
287+
← batched LLMEvents (text/error/done) ←
288+
```
289+
290+
**Key packages and files:**
291+
292+
- `packages/ai-core/` — LLMProvider interface, ProviderRegistry, AnthropicProvider, ContextBuilder, AIService
293+
- `apps/desktop/src/main/ai/ipc-ai.ts` — IPC bridge, 50ms batched event streaming
294+
- `apps/desktop/src/preload/index.ts``window.readied.ai` API (chat, onEvent, cancel)
295+
- `apps/desktop/src/renderer/components/ai/AiPanel.tsx` — Chat UI with streaming
296+
297+
**Adding a new LLM provider:**
298+
299+
1. Create `packages/ai-core/src/providers/my-provider.ts` implementing `LLMProvider`
300+
2. Register in `ProviderRegistry` at `apps/desktop/src/main/ai/ipc-ai.ts`
301+
3. Add option to `apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx`
302+
303+
**Key types:**
304+
305+
- `LLMEvent` — Protocol: `text` (delta), `error` (with code), `done`, `tool_call`, `tool_result`
306+
- `ChatOptions` — Provider, model, messages, tools, maxTokens
307+
- `LLMProvider``chat(options): AsyncGenerator<LLMEvent>` + `models()` + `validateKey()`
308+
309+
**Rules:**
310+
311+
- **No SDK dependencies in ai-core:** Providers use native `fetch` + SSE parsing
312+
- **Streaming only:** No request/response pattern — everything streams via `LLMEvent`
313+
- **Single panel instance:** Both Cmd+K and Sparkles button toggle the same AiPanel in App.tsx via CustomEvent (`readied:ai:toggle-panel`)
314+
- **Settings store is source of truth:** API key, model, and provider come from Zustand settings store (`selectAi` selector), not plugin config
315+
254316
## Documentation
255317

256318
- **Architecture decisions:** `plan.md`
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// apps/desktop/src/main/ai/built-in-tools.ts
2+
import type { ToolRegistry } from '@readied/ai-core';
3+
4+
/**
5+
* Register built-in AI tools for note operations.
6+
*
7+
* Read tools (auto-execute): search_notes, read_note, list_notebooks
8+
* Write tools (require confirmation): create_note, insert_text, replace_selection
9+
*
10+
* insert_text and replace_selection execute in the renderer (they need editor access).
11+
* They delegate via IPC: main → renderer → main.
12+
*/
13+
export function registerBuiltInTools(
14+
registry: ToolRegistry,
15+
deps: {
16+
searchNotes: (
17+
query: string,
18+
limit?: number
19+
) => Promise<Array<{ id: string; title: string; snippet: string }>>;
20+
readNote: (id: string) => Promise<{ id: string; title: string; content: string } | null>;
21+
listNotebooks: () => Promise<Array<{ id: string; name: string; noteCount: number }>>;
22+
createNote: (title: string, content: string, notebookId?: string) => Promise<{ id: string }>;
23+
}
24+
): void {
25+
registry.register({
26+
name: 'search_notes',
27+
description: 'Search notes by keyword query. Returns matching note IDs, titles, and snippets.',
28+
parameters: {
29+
type: 'object',
30+
properties: {
31+
query: { type: 'string', description: 'Search query' },
32+
limit: { type: 'number', description: 'Max results (default 10)' },
33+
},
34+
required: ['query'],
35+
},
36+
requiresConfirmation: false,
37+
execute: async args => {
38+
const results = await deps.searchNotes(args.query as string, (args.limit as number) ?? 10);
39+
return { ok: true, content: JSON.stringify(results) };
40+
},
41+
});
42+
43+
registry.register({
44+
name: 'read_note',
45+
description: 'Read the full content of a note by its ID.',
46+
parameters: {
47+
type: 'object',
48+
properties: {
49+
id: { type: 'string', description: 'Note ID' },
50+
},
51+
required: ['id'],
52+
},
53+
requiresConfirmation: false,
54+
execute: async args => {
55+
const note = await deps.readNote(args.id as string);
56+
if (!note) return { ok: false, content: 'Note not found', error: 'Note not found' };
57+
return { ok: true, content: JSON.stringify(note) };
58+
},
59+
});
60+
61+
registry.register({
62+
name: 'list_notebooks',
63+
description: 'List all notebooks with their names and note counts.',
64+
parameters: {
65+
type: 'object',
66+
properties: {},
67+
},
68+
requiresConfirmation: false,
69+
execute: async () => {
70+
const notebooks = await deps.listNotebooks();
71+
return { ok: true, content: JSON.stringify(notebooks) };
72+
},
73+
});
74+
75+
registry.register({
76+
name: 'create_note',
77+
description: 'Create a new note in a notebook.',
78+
parameters: {
79+
type: 'object',
80+
properties: {
81+
title: { type: 'string', description: 'Note title' },
82+
content: { type: 'string', description: 'Note content in markdown' },
83+
notebookId: {
84+
type: 'string',
85+
description: 'Target notebook ID (optional, uses default)',
86+
},
87+
},
88+
required: ['title', 'content'],
89+
},
90+
requiresConfirmation: true,
91+
execute: async args => {
92+
const result = await deps.createNote(
93+
args.title as string,
94+
args.content as string,
95+
args.notebookId as string | undefined
96+
);
97+
return { ok: true, content: JSON.stringify(result) };
98+
},
99+
});
100+
101+
registry.register({
102+
name: 'insert_text',
103+
description: 'Insert text into the current note at the cursor position or at the end.',
104+
parameters: {
105+
type: 'object',
106+
properties: {
107+
text: { type: 'string', description: 'Text to insert' },
108+
position: {
109+
type: 'string',
110+
description: "Where to insert: 'cursor' (default) or 'end'",
111+
},
112+
},
113+
required: ['text'],
114+
},
115+
requiresConfirmation: true,
116+
rendererOnly: true,
117+
execute: async () => {
118+
// Execution handled by ipc-ai.ts via executeToolInRenderer
119+
return { ok: false, content: 'Not reachable', error: 'Renderer tool called without IPC' };
120+
},
121+
});
122+
123+
registry.register({
124+
name: 'replace_selection',
125+
description: 'Replace the currently selected text in the editor.',
126+
parameters: {
127+
type: 'object',
128+
properties: {
129+
text: { type: 'string', description: 'Replacement text' },
130+
},
131+
required: ['text'],
132+
},
133+
requiresConfirmation: true,
134+
rendererOnly: true,
135+
execute: async () => {
136+
// Execution handled by ipc-ai.ts via executeToolInRenderer
137+
return { ok: false, content: 'Not reachable', error: 'Renderer tool called without IPC' };
138+
},
139+
});
140+
}

0 commit comments

Comments
 (0)