opensidian 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.eslintrc.json +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +35 -0
  4. package/.github/ISSUE_TEMPLATE/question.md +23 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +45 -0
  6. package/.github/README.md +5 -0
  7. package/.github/workflows/cd.yml +44 -0
  8. package/.github/workflows/ci.yml +128 -0
  9. package/.github/workflows/qa.yml +45 -0
  10. package/.planning/PROJECT.md +96 -0
  11. package/.planning/REQUIREMENTS.md +66 -0
  12. package/.planning/ROADMAP.md +129 -0
  13. package/.planning/STATE.md +47 -0
  14. package/.planning/config.json +14 -0
  15. package/CONTRIBUTING.md +232 -0
  16. package/LICENSE +21 -0
  17. package/README.md +244 -0
  18. package/dist/api/auth.d.ts +5 -0
  19. package/dist/api/auth.d.ts.map +1 -0
  20. package/dist/api/auth.js +112 -0
  21. package/dist/api/auth.js.map +1 -0
  22. package/dist/api/routes.d.ts +3 -0
  23. package/dist/api/routes.d.ts.map +1 -0
  24. package/dist/api/routes.js +119 -0
  25. package/dist/api/routes.js.map +1 -0
  26. package/dist/api/themes.d.ts +3 -0
  27. package/dist/api/themes.d.ts.map +1 -0
  28. package/dist/api/themes.js +48 -0
  29. package/dist/api/themes.js.map +1 -0
  30. package/dist/core/graph.d.ts +16 -0
  31. package/dist/core/graph.d.ts.map +1 -0
  32. package/dist/core/graph.js +115 -0
  33. package/dist/core/graph.js.map +1 -0
  34. package/dist/core/markdown.d.ts +21 -0
  35. package/dist/core/markdown.d.ts.map +1 -0
  36. package/dist/core/markdown.js +77 -0
  37. package/dist/core/markdown.js.map +1 -0
  38. package/dist/core/search.d.ts +34 -0
  39. package/dist/core/search.d.ts.map +1 -0
  40. package/dist/core/search.js +159 -0
  41. package/dist/core/search.js.map +1 -0
  42. package/dist/core/sync.d.ts +30 -0
  43. package/dist/core/sync.d.ts.map +1 -0
  44. package/dist/core/sync.js +121 -0
  45. package/dist/core/sync.js.map +1 -0
  46. package/dist/core/vault.d.ts +28 -0
  47. package/dist/core/vault.d.ts.map +1 -0
  48. package/dist/core/vault.js +235 -0
  49. package/dist/core/vault.js.map +1 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +32 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp/cli.d.ts +3 -0
  55. package/dist/mcp/cli.d.ts.map +1 -0
  56. package/dist/mcp/cli.js +7 -0
  57. package/dist/mcp/cli.js.map +1 -0
  58. package/dist/mcp/server.d.ts +18 -0
  59. package/dist/mcp/server.d.ts.map +1 -0
  60. package/dist/mcp/server.js +272 -0
  61. package/dist/mcp/server.js.map +1 -0
  62. package/dist/plugins/host.d.ts +23 -0
  63. package/dist/plugins/host.d.ts.map +1 -0
  64. package/dist/plugins/host.js +104 -0
  65. package/dist/plugins/host.js.map +1 -0
  66. package/dist/plugins/sample-plugin.d.ts +10 -0
  67. package/dist/plugins/sample-plugin.d.ts.map +1 -0
  68. package/dist/plugins/sample-plugin.js +23 -0
  69. package/dist/plugins/sample-plugin.js.map +1 -0
  70. package/dist/server.d.ts +15 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +77 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/shared/types.d.ts +86 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/shared/types.js +2 -0
  77. package/dist/shared/types.js.map +1 -0
  78. package/docker/Dockerfile +37 -0
  79. package/docker/docker-compose.yml +46 -0
  80. package/docs/ARCHITECTURE.md +321 -0
  81. package/futuras_implementacoes.md +0 -0
  82. package/package.json +65 -0
  83. package/scripts/fix-gitignore.ps1 +5 -0
  84. package/scripts/seed-notes.mjs +60 -0
  85. package/src/api/auth.ts +130 -0
  86. package/src/api/routes.ts +133 -0
  87. package/src/api/themes.ts +60 -0
  88. package/src/core/graph.ts +145 -0
  89. package/src/core/markdown.ts +92 -0
  90. package/src/core/search.ts +208 -0
  91. package/src/core/sync.ts +157 -0
  92. package/src/core/vault.ts +286 -0
  93. package/src/index.ts +37 -0
  94. package/src/mcp/cli.ts +7 -0
  95. package/src/mcp/server.ts +296 -0
  96. package/src/plugins/host.ts +120 -0
  97. package/src/plugins/sample-plugin.ts +29 -0
  98. package/src/server.ts +90 -0
  99. package/src/shared/types.ts +92 -0
  100. package/tests/api/routes.test.ts +167 -0
  101. package/tests/core/graph.test.ts +236 -0
  102. package/tests/core/markdown.test.ts +157 -0
  103. package/tests/core/search.test.ts +132 -0
  104. package/tests/core/sync.test.ts +62 -0
  105. package/tests/core/vault.test.ts +162 -0
  106. package/tests/mcp/server.test.ts +118 -0
  107. package/tests/plugins/host.test.ts +165 -0
  108. package/tests/plugins/sample-plugin.test.ts +35 -0
  109. package/tests/server.test.ts +76 -0
  110. package/tsconfig.json +27 -0
  111. package/vite.config.ts +27 -0
  112. package/vitest.config.ts +33 -0
  113. package/web/index.html +13 -0
  114. package/web/package.json +26 -0
  115. package/web/public/favicon.svg +4 -0
  116. package/web/src/App.tsx +63 -0
  117. package/web/src/api/auth.ts +65 -0
  118. package/web/src/api/client.ts +117 -0
  119. package/web/src/api/themes.ts +78 -0
  120. package/web/src/components/GraphView.tsx +139 -0
  121. package/web/src/components/Layout.tsx +74 -0
  122. package/web/src/components/LoginPage.tsx +52 -0
  123. package/web/src/components/NoteEditor.tsx +114 -0
  124. package/web/src/components/NoteList.tsx +95 -0
  125. package/web/src/components/RegisterPage.tsx +58 -0
  126. package/web/src/components/SearchBar.tsx +71 -0
  127. package/web/src/components/SearchPanel.tsx +152 -0
  128. package/web/src/components/ThemeEditor.tsx +129 -0
  129. package/web/src/components/ThemeSelector.tsx +41 -0
  130. package/web/src/components/VaultList.tsx +89 -0
  131. package/web/src/hooks/AuthContext.tsx +57 -0
  132. package/web/src/hooks/ThemeContext.tsx +77 -0
  133. package/web/src/hooks/useWebSocket.ts +34 -0
  134. package/web/src/main.tsx +10 -0
  135. package/web/src/styles/global.css +449 -0
  136. package/web/tsconfig.json +21 -0
  137. package/web/vite.config.ts +19 -0
@@ -0,0 +1,157 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { EventEmitter } from 'events';
3
+
4
+ interface SyncMessage {
5
+ type: 'note_created' | 'note_updated' | 'note_deleted' | 'sync_request' | 'sync_response';
6
+ vaultPath?: string;
7
+ note?: {
8
+ path: string;
9
+ content?: string;
10
+ timestamp?: string;
11
+ };
12
+ timestamp?: string;
13
+ }
14
+
15
+ interface Client {
16
+ id: string;
17
+ socket: WebSocket;
18
+ subscribedVaults: Set<string>;
19
+ }
20
+
21
+ export class SyncService extends EventEmitter {
22
+ private wss: WebSocketServer | null = null;
23
+ private clients: Map<string, Client> = new Map();
24
+ private port: number;
25
+ private isRunning: boolean = false;
26
+
27
+ constructor(port: number = 3001) {
28
+ super();
29
+ this.port = port;
30
+ }
31
+
32
+ start(): void {
33
+ if (this.isRunning) return;
34
+
35
+ this.wss = new WebSocketServer({ port: this.port });
36
+
37
+ this.wss.on('connection', (socket) => {
38
+ const clientId = this.generateClientId();
39
+ const client: Client = {
40
+ id: clientId,
41
+ socket,
42
+ subscribedVaults: new Set(),
43
+ };
44
+ this.clients.set(clientId, client);
45
+
46
+ socket.on('message', (data) => {
47
+ try {
48
+ const message: SyncMessage = JSON.parse(data.toString());
49
+ this.handleMessage(client, message);
50
+ } catch (error) {
51
+ console.error('Invalid sync message:', error);
52
+ }
53
+ });
54
+
55
+ socket.on('close', () => {
56
+ this.clients.delete(clientId);
57
+ });
58
+
59
+ socket.on('error', (error) => {
60
+ console.error('WebSocket error:', error);
61
+ this.clients.delete(clientId);
62
+ });
63
+ });
64
+
65
+ this.isRunning = true;
66
+ console.error(`Sync service running on port ${this.port}`);
67
+ }
68
+
69
+ stop(): void {
70
+ if (!this.isRunning) return;
71
+
72
+ for (const client of this.clients.values()) {
73
+ client.socket.close();
74
+ }
75
+ this.clients.clear();
76
+
77
+ if (this.wss) {
78
+ this.wss.close();
79
+ this.wss = null;
80
+ }
81
+
82
+ this.isRunning = false;
83
+ }
84
+
85
+ broadcastChange(message: SyncMessage): void {
86
+ const vaultPath = message.note?.path
87
+ ? message.note.path.split('/')[0]
88
+ : undefined;
89
+
90
+ const fullMessage: SyncMessage = {
91
+ ...message,
92
+ timestamp: new Date().toISOString(),
93
+ };
94
+
95
+ for (const client of this.clients.values()) {
96
+ if (vaultPath && client.subscribedVaults.has(vaultPath)) {
97
+ this.sendToClient(client, fullMessage);
98
+ } else if (!vaultPath) {
99
+ this.sendToClient(client, fullMessage);
100
+ }
101
+ }
102
+
103
+ this.emit('change', fullMessage);
104
+ }
105
+
106
+ subscribeToVault(clientId: string, vaultPath: string): boolean {
107
+ const client = this.clients.get(clientId);
108
+ if (!client) return false;
109
+
110
+ client.subscribedVaults.add(vaultPath);
111
+ return true;
112
+ }
113
+
114
+ unsubscribeFromVault(clientId: string, vaultPath: string): boolean {
115
+ const client = this.clients.get(clientId);
116
+ if (!client) return false;
117
+
118
+ client.subscribedVaults.delete(vaultPath);
119
+ return true;
120
+ }
121
+
122
+ private handleMessage(client: Client, message: SyncMessage): void {
123
+ switch (message.type) {
124
+ case 'sync_request':
125
+ this.handleSyncRequest(client, message);
126
+ break;
127
+ case 'note_created':
128
+ case 'note_updated':
129
+ case 'note_deleted':
130
+ this.broadcastChange(message);
131
+ break;
132
+ }
133
+ }
134
+
135
+ private handleSyncRequest(client: Client, _message: SyncMessage): void {
136
+ // Send acknowledgment with current state
137
+ const response: SyncMessage = {
138
+ type: 'sync_response',
139
+ timestamp: new Date().toISOString(),
140
+ };
141
+ this.sendToClient(client, response);
142
+ }
143
+
144
+ private sendToClient(client: Client, message: SyncMessage): void {
145
+ if (client.socket.readyState === WebSocket.OPEN) {
146
+ client.socket.send(JSON.stringify(message));
147
+ }
148
+ }
149
+
150
+ private generateClientId(): string {
151
+ return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
152
+ }
153
+
154
+ getConnectedClients(): number {
155
+ return this.clients.size;
156
+ }
157
+ }
@@ -0,0 +1,286 @@
1
+ import fs from 'fs/promises';
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
+ import path from 'path';
4
+ import { Note, Vault } from '../shared/types.js';
5
+ import { FullTextIndexer, SearchResult, SearchDoc } from './search.js';
6
+
7
+ export class VaultManager {
8
+ private vaults: Map<string, Vault> = new Map();
9
+ private indexer: FullTextIndexer;
10
+
11
+ constructor(indexDir?: string) {
12
+ this.indexer = new FullTextIndexer(indexDir);
13
+ }
14
+
15
+ async initIndex(): Promise<void> {
16
+ await this.indexer.load();
17
+ }
18
+
19
+ async persistIndex(): Promise<void> {
20
+ await this.indexer.save();
21
+ }
22
+
23
+ listVaults(): Vault[] {
24
+ return Array.from(this.vaults.values());
25
+ }
26
+
27
+ openVault(vaultPath: string): Vault | null {
28
+ try {
29
+ const stat = statSync(vaultPath);
30
+ if (!stat.isDirectory()) {
31
+ throw new Error('Vault path is not a directory');
32
+ }
33
+ } catch {
34
+ return null;
35
+ }
36
+
37
+ const vaultConfigPath = path.join(vaultPath, '.vault.json');
38
+ let config: Partial<Vault> = {};
39
+
40
+ try {
41
+ const configData = readFileSync(vaultConfigPath, 'utf-8');
42
+ config = JSON.parse(configData);
43
+ } catch {
44
+ config = { name: path.basename(vaultPath) };
45
+ }
46
+
47
+ const vault: Vault = {
48
+ path: vaultPath,
49
+ name: config.name || path.basename(vaultPath),
50
+ created: config.created || new Date().toISOString(),
51
+ notes: this.loadNotesFromVault(vaultPath),
52
+ };
53
+
54
+ this.vaults.set(vaultPath, vault);
55
+
56
+ for (const note of vault.notes) {
57
+ this.indexer.indexNote(this.toSearchDoc(note, vaultPath));
58
+ }
59
+
60
+ return vault;
61
+ }
62
+
63
+ createVault(name: string, vaultPath: string): Vault {
64
+ const vault: Vault = {
65
+ path: vaultPath,
66
+ name,
67
+ created: new Date().toISOString(),
68
+ notes: [],
69
+ };
70
+
71
+ fs.mkdir(vaultPath, { recursive: true }).catch(console.error);
72
+ fs.writeFile(
73
+ path.join(vaultPath, '.vault.json'),
74
+ JSON.stringify({ name, created: vault.created }, null, 2)
75
+ ).catch(console.error);
76
+
77
+ this.vaults.set(vaultPath, vault);
78
+ return vault;
79
+ }
80
+
81
+ async createNote(vaultPath: string, filename: string, content: string): Promise<Note> {
82
+ const filePath = path.join(vaultPath, `${filename}.md`);
83
+ const parsed = this.parseMarkdown(content);
84
+
85
+ const note: Note = {
86
+ path: `${filename}.md`,
87
+ filename,
88
+ content,
89
+ frontmatter: parsed.frontmatter,
90
+ links: parsed.links,
91
+ created: new Date().toISOString(),
92
+ modified: new Date().toISOString(),
93
+ };
94
+
95
+ await fs.writeFile(filePath, content);
96
+ this.upsertNoteInVault(vaultPath, note);
97
+ this.indexer.indexNote(this.toSearchDoc(note, vaultPath));
98
+
99
+ return note;
100
+ }
101
+
102
+ async readNote(vaultPath: string, notePath: string): Promise<Note | null> {
103
+ const fullPath = path.join(vaultPath, notePath);
104
+
105
+ try {
106
+ const stat = await fs.stat(fullPath);
107
+ if (!stat.isFile() || !notePath.endsWith('.md')) {
108
+ return null;
109
+ }
110
+
111
+ const content = await fs.readFile(fullPath, 'utf-8');
112
+ const parsed = this.parseMarkdown(content);
113
+ const filename = path.basename(notePath, '.md');
114
+
115
+ return {
116
+ path: notePath,
117
+ filename,
118
+ content,
119
+ frontmatter: parsed.frontmatter,
120
+ links: parsed.links,
121
+ created: stat.birthtime.toISOString(),
122
+ modified: stat.mtime.toISOString(),
123
+ };
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ async updateNote(vaultPath: string, notePath: string, content: string): Promise<Note> {
130
+ const fullPath = path.join(vaultPath, notePath);
131
+ const parsed = this.parseMarkdown(content);
132
+
133
+ const note: Note = {
134
+ path: notePath,
135
+ filename: path.basename(notePath, '.md'),
136
+ content,
137
+ frontmatter: parsed.frontmatter,
138
+ links: parsed.links,
139
+ modified: new Date().toISOString(),
140
+ created: (await this.readNote(vaultPath, notePath))?.created || new Date().toISOString(),
141
+ };
142
+
143
+ await fs.writeFile(fullPath, content);
144
+ this.upsertNoteInVault(vaultPath, note);
145
+ this.indexer.indexNote(this.toSearchDoc(note, vaultPath));
146
+ return note;
147
+ }
148
+
149
+ async deleteNote(vaultPath: string, notePath: string): Promise<void> {
150
+ const fullPath = path.join(vaultPath, notePath);
151
+ await fs.unlink(fullPath);
152
+
153
+ const vault = this.vaults.get(vaultPath);
154
+ if (vault) {
155
+ vault.notes = vault.notes.filter(n => n.path !== notePath);
156
+ }
157
+ this.indexer.removeNote(notePath);
158
+ }
159
+
160
+ async searchNotes(vaultPath: string, query: string): Promise<Note[]> {
161
+ const results: Note[] = [];
162
+ const vault = this.vaults.get(vaultPath) ?? this.openVault(vaultPath);
163
+
164
+ if (!vault) return results;
165
+
166
+ for (const note of vault.notes) {
167
+ if (note.content.toLowerCase().includes(query.toLowerCase())) {
168
+ results.push(note);
169
+ }
170
+ }
171
+
172
+ return results;
173
+ }
174
+
175
+ async fullTextSearch(vaultPath: string, query: string, filters?: {
176
+ tag?: string;
177
+ after?: string;
178
+ before?: string;
179
+ hasBacklinks?: boolean;
180
+ }): Promise<SearchResult[]> {
181
+ const vault = this.vaults.get(vaultPath) ?? this.openVault(vaultPath);
182
+ if (!vault) return [];
183
+
184
+ const docs = vault.notes.map(n => this.toSearchDoc(n, vaultPath));
185
+ return this.indexer.search(query, docs, filters);
186
+ }
187
+
188
+ private toSearchDoc(note: Note, _vaultPath: string): SearchDoc {
189
+ return {
190
+ path: note.path,
191
+ filename: note.filename,
192
+ content: note.content,
193
+ tags: Array.isArray(note.frontmatter?.tags)
194
+ ? (note.frontmatter.tags as string[]).map(String)
195
+ : typeof note.frontmatter?.tags === 'string'
196
+ ? (note.frontmatter.tags as string).split(',').map(t => t.trim())
197
+ : [],
198
+ modified: note.modified,
199
+ backlinks: note.links.length,
200
+ };
201
+ }
202
+
203
+ private parseMarkdown(content: string): { frontmatter: Record<string, unknown>, links: string[] } {
204
+ const frontmatter: Record<string, unknown> = {};
205
+ const links: string[] = [];
206
+
207
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
208
+ if (frontmatterMatch) {
209
+ try {
210
+ const fmContent = frontmatterMatch[1];
211
+ fmContent.split('\n').forEach(line => {
212
+ const [key, ...valueParts] = line.split(':');
213
+ if (key && valueParts.length > 0) {
214
+ frontmatter[key.trim()] = valueParts.join(':').trim();
215
+ }
216
+ });
217
+ } catch {
218
+ // Invalid frontmatter, ignore
219
+ }
220
+ }
221
+
222
+ const wikiLinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
223
+ const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
224
+
225
+ let match;
226
+ while ((match = wikiLinkRegex.exec(content)) !== null) {
227
+ links.push(match[1]);
228
+ }
229
+ while ((match = mdLinkRegex.exec(content)) !== null) {
230
+ if (!match[2].startsWith('http')) {
231
+ links.push(match[2].replace(/\.md$/, ''));
232
+ }
233
+ }
234
+
235
+ return { frontmatter, links };
236
+ }
237
+
238
+ private loadNotesFromVault(vaultPath: string): Note[] {
239
+ if (!existsSync(vaultPath)) {
240
+ return [];
241
+ }
242
+
243
+ const notes: Note[] = [];
244
+
245
+ for (const entry of readdirSync(vaultPath)) {
246
+ if (!entry.endsWith('.md')) {
247
+ continue;
248
+ }
249
+
250
+ const fullPath = path.join(vaultPath, entry);
251
+ const stat = statSync(fullPath);
252
+ if (!stat.isFile()) {
253
+ continue;
254
+ }
255
+
256
+ const content = readFileSync(fullPath, 'utf-8');
257
+ const parsed = this.parseMarkdown(content);
258
+ notes.push({
259
+ path: entry,
260
+ filename: path.basename(entry, '.md'),
261
+ content,
262
+ frontmatter: parsed.frontmatter,
263
+ links: parsed.links,
264
+ created: stat.birthtime.toISOString(),
265
+ modified: stat.mtime.toISOString(),
266
+ });
267
+ }
268
+
269
+ return notes;
270
+ }
271
+
272
+ private upsertNoteInVault(vaultPath: string, note: Note): void {
273
+ const vault = this.vaults.get(vaultPath) ?? this.openVault(vaultPath);
274
+ if (!vault) {
275
+ return;
276
+ }
277
+
278
+ const noteIndex = vault.notes.findIndex((existingNote) => existingNote.path === note.path);
279
+ if (noteIndex >= 0) {
280
+ vault.notes[noteIndex] = note;
281
+ return;
282
+ }
283
+
284
+ vault.notes.push(note);
285
+ }
286
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { OpenSidianServer } from './server.js';
2
+ import { Config } from './shared/types.js';
3
+
4
+ const config: Partial<Config> = {
5
+ server: {
6
+ port: parseInt(process.env.PORT || '3000', 10),
7
+ host: process.env.HOST || '0.0.0.0',
8
+ },
9
+ vaults: {
10
+ defaultPath: process.env.VAULT_PATH || './vaults',
11
+ autoOpen: true,
12
+ },
13
+ sync: {
14
+ enabled: process.env.SYNC_ENABLED !== 'false',
15
+ port: parseInt(process.env.SYNC_PORT || '3001', 10),
16
+ },
17
+ plugins: {
18
+ enabled: process.env.PLUGINS_ENABLED !== 'false',
19
+ paths: process.env.PLUGIN_PATHS?.split(',') || ['./plugins'],
20
+ },
21
+ };
22
+
23
+ const server = new OpenSidianServer(config);
24
+
25
+ process.on('SIGTERM', () => {
26
+ console.log('SIGTERM received, shutting down...');
27
+ server.stop();
28
+ process.exit(0);
29
+ });
30
+
31
+ process.on('SIGINT', () => {
32
+ console.log('SIGINT received, shutting down...');
33
+ server.stop();
34
+ process.exit(0);
35
+ });
36
+
37
+ server.start();
package/src/mcp/cli.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from './server.js';
3
+
4
+ startServer().catch((err) => {
5
+ console.error('MCP Server failed:', err);
6
+ process.exit(1);
7
+ });