@karmaniverous/jeeves-server 3.4.2 → 3.5.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 (127) hide show
  1. package/.tsbuildinfo +1 -1
  2. package/CHANGELOG.md +38 -1
  3. package/README.md +18 -17
  4. package/client/package.json +19 -19
  5. package/client/src/components/SearchModal.tsx +11 -1
  6. package/client/src/components/layout/Header.tsx +3 -3
  7. package/client/src/lib/api.ts +10 -5
  8. package/dist/client/assets/CodeEditor-Brh86AGF.js +1 -0
  9. package/dist/client/assets/CodeViewer-Cegj3cEn.js +1 -0
  10. package/dist/client/assets/dist-2YqVIvgv.js +2 -0
  11. package/dist/client/assets/dist-5vamY028.js +1 -0
  12. package/dist/client/assets/dist-6_auAGci.js +1 -0
  13. package/dist/client/assets/dist-B0kq1DQG.js +1 -0
  14. package/dist/client/assets/dist-B2SZD_eN.js +1 -0
  15. package/dist/client/assets/dist-B2t4dYA2.js +1 -0
  16. package/dist/client/assets/dist-B5gFYAn7.js +1 -0
  17. package/dist/client/assets/dist-BPy6CnYN.js +1 -0
  18. package/dist/client/assets/dist-CL6VCrQn.js +9 -0
  19. package/dist/client/assets/dist-CWsHar9N.js +1 -0
  20. package/dist/client/assets/dist-CnFc5Ssx.js +1 -0
  21. package/dist/client/assets/dist-DSgLBuTS.js +1 -0
  22. package/dist/client/assets/dist-DUcac0X_.js +7 -0
  23. package/dist/client/assets/dist-DcTcc-BG.js +6 -0
  24. package/dist/client/assets/dist-DvfTyWk_.js +1 -0
  25. package/dist/client/assets/dist-Dz1Ulpqa.js +1 -0
  26. package/dist/client/assets/dist-Kr-mUYW1.js +5 -0
  27. package/dist/client/assets/dist-OX4k3MMG.js +2 -0
  28. package/dist/client/assets/dist-qiU0qoeK.js +1 -0
  29. package/dist/client/assets/dist-ui4J6fvl.js +23 -0
  30. package/dist/client/assets/index-Dk_myGs4.css +2 -0
  31. package/dist/client/assets/index-DrBXupPz.js +62 -0
  32. package/dist/client/assets/theme-CPpIxvB0.js +2 -0
  33. package/dist/client/index.html +3 -2
  34. package/dist/src/cli/commands/config.test.js +5 -40
  35. package/dist/src/cli/index.js +9 -15
  36. package/dist/src/cli/start-server.js +16 -0
  37. package/dist/src/config/index.js +48 -37
  38. package/dist/src/config/loadConfig.test.js +27 -25
  39. package/dist/src/config/migration.js +60 -0
  40. package/dist/src/config/schema.js +4 -3
  41. package/dist/src/descriptor.js +46 -0
  42. package/dist/src/routes/api/diagramExport.js +101 -0
  43. package/dist/src/routes/api/diagramExport.test.js +134 -0
  44. package/dist/src/routes/api/events.js +13 -0
  45. package/dist/src/routes/api/export.js +6 -82
  46. package/dist/src/routes/api/index.js +4 -0
  47. package/dist/src/routes/api/search.js +9 -50
  48. package/dist/src/routes/api/sharing.js +40 -23
  49. package/dist/src/routes/api/sharing.test.js +52 -0
  50. package/dist/src/routes/auth.js +1 -1
  51. package/dist/src/routes/config.js +8 -2
  52. package/dist/src/routes/keys.js +4 -4
  53. package/dist/src/routes/path/index.js +1 -1
  54. package/dist/src/routes/status.js +15 -16
  55. package/dist/src/routes/status.test.js +13 -8
  56. package/dist/src/server.js +21 -16
  57. package/dist/src/services/markdown.js +2 -1
  58. package/dist/src/services/markdown.test.js +22 -0
  59. package/dist/src/util/packageVersion.js +7 -16
  60. package/dist/src/util/packageVersion.test.js +7 -0
  61. package/guides/api-integration.md +4 -0
  62. package/guides/deployment.md +11 -10
  63. package/guides/event-gateway.md +4 -0
  64. package/guides/exports.md +4 -0
  65. package/guides/index.md +1 -1
  66. package/guides/setup.md +17 -16
  67. package/guides/sharing.md +4 -0
  68. package/package.json +3 -3
  69. package/scripts/download-plantuml.js +0 -1
  70. package/src/cli/commands/config.test.ts +5 -45
  71. package/src/cli/index.ts +9 -16
  72. package/src/cli/start-server.ts +21 -0
  73. package/src/config/index.ts +56 -43
  74. package/src/config/loadConfig.test.ts +27 -29
  75. package/src/config/migration.ts +76 -0
  76. package/src/config/schema.ts +5 -4
  77. package/src/descriptor.ts +55 -0
  78. package/src/routes/api/diagramExport.test.ts +200 -0
  79. package/src/routes/api/diagramExport.ts +170 -0
  80. package/src/routes/api/events.ts +22 -0
  81. package/src/routes/api/export.ts +6 -131
  82. package/src/routes/api/index.ts +4 -0
  83. package/src/routes/api/search.ts +9 -63
  84. package/src/routes/api/sharing.test.ts +66 -0
  85. package/src/routes/api/sharing.ts +47 -23
  86. package/src/routes/auth.ts +1 -1
  87. package/src/routes/config.ts +15 -2
  88. package/src/routes/keys.ts +4 -4
  89. package/src/routes/path/index.ts +1 -1
  90. package/src/routes/status.test.ts +14 -8
  91. package/src/routes/status.ts +56 -62
  92. package/src/server.ts +29 -17
  93. package/src/services/markdown.test.ts +26 -0
  94. package/src/services/markdown.ts +2 -1
  95. package/src/util/packageVersion.test.ts +9 -0
  96. package/src/util/packageVersion.ts +11 -18
  97. package/src/util/platform.ts +1 -1
  98. package/dist/client/assets/CodeEditor-DQZZL5Rq.js +0 -1
  99. package/dist/client/assets/CodeViewer-ofJVD1Vn.js +0 -1
  100. package/dist/client/assets/index--MBieNJA.js +0 -1
  101. package/dist/client/assets/index-BENeXQI_.js +0 -1
  102. package/dist/client/assets/index-BbBpoOxz.js +0 -1
  103. package/dist/client/assets/index-BdV9g5AM.js +0 -6
  104. package/dist/client/assets/index-BjAilRri.js +0 -2
  105. package/dist/client/assets/index-BqbhWo2I.js +0 -3
  106. package/dist/client/assets/index-CVbycZ0H.js +0 -1
  107. package/dist/client/assets/index-Cs5oz2oJ.js +0 -5
  108. package/dist/client/assets/index-D-RC7ZS6.css +0 -1
  109. package/dist/client/assets/index-D8KZVveX.js +0 -1
  110. package/dist/client/assets/index-DC4HMHxY.js +0 -13
  111. package/dist/client/assets/index-DcY2RXqX.js +0 -1
  112. package/dist/client/assets/index-Duy-tZYV.js +0 -1
  113. package/dist/client/assets/index-Dw7rDFmE.js +0 -7
  114. package/dist/client/assets/index-FlCUvrjv.js +0 -2
  115. package/dist/client/assets/index-K6OVmfhg.js +0 -1
  116. package/dist/client/assets/index-MLwyFRN0.js +0 -1
  117. package/dist/client/assets/index-OpqBpSjn.js +0 -1
  118. package/dist/client/assets/index-SsHei0HE.js +0 -1
  119. package/dist/client/assets/index-jSGuHSeS.js +0 -62
  120. package/dist/client/assets/index-uQa2yckk.js +0 -1
  121. package/dist/client/assets/index-udkXoIER.js +0 -1
  122. package/dist/src/cli/commands/config.js +0 -105
  123. package/dist/src/cli/commands/service.js +0 -93
  124. package/dist/src/cli/commands/start.js +0 -24
  125. package/src/cli/commands/config.ts +0 -117
  126. package/src/cli/commands/service.ts +0 -129
  127. package/src/cli/commands/start.ts +0 -27
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- import { createConfigQueryHandler } from '@karmaniverous/jeeves';
8
+ import { createConfigApplyHandler, createConfigQueryHandler, } from '@karmaniverous/jeeves';
9
9
  import { getConfig } from '../config/index.js';
10
+ import { serverDescriptor } from '../descriptor.js';
10
11
  /** Return a sanitized copy of the config (redact sensitive fields). */
11
12
  export function sanitizeConfig(config) {
12
13
  return {
@@ -29,7 +30,7 @@ export function sanitizeConfig(config) {
29
30
  })),
30
31
  };
31
32
  }
32
- /** Register the GET /config route. */
33
+ /** Register the GET /config and POST /config/apply routes. */
33
34
  export function registerConfigRoute(app) {
34
35
  const configHandler = createConfigQueryHandler(() => sanitizeConfig(getConfig()));
35
36
  app.get('/config', async (request, reply) => {
@@ -37,4 +38,9 @@ export function registerConfigRoute(app) {
37
38
  const result = await configHandler({ path });
38
39
  return reply.status(result.status).send(result.body);
39
40
  });
41
+ const applyHandler = createConfigApplyHandler(serverDescriptor);
42
+ app.post('/config/apply', async (request, reply) => {
43
+ const result = await applyHandler(request.body);
44
+ return reply.status(result.status).send(result.body);
45
+ });
40
46
  }
@@ -53,7 +53,7 @@ export const keysRoute = async (fastify) => {
53
53
  const insiderResult = resolveInsiderKeyAuth(config, provided);
54
54
  if (insiderResult.valid && insiderResult.email) {
55
55
  // Insider key rotation
56
- return await rotateInsiderSeed(insiderResult.email, config);
56
+ return rotateInsiderSeed(insiderResult.email, config);
57
57
  }
58
58
  // Machine key rotation is not supported with TS config
59
59
  const matched = config.resolvedKeys.find((rk) => timingSafeEqual(provided, computeInsiderKey(rk.seed)));
@@ -67,7 +67,7 @@ export const keysRoute = async (fastify) => {
67
67
  // Try session-based auth
68
68
  const sessionResult = resolveSessionAuth(config, request);
69
69
  if (sessionResult.valid && sessionResult.email) {
70
- return await rotateInsiderSeed(sessionResult.email, config);
70
+ return rotateInsiderSeed(sessionResult.email, config);
71
71
  }
72
72
  return reply.code(401).send({ error: 'Invalid insider key' });
73
73
  });
@@ -93,7 +93,7 @@ export const keysRoute = async (fastify) => {
93
93
  return reply.code(401).send({ error: 'Invalid insider key' });
94
94
  });
95
95
  };
96
- async function rotateInsiderSeed(email, config) {
96
+ function rotateInsiderSeed(email, config) {
97
97
  const insider = findInsider(config.resolvedInsiders, email);
98
98
  if (!insider?.seed)
99
99
  return { ok: false, error: 'Insider not found' };
@@ -106,7 +106,7 @@ async function rotateInsiderSeed(email, config) {
106
106
  at: timestamp,
107
107
  });
108
108
  setKeyRotationTimestamp(timestamp);
109
- await resetConfig();
109
+ resetConfig();
110
110
  return { ok: true, keyName: insider.email };
111
111
  }
112
112
  function buildShareResponse(seed, targetPath, expiry) {
@@ -10,7 +10,7 @@ export const pathRoute = async (fastify) => {
10
10
  // Redirect /path/* to /browse/*
11
11
  fastify.get('/path/*', async (request, reply) => {
12
12
  const reqPath = request.params['*'];
13
- const url = new URL(request.url, 'http://localhost');
13
+ const url = new URL(request.url, 'http://127.0.0.1');
14
14
  const query = url.search;
15
15
  return reply.redirect(`/browse/${reqPath}${query}`);
16
16
  });
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Server status endpoint — structured metadata for diagnostics and TOOLS.md generation.
2
+ * Server status endpoint — uses the SDK's `createStatusHandler` factory.
3
3
  *
4
- * Returns version, uptime, port, connected services reachability,
5
- * event schemas, insider count (no PII), and export capabilities.
4
+ * Returns standard `{ name, version, uptime, status, health }` shape
5
+ * with server-specific details nested under `health`.
6
6
  */
7
+ import { createStatusHandler } from '@karmaniverous/jeeves';
7
8
  import { getConfig } from '../config/index.js';
8
- import { getRecentEvents } from '../services/eventLog.js';
9
9
  import { packageVersion } from '../util/packageVersion.js';
10
- const startTime = Date.now();
11
10
  async function checkService(url) {
12
- // Try /status first (watcher), then /health (runner)
13
11
  for (const endpoint of ['/status', '/health']) {
14
12
  try {
15
13
  const res = await fetch(`${url}${endpoint}`, {
@@ -26,9 +24,10 @@ async function checkService(url) {
26
24
  }
27
25
  return { url, reachable: false };
28
26
  }
29
- // eslint-disable-next-line @typescript-eslint/require-await
30
- export const statusRoutes = async (fastify) => {
31
- fastify.get('/status', async (request) => {
27
+ const handleStatus = createStatusHandler({
28
+ name: 'server',
29
+ version: packageVersion,
30
+ getHealth: async () => {
32
31
  const config = getConfig();
33
32
  const [watcher, runner, meta] = await Promise.all([
34
33
  config.watcherUrl ? checkService(config.watcherUrl) : null,
@@ -36,8 +35,6 @@ export const statusRoutes = async (fastify) => {
36
35
  config.metaUrl ? checkService(config.metaUrl) : null,
37
36
  ]);
38
37
  return {
39
- version: packageVersion,
40
- uptime: Math.floor((Date.now() - startTime) / 1000),
41
38
  port: config.port,
42
39
  chrome: {
43
40
  configured: Boolean(config.chromePath),
@@ -70,11 +67,13 @@ export const statusRoutes = async (fastify) => {
70
67
  runner,
71
68
  meta,
72
69
  },
73
- ...(request.query.events
74
- ? {
75
- eventLog: getRecentEvents(Math.min(parseInt(request.query.events, 10) || 20, 100)),
76
- }
77
- : {}),
78
70
  };
71
+ },
72
+ });
73
+ // eslint-disable-next-line @typescript-eslint/require-await
74
+ export const statusRoutes = async (fastify) => {
75
+ fastify.get('/status', async () => {
76
+ const result = await handleStatus();
77
+ return result.body;
79
78
  });
80
79
  };
@@ -27,7 +27,7 @@ vi.mock('../config/index.js', () => ({
27
27
  // Must import AFTER mock
28
28
  const { statusRoutes } = await import('./status.js');
29
29
  describe('GET /status', () => {
30
- it('returns structured status', async () => {
30
+ it('returns structured status with SDK shape', async () => {
31
31
  // Create a minimal Fastify-like test harness
32
32
  const routes = {};
33
33
  const fakeFastify = {
@@ -40,18 +40,23 @@ describe('GET /status', () => {
40
40
  expect(handler).toBeDefined();
41
41
  const result = await handler({ accessMode: 'insider', query: {} });
42
42
  const status = result;
43
+ // Standard SDK fields at top level
44
+ expect(status).toHaveProperty('name', 'server');
43
45
  expect(status).toHaveProperty('version');
44
46
  expect(status).toHaveProperty('uptime');
45
- expect(status.port).toBe(1934);
46
- expect(status.chrome.configured).toBe(true);
47
- expect(status.auth.insiderCount).toBe(2);
48
- expect(status.auth.keyCount).toBe(1);
49
- expect(status.events).toHaveLength(2);
50
- const exports = status.exports;
47
+ expect(status).toHaveProperty('status', 'healthy');
48
+ // Server-specific fields nested under health
49
+ const health = status.health;
50
+ expect(health.port).toBe(1934);
51
+ expect(health.chrome.configured).toBe(true);
52
+ expect(health.auth.insiderCount).toBe(2);
53
+ expect(health.auth.keyCount).toBe(1);
54
+ expect(health.events).toHaveLength(2);
55
+ const exports = health.exports;
51
56
  expect(exports.documents).toEqual(['pdf', 'docx']);
52
57
  expect(exports.directories).toEqual(['zip']);
53
58
  expect(exports.diagrams).toEqual(['svg', 'png']);
54
59
  expect(exports.chromeAvailable).toBe(true);
55
- expect(status.diagrams.mermaid).toBe(true);
60
+ expect(health.diagrams.mermaid).toBe(true);
56
61
  });
57
62
  });
@@ -23,7 +23,7 @@ import { initExportCache } from './services/exportCache.js';
23
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  async function start() {
25
25
  try {
26
- const config = isConfigInitialized() ? getConfig() : await initConfig();
26
+ const config = isConfigInitialized() ? getConfig() : initConfig();
27
27
  const fastify = Fastify({
28
28
  logger: true,
29
29
  });
@@ -49,21 +49,26 @@ async function start() {
49
49
  root: clientDir,
50
50
  prefix: '/app/',
51
51
  });
52
- // SPA fallback for React routes
53
- fastify.get('/', async (_request, reply) => {
54
- return reply.sendFile('index.html', clientDir);
55
- });
56
- fastify.get('/browse', async (_request, reply) => {
57
- return reply.sendFile('index.html', clientDir);
58
- });
59
- fastify.get('/browse/*', async (_request, reply) => {
60
- return reply.sendFile('index.html', clientDir);
61
- });
62
- fastify.get('/runner', async (_request, reply) => {
63
- return reply.sendFile('index.html', clientDir);
64
- });
65
- fastify.get('/runner/*', async (_request, reply) => {
66
- return reply.sendFile('index.html', clientDir);
52
+ // SPA fallback — all these routes serve index.html for client-side routing
53
+ const spaFallback = async (_request, reply) => reply.sendFile('index.html', clientDir);
54
+ for (const route of [
55
+ '/',
56
+ '/browse',
57
+ '/browse/*',
58
+ '/runner',
59
+ '/runner/*',
60
+ ]) {
61
+ fastify.get(route, spaFallback);
62
+ }
63
+ // Catch-all: serve SPA for any unmatched GET under /browse or /runner
64
+ // (handles edge cases like dotfile paths that wildcard routes may miss)
65
+ fastify.setNotFoundHandler(async (request, reply) => {
66
+ if (request.method === 'GET' &&
67
+ (request.url.startsWith('/browse') ||
68
+ request.url.startsWith('/runner'))) {
69
+ return reply.sendFile('index.html', clientDir);
70
+ }
71
+ return reply.code(404).send({ error: 'Not found' });
67
72
  });
68
73
  }
69
74
  // Initialize caches
@@ -2,6 +2,7 @@
2
2
  * Markdown rendering with TOC generation, Windows path linking, and syntax highlighting
3
3
  */
4
4
  import fs from 'node:fs';
5
+ import * as cheerio from 'cheerio';
5
6
  import { marked } from 'marked';
6
7
  import { registerDiagram } from './embeddedDiagrams.js';
7
8
  /**
@@ -96,7 +97,7 @@ export function parseMarkdown(markdown, options = {}) {
96
97
  .replace(/^-|-$/g, '');
97
98
  headings.push({
98
99
  level,
99
- text: renderedText.replace(/<[^>]+>/g, ''),
100
+ text: cheerio.load(renderedText).text(),
100
101
  slug,
101
102
  });
102
103
  return `<h${String(level)} id="${slug}">${renderedText} <a href="#${slug}" class="anchor">#</a></h${String(level)}>\n`;
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseMarkdown } from './markdown.js';
3
+ describe('parseMarkdown', () => {
4
+ it('decodes HTML entities in heading text for TOC', () => {
5
+ const md = '# Hello &amp; "World"';
6
+ const { headings } = parseMarkdown(md);
7
+ expect(headings).toHaveLength(1);
8
+ expect(headings[0].text).toBe('Hello & "World"');
9
+ });
10
+ it('strips HTML tags from heading text for TOC', () => {
11
+ const md = '## A <em>bold</em> heading';
12
+ const { headings } = parseMarkdown(md);
13
+ expect(headings).toHaveLength(1);
14
+ expect(headings[0].text).toBe('A bold heading');
15
+ });
16
+ it('decodes &#39; and &quot; entities in headings', () => {
17
+ const md = '### It&#39;s a &quot;test&quot;';
18
+ const { headings } = parseMarkdown(md);
19
+ expect(headings).toHaveLength(1);
20
+ expect(headings[0].text).toBe('It\'s a "test"');
21
+ });
22
+ });
@@ -1,25 +1,16 @@
1
1
  /**
2
- * Resolve the service package version by walking up from the caller's directory.
2
+ * Resolve the service package version using package-directory.
3
3
  */
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- function findPackageJson(startDir) {
8
- let dir = startDir;
9
- while (dir !== path.dirname(dir)) {
10
- const candidate = path.join(dir, 'package.json');
11
- if (fs.existsSync(candidate)) {
12
- const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
13
- // Find our package specifically, not the monorepo root
14
- if (pkg.name === '@karmaniverous/jeeves-server')
15
- return candidate;
16
- }
17
- dir = path.dirname(dir);
18
- }
19
- throw new Error('Could not find @karmaniverous/jeeves-server package.json');
20
- }
7
+ import { packageDirectorySync } from 'package-directory';
21
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
- const pkgPath = findPackageJson(__dirname);
9
+ const pkgDir = packageDirectorySync({ cwd: __dirname });
10
+ if (!pkgDir) {
11
+ throw new Error('Could not find package directory for jeeves-server');
12
+ }
13
+ const pkgPath = path.join(pkgDir, 'package.json');
23
14
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
24
15
  /** The package version of the jeeves-server service package. */
25
16
  export const packageVersion = pkg.version;
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { packageVersion } from './packageVersion.js';
3
+ describe('packageVersion', () => {
4
+ it('returns a valid semver version string', () => {
5
+ expect(packageVersion).toMatch(/^\d+\.\d+\.\d+/);
6
+ });
7
+ });
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "API & Integration Guide"
3
+ ---
4
+
1
5
  # API & Integration Guide
2
6
 
3
7
  How to interact with Jeeves Server programmatically — for scripts, bots, AI assistants, and CI/CD pipelines.
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Deployment"
3
+ ---
4
+
1
5
  # Deployment
2
6
 
3
7
  How to run Jeeves Server in production.
@@ -23,8 +27,9 @@ npm install -g @karmaniverous/jeeves-server
23
27
  Create your config file (see [Setup & Configuration](setup.md)):
24
28
 
25
29
  ```bash
26
- # Example: JSON config
27
- cat > /etc/jeeves-server.config.json << 'EOF'
30
+ # Example: JSON config (new path convention)
31
+ mkdir -p /etc/jeeves-server
32
+ cat > /etc/jeeves-server/config.json << 'EOF'
28
33
  {
29
34
  "chromePath": "/usr/bin/chromium-browser",
30
35
  "auth": { "modes": ["keys"] },
@@ -48,18 +53,14 @@ The server listens on the configured port (default: 1934) on all interfaces.
48
53
 
49
54
  ### As a Windows Service (NSSM)
50
55
 
51
- Use the built-in CLI to generate NSSM install commands:
52
-
53
- ```bash
54
- jeeves-server service install --config "C:\\config\\jeeves-server.config.json"
55
- ```
56
-
57
- This prints the `nssm install` commands. Run them, then:
56
+ The CLI installs and manages the NSSM service directly:
58
57
 
59
58
  ```bash
59
+ jeeves-server service install --config "C:\\config\\jeeves-server\\config.json"
60
60
  jeeves-server service start
61
61
  jeeves-server service stop
62
62
  jeeves-server service restart
63
+ jeeves-server service status
63
64
  ```
64
65
 
65
66
  Or use NSSM directly:
@@ -84,7 +85,7 @@ After=network.target
84
85
  [Service]
85
86
  Type=simple
86
87
  User=jeeves
87
- ExecStart=/usr/bin/env jeeves-server start --config /etc/jeeves-server.config.json
88
+ ExecStart=/usr/bin/env jeeves-server start --config /etc/jeeves-server/config.json
88
89
  Restart=on-failure
89
90
  RestartSec=5
90
91
  Environment=NODE_ENV=production
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Event Gateway"
3
+ ---
4
+
1
5
  # Event Gateway
2
6
 
3
7
  Jeeves Server includes a webhook gateway that receives HTTP POST requests, validates them against JSON Schema rules, and dispatches matched events to shell commands via a durable queue.
package/guides/exports.md CHANGED
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Exporting & Downloads"
3
+ ---
4
+
1
5
  # Exporting & Downloads
2
6
 
3
7
  Jeeves Server can export files as PDF, DOCX, or ZIP — turning Markdown into business-ready documents with one click.
package/guides/index.md CHANGED
@@ -12,7 +12,7 @@ children:
12
12
 
13
13
  # Service Guides
14
14
 
15
- - [Setup & Configuration](./setup.md) — Installation, auth modes, cosmiconfig, named scopes, and config reference.
15
+ - [Setup & Configuration](./setup.md) — Installation, auth modes, JSON config, named scopes, and config reference.
16
16
  - [Insiders, Outsiders & Sharing](./sharing.md) — The access model, HMAC key derivation, expiring links, and key rotation.
17
17
  - [Exporting & Downloads](./exports.md) — PDF, DOCX, SVG, PNG, and ZIP export via Puppeteer and bundled Mermaid/PlantUML.
18
18
  - [Event Gateway](./event-gateway.md) — Webhook receiving, JSON Schema matching, JsonMap body transforms, and durable queue processing.
package/guides/setup.md CHANGED
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Setup & Configuration"
3
+ ---
4
+
1
5
  # Setup & Configuration
2
6
 
3
7
  ## Prerequisites
@@ -15,26 +19,17 @@ The `postinstall` script automatically downloads the PlantUML jar for local diag
15
19
 
16
20
  ## Configuration
17
21
 
18
- Jeeves Server uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) for configuration. Create a config file in any supported format:
22
+ Jeeves Server uses **JSON-only** configuration. Config files follow the path convention `<configDir>/jeeves-server/config.json`.
19
23
 
20
24
  ```bash
21
- # JSON (recommended for simplicity)
22
- jeeves-server.config.json
25
+ # Generate a starter config
26
+ jeeves-server init --config /path/to/config-dir
23
27
 
24
- # YAML
25
- jeeves-server.config.yaml
26
-
27
- # TypeScript (for type checking during authoring)
28
- jeeves-server.config.ts
29
-
30
- # Or any cosmiconfig-supported format
28
+ # Or specify an explicit path
29
+ jeeves-server start --config /path/to/jeeves-server/config.json
31
30
  ```
32
31
 
33
- The server searches for config files starting from its working directory and walking up. You can also specify an explicit path:
34
-
35
- ```bash
36
- jeeves-server start --config /path/to/jeeves-server.config.json
37
- ```
32
+ Legacy paths (`jeeves-server.config.json`) are auto-migrated to the new convention on first use.
38
33
 
39
34
  ### Config structure (JSON)
40
35
 
@@ -96,7 +91,13 @@ String values in the config support `${VAR_NAME}` substitution from `process.env
96
91
  jeeves-server config validate [--config <path>]
97
92
  ```
98
93
 
99
- This loads and validates the config against the Zod schema, reporting any errors. Use `config show` to see the fully resolved configuration with all derived keys and scope assignments.
94
+ This loads and validates the config against the Zod schema, reporting any errors. Use `config [jsonpath]` to query the fully resolved configuration:
95
+
96
+ ```bash
97
+ jeeves-server config # Full resolved config
98
+ jeeves-server config '$.port' # Query specific field
99
+ jeeves-server config '$.auth.modes' # Nested query
100
+ ```
100
101
 
101
102
  ### Platform-specific settings
102
103
 
package/guides/sharing.md CHANGED
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Insiders, Outsiders & Sharing"
3
+ ---
4
+
1
5
  # Insiders, Outsiders & Sharing
2
6
 
3
7
  Jeeves Server has a clear access model built around two roles: **insiders** and **outsiders**. Understanding the difference is key to using sharing effectively.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-server",
3
- "version": "3.4.2",
3
+ "version": "3.5.0",
4
4
  "description": "Secure file browser, markdown viewer, and webhook gateway with PDF/DOCX export and expiring share links",
5
5
  "keywords": [
6
6
  "fastify",
@@ -49,18 +49,18 @@
49
49
  "@commander-js/extra-typings": "^14.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/static": "^8.3.0",
52
- "@karmaniverous/jeeves": "^0.3.0",
52
+ "@karmaniverous/jeeves": "^0.4.4",
53
53
  "@karmaniverous/jsonmap": "^0.3.1",
54
54
  "@mermaid-js/mermaid-cli": "^11.12.0",
55
55
  "@turbodocx/html-to-docx": "^1.1.0",
56
56
  "ajv": "^8.17.1",
57
57
  "archiver": "^7.0.1",
58
58
  "cheerio": "^1.2.0",
59
- "cosmiconfig": "^9.0.1",
60
59
  "fastify": "^5.2.3",
61
60
  "lz-string": "^1.5.0",
62
61
  "marked": "^17.0.1",
63
62
  "mime-types": "^3.0.2",
63
+ "package-directory": "^8.2.0",
64
64
  "picomatch": "^4.0.3",
65
65
  "plantuml-encoder": "^1.4.0",
66
66
  "puppeteer": "^23.11.1",
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable no-undef */
2
- /* eslint-env node */
3
2
 
4
3
  /**
5
4
  * Downloads a pinned PlantUML jar to vendor/plantuml.jar.
@@ -25,7 +25,10 @@ const VALID_CONFIG = {
25
25
  };
26
26
 
27
27
  function writeConfig(dir: string, config: unknown): string {
28
- const filePath = path.join(dir, 'jeeves-server.config.json');
28
+ // Write to the new convention path: {dir}/jeeves-server/config.json
29
+ const configDir = path.join(dir, 'jeeves-server');
30
+ fs.mkdirSync(configDir, { recursive: true });
31
+ const filePath = path.join(configDir, 'config.json');
29
32
  fs.writeFileSync(filePath, JSON.stringify(config));
30
33
  return filePath;
31
34
  }
@@ -47,13 +50,10 @@ describe('jeeves-server config validate', () => {
47
50
  fs.rmSync(tmpDir, { recursive: true, force: true });
48
51
  });
49
52
 
50
- it('validates a valid config and prints summary', async () => {
53
+ it('validates a valid config and prints success', async () => {
51
54
  const configPath = writeConfig(tmpDir, VALID_CONFIG);
52
55
  const { stdout } = await runCli(['config', 'validate', '-c', configPath]);
53
56
  expect(stdout).toContain('Configuration valid');
54
- expect(stdout).toContain('Port: 8765');
55
- expect(stdout).toContain('Auth modes: keys');
56
- expect(stdout).toContain('Keys: 2');
57
57
  });
58
58
 
59
59
  it('exits with error for invalid config', async () => {
@@ -65,43 +65,3 @@ describe('jeeves-server config validate', () => {
65
65
  });
66
66
  });
67
67
  });
68
-
69
- describe('jeeves-server config show', () => {
70
- let tmpDir: string;
71
-
72
- beforeEach(() => {
73
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-cli-'));
74
- });
75
-
76
- afterEach(() => {
77
- fs.rmSync(tmpDir, { recursive: true, force: true });
78
- });
79
-
80
- it('shows resolved config with key and insider details', async () => {
81
- const configPath = writeConfig(tmpDir, {
82
- ...VALID_CONFIG,
83
- insiders: { 'test@example.com': {} },
84
- watcherUrl: 'http://localhost:3458',
85
- });
86
- const { stdout } = await runCli(['config', 'show', '-c', configPath]);
87
- expect(stdout).toContain('Config file:');
88
- expect(stdout).toContain('port: 8765');
89
- expect(stdout).toContain('modes: keys');
90
- expect(stdout).toContain('primary:');
91
- expect(stdout).toContain('unscoped');
92
- expect(stdout).toContain('test@example.com');
93
- expect(stdout).toContain('watcherUrl: http://localhost:3458');
94
- });
95
-
96
- it('shows scoped keys correctly', async () => {
97
- const configPath = writeConfig(tmpDir, {
98
- ...VALID_CONFIG,
99
- keys: {
100
- ...VALID_CONFIG.keys,
101
- scoped: { key: 'c'.repeat(64), scopes: ['/docs'] },
102
- },
103
- });
104
- const { stdout } = await runCli(['config', 'show', '-c', configPath]);
105
- expect(stdout).toContain('scoped (allow: 1, deny: 0)');
106
- });
107
- });
package/src/cli/index.ts CHANGED
@@ -1,25 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @packageDocumentation
4
- *
5
3
  * jeeves-server CLI entrypoint.
6
- * Commands: start, config validate, config show, service install/uninstall.
4
+ *
5
+ * Uses `createServiceCli(descriptor)` from core for all standard commands.
6
+ * The `start` command uses `descriptor.startCommand` which points to
7
+ * `start-server.ts` for direct in-process server launch.
8
+ *
9
+ * @packageDocumentation
7
10
  */
8
11
 
9
- import { Command } from '@commander-js/extra-typings';
10
-
11
- import { packageVersion } from '../util/packageVersion.js';
12
- import { registerConfigCommand } from './commands/config.js';
13
- import { registerServiceCommand } from './commands/service.js';
14
- import { registerStartCommand } from './commands/start.js';
12
+ import { createServiceCli } from '@karmaniverous/jeeves';
15
13
 
16
- const cli = new Command()
17
- .name('jeeves-server')
18
- .description('Self-hosted file browser, document server, and webhook gateway')
19
- .version(packageVersion);
14
+ import { serverDescriptor } from '../descriptor.js';
20
15
 
21
- registerStartCommand(cli);
22
- registerConfigCommand(cli);
23
- registerServiceCommand(cli);
16
+ const cli = createServiceCli(serverDescriptor);
24
17
 
25
18
  cli.parse();
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Minimal server launcher for use by system service managers (NSSM, systemd, launchd).
4
+ *
5
+ * This is the entry point referenced by `descriptor.startCommand`. It initializes
6
+ * config from the `--config` CLI argument and starts the Fastify server directly,
7
+ * without going through the full Commander CLI.
8
+ *
9
+ * The CLI's `start` command uses this same logic in-process.
10
+ */
11
+
12
+ import { initConfig } from '../config/index.js';
13
+
14
+ const configIndex = process.argv.indexOf('--config');
15
+ const configPath =
16
+ configIndex !== -1 ? process.argv[configIndex + 1] : undefined;
17
+
18
+ initConfig(configPath);
19
+
20
+ // Dynamic import to ensure config is initialized before server modules load
21
+ await import('../server.js');