@pellux/goodvibes-agent 0.1.113 → 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 +6 -0
- package/dist/package/main.js +736 -50
- 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 +9 -1
- package/src/cli/local-library-command.ts +81 -1
- package/src/cli/routines-command.ts +156 -1
- 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/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',
|
|
@@ -243,15 +247,19 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
243
247
|
usage: [
|
|
244
248
|
'routines list',
|
|
245
249
|
'routines enabled',
|
|
250
|
+
'routines discover',
|
|
251
|
+
'routines import-discovered <name> [--enabled] --yes',
|
|
246
252
|
'routines show <id>',
|
|
247
253
|
'routines receipts',
|
|
248
254
|
'routines reconcile',
|
|
249
255
|
'routines receipt <receipt-id>',
|
|
250
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',
|
|
251
257
|
],
|
|
252
|
-
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.',
|
|
253
259
|
examples: [
|
|
254
260
|
'routines list',
|
|
261
|
+
'routines discover',
|
|
262
|
+
'routines import-discovered "Daily Brief" --enabled --yes',
|
|
255
263
|
'routines show daily-operations-sweep',
|
|
256
264
|
'routines receipts',
|
|
257
265
|
'routines reconcile',
|
|
@@ -1,4 +1,5 @@
|
|
|
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';
|
|
3
4
|
import { discoverSkills, type SkillRecord } from '../agent/skill-discovery.ts';
|
|
4
5
|
import {
|
|
@@ -156,6 +157,50 @@ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | nu
|
|
|
156
157
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
157
158
|
}
|
|
158
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
|
+
|
|
159
204
|
function summarizeSkill(skill: AgentSkillRecord): string {
|
|
160
205
|
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
161
206
|
const tags = skill.tags.length > 0 ? ` tags=${skill.tags.join(',')}` : '';
|
|
@@ -281,7 +326,7 @@ function renderBundle(bundle: AgentSkillBundleRecord): string {
|
|
|
281
326
|
}
|
|
282
327
|
|
|
283
328
|
function usagePersonas(): string {
|
|
284
|
-
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]';
|
|
285
330
|
}
|
|
286
331
|
|
|
287
332
|
function usageSkills(): string {
|
|
@@ -312,6 +357,41 @@ export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise
|
|
|
312
357
|
if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
|
|
313
358
|
return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
|
|
314
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
|
+
}
|
|
315
395
|
if (normalized === 'search' || normalized === 'find') {
|
|
316
396
|
const query = rest.join(' ').trim();
|
|
317
397
|
const results = registry.search(query);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createShellPathService } from '@/runtime/index.ts';
|
|
2
|
+
import { discoverRoutines, type DiscoveredRoutineRecord } from '../agent/routine-discovery.ts';
|
|
2
3
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
3
4
|
import {
|
|
4
5
|
buildRoutineSchedulePreview,
|
|
@@ -44,6 +45,13 @@ function routineRegistry(runtime: CliCommandRuntime): AgentRoutineRegistry {
|
|
|
44
45
|
}));
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function shellPaths(runtime: CliCommandRuntime): ReturnType<typeof createShellPathService> {
|
|
49
|
+
return createShellPathService({
|
|
50
|
+
workingDirectory: runtime.workingDirectory,
|
|
51
|
+
homeDirectory: runtime.homeDirectory,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
function routineReceiptStore(runtime: CliCommandRuntime): RoutineScheduleReceiptStore {
|
|
48
56
|
return RoutineScheduleReceiptStore.fromShellPaths(createShellPathService({
|
|
49
57
|
workingDirectory: runtime.workingDirectory,
|
|
@@ -51,12 +59,83 @@ function routineReceiptStore(runtime: CliCommandRuntime): RoutineScheduleReceipt
|
|
|
51
59
|
}));
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function splitList(value: string | undefined): readonly string[] {
|
|
63
|
+
if (!value) return [];
|
|
64
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseImportFlags(args: readonly string[]): {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly enabled: boolean;
|
|
70
|
+
readonly yes: boolean;
|
|
71
|
+
} {
|
|
72
|
+
const positionals: string[] = [];
|
|
73
|
+
let enabled = false;
|
|
74
|
+
let yes = false;
|
|
75
|
+
for (const arg of args) {
|
|
76
|
+
if (arg === '--enabled') {
|
|
77
|
+
enabled = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === '--yes') {
|
|
81
|
+
yes = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
positionals.push(arg);
|
|
85
|
+
}
|
|
86
|
+
return { name: positionals.join(' ').trim(), enabled, yes };
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
function summarizeRoutine(routine: AgentRoutineRecord): string {
|
|
55
90
|
const enabled = routine.enabled ? 'enabled' : 'disabled';
|
|
56
91
|
const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
|
|
57
92
|
return ` ${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}${tags}`;
|
|
58
93
|
}
|
|
59
94
|
|
|
95
|
+
function summarizeDiscoveredRoutine(routine: DiscoveredRoutineRecord): string {
|
|
96
|
+
const description = routine.description ? ` - ${routine.description}` : '';
|
|
97
|
+
return [
|
|
98
|
+
` ${routine.name} ${routine.origin}${description}`,
|
|
99
|
+
` path: ${routine.path}`,
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderDiscoveredRoutineList(routines: readonly DiscoveredRoutineRecord[]): string {
|
|
104
|
+
if (routines.length === 0) {
|
|
105
|
+
return [
|
|
106
|
+
'Discovered Agent routine files',
|
|
107
|
+
' No routine markdown files found in Agent routine folders.',
|
|
108
|
+
' Search roots: .goodvibes/routines, .goodvibes/agent/routines, ~/.goodvibes/routines, ~/.goodvibes/agent/routines',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
`Discovered Agent routine files (${routines.length})`,
|
|
113
|
+
...routines.map(summarizeDiscoveredRoutine),
|
|
114
|
+
'',
|
|
115
|
+
'Import one with: goodvibes-agent routines import-discovered <name> --yes',
|
|
116
|
+
].join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function discoveredRoutineLookupValues(routine: DiscoveredRoutineRecord): readonly string[] {
|
|
120
|
+
const slug = routine.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
121
|
+
const basename = routine.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
122
|
+
return [routine.name, slug, routine.path, basename]
|
|
123
|
+
.map((value) => value.trim().toLowerCase())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findDiscoveredRoutine(routines: readonly DiscoveredRoutineRecord[], idOrName: string): DiscoveredRoutineRecord | null {
|
|
128
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
129
|
+
if (!lookup) return null;
|
|
130
|
+
return routines.find((routine) => discoveredRoutineLookupValues(routine).includes(lookup)) ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function discoveredRoutineFrontmatterList(routine: DiscoveredRoutineRecord, key: string): readonly string[] {
|
|
134
|
+
const value = routine.frontmatter[key];
|
|
135
|
+
if (!value) return [];
|
|
136
|
+
return splitList(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
60
139
|
function renderRoutineList(title: string, path: string, routines: readonly AgentRoutineRecord[]): string {
|
|
61
140
|
if (routines.length === 0) {
|
|
62
141
|
return [
|
|
@@ -176,6 +255,82 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
176
255
|
exitCode: 0,
|
|
177
256
|
};
|
|
178
257
|
}
|
|
258
|
+
if (normalized === 'discover') {
|
|
259
|
+
const discovered = await discoverRoutines(shellPaths(runtime));
|
|
260
|
+
const value: RoutinesCommandSuccess<{ readonly routines: readonly DiscoveredRoutineRecord[] }> = {
|
|
261
|
+
ok: true,
|
|
262
|
+
kind: 'agent.routines.discover',
|
|
263
|
+
data: { routines: discovered },
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
output: jsonOrText(runtime, value, renderDiscoveredRoutineList(discovered)),
|
|
267
|
+
exitCode: 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (normalized === 'import-discovered' || normalized === 'import-routine') {
|
|
271
|
+
const parsed = parseImportFlags(rest);
|
|
272
|
+
if (!parsed.name) {
|
|
273
|
+
const failure: RoutinesCommandFailure = {
|
|
274
|
+
ok: false,
|
|
275
|
+
kind: 'invalid_routine_command',
|
|
276
|
+
error: 'Usage: goodvibes-agent routines import-discovered <name> [--enabled] --yes',
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
|
|
280
|
+
exitCode: 2,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const discovered = findDiscoveredRoutine(await discoverRoutines(shellPaths(runtime)), parsed.name);
|
|
284
|
+
if (!discovered) {
|
|
285
|
+
const failure: RoutinesCommandFailure = {
|
|
286
|
+
ok: false,
|
|
287
|
+
kind: 'routine_discovery_not_found',
|
|
288
|
+
error: `Unknown discovered Agent routine: ${parsed.name}\nRun goodvibes-agent routines discover to inspect available routine files.`,
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
|
|
292
|
+
exitCode: 1,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (!parsed.yes) {
|
|
296
|
+
const value: RoutinesCommandSuccess<{ readonly routine: DiscoveredRoutineRecord }> = {
|
|
297
|
+
ok: true,
|
|
298
|
+
kind: 'agent.routines.import_discovered.preview',
|
|
299
|
+
data: { routine: discovered },
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
output: jsonOrText(runtime, value, [
|
|
303
|
+
'Agent routine import preview',
|
|
304
|
+
` name: ${discovered.name}`,
|
|
305
|
+
` origin: ${discovered.origin}`,
|
|
306
|
+
` path: ${discovered.path}`,
|
|
307
|
+
` description: ${discovered.description || '(none)'}`,
|
|
308
|
+
` steps characters: ${discovered.steps.length}`,
|
|
309
|
+
' next: rerun with --yes to import into the Agent-local routine registry',
|
|
310
|
+
].join('\n')),
|
|
311
|
+
exitCode: 0,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const routine = registry.create({
|
|
315
|
+
name: discovered.name,
|
|
316
|
+
description: discovered.description || `Imported routine from ${discovered.origin} markdown file.`,
|
|
317
|
+
steps: discovered.steps,
|
|
318
|
+
tags: discoveredRoutineFrontmatterList(discovered, 'tags'),
|
|
319
|
+
triggers: discoveredRoutineFrontmatterList(discovered, 'triggers'),
|
|
320
|
+
enabled: parsed.enabled,
|
|
321
|
+
source: 'imported',
|
|
322
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
323
|
+
});
|
|
324
|
+
const value: RoutinesCommandSuccess<AgentRoutineRecord> = {
|
|
325
|
+
ok: true,
|
|
326
|
+
kind: 'agent.routines.import_discovered',
|
|
327
|
+
data: routine,
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
output: jsonOrText(runtime, value, `Imported Agent routine ${routine.id}: ${routine.name}${routine.enabled ? ' (enabled)' : ''}`),
|
|
331
|
+
exitCode: 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
179
334
|
if (normalized === 'show') {
|
|
180
335
|
const id = rest[0];
|
|
181
336
|
if (!id) return { output: 'Usage: goodvibes-agent routines show <id>', exitCode: 2 };
|
|
@@ -241,7 +396,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
241
396
|
return handleRoutinePromotion(runtime, rest);
|
|
242
397
|
}
|
|
243
398
|
return {
|
|
244
|
-
output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-channel <channel>|--delivery-route <route>|--delivery-webhook <url>] --yes]',
|
|
399
|
+
output: 'Usage: goodvibes-agent routines [list|enabled|discover|import-discovered <name> --yes|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-channel <channel>|--delivery-route <route>|--delivery-webhook <url>] --yes]',
|
|
245
400
|
exitCode: 2,
|
|
246
401
|
};
|
|
247
402
|
}
|