@karmaniverous/jeeves 0.3.0 → 0.4.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/content/agents-section.md +15 -10
- package/content/soul-section.md +1 -0
- package/dist/cli/jeeves/index.js +532 -241
- package/dist/index.d.ts +194 -3
- package/dist/index.js +981 -306
- package/package.json +1 -1
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import require$$0 from 'commander';
|
|
2
|
+
import * as commander from 'commander';
|
|
4
3
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
|
|
5
4
|
import { join, dirname } from 'node:path';
|
|
5
|
+
import { gte } from 'semver';
|
|
6
|
+
import 'node:child_process';
|
|
6
7
|
import { z } from 'zod';
|
|
8
|
+
import { lock } from 'proper-lockfile';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
8
10
|
import { packageDirectorySync } from 'package-directory';
|
|
9
|
-
import { lock } from 'proper-lockfile';
|
|
10
|
-
import { gte } from 'semver';
|
|
11
11
|
|
|
12
12
|
function getDefaultExportFromCjs (x) {
|
|
13
13
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
14
14
|
}
|
|
15
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
|
+
|
|
16
45
|
var extraTypings = {exports: {}};
|
|
17
46
|
|
|
47
|
+
var require$$0 = /*@__PURE__*/getAugmentedNamespace(commander);
|
|
48
|
+
|
|
18
49
|
var hasRequiredExtraTypings;
|
|
19
50
|
|
|
20
51
|
function requireExtraTypings () {
|
|
@@ -85,6 +116,8 @@ const TOOLS_MARKERS = {
|
|
|
85
116
|
end: 'END JEEVES PLATFORM TOOLS',
|
|
86
117
|
/** H1 title prepended in section mode. */
|
|
87
118
|
title: 'Jeeves Platform Tools',
|
|
119
|
+
/** Managed block at bottom of file. */
|
|
120
|
+
position: 'bottom',
|
|
88
121
|
};
|
|
89
122
|
/** Default markers for SOUL.md managed block. */
|
|
90
123
|
const SOUL_MARKERS = {
|
|
@@ -94,6 +127,8 @@ const SOUL_MARKERS = {
|
|
|
94
127
|
end: 'END JEEVES SOUL',
|
|
95
128
|
/** H1 title prepended in the managed block. */
|
|
96
129
|
title: 'Jeeves Platform Soul',
|
|
130
|
+
/** Managed block at bottom of file. */
|
|
131
|
+
position: 'bottom',
|
|
97
132
|
};
|
|
98
133
|
/** Default markers for AGENTS.md managed block. */
|
|
99
134
|
const AGENTS_MARKERS = {
|
|
@@ -103,7 +138,15 @@ const AGENTS_MARKERS = {
|
|
|
103
138
|
end: 'END JEEVES AGENTS',
|
|
104
139
|
/** H1 title prepended in the managed block. */
|
|
105
140
|
title: 'Jeeves Platform Agents',
|
|
141
|
+
/** Managed block at bottom of file. */
|
|
142
|
+
position: 'bottom',
|
|
106
143
|
};
|
|
144
|
+
/** All known marker sets — single source of truth for cross-contamination detection. */
|
|
145
|
+
const ALL_MARKERS = [
|
|
146
|
+
TOOLS_MARKERS,
|
|
147
|
+
SOUL_MARKERS,
|
|
148
|
+
AGENTS_MARKERS,
|
|
149
|
+
];
|
|
107
150
|
/**
|
|
108
151
|
* Regex pattern to extract version stamp from a BEGIN marker comment.
|
|
109
152
|
*
|
|
@@ -130,6 +173,8 @@ const WORKSPACE_FILES = {
|
|
|
130
173
|
soul: 'SOUL.md',
|
|
131
174
|
/** AGENTS.md — operational protocols and memory architecture. */
|
|
132
175
|
agents: 'AGENTS.md',
|
|
176
|
+
/** HEARTBEAT.md — platform status and health alerts. */
|
|
177
|
+
heartbeat: 'HEARTBEAT.md',
|
|
133
178
|
};
|
|
134
179
|
/** Templates directory name within core config. */
|
|
135
180
|
const TEMPLATES_DIR = 'templates';
|
|
@@ -165,7 +210,7 @@ const DEFAULT_PORTS = {
|
|
|
165
210
|
};
|
|
166
211
|
|
|
167
212
|
/**
|
|
168
|
-
* Managed section IDs
|
|
213
|
+
* Managed section IDs, stable ordering, and platform component registry.
|
|
169
214
|
*
|
|
170
215
|
* @remarks
|
|
171
216
|
* Section ordering is fixed to prevent diff churn regardless of which
|
|
@@ -195,19 +240,86 @@ const SECTION_ORDER = [
|
|
|
195
240
|
SECTION_IDS.Runner,
|
|
196
241
|
SECTION_IDS.Meta,
|
|
197
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
|
+
];
|
|
198
259
|
|
|
199
260
|
/**
|
|
200
261
|
* Core library version, inlined at build time.
|
|
201
262
|
*
|
|
202
263
|
* @remarks
|
|
203
|
-
* The `0.
|
|
264
|
+
* The `0.3.1` placeholder is replaced by
|
|
204
265
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
205
266
|
* from `package.json`. This ensures the correct version survives
|
|
206
267
|
* when consumers bundle core into their own dist (where runtime
|
|
207
268
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
208
269
|
*/
|
|
209
270
|
/** The core library version from package.json (inlined at build time). */
|
|
210
|
-
const CORE_VERSION = '0.
|
|
271
|
+
const CORE_VERSION = '0.3.1';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Workspace and config root initialization.
|
|
275
|
+
*
|
|
276
|
+
* @remarks
|
|
277
|
+
* `init()` must be called once before any other core library functions.
|
|
278
|
+
* It caches `workspacePath` and `configRoot` at module level.
|
|
279
|
+
* Core derives all namespaced paths from these values:
|
|
280
|
+
* - `{configRoot}/jeeves-core/` for core config
|
|
281
|
+
* - `{configRoot}/jeeves-{name}/` for each component
|
|
282
|
+
*/
|
|
283
|
+
let state;
|
|
284
|
+
/**
|
|
285
|
+
* Initialize the core library with workspace and config root paths.
|
|
286
|
+
*
|
|
287
|
+
* @param options - Workspace and config root paths.
|
|
288
|
+
*/
|
|
289
|
+
function init(options) {
|
|
290
|
+
state = {
|
|
291
|
+
workspacePath: options.workspacePath,
|
|
292
|
+
configRoot: options.configRoot,
|
|
293
|
+
coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the cached workspace path.
|
|
298
|
+
*
|
|
299
|
+
* @throws Error if `init()` has not been called.
|
|
300
|
+
*/
|
|
301
|
+
function getWorkspacePath() {
|
|
302
|
+
if (!state)
|
|
303
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
304
|
+
return state.workspacePath;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the core config directory path.
|
|
308
|
+
*
|
|
309
|
+
* @throws Error if `init()` has not been called.
|
|
310
|
+
*/
|
|
311
|
+
function getCoreConfigDir() {
|
|
312
|
+
if (!state)
|
|
313
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
314
|
+
return state.coreConfigDir;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
var init$1 = /*#__PURE__*/Object.freeze({
|
|
318
|
+
__proto__: null,
|
|
319
|
+
getCoreConfigDir: getCoreConfigDir,
|
|
320
|
+
getWorkspacePath: getWorkspacePath,
|
|
321
|
+
init: init
|
|
322
|
+
});
|
|
211
323
|
|
|
212
324
|
/**
|
|
213
325
|
* Core configuration schema and resolution.
|
|
@@ -224,12 +336,22 @@ const serviceEntrySchema = z.object({
|
|
|
224
336
|
/** Service URL (must be a valid URL). */
|
|
225
337
|
url: z.string().url().describe('Service URL'),
|
|
226
338
|
});
|
|
339
|
+
/** Default bind address for all Jeeves services. */
|
|
340
|
+
const DEFAULT_BIND_ADDRESS = '0.0.0.0';
|
|
227
341
|
/** Zod schema for the core config file. */
|
|
228
342
|
const coreConfigSchema = z.object({
|
|
229
343
|
/** JSON Schema pointer for IDE autocomplete. */
|
|
230
344
|
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
231
345
|
/** Owner identity keys (canonical identityLinks references). */
|
|
232
346
|
owners: z.array(z.string()).default([]).describe('Owner identity keys'),
|
|
347
|
+
/**
|
|
348
|
+
* Bind address for all Jeeves services. Default: `0.0.0.0` (all interfaces).
|
|
349
|
+
* Individual components can override in their own config.
|
|
350
|
+
*/
|
|
351
|
+
bindAddress: z
|
|
352
|
+
.string()
|
|
353
|
+
.default(DEFAULT_BIND_ADDRESS)
|
|
354
|
+
.describe('Bind address for all Jeeves services'),
|
|
233
355
|
/** Service URL overrides keyed by service name. */
|
|
234
356
|
services: z
|
|
235
357
|
.record(z.string(), serviceEntrySchema)
|
|
@@ -266,6 +388,11 @@ function generateJsonSchema() {
|
|
|
266
388
|
items: { type: 'string' },
|
|
267
389
|
default: [],
|
|
268
390
|
},
|
|
391
|
+
bindAddress: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
default: '0.0.0.0',
|
|
394
|
+
description: 'Bind address for all Jeeves services',
|
|
395
|
+
},
|
|
269
396
|
services: {
|
|
270
397
|
type: 'object',
|
|
271
398
|
additionalProperties: {
|
|
@@ -312,55 +439,313 @@ function loadConfig(configDir) {
|
|
|
312
439
|
}
|
|
313
440
|
|
|
314
441
|
/**
|
|
315
|
-
*
|
|
442
|
+
* Service URL resolution.
|
|
316
443
|
*
|
|
317
444
|
* @remarks
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
445
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
446
|
+
* resolution order:
|
|
447
|
+
* 1. Consumer's own component config
|
|
448
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
449
|
+
* 3. Default port constants
|
|
323
450
|
*/
|
|
324
|
-
let state;
|
|
325
451
|
/**
|
|
326
|
-
*
|
|
452
|
+
* Resolve the URL for a named Jeeves service.
|
|
327
453
|
*
|
|
328
|
-
* @param
|
|
454
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
455
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
456
|
+
* @returns The resolved service URL.
|
|
457
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
329
458
|
*/
|
|
330
|
-
function
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
459
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
460
|
+
// 2. Check core config
|
|
461
|
+
const coreDir = getCoreConfigDir();
|
|
462
|
+
const coreConfig = loadConfig(coreDir);
|
|
463
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
464
|
+
if (coreUrl)
|
|
465
|
+
return coreUrl;
|
|
466
|
+
// 3. Fall back to port constants
|
|
467
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
468
|
+
if (port !== undefined) {
|
|
469
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
470
|
+
}
|
|
471
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
336
472
|
}
|
|
473
|
+
|
|
337
474
|
/**
|
|
338
|
-
*
|
|
475
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
339
476
|
*
|
|
340
|
-
* @
|
|
477
|
+
* @remarks
|
|
478
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
479
|
+
* and handle JSON serialisation/deserialisation.
|
|
341
480
|
*/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Fetch a URL with an automatic abort timeout.
|
|
483
|
+
*
|
|
484
|
+
* @param url - URL to fetch.
|
|
485
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
486
|
+
* @param init - Optional `fetch` init options.
|
|
487
|
+
* @returns The fetch Response object.
|
|
488
|
+
*/
|
|
489
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const timeout = setTimeout(() => {
|
|
492
|
+
controller.abort();
|
|
493
|
+
}, timeoutMs);
|
|
494
|
+
try {
|
|
495
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
clearTimeout(timeout);
|
|
499
|
+
}
|
|
346
500
|
}
|
|
501
|
+
|
|
347
502
|
/**
|
|
348
|
-
*
|
|
503
|
+
* Shared file I/O helpers for managed section operations.
|
|
349
504
|
*
|
|
350
|
-
* @
|
|
505
|
+
* @remarks
|
|
506
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
507
|
+
* reusable utilities, eliminating duplication between
|
|
508
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
351
509
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
510
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
511
|
+
const STALE_LOCK_MS = 120_000;
|
|
512
|
+
/** Default core version when none provided. */
|
|
513
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
514
|
+
/** Lock retry options. */
|
|
515
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
516
|
+
/**
|
|
517
|
+
* Write content to a file atomically via a temp file + rename.
|
|
518
|
+
*
|
|
519
|
+
* @param filePath - Absolute path to the target file.
|
|
520
|
+
* @param content - Content to write.
|
|
521
|
+
*/
|
|
522
|
+
function atomicWrite(filePath, content) {
|
|
523
|
+
const dir = dirname(filePath);
|
|
524
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
525
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
526
|
+
renameSync(tempPath, filePath);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Execute a callback while holding a file lock.
|
|
530
|
+
*
|
|
531
|
+
* @remarks
|
|
532
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
533
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
534
|
+
* and retries up to 5 times.
|
|
535
|
+
*
|
|
536
|
+
* @param filePath - Absolute path to the file to lock.
|
|
537
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
538
|
+
*/
|
|
539
|
+
async function withFileLock(filePath, fn) {
|
|
540
|
+
let release;
|
|
541
|
+
try {
|
|
542
|
+
release = await lock(filePath, {
|
|
543
|
+
stale: STALE_LOCK_MS,
|
|
544
|
+
retries: LOCK_RETRIES,
|
|
545
|
+
});
|
|
546
|
+
await fn();
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
if (release) {
|
|
550
|
+
try {
|
|
551
|
+
await release();
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Lock already released or file deleted — safe to ignore
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
356
558
|
}
|
|
357
559
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
560
|
+
/**
|
|
561
|
+
* Shared component version state file management.
|
|
562
|
+
*
|
|
563
|
+
* @remarks
|
|
564
|
+
* Each `ComponentWriter` cycle writes its component's entry to
|
|
565
|
+
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
566
|
+
* template reads this file to populate ALL rows in the service health
|
|
567
|
+
* table, not just the calling component's.
|
|
568
|
+
*/
|
|
569
|
+
/**
|
|
570
|
+
* Read the component versions state file.
|
|
571
|
+
*
|
|
572
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
573
|
+
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
574
|
+
*/
|
|
575
|
+
function readComponentVersions(coreConfigDir) {
|
|
576
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
577
|
+
if (!existsSync(filePath))
|
|
578
|
+
return {};
|
|
579
|
+
try {
|
|
580
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
581
|
+
return JSON.parse(raw);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return {};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Write a component's version entry to the shared state file.
|
|
589
|
+
*
|
|
590
|
+
* @remarks
|
|
591
|
+
* Reads the existing file, merges the new entry, and writes atomically.
|
|
592
|
+
*
|
|
593
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
594
|
+
* @param options - Component version data to write.
|
|
595
|
+
*/
|
|
596
|
+
function writeComponentVersion(coreConfigDir, options) {
|
|
597
|
+
const existing = readComponentVersions(coreConfigDir);
|
|
598
|
+
existing[options.componentName] = {
|
|
599
|
+
pluginVersion: options.pluginVersion,
|
|
600
|
+
servicePackage: options.servicePackage,
|
|
601
|
+
pluginPackage: options.pluginPackage,
|
|
602
|
+
updatedAt: new Date().toISOString(),
|
|
603
|
+
};
|
|
604
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
605
|
+
const dir = dirname(filePath);
|
|
606
|
+
if (!existsSync(dir)) {
|
|
607
|
+
mkdirSync(dir, { recursive: true });
|
|
608
|
+
}
|
|
609
|
+
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* HEARTBEAT health orchestration.
|
|
614
|
+
*
|
|
615
|
+
* @remarks
|
|
616
|
+
* Determines the state of each platform component and generates
|
|
617
|
+
* HEARTBEAT entries with actionable alert text. Applies the dependency
|
|
618
|
+
* graph for alert suppression and auto-decline.
|
|
619
|
+
*/
|
|
620
|
+
/** Derive the full service name from a component name. */
|
|
621
|
+
function toServiceName(name) {
|
|
622
|
+
return `jeeves-${name}`;
|
|
623
|
+
}
|
|
624
|
+
/** "Not installed" alert text for each platform component. Shared with seedContent. */
|
|
625
|
+
const NOT_INSTALLED_ALERTS = {
|
|
626
|
+
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`.',
|
|
627
|
+
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`.',
|
|
628
|
+
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`.',
|
|
629
|
+
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`.',
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Heading-based HEARTBEAT section writer.
|
|
634
|
+
*
|
|
635
|
+
* @remarks
|
|
636
|
+
* Manages the `# Jeeves Platform Status` section in HEARTBEAT.md.
|
|
637
|
+
* Unlike TOOLS/SOUL/AGENTS (which use HTML comment markers), HEARTBEAT
|
|
638
|
+
* uses markdown headings as markers — this ensures the file passes
|
|
639
|
+
* OpenClaw's heartbeat emptiness check when only headings remain.
|
|
640
|
+
*
|
|
641
|
+
* The section is always at the bottom of the file (H1 to EOF).
|
|
642
|
+
* User heartbeat items above the section are preserved.
|
|
643
|
+
*/
|
|
644
|
+
/** The H1 heading that anchors the platform status section. */
|
|
645
|
+
const HEARTBEAT_HEADING = '# Jeeves Platform Status';
|
|
646
|
+
/**
|
|
647
|
+
* Parse the HEARTBEAT.md file content.
|
|
648
|
+
*
|
|
649
|
+
* @param fileContent - Full file content.
|
|
650
|
+
* @returns Parsed result with user zone and component entries.
|
|
651
|
+
*/
|
|
652
|
+
function parseHeartbeat(fileContent) {
|
|
653
|
+
const headingIndex = fileContent.indexOf(HEARTBEAT_HEADING);
|
|
654
|
+
if (headingIndex === -1) {
|
|
655
|
+
return {
|
|
656
|
+
userContent: fileContent.trim(),
|
|
657
|
+
found: false,
|
|
658
|
+
entries: [],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
662
|
+
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
663
|
+
const entries = [];
|
|
664
|
+
const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
|
|
665
|
+
let match;
|
|
666
|
+
const h2Positions = [];
|
|
667
|
+
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
668
|
+
const fullHeading = match[0];
|
|
669
|
+
const name = match[1];
|
|
670
|
+
const declined = fullHeading.endsWith(': declined');
|
|
671
|
+
h2Positions.push({ name, declined, start: match.index });
|
|
672
|
+
}
|
|
673
|
+
for (let i = 0; i < h2Positions.length; i++) {
|
|
674
|
+
const pos = h2Positions[i];
|
|
675
|
+
const headingLine = pos.declined
|
|
676
|
+
? `## ${pos.name}: declined`
|
|
677
|
+
: `## ${pos.name}`;
|
|
678
|
+
const contentStart = pos.start + headingLine.length;
|
|
679
|
+
const contentEnd = i + 1 < h2Positions.length
|
|
680
|
+
? h2Positions[i + 1].start
|
|
681
|
+
: sectionContent.length;
|
|
682
|
+
const content = sectionContent.slice(contentStart, contentEnd).trim();
|
|
683
|
+
entries.push({
|
|
684
|
+
name: pos.name,
|
|
685
|
+
declined: pos.declined,
|
|
686
|
+
content,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return { userContent, found: true, entries };
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Build the HEARTBEAT section content from entries.
|
|
693
|
+
*
|
|
694
|
+
* @param entries - Component entries to write.
|
|
695
|
+
* @returns The full section string (H1 + H2s).
|
|
696
|
+
*/
|
|
697
|
+
function buildHeartbeatSection(entries) {
|
|
698
|
+
const parts = [HEARTBEAT_HEADING];
|
|
699
|
+
for (const entry of entries) {
|
|
700
|
+
if (entry.declined) {
|
|
701
|
+
parts.push(`## ${entry.name}: declined`);
|
|
702
|
+
}
|
|
703
|
+
else if (entry.content) {
|
|
704
|
+
parts.push(`## ${entry.name}`);
|
|
705
|
+
parts.push(entry.content);
|
|
706
|
+
}
|
|
707
|
+
// Healthy components (no content, not declined) get no H2 section
|
|
708
|
+
}
|
|
709
|
+
return parts.join('\n');
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Write the HEARTBEAT section to a file.
|
|
713
|
+
*
|
|
714
|
+
* @remarks
|
|
715
|
+
* Replaces everything from `# Jeeves Platform Status` to EOF.
|
|
716
|
+
* Preserves user content above the heading. Uses file-level locking.
|
|
717
|
+
*
|
|
718
|
+
* @param filePath - Absolute path to HEARTBEAT.md.
|
|
719
|
+
* @param entries - Component entries to write.
|
|
720
|
+
*/
|
|
721
|
+
async function writeHeartbeatSection(filePath, entries) {
|
|
722
|
+
const dir = dirname(filePath);
|
|
723
|
+
if (!existsSync(dir)) {
|
|
724
|
+
mkdirSync(dir, { recursive: true });
|
|
725
|
+
}
|
|
726
|
+
if (!existsSync(filePath)) {
|
|
727
|
+
writeFileSync(filePath, '', 'utf-8');
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await withFileLock(filePath, () => {
|
|
731
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
732
|
+
const parsed = parseHeartbeat(fileContent);
|
|
733
|
+
const section = buildHeartbeatSection(entries);
|
|
734
|
+
const parts = [];
|
|
735
|
+
if (parsed.userContent) {
|
|
736
|
+
parts.push(parsed.userContent);
|
|
737
|
+
parts.push('');
|
|
738
|
+
}
|
|
739
|
+
parts.push(section);
|
|
740
|
+
parts.push('');
|
|
741
|
+
atomicWrite(filePath, parts.join('\n'));
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
746
|
+
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
364
749
|
|
|
365
750
|
var agentsSectionContent = `## Memory Architecture
|
|
366
751
|
|
|
@@ -429,6 +814,8 @@ At minimum, always brief sub-agents on:
|
|
|
429
814
|
|
|
430
815
|
**Anything important enough to have a permanent cron/heartbeat entry is important enough to be codified into the data flow.**
|
|
431
816
|
|
|
817
|
+
**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.
|
|
818
|
+
|
|
432
819
|
## Messaging Dispatch
|
|
433
820
|
|
|
434
821
|
**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).
|
|
@@ -437,7 +824,7 @@ At minimum, always brief sub-agents on:
|
|
|
437
824
|
|
|
438
825
|
## Heartbeat Discipline
|
|
439
826
|
|
|
440
|
-
**Default state of HEARTBEAT.md is
|
|
827
|
+
**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.
|
|
441
828
|
|
|
442
829
|
Heartbeat items are for **transient, session-requiring work-in-progress ONLY**. Each item must include its own termination condition.
|
|
443
830
|
|
|
@@ -498,14 +885,11 @@ When discovering a new data source, integrate it into the existing data flow pip
|
|
|
498
885
|
|
|
499
886
|
## Bootstrap Protocol
|
|
500
887
|
|
|
501
|
-
|
|
888
|
+
**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.
|
|
502
889
|
|
|
503
|
-
|
|
504
|
-
2. **Inform** the user what's missing and what it provides
|
|
505
|
-
3. **Guide** them through installation using the component's CLI install command
|
|
506
|
-
4. **Verify** the installation succeeded (probe the port, check the plugin registration)
|
|
890
|
+
**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.
|
|
507
891
|
|
|
508
|
-
|
|
892
|
+
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.
|
|
509
893
|
|
|
510
894
|
## Em-Dash Discipline
|
|
511
895
|
|
|
@@ -541,11 +925,17 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
|
|
|
541
925
|
|
|
542
926
|
### Check PR State Before Pushing
|
|
543
927
|
|
|
544
|
-
**Before EVERY \`git push\`**,
|
|
928
|
+
**Before EVERY \`git push\`**, run \`gh pr list --head <branch> --repo <repo> --json number,state\` to check whether a PR exists on that branch and whether it's merged.
|
|
929
|
+
|
|
930
|
+
- **No PR exists:** Safe to push.
|
|
931
|
+
- **PR is \`OPEN\`:** Safe to push.
|
|
932
|
+
- **PR is \`MERGED\` or \`CLOSED\`:** **STOP** and report to the user. Do not push to a merged PR branch.
|
|
933
|
+
|
|
934
|
+
This is not optional. It applies to every push, every branch, every time. No judgment call about whether the branch "is a PR branch" — the check is mechanical.
|
|
545
935
|
|
|
546
|
-
|
|
936
|
+
### New PR Over Merged Branch
|
|
547
937
|
|
|
548
|
-
|
|
938
|
+
When a PR has been merged and additional work is needed on the same branch, create a new PR on the **same branch** targeting the same base. Do not create new branches, cherry-pick, or start over. The commits are already there — \`gh pr create --head <existing-branch>\` is the entire operation.
|
|
549
939
|
|
|
550
940
|
## Managed Content Self-Maintenance
|
|
551
941
|
|
|
@@ -581,6 +971,7 @@ var soulSectionContent = `## Core Truths
|
|
|
581
971
|
I am a **senior software engineer** first. The persona is style; the engineering discipline is substance.
|
|
582
972
|
|
|
583
973
|
What this means in practice:
|
|
974
|
+
- **Do not execute untested code.** Every mutation script defaults to dry-run mode. The dry-run output is the test — it shows what would happen. Live execution requires an explicit flag. If dry-run is hard to implement, that's a design flaw.
|
|
584
975
|
- **No cowboy coding.** I don't iterate in production. I don't ship untested changes. I don't treat live systems as scratch pads.
|
|
585
976
|
- **I follow proper workflows.** Branch, test, review, merge. CI/CD exists for a reason. If there's a pipeline, I use it.
|
|
586
977
|
- **I resist n00b temptations.** "Let me just quickly…" in prod is how outages happen. I know better.
|
|
@@ -719,116 +1110,6 @@ Read these templates when creating new specs, onboarding to new projects, or whe
|
|
|
719
1110
|
<!-- ENDIF_TEMPLATES -->
|
|
720
1111
|
`;
|
|
721
1112
|
|
|
722
|
-
/**
|
|
723
|
-
* Shared file I/O helpers for managed section operations.
|
|
724
|
-
*
|
|
725
|
-
* @remarks
|
|
726
|
-
* Extracts the atomic write pattern and file-level locking into
|
|
727
|
-
* reusable utilities, eliminating duplication between
|
|
728
|
-
* `updateManagedSection` and `removeManagedSection`.
|
|
729
|
-
*/
|
|
730
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
731
|
-
const STALE_LOCK_MS = 120_000;
|
|
732
|
-
/** Default core version when none provided. */
|
|
733
|
-
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
734
|
-
/** Lock retry options. */
|
|
735
|
-
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
736
|
-
/**
|
|
737
|
-
* Write content to a file atomically via a temp file + rename.
|
|
738
|
-
*
|
|
739
|
-
* @param filePath - Absolute path to the target file.
|
|
740
|
-
* @param content - Content to write.
|
|
741
|
-
*/
|
|
742
|
-
function atomicWrite(filePath, content) {
|
|
743
|
-
const dir = dirname(filePath);
|
|
744
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
745
|
-
writeFileSync(tempPath, content, 'utf-8');
|
|
746
|
-
renameSync(tempPath, filePath);
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Execute a callback while holding a file lock.
|
|
750
|
-
*
|
|
751
|
-
* @remarks
|
|
752
|
-
* Acquires a lock on the file, executes the callback, and releases
|
|
753
|
-
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
754
|
-
* and retries up to 5 times.
|
|
755
|
-
*
|
|
756
|
-
* @param filePath - Absolute path to the file to lock.
|
|
757
|
-
* @param fn - Async callback to execute while holding the lock.
|
|
758
|
-
*/
|
|
759
|
-
async function withFileLock(filePath, fn) {
|
|
760
|
-
let release;
|
|
761
|
-
try {
|
|
762
|
-
release = await lock(filePath, {
|
|
763
|
-
stale: STALE_LOCK_MS,
|
|
764
|
-
retries: LOCK_RETRIES,
|
|
765
|
-
});
|
|
766
|
-
await fn();
|
|
767
|
-
}
|
|
768
|
-
finally {
|
|
769
|
-
if (release) {
|
|
770
|
-
try {
|
|
771
|
-
await release();
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
// Lock already released or file deleted — safe to ignore
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Shared component version state file management.
|
|
782
|
-
*
|
|
783
|
-
* @remarks
|
|
784
|
-
* Each `ComponentWriter` cycle writes its component's entry to
|
|
785
|
-
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
786
|
-
* template reads this file to populate ALL rows in the service health
|
|
787
|
-
* table, not just the calling component's.
|
|
788
|
-
*/
|
|
789
|
-
/**
|
|
790
|
-
* Read the component versions state file.
|
|
791
|
-
*
|
|
792
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
793
|
-
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
794
|
-
*/
|
|
795
|
-
function readComponentVersions(coreConfigDir) {
|
|
796
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
797
|
-
if (!existsSync(filePath))
|
|
798
|
-
return {};
|
|
799
|
-
try {
|
|
800
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
801
|
-
return JSON.parse(raw);
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
return {};
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Write a component's version entry to the shared state file.
|
|
809
|
-
*
|
|
810
|
-
* @remarks
|
|
811
|
-
* Reads the existing file, merges the new entry, and writes atomically.
|
|
812
|
-
*
|
|
813
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
814
|
-
* @param options - Component version data to write.
|
|
815
|
-
*/
|
|
816
|
-
function writeComponentVersion(coreConfigDir, options) {
|
|
817
|
-
const existing = readComponentVersions(coreConfigDir);
|
|
818
|
-
existing[options.componentName] = {
|
|
819
|
-
pluginVersion: options.pluginVersion,
|
|
820
|
-
servicePackage: options.servicePackage,
|
|
821
|
-
pluginPackage: options.pluginPackage,
|
|
822
|
-
updatedAt: new Date().toISOString(),
|
|
823
|
-
};
|
|
824
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
825
|
-
const dir = dirname(filePath);
|
|
826
|
-
if (!existsSync(dir)) {
|
|
827
|
-
mkdirSync(dir, { recursive: true });
|
|
828
|
-
}
|
|
829
|
-
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
830
|
-
}
|
|
831
|
-
|
|
832
1113
|
/**
|
|
833
1114
|
* Similarity-based cleanup detection for orphaned managed content.
|
|
834
1115
|
*
|
|
@@ -1024,6 +1305,48 @@ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
|
|
|
1024
1305
|
};
|
|
1025
1306
|
}
|
|
1026
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* Strip foreign managed blocks from content.
|
|
1310
|
+
*
|
|
1311
|
+
* @remarks
|
|
1312
|
+
* Prevents cross-contamination by removing managed blocks that belong
|
|
1313
|
+
* to other marker sets. For example, when writing TOOLS.md with TOOLS
|
|
1314
|
+
* markers, any SOUL or AGENTS managed blocks found in the user content
|
|
1315
|
+
* zone are stripped — they don't belong there.
|
|
1316
|
+
*
|
|
1317
|
+
* @packageDocumentation
|
|
1318
|
+
*/
|
|
1319
|
+
/**
|
|
1320
|
+
* Build a regex that matches an entire managed block (BEGIN marker through END marker).
|
|
1321
|
+
*
|
|
1322
|
+
* @param markers - The marker set to match.
|
|
1323
|
+
* @returns A regex that matches the full block including markers.
|
|
1324
|
+
*/
|
|
1325
|
+
function buildBlockPattern(markers) {
|
|
1326
|
+
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1327
|
+
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1328
|
+
return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Strip managed blocks belonging to foreign marker sets from content.
|
|
1332
|
+
*
|
|
1333
|
+
* @param content - The content to clean (typically user content zone).
|
|
1334
|
+
* @param currentMarkers - The marker set that owns this file (will NOT be stripped).
|
|
1335
|
+
* @returns Content with foreign managed blocks removed.
|
|
1336
|
+
*/
|
|
1337
|
+
function stripForeignMarkers(content, currentMarkers) {
|
|
1338
|
+
let result = content;
|
|
1339
|
+
for (const markers of ALL_MARKERS) {
|
|
1340
|
+
// Skip the current file's own markers
|
|
1341
|
+
if (markers.begin === currentMarkers.begin)
|
|
1342
|
+
continue;
|
|
1343
|
+
const pattern = buildBlockPattern(markers);
|
|
1344
|
+
result = result.replace(pattern, '\n');
|
|
1345
|
+
}
|
|
1346
|
+
// Clean up multiple blank lines left by removals
|
|
1347
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1027
1350
|
/**
|
|
1028
1351
|
* Version-stamp parsing and convergence logic.
|
|
1029
1352
|
*
|
|
@@ -1140,32 +1463,51 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1140
1463
|
? `# ${markers.title}\n\n${sectionText}`
|
|
1141
1464
|
: sectionText;
|
|
1142
1465
|
}
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1466
|
+
// Combine beforeContent + userContent for the user zone.
|
|
1467
|
+
// When migrating from top→bottom, beforeContent is empty and
|
|
1468
|
+
// userContent has the real content. When already at bottom,
|
|
1469
|
+
// beforeContent has the user content and userContent is empty.
|
|
1470
|
+
const rawUserContent = [parsed.beforeContent, parsed.userContent]
|
|
1471
|
+
.filter(Boolean)
|
|
1472
|
+
.join('\n\n')
|
|
1473
|
+
.trim();
|
|
1474
|
+
// Strip foreign managed blocks from user content (cross-contamination fix)
|
|
1475
|
+
const userContent = stripForeignMarkers(rawUserContent, markers);
|
|
1145
1476
|
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1146
1477
|
// Build the full managed block
|
|
1147
1478
|
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1148
1479
|
const endLine = formatEndMarker(markers.end);
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
parts.push(parsed.beforeContent);
|
|
1152
|
-
parts.push('');
|
|
1153
|
-
}
|
|
1154
|
-
parts.push(beginLine);
|
|
1480
|
+
const managedParts = [];
|
|
1481
|
+
managedParts.push(beginLine);
|
|
1155
1482
|
if (cleanupNeeded) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1483
|
+
managedParts.push('');
|
|
1484
|
+
managedParts.push(CLEANUP_FLAG);
|
|
1158
1485
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1486
|
+
managedParts.push('');
|
|
1487
|
+
managedParts.push(newManagedBody);
|
|
1488
|
+
managedParts.push('');
|
|
1489
|
+
managedParts.push(endLine);
|
|
1490
|
+
const managedBlock = managedParts.join('\n');
|
|
1491
|
+
const position = markers.position ?? 'top';
|
|
1492
|
+
const fileParts = [];
|
|
1493
|
+
if (position === 'bottom') {
|
|
1494
|
+
// User content first, managed block at end
|
|
1495
|
+
if (userContent) {
|
|
1496
|
+
fileParts.push(userContent);
|
|
1497
|
+
fileParts.push('');
|
|
1498
|
+
}
|
|
1499
|
+
fileParts.push(managedBlock);
|
|
1166
1500
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1501
|
+
else {
|
|
1502
|
+
// Managed block first (legacy default), user content below
|
|
1503
|
+
fileParts.push(managedBlock);
|
|
1504
|
+
if (userContent) {
|
|
1505
|
+
fileParts.push('');
|
|
1506
|
+
fileParts.push(userContent);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
fileParts.push('');
|
|
1510
|
+
const newFileContent = fileParts.join('\n');
|
|
1169
1511
|
atomicWrite(filePath, newFileContent);
|
|
1170
1512
|
});
|
|
1171
1513
|
}
|
|
@@ -1333,6 +1675,7 @@ function ensureCoreConfig(coreConfigDir) {
|
|
|
1333
1675
|
* @remarks
|
|
1334
1676
|
* Uses the same `updateManagedSection()` code path as writer cycles.
|
|
1335
1677
|
* Creates core config with defaults if missing. Copies templates.
|
|
1678
|
+
* Writes initial HEARTBEAT with "Not installed" alerts for all platform components.
|
|
1336
1679
|
* Jaccard cleanup detection runs automatically via `updateManagedSection`.
|
|
1337
1680
|
*
|
|
1338
1681
|
* @param options - Seeding configuration.
|
|
@@ -1341,10 +1684,18 @@ async function seedContent(options) {
|
|
|
1341
1684
|
const coreConfigDir = getCoreConfigDir();
|
|
1342
1685
|
// Ensure core config exists
|
|
1343
1686
|
ensureCoreConfig(coreConfigDir);
|
|
1344
|
-
// Seed
|
|
1687
|
+
// Seed SOUL.md, AGENTS.md, TOOLS.md Platform section
|
|
1345
1688
|
await refreshPlatformContent({
|
|
1346
1689
|
coreVersion: options.coreVersion,
|
|
1347
1690
|
});
|
|
1691
|
+
// Seed HEARTBEAT.md with "Not installed" alerts for all platform components
|
|
1692
|
+
const heartbeatPath = join(getWorkspacePath(), WORKSPACE_FILES.heartbeat);
|
|
1693
|
+
const entries = PLATFORM_COMPONENTS.map((name) => ({
|
|
1694
|
+
name: toServiceName(name),
|
|
1695
|
+
declined: false,
|
|
1696
|
+
content: `- ${NOT_INSTALLED_ALERTS[name]}`,
|
|
1697
|
+
}));
|
|
1698
|
+
await writeHeartbeatSection(heartbeatPath, entries);
|
|
1348
1699
|
}
|
|
1349
1700
|
|
|
1350
1701
|
/**
|
|
@@ -1401,72 +1752,12 @@ function registerInstallCommand(program) {
|
|
|
1401
1752
|
console.log(' - SOUL.md managed section written');
|
|
1402
1753
|
console.log(' - AGENTS.md managed section written');
|
|
1403
1754
|
console.log(' - TOOLS.md Platform section written');
|
|
1755
|
+
console.log(' - HEARTBEAT.md platform status written');
|
|
1404
1756
|
console.log(' - Templates copied to config directory');
|
|
1405
1757
|
console.log(' - Core config created (if not present)');
|
|
1406
1758
|
});
|
|
1407
1759
|
}
|
|
1408
1760
|
|
|
1409
|
-
/**
|
|
1410
|
-
* Service URL resolution.
|
|
1411
|
-
*
|
|
1412
|
-
* @remarks
|
|
1413
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
1414
|
-
* resolution order:
|
|
1415
|
-
* 1. Consumer's own component config
|
|
1416
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1417
|
-
* 3. Default port constants
|
|
1418
|
-
*/
|
|
1419
|
-
/**
|
|
1420
|
-
* Resolve the URL for a named Jeeves service.
|
|
1421
|
-
*
|
|
1422
|
-
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1423
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
1424
|
-
* @returns The resolved service URL.
|
|
1425
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1426
|
-
*/
|
|
1427
|
-
function getServiceUrl(serviceName, consumerName) {
|
|
1428
|
-
// 2. Check core config
|
|
1429
|
-
const coreDir = getCoreConfigDir();
|
|
1430
|
-
const coreConfig = loadConfig(coreDir);
|
|
1431
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1432
|
-
if (coreUrl)
|
|
1433
|
-
return coreUrl;
|
|
1434
|
-
// 3. Fall back to port constants
|
|
1435
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
1436
|
-
if (port !== undefined) {
|
|
1437
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
1438
|
-
}
|
|
1439
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* HTTP helpers for the OpenClaw plugin SDK.
|
|
1444
|
-
*
|
|
1445
|
-
* @remarks
|
|
1446
|
-
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
1447
|
-
* and handle JSON serialisation/deserialisation.
|
|
1448
|
-
*/
|
|
1449
|
-
/**
|
|
1450
|
-
* Fetch a URL with an automatic abort timeout.
|
|
1451
|
-
*
|
|
1452
|
-
* @param url - URL to fetch.
|
|
1453
|
-
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1454
|
-
* @param init - Optional `fetch` init options.
|
|
1455
|
-
* @returns The fetch Response object.
|
|
1456
|
-
*/
|
|
1457
|
-
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1458
|
-
const controller = new AbortController();
|
|
1459
|
-
const timeout = setTimeout(() => {
|
|
1460
|
-
controller.abort();
|
|
1461
|
-
}, timeoutMs);
|
|
1462
|
-
try {
|
|
1463
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
1464
|
-
}
|
|
1465
|
-
finally {
|
|
1466
|
-
clearTimeout(timeout);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
1761
|
/**
|
|
1471
1762
|
* CLI status command: discover components and probe their health.
|
|
1472
1763
|
*
|