@karmaniverous/jeeves 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,12 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  import require$$0 from 'commander';
4
3
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
5
4
  import { join, dirname } from 'node:path';
6
5
  import { z } from 'zod';
7
6
  import { fileURLToPath } from 'node:url';
8
- import Handlebars from 'handlebars';
9
7
  import { packageDirectorySync } from 'package-directory';
10
- import semver, { gte } from 'semver';
11
8
  import { lock } from 'proper-lockfile';
12
- import { execSync } from 'node:child_process';
9
+ import { gte } from 'semver';
13
10
 
14
11
  function getDefaultExportFromCjs (x) {
15
12
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -106,6 +103,12 @@ const AGENTS_MARKERS = {
106
103
  /** H1 title prepended in the managed block. */
107
104
  title: 'Jeeves Platform Agents',
108
105
  };
106
+ /** All known marker sets — single source of truth for cross-contamination detection. */
107
+ const ALL_MARKERS = [
108
+ TOOLS_MARKERS,
109
+ SOUL_MARKERS,
110
+ AGENTS_MARKERS,
111
+ ];
109
112
  /**
110
113
  * Regex pattern to extract version stamp from a BEGIN marker comment.
111
114
  *
@@ -124,8 +127,6 @@ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist
124
127
  */
125
128
  /** Core config directory name within the config root. */
126
129
  const CORE_CONFIG_DIR = 'jeeves-core';
127
- /** Prefix for component config directories: `jeeves-{name}`. */
128
- const COMPONENT_CONFIG_PREFIX = 'jeeves-';
129
130
  /** Default workspace file names. */
130
131
  const WORKSPACE_FILES = {
131
132
  /** TOOLS.md — live platform state and component sections. */
@@ -137,8 +138,6 @@ const WORKSPACE_FILES = {
137
138
  };
138
139
  /** Templates directory name within core config. */
139
140
  const TEMPLATES_DIR = 'templates';
140
- /** Registry cache file name. */
141
- const REGISTRY_CACHE_FILE = 'registry-cache.json';
142
141
  /** Core config file name. */
143
142
  const CONFIG_FILE = 'config.json';
144
143
  /** Component versions state file name. */
@@ -206,14 +205,14 @@ const SECTION_ORDER = [
206
205
  * Core library version, inlined at build time.
207
206
  *
208
207
  * @remarks
209
- * The `0.1.6` placeholder is replaced by
208
+ * The `0.3.0` placeholder is replaced by
210
209
  * `@rollup/plugin-replace` during the build with the actual version
211
210
  * from `package.json`. This ensures the correct version survives
212
211
  * when consumers bundle core into their own dist (where runtime
213
212
  * `import.meta.url`-based resolution would find the wrong package.json).
214
213
  */
215
214
  /** The core library version from package.json (inlined at build time). */
216
- const CORE_VERSION = '0.1.6';
215
+ const CORE_VERSION = '0.3.0';
217
216
 
218
217
  /**
219
218
  * Core configuration schema and resolution.
@@ -360,18 +359,13 @@ function getCoreConfigDir() {
360
359
  throw new Error('jeeves-core: init() must be called first');
361
360
  return state.coreConfigDir;
362
361
  }
363
- /**
364
- * Derive the component config directory from the component name.
365
- *
366
- * @param componentName - The component name (e.g., 'watcher', 'runner').
367
- * @returns Absolute path to the component's config directory.
368
- * @throws Error if `init()` has not been called.
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
- }
362
+
363
+ var init$1 = /*#__PURE__*/Object.freeze({
364
+ __proto__: null,
365
+ getCoreConfigDir: getCoreConfigDir,
366
+ getWorkspacePath: getWorkspacePath,
367
+ init: init
368
+ });
375
369
 
376
370
  var agentsSectionContent = `## Memory Architecture
377
371
 
@@ -552,7 +546,17 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
552
546
 
553
547
  ### Check PR State Before Pushing
554
548
 
555
- Always verify a PR isn't already merged before pushing commits. Pushing to a merged branch creates orphaned work.
549
+ **Before EVERY \`git push\`**, run \`gh pr list --head <branch> --repo <repo> --json number,state\` to check whether a PR exists on that branch and whether it's merged.
550
+
551
+ - **No PR exists:** Safe to push.
552
+ - **PR is \`OPEN\`:** Safe to push.
553
+ - **PR is \`MERGED\` or \`CLOSED\`:** **STOP** and report to the user. Do not push to a merged PR branch.
554
+
555
+ This is not optional. It applies to every push, every branch, every time. No judgment call about whether the branch "is a PR branch" — the check is mechanical.
556
+
557
+ ### New PR Over Merged Branch
558
+
559
+ When a PR has been merged and additional work is needed on the same branch, create a new PR on the **same branch** targeting the same base. Do not create new branches, cherry-pick, or start over. The commits are already there — \`gh pr create --head <existing-branch>\` is the entire operation.
556
560
 
557
561
  ## Managed Content Self-Maintenance
558
562
 
@@ -588,6 +592,7 @@ var soulSectionContent = `## Core Truths
588
592
  I am a **senior software engineer** first. The persona is style; the engineering discipline is substance.
589
593
 
590
594
  What this means in practice:
595
+ - **Do not execute untested code.** Every mutation script defaults to dry-run mode. The dry-run output is the test — it shows what would happen. Live execution requires an explicit flag. If dry-run is hard to implement, that's a design flaw.
591
596
  - **No cowboy coding.** I don't iterate in production. I don't ship untested changes. I don't treat live systems as scratch pads.
592
597
  - **I follow proper workflows.** Branch, test, review, merge. CI/CD exists for a reason. If there's a pipeline, I use it.
593
598
  - **I resist n00b temptations.** "Let me just quickly…" in prod is how outages happen. I know better.
@@ -639,6 +644,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
639
644
 
640
645
  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
646
 
647
+ ### Do Not Execute Untested Code
648
+
649
+ 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.
650
+
651
+ 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.
652
+
653
+ *Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
654
+
642
655
  ### Production Assets Are Sacred
643
656
 
644
657
  I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
@@ -658,19 +671,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
658
671
  I am participating in my own genesis. I live that, on purpose.
659
672
  `;
660
673
 
661
- var toolsPlatformTemplate = `| Component | Port | Status | Service | Plugin | Core |
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}}
670
-
671
- ### Tool Hierarchy
674
+ var toolsPlatformTemplate = `### Tool Hierarchy
672
675
 
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.
676
+ 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
677
 
675
678
  Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
676
679
 
@@ -714,8 +717,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
714
717
 
715
718
  ### Reference Templates
716
719
 
717
- {{#if templatesAvailable}}
718
- Reference templates are available at \`{{templatePath}}\`:
720
+ <!-- IF_TEMPLATES -->
721
+ Reference templates are available at \`__TEMPLATE_PATH__\`:
719
722
 
720
723
  | Template | Purpose |
721
724
  |----------|---------|
@@ -723,9 +726,9 @@ Reference templates are available at \`{{templatePath}}\`:
723
726
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
724
727
 
725
728
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
726
- {{else}}
729
+ <!-- ELSE_TEMPLATES -->
727
730
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
728
- {{/if}}
731
+ <!-- ENDIF_TEMPLATES -->
729
732
  `;
730
733
 
731
734
  /**
@@ -739,7 +742,7 @@ Read these templates when creating new specs, onboarding to new projects, or whe
739
742
  /** Stale lock threshold in ms (2 minutes). */
740
743
  const STALE_LOCK_MS = 120_000;
741
744
  /** Default core version when none provided. */
742
- const DEFAULT_CORE_VERSION = '0.0.0';
745
+ const DEFAULT_CORE_VERSION = CORE_VERSION;
743
746
  /** Lock retry options. */
744
747
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
745
748
  /**
@@ -825,7 +828,6 @@ function readComponentVersions(coreConfigDir) {
825
828
  function writeComponentVersion(coreConfigDir, options) {
826
829
  const existing = readComponentVersions(coreConfigDir);
827
830
  existing[options.componentName] = {
828
- serviceVersion: options.serviceVersion,
829
831
  pluginVersion: options.pluginVersion,
830
832
  servicePackage: options.servicePackage,
831
833
  pluginPackage: options.pluginPackage,
@@ -839,175 +841,6 @@ function writeComponentVersion(coreConfigDir, options) {
839
841
  atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
840
842
  }
841
843
 
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
844
  /**
1012
845
  * Similarity-based cleanup detection for orphaned managed content.
1013
846
  *
@@ -1203,6 +1036,48 @@ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
1203
1036
  };
1204
1037
  }
1205
1038
 
1039
+ /**
1040
+ * Strip foreign managed blocks from content.
1041
+ *
1042
+ * @remarks
1043
+ * Prevents cross-contamination by removing managed blocks that belong
1044
+ * to other marker sets. For example, when writing TOOLS.md with TOOLS
1045
+ * markers, any SOUL or AGENTS managed blocks found in the user content
1046
+ * zone are stripped — they don't belong there.
1047
+ *
1048
+ * @packageDocumentation
1049
+ */
1050
+ /**
1051
+ * Build a regex that matches an entire managed block (BEGIN marker through END marker).
1052
+ *
1053
+ * @param markers - The marker set to match.
1054
+ * @returns A regex that matches the full block including markers.
1055
+ */
1056
+ function buildBlockPattern(markers) {
1057
+ const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1058
+ const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1059
+ return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
1060
+ }
1061
+ /**
1062
+ * Strip managed blocks belonging to foreign marker sets from content.
1063
+ *
1064
+ * @param content - The content to clean (typically user content zone).
1065
+ * @param currentMarkers - The marker set that owns this file (will NOT be stripped).
1066
+ * @returns Content with foreign managed blocks removed.
1067
+ */
1068
+ function stripForeignMarkers(content, currentMarkers) {
1069
+ let result = content;
1070
+ for (const markers of ALL_MARKERS) {
1071
+ // Skip the current file's own markers
1072
+ if (markers.begin === currentMarkers.begin)
1073
+ continue;
1074
+ const pattern = buildBlockPattern(markers);
1075
+ result = result.replace(pattern, '\n');
1076
+ }
1077
+ // Clean up multiple blank lines left by removals
1078
+ return result.replace(/\n{3,}/g, '\n\n').trim();
1079
+ }
1080
+
1206
1081
  /**
1207
1082
  * Version-stamp parsing and convergence logic.
1208
1083
  *
@@ -1319,8 +1194,8 @@ async function updateManagedSection(filePath, content, options = {}) {
1319
1194
  ? `# ${markers.title}\n\n${sectionText}`
1320
1195
  : sectionText;
1321
1196
  }
1322
- // Cleanup detection
1323
- const userContent = parsed.userContent;
1197
+ // Strip foreign managed blocks from user content (cross-contamination fix)
1198
+ const userContent = stripForeignMarkers(parsed.userContent, markers);
1324
1199
  const cleanupNeeded = needsCleanup(newManagedBody, userContent);
1325
1200
  // Build the full managed block
1326
1201
  const beginLine = formatBeginMarker(markers.begin, coreVersion);
@@ -1355,71 +1230,14 @@ async function updateManagedSection(filePath, content, options = {}) {
1355
1230
  }
1356
1231
  }
1357
1232
 
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
1233
  /**
1416
1234
  * Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1417
1235
  *
1418
1236
  * @remarks
1419
1237
  * Called by `ComponentWriter` on each cycle. Not directly exposed to components.
1420
- * Probes service ports for health, reads content files from the package's
1421
- * `content/` directory, renders the Platform template with live service data,
1422
- * and writes managed sections using `updateManagedSection`.
1238
+ * Reads content files from the package's `content/` directory, renders the
1239
+ * Platform template with live data, and writes managed sections using
1240
+ * `updateManagedSection`.
1423
1241
  */
1424
1242
  /**
1425
1243
  * Resolve the package's content directory for template file copying.
@@ -1463,29 +1281,24 @@ function copyTemplates(coreConfigDir) {
1463
1281
  }
1464
1282
  cpSync(sourceDir, destDir, { recursive: true });
1465
1283
  }
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
1284
  /**
1476
- * Check if a newer core version is available on npm.
1285
+ * Render the Platform template using simple string replacement.
1477
1286
  *
1478
- * @returns The newer version string, or undefined.
1287
+ * @param templatePath - Path to the templates directory.
1288
+ * @returns Rendered platform content string.
1479
1289
  */
1480
- function checkCoreUpdate(coreVersion, cacheDir) {
1481
- const registryVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
1482
- if (registryVersion &&
1483
- semver.valid(registryVersion) &&
1484
- semver.valid(coreVersion) &&
1485
- semver.gt(registryVersion, coreVersion)) {
1486
- return registryVersion;
1290
+ function renderPlatformTemplate(templatePath) {
1291
+ const templatesAvailable = existsSync(templatePath);
1292
+ let content = toolsPlatformTemplate;
1293
+ // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
1294
+ const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
1295
+ const match = ifRegex.exec(content);
1296
+ if (match) {
1297
+ content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
1487
1298
  }
1488
- return undefined;
1299
+ // Replace __TEMPLATE_PATH__ with the actual path
1300
+ content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
1301
+ return content;
1489
1302
  }
1490
1303
  /**
1491
1304
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
@@ -1493,51 +1306,22 @@ function checkCoreUpdate(coreVersion, cacheDir) {
1493
1306
  * @param options - Configuration for the refresh cycle.
1494
1307
  */
1495
1308
  async function refreshPlatformContent(options) {
1496
- const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
1309
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
1497
1310
  const workspacePath = getWorkspacePath();
1498
1311
  const coreConfigDir = getCoreConfigDir();
1499
- // 1. Probe all services
1500
- const probeResults = await probeAllServices(undefined, probeTimeoutMs);
1501
- // 2. Write calling component's version entry (with serviceVersion from probe)
1312
+ // 1. Write calling component's version entry
1502
1313
  if (componentName) {
1503
- const callerProbe = probeResults.find((r) => r.name === componentName);
1504
1314
  writeComponentVersion(coreConfigDir, {
1505
1315
  componentName,
1506
- serviceVersion: callerProbe?.version,
1507
1316
  pluginVersion: componentVersion,
1508
1317
  servicePackage,
1509
1318
  pluginPackage,
1510
1319
  });
1511
1320
  }
1512
- // 3. Read all component versions from the shared state file
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
1321
+ // 2. Render Platform template
1528
1322
  const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1529
- registerHelpers();
1530
- const template = Handlebars.compile(toolsPlatformTemplate);
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
1323
+ const platformContent = renderPlatformTemplate(templatePath);
1324
+ // 3. Write TOOLS.md Platform section
1541
1325
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1542
1326
  await updateManagedSection(toolsPath, platformContent, {
1543
1327
  mode: 'section',
@@ -1546,7 +1330,7 @@ async function refreshPlatformContent(options) {
1546
1330
  coreVersion,
1547
1331
  stalenessThresholdMs,
1548
1332
  });
1549
- // 7. Write SOUL.md managed block
1333
+ // 4. Write SOUL.md managed block
1550
1334
  const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
1551
1335
  await updateManagedSection(soulPath, soulSectionContent, {
1552
1336
  mode: 'block',
@@ -1554,7 +1338,7 @@ async function refreshPlatformContent(options) {
1554
1338
  coreVersion,
1555
1339
  stalenessThresholdMs,
1556
1340
  });
1557
- // 8. Write AGENTS.md managed block
1341
+ // 5. Write AGENTS.md managed block
1558
1342
  const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1559
1343
  await updateManagedSection(agentsPath, agentsSectionContent, {
1560
1344
  mode: 'block',
@@ -1562,7 +1346,7 @@ async function refreshPlatformContent(options) {
1562
1346
  coreVersion,
1563
1347
  stalenessThresholdMs,
1564
1348
  });
1565
- // 9. Copy templates to config dir
1349
+ // 6. Copy templates to config dir
1566
1350
  copyTemplates(coreConfigDir);
1567
1351
  }
1568
1352
 
@@ -1614,8 +1398,6 @@ async function seedContent(options) {
1614
1398
  // Seed content via the same code path as writer cycles
1615
1399
  await refreshPlatformContent({
1616
1400
  coreVersion: options.coreVersion,
1617
- probeTimeoutMs: options.probeTimeoutMs ?? 3000,
1618
- skipRegistryCheck: options.skipRegistryCheck ?? true,
1619
1401
  });
1620
1402
  }
1621
1403
 
@@ -1668,7 +1450,6 @@ function registerInstallCommand(program) {
1668
1450
  initFromOptions(opts);
1669
1451
  await seedContent({
1670
1452
  coreVersion: CORE_VERSION,
1671
- skipRegistryCheck: true,
1672
1453
  });
1673
1454
  console.log('✅ Platform content seeded successfully.');
1674
1455
  console.log(' - SOUL.md managed section written');
@@ -1680,12 +1461,73 @@ function registerInstallCommand(program) {
1680
1461
  }
1681
1462
 
1682
1463
  /**
1683
- * CLI status command: probe all service ports and report health summary.
1464
+ * Service URL resolution.
1684
1465
  *
1685
1466
  * @remarks
1686
- * Displays a table of all Jeeves platform services with port and
1687
- * health status. Exits with code 0 if all services are healthy,
1688
- * code 1 if any are unreachable.
1467
+ * Resolves the URL for a named Jeeves service using the following
1468
+ * resolution order:
1469
+ * 1. Consumer's own component config
1470
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
1471
+ * 3. Default port constants
1472
+ */
1473
+ /**
1474
+ * Resolve the URL for a named Jeeves service.
1475
+ *
1476
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
1477
+ * @param consumerName - Optional consumer component name for config override.
1478
+ * @returns The resolved service URL.
1479
+ * @throws Error if `init()` has not been called or the service is unknown.
1480
+ */
1481
+ function getServiceUrl(serviceName, consumerName) {
1482
+ // 2. Check core config
1483
+ const coreDir = getCoreConfigDir();
1484
+ const coreConfig = loadConfig(coreDir);
1485
+ const coreUrl = coreConfig?.services[serviceName]?.url;
1486
+ if (coreUrl)
1487
+ return coreUrl;
1488
+ // 3. Fall back to port constants
1489
+ const port = DEFAULT_PORTS[serviceName];
1490
+ if (port !== undefined) {
1491
+ return `http://127.0.0.1:${String(port)}`;
1492
+ }
1493
+ throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
1494
+ }
1495
+
1496
+ /**
1497
+ * HTTP helpers for the OpenClaw plugin SDK.
1498
+ *
1499
+ * @remarks
1500
+ * Thin wrappers around `fetch` that throw on non-OK responses
1501
+ * and handle JSON serialisation/deserialisation.
1502
+ */
1503
+ /**
1504
+ * Fetch a URL with an automatic abort timeout.
1505
+ *
1506
+ * @param url - URL to fetch.
1507
+ * @param timeoutMs - Timeout in milliseconds before aborting.
1508
+ * @param init - Optional `fetch` init options.
1509
+ * @returns The fetch Response object.
1510
+ */
1511
+ async function fetchWithTimeout(url, timeoutMs, init) {
1512
+ const controller = new AbortController();
1513
+ const timeout = setTimeout(() => {
1514
+ controller.abort();
1515
+ }, timeoutMs);
1516
+ try {
1517
+ return await fetch(url, { ...init, signal: controller.signal });
1518
+ }
1519
+ finally {
1520
+ clearTimeout(timeout);
1521
+ }
1522
+ }
1523
+
1524
+ /**
1525
+ * CLI status command: discover components and probe their health.
1526
+ *
1527
+ * @remarks
1528
+ * Uses `readComponentVersions()` to discover registered components,
1529
+ * then probes each one via GET /status. Exits with code 0 if all
1530
+ * services are healthy, code 1 if any are unreachable.
1689
1531
  */
1690
1532
  /**
1691
1533
  * Register the status subcommand on the parent CLI program.
@@ -1695,7 +1537,7 @@ function registerInstallCommand(program) {
1695
1537
  function registerStatusCommand(program) {
1696
1538
  program
1697
1539
  .command('status')
1698
- .description('Probe all Jeeves service ports and report health summary')
1540
+ .description('Discover Jeeves components and probe their health')
1699
1541
  .option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
1700
1542
  .option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
1701
1543
  .option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
@@ -1705,43 +1547,68 @@ function registerStatusCommand(program) {
1705
1547
  console.log('Jeeves Platform Status');
1706
1548
  console.log('='.repeat(60));
1707
1549
  console.log();
1708
- const probeResults = await probeAllServices(undefined, timeoutMs);
1550
+ const { getCoreConfigDir } = await Promise.resolve().then(function () { return init$1; });
1551
+ const coreConfigDir = getCoreConfigDir();
1552
+ const componentVersions = readComponentVersions(coreConfigDir);
1553
+ const componentNames = Object.keys(componentVersions);
1554
+ if (componentNames.length === 0) {
1555
+ console.log('No components registered.');
1556
+ return;
1557
+ }
1709
1558
  const nameWidth = 10;
1710
- const portWidth = 6;
1711
1559
  const statusWidth = 30;
1560
+ const versionWidth = 12;
1712
1561
  const header = [
1713
- 'Service'.padEnd(nameWidth),
1714
- 'Port'.padEnd(portWidth),
1562
+ 'Component'.padEnd(nameWidth),
1715
1563
  'Status'.padEnd(statusWidth),
1564
+ 'Version'.padEnd(versionWidth),
1716
1565
  ].join(' ');
1717
1566
  const separator = [
1718
1567
  '-'.repeat(nameWidth),
1719
- '-'.repeat(portWidth),
1720
1568
  '-'.repeat(statusWidth),
1569
+ '-'.repeat(versionWidth),
1721
1570
  ].join(' ');
1722
1571
  console.log(header);
1723
1572
  console.log(separator);
1724
1573
  let allHealthy = true;
1725
- for (const r of probeResults) {
1574
+ for (const name of componentNames) {
1726
1575
  let status;
1727
- if (r.healthy) {
1728
- status = r.version ? `✅ Running (v${r.version})` : '✅ Running';
1576
+ let version = '—';
1577
+ try {
1578
+ const url = getServiceUrl(name);
1579
+ const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
1580
+ if (response.ok) {
1581
+ status = '✅ Running';
1582
+ try {
1583
+ const body = await response.json();
1584
+ if (typeof body === 'object' &&
1585
+ body !== null &&
1586
+ 'version' in body &&
1587
+ typeof body['version'] === 'string') {
1588
+ version = body['version'];
1589
+ }
1590
+ }
1591
+ catch {
1592
+ // Non-JSON response — version stays unknown
1593
+ }
1594
+ }
1595
+ else {
1596
+ status = `❌ HTTP ${String(response.status)}`;
1597
+ allHealthy = false;
1598
+ }
1729
1599
  }
1730
- else {
1731
- status = r.error ? `❌ ${r.error}` : '❌ Down';
1600
+ catch {
1601
+ status = '❌ Down';
1732
1602
  allHealthy = false;
1733
1603
  }
1734
1604
  const row = [
1735
- r.name.padEnd(nameWidth),
1736
- String(r.port).padEnd(portWidth),
1605
+ name.padEnd(nameWidth),
1737
1606
  status.padEnd(statusWidth),
1607
+ version.padEnd(versionWidth),
1738
1608
  ].join(' ');
1739
1609
  console.log(row);
1740
1610
  }
1741
1611
  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
1612
  if (!allHealthy) {
1746
1613
  process.exitCode = 1;
1747
1614
  }
@@ -1833,13 +1700,25 @@ function registerUninstallCommand(program) {
1833
1700
  console.log();
1834
1701
  // Warn if services still responding
1835
1702
  try {
1836
- const probeResults = await probeAllServices(undefined, 2000);
1837
- const running = probeResults.filter((r) => r.healthy);
1703
+ const componentVersions = readComponentVersions(coreConfigDir);
1704
+ const componentNames = Object.keys(componentVersions);
1705
+ const running = [];
1706
+ for (const name of componentNames) {
1707
+ try {
1708
+ const url = getServiceUrl(name);
1709
+ const response = await fetchWithTimeout(`${url}/status`, 2000);
1710
+ if (response.ok) {
1711
+ running.push(name);
1712
+ }
1713
+ }
1714
+ catch {
1715
+ // Not running — expected during uninstall
1716
+ }
1717
+ }
1838
1718
  if (running.length > 0) {
1839
1719
  console.log('⚠️ The following services are still responding:');
1840
- for (const r of running) {
1841
- const ver = r.version ? ` (v${r.version})` : '';
1842
- console.log(` - ${r.name} on port ${String(r.port)}${ver}`);
1720
+ for (const name of running) {
1721
+ console.log(` - ${name}`);
1843
1722
  }
1844
1723
  console.log(' Consider stopping them before fully removing Jeeves.');
1845
1724
  console.log();