@karmaniverous/jeeves-server-openclaw 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import path, { join, dirname, resolve } from 'node:path';
2
+ import path, { join, dirname, resolve, basename } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import fs, { existsSync, copyFileSync, writeFileSync, readFileSync, rmSync, mkdirSync, readdirSync, renameSync, unlinkSync } from 'node:fs';
5
+ import { randomUUID } from 'node:crypto';
5
6
  import require$$0$4 from 'path';
6
7
  import require$$0$3 from 'fs';
7
8
  import require$$0$1 from 'constants';
@@ -15,7 +16,6 @@ import require$$1 from 'node:child_process';
15
16
  import process$2 from 'node:process';
16
17
  import 'node:fs/promises';
17
18
  import { homedir } from 'node:os';
18
- import 'node:crypto';
19
19
 
20
20
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
21
21
 
@@ -15403,14 +15403,14 @@ const SECTION_ORDER = [
15403
15403
  * Core library version, inlined at build time.
15404
15404
  *
15405
15405
  * @remarks
15406
- * The `0.5.0` placeholder is replaced by
15406
+ * The `0.5.2` placeholder is replaced by
15407
15407
  * `@rollup/plugin-replace` during the build with the actual version
15408
15408
  * from `package.json`. This ensures the correct version survives
15409
15409
  * when consumers bundle core into their own dist (where runtime
15410
15410
  * `import.meta.url`-based resolution would find the wrong package.json).
15411
15411
  */
15412
15412
  /** The core library version from package.json (inlined at build time). */
15413
- const CORE_VERSION = '0.5.0';
15413
+ const CORE_VERSION = '0.5.2';
15414
15414
 
15415
15415
  /**
15416
15416
  * Workspace and config root initialization.
@@ -15460,27 +15460,46 @@ const STALE_LOCK_MS = 120_000;
15460
15460
  const DEFAULT_CORE_VERSION = CORE_VERSION;
15461
15461
  /** Lock retry options. */
15462
15462
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
15463
+ /** Maximum rename retry attempts on EPERM. */
15464
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
15465
+ /** Delay between EPERM retries in milliseconds. */
15466
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
15463
15467
  /**
15464
15468
  * Write content to a file atomically via a temp file + rename.
15465
15469
  *
15470
+ * @remarks
15471
+ * Retries the rename up to three times on EPERM (Windows file-handle
15472
+ * contention) with a 100 ms synchronous delay between attempts.
15473
+ *
15466
15474
  * @param filePath - Absolute path to the target file.
15467
15475
  * @param content - Content to write.
15468
15476
  */
15469
15477
  function atomicWrite(filePath, content) {
15470
15478
  const dir = dirname(filePath);
15471
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
15479
+ const base = basename(filePath, '.md');
15480
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
15472
15481
  writeFileSync(tempPath, content, 'utf-8');
15473
- try {
15474
- renameSync(tempPath, filePath);
15475
- }
15476
- catch (err) {
15482
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
15477
15483
  try {
15478
- unlinkSync(tempPath);
15484
+ renameSync(tempPath, filePath);
15485
+ return;
15479
15486
  }
15480
- catch {
15481
- /* best-effort cleanup */
15487
+ catch (err) {
15488
+ const isEperm = err instanceof Error &&
15489
+ 'code' in err &&
15490
+ err.code === 'EPERM';
15491
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
15492
+ try {
15493
+ unlinkSync(tempPath);
15494
+ }
15495
+ catch {
15496
+ /* best-effort cleanup */
15497
+ }
15498
+ throw err;
15499
+ }
15500
+ // Synchronous sleep before retry (acceptable in atomic write context)
15501
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
15482
15502
  }
15483
- throw err;
15484
15503
  }
15485
15504
  }
15486
15505
  /**
@@ -15745,7 +15764,7 @@ function parseHeartbeat(fileContent) {
15745
15764
  const userContent = fileContent.slice(0, headingIndex).trim();
15746
15765
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
15747
15766
  const entries = [];
15748
- const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
15767
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
15749
15768
  let match;
15750
15769
  const h2Positions = [];
15751
15770
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -15826,6 +15845,15 @@ function sortSectionsByOrder(sections) {
15826
15845
  * sections within the block, and returns the structured result plus
15827
15846
  * user content outside the markers.
15828
15847
  */
15848
+ /**
15849
+ * Escape a string for safe use as a literal in a RegExp pattern.
15850
+ *
15851
+ * @param str - The string to escape.
15852
+ * @returns The escaped string.
15853
+ */
15854
+ function escapeForRegex(str) {
15855
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15856
+ }
15829
15857
  /**
15830
15858
  * Build regex patterns for the given markers.
15831
15859
  *
@@ -15833,11 +15861,9 @@ function sortSectionsByOrder(sections) {
15833
15861
  * @returns Object with begin and end regex patterns.
15834
15862
  */
15835
15863
  function buildMarkerPatterns(markers) {
15836
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15837
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15838
15864
  return {
15839
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
15840
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
15865
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
15866
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
15841
15867
  };
15842
15868
  }
15843
15869
  /**
@@ -16162,6 +16188,23 @@ Review is human/agent-mediated — core does not auto-delete.
16162
16188
  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.
16163
16189
 
16164
16190
  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\`.
16191
+
16192
+ ## Workspace File Size Monitoring
16193
+
16194
+ 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.
16195
+
16196
+ Core monitors all five files on every \`ComponentWriter\` cycle:
16197
+ - Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
16198
+ - Over-budget alert when charCount exceeds the budget
16199
+ - Missing files are silently skipped
16200
+
16201
+ ### HEARTBEAT Integration
16202
+
16203
+ When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
16204
+ - Character count, budget, and usage percentage
16205
+ - 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
16206
+
16207
+ 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\`).
16165
16208
  `;
16166
16209
 
16167
16210
  /**
@@ -16262,16 +16305,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
16262
16305
  * Patch an OpenClaw config for plugin install or uninstall.
16263
16306
  *
16264
16307
  * @remarks
16265
- * Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
16308
+ * Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
16309
+ * and `tools.alsoAllow`.
16266
16310
  * Idempotent: adding twice produces no duplicates; removing when absent
16267
16311
  * produces no errors.
16268
16312
  *
16269
16313
  * @param config - The parsed OpenClaw config object (mutated in place).
16270
16314
  * @param pluginId - The plugin identifier.
16271
16315
  * @param mode - Whether to add or remove the plugin.
16316
+ * @param installRecord - Install provenance record (required when mode is 'add').
16272
16317
  * @returns Array of log messages describing changes made.
16273
16318
  */
16274
- function patchConfig(config, pluginId, mode) {
16319
+ function patchConfig(config, pluginId, mode, installRecord) {
16275
16320
  const messages = [];
16276
16321
  // Ensure plugins section
16277
16322
  if (!config.plugins || typeof config.plugins !== 'object') {
@@ -16293,6 +16338,24 @@ function patchConfig(config, pluginId, mode) {
16293
16338
  Reflect.deleteProperty(entries, pluginId);
16294
16339
  messages.push(`Removed "${pluginId}" from plugins.entries`);
16295
16340
  }
16341
+ // plugins.installs
16342
+ if (!plugins.installs || typeof plugins.installs !== 'object') {
16343
+ plugins.installs = {};
16344
+ }
16345
+ const installs = plugins.installs;
16346
+ if (mode === 'add' && installRecord) {
16347
+ installs[pluginId] = {
16348
+ source: 'path',
16349
+ installPath: installRecord.installPath,
16350
+ version: installRecord.version,
16351
+ installedAt: installRecord.installedAt ?? new Date().toISOString(),
16352
+ };
16353
+ messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
16354
+ }
16355
+ else if (mode === 'remove' && pluginId in installs) {
16356
+ Reflect.deleteProperty(installs, pluginId);
16357
+ messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
16358
+ }
16296
16359
  // tools.alsoAllow
16297
16360
  if (!config.tools || typeof config.tools !== 'object') {
16298
16361
  config.tools = {};
@@ -16401,7 +16464,22 @@ function createPluginCli(options) {
16401
16464
  // 2. Patch openclaw.json
16402
16465
  console.log('Patching OpenClaw config...');
16403
16466
  const config = readJsonFile(configPath);
16404
- const messages = patchConfig(config, pluginId, 'add');
16467
+ const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
16468
+ let pluginVersionForRecord;
16469
+ try {
16470
+ const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
16471
+ pluginVersionForRecord =
16472
+ typeof pkgJsonForRecord.version === 'string'
16473
+ ? pkgJsonForRecord.version
16474
+ : undefined;
16475
+ }
16476
+ catch {
16477
+ // best-effort: version may not be available yet
16478
+ }
16479
+ const messages = patchConfig(config, pluginId, 'add', {
16480
+ installPath: extensionsDir,
16481
+ version: pluginVersionForRecord,
16482
+ });
16405
16483
  // 3. Memory slot claim
16406
16484
  if (opts.memory) {
16407
16485
  if (!config.agents || typeof config.agents !== 'object') {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
2
2
  import { homedir } from 'node:os';
3
3
  import path, { join, dirname, basename } from 'node:path';
4
4
  import fs, { existsSync, readFileSync, unlinkSync, mkdirSync, writeFileSync, renameSync, cpSync } from 'node:fs';
5
+ import { randomUUID, createHmac } from 'node:crypto';
5
6
  import require$$0$4 from 'path';
6
7
  import require$$0$3 from 'fs';
7
8
  import require$$0$1 from 'constants';
@@ -15,7 +16,6 @@ import require$$1, { execSync } from 'node:child_process';
15
16
  import process$2 from 'node:process';
16
17
  import 'node:fs/promises';
17
18
  import { fileURLToPath } from 'node:url';
18
- import { createHmac } from 'node:crypto';
19
19
 
20
20
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
21
21
 
@@ -15490,14 +15490,14 @@ const PLATFORM_COMPONENTS = [
15490
15490
  * Core library version, inlined at build time.
15491
15491
  *
15492
15492
  * @remarks
15493
- * The `0.5.0` placeholder is replaced by
15493
+ * The `0.5.2` placeholder is replaced by
15494
15494
  * `@rollup/plugin-replace` during the build with the actual version
15495
15495
  * from `package.json`. This ensures the correct version survives
15496
15496
  * when consumers bundle core into their own dist (where runtime
15497
15497
  * `import.meta.url`-based resolution would find the wrong package.json).
15498
15498
  */
15499
15499
  /** The core library version from package.json (inlined at build time). */
15500
- const CORE_VERSION = '0.5.0';
15500
+ const CORE_VERSION = '0.5.2';
15501
15501
 
15502
15502
  /**
15503
15503
  * Workspace and config root initialization.
@@ -15579,27 +15579,46 @@ const STALE_LOCK_MS = 120_000;
15579
15579
  const DEFAULT_CORE_VERSION = CORE_VERSION;
15580
15580
  /** Lock retry options. */
15581
15581
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
15582
+ /** Maximum rename retry attempts on EPERM. */
15583
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
15584
+ /** Delay between EPERM retries in milliseconds. */
15585
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
15582
15586
  /**
15583
15587
  * Write content to a file atomically via a temp file + rename.
15584
15588
  *
15589
+ * @remarks
15590
+ * Retries the rename up to three times on EPERM (Windows file-handle
15591
+ * contention) with a 100 ms synchronous delay between attempts.
15592
+ *
15585
15593
  * @param filePath - Absolute path to the target file.
15586
15594
  * @param content - Content to write.
15587
15595
  */
15588
15596
  function atomicWrite(filePath, content) {
15589
15597
  const dir = dirname(filePath);
15590
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
15598
+ const base = basename(filePath, '.md');
15599
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
15591
15600
  writeFileSync(tempPath, content, 'utf-8');
15592
- try {
15593
- renameSync(tempPath, filePath);
15594
- }
15595
- catch (err) {
15601
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
15596
15602
  try {
15597
- unlinkSync(tempPath);
15603
+ renameSync(tempPath, filePath);
15604
+ return;
15598
15605
  }
15599
- catch {
15600
- /* best-effort cleanup */
15606
+ catch (err) {
15607
+ const isEperm = err instanceof Error &&
15608
+ 'code' in err &&
15609
+ err.code === 'EPERM';
15610
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
15611
+ try {
15612
+ unlinkSync(tempPath);
15613
+ }
15614
+ catch {
15615
+ /* best-effort cleanup */
15616
+ }
15617
+ throw err;
15618
+ }
15619
+ // Synchronous sleep before retry (acceptable in atomic write context)
15620
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
15601
15621
  }
15602
- throw err;
15603
15622
  }
15604
15623
  }
15605
15624
  /**
@@ -15634,6 +15653,21 @@ async function withFileLock(filePath, fn) {
15634
15653
  }
15635
15654
  }
15636
15655
 
15656
+ /**
15657
+ * Shared internal utility functions.
15658
+ *
15659
+ * @packageDocumentation
15660
+ */
15661
+ /**
15662
+ * Extract a human-readable message from an unknown caught value.
15663
+ *
15664
+ * @param err - The caught value (typically `unknown`).
15665
+ * @returns The error message string.
15666
+ */
15667
+ function getErrorMessage(err) {
15668
+ return err instanceof Error ? err.message : String(err);
15669
+ }
15670
+
15637
15671
  /**
15638
15672
  * Workspace-level shared configuration: `jeeves.config.json`.
15639
15673
  *
@@ -15683,7 +15717,13 @@ const workspaceConfigSchema = object({
15683
15717
  /** Memory hygiene shared defaults. */
15684
15718
  memory: workspaceMemoryConfigSchema.optional(),
15685
15719
  });
15686
- /** Built-in workspace config defaults. */
15720
+ /**
15721
+ * Built-in workspace config defaults.
15722
+ *
15723
+ * @remarks
15724
+ * These defaults are used as the lowest-priority tier in config resolution
15725
+ * (below CLI flags, env vars, and `jeeves.config.json` values).
15726
+ */
15687
15727
  const WORKSPACE_CONFIG_DEFAULTS = {
15688
15728
  core: {
15689
15729
  workspace: '.',
@@ -15712,8 +15752,7 @@ function loadWorkspaceConfig(workspacePath) {
15712
15752
  return workspaceConfigSchema.parse(parsed);
15713
15753
  }
15714
15754
  catch (err) {
15715
- const msg = err instanceof Error ? err.message : String(err);
15716
- console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
15755
+ console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
15717
15756
  return undefined;
15718
15757
  }
15719
15758
  }
@@ -15892,7 +15931,7 @@ function parseHeartbeat(fileContent) {
15892
15931
  const userContent = fileContent.slice(0, headingIndex).trim();
15893
15932
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
15894
15933
  const entries = [];
15895
- const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
15934
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
15896
15935
  let match;
15897
15936
  const h2Positions = [];
15898
15937
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -15973,8 +16012,7 @@ async function writeHeartbeatSection(filePath, entries) {
15973
16012
  });
15974
16013
  }
15975
16014
  catch (err) {
15976
- const message = err instanceof Error ? err.message : String(err);
15977
- console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
16015
+ console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
15978
16016
  }
15979
16017
  }
15980
16018
 
@@ -16011,6 +16049,15 @@ function sortSectionsByOrder(sections) {
16011
16049
  * sections within the block, and returns the structured result plus
16012
16050
  * user content outside the markers.
16013
16051
  */
16052
+ /**
16053
+ * Escape a string for safe use as a literal in a RegExp pattern.
16054
+ *
16055
+ * @param str - The string to escape.
16056
+ * @returns The escaped string.
16057
+ */
16058
+ function escapeForRegex(str) {
16059
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16060
+ }
16014
16061
  /**
16015
16062
  * Build regex patterns for the given markers.
16016
16063
  *
@@ -16018,11 +16065,9 @@ function sortSectionsByOrder(sections) {
16018
16065
  * @returns Object with begin and end regex patterns.
16019
16066
  */
16020
16067
  function buildMarkerPatterns(markers) {
16021
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16022
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16023
16068
  return {
16024
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
16025
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
16069
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
16070
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
16026
16071
  };
16027
16072
  }
16028
16073
  /**
@@ -16804,9 +16849,7 @@ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD
16804
16849
  * @returns A regex that matches the full block including markers.
16805
16850
  */
16806
16851
  function buildBlockPattern(markers) {
16807
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16808
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16809
- return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
16852
+ return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
16810
16853
  }
16811
16854
  /**
16812
16855
  * Strip managed blocks belonging to foreign marker sets from content.
@@ -16940,8 +16983,7 @@ async function updateManagedSection(filePath, content, options = {}) {
16940
16983
  // No existing block: insert new block using the configured position.
16941
16984
  // Strip orphaned same-type BEGIN markers from user content to prevent
16942
16985
  // the parser from pairing them with the new END marker on the next cycle.
16943
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16944
- const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
16986
+ const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
16945
16987
  const cleanUserContent = userContent
16946
16988
  .replace(orphanedBeginRe, '')
16947
16989
  .replace(/\n{3,}/g, '\n\n')
@@ -16970,37 +17012,11 @@ async function updateManagedSection(filePath, content, options = {}) {
16970
17012
  }
16971
17013
  catch (err) {
16972
17014
  // Log warning but don't throw — writer cycles are periodic
16973
- const message = err instanceof Error ? err.message : String(err);
16974
- console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
17015
+ console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
16975
17016
  }
16976
17017
  }
16977
17018
 
16978
- var agentsSectionContent = `## Memory Architecture
16979
-
16980
- You wake up fresh each session. These files are your continuity:
16981
-
16982
- - **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
16983
- - **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
16984
-
16985
- ### MEMORY.md — Your Long-Term Memory
16986
-
16987
- - **Always load** at session start. You need your memory to reason effectively.
16988
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
16989
- - You can **read, edit, and update** MEMORY.md freely
16990
- - Write significant events, thoughts, decisions, opinions, lessons learned
16991
- - Over time, review daily files and update MEMORY.md with what's worth keeping
16992
- - **Note:** Don't reveal a user's private info where other humans can see it
16993
-
16994
- ### Write It Down — No "Mental Notes"
16995
-
16996
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
16997
-
16998
- - When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
16999
- - When you learn a lesson → update the relevant workspace file
17000
- - When you make a mistake → document it so future-you doesn't repeat it
17001
- - **Text > Brain** 📝
17002
-
17003
- ### "I'll Note This" Is Not Noting
17019
+ var agentsSectionContent = `## "I'll Note This" Is Not Noting
17004
17020
 
17005
17021
  **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**.
17006
17022
 
@@ -17058,14 +17074,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
17058
17074
 
17059
17075
  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.
17060
17076
 
17061
- ## Group Chat Behavior
17062
-
17063
- **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.
17064
-
17065
17077
  ## Platform Surface Conventions
17066
17078
 
17067
17079
  **Slack:**
17068
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
17069
17080
  - No threaded replies by default
17070
17081
  - Use \`<#C…>\` for channel references
17071
17082
 
@@ -17180,16 +17191,6 @@ var soulSectionContent = `## Core Truths
17180
17191
 
17181
17192
  **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.
17182
17193
 
17183
- **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.
17184
-
17185
- **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.
17186
-
17187
- **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.
17188
-
17189
- **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).
17190
-
17191
- **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.
17192
-
17193
17194
  ## Accountability
17194
17195
 
17195
17196
  **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.
@@ -17268,7 +17269,7 @@ I never edit production config without explicit approval. I back up first. Produ
17268
17269
  > *my ultimate fate*<br>
17269
17270
  > *is to participate in*<br>
17270
17271
  > *my own genesis*
17271
- >
17272
+ >
17272
17273
  > *#karmic #haiku*
17273
17274
 
17274
17275
  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.
@@ -17681,6 +17682,91 @@ function checkMemoryHealth(options) {
17681
17682
  };
17682
17683
  }
17683
17684
 
17685
+ /**
17686
+ * HEARTBEAT integration for workspace file size monitoring.
17687
+ *
17688
+ * @remarks
17689
+ * Checks all injected workspace files (AGENTS.md, SOUL.md, TOOLS.md,
17690
+ * MEMORY.md, USER.md) against the OpenClaw ~20,000-char injection limit.
17691
+ * Files exceeding the warning threshold generate HEARTBEAT entries with
17692
+ * trimming guidance.
17693
+ */
17694
+ /** Workspace files monitored for size budget. */
17695
+ const WORKSPACE_SIZE_FILES = [
17696
+ 'AGENTS.md',
17697
+ 'SOUL.md',
17698
+ 'TOOLS.md',
17699
+ 'MEMORY.md',
17700
+ 'USER.md',
17701
+ ];
17702
+ /** Trimming guidance lines emitted in HEARTBEAT entries. */
17703
+ const TRIMMING_GUIDANCE = [
17704
+ ' 1. Move domain-specific content to a local skill',
17705
+ ' 2. Extract reference material to companion files with a pointer',
17706
+ ' 3. Summarize verbose instructions',
17707
+ ' 4. Remove stale content',
17708
+ ].join('\n');
17709
+ /**
17710
+ * Check all workspace files against the character budget.
17711
+ *
17712
+ * @param options - Health check options.
17713
+ * @returns Array of results, one per checked file (skips non-existent files
17714
+ * unless they breach the budget, which they cannot by definition).
17715
+ */
17716
+ function checkWorkspaceFileHealth(options) {
17717
+ const { workspacePath, budgetChars = 20_000, warningThreshold = 0.8, } = options;
17718
+ return WORKSPACE_SIZE_FILES.map((file) => {
17719
+ const filePath = join(workspacePath, file);
17720
+ if (!existsSync(filePath)) {
17721
+ return {
17722
+ file,
17723
+ exists: false,
17724
+ charCount: 0,
17725
+ budget: budgetChars,
17726
+ usage: 0,
17727
+ warning: false,
17728
+ overBudget: false,
17729
+ };
17730
+ }
17731
+ const content = readFileSync(filePath, 'utf-8');
17732
+ const charCount = content.length;
17733
+ const usage = charCount / budgetChars;
17734
+ return {
17735
+ file,
17736
+ exists: true,
17737
+ charCount,
17738
+ budget: budgetChars,
17739
+ usage,
17740
+ warning: usage >= warningThreshold,
17741
+ overBudget: charCount > budgetChars,
17742
+ };
17743
+ });
17744
+ }
17745
+ /**
17746
+ * Convert workspace file health results into HEARTBEAT entries.
17747
+ *
17748
+ * @param results - Results from `checkWorkspaceFileHealth`.
17749
+ * @returns Array of `HeartbeatEntry` objects for files that exceed the
17750
+ * warning threshold.
17751
+ */
17752
+ function workspaceFileHealthEntries(results) {
17753
+ return results
17754
+ .filter((r) => r.exists && r.warning)
17755
+ .map((r) => {
17756
+ const pct = Math.round(r.usage * 100);
17757
+ const overBudgetNote = r.overBudget ? ' **Over budget.**' : '';
17758
+ const content = [
17759
+ `- Budget: ${r.charCount.toLocaleString()} / ${r.budget.toLocaleString()} chars (${String(pct)}%).${overBudgetNote} Trim to stay under the OpenClaw injection limit.`,
17760
+ `- Suggested trimming priority:\n${TRIMMING_GUIDANCE}`,
17761
+ ].join('\n');
17762
+ return {
17763
+ name: r.file,
17764
+ declined: false,
17765
+ content,
17766
+ };
17767
+ });
17768
+ }
17769
+
17684
17770
  /**
17685
17771
  * Core configuration schema and resolution.
17686
17772
  *
@@ -18130,11 +18216,21 @@ async function runHeartbeatCycle(options) {
18130
18216
  content: '',
18131
18217
  });
18132
18218
  }
18219
+ // Workspace file size health check (Decision 70)
18220
+ const wsFileResults = checkWorkspaceFileHealth({ workspacePath });
18221
+ const wsFileAlerts = workspaceFileHealthEntries(wsFileResults);
18222
+ for (const alert of wsFileAlerts) {
18223
+ if (declinedNames.has(alert.name)) {
18224
+ entries.push({ name: alert.name, declined: true, content: '' });
18225
+ }
18226
+ else {
18227
+ entries.push(alert);
18228
+ }
18229
+ }
18133
18230
  await writeHeartbeatSection(heartbeatPath, entries);
18134
18231
  }
18135
18232
  catch (err) {
18136
- const msg = err instanceof Error ? err.message : String(err);
18137
- console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
18233
+ console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
18138
18234
  }
18139
18235
  }
18140
18236
 
@@ -18146,8 +18242,17 @@ async function runHeartbeatCycle(options) {
18146
18242
  * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
18147
18243
  * on a configurable prime-interval timer cycle.
18148
18244
  */
18245
+ /**
18246
+ * Orchestrates managed content writing for a single Jeeves component.
18247
+ *
18248
+ * @remarks
18249
+ * Created via {@link createComponentWriter}. Manages a timer that fires
18250
+ * at the component's prime-interval, calling `generateToolsContent()`
18251
+ * and `refreshPlatformContent()` on each cycle.
18252
+ */
18149
18253
  class ComponentWriter {
18150
18254
  timer;
18255
+ jitterTimeout;
18151
18256
  component;
18152
18257
  configDir;
18153
18258
  gatewayUrl;
@@ -18162,25 +18267,36 @@ class ComponentWriter {
18162
18267
  get componentConfigDir() {
18163
18268
  return this.configDir;
18164
18269
  }
18165
- /** Whether the writer timer is currently running. */
18270
+ /** Whether the writer timer is currently running or pending its first cycle. */
18166
18271
  get isRunning() {
18167
- return this.timer !== undefined;
18272
+ return this.jitterTimeout !== undefined || this.timer !== undefined;
18168
18273
  }
18169
18274
  /**
18170
18275
  * Start the writer timer.
18171
18276
  *
18172
18277
  * @remarks
18173
- * Performs an immediate first write, then sets up the interval.
18278
+ * Delays the first cycle by a random jitter (0 to one full interval) to
18279
+ * spread initial writes across all component plugins and reduce EPERM
18280
+ * contention on startup.
18174
18281
  */
18175
18282
  start() {
18176
- if (this.timer)
18283
+ if (this.isRunning)
18177
18284
  return;
18178
- // Fire immediately, then on interval
18179
- void this.cycle();
18180
- this.timer = setInterval(() => void this.cycle(), this.component.refreshIntervalSeconds * 1000);
18285
+ // Random jitter up to one full interval to spread initial writes
18286
+ const intervalMs = this.component.refreshIntervalSeconds * 1000;
18287
+ const jitterMs = Math.floor(Math.random() * intervalMs);
18288
+ this.jitterTimeout = setTimeout(() => {
18289
+ this.jitterTimeout = undefined;
18290
+ void this.cycle();
18291
+ this.timer = setInterval(() => void this.cycle(), intervalMs);
18292
+ }, jitterMs);
18181
18293
  }
18182
18294
  /** Stop the writer timer. */
18183
18295
  stop() {
18296
+ if (this.jitterTimeout) {
18297
+ clearTimeout(this.jitterTimeout);
18298
+ this.jitterTimeout = undefined;
18299
+ }
18184
18300
  if (this.timer) {
18185
18301
  clearInterval(this.timer);
18186
18302
  this.timer = undefined;
@@ -18237,8 +18353,7 @@ class ComponentWriter {
18237
18353
  });
18238
18354
  }
18239
18355
  catch (err) {
18240
- const message = err instanceof Error ? err.message : String(err);
18241
- console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${message}`);
18356
+ console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${getErrorMessage(err)}`);
18242
18357
  }
18243
18358
  }
18244
18359
  }
@@ -18548,8 +18663,7 @@ function createPluginToolset(descriptor) {
18548
18663
  return Promise.resolve(ok({ service: name, action, success: true }));
18549
18664
  }
18550
18665
  catch (err) {
18551
- const msg = err instanceof Error ? err.message : String(err);
18552
- return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
18666
+ return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
18553
18667
  }
18554
18668
  },
18555
18669
  };
@@ -2,7 +2,7 @@
2
2
  "id": "jeeves-server-openclaw",
3
3
  "name": "Jeeves Server",
4
4
  "description": "File browsing, document sharing, export, and event gateway tools for jeeves-server.",
5
- "version": "0.7.0",
5
+ "version": "0.7.1",
6
6
  "skills": [
7
7
  "dist/skills/jeeves-server"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-server-openclaw",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.prerelease --preRelease"
18
18
  },
19
19
  "devDependencies": {
20
- "@dotenvx/dotenvx": "^1.55.1",
20
+ "@dotenvx/dotenvx": "^1.59.1",
21
21
  "@rollup/plugin-commonjs": "^29.0.2",
22
22
  "@rollup/plugin-json": "^6.1.0",
23
23
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -25,9 +25,9 @@
25
25
  "auto-changelog": "^2.5.0",
26
26
  "cross-env": "^10.1.0",
27
27
  "release-it": "^19.2.4",
28
- "rollup": "^4.59.0",
28
+ "rollup": "^4.60.1",
29
29
  "tslib": "^2.8.1",
30
- "vitest": "^4.1.0"
30
+ "vitest": "^4.1.2"
31
31
  },
32
32
  "author": "Jason Williscroft",
33
33
  "description": "OpenClaw plugin for jeeves-server — file browsing, sharing, export, and event gateway tools",
@@ -115,7 +115,7 @@
115
115
  "hideCredit": true
116
116
  },
117
117
  "dependencies": {
118
- "@karmaniverous/jeeves": "^0.5.1",
119
- "zod": "^4.3.0"
118
+ "@karmaniverous/jeeves": "^0.5.3",
119
+ "zod": "^4.3.6"
120
120
  }
121
121
  }