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 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACrF,WAAW,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACrD,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACpD,eAAe,IAAI,YAAY,CAAC;IAChC,cAAc,IAAI,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,IAAI,EAAE;QACJ,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,37 @@
1
+ FROM node:20-alpine AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+
7
+ RUN npm ci
8
+
9
+ COPY . .
10
+
11
+ RUN npm run build
12
+
13
+ FROM node:20-alpine AS runner
14
+
15
+ WORKDIR /app
16
+
17
+ ENV NODE_ENV=production
18
+
19
+ RUN addgroup -g 1001 -S nodejs && \
20
+ adduser -S opensidian -u 1001
21
+
22
+ COPY --from=builder /app/dist ./dist
23
+ COPY --from=builder /app/package*.json ./
24
+
25
+ RUN npm ci --only=production && \
26
+ npm cache clean --force
27
+
28
+ RUN chown -R opensidian:nodejs /app
29
+
30
+ USER opensidian
31
+
32
+ EXPOSE 3000 3001
33
+
34
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
35
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
36
+
37
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,46 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ opensidian:
5
+ build:
6
+ context: .
7
+ dockerfile: docker/Dockerfile
8
+ container_name: opensidian
9
+ restart: unless-stopped
10
+ ports:
11
+ - "3000:3000"
12
+ - "3001:3001"
13
+ volumes:
14
+ - ./vaults:/app/vaults
15
+ - ./data:/app/data
16
+ environment:
17
+ - NODE_ENV=production
18
+ - PORT=3000
19
+ - SYNC_PORT=3001
20
+ - VAULT_PATH=/app/vaults
21
+ - SYNC_ENABLED=true
22
+ - PLUGINS_ENABLED=true
23
+ networks:
24
+ - opensidian-network
25
+
26
+ opensidian-mcp:
27
+ build:
28
+ context: .
29
+ dockerfile: docker/Dockerfile
30
+ container_name: opensidian-mcp
31
+ restart: unless-stopped
32
+ command: ["node", "dist/mcp/server.js"]
33
+ ports:
34
+ - "3002:3002"
35
+ volumes:
36
+ - ./vaults:/app/vaults
37
+ environment:
38
+ - NODE_ENV=production
39
+ networks:
40
+ - opensidian-network
41
+ depends_on:
42
+ - opensidian
43
+
44
+ networks:
45
+ opensidian-network:
46
+ driver: bridge
@@ -0,0 +1,321 @@
1
+ # OpenSidian Architecture
2
+
3
+ ## Overview
4
+
5
+ OpenSidian is a modular, extensible knowledge management system built with TypeScript/Node.js. It provides vault management, Markdown parsing, knowledge graph indexing, real-time synchronization, and a plugin architecture.
6
+
7
+ ## System Architecture
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────────┐
11
+ │ Client Layer │
12
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
13
+ │ │ Web UI │ │ CLI │ │ MCP Client │ │
14
+ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │
15
+ ├─────────────────────────────────────────────────────────────┤
16
+ │ API Gateway Layer │
17
+ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │
18
+ │ │ REST API (Express) │ │ WebSocket (Sync) │ │
19
+ │ │ Port 3000 │ │ Port 3001 │ │
20
+ │ └─────────────────────────┘ └─────────────────────────┘ │
21
+ ├─────────────────────────────────────────────────────────────┤
22
+ │ Service Layer │
23
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
24
+ │ │ Vault │ │ Markdown │ │ Graph │ │ Plugin │ │
25
+ │ │ Manager │ │ Parser │ │ Engine │ │ Host │ │
26
+ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
27
+ ├─────────────────────────────────────────────────────────────┤
28
+ │ MCP Server Layer │
29
+ │ ┌─────────────────────────────────────────────────────┐ │
30
+ │ │ Model Context Protocol Server │ │
31
+ │ │ (Stdio Transport) │ │
32
+ │ └─────────────────────────────────────────────────────┘ │
33
+ ├─────────────────────────────────────────────────────────────┤
34
+ │ Storage Layer │
35
+ │ ┌─────────────────┐ ┌─────────────────────────────────┐ │
36
+ │ │ File System │ │ In-Memory Graph Index │ │
37
+ │ │ (Vaults) │ │ (GraphEngine) │ │
38
+ │ └─────────────────┘ └─────────────────────────────────┘ │
39
+ └─────────────────────────────────────────────────────────────┘
40
+ ```
41
+
42
+ ## Core Components
43
+
44
+ ### 1. Vault Manager (`src/core/vault.ts`)
45
+
46
+ Responsible for vault and note lifecycle management.
47
+
48
+ **Responsibilities:**
49
+ - Create, open, and delete vaults
50
+ - Create, read, update, and delete notes
51
+ - Search notes by content
52
+ - Parse Markdown content for links and frontmatter
53
+
54
+ **Key Classes:**
55
+ - `VaultManager` - Main vault management class
56
+
57
+ **Public API:**
58
+ ```typescript
59
+ class VaultManager {
60
+ listVaults(): Vault[]
61
+ openVault(vaultPath: string): Vault | null
62
+ createVault(name: string, vaultPath: string): Vault
63
+ createNote(vaultPath: string, filename: string, content: string): Promise<Note>
64
+ readNote(vaultPath: string, notePath: string): Promise<Note | null>
65
+ updateNote(vaultPath: string, notePath: string, content: string): Promise<Note>
66
+ deleteNote(vaultPath: string, notePath: string): Promise<void>
67
+ searchNotes(vaultPath: string, query: string): Promise<Note[]>
68
+ }
69
+ ```
70
+
71
+ ### 2. Markdown Parser (`src/core/markdown.ts`)
72
+
73
+ Handles Markdown parsing, link extraction, and frontmatter processing.
74
+
75
+ **Responsibilities:**
76
+ - Parse Markdown to HTML using `marked`
77
+ - Extract frontmatter using `front-matter`
78
+ - Extract wiki links `[[...]]` and Markdown links `[text](url)`
79
+ - Extract headings with auto-generated IDs
80
+ - Convert notes back to Markdown format
81
+
82
+ **Key Classes:**
83
+ - `MarkdownParser` - Main parsing class
84
+
85
+ **Public API:**
86
+ ```typescript
87
+ class MarkdownParser {
88
+ parse<T>(content: string): ParsedNote<T>
89
+ extractLinks(content: string): string[]
90
+ extractHeadings(content: string): Heading[]
91
+ slugify(text: string): string
92
+ toMarkdown(note: { frontmatter?: object, body: string }): string
93
+ }
94
+ ```
95
+
96
+ ### 3. Graph Engine (`src/core/graph.ts`)
97
+
98
+ Maintains the knowledge graph for vault navigation.
99
+
100
+ **Responsibilities:**
101
+ - Index notes and their links
102
+ - Track graph nodes (notes) and edges (links)
103
+ - Query neighbors and backlinks
104
+ - Search by tags
105
+ - Compute graph statistics
106
+
107
+ **Key Classes:**
108
+ - `GraphEngine` - Main graph management class
109
+
110
+ **Public API:**
111
+ ```typescript
112
+ class GraphEngine {
113
+ indexNote(vaultPath: string, note: Note): void
114
+ removeNote(vaultPath: string, notePath: string): void
115
+ getGraph(vaultPath: string): Graph | null
116
+ getNeighbors(vaultPath: string, notePath: string): GraphNode[]
117
+ getBacklinks(vaultPath: string, notePath: string): GraphNode[]
118
+ searchByTag(vaultPath: string, tag: string): GraphNode[]
119
+ computeStats(vaultPath: string): { mostConnected: GraphNode[], orphanNotes: GraphNode[] }
120
+ }
121
+ ```
122
+
123
+ ### 4. Sync Service (`src/core/sync.ts`)
124
+
125
+ Provides real-time synchronization via WebSockets.
126
+
127
+ **Responsibilities:**
128
+ - Manage WebSocket connections
129
+ - Broadcast note changes to subscribed clients
130
+ - Handle sync requests and responses
131
+
132
+ **Key Classes:**
133
+ - `SyncService` - WebSocket server with event emitter
134
+
135
+ **Public API:**
136
+ ```typescript
137
+ class SyncService extends EventEmitter {
138
+ start(): void
139
+ stop(): void
140
+ broadcastChange(message: SyncMessage): void
141
+ subscribeToVault(clientId: string, vaultPath: string): boolean
142
+ unsubscribeFromVault(clientId: string, vaultPath: string): boolean
143
+ getConnectedClients(): number
144
+ }
145
+ ```
146
+
147
+ ### 5. Plugin Host (`src/plugins/host.ts`)
148
+
149
+ Provides extensibility through a plugin system.
150
+
151
+ **Responsibilities:**
152
+ - Load and unload plugins
153
+ - Manage plugin commands and hooks
154
+ - Execute hooks on note events
155
+
156
+ **Key Classes:**
157
+ - `PluginHost` - Plugin lifecycle manager
158
+
159
+ **Public API:**
160
+ ```typescript
161
+ class PluginHost {
162
+ loadPlugin(plugin: Plugin): Promise<void>
163
+ unloadPlugin(name: string): Promise<void>
164
+ registerCommand(command: PluginCommand): void
165
+ registerHook(hook: string, handler: Function): void
166
+ executeHook(hook: string, ...args: unknown[]): Promise<unknown[]>
167
+ getCommand(id: string): PluginCommand | undefined
168
+ listCommands(): PluginCommand[]
169
+ listPlugins(): string[]
170
+ }
171
+ ```
172
+
173
+ ### 6. MCP Server (`src/mcp/server.ts`)
174
+
175
+ Model Context Protocol server for AI integration.
176
+
177
+ **Responsibilities:**
178
+ - Expose tools for vault and note operations
179
+ - Handle MCP protocol requests via stdio
180
+ - Integrate with all core services
181
+
182
+ **Tools Exposed:**
183
+ - `vault_list`, `vault_open`, `vault_create`
184
+ - `note_create`, `note_read`, `note_update`, `note_delete`, `note_search`
185
+ - `graph_get`, `graph_neighbors`
186
+
187
+ ## Data Flow
188
+
189
+ ### Note Creation Flow
190
+
191
+ ```
192
+ Client -> REST API / MCP Tool
193
+ |
194
+ v
195
+ VaultManager.createNote()
196
+ |
197
+ +-> MarkdownParser.parse() (extract links, frontmatter)
198
+ |
199
+ v
200
+ Note stored to filesystem
201
+ |
202
+ v
203
+ GraphEngine.indexNote() (update graph)
204
+ |
205
+ v
206
+ SyncService.broadcastChange() (notify clients)
207
+ ```
208
+
209
+ ### Graph Building Flow
210
+
211
+ ```
212
+ Note saved to vault
213
+ |
214
+ v
215
+ MarkdownParser.extractLinks() -> wiki links + md links
216
+ |
217
+ v
218
+ GraphEngine.indexNote() -> create/update node, create edges
219
+ |
220
+ v
221
+ Graph metadata updated (totalNotes, totalLinks)
222
+ ```
223
+
224
+ ## API Routes
225
+
226
+ ### REST API (`src/api/routes.ts`)
227
+
228
+ **Vaults:**
229
+ - `GET /api/vaults` - List all vaults
230
+ - `POST /api/vaults` - Create a vault
231
+ - `GET /api/vaults/:id` - Get vault info
232
+ - `DELETE /api/vaults/:id` - Delete vault (not implemented)
233
+
234
+ **Notes:**
235
+ - `GET /api/notes?vault=<path>` - List notes in vault
236
+ - `POST /api/notes` - Create a note
237
+ - `GET /api/notes/:path?vault=<path>` - Get note content
238
+ - `PUT /api/notes/:path?vault=<path>` - Update note
239
+ - `DELETE /api/notes/:path?vault=<path>` - Delete note
240
+
241
+ **Graph:**
242
+ - `GET /api/graph?vault=<path>` - Get full graph
243
+ - `GET /api/graph/neighbors/:path?vault=<path>` - Get note neighbors
244
+
245
+ ## Configuration
246
+
247
+ Configuration is provided via environment variables or `config.json`:
248
+
249
+ ```typescript
250
+ interface Config {
251
+ server: {
252
+ port: number; // Default: 3000
253
+ host: string; // Default: '0.0.0.0'
254
+ };
255
+ vaults: {
256
+ defaultPath: string; // Default: './vaults'
257
+ autoOpen: boolean; // Default: true
258
+ };
259
+ sync: {
260
+ enabled: boolean; // Default: true
261
+ port: number; // Default: 3001
262
+ };
263
+ plugins: {
264
+ enabled: boolean; // Default: true
265
+ paths: string[]; // Default: ['./plugins']
266
+ };
267
+ }
268
+ ```
269
+
270
+ ## Error Handling
271
+
272
+ All layers use consistent error handling:
273
+
274
+ 1. **MCP Server**: Returns error messages in `isError: true` response
275
+ 2. **REST API**: Returns appropriate HTTP status codes with JSON error body
276
+ 3. **Core Services**: Throw typed errors with descriptive messages
277
+
278
+ ## Extension Points
279
+
280
+ ### Plugin Hooks
281
+
282
+ Available hooks for plugins:
283
+
284
+ - `note:pre-save` - Before note is saved (can modify note)
285
+ - `note:post-save` - After note is saved (side effects)
286
+ - `vault:pre-create` - Before vault is created
287
+ - `vault:post-create` - After vault is created
288
+ - `graph:pre-index` - Before note is indexed in graph
289
+ - `graph:post-index` - After note is indexed
290
+
291
+ ### Plugin Commands
292
+
293
+ Plugins can register commands that can be triggered via API or UI.
294
+
295
+ ## Testing Strategy
296
+
297
+ Tests are located in `tests/` directory:
298
+
299
+ - `tests/core/vault.test.ts` - VaultManager tests
300
+ - `tests/core/markdown.test.ts` - MarkdownParser tests
301
+ - `tests/core/graph.test.ts` - GraphEngine tests
302
+ - `tests/core/sync.test.ts` - SyncService tests
303
+ - `tests/plugins/host.test.ts` - PluginHost tests
304
+
305
+ Coverage threshold: >80% for statements, branches, functions, and lines.
306
+
307
+ ## Docker Deployment
308
+
309
+ The application can be deployed via Docker:
310
+
311
+ - **Dockerfile**: Multi-stage build (builder + runner)
312
+ - **docker-compose.yml**: Full stack with MCP server
313
+
314
+ Volumes:
315
+ - `./vaults` - Persist vault data
316
+ - `./data` - Application data
317
+
318
+ Ports:
319
+ - 3000: REST API
320
+ - 3001: WebSocket sync
321
+ - 3002: MCP server (in docker-compose)
File without changes
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "opensidian",
3
+ "version": "1.0.0",
4
+ "description": "Open-source knowledge management system with MCP server, graph indexing, and real-time sync",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "opensidian-mcp": "./dist/mcp/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx watch src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
17
+ "lint": "eslint src --ext .ts",
18
+ "lint:fix": "eslint src --ext .ts --fix",
19
+ "typecheck": "tsc --noEmit",
20
+ "prepare": "husky install"
21
+ },
22
+ "keywords": [
23
+ "knowledge-management",
24
+ "notes",
25
+ "markdown",
26
+ "graph",
27
+ "mcp",
28
+ "plugin",
29
+ "obsidian"
30
+ ],
31
+ "author": "OpenSidian Contributors",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "cors": "^2.8.5",
36
+ "d3": "^7.9.0",
37
+ "express": "^4.21.0",
38
+ "front-matter": "^4.0.2",
39
+ "gray-matter": "^4.0.3",
40
+ "marked": "^14.1.2",
41
+ "ws": "^8.18.0",
42
+ "yaml": "^2.5.0",
43
+ "zod": "^3.23.8"
44
+ },
45
+ "devDependencies": {
46
+ "@types/cors": "^2.8.17",
47
+ "@types/d3": "^7.4.3",
48
+ "@types/express": "^4.17.21",
49
+ "@types/node": "^22.5.0",
50
+ "@types/ws": "^8.5.12",
51
+ "@vitest/coverage-v8": "^2.1.1",
52
+ "@typescript-eslint/eslint-plugin": "^8.3.0",
53
+ "@typescript-eslint/parser": "^8.3.0",
54
+ "eslint": "^9.11.0",
55
+ "husky": "^9.1.5",
56
+ "lint-staged": "^15.2.10",
57
+ "tsx": "^4.19.1",
58
+ "typescript": "^5.6.2",
59
+ "vite": "^5.4.8",
60
+ "vitest": "^2.1.1"
61
+ },
62
+ "engines": {
63
+ "node": ">=20.0.0"
64
+ }
65
+ }
@@ -0,0 +1,5 @@
1
+ cd D:\Projetos\opensidian
2
+ git rm -r --cached local\
3
+ git add .gitignore
4
+ git commit -m "chore: ignore local vault files"
5
+ git push
@@ -0,0 +1,60 @@
1
+ const BASE = 'http://localhost:3000/api';
2
+
3
+ async function createNote(filename, content) {
4
+ const res = await fetch(`${BASE}/notes`, {
5
+ method: 'POST',
6
+ headers: { 'Content-Type': 'application/json' },
7
+ body: JSON.stringify({ vaultPath: 'local', filename, content }),
8
+ });
9
+ const data = await res.json();
10
+ console.log(`✅ ${filename}:`, data.note?.path || data);
11
+ }
12
+
13
+ async function main() {
14
+ await createNote('typescript-dicas', `# Dicas de TypeScript
15
+
16
+ ## Tipos úteis
17
+ - \`Partial<T>\` - deixa todas propriedades opcionais
18
+ - \`Pick<T, K>\` - pega só algumas propriedades
19
+ - \`Omit<T, K>\` - remove propriedades
20
+
21
+ tags: typescript, programação`);
22
+
23
+ await createNote('react-hooks', `# React Hooks Essenciais
24
+
25
+ ## useState
26
+ Estado local em componentes funcionais.
27
+
28
+ ## useEffect
29
+ Efeitos colaterais (API, timers, subscriptions).
30
+
31
+ ## useCallback
32
+ Memoriza funções para evitar re-renders.
33
+
34
+ tags: react, frontend`);
35
+
36
+ await createNote('aprendendo-python', `# Aprendendo Python
37
+
38
+ Python é ótimo para começar a programar.
39
+
40
+ ## Vantagens
41
+ - Sintaxe limpa e legível
42
+ - Ótimo para automação e scripts
43
+ - Comunidade gigante
44
+
45
+ tags: python, iniciante`);
46
+
47
+ await createNote('arquitetura-limpa', `# Arquitetura Limpa
48
+
49
+ Princípios para organizar código de forma sustentável.
50
+
51
+ ## Camadas
52
+ 1. **Entidades** - regras de negócio
53
+ 2. **Casos de uso** - orquestração
54
+ 3. **Adaptadores** - ponte entre camadas
55
+ 4. **Frameworks** - detalhes externos
56
+
57
+ tags: arquitetura, boas-praticas`);
58
+ }
59
+
60
+ main().catch(console.error);
@@ -0,0 +1,130 @@
1
+ import crypto from 'crypto';
2
+ import express, { Request, Response, NextFunction } from 'express';
3
+
4
+ interface User {
5
+ id: string;
6
+ email: string;
7
+ name: string;
8
+ passwordHash: string;
9
+ salt: string;
10
+ createdAt: string;
11
+ }
12
+
13
+ const users: User[] = [];
14
+ const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
15
+
16
+ function hashPassword(password: string, salt: string): string {
17
+ return crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha256').toString('hex');
18
+ }
19
+
20
+ function generateSalt(): string {
21
+ return crypto.randomBytes(16).toString('hex');
22
+ }
23
+
24
+ function base64UrlEncode(str: string): string {
25
+ return Buffer.from(str).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
26
+ }
27
+
28
+ function base64UrlDecode(str: string): string {
29
+ str = str.replace(/-/g, '+').replace(/_/g, '/');
30
+ while (str.length % 4) str += '=';
31
+ return Buffer.from(str, 'base64').toString();
32
+ }
33
+
34
+ function createToken(payload: Record<string, string | number>): string {
35
+ const header = base64UrlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
36
+ const body = base64UrlEncode(JSON.stringify({ ...payload, iat: Date.now() }));
37
+ const signature = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
38
+ return `${header}.${body}.${signature}`;
39
+ }
40
+
41
+ function verifyToken(token: string): Record<string, unknown> | null {
42
+ const parts = token.split('.');
43
+ if (parts.length !== 3) return null;
44
+ const signature = crypto.createHmac('sha256', JWT_SECRET).update(`${parts[0]}.${parts[1]}`).digest('base64url');
45
+ if (signature !== parts[2]) return null;
46
+ try {
47
+ return JSON.parse(base64UrlDecode(parts[1]));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
54
+ const header = req.headers.authorization;
55
+ if (!header?.startsWith('Bearer ')) {
56
+ res.status(401).json({ error: 'Token não fornecido' });
57
+ return;
58
+ }
59
+ const payload = verifyToken(header.slice(7));
60
+ if (!payload || !payload.userId) {
61
+ res.status(401).json({ error: 'Token inválido ou expirado' });
62
+ return;
63
+ }
64
+ (req as Request & { user: { userId: string; email: string } }).user = {
65
+ userId: payload.userId as string,
66
+ email: payload.email as string,
67
+ };
68
+ next();
69
+ }
70
+
71
+ const router = express.Router();
72
+
73
+ router.post('/register', (req: Request, res: Response) => {
74
+ const { email, password, name } = req.body;
75
+ if (!email || !password || !name) {
76
+ res.status(400).json({ error: 'email, password e name são obrigatórios' });
77
+ return;
78
+ }
79
+ if (password.length < 6) {
80
+ res.status(400).json({ error: 'Senha deve ter no mínimo 6 caracteres' });
81
+ return;
82
+ }
83
+ if (users.find(u => u.email === email)) {
84
+ res.status(409).json({ error: 'Email já cadastrado' });
85
+ return;
86
+ }
87
+ const salt = generateSalt();
88
+ const user: User = {
89
+ id: crypto.randomUUID(),
90
+ email,
91
+ name,
92
+ passwordHash: hashPassword(password, salt),
93
+ salt,
94
+ createdAt: new Date().toISOString(),
95
+ };
96
+ users.push(user);
97
+ const token = createToken({ userId: user.id, email: user.email });
98
+ res.status(201).json({ token, user: { id: user.id, email: user.email, name: user.name } });
99
+ });
100
+
101
+ router.post('/login', (req: Request, res: Response) => {
102
+ const { email, password } = req.body;
103
+ if (!email || !password) {
104
+ res.status(400).json({ error: 'email e password são obrigatórios' });
105
+ return;
106
+ }
107
+ const user = users.find(u => u.email === email);
108
+ if (!user || user.passwordHash !== hashPassword(password, user.salt)) {
109
+ res.status(401).json({ error: 'Email ou senha inválidos' });
110
+ return;
111
+ }
112
+ const token = createToken({ userId: user.id, email: user.email });
113
+ res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
114
+ });
115
+
116
+ router.get('/me', authMiddleware, (req: Request, res: Response) => {
117
+ const { userId } = (req as Request & { user: { userId: string; email: string } }).user;
118
+ const user = users.find(u => u.id === userId);
119
+ if (!user) {
120
+ res.status(404).json({ error: 'Usuário não encontrado' });
121
+ return;
122
+ }
123
+ res.json({ user: { id: user.id, email: user.email, name: user.name, createdAt: user.createdAt } });
124
+ });
125
+
126
+ router.post('/logout', (_req: Request, res: Response) => {
127
+ res.json({ message: 'Sessão encerrada' });
128
+ });
129
+
130
+ export default router;