@pellux/goodvibes-agent 0.1.101 → 0.1.103

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/docs/README.md +1 -1
  4. package/docs/getting-started.md +17 -3
  5. package/package.json +1 -18
  6. package/src/cli/help.ts +86 -0
  7. package/src/cli/local-library-command.ts +516 -0
  8. package/src/cli/management.ts +17 -0
  9. package/src/cli/memory-command.ts +646 -0
  10. package/src/cli/package-verification.ts +10 -0
  11. package/src/cli/parser.ts +8 -0
  12. package/src/cli/types.ts +3 -0
  13. package/src/input/agent-workspace-setup.ts +2 -2
  14. package/src/input/agent-workspace-snapshot.ts +4 -4
  15. package/src/input/agent-workspace-types.ts +2 -2
  16. package/src/input/command-registry.ts +0 -8
  17. package/src/input/feed-context-factory.ts +1 -3
  18. package/src/input/handler-feed.ts +1 -4
  19. package/src/input/handler-interactions.ts +0 -1
  20. package/src/input/handler-modal-stack.ts +0 -1
  21. package/src/input/handler-modal-token-routes.ts +0 -11
  22. package/src/input/handler-picker-routes.ts +11 -20
  23. package/src/input/handler-ui-state.ts +0 -6
  24. package/src/input/handler.ts +1 -17
  25. package/src/main.ts +0 -6
  26. package/src/panels/builtin/agent.ts +0 -17
  27. package/src/panels/index.ts +0 -2
  28. package/src/renderer/agent-workspace.ts +3 -3
  29. package/src/renderer/conversation-overlays.ts +0 -6
  30. package/src/renderer/live-tail-modal.ts +10 -69
  31. package/src/renderer/process-modal.ts +28 -530
  32. package/src/runtime/bootstrap-command-parts.ts +0 -28
  33. package/src/runtime/bootstrap-core.ts +1 -1
  34. package/src/runtime/bootstrap.ts +3 -12
  35. package/src/runtime/services.ts +3 -4
  36. package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
  37. package/src/version.ts +1 -1
  38. package/src/panels/agent-inspector-panel.ts +0 -521
  39. package/src/panels/agent-inspector-shared.ts +0 -94
  40. package/src/panels/agent-logs-panel.ts +0 -559
  41. package/src/panels/agent-logs-shared.ts +0 -129
  42. package/src/renderer/agent-detail-modal.ts +0 -331
  43. package/src/renderer/process-summary.ts +0 -67
@@ -0,0 +1,516 @@
1
+ import { createShellPathService } from '@/runtime/index.ts';
2
+ import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
3
+ import {
4
+ AgentSkillRegistry,
5
+ type AgentSkillBundleRecord,
6
+ type AgentSkillRecord,
7
+ } from '../agent/skill-registry.ts';
8
+ import type { CliCommandOutput } from './types.ts';
9
+ import type { CliCommandRuntime } from './management.ts';
10
+
11
+ interface CommandSuccess<TData> {
12
+ readonly ok: true;
13
+ readonly kind: string;
14
+ readonly data: TData;
15
+ }
16
+
17
+ interface CommandFailure {
18
+ readonly ok: false;
19
+ readonly kind: string;
20
+ readonly error: string;
21
+ }
22
+
23
+ interface ParsedOptions {
24
+ readonly values: ReadonlyMap<string, string>;
25
+ readonly flags: ReadonlySet<string>;
26
+ readonly positionals: readonly string[];
27
+ }
28
+
29
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
30
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
31
+ }
32
+
33
+ function success<TData>(runtime: CliCommandRuntime, kind: string, data: TData, text: string): CliCommandOutput {
34
+ const value: CommandSuccess<TData> = { ok: true, kind, data };
35
+ return { output: jsonOrText(runtime, value, text), exitCode: 0 };
36
+ }
37
+
38
+ function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCode: number): CliCommandOutput {
39
+ const value: CommandFailure = { ok: false, kind, error };
40
+ return {
41
+ output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : error,
42
+ exitCode,
43
+ };
44
+ }
45
+
46
+ function parseOptions(args: readonly string[]): ParsedOptions {
47
+ const values = new Map<string, string>();
48
+ const flags = new Set<string>();
49
+ const positionals: string[] = [];
50
+ for (let index = 0; index < args.length; index += 1) {
51
+ const arg = args[index] ?? '';
52
+ if (!arg.startsWith('--')) {
53
+ positionals.push(arg);
54
+ continue;
55
+ }
56
+ const equalIndex = arg.indexOf('=');
57
+ if (equalIndex >= 0) {
58
+ values.set(arg.slice(2, equalIndex), arg.slice(equalIndex + 1));
59
+ continue;
60
+ }
61
+ const name = arg.slice(2);
62
+ const next = args[index + 1];
63
+ if (next !== undefined && !next.startsWith('--')) {
64
+ values.set(name, next);
65
+ index += 1;
66
+ continue;
67
+ }
68
+ flags.add(name);
69
+ }
70
+ return { values, flags, positionals };
71
+ }
72
+
73
+ function optionValue(options: ParsedOptions, name: string): string | undefined {
74
+ const value = options.values.get(name);
75
+ if (value === undefined) return undefined;
76
+ const trimmed = value.trim();
77
+ return trimmed.length > 0 ? trimmed : undefined;
78
+ }
79
+
80
+ function requiredOption(options: ParsedOptions, name: string, usage: string): string {
81
+ const value = optionValue(options, name);
82
+ if (!value) throw new Error(`${usage}\nMissing --${name}.`);
83
+ return value;
84
+ }
85
+
86
+ function csvOption(options: ParsedOptions, name: string): readonly string[] | undefined {
87
+ const value = optionValue(options, name);
88
+ if (value === undefined) return undefined;
89
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
90
+ }
91
+
92
+ function hasFlag(options: ParsedOptions, name: string): boolean {
93
+ return options.flags.has(name);
94
+ }
95
+
96
+ function personaRegistry(runtime: CliCommandRuntime): AgentPersonaRegistry {
97
+ return AgentPersonaRegistry.fromShellPaths(createShellPathService({
98
+ workingDirectory: runtime.workingDirectory,
99
+ homeDirectory: runtime.homeDirectory,
100
+ }));
101
+ }
102
+
103
+ function skillRegistry(runtime: CliCommandRuntime): AgentSkillRegistry {
104
+ return AgentSkillRegistry.fromShellPaths(createShellPathService({
105
+ workingDirectory: runtime.workingDirectory,
106
+ homeDirectory: runtime.homeDirectory,
107
+ }));
108
+ }
109
+
110
+ function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
111
+ const active = persona.id === activePersonaId ? 'active' : 'available';
112
+ const tags = persona.tags.length > 0 ? ` tags=${persona.tags.join(',')}` : '';
113
+ return ` ${persona.id} ${active} ${persona.reviewState} ${persona.name} - ${persona.description}${tags}`;
114
+ }
115
+
116
+ function renderPersonaList(title: string, path: string, personas: readonly AgentPersonaRecord[], activePersonaId: string | null): string {
117
+ if (personas.length === 0) {
118
+ return [
119
+ title,
120
+ ' No local Agent personas yet.',
121
+ ' Create one with: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>',
122
+ ].join('\n');
123
+ }
124
+ return [
125
+ `${title} (${personas.length})`,
126
+ ` store: ${path}`,
127
+ ...personas.map((persona) => summarizePersona(persona, activePersonaId)),
128
+ ].join('\n');
129
+ }
130
+
131
+ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
132
+ return [
133
+ `Persona ${persona.name}`,
134
+ ` id: ${persona.id}`,
135
+ ` active: ${persona.id === activePersonaId ? 'yes' : 'no'}`,
136
+ ` review: ${persona.reviewState}`,
137
+ ` source: ${persona.source}`,
138
+ ` provenance: ${persona.provenance}`,
139
+ ` tags: ${persona.tags.join(', ') || '(none)'}`,
140
+ ` triggers: ${persona.triggers.join(', ') || '(manual)'}`,
141
+ ` created: ${persona.createdAt}`,
142
+ ` updated: ${persona.updatedAt}`,
143
+ persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
144
+ '',
145
+ persona.description,
146
+ '',
147
+ persona.body,
148
+ ].filter((line): line is string => Boolean(line)).join('\n');
149
+ }
150
+
151
+ function summarizeSkill(skill: AgentSkillRecord): string {
152
+ const enabled = skill.enabled ? 'enabled' : 'disabled';
153
+ const tags = skill.tags.length > 0 ? ` tags=${skill.tags.join(',')}` : '';
154
+ return ` ${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}${tags}`;
155
+ }
156
+
157
+ function summarizeBundle(bundle: AgentSkillBundleRecord): string {
158
+ const enabled = bundle.enabled ? 'enabled' : 'disabled';
159
+ return ` ${bundle.id} ${enabled} ${bundle.reviewState} ${bundle.name} - ${bundle.description} skills=${bundle.skillIds.join(',')}`;
160
+ }
161
+
162
+ function renderSkillList(title: string, path: string, skills: readonly AgentSkillRecord[]): string {
163
+ if (skills.length === 0) {
164
+ return [
165
+ title,
166
+ ' No local Agent skills yet.',
167
+ ' Create one with: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>',
168
+ ].join('\n');
169
+ }
170
+ return [
171
+ `${title} (${skills.length})`,
172
+ ` store: ${path}`,
173
+ ...skills.map(summarizeSkill),
174
+ ].join('\n');
175
+ }
176
+
177
+ function renderBundleList(title: string, path: string, bundles: readonly AgentSkillBundleRecord[]): string {
178
+ if (bundles.length === 0) {
179
+ return [
180
+ title,
181
+ ' No local Agent skill bundles yet.',
182
+ ' Create one with: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>',
183
+ ].join('\n');
184
+ }
185
+ return [
186
+ `${title} (${bundles.length})`,
187
+ ` store: ${path}`,
188
+ ...bundles.map(summarizeBundle),
189
+ ].join('\n');
190
+ }
191
+
192
+ function renderSkill(skill: AgentSkillRecord): string {
193
+ return [
194
+ `Skill ${skill.name}`,
195
+ ` id: ${skill.id}`,
196
+ ` enabled: ${skill.enabled ? 'yes' : 'no'}`,
197
+ ` review: ${skill.reviewState}`,
198
+ ` source: ${skill.source}`,
199
+ ` provenance: ${skill.provenance}`,
200
+ ` tags: ${skill.tags.join(', ') || '(none)'}`,
201
+ ` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
202
+ ` created: ${skill.createdAt}`,
203
+ ` updated: ${skill.updatedAt}`,
204
+ skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
205
+ '',
206
+ skill.description,
207
+ '',
208
+ skill.procedure,
209
+ ].filter((line): line is string => Boolean(line)).join('\n');
210
+ }
211
+
212
+ function renderBundle(bundle: AgentSkillBundleRecord): string {
213
+ return [
214
+ `Skill bundle ${bundle.name}`,
215
+ ` id: ${bundle.id}`,
216
+ ` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
217
+ ` review: ${bundle.reviewState}`,
218
+ ` source: ${bundle.source}`,
219
+ ` provenance: ${bundle.provenance}`,
220
+ ` skills: ${bundle.skillIds.join(', ')}`,
221
+ ` created: ${bundle.createdAt}`,
222
+ ` updated: ${bundle.updatedAt}`,
223
+ bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
224
+ '',
225
+ bundle.description,
226
+ ].filter((line): line is string => Boolean(line)).join('\n');
227
+ }
228
+
229
+ function usagePersonas(): string {
230
+ return 'Usage: goodvibes-agent personas [list|active|search <query>|show <id>|create|update <id>|use <id>|clear|review <id>|stale <id> <reason>|delete <id> --yes]';
231
+ }
232
+
233
+ function usageSkills(): string {
234
+ return 'Usage: goodvibes-agent skills [list|enabled|active|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes|bundle ...]';
235
+ }
236
+
237
+ function usageBundles(): string {
238
+ return 'Usage: goodvibes-agent skills bundle [list|enabled|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes]';
239
+ }
240
+
241
+ function errorOutput(runtime: CliCommandRuntime, error: unknown, kind: string): CliCommandOutput {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ const exitCode = message.startsWith('Usage:') || message.includes('\nMissing --') ? 2 : 1;
244
+ return failure(runtime, kind, message, exitCode);
245
+ }
246
+
247
+ export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
248
+ try {
249
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
250
+ const normalized = sub.toLowerCase();
251
+ const registry = personaRegistry(runtime);
252
+ const snapshot = registry.snapshot();
253
+ if (normalized === 'list' || normalized === 'ls') {
254
+ return success(runtime, 'agent.personas.list', snapshot, renderPersonaList('Agent personas', snapshot.path, snapshot.personas, snapshot.activePersonaId));
255
+ }
256
+ if (normalized === 'active') {
257
+ const active = snapshot.activePersona;
258
+ if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
259
+ return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
260
+ }
261
+ if (normalized === 'search' || normalized === 'find') {
262
+ const query = rest.join(' ').trim();
263
+ const results = registry.search(query);
264
+ return success(runtime, 'agent.personas.search', { query, results }, renderPersonaList(`Agent personas matching "${query}"`, snapshot.path, results, snapshot.activePersonaId));
265
+ }
266
+ if (normalized === 'show' || normalized === 'get') {
267
+ const id = rest[0];
268
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas show <id>', 2);
269
+ const persona = registry.get(id);
270
+ if (!persona) return failure(runtime, 'persona_not_found', `Unknown Agent persona: ${id}`, 1);
271
+ return success(runtime, 'agent.personas.show', persona, renderPersona(persona, snapshot.activePersonaId));
272
+ }
273
+ if (normalized === 'create') {
274
+ const options = parseOptions(rest);
275
+ const persona = registry.create({
276
+ name: requiredOption(options, 'name', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
277
+ description: requiredOption(options, 'description', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
278
+ body: requiredOption(options, 'body', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
279
+ tags: csvOption(options, 'tags'),
280
+ triggers: csvOption(options, 'triggers'),
281
+ provenance: optionValue(options, 'provenance') ?? 'cli',
282
+ });
283
+ if (hasFlag(options, 'use')) registry.setActive(persona.id);
284
+ return success(runtime, 'agent.personas.create', persona, `Agent persona created: ${persona.id}${hasFlag(options, 'use') ? ' (active)' : ''}`);
285
+ }
286
+ if (normalized === 'update') {
287
+ const id = rest[0];
288
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas update <id> [--name ...] [--description ...] [--body ...]', 2);
289
+ const options = parseOptions(rest.slice(1));
290
+ const persona = registry.update(id, {
291
+ name: optionValue(options, 'name'),
292
+ description: optionValue(options, 'description'),
293
+ body: optionValue(options, 'body'),
294
+ tags: csvOption(options, 'tags'),
295
+ triggers: csvOption(options, 'triggers'),
296
+ provenance: optionValue(options, 'provenance'),
297
+ });
298
+ return success(runtime, 'agent.personas.update', persona, `Agent persona updated: ${persona.id}`);
299
+ }
300
+ if (normalized === 'use') {
301
+ const id = rest[0];
302
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas use <id>', 2);
303
+ const persona = registry.setActive(id);
304
+ return success(runtime, 'agent.personas.use', persona, `Active Agent persona: ${persona.id}`);
305
+ }
306
+ if (normalized === 'clear') {
307
+ registry.clearActive();
308
+ return success(runtime, 'agent.personas.clear', { activePersonaId: null }, 'Active Agent persona cleared.');
309
+ }
310
+ if (normalized === 'review') {
311
+ const id = rest[0];
312
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas review <id>', 2);
313
+ const persona = registry.markReviewed(id);
314
+ return success(runtime, 'agent.personas.review', persona, `Agent persona reviewed: ${persona.id}`);
315
+ }
316
+ if (normalized === 'stale') {
317
+ const id = rest[0];
318
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas stale <id> <reason>', 2);
319
+ const persona = registry.markStale(id, rest.slice(1).join(' '));
320
+ return success(runtime, 'agent.personas.stale', persona, `Agent persona marked stale: ${persona.id}`);
321
+ }
322
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
323
+ const options = parseOptions(rest);
324
+ const id = options.positionals[0];
325
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas delete <id> --yes', 2);
326
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent persona ${id} without --yes.`, 2);
327
+ const persona = registry.deletePersona(id);
328
+ return success(runtime, 'agent.personas.delete', persona, `Agent persona deleted: ${persona.id}`);
329
+ }
330
+ return failure(runtime, 'invalid_persona_command', usagePersonas(), 2);
331
+ } catch (error) {
332
+ return errorOutput(runtime, error, 'agent.personas.error');
333
+ }
334
+ }
335
+
336
+ function skillPayloadFromOptions(options: ParsedOptions): {
337
+ readonly name: string;
338
+ readonly description: string;
339
+ readonly procedure: string;
340
+ readonly tags: readonly string[] | undefined;
341
+ readonly triggers: readonly string[] | undefined;
342
+ readonly enabled: boolean | undefined;
343
+ readonly provenance: string;
344
+ } {
345
+ const usage = 'Usage: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>';
346
+ return {
347
+ name: requiredOption(options, 'name', usage),
348
+ description: requiredOption(options, 'description', usage),
349
+ procedure: requiredOption(options, 'procedure', usage),
350
+ tags: csvOption(options, 'tags'),
351
+ triggers: csvOption(options, 'triggers'),
352
+ enabled: hasFlag(options, 'enabled') ? true : undefined,
353
+ provenance: optionValue(options, 'provenance') ?? 'cli',
354
+ };
355
+ }
356
+
357
+ async function handleSkillBundleCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
358
+ try {
359
+ const [sub = 'list', ...rest] = args;
360
+ const normalized = sub.toLowerCase();
361
+ const registry = skillRegistry(runtime);
362
+ const snapshot = registry.snapshot();
363
+ if (normalized === 'list' || normalized === 'ls') {
364
+ return success(runtime, 'agent.skills.bundles.list', { path: snapshot.path, bundles: snapshot.bundles }, renderBundleList('Agent skill bundles', snapshot.path, snapshot.bundles));
365
+ }
366
+ if (normalized === 'enabled') {
367
+ return success(runtime, 'agent.skills.bundles.enabled', { path: snapshot.path, bundles: snapshot.enabledBundles }, renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles));
368
+ }
369
+ if (normalized === 'search' || normalized === 'find') {
370
+ const query = rest.join(' ').trim();
371
+ const results = registry.searchBundles(query);
372
+ return success(runtime, 'agent.skills.bundles.search', { query, results }, renderBundleList(`Agent skill bundles matching "${query}"`, snapshot.path, results));
373
+ }
374
+ if (normalized === 'show' || normalized === 'get') {
375
+ const id = rest[0];
376
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle show <id>', 2);
377
+ const bundle = registry.getBundle(id);
378
+ if (!bundle) return failure(runtime, 'skill_bundle_not_found', `Unknown Agent skill bundle: ${id}`, 1);
379
+ return success(runtime, 'agent.skills.bundles.show', bundle, renderBundle(bundle));
380
+ }
381
+ if (normalized === 'create') {
382
+ const options = parseOptions(rest);
383
+ const usage = 'Usage: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>';
384
+ const bundle = registry.createBundle({
385
+ name: requiredOption(options, 'name', usage),
386
+ description: requiredOption(options, 'description', usage),
387
+ skillIds: requiredOption(options, 'skills', usage).split(',').map((entry) => entry.trim()).filter(Boolean),
388
+ enabled: hasFlag(options, 'enabled'),
389
+ provenance: optionValue(options, 'provenance') ?? 'cli',
390
+ });
391
+ return success(runtime, 'agent.skills.bundles.create', bundle, `Agent skill bundle created: ${bundle.id}`);
392
+ }
393
+ if (normalized === 'update') {
394
+ const id = rest[0];
395
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle update <id> [--name ...] [--description ...] [--skills id,id]', 2);
396
+ const options = parseOptions(rest.slice(1));
397
+ const bundle = registry.updateBundle(id, {
398
+ name: optionValue(options, 'name'),
399
+ description: optionValue(options, 'description'),
400
+ skillIds: csvOption(options, 'skills'),
401
+ provenance: optionValue(options, 'provenance'),
402
+ });
403
+ return success(runtime, 'agent.skills.bundles.update', bundle, `Agent skill bundle updated: ${bundle.id}`);
404
+ }
405
+ if (normalized === 'enable' || normalized === 'disable') {
406
+ const id = rest[0];
407
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', `Usage: goodvibes-agent skills bundle ${normalized} <id>`, 2);
408
+ const bundle = registry.setBundleEnabled(id, normalized === 'enable');
409
+ return success(runtime, `agent.skills.bundles.${normalized}`, bundle, `Agent skill bundle ${normalized}d: ${bundle.id}`);
410
+ }
411
+ if (normalized === 'review') {
412
+ const id = rest[0];
413
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle review <id>', 2);
414
+ const bundle = registry.markBundleReviewed(id);
415
+ return success(runtime, 'agent.skills.bundles.review', bundle, `Agent skill bundle reviewed: ${bundle.id}`);
416
+ }
417
+ if (normalized === 'stale') {
418
+ const id = rest[0];
419
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle stale <id> <reason>', 2);
420
+ const bundle = registry.markBundleStale(id, rest.slice(1).join(' '));
421
+ return success(runtime, 'agent.skills.bundles.stale', bundle, `Agent skill bundle marked stale: ${bundle.id}`);
422
+ }
423
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
424
+ const options = parseOptions(rest);
425
+ const id = options.positionals[0];
426
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle delete <id> --yes', 2);
427
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill bundle ${id} without --yes.`, 2);
428
+ const bundle = registry.deleteBundle(id);
429
+ return success(runtime, 'agent.skills.bundles.delete', bundle, `Agent skill bundle deleted: ${bundle.id}`);
430
+ }
431
+ return failure(runtime, 'invalid_skill_bundle_command', usageBundles(), 2);
432
+ } catch (error) {
433
+ return errorOutput(runtime, error, 'agent.skills.bundles.error');
434
+ }
435
+ }
436
+
437
+ export async function handleSkillsCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
438
+ try {
439
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
440
+ const normalized = sub.toLowerCase();
441
+ if (normalized === 'bundle' || normalized === 'bundles') return handleSkillBundleCommand(runtime, rest);
442
+ const registry = skillRegistry(runtime);
443
+ const snapshot = registry.snapshot();
444
+ if (normalized === 'list' || normalized === 'ls') {
445
+ return success(runtime, 'agent.skills.list', { path: snapshot.path, skills: snapshot.skills, enabledCount: snapshot.enabledSkills.length }, renderSkillList('Agent skills', snapshot.path, snapshot.skills));
446
+ }
447
+ if (normalized === 'enabled') {
448
+ return success(runtime, 'agent.skills.enabled', { path: snapshot.path, skills: snapshot.enabledSkills }, renderSkillList('Enabled Agent skills', snapshot.path, snapshot.enabledSkills));
449
+ }
450
+ if (normalized === 'active') {
451
+ return success(runtime, 'agent.skills.active', { path: snapshot.path, skills: snapshot.activeSkills, bundles: snapshot.enabledBundles }, [
452
+ renderSkillList('Active Agent skills', snapshot.path, snapshot.activeSkills),
453
+ snapshot.enabledBundles.length > 0 ? renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles) : '',
454
+ ].filter(Boolean).join('\n\n'));
455
+ }
456
+ if (normalized === 'search' || normalized === 'find') {
457
+ const query = rest.join(' ').trim();
458
+ const results = registry.search(query);
459
+ return success(runtime, 'agent.skills.search', { query, results }, renderSkillList(`Agent skills matching "${query}"`, snapshot.path, results));
460
+ }
461
+ if (normalized === 'show' || normalized === 'get') {
462
+ const id = rest[0];
463
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills show <id>', 2);
464
+ const skill = registry.get(id);
465
+ if (!skill) return failure(runtime, 'skill_not_found', `Unknown Agent skill: ${id}`, 1);
466
+ return success(runtime, 'agent.skills.show', skill, renderSkill(skill));
467
+ }
468
+ if (normalized === 'create') {
469
+ const skill = registry.create(skillPayloadFromOptions(parseOptions(rest)));
470
+ return success(runtime, 'agent.skills.create', skill, `Agent skill created: ${skill.id}${skill.enabled ? ' (enabled)' : ''}`);
471
+ }
472
+ if (normalized === 'update') {
473
+ const id = rest[0];
474
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills update <id> [--name ...] [--description ...] [--procedure ...]', 2);
475
+ const options = parseOptions(rest.slice(1));
476
+ const skill = registry.update(id, {
477
+ name: optionValue(options, 'name'),
478
+ description: optionValue(options, 'description'),
479
+ procedure: optionValue(options, 'procedure'),
480
+ tags: csvOption(options, 'tags'),
481
+ triggers: csvOption(options, 'triggers'),
482
+ provenance: optionValue(options, 'provenance'),
483
+ });
484
+ return success(runtime, 'agent.skills.update', skill, `Agent skill updated: ${skill.id}`);
485
+ }
486
+ if (normalized === 'enable' || normalized === 'disable') {
487
+ const id = rest[0];
488
+ if (!id) return failure(runtime, 'invalid_skill_command', `Usage: goodvibes-agent skills ${normalized} <id>`, 2);
489
+ const skill = registry.setEnabled(id, normalized === 'enable');
490
+ return success(runtime, `agent.skills.${normalized}`, skill, `Agent skill ${normalized}d: ${skill.id}`);
491
+ }
492
+ if (normalized === 'review') {
493
+ const id = rest[0];
494
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills review <id>', 2);
495
+ const skill = registry.markReviewed(id);
496
+ return success(runtime, 'agent.skills.review', skill, `Agent skill reviewed: ${skill.id}`);
497
+ }
498
+ if (normalized === 'stale') {
499
+ const id = rest[0];
500
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills stale <id> <reason>', 2);
501
+ const skill = registry.markStale(id, rest.slice(1).join(' '));
502
+ return success(runtime, 'agent.skills.stale', skill, `Agent skill marked stale: ${skill.id}`);
503
+ }
504
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
505
+ const options = parseOptions(rest);
506
+ const id = options.positionals[0];
507
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills delete <id> --yes', 2);
508
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill ${id} without --yes.`, 2);
509
+ const skill = registry.deleteSkill(id);
510
+ return success(runtime, 'agent.skills.delete', skill, `Agent skill deleted: ${skill.id}`);
511
+ }
512
+ return failure(runtime, 'invalid_skill_command', usageSkills(), 2);
513
+ } catch (error) {
514
+ return errorOutput(runtime, error, 'agent.skills.error');
515
+ }
516
+ }
@@ -30,6 +30,8 @@ import type { RuntimeEndpointId } from './endpoints.ts';
30
30
  import { handleBundleCommand } from './bundle-command.ts';
31
31
  import { handleSecrets, handleSessions, handleTasks, renderPairing, renderSubscriptions } from './management-commands.ts';
32
32
  import { handleAgentKnowledgeCommand, handleAgentKnowledgeShortcutCommand, handleCompatCommand, handleDelegateCommand } from './agent-knowledge-command.ts';
33
+ import { handlePersonasCommand, handleSkillsCommand } from './local-library-command.ts';
34
+ import { handleMemoryCommand } from './memory-command.ts';
33
35
  import { handleProfilesCommand } from './profiles-command.ts';
34
36
  import { handleRoutinesCommand } from './routines-command.ts';
35
37
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
@@ -615,6 +617,21 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
615
617
  console.log(result.output);
616
618
  return { handled: true, exitCode: result.exitCode };
617
619
  }
620
+ case 'personas': {
621
+ const result = await handlePersonasCommand(runtime);
622
+ console.log(result.output);
623
+ return { handled: true, exitCode: result.exitCode };
624
+ }
625
+ case 'skills': {
626
+ const result = await handleSkillsCommand(runtime);
627
+ console.log(result.output);
628
+ return { handled: true, exitCode: result.exitCode };
629
+ }
630
+ case 'memory': {
631
+ const result = await handleMemoryCommand(runtime);
632
+ console.log(result.output);
633
+ return { handled: true, exitCode: result.exitCode };
634
+ }
618
635
  case 'routines': {
619
636
  const result = await handleRoutinesCommand(runtime);
620
637
  console.log(result.output);