@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 +4 -4
- package/content/agents-section.md +1 -31
- package/content/skill.md +23 -0
- package/content/soul-section.md +1 -11
- package/dist/cli/jeeves/index.js +96 -71
- package/dist/cli/plugin/index.js +105 -20
- package/dist/cli/service/index.js +36 -27
- package/dist/index.d.ts +87 -24
- package/dist/index.js +426 -209
- package/package.json +1 -1
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`
|
|
234
|
-
- **`resolveConfigValue(
|
|
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:
|
|
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
|
-
##
|
|
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`).
|
package/content/soul-section.md
CHANGED
|
@@ -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.
|
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
862
|
-
renameSync(tempPath, filePath);
|
|
863
|
-
}
|
|
864
|
-
catch (err) {
|
|
891
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
865
892
|
try {
|
|
866
|
-
|
|
893
|
+
renameSync(tempPath, filePath);
|
|
894
|
+
return;
|
|
867
895
|
}
|
|
868
|
-
catch {
|
|
869
|
-
|
|
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
|
|
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
|
-
|
|
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 = `##
|
|
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*${
|
|
1558
|
-
endRe: new RegExp(`^<!--\\s*${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
package/dist/cli/plugin/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
163
|
-
renameSync(tempPath, filePath);
|
|
164
|
-
}
|
|
165
|
-
catch (err) {
|
|
172
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
166
173
|
try {
|
|
167
|
-
|
|
174
|
+
renameSync(tempPath, filePath);
|
|
175
|
+
return;
|
|
168
176
|
}
|
|
169
|
-
catch {
|
|
170
|
-
|
|
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
|
|
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*${
|
|
497
|
-
endRe: new RegExp(`^<!--\\s*${
|
|
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}
|
|
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
|
|
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') {
|