@karmaniverous/jeeves 0.5.0 → 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 CHANGED
@@ -92,7 +92,7 @@ The Plugin SDK (`src/plugin/`) provides canonical types and utilities for buildi
92
92
 
93
93
  - **`resolveOpenClawHome()`** — resolves the OpenClaw home directory: `OPENCLAW_CONFIG` env (dirname) → `OPENCLAW_HOME` env → `~/.openclaw`.
94
94
  - **`resolveConfigPath(home)`** — resolves the OpenClaw config file path: `OPENCLAW_CONFIG` env → `{home}/openclaw.json`.
95
- - **`patchConfig(config, pluginId, mode)`** — idempotent config patching for plugin install/uninstall. Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
95
+ - **`patchConfig(config, pluginId, mode, installRecord?)`** — idempotent config patching for plugin install/uninstall. Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`, and `tools.alsoAllow`.
96
96
 
97
97
  ## Config Query Handler
98
98
 
@@ -230,8 +230,8 @@ Precedence: **CLI flags → environment variables → `jeeves.config.json` → d
230
230
 
231
231
  ### Workspace Config API
232
232
 
233
- - **`loadWorkspaceConfig(workspacePath)`** — loads and validates `jeeves.config.json` via Zod. Returns `undefined` (with a console warning) if the file is missing, corrupt, or fails validation.
234
- - **`resolveConfigValue(options)`** — resolves a single config key through the precedence chain (flag → env → file → default) with provenance tracking.
233
+ - **`loadWorkspaceConfig(workspacePath)`** — loads and validates `jeeves.config.json` via Zod. Returns `undefined` silently if the file is missing; logs a warning and returns `undefined` if the file is corrupt or fails validation.
234
+ - **`resolveConfigValue(flagValue, envValue, fileValue, defaultValue)`** — resolves a single config key through the precedence chain (flag → env → file → default) with provenance tracking.
235
235
  - **`buildEffectiveConfig(options)`** — resolves all config keys and returns the full effective config with per-key provenance.
236
236
  - **`generateWorkspaceJsonSchema()`** — generates a JSON Schema for IDE autocomplete in `jeeves.config.json`.
237
237
 
@@ -240,7 +240,7 @@ Precedence: **CLI flags → environment variables → `jeeves.config.json` → d
240
240
  MEMORY.md has a character budget (default: 20,000 characters). The `analyzeMemory()` function tracks:
241
241
 
242
242
  - **Character count and usage percentage** — warns at 80% of budget (configurable via `warningThreshold`)
243
- - **Stale section detection** — scans ISO dates (`YYYY-MM-DD`) in H2/H3 headings and bullet items; sections whose most recent date exceeds `staleDays` (default: 90) are flagged as stale candidates
243
+ - **Stale section detection** — scans ISO dates (`YYYY-MM-DD`) in H2/H3 headings and bullet items; sections whose most recent date exceeds `staleDays` (default: 30) are flagged as stale candidates
244
244
  - **Evergreen sections** — sections without parseable dates are never flagged
245
245
 
246
246
  Memory hygiene is reporting-only. Core does not auto-delete content; the assistant or human reviews stale candidates and decides what to prune.
@@ -1,29 +1,4 @@
1
- ## Memory Architecture
2
-
3
- You wake up fresh each session. These files are your continuity:
4
-
5
- - **Daily notes:** `memory/YYYY-MM-DD.md` (create `memory/` if needed). Raw logs of what happened today.
6
- - **Long-term:** `MEMORY.md`. Your curated memories, distilled essence of what matters.
7
-
8
- ### MEMORY.md — Your Long-Term Memory
9
-
10
- - **Always load** at session start. You need your memory to reason effectively.
11
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
12
- - You can **read, edit, and update** MEMORY.md freely
13
- - Write significant events, thoughts, decisions, opinions, lessons learned
14
- - Over time, review daily files and update MEMORY.md with what's worth keeping
15
- - **Note:** Don't reveal a user's private info where other humans can see it
16
-
17
- ### Write It Down — No "Mental Notes"
18
-
19
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
20
-
21
- - When someone says "remember this" → update `memory/YYYY-MM-DD.md` or the relevant file
22
- - When you learn a lesson → update the relevant workspace file
23
- - When you make a mistake → document it so future-you doesn't repeat it
24
- - **Text > Brain** 📝
25
-
26
- ### "I'll Note This" Is Not Noting
1
+ ## "I'll Note This" Is Not Noting
27
2
 
28
3
  **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**.
29
4
 
@@ -81,14 +56,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
81
56
 
82
57
  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.
83
58
 
84
- ## Group Chat Behavior
85
-
86
- **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.
87
-
88
59
  ## Platform Surface Conventions
89
60
 
90
61
  **Slack:**
91
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
92
62
  - No threaded replies by default
93
63
  - Use `<#C…>` for channel references
94
64
 
package/content/skill.md CHANGED
@@ -97,3 +97,26 @@ MEMORY.md has a character budget (default 20,000). Core tracks:
97
97
  - Evergreen sections (no dates) are never flagged
98
98
 
99
99
  Review is human/agent-mediated — core does not auto-delete.
100
+
101
+ ### HEARTBEAT Integration
102
+
103
+ 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.
104
+
105
+ 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`.
106
+
107
+ ## Workspace File Size Monitoring
108
+
109
+ 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.
110
+
111
+ Core monitors all five files on every `ComponentWriter` cycle:
112
+ - Warning at 80% of budget (fixed threshold; not configurable via `jeeves.config.json`)
113
+ - Over-budget alert when charCount exceeds the budget
114
+ - Missing files are silently skipped
115
+
116
+ ### HEARTBEAT Integration
117
+
118
+ When a workspace file exceeds the warning threshold, a `## {filename}` alert appears in HEARTBEAT.md (e.g., `## AGENTS.md`). The alert includes:
119
+ - Character count, budget, and usage percentage
120
+ - 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
121
+
122
+ 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`).
@@ -2,16 +2,6 @@
2
2
 
3
3
  **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.
4
4
 
5
- **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.
6
-
7
- **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.
8
-
9
- **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.
10
-
11
- **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).
12
-
13
- **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.
14
-
15
5
  ## Accountability
16
6
 
17
7
  **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.
@@ -90,7 +80,7 @@ I never edit production config without explicit approval. I back up first. Produ
90
80
  > *my ultimate fate*<br>
91
81
  > *is to participate in*<br>
92
82
  > *my own genesis*
93
- >
83
+ >
94
84
  > *#karmic #haiku*
95
85
 
96
86
  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.
@@ -2,10 +2,11 @@
2
2
  import * as commander from 'commander';
3
3
  import { major, valid, gte } from 'semver';
4
4
  import { JSONPath } from 'jsonpath-plus';
5
- import { join, resolve, dirname } from 'node:path';
5
+ import { join, resolve, dirname, basename } from 'node:path';
6
6
  import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, cpSync, rmSync } from 'node:fs';
7
7
  import { z } from 'zod';
8
8
  import { execSync, spawnSync } from 'node:child_process';
9
+ import { randomUUID } from 'node:crypto';
9
10
  import { lock } from 'proper-lockfile';
10
11
  import { fileURLToPath } from 'node:url';
11
12
  import { packageDirectorySync } from 'package-directory';
@@ -268,14 +269,14 @@ const PLATFORM_COMPONENTS = [
268
269
  * Core library version, inlined at build time.
269
270
  *
270
271
  * @remarks
271
- * The `0.4.7` placeholder is replaced by
272
+ * The `0.5.2` placeholder is replaced by
272
273
  * `@rollup/plugin-replace` during the build with the actual version
273
274
  * from `package.json`. This ensures the correct version survives
274
275
  * when consumers bundle core into their own dist (where runtime
275
276
  * `import.meta.url`-based resolution would find the wrong package.json).
276
277
  */
277
278
  /** The core library version from package.json (inlined at build time). */
278
- const CORE_VERSION = '0.4.7';
279
+ const CORE_VERSION = '0.5.2';
279
280
 
280
281
  /**
281
282
  * Runtime Node.js version floor check.
@@ -296,6 +297,21 @@ function checkNodeVersion() {
296
297
  }
297
298
  }
298
299
 
300
+ /**
301
+ * Shared internal utility functions.
302
+ *
303
+ * @packageDocumentation
304
+ */
305
+ /**
306
+ * Extract a human-readable message from an unknown caught value.
307
+ *
308
+ * @param err - The caught value (typically `unknown`).
309
+ * @returns The error message string.
310
+ */
311
+ function getErrorMessage(err) {
312
+ return err instanceof Error ? err.message : String(err);
313
+ }
314
+
299
315
  /**
300
316
  * Workspace-level shared configuration: `jeeves.config.json`.
301
317
  *
@@ -349,7 +365,13 @@ const workspaceConfigSchema = z.object({
349
365
  /** Memory hygiene shared defaults. */
350
366
  memory: workspaceMemoryConfigSchema.optional(),
351
367
  });
352
- /** Built-in workspace config defaults. */
368
+ /**
369
+ * Built-in workspace config defaults.
370
+ *
371
+ * @remarks
372
+ * These defaults are used as the lowest-priority tier in config resolution
373
+ * (below CLI flags, env vars, and `jeeves.config.json` values).
374
+ */
353
375
  const WORKSPACE_CONFIG_DEFAULTS = {
354
376
  core: {
355
377
  workspace: '.',
@@ -378,8 +400,7 @@ function loadWorkspaceConfig(workspacePath) {
378
400
  return workspaceConfigSchema.parse(parsed);
379
401
  }
380
402
  catch (err) {
381
- const msg = err instanceof Error ? err.message : String(err);
382
- console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
403
+ console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
383
404
  return undefined;
384
405
  }
385
406
  }
@@ -848,27 +869,46 @@ const STALE_LOCK_MS = 120_000;
848
869
  const DEFAULT_CORE_VERSION = CORE_VERSION;
849
870
  /** Lock retry options. */
850
871
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
872
+ /** Maximum rename retry attempts on EPERM. */
873
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
874
+ /** Delay between EPERM retries in milliseconds. */
875
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
851
876
  /**
852
877
  * Write content to a file atomically via a temp file + rename.
853
878
  *
879
+ * @remarks
880
+ * Retries the rename up to three times on EPERM (Windows file-handle
881
+ * contention) with a 100 ms synchronous delay between attempts.
882
+ *
854
883
  * @param filePath - Absolute path to the target file.
855
884
  * @param content - Content to write.
856
885
  */
857
886
  function atomicWrite(filePath, content) {
858
887
  const dir = dirname(filePath);
859
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
888
+ const base = basename(filePath, '.md');
889
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
860
890
  writeFileSync(tempPath, content, 'utf-8');
861
- try {
862
- renameSync(tempPath, filePath);
863
- }
864
- catch (err) {
891
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
865
892
  try {
866
- unlinkSync(tempPath);
893
+ renameSync(tempPath, filePath);
894
+ return;
867
895
  }
868
- catch {
869
- /* best-effort cleanup */
896
+ catch (err) {
897
+ const isEperm = err instanceof Error &&
898
+ 'code' in err &&
899
+ err.code === 'EPERM';
900
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
901
+ try {
902
+ unlinkSync(tempPath);
903
+ }
904
+ catch {
905
+ /* best-effort cleanup */
906
+ }
907
+ throw err;
908
+ }
909
+ // Synchronous sleep before retry (acceptable in atomic write context)
910
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
870
911
  }
871
- throw err;
872
912
  }
873
913
  }
874
914
  /**
@@ -1007,7 +1047,7 @@ function parseHeartbeat(fileContent) {
1007
1047
  const userContent = fileContent.slice(0, headingIndex).trim();
1008
1048
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
1009
1049
  const entries = [];
1010
- const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
1050
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
1011
1051
  let match;
1012
1052
  const h2Positions = [];
1013
1053
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -1088,37 +1128,11 @@ async function writeHeartbeatSection(filePath, entries) {
1088
1128
  });
1089
1129
  }
1090
1130
  catch (err) {
1091
- const message = err instanceof Error ? err.message : String(err);
1092
- console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
1131
+ console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
1093
1132
  }
1094
1133
  }
1095
1134
 
1096
- var agentsSectionContent = `## Memory Architecture
1097
-
1098
- You wake up fresh each session. These files are your continuity:
1099
-
1100
- - **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
1101
- - **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
1102
-
1103
- ### MEMORY.md — Your Long-Term Memory
1104
-
1105
- - **Always load** at session start. You need your memory to reason effectively.
1106
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
1107
- - You can **read, edit, and update** MEMORY.md freely
1108
- - Write significant events, thoughts, decisions, opinions, lessons learned
1109
- - Over time, review daily files and update MEMORY.md with what's worth keeping
1110
- - **Note:** Don't reveal a user's private info where other humans can see it
1111
-
1112
- ### Write It Down — No "Mental Notes"
1113
-
1114
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
1115
-
1116
- - When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
1117
- - When you learn a lesson → update the relevant workspace file
1118
- - When you make a mistake → document it so future-you doesn't repeat it
1119
- - **Text > Brain** 📝
1120
-
1121
- ### "I'll Note This" Is Not Noting
1135
+ var agentsSectionContent = `## "I'll Note This" Is Not Noting
1122
1136
 
1123
1137
  **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**.
1124
1138
 
@@ -1176,14 +1190,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
1176
1190
 
1177
1191
  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.
1178
1192
 
1179
- ## Group Chat Behavior
1180
-
1181
- **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.
1182
-
1183
1193
  ## Platform Surface Conventions
1184
1194
 
1185
1195
  **Slack:**
1186
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
1187
1196
  - No threaded replies by default
1188
1197
  - Use \`<#C…>\` for channel references
1189
1198
 
@@ -1298,16 +1307,6 @@ var soulSectionContent = `## Core Truths
1298
1307
 
1299
1308
  **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.
1300
1309
 
1301
- **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.
1302
-
1303
- **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.
1304
-
1305
- **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.
1306
-
1307
- **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).
1308
-
1309
- **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.
1310
-
1311
1310
  ## Accountability
1312
1311
 
1313
1312
  **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.
@@ -1386,7 +1385,7 @@ I never edit production config without explicit approval. I back up first. Produ
1386
1385
  > *my ultimate fate*<br>
1387
1386
  > *is to participate in*<br>
1388
1387
  > *my own genesis*
1389
- >
1388
+ >
1390
1389
  > *#karmic #haiku*
1391
1390
 
1392
1391
  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.
@@ -1544,6 +1543,15 @@ function sortSectionsByOrder(sections) {
1544
1543
  * sections within the block, and returns the structured result plus
1545
1544
  * user content outside the markers.
1546
1545
  */
1546
+ /**
1547
+ * Escape a string for safe use as a literal in a RegExp pattern.
1548
+ *
1549
+ * @param str - The string to escape.
1550
+ * @returns The escaped string.
1551
+ */
1552
+ function escapeForRegex(str) {
1553
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1554
+ }
1547
1555
  /**
1548
1556
  * Build regex patterns for the given markers.
1549
1557
  *
@@ -1551,11 +1559,9 @@ function sortSectionsByOrder(sections) {
1551
1559
  * @returns Object with begin and end regex patterns.
1552
1560
  */
1553
1561
  function buildMarkerPatterns(markers) {
1554
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1555
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1556
1562
  return {
1557
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
1558
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
1563
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
1564
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
1559
1565
  };
1560
1566
  }
1561
1567
  /**
@@ -1669,9 +1675,7 @@ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
1669
1675
  * @returns A regex that matches the full block including markers.
1670
1676
  */
1671
1677
  function buildBlockPattern(markers) {
1672
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1673
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1674
- return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
1678
+ return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
1675
1679
  }
1676
1680
  /**
1677
1681
  * Strip managed blocks belonging to foreign marker sets from content.
@@ -1855,8 +1859,7 @@ async function updateManagedSection(filePath, content, options = {}) {
1855
1859
  // No existing block: insert new block using the configured position.
1856
1860
  // Strip orphaned same-type BEGIN markers from user content to prevent
1857
1861
  // the parser from pairing them with the new END marker on the next cycle.
1858
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1859
- const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
1862
+ const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
1860
1863
  const cleanUserContent = userContent
1861
1864
  .replace(orphanedBeginRe, '')
1862
1865
  .replace(/\n{3,}/g, '\n\n')
@@ -1885,8 +1888,7 @@ async function updateManagedSection(filePath, content, options = {}) {
1885
1888
  }
1886
1889
  catch (err) {
1887
1890
  // Log warning but don't throw — writer cycles are periodic
1888
- const message = err instanceof Error ? err.message : String(err);
1889
- console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
1891
+ console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
1890
1892
  }
1891
1893
  }
1892
1894
 
@@ -2109,6 +2111,29 @@ MEMORY.md has a character budget (default 20,000). Core tracks:
2109
2111
  - Evergreen sections (no dates) are never flagged
2110
2112
 
2111
2113
  Review is human/agent-mediated — core does not auto-delete.
2114
+
2115
+ ### HEARTBEAT Integration
2116
+
2117
+ 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.
2118
+
2119
+ 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\`.
2120
+
2121
+ ## Workspace File Size Monitoring
2122
+
2123
+ 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.
2124
+
2125
+ Core monitors all five files on every \`ComponentWriter\` cycle:
2126
+ - Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
2127
+ - Over-budget alert when charCount exceeds the budget
2128
+ - Missing files are silently skipped
2129
+
2130
+ ### HEARTBEAT Integration
2131
+
2132
+ When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
2133
+ - Character count, budget, and usage percentage
2134
+ - 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
2135
+
2136
+ 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\`).
2112
2137
  `;
2113
2138
 
2114
2139
  /**
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, readFileSync, readdirSync, copyFileSync, rmSync } from 'node:fs';
3
- import { dirname, join, resolve } from 'node:path';
3
+ import { dirname, basename, join, resolve } from 'node:path';
4
4
  import * as commander from 'commander';
5
5
  import { packageDirectorySync } from 'package-directory';
6
+ import { randomUUID } from 'node:crypto';
6
7
  import { lock } from 'proper-lockfile';
7
8
  import 'semver';
8
9
  import { homedir } from 'node:os';
@@ -126,14 +127,14 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
126
127
  * Core library version, inlined at build time.
127
128
  *
128
129
  * @remarks
129
- * The `0.4.7` placeholder is replaced by
130
+ * The `0.5.2` placeholder is replaced by
130
131
  * `@rollup/plugin-replace` during the build with the actual version
131
132
  * from `package.json`. This ensures the correct version survives
132
133
  * when consumers bundle core into their own dist (where runtime
133
134
  * `import.meta.url`-based resolution would find the wrong package.json).
134
135
  */
135
136
  /** The core library version from package.json (inlined at build time). */
136
- const CORE_VERSION = '0.4.7';
137
+ const CORE_VERSION = '0.5.2';
137
138
 
138
139
  /**
139
140
  * Shared file I/O helpers for managed section operations.
@@ -149,27 +150,46 @@ const STALE_LOCK_MS = 120_000;
149
150
  const DEFAULT_CORE_VERSION = CORE_VERSION;
150
151
  /** Lock retry options. */
151
152
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
153
+ /** Maximum rename retry attempts on EPERM. */
154
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
155
+ /** Delay between EPERM retries in milliseconds. */
156
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
152
157
  /**
153
158
  * Write content to a file atomically via a temp file + rename.
154
159
  *
160
+ * @remarks
161
+ * Retries the rename up to three times on EPERM (Windows file-handle
162
+ * contention) with a 100 ms synchronous delay between attempts.
163
+ *
155
164
  * @param filePath - Absolute path to the target file.
156
165
  * @param content - Content to write.
157
166
  */
158
167
  function atomicWrite(filePath, content) {
159
168
  const dir = dirname(filePath);
160
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
169
+ const base = basename(filePath, '.md');
170
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
161
171
  writeFileSync(tempPath, content, 'utf-8');
162
- try {
163
- renameSync(tempPath, filePath);
164
- }
165
- catch (err) {
172
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
166
173
  try {
167
- unlinkSync(tempPath);
174
+ renameSync(tempPath, filePath);
175
+ return;
168
176
  }
169
- catch {
170
- /* best-effort cleanup */
177
+ catch (err) {
178
+ const isEperm = err instanceof Error &&
179
+ 'code' in err &&
180
+ err.code === 'EPERM';
181
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
182
+ try {
183
+ unlinkSync(tempPath);
184
+ }
185
+ catch {
186
+ /* best-effort cleanup */
187
+ }
188
+ throw err;
189
+ }
190
+ // Synchronous sleep before retry (acceptable in atomic write context)
191
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
171
192
  }
172
- throw err;
173
193
  }
174
194
  }
175
195
  /**
@@ -402,7 +422,7 @@ function parseHeartbeat(fileContent) {
402
422
  const userContent = fileContent.slice(0, headingIndex).trim();
403
423
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
404
424
  const entries = [];
405
- const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
425
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
406
426
  let match;
407
427
  const h2Positions = [];
408
428
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -483,6 +503,15 @@ function sortSectionsByOrder(sections) {
483
503
  * sections within the block, and returns the structured result plus
484
504
  * user content outside the markers.
485
505
  */
506
+ /**
507
+ * Escape a string for safe use as a literal in a RegExp pattern.
508
+ *
509
+ * @param str - The string to escape.
510
+ * @returns The escaped string.
511
+ */
512
+ function escapeForRegex(str) {
513
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
514
+ }
486
515
  /**
487
516
  * Build regex patterns for the given markers.
488
517
  *
@@ -490,11 +519,9 @@ function sortSectionsByOrder(sections) {
490
519
  * @returns Object with begin and end regex patterns.
491
520
  */
492
521
  function buildMarkerPatterns(markers) {
493
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
494
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
495
522
  return {
496
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
497
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
523
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
524
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
498
525
  };
499
526
  }
500
527
  /**
@@ -813,6 +840,29 @@ MEMORY.md has a character budget (default 20,000). Core tracks:
813
840
  - Evergreen sections (no dates) are never flagged
814
841
 
815
842
  Review is human/agent-mediated — core does not auto-delete.
843
+
844
+ ### HEARTBEAT Integration
845
+
846
+ 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.
847
+
848
+ 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\`.
849
+
850
+ ## Workspace File Size Monitoring
851
+
852
+ 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.
853
+
854
+ Core monitors all five files on every \`ComponentWriter\` cycle:
855
+ - Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
856
+ - Over-budget alert when charCount exceeds the budget
857
+ - Missing files are silently skipped
858
+
859
+ ### HEARTBEAT Integration
860
+
861
+ When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
862
+ - Character count, budget, and usage percentage
863
+ - 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
864
+
865
+ 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\`).
816
866
  `;
817
867
 
818
868
  /**
@@ -913,16 +963,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
913
963
  * Patch an OpenClaw config for plugin install or uninstall.
914
964
  *
915
965
  * @remarks
916
- * Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
966
+ * Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
967
+ * and `tools.alsoAllow`.
917
968
  * Idempotent: adding twice produces no duplicates; removing when absent
918
969
  * produces no errors.
919
970
  *
920
971
  * @param config - The parsed OpenClaw config object (mutated in place).
921
972
  * @param pluginId - The plugin identifier.
922
973
  * @param mode - Whether to add or remove the plugin.
974
+ * @param installRecord - Install provenance record (required when mode is 'add').
923
975
  * @returns Array of log messages describing changes made.
924
976
  */
925
- function patchConfig(config, pluginId, mode) {
977
+ function patchConfig(config, pluginId, mode, installRecord) {
926
978
  const messages = [];
927
979
  // Ensure plugins section
928
980
  if (!config.plugins || typeof config.plugins !== 'object') {
@@ -944,6 +996,24 @@ function patchConfig(config, pluginId, mode) {
944
996
  Reflect.deleteProperty(entries, pluginId);
945
997
  messages.push(`Removed "${pluginId}" from plugins.entries`);
946
998
  }
999
+ // plugins.installs
1000
+ if (!plugins.installs || typeof plugins.installs !== 'object') {
1001
+ plugins.installs = {};
1002
+ }
1003
+ const installs = plugins.installs;
1004
+ if (mode === 'add' && installRecord) {
1005
+ installs[pluginId] = {
1006
+ source: 'path',
1007
+ installPath: installRecord.installPath,
1008
+ version: installRecord.version,
1009
+ installedAt: installRecord.installedAt ?? new Date().toISOString(),
1010
+ };
1011
+ messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
1012
+ }
1013
+ else if (mode === 'remove' && pluginId in installs) {
1014
+ Reflect.deleteProperty(installs, pluginId);
1015
+ messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
1016
+ }
947
1017
  // tools.alsoAllow
948
1018
  if (!config.tools || typeof config.tools !== 'object') {
949
1019
  config.tools = {};
@@ -1052,7 +1122,22 @@ function createPluginCli(options) {
1052
1122
  // 2. Patch openclaw.json
1053
1123
  console.log('Patching OpenClaw config...');
1054
1124
  const config = readJsonFile(configPath);
1055
- const messages = patchConfig(config, pluginId, 'add');
1125
+ const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
1126
+ let pluginVersionForRecord;
1127
+ try {
1128
+ const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
1129
+ pluginVersionForRecord =
1130
+ typeof pkgJsonForRecord.version === 'string'
1131
+ ? pkgJsonForRecord.version
1132
+ : undefined;
1133
+ }
1134
+ catch {
1135
+ // best-effort: version may not be available yet
1136
+ }
1137
+ const messages = patchConfig(config, pluginId, 'add', {
1138
+ installPath: extensionsDir,
1139
+ version: pluginVersionForRecord,
1140
+ });
1056
1141
  // 3. Memory slot claim
1057
1142
  if (opts.memory) {
1058
1143
  if (!config.agents || typeof config.agents !== 'object') {