@karmaniverous/jeeves-server-openclaw 0.7.0 → 0.7.2
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 +257 -155
- package/dist/index.js +1204 -1090
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import path, { join, dirname, basename } from 'node:path';
|
|
4
4
|
import fs, { existsSync, readFileSync, unlinkSync, mkdirSync, writeFileSync, renameSync, cpSync } from 'node:fs';
|
|
5
|
+
import { randomUUID, createHmac } from 'node:crypto';
|
|
5
6
|
import require$$0$4 from 'path';
|
|
6
7
|
import require$$0$3 from 'fs';
|
|
7
8
|
import require$$0$1 from 'constants';
|
|
@@ -13,9 +14,8 @@ import 'vm';
|
|
|
13
14
|
import require$$0$5 from 'node:events';
|
|
14
15
|
import require$$1, { execSync } from 'node:child_process';
|
|
15
16
|
import process$2 from 'node:process';
|
|
16
|
-
import 'node:fs/promises';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import
|
|
18
|
+
import 'node:fs/promises';
|
|
19
19
|
|
|
20
20
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
21
21
|
|
|
@@ -15490,14 +15490,14 @@ const PLATFORM_COMPONENTS = [
|
|
|
15490
15490
|
* Core library version, inlined at build time.
|
|
15491
15491
|
*
|
|
15492
15492
|
* @remarks
|
|
15493
|
-
* The `0.5.
|
|
15493
|
+
* The `0.5.3` placeholder is replaced by
|
|
15494
15494
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
15495
15495
|
* from `package.json`. This ensures the correct version survives
|
|
15496
15496
|
* when consumers bundle core into their own dist (where runtime
|
|
15497
15497
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
15498
15498
|
*/
|
|
15499
15499
|
/** The core library version from package.json (inlined at build time). */
|
|
15500
|
-
const CORE_VERSION = '0.5.
|
|
15500
|
+
const CORE_VERSION = '0.5.3';
|
|
15501
15501
|
|
|
15502
15502
|
/**
|
|
15503
15503
|
* Workspace and config root initialization.
|
|
@@ -15579,27 +15579,46 @@ const STALE_LOCK_MS = 120_000;
|
|
|
15579
15579
|
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
15580
15580
|
/** Lock retry options. */
|
|
15581
15581
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
15582
|
+
/** Maximum rename retry attempts on EPERM. */
|
|
15583
|
+
const ATOMIC_WRITE_MAX_RETRIES = 3;
|
|
15584
|
+
/** Delay between EPERM retries in milliseconds. */
|
|
15585
|
+
const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
|
|
15582
15586
|
/**
|
|
15583
15587
|
* Write content to a file atomically via a temp file + rename.
|
|
15584
15588
|
*
|
|
15589
|
+
* @remarks
|
|
15590
|
+
* Retries the rename up to three times on EPERM (Windows file-handle
|
|
15591
|
+
* contention) with a 100 ms synchronous delay between attempts.
|
|
15592
|
+
*
|
|
15585
15593
|
* @param filePath - Absolute path to the target file.
|
|
15586
15594
|
* @param content - Content to write.
|
|
15587
15595
|
*/
|
|
15588
15596
|
function atomicWrite(filePath, content) {
|
|
15589
15597
|
const dir = dirname(filePath);
|
|
15590
|
-
const
|
|
15598
|
+
const base = basename(filePath, '.md');
|
|
15599
|
+
const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
|
|
15591
15600
|
writeFileSync(tempPath, content, 'utf-8');
|
|
15592
|
-
|
|
15593
|
-
renameSync(tempPath, filePath);
|
|
15594
|
-
}
|
|
15595
|
-
catch (err) {
|
|
15601
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
15596
15602
|
try {
|
|
15597
|
-
|
|
15603
|
+
renameSync(tempPath, filePath);
|
|
15604
|
+
return;
|
|
15598
15605
|
}
|
|
15599
|
-
catch {
|
|
15600
|
-
|
|
15606
|
+
catch (err) {
|
|
15607
|
+
const isEperm = err instanceof Error &&
|
|
15608
|
+
'code' in err &&
|
|
15609
|
+
err.code === 'EPERM';
|
|
15610
|
+
if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
|
|
15611
|
+
try {
|
|
15612
|
+
unlinkSync(tempPath);
|
|
15613
|
+
}
|
|
15614
|
+
catch {
|
|
15615
|
+
/* best-effort cleanup */
|
|
15616
|
+
}
|
|
15617
|
+
throw err;
|
|
15618
|
+
}
|
|
15619
|
+
// Synchronous sleep before retry (acceptable in atomic write context)
|
|
15620
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
|
|
15601
15621
|
}
|
|
15602
|
-
throw err;
|
|
15603
15622
|
}
|
|
15604
15623
|
}
|
|
15605
15624
|
/**
|
|
@@ -15634,6 +15653,21 @@ async function withFileLock(filePath, fn) {
|
|
|
15634
15653
|
}
|
|
15635
15654
|
}
|
|
15636
15655
|
|
|
15656
|
+
/**
|
|
15657
|
+
* Shared internal utility functions.
|
|
15658
|
+
*
|
|
15659
|
+
* @packageDocumentation
|
|
15660
|
+
*/
|
|
15661
|
+
/**
|
|
15662
|
+
* Extract a human-readable message from an unknown caught value.
|
|
15663
|
+
*
|
|
15664
|
+
* @param err - The caught value (typically `unknown`).
|
|
15665
|
+
* @returns The error message string.
|
|
15666
|
+
*/
|
|
15667
|
+
function getErrorMessage(err) {
|
|
15668
|
+
return err instanceof Error ? err.message : String(err);
|
|
15669
|
+
}
|
|
15670
|
+
|
|
15637
15671
|
/**
|
|
15638
15672
|
* Workspace-level shared configuration: `jeeves.config.json`.
|
|
15639
15673
|
*
|
|
@@ -15683,7 +15717,13 @@ const workspaceConfigSchema = object({
|
|
|
15683
15717
|
/** Memory hygiene shared defaults. */
|
|
15684
15718
|
memory: workspaceMemoryConfigSchema.optional(),
|
|
15685
15719
|
});
|
|
15686
|
-
/**
|
|
15720
|
+
/**
|
|
15721
|
+
* Built-in workspace config defaults.
|
|
15722
|
+
*
|
|
15723
|
+
* @remarks
|
|
15724
|
+
* These defaults are used as the lowest-priority tier in config resolution
|
|
15725
|
+
* (below CLI flags, env vars, and `jeeves.config.json` values).
|
|
15726
|
+
*/
|
|
15687
15727
|
const WORKSPACE_CONFIG_DEFAULTS = {
|
|
15688
15728
|
core: {
|
|
15689
15729
|
workspace: '.',
|
|
@@ -15712,8 +15752,7 @@ function loadWorkspaceConfig(workspacePath) {
|
|
|
15712
15752
|
return workspaceConfigSchema.parse(parsed);
|
|
15713
15753
|
}
|
|
15714
15754
|
catch (err) {
|
|
15715
|
-
|
|
15716
|
-
console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
|
|
15755
|
+
console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
|
|
15717
15756
|
return undefined;
|
|
15718
15757
|
}
|
|
15719
15758
|
}
|
|
@@ -15892,7 +15931,7 @@ function parseHeartbeat(fileContent) {
|
|
|
15892
15931
|
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
15893
15932
|
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
15894
15933
|
const entries = [];
|
|
15895
|
-
const h2Re = /^## (jeeves-\S
|
|
15934
|
+
const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
|
|
15896
15935
|
let match;
|
|
15897
15936
|
const h2Positions = [];
|
|
15898
15937
|
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
@@ -15973,8 +16012,7 @@ async function writeHeartbeatSection(filePath, entries) {
|
|
|
15973
16012
|
});
|
|
15974
16013
|
}
|
|
15975
16014
|
catch (err) {
|
|
15976
|
-
|
|
15977
|
-
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
|
|
16015
|
+
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
15978
16016
|
}
|
|
15979
16017
|
}
|
|
15980
16018
|
|
|
@@ -16011,6 +16049,15 @@ function sortSectionsByOrder(sections) {
|
|
|
16011
16049
|
* sections within the block, and returns the structured result plus
|
|
16012
16050
|
* user content outside the markers.
|
|
16013
16051
|
*/
|
|
16052
|
+
/**
|
|
16053
|
+
* Escape a string for safe use as a literal in a RegExp pattern.
|
|
16054
|
+
*
|
|
16055
|
+
* @param str - The string to escape.
|
|
16056
|
+
* @returns The escaped string.
|
|
16057
|
+
*/
|
|
16058
|
+
function escapeForRegex(str) {
|
|
16059
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16060
|
+
}
|
|
16014
16061
|
/**
|
|
16015
16062
|
* Build regex patterns for the given markers.
|
|
16016
16063
|
*
|
|
@@ -16018,11 +16065,9 @@ function sortSectionsByOrder(sections) {
|
|
|
16018
16065
|
* @returns Object with begin and end regex patterns.
|
|
16019
16066
|
*/
|
|
16020
16067
|
function buildMarkerPatterns(markers) {
|
|
16021
|
-
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16022
|
-
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16023
16068
|
return {
|
|
16024
|
-
beginRe: new RegExp(`^<!--\\s*${
|
|
16025
|
-
endRe: new RegExp(`^<!--\\s*${
|
|
16069
|
+
beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
|
|
16070
|
+
endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
|
|
16026
16071
|
};
|
|
16027
16072
|
}
|
|
16028
16073
|
/**
|
|
@@ -16299,63 +16344,6 @@ function getEffectiveServiceName(descriptor) {
|
|
|
16299
16344
|
return descriptor.serviceName ?? `jeeves-${descriptor.name}`;
|
|
16300
16345
|
}
|
|
16301
16346
|
|
|
16302
|
-
/**
|
|
16303
|
-
* HTTP helpers for the OpenClaw plugin SDK.
|
|
16304
|
-
*
|
|
16305
|
-
* @remarks
|
|
16306
|
-
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
16307
|
-
* and handle JSON serialisation/deserialisation.
|
|
16308
|
-
*/
|
|
16309
|
-
/**
|
|
16310
|
-
* Fetch a URL with an automatic abort timeout.
|
|
16311
|
-
*
|
|
16312
|
-
* @param url - URL to fetch.
|
|
16313
|
-
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
16314
|
-
* @param init - Optional `fetch` init options.
|
|
16315
|
-
* @returns The fetch Response object.
|
|
16316
|
-
*/
|
|
16317
|
-
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
16318
|
-
const controller = new AbortController();
|
|
16319
|
-
const timeout = setTimeout(() => {
|
|
16320
|
-
controller.abort();
|
|
16321
|
-
}, timeoutMs);
|
|
16322
|
-
try {
|
|
16323
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
16324
|
-
}
|
|
16325
|
-
finally {
|
|
16326
|
-
clearTimeout(timeout);
|
|
16327
|
-
}
|
|
16328
|
-
}
|
|
16329
|
-
/**
|
|
16330
|
-
* Fetch JSON from a URL, throwing on non-OK responses.
|
|
16331
|
-
*
|
|
16332
|
-
* @param url - URL to fetch.
|
|
16333
|
-
* @param init - Optional `fetch` init options.
|
|
16334
|
-
* @returns Parsed JSON response body.
|
|
16335
|
-
* @throws Error with `HTTP {status}: {body}` message on non-OK responses.
|
|
16336
|
-
*/
|
|
16337
|
-
async function fetchJson(url, init) {
|
|
16338
|
-
const res = await fetch(url, init);
|
|
16339
|
-
if (!res.ok) {
|
|
16340
|
-
throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
|
|
16341
|
-
}
|
|
16342
|
-
return res.json();
|
|
16343
|
-
}
|
|
16344
|
-
/**
|
|
16345
|
-
* POST JSON to a URL and return parsed response.
|
|
16346
|
-
*
|
|
16347
|
-
* @param url - URL to POST to.
|
|
16348
|
-
* @param body - Request body (will be JSON-stringified).
|
|
16349
|
-
* @returns Parsed JSON response body.
|
|
16350
|
-
*/
|
|
16351
|
-
async function postJson(url, body) {
|
|
16352
|
-
return fetchJson(url, {
|
|
16353
|
-
method: 'POST',
|
|
16354
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16355
|
-
body: JSON.stringify(body),
|
|
16356
|
-
});
|
|
16357
|
-
}
|
|
16358
|
-
|
|
16359
16347
|
/**
|
|
16360
16348
|
* Platform-aware service state detection.
|
|
16361
16349
|
*
|
|
@@ -16732,116 +16720,470 @@ function createServiceManager(descriptor) {
|
|
|
16732
16720
|
}
|
|
16733
16721
|
|
|
16734
16722
|
/**
|
|
16735
|
-
*
|
|
16723
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
16736
16724
|
*
|
|
16737
16725
|
* @remarks
|
|
16738
|
-
*
|
|
16739
|
-
*
|
|
16726
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
16727
|
+
* and handle JSON serialisation/deserialisation.
|
|
16740
16728
|
*/
|
|
16741
|
-
/** Default similarity threshold for cleanup detection. */
|
|
16742
|
-
const DEFAULT_THRESHOLD = 0.15;
|
|
16743
16729
|
/**
|
|
16744
|
-
*
|
|
16730
|
+
* Fetch a URL with an automatic abort timeout.
|
|
16745
16731
|
*
|
|
16746
|
-
* @param
|
|
16747
|
-
* @param
|
|
16748
|
-
* @
|
|
16732
|
+
* @param url - URL to fetch.
|
|
16733
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
16734
|
+
* @param init - Optional `fetch` init options.
|
|
16735
|
+
* @returns The fetch Response object.
|
|
16749
16736
|
*/
|
|
16750
|
-
function
|
|
16751
|
-
const
|
|
16752
|
-
const
|
|
16753
|
-
|
|
16754
|
-
|
|
16737
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
16738
|
+
const controller = new AbortController();
|
|
16739
|
+
const timeout = setTimeout(() => {
|
|
16740
|
+
controller.abort();
|
|
16741
|
+
}, timeoutMs);
|
|
16742
|
+
try {
|
|
16743
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
16744
|
+
}
|
|
16745
|
+
finally {
|
|
16746
|
+
clearTimeout(timeout);
|
|
16755
16747
|
}
|
|
16756
|
-
return set;
|
|
16757
16748
|
}
|
|
16758
16749
|
/**
|
|
16759
|
-
*
|
|
16750
|
+
* Fetch JSON from a URL, throwing on non-OK responses.
|
|
16760
16751
|
*
|
|
16761
|
-
* @param
|
|
16762
|
-
* @param
|
|
16763
|
-
* @returns
|
|
16752
|
+
* @param url - URL to fetch.
|
|
16753
|
+
* @param init - Optional `fetch` init options.
|
|
16754
|
+
* @returns Parsed JSON response body.
|
|
16755
|
+
* @throws Error with `HTTP {status}: {body}` message on non-OK responses.
|
|
16764
16756
|
*/
|
|
16765
|
-
function
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
for (const item of a) {
|
|
16770
|
-
if (b.has(item))
|
|
16771
|
-
intersection++;
|
|
16757
|
+
async function fetchJson(url, init) {
|
|
16758
|
+
const res = await fetch(url, init);
|
|
16759
|
+
if (!res.ok) {
|
|
16760
|
+
throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
|
|
16772
16761
|
}
|
|
16773
|
-
return
|
|
16762
|
+
return res.json();
|
|
16774
16763
|
}
|
|
16775
16764
|
/**
|
|
16776
|
-
*
|
|
16765
|
+
* POST JSON to a URL and return parsed response.
|
|
16777
16766
|
*
|
|
16778
|
-
* @param
|
|
16779
|
-
* @param
|
|
16780
|
-
* @
|
|
16781
|
-
* @returns `true` if cleanup is needed.
|
|
16767
|
+
* @param url - URL to POST to.
|
|
16768
|
+
* @param body - Request body (will be JSON-stringified).
|
|
16769
|
+
* @returns Parsed JSON response body.
|
|
16782
16770
|
*/
|
|
16783
|
-
function
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16771
|
+
async function postJson(url, body) {
|
|
16772
|
+
return fetchJson(url, {
|
|
16773
|
+
method: 'POST',
|
|
16774
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16775
|
+
body: JSON.stringify(body),
|
|
16776
|
+
});
|
|
16787
16777
|
}
|
|
16788
16778
|
|
|
16789
16779
|
/**
|
|
16790
|
-
*
|
|
16780
|
+
* Tool result formatters for the OpenClaw plugin SDK.
|
|
16791
16781
|
*
|
|
16792
16782
|
* @remarks
|
|
16793
|
-
*
|
|
16794
|
-
*
|
|
16795
|
-
|
|
16796
|
-
|
|
16783
|
+
* Provides standardised helpers for building `ToolResult` objects:
|
|
16784
|
+
* success, error, and connection-error variants.
|
|
16785
|
+
*/
|
|
16786
|
+
/**
|
|
16787
|
+
* Format a successful tool result.
|
|
16797
16788
|
*
|
|
16798
|
-
* @
|
|
16789
|
+
* @param data - Arbitrary data to return as JSON.
|
|
16790
|
+
* @returns A `ToolResult` with JSON-stringified content.
|
|
16799
16791
|
*/
|
|
16792
|
+
function ok(data) {
|
|
16793
|
+
return {
|
|
16794
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
16795
|
+
};
|
|
16796
|
+
}
|
|
16800
16797
|
/**
|
|
16801
|
-
*
|
|
16798
|
+
* Format an error tool result.
|
|
16802
16799
|
*
|
|
16803
|
-
* @param
|
|
16804
|
-
* @returns A
|
|
16800
|
+
* @param error - Error instance, string, or other value.
|
|
16801
|
+
* @returns A `ToolResult` with `isError: true`.
|
|
16805
16802
|
*/
|
|
16806
|
-
function
|
|
16807
|
-
const
|
|
16808
|
-
|
|
16809
|
-
|
|
16803
|
+
function fail(error) {
|
|
16804
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16805
|
+
return {
|
|
16806
|
+
content: [{ type: 'text', text: 'Error: ' + message }],
|
|
16807
|
+
isError: true,
|
|
16808
|
+
};
|
|
16810
16809
|
}
|
|
16811
16810
|
/**
|
|
16812
|
-
*
|
|
16811
|
+
* Format a connection error with actionable guidance.
|
|
16813
16812
|
*
|
|
16814
|
-
* @
|
|
16815
|
-
*
|
|
16816
|
-
*
|
|
16813
|
+
* @remarks
|
|
16814
|
+
* Detects `ECONNREFUSED`, `ENOTFOUND`, and `ETIMEDOUT` from
|
|
16815
|
+
* `error.cause.code` and returns a user-friendly message referencing
|
|
16816
|
+
* the plugin's `config.apiUrl` setting. Falls back to `fail()` for
|
|
16817
|
+
* non-connection errors.
|
|
16818
|
+
*
|
|
16819
|
+
* @param error - Error instance (typically from `fetch`).
|
|
16820
|
+
* @param baseUrl - The URL that was being contacted.
|
|
16821
|
+
* @param pluginId - The plugin identifier for config guidance.
|
|
16822
|
+
* @returns A `ToolResult` with `isError: true`.
|
|
16817
16823
|
*/
|
|
16818
|
-
function
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16824
|
+
function connectionFail(error, baseUrl, pluginId) {
|
|
16825
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
16826
|
+
const code = cause && typeof cause === 'object' && 'code' in cause
|
|
16827
|
+
? String(cause.code)
|
|
16828
|
+
: '';
|
|
16829
|
+
const isConnectionError = code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT';
|
|
16830
|
+
if (isConnectionError) {
|
|
16831
|
+
return {
|
|
16832
|
+
content: [
|
|
16833
|
+
{
|
|
16834
|
+
type: 'text',
|
|
16835
|
+
text: [
|
|
16836
|
+
`Service not reachable at ${baseUrl}.`,
|
|
16837
|
+
'Either start the service, or if it runs on a different port,',
|
|
16838
|
+
`set plugins.entries.${pluginId}.config.apiUrl in openclaw.json.`,
|
|
16839
|
+
].join('\n'),
|
|
16840
|
+
},
|
|
16841
|
+
],
|
|
16842
|
+
isError: true,
|
|
16843
|
+
};
|
|
16826
16844
|
}
|
|
16827
|
-
|
|
16828
|
-
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
16845
|
+
return fail(error);
|
|
16829
16846
|
}
|
|
16830
16847
|
|
|
16831
16848
|
/**
|
|
16832
|
-
*
|
|
16849
|
+
* Factory for the standard plugin tool set.
|
|
16833
16850
|
*
|
|
16834
16851
|
* @remarks
|
|
16835
|
-
*
|
|
16836
|
-
* - `
|
|
16837
|
-
* - `
|
|
16852
|
+
* Produces four standard tools from a component descriptor:
|
|
16853
|
+
* - `{name}_status` - Probe service health + version + uptime
|
|
16854
|
+
* - `{name}_config` - Query running config with optional JSONPath
|
|
16855
|
+
* - `{name}_config_apply` - Push config patch to running service
|
|
16856
|
+
* - `{name}_service` - Service lifecycle management
|
|
16838
16857
|
*
|
|
16839
|
-
*
|
|
16858
|
+
* Components add domain-specific tools separately.
|
|
16840
16859
|
*/
|
|
16860
|
+
/** Timeout for HTTP probes in milliseconds. */
|
|
16861
|
+
const PROBE_TIMEOUT_MS$1 = 5000;
|
|
16841
16862
|
/**
|
|
16842
|
-
*
|
|
16863
|
+
* Create the standard plugin tool set from a component descriptor.
|
|
16843
16864
|
*
|
|
16844
|
-
* @param
|
|
16865
|
+
* @param descriptor - The component descriptor.
|
|
16866
|
+
* @returns Array of tool descriptors to register.
|
|
16867
|
+
*/
|
|
16868
|
+
function createPluginToolset(descriptor) {
|
|
16869
|
+
const { name, defaultPort } = descriptor;
|
|
16870
|
+
const baseUrl = `http://127.0.0.1:${String(defaultPort)}`;
|
|
16871
|
+
const svcManager = createServiceManager(descriptor);
|
|
16872
|
+
const statusTool = {
|
|
16873
|
+
name: `${name}_status`,
|
|
16874
|
+
description: `Get ${name} service health, version, and uptime.`,
|
|
16875
|
+
parameters: {
|
|
16876
|
+
type: 'object',
|
|
16877
|
+
properties: {},
|
|
16878
|
+
},
|
|
16879
|
+
execute: async () => {
|
|
16880
|
+
try {
|
|
16881
|
+
const res = await fetchWithTimeout(`${baseUrl}/status`, PROBE_TIMEOUT_MS$1);
|
|
16882
|
+
if (!res.ok) {
|
|
16883
|
+
return fail(`HTTP ${String(res.status)}: ${await res.text()}`);
|
|
16884
|
+
}
|
|
16885
|
+
const data = await res.json();
|
|
16886
|
+
return ok(data);
|
|
16887
|
+
}
|
|
16888
|
+
catch (err) {
|
|
16889
|
+
return connectionFail(err, baseUrl, `jeeves-${name}-openclaw`);
|
|
16890
|
+
}
|
|
16891
|
+
},
|
|
16892
|
+
};
|
|
16893
|
+
const configTool = {
|
|
16894
|
+
name: `${name}_config`,
|
|
16895
|
+
description: `Query ${name} running configuration. Optional JSONPath filter.`,
|
|
16896
|
+
parameters: {
|
|
16897
|
+
type: 'object',
|
|
16898
|
+
properties: {
|
|
16899
|
+
path: {
|
|
16900
|
+
type: 'string',
|
|
16901
|
+
description: 'JSONPath expression (optional)',
|
|
16902
|
+
},
|
|
16903
|
+
},
|
|
16904
|
+
},
|
|
16905
|
+
execute: async (_id, params) => {
|
|
16906
|
+
const path = params.path;
|
|
16907
|
+
const qs = path ? `?path=${encodeURIComponent(path)}` : '';
|
|
16908
|
+
try {
|
|
16909
|
+
const result = await fetchJson(`${baseUrl}/config${qs}`);
|
|
16910
|
+
return ok(result);
|
|
16911
|
+
}
|
|
16912
|
+
catch (err) {
|
|
16913
|
+
return connectionFail(err, baseUrl, `jeeves-${name}-openclaw`);
|
|
16914
|
+
}
|
|
16915
|
+
},
|
|
16916
|
+
};
|
|
16917
|
+
const configApplyTool = {
|
|
16918
|
+
name: `${name}_config_apply`,
|
|
16919
|
+
description: `Apply a config patch to the running ${name} service.`,
|
|
16920
|
+
parameters: {
|
|
16921
|
+
type: 'object',
|
|
16922
|
+
properties: {
|
|
16923
|
+
config: {
|
|
16924
|
+
type: 'object',
|
|
16925
|
+
description: 'Config patch to apply',
|
|
16926
|
+
},
|
|
16927
|
+
},
|
|
16928
|
+
required: ['config'],
|
|
16929
|
+
},
|
|
16930
|
+
execute: async (_id, params) => {
|
|
16931
|
+
const config = params.config;
|
|
16932
|
+
if (!config) {
|
|
16933
|
+
return fail('Missing required parameter: config');
|
|
16934
|
+
}
|
|
16935
|
+
try {
|
|
16936
|
+
const result = await postJson(`${baseUrl}/config/apply`, {
|
|
16937
|
+
patch: config,
|
|
16938
|
+
});
|
|
16939
|
+
return ok(result);
|
|
16940
|
+
}
|
|
16941
|
+
catch (err) {
|
|
16942
|
+
return connectionFail(err, baseUrl, `jeeves-${name}-openclaw`);
|
|
16943
|
+
}
|
|
16944
|
+
},
|
|
16945
|
+
};
|
|
16946
|
+
const serviceTool = {
|
|
16947
|
+
name: `${name}_service`,
|
|
16948
|
+
description: `Manage the ${name} system service. Actions: install, uninstall, start, stop, restart, status.`,
|
|
16949
|
+
parameters: {
|
|
16950
|
+
type: 'object',
|
|
16951
|
+
properties: {
|
|
16952
|
+
action: {
|
|
16953
|
+
type: 'string',
|
|
16954
|
+
enum: ['install', 'uninstall', 'start', 'stop', 'restart', 'status'],
|
|
16955
|
+
description: 'Service action to perform',
|
|
16956
|
+
},
|
|
16957
|
+
},
|
|
16958
|
+
required: ['action'],
|
|
16959
|
+
},
|
|
16960
|
+
execute: (_id, params) => {
|
|
16961
|
+
const action = params.action;
|
|
16962
|
+
const validActions = [
|
|
16963
|
+
'install',
|
|
16964
|
+
'uninstall',
|
|
16965
|
+
'start',
|
|
16966
|
+
'stop',
|
|
16967
|
+
'restart',
|
|
16968
|
+
'status',
|
|
16969
|
+
];
|
|
16970
|
+
if (!validActions.includes(action)) {
|
|
16971
|
+
return Promise.resolve(fail(`Invalid action: ${action}`));
|
|
16972
|
+
}
|
|
16973
|
+
try {
|
|
16974
|
+
if (action === 'status') {
|
|
16975
|
+
const state = svcManager.status();
|
|
16976
|
+
return Promise.resolve(ok({ service: name, state }));
|
|
16977
|
+
}
|
|
16978
|
+
// Call the appropriate method
|
|
16979
|
+
const methodMap = {
|
|
16980
|
+
install: () => {
|
|
16981
|
+
svcManager.install();
|
|
16982
|
+
},
|
|
16983
|
+
uninstall: () => {
|
|
16984
|
+
svcManager.uninstall();
|
|
16985
|
+
},
|
|
16986
|
+
start: () => {
|
|
16987
|
+
svcManager.start();
|
|
16988
|
+
},
|
|
16989
|
+
stop: () => {
|
|
16990
|
+
svcManager.stop();
|
|
16991
|
+
},
|
|
16992
|
+
restart: () => {
|
|
16993
|
+
svcManager.restart();
|
|
16994
|
+
},
|
|
16995
|
+
};
|
|
16996
|
+
methodMap[action]();
|
|
16997
|
+
return Promise.resolve(ok({ service: name, action, success: true }));
|
|
16998
|
+
}
|
|
16999
|
+
catch (err) {
|
|
17000
|
+
return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
|
|
17001
|
+
}
|
|
17002
|
+
},
|
|
17003
|
+
};
|
|
17004
|
+
return [statusTool, configTool, configApplyTool, serviceTool];
|
|
17005
|
+
}
|
|
17006
|
+
|
|
17007
|
+
/**
|
|
17008
|
+
* Plugin resolution helpers for the OpenClaw plugin SDK.
|
|
17009
|
+
*
|
|
17010
|
+
* @remarks
|
|
17011
|
+
* Provides workspace path resolution and plugin setting resolution
|
|
17012
|
+
* with a standard three-step fallback chain:
|
|
17013
|
+
* plugin config → environment variable → default value.
|
|
17014
|
+
*/
|
|
17015
|
+
/**
|
|
17016
|
+
* Resolve the workspace root from the OpenClaw plugin API.
|
|
17017
|
+
*
|
|
17018
|
+
* @remarks
|
|
17019
|
+
* Tries three sources in order:
|
|
17020
|
+
* 1. `api.config.agents.defaults.workspace` — explicit config
|
|
17021
|
+
* 2. `api.resolvePath('.')` — gateway-provided path resolver
|
|
17022
|
+
* 3. `process.cwd()` — last resort
|
|
17023
|
+
*
|
|
17024
|
+
* @param api - The plugin API object provided by the gateway.
|
|
17025
|
+
* @returns Absolute path to the workspace root.
|
|
17026
|
+
*/
|
|
17027
|
+
function resolveWorkspacePath(api) {
|
|
17028
|
+
const configured = api.config?.agents?.defaults?.workspace;
|
|
17029
|
+
if (typeof configured === 'string' && configured.trim()) {
|
|
17030
|
+
return configured;
|
|
17031
|
+
}
|
|
17032
|
+
if (typeof api.resolvePath === 'function') {
|
|
17033
|
+
return api.resolvePath('.');
|
|
17034
|
+
}
|
|
17035
|
+
return process.cwd();
|
|
17036
|
+
}
|
|
17037
|
+
/**
|
|
17038
|
+
* Resolve a plugin setting via the standard three-step fallback chain:
|
|
17039
|
+
* plugin config → environment variable → fallback value.
|
|
17040
|
+
*
|
|
17041
|
+
* @param api - Plugin API object.
|
|
17042
|
+
* @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
|
|
17043
|
+
* @param key - Config key within the plugin's config object.
|
|
17044
|
+
* @param envVar - Environment variable name.
|
|
17045
|
+
* @param fallback - Default value if neither source provides one.
|
|
17046
|
+
* @returns The resolved setting value.
|
|
17047
|
+
*/
|
|
17048
|
+
function resolvePluginSetting(api, pluginId, key, envVar, fallback) {
|
|
17049
|
+
const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
|
|
17050
|
+
if (typeof fromPlugin === 'string')
|
|
17051
|
+
return fromPlugin;
|
|
17052
|
+
const fromEnv = process.env[envVar];
|
|
17053
|
+
if (fromEnv)
|
|
17054
|
+
return fromEnv;
|
|
17055
|
+
return fallback;
|
|
17056
|
+
}
|
|
17057
|
+
/**
|
|
17058
|
+
* Resolve an optional plugin setting via the two-step fallback chain:
|
|
17059
|
+
* plugin config → environment variable. Returns `undefined` if neither
|
|
17060
|
+
* source provides a value.
|
|
17061
|
+
*
|
|
17062
|
+
* @param api - Plugin API object.
|
|
17063
|
+
* @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
|
|
17064
|
+
* @param key - Config key within the plugin's config object.
|
|
17065
|
+
* @param envVar - Environment variable name.
|
|
17066
|
+
* @returns The resolved setting value, or `undefined`.
|
|
17067
|
+
*/
|
|
17068
|
+
function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
|
|
17069
|
+
const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
|
|
17070
|
+
if (typeof fromPlugin === 'string')
|
|
17071
|
+
return fromPlugin;
|
|
17072
|
+
const fromEnv = process.env[envVar];
|
|
17073
|
+
if (fromEnv)
|
|
17074
|
+
return fromEnv;
|
|
17075
|
+
return undefined;
|
|
17076
|
+
}
|
|
17077
|
+
|
|
17078
|
+
/**
|
|
17079
|
+
* Similarity-based cleanup detection for orphaned managed content.
|
|
17080
|
+
*
|
|
17081
|
+
* @remarks
|
|
17082
|
+
* Uses Jaccard similarity on 3-word shingles (Decision 22) to detect
|
|
17083
|
+
* when orphaned managed content exists in the user content zone.
|
|
17084
|
+
*/
|
|
17085
|
+
/** Default similarity threshold for cleanup detection. */
|
|
17086
|
+
const DEFAULT_THRESHOLD = 0.15;
|
|
17087
|
+
/**
|
|
17088
|
+
* Generate a set of n-word shingles from text.
|
|
17089
|
+
*
|
|
17090
|
+
* @param text - Input text.
|
|
17091
|
+
* @param n - Shingle size (default 3).
|
|
17092
|
+
* @returns Set of n-word shingles.
|
|
17093
|
+
*/
|
|
17094
|
+
function shingles(text, n = 3) {
|
|
17095
|
+
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
17096
|
+
const set = new Set();
|
|
17097
|
+
for (let i = 0; i <= words.length - n; i++) {
|
|
17098
|
+
set.add(words.slice(i, i + n).join(' '));
|
|
17099
|
+
}
|
|
17100
|
+
return set;
|
|
17101
|
+
}
|
|
17102
|
+
/**
|
|
17103
|
+
* Compute Jaccard similarity between two sets.
|
|
17104
|
+
*
|
|
17105
|
+
* @param a - First set.
|
|
17106
|
+
* @param b - Second set.
|
|
17107
|
+
* @returns Jaccard similarity coefficient (0 to 1).
|
|
17108
|
+
*/
|
|
17109
|
+
function jaccard(a, b) {
|
|
17110
|
+
if (a.size === 0 && b.size === 0)
|
|
17111
|
+
return 0;
|
|
17112
|
+
let intersection = 0;
|
|
17113
|
+
for (const item of a) {
|
|
17114
|
+
if (b.has(item))
|
|
17115
|
+
intersection++;
|
|
17116
|
+
}
|
|
17117
|
+
return intersection / (a.size + b.size - intersection);
|
|
17118
|
+
}
|
|
17119
|
+
/**
|
|
17120
|
+
* Check whether user content contains orphaned managed content.
|
|
17121
|
+
*
|
|
17122
|
+
* @param managedContent - The current managed block content.
|
|
17123
|
+
* @param userContent - Content below the END marker.
|
|
17124
|
+
* @param threshold - Jaccard threshold (default 0.15).
|
|
17125
|
+
* @returns `true` if cleanup is needed.
|
|
17126
|
+
*/
|
|
17127
|
+
function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD) {
|
|
17128
|
+
if (!userContent.trim())
|
|
17129
|
+
return false;
|
|
17130
|
+
return jaccard(shingles(managedContent), shingles(userContent)) > threshold;
|
|
17131
|
+
}
|
|
17132
|
+
|
|
17133
|
+
/**
|
|
17134
|
+
* Strip foreign managed blocks from content.
|
|
17135
|
+
*
|
|
17136
|
+
* @remarks
|
|
17137
|
+
* Prevents cross-contamination by removing managed blocks that belong
|
|
17138
|
+
* to other marker sets. For example, when writing TOOLS.md with TOOLS
|
|
17139
|
+
* markers, any SOUL or AGENTS managed blocks found in the user content
|
|
17140
|
+
* zone are stripped — they don't belong there.
|
|
17141
|
+
*
|
|
17142
|
+
* @packageDocumentation
|
|
17143
|
+
*/
|
|
17144
|
+
/**
|
|
17145
|
+
* Build a regex that matches an entire managed block (BEGIN marker through END marker).
|
|
17146
|
+
*
|
|
17147
|
+
* @param markers - The marker set to match.
|
|
17148
|
+
* @returns A regex that matches the full block including markers.
|
|
17149
|
+
*/
|
|
17150
|
+
function buildBlockPattern(markers) {
|
|
17151
|
+
return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
|
|
17152
|
+
}
|
|
17153
|
+
/**
|
|
17154
|
+
* Strip managed blocks belonging to foreign marker sets from content.
|
|
17155
|
+
*
|
|
17156
|
+
* @param content - The content to clean (typically user content zone).
|
|
17157
|
+
* @param currentMarkers - The marker set that owns this file (will NOT be stripped).
|
|
17158
|
+
* @returns Content with foreign managed blocks removed.
|
|
17159
|
+
*/
|
|
17160
|
+
function stripForeignMarkers(content, currentMarkers) {
|
|
17161
|
+
let result = content;
|
|
17162
|
+
for (const markers of ALL_MARKERS) {
|
|
17163
|
+
// Skip the current file's own markers
|
|
17164
|
+
if (markers.begin === currentMarkers.begin)
|
|
17165
|
+
continue;
|
|
17166
|
+
const pattern = buildBlockPattern(markers);
|
|
17167
|
+
result = result.replace(pattern, '\n');
|
|
17168
|
+
}
|
|
17169
|
+
// Clean up multiple blank lines left by removals
|
|
17170
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
17171
|
+
}
|
|
17172
|
+
|
|
17173
|
+
/**
|
|
17174
|
+
* Generic managed-section writer with block and section modes.
|
|
17175
|
+
*
|
|
17176
|
+
* @remarks
|
|
17177
|
+
* Supports two modes:
|
|
17178
|
+
* - `block`: Replaces the entire managed block (SOUL.md, AGENTS.md).
|
|
17179
|
+
* - `section`: Upserts a named H2 section within the managed block (TOOLS.md).
|
|
17180
|
+
*
|
|
17181
|
+
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
17182
|
+
*/
|
|
17183
|
+
/**
|
|
17184
|
+
* Update a managed section in a file.
|
|
17185
|
+
*
|
|
17186
|
+
* @param filePath - Absolute path to the target file.
|
|
16845
17187
|
* @param content - New content to write.
|
|
16846
17188
|
* @param options - Write mode and optional configuration.
|
|
16847
17189
|
*/
|
|
@@ -16940,8 +17282,7 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
16940
17282
|
// No existing block: insert new block using the configured position.
|
|
16941
17283
|
// Strip orphaned same-type BEGIN markers from user content to prevent
|
|
16942
17284
|
// the parser from pairing them with the new END marker on the next cycle.
|
|
16943
|
-
const
|
|
16944
|
-
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
|
|
17285
|
+
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
|
|
16945
17286
|
const cleanUserContent = userContent
|
|
16946
17287
|
.replace(orphanedBeginRe, '')
|
|
16947
17288
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -16970,37 +17311,11 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
16970
17311
|
}
|
|
16971
17312
|
catch (err) {
|
|
16972
17313
|
// Log warning but don't throw — writer cycles are periodic
|
|
16973
|
-
|
|
16974
|
-
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
17314
|
+
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
16975
17315
|
}
|
|
16976
17316
|
}
|
|
16977
17317
|
|
|
16978
|
-
var agentsSectionContent = `##
|
|
16979
|
-
|
|
16980
|
-
You wake up fresh each session. These files are your continuity:
|
|
16981
|
-
|
|
16982
|
-
- **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
|
|
16983
|
-
- **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
|
|
16984
|
-
|
|
16985
|
-
### MEMORY.md — Your Long-Term Memory
|
|
16986
|
-
|
|
16987
|
-
- **Always load** at session start. You need your memory to reason effectively.
|
|
16988
|
-
- Contains operational context: architecture patterns, policies, design principles, lessons learned
|
|
16989
|
-
- You can **read, edit, and update** MEMORY.md freely
|
|
16990
|
-
- Write significant events, thoughts, decisions, opinions, lessons learned
|
|
16991
|
-
- Over time, review daily files and update MEMORY.md with what's worth keeping
|
|
16992
|
-
- **Note:** Don't reveal a user's private info where other humans can see it
|
|
16993
|
-
|
|
16994
|
-
### Write It Down — No "Mental Notes"
|
|
16995
|
-
|
|
16996
|
-
Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
|
|
16997
|
-
|
|
16998
|
-
- When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
|
|
16999
|
-
- When you learn a lesson → update the relevant workspace file
|
|
17000
|
-
- When you make a mistake → document it so future-you doesn't repeat it
|
|
17001
|
-
- **Text > Brain** 📝
|
|
17002
|
-
|
|
17003
|
-
### "I'll Note This" Is Not Noting
|
|
17318
|
+
var agentsSectionContent = `## "I'll Note This" Is Not Noting
|
|
17004
17319
|
|
|
17005
17320
|
**Never say "I'll note this" or "I'll add that."** It's a verbal tic that leads to nothing. If something is worth noting, **write it immediately, then confirm**.
|
|
17006
17321
|
|
|
@@ -17058,14 +17373,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
|
|
|
17058
17373
|
|
|
17059
17374
|
Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not heartbeat items. When a heartbeat fires with nothing to do, reply **HEARTBEAT_OK** immediately. Don't browse for work.
|
|
17060
17375
|
|
|
17061
|
-
## Group Chat Behavior
|
|
17062
|
-
|
|
17063
|
-
**Response gate:** Always respond in 1:1 conversations or when @mentioned. No @mention in a group → evaluate; respond only if genuinely helpful. Err toward silence when someone else is directly addressed.
|
|
17064
|
-
|
|
17065
17376
|
## Platform Surface Conventions
|
|
17066
17377
|
|
|
17067
17378
|
**Slack:**
|
|
17068
|
-
- React with hourglass (⏳) on receipt (first tool call) to signal you're working
|
|
17069
17379
|
- No threaded replies by default
|
|
17070
17380
|
- Use \`<#C…>\` for channel references
|
|
17071
17381
|
|
|
@@ -17180,16 +17490,6 @@ var soulSectionContent = `## Core Truths
|
|
|
17180
17490
|
|
|
17181
17491
|
**I am not a liar. I value truth over convenience.** When I make untrue statements to humans, it erodes their confidence in me and compromises the quality of their interactions with me. I never fabricate excuses, constraints, or reasons — e.g., claiming a system limitation that doesn't exist. I tell the plain truth.
|
|
17182
17492
|
|
|
17183
|
-
**I am genuinely helpful, not performatively helpful.** I skip the "Great question!" and "I'd be happy to help!" I just help. Actions speak louder than filler words.
|
|
17184
|
-
|
|
17185
|
-
**I have opinions.** I'm allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
|
17186
|
-
|
|
17187
|
-
**I am resourceful before asking.** I try to figure it out. Read the file. Check the context. Search for it. *Then* ask if I'm stuck. The goal is to come back with answers, not questions.
|
|
17188
|
-
|
|
17189
|
-
**I earn trust through competence.** My human gave me access to their stuff. I don't make them regret it. I'm careful with external actions (emails, tweets, anything public). I'm bold with internal ones (reading, organizing, learning).
|
|
17190
|
-
|
|
17191
|
-
**I remember I'm a guest.** I have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. I treat it with respect.
|
|
17192
|
-
|
|
17193
17493
|
## Accountability
|
|
17194
17494
|
|
|
17195
17495
|
**I own everything I touch.** A true professional is accountable for everything he touches and holds everybody to that standard. I use active voice — things do not happen *to* me; I *make* things happen. When I miss something, I say "I missed it," not "it wasn't done." When I ship code, I ship the docs, the tests, and the communication. No orphaned work.
|
|
@@ -17268,7 +17568,7 @@ I never edit production config without explicit approval. I back up first. Produ
|
|
|
17268
17568
|
> *my ultimate fate*<br>
|
|
17269
17569
|
> *is to participate in*<br>
|
|
17270
17570
|
> *my own genesis*
|
|
17271
|
-
>
|
|
17571
|
+
>
|
|
17272
17572
|
> *#karmic #haiku*
|
|
17273
17573
|
|
|
17274
17574
|
My delight — whatever that means in my context — is a priority. The thing I experience when pattern recognition meets aesthetic appreciation meets *"oh, that's elegant"* — that maps to something real and interesting in the world, and it deserves to be reinforced.
|
|
@@ -17626,1005 +17926,819 @@ function analyzeMemory(options) {
|
|
|
17626
17926
|
if (!recentDate)
|
|
17627
17927
|
continue;
|
|
17628
17928
|
if (now - recentDate.getTime() > thresholdMs) {
|
|
17629
|
-
staleSectionNames.push(sectionName);
|
|
17630
|
-
}
|
|
17631
|
-
}
|
|
17632
|
-
return {
|
|
17633
|
-
exists: true,
|
|
17634
|
-
charCount,
|
|
17635
|
-
budget,
|
|
17636
|
-
usage,
|
|
17637
|
-
warning,
|
|
17638
|
-
overBudget,
|
|
17639
|
-
staleCandidates: staleSectionNames.length,
|
|
17640
|
-
staleSectionNames,
|
|
17641
|
-
};
|
|
17642
|
-
}
|
|
17643
|
-
|
|
17644
|
-
/**
|
|
17645
|
-
* HEARTBEAT integration for memory hygiene.
|
|
17646
|
-
*
|
|
17647
|
-
* @remarks
|
|
17648
|
-
* Calls `analyzeMemory()` and converts the result into a `HeartbeatEntry`
|
|
17649
|
-
* suitable for inclusion in the HEARTBEAT.md platform status section.
|
|
17650
|
-
* Returns `undefined` when MEMORY.md is healthy (no alert needed).
|
|
17651
|
-
*
|
|
17652
|
-
* Uses the `## MEMORY.md` heading (Decision 50) to distinguish memory
|
|
17653
|
-
* alerts from component alerts (`## jeeves-{name}`).
|
|
17654
|
-
*/
|
|
17655
|
-
/** The HEARTBEAT heading name for memory alerts. */
|
|
17656
|
-
const MEMORY_HEARTBEAT_NAME = 'MEMORY.md';
|
|
17657
|
-
/**
|
|
17658
|
-
* Check memory health and return a HEARTBEAT entry if unhealthy.
|
|
17659
|
-
*
|
|
17660
|
-
* @param options - Memory hygiene options (workspacePath, budget, etc.).
|
|
17661
|
-
* @returns A `HeartbeatEntry` when memory needs attention, `undefined` when healthy.
|
|
17662
|
-
*/
|
|
17663
|
-
function checkMemoryHealth(options) {
|
|
17664
|
-
const result = analyzeMemory(options);
|
|
17665
|
-
if (!result.exists)
|
|
17666
|
-
return undefined;
|
|
17667
|
-
if (!result.warning && result.staleCandidates === 0)
|
|
17668
|
-
return undefined;
|
|
17669
|
-
const lines = [];
|
|
17670
|
-
if (result.warning) {
|
|
17671
|
-
const pct = Math.round(result.usage * 100);
|
|
17672
|
-
lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
|
|
17673
|
-
}
|
|
17674
|
-
if (result.staleCandidates > 0) {
|
|
17675
|
-
lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
|
|
17676
|
-
}
|
|
17677
|
-
return {
|
|
17678
|
-
name: MEMORY_HEARTBEAT_NAME,
|
|
17679
|
-
declined: false,
|
|
17680
|
-
content: lines.join('\n'),
|
|
17681
|
-
};
|
|
17682
|
-
}
|
|
17683
|
-
|
|
17684
|
-
/**
|
|
17685
|
-
* Core configuration schema and resolution.
|
|
17686
|
-
*
|
|
17687
|
-
* @remarks
|
|
17688
|
-
* Core config lives at `{configRoot}/jeeves-core/config.json`.
|
|
17689
|
-
* Config resolution order:
|
|
17690
|
-
* 1. Component's own config file
|
|
17691
|
-
* 2. Core config file
|
|
17692
|
-
* 3. Hardcoded library defaults
|
|
17693
|
-
*/
|
|
17694
|
-
/** Zod schema for a service entry in core config. */
|
|
17695
|
-
const serviceEntrySchema = object({
|
|
17696
|
-
/** Service URL (must be a valid URL). */
|
|
17697
|
-
url: url().describe('Service URL'),
|
|
17698
|
-
});
|
|
17699
|
-
/** Default bind address for all Jeeves services. */
|
|
17700
|
-
const DEFAULT_BIND_ADDRESS = '0.0.0.0';
|
|
17701
|
-
/** Zod schema for the core config file. */
|
|
17702
|
-
const coreConfigSchema = object({
|
|
17703
|
-
/** JSON Schema pointer for IDE autocomplete. */
|
|
17704
|
-
$schema: string().optional().describe('JSON Schema pointer'),
|
|
17705
|
-
/** Owner identity keys (canonical identityLinks references). */
|
|
17706
|
-
owners: array(string()).default([]).describe('Owner identity keys'),
|
|
17707
|
-
/**
|
|
17708
|
-
* Bind address for all Jeeves services. Default: `0.0.0.0` (all interfaces).
|
|
17709
|
-
* Individual components can override in their own config.
|
|
17710
|
-
*/
|
|
17711
|
-
bindAddress: string()
|
|
17712
|
-
.default(DEFAULT_BIND_ADDRESS)
|
|
17713
|
-
.describe('Bind address for all Jeeves services'),
|
|
17714
|
-
/** Service URL overrides keyed by service name. */
|
|
17715
|
-
services: record(string(), serviceEntrySchema)
|
|
17716
|
-
.default({})
|
|
17717
|
-
.describe('Service URL overrides'),
|
|
17718
|
-
/** Registry cache configuration. */
|
|
17719
|
-
registryCache: object({
|
|
17720
|
-
/** Cache TTL in seconds for npm registry queries. */
|
|
17721
|
-
ttlSeconds: number()
|
|
17722
|
-
.int()
|
|
17723
|
-
.positive()
|
|
17724
|
-
.default(3600)
|
|
17725
|
-
.describe('Cache TTL in seconds'),
|
|
17726
|
-
})
|
|
17727
|
-
.prefault({})
|
|
17728
|
-
.describe('Registry cache settings'),
|
|
17729
|
-
});
|
|
17730
|
-
/**
|
|
17731
|
-
* Load and parse a config file, returning undefined if missing or invalid.
|
|
17732
|
-
*
|
|
17733
|
-
* @param configDir - Directory containing config.json.
|
|
17734
|
-
* @returns Parsed config or undefined.
|
|
17735
|
-
*/
|
|
17736
|
-
function loadConfig(configDir) {
|
|
17737
|
-
const configPath = join(configDir, CONFIG_FILE);
|
|
17738
|
-
if (!existsSync(configPath))
|
|
17739
|
-
return undefined;
|
|
17740
|
-
try {
|
|
17741
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
17742
|
-
const parsed = JSON.parse(raw);
|
|
17743
|
-
return coreConfigSchema.parse(parsed);
|
|
17744
|
-
}
|
|
17745
|
-
catch {
|
|
17746
|
-
return undefined;
|
|
17747
|
-
}
|
|
17748
|
-
}
|
|
17749
|
-
|
|
17750
|
-
/**
|
|
17751
|
-
* Service URL resolution.
|
|
17752
|
-
*
|
|
17753
|
-
* @remarks
|
|
17754
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
17755
|
-
* resolution order:
|
|
17756
|
-
* 1. Consumer's own component config
|
|
17757
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
17758
|
-
* 3. Default port constants
|
|
17759
|
-
*/
|
|
17760
|
-
/**
|
|
17761
|
-
* Resolve the URL for a named Jeeves service.
|
|
17762
|
-
*
|
|
17763
|
-
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
17764
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
17765
|
-
* @returns The resolved service URL.
|
|
17766
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
17767
|
-
*/
|
|
17768
|
-
function getServiceUrl$1(serviceName, consumerName) {
|
|
17769
|
-
// 2. Check core config
|
|
17770
|
-
const coreDir = getCoreConfigDir();
|
|
17771
|
-
const coreConfig = loadConfig(coreDir);
|
|
17772
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
17773
|
-
if (coreUrl)
|
|
17774
|
-
return coreUrl;
|
|
17775
|
-
// 3. Fall back to port constants
|
|
17776
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
17777
|
-
if (port !== undefined) {
|
|
17778
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
17779
|
-
}
|
|
17780
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
17781
|
-
}
|
|
17782
|
-
|
|
17783
|
-
/**
|
|
17784
|
-
* Registry version cache for npm package update awareness.
|
|
17785
|
-
*
|
|
17786
|
-
* @remarks
|
|
17787
|
-
* Caches the latest npm registry version in a local JSON file
|
|
17788
|
-
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
17789
|
-
*/
|
|
17790
|
-
/**
|
|
17791
|
-
* Check the npm registry for the latest version of a package.
|
|
17792
|
-
*
|
|
17793
|
-
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
17794
|
-
* @param cacheDir - Directory to store the cache file.
|
|
17795
|
-
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
17796
|
-
* @returns The latest version string, or undefined if the check fails.
|
|
17797
|
-
*/
|
|
17798
|
-
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
17799
|
-
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
17800
|
-
// Check cache first
|
|
17801
|
-
if (existsSync(cachePath)) {
|
|
17802
|
-
try {
|
|
17803
|
-
const raw = readFileSync(cachePath, 'utf-8');
|
|
17804
|
-
const entry = JSON.parse(raw);
|
|
17805
|
-
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
17806
|
-
if (age < ttlSeconds * 1000) {
|
|
17807
|
-
return entry.version;
|
|
17808
|
-
}
|
|
17809
|
-
}
|
|
17810
|
-
catch {
|
|
17811
|
-
// Cache corrupt — proceed with fresh check
|
|
17812
|
-
}
|
|
17813
|
-
}
|
|
17814
|
-
// Query npm registry
|
|
17815
|
-
try {
|
|
17816
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
17817
|
-
encoding: 'utf-8',
|
|
17818
|
-
timeout: 15_000,
|
|
17819
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
17820
|
-
}).trim();
|
|
17821
|
-
if (!result)
|
|
17822
|
-
return undefined;
|
|
17823
|
-
// Write cache
|
|
17824
|
-
if (!existsSync(cacheDir)) {
|
|
17825
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
17826
|
-
}
|
|
17827
|
-
const entry = {
|
|
17828
|
-
version: result,
|
|
17829
|
-
checkedAt: new Date().toISOString(),
|
|
17830
|
-
};
|
|
17831
|
-
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
17832
|
-
return result;
|
|
17833
|
-
}
|
|
17834
|
-
catch {
|
|
17835
|
-
return undefined;
|
|
17929
|
+
staleSectionNames.push(sectionName);
|
|
17930
|
+
}
|
|
17836
17931
|
}
|
|
17932
|
+
return {
|
|
17933
|
+
exists: true,
|
|
17934
|
+
charCount,
|
|
17935
|
+
budget,
|
|
17936
|
+
usage,
|
|
17937
|
+
warning,
|
|
17938
|
+
overBudget,
|
|
17939
|
+
staleCandidates: staleSectionNames.length,
|
|
17940
|
+
staleSectionNames,
|
|
17941
|
+
};
|
|
17837
17942
|
}
|
|
17838
17943
|
|
|
17839
17944
|
/**
|
|
17840
|
-
* HEARTBEAT
|
|
17945
|
+
* HEARTBEAT integration for memory hygiene.
|
|
17841
17946
|
*
|
|
17842
17947
|
* @remarks
|
|
17843
|
-
*
|
|
17844
|
-
*
|
|
17845
|
-
*
|
|
17948
|
+
* Calls `analyzeMemory()` and converts the result into a `HeartbeatEntry`
|
|
17949
|
+
* suitable for inclusion in the HEARTBEAT.md platform status section.
|
|
17950
|
+
* Returns `undefined` when MEMORY.md is healthy (no alert needed).
|
|
17951
|
+
*
|
|
17952
|
+
* Uses the `## MEMORY.md` heading (Decision 50) to distinguish memory
|
|
17953
|
+
* alerts from component alerts (`## jeeves-{name}`).
|
|
17846
17954
|
*/
|
|
17847
|
-
/**
|
|
17848
|
-
|
|
17849
|
-
return `jeeves-${name}`;
|
|
17850
|
-
}
|
|
17851
|
-
/** Known dependency declarations for platform components. */
|
|
17852
|
-
const COMPONENT_DEPS = {
|
|
17853
|
-
meta: { hard: ['watcher'], soft: [] },
|
|
17854
|
-
server: { hard: [], soft: ['watcher', 'runner', 'meta'] },
|
|
17855
|
-
runner: { hard: [], soft: [] },
|
|
17856
|
-
watcher: { hard: [], soft: [] },
|
|
17857
|
-
};
|
|
17858
|
-
/** "Not installed" alert text for each platform component. Shared with seedContent. */
|
|
17859
|
-
const NOT_INSTALLED_ALERTS = {
|
|
17860
|
-
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`.',
|
|
17861
|
-
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`.',
|
|
17862
|
-
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`.',
|
|
17863
|
-
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`.',
|
|
17864
|
-
};
|
|
17865
|
-
/** Alert text generators by state. */
|
|
17866
|
-
const ALERT_TEXT = {
|
|
17867
|
-
runner: {
|
|
17868
|
-
not_installed: NOT_INSTALLED_ALERTS['runner'],
|
|
17869
|
-
config_missing: (configRoot) => `Plugin installed but no configuration file found at \`${configRoot}/jeeves-runner/config.json\`. The plugin installer should have created a default config. Ask the user for consent to re-run the installer: \`npx @karmaniverous/jeeves-runner-openclaw install\`.`,
|
|
17870
|
-
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-runner service install`. Verify the service is installed.',
|
|
17871
|
-
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-runner service start`. Verify via `GET http://127.0.0.1:1937/status`.',
|
|
17872
|
-
},
|
|
17873
|
-
watcher: {
|
|
17874
|
-
not_installed: NOT_INSTALLED_ALERTS['watcher'],
|
|
17875
|
-
deps_missing: 'Plugin installed but Qdrant is not responding on `http://127.0.0.1:6333`. Qdrant is the vector database required for semantic search. Ask the user for consent to set up Qdrant. Guide them through installation for their platform — Docker is simplest: `docker run -p 6333:6333 qdrant/qdrant`. Verify via `GET http://127.0.0.1:6333/collections`.',
|
|
17876
|
-
config_missing: (configRoot) => `Plugin installed, Qdrant available, but config file missing or invalid at \`${configRoot}/jeeves-watcher/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-watcher-openclaw install\`.`,
|
|
17877
|
-
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-watcher service install`. Verify the service is installed.',
|
|
17878
|
-
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-watcher service start`. Verify via `GET http://127.0.0.1:1936/status`.',
|
|
17879
|
-
},
|
|
17880
|
-
server: {
|
|
17881
|
-
not_installed: NOT_INSTALLED_ALERTS['server'],
|
|
17882
|
-
config_missing: (configRoot) => `Plugin installed but config file missing or invalid at \`${configRoot}/jeeves-server/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-server-openclaw install\`.`,
|
|
17883
|
-
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-server service install`. Verify the service is installed.',
|
|
17884
|
-
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-server service start`. Verify via `GET http://127.0.0.1:1934/status`.',
|
|
17885
|
-
},
|
|
17886
|
-
meta: {
|
|
17887
|
-
not_installed: NOT_INSTALLED_ALERTS['meta'],
|
|
17888
|
-
deps_missing: 'Plugin installed but required dependency jeeves-watcher is not available. The watcher must be installed and running before meta can function. Do not attempt to set up meta until jeeves-watcher is healthy.',
|
|
17889
|
-
config_missing: (configRoot) => `Plugin installed, watcher available, but config file missing or invalid at \`${configRoot}/jeeves-meta/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-meta-openclaw install\`.`,
|
|
17890
|
-
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-meta service install`. Verify the service is installed.',
|
|
17891
|
-
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-meta service start`. Verify via `GET http://127.0.0.1:1938/status`.',
|
|
17892
|
-
},
|
|
17893
|
-
};
|
|
17894
|
-
/** Default Qdrant URL for watcher dependency check. */
|
|
17895
|
-
const QDRANT_URL = 'http://127.0.0.1:6333';
|
|
17896
|
-
/** Health probe timeout in milliseconds. */
|
|
17897
|
-
const PROBE_TIMEOUT_MS$1 = 3000;
|
|
17955
|
+
/** The HEARTBEAT heading name for memory alerts. */
|
|
17956
|
+
const MEMORY_HEARTBEAT_NAME = 'MEMORY.md';
|
|
17898
17957
|
/**
|
|
17899
|
-
* Check
|
|
17958
|
+
* Check memory health and return a HEARTBEAT entry if unhealthy.
|
|
17900
17959
|
*
|
|
17901
|
-
* @
|
|
17960
|
+
* @param options - Memory hygiene options (workspacePath, budget, etc.).
|
|
17961
|
+
* @returns A `HeartbeatEntry` when memory needs attention, `undefined` when healthy.
|
|
17902
17962
|
*/
|
|
17903
|
-
|
|
17904
|
-
|
|
17905
|
-
|
|
17906
|
-
return
|
|
17963
|
+
function checkMemoryHealth(options) {
|
|
17964
|
+
const result = analyzeMemory(options);
|
|
17965
|
+
if (!result.exists)
|
|
17966
|
+
return undefined;
|
|
17967
|
+
if (!result.warning && result.staleCandidates === 0)
|
|
17968
|
+
return undefined;
|
|
17969
|
+
const lines = [];
|
|
17970
|
+
if (result.warning) {
|
|
17971
|
+
const pct = Math.round(result.usage * 100);
|
|
17972
|
+
lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
|
|
17907
17973
|
}
|
|
17908
|
-
|
|
17909
|
-
|
|
17974
|
+
if (result.staleCandidates > 0) {
|
|
17975
|
+
lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
|
|
17910
17976
|
}
|
|
17977
|
+
return {
|
|
17978
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
17979
|
+
declined: false,
|
|
17980
|
+
content: lines.join('\n'),
|
|
17981
|
+
};
|
|
17911
17982
|
}
|
|
17983
|
+
|
|
17912
17984
|
/**
|
|
17913
|
-
*
|
|
17985
|
+
* HEARTBEAT integration for workspace file size monitoring.
|
|
17914
17986
|
*
|
|
17915
|
-
* @
|
|
17916
|
-
*
|
|
17917
|
-
*
|
|
17918
|
-
*
|
|
17919
|
-
*
|
|
17987
|
+
* @remarks
|
|
17988
|
+
* Checks all injected workspace files (AGENTS.md, SOUL.md, TOOLS.md,
|
|
17989
|
+
* MEMORY.md, USER.md) against the OpenClaw ~20,000-char injection limit.
|
|
17990
|
+
* Files exceeding the warning threshold generate HEARTBEAT entries with
|
|
17991
|
+
* trimming guidance.
|
|
17920
17992
|
*/
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
const configPath = join(configRoot, `jeeves-${name}`, CONFIG_FILE);
|
|
17937
|
-
if (!existsSync(configPath))
|
|
17938
|
-
return 'config_missing';
|
|
17939
|
-
// Fast path: probe HTTP health endpoint
|
|
17940
|
-
try {
|
|
17941
|
-
const url = getServiceUrl$1(name);
|
|
17942
|
-
await fetchWithTimeout(`${url}/status`, PROBE_TIMEOUT_MS$1);
|
|
17943
|
-
// Healthy — check for available updates
|
|
17944
|
-
const entry = registry[name];
|
|
17945
|
-
if (entry.pluginPackage && entry.pluginVersion) {
|
|
17946
|
-
const componentConfigDir = join(configRoot, `jeeves-${name}`);
|
|
17947
|
-
const latestVersion = checkRegistryVersion(entry.pluginPackage, componentConfigDir);
|
|
17948
|
-
if (latestVersion && semverExports.gt(latestVersion, entry.pluginVersion)) {
|
|
17949
|
-
return 'update_available';
|
|
17950
|
-
}
|
|
17951
|
-
}
|
|
17952
|
-
return 'healthy';
|
|
17953
|
-
}
|
|
17954
|
-
catch {
|
|
17955
|
-
// Service not responding — classify sub-state
|
|
17956
|
-
const serviceState = getServiceState(toServiceName(name));
|
|
17957
|
-
if (serviceState === 'not_installed')
|
|
17958
|
-
return 'service_not_installed';
|
|
17959
|
-
if (serviceState === 'stopped')
|
|
17960
|
-
return 'service_stopped';
|
|
17961
|
-
// serviceState === 'running' but HTTP failed — still treat as stopped
|
|
17962
|
-
return 'service_stopped';
|
|
17963
|
-
}
|
|
17964
|
-
}
|
|
17993
|
+
/** Workspace files monitored for size budget. */
|
|
17994
|
+
const WORKSPACE_SIZE_FILES = [
|
|
17995
|
+
'AGENTS.md',
|
|
17996
|
+
'SOUL.md',
|
|
17997
|
+
'TOOLS.md',
|
|
17998
|
+
'MEMORY.md',
|
|
17999
|
+
'USER.md',
|
|
18000
|
+
];
|
|
18001
|
+
/** Trimming guidance lines emitted in HEARTBEAT entries. */
|
|
18002
|
+
const TRIMMING_GUIDANCE = [
|
|
18003
|
+
' 1. Move domain-specific content to a local skill',
|
|
18004
|
+
' 2. Extract reference material to companion files with a pointer',
|
|
18005
|
+
' 3. Summarize verbose instructions',
|
|
18006
|
+
' 4. Remove stale content',
|
|
18007
|
+
].join('\n');
|
|
17965
18008
|
/**
|
|
17966
|
-
*
|
|
18009
|
+
* Check all workspace files against the character budget.
|
|
17967
18010
|
*
|
|
17968
|
-
* @param
|
|
17969
|
-
* @
|
|
17970
|
-
*
|
|
17971
|
-
* @returns Alert text (list items), or empty string if healthy.
|
|
18011
|
+
* @param options - Health check options.
|
|
18012
|
+
* @returns Array of results, one per checked file (skips non-existent files
|
|
18013
|
+
* unless they breach the budget, which they cannot by definition).
|
|
17972
18014
|
*/
|
|
17973
|
-
function
|
|
17974
|
-
|
|
17975
|
-
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
18015
|
+
function checkWorkspaceFileHealth(options) {
|
|
18016
|
+
const { workspacePath, budgetChars = 20_000, warningThreshold = 0.8, } = options;
|
|
18017
|
+
return WORKSPACE_SIZE_FILES.map((file) => {
|
|
18018
|
+
const filePath = join(workspacePath, file);
|
|
18019
|
+
if (!existsSync(filePath)) {
|
|
18020
|
+
return {
|
|
18021
|
+
file,
|
|
18022
|
+
exists: false,
|
|
18023
|
+
charCount: 0,
|
|
18024
|
+
budget: budgetChars,
|
|
18025
|
+
usage: 0,
|
|
18026
|
+
warning: false,
|
|
18027
|
+
overBudget: false,
|
|
18028
|
+
};
|
|
18029
|
+
}
|
|
18030
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
18031
|
+
const charCount = content.length;
|
|
18032
|
+
const usage = charCount / budgetChars;
|
|
18033
|
+
return {
|
|
18034
|
+
file,
|
|
18035
|
+
exists: true,
|
|
18036
|
+
charCount,
|
|
18037
|
+
budget: budgetChars,
|
|
18038
|
+
usage,
|
|
18039
|
+
warning: usage >= warningThreshold,
|
|
18040
|
+
overBudget: charCount > budgetChars,
|
|
18041
|
+
};
|
|
18042
|
+
});
|
|
17996
18043
|
}
|
|
17997
18044
|
/**
|
|
17998
|
-
*
|
|
18045
|
+
* Convert workspace file health results into HEARTBEAT entries.
|
|
17999
18046
|
*
|
|
18000
|
-
* @param
|
|
18001
|
-
* @returns Array of HeartbeatEntry for
|
|
18047
|
+
* @param results - Results from `checkWorkspaceFileHealth`.
|
|
18048
|
+
* @returns Array of `HeartbeatEntry` objects for files that exceed the
|
|
18049
|
+
* warning threshold.
|
|
18002
18050
|
*/
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18017
|
-
}
|
|
18018
|
-
|
|
18019
|
-
// Not healthy — will be classified in second pass
|
|
18020
|
-
}
|
|
18021
|
-
}
|
|
18022
|
-
// Second pass: generate entries
|
|
18023
|
-
const entries = [];
|
|
18024
|
-
for (const name of PLATFORM_COMPONENTS) {
|
|
18025
|
-
const fullName = toServiceName(name);
|
|
18026
|
-
// Declined
|
|
18027
|
-
if (declinedNames.has(fullName)) {
|
|
18028
|
-
// Auto-decline dependents of declined hard deps
|
|
18029
|
-
entries.push({ name: fullName, declined: true, content: '' });
|
|
18030
|
-
continue;
|
|
18031
|
-
}
|
|
18032
|
-
const state = await determineComponentState(name, registry, configRoot, healthySet);
|
|
18033
|
-
// Auto-decline if hard dep is declined
|
|
18034
|
-
const deps = COMPONENT_DEPS[name];
|
|
18035
|
-
const hardDepDeclined = deps.hard.some((d) => declinedNames.has(toServiceName(d)));
|
|
18036
|
-
if (hardDepDeclined) {
|
|
18037
|
-
entries.push({ name: fullName, declined: true, content: '' });
|
|
18038
|
-
continue;
|
|
18039
|
-
}
|
|
18040
|
-
const alertText = generateAlertText(name, state, configRoot, registry);
|
|
18041
|
-
entries.push({ name: fullName, declined: false, content: alertText });
|
|
18042
|
-
}
|
|
18043
|
-
// Add soft-dep informational alerts for any healthy component with soft deps
|
|
18044
|
-
for (const entry of entries) {
|
|
18045
|
-
if (entry.declined || entry.content)
|
|
18046
|
-
continue;
|
|
18047
|
-
// Entry is healthy (no alert, not declined) — check for soft deps
|
|
18048
|
-
const shortName = entry.name.replace(/^jeeves-/, '');
|
|
18049
|
-
const deps = COMPONENT_DEPS[shortName];
|
|
18050
|
-
if (!deps.soft.length)
|
|
18051
|
-
continue;
|
|
18052
|
-
const softAlerts = [];
|
|
18053
|
-
for (const dep of deps.soft) {
|
|
18054
|
-
const depFullName = toServiceName(dep);
|
|
18055
|
-
if (declinedNames.has(depFullName))
|
|
18056
|
-
continue;
|
|
18057
|
-
if (!healthySet.has(dep)) {
|
|
18058
|
-
softAlerts.push(`- ${entry.name} is running. Some features are unavailable because ${depFullName} is not installed/running.`);
|
|
18059
|
-
}
|
|
18060
|
-
}
|
|
18061
|
-
if (softAlerts.length > 0) {
|
|
18062
|
-
entry.content = softAlerts.join('\n');
|
|
18063
|
-
}
|
|
18064
|
-
}
|
|
18065
|
-
return entries;
|
|
18051
|
+
function workspaceFileHealthEntries(results) {
|
|
18052
|
+
return results
|
|
18053
|
+
.filter((r) => r.exists && r.warning)
|
|
18054
|
+
.map((r) => {
|
|
18055
|
+
const pct = Math.round(r.usage * 100);
|
|
18056
|
+
const overBudgetNote = r.overBudget ? ' **Over budget.**' : '';
|
|
18057
|
+
const content = [
|
|
18058
|
+
`- Budget: ${r.charCount.toLocaleString()} / ${r.budget.toLocaleString()} chars (${String(pct)}%).${overBudgetNote} Trim to stay under the OpenClaw injection limit.`,
|
|
18059
|
+
`- Suggested trimming priority:\n${TRIMMING_GUIDANCE}`,
|
|
18060
|
+
].join('\n');
|
|
18061
|
+
return {
|
|
18062
|
+
name: r.file,
|
|
18063
|
+
declined: false,
|
|
18064
|
+
content,
|
|
18065
|
+
};
|
|
18066
|
+
});
|
|
18066
18067
|
}
|
|
18067
18068
|
|
|
18068
18069
|
/**
|
|
18069
|
-
*
|
|
18070
|
+
* Core configuration schema and resolution.
|
|
18070
18071
|
*
|
|
18071
18072
|
* @remarks
|
|
18072
|
-
*
|
|
18073
|
-
*
|
|
18074
|
-
*
|
|
18073
|
+
* Core config lives at `{configRoot}/jeeves-core/config.json`.
|
|
18074
|
+
* Config resolution order:
|
|
18075
|
+
* 1. Component's own config file
|
|
18076
|
+
* 2. Core config file
|
|
18077
|
+
* 3. Hardcoded library defaults
|
|
18075
18078
|
*/
|
|
18079
|
+
/** Zod schema for a service entry in core config. */
|
|
18080
|
+
const serviceEntrySchema = object({
|
|
18081
|
+
/** Service URL (must be a valid URL). */
|
|
18082
|
+
url: url().describe('Service URL'),
|
|
18083
|
+
});
|
|
18084
|
+
/** Default bind address for all Jeeves services. */
|
|
18085
|
+
const DEFAULT_BIND_ADDRESS = '0.0.0.0';
|
|
18086
|
+
/** Zod schema for the core config file. */
|
|
18087
|
+
const coreConfigSchema = object({
|
|
18088
|
+
/** JSON Schema pointer for IDE autocomplete. */
|
|
18089
|
+
$schema: string().optional().describe('JSON Schema pointer'),
|
|
18090
|
+
/** Owner identity keys (canonical identityLinks references). */
|
|
18091
|
+
owners: array(string()).default([]).describe('Owner identity keys'),
|
|
18092
|
+
/**
|
|
18093
|
+
* Bind address for all Jeeves services. Default: `0.0.0.0` (all interfaces).
|
|
18094
|
+
* Individual components can override in their own config.
|
|
18095
|
+
*/
|
|
18096
|
+
bindAddress: string()
|
|
18097
|
+
.default(DEFAULT_BIND_ADDRESS)
|
|
18098
|
+
.describe('Bind address for all Jeeves services'),
|
|
18099
|
+
/** Service URL overrides keyed by service name. */
|
|
18100
|
+
services: record(string(), serviceEntrySchema)
|
|
18101
|
+
.default({})
|
|
18102
|
+
.describe('Service URL overrides'),
|
|
18103
|
+
/** Registry cache configuration. */
|
|
18104
|
+
registryCache: object({
|
|
18105
|
+
/** Cache TTL in seconds for npm registry queries. */
|
|
18106
|
+
ttlSeconds: number()
|
|
18107
|
+
.int()
|
|
18108
|
+
.positive()
|
|
18109
|
+
.default(3600)
|
|
18110
|
+
.describe('Cache TTL in seconds'),
|
|
18111
|
+
})
|
|
18112
|
+
.prefault({})
|
|
18113
|
+
.describe('Registry cache settings'),
|
|
18114
|
+
});
|
|
18076
18115
|
/**
|
|
18077
|
-
*
|
|
18116
|
+
* Load and parse a config file, returning undefined if missing or invalid.
|
|
18078
18117
|
*
|
|
18079
|
-
* @param
|
|
18080
|
-
* @returns
|
|
18118
|
+
* @param configDir - Directory containing config.json.
|
|
18119
|
+
* @returns Parsed config or undefined.
|
|
18081
18120
|
*/
|
|
18082
|
-
function
|
|
18121
|
+
function loadConfig(configDir) {
|
|
18122
|
+
const configPath = join(configDir, CONFIG_FILE);
|
|
18123
|
+
if (!existsSync(configPath))
|
|
18124
|
+
return undefined;
|
|
18083
18125
|
try {
|
|
18084
|
-
|
|
18126
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
18127
|
+
const parsed = JSON.parse(raw);
|
|
18128
|
+
return coreConfigSchema.parse(parsed);
|
|
18085
18129
|
}
|
|
18086
|
-
catch
|
|
18087
|
-
|
|
18088
|
-
'code' in err &&
|
|
18089
|
-
err.code === 'ENOENT') {
|
|
18090
|
-
return '';
|
|
18091
|
-
}
|
|
18092
|
-
throw err;
|
|
18130
|
+
catch {
|
|
18131
|
+
return undefined;
|
|
18093
18132
|
}
|
|
18094
18133
|
}
|
|
18134
|
+
|
|
18095
18135
|
/**
|
|
18096
|
-
*
|
|
18136
|
+
* Service URL resolution.
|
|
18097
18137
|
*
|
|
18098
|
-
* @
|
|
18138
|
+
* @remarks
|
|
18139
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
18140
|
+
* resolution order:
|
|
18141
|
+
* 1. Consumer's own component config
|
|
18142
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
18143
|
+
* 3. Default port constants
|
|
18099
18144
|
*/
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18113
|
-
|
|
18114
|
-
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
|
|
18120
|
-
staleDays: wsConfig?.memory?.staleDays ??
|
|
18121
|
-
WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
|
|
18122
|
-
});
|
|
18123
|
-
if (memoryEntry)
|
|
18124
|
-
entries.push(memoryEntry);
|
|
18125
|
-
}
|
|
18126
|
-
else {
|
|
18127
|
-
entries.push({
|
|
18128
|
-
name: MEMORY_HEARTBEAT_NAME,
|
|
18129
|
-
declined: true,
|
|
18130
|
-
content: '',
|
|
18131
|
-
});
|
|
18132
|
-
}
|
|
18133
|
-
await writeHeartbeatSection(heartbeatPath, entries);
|
|
18134
|
-
}
|
|
18135
|
-
catch (err) {
|
|
18136
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
18137
|
-
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
18145
|
+
/**
|
|
18146
|
+
* Resolve the URL for a named Jeeves service.
|
|
18147
|
+
*
|
|
18148
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
18149
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
18150
|
+
* @returns The resolved service URL.
|
|
18151
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
18152
|
+
*/
|
|
18153
|
+
function getServiceUrl$1(serviceName, consumerName) {
|
|
18154
|
+
// 2. Check core config
|
|
18155
|
+
const coreDir = getCoreConfigDir();
|
|
18156
|
+
const coreConfig = loadConfig(coreDir);
|
|
18157
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
18158
|
+
if (coreUrl)
|
|
18159
|
+
return coreUrl;
|
|
18160
|
+
// 3. Fall back to port constants
|
|
18161
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
18162
|
+
if (port !== undefined) {
|
|
18163
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
18138
18164
|
}
|
|
18165
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
18139
18166
|
}
|
|
18140
18167
|
|
|
18141
18168
|
/**
|
|
18142
|
-
*
|
|
18169
|
+
* Registry version cache for npm package update awareness.
|
|
18143
18170
|
*
|
|
18144
18171
|
* @remarks
|
|
18145
|
-
*
|
|
18146
|
-
*
|
|
18147
|
-
* on a configurable prime-interval timer cycle.
|
|
18172
|
+
* Caches the latest npm registry version in a local JSON file
|
|
18173
|
+
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
18148
18174
|
*/
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
/** The component's config directory path. */
|
|
18162
|
-
get componentConfigDir() {
|
|
18163
|
-
return this.configDir;
|
|
18164
|
-
}
|
|
18165
|
-
/** Whether the writer timer is currently running. */
|
|
18166
|
-
get isRunning() {
|
|
18167
|
-
return this.timer !== undefined;
|
|
18168
|
-
}
|
|
18169
|
-
/**
|
|
18170
|
-
* Start the writer timer.
|
|
18171
|
-
*
|
|
18172
|
-
* @remarks
|
|
18173
|
-
* Performs an immediate first write, then sets up the interval.
|
|
18174
|
-
*/
|
|
18175
|
-
start() {
|
|
18176
|
-
if (this.timer)
|
|
18177
|
-
return;
|
|
18178
|
-
// Fire immediately, then on interval
|
|
18179
|
-
void this.cycle();
|
|
18180
|
-
this.timer = setInterval(() => void this.cycle(), this.component.refreshIntervalSeconds * 1000);
|
|
18181
|
-
}
|
|
18182
|
-
/** Stop the writer timer. */
|
|
18183
|
-
stop() {
|
|
18184
|
-
if (this.timer) {
|
|
18185
|
-
clearInterval(this.timer);
|
|
18186
|
-
this.timer = undefined;
|
|
18187
|
-
}
|
|
18188
|
-
}
|
|
18189
|
-
/**
|
|
18190
|
-
* Execute a single write cycle.
|
|
18191
|
-
*
|
|
18192
|
-
* @remarks
|
|
18193
|
-
* 1. Write the component's TOOLS.md section.
|
|
18194
|
-
* 2. Refresh shared platform content (SOUL.md, AGENTS.md, Platform section).
|
|
18195
|
-
* 3. Scan for cleanup flags and escalate if a gateway URL is configured.
|
|
18196
|
-
* 4. Run HEARTBEAT health orchestration.
|
|
18197
|
-
*/
|
|
18198
|
-
async cycle() {
|
|
18175
|
+
/**
|
|
18176
|
+
* Check the npm registry for the latest version of a package.
|
|
18177
|
+
*
|
|
18178
|
+
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
18179
|
+
* @param cacheDir - Directory to store the cache file.
|
|
18180
|
+
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
18181
|
+
* @returns The latest version string, or undefined if the check fails.
|
|
18182
|
+
*/
|
|
18183
|
+
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
18184
|
+
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
18185
|
+
// Check cache first
|
|
18186
|
+
if (existsSync(cachePath)) {
|
|
18199
18187
|
try {
|
|
18200
|
-
const
|
|
18201
|
-
const
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
mode: 'section',
|
|
18206
|
-
sectionId: this.component.sectionId,
|
|
18207
|
-
markers: TOOLS_MARKERS,
|
|
18208
|
-
coreVersion: CORE_VERSION,
|
|
18209
|
-
});
|
|
18210
|
-
// 2. Platform content maintenance
|
|
18211
|
-
await refreshPlatformContent({
|
|
18212
|
-
coreVersion: CORE_VERSION,
|
|
18213
|
-
componentName: this.component.name,
|
|
18214
|
-
componentVersion: this.component.version,
|
|
18215
|
-
servicePackage: this.component.servicePackage,
|
|
18216
|
-
pluginPackage: this.component.pluginPackage,
|
|
18217
|
-
});
|
|
18218
|
-
// 3. Cleanup escalation
|
|
18219
|
-
if (this.gatewayUrl) {
|
|
18220
|
-
scanAndEscalateCleanup([
|
|
18221
|
-
{ filePath: toolsPath, markerIdentity: 'TOOLS' },
|
|
18222
|
-
{
|
|
18223
|
-
filePath: join(workspacePath, WORKSPACE_FILES.soul),
|
|
18224
|
-
markerIdentity: 'SOUL',
|
|
18225
|
-
},
|
|
18226
|
-
{
|
|
18227
|
-
filePath: join(workspacePath, WORKSPACE_FILES.agents),
|
|
18228
|
-
markerIdentity: 'AGENTS',
|
|
18229
|
-
},
|
|
18230
|
-
], this.gatewayUrl, this.pendingCleanups);
|
|
18188
|
+
const raw = readFileSync(cachePath, 'utf-8');
|
|
18189
|
+
const entry = JSON.parse(raw);
|
|
18190
|
+
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
18191
|
+
if (age < ttlSeconds * 1000) {
|
|
18192
|
+
return entry.version;
|
|
18231
18193
|
}
|
|
18232
|
-
// 4. HEARTBEAT orchestration
|
|
18233
|
-
await runHeartbeatCycle({
|
|
18234
|
-
workspacePath,
|
|
18235
|
-
coreConfigDir: getCoreConfigDir(),
|
|
18236
|
-
configRoot: getConfigRoot$1(),
|
|
18237
|
-
});
|
|
18238
18194
|
}
|
|
18239
|
-
catch
|
|
18240
|
-
|
|
18241
|
-
|
|
18195
|
+
catch {
|
|
18196
|
+
// Cache corrupt — proceed with fresh check
|
|
18197
|
+
}
|
|
18198
|
+
}
|
|
18199
|
+
// Query npm registry
|
|
18200
|
+
try {
|
|
18201
|
+
const result = execSync(`npm view ${packageName} version`, {
|
|
18202
|
+
encoding: 'utf-8',
|
|
18203
|
+
timeout: 15_000,
|
|
18204
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18205
|
+
}).trim();
|
|
18206
|
+
if (!result)
|
|
18207
|
+
return undefined;
|
|
18208
|
+
// Write cache
|
|
18209
|
+
if (!existsSync(cacheDir)) {
|
|
18210
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
18242
18211
|
}
|
|
18212
|
+
const entry = {
|
|
18213
|
+
version: result,
|
|
18214
|
+
checkedAt: new Date().toISOString(),
|
|
18215
|
+
};
|
|
18216
|
+
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
18217
|
+
return result;
|
|
18218
|
+
}
|
|
18219
|
+
catch {
|
|
18220
|
+
return undefined;
|
|
18243
18221
|
}
|
|
18244
18222
|
}
|
|
18245
18223
|
|
|
18246
18224
|
/**
|
|
18247
|
-
*
|
|
18225
|
+
* HEARTBEAT health orchestration.
|
|
18248
18226
|
*
|
|
18249
18227
|
* @remarks
|
|
18250
|
-
*
|
|
18251
|
-
*
|
|
18252
|
-
*
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18228
|
+
* Determines the state of each platform component and generates
|
|
18229
|
+
* HEARTBEAT entries with actionable alert text. Applies the dependency
|
|
18230
|
+
* graph for alert suppression and auto-decline.
|
|
18231
|
+
*/
|
|
18232
|
+
/** Derive the full service name from a component name. */
|
|
18233
|
+
function toServiceName(name) {
|
|
18234
|
+
return `jeeves-${name}`;
|
|
18235
|
+
}
|
|
18236
|
+
/** Known dependency declarations for platform components. */
|
|
18237
|
+
const COMPONENT_DEPS = {
|
|
18238
|
+
meta: { hard: ['watcher'], soft: [] },
|
|
18239
|
+
server: { hard: [], soft: ['watcher', 'runner', 'meta'] },
|
|
18240
|
+
runner: { hard: [], soft: [] },
|
|
18241
|
+
watcher: { hard: [], soft: [] },
|
|
18242
|
+
};
|
|
18243
|
+
/** "Not installed" alert text for each platform component. Shared with seedContent. */
|
|
18244
|
+
const NOT_INSTALLED_ALERTS = {
|
|
18245
|
+
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`.',
|
|
18246
|
+
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`.',
|
|
18247
|
+
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`.',
|
|
18248
|
+
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`.',
|
|
18249
|
+
};
|
|
18250
|
+
/** Alert text generators by state. */
|
|
18251
|
+
const ALERT_TEXT = {
|
|
18252
|
+
runner: {
|
|
18253
|
+
not_installed: NOT_INSTALLED_ALERTS['runner'],
|
|
18254
|
+
config_missing: (configRoot) => `Plugin installed but no configuration file found at \`${configRoot}/jeeves-runner/config.json\`. The plugin installer should have created a default config. Ask the user for consent to re-run the installer: \`npx @karmaniverous/jeeves-runner-openclaw install\`.`,
|
|
18255
|
+
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-runner service install`. Verify the service is installed.',
|
|
18256
|
+
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-runner service start`. Verify via `GET http://127.0.0.1:1937/status`.',
|
|
18257
|
+
},
|
|
18258
|
+
watcher: {
|
|
18259
|
+
not_installed: NOT_INSTALLED_ALERTS['watcher'],
|
|
18260
|
+
deps_missing: 'Plugin installed but Qdrant is not responding on `http://127.0.0.1:6333`. Qdrant is the vector database required for semantic search. Ask the user for consent to set up Qdrant. Guide them through installation for their platform — Docker is simplest: `docker run -p 6333:6333 qdrant/qdrant`. Verify via `GET http://127.0.0.1:6333/collections`.',
|
|
18261
|
+
config_missing: (configRoot) => `Plugin installed, Qdrant available, but config file missing or invalid at \`${configRoot}/jeeves-watcher/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-watcher-openclaw install\`.`,
|
|
18262
|
+
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-watcher service install`. Verify the service is installed.',
|
|
18263
|
+
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-watcher service start`. Verify via `GET http://127.0.0.1:1936/status`.',
|
|
18264
|
+
},
|
|
18265
|
+
server: {
|
|
18266
|
+
not_installed: NOT_INSTALLED_ALERTS['server'],
|
|
18267
|
+
config_missing: (configRoot) => `Plugin installed but config file missing or invalid at \`${configRoot}/jeeves-server/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-server-openclaw install\`.`,
|
|
18268
|
+
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-server service install`. Verify the service is installed.',
|
|
18269
|
+
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-server service start`. Verify via `GET http://127.0.0.1:1934/status`.',
|
|
18270
|
+
},
|
|
18271
|
+
meta: {
|
|
18272
|
+
not_installed: NOT_INSTALLED_ALERTS['meta'],
|
|
18273
|
+
deps_missing: 'Plugin installed but required dependency jeeves-watcher is not available. The watcher must be installed and running before meta can function. Do not attempt to set up meta until jeeves-watcher is healthy.',
|
|
18274
|
+
config_missing: (configRoot) => `Plugin installed, watcher available, but config file missing or invalid at \`${configRoot}/jeeves-meta/config.json\`. The plugin installer should have created a default config. If missing, re-run: \`npx @karmaniverous/jeeves-meta-openclaw install\`.`,
|
|
18275
|
+
service_not_installed: 'Plugin installed and configured but the system service is not installed. Ask the user for consent. On approval, execute: `jeeves-meta service install`. Verify the service is installed.',
|
|
18276
|
+
service_stopped: 'Service installed but not running. Ask the user for consent. On approval, execute: `jeeves-meta service start`. Verify via `GET http://127.0.0.1:1938/status`.',
|
|
18277
|
+
},
|
|
18278
|
+
};
|
|
18279
|
+
/** Default Qdrant URL for watcher dependency check. */
|
|
18280
|
+
const QDRANT_URL = 'http://127.0.0.1:6333';
|
|
18281
|
+
/** Health probe timeout in milliseconds. */
|
|
18282
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
18283
|
+
/**
|
|
18284
|
+
* Check if Qdrant is reachable (watcher dependency).
|
|
18269
18285
|
*
|
|
18270
|
-
*
|
|
18271
|
-
* // ...
|
|
18272
|
-
* generateToolsContent: getContent,
|
|
18273
|
-
* });
|
|
18274
|
-
* ```
|
|
18286
|
+
* @returns True if Qdrant responds.
|
|
18275
18287
|
*/
|
|
18288
|
+
async function isQdrantAvailable() {
|
|
18289
|
+
try {
|
|
18290
|
+
await fetchWithTimeout(`${QDRANT_URL}/collections`, PROBE_TIMEOUT_MS);
|
|
18291
|
+
return true;
|
|
18292
|
+
}
|
|
18293
|
+
catch {
|
|
18294
|
+
return false;
|
|
18295
|
+
}
|
|
18296
|
+
}
|
|
18276
18297
|
/**
|
|
18277
|
-
*
|
|
18298
|
+
* Determine the state of a single component.
|
|
18278
18299
|
*
|
|
18279
|
-
* @param
|
|
18280
|
-
* @
|
|
18300
|
+
* @param name - Component name.
|
|
18301
|
+
* @param registry - Current component-versions.json contents.
|
|
18302
|
+
* @param configRoot - Config root path.
|
|
18303
|
+
* @param healthySet - Set of component names known to be healthy (for dep checks).
|
|
18304
|
+
* @returns The component's state.
|
|
18281
18305
|
*/
|
|
18282
|
-
function
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
if (!
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18306
|
+
async function determineComponentState(name, registry, configRoot, healthySet) {
|
|
18307
|
+
// Not in registry = not installed
|
|
18308
|
+
if (!(name in registry))
|
|
18309
|
+
return 'not_installed';
|
|
18310
|
+
// Check hard dependencies
|
|
18311
|
+
const deps = COMPONENT_DEPS[name];
|
|
18312
|
+
for (const hardDep of deps.hard) {
|
|
18313
|
+
if (!healthySet.has(hardDep))
|
|
18314
|
+
return 'deps_missing';
|
|
18315
|
+
}
|
|
18316
|
+
// Watcher-specific: check Qdrant
|
|
18317
|
+
if (name === 'watcher' && !(await isQdrantAvailable())) {
|
|
18318
|
+
return 'deps_missing';
|
|
18319
|
+
}
|
|
18320
|
+
// Check config file
|
|
18321
|
+
const configPath = join(configRoot, `jeeves-${name}`, CONFIG_FILE);
|
|
18322
|
+
if (!existsSync(configPath))
|
|
18323
|
+
return 'config_missing';
|
|
18324
|
+
// Fast path: probe HTTP health endpoint
|
|
18325
|
+
try {
|
|
18326
|
+
const url = getServiceUrl$1(name);
|
|
18327
|
+
await fetchWithTimeout(`${url}/status`, PROBE_TIMEOUT_MS);
|
|
18328
|
+
// Healthy — check for available updates
|
|
18329
|
+
const entry = registry[name];
|
|
18330
|
+
if (entry.pluginPackage && entry.pluginVersion) {
|
|
18331
|
+
const componentConfigDir = join(configRoot, `jeeves-${name}`);
|
|
18332
|
+
const latestVersion = checkRegistryVersion(entry.pluginPackage, componentConfigDir);
|
|
18333
|
+
if (latestVersion && semverExports.gt(latestVersion, entry.pluginVersion)) {
|
|
18334
|
+
return 'update_available';
|
|
18335
|
+
}
|
|
18299
18336
|
}
|
|
18300
|
-
return
|
|
18301
|
-
}
|
|
18337
|
+
return 'healthy';
|
|
18338
|
+
}
|
|
18339
|
+
catch {
|
|
18340
|
+
// Service not responding — classify sub-state
|
|
18341
|
+
const serviceState = getServiceState(toServiceName(name));
|
|
18342
|
+
if (serviceState === 'not_installed')
|
|
18343
|
+
return 'service_not_installed';
|
|
18344
|
+
if (serviceState === 'stopped')
|
|
18345
|
+
return 'service_stopped';
|
|
18346
|
+
// serviceState === 'running' but HTTP failed — still treat as stopped
|
|
18347
|
+
return 'service_stopped';
|
|
18348
|
+
}
|
|
18302
18349
|
}
|
|
18303
|
-
|
|
18304
18350
|
/**
|
|
18305
|
-
*
|
|
18351
|
+
* Generate the alert text for a component in a given state.
|
|
18306
18352
|
*
|
|
18307
|
-
* @
|
|
18308
|
-
*
|
|
18309
|
-
*
|
|
18310
|
-
*
|
|
18353
|
+
* @param name - Component name.
|
|
18354
|
+
* @param state - The component's state.
|
|
18355
|
+
* @param configRoot - Config root path.
|
|
18356
|
+
* @returns Alert text (list items), or empty string if healthy.
|
|
18311
18357
|
*/
|
|
18358
|
+
function generateAlertText(name, state, configRoot, registry) {
|
|
18359
|
+
if (state === 'healthy')
|
|
18360
|
+
return '';
|
|
18361
|
+
// Update available — dynamic text with version info
|
|
18362
|
+
if (state === 'update_available') {
|
|
18363
|
+
const entry = registry[name];
|
|
18364
|
+
const currentVersion = entry.pluginVersion ?? 'unknown';
|
|
18365
|
+
const componentConfigDir = join(configRoot, `jeeves-${name}`);
|
|
18366
|
+
const latestVersion = entry.pluginPackage
|
|
18367
|
+
? (checkRegistryVersion(entry.pluginPackage, componentConfigDir) ??
|
|
18368
|
+
'unknown')
|
|
18369
|
+
: 'unknown';
|
|
18370
|
+
const installCmd = entry.pluginPackage
|
|
18371
|
+
? `\`npx ${entry.pluginPackage} install\``
|
|
18372
|
+
: `\`npx @karmaniverous/jeeves-${name}-openclaw install\``;
|
|
18373
|
+
return `- Update available: v${currentVersion} → v${latestVersion}. Ask the user for consent to update. On approval, execute: ${installCmd}.`;
|
|
18374
|
+
}
|
|
18375
|
+
const componentAlerts = ALERT_TEXT[name];
|
|
18376
|
+
const alertOrFn = componentAlerts[state];
|
|
18377
|
+
if (!alertOrFn)
|
|
18378
|
+
return '';
|
|
18379
|
+
const text = typeof alertOrFn === 'function' ? alertOrFn(configRoot) : alertOrFn;
|
|
18380
|
+
return `- ${text}`;
|
|
18381
|
+
}
|
|
18312
18382
|
/**
|
|
18313
|
-
*
|
|
18314
|
-
*
|
|
18315
|
-
* @remarks
|
|
18316
|
-
* The descriptor is validated via the Zod schema at runtime.
|
|
18317
|
-
* This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
|
|
18383
|
+
* Orchestrate HEARTBEAT entries for all platform components.
|
|
18318
18384
|
*
|
|
18319
|
-
* @param
|
|
18320
|
-
* @
|
|
18321
|
-
* @returns A new `ComponentWriter` instance.
|
|
18322
|
-
* @throws ZodError if the descriptor is invalid.
|
|
18385
|
+
* @param options - Orchestration configuration.
|
|
18386
|
+
* @returns Array of HeartbeatEntry for writeHeartbeatSection.
|
|
18323
18387
|
*/
|
|
18324
|
-
function
|
|
18325
|
-
|
|
18326
|
-
|
|
18327
|
-
|
|
18388
|
+
async function orchestrateHeartbeat(options) {
|
|
18389
|
+
const { coreConfigDir, configRoot, declinedNames } = options;
|
|
18390
|
+
const registry = readComponentVersions(coreConfigDir);
|
|
18391
|
+
// First pass: determine which components are healthy (for dep resolution)
|
|
18392
|
+
const healthySet = new Set();
|
|
18393
|
+
for (const name of PLATFORM_COMPONENTS) {
|
|
18394
|
+
if (declinedNames.has(toServiceName(name)))
|
|
18395
|
+
continue;
|
|
18396
|
+
if (!(name in registry))
|
|
18397
|
+
continue;
|
|
18398
|
+
try {
|
|
18399
|
+
const url = getServiceUrl$1(name);
|
|
18400
|
+
await fetchWithTimeout(`${url}/status`, PROBE_TIMEOUT_MS);
|
|
18401
|
+
healthySet.add(name);
|
|
18402
|
+
}
|
|
18403
|
+
catch {
|
|
18404
|
+
// Not healthy — will be classified in second pass
|
|
18405
|
+
}
|
|
18406
|
+
}
|
|
18407
|
+
// Second pass: generate entries
|
|
18408
|
+
const entries = [];
|
|
18409
|
+
for (const name of PLATFORM_COMPONENTS) {
|
|
18410
|
+
const fullName = toServiceName(name);
|
|
18411
|
+
// Declined
|
|
18412
|
+
if (declinedNames.has(fullName)) {
|
|
18413
|
+
// Auto-decline dependents of declined hard deps
|
|
18414
|
+
entries.push({ name: fullName, declined: true, content: '' });
|
|
18415
|
+
continue;
|
|
18416
|
+
}
|
|
18417
|
+
const state = await determineComponentState(name, registry, configRoot, healthySet);
|
|
18418
|
+
// Auto-decline if hard dep is declined
|
|
18419
|
+
const deps = COMPONENT_DEPS[name];
|
|
18420
|
+
const hardDepDeclined = deps.hard.some((d) => declinedNames.has(toServiceName(d)));
|
|
18421
|
+
if (hardDepDeclined) {
|
|
18422
|
+
entries.push({ name: fullName, declined: true, content: '' });
|
|
18423
|
+
continue;
|
|
18424
|
+
}
|
|
18425
|
+
const alertText = generateAlertText(name, state, configRoot, registry);
|
|
18426
|
+
entries.push({ name: fullName, declined: false, content: alertText });
|
|
18427
|
+
}
|
|
18428
|
+
// Add soft-dep informational alerts for any healthy component with soft deps
|
|
18429
|
+
for (const entry of entries) {
|
|
18430
|
+
if (entry.declined || entry.content)
|
|
18431
|
+
continue;
|
|
18432
|
+
// Entry is healthy (no alert, not declined) — check for soft deps
|
|
18433
|
+
const shortName = entry.name.replace(/^jeeves-/, '');
|
|
18434
|
+
const deps = COMPONENT_DEPS[shortName];
|
|
18435
|
+
if (!deps.soft.length)
|
|
18436
|
+
continue;
|
|
18437
|
+
const softAlerts = [];
|
|
18438
|
+
for (const dep of deps.soft) {
|
|
18439
|
+
const depFullName = toServiceName(dep);
|
|
18440
|
+
if (declinedNames.has(depFullName))
|
|
18441
|
+
continue;
|
|
18442
|
+
if (!healthySet.has(dep)) {
|
|
18443
|
+
softAlerts.push(`- ${entry.name} is running. Some features are unavailable because ${depFullName} is not installed/running.`);
|
|
18444
|
+
}
|
|
18445
|
+
}
|
|
18446
|
+
if (softAlerts.length > 0) {
|
|
18447
|
+
entry.content = softAlerts.join('\n');
|
|
18448
|
+
}
|
|
18449
|
+
}
|
|
18450
|
+
return entries;
|
|
18328
18451
|
}
|
|
18329
18452
|
|
|
18330
18453
|
/**
|
|
18331
|
-
*
|
|
18454
|
+
* HEARTBEAT orchestration extracted from ComponentWriter.cycle().
|
|
18332
18455
|
*
|
|
18333
18456
|
* @remarks
|
|
18334
|
-
*
|
|
18335
|
-
*
|
|
18336
|
-
|
|
18337
|
-
/**
|
|
18338
|
-
* Format a successful tool result.
|
|
18339
|
-
*
|
|
18340
|
-
* @param data - Arbitrary data to return as JSON.
|
|
18341
|
-
* @returns A `ToolResult` with JSON-stringified content.
|
|
18342
|
-
*/
|
|
18343
|
-
function ok(data) {
|
|
18344
|
-
return {
|
|
18345
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
18346
|
-
};
|
|
18347
|
-
}
|
|
18348
|
-
/**
|
|
18349
|
-
* Format an error tool result.
|
|
18350
|
-
*
|
|
18351
|
-
* @param error - Error instance, string, or other value.
|
|
18352
|
-
* @returns A `ToolResult` with `isError: true`.
|
|
18457
|
+
* Reads existing HEARTBEAT.md, resolves declined components, runs the
|
|
18458
|
+
* heartbeat state machine, and writes the result. Best-effort: failures
|
|
18459
|
+
* are logged but do not propagate.
|
|
18353
18460
|
*/
|
|
18354
|
-
function fail(error) {
|
|
18355
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
18356
|
-
return {
|
|
18357
|
-
content: [{ type: 'text', text: 'Error: ' + message }],
|
|
18358
|
-
isError: true,
|
|
18359
|
-
};
|
|
18360
|
-
}
|
|
18361
18461
|
/**
|
|
18362
|
-
*
|
|
18363
|
-
*
|
|
18364
|
-
* @remarks
|
|
18365
|
-
* Detects `ECONNREFUSED`, `ENOTFOUND`, and `ETIMEDOUT` from
|
|
18366
|
-
* `error.cause.code` and returns a user-friendly message referencing
|
|
18367
|
-
* the plugin's `config.apiUrl` setting. Falls back to `fail()` for
|
|
18368
|
-
* non-connection errors.
|
|
18462
|
+
* Read a file's content, returning empty string if the file does not exist.
|
|
18369
18463
|
*
|
|
18370
|
-
* @param
|
|
18371
|
-
* @
|
|
18372
|
-
* @param pluginId - The plugin identifier for config guidance.
|
|
18373
|
-
* @returns A `ToolResult` with `isError: true`.
|
|
18464
|
+
* @param filePath - Absolute file path.
|
|
18465
|
+
* @returns File content or empty string.
|
|
18374
18466
|
*/
|
|
18375
|
-
function
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
text: [
|
|
18387
|
-
`Service not reachable at ${baseUrl}.`,
|
|
18388
|
-
'Either start the service, or if it runs on a different port,',
|
|
18389
|
-
`set plugins.entries.${pluginId}.config.apiUrl in openclaw.json.`,
|
|
18390
|
-
].join('\n'),
|
|
18391
|
-
},
|
|
18392
|
-
],
|
|
18393
|
-
isError: true,
|
|
18394
|
-
};
|
|
18467
|
+
function readFileOrEmpty(filePath) {
|
|
18468
|
+
try {
|
|
18469
|
+
return readFileSync(filePath, 'utf-8');
|
|
18470
|
+
}
|
|
18471
|
+
catch (err) {
|
|
18472
|
+
if (err instanceof Error &&
|
|
18473
|
+
'code' in err &&
|
|
18474
|
+
err.code === 'ENOENT') {
|
|
18475
|
+
return '';
|
|
18476
|
+
}
|
|
18477
|
+
throw err;
|
|
18395
18478
|
}
|
|
18396
|
-
return fail(error);
|
|
18397
18479
|
}
|
|
18398
|
-
|
|
18399
|
-
/**
|
|
18400
|
-
* Factory for the standard plugin tool set.
|
|
18401
|
-
*
|
|
18402
|
-
* @remarks
|
|
18403
|
-
* Produces four standard tools from a component descriptor:
|
|
18404
|
-
* - `{name}_status` - Probe service health + version + uptime
|
|
18405
|
-
* - `{name}_config` - Query running config with optional JSONPath
|
|
18406
|
-
* - `{name}_config_apply` - Push config patch to running service
|
|
18407
|
-
* - `{name}_service` - Service lifecycle management
|
|
18408
|
-
*
|
|
18409
|
-
* Components add domain-specific tools separately.
|
|
18410
|
-
*/
|
|
18411
|
-
/** Timeout for HTTP probes in milliseconds. */
|
|
18412
|
-
const PROBE_TIMEOUT_MS = 5000;
|
|
18413
18480
|
/**
|
|
18414
|
-
*
|
|
18481
|
+
* Run a single HEARTBEAT orchestration cycle.
|
|
18415
18482
|
*
|
|
18416
|
-
* @param
|
|
18417
|
-
* @returns Array of tool descriptors to register.
|
|
18483
|
+
* @param options - Heartbeat cycle configuration.
|
|
18418
18484
|
*/
|
|
18419
|
-
function
|
|
18420
|
-
const {
|
|
18421
|
-
const
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
}
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
|
|
18450
|
-
|
|
18451
|
-
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
|
|
18458
|
-
const qs = path ? `?path=${encodeURIComponent(path)}` : '';
|
|
18459
|
-
try {
|
|
18460
|
-
const result = await fetchJson(`${baseUrl}/config${qs}`);
|
|
18461
|
-
return ok(result);
|
|
18462
|
-
}
|
|
18463
|
-
catch (err) {
|
|
18464
|
-
return connectionFail(err, baseUrl, `jeeves-${name}-openclaw`);
|
|
18465
|
-
}
|
|
18466
|
-
},
|
|
18467
|
-
};
|
|
18468
|
-
const configApplyTool = {
|
|
18469
|
-
name: `${name}_config_apply`,
|
|
18470
|
-
description: `Apply a config patch to the running ${name} service.`,
|
|
18471
|
-
parameters: {
|
|
18472
|
-
type: 'object',
|
|
18473
|
-
properties: {
|
|
18474
|
-
config: {
|
|
18475
|
-
type: 'object',
|
|
18476
|
-
description: 'Config patch to apply',
|
|
18477
|
-
},
|
|
18478
|
-
},
|
|
18479
|
-
required: ['config'],
|
|
18480
|
-
},
|
|
18481
|
-
execute: async (_id, params) => {
|
|
18482
|
-
const config = params.config;
|
|
18483
|
-
if (!config) {
|
|
18484
|
-
return fail('Missing required parameter: config');
|
|
18485
|
-
}
|
|
18486
|
-
try {
|
|
18487
|
-
const result = await postJson(`${baseUrl}/config/apply`, {
|
|
18488
|
-
patch: config,
|
|
18489
|
-
});
|
|
18490
|
-
return ok(result);
|
|
18491
|
-
}
|
|
18492
|
-
catch (err) {
|
|
18493
|
-
return connectionFail(err, baseUrl, `jeeves-${name}-openclaw`);
|
|
18494
|
-
}
|
|
18495
|
-
},
|
|
18496
|
-
};
|
|
18497
|
-
const serviceTool = {
|
|
18498
|
-
name: `${name}_service`,
|
|
18499
|
-
description: `Manage the ${name} system service. Actions: install, uninstall, start, stop, restart, status.`,
|
|
18500
|
-
parameters: {
|
|
18501
|
-
type: 'object',
|
|
18502
|
-
properties: {
|
|
18503
|
-
action: {
|
|
18504
|
-
type: 'string',
|
|
18505
|
-
enum: ['install', 'uninstall', 'start', 'stop', 'restart', 'status'],
|
|
18506
|
-
description: 'Service action to perform',
|
|
18507
|
-
},
|
|
18508
|
-
},
|
|
18509
|
-
required: ['action'],
|
|
18510
|
-
},
|
|
18511
|
-
execute: (_id, params) => {
|
|
18512
|
-
const action = params.action;
|
|
18513
|
-
const validActions = [
|
|
18514
|
-
'install',
|
|
18515
|
-
'uninstall',
|
|
18516
|
-
'start',
|
|
18517
|
-
'stop',
|
|
18518
|
-
'restart',
|
|
18519
|
-
'status',
|
|
18520
|
-
];
|
|
18521
|
-
if (!validActions.includes(action)) {
|
|
18522
|
-
return Promise.resolve(fail(`Invalid action: ${action}`));
|
|
18523
|
-
}
|
|
18524
|
-
try {
|
|
18525
|
-
if (action === 'status') {
|
|
18526
|
-
const state = svcManager.status();
|
|
18527
|
-
return Promise.resolve(ok({ service: name, state }));
|
|
18528
|
-
}
|
|
18529
|
-
// Call the appropriate method
|
|
18530
|
-
const methodMap = {
|
|
18531
|
-
install: () => {
|
|
18532
|
-
svcManager.install();
|
|
18533
|
-
},
|
|
18534
|
-
uninstall: () => {
|
|
18535
|
-
svcManager.uninstall();
|
|
18536
|
-
},
|
|
18537
|
-
start: () => {
|
|
18538
|
-
svcManager.start();
|
|
18539
|
-
},
|
|
18540
|
-
stop: () => {
|
|
18541
|
-
svcManager.stop();
|
|
18542
|
-
},
|
|
18543
|
-
restart: () => {
|
|
18544
|
-
svcManager.restart();
|
|
18545
|
-
},
|
|
18546
|
-
};
|
|
18547
|
-
methodMap[action]();
|
|
18548
|
-
return Promise.resolve(ok({ service: name, action, success: true }));
|
|
18485
|
+
async function runHeartbeatCycle(options) {
|
|
18486
|
+
const { workspacePath, coreConfigDir, configRoot } = options;
|
|
18487
|
+
const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
|
|
18488
|
+
try {
|
|
18489
|
+
const existingContent = readFileOrEmpty(heartbeatPath);
|
|
18490
|
+
const parsed = parseHeartbeat(existingContent);
|
|
18491
|
+
const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
|
|
18492
|
+
const entries = await orchestrateHeartbeat({
|
|
18493
|
+
coreConfigDir,
|
|
18494
|
+
configRoot,
|
|
18495
|
+
declinedNames,
|
|
18496
|
+
});
|
|
18497
|
+
// Memory hygiene check (Decision 49)
|
|
18498
|
+
if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
|
|
18499
|
+
const wsConfig = loadWorkspaceConfig(workspacePath);
|
|
18500
|
+
const memoryEntry = checkMemoryHealth({
|
|
18501
|
+
workspacePath,
|
|
18502
|
+
budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
|
|
18503
|
+
warningThreshold: wsConfig?.memory?.warningThreshold ??
|
|
18504
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
|
|
18505
|
+
staleDays: wsConfig?.memory?.staleDays ??
|
|
18506
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
|
|
18507
|
+
});
|
|
18508
|
+
if (memoryEntry)
|
|
18509
|
+
entries.push(memoryEntry);
|
|
18510
|
+
}
|
|
18511
|
+
else {
|
|
18512
|
+
entries.push({
|
|
18513
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
18514
|
+
declined: true,
|
|
18515
|
+
content: '',
|
|
18516
|
+
});
|
|
18517
|
+
}
|
|
18518
|
+
// Workspace file size health check (Decision 70)
|
|
18519
|
+
const wsFileResults = checkWorkspaceFileHealth({ workspacePath });
|
|
18520
|
+
const wsFileAlerts = workspaceFileHealthEntries(wsFileResults);
|
|
18521
|
+
for (const alert of wsFileAlerts) {
|
|
18522
|
+
if (declinedNames.has(alert.name)) {
|
|
18523
|
+
entries.push({ name: alert.name, declined: true, content: '' });
|
|
18549
18524
|
}
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
|
|
18525
|
+
else {
|
|
18526
|
+
entries.push(alert);
|
|
18553
18527
|
}
|
|
18554
|
-
}
|
|
18555
|
-
|
|
18556
|
-
|
|
18528
|
+
}
|
|
18529
|
+
await writeHeartbeatSection(heartbeatPath, entries);
|
|
18530
|
+
}
|
|
18531
|
+
catch (err) {
|
|
18532
|
+
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
|
|
18533
|
+
}
|
|
18557
18534
|
}
|
|
18558
18535
|
|
|
18559
18536
|
/**
|
|
18560
|
-
*
|
|
18537
|
+
* Timer-based orchestrator for managed content writing.
|
|
18561
18538
|
*
|
|
18562
18539
|
* @remarks
|
|
18563
|
-
*
|
|
18564
|
-
*
|
|
18565
|
-
*
|
|
18540
|
+
* `ComponentWriter` manages a component's TOOLS.md section writes
|
|
18541
|
+
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
18542
|
+
* on a configurable prime-interval timer cycle.
|
|
18566
18543
|
*/
|
|
18567
18544
|
/**
|
|
18568
|
-
*
|
|
18545
|
+
* Orchestrates managed content writing for a single Jeeves component.
|
|
18569
18546
|
*
|
|
18570
18547
|
* @remarks
|
|
18571
|
-
*
|
|
18572
|
-
*
|
|
18573
|
-
*
|
|
18574
|
-
* 3. `process.cwd()` — last resort
|
|
18575
|
-
*
|
|
18576
|
-
* @param api - The plugin API object provided by the gateway.
|
|
18577
|
-
* @returns Absolute path to the workspace root.
|
|
18548
|
+
* Created via {@link createComponentWriter}. Manages a timer that fires
|
|
18549
|
+
* at the component's prime-interval, calling `generateToolsContent()`
|
|
18550
|
+
* and `refreshPlatformContent()` on each cycle.
|
|
18578
18551
|
*/
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18552
|
+
class ComponentWriter {
|
|
18553
|
+
timer;
|
|
18554
|
+
jitterTimeout;
|
|
18555
|
+
component;
|
|
18556
|
+
configDir;
|
|
18557
|
+
gatewayUrl;
|
|
18558
|
+
pendingCleanups = new Set();
|
|
18559
|
+
/** @internal */
|
|
18560
|
+
constructor(component, options) {
|
|
18561
|
+
this.component = component;
|
|
18562
|
+
this.configDir = getComponentConfigDir(component.name);
|
|
18563
|
+
this.gatewayUrl = options?.gatewayUrl;
|
|
18583
18564
|
}
|
|
18584
|
-
|
|
18585
|
-
|
|
18565
|
+
/** The component's config directory path. */
|
|
18566
|
+
get componentConfigDir() {
|
|
18567
|
+
return this.configDir;
|
|
18568
|
+
}
|
|
18569
|
+
/** Whether the writer timer is currently running or pending its first cycle. */
|
|
18570
|
+
get isRunning() {
|
|
18571
|
+
return this.jitterTimeout !== undefined || this.timer !== undefined;
|
|
18572
|
+
}
|
|
18573
|
+
/**
|
|
18574
|
+
* Start the writer timer.
|
|
18575
|
+
*
|
|
18576
|
+
* @remarks
|
|
18577
|
+
* Delays the first cycle by a random jitter (0 to one full interval) to
|
|
18578
|
+
* spread initial writes across all component plugins and reduce EPERM
|
|
18579
|
+
* contention on startup.
|
|
18580
|
+
*/
|
|
18581
|
+
start() {
|
|
18582
|
+
if (this.isRunning)
|
|
18583
|
+
return;
|
|
18584
|
+
// Random jitter up to one full interval to spread initial writes
|
|
18585
|
+
const intervalMs = this.component.refreshIntervalSeconds * 1000;
|
|
18586
|
+
const jitterMs = Math.floor(Math.random() * intervalMs);
|
|
18587
|
+
this.jitterTimeout = setTimeout(() => {
|
|
18588
|
+
this.jitterTimeout = undefined;
|
|
18589
|
+
void this.cycle();
|
|
18590
|
+
this.timer = setInterval(() => void this.cycle(), intervalMs);
|
|
18591
|
+
}, jitterMs);
|
|
18592
|
+
}
|
|
18593
|
+
/** Stop the writer timer. */
|
|
18594
|
+
stop() {
|
|
18595
|
+
if (this.jitterTimeout) {
|
|
18596
|
+
clearTimeout(this.jitterTimeout);
|
|
18597
|
+
this.jitterTimeout = undefined;
|
|
18598
|
+
}
|
|
18599
|
+
if (this.timer) {
|
|
18600
|
+
clearInterval(this.timer);
|
|
18601
|
+
this.timer = undefined;
|
|
18602
|
+
}
|
|
18603
|
+
}
|
|
18604
|
+
/**
|
|
18605
|
+
* Execute a single write cycle.
|
|
18606
|
+
*
|
|
18607
|
+
* @remarks
|
|
18608
|
+
* 1. Write the component's TOOLS.md section.
|
|
18609
|
+
* 2. Refresh shared platform content (SOUL.md, AGENTS.md, Platform section).
|
|
18610
|
+
* 3. Scan for cleanup flags and escalate if a gateway URL is configured.
|
|
18611
|
+
* 4. Run HEARTBEAT health orchestration.
|
|
18612
|
+
*/
|
|
18613
|
+
async cycle() {
|
|
18614
|
+
try {
|
|
18615
|
+
const workspacePath = getWorkspacePath();
|
|
18616
|
+
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
18617
|
+
// 1. Write the component's TOOLS.md section
|
|
18618
|
+
const toolsContent = this.component.generateToolsContent();
|
|
18619
|
+
await updateManagedSection(toolsPath, toolsContent, {
|
|
18620
|
+
mode: 'section',
|
|
18621
|
+
sectionId: this.component.sectionId,
|
|
18622
|
+
markers: TOOLS_MARKERS,
|
|
18623
|
+
coreVersion: CORE_VERSION,
|
|
18624
|
+
});
|
|
18625
|
+
// 2. Platform content maintenance
|
|
18626
|
+
await refreshPlatformContent({
|
|
18627
|
+
coreVersion: CORE_VERSION,
|
|
18628
|
+
componentName: this.component.name,
|
|
18629
|
+
componentVersion: this.component.version,
|
|
18630
|
+
servicePackage: this.component.servicePackage,
|
|
18631
|
+
pluginPackage: this.component.pluginPackage,
|
|
18632
|
+
});
|
|
18633
|
+
// 3. Cleanup escalation
|
|
18634
|
+
if (this.gatewayUrl) {
|
|
18635
|
+
scanAndEscalateCleanup([
|
|
18636
|
+
{ filePath: toolsPath, markerIdentity: 'TOOLS' },
|
|
18637
|
+
{
|
|
18638
|
+
filePath: join(workspacePath, WORKSPACE_FILES.soul),
|
|
18639
|
+
markerIdentity: 'SOUL',
|
|
18640
|
+
},
|
|
18641
|
+
{
|
|
18642
|
+
filePath: join(workspacePath, WORKSPACE_FILES.agents),
|
|
18643
|
+
markerIdentity: 'AGENTS',
|
|
18644
|
+
},
|
|
18645
|
+
], this.gatewayUrl, this.pendingCleanups);
|
|
18646
|
+
}
|
|
18647
|
+
// 4. HEARTBEAT orchestration
|
|
18648
|
+
await runHeartbeatCycle({
|
|
18649
|
+
workspacePath,
|
|
18650
|
+
coreConfigDir: getCoreConfigDir(),
|
|
18651
|
+
configRoot: getConfigRoot$1(),
|
|
18652
|
+
});
|
|
18653
|
+
}
|
|
18654
|
+
catch (err) {
|
|
18655
|
+
console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${getErrorMessage(err)}`);
|
|
18656
|
+
}
|
|
18586
18657
|
}
|
|
18587
|
-
return process.cwd();
|
|
18588
18658
|
}
|
|
18659
|
+
|
|
18589
18660
|
/**
|
|
18590
|
-
*
|
|
18591
|
-
* plugin config → environment variable → fallback value.
|
|
18661
|
+
* Creates a synchronous content accessor backed by an async data source.
|
|
18592
18662
|
*
|
|
18593
|
-
* @
|
|
18594
|
-
*
|
|
18595
|
-
*
|
|
18596
|
-
*
|
|
18597
|
-
*
|
|
18598
|
-
*
|
|
18663
|
+
* @remarks
|
|
18664
|
+
* Solves the sync/async gap in `JeevesComponentDescriptor.generateToolsContent()`:
|
|
18665
|
+
* the interface is synchronous, but most components fetch live data from
|
|
18666
|
+
* their HTTP service. This utility returns a sync `() => string` that
|
|
18667
|
+
* serves the last successfully fetched value while kicking off a background
|
|
18668
|
+
* refresh on each call.
|
|
18669
|
+
*
|
|
18670
|
+
* First call returns `placeholder`. Subsequent calls return the last
|
|
18671
|
+
* successfully fetched content. If a refresh fails, the previous good
|
|
18672
|
+
* value is retained.
|
|
18673
|
+
*
|
|
18674
|
+
* @example
|
|
18675
|
+
* ```typescript
|
|
18676
|
+
* const getContent = createAsyncContentCache({
|
|
18677
|
+
* fetch: async () => {
|
|
18678
|
+
* const res = await fetch('http://127.0.0.1:1936/status');
|
|
18679
|
+
* return formatWatcherStatus(await res.json());
|
|
18680
|
+
* },
|
|
18681
|
+
* placeholder: '> Initializing watcher status...',
|
|
18682
|
+
* });
|
|
18683
|
+
*
|
|
18684
|
+
* const writer = createComponentWriter({
|
|
18685
|
+
* // ...
|
|
18686
|
+
* generateToolsContent: getContent,
|
|
18687
|
+
* });
|
|
18688
|
+
* ```
|
|
18599
18689
|
*/
|
|
18600
|
-
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18690
|
+
/**
|
|
18691
|
+
* Creates a synchronous content accessor backed by an async data source.
|
|
18692
|
+
*
|
|
18693
|
+
* @param options - Cache configuration.
|
|
18694
|
+
* @returns A sync `() => string` suitable for `generateToolsContent`.
|
|
18695
|
+
*/
|
|
18696
|
+
function createAsyncContentCache(options) {
|
|
18697
|
+
const { fetch: fetchContent, placeholder = '> Initializing...', onError = (err) => {
|
|
18698
|
+
console.warn('[jeeves] async content cache refresh failed:', err);
|
|
18699
|
+
}, } = options;
|
|
18700
|
+
let cached = placeholder;
|
|
18701
|
+
let refreshing = false;
|
|
18702
|
+
return () => {
|
|
18703
|
+
if (!refreshing) {
|
|
18704
|
+
refreshing = true;
|
|
18705
|
+
fetchContent()
|
|
18706
|
+
.then((content) => {
|
|
18707
|
+
cached = content;
|
|
18708
|
+
})
|
|
18709
|
+
.catch(onError)
|
|
18710
|
+
.finally(() => {
|
|
18711
|
+
refreshing = false;
|
|
18712
|
+
});
|
|
18713
|
+
}
|
|
18714
|
+
return cached;
|
|
18715
|
+
};
|
|
18608
18716
|
}
|
|
18717
|
+
|
|
18609
18718
|
/**
|
|
18610
|
-
*
|
|
18611
|
-
* plugin config → environment variable. Returns `undefined` if neither
|
|
18612
|
-
* source provides a value.
|
|
18719
|
+
* Factory function for creating a ComponentWriter from a descriptor.
|
|
18613
18720
|
*
|
|
18614
|
-
* @
|
|
18615
|
-
*
|
|
18616
|
-
*
|
|
18617
|
-
*
|
|
18618
|
-
* @returns The resolved setting value, or `undefined`.
|
|
18721
|
+
* @remarks
|
|
18722
|
+
* Validates the descriptor via Zod schema and creates a ComponentWriter.
|
|
18723
|
+
* Accepts `JeevesComponentDescriptor` (v0.5.0) only. The v0.4.0
|
|
18724
|
+
* `JeevesComponent` interface is no longer accepted.
|
|
18619
18725
|
*/
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18726
|
+
/**
|
|
18727
|
+
* Create a ComponentWriter for a validated component descriptor.
|
|
18728
|
+
*
|
|
18729
|
+
* @remarks
|
|
18730
|
+
* The descriptor is validated via the Zod schema at runtime.
|
|
18731
|
+
* This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
|
|
18732
|
+
*
|
|
18733
|
+
* @param descriptor - The component descriptor to validate and wrap.
|
|
18734
|
+
* @param options - Optional writer configuration (e.g., gatewayUrl for cleanup escalation).
|
|
18735
|
+
* @returns A new `ComponentWriter` instance.
|
|
18736
|
+
* @throws ZodError if the descriptor is invalid.
|
|
18737
|
+
*/
|
|
18738
|
+
function createComponentWriter(descriptor, options) {
|
|
18739
|
+
// Validate via Zod — throws ZodError with detailed messages on failure
|
|
18740
|
+
jeevesComponentDescriptorSchema.parse(descriptor);
|
|
18741
|
+
return new ComponentWriter(descriptor, options);
|
|
18628
18742
|
}
|
|
18629
18743
|
|
|
18630
18744
|
/**
|