@pellux/goodvibes-agent 0.1.112 → 0.1.114
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 +12 -0
- package/dist/package/main.js +831 -85
- package/package.json +1 -1
- package/src/agent/persona-discovery.ts +117 -0
- package/src/agent/routine-discovery.ts +115 -0
- package/src/cli/help.ts +13 -1
- package/src/cli/local-library-command.ts +171 -2
- package/src/cli/routines-command.ts +156 -1
- package/src/cli/service-posture.ts +1 -8
- package/src/cli/status.ts +1 -30
- package/src/input/agent-workspace-basic-command-editors.ts +90 -0
- package/src/input/agent-workspace-categories.ts +4 -0
- package/src/input/agent-workspace-command-editor.ts +2 -0
- package/src/input/agent-workspace-types.ts +2 -0
- package/src/input/commands/personas-runtime.ts +87 -2
- package/src/input/commands/routines-runtime.ts +87 -2
- package/src/renderer/ui-factory.ts +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.114",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { promises as fsPromises } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
|
|
6
|
+
export type PersonaOrigin = 'project-local' | 'global' | 'custom';
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredPersonaRecord {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly origin: PersonaOrigin;
|
|
13
|
+
readonly body: string;
|
|
14
|
+
readonly frontmatter: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DIRECTORY_MARKERS: readonly string[] = ['PERSONA.md', 'persona.md', 'AGENT.md', 'agent.md'];
|
|
18
|
+
|
|
19
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!match) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const line of match[1].split('\n')) {
|
|
24
|
+
const [key, ...rest] = line.split(':');
|
|
25
|
+
if (key && rest.length > 0) {
|
|
26
|
+
result[key.trim()] = rest.join(':').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPersonaDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: PersonaOrigin }> {
|
|
33
|
+
return [
|
|
34
|
+
{ root: join(cwd, '.goodvibes', 'personas'), origin: 'project-local' },
|
|
35
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'project-local' },
|
|
36
|
+
{ root: join(cwd, '.goodvibes', 'agents'), origin: 'project-local' },
|
|
37
|
+
{ root: join(homeDir, '.goodvibes', 'personas'), origin: 'global' },
|
|
38
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'global' },
|
|
39
|
+
{ root: join(homeDir, '.goodvibes', 'agents'), origin: 'global' },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readPersonaFile(path: string, origin: PersonaOrigin): Promise<DiscoveredPersonaRecord | null> {
|
|
44
|
+
let content = '';
|
|
45
|
+
try {
|
|
46
|
+
content = await fsPromises.readFile(path, 'utf-8');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const frontmatter = parseFrontmatter(content);
|
|
52
|
+
const markdownBody = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
53
|
+
const body = (frontmatter.system_prompt ?? markdownBody).trim();
|
|
54
|
+
if (!body) return null;
|
|
55
|
+
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'persona';
|
|
56
|
+
const description = frontmatter.description ?? frontmatter.summary ?? frontmatter.title ?? '';
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
description,
|
|
61
|
+
path,
|
|
62
|
+
origin,
|
|
63
|
+
body,
|
|
64
|
+
frontmatter,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function scanPersonaDirectory(root: string, origin: PersonaOrigin): Promise<DiscoveredPersonaRecord[]> {
|
|
69
|
+
let entries: string[] = [];
|
|
70
|
+
try {
|
|
71
|
+
entries = await fsPromises.readdir(root);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const records: DiscoveredPersonaRecord[] = [];
|
|
77
|
+
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
78
|
+
if (entry.endsWith('.md')) {
|
|
79
|
+
const record = await readPersonaFile(join(root, entry), origin);
|
|
80
|
+
if (record) records.push(record);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const marker of DIRECTORY_MARKERS) {
|
|
85
|
+
const record = await readPersonaFile(join(root, entry, marker), origin);
|
|
86
|
+
if (record) {
|
|
87
|
+
records.push(record);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return records;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function discoverPersonas(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<DiscoveredPersonaRecord[]> {
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const records: DiscoveredPersonaRecord[] = [];
|
|
99
|
+
|
|
100
|
+
for (const { root, origin } of getPersonaDirectories(shellPaths.workingDirectory, shellPaths.homeDirectory)) {
|
|
101
|
+
for (const record of await scanPersonaDirectory(root, origin)) {
|
|
102
|
+
const key = record.name.toLowerCase();
|
|
103
|
+
if (seen.has(key)) continue;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
records.push(record);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return records.sort((a, b) => {
|
|
110
|
+
const originRank = a.origin === b.origin
|
|
111
|
+
? 0
|
|
112
|
+
: a.origin === 'project-local'
|
|
113
|
+
? -1
|
|
114
|
+
: 1;
|
|
115
|
+
return originRank || a.name.localeCompare(b.name);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { promises as fsPromises } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
|
|
6
|
+
export type RoutineOrigin = 'project-local' | 'global' | 'custom';
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredRoutineRecord {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly origin: RoutineOrigin;
|
|
13
|
+
readonly steps: string;
|
|
14
|
+
readonly frontmatter: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DIRECTORY_MARKERS: readonly string[] = ['ROUTINE.md', 'routine.md'];
|
|
18
|
+
|
|
19
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!match) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const line of match[1].split('\n')) {
|
|
24
|
+
const [key, ...rest] = line.split(':');
|
|
25
|
+
if (key && rest.length > 0) {
|
|
26
|
+
result[key.trim()] = rest.join(':').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getRoutineDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: RoutineOrigin }> {
|
|
33
|
+
return [
|
|
34
|
+
{ root: join(cwd, '.goodvibes', 'routines'), origin: 'project-local' },
|
|
35
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'project-local' },
|
|
36
|
+
{ root: join(homeDir, '.goodvibes', 'routines'), origin: 'global' },
|
|
37
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'global' },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readRoutineFile(path: string, origin: RoutineOrigin): Promise<DiscoveredRoutineRecord | null> {
|
|
42
|
+
let content = '';
|
|
43
|
+
try {
|
|
44
|
+
content = await fsPromises.readFile(path, 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const frontmatter = parseFrontmatter(content);
|
|
50
|
+
const markdownBody = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
51
|
+
const steps = (frontmatter.steps ?? markdownBody).trim();
|
|
52
|
+
if (!steps) return null;
|
|
53
|
+
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'routine';
|
|
54
|
+
const description = frontmatter.description ?? frontmatter.summary ?? frontmatter.title ?? '';
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
description,
|
|
59
|
+
path,
|
|
60
|
+
origin,
|
|
61
|
+
steps,
|
|
62
|
+
frontmatter,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function scanRoutineDirectory(root: string, origin: RoutineOrigin): Promise<DiscoveredRoutineRecord[]> {
|
|
67
|
+
let entries: string[] = [];
|
|
68
|
+
try {
|
|
69
|
+
entries = await fsPromises.readdir(root);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const records: DiscoveredRoutineRecord[] = [];
|
|
75
|
+
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
76
|
+
if (entry.endsWith('.md')) {
|
|
77
|
+
const record = await readRoutineFile(join(root, entry), origin);
|
|
78
|
+
if (record) records.push(record);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const marker of DIRECTORY_MARKERS) {
|
|
83
|
+
const record = await readRoutineFile(join(root, entry, marker), origin);
|
|
84
|
+
if (record) {
|
|
85
|
+
records.push(record);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return records;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function discoverRoutines(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<DiscoveredRoutineRecord[]> {
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
const records: DiscoveredRoutineRecord[] = [];
|
|
97
|
+
|
|
98
|
+
for (const { root, origin } of getRoutineDirectories(shellPaths.workingDirectory, shellPaths.homeDirectory)) {
|
|
99
|
+
for (const record of await scanRoutineDirectory(root, origin)) {
|
|
100
|
+
const key = record.name.toLowerCase();
|
|
101
|
+
if (seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
records.push(record);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return records.sort((a, b) => {
|
|
108
|
+
const originRank = a.origin === b.origin
|
|
109
|
+
? 0
|
|
110
|
+
: a.origin === 'project-local'
|
|
111
|
+
? -1
|
|
112
|
+
: 1;
|
|
113
|
+
return originRank || a.name.localeCompare(b.name);
|
|
114
|
+
});
|
|
115
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -165,6 +165,8 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
165
165
|
usage: [
|
|
166
166
|
'personas list',
|
|
167
167
|
'personas active',
|
|
168
|
+
'personas discover',
|
|
169
|
+
'personas import-discovered <name> [--use] --yes',
|
|
168
170
|
'personas search <query>',
|
|
169
171
|
'personas show <id>',
|
|
170
172
|
'personas create --name <name> --description <summary> --body <instructions> [--tags a,b] [--triggers a,b] [--use]',
|
|
@@ -178,6 +180,8 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
178
180
|
summary: 'Manage Agent-local personas for the serial main conversation. Personas do not spawn worker agents.',
|
|
179
181
|
examples: [
|
|
180
182
|
'personas list',
|
|
183
|
+
'personas discover',
|
|
184
|
+
'personas import-discovered "Travel Planner" --use --yes',
|
|
181
185
|
'personas create --name "Travel Planner" --description "Plan trips" --body "Compare options before booking" --use',
|
|
182
186
|
'personas review travel-planner',
|
|
183
187
|
'personas delete travel-planner --yes',
|
|
@@ -188,6 +192,8 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
188
192
|
'skills list',
|
|
189
193
|
'skills enabled',
|
|
190
194
|
'skills active',
|
|
195
|
+
'skills discover',
|
|
196
|
+
'skills import-discovered <name> [--enabled] --yes',
|
|
191
197
|
'skills search <query>',
|
|
192
198
|
'skills show <id>',
|
|
193
199
|
'skills create --name <name> --description <summary> --procedure <steps> [--tags a,b] [--triggers a,b] [--enabled]',
|
|
@@ -202,6 +208,8 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
202
208
|
summary: 'Manage reusable Agent-local procedures and skill bundles for the main conversation.',
|
|
203
209
|
examples: [
|
|
204
210
|
'skills list',
|
|
211
|
+
'skills discover',
|
|
212
|
+
'skills import-discovered "Daily Brief" --enabled --yes',
|
|
205
213
|
'skills create --name "Daily Brief" --description "Summarize operator state" --procedure "Review Agent Knowledge, work plans, approvals, and routines" --enabled',
|
|
206
214
|
'skills bundle create --name "Ops Pack" --description "Daily operations bundle" --skills daily-brief --enabled',
|
|
207
215
|
'skills delete daily-brief --yes',
|
|
@@ -239,15 +247,19 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
239
247
|
usage: [
|
|
240
248
|
'routines list',
|
|
241
249
|
'routines enabled',
|
|
250
|
+
'routines discover',
|
|
251
|
+
'routines import-discovered <name> [--enabled] --yes',
|
|
242
252
|
'routines show <id>',
|
|
243
253
|
'routines receipts',
|
|
244
254
|
'routines reconcile',
|
|
245
255
|
'routines receipt <receipt-id>',
|
|
246
256
|
'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--delivery-channel <channel[:route[:label]]>|--delivery-route <route[:label]>|--delivery-webhook <url>|--delivery-link <url>] [--disabled] --yes',
|
|
247
257
|
],
|
|
248
|
-
summary: 'Inspect Agent-local routines, review local promotion receipts, reconcile receipts against live connected schedules, and explicitly promote a reviewed routine into a GoodVibes schedule. Without --yes, promote only prints the schedules.create preview.',
|
|
258
|
+
summary: 'Inspect and import Agent-local routines, review local promotion receipts, reconcile receipts against live connected schedules, and explicitly promote a reviewed routine into a GoodVibes schedule. Without --yes, promote only prints the schedules.create preview.',
|
|
249
259
|
examples: [
|
|
250
260
|
'routines list',
|
|
261
|
+
'routines discover',
|
|
262
|
+
'routines import-discovered "Daily Brief" --enabled --yes',
|
|
251
263
|
'routines show daily-operations-sweep',
|
|
252
264
|
'routines receipts',
|
|
253
265
|
'routines reconcile',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createShellPathService } from '@/runtime/index.ts';
|
|
2
|
+
import { discoverPersonas, type DiscoveredPersonaRecord } from '../agent/persona-discovery.ts';
|
|
2
3
|
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
4
|
+
import { discoverSkills, type SkillRecord } from '../agent/skill-discovery.ts';
|
|
3
5
|
import {
|
|
4
6
|
AgentSkillRegistry,
|
|
5
7
|
type AgentSkillBundleRecord,
|
|
@@ -107,6 +109,13 @@ function skillRegistry(runtime: CliCommandRuntime): AgentSkillRegistry {
|
|
|
107
109
|
}));
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function shellPaths(runtime: CliCommandRuntime): ReturnType<typeof createShellPathService> {
|
|
113
|
+
return createShellPathService({
|
|
114
|
+
workingDirectory: runtime.workingDirectory,
|
|
115
|
+
homeDirectory: runtime.homeDirectory,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
|
|
111
120
|
const active = persona.id === activePersonaId ? 'active' : 'available';
|
|
112
121
|
const tags = persona.tags.length > 0 ? ` tags=${persona.tags.join(',')}` : '';
|
|
@@ -148,6 +157,50 @@ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | nu
|
|
|
148
157
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
149
158
|
}
|
|
150
159
|
|
|
160
|
+
function summarizeDiscoveredPersona(persona: DiscoveredPersonaRecord): string {
|
|
161
|
+
const description = persona.description ? ` - ${persona.description}` : '';
|
|
162
|
+
return [
|
|
163
|
+
` ${persona.name} ${persona.origin}${description}`,
|
|
164
|
+
` path: ${persona.path}`,
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderDiscoveredPersonaList(personas: readonly DiscoveredPersonaRecord[]): string {
|
|
169
|
+
if (personas.length === 0) {
|
|
170
|
+
return [
|
|
171
|
+
'Discovered Agent persona files',
|
|
172
|
+
' No persona markdown files found in Agent persona folders.',
|
|
173
|
+
' Search roots: .goodvibes/personas, .goodvibes/agent/personas, .goodvibes/agents, ~/.goodvibes/personas, ~/.goodvibes/agent/personas, ~/.goodvibes/agents',
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
return [
|
|
177
|
+
`Discovered Agent persona files (${personas.length})`,
|
|
178
|
+
...personas.map(summarizeDiscoveredPersona),
|
|
179
|
+
'',
|
|
180
|
+
'Import one with: goodvibes-agent personas import-discovered <name> --yes',
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function discoveredPersonaLookupValues(persona: DiscoveredPersonaRecord): readonly string[] {
|
|
185
|
+
const slug = persona.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
186
|
+
const basename = persona.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
187
|
+
return [persona.name, slug, persona.path, basename]
|
|
188
|
+
.map((value) => value.trim().toLowerCase())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function findDiscoveredPersona(personas: readonly DiscoveredPersonaRecord[], idOrName: string): DiscoveredPersonaRecord | null {
|
|
193
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
194
|
+
if (!lookup) return null;
|
|
195
|
+
return personas.find((persona) => discoveredPersonaLookupValues(persona).includes(lookup)) ?? null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function discoveredPersonaFrontmatterList(persona: DiscoveredPersonaRecord, key: string): readonly string[] {
|
|
199
|
+
const value = persona.frontmatter[key];
|
|
200
|
+
if (!value) return [];
|
|
201
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
|
|
151
204
|
function summarizeSkill(skill: AgentSkillRecord): string {
|
|
152
205
|
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
153
206
|
const tags = skill.tags.length > 0 ? ` tags=${skill.tags.join(',')}` : '';
|
|
@@ -174,6 +227,52 @@ function renderSkillList(title: string, path: string, skills: readonly AgentSkil
|
|
|
174
227
|
].join('\n');
|
|
175
228
|
}
|
|
176
229
|
|
|
230
|
+
function summarizeDiscoveredSkill(skill: SkillRecord): string {
|
|
231
|
+
const description = skill.description ? ` - ${skill.description}` : '';
|
|
232
|
+
const dependencies = skill.dependencies.length > 0 ? ` deps=${skill.dependencies.join(',')}` : '';
|
|
233
|
+
const includes = skill.includes.length > 0 ? ` includes=${skill.includes.join(',')}` : '';
|
|
234
|
+
return [
|
|
235
|
+
` ${skill.name} ${skill.origin}${description}${dependencies}${includes}`,
|
|
236
|
+
` path: ${skill.path}`,
|
|
237
|
+
].join('\n');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderDiscoveredSkillList(skills: readonly SkillRecord[]): string {
|
|
241
|
+
if (skills.length === 0) {
|
|
242
|
+
return [
|
|
243
|
+
'Discovered Agent skill files',
|
|
244
|
+
' No SKILL.md or .md skill files found in Agent skill folders.',
|
|
245
|
+
' Search roots: .goodvibes/skills, .goodvibes/agent/skills, ~/.goodvibes/skills, ~/.goodvibes/agent/skills',
|
|
246
|
+
].join('\n');
|
|
247
|
+
}
|
|
248
|
+
return [
|
|
249
|
+
`Discovered Agent skill files (${skills.length})`,
|
|
250
|
+
...skills.map(summarizeDiscoveredSkill),
|
|
251
|
+
'',
|
|
252
|
+
'Import one with: goodvibes-agent skills import-discovered <name> --yes',
|
|
253
|
+
].join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function discoveredSkillLookupValues(skill: SkillRecord): readonly string[] {
|
|
257
|
+
const slug = skill.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
258
|
+
const basename = skill.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
259
|
+
return [skill.name, slug, skill.path, basename]
|
|
260
|
+
.map((value) => value.trim().toLowerCase())
|
|
261
|
+
.filter(Boolean);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findDiscoveredSkill(skills: readonly SkillRecord[], idOrName: string): SkillRecord | null {
|
|
265
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
266
|
+
if (!lookup) return null;
|
|
267
|
+
return skills.find((skill) => discoveredSkillLookupValues(skill).includes(lookup)) ?? null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function discoveredFrontmatterList(skill: SkillRecord, key: string): readonly string[] {
|
|
271
|
+
const value = skill.frontmatter[key];
|
|
272
|
+
if (!value) return [];
|
|
273
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
274
|
+
}
|
|
275
|
+
|
|
177
276
|
function renderBundleList(title: string, path: string, bundles: readonly AgentSkillBundleRecord[]): string {
|
|
178
277
|
if (bundles.length === 0) {
|
|
179
278
|
return [
|
|
@@ -227,11 +326,11 @@ function renderBundle(bundle: AgentSkillBundleRecord): string {
|
|
|
227
326
|
}
|
|
228
327
|
|
|
229
328
|
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]';
|
|
329
|
+
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]';
|
|
231
330
|
}
|
|
232
331
|
|
|
233
332
|
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 ...]';
|
|
333
|
+
return 'Usage: goodvibes-agent skills [list|enabled|active|discover|import-discovered <name> --yes|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes|bundle ...]';
|
|
235
334
|
}
|
|
236
335
|
|
|
237
336
|
function usageBundles(): string {
|
|
@@ -258,6 +357,41 @@ export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise
|
|
|
258
357
|
if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
|
|
259
358
|
return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
|
|
260
359
|
}
|
|
360
|
+
if (normalized === 'discover') {
|
|
361
|
+
const discovered = await discoverPersonas(shellPaths(runtime));
|
|
362
|
+
return success(runtime, 'agent.personas.discover', { personas: discovered }, renderDiscoveredPersonaList(discovered));
|
|
363
|
+
}
|
|
364
|
+
if (normalized === 'import-discovered' || normalized === 'import-persona') {
|
|
365
|
+
const options = parseOptions(rest);
|
|
366
|
+
const name = options.positionals.join(' ').trim();
|
|
367
|
+
if (!name) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas import-discovered <name> [--use] --yes', 2);
|
|
368
|
+
const discovered = findDiscoveredPersona(await discoverPersonas(shellPaths(runtime)), name);
|
|
369
|
+
if (!discovered) {
|
|
370
|
+
return failure(runtime, 'persona_discovery_not_found', `Unknown discovered Agent persona: ${name}\nRun goodvibes-agent personas discover to inspect available persona files.`, 1);
|
|
371
|
+
}
|
|
372
|
+
if (!hasFlag(options, 'yes')) {
|
|
373
|
+
return success(runtime, 'agent.personas.import_discovered.preview', { persona: discovered }, [
|
|
374
|
+
'Agent persona import preview',
|
|
375
|
+
` name: ${discovered.name}`,
|
|
376
|
+
` origin: ${discovered.origin}`,
|
|
377
|
+
` path: ${discovered.path}`,
|
|
378
|
+
` description: ${discovered.description || '(none)'}`,
|
|
379
|
+
` body characters: ${discovered.body.length}`,
|
|
380
|
+
' next: rerun with --yes to import into the Agent-local persona registry',
|
|
381
|
+
].join('\n'));
|
|
382
|
+
}
|
|
383
|
+
const persona = registry.create({
|
|
384
|
+
name: discovered.name,
|
|
385
|
+
description: discovered.description || `Imported persona from ${discovered.origin} markdown file.`,
|
|
386
|
+
body: discovered.body,
|
|
387
|
+
tags: discoveredPersonaFrontmatterList(discovered, 'tags'),
|
|
388
|
+
triggers: discoveredPersonaFrontmatterList(discovered, 'triggers'),
|
|
389
|
+
source: 'imported',
|
|
390
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
391
|
+
});
|
|
392
|
+
if (hasFlag(options, 'use')) registry.setActive(persona.id);
|
|
393
|
+
return success(runtime, 'agent.personas.import_discovered', persona, `Imported Agent persona ${persona.id}: ${persona.name}${hasFlag(options, 'use') ? ' (active)' : ''}`);
|
|
394
|
+
}
|
|
261
395
|
if (normalized === 'search' || normalized === 'find') {
|
|
262
396
|
const query = rest.join(' ').trim();
|
|
263
397
|
const results = registry.search(query);
|
|
@@ -453,6 +587,41 @@ export async function handleSkillsCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
453
587
|
snapshot.enabledBundles.length > 0 ? renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles) : '',
|
|
454
588
|
].filter(Boolean).join('\n\n'));
|
|
455
589
|
}
|
|
590
|
+
if (normalized === 'discover') {
|
|
591
|
+
const discovered = await discoverSkills(shellPaths(runtime));
|
|
592
|
+
return success(runtime, 'agent.skills.discover', { skills: discovered }, renderDiscoveredSkillList(discovered));
|
|
593
|
+
}
|
|
594
|
+
if (normalized === 'import-discovered' || normalized === 'import-skill') {
|
|
595
|
+
const options = parseOptions(rest);
|
|
596
|
+
const name = options.positionals.join(' ').trim();
|
|
597
|
+
if (!name) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills import-discovered <name> [--enabled] --yes', 2);
|
|
598
|
+
const discovered = findDiscoveredSkill(await discoverSkills(shellPaths(runtime)), name);
|
|
599
|
+
if (!discovered) {
|
|
600
|
+
return failure(runtime, 'skill_discovery_not_found', `Unknown discovered Agent skill: ${name}\nRun goodvibes-agent skills discover to inspect available skill files.`, 1);
|
|
601
|
+
}
|
|
602
|
+
if (!hasFlag(options, 'yes')) {
|
|
603
|
+
return success(runtime, 'agent.skills.import_discovered.preview', { skill: discovered }, [
|
|
604
|
+
'Agent skill import preview',
|
|
605
|
+
` name: ${discovered.name}`,
|
|
606
|
+
` origin: ${discovered.origin}`,
|
|
607
|
+
` path: ${discovered.path}`,
|
|
608
|
+
` description: ${discovered.description || '(none)'}`,
|
|
609
|
+
` procedure characters: ${discovered.body.length}`,
|
|
610
|
+
' next: rerun with --yes to import into the Agent-local skill registry',
|
|
611
|
+
].join('\n'));
|
|
612
|
+
}
|
|
613
|
+
const skill = registry.create({
|
|
614
|
+
name: discovered.name,
|
|
615
|
+
description: discovered.description || `Imported skill from ${discovered.origin} skill file.`,
|
|
616
|
+
procedure: discovered.body,
|
|
617
|
+
tags: discoveredFrontmatterList(discovered, 'tags'),
|
|
618
|
+
triggers: discoveredFrontmatterList(discovered, 'triggers'),
|
|
619
|
+
enabled: hasFlag(options, 'enabled'),
|
|
620
|
+
source: 'imported',
|
|
621
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
622
|
+
});
|
|
623
|
+
return success(runtime, 'agent.skills.import_discovered', skill, `Imported Agent skill ${skill.id}: ${skill.name}${skill.enabled ? ' (enabled)' : ''}`);
|
|
624
|
+
}
|
|
456
625
|
if (normalized === 'search' || normalized === 'find') {
|
|
457
626
|
const query = rest.join(' ').trim();
|
|
458
627
|
const results = registry.search(query);
|