@karmaniverous/jeeves-server-openclaw 0.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/dist/cli.js ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, rmSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, dirname, resolve } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ /**
8
+ * CLI for installing/uninstalling the jeeves-server OpenClaw plugin.
9
+ *
10
+ * Usage:
11
+ * npx \@karmaniverous/jeeves-server-openclaw install
12
+ * npx \@karmaniverous/jeeves-server-openclaw uninstall
13
+ */
14
+ const PLUGIN_ID = 'jeeves-server-openclaw';
15
+ function resolveOpenClawHome() {
16
+ if (process.env.OPENCLAW_CONFIG)
17
+ return dirname(resolve(process.env.OPENCLAW_CONFIG));
18
+ if (process.env.OPENCLAW_HOME)
19
+ return resolve(process.env.OPENCLAW_HOME);
20
+ return join(homedir(), '.openclaw');
21
+ }
22
+ function resolveConfigPath(home) {
23
+ if (process.env.OPENCLAW_CONFIG)
24
+ return resolve(process.env.OPENCLAW_CONFIG);
25
+ return join(home, 'openclaw.json');
26
+ }
27
+ function getPackageRoot() {
28
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
29
+ }
30
+ function readJson(p) {
31
+ try {
32
+ return JSON.parse(readFileSync(p, 'utf8'));
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function writeJson(p, data) {
39
+ writeFileSync(p, JSON.stringify(data, null, 2) + '\n');
40
+ }
41
+ function patchAllowList(parent, key, label, mode) {
42
+ if (!Array.isArray(parent[key]) || parent[key].length === 0)
43
+ return undefined;
44
+ const list = parent[key];
45
+ if (mode === 'add') {
46
+ if (!list.includes(PLUGIN_ID)) {
47
+ list.push(PLUGIN_ID);
48
+ return 'Added "' + PLUGIN_ID + '" to ' + label;
49
+ }
50
+ }
51
+ else {
52
+ const filtered = list.filter((id) => id !== PLUGIN_ID);
53
+ if (filtered.length !== list.length) {
54
+ parent[key] = filtered;
55
+ return 'Removed "' + PLUGIN_ID + '" from ' + label;
56
+ }
57
+ }
58
+ return undefined;
59
+ }
60
+ function patchConfig(config, mode) {
61
+ const messages = [];
62
+ if (!config.plugins || typeof config.plugins !== 'object')
63
+ config.plugins = {};
64
+ const plugins = config.plugins;
65
+ const pluginAllow = patchAllowList(plugins, 'allow', 'plugins.allow', mode);
66
+ if (pluginAllow)
67
+ messages.push(pluginAllow);
68
+ if (!plugins.entries || typeof plugins.entries !== 'object')
69
+ plugins.entries = {};
70
+ const entries = plugins.entries;
71
+ if (mode === 'add') {
72
+ if (!entries[PLUGIN_ID]) {
73
+ entries[PLUGIN_ID] = { enabled: true };
74
+ messages.push('Added "' + PLUGIN_ID + '" to plugins.entries');
75
+ }
76
+ }
77
+ else if (PLUGIN_ID in entries) {
78
+ Reflect.deleteProperty(entries, PLUGIN_ID);
79
+ messages.push('Removed "' + PLUGIN_ID + '" from plugins.entries');
80
+ }
81
+ if (!config.tools || typeof config.tools !== 'object')
82
+ config.tools = {};
83
+ const tools = config.tools;
84
+ const toolAllow = patchAllowList(tools, 'allow', 'tools.allow', mode);
85
+ if (toolAllow)
86
+ messages.push(toolAllow);
87
+ return messages;
88
+ }
89
+ function install() {
90
+ const home = resolveOpenClawHome();
91
+ const configPath = resolveConfigPath(home);
92
+ const extDir = join(home, 'extensions', PLUGIN_ID);
93
+ const pkgRoot = getPackageRoot();
94
+ console.log('OpenClaw home: ' + home);
95
+ console.log('Config: ' + configPath);
96
+ console.log('Extensions dir: ' + extDir);
97
+ console.log('Package root: ' + pkgRoot);
98
+ console.log();
99
+ if (!existsSync(home)) {
100
+ console.error('Error: OpenClaw home not found at ' + home);
101
+ process.exit(1);
102
+ }
103
+ if (!existsSync(configPath)) {
104
+ console.error('Error: OpenClaw config not found at ' + configPath);
105
+ process.exit(1);
106
+ }
107
+ console.log('Copying plugin to extensions directory...');
108
+ if (existsSync(extDir))
109
+ rmSync(extDir, { recursive: true, force: true });
110
+ mkdirSync(extDir, { recursive: true });
111
+ for (const file of ['dist', 'openclaw.plugin.json', 'package.json']) {
112
+ const src = join(pkgRoot, file);
113
+ const dest = join(extDir, file);
114
+ if (existsSync(src)) {
115
+ cpSync(src, dest, { recursive: true });
116
+ console.log(' \u2713 ' + file);
117
+ }
118
+ }
119
+ const nodeModulesSrc = join(pkgRoot, 'node_modules');
120
+ if (existsSync(nodeModulesSrc)) {
121
+ cpSync(nodeModulesSrc, join(extDir, 'node_modules'), { recursive: true });
122
+ console.log(' \u2713 node_modules');
123
+ }
124
+ console.log();
125
+ console.log('Patching OpenClaw config...');
126
+ const config = readJson(configPath);
127
+ if (!config) {
128
+ console.error('Error: Could not parse ' + configPath);
129
+ process.exit(1);
130
+ }
131
+ for (const msg of patchConfig(config, 'add'))
132
+ console.log(' \u2713 ' + msg);
133
+ writeJson(configPath, config);
134
+ console.log();
135
+ console.log('\u2705 Plugin installed successfully.');
136
+ console.log(' Restart the OpenClaw gateway to load the plugin.');
137
+ }
138
+ function uninstall() {
139
+ const home = resolveOpenClawHome();
140
+ const configPath = resolveConfigPath(home);
141
+ const extDir = join(home, 'extensions', PLUGIN_ID);
142
+ console.log('OpenClaw home: ' + home);
143
+ console.log('Config: ' + configPath);
144
+ console.log('Extensions dir: ' + extDir);
145
+ console.log();
146
+ if (existsSync(extDir)) {
147
+ rmSync(extDir, { recursive: true, force: true });
148
+ console.log('\u2713 Removed ' + extDir);
149
+ }
150
+ else
151
+ console.log(' (extensions directory not found, skipping)');
152
+ if (existsSync(configPath)) {
153
+ console.log('Patching OpenClaw config...');
154
+ const config = readJson(configPath);
155
+ if (config) {
156
+ for (const msg of patchConfig(config, 'remove'))
157
+ console.log(' \u2713 ' + msg);
158
+ writeJson(configPath, config);
159
+ }
160
+ }
161
+ // Clean up TOOLS.md server section
162
+ cleanupToolsMd(home, configPath);
163
+ console.log();
164
+ console.log('\u2705 Plugin uninstalled successfully.');
165
+ console.log(' Restart the OpenClaw gateway to complete removal.');
166
+ }
167
+ function resolveWorkspaceDir(home, configPath) {
168
+ const config = readJson(configPath);
169
+ if (!config)
170
+ return null;
171
+ const agents = config.agents;
172
+ const defaults = agents?.defaults;
173
+ const workspace = defaults?.workspace;
174
+ if (workspace)
175
+ return resolve(workspace.replace(/^~/, homedir()));
176
+ return join(home, 'workspace');
177
+ }
178
+ function cleanupToolsMd(home, configPath) {
179
+ const workspaceDir = resolveWorkspaceDir(home, configPath);
180
+ if (!workspaceDir)
181
+ return;
182
+ const toolsPath = join(workspaceDir, 'TOOLS.md');
183
+ if (!existsSync(toolsPath))
184
+ return;
185
+ let content = readFileSync(toolsPath, 'utf8');
186
+ const serverRe = /^## Server\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
187
+ if (!serverRe.test(content))
188
+ return;
189
+ content = content.replace(serverRe, '').replace(/\n{3,}/g, '\n\n');
190
+ content = content.trim() + '\n';
191
+ writeFileSync(toolsPath, content);
192
+ console.log('\u2713 Cleaned up TOOLS.md (removed Server section)');
193
+ }
194
+ const command = process.argv[2];
195
+ switch (command) {
196
+ case 'install':
197
+ install();
198
+ break;
199
+ case 'uninstall':
200
+ uninstall();
201
+ break;
202
+ default:
203
+ console.log('@karmaniverous/jeeves-server-openclaw \u2014 OpenClaw plugin installer');
204
+ console.log();
205
+ console.log('Usage:');
206
+ console.log(' npx @karmaniverous/jeeves-server-openclaw install Install plugin');
207
+ console.log(' npx @karmaniverous/jeeves-server-openclaw uninstall Remove plugin');
208
+ console.log();
209
+ console.log('Environment variables:');
210
+ console.log(' OPENCLAW_CONFIG Path to openclaw.json (overrides all)');
211
+ console.log(' OPENCLAW_HOME Path to .openclaw directory');
212
+ if (command &&
213
+ command !== 'help' &&
214
+ command !== '--help' &&
215
+ command !== '-h') {
216
+ console.error('\nUnknown command: ' + command);
217
+ process.exit(1);
218
+ }
219
+ break;
220
+ }
221
+
222
+ export { patchConfig };
package/dist/index.js ADDED
@@ -0,0 +1,416 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+
5
+ /**
6
+ * Shared types and utility functions for the OpenClaw plugin tool registrations.
7
+ */
8
+ const DEFAULT_API_URL = 'http://127.0.0.1:1934';
9
+ const PLUGIN_ID = 'jeeves-server-openclaw';
10
+ /** Extract plugin config from the API object */
11
+ function getPluginConfig(api) {
12
+ return api.config?.plugins?.entries?.[PLUGIN_ID]?.config;
13
+ }
14
+ /** Resolve the server API base URL from plugin config. */
15
+ function getApiUrl(api) {
16
+ const url = getPluginConfig(api)?.apiUrl;
17
+ return typeof url === 'string' ? url : DEFAULT_API_URL;
18
+ }
19
+ /** Resolve the plugin key seed from plugin config. */
20
+ function getPluginKey(api) {
21
+ const key = getPluginConfig(api)?.pluginKey;
22
+ return typeof key === 'string' ? key : undefined;
23
+ }
24
+ /** Derive HMAC key from seed. */
25
+ function deriveKey(seed) {
26
+ return createHmac('sha256', seed).update('insider').digest('hex');
27
+ }
28
+ /** Append auth key query param to a URL. */
29
+ function withAuth(url, keySeed) {
30
+ if (!keySeed)
31
+ return url;
32
+ const derived = deriveKey(keySeed);
33
+ const sep = url.includes('?') ? '&' : '?';
34
+ return url + sep + 'key=' + derived;
35
+ }
36
+ /** Format a successful tool result. */
37
+ function ok(data) {
38
+ return {
39
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
40
+ };
41
+ }
42
+ /** Format an error tool result. */
43
+ function fail(error) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ return {
46
+ content: [{ type: 'text', text: 'Error: ' + message }],
47
+ isError: true,
48
+ };
49
+ }
50
+ /** Format a connection error with actionable guidance. */
51
+ function connectionFail(error, baseUrl) {
52
+ const cause = error instanceof Error ? error.cause : undefined;
53
+ const code = cause && typeof cause === 'object' && 'code' in cause
54
+ ? String(cause.code)
55
+ : '';
56
+ const isConnectionError = code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT';
57
+ if (isConnectionError) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text',
62
+ text: [
63
+ 'Server not reachable at ' + baseUrl + '.',
64
+ 'Either start the jeeves-server service, or if it runs on a different port,',
65
+ 'set plugins.entries.jeeves-server-openclaw.config.apiUrl in openclaw.json.',
66
+ ].join('\n'),
67
+ },
68
+ ],
69
+ isError: true,
70
+ };
71
+ }
72
+ return fail(error);
73
+ }
74
+ /** Fetch JSON from a URL, throwing on non-OK responses. */
75
+ async function fetchJson(url, init) {
76
+ const res = await fetch(url, init);
77
+ if (!res.ok) {
78
+ throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
79
+ }
80
+ return res.json();
81
+ }
82
+
83
+ /**
84
+ * Server tool registrations (server_* tools) for the OpenClaw plugin.
85
+ */
86
+ /** Normalize a browse path param: strip leading slash. */
87
+ function normalizePath(params) {
88
+ return String(params.path).replace(/^\//, '');
89
+ }
90
+ /** Register a single API tool with standard try/catch + ok/connectionFail. */
91
+ function registerApiTool(api, baseUrl, keySeed, config) {
92
+ api.registerTool({
93
+ name: config.name,
94
+ description: config.description,
95
+ parameters: config.parameters,
96
+ execute: async (_id, params) => {
97
+ try {
98
+ const [endpoint, method, body] = config.buildRequest(params);
99
+ const url = withAuth(baseUrl + endpoint, keySeed);
100
+ const init = {};
101
+ if (method)
102
+ init.method = method;
103
+ if (body !== undefined) {
104
+ init.method = method ?? 'POST';
105
+ init.headers = { 'Content-Type': 'application/json' };
106
+ init.body = JSON.stringify(body);
107
+ }
108
+ const data = await fetchJson(url, Object.keys(init).length > 0 ? init : undefined);
109
+ return ok(data);
110
+ }
111
+ catch (error) {
112
+ return connectionFail(error, baseUrl);
113
+ }
114
+ },
115
+ }, { optional: true });
116
+ }
117
+ /** Register all server_* tools with the OpenClaw plugin API. */
118
+ function registerServerTools(api, baseUrl) {
119
+ const keySeed = getPluginKey(api);
120
+ const tools = [
121
+ {
122
+ name: 'server_status',
123
+ description: 'Get jeeves-server health: version, uptime, port, Chrome availability, export formats, connected services.',
124
+ parameters: { type: 'object', properties: {} },
125
+ buildRequest: () => ['/api/status'],
126
+ },
127
+ {
128
+ name: 'server_link_info',
129
+ description: 'Query available link types for a path (exists, page URL, raw URL, export links for PDF/DOCX/SVG/PNG).',
130
+ parameters: {
131
+ type: 'object',
132
+ properties: {
133
+ path: {
134
+ type: 'string',
135
+ description: 'Browse path (e.g. "j/domains/projects/readme.md")',
136
+ },
137
+ },
138
+ required: ['path'],
139
+ },
140
+ buildRequest: (params) => {
141
+ const p = normalizePath(params);
142
+ return ['/api/link-info/' + p];
143
+ },
144
+ },
145
+ {
146
+ name: 'server_browse',
147
+ description: 'Get file or directory metadata. For files: size, mtime, content type, rendered HTML availability. For directories: listing of entries.',
148
+ parameters: {
149
+ type: 'object',
150
+ properties: {
151
+ path: {
152
+ type: 'string',
153
+ description: 'Browse path (e.g. "j/domains/projects")',
154
+ },
155
+ },
156
+ required: ['path'],
157
+ },
158
+ buildRequest: (params) => {
159
+ const p = normalizePath(params);
160
+ return ['/api/directory/' + p];
161
+ },
162
+ },
163
+ {
164
+ name: 'server_share',
165
+ description: 'Generate a share link for a path. Returns an HMAC-signed URL with optional expiry and directory depth.',
166
+ parameters: {
167
+ type: 'object',
168
+ properties: {
169
+ path: {
170
+ type: 'string',
171
+ description: 'Browse path to share',
172
+ },
173
+ expiryDays: {
174
+ type: 'number',
175
+ description: 'Link expiry in days (default: 30)',
176
+ },
177
+ depth: {
178
+ type: 'number',
179
+ description: 'Directory depth for deep shares (0 = file only)',
180
+ },
181
+ },
182
+ required: ['path'],
183
+ },
184
+ buildRequest: (params) => {
185
+ const p = normalizePath(params);
186
+ const qs = [];
187
+ if (params.expiryDays !== undefined)
188
+ qs.push('exp=' + String(params.expiryDays));
189
+ if (params.depth !== undefined)
190
+ qs.push('d=' + String(params.depth));
191
+ const query = qs.length > 0 ? '?' + qs.join('&') : '';
192
+ return ['/api/share/' + p + query];
193
+ },
194
+ },
195
+ {
196
+ name: 'server_export',
197
+ description: 'Trigger an export of a file or directory. Returns a download URL. Supported formats depend on file type and server capabilities (Chrome for PDF).',
198
+ parameters: {
199
+ type: 'object',
200
+ properties: {
201
+ path: {
202
+ type: 'string',
203
+ description: 'Browse path to export',
204
+ },
205
+ format: {
206
+ type: 'string',
207
+ description: 'Export format: pdf, docx, svg, png, zip',
208
+ enum: ['pdf', 'docx', 'svg', 'png', 'zip'],
209
+ },
210
+ },
211
+ required: ['path', 'format'],
212
+ },
213
+ buildRequest: (params) => {
214
+ const p = normalizePath(params);
215
+ const fmt = String(params.format);
216
+ return ['/export/' + p + '.' + fmt];
217
+ },
218
+ },
219
+ {
220
+ name: 'server_event_status',
221
+ description: 'Query event gateway status: active schemas, recent event log entries, and event counts.',
222
+ parameters: {
223
+ type: 'object',
224
+ properties: {
225
+ limit: {
226
+ type: 'number',
227
+ description: 'Maximum number of recent events to return (default: 20)',
228
+ },
229
+ },
230
+ },
231
+ buildRequest: (params) => {
232
+ const limit = params.limit ? String(params.limit) : '20';
233
+ return ['/api/status?events=' + limit];
234
+ },
235
+ },
236
+ ];
237
+ for (const tool of tools) {
238
+ registerApiTool(api, baseUrl, keySeed, tool);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Generates the Server menu string for TOOLS.md injection.
244
+ */
245
+ /**
246
+ * Fetch server status and generate a Markdown menu string.
247
+ */
248
+ async function generateServerMenu(apiUrl) {
249
+ let status;
250
+ try {
251
+ status = (await fetchJson(apiUrl + '/api/status', {
252
+ signal: AbortSignal.timeout(5000),
253
+ }));
254
+ }
255
+ catch {
256
+ return `> **ACTION REQUIRED: jeeves-server is unreachable.**
257
+ > The server API at ${apiUrl} is down or not configured.
258
+ >
259
+ > **Troubleshooting:**
260
+ > - Check if the JeevesServer service is running
261
+ > - Verify the apiUrl in plugins.entries.jeeves-server-openclaw.config
262
+ > - Try: \`jeeves-server service start\``;
263
+ }
264
+ const lines = [
265
+ `jeeves-server v${status.version ?? 'unknown'} running on port ${String(status.port ?? 'unknown')}.`,
266
+ '',
267
+ ];
268
+ // Export formats
269
+ if (status.exportFormats && status.exportFormats.length > 0) {
270
+ lines.push('### Export Formats');
271
+ lines.push('Available: ' + status.exportFormats.join(', '));
272
+ if (!status.chrome) {
273
+ lines.push('> **Note:** Chrome not detected — PDF export unavailable.');
274
+ }
275
+ lines.push('');
276
+ }
277
+ // Diagram support
278
+ if (status.diagrams && status.diagrams.length > 0) {
279
+ lines.push('### Diagram Support');
280
+ lines.push('Supported languages: ' + status.diagrams.join(', '));
281
+ lines.push('');
282
+ }
283
+ // Connected services
284
+ if (status.services) {
285
+ lines.push('### Connected Services');
286
+ for (const [name, svc] of Object.entries(status.services)) {
287
+ const icon = svc.reachable ? '\u2705' : '\u274c';
288
+ lines.push(`* ${icon} **${name}**: ${svc.url}`);
289
+ }
290
+ lines.push('');
291
+ }
292
+ // Event gateway
293
+ if (status.events && status.events.length > 0) {
294
+ lines.push('### Event Gateway');
295
+ lines.push('Active schemas:');
296
+ for (const evt of status.events) {
297
+ lines.push('* **' +
298
+ evt.name +
299
+ '**' +
300
+ (evt.pattern ? ' — pattern: `' + evt.pattern + '`' : ''));
301
+ }
302
+ lines.push('');
303
+ }
304
+ // Insider count
305
+ if (status.insiderCount !== undefined) {
306
+ lines.push('### Access');
307
+ lines.push(String(status.insiderCount) + ' insider(s) configured.');
308
+ lines.push('');
309
+ }
310
+ // Sharing guidance
311
+ lines.push('### Sharing');
312
+ lines.push('Use `server_share` to generate links. Insiders authenticate via Google; outsiders use HMAC share links.');
313
+ lines.push('Use `server_link_info` to check available export formats for a path before exporting.');
314
+ return lines.join('\n');
315
+ }
316
+
317
+ /**
318
+ * Writes the Server menu section directly to TOOLS.md on disk.
319
+ */
320
+ const REFRESH_INTERVAL_MS = 60_000;
321
+ let intervalHandle = null;
322
+ let lastWrittenMenu = '';
323
+ /**
324
+ * Resolve the workspace TOOLS.md path.
325
+ */
326
+ function resolveToolsPath(api) {
327
+ if (api.resolvePath) {
328
+ return api.resolvePath('TOOLS.md');
329
+ }
330
+ return resolve(process.cwd(), 'TOOLS.md');
331
+ }
332
+ /**
333
+ * Upsert the server section in TOOLS.md content.
334
+ */
335
+ function upsertServerContent(existing, serverMenu) {
336
+ const section = '## Server\n\n' + serverMenu;
337
+ // Replace existing server section
338
+ const re = /^## Server\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m;
339
+ if (re.test(existing)) {
340
+ return existing.replace(re, section);
341
+ }
342
+ // No existing section — insert under platform tools H1
343
+ const platformH1 = '# Jeeves Platform Tools';
344
+ if (existing.includes(platformH1)) {
345
+ // Find end of H1 line, insert after any existing ## sections
346
+ // Place after ## Watcher if it exists, otherwise after H1
347
+ const watcherEnd = existing.match(/^## Watcher\n[\s\S]*?(?=\n## |\n# |$(?![\s\S]))/m);
348
+ if (watcherEnd) {
349
+ const idx = existing.indexOf(watcherEnd[0]) + watcherEnd[0].length;
350
+ return existing.slice(0, idx) + '\n\n' + section + existing.slice(idx);
351
+ }
352
+ const idx = existing.indexOf(platformH1) + platformH1.length;
353
+ return existing.slice(0, idx) + '\n\n' + section + existing.slice(idx);
354
+ }
355
+ // No platform header — prepend
356
+ const trimmed = existing.trim();
357
+ if (trimmed.length === 0) {
358
+ return platformH1 + '\n\n' + section + '\n';
359
+ }
360
+ return platformH1 + '\n\n' + section + '\n\n' + trimmed + '\n';
361
+ }
362
+ /**
363
+ * Fetch the current server menu and write it to TOOLS.md if changed.
364
+ */
365
+ async function refreshToolsMd(api) {
366
+ const apiUrl = getApiUrl(api);
367
+ const menu = await generateServerMenu(apiUrl);
368
+ if (menu === lastWrittenMenu)
369
+ return false;
370
+ const toolsPath = resolveToolsPath(api);
371
+ let current = '';
372
+ try {
373
+ current = await readFile(toolsPath, 'utf8');
374
+ }
375
+ catch {
376
+ // File doesn't exist yet
377
+ }
378
+ const updated = upsertServerContent(current, menu);
379
+ if (updated !== current) {
380
+ await writeFile(toolsPath, updated, 'utf8');
381
+ lastWrittenMenu = menu;
382
+ return true;
383
+ }
384
+ lastWrittenMenu = menu;
385
+ return false;
386
+ }
387
+ /**
388
+ * Start the periodic TOOLS.md writer.
389
+ */
390
+ function startToolsWriter(api) {
391
+ refreshToolsMd(api).catch((err) => {
392
+ console.error('[jeeves-server] Failed to write TOOLS.md:', err);
393
+ });
394
+ if (intervalHandle)
395
+ clearInterval(intervalHandle);
396
+ intervalHandle = setInterval(() => {
397
+ refreshToolsMd(api).catch((err) => {
398
+ console.error('[jeeves-server] Failed to refresh TOOLS.md:', err);
399
+ });
400
+ }, REFRESH_INTERVAL_MS);
401
+ if (typeof intervalHandle === 'object' && 'unref' in intervalHandle) {
402
+ intervalHandle.unref();
403
+ }
404
+ }
405
+
406
+ /**
407
+ * OpenClaw plugin entry point. Registers all jeeves-server tools.
408
+ */
409
+ /** Register all jeeves-server tools with the OpenClaw plugin API. */
410
+ function register(api) {
411
+ const baseUrl = getApiUrl(api);
412
+ registerServerTools(api, baseUrl);
413
+ startToolsWriter(api);
414
+ }
415
+
416
+ export { register as default };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rollup configuration for the OpenClaw plugin package.
3
+ * Two entry points: plugin (ESM + declarations) and CLI (ESM executable).
4
+ */
5
+ import type { RollupOptions } from 'rollup';
6
+ declare const _default: RollupOptions[];
7
+ export default _default;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,139 @@
1
+ # Jeeves Server Skill
2
+
3
+ Operate and interact with a jeeves-server deployment. Use for file browsing, document sharing, export, link generation, event gateway queries, and server diagnostics.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Purpose |
8
+ |------|---------|
9
+ | `server_status` | Server health: version, uptime, Chrome availability, export formats, connected services |
10
+ | `server_browse` | Get file/directory metadata and listings for a browse path |
11
+ | `server_link_info` | Query available link types for a path (page URL, raw URL, export links) |
12
+ | `server_share` | Generate share links with optional expiry and directory depth |
13
+ | `server_export` | Trigger export (PDF, DOCX, SVG, PNG, ZIP) and get download URL |
14
+ | `server_event_status` | Query event gateway status, active schemas, and recent event log |
15
+
16
+ ## Browse Paths
17
+
18
+ All paths use the jeeves-server browse path format: `{drive}/{path}` (e.g., `j/domains/projects/readme.md`).
19
+
20
+ To convert a Windows file path to a browse path:
21
+ - `J:\domains\projects\readme.md` → `j/domains/projects/readme.md`
22
+ - Strip the colon, lowercase the drive letter, use forward slashes
23
+
24
+ ## Sharing
25
+
26
+ - **Insiders** authenticate via Google OAuth — bare URLs work for them
27
+ - **Outsiders** need HMAC share links — use `server_share` to generate them
28
+ - Share links have configurable expiry (default 30 days)
29
+ - Directory shares support depth control for recursive access
30
+
31
+ ## Export
32
+
33
+ Available formats depend on file type and server capabilities:
34
+ - **Markdown files:** PDF (requires Chrome), DOCX
35
+ - **Mermaid diagrams:** SVG, PNG, PDF
36
+ - **PlantUML diagrams:** Formats depend on server configuration
37
+ - **Directories:** ZIP (insider-only)
38
+
39
+ Use `server_link_info` first to check which formats are available for a path.
40
+
41
+ ## Diagnostics
42
+
43
+ Run `server_status` to check:
44
+ - Server version and uptime
45
+ - Chrome availability (required for PDF export)
46
+ - Connected services (watcher, runner) and their reachability
47
+ - Available export formats and diagram languages
48
+
49
+ ## Bootstrap: Full Stack Setup
50
+
51
+ ### Prerequisites
52
+
53
+ - **Node.js 20+** and npm
54
+ - **Java 8+** (optional, for local PlantUML rendering)
55
+ - **Chrome/Chromium** (optional, for PDF export)
56
+ - **NSSM** (Windows) or **systemd** (Linux) for service management
57
+ - **Caddy** (recommended) or nginx for reverse proxy with automatic TLS
58
+
59
+ ### 1. Install jeeves-server
60
+
61
+ ```bash
62
+ npm install -g @karmaniverous/jeeves-server
63
+ ```
64
+
65
+ ### 2. Create config
66
+
67
+ Create `jeeves-server.config.json` in the server's working directory:
68
+
69
+ ```json
70
+ {
71
+ "port": 1934,
72
+ "roots": {
73
+ "data": "/path/to/data"
74
+ },
75
+ "keys": {
76
+ "_internal": "random-hex-seed-for-puppeteer",
77
+ "_plugin": "random-hex-seed-for-openclaw-plugin"
78
+ },
79
+ "insiders": [
80
+ { "email": "you@example.com" }
81
+ ],
82
+ "google": {
83
+ "clientId": "${GOOGLE_CLIENT_ID}",
84
+ "clientSecret": "${GOOGLE_CLIENT_SECRET}"
85
+ },
86
+ "sessionSecret": "${SESSION_SECRET}",
87
+ "watcherUrl": "http://127.0.0.1:1936"
88
+ }
89
+ ```
90
+
91
+ ### 3. Validate config
92
+
93
+ ```bash
94
+ jeeves-server config validate
95
+ jeeves-server config show
96
+ ```
97
+
98
+ ### 4. Register as system service
99
+
100
+ **Windows (NSSM):**
101
+ ```bash
102
+ jeeves-server service install
103
+ jeeves-server service start
104
+ ```
105
+
106
+ **Linux (systemd):**
107
+ ```bash
108
+ sudo jeeves-server service install
109
+ sudo jeeves-server service start
110
+ ```
111
+
112
+ ### 5. Configure Caddy reverse proxy
113
+
114
+ Add to your Caddyfile:
115
+
116
+ ```
117
+ your-domain.com {
118
+ reverse_proxy localhost:1934
119
+ }
120
+ ```
121
+
122
+ Caddy handles TLS certificate provisioning automatically. Ensure DNS A/AAAA records point to your server.
123
+
124
+ ### 6. Install OpenClaw plugin
125
+
126
+ ```bash
127
+ npx @karmaniverous/jeeves-server-openclaw install
128
+ ```
129
+
130
+ Configure the plugin in `openclaw.json` with `apiUrl` and `pluginKey` (matching the `_plugin` key seed from server config). Restart the OpenClaw gateway.
131
+
132
+ ## Troubleshooting
133
+
134
+ If the server is unreachable:
135
+ 1. Is the service running? → `jeeves-server service start`
136
+ 2. Is the apiUrl correct? → Default: `http://127.0.0.1:1934`
137
+ 3. Is the `_plugin` key configured in both server config and plugin config?
138
+ 4. Is Caddy proxying to the correct port? → Check `Caddyfile`
139
+ 5. Is the firewall allowing traffic on port 1934? → Only needed for local access; Caddy handles external traffic
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CLI for installing/uninstalling the jeeves-server OpenClaw plugin.
3
+ *
4
+ * Usage:
5
+ * npx \@karmaniverous/jeeves-server-openclaw install
6
+ * npx \@karmaniverous/jeeves-server-openclaw uninstall
7
+ */
8
+ export declare function patchConfig(config: Record<string, unknown>, mode: 'add' | 'remove'): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared types and utility functions for the OpenClaw plugin tool registrations.
3
+ */
4
+ /** Minimal OpenClaw plugin API surface used for tool registration. */
5
+ export interface PluginApi {
6
+ config?: {
7
+ plugins?: {
8
+ entries?: Record<string, {
9
+ config?: Record<string, unknown>;
10
+ }>;
11
+ };
12
+ };
13
+ resolvePath?: (input: string) => string;
14
+ registerTool(tool: {
15
+ name: string;
16
+ description: string;
17
+ parameters: Record<string, unknown>;
18
+ execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
19
+ }, options?: {
20
+ optional?: boolean;
21
+ }): void;
22
+ }
23
+ /** Result shape returned by each tool execution. */
24
+ export interface ToolResult {
25
+ content: Array<{
26
+ type: string;
27
+ text: string;
28
+ }>;
29
+ isError?: boolean;
30
+ }
31
+ /** Resolve the server API base URL from plugin config. */
32
+ export declare function getApiUrl(api: PluginApi): string;
33
+ /** Resolve the plugin key seed from plugin config. */
34
+ export declare function getPluginKey(api: PluginApi): string | undefined;
35
+ /** Derive HMAC key from seed. */
36
+ export declare function deriveKey(seed: string): string;
37
+ /** Append auth key query param to a URL. */
38
+ export declare function withAuth(url: string, keySeed: string | undefined): string;
39
+ /** Format a successful tool result. */
40
+ export declare function ok(data: unknown): ToolResult;
41
+ /** Format an error tool result. */
42
+ export declare function fail(error: unknown): ToolResult;
43
+ /** Format a connection error with actionable guidance. */
44
+ export declare function connectionFail(error: unknown, baseUrl: string): ToolResult;
45
+ /** Fetch JSON from a URL, throwing on non-OK responses. */
46
+ export declare function fetchJson(url: string, init?: RequestInit): Promise<unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * OpenClaw plugin entry point. Registers all jeeves-server tools.
3
+ */
4
+ import type { PluginApi } from './helpers.js';
5
+ /** Register all jeeves-server tools with the OpenClaw plugin API. */
6
+ export default function register(api: PluginApi): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generates the Server menu string for TOOLS.md injection.
3
+ */
4
+ /**
5
+ * Fetch server status and generate a Markdown menu string.
6
+ */
7
+ export declare function generateServerMenu(apiUrl: string): Promise<string>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Server tool registrations (server_* tools) for the OpenClaw plugin.
3
+ */
4
+ import { type PluginApi } from './helpers.js';
5
+ /** Register all server_* tools with the OpenClaw plugin API. */
6
+ export declare function registerServerTools(api: PluginApi, baseUrl: string): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Writes the Server menu section directly to TOOLS.md on disk.
3
+ */
4
+ import { type PluginApi } from './helpers.js';
5
+ /**
6
+ * Start the periodic TOOLS.md writer.
7
+ */
8
+ export declare function startToolsWriter(api: PluginApi): void;
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "jeeves-server-openclaw",
3
+ "name": "Jeeves Server",
4
+ "description": "File browsing, document sharing, export, and event gateway tools for jeeves-server.",
5
+ "version": "0.1.0-0",
6
+ "skills": [
7
+ "dist/skills/jeeves-server"
8
+ ],
9
+ "configSchema": {
10
+ "type": "object",
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "apiUrl": {
14
+ "type": "string",
15
+ "description": "jeeves-server API base URL",
16
+ "default": "http://127.0.0.1:1934"
17
+ },
18
+ "pluginKey": {
19
+ "type": "string",
20
+ "description": "Server _plugin key seed (for authenticated API calls)"
21
+ }
22
+ }
23
+ },
24
+ "uiHints": {
25
+ "apiUrl": {
26
+ "label": "Server API URL",
27
+ "placeholder": "http://127.0.0.1:1934"
28
+ },
29
+ "pluginKey": {
30
+ "label": "Plugin Key Seed",
31
+ "placeholder": "hex string from server config keys._plugin"
32
+ }
33
+ }
34
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@karmaniverous/jeeves-server-openclaw",
3
+ "version": "0.1.0-0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build:plugin": "rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
8
+ "build:skills": "tsx scripts/build-skills.ts",
9
+ "build": "npm run build:plugin && npm run build:skills",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint --fix .",
12
+ "test": "vitest run",
13
+ "typecheck": "tsc",
14
+ "changelog": "auto-changelog",
15
+ "knip": "knip",
16
+ "release": "dotenvx run -f .env.local -- release-it",
17
+ "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.prerelease --preRelease"
18
+ },
19
+ "devDependencies": {
20
+ "@dotenvx/dotenvx": "^1.54.1",
21
+ "@rollup/plugin-typescript": "^12.3.0",
22
+ "auto-changelog": "^2.5.0",
23
+ "cross-env": "^10.1.0",
24
+ "release-it": "^19.2.4",
25
+ "rollup": "^4.59.0",
26
+ "tslib": "^2.8.1",
27
+ "vitest": "^4.0.18"
28
+ },
29
+ "author": "Jason Williscroft",
30
+ "description": "OpenClaw plugin for jeeves-server — file browsing, sharing, export, and event gateway tools",
31
+ "license": "BSD-3-Clause",
32
+ "bin": {
33
+ "jeeves-server-openclaw": "./dist/cli.js"
34
+ },
35
+ "exports": {
36
+ ".": {
37
+ "import": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ }
41
+ }
42
+ },
43
+ "types": "dist/index.d.ts",
44
+ "files": [
45
+ "dist",
46
+ "openclaw.plugin.json"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/karmaniverous/jeeves-server.git",
54
+ "directory": "packages/openclaw"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/karmaniverous/jeeves-server/issues"
58
+ },
59
+ "homepage": "https://github.com/karmaniverous/jeeves-server#readme",
60
+ "keywords": [
61
+ "openclaw",
62
+ "plugin",
63
+ "jeeves-server"
64
+ ],
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
68
+ "openclaw": {
69
+ "extensions": [
70
+ "./dist/index.js"
71
+ ]
72
+ },
73
+ "release-it": {
74
+ "git": {
75
+ "changelog": "npx auto-changelog --unreleased-only --stdout --template https://raw.githubusercontent.com/release-it/release-it/main/templates/changelog-compact.hbs",
76
+ "commitMessage": "chore: release @karmaniverous/jeeves-server-openclaw v${version}",
77
+ "tagName": "openclaw/${version}",
78
+ "requireBranch": "main"
79
+ },
80
+ "github": {
81
+ "release": true
82
+ },
83
+ "hooks": {
84
+ "after:bump": [
85
+ "node -e \"const f='openclaw.plugin.json';const j=JSON.parse(require('fs').readFileSync(f,'utf8'));j.version='${version}';require('fs').writeFileSync(f,JSON.stringify(j,null,2)+'\\n')\""
86
+ ],
87
+ "after:init": [
88
+ "npm run lint",
89
+ "npm run typecheck",
90
+ "npm run test",
91
+ "npm run build"
92
+ ],
93
+ "before:npm:release": [
94
+ "npx auto-changelog -p",
95
+ "git add -A"
96
+ ],
97
+ "after:release": [
98
+ "git switch -c release/openclaw/${version}",
99
+ "git push -u origin release/openclaw/${version}",
100
+ "git switch ${branchName}"
101
+ ]
102
+ },
103
+ "npm": {
104
+ "publish": true
105
+ }
106
+ }
107
+ }