@pellux/goodvibes-agent 0.1.46 → 0.1.48

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.
@@ -1,465 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- readdirSync,
7
- statSync,
8
- writeFileSync,
9
- } from 'node:fs';
10
- import { createReadStream } from 'node:fs';
11
- import { basename, dirname, join, relative, resolve, sep } from 'node:path';
12
- import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '@pellux/goodvibes-sdk/platform/config';
13
-
14
- export type HomeFileOwner = 'tui' | 'daemon' | 'foreign-goodvibes-product' | 'unknown-root';
15
-
16
- export type SettingsKeyClassification =
17
- | 'current-schema'
18
- | 'known-dynamic'
19
- | 'default-config-dynamic'
20
- | 'unknown-stale-candidate';
21
-
22
- export interface HomeAuditOptions {
23
- readonly homeDir: string;
24
- readonly includeHashes?: boolean;
25
- }
26
-
27
- export interface HomeFileSummary {
28
- readonly owner: HomeFileOwner;
29
- readonly files: number;
30
- readonly directories: number;
31
- readonly bytes: number;
32
- }
33
-
34
- export interface HomeFileRecord {
35
- readonly relativePath: string;
36
- readonly owner: HomeFileOwner;
37
- readonly bytes: number;
38
- readonly mode: string;
39
- readonly mtimeIso: string;
40
- readonly sha256?: string;
41
- }
42
-
43
- export interface SettingsKeyAudit {
44
- readonly key: string;
45
- readonly classification: SettingsKeyClassification;
46
- }
47
-
48
- export interface SettingsAudit {
49
- readonly path: string;
50
- readonly exists: boolean;
51
- readonly schemaKeyCount: number;
52
- readonly leafKeyCount: number;
53
- readonly recognizedKeyCount: number;
54
- readonly missingSchemaKeys: readonly string[];
55
- readonly keys: readonly SettingsKeyAudit[];
56
- readonly staleCandidates: readonly string[];
57
- }
58
-
59
- export interface HomeAuditFinding {
60
- readonly severity: 'info' | 'warn' | 'error';
61
- readonly code: string;
62
- readonly path?: string;
63
- readonly message: string;
64
- }
65
-
66
- export interface DuplicateProfilePattern {
67
- readonly normalizedName: string;
68
- readonly count: number;
69
- }
70
-
71
- export interface GoodVibesHomeAudit {
72
- readonly homeDir: string;
73
- readonly generatedAt: string;
74
- readonly summaries: readonly HomeFileSummary[];
75
- readonly files: readonly HomeFileRecord[];
76
- readonly settings: SettingsAudit;
77
- readonly duplicateProfilePatterns: readonly DuplicateProfilePattern[];
78
- readonly findings: readonly HomeAuditFinding[];
79
- readonly allowedWriteRoots: readonly string[];
80
- readonly readOnlyRoots: readonly string[];
81
- }
82
-
83
- export interface HomeSnapshotEntry {
84
- readonly relativePath: string;
85
- readonly bytes: number;
86
- readonly mode: string;
87
- readonly sha256: string;
88
- }
89
-
90
- export type HomeSnapshot = Readonly<Record<string, HomeSnapshotEntry>>;
91
-
92
- export interface HomeSnapshotDiff {
93
- readonly added: readonly string[];
94
- readonly removed: readonly string[];
95
- readonly changed: readonly string[];
96
- }
97
-
98
- const KNOWN_FOREIGN_ROOTS = new Set([
99
- '.backups',
100
- '.exec-output',
101
- 'archive',
102
- 'companion-chat',
103
- 'events',
104
- 'full-suite',
105
- 'hooks',
106
- 'logs',
107
- 'memory',
108
- 'providers',
109
- 'scripts',
110
- 'sdk',
111
- 'skills',
112
- 'state',
113
- 'telemetry',
114
- ]);
115
-
116
- const KNOWN_DYNAMIC_KEYS = [
117
- /^featureFlags(?:\.|$)/,
118
- /^notifications\.webhookUrls$/,
119
- /^wrfc\.gates$/,
120
- ];
121
-
122
- export const GOODVIBES_ALLOWED_WRITE_ROOTS = ['tui/', 'daemon/'] as const;
123
- export const GOODVIBES_READ_ONLY_ROOTS = [
124
- '*.api.json',
125
- 'archive/',
126
- 'sdk/',
127
- 'state/',
128
- 'events/',
129
- 'companion-chat/',
130
- 'full-suite/',
131
- ] as const;
132
-
133
- function toPosixRelative(path: string): string {
134
- return path.split(sep).join('/');
135
- }
136
-
137
- function flattenObject(value: unknown, prefix = '', out: Record<string, unknown> = {}): Record<string, unknown> {
138
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
139
- if (prefix) out[prefix] = value;
140
- return out;
141
- }
142
-
143
- for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
144
- const nextPrefix = prefix ? `${prefix}.${key}` : key;
145
- if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
146
- flattenObject(entry, nextPrefix, out);
147
- } else {
148
- out[nextPrefix] = entry;
149
- }
150
- }
151
- return out;
152
- }
153
-
154
- function classifyFile(relativePath: string): HomeFileOwner {
155
- if (relativePath.startsWith('tui/')) return 'tui';
156
- if (relativePath.startsWith('daemon/')) return 'daemon';
157
- if (!relativePath.includes('/')) return 'foreign-goodvibes-product';
158
- if (basename(relativePath).endsWith('.api.json')) return 'foreign-goodvibes-product';
159
- const firstSegment = relativePath.split('/')[0] ?? '';
160
- if (KNOWN_FOREIGN_ROOTS.has(firstSegment)) return 'foreign-goodvibes-product';
161
- return 'unknown-root';
162
- }
163
-
164
- function isKnownDynamicKey(key: string): boolean {
165
- return KNOWN_DYNAMIC_KEYS.some((pattern) => pattern.test(key));
166
- }
167
-
168
- function classifySettingsKey(
169
- key: string,
170
- schemaKeys: ReadonlySet<string>,
171
- defaultKeys: ReadonlySet<string>,
172
- ): SettingsKeyClassification {
173
- if (schemaKeys.has(key)) return 'current-schema';
174
- if (isKnownDynamicKey(key)) return 'known-dynamic';
175
- if (defaultKeys.has(key)) return 'default-config-dynamic';
176
- return 'unknown-stale-candidate';
177
- }
178
-
179
- function walkFiles(root: string): { files: string[]; directoryCountByOwner: Map<HomeFileOwner, number> } {
180
- const files: string[] = [];
181
- const directoryCountByOwner = new Map<HomeFileOwner, number>();
182
- if (!existsSync(root)) return { files, directoryCountByOwner };
183
-
184
- const visit = (dir: string): void => {
185
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
186
- const absolute = join(dir, entry.name);
187
- const rel = toPosixRelative(relative(root, absolute));
188
- if (entry.isDirectory()) {
189
- const owner = classifyFile(`${rel}/`);
190
- directoryCountByOwner.set(owner, (directoryCountByOwner.get(owner) ?? 0) + 1);
191
- visit(absolute);
192
- } else if (entry.isFile()) {
193
- files.push(absolute);
194
- }
195
- }
196
- };
197
-
198
- visit(root);
199
- files.sort((a, b) => a.localeCompare(b));
200
- return { files, directoryCountByOwner };
201
- }
202
-
203
- async function hashFile(path: string): Promise<string> {
204
- const hash = createHash('sha256');
205
- await new Promise<void>((resolvePromise, reject) => {
206
- const stream = createReadStream(path);
207
- stream.on('data', (chunk) => hash.update(chunk));
208
- stream.on('error', reject);
209
- stream.on('end', resolvePromise);
210
- });
211
- return hash.digest('hex');
212
- }
213
-
214
- function readJsonObject(path: string): Record<string, unknown> | null {
215
- if (!existsSync(path)) return null;
216
- try {
217
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
218
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
219
- ? parsed as Record<string, unknown>
220
- : null;
221
- } catch {
222
- return null;
223
- }
224
- }
225
-
226
- function auditSettings(homeDir: string): SettingsAudit {
227
- const path = join(homeDir, 'tui', 'settings.json');
228
- const settings = readJsonObject(path);
229
- const flatSettings = flattenObject(settings ?? {});
230
- const schemaKeys = new Set(CONFIG_SCHEMA.map((entry) => entry.key));
231
- const defaultKeys = new Set(Object.keys(flattenObject(DEFAULT_CONFIG)));
232
- const keys = Object.keys(flatSettings)
233
- .sort((a, b) => a.localeCompare(b))
234
- .map((key): SettingsKeyAudit => ({
235
- key,
236
- classification: classifySettingsKey(key, schemaKeys, defaultKeys),
237
- }));
238
- const missingSchemaKeys = [...schemaKeys]
239
- .filter((key) => !(key in flatSettings))
240
- .sort((a, b) => a.localeCompare(b));
241
- const staleCandidates = keys
242
- .filter((entry) => entry.classification === 'unknown-stale-candidate')
243
- .map((entry) => entry.key);
244
-
245
- return {
246
- path,
247
- exists: settings !== null,
248
- schemaKeyCount: schemaKeys.size,
249
- leafKeyCount: keys.length,
250
- recognizedKeyCount: keys.filter((entry) => entry.classification === 'current-schema').length,
251
- missingSchemaKeys,
252
- keys,
253
- staleCandidates,
254
- };
255
- }
256
-
257
- function modeString(path: string): string {
258
- return (statSync(path).mode & 0o777).toString(8).padStart(3, '0');
259
- }
260
-
261
- function collectPermissionFindings(homeDir: string): HomeAuditFinding[] {
262
- const findings: HomeAuditFinding[] = [];
263
- const sensitiveFiles = [
264
- join(homeDir, 'tui', 'secrets.enc'),
265
- join(homeDir, 'tui', 'auth-users.json'),
266
- join(homeDir, 'daemon', 'operator-tokens.json'),
267
- join(homeDir, 'daemon', '.goodvibes', 'daemon', 'operator-tokens.json'),
268
- join(homeDir, 'daemon', '.goodvibes', 'tui', 'auth-users.json'),
269
- join(homeDir, 'daemon', '.goodvibes', 'tui', 'auth-bootstrap.txt'),
270
- ];
271
-
272
- for (const path of sensitiveFiles) {
273
- if (!existsSync(path)) continue;
274
- const mode = modeString(path);
275
- if (mode !== '600') {
276
- findings.push({
277
- severity: 'warn',
278
- code: 'sensitive-file-permissions',
279
- path,
280
- message: `Sensitive file mode is ${mode}; expected 600.`,
281
- });
282
- }
283
- }
284
-
285
- return findings;
286
- }
287
-
288
- function collectDuplicateProfilePatterns(homeDir: string): DuplicateProfilePattern[] {
289
- const profilesDir = join(homeDir, 'tui', 'profiles');
290
- if (!existsSync(profilesDir)) return [];
291
-
292
- const counts = new Map<string, number>();
293
- for (const entry of readdirSync(profilesDir, { withFileTypes: true })) {
294
- if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
295
- const normalizedName = entry.name.replace(/^(team-)+/, 'team-');
296
- counts.set(normalizedName, (counts.get(normalizedName) ?? 0) + 1);
297
- }
298
-
299
- return [...counts.entries()]
300
- .filter(([, count]) => count > 1)
301
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
302
- .map(([normalizedName, count]) => ({ normalizedName, count }));
303
- }
304
-
305
- export async function auditGoodVibesHome(options: HomeAuditOptions): Promise<GoodVibesHomeAudit> {
306
- const homeDir = resolve(options.homeDir);
307
- const { files, directoryCountByOwner } = walkFiles(homeDir);
308
- const summaries = new Map<HomeFileOwner, { files: number; directories: number; bytes: number }>();
309
- const records: HomeFileRecord[] = [];
310
-
311
- for (const file of files) {
312
- const stats = statSync(file);
313
- const relativePath = toPosixRelative(relative(homeDir, file));
314
- const owner = classifyFile(relativePath);
315
- const summary = summaries.get(owner) ?? { files: 0, directories: directoryCountByOwner.get(owner) ?? 0, bytes: 0 };
316
- summary.files += 1;
317
- summary.bytes += stats.size;
318
- summaries.set(owner, summary);
319
- records.push({
320
- relativePath,
321
- owner,
322
- bytes: stats.size,
323
- mode: (stats.mode & 0o777).toString(8).padStart(3, '0'),
324
- mtimeIso: stats.mtime.toISOString(),
325
- sha256: options.includeHashes ? await hashFile(file) : undefined,
326
- });
327
- }
328
-
329
- const settings = auditSettings(homeDir);
330
- const findings = [
331
- ...collectPermissionFindings(homeDir),
332
- ...settings.staleCandidates.map((key): HomeAuditFinding => ({
333
- severity: 'warn',
334
- code: 'stale-settings-key',
335
- path: settings.path,
336
- message: `Setting '${key}' is not in CONFIG_SCHEMA, DEFAULT_CONFIG, or known dynamic config.`,
337
- })),
338
- ];
339
-
340
- return {
341
- homeDir,
342
- generatedAt: new Date().toISOString(),
343
- summaries: [...summaries.entries()]
344
- .sort(([a], [b]) => a.localeCompare(b))
345
- .map(([owner, summary]) => ({ owner, ...summary })),
346
- files: records,
347
- settings,
348
- duplicateProfilePatterns: collectDuplicateProfilePatterns(homeDir),
349
- findings,
350
- allowedWriteRoots: [...GOODVIBES_ALLOWED_WRITE_ROOTS],
351
- readOnlyRoots: [...GOODVIBES_READ_ONLY_ROOTS],
352
- };
353
- }
354
-
355
- export async function snapshotGoodVibesHome(homeDir: string): Promise<HomeSnapshot> {
356
- const root = resolve(homeDir);
357
- const { files } = walkFiles(root);
358
- const entries: Record<string, HomeSnapshotEntry> = {};
359
- for (const file of files) {
360
- const stats = statSync(file);
361
- const relativePath = toPosixRelative(relative(root, file));
362
- entries[relativePath] = {
363
- relativePath,
364
- bytes: stats.size,
365
- mode: (stats.mode & 0o777).toString(8).padStart(3, '0'),
366
- sha256: await hashFile(file),
367
- };
368
- }
369
- return entries;
370
- }
371
-
372
- export function diffHomeSnapshots(before: HomeSnapshot, after: HomeSnapshot): HomeSnapshotDiff {
373
- const beforeKeys = new Set(Object.keys(before));
374
- const afterKeys = new Set(Object.keys(after));
375
- const added = [...afterKeys].filter((key) => !beforeKeys.has(key)).sort((a, b) => a.localeCompare(b));
376
- const removed = [...beforeKeys].filter((key) => !afterKeys.has(key)).sort((a, b) => a.localeCompare(b));
377
- const changed = [...afterKeys]
378
- .filter((key) => beforeKeys.has(key))
379
- .filter((key) => {
380
- const prior = before[key];
381
- const next = after[key];
382
- return prior.sha256 !== next.sha256 || prior.mode !== next.mode || prior.bytes !== next.bytes;
383
- })
384
- .sort((a, b) => a.localeCompare(b));
385
- return { added, removed, changed };
386
- }
387
-
388
- export function findDisallowedHomeMutations(
389
- diff: HomeSnapshotDiff,
390
- allowedRoots: readonly string[] = GOODVIBES_ALLOWED_WRITE_ROOTS,
391
- ): string[] {
392
- const touched = [...diff.added, ...diff.removed, ...diff.changed];
393
- return touched
394
- .filter((relativePath) => !allowedRoots.some((root) => relativePath.startsWith(root)))
395
- .sort((a, b) => a.localeCompare(b));
396
- }
397
-
398
- export function renderGoodVibesHomeAuditMarkdown(audit: GoodVibesHomeAudit): string {
399
- const lines: string[] = [
400
- '# GoodVibes Home Audit',
401
- '',
402
- `Generated: ${audit.generatedAt}`,
403
- `Home: \`${audit.homeDir}\``,
404
- '',
405
- '## Ownership Summary',
406
- '',
407
- '| Owner | Files | Directories | Bytes |',
408
- '|---|---:|---:|---:|',
409
- ...audit.summaries.map((summary) => (
410
- `| ${summary.owner} | ${summary.files} | ${summary.directories} | ${summary.bytes} |`
411
- )),
412
- '',
413
- '## Settings',
414
- '',
415
- `Path: \`${audit.settings.path}\``,
416
- '',
417
- '| Metric | Count |',
418
- '|---|---:|',
419
- `| Schema keys | ${audit.settings.schemaKeyCount} |`,
420
- `| Persisted leaf keys | ${audit.settings.leafKeyCount} |`,
421
- `| Current-schema keys | ${audit.settings.recognizedKeyCount} |`,
422
- `| Missing schema keys | ${audit.settings.missingSchemaKeys.length} |`,
423
- `| Stale candidates | ${audit.settings.staleCandidates.length} |`,
424
- '',
425
- ];
426
-
427
- if (audit.settings.missingSchemaKeys.length > 0) {
428
- lines.push('### Missing Schema Keys', '');
429
- for (const key of audit.settings.missingSchemaKeys) lines.push(`- \`${key}\``);
430
- lines.push('');
431
- }
432
-
433
- if (audit.settings.staleCandidates.length > 0) {
434
- lines.push('### Stale Setting Candidates', '');
435
- for (const key of audit.settings.staleCandidates) lines.push(`- \`${key}\``);
436
- lines.push('');
437
- }
438
-
439
- if (audit.duplicateProfilePatterns.length > 0) {
440
- lines.push('## Duplicate Profile Patterns', '');
441
- for (const pattern of audit.duplicateProfilePatterns) {
442
- lines.push(`- \`${pattern.normalizedName}\`: ${pattern.count}`);
443
- }
444
- lines.push('');
445
- }
446
-
447
- lines.push('## Findings', '');
448
- if (audit.findings.length === 0) {
449
- lines.push('No findings.');
450
- } else {
451
- for (const finding of audit.findings) {
452
- const path = finding.path ? ` \`${finding.path}\`` : '';
453
- lines.push(`- ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`);
454
- }
455
- }
456
- lines.push('');
457
-
458
- return `${lines.join('\n')}\n`;
459
- }
460
-
461
- export function writeAuditReportFiles(audit: GoodVibesHomeAudit, outputDir: string): void {
462
- mkdirSync(outputDir, { recursive: true });
463
- writeFileSync(join(outputDir, 'goodvibes-home-audit.json'), `${JSON.stringify(audit, null, 2)}\n`, 'utf8');
464
- writeFileSync(join(outputDir, 'goodvibes-home-audit.md'), renderGoodVibesHomeAuditMarkdown(audit), 'utf8');
465
- }
@@ -1,88 +0,0 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
2
- import {
3
- filterOperatorCapabilities,
4
- renderOperatorCapabilityBenchmark,
5
- OPERATOR_CAPABILITY_BENCHMARKS,
6
- } from '../../operator/capability-benchmark.ts';
7
- import {
8
- buildDaemonCapabilityGapReport,
9
- buildDaemonCapabilityRouteRiskReport,
10
- fetchLiveDaemonCapabilityAudit,
11
- fetchLiveDaemonCapabilityInventory,
12
- filterDaemonCapabilityAuditAreas,
13
- filterDaemonCapabilityGaps,
14
- filterDaemonCapabilityInventoryGroups,
15
- filterDaemonCapabilityRouteRiskAreas,
16
- renderDaemonCapabilityAudit,
17
- renderDaemonCapabilityFailure,
18
- renderDaemonCapabilityGaps,
19
- renderDaemonCapabilityInventory,
20
- renderDaemonCapabilityRouteRisk,
21
- } from '../../operator/daemon-capability-audit.ts';
22
- import { resolveAgentDaemonConnection } from '../../agent/routine-schedule-promotion.ts';
23
-
24
- export function registerCapabilitiesRuntimeCommands(registry: CommandRegistry): void {
25
- registry.register({
26
- name: 'capabilities',
27
- aliases: ['caps', 'benchmark'],
28
- description: 'Show the OpenClaw/Hermes capability benchmark, Agent readiness, and live daemon coverage',
29
- usage: '[daemon|gaps|openclaw|hermes|query]',
30
- async handler(args, ctx) {
31
- if (args[0] === 'daemon') {
32
- const homeDirectory = ctx.platform.configManager.getHomeDirectory() ?? process.cwd();
33
- const connection = resolveAgentDaemonConnection(ctx.platform.configManager, homeDirectory);
34
- const audit = await fetchLiveDaemonCapabilityAudit(connection);
35
- if (!audit.ok) {
36
- ctx.print(renderDaemonCapabilityFailure(audit));
37
- return;
38
- }
39
- if (args[1] === 'gaps') {
40
- const report = buildDaemonCapabilityGapReport(audit);
41
- const query = args.slice(2).join(' ').trim() || undefined;
42
- const gaps = filterDaemonCapabilityGaps(report.gaps, query);
43
- ctx.print(renderDaemonCapabilityGaps(report, gaps));
44
- return;
45
- }
46
- if (args[1] === 'risk' || args[1] === 'route-risk') {
47
- const report = buildDaemonCapabilityRouteRiskReport(audit);
48
- const query = args.slice(2).join(' ').trim() || undefined;
49
- const areas = filterDaemonCapabilityRouteRiskAreas(report.areas, query);
50
- ctx.print(renderDaemonCapabilityRouteRisk(report, areas));
51
- return;
52
- }
53
- if (args[1] === 'inventory' || args[1] === 'methods' || args[1] === 'routes') {
54
- const inventory = await fetchLiveDaemonCapabilityInventory(connection);
55
- if (!inventory.ok) {
56
- ctx.print(renderDaemonCapabilityFailure(inventory));
57
- return;
58
- }
59
- const query = args.slice(2).join(' ').trim() || undefined;
60
- const groups = filterDaemonCapabilityInventoryGroups(inventory.groups, query);
61
- ctx.print(renderDaemonCapabilityInventory(inventory, groups));
62
- return;
63
- }
64
- const query = args.slice(1).join(' ').trim() || undefined;
65
- const areas = filterDaemonCapabilityAuditAreas(audit.areas, query);
66
- ctx.print(renderDaemonCapabilityAudit(audit, areas));
67
- return;
68
- }
69
- if (args[0] === 'gaps') {
70
- const homeDirectory = ctx.platform.configManager.getHomeDirectory() ?? process.cwd();
71
- const connection = resolveAgentDaemonConnection(ctx.platform.configManager, homeDirectory);
72
- const audit = await fetchLiveDaemonCapabilityAudit(connection);
73
- if (!audit.ok) {
74
- ctx.print(renderDaemonCapabilityFailure(audit));
75
- return;
76
- }
77
- const report = buildDaemonCapabilityGapReport(audit);
78
- const query = args.slice(1).join(' ').trim() || undefined;
79
- const gaps = filterDaemonCapabilityGaps(report.gaps, query);
80
- ctx.print(renderDaemonCapabilityGaps(report, gaps));
81
- return;
82
- }
83
- const query = args.join(' ').trim() || undefined;
84
- const capabilities = filterOperatorCapabilities(OPERATOR_CAPABILITY_BENCHMARKS, query);
85
- ctx.print(renderOperatorCapabilityBenchmark(capabilities));
86
- },
87
- });
88
- }