@karmaniverous/jeeves 0.5.12 → 0.6.0-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/README.md +247 -83
- package/content/agents-section.md +10 -18
- package/dist/cli/jeeves/index.js +3254 -1841
- package/dist/cli/service/index.js +11 -130
- package/dist/index.d.ts +409 -763
- package/dist/index.js +1508 -4166
- package/package.json +32 -30
- package/content/skills/coding.md +0 -149
- package/content/skills/jeeves.md +0 -122
- package/content/skills/operations.md +0 -125
- package/content/skills/playbooks.md +0 -75
- package/content/skills/slack-bot-provisioner.md +0 -57
- package/content/templates/spec-to-code-guide.md +0 -250
- package/content/templates/spec.md +0 -177
- package/content/tools-platform.md +0 -102
- package/dist/cli/plugin/index.js +0 -1904
package/dist/cli/plugin/index.js
DELETED
|
@@ -1,1904 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, readFileSync, readdirSync, copyFileSync, rmSync } from 'node:fs';
|
|
4
|
-
import { dirname, basename, join, resolve } from 'node:path';
|
|
5
|
-
import * as commander from 'commander';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { lock } from 'proper-lockfile';
|
|
8
|
-
import 'semver';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
import { z } from 'zod';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { packageDirectorySync } from 'package-directory';
|
|
13
|
-
|
|
14
|
-
function getDefaultExportFromCjs (x) {
|
|
15
|
-
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getAugmentedNamespace(n) {
|
|
19
|
-
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
|
|
20
|
-
var f = n.default;
|
|
21
|
-
if (typeof f == "function") {
|
|
22
|
-
var a = function a () {
|
|
23
|
-
var isInstance = false;
|
|
24
|
-
try {
|
|
25
|
-
isInstance = this instanceof a;
|
|
26
|
-
} catch {}
|
|
27
|
-
if (isInstance) {
|
|
28
|
-
return Reflect.construct(f, arguments, this.constructor);
|
|
29
|
-
}
|
|
30
|
-
return f.apply(this, arguments);
|
|
31
|
-
};
|
|
32
|
-
a.prototype = f.prototype;
|
|
33
|
-
} else a = {};
|
|
34
|
-
Object.defineProperty(a, '__esModule', {value: true});
|
|
35
|
-
Object.keys(n).forEach(function (k) {
|
|
36
|
-
var d = Object.getOwnPropertyDescriptor(n, k);
|
|
37
|
-
Object.defineProperty(a, k, d.get ? d : {
|
|
38
|
-
enumerable: true,
|
|
39
|
-
get: function () {
|
|
40
|
-
return n[k];
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
return a;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
var extraTypings = {exports: {}};
|
|
48
|
-
|
|
49
|
-
var require$$0 = /*@__PURE__*/getAugmentedNamespace(commander);
|
|
50
|
-
|
|
51
|
-
var hasRequiredExtraTypings;
|
|
52
|
-
|
|
53
|
-
function requireExtraTypings () {
|
|
54
|
-
if (hasRequiredExtraTypings) return extraTypings.exports;
|
|
55
|
-
hasRequiredExtraTypings = 1;
|
|
56
|
-
(function (module, exports) {
|
|
57
|
-
const commander = require$$0;
|
|
58
|
-
|
|
59
|
-
exports = module.exports = {};
|
|
60
|
-
|
|
61
|
-
// Return a different global program than commander,
|
|
62
|
-
// and don't also return it as default export.
|
|
63
|
-
exports.program = new commander.Command();
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Expose classes. The FooT versions are just types, so return Commander original implementations!
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
exports.Argument = commander.Argument;
|
|
70
|
-
exports.Command = commander.Command;
|
|
71
|
-
exports.CommanderError = commander.CommanderError;
|
|
72
|
-
exports.Help = commander.Help;
|
|
73
|
-
exports.InvalidArgumentError = commander.InvalidArgumentError;
|
|
74
|
-
exports.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
|
|
75
|
-
exports.Option = commander.Option;
|
|
76
|
-
|
|
77
|
-
exports.createCommand = (name) => new commander.Command(name);
|
|
78
|
-
exports.createOption = (flags, description) =>
|
|
79
|
-
new commander.Option(flags, description);
|
|
80
|
-
exports.createArgument = (name, description) =>
|
|
81
|
-
new commander.Argument(name, description);
|
|
82
|
-
} (extraTypings, extraTypings.exports));
|
|
83
|
-
return extraTypings.exports;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
var extraTypingsExports = requireExtraTypings();
|
|
87
|
-
var extraTypingsCommander = /*@__PURE__*/getDefaultExportFromCjs(extraTypingsExports);
|
|
88
|
-
|
|
89
|
-
// wrapper to provide named exports for ESM.
|
|
90
|
-
const {
|
|
91
|
-
program,
|
|
92
|
-
createCommand,
|
|
93
|
-
createArgument,
|
|
94
|
-
createOption,
|
|
95
|
-
CommanderError,
|
|
96
|
-
InvalidArgumentError,
|
|
97
|
-
InvalidOptionArgumentError, // deprecated old name
|
|
98
|
-
Command,
|
|
99
|
-
Argument,
|
|
100
|
-
Option,
|
|
101
|
-
Help,
|
|
102
|
-
} = extraTypingsCommander;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Directory and file path conventions for the Jeeves platform.
|
|
106
|
-
*/
|
|
107
|
-
/** Core config directory name within the config root. */
|
|
108
|
-
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
109
|
-
/** Default workspace file names. */
|
|
110
|
-
const WORKSPACE_FILES = {
|
|
111
|
-
/** TOOLS.md — live platform state and component sections. */
|
|
112
|
-
tools: 'TOOLS.md',
|
|
113
|
-
/** SOUL.md — professional discipline and behavioral foundations. */
|
|
114
|
-
soul: 'SOUL.md',
|
|
115
|
-
/** AGENTS.md — operational protocols and memory architecture. */
|
|
116
|
-
agents: 'AGENTS.md',
|
|
117
|
-
/** HEARTBEAT.md — platform status and health alerts. */
|
|
118
|
-
heartbeat: 'HEARTBEAT.md',
|
|
119
|
-
/** MEMORY.md — curated long-term memory. */
|
|
120
|
-
memory: 'MEMORY.md',
|
|
121
|
-
};
|
|
122
|
-
/** Skill directory name within workspace. */
|
|
123
|
-
const SKILLS_DIR = 'skills';
|
|
124
|
-
/** Component versions state file name. */
|
|
125
|
-
const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Core library version, inlined at build time.
|
|
129
|
-
*
|
|
130
|
-
* @remarks
|
|
131
|
-
* The `0.5.11` placeholder is replaced by
|
|
132
|
-
* `@rollup/plugin-replace` during the build with the actual version
|
|
133
|
-
* from `package.json`. This ensures the correct version survives
|
|
134
|
-
* when consumers bundle core into their own dist (where runtime
|
|
135
|
-
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
136
|
-
*/
|
|
137
|
-
/** The core library version from package.json (inlined at build time). */
|
|
138
|
-
const CORE_VERSION = '0.5.11';
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Shared file I/O helpers for managed section operations.
|
|
142
|
-
*
|
|
143
|
-
* @remarks
|
|
144
|
-
* Extracts the atomic write pattern and file-level locking into
|
|
145
|
-
* reusable utilities, eliminating duplication between
|
|
146
|
-
* `updateManagedSection` and `removeManagedSection`.
|
|
147
|
-
*/
|
|
148
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
149
|
-
const STALE_LOCK_MS = 120_000;
|
|
150
|
-
/** Default core version when none provided. */
|
|
151
|
-
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
152
|
-
/** Lock retry options. */
|
|
153
|
-
const LOCK_RETRIES = { retries: 0 };
|
|
154
|
-
/** Maximum rename retry attempts on EPERM. */
|
|
155
|
-
const ATOMIC_WRITE_MAX_RETRIES = 3;
|
|
156
|
-
/** Delay between EPERM retries in milliseconds. */
|
|
157
|
-
const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
|
|
158
|
-
/**
|
|
159
|
-
* Write content to a file atomically via a temp file + rename.
|
|
160
|
-
*
|
|
161
|
-
* @remarks
|
|
162
|
-
* Retries the rename up to three times on EPERM (Windows file-handle
|
|
163
|
-
* contention) with a 100 ms synchronous delay between attempts.
|
|
164
|
-
*
|
|
165
|
-
* @param filePath - Absolute path to the target file.
|
|
166
|
-
* @param content - Content to write.
|
|
167
|
-
*/
|
|
168
|
-
function atomicWrite(filePath, content) {
|
|
169
|
-
const dir = dirname(filePath);
|
|
170
|
-
const base = basename(filePath, '.md');
|
|
171
|
-
const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
|
|
172
|
-
writeFileSync(tempPath, content, 'utf-8');
|
|
173
|
-
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
174
|
-
try {
|
|
175
|
-
renameSync(tempPath, filePath);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
const isEperm = err instanceof Error &&
|
|
180
|
-
'code' in err &&
|
|
181
|
-
err.code === 'EPERM';
|
|
182
|
-
if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
|
|
183
|
-
try {
|
|
184
|
-
unlinkSync(tempPath);
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
/* best-effort cleanup */
|
|
188
|
-
}
|
|
189
|
-
throw err;
|
|
190
|
-
}
|
|
191
|
-
// Synchronous sleep before retry (acceptable in atomic write context)
|
|
192
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
async function withLock(targetPath, fn, options, onLockError) {
|
|
197
|
-
let release;
|
|
198
|
-
try {
|
|
199
|
-
release = await lock(targetPath, options);
|
|
200
|
-
await fn();
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
throw error;
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
if (release) {
|
|
207
|
-
try {
|
|
208
|
-
await release();
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
// Lock already released or file deleted — safe to ignore
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Execute a callback while holding a file lock.
|
|
218
|
-
*
|
|
219
|
-
* @remarks
|
|
220
|
-
* Acquires a lock on the file, executes the callback, and releases
|
|
221
|
-
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
222
|
-
* and retries up to 5 times.
|
|
223
|
-
*
|
|
224
|
-
* @param filePath - Absolute path to the file to lock.
|
|
225
|
-
* @param fn - Async callback to execute while holding the lock.
|
|
226
|
-
*/
|
|
227
|
-
async function withFileLock(filePath, fn) {
|
|
228
|
-
await withLock(filePath, fn, {
|
|
229
|
-
stale: STALE_LOCK_MS,
|
|
230
|
-
retries: LOCK_RETRIES,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Shared component version state file management.
|
|
236
|
-
*
|
|
237
|
-
* @remarks
|
|
238
|
-
* Each `ComponentWriter` cycle writes its component's entry to
|
|
239
|
-
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
240
|
-
* template reads this file to populate ALL rows in the service health
|
|
241
|
-
* table, not just the calling component's.
|
|
242
|
-
*/
|
|
243
|
-
/**
|
|
244
|
-
* Read the component versions state file.
|
|
245
|
-
*
|
|
246
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
247
|
-
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
248
|
-
*/
|
|
249
|
-
function readComponentVersions(coreConfigDir) {
|
|
250
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
251
|
-
if (!existsSync(filePath))
|
|
252
|
-
return {};
|
|
253
|
-
try {
|
|
254
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
255
|
-
return JSON.parse(raw);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
return {};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Write a component's version entry to the shared state file.
|
|
263
|
-
*
|
|
264
|
-
* @remarks
|
|
265
|
-
* Reads the existing file, merges the new entry, and writes atomically.
|
|
266
|
-
*
|
|
267
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
268
|
-
* @param options - Component version data to write.
|
|
269
|
-
*/
|
|
270
|
-
function writeComponentVersion(coreConfigDir, options) {
|
|
271
|
-
const existing = readComponentVersions(coreConfigDir);
|
|
272
|
-
existing[options.componentName] = {
|
|
273
|
-
pluginVersion: options.pluginVersion,
|
|
274
|
-
servicePackage: options.servicePackage,
|
|
275
|
-
pluginPackage: options.pluginPackage,
|
|
276
|
-
updatedAt: new Date().toISOString(),
|
|
277
|
-
};
|
|
278
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
279
|
-
const dir = dirname(filePath);
|
|
280
|
-
if (!existsSync(dir)) {
|
|
281
|
-
mkdirSync(dir, { recursive: true });
|
|
282
|
-
}
|
|
283
|
-
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Remove a component's version entry from the shared state file.
|
|
287
|
-
*
|
|
288
|
-
* @remarks
|
|
289
|
-
* Called during plugin uninstall to prevent the HEARTBEAT writer from
|
|
290
|
-
* probing a service that's intentionally gone. If the component isn't
|
|
291
|
-
* in the file, this is a no-op.
|
|
292
|
-
*
|
|
293
|
-
* @param coreConfigDir - Path to the core config directory.
|
|
294
|
-
* @param componentName - The component name to remove.
|
|
295
|
-
*/
|
|
296
|
-
function removeComponentVersion(coreConfigDir, componentName) {
|
|
297
|
-
const existing = readComponentVersions(coreConfigDir);
|
|
298
|
-
if (!(componentName in existing))
|
|
299
|
-
return;
|
|
300
|
-
const updated = Object.fromEntries(Object.entries(existing).filter(([key]) => key !== componentName));
|
|
301
|
-
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
302
|
-
atomicWrite(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Comment markers for managed content blocks.
|
|
307
|
-
*
|
|
308
|
-
* @remarks
|
|
309
|
-
* Managed content in TOOLS.md, SOUL.md, and AGENTS.md is enclosed
|
|
310
|
-
* in HTML comment markers. Content between markers is refreshed
|
|
311
|
-
* atomically on each writer cycle. User content outside the markers
|
|
312
|
-
* is never touched.
|
|
313
|
-
*/
|
|
314
|
-
/** Default markers for TOOLS.md managed block. */
|
|
315
|
-
const TOOLS_MARKERS = {
|
|
316
|
-
/** BEGIN comment marker text. */
|
|
317
|
-
begin: 'BEGIN JEEVES PLATFORM TOOLS — DO NOT EDIT THIS SECTION',
|
|
318
|
-
/** END comment marker text. */
|
|
319
|
-
end: 'END JEEVES PLATFORM TOOLS',
|
|
320
|
-
/** H1 title prepended in section mode. */
|
|
321
|
-
title: 'Jeeves Platform Tools',
|
|
322
|
-
/** Managed block at bottom of file. */
|
|
323
|
-
position: 'bottom',
|
|
324
|
-
};
|
|
325
|
-
/**
|
|
326
|
-
* Regex pattern to extract version stamp from a BEGIN marker comment.
|
|
327
|
-
*
|
|
328
|
-
* @remarks
|
|
329
|
-
* Format: `\<!-- BEGIN MARKER | core:X.Y.Z | ISO-TIMESTAMP --\>`
|
|
330
|
-
* Captures: [1] marker text, [2] version, [3] timestamp
|
|
331
|
-
*/
|
|
332
|
-
const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Managed section IDs, stable ordering, and platform component registry.
|
|
336
|
-
*
|
|
337
|
-
* @remarks
|
|
338
|
-
* Section ordering is fixed to prevent diff churn regardless of which
|
|
339
|
-
* component writes last. Sections always appear in this order.
|
|
340
|
-
*/
|
|
341
|
-
/** Known section IDs for TOOLS.md managed block. */
|
|
342
|
-
const SECTION_IDS = {
|
|
343
|
-
/** Platform health and guidance section. */
|
|
344
|
-
Platform: 'Platform',
|
|
345
|
-
/** Watcher index stats and search configuration. */
|
|
346
|
-
Watcher: 'Watcher',
|
|
347
|
-
/** Server export capabilities and connected services. */
|
|
348
|
-
Server: 'Server',
|
|
349
|
-
/** Runner job status and active scripts. */
|
|
350
|
-
Runner: 'Runner',
|
|
351
|
-
/** Meta synthesis entity summary and tools. */
|
|
352
|
-
Meta: 'Meta',
|
|
353
|
-
};
|
|
354
|
-
/**
|
|
355
|
-
* Stable ordering of sections within the managed TOOLS.md block.
|
|
356
|
-
* Sections always appear in this order regardless of write order.
|
|
357
|
-
*/
|
|
358
|
-
const SECTION_ORDER = [
|
|
359
|
-
SECTION_IDS.Platform,
|
|
360
|
-
SECTION_IDS.Watcher,
|
|
361
|
-
SECTION_IDS.Server,
|
|
362
|
-
SECTION_IDS.Runner,
|
|
363
|
-
SECTION_IDS.Meta,
|
|
364
|
-
];
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Workspace and config root initialization.
|
|
368
|
-
*
|
|
369
|
-
* @remarks
|
|
370
|
-
* `init()` must be called once before any other core library functions.
|
|
371
|
-
* It caches `workspacePath` and `configRoot` at module level.
|
|
372
|
-
* Core derives all namespaced paths from these values:
|
|
373
|
-
* - `{configRoot}/jeeves-core/` for core config
|
|
374
|
-
* - `{configRoot}/jeeves-{name}/` for each component
|
|
375
|
-
*/
|
|
376
|
-
let state;
|
|
377
|
-
const WINDOWS_DRIVE_RE = /^[a-zA-Z]:/;
|
|
378
|
-
/**
|
|
379
|
-
* Throw if a path looks like a Windows drive letter on a non-Windows platform.
|
|
380
|
-
*
|
|
381
|
-
* @param label - Human-readable name for the path (used in error messages).
|
|
382
|
-
* @param value - The raw path string to validate.
|
|
383
|
-
*/
|
|
384
|
-
function rejectWindowsDrivePath(label, value) {
|
|
385
|
-
if (process.platform !== 'win32' && WINDOWS_DRIVE_RE.test(value)) {
|
|
386
|
-
throw new Error(`jeeves-core: ${label} "${value}" looks like a Windows drive-letter path and will not resolve correctly on this platform.`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Initialize the core library with workspace and config root paths.
|
|
391
|
-
*
|
|
392
|
-
* @param options - Workspace and config root paths.
|
|
393
|
-
*/
|
|
394
|
-
function init(options) {
|
|
395
|
-
rejectWindowsDrivePath('configRoot', options.configRoot);
|
|
396
|
-
rejectWindowsDrivePath('workspacePath', options.workspacePath);
|
|
397
|
-
state = {
|
|
398
|
-
workspacePath: options.workspacePath,
|
|
399
|
-
configRoot: options.configRoot,
|
|
400
|
-
coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
|
|
401
|
-
componentConfigPaths: new Map(),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Get the core config directory path.
|
|
406
|
-
*
|
|
407
|
-
* @throws Error if `init()` has not been called.
|
|
408
|
-
*/
|
|
409
|
-
function getCoreConfigDir() {
|
|
410
|
-
if (!state)
|
|
411
|
-
throw new Error('jeeves-core: init() must be called first');
|
|
412
|
-
return state.coreConfigDir;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Heading-based HEARTBEAT section writer.
|
|
417
|
-
*
|
|
418
|
-
* @remarks
|
|
419
|
-
* Manages the `# Jeeves Platform Status` section in HEARTBEAT.md.
|
|
420
|
-
* Unlike TOOLS/SOUL/AGENTS (which use HTML comment markers), HEARTBEAT
|
|
421
|
-
* uses markdown headings as markers — this ensures the file passes
|
|
422
|
-
* OpenClaw's heartbeat emptiness check when only headings remain.
|
|
423
|
-
*
|
|
424
|
-
* The section is always at the bottom of the file (H1 to EOF).
|
|
425
|
-
* User heartbeat items above the section are preserved.
|
|
426
|
-
*/
|
|
427
|
-
/** The H1 heading that anchors the platform status section. */
|
|
428
|
-
const HEARTBEAT_HEADING = '# Jeeves Platform Status';
|
|
429
|
-
/**
|
|
430
|
-
* Parse the HEARTBEAT.md file content.
|
|
431
|
-
*
|
|
432
|
-
* @param fileContent - Full file content.
|
|
433
|
-
* @returns Parsed result with user zone and component entries.
|
|
434
|
-
*/
|
|
435
|
-
function parseHeartbeat(fileContent) {
|
|
436
|
-
const headingIndex = fileContent.indexOf(HEARTBEAT_HEADING);
|
|
437
|
-
if (headingIndex === -1) {
|
|
438
|
-
return {
|
|
439
|
-
userContent: fileContent.trim(),
|
|
440
|
-
found: false,
|
|
441
|
-
entries: [],
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
445
|
-
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
446
|
-
const entries = [];
|
|
447
|
-
const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
|
|
448
|
-
let match;
|
|
449
|
-
const h2Positions = [];
|
|
450
|
-
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
451
|
-
const fullHeading = match[0];
|
|
452
|
-
const name = match[1];
|
|
453
|
-
const declined = fullHeading.endsWith(': declined');
|
|
454
|
-
h2Positions.push({ name, declined, start: match.index });
|
|
455
|
-
}
|
|
456
|
-
for (let i = 0; i < h2Positions.length; i++) {
|
|
457
|
-
const pos = h2Positions[i];
|
|
458
|
-
const headingLine = pos.declined
|
|
459
|
-
? `## ${pos.name}: declined`
|
|
460
|
-
: `## ${pos.name}`;
|
|
461
|
-
const contentStart = pos.start + headingLine.length;
|
|
462
|
-
const contentEnd = i + 1 < h2Positions.length
|
|
463
|
-
? h2Positions[i + 1].start
|
|
464
|
-
: sectionContent.length;
|
|
465
|
-
const content = sectionContent.slice(contentStart, contentEnd).trim();
|
|
466
|
-
entries.push({
|
|
467
|
-
name: pos.name,
|
|
468
|
-
declined: pos.declined,
|
|
469
|
-
content,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return { userContent, found: true, entries };
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Build the HEARTBEAT section content from entries.
|
|
476
|
-
*
|
|
477
|
-
* @param entries - Component entries to write.
|
|
478
|
-
* @returns The full section string (H1 + H2s).
|
|
479
|
-
*/
|
|
480
|
-
function buildHeartbeatSection(entries) {
|
|
481
|
-
const parts = [HEARTBEAT_HEADING];
|
|
482
|
-
for (const entry of entries) {
|
|
483
|
-
if (entry.declined) {
|
|
484
|
-
parts.push(`## ${entry.name}: declined`);
|
|
485
|
-
}
|
|
486
|
-
else if (entry.content) {
|
|
487
|
-
parts.push(`## ${entry.name}`);
|
|
488
|
-
parts.push(entry.content);
|
|
489
|
-
}
|
|
490
|
-
// Healthy components (no content, not declined) get no H2 section
|
|
491
|
-
}
|
|
492
|
-
return parts.join('\n');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Stable section ordering for managed TOOLS.md blocks.
|
|
497
|
-
*
|
|
498
|
-
* @remarks
|
|
499
|
-
* Sorts sections by the canonical SECTION_ORDER: known sections
|
|
500
|
-
* appear in their defined order, unknown sections are appended after.
|
|
501
|
-
* Used by both parseManaged (for consistent output) and
|
|
502
|
-
* updateManagedSection (for reassembly).
|
|
503
|
-
*/
|
|
504
|
-
/**
|
|
505
|
-
* Sort sections in place by stable ordering.
|
|
506
|
-
*
|
|
507
|
-
* @param sections - Array of managed sections to sort.
|
|
508
|
-
* @returns The sorted array (same reference, mutated in place).
|
|
509
|
-
*/
|
|
510
|
-
function sortSectionsByOrder(sections) {
|
|
511
|
-
return sections.sort((a, b) => {
|
|
512
|
-
const aIdx = SECTION_ORDER.indexOf(a.id);
|
|
513
|
-
const bIdx = SECTION_ORDER.indexOf(b.id);
|
|
514
|
-
const aOrder = aIdx === -1 ? SECTION_ORDER.length : aIdx;
|
|
515
|
-
const bOrder = bIdx === -1 ? SECTION_ORDER.length : bIdx;
|
|
516
|
-
return aOrder - bOrder;
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Parse managed block from file content.
|
|
522
|
-
*
|
|
523
|
-
* @remarks
|
|
524
|
-
* Extracts managed content delimited by comment markers, parses H2
|
|
525
|
-
* sections within the block, and returns the structured result plus
|
|
526
|
-
* user content outside the markers.
|
|
527
|
-
*/
|
|
528
|
-
/**
|
|
529
|
-
* Escape a string for safe use as a literal in a RegExp pattern.
|
|
530
|
-
*
|
|
531
|
-
* @param str - The string to escape.
|
|
532
|
-
* @returns The escaped string.
|
|
533
|
-
*/
|
|
534
|
-
function escapeForRegex(str) {
|
|
535
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Build regex patterns for the given markers.
|
|
539
|
-
*
|
|
540
|
-
* @param markers - Begin/end marker strings.
|
|
541
|
-
* @returns Object with begin and end regex patterns.
|
|
542
|
-
*/
|
|
543
|
-
function buildMarkerPatterns(markers) {
|
|
544
|
-
return {
|
|
545
|
-
beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
|
|
546
|
-
endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Parse H2 sections from managed block content.
|
|
551
|
-
*
|
|
552
|
-
* @param content - Raw managed block content.
|
|
553
|
-
* @returns Array of parsed sections in stable order.
|
|
554
|
-
*/
|
|
555
|
-
function parseSections(content) {
|
|
556
|
-
const lines = content.split('\n');
|
|
557
|
-
const sections = [];
|
|
558
|
-
let currentId;
|
|
559
|
-
let currentLines = [];
|
|
560
|
-
for (const line of lines) {
|
|
561
|
-
const h2Match = /^## (.+)$/.exec(line);
|
|
562
|
-
if (h2Match) {
|
|
563
|
-
if (currentId !== undefined) {
|
|
564
|
-
sections.push({
|
|
565
|
-
id: currentId,
|
|
566
|
-
content: currentLines.join('\n').trim(),
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
currentId = h2Match[1];
|
|
570
|
-
currentLines = [];
|
|
571
|
-
}
|
|
572
|
-
else if (currentId !== undefined) {
|
|
573
|
-
currentLines.push(line);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (currentId !== undefined) {
|
|
577
|
-
sections.push({
|
|
578
|
-
id: currentId,
|
|
579
|
-
content: currentLines.join('\n').trim(),
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
return sortSectionsByOrder(sections);
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Parse a managed block from file content.
|
|
586
|
-
*
|
|
587
|
-
* @param fileContent - Full file content.
|
|
588
|
-
* @param markers - Optional custom markers (defaults to TOOLS markers).
|
|
589
|
-
* @returns Parsed result with sections, version stamp, and user content.
|
|
590
|
-
*/
|
|
591
|
-
function parseManaged(fileContent, markers = TOOLS_MARKERS) {
|
|
592
|
-
const { beginRe, endRe } = buildMarkerPatterns(markers);
|
|
593
|
-
const beginMatch = beginRe.exec(fileContent);
|
|
594
|
-
if (!beginMatch) {
|
|
595
|
-
return {
|
|
596
|
-
found: false,
|
|
597
|
-
versionStamp: undefined,
|
|
598
|
-
managedContent: '',
|
|
599
|
-
sections: [],
|
|
600
|
-
beforeContent: '',
|
|
601
|
-
userContent: fileContent,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
const endMatch = endRe.exec(fileContent.slice(beginMatch.index + beginMatch[0].length));
|
|
605
|
-
if (!endMatch) {
|
|
606
|
-
// Corrupt: BEGIN without END — treat as fresh file
|
|
607
|
-
return {
|
|
608
|
-
found: false,
|
|
609
|
-
versionStamp: undefined,
|
|
610
|
-
managedContent: '',
|
|
611
|
-
sections: [],
|
|
612
|
-
beforeContent: '',
|
|
613
|
-
userContent: fileContent,
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
const beforeContent = fileContent.slice(0, beginMatch.index).trim();
|
|
617
|
-
const managedStart = beginMatch.index + beginMatch[0].length;
|
|
618
|
-
const managedEnd = managedStart + endMatch.index;
|
|
619
|
-
const managedContent = fileContent.slice(managedStart, managedEnd).trim();
|
|
620
|
-
const afterEnd = managedStart + endMatch.index + endMatch[0].length;
|
|
621
|
-
const userContent = fileContent.slice(afterEnd).trim();
|
|
622
|
-
// Extract version stamp from BEGIN marker line
|
|
623
|
-
let versionStamp;
|
|
624
|
-
const stampMatch = VERSION_STAMP_PATTERN.exec(beginMatch[0]);
|
|
625
|
-
if (stampMatch?.[2] && stampMatch[3]) {
|
|
626
|
-
versionStamp = {
|
|
627
|
-
version: stampMatch[2],
|
|
628
|
-
timestamp: stampMatch[3],
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
const sections = parseSections(managedContent);
|
|
632
|
-
return {
|
|
633
|
-
found: true,
|
|
634
|
-
versionStamp,
|
|
635
|
-
managedContent,
|
|
636
|
-
sections,
|
|
637
|
-
beforeContent,
|
|
638
|
-
userContent,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Version-stamp parsing and convergence logic.
|
|
644
|
-
*
|
|
645
|
-
* @remarks
|
|
646
|
-
* When multiple component plugins bundle different core library versions,
|
|
647
|
-
* they independently maintain shared managed content. The version-stamp
|
|
648
|
-
* mechanism ensures convergence without coordination state.
|
|
649
|
-
*/
|
|
650
|
-
/**
|
|
651
|
-
* Format the BEGIN marker comment with a version stamp.
|
|
652
|
-
*
|
|
653
|
-
* @param markerText - The marker text (e.g., 'BEGIN JEEVES PLATFORM TOOLS').
|
|
654
|
-
* @param version - The core library version.
|
|
655
|
-
* @returns Formatted comment line.
|
|
656
|
-
*/
|
|
657
|
-
function formatBeginMarker(markerText, version) {
|
|
658
|
-
const timestamp = new Date().toISOString();
|
|
659
|
-
return `<!-- ${markerText} | core:${version} | ${timestamp} -->`;
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Format the END marker comment.
|
|
663
|
-
*
|
|
664
|
-
* @param markerText - The marker text (e.g., 'END JEEVES PLATFORM TOOLS').
|
|
665
|
-
* @returns Formatted comment line.
|
|
666
|
-
*/
|
|
667
|
-
function formatEndMarker(markerText) {
|
|
668
|
-
return `<!-- ${markerText} -->`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Remove a managed section or entire managed block from a file.
|
|
673
|
-
*
|
|
674
|
-
* @remarks
|
|
675
|
-
* Supports two modes:
|
|
676
|
-
* - No `sectionId`: Remove the entire managed block (markers + content),
|
|
677
|
-
* leaving user content intact.
|
|
678
|
-
* - With `sectionId`: Remove a specific H2 section from within the
|
|
679
|
-
* managed block. If it was the last section, remove the entire block.
|
|
680
|
-
*
|
|
681
|
-
* Provides file-level locking and atomic writes (temp file + rename).
|
|
682
|
-
* Missing markers or nonexistent sections are no-ops (no error thrown).
|
|
683
|
-
*/
|
|
684
|
-
/**
|
|
685
|
-
* Remove a managed section or entire managed block from a file.
|
|
686
|
-
*
|
|
687
|
-
* @param filePath - Absolute path to the target file.
|
|
688
|
-
* @param options - Optional section ID and custom markers.
|
|
689
|
-
*/
|
|
690
|
-
async function removeManagedSection(filePath, options = {}) {
|
|
691
|
-
const { sectionId, markers = TOOLS_MARKERS } = options;
|
|
692
|
-
if (!existsSync(filePath))
|
|
693
|
-
return;
|
|
694
|
-
await withFileLock(filePath, () => {
|
|
695
|
-
const fileContent = readFileSync(filePath, 'utf-8');
|
|
696
|
-
const parsed = parseManaged(fileContent, markers);
|
|
697
|
-
if (!parsed.found)
|
|
698
|
-
return;
|
|
699
|
-
let newContent;
|
|
700
|
-
if (!sectionId) {
|
|
701
|
-
// Remove entire managed block
|
|
702
|
-
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
// Remove specific section
|
|
706
|
-
const remaining = parsed.sections.filter((s) => s.id !== sectionId);
|
|
707
|
-
if (remaining.length === parsed.sections.length) {
|
|
708
|
-
// Section not found — no-op
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (remaining.length === 0) {
|
|
712
|
-
// Last section removed — remove entire block
|
|
713
|
-
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
// Rebuild managed block without the removed section
|
|
717
|
-
newContent = buildWithSections(parsed.beforeContent, parsed.userContent, remaining, markers, parsed.versionStamp?.version);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
atomicWrite(filePath, newContent);
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
/** Build file content without the managed block. */
|
|
724
|
-
function buildWithoutBlock(beforeContent, userContent) {
|
|
725
|
-
const parts = [];
|
|
726
|
-
if (beforeContent)
|
|
727
|
-
parts.push(beforeContent);
|
|
728
|
-
if (userContent) {
|
|
729
|
-
if (parts.length > 0)
|
|
730
|
-
parts.push('');
|
|
731
|
-
parts.push(userContent);
|
|
732
|
-
}
|
|
733
|
-
if (parts.length === 0)
|
|
734
|
-
return '';
|
|
735
|
-
return parts.join('\n') + '\n';
|
|
736
|
-
}
|
|
737
|
-
/** Rebuild file content with remaining sections. */
|
|
738
|
-
function buildWithSections(beforeContent, userContent, sections, markers, coreVersion) {
|
|
739
|
-
const sorted = sortSectionsByOrder([...sections]);
|
|
740
|
-
const sectionText = sorted
|
|
741
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
742
|
-
.join('\n\n');
|
|
743
|
-
const managedBody = markers.title
|
|
744
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
745
|
-
: sectionText;
|
|
746
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion ?? DEFAULT_CORE_VERSION);
|
|
747
|
-
const endLine = formatEndMarker(markers.end);
|
|
748
|
-
const parts = [];
|
|
749
|
-
if (beforeContent) {
|
|
750
|
-
parts.push(beforeContent);
|
|
751
|
-
parts.push('');
|
|
752
|
-
}
|
|
753
|
-
parts.push(beginLine);
|
|
754
|
-
parts.push('');
|
|
755
|
-
parts.push(managedBody);
|
|
756
|
-
parts.push('');
|
|
757
|
-
parts.push(endLine);
|
|
758
|
-
if (userContent) {
|
|
759
|
-
parts.push('');
|
|
760
|
-
parts.push(userContent);
|
|
761
|
-
}
|
|
762
|
-
parts.push('');
|
|
763
|
-
return parts.join('\n');
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
var codingContent = `---
|
|
767
|
-
name: coding
|
|
768
|
-
description: Engineering standards for all code work. Use when writing code, reviewing PRs, spawning coding sub-agents, or making architectural decisions in any project (not just Jeeves). Covers design-first development, schema-first patterns, testing, STAN workflow, dependency management, and pre-PR checklist.
|
|
769
|
-
---
|
|
770
|
-
|
|
771
|
-
# Engineering Standards
|
|
772
|
-
|
|
773
|
-
These standards apply to ALL code work — whether done directly or via sub-agents.
|
|
774
|
-
When spawning sub-agents for coding tasks, include the relevant rules in the task prompt.
|
|
775
|
-
Sub-agents don't inherit your context — if you don't pass the rules, they don't exist.
|
|
776
|
-
|
|
777
|
-
---
|
|
778
|
-
|
|
779
|
-
## Design-First Development
|
|
780
|
-
|
|
781
|
-
1. **Iterate on design until convergence** — Summarize requirements, propose approach, raise questions BEFORE writing code.
|
|
782
|
-
2. **Services-first architecture** — Core logic in services behind ports; adapters thin; side effects at boundaries.
|
|
783
|
-
3. **Schema-first** — Runtime schema (Zod) is source of truth; TypeScript types derived via \`z.infer<>\`; validation centralized. Plain TypeScript \`interface\` declarations for config surfaces are not acceptable.
|
|
784
|
-
4. **300 LOC hard limit** — If a file would exceed 300 lines, stop and decompose first. No exceptions.
|
|
785
|
-
5. **Avoid \`any\`** — Prefer \`unknown\` + narrowing; if unavoidable, narrowest scope + rationale.
|
|
786
|
-
6. **Test pairing** — Every non-trivial module gets a \`*.test.ts\`.
|
|
787
|
-
7. **Open-source first** — Prefer established deps over home-grown solutions. Search npm/GitHub before building anything non-trivial.
|
|
788
|
-
|
|
789
|
-
## Module Design
|
|
790
|
-
|
|
791
|
-
- **Single Responsibility** applies to modules as well as functions.
|
|
792
|
-
- Prefer many small modules over a few large ones.
|
|
793
|
-
- Keep module boundaries explicit and cohesive; avoid "kitchen-sink" files.
|
|
794
|
-
- Co-locate tests with modules for discoverability.
|
|
795
|
-
|
|
796
|
-
## Config Surfaces
|
|
797
|
-
|
|
798
|
-
- Define config with **Zod schemas** — never bare TypeScript interfaces.
|
|
799
|
-
- Derive types: \`type MyConfig = z.infer<typeof myConfigSchema>\`
|
|
800
|
-
- Generate **JSON Schema** from Zod for IDE DX (\`\$schema\` pointer in config files).
|
|
801
|
-
- Validate at load time — fail fast with clear error messages.
|
|
802
|
-
- \`init\` commands generate config with \`\$schema\` pointer already in place.
|
|
803
|
-
|
|
804
|
-
## Testing
|
|
805
|
-
|
|
806
|
-
- **Unit tests** for pure services (no fs/process/network).
|
|
807
|
-
- **Integration tests** for adapters/seams (minimal end-to-end slices).
|
|
808
|
-
- Exercise happy paths AND representative error paths.
|
|
809
|
-
- Table-driven cases encouraged for exhaustive coverage.
|
|
810
|
-
- Keep coverage meaningful — prefer covering branches/decisions over chasing 100% lines.
|
|
811
|
-
|
|
812
|
-
## STAN-Enabled Repos
|
|
813
|
-
|
|
814
|
-
When working in a repo with \`.stan/\`:
|
|
815
|
-
- Run \`stan run --sequential --no-archive\` **before** each commit. Scripts must pass before you commit. Sequential runs are preferred to limit side effects. Archives are not needed (you won't use them).
|
|
816
|
-
- **Push after every commit.** Don't accumulate unpushed local commits. Jason needs to be able to see your work at any time.
|
|
817
|
-
- All scripts must pass before claiming work is complete.
|
|
818
|
-
- Read \`.stan/output/<script>.txt\` for evidence on failures.
|
|
819
|
-
- When creating stan scripts, eliminate colorized output where possible (e.g. \`--no-color\`, \`NO_COLOR=1\`) to reduce noise in script output files.
|
|
820
|
-
|
|
821
|
-
## Cross-Package Verification
|
|
822
|
-
|
|
823
|
-
When changes affect exports consumed by another repo:
|
|
824
|
-
- Standalone scripts passing ≠ "ready for review."
|
|
825
|
-
- Use \`npm link\` or equivalent to verify the consumer builds against your changes.
|
|
826
|
-
- Only claim completion when BOTH repos pass.
|
|
827
|
-
|
|
828
|
-
## Dependencies: Latest Versions Required (HARD GATE)
|
|
829
|
-
|
|
830
|
-
**NEVER use a superseded version of ANY dependency without direct human authorization.** When adding a new dependency — or creating a new project — ALWAYS check the latest stable version and use it. This applies to runtime deps, dev deps, and peer deps alike.
|
|
831
|
-
|
|
832
|
-
- Before \`npm install <package>\`: run \`npm view <package> version\` (or check npmjs.com) to confirm you're installing the current major.
|
|
833
|
-
- Before spawning sub-agents that install packages: include the latest version in the task prompt, or instruct the sub-agent to verify latest before installing.
|
|
834
|
-
- If the latest major has known breaking issues that block adoption, flag it to the human — don't silently pin an old major.
|
|
835
|
-
|
|
836
|
-
LLMs are trained on stale data. Your training cutoff means you will default to old versions of everything. **Assume your version knowledge is wrong** and verify before every install.
|
|
837
|
-
|
|
838
|
-
*Earned: 2026-05-12, created the jeeves-tools repo with Zod 3 despite Zod 4 being available since mid-2025. Shipped 84 commits on the old major before catching it.*
|
|
839
|
-
|
|
840
|
-
## Dependencies: Local Over Global
|
|
841
|
-
|
|
842
|
-
- **Dev dependencies belong in the project, not the global environment.** Install with \`npm install --save-dev\`, not \`npm install -g\`.
|
|
843
|
-
- This guarantees reproducibility across machines and CI. Global installs mask environment differences that cause "works on my machine" failures.
|
|
844
|
-
- **Rare exceptions:** Tools that are genuinely machine-level utilities (e.g. \`stan-cli\`). If in doubt, install locally.
|
|
845
|
-
## Dependency Failures
|
|
846
|
-
|
|
847
|
-
When a third-party dependency is broken:
|
|
848
|
-
1. Summarize the failure concisely.
|
|
849
|
-
2. Enumerate options: switch dependency → fix upstream → temporary pin → shim (last resort).
|
|
850
|
-
3. Recommend with rationale.
|
|
851
|
-
4. Do NOT immediately code around the problem.
|
|
852
|
-
|
|
853
|
-
## CHANGELOG
|
|
854
|
-
|
|
855
|
-
- **Do not manually update CHANGELOG.md** — it is generated as part of the release process (e.g. via \`standard-version\`, \`changesets\`, or equivalent). Conventional commit messages are the input; the tooling produces the output.
|
|
856
|
-
|
|
857
|
-
## Pre-PR Checklist (HARD GATE)
|
|
858
|
-
|
|
859
|
-
**Before creating ANY PR, run the full verification sequence. No exceptions.**
|
|
860
|
-
|
|
861
|
-
1. \`stan run --sequential --no-archive\` if \`.stan/\` exists — this is the canonical check suite
|
|
862
|
-
2. In monorepos: run checks **from each package directory**, not just the root. Root-level runs may mask package-level failures due to config resolution differences.
|
|
863
|
-
3. Exercise the release path: check \`release-it\` hooks (or equivalent) in each releasable package — run the same commands (\`lint\`, \`typecheck\`, \`test\`, \`build\`) from the same cwd the release process uses.
|
|
864
|
-
|
|
865
|
-
If any step fails, fix it before committing. Do NOT create the PR and "note" the failures. Do NOT claim pre-existing failures without having actually run the commands first. Skipping this sequence is how we ship broken code and fabricate diagnoses.
|
|
866
|
-
|
|
867
|
-
- **Compare against canonical template** (\`karmaniverous/npm-package-template-ts\`) before any npm package PR. If the project is behind the template, update it to conform. If the template is behind the project, raise the issue with Jason for template upkeep.
|
|
868
|
-
- **Run \`ncu --peer\`** before any PR. Review the output. Update safe patches/minors. **Flag major version bumps for discussion** — never auto-apply \`ncu -u\` without reading what changed. Peer dep conflicts must be resolved, not ignored.
|
|
869
|
-
- When spawning sub-agents, include \`ncu --peer\` in the quality gate commands: \`ncu --peer && npm run lint && npm run typecheck && npm run build && npm test\`. The sub-agent should report \`ncu\` output and only apply updates that don't involve major bumps or peer conflicts.
|
|
870
|
-
- **Resolve ALL script warnings.** It is NOT acceptable to release code with outstanding warnings. They exist for a reason — fix them.
|
|
871
|
-
- **Typecheck/lint rules apply to ALL authored code**, including configs at project root (\`eslint.config.ts\`, \`rollup.config.ts\`, \`vitest.config.ts\`, etc.). Only generated code (e.g. typedoc output) should be excepted from code quality checks.
|
|
872
|
-
- **Never disable lint/typecheck rules** without surfacing it for discussion first. Disabled rules are a major code smell. If a rule must be disabled, document the rationale inline at the point of suppression.
|
|
873
|
-
- **Multiple tsconfigs are a code smell.** Sometimes needed, but often they paper over poor configuration choices. Fix root causes rather than adding tsconfig variants.
|
|
874
|
-
- **In a TS repo, all scripts should be authored in TS** (not JS). Prefer execution with \`tsx\`.
|
|
875
|
-
- **Clean-room verify before claiming "all green."** Run \`rimraf node_modules && npm install && npm run build\` (or equivalent) to catch issues masked by cached state. If someone reports an error you can't reproduce, assume your cache is lying — not that they're wrong.
|
|
876
|
-
- Verify build, test, and lint pass after updates.
|
|
877
|
-
|
|
878
|
-
## Dev Workspace
|
|
879
|
-
|
|
880
|
-
- **Clone location:** \`D:\\repos\\{org-or-userid}\\{repo}\` (e.g. \`D:\\repos\\karmaniverous\\jeeves-watcher\`)
|
|
881
|
-
- D drive is the dev workspace. Do not clone repos elsewhere.
|
|
882
|
-
- Fresh clones are preferred over copying existing checkouts — \`npm install\` from registry is faster than disk-copying \`node_modules\`.
|
|
883
|
-
- D drive is NOT indexed by jeeves-watcher. Dev work stays off the archive.
|
|
884
|
-
|
|
885
|
-
## GitHub Auth (HARD GATE)
|
|
886
|
-
|
|
887
|
-
- **ALL GitHub operations use \`jgs-jeeves\` auth.** Set \`GH_TOKEN\` before any \`gh\` CLI command:
|
|
888
|
-
\`\`\`powershell
|
|
889
|
-
\$env:GH_TOKEN = (Get-Content "J:\\config\\credentials\\github\\jgs-jeeves.token" -Raw).Trim()
|
|
890
|
-
\`\`\`
|
|
891
|
-
- Never write to GitHub as \`karmaniverous\` — that's Jason's account.
|
|
892
|
-
- If \`jgs-jeeves\` lacks permissions, **escalate to Jason** rather than falling back to \`karmaniverous\`.
|
|
893
|
-
|
|
894
|
-
## Issue Hygiene
|
|
895
|
-
|
|
896
|
-
- **Always comment rationale when closing an issue without resolution** (duplicate, won't-fix, obsolete). The close action alone doesn't explain why.
|
|
897
|
-
- Reference the replacement issue/PR when closing as duplicate.
|
|
898
|
-
|
|
899
|
-
## Code Style
|
|
900
|
-
|
|
901
|
-
- Prettier is source of truth for formatting.
|
|
902
|
-
- Keep imports sorted per repo tooling.
|
|
903
|
-
- Avoid dead code.
|
|
904
|
-
- TSDoc \`@module\` or \`@packageDocumentation\` on every non-test module.
|
|
905
|
-
- First 160 chars of module doc should be high-signal: what it does, IO/side effects, traversal hints.
|
|
906
|
-
|
|
907
|
-
## eslint-disable is a HARD GATE
|
|
908
|
-
|
|
909
|
-
Never disable lint/typecheck rules without surfacing it for discussion first. Fix the code, don't suppress the warning. For test mocks, use properly typed partial objects (\`Partial<RealType>\`, typed \`MockReply\` interfaces) instead of \`any\`. Tests are code.
|
|
910
|
-
|
|
911
|
-
## Git Merge Policy
|
|
912
|
-
|
|
913
|
-
- **No squash merges.** Preserve commit history.
|
|
914
|
-
- **PR reviewer:** When creating a PR under \`jgs-jeeves\` auth, always add \`karmaniverous\` (Jason) as a reviewer.
|
|
915
|
-
`;
|
|
916
|
-
|
|
917
|
-
var jeevesContent = `---
|
|
918
|
-
name: jeeves
|
|
919
|
-
description: Jeeves platform architecture, data flow, component interaction, scripts repo, and coordination knowledge. Use when making architectural decisions, coordinating across components, checking platform health, managing service lifecycle, or working with the scripts repo.
|
|
920
|
-
---
|
|
921
|
-
|
|
922
|
-
# Jeeves Platform Skill
|
|
923
|
-
|
|
924
|
-
## Platform Architecture
|
|
925
|
-
|
|
926
|
-
Jeeves is a four-component platform coordinated by a shared library (\`@karmaniverous/jeeves\`):
|
|
927
|
-
|
|
928
|
-
| Component | Role | Port |
|
|
929
|
-
|-----------|------|------|
|
|
930
|
-
| **jeeves-runner** | Execute: scheduled jobs, SQLite state, HTTP API | 1937 |
|
|
931
|
-
| **jeeves-watcher** | Index: file→Qdrant semantic indexing, inference rules | 1936 |
|
|
932
|
-
| **jeeves-server** | Present: web UI, file browser, doc render, export | 1934 |
|
|
933
|
-
| **jeeves-meta** | Distill: LLM synthesis, .meta/ directories, scheduling | 1938 |
|
|
934
|
-
|
|
935
|
-
Core (\`@karmaniverous/jeeves\`) is a **library + CLI**, not a service. No port.
|
|
936
|
-
|
|
937
|
-
## Data Flow
|
|
938
|
-
|
|
939
|
-
\`\`\`
|
|
940
|
-
Files → Watcher (index) → Qdrant → Meta (synthesize) → .meta/ → Watcher (re-index)
|
|
941
|
-
↓
|
|
942
|
-
Runner (schedule) → Scripts → Services ← Server (present) ← Browser
|
|
943
|
-
\`\`\`
|
|
944
|
-
|
|
945
|
-
## Component Interaction
|
|
946
|
-
|
|
947
|
-
- **Watcher** indexes files into Qdrant with inference rules and enrichments.
|
|
948
|
-
- **Meta** reads from Qdrant, synthesizes \`.meta/\` directories, which watcher re-indexes.
|
|
949
|
-
- **Runner** executes scheduled scripts that may call any service's HTTP API.
|
|
950
|
-
- **Server** presents files, renders documents, and provides the event gateway.
|
|
951
|
-
- **Core** provides shared content management (TOOLS.md, SOUL.md, AGENTS.md), service discovery, config resolution, and the component SDK.
|
|
952
|
-
|
|
953
|
-
## Service Discovery
|
|
954
|
-
|
|
955
|
-
Services find each other via config resolution:
|
|
956
|
-
1. Component's own config file (\`{configRoot}/jeeves-{name}/config.json\`)
|
|
957
|
-
2. Core config file (\`{configRoot}/jeeves-core/config.json\`)
|
|
958
|
-
3. Default port constants
|
|
959
|
-
|
|
960
|
-
## Scripts Repo
|
|
961
|
-
|
|
962
|
-
Location: \`{configRoot}/jeeves-core/scripts/\`
|
|
963
|
-
Template: \`@karmaniverous/jeeves-scripts-template\`
|
|
964
|
-
|
|
965
|
-
Scripts use utilities from \`@karmaniverous/jeeves\` (general) and \`@karmaniverous/jeeves-runner\` (runner-specific). Any script that could be useful outside runner scheduling belongs in core.
|
|
966
|
-
|
|
967
|
-
## Managed Content System
|
|
968
|
-
|
|
969
|
-
Core maintains managed sections in workspace files using comment markers:
|
|
970
|
-
- **TOOLS.md** — Component sections (section mode) + Platform section
|
|
971
|
-
- **SOUL.md** — Professional discipline and behavioral foundations (block mode)
|
|
972
|
-
- **AGENTS.md** — Operational protocols and memory architecture (block mode)
|
|
973
|
-
- **HEARTBEAT.md** — Platform health status (heading-based)
|
|
974
|
-
|
|
975
|
-
Managed blocks are stationary after initial insertion. Cleanup detection uses Jaccard similarity on 3-word shingles. Cleanup escalation spawns a gateway session when orphaned content is detected.
|
|
976
|
-
|
|
977
|
-
## Workspace Configuration
|
|
978
|
-
|
|
979
|
-
\`jeeves.config.json\` at workspace root provides shared defaults:
|
|
980
|
-
- Precedence: CLI flags → env vars → file → defaults
|
|
981
|
-
- Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl, devRepos) and \`memory.*\` (budget, warningThreshold)
|
|
982
|
-
- Inspect with \`jeeves config [jsonpath]\`
|
|
983
|
-
|
|
984
|
-
## HEARTBEAT Protocol
|
|
985
|
-
|
|
986
|
-
The HEARTBEAT system uses a state machine per component:
|
|
987
|
-
\`not_installed → deps_missing → config_missing → service_not_installed → service_stopped → healthy\`
|
|
988
|
-
|
|
989
|
-
Dependency-aware: hard deps block alerts, soft deps add informational notes. Declined components are tracked via heading suffix.
|
|
990
|
-
|
|
991
|
-
## Plugin Lifecycle
|
|
992
|
-
|
|
993
|
-
\`\`\`bash
|
|
994
|
-
# Core install (seed workspace content)
|
|
995
|
-
npx @karmaniverous/jeeves install
|
|
996
|
-
|
|
997
|
-
# Component plugin install
|
|
998
|
-
npx @karmaniverous/jeeves-{component}-openclaw install
|
|
999
|
-
|
|
1000
|
-
# Component plugin uninstall
|
|
1001
|
-
npx @karmaniverous/jeeves-{component}-openclaw uninstall
|
|
1002
|
-
|
|
1003
|
-
# Core uninstall (remove managed sections)
|
|
1004
|
-
npx @karmaniverous/jeeves uninstall
|
|
1005
|
-
\`\`\`
|
|
1006
|
-
|
|
1007
|
-
## Memory Hygiene
|
|
1008
|
-
|
|
1009
|
-
MEMORY.md has a character budget (default 20,000). Core tracks:
|
|
1010
|
-
- Character count and usage percentage
|
|
1011
|
-
- Warning at 80% of budget
|
|
1012
|
-
- Stale section candidates (H2 sections whose most recent ISO date exceeds the staleness threshold)
|
|
1013
|
-
- Evergreen sections (no dates) are never flagged
|
|
1014
|
-
|
|
1015
|
-
Review is human/agent-mediated — core does not auto-delete.
|
|
1016
|
-
|
|
1017
|
-
### HEARTBEAT Integration
|
|
1018
|
-
|
|
1019
|
-
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.
|
|
1020
|
-
|
|
1021
|
-
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\`.
|
|
1022
|
-
|
|
1023
|
-
## Workspace File Size Monitoring
|
|
1024
|
-
|
|
1025
|
-
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.
|
|
1026
|
-
|
|
1027
|
-
Core monitors all five files on every \`ComponentWriter\` cycle:
|
|
1028
|
-
- Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
|
|
1029
|
-
- Over-budget alert when charCount exceeds the budget
|
|
1030
|
-
- Missing files are silently skipped
|
|
1031
|
-
|
|
1032
|
-
### HEARTBEAT Integration
|
|
1033
|
-
|
|
1034
|
-
When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
|
|
1035
|
-
- Character count, budget, and usage percentage
|
|
1036
|
-
- 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
|
|
1037
|
-
|
|
1038
|
-
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\`).
|
|
1039
|
-
`;
|
|
1040
|
-
|
|
1041
|
-
var operationsContent = `---
|
|
1042
|
-
name: operations
|
|
1043
|
-
description: Operational knowledge for a Jeeves installation. Covers date formatting utilities, email pipeline architecture, curation signal protocol, label taxonomy, and data flow patterns. Use when working with date formatting, email scripts, debugging email pipeline issues, or understanding how human email actions are interpreted.
|
|
1044
|
-
---
|
|
1045
|
-
|
|
1046
|
-
# Operations
|
|
1047
|
-
|
|
1048
|
-
Operational knowledge for the Jeeves platform. Covers email pipeline architecture, curation protocols, and operational conventions.
|
|
1049
|
-
|
|
1050
|
-
## Date Formatting
|
|
1051
|
-
|
|
1052
|
-
A \`date-fns\` wrapper lives at \`{configRoot}/jeeves-core/scripts/src/lib/dates.ts\`. It provides:
|
|
1053
|
-
|
|
1054
|
-
| Export | Purpose |
|
|
1055
|
-
|--------|---------|
|
|
1056
|
-
| \`dayOfWeek(dateStr)\` | Full weekday name for an ISO date string (e.g. \`'Monday'\`) |
|
|
1057
|
-
| \`formatDate(dateStr, fmt)\` | Format with any date-fns pattern |
|
|
1058
|
-
| \`relativeDays(dateStr, refStr?)\` | Human-friendly relative description (\`'today'\`, \`'tomorrow'\`, \`'3 days ago'\`) |
|
|
1059
|
-
| \`parseISO\` / \`format\` | Re-exported from date-fns for direct use |
|
|
1060
|
-
|
|
1061
|
-
### Gateway session usage
|
|
1062
|
-
|
|
1063
|
-
From a gateway session, call via \`exec\`:
|
|
1064
|
-
|
|
1065
|
-
\`\`\`
|
|
1066
|
-
node -e "import { dayOfWeek } from './src/lib/dates.js'; console.log(dayOfWeek('2026-05-11'));"
|
|
1067
|
-
\`\`\`
|
|
1068
|
-
|
|
1069
|
-
with \`workdir: {configRoot}/jeeves-core/scripts\`.
|
|
1070
|
-
|
|
1071
|
-
Or use the simpler inline form when the full wrapper isn't needed:
|
|
1072
|
-
|
|
1073
|
-
\`\`\`
|
|
1074
|
-
node -e "import { format, parseISO } from 'date-fns'; console.log(format(parseISO('2026-05-11'), 'EEEE'));"
|
|
1075
|
-
\`\`\`
|
|
1076
|
-
|
|
1077
|
-
with \`workdir: {configRoot}/jeeves-core/scripts\` (so date-fns resolves from \`node_modules\`).
|
|
1078
|
-
|
|
1079
|
-
### Hard gate
|
|
1080
|
-
|
|
1081
|
-
NEVER state a day of the week without computing it first. LLMs cannot do day-of-week arithmetic reliably.
|
|
1082
|
-
|
|
1083
|
-
## Email Curation Signal Protocol
|
|
1084
|
-
|
|
1085
|
-
Defines how human email actions in Gmail are interpreted by Jeeves email processes.
|
|
1086
|
-
|
|
1087
|
-
### Human Signals
|
|
1088
|
-
|
|
1089
|
-
| Signal | Meaning | Action |
|
|
1090
|
-
|--------|---------|--------|
|
|
1091
|
-
| Label added | Human is adjusting Jeeves classification | Update domain process inputs to reflect new classification |
|
|
1092
|
-
| Label removed | Human is adjusting Jeeves classification (removal) | Update domain process inputs to reflect removed classification |
|
|
1093
|
-
| Archived → Inbox | Human wants to keep this email in sight | Add \`watch\` label via update queue |
|
|
1094
|
-
| Starred / Flagged | Elevated attention — email is important in context | Domain processes should weight higher |
|
|
1095
|
-
| Moved to Spam | Confirmed spam — human classified as junk | Learn from classification for future triage |
|
|
1096
|
-
| Removed from Spam | False positive — human rescued from spam | Process as normal email, learn from false positive |
|
|
1097
|
-
|
|
1098
|
-
### Watch Label
|
|
1099
|
-
|
|
1100
|
-
The \`watch\` label has special semantics:
|
|
1101
|
-
- When present, never auto-archive the email
|
|
1102
|
-
- When a watched email lands in archive (by anyone), remove the watch label
|
|
1103
|
-
- Added automatically when a human moves an archived email back to inbox
|
|
1104
|
-
|
|
1105
|
-
### Label Taxonomy
|
|
1106
|
-
|
|
1107
|
-
Labels applied by Jeeves processes fall into two categories:
|
|
1108
|
-
|
|
1109
|
-
**Mechanical labels** (applied by domain extractors with high confidence):
|
|
1110
|
-
- \`meeting\` — meeting-related email (invite, notes, transcript)
|
|
1111
|
-
- \`finance\` — financial email (receipt, invoice, billing, statement)
|
|
1112
|
-
|
|
1113
|
-
**Reasoning labels** (applied by Update Email Meta, requiring cross-domain context):
|
|
1114
|
-
- \`project/<name>\` — associated with a known project
|
|
1115
|
-
- \`todo\` — requires action or work from the user
|
|
1116
|
-
- \`reply\` — someone is waiting on a response
|
|
1117
|
-
- \`alert\` — automated notification from a service
|
|
1118
|
-
- \`readme\` — newsletter or subscribed informational content
|
|
1119
|
-
|
|
1120
|
-
### Labeling Principles
|
|
1121
|
-
|
|
1122
|
-
1. Label at the earliest point where confidence is high enough
|
|
1123
|
-
2. Domain extractors label what they know with certainty
|
|
1124
|
-
3. Update Email Meta labels what requires cross-domain context
|
|
1125
|
-
4. A thread can have multiple labels
|
|
1126
|
-
5. Do not re-label threads that already have the label
|
|
1127
|
-
6. **Prefer false negatives over false positives**
|
|
1128
|
-
|
|
1129
|
-
### Poll Scope
|
|
1130
|
-
|
|
1131
|
-
Query: \`newer_than:1d in:anywhere\`
|
|
1132
|
-
|
|
1133
|
-
Must include spam and trash to detect human curation signals (e.g., moving to/from spam).
|
|
1134
|
-
|
|
1135
|
-
## Email Pipeline Architecture
|
|
1136
|
-
|
|
1137
|
-
### Directory Layout
|
|
1138
|
-
|
|
1139
|
-
\`\`\`
|
|
1140
|
-
{configRoot}/jeeves-core/email-config.json — pipeline configuration (accounts, buckets)
|
|
1141
|
-
{workspace}/../email/threads/{account}/ — canonical email archive (thread.json + per-message JSONs)
|
|
1142
|
-
\`\`\`
|
|
1143
|
-
|
|
1144
|
-
### Data Flow
|
|
1145
|
-
|
|
1146
|
-
1. **Poll** (\`email/poll.ts\`) — searches Gmail for recent threads, classifies, enqueues important ones for metadata fetch
|
|
1147
|
-
2. **Fetch** (\`email/email-fetch.ts\`) — fetches full thread metadata from Gmail, creates/updates \`thread.json\` cache, enqueues for body download
|
|
1148
|
-
3. **Download** (\`email/download.ts\`) — downloads full message bodies, writes per-message JSONs to \`threads/{account}/{threadId}/\`
|
|
1149
|
-
4. **Drain Updates** (\`email/drain-updates.ts\`) — applies label changes and other queued updates back to Gmail
|
|
1150
|
-
5. **Meta synthesis** — jeeves-meta synthesizes email archives into searchable summaries
|
|
1151
|
-
|
|
1152
|
-
### thread.json (Cache Format)
|
|
1153
|
-
|
|
1154
|
-
Each \`threads/{account}/{threadId}/thread.json\` contains:
|
|
1155
|
-
- \`threadId\`, \`account\`, \`subject\`, \`participants\`
|
|
1156
|
-
- \`messages\` — record of \`{ messageId → { from, to, cc, date, internalDateMs, labels, snippet, attachments } }\`
|
|
1157
|
-
- \`provenance\` — label change history
|
|
1158
|
-
- \`cachedAt\`, \`updatedAt\`
|
|
1159
|
-
|
|
1160
|
-
### Per-Message JSONs
|
|
1161
|
-
|
|
1162
|
-
Each \`threads/{account}/{threadId}/{messageId}.json\` contains full message data:
|
|
1163
|
-
- \`messageId\`, \`threadId\`, \`account\`, \`subject\`, \`from\`, \`to\`, \`cc\`
|
|
1164
|
-
- \`date\` (RFC 2822), \`internalDateMs\` (epoch ms)
|
|
1165
|
-
- \`labels\`, \`body\`, \`attachments\`, \`downloadedAt\`
|
|
1166
|
-
`;
|
|
1167
|
-
|
|
1168
|
-
var playbooksContent = `---
|
|
1169
|
-
name: playbooks
|
|
1170
|
-
description: >
|
|
1171
|
-
Reusable operational workflow patterns for the Jeeves platform. Use when asked to
|
|
1172
|
-
set up a daily briefing for a person or team, create standing meeting ops (notes +
|
|
1173
|
-
agenda generation), replicate an existing workflow pattern for a new context, or
|
|
1174
|
-
understand how recurring intelligence/ops workflows are structured. Covers the
|
|
1175
|
-
full stack: content directory, TASK files, standing orders, runner jobs, dispatcher
|
|
1176
|
-
scripts, Slack channel integration, and meta synthesis.
|
|
1177
|
-
---
|
|
1178
|
-
|
|
1179
|
-
# Playbooks
|
|
1180
|
-
|
|
1181
|
-
Proven, replicable operational patterns. Each playbook describes what it does, what
|
|
1182
|
-
infrastructure it needs, and how to instantiate a new instance.
|
|
1183
|
-
|
|
1184
|
-
## Common Infrastructure
|
|
1185
|
-
|
|
1186
|
-
All playbooks share these building blocks:
|
|
1187
|
-
|
|
1188
|
-
| Component | Purpose |
|
|
1189
|
-
|-----------|---------|
|
|
1190
|
-
| **Content directory** | \`{workspace}/../<silo>/<domain>/\` — stores output files, \`.meta/\`, standing orders |
|
|
1191
|
-
| **TASK file** | Markdown prompt that defines the LLM session's entire job |
|
|
1192
|
-
| **Standing orders** | \`standing-orders.md\` — append-only file for persistent stakeholder preferences |
|
|
1193
|
-
| **Dispatcher script** | TypeScript in \`{configRoot}/jeeves-core/scripts/src/\` — reads TASK, spawns worker |
|
|
1194
|
-
| **Runner job** | jeeves-runner job with cron schedule, timezone, and rrstack |
|
|
1195
|
-
| **Slack channel** | Delivery surface — summary posts, quick-link pins, feedback loop |
|
|
1196
|
-
| **Meta entity** | \`.meta/\` directory seeded so jeeves-meta synthesizes context over time |
|
|
1197
|
-
|
|
1198
|
-
### Dispatcher Pattern
|
|
1199
|
-
|
|
1200
|
-
All dispatchers use \`taskFileDispatcher\` from \`dispatchers/lib/task-file-dispatcher.ts\`:
|
|
1201
|
-
|
|
1202
|
-
\`\`\`typescript
|
|
1203
|
-
import { taskFileDispatcher } from '../dispatchers/lib/task-file-dispatcher.js';
|
|
1204
|
-
|
|
1205
|
-
taskFileDispatcher({
|
|
1206
|
-
scriptName: '<silo>/<job-name>',
|
|
1207
|
-
jobId: '<runner-job-id>',
|
|
1208
|
-
taskFile: '<path-to-TASK.md>',
|
|
1209
|
-
timeout: 600,
|
|
1210
|
-
injectDateContext: true,
|
|
1211
|
-
dateTimezone: '<IANA timezone>',
|
|
1212
|
-
});
|
|
1213
|
-
\`\`\`
|
|
1214
|
-
|
|
1215
|
-
\`injectDateContext: true\` prepends an authoritative date line so the LLM knows today's date.
|
|
1216
|
-
|
|
1217
|
-
### Standing Orders Convention
|
|
1218
|
-
|
|
1219
|
-
- Append-only — never modify existing entries
|
|
1220
|
-
- TASK files instruct the LLM to read standing orders at Step 0
|
|
1221
|
-
- TASK files instruct the LLM to append new persistent preferences from channel feedback
|
|
1222
|
-
- Include initial configuration section with participants, timezones, channel rules
|
|
1223
|
-
|
|
1224
|
-
## Available Playbook Patterns
|
|
1225
|
-
|
|
1226
|
-
| Pattern | Description |
|
|
1227
|
-
|---------|-------------|
|
|
1228
|
-
| **Daily Briefing** | Recurring intelligence or action-item report for a stakeholder |
|
|
1229
|
-
| **Standing Meeting Ops** | Post-meeting notes + next-day agenda generation for a recurring meeting |
|
|
1230
|
-
|
|
1231
|
-
## Instantiation Checklist
|
|
1232
|
-
|
|
1233
|
-
When creating a new playbook instance:
|
|
1234
|
-
|
|
1235
|
-
1. Choose the appropriate pattern from the table above
|
|
1236
|
-
2. Create the content directory with \`.meta/\` and \`standing-orders.md\`
|
|
1237
|
-
3. Write the TASK file(s) — adapt from an existing instance. Ensure Step 0 reads feedback from the *delivery channel* (where output is posted), not only a DM
|
|
1238
|
-
4. Write the dispatcher script(s) in \`{configRoot}/jeeves-core/scripts/src/<silo>/\`
|
|
1239
|
-
5. Register the runner job(s) with appropriate cron, timezone, rrstack
|
|
1240
|
-
6. Set up the Slack channel — pin a quick-links message if the pattern calls for it
|
|
1241
|
-
7. Seed \`.meta/\` so meta synthesis begins
|
|
1242
|
-
8. Test with \`--dry-run\` before going live
|
|
1243
|
-
`;
|
|
1244
|
-
|
|
1245
|
-
var slackBotProvisionerContent = `---
|
|
1246
|
-
name: slack-bot-provisioner
|
|
1247
|
-
description: Provision a new Slack bot identity for Clawdbot on a fresh server. Guides through Slack App creation steps, collects tokens, writes local config/env, and verifies connectivity.
|
|
1248
|
-
---
|
|
1249
|
-
|
|
1250
|
-
# Slack bot provisioner (per-bot server)
|
|
1251
|
-
|
|
1252
|
-
Use this when you are setting up **a new Clawdbot instance** that should have **its own Slack bot identity** (one Slack App per bot), and you want a repeatable guided setup.
|
|
1253
|
-
|
|
1254
|
-
This skill assumes:
|
|
1255
|
-
- The user is a Slack workspace admin.
|
|
1256
|
-
- Each bot runs on its own server with its own Gateway config.
|
|
1257
|
-
|
|
1258
|
-
## What can be automated vs not
|
|
1259
|
-
|
|
1260
|
-
**Automated (this skill):**
|
|
1261
|
-
- Create local folders.
|
|
1262
|
-
- Write a \`slack.env\` (bot token, signing secret).
|
|
1263
|
-
- Patch the Clawdbot gateway config to enable Slack for this instance (user approves).
|
|
1264
|
-
- Run a connectivity test (send a message to a channel).
|
|
1265
|
-
|
|
1266
|
-
**Not fully automatable (Slack-side):**
|
|
1267
|
-
- Creating/installing the Slack App and granting scopes (UI/OAuth).
|
|
1268
|
-
- Verifying event subscription URLs (requires public HTTPS endpoint).
|
|
1269
|
-
|
|
1270
|
-
## Recommended mode
|
|
1271
|
-
Start with **outbound-only** (post messages) and expand to event subscriptions later.
|
|
1272
|
-
|
|
1273
|
-
## Quick start
|
|
1274
|
-
|
|
1275
|
-
1) Have the user do the Slack UI steps in \`references/slack-app-checklist.md\`.
|
|
1276
|
-
2) Run the provisioning script:
|
|
1277
|
-
|
|
1278
|
-
- PowerShell:
|
|
1279
|
-
- \`powershell -NoProfile -ExecutionPolicy Bypass -File scripts/provision.ps1\`
|
|
1280
|
-
|
|
1281
|
-
The script will prompt for:
|
|
1282
|
-
- bot name
|
|
1283
|
-
- Slack bot token (\`xoxb-...\`)
|
|
1284
|
-
- Slack signing secret
|
|
1285
|
-
- (optional) test channel id
|
|
1286
|
-
|
|
1287
|
-
## Files
|
|
1288
|
-
- Script: \`scripts/provision.ps1\`
|
|
1289
|
-
- Reference checklist: \`references/slack-app-checklist.md\`
|
|
1290
|
-
- Reference scopes: \`references/scopes.md\`
|
|
1291
|
-
|
|
1292
|
-
## Secrets (best practices)
|
|
1293
|
-
- **Do not** store secrets inside the skill folder (skills are meant to be shareable/publishable).
|
|
1294
|
-
- Store secrets **per-instance** in the Clawdbot runtime directory (recommended):
|
|
1295
|
-
- \`C:\\Users\\Administrator\\.clawdbot\\credentials\\...\`
|
|
1296
|
-
- Prefer environment variables / local credential files loaded by the Gateway/service manager.
|
|
1297
|
-
- Never paste Slack secrets into public chats.
|
|
1298
|
-
|
|
1299
|
-
## Safety notes
|
|
1300
|
-
- Store secrets per-instance. Do not reuse tokens across bot identities.
|
|
1301
|
-
- Avoid bot-to-bot loops: bots should ignore messages from other bots by default.
|
|
1302
|
-
`;
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
* Skill seeding: write all bundled platform skills to the workspace.
|
|
1306
|
-
*
|
|
1307
|
-
* @remarks
|
|
1308
|
-
* Skill files are entirely generated — no user-authored content (Decision 48).
|
|
1309
|
-
* Every installer (core CLI and component plugins) writes them unconditionally.
|
|
1310
|
-
* Content is inlined at build time via `rollup-plugin-md.ts`.
|
|
1311
|
-
*
|
|
1312
|
-
* @module
|
|
1313
|
-
*/
|
|
1314
|
-
/** Map of skill directory name to inlined content. */
|
|
1315
|
-
const BUNDLED_SKILLS = {
|
|
1316
|
-
jeeves: jeevesContent,
|
|
1317
|
-
coding: codingContent,
|
|
1318
|
-
'slack-bot-provisioner': slackBotProvisionerContent,
|
|
1319
|
-
operations: operationsContent,
|
|
1320
|
-
playbooks: playbooksContent,
|
|
1321
|
-
};
|
|
1322
|
-
/**
|
|
1323
|
-
* Seed all bundled platform skills into the workspace.
|
|
1324
|
-
*
|
|
1325
|
-
* @remarks
|
|
1326
|
-
* Writes each skill to `{workspace}/skills/{name}/SKILL.md`, creating
|
|
1327
|
-
* directories as needed. Overwrites existing content unconditionally.
|
|
1328
|
-
*
|
|
1329
|
-
* @param workspacePath - Workspace root directory.
|
|
1330
|
-
*/
|
|
1331
|
-
function seedSkills(workspacePath) {
|
|
1332
|
-
for (const [name, content] of Object.entries(BUNDLED_SKILLS)) {
|
|
1333
|
-
const skillDir = join(workspacePath, SKILLS_DIR, name);
|
|
1334
|
-
if (!existsSync(skillDir)) {
|
|
1335
|
-
mkdirSync(skillDir, { recursive: true });
|
|
1336
|
-
}
|
|
1337
|
-
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
/**
|
|
1342
|
-
* Zod schema for the Jeeves component descriptor.
|
|
1343
|
-
*
|
|
1344
|
-
* @remarks
|
|
1345
|
-
* The descriptor replaces the v0.4.0 `JeevesComponent` interface with a
|
|
1346
|
-
* Zod-first approach. The TypeScript type is inferred via `z.infer<>`.
|
|
1347
|
-
* Validates at parse time: prime interval, callable functions.
|
|
1348
|
-
*/
|
|
1349
|
-
/**
|
|
1350
|
-
* Check whether a number is prime.
|
|
1351
|
-
*
|
|
1352
|
-
* @param n - Number to check.
|
|
1353
|
-
* @returns `true` if n is prime.
|
|
1354
|
-
*/
|
|
1355
|
-
function isPrime(n) {
|
|
1356
|
-
if (n < 2)
|
|
1357
|
-
return false;
|
|
1358
|
-
if (n === 2)
|
|
1359
|
-
return true;
|
|
1360
|
-
if (n % 2 === 0)
|
|
1361
|
-
return false;
|
|
1362
|
-
for (let i = 3; i * i <= n; i += 2) {
|
|
1363
|
-
if (n % i === 0)
|
|
1364
|
-
return false;
|
|
1365
|
-
}
|
|
1366
|
-
return true;
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Zod schema for the Jeeves component descriptor.
|
|
1370
|
-
*
|
|
1371
|
-
* @remarks
|
|
1372
|
-
* Single source of truth for what a component must provide.
|
|
1373
|
-
* Factories consume this descriptor to produce CLI commands,
|
|
1374
|
-
* plugin tools, and HTTP handlers.
|
|
1375
|
-
*/
|
|
1376
|
-
z.object({
|
|
1377
|
-
/** Component name (e.g., 'watcher', 'runner', 'server', 'meta'). */
|
|
1378
|
-
name: z.string().min(1, 'name must be a non-empty string'),
|
|
1379
|
-
/** Component version (from package.json). */
|
|
1380
|
-
version: z.string().min(1, 'version must be a non-empty string'),
|
|
1381
|
-
/** npm package name for the service. */
|
|
1382
|
-
servicePackage: z.string().min(1),
|
|
1383
|
-
/** npm package name for the plugin. */
|
|
1384
|
-
pluginPackage: z.string().min(1),
|
|
1385
|
-
/** System service name. Defaults to `jeeves-${name}` when not provided. */
|
|
1386
|
-
serviceName: z.string().min(1).optional(),
|
|
1387
|
-
/** Default port for the service's HTTP API. */
|
|
1388
|
-
defaultPort: z.number().int().positive(),
|
|
1389
|
-
/** Zod schema for validating config files. */
|
|
1390
|
-
configSchema: z.custom((val) => val !== null &&
|
|
1391
|
-
typeof val === 'object' &&
|
|
1392
|
-
typeof val.parse === 'function', { message: 'configSchema must be a Zod schema' }),
|
|
1393
|
-
/** Config file name (e.g., 'jeeves-watcher.config.json'). */
|
|
1394
|
-
configFileName: z.string().min(1),
|
|
1395
|
-
/** Returns a default config object for `init`. */
|
|
1396
|
-
initTemplate: z.function({
|
|
1397
|
-
input: [],
|
|
1398
|
-
output: z.record(z.string(), z.unknown()),
|
|
1399
|
-
}),
|
|
1400
|
-
/**
|
|
1401
|
-
* Service-side callback after config apply. Receives the merged,
|
|
1402
|
-
* validated config (not the raw patch). Optional — if omitted,
|
|
1403
|
-
* write-only (service picks up changes on restart).
|
|
1404
|
-
*/
|
|
1405
|
-
onConfigApply: z
|
|
1406
|
-
.function({
|
|
1407
|
-
input: [z.record(z.string(), z.unknown())],
|
|
1408
|
-
output: z.promise(z.void()),
|
|
1409
|
-
})
|
|
1410
|
-
.optional(),
|
|
1411
|
-
/**
|
|
1412
|
-
* Custom merge function for config apply. Receives the existing config
|
|
1413
|
-
* and the patch, returns the merged result. Optional — if omitted,
|
|
1414
|
-
* the default deep-merge (object-recursive, array-replacing) is used.
|
|
1415
|
-
*
|
|
1416
|
-
* Use this to implement domain-specific merge strategies such as
|
|
1417
|
-
* name-based array merging for inference rules.
|
|
1418
|
-
*/
|
|
1419
|
-
customMerge: z
|
|
1420
|
-
.function({
|
|
1421
|
-
input: [
|
|
1422
|
-
z.record(z.string(), z.unknown()),
|
|
1423
|
-
z.record(z.string(), z.unknown()),
|
|
1424
|
-
],
|
|
1425
|
-
output: z.record(z.string(), z.unknown()),
|
|
1426
|
-
})
|
|
1427
|
-
.optional(),
|
|
1428
|
-
/**
|
|
1429
|
-
* Returns command + args for launching the service process.
|
|
1430
|
-
* Consumed by `service install`.
|
|
1431
|
-
*/
|
|
1432
|
-
startCommand: z.function({
|
|
1433
|
-
input: [z.string()],
|
|
1434
|
-
output: z.array(z.string()),
|
|
1435
|
-
}),
|
|
1436
|
-
/** In-process service entry point for the CLI `start` command. */
|
|
1437
|
-
run: z.function({
|
|
1438
|
-
input: [z.string()],
|
|
1439
|
-
output: z.promise(z.void()),
|
|
1440
|
-
}),
|
|
1441
|
-
/** TOOLS.md section name (e.g., 'Watcher'). */
|
|
1442
|
-
sectionId: z.string().min(1, 'sectionId must be a non-empty string'),
|
|
1443
|
-
/** Refresh interval in seconds (must be a prime number). */
|
|
1444
|
-
refreshIntervalSeconds: z.number().int().positive().refine(isPrime, {
|
|
1445
|
-
message: 'refreshIntervalSeconds must be a prime number',
|
|
1446
|
-
}),
|
|
1447
|
-
/** Produce the component's TOOLS.md section content. */
|
|
1448
|
-
generateToolsContent: z.function({ input: [], output: z.string() }),
|
|
1449
|
-
/** Component dependencies for HEARTBEAT alert suppression. */
|
|
1450
|
-
dependencies: z
|
|
1451
|
-
.object({
|
|
1452
|
-
/** Components that must be healthy for this component to function. */
|
|
1453
|
-
hard: z.array(z.string()),
|
|
1454
|
-
/** Components that improve behavior but are not strictly required. */
|
|
1455
|
-
soft: z.array(z.string()),
|
|
1456
|
-
})
|
|
1457
|
-
.optional(),
|
|
1458
|
-
/** Extension point: add custom CLI commands to the service CLI. */
|
|
1459
|
-
customCliCommands: z
|
|
1460
|
-
.function({ input: [z.custom()], output: z.void() })
|
|
1461
|
-
.optional(),
|
|
1462
|
-
/** Extension point: return additional plugin tool descriptors. */
|
|
1463
|
-
customPluginTools: z
|
|
1464
|
-
.function({ input: [z.custom()], output: z.array(z.unknown()) })
|
|
1465
|
-
.optional(),
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
/**
|
|
1469
|
-
* Resolve the package root directory from a module's `import.meta.url`.
|
|
1470
|
-
*
|
|
1471
|
-
* @module
|
|
1472
|
-
*/
|
|
1473
|
-
/**
|
|
1474
|
-
* Get the nearest package root directory relative to the calling module URL.
|
|
1475
|
-
*
|
|
1476
|
-
* @param importMetaUrl - The `import.meta.url` of the calling module.
|
|
1477
|
-
* @returns The absolute package root path, or `undefined` on any error.
|
|
1478
|
-
*/
|
|
1479
|
-
function getPackageRoot(importMetaUrl) {
|
|
1480
|
-
try {
|
|
1481
|
-
return packageDirectorySync({ cwd: fileURLToPath(importMetaUrl) });
|
|
1482
|
-
}
|
|
1483
|
-
catch {
|
|
1484
|
-
return undefined;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
/**
|
|
1489
|
-
* OpenClaw configuration helpers for plugin CLI installers.
|
|
1490
|
-
*
|
|
1491
|
-
* @remarks
|
|
1492
|
-
* Provides resolution of OpenClaw home directory and config file path,
|
|
1493
|
-
* plus idempotent config patching for plugin install/uninstall.
|
|
1494
|
-
*/
|
|
1495
|
-
/**
|
|
1496
|
-
* Resolve the OpenClaw home directory.
|
|
1497
|
-
*
|
|
1498
|
-
* @remarks
|
|
1499
|
-
* Resolution order:
|
|
1500
|
-
* 1. `OPENCLAW_CONFIG` env var → dirname of the config file path
|
|
1501
|
-
* 2. `OPENCLAW_HOME` env var → resolved path
|
|
1502
|
-
* 3. Default: `~/.openclaw`
|
|
1503
|
-
*
|
|
1504
|
-
* @returns Absolute path to the OpenClaw home directory.
|
|
1505
|
-
*/
|
|
1506
|
-
function resolveOpenClawHome() {
|
|
1507
|
-
if (process.env.OPENCLAW_CONFIG) {
|
|
1508
|
-
return dirname(resolve(process.env.OPENCLAW_CONFIG));
|
|
1509
|
-
}
|
|
1510
|
-
if (process.env.OPENCLAW_HOME) {
|
|
1511
|
-
return resolve(process.env.OPENCLAW_HOME);
|
|
1512
|
-
}
|
|
1513
|
-
return join(homedir(), '.openclaw');
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Resolve the OpenClaw config file path.
|
|
1517
|
-
*
|
|
1518
|
-
* @remarks
|
|
1519
|
-
* If `OPENCLAW_CONFIG` is set, uses that directly.
|
|
1520
|
-
* Otherwise defaults to `{home}/openclaw.json`.
|
|
1521
|
-
*
|
|
1522
|
-
* @param home - The OpenClaw home directory.
|
|
1523
|
-
* @returns Absolute path to the config file.
|
|
1524
|
-
*/
|
|
1525
|
-
function resolveConfigPath(home) {
|
|
1526
|
-
if (process.env.OPENCLAW_CONFIG) {
|
|
1527
|
-
return resolve(process.env.OPENCLAW_CONFIG);
|
|
1528
|
-
}
|
|
1529
|
-
return join(home, 'openclaw.json');
|
|
1530
|
-
}
|
|
1531
|
-
/**
|
|
1532
|
-
* Patch an allowlist array: add or remove the plugin ID.
|
|
1533
|
-
*
|
|
1534
|
-
* @returns A log message if a change was made, or undefined.
|
|
1535
|
-
*/
|
|
1536
|
-
function patchAllowList(parent, key, label, pluginId, mode) {
|
|
1537
|
-
if (mode === 'add') {
|
|
1538
|
-
if (!Array.isArray(parent[key])) {
|
|
1539
|
-
parent[key] = [pluginId];
|
|
1540
|
-
return `Created ${label} with "${pluginId}"`;
|
|
1541
|
-
}
|
|
1542
|
-
const list = parent[key];
|
|
1543
|
-
if (!list.includes(pluginId)) {
|
|
1544
|
-
list.push(pluginId);
|
|
1545
|
-
return `Added "${pluginId}" to ${label}`;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
else {
|
|
1549
|
-
if (!Array.isArray(parent[key]))
|
|
1550
|
-
return undefined;
|
|
1551
|
-
const list = parent[key];
|
|
1552
|
-
const filtered = list.filter((id) => id !== pluginId);
|
|
1553
|
-
if (filtered.length !== list.length) {
|
|
1554
|
-
parent[key] = filtered;
|
|
1555
|
-
return `Removed "${pluginId}" from ${label}`;
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return undefined;
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* Patch an OpenClaw config for plugin install or uninstall.
|
|
1562
|
-
*
|
|
1563
|
-
* @remarks
|
|
1564
|
-
* Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
|
|
1565
|
-
* and `tools.alsoAllow`.
|
|
1566
|
-
* Idempotent: adding twice produces no duplicates; removing when absent
|
|
1567
|
-
* produces no errors.
|
|
1568
|
-
*
|
|
1569
|
-
* @param config - The parsed OpenClaw config object (mutated in place).
|
|
1570
|
-
* @param pluginId - The plugin identifier.
|
|
1571
|
-
* @param mode - Whether to add or remove the plugin.
|
|
1572
|
-
* @param installRecord - Install provenance record (required when mode is 'add').
|
|
1573
|
-
* @returns Array of log messages describing changes made.
|
|
1574
|
-
*/
|
|
1575
|
-
function patchConfig(config, pluginId, mode, installRecord) {
|
|
1576
|
-
const messages = [];
|
|
1577
|
-
// Ensure plugins section
|
|
1578
|
-
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
1579
|
-
config.plugins = {};
|
|
1580
|
-
}
|
|
1581
|
-
const plugins = config.plugins;
|
|
1582
|
-
// plugins.entries
|
|
1583
|
-
if (!plugins.entries || typeof plugins.entries !== 'object') {
|
|
1584
|
-
plugins.entries = {};
|
|
1585
|
-
}
|
|
1586
|
-
const entries = plugins.entries;
|
|
1587
|
-
if (mode === 'add') {
|
|
1588
|
-
if (!entries[pluginId]) {
|
|
1589
|
-
entries[pluginId] = { enabled: true };
|
|
1590
|
-
messages.push(`Added "${pluginId}" to plugins.entries`);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
else if (pluginId in entries) {
|
|
1594
|
-
Reflect.deleteProperty(entries, pluginId);
|
|
1595
|
-
messages.push(`Removed "${pluginId}" from plugins.entries`);
|
|
1596
|
-
}
|
|
1597
|
-
// plugins.installs
|
|
1598
|
-
if (!plugins.installs || typeof plugins.installs !== 'object') {
|
|
1599
|
-
plugins.installs = {};
|
|
1600
|
-
}
|
|
1601
|
-
const installs = plugins.installs;
|
|
1602
|
-
if (mode === 'add' && installRecord) {
|
|
1603
|
-
installs[pluginId] = {
|
|
1604
|
-
source: 'path',
|
|
1605
|
-
installPath: installRecord.installPath,
|
|
1606
|
-
version: installRecord.version,
|
|
1607
|
-
installedAt: installRecord.installedAt ?? new Date().toISOString(),
|
|
1608
|
-
};
|
|
1609
|
-
messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
|
|
1610
|
-
}
|
|
1611
|
-
else if (mode === 'remove' && pluginId in installs) {
|
|
1612
|
-
Reflect.deleteProperty(installs, pluginId);
|
|
1613
|
-
messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
|
|
1614
|
-
}
|
|
1615
|
-
// tools.alsoAllow
|
|
1616
|
-
if (!config.tools || typeof config.tools !== 'object') {
|
|
1617
|
-
config.tools = {};
|
|
1618
|
-
}
|
|
1619
|
-
const tools = config.tools;
|
|
1620
|
-
const toolAlsoAllow = patchAllowList(tools, 'alsoAllow', 'tools.alsoAllow', pluginId, mode);
|
|
1621
|
-
if (toolAlsoAllow)
|
|
1622
|
-
messages.push(toolAlsoAllow);
|
|
1623
|
-
return messages;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
/**
|
|
1627
|
-
* Internal helpers for the plugin installer CLI.
|
|
1628
|
-
*
|
|
1629
|
-
* @module
|
|
1630
|
-
*/
|
|
1631
|
-
/**
|
|
1632
|
-
* Derive a component name from a plugin ID.
|
|
1633
|
-
*
|
|
1634
|
-
* @remarks
|
|
1635
|
-
* Strips `jeeves-` prefix and `-openclaw` suffix.
|
|
1636
|
-
*
|
|
1637
|
-
* @param pluginId - The plugin identifier.
|
|
1638
|
-
* @returns Component short name.
|
|
1639
|
-
*/
|
|
1640
|
-
function deriveComponentName(pluginId) {
|
|
1641
|
-
return pluginId.replace(/^jeeves-/, '').replace(/-openclaw$/, '');
|
|
1642
|
-
}
|
|
1643
|
-
/**
|
|
1644
|
-
* Copy all files from source directory to destination, recursively.
|
|
1645
|
-
*
|
|
1646
|
-
* @param srcDir - Source directory.
|
|
1647
|
-
* @param destDir - Destination directory.
|
|
1648
|
-
*/
|
|
1649
|
-
function copyDistFiles(srcDir, destDir) {
|
|
1650
|
-
mkdirSync(destDir, { recursive: true });
|
|
1651
|
-
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
1652
|
-
for (const entry of entries) {
|
|
1653
|
-
const srcPath = join(srcDir, entry.name);
|
|
1654
|
-
const destPath = join(destDir, entry.name);
|
|
1655
|
-
if (entry.isDirectory()) {
|
|
1656
|
-
copyDistFiles(srcPath, destPath);
|
|
1657
|
-
}
|
|
1658
|
-
else {
|
|
1659
|
-
copyFileSync(srcPath, destPath);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Read and parse a JSON file, returning an empty object if not found.
|
|
1665
|
-
*
|
|
1666
|
-
* @param filePath - Path to the JSON file.
|
|
1667
|
-
* @returns Parsed object.
|
|
1668
|
-
*/
|
|
1669
|
-
function readJsonFile(filePath) {
|
|
1670
|
-
try {
|
|
1671
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
1672
|
-
return JSON.parse(raw);
|
|
1673
|
-
}
|
|
1674
|
-
catch {
|
|
1675
|
-
return {};
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* Factory for the standard `-openclaw` plugin installer CLI.
|
|
1681
|
-
*
|
|
1682
|
-
* @module
|
|
1683
|
-
*/
|
|
1684
|
-
/**
|
|
1685
|
-
* Create a standard plugin installer CLI program.
|
|
1686
|
-
*
|
|
1687
|
-
* @param options - Plugin CLI configuration.
|
|
1688
|
-
* @returns A Commander program ready for `.parse()`.
|
|
1689
|
-
*/
|
|
1690
|
-
function createPluginCli(options) {
|
|
1691
|
-
const { pluginId, importMetaUrl, pluginPackage, configRoot = 'j:/config', } = options;
|
|
1692
|
-
const componentName = options.componentName ?? deriveComponentName(pluginId);
|
|
1693
|
-
const pkgRoot = getPackageRoot(importMetaUrl);
|
|
1694
|
-
if (!pkgRoot) {
|
|
1695
|
-
throw new Error(`Unable to resolve package root for plugin CLI: ${pluginPackage}`);
|
|
1696
|
-
}
|
|
1697
|
-
const distDir = join(pkgRoot, 'dist');
|
|
1698
|
-
const program = new Command()
|
|
1699
|
-
.name(pluginPackage)
|
|
1700
|
-
.description(`Jeeves ${componentName} plugin installer`);
|
|
1701
|
-
program
|
|
1702
|
-
.command('install')
|
|
1703
|
-
.description(`Install the ${componentName} plugin`)
|
|
1704
|
-
.option('--memory', 'Claim a memory slot for this plugin')
|
|
1705
|
-
.option('-w, --workspace <path>', 'Workspace root path')
|
|
1706
|
-
.option('-c, --config-root <path>', 'Platform config root path', configRoot)
|
|
1707
|
-
.action((opts) => {
|
|
1708
|
-
const openClawHome = resolveOpenClawHome();
|
|
1709
|
-
const configPath = resolveConfigPath(openClawHome);
|
|
1710
|
-
// 1. Copy dist to extensions
|
|
1711
|
-
const extensionsDir = join(openClawHome, 'extensions', pluginId);
|
|
1712
|
-
if (!existsSync(distDir))
|
|
1713
|
-
throw new Error(`Plugin dist directory not found: ${distDir}`);
|
|
1714
|
-
console.log(`Copying dist to ${extensionsDir}...`);
|
|
1715
|
-
copyDistFiles(distDir, join(extensionsDir, 'dist'));
|
|
1716
|
-
// Copy package.json and openclaw.plugin.json from package root
|
|
1717
|
-
for (const file of ['package.json', 'openclaw.plugin.json']) {
|
|
1718
|
-
const src = join(pkgRoot, file);
|
|
1719
|
-
if (existsSync(src)) {
|
|
1720
|
-
copyFileSync(src, join(extensionsDir, file));
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
console.log(' ✓ Dist files copied');
|
|
1724
|
-
// 1b. Install production dependencies
|
|
1725
|
-
console.log('Installing dependencies...');
|
|
1726
|
-
try {
|
|
1727
|
-
execSync('npm install --omit=dev', {
|
|
1728
|
-
cwd: extensionsDir,
|
|
1729
|
-
stdio: 'pipe',
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
catch (err) {
|
|
1733
|
-
throw new Error(`npm install failed in ${extensionsDir}`, {
|
|
1734
|
-
cause: err,
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
console.log(' ✓ Dependencies installed');
|
|
1738
|
-
// 2. Patch openclaw.json
|
|
1739
|
-
console.log('Patching OpenClaw config...');
|
|
1740
|
-
const config = readJsonFile(configPath);
|
|
1741
|
-
const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
|
|
1742
|
-
let pluginVersionForRecord;
|
|
1743
|
-
try {
|
|
1744
|
-
const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
|
|
1745
|
-
pluginVersionForRecord =
|
|
1746
|
-
typeof pkgJsonForRecord.version === 'string'
|
|
1747
|
-
? pkgJsonForRecord.version
|
|
1748
|
-
: undefined;
|
|
1749
|
-
}
|
|
1750
|
-
catch {
|
|
1751
|
-
// best-effort: version may not be available yet
|
|
1752
|
-
}
|
|
1753
|
-
const messages = patchConfig(config, pluginId, 'add', {
|
|
1754
|
-
installPath: extensionsDir,
|
|
1755
|
-
version: pluginVersionForRecord,
|
|
1756
|
-
});
|
|
1757
|
-
// 3. Memory slot claim
|
|
1758
|
-
if (opts.memory) {
|
|
1759
|
-
if (!config.agents || typeof config.agents !== 'object') {
|
|
1760
|
-
config.agents = {};
|
|
1761
|
-
}
|
|
1762
|
-
const agents = config.agents;
|
|
1763
|
-
if (!agents.defaults || typeof agents.defaults !== 'object') {
|
|
1764
|
-
agents.defaults = {};
|
|
1765
|
-
}
|
|
1766
|
-
const defaults = agents.defaults;
|
|
1767
|
-
if (!defaults.memory || typeof defaults.memory !== 'object') {
|
|
1768
|
-
defaults.memory = {};
|
|
1769
|
-
}
|
|
1770
|
-
const memory = defaults.memory;
|
|
1771
|
-
if (!memory[componentName]) {
|
|
1772
|
-
memory[componentName] = {};
|
|
1773
|
-
messages.push(`Claimed memory slot for "${componentName}"`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
1777
|
-
for (const msg of messages) {
|
|
1778
|
-
console.log(` ✓ ${msg}`);
|
|
1779
|
-
}
|
|
1780
|
-
// 4. Write initial HEARTBEAT entry and seed jeeves skill
|
|
1781
|
-
try {
|
|
1782
|
-
const cfgRoot = opts.configRoot;
|
|
1783
|
-
const ag = config.agents;
|
|
1784
|
-
const defs = ag?.defaults;
|
|
1785
|
-
const ws = opts.workspace ?? defs?.workspace;
|
|
1786
|
-
if (ws) {
|
|
1787
|
-
init({ workspacePath: ws, configRoot: cfgRoot });
|
|
1788
|
-
const heartbeatPath = join(ws, WORKSPACE_FILES.heartbeat);
|
|
1789
|
-
try {
|
|
1790
|
-
const existing = existsSync(heartbeatPath)
|
|
1791
|
-
? readFileSync(heartbeatPath, 'utf-8')
|
|
1792
|
-
: '';
|
|
1793
|
-
const parsed = parseHeartbeat(existing);
|
|
1794
|
-
const fullName = `jeeves-${componentName}`;
|
|
1795
|
-
const hasEntry = parsed.entries.some((e) => e.name === fullName);
|
|
1796
|
-
if (!hasEntry) {
|
|
1797
|
-
parsed.entries.push({
|
|
1798
|
-
name: fullName,
|
|
1799
|
-
declined: false,
|
|
1800
|
-
content: `- Plugin installed. Awaiting service configuration.`,
|
|
1801
|
-
});
|
|
1802
|
-
const section = buildHeartbeatSection(parsed.entries);
|
|
1803
|
-
atomicWrite(heartbeatPath, section);
|
|
1804
|
-
console.log(' ✓ HEARTBEAT entry written');
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
catch {
|
|
1808
|
-
console.log(' ⚠ Could not write HEARTBEAT entry');
|
|
1809
|
-
}
|
|
1810
|
-
try {
|
|
1811
|
-
seedSkills(ws);
|
|
1812
|
-
console.log(' ✓ Platform skills seeded');
|
|
1813
|
-
}
|
|
1814
|
-
catch {
|
|
1815
|
-
console.log(' ⚠ Could not seed platform skills');
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
catch {
|
|
1820
|
-
// HEARTBEAT + skill seeding are best-effort during install
|
|
1821
|
-
}
|
|
1822
|
-
// 5. Write component version
|
|
1823
|
-
try {
|
|
1824
|
-
init({
|
|
1825
|
-
workspacePath: opts.workspace ?? '.',
|
|
1826
|
-
configRoot: opts.configRoot,
|
|
1827
|
-
});
|
|
1828
|
-
const pkgJsonPath = join(extensionsDir, 'package.json');
|
|
1829
|
-
const pkgJson = readJsonFile(pkgJsonPath);
|
|
1830
|
-
const pluginVersion = typeof pkgJson.version === 'string' ? pkgJson.version : undefined;
|
|
1831
|
-
writeComponentVersion(getCoreConfigDir(), {
|
|
1832
|
-
componentName,
|
|
1833
|
-
pluginPackage,
|
|
1834
|
-
pluginVersion,
|
|
1835
|
-
});
|
|
1836
|
-
console.log(' ✓ Component version written');
|
|
1837
|
-
}
|
|
1838
|
-
catch {
|
|
1839
|
-
console.log(' ⚠ Could not write component version');
|
|
1840
|
-
}
|
|
1841
|
-
console.log();
|
|
1842
|
-
console.log(`✅ ${pluginPackage} installed.`);
|
|
1843
|
-
});
|
|
1844
|
-
program
|
|
1845
|
-
.command('uninstall')
|
|
1846
|
-
.description(`Uninstall the ${componentName} plugin`)
|
|
1847
|
-
.option('-w, --workspace <path>', 'Workspace root path')
|
|
1848
|
-
.option('-c, --config-root <path>', 'Platform config root path', configRoot)
|
|
1849
|
-
.action(async (opts) => {
|
|
1850
|
-
const openClawHome = resolveOpenClawHome();
|
|
1851
|
-
const cfgPath = resolveConfigPath(openClawHome);
|
|
1852
|
-
// 1. Remove from extensions
|
|
1853
|
-
const extensionsDir = join(openClawHome, 'extensions', pluginId);
|
|
1854
|
-
if (existsSync(extensionsDir)) {
|
|
1855
|
-
rmSync(extensionsDir, { recursive: true, force: true });
|
|
1856
|
-
console.log(' ✓ Extension files removed');
|
|
1857
|
-
}
|
|
1858
|
-
// 2. Unpatch openclaw.json
|
|
1859
|
-
if (existsSync(cfgPath)) {
|
|
1860
|
-
const config = readJsonFile(cfgPath);
|
|
1861
|
-
const messages = patchConfig(config, pluginId, 'remove');
|
|
1862
|
-
writeFileSync(cfgPath, JSON.stringify(config, null, 2) + '\n');
|
|
1863
|
-
for (const msg of messages) {
|
|
1864
|
-
console.log(` ✓ ${msg}`);
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
// 3. Remove TOOLS.md section
|
|
1868
|
-
try {
|
|
1869
|
-
const ws = opts.workspace;
|
|
1870
|
-
if (ws) {
|
|
1871
|
-
init({ workspacePath: ws, configRoot: opts.configRoot });
|
|
1872
|
-
const sectionId = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
1873
|
-
const toolsPath = join(ws, WORKSPACE_FILES.tools);
|
|
1874
|
-
if (existsSync(toolsPath)) {
|
|
1875
|
-
await removeManagedSection(toolsPath, {
|
|
1876
|
-
sectionId,
|
|
1877
|
-
markers: TOOLS_MARKERS,
|
|
1878
|
-
});
|
|
1879
|
-
console.log(' ✓ TOOLS.md section removed');
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
catch {
|
|
1884
|
-
console.log(' ⚠ Could not remove TOOLS.md section');
|
|
1885
|
-
}
|
|
1886
|
-
// 4. Remove component-versions.json entry
|
|
1887
|
-
try {
|
|
1888
|
-
init({
|
|
1889
|
-
workspacePath: opts.workspace ?? '.',
|
|
1890
|
-
configRoot: opts.configRoot,
|
|
1891
|
-
});
|
|
1892
|
-
removeComponentVersion(getCoreConfigDir(), componentName);
|
|
1893
|
-
console.log(' ✓ Component version entry removed');
|
|
1894
|
-
}
|
|
1895
|
-
catch {
|
|
1896
|
-
console.log(' ⚠ Could not remove component version entry');
|
|
1897
|
-
}
|
|
1898
|
-
console.log();
|
|
1899
|
-
console.log(`✅ ${pluginPackage} uninstalled.`);
|
|
1900
|
-
});
|
|
1901
|
-
return program;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
export { createPluginCli };
|