@pellux/goodvibes-agent 1.5.5 → 1.5.6
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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/package/main.js +94130 -79335
- package/docs/tools-and-commands.md +2 -1
- package/package.json +6 -3
- package/src/agent/autonomy-schedule-format.ts +1 -1
- package/src/agent/autonomy-schedule.ts +3 -3
- package/src/agent/calendar/calendar-oauth-service.ts +324 -0
- package/src/agent/calendar-subscription-registry.ts +497 -0
- package/src/agent/email/email-service.ts +57 -0
- package/src/agent/email/smtp-client.ts +30 -10
- package/src/agent/memory-prompt.ts +143 -12
- package/src/agent/operator-actions.ts +26 -22
- package/src/agent/prompt-context-receipts.ts +45 -18
- package/src/agent/reminder-schedule-format.ts +1 -1
- package/src/agent/reminder-schedule.ts +3 -3
- package/src/agent/routine-schedule-format.ts +2 -2
- package/src/agent/routine-schedule-promotion.ts +5 -5
- package/src/agent/routine-schedule-receipts.ts +2 -2
- package/src/agent/schedule-edit-format.ts +1 -1
- package/src/agent/schedule-edit.ts +5 -5
- package/src/agent/session-registration.ts +281 -0
- package/src/agent/vibe-file.ts +50 -0
- package/src/cli/local-library-command-shared.ts +146 -0
- package/src/cli/management.ts +2 -1
- package/src/cli/personas-command.ts +252 -0
- package/src/cli/resume-relaunch-notice.ts +151 -0
- package/src/cli/routines-command.ts +25 -2
- package/src/cli/service-posture.ts +3 -2
- package/src/cli/skill-bundle-command.ts +175 -0
- package/src/cli/skills-command.ts +309 -0
- package/src/cli/status.ts +43 -7
- package/src/cli/tui-startup.ts +24 -1
- package/src/config/agent-settings-policy.ts +0 -1
- package/src/config/secret-config.ts +4 -0
- package/src/core/conversation-rendering.ts +20 -11
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/system-message-noise.ts +108 -0
- package/src/core/system-message-router.ts +54 -1
- package/src/core/thinking-overlay.ts +83 -0
- package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
- package/src/input/agent-workspace-access-command-editors.ts +108 -128
- package/src/input/agent-workspace-activation.ts +15 -0
- package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
- package/src/input/agent-workspace-basic-command-editors.ts +433 -564
- package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
- package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
- package/src/input/agent-workspace-categories.ts +7 -2
- package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
- package/src/input/agent-workspace-channel-command-editors.ts +42 -46
- package/src/input/agent-workspace-command-editor-engine.ts +133 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
- package/src/input/agent-workspace-email-connect-editor.ts +162 -0
- package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
- package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
- package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
- package/src/input/agent-workspace-library-command-editors.ts +16 -2
- package/src/input/agent-workspace-live-counters.ts +55 -0
- package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
- package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
- package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
- package/src/input/agent-workspace-media-command-editors.ts +10 -8
- package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
- package/src/input/agent-workspace-memory-command-editors.ts +116 -141
- package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
- package/src/input/agent-workspace-operations-command-editors.ts +150 -162
- package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
- package/src/input/agent-workspace-provider-command-editors.ts +58 -68
- package/src/input/agent-workspace-search.ts +8 -1
- package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
- package/src/input/agent-workspace-session-command-editors.ts +160 -195
- package/src/input/agent-workspace-settings.ts +40 -1
- package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
- package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
- package/src/input/agent-workspace-snapshot-builders.ts +431 -0
- package/src/input/agent-workspace-snapshot-config.ts +43 -0
- package/src/input/agent-workspace-snapshot.ts +198 -432
- package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
- package/src/input/agent-workspace-task-command-editors.ts +27 -28
- package/src/input/agent-workspace-types.ts +35 -1
- package/src/input/agent-workspace.ts +5 -14
- package/src/input/command-registry.ts +23 -1
- package/src/input/commands/calendar-connect-runtime.ts +226 -0
- package/src/input/commands/calendar-runtime.ts +110 -7
- package/src/input/commands/calendar-subscription-runtime.ts +225 -0
- package/src/input/commands/email-runtime.ts +100 -40
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/network-scan-runtime.ts +75 -0
- package/src/input/commands/operator-actions-runtime.ts +9 -6
- package/src/input/commands/personas-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/input/commands/session-content.ts +13 -1
- package/src/input/commands/session-workflow.ts +15 -20
- package/src/input/commands/session.ts +3 -1
- package/src/input/commands/shell-core.ts +20 -6
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +10 -0
- package/src/input/handler-feed.ts +87 -0
- package/src/input/handler-modal-routes.ts +6 -1
- package/src/input/handler.ts +5 -0
- package/src/input/panel-paste-flood-guard.ts +94 -0
- package/src/input/settings-modal-types.ts +5 -3
- package/src/input/settings-modal.ts +21 -0
- package/src/main.ts +36 -36
- package/src/permissions/approval-posture.ts +141 -0
- package/src/renderer/agent-workspace-context-lines.ts +10 -2
- package/src/renderer/compositor.ts +27 -4
- package/src/renderer/diff.ts +61 -18
- package/src/renderer/fullscreen-primitives.ts +37 -18
- package/src/renderer/markdown.ts +20 -10
- package/src/renderer/modal-factory.ts +25 -15
- package/src/renderer/overlay-box.ts +23 -12
- package/src/renderer/process-indicator.ts +8 -3
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -0
- package/src/renderer/startup-theme-probe.ts +35 -0
- package/src/renderer/status-glyphs.ts +11 -15
- package/src/renderer/system-message.ts +17 -2
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/terminal-bg-probe.ts +373 -0
- package/src/renderer/terminal-escapes.ts +24 -0
- package/src/renderer/theme-mode-config.ts +87 -0
- package/src/renderer/theme.ts +241 -0
- package/src/renderer/thinking.ts +12 -3
- package/src/renderer/tool-call.ts +7 -3
- package/src/renderer/ui-factory.ts +92 -36
- package/src/renderer/ui-primitives.ts +33 -93
- package/src/runtime/bootstrap-core.ts +15 -0
- package/src/runtime/bootstrap-hook-bridge.ts +6 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/bootstrap.ts +68 -5
- package/src/runtime/calendar-boot-refresh.ts +105 -0
- package/src/runtime/lan-scan-consent.ts +253 -0
- package/src/runtime/services.ts +127 -2
- package/src/runtime/session-spine-rest-transport.ts +160 -0
- package/src/runtime/terminal-output-guard.ts +6 -1
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/agent-workspace-fullscreen.ts +5 -0
- package/src/shell/terminal-focus-mode.ts +120 -0
- package/src/shell/ui-openers.ts +13 -4
- package/src/tools/agent-harness-autonomy-intake.ts +5 -5
- package/src/tools/agent-harness-autonomy-queue.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +2 -1
- package/src/tools/agent-harness-metadata.ts +16 -8
- package/src/tools/agent-harness-operator-methods.ts +44 -8
- package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
- package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
- package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
- package/src/tools/agent-harness-personal-ops-types.ts +6 -0
- package/src/tools/agent-harness-prompt-context.ts +26 -12
- package/src/tools/agent-harness-workspace-actions.ts +4 -0
- package/src/tools/agent-local-registry-args.ts +117 -0
- package/src/tools/agent-local-registry-memory.ts +227 -0
- package/src/tools/agent-local-registry-tool.ts +19 -237
- package/src/tools/agent-operator-briefing-tool.ts +2 -2
- package/src/tools/agent-operator-method-tool.ts +13 -0
- package/src/tools/agent-policy-explanation.ts +39 -4
- package/src/tools/agent-schedule-tool.ts +5 -5
- package/src/utils/terminal-width.ts +98 -1
- package/src/version.ts +1 -1
- package/src/cli/local-library-command.ts +0 -825
|
@@ -1,825 +0,0 @@
|
|
|
1
|
-
import { createShellPathService } from '@/runtime/index.ts';
|
|
2
|
-
import { discoverPersonas, type DiscoveredPersonaRecord } from '../agent/persona-discovery.ts';
|
|
3
|
-
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
4
|
-
import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
5
|
-
import { discoverSkills, type SkillRecord } from '../agent/skill-discovery.ts';
|
|
6
|
-
import {
|
|
7
|
-
AgentSkillRegistry,
|
|
8
|
-
buildAgentSkillRequirements,
|
|
9
|
-
evaluateAgentSkillBundleReadiness,
|
|
10
|
-
evaluateAgentSkillReadiness,
|
|
11
|
-
formatAgentSkillRequirement,
|
|
12
|
-
type AgentSkillBundleRecord,
|
|
13
|
-
type AgentSkillRecord,
|
|
14
|
-
} from '../agent/skill-registry.ts';
|
|
15
|
-
import type { CliCommandOutput } from './types.ts';
|
|
16
|
-
import type { CliCommandRuntime } from './management.ts';
|
|
17
|
-
|
|
18
|
-
interface CommandSuccess<TData> {
|
|
19
|
-
readonly ok: true;
|
|
20
|
-
readonly kind: string;
|
|
21
|
-
readonly data: TData;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface CommandFailure {
|
|
25
|
-
readonly ok: false;
|
|
26
|
-
readonly kind: string;
|
|
27
|
-
readonly error: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface ParsedOptions {
|
|
31
|
-
readonly values: ReadonlyMap<string, string>;
|
|
32
|
-
readonly flags: ReadonlySet<string>;
|
|
33
|
-
readonly positionals: readonly string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const LOCAL_LIBRARY_VALUE_OPTIONS = [
|
|
37
|
-
'name',
|
|
38
|
-
'description',
|
|
39
|
-
'body',
|
|
40
|
-
'procedure',
|
|
41
|
-
'tags',
|
|
42
|
-
'triggers',
|
|
43
|
-
'requires-env',
|
|
44
|
-
'requires-command',
|
|
45
|
-
'requires-commands',
|
|
46
|
-
'skills',
|
|
47
|
-
'provenance',
|
|
48
|
-
] as const;
|
|
49
|
-
|
|
50
|
-
function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
|
|
51
|
-
return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function success<TData>(runtime: CliCommandRuntime, kind: string, data: TData, text: string): CliCommandOutput {
|
|
55
|
-
const value: CommandSuccess<TData> = { ok: true, kind, data };
|
|
56
|
-
return { output: jsonOrText(runtime, value, text), exitCode: 0 };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCode: number): CliCommandOutput {
|
|
60
|
-
const value: CommandFailure = { ok: false, kind, error };
|
|
61
|
-
return {
|
|
62
|
-
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : error,
|
|
63
|
-
exitCode,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseOptions(args: readonly string[], valueOptions: readonly string[] = LOCAL_LIBRARY_VALUE_OPTIONS): ParsedOptions {
|
|
68
|
-
const valued = new Set(valueOptions);
|
|
69
|
-
const values = new Map<string, string>();
|
|
70
|
-
const flags = new Set<string>();
|
|
71
|
-
const positionals: string[] = [];
|
|
72
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
73
|
-
const arg = args[index] ?? '';
|
|
74
|
-
if (arg === '--') {
|
|
75
|
-
positionals.push(...args.slice(index + 1));
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
if (!arg.startsWith('--')) {
|
|
79
|
-
positionals.push(arg);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const raw = arg.slice(2);
|
|
83
|
-
const equalIndex = raw.indexOf('=');
|
|
84
|
-
if (equalIndex >= 0) {
|
|
85
|
-
values.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const name = raw;
|
|
89
|
-
const next = args[index + 1];
|
|
90
|
-
if (next !== undefined && (valued.has(name) || !next.startsWith('--'))) {
|
|
91
|
-
values.set(name, next);
|
|
92
|
-
index += 1;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
flags.add(name);
|
|
96
|
-
}
|
|
97
|
-
return { values, flags, positionals };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function optionValue(options: ParsedOptions, name: string): string | undefined {
|
|
101
|
-
const value = options.values.get(name);
|
|
102
|
-
if (value === undefined) return undefined;
|
|
103
|
-
const trimmed = value.trim();
|
|
104
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function requiredOption(options: ParsedOptions, name: string, usage: string): string {
|
|
108
|
-
const value = optionValue(options, name);
|
|
109
|
-
if (!value) throw new Error(`${usage}\nMissing --${name}.`);
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function csvOption(options: ParsedOptions, name: string): readonly string[] | undefined {
|
|
114
|
-
const value = optionValue(options, name);
|
|
115
|
-
if (value === undefined) return undefined;
|
|
116
|
-
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function hasFlag(options: ParsedOptions, name: string): boolean {
|
|
120
|
-
return options.flags.has(name);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function personaRegistry(runtime: CliCommandRuntime): AgentPersonaRegistry {
|
|
124
|
-
return AgentPersonaRegistry.fromShellPaths(createShellPathService({
|
|
125
|
-
workingDirectory: runtime.workingDirectory,
|
|
126
|
-
homeDirectory: runtime.homeDirectory,
|
|
127
|
-
}));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function skillRegistry(runtime: CliCommandRuntime): AgentSkillRegistry {
|
|
131
|
-
return AgentSkillRegistry.fromShellPaths(createShellPathService({
|
|
132
|
-
workingDirectory: runtime.workingDirectory,
|
|
133
|
-
homeDirectory: runtime.homeDirectory,
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function shellPaths(runtime: CliCommandRuntime): ReturnType<typeof createShellPathService> {
|
|
138
|
-
return createShellPathService({
|
|
139
|
-
workingDirectory: runtime.workingDirectory,
|
|
140
|
-
homeDirectory: runtime.homeDirectory,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
|
|
145
|
-
const active = persona.id === activePersonaId ? 'active' : 'available';
|
|
146
|
-
const tags = persona.tags.length > 0 ? ` tags ${persona.tags.join(', ')}` : '';
|
|
147
|
-
return ` ${persona.id} ${active} ${formatAgentRecordReviewState(persona.reviewState)} ${persona.name} - ${persona.description}${tags}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderPersonaList(title: string, path: string, personas: readonly AgentPersonaRecord[], activePersonaId: string | null): string {
|
|
151
|
-
if (personas.length === 0) {
|
|
152
|
-
return [
|
|
153
|
-
title,
|
|
154
|
-
' No local Agent personas yet.',
|
|
155
|
-
' No Agent-local personas yet.',
|
|
156
|
-
' Create one with: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>',
|
|
157
|
-
].join('\n');
|
|
158
|
-
}
|
|
159
|
-
return [
|
|
160
|
-
`${title} (${personas.length})`,
|
|
161
|
-
` store ${path}`,
|
|
162
|
-
...personas.map((persona) => summarizePersona(persona, activePersonaId)),
|
|
163
|
-
].join('\n');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
|
|
167
|
-
return [
|
|
168
|
-
`Persona ${persona.name}`,
|
|
169
|
-
` id ${persona.id}`,
|
|
170
|
-
` active: ${persona.id === activePersonaId ? 'yes' : 'no'}`,
|
|
171
|
-
` review: ${formatAgentRecordReviewState(persona.reviewState)}`,
|
|
172
|
-
` origin: ${formatAgentRecordOrigin(persona.source, persona.provenance)}`,
|
|
173
|
-
` tags: ${persona.tags.join(', ') || '(none)'}`,
|
|
174
|
-
` triggers: ${persona.triggers.join(', ') || '(manual)'}`,
|
|
175
|
-
` created: ${persona.createdAt}`,
|
|
176
|
-
` updated: ${persona.updatedAt}`,
|
|
177
|
-
persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
|
|
178
|
-
'',
|
|
179
|
-
persona.description,
|
|
180
|
-
'',
|
|
181
|
-
persona.body,
|
|
182
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function summarizeDiscoveredPersona(persona: DiscoveredPersonaRecord): string {
|
|
186
|
-
const description = persona.description ? ` - ${persona.description}` : '';
|
|
187
|
-
return [
|
|
188
|
-
` ${persona.name} ${persona.origin}${description}`,
|
|
189
|
-
` path ${persona.path}`,
|
|
190
|
-
].join('\n');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function renderDiscoveredPersonaList(personas: readonly DiscoveredPersonaRecord[]): string {
|
|
194
|
-
if (personas.length === 0) {
|
|
195
|
-
return [
|
|
196
|
-
'Discovered Agent persona files',
|
|
197
|
-
' No persona markdown files found in Agent persona folders.',
|
|
198
|
-
' Search roots: .goodvibes/personas, .goodvibes/agent/personas, ~/.goodvibes/personas, ~/.goodvibes/agent/personas',
|
|
199
|
-
].join('\n');
|
|
200
|
-
}
|
|
201
|
-
return [
|
|
202
|
-
`Discovered Agent persona files (${personas.length})`,
|
|
203
|
-
...personas.map(summarizeDiscoveredPersona),
|
|
204
|
-
'',
|
|
205
|
-
'Import one with: goodvibes-agent personas import-discovered <name> --yes',
|
|
206
|
-
].join('\n');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function discoveredPersonaLookupValues(persona: DiscoveredPersonaRecord): readonly string[] {
|
|
210
|
-
const slug = persona.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
211
|
-
const basename = persona.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
212
|
-
return [persona.name, slug, persona.path, basename]
|
|
213
|
-
.map((value) => value.trim().toLowerCase())
|
|
214
|
-
.filter(Boolean);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function findDiscoveredPersona(personas: readonly DiscoveredPersonaRecord[], idOrName: string): DiscoveredPersonaRecord | null {
|
|
218
|
-
const lookup = idOrName.trim().toLowerCase();
|
|
219
|
-
if (!lookup) return null;
|
|
220
|
-
return personas.find((persona) => discoveredPersonaLookupValues(persona).includes(lookup)) ?? null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function discoveredPersonaFrontmatterList(persona: DiscoveredPersonaRecord, key: string): readonly string[] {
|
|
224
|
-
const value = persona.frontmatter[key];
|
|
225
|
-
if (!value) return [];
|
|
226
|
-
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function summarizeSkill(skill: AgentSkillRecord): string {
|
|
230
|
-
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
231
|
-
const tags = skill.tags.length > 0 ? ` tags ${skill.tags.join(', ')}` : '';
|
|
232
|
-
const readiness = evaluateAgentSkillReadiness(skill);
|
|
233
|
-
const ready = readiness.ready ? 'ready' : `needs ${readiness.missing.map(formatAgentSkillRequirement).join(',')}`;
|
|
234
|
-
return ` ${skill.id} ${enabled} ${formatAgentRecordReviewState(skill.reviewState)} ${ready} ${skill.name} - ${skill.description}${tags}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function summarizeBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
|
|
238
|
-
const enabled = bundle.enabled ? 'enabled' : 'disabled';
|
|
239
|
-
const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
|
|
240
|
-
const missing = [
|
|
241
|
-
...readiness.missingRequirements.map(formatAgentSkillRequirement),
|
|
242
|
-
...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
|
|
243
|
-
];
|
|
244
|
-
const ready = readiness.ready ? 'ready' : `needs ${missing.join(',')}`;
|
|
245
|
-
return ` ${bundle.id} ${enabled} ${formatAgentRecordReviewState(bundle.reviewState)} ${ready} ${bundle.name} - ${bundle.description} skills ${bundle.skillIds.join(',')}`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function renderSkillList(title: string, path: string, skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
|
|
249
|
-
if (skills.length === 0) {
|
|
250
|
-
return [
|
|
251
|
-
title,
|
|
252
|
-
emptyMessage ? '' : ' No local Agent skills yet.',
|
|
253
|
-
` ${emptyMessage ?? 'No Agent-local skills yet.'}`,
|
|
254
|
-
emptyMessage ? '' : ' Create one with: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>',
|
|
255
|
-
].filter(Boolean).join('\n');
|
|
256
|
-
}
|
|
257
|
-
return [
|
|
258
|
-
`${title} (${skills.length})`,
|
|
259
|
-
` store ${path}`,
|
|
260
|
-
...skills.map(summarizeSkill),
|
|
261
|
-
].join('\n');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function summarizeDiscoveredSkill(skill: SkillRecord): string {
|
|
265
|
-
const description = skill.description ? ` - ${skill.description}` : '';
|
|
266
|
-
const dependencies = skill.dependencies.length > 0 ? ` dependencies ${skill.dependencies.join(', ')}` : '';
|
|
267
|
-
const includes = skill.includes.length > 0 ? ` includes ${skill.includes.join(', ')}` : '';
|
|
268
|
-
return [
|
|
269
|
-
` ${skill.name} ${skill.origin}${description}${dependencies}${includes}`,
|
|
270
|
-
` path ${skill.path}`,
|
|
271
|
-
].join('\n');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function renderDiscoveredSkillList(skills: readonly SkillRecord[]): string {
|
|
275
|
-
if (skills.length === 0) {
|
|
276
|
-
return [
|
|
277
|
-
'Discovered Agent skill files',
|
|
278
|
-
' No SKILL.md or .md skill files found in Agent skill folders.',
|
|
279
|
-
' Search roots: .goodvibes/skills, .goodvibes/agent/skills, ~/.goodvibes/skills, ~/.goodvibes/agent/skills',
|
|
280
|
-
].join('\n');
|
|
281
|
-
}
|
|
282
|
-
return [
|
|
283
|
-
`Discovered Agent skill files (${skills.length})`,
|
|
284
|
-
...skills.map(summarizeDiscoveredSkill),
|
|
285
|
-
'',
|
|
286
|
-
'Import one with: goodvibes-agent skills import-discovered <name> --yes',
|
|
287
|
-
].join('\n');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function discoveredSkillLookupValues(skill: SkillRecord): readonly string[] {
|
|
291
|
-
const slug = skill.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
292
|
-
const basename = skill.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
293
|
-
return [skill.name, slug, skill.path, basename]
|
|
294
|
-
.map((value) => value.trim().toLowerCase())
|
|
295
|
-
.filter(Boolean);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function findDiscoveredSkill(skills: readonly SkillRecord[], idOrName: string): SkillRecord | null {
|
|
299
|
-
const lookup = idOrName.trim().toLowerCase();
|
|
300
|
-
if (!lookup) return null;
|
|
301
|
-
return skills.find((skill) => discoveredSkillLookupValues(skill).includes(lookup)) ?? null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function discoveredFrontmatterList(skill: SkillRecord, key: string): readonly string[] {
|
|
305
|
-
const value = skill.frontmatter[key];
|
|
306
|
-
if (!value) return [];
|
|
307
|
-
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function discoveredFrontmatterAnyList(skill: SkillRecord, keys: readonly string[]): readonly string[] {
|
|
311
|
-
for (const key of keys) {
|
|
312
|
-
const values = discoveredFrontmatterList(skill, key);
|
|
313
|
-
if (values.length > 0) return values;
|
|
314
|
-
}
|
|
315
|
-
return [];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function renderBundleList(title: string, path: string, bundles: readonly AgentSkillBundleRecord[], skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
|
|
319
|
-
if (bundles.length === 0) {
|
|
320
|
-
return [
|
|
321
|
-
title,
|
|
322
|
-
` ${emptyMessage ?? 'No Agent-local skill bundles yet.'}`,
|
|
323
|
-
emptyMessage ? '' : ' Create one with: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>',
|
|
324
|
-
].filter(Boolean).join('\n');
|
|
325
|
-
}
|
|
326
|
-
return [
|
|
327
|
-
`${title} (${bundles.length})`,
|
|
328
|
-
` store ${path}`,
|
|
329
|
-
...bundles.map((bundle) => summarizeBundle(bundle, skills)),
|
|
330
|
-
].join('\n');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function renderSkill(skill: AgentSkillRecord): string {
|
|
334
|
-
const readiness = evaluateAgentSkillReadiness(skill);
|
|
335
|
-
return [
|
|
336
|
-
`Skill ${skill.name}`,
|
|
337
|
-
` id ${skill.id}`,
|
|
338
|
-
` enabled: ${skill.enabled ? 'yes' : 'no'}`,
|
|
339
|
-
` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
|
|
340
|
-
` requirements: ${skill.requirements.map(formatAgentSkillRequirement).join(', ') || '(none)'}`,
|
|
341
|
-
readiness.missing.length > 0 ? ` missing: ${readiness.missing.map(formatAgentSkillRequirement).join(', ')}` : '',
|
|
342
|
-
` review: ${formatAgentRecordReviewState(skill.reviewState)}`,
|
|
343
|
-
` origin: ${formatAgentRecordOrigin(skill.source, skill.provenance)}`,
|
|
344
|
-
` tags: ${skill.tags.join(', ') || '(none)'}`,
|
|
345
|
-
` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
346
|
-
` created: ${skill.createdAt}`,
|
|
347
|
-
` updated: ${skill.updatedAt}`,
|
|
348
|
-
skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
|
|
349
|
-
'',
|
|
350
|
-
skill.description,
|
|
351
|
-
'',
|
|
352
|
-
skill.procedure,
|
|
353
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function renderBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
|
|
357
|
-
const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
|
|
358
|
-
const missing = [
|
|
359
|
-
...readiness.missingRequirements.map(formatAgentSkillRequirement),
|
|
360
|
-
...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
|
|
361
|
-
];
|
|
362
|
-
return [
|
|
363
|
-
`Skill bundle ${bundle.name}`,
|
|
364
|
-
` id ${bundle.id}`,
|
|
365
|
-
` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
|
|
366
|
-
` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
|
|
367
|
-
missing.length > 0 ? ` missing: ${missing.join(', ')}` : '',
|
|
368
|
-
` review: ${formatAgentRecordReviewState(bundle.reviewState)}`,
|
|
369
|
-
` origin: ${formatAgentRecordOrigin(bundle.source, bundle.provenance)}`,
|
|
370
|
-
` skills: ${bundle.skillIds.join(', ')}`,
|
|
371
|
-
` created: ${bundle.createdAt}`,
|
|
372
|
-
` updated: ${bundle.updatedAt}`,
|
|
373
|
-
bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
|
|
374
|
-
'',
|
|
375
|
-
bundle.description,
|
|
376
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function usagePersonas(): string {
|
|
380
|
-
return 'Usage: goodvibes-agent personas [list|active|discover|import-discovered <name> --yes|search <query>|show <id>|create|update <id>|use <id>|clear|review <id>|stale <id> <reason>|delete <id> --yes]';
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function usageSkills(): string {
|
|
384
|
-
return 'Usage: goodvibes-agent skills [list|enabled|active|attention|discover|import-discovered <name> --yes|search <query>|show <id>|create [--requires-env A,B] [--requires-command gh,jq]|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes|bundle ...]';
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function usageBundles(): string {
|
|
388
|
-
return 'Usage: goodvibes-agent skills bundle [list|enabled|attention|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes]';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function errorOutput(runtime: CliCommandRuntime, error: unknown, kind: string): CliCommandOutput {
|
|
392
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
-
const exitCode = message.startsWith('Usage:') || message.includes('\nMissing --') ? 2 : 1;
|
|
394
|
-
return failure(runtime, kind, message, exitCode);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
398
|
-
try {
|
|
399
|
-
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
400
|
-
const normalized = sub.toLowerCase();
|
|
401
|
-
const registry = personaRegistry(runtime);
|
|
402
|
-
const snapshot = registry.snapshot();
|
|
403
|
-
if (normalized === 'list' || normalized === 'ls') {
|
|
404
|
-
return success(runtime, 'agent.personas.list', snapshot, renderPersonaList('Agent personas', snapshot.path, snapshot.personas, snapshot.activePersonaId));
|
|
405
|
-
}
|
|
406
|
-
if (normalized === 'active') {
|
|
407
|
-
const active = snapshot.activePersona;
|
|
408
|
-
if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
|
|
409
|
-
return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
|
|
410
|
-
}
|
|
411
|
-
if (normalized === 'discover') {
|
|
412
|
-
const discovered = await discoverPersonas(shellPaths(runtime));
|
|
413
|
-
return success(runtime, 'agent.personas.discover', { personas: discovered }, renderDiscoveredPersonaList(discovered));
|
|
414
|
-
}
|
|
415
|
-
if (normalized === 'import-discovered' || normalized === 'import-persona') {
|
|
416
|
-
const options = parseOptions(rest);
|
|
417
|
-
const name = options.positionals.join(' ').trim();
|
|
418
|
-
if (!name) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas import-discovered <name> [--use] --yes', 2);
|
|
419
|
-
const discovered = findDiscoveredPersona(await discoverPersonas(shellPaths(runtime)), name);
|
|
420
|
-
if (!discovered) {
|
|
421
|
-
return failure(runtime, 'persona_discovery_not_found', `Unknown discovered Agent persona ${name}\nRun goodvibes-agent personas discover to inspect available persona files.`, 1);
|
|
422
|
-
}
|
|
423
|
-
if (!hasFlag(options, 'yes')) {
|
|
424
|
-
return success(runtime, 'agent.personas.import_discovered.preview', { persona: discovered }, [
|
|
425
|
-
'Agent persona import preview',
|
|
426
|
-
` name ${discovered.name}`,
|
|
427
|
-
` origin ${discovered.origin}`,
|
|
428
|
-
` path ${discovered.path}`,
|
|
429
|
-
` description ${discovered.description || '(none)'}`,
|
|
430
|
-
` body characters ${discovered.body.length}`,
|
|
431
|
-
' next rerun with --yes to import into the Agent-local persona registry',
|
|
432
|
-
].join('\n'));
|
|
433
|
-
}
|
|
434
|
-
const persona = registry.create({
|
|
435
|
-
name: discovered.name,
|
|
436
|
-
description: discovered.description || `Imported persona from ${discovered.origin} markdown file.`,
|
|
437
|
-
body: discovered.body,
|
|
438
|
-
tags: discoveredPersonaFrontmatterList(discovered, 'tags'),
|
|
439
|
-
triggers: discoveredPersonaFrontmatterList(discovered, 'triggers'),
|
|
440
|
-
source: 'imported',
|
|
441
|
-
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
442
|
-
});
|
|
443
|
-
if (hasFlag(options, 'use')) registry.setActive(persona.id);
|
|
444
|
-
return success(runtime, 'agent.personas.import_discovered', persona, [
|
|
445
|
-
`Imported Agent persona ${persona.id}: ${persona.name}${hasFlag(options, 'use') ? ' (active)' : ''}`,
|
|
446
|
-
` name ${persona.name}`,
|
|
447
|
-
` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
|
|
448
|
-
].join('\n'));
|
|
449
|
-
}
|
|
450
|
-
if (normalized === 'search' || normalized === 'find') {
|
|
451
|
-
const query = rest.join(' ').trim();
|
|
452
|
-
const results = registry.search(query);
|
|
453
|
-
return success(runtime, 'agent.personas.search', { query, results }, renderPersonaList(`Agent personas matching "${query}"`, snapshot.path, results, snapshot.activePersonaId));
|
|
454
|
-
}
|
|
455
|
-
if (normalized === 'show' || normalized === 'get') {
|
|
456
|
-
const id = rest[0];
|
|
457
|
-
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas show <id>', 2);
|
|
458
|
-
const persona = registry.get(id);
|
|
459
|
-
if (!persona) return failure(runtime, 'persona_not_found', `Unknown Agent persona ${id}`, 1);
|
|
460
|
-
return success(runtime, 'agent.personas.show', persona, renderPersona(persona, snapshot.activePersonaId));
|
|
461
|
-
}
|
|
462
|
-
if (normalized === 'create') {
|
|
463
|
-
const options = parseOptions(rest);
|
|
464
|
-
const persona = registry.create({
|
|
465
|
-
name: requiredOption(options, 'name', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
466
|
-
description: requiredOption(options, 'description', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
467
|
-
body: requiredOption(options, 'body', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
468
|
-
tags: csvOption(options, 'tags'),
|
|
469
|
-
triggers: csvOption(options, 'triggers'),
|
|
470
|
-
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
471
|
-
});
|
|
472
|
-
if (hasFlag(options, 'use')) registry.setActive(persona.id);
|
|
473
|
-
return success(runtime, 'agent.personas.create', persona, [
|
|
474
|
-
'Agent persona created',
|
|
475
|
-
` id ${persona.id}`,
|
|
476
|
-
hasFlag(options, 'use') ? ' (active)' : '',
|
|
477
|
-
` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
|
|
478
|
-
].filter(Boolean).join('\n'));
|
|
479
|
-
}
|
|
480
|
-
if (normalized === 'update') {
|
|
481
|
-
const id = rest[0];
|
|
482
|
-
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas update <id> [--name ...] [--description ...] [--body ...]', 2);
|
|
483
|
-
const options = parseOptions(rest.slice(1));
|
|
484
|
-
const persona = registry.update(id, {
|
|
485
|
-
name: optionValue(options, 'name'),
|
|
486
|
-
description: optionValue(options, 'description'),
|
|
487
|
-
body: optionValue(options, 'body'),
|
|
488
|
-
tags: csvOption(options, 'tags'),
|
|
489
|
-
triggers: csvOption(options, 'triggers'),
|
|
490
|
-
provenance: optionValue(options, 'provenance'),
|
|
491
|
-
});
|
|
492
|
-
return success(runtime, 'agent.personas.update', persona, [
|
|
493
|
-
'Agent persona updated',
|
|
494
|
-
` id ${persona.id}`,
|
|
495
|
-
].join('\n'));
|
|
496
|
-
}
|
|
497
|
-
if (normalized === 'use') {
|
|
498
|
-
const id = rest[0];
|
|
499
|
-
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas use <id>', 2);
|
|
500
|
-
const persona = registry.setActive(id);
|
|
501
|
-
return success(runtime, 'agent.personas.use', persona, [
|
|
502
|
-
'Active Agent persona',
|
|
503
|
-
` id ${persona.id}`,
|
|
504
|
-
].join('\n'));
|
|
505
|
-
}
|
|
506
|
-
if (normalized === 'clear') {
|
|
507
|
-
registry.clearActive();
|
|
508
|
-
return success(runtime, 'agent.personas.clear', { activePersonaId: null }, 'Active Agent persona cleared.');
|
|
509
|
-
}
|
|
510
|
-
if (normalized === 'review') {
|
|
511
|
-
const id = rest[0];
|
|
512
|
-
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas review <id>', 2);
|
|
513
|
-
const persona = registry.markReviewed(id);
|
|
514
|
-
return success(runtime, 'agent.personas.review', persona, [
|
|
515
|
-
'Agent persona reviewed',
|
|
516
|
-
` id ${persona.id}`,
|
|
517
|
-
].join('\n'));
|
|
518
|
-
}
|
|
519
|
-
if (normalized === 'stale') {
|
|
520
|
-
const id = rest[0];
|
|
521
|
-
if (!id || rest.length < 2) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas stale <id> <reason>', 2);
|
|
522
|
-
const persona = registry.markStale(id, rest.slice(1).join(' '));
|
|
523
|
-
return success(runtime, 'agent.personas.stale', persona, [
|
|
524
|
-
'Agent persona marked stale',
|
|
525
|
-
` id ${persona.id}`,
|
|
526
|
-
].join('\n'));
|
|
527
|
-
}
|
|
528
|
-
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
529
|
-
const options = parseOptions(rest);
|
|
530
|
-
const id = options.positionals[0];
|
|
531
|
-
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas delete <id> --yes', 2);
|
|
532
|
-
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent persona ${id} without --yes.`, 2);
|
|
533
|
-
const persona = registry.deletePersona(id);
|
|
534
|
-
return success(runtime, 'agent.personas.delete', persona, [
|
|
535
|
-
`Agent persona deleted: ${id}`,
|
|
536
|
-
` id ${persona.id}`,
|
|
537
|
-
].join('\n'));
|
|
538
|
-
}
|
|
539
|
-
return failure(runtime, 'invalid_persona_command', usagePersonas(), 2);
|
|
540
|
-
} catch (error) {
|
|
541
|
-
return errorOutput(runtime, error, 'agent.personas.error');
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function skillPayloadFromOptions(options: ParsedOptions): {
|
|
546
|
-
readonly name: string;
|
|
547
|
-
readonly description: string;
|
|
548
|
-
readonly procedure: string;
|
|
549
|
-
readonly tags: readonly string[] | undefined;
|
|
550
|
-
readonly triggers: readonly string[] | undefined;
|
|
551
|
-
readonly requirements: ReturnType<typeof buildAgentSkillRequirements>;
|
|
552
|
-
readonly enabled: boolean | undefined;
|
|
553
|
-
readonly provenance: string;
|
|
554
|
-
} {
|
|
555
|
-
const usage = 'Usage: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>';
|
|
556
|
-
return {
|
|
557
|
-
name: requiredOption(options, 'name', usage),
|
|
558
|
-
description: requiredOption(options, 'description', usage),
|
|
559
|
-
procedure: requiredOption(options, 'procedure', usage),
|
|
560
|
-
tags: csvOption(options, 'tags'),
|
|
561
|
-
triggers: csvOption(options, 'triggers'),
|
|
562
|
-
requirements: buildAgentSkillRequirements({
|
|
563
|
-
env: csvOption(options, 'requires-env'),
|
|
564
|
-
commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
|
|
565
|
-
}),
|
|
566
|
-
enabled: hasFlag(options, 'enabled') ? true : undefined,
|
|
567
|
-
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async function handleSkillBundleCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
572
|
-
try {
|
|
573
|
-
const [sub = 'list', ...rest] = args;
|
|
574
|
-
const normalized = sub.toLowerCase();
|
|
575
|
-
const registry = skillRegistry(runtime);
|
|
576
|
-
const snapshot = registry.snapshot();
|
|
577
|
-
if (normalized === 'list' || normalized === 'ls') {
|
|
578
|
-
return success(runtime, 'agent.skills.bundles.list', { path: snapshot.path, bundles: snapshot.bundles }, renderBundleList('Agent skill bundles', snapshot.path, snapshot.bundles, snapshot.skills));
|
|
579
|
-
}
|
|
580
|
-
if (normalized === 'enabled') {
|
|
581
|
-
return success(runtime, 'agent.skills.bundles.enabled', { path: snapshot.path, bundles: snapshot.enabledBundles }, renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills));
|
|
582
|
-
}
|
|
583
|
-
if (normalized === 'attention' || normalized === 'needs-setup') {
|
|
584
|
-
const bundles = snapshot.bundles.filter((bundle) => !evaluateAgentSkillBundleReadiness(bundle, snapshot.skills).ready);
|
|
585
|
-
return success(runtime, 'agent.skills.bundles.attention', { path: snapshot.path, bundles }, renderBundleList('Agent skill bundles needing setup', snapshot.path, bundles, snapshot.skills, 'No Agent-local skill bundles need setup.'));
|
|
586
|
-
}
|
|
587
|
-
if (normalized === 'search' || normalized === 'find') {
|
|
588
|
-
const query = rest.join(' ').trim();
|
|
589
|
-
const results = registry.searchBundles(query);
|
|
590
|
-
return success(runtime, 'agent.skills.bundles.search', { query, results }, renderBundleList(`Agent skill bundles matching "${query}"`, snapshot.path, results, snapshot.skills));
|
|
591
|
-
}
|
|
592
|
-
if (normalized === 'show' || normalized === 'get') {
|
|
593
|
-
const id = rest[0];
|
|
594
|
-
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle show <id>', 2);
|
|
595
|
-
const bundle = registry.getBundle(id);
|
|
596
|
-
if (!bundle) return failure(runtime, 'skill_bundle_not_found', `Unknown Agent skill bundle ${id}`, 1);
|
|
597
|
-
return success(runtime, 'agent.skills.bundles.show', bundle, renderBundle(bundle, snapshot.skills));
|
|
598
|
-
}
|
|
599
|
-
if (normalized === 'create') {
|
|
600
|
-
const options = parseOptions(rest);
|
|
601
|
-
const usage = 'Usage: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>';
|
|
602
|
-
const bundle = registry.createBundle({
|
|
603
|
-
name: requiredOption(options, 'name', usage),
|
|
604
|
-
description: requiredOption(options, 'description', usage),
|
|
605
|
-
skillIds: requiredOption(options, 'skills', usage).split(',').map((entry) => entry.trim()).filter(Boolean),
|
|
606
|
-
enabled: hasFlag(options, 'enabled'),
|
|
607
|
-
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
608
|
-
});
|
|
609
|
-
return success(runtime, 'agent.skills.bundles.create', bundle, [
|
|
610
|
-
'Agent skill bundle created',
|
|
611
|
-
` id ${bundle.id}`,
|
|
612
|
-
].join('\n'));
|
|
613
|
-
}
|
|
614
|
-
if (normalized === 'update') {
|
|
615
|
-
const id = rest[0];
|
|
616
|
-
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle update <id> [--name ...] [--description ...] [--skills id,id]', 2);
|
|
617
|
-
const options = parseOptions(rest.slice(1));
|
|
618
|
-
const bundle = registry.updateBundle(id, {
|
|
619
|
-
name: optionValue(options, 'name'),
|
|
620
|
-
description: optionValue(options, 'description'),
|
|
621
|
-
skillIds: csvOption(options, 'skills'),
|
|
622
|
-
provenance: optionValue(options, 'provenance'),
|
|
623
|
-
});
|
|
624
|
-
return success(runtime, 'agent.skills.bundles.update', bundle, [
|
|
625
|
-
'Agent skill bundle updated',
|
|
626
|
-
` id ${bundle.id}`,
|
|
627
|
-
].join('\n'));
|
|
628
|
-
}
|
|
629
|
-
if (normalized === 'enable' || normalized === 'disable') {
|
|
630
|
-
const id = rest[0];
|
|
631
|
-
if (!id) return failure(runtime, 'invalid_skill_bundle_command', `Usage: goodvibes-agent skills bundle ${normalized} <id>`, 2);
|
|
632
|
-
const bundle = registry.setBundleEnabled(id, normalized === 'enable');
|
|
633
|
-
return success(runtime, `agent.skills.bundles.${normalized}`, bundle, [
|
|
634
|
-
`Agent skill bundle ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
|
|
635
|
-
` id ${bundle.id}`,
|
|
636
|
-
].join('\n'));
|
|
637
|
-
}
|
|
638
|
-
if (normalized === 'review') {
|
|
639
|
-
const id = rest[0];
|
|
640
|
-
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle review <id>', 2);
|
|
641
|
-
const bundle = registry.markBundleReviewed(id);
|
|
642
|
-
return success(runtime, 'agent.skills.bundles.review', bundle, [
|
|
643
|
-
'Agent skill bundle reviewed',
|
|
644
|
-
` id ${bundle.id}`,
|
|
645
|
-
].join('\n'));
|
|
646
|
-
}
|
|
647
|
-
if (normalized === 'stale') {
|
|
648
|
-
const id = rest[0];
|
|
649
|
-
if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle stale <id> <reason>', 2);
|
|
650
|
-
const bundle = registry.markBundleStale(id, rest.slice(1).join(' '));
|
|
651
|
-
return success(runtime, 'agent.skills.bundles.stale', bundle, [
|
|
652
|
-
'Agent skill bundle marked stale',
|
|
653
|
-
` id ${bundle.id}`,
|
|
654
|
-
].join('\n'));
|
|
655
|
-
}
|
|
656
|
-
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
657
|
-
const options = parseOptions(rest);
|
|
658
|
-
const id = options.positionals[0];
|
|
659
|
-
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle delete <id> --yes', 2);
|
|
660
|
-
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill bundle ${id} without --yes.`, 2);
|
|
661
|
-
const bundle = registry.deleteBundle(id);
|
|
662
|
-
return success(runtime, 'agent.skills.bundles.delete', bundle, [
|
|
663
|
-
'Agent skill bundle deleted',
|
|
664
|
-
` id ${bundle.id}`,
|
|
665
|
-
].join('\n'));
|
|
666
|
-
}
|
|
667
|
-
return failure(runtime, 'invalid_skill_bundle_command', usageBundles(), 2);
|
|
668
|
-
} catch (error) {
|
|
669
|
-
return errorOutput(runtime, error, 'agent.skills.bundles.error');
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
export async function handleSkillsCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
674
|
-
try {
|
|
675
|
-
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
676
|
-
const normalized = sub.toLowerCase();
|
|
677
|
-
if (normalized === 'bundle' || normalized === 'bundles') return handleSkillBundleCommand(runtime, rest);
|
|
678
|
-
const registry = skillRegistry(runtime);
|
|
679
|
-
const snapshot = registry.snapshot();
|
|
680
|
-
if (normalized === 'list' || normalized === 'ls') {
|
|
681
|
-
return success(runtime, 'agent.skills.list', { path: snapshot.path, skills: snapshot.skills, enabledCount: snapshot.enabledSkills.length }, renderSkillList('Agent skills', snapshot.path, snapshot.skills));
|
|
682
|
-
}
|
|
683
|
-
if (normalized === 'enabled') {
|
|
684
|
-
return success(runtime, 'agent.skills.enabled', { path: snapshot.path, skills: snapshot.enabledSkills }, renderSkillList('Enabled Agent skills', snapshot.path, snapshot.enabledSkills));
|
|
685
|
-
}
|
|
686
|
-
if (normalized === 'active') {
|
|
687
|
-
return success(runtime, 'agent.skills.active', { path: snapshot.path, skills: snapshot.activeSkills, bundles: snapshot.enabledBundles }, [
|
|
688
|
-
renderSkillList('Active Agent skills', snapshot.path, snapshot.activeSkills),
|
|
689
|
-
snapshot.enabledBundles.length > 0 ? renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills) : '',
|
|
690
|
-
].filter(Boolean).join('\n\n'));
|
|
691
|
-
}
|
|
692
|
-
if (normalized === 'attention' || normalized === 'needs-setup') {
|
|
693
|
-
const skills = snapshot.skills.filter((skill) => !evaluateAgentSkillReadiness(skill).ready);
|
|
694
|
-
return success(runtime, 'agent.skills.attention', { path: snapshot.path, skills }, renderSkillList('Agent skills needing setup', snapshot.path, skills, 'No Agent-local skills need setup.'));
|
|
695
|
-
}
|
|
696
|
-
if (normalized === 'discover') {
|
|
697
|
-
const discovered = await discoverSkills(shellPaths(runtime));
|
|
698
|
-
return success(runtime, 'agent.skills.discover', { skills: discovered }, renderDiscoveredSkillList(discovered));
|
|
699
|
-
}
|
|
700
|
-
if (normalized === 'import-discovered' || normalized === 'import-skill') {
|
|
701
|
-
const options = parseOptions(rest);
|
|
702
|
-
const name = options.positionals.join(' ').trim();
|
|
703
|
-
if (!name) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills import-discovered <name> [--enabled] --yes', 2);
|
|
704
|
-
const discovered = findDiscoveredSkill(await discoverSkills(shellPaths(runtime)), name);
|
|
705
|
-
if (!discovered) {
|
|
706
|
-
return failure(runtime, 'skill_discovery_not_found', `Unknown discovered Agent skill ${name}\nRun goodvibes-agent skills discover to inspect available skill files.`, 1);
|
|
707
|
-
}
|
|
708
|
-
if (!hasFlag(options, 'yes')) {
|
|
709
|
-
return success(runtime, 'agent.skills.import_discovered.preview', { skill: discovered }, [
|
|
710
|
-
'Agent skill import preview',
|
|
711
|
-
` name ${discovered.name}`,
|
|
712
|
-
` origin ${discovered.origin}`,
|
|
713
|
-
` path ${discovered.path}`,
|
|
714
|
-
` description ${discovered.description || '(none)'}`,
|
|
715
|
-
` procedure characters ${discovered.body.length}`,
|
|
716
|
-
' next rerun with --yes to import into the Agent-local skill registry',
|
|
717
|
-
].join('\n'));
|
|
718
|
-
}
|
|
719
|
-
const skill = registry.create({
|
|
720
|
-
name: discovered.name,
|
|
721
|
-
description: discovered.description || `Imported skill from ${discovered.origin} skill file.`,
|
|
722
|
-
procedure: discovered.body,
|
|
723
|
-
tags: discoveredFrontmatterList(discovered, 'tags'),
|
|
724
|
-
triggers: discoveredFrontmatterList(discovered, 'triggers'),
|
|
725
|
-
requirements: buildAgentSkillRequirements({
|
|
726
|
-
env: discoveredFrontmatterAnyList(discovered, ['requiresEnv', 'requires-env', 'requires_env']),
|
|
727
|
-
commands: discoveredFrontmatterAnyList(discovered, ['requiresCommands', 'requires-commands', 'requires_commands', 'commands']),
|
|
728
|
-
}),
|
|
729
|
-
enabled: hasFlag(options, 'enabled'),
|
|
730
|
-
source: 'imported',
|
|
731
|
-
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
732
|
-
});
|
|
733
|
-
return success(runtime, 'agent.skills.import_discovered', skill, [
|
|
734
|
-
`Imported Agent skill ${skill.id}: ${skill.name}${skill.enabled ? ' (enabled)' : ''}`,
|
|
735
|
-
` name ${skill.name}`,
|
|
736
|
-
` enabled ${skill.enabled ? 'yes' : 'no'}`,
|
|
737
|
-
].join('\n'));
|
|
738
|
-
}
|
|
739
|
-
if (normalized === 'search' || normalized === 'find') {
|
|
740
|
-
const query = rest.join(' ').trim();
|
|
741
|
-
const results = registry.search(query);
|
|
742
|
-
return success(runtime, 'agent.skills.search', { query, results }, renderSkillList(`Agent skills matching "${query}"`, snapshot.path, results));
|
|
743
|
-
}
|
|
744
|
-
if (normalized === 'show' || normalized === 'get') {
|
|
745
|
-
const id = rest[0];
|
|
746
|
-
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills show <id>', 2);
|
|
747
|
-
const skill = registry.get(id);
|
|
748
|
-
if (!skill) return failure(runtime, 'skill_not_found', `Unknown Agent skill ${id}`, 1);
|
|
749
|
-
return success(runtime, 'agent.skills.show', skill, renderSkill(skill));
|
|
750
|
-
}
|
|
751
|
-
if (normalized === 'create') {
|
|
752
|
-
const skill = registry.create(skillPayloadFromOptions(parseOptions(rest)));
|
|
753
|
-
return success(runtime, 'agent.skills.create', skill, [
|
|
754
|
-
'Agent skill created',
|
|
755
|
-
` id ${skill.id}`,
|
|
756
|
-
skill.enabled ? ' (enabled)' : '',
|
|
757
|
-
` enabled ${skill.enabled ? 'yes' : 'no'}`,
|
|
758
|
-
].filter(Boolean).join('\n'));
|
|
759
|
-
}
|
|
760
|
-
if (normalized === 'update') {
|
|
761
|
-
const id = rest[0];
|
|
762
|
-
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills update <id> [--name ...] [--description ...] [--procedure ...]', 2);
|
|
763
|
-
const options = parseOptions(rest.slice(1));
|
|
764
|
-
const skill = registry.update(id, {
|
|
765
|
-
name: optionValue(options, 'name'),
|
|
766
|
-
description: optionValue(options, 'description'),
|
|
767
|
-
procedure: optionValue(options, 'procedure'),
|
|
768
|
-
tags: csvOption(options, 'tags'),
|
|
769
|
-
triggers: csvOption(options, 'triggers'),
|
|
770
|
-
requirements: options.values.has('requires-env') || options.values.has('requires-command') || options.values.has('requires-commands')
|
|
771
|
-
? buildAgentSkillRequirements({
|
|
772
|
-
env: csvOption(options, 'requires-env'),
|
|
773
|
-
commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
|
|
774
|
-
})
|
|
775
|
-
: undefined,
|
|
776
|
-
provenance: optionValue(options, 'provenance'),
|
|
777
|
-
});
|
|
778
|
-
return success(runtime, 'agent.skills.update', skill, [
|
|
779
|
-
'Agent skill updated',
|
|
780
|
-
` id ${skill.id}`,
|
|
781
|
-
].join('\n'));
|
|
782
|
-
}
|
|
783
|
-
if (normalized === 'enable' || normalized === 'disable') {
|
|
784
|
-
const id = rest[0];
|
|
785
|
-
if (!id) return failure(runtime, 'invalid_skill_command', `Usage: goodvibes-agent skills ${normalized} <id>`, 2);
|
|
786
|
-
const skill = registry.setEnabled(id, normalized === 'enable');
|
|
787
|
-
return success(runtime, `agent.skills.${normalized}`, skill, [
|
|
788
|
-
`Agent skill ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
|
|
789
|
-
` id ${skill.id}`,
|
|
790
|
-
].join('\n'));
|
|
791
|
-
}
|
|
792
|
-
if (normalized === 'review') {
|
|
793
|
-
const id = rest[0];
|
|
794
|
-
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills review <id>', 2);
|
|
795
|
-
const skill = registry.markReviewed(id);
|
|
796
|
-
return success(runtime, 'agent.skills.review', skill, [
|
|
797
|
-
'Agent skill reviewed',
|
|
798
|
-
` id ${skill.id}`,
|
|
799
|
-
].join('\n'));
|
|
800
|
-
}
|
|
801
|
-
if (normalized === 'stale') {
|
|
802
|
-
const id = rest[0];
|
|
803
|
-
if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills stale <id> <reason>', 2);
|
|
804
|
-
const skill = registry.markStale(id, rest.slice(1).join(' '));
|
|
805
|
-
return success(runtime, 'agent.skills.stale', skill, [
|
|
806
|
-
'Agent skill marked stale',
|
|
807
|
-
` id ${skill.id}`,
|
|
808
|
-
].join('\n'));
|
|
809
|
-
}
|
|
810
|
-
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
811
|
-
const options = parseOptions(rest);
|
|
812
|
-
const id = options.positionals[0];
|
|
813
|
-
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills delete <id> --yes', 2);
|
|
814
|
-
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill ${id} without --yes.`, 2);
|
|
815
|
-
const skill = registry.deleteSkill(id);
|
|
816
|
-
return success(runtime, 'agent.skills.delete', skill, [
|
|
817
|
-
`Agent skill deleted: ${id}`,
|
|
818
|
-
` id ${skill.id}`,
|
|
819
|
-
].join('\n'));
|
|
820
|
-
}
|
|
821
|
-
return failure(runtime, 'invalid_skill_command', usageSkills(), 2);
|
|
822
|
-
} catch (error) {
|
|
823
|
-
return errorOutput(runtime, error, 'agent.skills.error');
|
|
824
|
-
}
|
|
825
|
-
}
|