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.
- package/.eslintrc.json +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +35 -0
- package/.github/ISSUE_TEMPLATE/question.md +23 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +45 -0
- package/.github/README.md +5 -0
- package/.github/workflows/cd.yml +44 -0
- package/.github/workflows/ci.yml +128 -0
- package/.github/workflows/qa.yml +45 -0
- package/.planning/PROJECT.md +96 -0
- package/.planning/REQUIREMENTS.md +66 -0
- package/.planning/ROADMAP.md +129 -0
- package/.planning/STATE.md +47 -0
- package/.planning/config.json +14 -0
- package/CONTRIBUTING.md +232 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/api/auth.d.ts +5 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +112 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/routes.d.ts +3 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +119 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/api/themes.d.ts +3 -0
- package/dist/api/themes.d.ts.map +1 -0
- package/dist/api/themes.js +48 -0
- package/dist/api/themes.js.map +1 -0
- package/dist/core/graph.d.ts +16 -0
- package/dist/core/graph.d.ts.map +1 -0
- package/dist/core/graph.js +115 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/markdown.d.ts +21 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +77 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/search.d.ts +34 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +159 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sync.d.ts +30 -0
- package/dist/core/sync.d.ts.map +1 -0
- package/dist/core/sync.js +121 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/vault.d.ts +28 -0
- package/dist/core/vault.d.ts.map +1 -0
- package/dist/core/vault.js +235 -0
- package/dist/core/vault.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/cli.d.ts +3 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +7 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/server.d.ts +18 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +272 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/plugins/host.d.ts +23 -0
- package/dist/plugins/host.d.ts.map +1 -0
- package/dist/plugins/host.js +104 -0
- package/dist/plugins/host.js.map +1 -0
- package/dist/plugins/sample-plugin.d.ts +10 -0
- package/dist/plugins/sample-plugin.d.ts.map +1 -0
- package/dist/plugins/sample-plugin.js +23 -0
- package/dist/plugins/sample-plugin.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +77 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/types.d.ts +86 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/docker/Dockerfile +37 -0
- package/docker/docker-compose.yml +46 -0
- package/docs/ARCHITECTURE.md +321 -0
- package/futuras_implementacoes.md +0 -0
- package/package.json +65 -0
- package/scripts/fix-gitignore.ps1 +5 -0
- package/scripts/seed-notes.mjs +60 -0
- package/src/api/auth.ts +130 -0
- package/src/api/routes.ts +133 -0
- package/src/api/themes.ts +60 -0
- package/src/core/graph.ts +145 -0
- package/src/core/markdown.ts +92 -0
- package/src/core/search.ts +208 -0
- package/src/core/sync.ts +157 -0
- package/src/core/vault.ts +286 -0
- package/src/index.ts +37 -0
- package/src/mcp/cli.ts +7 -0
- package/src/mcp/server.ts +296 -0
- package/src/plugins/host.ts +120 -0
- package/src/plugins/sample-plugin.ts +29 -0
- package/src/server.ts +90 -0
- package/src/shared/types.ts +92 -0
- package/tests/api/routes.test.ts +167 -0
- package/tests/core/graph.test.ts +236 -0
- package/tests/core/markdown.test.ts +157 -0
- package/tests/core/search.test.ts +132 -0
- package/tests/core/sync.test.ts +62 -0
- package/tests/core/vault.test.ts +162 -0
- package/tests/mcp/server.test.ts +118 -0
- package/tests/plugins/host.test.ts +165 -0
- package/tests/plugins/sample-plugin.test.ts +35 -0
- package/tests/server.test.ts +76 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +27 -0
- package/vitest.config.ts +33 -0
- package/web/index.html +13 -0
- package/web/package.json +26 -0
- package/web/public/favicon.svg +4 -0
- package/web/src/App.tsx +63 -0
- package/web/src/api/auth.ts +65 -0
- package/web/src/api/client.ts +117 -0
- package/web/src/api/themes.ts +78 -0
- package/web/src/components/GraphView.tsx +139 -0
- package/web/src/components/Layout.tsx +74 -0
- package/web/src/components/LoginPage.tsx +52 -0
- package/web/src/components/NoteEditor.tsx +114 -0
- package/web/src/components/NoteList.tsx +95 -0
- package/web/src/components/RegisterPage.tsx +58 -0
- package/web/src/components/SearchBar.tsx +71 -0
- package/web/src/components/SearchPanel.tsx +152 -0
- package/web/src/components/ThemeEditor.tsx +129 -0
- package/web/src/components/ThemeSelector.tsx +41 -0
- package/web/src/components/VaultList.tsx +89 -0
- package/web/src/hooks/AuthContext.tsx +57 -0
- package/web/src/hooks/ThemeContext.tsx +77 -0
- package/web/src/hooks/useWebSocket.ts +34 -0
- package/web/src/main.tsx +10 -0
- package/web/src/styles/global.css +449 -0
- package/web/tsconfig.json +21 -0
- 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":"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"}
|
|
@@ -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,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);
|
package/src/api/auth.ts
ADDED
|
@@ -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;
|