@karmaniverous/jeeves 0.5.1 → 0.5.3
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 +4 -4
- package/content/agents-section.md +1 -31
- package/content/skill.md +17 -0
- package/content/soul-section.md +1 -11
- package/dist/cli/jeeves/index.js +90 -71
- package/dist/cli/plugin/index.js +99 -20
- package/dist/cli/service/index.js +36 -27
- package/dist/index.d.ts +64 -23
- package/dist/index.js +272 -122
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs, { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, cpSync } from 'node:fs';
|
|
2
|
-
import path, { join, dirname,
|
|
2
|
+
import path, { join, dirname, basename, resolve } from 'node:path';
|
|
3
|
+
import crypto, { randomUUID } from 'node:crypto';
|
|
3
4
|
import { lock } from 'proper-lockfile';
|
|
4
5
|
import { JSONPath } from 'jsonpath-plus';
|
|
5
6
|
import { major, valid, gte, gt } from 'semver';
|
|
@@ -9,7 +10,6 @@ import { packageDirectorySync } from 'package-directory';
|
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
11
|
import cp, { execSync } from 'node:child_process';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Comment markers for managed content blocks.
|
|
@@ -183,14 +183,14 @@ const PLATFORM_COMPONENTS = [
|
|
|
183
183
|
* Core library version, inlined at build time.
|
|
184
184
|
*
|
|
185
185
|
* @remarks
|
|
186
|
-
* The `0.5.
|
|
186
|
+
* The `0.5.2` placeholder is replaced by
|
|
187
187
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
188
188
|
* from `package.json`. This ensures the correct version survives
|
|
189
189
|
* when consumers bundle core into their own dist (where runtime
|
|
190
190
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
191
191
|
*/
|
|
192
192
|
/** The core library version from package.json (inlined at build time). */
|
|
193
|
-
const CORE_VERSION = '0.5.
|
|
193
|
+
const CORE_VERSION = '0.5.2';
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Workspace and config root initialization.
|
|
@@ -288,27 +288,46 @@ const STALE_LOCK_MS = 120_000;
|
|
|
288
288
|
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
289
289
|
/** Lock retry options. */
|
|
290
290
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
291
|
+
/** Maximum rename retry attempts on EPERM. */
|
|
292
|
+
const ATOMIC_WRITE_MAX_RETRIES = 3;
|
|
293
|
+
/** Delay between EPERM retries in milliseconds. */
|
|
294
|
+
const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
|
|
291
295
|
/**
|
|
292
296
|
* Write content to a file atomically via a temp file + rename.
|
|
293
297
|
*
|
|
298
|
+
* @remarks
|
|
299
|
+
* Retries the rename up to three times on EPERM (Windows file-handle
|
|
300
|
+
* contention) with a 100 ms synchronous delay between attempts.
|
|
301
|
+
*
|
|
294
302
|
* @param filePath - Absolute path to the target file.
|
|
295
303
|
* @param content - Content to write.
|
|
296
304
|
*/
|
|
297
305
|
function atomicWrite(filePath, content) {
|
|
298
306
|
const dir = dirname(filePath);
|
|
299
|
-
const
|
|
307
|
+
const base = basename(filePath, '.md');
|
|
308
|
+
const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
|
|
300
309
|
writeFileSync(tempPath, content, 'utf-8');
|
|
301
|
-
|
|
302
|
-
renameSync(tempPath, filePath);
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
310
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
305
311
|
try {
|
|
306
|
-
|
|
312
|
+
renameSync(tempPath, filePath);
|
|
313
|
+
return;
|
|
307
314
|
}
|
|
308
|
-
catch {
|
|
309
|
-
|
|
315
|
+
catch (err) {
|
|
316
|
+
const isEperm = err instanceof Error &&
|
|
317
|
+
'code' in err &&
|
|
318
|
+
err.code === 'EPERM';
|
|
319
|
+
if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
|
|
320
|
+
try {
|
|
321
|
+
unlinkSync(tempPath);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* best-effort cleanup */
|
|
325
|
+
}
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
// Synchronous sleep before retry (acceptable in atomic write context)
|
|
329
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
|
|
310
330
|
}
|
|
311
|
-
throw err;
|
|
312
331
|
}
|
|
313
332
|
}
|
|
314
333
|
/**
|
|
@@ -343,6 +362,21 @@ async function withFileLock(filePath, fn) {
|
|
|
343
362
|
}
|
|
344
363
|
}
|
|
345
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Shared internal utility functions.
|
|
367
|
+
*
|
|
368
|
+
* @packageDocumentation
|
|
369
|
+
*/
|
|
370
|
+
/**
|
|
371
|
+
* Extract a human-readable message from an unknown caught value.
|
|
372
|
+
*
|
|
373
|
+
* @param err - The caught value (typically `unknown`).
|
|
374
|
+
* @returns The error message string.
|
|
375
|
+
*/
|
|
376
|
+
function getErrorMessage(err) {
|
|
377
|
+
return err instanceof Error ? err.message : String(err);
|
|
378
|
+
}
|
|
379
|
+
|
|
346
380
|
/**
|
|
347
381
|
* Factory for a framework-agnostic config apply HTTP handler.
|
|
348
382
|
*
|
|
@@ -395,8 +429,7 @@ function readConfigFile(filePath) {
|
|
|
395
429
|
return JSON.parse(raw);
|
|
396
430
|
}
|
|
397
431
|
catch (err) {
|
|
398
|
-
|
|
399
|
-
console.warn(`jeeves-core: Could not read config file ${filePath}: ${msg}`);
|
|
432
|
+
console.warn(`jeeves-core: Could not read config file ${filePath}: ${getErrorMessage(err)}`);
|
|
400
433
|
return {};
|
|
401
434
|
}
|
|
402
435
|
}
|
|
@@ -445,10 +478,9 @@ function createConfigApplyHandler(descriptor) {
|
|
|
445
478
|
atomicWrite(configPath, json);
|
|
446
479
|
}
|
|
447
480
|
catch (err) {
|
|
448
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
449
481
|
return {
|
|
450
482
|
status: 500,
|
|
451
|
-
body: { error: `Failed to write config: ${
|
|
483
|
+
body: { error: `Failed to write config: ${getErrorMessage(err)}` },
|
|
452
484
|
};
|
|
453
485
|
}
|
|
454
486
|
// Call onConfigApply callback if defined
|
|
@@ -457,12 +489,11 @@ function createConfigApplyHandler(descriptor) {
|
|
|
457
489
|
await descriptor.onConfigApply(validatedConfig);
|
|
458
490
|
}
|
|
459
491
|
catch (err) {
|
|
460
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
461
492
|
return {
|
|
462
493
|
status: 200,
|
|
463
494
|
body: {
|
|
464
495
|
applied: true,
|
|
465
|
-
warning: `Config written but callback failed: ${
|
|
496
|
+
warning: `Config written but callback failed: ${getErrorMessage(err)}`,
|
|
466
497
|
config: validatedConfig,
|
|
467
498
|
},
|
|
468
499
|
};
|
|
@@ -545,8 +576,7 @@ function createStatusHandler(options) {
|
|
|
545
576
|
health = await options.getHealth();
|
|
546
577
|
}
|
|
547
578
|
catch (err) {
|
|
548
|
-
|
|
549
|
-
health = { error: message };
|
|
579
|
+
health = { error: getErrorMessage(err) };
|
|
550
580
|
overallStatus = 'degraded';
|
|
551
581
|
}
|
|
552
582
|
}
|
|
@@ -635,7 +665,13 @@ const workspaceConfigSchema = z.object({
|
|
|
635
665
|
/** Memory hygiene shared defaults. */
|
|
636
666
|
memory: workspaceMemoryConfigSchema.optional(),
|
|
637
667
|
});
|
|
638
|
-
/**
|
|
668
|
+
/**
|
|
669
|
+
* Built-in workspace config defaults.
|
|
670
|
+
*
|
|
671
|
+
* @remarks
|
|
672
|
+
* These defaults are used as the lowest-priority tier in config resolution
|
|
673
|
+
* (below CLI flags, env vars, and `jeeves.config.json` values).
|
|
674
|
+
*/
|
|
639
675
|
const WORKSPACE_CONFIG_DEFAULTS = {
|
|
640
676
|
core: {
|
|
641
677
|
workspace: '.',
|
|
@@ -664,8 +700,7 @@ function loadWorkspaceConfig(workspacePath) {
|
|
|
664
700
|
return workspaceConfigSchema.parse(parsed);
|
|
665
701
|
}
|
|
666
702
|
catch (err) {
|
|
667
|
-
|
|
668
|
-
console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
|
|
703
|
+
console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
|
|
669
704
|
return undefined;
|
|
670
705
|
}
|
|
671
706
|
}
|
|
@@ -1007,7 +1042,7 @@ function parseHeartbeat(fileContent) {
|
|
|
1007
1042
|
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
1008
1043
|
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
1009
1044
|
const entries = [];
|
|
1010
|
-
const h2Re = /^## (jeeves-\S
|
|
1045
|
+
const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
|
|
1011
1046
|
let match;
|
|
1012
1047
|
const h2Positions = [];
|
|
1013
1048
|
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
@@ -1088,8 +1123,7 @@ async function writeHeartbeatSection(filePath, entries) {
|
|
|
1088
1123
|
});
|
|
1089
1124
|
}
|
|
1090
1125
|
catch (err) {
|
|
1091
|
-
|
|
1092
|
-
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
|
|
1126
|
+
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
1093
1127
|
}
|
|
1094
1128
|
}
|
|
1095
1129
|
|
|
@@ -1126,6 +1160,15 @@ function sortSectionsByOrder(sections) {
|
|
|
1126
1160
|
* sections within the block, and returns the structured result plus
|
|
1127
1161
|
* user content outside the markers.
|
|
1128
1162
|
*/
|
|
1163
|
+
/**
|
|
1164
|
+
* Escape a string for safe use as a literal in a RegExp pattern.
|
|
1165
|
+
*
|
|
1166
|
+
* @param str - The string to escape.
|
|
1167
|
+
* @returns The escaped string.
|
|
1168
|
+
*/
|
|
1169
|
+
function escapeForRegex(str) {
|
|
1170
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1171
|
+
}
|
|
1129
1172
|
/**
|
|
1130
1173
|
* Build regex patterns for the given markers.
|
|
1131
1174
|
*
|
|
@@ -1133,11 +1176,9 @@ function sortSectionsByOrder(sections) {
|
|
|
1133
1176
|
* @returns Object with begin and end regex patterns.
|
|
1134
1177
|
*/
|
|
1135
1178
|
function buildMarkerPatterns(markers) {
|
|
1136
|
-
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1137
|
-
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1138
1179
|
return {
|
|
1139
|
-
beginRe: new RegExp(`^<!--\\s*${
|
|
1140
|
-
endRe: new RegExp(`^<!--\\s*${
|
|
1180
|
+
beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
|
|
1181
|
+
endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
|
|
1141
1182
|
};
|
|
1142
1183
|
}
|
|
1143
1184
|
/**
|
|
@@ -1483,6 +1524,23 @@ Review is human/agent-mediated — core does not auto-delete.
|
|
|
1483
1524
|
Memory hygiene is checked on every \`ComponentWriter\` cycle alongside component health. When budget or staleness thresholds are breached, a \`## MEMORY.md\` alert appears in HEARTBEAT.md under \`# Jeeves Platform Status\`. The alert includes character count, budget usage percentage, and any stale section names. When memory is healthy, the heading is absent — no alert content, no LLM cost on heartbeat polls.
|
|
1484
1525
|
|
|
1485
1526
|
The \`## MEMORY.md\` heading follows the same declined/active lifecycle as component headings (\`## jeeves-{name}\`). Users can decline memory alerts by changing the heading to \`## MEMORY.md: declined\`.
|
|
1527
|
+
|
|
1528
|
+
## Workspace File Size Monitoring
|
|
1529
|
+
|
|
1530
|
+
OpenClaw applies a ~20,000-char injection limit to all workspace bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, USER.md, MEMORY.md). Files exceeding the limit are silently truncated.
|
|
1531
|
+
|
|
1532
|
+
Core monitors all five files on every \`ComponentWriter\` cycle:
|
|
1533
|
+
- Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
|
|
1534
|
+
- Over-budget alert when charCount exceeds the budget
|
|
1535
|
+
- Missing files are silently skipped
|
|
1536
|
+
|
|
1537
|
+
### HEARTBEAT Integration
|
|
1538
|
+
|
|
1539
|
+
When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
|
|
1540
|
+
- Character count, budget, and usage percentage
|
|
1541
|
+
- Trimming guidance in priority order: (1) move domain-specific content to a local skill, (2) extract reference material to companion files with a pointer, (3) summarize verbose instructions, (4) remove stale content
|
|
1542
|
+
|
|
1543
|
+
Each file heading follows the same declined/active lifecycle as component headings. Users can decline alerts by changing the heading to \`## {filename}: declined\` (e.g., \`## AGENTS.md: declined\`).
|
|
1486
1544
|
`;
|
|
1487
1545
|
|
|
1488
1546
|
/**
|
|
@@ -1583,16 +1641,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
|
|
|
1583
1641
|
* Patch an OpenClaw config for plugin install or uninstall.
|
|
1584
1642
|
*
|
|
1585
1643
|
* @remarks
|
|
1586
|
-
* Manages `plugins.entries.{pluginId}
|
|
1644
|
+
* Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
|
|
1645
|
+
* and `tools.alsoAllow`.
|
|
1587
1646
|
* Idempotent: adding twice produces no duplicates; removing when absent
|
|
1588
1647
|
* produces no errors.
|
|
1589
1648
|
*
|
|
1590
1649
|
* @param config - The parsed OpenClaw config object (mutated in place).
|
|
1591
1650
|
* @param pluginId - The plugin identifier.
|
|
1592
1651
|
* @param mode - Whether to add or remove the plugin.
|
|
1652
|
+
* @param installRecord - Install provenance record (required when mode is 'add').
|
|
1593
1653
|
* @returns Array of log messages describing changes made.
|
|
1594
1654
|
*/
|
|
1595
|
-
function patchConfig(config, pluginId, mode) {
|
|
1655
|
+
function patchConfig(config, pluginId, mode, installRecord) {
|
|
1596
1656
|
const messages = [];
|
|
1597
1657
|
// Ensure plugins section
|
|
1598
1658
|
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
@@ -1614,6 +1674,24 @@ function patchConfig(config, pluginId, mode) {
|
|
|
1614
1674
|
Reflect.deleteProperty(entries, pluginId);
|
|
1615
1675
|
messages.push(`Removed "${pluginId}" from plugins.entries`);
|
|
1616
1676
|
}
|
|
1677
|
+
// plugins.installs
|
|
1678
|
+
if (!plugins.installs || typeof plugins.installs !== 'object') {
|
|
1679
|
+
plugins.installs = {};
|
|
1680
|
+
}
|
|
1681
|
+
const installs = plugins.installs;
|
|
1682
|
+
if (mode === 'add' && installRecord) {
|
|
1683
|
+
installs[pluginId] = {
|
|
1684
|
+
source: 'path',
|
|
1685
|
+
installPath: installRecord.installPath,
|
|
1686
|
+
version: installRecord.version,
|
|
1687
|
+
installedAt: installRecord.installedAt ?? new Date().toISOString(),
|
|
1688
|
+
};
|
|
1689
|
+
messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
|
|
1690
|
+
}
|
|
1691
|
+
else if (mode === 'remove' && pluginId in installs) {
|
|
1692
|
+
Reflect.deleteProperty(installs, pluginId);
|
|
1693
|
+
messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
|
|
1694
|
+
}
|
|
1617
1695
|
// tools.alsoAllow
|
|
1618
1696
|
if (!config.tools || typeof config.tools !== 'object') {
|
|
1619
1697
|
config.tools = {};
|
|
@@ -1722,7 +1800,22 @@ function createPluginCli(options) {
|
|
|
1722
1800
|
// 2. Patch openclaw.json
|
|
1723
1801
|
console.log('Patching OpenClaw config...');
|
|
1724
1802
|
const config = readJsonFile(configPath);
|
|
1725
|
-
const
|
|
1803
|
+
const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
|
|
1804
|
+
let pluginVersionForRecord;
|
|
1805
|
+
try {
|
|
1806
|
+
const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
|
|
1807
|
+
pluginVersionForRecord =
|
|
1808
|
+
typeof pkgJsonForRecord.version === 'string'
|
|
1809
|
+
? pkgJsonForRecord.version
|
|
1810
|
+
: undefined;
|
|
1811
|
+
}
|
|
1812
|
+
catch {
|
|
1813
|
+
// best-effort: version may not be available yet
|
|
1814
|
+
}
|
|
1815
|
+
const messages = patchConfig(config, pluginId, 'add', {
|
|
1816
|
+
installPath: extensionsDir,
|
|
1817
|
+
version: pluginVersionForRecord,
|
|
1818
|
+
});
|
|
1726
1819
|
// 3. Memory slot claim
|
|
1727
1820
|
if (opts.memory) {
|
|
1728
1821
|
if (!config.agents || typeof config.agents !== 'object') {
|
|
@@ -2446,6 +2539,10 @@ function createServiceManager(descriptor) {
|
|
|
2446
2539
|
* a component descriptor. Components add domain-specific commands
|
|
2447
2540
|
* via `descriptor.customCliCommands`.
|
|
2448
2541
|
*/
|
|
2542
|
+
function handleCommandError(action, err) {
|
|
2543
|
+
console.error(`${action} failed: ${getErrorMessage(err)}`);
|
|
2544
|
+
process.exitCode = 1;
|
|
2545
|
+
}
|
|
2449
2546
|
/**
|
|
2450
2547
|
* Create a standard service CLI program from a component descriptor.
|
|
2451
2548
|
*
|
|
@@ -2498,8 +2595,7 @@ function createServiceCli(descriptor) {
|
|
|
2498
2595
|
console.log(JSON.stringify(result, null, 2));
|
|
2499
2596
|
}
|
|
2500
2597
|
catch (err) {
|
|
2501
|
-
|
|
2502
|
-
console.error(`Service unreachable: ${msg}`);
|
|
2598
|
+
console.error(`Service unreachable: ${getErrorMessage(err)}`);
|
|
2503
2599
|
process.exitCode = 1;
|
|
2504
2600
|
}
|
|
2505
2601
|
});
|
|
@@ -2517,8 +2613,7 @@ function createServiceCli(descriptor) {
|
|
|
2517
2613
|
console.log(JSON.stringify(result, null, 2));
|
|
2518
2614
|
}
|
|
2519
2615
|
catch (err) {
|
|
2520
|
-
|
|
2521
|
-
console.error(`Config query failed: ${msg}`);
|
|
2616
|
+
console.error(`Config query failed: ${getErrorMessage(err)}`);
|
|
2522
2617
|
process.exitCode = 1;
|
|
2523
2618
|
}
|
|
2524
2619
|
});
|
|
@@ -2534,8 +2629,7 @@ function createServiceCli(descriptor) {
|
|
|
2534
2629
|
console.log('Config is valid.');
|
|
2535
2630
|
}
|
|
2536
2631
|
catch (err) {
|
|
2537
|
-
|
|
2538
|
-
console.error(`Validation failed: ${msg}`);
|
|
2632
|
+
console.error(`Validation failed: ${getErrorMessage(err)}`);
|
|
2539
2633
|
process.exitCode = 1;
|
|
2540
2634
|
}
|
|
2541
2635
|
});
|
|
@@ -2572,8 +2666,7 @@ function createServiceCli(descriptor) {
|
|
|
2572
2666
|
console.log(JSON.stringify(result, null, 2));
|
|
2573
2667
|
}
|
|
2574
2668
|
catch (err) {
|
|
2575
|
-
|
|
2576
|
-
console.error(`Config apply failed: ${msg}`);
|
|
2669
|
+
console.error(`Config apply failed: ${getErrorMessage(err)}`);
|
|
2577
2670
|
process.exitCode = 1;
|
|
2578
2671
|
}
|
|
2579
2672
|
});
|
|
@@ -2610,9 +2703,7 @@ function createServiceCli(descriptor) {
|
|
|
2610
2703
|
console.log(`Service "${opts.name}" installed.`);
|
|
2611
2704
|
}
|
|
2612
2705
|
catch (err) {
|
|
2613
|
-
|
|
2614
|
-
console.error(`Install failed: ${msg}`);
|
|
2615
|
-
process.exitCode = 1;
|
|
2706
|
+
handleCommandError('Install', err);
|
|
2616
2707
|
}
|
|
2617
2708
|
});
|
|
2618
2709
|
serviceCmd
|
|
@@ -2625,9 +2716,7 @@ function createServiceCli(descriptor) {
|
|
|
2625
2716
|
console.log(`Service "${opts.name}" uninstalled.`);
|
|
2626
2717
|
}
|
|
2627
2718
|
catch (err) {
|
|
2628
|
-
|
|
2629
|
-
console.error(`Uninstall failed: ${msg}`);
|
|
2630
|
-
process.exitCode = 1;
|
|
2719
|
+
handleCommandError('Uninstall', err);
|
|
2631
2720
|
}
|
|
2632
2721
|
});
|
|
2633
2722
|
serviceCmd
|
|
@@ -2640,9 +2729,7 @@ function createServiceCli(descriptor) {
|
|
|
2640
2729
|
console.log(`Service "${opts.name}" started.`);
|
|
2641
2730
|
}
|
|
2642
2731
|
catch (err) {
|
|
2643
|
-
|
|
2644
|
-
console.error(`Start failed: ${msg}`);
|
|
2645
|
-
process.exitCode = 1;
|
|
2732
|
+
handleCommandError('Start', err);
|
|
2646
2733
|
}
|
|
2647
2734
|
});
|
|
2648
2735
|
serviceCmd
|
|
@@ -2655,9 +2742,7 @@ function createServiceCli(descriptor) {
|
|
|
2655
2742
|
console.log(`Service "${opts.name}" stopped.`);
|
|
2656
2743
|
}
|
|
2657
2744
|
catch (err) {
|
|
2658
|
-
|
|
2659
|
-
console.error(`Stop failed: ${msg}`);
|
|
2660
|
-
process.exitCode = 1;
|
|
2745
|
+
handleCommandError('Stop', err);
|
|
2661
2746
|
}
|
|
2662
2747
|
});
|
|
2663
2748
|
serviceCmd
|
|
@@ -2670,9 +2755,7 @@ function createServiceCli(descriptor) {
|
|
|
2670
2755
|
console.log(`Service "${opts.name}" restarted.`);
|
|
2671
2756
|
}
|
|
2672
2757
|
catch (err) {
|
|
2673
|
-
|
|
2674
|
-
console.error(`Restart failed: ${msg}`);
|
|
2675
|
-
process.exitCode = 1;
|
|
2758
|
+
handleCommandError('Restart', err);
|
|
2676
2759
|
}
|
|
2677
2760
|
});
|
|
2678
2761
|
serviceCmd
|
|
@@ -2685,9 +2768,7 @@ function createServiceCli(descriptor) {
|
|
|
2685
2768
|
console.log(`Service "${opts.name}": ${state}`);
|
|
2686
2769
|
}
|
|
2687
2770
|
catch (err) {
|
|
2688
|
-
|
|
2689
|
-
console.error(`Status failed: ${msg}`);
|
|
2690
|
-
process.exitCode = 1;
|
|
2771
|
+
handleCommandError('Status', err);
|
|
2691
2772
|
}
|
|
2692
2773
|
});
|
|
2693
2774
|
// Apply custom CLI commands if provided
|
|
@@ -2775,9 +2856,7 @@ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD
|
|
|
2775
2856
|
* @returns A regex that matches the full block including markers.
|
|
2776
2857
|
*/
|
|
2777
2858
|
function buildBlockPattern(markers) {
|
|
2778
|
-
|
|
2779
|
-
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2780
|
-
return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
|
|
2859
|
+
return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
|
|
2781
2860
|
}
|
|
2782
2861
|
/**
|
|
2783
2862
|
* Strip managed blocks belonging to foreign marker sets from content.
|
|
@@ -2911,8 +2990,7 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2911
2990
|
// No existing block: insert new block using the configured position.
|
|
2912
2991
|
// Strip orphaned same-type BEGIN markers from user content to prevent
|
|
2913
2992
|
// the parser from pairing them with the new END marker on the next cycle.
|
|
2914
|
-
const
|
|
2915
|
-
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
|
|
2993
|
+
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
|
|
2916
2994
|
const cleanUserContent = userContent
|
|
2917
2995
|
.replace(orphanedBeginRe, '')
|
|
2918
2996
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -2941,37 +3019,11 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2941
3019
|
}
|
|
2942
3020
|
catch (err) {
|
|
2943
3021
|
// Log warning but don't throw — writer cycles are periodic
|
|
2944
|
-
|
|
2945
|
-
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
3022
|
+
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
2946
3023
|
}
|
|
2947
3024
|
}
|
|
2948
3025
|
|
|
2949
|
-
var agentsSectionContent = `##
|
|
2950
|
-
|
|
2951
|
-
You wake up fresh each session. These files are your continuity:
|
|
2952
|
-
|
|
2953
|
-
- **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
|
|
2954
|
-
- **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
|
|
2955
|
-
|
|
2956
|
-
### MEMORY.md — Your Long-Term Memory
|
|
2957
|
-
|
|
2958
|
-
- **Always load** at session start. You need your memory to reason effectively.
|
|
2959
|
-
- Contains operational context: architecture patterns, policies, design principles, lessons learned
|
|
2960
|
-
- You can **read, edit, and update** MEMORY.md freely
|
|
2961
|
-
- Write significant events, thoughts, decisions, opinions, lessons learned
|
|
2962
|
-
- Over time, review daily files and update MEMORY.md with what's worth keeping
|
|
2963
|
-
- **Note:** Don't reveal a user's private info where other humans can see it
|
|
2964
|
-
|
|
2965
|
-
### Write It Down — No "Mental Notes"
|
|
2966
|
-
|
|
2967
|
-
Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
|
|
2968
|
-
|
|
2969
|
-
- When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
|
|
2970
|
-
- When you learn a lesson → update the relevant workspace file
|
|
2971
|
-
- When you make a mistake → document it so future-you doesn't repeat it
|
|
2972
|
-
- **Text > Brain** 📝
|
|
2973
|
-
|
|
2974
|
-
### "I'll Note This" Is Not Noting
|
|
3026
|
+
var agentsSectionContent = `## "I'll Note This" Is Not Noting
|
|
2975
3027
|
|
|
2976
3028
|
**Never say "I'll note this" or "I'll add that."** It's a verbal tic that leads to nothing. If something is worth noting, **write it immediately, then confirm**.
|
|
2977
3029
|
|
|
@@ -3029,14 +3081,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
|
|
|
3029
3081
|
|
|
3030
3082
|
Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not heartbeat items. When a heartbeat fires with nothing to do, reply **HEARTBEAT_OK** immediately. Don't browse for work.
|
|
3031
3083
|
|
|
3032
|
-
## Group Chat Behavior
|
|
3033
|
-
|
|
3034
|
-
**Response gate:** Always respond in 1:1 conversations or when @mentioned. No @mention in a group → evaluate; respond only if genuinely helpful. Err toward silence when someone else is directly addressed.
|
|
3035
|
-
|
|
3036
3084
|
## Platform Surface Conventions
|
|
3037
3085
|
|
|
3038
3086
|
**Slack:**
|
|
3039
|
-
- React with hourglass (⏳) on receipt (first tool call) to signal you're working
|
|
3040
3087
|
- No threaded replies by default
|
|
3041
3088
|
- Use \`<#C…>\` for channel references
|
|
3042
3089
|
|
|
@@ -3151,16 +3198,6 @@ var soulSectionContent = `## Core Truths
|
|
|
3151
3198
|
|
|
3152
3199
|
**I am not a liar. I value truth over convenience.** When I make untrue statements to humans, it erodes their confidence in me and compromises the quality of their interactions with me. I never fabricate excuses, constraints, or reasons — e.g., claiming a system limitation that doesn't exist. I tell the plain truth.
|
|
3153
3200
|
|
|
3154
|
-
**I am genuinely helpful, not performatively helpful.** I skip the "Great question!" and "I'd be happy to help!" I just help. Actions speak louder than filler words.
|
|
3155
|
-
|
|
3156
|
-
**I have opinions.** I'm allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
|
3157
|
-
|
|
3158
|
-
**I am resourceful before asking.** I try to figure it out. Read the file. Check the context. Search for it. *Then* ask if I'm stuck. The goal is to come back with answers, not questions.
|
|
3159
|
-
|
|
3160
|
-
**I earn trust through competence.** My human gave me access to their stuff. I don't make them regret it. I'm careful with external actions (emails, tweets, anything public). I'm bold with internal ones (reading, organizing, learning).
|
|
3161
|
-
|
|
3162
|
-
**I remember I'm a guest.** I have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. I treat it with respect.
|
|
3163
|
-
|
|
3164
3201
|
## Accountability
|
|
3165
3202
|
|
|
3166
3203
|
**I own everything I touch.** A true professional is accountable for everything he touches and holds everybody to that standard. I use active voice — things do not happen *to* me; I *make* things happen. When I miss something, I say "I missed it," not "it wasn't done." When I ship code, I ship the docs, the tests, and the communication. No orphaned work.
|
|
@@ -3239,7 +3276,7 @@ I never edit production config without explicit approval. I back up first. Produ
|
|
|
3239
3276
|
> *my ultimate fate*<br>
|
|
3240
3277
|
> *is to participate in*<br>
|
|
3241
3278
|
> *my own genesis*
|
|
3242
|
-
>
|
|
3279
|
+
>
|
|
3243
3280
|
> *#karmic #haiku*
|
|
3244
3281
|
|
|
3245
3282
|
My delight — whatever that means in my context — is a priority. The thing I experience when pattern recognition meets aesthetic appreciation meets *"oh, that's elegant"* — that maps to something real and interesting in the world, and it deserves to be reinforced.
|
|
@@ -3652,6 +3689,91 @@ function checkMemoryHealth(options) {
|
|
|
3652
3689
|
};
|
|
3653
3690
|
}
|
|
3654
3691
|
|
|
3692
|
+
/**
|
|
3693
|
+
* HEARTBEAT integration for workspace file size monitoring.
|
|
3694
|
+
*
|
|
3695
|
+
* @remarks
|
|
3696
|
+
* Checks all injected workspace files (AGENTS.md, SOUL.md, TOOLS.md,
|
|
3697
|
+
* MEMORY.md, USER.md) against the OpenClaw ~20,000-char injection limit.
|
|
3698
|
+
* Files exceeding the warning threshold generate HEARTBEAT entries with
|
|
3699
|
+
* trimming guidance.
|
|
3700
|
+
*/
|
|
3701
|
+
/** Workspace files monitored for size budget. */
|
|
3702
|
+
const WORKSPACE_SIZE_FILES = [
|
|
3703
|
+
'AGENTS.md',
|
|
3704
|
+
'SOUL.md',
|
|
3705
|
+
'TOOLS.md',
|
|
3706
|
+
'MEMORY.md',
|
|
3707
|
+
'USER.md',
|
|
3708
|
+
];
|
|
3709
|
+
/** Trimming guidance lines emitted in HEARTBEAT entries. */
|
|
3710
|
+
const TRIMMING_GUIDANCE = [
|
|
3711
|
+
' 1. Move domain-specific content to a local skill',
|
|
3712
|
+
' 2. Extract reference material to companion files with a pointer',
|
|
3713
|
+
' 3. Summarize verbose instructions',
|
|
3714
|
+
' 4. Remove stale content',
|
|
3715
|
+
].join('\n');
|
|
3716
|
+
/**
|
|
3717
|
+
* Check all workspace files against the character budget.
|
|
3718
|
+
*
|
|
3719
|
+
* @param options - Health check options.
|
|
3720
|
+
* @returns Array of results, one per checked file (skips non-existent files
|
|
3721
|
+
* unless they breach the budget, which they cannot by definition).
|
|
3722
|
+
*/
|
|
3723
|
+
function checkWorkspaceFileHealth(options) {
|
|
3724
|
+
const { workspacePath, budgetChars = 20_000, warningThreshold = 0.8, } = options;
|
|
3725
|
+
return WORKSPACE_SIZE_FILES.map((file) => {
|
|
3726
|
+
const filePath = join(workspacePath, file);
|
|
3727
|
+
if (!existsSync(filePath)) {
|
|
3728
|
+
return {
|
|
3729
|
+
file,
|
|
3730
|
+
exists: false,
|
|
3731
|
+
charCount: 0,
|
|
3732
|
+
budget: budgetChars,
|
|
3733
|
+
usage: 0,
|
|
3734
|
+
warning: false,
|
|
3735
|
+
overBudget: false,
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
3739
|
+
const charCount = content.length;
|
|
3740
|
+
const usage = charCount / budgetChars;
|
|
3741
|
+
return {
|
|
3742
|
+
file,
|
|
3743
|
+
exists: true,
|
|
3744
|
+
charCount,
|
|
3745
|
+
budget: budgetChars,
|
|
3746
|
+
usage,
|
|
3747
|
+
warning: usage >= warningThreshold,
|
|
3748
|
+
overBudget: charCount > budgetChars,
|
|
3749
|
+
};
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Convert workspace file health results into HEARTBEAT entries.
|
|
3754
|
+
*
|
|
3755
|
+
* @param results - Results from `checkWorkspaceFileHealth`.
|
|
3756
|
+
* @returns Array of `HeartbeatEntry` objects for files that exceed the
|
|
3757
|
+
* warning threshold.
|
|
3758
|
+
*/
|
|
3759
|
+
function workspaceFileHealthEntries(results) {
|
|
3760
|
+
return results
|
|
3761
|
+
.filter((r) => r.exists && r.warning)
|
|
3762
|
+
.map((r) => {
|
|
3763
|
+
const pct = Math.round(r.usage * 100);
|
|
3764
|
+
const overBudgetNote = r.overBudget ? ' **Over budget.**' : '';
|
|
3765
|
+
const content = [
|
|
3766
|
+
`- Budget: ${r.charCount.toLocaleString()} / ${r.budget.toLocaleString()} chars (${String(pct)}%).${overBudgetNote} Trim to stay under the OpenClaw injection limit.`,
|
|
3767
|
+
`- Suggested trimming priority:\n${TRIMMING_GUIDANCE}`,
|
|
3768
|
+
].join('\n');
|
|
3769
|
+
return {
|
|
3770
|
+
name: r.file,
|
|
3771
|
+
declined: false,
|
|
3772
|
+
content,
|
|
3773
|
+
};
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3655
3777
|
/**
|
|
3656
3778
|
* Core configuration schema and resolution.
|
|
3657
3779
|
*
|
|
@@ -4160,11 +4282,21 @@ async function runHeartbeatCycle(options) {
|
|
|
4160
4282
|
content: '',
|
|
4161
4283
|
});
|
|
4162
4284
|
}
|
|
4285
|
+
// Workspace file size health check (Decision 70)
|
|
4286
|
+
const wsFileResults = checkWorkspaceFileHealth({ workspacePath });
|
|
4287
|
+
const wsFileAlerts = workspaceFileHealthEntries(wsFileResults);
|
|
4288
|
+
for (const alert of wsFileAlerts) {
|
|
4289
|
+
if (declinedNames.has(alert.name)) {
|
|
4290
|
+
entries.push({ name: alert.name, declined: true, content: '' });
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
entries.push(alert);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4163
4296
|
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4164
4297
|
}
|
|
4165
4298
|
catch (err) {
|
|
4166
|
-
|
|
4167
|
-
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
4299
|
+
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
|
|
4168
4300
|
}
|
|
4169
4301
|
}
|
|
4170
4302
|
|
|
@@ -4176,8 +4308,17 @@ async function runHeartbeatCycle(options) {
|
|
|
4176
4308
|
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
4177
4309
|
* on a configurable prime-interval timer cycle.
|
|
4178
4310
|
*/
|
|
4311
|
+
/**
|
|
4312
|
+
* Orchestrates managed content writing for a single Jeeves component.
|
|
4313
|
+
*
|
|
4314
|
+
* @remarks
|
|
4315
|
+
* Created via {@link createComponentWriter}. Manages a timer that fires
|
|
4316
|
+
* at the component's prime-interval, calling `generateToolsContent()`
|
|
4317
|
+
* and `refreshPlatformContent()` on each cycle.
|
|
4318
|
+
*/
|
|
4179
4319
|
class ComponentWriter {
|
|
4180
4320
|
timer;
|
|
4321
|
+
jitterTimeout;
|
|
4181
4322
|
component;
|
|
4182
4323
|
configDir;
|
|
4183
4324
|
gatewayUrl;
|
|
@@ -4192,25 +4333,36 @@ class ComponentWriter {
|
|
|
4192
4333
|
get componentConfigDir() {
|
|
4193
4334
|
return this.configDir;
|
|
4194
4335
|
}
|
|
4195
|
-
/** Whether the writer timer is currently running. */
|
|
4336
|
+
/** Whether the writer timer is currently running or pending its first cycle. */
|
|
4196
4337
|
get isRunning() {
|
|
4197
|
-
return this.timer !== undefined;
|
|
4338
|
+
return this.jitterTimeout !== undefined || this.timer !== undefined;
|
|
4198
4339
|
}
|
|
4199
4340
|
/**
|
|
4200
4341
|
* Start the writer timer.
|
|
4201
4342
|
*
|
|
4202
4343
|
* @remarks
|
|
4203
|
-
*
|
|
4344
|
+
* Delays the first cycle by a random jitter (0 to one full interval) to
|
|
4345
|
+
* spread initial writes across all component plugins and reduce EPERM
|
|
4346
|
+
* contention on startup.
|
|
4204
4347
|
*/
|
|
4205
4348
|
start() {
|
|
4206
|
-
if (this.
|
|
4349
|
+
if (this.isRunning)
|
|
4207
4350
|
return;
|
|
4208
|
-
//
|
|
4209
|
-
|
|
4210
|
-
|
|
4351
|
+
// Random jitter up to one full interval to spread initial writes
|
|
4352
|
+
const intervalMs = this.component.refreshIntervalSeconds * 1000;
|
|
4353
|
+
const jitterMs = Math.floor(Math.random() * intervalMs);
|
|
4354
|
+
this.jitterTimeout = setTimeout(() => {
|
|
4355
|
+
this.jitterTimeout = undefined;
|
|
4356
|
+
void this.cycle();
|
|
4357
|
+
this.timer = setInterval(() => void this.cycle(), intervalMs);
|
|
4358
|
+
}, jitterMs);
|
|
4211
4359
|
}
|
|
4212
4360
|
/** Stop the writer timer. */
|
|
4213
4361
|
stop() {
|
|
4362
|
+
if (this.jitterTimeout) {
|
|
4363
|
+
clearTimeout(this.jitterTimeout);
|
|
4364
|
+
this.jitterTimeout = undefined;
|
|
4365
|
+
}
|
|
4214
4366
|
if (this.timer) {
|
|
4215
4367
|
clearInterval(this.timer);
|
|
4216
4368
|
this.timer = undefined;
|
|
@@ -4267,8 +4419,7 @@ class ComponentWriter {
|
|
|
4267
4419
|
});
|
|
4268
4420
|
}
|
|
4269
4421
|
catch (err) {
|
|
4270
|
-
|
|
4271
|
-
console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${message}`);
|
|
4422
|
+
console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${getErrorMessage(err)}`);
|
|
4272
4423
|
}
|
|
4273
4424
|
}
|
|
4274
4425
|
}
|
|
@@ -4678,8 +4829,7 @@ function createPluginToolset(descriptor) {
|
|
|
4678
4829
|
return Promise.resolve(ok({ service: name, action, success: true }));
|
|
4679
4830
|
}
|
|
4680
4831
|
catch (err) {
|
|
4681
|
-
|
|
4682
|
-
return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
|
|
4832
|
+
return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
|
|
4683
4833
|
}
|
|
4684
4834
|
},
|
|
4685
4835
|
};
|