Skip to content

Commit d4ce973

Browse files
tomymaritanoclaude
andauthored
feat: local HTTP API, quick capture, mermaid/math plugins + ASAR fix (#231)
## Summary ### New Features - **Local HTTP API** (port 29168): REST API with bearer auth for Alfred/Raycast/curl integration - **Quick Capture** (Cmd+Shift+N): frameless floating window, always-on-top, auto-focus - **Mermaid diagrams**: code block renderer with "Open in Mermaid Live" button - **Math/LaTeX**: styled code block renderer with copy button - **Vim mode**: plugin stub with toggle command (ready for @codemirror/vim) ### Bug Fix - **ASAR crash fix**: `ERR_PACKAGE_PATH_NOT_EXPORTED` — workspace packages now bundled by electron-vite instead of externalized to node_modules ### Verified - Note window already shows only editor (no sidebar) — confirmed correct ## Test plan - [x] `pnpm typecheck` — 17/17 pass - [x] `pnpm build` (desktop) — builds successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Quick Capture window with Cmd/Ctrl+Shift+N for rapid note entry (floating, frameless). * Local HTTP server controls (start/stop/status/get token) accessible from the app. * Math & LaTeX plugin: render math blocks and copy LaTeX. * Mermaid plugin: render Mermaid blocks and copy source. * Vim Mode plugin: toggleable Vim Mode command and status indicator. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 67a0831 commit d4ce973

18 files changed

Lines changed: 1540 additions & 24 deletions

File tree

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@
8181
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/NoteListFilterBar*)",
8282
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/*.module.css)",
8383
"Bash(npm view:*)",
84-
"Bash(git log:*)"
84+
"Bash(git log:*)",
85+
"Bash(git branch:*)",
86+
"Bash(npx vercel:*)",
87+
"Bash(gh workflow:*)",
88+
"Bash(ls:*)"
8589
]
8690
}
8791
}

apps/desktop/electron-vite.config.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ import react from '@vitejs/plugin-react';
44

55
export default defineConfig({
66
main: {
7-
plugins: [externalizeDepsPlugin()],
7+
plugins: [
8+
externalizeDepsPlugin({
9+
exclude: [
10+
'@readied/core',
11+
'@readied/storage-core',
12+
'@readied/storage-sqlite',
13+
'@readied/sync-core',
14+
'@readied/licensing',
15+
'@readied/ai-core',
16+
],
17+
}),
18+
],
819
build: {
920
outDir: 'out/main',
1021
rollupOptions: {
@@ -18,7 +29,11 @@ export default defineConfig({
1829
},
1930
},
2031
preload: {
21-
plugins: [externalizeDepsPlugin()],
32+
plugins: [
33+
externalizeDepsPlugin({
34+
exclude: ['@readied/core', '@readied/storage-core', '@readied/licensing'],
35+
}),
36+
],
2237
build: {
2338
outDir: 'out/preload',
2439
rollupOptions: {
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* Local HTTP API Server IPC Handlers
3+
*
4+
* Manages the local API server lifecycle and exposes status/token info
5+
* to the renderer (settings UI).
6+
*/
7+
8+
import { ipcMain, app } from 'electron';
9+
import { createNoteId, createNoteOperation, updateNoteOperation } from '@readied/core';
10+
import {
11+
LocalServer,
12+
getOrCreateApiToken,
13+
type LocalServerHandlers,
14+
} from '../services/localServer.js';
15+
import type { SQLiteNoteRepository, DataPaths } from './types.js';
16+
17+
// ============================================================================
18+
// Types
19+
// ============================================================================
20+
21+
export interface LocalServerHandlerDeps {
22+
noteRepository: SQLiteNoteRepository;
23+
dataPaths: DataPaths;
24+
noteToSnapshot: (note: {
25+
id: string;
26+
notebookId: string;
27+
content: string;
28+
title: string;
29+
isPinned: boolean;
30+
isDeleted: boolean;
31+
status: import('@readied/core').NoteStatus;
32+
metadata: {
33+
createdAt: string;
34+
updatedAt: string;
35+
tags: readonly string[];
36+
wordCount: number;
37+
archivedAt: string | null;
38+
};
39+
}) => {
40+
id: string;
41+
notebookId: string;
42+
content: string;
43+
title: string;
44+
createdAt: string;
45+
updatedAt: string;
46+
tags: string[];
47+
wordCount: number;
48+
archivedAt: string | null;
49+
isArchived: boolean;
50+
isPinned: boolean;
51+
isDeleted: boolean;
52+
status: import('@readied/core').NoteStatus;
53+
};
54+
}
55+
56+
// ============================================================================
57+
// Module State
58+
// ============================================================================
59+
60+
const server = new LocalServer();
61+
let apiToken: string | null = null;
62+
63+
// ============================================================================
64+
// Registration
65+
// ============================================================================
66+
67+
export function registerLocalServerHandlers(deps: LocalServerHandlerDeps): void {
68+
const { noteRepository: repo, dataPaths, noteToSnapshot } = deps;
69+
70+
// Build handler callbacks that bridge HTTP requests to the note repository
71+
const handlers: LocalServerHandlers = {
72+
async listNotes() {
73+
const notes = await repo.list();
74+
return notes
75+
.filter(n => !n.isDeleted)
76+
.map(n => ({
77+
id: n.id,
78+
title: n.title,
79+
excerpt: n.content.slice(0, 200).replace(/\n/g, ' '),
80+
updatedAt: n.metadata.updatedAt,
81+
}));
82+
},
83+
84+
async getNote(id) {
85+
const note = await repo.get(createNoteId(id));
86+
if (!note) return null;
87+
const snap = noteToSnapshot(note);
88+
return {
89+
id: snap.id,
90+
title: snap.title,
91+
content: snap.content,
92+
notebookId: snap.notebookId,
93+
createdAt: snap.createdAt,
94+
updatedAt: snap.updatedAt,
95+
tags: snap.tags,
96+
wordCount: snap.wordCount,
97+
isPinned: snap.isPinned,
98+
};
99+
},
100+
101+
async createNote(input) {
102+
const result = await createNoteOperation(input, repo);
103+
if (result.ok) {
104+
return { ok: true, data: { id: result.data.id } };
105+
}
106+
return { ok: false, error: result.error };
107+
},
108+
109+
async updateNote(id, content) {
110+
const noteId = createNoteId(id);
111+
const result = await updateNoteOperation({ id: noteId, content }, repo);
112+
return { ok: result.ok, error: result.ok ? undefined : result.error };
113+
},
114+
115+
async searchNotes(query) {
116+
const notes = await repo.search(query, 50);
117+
return notes.map(n => ({
118+
id: n.id,
119+
title: n.title,
120+
excerpt: n.content.slice(0, 200).replace(/\n/g, ' '),
121+
updatedAt: n.metadata.updatedAt,
122+
}));
123+
},
124+
125+
async getNoteCount() {
126+
return repo.count();
127+
},
128+
129+
getAppVersion() {
130+
return app.getVersion();
131+
},
132+
};
133+
134+
// IPC: Start the local server
135+
ipcMain.handle('localServer:start', async (_event, port?: number) => {
136+
try {
137+
if (port !== undefined && (typeof port !== 'number' || port < 1 || port > 65535)) {
138+
return { ok: false, error: 'Invalid port' };
139+
}
140+
if (server.isRunning()) return { ok: true, port: server.getPort() };
141+
apiToken = await getOrCreateApiToken(dataPaths.root);
142+
await server.start(port, apiToken, handlers);
143+
return { ok: true, port: server.getPort() };
144+
} catch (err) {
145+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
146+
}
147+
});
148+
149+
// IPC: Stop the local server
150+
ipcMain.handle('localServer:stop', async () => {
151+
try {
152+
await server.stop();
153+
return { ok: true };
154+
} catch (err) {
155+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
156+
}
157+
});
158+
159+
// IPC: Get server status
160+
ipcMain.handle('localServer:status', () => {
161+
return {
162+
running: server.isRunning(),
163+
port: server.getPort(),
164+
};
165+
});
166+
167+
// IPC: Get the bearer token (for displaying in settings)
168+
ipcMain.handle('localServer:getToken', async () => {
169+
try {
170+
if (!apiToken) {
171+
apiToken = await getOrCreateApiToken(dataPaths.root);
172+
}
173+
return { ok: true, value: apiToken };
174+
} catch (err) {
175+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
176+
}
177+
});
178+
}
179+
180+
/**
181+
* Pre-initialise the API bearer token (called from main index).
182+
* The actual server start is controlled by settings — the renderer
183+
* will call localServer:start if the setting is enabled.
184+
*/
185+
export async function initApiToken(dataPaths: DataPaths): Promise<void> {
186+
apiToken = await getOrCreateApiToken(dataPaths.root);
187+
}
188+
189+
/** Stop the server on app quit */
190+
export async function stopLocalServer(): Promise<void> {
191+
await server.stop();
192+
}

0 commit comments

Comments
 (0)