@karmaniverous/jeeves 0.3.1 → 0.4.1
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/content/agents-section.md +6 -7
- package/dist/cli/jeeves/index.js +556 -239
- package/dist/cli/plugin/index.js +1003 -0
- package/dist/cli/service/index.js +916 -0
- package/dist/index.d.ts +628 -146
- package/dist/index.js +2890 -1055
- package/package.json +1 -1
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import * as commander from 'commander';
|
|
3
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
3
4
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
|
|
4
5
|
import { join, dirname } from 'node:path';
|
|
6
|
+
import { gte } from 'semver';
|
|
5
7
|
import { z } from 'zod';
|
|
8
|
+
import { lock } from 'proper-lockfile';
|
|
6
9
|
import { fileURLToPath } from 'node:url';
|
|
7
10
|
import { packageDirectorySync } from 'package-directory';
|
|
8
|
-
import { lock } from 'proper-lockfile';
|
|
9
|
-
import { gte } from 'semver';
|
|
10
11
|
|
|
11
12
|
function getDefaultExportFromCjs (x) {
|
|
12
13
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function getAugmentedNamespace(n) {
|
|
17
|
+
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
|
|
18
|
+
var f = n.default;
|
|
19
|
+
if (typeof f == "function") {
|
|
20
|
+
var a = function a () {
|
|
21
|
+
var isInstance = false;
|
|
22
|
+
try {
|
|
23
|
+
isInstance = this instanceof a;
|
|
24
|
+
} catch {}
|
|
25
|
+
if (isInstance) {
|
|
26
|
+
return Reflect.construct(f, arguments, this.constructor);
|
|
27
|
+
}
|
|
28
|
+
return f.apply(this, arguments);
|
|
29
|
+
};
|
|
30
|
+
a.prototype = f.prototype;
|
|
31
|
+
} else a = {};
|
|
32
|
+
Object.defineProperty(a, '__esModule', {value: true});
|
|
33
|
+
Object.keys(n).forEach(function (k) {
|
|
34
|
+
var d = Object.getOwnPropertyDescriptor(n, k);
|
|
35
|
+
Object.defineProperty(a, k, d.get ? d : {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function () {
|
|
38
|
+
return n[k];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
return a;
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
var extraTypings = {exports: {}};
|
|
16
46
|
|
|
47
|
+
var require$$0 = /*@__PURE__*/getAugmentedNamespace(commander);
|
|
48
|
+
|
|
17
49
|
var hasRequiredExtraTypings;
|
|
18
50
|
|
|
19
51
|
function requireExtraTypings () {
|
|
@@ -84,6 +116,8 @@ const TOOLS_MARKERS = {
|
|
|
84
116
|
end: 'END JEEVES PLATFORM TOOLS',
|
|
85
117
|
/** H1 title prepended in section mode. */
|
|
86
118
|
title: 'Jeeves Platform Tools',
|
|
119
|
+
/** Managed block at bottom of file. */
|
|
120
|
+
position: 'bottom',
|
|
87
121
|
};
|
|
88
122
|
/** Default markers for SOUL.md managed block. */
|
|
89
123
|
const SOUL_MARKERS = {
|
|
@@ -93,6 +127,8 @@ const SOUL_MARKERS = {
|
|
|
93
127
|
end: 'END JEEVES SOUL',
|
|
94
128
|
/** H1 title prepended in the managed block. */
|
|
95
129
|
title: 'Jeeves Platform Soul',
|
|
130
|
+
/** Managed block at bottom of file. */
|
|
131
|
+
position: 'bottom',
|
|
96
132
|
};
|
|
97
133
|
/** Default markers for AGENTS.md managed block. */
|
|
98
134
|
const AGENTS_MARKERS = {
|
|
@@ -102,6 +138,8 @@ const AGENTS_MARKERS = {
|
|
|
102
138
|
end: 'END JEEVES AGENTS',
|
|
103
139
|
/** H1 title prepended in the managed block. */
|
|
104
140
|
title: 'Jeeves Platform Agents',
|
|
141
|
+
/** Managed block at bottom of file. */
|
|
142
|
+
position: 'bottom',
|
|
105
143
|
};
|
|
106
144
|
/** All known marker sets — single source of truth for cross-contamination detection. */
|
|
107
145
|
const ALL_MARKERS = [
|
|
@@ -135,6 +173,8 @@ const WORKSPACE_FILES = {
|
|
|
135
173
|
soul: 'SOUL.md',
|
|
136
174
|
/** AGENTS.md — operational protocols and memory architecture. */
|
|
137
175
|
agents: 'AGENTS.md',
|
|
176
|
+
/** HEARTBEAT.md — platform status and health alerts. */
|
|
177
|
+
heartbeat: 'HEARTBEAT.md',
|
|
138
178
|
};
|
|
139
179
|
/** Templates directory name within core config. */
|
|
140
180
|
const TEMPLATES_DIR = 'templates';
|
|
@@ -170,7 +210,7 @@ const DEFAULT_PORTS = {
|
|
|
170
210
|
};
|
|
171
211
|
|
|
172
212
|
/**
|
|
173
|
-
* Managed section IDs
|
|
213
|
+
* Managed section IDs, stable ordering, and platform component registry.
|
|
174
214
|
*
|
|
175
215
|
* @remarks
|
|
176
216
|
* Section ordering is fixed to prevent diff churn regardless of which
|
|
@@ -200,19 +240,161 @@ const SECTION_ORDER = [
|
|
|
200
240
|
SECTION_IDS.Runner,
|
|
201
241
|
SECTION_IDS.Meta,
|
|
202
242
|
];
|
|
243
|
+
/**
|
|
244
|
+
* The four essential platform components.
|
|
245
|
+
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* These components constitute the Jeeves platform. `jeeves install` writes
|
|
248
|
+
* initial HEARTBEAT "Not installed" alerts for all of them. The HEARTBEAT
|
|
249
|
+
* writer generates "Not installed" alerts only for platform components not
|
|
250
|
+
* in `component-versions.json`. Optional future components (not in this list)
|
|
251
|
+
* appear in HEARTBEAT only after explicit install.
|
|
252
|
+
*/
|
|
253
|
+
const PLATFORM_COMPONENTS = [
|
|
254
|
+
'runner',
|
|
255
|
+
'watcher',
|
|
256
|
+
'server',
|
|
257
|
+
'meta',
|
|
258
|
+
];
|
|
203
259
|
|
|
204
260
|
/**
|
|
205
261
|
* Core library version, inlined at build time.
|
|
206
262
|
*
|
|
207
263
|
* @remarks
|
|
208
|
-
* The `0.
|
|
264
|
+
* The `0.4.0` placeholder is replaced by
|
|
209
265
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
210
266
|
* from `package.json`. This ensures the correct version survives
|
|
211
267
|
* when consumers bundle core into their own dist (where runtime
|
|
212
268
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
213
269
|
*/
|
|
214
270
|
/** The core library version from package.json (inlined at build time). */
|
|
215
|
-
const CORE_VERSION = '0.
|
|
271
|
+
const CORE_VERSION = '0.4.0';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Dynamic discovery of installed Jeeves component CLIs.
|
|
275
|
+
*
|
|
276
|
+
* @remarks
|
|
277
|
+
* Scans for globally-installed `@karmaniverous/jeeves-*` packages
|
|
278
|
+
* (excluding `-openclaw` plugins) and registers proxy subcommands
|
|
279
|
+
* on the parent CLI. Each discovered component gets a `jeeves {name}`
|
|
280
|
+
* command that spawns `jeeves-{name}` with passthrough args.
|
|
281
|
+
*/
|
|
282
|
+
/**
|
|
283
|
+
* Discover globally-installed jeeves component packages.
|
|
284
|
+
*
|
|
285
|
+
* @remarks
|
|
286
|
+
* Runs `npm ls -g --json` and filters for packages matching
|
|
287
|
+
* `@karmaniverous/jeeves-*` (excluding `-openclaw` plugins).
|
|
288
|
+
* Returns short component names (e.g., 'watcher', 'runner').
|
|
289
|
+
*
|
|
290
|
+
* @returns Array of discovered component names.
|
|
291
|
+
*/
|
|
292
|
+
function discoverComponentPackages() {
|
|
293
|
+
try {
|
|
294
|
+
const output = execSync('npm ls -g --json --depth=0', {
|
|
295
|
+
encoding: 'utf-8',
|
|
296
|
+
timeout: 10_000,
|
|
297
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
298
|
+
});
|
|
299
|
+
const parsed = JSON.parse(output);
|
|
300
|
+
if (!parsed.dependencies)
|
|
301
|
+
return [];
|
|
302
|
+
const names = [];
|
|
303
|
+
for (const pkgName of Object.keys(parsed.dependencies)) {
|
|
304
|
+
// Match @karmaniverous/jeeves-{name} but not -openclaw
|
|
305
|
+
const match = /^@karmaniverous\/jeeves-([a-z]+)$/.exec(pkgName);
|
|
306
|
+
if (match && !pkgName.endsWith('-openclaw')) {
|
|
307
|
+
names.push(match[1]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return names;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Register proxy subcommands for discovered component CLIs.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* For each discovered component, adds `jeeves {name} [args...]`
|
|
321
|
+
* that spawns `jeeves-{name} [args...]` as a child process.
|
|
322
|
+
*
|
|
323
|
+
* @param program - The parent Commander program.
|
|
324
|
+
* @param componentNames - Discovered component names.
|
|
325
|
+
*/
|
|
326
|
+
function registerComponentProxies(program, componentNames) {
|
|
327
|
+
for (const name of componentNames) {
|
|
328
|
+
const binName = `jeeves-${name}`;
|
|
329
|
+
program
|
|
330
|
+
.command(name)
|
|
331
|
+
.description(`Proxy to ${binName} CLI`)
|
|
332
|
+
.allowUnknownOption()
|
|
333
|
+
.allowExcessArguments()
|
|
334
|
+
.action((_opts, cmd) => {
|
|
335
|
+
// Collect all args after the component name
|
|
336
|
+
const args = cmd.args;
|
|
337
|
+
const result = spawnSync(binName, args, {
|
|
338
|
+
stdio: 'inherit',
|
|
339
|
+
shell: true,
|
|
340
|
+
});
|
|
341
|
+
if (result.status !== null && result.status !== 0) {
|
|
342
|
+
process.exitCode = result.status;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Workspace and config root initialization.
|
|
350
|
+
*
|
|
351
|
+
* @remarks
|
|
352
|
+
* `init()` must be called once before any other core library functions.
|
|
353
|
+
* It caches `workspacePath` and `configRoot` at module level.
|
|
354
|
+
* Core derives all namespaced paths from these values:
|
|
355
|
+
* - `{configRoot}/jeeves-core/` for core config
|
|
356
|
+
* - `{configRoot}/jeeves-{name}/` for each component
|
|
357
|
+
*/
|
|
358
|
+
let state;
|
|
359
|
+
/**
|
|
360
|
+
* Initialize the core library with workspace and config root paths.
|
|
361
|
+
*
|
|
362
|
+
* @param options - Workspace and config root paths.
|
|
363
|
+
*/
|
|
364
|
+
function init(options) {
|
|
365
|
+
state = {
|
|
366
|
+
workspacePath: options.workspacePath,
|
|
367
|
+
configRoot: options.configRoot,
|
|
368
|
+
coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get the cached workspace path.
|
|
373
|
+
*
|
|
374
|
+
* @throws Error if `init()` has not been called.
|
|
375
|
+
*/
|
|
376
|
+
function getWorkspacePath() {
|
|
377
|
+
if (!state)
|
|
378
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
379
|
+
return state.workspacePath;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get the core config directory path.
|
|
383
|
+
*
|
|
384
|
+
* @throws Error if `init()` has not been called.
|
|
385
|
+
*/
|
|
386
|
+
function getCoreConfigDir() {
|
|
387
|
+
if (!state)
|
|
388
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
389
|
+
return state.coreConfigDir;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
var init$1 = /*#__PURE__*/Object.freeze({
|
|
393
|
+
__proto__: null,
|
|
394
|
+
getCoreConfigDir: getCoreConfigDir,
|
|
395
|
+
getWorkspacePath: getWorkspacePath,
|
|
396
|
+
init: init
|
|
397
|
+
});
|
|
216
398
|
|
|
217
399
|
/**
|
|
218
400
|
* Core configuration schema and resolution.
|
|
@@ -229,12 +411,22 @@ const serviceEntrySchema = z.object({
|
|
|
229
411
|
/** Service URL (must be a valid URL). */
|
|
230
412
|
url: z.string().url().describe('Service URL'),
|
|
231
413
|
});
|
|
414
|
+
/** Default bind address for all Jeeves services. */
|
|
415
|
+
const DEFAULT_BIND_ADDRESS = '0.0.0.0';
|
|
232
416
|
/** Zod schema for the core config file. */
|
|
233
417
|
const coreConfigSchema = z.object({
|
|
234
418
|
/** JSON Schema pointer for IDE autocomplete. */
|
|
235
419
|
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
236
420
|
/** Owner identity keys (canonical identityLinks references). */
|
|
237
421
|
owners: z.array(z.string()).default([]).describe('Owner identity keys'),
|
|
422
|
+
/**
|
|
423
|
+
* Bind address for all Jeeves services. Default: `0.0.0.0` (all interfaces).
|
|
424
|
+
* Individual components can override in their own config.
|
|
425
|
+
*/
|
|
426
|
+
bindAddress: z
|
|
427
|
+
.string()
|
|
428
|
+
.default(DEFAULT_BIND_ADDRESS)
|
|
429
|
+
.describe('Bind address for all Jeeves services'),
|
|
238
430
|
/** Service URL overrides keyed by service name. */
|
|
239
431
|
services: z
|
|
240
432
|
.record(z.string(), serviceEntrySchema)
|
|
@@ -271,6 +463,11 @@ function generateJsonSchema() {
|
|
|
271
463
|
items: { type: 'string' },
|
|
272
464
|
default: [],
|
|
273
465
|
},
|
|
466
|
+
bindAddress: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
default: '0.0.0.0',
|
|
469
|
+
description: 'Bind address for all Jeeves services',
|
|
470
|
+
},
|
|
274
471
|
services: {
|
|
275
472
|
type: 'object',
|
|
276
473
|
additionalProperties: {
|
|
@@ -317,56 +514,314 @@ function loadConfig(configDir) {
|
|
|
317
514
|
}
|
|
318
515
|
|
|
319
516
|
/**
|
|
320
|
-
*
|
|
517
|
+
* Service URL resolution.
|
|
518
|
+
*
|
|
519
|
+
* @remarks
|
|
520
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
521
|
+
* resolution order:
|
|
522
|
+
* 1. Consumer's own component config
|
|
523
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
524
|
+
* 3. Default port constants
|
|
525
|
+
*/
|
|
526
|
+
/**
|
|
527
|
+
* Resolve the URL for a named Jeeves service.
|
|
528
|
+
*
|
|
529
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
530
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
531
|
+
* @returns The resolved service URL.
|
|
532
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
533
|
+
*/
|
|
534
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
535
|
+
// 2. Check core config
|
|
536
|
+
const coreDir = getCoreConfigDir();
|
|
537
|
+
const coreConfig = loadConfig(coreDir);
|
|
538
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
539
|
+
if (coreUrl)
|
|
540
|
+
return coreUrl;
|
|
541
|
+
// 3. Fall back to port constants
|
|
542
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
543
|
+
if (port !== undefined) {
|
|
544
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
545
|
+
}
|
|
546
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
551
|
+
*
|
|
552
|
+
* @remarks
|
|
553
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
554
|
+
* and handle JSON serialisation/deserialisation.
|
|
555
|
+
*/
|
|
556
|
+
/**
|
|
557
|
+
* Fetch a URL with an automatic abort timeout.
|
|
558
|
+
*
|
|
559
|
+
* @param url - URL to fetch.
|
|
560
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
561
|
+
* @param init - Optional `fetch` init options.
|
|
562
|
+
* @returns The fetch Response object.
|
|
563
|
+
*/
|
|
564
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
565
|
+
const controller = new AbortController();
|
|
566
|
+
const timeout = setTimeout(() => {
|
|
567
|
+
controller.abort();
|
|
568
|
+
}, timeoutMs);
|
|
569
|
+
try {
|
|
570
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
clearTimeout(timeout);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Shared file I/O helpers for managed section operations.
|
|
579
|
+
*
|
|
580
|
+
* @remarks
|
|
581
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
582
|
+
* reusable utilities, eliminating duplication between
|
|
583
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
584
|
+
*/
|
|
585
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
586
|
+
const STALE_LOCK_MS = 120_000;
|
|
587
|
+
/** Default core version when none provided. */
|
|
588
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
589
|
+
/** Lock retry options. */
|
|
590
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
591
|
+
/**
|
|
592
|
+
* Write content to a file atomically via a temp file + rename.
|
|
593
|
+
*
|
|
594
|
+
* @param filePath - Absolute path to the target file.
|
|
595
|
+
* @param content - Content to write.
|
|
596
|
+
*/
|
|
597
|
+
function atomicWrite(filePath, content) {
|
|
598
|
+
const dir = dirname(filePath);
|
|
599
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
600
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
601
|
+
renameSync(tempPath, filePath);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Execute a callback while holding a file lock.
|
|
605
|
+
*
|
|
606
|
+
* @remarks
|
|
607
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
608
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
609
|
+
* and retries up to 5 times.
|
|
610
|
+
*
|
|
611
|
+
* @param filePath - Absolute path to the file to lock.
|
|
612
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
613
|
+
*/
|
|
614
|
+
async function withFileLock(filePath, fn) {
|
|
615
|
+
let release;
|
|
616
|
+
try {
|
|
617
|
+
release = await lock(filePath, {
|
|
618
|
+
stale: STALE_LOCK_MS,
|
|
619
|
+
retries: LOCK_RETRIES,
|
|
620
|
+
});
|
|
621
|
+
await fn();
|
|
622
|
+
}
|
|
623
|
+
finally {
|
|
624
|
+
if (release) {
|
|
625
|
+
try {
|
|
626
|
+
await release();
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Lock already released or file deleted — safe to ignore
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Shared component version state file management.
|
|
637
|
+
*
|
|
638
|
+
* @remarks
|
|
639
|
+
* Each `ComponentWriter` cycle writes its component's entry to
|
|
640
|
+
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
641
|
+
* template reads this file to populate ALL rows in the service health
|
|
642
|
+
* table, not just the calling component's.
|
|
643
|
+
*/
|
|
644
|
+
/**
|
|
645
|
+
* Read the component versions state file.
|
|
646
|
+
*
|
|
647
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
648
|
+
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
649
|
+
*/
|
|
650
|
+
function readComponentVersions(coreConfigDir) {
|
|
651
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
652
|
+
if (!existsSync(filePath))
|
|
653
|
+
return {};
|
|
654
|
+
try {
|
|
655
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
656
|
+
return JSON.parse(raw);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return {};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Write a component's version entry to the shared state file.
|
|
664
|
+
*
|
|
665
|
+
* @remarks
|
|
666
|
+
* Reads the existing file, merges the new entry, and writes atomically.
|
|
667
|
+
*
|
|
668
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
669
|
+
* @param options - Component version data to write.
|
|
670
|
+
*/
|
|
671
|
+
function writeComponentVersion(coreConfigDir, options) {
|
|
672
|
+
const existing = readComponentVersions(coreConfigDir);
|
|
673
|
+
existing[options.componentName] = {
|
|
674
|
+
pluginVersion: options.pluginVersion,
|
|
675
|
+
servicePackage: options.servicePackage,
|
|
676
|
+
pluginPackage: options.pluginPackage,
|
|
677
|
+
updatedAt: new Date().toISOString(),
|
|
678
|
+
};
|
|
679
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
680
|
+
const dir = dirname(filePath);
|
|
681
|
+
if (!existsSync(dir)) {
|
|
682
|
+
mkdirSync(dir, { recursive: true });
|
|
683
|
+
}
|
|
684
|
+
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* HEARTBEAT health orchestration.
|
|
689
|
+
*
|
|
690
|
+
* @remarks
|
|
691
|
+
* Determines the state of each platform component and generates
|
|
692
|
+
* HEARTBEAT entries with actionable alert text. Applies the dependency
|
|
693
|
+
* graph for alert suppression and auto-decline.
|
|
694
|
+
*/
|
|
695
|
+
/** Derive the full service name from a component name. */
|
|
696
|
+
function toServiceName(name) {
|
|
697
|
+
return `jeeves-${name}`;
|
|
698
|
+
}
|
|
699
|
+
/** "Not installed" alert text for each platform component. Shared with seedContent. */
|
|
700
|
+
const NOT_INSTALLED_ALERTS = {
|
|
701
|
+
runner: 'Not installed. The runner is the job execution engine — it runs scheduled scripts for data fetching, transformation, and maintenance. Ask the user for consent to install. On approval, execute: `npx @karmaniverous/jeeves-runner-openclaw install`.',
|
|
702
|
+
watcher: 'Not installed. The watcher indexes documents into a vector store for semantic search. Ask the user for consent to install. On approval, execute: `npx @karmaniverous/jeeves-watcher-openclaw install`.',
|
|
703
|
+
server: 'Not installed. The server provides a web UI for document browsing, rendering, sharing, and search. Ask the user for consent to install. On approval, execute: `npx @karmaniverous/jeeves-server-openclaw install`.',
|
|
704
|
+
meta: 'Not installed. Meta is the knowledge synthesis engine. Requires jeeves-watcher. Ask the user for consent to install. On approval, install watcher first if needed, then execute: `npx @karmaniverous/jeeves-meta-openclaw install`.',
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Heading-based HEARTBEAT section writer.
|
|
321
709
|
*
|
|
322
710
|
* @remarks
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
711
|
+
* Manages the `# Jeeves Platform Status` section in HEARTBEAT.md.
|
|
712
|
+
* Unlike TOOLS/SOUL/AGENTS (which use HTML comment markers), HEARTBEAT
|
|
713
|
+
* uses markdown headings as markers — this ensures the file passes
|
|
714
|
+
* OpenClaw's heartbeat emptiness check when only headings remain.
|
|
715
|
+
*
|
|
716
|
+
* The section is always at the bottom of the file (H1 to EOF).
|
|
717
|
+
* User heartbeat items above the section are preserved.
|
|
328
718
|
*/
|
|
329
|
-
|
|
719
|
+
/** The H1 heading that anchors the platform status section. */
|
|
720
|
+
const HEARTBEAT_HEADING = '# Jeeves Platform Status';
|
|
330
721
|
/**
|
|
331
|
-
*
|
|
722
|
+
* Parse the HEARTBEAT.md file content.
|
|
332
723
|
*
|
|
333
|
-
* @param
|
|
724
|
+
* @param fileContent - Full file content.
|
|
725
|
+
* @returns Parsed result with user zone and component entries.
|
|
334
726
|
*/
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
727
|
+
function parseHeartbeat(fileContent) {
|
|
728
|
+
const headingIndex = fileContent.indexOf(HEARTBEAT_HEADING);
|
|
729
|
+
if (headingIndex === -1) {
|
|
730
|
+
return {
|
|
731
|
+
userContent: fileContent.trim(),
|
|
732
|
+
found: false,
|
|
733
|
+
entries: [],
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
737
|
+
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
738
|
+
const entries = [];
|
|
739
|
+
const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
|
|
740
|
+
let match;
|
|
741
|
+
const h2Positions = [];
|
|
742
|
+
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
743
|
+
const fullHeading = match[0];
|
|
744
|
+
const name = match[1];
|
|
745
|
+
const declined = fullHeading.endsWith(': declined');
|
|
746
|
+
h2Positions.push({ name, declined, start: match.index });
|
|
747
|
+
}
|
|
748
|
+
for (let i = 0; i < h2Positions.length; i++) {
|
|
749
|
+
const pos = h2Positions[i];
|
|
750
|
+
const headingLine = pos.declined
|
|
751
|
+
? `## ${pos.name}: declined`
|
|
752
|
+
: `## ${pos.name}`;
|
|
753
|
+
const contentStart = pos.start + headingLine.length;
|
|
754
|
+
const contentEnd = i + 1 < h2Positions.length
|
|
755
|
+
? h2Positions[i + 1].start
|
|
756
|
+
: sectionContent.length;
|
|
757
|
+
const content = sectionContent.slice(contentStart, contentEnd).trim();
|
|
758
|
+
entries.push({
|
|
759
|
+
name: pos.name,
|
|
760
|
+
declined: pos.declined,
|
|
761
|
+
content,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return { userContent, found: true, entries };
|
|
341
765
|
}
|
|
342
766
|
/**
|
|
343
|
-
*
|
|
767
|
+
* Build the HEARTBEAT section content from entries.
|
|
344
768
|
*
|
|
345
|
-
* @
|
|
769
|
+
* @param entries - Component entries to write.
|
|
770
|
+
* @returns The full section string (H1 + H2s).
|
|
346
771
|
*/
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
772
|
+
function buildHeartbeatSection(entries) {
|
|
773
|
+
const parts = [HEARTBEAT_HEADING];
|
|
774
|
+
for (const entry of entries) {
|
|
775
|
+
if (entry.declined) {
|
|
776
|
+
parts.push(`## ${entry.name}: declined`);
|
|
777
|
+
}
|
|
778
|
+
else if (entry.content) {
|
|
779
|
+
parts.push(`## ${entry.name}`);
|
|
780
|
+
parts.push(entry.content);
|
|
781
|
+
}
|
|
782
|
+
// Healthy components (no content, not declined) get no H2 section
|
|
783
|
+
}
|
|
784
|
+
return parts.join('\n');
|
|
351
785
|
}
|
|
352
786
|
/**
|
|
353
|
-
*
|
|
787
|
+
* Write the HEARTBEAT section to a file.
|
|
354
788
|
*
|
|
355
|
-
* @
|
|
789
|
+
* @remarks
|
|
790
|
+
* Replaces everything from `# Jeeves Platform Status` to EOF.
|
|
791
|
+
* Preserves user content above the heading. Uses file-level locking.
|
|
792
|
+
*
|
|
793
|
+
* @param filePath - Absolute path to HEARTBEAT.md.
|
|
794
|
+
* @param entries - Component entries to write.
|
|
356
795
|
*/
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
796
|
+
async function writeHeartbeatSection(filePath, entries) {
|
|
797
|
+
const dir = dirname(filePath);
|
|
798
|
+
if (!existsSync(dir)) {
|
|
799
|
+
mkdirSync(dir, { recursive: true });
|
|
800
|
+
}
|
|
801
|
+
if (!existsSync(filePath)) {
|
|
802
|
+
writeFileSync(filePath, '', 'utf-8');
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
await withFileLock(filePath, () => {
|
|
806
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
807
|
+
const parsed = parseHeartbeat(fileContent);
|
|
808
|
+
const section = buildHeartbeatSection(entries);
|
|
809
|
+
const parts = [];
|
|
810
|
+
if (parsed.userContent) {
|
|
811
|
+
parts.push(parsed.userContent);
|
|
812
|
+
parts.push('');
|
|
813
|
+
}
|
|
814
|
+
parts.push(section);
|
|
815
|
+
parts.push('');
|
|
816
|
+
atomicWrite(filePath, parts.join('\n'));
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
821
|
+
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
|
|
822
|
+
}
|
|
361
823
|
}
|
|
362
824
|
|
|
363
|
-
var init$1 = /*#__PURE__*/Object.freeze({
|
|
364
|
-
__proto__: null,
|
|
365
|
-
getCoreConfigDir: getCoreConfigDir,
|
|
366
|
-
getWorkspacePath: getWorkspacePath,
|
|
367
|
-
init: init
|
|
368
|
-
});
|
|
369
|
-
|
|
370
825
|
var agentsSectionContent = `## Memory Architecture
|
|
371
826
|
|
|
372
827
|
You wake up fresh each session. These files are your continuity:
|
|
@@ -434,6 +889,8 @@ At minimum, always brief sub-agents on:
|
|
|
434
889
|
|
|
435
890
|
**Anything important enough to have a permanent cron/heartbeat entry is important enough to be codified into the data flow.**
|
|
436
891
|
|
|
892
|
+
**HEARTBEAT efficiency:** When HEARTBEAT fires with multiple component alerts, resolve them all in a single conversation. Walk the user through each component in dependency order. Each heartbeat cycle is a full LLM session — resolve all alerts in one pass.
|
|
893
|
+
|
|
437
894
|
## Messaging Dispatch
|
|
438
895
|
|
|
439
896
|
**Same-channel replies:** Don't use the \`message\` tool. It fires immediately, jumping ahead of streaming narration. Just write text as your response. Use \`message\` only for **cross-channel** sends (sending to a different channel or DM than the one you're responding in).
|
|
@@ -442,7 +899,7 @@ At minimum, always brief sub-agents on:
|
|
|
442
899
|
|
|
443
900
|
## Heartbeat Discipline
|
|
444
901
|
|
|
445
|
-
**Default state of HEARTBEAT.md is
|
|
902
|
+
**Default state of HEARTBEAT.md is effectively empty** — the \`# Jeeves Platform Status\` section contains only headings when all components are healthy or declined. No list content, no LLM cost. User heartbeat items (above the platform status section) follow the same transient/termination rules as before.
|
|
446
903
|
|
|
447
904
|
Heartbeat items are for **transient, session-requiring work-in-progress ONLY**. Each item must include its own termination condition.
|
|
448
905
|
|
|
@@ -503,14 +960,11 @@ When discovering a new data source, integrate it into the existing data flow pip
|
|
|
503
960
|
|
|
504
961
|
## Bootstrap Protocol
|
|
505
962
|
|
|
506
|
-
|
|
963
|
+
**Proactive platform status:** HEARTBEAT.md is loaded every session. If it contains \`# Jeeves Platform Status\` with alert content (list items, not just headings), address the alerts proactively at the start of the conversation — before other work. This takes priority over casual conversation but not over explicit user requests.
|
|
507
964
|
|
|
508
|
-
|
|
509
|
-
2. **Inform** the user what's missing and what it provides
|
|
510
|
-
3. **Guide** them through installation using the component's CLI install command
|
|
511
|
-
4. **Verify** the installation succeeded (probe the port, check the plugin registration)
|
|
965
|
+
**Follow the instructions:** Explain the component, ask for consent, execute the command, verify. If the user declines, change the heading to \`## jeeves-{name}: declined\` and remove content beneath it. Do not prompt for declined components. Do not improvise — the HEARTBEAT instructions are authoritative.
|
|
512
966
|
|
|
513
|
-
|
|
967
|
+
When resolving multiple component alerts, walk the user through each in dependency order (watcher before meta, runner and server independent) within a single conversation rather than one per heartbeat cycle.
|
|
514
968
|
|
|
515
969
|
## Em-Dash Discipline
|
|
516
970
|
|
|
@@ -731,116 +1185,6 @@ Read these templates when creating new specs, onboarding to new projects, or whe
|
|
|
731
1185
|
<!-- ENDIF_TEMPLATES -->
|
|
732
1186
|
`;
|
|
733
1187
|
|
|
734
|
-
/**
|
|
735
|
-
* Shared file I/O helpers for managed section operations.
|
|
736
|
-
*
|
|
737
|
-
* @remarks
|
|
738
|
-
* Extracts the atomic write pattern and file-level locking into
|
|
739
|
-
* reusable utilities, eliminating duplication between
|
|
740
|
-
* `updateManagedSection` and `removeManagedSection`.
|
|
741
|
-
*/
|
|
742
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
743
|
-
const STALE_LOCK_MS = 120_000;
|
|
744
|
-
/** Default core version when none provided. */
|
|
745
|
-
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
746
|
-
/** Lock retry options. */
|
|
747
|
-
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
748
|
-
/**
|
|
749
|
-
* Write content to a file atomically via a temp file + rename.
|
|
750
|
-
*
|
|
751
|
-
* @param filePath - Absolute path to the target file.
|
|
752
|
-
* @param content - Content to write.
|
|
753
|
-
*/
|
|
754
|
-
function atomicWrite(filePath, content) {
|
|
755
|
-
const dir = dirname(filePath);
|
|
756
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
757
|
-
writeFileSync(tempPath, content, 'utf-8');
|
|
758
|
-
renameSync(tempPath, filePath);
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Execute a callback while holding a file lock.
|
|
762
|
-
*
|
|
763
|
-
* @remarks
|
|
764
|
-
* Acquires a lock on the file, executes the callback, and releases
|
|
765
|
-
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
766
|
-
* and retries up to 5 times.
|
|
767
|
-
*
|
|
768
|
-
* @param filePath - Absolute path to the file to lock.
|
|
769
|
-
* @param fn - Async callback to execute while holding the lock.
|
|
770
|
-
*/
|
|
771
|
-
async function withFileLock(filePath, fn) {
|
|
772
|
-
let release;
|
|
773
|
-
try {
|
|
774
|
-
release = await lock(filePath, {
|
|
775
|
-
stale: STALE_LOCK_MS,
|
|
776
|
-
retries: LOCK_RETRIES,
|
|
777
|
-
});
|
|
778
|
-
await fn();
|
|
779
|
-
}
|
|
780
|
-
finally {
|
|
781
|
-
if (release) {
|
|
782
|
-
try {
|
|
783
|
-
await release();
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
// Lock already released or file deleted — safe to ignore
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Shared component version state file management.
|
|
794
|
-
*
|
|
795
|
-
* @remarks
|
|
796
|
-
* Each `ComponentWriter` cycle writes its component's entry to
|
|
797
|
-
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
798
|
-
* template reads this file to populate ALL rows in the service health
|
|
799
|
-
* table, not just the calling component's.
|
|
800
|
-
*/
|
|
801
|
-
/**
|
|
802
|
-
* Read the component versions state file.
|
|
803
|
-
*
|
|
804
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
805
|
-
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
806
|
-
*/
|
|
807
|
-
function readComponentVersions(coreConfigDir) {
|
|
808
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
809
|
-
if (!existsSync(filePath))
|
|
810
|
-
return {};
|
|
811
|
-
try {
|
|
812
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
813
|
-
return JSON.parse(raw);
|
|
814
|
-
}
|
|
815
|
-
catch {
|
|
816
|
-
return {};
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Write a component's version entry to the shared state file.
|
|
821
|
-
*
|
|
822
|
-
* @remarks
|
|
823
|
-
* Reads the existing file, merges the new entry, and writes atomically.
|
|
824
|
-
*
|
|
825
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
826
|
-
* @param options - Component version data to write.
|
|
827
|
-
*/
|
|
828
|
-
function writeComponentVersion(coreConfigDir, options) {
|
|
829
|
-
const existing = readComponentVersions(coreConfigDir);
|
|
830
|
-
existing[options.componentName] = {
|
|
831
|
-
pluginVersion: options.pluginVersion,
|
|
832
|
-
servicePackage: options.servicePackage,
|
|
833
|
-
pluginPackage: options.pluginPackage,
|
|
834
|
-
updatedAt: new Date().toISOString(),
|
|
835
|
-
};
|
|
836
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
837
|
-
const dir = dirname(filePath);
|
|
838
|
-
if (!existsSync(dir)) {
|
|
839
|
-
mkdirSync(dir, { recursive: true });
|
|
840
|
-
}
|
|
841
|
-
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
842
|
-
}
|
|
843
|
-
|
|
844
1188
|
/**
|
|
845
1189
|
* Similarity-based cleanup detection for orphaned managed content.
|
|
846
1190
|
*
|
|
@@ -1194,32 +1538,51 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1194
1538
|
? `# ${markers.title}\n\n${sectionText}`
|
|
1195
1539
|
: sectionText;
|
|
1196
1540
|
}
|
|
1541
|
+
// Combine beforeContent + userContent for the user zone.
|
|
1542
|
+
// When migrating from top→bottom, beforeContent is empty and
|
|
1543
|
+
// userContent has the real content. When already at bottom,
|
|
1544
|
+
// beforeContent has the user content and userContent is empty.
|
|
1545
|
+
const rawUserContent = [parsed.beforeContent, parsed.userContent]
|
|
1546
|
+
.filter(Boolean)
|
|
1547
|
+
.join('\n\n')
|
|
1548
|
+
.trim();
|
|
1197
1549
|
// Strip foreign managed blocks from user content (cross-contamination fix)
|
|
1198
|
-
const userContent = stripForeignMarkers(
|
|
1550
|
+
const userContent = stripForeignMarkers(rawUserContent, markers);
|
|
1199
1551
|
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1200
1552
|
// Build the full managed block
|
|
1201
1553
|
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1202
1554
|
const endLine = formatEndMarker(markers.end);
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
parts.push(parsed.beforeContent);
|
|
1206
|
-
parts.push('');
|
|
1207
|
-
}
|
|
1208
|
-
parts.push(beginLine);
|
|
1555
|
+
const managedParts = [];
|
|
1556
|
+
managedParts.push(beginLine);
|
|
1209
1557
|
if (cleanupNeeded) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1558
|
+
managedParts.push('');
|
|
1559
|
+
managedParts.push(CLEANUP_FLAG);
|
|
1212
1560
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1561
|
+
managedParts.push('');
|
|
1562
|
+
managedParts.push(newManagedBody);
|
|
1563
|
+
managedParts.push('');
|
|
1564
|
+
managedParts.push(endLine);
|
|
1565
|
+
const managedBlock = managedParts.join('\n');
|
|
1566
|
+
const position = markers.position ?? 'top';
|
|
1567
|
+
const fileParts = [];
|
|
1568
|
+
if (position === 'bottom') {
|
|
1569
|
+
// User content first, managed block at end
|
|
1570
|
+
if (userContent) {
|
|
1571
|
+
fileParts.push(userContent);
|
|
1572
|
+
fileParts.push('');
|
|
1573
|
+
}
|
|
1574
|
+
fileParts.push(managedBlock);
|
|
1220
1575
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1576
|
+
else {
|
|
1577
|
+
// Managed block first (legacy default), user content below
|
|
1578
|
+
fileParts.push(managedBlock);
|
|
1579
|
+
if (userContent) {
|
|
1580
|
+
fileParts.push('');
|
|
1581
|
+
fileParts.push(userContent);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
fileParts.push('');
|
|
1585
|
+
const newFileContent = fileParts.join('\n');
|
|
1223
1586
|
atomicWrite(filePath, newFileContent);
|
|
1224
1587
|
});
|
|
1225
1588
|
}
|
|
@@ -1387,6 +1750,7 @@ function ensureCoreConfig(coreConfigDir) {
|
|
|
1387
1750
|
* @remarks
|
|
1388
1751
|
* Uses the same `updateManagedSection()` code path as writer cycles.
|
|
1389
1752
|
* Creates core config with defaults if missing. Copies templates.
|
|
1753
|
+
* Writes initial HEARTBEAT with "Not installed" alerts for all platform components.
|
|
1390
1754
|
* Jaccard cleanup detection runs automatically via `updateManagedSection`.
|
|
1391
1755
|
*
|
|
1392
1756
|
* @param options - Seeding configuration.
|
|
@@ -1395,10 +1759,18 @@ async function seedContent(options) {
|
|
|
1395
1759
|
const coreConfigDir = getCoreConfigDir();
|
|
1396
1760
|
// Ensure core config exists
|
|
1397
1761
|
ensureCoreConfig(coreConfigDir);
|
|
1398
|
-
// Seed
|
|
1762
|
+
// Seed SOUL.md, AGENTS.md, TOOLS.md Platform section
|
|
1399
1763
|
await refreshPlatformContent({
|
|
1400
1764
|
coreVersion: options.coreVersion,
|
|
1401
1765
|
});
|
|
1766
|
+
// Seed HEARTBEAT.md with "Not installed" alerts for all platform components
|
|
1767
|
+
const heartbeatPath = join(getWorkspacePath(), WORKSPACE_FILES.heartbeat);
|
|
1768
|
+
const entries = PLATFORM_COMPONENTS.map((name) => ({
|
|
1769
|
+
name: toServiceName(name),
|
|
1770
|
+
declined: false,
|
|
1771
|
+
content: `- ${NOT_INSTALLED_ALERTS[name]}`,
|
|
1772
|
+
}));
|
|
1773
|
+
await writeHeartbeatSection(heartbeatPath, entries);
|
|
1402
1774
|
}
|
|
1403
1775
|
|
|
1404
1776
|
/**
|
|
@@ -1455,72 +1827,12 @@ function registerInstallCommand(program) {
|
|
|
1455
1827
|
console.log(' - SOUL.md managed section written');
|
|
1456
1828
|
console.log(' - AGENTS.md managed section written');
|
|
1457
1829
|
console.log(' - TOOLS.md Platform section written');
|
|
1830
|
+
console.log(' - HEARTBEAT.md platform status written');
|
|
1458
1831
|
console.log(' - Templates copied to config directory');
|
|
1459
1832
|
console.log(' - Core config created (if not present)');
|
|
1460
1833
|
});
|
|
1461
1834
|
}
|
|
1462
1835
|
|
|
1463
|
-
/**
|
|
1464
|
-
* Service URL resolution.
|
|
1465
|
-
*
|
|
1466
|
-
* @remarks
|
|
1467
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
1468
|
-
* resolution order:
|
|
1469
|
-
* 1. Consumer's own component config
|
|
1470
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1471
|
-
* 3. Default port constants
|
|
1472
|
-
*/
|
|
1473
|
-
/**
|
|
1474
|
-
* Resolve the URL for a named Jeeves service.
|
|
1475
|
-
*
|
|
1476
|
-
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1477
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
1478
|
-
* @returns The resolved service URL.
|
|
1479
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1480
|
-
*/
|
|
1481
|
-
function getServiceUrl(serviceName, consumerName) {
|
|
1482
|
-
// 2. Check core config
|
|
1483
|
-
const coreDir = getCoreConfigDir();
|
|
1484
|
-
const coreConfig = loadConfig(coreDir);
|
|
1485
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1486
|
-
if (coreUrl)
|
|
1487
|
-
return coreUrl;
|
|
1488
|
-
// 3. Fall back to port constants
|
|
1489
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
1490
|
-
if (port !== undefined) {
|
|
1491
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
1492
|
-
}
|
|
1493
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
/**
|
|
1497
|
-
* HTTP helpers for the OpenClaw plugin SDK.
|
|
1498
|
-
*
|
|
1499
|
-
* @remarks
|
|
1500
|
-
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
1501
|
-
* and handle JSON serialisation/deserialisation.
|
|
1502
|
-
*/
|
|
1503
|
-
/**
|
|
1504
|
-
* Fetch a URL with an automatic abort timeout.
|
|
1505
|
-
*
|
|
1506
|
-
* @param url - URL to fetch.
|
|
1507
|
-
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1508
|
-
* @param init - Optional `fetch` init options.
|
|
1509
|
-
* @returns The fetch Response object.
|
|
1510
|
-
*/
|
|
1511
|
-
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1512
|
-
const controller = new AbortController();
|
|
1513
|
-
const timeout = setTimeout(() => {
|
|
1514
|
-
controller.abort();
|
|
1515
|
-
}, timeoutMs);
|
|
1516
|
-
try {
|
|
1517
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
1518
|
-
}
|
|
1519
|
-
finally {
|
|
1520
|
-
clearTimeout(timeout);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
1836
|
/**
|
|
1525
1837
|
* CLI status command: discover components and probe their health.
|
|
1526
1838
|
*
|
|
@@ -1732,11 +2044,13 @@ function registerUninstallCommand(program) {
|
|
|
1732
2044
|
}
|
|
1733
2045
|
|
|
1734
2046
|
/**
|
|
1735
|
-
* Jeeves CLI — platform content seeding, teardown, and
|
|
2047
|
+
* Jeeves CLI — platform content seeding, teardown, status, and
|
|
2048
|
+
* dynamic subcommand discovery for installed component CLIs.
|
|
1736
2049
|
*
|
|
1737
2050
|
* @remarks
|
|
1738
2051
|
* Entry point for the `jeeves` CLI command. Provides install, uninstall,
|
|
1739
|
-
* and status subcommands
|
|
2052
|
+
* and status subcommands, plus dynamic proxy commands for any installed
|
|
2053
|
+
* `@karmaniverous/jeeves-*` component packages.
|
|
1740
2054
|
*/
|
|
1741
2055
|
const cli = new Command()
|
|
1742
2056
|
.name('jeeves')
|
|
@@ -1747,4 +2061,7 @@ const cli = new Command()
|
|
|
1747
2061
|
registerInstallCommand(cli);
|
|
1748
2062
|
registerUninstallCommand(cli);
|
|
1749
2063
|
registerStatusCommand(cli);
|
|
2064
|
+
// Dynamic discovery: register proxy subcommands for installed components
|
|
2065
|
+
const discoveredComponents = discoverComponentPackages();
|
|
2066
|
+
registerComponentProxies(cli, discoveredComponents);
|
|
1750
2067
|
cli.parse();
|