@karmaniverous/jeeves-server 3.0.0 → 3.1.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/.tsbuildinfo +1 -1
- package/CHANGELOG.md +34 -1
- package/README.md +45 -0
- package/about.md +15 -39
- package/client/src/components/SearchModal.tsx +533 -222
- package/client/src/lib/api.ts +1 -0
- package/dist/client/assets/{CodeEditor-0XHVI8Nu.js → CodeEditor-DBeIVlfL.js} +1 -1
- package/dist/client/assets/{CodeViewer-CykMVsfX.js → CodeViewer-CbneqN2L.js} +1 -1
- package/dist/client/assets/index-D-RC7ZS6.css +1 -0
- package/dist/client/assets/index-fY6PleHE.js +62 -0
- package/dist/client/index.html +2 -2
- package/dist/src/routes/api/search.js +23 -1
- package/dist/src/routes/api/status.js +7 -1
- package/dist/src/routes/api/status.test.js +1 -1
- package/dist/src/services/eventLog.js +8 -0
- package/guides/api-integration.md +62 -155
- package/guides/deployment.md +60 -164
- package/guides/event-gateway.md +57 -50
- package/guides/exports.md +18 -20
- package/guides/index.md +21 -0
- package/guides/setup.md +211 -181
- package/guides/sharing.md +7 -7
- package/package.json +1 -1
- package/src/routes/api/search.ts +22 -1
- package/src/routes/api/status.test.ts +1 -1
- package/src/routes/api/status.ts +52 -41
- package/src/services/eventLog.ts +9 -0
- package/dist/client/assets/index-DbMebkkd.css +0 -1
- package/dist/client/assets/index-LjwgzZ7F.js +0 -62
package/dist/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<meta name="robots" content="noindex, nofollow" />
|
|
8
8
|
<title>Jeeves Server</title>
|
|
9
|
-
<script type="module" crossorigin src="/app/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/app/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/app/assets/index-fY6PleHE.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/app/assets/index-D-RC7ZS6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -119,6 +119,27 @@ export const searchRoutes = async (fastify) => {
|
|
|
119
119
|
let group = fileMap.get(key);
|
|
120
120
|
if (!group) {
|
|
121
121
|
const parts = key.split('/');
|
|
122
|
+
// Extract all non-internal payload fields as metadata
|
|
123
|
+
const meta = {};
|
|
124
|
+
const internalKeys = new Set([
|
|
125
|
+
'file_path',
|
|
126
|
+
'chunk_text',
|
|
127
|
+
'chunk_index',
|
|
128
|
+
'total_chunks',
|
|
129
|
+
'content_hash',
|
|
130
|
+
'embedded_at',
|
|
131
|
+
]);
|
|
132
|
+
for (const [k, v] of Object.entries(r.payload)) {
|
|
133
|
+
if (internalKeys.has(k) || v == null || v === '')
|
|
134
|
+
continue;
|
|
135
|
+
// Normalize singular 'domain' to 'domains' array to match facet field name
|
|
136
|
+
if (k === 'domain') {
|
|
137
|
+
meta['domains'] = Array.isArray(v) ? v : [v];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
meta[k] = v;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
122
143
|
group = {
|
|
123
144
|
filePath: r.payload.file_path ?? key,
|
|
124
145
|
browsePath: key,
|
|
@@ -132,6 +153,7 @@ export const searchRoutes = async (fastify) => {
|
|
|
132
153
|
title: r.payload.title,
|
|
133
154
|
author: r.payload.author,
|
|
134
155
|
participants: r.payload.participants,
|
|
156
|
+
metadata: meta,
|
|
135
157
|
chunks: [],
|
|
136
158
|
};
|
|
137
159
|
fileMap.set(key, group);
|
|
@@ -211,7 +233,7 @@ export const searchRoutes = async (fastify) => {
|
|
|
211
233
|
const watcherUrl = config.watcherUrl;
|
|
212
234
|
facetsFetchPromise = (async () => {
|
|
213
235
|
const watcherRes = await fetch(`${watcherUrl}/search/facets`, {
|
|
214
|
-
signal: AbortSignal.timeout(
|
|
236
|
+
signal: AbortSignal.timeout(15000),
|
|
215
237
|
});
|
|
216
238
|
if (!watcherRes.ok) {
|
|
217
239
|
throw new Error(`HTTP ${String(watcherRes.status)}`);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* event schemas, insider count (no PII), and export capabilities.
|
|
6
6
|
*/
|
|
7
7
|
import { getConfig } from '../../config/index.js';
|
|
8
|
+
import { getRecentEvents } from '../../services/eventLog.js';
|
|
8
9
|
import { packageVersion } from '../../util/packageVersion.js';
|
|
9
10
|
const startTime = Date.now();
|
|
10
11
|
async function checkService(url) {
|
|
@@ -27,7 +28,7 @@ async function checkService(url) {
|
|
|
27
28
|
}
|
|
28
29
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
29
30
|
export const statusRoutes = async (fastify) => {
|
|
30
|
-
fastify.get('/api/status', async () => {
|
|
31
|
+
fastify.get('/api/status', async (request) => {
|
|
31
32
|
const config = getConfig();
|
|
32
33
|
const [watcher, runner] = await Promise.all([
|
|
33
34
|
config.watcherUrl ? checkService(config.watcherUrl) : null,
|
|
@@ -67,6 +68,11 @@ export const statusRoutes = async (fastify) => {
|
|
|
67
68
|
watcher,
|
|
68
69
|
runner,
|
|
69
70
|
},
|
|
71
|
+
...(request.query.events
|
|
72
|
+
? {
|
|
73
|
+
eventLog: getRecentEvents(Math.min(parseInt(request.query.events, 10) || 20, 100)),
|
|
74
|
+
}
|
|
75
|
+
: {}),
|
|
70
76
|
};
|
|
71
77
|
});
|
|
72
78
|
};
|
|
@@ -36,7 +36,7 @@ describe('GET /api/status', () => {
|
|
|
36
36
|
await statusRoutes(fakeFastify, {});
|
|
37
37
|
const handler = routes['/api/status'];
|
|
38
38
|
expect(handler).toBeDefined();
|
|
39
|
-
const result = await handler({ accessMode: 'insider' });
|
|
39
|
+
const result = await handler({ accessMode: 'insider', query: {} });
|
|
40
40
|
const status = result;
|
|
41
41
|
expect(status).toHaveProperty('version');
|
|
42
42
|
expect(status).toHaveProperty('uptime');
|
|
@@ -53,3 +53,11 @@ export function logEvent(entry) {
|
|
|
53
53
|
// Write back
|
|
54
54
|
writeJsonl(eventLogPath, purgedEntries);
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the most recent N event log entries (newest first).
|
|
58
|
+
*/
|
|
59
|
+
export function getRecentEvents(limit) {
|
|
60
|
+
const { eventLogPath } = getConfig();
|
|
61
|
+
const entries = parseJsonl(eventLogPath);
|
|
62
|
+
return entries.slice(-limit).reverse();
|
|
63
|
+
}
|
|
@@ -4,15 +4,15 @@ How to interact with Jeeves Server programmatically — for scripts, bots, AI as
|
|
|
4
4
|
|
|
5
5
|
## Authentication for API Access
|
|
6
6
|
|
|
7
|
-
All API requests authenticate via `?key=<insider-key>` URL parameter or session cookie.
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
keys: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
7
|
+
All API requests (except `/health` and `/api/status`) authenticate via `?key=<insider-key>` URL parameter or session cookie.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"keys": {
|
|
12
|
+
"ci-bot": "random-seed-string",
|
|
13
|
+
"webhook": { "key": "another-seed", "scopes": ["/event"] }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
### Getting the derived key
|
|
@@ -20,7 +20,6 @@ keys: {
|
|
|
20
20
|
The config contains **seeds**. The actual URL key is derived via HMAC:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
# Get the insider key from a seed
|
|
24
23
|
curl -s "http://localhost:1934/insider-key" -H "X-API-Key: <seed>"
|
|
25
24
|
# Returns: { "key": "a1b2c3d4..." }
|
|
26
25
|
```
|
|
@@ -36,118 +35,78 @@ function insiderKey(seed) {
|
|
|
36
35
|
|
|
37
36
|
## API Endpoints
|
|
38
37
|
|
|
39
|
-
###
|
|
38
|
+
### Public (no auth required)
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
GET
|
|
44
|
-
|
|
40
|
+
| Method | Path | Description |
|
|
41
|
+
|--------|------|-------------|
|
|
42
|
+
| `GET` | `/health` | Simple health check (200 OK) |
|
|
43
|
+
| `GET` | `/api/status` | Server metadata: version, uptime, services, capabilities. Add `?events=N` for recent event log entries |
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
GET /api/file/d/docs/design.md?key=<key>&mode=raw
|
|
48
|
-
# Returns: { type: "text", content: "...", fileName: "..." }
|
|
45
|
+
### File Access (auth required)
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
| Method | Path | Description |
|
|
48
|
+
|--------|------|-------------|
|
|
49
|
+
| `GET` | `/api/file/<path>` | File content (rendered HTML for markdown, raw for others) |
|
|
50
|
+
| `GET` | `/api/raw/<path>` | Raw file bytes with appropriate Content-Type |
|
|
51
|
+
| `GET` | `/api/link-info/<path>` | Query available views and export formats for a path |
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
GET /path/d/docs/design.md?key=<key>&export=pdf
|
|
56
|
-
# Returns: application/pdf
|
|
53
|
+
### Directory Access
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
| Method | Path | Description |
|
|
56
|
+
|--------|------|-------------|
|
|
57
|
+
| `GET` | `/api/drives` | List available drives (Windows) or roots (Linux) |
|
|
58
|
+
| `GET` | `/api/directory/<path>` | List directory contents |
|
|
62
59
|
|
|
63
|
-
###
|
|
60
|
+
### Export
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
GET
|
|
68
|
-
|
|
62
|
+
| Method | Path | Description |
|
|
63
|
+
|--------|------|-------------|
|
|
64
|
+
| `GET` | `/api/export/<path>?format=pdf\|docx\|zip` | Export file or directory |
|
|
65
|
+
| `GET` | `/api/mermaid-export/<path>?format=svg\|png\|pdf` | Export Mermaid diagram |
|
|
66
|
+
| `GET` | `/api/plantuml-export/<path>?format=svg\|png\|pdf\|eps` | Export PlantUML diagram |
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
GET /api/directory/d/docs?key=<key>
|
|
72
|
-
# Returns: { path: "d/docs", entries: [{ name: "...", type: "file"|"directory", ... }] }
|
|
73
|
-
```
|
|
68
|
+
### Sharing
|
|
74
69
|
|
|
75
|
-
|
|
70
|
+
| Method | Path | Description |
|
|
71
|
+
|--------|------|-------------|
|
|
72
|
+
| `GET` | `/insider-key` | Get derived insider key (requires `X-API-Key` header with seed) |
|
|
73
|
+
| `GET` | `/key?path=<path>` | Compute outsider key for a path |
|
|
74
|
+
| `POST` | `/rotate-key` | Rotate an insider's key (invalidates all their outsider links) |
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
# Check auth status
|
|
79
|
-
GET /api/auth/status?key=<key>
|
|
80
|
-
# Returns: { authenticated: true, email: "...", isInsider: true, mode: "key" }
|
|
81
|
-
```
|
|
76
|
+
### Authentication
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Get insider key (requires X-API-Key header with seed)
|
|
87
|
-
GET /insider-key
|
|
88
|
-
# Headers: X-API-Key: <seed>
|
|
89
|
-
# Returns: { key: "a1b2c3d4..." }
|
|
90
|
-
|
|
91
|
-
# Compute outsider key for a path
|
|
92
|
-
GET /key?path=/d/docs/design.md
|
|
93
|
-
# Headers: X-API-Key: <seed>
|
|
94
|
-
# Returns: { key: "e5f6a7b8..." }
|
|
95
|
-
|
|
96
|
-
# Rotate a key
|
|
97
|
-
POST /rotate-key
|
|
98
|
-
# Body: { key: "<current-insider-key>" }
|
|
99
|
-
```
|
|
78
|
+
| Method | Path | Description |
|
|
79
|
+
|--------|------|-------------|
|
|
80
|
+
| `GET` | `/api/auth/status` | Check authentication status and mode |
|
|
100
81
|
|
|
101
82
|
### Event Gateway
|
|
102
83
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
POST
|
|
106
|
-
Content-Type: application/json
|
|
107
|
-
Body: { "type": "page.content_updated", "data": { "page_id": "abc123" } }
|
|
108
|
-
# Returns: { matched: "notion-page-update" } or { matched: null }
|
|
109
|
-
```
|
|
84
|
+
| Method | Path | Description |
|
|
85
|
+
|--------|------|-------------|
|
|
86
|
+
| `POST` | `/event` | Send a webhook (matched against configured schemas) |
|
|
110
87
|
|
|
111
|
-
###
|
|
88
|
+
### Search (requires watcher integration)
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
90
|
+
| Method | Path | Description |
|
|
91
|
+
|--------|------|-------------|
|
|
92
|
+
| `POST` | `/api/search` | Semantic search (proxied to jeeves-watcher) |
|
|
93
|
+
| `GET` | `/api/search/facets` | Get filter facets for search UI (cached) |
|
|
117
94
|
|
|
118
95
|
## Converting Windows Paths to URLs
|
|
119
96
|
|
|
120
|
-
Jeeves Server maps Windows filesystem paths to URL paths:
|
|
121
|
-
|
|
122
97
|
```
|
|
123
|
-
D
|
|
124
|
-
E
|
|
98
|
+
D:\\docs\\design.md → /d/docs/design.md
|
|
99
|
+
E:\\projects\\foo → /e/projects/foo
|
|
125
100
|
```
|
|
126
101
|
|
|
127
102
|
**Conversion formula:**
|
|
128
103
|
1. Replace backslashes with forward slashes
|
|
129
104
|
2. Replace the drive letter + colon with lowercase letter
|
|
130
|
-
3. Prepend the route prefix (`/
|
|
105
|
+
3. Prepend the route prefix (`/browse/` for SPA, `/api/file/` for API, `/path/` for legacy)
|
|
131
106
|
|
|
132
107
|
```javascript
|
|
133
|
-
function winPathToUrl(winPath, prefix = '/
|
|
134
|
-
|
|
135
|
-
.replace(/\\/g, '/')
|
|
136
|
-
.replace(/^([A-Z]):/, (_, d) => d.toLowerCase());
|
|
137
|
-
return `${prefix}${urlPath}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// D:\docs\design.md → /path/d/docs/design.md
|
|
141
|
-
// D:\docs\design.md → /browse/d/docs/design.md
|
|
142
|
-
// D:\docs\design.md → /api/file/d/docs/design.md
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
```powershell
|
|
146
|
-
# PowerShell equivalent
|
|
147
|
-
function Convert-ToJeevesUrl {
|
|
148
|
-
param([string]$Path, [string]$Prefix = '/path/')
|
|
149
|
-
$urlPath = $Path -replace '\\','/' -replace '^([A-Z]):',{ $_.Groups[1].Value.ToLower() }
|
|
150
|
-
return "${Prefix}${urlPath}"
|
|
108
|
+
function winPathToUrl(winPath, prefix = '/browse/') {
|
|
109
|
+
return prefix + winPath.replace(/\\\\/g, '/').replace(/^([A-Z]):/, (_, d) => d.toLowerCase());
|
|
151
110
|
}
|
|
152
111
|
```
|
|
153
112
|
|
|
@@ -157,7 +116,7 @@ function Convert-ToJeevesUrl {
|
|
|
157
116
|
|
|
158
117
|
```javascript
|
|
159
118
|
const insiderKey = computeInsiderKey(seed);
|
|
160
|
-
const url =
|
|
119
|
+
const url = \`https://jeeves.example.com/browse/d/docs/design.md?key=\${insiderKey}\`;
|
|
161
120
|
```
|
|
162
121
|
|
|
163
122
|
### Outsider links (path-scoped)
|
|
@@ -169,68 +128,16 @@ function outsiderKey(seed, path) {
|
|
|
169
128
|
const normalized = path.toLowerCase().replace(/^\/+|\/+$/g, '');
|
|
170
129
|
return crypto.createHmac('sha256', seed).update(normalized).digest('hex').substring(0, 32);
|
|
171
130
|
}
|
|
172
|
-
|
|
173
|
-
function outsiderKeyWithExpiry(seed, path, expiryMs) {
|
|
174
|
-
const normalized = path.toLowerCase().replace(/^\/+|\/+$/g, '');
|
|
175
|
-
const data = `${normalized}|${expiryMs}`;
|
|
176
|
-
return crypto.createHmac('sha256', seed).update(data).digest('hex').substring(0, 32);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Non-expiring outsider link
|
|
180
|
-
const key = outsiderKey(seed, 'd/docs/design.md');
|
|
181
|
-
const url = `https://jeeves.example.com/browse/d/docs/design.md?key=${key}`;
|
|
182
|
-
|
|
183
|
-
// Expiring outsider link (1 week)
|
|
184
|
-
const expiry = Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
185
|
-
const key = outsiderKeyWithExpiry(seed, 'd/docs/design.md', expiry);
|
|
186
|
-
const url = `https://jeeves.example.com/browse/d/docs/design.md?key=${key}&exp=${expiry}`;
|
|
187
131
|
```
|
|
188
132
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Outsider keys for directories grant access to all descendants:
|
|
192
|
-
|
|
193
|
-
```javascript
|
|
194
|
-
// Share an entire directory
|
|
195
|
-
const key = outsiderKey(seed, 'd/projects/client-x');
|
|
196
|
-
const url = `https://jeeves.example.com/browse/d/projects/client-x?key=${key}`;
|
|
197
|
-
// Grants access to all files under D:\projects\client-x\
|
|
198
|
-
```
|
|
133
|
+
See the [Sharing](sharing.md) guide for details on expiring links and directory sharing.
|
|
199
134
|
|
|
200
135
|
## For AI Assistants
|
|
201
136
|
|
|
202
|
-
If you're an AI assistant working with Jeeves Server
|
|
203
|
-
|
|
204
|
-
### Generating links to share with humans
|
|
205
|
-
|
|
206
|
-
When your human asks you to share a document:
|
|
207
|
-
|
|
208
|
-
1. **Convert the Windows path** to a URL path (see above)
|
|
209
|
-
2. **Use the insider key** for team members, or generate an outsider key for external recipients
|
|
210
|
-
3. **Choose the right route**: `/browse/` for browser viewing, `/path/` with `?export=pdf` for direct PDF download
|
|
211
|
-
|
|
212
|
-
### Authoring documents
|
|
213
|
-
|
|
214
|
-
Write Markdown files to the server's filesystem. Jeeves Server will render them beautifully. You can:
|
|
215
|
-
- Embed Mermaid diagrams (rendered inline)
|
|
216
|
-
- Embed SVG files (rendered with pan/zoom)
|
|
217
|
-
- Use code blocks with language hints (syntax highlighted)
|
|
218
|
-
- Reference other local files with relative paths
|
|
219
|
-
|
|
220
|
-
### Checking server status
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
curl -s http://localhost:1934/health
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Triggering webhooks
|
|
227
|
-
|
|
228
|
-
If you need to trigger an action via the event gateway:
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
curl -X POST "http://localhost:1934/event?key=<webhook-key>" \
|
|
232
|
-
-H "Content-Type: application/json" \
|
|
233
|
-
-d '{"action": "rebuild", "target": "docs"}'
|
|
234
|
-
```
|
|
137
|
+
If you're an AI assistant working with Jeeves Server:
|
|
235
138
|
|
|
236
|
-
|
|
139
|
+
1. **Use the OpenClaw plugin** if available — it provides `server_status`, `server_browse`, `server_link_info`, `server_share`, `server_export`, and `server_event_status` tools
|
|
140
|
+
2. **Convert Windows paths** to URL paths using the formula above
|
|
141
|
+
3. **Use `/api/status`** for health checks (no auth required)
|
|
142
|
+
4. **Prefer `/browse/` routes** for links you share with humans (renders the SPA)
|
|
143
|
+
5. **Use `/api/export/` routes** for direct PDF/DOCX downloads
|