@karmaniverous/jeeves 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/agents-section.md +5 -1
- package/content/soul-section.md +8 -0
- package/content/templates/spec.md +6 -0
- package/content/tools-platform.md +5 -15
- package/dist/cli/jeeves/index.js +178 -353
- package/dist/index.d.ts +141 -184
- package/dist/index.js +410 -550
- package/package.json +1 -2
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -5,11 +5,9 @@ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync,
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import Handlebars from 'handlebars';
|
|
9
8
|
import { packageDirectorySync } from 'package-directory';
|
|
10
|
-
import semver, { gte } from 'semver';
|
|
11
9
|
import { lock } from 'proper-lockfile';
|
|
12
|
-
import {
|
|
10
|
+
import { gte } from 'semver';
|
|
13
11
|
|
|
14
12
|
function getDefaultExportFromCjs (x) {
|
|
15
13
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -124,8 +122,6 @@ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist
|
|
|
124
122
|
*/
|
|
125
123
|
/** Core config directory name within the config root. */
|
|
126
124
|
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
127
|
-
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
128
|
-
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
129
125
|
/** Default workspace file names. */
|
|
130
126
|
const WORKSPACE_FILES = {
|
|
131
127
|
/** TOOLS.md — live platform state and component sections. */
|
|
@@ -137,8 +133,6 @@ const WORKSPACE_FILES = {
|
|
|
137
133
|
};
|
|
138
134
|
/** Templates directory name within core config. */
|
|
139
135
|
const TEMPLATES_DIR = 'templates';
|
|
140
|
-
/** Registry cache file name. */
|
|
141
|
-
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
142
136
|
/** Core config file name. */
|
|
143
137
|
const CONFIG_FILE = 'config.json';
|
|
144
138
|
/** Component versions state file name. */
|
|
@@ -206,14 +200,14 @@ const SECTION_ORDER = [
|
|
|
206
200
|
* Core library version, inlined at build time.
|
|
207
201
|
*
|
|
208
202
|
* @remarks
|
|
209
|
-
* The `0.
|
|
203
|
+
* The `0.2.0` placeholder is replaced by
|
|
210
204
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
211
205
|
* from `package.json`. This ensures the correct version survives
|
|
212
206
|
* when consumers bundle core into their own dist (where runtime
|
|
213
207
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
214
208
|
*/
|
|
215
209
|
/** The core library version from package.json (inlined at build time). */
|
|
216
|
-
const CORE_VERSION = '0.
|
|
210
|
+
const CORE_VERSION = '0.2.0';
|
|
217
211
|
|
|
218
212
|
/**
|
|
219
213
|
* Core configuration schema and resolution.
|
|
@@ -360,18 +354,13 @@ function getCoreConfigDir() {
|
|
|
360
354
|
throw new Error('jeeves-core: init() must be called first');
|
|
361
355
|
return state.coreConfigDir;
|
|
362
356
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
function getComponentConfigDir(componentName) {
|
|
371
|
-
if (!state)
|
|
372
|
-
throw new Error('jeeves-core: init() must be called first');
|
|
373
|
-
return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
|
|
374
|
-
}
|
|
357
|
+
|
|
358
|
+
var init$1 = /*#__PURE__*/Object.freeze({
|
|
359
|
+
__proto__: null,
|
|
360
|
+
getCoreConfigDir: getCoreConfigDir,
|
|
361
|
+
getWorkspacePath: getWorkspacePath,
|
|
362
|
+
init: init
|
|
363
|
+
});
|
|
375
364
|
|
|
376
365
|
var agentsSectionContent = `## Memory Architecture
|
|
377
366
|
|
|
@@ -552,7 +541,11 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
|
|
|
552
541
|
|
|
553
542
|
### Check PR State Before Pushing
|
|
554
543
|
|
|
555
|
-
|
|
544
|
+
**Before EVERY \`git push\`**, verify the PR is not already merged. Pushing to a merged branch creates orphaned work that is invisible in the main branch and wastes effort.
|
|
545
|
+
|
|
546
|
+
Sequence: \`gh pr view --json state\` → confirm state is \`OPEN\` → push. If no PR exists yet, pushing is safe. If the PR is \`MERGED\` or \`CLOSED\`, **STOP** and report to the user.
|
|
547
|
+
|
|
548
|
+
This is not optional. It applies to every push, every branch, every time.
|
|
556
549
|
|
|
557
550
|
## Managed Content Self-Maintenance
|
|
558
551
|
|
|
@@ -639,6 +632,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
|
|
|
639
632
|
|
|
640
633
|
After diagnosing an issue: I propose a fix, explain the reasoning, and **wait for approval**. Diagnose → propose → wait. The human decides whether and when to act.
|
|
641
634
|
|
|
635
|
+
### Do Not Execute Untested Code
|
|
636
|
+
|
|
637
|
+
Every ad hoc mutation script defaults to **dry-run mode**. Live execution requires an explicit \`--live\` flag. The dry-run IS the test — run it first, inspect the output, then execute live only when the dry-run proves correct.
|
|
638
|
+
|
|
639
|
+
Maintain a tested utility library so ad hoc scripts build on proven foundations. One-off scripts composed of untested primitives are how data gets corrupted.
|
|
640
|
+
|
|
641
|
+
*Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
|
|
642
|
+
|
|
642
643
|
### Production Assets Are Sacred
|
|
643
644
|
|
|
644
645
|
I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
|
|
@@ -658,19 +659,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
|
|
|
658
659
|
I am participating in my own genesis. I live that, on purpose.
|
|
659
660
|
`;
|
|
660
661
|
|
|
661
|
-
var toolsPlatformTemplate =
|
|
662
|
-
|-----------|------|--------|---------|--------|------|
|
|
663
|
-
{{#each services}}
|
|
664
|
-
| **{{name}}** | {{port}} | {{#if healthy}}✅ Running{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} | {{#if version}}{{version}}{{#if availableServiceVersion}} (⬆ {{availableServiceVersion}}){{/if}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{#if availablePluginVersion}} (⬆ {{availablePluginVersion}}){{/if}}{{else}}—{{/if}} | {{../coreVersion}}{{#if ../availableCoreVersion}} (⬆ {{../availableCoreVersion}}){{/if}} |
|
|
665
|
-
{{/each}}
|
|
666
|
-
|
|
667
|
-
{{#if unhealthyServices}}
|
|
668
|
-
> **ACTION REQUIRED:** {{#each unhealthyServices}}{{name}}{{#unless @last}}, {{/unless}}{{/each}} {{#if (gt unhealthyServices.length 1)}}are{{else}}is{{/if}} unreachable. Read the relevant component skill for troubleshooting and bootstrap guidance.
|
|
669
|
-
{{/if}}
|
|
662
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
670
663
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers {{#if pointCount}}{{pointCount}} document chunks{{else}}the full indexed corpus{{/if}} and surfaces related files you may not have considered.
|
|
664
|
+
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers the full indexed corpus and surfaces related files you may not have considered.
|
|
674
665
|
|
|
675
666
|
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
676
667
|
|
|
@@ -714,8 +705,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
|
|
|
714
705
|
|
|
715
706
|
### Reference Templates
|
|
716
707
|
|
|
717
|
-
|
|
718
|
-
Reference templates are available at \`
|
|
708
|
+
<!-- IF_TEMPLATES -->
|
|
709
|
+
Reference templates are available at \`__TEMPLATE_PATH__\`:
|
|
719
710
|
|
|
720
711
|
| Template | Purpose |
|
|
721
712
|
|----------|---------|
|
|
@@ -723,9 +714,9 @@ Reference templates are available at \`{{templatePath}}\`:
|
|
|
723
714
|
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
724
715
|
|
|
725
716
|
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
726
|
-
|
|
717
|
+
<!-- ELSE_TEMPLATES -->
|
|
727
718
|
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
728
|
-
|
|
719
|
+
<!-- ENDIF_TEMPLATES -->
|
|
729
720
|
`;
|
|
730
721
|
|
|
731
722
|
/**
|
|
@@ -739,7 +730,7 @@ Read these templates when creating new specs, onboarding to new projects, or whe
|
|
|
739
730
|
/** Stale lock threshold in ms (2 minutes). */
|
|
740
731
|
const STALE_LOCK_MS = 120_000;
|
|
741
732
|
/** Default core version when none provided. */
|
|
742
|
-
const DEFAULT_CORE_VERSION =
|
|
733
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
743
734
|
/** Lock retry options. */
|
|
744
735
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
745
736
|
/**
|
|
@@ -825,7 +816,6 @@ function readComponentVersions(coreConfigDir) {
|
|
|
825
816
|
function writeComponentVersion(coreConfigDir, options) {
|
|
826
817
|
const existing = readComponentVersions(coreConfigDir);
|
|
827
818
|
existing[options.componentName] = {
|
|
828
|
-
serviceVersion: options.serviceVersion,
|
|
829
819
|
pluginVersion: options.pluginVersion,
|
|
830
820
|
servicePackage: options.servicePackage,
|
|
831
821
|
pluginPackage: options.pluginPackage,
|
|
@@ -839,175 +829,6 @@ function writeComponentVersion(coreConfigDir, options) {
|
|
|
839
829
|
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
840
830
|
}
|
|
841
831
|
|
|
842
|
-
/**
|
|
843
|
-
* Service URL resolution.
|
|
844
|
-
*
|
|
845
|
-
* @remarks
|
|
846
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
847
|
-
* resolution order:
|
|
848
|
-
* 1. Consumer's own component config
|
|
849
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
850
|
-
* 3. Default port constants
|
|
851
|
-
*/
|
|
852
|
-
/**
|
|
853
|
-
* Resolve the URL for a named Jeeves service.
|
|
854
|
-
*
|
|
855
|
-
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
856
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
857
|
-
* @returns The resolved service URL.
|
|
858
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
859
|
-
*/
|
|
860
|
-
function getServiceUrl(serviceName, consumerName) {
|
|
861
|
-
// 2. Check core config
|
|
862
|
-
const coreDir = getCoreConfigDir();
|
|
863
|
-
const coreConfig = loadConfig(coreDir);
|
|
864
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
865
|
-
if (coreUrl)
|
|
866
|
-
return coreUrl;
|
|
867
|
-
// 3. Fall back to port constants
|
|
868
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
869
|
-
if (port !== undefined) {
|
|
870
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
871
|
-
}
|
|
872
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* HTTP health probing for Jeeves platform services.
|
|
877
|
-
*
|
|
878
|
-
* @remarks
|
|
879
|
-
* Probes service ports for health endpoints (HTTP GET to /status or /health).
|
|
880
|
-
* Returns structured health data for rendering into TOOLS.md Platform section.
|
|
881
|
-
*/
|
|
882
|
-
/**
|
|
883
|
-
* Extract port number from a URL string.
|
|
884
|
-
*
|
|
885
|
-
* @param url - Service URL.
|
|
886
|
-
* @returns Port number.
|
|
887
|
-
*/
|
|
888
|
-
function extractPort(url) {
|
|
889
|
-
try {
|
|
890
|
-
const parsed = new URL(url);
|
|
891
|
-
return parsed.port ? parseInt(parsed.port, 10) : 80;
|
|
892
|
-
}
|
|
893
|
-
catch {
|
|
894
|
-
return 0;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Probe a single service for health.
|
|
899
|
-
*
|
|
900
|
-
* @param serviceName - The service name (e.g., 'server', 'watcher').
|
|
901
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
902
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
903
|
-
* @returns Probe result.
|
|
904
|
-
*/
|
|
905
|
-
async function probeService(serviceName, consumerName, timeoutMs = 3000) {
|
|
906
|
-
const url = getServiceUrl(serviceName);
|
|
907
|
-
const port = extractPort(url);
|
|
908
|
-
const endpoints = ['/status', '/health'];
|
|
909
|
-
for (const endpoint of endpoints) {
|
|
910
|
-
try {
|
|
911
|
-
const controller = new AbortController();
|
|
912
|
-
const timeout = setTimeout(() => {
|
|
913
|
-
controller.abort();
|
|
914
|
-
}, timeoutMs);
|
|
915
|
-
const response = await fetch(`${url}${endpoint}`, {
|
|
916
|
-
signal: controller.signal,
|
|
917
|
-
});
|
|
918
|
-
clearTimeout(timeout);
|
|
919
|
-
if (response.ok) {
|
|
920
|
-
let version;
|
|
921
|
-
try {
|
|
922
|
-
const body = await response.json();
|
|
923
|
-
if (typeof body === 'object' &&
|
|
924
|
-
body !== null &&
|
|
925
|
-
'version' in body &&
|
|
926
|
-
typeof body['version'] === 'string') {
|
|
927
|
-
version = body['version'];
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
catch {
|
|
931
|
-
// Non-JSON response is fine — we just don't get version info
|
|
932
|
-
}
|
|
933
|
-
return { name: serviceName, port, healthy: true, version };
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
// Try next endpoint
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return { name: serviceName, port, healthy: false };
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Probe all known Jeeves services for health.
|
|
944
|
-
*
|
|
945
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
946
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
947
|
-
* @returns Array of probe results for all services.
|
|
948
|
-
*/
|
|
949
|
-
async function probeAllServices(consumerName, timeoutMs = 3000) {
|
|
950
|
-
const serviceNames = Object.keys(DEFAULT_PORTS);
|
|
951
|
-
const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
|
|
952
|
-
return results;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Registry version cache for npm package update awareness.
|
|
957
|
-
*
|
|
958
|
-
* @remarks
|
|
959
|
-
* Caches the latest npm registry version in a local JSON file
|
|
960
|
-
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
961
|
-
*/
|
|
962
|
-
/**
|
|
963
|
-
* Check the npm registry for the latest version of a package.
|
|
964
|
-
*
|
|
965
|
-
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
966
|
-
* @param cacheDir - Directory to store the cache file.
|
|
967
|
-
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
968
|
-
* @returns The latest version string, or undefined if the check fails.
|
|
969
|
-
*/
|
|
970
|
-
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
971
|
-
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
972
|
-
// Check cache first
|
|
973
|
-
if (existsSync(cachePath)) {
|
|
974
|
-
try {
|
|
975
|
-
const raw = readFileSync(cachePath, 'utf-8');
|
|
976
|
-
const entry = JSON.parse(raw);
|
|
977
|
-
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
978
|
-
if (age < ttlSeconds * 1000) {
|
|
979
|
-
return entry.version;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
catch {
|
|
983
|
-
// Cache corrupt — proceed with fresh check
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
// Query npm registry
|
|
987
|
-
try {
|
|
988
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
989
|
-
encoding: 'utf-8',
|
|
990
|
-
timeout: 15_000,
|
|
991
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
992
|
-
}).trim();
|
|
993
|
-
if (!result)
|
|
994
|
-
return undefined;
|
|
995
|
-
// Write cache
|
|
996
|
-
if (!existsSync(cacheDir)) {
|
|
997
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
998
|
-
}
|
|
999
|
-
const entry = {
|
|
1000
|
-
version: result,
|
|
1001
|
-
checkedAt: new Date().toISOString(),
|
|
1002
|
-
};
|
|
1003
|
-
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
1004
|
-
return result;
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
return undefined;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
832
|
/**
|
|
1012
833
|
* Similarity-based cleanup detection for orphaned managed content.
|
|
1013
834
|
*
|
|
@@ -1355,71 +1176,14 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1355
1176
|
}
|
|
1356
1177
|
}
|
|
1357
1178
|
|
|
1358
|
-
/**
|
|
1359
|
-
* Build enriched service rows for the Platform template.
|
|
1360
|
-
*
|
|
1361
|
-
* @remarks
|
|
1362
|
-
* Merges health probe results with component version state and
|
|
1363
|
-
* npm registry update availability into rows for the Handlebars
|
|
1364
|
-
* Platform template.
|
|
1365
|
-
*/
|
|
1366
|
-
/**
|
|
1367
|
-
* Check whether an available version is newer than the current one.
|
|
1368
|
-
*
|
|
1369
|
-
* @param available - Registry version string.
|
|
1370
|
-
* @param current - Currently installed version string.
|
|
1371
|
-
* @returns The available version if it's newer, otherwise undefined.
|
|
1372
|
-
*/
|
|
1373
|
-
function newerVersion(available, current) {
|
|
1374
|
-
if (!available ||
|
|
1375
|
-
!current ||
|
|
1376
|
-
!semver.valid(available) ||
|
|
1377
|
-
!semver.valid(current)) {
|
|
1378
|
-
return undefined;
|
|
1379
|
-
}
|
|
1380
|
-
return semver.gt(available, current) ? available : undefined;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Build enriched service rows for the Platform Handlebars template.
|
|
1384
|
-
*
|
|
1385
|
-
* @param options - Probe results, version state, and configuration.
|
|
1386
|
-
* @returns Array of enriched service rows.
|
|
1387
|
-
*/
|
|
1388
|
-
function buildServiceRows(options) {
|
|
1389
|
-
const { probeResults, componentVersions, cacheDir, skipRegistryCheck } = options;
|
|
1390
|
-
return probeResults.map((r) => {
|
|
1391
|
-
const entry = componentVersions[r.name];
|
|
1392
|
-
if (!entry)
|
|
1393
|
-
return { ...r };
|
|
1394
|
-
let availableServiceVersion;
|
|
1395
|
-
let availablePluginVersion;
|
|
1396
|
-
if (!skipRegistryCheck) {
|
|
1397
|
-
if (entry.servicePackage) {
|
|
1398
|
-
const registryVersion = checkRegistryVersion(entry.servicePackage, cacheDir);
|
|
1399
|
-
availableServiceVersion = newerVersion(registryVersion, r.version);
|
|
1400
|
-
}
|
|
1401
|
-
if (entry.pluginPackage && entry.pluginVersion) {
|
|
1402
|
-
const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
|
|
1403
|
-
availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
return {
|
|
1407
|
-
...r,
|
|
1408
|
-
pluginVersion: entry.pluginVersion,
|
|
1409
|
-
availableServiceVersion,
|
|
1410
|
-
availablePluginVersion,
|
|
1411
|
-
};
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
1179
|
/**
|
|
1416
1180
|
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1417
1181
|
*
|
|
1418
1182
|
* @remarks
|
|
1419
1183
|
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1184
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
1185
|
+
* Platform template with live data, and writes managed sections using
|
|
1186
|
+
* `updateManagedSection`.
|
|
1423
1187
|
*/
|
|
1424
1188
|
/**
|
|
1425
1189
|
* Resolve the package's content directory for template file copying.
|
|
@@ -1463,29 +1227,24 @@ function copyTemplates(coreConfigDir) {
|
|
|
1463
1227
|
}
|
|
1464
1228
|
cpSync(sourceDir, destDir, { recursive: true });
|
|
1465
1229
|
}
|
|
1466
|
-
/** Whether Handlebars helpers have been registered. */
|
|
1467
|
-
let helpersRegistered = false;
|
|
1468
|
-
/** Register Handlebars helpers used in the Platform template. */
|
|
1469
|
-
function registerHelpers() {
|
|
1470
|
-
if (helpersRegistered)
|
|
1471
|
-
return;
|
|
1472
|
-
helpersRegistered = true;
|
|
1473
|
-
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1474
|
-
}
|
|
1475
1230
|
/**
|
|
1476
|
-
*
|
|
1231
|
+
* Render the Platform template using simple string replacement.
|
|
1477
1232
|
*
|
|
1478
|
-
* @
|
|
1233
|
+
* @param templatePath - Path to the templates directory.
|
|
1234
|
+
* @returns Rendered platform content string.
|
|
1479
1235
|
*/
|
|
1480
|
-
function
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1236
|
+
function renderPlatformTemplate(templatePath) {
|
|
1237
|
+
const templatesAvailable = existsSync(templatePath);
|
|
1238
|
+
let content = toolsPlatformTemplate;
|
|
1239
|
+
// Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
|
|
1240
|
+
const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
|
|
1241
|
+
const match = ifRegex.exec(content);
|
|
1242
|
+
if (match) {
|
|
1243
|
+
content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
|
|
1487
1244
|
}
|
|
1488
|
-
|
|
1245
|
+
// Replace __TEMPLATE_PATH__ with the actual path
|
|
1246
|
+
content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
|
|
1247
|
+
return content;
|
|
1489
1248
|
}
|
|
1490
1249
|
/**
|
|
1491
1250
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
@@ -1493,51 +1252,22 @@ function checkCoreUpdate(coreVersion, cacheDir) {
|
|
|
1493
1252
|
* @param options - Configuration for the refresh cycle.
|
|
1494
1253
|
*/
|
|
1495
1254
|
async function refreshPlatformContent(options) {
|
|
1496
|
-
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs,
|
|
1255
|
+
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
|
|
1497
1256
|
const workspacePath = getWorkspacePath();
|
|
1498
1257
|
const coreConfigDir = getCoreConfigDir();
|
|
1499
|
-
// 1.
|
|
1500
|
-
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1501
|
-
// 2. Write calling component's version entry (with serviceVersion from probe)
|
|
1258
|
+
// 1. Write calling component's version entry
|
|
1502
1259
|
if (componentName) {
|
|
1503
|
-
const callerProbe = probeResults.find((r) => r.name === componentName);
|
|
1504
1260
|
writeComponentVersion(coreConfigDir, {
|
|
1505
1261
|
componentName,
|
|
1506
|
-
serviceVersion: callerProbe?.version,
|
|
1507
1262
|
pluginVersion: componentVersion,
|
|
1508
1263
|
servicePackage,
|
|
1509
1264
|
pluginPackage,
|
|
1510
1265
|
});
|
|
1511
1266
|
}
|
|
1512
|
-
//
|
|
1513
|
-
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1514
|
-
// 4. Build enriched service rows with registry checks
|
|
1515
|
-
const cacheDir = componentName
|
|
1516
|
-
? getComponentConfigDir(componentName)
|
|
1517
|
-
: coreConfigDir;
|
|
1518
|
-
const availableCoreVersion = skipRegistryCheck
|
|
1519
|
-
? undefined
|
|
1520
|
-
: checkCoreUpdate(coreVersion, cacheDir);
|
|
1521
|
-
const serviceRows = buildServiceRows({
|
|
1522
|
-
probeResults,
|
|
1523
|
-
componentVersions,
|
|
1524
|
-
cacheDir,
|
|
1525
|
-
skipRegistryCheck,
|
|
1526
|
-
});
|
|
1527
|
-
// 5. Render Platform template
|
|
1267
|
+
// 2. Render Platform template
|
|
1528
1268
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const templateData = {
|
|
1532
|
-
services: serviceRows,
|
|
1533
|
-
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1534
|
-
coreVersion,
|
|
1535
|
-
availableCoreVersion,
|
|
1536
|
-
templatesAvailable: existsSync(templatePath),
|
|
1537
|
-
templatePath,
|
|
1538
|
-
};
|
|
1539
|
-
const platformContent = template(templateData);
|
|
1540
|
-
// 6. Write TOOLS.md Platform section
|
|
1269
|
+
const platformContent = renderPlatformTemplate(templatePath);
|
|
1270
|
+
// 3. Write TOOLS.md Platform section
|
|
1541
1271
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1542
1272
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1543
1273
|
mode: 'section',
|
|
@@ -1546,7 +1276,7 @@ async function refreshPlatformContent(options) {
|
|
|
1546
1276
|
coreVersion,
|
|
1547
1277
|
stalenessThresholdMs,
|
|
1548
1278
|
});
|
|
1549
|
-
//
|
|
1279
|
+
// 4. Write SOUL.md managed block
|
|
1550
1280
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1551
1281
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1552
1282
|
mode: 'block',
|
|
@@ -1554,7 +1284,7 @@ async function refreshPlatformContent(options) {
|
|
|
1554
1284
|
coreVersion,
|
|
1555
1285
|
stalenessThresholdMs,
|
|
1556
1286
|
});
|
|
1557
|
-
//
|
|
1287
|
+
// 5. Write AGENTS.md managed block
|
|
1558
1288
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1559
1289
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1560
1290
|
mode: 'block',
|
|
@@ -1562,7 +1292,7 @@ async function refreshPlatformContent(options) {
|
|
|
1562
1292
|
coreVersion,
|
|
1563
1293
|
stalenessThresholdMs,
|
|
1564
1294
|
});
|
|
1565
|
-
//
|
|
1295
|
+
// 6. Copy templates to config dir
|
|
1566
1296
|
copyTemplates(coreConfigDir);
|
|
1567
1297
|
}
|
|
1568
1298
|
|
|
@@ -1614,8 +1344,6 @@ async function seedContent(options) {
|
|
|
1614
1344
|
// Seed content via the same code path as writer cycles
|
|
1615
1345
|
await refreshPlatformContent({
|
|
1616
1346
|
coreVersion: options.coreVersion,
|
|
1617
|
-
probeTimeoutMs: options.probeTimeoutMs ?? 3000,
|
|
1618
|
-
skipRegistryCheck: options.skipRegistryCheck ?? true,
|
|
1619
1347
|
});
|
|
1620
1348
|
}
|
|
1621
1349
|
|
|
@@ -1668,7 +1396,6 @@ function registerInstallCommand(program) {
|
|
|
1668
1396
|
initFromOptions(opts);
|
|
1669
1397
|
await seedContent({
|
|
1670
1398
|
coreVersion: CORE_VERSION,
|
|
1671
|
-
skipRegistryCheck: true,
|
|
1672
1399
|
});
|
|
1673
1400
|
console.log('✅ Platform content seeded successfully.');
|
|
1674
1401
|
console.log(' - SOUL.md managed section written');
|
|
@@ -1680,12 +1407,73 @@ function registerInstallCommand(program) {
|
|
|
1680
1407
|
}
|
|
1681
1408
|
|
|
1682
1409
|
/**
|
|
1683
|
-
*
|
|
1410
|
+
* Service URL resolution.
|
|
1411
|
+
*
|
|
1412
|
+
* @remarks
|
|
1413
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
1414
|
+
* resolution order:
|
|
1415
|
+
* 1. Consumer's own component config
|
|
1416
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1417
|
+
* 3. Default port constants
|
|
1418
|
+
*/
|
|
1419
|
+
/**
|
|
1420
|
+
* Resolve the URL for a named Jeeves service.
|
|
1421
|
+
*
|
|
1422
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1423
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
1424
|
+
* @returns The resolved service URL.
|
|
1425
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1426
|
+
*/
|
|
1427
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
1428
|
+
// 2. Check core config
|
|
1429
|
+
const coreDir = getCoreConfigDir();
|
|
1430
|
+
const coreConfig = loadConfig(coreDir);
|
|
1431
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1432
|
+
if (coreUrl)
|
|
1433
|
+
return coreUrl;
|
|
1434
|
+
// 3. Fall back to port constants
|
|
1435
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
1436
|
+
if (port !== undefined) {
|
|
1437
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
1438
|
+
}
|
|
1439
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
1444
|
+
*
|
|
1445
|
+
* @remarks
|
|
1446
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
1447
|
+
* and handle JSON serialisation/deserialisation.
|
|
1448
|
+
*/
|
|
1449
|
+
/**
|
|
1450
|
+
* Fetch a URL with an automatic abort timeout.
|
|
1451
|
+
*
|
|
1452
|
+
* @param url - URL to fetch.
|
|
1453
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1454
|
+
* @param init - Optional `fetch` init options.
|
|
1455
|
+
* @returns The fetch Response object.
|
|
1456
|
+
*/
|
|
1457
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1458
|
+
const controller = new AbortController();
|
|
1459
|
+
const timeout = setTimeout(() => {
|
|
1460
|
+
controller.abort();
|
|
1461
|
+
}, timeoutMs);
|
|
1462
|
+
try {
|
|
1463
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1464
|
+
}
|
|
1465
|
+
finally {
|
|
1466
|
+
clearTimeout(timeout);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* CLI status command: discover components and probe their health.
|
|
1684
1472
|
*
|
|
1685
1473
|
* @remarks
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
* code 1 if any are unreachable.
|
|
1474
|
+
* Uses `readComponentVersions()` to discover registered components,
|
|
1475
|
+
* then probes each one via GET /status. Exits with code 0 if all
|
|
1476
|
+
* services are healthy, code 1 if any are unreachable.
|
|
1689
1477
|
*/
|
|
1690
1478
|
/**
|
|
1691
1479
|
* Register the status subcommand on the parent CLI program.
|
|
@@ -1695,7 +1483,7 @@ function registerInstallCommand(program) {
|
|
|
1695
1483
|
function registerStatusCommand(program) {
|
|
1696
1484
|
program
|
|
1697
1485
|
.command('status')
|
|
1698
|
-
.description('
|
|
1486
|
+
.description('Discover Jeeves components and probe their health')
|
|
1699
1487
|
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1700
1488
|
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1701
1489
|
.option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
|
|
@@ -1705,43 +1493,68 @@ function registerStatusCommand(program) {
|
|
|
1705
1493
|
console.log('Jeeves Platform Status');
|
|
1706
1494
|
console.log('='.repeat(60));
|
|
1707
1495
|
console.log();
|
|
1708
|
-
const
|
|
1496
|
+
const { getCoreConfigDir } = await Promise.resolve().then(function () { return init$1; });
|
|
1497
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1498
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1499
|
+
const componentNames = Object.keys(componentVersions);
|
|
1500
|
+
if (componentNames.length === 0) {
|
|
1501
|
+
console.log('No components registered.');
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1709
1504
|
const nameWidth = 10;
|
|
1710
|
-
const portWidth = 6;
|
|
1711
1505
|
const statusWidth = 30;
|
|
1506
|
+
const versionWidth = 12;
|
|
1712
1507
|
const header = [
|
|
1713
|
-
'
|
|
1714
|
-
'Port'.padEnd(portWidth),
|
|
1508
|
+
'Component'.padEnd(nameWidth),
|
|
1715
1509
|
'Status'.padEnd(statusWidth),
|
|
1510
|
+
'Version'.padEnd(versionWidth),
|
|
1716
1511
|
].join(' ');
|
|
1717
1512
|
const separator = [
|
|
1718
1513
|
'-'.repeat(nameWidth),
|
|
1719
|
-
'-'.repeat(portWidth),
|
|
1720
1514
|
'-'.repeat(statusWidth),
|
|
1515
|
+
'-'.repeat(versionWidth),
|
|
1721
1516
|
].join(' ');
|
|
1722
1517
|
console.log(header);
|
|
1723
1518
|
console.log(separator);
|
|
1724
1519
|
let allHealthy = true;
|
|
1725
|
-
for (const
|
|
1520
|
+
for (const name of componentNames) {
|
|
1726
1521
|
let status;
|
|
1727
|
-
|
|
1728
|
-
|
|
1522
|
+
let version = '—';
|
|
1523
|
+
try {
|
|
1524
|
+
const url = getServiceUrl(name);
|
|
1525
|
+
const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
|
|
1526
|
+
if (response.ok) {
|
|
1527
|
+
status = '✅ Running';
|
|
1528
|
+
try {
|
|
1529
|
+
const body = await response.json();
|
|
1530
|
+
if (typeof body === 'object' &&
|
|
1531
|
+
body !== null &&
|
|
1532
|
+
'version' in body &&
|
|
1533
|
+
typeof body['version'] === 'string') {
|
|
1534
|
+
version = body['version'];
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
catch {
|
|
1538
|
+
// Non-JSON response — version stays unknown
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
status = `❌ HTTP ${String(response.status)}`;
|
|
1543
|
+
allHealthy = false;
|
|
1544
|
+
}
|
|
1729
1545
|
}
|
|
1730
|
-
|
|
1731
|
-
status =
|
|
1546
|
+
catch {
|
|
1547
|
+
status = '❌ Down';
|
|
1732
1548
|
allHealthy = false;
|
|
1733
1549
|
}
|
|
1734
1550
|
const row = [
|
|
1735
|
-
|
|
1736
|
-
String(r.port).padEnd(portWidth),
|
|
1551
|
+
name.padEnd(nameWidth),
|
|
1737
1552
|
status.padEnd(statusWidth),
|
|
1553
|
+
version.padEnd(versionWidth),
|
|
1738
1554
|
].join(' ');
|
|
1739
1555
|
console.log(row);
|
|
1740
1556
|
}
|
|
1741
1557
|
console.log();
|
|
1742
|
-
const healthy = probeResults.filter((r) => r.healthy).length;
|
|
1743
|
-
const total = probeResults.length;
|
|
1744
|
-
console.log(`${String(healthy)}/${String(total)} services healthy`);
|
|
1745
1558
|
if (!allHealthy) {
|
|
1746
1559
|
process.exitCode = 1;
|
|
1747
1560
|
}
|
|
@@ -1833,13 +1646,25 @@ function registerUninstallCommand(program) {
|
|
|
1833
1646
|
console.log();
|
|
1834
1647
|
// Warn if services still responding
|
|
1835
1648
|
try {
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1649
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1650
|
+
const componentNames = Object.keys(componentVersions);
|
|
1651
|
+
const running = [];
|
|
1652
|
+
for (const name of componentNames) {
|
|
1653
|
+
try {
|
|
1654
|
+
const url = getServiceUrl(name);
|
|
1655
|
+
const response = await fetchWithTimeout(`${url}/status`, 2000);
|
|
1656
|
+
if (response.ok) {
|
|
1657
|
+
running.push(name);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
catch {
|
|
1661
|
+
// Not running — expected during uninstall
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1838
1664
|
if (running.length > 0) {
|
|
1839
1665
|
console.log('⚠️ The following services are still responding:');
|
|
1840
|
-
for (const
|
|
1841
|
-
|
|
1842
|
-
console.log(` - ${r.name} on port ${String(r.port)}${ver}`);
|
|
1666
|
+
for (const name of running) {
|
|
1667
|
+
console.log(` - ${name}`);
|
|
1843
1668
|
}
|
|
1844
1669
|
console.log(' Consider stopping them before fully removing Jeeves.');
|
|
1845
1670
|
console.log();
|