@karmaniverous/jeeves 0.1.0
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/LICENSE +28 -0
- package/README.md +141 -0
- package/content/agents-section.md +190 -0
- package/content/soul-section.md +91 -0
- package/content/templates/spec-to-code-guide.md +250 -0
- package/content/templates/spec.md +171 -0
- package/content/tools-platform.md +78 -0
- package/dist/cli/jeeves/index.js +1341 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.js +1232 -0
- package/package.json +141 -0
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import require$$0 from 'commander';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, cpSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import Handlebars from 'handlebars';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { lock } from 'proper-lockfile';
|
|
12
|
+
import { gte } from 'semver';
|
|
13
|
+
|
|
14
|
+
function getDefaultExportFromCjs (x) {
|
|
15
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var extraTypings = {exports: {}};
|
|
19
|
+
|
|
20
|
+
var hasRequiredExtraTypings;
|
|
21
|
+
|
|
22
|
+
function requireExtraTypings () {
|
|
23
|
+
if (hasRequiredExtraTypings) return extraTypings.exports;
|
|
24
|
+
hasRequiredExtraTypings = 1;
|
|
25
|
+
(function (module, exports$1) {
|
|
26
|
+
const commander = require$$0;
|
|
27
|
+
|
|
28
|
+
exports$1 = module.exports = {};
|
|
29
|
+
|
|
30
|
+
// Return a different global program than commander,
|
|
31
|
+
// and don't also return it as default export.
|
|
32
|
+
exports$1.program = new commander.Command();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Expose classes. The FooT versions are just types, so return Commander original implementations!
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
exports$1.Argument = commander.Argument;
|
|
39
|
+
exports$1.Command = commander.Command;
|
|
40
|
+
exports$1.CommanderError = commander.CommanderError;
|
|
41
|
+
exports$1.Help = commander.Help;
|
|
42
|
+
exports$1.InvalidArgumentError = commander.InvalidArgumentError;
|
|
43
|
+
exports$1.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
|
|
44
|
+
exports$1.Option = commander.Option;
|
|
45
|
+
|
|
46
|
+
exports$1.createCommand = (name) => new commander.Command(name);
|
|
47
|
+
exports$1.createOption = (flags, description) =>
|
|
48
|
+
new commander.Option(flags, description);
|
|
49
|
+
exports$1.createArgument = (name, description) =>
|
|
50
|
+
new commander.Argument(name, description);
|
|
51
|
+
} (extraTypings, extraTypings.exports));
|
|
52
|
+
return extraTypings.exports;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var extraTypingsExports = requireExtraTypings();
|
|
56
|
+
var extraTypingsCommander = /*@__PURE__*/getDefaultExportFromCjs(extraTypingsExports);
|
|
57
|
+
|
|
58
|
+
// wrapper to provide named exports for ESM.
|
|
59
|
+
const {
|
|
60
|
+
program,
|
|
61
|
+
createCommand,
|
|
62
|
+
createArgument,
|
|
63
|
+
createOption,
|
|
64
|
+
CommanderError,
|
|
65
|
+
InvalidArgumentError,
|
|
66
|
+
InvalidOptionArgumentError, // deprecated old name
|
|
67
|
+
Command,
|
|
68
|
+
Argument,
|
|
69
|
+
Option,
|
|
70
|
+
Help,
|
|
71
|
+
} = extraTypingsCommander;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Comment markers for managed content blocks.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Managed content in TOOLS.md, SOUL.md, and AGENTS.md is enclosed
|
|
78
|
+
* in HTML comment markers. Content between markers is refreshed
|
|
79
|
+
* atomically on each writer cycle. User content outside the markers
|
|
80
|
+
* is never touched.
|
|
81
|
+
*/
|
|
82
|
+
/** Default markers for TOOLS.md managed block. */
|
|
83
|
+
const TOOLS_MARKERS = {
|
|
84
|
+
/** BEGIN comment marker text. */
|
|
85
|
+
begin: 'BEGIN JEEVES PLATFORM TOOLS — DO NOT EDIT THIS SECTION',
|
|
86
|
+
/** END comment marker text. */
|
|
87
|
+
end: 'END JEEVES PLATFORM TOOLS',
|
|
88
|
+
/** H1 title prepended in section mode. */
|
|
89
|
+
title: 'Jeeves Platform Tools',
|
|
90
|
+
};
|
|
91
|
+
/** Default markers for SOUL.md managed block. */
|
|
92
|
+
const SOUL_MARKERS = {
|
|
93
|
+
/** BEGIN comment marker text. */
|
|
94
|
+
begin: 'BEGIN JEEVES SOUL — DO NOT EDIT THIS SECTION',
|
|
95
|
+
/** END comment marker text. */
|
|
96
|
+
end: 'END JEEVES SOUL',
|
|
97
|
+
};
|
|
98
|
+
/** Default markers for AGENTS.md managed block. */
|
|
99
|
+
const AGENTS_MARKERS = {
|
|
100
|
+
/** BEGIN comment marker text. */
|
|
101
|
+
begin: 'BEGIN JEEVES AGENTS — DO NOT EDIT THIS SECTION',
|
|
102
|
+
/** END comment marker text. */
|
|
103
|
+
end: 'END JEEVES AGENTS',
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Regex pattern to extract version stamp from a BEGIN marker comment.
|
|
107
|
+
*
|
|
108
|
+
* @remarks
|
|
109
|
+
* Format: `\<!-- BEGIN MARKER | core:X.Y.Z | ISO-TIMESTAMP --\>`
|
|
110
|
+
* Captures: [1] marker text, [2] version, [3] timestamp
|
|
111
|
+
*/
|
|
112
|
+
const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
|
|
113
|
+
/** Staleness threshold for version-stamp convergence in milliseconds. */
|
|
114
|
+
const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
|
|
115
|
+
/** Warning text prepended inside managed block when cleanup is needed. */
|
|
116
|
+
const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Directory and file path conventions for the Jeeves platform.
|
|
120
|
+
*/
|
|
121
|
+
/** Core config directory name within the config root. */
|
|
122
|
+
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
123
|
+
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
124
|
+
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
125
|
+
/** Default workspace file names. */
|
|
126
|
+
const WORKSPACE_FILES = {
|
|
127
|
+
/** TOOLS.md — live platform state and component sections. */
|
|
128
|
+
tools: 'TOOLS.md',
|
|
129
|
+
/** SOUL.md — professional discipline and behavioral foundations. */
|
|
130
|
+
soul: 'SOUL.md',
|
|
131
|
+
/** AGENTS.md — operational protocols and memory architecture. */
|
|
132
|
+
agents: 'AGENTS.md',
|
|
133
|
+
};
|
|
134
|
+
/** Templates directory name within core config. */
|
|
135
|
+
const TEMPLATES_DIR = 'templates';
|
|
136
|
+
/** Registry cache file name. */
|
|
137
|
+
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
138
|
+
/** Core config file name. */
|
|
139
|
+
const CONFIG_FILE = 'config.json';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Default port assignments for Jeeves platform services.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* Each port number is a historical reference:
|
|
146
|
+
* - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
|
|
147
|
+
* - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
|
|
148
|
+
* - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
|
|
149
|
+
* - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
|
|
150
|
+
*/
|
|
151
|
+
/** Default port for jeeves-server. */
|
|
152
|
+
const SERVER_PORT = 1934;
|
|
153
|
+
/** Default port for jeeves-watcher. */
|
|
154
|
+
const WATCHER_PORT = 1936;
|
|
155
|
+
/** Default port for jeeves-runner. */
|
|
156
|
+
const RUNNER_PORT = 1937;
|
|
157
|
+
/** Default port for jeeves-meta. */
|
|
158
|
+
const META_PORT = 1938;
|
|
159
|
+
/** Map of service names to their default ports. */
|
|
160
|
+
const DEFAULT_PORTS = {
|
|
161
|
+
server: SERVER_PORT,
|
|
162
|
+
watcher: WATCHER_PORT,
|
|
163
|
+
runner: RUNNER_PORT,
|
|
164
|
+
meta: META_PORT,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Managed section IDs and their stable ordering for TOOLS.md.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* Section ordering is fixed to prevent diff churn regardless of which
|
|
172
|
+
* component writes last. Sections always appear in this order.
|
|
173
|
+
*/
|
|
174
|
+
/** Known section IDs for TOOLS.md managed block. */
|
|
175
|
+
const SECTION_IDS = {
|
|
176
|
+
/** Platform health and guidance section. */
|
|
177
|
+
Platform: 'Platform',
|
|
178
|
+
/** Watcher index stats and search configuration. */
|
|
179
|
+
Watcher: 'Watcher',
|
|
180
|
+
/** Server export capabilities and connected services. */
|
|
181
|
+
Server: 'Server',
|
|
182
|
+
/** Runner job status and active scripts. */
|
|
183
|
+
Runner: 'Runner',
|
|
184
|
+
/** Meta synthesis entity summary and tools. */
|
|
185
|
+
Meta: 'Meta',
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Stable ordering of sections within the managed TOOLS.md block.
|
|
189
|
+
* Sections always appear in this order regardless of write order.
|
|
190
|
+
*/
|
|
191
|
+
const SECTION_ORDER = [
|
|
192
|
+
SECTION_IDS.Platform,
|
|
193
|
+
SECTION_IDS.Watcher,
|
|
194
|
+
SECTION_IDS.Server,
|
|
195
|
+
SECTION_IDS.Runner,
|
|
196
|
+
SECTION_IDS.Meta,
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Core library version, read from package.json at runtime.
|
|
201
|
+
*
|
|
202
|
+
* @remarks
|
|
203
|
+
* Used for version-stamp convergence (Decision 21). The version stamp
|
|
204
|
+
* on managed content reflects the actual published library version,
|
|
205
|
+
* enabling higher-version writers to take precedence.
|
|
206
|
+
*/
|
|
207
|
+
const require$1 = createRequire(import.meta.url);
|
|
208
|
+
const pkg = require$1('../../package.json');
|
|
209
|
+
/** The core library version from package.json. */
|
|
210
|
+
const CORE_VERSION = pkg.version;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Core configuration schema and resolution.
|
|
214
|
+
*
|
|
215
|
+
* @remarks
|
|
216
|
+
* Core config lives at `{configRoot}/jeeves-core/config.json`.
|
|
217
|
+
* Config resolution order:
|
|
218
|
+
* 1. Component's own config file
|
|
219
|
+
* 2. Core config file
|
|
220
|
+
* 3. Hardcoded library defaults
|
|
221
|
+
*/
|
|
222
|
+
/** Zod schema for a service entry in core config. */
|
|
223
|
+
const serviceEntrySchema = z.object({
|
|
224
|
+
/** Service URL (must be a valid URL). */
|
|
225
|
+
url: z.string().url().describe('Service URL'),
|
|
226
|
+
});
|
|
227
|
+
/** Zod schema for the core config file. */
|
|
228
|
+
const coreConfigSchema = z.object({
|
|
229
|
+
/** JSON Schema pointer for IDE autocomplete. */
|
|
230
|
+
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
231
|
+
/** Owner identity keys (canonical identityLinks references). */
|
|
232
|
+
owners: z.array(z.string()).default([]).describe('Owner identity keys'),
|
|
233
|
+
/** Service URL overrides keyed by service name. */
|
|
234
|
+
services: z
|
|
235
|
+
.record(z.string(), serviceEntrySchema)
|
|
236
|
+
.default({})
|
|
237
|
+
.describe('Service URL overrides'),
|
|
238
|
+
/** Registry cache configuration. */
|
|
239
|
+
registryCache: z
|
|
240
|
+
.object({
|
|
241
|
+
/** Cache TTL in seconds for npm registry queries. */
|
|
242
|
+
ttlSeconds: z
|
|
243
|
+
.number()
|
|
244
|
+
.int()
|
|
245
|
+
.positive()
|
|
246
|
+
.default(3600)
|
|
247
|
+
.describe('Cache TTL in seconds'),
|
|
248
|
+
})
|
|
249
|
+
.default({})
|
|
250
|
+
.describe('Registry cache settings'),
|
|
251
|
+
});
|
|
252
|
+
/**
|
|
253
|
+
* Generate a JSON Schema from the Zod schema for `$schema` pointer support.
|
|
254
|
+
*
|
|
255
|
+
* @returns A JSON Schema object.
|
|
256
|
+
*/
|
|
257
|
+
function generateJsonSchema() {
|
|
258
|
+
return {
|
|
259
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
260
|
+
title: 'Jeeves Core Configuration',
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
$schema: { type: 'string' },
|
|
264
|
+
owners: {
|
|
265
|
+
type: 'array',
|
|
266
|
+
items: { type: 'string' },
|
|
267
|
+
default: [],
|
|
268
|
+
},
|
|
269
|
+
services: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
additionalProperties: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
url: { type: 'string', format: 'uri' },
|
|
275
|
+
},
|
|
276
|
+
required: ['url'],
|
|
277
|
+
},
|
|
278
|
+
default: {},
|
|
279
|
+
},
|
|
280
|
+
registryCache: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
ttlSeconds: {
|
|
284
|
+
type: 'integer',
|
|
285
|
+
minimum: 1,
|
|
286
|
+
default: 3600,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
default: {},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Load and parse a config file, returning undefined if missing or invalid.
|
|
296
|
+
*
|
|
297
|
+
* @param configDir - Directory containing config.json.
|
|
298
|
+
* @returns Parsed config or undefined.
|
|
299
|
+
*/
|
|
300
|
+
function loadConfig(configDir) {
|
|
301
|
+
const configPath = join(configDir, CONFIG_FILE);
|
|
302
|
+
if (!existsSync(configPath))
|
|
303
|
+
return undefined;
|
|
304
|
+
try {
|
|
305
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
306
|
+
const parsed = JSON.parse(raw);
|
|
307
|
+
return coreConfigSchema.parse(parsed);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Workspace and config root initialization.
|
|
316
|
+
*
|
|
317
|
+
* @remarks
|
|
318
|
+
* `init()` must be called once before any other core library functions.
|
|
319
|
+
* It caches `workspacePath` and `configRoot` at module level.
|
|
320
|
+
* Core derives all namespaced paths from these values:
|
|
321
|
+
* - `{configRoot}/jeeves-core/` for core config
|
|
322
|
+
* - `{configRoot}/jeeves-{name}/` for each component
|
|
323
|
+
*/
|
|
324
|
+
let state;
|
|
325
|
+
/**
|
|
326
|
+
* Initialize the core library with workspace and config root paths.
|
|
327
|
+
*
|
|
328
|
+
* @param options - Workspace and config root paths.
|
|
329
|
+
*/
|
|
330
|
+
function init(options) {
|
|
331
|
+
state = {
|
|
332
|
+
workspacePath: options.workspacePath,
|
|
333
|
+
configRoot: options.configRoot,
|
|
334
|
+
coreConfigDir: join(options.configRoot, CORE_CONFIG_DIR),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get the cached workspace path.
|
|
339
|
+
*
|
|
340
|
+
* @throws Error if `init()` has not been called.
|
|
341
|
+
*/
|
|
342
|
+
function getWorkspacePath() {
|
|
343
|
+
if (!state)
|
|
344
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
345
|
+
return state.workspacePath;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get the core config directory path.
|
|
349
|
+
*
|
|
350
|
+
* @throws Error if `init()` has not been called.
|
|
351
|
+
*/
|
|
352
|
+
function getCoreConfigDir() {
|
|
353
|
+
if (!state)
|
|
354
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
355
|
+
return state.coreConfigDir;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Derive the component config directory from the component name.
|
|
359
|
+
*
|
|
360
|
+
* @param componentName - The component name (e.g., 'watcher', 'runner').
|
|
361
|
+
* @returns Absolute path to the component's config directory.
|
|
362
|
+
* @throws Error if `init()` has not been called.
|
|
363
|
+
*/
|
|
364
|
+
function getComponentConfigDir(componentName) {
|
|
365
|
+
if (!state)
|
|
366
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
367
|
+
return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Service URL resolution.
|
|
372
|
+
*
|
|
373
|
+
* @remarks
|
|
374
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
375
|
+
* resolution order:
|
|
376
|
+
* 1. Consumer's own component config
|
|
377
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
378
|
+
* 3. Default port constants
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Resolve the URL for a named Jeeves service.
|
|
382
|
+
*
|
|
383
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
384
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
385
|
+
* @returns The resolved service URL.
|
|
386
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
387
|
+
*/
|
|
388
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
389
|
+
// 2. Check core config
|
|
390
|
+
const coreDir = getCoreConfigDir();
|
|
391
|
+
const coreConfig = loadConfig(coreDir);
|
|
392
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
393
|
+
if (coreUrl)
|
|
394
|
+
return coreUrl;
|
|
395
|
+
// 3. Fall back to port constants
|
|
396
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
397
|
+
if (port !== undefined) {
|
|
398
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* HTTP health probing for Jeeves platform services.
|
|
405
|
+
*
|
|
406
|
+
* @remarks
|
|
407
|
+
* Probes service ports for health endpoints (HTTP GET to /status or /health).
|
|
408
|
+
* Returns structured health data for rendering into TOOLS.md Platform section.
|
|
409
|
+
*/
|
|
410
|
+
/**
|
|
411
|
+
* Extract port number from a URL string.
|
|
412
|
+
*
|
|
413
|
+
* @param url - Service URL.
|
|
414
|
+
* @returns Port number.
|
|
415
|
+
*/
|
|
416
|
+
function extractPort(url) {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = new URL(url);
|
|
419
|
+
return parsed.port ? parseInt(parsed.port, 10) : 80;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Probe a single service for health.
|
|
427
|
+
*
|
|
428
|
+
* @param serviceName - The service name (e.g., 'server', 'watcher').
|
|
429
|
+
* @param consumerName - Optional consumer name for URL resolution.
|
|
430
|
+
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
431
|
+
* @returns Probe result.
|
|
432
|
+
*/
|
|
433
|
+
async function probeService(serviceName, consumerName, timeoutMs = 3000) {
|
|
434
|
+
const url = getServiceUrl(serviceName);
|
|
435
|
+
const port = extractPort(url);
|
|
436
|
+
const endpoints = ['/status', '/health'];
|
|
437
|
+
for (const endpoint of endpoints) {
|
|
438
|
+
try {
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
const timeout = setTimeout(() => {
|
|
441
|
+
controller.abort();
|
|
442
|
+
}, timeoutMs);
|
|
443
|
+
const response = await fetch(`${url}${endpoint}`, {
|
|
444
|
+
signal: controller.signal,
|
|
445
|
+
});
|
|
446
|
+
clearTimeout(timeout);
|
|
447
|
+
if (response.ok) {
|
|
448
|
+
let version;
|
|
449
|
+
try {
|
|
450
|
+
const body = await response.json();
|
|
451
|
+
if (typeof body === 'object' &&
|
|
452
|
+
body !== null &&
|
|
453
|
+
'version' in body &&
|
|
454
|
+
typeof body['version'] === 'string') {
|
|
455
|
+
version = body['version'];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Non-JSON response is fine — we just don't get version info
|
|
460
|
+
}
|
|
461
|
+
return { name: serviceName, port, healthy: true, version };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Try next endpoint
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { name: serviceName, port, healthy: false };
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Probe all known Jeeves services for health.
|
|
472
|
+
*
|
|
473
|
+
* @param consumerName - Optional consumer name for URL resolution.
|
|
474
|
+
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
475
|
+
* @returns Array of probe results for all services.
|
|
476
|
+
*/
|
|
477
|
+
async function probeAllServices(consumerName, timeoutMs = 3000) {
|
|
478
|
+
const serviceNames = Object.keys(DEFAULT_PORTS);
|
|
479
|
+
const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
|
|
480
|
+
return results;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Registry version cache for npm package update awareness.
|
|
485
|
+
*
|
|
486
|
+
* @remarks
|
|
487
|
+
* Caches the latest npm registry version in a local JSON file
|
|
488
|
+
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
489
|
+
*/
|
|
490
|
+
/**
|
|
491
|
+
* Check the npm registry for the latest version of a package.
|
|
492
|
+
*
|
|
493
|
+
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
494
|
+
* @param cacheDir - Directory to store the cache file.
|
|
495
|
+
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
496
|
+
* @returns The latest version string, or undefined if the check fails.
|
|
497
|
+
*/
|
|
498
|
+
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
499
|
+
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
500
|
+
// Check cache first
|
|
501
|
+
if (existsSync(cachePath)) {
|
|
502
|
+
try {
|
|
503
|
+
const raw = readFileSync(cachePath, 'utf-8');
|
|
504
|
+
const entry = JSON.parse(raw);
|
|
505
|
+
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
506
|
+
if (age < ttlSeconds * 1000) {
|
|
507
|
+
return entry.version;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Cache corrupt — proceed with fresh check
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Query npm registry
|
|
515
|
+
try {
|
|
516
|
+
const result = execSync(`npm view ${packageName} version`, {
|
|
517
|
+
encoding: 'utf-8',
|
|
518
|
+
timeout: 15_000,
|
|
519
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
520
|
+
}).trim();
|
|
521
|
+
if (!result)
|
|
522
|
+
return undefined;
|
|
523
|
+
// Write cache
|
|
524
|
+
if (!existsSync(cacheDir)) {
|
|
525
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
526
|
+
}
|
|
527
|
+
const entry = {
|
|
528
|
+
version: result,
|
|
529
|
+
checkedAt: new Date().toISOString(),
|
|
530
|
+
};
|
|
531
|
+
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Similarity-based cleanup detection for orphaned managed content.
|
|
541
|
+
*
|
|
542
|
+
* @remarks
|
|
543
|
+
* Uses Jaccard similarity on 3-word shingles (Decision 22) to detect
|
|
544
|
+
* when orphaned managed content exists in the user content zone.
|
|
545
|
+
*/
|
|
546
|
+
/** Default similarity threshold for cleanup detection. */
|
|
547
|
+
const DEFAULT_THRESHOLD = 0.15;
|
|
548
|
+
/**
|
|
549
|
+
* Generate a set of n-word shingles from text.
|
|
550
|
+
*
|
|
551
|
+
* @param text - Input text.
|
|
552
|
+
* @param n - Shingle size (default 3).
|
|
553
|
+
* @returns Set of n-word shingles.
|
|
554
|
+
*/
|
|
555
|
+
function shingles(text, n = 3) {
|
|
556
|
+
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
557
|
+
const set = new Set();
|
|
558
|
+
for (let i = 0; i <= words.length - n; i++) {
|
|
559
|
+
set.add(words.slice(i, i + n).join(' '));
|
|
560
|
+
}
|
|
561
|
+
return set;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Compute Jaccard similarity between two sets.
|
|
565
|
+
*
|
|
566
|
+
* @param a - First set.
|
|
567
|
+
* @param b - Second set.
|
|
568
|
+
* @returns Jaccard similarity coefficient (0 to 1).
|
|
569
|
+
*/
|
|
570
|
+
function jaccard(a, b) {
|
|
571
|
+
if (a.size === 0 && b.size === 0)
|
|
572
|
+
return 0;
|
|
573
|
+
let intersection = 0;
|
|
574
|
+
for (const item of a) {
|
|
575
|
+
if (b.has(item))
|
|
576
|
+
intersection++;
|
|
577
|
+
}
|
|
578
|
+
return intersection / (a.size + b.size - intersection);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Check whether user content contains orphaned managed content.
|
|
582
|
+
*
|
|
583
|
+
* @param managedContent - The current managed block content.
|
|
584
|
+
* @param userContent - Content below the END marker.
|
|
585
|
+
* @param threshold - Jaccard threshold (default 0.15).
|
|
586
|
+
* @returns `true` if cleanup is needed.
|
|
587
|
+
*/
|
|
588
|
+
function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD) {
|
|
589
|
+
if (!userContent.trim())
|
|
590
|
+
return false;
|
|
591
|
+
return jaccard(shingles(managedContent), shingles(userContent)) > threshold;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Stable section ordering for managed TOOLS.md blocks.
|
|
596
|
+
*
|
|
597
|
+
* @remarks
|
|
598
|
+
* Sorts sections by the canonical SECTION_ORDER: known sections
|
|
599
|
+
* appear in their defined order, unknown sections are appended after.
|
|
600
|
+
* Used by both parseManaged (for consistent output) and
|
|
601
|
+
* updateManagedSection (for reassembly).
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* Sort sections in place by stable ordering.
|
|
605
|
+
*
|
|
606
|
+
* @param sections - Array of managed sections to sort.
|
|
607
|
+
* @returns The sorted array (same reference, mutated in place).
|
|
608
|
+
*/
|
|
609
|
+
function sortSectionsByOrder(sections) {
|
|
610
|
+
return sections.sort((a, b) => {
|
|
611
|
+
const aIdx = SECTION_ORDER.indexOf(a.id);
|
|
612
|
+
const bIdx = SECTION_ORDER.indexOf(b.id);
|
|
613
|
+
const aOrder = aIdx === -1 ? SECTION_ORDER.length : aIdx;
|
|
614
|
+
const bOrder = bIdx === -1 ? SECTION_ORDER.length : bIdx;
|
|
615
|
+
return aOrder - bOrder;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Parse managed block from file content.
|
|
621
|
+
*
|
|
622
|
+
* @remarks
|
|
623
|
+
* Extracts managed content delimited by comment markers, parses H2
|
|
624
|
+
* sections within the block, and returns the structured result plus
|
|
625
|
+
* user content outside the markers.
|
|
626
|
+
*/
|
|
627
|
+
/**
|
|
628
|
+
* Build regex patterns for the given markers.
|
|
629
|
+
*
|
|
630
|
+
* @param markers - Begin/end marker strings.
|
|
631
|
+
* @returns Object with begin and end regex patterns.
|
|
632
|
+
*/
|
|
633
|
+
function buildMarkerPatterns(markers) {
|
|
634
|
+
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
635
|
+
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
636
|
+
return {
|
|
637
|
+
beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
|
|
638
|
+
endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Parse H2 sections from managed block content.
|
|
643
|
+
*
|
|
644
|
+
* @param content - Raw managed block content.
|
|
645
|
+
* @returns Array of parsed sections in stable order.
|
|
646
|
+
*/
|
|
647
|
+
function parseSections(content) {
|
|
648
|
+
const lines = content.split('\n');
|
|
649
|
+
const sections = [];
|
|
650
|
+
let currentId;
|
|
651
|
+
let currentLines = [];
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
const h2Match = /^## (.+)$/.exec(line);
|
|
654
|
+
if (h2Match) {
|
|
655
|
+
if (currentId !== undefined) {
|
|
656
|
+
sections.push({
|
|
657
|
+
id: currentId,
|
|
658
|
+
content: currentLines.join('\n').trim(),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
currentId = h2Match[1];
|
|
662
|
+
currentLines = [];
|
|
663
|
+
}
|
|
664
|
+
else if (currentId !== undefined) {
|
|
665
|
+
currentLines.push(line);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (currentId !== undefined) {
|
|
669
|
+
sections.push({
|
|
670
|
+
id: currentId,
|
|
671
|
+
content: currentLines.join('\n').trim(),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return sortSectionsByOrder(sections);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Parse a managed block from file content.
|
|
678
|
+
*
|
|
679
|
+
* @param fileContent - Full file content.
|
|
680
|
+
* @param markers - Optional custom markers (defaults to TOOLS markers).
|
|
681
|
+
* @returns Parsed result with sections, version stamp, and user content.
|
|
682
|
+
*/
|
|
683
|
+
function parseManaged(fileContent, markers = TOOLS_MARKERS) {
|
|
684
|
+
const { beginRe, endRe } = buildMarkerPatterns(markers);
|
|
685
|
+
const beginMatch = beginRe.exec(fileContent);
|
|
686
|
+
if (!beginMatch) {
|
|
687
|
+
return {
|
|
688
|
+
found: false,
|
|
689
|
+
versionStamp: undefined,
|
|
690
|
+
managedContent: '',
|
|
691
|
+
sections: [],
|
|
692
|
+
beforeContent: '',
|
|
693
|
+
userContent: fileContent,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const endMatch = endRe.exec(fileContent.slice(beginMatch.index + beginMatch[0].length));
|
|
697
|
+
if (!endMatch) {
|
|
698
|
+
// Corrupt: BEGIN without END — treat as fresh file
|
|
699
|
+
return {
|
|
700
|
+
found: false,
|
|
701
|
+
versionStamp: undefined,
|
|
702
|
+
managedContent: '',
|
|
703
|
+
sections: [],
|
|
704
|
+
beforeContent: '',
|
|
705
|
+
userContent: fileContent,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const beforeContent = fileContent.slice(0, beginMatch.index).trim();
|
|
709
|
+
const managedStart = beginMatch.index + beginMatch[0].length;
|
|
710
|
+
const managedEnd = managedStart + endMatch.index;
|
|
711
|
+
const managedContent = fileContent.slice(managedStart, managedEnd).trim();
|
|
712
|
+
const afterEnd = managedStart + endMatch.index + endMatch[0].length;
|
|
713
|
+
const userContent = fileContent.slice(afterEnd).trim();
|
|
714
|
+
// Extract version stamp from BEGIN marker line
|
|
715
|
+
let versionStamp;
|
|
716
|
+
const stampMatch = VERSION_STAMP_PATTERN.exec(beginMatch[0]);
|
|
717
|
+
if (stampMatch?.[2] && stampMatch[3]) {
|
|
718
|
+
versionStamp = {
|
|
719
|
+
version: stampMatch[2],
|
|
720
|
+
timestamp: stampMatch[3],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const sections = parseSections(managedContent);
|
|
724
|
+
return {
|
|
725
|
+
found: true,
|
|
726
|
+
versionStamp,
|
|
727
|
+
managedContent,
|
|
728
|
+
sections,
|
|
729
|
+
beforeContent,
|
|
730
|
+
userContent,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Version-stamp parsing and convergence logic.
|
|
736
|
+
*
|
|
737
|
+
* @remarks
|
|
738
|
+
* When multiple component plugins bundle different core library versions,
|
|
739
|
+
* they independently maintain shared managed content. The version-stamp
|
|
740
|
+
* mechanism ensures convergence without coordination state.
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* Format the BEGIN marker comment with a version stamp.
|
|
744
|
+
*
|
|
745
|
+
* @param markerText - The marker text (e.g., 'BEGIN JEEVES PLATFORM TOOLS').
|
|
746
|
+
* @param version - The core library version.
|
|
747
|
+
* @returns Formatted comment line.
|
|
748
|
+
*/
|
|
749
|
+
function formatBeginMarker(markerText, version) {
|
|
750
|
+
const timestamp = new Date().toISOString();
|
|
751
|
+
return `<!-- ${markerText} | core:${version} | ${timestamp} -->`;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Format the END marker comment.
|
|
755
|
+
*
|
|
756
|
+
* @param markerText - The marker text (e.g., 'END JEEVES PLATFORM TOOLS').
|
|
757
|
+
* @returns Formatted comment line.
|
|
758
|
+
*/
|
|
759
|
+
function formatEndMarker(markerText) {
|
|
760
|
+
return `<!-- ${markerText} -->`;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Determine whether this writer should proceed based on version-stamp
|
|
764
|
+
* convergence rules.
|
|
765
|
+
*
|
|
766
|
+
* @param myVersion - The current core library version.
|
|
767
|
+
* @param existing - The existing version stamp (if any).
|
|
768
|
+
* @param stalenessThresholdMs - Staleness threshold in ms (default: 5 min).
|
|
769
|
+
* @returns `true` if the writer should proceed with the write.
|
|
770
|
+
*/
|
|
771
|
+
function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRESHOLD_MS) {
|
|
772
|
+
// No existing stamp — always write
|
|
773
|
+
if (!existing)
|
|
774
|
+
return true;
|
|
775
|
+
// My version >= stamped version — always write (I'm current or newer)
|
|
776
|
+
if (gte(myVersion, existing.version))
|
|
777
|
+
return true;
|
|
778
|
+
// My version < stamped version — check staleness
|
|
779
|
+
const stampAge = Date.now() - new Date(existing.timestamp).getTime();
|
|
780
|
+
return stampAge >= stalenessThresholdMs;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Generic managed-section writer with block and section modes.
|
|
785
|
+
*
|
|
786
|
+
* @remarks
|
|
787
|
+
* Supports two modes:
|
|
788
|
+
* - `block`: Replaces the entire managed block (SOUL.md, AGENTS.md).
|
|
789
|
+
* - `section`: Upserts a named H2 section within the managed block (TOOLS.md).
|
|
790
|
+
*
|
|
791
|
+
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
792
|
+
*/
|
|
793
|
+
/** Default core version when none provided. */
|
|
794
|
+
const DEFAULT_VERSION = '0.0.0';
|
|
795
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
796
|
+
const STALE_LOCK_MS = 120_000;
|
|
797
|
+
/**
|
|
798
|
+
* Update a managed section in a file.
|
|
799
|
+
*
|
|
800
|
+
* @param filePath - Absolute path to the target file.
|
|
801
|
+
* @param content - New content to write.
|
|
802
|
+
* @param options - Write mode and optional configuration.
|
|
803
|
+
*/
|
|
804
|
+
async function updateManagedSection(filePath, content, options = {}) {
|
|
805
|
+
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_VERSION, stalenessThresholdMs, } = options;
|
|
806
|
+
if (mode === 'section' && !sectionId) {
|
|
807
|
+
throw new Error('sectionId is required when mode is "section"');
|
|
808
|
+
}
|
|
809
|
+
const dir = dirname(filePath);
|
|
810
|
+
if (!existsSync(dir)) {
|
|
811
|
+
mkdirSync(dir, { recursive: true });
|
|
812
|
+
}
|
|
813
|
+
// Create file if it doesn't exist
|
|
814
|
+
if (!existsSync(filePath)) {
|
|
815
|
+
writeFileSync(filePath, '', 'utf-8');
|
|
816
|
+
}
|
|
817
|
+
let release;
|
|
818
|
+
try {
|
|
819
|
+
release = await lock(filePath, {
|
|
820
|
+
stale: STALE_LOCK_MS,
|
|
821
|
+
retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
|
|
822
|
+
});
|
|
823
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
824
|
+
const parsed = parseManaged(fileContent, markers);
|
|
825
|
+
// Version-stamp convergence check (block mode only).
|
|
826
|
+
// In section mode, components always write their own sections — the version
|
|
827
|
+
// stamp governs shared content convergence, not component-specific sections.
|
|
828
|
+
if (mode === 'block' &&
|
|
829
|
+
!shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
let newManagedBody;
|
|
833
|
+
if (mode === 'block') {
|
|
834
|
+
newManagedBody = content;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
// Section mode: upsert the named section
|
|
838
|
+
const sections = [...parsed.sections];
|
|
839
|
+
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
840
|
+
if (existingIdx >= 0) {
|
|
841
|
+
sections[existingIdx] = { id: sectionId, content };
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
sections.push({ id: sectionId, content });
|
|
845
|
+
}
|
|
846
|
+
sortSectionsByOrder(sections);
|
|
847
|
+
const sectionText = sections
|
|
848
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
849
|
+
.join('\n\n');
|
|
850
|
+
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
851
|
+
newManagedBody = markers.title
|
|
852
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
853
|
+
: sectionText;
|
|
854
|
+
}
|
|
855
|
+
// Cleanup detection
|
|
856
|
+
const userContent = parsed.userContent;
|
|
857
|
+
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
858
|
+
// Build the full managed block
|
|
859
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
860
|
+
const endLine = formatEndMarker(markers.end);
|
|
861
|
+
const parts = [];
|
|
862
|
+
if (parsed.beforeContent) {
|
|
863
|
+
parts.push(parsed.beforeContent);
|
|
864
|
+
parts.push('');
|
|
865
|
+
}
|
|
866
|
+
parts.push(beginLine);
|
|
867
|
+
if (cleanupNeeded) {
|
|
868
|
+
parts.push('');
|
|
869
|
+
parts.push(CLEANUP_FLAG);
|
|
870
|
+
}
|
|
871
|
+
parts.push('');
|
|
872
|
+
parts.push(newManagedBody);
|
|
873
|
+
parts.push('');
|
|
874
|
+
parts.push(endLine);
|
|
875
|
+
if (userContent) {
|
|
876
|
+
parts.push('');
|
|
877
|
+
parts.push(userContent);
|
|
878
|
+
}
|
|
879
|
+
parts.push('');
|
|
880
|
+
const newFileContent = parts.join('\n');
|
|
881
|
+
// Atomic write: write to temp file, then rename
|
|
882
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
883
|
+
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
884
|
+
renameSync(tempPath, filePath);
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
// Log warning but don't throw — writer cycles are periodic
|
|
888
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
889
|
+
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
890
|
+
}
|
|
891
|
+
finally {
|
|
892
|
+
if (release) {
|
|
893
|
+
try {
|
|
894
|
+
await release();
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// Lock already released or file deleted — safe to ignore
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
905
|
+
*
|
|
906
|
+
* @remarks
|
|
907
|
+
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
908
|
+
* Probes service ports for health, reads content files from the package's
|
|
909
|
+
* `content/` directory, renders the Platform template with live service data,
|
|
910
|
+
* and writes managed sections using `updateManagedSection`.
|
|
911
|
+
*/
|
|
912
|
+
/**
|
|
913
|
+
* Resolve the package's content directory.
|
|
914
|
+
*
|
|
915
|
+
* @returns Absolute path to the content/ directory.
|
|
916
|
+
*/
|
|
917
|
+
function getContentDir() {
|
|
918
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
919
|
+
// From src/platform/refreshPlatformContent.ts → ../../content/
|
|
920
|
+
// From dist/platform/refreshPlatformContent.js → ../../content/
|
|
921
|
+
return join(dirname(thisFile), '..', '..', 'content');
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Read a content file from the package's content/ directory.
|
|
925
|
+
*
|
|
926
|
+
* @param fileName - File name within content/.
|
|
927
|
+
* @returns File content as string, or empty string if missing.
|
|
928
|
+
*/
|
|
929
|
+
function readContentFile(fileName) {
|
|
930
|
+
const filePath = join(getContentDir(), fileName);
|
|
931
|
+
if (!existsSync(filePath)) {
|
|
932
|
+
console.warn(`jeeves-core: content file missing: ${filePath}`);
|
|
933
|
+
return '';
|
|
934
|
+
}
|
|
935
|
+
return readFileSync(filePath, 'utf-8');
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Copy templates from content/templates/ to the core config directory.
|
|
939
|
+
*
|
|
940
|
+
* @param coreConfigDir - Core config directory path.
|
|
941
|
+
*/
|
|
942
|
+
function copyTemplates(coreConfigDir) {
|
|
943
|
+
const sourceDir = join(getContentDir(), 'templates');
|
|
944
|
+
if (!existsSync(sourceDir))
|
|
945
|
+
return;
|
|
946
|
+
const destDir = join(coreConfigDir, TEMPLATES_DIR);
|
|
947
|
+
if (!existsSync(destDir)) {
|
|
948
|
+
mkdirSync(destDir, { recursive: true });
|
|
949
|
+
}
|
|
950
|
+
cpSync(sourceDir, destDir, { recursive: true });
|
|
951
|
+
}
|
|
952
|
+
/** Whether Handlebars helpers have been registered. */
|
|
953
|
+
let helpersRegistered = false;
|
|
954
|
+
/**
|
|
955
|
+
* Register Handlebars helpers used in the Platform template.
|
|
956
|
+
*/
|
|
957
|
+
function registerHelpers() {
|
|
958
|
+
if (helpersRegistered)
|
|
959
|
+
return;
|
|
960
|
+
helpersRegistered = true;
|
|
961
|
+
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
965
|
+
*
|
|
966
|
+
* @param options - Configuration for the refresh cycle.
|
|
967
|
+
*/
|
|
968
|
+
async function refreshPlatformContent(options) {
|
|
969
|
+
const { coreVersion, componentName, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
|
|
970
|
+
const workspacePath = getWorkspacePath();
|
|
971
|
+
const coreConfigDir = getCoreConfigDir();
|
|
972
|
+
// 1. Probe all services
|
|
973
|
+
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
974
|
+
const unhealthyServices = probeResults.filter((r) => !r.healthy);
|
|
975
|
+
// 2. Build version info (registry check for the core package)
|
|
976
|
+
let availableVersion;
|
|
977
|
+
if (!skipRegistryCheck) {
|
|
978
|
+
const cacheDir = componentName
|
|
979
|
+
? getComponentConfigDir(componentName)
|
|
980
|
+
: coreConfigDir;
|
|
981
|
+
availableVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
|
|
982
|
+
}
|
|
983
|
+
const versionInfo = probeResults.map((r) => ({
|
|
984
|
+
name: r.name,
|
|
985
|
+
serviceVersion: r.version,
|
|
986
|
+
coreVersion,
|
|
987
|
+
availableVersion: availableVersion && availableVersion !== coreVersion
|
|
988
|
+
? availableVersion
|
|
989
|
+
: undefined,
|
|
990
|
+
}));
|
|
991
|
+
// 3. Check if templates are available
|
|
992
|
+
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
993
|
+
const templatesAvailable = existsSync(templatePath);
|
|
994
|
+
// 4. Render Platform template
|
|
995
|
+
registerHelpers();
|
|
996
|
+
const templateSrc = readContentFile('tools-platform.md');
|
|
997
|
+
const template = Handlebars.compile(templateSrc);
|
|
998
|
+
const templateData = {
|
|
999
|
+
services: probeResults,
|
|
1000
|
+
unhealthyServices,
|
|
1001
|
+
versionInfo: versionInfo.some((v) => v.serviceVersion)
|
|
1002
|
+
? versionInfo
|
|
1003
|
+
: undefined,
|
|
1004
|
+
templatesAvailable,
|
|
1005
|
+
templatePath,
|
|
1006
|
+
};
|
|
1007
|
+
const platformContent = template(templateData);
|
|
1008
|
+
// 5. Write TOOLS.md Platform section
|
|
1009
|
+
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1010
|
+
await updateManagedSection(toolsPath, platformContent, {
|
|
1011
|
+
mode: 'section',
|
|
1012
|
+
sectionId: 'Platform',
|
|
1013
|
+
markers: TOOLS_MARKERS,
|
|
1014
|
+
coreVersion,
|
|
1015
|
+
stalenessThresholdMs,
|
|
1016
|
+
});
|
|
1017
|
+
// 6. Write SOUL.md managed block
|
|
1018
|
+
const soulContent = readContentFile('soul-section.md');
|
|
1019
|
+
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1020
|
+
await updateManagedSection(soulPath, soulContent, {
|
|
1021
|
+
mode: 'block',
|
|
1022
|
+
markers: SOUL_MARKERS,
|
|
1023
|
+
coreVersion,
|
|
1024
|
+
stalenessThresholdMs,
|
|
1025
|
+
});
|
|
1026
|
+
// 7. Write AGENTS.md managed block
|
|
1027
|
+
const agentsContent = readContentFile('agents-section.md');
|
|
1028
|
+
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1029
|
+
await updateManagedSection(agentsPath, agentsContent, {
|
|
1030
|
+
mode: 'block',
|
|
1031
|
+
markers: AGENTS_MARKERS,
|
|
1032
|
+
coreVersion,
|
|
1033
|
+
stalenessThresholdMs,
|
|
1034
|
+
});
|
|
1035
|
+
// 8. Copy templates to config dir
|
|
1036
|
+
copyTemplates(coreConfigDir);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* One-shot content seeding used by the CLI install command.
|
|
1041
|
+
*
|
|
1042
|
+
* @remarks
|
|
1043
|
+
* Seeds SOUL.md, AGENTS.md, and TOOLS.md Platform section using the same
|
|
1044
|
+
* `updateManagedSection()` code path as writer cycles. Also copies templates
|
|
1045
|
+
* and creates core config with defaults if missing.
|
|
1046
|
+
*/
|
|
1047
|
+
/**
|
|
1048
|
+
* Create the core config file with defaults if it doesn't already exist.
|
|
1049
|
+
*
|
|
1050
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
1051
|
+
*/
|
|
1052
|
+
function ensureCoreConfig(coreConfigDir) {
|
|
1053
|
+
if (!existsSync(coreConfigDir)) {
|
|
1054
|
+
mkdirSync(coreConfigDir, { recursive: true });
|
|
1055
|
+
}
|
|
1056
|
+
const configPath = join(coreConfigDir, CONFIG_FILE);
|
|
1057
|
+
if (existsSync(configPath))
|
|
1058
|
+
return;
|
|
1059
|
+
const defaults = coreConfigSchema.parse({});
|
|
1060
|
+
const configWithSchema = {
|
|
1061
|
+
$schema: './config.schema.json',
|
|
1062
|
+
...defaults,
|
|
1063
|
+
};
|
|
1064
|
+
writeFileSync(configPath, JSON.stringify(configWithSchema, null, 2), 'utf-8');
|
|
1065
|
+
// Write JSON schema file alongside config
|
|
1066
|
+
const schemaPath = join(coreConfigDir, 'config.schema.json');
|
|
1067
|
+
const jsonSchema = generateJsonSchema();
|
|
1068
|
+
writeFileSync(schemaPath, JSON.stringify(jsonSchema, null, 2), 'utf-8');
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Seed all platform content into the workspace.
|
|
1072
|
+
*
|
|
1073
|
+
* @remarks
|
|
1074
|
+
* Uses the same `updateManagedSection()` code path as writer cycles.
|
|
1075
|
+
* Creates core config with defaults if missing. Copies templates.
|
|
1076
|
+
* Jaccard cleanup detection runs automatically via `updateManagedSection`.
|
|
1077
|
+
*
|
|
1078
|
+
* @param options - Seeding configuration.
|
|
1079
|
+
*/
|
|
1080
|
+
async function seedContent(options) {
|
|
1081
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1082
|
+
// Ensure core config exists
|
|
1083
|
+
ensureCoreConfig(coreConfigDir);
|
|
1084
|
+
// Seed content via the same code path as writer cycles
|
|
1085
|
+
await refreshPlatformContent({
|
|
1086
|
+
coreVersion: options.coreVersion,
|
|
1087
|
+
probeTimeoutMs: options.probeTimeoutMs ?? 3000,
|
|
1088
|
+
skipRegistryCheck: options.skipRegistryCheck ?? true,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Shared CLI defaults and option registration for Jeeves CLI commands.
|
|
1094
|
+
*
|
|
1095
|
+
* @remarks
|
|
1096
|
+
* All three CLI commands (install, uninstall, status) share the same
|
|
1097
|
+
* `--workspace` and `--config-root` options with the same defaults.
|
|
1098
|
+
* This module centralizes them to eliminate duplication.
|
|
1099
|
+
*/
|
|
1100
|
+
/** Default workspace path (current directory). */
|
|
1101
|
+
const DEFAULT_WORKSPACE = '.';
|
|
1102
|
+
/** Default config root path. */
|
|
1103
|
+
const DEFAULT_CONFIG_ROOT = './config';
|
|
1104
|
+
/**
|
|
1105
|
+
* Initialize core from standard CLI options.
|
|
1106
|
+
*
|
|
1107
|
+
* @param opts - Parsed Commander options with workspace and configRoot.
|
|
1108
|
+
*/
|
|
1109
|
+
function initFromOptions(opts) {
|
|
1110
|
+
init({ workspacePath: opts.workspace, configRoot: opts.configRoot });
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* CLI install command: seed platform content into the workspace.
|
|
1115
|
+
*
|
|
1116
|
+
* @remarks
|
|
1117
|
+
* Seeds SOUL.md, AGENTS.md, TOOLS.md Platform section using the same
|
|
1118
|
+
* `updateManagedSection()` code path as writer cycles. Copies templates
|
|
1119
|
+
* to config dir. Creates core config with defaults if missing.
|
|
1120
|
+
* Jaccard cleanup detection runs on install (Decision 22).
|
|
1121
|
+
*/
|
|
1122
|
+
/**
|
|
1123
|
+
* Register the install subcommand on the parent CLI program.
|
|
1124
|
+
*
|
|
1125
|
+
* @param program - The parent Commander program.
|
|
1126
|
+
*/
|
|
1127
|
+
function registerInstallCommand(program) {
|
|
1128
|
+
program
|
|
1129
|
+
.command('install')
|
|
1130
|
+
.description('Seed Jeeves platform content into the workspace')
|
|
1131
|
+
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1132
|
+
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1133
|
+
.action(async (opts) => {
|
|
1134
|
+
console.log('Jeeves platform install');
|
|
1135
|
+
console.log(` Workspace: ${opts.workspace}`);
|
|
1136
|
+
console.log(` Config root: ${opts.configRoot}`);
|
|
1137
|
+
console.log();
|
|
1138
|
+
initFromOptions(opts);
|
|
1139
|
+
await seedContent({
|
|
1140
|
+
coreVersion: CORE_VERSION,
|
|
1141
|
+
skipRegistryCheck: true,
|
|
1142
|
+
});
|
|
1143
|
+
console.log('✅ Platform content seeded successfully.');
|
|
1144
|
+
console.log(' - SOUL.md managed section written');
|
|
1145
|
+
console.log(' - AGENTS.md managed section written');
|
|
1146
|
+
console.log(' - TOOLS.md Platform section written');
|
|
1147
|
+
console.log(' - Templates copied to config directory');
|
|
1148
|
+
console.log(' - Core config created (if not present)');
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* CLI status command: probe all service ports and report health summary.
|
|
1154
|
+
*
|
|
1155
|
+
* @remarks
|
|
1156
|
+
* Displays a table of all Jeeves platform services with port and
|
|
1157
|
+
* health status. Exits with code 0 if all services are healthy,
|
|
1158
|
+
* code 1 if any are unreachable.
|
|
1159
|
+
*/
|
|
1160
|
+
/**
|
|
1161
|
+
* Register the status subcommand on the parent CLI program.
|
|
1162
|
+
*
|
|
1163
|
+
* @param program - The parent Commander program.
|
|
1164
|
+
*/
|
|
1165
|
+
function registerStatusCommand(program) {
|
|
1166
|
+
program
|
|
1167
|
+
.command('status')
|
|
1168
|
+
.description('Probe all Jeeves service ports and report health summary')
|
|
1169
|
+
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1170
|
+
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1171
|
+
.option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
|
|
1172
|
+
.action(async (opts) => {
|
|
1173
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
1174
|
+
initFromOptions(opts);
|
|
1175
|
+
console.log('Jeeves Platform Status');
|
|
1176
|
+
console.log('='.repeat(60));
|
|
1177
|
+
console.log();
|
|
1178
|
+
const probeResults = await probeAllServices(undefined, timeoutMs);
|
|
1179
|
+
const nameWidth = 10;
|
|
1180
|
+
const portWidth = 6;
|
|
1181
|
+
const statusWidth = 30;
|
|
1182
|
+
const header = [
|
|
1183
|
+
'Service'.padEnd(nameWidth),
|
|
1184
|
+
'Port'.padEnd(portWidth),
|
|
1185
|
+
'Status'.padEnd(statusWidth),
|
|
1186
|
+
].join(' ');
|
|
1187
|
+
const separator = [
|
|
1188
|
+
'-'.repeat(nameWidth),
|
|
1189
|
+
'-'.repeat(portWidth),
|
|
1190
|
+
'-'.repeat(statusWidth),
|
|
1191
|
+
].join(' ');
|
|
1192
|
+
console.log(header);
|
|
1193
|
+
console.log(separator);
|
|
1194
|
+
let allHealthy = true;
|
|
1195
|
+
for (const r of probeResults) {
|
|
1196
|
+
let status;
|
|
1197
|
+
if (r.healthy) {
|
|
1198
|
+
status = r.version ? `✅ Running (v${r.version})` : '✅ Running';
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
status = r.error ? `❌ ${r.error}` : '❌ Down';
|
|
1202
|
+
allHealthy = false;
|
|
1203
|
+
}
|
|
1204
|
+
const row = [
|
|
1205
|
+
r.name.padEnd(nameWidth),
|
|
1206
|
+
String(r.port).padEnd(portWidth),
|
|
1207
|
+
status.padEnd(statusWidth),
|
|
1208
|
+
].join(' ');
|
|
1209
|
+
console.log(row);
|
|
1210
|
+
}
|
|
1211
|
+
console.log();
|
|
1212
|
+
const healthy = probeResults.filter((r) => r.healthy).length;
|
|
1213
|
+
const total = probeResults.length;
|
|
1214
|
+
console.log(`${String(healthy)}/${String(total)} services healthy`);
|
|
1215
|
+
if (!allHealthy) {
|
|
1216
|
+
process.exitCode = 1;
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Shared helpers for the uninstall command.
|
|
1223
|
+
*
|
|
1224
|
+
* @remarks
|
|
1225
|
+
* Extracted for testability — these are the core uninstall operations.
|
|
1226
|
+
*/
|
|
1227
|
+
/**
|
|
1228
|
+
* Remove managed block from a file, keeping user content.
|
|
1229
|
+
*
|
|
1230
|
+
* @param filePath - Absolute path to the workspace file.
|
|
1231
|
+
* @param markers - Begin/end marker pair.
|
|
1232
|
+
*/
|
|
1233
|
+
function removeManagedBlockFromFile(filePath, markers) {
|
|
1234
|
+
if (!existsSync(filePath))
|
|
1235
|
+
return;
|
|
1236
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1237
|
+
const parsed = parseManaged(content, markers);
|
|
1238
|
+
if (!parsed.found)
|
|
1239
|
+
return;
|
|
1240
|
+
// Reconstruct file with only user content
|
|
1241
|
+
const parts = [];
|
|
1242
|
+
if (parsed.beforeContent) {
|
|
1243
|
+
parts.push(parsed.beforeContent);
|
|
1244
|
+
}
|
|
1245
|
+
if (parsed.userContent) {
|
|
1246
|
+
if (parts.length > 0)
|
|
1247
|
+
parts.push('');
|
|
1248
|
+
parts.push(parsed.userContent);
|
|
1249
|
+
}
|
|
1250
|
+
const newContent = parts.join('\n').trim() + '\n';
|
|
1251
|
+
writeFileSync(filePath, newContent, 'utf-8');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* CLI uninstall command: remove managed sections and platform artifacts.
|
|
1256
|
+
*
|
|
1257
|
+
* @remarks
|
|
1258
|
+
* Removes managed sections from SOUL.md, AGENTS.md, TOOLS.md.
|
|
1259
|
+
* Removes templates and config dir artifacts. Warns if services
|
|
1260
|
+
* still responding on known ports.
|
|
1261
|
+
*/
|
|
1262
|
+
/**
|
|
1263
|
+
* Register the uninstall subcommand on the parent CLI program.
|
|
1264
|
+
*
|
|
1265
|
+
* @param program - The parent Commander program.
|
|
1266
|
+
*/
|
|
1267
|
+
function registerUninstallCommand(program) {
|
|
1268
|
+
program
|
|
1269
|
+
.command('uninstall')
|
|
1270
|
+
.description('Remove Jeeves managed sections and platform artifacts')
|
|
1271
|
+
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1272
|
+
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1273
|
+
.action(async (opts) => {
|
|
1274
|
+
console.log('Jeeves platform uninstall');
|
|
1275
|
+
console.log(` Workspace: ${opts.workspace}`);
|
|
1276
|
+
console.log(` Config root: ${opts.configRoot}`);
|
|
1277
|
+
console.log();
|
|
1278
|
+
initFromOptions(opts);
|
|
1279
|
+
const wsPath = getWorkspacePath();
|
|
1280
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1281
|
+
// Remove managed sections from workspace files
|
|
1282
|
+
const toolsPath = join(wsPath, WORKSPACE_FILES.tools);
|
|
1283
|
+
removeManagedBlockFromFile(toolsPath, TOOLS_MARKERS);
|
|
1284
|
+
console.log(' ✓ TOOLS.md managed section removed');
|
|
1285
|
+
const soulPath = join(wsPath, WORKSPACE_FILES.soul);
|
|
1286
|
+
removeManagedBlockFromFile(soulPath, SOUL_MARKERS);
|
|
1287
|
+
console.log(' ✓ SOUL.md managed section removed');
|
|
1288
|
+
const agentsPath = join(wsPath, WORKSPACE_FILES.agents);
|
|
1289
|
+
removeManagedBlockFromFile(agentsPath, AGENTS_MARKERS);
|
|
1290
|
+
console.log(' ✓ AGENTS.md managed section removed');
|
|
1291
|
+
// Remove templates directory
|
|
1292
|
+
const templatesDir = join(coreConfigDir, TEMPLATES_DIR);
|
|
1293
|
+
if (existsSync(templatesDir)) {
|
|
1294
|
+
rmSync(templatesDir, { recursive: true, force: true });
|
|
1295
|
+
console.log(' ✓ Templates removed');
|
|
1296
|
+
}
|
|
1297
|
+
// Remove config schema file
|
|
1298
|
+
const schemaPath = join(coreConfigDir, 'config.schema.json');
|
|
1299
|
+
if (existsSync(schemaPath)) {
|
|
1300
|
+
rmSync(schemaPath);
|
|
1301
|
+
console.log(' ✓ Config schema removed');
|
|
1302
|
+
}
|
|
1303
|
+
console.log();
|
|
1304
|
+
// Warn if services still responding
|
|
1305
|
+
try {
|
|
1306
|
+
const probeResults = await probeAllServices(undefined, 2000);
|
|
1307
|
+
const running = probeResults.filter((r) => r.healthy);
|
|
1308
|
+
if (running.length > 0) {
|
|
1309
|
+
console.log('⚠️ The following services are still responding:');
|
|
1310
|
+
for (const r of running) {
|
|
1311
|
+
const ver = r.version ? ` (v${r.version})` : '';
|
|
1312
|
+
console.log(` - ${r.name} on port ${String(r.port)}${ver}`);
|
|
1313
|
+
}
|
|
1314
|
+
console.log(' Consider stopping them before fully removing Jeeves.');
|
|
1315
|
+
console.log();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
catch {
|
|
1319
|
+
// Probe failure is non-fatal during uninstall
|
|
1320
|
+
}
|
|
1321
|
+
console.log('✅ Jeeves platform artifacts removed.');
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Jeeves CLI — platform content seeding, teardown, and status.
|
|
1327
|
+
*
|
|
1328
|
+
* @remarks
|
|
1329
|
+
* Entry point for the `jeeves` CLI command. Provides install, uninstall,
|
|
1330
|
+
* and status subcommands.
|
|
1331
|
+
*/
|
|
1332
|
+
const cli = new Command()
|
|
1333
|
+
.name('jeeves')
|
|
1334
|
+
.description('Jeeves AI assistant platform — shared library and CLI')
|
|
1335
|
+
.version(CORE_VERSION)
|
|
1336
|
+
.enablePositionalOptions()
|
|
1337
|
+
.passThroughOptions();
|
|
1338
|
+
registerInstallCommand(cli);
|
|
1339
|
+
registerUninstallCommand(cli);
|
|
1340
|
+
registerStatusCommand(cli);
|
|
1341
|
+
cli.parse();
|