@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.
@@ -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-LjwgzZ7F.js"></script>
10
- <link rel="stylesheet" crossorigin href="/app/assets/index-DbMebkkd.css">
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(5000),
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. For programmatic access, use a key:
8
-
9
- ```typescript
10
- // In jeeves.config.ts
11
- keys: {
12
- 'ci-bot': 'random-seed-string',
13
- // Scoped key for webhooks only:
14
- 'webhook': { key: 'another-seed', scopes: ['/event'] },
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
- ### File Access
38
+ ### Public (no auth required)
40
39
 
41
- ```bash
42
- # Get file content (rendered)
43
- GET /api/file/d/docs/design.md?key=<key>
44
- # Returns: { type: "markdown", html: "...", headings: [...], content: "...", fileName: "..." }
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
- # Get file content (raw text)
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
- # Get raw file bytes
51
- GET /path/d/docs/design.md?key=<key>&raw=1
52
- # Returns: file content with appropriate Content-Type
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
- # Export as PDF
55
- GET /path/d/docs/design.md?key=<key>&export=pdf
56
- # Returns: application/pdf
53
+ ### Directory Access
57
54
 
58
- # Export as DOCX
59
- GET /path/d/docs/design.md?key=<key>&export=docx
60
- # Returns: application/vnd.openxmlformats-officedocument.wordprocessingml.document
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
- ### Directory Listing
60
+ ### Export
64
61
 
65
- ```bash
66
- # List drives
67
- GET /api/drives?key=<key>
68
- # Returns: { drives: [{ letter: "C", label: "System", ... }] }
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
- # List directory
71
- GET /api/directory/d/docs?key=<key>
72
- # Returns: { path: "d/docs", entries: [{ name: "...", type: "file"|"directory", ... }] }
73
- ```
68
+ ### Sharing
74
69
 
75
- ### Authentication
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
- ```bash
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
- ### Sharing
84
-
85
- ```bash
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
- ```bash
104
- # Send a webhook
105
- POST /event?key=<webhook-key>
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
- ### Health
88
+ ### Search (requires watcher integration)
112
89
 
113
- ```bash
114
- GET /health
115
- # Returns: 200 OK (no auth required)
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:\docs\design.md → /d/docs/design.md
124
- E:\projects\foo → /e/projects/foo
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 (`/path/` for legacy, `/browse/` for SPA, `/api/file/` for API)
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 = '/path/') {
134
- const urlPath = winPath
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 = `https://jeeves.example.com/browse/d/docs/design.md?key=${insiderKey}`;
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
- ### Directory links
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, here's what you need to know:
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
- Match this against a configured event schema to dispatch your handler.
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