@karmaniverous/jeeves 0.1.6 → 0.2.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/README.md +90 -11
- package/dist/cli/jeeves/index.js +277 -123
- package/dist/index.d.ts +385 -68
- package/dist/index.js +700 -167
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ That's it. I handle the rest.
|
|
|
16
16
|
|
|
17
17
|
## Who I Am
|
|
18
18
|
|
|
19
|
-
My name is Jeeves.
|
|
19
|
+
My name is Jeeves.
|
|
20
20
|
|
|
21
21
|
I add *identity* to OpenClaw: professional discipline, operational protocols, and a suite of services for data-wrangling, indexing, synthesis, and presentation.
|
|
22
22
|
|
|
@@ -60,19 +60,87 @@ I coordinate four service components. Each has its own repo, service, and OpenCl
|
|
|
60
60
|
| [jeeves-runner](https://github.com/karmaniverous/jeeves-runner) | 1937 | Turing's paper in the *Proceedings* (1937) | Scheduled jobs, zero-LLM-cost scripts |
|
|
61
61
|
| [jeeves-meta](https://github.com/karmaniverous/jeeves-meta) | 1938 | Shannon's switching circuits thesis (1938) | Three-step LLM synthesis |
|
|
62
62
|
|
|
63
|
-
This package (`@karmaniverous/jeeves`) is the substrate they all share: managed workspace content, service discovery, config resolution, version-stamp convergence. It's a library and CLI. No daemon, no port, no tools registered with the gateway.
|
|
63
|
+
This package (`@karmaniverous/jeeves`) is the substrate they all share: managed workspace content, service discovery, config resolution, version-stamp convergence, and a Plugin SDK for building component plugins. It's a library and CLI. No daemon, no port, no tools registered with the gateway.
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Plugin SDK
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
The Plugin SDK (`src/plugin/`) provides canonical types and utilities for building OpenClaw plugins that integrate with the Jeeves platform.
|
|
68
|
+
|
|
69
|
+
### Core Types
|
|
70
|
+
|
|
71
|
+
- **`PluginApi`** — the shape of the `api` object the OpenClaw gateway passes to plugins at registration time. Provides `config`, `resolvePath()`, and `registerTool()`.
|
|
72
|
+
- **`ToolResult`** — result shape returned by tool executions: an array of content blocks plus an optional `isError` flag.
|
|
73
|
+
- **`ToolDescriptor`** — tool definition for registration: `name`, `description`, `parameters` (JSON Schema), and an `execute` function.
|
|
74
|
+
|
|
75
|
+
### Result Formatters
|
|
76
|
+
|
|
77
|
+
- **`ok(data)`** — wraps arbitrary data as a successful `ToolResult` with JSON-stringified content.
|
|
78
|
+
- **`fail(error)`** — wraps an error into a `ToolResult` with `isError: true`.
|
|
79
|
+
- **`connectionFail(error, baseUrl, pluginId)`** — detects `ECONNREFUSED`, `ENOTFOUND`, and `ETIMEDOUT` from `error.cause.code` and returns a user-friendly message referencing the plugin's `config.apiUrl` setting. Falls back to `fail()` for non-connection errors.
|
|
80
|
+
|
|
81
|
+
### HTTP Helpers
|
|
82
|
+
|
|
83
|
+
- **`fetchJson(url, init?)`** — thin wrapper around `fetch` that throws on non-OK responses and returns parsed JSON.
|
|
84
|
+
- **`postJson(url, body)`** — POST JSON to a URL and return parsed response.
|
|
85
|
+
|
|
86
|
+
### Resolution Helpers
|
|
87
|
+
|
|
88
|
+
- **`resolveWorkspacePath(api)`** — resolves the workspace root from the plugin API via a three-step chain: `api.config.agents.defaults.workspace` → `api.resolvePath('.')` → `process.cwd()`.
|
|
89
|
+
- **`resolvePluginSetting(api, pluginId, key, envVar, fallback)`** — resolves a plugin setting via: plugin config → environment variable → fallback value.
|
|
90
|
+
|
|
91
|
+
### OpenClaw Config Utilities
|
|
92
|
+
|
|
93
|
+
- **`resolveOpenClawHome()`** — resolves the OpenClaw home directory: `OPENCLAW_CONFIG` env (dirname) → `OPENCLAW_HOME` env → `~/.openclaw`.
|
|
94
|
+
- **`resolveConfigPath(home)`** — resolves the OpenClaw config file path: `OPENCLAW_CONFIG` env → `{home}/openclaw.json`.
|
|
95
|
+
- **`patchConfig(config, pluginId, mode)`** — idempotent config patching for plugin install/uninstall. Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
|
|
96
|
+
|
|
97
|
+
## Config Query Handler
|
|
98
|
+
|
|
99
|
+
The `createConfigQueryHandler(getConfig)` factory produces a transport-agnostic handler for `GET /config` endpoints. It accepts a `getConfig` callback that returns the current config object.
|
|
100
|
+
|
|
101
|
+
- No `path` parameter → returns the full config document.
|
|
102
|
+
- Valid JSONPath expression → returns matching results with count (powered by `jsonpath-plus`).
|
|
103
|
+
- Invalid JSONPath → returns a 400 error.
|
|
104
|
+
|
|
105
|
+
Component services wire this into their HTTP server to expose config for diagnostic queries.
|
|
106
|
+
|
|
107
|
+
## Managed Content System
|
|
108
|
+
|
|
109
|
+
The managed content system maintains SOUL.md, AGENTS.md, and TOOLS.md without destroying user-authored content.
|
|
110
|
+
|
|
111
|
+
### Key Functions
|
|
112
|
+
|
|
113
|
+
- **`updateManagedSection(filePath, content, options)`** — writes managed content in either block mode (replaces entire managed block) or section mode (upserts a named H2 section within the block). Handles file locking, version-stamp convergence, cleanup detection, and atomic writes.
|
|
114
|
+
- **`removeManagedSection(filePath, options)`** — removes a specific section or the entire managed block. If the last section is removed, the entire block is removed.
|
|
115
|
+
- **`parseManaged(fileContent, markers)`** — parses a file into its managed block, version stamp, sections, and user content.
|
|
116
|
+
- **`atomicWrite(filePath, content)`** — writes via a temp file + rename to prevent partial writes.
|
|
117
|
+
- **`withFileLock(filePath, fn)`** — executes a callback while holding a file-level lock (2-minute stale threshold, 5 retries).
|
|
118
|
+
|
|
119
|
+
### ManagedMarkers Type
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface ManagedMarkers {
|
|
123
|
+
begin: string; // BEGIN comment marker text
|
|
124
|
+
end: string; // END comment marker text
|
|
125
|
+
title?: string; // Optional H1 title prepended inside managed block
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Pre-defined marker sets: `TOOLS_MARKERS`, `SOUL_MARKERS`, `AGENTS_MARKERS`.
|
|
130
|
+
|
|
131
|
+
See the [Managed Content System](https://docs.karmanivero.us/jeeves/documents/Managed_Content_System.html) guide for the full deep-dive.
|
|
132
|
+
|
|
133
|
+
## ComponentWriter and JeevesComponent
|
|
134
|
+
|
|
135
|
+
Component plugins implement the `JeevesComponent` interface and use `createComponentWriter()` to get a timer-based orchestrator:
|
|
68
136
|
|
|
69
137
|
```typescript
|
|
70
138
|
import { init, createComponentWriter } from '@karmaniverous/jeeves';
|
|
71
139
|
import type { JeevesComponent } from '@karmaniverous/jeeves';
|
|
72
140
|
|
|
73
141
|
init({
|
|
74
|
-
workspacePath: api
|
|
75
|
-
configRoot: api
|
|
142
|
+
workspacePath: resolveWorkspacePath(api),
|
|
143
|
+
configRoot: resolvePluginSetting(api, pluginId, 'configRoot', 'JEEVES_CONFIG_ROOT', 'j:/config'),
|
|
76
144
|
});
|
|
77
145
|
|
|
78
146
|
const writer = createComponentWriter({
|
|
@@ -88,18 +156,29 @@ const writer = createComponentWriter({
|
|
|
88
156
|
writer.start();
|
|
89
157
|
```
|
|
90
158
|
|
|
91
|
-
|
|
159
|
+
On each cycle the writer calls `generateToolsContent()`, writes the component's TOOLS.md section, and runs `refreshPlatformContent()` to maintain SOUL.md, AGENTS.md, and the Platform section with live service health data.
|
|
160
|
+
|
|
161
|
+
The `createAsyncContentCache({ fetch, placeholder? })` utility bridges the sync `generateToolsContent` interface with async data sources — returns a sync `() => string` that serves cached content while refreshing in the background.
|
|
92
162
|
|
|
93
|
-
See the [Building a Component Plugin](https://docs.karmanivero.us/jeeves/documents/
|
|
163
|
+
See the [Building a Component Plugin](https://docs.karmanivero.us/jeeves/documents/Building_a_Component_Plugin.html) guide for the full walkthrough.
|
|
164
|
+
|
|
165
|
+
## Service Discovery
|
|
166
|
+
|
|
167
|
+
- **`getServiceUrl(serviceName, consumerName?)`** — resolves a service URL via: consumer config → core config → default port constants.
|
|
168
|
+
- **`probeService(serviceName, consumerName?, timeoutMs?)`** — probes `/status` then `/health` endpoints, returns a `ProbeResult` with health status and version.
|
|
169
|
+
- **`probeAllServices(consumerName?, timeoutMs?)`** — probes all known services (server, watcher, runner, meta).
|
|
170
|
+
- **`checkRegistryVersion(packageName, cacheDir, ttlSeconds?)`** — checks npm registry for the latest version with local file caching (default 1-hour TTL).
|
|
94
171
|
|
|
95
172
|
## CLI
|
|
96
173
|
|
|
97
174
|
```bash
|
|
98
|
-
jeeves install #
|
|
99
|
-
jeeves uninstall # Remove managed sections
|
|
100
|
-
jeeves status # Probe all service ports, report health
|
|
175
|
+
jeeves install # Seed identity, protocols, platform content; create core config
|
|
176
|
+
jeeves uninstall # Remove managed sections, templates, config schema
|
|
177
|
+
jeeves status # Probe all service ports, report health table
|
|
101
178
|
```
|
|
102
179
|
|
|
180
|
+
All three commands accept `--workspace <path>` and `--config-root <path>` options.
|
|
181
|
+
|
|
103
182
|
## Configuration
|
|
104
183
|
|
|
105
184
|
Core config at `{configRoot}/jeeves-core/config.json`:
|
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
#!/usr/bin/env node
|
|
3
3
|
import require$$0 from 'commander';
|
|
4
|
-
import { existsSync, readFileSync,
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import Handlebars from 'handlebars';
|
|
9
9
|
import { packageDirectorySync } from 'package-directory';
|
|
10
|
-
import {
|
|
10
|
+
import semver, { gte } from 'semver';
|
|
11
11
|
import { lock } from 'proper-lockfile';
|
|
12
|
-
import {
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
13
|
|
|
14
14
|
function getDefaultExportFromCjs (x) {
|
|
15
15
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -141,6 +141,8 @@ const TEMPLATES_DIR = 'templates';
|
|
|
141
141
|
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
142
142
|
/** Core config file name. */
|
|
143
143
|
const CONFIG_FILE = 'config.json';
|
|
144
|
+
/** Component versions state file name. */
|
|
145
|
+
const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* Default port assignments for Jeeves platform services.
|
|
@@ -204,14 +206,14 @@ const SECTION_ORDER = [
|
|
|
204
206
|
* Core library version, inlined at build time.
|
|
205
207
|
*
|
|
206
208
|
* @remarks
|
|
207
|
-
* The `0.1.
|
|
209
|
+
* The `0.1.6` placeholder is replaced by
|
|
208
210
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
209
211
|
* from `package.json`. This ensures the correct version survives
|
|
210
212
|
* when consumers bundle core into their own dist (where runtime
|
|
211
213
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
212
214
|
*/
|
|
213
215
|
/** The core library version from package.json (inlined at build time). */
|
|
214
|
-
const CORE_VERSION = '0.1.
|
|
216
|
+
const CORE_VERSION = '0.1.6';
|
|
215
217
|
|
|
216
218
|
/**
|
|
217
219
|
* Core configuration schema and resolution.
|
|
@@ -726,6 +728,117 @@ Read these templates when creating new specs, onboarding to new projects, or whe
|
|
|
726
728
|
{{/if}}
|
|
727
729
|
`;
|
|
728
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Shared file I/O helpers for managed section operations.
|
|
733
|
+
*
|
|
734
|
+
* @remarks
|
|
735
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
736
|
+
* reusable utilities, eliminating duplication between
|
|
737
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
738
|
+
*/
|
|
739
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
740
|
+
const STALE_LOCK_MS = 120_000;
|
|
741
|
+
/** Default core version when none provided. */
|
|
742
|
+
const DEFAULT_CORE_VERSION = '0.0.0';
|
|
743
|
+
/** Lock retry options. */
|
|
744
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
745
|
+
/**
|
|
746
|
+
* Write content to a file atomically via a temp file + rename.
|
|
747
|
+
*
|
|
748
|
+
* @param filePath - Absolute path to the target file.
|
|
749
|
+
* @param content - Content to write.
|
|
750
|
+
*/
|
|
751
|
+
function atomicWrite(filePath, content) {
|
|
752
|
+
const dir = dirname(filePath);
|
|
753
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
754
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
755
|
+
renameSync(tempPath, filePath);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Execute a callback while holding a file lock.
|
|
759
|
+
*
|
|
760
|
+
* @remarks
|
|
761
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
762
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
763
|
+
* and retries up to 5 times.
|
|
764
|
+
*
|
|
765
|
+
* @param filePath - Absolute path to the file to lock.
|
|
766
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
767
|
+
*/
|
|
768
|
+
async function withFileLock(filePath, fn) {
|
|
769
|
+
let release;
|
|
770
|
+
try {
|
|
771
|
+
release = await lock(filePath, {
|
|
772
|
+
stale: STALE_LOCK_MS,
|
|
773
|
+
retries: LOCK_RETRIES,
|
|
774
|
+
});
|
|
775
|
+
await fn();
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
if (release) {
|
|
779
|
+
try {
|
|
780
|
+
await release();
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Lock already released or file deleted — safe to ignore
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Shared component version state file management.
|
|
791
|
+
*
|
|
792
|
+
* @remarks
|
|
793
|
+
* Each `ComponentWriter` cycle writes its component's entry to
|
|
794
|
+
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
795
|
+
* template reads this file to populate ALL rows in the service health
|
|
796
|
+
* table, not just the calling component's.
|
|
797
|
+
*/
|
|
798
|
+
/**
|
|
799
|
+
* Read the component versions state file.
|
|
800
|
+
*
|
|
801
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
802
|
+
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
803
|
+
*/
|
|
804
|
+
function readComponentVersions(coreConfigDir) {
|
|
805
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
806
|
+
if (!existsSync(filePath))
|
|
807
|
+
return {};
|
|
808
|
+
try {
|
|
809
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
810
|
+
return JSON.parse(raw);
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return {};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Write a component's version entry to the shared state file.
|
|
818
|
+
*
|
|
819
|
+
* @remarks
|
|
820
|
+
* Reads the existing file, merges the new entry, and writes atomically.
|
|
821
|
+
*
|
|
822
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
823
|
+
* @param options - Component version data to write.
|
|
824
|
+
*/
|
|
825
|
+
function writeComponentVersion(coreConfigDir, options) {
|
|
826
|
+
const existing = readComponentVersions(coreConfigDir);
|
|
827
|
+
existing[options.componentName] = {
|
|
828
|
+
serviceVersion: options.serviceVersion,
|
|
829
|
+
pluginVersion: options.pluginVersion,
|
|
830
|
+
servicePackage: options.servicePackage,
|
|
831
|
+
pluginPackage: options.pluginPackage,
|
|
832
|
+
updatedAt: new Date().toISOString(),
|
|
833
|
+
};
|
|
834
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
835
|
+
const dir = dirname(filePath);
|
|
836
|
+
if (!existsSync(dir)) {
|
|
837
|
+
mkdirSync(dir, { recursive: true });
|
|
838
|
+
}
|
|
839
|
+
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
840
|
+
}
|
|
841
|
+
|
|
729
842
|
/**
|
|
730
843
|
* Service URL resolution.
|
|
731
844
|
*
|
|
@@ -1149,10 +1262,6 @@ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRES
|
|
|
1149
1262
|
*
|
|
1150
1263
|
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
1151
1264
|
*/
|
|
1152
|
-
/** Default core version when none provided. */
|
|
1153
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
1154
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
1155
|
-
const STALE_LOCK_MS = 120_000;
|
|
1156
1265
|
/**
|
|
1157
1266
|
* Update a managed section in a file.
|
|
1158
1267
|
*
|
|
@@ -1161,7 +1270,7 @@ const STALE_LOCK_MS = 120_000;
|
|
|
1161
1270
|
* @param options - Write mode and optional configuration.
|
|
1162
1271
|
*/
|
|
1163
1272
|
async function updateManagedSection(filePath, content, options = {}) {
|
|
1164
|
-
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion =
|
|
1273
|
+
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_CORE_VERSION, stalenessThresholdMs, } = options;
|
|
1165
1274
|
if (mode === 'section' && !sectionId) {
|
|
1166
1275
|
throw new Error('sectionId is required when mode is "section"');
|
|
1167
1276
|
}
|
|
@@ -1173,93 +1282,134 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1173
1282
|
if (!existsSync(filePath)) {
|
|
1174
1283
|
writeFileSync(filePath, '', 'utf-8');
|
|
1175
1284
|
}
|
|
1176
|
-
let release;
|
|
1177
1285
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
newManagedBody = markers.title
|
|
1195
|
-
? `# ${markers.title}\n\n${content}`
|
|
1196
|
-
: content;
|
|
1197
|
-
}
|
|
1198
|
-
else {
|
|
1199
|
-
// Section mode: upsert the named section
|
|
1200
|
-
const sections = [...parsed.sections];
|
|
1201
|
-
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
1202
|
-
if (existingIdx >= 0) {
|
|
1203
|
-
sections[existingIdx] = { id: sectionId, content };
|
|
1286
|
+
await withFileLock(filePath, () => {
|
|
1287
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
1288
|
+
const parsed = parseManaged(fileContent, markers);
|
|
1289
|
+
// Version-stamp convergence check (block mode only).
|
|
1290
|
+
// In section mode, components always write their own sections — the version
|
|
1291
|
+
// stamp governs shared content convergence, not component-specific sections.
|
|
1292
|
+
if (mode === 'block' &&
|
|
1293
|
+
!shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
let newManagedBody;
|
|
1297
|
+
if (mode === 'block') {
|
|
1298
|
+
// Prepend H1 title if markers specify one
|
|
1299
|
+
newManagedBody = markers.title
|
|
1300
|
+
? `# ${markers.title}\n\n${content}`
|
|
1301
|
+
: content;
|
|
1204
1302
|
}
|
|
1205
1303
|
else {
|
|
1206
|
-
|
|
1304
|
+
// Section mode: upsert the named section
|
|
1305
|
+
const sections = [...parsed.sections];
|
|
1306
|
+
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
1307
|
+
if (existingIdx >= 0) {
|
|
1308
|
+
sections[existingIdx] = { id: sectionId, content };
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
sections.push({ id: sectionId, content });
|
|
1312
|
+
}
|
|
1313
|
+
sortSectionsByOrder(sections);
|
|
1314
|
+
const sectionText = sections
|
|
1315
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
1316
|
+
.join('\n\n');
|
|
1317
|
+
// Prepend H1 title if markers specify one
|
|
1318
|
+
newManagedBody = markers.title
|
|
1319
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
1320
|
+
: sectionText;
|
|
1321
|
+
}
|
|
1322
|
+
// Cleanup detection
|
|
1323
|
+
const userContent = parsed.userContent;
|
|
1324
|
+
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1325
|
+
// Build the full managed block
|
|
1326
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1327
|
+
const endLine = formatEndMarker(markers.end);
|
|
1328
|
+
const parts = [];
|
|
1329
|
+
if (parsed.beforeContent) {
|
|
1330
|
+
parts.push(parsed.beforeContent);
|
|
1331
|
+
parts.push('');
|
|
1332
|
+
}
|
|
1333
|
+
parts.push(beginLine);
|
|
1334
|
+
if (cleanupNeeded) {
|
|
1335
|
+
parts.push('');
|
|
1336
|
+
parts.push(CLEANUP_FLAG);
|
|
1207
1337
|
}
|
|
1208
|
-
sortSectionsByOrder(sections);
|
|
1209
|
-
const sectionText = sections
|
|
1210
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
1211
|
-
.join('\n\n');
|
|
1212
|
-
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
1213
|
-
newManagedBody = markers.title
|
|
1214
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
1215
|
-
: sectionText;
|
|
1216
|
-
}
|
|
1217
|
-
// Cleanup detection
|
|
1218
|
-
const userContent = parsed.userContent;
|
|
1219
|
-
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1220
|
-
// Build the full managed block
|
|
1221
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1222
|
-
const endLine = formatEndMarker(markers.end);
|
|
1223
|
-
const parts = [];
|
|
1224
|
-
if (parsed.beforeContent) {
|
|
1225
|
-
parts.push(parsed.beforeContent);
|
|
1226
1338
|
parts.push('');
|
|
1227
|
-
|
|
1228
|
-
parts.push(beginLine);
|
|
1229
|
-
if (cleanupNeeded) {
|
|
1339
|
+
parts.push(newManagedBody);
|
|
1230
1340
|
parts.push('');
|
|
1231
|
-
parts.push(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
parts.push(endLine);
|
|
1237
|
-
if (userContent) {
|
|
1341
|
+
parts.push(endLine);
|
|
1342
|
+
if (userContent) {
|
|
1343
|
+
parts.push('');
|
|
1344
|
+
parts.push(userContent);
|
|
1345
|
+
}
|
|
1238
1346
|
parts.push('');
|
|
1239
|
-
parts.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const newFileContent = parts.join('\n');
|
|
1243
|
-
// Atomic write: write to temp file, then rename
|
|
1244
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
1245
|
-
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
1246
|
-
renameSync(tempPath, filePath);
|
|
1347
|
+
const newFileContent = parts.join('\n');
|
|
1348
|
+
atomicWrite(filePath, newFileContent);
|
|
1349
|
+
});
|
|
1247
1350
|
}
|
|
1248
1351
|
catch (err) {
|
|
1249
1352
|
// Log warning but don't throw — writer cycles are periodic
|
|
1250
1353
|
const message = err instanceof Error ? err.message : String(err);
|
|
1251
1354
|
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
1252
1355
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1356
|
+
}
|
|
1357
|
+
|
|
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);
|
|
1257
1400
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1401
|
+
if (entry.pluginPackage && entry.pluginVersion) {
|
|
1402
|
+
const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
|
|
1403
|
+
availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
|
|
1260
1404
|
}
|
|
1261
1405
|
}
|
|
1262
|
-
|
|
1406
|
+
return {
|
|
1407
|
+
...r,
|
|
1408
|
+
pluginVersion: entry.pluginVersion,
|
|
1409
|
+
availableServiceVersion,
|
|
1410
|
+
availablePluginVersion,
|
|
1411
|
+
};
|
|
1412
|
+
});
|
|
1263
1413
|
}
|
|
1264
1414
|
|
|
1265
1415
|
/**
|
|
@@ -1315,15 +1465,28 @@ function copyTemplates(coreConfigDir) {
|
|
|
1315
1465
|
}
|
|
1316
1466
|
/** Whether Handlebars helpers have been registered. */
|
|
1317
1467
|
let helpersRegistered = false;
|
|
1318
|
-
/**
|
|
1319
|
-
* Register Handlebars helpers used in the Platform template.
|
|
1320
|
-
*/
|
|
1468
|
+
/** Register Handlebars helpers used in the Platform template. */
|
|
1321
1469
|
function registerHelpers() {
|
|
1322
1470
|
if (helpersRegistered)
|
|
1323
1471
|
return;
|
|
1324
1472
|
helpersRegistered = true;
|
|
1325
1473
|
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1326
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Check if a newer core version is available on npm.
|
|
1477
|
+
*
|
|
1478
|
+
* @returns The newer version string, or undefined.
|
|
1479
|
+
*/
|
|
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;
|
|
1487
|
+
}
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1327
1490
|
/**
|
|
1328
1491
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1329
1492
|
*
|
|
@@ -1335,55 +1498,46 @@ async function refreshPlatformContent(options) {
|
|
|
1335
1498
|
const coreConfigDir = getCoreConfigDir();
|
|
1336
1499
|
// 1. Probe all services
|
|
1337
1500
|
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1338
|
-
|
|
1339
|
-
|
|
1501
|
+
// 2. Write calling component's version entry (with serviceVersion from probe)
|
|
1502
|
+
if (componentName) {
|
|
1503
|
+
const callerProbe = probeResults.find((r) => r.name === componentName);
|
|
1504
|
+
writeComponentVersion(coreConfigDir, {
|
|
1505
|
+
componentName,
|
|
1506
|
+
serviceVersion: callerProbe?.version,
|
|
1507
|
+
pluginVersion: componentVersion,
|
|
1508
|
+
servicePackage,
|
|
1509
|
+
pluginPackage,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
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
|
|
1340
1515
|
const cacheDir = componentName
|
|
1341
1516
|
? getComponentConfigDir(componentName)
|
|
1342
1517
|
: coreConfigDir;
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
if (svcVersion) {
|
|
1354
|
-
availableServiceVersion = svcVersion;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
if (pluginPackage) {
|
|
1358
|
-
const plgVersion = checkRegistryVersion(pluginPackage, cacheDir);
|
|
1359
|
-
if (plgVersion) {
|
|
1360
|
-
availablePluginVersion = plgVersion;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
// 3. Build enriched service rows — match the calling component by name
|
|
1365
|
-
const serviceRows = probeResults.map((r) => ({
|
|
1366
|
-
...r,
|
|
1367
|
-
pluginVersion: r.name === componentName ? componentVersion : undefined,
|
|
1368
|
-
availableServiceVersion: r.name === componentName ? availableServiceVersion : undefined,
|
|
1369
|
-
availablePluginVersion: r.name === componentName ? availablePluginVersion : undefined,
|
|
1370
|
-
}));
|
|
1371
|
-
// 5. Check if templates are available
|
|
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
|
|
1372
1528
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1373
|
-
const templatesAvailable = existsSync(templatePath);
|
|
1374
|
-
// 6. Render Platform template
|
|
1375
1529
|
registerHelpers();
|
|
1376
1530
|
const template = Handlebars.compile(toolsPlatformTemplate);
|
|
1377
1531
|
const templateData = {
|
|
1378
1532
|
services: serviceRows,
|
|
1379
|
-
unhealthyServices,
|
|
1533
|
+
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1380
1534
|
coreVersion,
|
|
1381
1535
|
availableCoreVersion,
|
|
1382
|
-
templatesAvailable,
|
|
1536
|
+
templatesAvailable: existsSync(templatePath),
|
|
1383
1537
|
templatePath,
|
|
1384
1538
|
};
|
|
1385
1539
|
const platformContent = template(templateData);
|
|
1386
|
-
//
|
|
1540
|
+
// 6. Write TOOLS.md Platform section
|
|
1387
1541
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1388
1542
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1389
1543
|
mode: 'section',
|
|
@@ -1392,7 +1546,7 @@ async function refreshPlatformContent(options) {
|
|
|
1392
1546
|
coreVersion,
|
|
1393
1547
|
stalenessThresholdMs,
|
|
1394
1548
|
});
|
|
1395
|
-
//
|
|
1549
|
+
// 7. Write SOUL.md managed block
|
|
1396
1550
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1397
1551
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1398
1552
|
mode: 'block',
|
|
@@ -1400,7 +1554,7 @@ async function refreshPlatformContent(options) {
|
|
|
1400
1554
|
coreVersion,
|
|
1401
1555
|
stalenessThresholdMs,
|
|
1402
1556
|
});
|
|
1403
|
-
//
|
|
1557
|
+
// 8. Write AGENTS.md managed block
|
|
1404
1558
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1405
1559
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1406
1560
|
mode: 'block',
|
|
@@ -1408,7 +1562,7 @@ async function refreshPlatformContent(options) {
|
|
|
1408
1562
|
coreVersion,
|
|
1409
1563
|
stalenessThresholdMs,
|
|
1410
1564
|
});
|
|
1411
|
-
//
|
|
1565
|
+
// 9. Copy templates to config dir
|
|
1412
1566
|
copyTemplates(coreConfigDir);
|
|
1413
1567
|
}
|
|
1414
1568
|
|