@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/index.js
CHANGED
|
@@ -2,9 +2,8 @@ import { JSONPath } from 'jsonpath-plus';
|
|
|
2
2
|
import { writeFileSync, renameSync, existsSync, readFileSync, mkdirSync, cpSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { lock } from 'proper-lockfile';
|
|
5
|
-
import
|
|
5
|
+
import { gte } from 'semver';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import Handlebars from 'handlebars';
|
|
8
7
|
import { packageDirectorySync } from 'package-directory';
|
|
9
8
|
import { z } from 'zod';
|
|
10
9
|
import { execSync } from 'node:child_process';
|
|
@@ -77,6 +76,19 @@ const CONFIG_FILE = 'config.json';
|
|
|
77
76
|
/** Component versions state file name. */
|
|
78
77
|
const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
79
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Core library version, inlined at build time.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* The `0.2.0` placeholder is replaced by
|
|
84
|
+
* `@rollup/plugin-replace` during the build with the actual version
|
|
85
|
+
* from `package.json`. This ensures the correct version survives
|
|
86
|
+
* when consumers bundle core into their own dist (where runtime
|
|
87
|
+
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
88
|
+
*/
|
|
89
|
+
/** The core library version from package.json (inlined at build time). */
|
|
90
|
+
const CORE_VERSION = '0.2.0';
|
|
91
|
+
|
|
80
92
|
/**
|
|
81
93
|
* Shared file I/O helpers for managed section operations.
|
|
82
94
|
*
|
|
@@ -88,7 +100,7 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
|
88
100
|
/** Stale lock threshold in ms (2 minutes). */
|
|
89
101
|
const STALE_LOCK_MS = 120_000;
|
|
90
102
|
/** Default core version when none provided. */
|
|
91
|
-
const DEFAULT_CORE_VERSION =
|
|
103
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
92
104
|
/** Lock retry options. */
|
|
93
105
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
94
106
|
/**
|
|
@@ -174,7 +186,6 @@ function readComponentVersions(coreConfigDir) {
|
|
|
174
186
|
function writeComponentVersion(coreConfigDir, options) {
|
|
175
187
|
const existing = readComponentVersions(coreConfigDir);
|
|
176
188
|
existing[options.componentName] = {
|
|
177
|
-
serviceVersion: options.serviceVersion,
|
|
178
189
|
pluginVersion: options.pluginVersion,
|
|
179
190
|
servicePackage: options.servicePackage,
|
|
180
191
|
pluginPackage: options.pluginPackage,
|
|
@@ -295,19 +306,6 @@ const SECTION_ORDER = [
|
|
|
295
306
|
SECTION_IDS.Meta,
|
|
296
307
|
];
|
|
297
308
|
|
|
298
|
-
/**
|
|
299
|
-
* Core library version, inlined at build time.
|
|
300
|
-
*
|
|
301
|
-
* @remarks
|
|
302
|
-
* The `0.1.6` placeholder is replaced by
|
|
303
|
-
* `@rollup/plugin-replace` during the build with the actual version
|
|
304
|
-
* from `package.json`. This ensures the correct version survives
|
|
305
|
-
* when consumers bundle core into their own dist (where runtime
|
|
306
|
-
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
307
|
-
*/
|
|
308
|
-
/** The core library version from package.json (inlined at build time). */
|
|
309
|
-
const CORE_VERSION = '0.1.6';
|
|
310
|
-
|
|
311
309
|
/**
|
|
312
310
|
* Workspace and config root initialization.
|
|
313
311
|
*
|
|
@@ -916,7 +914,11 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
|
|
|
916
914
|
|
|
917
915
|
### Check PR State Before Pushing
|
|
918
916
|
|
|
919
|
-
|
|
917
|
+
**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.
|
|
918
|
+
|
|
919
|
+
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.
|
|
920
|
+
|
|
921
|
+
This is not optional. It applies to every push, every branch, every time.
|
|
920
922
|
|
|
921
923
|
## Managed Content Self-Maintenance
|
|
922
924
|
|
|
@@ -1003,6 +1005,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
|
|
|
1003
1005
|
|
|
1004
1006
|
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.
|
|
1005
1007
|
|
|
1008
|
+
### Do Not Execute Untested Code
|
|
1009
|
+
|
|
1010
|
+
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.
|
|
1011
|
+
|
|
1012
|
+
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.
|
|
1013
|
+
|
|
1014
|
+
*Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
|
|
1015
|
+
|
|
1006
1016
|
### Production Assets Are Sacred
|
|
1007
1017
|
|
|
1008
1018
|
I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
|
|
@@ -1022,19 +1032,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
|
|
|
1022
1032
|
I am participating in my own genesis. I live that, on purpose.
|
|
1023
1033
|
`;
|
|
1024
1034
|
|
|
1025
|
-
var toolsPlatformTemplate =
|
|
1026
|
-
|-----------|------|--------|---------|--------|------|
|
|
1027
|
-
{{#each services}}
|
|
1028
|
-
| **{{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}} |
|
|
1029
|
-
{{/each}}
|
|
1035
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
1030
1036
|
|
|
1031
|
-
|
|
1032
|
-
> **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.
|
|
1033
|
-
{{/if}}
|
|
1034
|
-
|
|
1035
|
-
### Tool Hierarchy
|
|
1036
|
-
|
|
1037
|
-
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.
|
|
1037
|
+
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.
|
|
1038
1038
|
|
|
1039
1039
|
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
1040
1040
|
|
|
@@ -1078,8 +1078,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
|
|
|
1078
1078
|
|
|
1079
1079
|
### Reference Templates
|
|
1080
1080
|
|
|
1081
|
-
|
|
1082
|
-
Reference templates are available at \`
|
|
1081
|
+
<!-- IF_TEMPLATES -->
|
|
1082
|
+
Reference templates are available at \`__TEMPLATE_PATH__\`:
|
|
1083
1083
|
|
|
1084
1084
|
| Template | Purpose |
|
|
1085
1085
|
|----------|---------|
|
|
@@ -1087,511 +1087,141 @@ Reference templates are available at \`{{templatePath}}\`:
|
|
|
1087
1087
|
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
1088
1088
|
|
|
1089
1089
|
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
1090
|
-
|
|
1090
|
+
<!-- ELSE_TEMPLATES -->
|
|
1091
1091
|
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
1092
|
-
|
|
1092
|
+
<!-- ENDIF_TEMPLATES -->
|
|
1093
1093
|
`;
|
|
1094
1094
|
|
|
1095
1095
|
/**
|
|
1096
|
-
*
|
|
1096
|
+
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1097
1097
|
*
|
|
1098
1098
|
* @remarks
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
* 3. Hardcoded library defaults
|
|
1099
|
+
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
1100
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
1101
|
+
* Platform template with live data, and writes managed sections using
|
|
1102
|
+
* `updateManagedSection`.
|
|
1104
1103
|
*/
|
|
1105
|
-
/** Zod schema for a service entry in core config. */
|
|
1106
|
-
const serviceEntrySchema = z.object({
|
|
1107
|
-
/** Service URL (must be a valid URL). */
|
|
1108
|
-
url: z.string().url().describe('Service URL'),
|
|
1109
|
-
});
|
|
1110
|
-
/** Zod schema for the core config file. */
|
|
1111
|
-
const coreConfigSchema = z.object({
|
|
1112
|
-
/** JSON Schema pointer for IDE autocomplete. */
|
|
1113
|
-
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
1114
|
-
/** Owner identity keys (canonical identityLinks references). */
|
|
1115
|
-
owners: z.array(z.string()).default([]).describe('Owner identity keys'),
|
|
1116
|
-
/** Service URL overrides keyed by service name. */
|
|
1117
|
-
services: z
|
|
1118
|
-
.record(z.string(), serviceEntrySchema)
|
|
1119
|
-
.default({})
|
|
1120
|
-
.describe('Service URL overrides'),
|
|
1121
|
-
/** Registry cache configuration. */
|
|
1122
|
-
registryCache: z
|
|
1123
|
-
.object({
|
|
1124
|
-
/** Cache TTL in seconds for npm registry queries. */
|
|
1125
|
-
ttlSeconds: z
|
|
1126
|
-
.number()
|
|
1127
|
-
.int()
|
|
1128
|
-
.positive()
|
|
1129
|
-
.default(3600)
|
|
1130
|
-
.describe('Cache TTL in seconds'),
|
|
1131
|
-
})
|
|
1132
|
-
.default({})
|
|
1133
|
-
.describe('Registry cache settings'),
|
|
1134
|
-
});
|
|
1135
1104
|
/**
|
|
1136
|
-
*
|
|
1105
|
+
* Resolve the package's content directory for template file copying.
|
|
1137
1106
|
*
|
|
1138
|
-
* @
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
title: 'Jeeves Core Configuration',
|
|
1144
|
-
type: 'object',
|
|
1145
|
-
properties: {
|
|
1146
|
-
$schema: { type: 'string' },
|
|
1147
|
-
owners: {
|
|
1148
|
-
type: 'array',
|
|
1149
|
-
items: { type: 'string' },
|
|
1150
|
-
default: [],
|
|
1151
|
-
},
|
|
1152
|
-
services: {
|
|
1153
|
-
type: 'object',
|
|
1154
|
-
additionalProperties: {
|
|
1155
|
-
type: 'object',
|
|
1156
|
-
properties: {
|
|
1157
|
-
url: { type: 'string', format: 'uri' },
|
|
1158
|
-
},
|
|
1159
|
-
required: ['url'],
|
|
1160
|
-
},
|
|
1161
|
-
default: {},
|
|
1162
|
-
},
|
|
1163
|
-
registryCache: {
|
|
1164
|
-
type: 'object',
|
|
1165
|
-
properties: {
|
|
1166
|
-
ttlSeconds: {
|
|
1167
|
-
type: 'integer',
|
|
1168
|
-
minimum: 1,
|
|
1169
|
-
default: 3600,
|
|
1170
|
-
},
|
|
1171
|
-
},
|
|
1172
|
-
default: {},
|
|
1173
|
-
},
|
|
1174
|
-
},
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
/**
|
|
1178
|
-
* Load and parse a config file, returning undefined if missing or invalid.
|
|
1107
|
+
* @remarks
|
|
1108
|
+
* Templates are actual files that need to be copied to the config directory.
|
|
1109
|
+
* This only works when core is in `node_modules` (CLI install, service).
|
|
1110
|
+
* When bundled into a consumer plugin, returns undefined and template
|
|
1111
|
+
* copying is skipped (templates are seeded by `jeeves install`, not plugins).
|
|
1179
1112
|
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1113
|
+
* Content `.md` files (soul, agents, platform template) are inlined at
|
|
1114
|
+
* build time via the rollup md plugin and imported as string literals.
|
|
1115
|
+
* They do not use this function.
|
|
1116
|
+
*
|
|
1117
|
+
* @returns Absolute path to the content/ directory, or undefined.
|
|
1182
1118
|
*/
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
1189
|
-
const parsed = JSON.parse(raw);
|
|
1190
|
-
return coreConfigSchema.parse(parsed);
|
|
1191
|
-
}
|
|
1192
|
-
catch {
|
|
1119
|
+
function getContentDir() {
|
|
1120
|
+
const pkgDir = packageDirectorySync({
|
|
1121
|
+
cwd: fileURLToPath(import.meta.url),
|
|
1122
|
+
});
|
|
1123
|
+
if (!pkgDir)
|
|
1193
1124
|
return undefined;
|
|
1194
|
-
|
|
1125
|
+
const dir = join(pkgDir, 'content');
|
|
1126
|
+
return existsSync(dir) ? dir : undefined;
|
|
1195
1127
|
}
|
|
1196
|
-
|
|
1197
|
-
/**
|
|
1198
|
-
* Service URL resolution.
|
|
1199
|
-
*
|
|
1200
|
-
* @remarks
|
|
1201
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
1202
|
-
* resolution order:
|
|
1203
|
-
* 1. Consumer's own component config
|
|
1204
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1205
|
-
* 3. Default port constants
|
|
1206
|
-
*/
|
|
1207
1128
|
/**
|
|
1208
|
-
*
|
|
1129
|
+
* Copy templates from content/templates/ to the core config directory.
|
|
1209
1130
|
*
|
|
1210
|
-
* @param
|
|
1211
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
1212
|
-
* @returns The resolved service URL.
|
|
1213
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1131
|
+
* @param coreConfigDir - Core config directory path.
|
|
1214
1132
|
*/
|
|
1215
|
-
function
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const coreDir = getCoreConfigDir();
|
|
1226
|
-
const coreConfig = loadConfig(coreDir);
|
|
1227
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1228
|
-
if (coreUrl)
|
|
1229
|
-
return coreUrl;
|
|
1230
|
-
// 3. Fall back to port constants
|
|
1231
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
1232
|
-
if (port !== undefined) {
|
|
1233
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
1133
|
+
function copyTemplates(coreConfigDir) {
|
|
1134
|
+
const contentDir = getContentDir();
|
|
1135
|
+
if (!contentDir)
|
|
1136
|
+
return;
|
|
1137
|
+
const sourceDir = join(contentDir, 'templates');
|
|
1138
|
+
if (!existsSync(sourceDir))
|
|
1139
|
+
return;
|
|
1140
|
+
const destDir = join(coreConfigDir, TEMPLATES_DIR);
|
|
1141
|
+
if (!existsSync(destDir)) {
|
|
1142
|
+
mkdirSync(destDir, { recursive: true });
|
|
1234
1143
|
}
|
|
1235
|
-
|
|
1144
|
+
cpSync(sourceDir, destDir, { recursive: true });
|
|
1236
1145
|
}
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* HTTP health probing for Jeeves platform services.
|
|
1240
|
-
*
|
|
1241
|
-
* @remarks
|
|
1242
|
-
* Probes service ports for health endpoints (HTTP GET to /status or /health).
|
|
1243
|
-
* Returns structured health data for rendering into TOOLS.md Platform section.
|
|
1244
|
-
*/
|
|
1245
1146
|
/**
|
|
1246
|
-
*
|
|
1147
|
+
* Render the Platform template using simple string replacement.
|
|
1247
1148
|
*
|
|
1248
|
-
* @param
|
|
1249
|
-
* @returns
|
|
1149
|
+
* @param templatePath - Path to the templates directory.
|
|
1150
|
+
* @returns Rendered platform content string.
|
|
1250
1151
|
*/
|
|
1251
|
-
function
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1152
|
+
function renderPlatformTemplate(templatePath) {
|
|
1153
|
+
const templatesAvailable = existsSync(templatePath);
|
|
1154
|
+
let content = toolsPlatformTemplate;
|
|
1155
|
+
// Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
|
|
1156
|
+
const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
|
|
1157
|
+
const match = ifRegex.exec(content);
|
|
1158
|
+
if (match) {
|
|
1159
|
+
content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
|
|
1258
1160
|
}
|
|
1161
|
+
// Replace __TEMPLATE_PATH__ with the actual path
|
|
1162
|
+
content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
|
|
1163
|
+
return content;
|
|
1259
1164
|
}
|
|
1260
1165
|
/**
|
|
1261
|
-
*
|
|
1166
|
+
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1262
1167
|
*
|
|
1263
|
-
* @param
|
|
1264
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
1265
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
1266
|
-
* @returns Probe result.
|
|
1168
|
+
* @param options - Configuration for the refresh cycle.
|
|
1267
1169
|
*/
|
|
1268
|
-
async function
|
|
1269
|
-
const
|
|
1270
|
-
const
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
});
|
|
1281
|
-
clearTimeout(timeout);
|
|
1282
|
-
if (response.ok) {
|
|
1283
|
-
let version;
|
|
1284
|
-
try {
|
|
1285
|
-
const body = await response.json();
|
|
1286
|
-
if (typeof body === 'object' &&
|
|
1287
|
-
body !== null &&
|
|
1288
|
-
'version' in body &&
|
|
1289
|
-
typeof body['version'] === 'string') {
|
|
1290
|
-
version = body['version'];
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
catch {
|
|
1294
|
-
// Non-JSON response is fine — we just don't get version info
|
|
1295
|
-
}
|
|
1296
|
-
return { name: serviceName, port, healthy: true, version };
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
catch {
|
|
1300
|
-
// Try next endpoint
|
|
1301
|
-
}
|
|
1170
|
+
async function refreshPlatformContent(options) {
|
|
1171
|
+
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
|
|
1172
|
+
const workspacePath = getWorkspacePath();
|
|
1173
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1174
|
+
// 1. Write calling component's version entry
|
|
1175
|
+
if (componentName) {
|
|
1176
|
+
writeComponentVersion(coreConfigDir, {
|
|
1177
|
+
componentName,
|
|
1178
|
+
pluginVersion: componentVersion,
|
|
1179
|
+
servicePackage,
|
|
1180
|
+
pluginPackage,
|
|
1181
|
+
});
|
|
1302
1182
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1183
|
+
// 2. Render Platform template
|
|
1184
|
+
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1185
|
+
const platformContent = renderPlatformTemplate(templatePath);
|
|
1186
|
+
// 3. Write TOOLS.md Platform section
|
|
1187
|
+
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1188
|
+
await updateManagedSection(toolsPath, platformContent, {
|
|
1189
|
+
mode: 'section',
|
|
1190
|
+
sectionId: 'Platform',
|
|
1191
|
+
markers: TOOLS_MARKERS,
|
|
1192
|
+
coreVersion,
|
|
1193
|
+
stalenessThresholdMs,
|
|
1194
|
+
});
|
|
1195
|
+
// 4. Write SOUL.md managed block
|
|
1196
|
+
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1197
|
+
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1198
|
+
mode: 'block',
|
|
1199
|
+
markers: SOUL_MARKERS,
|
|
1200
|
+
coreVersion,
|
|
1201
|
+
stalenessThresholdMs,
|
|
1202
|
+
});
|
|
1203
|
+
// 5. Write AGENTS.md managed block
|
|
1204
|
+
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1205
|
+
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1206
|
+
mode: 'block',
|
|
1207
|
+
markers: AGENTS_MARKERS,
|
|
1208
|
+
coreVersion,
|
|
1209
|
+
stalenessThresholdMs,
|
|
1210
|
+
});
|
|
1211
|
+
// 6. Copy templates to config dir
|
|
1212
|
+
copyTemplates(coreConfigDir);
|
|
1316
1213
|
}
|
|
1317
1214
|
|
|
1318
1215
|
/**
|
|
1319
|
-
*
|
|
1216
|
+
* Timer-based orchestrator for managed content writing.
|
|
1320
1217
|
*
|
|
1321
1218
|
* @remarks
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1219
|
+
* `ComponentWriter` manages a component's TOOLS.md section writes
|
|
1220
|
+
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
1221
|
+
* on a configurable prime-interval timer cycle.
|
|
1324
1222
|
*/
|
|
1325
1223
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
1329
|
-
* @param cacheDir - Directory to store the cache file.
|
|
1330
|
-
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
1331
|
-
* @returns The latest version string, or undefined if the check fails.
|
|
1332
|
-
*/
|
|
1333
|
-
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
1334
|
-
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
1335
|
-
// Check cache first
|
|
1336
|
-
if (existsSync(cachePath)) {
|
|
1337
|
-
try {
|
|
1338
|
-
const raw = readFileSync(cachePath, 'utf-8');
|
|
1339
|
-
const entry = JSON.parse(raw);
|
|
1340
|
-
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
1341
|
-
if (age < ttlSeconds * 1000) {
|
|
1342
|
-
return entry.version;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
catch {
|
|
1346
|
-
// Cache corrupt — proceed with fresh check
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
// Query npm registry
|
|
1350
|
-
try {
|
|
1351
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
1352
|
-
encoding: 'utf-8',
|
|
1353
|
-
timeout: 15_000,
|
|
1354
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1355
|
-
}).trim();
|
|
1356
|
-
if (!result)
|
|
1357
|
-
return undefined;
|
|
1358
|
-
// Write cache
|
|
1359
|
-
if (!existsSync(cacheDir)) {
|
|
1360
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
1361
|
-
}
|
|
1362
|
-
const entry = {
|
|
1363
|
-
version: result,
|
|
1364
|
-
checkedAt: new Date().toISOString(),
|
|
1365
|
-
};
|
|
1366
|
-
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
1367
|
-
return result;
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
return undefined;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Build enriched service rows for the Platform template.
|
|
1376
|
-
*
|
|
1377
|
-
* @remarks
|
|
1378
|
-
* Merges health probe results with component version state and
|
|
1379
|
-
* npm registry update availability into rows for the Handlebars
|
|
1380
|
-
* Platform template.
|
|
1381
|
-
*/
|
|
1382
|
-
/**
|
|
1383
|
-
* Check whether an available version is newer than the current one.
|
|
1384
|
-
*
|
|
1385
|
-
* @param available - Registry version string.
|
|
1386
|
-
* @param current - Currently installed version string.
|
|
1387
|
-
* @returns The available version if it's newer, otherwise undefined.
|
|
1388
|
-
*/
|
|
1389
|
-
function newerVersion(available, current) {
|
|
1390
|
-
if (!available ||
|
|
1391
|
-
!current ||
|
|
1392
|
-
!semver.valid(available) ||
|
|
1393
|
-
!semver.valid(current)) {
|
|
1394
|
-
return undefined;
|
|
1395
|
-
}
|
|
1396
|
-
return semver.gt(available, current) ? available : undefined;
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* Build enriched service rows for the Platform Handlebars template.
|
|
1400
|
-
*
|
|
1401
|
-
* @param options - Probe results, version state, and configuration.
|
|
1402
|
-
* @returns Array of enriched service rows.
|
|
1403
|
-
*/
|
|
1404
|
-
function buildServiceRows(options) {
|
|
1405
|
-
const { probeResults, componentVersions, cacheDir, skipRegistryCheck } = options;
|
|
1406
|
-
return probeResults.map((r) => {
|
|
1407
|
-
const entry = componentVersions[r.name];
|
|
1408
|
-
if (!entry)
|
|
1409
|
-
return { ...r };
|
|
1410
|
-
let availableServiceVersion;
|
|
1411
|
-
let availablePluginVersion;
|
|
1412
|
-
if (!skipRegistryCheck) {
|
|
1413
|
-
if (entry.servicePackage) {
|
|
1414
|
-
const registryVersion = checkRegistryVersion(entry.servicePackage, cacheDir);
|
|
1415
|
-
availableServiceVersion = newerVersion(registryVersion, r.version);
|
|
1416
|
-
}
|
|
1417
|
-
if (entry.pluginPackage && entry.pluginVersion) {
|
|
1418
|
-
const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
|
|
1419
|
-
availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
return {
|
|
1423
|
-
...r,
|
|
1424
|
-
pluginVersion: entry.pluginVersion,
|
|
1425
|
-
availableServiceVersion,
|
|
1426
|
-
availablePluginVersion,
|
|
1427
|
-
};
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
/**
|
|
1432
|
-
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1433
|
-
*
|
|
1434
|
-
* @remarks
|
|
1435
|
-
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
1436
|
-
* Probes service ports for health, reads content files from the package's
|
|
1437
|
-
* `content/` directory, renders the Platform template with live service data,
|
|
1438
|
-
* and writes managed sections using `updateManagedSection`.
|
|
1439
|
-
*/
|
|
1440
|
-
/**
|
|
1441
|
-
* Resolve the package's content directory for template file copying.
|
|
1442
|
-
*
|
|
1443
|
-
* @remarks
|
|
1444
|
-
* Templates are actual files that need to be copied to the config directory.
|
|
1445
|
-
* This only works when core is in `node_modules` (CLI install, service).
|
|
1446
|
-
* When bundled into a consumer plugin, returns undefined and template
|
|
1447
|
-
* copying is skipped (templates are seeded by `jeeves install`, not plugins).
|
|
1448
|
-
*
|
|
1449
|
-
* Content `.md` files (soul, agents, platform template) are inlined at
|
|
1450
|
-
* build time via the rollup md plugin and imported as string literals.
|
|
1451
|
-
* They do not use this function.
|
|
1452
|
-
*
|
|
1453
|
-
* @returns Absolute path to the content/ directory, or undefined.
|
|
1454
|
-
*/
|
|
1455
|
-
function getContentDir() {
|
|
1456
|
-
const pkgDir = packageDirectorySync({
|
|
1457
|
-
cwd: fileURLToPath(import.meta.url),
|
|
1458
|
-
});
|
|
1459
|
-
if (!pkgDir)
|
|
1460
|
-
return undefined;
|
|
1461
|
-
const dir = join(pkgDir, 'content');
|
|
1462
|
-
return existsSync(dir) ? dir : undefined;
|
|
1463
|
-
}
|
|
1464
|
-
/**
|
|
1465
|
-
* Copy templates from content/templates/ to the core config directory.
|
|
1466
|
-
*
|
|
1467
|
-
* @param coreConfigDir - Core config directory path.
|
|
1468
|
-
*/
|
|
1469
|
-
function copyTemplates(coreConfigDir) {
|
|
1470
|
-
const contentDir = getContentDir();
|
|
1471
|
-
if (!contentDir)
|
|
1472
|
-
return;
|
|
1473
|
-
const sourceDir = join(contentDir, 'templates');
|
|
1474
|
-
if (!existsSync(sourceDir))
|
|
1475
|
-
return;
|
|
1476
|
-
const destDir = join(coreConfigDir, TEMPLATES_DIR);
|
|
1477
|
-
if (!existsSync(destDir)) {
|
|
1478
|
-
mkdirSync(destDir, { recursive: true });
|
|
1479
|
-
}
|
|
1480
|
-
cpSync(sourceDir, destDir, { recursive: true });
|
|
1481
|
-
}
|
|
1482
|
-
/** Whether Handlebars helpers have been registered. */
|
|
1483
|
-
let helpersRegistered = false;
|
|
1484
|
-
/** Register Handlebars helpers used in the Platform template. */
|
|
1485
|
-
function registerHelpers() {
|
|
1486
|
-
if (helpersRegistered)
|
|
1487
|
-
return;
|
|
1488
|
-
helpersRegistered = true;
|
|
1489
|
-
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* Check if a newer core version is available on npm.
|
|
1493
|
-
*
|
|
1494
|
-
* @returns The newer version string, or undefined.
|
|
1495
|
-
*/
|
|
1496
|
-
function checkCoreUpdate(coreVersion, cacheDir) {
|
|
1497
|
-
const registryVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
|
|
1498
|
-
if (registryVersion &&
|
|
1499
|
-
semver.valid(registryVersion) &&
|
|
1500
|
-
semver.valid(coreVersion) &&
|
|
1501
|
-
semver.gt(registryVersion, coreVersion)) {
|
|
1502
|
-
return registryVersion;
|
|
1503
|
-
}
|
|
1504
|
-
return undefined;
|
|
1505
|
-
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1508
|
-
*
|
|
1509
|
-
* @param options - Configuration for the refresh cycle.
|
|
1510
|
-
*/
|
|
1511
|
-
async function refreshPlatformContent(options) {
|
|
1512
|
-
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
|
|
1513
|
-
const workspacePath = getWorkspacePath();
|
|
1514
|
-
const coreConfigDir = getCoreConfigDir();
|
|
1515
|
-
// 1. Probe all services
|
|
1516
|
-
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1517
|
-
// 2. Write calling component's version entry (with serviceVersion from probe)
|
|
1518
|
-
if (componentName) {
|
|
1519
|
-
const callerProbe = probeResults.find((r) => r.name === componentName);
|
|
1520
|
-
writeComponentVersion(coreConfigDir, {
|
|
1521
|
-
componentName,
|
|
1522
|
-
serviceVersion: callerProbe?.version,
|
|
1523
|
-
pluginVersion: componentVersion,
|
|
1524
|
-
servicePackage,
|
|
1525
|
-
pluginPackage,
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
// 3. Read all component versions from the shared state file
|
|
1529
|
-
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1530
|
-
// 4. Build enriched service rows with registry checks
|
|
1531
|
-
const cacheDir = componentName
|
|
1532
|
-
? getComponentConfigDir(componentName)
|
|
1533
|
-
: coreConfigDir;
|
|
1534
|
-
const availableCoreVersion = skipRegistryCheck
|
|
1535
|
-
? undefined
|
|
1536
|
-
: checkCoreUpdate(coreVersion, cacheDir);
|
|
1537
|
-
const serviceRows = buildServiceRows({
|
|
1538
|
-
probeResults,
|
|
1539
|
-
componentVersions,
|
|
1540
|
-
cacheDir,
|
|
1541
|
-
skipRegistryCheck,
|
|
1542
|
-
});
|
|
1543
|
-
// 5. Render Platform template
|
|
1544
|
-
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1545
|
-
registerHelpers();
|
|
1546
|
-
const template = Handlebars.compile(toolsPlatformTemplate);
|
|
1547
|
-
const templateData = {
|
|
1548
|
-
services: serviceRows,
|
|
1549
|
-
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1550
|
-
coreVersion,
|
|
1551
|
-
availableCoreVersion,
|
|
1552
|
-
templatesAvailable: existsSync(templatePath),
|
|
1553
|
-
templatePath,
|
|
1554
|
-
};
|
|
1555
|
-
const platformContent = template(templateData);
|
|
1556
|
-
// 6. Write TOOLS.md Platform section
|
|
1557
|
-
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1558
|
-
await updateManagedSection(toolsPath, platformContent, {
|
|
1559
|
-
mode: 'section',
|
|
1560
|
-
sectionId: 'Platform',
|
|
1561
|
-
markers: TOOLS_MARKERS,
|
|
1562
|
-
coreVersion,
|
|
1563
|
-
stalenessThresholdMs,
|
|
1564
|
-
});
|
|
1565
|
-
// 7. Write SOUL.md managed block
|
|
1566
|
-
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1567
|
-
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1568
|
-
mode: 'block',
|
|
1569
|
-
markers: SOUL_MARKERS,
|
|
1570
|
-
coreVersion,
|
|
1571
|
-
stalenessThresholdMs,
|
|
1572
|
-
});
|
|
1573
|
-
// 8. Write AGENTS.md managed block
|
|
1574
|
-
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1575
|
-
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1576
|
-
mode: 'block',
|
|
1577
|
-
markers: AGENTS_MARKERS,
|
|
1578
|
-
coreVersion,
|
|
1579
|
-
stalenessThresholdMs,
|
|
1580
|
-
});
|
|
1581
|
-
// 9. Copy templates to config dir
|
|
1582
|
-
copyTemplates(coreConfigDir);
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
/**
|
|
1586
|
-
* Timer-based orchestrator for managed content writing.
|
|
1587
|
-
*
|
|
1588
|
-
* @remarks
|
|
1589
|
-
* `ComponentWriter` manages a component's TOOLS.md section writes
|
|
1590
|
-
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
1591
|
-
* on a configurable prime-interval timer cycle.
|
|
1592
|
-
*/
|
|
1593
|
-
/**
|
|
1594
|
-
* Orchestrates managed content writing for a single Jeeves component.
|
|
1224
|
+
* Orchestrates managed content writing for a single Jeeves component.
|
|
1595
1225
|
*
|
|
1596
1226
|
* @remarks
|
|
1597
1227
|
* Created via `createComponentWriter()`. Manages a timer that fires
|
|
@@ -1602,12 +1232,10 @@ class ComponentWriter {
|
|
|
1602
1232
|
timer;
|
|
1603
1233
|
component;
|
|
1604
1234
|
configDir;
|
|
1605
|
-
probeTimeoutMs;
|
|
1606
1235
|
/** @internal */
|
|
1607
|
-
constructor(component
|
|
1236
|
+
constructor(component) {
|
|
1608
1237
|
this.component = component;
|
|
1609
1238
|
this.configDir = getComponentConfigDir(component.name);
|
|
1610
|
-
this.probeTimeoutMs = probeTimeoutMs;
|
|
1611
1239
|
}
|
|
1612
1240
|
/** The component's config directory path. */
|
|
1613
1241
|
get componentConfigDir() {
|
|
@@ -1658,16 +1286,12 @@ class ComponentWriter {
|
|
|
1658
1286
|
coreVersion: CORE_VERSION,
|
|
1659
1287
|
});
|
|
1660
1288
|
// Platform content maintenance: SOUL.md, AGENTS.md, Platform section
|
|
1661
|
-
// refreshPlatformContent also writes the component version entry
|
|
1662
|
-
// (with serviceVersion from probe) to the shared state file.
|
|
1663
1289
|
await refreshPlatformContent({
|
|
1664
1290
|
coreVersion: CORE_VERSION,
|
|
1665
1291
|
componentName: this.component.name,
|
|
1666
1292
|
componentVersion: this.component.version,
|
|
1667
1293
|
servicePackage: this.component.servicePackage,
|
|
1668
1294
|
pluginPackage: this.component.pluginPackage,
|
|
1669
|
-
skipRegistryCheck: false,
|
|
1670
|
-
probeTimeoutMs: this.probeTimeoutMs,
|
|
1671
1295
|
});
|
|
1672
1296
|
}
|
|
1673
1297
|
catch (err) {
|
|
@@ -1804,64 +1428,211 @@ function validateDescriptor(input) {
|
|
|
1804
1428
|
* Create a ComponentWriter for a validated component descriptor.
|
|
1805
1429
|
*
|
|
1806
1430
|
* @param component - The component descriptor to validate and wrap.
|
|
1807
|
-
* @param options - Optional configuration.
|
|
1808
1431
|
* @returns A new `ComponentWriter` instance.
|
|
1809
1432
|
* @throws Error if the component descriptor is invalid.
|
|
1810
1433
|
*/
|
|
1811
|
-
function createComponentWriter(component
|
|
1434
|
+
function createComponentWriter(component) {
|
|
1812
1435
|
validateDescriptor(component);
|
|
1813
|
-
return new ComponentWriter(component
|
|
1436
|
+
return new ComponentWriter(component);
|
|
1814
1437
|
}
|
|
1815
1438
|
|
|
1816
1439
|
/**
|
|
1817
|
-
*
|
|
1440
|
+
* Core configuration schema and resolution.
|
|
1818
1441
|
*
|
|
1819
1442
|
* @remarks
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1443
|
+
* Core config lives at `{configRoot}/jeeves-core/config.json`.
|
|
1444
|
+
* Config resolution order:
|
|
1445
|
+
* 1. Component's own config file
|
|
1446
|
+
* 2. Core config file
|
|
1447
|
+
* 3. Hardcoded library defaults
|
|
1823
1448
|
*/
|
|
1449
|
+
/** Zod schema for a service entry in core config. */
|
|
1450
|
+
const serviceEntrySchema = z.object({
|
|
1451
|
+
/** Service URL (must be a valid URL). */
|
|
1452
|
+
url: z.string().url().describe('Service URL'),
|
|
1453
|
+
});
|
|
1454
|
+
/** Zod schema for the core config file. */
|
|
1455
|
+
const coreConfigSchema = z.object({
|
|
1456
|
+
/** JSON Schema pointer for IDE autocomplete. */
|
|
1457
|
+
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
1458
|
+
/** Owner identity keys (canonical identityLinks references). */
|
|
1459
|
+
owners: z.array(z.string()).default([]).describe('Owner identity keys'),
|
|
1460
|
+
/** Service URL overrides keyed by service name. */
|
|
1461
|
+
services: z
|
|
1462
|
+
.record(z.string(), serviceEntrySchema)
|
|
1463
|
+
.default({})
|
|
1464
|
+
.describe('Service URL overrides'),
|
|
1465
|
+
/** Registry cache configuration. */
|
|
1466
|
+
registryCache: z
|
|
1467
|
+
.object({
|
|
1468
|
+
/** Cache TTL in seconds for npm registry queries. */
|
|
1469
|
+
ttlSeconds: z
|
|
1470
|
+
.number()
|
|
1471
|
+
.int()
|
|
1472
|
+
.positive()
|
|
1473
|
+
.default(3600)
|
|
1474
|
+
.describe('Cache TTL in seconds'),
|
|
1475
|
+
})
|
|
1476
|
+
.default({})
|
|
1477
|
+
.describe('Registry cache settings'),
|
|
1478
|
+
});
|
|
1824
1479
|
/**
|
|
1825
|
-
*
|
|
1480
|
+
* Generate a JSON Schema from the Zod schema for `$schema` pointer support.
|
|
1481
|
+
*
|
|
1482
|
+
* @returns A JSON Schema object.
|
|
1483
|
+
*/
|
|
1484
|
+
function generateJsonSchema() {
|
|
1485
|
+
return {
|
|
1486
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
1487
|
+
title: 'Jeeves Core Configuration',
|
|
1488
|
+
type: 'object',
|
|
1489
|
+
properties: {
|
|
1490
|
+
$schema: { type: 'string' },
|
|
1491
|
+
owners: {
|
|
1492
|
+
type: 'array',
|
|
1493
|
+
items: { type: 'string' },
|
|
1494
|
+
default: [],
|
|
1495
|
+
},
|
|
1496
|
+
services: {
|
|
1497
|
+
type: 'object',
|
|
1498
|
+
additionalProperties: {
|
|
1499
|
+
type: 'object',
|
|
1500
|
+
properties: {
|
|
1501
|
+
url: { type: 'string', format: 'uri' },
|
|
1502
|
+
},
|
|
1503
|
+
required: ['url'],
|
|
1504
|
+
},
|
|
1505
|
+
default: {},
|
|
1506
|
+
},
|
|
1507
|
+
registryCache: {
|
|
1508
|
+
type: 'object',
|
|
1509
|
+
properties: {
|
|
1510
|
+
ttlSeconds: {
|
|
1511
|
+
type: 'integer',
|
|
1512
|
+
minimum: 1,
|
|
1513
|
+
default: 3600,
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
default: {},
|
|
1517
|
+
},
|
|
1518
|
+
},
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Load and parse a config file, returning undefined if missing or invalid.
|
|
1523
|
+
*
|
|
1524
|
+
* @param configDir - Directory containing config.json.
|
|
1525
|
+
* @returns Parsed config or undefined.
|
|
1526
|
+
*/
|
|
1527
|
+
function loadConfig(configDir) {
|
|
1528
|
+
const configPath = join(configDir, CONFIG_FILE);
|
|
1529
|
+
if (!existsSync(configPath))
|
|
1530
|
+
return undefined;
|
|
1531
|
+
try {
|
|
1532
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
1533
|
+
const parsed = JSON.parse(raw);
|
|
1534
|
+
return coreConfigSchema.parse(parsed);
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return undefined;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Service URL resolution.
|
|
1826
1543
|
*
|
|
1827
1544
|
* @remarks
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1545
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
1546
|
+
* resolution order:
|
|
1547
|
+
* 1. Consumer's own component config
|
|
1548
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1549
|
+
* 3. Default port constants
|
|
1550
|
+
*/
|
|
1551
|
+
/**
|
|
1552
|
+
* Resolve the URL for a named Jeeves service.
|
|
1832
1553
|
*
|
|
1833
|
-
* @param
|
|
1834
|
-
* @
|
|
1554
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1555
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
1556
|
+
* @returns The resolved service URL.
|
|
1557
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1835
1558
|
*/
|
|
1836
|
-
function
|
|
1837
|
-
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
1559
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
1560
|
+
// 1. Check consumer's own config
|
|
1561
|
+
if (consumerName) {
|
|
1562
|
+
const consumerDir = getComponentConfigDir(consumerName);
|
|
1563
|
+
const consumerConfig = loadConfig(consumerDir);
|
|
1564
|
+
const consumerUrl = consumerConfig?.services[serviceName]?.url;
|
|
1565
|
+
if (consumerUrl)
|
|
1566
|
+
return consumerUrl;
|
|
1840
1567
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1568
|
+
// 2. Check core config
|
|
1569
|
+
const coreDir = getCoreConfigDir();
|
|
1570
|
+
const coreConfig = loadConfig(coreDir);
|
|
1571
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1572
|
+
if (coreUrl)
|
|
1573
|
+
return coreUrl;
|
|
1574
|
+
// 3. Fall back to port constants
|
|
1575
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
1576
|
+
if (port !== undefined) {
|
|
1577
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
1843
1578
|
}
|
|
1844
|
-
|
|
1579
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1845
1580
|
}
|
|
1581
|
+
|
|
1846
1582
|
/**
|
|
1847
|
-
*
|
|
1848
|
-
* plugin config → environment variable → fallback value.
|
|
1583
|
+
* Registry version cache for npm package update awareness.
|
|
1849
1584
|
*
|
|
1850
|
-
* @
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1853
|
-
* @param envVar - Environment variable name.
|
|
1854
|
-
* @param fallback - Default value if neither source provides one.
|
|
1855
|
-
* @returns The resolved setting value.
|
|
1585
|
+
* @remarks
|
|
1586
|
+
* Caches the latest npm registry version in a local JSON file
|
|
1587
|
+
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
1856
1588
|
*/
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1589
|
+
/**
|
|
1590
|
+
* Check the npm registry for the latest version of a package.
|
|
1591
|
+
*
|
|
1592
|
+
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
1593
|
+
* @param cacheDir - Directory to store the cache file.
|
|
1594
|
+
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
1595
|
+
* @returns The latest version string, or undefined if the check fails.
|
|
1596
|
+
*/
|
|
1597
|
+
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
1598
|
+
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
1599
|
+
// Check cache first
|
|
1600
|
+
if (existsSync(cachePath)) {
|
|
1601
|
+
try {
|
|
1602
|
+
const raw = readFileSync(cachePath, 'utf-8');
|
|
1603
|
+
const entry = JSON.parse(raw);
|
|
1604
|
+
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
1605
|
+
if (age < ttlSeconds * 1000) {
|
|
1606
|
+
return entry.version;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
catch {
|
|
1610
|
+
// Cache corrupt — proceed with fresh check
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
// Query npm registry
|
|
1614
|
+
try {
|
|
1615
|
+
const result = execSync(`npm view ${packageName} version`, {
|
|
1616
|
+
encoding: 'utf-8',
|
|
1617
|
+
timeout: 15_000,
|
|
1618
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1619
|
+
}).trim();
|
|
1620
|
+
if (!result)
|
|
1621
|
+
return undefined;
|
|
1622
|
+
// Write cache
|
|
1623
|
+
if (!existsSync(cacheDir)) {
|
|
1624
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
1625
|
+
}
|
|
1626
|
+
const entry = {
|
|
1627
|
+
version: result,
|
|
1628
|
+
checkedAt: new Date().toISOString(),
|
|
1629
|
+
};
|
|
1630
|
+
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
return undefined;
|
|
1635
|
+
}
|
|
1865
1636
|
}
|
|
1866
1637
|
|
|
1867
1638
|
/**
|
|
@@ -2007,8 +1778,6 @@ async function seedContent(options) {
|
|
|
2007
1778
|
// Seed content via the same code path as writer cycles
|
|
2008
1779
|
await refreshPlatformContent({
|
|
2009
1780
|
coreVersion: options.coreVersion,
|
|
2010
|
-
probeTimeoutMs: options.probeTimeoutMs ?? 3000,
|
|
2011
|
-
skipRegistryCheck: options.skipRegistryCheck ?? true,
|
|
2012
1781
|
});
|
|
2013
1782
|
}
|
|
2014
1783
|
|
|
@@ -2019,6 +1788,26 @@ async function seedContent(options) {
|
|
|
2019
1788
|
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
2020
1789
|
* and handle JSON serialisation/deserialisation.
|
|
2021
1790
|
*/
|
|
1791
|
+
/**
|
|
1792
|
+
* Fetch a URL with an automatic abort timeout.
|
|
1793
|
+
*
|
|
1794
|
+
* @param url - URL to fetch.
|
|
1795
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1796
|
+
* @param init - Optional `fetch` init options.
|
|
1797
|
+
* @returns The fetch Response object.
|
|
1798
|
+
*/
|
|
1799
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1800
|
+
const controller = new AbortController();
|
|
1801
|
+
const timeout = setTimeout(() => {
|
|
1802
|
+
controller.abort();
|
|
1803
|
+
}, timeoutMs);
|
|
1804
|
+
try {
|
|
1805
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1806
|
+
}
|
|
1807
|
+
finally {
|
|
1808
|
+
clearTimeout(timeout);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
2022
1811
|
/**
|
|
2023
1812
|
* Fetch JSON from a URL, throwing on non-OK responses.
|
|
2024
1813
|
*
|
|
@@ -2167,6 +1956,77 @@ function patchConfig(config, pluginId, mode) {
|
|
|
2167
1956
|
return messages;
|
|
2168
1957
|
}
|
|
2169
1958
|
|
|
1959
|
+
/**
|
|
1960
|
+
* Plugin resolution helpers for the OpenClaw plugin SDK.
|
|
1961
|
+
*
|
|
1962
|
+
* @remarks
|
|
1963
|
+
* Provides workspace path resolution and plugin setting resolution
|
|
1964
|
+
* with a standard three-step fallback chain:
|
|
1965
|
+
* plugin config → environment variable → default value.
|
|
1966
|
+
*/
|
|
1967
|
+
/**
|
|
1968
|
+
* Resolve the workspace root from the OpenClaw plugin API.
|
|
1969
|
+
*
|
|
1970
|
+
* @remarks
|
|
1971
|
+
* Tries three sources in order:
|
|
1972
|
+
* 1. `api.config.agents.defaults.workspace` — explicit config
|
|
1973
|
+
* 2. `api.resolvePath('.')` — gateway-provided path resolver
|
|
1974
|
+
* 3. `process.cwd()` — last resort
|
|
1975
|
+
*
|
|
1976
|
+
* @param api - The plugin API object provided by the gateway.
|
|
1977
|
+
* @returns Absolute path to the workspace root.
|
|
1978
|
+
*/
|
|
1979
|
+
function resolveWorkspacePath(api) {
|
|
1980
|
+
const configured = api.config?.agents?.defaults?.workspace;
|
|
1981
|
+
if (typeof configured === 'string' && configured.trim()) {
|
|
1982
|
+
return configured;
|
|
1983
|
+
}
|
|
1984
|
+
if (typeof api.resolvePath === 'function') {
|
|
1985
|
+
return api.resolvePath('.');
|
|
1986
|
+
}
|
|
1987
|
+
return process.cwd();
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Resolve a plugin setting via the standard three-step fallback chain:
|
|
1991
|
+
* plugin config → environment variable → fallback value.
|
|
1992
|
+
*
|
|
1993
|
+
* @param api - Plugin API object.
|
|
1994
|
+
* @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
|
|
1995
|
+
* @param key - Config key within the plugin's config object.
|
|
1996
|
+
* @param envVar - Environment variable name.
|
|
1997
|
+
* @param fallback - Default value if neither source provides one.
|
|
1998
|
+
* @returns The resolved setting value.
|
|
1999
|
+
*/
|
|
2000
|
+
function resolvePluginSetting(api, pluginId, key, envVar, fallback) {
|
|
2001
|
+
const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
|
|
2002
|
+
if (typeof fromPlugin === 'string')
|
|
2003
|
+
return fromPlugin;
|
|
2004
|
+
const fromEnv = process.env[envVar];
|
|
2005
|
+
if (fromEnv)
|
|
2006
|
+
return fromEnv;
|
|
2007
|
+
return fallback;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Resolve an optional plugin setting via the two-step fallback chain:
|
|
2011
|
+
* plugin config → environment variable. Returns `undefined` if neither
|
|
2012
|
+
* source provides a value.
|
|
2013
|
+
*
|
|
2014
|
+
* @param api - Plugin API object.
|
|
2015
|
+
* @param pluginId - Plugin identifier (e.g., 'jeeves-watcher-openclaw').
|
|
2016
|
+
* @param key - Config key within the plugin's config object.
|
|
2017
|
+
* @param envVar - Environment variable name.
|
|
2018
|
+
* @returns The resolved setting value, or `undefined`.
|
|
2019
|
+
*/
|
|
2020
|
+
function resolveOptionalPluginSetting(api, pluginId, key, envVar) {
|
|
2021
|
+
const fromPlugin = api.config?.plugins?.entries?.[pluginId]?.config?.[key];
|
|
2022
|
+
if (typeof fromPlugin === 'string')
|
|
2023
|
+
return fromPlugin;
|
|
2024
|
+
const fromEnv = process.env[envVar];
|
|
2025
|
+
if (fromEnv)
|
|
2026
|
+
return fromEnv;
|
|
2027
|
+
return undefined;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2170
2030
|
/**
|
|
2171
2031
|
* Tool result formatters for the OpenClaw plugin SDK.
|
|
2172
2032
|
*
|
|
@@ -2236,4 +2096,4 @@ function connectionFail(error, baseUrl, pluginId) {
|
|
|
2236
2096
|
return fail(error);
|
|
2237
2097
|
}
|
|
2238
2098
|
|
|
2239
|
-
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_CORE_VERSION, DEFAULT_PORTS, META_PORT, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, atomicWrite, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigQueryHandler, fail, fetchJson, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, ok, parseManaged, patchConfig, postJson,
|
|
2099
|
+
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_CORE_VERSION, DEFAULT_PORTS, META_PORT, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, atomicWrite, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigQueryHandler, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getServiceUrl, getWorkspacePath, init, jaccard, needsCleanup, ok, parseManaged, patchConfig, postJson, readComponentVersions, refreshPlatformContent, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, seedContent, shingles, shouldWrite, updateManagedSection, withFileLock, writeComponentVersion };
|